bug修改
This commit is contained in:
parent
b6011d3c16
commit
658bf0675b
|
|
@ -34,6 +34,9 @@ export function updateOrderStatus(id: number, data: {
|
|||
shippingCompany?: string
|
||||
shippingNo?: string
|
||||
receivedAt?: string
|
||||
cancelReason?: string
|
||||
refundProof?: string
|
||||
refundTime?: string
|
||||
}) {
|
||||
return http.put(`/admin/orders/${id}/status`, data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,40 @@
|
|||
</el-dialog>
|
||||
|
||||
<!-- 筛选配置弹窗 -->
|
||||
<el-dialog v-model="filterDialogVisible" :title="`筛选配置 - ${filterCategoryName}`" width="600px">
|
||||
<div v-for="(f, idx) in filterList" :key="idx" class="filter-item">
|
||||
<div class="filter-item__label">{{ f.filterName }}</div>
|
||||
<div class="tag-input-wrap">
|
||||
<el-tag v-for="(opt, oi) in f.options" :key="oi" closable size="small" @close="f.options.splice(oi, 1)" style="margin: 0 4px 4px 0">{{ opt }}</el-tag>
|
||||
<el-input v-model="f._input" size="small" placeholder="输入后回车添加" style="width:140px" @keyup.enter="addFilterOption(f)" />
|
||||
<el-dialog v-model="filterDialogVisible" :title="`价格区间配置 - ${filterCategoryName}`" width="600px">
|
||||
<div class="price-config">
|
||||
<div class="price-config__tip">配置价格筛选区间,如:1000以下、1000-2999、3000以上</div>
|
||||
<div class="price-config__list">
|
||||
<div v-for="(item, idx) in priceOptions" :key="idx" class="price-config__item">
|
||||
<el-tag closable @close="priceOptions.splice(idx, 1)" style="margin: 0 6px 6px 0">{{ item }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-config__add">
|
||||
<el-select v-model="priceType" size="small" style="width:120px;margin-right:8px">
|
||||
<el-option label="以下" value="below" />
|
||||
<el-option label="区间" value="range" />
|
||||
<el-option label="以上" value="above" />
|
||||
</el-select>
|
||||
<template v-if="priceType === 'below'">
|
||||
<el-input-number v-model="priceVal1" :min="0" size="small" controls-position="right" style="width:120px" />
|
||||
<span style="margin:0 8px;color:#999">以下</span>
|
||||
</template>
|
||||
<template v-else-if="priceType === 'range'">
|
||||
<el-input-number v-model="priceVal1" :min="0" size="small" controls-position="right" style="width:120px" />
|
||||
<span style="margin:0 8px;color:#999">-</span>
|
||||
<el-input-number v-model="priceVal2" :min="0" size="small" controls-position="right" style="width:120px" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-input-number v-model="priceVal1" :min="0" size="small" controls-position="right" style="width:120px" />
|
||||
<span style="margin:0 8px;color:#999">以上</span>
|
||||
</template>
|
||||
<el-button size="small" type="primary" @click="addPriceOption" style="margin-left:8px">添加</el-button>
|
||||
</div>
|
||||
<div class="price-config__note">
|
||||
<div style="margin-top:16px;padding:12px;background:#f0f9ff;border-radius:8px;font-size:13px;color:#666">
|
||||
<div style="font-weight:600;margin-bottom:4px;color:#333">提示</div>
|
||||
成色、主石、副石、手寸筛选项会自动从该分类下商品的规格数据中提取,无需手动配置。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
|
|
@ -130,58 +158,60 @@ async function handleDelete(id: number) {
|
|||
const filterDialogVisible = ref(false)
|
||||
const filterCategoryName = ref('')
|
||||
const filterCategoryId = ref(0)
|
||||
const filterList = ref<any[]>([])
|
||||
const filterSaving = ref(false)
|
||||
|
||||
const FIXED_FILTERS = [
|
||||
{ filterName: '成色', filterKey: 'fineness' },
|
||||
{ filterName: '副石', filterKey: 'side_stone' },
|
||||
{ filterName: '款式', filterKey: 'style' },
|
||||
{ filterName: '镶口', filterKey: 'setting' },
|
||||
{ filterName: '价格', filterKey: 'price' },
|
||||
]
|
||||
const priceOptions = ref<string[]>([])
|
||||
const priceType = ref('range')
|
||||
const priceVal1 = ref(0)
|
||||
const priceVal2 = ref(0)
|
||||
|
||||
async function openFilterDialog(cat: any) {
|
||||
filterCategoryId.value = cat.id
|
||||
filterCategoryName.value = cat.name
|
||||
filterDialogVisible.value = true
|
||||
let savedMap: Record<string, string[]> = {}
|
||||
priceOptions.value = []
|
||||
priceType.value = 'range'
|
||||
priceVal1.value = 0
|
||||
priceVal2.value = 0
|
||||
try {
|
||||
const res: any = await http.get(`/admin/categories/${cat.id}/filters`)
|
||||
for (const f of (res.data || [])) {
|
||||
const key = f.filter_key || f.filterKey
|
||||
const opts = typeof f.options === 'string' ? JSON.parse(f.options) : (f.options || [])
|
||||
savedMap[key] = opts
|
||||
if (key === 'price') {
|
||||
const opts = typeof f.options === 'string' ? JSON.parse(f.options) : (f.options || [])
|
||||
priceOptions.value = opts
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
filterList.value = FIXED_FILTERS.map(f => ({
|
||||
filterName: f.filterName,
|
||||
filterKey: f.filterKey,
|
||||
options: savedMap[f.filterKey] || [],
|
||||
_input: '',
|
||||
}))
|
||||
}
|
||||
|
||||
function addFilter() {
|
||||
// no longer needed
|
||||
}
|
||||
|
||||
function addFilterOption(f: any) {
|
||||
const val = (f._input || '').trim()
|
||||
if (val && !f.options.includes(val)) {
|
||||
f.options.push(val)
|
||||
function addPriceOption() {
|
||||
let label = ''
|
||||
if (priceType.value === 'below') {
|
||||
label = `${priceVal1.value}以下`
|
||||
} else if (priceType.value === 'above') {
|
||||
label = `${priceVal1.value}以上`
|
||||
} else {
|
||||
if (priceVal2.value <= priceVal1.value) {
|
||||
ElMessage.warning('区间上限需大于下限')
|
||||
return
|
||||
}
|
||||
label = `${priceVal1.value}-${priceVal2.value}`
|
||||
}
|
||||
f._input = ''
|
||||
if (!priceOptions.value.includes(label)) {
|
||||
priceOptions.value.push(label)
|
||||
}
|
||||
priceVal1.value = 0
|
||||
priceVal2.value = 0
|
||||
}
|
||||
|
||||
async function saveFilters() {
|
||||
filterSaving.value = true
|
||||
try {
|
||||
const data = filterList.value.map((f: any, i: number) => ({
|
||||
filterName: f.filterName,
|
||||
filterKey: f.filterKey || `filter_${i}`,
|
||||
options: f.options,
|
||||
}))
|
||||
const data = [{
|
||||
filterName: '价格',
|
||||
filterKey: 'price',
|
||||
options: priceOptions.value,
|
||||
}]
|
||||
await http.post(`/admin/categories/${filterCategoryId.value}/filters`, { filters: data })
|
||||
ElMessage.success('保存成功')
|
||||
filterDialogVisible.value = false
|
||||
|
|
@ -200,4 +230,7 @@ onMounted(loadCategories)
|
|||
.filter-item { margin-bottom: 16px; padding: 12px 16px; background: #fafafa; border-radius: 8px; }
|
||||
.filter-item__label { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 8px; }
|
||||
.tag-input-wrap { display: flex; flex-wrap: wrap; align-items: center; }
|
||||
.price-config__tip { font-size: 13px; color: #999; margin-bottom: 12px; }
|
||||
.price-config__list { display: flex; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.price-config__add { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,36 @@
|
|||
ref="orderTableRef">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.items && row.items.length" style="padding:12px 20px">
|
||||
<div v-if="row.items?.length || row.cancel_reason || row.refund_proof" style="padding:12px 20px">
|
||||
<!-- 支付信息 -->
|
||||
<div v-if="row.payment_time || row.payment_proof" style="margin-bottom:16px;padding:12px 16px;background:#f9f9f9;border-radius:8px">
|
||||
<div style="font-size:14px;font-weight:600;color:#333;margin-bottom:8px">支付信息</div>
|
||||
<div style="display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap">
|
||||
<div v-if="row.payment_time" style="font-size:13px;color:#666">
|
||||
<span style="color:#999">支付时间:</span>{{ formatTime(row.payment_time) }}
|
||||
</div>
|
||||
<div v-if="row.payment_proof">
|
||||
<div style="font-size:13px;color:#999;margin-bottom:4px">支付凭证:</div>
|
||||
<el-image :src="row.payment_proof" style="width:120px;height:120px;border-radius:6px" fit="cover" :preview-src-list="[row.payment_proof]" preview-teleported />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 取消/退款信息 -->
|
||||
<div v-if="row.cancel_reason || row.refund_proof || row.refund_time" style="margin-bottom:16px;padding:12px 16px;background:#fff2f0;border-radius:8px">
|
||||
<div style="font-size:14px;font-weight:600;color:#e4393c;margin-bottom:8px">取消/退款信息</div>
|
||||
<div v-if="row.cancel_reason" style="font-size:13px;color:#666;margin-bottom:8px">
|
||||
<span style="color:#999">取消原因:</span>{{ row.cancel_reason }}
|
||||
</div>
|
||||
<div style="display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap">
|
||||
<div v-if="row.refund_time" style="font-size:13px;color:#666">
|
||||
<span style="color:#999">退款时间:</span>{{ formatTime(row.refund_time) }}
|
||||
</div>
|
||||
<div v-if="row.refund_proof">
|
||||
<div style="font-size:13px;color:#999;margin-bottom:4px">退款凭证:</div>
|
||||
<el-image :src="row.refund_proof" style="width:120px;height:120px;border-radius:6px" fit="cover" :preview-src-list="[row.refund_proof]" preview-teleported />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="item in row.items" :key="item.id" style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid #f5f5f5">
|
||||
<el-image :src="getItemThumb(item)" style="width:60px;height:60px;border-radius:6px;flex-shrink:0" fit="cover">
|
||||
<template #error><div style="width:60px;height:60px;background:#f5f5f5;border-radius:6px"></div></template>
|
||||
|
|
@ -75,9 +104,30 @@
|
|||
<span style="color: #e4393c; font-weight: 600">¥{{ Number(row.total_price).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<el-table-column prop="status" label="状态" width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small" round effect="light">{{ statusLabel(row.status) }}</el-tag>
|
||||
<div v-if="row.status === 'cancelled' && row.cancel_reason" style="font-size:11px;color:#e4393c;margin-top:4px;line-height:1.4;word-break:break-all">
|
||||
{{ row.cancel_reason }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付信息" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.payment_proof || row.payment_time" style="display:flex;flex-direction:column;align-items:center;gap:4px">
|
||||
<el-image v-if="row.payment_proof" :src="row.payment_proof" style="width:48px;height:48px;border-radius:4px" fit="cover" :preview-src-list="[row.payment_proof]" preview-teleported />
|
||||
<span v-if="row.payment_time" style="font-size:11px;color:#999">{{ formatTime(row.payment_time) }}</span>
|
||||
</div>
|
||||
<span v-else style="color:#ccc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="退款信息" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.refund_proof || row.refund_time" style="display:flex;flex-direction:column;align-items:center;gap:4px">
|
||||
<el-image v-if="row.refund_proof" :src="row.refund_proof" style="width:48px;height:48px;border-radius:4px" fit="cover" :preview-src-list="[row.refund_proof]" preview-teleported />
|
||||
<span v-if="row.refund_time" style="font-size:11px;color:#999">{{ formatTime(row.refund_time) }}</span>
|
||||
</div>
|
||||
<span v-else style="color:#ccc">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" align="center">
|
||||
|
|
@ -85,11 +135,12 @@
|
|||
{{ formatTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button 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>
|
||||
<el-button v-if="row.status === 'shipped'" text type="success" size="small" @click="handleReceive(row)">确认收货</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -107,7 +158,7 @@
|
|||
</el-card>
|
||||
|
||||
<!-- Create Order Dialog -->
|
||||
<el-dialog v-model="showCreateDialog" title="手动创建订单" width="600px" @close="resetCreateForm">
|
||||
<el-dialog v-model="showCreateDialog" title="手动创建订单" width="680px" @close="resetCreateForm">
|
||||
<el-form :model="createForm" label-width="100px">
|
||||
<el-form-item label="用户 ID" required>
|
||||
<el-input-number v-model="createForm.userId" :min="1" />
|
||||
|
|
@ -122,13 +173,19 @@
|
|||
<el-input v-model="createForm.receiverAddress" />
|
||||
</el-form-item>
|
||||
<el-divider>商品列表</el-divider>
|
||||
<div v-for="(item, idx) in createForm.items" :key="idx" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center">
|
||||
<el-input-number v-model="item.productId" :min="1" placeholder="商品ID" style="width: 130px" />
|
||||
<el-input-number v-model="item.specDataId" :min="1" placeholder="规格ID" style="width: 130px" />
|
||||
<el-input-number v-model="item.quantity" :min="1" placeholder="数量" style="width: 110px" />
|
||||
<el-button text type="danger" @click="createForm.items.splice(idx, 1)" :disabled="createForm.items.length <= 1">删除</el-button>
|
||||
<div v-for="(item, idx) in createForm.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, 'create')">
|
||||
<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-input-number v-model="item.quantity" :min="1" style="width:100px" />
|
||||
<el-button text type="danger" @click="createForm.items.splice(idx, 1)" :disabled="createForm.items.length <= 1">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-button @click="createForm.items.push({ productId: 0, specDataId: 0, quantity: 1 })">添加商品</el-button>
|
||||
<el-button @click="createForm.items.push({ productId: 0, specDataId: 0, quantity: 1, _specList: [] })">+ 添加商品</el-button>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||
|
|
@ -137,7 +194,7 @@
|
|||
</el-dialog>
|
||||
|
||||
<!-- Edit Order Dialog -->
|
||||
<el-dialog v-model="showEditDialog" title="修改订单" width="600px">
|
||||
<el-dialog v-model="showEditDialog" title="修改订单" width="680px">
|
||||
<el-form :model="editForm" label-width="100px">
|
||||
<el-form-item label="收货人">
|
||||
<el-input v-model="editForm.receiverName" />
|
||||
|
|
@ -149,13 +206,19 @@
|
|||
<el-input v-model="editForm.receiverAddress" />
|
||||
</el-form-item>
|
||||
<el-divider>商品列表(修改后自动重算价格)</el-divider>
|
||||
<div v-for="(item, idx) in editForm.items" :key="idx" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center">
|
||||
<el-input-number v-model="item.productId" :min="1" placeholder="商品ID" style="width: 130px" />
|
||||
<el-input-number v-model="item.specDataId" :min="1" placeholder="规格ID" style="width: 130px" />
|
||||
<el-input-number v-model="item.quantity" :min="1" placeholder="数量" style="width: 110px" />
|
||||
<el-button text type="danger" @click="editForm.items.splice(idx, 1)" :disabled="editForm.items.length <= 1">删除</el-button>
|
||||
<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-input-number v-model="item.quantity" :min="1" style="width:100px" />
|
||||
<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 })">添加商品</el-button>
|
||||
<el-button @click="editForm.items.push({ productId: 0, specDataId: 0, quantity: 1, _specList: [] })">+ 添加商品</el-button>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
|
|
@ -218,6 +281,36 @@
|
|||
<el-button type="primary" :loading="updatingStatus" @click="confirmShip">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Cancel Order Dialog -->
|
||||
<el-dialog v-model="showCancelDialog" title="取消订单" width="500px">
|
||||
<el-form :model="cancelForm" label-width="100px">
|
||||
<el-form-item label="取消原因" required>
|
||||
<el-input v-model="cancelForm.cancelReason" type="textarea" :rows="3" placeholder="请填写取消原因" />
|
||||
</el-form-item>
|
||||
<el-form-item label="退款凭证">
|
||||
<el-upload
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleRefundProofSuccess"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-button>上传退款凭证</el-button>
|
||||
</el-upload>
|
||||
<div v-if="cancelForm.refundProof" style="margin-top: 8px">
|
||||
<el-image :src="cancelForm.refundProof" style="width: 120px; height: 120px" fit="cover" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="退款时间">
|
||||
<el-date-picker v-model="cancelForm.refundTime" type="datetime" placeholder="选择退款时间" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCancelDialog = false">取消</el-button>
|
||||
<el-button type="danger" :loading="updatingStatus" @click="confirmCancel">确认取消订单</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -225,6 +318,7 @@
|
|||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getOrders, getOrderDetail, createOrder, updateOrder, updateOrderStatus } from '../../api/order'
|
||||
import { getProducts, getSpecDataList } from '../../api/product'
|
||||
import { getUploadUrl } from '../../api/request'
|
||||
|
||||
const orders = ref<any[]>([])
|
||||
|
|
@ -236,6 +330,37 @@ const pageSize = 10
|
|||
const total = ref(0)
|
||||
const orderTableRef = ref<any>(null)
|
||||
|
||||
// All products for select
|
||||
const allProducts = ref<any[]>([])
|
||||
|
||||
async function loadAllProducts() {
|
||||
try {
|
||||
const res: any = await getProducts({ page: 1, pageSize: 9999 })
|
||||
allProducts.value = res.data.list || []
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function formatSpecLabel(s: any): string {
|
||||
const parts: string[] = []
|
||||
if (s.model_name) parts.push(`型号:${s.model_name}`)
|
||||
if (s.fineness) parts.push(`成色:${s.fineness}`)
|
||||
if (s.main_stone) parts.push(`主石:${s.main_stone}`)
|
||||
if (s.sub_stone) parts.push(`副石:${s.sub_stone}`)
|
||||
if (s.ring_size) parts.push(`手寸:${s.ring_size}`)
|
||||
if (s.total_price) parts.push(`¥${Number(s.total_price).toFixed(2)}`)
|
||||
return parts.join(' | ') || `规格#${s.id}`
|
||||
}
|
||||
|
||||
async function onProductChange(item: any, _mode: string) {
|
||||
item.specDataId = 0
|
||||
item._specList = []
|
||||
if (!item.productId) return
|
||||
try {
|
||||
const res: any = await getSpecDataList(item.productId)
|
||||
item._specList = res.data || []
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Create dialog
|
||||
const showCreateDialog = ref(false)
|
||||
const creating = ref(false)
|
||||
|
|
@ -244,7 +369,7 @@ const createForm = ref({
|
|||
receiverName: '',
|
||||
receiverPhone: '',
|
||||
receiverAddress: '',
|
||||
items: [{ productId: 0, specDataId: 0, quantity: 1 }],
|
||||
items: [{ productId: 0, specDataId: 0, quantity: 1, _specList: [] as any[] }],
|
||||
})
|
||||
|
||||
// Edit dialog
|
||||
|
|
@ -255,7 +380,7 @@ const editForm = ref({
|
|||
receiverName: '',
|
||||
receiverPhone: '',
|
||||
receiverAddress: '',
|
||||
items: [] as { productId: number; specDataId: number; quantity: number }[],
|
||||
items: [] as { productId: number; specDataId: number; quantity: number; _specList: any[] }[],
|
||||
})
|
||||
|
||||
// Payment dialog
|
||||
|
|
@ -268,6 +393,11 @@ const showShipDialog = ref(false)
|
|||
const shipOrderId = ref(0)
|
||||
const shipForm = ref({ shippingCompany: '', shippingNo: '' })
|
||||
|
||||
// Cancel dialog
|
||||
const showCancelDialog = ref(false)
|
||||
const cancelOrderId = ref(0)
|
||||
const cancelForm = ref({ cancelReason: '', refundProof: '', refundTime: '' })
|
||||
|
||||
// Receive dialog
|
||||
const showReceiveDialog = ref(false)
|
||||
const receiveOrderId = ref(0)
|
||||
|
|
@ -326,14 +456,18 @@ function resetCreateForm() {
|
|||
receiverName: '',
|
||||
receiverPhone: '',
|
||||
receiverAddress: '',
|
||||
items: [{ productId: 0, specDataId: 0, quantity: 1 }],
|
||||
items: [{ productId: 0, specDataId: 0, quantity: 1, _specList: [] }],
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
creating.value = true
|
||||
try {
|
||||
await createOrder(createForm.value)
|
||||
const payload = {
|
||||
...createForm.value,
|
||||
items: createForm.value.items.map(({ _specList, ...rest }) => rest),
|
||||
}
|
||||
await createOrder(payload)
|
||||
ElMessage.success('订单创建成功')
|
||||
showCreateDialog.value = false
|
||||
fetchOrders()
|
||||
|
|
@ -344,21 +478,51 @@ async function handleCreate() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
async function handleEdit(row: any) {
|
||||
editingOrderId.value = row.id
|
||||
editForm.value = {
|
||||
receiverName: row.receiver_name || '',
|
||||
receiverPhone: row.receiver_phone || '',
|
||||
receiverAddress: row.receiver_address || '',
|
||||
items: [{ productId: 0, specDataId: 0, quantity: 1 }],
|
||||
items: [],
|
||||
}
|
||||
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)
|
||||
}
|
||||
editForm.value.items = items
|
||||
} else {
|
||||
editForm.value.items = [{ productId: 0, specDataId: 0, quantity: 1, _specList: [] }]
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('获取订单详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
editing.value = true
|
||||
try {
|
||||
await updateOrder(editingOrderId.value, editForm.value)
|
||||
const payload = {
|
||||
...editForm.value,
|
||||
items: editForm.value.items.map(({ _specList, ...rest }) => rest),
|
||||
}
|
||||
await updateOrder(editingOrderId.value, payload)
|
||||
ElMessage.success('订单更新成功')
|
||||
showEditDialog.value = false
|
||||
fetchOrders()
|
||||
|
|
@ -431,6 +595,41 @@ async function confirmShip() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleCancel(row: any) {
|
||||
cancelOrderId.value = row.id
|
||||
cancelForm.value = { cancelReason: '', refundProof: '', refundTime: '' }
|
||||
showCancelDialog.value = true
|
||||
}
|
||||
|
||||
function handleRefundProofSuccess(response: any) {
|
||||
if (response.code === 0) {
|
||||
cancelForm.value.refundProof = response.data.url
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmCancel() {
|
||||
if (!cancelForm.value.cancelReason.trim()) {
|
||||
ElMessage.warning('请填写取消原因')
|
||||
return
|
||||
}
|
||||
updatingStatus.value = true
|
||||
try {
|
||||
await updateOrderStatus(cancelOrderId.value, {
|
||||
status: 'cancelled',
|
||||
cancelReason: cancelForm.value.cancelReason.trim(),
|
||||
refundProof: cancelForm.value.refundProof || undefined,
|
||||
refundTime: cancelForm.value.refundTime ? new Date(cancelForm.value.refundTime).toISOString() : undefined,
|
||||
})
|
||||
ElMessage.success('订单已取消')
|
||||
showCancelDialog.value = false
|
||||
fetchOrders()
|
||||
} catch {
|
||||
ElMessage.error('取消订单失败')
|
||||
} finally {
|
||||
updatingStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleReceive(row: any) {
|
||||
receiveOrderId.value = row.id
|
||||
receiveForm.value = { receivedAt: '' }
|
||||
|
|
@ -474,7 +673,10 @@ function handleRowClick(row: any, _column: any, event: Event) {
|
|||
orderTableRef.value?.toggleRowExpansion(row)
|
||||
}
|
||||
|
||||
onMounted(fetchOrders)
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
loadAllProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -488,4 +690,7 @@ onMounted(fetchOrders)
|
|||
:deep(.el-table th.el-table__cell) { font-size: 13px; }
|
||||
:deep(.el-table td.el-table__cell) { font-size: 13px; }
|
||||
:deep(.el-pagination) { margin-top: 20px; justify-content: flex-end; }
|
||||
.order-item-row { padding: 8px 0; border-bottom: 1px dashed #eee; }
|
||||
.order-item-row:last-child { border-bottom: none; }
|
||||
.order-item-row__main { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 筛选属性 -->
|
||||
<!-- 筛选属性(暂时隐藏)
|
||||
<div class="section-title">筛选属性</div>
|
||||
<el-row :gutter="32">
|
||||
<el-col :span="8">
|
||||
|
|
@ -90,6 +90,7 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
-->
|
||||
|
||||
<!-- 媒体资源 -->
|
||||
<div class="section-title">媒体资源</div>
|
||||
|
|
@ -234,7 +235,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-if="specDataRows.length" :data="specDataRows" size="small" stripe
|
||||
<el-table v-if="specDataRows.length" :data="specDataRows" size="small" stripe border
|
||||
style="width: 100%; margin-bottom: 20px" max-height="500"
|
||||
:header-cell-style="{ background: '#f0f0ff', color: '#1d1e3a', fontWeight: 600, fontSize: '13px', padding: '10px 0' }"
|
||||
:cell-style="{ padding: '8px 0', fontSize: '13px' }">
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ onShow(() => loadOrders())
|
|||
/* 物流信息 */
|
||||
.order-card__shipping {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fafafa;
|
||||
background: #FFFFFF;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -483,6 +483,14 @@ onMounted(() => {
|
|||
.product-item {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
.product-item:last-child {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.product-item__img {
|
||||
width: 160rpx;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// 手动切换后端地址,部署时改成线上域名即可
|
||||
// const BASE_URL = 'http://localhost:3000'
|
||||
const BASE_URL = 'http://115.190.188.216:2850'
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
// const BASE_URL = 'http://115.190.188.216:2850'
|
||||
|
||||
export { BASE_URL }
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ export async function adminGetOrders(req: Request, res: Response): Promise<void>
|
|||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT o.id, o.order_no, o.user_id, u.nickname as user_nickname, o.status,
|
||||
o.total_price, o.receiver_name, o.receiver_phone, o.receiver_address,
|
||||
o.payment_time, o.shipping_company, o.shipping_no,
|
||||
o.payment_time, o.payment_proof, o.shipping_company, o.shipping_no,
|
||||
o.cancel_reason, o.refund_proof, o.refund_time,
|
||||
o.created_at, o.updated_at
|
||||
FROM orders o
|
||||
LEFT JOIN users u ON o.user_id = u.id
|
||||
|
|
@ -84,8 +85,8 @@ export async function adminGetOrders(req: Request, res: Response): Promise<void>
|
|||
p.name as product_name, p.thumb, p.banner_images,
|
||||
sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size
|
||||
FROM order_items oi
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
JOIN spec_data sd ON oi.spec_data_id = sd.id
|
||||
LEFT JOIN products p ON oi.product_id = p.id
|
||||
LEFT JOIN spec_data sd ON oi.spec_data_id = sd.id
|
||||
WHERE oi.order_id IN (${placeholders})`,
|
||||
orderIds
|
||||
)
|
||||
|
|
@ -129,8 +130,8 @@ export async function adminGetOrderDetail(req: Request, res: Response): Promise<
|
|||
p.name as product_name, p.banner_images,
|
||||
sd.model_name, sd.fineness, sd.main_stone, sd.sub_stone, sd.ring_size, sd.total_price as spec_total_price
|
||||
FROM order_items oi
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
JOIN spec_data sd ON oi.spec_data_id = sd.id
|
||||
LEFT JOIN products p ON oi.product_id = p.id
|
||||
LEFT JOIN spec_data sd ON oi.spec_data_id = sd.id
|
||||
WHERE oi.order_id = ?`,
|
||||
[id]
|
||||
)
|
||||
|
|
@ -290,7 +291,7 @@ export async function adminUpdateOrder(req: Request, res: Response): Promise<voi
|
|||
export async function adminUpdateOrderStatus(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { status, paymentTime, paymentProof, shippingCompany, shippingNo, receivedAt } = req.body
|
||||
const { status, paymentTime, paymentProof, shippingCompany, shippingNo, receivedAt, cancelReason, refundProof, refundTime } = req.body
|
||||
|
||||
const [orderRows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id, status FROM orders WHERE id = ?',
|
||||
|
|
@ -306,7 +307,7 @@ export async function adminUpdateOrderStatus(req: Request, res: Response): Promi
|
|||
// Validate status transitions
|
||||
const validTransitions: Record<string, string[]> = {
|
||||
pending: ['paid', 'cancelled'],
|
||||
paid: ['shipped'],
|
||||
paid: ['shipped', 'cancelled'],
|
||||
shipped: ['received'],
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +342,23 @@ export async function adminUpdateOrderStatus(req: Request, res: Response): Promi
|
|||
params.push(toMySQLDatetime(receivedAt || new Date()))
|
||||
}
|
||||
|
||||
if (status === 'cancelled') {
|
||||
if (!cancelReason) {
|
||||
res.status(400).json({ code: 400, message: '请填写取消原因' })
|
||||
return
|
||||
}
|
||||
updates.push('cancel_reason = ?')
|
||||
params.push(cancelReason)
|
||||
if (refundProof) {
|
||||
updates.push('refund_proof = ?')
|
||||
params.push(refundProof)
|
||||
}
|
||||
if (refundTime) {
|
||||
updates.push('refund_time = ?')
|
||||
params.push(toMySQLDatetime(refundTime))
|
||||
}
|
||||
}
|
||||
|
||||
params.push(id)
|
||||
await pool.execute(`UPDATE orders SET ${updates.join(', ')} WHERE id = ?`, params)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ export async function getProducts(req: Request, res: Response): Promise<void> {
|
|||
|
||||
// Filter params
|
||||
const fineness = req.query.fineness as string | undefined
|
||||
const sideStone = req.query.side_stone as string | undefined
|
||||
const style = req.query.style as string | undefined
|
||||
const setting = req.query.setting as string | undefined
|
||||
const mainStone = req.query.mainStone as string | undefined
|
||||
const subStone = req.query.subStone as string | undefined
|
||||
const ringSize = req.query.ringSize as string | undefined
|
||||
const price = req.query.price as string | undefined
|
||||
|
||||
let where = "WHERE p.status = 'on'"
|
||||
|
|
@ -33,25 +33,26 @@ export async function getProducts(req: Request, res: Response): Promise<void> {
|
|||
params.push(kw, kw)
|
||||
}
|
||||
|
||||
// fineness: match against spec_data table
|
||||
// spec_data filters
|
||||
if (fineness) {
|
||||
needJoinSpec = true
|
||||
where += ' AND sd.fineness = ?'
|
||||
params.push(fineness)
|
||||
}
|
||||
|
||||
// side_stone, style, setting: match against products columns
|
||||
if (sideStone) {
|
||||
where += ' AND p.side_stone = ?'
|
||||
params.push(sideStone)
|
||||
if (mainStone) {
|
||||
needJoinSpec = true
|
||||
where += ' AND sd.main_stone = ?'
|
||||
params.push(mainStone)
|
||||
}
|
||||
if (style) {
|
||||
where += ' AND p.style = ?'
|
||||
params.push(style)
|
||||
if (subStone) {
|
||||
needJoinSpec = true
|
||||
where += ' AND sd.sub_stone = ?'
|
||||
params.push(subStone)
|
||||
}
|
||||
if (setting) {
|
||||
where += ' AND p.setting = ?'
|
||||
params.push(setting)
|
||||
if (ringSize) {
|
||||
needJoinSpec = true
|
||||
where += ' AND sd.ring_size = ?'
|
||||
params.push(ringSize)
|
||||
}
|
||||
|
||||
// price: parse range string like "1000以下", "1000-1499", "3000以上"
|
||||
|
|
@ -220,16 +221,61 @@ export async function getCategories(_req: Request, res: Response): Promise<void>
|
|||
export async function getCategoryFilters(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const [rows] = await pool.execute<RowDataPacket[]>(
|
||||
'SELECT id, filter_name AS filterName, filter_key AS filterKey, options, sort FROM category_filters WHERE category_id = ? ORDER BY sort ASC, id ASC',
|
||||
|
||||
// 1. 自动从该分类下商品的 spec_data 提取成色/主石/副石/手寸
|
||||
const [fRows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT DISTINCT sd.fineness FROM spec_data sd
|
||||
JOIN products p ON sd.product_id = p.id
|
||||
WHERE JSON_CONTAINS(p.category_id, ?) AND sd.fineness != '' AND p.status = 'on'
|
||||
ORDER BY sd.fineness`,
|
||||
[JSON.stringify(Number(id))]
|
||||
)
|
||||
const [mRows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT DISTINCT sd.main_stone FROM spec_data sd
|
||||
JOIN products p ON sd.product_id = p.id
|
||||
WHERE JSON_CONTAINS(p.category_id, ?) AND sd.main_stone != '' AND p.status = 'on'
|
||||
ORDER BY sd.main_stone`,
|
||||
[JSON.stringify(Number(id))]
|
||||
)
|
||||
const [sRows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT DISTINCT sd.sub_stone FROM spec_data sd
|
||||
JOIN products p ON sd.product_id = p.id
|
||||
WHERE JSON_CONTAINS(p.category_id, ?) AND sd.sub_stone != '' AND p.status = 'on'
|
||||
ORDER BY sd.sub_stone`,
|
||||
[JSON.stringify(Number(id))]
|
||||
)
|
||||
const [rRows] = await pool.execute<RowDataPacket[]>(
|
||||
`SELECT DISTINCT sd.ring_size FROM spec_data sd
|
||||
JOIN products p ON sd.product_id = p.id
|
||||
WHERE JSON_CONTAINS(p.category_id, ?) AND sd.ring_size != '' AND p.status = 'on'
|
||||
ORDER BY sd.ring_size`,
|
||||
[JSON.stringify(Number(id))]
|
||||
)
|
||||
|
||||
const autoFilters: any[] = []
|
||||
const fineness = fRows.map((r: any) => r.fineness)
|
||||
const mainStone = mRows.map((r: any) => r.main_stone)
|
||||
const subStone = sRows.map((r: any) => r.sub_stone)
|
||||
const ringSize = rRows.map((r: any) => r.ring_size)
|
||||
|
||||
if (fineness.length) autoFilters.push({ filterName: '成色', filterKey: 'fineness', options: fineness })
|
||||
if (mainStone.length) autoFilters.push({ filterName: '主石', filterKey: 'mainStone', options: mainStone })
|
||||
if (subStone.length) autoFilters.push({ filterName: '副石', filterKey: 'subStone', options: subStone })
|
||||
if (ringSize.length) autoFilters.push({ filterName: '手寸', filterKey: 'ringSize', options: ringSize })
|
||||
|
||||
// 2. 从 category_filters 读取价格配置
|
||||
const [priceRows] = await pool.execute<RowDataPacket[]>(
|
||||
"SELECT options FROM category_filters WHERE category_id = ? AND filter_key = 'price'",
|
||||
[id]
|
||||
)
|
||||
// Parse options JSON
|
||||
const data = rows.map((r: any) => ({
|
||||
...r,
|
||||
options: typeof r.options === 'string' ? JSON.parse(r.options) : r.options,
|
||||
}))
|
||||
res.json({ code: 0, data })
|
||||
if (priceRows.length > 0) {
|
||||
const opts = typeof priceRows[0].options === 'string' ? JSON.parse(priceRows[0].options) : priceRows[0].options
|
||||
if (opts && opts.length) {
|
||||
autoFilters.push({ filterName: '价格', filterKey: 'price', options: opts })
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ code: 0, data: autoFilters })
|
||||
} catch (err) {
|
||||
console.error('getCategoryFilters error:', err)
|
||||
res.status(500).json({ code: 500, message: '获取筛选配置失败' })
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user