diff --git a/AGENTS.md b/AGENTS.md
index 86b9c4a..62c7336 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -14,7 +14,7 @@ Light Code - 一个直觉、高效的代码编辑器前端界面
- **语言**: TypeScript 5
- **UI库**: React 19.2.3
- **样式**: Tailwind CSS 4
-- **组件库**: Radix UI (Dialog, Slot, etc.)
+- **组件库**: Radix UI (Dialog, Slot, etc.), Shadcn UI, @tankstack/react-table
- **状态管理**: Zustand valtio
- **图标**: Lucide React
- **工具库**:
diff --git a/package.json b/package.json
index e9b5a1b..b72e9b1 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-table": "^8.21.3",
"antd": "^6.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -46,6 +47,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"valtio": "^2.3.0",
+ "vaul": "^1.1.2",
"zustand": "^5.0.10"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f099063..e1c5922 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -56,6 +56,9 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ '@tanstack/react-table':
+ specifier: ^8.21.3
+ version: 8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
antd:
specifier: ^6.2.1
version: 6.2.1(date-fns@4.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -116,6 +119,9 @@ importers:
valtio:
specifier: ^2.3.0
version: 2.3.0(@types/react@19.2.7)(react@19.2.3)
+ vaul:
+ specifier: ^1.1.2
+ version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
zustand:
specifier: ^5.0.10
version: 5.0.10(@types/react@19.2.7)(react@19.2.3)
@@ -247,89 +253,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -411,24 +433,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.4':
resolution: {integrity: sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-gnu@16.1.4':
resolution: {integrity: sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-musl@16.1.4':
resolution: {integrity: sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.4':
resolution: {integrity: sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==}
@@ -1196,24 +1222,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -1246,6 +1276,17 @@ packages:
'@tailwindcss/postcss@4.1.18':
resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==}
+ '@tanstack/react-table@8.21.3':
+ resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ react: '>=16.8'
+ react-dom: '>=16.8'
+
+ '@tanstack/table-core@8.21.3':
+ resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
+ engines: {node: '>=12'}
+
'@types/node@25.0.3':
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
@@ -1388,24 +1429,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
@@ -1646,6 +1691,12 @@ packages:
react:
optional: true
+ vaul@1.1.2:
+ resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
+ peerDependencies:
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
+
zustand@5.0.10:
resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==}
engines: {node: '>=12.20.0'}
@@ -2777,6 +2828,14 @@ snapshots:
postcss: 8.5.6
tailwindcss: 4.1.18
+ '@tanstack/react-table@8.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
+ dependencies:
+ '@tanstack/table-core': 8.21.3
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
+ '@tanstack/table-core@8.21.3': {}
+
'@types/node@25.0.3':
dependencies:
undici-types: 7.16.0
@@ -3168,6 +3227,15 @@ snapshots:
'@types/react': 19.2.7
react: 19.2.3
+ vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+
zustand@5.0.10(@types/react@19.2.7)(react@19.2.3):
optionalDependencies:
'@types/react': 19.2.7
diff --git a/src/app/config/page.tsx b/src/app/config/page.tsx
new file mode 100644
index 0000000..aa6ec46
--- /dev/null
+++ b/src/app/config/page.tsx
@@ -0,0 +1,210 @@
+'use client';
+import { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { useConfigStore } from './store/config';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { Plus, Pencil, Trash2 } from 'lucide-react';
+import { LayoutMain } from '@/modules/layout';
+
+const TableList = () => {
+ const { list, setShowEdit, setFormData, deleteConfig } = useConfigStore();
+
+ interface ConfigItem {
+ id?: string;
+ key?: string;
+ description?: string;
+ createdAt?: string;
+ updatedAt?: string;
+ }
+
+ const handleEdit = (config: ConfigItem) => {
+ setShowEdit(true);
+ setFormData(config);
+ };
+
+ const handleDelete = (config: ConfigItem) => {
+ if (config.id) {
+ deleteConfig(config.id);
+ }
+ };
+
+ return (
+
+
+
+
+ 文件
+ 描述
+ 创建时间
+ 更新时间
+ 操作
+
+
+
+ {list.map((config) => (
+
+ {config.key || '-'}
+ {config.description || '-'}
+ {config.createdAt ? new Date(config.createdAt).toLocaleString() : '-'}
+ {config.updatedAt ? new Date(config.updatedAt).toLocaleString() : '-'}
+
+
+
+
+
+
+
+ 确认删除该配置?
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+const FormModal = () => {
+ const { showEdit, setShowEdit, formData, setFormData, updateData } = useConfigStore();
+ const {
+ handleSubmit,
+ formState: { errors },
+ reset,
+ register,
+ } = useForm();
+
+ useEffect(() => {
+ if (!showEdit) return;
+ if (formData?.id) {
+ reset(formData);
+ } else {
+ reset({ key: '', description: '' });
+ }
+ }, [formData, showEdit, reset]);
+
+ const onSubmit = async (data: any) => {
+ const res = await updateData(data);
+ if (res.code === 200) {
+ setShowEdit(false);
+ setFormData({});
+ }
+ };
+
+ return (
+
+ );
+};
+
+export const List = () => {
+ const { getConfigList, setShowEdit, setFormData } = useConfigStore();
+
+ useEffect(() => {
+ getConfigList();
+ }, [getConfigList]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default () => {
+ return
;
+}
diff --git a/src/app/config/store/config.ts b/src/app/config/store/config.ts
new file mode 100644
index 0000000..8b56532
--- /dev/null
+++ b/src/app/config/store/config.ts
@@ -0,0 +1,78 @@
+import { create } from 'zustand';
+import { query } from '@/modules/query';
+import { toast } from 'sonner';
+import { QueryConfig } from '@kevisual/api/config';
+
+export const queryConfig = new QueryConfig({ query: query as any });
+
+interface ConfigStore {
+ list: any[];
+ getConfigList: () => Promise;
+ updateData: (data: any, opts?: { refresh?: boolean }) => Promise;
+ showEdit: boolean;
+ setShowEdit: (showEdit: boolean) => void;
+ formData: any;
+ setFormData: (formData: any) => void;
+ deleteConfig: (id: string) => Promise;
+ detectConfig: () => Promise;
+ onOpenKey: (key: string) => Promise;
+}
+
+export const useConfigStore = create((set, get) => ({
+ list: [],
+ getConfigList: async () => {
+ const res = await queryConfig.listConfig();
+ if (res.code === 200) {
+ set({ list: res.data?.list || [] });
+ }
+ },
+ updateData: async (data: any, opts?: { refresh?: boolean }) => {
+ const res = await queryConfig.updateConfig(data);
+ if (res.code === 200) {
+ get().setFormData(res.data);
+ if (opts?.refresh ?? true) {
+ get().getConfigList();
+ }
+ toast.success('保存成功');
+ } else {
+ toast.error('保存失败');
+ }
+ return res;
+ },
+ showEdit: false,
+ setShowEdit: (showEdit: boolean) => set({ showEdit }),
+ formData: {},
+ setFormData: (formData: any) => set({ formData }),
+ deleteConfig: async (id: string) => {
+ const res = await queryConfig.deleteConfig({ id });
+ if (res.code === 200) {
+ get().getConfigList();
+ toast.success('删除成功');
+ } else {
+ toast.error('删除失败');
+ }
+ },
+ detectConfig: async () => {
+ const res = await queryConfig.detectConfig();
+ if (res.code === 200) {
+ const data = res?.data?.updateList || [];
+ console.log(data);
+ toast.success('检测成功');
+ } else {
+ toast.error('检测失败');
+ }
+ },
+ onOpenKey: async (key: string) => {
+ const { setFormData, setShowEdit, getConfigList } = get();
+ const res = await queryConfig.getConfigByKey(key as any);
+ if (res.code === 200) {
+ const data = res.data;
+ setFormData(data);
+ setShowEdit(true);
+ getConfigList();
+ } else {
+ console.log(res);
+ toast.error('获取配置失败');
+ }
+ },
+}));
diff --git a/src/app/org/components/UserDrawer.tsx b/src/app/org/components/UserDrawer.tsx
new file mode 100644
index 0000000..f371440
--- /dev/null
+++ b/src/app/org/components/UserDrawer.tsx
@@ -0,0 +1,211 @@
+'use client';
+import { useEffect } from 'react';
+import { useForm, Controller } from 'react-hook-form';
+import { useOrgStore } from '../store';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+ DrawerClose,
+} from '@/components/ui/drawer';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Plus, Trash2, X } from 'lucide-react';
+
+interface UserDrawerProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export const UserDrawer = ({ open, onOpenChange }: UserDrawerProps) => {
+ const {
+ orgId,
+ users,
+ getOrg,
+ addUser,
+ removeUser,
+ setUserFormData: setUserFormData,
+ userFormData,
+ showUserEdit,
+ setShowUserEdit,
+ } = useOrgStore();
+
+ const {
+ handleSubmit,
+ formState: { errors },
+ reset,
+ register,
+ control,
+ } = useForm();
+
+ useEffect(() => {
+ if (open && orgId) {
+ getOrg();
+ }
+ }, [open, orgId, getOrg]);
+
+ useEffect(() => {
+ if (!showUserEdit) return;
+ // 确保 userFormData 已更新后再重置表单
+ console.log('Resetting form with userFormData:', userFormData);
+ if (userFormData?.id) {
+ reset({ id: userFormData.id, username: userFormData.username, role: userFormData.role || 'member' });
+ } else {
+ reset({ id: '', username: '', role: 'member' });
+ }
+ }, [showUserEdit, userFormData, reset]);
+
+ const handleAddUser = async (data: any) => {
+ const res = await addUser({ ...data, action: 'add' });
+ if (res.code === 200) {
+ setShowUserEdit(false);
+ setUserFormData({});
+ }
+ };
+
+ const handleRemoveUser = async (uid: string) => {
+ await removeUser(uid);
+ };
+
+ const handleEditUser = (user: any) => {
+ console.log('Editing user:', user);
+ setUserFormData(user);
+ // 使用 setTimeout 确保 userFormData 更新后再打开弹窗
+ setShowUserEdit(true);
+ };
+
+ return (
+ <>
+
+
+
+ 用户管理
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 用户ID
+ 用户名
+ 角色
+ 操作
+
+
+
+ {users.map((user) => (
+
+ {user.id}
+ {user.username}
+ {user.role || 'member'}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/app/org/page.tsx b/src/app/org/page.tsx
new file mode 100644
index 0000000..9eedc9f
--- /dev/null
+++ b/src/app/org/page.tsx
@@ -0,0 +1,209 @@
+'use client';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useOrgStore } from './store';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { Plus, Pencil, Trash2, Users } from 'lucide-react';
+import { LayoutMain } from '@/modules/layout';
+import { UserDrawer } from './components/UserDrawer';
+
+const TableList = () => {
+ const { list, setShowEdit, setFormData, deleteData, setOrgId } = useOrgStore();
+ const [userDrawerOpen, setUserDrawerOpen] = useState(false);
+
+ const handleOpenUserDrawer = (org: any) => {
+ setOrgId(org.id);
+ setUserDrawerOpen(true);
+ };
+
+ return (
+
+
+
+
+ 名称
+ 描述
+ 创建时间
+ 更新时间
+ 操作
+
+
+
+ {list.map((org) => (
+
+ {org.username}
+ {org.description || '-'}
+ {org.createdAt ? new Date(org.createdAt).toLocaleString() : '-'}
+ {org.updatedAt ? new Date(org.updatedAt).toLocaleString() : '-'}
+
+
+
+
+
+
+
+
+ 确认删除该组织?
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+const FormModal = () => {
+ const { showEdit, setShowEdit, formData, setFormData, updateData } = useOrgStore();
+ const {
+ handleSubmit,
+ formState: { errors },
+ reset,
+ register,
+ } = useForm();
+
+ useEffect(() => {
+ if (!showEdit) return;
+ if (formData?.id) {
+ reset(formData);
+ } else {
+ reset({ username: '', description: '' });
+ }
+ }, [formData, showEdit, reset]);
+
+ const onSubmit = async (data: any) => {
+ const res = await updateData(data);
+ if (res.code === 200) {
+ setShowEdit(false);
+ setFormData({});
+ }
+ };
+
+ return (
+
+ );
+};
+
+export const List = () => {
+ const { getList, setShowEdit, setFormData } = useOrgStore();
+
+ useEffect(() => {
+ getList();
+ }, [getList]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default () => {
+ return
;
+}
diff --git a/src/app/org/store/index.ts b/src/app/org/store/index.ts
new file mode 100644
index 0000000..31d0be3
--- /dev/null
+++ b/src/app/org/store/index.ts
@@ -0,0 +1,143 @@
+'use client';
+import { create } from 'zustand';
+import { query } from '@/modules/index';
+import { toast as message } from 'sonner';
+type OrgStore = {
+ showEdit: boolean;
+ setShowEdit: (showEdit: boolean) => void;
+ formData: any;
+ setFormData: (formData: any) => void;
+ showUserEdit: boolean;
+ setShowUserEdit: (showUserEdit: boolean) => void;
+ userFormData: any;
+ setUserFormData: (userFormData: any) => void;
+ loading: boolean;
+ setLoading: (loading: boolean) => void;
+ list: any[];
+ getList: () => Promise;
+ updateData: (data: any) => Promise;
+ deleteData: (id: string) => Promise;
+ org: any;
+ setOrg: (org: any) => void;
+ users: { id: string; username: string; role?: string }[];
+ orgId: string;
+ setOrgId: (orgId: string) => void;
+ getOrg: () => Promise;
+ addUser: (data: { userId?: string; username?: string; role?: string }) => Promise;
+ removeUser: (userId: string) => Promise;
+};
+export const useOrgStore = create((set, get) => {
+ return {
+ showEdit: false,
+ setShowEdit: (showEdit) => set({ showEdit }),
+ formData: {},
+ setFormData: (formData) => set({ formData }),
+ loading: false,
+ setLoading: (loading) => set({ loading }),
+ showUserEdit: false,
+ setShowUserEdit: (showUserEdit) => set({ showUserEdit }),
+ userFormData: {},
+ setUserFormData: (userFormData) => set({ userFormData }),
+ list: [],
+ getList: async () => {
+ set({ loading: true });
+
+ const res = await query.post({
+ path: 'org',
+ key: 'list',
+ });
+ set({ loading: false });
+ if (res.code === 200) {
+ set({ list: res.data });
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ updateData: async (data) => {
+ const { getList } = get();
+ const res = await query.post({
+ path: 'org',
+ key: 'update',
+ data,
+ });
+ if (res.code === 200) {
+ message.success('Success');
+ set({ showEdit: false, formData: [] });
+ getList();
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ return res;
+ },
+ deleteData: async (id) => {
+ const { getList } = get();
+ const res = await query.post({
+ path: 'org',
+ key: 'delete',
+ payload: {
+ id,
+ }
+ });
+ if (res.code === 200) {
+ getList();
+ message.success('Success');
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ org: {},
+ setOrg: (org) => set({ org }),
+ orgId: '',
+ setOrgId: (orgId) => set({ orgId }),
+ users: [],
+ getOrg: async () => {
+ const { orgId } = get();
+ const res = await query.post({
+ path: 'org',
+ key: 'get',
+ payload: {
+ id: orgId,
+ }
+ });
+ if (res.code === 200) {
+ const { org, users } = res.data || {};
+ set({ org, users });
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ addUser: async (data) => {
+ const { orgId } = get();
+ const res = await query.post({
+ path: 'org-user',
+ key: 'operate',
+ data: { orgId, ...data, action: 'add' },
+ });
+ if (res.code === 200) {
+ message.success('Success');
+ get().getOrg();
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ return res
+ },
+ removeUser: async (userId: string) => {
+ const { orgId } = get();
+ const res = await query.post({
+ path: 'org-user',
+ key: 'operate',
+ data: {
+ orgId,
+ userId,
+ action: 'remove',
+ },
+ });
+ if (res.code === 200) {
+ message.success('Success');
+ get().getOrg();
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ };
+});
diff --git a/src/app/users/page.tsx b/src/app/users/page.tsx
new file mode 100644
index 0000000..3353c25
--- /dev/null
+++ b/src/app/users/page.tsx
@@ -0,0 +1,184 @@
+'use client';
+import { useEffect } from 'react';
+import { useForm } from 'react-hook-form';
+import { useUserStore } from './store/user';
+import { useAdminStore } from './store/admin';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Plus, Pencil, Trash2 } from 'lucide-react';
+import { LayoutMain } from '@/modules/layout';
+
+const TableList = () => {
+ const { list, setShowEdit, setFormData, deleteData } = useUserStore();
+
+ return (
+
+
+
+
+ ID
+ 用户名
+ 描述
+ 操作
+
+
+
+ {list.map((user) => (
+
+ {user.id}
+ {user.username}
+ {user.description || '-'}
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+const FormModal = () => {
+ const { showEdit, setShowEdit, formData, setFormData, getList } = useUserStore();
+ const { createNewUser, updateUser } = useAdminStore();
+ const {
+ handleSubmit,
+ formState: { errors },
+ reset,
+ register,
+ } = useForm();
+
+ useEffect(() => {
+ if (!showEdit) return;
+ if (formData?.id) {
+ reset({ username: formData.username, description: formData.description });
+ } else {
+ reset({ username: '', description: '' });
+ }
+ }, [formData, showEdit, reset]);
+
+ const onSubmit = async (data: any) => {
+ let res;
+ if (formData?.id) {
+ res = await updateUser(formData.id, data);
+ } else {
+ res = await createNewUser(data);
+ }
+ if (res?.code === 200) {
+ setShowEdit(false);
+ setFormData({});
+ getList();
+ }
+ };
+
+ return (
+
+ );
+};
+
+export const List = () => {
+ const { getList, setShowEdit, setFormData } = useUserStore();
+
+ useEffect(() => {
+ getList();
+ }, [getList]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default () => {
+ return
;
+}
diff --git a/src/app/users/store/admin.ts b/src/app/users/store/admin.ts
new file mode 100644
index 0000000..fd5977d
--- /dev/null
+++ b/src/app/users/store/admin.ts
@@ -0,0 +1,126 @@
+import { create } from 'zustand';
+import { query } from '@/modules';
+import { toast } from 'sonner';
+import { Result } from '@kevisual/query/query';
+type AdminStore = {
+ /**
+ * 创建新用户
+ * @returns
+ */
+ createNewUser: (data: any) => Promise;
+ /**
+ * 删除用户
+ * @param id
+ */
+ deleteUser: (id: string) => Promise;
+ /**
+ * 更新用户
+ * @param id
+ * @param data
+ */
+ updateUser: (id: string, data: any) => Promise;
+
+ /**
+ * 重置密码
+ * @param id
+ */
+ resetPassword: (id: string, password?: string) => Promise;
+
+ /**
+ * 修改用户名
+ * @param id
+ * @param name
+ */
+ changeName: (id: string, name: string) => Promise>;
+
+ /**
+ * 检查用户是否存在
+ * @param name
+ * @returns
+ */
+ checkUserExist: (name: string) => Promise;
+};
+
+export const useAdminStore = create((set) => ({
+ createNewUser: async (data: any) => {
+ const res = await query.post({
+ path: 'user',
+ key: 'createNewUser',
+ data,
+ });
+ if (res.code === 200) {
+ toast.success('创建用户成功');
+ } else {
+ toast.error(res.message || '创建用户失败');
+ }
+ },
+ deleteUser: async (id: string) => {
+ const res = await query.post({
+ path: 'user',
+ key: 'deleteUser',
+ data: {
+ id,
+ },
+ });
+ if (res.code === 200) {
+ toast.success('删除用户成功');
+ } else {
+ toast.error(res.message || '删除用户失败');
+ }
+ },
+ updateUser: async (id: string, data: any) => {
+ console.log('updateUser', id, data);
+ toast.success('功能开发中');
+ },
+ resetPassword: async (id: string, password?: string) => {
+ const res = await query.post({
+ path: 'user',
+ key: 'resetPassword',
+ data: {
+ id,
+ password,
+ },
+ });
+ if (res.code === 200) {
+ if (res.data.password) {
+ toast.success('new password is ' + res.data.password);
+ } else {
+ toast.success('重置密码成功');
+ }
+ } else {
+ toast.error(res.message || '重置密码失败');
+ }
+ },
+ changeName: async (id: string, name: string) => {
+ const res = await query.post({
+ path: 'user',
+ key: 'changeName',
+ data: {
+ id,
+ newName: name,
+ },
+ });
+ if (res.code === 200) {
+ toast.success('修改用户名成功');
+ } else {
+ toast.error(res.message || '修改用户名失败');
+ }
+ return res;
+ },
+ checkUserExist: async (name: string) => {
+ const res = await query.post({
+ path: 'user',
+ key: 'checkUserExist',
+ data: {
+ username: name,
+ },
+ });
+ if (res.code === 200) {
+ const user = res.data || {};
+ return !!user.id;
+ } else {
+ toast.error(res.message || '检查用户是否存在,请求失败');
+ }
+ return null;
+ },
+}));
diff --git a/src/app/users/store/user.ts b/src/app/users/store/user.ts
new file mode 100644
index 0000000..8a7673f
--- /dev/null
+++ b/src/app/users/store/user.ts
@@ -0,0 +1,99 @@
+import { create } from 'zustand';
+import { query } from '@/modules';
+import { toast as message } from 'sonner';
+type UserStore = {
+ showEdit: boolean;
+ setShowEdit: (showEdit: boolean) => void;
+ showNameEdit: boolean;
+ setShowNameEdit: (showNameEdit: boolean) => void;
+ showCheckUserExist: boolean;
+ setShowCheckUserExist: (showCheckUserExist: boolean) => void;
+ formData: any;
+ setFormData: (formData: any) => void;
+ loading: boolean;
+ setLoading: (loading: boolean) => void;
+ list: any[];
+ getList: () => Promise;
+ updateData: (data: any) => Promise;
+ updateSelf: (data: any) => Promise;
+ deleteData: (id: string) => Promise;
+ showChangePassword: boolean;
+ setShowChangePassword: (showChangePassword: boolean) => void;
+};
+export const useUserStore = create((set, get) => {
+ return {
+ showEdit: false,
+ setShowEdit: (showEdit) => set({ showEdit }),
+ showNameEdit: false,
+ setShowNameEdit: (showNameEdit) => set({ showNameEdit }),
+ showCheckUserExist: false,
+ setShowCheckUserExist: (showCheckUserExist) => set({ showCheckUserExist }),
+ formData: {},
+ setFormData: (formData) => set({ formData }),
+ loading: false,
+ setLoading: (loading) => set({ loading }),
+ list: [],
+ getList: async () => {
+ set({ loading: true });
+
+ const res = await query.post({
+ path: 'user',
+ key: 'list',
+ });
+ set({ loading: false });
+ if (res.code === 200) {
+ set({ list: res.data });
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ updateData: async (data) => {
+ const { getList } = get();
+ const res = await query.post({
+ path: 'user',
+ key: 'update',
+ data,
+ });
+ if (res.code === 200) {
+ message.success('Success');
+ set({ showEdit: false, formData: [] });
+ getList();
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ return res;
+ },
+ updateSelf: async (data) => {
+ const res = await query.post({
+ path: 'user',
+ key: 'updateSelf',
+ data,
+ });
+ if (res.code === 200) {
+ message.success('Success');
+ set({ formData: res.data });
+ return res.data;
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ deleteData: async (id) => {
+ const { getList } = get();
+ const res = await query.post({
+ path: 'user',
+ key: 'delete',
+ payload: {
+ id,
+ }
+ });
+ if (res.code === 200) {
+ getList();
+ message.success('Success');
+ } else {
+ message.error(res.message || 'Request failed');
+ }
+ },
+ showChangePassword: false,
+ setShowChangePassword: (showChangePassword) => set({ showChangePassword }),
+ };
+});
diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx
new file mode 100644
index 0000000..8aa6923
--- /dev/null
+++ b/src/components/ui/drawer.tsx
@@ -0,0 +1,135 @@
+"use client"
+
+import * as React from "react"
+import { Drawer as DrawerPrimitive } from "vaul"
+
+import { cn } from "@/lib/utils"
+
+function Drawer({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DrawerOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DrawerContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+ )
+}
+
+function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DrawerTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DrawerDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Drawer,
+ DrawerPortal,
+ DrawerOverlay,
+ DrawerTrigger,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerFooter,
+ DrawerTitle,
+ DrawerDescription,
+}
diff --git a/src/modules/index.ts b/src/modules/index.ts
new file mode 100644
index 0000000..d5dcdc5
--- /dev/null
+++ b/src/modules/index.ts
@@ -0,0 +1 @@
+export { query } from './query'
\ No newline at end of file
diff --git a/src/modules/layout/index.tsx b/src/modules/layout/index.tsx
index 27db1ea..098fa22 100644
--- a/src/modules/layout/index.tsx
+++ b/src/modules/layout/index.tsx
@@ -1,8 +1,10 @@
+'use client';
import { MenuOutlined, SwapOutlined } from '@ant-design/icons';
import { LayoutMenu, useQuickMenu } from './Menu';
import { useLayoutStore, usePlatformStore } from './store';
import { useShallow } from 'zustand/react/shallow';
import { useEffect, useLayoutEffect, useState } from 'react';
+import { usePathname } from 'next/navigation';
import { LayoutUser } from './LayoutUser';
import PandaPNG from '@/assets/panda.jpg';
import QRCodePNG from '@/assets/qrcode-8x8.jpg';
@@ -53,6 +55,7 @@ export const LayoutMain = (props: LayoutMainProps) => {
);
const { isMac, mount, isElectron } = platformStore;
const quickMenu = useQuickMenu();
+ const pathname = usePathname();
useLayoutEffect(() => {
platformStore.init();
@@ -74,8 +77,7 @@ export const LayoutMain = (props: LayoutMainProps) => {
{props.title}
{quickMenu.map((item, index) => {
- if (typeof window === 'undefined') return null;
- const isActive = location?.pathname === item.link;
+ const isActive = pathname === item.link;
return (