This commit is contained in:
18631081161 2026-03-31 22:47:18 +08:00
parent 0e870d1fd1
commit ff2b7f9f7c
2 changed files with 211 additions and 69 deletions

View File

@ -164,7 +164,7 @@
</el-table-column> </el-table-column>
<el-table-column label="操作" width="280" fixed="right" align="center"> <el-table-column label="操作" width="280" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button v-if="row.status === 'pending' || row.status === 'paid'" text type="primary" size="small" @click="handleEdit(row)">编辑</el-button> <el-button v-if="row.status === 'pending'" text type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.status === 'pending'" text type="danger" size="small" @click="handlePayment(row)">确认支付</el-button> <el-button v-if="row.status === 'pending'" text type="danger" size="small" @click="handlePayment(row)">确认支付</el-button>
<el-button v-if="row.status === 'paid'" text type="warning" size="small" @click="handleShip(row)">确认发货</el-button> <el-button v-if="row.status === 'paid'" text type="warning" size="small" @click="handleShip(row)">确认发货</el-button>
<el-button v-if="row.status === 'paid'" text type="danger" size="small" @click="handleCancel(row)">取消订单</el-button> <el-button v-if="row.status === 'paid'" text type="danger" size="small" @click="handleCancel(row)">取消订单</el-button>
@ -292,34 +292,85 @@
</el-dialog> </el-dialog>
<!-- Edit Order Dialog --> <!-- Edit Order Dialog -->
<el-dialog v-model="showEditDialog" title="修改订单" width="680px"> <el-dialog v-model="showEditDialog" title="修改订单" width="960px" top="5vh" @close="resetEditForm">
<el-form :model="editForm" label-width="100px"> <el-form :model="editForm" label-width="100px">
<el-form-item label="收货人"> <el-row :gutter="24">
<el-input v-model="editForm.receiverName" /> <el-col :span="8">
</el-form-item> <el-form-item label="收货人">
<el-form-item label="联系电话"> <el-input v-model="editForm.receiverName" />
<el-input v-model="editForm.receiverPhone" /> </el-form-item>
</el-form-item> </el-col>
<el-form-item label="收货地址"> <el-col :span="8">
<el-input v-model="editForm.receiverAddress" /> <el-form-item label="联系电话">
</el-form-item> <el-input v-model="editForm.receiverPhone" />
<el-divider>商品列表修改后自动重算价格</el-divider> </el-form-item>
<div v-for="(item, idx) in editForm.items" :key="idx" class="order-item-row"> </el-col>
<div class="order-item-row__main"> <el-col :span="8">
<el-select v-model="item.productId" filterable placeholder="选择商品" style="width:220px" @change="onProductChange(item, 'edit')"> <el-form-item label="收货地址">
<el-option v-for="p in allProducts" :key="p.id" :label="`${p.name}${p.style_no}`" :value="p.id" /> <el-input v-model="editForm.receiverAddress" />
</el-select> </el-form-item>
<el-select v-model="item.specDataId" filterable placeholder="选择规格" style="width:260px" :disabled="!item.productId"> </el-col>
<el-option v-for="s in (item._specList || [])" :key="s.id" :label="formatSpecLabel(s)" :value="s.id" /> </el-row>
</el-select>
<el-button text type="danger" @click="editForm.items.splice(idx, 1)" :disabled="editForm.items.length <= 1">删除</el-button>
</div>
</div>
<el-button @click="editForm.items.push({ productId: 0, specDataId: 0, quantity: 1, _specList: [] })">+ 添加商品</el-button>
</el-form> </el-form>
<el-divider>商品列表</el-divider>
<div class="create-toolbar">
<div class="create-toolbar__left">
<el-input v-model="editBarcode" placeholder="输入条形码后回车添加" style="width:240px"
@keyup.enter="handleEditAddBarcode" />
<el-button type="primary" plain @click="handleEditAddBarcode" :disabled="!editBarcode.trim()">添加</el-button>
</div>
<div class="create-toolbar__right">
<el-upload :show-file-list="false" accept=".csv,.txt" :before-upload="handleEditCsvImport">
<el-button type="success" plain><el-icon><Upload /></el-icon> CSV</el-button>
</el-upload>
</div>
</div>
<el-table v-if="editPreviewItems.length" :data="editPreviewItems" size="small" stripe border
style="width:100%;margin-top:12px" max-height="400"
:header-cell-style="{ background:'#f5f5ff', fontWeight:600, fontSize:'12px', padding:'8px 0' }"
:cell-style="{ padding:'6px 0', fontSize:'12px' }">
<el-table-column type="index" label="#" width="40" align="center" />
<el-table-column prop="barcode" label="条形码" width="130" show-overflow-tooltip />
<el-table-column prop="productName" label="商品名称" width="140" show-overflow-tooltip />
<el-table-column prop="styleNo" label="款号" width="100" show-overflow-tooltip />
<el-table-column label="规格信息" min-width="240">
<template #default="{ row }">
<div style="line-height:1.6;font-size:12px">
<span v-if="row.modelName">型号:{{ row.modelName }}</span>
<span v-if="row.fineness" style="margin-left:8px">成色:{{ row.fineness }}</span>
<span v-if="row.mainStone" style="margin-left:8px">主石:{{ row.mainStone }}</span>
<span v-if="row.subStone" style="margin-left:8px">副石:{{ row.subStone }}</span>
<span v-if="row.ringSize" style="margin-left:8px">手寸:{{ row.ringSize }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="totalPrice" label="单价" width="90" align="right">
<template #default="{ row }">
<span style="color:#e4393c;font-weight:600">¥{{ Number(row.totalPrice).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template #default="{ $index }">
<el-button type="danger" link size="small" @click="editPreviewItems.splice($index, 1)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="请添加商品" :image-size="50" style="padding:20px 0" />
<div v-if="editPreviewItems.length" style="text-align:right;margin-top:12px;font-size:14px">
<b>{{ editPreviewItems.length }}</b> 件商品合计
<span style="color:#e4393c;font-weight:700;font-size:16px">
¥{{ editPreviewItems.reduce((s, i) => s + Number(i.totalPrice), 0).toFixed(2) }}
</span>
</div>
<template #footer> <template #footer>
<el-button @click="showEditDialog = false">取消</el-button> <el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" :loading="editing" @click="handleUpdate">保存</el-button> <el-button type="primary" :loading="editing" :disabled="editPreviewItems.length === 0" @click="handleUpdate">保存</el-button>
</template> </template>
</el-dialog> </el-dialog>
@ -410,31 +461,27 @@
</el-dialog> </el-dialog>
<!-- Return Order Dialog --> <!-- Return Order Dialog -->
<el-dialog v-model="showReturnDialog" title="退货处理" width="780px" top="5vh"> <el-dialog v-model="showReturnDialog" title="退货处理" width="1060px" top="5vh">
<el-alert v-if="returnOrder_" :title="`订单号: ${returnOrder_.order_no}`" type="info" :closable="false" show-icon style="margin-bottom:16px" /> <el-alert v-if="returnOrder_" :title="`订单号: ${returnOrder_.order_no}`" type="info" :closable="false" show-icon style="margin-bottom:16px" />
<el-table :data="returnItems" size="small" stripe border max-height="320" <el-table :data="returnItems" size="small" stripe border max-height="360"
:header-cell-style="{ background:'#fafafa', fontWeight:600 }" :header-cell-style="{ background:'#fafafa', fontWeight:600, fontSize:'12px' }"
:cell-style="{ padding:'8px 0' }"> :cell-style="{ padding:'6px 0', fontSize:'12px' }">
<el-table-column width="50" align="center"> <el-table-column width="45" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-checkbox v-model="row.checked" :disabled="row.maxReturnQty <= 0" /> <el-checkbox v-model="row.checked" :disabled="row.maxReturnQty <= 0" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品" min-width="200"> <el-table-column prop="productName" label="商品名称" min-width="120" show-overflow-tooltip />
<template #default="{ row }"> <el-table-column prop="styleNo" label="款号" width="90" show-overflow-tooltip />
<div style="font-weight:600">{{ row.productName }}</div> <el-table-column prop="modelName" label="规格名称" width="100" show-overflow-tooltip />
<div style="font-size:12px;color:#999;margin-top:2px"> <el-table-column prop="barcode" label="条形码" width="110" show-overflow-tooltip />
<span v-if="row.modelName">型号:{{ row.modelName }}</span> <el-table-column prop="fineness" label="成色" width="80" align="center" />
<span v-if="row.fineness" style="margin-left:8px">成色:{{ row.fineness }}</span> <el-table-column prop="mainStone" label="主石" width="70" align="center" />
<span v-if="row.mainStone" style="margin-left:8px">主石:{{ row.mainStone }}</span> <el-table-column prop="subStone" label="副石" width="70" align="center" />
<span v-if="row.subStone" style="margin-left:8px">副石:{{ row.subStone }}</span> <el-table-column prop="ringSize" label="手寸" width="60" align="center" />
<span v-if="row.ringSize" style="margin-left:8px">手寸:{{ row.ringSize }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="单价" width="100" align="right"> <el-table-column label="单价" width="100" align="right">
<template #default="{ row }"> <template #default="{ row }">
<span style="color:#e4393c">¥{{ Number(row.unitPrice).toFixed(2) }}</span> <span style="color:#e4393c;font-weight:600">¥{{ Number(row.unitPrice).toFixed(2) }}</span>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -574,11 +621,12 @@ const createForm = ref({
const showEditDialog = ref(false) const showEditDialog = ref(false)
const editing = ref(false) const editing = ref(false)
const editingOrderId = ref(0) const editingOrderId = ref(0)
const editBarcode = ref('')
const editPreviewItems = ref<any[]>([])
const editForm = ref({ const editForm = ref({
receiverName: '', receiverName: '',
receiverPhone: '', receiverPhone: '',
receiverAddress: '', receiverAddress: '',
items: [] as { productId: number; specDataId: number; quantity: number; _specList: any[] }[],
}) })
// Payment dialog // Payment dialog
@ -719,11 +767,11 @@ function handleCsvImport(file: File) {
const text = raw.replace(/^\uFEFF/, '') const text = raw.replace(/^\uFEFF/, '')
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean) const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
const barcodes: string[] = [] const barcodes: string[] = []
for (const line of lines) { for (let i = 0; i < lines.length; i++) {
const val = line.split(',')[0].trim() const val = lines[i].split(',')[0].trim()
if (!val) continue if (!val) continue
if (/条[型形]码/i.test(val)) continue // /header
if (/^barcode$/i.test(val)) continue if (i === 0 && (/[\u4e00-\u9fa5]/.test(val) || /^barcode$/i.test(val))) continue
barcodes.push(val) barcodes.push(val)
} }
if (barcodes.length === 0) { if (barcodes.length === 0) {
@ -763,37 +811,104 @@ async function handleCreate() {
} }
} }
function resetEditForm() {
editForm.value = { receiverName: '', receiverPhone: '', receiverAddress: '' }
editPreviewItems.value = []
editBarcode.value = ''
}
async function resolveBarcodesForEdit(barcodes: string[]) {
const existingBarcodes = new Set(editPreviewItems.value.map((i: any) => i.barcode))
const newBarcodes = barcodes.filter(b => b && !existingBarcodes.has(b))
if (newBarcodes.length === 0) {
ElMessage.info('没有新的条形码需要添加')
return
}
try {
const res: any = await lookupBarcodes(newBarcodes)
const { items, notFound } = res.data
for (const barcode of newBarcodes) {
if (items[barcode]) {
editPreviewItems.value.push({ ...items[barcode], quantity: 1 })
}
}
if (notFound?.length) {
ElMessage.warning(`以下条形码未找到: ${notFound.join(', ')}`)
}
} catch {
ElMessage.error('查询条形码失败')
}
}
async function handleEditAddBarcode() {
const barcode = editBarcode.value.trim()
if (!barcode) return
await resolveBarcodesForEdit([barcode])
editBarcode.value = ''
}
function handleEditCsvImport(file: File) {
const reader = new FileReader()
reader.onload = async (e) => {
const raw = (e.target?.result as string) || ''
const text = raw.replace(/^\uFEFF/, '')
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
const barcodes: string[] = []
for (let i = 0; i < lines.length; i++) {
const val = lines[i].split(',')[0].trim()
if (!val) continue
if (i === 0 && (/[\u4e00-\u9fa5]/.test(val) || /^barcode$/i.test(val))) continue
barcodes.push(val)
}
if (barcodes.length === 0) {
ElMessage.warning('CSV 中未找到有效的条形码')
return
}
await resolveBarcodesForEdit(barcodes)
ElMessage.success(`已解析 ${barcodes.length} 个条形码`)
}
reader.readAsText(file, 'utf-8')
return false
}
async function handleEdit(row: any) { async function handleEdit(row: any) {
editingOrderId.value = row.id editingOrderId.value = row.id
editForm.value = { editForm.value = {
receiverName: row.receiver_name || '', receiverName: row.receiver_name || '',
receiverPhone: row.receiver_phone || '', receiverPhone: row.receiver_phone || '',
receiverAddress: row.receiver_address || '', receiverAddress: row.receiver_address || '',
items: [],
} }
editPreviewItems.value = []
editBarcode.value = ''
showEditDialog.value = true showEditDialog.value = true
try { try {
const res: any = await getOrderDetail(row.id) const res: any = await getOrderDetail(row.id)
const detail = res.data const detail = res.data
if (detail.items && detail.items.length) { if (detail.items && detail.items.length) {
const items: any[] = [] // previewItems
for (const it of detail.items) { const barcodes = detail.items.map((it: any) => it.barcode).filter(Boolean)
const item: any = { if (barcodes.length > 0) {
productId: it.product_id, await resolveBarcodesForEdit(barcodes)
specDataId: it.spec_data_id, }
quantity: it.quantity, // barcode spec_data item
_specList: [], for (const it of detail.items) {
} if (!it.barcode || !editPreviewItems.value.find((p: any) => p.barcode === it.barcode)) {
// Pre-load spec list for this product editPreviewItems.value.push({
try { barcode: it.barcode || '',
const specRes: any = await getSpecDataList(it.product_id) productName: it.product_name || '',
item._specList = specRes.data || [] styleNo: it.style_no || '',
} catch { /* ignore */ } modelName: it.model_name || '',
items.push(item) fineness: it.fineness || '',
mainStone: it.main_stone || '',
subStone: it.sub_stone || '',
ringSize: it.ring_size || '',
totalPrice: it.unit_price || 0,
specDataId: it.spec_data_id,
productId: it.product_id,
quantity: 1,
})
}
} }
editForm.value.items = items
} else {
editForm.value.items = [{ productId: 0, specDataId: 0, quantity: 1, _specList: [] }]
} }
} catch { } catch {
ElMessage.error('获取订单详情失败') ElMessage.error('获取订单详情失败')
@ -805,7 +920,12 @@ async function handleUpdate() {
try { try {
const payload = { const payload = {
...editForm.value, ...editForm.value,
items: editForm.value.items.map(({ _specList, ...rest }) => rest), items: editPreviewItems.value.map((i: any) => ({
barcode: i.barcode || undefined,
productId: i.productId,
specDataId: i.specDataId,
quantity: 1,
})),
} }
await updateOrder(editingOrderId.value, payload) await updateOrder(editingOrderId.value, payload)
ElMessage.success('订单更新成功') ElMessage.success('订单更新成功')
@ -984,6 +1104,8 @@ async function handleReturn(row: any) {
return { return {
orderItemId: it.id, orderItemId: it.id,
productName: it.product_name, productName: it.product_name,
styleNo: it.style_no,
barcode: it.barcode,
modelName: it.model_name, modelName: it.model_name,
fineness: it.fineness, fineness: it.fineness,
mainStone: it.main_stone, mainStone: it.main_stone,

View File

@ -13,6 +13,21 @@ function toMySQLDatetime(val: string | Date): string {
return `${Y}-${M}-${D} ${h}:${m}:${s}` return `${Y}-${M}-${D} ${h}:${m}:${s}`
} }
/**
* If spec_data was deleted (after payment), fill item fields from spec_snapshot.
*/
function fillFromSnapshot(item: any): void {
if (item.model_name || !item.spec_snapshot) return
const snap = typeof item.spec_snapshot === 'string' ? JSON.parse(item.spec_snapshot) : item.spec_snapshot
item.model_name = snap.model_name || null
item.fineness = snap.fineness || null
item.main_stone = snap.main_stone || null
item.sub_stone = snap.sub_stone || null
item.ring_size = snap.ring_size || null
item.barcode = snap.barcode || null
if (!item.style_no && snap.style_no) item.style_no = snap.style_no
}
function generateOrderNo(): string { function generateOrderNo(): string {
const now = new Date() const now = new Date()
const ts = now.getFullYear().toString() + const ts = now.getFullYear().toString() +
@ -81,7 +96,7 @@ export async function adminGetOrders(req: Request, res: Response): Promise<void>
const orderIds = rows.map((r: any) => r.id) const orderIds = rows.map((r: any) => r.id)
const placeholders = orderIds.map(() => '?').join(',') const placeholders = orderIds.map(() => '?').join(',')
const [itemRows] = await pool.execute<RowDataPacket[]>( const [itemRows] = await pool.execute<RowDataPacket[]>(
`SELECT oi.order_id, oi.id, oi.product_id, oi.spec_data_id, oi.quantity, oi.unit_price, `SELECT oi.order_id, oi.id, oi.product_id, oi.spec_data_id, oi.quantity, oi.unit_price, oi.spec_snapshot,
p.name as product_name, p.thumb, p.banner_images, p.style_no, p.name as product_name, p.thumb, p.banner_images, p.style_no,
sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size, sd.barcode sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size, sd.barcode
FROM order_items oi FROM order_items oi
@ -92,6 +107,7 @@ export async function adminGetOrders(req: Request, res: Response): Promise<void>
) )
const itemsMap: Record<number, any[]> = {} const itemsMap: Record<number, any[]> = {}
for (const item of itemRows) { for (const item of itemRows) {
fillFromSnapshot(item)
if (!itemsMap[item.order_id]) itemsMap[item.order_id] = [] if (!itemsMap[item.order_id]) itemsMap[item.order_id] = []
itemsMap[item.order_id].push(item) itemsMap[item.order_id].push(item)
} }
@ -126,7 +142,7 @@ export async function adminGetOrderDetail(req: Request, res: Response): Promise<
} }
const [itemRows] = await pool.execute<RowDataPacket[]>( const [itemRows] = await pool.execute<RowDataPacket[]>(
`SELECT oi.id, oi.product_id, oi.spec_data_id, oi.quantity, oi.unit_price, `SELECT oi.id, oi.product_id, oi.spec_data_id, oi.quantity, oi.unit_price, oi.spec_snapshot,
p.name as product_name, p.banner_images, p.style_no, p.name as product_name, p.banner_images, p.style_no,
sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size, sd.barcode, sd.total_price as spec_total_price sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size, sd.barcode, sd.total_price as spec_total_price
FROM order_items oi FROM order_items oi
@ -136,6 +152,10 @@ export async function adminGetOrderDetail(req: Request, res: Response): Promise<
[id] [id]
) )
for (const item of itemRows) {
fillFromSnapshot(item)
}
res.json({ code: 0, data: { ...orderRows[0], items: itemRows } }) res.json({ code: 0, data: { ...orderRows[0], items: itemRows } })
} catch (err) { } catch (err) {
console.error('adminGetOrderDetail error:', err) console.error('adminGetOrderDetail error:', err)