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

1. 教学目标

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

此图清晰地展示了从用户点击城市到首页刷新数据的完整响应式流程。

graph TD subgraph "首页 index.vue" A[用户点击城市名] --> B[调用 uni.navigateTo
跳转到城市页]; K[watch 监听器
watch currentCity, ...] -- 城市变化时触发 --> L[调用 updateWeather
刷新天气数据]; end subgraph "城市管理页 city.vue" C[页面加载 onMounted] --> D[从 store.js 获取
当前城市/热门城市/我的城市]; D --> E[渲染UI列表]; F[用户点击新城市] --> G[调用 selectCity 新城市]; G --> H[调用 location.js
获取新城市的完整信息]; H --> I[调用 store.js 的 setCityInfo
更新全局状态]; I --> J[调用 uni.navigateBack
返回首页]; end subgraph "状态层 store.js" M[cityInfo 状态改变] end subgraph "响应式系统 Vue" N[computed 属性 currentCity
在 index.vue 中] end I --> M; M -- 通知 --> N; N -- 值变化 --> K;

3. 核心步骤详解

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

为了记录用户添加的城市,我们需要对 utils/store.js 进行扩展。

点击展开/折叠 utils/store.js 扩展后源代码
import { reactive } from 'vue';

const store = reactive({
  cityInfo: { /* ... */ },
  // 新增:我的城市列表
  myCities: ['北京', '上海', '广州'],
  // 新增:热门城市列表
  hotCities: ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安']
});

// ... setCityInfo, getCityInfo ...

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

/**
 * 从我的城市列表中删除城市
 * @param {number} index - 城市在列表中的索引
 */
export const removeMyCity = (index) => {
  if (index > -1) {
    store.myCities.splice(index, 1);
    // 保存到本地存储
    uni.setStorageSync('myCities', store.myCities);
  }
};

export const getMyCities = () => {
  return store.myCities;
};

export const getHotCities = () => {
  return store.hotCities;
};

// 初始化时从本地存储加载数据
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;

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

这个页面是用户与城市列表交互的界面。

点击展开/折叠 pages/city/city.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"></uni-search-bar>
    </view>
    
    <view class="city-list">
      <!-- 热门城市 -->
      <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-right="deleteCity(index)">
            <uni-list-item :title="city" @click="selectCity(city)"></uni-list-item>
          </uni-swipe-action-item>
        </uni-swipe-action>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { getCityInfo, setCityInfo, getMyCities, getHotCities, removeMyCity, addMyCity } from '../../utils/store';
import { updateCityInfoByCity } from '../../utils/location';

const hotCities = ref([]);
const myCities = ref([]);

const rightOptions = [{ text: '删除', style: { backgroundColor: '#FF4757' } }];

const onBack = () => uni.navigateBack();

// 核心:选择城市
const selectCity = async (city) => {
  // 1. 获取新城市的完整信息
  const cityInfo = await updateCityInfoByCity(city);
  // 2. 更新全局状态
  setCityInfo(cityInfo);
  // 3. 添加到"我的城市"列表
  addMyCity(city);
  // 4. 返回首页
  uni.navigateBack();
};

// 删除城市
const deleteCity = (index) => {
  removeMyCity(index);
  // 直接修改 ref 来更新UI,因为 store 的数组变化不会直接通知到这里
  myCities.value = getMyCities(); 
};

// 页面加载时从 store 初始化数据
onMounted(() => {
  hotCities.value = getHotCities();
  myCities.value = getMyCities();
});
</script>

Code Icon 步骤三:改造首页以响应城市变化 (index.vue)

这是实现自动刷新的关键一步,也是 Vue 响应式魅力的体现。

点击展开/折叠 index.vue 响应式相关代码
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import { getCityInfo, setCityInfo } from '../../utils/store';
// ...其他导入

// 1. 创建一个计算属性,它的值依赖于 store
const currentCity = computed(() => getCityInfo().city);

// ...其他 ref 定义...

const updateWeather = async () => {
  // ...更新天气的逻辑...
  console.log(`开始获取【${currentCity.value}】的天气数据...`);
};

const initData = async () => { /* ... */ };

onMounted(() => {
  initData();
});

// 2. 监听这个计算属性的变化
watch(currentCity, (newCity, oldCity) => {
  // 确保城市真的发生了变化再刷新
  if (newCity && newCity !== oldCity) {
    updateWeather();
  }
});
</script>

4. 课后任务 (可选)


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