refactor(web): 系统设置模块页面表单一致性重构
- 新增 useCrudDrawer hook 封装 CRUD Drawer 通用模式(状态管理/提交/错误处理) - 新增 useListData hook 封装非分页列表数据获取 - 11 个页面统一迁移到 DrawerForm + 共享 hooks,消除重复代码 - 错误处理统一使用 useApiRequest.execute(),移除内联 try-catch - Modal 全部替换为 DrawerForm,保持 UI 一致性 - 净减少 ~1300 行代码(858 增 / 2136 删)
This commit is contained in:
74
apps/web/src/hooks/useCrudDrawer.ts
Normal file
74
apps/web/src/hooks/useCrudDrawer.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
33
apps/web/src/hooks/useListData.ts
Normal file
33
apps/web/src/hooks/useListData.ts
Normal 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 };
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user