324 lines
12 KiB
JavaScript
324 lines
12 KiB
JavaScript
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(''),
|
|
}))
|
|
}
|