mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
refine passkey settings list, rename and delete UX
This commit is contained in:
@@ -4,6 +4,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
|
|||||||
import qrcode from 'qrcode-generator';
|
import qrcode from 'qrcode-generator';
|
||||||
import type { Profile } from '@/lib/types';
|
import type { Profile } from '@/lib/types';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
|
import ConfirmDialog from '@/components/ConfirmDialog';
|
||||||
|
|
||||||
interface SettingsPageProps {
|
interface SettingsPageProps {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
@@ -52,6 +53,9 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
const [recoveryMasterPassword, setRecoveryMasterPassword] = useState('');
|
const [recoveryMasterPassword, setRecoveryMasterPassword] = useState('');
|
||||||
const [recoveryCode, setRecoveryCode] = useState('');
|
const [recoveryCode, setRecoveryCode] = useState('');
|
||||||
const [passkeyName, setPasskeyName] = useState('');
|
const [passkeyName, setPasskeyName] = useState('');
|
||||||
|
const [renamePasskey, setRenamePasskey] = useState<{ id: string; name: string } | null>(null);
|
||||||
|
const [renamePasskeyName, setRenamePasskeyName] = useState('');
|
||||||
|
const [deletePasskey, setDeletePasskey] = useState<{ id: string; name: string } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.totpEnabled) {
|
if (!props.totpEnabled) {
|
||||||
@@ -90,6 +94,28 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
props.onNotify?.('success', t('txt_recovery_code_loaded'));
|
props.onNotify?.('success', t('txt_recovery_code_loaded'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: string | null | undefined): string {
|
||||||
|
if (!value) return t('txt_dash');
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return value;
|
||||||
|
return parsed.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRenamePasskey(): Promise<void> {
|
||||||
|
if (!renamePasskey) return;
|
||||||
|
const nextName = renamePasskeyName.trim();
|
||||||
|
if (!nextName) return;
|
||||||
|
await props.onRenamePasskey(renamePasskey.id, nextName);
|
||||||
|
setRenamePasskey(null);
|
||||||
|
setRenamePasskeyName('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDeletePasskey(): Promise<void> {
|
||||||
|
if (!deletePasskey) return;
|
||||||
|
await props.onDeletePasskey(deletePasskey.id);
|
||||||
|
setDeletePasskey(null);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
<section className="card">
|
<section className="card">
|
||||||
@@ -159,19 +185,77 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="muted-inline" style={{ marginBottom: 8 }}>最多 5 个,支持重命名和删除。</p>
|
<p className="muted-inline" style={{ marginBottom: 8 }}>最多 5 个,支持重命名和删除。</p>
|
||||||
<div className="stack">
|
<div className="stack" style={{ gap: 6 }}>
|
||||||
{props.passkeys.map((item) => (
|
{props.passkeys.map((item) => (
|
||||||
<div key={item.id} className="card" style={{ marginBottom: 0 }}>
|
<div
|
||||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
key={item.id}
|
||||||
<input className="input" style={{ flex: 1, minWidth: 180 }} value={item.name} onInput={(e) => void props.onRenamePasskey(item.id, (e.currentTarget as HTMLInputElement).value)} />
|
style={{
|
||||||
<button type="button" className="btn btn-danger" onClick={() => void props.onDeletePasskey(item.id)}>删除</button>
|
display: 'flex',
|
||||||
</div>
|
gap: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
border: '1px solid var(--line)',
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: '10px 12px',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>{item.name}</strong>
|
||||||
|
<span style={{ marginLeft: 'auto', fontSize: 12, opacity: 0.72 }}>
|
||||||
|
创建于 {formatDateTime(item.creationDate)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary small"
|
||||||
|
onClick={() => {
|
||||||
|
setRenamePasskey({ id: item.id, name: item.name });
|
||||||
|
setRenamePasskeyName(item.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('txt_edit')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger small"
|
||||||
|
onClick={() => setDeletePasskey({ id: item.id, name: item.name })}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!props.passkeys.length && <div className="empty">暂无 Passkey</div>}
|
{!props.passkeys.length && <div className="empty">暂无 Passkey</div>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={!!renamePasskey}
|
||||||
|
title={t('txt_edit')}
|
||||||
|
message={t('txt_enter_a_folder_name')}
|
||||||
|
confirmText={t('txt_save')}
|
||||||
|
cancelText={t('txt_cancel')}
|
||||||
|
onConfirm={() => void confirmRenamePasskey()}
|
||||||
|
onCancel={() => {
|
||||||
|
setRenamePasskey(null);
|
||||||
|
setRenamePasskeyName('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="field">
|
||||||
|
<span>{t('txt_name')}</span>
|
||||||
|
<input className="input" value={renamePasskeyName} onInput={(e) => setRenamePasskeyName((e.currentTarget as HTMLInputElement).value)} />
|
||||||
|
</label>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={!!deletePasskey}
|
||||||
|
title={t('txt_delete')}
|
||||||
|
message={deletePasskey ? `确认删除 Passkey「${deletePasskey.name}」吗?` : ''}
|
||||||
|
variant="warning"
|
||||||
|
danger
|
||||||
|
confirmText={t('txt_delete')}
|
||||||
|
cancelText={t('txt_cancel')}
|
||||||
|
onConfirm={() => void confirmDeletePasskey()}
|
||||||
|
onCancel={() => setDeletePasskey(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="settings-twofactor-grid">
|
<div className="settings-twofactor-grid">
|
||||||
<div className="settings-subcard">
|
<div className="settings-subcard">
|
||||||
|
|||||||
Reference in New Issue
Block a user