+
+
+ 数据字典管理
+
+ } onClick={openCreateDict}>
+ 新建字典
+
+
+
+
(
+
+ ),
+ }}
+ />
+
+ {/* Dictionary Modal */}
+ dictForm.submit()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Dictionary Item Modal */}
+ itemForm.submit()}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/settings/MenuConfig.tsx b/apps/web/src/pages/settings/MenuConfig.tsx
new file mode 100644
index 0000000..15fcafa
--- /dev/null
+++ b/apps/web/src/pages/settings/MenuConfig.tsx
@@ -0,0 +1,321 @@
+import { useState, useEffect, useCallback } from 'react';
+import {
+ Table,
+ Button,
+ Space,
+ Modal,
+ Form,
+ Input,
+ InputNumber,
+ Select,
+ Switch,
+ TreeSelect,
+ Popconfirm,
+ message,
+ Typography,
+ Tag,
+} from 'antd';
+import { PlusOutlined } from '@ant-design/icons';
+import client from '../../api/client';
+
+// --- Types ---
+
+interface MenuItem {
+ id: string;
+ parent_id?: string | null;
+ title: string;
+ path?: string;
+ icon?: string;
+ menu_type: 'directory' | 'menu' | 'button';
+ sort_order: number;
+ visible: boolean;
+ permission?: string;
+ children?: MenuItem[];
+}
+
+// --- Helpers ---
+
+/** Convert flat menu list to tree structure for Table children prop */
+function buildMenuTree(items: MenuItem[]): MenuItem[] {
+ const map = new Map();
+ const roots: MenuItem[] = [];
+
+ const withChildren = items.map((item) => ({
+ ...item,
+ children: [] as MenuItem[],
+ }));
+
+ withChildren.forEach((item) => map.set(item.id, item));
+
+ withChildren.forEach((item) => {
+ if (item.parent_id && map.has(item.parent_id)) {
+ map.get(item.parent_id)!.children!.push(item);
+ } else {
+ roots.push(item);
+ }
+ });
+
+ return roots;
+}
+
+/** Convert menu tree to TreeSelect data nodes */
+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,
+ }));
+}
+
+const menuTypeLabels: Record = {
+ directory: { text: '目录', color: 'blue' },
+ menu: { text: '菜单', color: 'green' },
+ button: { text: '按钮', color: 'orange' },
+};
+
+// --- Component ---
+
+export default function MenuConfig() {
+ const [menus, setMenus] = useState