校赛内容 用户城市天气(微信小程序+node+mongodb)

161 阅读9分钟

完成作品演示

每个页面都是用swiper做的所以可以滑动 Screenshot 2025-05-10 085206.png

Screenshot 2025-05-10 085229.png

Screenshot 2025-05-10 085245.png

后台接口和数据库

添加天气数据

使用限次数的api将对应城市添加进已写好的数据库

// 需要添加的城市
const citys = ["北京", "上海", "广州", "深圳", "成都", "杭州", "重庆", "武汉", "西安", "苏州", "天津", "南京", "长沙", "郑州", "东莞", "青岛", "沈阳", "宁波", "昆明", "无锡", "佛山", "合肥", "大连", "福州", "厦门", "哈尔滨", "济南", "长春", "石家庄", "南宁"];


const weatch_URL = 'http://v0.yiketianqi.com/free/v2031';

// 发送请求获取天气数据 接收城市名并获取对应城市的天气数据
async function fetchWeatherData(city) {
    try {
        const response = await fetch(`${weatch_URL}?city=${city}&appid=23862154&appsecret=9MI9OxpP&hours=1`);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return await response.json();
    } catch (error) {
        console.error('获取天气数据时出错:', error);
        throw error;
    }
}




// 将获取的数据做处理并将需要的数据添加进数据库
async function saveWeatherData(data) {
    const city = data.city;
    const { hours, ...weather } = data.week[0];
    const firstDayWeather = [weather];
    const otherDays = data.week.slice(1).map(day => {
        const { hours, ...otherrest } = day;
        return otherrest;
    });

    const updateData =({
        city,
        firstDay:firstDayWeather,
        hoursData:hours,
        otherDays
    });

    try {
        // 使用 findOneAndUpdate 方法,有就更新,没有添加
        const result = await cityWeather.findOneAndUpdate(
            { city }, 
            updateData, // 更新的数据
            { 
                new: true, // 返回更新后的文档
                upsert: true // 若文档不存在则创建
            }
        );
        console.log('数据更新/插入成功:', result);
    } catch (error) {
        console.error('更新/插入数据时出错:', error);
        throw error;
    }
}

// 遍历城市列表并处理每个城市的天气数据
async function processCities() {
    for (const city of citys) {
        try {
            const data = await fetchWeatherData(city);
            await saveWeatherData(data);
        } catch (error) {
            console.error(`处理城市 ${city} 时出错:`, error);
        }
    }
    mongoose.connection.close();
}

// 连接数据库,成功后调用 processCities 函数
connectionCity.on('connected', async () => {
    console.log('数据库连接成功');
    try {
        await processCities();
    } catch (error) {
        console.error('处理城市数据时出错:', error);
    }
});

这时对应的城市天气数据集合已建好 需要手动更新数据 如果想自动判断是否添加 初次手动添加数据时增添一个时间字段 后续根据对应集合中的时间字段判断是否更新就行了

Screenshot 2025-05-10 090127.png 还有个用户数据 空表就行我这个-代表默认用户 data就是该用户对应的城市数据 Screenshot 2025-05-10 090502.png

后台接口

获取天气 提交用户数据 获取用户数据 没做跨域

app.get('/weather/:city',async(req,res)=>{
    const city=req.params.city
    console.log(city);
    try {
        // 从数据库查询城市天气数据
        
        const weatherData = await cityWeather.findOne({ city });
   
        
        if (weatherData) {
            res.json(weatherData);
        } else {
            res.status(404).json({ message: '没有该城市数据' });
        }
    } catch (error) {
        console.error('获取天气数据出错:', error);
        res.status(500).json({ error: '获取天气数据出错' });
    }
})
// 接收用户数据 根据昵称来有就更新没有就添加请求体参数
app.post('/userCityData', async (req, res) => {
    
    // console.log("sdddd");
    // 解构赋值
    const { nickname, data } = req.body;
    // console.log(nickname,data);
    
    if (!nickname || !data) {
        return res.status(400).json({ message: '昵称和数据均为必填项' });
    }
    try {
        // 使用 findOneAndUpdate 方法,若不存在则创建
        const updatedUser = await userCityData.findOneAndUpdate(
            { nickname },
            { nickname, data },
            { new: true, upsert: true }
        );
    } catch (error) {
        console.error('更新或添加用户数据出错:', error);
        res.status(500).json({ error: '更新或添加用户数据出错' });
    }
});
// 用于用户登录时获取已有数据
app.get('/userCityData/:nickname', async (req, res) => {
    const { nickname } = req.params;
    try {
        const user = await userCityData.findOne({ nickname });
        if (user) {
            // 假设第二个字段是 data
            res.json(user.data);
        } else {
            res.status(404).json({ message: '未找到匹配的用户数据' });
        }
    } catch (error) {
        console.error('获取用户数据出错:', error);
        res.status(500).json({ error: '获取用户数据出错' });
    }
});

