refactor(web): 系统设置模块页面表单一致性重构
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 新增 useCrudDrawer hook 封装 CRUD Drawer 通用模式(状态管理/提交/错误处理)
- 新增 useListData hook 封装非分页列表数据获取
- 11 个页面统一迁移到 DrawerForm + 共享 hooks,消除重复代码
- 错误处理统一使用 useApiRequest.execute(),移除内联 try-catch
- Modal 全部替换为 DrawerForm,保持 UI 一致性
- 净减少 ~1300 行代码(858 增 / 2136 删)
This commit is contained in:
iven
2026-05-04 11:57:38 +08:00
parent 444dc7dd8d
commit d436888ca5
13 changed files with 960 additions and 2131 deletions

View File

@@ -0,0 +1,74 @@
import { useState, useCallback } from 'react';
import { useApiRequest } from './useApiRequest';
export interface UseCrudDrawerOptions<T> {
getId: (record: T) => string;
onCreate: (values: Record<string, unknown>) => Promise<void>;
onUpdate: (id: string, values: Record<string, unknown> & { version: number }) => Promise<void>;
onSuccess?: () => void;
}
export interface UseCrudDrawerReturn<T> {
open: boolean;
editingRecord: T | null;
initialValues: Record<string, unknown> | undefined;
openCreate: (defaults?: Record<string, unknown>) => void;
openEdit: (record: T, fieldMap?: (record: T) => Record<string, unknown>) => void;
close: () => void;
handleSubmit: (values: Record<string, unknown>) => Promise<void>;
loading: boolean;
}
export function useCrudDrawer<T extends { version: number }>(
options: UseCrudDrawerOptions<T>,
): UseCrudDrawerReturn<T> {
const [open, setOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<T | null>(null);
const [initialValues, setInitialValues] = useState<Record<string, unknown> | undefined>(undefined);
const { execute, loading } = useApiRequest();
const openCreate = useCallback((defaults?: Record<string, unknown>) => {
setEditingRecord(null);
setInitialValues(defaults);
setOpen(true);
}, []);
const openEdit = useCallback((record: T, fieldMap?: (record: T) => Record<string, unknown>) => {
setEditingRecord(record);
setInitialValues(fieldMap ? fieldMap(record) : (record as unknown as Record<string, unknown>));
setOpen(true);
}, []);
const close = useCallback(() => {
setOpen(false);
setEditingRecord(null);
setInitialValues(undefined);
}, []);
const handleSubmit = useCallback(
async (values: Record<string, unknown>) => {
if (editingRecord) {
await execute(
() => options.onUpdate(options.getId(editingRecord), { ...values, version: (editingRecord as unknown as { version: number }).version }),
'更新成功',
);
} else {
await execute(() => options.onCreate(values), '创建成功');
}
close();
options.onSuccess?.();
},
[editingRecord, options, close, execute],
);
return {
open,
editingRecord,
initialValues,
openCreate,
openEdit,
close,
handleSubmit,
loading,
};
}

View File

@@ -0,0 +1,33 @@
import { useState, useCallback, useEffect, useRef } from 'react';
export interface UseListDataReturn<T> {
data: T[];
loading: boolean;
refresh: () => Promise<void>;
}
export function useListData<T>(fetchFn: () => Promise<T[]>, autoFetch = true): UseListDataReturn<T> {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(false);
const fetchFnRef = useRef(fetchFn);
fetchFnRef.current = fetchFn;
const refresh = useCallback(async () => {
setLoading(true);
try {
const result = await fetchFnRef.current();
setData(result);
} catch {
setData([]);
}
setLoading(false);
}, []);
useEffect(() => {
if (autoFetch) {
refresh();
}
}, [refresh, autoFetch]);
return { data, loading, refresh };
}

View File

@@ -1,15 +1,13 @@
import { useEffect, useState, useCallback } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import {
Tree,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
Table,
Popconfirm,
message,
Empty,
Tag,
} from 'antd';
@@ -21,7 +19,9 @@ import {
} from '@ant-design/icons';
import type { DataNode } from 'antd/es/tree';
import { useThemeMode } from '../hooks/useThemeMode';
import { handleApiError } from '../api/client';
import { DrawerForm } from '../components/DrawerForm';
import { useCrudDrawer } from '../hooks/useCrudDrawer';
import { useApiRequest } from '../hooks/useApiRequest';
import {
listOrgTree,
createOrg,
@@ -40,6 +40,7 @@ import {
export default function Organizations() {
const isDark = useThemeMode();
const { execute } = useApiRequest();
const cardStyle = {
background: isDark ? '#111827' : '#FFFFFF',
@@ -50,7 +51,6 @@ export default function Organizations() {
// --- Org tree state ---
const [orgTree, setOrgTree] = useState<OrganizationInfo[]>([]);
const [selectedOrg, setSelectedOrg] = useState<OrganizationInfo | null>(null);
const [, setLoading] = useState(false);
// --- Department tree state ---
const [deptTree, setDeptTree] = useState<DepartmentInfo[]>([]);
@@ -59,41 +59,44 @@ export default function Organizations() {
// --- Position list state ---
const [positions, setPositions] = useState<PositionInfo[]>([]);
// --- Modal state ---
const [orgModalOpen, setOrgModalOpen] = useState(false);
const [deptModalOpen, setDeptModalOpen] = useState(false);
const [positionModalOpen, setPositionModalOpen] = useState(false);
const [editOrg, setEditOrg] = useState<OrganizationInfo | null>(null);
const [orgForm] = Form.useForm();
const [deptForm] = Form.useForm();
const [positionForm] = Form.useForm();
// --- Ref for drawer onSuccess callback (avoids before-declaration issue) ---
const refreshOrgTreeRef = useRef<() => void>(() => {});
// --- Fetch org tree ---
const fetchOrgTree = useCallback(async () => {
setLoading(true);
try {
const tree = await listOrgTree();
setOrgTree(tree);
if (selectedOrg) {
const stillExists = findOrgInTree(tree, selectedOrg.id);
if (!stillExists) {
setSelectedOrg(null);
setDeptTree([]);
setPositions([]);
}
if (!stillExists) { setSelectedOrg(null); setDeptTree([]); setPositions([]); }
}
} catch {
message.error('加载组织树失败');
}
setLoading(false);
} catch { /* silent */ }
}, [selectedOrg]);
useEffect(() => {
fetchOrgTree();
}, [fetchOrgTree]);
refreshOrgTreeRef.current = () => { fetchOrgTree(); };
// --- Fetch dept tree when org selected ---
useEffect(() => { fetchOrgTree(); }, [fetchOrgTree]);
// --- Dept Drawer ---
const [deptDrawerOpen, setDeptDrawerOpen] = useState(false);
// --- Position Drawer ---
const [positionDrawerOpen, setPositionDrawerOpen] = useState(false);
// --- Org Drawer (uses ref to avoid before-declaration) ---
const orgDrawer = useCrudDrawer<OrganizationInfo>({
getId: (r) => r.id,
onCreate: async (values) => {
await createOrg({ ...(values as { name: string; code?: string; sort_order?: number }), parent_id: selectedOrg?.id });
},
onUpdate: async (id, values) => {
await updateOrg(id, values as { name: string; code?: string; sort_order?: number; version: number });
},
onSuccess: () => { refreshOrgTreeRef.current(); },
});
// --- Fetch dept tree ---
const fetchDeptTree = useCallback(async () => {
if (!selectedOrg) return;
try {
@@ -101,209 +104,87 @@ export default function Organizations() {
setDeptTree(tree);
if (selectedDept) {
const stillExists = findDeptInTree(tree, selectedDept.id);
if (!stillExists) {
setSelectedDept(null);
setPositions([]);
}
if (!stillExists) { setSelectedDept(null); setPositions([]); }
}
} catch {
message.error('加载部门树失败');
}
} catch { /* silent */ }
}, [selectedOrg, selectedDept]);
useEffect(() => {
fetchDeptTree();
}, [fetchDeptTree]);
useEffect(() => { fetchDeptTree(); }, [fetchDeptTree]);
// --- Fetch positions when dept selected ---
// --- Fetch positions ---
const fetchPositions = useCallback(async () => {
if (!selectedDept) return;
try {
const list = await listPositions(selectedDept.id);
setPositions(list);
} catch {
message.error('加载岗位列表失败');
}
setPositions(await listPositions(selectedDept.id));
} catch { /* silent */ }
}, [selectedDept]);
useEffect(() => {
fetchPositions();
}, [fetchPositions]);
useEffect(() => { fetchPositions(); }, [fetchPositions]);
// --- Org handlers ---
const handleCreateOrg = async (values: {
name: string;
code?: string;
sort_order?: number;
}) => {
try {
if (editOrg) {
await updateOrg(editOrg.id, {
name: values.name,
code: values.code,
sort_order: values.sort_order,
version: editOrg.version,
});
message.success('组织更新成功');
} else {
await createOrg({
name: values.name,
code: values.code,
parent_id: selectedOrg?.id,
sort_order: values.sort_order,
});
message.success('组织创建成功');
}
setOrgModalOpen(false);
setEditOrg(null);
orgForm.resetFields();
fetchOrgTree();
} catch (err: unknown) {
handleApiError(err, '操作失败');
}
};
const handleDeleteOrg = async (id: string) => {
try {
await deleteOrg(id);
message.success('组织已删除');
setSelectedOrg(null);
setDeptTree([]);
setPositions([]);
fetchOrgTree();
} catch (err: unknown) {
handleApiError(err, '删除失败');
}
await execute(() => deleteOrg(id), '组织已删除');
setSelectedOrg(null); setDeptTree([]); setPositions([]);
fetchOrgTree();
};
// --- Dept handlers ---
const handleCreateDept = async (values: {
name: string;
code?: string;
sort_order?: number;
}) => {
const handleCreateDept = async (values: Record<string, unknown>) => {
if (!selectedOrg) return;
try {
await createDept(selectedOrg.id, {
name: values.name,
code: values.code,
parent_id: selectedDept?.id,
sort_order: values.sort_order,
});
message.success('部门创建成功');
setDeptModalOpen(false);
deptForm.resetFields();
fetchDeptTree();
} catch (err: unknown) {
handleApiError(err, '操作失败');
}
await execute(() => createDept(selectedOrg.id, {
name: values.name as string, code: values.code as string | undefined,
parent_id: selectedDept?.id, sort_order: values.sort_order as number | undefined,
}), '部门创建成功');
setDeptDrawerOpen(false);
fetchDeptTree();
};
const handleDeleteDept = async (id: string) => {
try {
await deleteDept(id);
message.success('部门已删除');
setSelectedDept(null);
setPositions([]);
fetchDeptTree();
} catch (err: unknown) {
handleApiError(err, '删除失败');
}
await execute(() => deleteDept(id), '部门已删除');
setSelectedDept(null); setPositions([]);
fetchDeptTree();
};
// --- Position handlers ---
const handleCreatePosition = async (values: {
name: string;
code?: string;
level?: number;
sort_order?: number;
}) => {
const handleCreatePosition = async (values: Record<string, unknown>) => {
if (!selectedDept) return;
try {
await createPosition(selectedDept.id, {
name: values.name,
code: values.code,
level: values.level,
sort_order: values.sort_order,
});
message.success('岗位创建成功');
setPositionModalOpen(false);
positionForm.resetFields();
fetchPositions();
} catch (err: unknown) {
handleApiError(err, '操作失败');
}
await execute(() => createPosition(selectedDept.id, {
name: values.name as string, code: values.code as string | undefined,
level: values.level as number | undefined, sort_order: values.sort_order as number | undefined,
}), '岗位创建成功');
setPositionDrawerOpen(false);
fetchPositions();
};
const handleDeletePosition = async (id: string) => {
try {
await deletePosition(id);
message.success('岗位已删除');
fetchPositions();
} catch {
message.error('删除失败');
}
await execute(() => deletePosition(id), '岗位已删除');
fetchPositions();
};
// --- Tree node converters ---
const convertOrgTree = (items: OrganizationInfo[]): DataNode[] =>
items.map((item) => ({
key: item.id,
title: (
<span>
{item.name}{' '}
{item.code && <Tag style={{
marginLeft: 4,
background: isDark ? '#0f172a' : '#eff6ff',
border: 'none',
color: '#2563eb',
fontSize: 11,
}}>{item.code}</Tag>}
</span>
),
title: <span>{item.name} {item.code && <Tag style={{ marginLeft: 4, background: isDark ? '#0f172a' : '#eff6ff', border: 'none', color: '#2563eb', fontSize: 11 }}>{item.code}</Tag>}</span>,
children: convertOrgTree(item.children),
}));
const convertDeptTree = (items: DepartmentInfo[]): DataNode[] =>
items.map((item) => ({
key: item.id,
title: (
<span>
{item.name}{' '}
{item.code && <Tag style={{
marginLeft: 4,
background: isDark ? '#0f172a' : '#ECFDF5',
border: 'none',
color: '#059669',
fontSize: 11,
}}>{item.code}</Tag>}
</span>
),
title: <span>{item.name} {item.code && <Tag style={{ marginLeft: 4, background: isDark ? '#0f172a' : '#ECFDF5', border: 'none', color: '#059669', fontSize: 11 }}>{item.code}</Tag>}</span>,
children: convertDeptTree(item.children),
}));
const onSelectOrg = (selectedKeys: React.Key[]) => {
if (selectedKeys.length === 0) {
setSelectedOrg(null);
setDeptTree([]);
setSelectedDept(null);
setPositions([]);
return;
}
const org = findOrgInTree(orgTree, selectedKeys[0] as string);
setSelectedOrg(org);
setSelectedDept(null);
setPositions([]);
if (selectedKeys.length === 0) { setSelectedOrg(null); setDeptTree([]); setSelectedDept(null); setPositions([]); return; }
setSelectedOrg(findOrgInTree(orgTree, selectedKeys[0] as string));
setSelectedDept(null); setPositions([]);
};
const onSelectDept = (selectedKeys: React.Key[]) => {
if (selectedKeys.length === 0) {
setSelectedDept(null);
setPositions([]);
return;
}
const dept = findDeptInTree(deptTree, selectedKeys[0] as string);
setSelectedDept(dept);
if (selectedKeys.length === 0) { setSelectedDept(null); setPositions([]); return; }
setSelectedDept(findDeptInTree(deptTree, selectedKeys[0] as string));
};
const positionColumns = [
@@ -312,16 +193,10 @@ export default function Organizations() {
{ title: '级别', dataIndex: 'level', key: 'level' },
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order' },
{
title: '操作',
key: 'actions',
title: '操作', key: 'actions',
render: (_: unknown, record: PositionInfo) => (
<Popconfirm
title="确定删除此岗位?"
onConfirm={() => handleDeletePosition(record.id)}
>
<Button size="small" type="text" danger icon={<DeleteOutlined />}>
</Button>
<Popconfirm title="确定删除此岗位?" onConfirm={() => handleDeletePosition(record.id)}>
<Button size="small" type="text" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
),
},
@@ -329,60 +204,29 @@ export default function Organizations() {
return (
<div>
{/* 页面标题 */}
<div className="erp-page-header">
<div>
<h4>
<ApartmentOutlined style={{ marginRight: 8, color: '#2563eb' }} />
</h4>
<h4><ApartmentOutlined style={{ marginRight: 8, color: '#2563eb' }} /></h4>
<div className="erp-page-subtitle"></div>
</div>
</div>
{/* 三栏布局 */}
<div style={{ display: 'flex', gap: 16, minHeight: 500 }}>
{/* 左栏:组织树 */}
<div style={{ width: 300, flexShrink: 0, ...cardStyle, overflow: 'hidden' }}>
<div style={{
padding: '14px 20px',
borderBottom: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '14px 20px', borderBottom: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<span style={{ fontWeight: 600, fontSize: 14 }}></span>
<Space size={4}>
<Button
size="small"
type="text"
icon={<PlusOutlined />}
onClick={() => {
setEditOrg(null);
orgForm.resetFields();
setOrgModalOpen(true);
}}
/>
<Button size="small" type="text" icon={<PlusOutlined />} onClick={() => orgDrawer.openCreate()} />
{selectedOrg && (
<>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => {
setEditOrg(selectedOrg);
orgForm.setFieldsValue({
name: selectedOrg.name,
code: selectedOrg.code,
sort_order: selectedOrg.sort_order,
});
setOrgModalOpen(true);
}}
/>
<Popconfirm
title="确定删除此组织?"
onConfirm={() => handleDeleteOrg(selectedOrg.id)}
>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => orgDrawer.openEdit(selectedOrg, (r) => ({
name: r.name, code: r.code, sort_order: r.sort_order,
}))} />
<Popconfirm title="确定删除此组织?" onConfirm={() => handleDeleteOrg(selectedOrg.id)}>
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
</>
@@ -391,47 +235,23 @@ export default function Organizations() {
</div>
<div style={{ padding: 12 }}>
{orgTree.length > 0 ? (
<Tree
showLine
defaultExpandAll
treeData={convertOrgTree(orgTree)}
onSelect={onSelectOrg}
selectedKeys={selectedOrg ? [selectedOrg.id] : []}
/>
) : (
<Empty description="暂无组织" />
)}
<Tree showLine defaultExpandAll treeData={convertOrgTree(orgTree)} onSelect={onSelectOrg} selectedKeys={selectedOrg ? [selectedOrg.id] : []} />
) : <Empty description="暂无组织" />}
</div>
</div>
{/* 中栏:部门树 */}
<div style={{ width: 300, flexShrink: 0, ...cardStyle, overflow: 'hidden' }}>
<div style={{
padding: '14px 20px',
borderBottom: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '14px 20px', borderBottom: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<span style={{ fontWeight: 600, fontSize: 14 }}>
{selectedOrg ? `${selectedOrg.name} · 部门` : '部门'}
</span>
<span style={{ fontWeight: 600, fontSize: 14 }}>{selectedOrg ? `${selectedOrg.name} · 部门` : '部门'}</span>
{selectedOrg && (
<Space size={4}>
<Button
size="small"
type="text"
icon={<PlusOutlined />}
onClick={() => {
deptForm.resetFields();
setDeptModalOpen(true);
}}
/>
<Button size="small" type="text" icon={<PlusOutlined />} onClick={() => setDeptDrawerOpen(true)} />
{selectedDept && (
<Popconfirm
title="确定删除此部门?"
onConfirm={() => handleDeleteDept(selectedDept.id)}
>
<Popconfirm title="确定删除此部门?" onConfirm={() => handleDeleteDept(selectedDept.id)}>
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
)}
@@ -441,157 +261,98 @@ export default function Organizations() {
<div style={{ padding: 12 }}>
{selectedOrg ? (
deptTree.length > 0 ? (
<Tree
showLine
defaultExpandAll
treeData={convertDeptTree(deptTree)}
onSelect={onSelectDept}
selectedKeys={selectedDept ? [selectedDept.id] : []}
/>
) : (
<Empty description="暂无部门" />
)
) : (
<Empty description="请先选择组织" />
)}
<Tree showLine defaultExpandAll treeData={convertDeptTree(deptTree)} onSelect={onSelectDept} selectedKeys={selectedDept ? [selectedDept.id] : []} />
) : <Empty description="暂无部门" />
) : <Empty description="请先选择组织" />}
</div>
</div>
{/* 右栏:岗位表 */}
<div style={{ flex: 1, ...cardStyle, overflow: 'hidden' }}>
<div style={{
padding: '14px 20px',
borderBottom: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '14px 20px', borderBottom: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
}}>
<span style={{ fontWeight: 600, fontSize: 14 }}>
{selectedDept ? `${selectedDept.name} · 岗位` : '岗位'}
</span>
<span style={{ fontWeight: 600, fontSize: 14 }}>{selectedDept ? `${selectedDept.name} · 岗位` : '岗位'}</span>
{selectedDept && (
<Button
size="small"
type="text"
icon={<PlusOutlined />}
onClick={() => {
positionForm.resetFields();
setPositionModalOpen(true);
}}
>
</Button>
<Button size="small" type="text" icon={<PlusOutlined />} onClick={() => setPositionDrawerOpen(true)}></Button>
)}
</div>
<div style={{ padding: '0 4px' }}>
{selectedDept ? (
<Table
columns={positionColumns}
dataSource={positions}
rowKey="id"
size="small"
pagination={false}
/>
) : (
<div style={{ padding: 24 }}>
<Empty description="请先选择部门" />
</div>
)}
<Table columns={positionColumns} dataSource={positions} rowKey="id" size="small" pagination={false} />
) : <div style={{ padding: 24 }}><Empty description="请先选择部门" /></div>}
</div>
</div>
</div>
{/* Org Modal */}
<Modal
title={editOrg ? '编辑组织' : selectedOrg ? `${selectedOrg.name} 下新建子组织` : '新建根组织'}
open={orgModalOpen}
onCancel={() => {
setOrgModalOpen(false);
setEditOrg(null);
}}
onOk={() => orgForm.submit()}
{/* Org Drawer */}
<DrawerForm
title={orgDrawer.editingRecord ? '编辑组织' : selectedOrg ? `${selectedOrg.name} 下新建子组织` : '新建根组织'}
open={orgDrawer.open}
onClose={orgDrawer.close}
onSubmit={orgDrawer.handleSubmit}
initialValues={orgDrawer.initialValues}
loading={orgDrawer.loading}
width={480}
columns={1}
>
<Form form={orgForm} onFinish={handleCreateOrg} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入组织名称' }]}
>
<Input />
</Form.Item>
<Form.Item name="code" label="编码">
<Input />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入组织名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码"><Input /></Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</DrawerForm>
{/* Dept Modal */}
<Modal
title={
selectedDept
? `${selectedDept.name} 下新建子部门`
: `${selectedOrg?.name} 下新建部门`
}
open={deptModalOpen}
onCancel={() => setDeptModalOpen(false)}
onOk={() => deptForm.submit()}
{/* Dept Drawer */}
<DrawerForm
title={selectedDept ? `${selectedDept.name} 下新建子部门` : `${selectedOrg?.name} 下新建部门`}
open={deptDrawerOpen}
onClose={() => setDeptDrawerOpen(false)}
onSubmit={handleCreateDept}
initialValues={{ sort_order: 0 }}
loading={false}
width={480}
columns={1}
>
<Form form={deptForm} onFinish={handleCreateDept} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入部门名称' }]}
>
<Input />
</Form.Item>
<Form.Item name="code" label="编码">
<Input />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入部门名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码"><Input /></Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</DrawerForm>
{/* Position Modal */}
<Modal
{/* Position Drawer */}
<DrawerForm
title={`${selectedDept?.name} 下新建岗位`}
open={positionModalOpen}
onCancel={() => setPositionModalOpen(false)}
onOk={() => positionForm.submit()}
open={positionDrawerOpen}
onClose={() => setPositionDrawerOpen(false)}
onSubmit={handleCreatePosition}
initialValues={{ level: 1, sort_order: 0 }}
loading={false}
width={480}
columns={1}
>
<Form form={positionForm} onFinish={handleCreatePosition} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="name"
label="岗位名称"
rules={[{ required: true, message: '请输入岗位名称' }]}
>
<Input />
</Form.Item>
<Form.Item name="code" label="编码">
<Input />
</Form.Item>
<Form.Item name="level" label="级别" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="name" label="岗位名称" rules={[{ required: true, message: '请输入岗位名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码"><Input /></Form.Item>
<Form.Item name="level" label="级别" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</DrawerForm>
</div>
);
}
// --- Helpers ---
function findOrgInTree(
tree: OrganizationInfo[],
id: string,
): OrganizationInfo | null {
function findOrgInTree(tree: OrganizationInfo[], id: string): OrganizationInfo | null {
for (const item of tree) {
if (item.id === id) return item;
const found = findOrgInTree(item.children, id);
@@ -600,10 +361,7 @@ function findOrgInTree(
return null;
}
function findDeptInTree(
tree: DepartmentInfo[],
id: string,
): DepartmentInfo | null {
function findDeptInTree(tree: DepartmentInfo[], id: string): DepartmentInfo | null {
for (const item of tree) {
if (item.id === id) return item;
const found = findDeptInTree(item.children, id);

View File

@@ -1,15 +1,13 @@
import { useEffect, useState, useCallback } from 'react';
import { useState, useEffect } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
Tag,
Popconfirm,
Checkbox,
message,
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import {
@@ -23,78 +21,48 @@ import {
type RoleInfo,
type PermissionInfo,
} from '../api/roles';
import { handleApiError } from '../api/client';
import { PageContainer } from '../components/PageContainer';
import { DrawerForm } from '../components/DrawerForm';
import { useCrudDrawer } from '../hooks/useCrudDrawer';
import { useApiRequest } from '../hooks/useApiRequest';
import { useThemeMode } from '../hooks/useThemeMode';
import { useListData } from '../hooks/useListData';
export default function Roles() {
const [roles, setRoles] = useState<RoleInfo[]>([]);
const isDark = useThemeMode();
const { execute } = useApiRequest();
const { data: roles, loading, refresh } = useListData<RoleInfo>(async () => {
const result = await listRoles();
return result.data;
});
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
const [loading, setLoading] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editRole, setEditRole] = useState<RoleInfo | null>(null);
const [permModalOpen, setPermModalOpen] = useState(false);
const [permDrawerOpen, setPermDrawerOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
const [form] = Form.useForm();
const isDark = useThemeMode();
const fetchRoles = useCallback(async () => {
setLoading(true);
try {
const result = await listRoles();
setRoles(result.data);
} catch {
message.error('加载角色失败');
}
setLoading(false);
}, []);
const fetchPermissions = useCallback(async () => {
try {
setPermissions(await listPermissions());
} catch {
// 静默处理
}
}, []);
useEffect(() => {
fetchRoles();
fetchPermissions();
}, [fetchRoles, fetchPermissions]);
listPermissions().then(setPermissions).catch(() => {});
}, []);
const handleCreate = async (values: {
name: string;
code: string;
description?: string;
}) => {
try {
if (editRole) {
await updateRole(editRole.id, { ...values, version: editRole.version });
message.success('角色更新成功');
} else {
await createRole(values);
message.success('角色创建成功');
}
setCreateModalOpen(false);
setEditRole(null);
form.resetFields();
fetchRoles();
} catch (err: unknown) {
handleApiError(err, '操作失败');
}
};
const roleDrawer = useCrudDrawer<RoleInfo>({
getId: (r) => r.id,
onCreate: async (values) => {
await createRole(values as unknown as { name: string; code: string; description?: string });
},
onUpdate: async (id, values) => {
await updateRole(id, values as unknown as { name: string; code: string; description?: string; version: number });
},
onSuccess: refresh,
});
const handleDelete = async (id: string) => {
try {
await deleteRole(id);
message.success('角色已删除');
fetchRoles();
} catch {
message.error('删除失败');
}
await execute(() => deleteRole(id), '角色已删除');
refresh();
};
const openPermModal = async (role: RoleInfo) => {
const openPermDrawer = async (role: RoleInfo) => {
setSelectedRole(role);
try {
const rolePerms = await getRolePermissions(role.id);
@@ -102,64 +70,26 @@ export default function Roles() {
} catch {
setSelectedPermIds([]);
}
setPermModalOpen(true);
setPermDrawerOpen(true);
};
const savePermissions = async () => {
if (!selectedRole) return;
try {
await assignPermissions(selectedRole.id, selectedPermIds);
message.success('权限分配成功');
setPermModalOpen(false);
} catch {
message.error('权限分配失败');
}
};
const openEditModal = (role: RoleInfo) => {
setEditRole(role);
form.setFieldsValue({
name: role.name,
code: role.code,
description: role.description,
});
setCreateModalOpen(true);
};
const openCreateModal = () => {
setEditRole(null);
form.resetFields();
setCreateModalOpen(true);
};
const closeCreateModal = () => {
setCreateModalOpen(false);
setEditRole(null);
form.resetFields();
await execute(() => assignPermissions(selectedRole.id, selectedPermIds), '权限分配成功');
setPermDrawerOpen(false);
};
const columns = [
{
title: '角色名称',
dataIndex: 'name',
key: 'name',
title: '角色名称', dataIndex: 'name', key: 'name',
render: (v: string, record: RoleInfo) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div
style={{
width: 32,
height: 32,
borderRadius: 8,
background: record.is_system
? 'linear-gradient(135deg, #2563eb, #60a5fa)'
: isDark ? '#0f172a' : '#f8fafc',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: record.is_system ? '#fff' : isDark ? '#94a3b8' : '#475569',
fontSize: 14,
}}
>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: record.is_system ? 'linear-gradient(135deg, #2563eb, #60a5fa)' : isDark ? '#0f172a' : '#f8fafc',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: record.is_system ? '#fff' : isDark ? '#94a3b8' : '#475569', fontSize: 14,
}}>
<SafetyCertificateOutlined />
</div>
<span style={{ fontWeight: 500 }}>{v}</span>
@@ -167,82 +97,33 @@ export default function Roles() {
),
},
{
title: '编码',
dataIndex: 'code',
key: 'code',
render: (v: string) => (
<Tag style={{
background: isDark ? '#0f172a' : '#f8fafc',
border: 'none',
color: isDark ? '#94a3b8' : '#475569',
fontFamily: 'monospace',
fontSize: 12,
}}>
{v}
</Tag>
),
title: '编码', dataIndex: 'code', key: 'code',
render: (v: string) => <Tag style={{ background: isDark ? '#0f172a' : '#f8fafc', border: 'none', color: isDark ? '#94a3b8' : '#475569', fontFamily: 'monospace', fontSize: 12 }}>{v}</Tag>,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
render: (v: string | undefined) => (
<span style={{ color: isDark ? '#475569' : '#94a3b8' }}>{v || '-'}</span>
),
title: '描述', dataIndex: 'description', key: 'description', ellipsis: true,
render: (v?: string) => <span style={{ color: isDark ? '#475569' : '#94a3b8' }}>{v || '-'}</span>,
},
{
title: '类型',
dataIndex: 'is_system',
key: 'is_system',
width: 100,
title: '类型', dataIndex: 'is_system', key: 'is_system', width: 100,
render: (v: boolean) => (
<Tag
style={{
color: v ? '#2563eb' : (isDark ? '#94a3b8' : '#475569'),
background: v ? '#eff6ff' : (isDark ? '#0f172a' : '#f8fafc'),
border: 'none',
fontWeight: 500,
}}
>
<Tag style={{ color: v ? '#2563eb' : isDark ? '#94a3b8' : '#475569', background: v ? '#eff6ff' : isDark ? '#0f172a' : '#f8fafc', border: 'none', fontWeight: 500 }}>
{v ? '系统' : '自定义'}
</Tag>
),
},
{
title: '操作',
key: 'actions',
width: 180,
title: '操作', key: 'actions', width: 180,
render: (_: unknown, record: RoleInfo) => (
<Space size={4}>
<Button
size="small"
type="text"
icon={<SafetyCertificateOutlined />}
onClick={() => openPermModal(record)}
style={{ color: '#2563eb' }}
>
</Button>
<Button size="small" type="text" icon={<SafetyCertificateOutlined />} onClick={() => openPermDrawer(record)} style={{ color: '#2563eb' }}></Button>
{!record.is_system && (
<>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => openEditModal(record)}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Popconfirm
title="确定删除此角色?"
onConfirm={() => handleDelete(record.id)}
>
<Button
size="small"
type="text"
icon={<DeleteOutlined />}
danger
/>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => roleDrawer.openEdit(record, (r) => ({
name: r.name, code: r.code, description: r.description,
}))} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
<Popconfirm title="确定删除此角色?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
</>
)}
@@ -251,100 +132,67 @@ export default function Roles() {
},
];
const groupedPermissions = permissions.reduce<Record<string, PermissionInfo[]>>(
(acc, p) => {
if (!acc[p.resource]) acc[p.resource] = [];
acc[p.resource].push(p);
return acc;
},
{},
);
const groupedPermissions = permissions.reduce<Record<string, PermissionInfo[]>>((acc, p) => {
if (!acc[p.resource]) acc[p.resource] = [];
acc[p.resource].push(p);
return acc;
}, {});
return (
<div>
{/* 页面标题和工具栏 */}
<div className="erp-page-header">
<div>
<h4></h4>
<div className="erp-page-subtitle"></div>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
</div>
<PageContainer
title="角色管理"
subtitle="管理系统角色和权限分配"
actions={<Button type="primary" icon={<PlusOutlined />} onClick={() => roleDrawer.openCreate()}></Button>}
>
<Table
columns={columns}
dataSource={roles}
rowKey="id"
loading={loading}
pagination={{ pageSize: 20, showTotal: (t) => `${t} 条记录` }}
/>
{/* 表格容器 */}
<div style={{
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
overflow: 'hidden',
}}>
<Table
columns={columns}
dataSource={roles}
rowKey="id"
loading={loading}
pagination={{ pageSize: 20, showTotal: (t) => `${t} 条记录` }}
/>
</div>
{/* 新建/编辑角色弹窗 */}
<Modal
title={editRole ? '编辑角色' : '新建角色'}
open={createModalOpen}
onCancel={closeCreateModal}
onOk={() => form.submit()}
{/* 新建/编辑角色 Drawer */}
<DrawerForm
title={roleDrawer.editingRecord ? '编辑角色' : '新建角色'}
open={roleDrawer.open}
onClose={roleDrawer.close}
onSubmit={roleDrawer.handleSubmit}
initialValues={roleDrawer.initialValues}
loading={roleDrawer.loading}
width={480}
columns={1}
>
<Form form={form} onFinish={handleCreate} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="code"
label="编码"
rules={[{ required: true, message: '请输入角色编码' }]}
>
<Input disabled={!!editRole} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入角色名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码" rules={[{ required: true, message: '请输入角色编码' }]}>
<Input disabled={!!roleDrawer.editingRecord} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
</DrawerForm>
{/* 权限分配弹窗 */}
<Modal
{/* 权限分配 Drawer */}
<DrawerForm
title={`权限分配 - ${selectedRole?.name || ''}`}
open={permModalOpen}
onCancel={() => setPermModalOpen(false)}
onOk={savePermissions}
open={permDrawerOpen}
onClose={() => setPermDrawerOpen(false)}
onSubmit={savePermissions}
initialValues={{}}
loading={false}
width={600}
columns={1}
>
<div style={{ marginTop: 8 }}>
{Object.entries(groupedPermissions).map(([resource, perms]) => (
<div
key={resource}
style={{
marginBottom: 16,
padding: 16,
borderRadius: 10,
border: `1px solid ${isDark ? '#0f172a' : '#E2E8F0'}`,
background: isDark ? '#0B0F1A' : '#f1f5f9',
}}
>
<div style={{
fontWeight: 600,
marginBottom: 12,
textTransform: 'capitalize',
color: isDark ? '#E2E8F0' : '#334155',
fontSize: 14,
}}>
<div key={resource} style={{
marginBottom: 16, padding: 16, borderRadius: 10,
border: `1px solid ${isDark ? '#0f172a' : '#E2E8F0'}`,
background: isDark ? '#0B0F1A' : '#f1f5f9',
}}>
<div style={{ fontWeight: 600, marginBottom: 12, textTransform: 'capitalize', color: isDark ? '#E2E8F0' : '#334155', fontSize: 14 }}>
{resource}
</div>
<Checkbox.Group
@@ -353,19 +201,13 @@ export default function Roles() {
style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}
>
{perms.map((p) => (
<Checkbox
key={p.id}
value={p.id}
style={{ marginRight: 0 }}
>
{p.name}
</Checkbox>
<Checkbox key={p.id} value={p.id} style={{ marginRight: 0 }}>{p.name}</Checkbox>
))}
</Checkbox.Group>
</div>
))}
</div>
</Modal>
</div>
</DrawerForm>
</PageContainer>
);
}

View File

@@ -1,19 +1,16 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import { useState, useCallback, useEffect } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
Tag,
Popconfirm,
Checkbox,
message,
} from 'antd';
import {
PlusOutlined,
SearchOutlined,
EditOutlined,
DeleteOutlined,
UserOutlined,
@@ -32,311 +29,132 @@ import {
} from '../api/users';
import { listRoles, type RoleInfo } from '../api/roles';
import type { UserInfo } from '../api/auth';
import { handleApiError } from '../api/client';
import { PageContainer } from '../components/PageContainer';
import { DrawerForm } from '../components/DrawerForm';
import { useCrudDrawer } from '../hooks/useCrudDrawer';
import { usePaginatedData } from '../hooks/usePaginatedData';
import { useApiRequest } from '../hooks/useApiRequest';
import { useThemeMode } from '../hooks/useThemeMode';
const STATUS_COLOR_MAP: Record<string, string> = {
active: '#059669',
disabled: '#dc2626',
locked: '#d97706',
};
const STATUS_BG_MAP: Record<string, string> = {
active: '#ECFDF5',
disabled: '#FEF2F2',
locked: '#FFFBEB',
};
const STATUS_LABEL_MAP: Record<string, string> = {
active: '正常',
disabled: '禁用',
locked: '锁定',
};
const STATUS_COLOR_MAP: Record<string, string> = { active: '#059669', disabled: '#dc2626', locked: '#d97706' };
const STATUS_BG_MAP: Record<string, string> = { active: '#ECFDF5', disabled: '#FEF2F2', locked: '#FFFBEB' };
const STATUS_LABEL_MAP: Record<string, string> = { active: '正常', disabled: '禁用', locked: '锁定' };
export default function Users() {
const [users, setUsers] = useState<UserInfo[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editUser, setEditUser] = useState<UserInfo | null>(null);
const [roleModalOpen, setRoleModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserInfo | null>(null);
const [allRoles, setAllRoles] = useState<RoleInfo[]>([]);
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
const [form] = Form.useForm();
const isDark = useThemeMode();
const { execute } = useApiRequest();
const fetchUsers = useCallback(async (p = page) => {
setLoading(true);
try {
const result = await listUsers(p, 20, searchText);
setUsers(result.data);
setTotal(result.total);
} catch {
message.error('加载用户列表失败');
}
setLoading(false);
}, [page, searchText]);
const {
data: users, total, page, loading, refresh,
} = usePaginatedData<UserInfo>(async (p, pageSize, search) => {
const result = await listUsers(p, pageSize, search);
return { data: result.data, total: result.total };
}, 20);
// 搜索防抖:输入后 300ms 才触发查询
const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const debouncedSearch = useCallback((_text: string) => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
debounceTimer.current = setTimeout(() => {
setPage(1);
}, 300);
}, []);
const [allRoles, setAllRoles] = useState<RoleInfo[]>([]);
const [roleDrawerOpen, setRoleDrawerOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserInfo | null>(null);
const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);
const fetchRoles = useCallback(async () => {
try {
const result = await listRoles();
setAllRoles(result.data);
} catch {
// 静默处理
}
} catch { /* silent */ }
}, []);
useEffect(() => {
fetchUsers();
fetchRoles();
}, [fetchUsers, fetchRoles]);
useEffect(() => { fetchRoles(); }, [fetchRoles]);
const handleCreateOrEdit = async (values: {
username: string;
password?: string;
display_name?: string;
email?: string;
phone?: string;
}) => {
try {
if (editUser) {
const req: UpdateUserRequest = {
display_name: values.display_name,
email: values.email,
phone: values.phone,
version: editUser.version,
};
await updateUser(editUser.id, req);
message.success('用户更新成功');
} else {
const req: CreateUserRequest = {
username: values.username,
password: values.password ?? '',
display_name: values.display_name,
email: values.email,
phone: values.phone,
};
await createUser(req);
message.success('用户创建成功');
}
setCreateModalOpen(false);
setEditUser(null);
form.resetFields();
fetchUsers();
} catch (err: unknown) {
handleApiError(err, '操作失败');
}
};
const userDrawer = useCrudDrawer<UserInfo>({
getId: (r) => r.id,
onCreate: async (values) => {
await createUser(values as unknown as CreateUserRequest);
},
onUpdate: async (id, values) => {
await updateUser(id, values as unknown as UpdateUserRequest);
},
onSuccess: refresh,
});
const handleDelete = async (id: string) => {
try {
await deleteUser(id);
message.success('用户已删除');
fetchUsers();
} catch {
message.error('删除失败');
}
await execute(() => deleteUser(id), '用户已删除');
refresh();
};
const handleToggleStatus = async (id: string, status: string) => {
try {
const user = users.find(u => u.id === id);
if (!user) return;
await updateUser(id, { status, version: user.version });
message.success(status === 'disabled' ? '用户已禁用' : '用户已启用');
fetchUsers();
} catch {
message.error('状态更新失败');
}
const user = users.find(u => u.id === id);
if (!user) return;
await execute(() => updateUser(id, { status, version: user.version }), status === 'disabled' ? '用户已禁用' : '用户已启用');
refresh();
};
const handleAssignRoles = async () => {
if (!selectedUser) return;
try {
await assignRoles(selectedUser.id, selectedRoleIds);
message.success('角色分配成功');
setRoleModalOpen(false);
fetchUsers();
} catch {
message.error('角色分配失败');
}
await execute(() => assignRoles(selectedUser.id, selectedRoleIds), '角色分配成功');
setRoleDrawerOpen(false);
refresh();
};
const openCreateModal = () => {
setEditUser(null);
form.resetFields();
setCreateModalOpen(true);
};
const openEditModal = (user: UserInfo) => {
setEditUser(user);
form.setFieldsValue({
username: user.username,
display_name: user.display_name,
email: user.email,
phone: user.phone,
});
setCreateModalOpen(true);
};
const closeCreateModal = () => {
setCreateModalOpen(false);
setEditUser(null);
form.resetFields();
};
const openRoleModal = (user: UserInfo) => {
const openRoleDrawer = (user: UserInfo) => {
setSelectedUser(user);
setSelectedRoleIds(user.roles.map((r) => r.id));
setRoleModalOpen(true);
setRoleDrawerOpen(true);
};
const filteredUsers = users;
const columns = [
{
title: '用户',
dataIndex: 'username',
key: 'username',
title: '用户', dataIndex: 'username', key: 'username',
render: (v: string, record: UserInfo) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div
style={{
width: 32,
height: 32,
borderRadius: 8,
background: 'linear-gradient(135deg, #2563eb, #60a5fa)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: 13,
fontWeight: 600,
}}
>
<div style={{
width: 32, height: 32, borderRadius: 8,
background: 'linear-gradient(135deg, #2563eb, #60a5fa)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 13, fontWeight: 600,
}}>
{(record.display_name?.[0] || v?.[0] || 'U').toUpperCase()}
</div>
<div>
<div style={{ fontWeight: 500, fontSize: 14 }}>{v}</div>
{record.display_name && (
<div style={{ fontSize: 12, color: isDark ? '#475569' : '#94a3b8' }}>
{record.display_name}
</div>
)}
{record.display_name && <div style={{ fontSize: 12, color: isDark ? '#475569' : '#94a3b8' }}>{record.display_name}</div>}
</div>
</div>
),
},
{ title: '邮箱', dataIndex: 'email', key: 'email', render: (v?: string) => v || '-' },
{ title: '电话', dataIndex: 'phone', key: 'phone', render: (v?: string) => v || '-' },
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
render: (v: string | undefined) => v || '-',
},
{
title: '电话',
dataIndex: 'phone',
key: 'phone',
render: (v: string | undefined) => v || '-',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
title: '状态', dataIndex: 'status', key: 'status', width: 100,
render: (status: string) => (
<Tag
style={{
color: STATUS_COLOR_MAP[status] || '#62625b',
background: STATUS_BG_MAP[status] || '#f8fafc',
border: 'none',
fontWeight: 500,
}}
>
<Tag style={{ color: STATUS_COLOR_MAP[status] || '#62625b', background: STATUS_BG_MAP[status] || '#f8fafc', border: 'none', fontWeight: 500 }}>
{STATUS_LABEL_MAP[status] || status}
</Tag>
),
},
{
title: '角色',
dataIndex: 'roles',
key: 'roles',
title: '角色', dataIndex: 'roles', key: 'roles',
render: (roles: RoleInfo[]) =>
roles.length > 0
? roles.map((r) => (
<Tag key={r.id} style={{
background: isDark ? '#0f172a' : '#f8fafc',
border: 'none',
color: isDark ? '#CBD5E1' : '#475569',
}}>
{r.name}
</Tag>
))
? roles.map((r) => <Tag key={r.id} style={{ background: isDark ? '#0f172a' : '#f8fafc', border: 'none', color: isDark ? '#CBD5E1' : '#475569' }}>{r.name}</Tag>)
: <span style={{ color: isDark ? '#475569' : '#CBD5E1' }}>-</span>,
},
{
title: '操作',
key: 'actions',
width: 240,
title: '操作', key: 'actions', width: 240,
render: (_: unknown, record: UserInfo) => (
<Space size={4}>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => openEditModal(record)}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Button
size="small"
type="text"
icon={<SafetyCertificateOutlined />}
onClick={() => openRoleModal(record)}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => userDrawer.openEdit(record, (r) => ({
username: r.username, display_name: r.display_name, email: r.email, phone: r.phone,
}))} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
<Button size="small" type="text" icon={<SafetyCertificateOutlined />} onClick={() => openRoleDrawer(record)} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
{record.status === 'active' ? (
<Popconfirm
title="确定禁用此用户?"
onConfirm={() => handleToggleStatus(record.id, 'disabled')}
>
<Button
size="small"
type="text"
icon={<StopOutlined />}
danger
/>
<Popconfirm title="确定禁用此用户?" onConfirm={() => handleToggleStatus(record.id, 'disabled')}>
<Button size="small" type="text" icon={<StopOutlined />} danger />
</Popconfirm>
) : (
<Button
size="small"
type="text"
icon={<CheckCircleOutlined />}
onClick={() => handleToggleStatus(record.id, 'active')}
style={{ color: '#059669' }}
/>
<Button size="small" type="text" icon={<CheckCircleOutlined />} onClick={() => handleToggleStatus(record.id, 'active')} style={{ color: '#059669' }} />
)}
<Popconfirm
title="确定删除此用户?"
onConfirm={() => handleDelete(record.id)}
>
<Button
size="small"
type="text"
icon={<DeleteOutlined />}
danger
/>
<Popconfirm title="确定删除此用户?" onConfirm={() => handleDelete(record.id)}>
<Button size="small" type="text" icon={<DeleteOutlined />} danger />
</Popconfirm>
</Space>
),
@@ -344,112 +162,59 @@ export default function Users() {
];
return (
<div>
{/* 页面标题和工具栏 */}
<div className="erp-page-header">
<div>
<h4></h4>
<div className="erp-page-subtitle"></div>
</div>
<Space size={8}>
<Input
placeholder="搜索用户名..."
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
debouncedSearch(e.target.value);
}}
allowClear
style={{ width: 220, borderRadius: 8 }}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={openCreateModal}
>
</Button>
</Space>
</div>
<PageContainer
title="用户管理"
subtitle="管理系统用户账户、角色分配和状态"
actions={<Button type="primary" icon={<PlusOutlined />} onClick={() => userDrawer.openCreate()}></Button>}
>
<Table
columns={columns}
dataSource={users}
rowKey="id"
loading={loading}
pagination={{
current: page, total, pageSize: 20,
onChange: (p) => refresh(p),
showTotal: (t) => `${t} 条记录`,
}}
/>
{/* 表格容器 */}
<div style={{
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
overflow: 'hidden',
}}>
<Table
columns={columns}
dataSource={filteredUsers}
rowKey="id"
loading={loading}
pagination={{
current: page,
total,
pageSize: 20,
onChange: (p) => {
setPage(p);
fetchUsers(p);
},
showTotal: (t) => `${t} 条记录`,
style: { padding: '12px 16px', margin: 0 },
}}
/>
</div>
{/* 新建/编辑用户弹窗 */}
<Modal
title={editUser ? '编辑用户' : '新建用户'}
open={createModalOpen}
onCancel={closeCreateModal}
onOk={() => form.submit()}
{/* 新建/编辑用户 Drawer */}
<DrawerForm
title={userDrawer.editingRecord ? '编辑用户' : '新建用户'}
open={userDrawer.open}
onClose={userDrawer.close}
onSubmit={userDrawer.handleSubmit}
initialValues={userDrawer.initialValues}
loading={userDrawer.loading}
width={480}
columns={1}
>
<Form form={form} onFinish={handleCreateOrEdit} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="username"
label="用户名"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input prefix={<UserOutlined style={{ color: '#94a3b8' }} />} disabled={!!editUser} />
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input prefix={<UserOutlined style={{ color: '#94a3b8' }} />} disabled={!!userDrawer.editingRecord} />
</Form.Item>
{!userDrawer.editingRecord && (
<Form.Item name="password" label="密码" rules={[{ required: true, message: '请输入密码' }, { min: 6, message: '密码至少6位' }]}>
<Input.Password />
</Form.Item>
{!editUser && (
<Form.Item
name="password"
label="密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码至少6位' },
]}
>
<Input.Password />
</Form.Item>
)}
<Form.Item name="display_name" label="显示名">
<Input />
</Form.Item>
<Form.Item
name="email"
label="邮箱"
rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}
>
<Input type="email" />
</Form.Item>
<Form.Item name="phone" label="电话">
<Input />
</Form.Item>
</Form>
</Modal>
)}
<Form.Item name="display_name" label="显示名"><Input /></Form.Item>
<Form.Item name="email" label="邮箱" rules={[{ type: 'email', message: '请输入有效的邮箱地址' }]}>
<Input type="email" />
</Form.Item>
<Form.Item name="phone" label="电话"><Input /></Form.Item>
</DrawerForm>
{/* 角色分配弹窗 */}
<Modal
{/* 角色分配 Drawer */}
<DrawerForm
title={`分配角色 - ${selectedUser?.username || ''}`}
open={roleModalOpen}
onCancel={() => setRoleModalOpen(false)}
onOk={handleAssignRoles}
width={480}
open={roleDrawerOpen}
onClose={() => setRoleDrawerOpen(false)}
onSubmit={async () => { await handleAssignRoles(); }}
initialValues={{}}
loading={false}
width={520}
columns={1}
>
<div style={{ marginTop: 8 }}>
<Checkbox.Group
@@ -458,26 +223,20 @@ export default function Users() {
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
>
{allRoles.map((r) => (
<div
key={r.id}
style={{
padding: '10px 14px',
borderRadius: 8,
border: `1px solid ${isDark ? '#0f172a' : '#E2E8F0'}`,
background: isDark ? '#0B0F1A' : '#f1f5f9',
}}
>
<div key={r.id} style={{
padding: '10px 14px', borderRadius: 8,
border: `1px solid ${isDark ? '#0f172a' : '#E2E8F0'}`,
background: isDark ? '#0B0F1A' : '#f1f5f9',
}}>
<Checkbox value={r.id}>
<span style={{ fontWeight: 500 }}>{r.name}</span>
<span style={{ color: isDark ? '#475569' : '#94a3b8', marginLeft: 8, fontSize: 12 }}>
{r.code}
</span>
<span style={{ color: isDark ? '#475569' : '#94a3b8', marginLeft: 8, fontSize: 12 }}>{r.code}</span>
</Checkbox>
</div>
))}
</Checkbox.Group>
</div>
</Modal>
</div>
</DrawerForm>
</PageContainer>
);
}

View File

@@ -1,33 +1,23 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Table, Select, Input, Tag, message } from 'antd';
import { useEffect, useRef } from 'react';
import { Table, Select, Input, Tag } from 'antd';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import { listAuditLogs, type AuditLogItem, type AuditLogQuery } from '../../api/auditLogs';
import { listUsers } from '../../api/users';
import { useThemeMode } from '../../hooks/useThemeMode';
import { usePaginatedData } from '../../hooks/usePaginatedData';
const RESOURCE_TYPE_OPTIONS = [
{ value: 'user', label: '用户' },
{ value: 'role', label: '角色' },
{ value: 'position', label: '岗位' },
{ value: 'organization', label: '组织' },
{ value: 'department', label: '部门' },
{ value: 'process_instance', label: '流程实例' },
{ value: 'process_definition', label: '流程定义' },
{ value: 'task', label: '流程任务' },
{ value: 'dictionary', label: '字典' },
{ value: 'menu', label: '菜单' },
{ value: 'setting', label: '设置' },
{ value: 'numbering_rule', label: '编号规则' },
{ value: 'patient', label: '患者' },
{ value: 'patient_tag', label: '患者标签' },
{ value: 'patient_family_member', label: '家庭成员' },
{ value: 'patient_doctor_relation', label: '医患关系' },
{ value: 'points_transaction', label: '积分流水' },
{ value: 'points_product', label: '积分商品' },
{ value: 'points_order', label: '积分订单' },
{ value: 'points_rule', label: '积分规则' },
{ value: 'offline_event', label: '线下活动' },
{ value: 'offline_event_registration', label: '活动签到' },
{ value: 'user', label: '用户' }, { value: 'role', label: '角色' },
{ value: 'position', label: '岗位' }, { value: 'organization', label: '组织' },
{ value: 'department', label: '部门' }, { value: 'process_instance', label: '流程实例' },
{ value: 'process_definition', label: '流程定义' }, { value: 'task', label: '流程任务' },
{ value: 'dictionary', label: '字典' }, { value: 'menu', label: '菜单' },
{ value: 'setting', label: '设置' }, { value: 'numbering_rule', label: '编号规则' },
{ value: 'patient', label: '患者' }, { value: 'patient_tag', label: '患者标签' },
{ value: 'patient_family_member', label: '家庭成员' }, { value: 'patient_doctor_relation', label: '医患关系' },
{ value: 'points_transaction', label: '积分流水' }, { value: 'points_product', label: '积分商品' },
{ value: 'points_order', label: '积分订单' }, { value: 'points_rule', label: '积分规则' },
{ value: 'offline_event', label: '线下活动' }, { value: 'offline_event_registration', label: '活动签到' },
];
const ACTION_STYLES: Record<string, { bg: string; color: string; text: string }> = {
@@ -38,37 +28,25 @@ const ACTION_STYLES: Record<string, { bg: string; color: string; text: string }>
function formatDateTime(value: string): string {
return new Date(value).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
});
}
export default function AuditLogViewer() {
const [logs, setLogs] = useState<AuditLogItem[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState<AuditLogQuery>({ page: 1, page_size: 20 });
const isDark = useThemeMode();
const userNameCache = useRef<Record<string, string>>({});
const cacheLoaded = useRef(false);
const fetchLogs = useCallback(async (params: AuditLogQuery) => {
setLoading(true);
try {
const result = await listAuditLogs(params);
setLogs(result.data);
setTotal(result.total);
} catch {
message.error('加载审计日志失败');
}
setLoading(false);
}, []);
const { data: logs, total, page, loading, filters, setFilters, refresh } = usePaginatedData<AuditLogItem, AuditLogQuery>(
async (p, pageSize, query) => {
const result = await listAuditLogs({ ...query, page: p, page_size: pageSize });
return { data: result.data, total: result.total };
},
{ pageSize: 20, defaultFilters: { page: 1, page_size: 20 } as unknown as AuditLogQuery, autoFetch: false },
);
// 加载用户名称缓存(分页遍历所有用户)
// Load user name cache
useEffect(() => {
if (cacheLoaded.current) return;
let cancelled = false;
@@ -85,109 +63,45 @@ export default function AuditLogViewer() {
hasMore = result.data.length >= pageSize;
currentPage += 1;
}
if (!cancelled) {
cacheLoaded.current = true;
}
} catch {
// 静默失败,将显示 UUID
}
if (!cancelled) cacheLoaded.current = true;
} catch { /* silent */ }
};
loadAllUsers();
return () => { cancelled = true; };
}, []);
useEffect(() => {
fetchLogs(query);
}, [query, fetchLogs]);
useEffect(() => { refresh(1); }, []);
const handleFilterChange = (field: keyof AuditLogQuery, value: string | undefined) => {
setQuery((prev) => ({
...prev,
[field]: value || undefined,
page: 1,
}));
};
const handleTableChange = (pagination: TablePaginationConfig) => {
setQuery((prev) => ({
...prev,
page: pagination.current,
page_size: pagination.pageSize,
}));
setFilters((prev) => ({ ...prev, [field]: value || undefined, page: 1 }));
};
const columns: ColumnsType<AuditLogItem> = [
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 100,
title: '操作', dataIndex: 'action', key: 'action', width: 100,
render: (action: string) => {
const info = ACTION_STYLES[action] || { bg: '#f8fafc', color: '#475569', text: action };
return (
<Tag style={{
background: info.bg,
border: 'none',
color: info.color,
fontWeight: 500,
}}>
{info.text}
</Tag>
);
return <Tag style={{ background: info.bg, border: 'none', color: info.color, fontWeight: 500 }}>{info.text}</Tag>;
},
},
{
title: '资源类型',
dataIndex: 'resource_type',
key: 'resource_type',
width: 120,
render: (v: string) => (
<Tag style={{
background: isDark ? '#0f172a' : '#f8fafc',
border: 'none',
color: isDark ? '#CBD5E1' : '#475569',
}}>
{v}
</Tag>
),
title: '资源类型', dataIndex: 'resource_type', key: 'resource_type', width: 120,
render: (v: string) => <Tag style={{ background: isDark ? '#0f172a' : '#f8fafc', border: 'none', color: isDark ? '#CBD5E1' : '#475569' }}>{v}</Tag>,
},
{
title: '资源 ID',
dataIndex: 'resource_id',
key: 'resource_id',
width: 200,
ellipsis: true,
render: (v: string) => (
<span style={{ fontFamily: 'monospace', fontSize: 12, color: isDark ? '#94a3b8' : '#475569' }}>
{v}
</span>
),
title: '资源 ID', dataIndex: 'resource_id', key: 'resource_id', width: 200, ellipsis: true,
render: (v: string) => <span style={{ fontFamily: 'monospace', fontSize: 12, color: isDark ? '#94a3b8' : '#475569' }}>{v}</span>,
},
{
title: '操作用户',
dataIndex: 'user_id',
key: 'user_id',
width: 200,
ellipsis: true,
title: '操作用户', dataIndex: 'user_id', key: 'user_id', width: 200, ellipsis: true,
render: (v: string) => {
const name = userNameCache.current[v];
return (
<span title={v} style={{ fontSize: 13, color: isDark ? '#CBD5E1' : '#334155' }}>
{name || v}
</span>
);
return <span title={v} style={{ fontSize: 13, color: isDark ? '#CBD5E1' : '#334155' }}>{name || v}</span>;
},
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
render: (value: string) => (
<span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>
{formatDateTime(value)}
</span>
),
title: '时间', dataIndex: 'created_at', key: 'created_at', width: 180,
render: (value: string) => <span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>{formatDateTime(value)}</span>,
},
];
@@ -195,54 +109,35 @@ export default function AuditLogViewer() {
<div>
{/* 筛选工具栏 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 16,
padding: 12,
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 10,
display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16, padding: 12,
background: isDark ? '#111827' : '#FFFFFF', borderRadius: 10,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
}}>
<Select
allowClear
placeholder="资源类型"
style={{ width: 160 }}
allowClear placeholder="资源类型" style={{ width: 160 }}
options={RESOURCE_TYPE_OPTIONS}
value={query.resource_type}
value={filters.resource_type}
onChange={(value) => handleFilterChange('resource_type', value)}
/>
<Input
allowClear
placeholder="操作用户 ID"
style={{ width: 240 }}
value={query.user_id ?? ''}
allowClear placeholder="操作用户 ID" style={{ width: 240 }}
value={filters.user_id ?? ''}
onChange={(e) => handleFilterChange('user_id', e.target.value)}
/>
<span style={{ fontSize: 13, color: isDark ? '#475569' : '#94a3b8', marginLeft: 'auto' }}>
{total}
</span>
<span style={{ fontSize: 13, color: isDark ? '#475569' : '#94a3b8', marginLeft: 'auto' }}> {total} </span>
</div>
{/* 表格 */}
<div style={{
background: isDark ? '#111827' : '#FFFFFF',
borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
overflow: 'hidden',
background: isDark ? '#111827' : '#FFFFFF', borderRadius: 12,
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`, overflow: 'hidden',
}}>
<Table
rowKey="id"
columns={columns}
dataSource={logs}
loading={loading}
onChange={handleTableChange}
rowKey="id" columns={columns} dataSource={logs} loading={loading}
onChange={(pagination: TablePaginationConfig) => refresh(pagination.current)}
pagination={{
current: query.page,
pageSize: query.page_size,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
current: page, pageSize: 20, total,
showSizeChanger: true, showTotal: (t) => `${t}`,
}}
scroll={{ x: 900 }}
/>

View File

@@ -1,13 +1,12 @@
import { Form, Input, Button, message, Card, Typography } from 'antd';
import { Form, Input, Button, Card, Typography } from 'antd';
import { LockOutlined } from '@ant-design/icons';
import { useAuthStore } from '../../stores/auth';
import { changePassword } from '../../api/auth';
import { useNavigate } from 'react-router-dom';
const { Title } = Typography;
import { useApiRequest } from '../../hooks/useApiRequest';
export default function ChangePassword() {
const [messageApi, contextHolder] = message.useMessage();
const { execute } = useApiRequest();
const logout = useAuthStore((s) => s.logout);
const navigate = useNavigate();
const [form] = Form.useForm();
@@ -18,84 +17,53 @@ export default function ChangePassword() {
confirm_password: string;
}) => {
if (values.new_password !== values.confirm_password) {
messageApi.error('两次输入的新密码不一致');
return;
}
try {
await execute(async () => {
await changePassword(values.current_password, values.new_password);
messageApi.success('密码修改成功,请重新登录');
await logout();
navigate('/login');
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '密码修改失败';
messageApi.error(errorMsg);
}
}, '密码修改成功,请重新登录', '密码修改失败');
};
return (
<Card style={{ maxWidth: 480, margin: '0 auto' }}>
{contextHolder}
<Title level={4} style={{ marginBottom: 24 }}>
</Title>
<Typography.Title level={4} style={{ marginBottom: 24 }}></Typography.Title>
<Form form={form} layout="vertical" onFinish={onFinish}>
<Form.Item
name="current_password"
label="当前密码"
rules={[{ required: true, message: '请输入当前密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入当前密码"
/>
<Form.Item name="current_password" label="当前密码" rules={[{ required: true, message: '请输入当前密码' }]}>
<Input.Password prefix={<LockOutlined />} placeholder="请输入当前密码" />
</Form.Item>
<Form.Item
name="new_password"
label="新密码"
name="new_password" label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 8, message: '密码长度不能少于8位' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('current_password') !== value) {
return Promise.resolve();
}
if (!value || getFieldValue('current_password') !== value) return Promise.resolve();
return Promise.reject(new Error('新密码不能与当前密码相同'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入新密码至少8位"
/>
<Input.Password prefix={<LockOutlined />} placeholder="请输入新密码至少8位" />
</Form.Item>
<Form.Item
name="confirm_password"
label="确认新密码"
name="confirm_password" label="确认新密码"
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('new_password') === value) {
return Promise.resolve();
}
if (!value || getFieldValue('new_password') === value) return Promise.resolve();
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请再次输入新密码"
/>
<Input.Password prefix={<LockOutlined />} placeholder="请再次输入新密码" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
</Button>
<Button type="primary" htmlType="submit" block></Button>
</Form.Item>
</Form>
</Card>

View File

@@ -1,14 +1,12 @@
import { useEffect, useState, useCallback } from 'react';
import { useState } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
Popconfirm,
message,
Typography,
Tag,
} from 'antd';
@@ -27,100 +25,46 @@ import {
type CreateDictionaryItemRequest,
type UpdateDictionaryItemRequest,
} from '../../api/dictionaries';
// --- Types ---
import { useListData } from '../../hooks/useListData';
import { useCrudDrawer } from '../../hooks/useCrudDrawer';
import { DrawerForm } from '../../components/DrawerForm';
import { useApiRequest } from '../../hooks/useApiRequest';
type DictItem = DictionaryItemInfo;
type Dictionary = DictionaryInfo;
// --- Component ---
export default function DictionaryManager() {
const [dictionaries, setDictionaries] = useState<Dictionary[]>([]);
const [loading, setLoading] = useState(false);
const [dictModalOpen, setDictModalOpen] = useState(false);
const [editDict, setEditDict] = useState<Dictionary | null>(null);
const [itemModalOpen, setItemModalOpen] = useState(false);
const { data: dictionaries, loading, refresh } = useListData<Dictionary>(async () => {
const result = await listDictionaries();
return Array.isArray(result) ? result : result.data ?? [];
});
const { execute } = useApiRequest();
// 字典 CRUD Drawer
const dictDrawer = useCrudDrawer<Dictionary>({
getId: (r) => r.id,
onCreate: async (values) => {
await createDictionary(values as unknown as CreateDictionaryRequest);
},
onUpdate: async (id, values) => {
await updateDictionary(id, values as unknown as CreateDictionaryRequest & { version: number });
},
onSuccess: refresh,
});
// 字典项 Drawer — 因需要 activeDictId手写状态管理
const [itemDrawerOpen, setItemDrawerOpen] = useState(false);
const [activeDictId, setActiveDictId] = useState<string | null>(null);
const [editItem, setEditItem] = useState<DictItem | null>(null);
const [dictForm] = Form.useForm();
const [itemForm] = Form.useForm();
const fetchDictionaries = useCallback(async () => {
setLoading(true);
try {
const result = await listDictionaries();
setDictionaries(Array.isArray(result) ? result : result.data ?? []);
} catch {
message.error('加载字典列表失败');
}
setLoading(false);
}, []);
useEffect(() => {
fetchDictionaries();
}, [fetchDictionaries]);
// --- Dictionary CRUD ---
const handleDictSubmit = async (values: CreateDictionaryRequest) => {
try {
if (editDict) {
await updateDictionary(editDict.id, { ...values, version: editDict.version });
message.success('字典更新成功');
} else {
await createDictionary(values);
message.success('字典创建成功');
}
closeDictModal();
fetchDictionaries();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const handleDeleteDict = async (id: string, version: number) => {
try {
await deleteDictionary(id, version);
message.success('字典已删除');
fetchDictionaries();
} catch {
message.error('删除失败');
}
};
const openEditDict = (dict: Dictionary) => {
setEditDict(dict);
dictForm.setFieldsValue({
name: dict.name,
code: dict.code,
description: dict.description,
});
setDictModalOpen(true);
};
const openCreateDict = () => {
setEditDict(null);
dictForm.resetFields();
setDictModalOpen(true);
};
const closeDictModal = () => {
setDictModalOpen(false);
setEditDict(null);
dictForm.resetFields();
};
// --- Dictionary Item CRUD ---
const openAddItem = (dictId: string) => {
setActiveDictId(dictId);
setEditItem(null);
itemForm.resetFields();
setItemModalOpen(true);
itemForm.setFieldsValue({ sort_order: 0 });
setItemDrawerOpen(true);
};
const openEditItem = (dictId: string, item: DictItem) => {
@@ -132,75 +76,56 @@ export default function DictionaryManager() {
sort_order: item.sort_order,
color: item.color,
});
setItemModalOpen(true);
setItemDrawerOpen(true);
};
const handleItemSubmit = async (values: CreateDictionaryItemRequest & { sort_order: number }) => {
if (!activeDictId) return;
try {
if (editItem) {
await updateDictionaryItem(activeDictId, editItem.id, { ...values, version: editItem.version } as UpdateDictionaryItemRequest);
message.success('字典项更新成功');
} else {
await createDictionaryItem(activeDictId, values);
message.success('字典项添加成功');
}
closeItemModal();
fetchDictionaries();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const handleDeleteItem = async (dictId: string, itemId: string, version: number) => {
try {
await deleteDictionaryItem(dictId, itemId, version);
message.success('字典项已删除');
fetchDictionaries();
} catch {
message.error('删除失败');
}
};
const closeItemModal = () => {
setItemModalOpen(false);
const closeItemDrawer = () => {
setItemDrawerOpen(false);
setActiveDictId(null);
setEditItem(null);
itemForm.resetFields();
};
// --- Columns ---
const handleItemSubmit = async (values: Record<string, unknown>) => {
if (!activeDictId) return;
const itemValues = values as unknown as CreateDictionaryItemRequest & { sort_order: number };
if (editItem) {
await execute(
() => updateDictionaryItem(activeDictId, editItem.id, { ...itemValues, version: editItem.version } as UpdateDictionaryItemRequest),
'字典项更新成功',
);
} else {
await execute(() => createDictionaryItem(activeDictId, itemValues), '字典项添加成功');
}
closeItemDrawer();
refresh();
};
const handleDeleteDict = async (id: string, version: number) => {
await execute(() => deleteDictionary(id, version), '字典已删除');
refresh();
};
const handleDeleteItem = async (dictId: string, itemId: string, version: number) => {
await execute(() => deleteDictionaryItem(dictId, itemId, version), '字典项已删除');
refresh();
};
const columns = [
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '编码', dataIndex: 'code', key: 'code' },
{
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{ title: '说明', dataIndex: 'description', key: 'description', ellipsis: true },
{
title: '操作',
key: 'actions',
render: (_: unknown, record: Dictionary) => (
<Space>
<Button size="small" onClick={() => openAddItem(record.id)}>
</Button>
<Button size="small" onClick={() => openEditDict(record)}>
</Button>
<Popconfirm
title="确定删除此字典?"
onConfirm={() => handleDeleteDict(record.id, record.version)}
>
<Button size="small" danger>
</Button>
<Button size="small" onClick={() => openAddItem(record.id)}></Button>
<Button size="small" onClick={() => dictDrawer.openEdit(record, (r) => ({
name: r.name, code: r.code, description: r.description,
}))}></Button>
<Popconfirm title="确定删除此字典?" onConfirm={() => handleDeleteDict(record.id, record.version)}>
<Button size="small" danger></Button>
</Popconfirm>
</Space>
),
@@ -212,31 +137,17 @@ export default function DictionaryManager() {
{ title: '值', dataIndex: 'value', key: 'value' },
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 80 },
{
title: '颜色',
dataIndex: 'color',
key: 'color',
width: 80,
render: (color?: string) =>
color ? <Tag color={color}>{color}</Tag> : '-',
title: '颜色', dataIndex: 'color', key: 'color', width: 80,
render: (color?: string) => color ? <Tag color={color}>{color}</Tag> : '-',
},
{
title: '操作',
key: 'actions',
render: (_: unknown, record: DictItem) => (
<Space>
<Button
size="small"
onClick={() => openEditItem(dictId, record)}
>
</Button>
<Popconfirm
title="确定删除此字典项?"
onConfirm={() => handleDeleteItem(dictId, record.id, record.version)}
>
<Button size="small" danger>
</Button>
<Button size="small" onClick={() => openEditItem(dictId, record)}></Button>
<Popconfirm title="确定删除此字典项?" onConfirm={() => handleDeleteItem(dictId, record.id, record.version)}>
<Button size="small" danger></Button>
</Popconfirm>
</Space>
),
@@ -245,17 +156,9 @@ export default function DictionaryManager() {
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 16,
}}
>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateDict}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Typography.Title level={5} style={{ margin: 0 }}></Typography.Title>
<Button type="primary" icon={<PlusOutlined />} onClick={() => dictDrawer.openCreate()}>
</Button>
</div>
@@ -279,68 +182,52 @@ export default function DictionaryManager() {
}}
/>
{/* Dictionary Modal */}
<Modal
title={editDict ? '编辑字典' : '新建字典'}
open={dictModalOpen}
onCancel={closeDictModal}
onOk={() => dictForm.submit()}
{/* 字典 Drawer */}
<DrawerForm
title={dictDrawer.editingRecord ? '编辑字典' : '新建字典'}
open={dictDrawer.open}
onClose={dictDrawer.close}
onSubmit={dictDrawer.handleSubmit}
initialValues={dictDrawer.initialValues}
loading={dictDrawer.loading}
width={480}
columns={1}
>
<Form form={dictForm} onFinish={handleDictSubmit} layout="vertical">
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入字典名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="code"
label="编码"
rules={[{ required: true, message: '请输入字典编码' }]}
>
<Input disabled={!!editDict} />
</Form.Item>
<Form.Item name="description" label="说明">
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入字典名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码" rules={[{ required: true, message: '请输入字典编码' }]}>
<Input disabled={!!dictDrawer.editingRecord} />
</Form.Item>
<Form.Item name="description" label="说明">
<Input.TextArea rows={3} />
</Form.Item>
</DrawerForm>
{/* Dictionary Item Modal */}
<Modal
{/* 字典项 Drawer */}
<DrawerForm
title={editItem ? '编辑字典项' : '添加字典项'}
open={itemModalOpen}
onCancel={closeItemModal}
onOk={() => itemForm.submit()}
open={itemDrawerOpen}
onClose={closeItemDrawer}
onSubmit={handleItemSubmit}
initialValues={editItem ? { label: editItem.label, value: editItem.value, sort_order: editItem.sort_order, color: editItem.color } : { sort_order: 0 }}
loading={false}
width={480}
columns={1}
>
<Form form={itemForm} onFinish={handleItemSubmit} layout="vertical">
<Form.Item
name="label"
label="标签"
rules={[{ required: true, message: '请输入标签' }]}
>
<Input />
</Form.Item>
<Form.Item
name="value"
label="值"
rules={[{ required: true, message: '请输入值' }]}
>
<Input />
</Form.Item>
<Form.Item
name="sort_order"
label="排序"
initialValue={0}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="color" label="颜色">
<Input placeholder="如blue, red, green 或十六进制色值" />
</Form.Item>
</Form>
</Modal>
<Form.Item name="label" label="标签" rules={[{ required: true, message: '请输入标签' }]}>
<Input />
</Form.Item>
<Form.Item name="value" label="值" rules={[{ required: true, message: '请输入值' }]}>
<Input />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="color" label="颜色">
<Input placeholder="如blue, red, green 或十六进制色值" />
</Form.Item>
</DrawerForm>
</div>
);
}

View File

@@ -1,13 +1,10 @@
import { useEffect, useState, useCallback } from 'react';
import { useState } from 'react';
import {
Table,
Switch,
Modal,
Button,
Space,
Typography,
message,
Card,
Form,
Input,
} from 'antd';
@@ -17,113 +14,53 @@ import {
updateLanguage,
type LanguageInfo,
} from '../../api/languages';
import { useListData } from '../../hooks/useListData';
import { DrawerForm } from '../../components/DrawerForm';
import { useApiRequest } from '../../hooks/useApiRequest';
export default function LanguageManager() {
const [languages, setLanguages] = useState<LanguageInfo[]>([]);
const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const { data: languages, loading, refresh } = useListData<LanguageInfo>(listLanguages);
const { execute } = useApiRequest();
// LanguageInfo 没有 version/id 字段,手动管理 drawer 状态
const [drawerOpen, setDrawerOpen] = useState(false);
const [editingLang, setEditingLang] = useState<LanguageInfo | null>(null);
const [form] = Form.useForm<{ name: string }>();
const fetchLanguages = useCallback(async () => {
setLoading(true);
try {
const result = await listLanguages();
setLanguages(result);
} catch {
message.error('加载语言列表失败');
}
setLoading(false);
}, []);
useEffect(() => {
fetchLanguages();
}, [fetchLanguages]);
const handleToggle = async (record: LanguageInfo, checked: boolean) => {
try {
await updateLanguage(record.code, { is_active: checked });
setLanguages((prev) =>
prev.map((lang) =>
lang.code === record.code ? { ...lang, is_active: checked } : lang,
),
);
message.success(checked ? '已启用' : '已禁用');
} catch {
message.error('操作失败');
}
await execute(() => updateLanguage(record.code, { is_active: checked, name: record.name }), checked ? '已启用' : '已禁用');
refresh();
};
const openEdit = (lang: LanguageInfo) => {
setEditingLang(lang);
form.setFieldsValue({ name: lang.name });
setEditModalOpen(true);
setDrawerOpen(true);
};
const closeEdit = () => {
setEditModalOpen(false);
setEditingLang(null);
form.resetFields();
};
const handleEditSubmit = async () => {
const handleEditSubmit = async (values: Record<string, unknown>) => {
if (!editingLang) return;
try {
const values = await form.validateFields();
const updated = await updateLanguage(editingLang.code, {
is_active: editingLang.is_active,
name: values.name,
});
setLanguages((prev) =>
prev.map((lang) =>
lang.code === editingLang.code ? updated : lang,
),
);
message.success('语言更新成功');
closeEdit();
} catch (err: unknown) {
if (err && typeof err === 'object' && 'errorFields' in err) return;
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '更新失败';
message.error(errorMsg);
}
await execute(
() => updateLanguage(editingLang.code, { is_active: editingLang.is_active, name: values.name as string }),
'语言更新成功',
);
setDrawerOpen(false);
setEditingLang(null);
refresh();
};
const columns = [
{ title: '语言代码', dataIndex: 'code', key: 'code', width: 160 },
{ title: '语言名称', dataIndex: 'name', key: 'name', width: 200 },
{
title: '语言代码',
dataIndex: 'code',
key: 'code',
width: 160,
},
{
title: '语言名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '状态',
dataIndex: 'is_active',
key: 'is_active',
width: 120,
title: '状态', dataIndex: 'is_active', key: 'is_active', width: 120,
render: (is_active: boolean, record: LanguageInfo) => (
<Switch checked={is_active} onChange={(checked) => handleToggle(record, checked)} />
),
},
{
title: '操作',
key: 'actions',
title: '操作', key: 'actions',
render: (_: unknown, record: LanguageInfo) => (
<Space>
<Button
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(record)}></Button>
</Space>
),
},
@@ -131,40 +68,33 @@ export default function LanguageManager() {
return (
<div>
<Typography.Title level={5} style={{ marginBottom: 16 }}>
</Typography.Title>
<Typography.Title level={5} style={{ marginBottom: 16 }}></Typography.Title>
<Card>
<Table
columns={columns}
dataSource={languages}
rowKey="code"
loading={loading}
pagination={false}
/>
</Card>
<Table
columns={columns}
dataSource={languages}
rowKey="code"
loading={loading}
pagination={false}
/>
<Modal
<DrawerForm
title={`编辑语言 - ${editingLang?.code ?? ''}`}
open={editModalOpen}
onCancel={closeEdit}
onOk={handleEditSubmit}
okText="保存"
open={drawerOpen}
onClose={() => { setDrawerOpen(false); setEditingLang(null); }}
onSubmit={handleEditSubmit}
initialValues={editingLang ? { name: editingLang.name } : undefined}
loading={false}
width={480}
columns={1}
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item label="语言代码">
<Input value={editingLang?.code} disabled />
</Form.Item>
<Form.Item
label="语言名称"
name="name"
rules={[{ required: true, message: '请输入语言名称' }]}
>
<Input placeholder="例如:简体中文" maxLength={100} />
</Form.Item>
</Form>
</Modal>
<Form.Item label="语言代码">
<Input value={editingLang?.code} disabled />
</Form.Item>
<Form.Item label="语言名称" name="name" rules={[{ required: true, message: '请输入语言名称' }]}>
<Input placeholder="例如:简体中文" maxLength={100} />
</Form.Item>
</DrawerForm>
</div>
);
}

View File

@@ -1,9 +1,7 @@
import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
@@ -11,7 +9,6 @@ import {
Switch,
TreeSelect,
Popconfirm,
message,
Typography,
Tag,
} from 'antd';
@@ -24,190 +21,80 @@ import {
type MenuInfo,
type MenuItemReq,
} from '../../api/menus';
// --- Types ---
import { useListData } from '../../hooks/useListData';
import { useCrudDrawer } from '../../hooks/useCrudDrawer';
import { DrawerForm } from '../../components/DrawerForm';
import { useApiRequest } from '../../hooks/useApiRequest';
type MenuItem = MenuInfo;
// --- Helpers ---
/** Convert nested menu tree back to flat list */
function flattenMenuTree(tree: MenuItem[]): MenuItem[] {
const result: MenuItem[] = [];
const walk = (items: MenuItem[]) => {
for (const item of items) {
const { children, ...rest } = item;
result.push(rest as MenuItem);
if (children?.length) walk(children);
}
};
walk(tree);
return result;
}
/** Convert menu tree to TreeSelect data nodes */
function toTreeSelectData(
items: MenuItem[],
): Array<{ title: string; value: string; children?: Array<{ title: string; value: string }> }> {
function toTreeSelectData(items: MenuItem[]): Array<{ title: string; value: string; children?: Array<{ title: string; value: string }> }> {
return items.map((item) => ({
title: item.title,
value: item.id,
children:
item.children && item.children.length > 0
? toTreeSelectData(item.children)
: undefined,
children: item.children?.length ? toTreeSelectData(item.children) : undefined,
}));
}
const menuTypeLabels: Record<string, { text: string; color: string }> = {
const MENU_TYPE_LABELS: Record<string, { text: string; color: string }> = {
directory: { text: '目录', color: 'blue' },
menu: { text: '菜单', color: 'green' },
button: { text: '按钮', color: 'orange' },
};
// --- Component ---
export default function MenuConfig() {
const [_menus, setMenus] = useState<MenuItem[]>([]);
const [menuTree, setMenuTree] = useState<MenuItem[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editMenu, setEditMenu] = useState<MenuItem | null>(null);
const [form] = Form.useForm();
const { data: menuTree, loading, refresh } = useListData<MenuItem>(getMenus);
const fetchMenus = useCallback(async () => {
setLoading(true);
try {
const tree = await getMenus();
setMenus(flattenMenuTree(tree));
setMenuTree(tree);
} catch {
message.error('加载菜单失败');
}
setLoading(false);
}, []);
const { execute } = useApiRequest();
useEffect(() => {
fetchMenus();
}, [fetchMenus]);
const handleSubmit = async (values: MenuItemReq) => {
try {
if (editMenu) {
await updateMenu(editMenu.id, { ...values, version: editMenu.version });
message.success('菜单更新成功');
} else {
await createMenu(values);
message.success('菜单创建成功');
}
closeModal();
fetchMenus();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const drawer = useCrudDrawer<MenuItem>({
getId: (r) => r.id,
onCreate: async (values) => {
await createMenu(values as unknown as MenuItemReq);
},
onUpdate: async (id, values) => {
await updateMenu(id, values as unknown as MenuItemReq & { version: number });
},
onSuccess: refresh,
});
const handleDelete = async (id: string, version: number) => {
try {
await deleteMenu(id, version);
message.success('菜单已删除');
fetchMenus();
} catch {
message.error('删除失败');
}
};
const openCreate = () => {
setEditMenu(null);
form.resetFields();
form.setFieldsValue({
menu_type: 'menu',
sort_order: 0,
visible: true,
});
setModalOpen(true);
};
const openEdit = (menu: MenuItem) => {
setEditMenu(menu);
form.setFieldsValue({
parent_id: menu.parent_id || undefined,
title: menu.title,
path: menu.path,
icon: menu.icon,
menu_type: menu.menu_type,
sort_order: menu.sort_order,
visible: menu.visible,
permission: menu.permission,
});
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
setEditMenu(null);
form.resetFields();
await execute(() => deleteMenu(id, version), '菜单已删除');
refresh();
};
const columns = [
{ title: '标题', dataIndex: 'title', key: 'title', width: 200 },
{ title: '路径', dataIndex: 'path', key: 'path', ellipsis: true, render: (v?: string) => v || '-' },
{ title: '图标', dataIndex: 'icon', key: 'icon', width: 100, render: (v?: string) => v || '-' },
{
title: '路径',
dataIndex: 'path',
key: 'path',
ellipsis: true,
render: (v?: string) => v || '-',
},
{
title: '图标',
dataIndex: 'icon',
key: 'icon',
width: 100,
render: (v?: string) => v || '-',
},
{
title: '类型',
dataIndex: 'menu_type',
key: 'menu_type',
width: 90,
title: '类型', dataIndex: 'menu_type', key: 'menu_type', width: 90,
render: (v: string) => {
const info = menuTypeLabels[v] ?? { text: v, color: 'default' };
const info = MENU_TYPE_LABELS[v] ?? { text: v, color: 'default' };
return <Tag color={info.color}>{info.text}</Tag>;
},
},
{ title: '排序', dataIndex: 'sort_order', key: 'sort_order', width: 80 },
{
title: '排序',
dataIndex: 'sort_order',
key: 'sort_order',
width: 80,
title: '可见', dataIndex: 'visible', key: 'visible', width: 80,
render: (v: boolean) => v ? <Tag color="green"></Tag> : <Tag color="default"></Tag>,
},
{
title: '可见',
dataIndex: 'visible',
key: 'visible',
width: 80,
render: (v: boolean) =>
v ? <Tag color="green"></Tag> : <Tag color="default"></Tag>,
},
{
title: '操作',
key: 'actions',
width: 150,
title: '操作', key: 'actions', width: 150,
render: (_: unknown, record: MenuItem) => (
<Space>
<Button size="small" onClick={() => openEdit(record)}>
</Button>
<Popconfirm
title="确定删除此菜单?"
onConfirm={() => handleDelete(record.id, record.version)}
>
<Button size="small" danger>
</Button>
<Button size="small" onClick={() => drawer.openEdit(record, (r) => ({
parent_id: r.parent_id || undefined,
title: r.title,
path: r.path,
icon: r.icon,
menu_type: r.menu_type,
sort_order: r.sort_order,
visible: r.visible,
permission: r.permission,
}))}></Button>
<Popconfirm title="确定删除此菜单?" onConfirm={() => handleDelete(record.id, record.version)}>
<Button size="small" danger></Button>
</Popconfirm>
</Space>
),
@@ -216,17 +103,13 @@ export default function MenuConfig() {
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 16,
}}
>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Typography.Title level={5} style={{ margin: 0 }}></Typography.Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => drawer.openCreate({ menu_type: 'menu', sort_order: 0, visible: true })}
>
</Button>
</div>
@@ -240,59 +123,50 @@ export default function MenuConfig() {
indentSize={20}
/>
<Modal
title={editMenu ? '编辑菜单' : '添加菜单'}
open={modalOpen}
onCancel={closeModal}
onOk={() => form.submit()}
width={560}
<DrawerForm
title={drawer.editingRecord ? '编辑菜单' : '添加菜单'}
open={drawer.open}
onClose={drawer.close}
onSubmit={drawer.handleSubmit}
initialValues={drawer.initialValues}
loading={drawer.loading}
width={520}
columns={1}
>
<Form form={form} onFinish={handleSubmit} layout="vertical">
<Form.Item name="parent_id" label="上级菜单">
<TreeSelect
treeData={toTreeSelectData(menuTree)}
placeholder="无(顶级菜单)"
allowClear
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item
name="title"
label="标题"
rules={[{ required: true, message: '请输入菜单标题' }]}
>
<Input />
</Form.Item>
<Form.Item name="path" label="路径">
<Input placeholder="/example/path" />
</Form.Item>
<Form.Item name="icon" label="图标">
<Input placeholder="图标名称,如 HomeOutlined" />
</Form.Item>
<Form.Item
name="menu_type"
label="类型"
rules={[{ required: true, message: '请选择菜单类型' }]}
>
<Select
options={[
{ label: '目录', value: 'directory' },
{ label: '菜单', value: 'menu' },
{ label: '按钮', value: 'button' },
]}
/>
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="visible" label="可见" valuePropName="checked" initialValue>
<Switch />
</Form.Item>
<Form.Item name="permission" label="权限标识">
<Input placeholder="如 system:user:list" />
</Form.Item>
</Form>
</Modal>
<Form.Item name="parent_id" label="上级菜单">
<TreeSelect
treeData={toTreeSelectData(menuTree)}
placeholder="无(顶级菜单)"
allowClear
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入菜单标题' }]}>
<Input />
</Form.Item>
<Form.Item name="path" label="路径">
<Input placeholder="/example/path" />
</Form.Item>
<Form.Item name="icon" label="图标">
<Input placeholder="图标名称,如 HomeOutlined" />
</Form.Item>
<Form.Item name="menu_type" label="类型" rules={[{ required: true, message: '请选择菜单类型' }]}>
<Select options={[
{ label: '目录', value: 'directory' },
{ label: '菜单', value: 'menu' },
{ label: '按钮', value: 'button' },
]} />
</Form.Item>
<Form.Item name="sort_order" label="排序" initialValue={0}>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="visible" label="可见" valuePropName="checked" initialValue>
<Switch />
</Form.Item>
<Form.Item name="permission" label="权限标识">
<Input placeholder="如 system:user:list" />
</Form.Item>
</DrawerForm>
</div>
);
}

View File

@@ -1,15 +1,12 @@
import { useEffect, useState, useCallback } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
InputNumber,
Select,
Popconfirm,
message,
Typography,
} from 'antd';
import { PlusOutlined, NumberOutlined } from '@ant-design/icons';
@@ -23,123 +20,57 @@ import {
type CreateNumberingRuleRequest,
type UpdateNumberingRuleRequest,
} from '../../api/numberingRules';
// --- Types ---
import { useListData } from '../../hooks/useListData';
import { useCrudDrawer } from '../../hooks/useCrudDrawer';
import { DrawerForm } from '../../components/DrawerForm';
import { useApiRequest } from '../../hooks/useApiRequest';
import { message } from 'antd';
type NumberingRule = NumberingRuleInfo;
// --- Constants ---
const resetCycleOptions = [
const RESET_CYCLE_OPTIONS = [
{ label: '不重置', value: 'never' },
{ label: '每天', value: 'daily' },
{ label: '每月', value: 'monthly' },
{ label: '每年', value: 'yearly' },
];
const resetCycleLabels: Record<string, string> = {
const RESET_CYCLE_LABELS: Record<string, string> = {
never: '不重置',
daily: '每天',
monthly: '每月',
yearly: '每年',
};
// --- Component ---
export default function NumberingRules() {
const [rules, setRules] = useState<NumberingRule[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [editRule, setEditRule] = useState<NumberingRule | null>(null);
const [form] = Form.useForm();
const { data: rules, loading, refresh } = useListData<NumberingRule>(async () => {
const result = await listNumberingRules();
return Array.isArray(result) ? result : result.data ?? [];
});
const fetchRules = useCallback(async () => {
setLoading(true);
try {
const result = await listNumberingRules();
setRules(Array.isArray(result) ? result : result.data ?? []);
} catch {
message.error('加载编号规则失败');
}
setLoading(false);
}, []);
const { execute } = useApiRequest();
useEffect(() => {
fetchRules();
}, [fetchRules]);
const handleSubmit = async (values: CreateNumberingRuleRequest) => {
try {
if (editRule) {
await updateNumberingRule(editRule.id, { ...values, version: editRule.version } as UpdateNumberingRuleRequest);
message.success('编号规则更新成功');
} else {
await createNumberingRule(values);
message.success('编号规则创建成功');
}
closeModal();
fetchRules();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const drawer = useCrudDrawer<NumberingRule>({
getId: (r) => r.id,
onCreate: async (values) => {
await createNumberingRule(values as unknown as CreateNumberingRuleRequest);
},
onUpdate: async (id, values) => {
await updateNumberingRule(id, values as unknown as UpdateNumberingRuleRequest);
},
onSuccess: refresh,
});
const handleDelete = async (id: string, version: number) => {
try {
await deleteNumberingRule(id, version);
message.success('编号规则已删除');
fetchRules();
} catch {
message.error('删除失败');
}
await execute(() => deleteNumberingRule(id, version), '删除成功');
refresh();
};
const handleGenerate = async (rule: NumberingRule) => {
try {
await execute(async () => {
const result = await generateNumber(rule.id);
message.success(`生成编号: ${result.number}`);
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '生成编号失败';
message.error(errorMsg);
}
};
const openCreate = () => {
setEditRule(null);
form.resetFields();
form.setFieldsValue({
seq_length: 4,
seq_start: 1,
separator: '-',
reset_cycle: 'never',
});
setModalOpen(true);
};
const openEdit = (rule: NumberingRule) => {
setEditRule(rule);
form.setFieldsValue({
name: rule.name,
code: rule.code,
prefix: rule.prefix,
date_format: rule.date_format,
seq_length: rule.seq_length,
seq_start: rule.seq_start,
separator: rule.separator,
reset_cycle: rule.reset_cycle,
});
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
setEditRule(null);
form.resetFields();
}, undefined, '生成编号失败');
};
const columns = [
@@ -174,7 +105,7 @@ export default function NumberingRules() {
dataIndex: 'reset_cycle',
key: 'reset_cycle',
width: 100,
render: (v: string) => resetCycleLabels[v] ?? v,
render: (v: string) => RESET_CYCLE_LABELS[v] ?? v,
},
{
title: '操作',
@@ -188,16 +119,23 @@ export default function NumberingRules() {
>
</Button>
<Button size="small" onClick={() => openEdit(record)}>
<Button size="small" onClick={() => drawer.openEdit(record, (r) => ({
name: r.name,
code: r.code,
prefix: r.prefix,
date_format: r.date_format,
seq_length: r.seq_length,
seq_start: r.seq_start,
separator: r.separator,
reset_cycle: r.reset_cycle,
}))}>
</Button>
<Popconfirm
title="确定删除此编号规则?"
onConfirm={() => handleDelete(record.id, record.version)}
>
<Button size="small" danger>
</Button>
<Button size="small" danger></Button>
</Popconfirm>
</Space>
),
@@ -206,17 +144,17 @@ export default function NumberingRules() {
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 16,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => drawer.openCreate({
seq_length: 4, seq_start: 1, separator: '-', reset_cycle: 'never',
})}
>
</Button>
</div>
@@ -229,56 +167,41 @@ export default function NumberingRules() {
pagination={{ pageSize: 20 }}
/>
<Modal
title={editRule ? '编辑编号规则' : '新建编号规则'}
open={modalOpen}
onCancel={closeModal}
onOk={() => form.submit()}
width={560}
<DrawerForm
title={drawer.editingRecord ? '编辑编号规则' : '新建编号规则'}
open={drawer.open}
onClose={drawer.close}
onSubmit={drawer.handleSubmit}
initialValues={drawer.initialValues}
loading={drawer.loading}
width={520}
columns={1}
>
<Form form={form} onFinish={handleSubmit} layout="vertical">
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入规则名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="code"
label="编码"
rules={[{ required: true, message: '请输入规则编码' }]}
>
<Input disabled={!!editRule} />
</Form.Item>
<Form.Item name="prefix" label="前缀">
<Input placeholder="如 PO、SO" />
</Form.Item>
<Form.Item name="date_format" label="日期格式">
<Input placeholder="如 YYYYMMDD" />
</Form.Item>
<Form.Item name="separator" label="分隔符">
<Input placeholder="默认 -" />
</Form.Item>
<Form.Item
name="seq_length"
label="序列长度"
rules={[{ required: true, message: '请输入序列长度' }]}
>
<InputNumber min={1} max={20} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="seq_start" label="起始值" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="reset_cycle"
label="重置周期"
rules={[{ required: true, message: '请选择重置周期' }]}
>
<Select options={resetCycleOptions} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入规则名称' }]}>
<Input />
</Form.Item>
<Form.Item name="code" label="编码" rules={[{ required: true, message: '请输入规则编码' }]}>
<Input disabled={!!drawer.editingRecord} />
</Form.Item>
<Form.Item name="prefix" label="前缀">
<Input placeholder="如 PO、SO" />
</Form.Item>
<Form.Item name="date_format" label="日期格式">
<Input placeholder="如 YYYYMMDD" />
</Form.Item>
<Form.Item name="separator" label="分隔符">
<Input placeholder="默认 -" />
</Form.Item>
<Form.Item name="seq_length" label="序列长度" rules={[{ required: true, message: '请输入序列长度' }]}>
<InputNumber min={1} max={20} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="seq_start" label="起始值" initialValue={1}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="reset_cycle" label="重置周期" rules={[{ required: true, message: '请选择重置周期' }]}>
<Select options={RESET_CYCLE_OPTIONS} />
</Form.Item>
</DrawerForm>
</div>
);
}

View File

@@ -5,10 +5,9 @@ import {
Input,
Space,
Popconfirm,
message,
Table,
Modal,
Tag,
message,
} from 'antd';
import { PlusOutlined, SearchOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import {
@@ -16,7 +15,8 @@ import {
updateSetting,
deleteSetting,
} from '../../api/settings';
import { handleApiError } from '../../api/client';
import { DrawerForm } from '../../components/DrawerForm';
import { useApiRequest } from '../../hooks/useApiRequest';
import { useThemeMode } from '../../hooks/useThemeMode';
interface SettingEntry {
@@ -28,23 +28,22 @@ interface SettingEntry {
export default function SystemSettings() {
const [entries, setEntries] = useState<SettingEntry[]>([]);
const [searchKey, setSearchKey] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const [editEntry, setEditEntry] = useState<SettingEntry | null>(null);
const [form] = Form.useForm();
const isDark = useThemeMode();
const { execute } = useApiRequest();
const handleSearch = async () => {
if (!searchKey.trim()) {
message.warning('请输入设置键名');
return;
}
try {
await execute(async () => {
const result = await getSetting(searchKey.trim());
const value = typeof result.setting_value === 'object' && result.setting_value !== null
? JSON.stringify(result.setting_value, null, 2)
: String(result.setting_value ?? '');
const version = result.version;
setEntries((prev) => {
const exists = prev.findIndex((e) => e.key === searchKey.trim());
if (exists >= 0) {
@@ -54,34 +53,25 @@ export default function SystemSettings() {
}
return [...prev, { key: searchKey.trim(), value, version }];
});
message.success('查询成功');
} catch (err: unknown) {
const status = (err as { response?: { status?: number } })?.response?.status;
if (status === 404) {
message.info('该设置键不存在,可点击"添加设置"创建');
} else {
message.error('查询失败');
}
}
}, '查询成功', '查询失败');
};
const handleSave = async (values: { setting_key: string; setting_value: string }) => {
const key = values.setting_key.trim();
const value = values.setting_value;
const handleSave = async (values: Record<string, unknown>) => {
const key = (values.setting_key as string).trim();
const value = values.setting_value as string;
try {
try {
JSON.parse(value);
} catch {
message.error('设置值必须是有效的 JSON 格式');
return;
}
JSON.parse(value);
} catch {
message.error('设置值必须是有效的 JSON 格式');
return;
}
const editVersion = editEntry?.version;
const jsonValue = JSON.parse(value);
const editVersion = editEntry?.version;
const jsonValue = JSON.parse(value);
await execute(async () => {
const result = await updateSetting(key, jsonValue, editVersion);
const displayValue = typeof jsonValue === 'object' ? JSON.stringify(jsonValue, null, 2) : value;
setEntries((prev) => {
const displayValue = typeof jsonValue === 'object' ? JSON.stringify(jsonValue, null, 2) : value;
const exists = prev.findIndex((e) => e.key === key);
if (exists >= 0) {
const updated = [...prev];
@@ -90,95 +80,48 @@ export default function SystemSettings() {
}
return [...prev, { key, value: displayValue, version: result.version }];
});
}, '设置已保存');
message.success('设置已保存');
closeModal();
} catch (err: unknown) {
handleApiError(err, '保存失败');
}
setDrawerOpen(false);
setEditEntry(null);
};
const handleDelete = async (key: string, version: number) => {
try {
await deleteSetting(key, version);
setEntries((prev) => prev.filter((e) => e.key !== key));
message.success('设置已删除');
} catch {
message.error('删除失败');
}
await execute(() => deleteSetting(key, version), '设置已删除');
setEntries((prev) => prev.filter((e) => e.key !== key));
};
const openCreate = () => {
setEditEntry(null);
form.resetFields();
setModalOpen(true);
setDrawerOpen(true);
};
const openEdit = (entry: SettingEntry) => {
setEditEntry(entry);
form.setFieldsValue({
setting_key: entry.key,
setting_value: entry.value,
});
setModalOpen(true);
};
const closeModal = () => {
setModalOpen(false);
setEditEntry(null);
form.resetFields();
setDrawerOpen(true);
};
const columns = [
{
title: '键',
dataIndex: 'key',
key: 'key',
width: 250,
title: '键', dataIndex: 'key', key: 'key', width: 250,
render: (v: string) => (
<Tag style={{
background: isDark ? '#0f172a' : '#f8fafc',
border: 'none',
color: isDark ? '#CBD5E1' : '#475569',
fontFamily: 'monospace',
fontSize: 12,
}}>
{v}
</Tag>
background: isDark ? '#0f172a' : '#f8fafc', border: 'none',
color: isDark ? '#CBD5E1' : '#475569', fontFamily: 'monospace', fontSize: 12,
}}>{v}</Tag>
),
},
{
title: '值 (JSON)',
dataIndex: 'value',
key: 'value',
ellipsis: true,
render: (v: string) => (
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v}</span>
),
title: '值 (JSON)', dataIndex: 'value', key: 'value', ellipsis: true,
render: (v: string) => <span style={{ fontFamily: 'monospace', fontSize: 12 }}>{v}</span>,
},
{
title: '操作',
key: 'actions',
width: 120,
title: '操作', key: 'actions', width: 120,
render: (_: unknown, record: SettingEntry) => (
<Space size={4}>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Popconfirm
title="确定删除此设置?"
onConfirm={() => handleDelete(record.key, record.version)}
>
<Button
size="small"
type="text"
danger
icon={<DeleteOutlined />}
/>
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} style={{ color: isDark ? '#94a3b8' : '#475569' }} />
<Popconfirm title="确定删除此设置?" onConfirm={() => handleDelete(record.key, record.version)}>
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
@@ -187,12 +130,7 @@ export default function SystemSettings() {
return (
<div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<Space>
<Input
placeholder="输入设置键名查询"
@@ -204,9 +142,7 @@ export default function SystemSettings() {
/>
<Button onClick={handleSearch}></Button>
</Space>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}></Button>
</div>
<div style={{
@@ -225,30 +161,23 @@ export default function SystemSettings() {
/>
</div>
<Modal
<DrawerForm
title={editEntry ? '编辑设置' : '添加设置'}
open={modalOpen}
onCancel={closeModal}
onOk={() => form.submit()}
open={drawerOpen}
onClose={() => { setDrawerOpen(false); setEditEntry(null); }}
onSubmit={handleSave}
initialValues={editEntry ? { setting_key: editEntry.key, setting_value: editEntry.value } : undefined}
loading={false}
width={560}
columns={1}
>
<Form form={form} onFinish={handleSave} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="setting_key"
label="键名"
rules={[{ required: true, message: '请输入设置键名' }]}
>
<Input disabled={!!editEntry} />
</Form.Item>
<Form.Item
name="setting_value"
label="值 (JSON)"
rules={[{ required: true, message: '请输入设置值' }]}
>
<Input.TextArea rows={6} placeholder='{"key": "value"}' style={{ fontFamily: 'monospace' }} />
</Form.Item>
</Form>
</Modal>
<Form.Item name="setting_key" label="键名" rules={[{ required: true, message: '请输入设置键名' }]}>
<Input disabled={!!editEntry} />
</Form.Item>
<Form.Item name="setting_value" label="值 (JSON)" rules={[{ required: true, message: '请输入设置值' }]}>
<Input.TextArea rows={6} placeholder='{"key": "value"}' style={{ fontFamily: 'monospace' }} />
</Form.Item>
</DrawerForm>
</div>
);
}

View File

@@ -1,17 +1,13 @@
import { useEffect, useState, useCallback } from 'react';
import { Form, Input, Select, Button, ColorPicker, message, Typography, Divider } from 'antd';
import {
getTheme,
updateTheme,
} from '../../api/themes';
import { useEffect, useCallback } from 'react';
import { Form, Input, Select, Button, ColorPicker, Typography, Divider } from 'antd';
import { getTheme, updateTheme } from '../../api/themes';
import { useApiRequest } from '../../hooks/useApiRequest';
export default function ThemeSettings() {
const [form] = Form.useForm();
const [, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const { execute, loading } = useApiRequest();
const fetchTheme = useCallback(async () => {
setLoading(true);
try {
const theme = await getTheme();
form.setFieldsValue({
@@ -25,67 +21,35 @@ export default function ThemeSettings() {
});
} catch {
form.setFieldsValue({
primary_color: '#1677ff',
logo_url: '',
sidebar_style: 'light',
brand_name: '',
brand_slogan: '',
brand_features: '',
brand_copyright: '',
primary_color: '#1677ff', logo_url: '', sidebar_style: 'light',
brand_name: '', brand_slogan: '', brand_features: '', brand_copyright: '',
});
}
setLoading(false);
}, [form]);
useEffect(() => {
fetchTheme();
}, [fetchTheme]);
useEffect(() => { fetchTheme(); }, [fetchTheme]);
const handleSave = async (values: {
primary_color: string;
logo_url: string;
sidebar_style: 'light' | 'dark';
brand_name: string;
brand_slogan: string;
brand_features: string;
brand_copyright: string;
}) => {
setSaving(true);
try {
const handleSave = async (values: Record<string, unknown>) => {
await execute(async () => {
await updateTheme({
primary_color:
typeof values.primary_color === 'string'
? values.primary_color
: (values.primary_color as { toHexString?: () => string }).toHexString?.() ?? String(values.primary_color),
logo_url: values.logo_url,
sidebar_style: values.sidebar_style,
brand_name: values.brand_name || undefined,
brand_slogan: values.brand_slogan || undefined,
brand_features: values.brand_features || undefined,
brand_copyright: values.brand_copyright || undefined,
primary_color: typeof values.primary_color === 'string'
? values.primary_color
: (values.primary_color as { toHexString?: () => string }).toHexString?.() ?? String(values.primary_color),
logo_url: values.logo_url as string,
sidebar_style: values.sidebar_style as 'light' | 'dark',
brand_name: (values.brand_name as string) || undefined,
brand_slogan: (values.brand_slogan as string) || undefined,
brand_features: (values.brand_features as string) || undefined,
brand_copyright: (values.brand_copyright as string) || undefined,
});
message.success('主题设置已保存');
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '保存失败';
message.error(errorMsg);
}
setSaving(false);
}, '主题设置已保存');
};
return (
<div>
<Typography.Title level={5} style={{ marginBottom: 16 }}>
</Typography.Title>
<Typography.Title level={5} style={{ marginBottom: 16 }}></Typography.Title>
<Form
form={form}
onFinish={handleSave}
layout="vertical"
style={{ maxWidth: 480 }}
>
<Form form={form} onFinish={handleSave} layout="vertical" style={{ maxWidth: 480 }}>
<Form.Item name="primary_color" label="主色调">
<ColorPicker format="hex" />
</Form.Item>
@@ -93,12 +57,7 @@ export default function ThemeSettings() {
<Input placeholder="https://example.com/logo.png" />
</Form.Item>
<Form.Item name="sidebar_style" label="侧边栏风格">
<Select
options={[
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
]}
/>
<Select options={[{ label: '亮色', value: 'light' }, { label: '暗色', value: 'dark' }]} />
</Form.Item>
<Divider></Divider>
@@ -117,9 +76,7 @@ export default function ThemeSettings() {
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving}>
</Button>
<Button type="primary" htmlType="submit" loading={loading}></Button>
</Form.Item>
</Form>
</div>