微信小程序开发(三):城市选择页面开发实战
·
Hi,我是前端人类学(之前叫布兰妮甜)!
在前两篇文章中,我们完成了小程序基础环境的搭建和首页天气展示功能的实现。本文将继续深入开发天气预报小程序的第三个核心功能模块——城市选择页面。
城市选择功能是天气应用中不可或缺的部分,它直接影响用户体验的流畅度和便捷性。一个优秀的城市选择页面应该具备快速搜索、热门城市推荐、定位功能以及清晰的城市列表展示等特性。
通过本文的学习,您将掌握小程序中复杂列表页面的开发技巧,包括:城市搜索功能实现、城市定位、热门城市展示以及历史记录展示和删除历史记录等重要技能。
一、城市选择功能规划与设计
1. 核心功能
- 城市搜索功能:支持实时搜索和模糊匹配
- 热门城市展示:常用城市快速选择
- 定位功能:获取当前所在城市
- 选择历史记录:记忆用户最近选择的城市
- 清除历史记录:清除历史选择记录
2. 页面布局设计
城市选择页面布局:
- 搜索框
- 当前城市定位
- 热门城市展示
- 历史选择以及清除历史
3. 实现细节与代码示例
页面结构(citySelect.wxml)
<view class="city-select">
<!-- 搜索框 -->
<view class="search-bar">
<input placeholder="输入中文城市名" value="{{searchValue}}" bindinput="handleInputChange" class="search-input" />
<text wx:if="{{searchValue}}" bindtap="clearSearch" class="clear-btn">×</text>
</view>
<!-- 搜索结果 -->
<view wx:if="{{showResult}}" class="search-result">
<view wx:for="{{searchResult}}" wx:key="id" bindtap="selectCity" data-city="{{item.name}}" data-id="{{item.id}}" data-adm2="{{item.adm2}}" class="result-item">
<view class="city-name">{{item.name}}</view>
<view class="city-adm2">{{item.fullAdm}}</view>
</view>
<view wx:if="{{searchResult.length === 0 && !isLoading}}" class="no-result">
<image src="/assets/images/no-data.webp" mode="aspectFit"></image>
<view><text>未找到匹配的城市</text></view>
</view>
</view>
<!-- 城市定位 -->
<view wx:if="{{!showResult && currentCity}}" class="section">
<view class="section-title">当前定位</view>
<view class="city-list">
<view bindtap="selectCurrentCity" class="city-item current-city">
<text class="icon-location">📍</text>
<view class="city-info">
<view wx:if="{{currentCity.adm2}}" class="city-adm2">{{currentCity.adm2}}</view>
<view class="city-point">·</view>
<view class="city-name">{{currentCity.name}}</view>
</view>
</view>
</view>
</view>
<!-- 热门城市 -->
<view wx:if="{{!showResult}}" class="section">
<view class="section-title">热门城市</view>
<view class="city-list">
<view wx:for="{{hotCities}}" wx:key="id" bindtap="selectCity" data-city="{{item.name}}" data-adm2="{{item.adm2}}" data-id="{{item.id}}" class="city-item">
{{item.name}}
</view>
</view>
</view>
<view wx:if="{{isLoading}}" class="loading">
<image src="/assets/images/loading.webp" mode="aspectFit"></image>
<text>搜索中...</text>
</view>
<!-- 历史记录 -->
<view wx:if="{{!showResult && historyCities.length > 0}}" class="section">
<view class="section-header">
<view class="section-title">历史选择</view>
<view bindtap="showClearConfirm" class="clear-history-btn">清除历史</view>
</view>
<view class="city-list">
<view wx:for="{{historyCities}}" wx:key="*this" bindtap="selectCity" data-city="{{item.name}}" data-id="{{item.id}}" class="city-item">
<view class="city-name">{{item.name}}</view>
<view wx:if="{{item.adm2}}" class="city-adm2">{{item.adm2}}</view>
</view>
</view>
</view>
<!-- 清除确认对话框 -->
<view wx:if="{{showClearDialog}}" class="dialog-mask">
<view class="clear-dialog">
<view class="dialog-title">确认清除所有历史记录?</view>
<view class="dialog-btns">
<view bindtap="cancelClearHistory" class="dialog-btn cancel-btn">取消</view>
<view bindtap="confirmClearHistory" class="dialog-btn confirm-btn">确定</view>
</view>
</view>
</view>
</view>
页面样式(citySelect.wxss)
.city-select {
padding: 20rpx;
}
/* 搜索框 */
.search-bar {
position: relative;
margin-bottom: 30rpx;
}
.search-input {
height: 80rpx;
background: #f5f5f5;
border-radius: 40rpx;
padding: 0 80rpx 0 30rpx;
font-size: 28rpx;
}
.clear-btn {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
font-size: 40rpx;
color: #999;
}
/* 搜索结果 */
.search-result {
background: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.result-item {
padding: 25rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.no-result {
padding: 30rpx;
text-align: center;
color: #999;
}
.section {
margin-top: 40rpx;
}
.section-title {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
padding-left: 10rpx;
}
.city-list {
display: flex;
flex-wrap: wrap;
}
.city-item {
background: #f5f5f5;
padding: 15rpx 30rpx;
border-radius: 40rpx;
margin: 0 20rpx 20rpx 0;
font-size: 28rpx;
}
/* 城市定位 */
.current-city {
display: flex;
align-items: center;
}
.icon-location {
margin-right: 15rpx;
font-size: 36rpx;
}
.city-info {
display: flex;
flex-direction: row;
align-items: center;
}
.city-info .city-name {
margin-right: 15rpx;
}
.city-info .city-point {
margin: -10rpx 10rpx 0;
font-size: 50rpx;
}
/* 加载状态 */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.loading image {
width: 60rpx;
height: 60rpx;
margin-bottom: 20rpx;
}
/* 历史记录 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.clear-history-btn {
color: #999;
font-size: 24rpx;
padding: 8rpx 16rpx;
border: 1rpx solid #eee;
border-radius: 30rpx;
}
/* 清除对话框样式 */
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.clear-dialog {
width: 80%;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
animation: fadeIn 0.3s;
}
.dialog-title {
padding: 40rpx 30rpx;
text-align: center;
font-size: 32rpx;
}
.dialog-btns {
display: flex;
border-top: 1rpx solid #f5f5f5;
}
.dialog-btn {
flex: 1;
text-align: center;
padding: 25rpx 0;
font-size: 30rpx;
}
.cancel-btn {
border-right: 1rpx solid #f5f5f5;
color: #666;
}
.confirm-btn {
color: #07c160;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
业务逻辑(citySelect.js)
const app = getApp()
Page({
data: {
searchValue: '', // 搜索框内容
hotCities: [ // 热门城市列表
{ name: '北京', adm2: '北京', id: '101010100' },
{ name: '上海', adm2: '上海', id: '101020100' },
{ name: '广州', adm2: '广东', id: '101280101' },
{ name: '深圳', adm2: '广东', id: '101280601' },
{ name: '杭州', adm2: '浙江', id: '101210101' },
{ name: '成都', adm2: '四川', id: '101270101' },
{ name: '武汉', adm2: '湖北', id: '101200101' },
{ name: '南京', adm2: '江苏', id: '101190101' }
],
searchResult: [], // 搜索结果
historyCities: [], // 历史选择城市
showResult: false, // 是否显示搜索结果
showClearDialog: false, // 控制清除确认对话框显示
isLoading: false, // 加载状态
currentCity: null, // 当前定位城市
searchTip: '' // 搜索提示信息
},
onLoad() {
// 从缓存加载历史记录
this.loadHistoryCities()
// 权限检查
this.checkLocationPermission()
// 获取定位城市
this.getLocationCity()
},
// 加载历史选择城市
loadHistoryCities() {
const history = wx.getStorageSync('cityHistory') || []
// 确保读取的是对象数组
this.setData({
historyCities: Array.isArray(history[0]) ? history : history.map(item => {
if (typeof item === 'string') {
const match = item.match(/(.+?) \((.+)\)/)
return match ? { name: match[1], adm2: match[2] } : { name: item }
}
return item
})
})
},
// 检查定位权限
checkLocationPermission() {
const that = this
wx.getSetting({
success(res) {
if (!res.authSetting['scope.userLocation']) {
// 未授权,显示授权提示
that.showLocationAuthDialog()
} else {
// 已授权,开始定位
that.getLocationCity()
}
}
})
},
// 显示定位授权对话框
showLocationAuthDialog() {
wx.showModal({
title: '位置信息权限',
content: '需要获取您的位置信息来提供当地天气服务',
confirmText: '去授权',
cancelText: '手动选择',
success(res) {
if (res.confirm) {
// 打开设置页面
wx.openSetting({
success(settingRes) {
if (settingRes.authSetting['scope.userLocation']) {
that.getLocationCity()
}
}
})
}
}
})
},
// 获取定位城市
getLocationCity() {
const that = this
wx.showLoading({
title: '定位中...',
})
wx.getLocation({
type: 'gcj02',
success: function (res) {
const { longitude, latitude } = res
that.reverseGeocoder(longitude, latitude)
},
fail: function (error) {
wx.hideLoading()
console.error('获取位置失败:', error)
wx.showToast({
title: '定位失败,请手动选择城市',
icon: 'none',
duration: 2000
})
that.setData({
currentCity: null
})
}
})
},
// 逆地理编码
reverseGeocoder(lng, lat) {
const that = this
wx.request({
url: 'https://n657rjvy3a.re.qweatherapi.com/geo/v2/city/lookup',
data: {
location: `${lng},${lat}`,
key: app.globalData.weatherApiKey,
number: 1,
lang: 'zh'
},
success: function (res) {
if (res.data.code == 200 && res.data.location && res.data.location.length > 0) {
const city = res.data.location[0]
that.setData({
currentCity: {
name: city.name,
adm2: city.adm2,
id: city.id
}
})
wx.showToast({
title: '定位成功:' + city.name,
icon: 'success',
duration: 1500
})
} else {
wx.showToast({
title: '定位解析失败',
icon: 'none'
})
}
},
fail: function (error) {
wx.hideLoading()
console.error('逆地理编码失败:', error)
wx.showToast({
title: '定位服务异常',
icon: 'none'
})
}
})
},
// 输入框内容变化(添加防抖)
handleInputChange: debounce(function (e) {
const value = e.detail.value.trim()
this.setData({
searchValue: value,
showResult: value.length > 0,
searchTip: ''
})
if (value) {
this.searchCity(value)
} else {
this.setData({ searchResult: [] })
}
}, 300),
// 搜索城市
searchCity(keyword) {
this.setData({ isLoading: true })
// 首先检查是否是省份搜索
this.handleNormalSearch(keyword)
},
// 处理普通搜索
handleNormalSearch(keyword) {
const that = this
wx.request({
url: 'https://n657rjvy3a.re.qweatherapi.com/geo/v2/city/lookup',
data: {
location: keyword,
key: app.globalData.weatherApiKey,
number: 20,
lang: 'zh'
},
success: res => {
console.log(res);
if (res.data.code == 200 && res.data.location) {
const processedResults = that.processNormalResults(res.data.location, keyword)
that.setData({
searchResult: processedResults,
isLoading: false
})
} else {
that.setData({
searchResult: [],
isLoading: false
})
wx.showToast({
title: '搜索失败,请重试',
icon: 'none'
})
}
},
fail: () => {
that.setData({
searchResult: [],
isLoading: false
})
wx.showToast({
title: '网络错误,请检查连接',
icon: 'none'
})
}
})
},
// 处理普通搜索结果
processNormalResults(locations, keyword) {
if (!locations || !Array.isArray(locations)) return []
return locations.filter(item => /[\u4e00-\u9fa5]/.test(item.name)).map(item => ({
name: item.name,
id: item.id,
adm1: item.adm1 || item.adm,
displayName: item.name // 直接显示城市名称
})).filter((item, index, self) =>
index === self.findIndex(i => i.name === item.name)
).sort((a, b) => {
const aExactMatch = a.name === keyword
const bExactMatch = b.name === keyword
if (aExactMatch && !bExactMatch) return -1
if (!aExactMatch && bExactMatch) return 1
return 0
})
},
// 选择城市
selectCity(e) {
const cityInfo = {
name: e.currentTarget.dataset.city,
adm1: e.currentTarget.dataset.adm1 || '',
id: e.currentTarget.dataset.id || ''
}
// 更新全局当前城市
app.globalData.currentCity = cityInfo.name
// 更新历史记录
this.updateHistory(cityInfo)
// 返回上一页
wx.navigateBack()
},
// 选择定位城市
selectCurrentCity() {
if (this.data.currentCity) {
this.selectCity({
currentTarget: {
dataset: this.data.currentCity
}
})
} else {
wx.showToast({
title: '定位失败,请手动选择',
icon: 'none'
})
}
},
// 更新历史记录
updateHistory(cityInfo) {
let history = wx.getStorageSync('cityHistory') || []
// 去重(比较城市名)
history = history.filter(item => item.name !== cityInfo.name)
// 添加到开头
history.unshift({
name: cityInfo.name,
adm2: cityInfo.adm2,
id: cityInfo.id
})
// 保留最近5个
history = history.slice(0, 5)
wx.setStorageSync('cityHistory', history)
this.setData({ historyCities: history })
},
// 显示清除确认对话框
showClearConfirm() {
if (this.data.historyCities.length === 0) {
wx.showToast({
title: '没有历史记录可清除',
icon: 'none'
})
return
}
this.setData({ showClearDialog: true })
},
// 确认清除历史记录
confirmClearHistory() {
wx.removeStorageSync('cityHistory')
this.setData({
historyCities: [],
showClearDialog: false
})
wx.showToast({
title: '历史记录已清除',
icon: 'success'
})
},
// 取消清除
cancelClearHistory() {
this.setData({ showClearDialog: false })
},
// 清空搜索
clearSearch() {
this.setData({
searchValue: '',
showResult: false,
searchResult: [],
searchTip: ''
})
}
})
// 防抖函数
function debounce(fn, delay) {
let timer = null
return function () {
const context = this
const args = arguments
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
二、性能优化策略
- 防抖处理:减少搜索API请求频率
- 数据缓存:使用本地存储减少重复请求
- 结果过滤:对API返回数据进行去重和排序
- 条件渲染:只在需要时显示搜索结果列表
三、用户体验优化
- 权限引导:友好的定位权限申请流程
- 加载状态:搜索时显示加载反馈
- 空状态处理:无搜索结果时的提示信息
- 历史记录:保存用户选择记录方便再次使用
- 错误处理:网络异常或API错误的用户提示
四、关键技术点
- 组件通信:使用全局变量(app.globalData)传递城市信息
- 页面导航:使用wx.navigateBack()返回上一页
- 数据持久化:使用wx.setStorageSync进行本地存储
- 权限管理:使用wx.getSetting和wx.openSetting处理权限
五、总结与下一步计划
通过本文,我们完成了天气预报小程序的城市选择开发,实现了:
- 城市搜索:展示搜索城市
- 热门城市:常用城市的展示
- 自动定位:定位到当前所在城市
- 历史选择:将用户选择过的城市展示
- 清除历史记录:将用户选择过的城市清除
开发小贴士:
- 经常使用Ctrl/Cmd + S保存文件
- 关注控制台的错误提示
- 使用模拟器的调试功能排查问题
下篇预告: 在下一篇文章中,我将详细介绍10天天气预报页面(forecast)的开发,实现:
- 预警时间和预警信息的展示
- 多日天气预报的展示
- 天气指数分析
如果你觉得文章有用,请点赞👍 + 收藏⭐ + 关注👀 你的支持是我持续创作的最大动力!
📚 系列文章目录
本系列文章将带你从零开始开发一个完整的天气预报小程序,以下是各章节的详细内容:
使用开发者工具创建天气预报项目
首页实现与和风天气API整合
城市选择页面开发实战 (本文)
10天天气预报详情页面实现
设置页面与个性化功能
🔗 相关推荐
有关小程序的更多内容,请看:
更多推荐


所有评论(0)