From 8e3d4ed190d84e8d936c4acf645a4d0f5e737a5a Mon Sep 17 00:00:00 2001 From: Wang Zhuoxuan Date: Sat, 11 Apr 2026 18:53:37 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AF=B9=E6=8E=A5=20duoqi-api=20?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E7=AE=A1=E7=90=86=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 精简角色类型:移除 moderator,仅保留 super_admin 和 admin - Admin 数据模型补全 isActive、updatedAt 字段 - 创建/重置密码改为展示服务端生成的 plainPassword(含复制按钮) - 新增编辑管理员对话框(用户名、角色、启用/停用状态) - fetchAdmins 支持分页和筛选参数 - loginWithToken 适配向后兼容的 { authenticated } 响应格式 - 添加内联成功/错误消息提示 --- src/lib/api/admin-api.ts | 67 +++-- src/lib/constants.ts | 1 - src/routes/admins/index.tsx | 481 ++++++++++++++++++++++++++---------- src/routes/login.tsx | 13 +- src/types/admin.ts | 38 ++- src/types/api.ts | 4 +- 6 files changed, 440 insertions(+), 164 deletions(-) diff --git a/src/lib/api/admin-api.ts b/src/lib/api/admin-api.ts index 7c109e0..7b978bd 100644 --- a/src/lib/api/admin-api.ts +++ b/src/lib/api/admin-api.ts @@ -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> { return apiClient.post("auth/login", { json: credentials }).json>() } @@ -20,8 +20,8 @@ export async function loginAdmin( */ export async function loginWithToken( token: string -): Promise> { - return apiClient.post("auth", { json: { token } }).json>() +): Promise> { + return apiClient.post("auth", { json: { token } }).json>() } /** @@ -46,12 +46,30 @@ export async function fetchMe(): Promise> { // ==================== 管理员管理 ==================== +/** 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> { - return apiClient.get("admins").json>() +export async function fetchAdmins( + params?: FetchAdminsParams +): Promise> { + 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>() } /** @@ -63,43 +81,44 @@ export async function fetchAdmin(id: string): Promise> { } /** - * 创建管理员 + * 创建管理员(super_admin 专属) * POST /admin/admins + * 服务端生成密码,响应包含 plainPassword */ export async function createAdmin( - data: CreateAdminForm -): Promise> { - return apiClient.post("admins", { json: data }).json>() + data: CreateAdminRequest +): Promise> { + return apiClient.post("admins", { json: data }).json>() } /** - * 更新管理员信息 + * 更新管理员信息(super_admin 专属) * PUT /admin/admins/:id */ export async function updateAdmin( id: string, - data: Partial + data: UpdateAdminRequest ): Promise> { return apiClient.put(`admins/${id}`, { json: data }).json>() } /** - * 删除管理员 + * 软删除管理员(super_admin 专属) * DELETE /admin/admins/:id */ -export async function deleteAdmin(id: string): Promise> { - return apiClient.delete(`admins/${id}`).json>() +export async function deleteAdmin(id: string): Promise> { + return apiClient.delete(`admins/${id}`).json>() } /** - * 重置管理员密码 + * 重置管理员密码(super_admin 专属) * POST /admin/admins/:id/reset-password + * 服务端生成随机密码,响应包含 plainPassword */ export async function resetAdminPassword( - id: string, - newPassword: string -): Promise> { + id: string +): Promise> { return apiClient - .post(`admins/${id}/reset-password`, { json: { password: newPassword } }) - .json>() + .post(`admins/${id}/reset-password`) + .json>() } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index dc6fdaa..638bced 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -107,5 +107,4 @@ export const SETTING_CATEGORY_LABELS: Record = { export const ADMIN_ROLE_LABELS: Record = { super_admin: "超级管理员", admin: "管理员", - moderator: "审核员", } diff --git a/src/routes/admins/index.tsx b/src/routes/admins/index.tsx index 77fafb3..b169965 100644 --- a/src/routes/admins/index.tsx +++ b/src/routes/admins/index.tsx @@ -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
{children}
} -const roleBadgeVariants: Record = { - 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 ( + + ) } +// ==================== 主页面 ==================== + export default function AdminsPage() { const [admins, setAdmins] = useState([]) 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(null) const [submitting, setSubmitting] = useState(false) - // 表单状态 - const [formData, setFormData] = useState({ + // 创建管理员表单 + const [createForm, setCreateForm] = useState({ username: "", password: "", - role: "moderator", + role: "admin", }) - // 重置密码表单 - const [resetPasswordData, setResetPasswordData] = useState({ - newPassword: "", - confirmPassword: "", + // 编辑管理员表单 + const [editForm, setEditForm] = useState({ + 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) - await loadAdmins() + 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() { + {pageMessage && ( + {pageMessage.text} + )} +
用户名 角色 + 状态 创建时间 最后登录 操作 @@ -180,25 +309,24 @@ export default function AdminsPage() { {loading ? ( - + 加载中... ) : admins.length === 0 ? ( - + 暂无管理员账号 ) : ( admins.map((admin) => { - const RoleIcon = roleIcons[admin.role] const isCurrentUser = admin.id === currentAdminId return (
- + {admin.username} {isCurrentUser && ( 当前账号 @@ -206,10 +334,15 @@ export default function AdminsPage() {
- + {ADMIN_ROLE_LABELS[admin.role]} + + + {admin.isActive ? "活跃" : "停用"} + + {new Date(admin.createdAt).toLocaleString("zh-CN")} @@ -223,7 +356,15 @@ export default function AdminsPage() { + @@ -249,7 +390,7 @@ export default function AdminsPage() { {/* 创建管理员对话框 */} - + 新建管理员 @@ -258,35 +399,114 @@ export default function AdminsPage() { + {passwordResult ? ( + // 创建成功,显示生成的密码 +
+ + 管理员「{passwordResult.username}」已创建成功 + +
+ +
+ + {passwordResult.password} + + +
+

+ 请复制密码并妥善保存,此密码仅显示一次 +

+
+
+ ) : ( + // 创建表单 +
+
+ + setCreateForm({ ...createForm, username: e.target.value })} + placeholder="请输入用户名(3-50字符)" + /> +
+
+ + setCreateForm({ ...createForm, password: e.target.value })} + placeholder="请输入密码(8-128字符)" + /> +
+
+ + +
+
+ )} + + + {passwordResult ? ( + + ) : ( + <> + + + + )} + +
+
+ + {/* 编辑管理员对话框 */} + + + + 编辑管理员 + + 修改「{selectedAdmin?.username}」的信息 + + +
- + setFormData({ ...formData, username: e.target.value })} - placeholder="请输入用户名" + id="edit-username" + value={editForm.username} + onChange={(e) => setEditForm({ ...editForm, username: e.target.value })} />
-
- - setFormData({ ...formData, password: e.target.value })} - placeholder="请输入密码(至少 6 位)" - /> -
- -
- +
+
+
+ +

+ {editForm.isActive ? "账号活跃中" : "账号已停用"} +

+
+ setEditForm({ ...editForm, isActive: checked ? 1 : 0 })} + /> +
- -
@@ -315,74 +547,67 @@ export default function AdminsPage() { - 确认删除 + 确认停用 - 确定要删除管理员"{selectedAdmin?.username}"吗?此操作不可撤销。 + 确定要停用管理员「{selectedAdmin?.username}」吗?停用后该账号将无法登录管理后台。 - 取消 + 取消 - 删除 + {submitting ? "停用中..." : "确认停用"} - {/* 重置密码对话框 */} - + {/* 重置密码确认 */} + + + + 重置密码 + + 确定为「{selectedAdmin?.username}」生成新密码?系统将随机生成一个新密码,旧密码将立即失效。 + + + + 取消 + + {submitting ? "生成中..." : "确认重置"} + + + + + + {/* 密码结果对话框(重置密码后显示) */} + - 重置密码 + 密码已重置 - 为管理员"{selectedAdmin?.username}"设置新密码 + 管理员「{passwordResult?.username}」的新密码已生成 -
-
- - - setResetPasswordData({ ...resetPasswordData, newPassword: e.target.value }) - } - placeholder="请输入新密码(至少 6 位)" - /> -
- -
- - - setResetPasswordData({ ...resetPasswordData, confirmPassword: e.target.value }) - } - placeholder="请再次输入新密码" - /> - {resetPasswordData.newPassword !== resetPasswordData.confirmPassword && ( -

两次输入的密码不一致

- )} +
+ +
+ + {passwordResult?.password} + +
+

+ 请复制密码并妥善保存,此密码仅显示一次 +

- - +
diff --git a/src/routes/login.tsx b/src/routes/login.tsx index 79ab60b..9d58c44 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -51,10 +51,15 @@ export default function LoginPage() { async function handleTokenLogin(data: TokenLoginForm) { setError("") try { - const response: ApiResponse = 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("/") } diff --git a/src/types/admin.ts b/src/types/admin.ts index 4dffe50..a7328ae 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -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 +} diff --git a/src/types/api.ts b/src/types/api.ts index 23519d5..0c3ce1c 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -26,11 +26,11 @@ export interface PaginatedResponse { error: null } -// 管理员用户信息 +// 管理员用户信息(登录响应中的 admin 字段) export interface AdminUser { id: string username: string - role: "super_admin" | "admin" | "moderator" + role: "super_admin" | "admin" } // Token 登录请求