完整迁移调整 vue 并且融合原代码结构。

This commit is contained in:
李季 2026-01-08 14:34:06 +08:00
parent 06bf9026f7
commit 6ec4ff9666
75 changed files with 2134 additions and 3822 deletions

305
.cursorrules Normal file
View File

@ -0,0 +1,305 @@
# Finyx AI 前端开发 Cursor Rules
## 项目概述
这是一个基于 Vue 3 + TypeScript + Tailwind CSS 的企业级数据资产管理平台前端项目。项目需要与旧系统Vue 3 + Element Plus + SCSS融合保持设计风格一致性。
## 技术栈约束
### 必须使用
- Vue 3.4.0+ (Composition API)
- TypeScript 5.2.2+
- Tailwind CSS 3.3.6+
- Pinia 2.1.0+ (状态管理)
- Vue Router 4.2.0+ (路由)
- Lucide Vue Next 0.344.0+ (图标)
### 禁止使用
- ❌ React 及其相关库
- ❌ Element Plus (新功能中)
- ❌ SCSS/SASS (统一使用 Tailwind CSS)
- ❌ 内联样式 (除非动态样式)
- ❌ `any` 类型 (除非特殊情况)
- ❌ `@ts-ignore` (除非有充分理由)
## 色彩系统规范
### 必须使用 Tailwind 配置的颜色类
所有颜色必须使用 `tailwind.config.js` 中定义的颜色变量:
```typescript
// ✅ 正确
<div className="bg-app-primary text-app-white">按钮</div>
<div className="bg-app-bg text-app-text">内容</div>
<div className="border-app-border">边框</div>
// ❌ 错误 - 禁止硬编码颜色
<div className="bg-[#3067EF]">按钮</div>
<div style={{ backgroundColor: '#3067EF' }}>按钮</div>
```
### 颜色变量映射
- `app-primary`: #3067EF (主色)
- `app-bg`: #F4F8FF (背景色)
- `app-text`: #1f2329 (主文本)
- `app-text-secondary`: #646a73 (次文本)
- `app-text-disable`: #bbbfc4 (禁用文本)
- `app-border`: #dee0e3 (边框)
- `app-white`: #ffffff (白色)
- `app-sidebar-bg`: #ffffff (侧边栏背景)
- `app-hover`: rgba(48, 103, 239, 0.06) (悬停背景)
- `app-active`: #ECF2FF (激活背景)
## 组件开发规范
### Vue 组件结构
```vue
<template>
<!-- 模板内容 -->
</template>
<script setup lang="ts">
// 1. Vue 核心导入
import { ref, computed, onMounted } from 'vue'
// 2. 第三方库导入
import { useRouter } from 'vue-router'
// 3. 项目内部导入
import { SidebarItem } from '@/components/SidebarItem'
import type { Project } from '@/types'
// 4. Props 定义
interface Props {
title: string
items: Project[]
count?: number
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// 5. Emits 定义
interface Emits {
(e: 'update', value: string): void
}
const emit = defineEmits<Emits>()
// 6. 响应式数据
const count = ref<number>(0)
const projects = ref<Project[]>([])
// 7. 计算属性
const total = computed(() => projects.value.length)
// 8. 生命周期
onMounted(() => {
// 初始化逻辑
})
// 9. 方法
const handleClick = () => {
// 处理逻辑
}
</script>
<style scoped>
/* 仅在必要时使用自定义样式 */
</style>
```
### TypeScript 类型规范
- 所有 Props 必须定义 interface
- 所有响应式数据必须指定类型
- 禁止使用 `any`,使用 `unknown` 或具体类型
- 使用 `type` 定义联合类型,`interface` 定义对象类型
```typescript
// ✅ 正确
interface Project {
id: string
name: string
progress: number
}
type Step = 'setup' | 'inventory' | 'context' | 'value' | 'delivery'
const count = ref<number>(0)
const projects = ref<Project[]>([])
// ❌ 错误
const count = ref(0) // 缺少类型
const data: any = {} // 使用 any
```
## 样式规范
### Tailwind CSS 使用
- 优先使用 Tailwind 工具类
- 使用配置的颜色变量app-*
- 响应式设计使用 Tailwind 断点sm, md, lg, xl
- 避免自定义 CSS除非绝对必要
```vue
<template>
<!-- ✅ 正确 -->
<div class="bg-app-white border border-app-border rounded-lg p-6 shadow-app">
<h2 class="text-xl font-bold text-app-text mb-4">标题</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 内容 -->
</div>
</div>
<!-- ❌ 错误 -->
<div style="background: white; padding: 24px;">
内容
</div>
</template>
```
### 字体和间距
- 基础字号: 14px (`text-base`)
- 基础间距单位: 8px
- 页面内边距: 24px (`p-6`)
- 卡片内边距: 16px (`p-4`) 或 24px (`p-6`)
## 文件命名规范
### 组件文件
- Vue 组件: PascalCase如 `SidebarItem.vue`
- TypeScript 文件: camelCase如 `mockData.ts`
- 工具函数: camelCase如 `formatDate.ts`
### 组件命名
- 组件名必须与文件名保持一致
- 使用 PascalCase
## 代码质量要求
### 必须遵循
1. 所有组件必须使用 TypeScript
2. 所有 Props 必须定义类型
3. 所有事件必须使用 `defineEmits` 定义
4. 使用 Composition API禁止 Options API
5. 使用 `ref` 和 `computed` 管理状态
6. 异步操作必须使用 try-catch
7. 错误信息使用 Toast 显示
### 性能优化
- 使用 `computed` 而非 `watch`(如可能)
- 使用 `v-show` 而非 `v-if`(频繁切换)
- 大列表考虑虚拟滚动
- 图片使用懒加载
### 可访问性
- 使用语义化 HTML 标签
- 为交互元素添加 `aria-label`
- 确保键盘导航可用
- 确保颜色对比度符合标准
## 与旧系统融合注意事项
### 色彩一致性
- 必须使用旧系统的色彩变量
- 禁止使用新的颜色值(除非经过批准)
### 组件风格
- 侧边栏: 白色背景 (`bg-app-sidebar-bg`)
- 卡片: 白色背景 (`bg-app-white`),浅色边框 (`border-app-border`)
- 按钮: 主色背景 (`bg-app-primary`)
- 激活状态: 浅蓝色背景 (`bg-app-active`)
### 技术栈兼容
- 新功能使用 Vue 3 + TypeScript + Tailwind CSS
- 旧功能保持 Vue 3 + Element Plus + SCSS
- 新功能作为独立模块,通过路由集成
## 代码生成规则
### 生成新组件时
1. 使用 Vue 3 Composition API
2. 完整的 TypeScript 类型定义
3. 使用 Tailwind CSS 样式
4. 使用配置的颜色变量
5. 添加必要的注释
6. 遵循项目目录结构
### 修改现有代码时
1. 保持现有代码风格
2. 不破坏现有功能
3. 更新相关类型定义
4. 确保样式一致性
## 常见错误避免
### ❌ 禁止
```typescript
// 禁止使用 any
const data: any = {}
// 禁止硬编码颜色
<div className="bg-[#3067EF]">按钮</div>
// 禁止使用内联样式
<div style={{ backgroundColor: 'white' }}>内容</div>
// 禁止使用 React
import React from 'react'
// 禁止使用 Element Plus (新功能)
import { ElButton } from 'element-plus'
```
### ✅ 正确
```typescript
// 使用具体类型
const data: Project = {}
// 使用 Tailwind 配置的颜色
<div className="bg-app-primary">按钮</div>
// 使用 Tailwind 类
<div className="bg-app-white">内容</div>
// 使用 Vue
import { ref } from 'vue'
// 使用 Tailwind 构建组件
<button className="bg-app-primary text-app-white px-4 py-2 rounded-lg">
按钮
</button>
```
## 代码审查重点
在生成或修改代码时,确保:
1. ✅ 使用正确的颜色变量app-*
2. ✅ TypeScript 类型完整
3. ✅ 使用 Tailwind CSS 而非内联样式
4. ✅ 遵循 Vue 3 Composition API
5. ✅ 组件结构清晰
6. ✅ 无 ESLint 错误
7. ✅ 响应式设计正确
8. ✅ 与旧系统风格一致
## 项目结构参考
```
src/
├── components/ # 通用可复用组件
├── layouts/ # 布局组件
├── pages/ # 页面组件
├── stores/ # Pinia 状态管理
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
└── api/ # API 接口定义
```
## 重要提醒
1. **色彩系统**: 始终使用 `tailwind.config.js` 中定义的颜色变量
2. **技术栈**: 新功能必须使用 Vue 3 + TypeScript + Tailwind CSS
3. **类型安全**: 所有代码必须使用 TypeScript避免 `any`
4. **样式一致性**: 与旧系统保持设计风格一致
5. **代码质量**: 遵循最佳实践,确保可维护性

View File

