阶段三:城市管理功能实现
1. 教学目标
- 掌握页面导航:熟练运用
uni.navigateTo和uni.navigateBack实现跨页面跳转与返回。 - 综合状态管理:学习在一个简易的
store中同时管理对象(cityInfo)和列表(myCities),并实现增、删、持久化等操作。 - 掌握复合组件:学习使用
uni-swipe-action实现列表的左滑删除功能,提升应用交互体验。 - 理解响应式联动:深入理解 Vue 3 的响应式核心——
computed和watch,并利用它们实现跨页面状态同步,即城市页的修改能够自动触发首页的刷新。 - 完成功能闭环:将城市选择、状态更新、页面返回、数据刷新等环节串联起来,完成“城市管理”这一核心功能的完整开发。
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;
跳转到城市页]; 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. 核心步骤详解
步骤一:扩展 store.js 以支持城市列表
为了记录用户添加的城市,我们需要对 utils/store.js 进行扩展。
- 职责:
- 在
store对象中新增myCities: []和hotCities: []两个数组。 - 增加
addMyCity和removeMyCity方法,用于添加和删除“我的城市”列表中的项。 - 在这两个方法中,调用
uni.setStorageSync,确保列表的改动能被持久化。 - 修改
initStore函数,使其在应用启动时,也能从本地存储中加载myCities列表。
- 在
- 知识点: 在
reactive对象中操作数组,数组的push,splice,includes方法。
点击展开/折叠 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;
步骤二:构建城市管理页面 (city.vue)
这个页面是用户与城市列表交互的界面。
- UI布局: 使用
uni-nav-bar提供标题和返回按钮,uni-search-bar提供搜索功能,页面主体分为“当前城市”、“热门城市”和“我的城市”三个区块。其中“我的城市”列表项包裹在uni-swipe-action中,以实现左滑删除。 - 逻辑实现:
onMounted中,从store.js调用getCityInfo,getHotCities,getMyCities获取数据,填充页面的初始状态。selectCity(city)函数是核心。当用户点击一个城市时:- 调用
location.js的updateCityInfoByCity(city)获取该城市的完整信息(包括经纬度和locationId)。 - 调用
store.js的setCityInfo()更新全局状态。 - 调用
store.js的addMyCity()将其加入“我的城市”列表(如果不存在)。 - 调用
uni.navigateBack()返回首页。
- 调用
deleteCity(index)函数在用户左滑删除时触发,调用store.js的removeMyCity()。
- 知识点:
onMounted,uni-nav-bar,uni-swipe-action的使用,函数的导入与调用。
点击展开/折叠 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>
步骤三:改造首页以响应城市变化 (index.vue)
这是实现自动刷新的关键一步,也是 Vue 响应式魅力的体现。
- 实现原理: 我们不使用
onShow来手动检查,而是利用computed和watch建立一个响应式链条。 - 实现逻辑:
- 在
index.vue中,我们创建一个计算属性currentCity,它的值依赖于store.js中的getCityInfo()。 - 我们创建一个
watch监听器,来“监视”这个currentCity计算属性的变化。
- 在
- 联动流程:
- 当在
city.vue页面调用setCityInfo()时,store.js中的reactive对象发生变化。 - Vue 的响应式系统会侦测到这个变化,并通知所有依赖该状态的
computed属性重新计算。 index.vue中的currentCity因此得到了一个新的值。watch监听到currentCity的值发生了改变,于是执行其回调函数,调用updateWeather()。- 页面天气数据被刷新,完成闭环。
- 当在
点击展开/折叠 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. 课后任务 (可选)
- 任务: 目前
city.vue中的搜索功能是缺失的。请在utils/api.js中实现searchCity函数(可对接和风天气的城市搜索API,或暂时返回模拟数据),并在city.vue中完成搜索逻辑:用户输入时,调用此API,并将结果展示在一个临时的搜索结果列表中。 - 目的: 巩固 API 封装与调用,并练习处理用户输入、异步搜索和结果展示的常见场景。可以额外挑战一下“防抖”实现。
教师提示: 本阶段的精髓在于 computed + watch 组成的响应式数据流。务必向学生讲清这个“魔法”背后的原理,让他们理解 Vue 是如何自动完成状态同步的。这比使用 onShow 配合全局事件总线或手动比对的传统方法更优雅、更符合 Vue 的设计哲学。强调 store 作为单一数据源的重要性。