refactor(admin): 接入 PasswordInput,删除分类改用 Modal 确认
- 登录弹窗与系统改密统一用 @stash/ui 的 PasswordInput(改密页补上显隐切换) - 删除分类不再用 window.confirm,改用 Modal + 危险按钮(组件库推荐的组合) - 删除页面私有的 PasswordInput 组件与 .password-field 样式
This commit is contained in:
+2
-13
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user