显示效果X3:
微信小程序本身(2023.01)并未提供天气预报功能,但允许接入高德(或其他)的天气预报。
接入高德坏处是能获取的天气信息比较有限,只能提供今日实时天气和未来3日天气预报。
每日可调用次数3,000,000次,对小项目来说完全够用。
实际上,高德提供了至少2种可接入方式,1、插件,2、网络接口。而插件本质也是调用网络接口(呵呵),这里使用的是插件。(高德插件文档)
接入准备
1、在高德控制台创建一个应用,然后添加key
2、在微信小程序(官网)后台,开发管理->开发设置->服务器域名->request合法域名里
添加白名单'https://restapi.amap.com;'
代码
weather.js
const app = getApp()
const amap = require('../../../utils/amap-wx.130')
const util = require('../../../utils/util')
Page({
data: {
// 天气
weather: {},
casts:[],
myAmap: {}, // 高德-天气预报工具
},
onLoad(options) {
this.data.myAmap = new amap.AMapWX({key:app.globalData.amap_key}); // 你在高德注册的key
},
onShow() {
let city = '北京市'
this.requestWeather(city)
},
/*
绘图思路:先定x,y轴,分别在左边和底部留出空隙(x_left & y_bottom);右边也留点,顶部也留点(y_top)
x轴绘制:从y_top画到y_data_bottom;这一段是【实线】,所有温度处于其间。上下顶点分别是最高温和最低温值。
【虚线】部分从y_data_bottom到y_bottom;因为最小温度不定,所以无法确定x,y轴交点温度是多少,所以留一段虚线;这样实线底端可表示任意温度值。
数据绘制:天气数据的(x,y),x好确定,把x轴长度分均分成casts.length分就行了(实际要略短,因为要在右边留一点距离)
y值稍麻烦。casts最低温度为tp_min,最高温度为tp_max,温差tp_diff = tp_max - tp_min,
y轴实线底端表示的温度即tp_min,y轴实线总长表示温差tp_diff。
根据这个关系,对任意温度tp_d,有 let len = (tp_d - tp_min)*400/tp_diff,len换算成单位px得到len_px,
从y轴实线底端y_data_bottom往上回退len_px,就得到了当前温度值的y坐标
*/
drawCanvas(canvas) {
let casts = this.data.casts
if(casts.length < 3) {
return
}
let _this = this
const query = wx.createSelectorQuery()
query.select('#myCanvas2')
.fields({ node: true, size: true })
.exec((res) => {
// 初始化
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
// --------------- x,y坐标轴 ---------------
let x_left = util.width_of_px_from_rpx(50)
let x_right = util.width_of_px_from_rpx(700)
let y_top = util.width_of_px_from_rpx(60)
let y_bottom = util.width_of_px_from_rpx(530)
let y_data_len = util.width_of_px_from_rpx(400) // 数据区域高度
let y_data_bottom = y_top + y_data_len
let dash_gap = util.width_of_px_from_rpx(10)
let axis_w = util.width_of_px_from_rpx(3)
ctx.beginPath()
ctx.strokeStyle = '#C1CDC1'
ctx.lineWidth = axis_w
ctx.moveTo(x_left, y_top)
ctx.lineTo(x_left, y_data_bottom)
// --------------- dash line ---------------
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = '#C1CDCD'
for(let i = 0; true; i++) {
if(i%2 == 0) {
ctx.moveTo(x_left, y_data_bottom + dash_gap*i)
ctx.lineTo(x_left, y_data_bottom + dash_gap*(i+1))
}
if(y_data_bottom + dash_gap*(i+1) > y_bottom) {
break
}
}
ctx.stroke()
ctx.beginPath()
ctx.strokeStyle = '#C1CDC1'
ctx.moveTo(x_left, y_bottom)
ctx.lineTo(x_right, y_bottom)
ctx.stroke()
// --------------- 数据 ---------------
// let y_gap = util.width_of_px_from_rpx(80) // y_data_len = 400rpx。分成4分
let tp_min = 100 // 最低温度,初始值一定要比casts任何温度高
let tp_max = -100 // 最高温度,初始值一定要比casts任何温度低
for(let i = 0; i < casts.length; i++) {
let item = casts[i]
let tp_d = Number(item.daytemp)
let tp_n = Number(item.nighttemp)
if(tp_d < tp_min) {tp_min = tp_d}
if(tp_n < tp_min) {tp_min = tp_n}
if(tp_d > tp_max) {tp_max = tp_d}
if(tp_n > tp_max) {tp_max = tp_n}
}
let tp_diff = tp_max - tp_min // 温差
if(tp_diff == 0){tp_diff = 1} // 理论上分母 tp_diff 不能为0;实际也可不处理,因为不可能4天温差为0。
let fontPX = Math.floor(util.width_of_px_from_rpx(22))
ctx.font = 'normal ' + fontPX + 'px 微软雅黑'
// 白天 曲线
_this.drawBrokenLine(0, ctx, casts, tp_min, tp_diff, x_left, y_bottom, y_data_bottom)
// 黑夜 曲线
_this.drawBrokenLine(1, ctx, casts, tp_min, tp_diff, x_left, y_bottom, y_data_bottom)
})
},
// 绘制折线
/**
* @param {*} type 0-白天,1-晚上
* @param {*} ctx canvas context
* @param {*} casts 天气预报信息(今日+3日),数组,length = 4。
* @param {*} tp_min casts 内最低温度(白天、黑夜最低)
* @param {*} tp_diff 温差:casts 中最高温度 - 最低温度
* @param {*} x_left x轴起点 在 canvas 的实际坐标 单位px
* @param {*} y_bottom y轴最低点 在 canvas 的实际坐标 单位px
* @param {*} y_data_bottom 实际数据最低值(虚线以上部分) 在 canvas 的实际坐标 单位px
*/
drawBrokenLine(type, ctx, casts, tp_min, tp_diff, x_left, y_bottom, y_data_bottom) {
ctx.beginPath()
type == 0 ? ctx.strokeStyle = '#FF8C00' : ctx.strokeStyle = '#8B7D6B'
let color = type == 0 ? '#8A2BE2' : '#4169E1'
let txt_offset_y = util.width_of_px_from_rpx(30)
let day_offset_y = txt_offset_y + util.width_of_px_from_rpx(30)
let date_offset_x = util.width_of_px_from_rpx(28)
let week_offset_x = util.width_of_px_from_rpx(18)
let days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
let dot_r = util.width_of_px_from_rpx(10) // 数据点的小圆的半径
let x_gap = util.width_of_px_from_rpx(200) // x轴2个数据的间距,分成5分
let txt_offset_x = util.width_of_px_from_rpx(40)
for(let i = 0; i < casts.length; i++) {
let item = casts[i]
let tp_d = type == 0 ? Number(item.daytemp) : Number(item.nighttemp)
// len:400 = (tp_d - tp_min):tp_diff
let len = (tp_d - tp_min)*400/tp_diff // 温度tp_d对应在y轴的高度
let len_c = Math.floor(len)
let len_px = util.width_of_px_from_rpx(len_c)
let x = x_left + x_gap*i
let y = y_data_bottom - len_px
if(i == 0) {
ctx.moveTo(x, y)
}else {
ctx.lineTo(x, y)
}
ctx.stroke()
}
for(let i = 0; i < casts.length; i++) {
let item = casts[i]
let tp_d = type == 0 ? Number(item.daytemp) : Number(item.nighttemp)
let len = (tp_d - tp_min)*400/tp_diff // 温度tp_d对应在y轴的高度
let len_c = Math.floor(len)
let len_px = util.width_of_px_from_rpx(len_c)
let x = x_left + x_gap*i
let y = y_data_bottom - len_px
ctx.beginPath()
// 圆点
ctx.arc(x, y, dot_r, 0, Math.PI*2)
ctx.fillStyle = color
ctx.fill()
ctx.stroke()
// 文字
let tp = tp_d + '°,' + (type == 0 ? item.dayweather : item.nightweather)
let offset_y = type == 0 ? -txt_offset_y : (txt_offset_y*1.2)
ctx.fillText(tp, x - txt_offset_x, y + offset_y)
if(type == 0) {
if(item.date && item.date.length == 10) { // 日期
let dateStr = item.date.substr(5, 5)
ctx.fillText(dateStr, x - date_offset_x, y_bottom + txt_offset_y)
}
}else {
if(item.week && item.week <= 7) { // 星期几
let weekStr = days[item.week - 1]
ctx.fillText(weekStr, x - week_offset_x, y_bottom + day_offset_y)
}
}
}
},
// -------------------------- request ---------------------------
requestWeather(city) {
if(!city) {
return
}
let _this = this
// 获取实时天气
this.data.myAmap.getWeather({
city: city,
success: function(data){
_this.setData({weather: data})
},
fail: function(info){ }
})
// 获取3日天气预报
this.data.myAmap.getWeather({
city: city,
type: 'forecast',
success: function(data){
if(data.forecast && data.forecast.casts) {
_this.setData({casts: data.forecast.casts})
_this.drawCanvas()
}
},
fail: function(info){ }
})
}
})
插件本身没相关文档,因为插件本质调用https://restapi.amap.com/v3/weather/weatherInfo,所以可以参考Web服务 API(注意,虽然是同一个接口,但插件可能对结果重新封装了,数据要以实际为准,不一定100%同这个文档)。
至于getWeather({..type: 'forecast'
..}) 方法里type
,
不传该字段就能获取实时天气,传了forecast
就是3日天气预报(+今日=4日)
文档缺失,只能东搞搞西搞搞瞎搞出来要的东西。
/**
forcast 对象说明。
date 日期
week 星期几
dayweather 白天天气现象
nightweather 晚上天气现象
daytemp 白天温度
nighttemp 晚上温度
daywind 白天风向
nightwind 晚上风向
daypower 白天风力
nightpower 晚上风力
*/
weather.wxml
<view class="container">
<block wx:if="{{weather.liveData}}">
<view class="rowh15"></view>
<view class="city flex-r-end">
<view class="columnw10"></view>
<view class="">{{weather.city.text}}:</view>
<view class="">{{weather.city.data}}</view>
</view>
<view class="rowh40"></view>
<view class="flex-r-end">
<view class="key1">天气:</view>
<view class="value1">{{weather.liveData.weather}}</view>
<view class="columnw50"></view>
<view class="key1">温度:</view>
<view class="value1">{{weather.liveData.temperature}}°</view>
</view>
<view class="rowh10"></view>
<view class="flex-r-end">
<view class="key2">风力:</view>
<view class="value2">{{weather.liveData.windpower}}</view>
<view class="columnw50"></view>
<view class="key2">风向:</view>
<view class="value2">{{weather.winddirection.data}}</view>
</view>
<view class="rowh15"></view>
<view class="flex-r-end">
<view class="key2">{{weather.humidity.text}}:</view>
<view class="value2">{{weather.humidity.data}}</view>
</view>
</block>
<view class="rowh50"></view>
<view class="forecast-title flex-center">未来3日天气预报</view>
<view class="flex-r tips">
<view class="day-line">橙色线表示-白天温度</view>
<view class="night-line">灰色线表示-夜间温度</view>
</view>
<canvas class="canvas" type="2d" id="myCanvas2" disable-scroll="true"></canvas>
</view>
weather.wxss
page {
--fs-medium: 30rpx;
--fs-tiny: 22rpx;
--theme-blue: #4272EA;
}
.container{
position: absolute;
top: 0rpx;
bottom: 0;
left: 0;
right: 0;
background: #4D8AD7;
color: #fff;
font-size: 18px;
box-sizing: border-box;
padding-left: 0rpx;
}
.city {
font-size: var(--fs-tiny);
color: #eeeeee;
}
.key1 {
/* color: var(--gray-8f); */
margin-left: 30rpx;
color: #e0e0e0;
font-size: 30rpx;
}
.value1 {
font-size: 50rpx;
color: white;
}
.key2 {
margin-left: 30rpx;
font-size: 26rpx;
color: #e0e0e0;
}
.value2 {
font-size: 26rpx;
color: #e0e0e0;
}
.forecast-title {
width: 100vmin;
height: 70rpx;
font-size: var(--fs-medium);
background-color: white;
color: var(--theme-blue);
}
.tips {
width: 100vmin;
background-color: white;
}
.day-line {
font-size: var(--fs-tiny);
color: #FF8C00;
margin-left: 20rpx;
}
.night-line {
font-size: var(--fs-tiny);
color: #8B7D6B;
margin-left: 40rpx;
}
.canvas {
width: 750rpx;
height: 600rpx;
background-color: white;
}
.rowh10 {width: 100%;height: 10rpx;}
.rowh15 {width: 100%;height: 15rpx;}
.rowh40 {width: 100%;height: 40rpx;}
.rowh50 {width: 100%;height: 50rpx;}
.columnw10 {width: 10rpx;height: 100%;}
.columnw50 {width: 50rpx;height: 100%;}
.flex-r {display: flex;align-items: flex-start;}
.flex-r-end {display: flex;align-items: flex-end;}
.flex-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
工具类../../../utils/util.js
const screenWidth = wx.getSystemInfoSync().screenWidth
// 将 rpx 单位长度换算成 px。
function width_of_px_from_rpx(value) {
let value_of_px = value/750.0*screenWidth
return value_of_px
}
module.exports = {
width_of_px_from_rpx:width_of_px_from_rpx
}