VUE3+DRF 网页天气卡片组件实现

简介

这次要实现的是一个普通的网页卡片组件,用于显示访问ip所在城市的基本天气信息,用处不大,我的初心就是给我的网站首页充充门面的。由于本站前端是基于VUE3,那么本例也是如此。后端部分涉及到的第三方比较多,但还是基于DRF
在这里插入图片描述

接口实现流程

  1. 首先需要再和风天气官网注册账户,创建你的项目并拿到你专属的key,没关系,我们可以用免费订阅
  2. 收集你前端要用到的各种天气图标图片资源,我是在和风天气官网中简单的找了一些用于我的项目。
  3. 在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
  1. 创建你的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)
  1. 获取请求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)  
  1. 请求天气信息
        weather_url = settings.WEATHER_NOW_URL + '&' + urllib.parse.urlencode({'location': coordinate})  
        weather_requests = urllib.request.Request(weather_url)  
  1. 读取天气和城市请求的返回体信息
    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_URLWEATHER_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>&nbsp;
          <span
            >{{
              weather_data?.city?.location[0]?.adm1 +
              "·" +
              weather_data?.city?.location[0]?.adm2
            }}
          </span>
        </p>
        <P>
          <IconFont type="icon-gengxinshijian"></IconFont>&nbsp;
          <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

个人站点原文地址: www.simplespace.site

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值