refactor: 对接 duoqi-api 管理员管理接口规范
- 精简角色类型:移除 moderator,仅保留 super_admin 和 admin
- Admin 数据模型补全 isActive、updatedAt 字段
- 创建/重置密码改为展示服务端生成的 plainPassword(含复制按钮)
- 新增编辑管理员对话框(用户名、角色、启用/停用状态)
- fetchAdmins 支持分页和筛选参数
- loginWithToken 适配向后兼容的 { authenticated } 响应格式
- 添加内联成功/错误消息提示
This commit is contained in:
parent
66fc078b3c
commit
8e3d4ed190
@ -1,6 +1,6 @@
|
||||
import { apiClient } from "@/lib/api-client"
|
||||
import type { ApiResponse, LoginResponse, RefreshTokenResponse } from "@/types/api"
|
||||
import type { Admin, AdminLoginForm, CreateAdminForm } from "@/types/admin"
|
||||
import type { ApiResponse, LoginResponse, PaginatedResponse, RefreshTokenResponse } from "@/types/api"
|
||||
import type { Admin, CreateAdminRequest, CreateAdminResponse, ResetPasswordResponse, UpdateAdminRequest } from "@/types/admin"
|
||||
|
||||
// ==================== 认证相关 ====================
|
||||
|
||||
@ -9,7 +9,7 @@ import type { Admin, AdminLoginForm, CreateAdminForm } from "@/types/admin"
|
||||
* POST /admin/auth/login
|
||||
*/
|
||||
export async function loginAdmin(
|
||||
credentials: AdminLoginForm
|
||||
credentials: { username: string; password: string }
|
||||
): Promise<ApiResponse<LoginResponse>> {
|
||||
return apiClient.post("auth/login", { json: credentials }).json<ApiResponse<LoginResponse>>()
|
||||
}
|
||||
@ -20,8 +20,8 @@ export async function loginAdmin(
|
||||
*/
|
||||
export async function loginWithToken(
|
||||
token: string
|
||||
): Promise<ApiResponse<LoginResponse>> {
|
||||
return apiClient.post("auth", { json: { token } }).json<ApiResponse<LoginResponse>>()
|
||||
): Promise<ApiResponse<{ authenticated: boolean }>> {
|
||||
return apiClient.post("auth", { json: { token } }).json<ApiResponse<{ authenticated: boolean }>>()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,12 +46,30 @@ export async function fetchMe(): Promise<ApiResponse<Admin>> {
|
||||
|
||||
// ==================== 管理员管理 ====================
|
||||
|
||||
/** fetchAdmins 的查询参数 */
|
||||
export interface FetchAdminsParams {
|
||||
page?: number
|
||||
limit?: number
|
||||
role?: "admin" | "super_admin"
|
||||
isActive?: 0 | 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理员列表
|
||||
* GET /admin/admins
|
||||
* 获取管理员列表(支持分页和筛选)
|
||||
* GET /admin/admins?page=&limit=&role=&isActive=
|
||||
*/
|
||||
export async function fetchAdmins(): Promise<ApiResponse<Admin[]>> {
|
||||
return apiClient.get("admins").json<ApiResponse<Admin[]>>()
|
||||
export async function fetchAdmins(
|
||||
params?: FetchAdminsParams
|
||||
): Promise<PaginatedResponse<Admin>> {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params?.page) searchParams.set("page", String(params.page))
|
||||
if (params?.limit) searchParams.set("limit", String(params.limit))
|
||||
if (params?.role) searchParams.set("role", params.role)
|
||||
if (params?.isActive !== undefined) searchParams.set("isActive", String(params.isActive))
|
||||
|
||||
const qs = searchParams.toString()
|
||||
const path = qs ? `admins?${qs}` : "admins"
|
||||
return apiClient.get(path).json<PaginatedResponse<Admin>>()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,43 +81,44 @@ export async function fetchAdmin(id: string): Promise<ApiResponse<Admin>> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建管理员
|
||||
* 创建管理员(super_admin 专属)
|
||||
* POST /admin/admins
|
||||
* 服务端生成密码,响应包含 plainPassword
|
||||
*/
|
||||
export async function createAdmin(
|
||||
data: CreateAdminForm
|
||||
): Promise<ApiResponse<Admin>> {
|
||||
return apiClient.post("admins", { json: data }).json<ApiResponse<Admin>>()
|
||||
data: CreateAdminRequest
|
||||
): Promise<ApiResponse<CreateAdminResponse>> {
|
||||
return apiClient.post("admins", { json: data }).json<ApiResponse<CreateAdminResponse>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新管理员信息
|
||||
* 更新管理员信息(super_admin 专属)
|
||||
* PUT /admin/admins/:id
|
||||
*/
|
||||
export async function updateAdmin(
|
||||
id: string,
|
||||
data: Partial<CreateAdminForm>
|
||||
data: UpdateAdminRequest
|
||||
): Promise<ApiResponse<Admin>> {
|
||||
return apiClient.put(`admins/${id}`, { json: data }).json<ApiResponse<Admin>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
* 软删除管理员(super_admin 专属)
|
||||
* DELETE /admin/admins/:id
|
||||
*/
|
||||
export async function deleteAdmin(id: string): Promise<ApiResponse<{ id: string }>> {
|
||||
return apiClient.delete(`admins/${id}`).json<ApiResponse<{ id: string }>>()
|
||||
export async function deleteAdmin(id: string): Promise<ApiResponse<Admin>> {
|
||||
return apiClient.delete(`admins/${id}`).json<ApiResponse<Admin>>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置管理员密码
|
||||
* 重置管理员密码(super_admin 专属)
|
||||
* POST /admin/admins/:id/reset-password
|
||||
* 服务端生成随机密码,响应包含 plainPassword
|
||||
*/
|
||||
export async function resetAdminPassword(
|
||||
id: string,
|
||||
newPassword: string
|
||||
): Promise<ApiResponse<Admin>> {
|
||||
id: string
|
||||
): Promise<ApiResponse<ResetPasswordResponse>> {
|
||||
return apiClient
|
||||
.post(`admins/${id}/reset-password`, { json: { password: newPassword } })
|
||||
.json<ApiResponse<Admin>>()
|
||||
.post(`admins/${id}/reset-password`)
|
||||
.json<ApiResponse<ResetPasswordResponse>>()
|
||||
}
|
||||
|
||||
@ -107,5 +107,4 @@ export const SETTING_CATEGORY_LABELS: Record<SettingCategory, string> = {
|
||||
export const ADMIN_ROLE_LABELS: Record<AdminRole, string> = {
|
||||
super_admin: "超级管理员",
|
||||
admin: "管理员",
|
||||
moderator: "审核员",
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Plus, Trash2, Shield, ShieldAlert, Key } from "lucide-react"
|
||||
import { Check, Copy, Pencil, Plus, Shield, Trash2, Key } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Table,
|
||||
@ -27,6 +27,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -37,44 +38,94 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { fetchAdmins, createAdmin, deleteAdmin, resetAdminPassword } from "@/lib/api/admin-api"
|
||||
import {
|
||||
fetchAdmins,
|
||||
createAdmin,
|
||||
updateAdmin,
|
||||
deleteAdmin,
|
||||
resetAdminPassword,
|
||||
} from "@/lib/api/admin-api"
|
||||
import { ADMIN_ROLE_LABELS } from "@/lib/constants"
|
||||
import type { Admin, AdminRole, CreateAdminForm } from "@/types/admin"
|
||||
import { getCurrentAdminId } from "@/lib/auth"
|
||||
import type { Admin, AdminRole, CreateAdminRequest, UpdateAdminRequest } from "@/types/admin"
|
||||
|
||||
const roleIcons = {
|
||||
super_admin: Shield,
|
||||
admin: Shield,
|
||||
moderator: ShieldAlert,
|
||||
// ==================== 内联消息组件 ====================
|
||||
|
||||
function InlineMessage({
|
||||
variant,
|
||||
children,
|
||||
}: {
|
||||
variant: "success" | "error"
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const base = "rounded-md border px-4 py-3 text-sm"
|
||||
const styles =
|
||||
variant === "success"
|
||||
? "border-green-200 bg-green-50 text-green-800"
|
||||
: "border-red-200 bg-red-50 text-red-800"
|
||||
return <div className={`${base} ${styles}`}>{children}</div>
|
||||
}
|
||||
|
||||
const roleBadgeVariants: Record<AdminRole, "default" | "secondary"> = {
|
||||
super_admin: "default",
|
||||
admin: "default",
|
||||
moderator: "secondary",
|
||||
// ==================== 复制按钮 ====================
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
async function handleCopy() {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" onClick={handleCopy}>
|
||||
{copied ? <Check className="size-3.5 mr-1" /> : <Copy className="size-3.5 mr-1" />}
|
||||
{copied ? "已复制" : "复制"}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 主页面 ====================
|
||||
|
||||
export default function AdminsPage() {
|
||||
const [admins, setAdmins] = useState<Admin[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [pageMessage, setPageMessage] = useState<{
|
||||
variant: "success" | "error"
|
||||
text: string
|
||||
} | null>(null)
|
||||
|
||||
// 对话框状态
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
const [resetPasswordOpen, setResetPasswordOpen] = useState(false)
|
||||
const [resetOpen, setResetOpen] = useState(false)
|
||||
const [passwordResultOpen, setPasswordResultOpen] = useState(false)
|
||||
const [selectedAdmin, setSelectedAdmin] = useState<Admin | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState<CreateAdminForm>({
|
||||
// 创建管理员表单
|
||||
const [createForm, setCreateForm] = useState<CreateAdminRequest>({
|
||||
username: "",
|
||||
password: "",
|
||||
role: "moderator",
|
||||
role: "admin",
|
||||
})
|
||||
|
||||
// 重置密码表单
|
||||
const [resetPasswordData, setResetPasswordData] = useState({
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
// 编辑管理员表单
|
||||
const [editForm, setEditForm] = useState<UpdateAdminRequest & { username: string }>({
|
||||
username: "",
|
||||
role: "admin",
|
||||
isActive: 1,
|
||||
})
|
||||
|
||||
// 密码结果(创建或重置返回的 plainPassword)
|
||||
const [passwordResult, setPasswordResult] = useState<{
|
||||
username: string
|
||||
password: string
|
||||
} | null>(null)
|
||||
|
||||
const currentAdminId = getCurrentAdminId()
|
||||
|
||||
const loadAdmins = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@ -91,14 +142,30 @@ export default function AdminsPage() {
|
||||
loadAdmins()
|
||||
}, [loadAdmins])
|
||||
|
||||
// 清除页面消息
|
||||
useEffect(() => {
|
||||
if (pageMessage) {
|
||||
const timer = setTimeout(() => setPageMessage(null), 5000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [pageMessage])
|
||||
|
||||
// ---- 对话框操作 ----
|
||||
|
||||
function openCreateDialog() {
|
||||
setSelectedAdmin(null)
|
||||
setFormData({
|
||||
username: "",
|
||||
password: "",
|
||||
role: "moderator",
|
||||
setCreateForm({ username: "", password: "", role: "admin" })
|
||||
setPasswordResult(null)
|
||||
setCreateOpen(true)
|
||||
}
|
||||
|
||||
function openEditDialog(admin: Admin) {
|
||||
setSelectedAdmin(admin)
|
||||
setEditForm({
|
||||
username: admin.username,
|
||||
role: admin.role,
|
||||
isActive: admin.isActive as 0 | 1,
|
||||
})
|
||||
setDialogOpen(true)
|
||||
setEditOpen(true)
|
||||
}
|
||||
|
||||
function openDeleteDialog(admin: Admin) {
|
||||
@ -106,21 +173,58 @@ export default function AdminsPage() {
|
||||
setDeleteOpen(true)
|
||||
}
|
||||
|
||||
function openResetPasswordDialog(admin: Admin) {
|
||||
function openResetDialog(admin: Admin) {
|
||||
setSelectedAdmin(admin)
|
||||
setResetPasswordData({ newPassword: "", confirmPassword: "" })
|
||||
setResetPasswordOpen(true)
|
||||
setResetOpen(true)
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formData.password) {
|
||||
return
|
||||
}
|
||||
// ---- 提交操作 ----
|
||||
|
||||
async function handleCreate() {
|
||||
if (!createForm.username || !createForm.password) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await createAdmin(formData)
|
||||
setDialogOpen(false)
|
||||
const res = await createAdmin(createForm)
|
||||
if (res.success && res.data) {
|
||||
setPasswordResult({ username: res.data.username, password: res.data.plainPassword })
|
||||
await loadAdmins()
|
||||
} else {
|
||||
setPageMessage({ variant: "error", text: res.error?.message ?? "创建失败" })
|
||||
setCreateOpen(false)
|
||||
}
|
||||
} catch {
|
||||
setPageMessage({ variant: "error", text: "网络错误,创建失败" })
|
||||
setCreateOpen(false)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
if (!selectedAdmin) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const body: UpdateAdminRequest = {}
|
||||
if (editForm.username && editForm.username !== selectedAdmin.username) {
|
||||
body.username = editForm.username
|
||||
}
|
||||
if (editForm.role && editForm.role !== selectedAdmin.role) {
|
||||
body.role = editForm.role
|
||||
}
|
||||
if (editForm.isActive !== undefined && editForm.isActive !== selectedAdmin.isActive) {
|
||||
body.isActive = editForm.isActive as 0 | 1
|
||||
}
|
||||
|
||||
const res = await updateAdmin(selectedAdmin.id, body)
|
||||
if (res.success) {
|
||||
setEditOpen(false)
|
||||
setPageMessage({ variant: "success", text: "管理员信息已更新" })
|
||||
await loadAdmins()
|
||||
} else {
|
||||
setPageMessage({ variant: "error", text: res.error?.message ?? "更新失败" })
|
||||
}
|
||||
} catch {
|
||||
setPageMessage({ variant: "error", text: "网络错误,更新失败" })
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@ -128,28 +232,48 @@ export default function AdminsPage() {
|
||||
|
||||
async function handleDelete() {
|
||||
if (!selectedAdmin) return
|
||||
await deleteAdmin(selectedAdmin.id)
|
||||
setDeleteOpen(false)
|
||||
setSelectedAdmin(null)
|
||||
await loadAdmins()
|
||||
}
|
||||
|
||||
async function handleResetPassword() {
|
||||
if (!selectedAdmin || resetPasswordData.newPassword !== resetPasswordData.confirmPassword) {
|
||||
return
|
||||
}
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await resetAdminPassword(selectedAdmin.id, resetPasswordData.newPassword)
|
||||
setResetPasswordOpen(false)
|
||||
// TODO: 显示成功提示
|
||||
const res = await deleteAdmin(selectedAdmin.id)
|
||||
if (res.success) {
|
||||
setDeleteOpen(false)
|
||||
setSelectedAdmin(null)
|
||||
setPageMessage({ variant: "success", text: "管理员已停用" })
|
||||
await loadAdmins()
|
||||
} else {
|
||||
setPageMessage({ variant: "error", text: res.error?.message ?? "删除失败" })
|
||||
setDeleteOpen(false)
|
||||
}
|
||||
} catch {
|
||||
setPageMessage({ variant: "error", text: "网络错误,删除失败" })
|
||||
setDeleteOpen(false)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前管理员的信息(从 localStorage)
|
||||
const currentAdminId = localStorage.getItem("duoqi_admin_current_id")
|
||||
async function handleResetPassword() {
|
||||
if (!selectedAdmin) return
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await resetAdminPassword(selectedAdmin.id)
|
||||
if (res.success && res.data) {
|
||||
setResetOpen(false)
|
||||
setPasswordResult({ username: res.data.username, password: res.data.plainPassword })
|
||||
setPasswordResultOpen(true)
|
||||
} else {
|
||||
setPageMessage({ variant: "error", text: res.error?.message ?? "重置失败" })
|
||||
setResetOpen(false)
|
||||
}
|
||||
} catch {
|
||||
setPageMessage({ variant: "error", text: "网络错误,重置失败" })
|
||||
setResetOpen(false)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 渲染 ----
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -166,12 +290,17 @@ export default function AdminsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{pageMessage && (
|
||||
<InlineMessage variant={pageMessage.variant}>{pageMessage.text}</InlineMessage>
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>最后登录</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
@ -180,25 +309,24 @@ export default function AdminsPage() {
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
|
||||
<TableCell colSpan={6} className="h-24 text-center text-muted-foreground">
|
||||
加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : admins.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
|
||||
<TableCell colSpan={6} className="h-24 text-center text-muted-foreground">
|
||||
暂无管理员账号
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
admins.map((admin) => {
|
||||
const RoleIcon = roleIcons[admin.role]
|
||||
const isCurrentUser = admin.id === currentAdminId
|
||||
return (
|
||||
<TableRow key={admin.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<RoleIcon className="size-4 text-muted-foreground" />
|
||||
<Shield className="size-4 text-muted-foreground" />
|
||||
{admin.username}
|
||||
{isCurrentUser && (
|
||||
<Badge variant="outline" className="text-xs">当前账号</Badge>
|
||||
@ -206,10 +334,15 @@ export default function AdminsPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={roleBadgeVariants[admin.role]}>
|
||||
<Badge variant={admin.role === "super_admin" ? "default" : "secondary"}>
|
||||
{ADMIN_ROLE_LABELS[admin.role]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={admin.isActive ? "default" : "outline"}>
|
||||
{admin.isActive ? "活跃" : "停用"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{new Date(admin.createdAt).toLocaleString("zh-CN")}
|
||||
</TableCell>
|
||||
@ -223,7 +356,15 @@ export default function AdminsPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-xs"
|
||||
onClick={() => openResetPasswordDialog(admin)}
|
||||
onClick={() => openEditDialog(admin)}
|
||||
title="编辑"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-xs"
|
||||
onClick={() => openResetDialog(admin)}
|
||||
title="重置密码"
|
||||
>
|
||||
<Key className="size-3.5" />
|
||||
@ -234,7 +375,7 @@ export default function AdminsPage() {
|
||||
className="text-destructive"
|
||||
onClick={() => openDeleteDialog(admin)}
|
||||
disabled={isCurrentUser}
|
||||
title={isCurrentUser ? "不能删除当前账号" : "删除"}
|
||||
title={isCurrentUser ? "不能停用当前账号" : "停用"}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
@ -249,7 +390,7 @@ export default function AdminsPage() {
|
||||
</div>
|
||||
|
||||
{/* 创建管理员对话框 */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建管理员</DialogTitle>
|
||||
@ -258,35 +399,54 @@ export default function AdminsPage() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{passwordResult ? (
|
||||
// 创建成功,显示生成的密码
|
||||
<div className="space-y-4 py-4">
|
||||
<InlineMessage variant="success">
|
||||
管理员「{passwordResult.username}」已创建成功
|
||||
</InlineMessage>
|
||||
<div>
|
||||
<Label>初始密码</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<code className="flex-1 rounded bg-muted px-3 py-2 text-sm font-mono">
|
||||
{passwordResult.password}
|
||||
</code>
|
||||
<CopyButton text={passwordResult.password} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
请复制密码并妥善保存,此密码仅显示一次
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 创建表单
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="username">用户名</Label>
|
||||
<Label htmlFor="create-username">用户名</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
placeholder="请输入用户名"
|
||||
id="create-username"
|
||||
value={createForm.username}
|
||||
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
|
||||
placeholder="请输入用户名(3-50字符)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Label htmlFor="create-password">密码</Label>
|
||||
<Input
|
||||
id="password"
|
||||
id="create-password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="请输入密码(至少 6 位)"
|
||||
value={createForm.password}
|
||||
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
||||
placeholder="请输入密码(8-128字符)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="role">角色</Label>
|
||||
<Label htmlFor="create-role">角色</Label>
|
||||
<Select
|
||||
value={formData.role}
|
||||
onValueChange={(val) => setFormData({ ...formData, role: val as AdminRole })}
|
||||
value={createForm.role}
|
||||
onValueChange={(val) => setCreateForm({ ...createForm, role: val as AdminRole })}
|
||||
>
|
||||
<SelectTrigger id="role">
|
||||
<SelectTrigger id="create-role">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -299,14 +459,86 @@ export default function AdminsPage() {
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={submitting}>
|
||||
{passwordResult ? (
|
||||
<Button onClick={() => setCreateOpen(false)}>完成</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" onClick={() => setCreateOpen(false)} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting || !formData.username || !formData.password}>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={submitting || !createForm.username || createForm.password.length < 8}
|
||||
>
|
||||
{submitting ? "创建中..." : "创建"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 编辑管理员对话框 */}
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑管理员</DialogTitle>
|
||||
<DialogDescription>
|
||||
修改「{selectedAdmin?.username}」的信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="edit-username">用户名</Label>
|
||||
<Input
|
||||
id="edit-username"
|
||||
value={editForm.username}
|
||||
onChange={(e) => setEditForm({ ...editForm, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-role">角色</Label>
|
||||
<Select
|
||||
value={editForm.role}
|
||||
onValueChange={(val) => setEditForm({ ...editForm, role: val as AdminRole })}
|
||||
>
|
||||
<SelectTrigger id="edit-role">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(ADMIN_ROLE_LABELS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div>
|
||||
<Label>账号状态</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{editForm.isActive ? "账号活跃中" : "账号已停用"}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={editForm.isActive === 1}
|
||||
onCheckedChange={(checked) => setEditForm({ ...editForm, isActive: checked ? 1 : 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditOpen(false)} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleEdit} disabled={submitting}>
|
||||
{submitting ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@ -315,74 +547,67 @@ export default function AdminsPage() {
|
||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
||||
<AlertDialogTitle>确认停用</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除管理员"{selectedAdmin?.username}"吗?此操作不可撤销。
|
||||
确定要停用管理员「{selectedAdmin?.username}」吗?停用后该账号将无法登录管理后台。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={submitting}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-destructive text-white hover:bg-destructive/90"
|
||||
disabled={submitting}
|
||||
>
|
||||
删除
|
||||
{submitting ? "停用中..." : "确认停用"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 重置密码对话框 */}
|
||||
<Dialog open={resetPasswordOpen} onOpenChange={setResetPasswordOpen}>
|
||||
{/* 重置密码确认 */}
|
||||
<AlertDialog open={resetOpen} onOpenChange={setResetOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>重置密码</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定为「{selectedAdmin?.username}」生成新密码?系统将随机生成一个新密码,旧密码将立即失效。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={submitting}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleResetPassword} disabled={submitting}>
|
||||
{submitting ? "生成中..." : "确认重置"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 密码结果对话框(重置密码后显示) */}
|
||||
<Dialog open={passwordResultOpen} onOpenChange={setPasswordResultOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>重置密码</DialogTitle>
|
||||
<DialogTitle>密码已重置</DialogTitle>
|
||||
<DialogDescription>
|
||||
为管理员"{selectedAdmin?.username}"设置新密码
|
||||
管理员「{passwordResult?.username}」的新密码已生成
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div>
|
||||
<Label htmlFor="new-password">新密码</Label>
|
||||
<Input
|
||||
id="new-password"
|
||||
type="password"
|
||||
value={resetPasswordData.newPassword}
|
||||
onChange={(e) =>
|
||||
setResetPasswordData({ ...resetPasswordData, newPassword: e.target.value })
|
||||
}
|
||||
placeholder="请输入新密码(至少 6 位)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="confirm-password">确认密码</Label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
value={resetPasswordData.confirmPassword}
|
||||
onChange={(e) =>
|
||||
setResetPasswordData({ ...resetPasswordData, confirmPassword: e.target.value })
|
||||
}
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
{resetPasswordData.newPassword !== resetPasswordData.confirmPassword && (
|
||||
<p className="text-sm text-destructive mt-1">两次输入的密码不一致</p>
|
||||
)}
|
||||
<div className="py-4">
|
||||
<Label>新密码</Label>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<code className="flex-1 rounded bg-muted px-3 py-2 text-sm font-mono">
|
||||
{passwordResult?.password}
|
||||
</code>
|
||||
<CopyButton text={passwordResult?.password ?? ""} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
请复制密码并妥善保存,此密码仅显示一次
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setResetPasswordOpen(false)} disabled={submitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleResetPassword}
|
||||
disabled={submitting || !resetPasswordData.newPassword || resetPasswordData.newPassword !== resetPasswordData.confirmPassword}
|
||||
>
|
||||
{submitting ? "重置中..." : "确认重置"}
|
||||
</Button>
|
||||
<Button onClick={() => setPasswordResultOpen(false)}>完成</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -51,10 +51,15 @@ export default function LoginPage() {
|
||||
async function handleTokenLogin(data: TokenLoginForm) {
|
||||
setError("")
|
||||
try {
|
||||
const response: ApiResponse<LoginResponse> = await loginWithToken(data.token)
|
||||
const response = await loginWithToken(data.token)
|
||||
|
||||
if (response.success && response.data) {
|
||||
login(response.data.accessToken, response.data.refreshToken, response.data.admin)
|
||||
if (response.success && response.data?.authenticated) {
|
||||
// Token 认证成功 — 使用 token 本身作为 access token(向后兼容模式)
|
||||
login(data.token, "", {
|
||||
id: "token-admin",
|
||||
username: "admin",
|
||||
role: "super_admin",
|
||||
})
|
||||
navigate("/")
|
||||
} else if (response.error) {
|
||||
setError(response.error.message)
|
||||
@ -64,7 +69,7 @@ export default function LoginPage() {
|
||||
login(`offline_${data.token}`, `offline_refresh_${data.token}`, {
|
||||
id: "offline-admin",
|
||||
username: "admin",
|
||||
role: "admin",
|
||||
role: "super_admin",
|
||||
})
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import type { AdminUser } from "./api"
|
||||
|
||||
// 管理员角色类型(匹配 duoqi-api 规范)
|
||||
export type AdminRole = "super_admin" | "admin" | "moderator"
|
||||
export type AdminRole = "super_admin" | "admin"
|
||||
|
||||
// 完整的管理员信息
|
||||
// 完整的管理员信息(匹配 GET /admin/admins 响应)
|
||||
export interface Admin {
|
||||
id: string
|
||||
username: string
|
||||
role: AdminRole
|
||||
isActive: number
|
||||
lastLoginAt: string | null
|
||||
createdAt: string
|
||||
lastLoginAt?: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 登录表单(用户名密码)
|
||||
@ -25,9 +27,35 @@ export interface AdminSession {
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
// 创建管理员表单
|
||||
export interface CreateAdminForm {
|
||||
// 创建管理员请求(服务端生成密码)
|
||||
export interface CreateAdminRequest {
|
||||
username: string
|
||||
password: string
|
||||
role: AdminRole
|
||||
}
|
||||
|
||||
// 创建管理员响应(含服务端生成的明文密码)
|
||||
export interface CreateAdminResponse {
|
||||
id: string
|
||||
username: string
|
||||
role: AdminRole
|
||||
isActive: number
|
||||
lastLoginAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
plainPassword: string
|
||||
}
|
||||
|
||||
// 更新管理员请求
|
||||
export interface UpdateAdminRequest {
|
||||
username?: string
|
||||
role?: AdminRole
|
||||
isActive?: 0 | 1
|
||||
}
|
||||
|
||||
// 重置密码响应(含新生成的明文密码)
|
||||
export interface ResetPasswordResponse {
|
||||
adminId: string
|
||||
username: string
|
||||
plainPassword: string
|
||||
}
|
||||
|
||||
@ -26,11 +26,11 @@ export interface PaginatedResponse<T> {
|
||||
error: null
|
||||
}
|
||||
|
||||
// 管理员用户信息
|
||||
// 管理员用户信息(登录响应中的 admin 字段)
|
||||
export interface AdminUser {
|
||||
id: string
|
||||
username: string
|
||||
role: "super_admin" | "admin" | "moderator"
|
||||
role: "super_admin" | "admin"
|
||||
}
|
||||
|
||||
// Token 登录请求
|
||||
|
||||
Loading…
Reference in New Issue
Block a user