微信小程序

wxml城市选择栏

城市选择栏 使用了类似轮播图的技术 并根据swiper的切换事件来修改值已确定当前页面索引 最右方如果有用户会显示用户名 并用了视口监控来改变城市选择栏的显示状态

屏幕截图 2025-05-09 212755.png

<view class="headerLock"  style="{{lockBoolean===true?'background-color:#0476E6':''}}">
<!-- 而在城市选择栏中的城市展示使用了类似轮播图的技术  并通过swiper的切换来改变值确认当前轮播对象 -->
  <view class="qk1" wx:if="{{!lockBoolean}}" bindtap="delectCitys">
    <input type="button" value="+" />
      <view >
        <view class="cityName">
          {{citys[swiperIndex].city}}市
        </view>
        <view class="lockUpdata" wx:if="{{lockUpdata}}">
        更新成功
        </view>
        <view class="slideshow">
          <view 
            wx:for="{{citys.length}}" 
            wx:key="*this" 
            class="{{index !== swiperIndex? 'indicator active' : 'indicator'}}"
          ></view>
        </view>
      </view>
      <view  wx:if="{{citys[swiperIndex].city === '武汉'}}">
        <image src="/image/定位.png" mode=""/>
      </view>
  </view>
  <!-- 当视口到指定位置 -->
  <view class="qk1" wx:if="{{lockBoolean}}" bindtap="delectCitys">
    <view  style="margin-right: 10rpx;">
        <view class="cityName">
          {{citys[swiperIndex].city}}市
        </view>
        <view class="lockUpdata" wx:if="{{lockUpdata}}">
        更新成功
        </view>

      </view>
      <view  style="margin-right: 10rpx;">
        <image src="{{'/image/icon/' + citys[swiperIndex].firstDay[0].wea + '.png'}}" />
      </view>
      <view>
        {{citys[swiperIndex].firstDay[0].tem}}℃
        </view>
  </view>
  <!--在城市选择栏我们也通过wxif判断并显示当前用户-->
 <view class="userName">
  <view wx:if="{{isLogon}}" >{{userName}}</view>
 </view>
</view>

wxml主题数据部分(图表和温差温度计)

图表

Screenshot 2025-05-10 092017.png

    <view class="my-char" style="height: {{canvasHeight}};">
      <ec-canvas  class="mychar-dom" canvas-id="mychart" ec="{{cities[swiperIndex].ec}}" ></ec-canvas>
    </view>

温差计 由于高度是后台计算的而温差是有动画从0到100%的效果的 所以需要给其一个父元素来限制他的100%才能实现动画效果 css动画就是一个from 高度0 到结束高度 100%

Screenshot 2025-05-10 092035.png

            <view class="temperaturesLength" style="height:{{cityTempBarHeights[swiperIndex][index+1]}}px;">
                <view class="temperatures" ></view>
            </view>

微信小程序js使用的数据

