备份与迁移导入改用应用内弹窗,并补充导入导出更新日志与关于页

- 导入的覆盖确认与成功提示改为应用内弹窗(替代原生系统对话框),导入完成后自动重启

- 导出/导入失败提示改为卡片内内联横幅,与导出成功/进度样式统一

- CHANGELOG 将「备份与迁移」列为新增功能并说明导入导出;关于页发布亮点同步补充
This commit is contained in:
crosswk committed 2026-07-24 15:07:27 +08:00
1 parent d9ae553d3c
commit 73c8fa2a08
4 files changed
+106 -38

No files matched your search

+3 -1
View File
@@ -11,8 +11,10 @@
## [0.1.2] - 2026-07-24
### 新增
- 新增「备份与迁移」,支持导出与导入两种备份:「配置」(仅设置、密钥、热词、提示词,文件小、便于日常备份)和「全部数据」(在配置基础上额外包含历史记录与录音,适合换机完整迁移)。导出直接保存到默认下载目录并就地显示文件路径与「打开所在文件夹」;导入采用应用内确认弹窗(会覆盖当前数据),完成后自动重启使更改生效。
### 优化
- 优化「备份与迁移」:导出不再弹出保存位置对话框,「配置」与「全部数据」都直接保存到默认下载目录,导出完成后就地显示文件路径与「打开所在文件夹」按钮,和其他下载体验保持一致。同时优化了页面文案,更清楚地说明两种备份分别包含什么、适合什么场景(「全部数据」在「配置」基础上额外包含历史记录与录音)。
- 优化诊断页面「AI 校对」的状态显示:未开启时不再显示为绿色对勾(易被误解为已启用),改为中性的「未开启」状态,并温和提示依赖 AI 的功能当前不会生效、以及在哪里开启。
### 修复
+89 -10
View File
@@ -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<ImportKind, string> = {
config: '设置、供应商配置与密钥、热词、Prompt',
full: '全部数据(设置、历史记录与录音)',
}
type BusyAction = 'exportConfig' | 'exportFull' | 'importConfig' | 'importFull' | null
const phaseLabels: Record<BackupExportProgress['phase'], string> = {
@@ -105,6 +112,12 @@ export default function BackupSection() {
const [configPath, setConfigPath] = useState('')
const [fullPath, setFullPath] = useState('')
const [fullProgress, setFullProgress] = useState<BackupExportProgress | null>(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 (
<Card>
<CardContent className="p-6">
@@ -220,7 +255,51 @@ export default function BackupSection() {
</div>
)}
</div>
{actionError && (
<div className="mt-4 rounded-md bg-destructive/10 px-3 py-2 text-xs text-destructive">
{actionError}
</div>
)}
</CardContent>
{/* 导入确认(应用内弹窗,样式与其余确认框一致) */}
{importConfirm && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={() => setImportConfirm(null)}
>
<div className="w-96 rounded-xl border bg-card p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 shrink-0 text-amber-500" />
<h3 className="text-base font-semibold">确认导入</h3>
</div>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
导入将用备份覆盖当前的{IMPORT_OVERWRITES[importConfirm.kind]},此操作无法撤销。导入完成后应用会自动重启。确定继续吗?
</p>
<div className="mt-5 flex justify-end gap-2">
<Button size="sm" variant="outline" onClick={() => setImportConfirm(null)}>取消</Button>
<Button size="sm" variant="destructive" onClick={() => void confirmImport()}>覆盖导入</Button>
</div>
</div>
</div>
)}
{/* 导入成功提示,稍后自动重启 */}
{importDone && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="w-80 rounded-xl border bg-card p-6 shadow-xl">
<div className="flex items-center gap-2">
<Check className="h-4 w-4 shrink-0 text-success" />
<h3 className="text-base font-semibold">导入成功</h3>
</div>
<p className="mt-2 flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
应用即将重启以使更改生效…
</p>
</div>
</div>
)}
</Card>
)
}
@@ -12,6 +12,7 @@ export interface ReleaseHighlights {
export const RELEASE_HIGHLIGHTS: ReleaseHighlights = {
version: '0.1.2',
items: [
'新增「备份与迁移」:可导出 / 导入「配置」或「全部数据」,方便换机、重装与定期备份',
'修复长时间后台运行后偶发「一直处理中、超时无结果」的问题:后台连接更稳定,不再被误判空闲而断开',
'识别超时不再丢录音:即使这次超时没结果,录音也会保存到历史,可随时「重新识别」',
'修复保存润色模式等设置后卡顿 2-4 秒的问题:机器信息改为启动时采集一次,日常改设置不再卡顿',
+13 -27
View File
@@ -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<ExportResult> {
return { canceled: false, filePath: path }
}
/** 导入结果:done = 已导入(即将重启);canceled = 用户取消。 */
export type ImportOutcome = 'done' | 'canceled'
async function pickConfirmImport(kind: 'config' | 'full'): Promise<ImportOutcome> {
/** 让用户选择要导入的备份文件;返回路径,取消则返回 null。文件选择走系统原生对话框。 */
export async function pickImportFile(kind: 'config' | 'full'): Promise<string | null> {
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<void> {
await invoke(kind === 'config' ? 'import_config' : 'import_full', { inPath })
}
/** 重启应用(Tauri 内置 app.restart())。 */
export function restartApp(): Promise<void> {
return invoke('restart_app')
}