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(''),
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user