阶段三:城市管理功能实现
1. 教学目标
- 掌握页面导航:熟练运用
uni.navigateTo和uni.navigateBack实现跨页面跳转与返回。 - 综合状态管理:学习在一个简易的
store中同时管理对象(cityInfo)和列表(myCities),并实现增、删、持久化等操作。 - 掌握复合组件:学习使用
uni-swipe-action实现列表的左滑删除功能。 - 理解响应式联动:深入理解 Vue 3 的响应式核心——
computed和watch,并利用它们实现跨页面状态同步。
2. 城市切换与数据刷新流程图 (Mermaid)
3. 核心步骤详解
🗄️ 步骤一:扩展 store.js 以支持城市列表
设计流程
为了实现城市管理,我们的全局状态 store 需要进化。除了记录“当前城市”外,还必须能够维护一个“我的城市”列表。我们规划的功能点如下:
- 状态:
store对象中需要一个myCities、hotCities、allCities数组来存放用户添加的城市、热门城市、所有城市。 - 获取城市:
getMyCities获取我的城市列表,getHotCities获取热门城市列表,getAllCities获取所有城市列表。 - 添加: 需要一个
addMyCity函数,当用户选择一个新城市时,可以将其加入myCities列表(并要去重)。 - 删除: 需要一个
removeMyCity函数,允许用户从myCities列表中移除城市。 - 持久化: 用户的城市列表应该在关闭小程序后依然存在。因此,任何对
myCities的修改都应立即通过uni.setStorageSync保存到本地存储。应用启动时,也应从本地存储加载。
编码流程
- 打开
utils/store.js文件。 - 在
reactive对象中,新增myCities: []、hotCities: []和allCities: []三个数组。hotCities可作为固定数据。 - 实现三个获取函数,直接
return store.[citis]。getMyCities获取我的城市列表,getHotCities获取热门城市列表,getAllCities获取所有城市列表。 - 实现
addMyCity(city)函数,内部要先判断store.myCities中是否已存在该城市,如果不存在再push,然后调用uni.setStorageSync。 - 实现
removeMyCity(index)函数,使用splice方法移除指定索引的城市,然后调用uni.setStorageSync。 - 在文件底部的
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。设计上,它需要为用户提供清晰的城市列表(如热门城市、我的城市、所有城市),并提供搜索功能。核心交互是:当用户点击任何一个城市时,应用应将此城市设为“当前城市”,并自动返回天气主页显示该城市的天气。
编码流程
- 在
pages目录下创建city/city.vue,并在pages.json中注册路由。 - 在模板中,使用
uni-nav-bar创建顶部导航,并绑定返回事件。使用uni-search-bar创建搜索框。 - 使用
v-for分别渲染“热门城市”、“我的城市”、“所有城市”列表。将“我的城市”列表项包裹在uni-swipe-action-item中,并配置right-options实现左滑删除。 - 在脚本中,从
store.js导入getMyCities,getHotCities,setCityInfo,addMyCity,getAllCity等函数。 - 在
onMounted钩子中,调用getMyCities、getHotCities、getAllCities,将返回的列表赋值给本地ref变量,以驱动UI渲染。 - 实现
selectCity(city)核心函数。它接收城市名作为参数,调用location.js的updateCityInfoByCity获取完整信息,然后调用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)
编码流程
- 在
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 监听这个计算属性的变化,一旦变化就触发天气刷新函数。这样就形成了一个无需手动干预的、响应式的数据闭环。
编码流程
- 在
index.vue的脚本区,从vue导入computed和watch。 - 从
store.js导入getCityInfo。 - 创建一个名为
currentCity的计算属性,其getter函数返回getCityInfo().city。 - 创建一个
watch,监听currentCity。在回调函数中,判断新旧值是否不同,如果不同,则调用updateWeather()函数。 - 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 的设计哲学。