feat(M09-D4): 完成商品选购与运营工作台

This commit is contained in:
Codex
2026-08-11 08:06:13 +08:00
parent 60264cbd11
commit e91b979128
32 changed files with 1065 additions and 60 deletions
+323
View File
@@ -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(''),
}))
}