forked from lunanting/cangku
700 lines
22 KiB
JavaScript
700 lines
22 KiB
JavaScript
import React, { useCallback, useEffect, useState } from 'react';
|
||
import {
|
||
Alert,
|
||
Button,
|
||
DatePicker,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Popconfirm,
|
||
Select,
|
||
Space,
|
||
Switch,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Tooltip,
|
||
Typography,
|
||
message,
|
||
} from 'antd';
|
||
import {
|
||
CopyOutlined,
|
||
DeleteOutlined,
|
||
DesktopOutlined,
|
||
EditOutlined,
|
||
ExportOutlined,
|
||
KeyOutlined,
|
||
LinkOutlined,
|
||
PlusOutlined,
|
||
ReloadOutlined,
|
||
} from '@ant-design/icons';
|
||
import dayjs from 'dayjs';
|
||
import { SycmCard } from '../../components/Common';
|
||
import {
|
||
createApiKey,
|
||
createExternalAutomationApp,
|
||
deleteApiKey,
|
||
deleteExternalAutomationApp,
|
||
getApiKeys,
|
||
getExternalAutomationApps,
|
||
updateApiKey,
|
||
updateExternalAutomationApp,
|
||
} from '../../api/admin';
|
||
|
||
const { Paragraph, Text, Title } = Typography;
|
||
const API_BASE_URL = 'https://pddg.jiansuntuzhi.cn/api/external/selection-fission';
|
||
const PRESALE_API_URL = `${API_BASE_URL}/presale/tasks`;
|
||
const PLATFORM_LABELS = {
|
||
windows: 'Windows',
|
||
macos: 'macOS',
|
||
linux: 'Linux',
|
||
all: '全平台',
|
||
};
|
||
|
||
function formatDateTime(value) {
|
||
if (!value) return '-';
|
||
const date = dayjs(value);
|
||
return date.isValid() ? date.format('YYYY-MM-DD HH:mm:ss') : '-';
|
||
}
|
||
|
||
const ApiKeySettings = () => {
|
||
const [form] = Form.useForm();
|
||
const [appForm] = Form.useForm();
|
||
const [rows, setRows] = useState([]);
|
||
const [appRows, setAppRows] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [appLoading, setAppLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [updatingId, setUpdatingId] = useState(null);
|
||
const [editingRecord, setEditingRecord] = useState(null);
|
||
const [editorOpen, setEditorOpen] = useState(false);
|
||
const [revealedKey, setRevealedKey] = useState('');
|
||
const [appEditorOpen, setAppEditorOpen] = useState(false);
|
||
const [editingApp, setEditingApp] = useState(null);
|
||
const [appSaving, setAppSaving] = useState(false);
|
||
|
||
const loadRows = useCallback(async () => {
|
||
try {
|
||
setLoading(true);
|
||
const result = await getApiKeys();
|
||
setRows(Array.isArray(result?.data) ? result.data : []);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '加载 API Key 失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
const loadAppRows = useCallback(async () => {
|
||
try {
|
||
setAppLoading(true);
|
||
const result = await getExternalAutomationApps();
|
||
setAppRows(Array.isArray(result?.data) ? result.data : []);
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '加载 App 链接失败');
|
||
} finally {
|
||
setAppLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadRows();
|
||
loadAppRows();
|
||
}, [loadAppRows, loadRows]);
|
||
|
||
const refreshAll = useCallback(() => {
|
||
loadRows();
|
||
loadAppRows();
|
||
}, [loadAppRows, loadRows]);
|
||
|
||
const copyText = async (value, successText) => {
|
||
try {
|
||
await navigator.clipboard.writeText(value);
|
||
message.success(successText);
|
||
} catch {
|
||
message.error('复制失败,请手动选择复制');
|
||
}
|
||
};
|
||
|
||
const openCreateApp = () => {
|
||
setEditingApp(null);
|
||
appForm.resetFields();
|
||
appForm.setFieldsValue({ platform: 'windows', enabled: true, sortOrder: 0 });
|
||
setAppEditorOpen(true);
|
||
};
|
||
|
||
const openEditApp = (record) => {
|
||
setEditingApp(record);
|
||
appForm.setFieldsValue(record);
|
||
setAppEditorOpen(true);
|
||
};
|
||
|
||
const saveApp = async () => {
|
||
try {
|
||
const values = await appForm.validateFields();
|
||
setAppSaving(true);
|
||
if (editingApp) {
|
||
await updateExternalAutomationApp(editingApp.id, values);
|
||
} else {
|
||
await createExternalAutomationApp(values);
|
||
}
|
||
message.success(editingApp ? 'App 链接已更新' : 'App 链接已创建');
|
||
setAppEditorOpen(false);
|
||
appForm.resetFields();
|
||
await loadAppRows();
|
||
} catch (error) {
|
||
if (!error?.errorFields) {
|
||
message.error(error.response?.data?.error || '保存 App 链接失败');
|
||
}
|
||
} finally {
|
||
setAppSaving(false);
|
||
}
|
||
};
|
||
|
||
const toggleApp = async (record, enabled) => {
|
||
try {
|
||
setUpdatingId(`app-${record.id}`);
|
||
await updateExternalAutomationApp(record.id, { enabled });
|
||
message.success(enabled ? 'App 链接已启用' : 'App 链接已停用');
|
||
await loadAppRows();
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '更新 App 链接状态失败');
|
||
} finally {
|
||
setUpdatingId(null);
|
||
}
|
||
};
|
||
|
||
const removeApp = async (record) => {
|
||
try {
|
||
setUpdatingId(`app-${record.id}`);
|
||
await deleteExternalAutomationApp(record.id);
|
||
message.success('App 链接已删除');
|
||
await loadAppRows();
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '删除 App 链接失败');
|
||
} finally {
|
||
setUpdatingId(null);
|
||
}
|
||
};
|
||
|
||
const openCreate = () => {
|
||
setEditingRecord(null);
|
||
form.resetFields();
|
||
form.setFieldsValue({ enabled: true, hmacRequired: false, allowedIps: '' });
|
||
setEditorOpen(true);
|
||
};
|
||
|
||
const openEdit = (record) => {
|
||
setEditingRecord(record);
|
||
form.setFieldsValue({
|
||
name: record.name,
|
||
description: record.description || '',
|
||
apiKey: '',
|
||
allowedIps: Array.isArray(record.allowedIps) ? record.allowedIps.join('\n') : '',
|
||
hmacRequired: record.hmacRequired === true,
|
||
enabled: record.enabled,
|
||
expiresAt: record.expiresAt ? dayjs(record.expiresAt) : null,
|
||
});
|
||
setEditorOpen(true);
|
||
};
|
||
|
||
const saveKey = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
setSaving(true);
|
||
const payload = {
|
||
name: values.name,
|
||
description: values.description || '',
|
||
allowedIps: String(values.allowedIps || '')
|
||
.split(/[\s,;]+/)
|
||
.map((item) => item.trim())
|
||
.filter(Boolean),
|
||
hmacRequired: values.hmacRequired === true,
|
||
enabled: values.enabled !== false,
|
||
expiresAt: values.expiresAt ? values.expiresAt.toISOString() : null,
|
||
};
|
||
const apiKey = String(values.apiKey || '').trim();
|
||
if (apiKey) payload.apiKey = apiKey;
|
||
|
||
const result = editingRecord
|
||
? await updateApiKey(editingRecord.id, payload)
|
||
: await createApiKey(payload);
|
||
setEditorOpen(false);
|
||
form.resetFields();
|
||
message.success(editingRecord ? 'API Key 已更新' : 'API Key 已创建');
|
||
if (result?.data?.plainApiKey) {
|
||
setRevealedKey(result.data.plainApiKey);
|
||
}
|
||
await loadRows();
|
||
} catch (error) {
|
||
if (!error?.errorFields) {
|
||
message.error(error.response?.data?.error || '保存 API Key 失败');
|
||
}
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const toggleKey = async (record, enabled) => {
|
||
try {
|
||
setUpdatingId(record.id);
|
||
await updateApiKey(record.id, { enabled });
|
||
message.success(enabled ? 'API Key 已启用' : 'API Key 已停用');
|
||
await loadRows();
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '更新 API Key 状态失败');
|
||
} finally {
|
||
setUpdatingId(null);
|
||
}
|
||
};
|
||
|
||
const removeKey = async (record) => {
|
||
try {
|
||
setUpdatingId(record.id);
|
||
await deleteApiKey(record.id);
|
||
message.success('API Key 已删除');
|
||
await loadRows();
|
||
} catch (error) {
|
||
message.error(error.response?.data?.error || '删除 API Key 失败');
|
||
} finally {
|
||
setUpdatingId(null);
|
||
}
|
||
};
|
||
|
||
const copyRevealedKey = async () => {
|
||
await copyText(revealedKey, '完整 API Key 已复制');
|
||
};
|
||
|
||
const appColumns = [
|
||
{
|
||
title: 'App',
|
||
dataIndex: 'name',
|
||
width: 220,
|
||
render: (value, record) => (
|
||
<div className="api-key-name">
|
||
<Space size={6}>
|
||
<Text strong>{value}</Text>
|
||
{record.version ? <Tag>{record.version}</Tag> : null}
|
||
</Space>
|
||
{record.description ? <Text type="secondary">{record.description}</Text> : null}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '平台',
|
||
dataIndex: 'platform',
|
||
width: 110,
|
||
render: (value) => <Tag color="blue">{PLATFORM_LABELS[value] || value}</Tag>,
|
||
},
|
||
{
|
||
title: '下载/启动链接',
|
||
dataIndex: 'downloadUrl',
|
||
width: 420,
|
||
render: (value) => (
|
||
<Space size={4} className="external-app-link">
|
||
<Text ellipsis={{ tooltip: value }}>{value}</Text>
|
||
<Tooltip title="复制链接">
|
||
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => copyText(value, 'App 链接已复制')} />
|
||
</Tooltip>
|
||
<Tooltip title="打开链接">
|
||
<Button type="text" size="small" icon={<ExportOutlined />} href={value} target="_blank" rel="noreferrer" />
|
||
</Tooltip>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'enabled',
|
||
width: 120,
|
||
render: (enabled, record) => (
|
||
<Switch
|
||
checked={enabled}
|
||
loading={updatingId === `app-${record.id}`}
|
||
checkedChildren="启用"
|
||
unCheckedChildren="停用"
|
||
onChange={(checked) => toggleApp(record, checked)}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
title: '更新时间',
|
||
dataIndex: 'updatedAt',
|
||
width: 170,
|
||
render: formatDateTime,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
fixed: 'right',
|
||
width: 104,
|
||
render: (_, record) => (
|
||
<Space size={4}>
|
||
<Tooltip title="编辑 App 链接">
|
||
<Button type="text" icon={<EditOutlined />} aria-label="编辑 App 链接" onClick={() => openEditApp(record)} />
|
||
</Tooltip>
|
||
<Popconfirm
|
||
title="删除 App 链接?"
|
||
description="删除后,系统设置中将不再提供这个下载入口。"
|
||
okText="删除"
|
||
cancelText="取消"
|
||
okButtonProps={{ danger: true }}
|
||
onConfirm={() => removeApp(record)}
|
||
>
|
||
<Tooltip title="删除">
|
||
<Button
|
||
type="text"
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
aria-label="删除 App 链接"
|
||
loading={updatingId === `app-${record.id}`}
|
||
/>
|
||
</Tooltip>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
const columns = [
|
||
{
|
||
title: '名称',
|
||
dataIndex: 'name',
|
||
width: 220,
|
||
render: (value, record) => (
|
||
<div className="api-key-name">
|
||
<Text strong>{value}</Text>
|
||
{record.description ? <Text type="secondary">{record.description}</Text> : null}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '用途',
|
||
dataIndex: 'serviceName',
|
||
width: 210,
|
||
render: (value) => <Tag color="blue">{value}</Tag>,
|
||
},
|
||
{
|
||
title: 'API Key',
|
||
dataIndex: 'keyMasked',
|
||
width: 220,
|
||
render: (value) => <Text code>{value}</Text>,
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'enabled',
|
||
width: 170,
|
||
render: (enabled, record) => (
|
||
<Space>
|
||
<Switch
|
||
checked={enabled}
|
||
loading={updatingId === record.id}
|
||
checkedChildren="启用"
|
||
unCheckedChildren="停用"
|
||
onChange={(checked) => toggleKey(record, checked)}
|
||
/>
|
||
{record.expired ? <Tag color="error">已过期</Tag> : null}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '安全策略',
|
||
key: 'security',
|
||
width: 220,
|
||
render: (_, record) => (
|
||
<Space size={4} wrap>
|
||
<Tag color={record.allowedIps?.length ? 'blue' : 'default'}>
|
||
{record.allowedIps?.length ? `IP ${record.allowedIps.length} 个` : '不限 IP'}
|
||
</Tag>
|
||
<Tag color={record.hmacRequired ? 'green' : 'default'}>
|
||
{record.hmacRequired ? '强制 HMAC' : 'API Key'}
|
||
</Tag>
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '有效期至',
|
||
dataIndex: 'expiresAt',
|
||
width: 170,
|
||
render: (value) => value ? formatDateTime(value) : <Tag>长期有效</Tag>,
|
||
},
|
||
{
|
||
title: '最近使用',
|
||
dataIndex: 'lastUsedAt',
|
||
width: 170,
|
||
render: formatDateTime,
|
||
},
|
||
{
|
||
title: '创建人',
|
||
dataIndex: 'createdBy',
|
||
width: 130,
|
||
render: (value) => value || '-',
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
fixed: 'right',
|
||
width: 104,
|
||
render: (_, record) => (
|
||
<Space size={4}>
|
||
<Tooltip title="编辑或轮换 Key">
|
||
<Button
|
||
type="text"
|
||
icon={<EditOutlined />}
|
||
aria-label="编辑 API Key"
|
||
onClick={() => openEdit(record)}
|
||
/>
|
||
</Tooltip>
|
||
<Popconfirm
|
||
title="删除 API Key"
|
||
description="删除后,使用该 Key 的第三方请求会立即失效。"
|
||
okText="删除"
|
||
cancelText="取消"
|
||
okButtonProps={{ danger: true }}
|
||
onConfirm={() => removeKey(record)}
|
||
>
|
||
<Tooltip title="删除">
|
||
<Button
|
||
type="text"
|
||
danger
|
||
icon={<DeleteOutlined />}
|
||
aria-label="删除 API Key"
|
||
loading={updatingId === record.id}
|
||
/>
|
||
</Tooltip>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="settings-tab-page api-key-settings-page">
|
||
<SycmCard bodyStyle={{ padding: 16 }}>
|
||
<div className="api-key-settings-head">
|
||
<Space align="center">
|
||
<DesktopOutlined className="api-key-settings-icon" />
|
||
<div>
|
||
<Title level={4}>自动上架 App 接入</Title>
|
||
<Text type="secondary">统一管理桌面 App 链接和接口请求密钥</Text>
|
||
</div>
|
||
</Space>
|
||
<Button icon={<ReloadOutlined />} loading={loading || appLoading} onClick={refreshAll}>刷新</Button>
|
||
</div>
|
||
|
||
<Alert
|
||
className="external-automation-endpoint"
|
||
type="info"
|
||
showIcon
|
||
icon={<LinkOutlined />}
|
||
message="对外接口地址"
|
||
description={(
|
||
<div className="external-automation-endpoint-list">
|
||
<div>
|
||
<Text type="secondary">自动上架</Text>
|
||
<Text code copyable={{ text: API_BASE_URL }}>{API_BASE_URL}</Text>
|
||
</div>
|
||
<div>
|
||
<Text type="secondary">预售任务</Text>
|
||
<Text code copyable={{ text: PRESALE_API_URL }}>{PRESALE_API_URL}</Text>
|
||
</div>
|
||
</div>
|
||
)}
|
||
/>
|
||
|
||
<Tabs
|
||
className="external-automation-tabs"
|
||
items={[
|
||
{
|
||
key: 'apps',
|
||
label: <span><DesktopOutlined /> App 链接</span>,
|
||
children: (
|
||
<>
|
||
<div className="external-automation-toolbar">
|
||
<Text type="secondary">提供桌面端安装包、启动地址和版本更新入口</Text>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateApp}>新增 App 链接</Button>
|
||
</div>
|
||
<Table
|
||
rowKey="id"
|
||
columns={appColumns}
|
||
dataSource={appRows}
|
||
loading={appLoading}
|
||
pagination={appRows.length > 10 ? { pageSize: 10, showSizeChanger: false } : false}
|
||
scroll={{ x: 1120 }}
|
||
locale={{ emptyText: '暂无 App 链接,点击右上角新增' }}
|
||
/>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
key: 'keys',
|
||
label: <span><KeyOutlined /> 请求 Key</span>,
|
||
children: (
|
||
<>
|
||
<div className="external-automation-toolbar">
|
||
<Text type="secondary">同一个 Key 用于自动上架、店铺同步、结果回传和预售任务查询</Text>
|
||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>生成请求 Key</Button>
|
||
</div>
|
||
<Table
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={rows}
|
||
loading={loading}
|
||
pagination={rows.length > 10 ? { pageSize: 10, showSizeChanger: false } : false}
|
||
scroll={{ x: 1540 }}
|
||
locale={{ emptyText: '暂无请求 Key,点击右上角生成' }}
|
||
/>
|
||
</>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</SycmCard>
|
||
|
||
<Modal
|
||
title={editingApp ? '编辑 App 链接' : '新增 App 链接'}
|
||
open={appEditorOpen}
|
||
okText={editingApp ? '保存修改' : '创建'}
|
||
cancelText="取消"
|
||
confirmLoading={appSaving}
|
||
onOk={saveApp}
|
||
onCancel={() => setAppEditorOpen(false)}
|
||
destroyOnHidden
|
||
>
|
||
<Form form={appForm} layout="vertical" className="api-key-editor-form">
|
||
<Form.Item
|
||
label="App 名称"
|
||
name="name"
|
||
rules={[{ required: true, message: '请输入 App 名称' }, { max: 120 }]}
|
||
>
|
||
<Input placeholder="例如 自动上架桌面端" />
|
||
</Form.Item>
|
||
<Space size={12} align="start" className="external-app-form-row">
|
||
<Form.Item label="适用平台" name="platform" rules={[{ required: true }]}>
|
||
<Select
|
||
style={{ width: 160 }}
|
||
options={Object.entries(PLATFORM_LABELS).map(([value, label]) => ({ value, label }))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item label="版本号" name="version">
|
||
<Input placeholder="例如 1.2.0" maxLength={80} />
|
||
</Form.Item>
|
||
<Form.Item label="排序" name="sortOrder">
|
||
<InputNumber min={-9999} max={9999} />
|
||
</Form.Item>
|
||
</Space>
|
||
<Form.Item
|
||
label="下载或启动链接"
|
||
name="downloadUrl"
|
||
rules={[{ required: true, message: '请输入 App 链接' }, { type: 'url', message: '请输入有效链接' }]}
|
||
>
|
||
<Input prefix={<LinkOutlined />} placeholder="https://..." />
|
||
</Form.Item>
|
||
<Form.Item label="更新说明链接" name="releaseNotesUrl" rules={[{ type: 'url', warningOnly: true }]}>
|
||
<Input prefix={<LinkOutlined />} placeholder="选填,https://..." />
|
||
</Form.Item>
|
||
<Form.Item label="备注" name="description">
|
||
<Input.TextArea rows={2} maxLength={500} showCount placeholder="记录安装要求、发布渠道等信息" />
|
||
</Form.Item>
|
||
<Form.Item label="启用状态" name="enabled" valuePropName="checked">
|
||
<Switch checkedChildren="启用" unCheckedChildren="停用" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title={editingRecord ? '编辑请求 Key' : '生成请求 Key'}
|
||
open={editorOpen}
|
||
okText={editingRecord ? '保存修改' : '创建'}
|
||
cancelText="取消"
|
||
confirmLoading={saving}
|
||
onOk={saveKey}
|
||
onCancel={() => setEditorOpen(false)}
|
||
destroyOnHidden
|
||
>
|
||
<Form form={form} layout="vertical" className="api-key-editor-form">
|
||
<Form.Item
|
||
label="名称"
|
||
name="name"
|
||
rules={[
|
||
{ required: true, message: '请输入请求 Key 名称' },
|
||
{ max: 120, message: '名称不能超过 120 个字符' },
|
||
]}
|
||
>
|
||
<Input placeholder="例如 ERP 选品同步" />
|
||
</Form.Item>
|
||
<Form.Item label="备注" name="description">
|
||
<Input.TextArea rows={2} maxLength={500} showCount placeholder="记录使用方或接入系统" />
|
||
</Form.Item>
|
||
<Form.Item
|
||
label={editingRecord ? '替换请求 Key' : '请求 Key'}
|
||
name="apiKey"
|
||
extra={editingRecord ? '留空保留原 Key;填写后会立即轮换。' : '留空时由系统自动生成安全 Key。'}
|
||
rules={[{ min: 16, message: 'API Key 长度不能少于 16 位' }]}
|
||
>
|
||
<Input.Password
|
||
autoComplete="new-password"
|
||
placeholder={editingRecord ? '留空则不修改' : '留空自动生成'}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
label="IP 白名单"
|
||
name="allowedIps"
|
||
extra="留空表示不限制;每行填写一个 IPv4 或 IPv6 地址。"
|
||
>
|
||
<Input.TextArea rows={3} placeholder={'例如:\n203.0.113.10\n2001:db8::10'} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
label="强制 HMAC 签名"
|
||
name="hmacRequired"
|
||
valuePropName="checked"
|
||
extra="开启后,第三方每次请求都必须使用当前 API Key 计算 SHA-256 HMAC 签名。"
|
||
>
|
||
<Switch checkedChildren="强制" unCheckedChildren="关闭" />
|
||
</Form.Item>
|
||
<Form.Item label="有效期至" name="expiresAt">
|
||
<DatePicker
|
||
showTime
|
||
allowClear
|
||
style={{ width: '100%' }}
|
||
placeholder="留空表示长期有效"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item label="启用状态" name="enabled" valuePropName="checked">
|
||
<Switch checkedChildren="启用" unCheckedChildren="停用" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
title="完整 API Key"
|
||
open={Boolean(revealedKey)}
|
||
closable={false}
|
||
maskClosable={false}
|
||
footer={(
|
||
<Button type="primary" onClick={() => setRevealedKey('')}>
|
||
我已保存
|
||
</Button>
|
||
)}
|
||
>
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
message="完整 Key 仅显示这一次"
|
||
description="关闭后系统只保留不可逆摘要,无法再次查看。"
|
||
/>
|
||
<div className="api-key-reveal">
|
||
<Paragraph code copyable={{ text: revealedKey }} ellipsis={false}>
|
||
{revealedKey}
|
||
</Paragraph>
|
||
<Button icon={<CopyOutlined />} onClick={copyRevealedKey}>
|
||
复制完整 Key
|
||
</Button>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ApiKeySettings;
|