第一次 基于uniapp的本地生活小程序 城市列表 切换城市列表(组件通信:子传父) 搜索(定位:父相子绝) 服务分类(flex布局+vue组件复用) 店铺列表(flex布局+vue组件复用) 底部导航栏(页面注册与跳转) 详细分析并写出代码
时间: 2025-07-28 18:09:35 浏览: 5
<think>我们正在开发一个基于uniapp的本地生活小程序,主要功能包括:1.城市列表展示与切换(涉及子组件向父组件通信)2.搜索功能实现3.定位功能4.服务分类展示(使用flex布局,父相对子绝对定位)5.店铺列表展示(复用vue组件)6.底部导航栏(页面注册与跳转)我们将按照以下步骤进行:1.创建uniapp项目2.设计页面结构和组件3.实现各个功能模块4.进行组件通信和状态管理下面详细展开:一、项目结构(参考引用[1])根据引用[1]的目录结构,我们主要关注pages、components和static目录。二、页面设计1.首页(index):包含定位/城市选择、搜索框、服务分类、店铺列表2.城市选择页(city):展示城市列表3.其他页面(如个人中心等)由底部导航栏切换三、功能实现1.城市列表展示与切换-创建城市选择页(pages/city/city.vue)-在首页顶部显示当前城市,点击跳转到城市选择页-城市选择页使用列表展示城市,点击城市后返回首页并更新城市关键点:子组件(城市选择页)向父组件(首页)传递选中的城市。解决方案:使用uni.navigateTo跳转时携带参数,或者使用全局事件(如uni.$emit)或Vuex。这里我们使用uni.navigateTo的URL参数传递,并在首页的onShow生命周期中获取。2.搜索功能-在首页顶部放置搜索框(可跳转到搜索页,或直接在当前页搜索)-搜索页(pages/search/search.vue)包含搜索框和搜索结果列表3.定位功能-使用uni.getLocation获取当前位置-将经纬度转换为城市名称(需要逆地址解析,可使用高德或腾讯地图API)4.服务分类展示-使用flex布局,父元素相对定位,子元素绝对定位(如果有特殊布局需求)-横向滚动展示分类图标和名称5.店铺列表-创建店铺卡片组件(components/store-card.vue)-在首页和搜索结果页复用该组件6.底部导航栏-在pages.json中配置tabBar-包含首页、分类、订单、个人中心等四、代码实现1.首页(pages/index/index.vue)```vue<template><view><!--顶部定位和搜索--><viewclass="header"><viewclass="city-select"@click="goToCitySelect"><text>{{currentCity}}</text><uni-iconstype="arrowdown"size="16"></uni-icons></view><viewclass="search-box"@click="goToSearch"><uni-iconstype="search"size="16"></uni-icons><text>请输入商家或服务名称</text></view></view><!--服务分类--><scroll-viewscroll-xclass="category-scroll"><viewv-for="(category,index)incategories":key="index"class="category-item"><image:src="category.icon"class="category-icon"></image><text>{{category.name}}</text></view></scroll-view><!--店铺列表--><viewclass="store-list"><store-cardv-for="storeinstores":key="store.id":store="store"></store-card></view></view></template><script>importStoreCardfrom'@/components/store-card.vue'exportdefault{components:{StoreCard},data(){return{currentCity:'北京市',//默认城市categories:[],//服务分类数据stores:[]//店铺列表数据}},onShow(){//从城市选择页返回时,获取传递的城市constpages=getCurrentPages()constcurrentPage=pages[pages.length-1]if(currentPage.$route.query.city){this.currentCity=currentPage.$route.query.city}},methods:{goToCitySelect(){uni.navigateTo({url:'/pages/city/city'})},goToSearch(){uni.navigateTo({url:'/pages/search/search'})}}}</script><style>.header{display:flex;padding:10px15px;align-items:center;}.city-select{display:flex;align-items:center;margin-right:15px;}.search-box{flex:1;display:flex;align-items:center;background-color:#f5f5f5;border-radius:15px;padding:5px10px;font-size:14px;color:#999;}.category-scroll{white-space:nowrap;width:100%;height:100px;}.category-item{display:inline-block;width:70px;text-align:center;margin:05px;}.category-icon{width:50px;height:50px;border-radius:25px;}.store-list{padding:10px;}</style>```2.城市选择页(pages/city/city.vue)```vue<template><view><viewv-for="(city,index)incityList":key="index"@click="selectCity(city)"><text>{{city}}</text></view></view></template><script>exportdefault{data(){return{cityList:['北京市','上海市','广州市','深圳市','杭州市']}},methods:{selectCity(city){//返回上一页并传递选中的城市constpages=getCurrentPages()constprevPage=pages[pages.length-2]prevPage.$vm.currentCity=cityuni.navigateBack()}}}</script>```3.搜索页(pages/search/search.vue)略,结构类似首页,增加搜索框输入和搜索结果展示。4.定位功能(在首页的onLoad中调用)```javascript//在index.vue的methods中添加getLocation(){uni.getLocation({type:'wgs84',success:(res)=>{//调用逆地址解析API,这里假设有一个方法reverseGeocoderthis.reverseGeocoder(res.latitude,res.longitude)},fail:(err)=>{console.error('定位失败',err)}})},reverseGeocoder(lat,lng){//使用腾讯地图或高德地图的逆地址解析API,这里以腾讯地图为例uni.request({url:'https://apis.map.qq.com/ws/geocoder/v1/',data:{location:`${lat},${lng}`,key:'你的key'},success:(res)=>{if(res.data.status===0){this.currentCity=res.data.result.address_component.city}}})}```5.店铺卡片组件(components/store-card.vue)```vue<template><viewclass="store-card"><image:src="store.logo"class="store-logo"></image><viewclass="store-info"><textclass="store-name">{{store.name}}</text><text>评分:{{store.rating}}</text></view></view></template><script>exportdefault{props:{store:Object}}</script><style>.store-card{display:flex;margin-bottom:10px;padding:10px;background-color:#fff;border-radius:5px;}.store-logo{width:80px;height:80px;margin-right:10px;}.store-info{display:flex;flex-direction:column;}.store-name{font-size:16px;font-weight:bold;}</style>```6.底部导航栏配置(pages.json)```json{"pages":[{"path":"pages/index/index","style":{"navigationBarTitleText":"首页"}},{"path":"pages/category/category","style":{"navigationBarTitleText":"分类"}},{"path":"pages/cart/cart","style":{"navigationBarTitleText":"购物车"}},{"path":"pages/profile/profile","style":{"navigationBarTitleText":"个人中心"}}],"tabBar":{"list":[{"pagePath":"pages/index/index","text":"首页","iconPath":"static/tabbar/home.png","selectedIconPath":"static/tabbar/home_active.png"},{"pagePath":"pages/category/category","text":"分类","iconPath":"static/tabbar/category.png","selectedIconPath":"static/tabbar/category_active.png"},{"pagePath":"pages/cart/cart","text":"购物车","iconPath":"static/tabbar/cart.png","selectedIconPath":"static/tabbar/cart_active.png"},{"pagePath":"pages/profile/profile","text":"我的","iconPath":"static/tabbar/profile.png","selectedIconPath":"static/tabbar/profile_active.png"}]}}```五、注意事项1.组件通信:除了通过URL参数传递,也可以使用Vuex或全局事件总线(uni.$emit和uni.$on)[^2]。2.样式布局:使用flex布局实现自适应,注意父元素相对定位子元素绝对定位的场景(如悬浮按钮)。3.复用组件:店铺卡片在多个页面复用,通过props传递数据。六、扩展1.服务分类数据从后端获取,使用uni.request调用API。2.店铺列表使用上拉加载更多(onReachBottom)和下拉刷新(onPullDownRefresh)。以上代码为示例,实际开发中需要根据具体需求调整。§§相关问题§§1.在uniapp中如何实现更高效的城市列表(如按首字母分组)?2.如何优化店铺列表的图片加载性能?3.如何处理定位失败的情况(如用户拒绝授权)?4.在搜索功能中如何实现搜索建议(联想词)?5.如何实现服务分类的图标懒加载?6.如何管理跨页面的状态(如当前城市)?</think>### 基于uniapp的本地生活小程序实现方案
#### 一、整体架构设计
```mermaid
graph TD
A[首页] --> B[城市选择组件]
A --> C[搜索组件]
A --> D[服务分类组件]
A --> E[店铺列表组件]
F[底部导航] --> G[首页]
F --> H[分类页]
F --> I[订单页]
F --> J[个人中心]
```
#### 二、核心功能实现
##### 1. 城市列表展示与切换
**组件结构:**
```vue
<!-- components/city-selector.vue -->
<template>
<view class="city-selector">
<view class="current-city" @click="showCityList">
{{ currentCity }} <uni-icons type="arrowdown" size="16"></uni-icons>
</view>
<view v-if="showList" class="city-list">
<view
v-for="(city, index) in cities"
:key="index"
@click="selectCity(city)"
>
{{ city.name }}
</view>
</view>
</view>
</template>
<script>
export default {
props: {
cities: Array, // 从父组件传入的城市列表
currentCity: String // 当前选中城市
},
data() {
return {
showList: false
}
},
methods: {
showCityList() {
this.showList = !this.showList;
},
selectCity(city) {
this.showList = false;
this.$emit('cityChange', city); // 触发自定义事件
}
}
}
</script>
```
**在首页使用:**
```vue
<!-- pages/index/index.vue -->
<template>
<view>
<city-selector
:cities="cityList"
:current-city="currentCity"
@cityChange="handleCityChange"
/>
<!-- 其他内容 -->
</view>
</template>
<script>
import CitySelector from '@/components/city-selector.vue';
export default {
components: { CitySelector },
data() {
return {
cityList: [], // 从后端获取的城市数据
currentCity: '北京市'
}
},
methods: {
handleCityChange(city) {
this.currentCity = city.name;
// 刷新店铺列表
this.loadStores(city.id);
}
}
}
</script>
```
##### 2. 搜索功能实现
```vue
<!-- components/search-bar.vue -->
<template>
<view class="search-bar">
<input
type="text"
placeholder="请输入商家或服务名称"
v-model="keyword"
@input="handleInput"
/>
<button @click="search">搜索</button>
</view>
</template>
<script>
export default {
data() {
return {
keyword: ''
}
},
methods: {
handleInput() {
// 实时搜索建议
this.$emit('suggest', this.keyword);
},
search() {
this.$emit('search', this.keyword);
}
}
}
</script>
```
##### 3. 定位功能实现
```vue
<!-- pages/index/index.vue -->
<script>
export default {
methods: {
// 获取当前位置
getLocation() {
uni.getLocation({
type: 'wgs84',
success: (res) => {
this.reverseGeocode(res.latitude, res.longitude);
},
fail: (err) => {
uni.showToast({ title: '定位失败', icon: 'none' });
}
});
},
// 逆地理编码
reverseGeocode(lat, lng) {
// 使用腾讯地图API进行逆地理编码
uni.request({
url: 'https://apis.map.qq.com/ws/geocoder/v1/',
data: {
location: `${lat},${lng}`,
key: 'YOUR_MAP_KEY'
},
success: (res) => {
if (res.data.status === 0) {
this.currentCity = res.data.result.address_component.city;
}
}
});
}
},
onLoad() {
this.getLocation();
}
}
</script>
```
##### 4. 服务分类展示(Flex布局)
```vue
<!-- components/service-categories.vue -->
<template>
<view class="categories-container">
<scroll-view scroll-x class="scroll-view">
<view
v-for="(category, index) in categories"
:key="index"
class="category-item"
:class="{ active: activeIndex === index }"
@click="selectCategory(index)"
>
<image :src="category.icon" class="category-icon"></image>
<text>{{ category.name }}</text>
</view>
</scroll-view>
</view>
</template>
<style>
.categories-container {
position: relative; /* 父元素相对定位 */
padding: 10px 0;
}
.scroll-view {
white-space: nowrap;
}
.category-item {
display: inline-flex;
flex-direction: column;
align-items: center;
width: 70px;
margin: 0 5px;
position: relative; /* 子元素绝对定位 */
}
.category-icon {
width: 50px;
height: 50px;
border-radius: 25px;
}
.active {
color: #ff5000;
font-weight: bold;
}
</style>
```
##### 5. 店铺列表组件(复用)
```vue
<!-- components/store-list.vue -->
<template>
<view class="store-list">
<view
v-for="(store, index) in stores"
:key="index"
class="store-item"
@click="navigateToDetail(store.id)"
>
<image :src="store.logo" class="store-logo"></image>
<view class="store-info">
<text class="store-name">{{ store.name }}</text>
<view class="store-meta">
<text>评分: {{ store.rating }}</text>
<text>距离: {{ store.distance }}km</text>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
stores: Array // 店铺数据
},
methods: {
navigateToDetail(id) {
uni.navigateTo({
url: `/pages/store/detail?id=${id}`
});
}
}
}
</script>
```
##### 6. 底部导航栏实现
**pages.json配置:**
```json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/category/category",
"style": {
"navigationBarTitleText": "分类"
}
},
{
"path": "pages/order/order",
"style": {
"navigationBarTitleText": "订单"
}
},
{
"path": "pages/profile/profile",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#ff5000",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_active.png",
"text": "首页"
},
{
"pagePath": "pages/category/category",
"iconPath": "static/tabbar/category.png",
"selectedIconPath": "static/tabbar/category_active.png",
"text": "分类"
},
{
"pagePath": "pages/order/order",
"iconPath": "static/tabbar/order.png",
"selectedIconPath": "static/tabbar/order_active.png",
"text": "订单"
},
{
"pagePath": "pages/profile/profile",
"iconPath": "static/tabbar/profile.png",
"selectedIconPath": "static/tabbar/profile_active.png",
"text": "我的"
}
]
}
}
```
#### 三、页面跳转与交互
```vue
<!-- 页面跳转示例 -->
<script>
export default {
methods: {
goToCategory() {
uni.switchTab({
url: '/pages/category/category'
});
},
goToStoreDetail(id) {
uni.navigateTo({
url: `/pages/store/detail?id=${id}`
});
}
}
}
</script>
```
#### 四、优化建议
1. **性能优化:**
- 使用`scroll-view`的`lower-threshold`实现上拉加载更多
- 图片使用懒加载:`<image lazy-load>`
- 复杂列表使用虚拟列表
2. **状态管理:**
- 使用Vuex管理全局状态(如当前城市)
```javascript
// store.js
export default new Vuex.Store({
state: {
currentCity: '北京市'
},
mutations: {
setCity(state, city) {
state.currentCity = city;
}
}
});
```
3. **数据缓存:**
```javascript
// 缓存城市数据
uni.setStorageSync('cityList', cityData);
const cachedCities = uni.getStorageSync('cityList');
```
#### 五、与后端API对接
```javascript
// 获取城市列表
getCityList() {
uni.request({
url: 'https://api.example.com/cities',
method: 'GET',
success: (res) => {
if (res.data.code === 200) {
this.cityList = res.data.data;
}
}
});
},
// 获取店铺列表
loadStores(cityId, categoryId) {
uni.showLoading({ title: '加载中' });
uni.request({
url: 'https://api.example.com/stores',
data: {
cityId: cityId,
categoryId: categoryId,
page: this.page,
size: this.pageSize
},
success: (res) => {
this.stores = [...this.stores, ...res.data.data];
this.page++;
},
complete: () => {
uni.hideLoading();
}
});
}
```
### 相关问题
1. **如何实现更高效的城市列表(如按首字母分组)?**
可以使用`computed`属性对城市数据进行分组处理:
```javascript
computed: {
groupedCities() {
const groups = {};
this.cities.forEach(city => {
const firstLetter = city.pinyin.charAt(0).toUpperCase();
if (!groups[firstLetter]) {
groups[firstLetter] = [];
}
groups[firstLetter].push(city);
});
return groups;
}
}
```
2. **如何优化店铺列表的图片加载性能?**
- 使用CDN加速图片加载
- 实现图片懒加载:`<image lazy-load>`
- 使用合适的图片格式(WebP)
- 设置合适的图片尺寸(避免加载过大图片)
3. **如何处理定位失败的情况?**
```javascript
getLocation() {
uni.getLocation({
success: () => { /* ... */ },
fail: (err) => {
if (err.errMsg.includes('auth deny')) {
this.showLocationSettingGuide();
} else {
uni.showToast({ title: '定位失败', icon: 'none' });
}
}
});
},
showLocationSettingGuide() {
uni.showModal({
title: '定位权限被拒绝',
content: '需要您开启定位权限才能获取当前位置',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting();
}
}
});
}
```
4. **如何实现搜索建议(联想词)?**
```javascript
// 在搜索组件中
watch: {
keyword: _.debounce(function(newVal) {
if (newVal.length > 1) {
this.fetchSuggestions(newVal);
}
}, 300)
},
methods: {
fetchSuggestions(keyword) {
uni.request({
url: 'https://api.example.com/suggest',
data: { keyword },
success: (res) => {
this.suggestions = res.data.data;
}
});
}
}
```
5. **如何实现服务分类的图标懒加载?**
```vue
<image
:src="category.icon"
lazy-load
:data-index="index"
@load="onImageLoad"
></image>
<script>
export default {
methods: {
onImageLoad(e) {
const index = e.currentTarget.dataset.index;
// 更新状态,显示加载完成的图标
}
}
}
</script>
```
6. **如何管理跨页面的状态(如当前城市)?**
- 使用Vuex进行全局状态管理
- 使用uni-app的globalData
```javascript
// main.js
Vue.prototype.$globalData = {
currentCity: '北京市'
};
// 页面中使用
this.$globalData.currentCity = '上海市';
```
- 使用本地存储持久化关键状态
```javascript
uni.setStorageSync('currentCity', '广州市');
const city = uni.getStorageSync('currentCity');
```
阅读全文
相关推荐


















