- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru - Auto-detect system language via navigator.languages; fallback to English - Add BCP47 locale mapping for date formatting and sorting - Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each) - Update language selector with native language names - Extend locale key parity test to cover all 7 languages (70 assertions) - Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features - Add credential encryption, host metrics, session backup crypto, and zmodem bridge - Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
203 lines
6.0 KiB
TypeScript
203 lines
6.0 KiB
TypeScript
/**
|
||
* ConfirmDialog — 基于 Promise 的确认对话框
|
||
*
|
||
* 用于替代 window.confirm(),支持异步等待结果
|
||
*
|
||
* @example
|
||
* const { confirm, ConfirmDialogComponent } = useConfirmDialog()
|
||
* const confirmed = await confirm({ title: '确认删除', message: '确定删除?', danger: true })
|
||
*
|
||
* // 在 JSX 中渲染:
|
||
* <ConfirmDialogComponent />
|
||
*/
|
||
|
||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||
import { useTranslation } from 'react-i18next'
|
||
import Modal from './Modal'
|
||
import Button from './Button'
|
||
import { AlertTriangle } from 'lucide-react'
|
||
import i18n from '../../i18n'
|
||
|
||
export interface ConfirmOptions {
|
||
/** 对话框标题 */
|
||
title: string
|
||
/** 对话框内容 */
|
||
message: string
|
||
/** 是否为危险操作(红色确认按钮) */
|
||
danger?: boolean
|
||
/** 确认按钮文本 */
|
||
confirmText?: string
|
||
/** 取消按钮文本 */
|
||
cancelText?: string
|
||
}
|
||
|
||
/** Agent 高风险确认:Reject / Once / Always(Always 仅会话内存) */
|
||
export type AgentRiskChoice = 'reject' | 'once' | 'always'
|
||
|
||
export interface AgentRiskConfirmOptions {
|
||
title: string
|
||
message: string
|
||
/** 高风险时 Once/Always 用危险样式 */
|
||
danger?: boolean
|
||
}
|
||
|
||
interface ConfirmState extends ConfirmOptions {
|
||
isOpen: boolean
|
||
}
|
||
|
||
interface AgentRiskState {
|
||
isOpen: boolean
|
||
title: string
|
||
message: string
|
||
danger: boolean
|
||
}
|
||
|
||
const initialState: ConfirmState = {
|
||
isOpen: false,
|
||
title: '',
|
||
message: '',
|
||
danger: false,
|
||
confirmText: i18n.t('common:action.confirm'),
|
||
cancelText: i18n.t('common:action.cancel'),
|
||
}
|
||
|
||
const initialAgentRisk: AgentRiskState = {
|
||
isOpen: false,
|
||
title: '',
|
||
message: '',
|
||
danger: true,
|
||
}
|
||
|
||
/**
|
||
* ConfirmDialog 组件 + Hook
|
||
*
|
||
* 返回 confirm 函数和需要渲染的组件
|
||
*/
|
||
export function useConfirmDialog() {
|
||
const { t } = useTranslation(['common', 'ai'])
|
||
const [state, setState] = useState<ConfirmState>(initialState)
|
||
const [agentRisk, setAgentRisk] = useState<AgentRiskState>(initialAgentRisk)
|
||
const resolverRef = useRef<((value: boolean) => void) | null>(null)
|
||
const agentResolverRef = useRef<((value: AgentRiskChoice) => void) | null>(null)
|
||
|
||
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||
return new Promise<boolean>((resolve) => {
|
||
resolverRef.current = resolve
|
||
setState({
|
||
...options,
|
||
isOpen: true,
|
||
danger: options.danger ?? false,
|
||
confirmText: options.confirmText ?? i18n.t('common:action.confirm'),
|
||
cancelText: options.cancelText ?? i18n.t('common:action.cancel'),
|
||
})
|
||
})
|
||
}, [])
|
||
|
||
const confirmAgentRisk = useCallback((options: AgentRiskConfirmOptions): Promise<AgentRiskChoice> => {
|
||
return new Promise<AgentRiskChoice>((resolve) => {
|
||
agentResolverRef.current = resolve
|
||
setAgentRisk({
|
||
isOpen: true,
|
||
title: options.title,
|
||
message: options.message,
|
||
danger: options.danger ?? true,
|
||
})
|
||
})
|
||
}, [])
|
||
|
||
const handleClose = useCallback(() => {
|
||
setState((prev) => ({ ...prev, isOpen: false }))
|
||
resolverRef.current?.(false)
|
||
resolverRef.current = null
|
||
}, [])
|
||
|
||
const handleConfirm = useCallback(() => {
|
||
setState((prev) => ({ ...prev, isOpen: false }))
|
||
resolverRef.current?.(true)
|
||
resolverRef.current = null
|
||
}, [])
|
||
|
||
const resolveAgentRisk = useCallback((choice: AgentRiskChoice) => {
|
||
setAgentRisk((prev) => ({ ...prev, isOpen: false }))
|
||
agentResolverRef.current?.(choice)
|
||
agentResolverRef.current = null
|
||
}, [])
|
||
|
||
/** 停止 Agent 时关闭待确认对话框(等同 Reject,不计入用户决策) */
|
||
const dismissAgentRisk = useCallback(() => {
|
||
if (!agentResolverRef.current) return
|
||
resolveAgentRisk('reject')
|
||
}, [resolveAgentRisk])
|
||
|
||
useEffect(() => {
|
||
if (!agentRisk.isOpen) return
|
||
const onKey = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') {
|
||
event.preventDefault()
|
||
resolveAgentRisk('reject')
|
||
} else if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
||
event.preventDefault()
|
||
resolveAgentRisk('once')
|
||
}
|
||
}
|
||
window.addEventListener('keydown', onKey)
|
||
return () => window.removeEventListener('keydown', onKey)
|
||
}, [agentRisk.isOpen, resolveAgentRisk])
|
||
|
||
const ConfirmDialogComponent = (
|
||
<>
|
||
<Modal
|
||
isOpen={state.isOpen}
|
||
title={state.title}
|
||
onConfirm={handleConfirm}
|
||
onClose={handleClose}
|
||
confirmText={state.confirmText}
|
||
cancelText={state.cancelText}
|
||
isDanger={state.danger}
|
||
size="sm"
|
||
icon={
|
||
state.danger ? (
|
||
<AlertTriangle className="w-5 h-5 text-red-400" />
|
||
) : undefined
|
||
}
|
||
>
|
||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-content-secondary">{state.message}</p>
|
||
</Modal>
|
||
|
||
<Modal
|
||
isOpen={agentRisk.isOpen}
|
||
title={agentRisk.title}
|
||
onClose={() => resolveAgentRisk('reject')}
|
||
showConfirm={false}
|
||
size="sm"
|
||
disableBackdropClose
|
||
icon={<AlertTriangle className="w-5 h-5 text-red-400" />}
|
||
footer={
|
||
<div className="flex w-full flex-wrap items-center justify-end gap-2">
|
||
<Button variant="ghost" size="sm" onClick={() => resolveAgentRisk('reject')}>
|
||
{t('ai:confirm.reject')}
|
||
</Button>
|
||
<Button
|
||
variant={agentRisk.danger ? 'danger' : 'primary'}
|
||
size="sm"
|
||
onClick={() => resolveAgentRisk('once')}
|
||
>
|
||
{t('ai:confirm.once')}
|
||
</Button>
|
||
<Button variant="secondary" size="sm" onClick={() => resolveAgentRisk('always')}>
|
||
{t('ai:confirm.always')}
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<p className="whitespace-pre-wrap text-sm leading-relaxed text-content-secondary">{agentRisk.message}</p>
|
||
<p className="mt-3 text-[11px] text-txt-faint">{t('ai:confirm.hint')}</p>
|
||
</Modal>
|
||
</>
|
||
)
|
||
|
||
return { confirm, confirmAgentRisk, dismissAgentRisk, ConfirmDialogComponent }
|
||
}
|
||
|
||
export default useConfirmDialog
|