@ -13,6 +13,11 @@ module.exports = {
parserOptions: { parserOptions: {
ecmaVersion: 2021, ecmaVersion: 2021,
}, },
ignorePatterns: [
'旧代码/**',
'dist/**',
'node_modules/**',
],
rules: { rules: {
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
}, },

528
docs/前端开发规范.md Normal file
View File

@ -0,0 +1,528 @@
# Finyx AI 前端开发规范与约束
## 📋 文档说明
本文档定义了 Finyx AI 前端项目的开发规范、约束和最佳实践,确保代码质量、一致性和可维护性。所有开发人员必须遵循本规范。
**最后更新**: 2025-01-XX
**适用版本**: v1.0.0+
---
## 🎨 设计系统规范
### 1. 色彩系统
#### 1.1 主色调
- **主色 (Primary)**: `#3067EF` - 用于主要操作按钮、链接、激活状态
- **背景色 (Background)**: `#F4F8FF` - 页面主背景色
- **白色 (White)**: `#ffffff` - 卡片、侧边栏背景
#### 1.2 文本颜色
- **主文本**: `#1f2329` - 主要文本内容
- **次文本**: `#646a73` - 次要文本、说明文字
- **禁用文本**: `#bbbfc4` - 禁用状态的文本
#### 1.3 边框颜色
- **默认边框**: `#dee0e3` - 卡片、输入框边框
- **激活边框**: `#3067EF` - 激活状态的边框
#### 1.4 状态颜色
- **悬停背景**: `rgba(48, 103, 239, 0.06)` - 鼠标悬停时的背景色
- **激活背景**: `#ECF2FF` - 激活状态的背景色
#### 1.5 使用规范
```typescript
// ✅ 正确:使用 Tailwind 配置的颜色类
<div className="bg-app-primary text-app-white">按钮</div>
<div className="bg-app-bg text-app-text">内容</div>
// ❌ 错误:直接使用颜色值
<div className="bg-[#3067EF]">按钮</div>
<div style={{ backgroundColor: '#3067EF' }}>按钮</div>
```
### 2. 字体系统
#### 2.1 字体族
- **主字体**: `PingFang SC`, `AlibabaPuHuiTi`, `sans-serif`
- **等宽字体**: 用于代码显示
#### 2.2 字体大小
- **基础字号**: `14px` (Tailwind: `text-base`)
- **标题层级**:
- H1: `24px` (`text-2xl`)
- H2: `20px` (`text-xl`)
- H3: `18px` (`text-lg`)
- H4: `16px` (`text-base`)
#### 2.3 字重
- **常规**: `400` (`font-normal`)
- **中等**: `500` (`font-medium`)
- **加粗**: `600` (`font-bold`)
### 3. 间距系统
#### 3.1 基础间距单位
- **基础单位**: `8px` (对应 Tailwind 的 `1` 单位)
- **常用间距**:
- `4px` (`p-1`, `m-1`)
- `8px` (`p-2`, `m-2`)
- `12px` (`p-3`, `m-3`)
- `16px` (`p-4`, `m-4`)
- `24px` (`p-6`, `m-6`)
#### 3.2 页面内边距
- **页面容器**: `24px` (`p-6`)
- **卡片内边距**: `16px` (`p-4`) 或 `24px` (`p-6`)
### 4. 阴影系统
#### 4.1 标准阴影
- **卡片阴影**: `0px 2px 4px 0px rgba(31, 35, 41, 0.12)` (`shadow-app`)
- **悬停阴影**: 可适当增强
---
## 🏗️ 技术栈约束
### 1. 核心框架
- **前端框架**: Vue 3.4.0+ (Composition API)
- **开发语言**: TypeScript 5.2.2+
- **构建工具**: Vite 5.0.8+
- **样式方案**: Tailwind CSS 3.3.6+
### 2. 状态管理
- **状态管理库**: Pinia 2.1.0+
- **路由管理**: Vue Router 4.2.0+
### 3. UI 组件库
- **图标库**: Lucide Vue Next 0.344.0+
- **图表库**: Recharts 2.10.3+ (如需要)
### 4. 禁止使用的技术
- ❌ **React**: 本项目已迁移至 Vue禁止使用 React
- ❌ **Element Plus**: 旧系统使用,新功能应使用 Tailwind CSS 构建
- ❌ **SCSS/SASS**: 统一使用 Tailwind CSS禁止使用 SCSS
- ❌ **内联样式**: 除非动态样式,否则使用 Tailwind 类
---
## 📁 项目结构规范
### 1. 目录结构
```
src/
├── components/ # 通用可复用组件
│ ├── common/ # 基础通用组件
│ └── business/ # 业务相关组件
├── layouts/ # 布局组件
├── pages/ # 页面组件
│ └── engagement/ # 业务模块子页面
├── stores/ # Pinia 状态管理
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
├── api/ # API 接口定义
├── assets/ # 静态资源
└── styles/ # 全局样式(如需要)
```
### 2. 文件命名规范
#### 2.1 组件文件
- **Vue 组件**: 使用 PascalCase`SidebarItem.vue`
- **TypeScript 文件**: 使用 camelCase`mockData.ts`
- **工具函数**: 使用 camelCase`formatDate.ts`
#### 2.2 组件命名
- **单文件组件**: 使用 PascalCase
- **组件名**: 与文件名保持一致
```vue
<!-- ✅ 正确 -->
<script setup lang="ts">
// SidebarItem.vue
</script>
<!-- ❌ 错误 -->
<script setup lang="ts">
// 文件名: SidebarItem.vue但组件名不一致
</script>
```
### 3. 导入顺序规范
```typescript
// 1. Vue 核心
import { ref, computed, onMounted } from 'vue'
// 2. 第三方库
import { useRouter } from 'vue-router'
import { useToast } from '@/components/Toast'
// 3. 项目内部组件
import { SidebarItem } from '@/components/SidebarItem'
// 4. 类型定义
import type { Project, Step } from '@/types'
// 5. 工具函数
import { formatDate } from '@/utils/date'
// 6. 样式(如需要)
import './styles.css'
```
---
## 💻 代码规范
### 1. TypeScript 规范
#### 1.1 类型定义
```typescript
// ✅ 正确:使用 interface 定义对象类型
interface Project {
id: string
name: string
progress: number
}
// ✅ 正确:使用 type 定义联合类型
type Step = 'setup' | 'inventory' | 'context' | 'value' | 'delivery'
// ❌ 错误:使用 any
function processData(data: any) { }
```
#### 1.2 Props 定义
```vue
<script setup lang="ts">
// ✅ 正确:使用 defineProps 和类型
interface Props {
title: string
count?: number
items: Project[]
}
const props = defineProps<Props>()
// ❌ 错误:不使用类型
const props = defineProps(['title', 'count'])
</script>
```
#### 1.3 响应式数据
```vue
<script setup lang="ts">
// ✅ 正确:明确类型
const count = ref<number>(0)
const projects = ref<Project[]>([])
// ❌ 错误:不指定类型
const count = ref(0)
</script>
```
### 2. Vue 组件规范
#### 2.1 Composition API
```vue
<script setup lang="ts">
// ✅ 正确:使用 Composition API
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
onMounted(() => {
// 初始化逻辑
})
</script>
```
#### 2.2 组件结构
```vue
<template>
<!-- 模板内容 -->
</template>
<script setup lang="ts">
// 脚本内容
</script>
<style scoped>
/* 样式内容(尽量使用 Tailwind避免使用 */
</style>
```
#### 2.3 事件处理
```vue
<template>
<!-- ✅ 正确:使用 @click -->
<button @click="handleClick">点击</button>
<!-- ✅ 正确:传递参数 -->
<button @click="handleDelete(item.id)">删除</button>
</template>
<script setup lang="ts">
const handleClick = () => {
// 处理逻辑
}
const handleDelete = (id: string) => {
// 处理逻辑
}
</script>
```
### 3. 样式规范
#### 3.1 Tailwind CSS 使用
```vue
<template>
<!-- ✅ 正确:使用 Tailwind 类 -->
<div class="bg-app-white border border-app-border rounded-lg p-6">
<h2 class="text-xl font-bold text-app-text">标题</h2>
</div>
<!-- ❌ 错误:使用内联样式 -->
<div style="background: white; border: 1px solid #dee0e3;">
内容
</div>
</template>
```
#### 3.2 响应式设计
```vue
<template>
<!-- ✅ 正确:使用 Tailwind 响应式类 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 内容 -->
</div>
</template>
```
#### 3.3 自定义样式
```vue
<style scoped>
/* ✅ 仅在必要时使用自定义样式 */
.custom-animation {
animation: fade-in 0.3s ease-out;
}
/* ❌ 避免覆盖 Tailwind 类 */
.bg-white {
background: red; /* 错误 */
}
</style>
```
### 4. 组件设计规范
#### 4.1 组件职责
- **单一职责**: 每个组件只负责一个功能
- **可复用性**: 通用组件应设计为可复用
- **可组合性**: 复杂组件应由简单组件组合而成
#### 4.2 Props 设计
```vue
<script setup lang="ts">
// ✅ 正确:明确的 Props 定义
interface Props {
// 必需属性
title: string
items: Project[]
// 可选属性
count?: number
showActions?: boolean
// 带默认值
variant?: 'primary' | 'secondary'
}
const props = withDefaults(defineProps<Props>(), {
showActions: true,
variant: 'primary'
})
</script>
```
#### 4.3 事件定义
```vue
<script setup lang="ts">
// ✅ 正确:使用 defineEmits
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: string): void
}
const emit = defineEmits<Emits>()
const handleUpdate = (value: string) => {
emit('update', value)
}
</script>
```
---
## 🎯 与旧系统融合规范
### 1. 色彩一致性
- **必须使用**: 旧系统的色彩变量(已在 `tailwind.config.js` 中定义)
- **禁止**: 使用新的颜色值,除非经过设计团队批准
### 2. 组件风格
- **侧边栏**: 白色背景,浅色激活状态
- **卡片**: 白色背景,浅色边框,标准阴影
- **按钮**: 主色背景,圆角设计
### 3. 技术栈兼容
- **新功能**: 使用 Vue 3 + TypeScript + Tailwind CSS
- **旧功能**: 保持 Vue 3 + Element Plus + SCSS
- **融合策略**: 新功能作为独立模块,通过路由集成
### 4. 样式隔离
- **新组件**: 使用 Tailwind CSS避免全局样式污染
- **旧组件**: 保持现有样式,不强制迁移
- **冲突处理**: 使用 CSS Modules 或作用域样式
---
## 🚫 禁止事项
### 1. 代码层面
- ❌ 禁止使用 `any` 类型(除非特殊情况)
- ❌ 禁止使用 `@ts-ignore`(除非有充分理由)
- ❌ 禁止使用内联样式(除非动态样式)
- ❌ 禁止直接修改 DOM使用 Vue 响应式系统)
### 2. 样式层面
- ❌ 禁止使用 SCSS/SASS统一使用 Tailwind
- ❌ 禁止使用硬编码颜色值(使用 Tailwind 配置的颜色)
- ❌ 禁止使用 `!important`(除非绝对必要)
### 3. 依赖层面
- ❌ 禁止引入 React 相关依赖
- ❌ 禁止引入 Element Plus新功能
- ❌ 禁止引入未经过审查的第三方库
---
## ✅ 最佳实践
### 1. 性能优化
- 使用 `computed` 而非 `watch`(如可能)
- 使用 `v-show` 而非 `v-if`(频繁切换)
- 大列表使用虚拟滚动
- 图片使用懒加载
### 2. 可访问性
- 使用语义化 HTML 标签
- 为交互元素添加 `aria-label`
- 确保键盘导航可用
- 确保颜色对比度符合 WCAG 标准
### 3. 错误处理
- 使用 try-catch 处理异步操作
- 使用 Toast 显示错误信息
- 提供友好的错误提示
### 4. 代码注释
```typescript
// ✅ 正确:清晰的注释
/**
* 计算项目进度百分比
* @param completed - 已完成步骤数
* @param total - 总步骤数
* @returns 进度百分比 (0-100)
*/
const calculateProgress = (completed: number, total: number): number => {
return Math.round((completed / total) * 100)
}
```
---
## 📝 提交规范
### 1. Commit 消息格式
```
<type>(<scope>): <subject>
<body>
<footer>
```
### 2. Type 类型
- `feat`: 新功能
- `fix`: 修复 bug
- `docs`: 文档更新
- `style`: 代码格式调整
- `refactor`: 代码重构
- `perf`: 性能优化
- `test`: 测试相关
- `chore`: 构建/工具相关
### 3. 示例
```
feat(dashboard): 添加项目进度可视化
- 添加进度条组件
- 集成到 DashboardView
- 支持实时更新
Closes #123
```
---
## 🔍 代码审查清单
### 1. 功能检查
- [ ] 功能按需求实现
- [ ] 边界情况已处理
- [ ] 错误处理完善
- [ ] 用户体验良好
### 2. 代码质量
- [ ] TypeScript 类型完整
- [ ] 无 ESLint 错误
- [ ] 代码结构清晰
- [ ] 注释充分
### 3. 样式检查
- [ ] 使用 Tailwind 配置的颜色
- [ ] 响应式设计正确
- [ ] 与设计稿一致
- [ ] 无样式冲突
### 4. 性能检查
- [ ] 无不必要的重渲染
- [ ] 异步操作正确处理
- [ ] 大列表已优化
---
## 📚 参考资源
- [Vue 3 官方文档](https://vuejs.org/)
- [TypeScript 官方文档](https://www.typescriptlang.org/)
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
- [Pinia 官方文档](https://pinia.vuejs.org/)
- [Vue Router 官方文档](https://router.vuejs.org/)
---
## 📅 更新记录
- **2025-01-XX**: 初始版本创建
---
## 👥 维护者
- Finyx AI 前端团队

144
package-lock.json generated
View File

@ -8,8 +8,11 @@
"name": "finyx-frontend", "name": "finyx-frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.0",
"element-plus": "^2.13.0",
"lucide-vue-next": "^0.344.0", "lucide-vue-next": "^0.344.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.0", "pinia": "^2.1.0",
"recharts": "^2.10.3", "recharts": "^2.10.3",
"vue": "^3.4.0", "vue": "^3.4.0",
@ -98,6 +101,24 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -576,6 +597,31 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -714,6 +760,17 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.55.1", "version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
@ -1141,6 +1198,21 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.7.1", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
@ -1708,6 +1780,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.23", "version": "10.4.23",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
@ -2143,6 +2221,12 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/de-indent": { "node_modules/de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@ -2238,6 +2322,31 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/element-plus": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.0.tgz",
"integrity": "sha512-qjxS+SBChvqCl6lU6ShiliLMN6WqFHiXQENYbAY3GKNflG+FS3jqn8JmQq0CBZq4koFqsi95NT1M6SL4whZfrA==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.2",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "^10.11.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.19",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.3",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.3.0"
}
},
"node_modules/entities": { "node_modules/entities": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
@ -3099,6 +3208,23 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash-es": {
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
"version": "4.6.2", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -3136,6 +3262,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge2": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -3244,6 +3376,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
"license": "MIT"
},
"node_modules/nth-check": { "node_modules/nth-check": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",

View File

@ -6,26 +6,29 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext .vue,.js,.cjs,.mjs,.ts --ignore-pattern '旧代码/**' --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview", "preview": "vite preview",
"type-check": "vue-tsc --noEmit" "type-check": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"vue": "^3.4.0", "@element-plus/icons-vue": "^2.3.2",
"pinia": "^2.1.0",
"vue-router": "^4.2.0",
"lucide-vue-next": "^0.344.0",
"@vueuse/core": "^10.7.0", "@vueuse/core": "^10.7.0",
"recharts": "^2.10.3" "element-plus": "^2.13.0",
"lucide-vue-next": "^0.344.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.0",
"recharts": "^2.10.3",
"vue": "^3.4.0",
"vue-router": "^4.2.0"
}, },
"devDependencies": { "devDependencies": {
"@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/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0", "@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-vue": "^5.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-plugin-vue": "^9.20.0",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"tailwindcss": "^3.3.6", "tailwindcss": "^3.3.6",
"typescript": "^5.2.2", "typescript": "^5.2.2",

View File

@ -1,25 +0,0 @@
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<ViewMode>('projects');
const [currentStep, setCurrentStep] = useState<Step>('setup');
const [isPresentationMode, setIsPresentationMode] = useState(false);
return (
<ToastProvider>
<MainLayout
currentView={currentView}
setCurrentView={setCurrentView}
currentStep={currentStep}
setCurrentStep={setCurrentStep}
isPresentationMode={isPresentationMode}
setIsPresentationMode={setIsPresentationMode}
/>
</ToastProvider>
);
}
export default App;

View File

@ -1,34 +1,8 @@
<template> <template>
<MainLayout <router-view />
:currentView="currentView"
:setCurrentView="setCurrentView"
:currentStep="currentStep"
:setCurrentStep="setCurrentStep"
:isPresentationMode="isPresentationMode"
:setIsPresentationMode="setIsPresentationMode"
/>
<ToastContainer /> <ToastContainer />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue';
import MainLayout from '@/layouts/MainLayout.vue';
import ToastContainer from '@/components/ToastContainer.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> </script>

View File

@ -1,62 +0,0 @@
import React from 'react';
import { createPortal } from 'react-dom';
import { AlertTriangle } from 'lucide-react';
interface ConfirmDialogProps {
visible: boolean;
title?: string;
message?: string;
onConfirm: () => void;
onCancel: () => void;
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
visible,
title = '确认删除',
message = '删除后数据将无法恢复,确定要继续吗?',
onConfirm,
onCancel,
}) => {
if (!visible) return null;
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onCancel();
}
};
const content = (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 transition-opacity duration-200"
onClick={handleBackdropClick}
>
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6 transform transition-all duration-200">
<div className="flex items-start mb-4">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-red-100 flex items-center justify-center mr-3">
<AlertTriangle size={20} className="text-red-600" />
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-slate-900 mb-2">{title}</h3>
<p className="text-sm text-slate-600">{message}</p>
</div>
</div>
<div className="flex justify-end space-x-3 mt-6">
<button
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-md hover:bg-slate-200 transition-colors"
>
</button>
<button
onClick={onConfirm}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 transition-colors"
>
</button>
</div>
</div>
</div>
);
return createPortal(content, document.body);
};

View File

@ -70,7 +70,7 @@ interface Emits {
(e: 'update:visible', value: boolean): void; (e: 'update:visible', value: boolean): void;
} }
const props = withDefaults(defineProps<Props>(), { withDefaults(defineProps<Props>(), {
title: '确认删除', title: '确认删除',
message: '删除后数据将无法恢复,确定要继续吗?', message: '删除后数据将无法恢复,确定要继续吗?',
}); });

View File

@ -1,20 +0,0 @@
import React from 'react';
interface ProgressBarProps {
percent: number;
status: string;
}
export const ProgressBar: React.FC<ProgressBarProps> = ({ percent, status }) => {
let color = 'bg-blue-500';
if (status === 'risk') color = 'bg-red-500';
if (status === 'warning') color = 'bg-amber-500';
if (status === 'review') color = 'bg-purple-500';
if (status === 'new') color = 'bg-slate-300';
return (
<div className="w-full bg-slate-200 rounded-full h-2">
<div className={`${color} h-2 rounded-full`} style={{ width: `${percent}%` }}></div>
</div>
);
};

View File

@ -1,21 +0,0 @@
import React from 'react';
import { LucideIcon } from 'lucide-react';
interface SidebarItemProps {
icon: LucideIcon;
text: string;
active: boolean;
onClick: () => void;
}
export const SidebarItem: React.FC<SidebarItemProps> = ({ icon: Icon, text, active, onClick }) => (
<div
onClick={onClick}
className={`flex items-center space-x-3 px-6 py-4 cursor-pointer transition-colors duration-200 ${
active ? 'bg-slate-800 border-l-4 border-blue-500 text-white' : 'text-slate-400 hover:bg-slate-800 hover:text-white'
}`}
>
<Icon size={20} />
<span className="font-medium text-sm tracking-wide">{text}</span>
</div>
);

View File

@ -2,14 +2,14 @@
<div <div
@click="onClick" @click="onClick"
:class="[ :class="[
'flex items-center space-x-3 px-6 py-4 cursor-pointer transition-colors duration-200', 'flex items-center space-x-3 px-6 py-3 mx-2 mb-1.5 cursor-pointer transition-colors duration-200 rounded-xl',
active active
? 'bg-slate-800 border-l-4 border-blue-500 text-white' ? 'bg-app-active text-app-primary'
: 'text-slate-400 hover:bg-slate-800 hover:text-white' : 'text-app-text-secondary hover:bg-app-hover hover:text-app-text'
]" ]"
> >
<component :is="icon" :size="20" /> <component :is="icon" :size="16" :class="active ? 'text-app-primary' : ''" />
<span class="font-medium text-sm tracking-wide">{{ text }}</span> <span :class="['font-medium text-sm', active ? 'text-app-primary font-medium' : '']">{{ text }}</span>
</div> </div>
</template> </template>

View File

@ -1,18 +0,0 @@
import React from 'react';
interface TableCheckItemProps {
name: string;
required?: boolean;
}
export const TableCheckItem: React.FC<TableCheckItemProps> = ({ name, required }) => (
<div className="flex items-center justify-between p-3 bg-white border border-slate-200 rounded-lg hover:border-blue-400 transition-colors cursor-pointer group">
<div className="flex items-center">
<div className="w-5 h-5 rounded border border-slate-300 mr-3 flex items-center justify-center text-white group-hover:border-blue-500">
<div className="w-3 h-3 bg-slate-200 rounded-sm group-hover:bg-blue-100"></div>
</div>
<span className="text-sm font-medium text-slate-700">{name}</span>
</div>
{required && <span className="text-[10px] text-red-500 font-bold bg-red-50 px-1.5 py-0.5 rounded"></span>}
</div>
);

View File

@ -1,116 +0,0 @@
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<ToastProps> = ({ 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 (
<div
className={`flex items-center gap-3 px-4 py-3 rounded-lg border shadow-lg min-w-[300px] max-w-[500px] animate-slide-in-right ${colors[toast.type]}`}
role="alert"
>
<Icon size={20} className={`flex-shrink-0 ${iconColors[toast.type]}`} />
<p className="flex-1 text-sm font-medium">{toast.message}</p>
<button
onClick={() => onClose(toast.id)}
className="flex-shrink-0 text-slate-400 hover:text-slate-600 transition-colors"
aria-label="关闭通知"
>
<X size={16} />
</button>
</div>
);
};
interface ToastContainerProps {
toasts: Toast[];
onClose: (id: string) => void;
}
export const ToastContainer: React.FC<ToastContainerProps> = ({ toasts, onClose }) => {
if (toasts.length === 0) return null;
return (
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
{toasts.map((toast) => (
<div key={toast.id} className="pointer-events-auto">
<ToastComponent toast={toast} onClose={onClose} />
</div>
))}
</div>
);
};
// Hook for managing toasts
export const useToast = () => {
const [toasts, setToasts] = React.useState<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 };
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,
};
};

View File

@ -0,0 +1,42 @@
<template>
<component :is="linkType" v-bind="linkProps(to)">
<slot></slot>
</component>
</template>
<script setup lang="ts">
import { computed } from 'vue'
defineOptions({
name: 'AppLink',
inheritAttrs: false,
})
const props = defineProps({
to: {
type: Object,
required: true,
},
})
const isExternal = (path: string) => {
return /^(https?:|mailto:|tel:)/.test(path)
}
const isExternalLink = computed(() => {
return isExternal(props.to.path || '')
})
const linkType = computed(() => (isExternalLink.value ? 'a' : 'router-link'))
const linkProps = (to: any) => {
if (isExternalLink.value) {
return {
href: to.path,
target: '_blank',
rel: 'noopener noreferrer',
}
}
return { to: to }
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<i :class="iconClass" :style="iconStyle"></i>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
iconClass: string
size?: number
}>()
const iconClass = computed(() => {
return `svg-icon ${props.iconClass}`
})
const iconStyle = computed(() => {
if (props.size) {
return {
fontSize: `${props.size}px`,
width: `${props.size}px`,
height: `${props.size}px`
}
}
return {}
})
</script>
<style scoped>
.svg-icon {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: middle;
}
</style>

View File

