VUE3+DRF 网页天气卡片组件实现
简介
这次要实现的是一个普通的网页卡片组件,用于显示访问ip所在城市的基本天气信息,用处不大,我的初心就是给我的网站首页充充门面的。由于本站前端是基于VUE3,那么本例也是如此。后端部分涉及到的第三方比较多,但还是基于DRF的
接口实现流程
- 首先需要再和风天气官网注册账户,创建你的项目并拿到你专属的key,没关系,我们可以用免费订阅
- 收集你前端要用到的各种天气图标图片资源,我是在和风天气官网中简单的找了一些用于我的项目。
- 在settings.py中配置你的url,方便后续在接口中直接调用(当然也可以不在settings.py中配置,直接写在view.py中一样的,个人习惯问题)
# 和风天气KEY
WEATHER_KEY = '你的key'
# 携带经纬度参数,获取实时天气
WEATHER_NOW_URL = 'https://devapi.qweather.com/v7/weather/now?&key='+WEATHER_KEY
# 携带经纬度参数,获取城市信息
WEATHER_GEO_URL = 'https://geoapi.qweather.com/v2/city/lookup?&key='+WEATHER_KEY
- 创建你的DRF天气获取接口,并在代码中获取请求的IP
# 别忘了导包
import requests
import json
import gzip
import urllib.request # 请求接口
import urllib.parse # 返回体解析
from loguru import logger # 日志用
from rest_framework import generics, viewsets, filters, status
"""
和风天气请求
"""
class WeatherView(generics.GenericAPIView):
def get(self, request, *args, **kwargs):
"""
通过请求IP获取经纬度, 再通过和风天气接口获取城市和天气信息
"""
source_ip = request.META.get('REMOTE_ADDR') # DRF中可以通过这中方式获取请求IP
logger.debug('访问者IP:'+source_ip)
- 获取请求IP的经纬度信息(如果是本机localhost地址,那就通过设定的默认经纬度【杭州】发送天气请求)
coordinate = '120.2052639863281,30.231124817451985' # 这个是杭州经纬度,默认设置
if source_ip != '127.0.0.1':
# 当不是本机发送的请求(开发过程),通过ip-api网站接口携带前端请求ID获取经纬度信息
response = json.loads(requests.get('http://ip-api.com/json/'+source_ip).text)
coordinate = str(response['lon']) + ',' + str(response['lat'])
# 携带经纬度信息请求和风天气的GEO接口
# 这个接口返回的是城市信息
geo_url = settings.WEATHER_GEO_URL + '&' + urllib.parse.urlencode({'location': coordinate})
geo_requests = urllib.request.Request(geo_url)
- 请求天气信息
weather_url = settings.WEATHER_NOW_URL + '&' + urllib.parse.urlencode({'location': coordinate})
weather_requests = urllib.request.Request(weather_url)
- 读取天气和城市请求的返回体信息
try:
with urllib.request.urlopen(geo_requests) as response:
# 检查响应是否是gzip压缩的
if response.headers.get('Content-Encoding') == 'gzip':
# 使用gzip解压响应内容
compressed_data = response.read()
with gzip.GzipFile(fileobj=io.BytesIO(compressed_data)) as gzip_file:
decompressed_data = gzip_file.read()
GEO_RES = decompressed_data.decode('utf-8') # 解码为utf-8字符串
else:
# 如果没有gzip压缩,直接读取并解码
GEO_RES = response.read().decode('utf-8')
logger.debug('城市信息:'+GEO_RES)
with urllib.request.urlopen(weather_requests) as response:
# 检查响应是否是gzip压缩的
if response.headers.get('Content-Encoding') == 'gzip':
# 使用gzip解压响应内容
compressed_data = response.read()
with gzip.GzipFile(fileobj=io.BytesIO(compressed_data)) as gzip_file:
decompressed_data = gzip_file.read()
WEATHER_RES = decompressed_data.decode('utf-8') # 解码为utf-8字符串
else:
# 如果没有gzip压缩,直接读取并解码
WEATHER_RES = response.read().decode('utf-8')
logger.debug('天气信息:'+WEATHER_RES)
except urllib.error.HTTPError as e:
logger.error(f"HTTP Error: {e.code} {e.reason}")
except urllib.error.URLError as e:
logger.error(f"URL Error: {e.reason}")
except Exception as e:
logger.error(f"An error occurred: {e}")
8.组合返回数据
return Response(data={'city':json.loads(GEO_RES), 'weather':json.loads(WEATHER_RES)}, status=status.HTTP_200_OK)
看这里
将步骤4-8代码按顺序组合久是我的完整的接口代码,当然别忘记配置WEATHER_NOW_URL 和WEATHER_GEO_URL这两端URL,在哪里配置无所谓
关于请求库
之所以使用urllib请求,是因为使用常用的requests库的时候,当我本机开启VPN的时候或者服务器生产环境部署后请求地理信息会有报错,暂时没理解原理,故使用urllib代替,写法是麻烦了点,目前没遇到bug
其他获取城市经纬度的方式(题外话)
目前我们使用的是response = json.loads(requests.get('http://ip-api.com/json/'+source_ip).text)
这样一个简单的请求完成经纬度的获取,但是还是有精度问题,通过经纬度信息请求和风天气的GEO接口,返回的区县,街道仍旧有较大偏差,所以我在前端只是显示市级地址,在这之前使用了别的更复杂的GEO库,不知道是库的问题还是和风免费订阅的问题,精度离谱.
追求更高的定位精度可以再研究研究别的GEO库,目前GeoIP2已经踩坑
前端卡片设计
由于我的组件没有和我的项目充分解耦,我先将完整代码放出来,部分引用手动替换一下,原本都是vuex管理的行为和状态我都重写到本组件中,再补充一下几个调用的工具方法,大伙将就着看吧
主要代码
<script setup>
import { createFromIconfontCN } from "@ant-design/icons-vue";
import { ref, onMounted, computed, watch} from "vue";
import common from "@/utils/common";
import weatherUtil from "../utils/weatherIconUtil";
const weather_data = ref({})
const weather_icon = ref("");
const weather_background_url = "你的天气图片背景地址,可以这里引入也可以直接在css中写死,本站点的图片是后台配置的,不具备参考价值"
// 这是ANTD下的iconFont组件,对应官网iconfont.cn,布局中我用到几个iconFont图标,需要去官网找到图标并生成自己的链接
// 需要先安装antd相关组件,或者用别的形式下载替换图标
const IconFont = createFromIconfontCN({scriptUrl: api.iconfont,});
// 检测到数据变化时(包括首次启动),根据自己的工具方法(后续将摘录),更改天气图标
watch(weather_data, (newValue) => {
console.log("weather_data:", newValue);
weather_icon.value = weatherUtil.getWeatherIcon(
weather_data.value.weather?.now?.icon
);
});
onMounted(() => {
// 请求天气信息
// 这里的api.weather就是你的后端天气接口地址
axios.get(api.weather).then(res => {
if (res.status == 200) {
weather_data.value = res.data
} else {
console.log("weather请求失败");
}
}).catch(err => {
console.log('err:', err);
})
});
</script>
<template>
<div
class="weather-card-content"
:style="{ backgroundImage: `url(${weather_background_url})` }"
>
<div class="weather-icon-box">
<img
:src="weatherUtil.getWeatherIcon(weather_data.weather.now.icon)"
width="200px"
/>
<div class="weather-line">
<div class="temp-item">
<img
src="/weatherIcons/design/fill/animation-ready/thermometer.svg"
alt=""
/>
<span>{{ weather_data?.weather?.now?.temp + "℃" }}</span>
</div>
<div class="weather-item">
<span>{{ weather_data?.weather?.now?.text }}</span>
</div>
</div>
<div class="location-box">
<p>
<IconFont type="icon-weizhi"></IconFont>
<span
>{{
weather_data?.city?.location[0]?.adm1 +
"·" +
weather_data?.city?.location[0]?.adm2
}}
</span>
</p>
<P>
<IconFont type="icon-gengxinshijian"></IconFont>
<span
>{{ common.formatDataTime(weather_data?.weather?.updateTime) }}
</span>
</P>
</div>
</div>
</div>
</template>
<style scoped lang='scss'>
@import "@/styles/base.scss";
p {
line-height: 1.7;
}
.weather-card-content {
border-radius: 8px;
margin: 0.8rem 0;
padding: 1rem 1.2rem;
min-height: 300px;
font-family: "阿里妈妈方圆体 VF Regular";
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: relative;
background-size: cover;
background-position: 50%;
user-select: none;
.weather-icon-box {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
img {
pointer-events: none;
}
.weather-line {
width: 100%;
top: -1rem;
display: flex;
align-items: center;
justify-content: space-between;
span {
color: #fff;
font-weight: 700;
font-size: 2.5rem;
}
.temp-item {
display: flex;
align-items: center;
width: 45%;
justify-content: flex-start;
left: -2.5rem;
img {
height: 100%;
}
span {
color: #fff;
font-weight: 700;
font-size: 2.5rem;
left: -2rem;
}
}
.weather-item {
width: 45%;
text-align: right;
span {
font-size: 1.8rem;
}
}
}
.location-box {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100px;
span {
color: #fff;
font-weight: 700;
font-size: 1.5rem;
}
}
}
}
.weather-card-content::before {
content: "";
border-radius: 8px;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(46, 44, 44, 0.5); /* 背景颜色需要有一定的透明度 */
backdrop-filter: blur(3px); /* 添加模糊效果 */
}
</style>
weatherUtil.getWeatherIcon
const iconChangeData = {
100: 'clear-day',
101: 'overcast-day',
102: 'cloudy',
103: 'partly-cloudy-day',
104: 'overcast',
150: 'clear-day',
151: 'overcast-night',
152: 'cloudy',
153: 'partly-cloudy-day',
300: 'rain',
301: 'rain',
302: 'thunderstorms-day-rain',
303: 'thunderstorms-rain',
304: 'thunderstorms-rain',
305: 'drizzle',
306: 'rain',
307: 'rain',
308: 'rain',
309: 'drizzle',
310: 'rain',
311: 'rain',
312: 'rain',
313: 'rain',
314: 'rain',
315: 'rain',
316: 'rain',
317: 'rain',
318: 'rain',
350: 'rain',
351: 'rain',
352: 'thunderstorms-day-rain',
399: 'rain',
400: 'snow',
401: 'snow',
402: 'snow',
403: 'snow',
404: 'sleet',
405: 'sleet',
406: 'sleet',
407: 'snow',
408: 'snow',
409: 'snow',
410: 'snow',
456: 'sleet',
457: 'snow',
499: 'snow',
500: 'mist',
501: 'fog',
502: 'haze',
503: 'dust-wind',
504: 'dust-wind',
507: 'dust-wind',
508: 'dust-wind',
509: 'mist',
510: 'mist',
511: 'mist',
512: 'mist',
513: 'mist',
514: 'mist',
515: 'mist',
900: 'pressure-high-alt',
901: 'pressure-low-alt',
999: 'overcast-day',
}
const iconPath = '/weatherIcons/design/fill/animation-ready/'
export default {
getWeatherIcon: (iconId) => {
console.log('iconId', iconId);
let iconUrl
if (iconChangeData.hasOwnProperty(iconId)) {
iconUrl = iconPath + iconChangeData[iconId] + '.svg'
} else {
iconUrl = iconPath + 'overcast-day.svg'
}
return iconUrl
}
}
common.formatDataTime
formatDataTime(sourceData) {
return new Date(sourceData).toLocaleString()
},
关于素材地址
素材地址,整理后在本站资源模块,网站素材中可下载, 素材文件夹weatherIcons我是放置在public下,防止编译后的资源请求出错。
用到的第三方地址
和风天气官网:https://id.qweather.com/
IconFont:https://www.iconfont.cn/
MAXMIND地址:https://www.maxmind.com/en/accounts/982915/geoip/downloads