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 的令牌。 */
.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 {
display: inline-flex;
+48 -10
View File
@@ -4,7 +4,9 @@ import {
FormField,
Icon,
Input,
Modal,
Notice,
PasswordInput,
Select,
Space,
Switch,
@@ -55,6 +57,9 @@ export function AdminPage() {
const [categories, setCategories] = useState<CategorySummary[]>([])
const [tab, setTab] = useState('header')
const [saving, setSaving] = useState(false)
/** 待删除的分类;非 null 时确认弹窗打开 */
const [pendingDelete, setPendingDelete] = useState<CategorySummary | null>(null)
const [deleting, setDeleting] = useState(false)
const load = async () => {
const [nextSession, nextDocument] = await Promise.all([fetchSession(), fetchSettings()])
@@ -94,14 +99,21 @@ export function AdminPage() {
toast.success('已保存')
}
const removeCategory = async (card: CategorySummary) => {
if (!window.confirm(`删除「${card.title}」会同时删除它下面的所有条目,确定继续?`)) return
const result = await deleteCategory(card.id)
const requestDelete = (card: CategorySummary) => setPendingDelete(card)
const confirmDelete = async () => {
if (pendingDelete === null) return
setDeleting(true)
const result = await deleteCategory(pendingDelete.id)
setDeleting(false)
if (!result.ok) {
toast.error(result.error)
return
}
toast.success(`已删除「${card.title}」`)
toast.success(`已删除「${pendingDelete.title}」`)
setPendingDelete(null)
await load()
}
@@ -179,7 +191,7 @@ export function AdminPage() {
categories={categories}
value={draft.sections}
onChange={(sections) => setDraft({ ...draft, sections })}
onDelete={removeCategory}
onDelete={requestDelete}
onReload={load}
/>
) : tab === 'sidebar' ? (
@@ -187,7 +199,7 @@ export function AdminPage() {
categories={categories}
value={draft.sidebar}
onChange={(sidebar) => setDraft({ ...draft, sidebar })}
onDelete={removeCategory}
onDelete={requestDelete}
onReload={load}
/>
) : tab === 'profile' ? (
@@ -228,6 +240,32 @@ export function AdminPage() {
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>
)
}
@@ -663,17 +701,17 @@ function SystemPanel() {
return (
<Space direction="column" gap="lg" align="stretch" className="admin-form">
<FormField label="当前密码" htmlFor="current-password">
<Input
<PasswordInput
id="current-password"
type="password"
autoComplete="current-password"
value={current}
onChange={(event) => setCurrent(event.target.value)}
/>
</FormField>
<FormField label="新密码" htmlFor="next-password" hint="至少 8 位">
<Input
<PasswordInput
id="next-password"
type="password"
autoComplete="new-password"
value={next}
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 { login, setup, type SessionState } from '../../auth'
import { PasswordInput } from './PasswordInput'
/**
* 登录弹窗:首次使用显示「设置密码 + 确认」,之后只显示密码。
@@ -88,10 +87,13 @@ export function LoginDialog({ open, hasAdmin, onClose, onSuccess }: LoginDialogP
<PasswordInput
id="login-password"
placeholder="输入密码"
autoComplete={isSetup ? 'new-password' : 'current-password'}
value={password}
autoFocus
onChange={setPassword}
onEnter={() => void submit()}
onChange={(event) => setPassword(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') void submit()
}}
/>
</FormField>
@@ -100,9 +102,12 @@ export function LoginDialog({ open, hasAdmin, onClose, onSuccess }: LoginDialogP
<PasswordInput
id="login-confirm"
placeholder="再输入一次"
autoComplete="new-password"
value={confirm}
onChange={setConfirm}
onEnter={() => void submit()}
onChange={(event) => setConfirm(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Enter') void submit()
}}
/>
</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>
)
}