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:
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 }),
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user