feat(M08-A): 接入顾客端选店下单入口
This commit is contained in:
@@ -59,6 +59,12 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
openStore(event) {
|
||||
const storeId = event.currentTarget.dataset.storeId
|
||||
if (!storeId) return
|
||||
wx.navigateTo({ url: `/pages/store/detail?storeId=${encodeURIComponent(storeId)}` })
|
||||
},
|
||||
|
||||
async resolveScene(code, sourceType) {
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
</view>
|
||||
<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 wx:for="{{stores}}" wx:key="id" class="store-card" bindtap="openStore" data-store-id="{{item.id}}">
|
||||
<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 class="card-action">查看房间</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.store-card:active {
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
@@ -55,3 +59,9 @@
|
||||
color: #888888;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-action {
|
||||
margin-top: 16rpx;
|
||||
color: #1f6feb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
const {
|
||||
request,
|
||||
ensureLogin,
|
||||
cents,
|
||||
clientRequestId,
|
||||
} = require('../../utils/api.js')
|
||||
|
||||
function defaultStartAt() {
|
||||
const date = new Date(Date.now() + 30 * 60 * 1000)
|
||||
date.setSeconds(0, 0)
|
||||
return date
|
||||
}
|
||||
|
||||
function isoMinutesAfter(date, minutes) {
|
||||
return new Date(date.getTime() + minutes * 60 * 1000).toISOString()
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
storeId: '',
|
||||
roomId: '',
|
||||
loading: false,
|
||||
errorMessage: '',
|
||||
successMessage: '',
|
||||
durationMinutes: 120,
|
||||
pricingMode: 'HOURLY',
|
||||
quote: null,
|
||||
order: null,
|
||||
payment: null,
|
||||
},
|
||||
onLoad(options) {
|
||||
this.setData({
|
||||
@@ -9,4 +34,104 @@ Page({
|
||||
roomId: options.roomId || '',
|
||||
})
|
||||
},
|
||||
|
||||
setDuration(event) {
|
||||
this.setData({
|
||||
durationMinutes: Number(event.currentTarget.dataset.minutes),
|
||||
quote: null,
|
||||
order: null,
|
||||
payment: null,
|
||||
errorMessage: '',
|
||||
successMessage: '',
|
||||
})
|
||||
},
|
||||
|
||||
async quote() {
|
||||
await this.withRequest(async () => {
|
||||
const payload = this.pricingPayload()
|
||||
const response = await request('/pricing/quote', { method: 'POST', data: payload })
|
||||
this.setData({
|
||||
quote: {
|
||||
...response.data,
|
||||
totalText: cents(response.data.totalCents),
|
||||
depositText: cents(response.data.depositCents),
|
||||
},
|
||||
successMessage: '报价已生成',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async reserve() {
|
||||
await this.withRequest(async () => {
|
||||
const payload = this.pricingPayload()
|
||||
const response = await request('/orders/reserve', { method: 'POST', data: payload })
|
||||
this.setData({
|
||||
order: response.data,
|
||||
successMessage: '时段已预占,请继续支付',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async createTestPayment() {
|
||||
if (!this.data.order || !this.data.order.orderId) {
|
||||
this.setData({ errorMessage: '请先预占订单' })
|
||||
return
|
||||
}
|
||||
await this.withRequest(async () => {
|
||||
const response = await request('/payments', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
orderId: this.data.order.orderId,
|
||||
provider: 'TEST',
|
||||
clientRequestId: clientRequestId('miniapp-pay'),
|
||||
},
|
||||
})
|
||||
this.setData({
|
||||
payment: {
|
||||
...response.data,
|
||||
amountText: cents(response.data.amountCents),
|
||||
},
|
||||
successMessage: '测试支付单已创建',
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async completeTestPayment() {
|
||||
if (!this.data.payment || !this.data.payment.paymentId) {
|
||||
this.setData({ errorMessage: '请先创建测试支付单' })
|
||||
return
|
||||
}
|
||||
await this.withRequest(async () => {
|
||||
await request(`/payments/${encodeURIComponent(this.data.payment.paymentId)}/test-complete`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
callbackId: clientRequestId('miniapp-callback'),
|
||||
amountCents: this.data.payment.amountCents,
|
||||
},
|
||||
})
|
||||
this.setData({ successMessage: '测试支付已完成,生产环境将切换微信支付' })
|
||||
})
|
||||
},
|
||||
|
||||
pricingPayload() {
|
||||
const start = defaultStartAt()
|
||||
return {
|
||||
roomId: this.data.roomId,
|
||||
startAt: start.toISOString(),
|
||||
endAt: isoMinutesAfter(start, this.data.durationMinutes),
|
||||
pricingMode: this.data.pricingMode,
|
||||
}
|
||||
},
|
||||
|
||||
async withRequest(work) {
|
||||
this.setData({ loading: true, errorMessage: '', successMessage: '' })
|
||||
try {
|
||||
await ensureLogin()
|
||||
await work()
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: error.message || '操作失败' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,5 +2,41 @@
|
||||
<view class="title">房间详情</view>
|
||||
<view>门店编号:{{storeId}}</view>
|
||||
<view>房间编号:{{roomId}}</view>
|
||||
<view class="notice">扫码或 NFC 只打开此页面,开门权限仍由有效订单单独校验。</view>
|
||||
<view class="section-title">选择时长</view>
|
||||
<view class="duration-row">
|
||||
<button size="mini" class="{{durationMinutes === 60 ? 'selected' : ''}}" data-minutes="60" bindtap="setDuration">1小时</button>
|
||||
<button size="mini" class="{{durationMinutes === 120 ? 'selected' : ''}}" data-minutes="120" bindtap="setDuration">2小时</button>
|
||||
<button size="mini" class="{{durationMinutes === 180 ? 'selected' : ''}}" data-minutes="180" bindtap="setDuration">3小时</button>
|
||||
</view>
|
||||
|
||||
<view class="action-grid">
|
||||
<button loading="{{loading}}" bindtap="quote">报价</button>
|
||||
<button loading="{{loading}}" bindtap="reserve">预占</button>
|
||||
<button loading="{{loading}}" bindtap="createTestPayment">测试支付</button>
|
||||
<button loading="{{loading}}" bindtap="completeTestPayment">完成测试支付</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
|
||||
<view wx:if="{{successMessage}}" class="success">{{successMessage}}</view>
|
||||
|
||||
<view wx:if="{{quote}}" class="result-card">
|
||||
<view class="result-title">报价</view>
|
||||
<view>总价:{{quote.totalText}}</view>
|
||||
<view>押金:{{quote.depositText}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{order}}" class="result-card">
|
||||
<view class="result-title">订单</view>
|
||||
<view>订单编号:{{order.orderId}}</view>
|
||||
<view wx:if="{{order.reservationId}}">预占编号:{{order.reservationId}}</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{payment}}" class="result-card">
|
||||
<view class="result-title">支付</view>
|
||||
<view>支付编号:{{payment.paymentId}}</view>
|
||||
<view>金额:{{payment.amountText}}</view>
|
||||
<view>状态:{{payment.status}}</view>
|
||||
</view>
|
||||
|
||||
<view class="notice">扫码或 NFC 只打开此页面,开门权限仍由有效订单单独校验;生产支付以微信预支付为准。</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -12,3 +14,49 @@
|
||||
margin-top: 32rpx;
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 32rpx 0 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.duration-row,
|
||||
.action-grid {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.action-grid button {
|
||||
flex: 1 1 280rpx;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: #ffffff;
|
||||
background: #1f6feb;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
margin-top: 20rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success {
|
||||
margin: 16rpx 0;
|
||||
color: #17823b;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 16rpx 0;
|
||||
color: #c73535;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,60 @@
|
||||
const { request, ensureLogin, cents } = require('../../utils/api.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
storeId: '',
|
||||
loading: false,
|
||||
errorMessage: '',
|
||||
store: null,
|
||||
rooms: [],
|
||||
wifi: null,
|
||||
},
|
||||
onLoad(options) {
|
||||
this.setData({ storeId: options.storeId || '' })
|
||||
if (options.storeId) this.loadStore(options.storeId)
|
||||
},
|
||||
|
||||
async loadStore(storeId) {
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
const [storeResponse, roomsResponse] = await Promise.all([
|
||||
request(`/stores/${encodeURIComponent(storeId)}`),
|
||||
request(`/stores/${encodeURIComponent(storeId)}/rooms`),
|
||||
])
|
||||
this.setData({
|
||||
store: storeResponse.data,
|
||||
rooms: (roomsResponse.data || []).map((room) => ({
|
||||
...room,
|
||||
basePriceText: cents(room.basePriceCents),
|
||||
depositText: cents(room.depositCents),
|
||||
})),
|
||||
})
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: error.message || '门店加载失败' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
openRoom(event) {
|
||||
const roomId = event.currentTarget.dataset.roomId
|
||||
if (!roomId || !this.data.storeId) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/room/detail?storeId=${encodeURIComponent(this.data.storeId)}&roomId=${encodeURIComponent(roomId)}`,
|
||||
})
|
||||
},
|
||||
|
||||
async loadWifi() {
|
||||
if (!this.data.storeId) return
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
await ensureLogin()
|
||||
const response = await request(`/stores/${encodeURIComponent(this.data.storeId)}/wifi`)
|
||||
this.setData({ wifi: response.data })
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: error.message || '请登录并确认到店权限后查看 Wi-Fi' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
<view class="page">
|
||||
<view class="title">门店详情</view>
|
||||
<view>门店编号:{{storeId}}</view>
|
||||
<view wx:if="{{store}}" class="store-head">
|
||||
<view class="title">{{store.name}}</view>
|
||||
<view class="muted">{{store.city}}{{store.district}} {{store.address}}</view>
|
||||
<view class="{{store.openNow ? 'open' : 'closed'}}">{{store.openNow ? '营业中' : '休息中'}}</view>
|
||||
<view wx:if="{{store.contactPhone}}" class="muted">电话:{{store.contactPhone}}</view>
|
||||
</view>
|
||||
<view wx:else class="title">门店详情</view>
|
||||
|
||||
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
|
||||
|
||||
<view class="section-title">房间</view>
|
||||
<view wx:if="{{!loading && rooms.length === 0}}" class="empty">暂无可预订房间</view>
|
||||
<view wx:for="{{rooms}}" wx:key="id" class="room-card" bindtap="openRoom" data-room-id="{{item.id}}">
|
||||
<view class="room-name">{{item.name}}</view>
|
||||
<view class="muted">{{item.categoryName}} · {{item.capacity}}人 · {{item.roomNo}}</view>
|
||||
<view class="price">{{item.basePriceText}}/小时</view>
|
||||
<view class="muted">押金 {{item.depositText}} · 最少 {{item.minimumMinutes}} 分钟</view>
|
||||
<view class="{{item.operationalStatus === 'AVAILABLE' ? 'open' : 'closed'}}">
|
||||
{{item.operationalStatus === 'AVAILABLE' ? '可预订' : '暂不可订'}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button loading="{{loading}}" bindtap="loadWifi">查看到店 Wi-Fi</button>
|
||||
<view wx:if="{{wifi}}" class="wifi-box">
|
||||
<view>SSID:{{wifi.ssid}}</view>
|
||||
<view>密码:{{wifi.password}}</view>
|
||||
</view>
|
||||
|
||||
<view class="notice">场景码仅用于页面导航,不授予开门或设备控制权限。</view>
|
||||
</view>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.page {
|
||||
padding: 32rpx;
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -12,3 +14,56 @@
|
||||
margin-top: 32rpx;
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
.store-head,
|
||||
.room-card,
|
||||
.wifi-box {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.room-card:active {
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 32rpx 0 16rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.muted {
|
||||
margin-top: 8rpx;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.price {
|
||||
margin-top: 12rpx;
|
||||
color: #b42318;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.open {
|
||||
margin-top: 8rpx;
|
||||
color: #17823b;
|
||||
}
|
||||
|
||||
.closed,
|
||||
.error {
|
||||
margin-top: 8rpx;
|
||||
color: #c73535;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 64rpx 0;
|
||||
color: #888888;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user