From 658bf0675b35aaf0782189efe46cbe5883c3d496 Mon Sep 17 00:00:00 2001 From: 18631081161 <2088094923@qq.com> Date: Thu, 5 Mar 2026 17:36:18 +0800 Subject: [PATCH] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/api/order.ts | 3 + admin/src/views/category/CategoryList.vue | 109 +++++---- admin/src/views/order/OrderList.vue | 255 +++++++++++++++++++--- admin/src/views/product/ProductForm.vue | 5 +- miniprogram/pages/order/list.vue | 2 +- miniprogram/pages/order/submit.vue | 8 + miniprogram/utils/request.ts | 4 +- server/src/controllers/adminOrder.ts | 32 ++- server/src/controllers/product.ts | 92 ++++++-- 9 files changed, 412 insertions(+), 98 deletions(-) diff --git a/admin/src/api/order.ts b/admin/src/api/order.ts index f6b49b6c..d8b2819f 100644 --- a/admin/src/api/order.ts +++ b/admin/src/api/order.ts @@ -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) } diff --git a/admin/src/views/category/CategoryList.vue b/admin/src/views/category/CategoryList.vue index e73f5fc8..3a9bae2f 100644 --- a/admin/src/views/category/CategoryList.vue +++ b/admin/src/views/category/CategoryList.vue @@ -57,12 +57,40 @@ - -
-
{{ f.filterName }}
-
- {{ opt }} - + +
+
配置价格筛选区间,如:1000以下、1000-2999、3000以上
+
+
+ {{ item }} +
+
+
+ + + + + + + + + 添加 +
+
+
+
提示
+ 成色、主石、副石、手寸筛选项会自动从该分类下商品的规格数据中提取,无需手动配置。 +
- + @@ -107,7 +158,7 @@ - + @@ -122,13 +173,19 @@ 商品列表 -
- - - - 删除 +
+
+ + + + + + + + 删除 +
- 添加商品 + + 添加商品 @@ -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([]) @@ -236,6 +330,37 @@ const pageSize = 10 const total = ref(0) const orderTableRef = ref(null) +// All products for select +const allProducts = ref([]) + +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() +}) diff --git a/admin/src/views/product/ProductForm.vue b/admin/src/views/product/ProductForm.vue index a71fafb8..fedc3a2e 100644 --- a/admin/src/views/product/ProductForm.vue +++ b/admin/src/views/product/ProductForm.vue @@ -71,7 +71,7 @@ - +
媒体资源
@@ -234,7 +235,7 @@
- diff --git a/miniprogram/pages/order/list.vue b/miniprogram/pages/order/list.vue index 86dff6c0..e7923e11 100644 --- a/miniprogram/pages/order/list.vue +++ b/miniprogram/pages/order/list.vue @@ -341,7 +341,7 @@ onShow(() => loadOrders()) /* 物流信息 */ .order-card__shipping { padding: 16rpx 24rpx; - background: #fafafa; + background: #FFFFFF; font-size: 24rpx; color: #666; display: flex; diff --git a/miniprogram/pages/order/submit.vue b/miniprogram/pages/order/submit.vue index 9e5844fe..99407ea1 100644 --- a/miniprogram/pages/order/submit.vue +++ b/miniprogram/pages/order/submit.vue @@ -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; diff --git a/miniprogram/utils/request.ts b/miniprogram/utils/request.ts index e3dcde00..4f50c27a 100644 --- a/miniprogram/utils/request.ts +++ b/miniprogram/utils/request.ts @@ -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 } diff --git a/server/src/controllers/adminOrder.ts b/server/src/controllers/adminOrder.ts index e1ccbe9f..ae708852 100644 --- a/server/src/controllers/adminOrder.ts +++ b/server/src/controllers/adminOrder.ts @@ -66,7 +66,8 @@ export async function adminGetOrders(req: Request, res: Response): Promise const [rows] = await pool.execute( `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 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 { 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( '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 = { 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) diff --git a/server/src/controllers/product.ts b/server/src/controllers/product.ts index ee80f906..572fc544 100644 --- a/server/src/controllers/product.ts +++ b/server/src/controllers/product.ts @@ -13,9 +13,9 @@ export async function getProducts(req: Request, res: Response): Promise { // 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 { 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 export async function getCategoryFilters(req: Request, res: Response): Promise { try { const { id } = req.params - const [rows] = await pool.execute( - '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( + `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( + `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( + `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( + `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( + "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: '获取筛选配置失败' })