备份与迁移导入改用应用内弹窗,并补充导入导出更新日志与关于页
- 导入的覆盖确认与成功提示改为应用内弹窗(替代原生系统对话框),导入完成后自动重启 - 导出/导入失败提示改为卡片内内联横幅,与导出成功/进度样式统一 - CHANGELOG 将「备份与迁移」列为新增功能并说明导入导出;关于页发布亮点同步补充
This commit is contained in:
1 parent
d9ae553d3c
commit
73c8fa2a08
4 files changed
+106
-38
No files matched your search
+3
-1
@@ -11,8 +11,10 @@
|
||||
|
||||
## [0.1.2] - 2026-07-24
|
||||
|
||||
### 新增
|
||||
- 新增「备份与迁移」,支持导出与导入两种备份:「配置」(仅设置、密钥、热词、提示词,文件小、便于日常备份)和「全部数据」(在配置基础上额外包含历史记录与录音,适合换机完整迁移)。导出直接保存到默认下载目录并就地显示文件路径与「打开所在文件夹」;导入采用应用内确认弹窗(会覆盖当前数据),完成后自动重启使更改生效。
|
||||
|
||||
### 优化
|
||||
- 优化「备份与迁移」:导出不再弹出保存位置对话框,「配置」与「全部数据」都直接保存到默认下载目录,导出完成后就地显示文件路径与「打开所在文件夹」按钮,和其他下载体验保持一致。同时优化了页面文案,更清楚地说明两种备份分别包含什么、适合什么场景(「全部数据」在「配置」基础上额外包含历史记录与录音)。
|
||||
- 优化诊断页面「AI 校对」的状态显示:未开启时不再显示为绿色对勾(易被误解为已启用),改为中性的「未开启」状态,并温和提示依赖 AI 的功能当前不会生效、以及在哪里开启。
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -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 秒的问题:机器信息改为启动时采集一次,日常改设置不再卡顿',
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
Reference in new issue
Block a user