原型设计,初始化提交。

This commit is contained in:
李季 2026-01-07 08:13:50 +08:00
commit 873efbe647
33 changed files with 9388 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

555
DEPLOYMENT.md Normal file
View File

@ -0,0 +1,555 @@
# 部署与运行说明文档
本文档详细说明 Finyx AI Prototype 项目的部署和运行流程。
## 📋 目录
- [环境准备](#环境准备)
- [本地开发](#本地开发)
- [生产构建](#生产构建)
- [部署方式](#部署方式)
- [静态文件部署](#静态文件部署)
- [Docker 部署](#docker-部署)
- [Nginx 配置](#nginx-配置)
- [环境变量配置](#环境变量配置)
- [常见问题](#常见问题)
## 🔧 环境准备
### 系统要求
- **操作系统**: Linux / macOS / Windows
- **Node.js**: >= 16.0.0 (推荐使用 LTS 版本)
- **npm**: >= 7.0.0 或 **yarn**: >= 1.22.0
- **内存**: 至少 2GB 可用内存
- **磁盘空间**: 至少 500MB 可用空间
### 检查环境
```bash
# 检查 Node.js 版本
node -v
# 应显示 v16.x.x 或更高版本
# 检查 npm 版本
npm -v
# 应显示 7.x.x 或更高版本
```
### 安装 Node.js如未安装
#### Linux (Ubuntu/Debian)
```bash
# 使用 NodeSource 仓库安装
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
```
#### macOS
```bash
# 使用 Homebrew
brew install node
```
#### Windows
从 [Node.js 官网](https://nodejs.org/) 下载安装包并安装
## 💻 本地开发
### 1. 克隆项目
```bash
git clone <repository-url>
cd finyx_data_frontend
```
### 2. 安装依赖
```bash
npm install
```
如果安装速度慢,可以使用国内镜像:
```bash
# 使用淘宝镜像
npm install --registry=https://registry.npmmirror.com
# 或使用 cnpm
npm install -g cnpm --registry=https://registry.npmmirror.com
cnpm install
```
### 3. 启动开发服务器
```bash
npm run dev
```
启动成功后,终端会显示:
```
VITE v5.x.x ready in xxx ms
➜ Local: http://localhost:5173/
➜ Network: http://192.168.x.x:5173/
```
可以通过以下方式访问:
- **本地访问**: `http://localhost:5173`
- **局域网访问**: 使用终端显示的 Network 地址(例如:`http://192.168.1.100:5173`
**注意**: 开发服务器已配置为允许外部访问,可以通过局域网 IP 地址访问,方便移动设备测试或远程调试。
### 4. 开发模式特性
- **热模块替换 (HMR)**: 代码修改后自动刷新
- **快速构建**: Vite 提供极速的开发体验
- **TypeScript 支持**: 实时类型检查
- **ESLint 检查**: 代码质量检查
### 5. 修改端口和主机配置(如需要)
如果 5173 端口被占用,可以通过以下方式修改:
**方式一:命令行参数**
```bash
# 修改端口
npm run dev -- --port 3000
# 指定主机(允许外部访问)
npm run dev -- --host 0.0.0.0 --port 3000
```
**方式二:修改 vite.config.ts**
```typescript
export default defineConfig({
server: {
host: '0.0.0.0', // 允许外部访问
port: 3000, // 自定义端口
strictPort: false, // 端口被占用时自动尝试下一个
}
})
```
**获取本机 IP 地址**:
```bash
# Linux/macOS
ip addr show | grep "inet " | grep -v 127.0.0.1
# 或
ifconfig | grep "inet " | grep -v 127.0.0.1
# Windows
ipconfig
# 查找 IPv4 地址
```
## 🏗️ 生产构建
### 1. 构建生产版本
```bash
npm run build
```
构建完成后,会在 `dist/` 目录生成优化后的静态文件:
```
dist/
├── index.html
├── assets/
│ ├── index-[hash].js
│ ├── index-[hash].css
│ └── ...
```
### 2. 预览生产构建
在部署前,可以本地预览生产版本:
```bash
npm run preview
```
访问 `http://localhost:4173` 查看预览效果。
### 3. 构建优化
生产构建已自动启用以下优化:
- 代码压缩和混淆
- Tree-shaking移除未使用代码
- 资源压缩CSS、JS
- 代码分割(按需加载)
## 🚀 部署方式
### 静态文件部署
#### 方式一:直接部署 dist 目录
1. **构建项目**
```bash
npm run build
```
2. **上传 dist 目录**
`dist/` 目录下的所有文件上传到 Web 服务器
3. **配置 Web 服务器**
参考下面的 [Nginx 配置](#nginx-配置)
#### 方式二:使用 CI/CD 自动部署
**GitHub Actions 示例** (`.github/workflows/deploy.yml`):
```yaml
name: Deploy
on:
push:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
```
### Docker 部署
#### 1. 创建 Dockerfile
在项目根目录创建 `Dockerfile`:
```dockerfile
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
# 复制 package 文件
COPY package*.json ./
# 安装依赖
RUN npm ci
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine
# 复制构建产物到 nginx
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制 nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
#### 2. 创建 .dockerignore
```
node_modules
dist
.git
.env
*.log
.DS_Store
```
#### 3. 构建和运行 Docker 容器
```bash
# 构建镜像
docker build -t finyx-frontend .
# 运行容器
docker run -d -p 80:80 --name finyx-app finyx-frontend
```
#### 4. 使用 Docker Compose
创建 `docker-compose.yml`:
```yaml
version: '3.8'
services:
frontend:
build: .
ports:
- "80:80"
restart: unless-stopped
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
```
运行:
```bash
docker-compose up -d
```
### Nginx 配置
创建 `nginx.conf` 文件:
```nginx
server {
listen 80;
server_name your-domain.com;
root /usr/share/nginx/html;
index index.html;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# 静态资源缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA 路由支持
location / {
try_files $uri $uri/ /index.html;
}
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 错误页面
error_page 404 /index.html;
}
```
部署到服务器:
```bash
# 复制配置文件
sudo cp nginx.conf /etc/nginx/sites-available/finyx-frontend
# 创建软链接
sudo ln -s /etc/nginx/sites-available/finyx-frontend /etc/nginx/sites-enabled/
# 测试配置
sudo nginx -t
# 重启 Nginx
sudo systemctl restart nginx
```
## 🔐 环境变量配置
### 开发环境
创建 `.env.development`:
```env
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=Finyx AI Prototype (Dev)
```
### 生产环境
创建 `.env.production`:
```env
VITE_API_BASE_URL=https://api.yourdomain.com
VITE_APP_TITLE=Finyx AI Prototype
```
### 使用环境变量
在代码中使用:
```typescript
const apiUrl = import.meta.env.VITE_API_BASE_URL;
```
**注意**: Vite 要求环境变量必须以 `VITE_` 开头才能在客户端代码中访问。
## ❓ 常见问题
### 1. 端口被占用
**问题**: `Error: Port 5173 is already in use`
**解决方案**:
```bash
# 查找占用端口的进程
lsof -i :5173 # macOS/Linux
netstat -ano | findstr :5173 # Windows
# 杀死进程或使用其他端口
npm run dev -- --port 3000
```
### 1.1 无法通过 IP 地址访问
**问题**: 只能通过 localhost 访问,无法通过局域网 IP 地址访问
**解决方案**:
1. **检查配置**: 确保 `vite.config.ts``server.host` 设置为 `'0.0.0.0'`(已默认配置)
2. **检查防火墙**: 确保防火墙允许端口访问
```bash
# Ubuntu/Debian
sudo ufw allow 5173/tcp
sudo ufw reload
# CentOS/RHEL
sudo firewall-cmd --add-port=5173/tcp --permanent
sudo firewall-cmd --reload
# macOS (如果启用了防火墙)
# 系统偏好设置 > 安全性与隐私 > 防火墙 > 防火墙选项
```
3. **检查网络**: 确保设备在同一局域网内
4. **获取本机 IP**:
```bash
# Linux/macOS
ip addr show | grep "inet " | grep -v 127.0.0.1
# 或
hostname -I # Linux
ifconfig | grep "inet " | grep -v 127.0.0.1 # macOS
# Windows
ipconfig
# 查找 IPv4 地址(通常是 192.168.x.x 或 10.x.x.x
```
5. **使用 Network 地址**: 启动开发服务器后,终端会显示 Network 地址,使用该地址访问
### 2. 依赖安装失败
**问题**: `npm install` 失败或很慢
**解决方案**:
```bash
# 清除 npm 缓存
npm cache clean --force
# 删除 node_modules 和 package-lock.json 重新安装
rm -rf node_modules package-lock.json
npm install
# 或使用国内镜像
npm install --registry=https://registry.npmmirror.com
```
### 3. 构建失败
**问题**: `npm run build` 报错
**解决方案**:
- 检查 Node.js 版本是否符合要求
- 清除缓存和依赖重新安装
- 检查 TypeScript 类型错误:`npm run lint`
### 4. 路由刷新 404
**问题**: 刷新页面后出现 404
**解决方案**:
确保 Web 服务器配置了 SPA 路由支持(参考上面的 Nginx 配置)
### 5. 跨域问题
**问题**: 开发时 API 请求跨域
**解决方案**: 在 `vite.config.ts` 中配置代理:
```typescript
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
```
### 6. 内存不足
**问题**: 构建时内存溢出
**解决方案**:
```bash
# 增加 Node.js 内存限制
NODE_OPTIONS="--max-old-space-size=4096" npm run build
```
## 📊 性能优化建议
### 1. 代码分割
- 使用 React.lazy() 进行路由级别的代码分割
- 按需加载大型组件库
### 2. 资源优化
- 压缩图片资源
- 使用 WebP 格式
- 启用 CDN 加速静态资源
### 3. 缓存策略
- 配置合理的 HTTP 缓存头
- 使用 Service Worker 进行离线缓存
### 4. 监控和分析
- 集成性能监控工具(如 Sentry
- 使用 Web Vitals 监控核心指标
## 🔍 健康检查
部署后,可以通过以下方式检查应用状态:
```bash
# 检查 HTTP 响应
curl -I http://your-domain.com
# 检查页面内容
curl http://your-domain.com | head -20
```
## 📞 技术支持
如遇到部署问题,请:
1. 查看本文档的常见问题部分
2. 检查服务器日志
3. 联系开发团队获取支持
---
**最后更新**: 2024年
**文档版本**: 1.0.0

185
README.md Normal file
View File

@ -0,0 +1,185 @@
# Finyx AI Prototype
一个现代化的数据资产盘点与价值挖掘平台前端应用,基于 React + TypeScript + Tailwind CSS 构建。
## 📋 项目简介
Finyx AI Prototype 是一个企业级数据资产管理平台,提供以下核心功能:
- **指挥中心 (Dashboard)**: 全局项目概览、风险监控、KPI 指标展示
- **项目列表 (Projects)**: 项目全生命周期管理,支持搜索、筛选、状态跟踪
- **项目作业台 (Engagement Workspace)**: 多步骤工作流,包括:
- 项目配置:行业模板选择、历史资产继承
- 数据盘点支持多种数据源接入方案文档导入、SQL 脚本、业务表导入)
- 背景调研:企业数字化现状调研、存量应用场景分析
- 价值挖掘:潜在场景挖掘、存量应用优化建议
- 成果交付:交付物打包下载、审计锁定
## 🚀 技术栈
- **前端框架**: React 18.2.0
- **开发语言**: TypeScript 5.2.2
- **构建工具**: Vite 5.0.8
- **样式方案**: Tailwind CSS 3.3.6
- **图标库**: Lucide React 0.344.0
- **图表库**: Recharts 2.10.3
## 📁 项目结构
```
src/
├── App.tsx # 主应用入口,状态管理
├── components/ # 通用可复用组件
│ ├── ProgressBar.tsx # 进度条组件
│ ├── SidebarItem.tsx # 侧边栏菜单项
│ └── TableCheckItem.tsx # 表格复选框项
├── data/ # 模拟数据
│ └── mockData.ts # 项目、风险、盘点等模拟数据
├── layouts/ # 布局组件
│ ├── MainLayout.tsx # 主布局容器
│ └── Sidebar.tsx # 侧边栏布局
├── pages/ # 页面组件
│ ├── DashboardView.tsx # 指挥中心视图
│ ├── ProjectListView.tsx # 项目列表视图
│ ├── EngagementView.tsx # 项目作业台主视图
│ └── engagement/ # 项目作业步骤组件
│ ├── SetupStep.tsx # 项目配置步骤
│ ├── InventoryStep.tsx # 数据盘点步骤
│ ├── ContextStep.tsx # 背景调研步骤
│ ├── ValueStep.tsx # 价值挖掘步骤
│ └── DeliveryStep.tsx # 成果交付步骤
└── types/ # TypeScript 类型定义
└── index.ts # 全局类型定义
```
## 🛠️ 开发指南
### 环境要求
- Node.js >= 16.0.0
- npm >= 7.0.0 或 yarn >= 1.22.0
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev
```
启动后可以通过以下方式访问:
- **本地访问**: `http://localhost:5173`
- **局域网访问**: `http://<你的IP地址>:5173`(例如:`http://192.168.1.100:5173`
**注意**: 开发服务器已配置为允许外部访问(`0.0.0.0`),可以通过局域网 IP 地址访问,方便移动设备或远程调试。
### 构建生产版本
```bash
npm run build
```
构建产物将输出到 `dist/` 目录
### 预览生产构建
```bash
npm run preview
```
### 代码检查
```bash
npm run lint
```
## 🎨 功能特性
### 1. 指挥中心
- 实时 KPI 指标展示
- 项目作业全景视图
- 风险雷达预警系统
### 2. 项目列表
- 项目搜索与筛选
- 进度可视化
- 快速创建新项目
### 3. 项目作业台
- **多步骤工作流**: 5 个主要步骤,支持步骤间导航
- **数据盘点方案**:
- 方案一已有文档导入Excel/Word
- 方案二IT 脚本提取SQL
- 方案三业务关键表导入SaaS 系统)
- **AI 智能分析**: 自动识别 PII、重要数据、生成业务含义
- **场景挖掘**: AI 推荐潜在数据应用场景
- **优化建议**: 基于存量应用的 AI 诊断与优化建议
### 4. 演示模式
- 全屏演示视图
- 隐藏侧边栏和导航
- 适合客户演示和汇报
## 📝 代码规范
- 使用 TypeScript 进行类型检查
- 遵循 React Hooks 最佳实践
- 组件采用函数式组件 + Hooks
- 使用 Tailwind CSS 进行样式管理
- 组件职责单一,便于维护和测试
## 🔧 配置说明
### Vite 配置
项目使用 Vite 作为构建工具,配置文件位于 `vite.config.ts`
### Tailwind CSS 配置
Tailwind 配置文件位于 `tailwind.config.js`,可根据需要自定义主题和样式
### TypeScript 配置
TypeScript 配置位于 `tsconfig.json`,已优化用于 React 开发
## 📦 依赖说明
### 核心依赖
- `react` / `react-dom`: React 核心库
- `lucide-react`: 图标库
- `recharts`: 图表库(当前未使用,但已安装)
### 开发依赖
- `@vitejs/plugin-react`: Vite React 插件
- `typescript`: TypeScript 编译器
- `tailwindcss`: Tailwind CSS 框架
- `eslint`: 代码检查工具
## 🐛 已知问题
- 当前使用模拟数据,需要对接后端 API
- 文件上传功能为 UI 展示,需要实现实际的上传逻辑
- 部分交互功能(如场景选择、文件删除)需要完善状态管理
## 🔮 后续计划
- [ ] 集成后端 API
- [ ] 实现文件上传功能
- [ ] 添加路由管理React Router
- [ ] 实现状态管理Redux/Zustand
- [ ] 添加单元测试和集成测试
- [ ] 优化移动端适配
- [ ] 添加国际化支持
## 📄 许可证
本项目为内部项目,版权归 Finyx 所有。
## 👥 贡献者
- 开发团队Finyx AI Team
## 📞 联系方式
如有问题或建议,请联系开发团队。

BIN
docs/finyx设计思路.docx Normal file

Binary file not shown.

421
docs/界面优化方案.md Normal file
View File

@ -0,0 +1,421 @@
# Finyx AI 界面优化方案
## 📋 文档说明
本文档基于 Finyx 设计思路文档和当前代码实现,提出系统性的界面优化方案,旨在提升用户体验、增强功能可用性和视觉一致性。
---
## 🎯 优化目标
1. **提升用户体验**:简化操作流程,减少认知负担
2. **增强视觉一致性**:统一设计语言,优化视觉层次
3. **改善信息架构**:优化信息展示,提升可读性
4. **强化交互反馈**:增强操作反馈,提升响应性
5. **优化移动端适配**:提升多设备兼容性
---
## 📊 当前界面分析
### 优势
- ✅ 清晰的步骤式工作流设计
- ✅ 良好的组件化架构
- ✅ 统一的色彩体系Slate/Blue
- ✅ 响应式布局基础
### 待优化点
- ⚠️ 部分页面信息密度过高
- ⚠️ 交互反馈不够明显
- ⚠️ 部分功能入口不够直观
- ⚠️ 数据可视化能力有限
- ⚠️ 移动端体验待优化
---
## 🎨 优化方案
### 1. 指挥中心 (Dashboard) 优化
#### 1.1 KPI 卡片优化
**当前问题**
- KPI 卡片信息展示较为静态
- 缺少趋势指示和对比数据
**优化方案**
```typescript
// 建议增强 KPI 卡片
- 添加趋势箭头(↑↓)和百分比变化
- 增加迷你图表Sparkline展示趋势
- 添加点击展开详情功能
- 支持自定义 KPI 卡片排序
```
**视觉优化**
- 增加卡片悬停效果(轻微阴影提升)
- 添加加载动画(骨架屏)
- 优化图标与数字的视觉平衡
#### 1.2 项目作业全景表优化
**当前问题**
- 表格信息密度高,可读性一般
- 缺少快速筛选和排序功能
**优化方案**
- 添加表格列筛选器(状态、负责人、进度范围)
- 支持列排序(点击表头)
- 增加表格视图切换(列表/卡片)
- 添加批量操作功能(如批量导出)
- 优化进度条显示(添加阶段标签)
#### 1.3 风险雷达优化
**当前问题**
- 风险信息展示较为单一
- 缺少风险等级分类和优先级排序
**优化方案**
- 添加风险等级标签(高/中/低)
- 支持按风险等级筛选
- 增加风险趋势图表
- 添加风险详情展开面板
- 优化风险卡片视觉层次(颜色区分等级)
---
### 2. 项目列表 (Projects) 优化
#### 2.1 搜索与筛选优化
**当前问题**
- 搜索功能单一,仅支持文本搜索
- 缺少高级筛选器
**优化方案**
- 添加高级筛选面板(项目状态、类型、负责人、日期范围)
- 支持保存筛选条件(常用筛选器)
- 添加搜索建议和自动补全
- 支持多条件组合筛选
#### 2.2 项目卡片/列表视图优化
**当前问题**
- 仅支持表格视图,信息展示单一
**优化方案**
- 添加卡片视图切换Grid/List
- 卡片视图显示项目缩略图/图标
- 优化项目状态标签(颜色编码)
- 添加项目快速预览(悬停显示详情)
#### 2.3 批量操作优化
**当前问题**
- 缺少批量操作功能
**优化方案**
- 添加全选/反选功能
- 支持批量删除、批量导出
- 批量修改项目状态
- 添加操作确认对话框
---
### 3. 项目作业台 (Engagement Workspace) 优化
#### 3.1 步骤导航器优化
**当前问题**
- 步骤导航器在演示模式下隐藏,但缺少快速跳转
- 步骤状态反馈不够明显
**优化方案**
- 添加步骤进度百分比显示
- 支持键盘快捷键导航(← →)
- 添加步骤完成时间戳
- 优化步骤间的连接线动画
- 添加步骤提示气泡Tooltip
#### 3.2 项目配置步骤 (SetupStep) 优化
**当前问题**
- 行业选择界面信息密度高
- 缺少行业模板预览
**优化方案**
- 优化行业选择为标签云或卡片式布局
- 添加行业模板预览功能
- 支持行业搜索和筛选
- 添加历史项目模板快速导入
- 优化表单验证反馈(实时验证)
#### 3.3 数据盘点步骤 (InventoryStep) 优化
**当前问题**
- 方案选择界面缺少方案对比
- 处理过程缺少详细进度信息
**优化方案**
- 添加方案对比表格(功能对比)
- 优化处理进度显示(百分比 + 详细步骤)
- 添加处理日志查看功能
- 支持处理中断和恢复
- 优化结果表格(支持列宽调整、排序、筛选)
**方案选择优化**
```typescript
// 建议添加方案对比卡片
- 方案一:已有文档导入
✓ 支持格式Excel, Word
✓ 处理速度:快
✓ 准确度:高(需文档规范)
⚠️ 适用场景:有完整文档的企业
- 方案二IT 脚本提取
✓ 支持格式SQL 脚本
✓ 处理速度:中等
✓ 准确度:高
⚠️ 适用场景:有 IT 支持但无文档
- 方案三:业务关键表导入
✓ 支持格式Excel, CSV
✓ 处理速度:快
✓ 准确度:中等(需人工识别)
⚠️ 适用场景SaaS 系统/无数据库权限
```
#### 3.4 背景调研步骤 (ContextStep) 优化
**当前问题**
- 表单字段较多,缺少分组
- 存量场景添加流程不够直观
**优化方案**
- 添加表单分组和折叠面板
- 优化存量场景列表(支持拖拽排序)
- 添加场景模板库(快速添加常见场景)
- 支持场景导入/导出
- 添加表单自动保存功能
#### 3.5 价值挖掘步骤 (ValueStep) 优化
**当前问题**
- 场景卡片信息展示有限
- 缺少场景对比功能
**优化方案**
- 添加场景详情展开面板
- 支持场景对比(并排对比)
- 添加场景筛选(按类型、影响度)
- 优化场景卡片布局(响应式网格)
- 添加场景收藏功能
#### 3.6 成果交付步骤 (DeliveryStep) 优化
**当前问题**
- 交付物列表缺少预览缩略图
- 下载功能缺少进度提示
**优化方案**
- 添加交付物预览缩略图
- 优化下载进度显示(进度条)
- 支持批量下载ZIP 打包)
- 添加交付物版本管理
- 优化报告查看器(支持全屏、缩放)
---
### 4. 通用组件优化
#### 4.1 侧边栏 (Sidebar) 优化
**当前问题**
- 侧边栏功能入口较少
- 缺少快速操作入口
**优化方案**
- 添加常用功能快捷入口
- 支持侧边栏折叠/展开
- 添加最近访问项目列表
- 优化用户信息展示(头像、角色)
#### 4.2 进度条组件 (ProgressBar) 优化
**当前问题**
- 进度条样式较为单一
- 缺少阶段标签
**优化方案**
- 添加阶段标签显示
- 支持不同状态样式(进行中/已完成/已暂停)
- 添加动画效果(平滑过渡)
- 支持点击跳转到对应阶段
#### 4.3 表格组件优化
**当前问题**
- 表格功能较为基础
- 缺少高级功能
**优化方案**
- 添加列宽调整功能
- 支持列显示/隐藏
- 添加表格导出功能Excel/CSV
- 优化表格响应式布局
- 添加虚拟滚动(大数据量优化)
---
### 5. 视觉设计优化
#### 5.1 色彩系统优化
**当前方案**
- 主色Slate灰色系+ Blue蓝色系
- 功能色Green成功、Red警告、Amber注意
**优化建议**
- 建立完整的色彩语义系统
- 添加色彩对比度检查(确保可访问性)
- 优化深色模式支持(未来考虑)
#### 5.2 字体与排版优化
**当前问题**
- 字体层次不够明显
- 部分文本可读性待提升
**优化方案**
- 建立字体层级系统(标题/正文/辅助文本)
- 优化行高和字间距
- 添加文本截断和展开功能
- 优化代码块显示(等宽字体)
#### 5.3 图标系统优化
**当前方案**
- 使用 Lucide React 图标库
**优化建议**
- 统一图标尺寸规范
- 添加图标悬停效果
- 优化图标与文本的对齐
- 考虑添加自定义图标(品牌标识)
#### 5.4 动画与过渡优化
**当前问题**
- 动画效果较少
- 页面切换缺少过渡
**优化方案**
- 添加页面切换动画(淡入淡出)
- 优化按钮点击反馈(涟漪效果)
- 添加加载动画(骨架屏)
- 优化表单验证动画(错误提示)
---
### 6. 交互体验优化
#### 6.1 键盘快捷键支持
**优化方案**
```typescript
// 建议添加快捷键
- Cmd/Ctrl + K: 全局搜索
- Cmd/Ctrl + N: 新建项目
- Cmd/Ctrl + S: 保存(表单)
- Esc: 关闭弹窗/返回
- ← →: 步骤导航
- /: 聚焦搜索框
```
#### 6.2 操作反馈优化
**优化方案**
- 添加 Toast 通知(成功/错误/警告)
- 优化按钮加载状态(禁用 + 加载图标)
- 添加操作确认对话框(危险操作)
- 优化表单验证反馈(实时验证 + 错误提示)
#### 6.3 空状态优化
**当前问题**
- 部分页面缺少空状态提示
**优化方案**
- 添加友好的空状态插画
- 提供空状态操作建议
- 优化加载状态(骨架屏)
---
### 7. 响应式设计优化
#### 7.1 移动端适配
**当前问题**
- 移动端体验待优化
**优化方案**
- 优化侧边栏(移动端改为抽屉式)
- 优化表格(移动端改为卡片式)
- 添加移动端底部导航栏
- 优化触摸交互(增大点击区域)
#### 7.2 平板端适配
**优化方案**
- 优化网格布局(自适应列数)
- 优化侧边栏宽度
- 优化表格显示(支持横向滚动)
---
### 8. 性能优化
#### 8.1 加载性能
**优化方案**
- 添加代码分割(路由级别)
- 优化图片加载(懒加载)
- 添加骨架屏(减少感知加载时间)
- 优化首屏渲染(关键 CSS 内联)
#### 8.2 运行时性能
**优化方案**
- 优化大数据量渲染(虚拟滚动)
- 添加防抖/节流(搜索、滚动)
- 优化状态管理(避免不必要的重渲染)
- 添加性能监控Web Vitals
---
## 📝 实施建议
### 阶段一:核心体验优化(优先级:高)
1. ✅ 优化步骤导航器(添加进度百分比)
2. ✅ 优化数据盘点结果表格(列宽调整、排序)
3. ✅ 添加操作反馈Toast 通知)
4. ✅ 优化表单验证(实时验证)
### 阶段二:功能增强(优先级:中)
1. ✅ 添加高级筛选功能
2. ✅ 优化项目列表(卡片/列表视图切换)
3. ✅ 添加批量操作功能
4. ✅ 优化交付物预览
### 阶段三:体验提升(优先级:中低)
1. ✅ 添加键盘快捷键
2. ✅ 优化动画和过渡
3. ✅ 移动端适配优化
4. ✅ 性能优化
---
## 🎯 设计原则
1. **一致性**:保持设计语言和交互模式的一致性
2. **简洁性**:减少不必要的视觉元素,突出核心功能
3. **可访问性**:确保色彩对比度、键盘导航等可访问性要求
4. **响应性**:优化多设备体验,确保良好的响应式布局
5. **反馈性**:及时、清晰的用户操作反馈
---
## 📚 参考资源
- [Material Design Guidelines](https://material.io/design)
- [Ant Design Design Principles](https://ant.design/docs/spec/introduce)
- [Tailwind CSS Design System](https://tailwindcss.com/docs)
- [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/WAI/WCAG21/quickref/)
---
## 📅 更新记录
- **2025-01-XX**: 初始版本创建
---
## 👥 贡献者
- Finyx AI Team

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Finyx Frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4534
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "finyx-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"lucide-react": "^0.344.0",
"recharts": "^2.10.3"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.16",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.32",
"tailwindcss": "^3.3.6",
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

22
src/App.tsx Normal file
View File

@ -0,0 +1,22 @@
import { useState } from 'react';
import { ViewMode, Step } from './types';
import { MainLayout } from './layouts/MainLayout';
function App() {
const [currentView, setCurrentView] = useState<ViewMode>('projects');
const [currentStep, setCurrentStep] = useState<Step>('setup');
const [isPresentationMode, setIsPresentationMode] = useState(false);
return (
<MainLayout
currentView={currentView}
setCurrentView={setCurrentView}
currentStep={currentStep}
setCurrentStep={setCurrentStep}
isPresentationMode={isPresentationMode}
setIsPresentationMode={setIsPresentationMode}
/>
);
}
export default App;

View File

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

View File

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

View File

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

119
src/data/mockData.ts Normal file
View File

@ -0,0 +1,119 @@
import { Project, Risk, InventoryItem, Scenario, LegacyOptimization } from '../types';
export const projectsList: Project[] = [
{ id: 1, name: '鲜果连锁2025资产盘点', client: '鲜果连锁', type: '资产盘点', progress: 45, status: 'processing', owner: '张三', lastUpdate: '2小时前' },
{ id: 2, name: '北方重工数据治理一期', client: '北方重工', type: '数据治理', progress: 80, status: 'processing', owner: '李四', lastUpdate: '1天前' },
{ id: 3, name: '城商行信贷数据估值', client: '城商行', type: '估值入表', progress: 20, status: 'risk', owner: '王五', lastUpdate: '30分钟前' },
{ id: 4, name: '跨境电商合规审计', client: '跨境通电商', type: '合规审计', progress: 95, status: 'review', owner: '赵六', lastUpdate: '3天前' },
{ id: 5, name: 'Future BioMed 研发数据盘点', client: 'Future BioMed', type: '资产盘点', progress: 0, status: 'new', owner: 'Sarah', lastUpdate: '刚刚' },
];
export const riskData: Risk[] = [
{ id: 1, project: '城商行信贷', risk: '生物识别信息未脱敏', level: 'high' },
{ id: 2, project: '鲜果连锁', risk: '会员数据来源合同缺失', level: 'medium' },
];
export const inventoryData: InventoryItem[] = [
{
id: 1,
raw: 't_user_base_01',
aiName: '会员基础信息表',
desc: '存储C端注册用户的核心身份信息',
rows: '2,400,000',
pii: ['手机号', '身份证'],
important: false,
confidence: 98,
aiCompleted: true
},
{
id: 2,
raw: 'ord_logs_2023',
aiName: '订单流水记录表',
desc: '全渠道销售交易明细',
rows: '15,000,000',
pii: ['收货地址'],
important: false,
confidence: 95,
aiCompleted: true
},
{
id: 3,
raw: 'geo_survey_data',
aiName: '门店测绘地理信息',
desc: '包含高精度地理坐标数据',
rows: '4,500',
pii: [],
important: true,
confidence: 88,
aiCompleted: true
},
{
id: 4,
raw: 'tmp_export_v2',
aiName: '临时导出文件(建议废弃)',
desc: '无明确业务含义',
rows: '100',
pii: [],
important: false,
confidence: 90,
aiCompleted: false
},
];
export const scenarioData: Scenario[] = [
{
id: 1,
name: '冷链物流路径优化',
type: '降本增效',
impact: 'High',
desc: '利用车辆轨迹与订单位置数据,优化配送路线,降低燃油成本。',
dependencies: ['订单流水记录表', '门店测绘地理信息'],
selected: true
},
{
id: 2,
name: '精准会员营销',
type: '营销增长',
impact: 'High',
desc: '基于用户画像与历史交易行为,实现千人千面的优惠券发放。',
dependencies: ['会员基础信息表', '订单流水记录表'],
selected: true
},
{
id: 3,
name: '供应链金融征信',
type: '金融服务',
impact: 'Medium',
desc: '将采购与库存数据作为增信依据,为上游供应商提供融资服务。',
dependencies: ['库存变动表(未识别)', '采购订单表(未识别)'],
selected: false
},
{
id: 4,
name: '门店选址辅助模型',
type: '决策支持',
impact: 'Medium',
desc: '结合外部人口热力图与内部门店业绩,辅助新店选址决策。',
dependencies: ['门店测绘地理信息', '外部人口数据'],
selected: false
},
];
export const legacyOptimizationData: LegacyOptimization[] = [
{
id: 1,
title: '月度销售经营报表',
issue: '指标维度单一,仅统计金额',
suggestion: '增加"复购率"、"SKU动销率"深度指标',
impact: '高',
status: 'analyzed'
},
{
id: 2,
title: '物流配送监控大屏',
issue: '缺乏实时预警机制',
suggestion: '基于历史数据增加"配送超时预测"模型',
impact: '中',
status: 'analyzed'
}
];

1431
src/finyx_designV2.tsx Normal file

File diff suppressed because it is too large Load Diff

3
src/index.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

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

60
src/layouts/Sidebar.tsx Normal file
View File

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

10
src/main.tsx Normal file
View File

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

150
src/pages/DashboardView.tsx Normal file
View File

@ -0,0 +1,150 @@
import React from 'react';
import {
Briefcase,
Clock,
AlertTriangle,
FileText,
ArrowRight,
Download
} from 'lucide-react';
import { ViewMode, Project, Step } from '../types';
import { projectsList, riskData } 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"> (Command Center)</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-4 gap-6 mb-8">
{[
{ title: '进行中项目', value: '12', sub: '+2 本周新增', icon: Briefcase, color: 'text-blue-600' },
{ title: '本周待交付', value: '3', sub: '需重点关注', icon: Clock, color: 'text-amber-600' },
{ title: '高风险合规预警', value: '2', sub: '阻断级风险', icon: AlertTriangle, color: 'text-red-600' },
{ title: '待复核节点', value: '5', sub: '专家审核队列', icon: FileText, color: 'text-purple-600' },
].map((kpi, idx) => (
<div key={idx} className="bg-white p-6 rounded-lg border border-slate-200 shadow-sm">
<div className="flex justify-between items-start">
<div>
<p className="text-slate-500 text-xs font-medium uppercase tracking-wider">{kpi.title}</p>
<h3 className="text-3xl font-bold text-slate-800 mt-2">{kpi.value}</h3>
<p className="text-xs text-slate-400 mt-1">{kpi.sub}</p>
</div>
<div className={`p-3 rounded-full bg-opacity-10 ${kpi.color.replace('text', 'bg')}`}>
<kpi.icon className={kpi.color} size={24} />
</div>
</div>
</div>
))}
</div>
<div className="grid grid-cols-12 gap-8">
<div className="col-span-8 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 className="col-span-4 bg-white rounded-lg border border-slate-200 shadow-sm p-6">
<div className="flex items-center mb-6 text-red-600">
<AlertTriangle size={20} className="mr-2"/>
<h3 className="font-bold text-lg"> (Risk Radar)</h3>
</div>
<div className="space-y-4">
{riskData.map((risk) => (
<div key={risk.id} className="p-4 rounded-lg bg-red-50 border border-red-100 flex items-start space-x-3">
<div className="mt-1 min-w-[6px] h-[6px] rounded-full bg-red-500"></div>
<div>
<p className="text-sm font-bold text-red-900">{risk.risk}</p>
<p className="text-xs text-red-600 mt-1">: {risk.project}</p>
<button className="mt-2 text-xs bg-white border border-red-200 text-red-700 px-2 py-1 rounded hover:bg-red-50 transition-colors">
AI
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);

View File

@ -0,0 +1,180 @@
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: 'setup', label: '1. 项目配置' },
{ id: 'inventory', label: '2. 数据盘点' },
{ id: 'context', label: '3. 背景调研' },
{ id: 'value', label: '4. 价值挖掘' },
{ id: 'delivery', label: '5. 成果交付' },
];
const currentStepIndex = steps.findIndex(s => s.id === currentStep);
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'}`}>
<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={`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>
<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 ${isCompleted ? 'bg-green-500' : 'bg-slate-100'}`}></div>
)}
</div>
</div>
);
})}
</div>
</div>
{/* 演示模式退出按钮 */}
{isPresentationMode && (
<button
onClick={() => setIsPresentationMode(false)}
className="fixed top-4 right-4 z-50 bg-white/90 backdrop-blur text-slate-800 px-4 py-2 rounded-full shadow-lg font-medium text-sm flex items-center hover:bg-white"
>
<EyeOff size={16} className="mr-2"/> 退
</button>
)}
{/* Main Workspace Area */}
<div className="flex-1 bg-white border border-slate-200 rounded-lg shadow-sm overflow-hidden flex flex-col">
{currentStep === 'setup' && (
<SetupStep
setCurrentStep={setCurrentStep}
setInventoryMode={setInventoryMode}
/>
)}
{currentStep === 'inventory' && (
<InventoryStep
inventoryMode={inventoryMode}
setInventoryMode={setInventoryMode}
setCurrentStep={setCurrentStep}
/>
)}
{currentStep === 'context' && (
<ContextStep setCurrentStep={setCurrentStep} />
)}
{currentStep === 'value' && (
<ValueStep
setCurrentStep={setCurrentStep}
selectedScenarios={selectedScenarios}
allScenarios={initialScenarioData}
toggleScenarioSelection={toggleScenarioSelection}
/>
)}
{currentStep === 'delivery' && (
<DeliveryStep selectedScenarios={selectedScenarios} />
)}
</div>
</div>
);
};

View File

@ -0,0 +1,147 @@
import React from 'react';
import {
Search,
Plus,
Settings,
MoreHorizontal,
Layers
} from 'lucide-react';
import { ViewMode, Step } from '../types';
import { projectsList } from '../data/mockData';
import { ProgressBar } from '../components/ProgressBar';
interface ProjectListViewProps {
setCurrentView: (view: ViewMode) => void;
setCurrentStep: (step: Step) => void;
}
export const ProjectListView: React.FC<ProjectListViewProps> = ({ setCurrentView, setCurrentStep }) => (
<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"></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">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600">
{project.type}
</span>
</td>
<td className="px-6 py-4 w-48">
<div className="flex items-center justify-between mb-1 text-xs text-slate-500">
<span>{project.progress}%</span>
</div>
<ProgressBar percent={project.progress} status={project.status} />
</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">{project.owner[0]}</div>
{project.owner}
</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()}>
<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 mr-3"
>
</button>
<button className="text-slate-400 hover:text-slate-600"><MoreHorizontal size={16} /></button>
</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>
</div>
);

View File

@ -0,0 +1,133 @@
import React from 'react';
import {
Server,
ClipboardList,
Plus,
XCircle,
Image as ImageIcon,
Sparkles
} from 'lucide-react';
import { Step } from '../../types';
interface ContextStepProps {
setCurrentStep: (step: Step) => void;
}
export const ContextStep: React.FC<ContextStepProps> = ({ setCurrentStep }) => (
<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>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={() => setCurrentStep('value')}
className="px-8 py-3 bg-slate-900 text-white rounded-lg font-bold shadow-lg hover:bg-slate-800 transition-all flex items-center"
>
<Sparkles size={18} className="mr-2"/>
</button>
</div>
</div>
</div>
);

View File

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

View File

@ -0,0 +1,419 @@
import React, { useState, useEffect } from 'react';
import {
FileJson,
Terminal,
Table as TableIcon,
CheckCircle2,
ArrowLeft,
ArrowRight,
Upload,
Copy,
FileSpreadsheet,
CheckSquare,
Sparkles,
Loader2,
AlertOctagon
} from 'lucide-react';
import { InventoryMode, Step } from '../../types';
import { inventoryData } from '../../data/mockData';
interface InventoryStepProps {
inventoryMode: InventoryMode;
setInventoryMode: (mode: InventoryMode) => void;
setCurrentStep: (step: Step) => void;
}
export const InventoryStep: React.FC<InventoryStepProps> = ({
inventoryMode,
setInventoryMode,
setCurrentStep
}) => {
const [processingStage, setProcessingStage] = useState(0);
// 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={() => setInventoryMode('processing')} className="px-6 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 shadow-sm flex items-center">
<ArrowRight size={16} className="ml-2"/>
</button>
</div>
</div>
</div>
</div>
)}
{/* MODE 2.2: SCHEME 2 INTERACTION (SQL Script) */}
{inventoryMode === 'scheme2' && (
<div className="flex-1 flex flex-col items-center justify-center bg-slate-50 p-8 animate-fade-in min-h-0 overflow-hidden">
<div className="max-w-3xl w-full h-full bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col">
<div className="p-6 border-b border-slate-100 flex items-center flex-none">
<button onClick={() => setInventoryMode('selection')} className="mr-4 text-slate-400 hover:text-slate-600">
<ArrowLeft size={20}/>
</button>
<div>
<h3 className="text-lg font-bold text-slate-800">IT </h3>
<p className="text-xs text-slate-500"> SQL IT </p>
</div>
</div>
<div className="flex-1 overflow-y-auto p-8 space-y-8">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-slate-700 flex items-center">
<span className="w-5 h-5 rounded-full bg-slate-800 text-white flex items-center justify-center text-xs mr-2">1</span>
</span>
<button className="text-xs text-blue-600 hover:text-blue-800 flex items-center font-medium">
<Copy size={12} className="mr-1"/>
</button>
</div>
<div className="bg-slate-900 rounded-lg p-4 font-mono text-xs text-slate-300 leading-relaxed overflow-x-auto shadow-inner">
<span className="text-purple-400">SELECT</span> <br/>
&nbsp;&nbsp;TABLE_NAME <span className="text-purple-400">AS</span> <span className="text-green-400">'表英文名'</span>,<br/>
&nbsp;&nbsp;TABLE_COMMENT <span className="text-purple-400">AS</span> <span className="text-green-400">'表中文名/描述'</span>,<br/>
&nbsp;&nbsp;COLUMN_NAME <span className="text-purple-400">AS</span> <span className="text-green-400">'字段英文名'</span>,<br/>
&nbsp;&nbsp;COLUMN_COMMENT <span className="text-purple-400">AS</span> <span className="text-green-400">'字段中文名'</span>,<br/>
&nbsp;&nbsp;COLUMN_TYPE <span className="text-purple-400">AS</span> <span className="text-green-400">'字段类型'</span><br/>
<span className="text-purple-400">FROM</span> information_schema.COLUMNS <br/>
<span className="text-purple-400">WHERE</span> TABLE_SCHEMA = <span className="text-green-400">'您的数据库名'</span>;
</div>
</div>
<div>
<span className="text-sm font-bold text-slate-700 flex items-center mb-3">
<span className="w-5 h-5 rounded-full bg-slate-800 text-white flex items-center justify-center text-xs mr-2">2</span>
</span>
<div className="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:border-purple-400 transition-colors cursor-pointer bg-slate-50">
<Upload size={24} className="text-slate-400 mx-auto mb-2"/>
<p className="text-xs text-slate-500"> Excel / CSV </p>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-100 bg-slate-50 flex justify-end flex-none">
<button onClick={() => 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={() => 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={() => 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"> (Raw)</th>
<th className="px-6 py-3 w-1/6"> (AI)</th>
<th className="px-6 py-3 w-1/4"></th>
<th className="px-6 py-3 w-1/6"> (PII)</th>
<th className="px-6 py-3 w-1/6"></th>
<th className="px-6 py-3 text-right"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{inventoryData.map((row) => (
<tr key={row.id} className="hover:bg-blue-50/30 transition-colors group">
<td className="px-6 py-4 font-mono text-slate-600">{row.raw}</td>
<td className="px-6 py-4">
<div className={`font-bold ${row.aiCompleted ? 'text-blue-700' : 'text-slate-800'}`}>
{row.aiName}
{row.aiCompleted && <Sparkles size={12} className="inline ml-1 text-blue-400"/>}
</div>
</td>
<td className="px-6 py-4 text-slate-600 text-xs">
<div className={row.aiCompleted ? 'p-1 -ml-1 rounded bg-blue-50 text-blue-800 inline-block' : ''}>
{row.desc}
</div>
</td>
<td className="px-6 py-4">
{row.pii.length > 0 ? (
<div className="flex flex-wrap gap-1">
{row.pii.map(tag => (
<span key={tag} className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 text-xs font-medium border border-amber-200">
{tag}
</span>
))}
</div>
) : (
<span className="text-slate-300">-</span>
)}
</td>
<td className="px-6 py-4">
{row.important ? (
<span className="flex items-center px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-xs font-bold border border-red-200 w-fit">
<AlertOctagon size={12} className="mr-1"/>
</span>
) : (
<span className="text-slate-300">-</span>
)}
</td>
<td className="px-6 py-4 text-right">
<span className="text-xs font-bold text-green-600 bg-green-50 px-2 py-1 rounded">{row.confidence}%</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
)}
</div>
);
};

View File

@ -0,0 +1,162 @@
import React, { useState } from 'react';
import {
Sparkles,
ArrowRight
} from 'lucide-react';
import { Step, InventoryMode } from '../../types';
interface SetupStepProps {
setCurrentStep: (step: Step) => void;
setInventoryMode?: (mode: InventoryMode) => void;
}
export const SetupStep: React.FC<SetupStepProps> = ({ setCurrentStep, setInventoryMode }) => {
const [projectName, setProjectName] = useState('');
const [companyDescription, setCompanyDescription] = useState('');
const [selectedIndustries, setSelectedIndustries] = useState<string[]>([]);
const [errors, setErrors] = useState<{ projectName?: string; companyDescription?: string; industries?: string }>({});
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 (errors.projectName) setErrors(prev => ({ ...prev, projectName: undefined }));
}}
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 ${
errors.projectName ? 'border-red-300 bg-red-50' : 'border-slate-300'
}`}
/>
{errors.projectName && (
<p className="text-xs text-red-600 mt-1">{errors.projectName}</p>
)}
</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 (errors.companyDescription) setErrors(prev => ({ ...prev, companyDescription: undefined }));
}}
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 ${
errors.companyDescription ? 'border-red-300 bg-red-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 (errors.industries) setErrors(prev => ({ ...prev, industries: undefined }));
}}
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={() => {
// 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);
// Scroll to first error
const firstErrorElement = document.querySelector('.border-red-300');
if (firstErrorElement) {
firstErrorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
setErrors({});
setCurrentStep('inventory');
setInventoryMode?.('selection');
}}
className="w-full py-4 bg-slate-900 text-white rounded-lg font-bold text-lg shadow-xl hover:bg-slate-800 hover:scale-[1.01] transition-all flex items-center justify-center group"
>
<Sparkles size={20} className="mr-2 group-hover:animate-pulse" />
<ArrowRight size={20} className="ml-2 group-hover:translate-x-1 transition-transform" />
</button>
</div>
</div>
);
};

View File

@ -0,0 +1,126 @@
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>
);
};

55
src/types/index.ts Normal file
View File

@ -0,0 +1,55 @@
// Type definitions for Finyx AI Prototype
export type ViewMode = 'dashboard' | 'projects' | 'engagement';
export type Step = 'setup' | 'inventory' | 'context' | 'value' | 'delivery';
export type InventoryMode = 'selection' | 'scheme1' | 'scheme2' | 'scheme3' | 'processing' | 'results';
export type ValueTab = 'new-scenarios' | 'legacy-optimization';
export interface Project {
id: number;
name: string;
client: string;
type: string;
progress: number;
status: 'processing' | 'risk' | 'warning' | 'review' | 'new';
owner: string;
lastUpdate: string;
}
export interface Risk {
id: number;
project: string;
risk: string;
level: 'high' | 'medium' | 'low';
}
export interface InventoryItem {
id: number;
raw: string;
aiName: string;
desc: string;
rows: string;
pii: string[];
important: boolean;
confidence: number;
aiCompleted: boolean;
}
export interface Scenario {
id: number;
name: string;
type: string;
impact: 'High' | 'Medium' | 'Low';
desc: string;
dependencies: string[];
selected: boolean;
}
export interface LegacyOptimization {
id: number;
title: string;
issue: string;
suggestion: string;
impact: string;
status: 'analyzed' | 'pending';
}

26
tailwind.config.js Normal file
View File

@ -0,0 +1,26 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
keyframes: {
'fade-in': {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
'bounce-short': {
'0%, 100%': { transform: 'translateY(0)' },
'50%': { transform: 'translateY(-10px)' },
},
},
animation: {
'fade-in': 'fade-in 0.3s ease-out',
'bounce-short': 'bounce-short 1s ease-in-out infinite',
},
},
},
plugins: [],
}

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

17
vite.config.ts Normal file
View File

@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0', // 允许外部访问
port: 5173, // 默认端口
strictPort: false, // 如果端口被占用,自动尝试下一个可用端口
open: false, // 不自动打开浏览器
},
preview: {
host: '0.0.0.0', // 预览模式也允许外部访问
port: 4173,
},
})