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 { NavLink, useNavigate } from "react-router"
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@ -12,9 +13,11 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
Shield,
|
Shield,
|
||||||
BookMarked,
|
BookMarked,
|
||||||
|
KeyRound,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useAuth } from "@/hooks/use-auth"
|
import { useAuth } from "@/hooks/use-auth"
|
||||||
|
import { ChangePasswordDialog } from "@/components/admin/ChangePasswordDialog"
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ to: "/", label: "数据看板", icon: LayoutDashboard, end: true },
|
{ to: "/", label: "数据看板", icon: LayoutDashboard, end: true },
|
||||||
@ -33,6 +36,7 @@ const navItems = [
|
|||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { logout } = useAuth()
|
const { logout } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const [changePasswordOpen, setChangePasswordOpen] = useState(false)
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
logout()
|
logout()
|
||||||
@ -40,41 +44,52 @@ export function Sidebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<aside className="flex h-screen w-60 flex-col border-r bg-sidebar-background">
|
||||||
<span className="text-lg font-semibold">多奇管理后台</span>
|
<div className="flex h-14 items-center border-b px-4">
|
||||||
</div>
|
<span className="text-lg font-semibold">多奇管理后台</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 space-y-1 p-3">
|
<nav className="flex-1 space-y-1 p-3">
|
||||||
{navItems.map(({ to, label, icon: Icon, end }) => (
|
{navItems.map(({ to, label, icon: Icon, end }) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={to}
|
key={to}
|
||||||
to={to}
|
to={to}
|
||||||
end={end}
|
end={end}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
||||||
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
|
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
{label}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<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"
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<KeyRound className="h-4 w-4" />
|
||||||
{label}
|
修改密码
|
||||||
</NavLink>
|
</button>
|
||||||
))}
|
<button
|
||||||
</nav>
|
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"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div className="border-t p-3">
|
<ChangePasswordDialog open={changePasswordOpen} onOpenChange={setChangePasswordOpen} />
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<LogOut className="h-4 w-4" />
|
|
||||||
退出登录
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,24 @@ export async function fetchMe(): Promise<ApiResponse<Admin>> {
|
|||||||
return apiClient.get("auth/me").json<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 的查询参数 */
|
/** fetchAdmins 的查询参数 */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user