优化核销展示
This commit is contained in:
@@ -227,9 +227,20 @@ export async function listKuaishouIndustryVouchersByTaskId(
|
|||||||
return result.rows
|
return result.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type KuaishouIndustryVoucherAdminListRow = KuaishouIndustryVoucherRow & {
|
||||||
|
sku_code?: string | null
|
||||||
|
sku_name?: string | null
|
||||||
|
task_no?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export async function listKuaishouIndustryVouchersForAdmin(
|
export async function listKuaishouIndustryVouchersForAdmin(
|
||||||
input: KuaishouIndustryVoucherAdminListQuery = {},
|
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 page = normalizePositiveLimit(input.page, 1)
|
||||||
const pageSize = Math.min(normalizePositiveLimit(input.pageSize, 50), 200)
|
const pageSize = Math.min(normalizePositiveLimit(input.pageSize, 50), 200)
|
||||||
const where: string[] = []
|
const where: string[] = []
|
||||||
@@ -238,50 +249,65 @@ export async function listKuaishouIndustryVouchersForAdmin(
|
|||||||
const oid = String(input.oid || '').trim()
|
const oid = String(input.oid || '').trim()
|
||||||
if (oid) {
|
if (oid) {
|
||||||
params.push(oid)
|
params.push(oid)
|
||||||
where.push(`oid = $${params.length}`)
|
where.push(`v.oid = $${params.length}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const voucherCode = String(input.voucherCode || '').trim()
|
const voucherCode = String(input.voucherCode || '').trim()
|
||||||
if (voucherCode) {
|
if (voucherCode) {
|
||||||
params.push(Array.from(new Set([voucherCode, voucherCode.toUpperCase()])))
|
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)
|
const taskId = Number(input.taskId || 0)
|
||||||
if (Number.isFinite(taskId) && taskId > 0) {
|
if (Number.isFinite(taskId) && taskId > 0) {
|
||||||
params.push(Math.trunc(taskId))
|
params.push(Math.trunc(taskId))
|
||||||
where.push(`task_id = $${params.length}`)
|
where.push(`v.task_id = $${params.length}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sellerId = String(input.sellerId || '').trim()
|
const sellerId = String(input.sellerId || '').trim()
|
||||||
if (sellerId) {
|
if (sellerId) {
|
||||||
params.push(sellerId)
|
params.push(sellerId)
|
||||||
where.push(`seller_id = $${params.length}`)
|
where.push(`v.seller_id = $${params.length}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = String(input.status || '').trim().toUpperCase()
|
const status = String(input.status || '').trim().toUpperCase()
|
||||||
if (status) {
|
if (status) {
|
||||||
params.push(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 whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''
|
||||||
const totalResult = await query<{ total: string | number }>(
|
const totalResult = await query<{ total: string | number }>(
|
||||||
`
|
`
|
||||||
SELECT COUNT(*) AS total
|
SELECT COUNT(*) AS total
|
||||||
FROM kuaishou_industry_vouchers
|
FROM kuaishou_industry_vouchers v
|
||||||
${whereSql}
|
${whereSql}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
)
|
)
|
||||||
|
|
||||||
const listParams = [...params, pageSize, (page - 1) * pageSize]
|
const listParams = [...params, pageSize, (page - 1) * pageSize]
|
||||||
const listResult = await query<KuaishouIndustryVoucherRow>(
|
// 关联任务/订单商品,便于运营后台展示「买的是什么」
|
||||||
|
const listResult = await query<KuaishouIndustryVoucherAdminListRow>(
|
||||||
`
|
`
|
||||||
SELECT *
|
SELECT
|
||||||
FROM kuaishou_industry_vouchers
|
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}
|
${whereSql}
|
||||||
ORDER BY updated_at DESC, id DESC
|
ORDER BY v.updated_at DESC, v.id DESC
|
||||||
LIMIT $${params.length + 1}
|
LIMIT $${params.length + 1}
|
||||||
OFFSET $${params.length + 2}
|
OFFSET $${params.length + 2}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
listKuaishouIndustryVouchersForAdmin,
|
listKuaishouIndustryVouchersForAdmin,
|
||||||
updateKuaishouIndustryVoucherByCode,
|
updateKuaishouIndustryVoucherByCode,
|
||||||
type KuaishouIndustryVoucherAdminListQuery,
|
type KuaishouIndustryVoucherAdminListQuery,
|
||||||
|
type KuaishouIndustryVoucherAdminListRow,
|
||||||
} from '../../repositories/kuaishou-industry-voucher-repo.js'
|
} from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
import { getOrderById } from '../../repositories/order-repo.js'
|
import { getOrderById } from '../../repositories/order-repo.js'
|
||||||
import { getTaskById } from '../../repositories/task-repo.js'
|
import { getTaskById } from '../../repositories/task-repo.js'
|
||||||
@@ -395,19 +396,23 @@ function sanitizeOpenApiRequest(request: JsonObject | undefined): JsonObject | n
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapAdminKuaishouIndustryVoucher(
|
function mapAdminKuaishouIndustryVoucher(
|
||||||
voucher: KuaishouIndustryVoucherRow,
|
voucher: KuaishouIndustryVoucherRow | KuaishouIndustryVoucherAdminListRow,
|
||||||
shopNameBySellerId: Map<string, string> = buildKuaishouIndustryShopNameMap(),
|
shopNameBySellerId: Map<string, string> = buildKuaishouIndustryShopNameMap(),
|
||||||
) {
|
) {
|
||||||
const sellerId = String(voucher.seller_id || '').trim()
|
const sellerId = String(voucher.seller_id || '').trim()
|
||||||
|
const listRow = voucher as KuaishouIndustryVoucherAdminListRow
|
||||||
return {
|
return {
|
||||||
id: voucher.id,
|
id: voucher.id,
|
||||||
voucherCode: voucher.voucher_code,
|
voucherCode: voucher.voucher_code,
|
||||||
oid: voucher.oid,
|
oid: voucher.oid,
|
||||||
orderId: voucher.order_id,
|
orderId: voucher.order_id,
|
||||||
taskId: voucher.task_id,
|
taskId: voucher.task_id,
|
||||||
|
taskNo: String(listRow.task_no || '').trim(),
|
||||||
unitIndex: voucher.unit_index,
|
unitIndex: voucher.unit_index,
|
||||||
sellerId,
|
sellerId,
|
||||||
shopName: shopNameBySellerId.get(sellerId) || '',
|
shopName: shopNameBySellerId.get(sellerId) || '',
|
||||||
|
skuCode: String(listRow.sku_code || '').trim(),
|
||||||
|
skuName: String(listRow.sku_name || '').trim(),
|
||||||
tokenMasked: maskSecret(voucher.token),
|
tokenMasked: maskSecret(voucher.token),
|
||||||
status: voucher.status,
|
status: voucher.status,
|
||||||
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
||||||
|
|||||||
@@ -207,10 +207,10 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
title: '券码',
|
title: '券码',
|
||||||
minWidth: 220,
|
width: 168,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text strong copyable>
|
<Typography.Text strong copyable ellipsis={{ tooltip: row.voucherCode }}>
|
||||||
{row.voucherCode}
|
{row.voucherCode}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<span className="muted">序号:{row.unitIndex}</span>
|
<span className="muted">序号:{row.unitIndex}</span>
|
||||||
@@ -218,20 +218,44 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '订单 / 任务',
|
title: '订单商品',
|
||||||
minWidth: 220,
|
// 主列自适应:订单号 + 商品名,避免额外「商品」列占宽
|
||||||
render: (_, row) => (
|
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 (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text copyable>{row.oid || '-'}</Typography.Text>
|
<Typography.Text copyable={Boolean(row.oid)} ellipsis={{ tooltip: row.oid || undefined }}>
|
||||||
|
{row.oid || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
{productLabel ? (
|
||||||
|
<Typography.Text
|
||||||
|
ellipsis={{ tooltip: skuCode ? `${skuName || '-'}(${skuCode})` : productLabel }}
|
||||||
|
>
|
||||||
|
{productLabel}
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<span className="muted">未关联商品</span>
|
||||||
|
)}
|
||||||
<span className="muted">
|
<span className="muted">
|
||||||
任务:{row.taskId || '-'} / 订单:{row.orderId || '-'}
|
任务 {taskLabel}
|
||||||
|
{row.orderId ? ` · 订单 #${row.orderId}` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
width: 150,
|
width: 118,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
|
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
|
||||||
@@ -243,38 +267,38 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '店铺',
|
title: '店铺',
|
||||||
minWidth: 180,
|
width: 128,
|
||||||
|
ellipsis: true,
|
||||||
render: (_, row) =>
|
render: (_, row) =>
|
||||||
renderShopCell(
|
renderShopCell(
|
||||||
row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '',
|
row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '',
|
||||||
row.sellerId,
|
row.sellerId,
|
||||||
isSupportOnly ? '' : row.tokenMasked,
|
isSupportOnly ? '' : row.tokenMasked,
|
||||||
|
{ compact: true },
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '核销',
|
title: '核销',
|
||||||
minWidth: 180,
|
width: 132,
|
||||||
render: (_, row) => (
|
render: (_, row) => {
|
||||||
|
const serial = String(row.consumeSerialNum || '').trim()
|
||||||
|
const consumedAt = formatAdminDateTime(row.consumedAt)
|
||||||
|
return (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<span>{row.consumeSerialNum || '-'}</span>
|
<span title={serial || undefined}>{consumedAt !== '-' ? consumedAt : serial || '-'}</span>
|
||||||
<span className="muted">{formatAdminDateTime(row.consumedAt)}</span>
|
{serial && consumedAt !== '-' ? (
|
||||||
|
<Typography.Text type="secondary" ellipsis={{ tooltip: serial }} className="muted">
|
||||||
|
{serial}
|
||||||
|
</Typography.Text>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
),
|
)
|
||||||
},
|
},
|
||||||
...(isSupportOnly
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
title: '更新时间',
|
|
||||||
width: 170,
|
|
||||||
render: (_: unknown, row: AdminKuaishouIndustryVoucher) =>
|
|
||||||
formatAdminDateTime(row.updatedAt),
|
|
||||||
},
|
},
|
||||||
]),
|
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
width: isSupportOnly ? 100 : 120,
|
width: isSupportOnly ? 88 : 72,
|
||||||
render: (_: unknown, row: AdminKuaishouIndustryVoucher) => {
|
render: (_: unknown, row: AdminKuaishouIndustryVoucher) => {
|
||||||
if (isSupportOnly) {
|
if (isSupportOnly) {
|
||||||
const canConsume = row.status === 'UNUSED'
|
const canConsume = row.status === 'UNUSED'
|
||||||
@@ -796,14 +820,17 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVoucherTable(scrollX = 1180) {
|
function renderVoucherTable(scrollX?: number) {
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
size="middle"
|
||||||
|
tableLayout="fixed"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={vouchers}
|
dataSource={vouchers}
|
||||||
loading={voucherLoading}
|
loading={voucherLoading}
|
||||||
scroll={{ x: scrollX }}
|
// 列已压缩;仅在极窄容器时横向滚动
|
||||||
|
scroll={scrollX ? { x: scrollX } : undefined}
|
||||||
pagination={buildAdminTablePagination({
|
pagination={buildAdminTablePagination({
|
||||||
current: voucherFilters.page,
|
current: voucherFilters.page,
|
||||||
pageSize: voucherFilters.pageSize,
|
pageSize: voucherFilters.pageSize,
|
||||||
@@ -818,7 +845,7 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
{renderVoucherFilters()}
|
{renderVoucherFilters()}
|
||||||
<Card title="本地券码">{renderVoucherTable(1080)}</Card>
|
<Card title="本地券码">{renderVoucherTable()}</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -829,7 +856,9 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
{renderVoucherFilters()}
|
{renderVoucherFilters()}
|
||||||
|
|
||||||
<div className="kuaishou-industry-split">
|
<div className="kuaishou-industry-split">
|
||||||
<Card title="本地券码">{renderVoucherTable()}</Card>
|
<Card title="本地券码" className="kuaishou-industry-voucher-card">
|
||||||
|
{renderVoucherTable(760)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card title="接口操作">
|
<Card title="接口操作">
|
||||||
<div className="kuaishou-industry-tool-grid">
|
<div className="kuaishou-industry-tool-grid">
|
||||||
@@ -1295,15 +1324,33 @@ function renderShopCell(
|
|||||||
shopName?: string | null,
|
shopName?: string | null,
|
||||||
sellerId?: string | null,
|
sellerId?: string | null,
|
||||||
tokenMasked?: string | null,
|
tokenMasked?: string | null,
|
||||||
|
options: { compact?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const name = String(shopName || '').trim()
|
const name = String(shopName || '').trim()
|
||||||
const id = String(sellerId || '').trim()
|
const id = String(sellerId || '').trim()
|
||||||
const token = String(tokenMasked || '').trim()
|
const token = String(tokenMasked || '').trim()
|
||||||
|
const compact = options.compact === true
|
||||||
|
|
||||||
if (!name && !id) {
|
if (!name && !id) {
|
||||||
return <span>-</span>
|
return <span>-</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tipParts = [
|
||||||
|
name || null,
|
||||||
|
id ? `ID:${id}` : null,
|
||||||
|
token ? `Token:${token}` : null,
|
||||||
|
].filter(Boolean)
|
||||||
|
const tip = tipParts.join('\n')
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<div className="cell-stack" title={tip}>
|
||||||
|
<Typography.Text ellipsis={{ tooltip: tip }}>{name || id}</Typography.Text>
|
||||||
|
{name && id ? <span className="muted">ID:{id}</span> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<span>{name || id}</span>
|
<span>{name || id}</span>
|
||||||
|
|||||||
@@ -308,11 +308,24 @@ select {
|
|||||||
|
|
||||||
.kuaishou-industry-split {
|
.kuaishou-industry-split {
|
||||||
display: grid;
|
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;
|
gap: 16px;
|
||||||
align-items: start;
|
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 {
|
.kuaishou-industry-tool-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
|||||||
@@ -17,10 +17,16 @@ export interface AdminKuaishouIndustryVoucher {
|
|||||||
oid: string
|
oid: string
|
||||||
orderId: number | null
|
orderId: number | null
|
||||||
taskId: number | null
|
taskId: number | null
|
||||||
|
/** 关联履约任务号(列表 join 可选) */
|
||||||
|
taskNo?: string
|
||||||
unitIndex: number
|
unitIndex: number
|
||||||
sellerId: string
|
sellerId: string
|
||||||
/** 来自行业店铺配置的展示名(customShopName 优先) */
|
/** 来自行业店铺配置的展示名(customShopName 优先) */
|
||||||
shopName: string
|
shopName: string
|
||||||
|
/** 关联订单商品编码(列表 join) */
|
||||||
|
skuCode?: string
|
||||||
|
/** 关联订单商品名称(列表 join) */
|
||||||
|
skuName?: string
|
||||||
tokenMasked: string
|
tokenMasked: string
|
||||||
status: string
|
status: string
|
||||||
validStartTime: number
|
validStartTime: number
|
||||||
|
|||||||
Reference in New Issue
Block a user