import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, App as AntdApp, Button, Card, Empty, Select, Space, Statistic, Table, Tag, Typography, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { CloudSyncOutlined, KeyOutlined, ReloadOutlined } from '@ant-design/icons'; import { apiErrorMessage } from '@/services/api'; import { platformCatalogAdminApi, type AdminPlatform, type CatalogAttribute, type CatalogCategory, type CatalogRun, type SyncRequest, } from '@/services/platformCatalogAdmin'; import CatalogSourceModal from './components/CatalogSourceModal'; import DictionaryValuesDrawer from './components/DictionaryValuesDrawer'; import PlatformAttributesTable from './components/PlatformAttributesTable'; import PlatformCategoryTree from './components/PlatformCategoryTree'; import './SystemCatalogPage.css'; function dateTime(value: string | null) { return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '尚未同步'; } const statusLabels: Record = { queued: { label: '排队中', color: 'gold' }, running: { label: '同步中', color: 'blue' }, succeeded: { label: '成功', color: 'green' }, failed: { label: '失败', color: 'red' }, cancelled: { label: '已取消', color: 'default' }, }; const scopeLabels = { tree: '类目树', attributes: '类型属性', dictionary: '字典候选值' }; export default function SystemCatalogPage() { const { message } = AntdApp.useApp(); const [platforms, setPlatforms] = useState([]); const [platformId, setPlatformId] = useState(null); const [categories, setCategories] = useState([]); const [attributes, setAttributes] = useState([]); const [runs, setRuns] = useState([]); const [selectedCategoryId, setSelectedCategoryId] = useState(null); const [selectedAttribute, setSelectedAttribute] = useState(null); const [sourceOpen, setSourceOpen] = useState(false); const [loadingPlatforms, setLoadingPlatforms] = useState(false); const [loadingCategories, setLoadingCategories] = useState(false); const [loadingAttributes, setLoadingAttributes] = useState(false); const [syncingScope, setSyncingScope] = useState(null); const [refreshToken, setRefreshToken] = useState(0); const previousRunStatuses = useRef>(new Map()); const selectedCategoryIdRef = useRef(null); const currentPlatform = useMemo( () => platforms.find((item) => item.id === platformId) ?? null, [platforms, platformId], ); const selectedCategory = useMemo( () => categories.find((item) => item.id === selectedCategoryId) ?? null, [categories, selectedCategoryId], ); const loadPlatforms = useCallback(async () => { setLoadingPlatforms(true); try { const items = await platformCatalogAdminApi.platforms(); setPlatforms(items); setPlatformId((previous) => previous && items.some((item) => item.id === previous) ? previous : (items.find((item) => item.code.toUpperCase() === 'OZON')?.id ?? items[0]?.id ?? null)); } catch (error) { message.error(apiErrorMessage(error)); } finally { setLoadingPlatforms(false); } }, [message]); const loadCategories = useCallback(async (id: string) => { setLoadingCategories(true); try { setCategories(await platformCatalogAdminApi.categories(id)); } catch (error) { message.error(apiErrorMessage(error)); } finally { setLoadingCategories(false); } }, [message]); const loadAttributes = useCallback(async (id: string, categoryId: string) => { setLoadingAttributes(true); try { setAttributes(await platformCatalogAdminApi.attributes(id, categoryId)); } catch (error) { message.error(apiErrorMessage(error)); } finally { setLoadingAttributes(false); } }, [message]); const loadRuns = useCallback(async (id: string) => { try { const next = await platformCatalogAdminApi.runs(id); const completed = next.some((run) => run.status === 'succeeded' && previousRunStatuses.current.has(run.id) && previousRunStatuses.current.get(run.id) !== 'succeeded'); previousRunStatuses.current = new Map(next.map((run) => [run.id, run.status])); setRuns(next); if (completed) { setRefreshToken((value) => value + 1); void loadPlatforms(); void loadCategories(id); if (selectedCategoryIdRef.current) void loadAttributes(id, selectedCategoryIdRef.current); } } catch { // 轮询失败时保留最后一次任务列表,页面手动刷新仍可重试。 } }, [loadPlatforms, loadCategories, loadAttributes]); useEffect(() => { void loadPlatforms(); }, [loadPlatforms]); useEffect(() => { if (!platformId) return; setSelectedCategoryId(null); selectedCategoryIdRef.current = null; setSelectedAttribute(null); setCategories([]); setAttributes([]); previousRunStatuses.current.clear(); void loadCategories(platformId); void loadRuns(platformId); }, [platformId, loadCategories, loadRuns]); useEffect(() => { if (!platformId || !selectedCategoryId) return; void loadAttributes(platformId, selectedCategoryId); }, [platformId, selectedCategoryId, loadAttributes]); useEffect(() => { if (!platformId) return; const timer = window.setInterval(() => void loadRuns(platformId), 5000); return () => window.clearInterval(timer); }, [platformId, loadRuns]); const sync = async (input: SyncRequest) => { if (!platformId) return; setSyncingScope(input.scope); try { await platformCatalogAdminApi.sync(platformId, input); message.success('同步任务已排队'); await loadRuns(platformId); } catch (error) { message.error(apiErrorMessage(error)); } finally { setSyncingScope(null); } }; const saveSource = async (input: { client_id: string; api_key: string }) => { if (!platformId) return; try { const result = await platformCatalogAdminApi.configureSource(platformId, input); message.success(result.changed ? '同步来源已保存,类目树同步已排队' : '同步来源没有变化'); await loadPlatforms(); await loadRuns(platformId); } catch (error) { message.error(apiErrorMessage(error)); throw error; } }; const runColumns: ColumnsType = [ { title: '范围', dataIndex: 'scope', key: 'scope', render: (scope: CatalogRun['scope']) => scopeLabels[scope] }, { title: '触发', dataIndex: 'trigger', key: 'trigger', render: (value: string) => value === 'scheduled' ? '定时' : value === 'source_configured' ? '配置来源' : '手动' }, { title: '状态', dataIndex: 'status', key: 'status', render: (value: string) => {statusLabels[value]?.label ?? value} }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', render: dateTime }, { title: '结果', key: 'result', render: (_value, run) => run.error_message ?? (run.status === 'succeeded' ? Object.entries(run.result) .map(([key, value]) => `${key}: ${String(value)}`).join(' · ') : '—') }, ]; return
平台类目与属性 按平台查看商品类型、属性定义与候选值,并维护同步状态。