1019 lines
24 KiB
Markdown
1019 lines
24 KiB
Markdown
# React 到 Vue 完整迁移方案
|
||
|
||
## 📋 文档说明
|
||
|
||
本文档提供从 React 技术栈完整迁移到 Vue 3 技术栈的详细方案,包括技术选型、迁移步骤、代码转换示例和测试策略。
|
||
|
||
---
|
||
|
||
## 🎯 迁移目标
|
||
|
||
- **完全移除 React 依赖**
|
||
- **使用 Vue 3 Composition API**
|
||
- **保持所有现有功能**
|
||
- **保持 Tailwind CSS 样式**
|
||
- **保持 TypeScript 类型系统**
|
||
|
||
---
|
||
|
||
## 🛠️ 技术选型
|
||
|
||
### 核心框架
|
||
- **Vue**: `^3.4.0` (使用 Composition API)
|
||
- **TypeScript**: `^5.2.2` (保持不变)
|
||
- **Vite**: `^5.0.8` (保持不变,更换插件)
|
||
|
||
### 状态管理
|
||
- **Pinia**: `^2.1.0` (推荐,替代 Context API)
|
||
|
||
### 路由(可选)
|
||
- **Vue Router**: `^4.2.0` (如果需要路由功能)
|
||
|
||
### UI 库
|
||
- **图标库**: `lucide-vue-next`: `^0.344.0`
|
||
- **图表库**: `echarts-for-vue`: `^1.2.0` 或 `vue-echarts`: `^6.6.0`
|
||
|
||
### 样式
|
||
- **Tailwind CSS**: `^3.3.6` (保持不变)
|
||
|
||
---
|
||
|
||
## 📦 依赖包替换清单
|
||
|
||
### 需要移除的依赖
|
||
```json
|
||
{
|
||
"react": "^18.2.0",
|
||
"react-dom": "^18.2.0",
|
||
"lucide-react": "^0.344.0",
|
||
"@vitejs/plugin-react": "^4.2.1",
|
||
"@types/react": "^18.2.43",
|
||
"@types/react-dom": "^18.2.17",
|
||
"eslint-plugin-react-hooks": "^4.6.0",
|
||
"eslint-plugin-react-refresh": "^0.4.5"
|
||
}
|
||
```
|
||
|
||
### 需要添加的依赖
|
||
```json
|
||
{
|
||
"vue": "^3.4.0",
|
||
"@vitejs/plugin-vue": "^5.0.0",
|
||
"vue-tsc": "^1.8.25",
|
||
"lucide-vue-next": "^0.344.0",
|
||
"pinia": "^2.1.0",
|
||
"vue-router": "^4.2.0",
|
||
"@vueuse/core": "^10.7.0"
|
||
}
|
||
```
|
||
|
||
### 开发依赖
|
||
```json
|
||
{
|
||
"@vitejs/plugin-vue": "^5.0.0",
|
||
"@vue/eslint-config-typescript": "^12.0.0",
|
||
"eslint-plugin-vue": "^9.20.0"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🗂️ 迁移阶段规划
|
||
|
||
### 阶段一:项目基础配置(1-2 天)
|
||
|
||
#### 1.1 更新 package.json
|
||
```json
|
||
{
|
||
"name": "finyx-frontend",
|
||
"version": "0.0.0",
|
||
"type": "module",
|
||
"scripts": {
|
||
"dev": "vite",
|
||
"build": "vue-tsc && vite build",
|
||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx",
|
||
"preview": "vite preview",
|
||
"type-check": "vue-tsc --noEmit"
|
||
},
|
||
"dependencies": {
|
||
"vue": "^3.4.0",
|
||
"pinia": "^2.1.0",
|
||
"vue-router": "^4.2.0",
|
||
"lucide-vue-next": "^0.344.0",
|
||
"@vueuse/core": "^10.7.0"
|
||
},
|
||
"devDependencies": {
|
||
"@vitejs/plugin-vue": "^5.0.0",
|
||
"@vue/eslint-config-typescript": "^12.0.0",
|
||
"eslint-plugin-vue": "^9.20.0",
|
||
"typescript": "^5.2.2",
|
||
"vite": "^5.0.8",
|
||
"tailwindcss": "^3.3.6",
|
||
"vue-tsc": "^1.8.25"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 1.2 更新 vite.config.ts
|
||
```typescript
|
||
import { defineConfig } from 'vite'
|
||
import vue from '@vitejs/plugin-vue'
|
||
import { fileURLToPath, URL } from 'node:url'
|
||
|
||
export default defineConfig({
|
||
plugins: [vue()],
|
||
resolve: {
|
||
alias: {
|
||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||
}
|
||
},
|
||
server: {
|
||
host: '0.0.0.0',
|
||
port: 5173,
|
||
strictPort: false,
|
||
open: false,
|
||
},
|
||
preview: {
|
||
host: '0.0.0.0',
|
||
port: 4173,
|
||
},
|
||
})
|
||
```
|
||
|
||
#### 1.3 更新 tsconfig.json
|
||
```json
|
||
{
|
||
"compilerOptions": {
|
||
"target": "ES2020",
|
||
"useDefineForClassFields": true,
|
||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||
"module": "ESNext",
|
||
"skipLibCheck": true,
|
||
"moduleResolution": "bundler",
|
||
"allowImportingTsExtensions": true,
|
||
"resolveJsonModule": true,
|
||
"isolatedModules": true,
|
||
"noEmit": true,
|
||
"jsx": "preserve",
|
||
|
||
"strict": true,
|
||
"noUnusedLocals": true,
|
||
"noUnusedParameters": true,
|
||
"noFallthroughCasesInSwitch": true,
|
||
|
||
"baseUrl": ".",
|
||
"paths": {
|
||
"@/*": ["./src/*"]
|
||
}
|
||
},
|
||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||
"references": [{ "path": "./tsconfig.node.json" }]
|
||
}
|
||
```
|
||
|
||
#### 1.4 创建 env.d.ts
|
||
```typescript
|
||
/// <reference types="vite/client" />
|
||
|
||
declare module '*.vue' {
|
||
import type { DefineComponent } from 'vue'
|
||
const component: DefineComponent<{}, {}, any>
|
||
export default component
|
||
}
|
||
```
|
||
|
||
### 阶段二:类型定义迁移(1 天)
|
||
|
||
类型定义文件基本不需要修改,只需确保兼容 Vue。
|
||
|
||
**src/types/index.ts** - 保持不变
|
||
|
||
### 阶段三:工具组件迁移(2-3 天)
|
||
|
||
#### 3.1 ProgressBar 组件
|
||
|
||
**React 版本** (`src/components/ProgressBar.tsx`):
|
||
```typescript
|
||
import React from 'react';
|
||
|
||
interface ProgressBarProps {
|
||
percent: number;
|
||
status: string;
|
||
}
|
||
|
||
export const ProgressBar: React.FC<ProgressBarProps> = ({ percent, status }) => {
|
||
// ...
|
||
};
|
||
```
|
||
|
||
**Vue 版本** (`src/components/ProgressBar.vue`):
|
||
```vue
|
||
<template>
|
||
<div class="w-full bg-slate-200 rounded-full h-2">
|
||
<div
|
||
:class="colorClass"
|
||
class="h-2 rounded-full"
|
||
:style="{ width: `${percent}%` }"
|
||
></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
interface Props {
|
||
percent: number;
|
||
status: string;
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
|
||
const colorClass = computed(() => {
|
||
const colorMap: Record<string, string> = {
|
||
risk: 'bg-red-500',
|
||
warning: 'bg-amber-500',
|
||
review: 'bg-purple-500',
|
||
new: 'bg-slate-300',
|
||
};
|
||
return colorMap[props.status] || 'bg-blue-500';
|
||
});
|
||
</script>
|
||
```
|
||
|
||
#### 3.2 Toast 组件迁移
|
||
|
||
**React 版本** (`src/components/Toast.tsx`):
|
||
```typescript
|
||
export const useToast = () => {
|
||
const [toasts, setToasts] = React.useState<Toast[]>([]);
|
||
// ...
|
||
};
|
||
```
|
||
|
||
**Vue 版本** (`src/components/Toast.vue`):
|
||
```vue
|
||
<template>
|
||
<div v-if="toasts.length > 0" class="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
|
||
<div
|
||
v-for="toast in toasts"
|
||
:key="toast.id"
|
||
class="pointer-events-auto"
|
||
>
|
||
<ToastItem :toast="toast" @close="closeToast" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import ToastItem from './ToastItem.vue';
|
||
import type { Toast, ToastType } from './types';
|
||
|
||
const toasts = ref<Toast[]>([]);
|
||
|
||
const showToast = (message: string, type: ToastType = 'info', duration?: number) => {
|
||
const id = Math.random().toString(36).substring(7);
|
||
const newToast: Toast = { id, message, type, duration };
|
||
toasts.value.push(newToast);
|
||
|
||
setTimeout(() => {
|
||
closeToast(id);
|
||
}, duration || 3000);
|
||
|
||
return id;
|
||
};
|
||
|
||
const closeToast = (id: string) => {
|
||
const index = toasts.value.findIndex(t => t.id === id);
|
||
if (index > -1) {
|
||
toasts.value.splice(index, 1);
|
||
}
|
||
};
|
||
|
||
defineExpose({
|
||
success: (message: string, duration?: number) => showToast(message, 'success', duration),
|
||
error: (message: string, duration?: number) => showToast(message, 'error', duration),
|
||
warning: (message: string, duration?: number) => showToast(message, 'warning', duration),
|
||
info: (message: string, duration?: number) => showToast(message, 'info', duration),
|
||
});
|
||
</script>
|
||
```
|
||
|
||
**Toast Context 迁移为 Pinia Store** (`src/stores/toast.ts`):
|
||
```typescript
|
||
import { defineStore } from 'pinia';
|
||
import { ref } from 'vue';
|
||
import type { Toast, ToastType } from '@/types';
|
||
|
||
export const useToastStore = defineStore('toast', () => {
|
||
const toasts = ref<Toast[]>([]);
|
||
|
||
const showToast = (message: string, type: ToastType = 'info', duration?: number) => {
|
||
const id = Math.random().toString(36).substring(7);
|
||
const newToast: Toast = { id, message, type, duration };
|
||
toasts.value.push(newToast);
|
||
|
||
setTimeout(() => {
|
||
closeToast(id);
|
||
}, duration || 3000);
|
||
|
||
return id;
|
||
};
|
||
|
||
const closeToast = (id: string) => {
|
||
const index = toasts.value.findIndex(t => t.id === id);
|
||
if (index > -1) {
|
||
toasts.value.splice(index, 1);
|
||
}
|
||
};
|
||
|
||
const success = (message: string, duration?: number) => showToast(message, 'success', duration);
|
||
const error = (message: string, duration?: number) => showToast(message, 'error', duration);
|
||
const warning = (message: string, duration?: number) => showToast(message, 'warning', duration);
|
||
const info = (message: string, duration?: number) => showToast(message, 'info', duration);
|
||
|
||
return {
|
||
toasts,
|
||
showToast,
|
||
closeToast,
|
||
success,
|
||
error,
|
||
warning,
|
||
info,
|
||
};
|
||
});
|
||
```
|
||
|
||
### 阶段四:布局组件迁移(2-3 天)
|
||
|
||
#### 4.1 MainLayout 组件
|
||
|
||
**React 版本**:
|
||
```typescript
|
||
export const MainLayout: React.FC<MainLayoutProps> = ({
|
||
currentView,
|
||
setCurrentView,
|
||
// ...
|
||
}) => (
|
||
<div className="flex h-screen w-full bg-slate-900">
|
||
{/* ... */}
|
||
</div>
|
||
);
|
||
```
|
||
|
||
**Vue 版本** (`src/layouts/MainLayout.vue`):
|
||
```vue
|
||
<template>
|
||
<div class="flex h-screen w-full bg-slate-900 font-sans text-slate-900 overflow-hidden">
|
||
<Sidebar
|
||
:current-view="currentView"
|
||
@update:current-view="setCurrentView"
|
||
:is-presentation-mode="isPresentationMode"
|
||
/>
|
||
|
||
<div class="flex-1 flex flex-col bg-slate-50 relative overflow-hidden">
|
||
<DashboardView
|
||
v-if="currentView === 'dashboard'"
|
||
@update:current-view="setCurrentView"
|
||
@update:current-step="setCurrentStep"
|
||
/>
|
||
<ProjectListView
|
||
v-if="currentView === 'projects'"
|
||
@update:current-view="setCurrentView"
|
||
@update:current-step="setCurrentStep"
|
||
/>
|
||
<EngagementView
|
||
v-if="currentView === 'engagement'"
|
||
:current-step="currentStep"
|
||
@update:current-step="setCurrentStep"
|
||
@update:current-view="setCurrentView"
|
||
:is-presentation-mode="isPresentationMode"
|
||
@update:is-presentation-mode="setIsPresentationMode"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import { Sidebar } from './Sidebar.vue';
|
||
import DashboardView from '@/pages/DashboardView.vue';
|
||
import ProjectListView from '@/pages/ProjectListView.vue';
|
||
import EngagementView from '@/pages/EngagementView.vue';
|
||
import type { ViewMode, Step } from '@/types';
|
||
|
||
const currentView = ref<ViewMode>('projects');
|
||
const currentStep = ref<Step>('setup');
|
||
const isPresentationMode = ref(false);
|
||
|
||
const setCurrentView = (view: ViewMode) => {
|
||
currentView.value = view;
|
||
};
|
||
|
||
const setCurrentStep = (step: Step) => {
|
||
currentStep.value = step;
|
||
};
|
||
|
||
const setIsPresentationMode = (mode: boolean) => {
|
||
isPresentationMode.value = mode;
|
||
};
|
||
</script>
|
||
```
|
||
|
||
### 阶段五:页面组件迁移(5-7 天)
|
||
|
||
#### 5.1 SetupStep 组件示例
|
||
|
||
**React 版本关键代码**:
|
||
```typescript
|
||
export const SetupStep: React.FC<SetupStepProps> = ({ setCurrentStep }) => {
|
||
const toast = useToast();
|
||
const [projectName, setProjectName] = useState('');
|
||
const [selectedIndustries, setSelectedIndustries] = useState<string[]>([]);
|
||
const [errors, setErrors] = useState({});
|
||
|
||
useEffect(() => {
|
||
// 验证逻辑
|
||
}, [projectName, selectedIndustries, touched]);
|
||
|
||
return (
|
||
<div className="p-8">
|
||
<input
|
||
value={projectName}
|
||
onChange={(e) => setProjectName(e.target.value)}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
**Vue 版本** (`src/pages/engagement/SetupStep.vue`):
|
||
```vue
|
||
<template>
|
||
<div class="p-8 h-full flex flex-col overflow-y-auto animate-fade-in">
|
||
<div class="text-center mb-8">
|
||
<h2 class="text-3xl font-bold text-slate-900">项目初始化配置</h2>
|
||
<p class="text-slate-500 mt-2">请填写项目基本信息,系统将为您配置数据资产盘点环境</p>
|
||
</div>
|
||
|
||
<div class="max-w-4xl mx-auto w-full space-y-6 mb-8">
|
||
<!-- Project Name -->
|
||
<div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
||
<label class="block text-sm font-bold text-slate-700 mb-3">
|
||
项目名称 <span class="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
v-model="projectName"
|
||
@blur="touched.projectName = true"
|
||
:class="[
|
||
'w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||
errors.projectName ? 'border-red-500' : 'border-slate-300'
|
||
]"
|
||
placeholder="请输入项目名称"
|
||
/>
|
||
<p v-if="errors.projectName" class="text-red-500 text-sm mt-1">
|
||
{{ errors.projectName }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Industries Selection -->
|
||
<div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
|
||
<label class="block text-sm font-bold text-slate-700 mb-3">
|
||
所属行业 <span class="text-red-500">*</span>
|
||
</label>
|
||
<div class="flex flex-wrap gap-2">
|
||
<button
|
||
v-for="industry in industries"
|
||
:key="industry"
|
||
@click="toggleIndustry(industry)"
|
||
:class="[
|
||
'px-4 py-2 rounded-lg border transition-colors',
|
||
selectedIndustries.includes(industry)
|
||
? 'bg-blue-50 border-blue-500 text-blue-700'
|
||
: 'bg-white border-slate-300 text-slate-700 hover:border-blue-300'
|
||
]"
|
||
>
|
||
{{ industry }}
|
||
</button>
|
||
</div>
|
||
<p v-if="errors.industries" class="text-red-500 text-sm mt-2">
|
||
{{ errors.industries }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Actions -->
|
||
<div class="max-w-4xl mx-auto w-full flex justify-end gap-4">
|
||
<button
|
||
@click="handleNext"
|
||
:disabled="!isValid"
|
||
class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||
>
|
||
下一步
|
||
<ArrowRight :size="18" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue';
|
||
import { ArrowRight } from 'lucide-vue-next';
|
||
import { useToastStore } from '@/stores/toast';
|
||
import type { Step } from '@/types';
|
||
|
||
interface Props {
|
||
setCurrentStep?: (step: Step) => void;
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
|
||
const toast = useToastStore();
|
||
|
||
const projectName = ref('');
|
||
const companyDescription = ref('');
|
||
const owner = ref('');
|
||
const selectedIndustries = ref<string[]>([]);
|
||
const errors = ref<{
|
||
projectName?: string;
|
||
companyDescription?: string;
|
||
industries?: string;
|
||
}>({});
|
||
const touched = ref<{
|
||
projectName?: boolean;
|
||
companyDescription?: boolean;
|
||
industries?: boolean;
|
||
}>({});
|
||
|
||
const industries = [
|
||
'零售 - 生鲜连锁',
|
||
'零售 - 快消品',
|
||
'金融 - 商业银行',
|
||
// ... 更多行业
|
||
];
|
||
|
||
// 实时验证
|
||
watch([projectName, companyDescription, selectedIndustries, touched], () => {
|
||
const newErrors: typeof errors.value = {};
|
||
|
||
if (touched.value.projectName && !projectName.value.trim()) {
|
||
newErrors.projectName = '请填写项目名称';
|
||
}
|
||
|
||
if (touched.value.companyDescription && !companyDescription.value.trim()) {
|
||
newErrors.companyDescription = '请填写企业及主营业务简介';
|
||
}
|
||
|
||
if (touched.value.industries && selectedIndustries.value.length === 0) {
|
||
newErrors.industries = '请至少选择一个所属行业';
|
||
}
|
||
|
||
errors.value = newErrors;
|
||
}, { deep: true });
|
||
|
||
const isValid = computed(() => {
|
||
return projectName.value.trim() !== '' &&
|
||
companyDescription.value.trim() !== '' &&
|
||
selectedIndustries.value.length > 0;
|
||
});
|
||
|
||
const toggleIndustry = (industry: string) => {
|
||
const index = selectedIndustries.value.indexOf(industry);
|
||
if (index > -1) {
|
||
selectedIndustries.value.splice(index, 1);
|
||
} else {
|
||
selectedIndustries.value.push(industry);
|
||
}
|
||
touched.value.industries = true;
|
||
};
|
||
|
||
const handleNext = () => {
|
||
if (!isValid.value) {
|
||
toast.error('请完成所有必填项');
|
||
return;
|
||
}
|
||
|
||
toast.success('项目配置已保存');
|
||
props.setCurrentStep?.('inventory');
|
||
};
|
||
</script>
|
||
```
|
||
|
||
#### 5.2 InventoryStep 组件迁移要点
|
||
|
||
**useMemo 转换**:
|
||
```typescript
|
||
// React
|
||
const sortedData = useMemo(() => {
|
||
// 排序逻辑
|
||
}, [sortField, sortDirection]);
|
||
|
||
// Vue
|
||
const sortedData = computed(() => {
|
||
if (!sortField.value || !sortDirection.value) return inventoryData;
|
||
// 排序逻辑
|
||
});
|
||
```
|
||
|
||
**useEffect 转换**:
|
||
```typescript
|
||
// React
|
||
useEffect(() => {
|
||
if (inventoryMode === 'processing') {
|
||
// 处理逻辑
|
||
}
|
||
}, [inventoryMode]);
|
||
|
||
// Vue
|
||
watch(() => props.inventoryMode, (newMode) => {
|
||
if (newMode === 'processing') {
|
||
// 处理逻辑
|
||
}
|
||
}, { immediate: true });
|
||
```
|
||
|
||
### 阶段六:主应用入口迁移(1 天)
|
||
|
||
#### 6.1 main.ts 文件
|
||
|
||
**React 版本** (`src/main.tsx`):
|
||
```typescript
|
||
import React from 'react'
|
||
import ReactDOM from 'react-dom/client'
|
||
import App from './App.tsx'
|
||
import './index.css'
|
||
|
||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||
<React.StrictMode>
|
||
<App />
|
||
</React.StrictMode>,
|
||
)
|
||
```
|
||
|
||
**Vue 版本** (`src/main.ts`):
|
||
```typescript
|
||
import { createApp } from 'vue'
|
||
import { createPinia } from 'pinia'
|
||
import App from './App.vue'
|
||
import './index.css'
|
||
|
||
const app = createApp(App)
|
||
const pinia = createPinia()
|
||
|
||
app.use(pinia)
|
||
app.mount('#root')
|
||
```
|
||
|
||
#### 6.2 App.vue
|
||
|
||
**React 版本** (`src/App.tsx`):
|
||
```typescript
|
||
function App() {
|
||
const [currentView, setCurrentView] = useState<ViewMode>('projects');
|
||
// ...
|
||
return (
|
||
<ToastProvider>
|
||
<MainLayout ... />
|
||
</ToastProvider>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Vue 版本** (`src/App.vue`):
|
||
```vue
|
||
<template>
|
||
<MainLayout
|
||
:current-view="currentView"
|
||
@update:current-view="setCurrentView"
|
||
:current-step="currentStep"
|
||
@update:current-step="setCurrentStep"
|
||
:is-presentation-mode="isPresentationMode"
|
||
@update:is-presentation-mode="setIsPresentationMode"
|
||
/>
|
||
<ToastContainer />
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue';
|
||
import MainLayout from '@/layouts/MainLayout.vue';
|
||
import ToastContainer from '@/components/ToastContainer.vue';
|
||
import type { ViewMode, Step } from '@/types';
|
||
|
||
const currentView = ref<ViewMode>('projects');
|
||
const currentStep = ref<Step>('setup');
|
||
const isPresentationMode = ref(false);
|
||
|
||
const setCurrentView = (view: ViewMode) => {
|
||
currentView.value = view;
|
||
};
|
||
|
||
const setCurrentStep = (step: Step) => {
|
||
currentStep.value = step;
|
||
};
|
||
|
||
const setIsPresentationMode = (mode: boolean) => {
|
||
isPresentationMode.value = mode;
|
||
};
|
||
</script>
|
||
```
|
||
|
||
### 阶段七:图标库迁移(1 天)
|
||
|
||
#### 7.1 全局替换图标导入
|
||
|
||
**查找替换规则**:
|
||
```typescript
|
||
// 从
|
||
import { IconName } from 'lucide-react';
|
||
<IconName size={20} />
|
||
|
||
// 到
|
||
import { IconName } from 'lucide-vue-next';
|
||
<IconName :size="20" />
|
||
```
|
||
|
||
**批量替换脚本** (`scripts/replace-icons.sh`):
|
||
```bash
|
||
#!/bin/bash
|
||
# 替换所有文件中的 lucide-react 导入
|
||
find src -type f \( -name "*.vue" -o -name "*.ts" -o -name "*.tsx" \) -exec sed -i 's/lucide-react/lucide-vue-next/g' {} \;
|
||
|
||
# 替换图标属性 size={} 为 :size=""
|
||
find src -type f -name "*.vue" -exec sed -i 's/size={\([^}]*\)}/:size="\1"/g' {} \;
|
||
```
|
||
|
||
### 阶段八:测试与验证(3-5 天)
|
||
|
||
#### 8.1 功能测试清单
|
||
|
||
- [ ] Dashboard 页面渲染正常
|
||
- [ ] 项目列表显示正常
|
||
- [ ] 项目搜索和筛选功能
|
||
- [ ] 项目作业台所有步骤正常
|
||
- [ ] 步骤导航功能
|
||
- [ ] 表单验证功能
|
||
- [ ] Toast 通知功能
|
||
- [ ] 演示模式切换
|
||
- [ ] 响应式布局
|
||
- [ ] 所有交互功能
|
||
|
||
#### 8.2 性能测试
|
||
|
||
- [ ] 首屏加载时间
|
||
- [ ] 组件渲染性能
|
||
- [ ] 大数据量表格性能
|
||
- [ ] 内存使用情况
|
||
|
||
---
|
||
|
||
## 📝 代码转换对照表
|
||
|
||
### Hooks 转换对照
|
||
|
||
| React | Vue 3 |
|
||
|-------|-------|
|
||
| `useState(value)` | `ref(value)` 或 `reactive({})` |
|
||
| `useEffect(() => {}, [deps])` | `watch(() => deps, () => {})` 或 `watchEffect(() => {})` |
|
||
| `useMemo(() => value, [deps])` | `computed(() => value)` |
|
||
| `useCallback(() => {}, [deps])` | `computed(() => () => {})` 或直接定义函数 |
|
||
| `useContext(Context)` | `inject(key)` 或 Pinia store |
|
||
| `useRef(initial)` | `ref(initial)` |
|
||
| `useReducer(reducer, initial)` | `ref(initial)` + 自定义函数或 Pinia |
|
||
|
||
### 事件处理转换
|
||
|
||
| React | Vue 3 |
|
||
|-------|-------|
|
||
| `onClick={handler}` | `@click="handler"` |
|
||
| `onChange={(e) => {}}` | `@change="handler"` 或 `v-model="value"` |
|
||
| `onSubmit={handler}` | `@submit.prevent="handler"` |
|
||
| `onKeyDown={handler}` | `@keydown="handler"` |
|
||
|
||
### 条件渲染转换
|
||
|
||
| React | Vue 3 |
|
||
|-------|-------|
|
||
| `{condition && <Component />}` | `<Component v-if="condition" />` |
|
||
| `{condition ? <A /> : <B />}` | `<A v-if="condition" /><B v-else />` |
|
||
| `{items.map(item => <Item key={id} />)}` | `<Item v-for="item in items" :key="id" />` |
|
||
|
||
### Props 传递转换
|
||
|
||
| React | Vue 3 |
|
||
|-------|-------|
|
||
| `<Component prop={value} />` | `<Component :prop="value" />` |
|
||
| `<Component onClick={handler} />` | `<Component @click="handler" />` |
|
||
| `{...props}` | `v-bind="props"` |
|
||
|
||
---
|
||
|
||
## 🔧 配置文件更新清单
|
||
|
||
### 1. package.json
|
||
- [ ] 移除所有 React 相关依赖
|
||
- [ ] 添加 Vue 3 相关依赖
|
||
- [ ] 更新构建脚本
|
||
- [ ] 更新 lint 脚本
|
||
|
||
### 2. vite.config.ts
|
||
- [ ] 替换 `@vitejs/plugin-react` 为 `@vitejs/plugin-vue`
|
||
- [ ] 添加路径别名配置
|
||
- [ ] 保持服务器配置
|
||
|
||
### 3. tsconfig.json
|
||
- [ ] 更新 `jsx` 选项为 `"preserve"`
|
||
- [ ] 添加 Vue 文件类型支持
|
||
- [ ] 添加路径别名
|
||
|
||
### 4. 创建 env.d.ts
|
||
- [ ] 添加 Vue 模块声明
|
||
- [ ] 添加 Vite 客户端类型
|
||
|
||
### 5. ESLint 配置
|
||
创建 `.eslintrc.cjs`:
|
||
```javascript
|
||
module.exports = {
|
||
root: true,
|
||
env: {
|
||
node: true,
|
||
browser: true,
|
||
es2021: true,
|
||
},
|
||
extends: [
|
||
'plugin:vue/vue3-essential',
|
||
'eslint:recommended',
|
||
'@vue/eslint-config-typescript',
|
||
],
|
||
parserOptions: {
|
||
ecmaVersion: 2021,
|
||
},
|
||
rules: {
|
||
// 自定义规则
|
||
},
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 迁移执行步骤
|
||
|
||
### 第一步:备份当前代码
|
||
```bash
|
||
git checkout -b vue-migration
|
||
git commit -am "Backup before Vue migration"
|
||
```
|
||
|
||
### 第二步:安装新依赖
|
||
```bash
|
||
npm uninstall react react-dom lucide-react @vitejs/plugin-react @types/react @types/react-dom
|
||
npm install vue@^3.4.0 pinia@^2.1.0 vue-router@^4.2.0 lucide-vue-next@^0.344.0 @vueuse/core@^10.7.0
|
||
npm install -D @vitejs/plugin-vue@^5.0.0 vue-tsc@^1.8.25 @vue/eslint-config-typescript@^12.0.0 eslint-plugin-vue@^9.20.0
|
||
```
|
||
|
||
### 第三步:更新配置文件
|
||
按照上述配置文件更新清单逐一更新。
|
||
|
||
### 第四步:迁移组件(按阶段执行)
|
||
1. 先迁移简单组件(ProgressBar、SidebarItem)
|
||
2. 迁移工具组件(Toast)
|
||
3. 迁移布局组件(Sidebar、MainLayout)
|
||
4. 迁移页面组件(按复杂度从低到高)
|
||
|
||
### 第五步:测试验证
|
||
- 运行 `npm run dev` 检查编译错误
|
||
- 运行 `npm run type-check` 检查类型错误
|
||
- 手动测试所有功能
|
||
|
||
### 第六步:清理
|
||
- 删除所有 `.tsx` 文件
|
||
- 删除 React 相关配置
|
||
- 更新文档
|
||
|
||
---
|
||
|
||
## ⚠️ 常见问题与解决方案
|
||
|
||
### 问题 1: 图标组件不显示
|
||
**解决方案**: 确保使用 `lucide-vue-next` 并正确导入:
|
||
```typescript
|
||
import { IconName } from 'lucide-vue-next';
|
||
// 使用 :size 而不是 size
|
||
<IconName :size="20" />
|
||
```
|
||
|
||
### 问题 2: TypeScript 类型错误
|
||
**解决方案**:
|
||
- 确保 `env.d.ts` 文件存在
|
||
- 检查 `tsconfig.json` 配置
|
||
- 使用 `vue-tsc` 进行类型检查
|
||
|
||
### 问题 3: 响应式数据不更新
|
||
**解决方案**:
|
||
- 使用 `ref()` 包装基本类型
|
||
- 使用 `reactive()` 包装对象
|
||
- 访问 ref 值时使用 `.value`
|
||
|
||
### 问题 4: 事件处理不工作
|
||
**解决方案**:
|
||
- 使用 `@click` 而不是 `onClick`
|
||
- 使用 `@submit.prevent` 阻止默认行为
|
||
- 事件参数通过 `$event` 访问
|
||
|
||
---
|
||
|
||
## 📊 迁移进度跟踪
|
||
|
||
### 组件迁移清单
|
||
|
||
#### 基础组件
|
||
- [ ] ProgressBar.vue
|
||
- [ ] SidebarItem.vue
|
||
- [ ] TableCheckItem.vue
|
||
- [ ] Toast.vue
|
||
- [ ] ToastContainer.vue
|
||
|
||
#### 布局组件
|
||
- [ ] Sidebar.vue
|
||
- [ ] MainLayout.vue
|
||
|
||
#### 页面组件
|
||
- [ ] DashboardView.vue
|
||
- [ ] ProjectListView.vue
|
||
- [ ] EngagementView.vue
|
||
|
||
#### 工作流步骤组件
|
||
- [ ] SetupStep.vue
|
||
- [ ] InventoryStep.vue
|
||
- [ ] ContextStep.vue
|
||
- [ ] ValueStep.vue
|
||
- [ ] DeliveryStep.vue
|
||
|
||
#### 状态管理
|
||
- [ ] stores/toast.ts (Pinia)
|
||
- [ ] stores/app.ts (可选,全局状态)
|
||
|
||
#### 配置文件
|
||
- [ ] package.json
|
||
- [ ] vite.config.ts
|
||
- [ ] tsconfig.json
|
||
- [ ] env.d.ts
|
||
- [ ] .eslintrc.cjs
|
||
- [ ] main.ts
|
||
- [ ] App.vue
|
||
|
||
---
|
||
|
||
## 🎯 验收标准
|
||
|
||
### 功能完整性
|
||
- ✅ 所有原有功能正常工作
|
||
- ✅ 所有交互行为保持一致
|
||
- ✅ 所有样式保持一致
|
||
|
||
### 代码质量
|
||
- ✅ 无 TypeScript 类型错误
|
||
- ✅ 无 ESLint 错误
|
||
- ✅ 代码符合 Vue 3 最佳实践
|
||
|
||
### 性能指标
|
||
- ✅ 首屏加载时间 ≤ 2s
|
||
- ✅ 页面切换流畅
|
||
- ✅ 无内存泄漏
|
||
|
||
---
|
||
|
||
## 📚 参考资源
|
||
|
||
- [Vue 3 官方文档](https://vuejs.org/)
|
||
- [Vue 3 Composition API](https://vuejs.org/guide/extras/composition-api-faq.html)
|
||
- [Pinia 文档](https://pinia.vuejs.org/)
|
||
- [Vue Router 文档](https://router.vuejs.org/)
|
||
- [Vite 文档](https://vitejs.dev/)
|
||
|
||
---
|
||
|
||
## 📅 时间估算
|
||
|
||
| 阶段 | 工作量 | 说明 |
|
||
|------|--------|------|
|
||
| 阶段一:项目配置 | 1-2 天 | 配置文件更新 |
|
||
| 阶段二:类型定义 | 1 天 | 类型文件检查 |
|
||
| 阶段三:工具组件 | 2-3 天 | 简单组件迁移 |
|
||
| 阶段四:布局组件 | 2-3 天 | 布局组件迁移 |
|
||
| 阶段五:页面组件 | 5-7 天 | 复杂页面迁移 |
|
||
| 阶段六:应用入口 | 1 天 | 主文件迁移 |
|
||
| 阶段七:图标库 | 1 天 | 图标替换 |
|
||
| 阶段八:测试验证 | 3-5 天 | 功能测试和修复 |
|
||
| **总计** | **16-25 天** | **约 3-5 周** |
|
||
|
||
---
|
||
|
||
## 📝 更新记录
|
||
|
||
- **2025-01-XX**: 初始迁移方案创建
|
||
|
||
---
|
||
|
||
## 👥 贡献者
|
||
|
||
- Finyx AI Team
|