Skip to content

阶段三:城市管理功能实现

1. 教学目标

  • 掌握页面导航:熟练运用 uni.navigateTouni.navigateBack 实现跨页面跳转与返回。
  • 综合状态管理:学习在一个简易的 store 中同时管理对象(cityInfo)和列表(myCities),并实现增、删、持久化等操作。
  • 掌握复合组件:学习使用 uni-swipe-action 实现列表的左滑删除功能。
  • 理解响应式联动:深入理解 Vue 3 的响应式核心——computedwatch,并利用它们实现跨页面状态同步。

2. 城市切换与数据刷新流程图 (Mermaid)

3. 核心步骤详解

🗄️ 步骤一:扩展 store.js 以支持城市列表

设计流程

为了实现城市管理,我们的全局状态 store 需要进化。除了记录“当前城市”外,还必须能够维护一个“我的城市”列表。我们规划的功能点如下:

  • 状态: store 对象中需要一个 myCitieshotCitiesallCities 数组来存放用户添加的城市、热门城市、所有城市。
  • 获取城市getMyCities获取我的城市列表,getHotCities获取热门城市列表,getAllCities获取所有城市列表。
  • 添加: 需要一个 addMyCity 函数,当用户选择一个新城市时,可以将其加入 myCities 列表(并要去重)。
  • 删除: 需要一个 removeMyCity 函数,允许用户从 myCities 列表中移除城市。
  • 持久化: 用户的城市列表应该在关闭小程序后依然存在。因此,任何对 myCities 的修改都应立即通过 uni.setStorageSync 保存到本地存储。应用启动时,也应从本地存储加载。

编码流程

  1. 打开 utils/store.js 文件。
  2. reactive 对象中,新增 myCities: []hotCities: []allCities: [] 三个数组。hotCities 可作为固定数据。
  3. 实现三个获取函数,直接return store.[citis]getMyCities获取我的城市列表,getHotCities获取热门城市列表,getAllCities获取所有城市列表。
  4. 实现 addMyCity(city) 函数,内部要先判断 store.myCities 中是否已存在该城市,如果不存在再 push,然后调用 uni.setStorageSync
  5. 实现 removeMyCity(index) 函数,使用 splice 方法移除指定索引的城市,然后调用 uni.setStorageSync
  6. 在文件底部的 initStore 函数中,增加从本地存储读取 myCities 并赋值给 store.myCities 的逻辑。
点击展开/折叠 utils/store.js 扩展后源代码
vue
import { reactive } from 'vue';

