feat: 添加当前管理员修改密码功能
All checks were successful
Build & Deploy Admin / deploy (push) Successful in 28s
All checks were successful
Build & Deploy Admin / deploy (push) Successful in 28s
在侧边栏底部新增"修改密码"入口,支持输入当前密码和新密码修改自己的登录密码。
This commit is contained in:
parent
91f88acba7
commit
165625f362
204
src/components/admin/ChangePasswordDialog.tsx
Normal file
204
src/components/admin/ChangePasswordDialog.tsx
Normal file
@ -0,0 +1,204 @@
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { z } from "zod/v4"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Eye, EyeOff } from "lucide-react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { changePassword } from "@/lib/api/admin-api"
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1, "请输入当前密码"),
|
||||
newPassword: z.string().min(8, "新密码至少8个字符").max(128, "密码不超过128个字符"),
|
||||
confirmPassword: z.string().min(1, "请确认新密码"),
|
||||
})
|
||||
.refine((d) => d.newPassword === d.confirmPassword, {
|
||||
message: "两次输入的密码不一致",
|
||||
path: ["confirmPassword"],
|
||||
})
|
||||
.refine((d) => d.currentPassword !== d.newPassword, {
|
||||
message: "新密码不能与当前密码相同",
|
||||
path: ["newPassword"],
|
||||
})
|
||||
|
||||
type FormValues = z.infer<typeof schema>
|
||||
|
||||
interface ChangePasswordDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ChangePasswordDialog({ open, onOpenChange }: ChangePasswordDialogProps) {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [serverError, setServerError] = useState("")
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [showCurrent, setShowCurrent] = useState(false)
|
||||
const [showNew, setShowNew] = useState(false)
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { currentPassword: "", newPassword: "", confirmPassword: "" },
|
||||
})
|
||||
|
||||
function handleOpenChange(nextOpen: boolean) {
|
||||
if (!nextOpen) {
|
||||
form.reset()
|
||||
setServerError("")
|
||||
setSuccess(false)
|
||||
}
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
async function handleSubmit(data: FormValues) {
|
||||
setServerError("")
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const res = await changePassword({
|
||||
currentPassword: data.currentPassword,
|
||||
newPassword: data.newPassword,
|
||||
})
|
||||
if (res.success) {
|
||||
setSuccess(true)
|
||||
} else {
|
||||
const code = res.error?.code ?? ""
|
||||
if (code === "UNAUTHORIZED") {
|
||||
setServerError("当前密码错误")
|
||||
} else if (code === "VALIDATION_ERROR") {
|
||||
setServerError(res.error?.message ?? "参数校验失败")
|
||||
} else if (code === "FORBIDDEN") {
|
||||
setServerError("账户已被禁用")
|
||||
} else {
|
||||
setServerError(res.error?.message ?? "修改失败")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setServerError("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>修改密码</DialogTitle>
|
||||
<DialogDescription>
|
||||
{success ? "密码修改成功" : "修改当前账号的登录密码"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{success ? (
|
||||
<div className="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800">
|
||||
密码已成功修改,下次登录请使用新密码。
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
{serverError && (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{serverError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="currentPassword">当前密码</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="currentPassword"
|
||||
type={showCurrent ? "text" : "password"}
|
||||
autoComplete="current-password"
|
||||
{...form.register("currentPassword")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||
onClick={() => setShowCurrent(!showCurrent)}
|
||||
>
|
||||
{showCurrent ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{form.formState.errors.currentPassword && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.currentPassword.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="newPassword">新密码</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="newPassword"
|
||||
type={showNew ? "text" : "password"}
|
||||
autoComplete="new-password"
|
||||
{...form.register("newPassword")}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2"
|
||||
onClick={() => setShowNew(!showNew)}
|
||||
>
|
||||
{showNew ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
{form.formState.errors.newPassword && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.newPassword.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">确认新密码</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
{...form.register("confirmPassword")}
|
||||
/>
|
||||
{form.formState.errors.confirmPassword && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.confirmPassword.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => handleOpenChange(false)}
|
||||
disabled={submitting}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
{submitting ? "提交中..." : "确认修改"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<DialogFooter>
|
||||
<Button onClick={() => handleOpenChange(false)}>完成</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { useState } from "react"
|
||||
import { NavLink, useNavigate } from "react-router"
|
||||
import {
|
||||
LayoutDashboard,
|
||||
@ -12,9 +13,11 @@ import {
|
||||
AlertCircle,
|
||||
Shield,
|
||||
BookMarked,
|
||||
KeyRound,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAuth } from "@/hooks/use-auth"
|
||||
import { ChangePasswordDialog } from "@/components/admin/ChangePasswordDialog"
|
||||
|
||||
const navItems = [
|
||||
{ to: "/", label: "数据看板", icon: LayoutDashboard, end: true },
|
||||
@ -33,6 +36,7 @@ const navItems = [
|
||||
export function Sidebar() {
|
||||
const { logout } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const [changePasswordOpen, setChangePasswordOpen] = useState(false)
|
||||
|
||||
function handleLogout() {
|
||||
logout()
|
||||
@ -40,6 +44,7 @@ export function Sidebar() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="flex h-screen w-60 flex-col border-r bg-sidebar-background">
|
||||
<div className="flex h-14 items-center border-b px-4">
|
||||
<span className="text-lg font-semibold">多奇管理后台</span>
|
||||
@ -66,7 +71,14 @@ export function Sidebar() {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="border-t p-3">
|
||||
<div className="border-t p-3 space-y-1">
|
||||
<button
|
||||
onClick={() => setChangePasswordOpen(true)}
|
||||
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
修改密码
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
|
||||
@ -76,5 +88,8 @@ export function Sidebar() {
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<ChangePasswordDialog open={changePasswordOpen} onOpenChange={setChangePasswordOpen} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -44,6 +44,24 @@ export async function fetchMe(): Promise<ApiResponse<Admin>> {
|
||||
return apiClient.get("auth/me").json<ApiResponse<Admin>>()
|
||||
}
|
||||
|
||||
// ==================== 修改密码 ====================
|
||||
|
||||
/** 修改密码请求 */
|
||||
export interface ChangePasswordRequest {
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改当前管理员密码
|
||||
* PUT /admin/change-password
|
||||
*/
|
||||
export async function changePassword(
|
||||
data: ChangePasswordRequest
|
||||
): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return apiClient.put("change-password", { json: data }).json<ApiResponse<{ success: boolean }>>()
|
||||
}
|
||||
|
||||
// ==================== 管理员管理 ====================
|
||||
|
||||
/** fetchAdmins 的查询参数 */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user