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

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