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(''), })) }