diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..2617cbb --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,19 @@ +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: { + 'vue/multi-word-component-names': 'off', + }, +} diff --git a/docs/Vue技术栈转换风险评估.md b/docs/Vue技术栈转换风险评估.md new file mode 100644 index 0000000..680a7c5 --- /dev/null +++ b/docs/Vue技术栈转换风险评估.md @@ -0,0 +1,314 @@ +# Vue 技术栈转换风险评估报告 + +## 📋 项目现状分析 + +### 当前技术栈 +- **前端框架**: React 18.2.0 +- **开发语言**: TypeScript 5.2.2 +- **构建工具**: Vite 5.0.8 +- **样式方案**: Tailwind CSS 3.3.6 +- **图标库**: Lucide React 0.344.0 +- **图表库**: Recharts 2.10.3(已安装但可能未使用) + +### 项目规模 +- **组件文件**: 18 个 `.tsx` 文件 +- **核心页面**: 3 个主要视图(Dashboard、Projects、Engagement) +- **工作流步骤**: 5 个步骤组件(Setup、Inventory、Context、Value、Delivery) +- **状态管理**: 使用 React Context API + Hooks +- **路由**: 未使用路由库(当前为组件级切换) + +--- + +## ⚠️ 转换风险分析 + +### 🔴 高风险项 + +#### 1. **React Hooks 迁移复杂度** +**风险等级**: 🔴 高 + +**问题描述**: +- 项目大量使用 React Hooks(`useState`, `useEffect`, `useMemo`, `useContext`) +- 在 8 个文件中发现 42+ 处 Hooks 使用 +- Vue 3 的 Composition API 虽然类似,但语法和概念有差异 + +**具体影响**: +```typescript +// React 写法 +const [state, setState] = useState(initial); +useEffect(() => { ... }, [deps]); +const memoized = useMemo(() => { ... }, [deps]); + +// Vue 3 需要改写为 +const state = ref(initial); +watchEffect(() => { ... }); +const memoized = computed(() => { ... }); +``` + +**迁移工作量**: +- 需要重写所有组件逻辑 +- 估计工作量: **15-20 个工作日** + +--- + +#### 2. **React Context API 迁移** +**风险等级**: 🔴 高 + +**问题描述**: +- 项目使用 `ToastContext` 进行全局状态管理 +- 使用 `createContext` + `useContext` 模式 +- Vue 3 需要使用 `provide/inject` 或状态管理库 + +**具体影响**: +```typescript +// React Context +const ToastContext = createContext(...); +export const useToast = () => useContext(ToastContext); + +// Vue 3 需要改为 +const toastKey = Symbol('toast'); +provide(toastKey, toastState); +const toast = inject(toastKey); +``` + +**迁移工作量**: +- 需要重构所有使用 Context 的组件 +- 估计工作量: **3-5 个工作日** + +--- + +#### 3. **组件生命周期差异** +**风险等级**: 🟡 中高 + +**问题描述**: +- React 使用 `useEffect` 处理副作用 +- Vue 3 使用 `onMounted`, `onUpdated`, `watch` 等 +- 需要仔细分析每个 `useEffect` 的依赖和用途 + +**迁移工作量**: +- 需要逐个分析并转换 +- 估计工作量: **5-8 个工作日** + +--- + +### 🟡 中风险项 + +#### 4. **图标库迁移** +**风险等级**: 🟡 中 + +**问题描述**: +- 当前使用 `lucide-react`(React 专用) +- Vue 需要使用 `lucide-vue-next` 或 `@lucide/vue` +- 图标组件导入方式不同 + +**具体影响**: +```typescript +// React +import { FileJson, Terminal } from 'lucide-react'; + + +// Vue 3 +import { FileJson, Terminal } from 'lucide-vue-next'; + +``` + +**迁移工作量**: +- 需要替换所有图标导入和使用 +- 估计工作量: **2-3 个工作日** + +--- + +#### 5. **TypeScript 类型系统适配** +**风险等级**: 🟡 中 + +**问题描述**: +- Vue 3 + TypeScript 的类型定义方式与 React 不同 +- 组件 Props 定义方式不同 +- 需要调整类型声明 + +**具体影响**: +```typescript +// React +interface Props { ... } +const Component: React.FC = ({ prop }) => { ... } + +// Vue 3 +interface Props { ... } +defineProps() +``` + +**迁移工作量**: +- 需要调整所有组件的类型定义 +- 估计工作量: **3-5 个工作日** + +--- + +#### 6. **事件处理方式差异** +**风险等级**: 🟡 中 + +**问题描述**: +- React 使用 `onClick`, `onChange` 等驼峰命名 +- Vue 使用 `@click`, `@change` 等指令 +- 事件对象处理方式不同 + +**迁移工作量**: +- 需要替换所有事件绑定 +- 估计工作量: **2-3 个工作日** + +--- + +### 🟢 低风险项 + +#### 7. **样式方案(Tailwind CSS)** +**风险等级**: 🟢 低 + +**优势**: +- Tailwind CSS 是框架无关的 +- 可以直接复用现有样式类 +- 无需修改 + +**迁移工作量**: **0 个工作日** + +--- + +#### 8. **构建工具(Vite)** +**风险等级**: 🟢 低 + +**优势**: +- Vite 同时支持 React 和 Vue +- 只需更换插件:`@vitejs/plugin-react` → `@vitejs/plugin-vue` +- 配置调整最小 + +**迁移工作量**: **0.5 个工作日** + +--- + +#### 9. **图表库(Recharts)** +**风险等级**: 🟡 中低 + +**问题描述**: +- Recharts 是 React 专用库 +- Vue 需要使用 `vue-chartjs` 或 `echarts-for-vue` +- 如果当前未使用,影响较小 + +**迁移工作量**: +- 如果已使用: **2-3 个工作日** +- 如果未使用: **0 个工作日** + +--- + +## 📊 总体风险评估 + +### 风险矩阵 + +| 风险项 | 风险等级 | 影响范围 | 工作量(工作日) | +|--------|---------|---------|----------------| +| React Hooks 迁移 | 🔴 高 | 所有组件 | 15-20 | +| Context API 迁移 | 🔴 高 | 全局状态 | 3-5 | +| 生命周期转换 | 🟡 中高 | 所有组件 | 5-8 | +| 图标库迁移 | 🟡 中 | 所有组件 | 2-3 | +| TypeScript 适配 | 🟡 中 | 所有组件 | 3-5 | +| 事件处理转换 | 🟡 中 | 所有组件 | 2-3 | +| 图表库迁移 | 🟡 中低 | 部分页面 | 2-3 | +| 样式方案 | 🟢 低 | 无 | 0 | +| 构建工具 | 🟢 低 | 配置 | 0.5 | + +### 总工作量估算 + +**保守估计**: **32-45 个工作日**(约 6-9 周) +**乐观估计**: **25-35 个工作日**(约 5-7 周) + +--- + +## 🎯 风险缓解建议 + +### 1. **分阶段迁移策略** +- **阶段一**: 先迁移简单组件(如 ProgressBar、SidebarItem) +- **阶段二**: 迁移页面组件(Dashboard、Projects) +- **阶段三**: 迁移复杂工作流组件(Engagement 相关) +- **阶段四**: 迁移全局状态和 Context + +### 2. **并行开发方案** +- 保持 React 版本继续维护 +- 新建 Vue 分支并行开发 +- 逐步验证功能对等性 + +### 3. **技术选型建议** +- **Vue 版本**: Vue 3.3+(Composition API) +- **状态管理**: Pinia(推荐)或 Vuex +- **图标库**: `lucide-vue-next` +- **图表库**: `echarts-for-vue` 或 `vue-chartjs` +- **路由**: Vue Router 4(如果需要) + +### 4. **测试策略** +- 建立功能对比清单 +- 逐项验证功能对等性 +- 进行回归测试 + +--- + +## ⚖️ 转换收益分析 + +### ✅ 潜在收益 +1. **团队技术栈统一**(如果团队更熟悉 Vue) +2. **Vue 3 性能优势**(在某些场景下) +3. **更好的模板语法**(对某些开发者更友好) + +### ❌ 潜在成本 +1. **开发时间成本**: 6-9 周 +2. **测试成本**: 需要全面回归测试 +3. **学习成本**: 团队需要熟悉 Vue 生态 +4. **维护成本**: 短期内需要维护两套代码 + +--- + +## 🎯 最终建议 + +### 建议转换的情况 +✅ **推荐转换**,如果: +- 团队对 Vue 更熟悉,且长期使用 Vue +- 有充足的时间和资源(6-9 周) +- 项目处于早期阶段,重构成本较低 +- 有明确的业务需求(如与 Vue 项目集成) + +### 不建议转换的情况 +❌ **不推荐转换**,如果: +- 项目已进入稳定维护期 +- 时间紧迫,需要快速迭代 +- 团队对 React 更熟悉 +- 没有明确的业务驱动因素 + +### 折中方案 +🔄 **渐进式迁移**: +- 新功能使用 Vue 开发 +- 旧功能保持 React +- 通过微前端架构(如 qiankun)集成 +- 逐步迁移,降低风险 + +--- + +## 📝 结论 + +**总体风险等级**: 🟡 **中等偏高** + +**主要风险点**: +1. React Hooks 到 Vue Composition API 的迁移复杂度高 +2. 需要重写所有组件逻辑 +3. 工作量较大(6-9 周) + +**建议**: +- 如果必须转换,建议采用**分阶段迁移**策略 +- 充分评估团队能力和时间资源 +- 建立详细的功能对比清单 +- 考虑是否真的需要转换(评估收益与成本) + +--- + +## 📅 更新记录 + +- **2025-01-XX**: 初始风险评估报告 + +--- + +## 👥 评估者 + +- Finyx AI Team diff --git a/docs/Vue迁移完整方案.md b/docs/Vue迁移完整方案.md new file mode 100644 index 0000000..b3edd7c --- /dev/null +++ b/docs/Vue迁移完整方案.md @@ -0,0 +1,1018 @@ +# 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 +/// + +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 = ({ percent, status }) => { + // ... +}; +``` + +**Vue 版本** (`src/components/ProgressBar.vue`): +```vue + + + +``` + +#### 3.2 Toast 组件迁移 + +**React 版本** (`src/components/Toast.tsx`): +```typescript +export const useToast = () => { + const [toasts, setToasts] = React.useState([]); + // ... +}; +``` + +**Vue 版本** (`src/components/Toast.vue`): +```vue + + + +``` + +**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([]); + + 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 = ({ + currentView, + setCurrentView, + // ... +}) => ( +
+ {/* ... */} +
+); +``` + +**Vue 版本** (`src/layouts/MainLayout.vue`): +```vue + + + +``` + +### 阶段五:页面组件迁移(5-7 天) + +#### 5.1 SetupStep 组件示例 + +**React 版本关键代码**: +```typescript +export const SetupStep: React.FC = ({ setCurrentStep }) => { + const toast = useToast(); + const [projectName, setProjectName] = useState(''); + const [selectedIndustries, setSelectedIndustries] = useState([]); + const [errors, setErrors] = useState({}); + + useEffect(() => { + // 验证逻辑 + }, [projectName, selectedIndustries, touched]); + + return ( +
+ setProjectName(e.target.value)} + /> +
+ ); +}; +``` + +**Vue 版本** (`src/pages/engagement/SetupStep.vue`): +```vue + + + +``` + +#### 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( + + + , +) +``` + +**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('projects'); + // ... + return ( + + + + ); +} +``` + +**Vue 版本** (`src/App.vue`): +```vue + + + +``` + +### 阶段七:图标库迁移(1 天) + +#### 7.1 全局替换图标导入 + +**查找替换规则**: +```typescript +// 从 +import { IconName } from 'lucide-react'; + + +// 到 +import { IconName } from 'lucide-vue-next'; + +``` + +**批量替换脚本** (`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 && }` | `` | +| `{condition ? : }` | `` | +| `{items.map(item => )}` | `` | + +### Props 传递转换 + +| React | Vue 3 | +|-------|-------| +| `` | `` | +| `` | `` | +| `{...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 + +``` + +### 问题 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 diff --git a/docs/代码转换快速参考.md b/docs/代码转换快速参考.md new file mode 100644 index 0000000..6bcf164 --- /dev/null +++ b/docs/代码转换快速参考.md @@ -0,0 +1,564 @@ +# React 到 Vue 代码转换快速参考 + +## 🔄 常用转换模式 + +### 1. 组件定义 + +#### React +```typescript +import React from 'react'; + +interface Props { + title: string; + count: number; +} + +export const Component: React.FC = ({ title, count }) => { + return
{title}: {count}
; +}; +``` + +#### Vue 3 +```vue + + + +``` + +--- + +### 2. 状态管理 + +#### React useState +```typescript +const [count, setCount] = useState(0); +const [user, setUser] = useState({ name: '', age: 0 }); + +setCount(5); +setUser({ ...user, name: 'John' }); +``` + +#### Vue ref +```typescript +const count = ref(0); +const user = ref({ name: '', age: 0 }); + +count.value = 5; +user.value.name = 'John'; +``` + +#### Vue reactive (对象) +```typescript +const user = reactive({ name: '', age: 0 }); + +user.name = 'John'; // 不需要 .value +``` + +--- + +### 3. 副作用处理 + +#### React useEffect +```typescript +useEffect(() => { + // 副作用逻辑 + return () => { + // 清理逻辑 + }; +}, [dependency]); +``` + +#### Vue watchEffect +```typescript +watchEffect(() => { + // 副作用逻辑 + return () => { + // 清理逻辑 + }; +}); +``` + +#### Vue watch +```typescript +watch(() => dependency, (newVal, oldVal) => { + // 副作用逻辑 +}, { immediate: true }); +``` + +--- + +### 4. 计算属性 + +#### React useMemo +```typescript +const doubled = useMemo(() => { + return count * 2; +}, [count]); +``` + +#### Vue computed +```typescript +const doubled = computed(() => { + return count.value * 2; +}); +``` + +--- + +### 5. 条件渲染 + +#### React +```tsx +{isVisible && } +{condition ?
: } +{items.length > 0 && items.map(item => )} +``` + +#### Vue +```vue + + + + +``` + +--- + +### 6. 事件处理 + +#### React +```tsx + + setValue(e.target.value)} /> +
+``` + +#### Vue +```vue + + + +``` + +--- + +### 7. 双向绑定 + +#### React +```tsx + setValue(e.target.value)} +/> +``` + +#### Vue +```vue + +``` + +--- + +### 8. 样式绑定 + +#### React +```tsx +
+
+
+``` + +#### Vue +```vue +
+
+
+``` + +--- + +### 9. 列表渲染 + +#### React +```tsx +{items.map(item => ( + +))} +``` + +#### Vue +```vue + +``` + +--- + +### 10. 组件通信 + +#### React Props +```tsx + +``` + +#### Vue Props & Emits +```vue + +``` + +--- + +### 11. Context API + +#### React Context +```typescript +const Context = createContext(); +const value = useContext(Context); +``` + +#### Vue provide/inject +```typescript +// 提供 +provide(key, value); + +// 注入 +const value = inject(key); +``` + +#### Pinia Store (推荐) +```typescript +// store +export const useStore = defineStore('store', () => { + const state = ref(0); + return { state }; +}); + +// 使用 +const store = useStore(); +store.state; +``` + +--- + +### 12. 生命周期 + +#### React +```typescript +useEffect(() => { + // componentDidMount + return () => { + // componentWillUnmount + }; +}, []); + +useEffect(() => { + // componentDidUpdate +}, [dependency]); +``` + +#### Vue +```typescript +onMounted(() => { + // 组件挂载后 +}); + +onUnmounted(() => { + // 组件卸载前 +}); + +onUpdated(() => { + // 组件更新后 +}); +``` + +--- + +### 13. 表单处理 + +#### React +```tsx +const [form, setForm] = useState({ name: '', email: '' }); + + setForm({ ...form, name: e.target.value })} +/> +``` + +#### Vue +```vue + + + +``` + +--- + +### 14. 条件类名 + +#### React +```tsx +
+``` + +#### Vue +```vue +
+``` + +--- + +### 15. 动态属性 + +#### React +```tsx +
+{altText} +``` + +#### Vue +```vue +
+ +``` + +--- + +### 16. 插槽 (Slots) + +#### React +```tsx +function Layout({ children }) { + return
{children}
; +} +``` + +#### Vue +```vue + +``` + +#### 具名插槽 +```vue + + + + + + + + + +``` + +--- + +### 17. 图标使用 + +#### React (lucide-react) +```tsx +import { FileJson, Terminal } from 'lucide-react'; + + + +``` + +#### Vue (lucide-vue-next) +```vue + + + +``` + +--- + +### 18. 异步数据获取 + +#### React +```typescript +const [data, setData] = useState(null); +const [loading, setLoading] = useState(false); + +useEffect(() => { + setLoading(true); + fetchData().then(result => { + setData(result); + setLoading(false); + }); +}, []); +``` + +#### Vue +```typescript +const data = ref(null); +const loading = ref(false); + +onMounted(async () => { + loading.value = true; + data.value = await fetchData(); + loading.value = false; +}); +``` + +--- + +### 19. 防抖/节流 + +#### React +```typescript +const debouncedSearch = useMemo( + () => debounce((value) => { + // 搜索逻辑 + }, 300), + [] +); +``` + +#### Vue (使用 @vueuse/core) +```typescript +import { useDebounceFn } from '@vueuse/core'; + +const debouncedSearch = useDebounceFn((value) => { + // 搜索逻辑 +}, 300); +``` + +--- + +### 20. 组件引用 + +#### React useRef +```tsx +const inputRef = useRef(null); + + +inputRef.current?.focus(); +``` + +#### Vue ref +```vue + + + +``` + +--- + +## 📝 常见陷阱 + +### 1. ref 值访问 +```typescript +// ❌ 错误 +const count = ref(0); +console.log(count); // RefImpl 对象 + +// ✅ 正确 +const count = ref(0); +console.log(count.value); // 0 +``` + +### 2. 响应式对象 +```typescript +// ❌ 错误 - 解构会失去响应性 +const { name } = user; + +// ✅ 正确 - 使用 toRefs +const { name } = toRefs(user); +``` + +### 3. 数组更新 +```typescript +// React +setItems([...items, newItem]); + +// Vue +items.value.push(newItem); +// 或 +items.value = [...items.value, newItem]; +``` + +### 4. 对象更新 +```typescript +// React +setUser({ ...user, name: 'John' }); + +// Vue (ref) +user.value = { ...user.value, name: 'John' }; + +// Vue (reactive) +user.name = 'John'; // 直接修改 +``` + +--- + +## 🎯 最佳实践 + +1. **优先使用 `ref` 处理基本类型,`reactive` 处理对象** +2. **使用 `computed` 而不是 `watch` 处理派生状态** +3. **使用 `watchEffect` 处理副作用,`watch` 处理特定依赖** +4. **使用 Pinia 管理全局状态,而不是 provide/inject** +5. **使用 ` + diff --git a/package-lock.json b/package-lock.json index b3d56c6..ea97509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,25 +8,26 @@ "name": "finyx-frontend", "version": "0.0.0", "dependencies": { - "lucide-react": "^0.344.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "recharts": "^2.10.3" + "@vueuse/core": "^10.7.0", + "lucide-vue-next": "^0.344.0", + "pinia": "^2.1.0", + "recharts": "^2.10.3", + "vue": "^3.4.0", + "vue-router": "^4.2.0" }, "devDependencies": { - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", - "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-vue": "^5.0.0", + "@vue/eslint-config-typescript": "^12.0.0", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", + "eslint-plugin-vue": "^9.20.0", "postcss": "^8.4.32", "tailwindcss": "^3.3.6", "typescript": "^5.2.2", - "vite": "^5.0.8" + "vite": "^5.0.8", + "vue-tsc": "^1.8.25" } }, "node_modules/@alloc/quick-lru": { @@ -42,173 +43,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -218,41 +56,15 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -264,38 +76,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -305,45 +85,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -904,17 +649,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -929,7 +663,6 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -981,13 +714,6 @@ "node": ">= 8" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -1338,51 +1064,6 @@ "win32" ] }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -1460,34 +1141,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -1495,6 +1148,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -1700,25 +1359,242 @@ "dev": true, "license": "ISC" }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^14.17.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "vue": "3.5.26" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, "node_modules/acorn": { @@ -1899,6 +1775,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2091,6 +1974,13 @@ "node": ">= 6" } }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2098,13 +1988,6 @@ "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2260,6 +2143,13 @@ "node": ">=12" } }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2348,6 +2238,18 @@ "dev": true, "license": "ISC" }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2467,27 +2369,27 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", "dev": true, "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, "engines": { - "node": ">=10" + "node": "^14.17.0 || >=16.0.0" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-scope": { @@ -2598,6 +2500,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2795,16 +2703,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2931,6 +2829,16 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3104,19 +3012,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3138,19 +3033,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3236,23 +3118,22 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { + "node_modules/lucide-vue-next": { "version": "0.344.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", - "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.344.0.tgz", + "integrity": "sha512-YvexWTTzcRScmOKUvbQvexDmIK63x6NT8lV3Cot9TShQyy3PpFxfx5i2wZNhaZfV8bKqTMRXgCs372rnrtPYKQ==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + "vue": ">=3.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/merge2": { @@ -3302,6 +3183,13 @@ "dev": true, "license": "MIT" }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -3318,7 +3206,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -3357,6 +3244,19 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3449,6 +3349,13 @@ "node": ">=6" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3500,7 +3407,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3526,6 +3432,28 @@ "node": ">=0.10.0" } }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -3540,7 +3468,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3762,6 +3689,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3774,6 +3702,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3788,16 +3717,6 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-smooth": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", @@ -4017,6 +3936,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -4071,7 +3991,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4337,7 +4256,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -4477,6 +4396,122 @@ } } }, + "node_modules/vue": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4510,12 +4545,15 @@ "dev": true, "license": "ISC" }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } }, "node_modules/yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 23fc222..2903e3e 100644 --- a/package.json +++ b/package.json @@ -5,29 +5,31 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "build": "vite build", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + "type-check": "vue-tsc --noEmit" }, "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "lucide-react": "^0.344.0", + "vue": "^3.4.0", + "pinia": "^2.1.0", + "vue-router": "^4.2.0", + "lucide-vue-next": "^0.344.0", + "@vueuse/core": "^10.7.0", "recharts": "^2.10.3" }, "devDependencies": { - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", + "@vitejs/plugin-vue": "^5.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "eslint-plugin-vue": "^9.20.0", "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", - "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.16", "eslint": "^8.55.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.32", "tailwindcss": "^3.3.6", "typescript": "^5.2.2", - "vite": "^5.0.8" + "vite": "^5.0.8", + "vue-tsc": "^1.8.25" } } diff --git a/src/App.tsx b/src/App.tsx index e59c22f..ca84997 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { ViewMode, Step } from './types'; import { MainLayout } from './layouts/MainLayout'; +import { ToastProvider } from './contexts/ToastContext'; function App() { const [currentView, setCurrentView] = useState('projects'); @@ -8,14 +9,16 @@ function App() { const [isPresentationMode, setIsPresentationMode] = useState(false); return ( - + + + ); } diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..86fd50a --- /dev/null +++ b/src/App.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/ProgressBar.vue b/src/components/ProgressBar.vue new file mode 100644 index 0000000..15fb114 --- /dev/null +++ b/src/components/ProgressBar.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/SidebarItem.vue b/src/components/SidebarItem.vue new file mode 100644 index 0000000..248309f --- /dev/null +++ b/src/components/SidebarItem.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/TableCheckItem.vue b/src/components/TableCheckItem.vue new file mode 100644 index 0000000..df7164e --- /dev/null +++ b/src/components/TableCheckItem.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..98bbd1a --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,116 @@ +import React, { useEffect } from 'react'; +import { CheckCircle2, XCircle, AlertTriangle, Info, X } from 'lucide-react'; + +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface Toast { + id: string; + message: string; + type: ToastType; + duration?: number; +} + +interface ToastProps { + toast: Toast; + onClose: (id: string) => void; +} + +const ToastComponent: React.FC = ({ toast, onClose }) => { + useEffect(() => { + const timer = setTimeout(() => { + onClose(toast.id); + }, toast.duration || 3000); + + return () => clearTimeout(timer); + }, [toast.id, toast.duration, onClose]); + + const icons = { + success: CheckCircle2, + error: XCircle, + warning: AlertTriangle, + info: Info, + }; + + const colors = { + success: 'bg-green-50 border-green-200 text-green-800', + error: 'bg-red-50 border-red-200 text-red-800', + warning: 'bg-amber-50 border-amber-200 text-amber-800', + info: 'bg-blue-50 border-blue-200 text-blue-800', + }; + + const iconColors = { + success: 'text-green-600', + error: 'text-red-600', + warning: 'text-amber-600', + info: 'text-blue-600', + }; + + const Icon = icons[toast.type]; + + return ( +
+ +

{toast.message}

+ +
+ ); +}; + +interface ToastContainerProps { + toasts: Toast[]; + onClose: (id: string) => void; +} + +export const ToastContainer: React.FC = ({ toasts, onClose }) => { + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map((toast) => ( +
+ +
+ ))} +
+ ); +}; + +// Hook for managing toasts +export const useToast = () => { + const [toasts, setToasts] = React.useState([]); + + const showToast = (message: string, type: ToastType = 'info', duration?: number) => { + const id = Math.random().toString(36).substring(7); + const newToast: Toast = { id, message, type, duration }; + setToasts((prev) => [...prev, newToast]); + return id; + }; + + const closeToast = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + 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, + }; +}; diff --git a/src/components/ToastContainer.vue b/src/components/ToastContainer.vue new file mode 100644 index 0000000..44c985c --- /dev/null +++ b/src/components/ToastContainer.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/components/ToastItem.vue b/src/components/ToastItem.vue new file mode 100644 index 0000000..2f8ed19 --- /dev/null +++ b/src/components/ToastItem.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/contexts/ToastContext.tsx b/src/contexts/ToastContext.tsx new file mode 100644 index 0000000..ef8a114 --- /dev/null +++ b/src/contexts/ToastContext.tsx @@ -0,0 +1,30 @@ +import React, { createContext, useContext, useState, ReactNode } from 'react'; +import { Toast, ToastContainer, useToast as useToastHook } from '../components/Toast'; + +interface ToastContextType { + success: (message: string, duration?: number) => void; + error: (message: string, duration?: number) => void; + warning: (message: string, duration?: number) => void; + info: (message: string, duration?: number) => void; +} + +const ToastContext = createContext(undefined); + +export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const { toasts, closeToast, success, error, warning, info } = useToastHook(); + + return ( + + {children} + + + ); +}; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within ToastProvider'); + } + return context; +}; diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..323c78a --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/src/finyx_designV2.tsx b/src/finyx_designV2.tsx index bf74432..29e3e5d 100644 --- a/src/finyx_designV2.tsx +++ b/src/finyx_designV2.tsx @@ -228,6 +228,7 @@ export default function FinyxAI() { // Project setup form state const [projectName, setProjectName] = useState(''); const [companyDescription, setCompanyDescription] = useState(''); + const [owner, setOwner] = useState(''); const [selectedIndustries, setSelectedIndustries] = useState([]); // 模拟盘点处理动画 @@ -243,12 +244,12 @@ export default function FinyxAI() { } }, [inventoryMode]); - // --- 视图 1: 指挥中心 (Dashboard) --- + // --- 视图 1: 工作台 (Dashboard) --- const DashboardView = () => (
-

