Files
opsxshell/src/components/common/ConfirmDialog.tsx
T
Logan.Li b0d0e3a5f3 feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection
- 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
2026-08-12 00:07:48 +08:00

203 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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