refactor(admin): 接入 PasswordInput,删除分类改用 Modal 确认

- 登录弹窗与系统改密统一用 @stash/ui 的 PasswordInput(改密页补上显隐切换)
- 删除分类不再用 window.confirm,改用 Modal + 危险按钮(组件库推荐的组合)
- 删除页面私有的 PasswordInput 组件与 .password-field 样式
This commit is contained in:
2026-10-29 20:46:11 +08:00
parent 5de1382262
commit fe9d1b2659
4 changed files with 61 additions and 77 deletions
+2 -13
View File
@@ -1,4 +1,4 @@
/* 页面级样式:只保留「组件库没有」的部分(背景、外壳、封面兜底、正文版式)。 /* 页面级样式:只保留「组件库没有」的部分(背景、外壳、封面兜底、正文版式)。
颜色 / 间距 / 字号全部取自 @stash/ui 的令牌。 */ 颜色 / 间距 / 字号全部取自 @stash/ui 的令牌。 */
.app { .app {
@@ -907,18 +907,7 @@
} }
} }
/* ---------- 登录密码框 / 条目操作条 / 设置页细节 ---------- */ /* ---------- 条目操作条 / 设置页细节 ---------- */
.password-field {
display: flex;
align-items: center;
gap: var(--su-space-2);
}
.password-field > :first-child {
flex: 1;
min-width: 0;
}
.item-stat { .item-stat {
display: inline-flex; display: inline-flex;
+48 -10
View File
@@ -4,7 +4,9 @@ import {
FormField, FormField,
Icon, Icon,
Input, Input,
Modal,
Notice, Notice,
PasswordInput,
Select, Select,
Space, Space,
Switch, Switch,
@@ -55,6 +57,9 @@ export function AdminPage() {
const [categories, setCategories] = useState<CategorySummary[]>([]) const [categories, setCategories] = useState<CategorySummary[]>([])
const [tab, setTab] = useState('header') const [tab, setTab] = useState('header')
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
/** 待删除的分类;非 null 时确认弹窗打开 */
const [pendingDelete, setPendingDelete] = useState<CategorySummary | null>(null)
const [deleting, setDeleting] = useState(false)
const load = async () => { const load = async () => {
const [nextSession, nextDocument] = await Promise.all([fetchSession(), fetchSettings()]) const [nextSession, nextDocument] = await Promise.all([fetchSession(), fetchSettings()])
@@ -94,14 +99,21 @@ export function AdminPage() {
toast.success('已保存') toast.success('已保存')
} }
const removeCategory = async (card: CategorySummary) => { const requestDelete = (card: CategorySummary) => setPendingDelete(card)
if (!window.confirm(`删除「${card.title}」会同时删除它下面的所有条目,确定继续?`)) return
const result = await deleteCategory(card.id) const confirmDelete = async () => {
if (pendingDelete === null) return
setDeleting(true)
const result = await deleteCategory(pendingDelete.id)
setDeleting(false)
if (!result.ok) { if (!result.ok) {
toast.error(result.error) toast.error(result.error)
return return
} }
toast.success(`已删除「${card.title}」`)
toast.success(`已删除「${pendingDelete.title}」`)
setPendingDelete(null)
await load() await load()
} }
@@ -179,7 +191,7 @@ export function AdminPage() {
categories={categories} categories={categories}
value={draft.sections} value={draft.sections}
onChange={(sections) => setDraft({ ...draft, sections })} onChange={(sections) => setDraft({ ...draft, sections })}
onDelete={removeCategory} onDelete={requestDelete}
onReload={load} onReload={load}
/> />
) : tab === 'sidebar' ? ( ) : tab === 'sidebar' ? (
@@ -187,7 +199,7 @@ export function AdminPage() {
categories={categories} categories={categories}
value={draft.sidebar} value={draft.sidebar}
onChange={(sidebar) => setDraft({ ...draft, sidebar })} onChange={(sidebar) => setDraft({ ...draft, sidebar })}
onDelete={removeCategory} onDelete={requestDelete}
onReload={load} onReload={load}
/> />
) : tab === 'profile' ? ( ) : tab === 'profile' ? (
@@ -228,6 +240,32 @@ export function AdminPage() {
void load() void load()
}} }}
/> />
<Modal
open={pendingDelete !== null}
onClose={() => {
if (!deleting) setPendingDelete(null)
}}
title={pendingDelete === null ? '删除分类' : `删除「${pendingDelete.title}」?`}
size="sm"
footer={
<Space gap="sm" justify="end">
<Button variant="secondary" disabled={deleting} onClick={() => setPendingDelete(null)}>
取消
</Button>
<Button
variant="danger"
loading={deleting}
icon={<Icon name="delete_2_line" />}
onClick={() => void confirmDelete()}
>
删除
</Button>
</Space>
}
>
<Text>删除后,该分类下的所有条目会一起删除,且不可恢复。</Text>
</Modal>
</div> </div>
) )
} }
@@ -663,17 +701,17 @@ function SystemPanel() {
return ( return (
<Space direction="column" gap="lg" align="stretch" className="admin-form"> <Space direction="column" gap="lg" align="stretch" className="admin-form">
<FormField label="当前密码" htmlFor="current-password"> <FormField label="当前密码" htmlFor="current-password">
<Input <PasswordInput
id="current-password" id="current-password"
type="password" autoComplete="current-password"
value={current} value={current}
onChange={(event) => setCurrent(event.target.value)} onChange={(event) => setCurrent(event.target.value)}
/> />
</FormField> </FormField>
<FormField label="新密码" htmlFor="next-password" hint="至少 8 位"> <FormField label="新密码" htmlFor="next-password" hint="至少 8 位">
<Input <PasswordInput
id="next-password" id="next-password"
type="password" autoComplete="new-password"
value={next} value={next}
onChange={(event) => setNext(event.target.value)} onChange={(event) => setNext(event.target.value)}
/> />
+11 -6
View File
@@ -1,7 +1,6 @@
import { Button, FormField, Icon, Modal, Notice, Space, Text } from '@stash/ui' import { Button, FormField, Icon, Modal, Notice, PasswordInput, Space, Text } from '@stash/ui'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { login, setup, type SessionState } from '../../auth' import { login, setup, type SessionState } from '../../auth'
import { PasswordInput } from './PasswordInput'
/** /**
* 登录弹窗:首次使用显示「设置密码 + 确认」,之后只显示密码。 * 登录弹窗:首次使用显示「设置密码 + 确认」,之后只显示密码。
@@ -88,10 +87,13 @@ export function LoginDialog({ open, hasAdmin, onClose, onSuccess }: LoginDialogP
<PasswordInput <PasswordInput
id="login-password" id="login-password"
placeholder="输入密码" placeholder="输入密码"
autoComplete={isSetup ? 'new-password' : 'current-password'}
value={password} value={password}
autoFocus autoFocus
onChange={setPassword} onChange={(event) => setPassword(event.target.value)}
onEnter={() => void submit()} onKeyDown={(event) => {
if (event.key === 'Enter') void submit()
}}
/> />
</FormField> </FormField>
@@ -100,9 +102,12 @@ export function LoginDialog({ open, hasAdmin, onClose, onSuccess }: LoginDialogP
<PasswordInput <PasswordInput
id="login-confirm" id="login-confirm"
placeholder="再输入一次" placeholder="再输入一次"
autoComplete="new-password"
value={confirm} value={confirm}
onChange={setConfirm} onChange={(event) => setConfirm(event.target.value)}
onEnter={() => void submit()} onKeyDown={(event) => {
if (event.key === 'Enter') void submit()
}}
/> />
</FormField> </FormField>
)} )}
@@ -1,48 +0,0 @@
import { Icon, IconButton, Input } from '@stash/ui'
import { useState } from 'react'
/**
* 语言无关的密码可见切换:右侧一个眼睛按钮,切换 input type。
* 错误提示后再点仍然有效(状态在组件里,不会被错误重置)。
*/
type PasswordInputProps = {
id: string
value: string
placeholder?: string
autoFocus?: boolean
onChange: (value: string) => void
onEnter?: () => void
}
export function PasswordInput({
id,
value,
placeholder,
autoFocus = false,
onChange,
onEnter,
}: PasswordInputProps) {
const [visible, setVisible] = useState(false)
return (
<div className="password-field">
<Input
id={id}
type={visible ? 'text' : 'password'}
icon="lock_line"
placeholder={placeholder}
value={value}
autoFocus={autoFocus}
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') onEnter?.()
}}
/>
<IconButton
label={visible ? '隐藏密码' : '显示密码'}
icon={<Icon name={visible ? 'eye_close_line' : 'eye_line'} />}
onClick={() => setVisible((current) => !current)}
/>
</div>
)
}