feat(migration): enhance migration UI and refactor layout

This commit updates the migration window dimensions for improved usability, sets explicit entry points for preload scripts, and enhances the overall layout of the migration interface. It introduces new styles for buttons and alerts, improves the structure of the migration steps, and refines the user experience with clearer feedback and options during the migration process.
This commit is contained in:
fullex 2025-08-10 13:51:08 +08:00
parent 06dab978f7
commit 39257f64b1
6 changed files with 377 additions and 146 deletions

View File

@ -54,7 +54,18 @@ export default defineConfig({
}
},
build: {
sourcemap: isDev
sourcemap: isDev,
rollupOptions: {
// Unlike renderer which auto-discovers entries from HTML files,
// preload requires explicit entry point configuration for multiple scripts
input: {
index: resolve(__dirname, 'src/preload/index.ts'),
simplest: resolve(__dirname, 'src/preload/simplest.ts') // Minimal preload
},
output: {
entryFileNames: '[name].js'
}
}
}
},
renderer: {

View File

@ -375,16 +375,16 @@ class DataRefactorMigrateService {
this.registerMigrationIpcHandlers()
this.migrateWindow = new BrowserWindow({
width: 800,
height: 650,
resizable: true,
maximizable: true,
minimizable: true,
width: 640,
height: 480,
resizable: false,
maximizable: false,
minimizable: false,
show: false,
frame: false,
autoHideMenuBar: true,
titleBarStyle: 'default',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
preload: join(__dirname, '../preload/simplest.js'),
sandbox: false,
webSecurity: false,
contextIsolation: true
@ -400,9 +400,6 @@ class DataRefactorMigrateService {
this.migrateWindow.once('ready-to-show', () => {
this.migrateWindow?.show()
if (!app.isPackaged) {
this.migrateWindow?.webContents.openDevTools()
}
})
this.migrateWindow.on('closed', () => {

17
src/preload/simplest.ts Normal file
View File

@ -0,0 +1,17 @@
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge } from 'electron'
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
} catch (error) {
// eslint-disable-next-line no-restricted-syntax
console.error('[Preload]Failed to expose APIs:', error as Error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
}

View File

@ -9,8 +9,54 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body id="root">
<body id="root" theme-mode="light">
<script type="module" src="/src/windows/dataRefactorMigrate/entryPoint.tsx"></script>
<style>
html {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
box-sizing: border-box;
}
#root {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
}
/* Custom button styles */
.ant-btn-primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.ant-btn-primary:hover {
background-color: var(--color-primary-soft) !important;
border-color: var(--color-primary-soft) !important;
}
.ant-btn-primary:active,
.ant-btn-primary:focus {
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
}
/* Non-primary button hover styles */
.ant-btn:not(.ant-btn-primary):hover {
border-color: var(--color-primary-soft) !important;
color: var(--color-primary) !important;
}
</style>
</body>
</html>

View File

@ -1,11 +1,10 @@
import { CheckCircleOutlined, CloudUploadOutlined, LoadingOutlined, RocketOutlined } from '@ant-design/icons'
import { AppLogo } from '@renderer/config/env'
import { IpcChannel } from '@shared/IpcChannel'
import { Alert, Button, Card, Progress, Space, Steps, Typography } from 'antd'
import { Button, Progress, Space, Steps } from 'antd'
import { AlertTriangle, CheckCircle, Database, Loader2, Rocket } from 'lucide-react'
import React, { useEffect, useMemo, useState } from 'react'
import styled from 'styled-components'
const { Title, Text } = Typography
type MigrationStage =
| 'introduction' // Introduction phase - user can cancel
| 'backup_required' // Backup required - show backup requirement
@ -116,33 +115,37 @@ const MigrateApp: React.FC = () => {
const getProgressColor = () => {
switch (progress.stage) {
case 'completed':
return '#52c41a'
return 'var(--color-primary)'
case 'error':
return '#ff4d4f'
case 'backup_confirmed':
return '#52c41a'
return 'var(--color-primary)'
default:
return '#1890ff'
return 'var(--color-primary)'
}
}
const getCurrentStepIcon = () => {
switch (progress.stage) {
case 'introduction':
return <RocketOutlined style={{ fontSize: 64, color: '#1890ff', margin: '24px 0' }} />
return <Rocket size={48} color="var(--color-primary)" />
case 'backup_required':
case 'backup_progress':
return <CloudUploadOutlined style={{ fontSize: 64, color: '#faad14', margin: '24px 0' }} />
return <Database size={48} color="var(--color-primary)" />
case 'backup_confirmed':
return <CheckCircleOutlined style={{ fontSize: 64, color: '#52c41a', margin: '24px 0' }} />
return <CheckCircle size={48} color="var(--color-primary)" />
case 'migration':
return <LoadingOutlined style={{ fontSize: 64, color: '#1890ff', margin: '24px 0' }} />
return (
<SpinningIcon>
<Loader2 size={48} color="var(--color-primary)" />
</SpinningIcon>
)
case 'completed':
return <CheckCircleOutlined style={{ fontSize: 64, color: '#52c41a', margin: '24px 0' }} />
return <CheckCircle size={48} color="var(--color-primary)" />
case 'error':
return <div style={{ fontSize: 64, color: '#ff4d4f', margin: '24px 0' }}></div>
return <AlertTriangle size={48} color="#ff4d4f" />
default:
return <RocketOutlined style={{ fontSize: 64, color: '#1890ff', margin: '24px 0' }} />
return <Rocket size={48} color="var(--color-primary)" />
}
}
@ -150,48 +153,62 @@ const MigrateApp: React.FC = () => {
switch (progress.stage) {
case 'introduction':
return (
<Space>
<>
<Button onClick={handleCancel}></Button>
<Spacer />
<Button type="primary" onClick={handleProceedToBackup}>
</Button>
</Space>
</>
)
case 'backup_required':
return (
<Space>
<>
<Button onClick={handleCancel}></Button>
<Spacer />
<Button onClick={handleBackupCompleted}></Button>
<Button type="primary" onClick={handleShowBackupDialog}>
</Button>
<Button onClick={handleBackupCompleted}></Button>
</Space>
</>
)
case 'backup_confirmed':
return (
<Space>
<ButtonRow>
<Button onClick={handleCancel}></Button>
<Button type="primary" onClick={handleStartMigration}>
</Button>
</Space>
<Space>
<Button type="primary" onClick={handleStartMigration}>
</Button>
</Space>
</ButtonRow>
)
case 'migration':
return <Button disabled>...</Button>
return (
<ButtonRow>
<div></div>
<Button disabled>...</Button>
</ButtonRow>
)
case 'completed':
return (
<Button type="primary" onClick={handleRestartApp}>
</Button>
<ButtonRow>
<div></div>
<Button type="primary" onClick={handleRestartApp}>
</Button>
</ButtonRow>
)
case 'error':
return (
<Space>
<ButtonRow>
<Button onClick={handleCloseWindow}></Button>
<Button type="primary" onClick={handleRetryMigration}>
</Button>
</Space>
<Space>
<Button type="primary" onClick={handleRetryMigration}>
</Button>
</Space>
</ButtonRow>
)
default:
return null
@ -200,94 +217,109 @@ const MigrateApp: React.FC = () => {
return (
<Container>
<MigrationCard
title={
<div style={{ textAlign: 'center' }}>
<Title level={4} style={{ margin: 0, fontWeight: 'normal' }}>
</Title>
</div>
}
bordered={false}>
<div style={{ margin: '16px 0 32px 0' }}>
<Steps
current={currentStep}
status={stepStatus}
size="small"
items={[{ title: '介绍' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
/>
</div>
{/* Header */}
<Header>
<HeaderLogo src={AppLogo} />
<div style={{ textAlign: 'center' }}>{getCurrentStepIcon()}</div>
<HeaderTitle></HeaderTitle>
</Header>
{progress.stage !== 'introduction' && progress.stage !== 'error' && (
<ProgressContainer>
<Progress
percent={progress.progress}
strokeColor={getProgressColor()}
trailColor="#f0f0f0"
size="default"
showInfo={true}
{/* Main Content */}
<MainContent>
{/* Left Sidebar with Steps */}
<LeftSidebar>
<StepsContainer>
<Steps
direction="vertical"
current={currentStep}
status={stepStatus}
size="small"
items={[{ title: '介绍' }, { title: '备份' }, { title: '迁移' }, { title: '完成' }]}
/>
</ProgressContainer>
)}
</StepsContainer>
</LeftSidebar>
<MessageContainer>
<Text type="secondary">{progress.message}</Text>
</MessageContainer>
{/* Right Content Area */}
<RightContent>
<ContentArea>
<InfoIcon>{getCurrentStepIcon()}</InfoIcon>
{progress.stage === 'introduction' && (
<Alert
message="数据迁移向导"
description="本次更新将数据迁移到更高效的存储格式,迁移前会创建完整备份确保数据安全。"
type="info"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'introduction' && (
<InfoCard>
<InfoTitle></InfoTitle>
<InfoDescription>
Cherry Studio对数据的存储和使用方式进行了重大重构
<br />
<br />
使
<br />
<br />
使
</InfoDescription>
</InfoCard>
)}
{progress.stage === 'backup_required' && (
<Alert
message="创建数据备份"
description="迁移前必须创建数据备份以确保数据安全。请选择备份位置或确认已有最新备份。"
type="warning"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'backup_required' && (
<InfoCard variant="warning">
<InfoTitle></InfoTitle>
<InfoDescription style={{ textAlign: 'center' }}>
</InfoDescription>
</InfoCard>
)}
{progress.stage === 'backup_confirmed' && (
<Alert
message="备份完成"
description="数据备份已完成,现在可以安全地开始迁移。"
type="success"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'backup_progress' && (
<InfoCard variant="warning">
<InfoTitle></InfoTitle>
<InfoDescription style={{ textAlign: 'center' }}></InfoDescription>
</InfoCard>
)}
{progress.stage === 'error' && (
<Alert
message="迁移失败"
description={progress.error || '迁移过程遇到错误,您可以重新尝试或继续使用之前版本(原始数据完好保存)。'}
type="error"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'backup_confirmed' && (
<InfoCard variant="success">
<InfoTitle></InfoTitle>
<InfoDescription style={{ textAlign: 'center' }}>
</InfoDescription>
</InfoCard>
)}
{progress.stage === 'completed' && (
<Alert
message="迁移完成"
description="数据已成功迁移到新格式,应用将重新启动以应用更改。"
type="success"
showIcon
style={{ marginTop: 16 }}
/>
)}
{progress.stage === 'error' && (
<InfoCard variant="error">
<InfoTitle></InfoTitle>
<InfoDescription>
{progress.error || '迁移过程遇到错误,您可以重新尝试或继续使用之前版本(原始数据完好保存)。'}
</InfoDescription>
</InfoCard>
)}
<div style={{ textAlign: 'center', marginTop: 24 }}>{renderActionButtons()}</div>
</MigrationCard>
{progress.stage === 'completed' && (
<InfoCard variant="success">
<InfoTitle></InfoTitle>
<InfoDescription>使</InfoDescription>
</InfoCard>
)}
{progress.stage !== 'introduction' &&
progress.stage !== 'error' &&
progress.stage !== 'backup_required' &&
progress.stage !== 'backup_confirmed' && (
<ProgressContainer>
<Progress
percent={progress.progress}
strokeColor={getProgressColor()}
trailColor="#f0f0f0"
size="default"
showInfo={true}
/>
</ProgressContainer>
)}
</ContentArea>
</RightContent>
</MainContent>
{/* Footer */}
<Footer>{renderActionButtons()}</Footer>
</Container>
)
}
@ -297,36 +329,163 @@ const Container = styled.div`
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #f5f5f5;
padding: 20px;
background: #fff;
`
const MigrationCard = styled(Card)`
const Header = styled.div`
height: 48px;
background: rgb(240, 240, 240);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
-webkit-app-region: drag;
user-select: none;
`
const HeaderTitle = styled.div`
font-size: 16px;
font-weight: 600;
color: black;
margin-left: 12px;
`
const HeaderLogo = styled.img`
width: 24px;
height: 24px;
border-radius: 6px;
`
const MainContent = styled.div`
flex: 1;
display: flex;
overflow: hidden;
`
const LeftSidebar = styled.div`
width: 150px;
background: #fff;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
`
const StepsContainer = styled.div`
padding: 32px 24px;
flex: 1;
.ant-steps-item-process .ant-steps-item-icon {
background-color: var(--color-primary);
border-color: var(--color-primary-soft);
}
.ant-steps-item-finish .ant-steps-item-icon {
background-color: var(--color-primary-mute);
border-color: var(--color-primary-mute);
}
.ant-steps-item-finish .ant-steps-item-icon > .ant-steps-icon {
color: var(--color-primary);
}
.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon {
color: #fff;
}
.ant-steps-item-wait .ant-steps-item-icon {
border-color: #d9d9d9;
}
`
const RightContent = styled.div`
flex: 1;
display: flex;
flex-direction: column;
`
const ContentArea = styled.div`
flex: 1;
display: flex;
flex-direction: column;
/* justify-content: center; */
/* align-items: center; */
/* margin: 0 auto; */
width: 100%;
max-width: 600px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
padding: 24px;
`
.ant-card-head {
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
const Footer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.ant-card-body {
padding: 24px 32px 32px 32px;
}
background: rgb(250, 250, 250);
height: 64px;
padding: 0 24px;
gap: 16px;
`
const Spacer = styled.div`
flex: 1;
`
const ProgressContainer = styled.div`
margin: 24px 0;
margin: 32px 0;
width: 100%;
`
const MessageContainer = styled.div`
const ButtonRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
min-width: 300px;
`
const InfoIcon = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-top: 12px;
`
const InfoCard = styled.div<{ variant?: 'info' | 'warning' | 'success' | 'error' }>`
width: 100%;
`
const InfoTitle = styled.div`
margin-bottom: 32px;
margin-top: 32px;
font-size: 16px;
font-weight: 600;
color: var(--color-primary);
line-height: 1.4;
text-align: center;
margin: 16px 0;
min-height: 24px;
`
const InfoDescription = styled.p`
margin: 0;
color: rgba(0, 0, 0, 0.68);
line-height: 1.8;
max-width: 420px;
margin: 0 auto;
`
const SpinningIcon = styled.div`
display: inline-block;
animation: spin 2s linear infinite;
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`
export default MigrateApp

View File

@ -1,9 +1,10 @@
import '../../assets/styles/index.scss'
import '@ant-design/v5-patch-for-react-19'
import '@renderer/assets/styles/index.scss'
import ReactDOM from 'react-dom/client'
import { createRoot } from 'react-dom/client'
import MigrateApp from './MigrateApp'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
const root = createRoot(document.getElementById('root') as HTMLElement)
root.render(<MigrateApp />)