完整迁移调整 vue 并且融合原代码结构。
This commit is contained in:
parent
06bf9026f7
commit
6ec4ff9666
305
.cursorrules
Normal file
305
.cursorrules
Normal 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. **代码质量**: 遵循最佳实践,确保可维护性
|
||||||
@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
528
docs/前端开发规范.md
Normal file
528
docs/前端开发规范.md
Normal 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
144
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
21
package.json
21
package.json
@ -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",
|
||||||
|
|||||||
25
src/App.tsx
25
src/App.tsx
@ -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;
|
|
||||||
28
src/App.vue
28
src/App.vue
@ -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>
|
||||||
|
|||||||
@ -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);
|
|
||||||
};
|
|
||||||
@ -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: '删除后数据将无法恢复,确定要继续吗?',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
42
src/components/app-link/index.vue
Normal file
42
src/components/app-link/index.vue
Normal 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>
|
||||||
36
src/components/svg-icon/index.vue
Normal file
36
src/components/svg-icon/index.vue
Normal 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>
|
||||||
@ -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
@ -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
68
src/layouts/AppLayout.vue
Normal 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>
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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>
|
||||||
|
|||||||
61
src/layouts/components/AppHeader.vue
Normal file
61
src/layouts/components/AppHeader.vue
Normal 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>
|
||||||
22
src/layouts/components/AppMain.vue
Normal file
22
src/layouts/components/AppMain.vue
Normal 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>
|
||||||
116
src/layouts/components/Sidebar.vue
Normal file
116
src/layouts/components/Sidebar.vue
Normal 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>
|
||||||
15
src/layouts/components/UserAvatar.vue
Normal file
15
src/layouts/components/UserAvatar.vue
Normal 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>
|
||||||
96
src/layouts/components/sidebar-item/index.vue
Normal file
96
src/layouts/components/sidebar-item/index.vue
Normal 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>
|
||||||
@ -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')
|
||||||
|
|||||||
10
src/main.tsx
10
src/main.tsx
@ -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>,
|
|
||||||
)
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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) => {
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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/>
|
|
||||||
TABLE_NAME <span className="text-purple-400">AS</span> <span className="text-green-400">'表英文名'</span>,<br/>
|
|
||||||
TABLE_COMMENT <span className="text-purple-400">AS</span> <span className="text-green-400">'表中文名/描述'</span>,<br/>
|
|
||||||
COLUMN_NAME <span className="text-purple-400">AS</span> <span className="text-green-400">'字段英文名'</span>,<br/>
|
|
||||||
COLUMN_COMMENT <span className="text-purple-400">AS</span> <span className="text-green-400">'字段中文名'</span>,<br/>
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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
67
src/router/index.ts
Normal 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
317
src/router/routes.ts
Normal 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')
|
||||||
|
}
|
||||||
|
]
|
||||||
15
src/utils/createPlaceholderView.ts
Normal file
15
src/utils/createPlaceholderView.ts
Normal 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
9
src/views/chat/index.vue
Normal 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
23
src/views/error/404.vue
Normal 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>
|
||||||
1
src/views/knowledge/dataset/importDoc.vue
Normal file
1
src/views/knowledge/dataset/importDoc.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>importDoc</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ImportDoc'})</script>
|
||||||
1
src/views/knowledge/dataset/index.vue
Normal file
1
src/views/knowledge/dataset/index.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>index</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Index'})</script>
|
||||||
1
src/views/knowledge/dataset/traceabilityDetails.vue
Normal file
1
src/views/knowledge/dataset/traceabilityDetails.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>traceabilityDetails</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'TraceabilityDetails'})</script>
|
||||||
1
src/views/knowledge/dataset/view.vue
Normal file
1
src/views/knowledge/dataset/view.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>view</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'View'})</script>
|
||||||
1
src/views/knowledge/dataset/viewParagraph.vue
Normal file
1
src/views/knowledge/dataset/viewParagraph.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>viewParagraph</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ViewParagraph'})</script>
|
||||||
1
src/views/knowledge/document/create.vue
Normal file
1
src/views/knowledge/document/create.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>新增文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocumentCreate"})</script>
|
||||||
1
src/views/knowledge/document/index.vue
Normal file
1
src/views/knowledge/document/index.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>index</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Index'})</script>
|
||||||
1
src/views/knowledge/document/list.vue
Normal file
1
src/views/knowledge/document/list.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>智能文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocument"})</script>
|
||||||
1
src/views/knowledge/document/reviewDoc.vue
Normal file
1
src/views/knowledge/document/reviewDoc.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>reviewDoc</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ReviewDoc'})</script>
|
||||||
1
src/views/knowledge/document/uploadDoc.vue
Normal file
1
src/views/knowledge/document/uploadDoc.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>uploadDoc</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'UploadDoc'})</script>
|
||||||
1
src/views/knowledge/tag/create.vue
Normal file
1
src/views/knowledge/tag/create.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>create</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Create'})</script>
|
||||||
1
src/views/knowledge/tag/index.vue
Normal file
1
src/views/knowledge/tag/index.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>index</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Index'})</script>
|
||||||
1
src/views/knowledge/tag/viewTag.vue
Normal file
1
src/views/knowledge/tag/viewTag.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>viewTag</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'ViewTag'})</script>
|
||||||
9
src/views/login/index.vue
Normal file
9
src/views/login/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-8">
|
||||||
|
<h2>登录</h2>
|
||||||
|
<p>功能迁移中...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({name: "Login"})
|
||||||
|
</script>
|
||||||
1
src/views/smart/answer/index.vue
Normal file
1
src/views/smart/answer/index.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>智能问答</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartAnswer"})</script>
|
||||||
9
src/views/smart/answer/setting.vue
Normal file
9
src/views/smart/answer/setting.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-8">
|
||||||
|
<h2>智能设置</h2>
|
||||||
|
<p>功能迁移中...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({name: "SmartAnswerSetting"})
|
||||||
|
</script>
|
||||||
1
src/views/smart/document/create.vue
Normal file
1
src/views/smart/document/create.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>新增文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocumentCreate"})</script>
|
||||||
1
src/views/smart/document/list.vue
Normal file
1
src/views/smart/document/list.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>智能文书</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartDocument"})</script>
|
||||||
12
src/views/smart/review/audit/index.vue
Normal file
12
src/views/smart/review/audit/index.vue
Normal 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>
|
||||||
12
src/views/smart/review/extraction/index.vue
Normal file
12
src/views/smart/review/extraction/index.vue
Normal 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>
|
||||||
12
src/views/smart/review/list.vue
Normal file
12
src/views/smart/review/list.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-8">
|
||||||
|
<h2>智能审核</h2>
|
||||||
|
<p>功能迁移中...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'SmartReview'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
1
src/views/smart/review/plan.vue
Normal file
1
src/views/smart/review/plan.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class='p-8'><h2>plan</h2><p>功能迁移中...</p></div></template><script setup lang='ts'>defineOptions({name: 'Plan'})</script>
|
||||||
12
src/views/smart/review/rule/index.vue
Normal file
12
src/views/smart/review/rule/index.vue
Normal 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>
|
||||||
1
src/views/smart/writing/index.vue
Normal file
1
src/views/smart/writing/index.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>智能编辑</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartWritingEditor"})</script>
|
||||||
1
src/views/smart/writing/list.vue
Normal file
1
src/views/smart/writing/list.vue
Normal file
@ -0,0 +1 @@
|
|||||||
|
<template><div class="p-8"><h2>智能写作</h2><p>功能迁移中...</p></div></template><script setup lang="ts">defineOptions({name: "SmartWriting"})</script>
|
||||||
@ -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: [],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user