finyx_data_frontend/docs/Vue迁移完整方案.md

24 KiB
Raw Permalink Blame History

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.0vue-echarts: ^6.6.0

样式

  • Tailwind CSS: ^3.3.6 (保持不变)

📦 依赖包替换清单

需要移除的依赖

{
  "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"
}

需要添加的依赖

{
  "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"
}

开发依赖

{
  "@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

{
  "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

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

{
  "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

/// <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):

import React from 'react';

interface ProgressBarProps {
  percent: number;
  status: string;
}

export const ProgressBar: React.FC<ProgressBarProps> = ({ percent, status }) => {
  // ...
};

Vue 版本 (src/components/ProgressBar.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):

export const useToast = () => {
  const [toasts, setToasts] = React.useState<Toast[]>([]);
  // ...
};

Vue 版本 (src/components/Toast.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):

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 版本:

export const MainLayout: React.FC<MainLayoutProps> = ({
  currentView,
  setCurrentView,
  // ...
}) => (
  <div className="flex h-screen w-full bg-slate-900">
    {/* ... */}
  </div>
);

Vue 版本 (src/layouts/MainLayout.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 版本关键代码:

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):

<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 转换:

// React
const sortedData = useMemo(() => {
  // 排序逻辑
}, [sortField, sortDirection]);

// Vue
const sortedData = computed(() => {
  if (!sortField.value || !sortDirection.value) return inventoryData;
  // 排序逻辑
});

useEffect 转换:

// 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):

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):

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):

function App() {
  const [currentView, setCurrentView] = useState<ViewMode>('projects');
  // ...
  return (
    <ToastProvider>
      <MainLayout ... />
    </ToastProvider>
  );
}

Vue 版本 (src/App.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 全局替换图标导入

查找替换规则:

// 从
import { IconName } from 'lucide-react';
<IconName size={20} />

// 到
import { IconName } from 'lucide-vue-next';
<IconName :size="20" />

批量替换脚本 (scripts/replace-icons.sh):

#!/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:

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: {
    // 自定义规则
  },
};

🚀 迁移执行步骤

第一步:备份当前代码

git checkout -b vue-migration
git commit -am "Backup before Vue migration"

第二步:安装新依赖

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 并正确导入:

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
  • 页面切换流畅
  • 无内存泄漏

📚 参考资源


📅 时间估算

阶段 工作量 说明
阶段一:项目配置 1-2 天 配置文件更新
阶段二:类型定义 1 天 类型文件检查
阶段三:工具组件 2-3 天 简单组件迁移
阶段四:布局组件 2-3 天 布局组件迁移
阶段五:页面组件 5-7 天 复杂页面迁移
阶段六:应用入口 1 天 主文件迁移
阶段七:图标库 1 天 图标替换
阶段八:测试验证 3-5 天 功能测试和修复
总计 16-25 天 约 3-5 周

📝 更新记录

  • 2025-01-XX: 初始迁移方案创建

👥 贡献者

  • Finyx AI Team