修复 kuaishou-lewan 多账号健康检查与平台配置展示

恢复 health 任务对全部账号的合并监控,补齐 React 后台多账号 UI 与账号选择,并将用户可见文案统一为 kuaishou-lewan。
This commit is contained in:
yml2213
2026-07-10 21:43:32 +08:00
parent 01f46e4fc7
commit 3196e1ccdc
7 changed files with 558 additions and 112 deletions
@@ -170,21 +170,63 @@ function mapScheduledJobCloudtentaclesAccounts(
cloudtentaclesAccountMap: Map<string, JsonObject>,
) {
const accounts = Array.isArray(rawAccounts) ? rawAccounts : []
return accounts.map((item) => {
const source = isPlainObject(item) ? item : {}
const sourceKey = String(source.sourceKey || source.key || '').trim()
const option = cloudtentaclesAccountMap.get(sourceKey) || {}
const configuredMap = new Map(
accounts
.map((item) => {
const source = isPlainObject(item) ? item : {}
const sourceKey = String(source.sourceKey || source.key || '').trim()
if (!sourceKey) {
return null
}
const option = cloudtentaclesAccountMap.get(sourceKey) || {}
return [
sourceKey,
{
sourceKey,
label: String(source.label || option.label || sourceKey).trim(),
enabled: source.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
source.assetThreshold ?? source.threshold,
defaultAssetThreshold,
),
},
] as const
})
.filter(Boolean) as Array<readonly [string, {
sourceKey: string
label: string
enabled: boolean
assetThreshold: number
}]>,
)
// 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。
const merged = Array.from(cloudtentaclesAccountMap.values()).map((option) => {
const sourceKey = String(option.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(source.label || option.label || sourceKey).trim(),
enabled: source.enabled !== false,
label: String(configured?.label || option.label || sourceKey).trim(),
enabled: configured
? configured.enabled !== false
: option.enabled !== false,
assetThreshold: normalizeNonNegativeNumber(
source.assetThreshold ?? source.threshold,
configured?.assetThreshold,
defaultAssetThreshold,
),
}
}).filter((item) => item.sourceKey)
const mergedKeys = new Set(merged.map((item) => item.sourceKey))
for (const [sourceKey, configured] of configuredMap.entries()) {
if (mergedKeys.has(sourceKey)) {
continue
}
merged.push(configured)
}
return merged
}
function normalizeNonNegativeNumber(value: unknown, fallback: number) {
@@ -191,23 +191,64 @@ function resolveCloudtentaclesHealthAccounts(job: JsonObject) {
const rawAccounts = Array.isArray(config.accounts) ? config.accounts : []
const defaultThreshold = normalizeNonNegativeInteger(config.assetThreshold, 500)
const sourceConfig = listCloudtentaclesSources()
const sources = Array.isArray(sourceConfig.sources) ? sourceConfig.sources : []
const sourceMap = new Map(
(Array.isArray(sourceConfig.sources) ? sourceConfig.sources : [])
.map((source) => [String(source.key || '').trim(), source]),
sources
.map((source) => [String(source.key || '').trim(), source] as const)
.filter(([sourceKey]) => Boolean(sourceKey)),
)
const normalizedAccounts = rawAccounts
.map((item) => normalizeCloudtentaclesHealthAccount(item, defaultThreshold, sourceMap))
const configuredMap = new Map(
rawAccounts
.map((item) => normalizeCloudtentaclesHealthAccount(item, defaultThreshold, sourceMap))
.filter(Boolean)
.map((item) => [String(item!.sourceKey || '').trim(), item!] as const)
.filter(([sourceKey]) => Boolean(sourceKey)),
)
// 以平台配置中的全部账号为准,合并任务里已保存的阈值/开关;新增账号自动纳入监控。
const mergedAccounts = sources
.map((source) => {
const sourceKey = String(source.key || '').trim()
if (!sourceKey) {
return null
}
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(
configured?.label || source.label || source.username || sourceKey,
).trim() || sourceKey,
enabled: configured
? configured.enabled !== false
: source.enabled !== false,
assetThreshold: normalizeNonNegativeInteger(
configured?.assetThreshold,
defaultThreshold,
),
}
})
.filter(Boolean) as JsonObject[]
if (normalizedAccounts.length > 0) {
return dedupeAccounts(normalizedAccounts)
// 保留已配置但源账号已删除的项,便于报 source_missing。
for (const [sourceKey, configured] of configuredMap.entries()) {
if (sourceMap.has(sourceKey)) {
continue
}
mergedAccounts.push(configured)
}
if (mergedAccounts.length > 0) {
return dedupeAccounts(mergedAccounts)
}
// 兼容旧配置:没有任何平台账号时,仍检查历史 default。
const fallback = configuredMap.get('default')
return [{
sourceKey: 'default',
label: sourceMap.get('default')?.label || '默认账号',
enabled: true,
assetThreshold: defaultThreshold,
label: String(fallback?.label || '默认账号').trim() || '默认账号',
enabled: fallback ? fallback.enabled !== false : true,
assetThreshold: normalizeNonNegativeInteger(fallback?.assetThreshold, defaultThreshold),
}]
}
@@ -249,7 +290,7 @@ function dedupeAccounts(accounts: JsonObject[]) {
function buildCloudtentaclesHealthSummary(results: CloudtentaclesHealthAccountResult[]) {
if (results.length === 0) {
return '未配置 cloudtentacles 监控账号'
return '未配置 kuaishou-lewan 监控账号'
}
const checkedResults = results.filter((item) => !item.skipped)
@@ -191,7 +191,7 @@ export default function AdminCloudtentaclesRecordsPage() {
setSourceKey(nextSourceKey)
return nextSourceKey
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : '读取 cloudtentacles 账号失败')
setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 账号失败')
return ''
} finally {
setSourceLoading(false)
@@ -213,7 +213,7 @@ export default function AdminCloudtentaclesRecordsPage() {
const orderKeyword = (options.platformOrderId ?? platformOrderId).trim()
if (!nextSourceKey && !orderKeyword) {
showError('请先选择 cloudtentacles 账号')
showError('请先选择 kuaishou-lewan 账号')
return
}
@@ -291,7 +291,7 @@ export default function AdminCloudtentaclesRecordsPage() {
<section className="page-stack">
<PageHeader
title="查询发货记录"
description="按订单号、cloudtentacles 账号和时间范围查询平台发货记录。"
description="按订单号、kuaishou-lewan 账号和时间范围查询平台发货记录。"
extra={<span className="total-badge"> {total} </span>}
/>
@@ -299,7 +299,7 @@ export default function AdminCloudtentaclesRecordsPage() {
title="查询条件"
extra={
<Typography.Text type="secondary">
cloudtentacles
kuaishou-lewan
</Typography.Text>
}
>
@@ -307,7 +307,7 @@ export default function AdminCloudtentaclesRecordsPage() {
<Select
value={sourceKey || undefined}
loading={sourceLoading}
placeholder="cloudtentacles 账号"
placeholder="kuaishou-lewan 账号"
options={sourceOptions.map((source) => ({
label: `${source.label || source.key}${source.hasToken ? '' : ' · 未登录'}`,
value: source.key,
@@ -430,7 +430,7 @@ export default function AdminTaskDetailPage() {
'refresh-role',
() => refreshAdminTaskKuaishouCloudRoleInfo(resolvedDetail.task.taskId),
'角色信息已刷新',
`确认刷新任务 ${resolvedDetail.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 cloudtentacles 的绑定结果。`,
`确认刷新任务 ${resolvedDetail.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 kuaishou-lewan 的绑定结果。`,
)
}
/>
@@ -768,7 +768,7 @@ function KuaishouCloudFulfillmentPanel() {
)
setSelectedSourceKey(resolveDefaultSourceKey(nextRows, selectedSourceKey))
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : '读取 cloudtentacles 履约配置失败')
setErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 履约配置失败')
} finally {
setLoading(false)
}
@@ -788,7 +788,7 @@ function KuaishouCloudFulfillmentPanel() {
setSkuItems(Array.isArray(response.data.items) ? response.data.items : [])
} catch (error) {
setSkuItems([])
setSkuErrorMessage(error instanceof Error ? error.message : '读取 cloudtentacles 商品失败')
setSkuErrorMessage(error instanceof Error ? error.message : '读取 kuaishou-lewan 商品失败')
} finally {
setSkuLoading(false)
}
@@ -818,7 +818,7 @@ function KuaishouCloudFulfillmentPanel() {
)
showSuccess(`商品覆盖规则已保存,共 ${response.data.rules.length}`)
} catch (error) {
const message = error instanceof Error ? error.message : '保存 cloudtentacles 覆盖规则失败'
const message = error instanceof Error ? error.message : '保存 kuaishou-lewan 覆盖规则失败'
setOverrideErrorMessage(message)
showError(message)
} finally {
@@ -923,7 +923,7 @@ function KuaishouCloudFulfillmentPanel() {
</div>
<div className="platform-split-layout">
<Card title="cloudtentacles 账号" className="platform-side-card">
<Card title="kuaishou-lewan 账号" className="platform-side-card">
{sourceRows.length === 0 ? (
<Empty description="暂无账号,请先到平台配置登录。" />
) : (
@@ -980,7 +980,7 @@ function KuaishouCloudFulfillmentPanel() {
<Input
allowClear
value={matchInput}
placeholder="输入 91 卡券 productNo / 商品名字,查看命中的 cloudtentacles 商品"
placeholder="输入 91 卡券 productNo / 商品名字,查看命中的 kuaishou-lewan 商品"
onChange={(event) => setMatchInput(event.target.value)}
/>
<Button icon={<PlusOutlined />} onClick={addOverrideRule}>
@@ -1084,7 +1084,7 @@ function KuaishouCloudFulfillmentPanel() {
<Select
showSearch
value={item.cloudSkuId || undefined}
placeholder="选择 cloudtentacles 商品"
placeholder="选择 kuaishou-lewan 商品"
optionFilterProp="label"
options={skuItems.map((sku) => ({
label: formatSkuOption(sku),
@@ -1094,7 +1094,7 @@ function KuaishouCloudFulfillmentPanel() {
/>
<Input
value={item.cloudSkuName}
placeholder="cloudtentacles 商品名"
placeholder="kuaishou-lewan 商品名"
onChange={(event) =>
updateDeliveryItem(ruleIndex, itemIndex, {
cloudSkuName: event.target.value,
@@ -1129,14 +1129,14 @@ function KuaishouCloudFulfillmentPanel() {
)}
</Card>
<Card title="cloudtentacles 商品列表">
<Card title="kuaishou-lewan 商品列表">
<Table<AdminCloudtentaclesSkuItem>
rowKey="id"
loading={skuLoading}
dataSource={skuItems}
columns={[
{
title: 'cloudtentacles 商品',
title: 'kuaishou-lewan 商品',
dataIndex: 'name',
minWidth: 260,
render: (_, row) => (
@@ -59,6 +59,7 @@ import {
validateAdminCloudtentaclesSession,
} from '@/services/admin'
import type {
AdminCloudtentaclesMonitorAccount,
AdminCloudtentaclesSourceConfigResponse,
AdminCloudtentaclesSourceItem,
AdminKuaishouIndustryConfigResponse,
@@ -72,6 +73,8 @@ import type {
AdminNinetyoneOrderListResult,
AdminNotificationConfig,
AdminNotificationTestResult,
AdminScheduledJobAccountRuntime,
AdminScheduledJobCloudtentaclesAccount,
AdminScheduledJobItem,
AdminScheduledJobRuntimeState,
AdminScheduledJobsConfig,
@@ -100,15 +103,7 @@ type ScheduledJobsState = {
filePath: string
source: AdminScheduledJobsConfig
runtime: AdminScheduledJobRuntimeState[]
cloudtentaclesAccounts: Array<{
sourceKey: string
label: string
enabled: boolean
username: string
phoneMasked: string
hasToken: boolean
loggedInAt: string
}>
cloudtentaclesAccounts: AdminCloudtentaclesMonitorAccount[]
}
const platformTabs: PlatformTab[] = [
@@ -259,14 +254,14 @@ export default function AdminPlatformShopsPage() {
},
{
key: 'cloudtentacles',
label: 'cloudtentacles',
label: 'kuaishou-lewan',
children: cloudtentaclesConfig ? (
<CloudtentaclesPlatformPanel
config={cloudtentaclesConfig}
onChange={setCloudtentaclesConfig}
/>
) : (
<Empty description="cloudtentacles 配置未加载" />
<Empty description="kuaishou-lewan 配置未加载" />
),
},
]}
@@ -474,6 +469,18 @@ function NotificationPanel({
const [runningJobId, setRunningJobId] = useState('')
const [testResult, setTestResult] = useState<AdminNotificationTestResult | null>(null)
const source = notificationConfig.source
const monitorAccounts = scheduledJobs.cloudtentaclesAccounts || []
const jobsWithMergedAccounts = scheduledJobs.source.jobs.map((job) => ({
...job,
config: {
...job.config,
accounts: mergeScheduledJobAccounts(
job.config?.accounts || [],
monitorAccounts,
Number(job.config?.assetThreshold ?? 500),
),
},
}))
async function saveNotification() {
setSavingNotification(true)
@@ -507,7 +514,22 @@ function NotificationPanel({
async function saveJobs() {
setSavingJobs(true)
try {
const response = await saveAdminScheduledJobsConfig(scheduledJobs.source)
const payload: AdminScheduledJobsConfig = {
...scheduledJobs.source,
jobs: jobsWithMergedAccounts.map((job) => ({
...job,
config: {
assetThreshold: Number(job.config?.assetThreshold ?? 500),
accounts: (job.config?.accounts || []).map((account) => ({
sourceKey: account.sourceKey,
label: account.label,
enabled: account.enabled !== false,
assetThreshold: Number(account.assetThreshold ?? job.config?.assetThreshold ?? 500),
})),
},
})),
}
const response = await saveAdminScheduledJobsConfig(payload)
onScheduledJobsChange(response.data)
showSuccess('定时任务配置已保存')
} catch (error) {
@@ -541,6 +563,15 @@ function NotificationPanel({
onScheduledJobsChange({ ...scheduledJobs, source: nextSource })
}
function updateJob(index: number, nextJob: AdminScheduledJobItem) {
updateJobs({
...scheduledJobs.source,
jobs: jobsWithMergedAccounts.map((item, jobIndex) =>
jobIndex === index ? nextJob : item,
),
})
}
return (
<section className="platform-panel-stack">
<Card
@@ -649,7 +680,7 @@ function NotificationPanel({
</Card>
<Card
title="定时任务"
title="监控任务"
extra={
<Space wrap>
<Typography.Text type="secondary">{scheduledJobs.filePath || '默认配置'}</Typography.Text>
@@ -659,6 +690,9 @@ function NotificationPanel({
</Space>
}
>
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
kuaishou-lewan
</Typography.Paragraph>
<FieldSwitch
label="任务总开关"
checked={scheduledJobs.source.enabled}
@@ -666,21 +700,15 @@ function NotificationPanel({
/>
<Space direction="vertical" className="full-width platform-section-gap" size={12}>
{scheduledJobs.source.jobs.map((job, index) => (
{jobsWithMergedAccounts.map((job, index) => (
<ScheduledJobCard
key={job.id}
job={job}
runtime={scheduledJobs.runtime.find((item) => item.id === job.id) || null}
monitorAccounts={monitorAccounts}
running={runningJobId === job.id}
onRun={() => runJob(job.id)}
onChange={(nextJob) =>
updateJobs({
...scheduledJobs.source,
jobs: scheduledJobs.source.jobs.map((item, jobIndex) =>
jobIndex === index ? nextJob : item,
),
})
}
onChange={(nextJob) => updateJob(index, nextJob)}
/>
))}
</Space>
@@ -754,26 +782,138 @@ function RecipientList<T extends { id: string; name: string; enabled: boolean }>
function ScheduledJobCard({
job,
runtime,
monitorAccounts,
running,
onRun,
onChange,
}: {
job: AdminScheduledJobItem
runtime: AdminScheduledJobRuntimeState | null
monitorAccounts: AdminCloudtentaclesMonitorAccount[]
running: boolean
onRun: () => void
onChange: (job: AdminScheduledJobItem) => void
}) {
const accounts = job.config?.accounts || []
const defaultThreshold = Number(job.config?.assetThreshold ?? 500)
function updateAccount(sourceKey: string, patch: Partial<AdminScheduledJobCloudtentaclesAccount>) {
onChange({
...job,
config: {
...job.config,
accounts: accounts.map((account) =>
account.sourceKey === sourceKey ? { ...account, ...patch } : account,
),
},
})
}
function updateDefaultThreshold(assetThreshold: number) {
onChange({
...job,
config: {
...job.config,
assetThreshold,
accounts: accounts.map((account) => ({
...account,
// 仅同步仍等于旧默认阈值的账号,避免覆盖用户单独设置
assetThreshold:
Number(account.assetThreshold) === defaultThreshold
? assetThreshold
: account.assetThreshold,
})),
},
})
}
const accountColumns: TableColumnsType<AdminScheduledJobCloudtentaclesAccount> = [
{
title: '账号',
minWidth: 220,
render: (_, account) => {
const source = monitorAccounts.find((item) => item.sourceKey === account.sourceKey)
return (
<Space direction="vertical" size={0}>
<Typography.Text strong>{account.label || account.sourceKey}</Typography.Text>
<Typography.Text type="secondary">{account.sourceKey}</Typography.Text>
<Typography.Text type="secondary">
{formatMonitorAccountMeta(account, source)}
</Typography.Text>
</Space>
)
},
},
{
title: '监控',
width: 90,
align: 'center',
render: (_, account) => (
<Switch
size="small"
checked={account.enabled !== false}
onChange={(enabled) => updateAccount(account.sourceKey, { enabled })}
/>
),
},
{
title: '余额阈值',
width: 140,
render: (_, account) => (
<InputNumber
min={0}
value={account.assetThreshold}
style={{ width: '100%' }}
onChange={(value) =>
updateAccount(account.sourceKey, {
assetThreshold: Number(value ?? defaultThreshold),
})
}
/>
),
},
{
title: '最近结果',
minWidth: 240,
render: (_, account) => {
const accountRuntime = getAccountRuntime(runtime, account.sourceKey)
if (!accountRuntime) {
return <Typography.Text type="secondary"></Typography.Text>
}
return (
<Space direction="vertical" size={2}>
<Space wrap size={6}>
<Tag color={resolveHealthStatusColor(accountRuntime.status)}>
{formatHealthStatus(accountRuntime.status)}
</Tag>
<Typography.Text type="secondary">
{accountRuntime.asset ?? '-'} / {accountRuntime.threshold ?? account.assetThreshold}
</Typography.Text>
</Space>
<Typography.Text type="secondary">
{accountRuntime.message || formatHealthStatus(accountRuntime.status)}
</Typography.Text>
<Typography.Text type="secondary">
{formatAdminDateTime(accountRuntime.checkedAt)}
</Typography.Text>
</Space>
)
},
},
]
return (
<Card
size="small"
title={
<Space>
<Space wrap>
<Switch checked={job.enabled !== false} onChange={(enabled) => onChange({ ...job, enabled })} />
<span>{job.id}</span>
<span>{formatScheduledJobTitle(job)}</span>
<Tag color={job.enabled ? 'green' : 'default'}>{job.enabled ? '已启用' : '已停用'}</Tag>
</Space>
}
extra={<Button icon={<ReloadOutlined />} loading={running} onClick={onRun}></Button>}
extra={<Button icon={<ReloadOutlined />} loading={running} onClick={onRun}></Button>}
>
<div className="platform-form-grid">
<NumberField
@@ -789,28 +929,148 @@ function ScheduledJobCard({
onChange={(cooldownSeconds) => onChange({ ...job, cooldownSeconds })}
/>
<NumberField
label="资产阈值"
value={job.config.assetThreshold}
label="默认余额阈值"
value={defaultThreshold}
min={0}
onChange={(assetThreshold) =>
onChange({ ...job, config: { ...job.config, assetThreshold } })
}
onChange={(assetThreshold) => updateDefaultThreshold(Number(assetThreshold ?? 500))}
/>
</div>
{runtime ? (
<Alert
className="platform-section-gap"
type={runtime.lastStatus === 'success' ? 'success' : runtime.lastStatus ? 'warning' : 'info'}
type={
runtime.lastStatus === 'ok' || runtime.lastStatus === 'success'
? 'success'
: runtime.lastStatus
? 'warning'
: 'info'
}
showIcon
message={runtime.lastMessage || '尚未运行'}
description={`上次完成:${formatAdminDateTime(runtime.lastFinishedAt)} · 下次:${formatAdminDateTime(runtime.nextRunAt)}`}
description={
[
`已检查 ${runtime.lastCheckedCount ?? 0} / ${accounts.length}`,
`预警 ${runtime.lastLowAssetCount ?? 0}`,
`异常 ${runtime.lastFailedCount ?? 0}`,
`上次:${formatAdminDateTime(runtime.lastFinishedAt || runtime.lastRunAt)}`,
`下次:${formatAdminDateTime(runtime.nextRunAt)}`,
].join(' · ')
}
/>
) : null}
<Table<AdminScheduledJobCloudtentaclesAccount>
className="platform-section-gap"
rowKey="sourceKey"
size="small"
pagination={false}
columns={accountColumns}
dataSource={accounts}
locale={{ emptyText: '暂无 kuaishou-lewan 账号,请先在平台配置登录。' }}
/>
</Card>
)
}
function mergeScheduledJobAccounts(
configuredAccounts: AdminScheduledJobCloudtentaclesAccount[],
monitorAccounts: AdminCloudtentaclesMonitorAccount[],
defaultAssetThreshold: number,
) {
const configuredMap = new Map(
configuredAccounts
.map((item) => [String(item.sourceKey || '').trim(), item] as const)
.filter(([sourceKey]) => Boolean(sourceKey)),
)
const merged = monitorAccounts
.filter((item) => String(item.sourceKey || '').trim())
.map((item) => {
const sourceKey = String(item.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey)
return {
sourceKey,
label: String(configured?.label || item.label || sourceKey).trim(),
enabled: configured ? configured.enabled !== false : item.enabled !== false,
assetThreshold: Number(
configured?.assetThreshold ?? defaultAssetThreshold,
),
}
})
const mergedKeys = new Set(merged.map((item) => item.sourceKey))
for (const configured of configuredAccounts) {
const sourceKey = String(configured.sourceKey || '').trim()
if (!sourceKey || mergedKeys.has(sourceKey)) {
continue
}
merged.push({
sourceKey,
label: String(configured.label || sourceKey).trim(),
enabled: configured.enabled !== false,
assetThreshold: Number(configured.assetThreshold ?? defaultAssetThreshold),
})
}
return merged
}
function formatScheduledJobTitle(job: AdminScheduledJobItem) {
if (job.type === 'cloudtentacles_health' || job.id === 'cloudtentacles-health') {
return 'kuaishou-lewan 健康检查'
}
return job.id || job.type || '定时任务'
}
function formatMonitorAccountMeta(
account: AdminScheduledJobCloudtentaclesAccount,
source: AdminCloudtentaclesMonitorAccount | undefined,
) {
if (!source) {
return '账号配置不存在'
}
return [
source.username ? `登录名 ${source.username}` : '',
source.phoneMasked ? `手机 ${source.phoneMasked}` : '',
source.hasToken ? 'Token 已保存' : '未登录',
]
.filter(Boolean)
.join(' · ') || account.sourceKey
}
function getAccountRuntime(
runtime: AdminScheduledJobRuntimeState | null,
sourceKey: string,
): AdminScheduledJobAccountRuntime | null {
if (!runtime?.lastAccounts?.length) {
return null
}
return runtime.lastAccounts.find((item) => item.sourceKey === sourceKey) || null
}
function formatHealthStatus(status: string) {
const normalized = String(status || '').trim()
if (normalized === 'ok' || normalized === 'success') return '正常'
if (normalized === 'asset_low') return '余额预警'
if (normalized === 'auth_missing') return '未登录'
if (normalized === 'source_missing') return '账号缺失'
if (normalized === 'disabled') return '已停用'
if (normalized === 'skipped') return '已跳过'
if (normalized === 'running') return '执行中'
if (normalized === 'failed') return '检查失败'
if (normalized === 'pending') return '待执行'
return normalized || '待执行'
}
function resolveHealthStatusColor(status: string) {
const normalized = String(status || '').trim()
if (normalized === 'ok' || normalized === 'success') return 'green'
if (normalized === 'asset_low' || normalized === 'running') return 'orange'
if (['auth_missing', 'source_missing', 'failed'].includes(normalized)) return 'red'
return 'default'
}
function KuaishouIndustryPanel({
config,
onChange,
@@ -1529,7 +1789,14 @@ function CloudtentaclesPlatformPanel({
const [selectedSourceKey, setSelectedSourceKey] = useState(config.sources[0]?.key || '')
const [smsCode, setSmsCode] = useState('')
const [debugResult, setDebugResult] = useState<unknown>(null)
const selectedSource = config.sources.find((source) => source.key === selectedSourceKey) || config.sources[0] || null
useEffect(() => {
if (!config.sources.some((source) => source.key === selectedSourceKey)) {
setSelectedSourceKey(config.sources[0]?.key || '')
}
}, [config.sources, selectedSourceKey])
const selectedSource = config.sources.find((source) => source.key === selectedSourceKey) || null
const selectedSession = selectedSource ? config.sessions[selectedSource.key] : null
async function saveConfig() {
@@ -1540,9 +1807,13 @@ function CloudtentaclesPlatformPanel({
sources: config.sources,
})
onChange(response.data)
showSuccess('cloudtentacles 账号配置已保存')
const nextKey = selectedSourceKey && response.data.sources.some((item) => item.key === selectedSourceKey)
? selectedSourceKey
: response.data.sources[0]?.key || ''
setSelectedSourceKey(nextKey)
showSuccess('kuaishou-lewan 账号配置已保存')
} catch (error) {
showError(error instanceof Error ? error.message : '保存 cloudtentacles 账号失败')
showError(error instanceof Error ? error.message : '保存 kuaishou-lewan 账号失败')
} finally {
setSaving(false)
}
@@ -1554,24 +1825,35 @@ function CloudtentaclesPlatformPanel({
await deleteAdminCloudtentaclesSource(sourceKey)
const response = await fetchAdminCloudtentaclesSourceConfig()
onChange(response.data)
setSelectedSourceKey(response.data.sources[0]?.key || '')
showSuccess('cloudtentacles 账号已删除')
const nextKey = selectedSourceKey === sourceKey
? response.data.sources[0]?.key || ''
: selectedSourceKey
setSelectedSourceKey(
response.data.sources.some((item) => item.key === nextKey)
? nextKey
: response.data.sources[0]?.key || '',
)
showSuccess('kuaishou-lewan 账号已删除')
} catch (error) {
showError(error instanceof Error ? error.message : '删除 cloudtentacles 账号失败')
showError(error instanceof Error ? error.message : '删除 kuaishou-lewan 账号失败')
} finally {
setDebugLoading('')
}
}
async function runDebug(action: string, handler: () => Promise<unknown>) {
if (!selectedSource) {
showError('请先选择一个账号')
return
}
setDebugLoading(action)
setDebugResult(null)
try {
const result = await handler()
setDebugResult(result)
showSuccess('cloudtentacles 调试完成')
showSuccess(`账号 ${formatCloudtentaclesSourceLabel(selectedSource)} 调试完成`)
} catch (error) {
showError(error instanceof Error ? error.message : 'cloudtentacles 调试失败')
showError(error instanceof Error ? error.message : 'kuaishou-lewan 调试失败')
} finally {
setDebugLoading('')
}
@@ -1587,6 +1869,9 @@ function CloudtentaclesPlatformPanel({
source.key === sourceKey ? { ...source, ...patch } : source,
),
})
if (patch.key && patch.key !== sourceKey && selectedSourceKey === sourceKey) {
setSelectedSourceKey(String(patch.key))
}
}
function addSource() {
@@ -1613,7 +1898,7 @@ function CloudtentaclesPlatformPanel({
return (
<section className="platform-panel-stack">
<Card
title="cloudtentacles 履约账号"
title="kuaishou-lewan 履约账号"
extra={
<Space wrap>
<Typography.Text type="secondary">{config.filePath || '默认配置'}</Typography.Text>
@@ -1624,44 +1909,114 @@ function CloudtentaclesPlatformPanel({
}
>
{config.sources.length === 0 ? (
<Empty description="暂无 cloudtentacles 账号" />
<Empty description="暂无 kuaishou-lewan 账号" />
) : (
<Space direction="vertical" className="full-width" size={12}>
{config.sources.map((source) => {
const session = config.sessions[source.key]
const active = source.key === selectedSourceKey
return (
<Card
key={source.key}
size="small"
className={active ? 'platform-source-card active' : 'platform-source-card'}
title={
<Space>
<Switch checked={source.enabled !== false} onChange={(enabled) => updateSource(source.key, { enabled })} />
<Button type="link" onClick={() => setSelectedSourceKey(source.key)}>
{source.label || source.username || source.key}
</Button>
<Tag color={session?.hasToken ? 'green' : 'orange'}>
{session?.hasToken ? '已登录' : '未登录'}
<div className="platform-split-layout">
<Card title="账号列表" size="small" className="platform-side-card">
<Space direction="vertical" className="full-width" size={8}>
{config.sources.map((source) => {
const session = config.sessions[source.key]
const active = source.key === selectedSourceKey
return (
<button
key={source.key}
type="button"
className={active ? 'platform-select-card active' : 'platform-select-card'}
onClick={() => setSelectedSourceKey(source.key)}
>
<span>
<strong>{formatCloudtentaclesSourceLabel(source)}</strong>
<small>{source.username || source.key}</small>
</span>
<Tag color={session?.hasToken ? 'green' : source.enabled === false ? 'default' : 'orange'}>
{session?.hasToken ? '已登录' : source.enabled === false ? '已停用' : '未登录'}
</Tag>
</Space>
}
extra={<Button danger icon={<DeleteOutlined />} loading={debugLoading === `delete-${source.key}`} onClick={() => deleteSource(source.key)}></Button>}
>
</button>
)
})}
</Space>
</Card>
<Card
size="small"
title={selectedSource ? `编辑账号:${formatCloudtentaclesSourceLabel(selectedSource)}` : '编辑账号'}
extra={
selectedSource ? (
<Button
danger
icon={<DeleteOutlined />}
loading={debugLoading === `delete-${selectedSource.key}`}
onClick={() => deleteSource(selectedSource.key)}
>
</Button>
) : null
}
>
{!selectedSource ? (
<Empty description="请选择左侧账号" />
) : (
<>
<div className="platform-form-grid">
<LabeledInput label="账号标识" value={source.key} onChange={(value) => updateSource(source.key, { key: value })} />
<LabeledInput label="显示名称" value={source.label} onChange={(value) => updateSource(source.key, { label: value })} />
<LabeledInput label="接口地址" value={source.baseUrl} onChange={(value) => updateSource(source.key, { baseUrl: value })} />
<LabeledInput label="用户名" value={source.username} onChange={(value) => updateSource(source.key, { username: value })} />
<LabeledInput label="密码" password value={source.password} onChange={(value) => updateSource(source.key, { password: value })} />
<LabeledInput label="手机号" value={source.phone} onChange={(value) => updateSource(source.key, { phone: value })} />
<LabeledInput label="设备 ID" value={source.deviceId} onChange={(value) => updateSource(source.key, { deviceId: value })} />
<NumberField label="设备类型" value={source.deviceType} min={0} onChange={(deviceType) => updateSource(source.key, { deviceType })} />
<FieldSwitch
label="启用账号"
checked={selectedSource.enabled !== false}
onChange={(enabled) => updateSource(selectedSource.key, { enabled })}
/>
<LabeledInput
label="账号标识"
value={selectedSource.key}
onChange={(value) => updateSource(selectedSource.key, { key: value })}
/>
<LabeledInput
label="显示名称"
value={selectedSource.label}
onChange={(value) => updateSource(selectedSource.key, { label: value })}
/>
<LabeledInput
label="接口地址"
value={selectedSource.baseUrl}
onChange={(value) => updateSource(selectedSource.key, { baseUrl: value })}
/>
<LabeledInput
label="用户名"
value={selectedSource.username}
onChange={(value) => updateSource(selectedSource.key, { username: value })}
/>
<LabeledInput
label="密码"
password
value={selectedSource.password}
onChange={(value) => updateSource(selectedSource.key, { password: value })}
/>
<LabeledInput
label="手机号"
value={selectedSource.phone}
onChange={(value) => updateSource(selectedSource.key, { phone: value })}
/>
<LabeledInput
label="设备 ID"
value={selectedSource.deviceId}
onChange={(value) => updateSource(selectedSource.key, { deviceId: value })}
/>
<NumberField
label="设备类型"
value={selectedSource.deviceType}
min={0}
onChange={(deviceType) => updateSource(selectedSource.key, { deviceType })}
/>
</div>
</Card>
)
})}
</Space>
<Alert
className="platform-section-gap"
type="info"
showIcon
message={`当前调试账号:${formatCloudtentaclesSourceLabel(selectedSource)}${selectedSource.key}`}
description="下方登录 / 查余额 / 查商品都只作用于当前选中账号。"
/>
</>
)}
</Card>
</div>
)}
</Card>
@@ -1673,8 +2028,12 @@ function CloudtentaclesPlatformPanel({
<Alert
type={selectedSession?.hasToken ? 'success' : 'info'}
showIcon
message={selectedSession?.hasToken ? '当前账号已有持久化会话' : '当前账号还没有可用会话'}
description={`登录时间:${formatAdminDateTime(selectedSession?.loggedInAt || '')}`}
message={
selectedSession?.hasToken
? `${formatCloudtentaclesSourceLabel(selectedSource)} 已有持久化会话`
: `${formatCloudtentaclesSourceLabel(selectedSource)} 还没有可用会话`
}
description={`sourceKey${selectedSource.key} · 登录时间:${formatAdminDateTime(selectedSession?.loggedInAt || '')}`}
/>
<Space wrap className="platform-section-gap">
<Input
@@ -1757,7 +2116,7 @@ function CloudtentaclesPlatformPanel({
})
}
>
</Button>
<Button
loading={debugLoading === 'sku'}
@@ -1785,6 +2144,10 @@ function CloudtentaclesPlatformPanel({
)
}
function formatCloudtentaclesSourceLabel(source: Pick<AdminCloudtentaclesSourceItem, 'label' | 'username' | 'key'>) {
return String(source.label || source.username || source.key || '').trim() || source.key
}
function resolveIndustryAccessTokenStatus(
source: Pick<AdminKuaishouIndustrySourceConfig, 'accessTokenStatus'>,
) {
+2 -2
View File
@@ -9,7 +9,7 @@ export const ADMIN_DEFAULT_PAGE_SIZE = 20
*/
export const ADMIN_PAGE_SIZE_OPTIONS = [10, 20, 50, 100] as const
/** 云触手发货记录:上游接口更适合大批量拉取 */
/** kuaishou-lewan 发货记录:上游接口更适合大批量拉取 */
export const CLOUDTENTACLES_DEFAULT_PAGE_SIZE = 100
export const CLOUDTENTACLES_PAGE_SIZE_OPTIONS = [100, 200, 500, 1000] as const
@@ -60,7 +60,7 @@ export function buildAdminLocalTablePagination(
}
}
/** 云触手发货记录分页(1001000 */
/** kuaishou-lewan 发货记录分页(1001000 */
export function buildCloudtentaclesTablePagination(options: {
current?: number
page?: number