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