退货
This commit is contained in:
parent
0e870d1fd1
commit
ff2b7f9f7c
|
|
@ -164,7 +164,7 @@
|
|||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<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 === '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>
|
||||
|
|
@ -292,34 +292,85 @@
|
|||
</el-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-item label="收货人">
|
||||
<el-input v-model="editForm.receiverName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="editForm.receiverPhone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="收货地址">
|
||||
<el-input v-model="editForm.receiverAddress" />
|
||||
</el-form-item>
|
||||
<el-divider>商品列表(修改后自动重算价格)</el-divider>
|
||||
<div v-for="(item, idx) in editForm.items" :key="idx" class="order-item-row">
|
||||
<div class="order-item-row__main">
|
||||
<el-select v-model="item.productId" filterable placeholder="选择商品" style="width:220px" @change="onProductChange(item, 'edit')">
|
||||
<el-option v-for="p in allProducts" :key="p.id" :label="`${p.name}(${p.style_no})`" :value="p.id" />
|
||||
</el-select>
|
||||
<el-select v-model="item.specDataId" filterable placeholder="选择规格" style="width:260px" :disabled="!item.productId">
|
||||
<el-option v-for="s in (item._specList || [])" :key="s.id" :label="formatSpecLabel(s)" :value="s.id" />
|
||||
</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-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="收货人">
|
||||
<el-input v-model="editForm.receiverName" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="editForm.receiverPhone" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="收货地址">
|
||||
<el-input v-model="editForm.receiverAddress" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</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>
|
||||
<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>
|
||||
</el-dialog>
|
||||
|
||||
|
|
@ -410,31 +461,27 @@
|
|||
</el-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-table :data="returnItems" size="small" stripe border max-height="320"
|
||||
:header-cell-style="{ background:'#fafafa', fontWeight:600 }"
|
||||
:cell-style="{ padding:'8px 0' }">
|
||||
<el-table-column width="50" align="center">
|
||||
<el-table :data="returnItems" size="small" stripe border max-height="360"
|
||||
:header-cell-style="{ background:'#fafafa', fontWeight:600, fontSize:'12px' }"
|
||||
:cell-style="{ padding:'6px 0', fontSize:'12px' }">
|
||||
<el-table-column width="45" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.checked" :disabled="row.maxReturnQty <= 0" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div style="font-weight:600">{{ row.productName }}</div>
|
||||
<div style="font-size:12px;color:#999;margin-top:2px">
|
||||
<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="productName" label="商品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="styleNo" label="款号" width="90" show-overflow-tooltip />
|
||||
<el-table-column prop="modelName" label="规格名称" width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="barcode" label="条形码" width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="fineness" label="成色" width="80" align="center" />
|
||||
<el-table-column prop="mainStone" label="主石" width="70" align="center" />
|
||||
<el-table-column prop="subStone" label="副石" width="70" align="center" />
|
||||
<el-table-column prop="ringSize" label="手寸" width="60" align="center" />
|
||||
<el-table-column label="单价" width="100" align="right">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
@ -574,11 +621,12 @@ const createForm = ref({
|
|||
const showEditDialog = ref(false)
|
||||
const editing = ref(false)
|
||||
const editingOrderId = ref(0)
|
||||
const editBarcode = ref('')
|
||||
const editPreviewItems = ref<any[]>([])
|
||||
const editForm = ref({
|
||||
receiverName: '',
|
||||
receiverPhone: '',
|
||||
receiverAddress: '',
|
||||
items: [] as { productId: number; specDataId: number; quantity: number; _specList: any[] }[],
|
||||
})
|
||||
|
||||
// Payment dialog
|
||||
|
|
@ -719,11 +767,11 @@ function handleCsvImport(file: File) {
|
|||
const text = raw.replace(/^\uFEFF/, '')
|
||||
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean)
|
||||
const barcodes: string[] = []
|
||||
for (const line of lines) {
|
||||
const val = line.split(',')[0].trim()
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const val = lines[i].split(',')[0].trim()
|
||||
if (!val) continue
|
||||
if (/条[型形]码/i.test(val)) continue
|
||||
if (/^barcode$/i.test(val)) continue
|
||||
// 跳过表头行:第一行或包含中文/header关键字的行
|
||||
if (i === 0 && (/[\u4e00-\u9fa5]/.test(val) || /^barcode$/i.test(val))) continue
|
||||
barcodes.push(val)
|
||||
}
|
||||
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) {
|
||||
editingOrderId.value = row.id
|
||||
editForm.value = {
|
||||
receiverName: row.receiver_name || '',
|
||||
receiverPhone: row.receiver_phone || '',
|
||||
receiverAddress: row.receiver_address || '',
|
||||
items: [],
|
||||
}
|
||||
editPreviewItems.value = []
|
||||
editBarcode.value = ''
|
||||
showEditDialog.value = true
|
||||
try {
|
||||
const res: any = await getOrderDetail(row.id)
|
||||
const detail = res.data
|
||||
if (detail.items && detail.items.length) {
|
||||
const items: any[] = []
|
||||
for (const it of detail.items) {
|
||||
const item: any = {
|
||||
productId: it.product_id,
|
||||
specDataId: it.spec_data_id,
|
||||
quantity: it.quantity,
|
||||
_specList: [],
|
||||
}
|
||||
// Pre-load spec list for this product
|
||||
try {
|
||||
const specRes: any = await getSpecDataList(it.product_id)
|
||||
item._specList = specRes.data || []
|
||||
} catch { /* ignore */ }
|
||||
items.push(item)
|
||||
// 把已有商品转成 previewItems 格式
|
||||
const barcodes = detail.items.map((it: any) => it.barcode).filter(Boolean)
|
||||
if (barcodes.length > 0) {
|
||||
await resolveBarcodesForEdit(barcodes)
|
||||
}
|
||||
// 对于没有 barcode 的(可能 spec_data 已删除),从 item 数据直接构建
|
||||
for (const it of detail.items) {
|
||||
if (!it.barcode || !editPreviewItems.value.find((p: any) => p.barcode === it.barcode)) {
|
||||
editPreviewItems.value.push({
|
||||
barcode: it.barcode || '',
|
||||
productName: it.product_name || '',
|
||||
styleNo: it.style_no || '',
|
||||
modelName: it.model_name || '',
|
||||
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 {
|
||||
ElMessage.error('获取订单详情失败')
|
||||
|
|
@ -805,7 +920,12 @@ async function handleUpdate() {
|
|||
try {
|
||||
const payload = {
|
||||
...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)
|
||||
ElMessage.success('订单更新成功')
|
||||
|
|
@ -984,6 +1104,8 @@ async function handleReturn(row: any) {
|
|||
return {
|
||||
orderItemId: it.id,
|
||||
productName: it.product_name,
|
||||
styleNo: it.style_no,
|
||||
barcode: it.barcode,
|
||||
modelName: it.model_name,
|
||||
fineness: it.fineness,
|
||||
mainStone: it.main_stone,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,21 @@ function toMySQLDatetime(val: string | Date): string {
|
|||
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 {
|
||||
const now = new Date()
|
||||
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 placeholders = orderIds.map(() => '?').join(',')
|
||||
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,
|
||||
sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size, sd.barcode
|
||||
FROM order_items oi
|
||||
|
|
@ -92,6 +107,7 @@ export async function adminGetOrders(req: Request, res: Response): Promise<void>
|
|||
)
|
||||
const itemsMap: Record<number, any[]> = {}
|
||||
for (const item of itemRows) {
|
||||
fillFromSnapshot(item)
|
||||
if (!itemsMap[item.order_id]) itemsMap[item.order_id] = []
|
||||
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[]>(
|
||||
`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,
|
||||
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
|
||||
|
|
@ -136,6 +152,10 @@ export async function adminGetOrderDetail(req: Request, res: Response): Promise<
|
|||
[id]
|
||||
)
|
||||
|
||||
for (const item of itemRows) {
|
||||
fillFromSnapshot(item)
|
||||
}
|
||||
|
||||
res.json({ code: 0, data: { ...orderRows[0], items: itemRows } })
|
||||
} catch (err) {
|
||||
console.error('adminGetOrderDetail error:', err)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user