// 创建全局状态
const store = reactive({
  // 当前城市信息
  cityInfo: {
    city: '北京',
    longitude: 116.41,
    latitude: 39.92,
    locationId: '101010100'
  },
  // 我的城市列表
  myCities: ['北京', '上海', '广州', '九江', '南昌'],
  // 热门城市列表
  hotCities: ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安'],
  // 所有城市,按字母分类
  allCities : [
    ["A", ["阿坝", "阿克苏", "阿拉善", "阿勒泰", "安康", "安庆", "鞍山", "安顺", "安阳"]],
    ["B", ["巴彦淖尔", "巴音郭楞", "巴中", "白城", "百色", "白山", "白银", "保定", "宝鸡", "保山", "包头", "北海", "北京", "蚌埠", "本溪", "毕节", "滨州", "博尔塔拉", "亳州"]],
    ["C", ["沧州", "长春", "常德", "昌都", "长治", "常州", "巢湖", "朝阳", "潮州", "承德", "成都", "郴州", "赤峰", "池州", "重庆", "崇左", "楚雄", "滁州"]],
    ["D", ["大理", "大连", "丹东", "大庆", "大同", "大兴安岭", "达州", "德宏", "德阳", "德州", "定西", "迪庆", "东莞", "东营"]],
    ["E", ["鄂尔多斯", "恩施", "鄂州"]],
    ["F", ["防城港", "佛山", "抚顺", "阜新", "阜阳", "福州", "抚州"]],
    ["G", ["甘南", "赣州", "甘孜", "广安", "广元", "广州", "贵港", "桂林", "贵阳", "果洛", "固原"]],
    ["H", ["哈尔滨", "海北", "海东", "海口", "海南", "海西", "邯郸", "杭州", "汉中", "鹤壁", "河池", "合肥", "鹤岗", "黑河", "衡水", "衡阳", "和田", "河源", "菏泽", "贺州", "红河", "淮安", "淮北", "怀化", "淮南", "黄冈", "黄南", "黄山", "黄石", "呼和浩特", "惠州", "葫芦岛", "呼伦贝尔", "湖州"]],
    ["J", ["佳木斯", "吉安", "江门", "焦作", "嘉兴", "嘉峪关", "揭阳", "吉林", "济南", "金昌", "晋城", "景德镇", "荆门", "荆州", "金华", "济宁", "晋中", "锦州", "九江", "酒泉", "鸡西", "济源"]],
    ["K", ["开封", "喀什", "克拉玛依", "克孜勒苏", "昆明"]],
    ["L", ["来宾", "莱芜", "廊坊", "兰州", "拉萨", "乐山", "凉山", "连云港", "聊城", "辽阳", "辽源", "丽江", "临沧", "临汾", "临夏", "临沂", "林芝", "丽水", "六安", "六盘水", "柳州", "陇南", "龙岩", "娄底", "漯河", "洛阳", "泸州", "吕梁"]],
    ["M", ["马鞍山", "茂名", "眉山", "梅州", "绵阳", "牡丹江"]],
    ["N", ["南昌", "南充", "南京", "南宁", "南平", "南通", "南阳", "那曲", "内江", "宁波", "宁德", "怒江"]],
    ["P", ["盘锦", "攀枝花", "平顶山", "平凉", "萍乡", "莆田", "濮阳"]],
    ["Q", ["黔东南", "黔南", "黔西南", "青岛", "庆阳", "清远", "秦皇岛", "钦州", "齐齐哈尔", "七台河", "泉州", "曲靖", "衢州"]],
    ["R", ["日喀则", "日照"]],
    ["S", ["三门峡", "三明", "三亚", "上海", "商洛", "商丘", "上饶", "山南", "汕头", "汕尾", "韶关", "绍兴", "邵阳", "沈阳", "深圳", "石家庄", "十堰", "石嘴山", "双鸭山", "朔州", "四平", "松原", "绥化", "遂宁", "随州", "宿迁", "苏州", "宿州"]],
    ["T", ["塔城", "泰安", "太原", "台州", "泰州", "唐山", "天津", "天水", "铁岭", "铜川", "通化", "通辽", "铜陵", "铜仁", "吐鲁番"]],
    ["W", ["威海", "潍坊", "文山", "温州", "乌海", "武汉", "芜湖", "五家渠", "乌兰察布", "乌鲁木齐", "武威", "无锡", "吴忠", "梧州"]],
    ["X", ["厦门", "西安", "湘潭", "湘西", "襄阳", "咸宁", "仙桃", "咸阳", "孝感", "锡林郭勒", "兴安", "邢台", "西宁", "新乡", "信阳", "新余", "忻州", "西双版纳", "宣城", "许昌", "徐州"]],
    ["Y", ["雅安", "延安", "延边", "盐城", "阳江", "阳泉", "扬州", "烟台", "宜宾", "宜昌", "宜春", "伊春", "伊犁", "银川", "营口", "鹰潭", "益阳", "永州", "岳阳", "玉林", "榆林", "运城", "云浮", "玉树", "玉溪"]],
    ["Z", ["枣庄", "张家界", "张家口", "张掖", "漳州", "湛江", "肇庆", "昭通", "郑州", "镇江", "中山", "中卫", "周口", "舟山", "珠海", "驻马店", "株洲", "淄博", "自贡", "资阳", "遵义"]]
  ],
  isRefreshingCity: true,
})
/**
 * 设置当前城市信息
 * @param {Object} cityInfo - 城市信息对象,包含city、longitude、latitude、locationId
 */
