|
|
@@ -5,7 +5,7 @@ import { zhCN } from 'date-fns/locale';
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
import { useForm } from 'react-hook-form';
|
|
|
import { toast } from 'sonner';
|
|
|
-import { Plus, Search, Edit, Trash2, Eye } from 'lucide-react';
|
|
|
+import { Plus, Search, Edit, Trash2, Eye, Calendar, UserCheck, UserX } from 'lucide-react';
|
|
|
|
|
|
import { userClient } from '@/client/api';
|
|
|
import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
@@ -46,6 +46,10 @@ export const UsersPage = () => {
|
|
|
const [editingUser, setEditingUser] = useState<any>(null);
|
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
|
const [userToDelete, setUserToDelete] = useState<number | null>(null);
|
|
|
+ const [traineeDialogOpen, setTraineeDialogOpen] = useState(false);
|
|
|
+ const [selectedUser, setSelectedUser] = useState<any>(null);
|
|
|
+ const [traineeValidFrom, setTraineeValidFrom] = useState<string>('');
|
|
|
+ const [traineeValidTo, setTraineeValidTo] = useState<string>('');
|
|
|
|
|
|
// 创建表单
|
|
|
const createForm = useForm<CreateUserRequest>({
|
|
|
@@ -192,6 +196,84 @@ export const UsersPage = () => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ // 打开设置学员对话框
|
|
|
+ const showSetTraineeDialog = (user: any) => {
|
|
|
+ setSelectedUser(user);
|
|
|
+ setTraineeValidFrom(user.traineeValidFrom ? new Date(user.traineeValidFrom).toISOString().split('T')[0] : '');
|
|
|
+ setTraineeValidTo(user.traineeValidTo ? new Date(user.traineeValidTo).toISOString().split('T')[0] : '');
|
|
|
+ setTraineeDialogOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开撤销学员对话框
|
|
|
+ const showRemoveTraineeDialog = (user: any) => {
|
|
|
+ setSelectedUser(user);
|
|
|
+ setTraineeDialogOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理设置学员
|
|
|
+ const handleSetTrainee = async () => {
|
|
|
+ if (!selectedUser || !traineeValidFrom || !traineeValidTo) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const validFrom = new Date(traineeValidFrom);
|
|
|
+ const validTo = new Date(traineeValidTo);
|
|
|
+
|
|
|
+ if (validFrom >= validTo) {
|
|
|
+ toast.error('有效期结束时间必须晚于开始时间');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await userClient[':id']['$put']({
|
|
|
+ param: { id: selectedUser.id.toString() },
|
|
|
+ json: {
|
|
|
+ userType: UserType.TRAINEE,
|
|
|
+ traineeValidFrom: validFrom.toISOString(),
|
|
|
+ traineeValidTo: validTo.toISOString()
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('设置学员失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ toast.success('设置学员成功');
|
|
|
+ setTraineeDialogOpen(false);
|
|
|
+ setSelectedUser(null);
|
|
|
+ setTraineeValidFrom('');
|
|
|
+ setTraineeValidTo('');
|
|
|
+ refetch();
|
|
|
+ } catch (error) {
|
|
|
+ toast.error(error instanceof Error ? error.message : '设置失败,请重试');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理撤销学员
|
|
|
+ const handleRemoveTrainee = async () => {
|
|
|
+ if (!selectedUser) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await userClient[':id']['$put']({
|
|
|
+ param: { id: selectedUser.id.toString() },
|
|
|
+ json: {
|
|
|
+ userType: UserType.STUDENT,
|
|
|
+ traineeValidFrom: null,
|
|
|
+ traineeValidTo: null
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('撤销学员失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ toast.success('撤销学员成功');
|
|
|
+ setTraineeDialogOpen(false);
|
|
|
+ setSelectedUser(null);
|
|
|
+ refetch();
|
|
|
+ } catch (error) {
|
|
|
+ toast.error(error instanceof Error ? error.message : '撤销失败,请重试');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 加载状态
|
|
|
if (isLoading) {
|
|
|
return (
|
|
|
@@ -265,6 +347,8 @@ export const UsersPage = () => {
|
|
|
<TableHead>邮箱</TableHead>
|
|
|
<TableHead>真实姓名</TableHead>
|
|
|
<TableHead>用户类型</TableHead>
|
|
|
+ <TableHead>学员状态</TableHead>
|
|
|
+ <TableHead>学员有效期</TableHead>
|
|
|
<TableHead>状态</TableHead>
|
|
|
<TableHead>创建时间</TableHead>
|
|
|
<TableHead className="text-right">操作</TableHead>
|
|
|
@@ -307,7 +391,24 @@ export const UsersPage = () => {
|
|
|
</Badge>
|
|
|
</TableCell>
|
|
|
<TableCell>
|
|
|
- <Badge
|
|
|
+ <Badge
|
|
|
+ variant={user.userType === 'trainee' ? 'default' : 'secondary'}
|
|
|
+ className={user.userType === 'trainee' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'}
|
|
|
+ >
|
|
|
+ {user.userType === 'trainee' ? '学员' : '非学员'}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {user.userType === 'trainee' && user.traineeValidFrom && user.traineeValidTo ? (
|
|
|
+ <div className="text-sm">
|
|
|
+ <div>{format(new Date(user.traineeValidFrom), 'yyyy-MM-dd')}</div>
|
|
|
+ <div>至</div>
|
|
|
+ <div>{format(new Date(user.traineeValidTo), 'yyyy-MM-dd')}</div>
|
|
|
+ </div>
|
|
|
+ ) : '-'}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <Badge
|
|
|
variant={user.isDisabled === 0 ? 'default' : 'destructive'}
|
|
|
className={user.isDisabled === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}
|
|
|
>
|
|
|
@@ -319,6 +420,27 @@ export const UsersPage = () => {
|
|
|
</TableCell>
|
|
|
<TableCell className="text-right">
|
|
|
<div className="flex justify-end gap-2">
|
|
|
+ {user.userType === 'trainee' ? (
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => showRemoveTraineeDialog(user)}
|
|
|
+ className="text-purple-600 hover:text-purple-800"
|
|
|
+ title="撤销学员身份"
|
|
|
+ >
|
|
|
+ <UserX className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ ) : (
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => showSetTraineeDialog(user)}
|
|
|
+ className="text-purple-600 hover:text-purple-800"
|
|
|
+ title="设置为学员"
|
|
|
+ >
|
|
|
+ <UserCheck className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
<Button
|
|
|
variant="ghost"
|
|
|
size="sm"
|
|
|
@@ -653,6 +775,57 @@ export const UsersPage = () => {
|
|
|
</AlertDialogFooter>
|
|
|
</AlertDialogContent>
|
|
|
</AlertDialog>
|
|
|
+
|
|
|
+ {/* 学员设置对话框 */}
|
|
|
+ <Dialog open={traineeDialogOpen} onOpenChange={setTraineeDialogOpen}>
|
|
|
+ <DialogContent className="sm:max-w-[400px]">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>
|
|
|
+ {selectedUser?.userType === 'trainee' ? '撤销学员身份' : '设置为学员'}
|
|
|
+ </DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ {selectedUser?.userType === 'trainee'
|
|
|
+ ? '确定要撤销该用户的学员身份吗?'
|
|
|
+ : '请设置学员的有效期'}
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ {selectedUser?.userType !== 'trainee' && (
|
|
|
+ <div className="space-y-4">
|
|
|
+ <div>
|
|
|
+ <label className="text-sm font-medium">有效期开始时间</label>
|
|
|
+ <Input
|
|
|
+ type="date"
|
|
|
+ value={traineeValidFrom}
|
|
|
+ onChange={(e) => setTraineeValidFrom(e.target.value)}
|
|
|
+ className="mt-1"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label className="text-sm font-medium">有效期结束时间</label>
|
|
|
+ <Input
|
|
|
+ type="date"
|
|
|
+ value={traineeValidTo}
|
|
|
+ onChange={(e) => setTraineeValidTo(e.target.value)}
|
|
|
+ className="mt-1"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button variant="outline" onClick={() => setTraineeDialogOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={selectedUser?.userType === 'trainee' ? handleRemoveTrainee : handleSetTrainee}
|
|
|
+ className={selectedUser?.userType === 'trainee' ? 'bg-red-600 hover:bg-red-700' : ''}
|
|
|
+ >
|
|
|
+ {selectedUser?.userType === 'trainee' ? '撤销学员' : '设置为学员'}
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
</div>
|
|
|
);
|
|
|
};
|