@ -1,30 +0,0 @@
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<ToastContextType | undefined>(undefined);
export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const { toasts, closeToast, success, error, warning, info } = useToastHook();
return (
<ToastContext.Provider value={{ success, error, warning, info }}>
{children}
<ToastContainer toasts={toasts} onClose={closeToast} />
</ToastContext.Provider>
);
};
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within ToastProvider');
}
return context;
};

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,74 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* 基础样式重置 - 匹配旧系统 */
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
height: 100%;
}
body {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: 'PingFang SC', 'AlibabaPuHuiTi', sans-serif;
font-size: 14px;
font-weight: 500;
height: 100%;
margin: 0;
padding: 0;
color: #1f2329;
background-color: #F4F8FF;
}
#app {
height: 100%;
}
:focus {
outline: none;
}
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
/* 滚动条样式 - 匹配旧系统 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-track {
border-radius: 5px;
background-color: transparent;
}
}
@keyframes slide-in-right { @keyframes slide-in-right {
from { from {
transform: translateX(100%); transform: translateX(100%);

68
src/layouts/AppLayout.vue Normal file
View File

@ -0,0 +1,68 @@
<template>
<div class="app-layout">
<AppHeader />
<div class="app-main webapp-main">
<div class="menu">
<div class="sidebar-container">
<Sidebar />
</div>
</div>
<div class="res-app-main">
<AppMain />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import AppHeader from './components/AppHeader.vue'
import AppMain from './components/AppMain.vue'
import Sidebar from './components/Sidebar.vue'
defineOptions({
name: 'AppLayout'
})
</script>
<style scoped>
.app-layout {
background-color: var(--app-layout-bg-color, #F4F8FF);
height: 100%;
}
.app-main {
position: relative;
height: 100%;
padding: var(--app-header-height, 60px) 0 0 !important;
box-sizing: border-box;
scrollbar-color: transparent transparent;
display: flex;
}
.app-main .menu {
width: var(--app-main-menu-width, 240px);
overflow-y: auto;
margin-top: 20px;
padding-left: 10px;
}
.app-main .sidebar-container {
height: 100%;
}
.app-main .res-app-main {
height: calc(100% - 60px);
background-color: #fff;
border-radius: 16px;
margin-top: 20px;
padding: 20px 10px 20px 20px;
width: calc(100vw - var(--app-main-menu-width, 240px));
overflow-y: auto;
position: relative;
}
.app-main .res-app-main:has(.chat-pc-new) {
background-color: #F4F8FF !important;
padding: 0 20px !important;
}
</style>

View File

@ -1,57 +0,0 @@
import React from 'react';
import { ViewMode } from '../types';
import { Sidebar } from './Sidebar';
import { DashboardView } from '../pages/DashboardView';
import { ProjectListView } from '../pages/ProjectListView';
import { EngagementView } from '../pages/EngagementView';
interface MainLayoutProps {
currentView: ViewMode;
setCurrentView: (view: ViewMode) => void;
currentStep: any;
setCurrentStep: (step: any) => void;
isPresentationMode: boolean;
setIsPresentationMode: (mode: boolean) => void;
}
export const MainLayout: React.FC<MainLayoutProps> = ({
currentView,
setCurrentView,
currentStep,
setCurrentStep,
isPresentationMode,
setIsPresentationMode,
}) => (
<div className="flex h-screen w-full bg-slate-900 font-sans text-slate-900 overflow-hidden">
<Sidebar
currentView={currentView}
setCurrentView={setCurrentView}
isPresentationMode={isPresentationMode}
/>
{/* Main Area */}
<div className="flex-1 flex flex-col bg-slate-50 relative overflow-hidden">
{currentView === 'dashboard' && (
<DashboardView
setCurrentView={setCurrentView}
setCurrentStep={setCurrentStep}
/>
)}
{currentView === 'projects' && (
<ProjectListView
setCurrentView={setCurrentView}
setCurrentStep={setCurrentStep}
/>
)}
{currentView === 'engagement' && (
<EngagementView
currentStep={currentStep}
setCurrentStep={setCurrentStep}
setCurrentView={setCurrentView}
isPresentationMode={isPresentationMode}
setIsPresentationMode={setIsPresentationMode}
/>
)}
</div>
</div>
);

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex h-screen w-full bg-slate-900 font-sans text-slate-900 overflow-hidden"> <div class="flex h-screen w-full bg-app-bg font-sans text-app-text overflow-hidden">
<Sidebar <Sidebar
:currentView="currentView" :currentView="currentView"
:setCurrentView="setCurrentView" :setCurrentView="setCurrentView"

View File

@ -1,60 +0,0 @@
import React from 'react';
import {
LayoutDashboard,
Briefcase,
BookOpen,
Settings
} from 'lucide-react';
import { ViewMode } from '../types';
import { SidebarItem } from '../components/SidebarItem';
interface SidebarProps {
currentView: ViewMode;
setCurrentView: (view: ViewMode) => void;
isPresentationMode: boolean;
}
export const Sidebar: React.FC<SidebarProps> = ({
currentView,
setCurrentView,
isPresentationMode
}) => (
<div className={`w-64 bg-slate-900 flex flex-col border-r border-slate-800 transition-all duration-300 ${isPresentationMode ? '-ml-64' : ''}`}>
<div className="h-16 flex items-center px-6 border-b border-slate-800">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center mr-3">
<span className="font-bold text-white text-lg">F</span>
</div>
<span className="text-white font-bold text-lg tracking-tight">FINYX AI</span>
</div>
<div className="flex-1 overflow-y-auto py-6">
<div className="px-6 mb-2 text-xs font-bold text-slate-500 uppercase tracking-wider">Main</div>
<SidebarItem
icon={LayoutDashboard}
text="工作台"
active={currentView === 'dashboard'}
onClick={() => setCurrentView('dashboard')}
/>
<SidebarItem
icon={Briefcase}
text="项目列表"
active={currentView === 'projects' || currentView === 'engagement'}
onClick={() => setCurrentView('projects')}
/>
<div className="px-6 mt-8 mb-2 text-xs font-bold text-slate-500 uppercase tracking-wider">Assets</div>
<SidebarItem icon={BookOpen} text="知识库 & 模板" active={false} onClick={() => {}} />
</div>
<div className="p-4 border-t border-slate-800">
<SidebarItem icon={Settings} text="系统配置" active={false} onClick={() => {}} />
<div className="mt-4 flex items-center px-6">
<div className="w-8 h-8 rounded-full bg-slate-700"></div>
<div className="ml-3">
<p className="text-sm font-medium text-white">Sarah Jenkins</p>
<p className="text-xs text-slate-500">Partner</p>
</div>
</div>
</div>
</div>
);

View File

