569 lines
17 KiB
TypeScript
Raw Permalink Normal View History

2026-06-22 14:43:46 +08:00
'use client';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import {
Plus,
Pencil,
Trash2,
KeyRound,
Loader2,
ShieldCheck,
Crown,
} from 'lucide-react';
import { AdminHeader } from '@/components/admin/AdminHeader';
import { PaginationTable, type Column } from '@/components/admin/PaginationTable';
import { TableToolbar } from '@/components/admin/TableToolbar';
import { Avatar } from '@/components/admin/Avatar';
import { ImageUpload } from '@/components/admin/ImageUpload';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { adminApi } from '@/lib/admin-services';
import { useAdminStore } from '@/store/adminStore';
import { formatDate } from '@/lib/utils';
import type { AdminRole, AdminUser } from '@/lib/types';
interface CreateForm {
username: string;
password: string;
nickname: string;
avatar: string;
role: AdminRole;
}
interface EditForm {
nickname: string;
avatar: string;
role: AdminRole;
}
const CREATE_DEFAULT: CreateForm = {
username: '',
password: '',
nickname: '',
avatar: '',
role: 'normal',
};
const ROLE_LABEL: Record<AdminRole, string> = {
super_admin: '超级管理员',
normal: '普通管理员',
};
export default function AdminUserPage() {
const [hydrated, setHydrated] = useState(false);
const [page, setPage] = useState(1);
const [keyword, setKeyword] = useState('');
const [searchKw, setSearchKw] = useState('');
// 创建/编辑
const [open, setOpen] = useState(false);
const [editId, setEditId] = useState<number | null>(null);
const [createForm, setCreateForm] = useState<CreateForm>(CREATE_DEFAULT);
const [editForm, setEditForm] = useState<EditForm>({
nickname: '',
avatar: '',
role: 'normal',
});
const [saving, setSaving] = useState(false);
// 重置密码
const [pwdOpen, setPwdOpen] = useState(false);
const [pwdId, setPwdId] = useState<number | null>(null);
const [newPassword, setNewPassword] = useState('');
const [pwdConfirm, setPwdConfirm] = useState('');
const [pwdSaving, setPwdSaving] = useState(false);
const currentAdminId = useAdminStore((s) => s.admin?.id);
const currentAdminRole = useAdminStore((s) => s.admin?.role);
const isSuperAdmin = currentAdminRole === 'super_admin';
useEffect(() => setHydrated(true), []);
const { data, isLoading, mutate } = useSWR(
hydrated ? ['/admin/admin-user', page, searchKw] : null,
() =>
adminApi.adminUserPaginate({
page,
pageSize: 10,
keyword: searchKw || undefined,
}),
);
const openCreate = () => {
setEditId(null);
setCreateForm(CREATE_DEFAULT);
setOpen(true);
};
const openEdit = async (id: number) => {
const detail = await adminApi.adminUserDetail(id);
setEditId(id);
setEditForm({
nickname: detail.nickname,
avatar: detail.avatar ?? '',
role: detail.role ?? 'normal',
});
setOpen(true);
};
const onSave = async () => {
if (editId) {
if (!editForm.nickname.trim()) {
alert('请填写名称');
return;
}
setSaving(true);
try {
await adminApi.adminUserUpdate(editId, {
nickname: editForm.nickname.trim(),
avatar: editForm.avatar,
role: editForm.role,
});
setOpen(false);
await mutate();
} catch (e) {
alert((e as Error).message);
} finally {
setSaving(false);
}
return;
}
// create
if (!createForm.username.trim() || !createForm.password || !createForm.nickname.trim()) {
alert('请填写登录账号、初始密码和名称');
return;
}
setSaving(true);
try {
await adminApi.adminUserCreate({
username: createForm.username.trim(),
password: createForm.password,
nickname: createForm.nickname.trim(),
avatar: createForm.avatar,
role: createForm.role,
});
setOpen(false);
await mutate();
} catch (e) {
alert((e as Error).message);
} finally {
setSaving(false);
}
};
const onDelete = async (id: number) => {
if (!confirm('确认删除该管理员账号?此操作不可恢复。')) return;
try {
await adminApi.adminUserDelete(id);
await mutate();
} catch (e) {
alert((e as Error).message);
}
};
const openResetPassword = (id: number) => {
setPwdId(id);
setNewPassword('');
setPwdConfirm('');
setPwdOpen(true);
};
const onResetPassword = async () => {
if (!pwdId) return;
if (newPassword.length < 6) {
alert('新密码至少 6 位');
return;
}
if (newPassword !== pwdConfirm) {
alert('两次输入的密码不一致');
return;
}
setPwdSaving(true);
try {
await adminApi.adminUserResetPassword(pwdId, newPassword);
setPwdOpen(false);
} catch (e) {
alert((e as Error).message);
} finally {
setPwdSaving(false);
}
};
const columns: Column<AdminUser>[] = [
{ key: 'id', title: 'ID', width: 60 },
{
key: 'avatar',
title: '头像',
width: 80,
render: (r) => <Avatar src={r.avatar} name={r.nickname} size={36} />,
},
{
key: 'username',
title: '登录账号',
width: 200,
render: (r) => (
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">{r.username}</span>
{r.id === currentAdminId && (
<Badge variant="success">
<ShieldCheck className="h-3 w-3" />
</Badge>
)}
</div>
),
},
{ key: 'nickname', title: '名称', width: 140 },
{
key: 'role',
title: '角色',
width: 130,
render: (r) =>
r.role === 'super_admin' ? (
<Badge variant="warning">
<Crown className="h-3 w-3" /> {ROLE_LABEL[r.role]}
</Badge>
) : (
<Badge variant="secondary">{ROLE_LABEL[r.role]}</Badge>
),
},
{
key: 'createdAt',
title: '创建时间',
width: 160,
render: (r) => formatDate(r.createdAt, 'YYYY-MM-DD HH:mm'),
},
{
key: '_op',
title: '操作',
width: 240,
render: (r) =>
isSuperAdmin ? (
<div className="flex items-center gap-1.5 whitespace-nowrap">
<Button size="sm" variant="outline" onClick={() => openEdit(r.id)}>
<Pencil className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => openResetPassword(r.id)}
>
<KeyRound className="h-3 w-3" />
</Button>
{r.id !== currentAdminId && (
<Button
size="sm"
variant="destructive"
onClick={() => onDelete(r.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
) : (
<span className="text-xs text-gray-400"></span>
),
},
];
return (
<div>
<AdminHeader
title="管理员账号"
description="维护可登录后台的员工账号(支持手机号/邮箱/自定义账号)"
/>
<TableToolbar
left={
<>
<Input
placeholder="搜索 登录账号/名称"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setSearchKw(keyword);
setPage(1);
}
}}
className="max-w-xs"
/>
<Button
variant="outline"
onClick={() => {
setSearchKw(keyword);
setPage(1);
}}
>
</Button>
</>
}
right={
isSuperAdmin ? (
<Button onClick={openCreate}>
<Plus className="h-4 w-4" />
</Button>
) : null
}
/>
<PaginationTable<AdminUser>
columns={columns}
rows={data?.list ?? []}
total={data?.total ?? 0}
page={page}
pageSize={10}
loading={isLoading}
onPageChange={setPage}
rowKey={(r) => String(r.id)}
/>
{/* 创建 / 编辑弹窗 */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>
{editId ? '编辑管理员账号' : '新增管理员账号'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{editId ? (
<>
<div className="space-y-1.5">
<Label></Label>
<Input
value={
data?.list.find((x) => x.id === editId)?.username ?? ''
}
disabled
/>
<p className="text-xs text-gray-500">
</p>
</div>
<div className="space-y-1.5">
<Label> *</Label>
<Input
value={editForm.nickname}
onChange={(e) =>
setEditForm({ ...editForm, nickname: e.target.value })
}
/>
</div>
<div className="space-y-1.5">
<Label></Label>
<ImageUpload
value={editForm.avatar}
onChange={(url) =>
setEditForm({ ...editForm, avatar: url })
}
hint="建议正方形头像,单图 ≤ 2M"
/>
</div>
<div className="space-y-1.5">
<Label> *</Label>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="edit-role"
value="normal"
checked={editForm.role === 'normal'}
onChange={() =>
setEditForm({ ...editForm, role: 'normal' })
}
/>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="edit-role"
value="super_admin"
checked={editForm.role === 'super_admin'}
onChange={() =>
setEditForm({ ...editForm, role: 'super_admin' })
}
/>
</label>
</div>
{editId === currentAdminId && (
<p className="text-xs text-amber-600">
</p>
)}
</div>
</>
) : (
<>
<div className="space-y-1.5">
<Label> *</Label>
<Input
placeholder="可使用手机号、邮箱或自定义账号"
value={createForm.username}
maxLength={50}
onChange={(e) =>
setCreateForm({
...createForm,
username: e.target.value,
})
}
/>
<p className="text-xs text-gray-500">
2-50
</p>
</div>
<div className="space-y-1.5">
<Label> *</Label>
<Input
type="password"
placeholder="至少 6 位"
value={createForm.password}
onChange={(e) =>
setCreateForm({
...createForm,
password: e.target.value,
})
}
/>
<p className="text-xs text-gray-500">
</p>
</div>
<div className="space-y-1.5">
<Label> *</Label>
<Input
placeholder="员工姓名或别名"
value={createForm.nickname}
maxLength={50}
onChange={(e) =>
setCreateForm({
...createForm,
nickname: e.target.value,
})
}
/>
</div>
<div className="space-y-1.5">
<Label></Label>
<ImageUpload
value={createForm.avatar}
onChange={(url) =>
setCreateForm({ ...createForm, avatar: url })
}
hint="可选,建议正方形头像,单图 ≤ 2M"
/>
</div>
<div className="space-y-1.5">
<Label> *</Label>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="create-role"
value="normal"
checked={createForm.role === 'normal'}
onChange={() =>
setCreateForm({ ...createForm, role: 'normal' })
}
/>
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="create-role"
value="super_admin"
checked={createForm.role === 'super_admin'}
onChange={() =>
setCreateForm({ ...createForm, role: 'super_admin' })
}
/>
</label>
</div>
<p className="text-xs text-gray-500">
</p>
</div>
</>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
</Button>
<Button onClick={onSave} disabled={saving}>
{saving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
</>
) : (
'保存'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 重置密码弹窗 */}
<Dialog open={pwdOpen} onOpenChange={setPwdOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label> *</Label>
<Input
type="password"
placeholder="至少 6 位"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label> *</Label>
<Input
type="password"
placeholder="再次输入新密码"
value={pwdConfirm}
onChange={(e) => setPwdConfirm(e.target.value)}
/>
</div>
<p className="text-xs text-gray-500">
</p>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setPwdOpen(false)}>
</Button>
<Button onClick={onResetPassword} disabled={pwdSaving}>
{pwdSaving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
</>
) : (
'确认重置'
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}