feat(M08-A): 接入顾客端选店下单入口

This commit is contained in:
Codex
2026-06-24 21:26:32 +08:00
parent c90f6e34a1
commit 4ac2c960e4
20 changed files with 734 additions and 32 deletions
+125
View File
@@ -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 })
}
},
})
+37 -1
View File
@@ -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>
+48
View File
@@ -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;
}