首先引入ec-canvas组件库 这个每个人不同就不演示了 主要数据

 // 请求后的所有城市都会存储在里面aw
    citys:[],
    // 滑块页面的索引值
    swiperIndex:0,
    // 异步请求后是否显示更新成功
    lockUpdata:false,
    // 触发的视口高度  城市选择栏改变时对应的视口高度
    triggerHeight:600 * 0.8 ,
    //城市选择栏状态
    lockBoolean:false,
    // 默认用户名
    userName:'-',
    // 默认用户头像
    avatarUrl:"",
    // 登录状态
    isLogon:false,
    // 初始图表数据城市
    param:'武汉',
    // 所有图标配置
    cities:[],
    // 图表的显示状态 由于canvas是画布也就是脱标的所以需要隐藏以防遮挡其他数据展示
    iscanvas:false,
    // 在后台设置图表的高度通过视口事件改变高度来实现隐藏图表(不用wxif  是由于图表是通过属性获取配置要实时获取 当我们删除图表再复原是不行的)
    canvasHeight:500,

js温差计算

由于citys也就是所有城市是多个所以就是个二维数组,外层是城市里层是7天温差高度,而城市数据又是请求来的 所以computeHeights需要做异步处理或在对应请求成功的回调里调用

computeHeights:function(){
    const maxRange = 20;
    const containerHeight = 150;
    // console.log(this.data.citys);
    const cityTempBarHeights = this.data.citys.map(city => {
    //   console.log(city);
        const allDays = [...city.firstDay, ...city.otherDays];
        return allDays.map(day => {
            const maxTemp = parseInt(day.tem1);
            const minTemp = parseInt(day.tem2);
            const tempRange = maxTemp - minTemp;
            let heightRatio = tempRange / maxRange;
            if (heightRatio > 1) {
                heightRatio = 1;
            }
            return containerHeight * heightRatio;
        });
    });
    this.setData({
        cityTempBarHeights
    });


  },

图表配置

由于ec一般会调用两次 一次是函数中调用 如onload里调用this.ininChart,一次是元素中的参数由于ec是一个函数所以也会执行一次 而我们需要的多个城市 也就是多个图表配置, 由于图表我们使用的是echarts的外部模块 而里面的不在配置开始时确定的话就能通过返回的charts再后续修改 但我们是用数组将数组将配置函数包括的回的charts再后续修改就行不通了 通过滑块事件后续修改配置以不行 而由于一个一个配置是函数而我们又要经过遍历城市设置对应的图表配置就形成闭包,遍历的item和index永远在0 第一个 初始话确定所有配置也出现了问题 在重新学习闭包的特性 在闭包中外函数设置的初始值在内函数里相加 内函数里的对应值会越来越大因为闭包会捕获并记住外部函数的变量环境闭包会使函数中引用的变量常驻内存造成内存泄露

initChart() {
    const cities = this.data.citys.map(item => {
    // 由于ec是一个属性 在onInit里就形成闭包了所以 ec里的item和index是固定的  ec的显示一是加载时显示一次和元素里固定一次而以index为显示时其实是多个0
    // 使用闭包的内属性会一直存在不会清空 实现索引增长
    let index=0
    return {
        name: item.city,
        ec: {
          onInit: (canvas, width, height, dpr) => {              
              const chart = echarts.init(canvas, null, {
                  width: width,
                  height: height,
                  devicePixelRatio: dpr
              });
              console.log(this.data.citys);
              canvas.setChart(chart);

              // 加载数据
              this.loadChartData(this.data.citys[index].city, chart);
              // 显示后再加  显示的是0 其实已经是1了
              console.log(index++);
              return chart;
          }
        }
      };
    });
    this.setData({
      cities: cities,
    });
    console.log(this.data.cities);
  },
  loadChartData(city, chart) {
    const that = this;
    const apiUrl = 'http://localhost:3000/weather/';
    const fullUrl = `${apiUrl}${city}`;

    wx.request({
        url: fullUrl,
        success(res) {
            if (res.statusCode === 200) {
                const data = res.data;
                // 根据需要调整数据处理逻辑
                const xAxisData = data.hoursData.slice(0, 7).map(item => item.hours);
                const seriesData = data.hoursData.slice(0, 7).map(item => parseInt(item.tem));

                const option = {
                    tooltip: {
                        show: false
                    },
                    legend: {
                        data: ['未来7小时温度变化'],
                        textStyle: {
                          color: '#F0EDF4' // 设置图例字体颜色,可按需更换为其他颜色值
                      }
                    },
                    xAxis: {
                        type: 'category',
                        data: xAxisData,
                        splitLine: {
                            show: false // 隐藏 x 轴网格线
                        }
                    },
                    yAxis: {
                        type: 'value',
                        splitLine: {
                            show: false // 隐藏 y 轴网格线
                        }
                    },
                    series: [
                        {
                            name: '未来7小时温度变化',
                            type: 'line',
                            stack: '总量',
                            areaStyle: {},
                            data: seriesData,
                            label: {
                              show: true, // 显示数据值
                              position: 'top', // 数据标签位置在折线点上方
                              color: '#FFFFFF'
                            }
                        }
                    ]
                };

                if (chart) {
                    chart.setOption(option);
                } else {
                    console.error(`Chart for ${city} is not initialized.`);
                }
            } else {
                console.error(`请求 ${city} 数据失败,状态码:${res.statusCode}`);
            }
        },
        fail(err) {
            console.error(`请求 ${city} 数据失败:`, err);
        }
    });
},

