diff --git a/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts b/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts index 771527a3..d0fcfc99 100644 --- a/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts +++ b/apps/backend/src/repositories/kuaishou-industry-voucher-repo.ts @@ -227,9 +227,20 @@ export async function listKuaishouIndustryVouchersByTaskId( return result.rows } +export type KuaishouIndustryVoucherAdminListRow = KuaishouIndustryVoucherRow & { + sku_code?: string | null + sku_name?: string | null + task_no?: string | null +} + export async function listKuaishouIndustryVouchersForAdmin( input: KuaishouIndustryVoucherAdminListQuery = {}, -): Promise<{ items: KuaishouIndustryVoucherRow[]; total: number; page: number; pageSize: number }> { +): Promise<{ + items: KuaishouIndustryVoucherAdminListRow[] + total: number + page: number + pageSize: number +}> { const page = normalizePositiveLimit(input.page, 1) const pageSize = Math.min(normalizePositiveLimit(input.pageSize, 50), 200) const where: string[] = [] @@ -238,50 +249,65 @@ export async function listKuaishouIndustryVouchersForAdmin( const oid = String(input.oid || '').trim() if (oid) { params.push(oid) - where.push(`oid = $${params.length}`) + where.push(`v.oid = $${params.length}`) } const voucherCode = String(input.voucherCode || '').trim() if (voucherCode) { params.push(Array.from(new Set([voucherCode, voucherCode.toUpperCase()]))) - where.push(`voucher_code = ANY($${params.length}::text[])`) + where.push(`v.voucher_code = ANY($${params.length}::text[])`) } const taskId = Number(input.taskId || 0) if (Number.isFinite(taskId) && taskId > 0) { params.push(Math.trunc(taskId)) - where.push(`task_id = $${params.length}`) + where.push(`v.task_id = $${params.length}`) } const sellerId = String(input.sellerId || '').trim() if (sellerId) { params.push(sellerId) - where.push(`seller_id = $${params.length}`) + where.push(`v.seller_id = $${params.length}`) } const status = String(input.status || '').trim().toUpperCase() if (status) { params.push(status) - where.push(`status = $${params.length}`) + where.push(`v.status = $${params.length}`) } const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '' const totalResult = await query<{ total: string | number }>( ` SELECT COUNT(*) AS total - FROM kuaishou_industry_vouchers + FROM kuaishou_industry_vouchers v ${whereSql} `, params, ) const listParams = [...params, pageSize, (page - 1) * pageSize] - const listResult = await query( + // 关联任务/订单商品,便于运营后台展示「买的是什么」 + const listResult = await query( ` - SELECT * - FROM kuaishou_industry_vouchers + SELECT + v.*, + COALESCE(oi_task.sku_code, oi_order.sku_code, '') AS sku_code, + COALESCE(oi_task.sku_name, oi_order.sku_name, '') AS sku_name, + COALESCE(ft.task_no, '') AS task_no + FROM kuaishou_industry_vouchers v + LEFT JOIN fulfillment_tasks ft ON ft.id = v.task_id + LEFT JOIN order_items oi_task ON oi_task.id = ft.order_item_id + LEFT JOIN LATERAL ( + SELECT oi.sku_code, oi.sku_name + FROM order_items oi + WHERE v.order_id IS NOT NULL + AND oi.order_id = v.order_id + ORDER BY oi.id ASC + LIMIT 1 + ) oi_order ON true ${whereSql} - ORDER BY updated_at DESC, id DESC + ORDER BY v.updated_at DESC, v.id DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2} `, diff --git a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts index 71b6fa06..0cfbd5fb 100644 --- a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts +++ b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts @@ -6,6 +6,7 @@ import { listKuaishouIndustryVouchersForAdmin, updateKuaishouIndustryVoucherByCode, type KuaishouIndustryVoucherAdminListQuery, + type KuaishouIndustryVoucherAdminListRow, } from '../../repositories/kuaishou-industry-voucher-repo.js' import { getOrderById } from '../../repositories/order-repo.js' import { getTaskById } from '../../repositories/task-repo.js' @@ -395,19 +396,23 @@ function sanitizeOpenApiRequest(request: JsonObject | undefined): JsonObject | n } function mapAdminKuaishouIndustryVoucher( - voucher: KuaishouIndustryVoucherRow, + voucher: KuaishouIndustryVoucherRow | KuaishouIndustryVoucherAdminListRow, shopNameBySellerId: Map = buildKuaishouIndustryShopNameMap(), ) { const sellerId = String(voucher.seller_id || '').trim() + const listRow = voucher as KuaishouIndustryVoucherAdminListRow return { id: voucher.id, voucherCode: voucher.voucher_code, oid: voucher.oid, orderId: voucher.order_id, taskId: voucher.task_id, + taskNo: String(listRow.task_no || '').trim(), unitIndex: voucher.unit_index, sellerId, shopName: shopNameBySellerId.get(sellerId) || '', + skuCode: String(listRow.sku_code || '').trim(), + skuName: String(listRow.sku_name || '').trim(), tokenMasked: maskSecret(voucher.token), status: voucher.status, validStartTime: Number(voucher.valid_start_time || 0) || 0, diff --git a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx index cc6b374e..fc148f31 100644 --- a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx +++ b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx @@ -207,10 +207,10 @@ export default function AdminKuaishouIndustryPage() { () => [ { title: '券码', - minWidth: 220, + width: 168, render: (_, row) => (
- + {row.voucherCode} 序号:{row.unitIndex} @@ -218,20 +218,44 @@ export default function AdminKuaishouIndustryPage() { ), }, { - title: '订单 / 任务', - minWidth: 220, - render: (_, row) => ( -
- {row.oid || '-'} - - 任务:{row.taskId || '-'} / 订单:{row.orderId || '-'} - -
- ), + title: '订单商品', + // 主列自适应:订单号 + 商品名,避免额外「商品」列占宽 + ellipsis: true, + render: (_, row) => { + const skuName = String(row.skuName || '').trim() + const skuCode = String(row.skuCode || '').trim() + const productLabel = skuName || (skuCode ? `编码 ${skuCode}` : '') + const taskLabel = row.taskNo + ? `${row.taskNo}${row.taskId ? ` (#${row.taskId})` : ''}` + : row.taskId + ? `#${row.taskId}` + : '-' + + return ( +
+ + {row.oid || '-'} + + {productLabel ? ( + + {productLabel} + + ) : ( + 未关联商品 + )} + + 任务 {taskLabel} + {row.orderId ? ` · 订单 #${row.orderId}` : ''} + +
+ ) + }, }, { title: '状态', - width: 150, + width: 118, render: (_, row) => (
{getVoucherStatusLabel(row.status)} @@ -243,38 +267,38 @@ export default function AdminKuaishouIndustryPage() { }, { title: '店铺', - minWidth: 180, + width: 128, + ellipsis: true, render: (_, row) => renderShopCell( row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '', row.sellerId, isSupportOnly ? '' : row.tokenMasked, + { compact: true }, ), }, { title: '核销', - minWidth: 180, - render: (_, row) => ( -
- {row.consumeSerialNum || '-'} - {formatAdminDateTime(row.consumedAt)} -
- ), + width: 132, + render: (_, row) => { + const serial = String(row.consumeSerialNum || '').trim() + const consumedAt = formatAdminDateTime(row.consumedAt) + return ( +
+ {consumedAt !== '-' ? consumedAt : serial || '-'} + {serial && consumedAt !== '-' ? ( + + {serial} + + ) : null} +
+ ) + }, }, - ...(isSupportOnly - ? [] - : [ - { - title: '更新时间', - width: 170, - render: (_: unknown, row: AdminKuaishouIndustryVoucher) => - formatAdminDateTime(row.updatedAt), - }, - ]), { title: '操作', fixed: 'right' as const, - width: isSupportOnly ? 100 : 120, + width: isSupportOnly ? 88 : 72, render: (_: unknown, row: AdminKuaishouIndustryVoucher) => { if (isSupportOnly) { const canConsume = row.status === 'UNUSED' @@ -796,14 +820,17 @@ export default function AdminKuaishouIndustryPage() { ) } - function renderVoucherTable(scrollX = 1180) { + function renderVoucherTable(scrollX?: number) { return ( {renderVoucherFilters()} - {renderVoucherTable(1080)} + {renderVoucherTable()} ) } @@ -829,7 +856,9 @@ export default function AdminKuaishouIndustryPage() { {renderVoucherFilters()}
- {renderVoucherTable()} + + {renderVoucherTable(760)} +
@@ -1295,15 +1324,33 @@ function renderShopCell( shopName?: string | null, sellerId?: string | null, tokenMasked?: string | null, + options: { compact?: boolean } = {}, ) { const name = String(shopName || '').trim() const id = String(sellerId || '').trim() const token = String(tokenMasked || '').trim() + const compact = options.compact === true if (!name && !id) { return - } + const tipParts = [ + name || null, + id ? `ID:${id}` : null, + token ? `Token:${token}` : null, + ].filter(Boolean) + const tip = tipParts.join('\n') + + if (compact) { + return ( +
+ {name || id} + {name && id ? ID:{id} : null} +
+ ) + } + return (
{name || id} diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index a51767b3..fe4c9df3 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -308,11 +308,24 @@ select { .kuaishou-industry-split { display: grid; - grid-template-columns: minmax(0, 1.45fr) minmax(360px, 0.75fr); + /* 本地券码优先占宽,减少表格横向滚动 */ + grid-template-columns: minmax(0, 1.7fr) minmax(300px, 0.55fr); gap: 16px; align-items: start; } +.kuaishou-industry-voucher-card { + min-width: 0; +} + +.kuaishou-industry-voucher-card .ant-table { + font-size: 13px; +} + +.kuaishou-industry-voucher-card .ant-table-cell { + vertical-align: top; +} + .kuaishou-industry-tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); diff --git a/apps/frontend/src/types/admin/kuaishou-industry.ts b/apps/frontend/src/types/admin/kuaishou-industry.ts index 4944d646..393ba4b3 100644 --- a/apps/frontend/src/types/admin/kuaishou-industry.ts +++ b/apps/frontend/src/types/admin/kuaishou-industry.ts @@ -17,10 +17,16 @@ export interface AdminKuaishouIndustryVoucher { oid: string orderId: number | null taskId: number | null + /** 关联履约任务号(列表 join 可选) */ + taskNo?: string unitIndex: number sellerId: string /** 来自行业店铺配置的展示名(customShopName 优先) */ shopName: string + /** 关联订单商品编码(列表 join) */ + skuCode?: string + /** 关联订单商品名称(列表 join) */ + skuName?: string tokenMasked: string status: string validStartTime: number