From 73c8fa2a0855f25ee56c049d28472c63e1fe40ac Mon Sep 17 00:00:00 2001 From: crosswk Date: Fri, 24 Jul 2026 15:07:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=87=E4=BB=BD=E4=B8=8E=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E6=94=B9=E7=94=A8=E5=BA=94=E7=94=A8=E5=86=85?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=EF=BC=8C=E5=B9=B6=E8=A1=A5=E5=85=85=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E5=AF=BC=E5=87=BA=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E4=B8=8E=E5=85=B3=E4=BA=8E=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 导入的覆盖确认与成功提示改为应用内弹窗(替代原生系统对话框),导入完成后自动重启 - 导出/导入失败提示改为卡片内内联横幅,与导出成功/进度样式统一 - CHANGELOG 将「备份与迁移」列为新增功能并说明导入导出;关于页发布亮点同步补充 --- CHANGELOG.md | 4 +- .../src/features/settings/BackupSection.tsx | 99 +++++++++++++++++-- .../src/features/update/releaseHighlights.ts | 1 + client/src/services/backup.ts | 40 +++----- 4 files changed, 106 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19b196..3a56842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ ## [0.1.2] - 2026-07-24 +### 新增 +- 新增「备份与迁移」,支持导出与导入两种备份:「配置」(仅设置、密钥、热词、提示词,文件小、便于日常备份)和「全部数据」(在配置基础上额外包含历史记录与录音,适合换机完整迁移)。导出直接保存到默认下载目录并就地显示文件路径与「打开所在文件夹」;导入采用应用内确认弹窗(会覆盖当前数据),完成后自动重启使更改生效。 + ### 优化 -- 优化「备份与迁移」:导出不再弹出保存位置对话框,「配置」与「全部数据」都直接保存到默认下载目录,导出完成后就地显示文件路径与「打开所在文件夹」按钮,和其他下载体验保持一致。同时优化了页面文案,更清楚地说明两种备份分别包含什么、适合什么场景(「全部数据」在「配置」基础上额外包含历史记录与录音)。 - 优化诊断页面「AI 校对」的状态显示:未开启时不再显示为绿色对勾(易被误解为已启用),改为中性的「未开启」状态,并温和提示依赖 AI 的功能当前不会生效、以及在哪里开启。 ### 修复 diff --git a/client/src/features/settings/BackupSection.tsx b/client/src/features/settings/BackupSection.tsx index 7d91d8b..4f589ae 100644 --- a/client/src/features/settings/BackupSection.tsx +++ b/client/src/features/settings/BackupSection.tsx @@ -1,19 +1,26 @@ import { useEffect, useMemo, useState } from 'react' import { invoke } from '@tauri-apps/api/core' -import { message } from '@tauri-apps/plugin-dialog' -import { Check, Download, FolderOpen, Loader2, Upload } from 'lucide-react' +import { AlertTriangle, Check, Download, FolderOpen, Loader2, Upload } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent } from '@/components/ui/card' import { Tooltip } from '@/components/ui/tooltip' import { exportConfigFile, exportFullFile, - importConfigFile, - importFullFile, + pickImportFile, + runImport, + restartApp, onBackupExportProgress, type BackupExportProgress, } from '@/services/backup' +type ImportKind = 'config' | 'full' + +const IMPORT_OVERWRITES: Record = { + config: '设置、供应商配置与密钥、热词、Prompt', + full: '全部数据(设置、历史记录与录音)', +} + type BusyAction = 'exportConfig' | 'exportFull' | 'importConfig' | 'importFull' | null const phaseLabels: Record = { @@ -105,6 +112,12 @@ export default function BackupSection() { const [configPath, setConfigPath] = useState('') const [fullPath, setFullPath] = useState('') const [fullProgress, setFullProgress] = useState(null) + // 导入确认(应用内弹窗,替代原生系统对话框) + const [importConfirm, setImportConfirm] = useState<{ kind: ImportKind; path: string } | null>(null) + // 导入成功后展示提示并自动重启 + const [importDone, setImportDone] = useState(false) + // 导出/导入的错误提示,改为应用内内联横幅 + const [actionError, setActionError] = useState('') useEffect(() => { const unlisten = onBackupExportProgress((progress) => { @@ -127,11 +140,12 @@ export default function BackupSection() { const handleExportConfig = async () => { setBusyAction('exportConfig') + setActionError('') try { const result = await exportConfigFile() if (result.filePath) setConfigPath(result.filePath) } catch (error) { - await message(`导出失败:${String(error)}`, { title: '导出失败', kind: 'error' }) + setActionError(`导出失败:${String(error)}`) } finally { setBusyAction(null) } @@ -139,29 +153,50 @@ export default function BackupSection() { const handleExportFull = async () => { setBusyAction('exportFull') + setActionError('') setFullProgress(null) try { const result = await exportFullFile() if (result.filePath) setFullPath(result.filePath) } catch (error) { - await message(`导出失败:${String(error)}`, { title: '导出失败', kind: 'error' }) + setActionError(`导出失败:${String(error)}`) } finally { setBusyAction(null) } } - const handleImport = async (kind: 'config' | 'full') => { + // 第一步:选文件(系统原生对话框),选好后弹出应用内确认弹窗 + const handleImport = async (kind: ImportKind) => { setBusyAction(kind === 'config' ? 'importConfig' : 'importFull') + setActionError('') try { - if (kind === 'config') await importConfigFile() - else await importFullFile() + const path = await pickImportFile(kind) + if (path) setImportConfirm({ kind, path }) } catch (error) { - await message(`导入失败:${String(error)}`, { title: '导入失败', kind: 'error' }) + setActionError(`导入失败:${String(error)}`) } finally { setBusyAction(null) } } + // 第二步:确认覆盖后执行导入,成功则展示提示并自动重启 + const confirmImport = async () => { + if (!importConfirm) return + const { kind, path } = importConfirm + setImportConfirm(null) + setBusyAction(kind === 'config' ? 'importConfig' : 'importFull') + setActionError('') + try { + await runImport(kind, path) + setImportDone(true) + // 略作停留让用户看到提示,再自动重启使更改生效 + setTimeout(() => { void restartApp() }, 1500) + } catch (error) { + setActionError(`导入失败:${String(error)}`) + setBusyAction(null) + } + } + return ( @@ -220,7 +255,51 @@ export default function BackupSection() { )} + + {actionError && ( +
+ {actionError} +
+ )}
+ + {/* 导入确认(应用内弹窗,样式与其余确认框一致) */} + {importConfirm && ( +
setImportConfirm(null)} + > +
e.stopPropagation()}> +
+ +

