622 lines
20 KiB
TypeScript
622 lines
20 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import {
|
||
Avatar,
|
||
Button,
|
||
Checkbox,
|
||
Descriptions,
|
||
Drawer,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Popconfirm,
|
||
Select,
|
||
Space,
|
||
Switch,
|
||
Table,
|
||
Tag,
|
||
Typography,
|
||
message,
|
||
} from 'antd';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import {
|
||
WECOM_BOT_PERMISSION_GROUPS,
|
||
WECOM_BOT_PERMISSION_LABELS,
|
||
WECOM_BOT_ROLE_DEFAULT_PERMISSIONS,
|
||
WECOM_BOT_ROLE_LABELS,
|
||
WECOM_BOT_ROLES,
|
||
type LlmApiConfigOptionDto,
|
||
type KnowledgeBaseOptionDto,
|
||
type WecomAibotRuntimeDto,
|
||
type WecomBotDto,
|
||
type WecomBotPermission,
|
||
type WecomBotRole,
|
||
} from '@dukang/shared-types';
|
||
import { request } from '../lib/api';
|
||
import { fmtTime } from '../lib/constants';
|
||
import { useAdminList } from '../lib/useAdminList';
|
||
import OssUpload from '../components/OssUpload';
|
||
|
||
type FormValues = {
|
||
name: string;
|
||
role: WecomBotRole;
|
||
botId: string;
|
||
secret?: string;
|
||
avatarUrl?: string;
|
||
welcome?: string;
|
||
permissions: WecomBotPermission[];
|
||
reviewSuperAdminWecomUserIds?: string;
|
||
aiEnabled: boolean;
|
||
llmConfigId?: string | null;
|
||
knowledgeBaseId?: string | null;
|
||
enabled: boolean;
|
||
sortOrder: number;
|
||
};
|
||
|
||
function parseUserIdLines(text?: string): string[] {
|
||
if (!text?.trim()) return [];
|
||
return [...new Set(text.split(/[,,\s\n]+/).map((s) => s.trim()).filter(Boolean))];
|
||
}
|
||
|
||
function formatUserIdLines(ids?: string[]): string {
|
||
return (ids ?? []).join('\n');
|
||
}
|
||
|
||
function mapDtoToForm(row: WecomBotDto): FormValues {
|
||
return {
|
||
name: row.name,
|
||
role: row.role,
|
||
botId: row.botId,
|
||
secret: '',
|
||
avatarUrl: row.avatarUrl || '',
|
||
welcome: row.welcome || '',
|
||
permissions: [...row.permissions],
|
||
reviewSuperAdminWecomUserIds: formatUserIdLines(row.reviewSuperAdminWecomUserIds),
|
||
aiEnabled: row.aiEnabled,
|
||
llmConfigId: row.llmConfigId,
|
||
knowledgeBaseId: row.knowledgeBaseId,
|
||
enabled: row.enabled,
|
||
sortOrder: row.sortOrder,
|
||
};
|
||
}
|
||
|
||
function samePermissionSet(a: WecomBotPermission[], b: WecomBotPermission[]): boolean {
|
||
if (a.length !== b.length) return false;
|
||
const setB = new Set(b);
|
||
return a.every((p) => setB.has(p));
|
||
}
|
||
|
||
const CREATE_DEFAULTS: FormValues = {
|
||
name: '',
|
||
role: 'CUSTOMER_SERVICE',
|
||
botId: '',
|
||
secret: '',
|
||
avatarUrl: '',
|
||
welcome: '',
|
||
permissions: [...WECOM_BOT_ROLE_DEFAULT_PERMISSIONS.CUSTOMER_SERVICE],
|
||
reviewSuperAdminWecomUserIds: '',
|
||
aiEnabled: false,
|
||
llmConfigId: null,
|
||
knowledgeBaseId: null,
|
||
enabled: true,
|
||
sortOrder: 0,
|
||
};
|
||
|
||
export default function WecomBotsPage() {
|
||
const [filterForm] = Form.useForm();
|
||
const [form] = Form.useForm<FormValues>();
|
||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [editing, setEditing] = useState<WecomBotDto | null>(null);
|
||
const [formInitial, setFormInitial] = useState<FormValues | null>(null);
|
||
const [formReady, setFormReady] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const [detail, setDetail] = useState<WecomBotDto | null>(null);
|
||
const [runtime, setRuntime] = useState<WecomAibotRuntimeDto>();
|
||
const [llmOptions, setLlmOptions] = useState<LlmApiConfigOptionDto[]>([]);
|
||
const [kbOptions, setKbOptions] = useState<KnowledgeBaseOptionDto[]>([]);
|
||
const roleWatch = Form.useWatch('role', form);
|
||
const permissionsWatch = Form.useWatch('permissions', form) as WecomBotPermission[] | undefined;
|
||
|
||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<WecomBotDto>(
|
||
'/admin/wecom-bots',
|
||
() => {
|
||
const qs = new URLSearchParams();
|
||
if (filters.name) qs.set('name', filters.name);
|
||
if (filters.role) qs.set('role', filters.role);
|
||
if (filters.enabled) qs.set('enabled', filters.enabled);
|
||
return qs;
|
||
},
|
||
[filters],
|
||
);
|
||
|
||
const loadRuntime = () =>
|
||
request<WecomAibotRuntimeDto>('/admin/wecom-bots/runtime')
|
||
.then(setRuntime)
|
||
.catch(() => {});
|
||
|
||
useEffect(() => {
|
||
void loadRuntime();
|
||
}, [data]);
|
||
|
||
useEffect(() => {
|
||
void request<{
|
||
llmConfigs: LlmApiConfigOptionDto[];
|
||
knowledgeBases: KnowledgeBaseOptionDto[];
|
||
}>('/admin/wecom-bots/ai-options')
|
||
.then((res) => {
|
||
setLlmOptions(res.llmConfigs);
|
||
setKbOptions(res.knowledgeBases);
|
||
})
|
||
.catch(() => {
|
||
setLlmOptions([]);
|
||
setKbOptions([]);
|
||
});
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!modalOpen || !formReady || !formInitial) return;
|
||
form.resetFields();
|
||
form.setFieldsValue(formInitial);
|
||
}, [modalOpen, formReady, formInitial, form]);
|
||
|
||
function closeModal() {
|
||
setModalOpen(false);
|
||
setFormReady(false);
|
||
setFormInitial(null);
|
||
setEditing(null);
|
||
form.resetFields();
|
||
}
|
||
|
||
function openCreate() {
|
||
setEditing(null);
|
||
setFormInitial({ ...CREATE_DEFAULTS });
|
||
setFormReady(true);
|
||
setModalOpen(true);
|
||
}
|
||
|
||
async function openEdit(row: WecomBotDto) {
|
||
setEditing(row);
|
||
setFormInitial(null);
|
||
setFormReady(false);
|
||
setModalOpen(true);
|
||
try {
|
||
const detail = await request<WecomBotDto>(`/admin/wecom-bots/${row.id}`);
|
||
setEditing(detail);
|
||
setFormInitial(mapDtoToForm(detail));
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '加载机器人详情失败');
|
||
setFormInitial(mapDtoToForm(row));
|
||
} finally {
|
||
setFormReady(true);
|
||
}
|
||
}
|
||
|
||
async function submit() {
|
||
const values = await form.validateFields();
|
||
const permissions = (form.getFieldValue('permissions') ?? values.permissions) as WecomBotPermission[];
|
||
if (!permissions?.length) {
|
||
message.error('请至少选择一项权限');
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
const payload = {
|
||
name: values.name.trim(),
|
||
role: values.role,
|
||
botId: values.botId.trim(),
|
||
avatarUrl: values.avatarUrl?.trim() || null,
|
||
welcome: values.welcome?.trim() || null,
|
||
permissions,
|
||
reviewSuperAdminWecomUserIds: parseUserIdLines(values.reviewSuperAdminWecomUserIds),
|
||
aiEnabled: values.aiEnabled,
|
||
llmConfigId: values.llmConfigId || null,
|
||
knowledgeBaseId: values.knowledgeBaseId || null,
|
||
enabled: values.enabled,
|
||
sortOrder: values.sortOrder,
|
||
};
|
||
if (editing) {
|
||
const saved = await request<WecomBotDto>(`/admin/wecom-bots/${editing.id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({
|
||
...payload,
|
||
secret: values.secret?.trim() || undefined,
|
||
}),
|
||
});
|
||
if (!samePermissionSet(permissions, saved.permissions)) {
|
||
message.warning('权限与保存结果不一致,已用服务端数据刷新表单');
|
||
setFormInitial(mapDtoToForm(saved));
|
||
setEditing(saved);
|
||
reload();
|
||
return;
|
||
}
|
||
message.success('已更新');
|
||
} else {
|
||
if (!values.secret?.trim()) {
|
||
message.error('请填写 Secret');
|
||
return;
|
||
}
|
||
await request('/admin/wecom-bots', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
...payload,
|
||
secret: values.secret.trim(),
|
||
}),
|
||
});
|
||
message.success('已创建');
|
||
}
|
||
closeModal();
|
||
reload();
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '保存失败');
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
}
|
||
|
||
async function remove(id: string) {
|
||
try {
|
||
await request(`/admin/wecom-bots/${id}`, { method: 'DELETE' });
|
||
message.success('已删除');
|
||
reload();
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败');
|
||
}
|
||
}
|
||
|
||
async function reloadConnections() {
|
||
try {
|
||
const st = await request<WecomAibotRuntimeDto>('/admin/wecom-bots/reload', {
|
||
method: 'POST',
|
||
body: '{}',
|
||
});
|
||
message.success('已重载长连接');
|
||
setRuntime(st);
|
||
reload();
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '重载失败');
|
||
}
|
||
}
|
||
|
||
const runtimeMap = new Map((runtime?.bots ?? []).map((b) => [b.id, b]));
|
||
|
||
const columns: ColumnsType<WecomBotDto> = [
|
||
{
|
||
title: '头像',
|
||
dataIndex: 'avatarUrl',
|
||
width: 64,
|
||
render: (url: string | null, row) => (
|
||
<Avatar src={url || undefined} shape="square" size={40}>
|
||
{row.name.slice(0, 1)}
|
||
</Avatar>
|
||
),
|
||
},
|
||
{ title: '名称', dataIndex: 'name', width: 140, ellipsis: true },
|
||
{
|
||
title: '角色',
|
||
dataIndex: 'role',
|
||
width: 120,
|
||
render: (r: WecomBotRole) => WECOM_BOT_ROLE_LABELS[r] || r,
|
||
},
|
||
{ title: 'BotID', dataIndex: 'botId', width: 160, ellipsis: true },
|
||
{
|
||
title: '权限',
|
||
dataIndex: 'permissions',
|
||
ellipsis: true,
|
||
render: (perms: WecomBotPermission[]) =>
|
||
perms.map((p) => (
|
||
<Tag key={p} style={{ marginBottom: 2 }}>
|
||
{WECOM_BOT_PERMISSION_LABELS[p] || p}
|
||
</Tag>
|
||
)),
|
||
},
|
||
{
|
||
title: 'AI',
|
||
width: 100,
|
||
render: (_, row) =>
|
||
row.aiEnabled ? (
|
||
<Tag color="blue">{row.llmConfigName || '已开'}</Tag>
|
||
) : (
|
||
<Tag>关</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '启用',
|
||
dataIndex: 'enabled',
|
||
width: 70,
|
||
render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '是' : '否'}</Tag>,
|
||
},
|
||
{
|
||
title: '连接',
|
||
width: 80,
|
||
render: (_, row) => {
|
||
const rt = runtimeMap.get(row.id);
|
||
if (!runtime?.masterEnabled) return <Tag>总开关关</Tag>;
|
||
if (!row.enabled) return <Tag>未启用</Tag>;
|
||
return (
|
||
<Tag color={rt?.connected ? 'green' : 'orange'}>{rt?.connected ? '已连接' : '未连接'}</Tag>
|
||
);
|
||
},
|
||
},
|
||
{ title: '更新', dataIndex: 'updatedAt', width: 160, render: fmtTime },
|
||
{
|
||
title: '操作',
|
||
width: 180,
|
||
fixed: 'right',
|
||
render: (_, row) => (
|
||
<Space>
|
||
<Button type="link" size="small" onClick={() => void openEdit(row)}>
|
||
编辑
|
||
</Button>
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={async () => {
|
||
setDetail(await request<WecomBotDto>(`/admin/wecom-bots/${row.id}`));
|
||
}}
|
||
>
|
||
详情
|
||
</Button>
|
||
<Popconfirm title="确认删除该机器人?" onConfirm={() => remove(row.id)}>
|
||
<Button type="link" size="small" danger>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }} wrap>
|
||
<div>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>
|
||
企微机器人
|
||
</Typography.Title>
|
||
<Typography.Text type="secondary">
|
||
并列创建多个智能机器人,配置 BotID / Secret / 权限 / 角色 / 头像。总开关在「系统设置 → 功能开关」。
|
||
{runtime ? ` 当前总开关:${runtime.masterEnabled ? '开' : '关'}` : ''}
|
||
</Typography.Text>
|
||
</div>
|
||
<Space>
|
||
<Button onClick={() => void reloadConnections()}>重载连接</Button>
|
||
<Button type="primary" onClick={openCreate}>
|
||
创建机器人
|
||
</Button>
|
||
</Space>
|
||
</Space>
|
||
|
||
<Form
|
||
form={filterForm}
|
||
layout="inline"
|
||
style={{ marginBottom: 16 }}
|
||
onFinish={(v) => {
|
||
setFilters(v);
|
||
setPage(1);
|
||
}}
|
||
>
|
||
<Form.Item name="name" label="名称">
|
||
<Input allowClear placeholder="名称" />
|
||
</Form.Item>
|
||
<Form.Item name="role" label="角色">
|
||
<Select
|
||
allowClear
|
||
style={{ width: 160 }}
|
||
options={WECOM_BOT_ROLES.map((r) => ({ value: r, label: WECOM_BOT_ROLE_LABELS[r] }))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="enabled" label="启用">
|
||
<Select
|
||
allowClear
|
||
style={{ width: 100 }}
|
||
options={[
|
||
{ value: 'true', label: '是' },
|
||
{ value: 'false', label: '否' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Button type="primary" htmlType="submit">
|
||
查询
|
||
</Button>
|
||
</Form.Item>
|
||
</Form>
|
||
|
||
<Table
|
||
rowKey="id"
|
||
loading={loading}
|
||
columns={columns}
|
||
dataSource={data?.items ?? []}
|
||
scroll={{ x: 1200 }}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total: data?.total ?? 0,
|
||
showSizeChanger: true,
|
||
onChange: (p, ps) => {
|
||
setPage(p);
|
||
setPageSize(ps);
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Modal
|
||
title={editing ? '编辑企微机器人' : '创建企微机器人'}
|
||
open={modalOpen}
|
||
onCancel={closeModal}
|
||
onOk={() => void submit()}
|
||
confirmLoading={saving}
|
||
width={640}
|
||
destroyOnClose
|
||
>
|
||
{formReady && formInitial ? (
|
||
<Form
|
||
key={editing ? `edit-${editing.id}` : 'create'}
|
||
form={form}
|
||
layout="vertical"
|
||
preserve={false}
|
||
initialValues={formInitial}
|
||
>
|
||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请填写名称' }]}>
|
||
<Input placeholder="如:客服机器人" maxLength={64} />
|
||
</Form.Item>
|
||
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
|
||
<Select
|
||
options={WECOM_BOT_ROLES.map((r) => ({ value: r, label: WECOM_BOT_ROLE_LABELS[r] }))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item>
|
||
<Button
|
||
size="small"
|
||
onClick={() => {
|
||
const role = form.getFieldValue('role') as WecomBotRole | undefined;
|
||
if (!role) return;
|
||
form.setFieldValue('permissions', [...WECOM_BOT_ROLE_DEFAULT_PERMISSIONS[role]]);
|
||
}}
|
||
>
|
||
应用当前角色默认权限
|
||
</Button>
|
||
</Form.Item>
|
||
<Form.Item name="avatarUrl" label="头像">
|
||
<OssUpload bizType="WECOM_BOT_AVATAR" />
|
||
</Form.Item>
|
||
<Form.Item name="botId" label="BotID" rules={[{ required: true, message: '请填写 BotID' }]}>
|
||
<Input placeholder="企业微信后台长连接 BotID" />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="secret"
|
||
label="Secret"
|
||
rules={editing ? [] : [{ required: true, message: '请填写 Secret' }]}
|
||
extra={editing ? '留空表示不修改' : undefined}
|
||
>
|
||
<Input.Password placeholder={editing ? '留空不修改' : '长连接专用 Secret'} />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="permissions"
|
||
label="权限(按模块)"
|
||
rules={[
|
||
{
|
||
validator: (_, v: WecomBotPermission[] | undefined) =>
|
||
v?.length ? Promise.resolve() : Promise.reject(new Error('请至少选择一项权限')),
|
||
},
|
||
]}
|
||
extra={
|
||
<>
|
||
{roleWatch
|
||
? `角色默认:${WECOM_BOT_ROLE_DEFAULT_PERMISSIONS[roleWatch as WecomBotRole]?.map((p) => WECOM_BOT_PERMISSION_LABELS[p]).join('、') || '无'}`
|
||
: null}
|
||
{permissionsWatch?.length ? (
|
||
<Typography.Text type="secondary"> · 已选 {permissionsWatch.length} 项</Typography.Text>
|
||
) : null}
|
||
</>
|
||
}
|
||
>
|
||
<Checkbox.Group style={{ width: '100%' }}>
|
||
{WECOM_BOT_PERMISSION_GROUPS.map((group) => (
|
||
<div key={group.key} style={{ marginBottom: 12 }}>
|
||
<Typography.Text strong>{group.label}</Typography.Text>
|
||
<div style={{ marginTop: 4 }}>
|
||
{group.permissions.map((p) => (
|
||
<Checkbox key={p} value={p} style={{ marginInlineStart: 0, marginRight: 12 }}>
|
||
{WECOM_BOT_PERMISSION_LABELS[p]}
|
||
</Checkbox>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</Checkbox.Group>
|
||
</Form.Item>
|
||
{(roleWatch === 'TECH_SUPPORT' || permissionsWatch?.includes('support_ticket.review')) && (
|
||
<Form.Item
|
||
name="reviewSuperAdminWecomUserIds"
|
||
label="审批白名单(企微 userid)"
|
||
extra="填写成员企微 userid(如 woDacESQAAD…),不是姓名/昵称;每行或逗号分隔。发「状态」或在服务端日志 inbound user= 可核对"
|
||
>
|
||
<Input.TextArea rows={3} placeholder="zhangsan lisi" />
|
||
</Form.Item>
|
||
)}
|
||
<Form.Item name="welcome" label="欢迎语">
|
||
<Input.TextArea rows={3} placeholder="进入会话时的欢迎语,可空" />
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="aiEnabled"
|
||
label="启用 AI 问答"
|
||
valuePropName="checked"
|
||
extra="开启后,未匹配指令的自然语言将调用绑定的语言模型(可挂知识库)"
|
||
>
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item name="llmConfigId" label="语言模型">
|
||
<Select
|
||
allowClear
|
||
placeholder="选择已生效的配置"
|
||
options={llmOptions.map((o) => ({
|
||
value: o.id,
|
||
label: `${o.name}(${o.modelName})`,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item name="knowledgeBaseId" label="知识库">
|
||
<Select
|
||
allowClear
|
||
placeholder="可选,供 AI 检索"
|
||
options={kbOptions.map((o) => ({
|
||
value: o.id,
|
||
label: `${o.name}(${o.documentCount} 篇)`,
|
||
}))}
|
||
/>
|
||
</Form.Item>
|
||
<Space size="large">
|
||
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
||
<Switch />
|
||
</Form.Item>
|
||
<Form.Item name="sortOrder" label="排序">
|
||
<InputNumber style={{ width: 120 }} />
|
||
</Form.Item>
|
||
</Space>
|
||
</Form>
|
||
) : (
|
||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||
)}
|
||
</Modal>
|
||
|
||
<Drawer title="机器人详情" width={480} open={!!detail} onClose={() => setDetail(null)}>
|
||
{detail && (
|
||
<Descriptions column={1} size="small" bordered>
|
||
<Descriptions.Item label="头像">
|
||
<Avatar src={detail.avatarUrl || undefined} size={64} shape="square">
|
||
{detail.name.slice(0, 1)}
|
||
</Avatar>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="名称">{detail.name}</Descriptions.Item>
|
||
<Descriptions.Item label="角色">
|
||
{WECOM_BOT_ROLE_LABELS[detail.role] || detail.role}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="BotID">{detail.botId}</Descriptions.Item>
|
||
<Descriptions.Item label="Secret">
|
||
{detail.secretConfigured ? '已配置' : '未配置'}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="权限">
|
||
{detail.permissions.map((p) => WECOM_BOT_PERMISSION_LABELS[p] || p).join('、')}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="审批白名单">
|
||
{detail.reviewSuperAdminWecomUserIds?.length
|
||
? detail.reviewSuperAdminWecomUserIds.join('、')
|
||
: '—'}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="欢迎语">{detail.welcome || '—'}</Descriptions.Item>
|
||
<Descriptions.Item label="AI 问答">{detail.aiEnabled ? '开' : '关'}</Descriptions.Item>
|
||
<Descriptions.Item label="语言模型">{detail.llmConfigName || '—'}</Descriptions.Item>
|
||
<Descriptions.Item label="知识库">{detail.knowledgeBaseName || '—'}</Descriptions.Item>
|
||
<Descriptions.Item label="启用">{detail.enabled ? '是' : '否'}</Descriptions.Item>
|
||
<Descriptions.Item label="排序">{detail.sortOrder}</Descriptions.Item>
|
||
<Descriptions.Item label="创建">{fmtTime(detail.createdAt)}</Descriptions.Item>
|
||
<Descriptions.Item label="更新">{fmtTime(detail.updatedAt)}</Descriptions.Item>
|
||
</Descriptions>
|
||
)}
|
||
</Drawer>
|
||
</div>
|
||
);
|
||
}
|