@ -1,35 +1,37 @@
<template> <template>
<div :class="[ <div :class="[
'w-64 bg-slate-900 flex flex-col border-r border-slate-800 transition-all duration-300', 'w-64 bg-app-sidebar-bg flex flex-col border-r border-app-border transition-all duration-300',
isPresentationMode ? '-ml-64' : '' isPresentationMode ? '-ml-64' : ''
]"> ]">
<div class="h-16 flex items-center px-6 border-b border-slate-800"> <div class="h-16 flex items-center px-6 border-b border-app-border">
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center mr-3"> <div class="w-8 h-8 bg-gradient-to-br from-app-primary to-purple-600 rounded-lg flex items-center justify-center mr-3">
<span class="font-bold text-white text-lg">F</span> <span class="font-bold text-white text-lg">F</span>
</div> </div>
<span class="text-white font-bold text-lg tracking-tight">FINYX AI</span> <span class="text-app-text font-bold text-lg tracking-tight">FINYX AI</span>
</div> </div>
<div class="flex-1 overflow-y-auto py-6"> <div class="flex-1 overflow-y-auto py-6">
<div class="px-6 mb-2 text-xs font-bold text-slate-500 uppercase tracking-wider">Main</div> <div class="px-6 mb-2 text-xs font-bold text-app-text-secondary uppercase tracking-wider">Main</div>
<SidebarItem <SidebarItem
:icon="Briefcase" :icon="Briefcase"
text="项目列表" text="数据资源盘点"
:active="currentView === 'projects' || currentView === 'engagement'" :active="currentView === 'projects' || currentView === 'engagement'"
:onClick="() => setCurrentView('projects')" :onClick="() => setCurrentView('projects')"
/> />
<div class="px-6 mt-8 mb-2 text-xs font-bold text-slate-500 uppercase tracking-wider">Assets</div> <div class="px-6 mt-8 mb-2 text-xs font-bold text-app-text-secondary uppercase tracking-wider">Assets</div>
<SidebarItem :icon="BookOpen" text="知识库 & 模板" :active="false" :onClick="() => {}" /> <SidebarItem :icon="BookOpen" text="知识库 & 模板" :active="false" :onClick="() => {}" />
</div> </div>
<div class="p-4 border-t border-slate-800"> <div class="p-4 border-t border-app-border">
<SidebarItem :icon="Settings" text="系统配置" :active="false" :onClick="() => {}" /> <SidebarItem :icon="Settings" text="系统配置" :active="false" :onClick="() => {}" />
<div class="mt-4 flex items-center px-6"> <div class="mt-4 flex items-center px-6">
<div class="w-8 h-8 rounded-full bg-slate-700"></div> <div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-app-primary flex items-center justify-center">
<span class="text-white text-xs font-bold">S</span>
</div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm font-medium text-white">Sarah Jenkins</p> <p class="text-sm font-medium text-app-text">Sarah Jenkins</p>
<p class="text-xs text-slate-500">Partner</p> <p class="text-xs text-app-text-secondary">Partner</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,61 @@
<template>
<div class="app-header">
<div class="left">
<a class="logo" @click="goHome">
<div class="flex items-center gap-2">
<!-- Logo will be added later -->
<span class="text-xl font-bold text-app-text">FINYX AI</span>
</div>
</a>
</div>
<div class="right">
<UserAvatar />
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import UserAvatar from './UserAvatar.vue'
const router = useRouter()
const goHome = () => {
router.push('/')
}
</script>
<style scoped>
.app-header {
background: var(--app-header-bg-color, #F4F8FF);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
height: var(--app-header-height, 60px);
}
.app-header .left a {
display: flex;
align-items: center;
justify-content: center;
padding-left: 30px;
cursor: pointer;
}
.app-header .left .logo-img {
width: 150px;
height: auto;
}
.app-header .right {
padding-right: 30px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</template>
<script setup lang="ts">
import { ref, onBeforeUpdate } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const cachedViews = ref<string[]>([])
onBeforeUpdate(() => {
const { name } = route
if (name && !cachedViews.value.includes(name.toString())) {
cachedViews.value.push(name.toString())
}
})
</script>

View File

@ -0,0 +1,116 @@
<template>
<div class="sidebar-wrapper">
<!-- 数据资源盘点 - 新功能菜单第一项 -->
<div class="new-menu-section">
<div class="px-6 mb-2 text-xs font-bold text-app-text-secondary uppercase tracking-wider">数据资源</div>
<SidebarItem
:icon="Briefcase"
text="数据资源盘点"
:active="isActiveRoute('/data-inventory')"
@click="navigateTo('/data-inventory')"
/>
</div>
<!-- 旧系统菜单 - 使用 Element Plus -->
<div class="old-menu-section">
<el-menu
:default-active="activeMenu"
:default-openeds="['/smart', '/knowledge']"
router
:collapse-transition="false"
:unique-opened="false"
class="legacy-menu"
>
<SidebarItemLegacy
v-for="(menu) in legacyMenuList"
:key="menu.path"
:menu="menu"
:activeMenu="activeMenu"
/>
</el-menu>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Briefcase } from 'lucide-vue-next'
import { routes } from '@/router/routes'
import SidebarItem from '@/components/SidebarItem.vue'
import LegacySidebarItem from './sidebar-item/index.vue'
// SidebarItem
const SidebarItemLegacy = LegacySidebarItem
const route = useRoute()
const router = useRouter()
const activeMenu = computed(() => {
const { path, meta } = route
const basePath = path.split('?')[0].split('/').slice(0, 4).join('/')
return meta.activeMenu || basePath
})
//
const legacyMenuList = computed(() => {
return routes.filter((el: any) =>
el.meta &&
!el.meta.hidden &&
(el.path === '/smart' || el.path === '/knowledge')
)
})
const isActiveRoute = (path: string) => {
return route.path.startsWith(path)
}
const navigateTo = (path: string) => {
router.push(path)
}
</script>
<style scoped>
.sidebar-wrapper {
height: 100%;
}
.new-menu-section {
padding: 16px 0;
border-bottom: 1px solid var(--app-border, #dee0e3);
}
.old-menu-section {
flex: 1;
overflow-y: auto;
}
:deep(.legacy-menu) {
border: none;
background: transparent;
}
:deep(.legacy-menu .el-menu-item),
:deep(.legacy-menu .el-sub-menu__title) {
padding: 13px 12px 13px 16px !important;
font-weight: 500;
border-radius: 4px;
height: auto;
line-height: normal;
}
:deep(.legacy-menu .el-menu-item:hover),
:deep(.legacy-menu .el-sub-menu__title:hover) {
background: none;
color: var(--el-color-primary, #3067EF);
}
:deep(.legacy-menu .el-menu-item.is-active) {
color: var(--el-color-primary, #3067EF);
background: var(--el-color-primary-light-9, #ECF2FF);
}
:deep(.legacy-menu .el-sub-menu .el-menu-item) {
padding-left: 43px !important;
}
</style>

View File

@ -0,0 +1,15 @@
<template>
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-app-primary flex items-center justify-center">
<span class="text-white text-xs font-bold"></span>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-app-text">管理员</p>
<p class="text-xs text-app-text-secondary">管理员</p>
</div>
</div>
</template>
<script setup lang="ts">
// store
</script>

View File

@ -0,0 +1,96 @@
<template>
<div v-if="!menu.meta || !menu.meta.hidden" class="sidebar-item">
<el-sub-menu
v-if="menu?.children && menu?.children.length > 0"
:index="menu.path"
popper-class="sidebar-container-popper"
>
<template #title>
<svg-icon
v-if="menu.meta && menu.meta.icon"
:icon-class="menuIcon"
class="sidebar-icon"
/>
<span>{{ menu.meta?.title }}</span>
</template>
<sidebar-item
v-for="(child, index) in menu?.children"
:key="index"
:menu="child"
:activeMenu="activeMenu"
/>
</el-sub-menu>
<app-link
v-else-if="menu.meta && !menu.meta.hidden"
:to="{ path: menu.path }"
>
<el-menu-item
:index="menu.path"
popper-class="sidebar-popper"
>
<template #title>
<svg-icon
v-if="menu.meta && menu.meta.icon"
:icon-class="menuIcon"
class="sidebar-icon"
/>
<span v-if="menu.meta && menu.meta.title">{{ menu.meta.title }}</span>
</template>
</el-menu-item>
</app-link>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { type RouteRecordRaw } from 'vue-router'
import AppLink from '@/components/app-link/index.vue'
import SvgIcon from '@/components/svg-icon/index.vue'
defineOptions({ name: 'LegacySidebarItem', inheritAttrs: false })
const props = defineProps<{
menu: RouteRecordRaw
activeMenu: any
}>()
const menuIcon = computed(() => {
if (props.activeMenu === props.menu.path) {
return props.menu.meta?.iconActive || props.menu?.meta?.icon
} else {
return props.menu?.meta?.icon
}
})
</script>
<style scoped>
.sidebar-item {
.sidebar-icon {
font-size: 20px;
margin-top: -2px;
margin-right: 8px;
}
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
padding: 13px 12px 13px 16px !important;
font-weight: 500;
border-radius: 4px;
}
:deep(.el-menu-item:hover),
:deep(.el-sub-menu__title:hover) {
background: none;
color: var(--el-color-primary);
}
:deep(.el-menu-item.is-active) {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
:deep(.el-sub-menu .el-menu-item) {
padding-left: 43px !important;
}
</style>

View File

@ -1,10 +1,16 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'nprogress/nprogress.css'
import './index.css' import './index.css'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia() const pinia = createPinia()
app.use(pinia) app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#root') app.mount('#root')

View File

@ -1,10 +0,0 @@
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>,
)

View File

@ -1,103 +0,0 @@
import React from 'react';
import {
ArrowRight,
Download
} from 'lucide-react';
import { ViewMode, Project, Step } from '../types';
import { projectsList } from '../data/mockData';
import { ProgressBar } from '../components/ProgressBar';
interface DashboardViewProps {
setCurrentView: (view: ViewMode) => void;
setCurrentStep?: (step: Step) => void;
}
export const DashboardView: React.FC<DashboardViewProps> = ({ setCurrentView, setCurrentStep }) => (
<div className="p-8 bg-slate-50 min-h-screen animate-fade-in">
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl font-bold text-slate-900"></h1>
<p className="text-slate-500 text-sm mt-1">, Sarah () | </p>
</div>
<div className="flex space-x-4">
<button className="flex items-center px-4 py-2 bg-white border border-slate-200 rounded-md shadow-sm text-sm font-medium text-slate-600 hover:bg-slate-50">
<Download size={16} className="mr-2" />
</button>
</div>
</div>
<div className="grid grid-cols-12 gap-8">
<div className="col-span-12 bg-white rounded-lg border border-slate-200 shadow-sm p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="font-bold text-lg text-slate-800"></h3>
<button onClick={() => setCurrentView('projects')} className="text-blue-600 text-sm font-medium hover:underline flex items-center">
<ArrowRight size={14} className="ml-1"/>
</button>
</div>
<div className="overflow-hidden">
<table className="min-w-full text-left text-sm">
<thead className="bg-slate-50 text-slate-500 font-medium">
<tr>
<th className="px-4 py-3"></th>
<th className="px-4 py-3"></th>
<th className="px-4 py-3 w-1/3"> & </th>
<th className="px-4 py-3 text-right"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{projectsList.slice(0, 4).map((project, idx) => (
<tr
key={idx}
className="hover:bg-slate-50 transition-colors cursor-pointer"
onClick={() => {
setCurrentView('engagement');
// Set step based on project progress (simplified logic)
if (setCurrentStep) {
if (project.progress === 0) setCurrentStep('setup');
else if (project.progress < 25) setCurrentStep('inventory');
else if (project.progress < 50) setCurrentStep('context');
else if (project.progress < 75) setCurrentStep('value');
else setCurrentStep('delivery');
}
}}
>
<td className="px-4 py-4 font-medium text-slate-800">{project.name}</td>
<td className="px-4 py-4 text-slate-600 flex items-center">
<div className="w-6 h-6 rounded-full bg-slate-200 flex items-center justify-center text-xs mr-2 font-bold text-slate-600">
{project.owner[0]}
</div>
{project.owner}
</td>
<td className="px-4 py-4">
<div className="flex items-center justify-between mb-1 text-xs text-slate-500">
<span></span>
<span>{project.progress}%</span>
</div>
<ProgressBar percent={project.progress} status={project.status} />
</td>
<td className="px-4 py-4 text-right" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => {
setCurrentView('engagement');
if (setCurrentStep) {
if (project.progress === 0) setCurrentStep('setup');
else if (project.progress < 25) setCurrentStep('inventory');
else if (project.progress < 50) setCurrentStep('context');
else if (project.progress < 75) setCurrentStep('value');
else setCurrentStep('delivery');
}
}}
className="text-blue-600 hover:text-blue-800 font-medium text-xs"
>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);

View File

@ -1,200 +0,0 @@
import React, { useState } from 'react';
import {
ArrowLeft,
ChevronRight,
Sparkles,
EyeOff,
CheckCircle2
} from 'lucide-react';
import { ViewMode, Step, InventoryMode, Scenario } from '../types';
import { SetupStep } from './engagement/SetupStep';
import { InventoryStep } from './engagement/InventoryStep';
import { ContextStep } from './engagement/ContextStep';
import { ValueStep } from './engagement/ValueStep';
import { DeliveryStep } from './engagement/DeliveryStep';
import { scenarioData as initialScenarioData } from '../data/mockData';
interface EngagementViewProps {
currentStep: Step;
setCurrentStep: (step: Step) => void;
setCurrentView: (view: ViewMode) => void;
isPresentationMode: boolean;
setIsPresentationMode: (mode: boolean) => void;
}
export const EngagementView: React.FC<EngagementViewProps> = ({
currentStep,
setCurrentStep,
setCurrentView,
isPresentationMode,
setIsPresentationMode,
}) => {
const [inventoryMode, setInventoryMode] = useState<InventoryMode>('selection');
// Initialize with scenarios that are marked as selected in mock data
const [selectedScenarios, setSelectedScenarios] = useState<Scenario[]>(
initialScenarioData.filter(s => s.selected).map(s => ({ ...s }))
);
// Toggle scenario selection
const toggleScenarioSelection = (scenarioId: number) => {
setSelectedScenarios(prev => {
const isSelected = prev.some(s => s.id === scenarioId);
if (isSelected) {
// Remove from selection
return prev.filter(s => s.id !== scenarioId);
} else {
// Add to selection - get full scenario data from initial data
const scenario = initialScenarioData.find(s => s.id === scenarioId);
if (!scenario) return prev;
return [...prev, { ...scenario, selected: true }];
}
});
};
// Stepper Configuration
const steps = [
{ id: 'inventory', label: '上传数据资源表' },
{ id: 'context', label: '背景调研' },
{ id: 'value', label: '识别场景' },
{ id: 'delivery', label: '盘点报告' },
];
const currentStepIndex = steps.findIndex(s => s.id === currentStep);
// Calculate overall progress percentage
const overallProgress = ((currentStepIndex + 1) / steps.length) * 100;
return (
<div className={`bg-slate-50 h-full flex flex-col ${isPresentationMode ? 'p-0' : 'p-6'} overflow-hidden`}>
{/* Project Header */}
{!isPresentationMode && (
<div className="mb-6 flex items-center justify-between">
<div>
{/* Back to Project List Navigation */}
<div className="flex items-center text-sm text-slate-500 mb-1 cursor-pointer hover:text-blue-600 transition-colors" onClick={() => setCurrentView('projects')}>
<ArrowLeft size={14} className="mr-1" />
<span></span>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex -space-x-2">
<div className="w-8 h-8 rounded-full bg-blue-100 border-2 border-white flex items-center justify-center text-xs font-bold text-blue-800" title="Project Manager">PM</div>
<div className="w-8 h-8 rounded-full bg-purple-100 border-2 border-white flex items-center justify-center text-xs font-bold text-purple-800" title="Data Analyst">DA</div>
<div className="w-8 h-8 rounded-full bg-gray-100 border-2 border-white flex items-center justify-center text-xs text-gray-500">+2</div>
</div>
<div className="h-6 w-px bg-slate-300"></div>
<button
onClick={() => setIsPresentationMode(true)}
className="flex items-center px-4 py-2 bg-slate-900 text-white rounded-md text-sm hover:bg-slate-700 shadow-lg transition-all"
>
<Sparkles size={16} className="mr-2" />
</button>
</div>
</div>
)}
{/* Stepper */}
<div className={`bg-white border border-slate-200 rounded-lg mb-6 ${isPresentationMode ? 'hidden' : 'block'}`}>
{/* Progress Header */}
<div className="px-4 pt-4 pb-2 flex items-center justify-between border-b border-slate-100">
<span className="text-xs font-medium text-slate-500 uppercase tracking-wider"></span>
<span className="text-sm font-bold text-blue-600">{Math.round(overallProgress)}%</span>
</div>
<div className="px-4 py-2">
<div className="w-full bg-slate-100 rounded-full h-1.5 mb-2">
<div
className="bg-blue-600 h-1.5 rounded-full transition-all duration-500 ease-out"
style={{ width: `${overallProgress}%` }}
></div>
</div>
</div>
<div className="flex items-center p-4">
{steps.map((step, idx) => {
const isActive = step.id === currentStep;
const isCompleted = idx < currentStepIndex;
return (
<div key={step.id} className="flex items-center flex-1">
<div
onClick={() => {
setCurrentStep(step.id as Step);
// Reset inventory mode if returning to that step
if (step.id === 'inventory' && inventoryMode === 'results') {
// keep results
} else if (step.id === 'inventory') {
setInventoryMode('selection');
}
}}
className={`flex items-center cursor-pointer group ${idx === steps.length - 1 ? 'flex-none' : 'w-full'}`}
>
<div className="relative">
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold border-2 transition-colors ${
isActive ? 'border-blue-600 bg-blue-600 text-white' :
isCompleted ? 'border-green-500 bg-green-500 text-white' : 'border-slate-200 text-slate-400 bg-slate-50'
}`}>
{isCompleted ? <CheckCircle2 size={16} /> : idx + 1}
</div>
</div>
<span className={`ml-3 text-sm font-medium ${isActive ? 'text-slate-900' : 'text-slate-500'}`}>
{step.label}
</span>
{idx !== steps.length - 1 && (
<div className={`flex-1 h-0.5 mx-4 relative ${isCompleted ? 'bg-green-500' : 'bg-slate-100'}`}>
{isCompleted && (
<div className="absolute inset-0 bg-green-500"></div>
)}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
{/* 演示模式退出按钮 */}
{isPresentationMode && (
<button
onClick={() => setIsPresentationMode(false)}
className="fixed top-4 right-4 z-50 bg-white/90 backdrop-blur text-slate-800 px-4 py-2 rounded-full shadow-lg font-medium text-sm flex items-center hover:bg-white"
>
<EyeOff size={16} className="mr-2"/> 退
</button>
)}
{/* Main Workspace Area */}
<div className="flex-1 bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden flex flex-col">
{currentStep === 'setup' && (
<SetupStep
setCurrentStep={setCurrentStep}
setInventoryMode={setInventoryMode}
/>
)}
{currentStep === 'inventory' && (
<InventoryStep
inventoryMode={inventoryMode}
setInventoryMode={setInventoryMode}
setCurrentStep={setCurrentStep}
/>
)}
{currentStep === 'context' && (
<ContextStep setCurrentStep={setCurrentStep} />
)}
{currentStep === 'value' && (
<ValueStep
setCurrentStep={setCurrentStep}
selectedScenarios={selectedScenarios}
allScenarios={initialScenarioData}
toggleScenarioSelection={toggleScenarioSelection}
/>
)}
{currentStep === 'delivery' && (
<DeliveryStep selectedScenarios={selectedScenarios} />
)}
</div>
</div>
);
};

View File

@ -9,7 +9,7 @@
<!-- Back to Project List Navigation --> <!-- Back to Project List Navigation -->
<div <div
class="flex items-center text-sm text-slate-500 mb-1 cursor-pointer hover:text-blue-600 transition-colors" class="flex items-center text-sm text-slate-500 mb-1 cursor-pointer hover:text-blue-600 transition-colors"
@click="setCurrentView('projects')" @click="goBackToList"
> >
<ArrowLeft :size="14" class="mr-1" /> <ArrowLeft :size="14" class="mr-1" />
<span>返回项目列表</span> <span>返回项目列表</span>
@ -23,7 +23,7 @@
</div> </div>
<div class="h-6 w-px bg-slate-300"></div> <div class="h-6 w-px bg-slate-300"></div>
<button <button
@click="setIsPresentationMode(true)" @click="() => setIsPresentationMode(true)"
class="flex items-center px-4 py-2 bg-slate-900 text-white rounded-md text-sm hover:bg-slate-700 shadow-lg transition-all" class="flex items-center px-4 py-2 bg-slate-900 text-white rounded-md text-sm hover:bg-slate-700 shadow-lg transition-all"
> >
<Sparkles :size="16" class="mr-2" /> 演示模式 <Sparkles :size="16" class="mr-2" /> 演示模式
@ -108,17 +108,17 @@
v-if="currentStep === 'inventory'" v-if="currentStep === 'inventory'"
:inventory-mode="inventoryMode" :inventory-mode="inventoryMode"
:set-inventory-mode="setInventoryMode" :set-inventory-mode="setInventoryMode"
:set-current-step="setCurrentStep" :set-current-step="handleStepClick"
/> />
<ContextStep <ContextStep
v-if="currentStep === 'context'" v-if="currentStep === 'context'"
:set-current-step="setCurrentStep" :set-current-step="handleStepClick"
/> />
<ValueStep <ValueStep
v-if="currentStep === 'value'" v-if="currentStep === 'value'"
:set-current-step="setCurrentStep" :set-current-step="handleStepClick"
:selected-scenarios="selectedScenarios" :selected-scenarios="selectedScenarios"
:all-scenarios="initialScenarioData" :all-scenarios="initialScenarioData"
:toggle-scenario-selection="toggleScenarioSelection" :toggle-scenario-selection="toggleScenarioSelection"
@ -133,24 +133,29 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import { ArrowLeft, ChevronRight, Sparkles, EyeOff, CheckCircle2 } from 'lucide-vue-next'; import { useRouter, useRoute } from 'vue-router';
import { ArrowLeft, Sparkles, EyeOff, CheckCircle2 } from 'lucide-vue-next';
import InventoryStep from './engagement/InventoryStep.vue'; import InventoryStep from './engagement/InventoryStep.vue';
import ContextStep from './engagement/ContextStep.vue'; import ContextStep from './engagement/ContextStep.vue';
import ValueStep from './engagement/ValueStep.vue'; import ValueStep from './engagement/ValueStep.vue';
import DeliveryStep from './engagement/DeliveryStep.vue'; import DeliveryStep from './engagement/DeliveryStep.vue';
import { scenarioData as initialScenarioData } from '@/data/mockData'; import { scenarioData as initialScenarioData } from '@/data/mockData';
import type { ViewMode, Step, InventoryMode, Scenario } from '@/types'; import type { Step, InventoryMode, Scenario } from '@/types';
interface Props { const router = useRouter();
currentStep: Step; const route = useRoute();
setCurrentStep: (step: Step) => void;
setCurrentView: (view: ViewMode) => void;
isPresentationMode: boolean;
setIsPresentationMode: (mode: boolean) => void;
}
const props = defineProps<Props>(); // Get step from route query, default to 'inventory'
const currentStep = ref<Step>((route.query.step as Step) || 'inventory');
const isPresentationMode = ref(false);
// Watch route query changes to update current step
watch(() => route.query.step, (newStep) => {
if (newStep && typeof newStep === 'string') {
currentStep.value = newStep as Step;
}
});
const inventoryMode = ref<InventoryMode>('selection'); const inventoryMode = ref<InventoryMode>('selection');
const selectedScenarios = ref<Scenario[]>( const selectedScenarios = ref<Scenario[]>(
@ -184,18 +189,23 @@ const steps = [
]; ];
const currentStepIndex = computed(() => const currentStepIndex = computed(() =>
steps.findIndex(s => s.id === props.currentStep) steps.findIndex(s => s.id === currentStep.value)
); );
const overallProgress = computed(() => const overallProgress = computed(() =>
((currentStepIndex.value + 1) / steps.length) * 100 ((currentStepIndex.value + 1) / steps.length) * 100
); );
const isStepActive = (stepId: string) => stepId === props.currentStep; const isStepActive = (stepId: string) => stepId === currentStep.value;
const isStepCompleted = (idx: number) => idx < currentStepIndex.value; const isStepCompleted = (idx: number) => idx < currentStepIndex.value;
const handleStepClick = (stepId: Step) => { const handleStepClick = (stepId: Step) => {
props.setCurrentStep(stepId); currentStep.value = stepId;
// Update route query
router.push({
name: 'dataInventoryEngagement',
query: { ...route.query, step: stepId }
});
// Reset inventory mode if returning to that step // Reset inventory mode if returning to that step
if (stepId === 'inventory' && inventoryMode.value === 'results') { if (stepId === 'inventory' && inventoryMode.value === 'results') {
// keep results // keep results
@ -203,4 +213,12 @@ const handleStepClick = (stepId: Step) => {
inventoryMode.value = 'selection'; inventoryMode.value = 'selection';
} }
}; };
const goBackToList = () => {
router.push({ name: 'dataInventoryList' });
};
const setIsPresentationMode = (mode: boolean) => {
isPresentationMode.value = mode;
};
</script> </script>

View File

@ -1,182 +0,0 @@
import React, { useState } from 'react';
import {
Search,
Plus,
Settings,
Layers
} from 'lucide-react';
import { ViewMode, Step, Project } from '../types';
import { projectsList } from '../data/mockData';
import { ConfirmDialog } from '../components/ConfirmDialog';
import { useToast } from '../components/Toast';
// 当前用户信息可以从用户store或API获取
const CURRENT_USER_NAME = 'Sarah Jenkins';
interface ProjectListViewProps {
setCurrentView: (view: ViewMode) => void;
setCurrentStep: (step: Step) => void;
}
export const ProjectListView: React.FC<ProjectListViewProps> = ({ setCurrentView, setCurrentStep }) => {
const [deleteDialogVisible, setDeleteDialogVisible] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<Project | null>(null);
const toast = useToast();
const handleDeleteProject = (project: Project) => {
setProjectToDelete(project);
setDeleteDialogVisible(true);
};
const confirmDelete = () => {
if (projectToDelete) {
// 这里应该调用API删除项目目前只是模拟
const index = projectsList.findIndex(p => p.id === projectToDelete.id);
if (index > -1) {
projectsList.splice(index, 1);
toast.success(`项目 "${projectToDelete.name}" 已删除`);
}
setProjectToDelete(null);
setDeleteDialogVisible(false);
}
};
const cancelDelete = () => {
setProjectToDelete(null);
setDeleteDialogVisible(false);
};
return (
<div className="p-8 bg-slate-50 min-h-screen animate-fade-in">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl font-bold text-slate-900"> (Projects)</h1>
<p className="text-slate-500 text-sm mt-1"></p>
</div>
<div className="flex space-x-4">
<div className="relative">
<Search size={18} className="absolute left-3 top-2.5 text-slate-400" />
<input type="text" placeholder="搜索项目、客户..." className="pl-10 pr-4 py-2 bg-white border border-slate-200 rounded-md text-sm w-64 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" />
</div>
{/* New Project Button -> Goes to Engagement Setup */}
<button
onClick={() => {
setCurrentStep('setup');
setCurrentView('engagement');
}}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-md shadow-sm text-sm font-bold hover:bg-blue-700 transition-colors"
>
<Plus size={18} className="mr-2" />
</button>
</div>
</div>
{/* Projects Table */}
<div className="bg-white rounded-lg border border-slate-200 shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
<div className="flex space-x-4">
<span className="text-sm font-bold text-slate-800 border-b-2 border-blue-500 pb-1 cursor-pointer"> (12)</span>
<span className="text-sm font-medium text-slate-500 hover:text-slate-700 cursor-pointer"></span>
<span className="text-sm font-medium text-slate-500 hover:text-slate-700 cursor-pointer"></span>
</div>
<button className="text-slate-400 hover:text-slate-600"><Settings size={16} /></button>
</div>
<table className="min-w-full text-left text-sm">
<thead className="bg-slate-50 text-slate-500 font-medium">
<tr>
<th className="px-6 py-3"></th>
<th className="px-6 py-3"></th>
<th className="px-6 py-3"></th>
<th className="px-6 py-3"></th>
<th className="px-6 py-3 text-right"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{projectsList.map((project) => (
<tr
key={project.id}
className="hover:bg-slate-50 transition-colors group cursor-pointer"
onClick={(e) => {
// Don't trigger if clicking on action buttons
if ((e.target as HTMLElement).closest('button')) return;
setCurrentView('engagement');
// Set step based on project progress (simplified logic)
if (project.progress === 0) setCurrentStep('setup');
else if (project.progress < 25) setCurrentStep('inventory');
else if (project.progress < 50) setCurrentStep('context');
else if (project.progress < 75) setCurrentStep('value');
else setCurrentStep('delivery');
}}
>
<td className="px-6 py-4">
<div className="flex items-center">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center text-blue-600 mr-3">
<Layers size={16}/>
</div>
<div className="font-bold text-slate-800">{project.name}</div>
</div>
</td>
<td className="px-6 py-4 text-slate-600">
{project.client}
</td>
<td className="px-6 py-4 text-slate-600">
<div className="flex items-center">
<div className="w-5 h-5 rounded-full bg-slate-200 text-xs flex items-center justify-center mr-2">{CURRENT_USER_NAME[0]}</div>
{CURRENT_USER_NAME}
</div>
</td>
<td className="px-6 py-4 text-slate-400 text-xs">
{project.lastUpdate}
</td>
<td className="px-6 py-4 text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-end space-x-3">
<button
onClick={() => {
setCurrentView('engagement');
// Set step based on project progress
if (project.progress === 0) setCurrentStep('setup');
else if (project.progress < 25) setCurrentStep('inventory');
else if (project.progress < 50) setCurrentStep('context');
else if (project.progress < 75) setCurrentStep('value');
else setCurrentStep('delivery');
}}
className="text-blue-600 hover:text-blue-800 font-medium text-xs"
>
</button>
<button
onClick={() => handleDeleteProject(project)}
className="text-red-600 hover:text-red-800 font-medium text-xs"
>
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination Mockup */}
<div className="px-6 py-4 border-t border-slate-100 flex items-center justify-between bg-slate-50/30">
<span className="text-xs text-slate-500"> 1-5 12 </span>
<div className="flex space-x-1">
<button className="px-3 py-1 border border-slate-200 rounded text-xs text-slate-600 hover:bg-slate-100 disabled:opacity-50"></button>
<button className="px-3 py-1 border border-slate-200 rounded text-xs text-slate-600 hover:bg-slate-100"></button>
</div>
</div>
</div>
{/* 删除确认弹窗 */}
<ConfirmDialog
visible={deleteDialogVisible}
title={projectToDelete ? `确认删除项目 "${projectToDelete.name}"` : '确认删除'}
message="删除后数据将无法恢复,确定要继续吗?"
onConfirm={confirmDelete}
onCancel={cancelDelete}
/>
</div>
);
};

View File

@ -64,10 +64,7 @@
{{ project.client }} {{ project.client }}
</td> </td>
<td class="px-6 py-4 text-slate-600"> <td class="px-6 py-4 text-slate-600">
<div class="flex items-center"> 管理员
<div class="w-5 h-5 rounded-full bg-slate-200 text-xs flex items-center justify-center mr-2">{{ currentUserName[0] }}</div>
{{ currentUserName }}
</div>
</td> </td>
<td class="px-6 py-4 text-right" @click.stop> <td class="px-6 py-4 text-right" @click.stop>
<div class="flex items-center justify-end space-x-3"> <div class="flex items-center justify-end space-x-3">
@ -119,24 +116,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { Search, Plus, Settings, Layers } from 'lucide-vue-next'; import { Search, Plus, Settings, Layers } from 'lucide-vue-next';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { useToastStore } from '@/stores/toast'; import { useToastStore } from '@/stores/toast';
import ConfirmDialog from '@/components/ConfirmDialog.vue'; import ConfirmDialog from '@/components/ConfirmDialog.vue';
import NewProjectDialog from '@/components/NewProjectDialog.vue'; import NewProjectDialog from '@/components/NewProjectDialog.vue';
import { projectsList } from '@/data/mockData'; import { projectsList } from '@/data/mockData';
import type { ViewMode, Step, Project } from '@/types'; import type { Project } from '@/types';
interface Props { const router = useRouter();
setCurrentView: (view: ViewMode) => void;
setCurrentStep: (step: Step) => void;
}
const props = defineProps<Props>();
const userStore = useUserStore(); const userStore = useUserStore();
const toast = useToastStore(); const toast = useToastStore();
const currentUserName = ref(userStore.getCurrentUser().name); const currentUserName = ref('管理员');
const deleteDialogVisible = ref(false); const deleteDialogVisible = ref(false);
const projectToDelete = ref<Project | null>(null); const projectToDelete = ref<Project | null>(null);
const newProjectDialogVisible = ref(false); const newProjectDialogVisible = ref(false);
@ -182,9 +175,11 @@ const handleProjectClick = (project: Project, event?: Event) => {
} }
} }
// Go to engagement view with inventory step (first step after removing setup) // Navigate to engagement view with inventory step
props.setCurrentView('engagement'); router.push({
props.setCurrentStep('inventory'); name: 'dataInventoryEngagement',
query: { step: 'inventory', projectId: project.id.toString() }
});
}; };
const handleDeleteProject = (project: Project) => { const handleDeleteProject = (project: Project) => {

View File

@ -1,142 +0,0 @@
import React from 'react';
import {
Server,
ClipboardList,
Plus,
XCircle,
Image as ImageIcon,
Sparkles
} from 'lucide-react';
import { Step } from '../../types';
import { useToast } from '../../contexts/ToastContext';
interface ContextStepProps {
setCurrentStep: (step: Step) => void;
}
export const ContextStep: React.FC<ContextStepProps> = ({ setCurrentStep }) => {
const toast = useToast();
return (
<div className="flex-1 p-8 bg-slate-50 overflow-y-auto animate-fade-in flex justify-center min-h-0">
<div className="max-w-4xl w-full h-full bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col">
<div className="p-6 border-b border-slate-100 flex-none">
<h3 className="text-xl font-bold text-slate-900"> (Business Context)</h3>
<p className="text-sm text-slate-500 mt-1">AI </p>
</div>
<div className="flex-1 overflow-y-auto p-8 space-y-8">
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">
<label className="block text-sm font-bold text-slate-700 mb-2"></label>
<textarea className="w-full p-3 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none" rows={3} placeholder="例如某连锁生鲜零售企业主营水果、蔬菜、肉禽蛋奶等生鲜产品拥有线下门店500家..."></textarea>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2"></label>
<select className="w-full p-3 border border-slate-300 rounded-lg text-sm bg-white">
<option> - </option>
<option> - </option>
<option> - </option>
</select>
</div>
<div>
<label className="block text-sm font-bold text-slate-700 mb-2"> ()</label>
<div className="flex">
<input type="text" className="flex-1 p-3 border border-slate-300 rounded-l-lg text-sm" placeholder="100"/>
<select className="p-3 border-y border-r border-slate-300 rounded-r-lg text-sm bg-slate-50 text-slate-600 w-24">
<option>GB</option>
<option>TB</option>
<option>PB</option>
<option>亿</option>
</select>
</div>
</div>
</div>
<div className="bg-slate-50 p-6 rounded-lg border border-slate-200">
<h4 className="text-sm font-bold text-slate-800 mb-4 flex items-center">
<Server size={16} className="mr-2 text-blue-500"/>
</h4>
<div className="grid grid-cols-2 gap-6">
<div>
<label className="block text-xs font-bold text-slate-500 mb-2 uppercase"> ()</label>
<div className="space-y-2">
{['内部业务系统 (ERP/CRM/POS)', '外部采购/合作', '网络爬虫/公开数据', 'IoT 设备采集'].map(opt => (
<label key={opt} className="flex items-center space-x-2 cursor-pointer">
<input type="checkbox" className="rounded text-blue-600 focus:ring-blue-500"/>
<span className="text-sm text-slate-700">{opt}</span>
</label>
))}
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-xs font-bold text-slate-500 mb-2 uppercase"></label>
<input type="text" className="w-full p-2 border border-slate-300 rounded text-sm" placeholder="例如使用金蝶云星空作为核心ERP..."/>
</div>
<div>
<label className="block text-xs font-bold text-slate-500 mb-2 uppercase"></label>
<div className="flex space-x-4">
{['公有云', '私有云/本地', '混合部署'].map(opt => (
<label key={opt} className="flex items-center space-x-2">
<input type="radio" name="storage" className="text-blue-600 focus:ring-blue-500"/>
<span className="text-sm text-slate-700">{opt}</span>
</label>
))}
</div>
</div>
</div>
</div>
</div>
<div className="border border-blue-100 bg-blue-50/30 p-6 rounded-lg">
<div className="flex justify-between items-center mb-4">
<h4 className="text-sm font-bold text-slate-800 flex items-center">
<ClipboardList size={16} className="mr-2 text-blue-500"/>
()
</h4>
<button className="text-xs text-blue-600 hover:text-blue-800 font-medium flex items-center">
<Plus size={14} className="mr-1"/>
</button>
</div>
<div className="space-y-3">
<div className="flex items-start space-x-3 p-3 bg-white border border-slate-200 rounded-md shadow-sm">
<div className="flex-1 grid grid-cols-2 gap-4">
<input type="text" className="p-2 border border-slate-200 rounded text-sm" placeholder="场景名称 (如: 月度销售报表)" defaultValue="月度销售经营报表" />
<input type="text" className="p-2 border border-slate-200 rounded text-sm" placeholder="简述 (如: 统计各门店销售额)" defaultValue="统计各区域门店的月度GMV维度单一" />
</div>
<button className="p-2 text-slate-400 hover:text-red-500"><XCircle size={18}/></button>
</div>
<div className="flex items-start space-x-3 p-3 bg-white border border-slate-200 rounded-md shadow-sm">
<div className="flex-1 grid grid-cols-2 gap-4">
<input type="text" className="p-2 border border-slate-200 rounded text-sm" placeholder="场景名称 (如: 月度销售报表)" defaultValue="物流配送监控大屏" />
<input type="text" className="p-2 border border-slate-200 rounded text-sm" placeholder="简述 (如: 统计各门店销售额)" defaultValue="实时展示车辆位置,但缺乏预警功能" />
</div>
<button className="p-2 text-slate-400 hover:text-red-500"><XCircle size={18}/></button>
</div>
<div className="mt-4 border-2 border-dashed border-slate-300 rounded-lg p-4 text-center cursor-pointer hover:bg-white transition-colors">
<p className="text-xs text-slate-500 flex items-center justify-center">
<ImageIcon size={14} className="mr-2"/> / ( AI )
</p>
</div>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50 flex justify-end flex-none">
<button
onClick={() => {
toast.success('背景调研信息已保存,开始生成场景挖掘建议');
setCurrentStep('value');
}}
className="px-8 py-3 bg-slate-900 text-white rounded-lg font-bold shadow-lg hover:bg-slate-800 transition-all flex items-center"
>
<Sparkles size={18} className="mr-2"/>
</button>
</div>
</div>
</div>
);
};

View File

@ -215,7 +215,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, computed } from 'vue'; import { ref, watch } from 'vue';
import { import {
Server, Server,
ClipboardList, ClipboardList,

View File

@ -1,407 +0,0 @@
import React, { useState } from 'react';
import {
PackageCheck,
FileText,
Database,
FileSpreadsheet,
TrendingUp,
Download,
Eye,
ArrowLeft
} from 'lucide-react';
import { Scenario } from '../../types';
import { legacyOptimizationData } from '../../data/mockData';
type ReportType = 'summary' | 'inventory' | 'legacy-optimization' | 'potential-scenarios' | null;
interface DeliveryStepProps {
selectedScenarios: Scenario[];
}
export const DeliveryStep: React.FC<DeliveryStepProps> = ({ selectedScenarios }) => {
const [viewingReport, setViewingReport] = useState<ReportType>(null);
const [viewingInventoryId, setViewingInventoryId] = useState<number | null>(null);
// Report Detail View
if (viewingReport) {
return (
<div className="flex flex-col h-full bg-white">
<div className="p-6 border-b border-slate-200 flex items-center justify-between flex-none">
<div className="flex items-center">
<button
onClick={() => {
setViewingReport(null);
setViewingInventoryId(null);
}}
className="mr-4 text-slate-400 hover:text-slate-600"
>
<ArrowLeft size={20}/>
</button>
<h2 className="text-xl font-bold text-slate-900">
{viewingReport === 'summary' && '整体数据资产盘点工作总结'}
{viewingReport === 'inventory' && viewingInventoryId && `数据资产目录 - ${selectedScenarios.find(s => s.id === viewingInventoryId)?.name}`}
{viewingReport === 'legacy-optimization' && '存量数据应用场景优化建议'}
{viewingReport === 'potential-scenarios' && '潜在数据应用场景评估'}
</h2>
</div>
<button
onClick={() => {
// TODO: Implement download functionality based on report type
const reportName =
viewingReport === 'summary' ? '整体数据资产盘点工作总结' :
viewingReport === 'inventory' ? `数据资产目录-${selectedScenarios.find(s => s.id === viewingInventoryId)?.name}` :
viewingReport === 'legacy-optimization' ? '存量数据应用场景优化建议' :
'潜在数据应用场景评估';
console.log(`Download PDF: ${reportName}`);
}}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Download size={16} className="mr-2" />
PDF
</button>
</div>
<div className="flex-1 overflow-y-auto p-8 bg-slate-50">
<div className="max-w-4xl mx-auto bg-white rounded-lg shadow-sm border border-slate-200 p-8">
{viewingReport === 'summary' && (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold text-slate-900 mb-4"></h3>
<p className="text-slate-700 leading-relaxed">
AI
PII
</p>
</div>
<div>
<h3 className="text-xl font-bold text-slate-900 mb-3"></h3>
<ul className="space-y-2 text-slate-700">
<li className="flex items-start">
<span className="text-blue-600 mr-2"></span>
<span> <strong>4 </strong> <strong>2,400 +</strong> </span>
</li>
<li className="flex items-start">
<span className="text-blue-600 mr-2"></span>
<span> PII <strong>3 </strong></span>
</li>
<li className="flex items-start">
<span className="text-blue-600 mr-2"></span>
<span> <strong>1 </strong></span>
</li>
<li className="flex items-start">
<span className="text-blue-600 mr-2"></span>
<span> <strong>{selectedScenarios.length} </strong></span>
</li>
</ul>
</div>
<div>
<h3 className="text-xl font-bold text-slate-900 mb-3"></h3>
<p className="text-slate-700 leading-relaxed">
</p>
</div>
</div>
)}
{viewingReport === 'inventory' && viewingInventoryId && (() => {
const scenario = selectedScenarios.find(s => s.id === viewingInventoryId);
if (!scenario) return null;
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold text-slate-900 mb-4"></h3>
<p className="text-slate-600 mb-6"></p>
</div>
<div className="border border-slate-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-bold text-slate-900">{scenario.name}</h4>
<span className="px-3 py-1 bg-blue-100 text-blue-700 text-xs font-bold rounded-full">
{scenario.type}
</span>
</div>
<p className="text-slate-600 mb-4">{scenario.desc}</p>
<div>
<p className="text-sm font-bold text-slate-700 mb-2"></p>
<div className="flex flex-wrap gap-2">
{scenario.dependencies.map((dep, i) => (
<span key={i} className="px-2 py-1 bg-slate-100 text-slate-700 text-xs rounded border border-slate-200">
{dep}
</span>
))}
</div>
</div>
</div>
</div>
);
})()}
{viewingReport === 'legacy-optimization' && (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold text-slate-900 mb-4"></h3>
<p className="text-slate-600 mb-6">
Step 3 AI
</p>
</div>
<div className="space-y-6">
{legacyOptimizationData.map((item) => (
<div key={item.id} className="border border-slate-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-xl font-bold text-slate-900">{item.title}</h4>
<span className={`px-3 py-1 rounded text-xs font-bold ${
item.impact === '高' ? 'bg-amber-100 text-amber-700' : 'bg-blue-100 text-blue-700'
}`}>
{item.impact}
</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-red-50 p-4 rounded-lg border border-red-100">
<p className="text-xs font-bold text-red-700 mb-2"></p>
<p className="text-sm text-slate-700">{item.issue}</p>
</div>
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
<p className="text-xs font-bold text-green-700 mb-2">AI </p>
<p className="text-sm text-slate-700">{item.suggestion}</p>
</div>
</div>
</div>
))}
</div>
</div>
)}
{viewingReport === 'potential-scenarios' && (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold text-slate-900 mb-4"></h3>
<p className="text-slate-600 mb-6">
AI
</p>
</div>
<div className="space-y-4">
{selectedScenarios.map((scenario) => (
<div key={scenario.id} className="border border-slate-200 rounded-lg p-6">
<div className="flex items-center justify-between mb-3">
<h4 className="text-lg font-bold text-slate-900">{scenario.name}</h4>
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-slate-500 border border-slate-200 px-2 py-1 rounded">
{scenario.type}
</span>
<span className={`text-xs font-bold px-2 py-1 rounded ${
scenario.impact === 'High' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
{scenario.impact} Impact
</span>
</div>
</div>
<p className="text-slate-600 mb-4">{scenario.desc}</p>
<div>
<p className="text-sm font-bold text-slate-700 mb-2"></p>
<div className="flex flex-wrap gap-2">
{scenario.dependencies.map((dep, i) => (
<span key={i} className="px-2 py-1 bg-slate-100 text-slate-700 text-xs rounded border border-slate-200">
{dep}
</span>
))}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
}
// Main Delivery View
return (
<div className="flex flex-col h-full bg-slate-50 overflow-y-auto animate-fade-in p-8 min-h-0">
<div className="text-center mb-10">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center text-green-600 mx-auto mb-4 shadow-sm">
<PackageCheck size={36} />
</div>
<h2 className="text-3xl font-bold text-slate-900"></h2>
<p className="text-slate-500 mt-2"></p>
</div>
<div className="max-w-5xl mx-auto w-full space-y-6 mb-12">
{/* 整体数据资产盘点工作总结 */}
<div className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex items-center flex-1">
<div className="p-3 bg-blue-100 rounded-lg text-blue-600 mr-4">
<FileText size={24}/>
</div>
<div className="flex-1">
<h3 className="font-bold text-slate-900 text-lg mb-1"></h3>
<p className="text-sm text-slate-500"></p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setViewingReport('summary')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center"
>
<Eye size={16} className="mr-2" />
线
</button>
<button
onClick={(e) => {
e.stopPropagation();
// TODO: Implement download functionality
console.log('Download: 整体数据资产盘点工作总结');
}}
className="p-2 text-slate-400 hover:text-blue-600 transition-colors"
title="下载 PDF"
>
<Download size={18} />
</button>
</div>
</div>
</div>
{/* 数据资产目录 - 多个 */}
<div>
<h3 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4 flex items-center">
<Database size={16} className="mr-2"/>
</h3>
{selectedScenarios.length === 0 ? (
<div className="bg-slate-50 border border-slate-200 rounded-lg p-6 text-center">
<p className="text-sm text-slate-500"></p>
</div>
) : (
<div className="space-y-4">
{selectedScenarios.map((scenario, idx) => (
<div key={scenario.id} className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex items-center flex-1">
<div className="p-3 bg-green-50 rounded-lg text-green-600 mr-4">
<FileSpreadsheet size={24}/>
</div>
<div className="flex-1">
<h4 className="font-bold text-slate-900 mb-1"> - {scenario.name}</h4>
<p className="text-xs text-slate-500"></p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
setViewingInventoryId(scenario.id);
setViewingReport('inventory');
}}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center"
>
<Eye size={16} className="mr-2" />
线
</button>
<button
onClick={(e) => {
e.stopPropagation();
console.log(`Download: 数据资产目录-${scenario.name}`);
}}
className="p-2 text-slate-400 hover:text-green-600 transition-colors"
title="下载 Excel"
>
<Download size={18} />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* 存量数据应用场景优化建议 */}
<div className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex items-center flex-1">
<div className="p-3 bg-amber-50 rounded-lg text-amber-600 mr-4">
<TrendingUp size={24}/>
</div>
<div className="flex-1">
<h3 className="font-bold text-slate-900 text-lg mb-1"></h3>
<p className="text-sm text-slate-500"> AI </p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setViewingReport('legacy-optimization')}
className="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 flex items-center"
>
<Eye size={16} className="mr-2" />
线
</button>
<button
onClick={(e) => {
e.stopPropagation();
console.log('Download: 存量数据应用场景优化建议');
}}
className="p-2 text-slate-400 hover:text-amber-600 transition-colors"
title="下载 PDF"
>
<Download size={18} />
</button>
</div>
</div>
</div>
{/* 潜在数据应用场景评估 */}
{selectedScenarios.length > 0 && (
<div className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex items-center flex-1">
<div className="p-3 bg-purple-50 rounded-lg text-purple-600 mr-4">
<TrendingUp size={24}/>
</div>
<div className="flex-1">
<h3 className="font-bold text-slate-900 text-lg mb-1"></h3>
<p className="text-sm text-slate-500">AI </p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setViewingReport('potential-scenarios')}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center"
>
<Eye size={16} className="mr-2" />
线
</button>
<button
onClick={(e) => {
e.stopPropagation();
console.log('Download: 潜在数据应用场景评估');
}}
className="p-2 text-slate-400 hover:text-purple-600 transition-colors"
title="下载 PDF"
>
<Download size={18} />
</button>
</div>
</div>
</div>
)}
</div>
<div className="max-w-4xl mx-auto w-full text-center">
<button
onClick={() => {
// TODO: Implement batch download functionality
if (confirm('确认下载所有交付物?下载后系统将自动锁定本项目的所有编辑权限 (Audit Lock)。')) {
console.log('Download all deliverables as ZIP');
}
}}
className="px-8 py-3 bg-slate-900 text-white rounded-lg font-bold shadow-lg hover:bg-slate-800 transition-transform hover:scale-105 flex items-center mx-auto"
>
<PackageCheck size={20} className="mr-2"/>
(Zip)
</button>
<p className="text-xs text-slate-400 mt-4">
操作提示: 确认下载后 (Audit Lock)
</p>
</div>
</div>
);
};

View File

@ -1,536 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react';
import {
FileJson,
Terminal,
Table as TableIcon,
CheckCircle2,
ArrowLeft,
ArrowRight,
Upload,
Copy,
FileSpreadsheet,
CheckSquare,
Sparkles,
Loader2,
AlertOctagon,
ArrowUp,
ArrowDown,
ArrowUpDown
} from 'lucide-react';
import { InventoryMode, Step, InventoryItem } from '../../types';
import { inventoryData } from '../../data/mockData';
import { useToast } from '../../contexts/ToastContext';
type SortField = keyof InventoryItem | null;
type SortDirection = 'asc' | 'desc' | null;
interface InventoryStepProps {
inventoryMode: InventoryMode;
setInventoryMode: (mode: InventoryMode) => void;
setCurrentStep: (step: Step) => void;
}
export const InventoryStep: React.FC<InventoryStepProps> = ({
inventoryMode,
setInventoryMode,
setCurrentStep
}) => {
const toast = useToast();
const [processingStage, setProcessingStage] = useState(0);
const [sortField, setSortField] = useState<SortField>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
// Sort data
const sortedData = useMemo(() => {
if (!sortField || !sortDirection) return inventoryData;
return [...inventoryData].sort((a, b) => {
let aVal: any = a[sortField];
let bVal: any = b[sortField];
// Handle different data types
if (typeof aVal === 'string') {
aVal = aVal.toLowerCase();
bVal = bVal.toLowerCase();
}
if (sortField === 'confidence' || sortField === 'id') {
aVal = Number(aVal);
bVal = Number(bVal);
}
if (sortField === 'pii') {
aVal = aVal.length;
bVal = bVal.length;
}
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
}, [sortField, sortDirection]);
const handleSort = (field: SortField) => {
if (sortField === field) {
// Toggle direction
if (sortDirection === 'asc') {
setSortDirection('desc');
} else if (sortDirection === 'desc') {
setSortField(null);
setSortDirection(null);
}
} else {
setSortField(field);
setSortDirection('asc');
}
};
const getSortIcon = (field: SortField) => {
if (sortField !== field) {
return <ArrowUpDown size={14} className="text-slate-400" />;
}
if (sortDirection === 'asc') {
return <ArrowUp size={14} className="text-blue-600" />;
}
return <ArrowDown size={14} className="text-blue-600" />;
};
// Simulate processing animation
useEffect(() => {
if (inventoryMode === 'processing') {
const t1 = setTimeout(() => setProcessingStage(1), 500);
const t2 = setTimeout(() => setProcessingStage(2), 2000);
const t3 = setTimeout(() => {
setProcessingStage(3);
setInventoryMode('results');
}, 3500);
return () => {
clearTimeout(t1);
clearTimeout(t2);
clearTimeout(t3);
};
}
}, [inventoryMode, setInventoryMode]);
return (
<div className="h-full flex flex-col">
{/* MODE 1: SELECTION */}
{inventoryMode === 'selection' && (
<div className="flex-1 p-12 bg-slate-50 flex flex-col items-center justify-center animate-fade-in">
<div className="text-center mb-10 max-w-2xl">
<h2 className="text-3xl font-bold text-slate-900"></h2>
<p className="text-slate-500 mt-3 text-lg"></p>
</div>
<div className="grid grid-cols-3 gap-6 w-full max-w-5xl">
{/* Scheme 1 */}
<div
onClick={() => setInventoryMode('scheme1')}
className="bg-white p-8 rounded-xl border border-slate-200 shadow-sm hover:shadow-xl hover:border-blue-500 cursor-pointer transition-all group relative"
>
<div className="w-14 h-14 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600 mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<FileJson size={32}/>
</div>
<div className="absolute top-4 right-4 bg-slate-100 text-slate-500 text-xs px-2 py-1 rounded font-medium"></div>
<h3 className="text-xl font-bold text-slate-800 mb-2"></h3>
<p className="text-sm text-slate-500 mb-4"> (Excel/Word)</p>
<div className="flex items-center text-xs text-slate-400 bg-slate-50 p-2 rounded">
<CheckCircle2 size={12} className="mr-1 text-green-500"/>
</div>
</div>
{/* Scheme 2 */}
<div
onClick={() => setInventoryMode('scheme2')}
className="bg-white p-8 rounded-xl border border-slate-200 shadow-sm hover:shadow-xl hover:border-purple-500 cursor-pointer transition-all group relative"
>
<div className="w-14 h-14 bg-purple-50 rounded-2xl flex items-center justify-center text-purple-600 mb-6 group-hover:bg-purple-600 group-hover:text-white transition-colors">
<Terminal size={32}/>
</div>
<div className="absolute top-4 right-4 bg-slate-100 text-slate-500 text-xs px-2 py-1 rounded font-medium"></div>
<h3 className="text-xl font-bold text-slate-800 mb-2">IT </h3>
<p className="text-sm text-slate-500 mb-4"> IT SQL Schema </p>
<div className="flex items-center text-xs text-slate-400 bg-slate-50 p-2 rounded">
<CheckCircle2 size={12} className="mr-1 text-green-500"/> IT
</div>
</div>
{/* Scheme 3 */}
<div
onClick={() => setInventoryMode('scheme3')}
className="bg-white p-8 rounded-xl border border-slate-200 shadow-sm hover:shadow-xl hover:border-amber-500 cursor-pointer transition-all group relative"
>
<div className="w-14 h-14 bg-amber-50 rounded-2xl flex items-center justify-center text-amber-600 mb-6 group-hover:bg-amber-600 group-hover:text-white transition-colors">
<TableIcon size={32}/>
</div>
<div className="absolute top-4 right-4 bg-slate-100 text-slate-500 text-xs px-2 py-1 rounded font-medium"></div>
<h3 className="text-xl font-bold text-slate-800 mb-2"></h3>
<p className="text-sm text-slate-500 mb-4"></p>
<div className="flex items-center text-xs text-slate-400 bg-slate-50 p-2 rounded">
<CheckCircle2 size={12} className="mr-1 text-green-500"/> SaaS/
</div>
</div>
</div>
</div>
)}
{/* MODE 2.1: SCHEME 1 INTERACTION (Document Upload) */}
{inventoryMode === 'scheme1' && (
<div className="flex-1 flex flex-col items-center justify-center bg-slate-50 p-8 animate-fade-in">
<div className="max-w-2xl w-full bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden">
<div className="p-6 border-b border-slate-100 flex items-center">
<button onClick={() => setInventoryMode('selection')} className="mr-4 text-slate-400 hover:text-slate-600">
<ArrowLeft size={20}/>
</button>
<div>
<h3 className="text-lg font-bold text-slate-800"></h3>
<p className="text-xs text-slate-500"></p>
</div>
</div>
<div className="p-10">
<div className="border-2 border-dashed border-blue-200 rounded-xl bg-blue-50/50 p-12 text-center hover:border-blue-400 transition-colors cursor-pointer group">
<div className="w-16 h-16 bg-white rounded-full flex items-center justify-center mx-auto mb-4 shadow-sm group-hover:scale-110 transition-transform">
<Upload size={32} className="text-blue-500"/>
</div>
<h4 className="text-slate-700 font-medium mb-1"></h4>
<p className="text-xs text-slate-400"> .xlsx, .doc, .docx (Max 50MB)</p>
</div>
<div className="mt-6 flex justify-end">
<button onClick={() => {
toast.info('开始解析文档,请稍候...');
setInventoryMode('processing');
}} className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 shadow-sm flex items-center">
<ArrowRight size={16} className="ml-2"/>
</button>
</div>
</div>
</div>
</div>
)}
{/* MODE 2.2: SCHEME 2 INTERACTION (SQL Script) */}
{inventoryMode === 'scheme2' && (
<div className="flex-1 flex flex-col items-center justify-center bg-slate-50 p-8 animate-fade-in min-h-0 overflow-hidden">
<div className="max-w-3xl w-full h-full bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col">
<div className="p-6 border-b border-slate-100 flex items-center flex-none">
<button onClick={() => setInventoryMode('selection')} className="mr-4 text-slate-400 hover:text-slate-600">
<ArrowLeft size={20}/>
</button>
<div>
<h3 className="text-lg font-bold text-slate-800">IT </h3>
<p className="text-xs text-slate-500"> SQL IT </p>
</div>
</div>
<div className="flex-1 overflow-y-auto p-8 space-y-8">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-slate-700 flex items-center">
<span className="w-5 h-5 rounded-full bg-slate-800 text-white flex items-center justify-center text-xs mr-2">1</span>
</span>
<button className="text-xs text-blue-600 hover:text-blue-800 flex items-center font-medium">
<Copy size={12} className="mr-1"/>
</button>
</div>
<div className="bg-slate-900 rounded-lg p-4 font-mono text-xs text-slate-300 leading-relaxed overflow-x-auto shadow-inner">
<span className="text-purple-400">SELECT</span> <br/>
&nbsp;&nbsp;TABLE_NAME <span className="text-purple-400">AS</span> <span className="text-green-400">'表英文名'</span>,<br/>
&nbsp;&nbsp;TABLE_COMMENT <span className="text-purple-400">AS</span> <span className="text-green-400">'表中文名/描述'</span>,<br/>
&nbsp;&nbsp;COLUMN_NAME <span className="text-purple-400">AS</span> <span className="text-green-400">'字段英文名'</span>,<br/>
&nbsp;&nbsp;COLUMN_COMMENT <span className="text-purple-400">AS</span> <span className="text-green-400">'字段中文名'</span>,<br/>
&nbsp;&nbsp;COLUMN_TYPE <span className="text-purple-400">AS</span> <span className="text-green-400">'字段类型'</span><br/>
<span className="text-purple-400">FROM</span> information_schema.COLUMNS <br/>
<span className="text-purple-400">WHERE</span> TABLE_SCHEMA = <span className="text-green-400">'您的数据库名'</span>;
</div>
</div>
<div>
<span className="text-sm font-bold text-slate-700 flex items-center mb-3">
<span className="w-5 h-5 rounded-full bg-slate-800 text-white flex items-center justify-center text-xs mr-2">2</span>
</span>
<div className="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:border-purple-400 transition-colors cursor-pointer bg-slate-50">
<Upload size={24} className="text-slate-400 mx-auto mb-2"/>
<p className="text-xs text-slate-500"> Excel / CSV </p>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50 flex justify-end flex-none">
<button onClick={() => {
toast.info('开始 AI 盘点,请稍候...');
setInventoryMode('processing');
}} className="px-6 py-2 bg-purple-600 text-white rounded-lg font-medium hover:bg-purple-700 shadow-sm flex items-center">
AI <ArrowRight size={16} className="ml-2"/>
</button>
</div>
</div>
</div>
)}
{/* MODE 2.3: SCHEME 3 INTERACTION (Manual Export Checklist) */}
{inventoryMode === 'scheme3' && (
<div className="flex-1 flex flex-col items-center justify-center bg-slate-50 p-8 animate-fade-in min-h-0 overflow-hidden">
<div className="max-w-4xl w-full h-full bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col">
<div className="p-6 border-b border-slate-100 flex items-center flex-none">
<button onClick={() => setInventoryMode('selection')} className="mr-4 text-slate-400 hover:text-slate-600">
<ArrowLeft size={20}/>
</button>
<div>
<h3 className="text-lg font-bold text-slate-800"></h3>
<p className="text-xs text-slate-500"> SaaS Salesforce, , </p>
</div>
</div>
<div className="flex-1 flex overflow-hidden">
{/* Left: Core Report Suggestions */}
<div className="w-1/2 p-8 border-r border-slate-100 bg-slate-50/50 overflow-y-auto">
<h4 className="text-sm font-bold text-slate-700 mb-4 flex items-center">
<CheckSquare size={16} className="mr-2 text-amber-500"/>
</h4>
<p className="text-xs text-slate-500 mb-4"></p>
<div className="space-y-4">
<div>
<div className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3"></div>
<div className="space-y-2">
<div className="p-3 bg-white border border-slate-200 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1"> (Orders)</div>
<p className="text-xs text-slate-500"></p>
</div>
<div className="p-3 bg-white border border-slate-200 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1">退 (Returns)</div>
<p className="text-xs text-slate-500">退</p>
</div>
</div>
</div>
<div>
<div className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3"></div>
<div className="space-y-2">
<div className="p-3 bg-white border border-slate-200 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1"> (Members)</div>
<p className="text-xs text-slate-500"></p>
</div>
<div className="p-3 bg-white border border-slate-200 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1"> (Points)</div>
<p className="text-xs text-slate-500"></p>
</div>
</div>
</div>
<div>
<div className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3"></div>
<div className="space-y-2">
<div className="p-3 bg-white border border-slate-200 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1">SKU档案 (Products)</div>
<p className="text-xs text-slate-500">SKU编码</p>
</div>
<div className="p-3 bg-white border border-slate-200 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1"> (Inventory)</div>
<p className="text-xs text-slate-500">SKU等维度统计</p>
</div>
</div>
</div>
</div>
</div>
{/* Right: File Upload */}
<div className="w-1/2 p-8 flex flex-col">
<h4 className="text-sm font-bold text-slate-700 mb-4 flex items-center">
<Upload size={16} className="mr-2 text-amber-500"/>
</h4>
<div className="flex-1 border-2 border-dashed border-amber-200 rounded-xl bg-amber-50/30 flex flex-col items-center justify-center p-6 text-center hover:bg-amber-50/50 transition-colors cursor-pointer">
<div className="w-16 h-16 bg-white rounded-full shadow-sm flex items-center justify-center mb-4">
<FileSpreadsheet size={32} className="text-amber-500"/>
</div>
<p className="text-sm font-medium text-slate-700 mb-1"></p>
<p className="text-xs text-slate-400"> Excel / CSV </p>
<p className="text-xs text-slate-400 mt-1">AI </p>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-100 flex justify-between items-center flex-none">
<span className="text-xs text-slate-500"> <span className="font-bold text-amber-600">0</span> </span>
<button onClick={() => {
toast.info('开始识别表结构,请稍候...');
setInventoryMode('processing');
}} className="px-6 py-2 bg-amber-500 text-white rounded-lg font-medium hover:bg-amber-600 shadow-sm flex items-center">
<ArrowRight size={16} className="ml-2"/>
</button>
</div>
</div>
</div>
)}
{/* MODE 3: PROCESSING (Animation) */}
{inventoryMode === 'processing' && (
<div className="flex-1 flex flex-col items-center justify-center bg-slate-50 animate-fade-in">
<div className="bg-white p-10 rounded-2xl shadow-xl border border-slate-100 text-center max-w-md w-full">
<div className="relative mb-8 flex justify-center">
<div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center animate-pulse">
<Sparkles size={40} className="text-blue-600" />
</div>
<div className="absolute top-0 right-1/4">
<Loader2 size={24} className="text-blue-400 animate-spin" />
</div>
</div>
<h3 className="text-2xl font-bold text-slate-800 mb-6">AI ...</h3>
<div className="space-y-4 text-left">
<div className="flex items-center">
<div className={`w-6 h-6 rounded-full flex items-center justify-center mr-3 ${processingStage >= 1 ? 'bg-green-100 text-green-600' : 'bg-slate-100 text-slate-300'}`}>
{processingStage >= 1 ? <CheckCircle2 size={16}/> : 1}
</div>
<span className={`text-sm ${processingStage >= 1 ? 'text-slate-800 font-medium' : 'text-slate-400'}`}> / SQL </span>
</div>
<div className="flex items-center">
<div className={`w-6 h-6 rounded-full flex items-center justify-center mr-3 ${processingStage >= 2 ? 'bg-green-100 text-green-600' : 'bg-slate-100 text-slate-300'}`}>
{processingStage >= 2 ? <CheckCircle2 size={16}/> : 2}
</div>
<span className={`text-sm ${processingStage >= 2 ? 'text-slate-800 font-medium' : 'text-slate-400'}`}> PII & </span>
</div>
<div className="flex items-center">
<div className={`w-6 h-6 rounded-full flex items-center justify-center mr-3 ${processingStage >= 3 ? 'bg-green-100 text-green-600' : 'bg-slate-100 text-slate-300'}`}>
{processingStage >= 3 ? <CheckCircle2 size={16}/> : 3}
</div>
<span className={`text-sm ${processingStage >= 3 ? 'text-slate-800 font-medium' : 'text-slate-400'}`}></span>
</div>
</div>
</div>
</div>
)}
{/* MODE 4: RESULTS (Final List) */}
{inventoryMode === 'results' && (
<>
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-white shadow-sm z-10 animate-fade-in">
<div>
<div className="flex items-center space-x-2">
<h3 className="text-lg font-bold text-slate-800"></h3>
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs rounded-full font-bold">AI Completed</span>
</div>
<p className="text-xs text-slate-500 mt-1"> 4 | <span className="text-blue-600 font-medium"></span> AI </p>
</div>
<div className="flex space-x-3">
<button className="flex items-center px-3 py-1.5 text-sm font-medium text-slate-700 bg-slate-100 border border-slate-200 rounded hover:bg-white hover:border-slate-300 transition-colors">
<FileSpreadsheet size={16} className="mr-2 text-green-600" />
</button>
<button
onClick={() => {
toast.success('数据盘点结果已确认');
setCurrentStep('context');
}}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded hover:bg-blue-700 shadow-sm flex items-center"
>
<Sparkles size={16} className="mr-2" />
</button>
</div>
</div>
<div className="flex-1 overflow-auto bg-slate-50 p-6 animate-fade-in">
<div className="bg-white rounded-lg border border-slate-200 shadow-sm overflow-hidden">
<table className="min-w-full text-left text-sm">
<thead className="bg-slate-50 text-slate-500 font-medium border-b border-slate-200">
<tr>
<th
className="px-6 py-3 w-1/6 cursor-pointer hover:bg-slate-100 transition-colors select-none"
onClick={() => handleSort('raw')}
>
<div className="flex items-center justify-between">
<span> (Raw)</span>
{getSortIcon('raw')}
</div>
</th>
<th
className="px-6 py-3 w-1/6 cursor-pointer hover:bg-slate-100 transition-colors select-none"
onClick={() => handleSort('aiName')}
>
<div className="flex items-center justify-between">
<span> (AI)</span>
{getSortIcon('aiName')}
</div>
</th>
<th className="px-6 py-3 w-1/4"></th>
<th
className="px-6 py-3 w-1/6 cursor-pointer hover:bg-slate-100 transition-colors select-none"
onClick={() => handleSort('pii')}
>
<div className="flex items-center justify-between">
<span> (PII)</span>
{getSortIcon('pii')}
</div>
</th>
<th
className="px-6 py-3 w-1/6 cursor-pointer hover:bg-slate-100 transition-colors select-none"
onClick={() => handleSort('important')}
>
<div className="flex items-center justify-between">
<span></span>
{getSortIcon('important')}
</div>
</th>
<th
className="px-6 py-3 text-right cursor-pointer hover:bg-slate-100 transition-colors select-none"
onClick={() => handleSort('confidence')}
>
<div className="flex items-center justify-end gap-2">
<span></span>
{getSortIcon('confidence')}
</div>
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{sortedData.map((row) => (
<tr key={row.id} className="hover:bg-blue-50/30 transition-colors group">
<td className="px-6 py-4 font-mono text-slate-600">{row.raw}</td>
<td className="px-6 py-4">
<div className={`font-bold ${row.aiCompleted ? 'text-blue-700' : 'text-slate-800'}`}>
{row.aiName}
{row.aiCompleted && <Sparkles size={12} className="inline ml-1 text-blue-400"/>}
</div>
</td>
<td className="px-6 py-4 text-slate-600 text-xs">
<div className={row.aiCompleted ? 'p-1 -ml-1 rounded bg-blue-50 text-blue-800 inline-block' : ''}>
{row.desc}
</div>
</td>
<td className="px-6 py-4">
{row.pii.length > 0 ? (
<div className="flex flex-wrap gap-1">
{row.pii.map(tag => (
<span key={tag} className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 text-xs font-medium border border-amber-200">
{tag}
</span>
))}
</div>
) : (
<span className="text-slate-300">-</span>
)}
</td>
<td className="px-6 py-4">
{row.important ? (
<span className="flex items-center px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs font-bold border border-red-200 w-fit">
<AlertOctagon size={12} className="mr-1"/>
</span>
) : (
<span className="text-slate-300">-</span>
)}
</td>
<td className="px-6 py-4 text-right">
<span className="text-xs font-bold text-green-600 bg-green-50 px-2 py-1 rounded">{row.confidence}%</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
)}
</div>
);
};

View File

@ -1,213 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Sparkles,
ArrowRight
} from 'lucide-react';
import { Step, InventoryMode } from '../../types';
import { useToast } from '../../contexts/ToastContext';
interface SetupStepProps {
setCurrentStep: (step: Step) => void;
setInventoryMode?: (mode: InventoryMode) => void;
}
export const SetupStep: React.FC<SetupStepProps> = ({ setCurrentStep, setInventoryMode }) => {
const toast = useToast();
const [projectName, setProjectName] = useState('');
const [companyDescription, setCompanyDescription] = useState('');
const [owner, setOwner] = useState('');
const [selectedIndustries, setSelectedIndustries] = useState<string[]>([]);
const [errors, setErrors] = useState<{ projectName?: string; companyDescription?: string; industries?: string }>({});
const [touched, setTouched] = useState<{ projectName?: boolean; companyDescription?: boolean; industries?: boolean }>({});
// Real-time validation
useEffect(() => {
const newErrors: typeof errors = {};
if (touched.projectName && !projectName.trim()) {
newErrors.projectName = '请填写项目名称';
}
if (touched.companyDescription && !companyDescription.trim()) {
newErrors.companyDescription = '请填写企业及主营业务简介';
}
if (touched.industries && selectedIndustries.length === 0) {
newErrors.industries = '请至少选择一个所属行业';
}
setErrors(newErrors);
}, [projectName, companyDescription, selectedIndustries, touched]);
const industries = [
'零售 - 生鲜连锁',
'零售 - 快消品',
'金融 - 商业银行',
'金融 - 保险',
'制造 - 汽车供应链',
'制造 - 电子制造',
'医疗 - 医院',
'医疗 - 制药',
'教育 - 高等院校',
'教育 - 培训机构',
'物流 - 快递',
'物流 - 仓储',
'互联网 - 电商',
'互联网 - 社交',
'房地产 - 开发',
'房地产 - 物业管理'
];
return (
<div className="p-8 h-full flex flex-col overflow-y-auto animate-fade-in">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-slate-900"></h2>
<p className="text-slate-500 mt-2"></p>
</div>
<div className="max-w-4xl mx-auto w-full space-y-6 mb-8">
{/* Project Name */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<label className="block text-sm font-bold text-slate-700 mb-3">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={projectName}
onChange={(e) => {
setProjectName(e.target.value);
if (!touched.projectName) {
setTouched(prev => ({ ...prev, projectName: true }));
}
}}
onBlur={() => setTouched(prev => ({ ...prev, projectName: true }))}
placeholder="例如2025 年度数据资产盘点项目"
className={`w-full p-3 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors ${
errors.projectName ? 'border-red-300 bg-red-50' : touched.projectName && projectName.trim() ? 'border-green-300 bg-green-50' : 'border-slate-300'
}`}
/>
{errors.projectName && (
<p className="text-xs text-red-600 mt-1">{errors.projectName}</p>
)}
</div>
{/* Owner */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<label className="block text-sm font-bold text-slate-700 mb-3">
</label>
<input
type="text"
value={owner}
onChange={(e) => 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 transition-colors"
/>
</div>
{/* Company Description */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<label className="block text-sm font-bold text-slate-700 mb-3">
<span className="text-red-500">*</span>
</label>
<textarea
value={companyDescription}
onChange={(e) => {
setCompanyDescription(e.target.value);
if (!touched.companyDescription) {
setTouched(prev => ({ ...prev, companyDescription: true }));
}
}}
onBlur={() => setTouched(prev => ({ ...prev, companyDescription: true }))}
placeholder="例如某连锁生鲜零售企业主营水果、蔬菜、肉禽蛋奶等生鲜产品拥有线下门店500家..."
rows={4}
className={`w-full p-3 border rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none transition-colors ${
errors.companyDescription ? 'border-red-300 bg-red-50' : touched.companyDescription && companyDescription.trim() ? 'border-green-300 bg-green-50' : 'border-slate-300'
}`}
/>
{errors.companyDescription && (
<p className="text-xs text-red-600 mt-1">{errors.companyDescription}</p>
)}
</div>
{/* Industry Selection */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm">
<label className="block text-sm font-bold text-slate-700 mb-3">
<span className="text-red-500">*</span> <span className="text-xs font-normal text-slate-500">()</span>
</label>
{errors.industries && (
<p className="text-xs text-red-600 mb-2">{errors.industries}</p>
)}
<div className="grid grid-cols-2 gap-3">
{industries.map((industry) => (
<label
key={industry}
className="flex items-center space-x-2 p-3 border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-50 hover:border-blue-400 transition-colors"
>
<input
type="checkbox"
checked={selectedIndustries.includes(industry)}
onChange={(e) => {
if (e.target.checked) {
setSelectedIndustries([...selectedIndustries, industry]);
} else {
setSelectedIndustries(selectedIndustries.filter(i => i !== industry));
}
if (!touched.industries) {
setTouched(prev => ({ ...prev, industries: true }));
}
}}
onBlur={() => setTouched(prev => ({ ...prev, industries: true }))}
className="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 focus:ring-2"
/>
<span className="text-sm text-slate-700">{industry}</span>
</label>
))}
</div>
</div>
</div>
<div className="max-w-4xl mx-auto w-full">
<button
onClick={() => {
// Mark all fields as touched
setTouched({ projectName: true, companyDescription: true, industries: true });
// Validation
const newErrors: typeof errors = {};
if (!projectName.trim()) {
newErrors.projectName = '请填写项目名称';
}
if (!companyDescription.trim()) {
newErrors.companyDescription = '请填写企业及主营业务简介';
}
if (selectedIndustries.length === 0) {
newErrors.industries = '请至少选择一个所属行业';
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
toast.error('请完善所有必填项');
// Scroll to first error
const firstErrorElement = document.querySelector('.border-red-300');
if (firstErrorElement) {
firstErrorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
setErrors({});
toast.success('项目配置已保存,开始数据盘点');
setCurrentStep('inventory');
setInventoryMode?.('selection');
}}
className="w-full py-4 bg-slate-900 text-white rounded-lg font-bold text-lg shadow-xl hover:bg-slate-800 hover:scale-[1.01] transition-all flex items-center justify-center group"
>
<Sparkles size={20} className="mr-2 group-hover:animate-pulse" />
<ArrowRight size={20} className="ml-2 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
);
};

View File

@ -1,126 +0,0 @@
import React from 'react';
import {
Target,
ArrowRight,
CheckCircle2,
Database,
Link as LinkIcon,
Plus
} from 'lucide-react';
import { Step, Scenario } from '../../types';
interface ValueStepProps {
setCurrentStep: (step: Step) => void;
selectedScenarios: Scenario[];
allScenarios: Scenario[];
toggleScenarioSelection: (scenarioId: number) => void;
}
export const ValueStep: React.FC<ValueStepProps> = ({
setCurrentStep,
selectedScenarios,
allScenarios,
toggleScenarioSelection
}) => {
const selectedCount = selectedScenarios.length;
const isScenarioSelected = (scenarioId: number) => {
return selectedScenarios.some(s => s.id === scenarioId);
};
return (
<div className="flex h-full flex-col">
<div className="flex-1 flex overflow-hidden bg-slate-50">
{/* Content: New Scenarios */}
<div className="w-full p-8 overflow-y-auto">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-bold text-slate-900 flex items-center">
AI
<span className="ml-3 px-3 py-1 bg-blue-100 text-blue-700 text-xs rounded-full">
{selectedCount}
</span>
</h3>
<button
onClick={() => setCurrentStep('delivery')}
disabled={selectedCount === 0}
className={`px-6 py-2 rounded-lg font-medium shadow-sm flex items-center transition-all ${
selectedCount === 0
? 'bg-slate-300 text-slate-500 cursor-not-allowed'
: 'bg-slate-900 text-white hover:bg-slate-800'
}`}
>
<ArrowRight size={16} className="ml-2"/>
</button>
</div>
<div className="grid grid-cols-3 gap-6">
{allScenarios.map((scen) => {
const isSelected = isScenarioSelected(scen.id);
return (
<div
key={scen.id}
onClick={() => toggleScenarioSelection(scen.id)}
className={`cursor-pointer rounded-xl border-2 p-6 transition-all relative flex flex-col group ${
isSelected ? 'border-blue-500 bg-white shadow-md' : 'border-slate-200 bg-white hover:border-blue-300 hover:shadow-sm'
}`}
>
{isSelected && (
<div className="absolute top-4 right-4 text-blue-500">
<CheckCircle2 size={24} fill="currentColor" className="text-white"/>
</div>
)}
<div className="mb-4 pr-8">
<div className="flex items-center space-x-2 mb-2">
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-500 border border-slate-200 px-1.5 py-0.5 rounded">{scen.type}</span>
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
scen.impact === 'High' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
{scen.impact} Impact
</span>
</div>
<h4 className="text-lg font-bold text-slate-900 leading-tight">{scen.name}</h4>
</div>
<p className="text-sm text-slate-600 mb-4 line-clamp-3 flex-grow">{scen.desc}</p>
<div className="mb-6 bg-slate-50 rounded-lg p-3 border border-slate-100">
<p className="text-xs font-bold text-slate-500 mb-2 flex items-center">
<Database size={12} className="mr-1"/>
</p>
<div className="flex flex-wrap gap-1.5">
{scen.dependencies.map((dep, i) => (
<span key={i} className="text-[10px] bg-white border border-slate-200 px-1.5 py-0.5 rounded text-slate-600 flex items-center">
<LinkIcon size={8} className="mr-1 text-blue-400"/>
{dep}
</span>
))}
</div>
</div>
<div className="pt-4 border-t border-slate-100 mt-auto">
<div className={`w-full py-2 rounded-lg text-sm font-bold text-center transition-colors ${
isSelected ? 'bg-blue-50 text-blue-700' : 'bg-slate-100 text-slate-600'
}`}>
{isSelected ? '已加入规划' : '加入场景规划'}
</div>
</div>
</div>
);
})}
<div className="border-2 border-dashed border-slate-300 rounded-xl p-6 flex flex-col items-center justify-center text-slate-400 hover:border-slate-400 hover:text-slate-500 cursor-pointer min-h-[320px] transition-colors bg-slate-50/50 hover:bg-white">
<div className="w-12 h-12 rounded-full bg-slate-100 flex items-center justify-center mb-3 group-hover:scale-110 transition-transform">
<Plus size={24} />
</div>
<span className="text-sm font-medium"></span>
<p className="text-xs mt-1 text-center max-w-[150px]"> AI </p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -103,7 +103,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { import {
Target,
ArrowRight, ArrowRight,
CheckCircle2, CheckCircle2,
Database, Database,

67
src/router/index.ts Normal file
View File

@ -0,0 +1,67 @@
import NProgress from 'nprogress'
import {
createRouter,
createWebHashHistory,
type NavigationGuardNext,
type RouteLocationNormalized,
type RouteRecordRaw,
type RouteRecordName
} from 'vue-router'
import { routes } from '@/router/routes'
NProgress.configure({ showSpinner: false, speed: 500, minimum: 0.3 })
const router = createRouter({
history: createWebHashHistory(),
routes: routes
})
// 路由前置拦截器
router.beforeEach(
async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
NProgress.start()
if (to.name === '404') {
next()
return
}
// 暂时跳过权限验证,后续可以添加
// const notAuthRouteNameList = ['login']
// if (!notAuthRouteNameList.includes(to.name ? to.name.toString() : '')) {
// // 权限验证逻辑
// }
next()
}
)
router.afterEach(() => {
NProgress.done()
})
export const getChildRouteListByPathAndName = (path: any, name?: RouteRecordName | any) => {
return getChildRouteList(routes, path, name)
}
export const getChildRouteList: (
routeList: Array<RouteRecordRaw>,
path: string,
name?: RouteRecordName | null | undefined
) => Array<RouteRecordRaw> = (routeList, path, name) => {
for (let index = 0; index < routeList.length; index++) {
const route = routeList[index]
if (name === route.name && path === route.path) {
return route.children || []
}
if (route.children && route.children.length > 0) {
const result = getChildRouteList(route.children, path, name)
if (result && result?.length > 0) {
return result
}
}
}
return []
}
export default router

317
src/router/routes.ts Normal file
View File

@ -0,0 +1,317 @@
import type { RouteRecordRaw } from 'vue-router'
const Layout = () => import('@/layouts/AppLayout.vue')
export const routes: Array<RouteRecordRaw> = [
/** 默认跳转到数据资源盘点 */
{
path: '/',
name: 'home',
redirect: '/data-inventory',
},
/** 数据资源盘点 - 新增功能 */
{
path: '/data-inventory',
name: 'dataInventory',
meta: { title: '数据资源盘点', icon: 'inventory' },
component: Layout,
redirect: { name: 'dataInventoryList' },
children: [
{
path: '/data-inventory/list',
name: 'dataInventoryList',
component: () => import('@/pages/ProjectListView.vue'),
meta: {
title: '项目列表',
keepAlive: false
}
},
{
path: '/data-inventory/engagement',
name: 'dataInventoryEngagement',
component: () => import('@/pages/EngagementView.vue'),
meta: {
title: '项目作业',
keepAlive: false,
hidden: true
}
}
]
},
/** 智能中心 - 旧系统功能 */
{
path: '/smart',
name: 'smart',
meta: { title: '智能中心', icon: 'znzx' },
component: Layout,
redirect: { name: 'smartAnswer' },
children: [
{
path: '/smart/answer/index',
name: 'smartAnswer',
component: () => import('@/views/smart/answer/index.vue'),
meta: {
title: '智能问答',
keepAlive: false
}
},
{
path: '/smart/writing/list',
name: 'smartWriting',
component: () => import('@/views/smart/writing/list.vue'),
meta: {
title: '智能写作',
keepAlive: false,
hidden: typeof window !== 'undefined' && window.location.hostname === '10.100.31.21'
}
},
{
path: '/smart/document/list',
name: 'smartDocument',
component: () => import('@/views/smart/document/list.vue'),
meta: {
title: '智能文书',
keepAlive: false
}
},
{
path: '/smart/review/list',
name: 'smartReview',
component: () => import('@/views/smart/review/list.vue'),
meta: {
title: '智能审核',
keepAlive: false,
hidden: typeof window !== 'undefined' && window.location.hostname === '10.100.31.21'
}
},
// 二级页面
{
path: '/smart/review/plan',
name: 'smartReviewPlan',
component: () => import('@/views/smart/review/plan.vue'),
meta: {
title: '审核方案名称',
keepAlive: false,
activeMenu: '/smart/review/list',
hidden: true
}
},
{
path: '/smart/review/rule',
name: 'smartReviewRule',
component: () => import('@/views/smart/review/rule/index.vue'),
meta: {
title: '规则详情',
keepAlive: false,
activeMenu: '/smart/review/list',
hidden: true
}
},
{
path: '/smart/answer/:id/setting',
name: 'smartAnswerSetting',
component: () => import('@/views/smart/answer/setting.vue'),
meta: {
title: '智能设置',
keepAlive: false,
activeMenu: '/smart/answer/index',
hidden: true
}
},
{
path: '/smart/document/create',
name: 'smartDocumentCreate',
component: () => import('@/views/smart/document/create.vue'),
meta: {
title: '新增文书',
keepAlive: false,
activeMenu: '/smart/document/list',
hidden: true
}
},
]
},
/** 知识中心 - 旧系统功能 */
{
path: '/knowledge',
name: 'knowledge',
meta: { title: '知识中心', icon: 'zszx' },
component: Layout,
redirect: { name: 'knowledgeDataset' },
children: [
{
path: '/knowledge/dataset/index',
name: 'knowledgeDataset',
component: () => import('@/views/knowledge/dataset/index.vue'),
meta: {
title: '知识库管理',
keepAlive: false
}
},
{
path: '/knowledge/dataset/view',
name: 'knowledgeDatasetView',
component: () => import('@/views/knowledge/dataset/view.vue'),
meta: {
title: '知识库详情',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/dataset/index',
}
},
{
path: '/knowledge/dataset/importDoc',
name: 'knowledgeDatasetImportDoc',
component: () => import('@/views/knowledge/dataset/importDoc.vue'),
meta: {
title: '知识库详情',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/dataset/index',
}
},
{
path: '/knowledge/dataset/viewParagraph',
name: 'knowledgeDatasetViewParagraph',
component: () => import('@/views/knowledge/dataset/viewParagraph.vue'),
meta: {
title: '查看分段',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/dataset/index',
}
},
{
path: '/knowledge/dataset/traceabilityDetails',
name: 'knowledgeDatasetTraceabilityDetails',
component: () => import('@/views/knowledge/dataset/traceabilityDetails.vue'),
meta: {
title: '查看分段',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/dataset/index',
}
},
{
path: '/knowledge/document/index',
name: 'knowledgeDocument',
component: () => import('@/views/knowledge/document/index.vue'),
meta: {
title: '文档管理',
keepAlive: false
}
},
{
path: '/knowledge/document/uploadDoc',
name: 'knowledgeUploadDoc',
component: () => import('@/views/knowledge/document/uploadDoc.vue'),
meta: {
title: '上传文档',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/document/index',
}
},
{
path: '/knowledge/document/reviewDoc',
name: 'knowledgeReviewDoc',
component: () => import('@/views/knowledge/document/reviewDoc.vue'),
meta: {
title: '预览文档',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/document/index',
}
},
{
path: '/knowledge/tag/index',
name: 'knowledgeTag',
component: () => import('@/views/knowledge/tag/index.vue'),
meta: {
title: '标签管理',
keepAlive: false,
hidden: typeof window !== 'undefined' && window.location.hostname === '10.100.31.21'
}
},
{
path: '/knowledge/tag/create',
name: 'knowledgeTagCreate',
component: () => import('@/views/knowledge/tag/create.vue'),
meta: {
title: '新建维度及标签',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/tag/index',
}
},
{
path: '/knowledge/tag/viewTag',
name: 'knowledgeViewTag',
component: () => import('@/views/knowledge/tag/viewTag.vue'),
meta: {
title: '标签管理',
keepAlive: false,
hidden: true,
activeMenu: '/knowledge/tag/index',
}
}
]
},
/** 无菜单页面 */
{
path: '/smart/writing/index',
name: 'smartWritingEditor',
component: () => import('@/views/smart/writing/index.vue'),
meta: {
title: '智能编辑',
keepAlive: false,
hidden: true
}
},
{
path: '/smart/review/audit',
name: 'smartReviewAudit',
component: () => import('@/views/smart/review/audit/index.vue'),
meta: {
title: '智能审核',
keepAlive: false,
hidden: true
}
},
{
path: '/smart/review/extraction',
name: 'smartReviewExtraction',
component: () => import('@/views/smart/review/extraction/index.vue'),
meta: {
title: '文档抽取',
keepAlive: false,
hidden: true
}
},
{
path: '/chat/:chatid/index',
name: 'AiChat',
component: () => import('@/views/chat/index.vue'),
meta: {
title: '智能体对话',
keepAlive: false,
hidden: true
}
},
/** 登录 */
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue')
},
/** 错误页面 */
{
path: '/:pathMatch(.*)*',
name: '404',
component: () => import('@/views/error/404.vue')
}
]

View File

@ -0,0 +1,15 @@
// 创建占位视图组件的工具函数
export const createPlaceholderView = (name: string) => {
return {
name,
template: `
<div class="placeholder-view">
<h2>${name}</h2>
<p>...</p>
</div>
`,
setup() {
return {}
}
}
}

9
src/views/chat/index.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<div class="p-8">
<h2>智能体对话</h2>
<p>功能迁移中...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({name: "AiChat"})
</script>

23
src/views/error/404.vue Normal file
View File

@ -0,0 +1,23 @@
<template>
<div class="error-page">
<h1>404</h1>
<p>页面未找到</p>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Error404'
})
</script>
<style scoped>
.error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 400px;
}
</style>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>importDoc</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ImportDoc'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>index</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Index'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>traceabilityDetails</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'TraceabilityDetails'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>view</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'View'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>viewParagraph</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ViewParagraph'})</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>新增文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocumentCreate"})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>index</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Index'})</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>智能文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocument"})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>reviewDoc</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ReviewDoc'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>uploadDoc</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'UploadDoc'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>create</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Create'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>index</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Index'})</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>viewTag</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ViewTag'})</script>

View File

@ -0,0 +1,9 @@
<template>
<div class="p-8">
<h2>登录</h2>
<p>功能迁移中...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({name: "Login"})
</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>智能问答</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartAnswer"})</script>

View File

@ -0,0 +1,9 @@
<template>
<div class="p-8">
<h2>智能设置</h2>
<p>功能迁移中...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({name: "SmartAnswerSetting"})
</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>新增文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocumentCreate"})</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>智能文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocument"})</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="p-8">
<h2>index</h2>
<p>功能迁移中请稍候...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Index'
})
</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="p-8">
<h2>index</h2>
<p>功能迁移中请稍候...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Index'
})
</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="p-8">
<h2>智能审核</h2>
<p>功能迁移中...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'SmartReview'
})
</script>

View File

@ -0,0 +1 @@
<template><div class='p-8'><h2>plan</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Plan'})</script>

View File

@ -0,0 +1,12 @@
<template>
<div class="p-8">
<h2>index</h2>
<p>功能迁移中请稍候...</p>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: 'Index'
})
</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>智能编辑</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartWritingEditor"})</script>

View File

@ -0,0 +1 @@
<template><div class="p-8"><h2>智能写作</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartWriting"})</script>

View File

@ -2,10 +2,25 @@
export default { export default {
content: [ content: [
"./index.html", "./index.html",
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,vue}",
], ],
theme: { theme: {
extend: { extend: {
colors: {
// 旧系统色彩方案
'app-primary': '#3067EF',
'app-bg': '#F4F8FF',
'app-text': '#1f2329',
'app-text-secondary': '#646a73',
'app-text-disable': '#bbbfc4',
'app-border': '#dee0e3',
'app-white': '#ffffff',
'app-header-bg': '#F4F8FF',
'app-sidebar-bg': '#ffffff',
'app-card-bg': '#ffffff',
'app-hover': 'rgba(48, 103, 239, 0.06)',
'app-active': '#ECF2FF',
},
keyframes: { keyframes: {
'fade-in': { 'fade-in': {
'0%': { opacity: '0', transform: 'translateY(10px)' }, '0%': { opacity: '0', transform: 'translateY(10px)' },
@ -20,6 +35,15 @@ export default {
'fade-in': 'fade-in 0.3s ease-out', 'fade-in': 'fade-in 0.3s ease-out',
'bounce-short': 'bounce-short 1s ease-in-out infinite', 'bounce-short': 'bounce-short 1s ease-in-out infinite',
}, },
fontFamily: {
'sans': ['PingFang SC', 'AlibabaPuHuiTi', 'sans-serif'],
},
fontSize: {
'base': '14px',
},
boxShadow: {
'app': '0px 2px 4px 0px rgba(31, 35, 41, 0.12)',
},
}, },
}, },
plugins: [], plugins: [],