feat(migration): enhance migration flow with new stages and error handling

This commit introduces additional stages to the migration process, including 'backup_required', 'backup_progress', and 'backup_confirmed', improving user guidance during data migration. It also adds new IPC channels for proceeding to backup and retrying migration, along with enhanced error handling and logging throughout the migration flow. The user interface has been updated to reflect these changes, providing clearer feedback and options during the migration process.
This commit is contained in:
fullex 2025-08-10 01:40:03 +08:00
parent 8715eb1f41
commit c3f61533f7
4 changed files with 470 additions and 147 deletions

View File

@ -186,13 +186,15 @@ export enum IpcChannel {
// data migration
DataMigrate_CheckNeeded = 'data-migrate:check-needed',
DataMigrate_StartMigration = 'data-migrate:start-migration',
DataMigrate_GetProgress = 'data-migrate:get-progress',
DataMigrate_Cancel = 'data-migrate:cancel',
DataMigrate_RequireBackup = 'data-migrate:require-backup',
DataMigrate_BackupCompleted = 'data-migrate:backup-completed',
DataMigrate_ShowBackupDialog = 'data-migrate:show-backup-dialog',
DataMigrate_StartFlow = 'data-migrate:start-flow',
DataMigrate_ProceedToBackup = 'data-migrate:proceed-to-backup',
DataMigrate_StartMigration = 'data-migrate:start-migration',
DataMigrate_RetryMigration = 'data-migrate:retry-migration',
DataMigrate_RestartApp = 'data-migrate:restart-app',
DataMigrate_CloseWindow = 'data-migrate:close-window',

View File

@ -22,11 +22,21 @@ interface DataRefactorMigrationStatus {
version?: string
}
type MigrationStage =
| 'introduction' // Introduction phase - user can cancel
| 'backup_required' // Backup required - show backup requirement
| 'backup_progress' // Backup in progress - user is backing up
| 'backup_confirmed' // Backup confirmed - ready to migrate
| 'migration' // Migration in progress - cannot cancel
| 'completed' // Completed - restart app
| 'error' // Error - recovery options
interface MigrationProgress {
stage: string
stage: MigrationStage
progress: number
total: number
message: string
error?: string
}
interface MigrationResult {
@ -39,13 +49,12 @@ class DataRefactorMigrateService {
private static instance: DataRefactorMigrateService | null = null
private migrateWindow: BrowserWindow | null = null
private backupManager: BackupManager
private backupCompletionResolver: ((value: boolean) => void) | null = null
private db = dbService.getDb()
private currentProgress: MigrationProgress = {
stage: 'idle',
stage: 'introduction',
progress: 0,
total: 100,
message: 'Ready to migrate'
message: 'Ready to start data migration'
}
private isMigrating: boolean = false
@ -77,11 +86,32 @@ class DataRefactorMigrateService {
}
})
ipcMain.handle(IpcChannel.DataMigrate_ProceedToBackup, async () => {
try {
await this.proceedToBackup()
return true
} catch (error) {
logger.error('IPC handler error: proceedToBackup', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_StartMigration, async () => {
try {
return await this.runMigration()
await this.startMigrationProcess()
return true
} catch (error) {
logger.error('IPC handler error: runMigration', error as Error)
logger.error('IPC handler error: startMigrationProcess', error as Error)
throw error
}
})
ipcMain.handle(IpcChannel.DataMigrate_RetryMigration, async () => {
try {
await this.retryMigration()
return true
} catch (error) {
logger.error('IPC handler error: retryMigration', error as Error)
throw error
}
})
@ -104,9 +134,9 @@ class DataRefactorMigrateService {
}
})
ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, () => {
ipcMain.handle(IpcChannel.DataMigrate_BackupCompleted, async () => {
try {
this.notifyBackupCompleted()
await this.notifyBackupCompleted()
return true
} catch (error) {
logger.error('IPC handler error: notifyBackupCompleted', error as Error)
@ -114,14 +144,55 @@ class DataRefactorMigrateService {
}
})
ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, () => {
ipcMain.handle(IpcChannel.DataMigrate_ShowBackupDialog, async () => {
try {
// Show the backup dialog/interface
// This could integrate with existing backup UI or create a new backup interface
logger.info('Backup dialog request received')
return true
logger.info('Opening backup dialog for migration')
// Update progress to indicate backup dialog is opening
await this.updateProgress('backup_progress', 10, 'Opening backup dialog...')
// Instead of performing backup automatically, let's open the file dialog
// and let the user choose where to save the backup
const { dialog } = await import('electron')
const result = await dialog.showSaveDialog({
title: 'Save Migration Backup',
defaultPath: `cherry-studio-migration-backup-${new Date().toISOString().split('T')[0]}.zip`,
filters: [
{ name: 'Backup Files', extensions: ['zip'] },
{ name: 'All Files', extensions: ['*'] }
]
})
if (!result.canceled && result.filePath) {
logger.info('User selected backup location', { filePath: result.filePath })
await this.updateProgress('backup_progress', 50, 'Creating backup file...')
// Perform the actual backup to the selected location
const backupResult = await this.performBackupToFile(result.filePath)
if (backupResult.success) {
await this.updateProgress('backup_progress', 100, 'Backup created successfully!')
// Wait a moment to show the success message, then transition to confirmed state
setTimeout(async () => {
await this.updateProgress(
'backup_confirmed',
100,
'Backup completed! Ready to start migration. Click "Start Migration" to continue.'
)
}, 1000)
} else {
await this.updateProgress('backup_required', 0, `Backup failed: ${backupResult.error}`)
}
return backupResult
} else {
logger.info('User cancelled backup dialog')
await this.updateProgress('backup_required', 0, 'Backup cancelled. Please create a backup to continue.')
return { success: false, error: 'Backup cancelled by user' }
}
} catch (error) {
logger.error('IPC handler error: showBackupDialog', error as Error)
await this.updateProgress('backup_required', 0, 'Backup process failed')
throw error
}
})
@ -135,9 +206,9 @@ class DataRefactorMigrateService {
}
})
ipcMain.handle(IpcChannel.DataMigrate_RestartApp, () => {
ipcMain.handle(IpcChannel.DataMigrate_RestartApp, async () => {
try {
this.restartApplication()
await this.restartApplication()
return true
} catch (error) {
logger.error('IPC handler error: restartApplication', error as Error)
@ -167,12 +238,14 @@ class DataRefactorMigrateService {
try {
ipcMain.removeAllListeners(IpcChannel.DataMigrate_CheckNeeded)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_GetProgress)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_Cancel)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_BackupCompleted)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_ShowBackupDialog)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartFlow)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_ProceedToBackup)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_StartMigration)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_RetryMigration)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_RestartApp)
ipcMain.removeAllListeners(IpcChannel.DataMigrate_CloseWindow)
@ -200,6 +273,7 @@ class DataRefactorMigrateService {
return true
}
logger.info('Data Refactor Migration is needed')
return false
} catch (error) {
logger.error('Failed to check migration status', error as Error)
@ -212,18 +286,40 @@ class DataRefactorMigrateService {
*/
private async isMigrationCompleted(): Promise<boolean> {
try {
logger.debug('Checking migration completion status in database')
// First check if the database is available
if (!this.db) {
logger.warn('Database not initialized, assuming migration not completed')
return false
}
const result = await this.db
.select()
.from(appStateTable)
.where(eq(appStateTable.key, DATA_REFACTOR_MIGRATION_STATUS))
.limit(1)
if (result.length === 0) return false
logger.debug('Migration status query result', { resultCount: result.length })
if (result.length === 0) {
logger.info('No migration status record found, migration needed')
return false
}
const status = result[0].value as DataRefactorMigrationStatus
return status.completed === true
const isCompleted = status.completed === true
logger.info('Migration status found', {
completed: isCompleted,
completedAt: status.completedAt,
version: status.version
})
return isCompleted
} catch (error) {
logger.warn('Failed to check migration state', error as Error)
logger.error('Failed to check migration state - treating as not completed', error as Error)
// In case of database errors, assume migration is needed to be safe
return false
}
}
@ -320,7 +416,7 @@ class DataRefactorMigrateService {
}
/**
* Run the complete migration process
* Show migration window and initialize introduction stage
*/
async runMigration(): Promise<void> {
if (this.isMigrating) {
@ -332,6 +428,9 @@ class DataRefactorMigrateService {
this.isMigrating = true
logger.info('Showing migration window')
// Initialize introduction stage
await this.updateProgress('introduction', 0, 'Welcome to Cherry Studio data migration')
// Create migration window
const window = this.createMigrateWindow()
@ -345,12 +444,45 @@ class DataRefactorMigrateService {
})
}
/**
* Start migration flow - simply ensure we're in introduction stage
* This is called when user first opens the migration window
*/
async startMigrationFlow(): Promise<void> {
if (!this.isMigrating) {
logger.warn('Migration not started, cannot execute flow.')
return
}
logger.info('Starting migration flow from user action')
logger.info('Confirming introduction stage for migration flow')
await this.updateProgress('introduction', 0, 'Ready to begin migration process. Please read the information below.')
}
/**
* Proceed from introduction to backup requirement stage
* This is called when user clicks "Next" in introduction
*/
async proceedToBackup(): Promise<void> {
if (!this.isMigrating) {
logger.warn('Migration not started, cannot proceed to backup.')
return
}
logger.info('Proceeding from introduction to backup stage')
await this.updateProgress('backup_required', 0, 'Data backup is required before migration can proceed')
}
/**
* Start the actual migration process
* This is called when user confirms backup and clicks "Start Migration"
*/
async startMigrationProcess(): Promise<void> {
if (!this.isMigrating) {
logger.warn('Migration not started, cannot start migration process.')
return
}
logger.info('Starting actual migration process')
try {
await this.executeMigrationFlow()
} catch (error) {
@ -360,21 +492,12 @@ class DataRefactorMigrateService {
}
/**
* Execute the complete migration flow
* Execute the actual migration process
* Called after user has confirmed backup completion
*/
private async executeMigrationFlow(): Promise<void> {
try {
// Step 1: Enforce backup
await this.updateProgress('backup', 0, 'Starting backup process...')
const backupSuccess = await this.enforceBackup()
if (!backupSuccess) {
throw new Error('Backup process failed or was cancelled by user')
}
await this.updateProgress('backup', 100, 'Backup completed successfully')
// Step 2: Execute migration
// Start migration
await this.updateProgress('migration', 0, 'Starting data migration...')
const migrationResult = await this.executeMigration()
@ -388,16 +511,18 @@ class DataRefactorMigrateService {
`Migration completed: ${migrationResult.migratedCount} items migrated`
)
// Step 3: Mark as completed
// Mark as completed
await this.markMigrationCompleted()
await this.updateProgress('completed', 100, 'Migration completed! Please restart the app.')
await this.updateProgress('completed', 100, 'Migration completed successfully! Click restart to continue.')
} catch (error) {
logger.error('Migration flow failed', error as Error)
const errorMessage = error instanceof Error ? error.message : String(error)
await this.updateProgress(
'error',
0,
`Migration failed: ${error instanceof Error ? error.message : String(error)}. Please close this window and restart the app to try again.`
'Migration failed. You can close this window and try again, or continue using the previous version.',
errorMessage
)
throw error
@ -405,58 +530,113 @@ class DataRefactorMigrateService {
}
/**
* Enforce backup before migration
* Perform backup to a specific file location
*/
private async enforceBackup(): Promise<boolean> {
private async performBackupToFile(filePath: string): Promise<{ success: boolean; error?: string }> {
try {
logger.info('Enforcing backup before migration')
logger.info('Performing backup to file', { filePath })
await this.updateProgress('backup', 0, 'Backup is required before migration')
// Get backup data from the current application state
const backupData = await this.getBackupData()
// Send backup requirement to renderer
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
this.migrateWindow.webContents.send(IpcChannel.DataMigrate_RequireBackup)
}
// Extract directory and filename from the full path
const path = await import('path')
const destinationDir = path.dirname(filePath)
const fileName = path.basename(filePath)
// Wait for user to complete backup
const backupResult = await this.waitForBackupCompletion()
// Use the existing backup manager to create a backup
const backupPath = await this.backupManager.backup(
null as any, // IpcMainInvokeEvent - we're calling directly so pass null
fileName,
backupData,
destinationDir,
false // Don't skip backup files - full backup for migration safety
)
if (backupResult) {
await this.updateProgress('backup', 100, 'Backup completed successfully')
return true
if (backupPath) {
logger.info('Backup created successfully', { path: backupPath })
return { success: true }
} else {
await this.updateProgress('backup', 0, 'Backup is required to proceed with migration')
return false
return {
success: false,
error: 'Backup process did not return a file path'
}
}
} catch (error) {
logger.error('Backup enforcement failed', error as Error)
await this.updateProgress('backup', 0, 'Backup process failed')
return false
const errorMessage = error instanceof Error ? error.message : String(error)
logger.error('Backup failed during migration:', error as Error)
return {
success: false,
error: errorMessage
}
}
}
/**
* Wait for user to complete backup
* Get backup data from the current application
* This creates a minimal backup with essential system information
*/
private async waitForBackupCompletion(): Promise<boolean> {
return new Promise((resolve) => {
// Store resolver for later use
this.backupCompletionResolver = resolve
private async getBackupData(): Promise<string> {
try {
const fs = await import('fs-extra')
const path = await import('path')
// The actual completion will be triggered by notifyBackupCompleted() method
})
// Gather basic system information
const data = {
backup: {
timestamp: new Date().toISOString(),
version: electronApp.getVersion(),
type: 'pre-migration-backup',
note: 'This is a safety backup created before data migration'
},
system: {
platform: process.platform,
arch: process.arch,
nodeVersion: process.version
},
// Include basic configuration files if they exist
configs: {} as Record<string, any>
}
// Try to read some basic configuration files (non-critical if they fail)
try {
const { getDataPath } = await import('@main/utils')
const dataPath = getDataPath()
// Check if there are any config files we should backup
const configFiles = ['config.json', 'settings.json', 'preferences.json']
for (const configFile of configFiles) {
const configPath = path.join(dataPath, configFile)
if (await fs.pathExists(configPath)) {
try {
const configContent = await fs.readJson(configPath)
data.configs[configFile] = configContent
} catch (err) {
logger.warn(`Could not read config file ${configFile}`, err as Error)
}
}
}
} catch (err) {
logger.warn('Could not access data directory for config backup', err as Error)
}
return JSON.stringify(data, null, 2)
} catch (error) {
logger.error('Failed to get backup data:', error as Error)
throw error
}
}
/**
* Notify that backup has been completed (called from IPC handler)
*/
public notifyBackupCompleted(): void {
if (this.backupCompletionResolver) {
logger.info('Backup completed by user')
this.backupCompletionResolver(true)
this.backupCompletionResolver = null
}
public async notifyBackupCompleted(): Promise<void> {
logger.info('Backup completed by user')
await this.updateProgress(
'backup_confirmed',
100,
'Backup completed! Ready to start migration. Click "Start Migration" to continue.'
)
}
/**
@ -494,12 +674,18 @@ class DataRefactorMigrateService {
/**
* Update migration progress and broadcast to window
*/
private async updateProgress(stage: string, progress: number, message: string): Promise<void> {
private async updateProgress(
stage: MigrationStage,
progress: number,
message: string,
error?: string
): Promise<void> {
this.currentProgress = {
stage,
progress,
total: 100,
message
message,
error
}
if (this.migrateWindow && !this.migrateWindow.isDestroyed()) {
@ -518,18 +704,36 @@ class DataRefactorMigrateService {
/**
* Cancel migration process
* Only allowed during introduction and backup phases
*/
async cancelMigration(): Promise<void> {
if (!this.isMigrating) {
return
}
const currentStage = this.currentProgress.stage
if (currentStage === 'migration') {
logger.warn('Cannot cancel migration during migration process')
return
}
logger.info('Cancelling migration process')
this.isMigrating = false
await this.updateProgress('cancelled', 0, 'Migration cancelled by user')
this.closeMigrateWindow()
}
/**
* Retry migration after error
*/
async retryMigration(): Promise<void> {
logger.info('Retrying migration process')
await this.updateProgress(
'introduction',
0,
'Ready to restart migration process. Please read the information below.'
)
}
/**
* Close migration window
*/
@ -547,22 +751,73 @@ class DataRefactorMigrateService {
/**
* Restart the application after successful migration
*/
private restartApplication(): void {
private async restartApplication(): Promise<void> {
try {
logger.info('Restarting application after migration completion')
logger.info('Preparing to restart application after migration completion')
// Clean up migration window and handlers before restart
this.closeMigrateWindow()
// Ensure migration status is properly saved before restart
await this.verifyMigrationStatus()
// Restart the app using Electron's relaunch mechanism
app.relaunch()
app.exit(0)
// Give some time for database operations to complete
await new Promise((resolve) => setTimeout(resolve, 500))
logger.info('Restarting application now')
// In development mode, relaunch might not work properly
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
logger.warn('Development mode detected - showing restart instruction instead of auto-restart')
const { dialog } = await import('electron')
await dialog.showMessageBox({
type: 'info',
title: 'Migration Complete - Restart Required',
message:
'Data migration completed successfully!\n\nSince you are in development mode, please manually restart the application to continue.',
buttons: ['Close App'],
defaultId: 0
})
// Clean up migration window and handlers after showing dialog
this.closeMigrateWindow()
app.quit()
} else {
// Production mode - clean up first, then relaunch
this.closeMigrateWindow()
app.relaunch()
app.exit(0)
}
} catch (error) {
logger.error('Failed to restart application', error as Error)
// Fallback: just close migration window and let user manually restart
this.closeMigrateWindow()
}
}
/**
* Verify that migration status is properly saved
*/
private async verifyMigrationStatus(): Promise<void> {
try {
const isCompleted = await this.isMigrationCompleted()
if (isCompleted) {
logger.info('Migration status verified as completed')
} else {
logger.warn('Migration status not found as completed, attempting to mark again')
await this.markMigrationCompleted()
// Double-check
const recheck = await this.isMigrationCompleted()
if (recheck) {
logger.info('Migration status successfully marked as completed on retry')
} else {
logger.error('Failed to mark migration as completed even on retry')
}
}
} catch (error) {
logger.error('Failed to verify migration status', error as Error)
// Don't throw - still allow restart
}
}
}
// Export singleton instance

View File

@ -113,20 +113,43 @@ if (!app.requestSingleInstanceLock()) {
// Data Refactor Migration
// Check if data migration is needed BEFORE creating any windows
try {
logger.info('Checking if data refactor migration is needed')
const isMigrated = await dataRefactorMigrateService.isMigrated()
logger.info('Migration status check result', { isMigrated })
if (!isMigrated) {
logger.info('Data Refactor Migration needed, starting migration process')
await dataRefactorMigrateService.runMigration()
logger.info('Migration completed, app will restart automatically')
// Migration service will handle app restart, no need to continue startup
return
try {
await dataRefactorMigrateService.runMigration()
logger.info('Migration window created successfully')
// Migration service will handle the migration flow, no need to continue startup
return
} catch (migrationError) {
logger.error('Failed to start migration process', migrationError as Error)
// Migration is required for this version - show error and exit
await dialog.showErrorBox(
'Migration Required - Application Cannot Start',
`This version of Cherry Studio requires data migration to function properly.\n\nMigration window failed to start: ${(migrationError as Error).message}\n\nThe application will now exit. Please try starting again or contact support if the problem persists.`
)
logger.error('Exiting application due to failed migration startup')
app.quit()
return
}
}
} catch (error) {
logger.error('Migration process failed', error as Error)
dialog.showErrorBox(
'Fatal Error: Data Refactor Migration Failed',
`The application could not start due to a critical error during data migration.\n\nPlease contact support or try restoring data from a backup.\n\nError details:\n${(error as Error).message}`
logger.error('Migration status check failed', error as Error)
// If we can't check migration status, this could indicate a serious database issue
// Since migration may be required, it's safer to exit and let user investigate
await dialog.showErrorBox(
'Migration Status Check Failed - Application Cannot Start',
`Could not determine if data migration is completed.\n\nThis may indicate a database connectivity issue: ${(error as Error).message}\n\nThe application will now exit. Please check your installation and try again.`
)
logger.error('Exiting application due to migration status check failure')
app.quit()
return
}
@ -140,7 +163,7 @@ if (!app.requestSingleInstanceLock()) {
app.dock?.hide()
}
// Only create main window if no migration was needed or migration failed
// Create main window - migration has either completed or was not needed
const mainWindow = windowService.createMainWindow()
new TrayService()

View File

@ -6,21 +6,30 @@ import styled from 'styled-components'
const { Title, Text } = Typography
type MigrationStage =
| 'introduction' // Introduction phase - user can cancel
| 'backup_required' // Backup required - show backup requirement
| 'backup_progress' // Backup in progress - user is backing up
| 'backup_confirmed' // Backup confirmed - ready to migrate
| 'migration' // Migration in progress - cannot cancel
| 'completed' // Completed - restart app
| 'error' // Error - recovery options
interface MigrationProgress {
stage: string
stage: MigrationStage
progress: number
total: number
message: string
error?: string
}
const MigrateApp: React.FC = () => {
const [progress, setProgress] = useState<MigrationProgress>({
stage: 'idle',
stage: 'introduction',
progress: 0,
total: 100,
message: '准备开始迁移...'
message: 'Ready to start data migration'
})
const [showBackupRequired, setShowBackupRequired] = useState(false)
useEffect(() => {
// Listen for progress updates
@ -28,13 +37,7 @@ const MigrateApp: React.FC = () => {
setProgress(progressData)
}
// Listen for backup requirement
const handleBackupRequired = () => {
setShowBackupRequired(true)
}
window.electron.ipcRenderer.on(IpcChannel.DataMigrateProgress, handleProgress)
window.electron.ipcRenderer.on(IpcChannel.DataMigrate_RequireBackup, handleBackupRequired)
// Request initial progress
window.electron.ipcRenderer
@ -47,37 +50,41 @@ const MigrateApp: React.FC = () => {
return () => {
window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrateProgress)
window.electron.ipcRenderer.removeAllListeners(IpcChannel.DataMigrate_RequireBackup)
}
}, [])
const currentStep = useMemo(() => {
switch (progress.stage) {
case 'idle':
case 'introduction':
return 0
case 'backup':
case 'backup_required':
case 'backup_progress':
case 'backup_confirmed':
return 1
case 'migration':
return 2
case 'completed':
return 4
case 'error':
case 'cancelled':
return 3
case 'error':
return -1 // Error state - will be handled separately
default:
return 0
}
}, [progress.stage])
const stepStatus = useMemo(() => {
if (progress.stage === 'error' || progress.stage === 'cancelled') {
if (progress.stage === 'error') {
return 'error'
}
return 'process'
}, [progress.stage])
const handleProceedToBackup = () => {
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_ProceedToBackup)
}
const handleStartMigration = () => {
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartFlow)
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_StartMigration)
}
const handleRestartApp = () => {
@ -98,18 +105,22 @@ const MigrateApp: React.FC = () => {
}
const handleBackupCompleted = () => {
setShowBackupRequired(false)
// Notify the main process that backup is completed
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_BackupCompleted)
}
const handleRetryMigration = () => {
window.electron.ipcRenderer.invoke(IpcChannel.DataMigrate_RetryMigration)
}
const getProgressColor = () => {
switch (progress.stage) {
case 'completed':
return '#52c41a'
case 'error':
case 'cancelled':
return '#ff4d4f'
case 'backup_confirmed':
return '#52c41a'
default:
return '#1890ff'
}
@ -117,12 +128,36 @@ const MigrateApp: React.FC = () => {
const renderActionButtons = () => {
switch (progress.stage) {
case 'idle':
case 'introduction':
return (
<Button type="primary" onClick={handleStartMigration}>
</Button>
<Space>
<Button onClick={handleCancel}></Button>
<Button type="primary" onClick={handleProceedToBackup}>
</Button>
</Space>
)
case 'backup_required':
return (
<Space>
<Button onClick={handleCancel}></Button>
<Button type="primary" onClick={handleShowBackupDialog}>
</Button>
<Button onClick={handleBackupCompleted}></Button>
</Space>
)
case 'backup_confirmed':
return (
<Space>
<Button onClick={handleCancel}></Button>
<Button type="primary" onClick={handleStartMigration}>
</Button>
</Space>
)
case 'migration':
return <Button disabled>...</Button>
case 'completed':
return (
<Button type="primary" onClick={handleRestartApp}>
@ -130,19 +165,14 @@ const MigrateApp: React.FC = () => {
</Button>
)
case 'error':
case 'cancelled':
return (
<Space>
<Button onClick={handleCloseWindow}></Button>
<Button onClick={handleCloseWindow}></Button>
<Button type="primary" onClick={handleRetryMigration}>
</Button>
</Space>
)
case 'backup':
case 'migration':
return (
<Button onClick={handleCancel} disabled={progress.stage === 'backup'}>
</Button>
)
default:
return null
}
@ -167,11 +197,11 @@ const MigrateApp: React.FC = () => {
<Steps
current={currentStep}
status={stepStatus}
items={[{ title: '开始' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
items={[{ title: '介绍' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
/>
</div>
{progress.stage !== 'idle' && (
{progress.stage !== 'introduction' && progress.stage !== 'error' && (
<ProgressContainer>
<Progress
percent={progress.progress}
@ -187,10 +217,43 @@ const MigrateApp: React.FC = () => {
<Text type="secondary">{progress.message}</Text>
</MessageContainer>
{progress.stage === 'introduction' && (
<Alert
message="欢迎使用Cherry Studio数据迁移向导"
description="本次更新将您的数据迁移到更高效的存储格式。迁移前会创建完整备份,确保数据安全。整个过程大约需要几分钟时间。"
type="info"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'backup_required' && (
<Alert
message="需要数据备份"
description="为确保数据安全,迁移前必须创建数据备份。请点击'创建备份'按钮,或者如果您已经有最新备份,可以直接确认。"
type="warning"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'backup_confirmed' && (
<Alert
message="备份完成"
description="数据备份已完成,现在可以安全地开始迁移。点击'开始迁移'继续。"
type="success"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'error' && (
<Alert
message="Migration Error"
description="The migration process encountered an error. You can try again or restore from a backup using an older version."
message="迁移出现错误"
description={
progress.error ||
'迁移过程中遇到错误。您可以关闭窗口重新尝试,或继续使用之前版本(所有原始数据都完好保存)。'
}
type="error"
showIcon
style={{ marginTop: 16 }}
@ -199,34 +262,14 @@ const MigrateApp: React.FC = () => {
{progress.stage === 'completed' && (
<Alert
message="Migration Successful"
description="Your data has been successfully migrated to the new format. Cherry Studio will now start with your updated data."
message="迁移成功完成"
description="数据已成功迁移到新格式。Cherry Studio将使用更新后的数据重新启动享受更流畅的使用体验。"
type="success"
showIcon
style={{ marginTop: 16 }}
/>
)}
{showBackupRequired && (
<Alert
message="Backup Required"
description="A backup is required before migration can proceed. Please create a backup of your data to ensure safety."
type="warning"
showIcon
style={{ marginTop: 16 }}
action={
<Space>
<Button size="small" onClick={handleShowBackupDialog}>
Create Backup
</Button>
<Button size="small" type="link" onClick={handleBackupCompleted}>
I've Already Created a Backup
</Button>
</Space>
}
/>
)}
<div style={{ textAlign: 'center', marginTop: 24 }}>{renderActionButtons()}</div>
</MigrationCard>
</Container>