feat(M09-D4): 完成商品选购与运营工作台
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
const { request, ensureLogin, cents, clientRequestId } = require('../../utils/api.js')
|
||||
|
||||
const ACTIVE_STORAGE = ['STORED', 'PARTIALLY_RETRIEVED']
|
||||
|
||||
Page({
|
||||
data: {
|
||||
storeId: '', activeTab: 'catalog', loading: false, submitting: false,
|
||||
errorMessage: '', catalog: { salesOpen: false, categories: [] },
|
||||
cartItems: [], cartQuantity: 0, cartTotalText: cents(0),
|
||||
orders: [], storages: [], selectedStorage: null,
|
||||
oneTimeCredential: '', oneTimeCredentialTitle: '',
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.setData({
|
||||
storeId: options.storeId || '',
|
||||
activeTab: ['catalog', 'orders', 'storages'].includes(options.tab) ? options.tab : 'catalog',
|
||||
})
|
||||
this.loadAll()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.orders.length || this.data.storages.length) this.loadBusinessLists()
|
||||
},
|
||||
|
||||
async loadAll() {
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
try {
|
||||
await ensureLogin()
|
||||
const jobs = [
|
||||
request('/product-orders?page=1&pageSize=50'),
|
||||
request('/product-storages?page=1&pageSize=50'),
|
||||
]
|
||||
if (this.data.storeId) {
|
||||
jobs.push(request(`/stores/${encodeURIComponent(this.data.storeId)}/product-catalog`))
|
||||
}
|
||||
const [ordersResponse, storagesResponse, catalogResponse] = await Promise.all(jobs)
|
||||
this.setData({
|
||||
orders: (ordersResponse.data.items || []).map(formatOrder),
|
||||
storages: (storagesResponse.data.items || []).map(formatStorage),
|
||||
catalog: catalogResponse ? formatCatalog(catalogResponse.data) : this.data.catalog,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: error.message || '商品服务加载失败' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
async loadBusinessLists() {
|
||||
try {
|
||||
const [ordersResponse, storagesResponse] = await Promise.all([
|
||||
request('/product-orders?page=1&pageSize=50'),
|
||||
request('/product-storages?page=1&pageSize=50'),
|
||||
])
|
||||
this.setData({
|
||||
orders: (ordersResponse.data.items || []).map(formatOrder),
|
||||
storages: (storagesResponse.data.items || []).map(formatStorage),
|
||||
})
|
||||
} catch (error) {
|
||||
this.setData({ errorMessage: error.message || '订单与寄存刷新失败' })
|
||||
}
|
||||
},
|
||||
|
||||
changeTab(event) {
|
||||
this.setData({ activeTab: event.currentTarget.dataset.tab, selectedStorage: null })
|
||||
},
|
||||
|
||||
changeQuantity(event) {
|
||||
const skuId = event.currentTarget.dataset.skuId
|
||||
const delta = Number(event.currentTarget.dataset.delta || 0)
|
||||
const categories = this.data.catalog.categories.map((category) => ({
|
||||
...category,
|
||||
products: category.products.map((product) => ({
|
||||
...product,
|
||||
skus: product.skus.map((sku) => sku.id === skuId
|
||||
? { ...sku, selectedQuantity: Math.max(0, Math.min(99, sku.selectedQuantity + delta)) }
|
||||
: sku),
|
||||
})),
|
||||
}))
|
||||
this.setData({ 'catalog.categories': categories })
|
||||
this.rebuildCart(categories)
|
||||
},
|
||||
|
||||
rebuildCart(categories) {
|
||||
const cartItems = []
|
||||
categories.forEach((category) => category.products.forEach((product) => {
|
||||
product.skus.forEach((sku) => {
|
||||
if (sku.selectedQuantity > 0) cartItems.push({
|
||||
skuId: sku.id, name: `${product.name} · ${sku.name}`,
|
||||
quantity: sku.selectedQuantity, salePriceCents: sku.salePriceCents,
|
||||
})
|
||||
})
|
||||
}))
|
||||
const cartQuantity = cartItems.reduce((sum, item) => sum + item.quantity, 0)
|
||||
const total = cartItems.reduce((sum, item) => sum + item.quantity * item.salePriceCents, 0)
|
||||
this.setData({ cartItems, cartQuantity, cartTotalText: cents(total) })
|
||||
},
|
||||
|
||||
async createOrder() {
|
||||
if (!this.data.storeId || !this.data.cartItems.length || !this.data.catalog.salesOpen) return
|
||||
await this.run(async () => {
|
||||
const response = await request('/product-orders', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
storeId: this.data.storeId, requestId: clientRequestId('goods-order'),
|
||||
fulfillmentMode: 'SELF_SERVICE', roomOrderId: null,
|
||||
note: '顾客小程序自助商品订单',
|
||||
items: this.data.cartItems.map((item) => ({
|
||||
skuId: item.skuId, quantity: item.quantity, note: '',
|
||||
})),
|
||||
},
|
||||
})
|
||||
wx.showToast({ title: '订单已创建', icon: 'success' })
|
||||
this.clearCart()
|
||||
this.setData({ activeTab: 'orders' })
|
||||
await this.loadBusinessLists()
|
||||
if (response.data.status === 'PENDING_PAYMENT') wx.showModal({
|
||||
title: '等待支付',
|
||||
content: '金额已由服务端复算并锁定库存。生产支付入口启用后可继续支付;超时未支付会自动释放库存。',
|
||||
showCancel: false,
|
||||
})
|
||||
}, '商品订单创建失败')
|
||||
},
|
||||
|
||||
clearCart() {
|
||||
const categories = this.data.catalog.categories.map((category) => ({
|
||||
...category,
|
||||
products: category.products.map((product) => ({
|
||||
...product,
|
||||
skus: product.skus.map((sku) => ({ ...sku, selectedQuantity: 0 })),
|
||||
})),
|
||||
}))
|
||||
this.setData({
|
||||
'catalog.categories': categories,
|
||||
cartItems: [], cartQuantity: 0, cartTotalText: cents(0),
|
||||
})
|
||||
},
|
||||
|
||||
async cancelOrder(event) {
|
||||
const order = this.data.orders.find((item) => item.id === event.currentTarget.dataset.orderId)
|
||||
if (!order || !(await confirm(`取消商品订单 ${order.orderNo}?库存与退款会按当前状态处理。`))) return
|
||||
await this.run(async () => {
|
||||
await request(`/product-orders/${encodeURIComponent(order.id)}/cancel`, {
|
||||
method: 'POST',
|
||||
data: { requestId: clientRequestId('goods-cancel'), reason: '顾客小程序取消商品订单' },
|
||||
})
|
||||
wx.showToast({ title: '订单已取消', icon: 'success' })
|
||||
await this.loadBusinessLists()
|
||||
}, '商品订单取消失败')
|
||||
},
|
||||
|
||||
async storeOrder(event) {
|
||||
const orderId = event.currentTarget.dataset.orderId
|
||||
if (!(await confirm('将此已完成商品订单全部转为寄存?领取码只会显示一次。'))) return
|
||||
await this.run(async () => {
|
||||
const response = await request('/product-storages', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
requestId: clientRequestId('goods-storage'), sourceOrderId: orderId,
|
||||
expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
|
||||
},
|
||||
})
|
||||
this.showCredential(response.data.claimCredential, '寄存已创建')
|
||||
this.setData({ activeTab: 'storages' })
|
||||
await this.loadBusinessLists()
|
||||
}, '创建寄存失败')
|
||||
},
|
||||
|
||||
async openStorage(event) {
|
||||
await this.run(async () => {
|
||||
const response = await request(`/product-storages/${encodeURIComponent(event.currentTarget.dataset.storageId)}`)
|
||||
this.setData({ selectedStorage: formatStorageDetail(response.data) })
|
||||
}, '寄存详情加载失败')
|
||||
},
|
||||
|
||||
closeStorage() { this.setData({ selectedStorage: null }) },
|
||||
|
||||
async retrieveItem(event) {
|
||||
const storage = this.data.selectedStorage
|
||||
const storageItemId = event.currentTarget.dataset.itemId
|
||||
const item = storage?.items.find((entry) => entry.id === storageItemId)
|
||||
if (!storage || !item || item.remainingQuantity < 1) return
|
||||
const credential = await prompt('领取码核验', '请输入当前一次性领取码')
|
||||
if (!credential) return
|
||||
const quantity = Number(await prompt(
|
||||
'取出数量', `当前剩余 ${item.remainingQuantity},请输入本次取出数量`, '1'
|
||||
))
|
||||
if (!Number.isInteger(quantity) || quantity < 1 || quantity > item.remainingQuantity) {
|
||||
this.setData({ errorMessage: '取出数量必须是未超过剩余量的正整数' })
|
||||
return
|
||||
}
|
||||
await this.run(async () => {
|
||||
const response = await request(`/product-storages/${encodeURIComponent(storage.id)}/retrieve`, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
requestId: clientRequestId('goods-retrieve'), claimCredential: credential,
|
||||
items: [{ storageItemId, quantity }],
|
||||
},
|
||||
})
|
||||
if (response.data.nextClaimCredential) {
|
||||
this.showCredential(response.data.nextClaimCredential, '部分取出成功,旧码已失效')
|
||||
} else wx.showToast({ title: '取出成功', icon: 'success' })
|
||||
this.setData({ selectedStorage: formatStorageDetail(response.data) })
|
||||
await this.loadBusinessLists()
|
||||
}, '商品取出失败')
|
||||
},
|
||||
|
||||
async rotateCredential() {
|
||||
const storage = this.data.selectedStorage
|
||||
if (!storage || !ACTIVE_STORAGE.includes(storage.status)
|
||||
|| !(await confirm('轮换后当前领取码立即失效,新码只显示一次。继续吗?'))) return
|
||||
await this.run(async () => {
|
||||
const response = await request(`/product-storages/${encodeURIComponent(storage.id)}/credential/rotate`, {
|
||||
method: 'POST', data: { requestId: clientRequestId('goods-rotate') },
|
||||
})
|
||||
this.showCredential(response.data.claimCredential, '领取码已轮换')
|
||||
this.setData({ selectedStorage: formatStorageDetail(response.data) })
|
||||
await this.loadBusinessLists()
|
||||
}, '领取码轮换失败')
|
||||
},
|
||||
|
||||
async cancelStorage() {
|
||||
const storage = this.data.selectedStorage
|
||||
if (!storage || !ACTIVE_STORAGE.includes(storage.status)) return
|
||||
const reason = await prompt('取消寄存', '取消后不能继续领取,剩余商品不会自动回库存。请输入原因。')
|
||||
if (!reason) return
|
||||
await this.run(async () => {
|
||||
const response = await request(`/product-storages/${encodeURIComponent(storage.id)}/cancel`, {
|
||||
method: 'POST', data: { requestId: clientRequestId('goods-storage-cancel'), reason },
|
||||
})
|
||||
this.setData({ selectedStorage: formatStorageDetail(response.data) })
|
||||
await this.loadBusinessLists()
|
||||
wx.showToast({ title: '寄存已取消', icon: 'success' })
|
||||
}, '取消寄存失败')
|
||||
},
|
||||
|
||||
showCredential(value, title) {
|
||||
if (value) this.setData({ oneTimeCredential: value, oneTimeCredentialTitle: title })
|
||||
},
|
||||
copyCredential() {
|
||||
if (this.data.oneTimeCredential) wx.setClipboardData({ data: this.data.oneTimeCredential })
|
||||
},
|
||||
clearCredential() { this.setData({ oneTimeCredential: '', oneTimeCredentialTitle: '' }) },
|
||||
|
||||
async run(work, fallback) {
|
||||
this.setData({ submitting: true, errorMessage: '' })
|
||||
try { await work() } catch (error) {
|
||||
this.setData({ errorMessage: error.message || fallback })
|
||||
} finally { this.setData({ submitting: false }) }
|
||||
},
|
||||
})
|
||||
|
||||
function formatCatalog(catalog) {
|
||||
return {
|
||||
...catalog,
|
||||
categories: (catalog.categories || []).map((category) => ({
|
||||
...category,
|
||||
products: (category.products || []).map((product) => ({
|
||||
...product,
|
||||
skus: (product.skus || []).map((sku) => ({
|
||||
...sku, priceText: cents(sku.salePriceCents), selectedQuantity: 0,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function formatOrder(order) {
|
||||
const labels = {
|
||||
PENDING_PAYMENT: '待支付', PAID: '已支付', ACCEPTED: '已接单', DELIVERING: '配送中',
|
||||
READY_FOR_SELF_SERVICE: '待自取', COMPLETED: '已完成', CANCELLED: '已取消',
|
||||
REFUNDING: '退款中', REFUNDED: '已退款', REFUND_FAILED: '退款失败',
|
||||
}
|
||||
return {
|
||||
...order, statusText: labels[order.status] || order.status,
|
||||
amountText: cents(order.totalAmountCents), createdText: formatDate(order.createdAt),
|
||||
canCancel: ['PENDING_PAYMENT', 'PAID', 'ACCEPTED'].includes(order.status),
|
||||
canStore: order.status === 'COMPLETED',
|
||||
}
|
||||
}
|
||||
|
||||
function formatStorage(storage) {
|
||||
const labels = {
|
||||
STORED: '已寄存', PARTIALLY_RETRIEVED: '部分取出', RETRIEVED: '已取完',
|
||||
EXPIRED: '已过期', CANCELLED: '已取消',
|
||||
}
|
||||
return {
|
||||
...storage, statusText: labels[storage.status] || storage.status,
|
||||
expiresText: formatDate(storage.expiresAt), active: ACTIVE_STORAGE.includes(storage.status),
|
||||
}
|
||||
}
|
||||
|
||||
function formatStorageDetail(storage) {
|
||||
return {
|
||||
...formatStorage(storage), items: (storage.items || []).map((item) => ({ ...item })),
|
||||
movements: (storage.movements || []).map((item) => ({
|
||||
...item, createdText: formatDate(item.createdAt),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '-'
|
||||
const date = new Date(value)
|
||||
const pad = (input) => String(input).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
|
||||
function confirm(content) {
|
||||
return new Promise((resolve) => wx.showModal({
|
||||
title: '请确认', content,
|
||||
success: (result) => resolve(Boolean(result.confirm)), fail: () => resolve(false),
|
||||
}))
|
||||
}
|
||||
|
||||
function prompt(title, placeholderText, defaultValue = '') {
|
||||
return new Promise((resolve) => wx.showModal({
|
||||
title, placeholderText, editable: true, content: defaultValue,
|
||||
success: (result) => resolve(result.confirm ? String(result.content || '').trim() : ''),
|
||||
fail: () => resolve(''),
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"navigationBarTitleText": "商品与寄存"
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<view class="page">
|
||||
<view class="hero">
|
||||
<view>
|
||||
<view class="eyebrow">M09 · 门店增值服务</view>
|
||||
<view class="title">商品与寄存</view>
|
||||
</view>
|
||||
<view class="hero-store">门店 #{{storeId || '-'}}</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view class="tab {{activeTab === 'catalog' ? 'active' : ''}}" data-tab="catalog" bindtap="changeTab">选购</view>
|
||||
<view class="tab {{activeTab === 'orders' ? 'active' : ''}}" data-tab="orders" bindtap="changeTab">商品订单</view>
|
||||
<view class="tab {{activeTab === 'storages' ? 'active' : ''}}" data-tab="storages" bindtap="changeTab">我的寄存</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
|
||||
<view wx:if="{{loading}}" class="notice">正在加载商品服务…</view>
|
||||
|
||||
<view wx:if="{{oneTimeCredential}}" class="credential-card">
|
||||
<view class="credential-title">{{oneTimeCredentialTitle}}</view>
|
||||
<view class="credential-warning">领取码仅展示本次,不会写入本地缓存。请立即妥善保存。</view>
|
||||
<view class="credential-value">{{oneTimeCredential}}</view>
|
||||
<view class="row-actions">
|
||||
<button class="button primary compact" bindtap="copyCredential">复制领取码</button>
|
||||
<button class="button ghost compact" bindtap="clearCredential">我已保存并隐藏</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<block wx:if="{{activeTab === 'catalog'}}">
|
||||
<view wx:if="{{!storeId}}" class="notice">请从门店详情进入商品页,以载入对应门店的商品目录。</view>
|
||||
<view wx:elif="{{!catalog.salesOpen}}" class="notice warning">当前不在商品售卖时段,可查看目录,暂不能提交订单。</view>
|
||||
<block wx:for="{{catalog.categories}}" wx:key="id" wx:for-item="category">
|
||||
<view class="section-title">{{category.name}}</view>
|
||||
<view wx:for="{{category.products}}" wx:key="id" wx:for-item="product" class="product-card">
|
||||
<image wx:if="{{product.coverUrl}}" class="product-cover" src="{{product.coverUrl}}" mode="aspectFill" />
|
||||
<view class="product-main">
|
||||
<view class="product-head">
|
||||
<view>
|
||||
<view class="product-name">{{product.name}}</view>
|
||||
<view class="meta">{{product.unitName}} · {{product.fulfillmentMode === 'DELIVERY' ? '可配送' : '门店自取'}}</view>
|
||||
</view>
|
||||
<view wx:if="{{product.storageEnabled}}" class="chip">支持寄存</view>
|
||||
</view>
|
||||
<view wx:if="{{product.description}}" class="description">{{product.description}}</view>
|
||||
<view wx:for="{{product.skus}}" wx:key="id" wx:for-item="sku" class="sku-row">
|
||||
<view>
|
||||
<view class="sku-name">{{sku.name}}</view>
|
||||
<view class="price">{{sku.priceText}}</view>
|
||||
</view>
|
||||
<view class="quantity-control">
|
||||
<button class="quantity-button" data-sku-id="{{sku.id}}" data-delta="-1" bindtap="changeQuantity">−</button>
|
||||
<text>{{sku.selectedQuantity}}</text>
|
||||
<button class="quantity-button plus" data-sku-id="{{sku.id}}" data-delta="1" bindtap="changeQuantity">+</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view wx:if="{{storeId && !loading && !catalog.categories.length}}" class="empty">当前门店暂无在售商品</view>
|
||||
<view wx:if="{{cartQuantity > 0}}" class="cart-bar">
|
||||
<view><view class="cart-count">已选 {{cartQuantity}} 件</view><view class="cart-total">{{cartTotalText}}</view></view>
|
||||
<button class="button primary cart-submit" loading="{{submitting}}" disabled="{{submitting || !catalog.salesOpen}}" bindtap="createOrder">提交订单</button>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:if="{{activeTab === 'orders'}}">
|
||||
<view wx:for="{{orders}}" wx:key="id" wx:for-item="order" class="business-card">
|
||||
<view class="card-head">
|
||||
<view><view class="card-title">{{order.orderNo}}</view><view class="meta">{{order.createdText}}</view></view>
|
||||
<view class="status">{{order.statusText}}</view>
|
||||
</view>
|
||||
<view class="summary-row"><text>{{order.totalQuantity}} 件 · {{order.fulfillmentMode === 'DELIVERY' ? '配送' : '门店自取'}}</text><strong>{{order.amountText}}</strong></view>
|
||||
<view class="row-actions">
|
||||
<button wx:if="{{order.canCancel}}" class="button ghost compact" data-order-id="{{order.id}}" bindtap="cancelOrder">取消订单</button>
|
||||
<button wx:if="{{order.canStore}}" class="button primary compact" data-order-id="{{order.id}}" bindtap="storeOrder">整单寄存</button>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!loading && !orders.length}}" class="empty">暂无商品订单</view>
|
||||
</block>
|
||||
|
||||
<block wx:if="{{activeTab === 'storages'}}">
|
||||
<view wx:if="{{!selectedStorage}}">
|
||||
<view wx:for="{{storages}}" wx:key="id" wx:for-item="storage" class="business-card tappable" data-storage-id="{{storage.id}}" bindtap="openStorage">
|
||||
<view class="card-head">
|
||||
<view><view class="card-title">{{storage.storageNo}}</view><view class="meta">到期 {{storage.expiresText}}</view></view>
|
||||
<view class="status">{{storage.statusText}}</view>
|
||||
</view>
|
||||
<view class="summary-row"><text>剩余 / 总数</text><strong>{{storage.remainingQuantity}} / {{storage.totalQuantity}}</strong></view>
|
||||
<view class="detail-link">查看寄存明细与取出记录 ›</view>
|
||||
</view>
|
||||
<view wx:if="{{!loading && !storages.length}}" class="empty">暂无商品寄存</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{selectedStorage}}" class="storage-detail">
|
||||
<view class="back" bindtap="closeStorage">‹ 返回寄存列表</view>
|
||||
<view class="card-head">
|
||||
<view><view class="card-title">{{selectedStorage.storageNo}}</view><view class="meta">到期 {{selectedStorage.expiresText}}</view></view>
|
||||
<view class="status">{{selectedStorage.statusText}}</view>
|
||||
</view>
|
||||
<view class="credential-warning">取出时需输入当前领取码;服务端仅保存不可逆摘要。</view>
|
||||
<view wx:for="{{selectedStorage.items}}" wx:key="id" wx:for-item="item" class="storage-item">
|
||||
<view><view class="sku-name">{{item.productName}} · {{item.skuName}}</view><view class="meta">剩余 {{item.remainingQuantity}} / {{item.totalQuantity}}</view></view>
|
||||
<button wx:if="{{selectedStorage.active && item.remainingQuantity > 0}}" class="button primary compact" data-item-id="{{item.id}}" bindtap="retrieveItem">取出</button>
|
||||
</view>
|
||||
<view wx:if="{{selectedStorage.active}}" class="row-actions detail-actions">
|
||||
<button class="button ghost compact" bindtap="rotateCredential">更换领取码</button>
|
||||
<button class="button danger compact" bindtap="cancelStorage">取消寄存</button>
|
||||
</view>
|
||||
<view class="section-title small">取出流水</view>
|
||||
<view wx:for="{{selectedStorage.movements}}" wx:key="id" wx:for-item="movement" class="movement-row">
|
||||
<text>{{movement.createdText}} · 明细 #{{movement.storageItemId}}</text><strong>取出 {{movement.quantity}},剩余 {{movement.remainingAfter}}</strong>
|
||||
</view>
|
||||
<view wx:if="{{!selectedStorage.movements.length}}" class="empty inline">暂无取出记录</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -0,0 +1,52 @@
|
||||
.scrollarea { height: 100vh; background: #f4f6f8; color: #152019; }
|
||||
.page { min-height: 100%; padding: 36rpx 28rpx 180rpx; box-sizing: border-box; }
|
||||
.hero { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 28rpx; }
|
||||
.eyebrow { color: #6a776f; font-size: 22rpx; letter-spacing: 2rpx; }
|
||||
.title { margin-top: 8rpx; font-size: 46rpx; line-height: 1.2; font-weight: 750; }
|
||||
.hero-store { padding: 10rpx 16rpx; border-radius: 999rpx; background: #e4ebe7; color: #506158; font-size: 22rpx; }
|
||||
.tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8rpx; margin-bottom: 24rpx; padding: 8rpx; border-radius: 22rpx; background: #e7ece9; }
|
||||
.tab { padding: 18rpx 8rpx; border-radius: 16rpx; color: #647168; font-size: 26rpx; text-align: center; }
|
||||
.tab.active { background: #fff; color: #16794b; font-weight: 700; box-shadow: 0 6rpx 20rpx rgba(22, 60, 40, .08); }
|
||||
.notice, .error, .empty { margin: 20rpx 0; padding: 28rpx; border-radius: 20rpx; background: #fff; color: #6a756e; text-align: center; }
|
||||
.notice.warning { background: #fff6dd; color: #865b00; }
|
||||
.error { background: #fff0ee; color: #b53b31; text-align: left; }
|
||||
.section-title { margin: 34rpx 4rpx 18rpx; font-size: 30rpx; font-weight: 750; }
|
||||
.section-title.small { margin-top: 36rpx; font-size: 26rpx; }
|
||||
.product-card, .business-card, .storage-detail, .credential-card { margin-bottom: 20rpx; overflow: hidden; border: 1rpx solid #e4e9e6; border-radius: 24rpx; background: #fff; box-shadow: 0 10rpx 30rpx rgba(38, 57, 46, .05); }
|
||||
.product-cover { display: block; width: 100%; height: 280rpx; background: #e6ebe8; }
|
||||
.product-main, .business-card, .storage-detail, .credential-card { padding: 26rpx; }
|
||||
.product-head, .card-head, .summary-row, .storage-item { display: flex; align-items: center; justify-content: space-between; gap: 20rpx; }
|
||||
.product-name, .card-title { font-size: 30rpx; font-weight: 750; }
|
||||
.meta, .description { margin-top: 8rpx; color: #748077; font-size: 23rpx; line-height: 1.5; }
|
||||
.description { margin: 20rpx 0 4rpx; }
|
||||
.chip, .status { flex: 0 0 auto; padding: 8rpx 14rpx; border-radius: 999rpx; background: #e4f3eb; color: #17794c; font-size: 21rpx; }
|
||||
.sku-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 22rpx; padding-top: 22rpx; border-top: 1rpx solid #edf0ee; }
|
||||
.sku-name { font-size: 26rpx; font-weight: 650; }
|
||||
.price { margin-top: 6rpx; color: #d45d32; font-size: 28rpx; font-weight: 750; }
|
||||
.quantity-control { display: flex; align-items: center; gap: 18rpx; }
|
||||
.quantity-button { width: 58rpx; min-width: 58rpx; height: 58rpx; padding: 0; border: 0; border-radius: 18rpx; background: #eef2ef; color: #486052; font-size: 34rpx; line-height: 58rpx; }
|
||||
.quantity-button::after, .button::after { border: 0; }
|
||||
.quantity-button.plus { background: #16794b; color: #fff; }
|
||||
.summary-row { margin-top: 26rpx; padding-top: 22rpx; border-top: 1rpx solid #edf0ee; color: #657269; font-size: 25rpx; }
|
||||
.summary-row strong { color: #18251d; font-size: 28rpx; }
|
||||
.row-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 14rpx; margin-top: 22rpx; }
|
||||
.button { margin: 0; border-radius: 16rpx; font-size: 25rpx; font-weight: 650; }
|
||||
.button.compact { min-width: 150rpx; padding: 0 24rpx; line-height: 70rpx; }
|
||||
.button.primary { background: #16794b; color: #fff; }
|
||||
.button.ghost { background: #edf2ef; color: #315442; }
|
||||
.button.danger { background: #fff0ee; color: #b23f37; }
|
||||
.detail-link, .back { margin-top: 22rpx; color: #16794b; font-size: 24rpx; }
|
||||
.tappable:active { transform: scale(.992); }
|
||||
.credential-card { border-color: #f2d783; background: #fffbeb; }
|
||||
.credential-title { color: #765100; font-size: 28rpx; font-weight: 750; }
|
||||
.credential-warning { margin-top: 10rpx; color: #8b6b1d; font-size: 23rpx; line-height: 1.5; }
|
||||
.credential-value { margin-top: 20rpx; padding: 20rpx; border-radius: 16rpx; background: #fff; font-family: monospace; font-size: 25rpx; line-height: 1.5; word-break: break-all; user-select: all; }
|
||||
.storage-item { margin-top: 22rpx; padding-top: 22rpx; border-top: 1rpx solid #edf0ee; }
|
||||
.detail-actions { justify-content: flex-start; }
|
||||
.movement-row { display: grid; gap: 8rpx; padding: 18rpx 0; border-top: 1rpx solid #edf0ee; color: #6b786f; font-size: 22rpx; }
|
||||
.movement-row strong { color: #27362d; font-size: 24rpx; }
|
||||
.empty.inline { margin: 0; padding: 20rpx; background: #f7f9f8; }
|
||||
.cart-bar { position: fixed; right: 24rpx; bottom: calc(24rpx + env(safe-area-inset-bottom)); left: 24rpx; z-index: 20; display: flex; align-items: center; justify-content: space-between; padding: 22rpx 24rpx; border-radius: 24rpx; background: #13291e; color: #fff; box-shadow: 0 18rpx 50rpx rgba(14, 42, 27, .25); }
|
||||
.cart-count { color: #b8c8bf; font-size: 22rpx; }
|
||||
.cart-total { margin-top: 4rpx; font-size: 32rpx; font-weight: 750; }
|
||||
.cart-submit { min-width: 210rpx; }
|
||||
@@ -40,6 +40,10 @@ Page({
|
||||
openBenefits() {
|
||||
wx.navigateTo({ url: '/pages/benefits/index' })
|
||||
},
|
||||
|
||||
openGoods() {
|
||||
wx.navigateTo({ url: '/pages/goods/index?tab=orders' })
|
||||
},
|
||||
})
|
||||
|
||||
function formatProfile(profile) {
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
</view>
|
||||
<button bindtap="openRecharge">充值</button>
|
||||
<button bindtap="openOrders">查看订单</button>
|
||||
<button bindtap="openGoods">商品订单与寄存</button>
|
||||
</view>
|
||||
|
||||
<view class="section-title ledger-title">最近账单</view>
|
||||
|
||||
@@ -44,6 +44,13 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
openGoods() {
|
||||
if (!this.data.storeId) return
|
||||
wx.navigateTo({
|
||||
url: `/pages/goods/index?storeId=${encodeURIComponent(this.data.storeId)}`,
|
||||
})
|
||||
},
|
||||
|
||||
async loadWifi() {
|
||||
if (!this.data.storeId) return
|
||||
this.setData({ loading: true, errorMessage: '' })
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<view wx:else class="title">门店详情</view>
|
||||
|
||||
<view wx:if="{{errorMessage}}" class="error">{{errorMessage}}</view>
|
||||
<button class="goods-entry" bindtap="openGoods">选购商品 · 查看商品订单与寄存</button>
|
||||
|
||||
<view class="section-title">房间</view>
|
||||
<view wx:if="{{!loading && rooms.length === 0}}" class="empty">暂无可预订房间</view>
|
||||
|
||||
@@ -67,3 +67,9 @@
|
||||
color: #888888;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.goods-entry {
|
||||
margin: 24rpx 0;
|
||||
color: #ffffff;
|
||||
background: #1d4ed8;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user