export const setCityInfo = (cityInfo) => {
  store.cityInfo = cityInfo;
  // 保存到本地存储,使用单个键"CityInfo"
  uni.setStorageSync('CityInfo', cityInfo);
};

/**
 * 获取当前城市信息
 * @returns {Object} - 当前城市信息对象
 */
export const getCityInfo = () => {
  return store.cityInfo;
};

/**
 * 获取我的城市列表
 * @returns {Array} - 我的城市列表
 */
export const getMyCities = () => {
  return store.myCities;
};

/**
 * 获取热门城市列表
 * @returns {Array} - 热门城市列表
 */
export const getHotCities = () => {
  return store.hotCities;
};

/**
 * 获取所有城市列表
 * @returns {Array} - 所有城市列表,按字母分类
 */
export const getAllCities = () => {
  return store.allCities;
};

/**
 * 添加城市到我的城市列表
 * @param {string} city - 城市名称
 */
export const addMyCity = (city) => {
  if (!store.myCities.includes(city)) {
    store.myCities.push(city);
    // 保存到本地存储
    uni.setStorageSync('myCities', store.myCities);
  }
};

/**
 * 从我的城市列表中删除城市
 * @param {string|number} city - 城市名称或索引
 */
export const removeMyCity = (city) => {
  let index;
  if (typeof city === 'string') {
    index = store.myCities.indexOf(city);
  } else {
    index = city;
  }
  
  if (index !== -1) {
    store.myCities.splice(index, 1);
    // 保存到本地存储
    uni.setStorageSync('myCities', store.myCities);
  }
};


// 初始化时从本地存储加载数据
const initStore = () => {
  // 加载城市信息
  const cityInfo = uni.getStorageSync('CityInfo');
  if (cityInfo) {
    store.cityInfo = cityInfo;
  }
  
  // 加载我的城市列表
  const cities = uni.getStorageSync('myCities');
  if (cities && Array.isArray(cities)) {
    store.myCities = cities;
  }
};

initStore();

export default store;

🧩 步骤二:构建城市管理页面 (city.vue)

设计流程

这个页面是城市管理功能的核心UI。设计上,它需要为用户提供清晰的城市列表(如热门城市、我的城市、所有城市),并提供搜索功能。核心交互是:当用户点击任何一个城市时,应用应将此城市设为“当前城市”,并自动返回天气主页显示该城市的天气。

编码流程

  1. pages 目录下创建 city/city.vue,并在 pages.json 中注册路由。
  2. 在模板中,使用 uni-nav-bar 创建顶部导航,并绑定返回事件。使用 uni-search-bar 创建搜索框。
  3. 使用 v-for 分别渲染“热门城市”、“我的城市”、“所有城市”列表。将“我的城市”列表项包裹在 uni-swipe-action-item 中,并配置 right-options 实现左滑删除。
  4. 在脚本中,从 store.js 导入 getMyCities, getHotCities, setCityInfo, addMyCity , getAllCity等函数。
  5. onMounted 钩子中,调用 getMyCitiesgetHotCitiesgetAllCities,将返回的列表赋值给本地 ref 变量,以驱动UI渲染。
  6. 实现 selectCity(city) 核心函数。它接收城市名作为参数,调用 location.jsupdateCityInfoByCity 获取完整信息,然后调用 setCityInfo 更新全局状态,最后调用 uni.reLaunch({url:'/pages/index/index'}) 返回首页(需读取store.isRefreshingCity,来判断首页是否重新定位、加载数据)。
