Merge branch 'dev'

This commit is contained in:
2026-11-02 15:03:01 +08:00
30 changed files with 1922 additions and 50 deletions
@@ -0,0 +1,266 @@
import { useQuery } from '@tanstack/react-query'
import { ReloadOutlined } from '@ant-design/icons'
import { Alert, Button, Checkbox, Empty, Input, Modal, Space, Tag, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import PercentTable from '@/components/common/PercentTable'
import { fetchPendingPistolMatches } from '@/services/admin/consignment'
import type { PendingPistolMatchPair } from '@/types/consignment'
import { formatDateTime } from '@/utils/date-time'
const { Text } = Typography
function resolveScoreTag(pair: PendingPistolMatchPair) {
if (pair.matchType === 'exact') return { color: 'green', label: '精确' }
if (pair.matchType === 'conflict') return { color: 'default', label: '不匹配' }
if (pair.score >= 90) return { color: 'blue', label: '非精确' }
if (pair.score >= 70) return { color: 'orange', label: '非精确' }
return { color: 'gold', label: '需核对' }
}
export default function ConsignmentPendingMatchModal({
open,
onClose,
}: {
open: boolean
onClose: () => void
}) {
const [keyword, setKeyword] = useState('')
const [includeConflicts, setIncludeConflicts] = useState(false)
const query = useQuery({
queryKey: ['admin-pending-pistol-matches', keyword, includeConflicts],
queryFn: () => fetchPendingPistolMatches({ keyword, includeConflicts, limit: 300 }),
enabled: open,
refetchOnWindowFocus: false,
retry: false,
})
const data = query.data?.data
const pairs = data?.pairs || []
const columns: TableColumnsType<PendingPistolMatchPair> = [
{
title: '匹配打分',
key: 'score',
width: 90,
render: (_, record) => {
const tag = resolveScoreTag(record)
return (
<Space direction="vertical" size={3} style={{ width: '100%' }}>
<span style={{ fontSize: 18, fontWeight: 700, lineHeight: 1 }}>{record.score}</span>
<Tag color={tag.color} style={{ marginInlineEnd: 0 }}>
{tag.label}
</Tag>
</Space>
)
},
},
{
title: '订单信息',
key: 'order',
width: 280,
render: (_, record) => (
<Space direction="vertical" size={2} style={{ width: '100%' }}>
<Space size={4} align="center">
<Text type="secondary" style={{ fontSize: 11 }}>
工单
</Text>
{record.workOrderNo ? (
<Text
copyable={{ text: record.workOrderNo }}
style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}
>
{record.workOrderNo}
</Text>
) : (
<Text type="secondary">-</Text>
)}
</Space>
<Space size={4} align="center">
<Text type="secondary" style={{ fontSize: 11 }}>
平台
</Text>
{record.platformOrderId ? (
<Text
copyable={{ text: record.platformOrderId }}
style={{ fontSize: 11, whiteSpace: 'nowrap' }}
>
{record.platformOrderId}
</Text>
) : (
<Text type="secondary">-</Text>
)}
</Space>
<Text type="secondary" style={{ fontSize: 11 }}>
录单 {record.workOrderCreatedAt ? formatDateTime(record.workOrderCreatedAt) : '-'}
</Text>
</Space>
),
},
{
title: '商品名',
key: 'productName',
width: 220,
render: (_, record) => (
<Text ellipsis={{ tooltip: record.productName }} style={{ maxWidth: '100%' }}>
{record.productName || '-'}
</Text>
),
},
{
title: '寄售信息',
key: 'consignment',
width: 280,
render: (_, record) => (
<Space direction="vertical" size={2} style={{ width: '100%' }}>
<Space size={4} align="center">
<Text type="secondary" style={{ fontSize: 11 }}>
寄售
</Text>
<Text
copyable={{ text: record.consignmentNo || record.inventoryNo }}
style={{ fontSize: 12, fontWeight: 600, whiteSpace: 'nowrap' }}
>
{record.consignmentNo || record.inventoryNo || '-'}
</Text>
</Space>
{record.inventoryNo && record.inventoryNo !== record.consignmentNo ? (
<Space size={4} align="center">
<Text type="secondary" style={{ fontSize: 11 }}>
仓库
</Text>
<Text
copyable={{ text: record.inventoryNo }}
style={{ fontSize: 11, whiteSpace: 'nowrap' }}
>
{record.inventoryNo}
</Text>
</Space>
) : null}
<Text type="secondary" style={{ fontSize: 11 }}>
入库 {record.inventoryInboundAt ? formatDateTime(record.inventoryInboundAt) : '-'}
</Text>
</Space>
),
},
{
title: '寄售物品',
key: 'itemName',
width: 220,
render: (_, record) => (
<Text ellipsis={{ tooltip: record.itemName }} style={{ maxWidth: '100%' }}>
{record.itemName || '-'}
</Text>
),
},
{
title: '匹配说明',
key: 'reason',
width: 300,
render: (_, record) => (
<Space direction="vertical" size={2}>
<Text style={{ fontSize: 12 }}>{record.reason || '-'}</Text>
{record.conflicts.length > 0 ? (
<Text type="danger" style={{ fontSize: 11 }}>
{record.conflicts.join(';')}
</Text>
) : null}
</Space>
),
},
]
return (
<Modal
title="匹配 · 待抢单手枪 × 在库现货"
open={open}
width="min(1320px, 94vw)"
footer={null}
destroyOnHidden
onCancel={onClose}
>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Alert
type="info"
showIcon
message="只查询非拼单的待抢单手枪单 × 在库待匹配现货,不直接改单。"
description="分数 0-100:系统 20 / 区服 20 / 系列 20 / 钻级 25 / 璀璨 15。非精确项仍需按现有规则确认;不按录单、入库时间先后过滤。"
/>
<Space wrap size={12}>
<Tag>待抢单 {data?.pendingOrderCount ?? 0} 单</Tag>
<Tag>在库待匹配 {data?.inStockInventoryCount ?? 0} 件</Tag>
<Tag color="blue">匹配组合 {data?.pairCount ?? 0} 条</Tag>
<Tag color="green">精确 {data?.exactCount ?? 0}</Tag>
<Tag color="orange">非精确 {data?.partialCount ?? 0}</Tag>
{includeConflicts ? <Tag>不匹配 {data?.conflictCount ?? 0}</Tag> : null}
</Space>
<Space wrap size={12}>
<Input.Search
allowClear
placeholder="搜索平台号 / 工单号 / 商品名 / 寄售单号 / 寄售物品"
style={{ width: 420 }}
onSearch={(value) => setKeyword(value.trim())}
/>
<Checkbox
checked={includeConflicts}
onChange={(event) => setIncludeConflicts(event.target.checked)}
>
显示 0 分不匹配项
</Checkbox>
<Button
icon={<ReloadOutlined />}
loading={query.isFetching}
onClick={() => void query.refetch()}
>
刷新
</Button>
</Space>
{query.isError ? (
<Alert
type="error"
showIcon
message="读取匹配结果失败"
description={query.error instanceof Error ? query.error.message : '请稍后重试'}
/>
) : null}
{data?.truncated ? (
<Alert
type="warning"
showIcon
message={`结果较多,仅展示前 ${pairs.length} 条,请用搜索缩小范围`}
/>
) : null}
<PercentTable<PendingPistolMatchPair>
rowKey={(record) => `${record.workOrderId}-${record.inventoryId}`}
size="small"
loading={query.isFetching}
dataSource={pairs}
columns={columns}
scroll={{ y: 460 }}
pagination={{
pageSize: 20,
showSizeChanger: false,
showTotal: (total) => `共 ${total} 条`,
}}
locale={{
emptyText: (
<Empty
description={
includeConflicts
? '没有可展示的配对'
: '没有有分匹配;可勾选「显示 0 分不匹配项」查看冲突'
}
/>
),
}}
/>
</Space>
</Modal>
)
}
@@ -1,5 +1,5 @@
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { ReloadOutlined, SaveOutlined } from '@ant-design/icons'
import { DeleteOutlined, PlusOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons'
import {
Alert,
App,
@@ -9,12 +9,13 @@ import {
Input,
InputNumber,
Popconfirm,
Select,
Space,
Switch,
Tag,
Typography,
} from 'antd'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import {
addAdminMockCustomShop,
@@ -22,14 +23,18 @@ import {
fetchAdminMockCustomShops,
fetchAdminScheduledJobsConfig,
fetchAdminWorkerAnnouncement,
fetchAdminWorkerProductNameSuffix,
fetchAdminWorkerProductNameSuffixSources,
renameAdminMockCustomShop,
runAdminScheduledJob,
saveAdminWorkerAnnouncement,
saveAdminWorkerProductNameSuffix,
saveAdminScheduledJobsConfig,
} from '@/services/admin'
import type { AdminScheduledJobItem, AdminScheduledJobRuntimeState } from '@/types/admin'
import type { WorkerAnnouncementConfig } from '@/types/worker-platform'
import type { WorkerAnnouncementConfig, WorkerProductNameSuffixItem } from '@/types/worker-platform'
import { formatAdminDateTime } from '@/utils/admin-time'
import { normalizeProductNameSuffixText } from '@/utils/product-name-source-suffix'
import HallConfigPanel from './HallConfigPanel'
@@ -41,6 +46,7 @@ export default function AnnouncementPanel() {
return (
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<AnnouncementCard />
<ProductNameSourceSuffixCard />
<DepositUnfreezeCard />
<MockCustomShopsCard />
<HallConfigPanel />
@@ -241,6 +247,233 @@ function AnnouncementCard() {
)
}
type ProductNameSuffixDraft = WorkerProductNameSuffixItem & { rowKey: string }
let productNameSuffixRowSeed = 0
function createProductNameSuffixRow(
source = '',
suffix = '',
enabled = false,
note = '',
): ProductNameSuffixDraft {
productNameSuffixRowSeed += 1
return {
rowKey: `product-name-suffix-${productNameSuffixRowSeed}`,
source,
suffix,
enabled,
note,
}
}
/** 商品名来源后缀:按工单来源在商品名后追加「(文字)」,只影响展示,不改库里的商品名。 */
function ProductNameSourceSuffixCard() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [saving, setSaving] = useState(false)
const [items, setItems] = useState<ProductNameSuffixDraft[]>([])
const suffixQuery = useQuery({
queryKey: ['admin-worker-product-name-suffix'],
queryFn: fetchAdminWorkerProductNameSuffix,
staleTime: 5 * 60 * 1000,
})
const sourcesQuery = useQuery({
queryKey: ['admin-worker-product-name-suffix-sources'],
queryFn: fetchAdminWorkerProductNameSuffixSources,
staleTime: 5 * 60 * 1000,
})
useEffect(() => {
const remoteItems = suffixQuery.data?.data.items || []
setItems(
remoteItems.map((item) =>
createProductNameSuffixRow(item.source, item.suffix, item.enabled, item.note),
),
)
}, [suffixQuery.data])
// 下拉选项来自订单来源字段;已配置但暂时没有工单的来源也保留,避免编辑时丢值。
const sourceOptions = useMemo(() => {
const values = new Map<string, string>()
for (const item of items) {
const text = String(item.source || '').trim()
if (!text) continue
const key = text.toLowerCase()
if (!values.has(key)) values.set(key, text)
}
for (const value of sourcesQuery.data?.data.items || []) {
const text = String(value || '').trim()
if (!text) continue
const key = text.toLowerCase()
if (!values.has(key)) values.set(key, text)
}
return [...values.values()]
.sort((a, b) => a.localeCompare(b))
.map((value) => ({ value, label: value }))
}, [items, sourcesQuery.data])
function patchRow(rowKey: string, patch: Partial<WorkerProductNameSuffixItem>) {
setItems((prev) => prev.map((item) => (item.rowKey === rowKey ? { ...item, ...patch } : item)))
}
function toggleRow(rowKey: string, enabled: boolean) {
setItems((prev) =>
prev.map((item) => {
if (item.rowKey === rowKey) return { ...item, enabled }
// 同一时间只能启用一个来源:启用当前行时自动停用其他行。
return enabled ? { ...item, enabled: false } : item
}),
)
}
function removeRow(rowKey: string) {
setItems((prev) => prev.filter((item) => item.rowKey !== rowKey))
}
async function save() {
const normalized: WorkerProductNameSuffixItem[] = []
const seenSources = new Set<string>()
let hasEnabledItem = false
for (const [index, item] of items.entries()) {
const source = String(item.source || '').trim()
const suffix = normalizeProductNameSuffixText(item.suffix)
if (!source && !suffix) continue
if (!source || !suffix) {
message.error(`第 ${index + 1} 行:来源和后缀都要填写`)
return
}
const normalizedSource = source.toLowerCase()
if (seenSources.has(normalizedSource)) {
message.error(`来源「${source}」重复,一行只能配置一个来源`)
return
}
seenSources.add(normalizedSource)
const enabled = item.enabled === true && !hasEnabledItem
if (enabled) hasEnabledItem = true
normalized.push({
source,
suffix,
enabled,
note: String(item.note || '').trim(),
})
}
if (normalized.length > 50) {
message.error('最多配置 50 条来源后缀')
return
}
setSaving(true)
try {
await saveAdminWorkerProductNameSuffix({ items: normalized })
await queryClient.invalidateQueries({ queryKey: ['admin-worker-product-name-suffix'] })
message.success('商品名来源后缀已保存')
} catch (error) {
message.error(error instanceof Error ? error.message : '保存商品名来源后缀失败')
} finally {
setSaving(false)
}
}
return (
<Card
title="商品名来源后缀"
bordered={false}
extra={
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void save()}>
保存
</Button>
}
>
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
来源下拉数据取自工单的「来源」字段;选中来源后填写后缀,启用后展示为「商品名(后缀)」。
同一时间只能启用一个来源,启用新来源会自动停用其他行;未启用或没有匹配配置的来源不追加。
备注仅后台可见。只影响列表/详情的展示,不会改写工单里的商品名。
</Typography.Paragraph>
{sourcesQuery.isError ? (
<Typography.Text type="danger" style={{ display: 'block', marginBottom: 8 }}>
读取工单来源选项失败,已配置的来源仍可编辑;请稍后刷新重试。
</Typography.Text>
) : null}
{items.length === 0 ? (
<Typography.Text type="secondary">
还没有配置,所有商品名保持原样;点击「添加一行」开始配置。
</Typography.Text>
) : null}
<Space direction="vertical" size={4} style={{ width: '100%' }}>
{items.map((item, index) => {
const suffix = normalizeProductNameSuffixText(item.suffix)
return (
<Space
key={item.rowKey}
size={8}
wrap
align="center"
style={{ width: '100%', padding: '6px 0', borderBottom: '1px solid #f0f0f0' }}
>
<Switch
checked={item.enabled === true}
checkedChildren="启用"
unCheckedChildren="停用"
onChange={(checked) => toggleRow(item.rowKey, checked)}
/>
<Select
showSearch
allowClear
loading={sourcesQuery.isLoading}
optionFilterProp="label"
style={{ width: 220 }}
placeholder="选择来源"
options={sourceOptions}
value={item.source || undefined}
onChange={(value) => patchRow(item.rowKey, { source: String(value || '') })}
/>
<Input
addonBefore="后缀"
style={{ width: 200 }}
maxLength={34}
placeholder="如 91卡券"
value={item.suffix}
onChange={(event) => patchRow(item.rowKey, { suffix: event.target.value })}
/>
<Input
style={{ width: 220 }}
maxLength={200}
placeholder="备注(选填)"
value={item.note}
onChange={(event) => patchRow(item.rowKey, { note: event.target.value })}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{suffix
? item.enabled
? `示例:商品名(${suffix})`
: '未启用,不追加'
: '未填写后缀时不追加'}
</Typography.Text>
<Button
danger
type="text"
icon={<DeleteOutlined />}
aria-label={`删除第 ${index + 1} 行`}
onClick={() => removeRow(item.rowKey)}
>
删除
</Button>
</Space>
)
})}
<Button
icon={<PlusOutlined />}
disabled={items.length >= 50}
onClick={() => setItems((prev) => [...prev, createProductNameSuffixRow()])}
>
添加一行
</Button>
</Space>
</Card>
)
}
function MockCustomShopsCard() {
const { message, modal } = App.useApp()
const queryClient = useQueryClient()
@@ -1,4 +1,10 @@
import { EyeOutlined, ReloadOutlined, RollbackOutlined, StopOutlined } from '@ant-design/icons'
import {
EyeOutlined,
ReloadOutlined,
RollbackOutlined,
StopOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import { useQuery } from '@tanstack/react-query'
import {
Alert,
@@ -27,6 +33,7 @@ import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
import ConsignmentPendingMatchModal from '@/components/admin/ConsignmentPendingMatchModal'
import {
archiveAdminConsignmentInventory,
fetchAdminConsignmentContacts,
@@ -149,6 +156,7 @@ export default function ConsignmentInventoryPanel() {
const [viewingItem, setViewingItem] = useState<ConsignmentInventory | null>(null)
const [returnItem, setReturnItem] = useState<ConsignmentInventory | null>(null)
const [manualMatchItem, setManualMatchItem] = useState<ConsignmentInventory | null>(null)
const [pendingMatchOpen, setPendingMatchOpen] = useState(false)
const [returnReason, setReturnReason] = useState<string>('')
const [editForm] = Form.useForm()
@@ -836,6 +844,10 @@ export default function ConsignmentInventoryPanel() {
}}
/>
<Button icon={<ThunderboltOutlined />} onClick={() => setPendingMatchOpen(true)}>
一键查询待匹配订单
</Button>
<Button icon={<ReloadOutlined />} onClick={refresh}>
刷新
</Button>
@@ -1305,6 +1317,12 @@ export default function ConsignmentInventoryPanel() {
}
onMatched={() => void inventoriesQuery.refetch()}
/>
{/* 全局匹配看板:非拼单待抢单手枪 × 在库待匹配现货 */}
<ConsignmentPendingMatchModal
open={pendingMatchOpen}
onClose={() => setPendingMatchOpen(false)}
/>
</div>
)
}
@@ -4,6 +4,7 @@ import { useRef, useState } from 'react'
import { updateAdminWorkOrder, updateAdminWorkOrderPaymentAmount } from '@/services/admin'
import type { WorkOrder } from '@/types/worker-platform'
import { appendProductNameSourceSuffix } from '@/utils/product-name-source-suffix'
import { formatMoney } from './shared'
import {
@@ -15,10 +16,13 @@ import {
/** 商品名:单击看详情,双击原地修改(仅未接走工单) */
export function ProductNameEditableCell({
order,
sourceSuffix = '',
onOpenDetail,
onSaved,
}: {
order: WorkOrder
/** 按来源匹配到的后缀(不含括号):只用于展示,编辑态仍显示原始商品名 */
sourceSuffix?: string
onOpenDetail: () => void
onSaved: () => Promise<void> | void
}) {
@@ -69,13 +73,15 @@ export function ProductNameEditableCell({
}
if (!editing) {
const displayName =
appendProductNameSourceSuffix(order.productName, sourceSuffix) || '未命名商品'
return (
<Typography.Link
title={editable ? '双击修改商品名' : undefined}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
{order.productName || '未命名商品'}
{displayName}
</Typography.Link>
)
}
@@ -68,6 +68,7 @@ import {
fetchAdminWorkOrderSharing,
fetchAdminWorkOrderShopOptions,
fetchAdminWorkerLevels,
fetchAdminWorkerProductNameSuffix,
fetchAdminWorkerUsers,
fetchAdminWorkOrders,
markAdminWorkOrderProblem,
@@ -107,6 +108,10 @@ import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admi
import { getAdminUsername, hasAdminPermission, hasAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import { buildCollectMaterialLinkUrl } from '@/utils/collect-link'
import {
applyProductNameSourceSuffix,
resolveProductNameSourceSuffix,
} from '@/utils/product-name-source-suffix'
import { isExplicitOrderNoSearch, normalizeSearchText } from '@/utils/admin-realtime-work-orders'
import {
DeadlineCountdown,
@@ -285,6 +290,13 @@ export default function WorkOrdersPanel() {
queryFn: fetchAdminSession,
retry: false,
})
// 商品名来源后缀(杂项配置维护):只影响列表/详情展示,编辑与复制仍用原始商品名。
const productNameSuffixQuery = useQuery({
queryKey: ['admin-worker-product-name-suffix'],
queryFn: fetchAdminWorkerProductNameSuffix,
staleTime: 5 * 60 * 1000,
})
const productNameSuffixItems = productNameSuffixQuery.data?.data.items || []
const sessionPermissions = adminSessionQuery.data?.data.user.permissions
const canReturnToHall = sessionPermissions
? sessionPermissions.includes('worker_order.return_to_hall')
@@ -436,6 +448,13 @@ export default function WorkOrdersPanel() {
// 详情抽屉里的寄售履约信息(有寄售履约才展示寄卖单号)
const detailConsignment = detailOrder ? getConsignmentFulfillment(detailOrder) : null
const detailProductName = detailOrder
? applyProductNameSourceSuffix(
detailOrder.productName,
asRecord(detailOrder.material?.source).provider,
productNameSuffixItems,
)
: ''
// 仅单独筛选「待验收」时提供批量验收入口(多选状态下不启用勾选,避免误操作)
const isPendingAcceptanceOnly = statuses.length === 1 && statuses[0] === 'pending_acceptance'
@@ -1676,11 +1695,13 @@ export default function WorkOrdersPanel() {
const source = asRecord(row.material?.source)
const shopName = String(source.shopName || '').trim()
const provider = String(source.provider || '').trim()
const sourceSuffix = resolveProductNameSourceSuffix(productNameSuffixItems, provider)
const consignment = getConsignmentFulfillment(row)
return (
<div className="cell-stack">
<ProductNameEditableCell
order={row}
sourceSuffix={sourceSuffix}
onOpenDetail={() => openDetailDrawer(row)}
onSaved={refreshAll}
/>
@@ -2360,7 +2381,7 @@ export default function WorkOrdersPanel() {
className="worker-order-detail-drawer"
title={
detailOrder
? `${detailOrder.productName || '工单详情'} · ${getDisplayOrderNo(detailOrder)}`
? `${detailProductName || '工单详情'} · ${getDisplayOrderNo(detailOrder)}`
: '工单详情'
}
width={860}
@@ -2408,7 +2429,7 @@ export default function WorkOrdersPanel() {
<Descriptions.Item label="订单子项 ID">
{detailOrder.orderItemId || '-'}
</Descriptions.Item>
<Descriptions.Item label="商品">{detailOrder.productName || '-'}</Descriptions.Item>
<Descriptions.Item label="商品">{detailProductName || '-'}</Descriptions.Item>
<Descriptions.Item label="分类">
{detailOrder.categoryName || '-'}
</Descriptions.Item>
@@ -33,7 +33,15 @@ import {
Tooltip,
Typography,
} from 'antd'
import { memo, useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react'
import {
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from 'react'
import { Link } from 'react-router'
import { WorkerContactModal } from '@/components/worker/WorkerContactModal'
@@ -44,6 +52,7 @@ import {
fetchWorkerHallLeaderboard,
fetchWorkerMyOrders,
fetchWorkerMyOrderOverview,
fetchWorkerProductNameSuffix,
fetchWorkerProfile,
grabWorkerOrder,
joinWorkerSharingOrder,
@@ -67,6 +76,10 @@ import {
resolveHallRemainingQuantity,
resolveHallRewardAmount,
} from '@/utils/worker-product-name'
import {
applyProductNameSourceSuffix,
type ProductNameSourceSuffixItem,
} from '@/utils/product-name-source-suffix'
import { formatDateTime, formatPublishedElapsedTime } from '@/utils/date-time'
import { createRequestId } from '@/utils/request-id'
@@ -161,6 +174,28 @@ export default function WorkerHallPage() {
const orders = ordersQuery.data?.pages.flatMap((page) => page.data.items) || []
const firstPage = ordersQuery.data?.pages[0]?.data
// 商品名来源后缀(后台「杂项配置」维护):只影响展示,不改商品名本身。
const productNameSuffixQuery = useQuery({
queryKey: ['worker-product-name-suffix'],
queryFn: fetchWorkerProductNameSuffix,
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: false,
})
const productNameSuffixItems = useMemo(
() => productNameSuffixQuery.data?.data.items || [],
[productNameSuffixQuery.data],
)
const resolveDisplayProductName = useCallback(
(order: WorkOrder) =>
applyProductNameSourceSuffix(
resolveHallProductName(order),
order.sourceProvider,
productNameSuffixItems,
) || '未命名商品',
[productNameSuffixItems],
)
const [categoryMeta, setCategoryMeta] = useState<{
categories: WorkCategory[]
showSubcategories: boolean
@@ -337,7 +372,7 @@ export default function WorkerHallPage() {
modal.confirm({
title: '确认抢单',
icon: <QuestionCircleOutlined />,
content: `确认抢下「${resolveHallProductName(order)}」?奖励 ${formatMoney(
content: `确认抢下「${resolveDisplayProductName(order)}」?奖励 ${formatMoney(
resolveHallRewardAmount(order),
)}`,
okText: '确认抢单',
@@ -345,7 +380,7 @@ export default function WorkerHallPage() {
onOk: () => submitGrab(order),
})
},
[grabConfirmEnabled, modal, submitGrab],
[grabConfirmEnabled, modal, submitGrab, resolveDisplayProductName],
)
const openSharingModal = useCallback((order: WorkOrder) => {
@@ -679,6 +714,7 @@ export default function WorkerHallPage() {
remainingSlots={remainingSlots}
isInternalWorker={isInternalWorker}
grabbing={grabbingId === order.workOrderId}
productNameSuffixItems={productNameSuffixItems}
onGrab={requestGrab}
onOpenSharing={openSharingModal}
/>
@@ -1017,6 +1053,7 @@ export default function WorkerHallPage() {
remainingSlots={remainingSlots}
isInternalWorker={isInternalWorker}
grabbing={grabbingId === order.workOrderId}
productNameSuffixItems={productNameSuffixItems}
onGrab={requestGrab}
onOpenSharing={openSharingModal}
/>
@@ -1031,7 +1068,7 @@ export default function WorkerHallPage() {
)}
<Modal
title={`拼单参与 · ${sharingOrder ? resolveHallProductName(sharingOrder) : ''}`}
title={`拼单参与 · ${sharingOrder ? resolveDisplayProductName(sharingOrder) : ''}`}
width={isMobile ? '92%' : 520}
open={Boolean(sharingOrder)}
confirmLoading={submittingSharing}
@@ -1169,6 +1206,8 @@ type HallCardProps = {
remainingSlots: number | null
isInternalWorker: boolean
grabbing: boolean
/** 后台「杂项配置」维护的来源后缀;仅展示追加,不影响复制内容 */
productNameSuffixItems: ProductNameSourceSuffixItem[]
onGrab: (order: WorkOrder) => void
onOpenSharing: (order: WorkOrder) => void
}
@@ -1202,11 +1241,17 @@ const HallDesktopCard = memo(function HallDesktopCard({
remainingSlots,
isInternalWorker,
grabbing,
productNameSuffixItems,
onGrab,
onOpenSharing,
}: HallCardProps) {
const disabledReason = resolveGrabDisabledReason(order, worker, remainingSlots)
const productName = resolveHallProductName(order) || '未命名商品'
const productName =
applyProductNameSourceSuffix(
resolveHallProductName(order),
order.sourceProvider,
productNameSuffixItems,
) || '未命名商品'
const sharingRemaining = resolveHallRemainingQuantity(order)
const depositAmount = resolveHallDepositAmount(order)
const rewardAmount = resolveHallRewardAmount(order)
@@ -1326,11 +1371,17 @@ const HallMobileCard = memo(function HallMobileCard({
remainingSlots,
isInternalWorker,
grabbing,
productNameSuffixItems,
onGrab,
onOpenSharing,
}: HallCardProps) {
const disabledReason = resolveGrabDisabledReason(order, worker, remainingSlots)
const productName = resolveHallProductName(order) || '未命名商品'
const productName =
applyProductNameSourceSuffix(
resolveHallProductName(order),
order.sourceProvider,
productNameSuffixItems,
) || '未命名商品'
const sharingRemaining = resolveHallRemainingQuantity(order)
const myShareSummary = resolveHallMyShareSummary(order)
@@ -7,6 +7,10 @@ import VideoPreview, { formatAcceptanceVideoDuration } from '@/components/files/
import type { WorkOrder } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import { resolveOrderConsignmentScreenshotUrls } from '@/utils/consignment-display'
import {
applyProductNameSourceSuffix,
type ProductNameSourceSuffixItem,
} from '@/utils/product-name-source-suffix'
import {
canEditAcceptanceImages,
@@ -35,6 +39,7 @@ export function WorkerOrderDetailDrawer({
isMobile,
order,
orderTimelineVisible,
productNameSuffixItems,
onClose,
onOpenAcceptance,
onOpenGiftMaterial,
@@ -44,6 +49,8 @@ export function WorkerOrderDetailDrawer({
isMobile: boolean
order: WorkOrder | null
orderTimelineVisible: boolean
/** 后台「杂项配置」维护的来源后缀;仅展示追加,不影响复制内容 */
productNameSuffixItems: ProductNameSourceSuffixItem[]
onClose: () => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
@@ -51,6 +58,13 @@ export function WorkerOrderDetailDrawer({
onOpenFeedback: (order: WorkOrder) => void
}) {
const isTimedOut = Boolean(order && (order.status === 'open' || order.timeoutNotice?.isTimedOut))
const displayProductName = order
? applyProductNameSourceSuffix(
order.productName,
order.sourceProvider,
productNameSuffixItems,
) || '-'
: '-'
return (
<Drawer
@@ -121,7 +135,7 @@ export function WorkerOrderDetailDrawer({
</div>
<Card title="基础信息" size="small">
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
<Descriptions.Item label="商品">{order.productName || '-'}</Descriptions.Item>
<Descriptions.Item label="商品">{displayProductName}</Descriptions.Item>
<Descriptions.Item label="分类">{order.categoryName || '-'}</Descriptions.Item>
<Descriptions.Item label="工单编号">{getDisplayOrderNo(order)}</Descriptions.Item>
<Descriptions.Item label="状态">
@@ -20,6 +20,10 @@ import type {
WorkOrder,
} from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import {
applyProductNameSourceSuffix,
type ProductNameSourceSuffixItem,
} from '@/utils/product-name-source-suffix'
import type { WorkerOrderStatusOption } from './WorkerOrderDesktopList'
import {
@@ -56,6 +60,7 @@ export function WorkerOrderMobileList({
pagination,
page,
pageSize,
productNameSuffixItems,
renderShareTable,
loading,
searching,
@@ -85,6 +90,8 @@ export function WorkerOrderMobileList({
pagination: PaginationData | undefined
page: number
pageSize: number
/** 后台「杂项配置」维护的来源后缀;仅展示追加,不影响复制内容 */
productNameSuffixItems: ProductNameSourceSuffixItem[]
renderShareTable?: (order: WorkOrder) => ReactNode
loading: boolean
searching: boolean
@@ -178,6 +185,7 @@ export function WorkerOrderMobileList({
<WorkerOrderMobileCard
key={order.workOrderId}
order={order}
productNameSuffixItems={productNameSuffixItems}
renderShareTable={renderShareTable}
onOpenDetail={onOpenDetail}
onOpenAcceptance={onOpenAcceptance}
@@ -209,6 +217,7 @@ export function WorkerOrderMobileList({
function WorkerOrderMobileCard({
order,
productNameSuffixItems,
renderShareTable,
onOpenDetail,
onOpenAcceptance,
@@ -251,6 +260,9 @@ function WorkerOrderMobileCard({
const workerNote = String(order.workerNote || '').trim()
const statusDisplay = getOrderStatusDisplay(order)
const flagTags = getOrderFlagTags(order)
const displayProductName =
applyProductNameSourceSuffix(order.productName, order.sourceProvider, productNameSuffixItems) ||
'未命名商品'
return (
<article className="worker-order-mobile-card">
@@ -308,7 +320,7 @@ function WorkerOrderMobileCard({
</div>
<div className="worker-order-mobile-main-row">
<div className="worker-order-mobile-product-title">{order.productName || '未命名商品'}</div>
<div className="worker-order-mobile-product-title">{displayProductName}</div>
{order.status === 'open' || order.timeoutNotice?.isTimedOut ? (
<div className="worker-order-mobile-price-badge">
<span className="worker-order-mobile-price-label" style={{ color: '#cf1322' }}>
@@ -12,6 +12,10 @@ import { DeadlineCountdown } from '@/components/DeadlineCountdown'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import type { WorkOrder } from '@/types/worker-platform'
import { formatDateTime } from '@/utils/date-time'
import {
applyProductNameSourceSuffix,
type ProductNameSourceSuffixItem,
} from '@/utils/product-name-source-suffix'
import {
canEditAcceptanceImages,
@@ -40,6 +44,7 @@ import {
type NoteTheme = 'success' | 'processing' | 'danger' | 'warning' | 'purple' | 'default'
export function buildWorkerOrderTableColumns({
productNameSuffixItems,
onOpenDetail,
onOpenAcceptance,
onOpenGiftMaterial,
@@ -50,6 +55,7 @@ export function buildWorkerOrderTableColumns({
onCopyOrderInfo,
onCopyMaterialImages,
}: {
productNameSuffixItems: ProductNameSourceSuffixItem[]
onOpenDetail: (order: WorkOrder) => void
onOpenAcceptance: (order: WorkOrder) => void
onOpenGiftMaterial: (order: WorkOrder) => void
@@ -64,41 +70,49 @@ export function buildWorkerOrderTableColumns({
{
title: '订单信息',
width: 270,
render: (_, row) => (
<div className="cell-stack">
<Space size={4} align="center">
{row.isUnread ? (
<Tag color="purple" className="worker-order-unread-tag">
新
</Tag>
) : null}
<Typography.Link
strong
onClick={() => onOpenDetail(row)}
title={row.productName || '未命名商品'}
ellipsis
render: (_, row) => {
const displayProductName =
applyProductNameSourceSuffix(
row.productName,
row.sourceProvider,
productNameSuffixItems,
) || '未命名商品'
return (
<div className="cell-stack">
<Space size={4} align="center">
{row.isUnread ? (
<Tag color="purple" className="worker-order-unread-tag">
新
</Tag>
) : null}
<Typography.Link
strong
onClick={() => onOpenDetail(row)}
title={displayProductName}
ellipsis
>
{displayProductName}
</Typography.Link>
</Space>
<Space size={[4, 4]} wrap>
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
{row.myShares?.length || row.myShare ? (
<Tag color="purple">拼单 {row.myShares?.length || 1} 次</Tag>
) : null}
</Space>
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: getDisplayOrderNo(row) }}
>
{row.productName || '未命名商品'}
</Typography.Link>
</Space>
<Space size={[4, 4]} wrap>
{row.categoryName ? <Tag color="blue">{row.categoryName}</Tag> : null}
{row.myShares?.length || row.myShare ? (
<Tag color="purple">拼单 {row.myShares?.length || 1} 次</Tag>
) : null}
</Space>
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: getDisplayOrderNo(row) }}
>
{getDisplayOrderNo(row)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
接单:{formatDateTime(row.assignedAt)}
</Typography.Text>
</div>
),
{getDisplayOrderNo(row)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
接单:{formatDateTime(row.assignedAt)}
</Typography.Text>
</div>
)
},
},
{
title: '状态',
@@ -1,7 +1,7 @@
import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router'
import { App, Badge, Button, Form, Modal, Radio, Space, Tag, Typography } from 'antd'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import {
fetchWorkerAfterSalesCases,
@@ -9,6 +9,7 @@ import {
fetchWorkerOrderFeedbacks,
fetchWorkerMyOrderOverview,
fetchWorkerMyOrders,
fetchWorkerProductNameSuffix,
fetchWorkerProfile,
markWorkerOrderRead,
remindWorkerAcceptance,
@@ -206,6 +207,18 @@ export default function WorkerOrdersPage() {
const pagination = ordersQuery.data?.data.pagination
const orders = ordersQuery.data?.data.items || []
// 商品名来源后缀(后台「杂项配置」维护):仅展示追加,复制资料仍用原始商品名。
const productNameSuffixQuery = useQuery({
queryKey: ['worker-product-name-suffix'],
queryFn: fetchWorkerProductNameSuffix,
staleTime: 5 * 60 * 1000,
retry: false,
})
const productNameSuffixItems = useMemo(
() => productNameSuffixQuery.data?.data.items || [],
[productNameSuffixQuery.data],
)
// 流转记录对打手是否可见(后台配置开关,默认关闭)
const profileQuery = useQuery({
queryKey: ['worker-profile'],
@@ -626,6 +639,7 @@ export default function WorkerOrdersPage() {
}
const columns = buildWorkerOrderTableColumns({
productNameSuffixItems,
onOpenDetail: openOrderDetail,
onOpenAcceptance: openAcceptanceModal,
onOpenGiftMaterial: openGiftMaterialModal,
@@ -686,6 +700,7 @@ export default function WorkerOrdersPage() {
pagination={pagination}
page={page}
pageSize={pageSize}
productNameSuffixItems={productNameSuffixItems}
renderShareTable={renderShareTable}
loading={ordersQuery.isLoading}
searching={ordersQuery.isFetching}
@@ -767,6 +782,7 @@ export default function WorkerOrdersPage() {
isMobile={isMobile}
order={detailOrder}
orderTimelineVisible={orderTimelineVisible}
productNameSuffixItems={productNameSuffixItems}
onClose={() => setDetailOrder(null)}
onOpenAcceptance={openAcceptanceModal}
onOpenGiftMaterial={openGiftMaterialModal}
@@ -5,6 +5,7 @@ import type {
ConsignmentStatsSummary,
ConsignmentSubmission,
InventoryMatchableOrders,
PendingPistolMatchResult,
WorkOrderInventoryMatchCheck,
WorkOrderInventoryMatchOptions,
} from '@/types/consignment'
@@ -158,6 +159,22 @@ export function fetchInventoryMatchableOrders(
)
}
/** 全局匹配看板:非拼单待抢单手枪 × 在库待匹配现货,按细粒度评分排序。 */
export function fetchPendingPistolMatches(params?: {
keyword?: string
includeConflicts?: boolean
limit?: number
}) {
return apiGet<PendingPistolMatchResult>(
'/api/v1/admin/worker-platform/consignment/pending-pistol-matches',
{
keyword: params?.keyword?.trim() || undefined,
includeConflicts: params?.includeConflicts ? 'true' : undefined,
limit: params?.limit,
},
)
}
/** 履约/指派返回的电子凭证核销结果:ok=false 时必须提示运营人工核销,不能报成功 */
export type WorkOrderVoucherConsumeResult = {
ok: boolean
@@ -29,6 +29,7 @@ import type {
WorkerPlatformNotificationConfig,
WorkerProductMatchConfig,
WorkerAnnouncementConfig,
WorkerProductNameSuffixConfig,
WorkerLevel,
WorkerListResponse,
WorkerUserListResponse,
@@ -109,6 +110,22 @@ export function saveAdminWorkerAnnouncement(payload: WorkerAnnouncementConfig) {
return apiPost<WorkerAnnouncementConfig>('/api/v1/admin/worker-platform/announcement', payload)
}
export function fetchAdminWorkerProductNameSuffix() {
return apiGet<WorkerProductNameSuffixConfig>('/api/v1/admin/worker-platform/product-name-suffix')
}
/** 来源下拉选项:来自工单资料里的 source.provider */
export function fetchAdminWorkerProductNameSuffixSources() {
return apiGet<{ items: string[] }>('/api/v1/admin/worker-platform/product-name-suffix/sources')
}
export function saveAdminWorkerProductNameSuffix(payload: WorkerProductNameSuffixConfig) {
return apiPost<WorkerProductNameSuffixConfig>(
'/api/v1/admin/worker-platform/product-name-suffix',
payload,
)
}
export function fetchAdminWorkerCancelRequests(
status: 'pending' | 'approved' | 'rejected' = 'pending',
) {
+7
View File
@@ -11,6 +11,8 @@ import type {
WorkerCancelRequest,
WorkerCancelRequestRisk,
WorkerAnnouncementConfig,
WorkerProductNameSuffixConfig,
WorkerProductNameSuffixDisplayConfig,
WorkerAcceptanceConfig,
WorkerAcceptanceVideo,
WorkerOrderFeedback,
@@ -152,6 +154,11 @@ export function fetchWorkerAnnouncement() {
return apiGet<WorkerAnnouncementConfig>('/api/v1/worker/announcement')
}
/** 商品名来源后缀映射(后台「杂项配置」维护,只返回已启用项),打手端列表/详情/大厅展示用。 */
export function fetchWorkerProductNameSuffix() {
return apiGet<WorkerProductNameSuffixDisplayConfig>('/api/v1/worker/product-name-suffix')
}
/** 验收视频配置(后台可改):时长上限、目标高度、码率等,压缩前读取。 */
export function fetchWorkerAcceptanceConfig() {
return apiGet<WorkerAcceptanceConfig>('/api/v1/worker/acceptance/config')
+29
View File
@@ -240,3 +240,32 @@ export type WorkOrderInventoryMatchCheck = {
partialCount: number
matches: ConsignmentMatchResult[]
}
/** 后台「寄售仓库 → 匹配」:待抢单手枪 × 在库待匹配现货的配对明细 */
export type PendingPistolMatchPair = {
workOrderId: number
workOrderNo: string
platformOrderId: string
productName: string
workOrderCreatedAt: string | null
inventoryId: number
consignmentNo: string
inventoryNo: string
itemName: string
inventoryInboundAt: string | null
matchType: 'exact' | 'partial' | 'conflict'
score: number
reason: string
conflicts: string[]
}
export type PendingPistolMatchResult = {
pendingOrderCount: number
inStockInventoryCount: number
pairCount: number
exactCount: number
partialCount: number
conflictCount: number
truncated: boolean
pairs: PendingPistolMatchPair[]
}
@@ -325,6 +325,26 @@ export type WorkerAnnouncementConfig = {
content: string
}
export type WorkerProductNameSuffixItem = {
/** 来源标识(精确匹配、不区分大小写),如 91kaquan */
source: string
/** 括号内的后缀文字,如 91卡券 */
suffix: string
/** 是否启用;同一时间只允许启用一条 */
enabled: boolean
/** 后台备注(仅后台配置页可见) */
note: string
}
export type WorkerProductNameSuffixConfig = {
items: WorkerProductNameSuffixItem[]
}
/** 打手端只拿启用的来源 -> 后缀映射 */
export type WorkerProductNameSuffixDisplayConfig = {
items: Array<Pick<WorkerProductNameSuffixItem, 'source' | 'suffix'>>
}
export type WorkerHallConfig = {
maxVisibleOrders: number
showSubcategories?: boolean
@@ -622,6 +642,8 @@ export type WorkOrder = {
workOrderNo: string
platformOrderId: string
productName: string
/** 来源平台标识(如 91kaquan),仅打手端返回;用于按后台配置追加展示后缀 */
sourceProvider?: string
categoryId?: number | null
categoryName: string
parentCategoryId?: number | null
@@ -0,0 +1,69 @@
export type ProductNameSourceSuffixItem = {
/** 来源标识,如 91kaquan */
source: string
/** 括号内的后缀文字,如 91卡券 */
suffix: string
/** 未启用(false)的配置不参与展示;缺省按启用处理,兼容打手端精简数据 */
enabled?: boolean
}
/** 后缀只保留括号内的文字:填了外层全角/半角括号时自动剥离。 */
export function normalizeProductNameSuffixText(value: unknown): string {
let text = String(value ?? '').trim()
if (!text) return ''
const wrappedFullWidth = text.startsWith('(') && text.endsWith(')')
const wrappedHalfWidth = text.startsWith('(') && text.endsWith(')')
if ((wrappedFullWidth || wrappedHalfWidth) && text.length >= 2) {
text = text.slice(1, -1).trim()
}
return text
}
/**
* 按来源取后缀:来源精确匹配、不区分大小写,空来源或空后缀不命中。
* 同一来源配置多条时以第一条有效配置为准。
*/
export function resolveProductNameSourceSuffix(
items: ProductNameSourceSuffixItem[] | null | undefined,
provider: unknown,
): string {
const normalizedProvider = String(provider ?? '')
.trim()
.toLowerCase()
if (!normalizedProvider) return ''
for (const item of items ?? []) {
if (item?.enabled === false) continue
const source = String(item?.source ?? '')
.trim()
.toLowerCase()
if (!source || source !== normalizedProvider) continue
const suffix = normalizeProductNameSuffixText(item?.suffix)
if (suffix) return suffix
}
return ''
}
/**
* 在商品名后追加「(后缀)」;没有后缀时原样返回。
* 商品名已以相同后缀结尾(全角/半角括号均可)时不重复追加。
*/
export function appendProductNameSourceSuffix(productName: unknown, suffix: unknown): string {
const name = String(productName ?? '').trim()
const normalizedSuffix = normalizeProductNameSuffixText(suffix)
if (!name || !normalizedSuffix) return name
if (name.endsWith(`(${normalizedSuffix})`) || name.endsWith(`(${normalizedSuffix})`)) {
return name
}
return `${name}(${normalizedSuffix})`
}
/** 展示用:按来源匹配后一次性完成后缀追加;无匹配时返回原商品名。 */
export function applyProductNameSourceSuffix(
productName: unknown,
provider: unknown,
items: ProductNameSourceSuffixItem[] | null | undefined,
): string {
return appendProductNameSourceSuffix(productName, resolveProductNameSourceSuffix(items, provider))
}
@@ -0,0 +1,92 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
appendProductNameSourceSuffix,
applyProductNameSourceSuffix,
normalizeProductNameSuffixText,
resolveProductNameSourceSuffix,
} from '../src/utils/product-name-source-suffix.ts'
const items = [
{ source: '91kaquan', suffix: '91卡券' },
{ source: 'douyin', suffix: '抖音' },
]
test('来源后缀:空配置不追加', () => {
assert.equal(applyProductNameSourceSuffix('苏尔南手枪', '91kaquan', []), '苏尔南手枪')
assert.equal(applyProductNameSourceSuffix('苏尔南手枪', '91kaquan', null), '苏尔南手枪')
})
test('来源后缀:精确匹配后追加全角括号后缀', () => {
assert.equal(
applyProductNameSourceSuffix('苏尔南手枪', '91kaquan', items),
'苏尔南手枪(91卡券)',
)
})
test('来源后缀:来源匹配不区分大小写并去除首尾空格', () => {
assert.equal(resolveProductNameSourceSuffix(items, ' 91KAQUAN '), '91卡券')
assert.equal(
applyProductNameSourceSuffix('苏尔南手枪', ' 91Kaquan ', items),
'苏尔南手枪(91卡券)',
)
})
test('来源后缀:没有匹配来源时不追加', () => {
assert.equal(applyProductNameSourceSuffix('苏尔南手枪', 'kuaishou', items), '苏尔南手枪')
assert.equal(applyProductNameSourceSuffix('苏尔南手枪', '', items), '苏尔南手枪')
})
test('来源后缀:商品名已带相同后缀时不重复追加', () => {
assert.equal(
appendProductNameSourceSuffix('苏尔南手枪(91卡券)', '91卡券'),
'苏尔南手枪(91卡券)',
)
assert.equal(appendProductNameSourceSuffix('苏尔南手枪(91卡券)', '91卡券'), '苏尔南手枪(91卡券)')
})
test('来源后缀:后缀为空或商品名为空时不做处理', () => {
assert.equal(appendProductNameSourceSuffix('苏尔南手枪', ''), '苏尔南手枪')
assert.equal(appendProductNameSourceSuffix('', '91卡券'), '')
assert.equal(applyProductNameSourceSuffix('', '91kaquan', items), '')
})
test('来源后缀:配置里的外层括号会被剥离后再拼接', () => {
assert.equal(normalizeProductNameSuffixText('(91卡券)'), '91卡券')
assert.equal(normalizeProductNameSuffixText('(91卡券)'), '91卡券')
assert.equal(
applyProductNameSourceSuffix('苏尔南手枪', '91kaquan', [
{ source: '91kaquan', suffix: '(91卡券)' },
]),
'苏尔南手枪(91卡券)',
)
})
test('来源后缀:停用的配置不参与展示,启用后才追加', () => {
assert.equal(
applyProductNameSourceSuffix('苏尔南手枪', '91kaquan', [
{ source: '91kaquan', suffix: '91卡券', enabled: false },
]),
'苏尔南手枪',
)
assert.equal(
applyProductNameSourceSuffix('苏尔南手枪', '91kaquan', [
{ source: '91kaquan', suffix: '91卡券', enabled: true },
]),
'苏尔南手枪(91卡券)',
)
})
test('来源后缀:同一来源多条配置时取第一条有效后缀', () => {
assert.equal(
resolveProductNameSourceSuffix(
[
{ source: '91kaquan', suffix: '第一' },
{ source: '91kaquan', suffix: '第二' },
],
'91kaquan',
),
'第一',
)
})