import { useCallback, useEffect, useMemo, useState } from 'react'; import { App as AntdApp, Button, Card, Dropdown, Empty, Form, Input, Modal, Select, Space, Switch, Table, Tag, Tooltip, Typography, } from 'antd'; import type { MenuProps, TableProps } from 'antd'; import { ApiOutlined, CloudDownloadOutlined, DeleteOutlined, MoreOutlined, PlusOutlined, ReloadOutlined, WarningFilled, } from '@ant-design/icons'; import axios from 'axios'; import type { Store } from '@atlas/contracts'; import { apiErrorCode, apiErrorMessage } from '@/services/api'; import { storesApi } from '@/services/stores'; import { usePlatform } from '@/services/platform'; import './ShopsPage.css'; const STORE_STATUS_MAP: Record = { verifying: { color: 'gold', text: '验证中' }, active: { color: 'green', text: '正常' }, credential_invalid: { color: 'red', text: '凭据无效' }, sync_error: { color: 'orange', text: '同步异常' }, disabled: { color: 'default', text: '已停用' }, deleted: { color: 'default', text: '已删除' }, }; const CREDENTIAL_STATUS_MAP: Record = { unknown: { color: 'default', text: '未验证' }, valid: { color: 'green', text: '凭据有效' }, invalid: { color: 'red', text: '凭据无效' }, expired: { color: 'red', text: '已过期' }, rotation_required: { color: 'gold', text: '待轮换' }, }; function formatDateTime(value: string | null): string { if (!value) return '—'; const date = new Date(value); return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString('zh-CN', { hour12: false }); } interface CreateFormValues { name: string; client_id: string; api_key: string; currency_code: string; is_default: boolean; note?: string; } interface EditFormValues { name: string; currency_code: string; note?: string; api_key?: string; } /** * 店铺管理页,契约见 docs/api/stores-and-platforms.md。 * 列表按页头当前平台过滤(platform_id 查询参数);创建与密钥替换为异步验证, * Worker 消费验证操作前店铺会停留在「验证中」状态。 */ const ShopsPage = () => { const { message, modal } = AntdApp.useApp(); const { currentPlatform, platforms, error: platformError, reload, refreshContext, } = usePlatform(); const [stores, setStores] = useState([]); const [nextCursor, setNextCursor] = useState(null); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [pendingStoreId, setPendingStoreId] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [creating, setCreating] = useState(false); const [createForm] = Form.useForm(); const [editingStore, setEditingStore] = useState(null); const [saving, setSaving] = useState(false); const [editForm] = Form.useForm(); const platformId = currentPlatform?.id ?? null; const currencyOptions = useMemo( () => (currentPlatform?.supported_currencies ?? []).map((code) => ({ value: code, label: code, })), [currentPlatform] ); const loadStores = useCallback( async (targetPlatformId: string) => { setLoading(true); try { const result = await storesApi.list(targetPlatformId); setStores(result.data); setNextCursor(result.meta.next_cursor); } catch (err) { message.error(apiErrorMessage(err)); } finally { setLoading(false); } }, [message] ); useEffect(() => { if (!platformId) return; void loadStores(platformId); }, [platformId, loadStores]); const loadMore = async () => { if (!platformId || !nextCursor) return; setLoadingMore(true); try { const result = await storesApi.list(platformId, nextCursor); setStores((prev) => [...prev, ...result.data]); setNextCursor(result.meta.next_cursor); } catch (err) { message.error(apiErrorMessage(err)); } finally { setLoadingMore(false); } }; /** 任何变更后同时刷新列表与页头默认门店摘要 */ const refreshAll = useCallback(async () => { if (!platformId) return; await loadStores(platformId); await refreshContext(); }, [platformId, loadStores, refreshContext]); const openCreate = () => { createForm.resetFields(); createForm.setFieldsValue({ is_default: stores.length === 0 }); setCreateOpen(true); }; const handleCreate = async (values: CreateFormValues) => { if (!currentPlatform) return; setCreating(true); try { await storesApi.create({ platform_id: currentPlatform.id, name: values.name.trim(), client_id: values.client_id.trim(), api_key: values.api_key, currency_code: values.currency_code, is_default: values.is_default, note: values.note?.trim() ? values.note.trim() : null, }); message.success('店铺已创建,凭据验证进行中,稍后可刷新查看结果'); setCreateOpen(false); await refreshAll(); } catch (err) { message.error(apiErrorMessage(err)); } finally { setCreating(false); } }; const openEdit = (store: Store) => { setEditingStore(store); editForm.setFieldsValue({ name: store.name, currency_code: store.currency_code, note: store.note ?? '', }); }; const handleEditSave = async (values: EditFormValues) => { if (!editingStore) return; setSaving(true); try { const result = await storesApi.update( editingStore.id, { name: values.name.trim(), currency_code: values.currency_code, note: values.note?.trim() ? values.note.trim() : null, // 留空表示保留旧密钥;填写则替换并重新触发验证 ...(values.api_key ? { api_key: values.api_key } : {}), }, { revision: editingStore.revision } ); if (result && 'operation' in result) { message.success('已保存,新密钥验证进行中,稍后可刷新查看结果'); } else { message.success('店铺已保存'); } setEditingStore(null); await refreshAll(); } catch (err) { // 乐观锁版本过期:提示后刷新列表,用户重新编辑拿到新 revision if (axios.isAxiosError(err) && err.response?.status === 409) { message.error('店铺信息已被其他操作更新,列表已刷新,请重新编辑'); setEditingStore(null); await refreshAll(); } else { message.error(apiErrorMessage(err)); } } finally { setSaving(false); } }; const withPending = async (store: Store, action: () => Promise) => { setPendingStoreId(store.id); try { await action(); await refreshAll(); } catch (err) { message.error(apiErrorMessage(err)); } finally { setPendingStoreId(null); } }; const handleSetDefault = (store: Store) => withPending(store, async () => { await storesApi.setDefault(store.id); message.success(`已将「${store.name}」设为默认店铺`); }); const handleVerify = (store: Store) => withPending(store, async () => { await storesApi.requestCredentialVerification(store.id); message.success('已发起凭据验证,稍后可刷新查看结果'); }); const handleSyncWarehouses = (store: Store) => withPending(store, async () => { await storesApi.requestWarehouseSync(store.id); message.success('已发起仓库同步,稍后可刷新查看结果'); }); const handleDelete = (store: Store) => { modal.confirm({ title: '删除店铺', content: `仅删除 Atlas 内的店铺连接,不影响平台后台。确定删除「${store.name}」吗?`, okText: '删除', okButtonProps: { danger: true }, cancelText: '取消', onOk: () => withPending(store, async () => { try { await storesApi.remove(store.id); message.success('店铺已删除'); } catch (err) { if (apiErrorCode(err) === 'DEFAULT_STORE_REPLACEMENT_REQUIRED') { message.error( '该店铺是默认店铺,请先将同平台的其他店铺设为默认,再删除' ); return; } throw err; } }), }); }; const rowMenu = (store: Store): MenuProps['items'] => [ { key: 'set-default', icon: , label: '设为默认店铺', disabled: store.is_default, }, { key: 'verify', icon: , label: '重新验证凭据' }, { key: 'sync', icon: , label: '同步仓库' }, { type: 'divider' }, { key: 'delete', icon: , label: '删除店铺', danger: true }, ]; const handleRowMenuClick = (store: Store): MenuProps['onClick'] => ({ key }) => { if (key === 'set-default') void handleSetDefault(store); else if (key === 'verify') void handleVerify(store); else if (key === 'sync') void handleSyncWarehouses(store); else if (key === 'delete') handleDelete(store); }; const columns: TableProps['columns'] = [ { title: '店铺名称', dataIndex: 'name', width: 220, render: (_, store) => ( {store.name} {store.is_default && 默认} ), }, { title: 'Client ID', dataIndex: 'platform_client_id', width: 150, render: (value: string) => ( {value} ), }, { title: '币种', dataIndex: 'currency_code', width: 80, }, { title: '状态', dataIndex: 'status', width: 200, render: (_, store) => { const status = STORE_STATUS_MAP[store.status] ?? { color: 'default', text: store.status, }; const credential = CREDENTIAL_STATUS_MAP[store.credential_status] ?? { color: 'default', text: store.credential_status, }; return ( {status.text} {credential.text} {store.last_error && ( )} ); }, }, { title: '仓库', dataIndex: 'warehouse_count', width: 90, render: (count: number, store) => ( {store.warehouses_synced_at ? count : '—'} ), }, { title: '水印模板', dataIndex: 'watermark_template', width: 130, render: (template: Store['watermark_template']) => template ? ( template.name ) : ( 未绑定 ), }, { title: '备注', dataIndex: 'note', ellipsis: true, render: (note: string | null) => note ? ( {note} ) : ( — ), }, { title: '更新时间', dataIndex: 'updated_at', width: 170, render: (value: string) => ( {formatDateTime(value)} ), }, { title: '操作', key: 'actions', width: 140, fixed: 'right', render: (_, store) => ( ); } return (
} disabled={!currentPlatform} onClick={openCreate} > 添加店铺 } > {platforms.length === 0 && !currentPlatform ? ( ) : ( <> {nextCursor && (
)} )} setCreateOpen(false)} onOk={() => createForm.submit()} confirmLoading={creating} okText="创建并验证" cancelText="取消" destroyOnHidden maskClosable={false} > form={createForm} layout="vertical" requiredMark={false} onFinish={handleCreate} > {/* 契约上限 8192(packages/contracts/src/stores.ts),不自行收紧 */}