点击展开/折叠 pages/city/city.vue 源代码
vue
<template>
  <view class="city-container">
    <uni-nav-bar title="城市管理" left-text="返回" left-icon="left" @click-left="onBack"></uni-nav-bar>
    
    <view class="search-section">
      <uni-search-bar placeholder="搜索城市" @input="onSearch" @confirm="onSearchConfirm"></uni-search-bar>
    </view>
    
    <!-- 搜索结果展示 -->
   <view class="search-result" v-if="hasSearchResult">
      <text class="section-title">搜索结果</text>
      <uni-list>
        <uni-list-item 
          v-for="city in searchResult" 
          :key="city" 
          :title="city" 
		  clickable
          @click="selectCity(city)"
        ></uni-list-item>
      </uni-list>
    </view>
    
    <!-- 城市列表 -->
    <view class="city-content" v-else>
      <view class="city-list">
        <view class="current-city">
          <text class="section-title">当前城市</text>
          <uni-list-item :title="currentCity" clickable @click="selectCity(currentCity)"></uni-list-item>
        </view>
        
        <view class="hot-cities">
          <text class="section-title">热门城市</text>
          <view class="hot-cities-grid">
            <view class="city-item" v-for="city in hotCities" :key="city" @click="selectCity(city)">
              {{ city }}
            </view>
          </view>
        </view>
        
        <view class="my-cities">
          <text class="section-title">我的城市</text>
          <uni-swipe-action>
            <uni-swipe-action-item v-for="(city, index) in myCities" :key="city" :right-options="rightOptions" @click="deleteCity(index)">
              <uni-list-item clickable :title="city" @click="selectCity(city)"></uni-list-item>
            </uni-swipe-action-item>
          </uni-swipe-action>
        </view>
        
        <!-- 所有城市 -->
        <view class="all-cities">
          <text class="section-title">所有城市</text>
          <view 
            v-for="[letter, cities] in allCities" 
            :key="letter"
            :id="`letter-${letter}`"
            class="city-group"
          >
            <view class="letter-title">{{ letter }}</view>
            <uni-list>
              <uni-list-item 
                v-for="city in cities" 
                :key="city" 
                :title="city" 
				clickable
                @click="selectCity(city)"
              ></uni-list-item>
            </uni-list>
          </view>
        </view>
      </view>
      
      <!-- 字母索引侧边栏 -->
      <view class="letter-index">
        <view 
          v-for="letter in letterList" 
          :key="letter" 
          class="letter-item" 
          @click="scrollToLetter(letter)"
        >
          {{ letter }}
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted, computed } from 'vue';
import { searchCity } from '../../utils/api';
import { getCityInfo, setCityInfo, getMyCities, getHotCities, removeMyCity, addMyCity, getAllCities } from '../../utils/store';
import store from '../../utils/store.js'
// 当前城市
const currentCity = ref('北京');

// 热门城市
const hotCities = ref([]);

// 我的城市
const myCities = ref([]);

// 所有城市
const allCities = ref([]);

// 搜索关键词
const searchKeyword = ref('');

// 搜索结果
const searchResult = ref([]);

// 字母索引列表
const letterList = ref([]);

// 计算搜索结果,当搜索关键词变化时自动更新
const hasSearchResult = computed(() => {
  return searchResult.value.length > 0;
});

// 右侧滑动选项
const rightOptions = [
  {
    text: '删除',
    style: {
      backgroundColor: '#FF4757'
    }
  }
];

// 返回上一页
const onBack = () => {
  uni.navigateBack();
};

// // 搜索
// const onSearch = (e) => {
//   searchKeyword.value = e.value;
//   performSearch(e.value);
// };

// // 搜索确认
// const onSearchConfirm = (e) => {
//   searchKeyword.value = e.value;
//   performSearch(e.value);
// };

// // 执行搜索
// const performSearch = (keyword) => {
//   if (!keyword) {
//     searchResult.value = [];
//     return;
//   }
  
//   // 创建所有城市的扁平列表,用于搜索
//   const allCitiesFlat = [];
  
//   // 添加当前城市
//   allCitiesFlat.push(currentCity.value);
  
//   // 添加热门城市
//   hotCities.value.forEach(city => {
//     if (!allCitiesFlat.includes(city)) {
//       allCitiesFlat.push(city);
//     }
//   });
  
//   // 添加所有城市
//   allCities.value.forEach(([letter, cities]) => {
//     cities.forEach(city => {
//       if (!allCitiesFlat.includes(city)) {
//         allCitiesFlat.push(city);
//       }
//     });
//   });
  