确认导入

+
+

+ 导入将用备份覆盖当前的{IMPORT_OVERWRITES[importConfirm.kind]},此操作无法撤销。导入完成后应用会自动重启。确定继续吗? +

+
+ + +
+
+
+ )} + + {/* 导入成功提示,稍后自动重启 */} + {importDone && ( +
+
+
+ +

导入成功

+
+

+ + 应用即将重启以使更改生效… +

+
+
+ )}
) } diff --git a/client/src/features/update/releaseHighlights.ts b/client/src/features/update/releaseHighlights.ts index b1a219b..e6ef30e 100644 --- a/client/src/features/update/releaseHighlights.ts +++ b/client/src/features/update/releaseHighlights.ts @@ -12,6 +12,7 @@ export interface ReleaseHighlights { export const RELEASE_HIGHLIGHTS: ReleaseHighlights = { version: '0.1.2', items: [ + '新增「备份与迁移」:可导出 / 导入「配置」或「全部数据」,方便换机、重装与定期备份', '修复长时间后台运行后偶发「一直处理中、超时无结果」的问题:后台连接更稳定,不再被误判空闲而断开', '识别超时不再丢录音:即使这次超时没结果,录音也会保存到历史,可随时「重新识别」', '修复保存润色模式等设置后卡顿 2-4 秒的问题:机器信息改为启动时采集一次,日常改设置不再卡顿', diff --git a/client/src/services/backup.ts b/client/src/services/backup.ts index a12a7d2..66ea390 100644 --- a/client/src/services/backup.ts +++ b/client/src/services/backup.ts @@ -7,7 +7,7 @@ import { invoke } from '@tauri-apps/api/core' import { listen, type UnlistenFn } from '@tauri-apps/api/event' -import { open, confirm, message } from '@tauri-apps/plugin-dialog' +import { open } from '@tauri-apps/plugin-dialog' export interface ExportResult { canceled: boolean @@ -49,36 +49,22 @@ export async function exportFullFile(): Promise { return { canceled: false, filePath: path } } -/** 导入结果:done = 已导入(即将重启);canceled = 用户取消。 */ -export type ImportOutcome = 'done' | 'canceled' - -async function pickConfirmImport(kind: 'config' | 'full'): Promise { +/** 让用户选择要导入的备份文件;返回路径,取消则返回 null。文件选择走系统原生对话框。 */ +export async function pickImportFile(kind: 'config' | 'full'): Promise { const filters = kind === 'config' ? [{ name: 'SayIt 配置', extensions: ['json'] }] : [{ name: 'SayIt 备份', extensions: ['zip'] }] - const picked = await open({ multiple: false, directory: false, filters }) - const inPath = typeof picked === 'string' ? picked : null - if (!inPath) return 'canceled' - - const what = - kind === 'config' - ? '设置、供应商配置与密钥、热词、Prompt' - : '全部数据(设置、历史记录与录音)' - const ok = await confirm(`导入将用备份覆盖当前的${what},此操作无法撤销。确定继续吗?`, { - title: '确认导入', - kind: 'warning', - okLabel: '覆盖导入', - cancelLabel: '取消', - }) - if (!ok) return 'canceled' - - await invoke(kind === 'config' ? 'import_config' : 'import_full', { inPath }) - await message('导入成功,应用将重启以使更改生效。', { title: '导入完成' }) - await invoke('restart_app') - return 'done' + return typeof picked === 'string' ? picked : null } -export const importConfigFile = () => pickConfirmImport('config') -export const importFullFile = () => pickConfirmImport('full') +/** 执行导入(会用备份覆盖当前数据)。确认与提示由调用方在应用内 UI 完成。 */ +export async function runImport(kind: 'config' | 'full', inPath: string): Promise { + await invoke(kind === 'config' ? 'import_config' : 'import_full', { inPath }) +} + +/** 重启应用(Tauri 内置 app.restart())。 */ +export function restartApp(): Promise { + return invoke('restart_app') +}