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 { apiClient } from "@/lib/api-client"
|
||||||
import type { ApiResponse, LoginResponse, RefreshTokenResponse } from "@/types/api"
|
import type { ApiResponse, LoginResponse, PaginatedResponse, RefreshTokenResponse } from "@/types/api"
|
||||||
import type { Admin, AdminLoginForm, CreateAdminForm } from "@/types/admin"
|
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
|
* POST /admin/auth/login
|
||||||
*/
|
*/
|
||||||
export async function loginAdmin(
|
export async function loginAdmin(
|
||||||
credentials: AdminLoginForm
|
credentials: { username: string; password: string }
|
||||||
): Promise<ApiResponse<LoginResponse>> {
|
): Promise<ApiResponse<LoginResponse>> {
|
||||||
return apiClient.post("auth/login", { json: credentials }).json<ApiResponse<LoginResponse>>()
|
return apiClient.post("auth/login", { json: credentials }).json<ApiResponse<LoginResponse>>()
|
||||||
}
|
}
|
||||||
@ -20,8 +20,8 @@ export async function loginAdmin(
|
|||||||
*/
|
*/
|
||||||
export async function loginWithToken(
|
export async function loginWithToken(
|
||||||
token: string
|
token: string
|
||||||
): Promise<ApiResponse<LoginResponse>> {
|
): Promise<ApiResponse<{ authenticated: boolean }>> {
|
||||||
return apiClient.post("auth", { json: { token } }).json<ApiResponse<LoginResponse>>()
|
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[]>> {
|
export async function fetchAdmins(
|
||||||
return apiClient.get("admins").json<ApiResponse<Admin[]>>()
|
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
|
* POST /admin/admins
|
||||||
|
* 服务端生成密码,响应包含 plainPassword
|
||||||
*/
|
*/
|
||||||
export async function createAdmin(
|
export async function createAdmin(
|
||||||
data: CreateAdminForm
|
data: CreateAdminRequest
|
||||||
): Promise<ApiResponse<Admin>> {
|
): Promise<ApiResponse<CreateAdminResponse>> {
|
||||||
return apiClient.post("admins", { json: data }).json<ApiResponse<Admin>>()
|
return apiClient.post("admins", { json: data }).json<ApiResponse<CreateAdminResponse>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新管理员信息
|
* 更新管理员信息(super_admin 专属)
|
||||||
* PUT /admin/admins/:id
|
* PUT /admin/admins/:id
|
||||||
*/
|
*/
|
||||||
export async function updateAdmin(
|
export async function updateAdmin(
|
||||||
id: string,
|
id: string,
|
||||||
data: Partial<CreateAdminForm>
|
data: UpdateAdminRequest
|
||||||
): Promise<ApiResponse<Admin>> {
|
): Promise<ApiResponse<Admin>> {
|
||||||
return apiClient.put(`admins/${id}`, { json: data }).json<ApiResponse<Admin>>()
|
return apiClient.put(`admins/${id}`, { json: data }).json<ApiResponse<Admin>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除管理员
|
* 软删除管理员(super_admin 专属)
|
||||||
* DELETE /admin/admins/:id
|
* DELETE /admin/admins/:id
|
||||||
*/
|
*/
|
||||||
export async function deleteAdmin(id: string): Promise<ApiResponse<{ id: string }>> {
|
export async function deleteAdmin(id: string): Promise<ApiResponse<Admin>> {
|
||||||
return apiClient.delete(`admins/${id}`).json<ApiResponse<{ id: string }>>()
|
return apiClient.delete(`admins/${id}`).json<ApiResponse<Admin>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置管理员密码
|
* 重置管理员密码(super_admin 专属)
|
||||||
* POST /admin/admins/:id/reset-password
|
* POST /admin/admins/:id/reset-password
|
||||||
|
* 服务端生成随机密码,响应包含 plainPassword
|
||||||
*/
|
*/
|
||||||
export async function resetAdminPassword(
|
export async function resetAdminPassword(
|
||||||
id: string,
|
id: string
|
||||||
newPassword: string
|
): Promise<ApiResponse<ResetPasswordResponse>> {
|
||||||
): Promise<ApiResponse<Admin>> {
|
|
||||||
return apiClient
|
return apiClient
|
||||||
.post(`admins/${id}/reset-password`, { json: { password: newPassword } })
|
.post(`admins/${id}/reset-password`)
|
||||||
.json<ApiResponse<Admin>>()
|
.json<ApiResponse<ResetPasswordResponse>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,5 +107,4 @@ export const SETTING_CATEGORY_LABELS: Record<SettingCategory, string> = {
|
|||||||
export const ADMIN_ROLE_LABELS: Record<AdminRole, string> = {
|
export const ADMIN_ROLE_LABELS: Record<AdminRole, string> = {
|
||||||
super_admin: "超级管理员",
|
super_admin: "超级管理员",
|
||||||
admin: "管理员",
|
admin: "管理员",
|
||||||
moderator: "审核员",
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from "react"
|
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 { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -37,44 +38,94 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog"
|
} 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 { 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,
|
function InlineMessage({
|
||||||
moderator: ShieldAlert,
|
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",
|
function CopyButton({ text }: { text: string }) {
|
||||||
moderator: "secondary",
|
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() {
|
export default function AdminsPage() {
|
||||||
const [admins, setAdmins] = useState<Admin[]>([])
|
const [admins, setAdmins] = useState<Admin[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
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 [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 [selectedAdmin, setSelectedAdmin] = useState<Admin | null>(null)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
// 表单状态
|
// 创建管理员表单
|
||||||
const [formData, setFormData] = useState<CreateAdminForm>({
|
const [createForm, setCreateForm] = useState<CreateAdminRequest>({
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
role: "moderator",
|
role: "admin",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重置密码表单
|
// 编辑管理员表单
|
||||||
const [resetPasswordData, setResetPasswordData] = useState({
|
const [editForm, setEditForm] = useState<UpdateAdminRequest & { username: string }>({
|
||||||
newPassword: "",
|
username: "",
|
||||||
confirmPassword: "",
|
role: "admin",
|
||||||
|
isActive: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 密码结果(创建或重置返回的 plainPassword)
|
||||||
|
const [passwordResult, setPasswordResult] = useState<{
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const currentAdminId = getCurrentAdminId()
|
||||||
|
|
||||||
const loadAdmins = useCallback(async () => {
|
const loadAdmins = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
@ -91,14 +142,30 @@ export default function AdminsPage() {
|
|||||||
loadAdmins()
|
loadAdmins()
|
||||||
}, [loadAdmins])
|
}, [loadAdmins])
|
||||||
|
|
||||||
|
// 清除页面消息
|
||||||
|
useEffect(() => {
|
||||||
|
if (pageMessage) {
|
||||||
|
const timer = setTimeout(() => setPageMessage(null), 5000)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [pageMessage])
|
||||||
|
|
||||||
|
// ---- 对话框操作 ----
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openCreateDialog() {
|
||||||
setSelectedAdmin(null)
|
setCreateForm({ username: "", password: "", role: "admin" })
|
||||||
setFormData({
|
setPasswordResult(null)
|
||||||
username: "",
|
setCreateOpen(true)
|
||||||
password: "",
|
}
|
||||||
role: "moderator",
|
|
||||||
|
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) {
|
function openDeleteDialog(admin: Admin) {
|
||||||
@ -106,21 +173,58 @@ export default function AdminsPage() {
|
|||||||
setDeleteOpen(true)
|
setDeleteOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openResetPasswordDialog(admin: Admin) {
|
function openResetDialog(admin: Admin) {
|
||||||
setSelectedAdmin(admin)
|
setSelectedAdmin(admin)
|
||||||
setResetPasswordData({ newPassword: "", confirmPassword: "" })
|
setResetOpen(true)
|
||||||
setResetPasswordOpen(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
// ---- 提交操作 ----
|
||||||
if (!formData.password) {
|
|
||||||
return
|
async function handleCreate() {
|
||||||
}
|
if (!createForm.username || !createForm.password) return
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await createAdmin(formData)
|
const res = await createAdmin(createForm)
|
||||||
setDialogOpen(false)
|
if (res.success && res.data) {
|
||||||
await loadAdmins()
|
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
@ -128,28 +232,48 @@ export default function AdminsPage() {
|
|||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!selectedAdmin) return
|
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)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await resetAdminPassword(selectedAdmin.id, resetPasswordData.newPassword)
|
const res = await deleteAdmin(selectedAdmin.id)
|
||||||
setResetPasswordOpen(false)
|
if (res.success) {
|
||||||
// TODO: 显示成功提示
|
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 {
|
} finally {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前管理员的信息(从 localStorage)
|
async function handleResetPassword() {
|
||||||
const currentAdminId = localStorage.getItem("duoqi_admin_current_id")
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -166,12 +290,17 @@ export default function AdminsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{pageMessage && (
|
||||||
|
<InlineMessage variant={pageMessage.variant}>{pageMessage.text}</InlineMessage>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="rounded-lg border">
|
<div className="rounded-lg border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>用户名</TableHead>
|
<TableHead>用户名</TableHead>
|
||||||
<TableHead>角色</TableHead>
|
<TableHead>角色</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
<TableHead>创建时间</TableHead>
|
<TableHead>创建时间</TableHead>
|
||||||
<TableHead>最后登录</TableHead>
|
<TableHead>最后登录</TableHead>
|
||||||
<TableHead>操作</TableHead>
|
<TableHead>操作</TableHead>
|
||||||
@ -180,25 +309,24 @@ export default function AdminsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
|
<TableCell colSpan={6} className="h-24 text-center text-muted-foreground">
|
||||||
加载中...
|
加载中...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : admins.length === 0 ? (
|
) : admins.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="h-24 text-center text-muted-foreground">
|
<TableCell colSpan={6} className="h-24 text-center text-muted-foreground">
|
||||||
暂无管理员账号
|
暂无管理员账号
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
admins.map((admin) => {
|
admins.map((admin) => {
|
||||||
const RoleIcon = roleIcons[admin.role]
|
|
||||||
const isCurrentUser = admin.id === currentAdminId
|
const isCurrentUser = admin.id === currentAdminId
|
||||||
return (
|
return (
|
||||||
<TableRow key={admin.id}>
|
<TableRow key={admin.id}>
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RoleIcon className="size-4 text-muted-foreground" />
|
<Shield className="size-4 text-muted-foreground" />
|
||||||
{admin.username}
|
{admin.username}
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<Badge variant="outline" className="text-xs">当前账号</Badge>
|
<Badge variant="outline" className="text-xs">当前账号</Badge>
|
||||||
@ -206,10 +334,15 @@ export default function AdminsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={roleBadgeVariants[admin.role]}>
|
<Badge variant={admin.role === "super_admin" ? "default" : "secondary"}>
|
||||||
{ADMIN_ROLE_LABELS[admin.role]}
|
{ADMIN_ROLE_LABELS[admin.role]}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={admin.isActive ? "default" : "outline"}>
|
||||||
|
{admin.isActive ? "活跃" : "停用"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{new Date(admin.createdAt).toLocaleString("zh-CN")}
|
{new Date(admin.createdAt).toLocaleString("zh-CN")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -223,7 +356,15 @@ export default function AdminsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon-xs"
|
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="重置密码"
|
title="重置密码"
|
||||||
>
|
>
|
||||||
<Key className="size-3.5" />
|
<Key className="size-3.5" />
|
||||||
@ -234,7 +375,7 @@ export default function AdminsPage() {
|
|||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
onClick={() => openDeleteDialog(admin)}
|
onClick={() => openDeleteDialog(admin)}
|
||||||
disabled={isCurrentUser}
|
disabled={isCurrentUser}
|
||||||
title={isCurrentUser ? "不能删除当前账号" : "删除"}
|
title={isCurrentUser ? "不能停用当前账号" : "停用"}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-3.5" />
|
<Trash2 className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -249,7 +390,7 @@ export default function AdminsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 创建管理员对话框 */}
|
{/* 创建管理员对话框 */}
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>新建管理员</DialogTitle>
|
<DialogTitle>新建管理员</DialogTitle>
|
||||||
@ -258,35 +399,114 @@ export default function AdminsPage() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</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="create-username">用户名</Label>
|
||||||
|
<Input
|
||||||
|
id="create-username"
|
||||||
|
value={createForm.username}
|
||||||
|
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
|
||||||
|
placeholder="请输入用户名(3-50字符)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="create-password">密码</Label>
|
||||||
|
<Input
|
||||||
|
id="create-password"
|
||||||
|
type="password"
|
||||||
|
value={createForm.password}
|
||||||
|
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
||||||
|
placeholder="请输入密码(8-128字符)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="create-role">角色</Label>
|
||||||
|
<Select
|
||||||
|
value={createForm.role}
|
||||||
|
onValueChange={(val) => setCreateForm({ ...createForm, role: val as AdminRole })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="create-role">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{Object.entries(ADMIN_ROLE_LABELS).map(([value, label]) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{passwordResult ? (
|
||||||
|
<Button onClick={() => setCreateOpen(false)}>完成</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setCreateOpen(false)} disabled={submitting}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<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 className="space-y-4 py-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="username">用户名</Label>
|
<Label htmlFor="edit-username">用户名</Label>
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="edit-username"
|
||||||
value={formData.username}
|
value={editForm.username}
|
||||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
onChange={(e) => setEditForm({ ...editForm, username: e.target.value })}
|
||||||
placeholder="请输入用户名"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="password">密码</Label>
|
<Label htmlFor="edit-role">角色</Label>
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
||||||
placeholder="请输入密码(至少 6 位)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="role">角色</Label>
|
|
||||||
<Select
|
<Select
|
||||||
value={formData.role}
|
value={editForm.role}
|
||||||
onValueChange={(val) => setFormData({ ...formData, role: val as AdminRole })}
|
onValueChange={(val) => setEditForm({ ...editForm, role: val as AdminRole })}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="role">
|
<SelectTrigger id="edit-role">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -298,14 +518,26 @@ export default function AdminsPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setDialogOpen(false)} disabled={submitting}>
|
<Button variant="outline" onClick={() => setEditOpen(false)} disabled={submitting}>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSubmit} disabled={submitting || !formData.username || !formData.password}>
|
<Button onClick={handleEdit} disabled={submitting}>
|
||||||
{submitting ? "创建中..." : "创建"}
|
{submitting ? "保存中..." : "保存"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@ -315,74 +547,67 @@ export default function AdminsPage() {
|
|||||||
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
<AlertDialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
<AlertDialogTitle>确认停用</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
确定要删除管理员"{selectedAdmin?.username}"吗?此操作不可撤销。
|
确定要停用管理员「{selectedAdmin?.username}」吗?停用后该账号将无法登录管理后台。
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
<AlertDialogCancel disabled={submitting}>取消</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
className="bg-destructive text-white hover:bg-destructive/90"
|
className="bg-destructive text-white hover:bg-destructive/90"
|
||||||
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
删除
|
{submitting ? "停用中..." : "确认停用"}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</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>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>重置密码</DialogTitle>
|
<DialogTitle>密码已重置</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
为管理员"{selectedAdmin?.username}"设置新密码
|
管理员「{passwordResult?.username}」的新密码已生成
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="py-4">
|
||||||
<div>
|
<Label>新密码</Label>
|
||||||
<Label htmlFor="new-password">新密码</Label>
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<Input
|
<code className="flex-1 rounded bg-muted px-3 py-2 text-sm font-mono">
|
||||||
id="new-password"
|
{passwordResult?.password}
|
||||||
type="password"
|
</code>
|
||||||
value={resetPasswordData.newPassword}
|
<CopyButton text={passwordResult?.password ?? ""} />
|
||||||
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>
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
请复制密码并妥善保存,此密码仅显示一次
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setResetPasswordOpen(false)} disabled={submitting}>
|
<Button onClick={() => setPasswordResultOpen(false)}>完成</Button>
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleResetPassword}
|
|
||||||
disabled={submitting || !resetPasswordData.newPassword || resetPasswordData.newPassword !== resetPasswordData.confirmPassword}
|
|
||||||
>
|
|
||||||
{submitting ? "重置中..." : "确认重置"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -51,10 +51,15 @@ export default function LoginPage() {
|
|||||||
async function handleTokenLogin(data: TokenLoginForm) {
|
async function handleTokenLogin(data: TokenLoginForm) {
|
||||||
setError("")
|
setError("")
|
||||||
try {
|
try {
|
||||||
const response: ApiResponse<LoginResponse> = await loginWithToken(data.token)
|
const response = await loginWithToken(data.token)
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data?.authenticated) {
|
||||||
login(response.data.accessToken, response.data.refreshToken, response.data.admin)
|
// Token 认证成功 — 使用 token 本身作为 access token(向后兼容模式)
|
||||||
|
login(data.token, "", {
|
||||||
|
id: "token-admin",
|
||||||
|
username: "admin",
|
||||||
|
role: "super_admin",
|
||||||
|
})
|
||||||
navigate("/")
|
navigate("/")
|
||||||
} else if (response.error) {
|
} else if (response.error) {
|
||||||
setError(response.error.message)
|
setError(response.error.message)
|
||||||
@ -64,7 +69,7 @@ export default function LoginPage() {
|
|||||||
login(`offline_${data.token}`, `offline_refresh_${data.token}`, {
|
login(`offline_${data.token}`, `offline_refresh_${data.token}`, {
|
||||||
id: "offline-admin",
|
id: "offline-admin",
|
||||||
username: "admin",
|
username: "admin",
|
||||||
role: "admin",
|
role: "super_admin",
|
||||||
})
|
})
|
||||||
navigate("/")
|
navigate("/")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import type { AdminUser } from "./api"
|
import type { AdminUser } from "./api"
|
||||||
|
|
||||||
// 管理员角色类型(匹配 duoqi-api 规范)
|
// 管理员角色类型(匹配 duoqi-api 规范)
|
||||||
export type AdminRole = "super_admin" | "admin" | "moderator"
|
export type AdminRole = "super_admin" | "admin"
|
||||||
|
|
||||||
// 完整的管理员信息
|
// 完整的管理员信息(匹配 GET /admin/admins 响应)
|
||||||
export interface Admin {
|
export interface Admin {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
role: AdminRole
|
role: AdminRole
|
||||||
|
isActive: number
|
||||||
|
lastLoginAt: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
lastLoginAt?: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录表单(用户名密码)
|
// 登录表单(用户名密码)
|
||||||
@ -25,9 +27,35 @@ export interface AdminSession {
|
|||||||
refreshToken: string
|
refreshToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建管理员表单
|
// 创建管理员请求(服务端生成密码)
|
||||||
export interface CreateAdminForm {
|
export interface CreateAdminRequest {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
role: AdminRole
|
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
|
error: null
|
||||||
}
|
}
|
||||||
|
|
||||||
// 管理员用户信息
|
// 管理员用户信息(登录响应中的 admin 字段)
|
||||||
export interface AdminUser {
|
export interface AdminUser {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
role: "super_admin" | "admin" | "moderator"
|
role: "super_admin" | "admin"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token 登录请求
|
// Token 登录请求
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user