//   // 过滤匹配的城市
//   searchResult.value = allCitiesFlat.filter(city => {
//     return city.includes(keyword);
//   });
// };

// 滚动到指定字母位置
const scrollToLetter = (letter) => {
  const element = document.getElementById(`letter-${letter}`);
  if (element) {
    element.scrollIntoView({ behavior: 'smooth', block: 'start' });
  }
};

// 选择城市
const selectCity = async (city) => {
  // 导入updateCityInfoByCity函数
  const { updateCityInfoByCity } = await import('../../utils/location');
  // 获取城市信息
  console.log(city)
  const cityInfo = await updateCityInfoByCity(city);
  // 设置当前城市信息
  setCityInfo(cityInfo);
  // 添加到我的城市列表
  addMyCity(city);
  // 返回首页
  store.isRefreshingCity = false;
  uni.reLaunch({url:'/pages/index/index'});
};

// 删除城市
const deleteCity = (index) => {
  // 从我的城市列表中删除
  removeMyCity(index);
};

// 页面加载时初始化数据
onMounted(() => {
  // 获取当前城市信息
  const cityInfo = getCityInfo();
  currentCity.value = cityInfo.city;
  // 获取热门城市
  hotCities.value = getHotCities();
  // 获取我的城市列表
  myCities.value = getMyCities();
  // 获取所有城市
  allCities.value = getAllCities();
  // 初始化字母索引列表
  letterList.value = allCities.value.map(([letter]) => letter);
});
</script>

<style scoped>
.city-container {
  padding: 20rpx;
  background-color: #f5f5f5;
  min-height: 100vh;
}

.search-section {
  margin-bottom: 20rpx;
}

/* 搜索结果样式 */
.search-result {
  background-color: #fff;
  border-radius: 10rpx;
  padding: 20rpx;
  margin-bottom: 20rpx;
}

/* 城市内容容器,用于容纳城市列表和字母索引 */
.city-content {
  position: relative;
  display: flex;
}

.city-list {
  flex: 1;
  background-color: #fff;
  border-radius: 10rpx;
  padding: 20rpx;
  max-height: calc(100vh - 200rpx);
  overflow-y: auto;
}

.section-title {
  font-size: 28rpx;
  color: #666;
  margin: 20rpx 0;
  display: block;
}

.hot-cities-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 20rpx;
  margin-bottom: 30rpx;
}

.city-item {
  width: 120rpx;
  height: 60rpx;
  background-color: #f0f0f0;
  border-radius: 30rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28rpx;
  color: #333;
}

.city-item:active {
  background-color: #e0e0e0;
}

.current-city,
.hot-cities,
.my-cities {
  margin-bottom: 30rpx;
}

/* 所有城市样式 */
.all-cities {
  margin-top: 20rpx;
}

.city-group {
  margin-bottom: 20rpx;
}

.letter-title {
  background-color: #f5f5f5;
  color: #666;
  font-size: 24rpx;
  padding: 10rpx 20rpx;
  margin: 0 -20rpx;
  font-weight: bold;
}

