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 ( + { + setShowEdit(open); + if (!open) setFormData({}); + }}> + + + {formData?.id ? '编辑配置' : '添加配置'} + +
+
+
+ + + {errors.key && {errors.key.message as string}} +
+
+ + +
+
+ + +
+
+
+
+
+ ); +}; + +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'} + + + + + + ))} + +
+
+
+
+ + + + + {userFormData?.id ? '编辑用户' : '添加用户'} + +
+
+
+ + + {errors.id && {errors.id.message as string}} +
+
{userFormData?.username}
+ ( +
+ + +
+ )} + /> +
+ + +
+ +
+
+
+ + ); +}; 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 ( + { + setShowEdit(open); + if (!open) setFormData({}); + }}> + + + {formData?.id ? '编辑组织' : '添加组织'} + +
+
+
+ + + {errors.username && {errors.username.message as string}} +
+
+ + +
+
+ + +
+
+
+
+
+ ); +}; + +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 ( + { + setShowEdit(open); + if (!open) setFormData({}); + }}> + + + {formData?.id ? '编辑用户' : '添加用户'} + +
+
+
+ + + {errors.username && {errors.username.message as string}} +
+
+ + +
+ {!formData?.id && ( +
+ + + {errors.password && {errors.password.message as string}} +
+ )} +
+ + +
+
+
+
+
+ ); +}; + +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 (