指挥中心 (Command Center)

+

工作台

下午好, Sarah (合伙人) | 全局概览

@@ -258,30 +259,8 @@ export default function FinyxAI() {
-
- {[ - { title: '进行中项目', value: '12', sub: '+2 本周新增', icon: Briefcase, color: 'text-blue-600' }, - { title: '本周待交付', value: '3', sub: '需重点关注', icon: Clock, color: 'text-amber-600' }, - { title: '高风险合规预警', value: '2', sub: '阻断级风险', icon: AlertTriangle, color: 'text-red-600' }, - { title: '待复核节点', value: '5', sub: '专家审核队列', icon: FileText, color: 'text-purple-600' }, - ].map((kpi, idx) => ( -
-
-
-

{kpi.title}

-

{kpi.value}

-

{kpi.sub}

-
-
- -
-
-
- ))} -
-
-
+

项目作业全景

- -
-
- -

风险雷达 (Risk Radar)

-
-
- {riskData.map((risk) => ( -
-
-
-

{risk.risk}

-

项目: {risk.project}

- -
-
- ))} -
-
); @@ -586,6 +544,20 @@ export default function FinyxAI() { />
+ {/* Owner */} +
+ + setOwner(e.target.value)} + placeholder="请输入项目负责人" + className="w-full p-3 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none" + /> +
+ {/* Company Description */}