From 5856483cc81b53f81b66aff74ea39e6a95e29d32 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 7 Jul 2026 18:47:27 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=B9=B3=E5=8F=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend-react/src/App.tsx | 8 +- .../platform/AdminPlatformFulfillmentPage.tsx | 873 +++++++++ .../admin/platform/AdminPlatformShopsPage.tsx | 1677 +++++++++++++++++ apps/frontend-react/src/styles/main.css | 182 ++ 4 files changed, 2738 insertions(+), 2 deletions(-) create mode 100644 apps/frontend-react/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx create mode 100644 apps/frontend-react/src/pages/admin/platform/AdminPlatformShopsPage.tsx diff --git a/apps/frontend-react/src/App.tsx b/apps/frontend-react/src/App.tsx index 721e29d4..1e01f61e 100644 --- a/apps/frontend-react/src/App.tsx +++ b/apps/frontend-react/src/App.tsx @@ -10,6 +10,10 @@ const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage') const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage')) const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage')) const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage')) +const AdminPlatformFulfillmentPage = lazy( + () => import('@/pages/admin/platform/AdminPlatformFulfillmentPage'), +) +const AdminPlatformShopsPage = lazy(() => import('@/pages/admin/platform/AdminPlatformShopsPage')) const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage')) const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage')) const AdminUsersPage = lazy(() => import('@/pages/admin/AdminUsersPage')) @@ -59,8 +63,8 @@ export default function App() { } /> }> } /> - } /> - } /> + } /> + } /> } /> diff --git a/apps/frontend-react/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx b/apps/frontend-react/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx new file mode 100644 index 00000000..cd5b2525 --- /dev/null +++ b/apps/frontend-react/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx @@ -0,0 +1,873 @@ +import { + DeleteOutlined, + PlusOutlined, + ReloadOutlined, + SaveOutlined, + SearchOutlined, +} from '@ant-design/icons' +import { + Alert, + Button, + Card, + Empty, + Input, + InputNumber, + Select, + Space, + Spin, + Switch, + Table, + Tabs, + Tag, + Typography, +} from 'antd' +import type { TableColumnsType } from 'antd' +import { useEffect, useMemo, useState } from 'react' + +import PageHeader from '@/components/admin/PageHeader' +import { showError, showSuccess } from '@/lib/feedback' +import { + fetchAdminCloudtentaclesOverrideRules, + fetchAdminCloudtentaclesSkuList, + fetchAdminCloudtentaclesSourceConfig, + fetchAdminKuaishouFeifeiConfig, + matchAdminKuaishouFeifeiProduct, + saveAdminCloudtentaclesOverrideRules, + syncAdminKuaishouFeifeiProducts, +} from '@/services/admin' +import type { + AdminCloudtentaclesOverrideDeliveryItem, + AdminCloudtentaclesOverrideRule, + AdminCloudtentaclesSessionItem, + AdminCloudtentaclesSessionsMap, + AdminCloudtentaclesSkuItem, + AdminCloudtentaclesSourceItem, + AdminKuaishouFeifeiConfigResponse, + AdminKuaishouFeifeiMatchResult, + AdminKuaishouFeifeiProductRule, +} from '@/types/admin' +import { hasAdminRole } from '@/utils/admin-auth' + +type EditableOverrideRule = Omit & { + normalizedProductName?: string +} + +type SourceRow = AdminCloudtentaclesSourceItem & { + session: AdminCloudtentaclesSessionItem | null + ready: boolean +} + +export default function AdminPlatformFulfillmentPage() { + return ( +
+ + + {!hasAdminRole('admin') ? ( + + + + ) : ( + , + }, + { + key: 'kuaishou-cloud', + label: 'kuaishou-cloud', + children: , + }, + ]} + /> + )} +
+ ) +} + +function KuaishouFeifeiFulfillmentPanel() { + const [loading, setLoading] = useState(true) + const [syncing, setSyncing] = useState(false) + const [matching, setMatching] = useState(false) + const [errorMessage, setErrorMessage] = useState('') + const [filePath, setFilePath] = useState('') + const [rules, setRules] = useState([]) + const [matchInput, setMatchInput] = useState('') + const [matchResult, setMatchResult] = useState(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) { + setFilePath(data.filePath || '') + setRules( + Array.isArray(data.source.productRules) + ? data.source.productRules.map((rule) => normalizeFeifeiRule(rule)) + : [], + ) + } + + const columns: TableColumnsType = [ + { + title: 'feifei 商品名', + dataIndex: 'productName', + minWidth: 260, + render: (_, row) => ( +
+ + {row.productName || row.skuName || '-'} + + + {row.normalizedProductName || '-'} + +
+ ), + }, + { title: 'product_code', dataIndex: 'productCode', minWidth: 150 }, + { title: '展示名称', dataIndex: 'skuName', minWidth: 220 }, + { + title: '状态', + width: 100, + render: (_, row) => ( + + {row.enabled !== false ? '启用' : '停用'} + + ), + }, + { title: '来源', dataIndex: 'notes', minWidth: 180 }, + ] + + if (loading) { + return + } + + return ( +
+ {errorMessage ? : null} + +
+ + + + + + + + + +
+ + {lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。'}} + > + + setMatchInput(event.target.value)} + onPressEnter={previewMatch} + /> + + + + {matchResult ? ( + ${matchResult.match.productCode}` + : matchResult.normalizedProductName || matchResult.productName + } + /> + ) : null} + + + rowKey={(row) => row.id || `${row.productName}-${row.productCode}`} + columns={columns} + dataSource={rules} + pagination={{ pageSize: 20, showSizeChanger: true }} + scroll={{ x: 920, y: 520 }} + locale={{ emptyText: '暂无 kuaishou-feifei 商品映射' }} + className="platform-section-gap" + /> + +
+ ) +} + +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([]) + const [sessions, setSessions] = useState({}) + const [skuItems, setSkuItems] = useState([]) + const [overrideEnabled, setOverrideEnabled] = useState(true) + const [overrideRules, setOverrideRules] = useState([]) + 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 : '读取 cloudtentacles 履约配置失败') + } 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 : '读取 cloudtentacles 商品失败') + } 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 : '保存 cloudtentacles 覆盖规则失败' + 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) { + setOverrideRules((current) => + current.map((rule, ruleIndex) => (ruleIndex === index ? { ...rule, ...patch } : rule)), + ) + } + + function updateDeliveryItem(ruleIndex: number, itemIndex: number, patch: Partial) { + 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 + } + + return ( +
+ {errorMessage ? : null} + +
+ + + + + + + +
+ +
+ + {sourceRows.length === 0 ? ( + + ) : ( + + {sourceRows.map((source) => ( + + ))} + + )} + + +
+ + + 当前账号:{selectedSource ? formatSourceLabel(selectedSource) : '-'} + + + + } + > + {skuErrorMessage ? ( + + ) : null} + + + + setMatchInput(event.target.value)} + /> + + + {matchedSku ? ( + 命中 #{matchedSku.id} {matchedSku.name} + ) : matchInput.trim() ? ( + 未命中 + ) : null} + + + + + + + + + } + > + {overrideErrorMessage ? ( + + ) : null} + + {overrideRules.length === 0 ? ( + + ) : ( + + {overrideRules.map((rule, ruleIndex) => ( + + updateRule(ruleIndex, { enabled })} + /> + {rule.productName || '未命名规则'} + + } + extra={ + + } + > +
+ updateRule(ruleIndex, { productName: event.target.value })} + /> + ({ + label: formatSkuOption(sku), + value: sku.id, + }))} + onChange={(skuId) => handleDeliverySkuChange(ruleIndex, itemIndex, Number(skuId || 0))} + /> + + updateDeliveryItem(ruleIndex, itemIndex, { + cloudSkuName: event.target.value, + }) + } + /> + + updateDeliveryItem(ruleIndex, itemIndex, { + quantity: Math.max(1, Number(quantity || 1) || 1), + }) + } + /> + +
+ ))} + + + + updateRule(ruleIndex, { notes: event.target.value })} + /> +
+ ))} + + )} + + + + + rowKey="id" + loading={skuLoading} + dataSource={skuItems} + columns={[ + { + title: 'cloudtentacles 商品', + dataIndex: 'name', + minWidth: 260, + render: (_, row) => ( +
+ {row.name || '-'} + + #{row.id} · {row.description || row.name || '-'} + +
+ ), + }, + { + title: '91 自动匹配名', + dataIndex: 'name', + minWidth: 220, + render: (value) => {String(value || '-')}, + }, + { 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) => ( + 0 ? 'green' : 'red'}> + {Number(row.inventory || 0) > 0 ? '可履约' : '无库存'} + + ), + }, + ]} + pagination={{ pageSize: 20, showSizeChanger: true }} + scroll={{ x: 1000, y: 420 }} + locale={{ emptyText: '当前账号暂无可展示商品' }} + /> +
+
+
+
+ ) +} + +function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) { + return ( + + {label} + {value} + {detail ? {detail} : null} + + ) +} + +function normalizeFeifeiRule( + rule: Partial, +): 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) { + 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)}` +} diff --git a/apps/frontend-react/src/pages/admin/platform/AdminPlatformShopsPage.tsx b/apps/frontend-react/src/pages/admin/platform/AdminPlatformShopsPage.tsx new file mode 100644 index 00000000..3578a4b3 --- /dev/null +++ b/apps/frontend-react/src/pages/admin/platform/AdminPlatformShopsPage.tsx @@ -0,0 +1,1677 @@ +import { + CheckCircleOutlined, + DeleteOutlined, + PlusOutlined, + ReloadOutlined, + SaveOutlined, + SearchOutlined, + SendOutlined, +} from '@ant-design/icons' +import { + Alert, + Avatar, + Button, + Card, + Empty, + Input, + InputNumber, + Select, + Space, + Spin, + Switch, + Table, + Tabs, + Tag, + Typography, +} from 'antd' +import type { TableColumnsType } from 'antd' +import { useEffect, useMemo, useState } from 'react' +import { useSearchParams } from 'react-router' + +import PageHeader from '@/components/admin/PageHeader' +import { isFeedbackDismissed, showError, showPrompt, showSuccess } from '@/lib/feedback' +import { + deleteAdminCloudtentaclesSource, + failAdminNinetyoneOrder, + fetchAdminCloudtentaclesAsset, + fetchAdminCloudtentaclesSkuList, + fetchAdminCloudtentaclesSourceConfig, + fetchAdminKuaishouEticketSourceConfig, + fetchAdminKuaishouFeifeiConfig, + fetchAdminNinetyoneOrders, + fetchAdminNotificationConfig, + fetchAdminScheduledJobsConfig, + matchAdminKuaishouFeifeiProduct, + queryAdminKuaishouEticketDetail, + queryAdminKuaishouEticketShopInfo, + retryAdminNinetyoneOrder, + runAdminScheduledJob, + saveAdminCloudtentaclesSourceConfig, + saveAdminKuaishouEticketSourceConfig, + saveAdminKuaishouFeifeiConfig, + saveAdminNotificationConfig, + saveAdminScheduledJobsConfig, + sendAdminCloudtentaclesSmsCode, + syncAdminKuaishouFeifeiProducts, + testAdminCloudtentaclesLogin, + testAdminNotification, + validateAdminCloudtentaclesSession, +} from '@/services/admin' +import type { + AdminCloudtentaclesSessionsMap, + AdminCloudtentaclesSourceConfigResponse, + AdminCloudtentaclesSourceItem, + AdminKuaishouEticketDetailResult, + AdminKuaishouEticketShopConfigItem, + AdminKuaishouEticketShopInfoResult, + AdminKuaishouEticketSourceConfig, + AdminKuaishouFeifeiConfig, + AdminKuaishouFeifeiConfigResponse, + AdminKuaishouFeifeiMatchResult, + AdminKuaishouFeifeiProductRule, + AdminNinetyoneOrderItem, + AdminNinetyoneOrderListResult, + AdminNotificationConfig, + AdminNotificationTestResult, + AdminScheduledJobItem, + AdminScheduledJobRuntimeState, + AdminScheduledJobsConfig, +} from '@/types/admin' +import { hasAdminRole } from '@/utils/admin-auth' +import { formatAdminDateTime } from '@/utils/admin-time' + +type PlatformTab = + | 'ninetyone' + | 'notifications' + | 'kuaishouEticket' + | 'kuaishouFeifei' + | 'cloudtentacles' + +type NotificationConfigResponse = { + filePath: string + source: AdminNotificationConfig +} + +type ScheduledJobsState = { + filePath: string + source: AdminScheduledJobsConfig + runtime: AdminScheduledJobRuntimeState[] + cloudtentaclesAccounts: Array<{ + sourceKey: string + label: string + enabled: boolean + username: string + phoneMasked: string + hasToken: boolean + loggedInAt: string + }> +} + +const platformTabs: PlatformTab[] = [ + 'ninetyone', + 'notifications', + 'kuaishouEticket', + 'kuaishouFeifei', + 'cloudtentacles', +] + +export default function AdminPlatformShopsPage() { + const [searchParams, setSearchParams] = useSearchParams() + const activeTab = normalizePlatformTab(searchParams.get('tab')) + const [loading, setLoading] = useState(true) + const [errorMessage, setErrorMessage] = useState('') + const [notificationConfig, setNotificationConfig] = useState(null) + const [scheduledJobs, setScheduledJobs] = useState(null) + const [eticketConfig, setEticketConfig] = useState<{ + filePath: string + source: AdminKuaishouEticketSourceConfig + } | null>(null) + const [feifeiConfig, setFeifeiConfig] = useState(null) + const [cloudtentaclesConfig, setCloudtentaclesConfig] = + useState(null) + const [ninetyoneData, setNinetyoneData] = useState(null) + + const overviewCards = useMemo( + () => + buildOverviewCards({ + notificationConfig, + scheduledJobs, + eticketConfig, + feifeiConfig, + cloudtentaclesConfig, + ninetyoneData, + }), + [ + cloudtentaclesConfig, + eticketConfig, + feifeiConfig, + ninetyoneData, + notificationConfig, + scheduledJobs, + ], + ) + const activeOverview = + overviewCards.find((card) => card.key === activeTab) || overviewCards[0] + + useEffect(() => { + void loadConfigs() + }, []) + + async function loadConfigs() { + if (!hasAdminRole('admin')) { + setLoading(false) + return + } + + setLoading(true) + setErrorMessage('') + + try { + const [ + notificationResponse, + scheduledJobsResponse, + eticketResponse, + feifeiResponse, + cloudtentaclesResponse, + ninetyoneResponse, + ] = await Promise.all([ + fetchAdminNotificationConfig(), + fetchAdminScheduledJobsConfig(), + fetchAdminKuaishouEticketSourceConfig(), + fetchAdminKuaishouFeifeiConfig(), + fetchAdminCloudtentaclesSourceConfig(), + fetchAdminNinetyoneOrders({ page: 1, pageSize: 20 }), + ]) + + setNotificationConfig(notificationResponse.data) + setScheduledJobs(scheduledJobsResponse.data) + setEticketConfig(eticketResponse.data) + setFeifeiConfig(feifeiResponse.data) + setCloudtentaclesConfig(cloudtentaclesResponse.data) + setNinetyoneData(ninetyoneResponse.data) + } catch (error) { + setErrorMessage(error instanceof Error ? error.message : '读取平台配置失败') + } finally { + setLoading(false) + } + } + + function updateActiveTab(key: string) { + setSearchParams({ tab: normalizePlatformTab(key) }) + } + + return ( +
+ } loading={loading} onClick={loadConfigs}> + 刷新全部 + + } + /> + + {!hasAdminRole('admin') ? ( + + + + ) : ( + <> + {errorMessage ? : null} + {loading ? ( + + + + ) : ( + <> + +
+
{activeOverview.tag}
+ {activeOverview.label} +

{activeOverview.meta}

+
+ + + {activeOverview.status} + +
+ + + ), + }, + { + key: 'notifications', + label: '内部通知', + children: notificationConfig && scheduledJobs ? ( + + ) : ( + + ), + }, + { + key: 'kuaishouEticket', + label: '快手小店核销', + children: eticketConfig ? ( + + ) : ( + + ), + }, + { + key: 'kuaishouFeifei', + label: 'kuaishou-feifei', + children: feifeiConfig ? ( + + ) : ( + + ), + }, + { + key: 'cloudtentacles', + label: 'cloudtentacles', + children: cloudtentaclesConfig ? ( + + ) : ( + + ), + }, + ]} + /> + + )} + + )} +
+ ) +} + +function NinetyonePanel({ + data, + onDataChange, +}: { + data: AdminNinetyoneOrderListResult | null + onDataChange: (data: AdminNinetyoneOrderListResult) => void +}) { + const [loading, setLoading] = useState(false) + const [actionLoadingId, setActionLoadingId] = useState('') + const [status, setStatus] = useState('') + const page = data?.page || 1 + const pageSize = data?.pageSize || 20 + + async function loadOrders(nextPage = page, nextPageSize = pageSize, nextStatus = status) { + setLoading(true) + try { + const response = await fetchAdminNinetyoneOrders({ + page: nextPage, + pageSize: nextPageSize, + status: nextStatus || undefined, + }) + onDataChange(response.data) + } catch (error) { + showError(error instanceof Error ? error.message : '读取 91 卡券订单失败') + } finally { + setLoading(false) + } + } + + async function retryOrder(order: AdminNinetyoneOrderItem) { + setActionLoadingId(`retry-${order.orderId}`) + try { + await retryAdminNinetyoneOrder(order.orderId) + showSuccess('已重新处理 91 卡券订单') + await loadOrders() + } catch (error) { + showError(error instanceof Error ? error.message : '重试订单失败') + } finally { + setActionLoadingId('') + } + } + + async function failOrder(order: AdminNinetyoneOrderItem) { + try { + const result = await showPrompt('请输入失败原因', '标记订单失败') + setActionLoadingId(`fail-${order.orderId}`) + await failAdminNinetyoneOrder(order.orderId, { reason: result.value }) + showSuccess('已标记 91 卡券订单失败') + await loadOrders() + } catch (error) { + if (!isFeedbackDismissed(error)) { + showError(error instanceof Error ? error.message : '标记失败订单失败') + } + } finally { + setActionLoadingId('') + } + } + + const columns: TableColumnsType = [ + { + title: '订单', + dataIndex: 'orderNo', + minWidth: 220, + render: (_, row) => ( +
+ {row.orderNo || '-'} + {row.outTradeNo || '-'} +
+ ), + }, + { + title: '商品', + dataIndex: 'productName', + minWidth: 260, + render: (_, row) => ( +
+ {row.productName || '-'} + {row.productNo || '-'} · 数量 {row.buyNum || 0} +
+ ), + }, + { + title: '店铺', + minWidth: 160, + render: (_, row) => row.shopName || row.shopId || '-', + }, + { + title: '状态', + minWidth: 160, + render: (_, row) => ( + + {row.orderStatus || '-'} + {row.payStatus || '-'} + + ), + }, + { + title: '任务', + dataIndex: 'taskCount', + width: 110, + render: (value) => `${Number(value || 0)} 个`, + }, + { + title: '时间', + dataIndex: 'createdAt', + width: 180, + render: (value) => formatAdminDateTime(value), + }, + { + title: '操作', + fixed: 'right', + width: 180, + render: (_, row) => ( + + + + + ), + }, + ] + + return ( + + + updateNotification({ + ...source, + channels: { + ...source.channels, + bark: { ...source.channels.bark, serverUrl: event.target.value }, + }, + }) + } + /> + + + + updateNotification({ + ...source, + channels: { + ...source.channels, + bark: { ...source.channels.bark, recipients }, + }, + }) + } + /> + + + updateNotification({ + ...source, + channels: { + ...source.channels, + wpush: { ...source.channels.wpush, recipients }, + }, + }) + } + /> + + {testResult ? ( + 0 ? 'warning' : 'success'} + showIcon + message={`测试结果:成功 ${testResult.successCount},失败 ${testResult.failedCount},跳过 ${testResult.skippedCount}`} + /> + ) : null} + + + + {scheduledJobs.filePath || '默认配置'} + + + } + > + updateJobs({ ...scheduledJobs.source, enabled })} + /> + + + {scheduledJobs.source.jobs.map((job, index) => ( + item.id === job.id) || null} + running={runningJobId === job.id} + onRun={() => runJob(job.id)} + onChange={(nextJob) => + updateJobs({ + ...scheduledJobs.source, + jobs: scheduledJobs.source.jobs.map((item, jobIndex) => + jobIndex === index ? nextJob : item, + ), + }) + } + /> + ))} + + + + ) +} + +function RecipientList({ + title, + secretField, + items, + onChange, +}: { + title: string + secretField: 'deviceKey' | 'apiKey' + items: Array>> + onChange: (items: Array>>) => void +}) { + function addRecipient() { + onChange([ + ...items, + { + id: `recipient_${Date.now()}`, + name: '', + enabled: true, + [secretField]: '', + } as T & Partial>, + ]) + } + + function updateRecipient(index: number, patch: Partial>) { + onChange(items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item))) + } + + return ( + } onClick={addRecipient}>新增}> + {items.length === 0 ? ( + + ) : ( + + {items.map((item, index) => ( +
+ updateRecipient(index, { enabled } as Partial>)} + /> + updateRecipient(index, { name: event.target.value } as Partial>)} + /> + + updateRecipient(index, { + [secretField]: event.target.value, + } as Partial>) + } + /> +
+ ))} +
+ )} +
+ ) +} + +function ScheduledJobCard({ + job, + runtime, + running, + onRun, + onChange, +}: { + job: AdminScheduledJobItem + runtime: AdminScheduledJobRuntimeState | null + running: boolean + onRun: () => void + onChange: (job: AdminScheduledJobItem) => void +}) { + return ( + + onChange({ ...job, enabled })} /> + {job.id} + + } + extra={} + > +
+ onChange({ ...job, intervalSeconds })} + /> + onChange({ ...job, cooldownSeconds })} + /> + + onChange({ ...job, config: { ...job.config, assetThreshold } }) + } + /> +
+ + {runtime ? ( + + ) : null} +
+ ) +} + +function KuaishouEticketPanel({ + config, + onChange, +}: { + config: { filePath: string; source: AdminKuaishouEticketSourceConfig } + onChange: (config: { filePath: string; source: AdminKuaishouEticketSourceConfig }) => void +}) { + const [saving, setSaving] = useState(false) + const [querying, setQuerying] = useState(false) + const [selectedShopId, setSelectedShopId] = useState(config.source.shops[0]?.shopId || '') + const [ticketCode, setTicketCode] = useState('') + const [shopInfoResult, setShopInfoResult] = useState(null) + const [detailResult, setDetailResult] = useState(null) + const source = config.source + const selectedShop = source.shops.find((shop) => shop.shopId === selectedShopId) || source.shops[0] || null + + async function saveConfig() { + setSaving(true) + try { + const response = await saveAdminKuaishouEticketSourceConfig(source) + onChange(response.data) + showSuccess('快手小店核销配置已保存') + } catch (error) { + showError(error instanceof Error ? error.message : '保存快手小店核销配置失败') + } finally { + setSaving(false) + } + } + + async function queryShopInfo(shop = selectedShop) { + if (!shop?.cookie) { + showError('请先选择并填写店铺 Cookie') + return + } + + setQuerying(true) + try { + const response = await queryAdminKuaishouEticketShopInfo({ + baseUrl: source.baseUrl, + shopId: shop.shopId, + cookie: shop.cookie, + }) + setShopInfoResult(response.data) + updateShop(shop.shopId, { + shopId: response.data.shop.shopId || shop.shopId, + kshopName: response.data.shop.kshopName || shop.kshopName, + userAvatar: response.data.shop.userAvatar || shop.userAvatar, + }) + showSuccess(`已识别店铺:${response.data.shop.kshopName || response.data.shop.shopId}`) + } catch (error) { + showError(error instanceof Error ? error.message : '读取快手小店店铺信息失败') + } finally { + setQuerying(false) + } + } + + async function queryDetail() { + if (!selectedShop?.cookie) { + showError('请先选择并填写店铺 Cookie') + return + } + if (!ticketCode.trim()) { + showError('请输入查询券码') + return + } + + setQuerying(true) + try { + const response = await queryAdminKuaishouEticketDetail({ + baseUrl: source.baseUrl, + shopId: selectedShop.shopId, + cookie: selectedShop.cookie, + eTicketId: ticketCode.trim(), + }) + setDetailResult(response.data) + showSuccess('核销券详情查询完成') + } catch (error) { + showError(error instanceof Error ? error.message : '查询核销券详情失败') + } finally { + setQuerying(false) + } + } + + function updateSource(nextSource: AdminKuaishouEticketSourceConfig) { + onChange({ ...config, source: nextSource }) + } + + function updateShop(shopId: string, patch: Partial) { + updateSource({ + ...source, + shops: source.shops.map((shop) => (shop.shopId === shopId ? { ...shop, ...patch } : shop)), + }) + } + + function addShop() { + const shopId = `shop_${Date.now()}` + updateSource({ + ...source, + shops: [ + ...source.shops, + { + shopId, + kshopName: '', + cookie: '', + cookieMasked: '', + hasCookie: false, + userAvatar: '', + enabled: true, + }, + ], + }) + setSelectedShopId(shopId) + } + + return ( +
+ + {config.filePath || '默认配置'} + + + } + > +
+ updateSource({ ...source, enabled })} + /> +
+ 接口地址 + updateSource({ ...source, baseUrl: event.target.value })} + /> +
+
+ + } onClick={addShop}>新增店铺}> + {source.shops.length === 0 ? ( + + ) : ( + + {source.shops.map((shop) => ( +
+ updateShop(shop.shopId, { enabled })} /> + {shop.kshopName?.slice(0, 1) || '店'} + updateShop(shop.shopId, { shopId: event.target.value })} + onFocus={() => setSelectedShopId(shop.shopId)} + /> + updateShop(shop.shopId, { kshopName: event.target.value })} + /> + + updateShop(shop.shopId, { + cookie: event.target.value, + hasCookie: Boolean(event.target.value.trim()), + }) + } + /> + +
+ ))} +
+ )} +
+
+ + + + setTicketCode(event.target.value)} + onPressEnter={queryDetail} + /> + + + + {shopInfoResult || detailResult ? ( +
+            {JSON.stringify(detailResult || shopInfoResult, null, 2)}
+          
+ ) : null} +
+
+ ) +} + +function KuaishouFeifeiPlatformPanel({ + config, + onChange, +}: { + config: AdminKuaishouFeifeiConfigResponse + onChange: (config: AdminKuaishouFeifeiConfigResponse) => void +}) { + const [saving, setSaving] = useState(false) + const [syncing, setSyncing] = useState(false) + const [matching, setMatching] = useState(false) + const [matchInput, setMatchInput] = useState('') + const [matchResult, setMatchResult] = useState(null) + const source = config.source + const enabledRules = source.productRules.filter((rule) => rule.enabled !== false) + + async function saveConfig() { + setSaving(true) + try { + const response = await saveAdminKuaishouFeifeiConfig(source) + onChange(response.data) + showSuccess('kuaishou-feifei 平台配置已保存') + } catch (error) { + showError(error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败') + } finally { + setSaving(false) + } + } + + async function syncProducts() { + setSyncing(true) + try { + const response = await syncAdminKuaishouFeifeiProducts({ status: 'on_sale' }) + onChange(response.data) + showSuccess(`商品映射已同步:商品 ${response.data.sync.productCount} 个,规则 ${response.data.sync.ruleCount} 条`) + } catch (error) { + showError(error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败') + } finally { + setSyncing(false) + } + } + + async function previewMatch() { + if (!matchInput.trim()) { + showError('请输入 91 商品名') + return + } + + setMatching(true) + try { + const response = await matchAdminKuaishouFeifeiProduct(matchInput.trim()) + setMatchResult(response.data) + } catch (error) { + showError(error instanceof Error ? error.message : '预览映射失败') + } finally { + setMatching(false) + } + } + + function updateSource(patch: Partial) { + onChange({ ...config, source: { ...source, ...patch } }) + } + + function updateRule(index: number, patch: Partial) { + updateSource({ + productRules: source.productRules.map((rule, ruleIndex) => + ruleIndex === index ? { ...rule, ...patch } : rule, + ), + }) + } + + return ( +
+
+ + + + +
+ + + + + + } + > +
+ updateSource({ enabled })} /> +
+ 接口地址 + updateSource({ baseUrl: event.target.value })} /> +
+
+ App Key + updateSource({ appKey: event.target.value })} /> +
+
+ App Secret + updateSource({ appSecret: event.target.value })} /> +
+ updateSource({ timeoutMs })} /> +
+ 通知地址 + updateSource({ notifyUrl: event.target.value })} /> +
+
+
+ + + + setMatchInput(event.target.value)} onPressEnter={previewMatch} /> + + + {matchResult ? ( + ${matchResult.match.productCode}` : matchResult.normalizedProductName} + /> + ) : null} + + + className="platform-section-gap" + rowKey={(row) => row.id || row.productCode || row.productName} + dataSource={source.productRules} + pagination={{ pageSize: 10, showSizeChanger: true }} + scroll={{ x: 980 }} + columns={[ + { + title: '启用', + width: 80, + render: (_, row, index) => ( + updateRule(index, { enabled })} /> + ), + }, + { + title: '商品名', + minWidth: 260, + render: (_, row, index) => ( + updateRule(index, { productName: event.target.value })} /> + ), + }, + { + title: 'product_code', + minWidth: 160, + render: (_, row, index) => ( + updateRule(index, { productCode: event.target.value })} /> + ), + }, + { + title: '展示名', + minWidth: 220, + render: (_, row, index) => ( + updateRule(index, { skuName: event.target.value })} /> + ), + }, + { + title: '备注', + minWidth: 180, + render: (_, row, index) => ( + updateRule(index, { notes: event.target.value })} /> + ), + }, + ]} + /> + +
+ ) +} + +function CloudtentaclesPlatformPanel({ + config, + onChange, +}: { + config: AdminCloudtentaclesSourceConfigResponse + onChange: (config: AdminCloudtentaclesSourceConfigResponse) => void +}) { + const [saving, setSaving] = useState(false) + const [debugLoading, setDebugLoading] = useState('') + const [selectedSourceKey, setSelectedSourceKey] = useState(config.sources[0]?.key || '') + const [smsCode, setSmsCode] = useState('') + const [debugResult, setDebugResult] = useState(null) + const selectedSource = config.sources.find((source) => source.key === selectedSourceKey) || config.sources[0] || null + const selectedSession = selectedSource ? config.sessions[selectedSource.key] : null + + async function saveConfig() { + setSaving(true) + try { + const response = await saveAdminCloudtentaclesSourceConfig({ + enabled: config.enabled, + sources: config.sources, + }) + onChange(response.data) + showSuccess('cloudtentacles 账号配置已保存') + } catch (error) { + showError(error instanceof Error ? error.message : '保存 cloudtentacles 账号失败') + } finally { + setSaving(false) + } + } + + async function deleteSource(sourceKey: string) { + setDebugLoading(`delete-${sourceKey}`) + try { + await deleteAdminCloudtentaclesSource(sourceKey) + const response = await fetchAdminCloudtentaclesSourceConfig() + onChange(response.data) + setSelectedSourceKey(response.data.sources[0]?.key || '') + showSuccess('cloudtentacles 账号已删除') + } catch (error) { + showError(error instanceof Error ? error.message : '删除 cloudtentacles 账号失败') + } finally { + setDebugLoading('') + } + } + + async function runDebug(action: string, handler: () => Promise) { + setDebugLoading(action) + setDebugResult(null) + try { + const result = await handler() + setDebugResult(result) + showSuccess('cloudtentacles 调试完成') + } catch (error) { + showError(error instanceof Error ? error.message : 'cloudtentacles 调试失败') + } finally { + setDebugLoading('') + } + } + + function updateConfig(next: Partial) { + onChange({ ...config, ...next }) + } + + function updateSource(sourceKey: string, patch: Partial) { + updateConfig({ + sources: config.sources.map((source) => + source.key === sourceKey ? { ...source, ...patch } : source, + ), + }) + } + + function addSource() { + const key = `account_${Date.now().toString(36)}` + updateConfig({ + sources: [ + ...config.sources, + { + key, + label: '新履约账号', + enabled: true, + baseUrl: 'https://123.207.217.176', + username: '', + password: '', + phone: '', + deviceId: '1', + deviceType: 2, + }, + ], + }) + setSelectedSourceKey(key) + } + + return ( +
+ + {config.filePath || '默认配置'} + updateConfig({ enabled })} /> + + + + } + > + {config.sources.length === 0 ? ( + + ) : ( + + {config.sources.map((source) => { + const session = config.sessions[source.key] + const active = source.key === selectedSourceKey + return ( + + updateSource(source.key, { enabled })} /> + + + {session?.hasToken ? '已登录' : '未登录'} + + + } + extra={} + > +
+ updateSource(source.key, { key: value })} /> + updateSource(source.key, { label: value })} /> + updateSource(source.key, { baseUrl: value })} /> + updateSource(source.key, { username: value })} /> + updateSource(source.key, { password: value })} /> + updateSource(source.key, { phone: value })} /> + updateSource(source.key, { deviceId: value })} /> + updateSource(source.key, { deviceType })} /> +
+
+ ) + })} + + )} + + + + {!selectedSource ? ( + + ) : ( + <> + + + setSmsCode(event.target.value)} + /> + + + + + + + {debugResult ?
{JSON.stringify(debugResult, null, 2)}
: null} + + )} +
+
+ ) +} + +function MetricCard({ label, value, detail }: { label: string; value: string; detail?: string }) { + return ( + + {label} + {value} + {detail ? {detail} : null} + + ) +} + +function FieldSwitch({ + label, + checked, + onChange, +}: { + label: string + checked: boolean + onChange: (checked: boolean) => void +}) { + return ( +
+ {label} + +
+ ) +} + +function NumberField({ + label, + value, + min, + onChange, +}: { + label: string + value: number + min?: number + onChange: (value: number) => void +}) { + return ( +
+ {label} + onChange(Number(nextValue || 0))} + /> +
+ ) +} + +function LabeledInput({ + label, + value, + password, + onChange, +}: { + label: string + value: string + password?: boolean + onChange: (value: string) => void +}) { + const InputComponent = password ? Input.Password : Input + return ( +
+ {label} + onChange(event.target.value)} /> +
+ ) +} + +function normalizePlatformTab(value: unknown): PlatformTab { + return typeof value === 'string' && platformTabs.includes(value as PlatformTab) + ? (value as PlatformTab) + : 'ninetyone' +} + +function buildOverviewCards({ + notificationConfig, + scheduledJobs, + eticketConfig, + feifeiConfig, + cloudtentaclesConfig, + ninetyoneData, +}: { + notificationConfig: NotificationConfigResponse | null + scheduledJobs: ScheduledJobsState | null + eticketConfig: { filePath: string; source: AdminKuaishouEticketSourceConfig } | null + feifeiConfig: AdminKuaishouFeifeiConfigResponse | null + cloudtentaclesConfig: AdminCloudtentaclesSourceConfigResponse | null + ninetyoneData: AdminNinetyoneOrderListResult | null +}) { + const barkRecipients = notificationConfig?.source.channels.bark.recipients || [] + const wpushRecipients = notificationConfig?.source.channels.wpush.recipients || [] + const eticketShops = eticketConfig?.source.shops || [] + const feifeiRules = feifeiConfig?.source.productRules || [] + const cloudSources = cloudtentaclesConfig?.sources || [] + const cloudSessions: AdminCloudtentaclesSessionsMap = cloudtentaclesConfig?.sessions || {} + const cloudReadyCount = cloudSources.filter((source) => source.enabled !== false && cloudSessions[source.key]?.hasToken).length + + return [ + { + key: 'notifications', + label: '内部通知', + tag: '通知与监控', + value: barkRecipients.filter((item) => item.enabled !== false).length + wpushRecipients.filter((item) => item.enabled !== false).length, + unit: '启用接收人', + meta: `Bark ${barkRecipients.length} 人 · WPush ${wpushRecipients.length} 人 · 任务 ${scheduledJobs?.source.jobs.length || 0} 个`, + status: notificationConfig?.source.enabled ? '已启用' : '待配置', + statusColor: notificationConfig?.source.enabled ? 'green' : 'orange', + }, + { + key: 'ninetyone', + label: '91卡券', + tag: '来源订单', + value: ninetyoneData?.total || 0, + unit: '当前订单', + meta: `当前页 ${ninetyoneData?.items.length || 0} 条 · 用于补全订单和生成任务`, + status: (ninetyoneData?.total || 0) > 0 ? '可查看' : '正常', + statusColor: 'blue', + }, + { + key: 'kuaishouEticket', + label: '快手小店核销', + tag: '店铺凭据', + value: eticketShops.length, + unit: '已配置店铺', + meta: `Cookie 就绪 ${eticketShops.filter((shop) => shop.hasCookie || shop.cookie).length} 家`, + status: eticketShops.some((shop) => shop.hasCookie || shop.cookie) ? '已就绪' : '待配置', + statusColor: eticketShops.some((shop) => shop.hasCookie || shop.cookie) ? 'green' : 'orange', + }, + { + key: 'kuaishouFeifei', + label: 'kuaishou-feifei', + tag: '补充发货平台', + value: feifeiRules.filter((rule) => rule.enabled !== false).length, + unit: '启用规则', + meta: `API ${feifeiConfig?.effective.hasAppKey && feifeiConfig.effective.hasAppSecret ? '已配置' : '待补全'} · 总规则 ${feifeiRules.length} 条`, + status: feifeiConfig?.effective.enabled ? '已启用' : '待配置', + statusColor: feifeiConfig?.effective.enabled ? 'green' : 'orange', + }, + { + key: 'cloudtentacles', + label: 'cloudtentacles', + tag: '履约账号', + value: `${cloudReadyCount} / ${cloudSources.length}`, + unit: '可用账号', + meta: `会话文件:${cloudtentaclesConfig?.sessionFilePath || '-'}`, + status: cloudReadyCount > 0 ? '已验证' : '待验证', + statusColor: cloudReadyCount > 0 ? 'green' : 'orange', + }, + ] as const +} diff --git a/apps/frontend-react/src/styles/main.css b/apps/frontend-react/src/styles/main.css index 05127afd..4351b551 100644 --- a/apps/frontend-react/src/styles/main.css +++ b/apps/frontend-react/src/styles/main.css @@ -569,6 +569,166 @@ select { background: rgba(248, 250, 252, 0.72); } +.platform-tabs { + min-width: 0; +} + +.platform-panel-stack { + display: grid; + gap: 16px; + min-width: 0; +} + +.platform-active-summary .ant-card-body { + display: flex; + justify-content: space-between; + gap: 18px; + align-items: center; +} + +.platform-active-summary h3 { + margin: 4px 0 0 !important; +} + +.platform-active-summary p { + margin: 8px 0 0; + color: #6b7280; +} + +.summary-kicker { + color: #1677ff; + font-size: 12px; + font-weight: 800; +} + +.metric-grid { + display: grid; + gap: 12px; +} + +.metric-grid.four { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.metric-grid.five { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.metric-card .ant-card-body, +.metric-card { + min-width: 0; +} + +.metric-card .ant-card-body { + display: grid; + gap: 5px; +} + +.metric-card span, +.metric-card small { + color: #6b7280; +} + +.metric-card strong { + color: #111827; + font-size: 24px; + line-height: 1.1; +} + +.platform-section-gap { + margin-top: 14px; +} + +.platform-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + align-items: end; +} + +.field-switch { + min-height: 54px; + padding: 10px 12px; + border: 1px solid #edf0f5; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.field-switch span { + color: #4b5563; + font-weight: 600; +} + +.platform-inline-row, +.platform-shop-row, +.platform-rule-head, +.platform-delivery-row { + display: grid; + gap: 10px; + align-items: center; + min-width: 0; +} + +.platform-inline-row { + grid-template-columns: auto minmax(120px, 0.8fr) minmax(180px, 1.2fr) auto; +} + +.platform-shop-row { + grid-template-columns: auto auto minmax(120px, 0.9fr) minmax(140px, 1fr) minmax(220px, 1.5fr) auto auto; +} + +.platform-rule-head { + grid-template-columns: minmax(220px, 1fr) minmax(180px, 0.8fr); +} + +.platform-delivery-row { + grid-template-columns: minmax(220px, 1.2fr) minmax(180px, 1fr) 110px auto; +} + +.platform-split-layout { + display: grid; + grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.platform-side-card { + position: sticky; + top: 82px; +} + +.platform-select-card { + width: 100%; + padding: 10px 12px; + border: 1px solid #edf0f5; + border-radius: 6px; + background: #ffffff; + color: inherit; + cursor: pointer; + display: flex; + justify-content: space-between; + gap: 12px; + text-align: left; +} + +.platform-select-card.active, +.platform-source-card.active { + border-color: #1677ff; + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.08); +} + +.platform-select-card span { + display: grid; + gap: 4px; +} + +.platform-select-card small { + color: #6b7280; +} + @media (max-width: 900px) { .admin-sider { position: fixed !important; @@ -589,6 +749,28 @@ select { .page-header { display: grid; } + + .metric-grid.four, + .metric-grid.five, + .platform-form-grid, + .platform-split-layout { + grid-template-columns: 1fr; + } + + .platform-side-card { + position: static; + } + + .platform-inline-row, + .platform-shop-row, + .platform-rule-head, + .platform-delivery-row { + grid-template-columns: 1fr; + } + + .platform-active-summary .ant-card-body { + display: grid; + } } @media (max-width: 720px) {