feat(settings): 新增设置页重启按钮,区分保存后触发与手动触发

状态从 showRestartDialog: bool 改为 restartDialogMode: 'saved' | 'manual' | null,三态清晰区分触发来源。

改动:- 保存后自动弹出 'saved' 模式对话框(标题 '配置已保存',按钮 '稍后') - 新增独立 '重启' 按钮,点击弹出 'manual' 模式对话框(标题 '重启服务',按钮 '取消'),dirty 时提示未保存更改 - 重启中禁用保存与重启按钮,handleClose 阻断关闭并提示 - ESC 在对话框打开时仅关闭对话框,不关闭整个页面 - 重启按钮 loading 状态与 restarting 同步
This commit is contained in:
oudecheng 2026-08-05 09:43:05 +08:00
parent 2255d2e774
commit 235f57c9ae

View File

@ -49,7 +49,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
const [error, setError] = useState('');
const [toast, setToastState] = useState('');
const [dirty, setDirty] = useState(false);
const [showRestartDialog, setShowRestartDialog] = useState(false);
const [restartDialogMode, setRestartDialogMode] = useState<'saved' | 'manual' | null>(null);
const [restarting, setRestarting] = useState(false);
// toast 自动清除:每次设置非空 toast 时启动 3s 计时器
@ -77,9 +77,13 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
}, []);
const handleClose = useCallback(() => {
if (restarting) {
showToast('服务正在重启,请等待完成');
return;
}
if (dirty && !confirm('有未保存的更改,确定要关闭吗?')) return;
onClose();
}, [dirty, onClose]);
}, [dirty, restarting, showToast, onClose]);
// Load config
useEffect(() => {
@ -93,11 +97,18 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
// ESC 关闭(子模态框的 ESC 在 capture 阶段已 stopPropagation不会触发此处
useEffect(() => {
const h = (e: KeyboardEvent) => {
if (e.key === 'Escape') handleClose();
if (e.key === 'Escape') {
// 重启确认对话框打开时ESC 仅关闭对话框,不关闭整个页面
if (restartDialogMode !== null) {
setRestartDialogMode(null);
return;
}
handleClose();
}
};
document.addEventListener('keydown', h);
return () => document.removeEventListener('keydown', h);
}, [handleClose]);
}, [handleClose, restartDialogMode]);
const update = useCallback(<K extends keyof AppConfig>(key: K, value: AppConfig[K]) => {
setConfig((prev) => (prev ? { ...prev, [key]: value } : prev));
@ -115,13 +126,13 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
// Config is now synced to both disk and in-memory state,
// so the local state is already correct. No need to re-fetch.
setDirty(false);
setShowRestartDialog(true);
setRestartDialogMode('saved');
}
setSaving(false);
};
const handleRestart = async () => {
setShowRestartDialog(false);
setRestartDialogMode(null);
setRestarting(true);
try {
const { status, data } = await restartGateway();
@ -289,12 +300,21 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
</button>
<button
onClick={handleSave}
disabled={saving || !dirty}
disabled={saving || restarting || !dirty}
className="flex items-center gap-2 px-5 py-2 rounded-lg text-sm font-medium text-white bg-[var(--accent-cyan)]/20 border border-[var(--accent-cyan)]/30 hover:bg-[var(--accent-cyan)]/30 hover:border-[var(--accent-cyan)]/50 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
{saving ? '保存中...' : '保存配置'}
</button>
<button
onClick={() => setRestartDialogMode('manual')}
disabled={restarting || saving}
title="重启网关服务"
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-[var(--text-primary)] bg-[var(--bg-tertiary)] border border-[var(--border-color)] hover:bg-[var(--overlay-hover)] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
>
{restarting ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
{restarting ? '重启中...' : '重启'}
</button>
</div>
{/* Toast */}
@ -310,7 +330,7 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
)}
{/* Restart confirmation dialog */}
{showRestartDialog && (
{restartDialogMode && (
<div className="absolute inset-0 z-20 flex items-center justify-center bg-black/50 backdrop-blur-sm rounded-2xl">
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-xl p-6 max-w-sm mx-4 shadow-2xl animate-[scaleIn_0.2s_ease-out]">
<div className="flex items-center gap-3 mb-4">
@ -318,16 +338,24 @@ export function ConfigPage({ onClose, onSaveConnection, initialTab }: ConfigPage
<RefreshCw className="h-5 w-5 text-[var(--accent-cyan)]" />
</div>
<div>
<h3 className="text-sm font-semibold text-[var(--text-primary)]"></h3>
<p className="text-xs text-[var(--text-muted)]">使</p>
<h3 className="text-sm font-semibold text-[var(--text-primary)]">
{restartDialogMode === 'saved' ? '配置已保存' : '重启服务'}
</h3>
<p className="text-xs text-[var(--text-muted)]">
{restartDialogMode === 'saved'
? '是否立即重启服务使配置生效?'
: dirty
? '有未保存的更改,重启后将丢失。确定要重启吗?'
: '确定要重启服务吗?'}
</p>
</div>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => setShowRestartDialog(false)}
onClick={() => setRestartDialogMode(null)}
className="px-4 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors"
>
{restartDialogMode === 'saved' ? '稍后' : '取消'}
</button>
<button
onClick={handleRestart}