用户登录和删除城市及添加城市

用户页面和首页采用Storage的方式读取和设置 添加页面和删除就是通过跳转并附带参数的方式修改城市 Screenshot 2025-05-10 113601.png

Screenshot 2025-05-10 113619.png

Screenshot 2025-05-10 113634.png

添加页面和删除

 onLoad(options) {
    const swiperIndex = options.swiperIndex;
    const userName=options.userName
    console.log(options);
    // 转化url为字符串
    const citysStr = decodeURIComponent(options.citys);
    // json转换为对象    
    const citys=JSON.parse(citysStr);
    // console.log(citys[0],swiperIndex);
    // 将首页附带的已有城市和姓名和页面索引获取并设置数据
    this.setData({
      citys:citys,
      swiperIndex:swiperIndex,
      userName
    })
  },

用户页面和首页

NameGetStorage:async function(params) {
    return new Promise((resolve, reject) => {
      wx.getStorage({
          key: 'userInfo',
          success: (res) => {
              this.setData({
                  userName: res.data.name,
                  isLogon: res.data.isLogon
              });
            //   console.log("res.data,name", res.data.name);
              resolve();
          },
          fail: (error) => {
              console.log('您是未登录状态');
              reject(error);
          }
      });
  });
  },

总结困难点

  1. 多城市数据管理与动态渲染

    • 系统需支持多个城市数据的并行展示,每个城市对应独立的温差配置与图表可视化

    • 数据来源复杂:

      • 部分用户默认加载武汉数据
      • 部分用户根据历史数据加载个性化城市列表
    • 数据加载的异步特性导致渲染时序复杂,需确保 UI 与数据状态的一致性

  2. 温差可视化实现挑战

    • 高度计算逻辑:采用「固定总高 × 温度百分比」的动态计算方式

      • 温度差值 = 最高温 - 最低温
      • 显示高度 = 容器总高 × (当前温度 - 最低温) / 温度差值
    • CSS 动态绑定问题:行内样式与外部 CSS 的同步延迟

      • 行内样式实时计算高度,但外部 CSS 无法直接获取该值
      • 解决方案:将高度值绑定到父元素的 data 属性,通过 CSS 变量传递给子元素
  3. ECharts 多实例配置与闭包陷阱

  • 动态配置需求:每个城市需独立配置图表,支持后续滑块交互修改

  • 闭包导致的问题

    • 遍历生成图表配置时,闭包捕获循环变量的最终值,所有实例指向同一城市数据
    • 尝试通过滑块事件动态修改配置时,因闭包环境固化导致修改失效
  • 内存管理风险:闭包持续引用外部变量,可能导致内存泄漏

  • 解决方案:考虑到ec的函数内会调用两次 我们在map也就是外函数设置一个变量 而内函数里将变量修改就能解决

项目展望

可以已引入地图api 并在用户新登录时展示现有用户名称询问其是否和某人绑定,绑定后 在地图上可以看到该绑定的对象 并使用自定义标注将其改为这个用户的居住城市和当前温度及今日天气

比赛总结

由于是最后改成了ui设计比赛,而我的程序偏向开发,也可能是关键计数重点区域没讲明白,最后得了三等奖,当然ui方面也是需要训练补足的,应该做出更多直观的效果