- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂 - i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏 - 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条 - 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器 - UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正 - 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简 - 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
import { memo, useEffect, useState } from 'react'
|
||
import { Dice5 } from 'lucide-react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { Button } from '@/components/ui/button'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog'
|
||
import { Label } from '@/components/ui/label'
|
||
import { PasswordInput } from '@/components/PasswordInput'
|
||
import { userApi } from '@/api'
|
||
import { toast } from 'sonner'
|
||
import type { User } from '@/types'
|
||
|
||
// 随机密码:12 位,大小写字母 + 数字,满足后端 min=6
|
||
function randomPassword(): string {
|
||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
|
||
let pwd = ''
|
||
const array = new Uint32Array(12)
|
||
crypto.getRandomValues(array)
|
||
for (let i = 0; i < 12; i++) {
|
||
pwd += chars[array[i] % chars.length]
|
||
}
|
||
return pwd
|
||
}
|
||
|
||
interface ResetPasswordDialogProps {
|
||
user: User | null
|
||
onClose: () => void
|
||
/** 重置成功回调(提示信息由调用方决定) */
|
||
onReset: () => void
|
||
}
|
||
|
||
/** 管理员重置密码弹窗 — 重置后目标用户全部旧会话立即失效(后端递增 TokenVersion) */
|
||
export const ResetPasswordDialog = memo(function ResetPasswordDialog({
|
||
user,
|
||
onClose,
|
||
onReset,
|
||
}: ResetPasswordDialogProps) {
|
||
const { t } = useTranslation('users')
|
||
const [password, setPassword] = useState('')
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [error, setError] = useState('')
|
||
|
||
useEffect(() => {
|
||
if (user) {
|
||
setPassword('')
|
||
setError('')
|
||
}
|
||
}, [user])
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
if (password.length < 6) {
|
||
setError(t('form.passwordMin'))
|
||
return
|
||
}
|
||
if (!user) return
|
||
setSubmitting(true)
|
||
try {
|
||
await userApi.resetPassword(user.id, password)
|
||
onClose()
|
||
onReset()
|
||
} catch (err) {
|
||
toast.error(err instanceof Error ? err.message : t('resetFailed'))
|
||
} finally {
|
||
setSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog open={user !== null} onOpenChange={(open) => !open && onClose()}>
|
||
<DialogContent className="max-w-md">
|
||
<DialogHeader>
|
||
<DialogTitle>{t('resetPassword')}</DialogTitle>
|
||
<DialogDescription>
|
||
{t('resetDesc', { name: user?.username ?? '' })}
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="reset-password">{t('newPassword')}</Label>
|
||
<div className="flex gap-2">
|
||
<PasswordInput
|
||
id="reset-password"
|
||
className="flex-1"
|
||
placeholder={t('form.passwordPlaceholder')}
|
||
value={password}
|
||
onChange={(e) => {
|
||
setPassword(e.target.value)
|
||
setError('')
|
||
}}
|
||
autoComplete="new-password"
|
||
required
|
||
/>
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
aria-label={t('randomPassword')}
|
||
title={t('randomPassword')}
|
||
onClick={() => {
|
||
setPassword(randomPassword())
|
||
setError('')
|
||
}}
|
||
>
|
||
<Dice5 className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||
</div>
|
||
<div className="flex justify-end gap-2">
|
||
<Button type="button" variant="outline" onClick={onClose} disabled={submitting}>
|
||
{t('dialog.cancel', { ns: 'common' })}
|
||
</Button>
|
||
<Button type="submit" disabled={submitting || password.length < 6}>
|
||
{submitting ? t('resetting') : t('resetPassword')}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
})
|