736 lines
28 KiB
TypeScript
736 lines
28 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, 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,
|
||
} from '@ant-design/icons';
|
||
import axios from 'axios';
|
||
import type { Store, WatermarkTemplate } from '@atlas/contracts';
|
||
import { apiErrorCode, apiErrorMessage } from '@/services/api';
|
||
import { storesApi } from '@/services/stores';
|
||
import { watermarksApi } from '@/services/watermarks';
|
||
import { operationsApi, OPERATION_TERMINAL_STATUSES } from '@/services/operations';
|
||
import { usePlatform } from '@/services/platform';
|
||
import './ShopsPage.css';
|
||
|
||
const STORE_STATUS_MAP: Record<string, { color: string; text: string }> = {
|
||
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: '已删除' },
|
||
};
|
||
|
||
interface StoreStatusView {
|
||
color: string;
|
||
text: string;
|
||
/** 状态附注,如「等待平台验证结果」或凭据验证的脱敏原因 */
|
||
note?: string;
|
||
}
|
||
|
||
/**
|
||
* 把门店生命周期状态(status)与凭据状态(credential_status)派生为单一展示状态,
|
||
* 避免两个内部状态并列渲染造成「验证中 + 未验证」的误导:
|
||
* - verifying(或凭据仍 unknown,Atlas 尚未拿到验证结果):只显示「验证中」并附注
|
||
* - active 且凭据 valid:正常
|
||
* - 凭据验证失败:401 → 凭据无效;403 → 访问被拒绝(提示检查 Client ID/API Key 归属与权限)
|
||
* - 同步异常:直接展示 last_error.message——HTTP 400/404/422、超时/429/5xx 等
|
||
* 验证请求失败不代表 API Key 无效(OZON_VERIFICATION_UNAVAILABLE 的消息即「暂时无法验证」)
|
||
* 未穷举的组合按凭据状态(过期/待轮换)或生命周期兜底。
|
||
*/
|
||
function deriveStoreStatusView(store: Store): StoreStatusView {
|
||
const { status, credential_status: credential, last_error } = store;
|
||
if (status === 'verifying' || credential === 'unknown') {
|
||
return { color: 'gold', text: '验证中', note: '等待平台验证结果' };
|
||
}
|
||
if (status === 'credential_invalid' || credential === 'invalid') {
|
||
if (last_error?.code === 'OZON_CREDENTIAL_FORBIDDEN') {
|
||
return {
|
||
color: 'red',
|
||
text: '访问被拒绝',
|
||
note: '请检查 Client ID、API Key 是否同账户,以及密钥是否有读取权限',
|
||
};
|
||
}
|
||
return { color: 'red', text: '凭据无效' };
|
||
}
|
||
if (status === 'sync_error') {
|
||
return {
|
||
color: 'orange',
|
||
text: '同步异常',
|
||
note: last_error?.message ?? '同步过程出现异常,可重新发起仓库同步或凭据验证',
|
||
};
|
||
}
|
||
if (status === 'active' && credential === 'valid') {
|
||
return { color: 'green', text: '正常' };
|
||
}
|
||
if (credential === 'expired') {
|
||
return { color: 'red', text: '凭据已过期' };
|
||
}
|
||
if (credential === 'rotation_required') {
|
||
return { color: 'gold', text: '凭据待轮换' };
|
||
}
|
||
const fallback = STORE_STATUS_MAP[status];
|
||
return fallback ?? { color: 'default', text: status };
|
||
}
|
||
|
||
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;
|
||
watermark_template_id?: string | null;
|
||
}
|
||
|
||
/**
|
||
* 店铺管理页,契约见 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<Store[]>([]);
|
||
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [loadingMore, setLoadingMore] = useState(false);
|
||
const [pendingStoreId, setPendingStoreId] = useState<string | null>(null);
|
||
|
||
const [createOpen, setCreateOpen] = useState(false);
|
||
const [creating, setCreating] = useState(false);
|
||
const [createForm] = Form.useForm<CreateFormValues>();
|
||
|
||
const [editingStore, setEditingStore] = useState<Store | null>(null);
|
||
const [saving, setSaving] = useState(false);
|
||
const [editForm] = Form.useForm<EditFormValues>();
|
||
|
||
// 水印模板选项(工作空间全量活动模板,量小一次拉取)
|
||
const [watermarkTemplates, setWatermarkTemplates] = useState<WatermarkTemplate[]>([]);
|
||
const bindingBeforeRef = useRef<string | null>(null);
|
||
|
||
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);
|
||
}
|
||
};
|
||
|
||
// 水印模板选项与平台无关(工作空间全量),挂载后拉取一次
|
||
useEffect(() => {
|
||
void watermarksApi
|
||
.list()
|
||
.then((result) => setWatermarkTemplates(result.data))
|
||
.catch(() => setWatermarkTemplates([]));
|
||
}, []);
|
||
|
||
/** 任何变更后同时刷新列表与页头默认门店摘要 */
|
||
const refreshAll = useCallback(async () => {
|
||
if (!platformId) return;
|
||
await loadStores(platformId);
|
||
await refreshContext();
|
||
}, [platformId, loadStores, refreshContext]);
|
||
|
||
// 组件卸载后停止轮询
|
||
const mountedRef = useRef(true);
|
||
useEffect(() => {
|
||
mountedRef.current = true;
|
||
return () => {
|
||
mountedRef.current = false;
|
||
};
|
||
}, []);
|
||
|
||
/**
|
||
* 轮询异步验证/同步操作至终态,随后刷新列表。
|
||
* 页面最终状态以刷新后的店铺资源为准(成败原因看店铺 last_error),不以 operation 为准;
|
||
* 超时未终态则停止轮询并刷新一次,用户可手动刷新继续观察。
|
||
*/
|
||
const pollOperation = useCallback(
|
||
async (operationId: string) => {
|
||
const intervalMs = 2500;
|
||
const maxAttempts = 40;
|
||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||
if (!mountedRef.current) return;
|
||
try {
|
||
const operation = await operationsApi.get(operationId);
|
||
if (OPERATION_TERMINAL_STATUSES.has(operation.status)) break;
|
||
} catch {
|
||
// 单次查询失败(网络抖动等)忽略,继续下一轮
|
||
}
|
||
}
|
||
if (mountedRef.current) await refreshAll();
|
||
},
|
||
[refreshAll]
|
||
);
|
||
|
||
const openCreate = () => {
|
||
createForm.resetFields();
|
||
createForm.setFieldsValue({
|
||
is_default: stores.length === 0,
|
||
currency_code: currentPlatform?.default_currency_code ?? currencyOptions[0]?.value,
|
||
});
|
||
setCreateOpen(true);
|
||
};
|
||
|
||
const handleCreate = async (values: CreateFormValues) => {
|
||
if (!currentPlatform) return;
|
||
setCreating(true);
|
||
try {
|
||
const result = 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();
|
||
// 轮询验证操作至终态后自动刷新,无需手动刷新页面
|
||
if (result?.operation) void pollOperation(result.operation.id);
|
||
} catch (err) {
|
||
message.error(apiErrorMessage(err));
|
||
} finally {
|
||
setCreating(false);
|
||
}
|
||
};
|
||
|
||
const openEdit = (store: Store) => {
|
||
setEditingStore(store);
|
||
bindingBeforeRef.current = store.watermark_template?.id ?? null;
|
||
editForm.setFieldsValue({
|
||
name: store.name,
|
||
currency_code: store.currency_code,
|
||
note: store.note ?? '',
|
||
watermark_template_id: store.watermark_template?.id ?? null,
|
||
});
|
||
};
|
||
|
||
const handleEditSave = async (values: EditFormValues) => {
|
||
if (!editingStore) return;
|
||
const storeId = editingStore.id;
|
||
const bindingBefore = bindingBeforeRef.current;
|
||
const bindingAfter = values.watermark_template_id ?? null;
|
||
const bindingChanged = bindingBefore !== bindingAfter;
|
||
setSaving(true);
|
||
try {
|
||
const result = await storesApi.update(
|
||
storeId,
|
||
{
|
||
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 (bindingChanged) {
|
||
await storesApi.bindWatermark(storeId, bindingAfter);
|
||
}
|
||
if (result && 'operation' in result && result.operation) {
|
||
message.success('已保存,正在验证新密钥,结果将自动刷新');
|
||
setEditingStore(null);
|
||
await refreshAll();
|
||
void pollOperation(result.operation.id);
|
||
} 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<unknown>) => {
|
||
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 () => {
|
||
const result = await storesApi.requestCredentialVerification(store.id);
|
||
message.success('已发起凭据验证,结果将自动刷新');
|
||
if (result?.operation) void pollOperation(result.operation.id);
|
||
});
|
||
|
||
const handleSyncWarehouses = (store: Store) =>
|
||
withPending(store, async () => {
|
||
const result = await storesApi.requestWarehouseSync(store.id);
|
||
message.success('已发起仓库同步,结果将自动刷新');
|
||
if (result?.operation) void pollOperation(result.operation.id);
|
||
});
|
||
|
||
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: <ApiOutlined />,
|
||
label: '设为默认店铺',
|
||
disabled: store.is_default,
|
||
},
|
||
{ key: 'verify', icon: <ReloadOutlined />, label: '重新验证凭据' },
|
||
{ key: 'sync', icon: <CloudDownloadOutlined />, label: '同步仓库' },
|
||
{ type: 'divider' },
|
||
{ key: 'delete', icon: <DeleteOutlined />, 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<Store>['columns'] = [
|
||
{
|
||
title: '店铺名称',
|
||
dataIndex: 'name',
|
||
width: 220,
|
||
render: (_, store) => (
|
||
<Space size={6}>
|
||
<span>{store.name}</span>
|
||
{store.is_default && <Tag color="purple">默认</Tag>}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: 'Client ID',
|
||
dataIndex: 'platform_client_id',
|
||
width: 150,
|
||
render: (value: string) => (
|
||
<Typography.Text copyable={{ text: value }}>{value}</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '币种',
|
||
dataIndex: 'currency_code',
|
||
width: 80,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
width: 200,
|
||
render: (_, store) => {
|
||
const view = deriveStoreStatusView(store);
|
||
return (
|
||
<Space size={4} wrap>
|
||
<Tag color={view.color}>{view.text}</Tag>
|
||
{view.note && (
|
||
<Typography.Text
|
||
type="secondary"
|
||
style={{ fontSize: 12, maxWidth: 180 }}
|
||
ellipsis={{ tooltip: view.note }}
|
||
>
|
||
{view.note}
|
||
</Typography.Text>
|
||
)}
|
||
</Space>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '仓库',
|
||
dataIndex: 'warehouse_count',
|
||
width: 90,
|
||
render: (count: number, store) => (
|
||
<Tooltip
|
||
title={
|
||
store.warehouses_synced_at
|
||
? `同步于 ${formatDateTime(store.warehouses_synced_at)}`
|
||
: '尚未同步仓库'
|
||
}
|
||
>
|
||
<span>{store.warehouses_synced_at ? count : '—'}</span>
|
||
</Tooltip>
|
||
),
|
||
},
|
||
{
|
||
title: '水印模板',
|
||
dataIndex: 'watermark_template',
|
||
width: 130,
|
||
render: (template: Store['watermark_template']) =>
|
||
template ? (
|
||
template.name
|
||
) : (
|
||
<Typography.Text type="secondary">未绑定</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '备注',
|
||
dataIndex: 'note',
|
||
ellipsis: true,
|
||
render: (note: string | null) =>
|
||
note ? (
|
||
<Typography.Text ellipsis>{note}</Typography.Text>
|
||
) : (
|
||
<Typography.Text type="secondary">—</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '更新时间',
|
||
dataIndex: 'updated_at',
|
||
width: 170,
|
||
render: (value: string) => (
|
||
<Typography.Text type="secondary">{formatDateTime(value)}</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 140,
|
||
fixed: 'right',
|
||
render: (_, store) => (
|
||
<Space size={4}>
|
||
<Button type="link" size="small" onClick={() => openEdit(store)}>
|
||
编辑
|
||
</Button>
|
||
<Dropdown
|
||
menu={{ items: rowMenu(store), onClick: handleRowMenuClick(store) }}
|
||
trigger={['click']}
|
||
>
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
icon={<MoreOutlined />}
|
||
loading={pendingStoreId === store.id}
|
||
/>
|
||
</Dropdown>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
if (platformError) {
|
||
return (
|
||
<Card className="shops-page">
|
||
<Empty description={`平台目录加载失败:${platformError}`}>
|
||
<Button onClick={reload}>重试</Button>
|
||
</Empty>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="shops-page">
|
||
<Card
|
||
title={`店铺管理${currentPlatform ? ` · ${currentPlatform.name}` : ''}`}
|
||
extra={
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
disabled={!currentPlatform}
|
||
onClick={openCreate}
|
||
>
|
||
添加店铺
|
||
</Button>
|
||
}
|
||
>
|
||
{platforms.length === 0 && !currentPlatform ? (
|
||
<Empty description="暂无可用平台" />
|
||
) : (
|
||
<>
|
||
<Table
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={stores}
|
||
loading={loading}
|
||
pagination={false}
|
||
scroll={{ x: 1100 }}
|
||
locale={{
|
||
emptyText: currentPlatform
|
||
? `当前平台还没有店铺,点击右上角「添加店铺」完成 ${currentPlatform.name} 授权`
|
||
: '请先在页头选择平台',
|
||
}}
|
||
/>
|
||
{nextCursor && (
|
||
<div className="shops-load-more">
|
||
<Button loading={loadingMore} onClick={() => void loadMore()}>
|
||
加载更多
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</Card>
|
||
|
||
<Modal
|
||
title={`添加${currentPlatform?.name ?? ''}店铺`}
|
||
open={createOpen}
|
||
onCancel={() => setCreateOpen(false)}
|
||
onOk={() => createForm.submit()}
|
||
confirmLoading={creating}
|
||
okText="创建并验证"
|
||
cancelText="取消"
|
||
destroyOnHidden
|
||
maskClosable={false}
|
||
>
|
||
<Form<CreateFormValues> form={createForm} layout="vertical" onFinish={handleCreate}>
|
||
<Form.Item
|
||
name="name"
|
||
label="店铺名称"
|
||
required
|
||
rules={[{ required: true, whitespace: true, message: '请输入店铺名称' }]}
|
||
>
|
||
<Input maxLength={120} placeholder="例如:Ozon 俄罗斯主店" />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="client_id"
|
||
label="Client ID"
|
||
required
|
||
rules={[
|
||
{ required: true, whitespace: true, message: '请输入平台 Client ID' },
|
||
]}
|
||
>
|
||
<Input maxLength={120} placeholder="平台后台的 Client ID" />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="api_key"
|
||
label="API Key"
|
||
required
|
||
rules={[
|
||
{ required: true, whitespace: true, message: '请输入平台 API Key' },
|
||
]}
|
||
extra="仅用于服务端加密保存与凭据验证,不会出现在日志或响应中"
|
||
>
|
||
{/* 契约上限 8192(packages/contracts/src/stores.ts),不自行收紧 */}
|
||
<Input.Password
|
||
maxLength={8192}
|
||
placeholder="平台后台的 API Key"
|
||
autoComplete="new-password"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="currency_code"
|
||
label="默认计价币种"
|
||
required
|
||
rules={[{ required: true, message: '请选择币种' }]}
|
||
>
|
||
<Select options={currencyOptions} placeholder="选择币种" />
|
||
</Form.Item>
|
||
<Form.Item name="is_default" label="设为默认店铺" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="note"
|
||
label="备注"
|
||
rules={[{ max: 2000, message: '备注不能超过 2000 字' }]}
|
||
>
|
||
<Input.TextArea rows={2} maxLength={2000} placeholder="选填" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={editingStore ? `编辑店铺:${editingStore.name}` : '编辑店铺'}
|
||
open={!!editingStore}
|
||
onCancel={() => setEditingStore(null)}
|
||
onOk={() => editForm.submit()}
|
||
confirmLoading={saving}
|
||
okText="保存"
|
||
cancelText="取消"
|
||
destroyOnHidden
|
||
maskClosable={false}
|
||
>
|
||
{editingStore && (
|
||
<Form<EditFormValues>
|
||
form={editForm}
|
||
layout="vertical"
|
||
onFinish={handleEditSave}
|
||
>
|
||
<Form.Item
|
||
name="name"
|
||
label="店铺名称"
|
||
required
|
||
rules={[{ required: true, message: '请输入店铺名称' }]}
|
||
>
|
||
<Input maxLength={120} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="currency_code"
|
||
label="默认计价币种"
|
||
required
|
||
rules={[{ required: true, message: '请选择币种' }]}
|
||
>
|
||
<Select options={currencyOptions} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="note"
|
||
label="备注"
|
||
rules={[{ max: 2000, message: '备注不能超过 2000 字' }]}
|
||
>
|
||
<Input.TextArea rows={2} maxLength={2000} placeholder="选填" />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="api_key"
|
||
label="替换 API Key"
|
||
extra="留空保留现有密钥;填写后将替换并重新验证凭据"
|
||
>
|
||
<Input.Password
|
||
maxLength={8192}
|
||
placeholder="不修改请留空"
|
||
autoComplete="new-password"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="watermark_template_id"
|
||
label="水印模板"
|
||
extra="作为商品图片编辑时的推荐水印;可随时更换或清空"
|
||
>
|
||
<Select
|
||
allowClear
|
||
placeholder="暂不绑定"
|
||
notFoundContent="暂无水印模板,可在水印管理页创建"
|
||
options={watermarkTemplates.map((template) => ({
|
||
value: template.id,
|
||
label: template.name,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item label="Client ID">
|
||
<Typography.Text type="secondary">
|
||
{editingStore.platform_client_id}(平台与账户不可修改)
|
||
</Typography.Text>
|
||
</Form.Item>
|
||
</Form>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ShopsPage;
|