refactor(fulfillment): 移除飞飞/乐玩通道,仅保留 affiliate_dash
新单只路由 affiliate_dash(未命中走 manual_dispatch);历史飞飞/乐玩任务保留只读 stub,避免订单重同步失败。同步清理 cloudtentacles 调度、Admin 写路径与相关 env。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,9 +20,6 @@ import WorkerLayout from '@/layouts/WorkerLayout'
|
||||
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
|
||||
const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
|
||||
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
|
||||
const AdminCloudtentaclesRecordsPage = lazy(
|
||||
() => import('@/pages/admin/AdminCloudtentaclesRecordsPage'),
|
||||
)
|
||||
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
|
||||
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
||||
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
||||
@@ -120,7 +117,6 @@ function AdminApplication() {
|
||||
<Route path="worker-platform" element={<AdminWorkerPlatformPage />} />
|
||||
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
|
||||
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
|
||||
<Route path="cloudtentacles-records" element={<AdminCloudtentaclesRecordsPage />} />
|
||||
<Route path="dev-mock" element={<AdminDevMockPage />} />
|
||||
</Route>
|
||||
<Route element={<RequireRole roles={['admin']} />}>
|
||||
|
||||
@@ -132,11 +132,6 @@ export default function AdminLayout() {
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
label: '电子凭证',
|
||||
},
|
||||
{
|
||||
key: '/admin/cloudtentacles-records',
|
||||
icon: <FileSearchOutlined />,
|
||||
label: '发货记录',
|
||||
},
|
||||
]
|
||||
|
||||
const items: MenuProps['items'] = [
|
||||
|
||||
@@ -1,670 +0,0 @@
|
||||
import {
|
||||
DownloadOutlined,
|
||||
FileImageOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Image as AntdImage,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import dayjs, { type Dayjs } from '@/lib/dayjs'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminCloudtentaclesDeliveryRecords,
|
||||
fetchAdminCloudtentaclesRecordSources,
|
||||
} from '@/services/admin'
|
||||
import type { AdminCloudtentaclesDeliveryRecordItem } from '@/types/admin'
|
||||
import { getAdminToken } from '@/utils/admin-auth'
|
||||
import {
|
||||
buildCloudtentaclesTablePagination,
|
||||
CLOUDTENTACLES_DEFAULT_PAGE_SIZE,
|
||||
} from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type RecordSourceOption = {
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
hasToken: boolean
|
||||
loggedInAt: string
|
||||
}
|
||||
|
||||
type DateRangeValue = [Dayjs | null, Dayjs | null] | null
|
||||
|
||||
export default function AdminCloudtentaclesRecordsPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [sourceLoading, setSourceLoading] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [sourceKey, setSourceKey] = useState('')
|
||||
const [platformOrderId, setPlatformOrderId] = useState('')
|
||||
const [dateRange, setDateRange] = useState<DateRangeValue>(createDefaultDateRange())
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(CLOUDTENTACLES_DEFAULT_PAGE_SIZE)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [items, setItems] = useState<AdminCloudtentaclesDeliveryRecordItem[]>([])
|
||||
const [sourceOptions, setSourceOptions] = useState<RecordSourceOption[]>([])
|
||||
const [previewVisible, setPreviewVisible] = useState(false)
|
||||
const [previewImageUrl, setPreviewImageUrl] = useState('')
|
||||
const [previewFileName, setPreviewFileName] = useState('')
|
||||
|
||||
const currentSourceLabel = useMemo(() => {
|
||||
const matched = sourceOptions.find((source) => source.key === sourceKey)
|
||||
return matched?.label || sourceKey || '-'
|
||||
}, [sourceKey, sourceOptions])
|
||||
|
||||
const columns: TableColumnsType<AdminCloudtentaclesDeliveryRecordItem> = [
|
||||
{
|
||||
title: '商品 / 角色',
|
||||
minWidth: 260,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong ellipsis={{ tooltip: row.name || '-' }}>
|
||||
{row.name || '-'}
|
||||
</Typography.Text>
|
||||
<span className="muted">账号:{row.fulfillUser || '-'}</span>
|
||||
{row.roleName || row.roleId ? (
|
||||
<span className="muted">
|
||||
角色:{row.roleName || '-'}
|
||||
{row.roleId ? `(${row.roleId})` : ''}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '订单 / 店铺',
|
||||
minWidth: 260,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong ellipsis={{ tooltip: row.platformOrderId || '-' }}>
|
||||
{row.platformOrderId || '-'}
|
||||
</Typography.Text>
|
||||
<span className="muted">订单店铺:{row.orderShopName || row.orderShopId || '-'}</span>
|
||||
<span className="muted">核销店铺:{row.consumeShopName || row.consumeShopId || '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '虚拟号',
|
||||
minWidth: 160,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<span>{row.phone || '-'}</span>
|
||||
<span className="muted">VN {row.virtualNumberId || '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 140,
|
||||
render: (_, row) => (
|
||||
<div className="status-stack">
|
||||
<Tag color={getStatusColor(row.status)}>{getStatusLabel(row.status)}</Tag>
|
||||
{row.localDispatchStatus ? (
|
||||
<span className="muted">{getLocalDispatchStatusLabel(row.localDispatchStatus)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
minWidth: 120,
|
||||
render: (_, row) => row.operatorName || '-',
|
||||
},
|
||||
{
|
||||
title: '发货时间',
|
||||
minWidth: 170,
|
||||
render: (_, row) => formatAdminDateTime(row.createdAt),
|
||||
},
|
||||
{
|
||||
title: '记录 ID',
|
||||
minWidth: 230,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text type="secondary" ellipsis={{ tooltip: row.recordId || '-' }}>
|
||||
{row.recordId || '-'}
|
||||
</Typography.Text>
|
||||
{row.taskNo ? (
|
||||
<span className="muted">
|
||||
任务:{row.taskNo}
|
||||
{getMatchConfidenceLabel(row.matchConfidence)
|
||||
? ` · 匹配${getMatchConfidenceLabel(row.matchConfidence)}`
|
||||
: ''}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 136,
|
||||
render: (_, row) => (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<FileImageOutlined />}
|
||||
onClick={() => generateRecordImage(row)}
|
||||
>
|
||||
生成图片
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
void bootstrap()
|
||||
}, [])
|
||||
|
||||
async function bootstrap() {
|
||||
const nextSourceKey = await loadSources()
|
||||
if (nextSourceKey) {
|
||||
await loadRecords(1, { sourceKey: nextSourceKey })
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSources() {
|
||||
setSourceLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesRecordSources()
|
||||
const nextSources = response.data.sources || []
|
||||
const usableSources = nextSources.filter((source) => source.enabled && source.hasToken)
|
||||
const nextSourceKey = sourceKey || usableSources[0]?.key || nextSources[0]?.key || ''
|
||||
setSourceOptions(nextSources)
|
||||
setSourceKey(nextSourceKey)
|
||||
return nextSourceKey
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 账号失败')
|
||||
return ''
|
||||
} finally {
|
||||
setSourceLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecords(
|
||||
nextPage = page,
|
||||
options: {
|
||||
sourceKey?: string
|
||||
pageSize?: number
|
||||
platformOrderId?: string
|
||||
dateRange?: DateRangeValue
|
||||
} = {},
|
||||
) {
|
||||
const nextSourceKey = options.sourceKey ?? sourceKey
|
||||
const nextPageSize = options.pageSize ?? pageSize
|
||||
const nextDateRange = options.dateRange ?? dateRange
|
||||
const orderKeyword = (options.platformOrderId ?? platformOrderId).trim()
|
||||
|
||||
if (!nextSourceKey && !orderKeyword) {
|
||||
showError('请先选择 kuaishou-lewan 账号')
|
||||
return
|
||||
}
|
||||
|
||||
const [startDate, endDate] = nextDateRange || []
|
||||
if (!startDate || !endDate) {
|
||||
showError('请选择查询时间范围')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setErrorMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesDeliveryRecords({
|
||||
sourceKey: nextSourceKey || undefined,
|
||||
page: nextPage,
|
||||
size: nextPageSize,
|
||||
startDate: formatQueryDateTime(startDate),
|
||||
endDate: formatQueryDateTime(endDate),
|
||||
platformOrderId: orderKeyword || undefined,
|
||||
})
|
||||
|
||||
if (response.data.sourceKey && response.data.sourceKey !== sourceKey) {
|
||||
setSourceKey(response.data.sourceKey)
|
||||
}
|
||||
|
||||
setPage(response.data.page)
|
||||
setPageSize(response.data.size)
|
||||
setTotal(response.data.total)
|
||||
setItems(response.data.items || [])
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '查询发货记录失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
const nextDateRange = createDefaultDateRange()
|
||||
setPlatformOrderId('')
|
||||
setDateRange(nextDateRange)
|
||||
setPage(1)
|
||||
setPageSize(CLOUDTENTACLES_DEFAULT_PAGE_SIZE)
|
||||
void loadRecords(1, {
|
||||
pageSize: CLOUDTENTACLES_DEFAULT_PAGE_SIZE,
|
||||
platformOrderId: '',
|
||||
dateRange: nextDateRange,
|
||||
})
|
||||
}
|
||||
|
||||
async function generateRecordImage(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
try {
|
||||
const dataUrl = await drawRecordImage(record)
|
||||
setPreviewImageUrl(dataUrl)
|
||||
setPreviewFileName(buildRecordImageFileName(record))
|
||||
setPreviewVisible(true)
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '生成发货记录图片失败')
|
||||
}
|
||||
}
|
||||
|
||||
function downloadPreviewImage() {
|
||||
if (!previewImageUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = previewImageUrl
|
||||
link.download = previewFileName || 'cloudtentacles-delivery-record.png'
|
||||
link.click()
|
||||
showSuccess('发货记录图片已生成')
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="page-stack">
|
||||
<PageHeader
|
||||
title="查询发货记录"
|
||||
description="按订单号、kuaishou-lewan 账号和时间范围查询平台发货记录。"
|
||||
extra={<span className="total-badge">共 {total} 条记录</span>}
|
||||
/>
|
||||
|
||||
<Card
|
||||
title="查询条件"
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
输入订单号时会先关联本地任务,再匹配 kuaishou-lewan 发货记录。
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<div className="cloud-records-filter">
|
||||
<Select
|
||||
value={sourceKey || undefined}
|
||||
loading={sourceLoading}
|
||||
placeholder="kuaishou-lewan 账号"
|
||||
options={sourceOptions.map((source) => ({
|
||||
label: `${source.label || source.key}${source.hasToken ? '' : ' · 未登录'}`,
|
||||
value: source.key,
|
||||
disabled: !source.enabled || !source.hasToken,
|
||||
}))}
|
||||
onChange={setSourceKey}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
value={platformOrderId}
|
||||
placeholder="订单号"
|
||||
onChange={(event) => setPlatformOrderId(event.target.value)}
|
||||
onPressEnter={() => loadRecords(1)}
|
||||
/>
|
||||
<AdminRangePicker
|
||||
showTime
|
||||
value={dateRange}
|
||||
className="full-width"
|
||||
onChange={(value) => setDateRange(value)}
|
||||
/>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
loading={loading}
|
||||
onClick={() => loadRecords(1)}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
|
||||
|
||||
<Card>
|
||||
<div className="table-toolbar">
|
||||
<strong>发货记录</strong>
|
||||
<span>
|
||||
账号 {currentSourceLabel} · 第 {page} 页,当前展示 {items.length} 条
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Table<AdminCloudtentaclesDeliveryRecordItem>
|
||||
rowKey={(row) => row.recordId || `${row.virtualNumberId}-${row.createdAt}`}
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
size="small"
|
||||
scroll={{ x: 1450 }}
|
||||
pagination={buildCloudtentaclesTablePagination({
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
void loadRecords(nextPage, { pageSize: nextPageSize })
|
||||
},
|
||||
})}
|
||||
locale={{ emptyText: loading ? '发货记录加载中' : '当前筛选条件下暂无发货记录' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="发货记录图片"
|
||||
open={previewVisible}
|
||||
width={780}
|
||||
destroyOnHidden
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setPreviewVisible(false)}>
|
||||
关闭
|
||||
</Button>,
|
||||
<Button
|
||||
key="download"
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={downloadPreviewImage}
|
||||
>
|
||||
下载图片
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className="record-preview">
|
||||
{previewImageUrl ? (
|
||||
<AntdImage src={previewImageUrl} alt="发货记录图片预览" preview={false} />
|
||||
) : null}
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function getStatusLabel(status: number) {
|
||||
if (status === 4) return '已完成'
|
||||
if (status === 1) return '处理中'
|
||||
if (status === 2) return '待处理'
|
||||
if (status === 3) return '失败'
|
||||
return `状态 ${status || '-'}`
|
||||
}
|
||||
|
||||
function getStatusColor(status: number) {
|
||||
if (status === 4) return 'green'
|
||||
if (status === 3) return 'red'
|
||||
if (status === 1) return 'orange'
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
function getLocalDispatchStatusLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (normalized === 'success') return '本地已发货'
|
||||
if (normalized === 'failed') return '本地发货失败'
|
||||
if (normalized === 'pending') return '本地待发货'
|
||||
return normalized || '-'
|
||||
}
|
||||
|
||||
function getMatchConfidenceLabel(value: string) {
|
||||
if (value === 'high') return '高'
|
||||
if (value === 'medium') return '中'
|
||||
if (value === 'low') return '低'
|
||||
if (value === 'local') return '本地'
|
||||
return ''
|
||||
}
|
||||
|
||||
function createDefaultDateRange(): DateRangeValue {
|
||||
const end = dayjs()
|
||||
return [end.subtract(7, 'day').startOf('day'), end]
|
||||
}
|
||||
|
||||
function formatQueryDateTime(value: Dayjs) {
|
||||
return value.format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
async function drawRecordImage(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const productImage = await loadRecordProductImage(record.skuImage)
|
||||
const canvas = document.createElement('canvas')
|
||||
const scale = window.devicePixelRatio || 1
|
||||
const width = 720
|
||||
const height = 330
|
||||
canvas.width = width * scale
|
||||
canvas.height = height * scale
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
throw new Error('当前浏览器不支持图片生成')
|
||||
}
|
||||
|
||||
ctx.scale(scale, scale)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.fillStyle = '#1f2937'
|
||||
ctx.font = '20px sans-serif'
|
||||
ctx.fillText('订单详情', 28, 38)
|
||||
ctx.fillStyle = '#9ca3af'
|
||||
ctx.font = '24px sans-serif'
|
||||
ctx.fillText('x', width - 40, 38)
|
||||
|
||||
ctx.fillStyle = '#111827'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(`购买时间:${formatAdminDateTime(record.createdAt)}`, width / 2, 88)
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
drawTicketCard(ctx, record, productImage)
|
||||
drawRecordMeta(ctx, record)
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
function drawTicketCard(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
record: AdminCloudtentaclesDeliveryRecordItem,
|
||||
productImage: HTMLImageElement | null,
|
||||
) {
|
||||
const x = 170
|
||||
const y = 108
|
||||
const width = 392
|
||||
const height = 108
|
||||
const imageWidth = 106
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + 18, y)
|
||||
ctx.lineTo(x + width, y)
|
||||
ctx.lineTo(x + width, y + height - 22)
|
||||
ctx.lineTo(x + width - 18, y + height)
|
||||
ctx.lineTo(x, y + height)
|
||||
ctx.lineTo(x, y + 18)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = '#f8fbff'
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = '#2f80d1'
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
|
||||
const gradient = ctx.createLinearGradient(x, y, x + imageWidth, y + height)
|
||||
gradient.addColorStop(0, '#e7f1ff')
|
||||
gradient.addColorStop(1, '#b8d7ff')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, y, imageWidth, height)
|
||||
|
||||
if (productImage) {
|
||||
drawCoverImage(ctx, productImage, x, y, imageWidth, height)
|
||||
} else {
|
||||
ctx.fillStyle = '#355d91'
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('套装发货记录', x + imageWidth / 2, y + 25)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(x + 34, y + 42, 38, 38)
|
||||
ctx.strokeStyle = '#8ab6ef'
|
||||
ctx.strokeRect(x + 34, y + 42, 38, 38)
|
||||
ctx.fillStyle = '#2f80d1'
|
||||
ctx.font = '20px sans-serif'
|
||||
ctx.fillText('GP', x + imageWidth / 2, y + 69)
|
||||
}
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
const contentX = x + imageWidth + 16
|
||||
const orderText = `订单号:${record.platformOrderId || record.recordId.slice(-8) || '-'}`
|
||||
ctx.fillStyle = '#1d5ea8'
|
||||
ctx.font = '16px sans-serif'
|
||||
drawEllipsisText(ctx, record.name || '未命名商品', contentX, y + 31, 170)
|
||||
ctx.fillStyle = '#3478c6'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(orderText, x + width - 12, y + 30)
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
ctx.strokeStyle = '#c8d8ec'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(contentX, y + 42)
|
||||
ctx.lineTo(x + width - 12, y + 42)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = '#f59e0b'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText(`购买机会 x ${record.count || 1}`, contentX, y + 66)
|
||||
ctx.fillStyle = '#16a34a'
|
||||
ctx.textAlign = 'right'
|
||||
ctx.fillText(getStatusLabel(record.status), x + width - 12, y + 66)
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
ctx.fillStyle = '#3478c6'
|
||||
ctx.font = '14px sans-serif'
|
||||
drawEllipsisText(ctx, `账号:${record.fulfillUser || record.phone || '-'}`, contentX, y + 90, 250)
|
||||
}
|
||||
|
||||
function drawRecordMeta(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
record: AdminCloudtentaclesDeliveryRecordItem,
|
||||
) {
|
||||
const left = 80
|
||||
const top = 250
|
||||
const lineHeight = 22
|
||||
const values = [
|
||||
`订单店铺:${record.orderShopName || record.orderShopId || '-'}`,
|
||||
`核销店铺:${record.consumeShopName || record.consumeShopId || '-'}`,
|
||||
`绑定角色:${record.roleName || record.fulfillUser || '-'}${record.roleId ? `(${record.roleId})` : ''}`,
|
||||
]
|
||||
|
||||
ctx.fillStyle = '#374151'
|
||||
ctx.font = '13px sans-serif'
|
||||
values.forEach((value, index) => {
|
||||
drawEllipsisText(ctx, value, left, top + index * lineHeight, 560)
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRecordProductImage(imageUrl: string) {
|
||||
const normalizedUrl = String(imageUrl || '').trim()
|
||||
if (!normalizedUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
const proxyUrl = `/api/v1/admin/cloudtentacles-records/image?url=${encodeURIComponent(
|
||||
normalizedUrl,
|
||||
)}`
|
||||
return loadProtectedImage(proxyUrl).catch(() => null)
|
||||
}
|
||||
|
||||
async function loadProtectedImage(src: string) {
|
||||
const token = getAdminToken()
|
||||
const response = await fetch(src, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('商品图片加载失败')
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(await response.blob())
|
||||
try {
|
||||
return await loadImage(objectUrl)
|
||||
} finally {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(src: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image()
|
||||
image.onload = () => resolve(image)
|
||||
image.onerror = () => reject(new Error('商品图片加载失败'))
|
||||
image.src = src
|
||||
})
|
||||
}
|
||||
|
||||
function drawCoverImage(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
image: HTMLImageElement,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
) {
|
||||
const ratio = Math.max(width / image.naturalWidth, height / image.naturalHeight)
|
||||
const drawWidth = image.naturalWidth * ratio
|
||||
const drawHeight = image.naturalHeight * ratio
|
||||
const drawX = x + (width - drawWidth) / 2
|
||||
const drawY = y + (height - drawHeight) / 2
|
||||
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.rect(x, y, width, height)
|
||||
ctx.clip()
|
||||
ctx.drawImage(image, drawX, drawY, drawWidth, drawHeight)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
function drawEllipsisText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth: number,
|
||||
) {
|
||||
let output = text
|
||||
while (output.length > 0 && ctx.measureText(output).width > maxWidth) {
|
||||
output = `${output.slice(0, -2)}...`
|
||||
}
|
||||
ctx.fillText(output || '-', x, y)
|
||||
}
|
||||
|
||||
function buildRecordImageFileName(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const name = sanitizeFileName(record.name || '发货记录')
|
||||
const id = sanitizeFileName(
|
||||
record.platformOrderId ||
|
||||
record.recordId.slice(0, 8) ||
|
||||
String(record.virtualNumberId || 'record'),
|
||||
)
|
||||
return `${name}-${id}.png`
|
||||
}
|
||||
|
||||
function sanitizeFileName(value: string) {
|
||||
return value.replace(/[\\/:*?"<>|]/g, '_').slice(0, 48)
|
||||
}
|
||||
@@ -22,10 +22,8 @@ import PageHeader from '@/components/admin/PageHeader'
|
||||
import {
|
||||
buildOpen91QueryDevMock,
|
||||
createAffiliateDashDevMock,
|
||||
createFeifeiDevMock,
|
||||
createKuaishouIndustryVoucherDevMock,
|
||||
createLewanDevMock,
|
||||
fetchDevMockStatus,
|
||||
createKuaishouIndustryVoucherDevMock,
|
||||
fetchDevMockStatus,
|
||||
type DevMockCreateResult,
|
||||
type DevMockKuaishouIndustryVoucherResult,
|
||||
type DevMockOpen91QueryResult,
|
||||
@@ -47,27 +45,7 @@ export default function AdminDevMockPage() {
|
||||
const status = statusQuery.data?.data
|
||||
const enabled = Boolean(status?.enabled)
|
||||
|
||||
const lewanMutation = useMutation({
|
||||
mutationFn: createLewanDevMock,
|
||||
onSuccess: (response) => {
|
||||
setLastCreate(response.data)
|
||||
message.success('lewan mock 已生成')
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error instanceof Error ? error.message : '生成失败')
|
||||
},
|
||||
})
|
||||
|
||||
const feifeiMutation = useMutation({
|
||||
mutationFn: createFeifeiDevMock,
|
||||
onSuccess: (response) => {
|
||||
setLastCreate(response.data)
|
||||
message.success('feifei mock 已生成')
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error instanceof Error ? error.message : '生成失败')
|
||||
},
|
||||
})
|
||||
|
||||
const affiliateDashMutation = useMutation({
|
||||
mutationFn: createAffiliateDashDevMock,
|
||||
@@ -133,7 +111,7 @@ export default function AdminDevMockPage() {
|
||||
<section className="page-stack">
|
||||
<PageHeader
|
||||
title="开发 Mock"
|
||||
description="一键造单、测领取页与 91 查单,无需真实 91 / 快手 / feifei 环境。"
|
||||
description="一键造单、测领取页与 91 查单,无需真实 91 / affiliate-dash 环境。"
|
||||
extra={
|
||||
<Typography.Text type="secondary">NODE_ENV={status?.nodeEnv || '-'}</Typography.Text>
|
||||
}
|
||||
@@ -143,32 +121,14 @@ export default function AdminDevMockPage() {
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<BugOutlined />}
|
||||
message="Mock 会写入开发库真实订单/任务,但 lewan 兑换走 mock 成功路径,feifei 不调用真实平台下单。"
|
||||
description="建议 UID 与默认角色一致:lewan 默认 10001,feifei 默认 166909256。"
|
||||
message="Mock 会写入开发库真实订单/任务;当前仅保留 affiliate-dash / 电子凭证 / 91 查单。"
|
||||
description="affiliate-dash 可用默认 UID 测试绑定与提交发货。"
|
||||
/>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} xl={12}>
|
||||
<Card title="1. lewan 领取(强制 UID)" bordered={false}>
|
||||
<LewanMockForm
|
||||
defaultUid={status?.defaultUid || '10001'}
|
||||
loading={lewanMutation.isPending}
|
||||
onSubmit={(values) => lewanMutation.mutate(values)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} xl={12}>
|
||||
<Card title="2. feifei 领取(拼 UID 到 H5)" bordered={false}>
|
||||
<FeifeiMockForm
|
||||
loading={feifeiMutation.isPending}
|
||||
onSubmit={(values) => feifeiMutation.mutate(values)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} xl={12}>
|
||||
<Card title="3. affiliate-dash 领取(mock 二维码/提交发货)" bordered={false}>
|
||||
<Card title="1. affiliate-dash 领取(mock 二维码/提交发货)" bordered={false}>
|
||||
<AffiliateDashMockForm
|
||||
loading={affiliateDashMutation.isPending}
|
||||
onSubmit={(values) => affiliateDashMutation.mutate(values)}
|
||||
@@ -177,7 +137,7 @@ export default function AdminDevMockPage() {
|
||||
</Col>
|
||||
|
||||
<Col xs={24} xl={12}>
|
||||
<Card title="4. 快手电子凭证测试数据" bordered={false}>
|
||||
<Card title="2. 快手电子凭证测试数据" bordered={false}>
|
||||
<KuaishouIndustryVoucherMockForm
|
||||
loading={voucherMutation.isPending}
|
||||
onSubmit={(values) => voucherMutation.mutate(values)}
|
||||
@@ -186,7 +146,7 @@ export default function AdminDevMockPage() {
|
||||
</Col>
|
||||
|
||||
<Col xs={24}>
|
||||
<Card title="5. 模拟 91 查单(open-91/query)" bordered={false}>
|
||||
<Card title="3. 模拟 91 查单(open-91/query)" bordered={false}>
|
||||
<Form
|
||||
layout="inline"
|
||||
initialValues={{ orderNo: defaultOrderNo, execute: '1' }}
|
||||
@@ -354,139 +314,6 @@ export default function AdminDevMockPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function LewanMockForm({
|
||||
defaultUid,
|
||||
loading,
|
||||
onSubmit,
|
||||
}: {
|
||||
defaultUid: string
|
||||
loading: boolean
|
||||
onSubmit: (values: Record<string, unknown>) => void
|
||||
}) {
|
||||
const [form] = Form.useForm()
|
||||
const step = Form.useWatch('step', form)
|
||||
const uidRequired = step !== 'uid'
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
step: 'uid',
|
||||
uid: defaultUid || '10001',
|
||||
productNo: '套餐_1----3676797936',
|
||||
}}
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="页面步骤"
|
||||
name="step"
|
||||
tooltip="uid=只到填编号第一步;binding=已预填编号且角色匹配;confirm/result=后续步骤"
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
|
||||
{ value: 'binding', label: 'binding · 绑定且 UID 已匹配(推荐)' },
|
||||
{ value: 'confirm', label: 'confirm · 待确认兑换' },
|
||||
{ value: 'result', label: 'result · 已完成' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="游戏 UID"
|
||||
name="uid"
|
||||
required={uidRequired}
|
||||
rules={
|
||||
uidRequired
|
||||
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
|
||||
: []
|
||||
}
|
||||
extra={
|
||||
step === 'uid'
|
||||
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
|
||||
: '后续步骤用于模拟已提交的编号,建议与默认角色一致(如 10001)'
|
||||
}
|
||||
>
|
||||
<Input allowClear placeholder={defaultUid || '10001'} />
|
||||
</Form.Item>
|
||||
<Form.Item label="91 商品 productNo" name="productNo">
|
||||
<Input placeholder="套餐_1----3676797936" />
|
||||
</Form.Item>
|
||||
<Form.Item label="订单号 orderNo(可空自动生成)" name="orderNo">
|
||||
<Input placeholder="留空自动 MOCK91…" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
生成 lewan Mock
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
function FeifeiMockForm({
|
||||
loading,
|
||||
onSubmit,
|
||||
}: {
|
||||
loading: boolean
|
||||
onSubmit: (values: Record<string, unknown>) => void
|
||||
}) {
|
||||
const [form] = Form.useForm()
|
||||
const step = Form.useWatch('step', form)
|
||||
const uidRequired = step !== 'uid'
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
step: 'uid',
|
||||
uid: '166909256',
|
||||
productName: '套装-浪漫天命',
|
||||
}}
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item label="页面步骤" name="step">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
|
||||
{ value: 'ready', label: 'ready · 已填 UID,可打开 H5(推荐)' },
|
||||
{ value: 'completed', label: 'completed · 模拟成功' },
|
||||
{ value: 'failed', label: 'failed · 模拟失败' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="游戏 UID"
|
||||
name="uid"
|
||||
required={uidRequired}
|
||||
rules={
|
||||
uidRequired
|
||||
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
|
||||
: []
|
||||
}
|
||||
extra={
|
||||
step === 'uid'
|
||||
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
|
||||
: '后续步骤用于模拟已提交的编号'
|
||||
}
|
||||
>
|
||||
<Input allowClear placeholder="166909256" />
|
||||
</Form.Item>
|
||||
<Form.Item label="商品名" name="productName">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="H5 链接(可空用 mock 地址)" name="h5Url">
|
||||
<Input placeholder="http://skin-exchange.../h5/bind?code=..." />
|
||||
</Form.Item>
|
||||
<Form.Item label="订单号 orderNo(可空)" name="orderNo">
|
||||
<Input placeholder="留空自动 MOCKFF…" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
生成 feifei Mock
|
||||
</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
function AffiliateDashMockForm({
|
||||
loading,
|
||||
onSubmit,
|
||||
|
||||
@@ -4,10 +4,6 @@ import {
|
||||
CloseCircleOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
RollbackOutlined,
|
||||
SendOutlined,
|
||||
SwapOutlined,
|
||||
ToolOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
@@ -38,16 +34,11 @@ import {
|
||||
closeAdminTask,
|
||||
completeAdminTaskManualDispatch,
|
||||
consumeAdminTaskKuaishouIndustryVoucher,
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
fetchAdminTaskDetail,
|
||||
fetchAdminTaskScreenshot,
|
||||
markAdminTaskManualReview,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
rebindAdminTaskKuaishouCloudRole,
|
||||
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||
resendAdminTaskKuaishouIndustryVoucherCode,
|
||||
retryAdminTask,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
} from '@/services/admin'
|
||||
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
@@ -275,38 +266,6 @@ export default function AdminTaskDetailPage() {
|
||||
`确认重试任务 ${resolvedDetail.task.taskNo} 吗?`,
|
||||
)
|
||||
}
|
||||
onRebindRole={() =>
|
||||
runTaskAction(
|
||||
'rebind-role',
|
||||
() => rebindAdminTaskKuaishouCloudRole(resolvedDetail.task.taskId),
|
||||
'换绑完成:已生成新的绑定链接',
|
||||
`确认为任务 ${resolvedDetail.task.taskNo} 换绑吗?会退还当前虚拟号并生成新绑链;若旧号已被系统回收会自动跳过退号并直接新取号。原领取链接可继续使用。`,
|
||||
)
|
||||
}
|
||||
onPrepareKuaishouCloud={() =>
|
||||
runTaskAction(
|
||||
'prepare-kuaishou-cloud',
|
||||
() => prepareAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId),
|
||||
'绑定资源已准备完成',
|
||||
`确认开始为任务 ${resolvedDetail.task.taskNo} 准备绑定资源吗?系统会自动检查库存配置并申请虚拟号。`,
|
||||
)
|
||||
}
|
||||
onDispatchKuaishouCloud={() =>
|
||||
runTaskAction(
|
||||
'dispatch-kuaishou-cloud',
|
||||
() => dispatchAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId),
|
||||
'已完成绑定确认并发货',
|
||||
`确认客户已经完成绑定,并立即为任务 ${resolvedDetail.task.taskNo} 执行发货吗?这个动作会把"确认绑定完成"和"发货"合并为一步。`,
|
||||
)
|
||||
}
|
||||
onReturnKuaishouCloud={() =>
|
||||
runTaskAction(
|
||||
'return-kuaishou-cloud',
|
||||
() => returnNumberAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId),
|
||||
'号码已退还',
|
||||
`确认退还任务 ${resolvedDetail.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
|
||||
)
|
||||
}
|
||||
onResendIndustryVoucher={() =>
|
||||
runTaskAction(
|
||||
'resend-industry-voucher',
|
||||
@@ -419,23 +378,11 @@ export default function AdminTaskDetailPage() {
|
||||
items={[
|
||||
{
|
||||
key: 'cloud',
|
||||
label: 'kuaishou-lewan',
|
||||
label: '乐玩云(历史)',
|
||||
children: flow ? (
|
||||
<KuaishouCloudPanel
|
||||
flow={flow}
|
||||
canRefresh={resolvedDetail.operations.canRefreshKuaishouCloudRoleInfo}
|
||||
actionLoadingKey={actionLoadingKey}
|
||||
onRefreshRole={() =>
|
||||
runTaskAction(
|
||||
'refresh-role',
|
||||
() => refreshAdminTaskKuaishouCloudRoleInfo(resolvedDetail.task.taskId),
|
||||
'角色信息已刷新',
|
||||
`确认刷新任务 ${resolvedDetail.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 kuaishou-lewan 的绑定结果。`,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<KuaishouCloudPanel flow={flow} />
|
||||
) : (
|
||||
<Empty description="无 kuaishou-lewan 履约上下文" />
|
||||
<Empty description="无历史乐玩云履约上下文" />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -496,10 +443,6 @@ function TaskActionPanel({
|
||||
canCloseTasks,
|
||||
actionLoadingKey,
|
||||
onRetry,
|
||||
onRebindRole,
|
||||
onPrepareKuaishouCloud,
|
||||
onDispatchKuaishouCloud,
|
||||
onReturnKuaishouCloud,
|
||||
onResendIndustryVoucher,
|
||||
onConsumeIndustryVoucher,
|
||||
onMarkManualReview,
|
||||
@@ -511,10 +454,6 @@ function TaskActionPanel({
|
||||
canCloseTasks: boolean
|
||||
actionLoadingKey: string
|
||||
onRetry: () => void
|
||||
onRebindRole: () => void
|
||||
onPrepareKuaishouCloud: () => void
|
||||
onDispatchKuaishouCloud: () => void
|
||||
onReturnKuaishouCloud: () => void
|
||||
onResendIndustryVoucher: () => void
|
||||
onConsumeIndustryVoucher: () => void
|
||||
onMarkManualReview: () => void
|
||||
@@ -522,11 +461,6 @@ function TaskActionPanel({
|
||||
}) {
|
||||
const operations = detail.operations
|
||||
const loading = Boolean(actionLoadingKey)
|
||||
const hasCloudActions =
|
||||
operations.canPrepareKuaishouCloudFulfillment ||
|
||||
operations.canRebindKuaishouCloudRole ||
|
||||
operations.canDispatchKuaishouCloudFulfillment ||
|
||||
operations.canReturnKuaishouCloudFulfillment
|
||||
const hasIndustryVoucherActions =
|
||||
operations.canResendKuaishouIndustryVoucherCode ||
|
||||
operations.canConsumeKuaishouIndustryVoucher ||
|
||||
@@ -560,57 +494,6 @@ function TaskActionPanel({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{hasCloudActions ? (
|
||||
<section className="task-action-group">
|
||||
<Typography.Text type="secondary">kuaishou-lewan</Typography.Text>
|
||||
<Space wrap>
|
||||
{canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? (
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ToolOutlined />}
|
||||
disabled={loading}
|
||||
loading={actionLoadingKey === 'prepare-kuaishou-cloud'}
|
||||
onClick={onPrepareKuaishouCloud}
|
||||
>
|
||||
准备资源
|
||||
</Button>
|
||||
) : null}
|
||||
{operations.canRebindKuaishouCloudRole ? (
|
||||
<Button
|
||||
icon={<SwapOutlined />}
|
||||
disabled={loading}
|
||||
loading={actionLoadingKey === 'rebind-role'}
|
||||
onClick={onRebindRole}
|
||||
>
|
||||
换绑
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SendOutlined />}
|
||||
disabled={loading}
|
||||
loading={actionLoadingKey === 'dispatch-kuaishou-cloud'}
|
||||
onClick={onDispatchKuaishouCloud}
|
||||
>
|
||||
发货
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? (
|
||||
<Button
|
||||
icon={<RollbackOutlined />}
|
||||
disabled={loading}
|
||||
loading={actionLoadingKey === 'return-kuaishou-cloud'}
|
||||
onClick={onReturnKuaishouCloud}
|
||||
>
|
||||
退号
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{canOperateIndustryVoucher && hasIndustryVoucherActions ? (
|
||||
<section className="task-action-group">
|
||||
<Typography.Text type="secondary">电子凭证</Typography.Text>
|
||||
@@ -652,10 +535,7 @@ function TaskActionPanel({
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{!canManageTaskLifecycle &&
|
||||
!canOperateIndustryVoucher &&
|
||||
!canCloseTasks &&
|
||||
!hasCloudActions ? (
|
||||
{!canManageTaskLifecycle && !canOperateIndustryVoucher && !canCloseTasks ? (
|
||||
<Typography.Text type="secondary">当前账号没有可执行的任务操作</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -964,14 +844,17 @@ function resolveDeliveryStepSummary(detail: AdminTaskDetail) {
|
||||
|
||||
function resolveTaskExecutorDisplay(value: unknown) {
|
||||
const executorKey = String(value || '').trim()
|
||||
if (executorKey === 'affiliate_dash') {
|
||||
return { label: 'Affiliate Dash', color: 'green' }
|
||||
}
|
||||
if (executorKey === 'kuaishou_ct_assisted') {
|
||||
return { label: 'kuaishou-lewan', color: 'blue' }
|
||||
return { label: '乐玩云(已下线)', color: 'default' }
|
||||
}
|
||||
if (executorKey === 'kuaishou-industry') {
|
||||
return { label: '行业电子凭证', color: 'cyan' }
|
||||
}
|
||||
if (executorKey === 'kuaishou_feifei') {
|
||||
return { label: 'kuaishou-feifei', color: 'purple' }
|
||||
return { label: '飞飞(已下线)', color: 'default' }
|
||||
}
|
||||
if (executorKey === 'manual_dispatch') {
|
||||
return { label: '人工履约', color: 'orange' }
|
||||
@@ -1152,14 +1035,8 @@ function KuaishouIndustryVoucherPanel({ voucher }: { voucher: KuaishouIndustryVo
|
||||
|
||||
function KuaishouCloudPanel({
|
||||
flow,
|
||||
canRefresh,
|
||||
actionLoadingKey,
|
||||
onRefreshRole,
|
||||
}: {
|
||||
flow: NonNullable<AdminTaskDetail['kuaishouCloudFulfillment']>
|
||||
canRefresh: boolean
|
||||
actionLoadingKey: string
|
||||
onRefreshRole: () => void
|
||||
}) {
|
||||
const roleInfoEntries = flow.role.rawInfo
|
||||
? Object.entries(flow.role.rawInfo).filter(
|
||||
@@ -1220,19 +1097,7 @@ function KuaishouCloudPanel({
|
||||
|
||||
return (
|
||||
<section className="kuaishou-cloud-stack">
|
||||
<Card
|
||||
title="kuaishou-lewan 履约"
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
disabled={Boolean(actionLoadingKey) || !canRefresh}
|
||||
loading={actionLoadingKey === 'refresh-role'}
|
||||
onClick={onRefreshRole}
|
||||
>
|
||||
刷新角色
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Card title="乐玩云履约(只读历史)">
|
||||
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
|
||||
<Descriptions.Item label="来源账号">
|
||||
{flow.binding.resolvedSourceLabel || flow.binding.resolvedSourceKey || '-'}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
CloseCircleOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
SwapOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
@@ -32,7 +31,6 @@ import {
|
||||
closeAdminTask,
|
||||
fetchAdminTasks,
|
||||
markAdminTaskManualReview,
|
||||
rebindAdminTaskKuaishouCloudRole,
|
||||
retryAdminTask,
|
||||
} from '@/services/admin'
|
||||
import type { AdminTaskActionResponse, AdminTaskListItem } from '@/types/admin'
|
||||
@@ -52,7 +50,7 @@ type TaskFilterForm = {
|
||||
dateRange?: unknown
|
||||
}
|
||||
|
||||
type TaskListAction = 'retry' | 'rebind_role' | 'manual_review' | 'close'
|
||||
type TaskListAction = 'retry' | 'manual_review' | 'close'
|
||||
type TaskListBlocker = {
|
||||
label: string
|
||||
color: string
|
||||
@@ -72,8 +70,6 @@ export default function AdminTasksPage() {
|
||||
const page = Number(searchParams.get('page') || 1) || 1
|
||||
const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const canManageTaskLifecycle = hasAdminRole('operator')
|
||||
// 与详情页一致:客服可换绑协助处理(canOperateAssistedTask)
|
||||
const canRebindRole = hasAdminRole('support')
|
||||
const canCloseTasks = hasAdminRole('support')
|
||||
const filters = {
|
||||
keyword:
|
||||
@@ -235,12 +231,11 @@ export default function AdminTasksPage() {
|
||||
canManageTaskLifecycle &&
|
||||
row.executorKey !== 'manual_dispatch' &&
|
||||
['retry_pending', 'manual_review'].includes(row.status)
|
||||
const canRebind = canRebindListTask(row, canRebindRole)
|
||||
const canManualReview =
|
||||
canManageTaskLifecycle &&
|
||||
!['redeemed', 'closed', 'completed', 'manual_review'].includes(row.status)
|
||||
const canClose = canCloseTasks && !['redeemed', 'closed'].includes(row.status)
|
||||
const hasActions = canRetry || canRebind || canManualReview || canClose
|
||||
const hasActions = canRetry || canManualReview || canClose
|
||||
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
@@ -264,17 +259,6 @@ export default function AdminTasksPage() {
|
||||
重试
|
||||
</Button>
|
||||
) : null}
|
||||
{canRebind ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
disabled={actionRunning}
|
||||
loading={rowLoading}
|
||||
onClick={() => handleActionCommand('rebind_role', row)}
|
||||
>
|
||||
换绑
|
||||
</Button>
|
||||
) : null}
|
||||
{canManualReview ? (
|
||||
<Button
|
||||
size="small"
|
||||
@@ -351,14 +335,6 @@ export default function AdminTasksPage() {
|
||||
}
|
||||
}
|
||||
|
||||
if (actionKey === 'rebind_role') {
|
||||
return {
|
||||
action: () => rebindAdminTaskKuaishouCloudRole(item.taskId),
|
||||
confirmText: `确认为任务 ${item.taskNo} 换绑角色吗?当前虚拟号会退还,并生成新的绑定二维码。`,
|
||||
successMessage: '角色换绑资源已准备完成',
|
||||
}
|
||||
}
|
||||
|
||||
if (actionKey === 'manual_review') {
|
||||
return {
|
||||
action: () => markAdminTaskManualReview(item.taskId),
|
||||
@@ -491,24 +467,19 @@ export default function AdminTasksPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function canRebindListTask(item: AdminTaskListItem, canRebindRole: boolean) {
|
||||
return (
|
||||
canRebindRole &&
|
||||
item.executorKey === 'kuaishou_ct_assisted' &&
|
||||
['waiting_binding', 'role_confirmed', 'manual_review', 'retry_pending'].includes(item.status)
|
||||
)
|
||||
}
|
||||
|
||||
function resolveTaskExecutorDisplay(value: unknown) {
|
||||
const executorKey = String(value || '').trim()
|
||||
if (executorKey === 'affiliate_dash') {
|
||||
return { key: executorKey, label: 'Affiliate Dash', color: 'green' }
|
||||
}
|
||||
if (executorKey === 'kuaishou_ct_assisted') {
|
||||
return { key: executorKey, label: '乐玩云', color: 'blue' }
|
||||
return { key: executorKey, label: '乐玩云(已下线)', color: 'default' }
|
||||
}
|
||||
if (executorKey === 'kuaishou-industry') {
|
||||
return { key: executorKey, label: '行业凭证', color: 'cyan' }
|
||||
}
|
||||
if (executorKey === 'kuaishou_feifei') {
|
||||
return { key: executorKey, label: '飞飞', color: 'purple' }
|
||||
return { key: executorKey, label: '飞飞(已下线)', color: 'default' }
|
||||
}
|
||||
if (executorKey === 'manual_dispatch') {
|
||||
return { key: executorKey, label: '人工履约', color: 'orange' }
|
||||
@@ -654,4 +625,4 @@ function cleanParams(params: Record<string, unknown>) {
|
||||
}
|
||||
})
|
||||
return next
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
Spin,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
@@ -29,51 +28,25 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminCloudtentaclesOverrideRules,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
fetchAdminFulfillmentRoutingConfig,
|
||||
fetchAdminKuaishouFeifeiConfig,
|
||||
matchAdminKuaishouFeifeiProduct,
|
||||
previewAdminFulfillmentRouting,
|
||||
saveAdminCloudtentaclesOverrideRules,
|
||||
saveAdminFulfillmentRoutingConfig,
|
||||
syncAdminKuaishouFeifeiProducts,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminFulfillmentRoutingConfig,
|
||||
AdminFulfillmentRoutingPreviewResult,
|
||||
AdminFulfillmentRoutingRule,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { buildAdminLocalTablePagination } from '@/utils/admin-pagination'
|
||||
|
||||
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
|
||||
normalizedProductName?: string
|
||||
}
|
||||
type EditableRoutingRule = Omit<AdminFulfillmentRoutingRule, 'normalizedProductName'> & {
|
||||
normalizedProductName?: string
|
||||
}
|
||||
|
||||
type SourceRow = AdminCloudtentaclesSourceItem & {
|
||||
session: AdminCloudtentaclesSessionItem | null
|
||||
ready: boolean
|
||||
}
|
||||
|
||||
const ROUTING_EXECUTORS = [
|
||||
{ key: 'kuaishou_ct_assisted', label: 'kuaishou-lewan', color: 'blue' },
|
||||
{ key: 'kuaishou_feifei', label: 'kuaishou-feifei', color: 'purple' },
|
||||
{ key: 'affiliate_dash', label: 'affiliate-dash', color: 'gold' },
|
||||
{ key: 'manual_dispatch', label: '人工履约', color: 'orange' },
|
||||
{ key: 'manual_dispatch', label: '人工履约', color: 'default' },
|
||||
]
|
||||
|
||||
export default function AdminPlatformFulfillmentPage() {
|
||||
@@ -81,7 +54,7 @@ export default function AdminPlatformFulfillmentPage() {
|
||||
<section className="page-stack">
|
||||
<PageHeader
|
||||
title="履约配置"
|
||||
description="维护 91 卡券商品到 kuaishou-feifei 与 kuaishou-lewan 的履约映射规则。"
|
||||
description="维护 91 卡券商品到 affiliate-dash / 人工履约的路由规则。"
|
||||
/>
|
||||
|
||||
{!hasAdminRole('admin') ? (
|
||||
@@ -89,27 +62,7 @@ export default function AdminPlatformFulfillmentPage() {
|
||||
<Empty description="仅管理员可以查看履约配置" />
|
||||
</Card>
|
||||
) : (
|
||||
<Tabs
|
||||
className="platform-tabs"
|
||||
destroyOnHidden={false}
|
||||
items={[
|
||||
{
|
||||
key: 'fulfillment-routing',
|
||||
label: '履约路由',
|
||||
children: <FulfillmentRoutingPanel />,
|
||||
},
|
||||
{
|
||||
key: 'kuaishou-feifei',
|
||||
label: 'kuaishou-feifei',
|
||||
children: <KuaishouFeifeiFulfillmentPanel />,
|
||||
},
|
||||
{
|
||||
key: 'kuaishou-cloud',
|
||||
label: 'kuaishou-lewan',
|
||||
children: <KuaishouCloudFulfillmentPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<FulfillmentRoutingPanel />
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
@@ -121,7 +74,7 @@ function FulfillmentRoutingPanel() {
|
||||
const [previewing, setPreviewing] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [enabled, setEnabled] = useState(true)
|
||||
const [priority, setPriority] = useState<string[]>(['kuaishou_ct_assisted', 'kuaishou_feifei'])
|
||||
const [priority, setPriority] = useState<string[]>(['affiliate_dash'])
|
||||
const [unmatchedExecutorKey, setUnmatchedExecutorKey] = useState('')
|
||||
const [executors, setExecutors] = useState<AdminFulfillmentRoutingConfig['executors']>({})
|
||||
const [rules, setRules] = useState<EditableRoutingRule[]>([])
|
||||
@@ -256,7 +209,7 @@ function FulfillmentRoutingPanel() {
|
||||
productName,
|
||||
normalizedProductName: '',
|
||||
matchType: 'exact',
|
||||
executorKey: 'kuaishou_feifei',
|
||||
executorKey: 'affiliate_dash',
|
||||
priority: 100,
|
||||
notes: '',
|
||||
},
|
||||
@@ -502,747 +455,6 @@ function FulfillmentRoutingPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function KuaishouFeifeiFulfillmentPanel() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [matching, setMatching] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [rules, setRules] = useState<AdminKuaishouFeifeiProductRule[]>([])
|
||||
const [matchInput, setMatchInput] = useState('')
|
||||
const [matchResult, setMatchResult] = useState<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||
const [lastSyncText, setLastSyncText] = useState('')
|
||||
|
||||
const enabledRuleCount = rules.filter((rule) => rule.enabled !== false).length
|
||||
const mappingCount = rules.filter(
|
||||
(rule) => rule.productName.trim() && rule.productCode.trim(),
|
||||
).length
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfig()
|
||||
}, [])
|
||||
|
||||
async function loadConfig() {
|
||||
setLoading(true)
|
||||
setErrorMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||
hydrateConfig(response.data)
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-feifei 履约映射失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function syncProducts() {
|
||||
setSyncing(true)
|
||||
setErrorMessage('')
|
||||
|
||||
try {
|
||||
const response = await syncAdminKuaishouFeifeiProducts({ status: 'on_sale' })
|
||||
hydrateConfig(response.data)
|
||||
const text = `商品 ${response.data.sync.productCount} 个,映射 ${response.data.sync.ruleCount} 条`
|
||||
setLastSyncText(text)
|
||||
showSuccess(`kuaishou-feifei 商品映射已同步,${text}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败'
|
||||
setErrorMessage(message)
|
||||
showError(message)
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function previewMatch() {
|
||||
const productName = matchInput.trim()
|
||||
if (!productName) {
|
||||
showError('请输入 91 商品名')
|
||||
return
|
||||
}
|
||||
|
||||
setMatching(true)
|
||||
setMatchResult(null)
|
||||
|
||||
try {
|
||||
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
||||
setMatchResult(response.data)
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '预览 kuaishou-feifei 映射失败')
|
||||
} finally {
|
||||
setMatching(false)
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||
setRules(
|
||||
Array.isArray(data.source.productRules)
|
||||
? data.source.productRules.map((rule) => normalizeFeifeiRule(rule))
|
||||
: [],
|
||||
)
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<AdminKuaishouFeifeiProductRule> = [
|
||||
{
|
||||
title: 'feifei 商品名',
|
||||
dataIndex: 'productName',
|
||||
minWidth: 260,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong ellipsis>
|
||||
{row.productName || row.skuName || '-'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" ellipsis>
|
||||
{row.normalizedProductName || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ title: 'product_code', dataIndex: 'productCode', minWidth: 150 },
|
||||
{ title: '展示名称', dataIndex: 'skuName', minWidth: 220 },
|
||||
{
|
||||
title: '状态',
|
||||
width: 100,
|
||||
render: (_, row) => (
|
||||
<Tag color={row.enabled !== false ? 'green' : 'default'}>
|
||||
{row.enabled !== false ? '启用' : '停用'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '来源', dataIndex: 'notes', minWidth: 180 },
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return <Spin />
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="platform-panel-stack">
|
||||
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
|
||||
|
||||
<div className="metric-grid three">
|
||||
<MetricCard
|
||||
label="名字映射"
|
||||
value={String(mappingCount)}
|
||||
detail={`启用 ${enabledRuleCount} 条`}
|
||||
/>
|
||||
<MetricCard label="匹配方式" value="同名" detail="使用 91 商品名规范化匹配" />
|
||||
<Card>
|
||||
<Space wrap>
|
||||
<Button icon={<ReloadOutlined />} loading={loading} onClick={loadConfig}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={syncing}
|
||||
onClick={syncProducts}
|
||||
>
|
||||
同步商品映射
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="商品名字映射"
|
||||
extra={
|
||||
<Typography.Text type="secondary">
|
||||
{lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。'}
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<Space.Compact className="full-width">
|
||||
<Input
|
||||
allowClear
|
||||
value={matchInput}
|
||||
placeholder="91 商品名,例如 套装-Alan Walker"
|
||||
onChange={(event) => setMatchInput(event.target.value)}
|
||||
onPressEnter={previewMatch}
|
||||
/>
|
||||
<Button icon={<SearchOutlined />} loading={matching} onClick={previewMatch}>
|
||||
预览命中
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{matchResult ? (
|
||||
<Alert
|
||||
className="platform-section-gap"
|
||||
type={matchResult.matched ? 'success' : 'warning'}
|
||||
showIcon
|
||||
message={matchResult.matched ? '已命中' : '未命中'}
|
||||
description={
|
||||
matchResult.match
|
||||
? `${matchResult.match.productName} -> ${matchResult.match.productCode}`
|
||||
: matchResult.normalizedProductName || matchResult.productName
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Table<AdminKuaishouFeifeiProductRule>
|
||||
rowKey={(row) => row.id || `${row.productName}-${row.productCode}`}
|
||||
columns={columns}
|
||||
dataSource={rules}
|
||||
pagination={buildAdminLocalTablePagination()}
|
||||
scroll={{ x: 920, y: 520 }}
|
||||
locale={{ emptyText: '暂无 kuaishou-feifei 商品映射' }}
|
||||
className="platform-section-gap"
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function KuaishouCloudFulfillmentPanel() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [skuLoading, setSkuLoading] = useState(false)
|
||||
const [overrideSaving, setOverrideSaving] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [skuErrorMessage, setSkuErrorMessage] = useState('')
|
||||
const [overrideErrorMessage, setOverrideErrorMessage] = useState('')
|
||||
const [selectedSourceKey, setSelectedSourceKey] = useState('')
|
||||
const [sources, setSources] = useState<AdminCloudtentaclesSourceItem[]>([])
|
||||
const [sessions, setSessions] = useState<AdminCloudtentaclesSessionsMap>({})
|
||||
const [skuItems, setSkuItems] = useState<AdminCloudtentaclesSkuItem[]>([])
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(true)
|
||||
const [overrideRules, setOverrideRules] = useState<EditableOverrideRule[]>([])
|
||||
const [matchInput, setMatchInput] = useState('')
|
||||
|
||||
const sourceRows = useMemo(
|
||||
() =>
|
||||
sources.map((source) => {
|
||||
const session = sessions[source.key] || null
|
||||
return {
|
||||
...source,
|
||||
session,
|
||||
ready: source.enabled !== false && Boolean(session?.hasToken),
|
||||
}
|
||||
}),
|
||||
[sessions, sources],
|
||||
)
|
||||
const readySources = sourceRows.filter((source) => source.ready)
|
||||
const selectedSource = sourceRows.find((source) => source.key === selectedSourceKey) || null
|
||||
const matchedSku = useMemo(() => {
|
||||
const normalizedInput = normalizeMatchName(matchInput)
|
||||
if (!normalizedInput) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
skuItems.find((item) => String(item.name || '').trim() === matchInput.trim()) ||
|
||||
skuItems.find((item) => normalizeMatchName(item.name) === normalizedInput) ||
|
||||
null
|
||||
)
|
||||
}, [matchInput, skuItems])
|
||||
const metrics = {
|
||||
sourceCount: sources.length,
|
||||
readySourceCount: readySources.length,
|
||||
skuCount: skuItems.length,
|
||||
inventoryTotal: skuItems.reduce(
|
||||
(sum, item) => sum + Math.max(0, Number(item.inventory || 0)),
|
||||
0,
|
||||
),
|
||||
overrideRuleCount: overrideRules.length,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadPage()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSourceKey) {
|
||||
void loadSkuList(selectedSourceKey)
|
||||
} else {
|
||||
setSkuItems([])
|
||||
}
|
||||
}, [selectedSourceKey])
|
||||
|
||||
async function loadPage() {
|
||||
setLoading(true)
|
||||
setErrorMessage('')
|
||||
|
||||
try {
|
||||
const [sourceResponse, overrideResponse] = await Promise.all([
|
||||
fetchAdminCloudtentaclesSourceConfig(),
|
||||
fetchAdminCloudtentaclesOverrideRules(),
|
||||
])
|
||||
const nextSources = Array.isArray(sourceResponse.data.sources)
|
||||
? sourceResponse.data.sources
|
||||
: []
|
||||
const nextSessions = sourceResponse.data.sessions || {}
|
||||
const nextRows = nextSources.map((source) => {
|
||||
const session = nextSessions[source.key] || null
|
||||
return {
|
||||
...source,
|
||||
session,
|
||||
ready: source.enabled !== false && Boolean(session?.hasToken),
|
||||
}
|
||||
})
|
||||
|
||||
setSources(nextSources)
|
||||
setSessions(nextSessions)
|
||||
setOverrideEnabled(overrideResponse.data.enabled !== false)
|
||||
setOverrideRules(
|
||||
Array.isArray(overrideResponse.data.rules)
|
||||
? overrideResponse.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||
: [],
|
||||
)
|
||||
setSelectedSourceKey(resolveDefaultSourceKey(nextRows, selectedSourceKey))
|
||||
} catch (error) {
|
||||
setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 履约配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkuList(sourceKey = selectedSourceKey) {
|
||||
if (!sourceKey) {
|
||||
setSkuItems([])
|
||||
return
|
||||
}
|
||||
|
||||
setSkuLoading(true)
|
||||
setSkuErrorMessage('')
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesSkuList({ sourceKey })
|
||||
setSkuItems(Array.isArray(response.data.items) ? response.data.items : [])
|
||||
} catch (error) {
|
||||
setSkuItems([])
|
||||
setSkuErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 商品失败')
|
||||
} finally {
|
||||
setSkuLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOverrideRules() {
|
||||
setOverrideSaving(true)
|
||||
setOverrideErrorMessage('')
|
||||
|
||||
try {
|
||||
const response = await saveAdminCloudtentaclesOverrideRules({
|
||||
enabled: overrideEnabled,
|
||||
rules: overrideRules.map((rule) => ({
|
||||
id: rule.id,
|
||||
enabled: rule.enabled,
|
||||
productName: rule.productName,
|
||||
sourceKey: rule.sourceKey,
|
||||
deliveryItems: rule.deliveryItems,
|
||||
notes: rule.notes,
|
||||
})),
|
||||
})
|
||||
setOverrideEnabled(response.data.enabled !== false)
|
||||
setOverrideRules(
|
||||
Array.isArray(response.data.rules)
|
||||
? response.data.rules.map((rule) => normalizeEditableRule(rule))
|
||||
: [],
|
||||
)
|
||||
showSuccess(`商品覆盖规则已保存,共 ${response.data.rules.length} 条`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '保存 kuaishou-lewan 覆盖规则失败'
|
||||
setOverrideErrorMessage(message)
|
||||
showError(message)
|
||||
} finally {
|
||||
setOverrideSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
function addOverrideRule() {
|
||||
setOverrideRules((current) => [
|
||||
{
|
||||
id: createRuleId(),
|
||||
enabled: true,
|
||||
productName: matchInput.trim(),
|
||||
normalizedProductName: normalizeMatchName(matchInput),
|
||||
sourceKey: selectedSourceKey,
|
||||
deliveryItems: [createDeliveryItem(skuItems)],
|
||||
notes: '',
|
||||
},
|
||||
...current,
|
||||
])
|
||||
}
|
||||
|
||||
function updateRule(index: number, patch: Partial<EditableOverrideRule>) {
|
||||
setOverrideRules((current) =>
|
||||
current.map((rule, ruleIndex) => (ruleIndex === index ? { ...rule, ...patch } : rule)),
|
||||
)
|
||||
}
|
||||
|
||||
function updateDeliveryItem(
|
||||
ruleIndex: number,
|
||||
itemIndex: number,
|
||||
patch: Partial<AdminCloudtentaclesOverrideDeliveryItem>,
|
||||
) {
|
||||
setOverrideRules((current) =>
|
||||
current.map((rule, currentRuleIndex) => {
|
||||
if (currentRuleIndex !== ruleIndex) {
|
||||
return rule
|
||||
}
|
||||
|
||||
return {
|
||||
...rule,
|
||||
deliveryItems: rule.deliveryItems.map((item, currentItemIndex) =>
|
||||
currentItemIndex === itemIndex ? { ...item, ...patch } : item,
|
||||
),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function removeOverrideRule(index: number) {
|
||||
setOverrideRules((current) => current.filter((_, ruleIndex) => ruleIndex !== index))
|
||||
}
|
||||
|
||||
function addDeliveryItem(ruleIndex: number) {
|
||||
setOverrideRules((current) =>
|
||||
current.map((rule, currentRuleIndex) =>
|
||||
currentRuleIndex === ruleIndex
|
||||
? { ...rule, deliveryItems: [...rule.deliveryItems, createDeliveryItem(skuItems)] }
|
||||
: rule,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function removeDeliveryItem(ruleIndex: number, itemIndex: number) {
|
||||
setOverrideRules((current) =>
|
||||
current.map((rule, currentRuleIndex) => {
|
||||
if (currentRuleIndex !== ruleIndex) {
|
||||
return rule
|
||||
}
|
||||
|
||||
const nextItems = rule.deliveryItems.filter(
|
||||
(_, currentItemIndex) => currentItemIndex !== itemIndex,
|
||||
)
|
||||
return {
|
||||
...rule,
|
||||
deliveryItems: nextItems.length ? nextItems : [createDeliveryItem(skuItems)],
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function handleDeliverySkuChange(ruleIndex: number, itemIndex: number, skuId: number) {
|
||||
const sku = skuItems.find((item) => Number(item.id || 0) === Number(skuId || 0))
|
||||
updateDeliveryItem(ruleIndex, itemIndex, {
|
||||
cloudSkuId: Number(skuId || 0) || 0,
|
||||
cloudSkuName: String(sku?.name || '').trim(),
|
||||
})
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Spin />
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="platform-panel-stack">
|
||||
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
|
||||
|
||||
<div className="metric-grid five">
|
||||
<MetricCard
|
||||
label="履约账号"
|
||||
value={`${metrics.readySourceCount} / ${metrics.sourceCount}`}
|
||||
/>
|
||||
<MetricCard label="可发商品" value={String(metrics.skuCount)} />
|
||||
<MetricCard label="总库存" value={String(metrics.inventoryTotal)} />
|
||||
<MetricCard label="覆盖规则" value={String(metrics.overrideRuleCount)} />
|
||||
<Card>
|
||||
<Button icon={<ReloadOutlined />} loading={loading || skuLoading} onClick={loadPage}>
|
||||
刷新
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="platform-split-layout">
|
||||
<Card title="kuaishou-lewan 账号" className="platform-side-card">
|
||||
{sourceRows.length === 0 ? (
|
||||
<Empty description="暂无账号,请先到平台配置登录。" />
|
||||
) : (
|
||||
<Space direction="vertical" className="full-width">
|
||||
{sourceRows.map((source) => (
|
||||
<button
|
||||
key={source.key}
|
||||
type="button"
|
||||
className={
|
||||
source.key === selectedSourceKey
|
||||
? 'platform-select-card active'
|
||||
: 'platform-select-card'
|
||||
}
|
||||
onClick={() => setSelectedSourceKey(source.key)}
|
||||
>
|
||||
<span>
|
||||
<strong>{formatSourceLabel(source)}</strong>
|
||||
<small>{source.username || source.key}</small>
|
||||
</span>
|
||||
<Tag
|
||||
color={source.ready ? 'green' : source.enabled === false ? 'default' : 'orange'}
|
||||
>
|
||||
{source.ready ? '可用' : source.enabled === false ? '已停用' : '未登录'}
|
||||
</Tag>
|
||||
</button>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<section className="platform-panel-stack">
|
||||
<Card
|
||||
title="匹配状态与覆盖规则"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Typography.Text type="secondary">
|
||||
当前账号:{selectedSource ? formatSourceLabel(selectedSource) : '-'}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={skuLoading}
|
||||
disabled={!selectedSourceKey}
|
||||
onClick={() => loadSkuList()}
|
||||
>
|
||||
刷新商品
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{skuErrorMessage ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={skuErrorMessage}
|
||||
className="platform-section-gap"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Space className="full-width" direction="vertical">
|
||||
<Space.Compact className="full-width">
|
||||
<Input
|
||||
allowClear
|
||||
value={matchInput}
|
||||
placeholder="输入 91 卡券 productNo / 商品名字,查看命中的 kuaishou-lewan 商品"
|
||||
onChange={(event) => setMatchInput(event.target.value)}
|
||||
/>
|
||||
<Button icon={<PlusOutlined />} onClick={addOverrideRule}>
|
||||
生成规则
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{matchedSku ? (
|
||||
<Tag color="green">
|
||||
命中 #{matchedSku.id} {matchedSku.name}
|
||||
</Tag>
|
||||
) : matchInput.trim() ? (
|
||||
<Tag color="orange">未命中</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="商品覆盖规则"
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Switch
|
||||
checked={overrideEnabled}
|
||||
checkedChildren="启用"
|
||||
unCheckedChildren="停用"
|
||||
onChange={setOverrideEnabled}
|
||||
/>
|
||||
<Button icon={<PlusOutlined />} onClick={addOverrideRule}>
|
||||
新增规则
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={overrideSaving}
|
||||
onClick={saveOverrideRules}
|
||||
>
|
||||
保存规则
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{overrideErrorMessage ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={overrideErrorMessage}
|
||||
className="platform-section-gap"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{overrideRules.length === 0 ? (
|
||||
<Empty description="暂无覆盖规则" />
|
||||
) : (
|
||||
<Space direction="vertical" className="full-width" size={12}>
|
||||
{overrideRules.map((rule, ruleIndex) => (
|
||||
<Card
|
||||
key={rule.id}
|
||||
size="small"
|
||||
title={
|
||||
<Space>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={rule.enabled !== false}
|
||||
onChange={(enabled) => updateRule(ruleIndex, { enabled })}
|
||||
/>
|
||||
<span>{rule.productName || '未命名规则'}</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => removeOverrideRule(ruleIndex)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="platform-rule-head">
|
||||
<Input
|
||||
value={rule.productName}
|
||||
placeholder="91 商品名 / productNo"
|
||||
onChange={(event) =>
|
||||
updateRule(ruleIndex, { productName: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
value={rule.sourceKey || undefined}
|
||||
placeholder="全部账号"
|
||||
options={[
|
||||
{ label: '全部账号', value: '' },
|
||||
...sourceRows.map((source) => ({
|
||||
label: formatSourceLabel(source),
|
||||
value: source.key,
|
||||
})),
|
||||
]}
|
||||
onChange={(sourceKey) => {
|
||||
updateRule(ruleIndex, { sourceKey: String(sourceKey || '') })
|
||||
if (sourceKey) {
|
||||
setSelectedSourceKey(String(sourceKey))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Space
|
||||
direction="vertical"
|
||||
className="full-width platform-section-gap"
|
||||
size={8}
|
||||
>
|
||||
{rule.deliveryItems.map((item, itemIndex) => (
|
||||
<div key={`${rule.id}-${itemIndex}`} className="platform-delivery-row">
|
||||
<Select
|
||||
showSearch
|
||||
value={item.cloudSkuId || undefined}
|
||||
placeholder="选择 kuaishou-lewan 商品"
|
||||
optionFilterProp="label"
|
||||
options={skuItems.map((sku) => ({
|
||||
label: formatSkuOption(sku),
|
||||
value: sku.id,
|
||||
}))}
|
||||
onChange={(skuId) =>
|
||||
handleDeliverySkuChange(ruleIndex, itemIndex, Number(skuId || 0))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={item.cloudSkuName}
|
||||
placeholder="kuaishou-lewan 商品名"
|
||||
onChange={(event) =>
|
||||
updateDeliveryItem(ruleIndex, itemIndex, {
|
||||
cloudSkuName: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={999}
|
||||
value={item.quantity}
|
||||
onChange={(quantity) =>
|
||||
updateDeliveryItem(ruleIndex, itemIndex, {
|
||||
quantity: Math.max(1, Number(quantity || 1) || 1),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button onClick={() => removeDeliveryItem(ruleIndex, itemIndex)}>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button onClick={() => addDeliveryItem(ruleIndex)}>增加发货商品</Button>
|
||||
</Space>
|
||||
|
||||
<Input
|
||||
className="platform-section-gap"
|
||||
value={rule.notes}
|
||||
placeholder="备注"
|
||||
onChange={(event) => updateRule(ruleIndex, { notes: event.target.value })}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="kuaishou-lewan 商品列表">
|
||||
<Table<AdminCloudtentaclesSkuItem>
|
||||
rowKey="id"
|
||||
loading={skuLoading}
|
||||
dataSource={skuItems}
|
||||
columns={[
|
||||
{
|
||||
title: 'kuaishou-lewan 商品',
|
||||
dataIndex: 'name',
|
||||
minWidth: 260,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong ellipsis>
|
||||
{row.name || '-'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" ellipsis>
|
||||
#{row.id} · {row.description || row.name || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '91 自动匹配名',
|
||||
dataIndex: 'name',
|
||||
minWidth: 220,
|
||||
render: (value) => <code>{String(value || '-')}</code>,
|
||||
},
|
||||
{ title: '价格', dataIndex: 'price', width: 120, render: formatCloudPrice },
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'inventory',
|
||||
width: 120,
|
||||
sorter: (a, b) => a.inventory - b.inventory,
|
||||
},
|
||||
{
|
||||
title: '发货限制',
|
||||
width: 150,
|
||||
render: (_, row) => `${row.buyLimitMin || 1} - ${row.buyLimitMax || 1}`,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 120,
|
||||
render: (_, row) => (
|
||||
<Tag color={Number(row.inventory || 0) > 0 ? 'green' : 'red'}>
|
||||
{Number(row.inventory || 0) > 0 ? '可履约' : '无库存'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
]}
|
||||
pagination={buildAdminLocalTablePagination()}
|
||||
scroll={{ x: 1000, y: 420 }}
|
||||
locale={{ emptyText: '当前账号暂无可展示商品' }}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) {
|
||||
return (
|
||||
<Card className="metric-card">
|
||||
@@ -1263,7 +475,7 @@ function normalizeRoutingPriority(value: unknown) {
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter((item) => allowed.includes(item) && !seen.has(item) && (seen.add(item), true))
|
||||
|
||||
return items.length > 0 ? items : ['kuaishou_ct_assisted', 'kuaishou_feifei']
|
||||
return items.length > 0 ? items : ['affiliate_dash']
|
||||
}
|
||||
|
||||
function normalizeRoutingExecutors(value: unknown): AdminFulfillmentRoutingConfig['executors'] {
|
||||
@@ -1293,7 +505,7 @@ function normalizeRoutingRule(rule: AdminFulfillmentRoutingRule): EditableRoutin
|
||||
productName,
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
matchType: rule.matchType === 'contains' ? 'contains' : 'exact',
|
||||
executorKey: executorKey || 'kuaishou_feifei',
|
||||
executorKey: executorKey || 'affiliate_dash',
|
||||
priority: Number(rule.priority || 0) || 0,
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}
|
||||
@@ -1309,87 +521,6 @@ function resolveRoutingExecutorColor(executorKey: unknown) {
|
||||
return ROUTING_EXECUTORS.find((executor) => executor.key === key)?.color || 'default'
|
||||
}
|
||||
|
||||
function normalizeFeifeiRule(
|
||||
rule: Partial<AdminKuaishouFeifeiProductRule>,
|
||||
): AdminKuaishouFeifeiProductRule {
|
||||
const productName = String(rule.productName || '').trim()
|
||||
const productCode = String(rule.productCode || '').trim()
|
||||
|
||||
return {
|
||||
id: String(rule.id || productName || productCode).trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
productName,
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
productCode,
|
||||
skuName: String(rule.skuName || productName || productCode).trim(),
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeEditableRule(rule: AdminCloudtentaclesOverrideRule): EditableOverrideRule {
|
||||
return {
|
||||
id: String(rule.id || createRuleId()).trim(),
|
||||
enabled: rule.enabled !== false,
|
||||
productName: String(rule.productName || '').trim(),
|
||||
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
||||
sourceKey: String(rule.sourceKey || '').trim(),
|
||||
deliveryItems:
|
||||
Array.isArray(rule.deliveryItems) && rule.deliveryItems.length > 0
|
||||
? rule.deliveryItems.map((item) => ({
|
||||
cloudSkuId: Number(item.cloudSkuId || 0) || 0,
|
||||
cloudSkuName: String(item.cloudSkuName || '').trim(),
|
||||
quantity: Math.max(1, Number(item.quantity || 1) || 1),
|
||||
}))
|
||||
: [createDeliveryItem([])],
|
||||
notes: String(rule.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function createDeliveryItem(
|
||||
skuItems: AdminCloudtentaclesSkuItem[],
|
||||
): AdminCloudtentaclesOverrideDeliveryItem {
|
||||
const firstSku = skuItems[0]
|
||||
return {
|
||||
cloudSkuId: Number(firstSku?.id || 0) || 0,
|
||||
cloudSkuName: String(firstSku?.name || '').trim(),
|
||||
quantity: 1,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDefaultSourceKey(sourceRows: SourceRow[], selectedSourceKey: string) {
|
||||
const current = sourceRows.find((source) => source.key === selectedSourceKey)
|
||||
if (current?.ready) {
|
||||
return current.key
|
||||
}
|
||||
|
||||
return sourceRows.find((source) => source.ready)?.key || sourceRows[0]?.key || ''
|
||||
}
|
||||
|
||||
function normalizeMatchName(value: unknown) {
|
||||
return String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[【】[\]()()]/g, ' ')
|
||||
.replace(/(自动发货|秒发|极速发货|官方直充|官方充值)/gi, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function formatSourceLabel(
|
||||
source: Pick<AdminCloudtentaclesSourceItem, 'key' | 'label' | 'username'>,
|
||||
) {
|
||||
return source.label || source.username || source.key
|
||||
}
|
||||
|
||||
function formatCloudPrice(value: unknown) {
|
||||
const price = Number(value)
|
||||
return Number.isFinite(price) ? String(price) : '-'
|
||||
}
|
||||
|
||||
function formatSkuOption(sku: AdminCloudtentaclesSkuItem) {
|
||||
return `#${sku.id} ${sku.name}`
|
||||
}
|
||||
|
||||
function createRuleId() {
|
||||
return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,100 +0,0 @@
|
||||
import { CheckCircleOutlined, ExclamationCircleOutlined, LinkOutlined } from '@ant-design/icons'
|
||||
import { Alert, Button, Card, Typography } from 'antd'
|
||||
import type { ClaimKuaishouFeifeiFlowInfo, ClaimOrderInfo, ClaimProductInfo } from '@/types/claim'
|
||||
import type { ClaimSnapshot } from './claim-snapshot'
|
||||
import { InfoTile, UidEditRow } from './claim-shared'
|
||||
|
||||
export function FeifeiClaimPanel({
|
||||
feifei,
|
||||
expectedUid,
|
||||
order,
|
||||
product,
|
||||
taskLastError,
|
||||
onOpen,
|
||||
uidInput,
|
||||
submittingUid,
|
||||
onUidChange,
|
||||
onResubmitUid,
|
||||
}: {
|
||||
feifei: ClaimKuaishouFeifeiFlowInfo
|
||||
expectedUid: string
|
||||
order: ClaimOrderInfo | null
|
||||
product: ClaimProductInfo | null
|
||||
taskLastError?: string
|
||||
onOpen: () => void
|
||||
uidInput: string
|
||||
submittingUid: boolean
|
||||
onUidChange: (value: string) => void
|
||||
onResubmitUid: () => void
|
||||
}) {
|
||||
const openReady = Boolean(feifei.h5UrlWithUid || feifei.h5.rechargeUrl || feifei.h5.entryUrl)
|
||||
|
||||
return (
|
||||
<Card className="claim-content-card">
|
||||
<div className="claim-feifei-main">
|
||||
<span className="claim-feifei-label">kuaishou-feifei</span>
|
||||
<Typography.Title level={2}>第 2 步:打开领取链接</Typography.Title>
|
||||
<p>{feifei.rechargeStatusLabel || '待领取'}</p>
|
||||
</div>
|
||||
|
||||
<UidEditRow
|
||||
uidInput={uidInput}
|
||||
submitting={submittingUid}
|
||||
onChange={onUidChange}
|
||||
onSubmit={onResubmitUid}
|
||||
/>
|
||||
|
||||
<div className="claim-info-grid">
|
||||
<InfoTile label="填写 UID" value={expectedUid || '-'} />
|
||||
<InfoTile label="订单号" value={order?.platformOrderId || '-'} />
|
||||
<InfoTile label="平台单号" value={feifei.orderNo || feifei.platformOrderNo || '-'} />
|
||||
<InfoTile label="领取商品" value={product?.title || '-'} />
|
||||
</div>
|
||||
|
||||
<Alert type="info" showIcon message="将携带你填写的 UID 打开领取页,请确认游戏内角色一致。" />
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<LinkOutlined />}
|
||||
disabled={!openReady || !expectedUid}
|
||||
onClick={onOpen}
|
||||
block
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
打开领取链接
|
||||
</Button>
|
||||
|
||||
{taskLastError ? (
|
||||
<Alert type="warning" showIcon message={taskLastError} style={{ marginTop: 12 }} />
|
||||
) : null}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeifeiResultStep({ snapshot }: { snapshot: ClaimSnapshot }) {
|
||||
return (
|
||||
<Card className="claim-content-card">
|
||||
<div className="claim-result-header">
|
||||
{snapshot.resultVariant === 'warning' ? (
|
||||
<ExclamationCircleOutlined className="claim-large-icon claim-icon-warning" />
|
||||
) : (
|
||||
<CheckCircleOutlined
|
||||
className={`claim-large-icon claim-icon-${snapshot.resultVariant}`}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Typography.Title level={2}>{snapshot.resultTitle}</Typography.Title>
|
||||
<p className="claim-muted">{snapshot.resultDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="claim-info-grid">
|
||||
<InfoTile label="填写 UID" value={snapshot.expectedUid || '-'} />
|
||||
<InfoTile label="状态" value={snapshot.feifei?.rechargeStatusLabel || '-'} />
|
||||
<InfoTile label="订单号" value={snapshot.order?.platformOrderId || '-'} />
|
||||
<InfoTile label="领取商品" value={snapshot.product?.title || '-'} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
LinkOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
SwapOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Alert, Button, Card, Space, Tooltip, Typography } from 'antd'
|
||||
import type { ClaimProductInfo } from '@/types/claim'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import type { ClaimSnapshot } from './claim-snapshot'
|
||||
import { ClaimProductItems, InfoRow, InfoTile, UidEditRow } from './claim-shared'
|
||||
|
||||
export function KuaishouCloudClaimSteps({
|
||||
snapshot,
|
||||
qrCodeDataUrl,
|
||||
refreshingRole,
|
||||
confirmingRole,
|
||||
rebindingRole,
|
||||
redeeming,
|
||||
onOpenBindUrl,
|
||||
onRefreshRole,
|
||||
onRebindRole,
|
||||
onConfirmRole,
|
||||
onConfirmRedeem,
|
||||
uidInput,
|
||||
submittingUid,
|
||||
onUidChange,
|
||||
onResubmitUid,
|
||||
}: {
|
||||
snapshot: ClaimSnapshot
|
||||
qrCodeDataUrl: string
|
||||
refreshingRole: boolean
|
||||
confirmingRole: boolean
|
||||
rebindingRole: boolean
|
||||
redeeming: boolean
|
||||
onOpenBindUrl: (useCurrentPage?: boolean) => void
|
||||
onRefreshRole: () => void
|
||||
onRebindRole: () => void
|
||||
onConfirmRole: () => void
|
||||
onConfirmRedeem: () => void
|
||||
uidInput: string
|
||||
submittingUid: boolean
|
||||
onUidChange: (value: string) => void
|
||||
onResubmitUid: () => void
|
||||
}) {
|
||||
if (!snapshot.flow) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (snapshot.currentStep === 3) {
|
||||
return (
|
||||
<ClaimConfirmStep
|
||||
roleName={snapshot.roleName}
|
||||
roleId={snapshot.roleId}
|
||||
expectedUid={snapshot.expectedUid}
|
||||
product={snapshot.product}
|
||||
redeeming={redeeming}
|
||||
rebindingRole={rebindingRole}
|
||||
onConfirmRedeem={onConfirmRedeem}
|
||||
onRebindRole={onRebindRole}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ClaimBindingStep
|
||||
snapshot={snapshot}
|
||||
qrCodeDataUrl={qrCodeDataUrl}
|
||||
refreshingRole={refreshingRole}
|
||||
confirmingRole={confirmingRole}
|
||||
rebindingRole={rebindingRole}
|
||||
redeeming={redeeming}
|
||||
onOpenBindUrl={onOpenBindUrl}
|
||||
onRefreshRole={onRefreshRole}
|
||||
onRebindRole={onRebindRole}
|
||||
onConfirmRole={onConfirmRole}
|
||||
onConfirmRedeem={onConfirmRedeem}
|
||||
uidInput={uidInput}
|
||||
submittingUid={submittingUid}
|
||||
onUidChange={onUidChange}
|
||||
onResubmitUid={onResubmitUid}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ClaimBindingStep({
|
||||
snapshot,
|
||||
qrCodeDataUrl,
|
||||
refreshingRole,
|
||||
confirmingRole,
|
||||
rebindingRole,
|
||||
redeeming,
|
||||
onOpenBindUrl,
|
||||
onRefreshRole,
|
||||
onRebindRole,
|
||||
onConfirmRole,
|
||||
onConfirmRedeem,
|
||||
uidInput,
|
||||
submittingUid,
|
||||
onUidChange,
|
||||
onResubmitUid,
|
||||
}: {
|
||||
snapshot: ClaimSnapshot
|
||||
qrCodeDataUrl: string
|
||||
refreshingRole: boolean
|
||||
confirmingRole: boolean
|
||||
rebindingRole: boolean
|
||||
redeeming: boolean
|
||||
onOpenBindUrl: (useCurrentPage?: boolean) => void
|
||||
onRefreshRole: () => void
|
||||
onRebindRole: () => void
|
||||
onConfirmRole: () => void
|
||||
onConfirmRedeem: () => void
|
||||
uidInput: string
|
||||
submittingUid: boolean
|
||||
onUidChange: (value: string) => void
|
||||
onResubmitUid: () => void
|
||||
}) {
|
||||
const flow = snapshot.flow
|
||||
if (!flow) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="claim-content-card claim-binding-card">
|
||||
<Typography.Title level={2}>第 2 步:绑定角色</Typography.Title>
|
||||
<p className="claim-muted">
|
||||
请使用与 UID <strong>{snapshot.expectedUid || '-'}</strong> 对应的角色扫码绑定。
|
||||
</p>
|
||||
|
||||
<UidEditRow
|
||||
uidInput={uidInput}
|
||||
submitting={submittingUid}
|
||||
onChange={onUidChange}
|
||||
onSubmit={onResubmitUid}
|
||||
/>
|
||||
|
||||
{snapshot.isBindingPrepared && qrCodeDataUrl ? (
|
||||
<div className="claim-qr-block">
|
||||
<div className="claim-qr-guide-panel">
|
||||
<div className="claim-scan-label claim-scan-label-left">Q区用Q扫</div>
|
||||
<img src={qrCodeDataUrl} alt="绑定二维码" className="claim-qr-code" />
|
||||
<div className="claim-scan-label claim-scan-label-right">V区用V扫</div>
|
||||
<p className="claim-save-hint">长按保存相册进行扫码绑定</p>
|
||||
</div>
|
||||
|
||||
<Button icon={<LinkOutlined />} onClick={() => onOpenBindUrl(false)}>
|
||||
打开绑定链接
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<LoadingOutlined spin />}
|
||||
message="系统正在准备绑定资源,请稍后自动刷新。"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={snapshot.isUidMatched ? 'claim-role-panel' : 'claim-role-panel pending'}>
|
||||
<InfoRow label="填写 UID" value={snapshot.expectedUid || '-'} />
|
||||
<InfoRow label="当前角色" value={snapshot.roleName || '待识别'} />
|
||||
<InfoRow label="角色 ID" value={snapshot.roleId || '-'} />
|
||||
<InfoRow label="匹配状态" value={snapshot.isUidMatched ? '已匹配' : '未匹配'} />
|
||||
</div>
|
||||
|
||||
{!snapshot.isUidMatched && snapshot.roleId ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`当前绑定角色 ID(${snapshot.roleId})与填写 UID(${snapshot.expectedUid})不一致,请换绑正确角色。`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Space size={12} wrap className="claim-action-row">
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={refreshingRole}
|
||||
onClick={onRefreshRole}
|
||||
>
|
||||
刷新角色信息
|
||||
</Button>
|
||||
<Tooltip title="角色绑定错误时,请点击「换绑角色」,然后重新扫码绑定正确的角色">
|
||||
<span className="claim-action-tooltip-wrap">
|
||||
<Button
|
||||
size="large"
|
||||
icon={<SwapOutlined />}
|
||||
loading={rebindingRole}
|
||||
disabled={confirmingRole || redeeming}
|
||||
onClick={onRebindRole}
|
||||
>
|
||||
换绑角色
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{snapshot.isUidMatched ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
danger
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={redeeming}
|
||||
disabled={rebindingRole}
|
||||
onClick={onConfirmRedeem}
|
||||
>
|
||||
UID 已匹配,确认兑换
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip title={snapshot.confirmRoleDisabledReason || undefined}>
|
||||
<span className="claim-action-tooltip-wrap">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={confirmingRole}
|
||||
disabled={Boolean(snapshot.confirmRoleDisabledReason)}
|
||||
onClick={onConfirmRole}
|
||||
>
|
||||
刷新后重试匹配
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<div className="claim-meta-footer">
|
||||
<span>最近刷新:{formatAdminDateTime(flow.role.refreshedAt)}</span>
|
||||
<span>链接有效期:{formatAdminDateTime(flow.binding.bindExpiresAt)}</span>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function ClaimConfirmStep({
|
||||
roleName,
|
||||
roleId,
|
||||
expectedUid,
|
||||
product,
|
||||
redeeming,
|
||||
rebindingRole,
|
||||
onConfirmRedeem,
|
||||
onRebindRole,
|
||||
}: {
|
||||
roleName: string
|
||||
roleId: string
|
||||
expectedUid: string
|
||||
product: ClaimProductInfo | null
|
||||
redeeming: boolean
|
||||
rebindingRole: boolean
|
||||
onConfirmRedeem: () => void
|
||||
onRebindRole: () => void
|
||||
}) {
|
||||
return (
|
||||
<Card className="claim-content-card">
|
||||
<Typography.Title level={2}>第 3 步:确认兑换信息</Typography.Title>
|
||||
<p className="claim-muted">请再次确认 UID 和商品信息,确认无误后再继续兑换。</p>
|
||||
|
||||
<div className="claim-info-grid">
|
||||
<InfoTile label="填写 UID" value={expectedUid || '-'} />
|
||||
<InfoTile label="角色名称" value={roleName || '-'} />
|
||||
<InfoTile label="角色 ID" value={roleId || '-'} />
|
||||
<InfoTile label="领取商品" value={product?.title || '-'} />
|
||||
<InfoTile label="商品数量" value={String(product?.quantity || 0)} />
|
||||
</div>
|
||||
|
||||
<ClaimProductItems product={product} />
|
||||
|
||||
<Alert type="warning" showIcon message="兑换后不可取消,也不可退货。" />
|
||||
|
||||
<Space size={12} wrap className="claim-action-row">
|
||||
<Tooltip title="角色绑定错误时,请点击「换绑角色」,然后重新扫码绑定正确的角色">
|
||||
<span className="claim-action-tooltip-wrap">
|
||||
<Button
|
||||
size="large"
|
||||
icon={<SwapOutlined />}
|
||||
loading={rebindingRole}
|
||||
disabled={redeeming}
|
||||
onClick={onRebindRole}
|
||||
>
|
||||
换绑角色
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={redeeming}
|
||||
disabled={rebindingRole}
|
||||
onClick={onConfirmRedeem}
|
||||
>
|
||||
确认兑换
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClaimResultStep({ snapshot }: { snapshot: ClaimSnapshot }) {
|
||||
const flow = snapshot.flow
|
||||
|
||||
return (
|
||||
<Card className="claim-content-card">
|
||||
<div className="claim-result-header">
|
||||
{snapshot.resultVariant === 'warning' ? (
|
||||
<ExclamationCircleOutlined className="claim-large-icon claim-icon-warning" />
|
||||
) : (
|
||||
<CheckCircleOutlined
|
||||
className={`claim-large-icon claim-icon-${snapshot.resultVariant}`}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<Typography.Title level={2}>{snapshot.resultTitle}</Typography.Title>
|
||||
<p className="claim-muted">{snapshot.resultDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="claim-info-grid">
|
||||
<InfoTile label="填写 UID" value={snapshot.expectedUid || '-'} />
|
||||
<InfoTile label="结果时间" value={formatAdminDateTime(flow?.dispatch.dispatchAt || null)} />
|
||||
<InfoTile label="领取商品" value={snapshot.product?.title || '-'} />
|
||||
<InfoTile label="角色名称" value={snapshot.roleName || '-'} />
|
||||
<InfoTile label="角色 ID" value={snapshot.roleId || '-'} />
|
||||
<InfoTile label="订单号" value={snapshot.order?.platformOrderId || '-'} />
|
||||
</div>
|
||||
|
||||
<ClaimProductItems product={snapshot.product} />
|
||||
|
||||
<Alert
|
||||
type={snapshot.resultVariant}
|
||||
showIcon
|
||||
message={
|
||||
snapshot.resultVariant === 'warning'
|
||||
? '当前兑换未完成,客服会根据后台任务记录继续处理。'
|
||||
: '客户侧操作已经完成,后续履约结果请以后台任务界面为准。'
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -4,21 +4,16 @@ import QRCode from 'qrcode'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router'
|
||||
|
||||
import { isFeedbackDismissed, showConfirm, showError, showSuccess } from '@/lib/feedback'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
confirmKuaishouCloudClaimRole,
|
||||
fetchClaimDetail,
|
||||
rebindKuaishouCloudClaimRole,
|
||||
redeemKuaishouCloudClaim,
|
||||
submitAffiliateDashClaim,
|
||||
submitClaimUid,
|
||||
} from '@/services/claim'
|
||||
import type { ClaimDetailData } from '@/types/claim'
|
||||
|
||||
import { AffiliateDashClaimPanel, AffiliateDashResultStep } from './ClaimAffiliateDashSteps'
|
||||
import { FeifeiClaimPanel, FeifeiResultStep } from './ClaimFeifeiSteps'
|
||||
import { ClaimHeaderCard } from './ClaimHeaderCard'
|
||||
import { ClaimResultStep, KuaishouCloudClaimSteps } from './ClaimLewanSteps'
|
||||
import { ClaimUidStep } from './ClaimUidStep'
|
||||
import { resolveNextPollDelayMs, resolveRolePollDelayMs } from './claim-poll'
|
||||
import { createClaimSnapshot, type ClaimSnapshot } from './claim-snapshot'
|
||||
@@ -30,10 +25,6 @@ export default function ClaimPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submittingUid, setSubmittingUid] = useState(false)
|
||||
const [uidInput, setUidInput] = useState('')
|
||||
const [refreshingRole, setRefreshingRole] = useState(false)
|
||||
const [confirmingRole, setConfirmingRole] = useState(false)
|
||||
const [rebindingRole, setRebindingRole] = useState(false)
|
||||
const [redeeming, setRedeeming] = useState(false)
|
||||
const [submittingSubmit, setSubmittingSubmit] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
|
||||
@@ -42,10 +33,7 @@ export default function ClaimPage() {
|
||||
const pollTimerRef = useRef(0)
|
||||
const rolePollBaselineRef = useRef({ roleKey: '', at: 0 })
|
||||
|
||||
const snapshot = useMemo(
|
||||
() => createClaimSnapshot(detail, { rebindingRole }),
|
||||
[detail, rebindingRole],
|
||||
)
|
||||
const snapshot = useMemo(() => createClaimSnapshot(detail), [detail])
|
||||
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollTimerRef.current) {
|
||||
@@ -83,13 +71,7 @@ export default function ClaimPage() {
|
||||
if (expectedUid) {
|
||||
setUidInput(expectedUid)
|
||||
}
|
||||
await generateQRCode(
|
||||
String(
|
||||
nextDetail.kuaishouCloudFulfillment?.binding.bindUrl ||
|
||||
nextDetail.affiliateDash?.bindUrl ||
|
||||
'',
|
||||
).trim(),
|
||||
)
|
||||
await generateQRCode(String(nextDetail.affiliateDash?.bindUrl || '').trim())
|
||||
},
|
||||
[generateQRCode],
|
||||
)
|
||||
@@ -145,6 +127,9 @@ export default function ClaimPage() {
|
||||
|
||||
useEffect(() => {
|
||||
stopPolling()
|
||||
if (snapshot.channelOffline) {
|
||||
return undefined
|
||||
}
|
||||
const delay = resolveNextPollDelay(snapshot)
|
||||
if (!delay) {
|
||||
return undefined
|
||||
@@ -161,7 +146,6 @@ export default function ClaimPage() {
|
||||
}, [loadDetail, resolveNextPollDelay, snapshot, stopPolling])
|
||||
|
||||
async function handleSubmitUid() {
|
||||
// 防并发:提交中忽略重复触发(按钮 loading + 回车 Enter 都可能连续触发)
|
||||
if (submittingUid) {
|
||||
return
|
||||
}
|
||||
@@ -219,139 +203,6 @@ export default function ClaimPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRole() {
|
||||
setRefreshingRole(true)
|
||||
try {
|
||||
const nextDetail = await loadDetail({ silent: true })
|
||||
const nextSnapshot = createClaimSnapshot(nextDetail, { rebindingRole })
|
||||
|
||||
if (nextSnapshot.isUidMatched) {
|
||||
showSuccess('角色信息已匹配,可确认兑换')
|
||||
} else if (nextSnapshot.roleId) {
|
||||
showError(
|
||||
`当前绑定角色 ID(${nextSnapshot.roleId})与填写 UID(${nextSnapshot.expectedUid || '-'})不一致`,
|
||||
)
|
||||
} else if (nextDetail) {
|
||||
showError(
|
||||
nextSnapshot.flow?.role.errorMessage ||
|
||||
'暂时还没有识别到角色信息,请完成绑定后稍等片刻再试',
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
setRefreshingRole(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRole() {
|
||||
setConfirmingRole(true)
|
||||
try {
|
||||
const response = await confirmKuaishouCloudClaimRole(token)
|
||||
await applyDetail(response.data)
|
||||
showSuccess('角色已确认,进入下一步')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '确认角色失败')
|
||||
} finally {
|
||||
setConfirmingRole(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRedeem() {
|
||||
try {
|
||||
await showConfirm('兑换后不可取消,也不可退货。请确认 UID 与商品信息完全正确。', '确认兑换', {
|
||||
okText: '确认兑换',
|
||||
okButtonProps: { danger: true },
|
||||
})
|
||||
} catch (error) {
|
||||
if (!isFeedbackDismissed(error)) {
|
||||
showError('确认弹窗已关闭')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setRedeeming(true)
|
||||
try {
|
||||
const response = await redeemKuaishouCloudClaim(token)
|
||||
await applyDetail(response.data)
|
||||
showSuccess('兑换请求已提交')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '兑换失败')
|
||||
} finally {
|
||||
setRedeeming(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebindRole() {
|
||||
try {
|
||||
await showConfirm(
|
||||
'换绑后当前绑定链接会失效,系统会退还当前虚拟号并生成新的绑定二维码。请使用你填写的 UID 对应角色重新绑定。',
|
||||
'确认换绑角色',
|
||||
{
|
||||
okText: '确认换绑',
|
||||
okButtonProps: { danger: true },
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
if (!isFeedbackDismissed(error)) {
|
||||
showError('确认弹窗已关闭')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setRebindingRole(true)
|
||||
try {
|
||||
const response = await rebindKuaishouCloudClaimRole(token)
|
||||
await applyDetail(response.data)
|
||||
showSuccess('新的绑定二维码已生成,请重新绑定角色')
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '换绑角色失败')
|
||||
} finally {
|
||||
setRebindingRole(false)
|
||||
}
|
||||
}
|
||||
|
||||
function openBindUrl(useCurrentPage = false) {
|
||||
const bindUrl = String(
|
||||
snapshot.flow?.binding.bindUrl || snapshot.affiliateDash?.bindUrl || '',
|
||||
).trim()
|
||||
if (!bindUrl) {
|
||||
showError('绑定链接还没准备好,请稍后刷新')
|
||||
return
|
||||
}
|
||||
|
||||
if (useCurrentPage) {
|
||||
window.location.assign(bindUrl)
|
||||
return
|
||||
}
|
||||
|
||||
window.open(bindUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function openFeifeiUrl(useCurrentPage = false) {
|
||||
const url = String(
|
||||
snapshot.feifei?.h5UrlWithUid ||
|
||||
snapshot.feifei?.h5.rechargeUrl ||
|
||||
snapshot.feifei?.h5.entryUrl ||
|
||||
'',
|
||||
).trim()
|
||||
|
||||
if (!snapshot.hasExpectedUid) {
|
||||
showError('请先填写游戏 UID')
|
||||
return
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
showError('领取链接还没准备好,请稍后刷新')
|
||||
return
|
||||
}
|
||||
|
||||
if (useCurrentPage) {
|
||||
window.location.assign(url)
|
||||
return
|
||||
}
|
||||
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="claim-page">
|
||||
<ClaimHeaderCard
|
||||
@@ -376,6 +227,11 @@ export default function ClaimPage() {
|
||||
<Card className="claim-content-card claim-state-card">
|
||||
<Empty description="流程数据不完整,请联系客服处理" />
|
||||
</Card>
|
||||
) : snapshot.channelOffline ? (
|
||||
<Card className="claim-content-card claim-state-card">
|
||||
<ExclamationCircleOutlined className="claim-large-icon claim-icon-error" />
|
||||
<p>{snapshot.channelOfflineMessage || '该履约渠道已下线,无法继续领取'}</p>
|
||||
</Card>
|
||||
) : snapshot.currentStep === 1 ? (
|
||||
<ClaimUidStep
|
||||
uidInput={uidInput}
|
||||
@@ -383,14 +239,8 @@ export default function ClaimPage() {
|
||||
onChange={setUidInput}
|
||||
onSubmit={() => void handleSubmitUid()}
|
||||
/>
|
||||
) : snapshot.hasRedeemResult ? (
|
||||
snapshot.isFeifeiFlow ? (
|
||||
<FeifeiResultStep snapshot={snapshot} />
|
||||
) : snapshot.isAffiliateDashFlow ? (
|
||||
<AffiliateDashResultStep snapshot={snapshot} />
|
||||
) : (
|
||||
<ClaimResultStep snapshot={snapshot} />
|
||||
)
|
||||
) : snapshot.hasRedeemResult && snapshot.isAffiliateDashFlow ? (
|
||||
<AffiliateDashResultStep snapshot={snapshot} />
|
||||
) : snapshot.isAffiliateDashFlow && snapshot.affiliateDash ? (
|
||||
<AffiliateDashClaimPanel
|
||||
affiliateDash={snapshot.affiliateDash}
|
||||
@@ -404,40 +254,9 @@ export default function ClaimPage() {
|
||||
onResubmitUid={() => void handleSubmitUid()}
|
||||
onSubmit={() => void handleSubmitAffiliateDash()}
|
||||
/>
|
||||
) : snapshot.isFeifeiFlow && snapshot.feifei ? (
|
||||
<FeifeiClaimPanel
|
||||
feifei={snapshot.feifei}
|
||||
expectedUid={snapshot.expectedUid}
|
||||
order={snapshot.order}
|
||||
product={snapshot.product}
|
||||
taskLastError={snapshot.task?.lastError}
|
||||
onOpen={() => openFeifeiUrl(false)}
|
||||
uidInput={uidInput}
|
||||
submittingUid={submittingUid}
|
||||
onUidChange={setUidInput}
|
||||
onResubmitUid={() => void handleSubmitUid()}
|
||||
/>
|
||||
) : snapshot.flow ? (
|
||||
<KuaishouCloudClaimSteps
|
||||
snapshot={snapshot}
|
||||
qrCodeDataUrl={qrCodeDataUrl}
|
||||
refreshingRole={refreshingRole}
|
||||
confirmingRole={confirmingRole}
|
||||
rebindingRole={rebindingRole}
|
||||
redeeming={redeeming}
|
||||
onOpenBindUrl={openBindUrl}
|
||||
onRefreshRole={refreshRole}
|
||||
onRebindRole={rebindRole}
|
||||
onConfirmRole={confirmRole}
|
||||
onConfirmRedeem={confirmRedeem}
|
||||
uidInput={uidInput}
|
||||
submittingUid={submittingUid}
|
||||
onUidChange={setUidInput}
|
||||
onResubmitUid={() => void handleSubmitUid()}
|
||||
/>
|
||||
) : (
|
||||
<Card className="claim-content-card claim-state-card">
|
||||
<Empty description="流程数据不完整,请联系客服处理" />
|
||||
<Empty description="当前领取流程不可用,请联系客服处理" />
|
||||
</Card>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -2,25 +2,14 @@ import {
|
||||
TASK_STATUS,
|
||||
hasKuaishouCloudRedeemResultStatus,
|
||||
isKuaishouCloudCompletedStatus,
|
||||
isKuaishouCloudRoleConfirmedStatus,
|
||||
normalizeTaskStatus,
|
||||
} from '@/domain/task-status'
|
||||
import type {
|
||||
ClaimAffiliateDashFlowInfo,
|
||||
ClaimDetailData,
|
||||
ClaimKuaishouCloudFlowInfo,
|
||||
ClaimKuaishouFeifeiFlowInfo,
|
||||
} from '@/types/claim'
|
||||
import type { ClaimAffiliateDashFlowInfo, ClaimDetailData } from '@/types/claim'
|
||||
|
||||
export type ResultVariant = 'success' | 'warning' | 'info'
|
||||
export type ClaimSnapshot = ReturnType<typeof createClaimSnapshot>
|
||||
|
||||
export function createClaimSnapshot(
|
||||
detail: ClaimDetailData | null,
|
||||
options: { rebindingRole?: boolean } = {},
|
||||
) {
|
||||
const flow = detail?.kuaishouCloudFulfillment || null
|
||||
const feifei = detail?.kuaishouFeifei || null
|
||||
export function createClaimSnapshot(detail: ClaimDetailData | null) {
|
||||
const affiliateDash = detail?.affiliateDash || null
|
||||
const order = detail?.order || null
|
||||
const orderItem = detail?.orderItem || null
|
||||
@@ -30,9 +19,8 @@ export function createClaimSnapshot(
|
||||
|
||||
const expectedUid = String(claimIdentity?.expectedUid || '').trim()
|
||||
const hasExpectedUid = Boolean(expectedUid || claimIdentity?.ready)
|
||||
const roleName = flow?.role.name || flow?.binding.roleName || ''
|
||||
const roleId = flow?.role.rid || flow?.binding.roleId || ''
|
||||
const isFeifeiFlow = detail?.flowType === 'kuaishou_feifei'
|
||||
const channelOffline = Boolean(detail?.channelOffline)
|
||||
const channelOfflineMessage = String(detail?.channelOfflineMessage || '').trim()
|
||||
const isAffiliateDashFlow = detail?.flowType === 'affiliate_dash'
|
||||
const affiliateDashExpectedUid = String(
|
||||
expectedUid || affiliateDash?.expectedGameAccount || affiliateDash?.gameAccount || '',
|
||||
@@ -55,78 +43,45 @@ export function createClaimSnapshot(
|
||||
const isAffiliateDashReadyToSubmit = Boolean(
|
||||
isAffiliateDashBound && isAffiliateDashUidMatched && !isAffiliateDashBindMismatch,
|
||||
)
|
||||
const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '')
|
||||
const isBindingPrepared =
|
||||
flow?.binding.prepareStatus === 'ready' &&
|
||||
Boolean(String(flow?.binding.bindUrl || '').trim()) &&
|
||||
!isBindUrlExpired
|
||||
const isBindingPreparing = flow?.binding.prepareStatus === 'pending'
|
||||
const isRoleReady = Boolean(roleName || roleId)
|
||||
const isUidMatched =
|
||||
Boolean(expectedUid) && Boolean(roleId) && normalizeUid(expectedUid) === normalizeUid(roleId)
|
||||
const confirmRoleDisabledReason = resolveConfirmRoleDisabledReason({
|
||||
isBindingPrepared,
|
||||
isRoleReady,
|
||||
isUidMatched,
|
||||
expectedUid,
|
||||
roleId,
|
||||
rebindingRole: Boolean(options.rebindingRole),
|
||||
})
|
||||
const isRoleConfirmed = isKuaishouCloudRoleConfirmedStatus(task?.status)
|
||||
const isDispatched = String(flow?.dispatch.status || '').trim() === 'success'
|
||||
const normalizedStatus = normalizeTaskStatus(task?.status)
|
||||
const isRedeemFailed =
|
||||
normalizedStatus === TASK_STATUS.MANUAL_REVIEW ||
|
||||
normalizedStatus === TASK_STATUS.FAILED ||
|
||||
normalizedStatus === TASK_STATUS.CLOSED ||
|
||||
String(flow?.dispatch.status || '').trim() === 'failed' ||
|
||||
(isFeifeiFlow && [40, 50].includes(Number(feifei?.rechargeStatus || 0))) ||
|
||||
(isAffiliateDashFlow &&
|
||||
['ship_failed', 'cancelled'].includes(String(affiliateDash?.orderStatus || '').trim()))
|
||||
const isCompleted =
|
||||
isKuaishouCloudCompletedStatus(task?.status) ||
|
||||
(isFeifeiFlow && Number(feifei?.rechargeStatus || 0) === 30)
|
||||
const isCompleted = isKuaishouCloudCompletedStatus(task?.status)
|
||||
const hasRedeemResult =
|
||||
isDispatched ||
|
||||
hasKuaishouCloudRedeemResultStatus(task?.status) ||
|
||||
(isFeifeiFlow && [30, 40, 50, 60].includes(Number(feifei?.rechargeStatus || 0))) ||
|
||||
(isAffiliateDashFlow &&
|
||||
['delivered', 'ship_failed', 'cancelled'].includes(
|
||||
String(affiliateDash?.orderStatus || '').trim(),
|
||||
))
|
||||
const currentStep = resolveCurrentStep({
|
||||
channelOffline,
|
||||
hasExpectedUid,
|
||||
hasRedeemResult,
|
||||
isRoleConfirmed,
|
||||
isFeifeiFlow,
|
||||
isAffiliateDashFlow,
|
||||
isAffiliateDashReadyToSubmit,
|
||||
})
|
||||
const progressText = resolveProgressText({
|
||||
feifei,
|
||||
isFeifeiFlow,
|
||||
isAffiliateDashFlow,
|
||||
channelOffline,
|
||||
channelOfflineMessage,
|
||||
affiliateDash,
|
||||
isAffiliateDashFlow,
|
||||
hasExpectedUid,
|
||||
expectedUid,
|
||||
hasRedeemResult,
|
||||
isRoleConfirmed,
|
||||
isBindingPrepared,
|
||||
isUidMatched,
|
||||
isAffiliateDashReadyToSubmit,
|
||||
isAffiliateDashBindMismatch,
|
||||
})
|
||||
const resultTitle = resolveResultTitle({
|
||||
isRedeemFailed,
|
||||
isCompleted,
|
||||
isDispatched,
|
||||
isAffiliateDashFlow,
|
||||
affiliateDash,
|
||||
})
|
||||
const resultDescription = resolveResultDescription({
|
||||
detail,
|
||||
flow,
|
||||
feifei,
|
||||
affiliateDash,
|
||||
task,
|
||||
isRedeemFailed,
|
||||
@@ -134,13 +89,13 @@ export function createClaimSnapshot(
|
||||
})
|
||||
const resultVariant: ResultVariant = isRedeemFailed
|
||||
? 'warning'
|
||||
: isCompleted || isDispatched
|
||||
: isCompleted
|
||||
? 'success'
|
||||
: 'info'
|
||||
|
||||
return {
|
||||
flow,
|
||||
feifei,
|
||||
flow: null as null,
|
||||
feifei: null as null,
|
||||
affiliateDash,
|
||||
order,
|
||||
orderItem,
|
||||
@@ -149,9 +104,11 @@ export function createClaimSnapshot(
|
||||
claimIdentity,
|
||||
expectedUid,
|
||||
hasExpectedUid,
|
||||
roleName,
|
||||
roleId,
|
||||
isFeifeiFlow,
|
||||
channelOffline,
|
||||
channelOfflineMessage,
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
isFeifeiFlow: false,
|
||||
isAffiliateDashFlow,
|
||||
affiliateDashExpectedUid,
|
||||
affiliateDashBoundAccount,
|
||||
@@ -159,14 +116,14 @@ export function createClaimSnapshot(
|
||||
isAffiliateDashUidMatched,
|
||||
isAffiliateDashBindMismatch,
|
||||
isAffiliateDashReadyToSubmit,
|
||||
isBindUrlExpired,
|
||||
isBindingPrepared,
|
||||
isBindingPreparing,
|
||||
isRoleReady,
|
||||
isUidMatched,
|
||||
confirmRoleDisabledReason,
|
||||
isRoleConfirmed,
|
||||
isDispatched,
|
||||
isBindUrlExpired: false,
|
||||
isBindingPrepared: false,
|
||||
isBindingPreparing: false,
|
||||
isRoleReady: false,
|
||||
isUidMatched: false,
|
||||
confirmRoleDisabledReason: '',
|
||||
isRoleConfirmed: false,
|
||||
isDispatched: false,
|
||||
isRedeemFailed,
|
||||
isCompleted,
|
||||
hasRedeemResult,
|
||||
@@ -184,37 +141,16 @@ export function normalizeUid(value: unknown) {
|
||||
.replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function resolveConfirmRoleDisabledReason(options: {
|
||||
isBindingPrepared: boolean
|
||||
isRoleReady: boolean
|
||||
isUidMatched: boolean
|
||||
expectedUid: string
|
||||
roleId: string
|
||||
rebindingRole: boolean
|
||||
}) {
|
||||
if (!options.isBindingPrepared) {
|
||||
return '绑定二维码还在准备中,请稍后自动刷新'
|
||||
}
|
||||
if (!options.isRoleReady) {
|
||||
return '请先扫码绑定自己的角色,并刷新角色信息'
|
||||
}
|
||||
if (!options.isUidMatched) {
|
||||
return `当前绑定角色 ID(${options.roleId || '-'})与填写 UID(${options.expectedUid || '-'})不一致`
|
||||
}
|
||||
if (options.rebindingRole) {
|
||||
return '正在换绑角色,请稍候'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function resolveCurrentStep(options: {
|
||||
channelOffline: boolean
|
||||
hasExpectedUid: boolean
|
||||
hasRedeemResult: boolean
|
||||
isRoleConfirmed: boolean
|
||||
isFeifeiFlow: boolean
|
||||
isAffiliateDashFlow: boolean
|
||||
isAffiliateDashReadyToSubmit: boolean
|
||||
}) {
|
||||
if (options.channelOffline) {
|
||||
return 1
|
||||
}
|
||||
if (!options.hasExpectedUid) {
|
||||
return 1
|
||||
}
|
||||
@@ -227,26 +163,22 @@ function resolveCurrentStep(options: {
|
||||
}
|
||||
return 3
|
||||
}
|
||||
if (!options.isFeifeiFlow && options.isRoleConfirmed) {
|
||||
return 3
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
function resolveProgressText(options: {
|
||||
feifei: ClaimKuaishouFeifeiFlowInfo | null
|
||||
channelOffline: boolean
|
||||
channelOfflineMessage: string
|
||||
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||
isFeifeiFlow: boolean
|
||||
isAffiliateDashFlow: boolean
|
||||
hasExpectedUid: boolean
|
||||
expectedUid: string
|
||||
hasRedeemResult: boolean
|
||||
isRoleConfirmed: boolean
|
||||
isBindingPrepared: boolean
|
||||
isUidMatched: boolean
|
||||
isAffiliateDashReadyToSubmit: boolean
|
||||
isAffiliateDashBindMismatch: boolean
|
||||
}) {
|
||||
if (options.channelOffline) {
|
||||
return options.channelOfflineMessage || '该履约渠道已下线'
|
||||
}
|
||||
if (!options.hasExpectedUid) {
|
||||
return '请填写游戏编号'
|
||||
}
|
||||
@@ -267,28 +199,12 @@ function resolveProgressText(options: {
|
||||
}
|
||||
return '已绑定,请提交发货'
|
||||
}
|
||||
if (options.isFeifeiFlow) {
|
||||
return options.feifei?.rechargeStatusLabel || `UID ${options.expectedUid},请打开领取链接`
|
||||
}
|
||||
if (options.hasRedeemResult) {
|
||||
return '兑换结果已生成'
|
||||
}
|
||||
if (options.isRoleConfirmed) {
|
||||
return '角色已确认,等待兑换'
|
||||
}
|
||||
if (!options.isBindingPrepared) {
|
||||
return '绑定链接准备中,请稍候'
|
||||
}
|
||||
if (options.isUidMatched) {
|
||||
return 'UID 已匹配,请确认兑换'
|
||||
}
|
||||
return '请完成扫码绑定并匹配 UID'
|
||||
return '当前领取流程不可用'
|
||||
}
|
||||
|
||||
function resolveResultTitle(options: {
|
||||
isRedeemFailed: boolean
|
||||
isCompleted: boolean
|
||||
isDispatched: boolean
|
||||
isAffiliateDashFlow: boolean
|
||||
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||
}) {
|
||||
@@ -298,9 +214,6 @@ function resolveResultTitle(options: {
|
||||
if (options.isCompleted) {
|
||||
return '兑换成功'
|
||||
}
|
||||
if (options.isDispatched) {
|
||||
return '兑换请求已提交'
|
||||
}
|
||||
if (options.isAffiliateDashFlow && options.affiliateDash?.orderStatus === 'delivered') {
|
||||
return '发货已完成'
|
||||
}
|
||||
@@ -309,8 +222,6 @@ function resolveResultTitle(options: {
|
||||
|
||||
function resolveResultDescription(options: {
|
||||
detail: ClaimDetailData | null
|
||||
flow: ClaimKuaishouCloudFlowInfo | null
|
||||
feifei: ClaimKuaishouFeifeiFlowInfo | null
|
||||
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||
task: ClaimDetailData['task'] | null
|
||||
isRedeemFailed: boolean
|
||||
@@ -319,8 +230,6 @@ function resolveResultDescription(options: {
|
||||
if (options.isRedeemFailed) {
|
||||
const message = String(
|
||||
options.task?.lastError ||
|
||||
options.flow?.dispatch.errorMessage ||
|
||||
options.feifei?.rechargeResultMessage ||
|
||||
options.affiliateDash?.failureReason ||
|
||||
options.detail?.result?.resultMessage ||
|
||||
'',
|
||||
@@ -338,13 +247,3 @@ function resolveResultDescription(options: {
|
||||
|
||||
return '你的兑换请求已经提交。后续结果会由系统保存在后台任务界面。'
|
||||
}
|
||||
|
||||
function isDateExpired(value: string | null) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return false
|
||||
}
|
||||
|
||||
const expiresTime = Date.parse(normalized)
|
||||
return Number.isFinite(expiresTime) && expiresTime <= Date.now()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export type DevMockStatus = {
|
||||
}
|
||||
|
||||
export type DevMockCreateResult = {
|
||||
platform: 'lewan' | 'feifei' | 'affiliate_dash'
|
||||
platform: 'affiliate_dash'
|
||||
orderId: number
|
||||
orderNo: string
|
||||
taskId: number
|
||||
@@ -49,26 +49,6 @@ export function fetchDevMockStatus() {
|
||||
return apiGet<DevMockStatus>('/api/v1/admin/dev-mock/status')
|
||||
}
|
||||
|
||||
export function createLewanDevMock(payload: {
|
||||
step?: string
|
||||
uid?: string
|
||||
orderNo?: string
|
||||
productNo?: string
|
||||
}) {
|
||||
return apiPost<DevMockCreateResult>('/api/v1/admin/dev-mock/lewan', payload)
|
||||
}
|
||||
|
||||
export function createFeifeiDevMock(payload: {
|
||||
step?: string
|
||||
uid?: string
|
||||
orderNo?: string
|
||||
productName?: string
|
||||
productCode?: string
|
||||
h5Url?: string
|
||||
}) {
|
||||
return apiPost<DevMockCreateResult>('/api/v1/admin/dev-mock/feifei', payload)
|
||||
}
|
||||
|
||||
export function createAffiliateDashDevMock(payload: {
|
||||
step?: string
|
||||
uid?: string
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
import { apiDelete, apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
AdminCloudtentaclesVnListResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminCloudtentaclesSourceConfig() {
|
||||
return apiGet<AdminCloudtentaclesSourceConfigResponse>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles-source',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminCloudtentaclesSourceConfig(
|
||||
payload:
|
||||
| AdminCloudtentaclesSourcesConfig
|
||||
| ({ sourceKey: string } & Partial<AdminCloudtentaclesSourceItem>),
|
||||
) {
|
||||
return apiPost<AdminCloudtentaclesSourceConfigResponse>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles-source',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteAdminCloudtentaclesSource(sourceKey: string) {
|
||||
return apiDelete<{ success: boolean }>(
|
||||
`/api/v1/admin/platform-config/cloudtentacles-source/${sourceKey}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesOverrideRules() {
|
||||
return apiGet<AdminCloudtentaclesOverrideRuleConfig>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
|
||||
)
|
||||
}
|
||||
|
||||
export function saveAdminCloudtentaclesOverrideRules(payload: {
|
||||
enabled: boolean
|
||||
rules: Array<{
|
||||
id?: string
|
||||
enabled?: boolean
|
||||
productName?: string
|
||||
sourceKey?: string
|
||||
deliveryItems?: Array<{
|
||||
cloudSkuId?: number
|
||||
cloudSkuName?: string
|
||||
quantity?: number
|
||||
}>
|
||||
notes?: string
|
||||
}>
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesOverrideRuleConfig>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/override-rules',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function sendAdminCloudtentaclesSmsCode(payload: {
|
||||
baseUrl?: string
|
||||
username?: string
|
||||
phone?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesSendSmsResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/send-sms-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function testAdminCloudtentaclesLogin(payload: {
|
||||
baseUrl?: string
|
||||
username?: string
|
||||
password?: string
|
||||
phone?: string
|
||||
code?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesLoginTestResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/test-login',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function validateAdminCloudtentaclesSession(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesValidateSessionResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/validate-session',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesAsset(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/asset',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesCategories(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/categories',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesSkuList(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesSkuListResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/sku/list',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function buyAdminCloudtentaclesSku(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
id?: number
|
||||
count?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/sku/buy',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function useAdminCloudtentaclesSku(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
id?: number
|
||||
virtualNumberId?: number
|
||||
phone?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/sku/use',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesKnapsack(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/knapsack',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesDeliveryRecords(payload: {
|
||||
sourceKey?: string
|
||||
page?: number
|
||||
size?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
platformOrderId?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesDeliveryRecordListResult>(
|
||||
'/api/v1/admin/cloudtentacles-records',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesRecordSources() {
|
||||
return apiGet<{
|
||||
sources: Array<{
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
hasToken: boolean
|
||||
loggedInAt: string
|
||||
}>
|
||||
}>('/api/v1/admin/cloudtentacles-records/sources')
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesVnList(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesVnListResult>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/list',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function appointAdminCloudtentaclesVn(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/appoint',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function generateAdminCloudtentaclesVnLoginCode(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/generate-login-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesVnCode(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
phone?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/fetch-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function verifyAdminCloudtentaclesVnCode(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
code?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/verify-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminCloudtentaclesBindUrl(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/bind-url',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function backAdminCloudtentaclesVn(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
key?: string
|
||||
id?: number
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/vn/back',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function runAdminCloudtentaclesFullFlow(payload: {
|
||||
baseUrl?: string
|
||||
token?: string
|
||||
deviceId?: string
|
||||
deviceType?: number
|
||||
skuId?: number
|
||||
skuCount?: number
|
||||
vnKey?: string
|
||||
sourceKey?: string
|
||||
}) {
|
||||
return apiPost<Record<string, unknown>>(
|
||||
'/api/v1/admin/platform-config/cloudtentacles/debug/full-flow',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
export * from './notifications'
|
||||
export * from './scheduled-jobs'
|
||||
export * from './kuaishou-industry'
|
||||
export * from './kuaishou-feifei'
|
||||
export * from './affiliate-dash'
|
||||
export * from './cloudtentacles'
|
||||
export * from './fulfillment-routing'
|
||||
export * from './pdd'
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiProductSyncResult,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminKuaishouFeifeiConfig() {
|
||||
return apiGet<AdminKuaishouFeifeiConfigResponse>('/api/v1/admin/platform-config/kuaishou-feifei')
|
||||
}
|
||||
|
||||
export function saveAdminKuaishouFeifeiConfig(payload: AdminKuaishouFeifeiConfig) {
|
||||
return apiPost<AdminKuaishouFeifeiConfigResponse>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function matchAdminKuaishouFeifeiProduct(productName: string) {
|
||||
return apiPost<AdminKuaishouFeifeiMatchResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/match',
|
||||
{ productName },
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminKuaishouFeifeiProducts(payload: {
|
||||
page?: number
|
||||
perPage?: number
|
||||
status?: string
|
||||
supplyProductName?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouFeifeiProductListResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/products',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function syncAdminKuaishouFeifeiProducts(
|
||||
payload: {
|
||||
status?: string
|
||||
supplyProductName?: string
|
||||
} = {},
|
||||
) {
|
||||
return apiPost<AdminKuaishouFeifeiProductSyncResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/sync-products',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function createAdminKuaishouFeifeiTestOrder(payload: {
|
||||
platformOrderNo: string
|
||||
productCode: string
|
||||
platformBuyNum?: number
|
||||
platformAmount?: number | null
|
||||
playerAccount?: string
|
||||
playerGameRegion?: string
|
||||
playerGameSrv?: string
|
||||
playerGameRole?: string
|
||||
submitPlayer?: boolean
|
||||
notifyUrl?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouFeifeiOrderResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/test-order',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function queryAdminKuaishouFeifeiOrder(payload: {
|
||||
platformOrderNo?: string
|
||||
orderNo?: string
|
||||
}) {
|
||||
return apiPost<AdminKuaishouFeifeiOrderResult>(
|
||||
'/api/v1/admin/platform-config/kuaishou-feifei/query-order',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -52,41 +52,6 @@ export function completeAdminTaskManualDispatch(
|
||||
)
|
||||
}
|
||||
|
||||
export function prepareAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/prepare`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function refreshAdminTaskKuaishouCloudRoleInfo(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/refresh-role-info`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function rebindAdminTaskKuaishouCloudRole(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/rebind-role`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function dispatchAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/dispatch`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/return-number`,
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export function resendAdminTaskKuaishouIndustryVoucherCode(taskId: number | string) {
|
||||
return apiPost<AdminTaskActionResponse>(
|
||||
`/api/v1/admin/tasks/${taskId}/kuaishou-industry/resend-code`,
|
||||
|
||||
@@ -15,15 +15,3 @@ export function submitAffiliateDashClaim(
|
||||
) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/affiliate-dash/submit`, payload)
|
||||
}
|
||||
|
||||
export function confirmKuaishouCloudClaimRole(token: string) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
||||
}
|
||||
|
||||
export function rebindKuaishouCloudClaimRole(token: string) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/rebind-role`, {})
|
||||
}
|
||||
|
||||
export function redeemKuaishouCloudClaim(token: string) {
|
||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {})
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ export type {
|
||||
AdminNotificationConfig,
|
||||
AdminNotificationSendResult,
|
||||
AdminNotificationTestResult,
|
||||
AdminCloudtentaclesMonitorAccount,
|
||||
AdminScheduledJobAccountRuntime,
|
||||
AdminScheduledJobCloudtentaclesAccount,
|
||||
AdminScheduledJobItem,
|
||||
@@ -73,36 +72,6 @@ export type {
|
||||
AdminKuaishouIndustryRefreshTokenResponse,
|
||||
AdminKuaishouIndustryAuthorizationCodePayload,
|
||||
AdminKuaishouIndustryExchangeCodeResponse,
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductSyncResult,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceConfig,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionSummary,
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesVnListItem,
|
||||
AdminCloudtentaclesVnListResult,
|
||||
AdminCloudtentaclesDeliveryRecordItem,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
AdminFulfillmentRoutingConfig,
|
||||
AdminFulfillmentRoutingExecutorConfig,
|
||||
AdminFulfillmentRoutingPreviewResult,
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
export interface AdminCloudtentaclesSourceItem {
|
||||
key: string
|
||||
label: string
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSourcesConfig {
|
||||
enabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesPersistedSession {
|
||||
token: string
|
||||
tokenMasked: string
|
||||
baseUrl: string
|
||||
username: string
|
||||
phoneMasked: string
|
||||
loggedInAt: string
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
hasToken: boolean
|
||||
}
|
||||
|
||||
export type AdminCloudtentaclesSessionItem = AdminCloudtentaclesPersistedSession
|
||||
|
||||
export interface AdminCloudtentaclesSessionsMap {
|
||||
[sourceKey: string]: AdminCloudtentaclesPersistedSession
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSourceConfigResponse {
|
||||
filePath: string
|
||||
sessionFilePath: string
|
||||
enabled: boolean
|
||||
sources: AdminCloudtentaclesSourceItem[]
|
||||
sessions: AdminCloudtentaclesSessionsMap
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideDeliveryItem {
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideRule {
|
||||
id: string
|
||||
enabled: boolean
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
sourceKey: string
|
||||
deliveryItems: AdminCloudtentaclesOverrideDeliveryItem[]
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesOverrideRuleConfig {
|
||||
filePath: string
|
||||
enabled: boolean
|
||||
rules: AdminCloudtentaclesOverrideRule[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSendSmsResult {
|
||||
baseUrl: string
|
||||
username: string
|
||||
phone: string
|
||||
phoneMasked: string
|
||||
sentAt: string
|
||||
responseMessage: string
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSessionSummary {
|
||||
tokenMasked: string
|
||||
permissionCount: number
|
||||
permissions: string[]
|
||||
persisted?: boolean
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesLoginTestResult {
|
||||
baseUrl: string
|
||||
username: string
|
||||
phoneMasked: string
|
||||
loggedInAt: string
|
||||
responseMessage: string
|
||||
token: string
|
||||
session: AdminCloudtentaclesSessionSummary
|
||||
userInfo: Record<string, unknown>
|
||||
asset: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesValidateSessionResult {
|
||||
baseUrl: string
|
||||
loggedInAt: string
|
||||
session: AdminCloudtentaclesSessionSummary
|
||||
userInfo: Record<string, unknown>
|
||||
asset: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSkuItem {
|
||||
id: number
|
||||
categoriesId: number
|
||||
name: string
|
||||
description: string
|
||||
image: string
|
||||
inventory: number
|
||||
price: number
|
||||
listingTime: string
|
||||
delistingTime: string
|
||||
buyLimitMin: number
|
||||
buyLimitMax: number
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesSkuListResult {
|
||||
baseUrl: string
|
||||
itemCount: number
|
||||
occupiedItemCount: number
|
||||
items: AdminCloudtentaclesSkuItem[]
|
||||
rawItems: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesDeliveryRecordItem {
|
||||
createdAt: string
|
||||
recordId: string
|
||||
virtualNumberId: number
|
||||
userId: number
|
||||
count: number
|
||||
cdk: string
|
||||
exchangeUrl: string
|
||||
name: string
|
||||
skuId: number
|
||||
skuName: string
|
||||
skuImage: string
|
||||
phone: string
|
||||
status: number
|
||||
fulfillUser: string
|
||||
operatorName: string
|
||||
platformOrderId: string
|
||||
taskId: number
|
||||
taskNo: string
|
||||
orderShopId: string
|
||||
orderShopName: string
|
||||
consumeShopId: string
|
||||
consumeShopName: string
|
||||
roleName: string
|
||||
roleId: string
|
||||
localTaskStatus: string
|
||||
localDeliveryStatus: string
|
||||
localDispatchStatus: string
|
||||
localDispatchAt: string
|
||||
localResultCode: string
|
||||
localResultMessage: string
|
||||
matchConfidence: string
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesDeliveryRecordListResult {
|
||||
baseUrl: string
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
sourceKey?: string
|
||||
queryStartDate?: string
|
||||
queryEndDate?: string
|
||||
localOrderMatched?: boolean
|
||||
items: AdminCloudtentaclesDeliveryRecordItem[]
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** @deprecated Use AdminCloudtentaclesSourceItem + AdminCloudtentaclesSourcesConfig instead */
|
||||
export interface AdminCloudtentaclesSourceConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
username: string
|
||||
password: string
|
||||
phone: string
|
||||
deviceId: string
|
||||
deviceType: number
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesVnListItem {
|
||||
id: number
|
||||
phone: string
|
||||
status: number
|
||||
countTime: number
|
||||
bindInfo: string | Record<string, unknown> | null
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminCloudtentaclesVnListResult {
|
||||
baseUrl: string
|
||||
key: string
|
||||
itemCount: number
|
||||
items: AdminCloudtentaclesVnListItem[]
|
||||
rawItems: Array<Record<string, unknown>>
|
||||
}
|
||||
@@ -26,19 +26,6 @@ export type {
|
||||
AdminKuaishouIndustryExchangeCodeResponse,
|
||||
} from './kuaishou-industry'
|
||||
|
||||
export type {
|
||||
AdminKuaishouFeifeiProductRule,
|
||||
AdminKuaishouFeifeiConfig,
|
||||
AdminKuaishouFeifeiEffectiveConfig,
|
||||
AdminKuaishouFeifeiConfigResponse,
|
||||
AdminKuaishouFeifeiProductSyncResult,
|
||||
AdminKuaishouFeifeiProductMatch,
|
||||
AdminKuaishouFeifeiMatchResult,
|
||||
AdminKuaishouFeifeiProductItem,
|
||||
AdminKuaishouFeifeiProductListResult,
|
||||
AdminKuaishouFeifeiOrderResult,
|
||||
} from './kuaishou-feifei'
|
||||
|
||||
export type {
|
||||
AdminAffiliateDashConfig,
|
||||
AdminAffiliateDashEffectiveConfig,
|
||||
@@ -49,29 +36,6 @@ export type {
|
||||
AdminAffiliateDashWallet,
|
||||
} from './affiliate-dash'
|
||||
|
||||
export type {
|
||||
AdminCloudtentaclesSourceItem,
|
||||
AdminCloudtentaclesSourcesConfig,
|
||||
AdminCloudtentaclesSourceConfigResponse,
|
||||
AdminCloudtentaclesOverrideDeliveryItem,
|
||||
AdminCloudtentaclesOverrideRule,
|
||||
AdminCloudtentaclesOverrideRuleConfig,
|
||||
AdminCloudtentaclesSessionsMap,
|
||||
AdminCloudtentaclesSourceConfig,
|
||||
AdminCloudtentaclesPersistedSession,
|
||||
AdminCloudtentaclesSessionItem,
|
||||
AdminCloudtentaclesSendSmsResult,
|
||||
AdminCloudtentaclesSessionSummary,
|
||||
AdminCloudtentaclesLoginTestResult,
|
||||
AdminCloudtentaclesValidateSessionResult,
|
||||
AdminCloudtentaclesSkuItem,
|
||||
AdminCloudtentaclesSkuListResult,
|
||||
AdminCloudtentaclesVnListItem,
|
||||
AdminCloudtentaclesVnListResult,
|
||||
AdminCloudtentaclesDeliveryRecordItem,
|
||||
AdminCloudtentaclesDeliveryRecordListResult,
|
||||
} from './cloudtentacles'
|
||||
|
||||
export type {
|
||||
AdminFulfillmentRoutingConfig,
|
||||
AdminFulfillmentRoutingExecutorConfig,
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
export interface AdminKuaishouFeifeiProductRule {
|
||||
id: string
|
||||
enabled: boolean
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
productCode: string
|
||||
skuName: string
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
timeoutMs: number
|
||||
notifyUrl: string
|
||||
productRules: AdminKuaishouFeifeiProductRule[]
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiEffectiveConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
timeoutMs: number
|
||||
notifyUrl: string
|
||||
hasAppKey: boolean
|
||||
hasAppSecret: boolean
|
||||
productRuleCount: number
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiConfigResponse {
|
||||
filePath: string
|
||||
source: AdminKuaishouFeifeiConfig
|
||||
effective: AdminKuaishouFeifeiEffectiveConfig
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductSyncResult extends AdminKuaishouFeifeiConfigResponse {
|
||||
sync: {
|
||||
productCount: number
|
||||
ruleCount: number
|
||||
status: string
|
||||
supplyProductName: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductMatch {
|
||||
matchMode: string
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
productCode: string
|
||||
skuName: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiMatchResult {
|
||||
productName: string
|
||||
normalizedProductName: string
|
||||
matched: boolean
|
||||
match: AdminKuaishouFeifeiProductMatch | null
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductItem {
|
||||
productCode: string
|
||||
name: string
|
||||
channel: string
|
||||
externalProductId: string
|
||||
imageUrl: string
|
||||
status: string
|
||||
supplyStatus: string
|
||||
supplyPricePoints: number
|
||||
feePoints: number
|
||||
unitCostPoints: number
|
||||
maxOrderQuantity: number
|
||||
salePricePoints: number | null
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiProductListResult {
|
||||
items: AdminKuaishouFeifeiProductItem[]
|
||||
total: number
|
||||
page: number
|
||||
perPage: number
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface AdminKuaishouFeifeiOrderResult {
|
||||
orderNo: string
|
||||
platformOrderNo: string
|
||||
productCode: string
|
||||
productName: string
|
||||
rechargeStatus: number
|
||||
rechargeStatusLabel: string
|
||||
pointsCharged: number
|
||||
playerAccount: string
|
||||
platformBuyNum: number
|
||||
rechargeResultMessage: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
rechargeFinishAt: string
|
||||
h5: {
|
||||
entryUrl: string
|
||||
rechargeUrl: string
|
||||
}
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
@@ -198,6 +198,8 @@ export interface ClaimDetailData {
|
||||
| 'kuaishou_feifei'
|
||||
| 'affiliate_dash'
|
||||
| (string & {})
|
||||
channelOffline?: boolean
|
||||
channelOfflineMessage?: string
|
||||
claimIdentity: ClaimIdentityInfo | null
|
||||
task: ClaimTaskInfo
|
||||
order: ClaimOrderInfo
|
||||
|
||||
@@ -7,8 +7,6 @@ export const adminPayStatusOptions = [
|
||||
]
|
||||
|
||||
export const adminOrderExecutorOptions = [
|
||||
{ label: '乐玩云', value: 'kuaishou_ct_assisted' },
|
||||
{ label: '飞飞', value: 'kuaishou_feifei' },
|
||||
{ label: 'Affiliate Dash', value: 'affiliate_dash' },
|
||||
{ label: '行业凭证', value: 'kuaishou-industry' },
|
||||
{ label: '人工履约', value: 'manual_dispatch' },
|
||||
@@ -58,7 +56,6 @@ export const adminAuditActionOptions = [
|
||||
{ label: '修改用户角色', value: 'admin_user_role_updated' },
|
||||
{ label: '修改用户状态', value: 'admin_user_status_updated' },
|
||||
{ label: '重置用户密码', value: 'admin_user_password_reset' },
|
||||
{ label: '换绑角色', value: 'task_kuaishou_cloud_rebind_role' },
|
||||
{ label: '关闭任务', value: 'task_closed' },
|
||||
{ label: '转人工处理', value: 'task_mark_manual_review' },
|
||||
{ label: '工单发布', value: 'work_order_published' },
|
||||
|
||||
Reference in New Issue
Block a user