import { useEffect, useState } from 'react' // 桌面端启动诊断横幅。监听 Tauri 侧 emit 的 backend-warning / backend-error / backend-terminated。 // 只在 Tauri 环境生效;纯 web 环境(无 window.__TAURI_INTERNALS__)下静默不挂载。 type Severity = 'info' | 'warning' | 'error' interface DiagnosticPayload { exe_path?: string path_has_non_ascii?: boolean path_has_space?: boolean parent_writable?: boolean platform?: string } interface BannerState { severity: Severity title: string detail: string payload?: DiagnosticPayload dismissible: boolean } const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window function describeWarning(payload: DiagnosticPayload): { title: string; detail: string } { const parts: string[] = [] if (payload.path_has_non_ascii) { parts.push('安装路径包含非 ASCII 字符(中文 / 日文等)') } if (payload.path_has_space) { parts.push('安装路径包含空格') } if (payload.parent_writable === false) { parts.push('安装目录不可写(缺少权限或只读)') } return { title: '检测到可能导致后端启动失败的安装路径', detail: `${parts.join(';')}。\n` + '建议把 BiliNote 重新安装到一个纯英文、无空格、可写的路径下(如 C:\\BiliNote\\ 或 /Applications/)。\n' + `当前路径:${payload.exe_path || '未知'}`, } } const StartupBanner = () => { const [banner, setBanner] = useState(null) useEffect(() => { if (!isTauri) return let unlisteners: Array<() => void> = [] ;(async () => { const { listen } = await import('@tauri-apps/api/event') const offWarning = await listen('backend-warning', event => { const { title, detail } = describeWarning(event.payload || {}) setBanner({ severity: 'warning', title, detail, payload: event.payload, dismissible: true, }) }) const offTerminated = await listen('backend-terminated', event => { setBanner({ severity: 'error', title: '后端进程已退出', detail: `退出码:${event.payload ?? '未知'}。打开「部署监控」或重启应用以恢复。`, dismissible: false, }) }) // 后端被「重启后端」按钮拉起来后 / Rust ready-probe 检测到新 sidecar 真的就绪后, // 自动清掉 terminated 横幅。之前 dismissible:false + 没自动清逻辑 = banner 永远卡。 const offRestarted = await listen('backend-restarted', () => { setBanner(b => (b?.severity === 'error' ? null : b)) }) const offReady = await listen('backend-ready', () => { setBanner(b => (b?.severity === 'error' ? null : b)) }) // backend-error 是 sidecar stderr,量大噪音多,这里不直接展示,留给 P2 的日志面板。 unlisteners = [offWarning, offTerminated, offRestarted, offReady] })() return () => { unlisteners.forEach(fn => fn()) } }, []) if (!banner) return null const colorByLevel: Record = { info: 'bg-blue-50 border-blue-300 text-blue-900', warning: 'bg-amber-50 border-amber-300 text-amber-900', error: 'bg-red-50 border-red-300 text-red-900', } const iconByLevel: Record = { info: 'ℹ️', warning: '⚠️', error: '✕', } return (
{iconByLevel[banner.severity]}
{banner.title}
          {banner.detail}
        
{banner.dismissible && ( )}
) } export default StartupBanner