feat(M03-C): 完成地图选店与距离排序

This commit is contained in:
Codex
2026-06-18 15:15:39 +08:00
parent 9417064e61
commit df373faa82
24 changed files with 614 additions and 132 deletions
+1
View File
@@ -4,4 +4,5 @@ module.exports = {
API_ORIGIN,
APP_API_BASE_URL: `${API_ORIGIN}/app-api`,
UPLOAD_BASE_URL: `${API_ORIGIN}/uploads/`,
WECHAT_APP_ID: 'wx2a5721d5d0fb81a6',
}
+47 -38
View File
@@ -1,46 +1,55 @@
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
const { request } = require('../../utils/api.js')
Page({
data: {
motto: '自助棋牌室',
userInfo: {
avatarUrl: defaultAvatarUrl,
nickName: '',
},
hasUserInfo: false,
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
city: '',
locating: false,
loading: false,
errorMessage: '',
stores: [],
},
bindViewTap() {
wx.navigateTo({
url: '../logs/logs'
onCityInput(event) {
this.setData({ city: event.detail.value })
},
async searchByCity() {
const city = this.data.city.trim()
if (!city) {
this.setData({ errorMessage: '请输入城市名称' })
return
}
await this.loadStores({ city })
},
locateNearby() {
this.setData({ locating: true, errorMessage: '' })
wx.getLocation({
type: 'gcj02',
success: async ({ latitude, longitude }) => {
await this.loadStores({ latitude, longitude })
},
fail: () => {
this.setData({ errorMessage: '定位未授权,请输入城市手工选店' })
},
complete: () => {
this.setData({ locating: false })
},
})
},
onChooseAvatar(e) {
const { avatarUrl } = e.detail
const { nickName } = this.data.userInfo
this.setData({
'userInfo.avatarUrl': avatarUrl,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
onInputChange(e) {
const nickName = e.detail.value
const { avatarUrl } = this.data.userInfo
this.setData({
'userInfo.nickName': nickName,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
getUserProfile() {
wx.getUserProfile({
desc: '用于展示用户头像和昵称',
success: (res) => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
async loadStores(filters) {
this.setData({ loading: true, errorMessage: '' })
try {
const query = Object.entries(filters)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&')
const response = await request(`/stores?${query}`)
this.setData({ stores: response.data || [] })
} catch (error) {
this.setData({ errorMessage: error.message || '门店加载失败' })
} finally {
this.setData({ loading: false })
}
},
})
+14 -20
View File
@@ -1,26 +1,20 @@
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<view class="userinfo">
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</button>
<view class="nickname-wrapper">
<text class="nickname-label">昵称</text>
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
</view>
</block>
<block wx:elif="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile">获取头像昵称</button>
<view wx:else>请升级微信后重试</view>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
<view class="title">选择门店</view>
<button loading="{{locating}}" bindtap="locateNearby">定位附近门店</button>
<view class="city-search">
<input value="{{city}}" placeholder="拒绝定位时输入城市" bindinput="onCityInput" />
<button size="mini" loading="{{loading}}" bindtap="searchByCity">查询</button>
</view>
<view class="usermotto">
<text class="user-motto">{{motto}}</text>
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
<view wx:if="{{!loading && stores.length === 0}}" class="empty">暂无门店</view>
<view wx:for="{{stores}}" wx:key="id" class="store-card">
<view class="store-name">{{item.name}}</view>
<view>{{item.city}}{{item.district}} {{item.address}}</view>
<view wx:if="{{item.distanceMeters !== null}}">距离 {{item.distanceMeters}} 米</view>
<view class="{{item.openNow ? 'open' : 'closed'}}">
{{item.openNow ? '营业中' : '休息中'}}
</view>
</view>
</view>
</scroll-view>
+48 -53
View File
@@ -1,62 +1,57 @@
/**index.wxss**/
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
height: 100vh;
background: #f5f6f8;
}
.userinfo {
.container {
padding: 32rpx;
}
.title {
margin-bottom: 24rpx;
font-size: 40rpx;
font-weight: 600;
}
.city-search {
display: flex;
flex-direction: column;
gap: 16rpx;
align-items: center;
color: #aaa;
width: 80%;
margin: 24rpx 0;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 200px;
}
.avatar-wrapper {
padding: 0;
width: 56px !important;
border-radius: 8px;
margin-top: 40px;
margin-bottom: 40px;
}
.avatar {
display: block;
width: 56px;
height: 56px;
}
.nickname-wrapper {
display: flex;
width: 100%;
padding: 16px;
box-sizing: border-box;
border-top: .5px solid rgba(0, 0, 0, 0.1);
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
color: black;
}
.nickname-label {
width: 105px;
}
.nickname-input {
.city-search input {
flex: 1;
padding: 18rpx 24rpx;
border: 1rpx solid #d7dbe0;
border-radius: 12rpx;
background: #ffffff;
}
.store-card {
margin-top: 20rpx;
padding: 28rpx;
border-radius: 16rpx;
background: #ffffff;
line-height: 1.7;
}
.store-name {
font-size: 34rpx;
font-weight: 600;
}
.open {
color: #17823b;
}
.closed,
.error {
color: #c73535;
}
.empty {
padding: 80rpx 0;
color: #888888;
text-align: center;
}
+25
View File
@@ -0,0 +1,25 @@
const { APP_API_BASE_URL, WECHAT_APP_ID } = require('../config/env.js')
function request(path, options = {}) {
return new Promise((resolve, reject) => {
wx.request({
url: `${APP_API_BASE_URL}${path}`,
method: options.method || 'GET',
data: options.data,
header: {
'x-wechat-appid': WECHAT_APP_ID,
...(options.headers || {}),
},
success(response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
resolve(response.data)
return
}
reject(new Error(response.data?.message || `请求失败:${response.statusCode}`))
},
fail: reject,
})
})
}
module.exports = { request }