feat: implement configuration management with CRUD operations
- Added configuration management page with table view and modal forms for adding/editing configurations. - Integrated Zustand for state management of configurations. - Implemented user management drawer for organizations with user addition/removal functionality. - Created user management page with CRUD operations for users. - Introduced admin store for user-related actions including user creation, deletion, and updates. - Developed reusable drawer component for UI consistency across user management. - Enhanced error handling and user feedback with toast notifications.
This commit is contained in:
@@ -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
|
||||
- **工具库**:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
210
src/app/config/page.tsx
Normal file
210
src/app/config/page.tsx
Normal file
@@ -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 (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>文件</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>更新时间</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((config) => (
|
||||
<TableRow key={config.id}>
|
||||
<TableCell>{config.key || '-'}</TableCell>
|
||||
<TableCell>{config.description || '-'}</TableCell>
|
||||
<TableCell>{config.createdAt ? new Date(config.createdAt).toLocaleString() : '-'}</TableCell>
|
||||
<TableCell>{config.updatedAt ? new Date(config.updatedAt).toLocaleString() : '-'}</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(config)}>
|
||||
<Pencil className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm">
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-2">
|
||||
<div className="text-sm text-center mb-2">确认删除该配置?</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(config);
|
||||
}}>
|
||||
确认
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog open={showEdit} onOpenChange={(open) => {
|
||||
setShowEdit(open);
|
||||
if (!open) setFormData({});
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formData?.id ? '编辑配置' : '添加配置'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">文件</label>
|
||||
<Input
|
||||
{...register('key', { required: '请输入文件' })}
|
||||
placeholder="请输入文件"
|
||||
className={errors.key ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.key && <span className="text-xs text-red-500">{errors.key.message as string}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Input
|
||||
{...register('description')}
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setShowEdit(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">提交</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const { getConfigList, setShowEdit, setFormData } = useConfigStore();
|
||||
|
||||
useEffect(() => {
|
||||
getConfigList();
|
||||
}, [getConfigList]);
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full h-full overflow-auto">
|
||||
<div className="flex mb-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData({});
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<TableList />
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain><List /></LayoutMain>;
|
||||
}
|
||||
78
src/app/config/store/config.ts
Normal file
78
src/app/config/store/config.ts
Normal file
@@ -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<void>;
|
||||
updateData: (data: any, opts?: { refresh?: boolean }) => Promise<any>;
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
formData: any;
|
||||
setFormData: (formData: any) => void;
|
||||
deleteConfig: (id: string) => Promise<void>;
|
||||
detectConfig: () => Promise<void>;
|
||||
onOpenKey: (key: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useConfigStore = create<ConfigStore>((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('获取配置失败');
|
||||
}
|
||||
},
|
||||
}));
|
||||
211
src/app/org/components/UserDrawer.tsx
Normal file
211
src/app/org/components/UserDrawer.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Drawer open={open} onOpenChange={onOpenChange} direction="right">
|
||||
<DrawerContent className="h-full !max-w-xl ml-auto">
|
||||
<DrawerHeader className="flex flex-row items-center justify-between">
|
||||
<DrawerTitle>用户管理</DrawerTitle>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</DrawerHeader>
|
||||
|
||||
<div className="flex gap-2 mb-4 px-4">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setUserFormData({});
|
||||
setShowUserEdit(true);
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto px-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户ID</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.role || 'member'}</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditUser(user)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveUser(user.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Dialog open={showUserEdit} onOpenChange={setShowUserEdit}>
|
||||
<DialogContent className='px-4 overflow-hidden'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{userFormData?.id ? '编辑用户' : '添加用户'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 ">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(handleAddUser)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">用户ID</label>
|
||||
<Input
|
||||
{...register('id', { required: '请输入用户ID' })}
|
||||
placeholder="请输入用户ID"
|
||||
className={errors.id ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.id && <span className="text-xs text-red-500">{errors.id.message as string}</span>}
|
||||
</div>
|
||||
<div>{userFormData?.username}</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue={userFormData?.role || 'member'}
|
||||
render={({ field }) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">角色</label>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="owner">owner</SelectItem>
|
||||
<SelectItem value="admin">admin</SelectItem>
|
||||
<SelectItem value="member">member</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setShowUserEdit(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">提交</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
209
src/app/org/page.tsx
Normal file
209
src/app/org/page.tsx
Normal file
@@ -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 (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>更新时间</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((org) => (
|
||||
<TableRow key={org.id}>
|
||||
<TableCell>{org.username}</TableCell>
|
||||
<TableCell>{org.description || '-'}</TableCell>
|
||||
<TableCell>{org.createdAt ? new Date(org.createdAt).toLocaleString() : '-'}</TableCell>
|
||||
<TableCell>{org.updatedAt ? new Date(org.updatedAt).toLocaleString() : '-'}</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleOpenUserDrawer(org)}>
|
||||
<Users className="w-4 h-4 mr-1" />
|
||||
用户管理
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData(org);
|
||||
}}>
|
||||
<Pencil className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm">
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-2">
|
||||
<div className="text-sm text-center mb-2">确认删除该组织?</div>
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteData(org.id);
|
||||
}}>
|
||||
确认
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<UserDrawer open={userDrawerOpen} onOpenChange={setUserDrawerOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog open={showEdit} onOpenChange={(open) => {
|
||||
setShowEdit(open);
|
||||
if (!open) setFormData({});
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formData?.id ? '编辑组织' : '添加组织'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 ">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">名称</label>
|
||||
<Input
|
||||
{...register('username', { required: '请输入名称' })}
|
||||
placeholder="请输入名称"
|
||||
className={errors.username ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.username && <span className="text-xs text-red-500">{errors.username.message as string}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Input
|
||||
{...register('description')}
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setShowEdit(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">提交</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const { getList, setShowEdit, setFormData } = useOrgStore();
|
||||
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, [getList]);
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full h-full overflow-auto">
|
||||
<div className="flex mb-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData({});
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<TableList />
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain><List /></LayoutMain>;
|
||||
}
|
||||
143
src/app/org/store/index.ts
Normal file
143
src/app/org/store/index.ts
Normal file
@@ -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<void>;
|
||||
updateData: (data: any) => Promise<any>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
org: any;
|
||||
setOrg: (org: any) => void;
|
||||
users: { id: string; username: string; role?: string }[];
|
||||
orgId: string;
|
||||
setOrgId: (orgId: string) => void;
|
||||
getOrg: () => Promise<any>;
|
||||
addUser: (data: { userId?: string; username?: string; role?: string }) => Promise<any>;
|
||||
removeUser: (userId: string) => Promise<void>;
|
||||
};
|
||||
export const useOrgStore = create<OrgStore>((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');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
184
src/app/users/page.tsx
Normal file
184
src/app/users/page.tsx
Normal file
@@ -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 (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.description || '-'}</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData(user);
|
||||
}}>
|
||||
<Pencil className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteData(user.id)}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dialog open={showEdit} onOpenChange={(open) => {
|
||||
setShowEdit(open);
|
||||
if (!open) setFormData({});
|
||||
}}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formData?.id ? '编辑用户' : '添加用户'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">用户名</label>
|
||||
<Input
|
||||
{...register('username', { required: '请输入用户名' })}
|
||||
placeholder="请输入用户名"
|
||||
className={errors.username ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.username && <span className="text-xs text-red-500">{errors.username.message as string}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
<Input
|
||||
{...register('description')}
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</div>
|
||||
{!formData?.id && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">密码</label>
|
||||
<Input
|
||||
{...register('password', { required: '请输入密码' })}
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
className={errors.password ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.password && <span className="text-xs text-red-500">{errors.password.message as string}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button type="button" variant="outline" onClick={() => setShowEdit(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">提交</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const { getList, setShowEdit, setFormData } = useUserStore();
|
||||
|
||||
useEffect(() => {
|
||||
getList();
|
||||
}, [getList]);
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full h-full overflow-auto">
|
||||
<div className="flex mb-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEdit(true);
|
||||
setFormData({});
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
<TableList />
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain><List /></LayoutMain>;
|
||||
}
|
||||
126
src/app/users/store/admin.ts
Normal file
126
src/app/users/store/admin.ts
Normal file
@@ -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<void>;
|
||||
/**
|
||||
* 删除用户
|
||||
* @param id
|
||||
*/
|
||||
deleteUser: (id: string) => Promise<void>;
|
||||
/**
|
||||
* 更新用户
|
||||
* @param id
|
||||
* @param data
|
||||
*/
|
||||
updateUser: (id: string, data: any) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param id
|
||||
*/
|
||||
resetPassword: (id: string, password?: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 修改用户名
|
||||
* @param id
|
||||
* @param name
|
||||
*/
|
||||
changeName: (id: string, name: string) => Promise<Result<any>>;
|
||||
|
||||
/**
|
||||
* 检查用户是否存在
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
checkUserExist: (name: string) => Promise<boolean | null>;
|
||||
};
|
||||
|
||||
export const useAdminStore = create<AdminStore>((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;
|
||||
},
|
||||
}));
|
||||
99
src/app/users/store/user.ts
Normal file
99
src/app/users/store/user.ts
Normal file
@@ -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<void>;
|
||||
updateData: (data: any) => Promise<any>;
|
||||
updateSelf: (data: any) => Promise<any>;
|
||||
deleteData: (id: string) => Promise<void>;
|
||||
showChangePassword: boolean;
|
||||
setShowChangePassword: (showChangePassword: boolean) => void;
|
||||
};
|
||||
export const useUserStore = create<UserStore>((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 }),
|
||||
};
|
||||
});
|
||||
135
src/components/ui/drawer.tsx
Normal file
135
src/components/ui/drawer.tsx
Normal file
@@ -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<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
1
src/modules/index.ts
Normal file
1
src/modules/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { query } from './query'
|
||||
@@ -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) => {
|
||||
<div className='text-xl font-bold '>{props.title}</div>
|
||||
<div className='flex items-center gap-2 text-sm '>
|
||||
{quickMenu.map((item, index) => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const isActive = location?.pathname === item.link;
|
||||
const isActive = pathname === item.link;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
|
||||
Reference in New Issue
Block a user