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:
2026-01-26 20:51:35 +08:00
parent e8e2765c27
commit 30388533c0
14 changed files with 1471 additions and 3 deletions

View File

@@ -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
- **工具库**:

View File

@@ -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
View File

@@ -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
View 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>;
}

View 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('获取配置失败');
}
},
}));

View 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
View 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
View 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
View 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>;
}

View 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;
},
}));

View 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 }),
};
});

View 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
View File

@@ -0,0 +1 @@
export { query } from './query'

View File

@@ -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}