/* 字母索引侧边栏样式 */
.letter-index {
  position: fixed;
  right: 20rpx;
  top: 50%;
  transform: translateY(-50%);
  background-color: rgba(255, 255, 255, 0.8);
  border-radius: 30rpx;
  padding: 10rpx 5rpx;
  z-index: 999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.letter-item {
  font-size: 20rpx;
  color: #333;
  padding: 6rpx 10rpx;
  cursor: pointer;
  border-radius: 50%;
  transition: all 0.2s;
}

.letter-item:active {
  background-color: #007aff;
  color: #fff;
}
</style>

🔗 步骤三:添加更新城市信息函数 (location.js)

设计流程

只需要在location.js 中添加 updateCityInfoByCity(cityName) 函数,用于根据新的城市名称,来获取 CityInfo 的其他信息如(city、longitude、latitude、locationId

编码流程

  1. location.js 后面添加updateCityInfoByCity(cityName) 函数。
点击展开/折叠 location.js 相关代码
vue
/**
 * 定位工具函数
 * 1、获取天气情况的api中,需要用到loaction(城市id),需要先获取城市id,
 * 2、获取城市的id,可以通过城市名称来获取(调用和风天气api)
 * 3、城市名称,可以根据ip地址获取,通过ip地址获取城市名称和经纬度信息。
 * 
 * 步骤一、根据ip地址获取城市名称和经纬度信息(用高德地图api)
 * 步骤二、根据城市名称获取城市id(用和风天气api)
 * 
 * 错误处理:失败的话,都默认返回北京id、城市名、经纬度信息
 */

// 和风天气API Key
import UrlConfig from '../datas/url.json';
const QWEATHER_KEY = UrlConfig.getCityIdUrl.key;

const AMAP_URL = UrlConfig.getCurrentCityUrl.AMAP_URL;
const AMAP_KEY = UrlConfig.getCurrentCityUrl.AMAP_KEY;


// const BAIDU_URL = UrlConfig.getCurrentCityUrl.BAIDU_URL;
// const BAIDU_KEY = UrlConfig.getCurrentCityUrl.BAIDU_KEY;

/**
 * 获取当前位置信息
 * @returns {Promise} - 城市信息对象,包含city、longitude、latitude、locationId
 */
export const getCurrentCity = async () => {
  // 第一步:获取IP对应的城市
  const ipData = await new Promise((resolve) => {
    uni.request({
      // // url: 'http://ip-api.com/json/?lang=zh-CN' 
      url: AMAP_URL, //高德 /百度
      data: {
        key: AMAP_KEY, //高德 
		ip: '223.83.97.21'
        // ak: "BAIDU_KEY"  , coor:"bd09ll" //百度
      },
      method: 'GET',
      success: (res) => {
        const [lng, lat] = res.data.rectangle.split(';')[0].split(',');
        resolve({ 
          city: res.data.city, 
          longitude: parseFloat(lng).toFixed(2), //经度
          latitude: parseFloat(lat).toFixed(2)   //纬度
        });
      },
      fail: (err) => {
        resolve({ 
          city: '北京',
          longitude: 116.41,//经度
          latitude: 39.92   //纬度
        });
      }
    });
  });

  const cityName = ipData.city || '北京';
  
  // 第二步:获取城市对应的locationId
  const cityIdData = await fetchCityId(cityName);
  const locationId = cityIdData?.location?.[0]?.id || '101010100';
  
  // 创建城市信息对象
  const cityInfo = {
    city: cityName,
    longitude: ipData.longitude,
    latitude: ipData.latitude,
    locationId: locationId
  };
  
  // 返回城市信息对象
  return cityInfo;
};

/**
 * 根据城市名获取和风天气locationId
 * @param {string} cityName - 城市名称
 * @returns {Promise} - 包含locationId的响应数据
 */
export const fetchCityId = (cityName) => {
  return new Promise((resolve) => {
    uni.request({
      url: UrlConfig.getCityIdUrl.url,
      method: 'GET',
      data: {
        location: cityName,
        key: QWEATHER_KEY
      },
      success: (res) => {
        resolve(res.data);
      },
      fail: () => {
        // 失败时返回默认数据
        resolve({ location: [{ id: '101010100' }] });
      }
    });
  });
};

/**
 * 根据城市名更新城市信息
 * @param {string} cityName - 城市名称
 * @returns {Promise} - 城市信息对象,包含city、longitude、latitude、locationId
 */
export const updateCityInfoByCity = async (cityName) => {
  // 获取城市对应的locationId
  const cityIdData = await fetchCityId(cityName);
  const locationId = cityIdData?.location?.[0]?.id || '101010100';
  
  // 获取经纬度(如果cityIdData中包含)
  const longitude = cityIdData?.location?.[0]?.lon || 116.41;
  const latitude = cityIdData?.location?.[0]?.lat || 39.92;
  
  // 创建城市信息对象
  const cityInfo = {
    city: cityName,
    longitude: longitude,
    latitude: latitude,
    locationId: locationId
  };
  
  // 返回城市信息对象
  return cityInfo;
};

💻 步骤四:改造首页以响应城市变化 (index.vue)

设计流程

这是实现自动刷新的关键。当从城市页返回时,首页需要知道当前城市已变更,并自动刷新天气。如果使用 onShow 生命周期钩子来做,代码会比较繁琐(需要手动比较前后城市名)。更优雅的方案是利用Vue的响应式系统。

我们的设计是:创建一个依赖 store 的计算属性 currentCity。当 store 的状态被 city.vue 改变时,这个计算属性会自动更新。我们再用 watch 监听这个计算属性的变化,一旦变化就触发天气刷新函数。这样就形成了一个无需手动干预的、响应式的数据闭环。

编码流程

  1. index.vue 的脚本区,从vue导入 computedwatch
  2. store.js 导入 getCityInfo
  3. 创建一个名为 currentCity 的计算属性,其 getter 函数返回 getCityInfo().city
  4. 创建一个 watch,监听 currentCity。在回调函数中,判断新旧值是否不同,如果不同,则调用 updateWeather() 函数。
  5. 4或者5任选其一,改造onLoad函数,通过store.isRefreshingCity的值来判断是否需要重新定位、刷新数据
点击展开/折叠 index.vue 响应式相关代码
vue
<template>
	<view class="weather-container">
		<scroll-view scroll-y style="height: 100vh;">
			<!-- 城市选择栏 (静态) -->
			<view class="city-bar" @click="goToCity">
				<text class="city-name">{{ currentCity }}</text>
				<uni-icons type="arrow-down" size="20"></uni-icons>
			</view>

			<!-- 3. 在模板中使用所有组件,并通过 props 传入数据 -->
			<CurrentEnvironment :weather-data="currentWeather" :air-data="airQuality"></CurrentEnvironment>
			<HourlyForecast :hourly-data="hourlyForecast"></HourlyForecast>
			<DailyForecast :daily-data="dailyForecast"></DailyForecast>
			<LifeIndex :index-data="lifeIndices" @show-detail="showIndexDetail"></LifeIndex>

			<!-- 指数详情弹窗 (静态阶段不实现功能) -->
			<!-- <IndexDetailPopup :visible="detailVisible" :index-data="currentIndex" @close="closePopup"></IndexDetailPopup> -->
		</scroll-view>
	</view>
</template>

<script setup>
	import {
		ref,
		onMounted,
		computed,
		watch
	} from 'vue';
	
	import { onLoad } from '@dcloudio/uni-app';
	// 1. 导入所有组件和服务
	import CurrentEnvironment from '../../components/CurrentEnvironment.vue';
	import HourlyForecast from '../../components/HourlyForecast.vue';
	import DailyForecast from '../../components/DailyForecast.vue';
	import LifeIndex from '../../components/LifeIndex.vue';

	import {
		getCurrentWeather,
		get24hForecast,
		get7dForecast,
		getAirQuality,
		getLifeIndex,
	} from '../../utils/api';
	import {
		getCityInfo,
		setCityInfo
	} from '../../utils/store';
	
	import store from '../../utils/store.js'
	
	import {
		getCurrentCity
	} from '../../utils/location';
	// ...

	const currentCity = computed(() => getCityInfo().city);

	// 2. 定义 ref 变量
	const currentWeather = ref({});
	const airQuality = ref({});
	const hourlyForecast = ref([]);
	const dailyForecast = ref([]);
	const lifeIndices = ref([]);
	// dailyForecast  、 lifeIndices

	// 3. 核心数据更新函数
	const updateWeather = async () => {
		const city = getCityInfo().city;

		// 获取当前天气情况 currentWeather
		const weatherRes = await getCurrentWeather(city);
		if (weatherRes.code === '200') {
			currentWeather.value = weatherRes.data;
		}
		// 获取当前空气质量 airQuality
		const airQualityRes = await getAirQuality(city);
		if (airQualityRes.code === '200') {
			airQuality.value = airQualityRes.data;
		}

		// 获取24小时预报
		const hourlyRes = await get24hForecast(currentCity.value);
		if (hourlyRes.code === '200') {
			hourlyForecast.value = hourlyRes.data;

		}

		// 获取7天预报
		const dailyRes = await get7dForecast(currentCity.value);
		if (dailyRes.code === '200') {
			dailyForecast.value = dailyRes.data;
		}

		// 获取生活指数
		const lifeRes = await getLifeIndex(currentCity.value);
		if (lifeRes.code === '200') {
			lifeIndices.value = lifeRes.data;
		}
	};

	// 4. 初始化函数
	const initData = async () => {
		const currentLocation = await getCurrentCity();
		setCityInfo(currentLocation);
		await updateWeather();
	};

	// 5. 在页面加载时触发所有逻辑
	onLoad(() => {
		if(store.isRefreshingCity){
			initData();
		}
		else{
			updateWeather();
			store.isRefreshingCity = true;
		}

	});
	
	// 监听 currentCity 变化,当城市改变时自动刷新天气数据
	watch(currentCity, (newCity, oldCity) => {
	  if (newCity && newCity !== oldCity) {
	    updateWeather();
	  }
	});
	
	// 显示指数详情
	const showIndexDetail = (index) => {
		alert(index.desc)
	};
	
	// 跳转到城市管理页
	const goToCity = () => {
	  uni.navigateTo({
	    url: '/pages/city/city'
	  });
	};
	
</script>

<style scoped>
	/* 容器美化:
   设定一个从顶部“天空蓝”过渡到底部“背景灰”的渐变,
   让页面看起来不那么死板,更有天气的氛围感。
*/
	.weather-container {
		min-height: 100vh;
		/* 渐变背景:顶部淡青色(#E0F7FA) -> 底部浅灰色(#F5F5F5) */
		background: linear-gradient(180deg, #E0F7FA 0%, #F5F5F5 35%, #F5F5F5 100%);

		/* 左右留白:让所有子组件(卡片)悬浮在中间,不贴边 */
		padding: 0 24rpx;
		box-sizing: border-box;

		/* 底部安全区:防止内容被底部小黑条遮挡 */
		padding-bottom: calc(40rpx + constant(safe-area-inset-bottom));
		padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
	}

	/* 城市选择栏美化:
   使其看起来更像一个可交互的标题
*/
	.city-bar {
		display: flex;
		flex-direction: row;
		align-items: center;
		justify-content: center;
		/* 左对齐通常比居中更符合现代App习惯,如果您喜欢居中可保留 justify-content: center */
		justify-content: center;
		padding: 20rpx 0 10rpx 30rpx;
		/* 增加底部padding,与下方卡片拉开距离 */
		position: relative;
		z-index: 10;
	}

	/* 点击时的反馈效果 */
	.city-bar:active {
		opacity: 0.7;
	}

	.city-name {
		font-size: 40rpx;
		/* 加大字号 */
		font-weight: 600;
		color: #333;
		margin-right: 12rpx;
		letter-spacing: 2rpx;
		text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
		/* 微微的文字阴影,增加立体感 */
	}

	/* 图标微调:确保箭头垂直居中 */
	.city-bar :deep(.uni-icons) {
		margin-top: 4rpx;
		font-weight: bold;
	}
</style>

4. 课后任务 (可选)

  • 任务: 目前 city.vue 中的搜索功能是缺失的。请在 utils/api.js 中实现 searchCity 函数,并在 city.vue 中完成搜索逻辑。
  • 目的: 巩固 API 封装与调用,并练习处理用户输入、异步搜索和结果展示的常见场景。

教师提示 本阶段的精髓在于 computed + watch 组成的响应式数据流。务必向学生讲清这个“魔法”背后的原理,让他们理解 Vue 是如何自动完成状态同步的。这比使用 onShow 配合全局事件总线或手动比对的传统方法更优雅、更符合 Vue 的设计哲学。