feat(admin): Add user management and upgrade to module permission system
Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
# 宸ュ叿C Day 4-5 鍓嶇<EFBFBD>寮€鍙戣<EFBFBD>鍒掞紙鏈€缁堢増锛?
|
||||
# 工具C Day 4-5 前端开发计划(最终版)
|
||||
|
||||
> **制定日期**: 2025-12-07
|
||||
> **寮€鍙戠洰鏍?*: Tool C鍓嶇<EFBFBD>MVP - AI椹卞姩鐨勭<EFBFBD>鐮旀暟鎹<EFBFBD>紪杈戝櫒
|
||||
> **棰勮<EFBFBD>宸ユ椂**: 12-16灏忔椂锛圖ay 4: 6-8h, Day 5: 6-8h锛?
|
||||
> **开发目标**: Tool C前端MVP - AI驱动的科研数据编辑器
|
||||
> **预计工时**: 12-16小时(Day 4: 6-8h, Day 5: 6-8h)
|
||||
> **核心目标**: 端到端AI数据清洗流程可用
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心决策
|
||||
|
||||
| 鍐崇瓥椤?| 鏈€缁堟柟妗?| 鐞嗙敱 |
|
||||
| 决策项 | 最终方案 | 理由 |
|
||||
|--------|---------|------|
|
||||
| **琛ㄦ牸缁勪欢** | **AG Grid Community** | 鐢ㄦ埛寮虹儓瑕佹眰锛孍xcel绾т綋楠?|
|
||||
| **表格组件** | **AG Grid Community** | 用户强烈要求,Excel级体验 |
|
||||
| **开发优先级** | **AI核心功能优先** | 先验证后端,UI细节后续优化 |
|
||||
| **UI风格** | **严格还原原型V6** | Emerald绿色主题,符合Tool C定位 |
|
||||
| **娴嬭瘯鏁版嵁** | **鐪熷疄鍖荤枟鏁版嵁** | cqol-demo.csv (21鍒梮300+琛? |
|
||||
| **测试数据** | **真实医疗数据** | cqol-demo.csv (21列x300+行) |
|
||||
| **Day 4-5目标** | **AI核心功能可用** | 上传→AI对话→执行→更新表格 |
|
||||
| **代码风格** | **同Tool B标准** | TypeScript + Tailwind + Lucide |
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"ag-grid-react": "^31.0.0", // React集成
|
||||
"react": "^18.2.0",
|
||||
"react-router-dom": "^6.x",
|
||||
"lucide-react": "^0.x", // 鍥炬爣搴?
|
||||
"axios": "^1.x" // HTTP瀹㈡埛绔?
|
||||
"lucide-react": "^0.x", // 图标库
|
||||
"axios": "^1.x" // HTTP客户端
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,76 +39,76 @@
|
||||
- **Tailwind CSS**: 原子化CSS
|
||||
- **AG Grid Community**: 开源版,功能足够MVP
|
||||
- **Prism.js**: 代码语法高亮
|
||||
- **React Markdown**: Markdown娓叉煋锛圓I鍥炲<EFBFBD>锛?
|
||||
- **React Markdown**: Markdown渲染(AI回复)
|
||||
|
||||
---
|
||||
|
||||
## 📐 页面布局设计
|
||||
|
||||
```
|
||||
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?
|
||||
鈹?Header (h-14) 鈹?
|
||||
鈹?[馃煝 绉戠爺鏁版嵁缂栬緫鍣╙ lung_cancer.csv [鎾ら攢/閲嶅仛] [瀵煎嚭] 鈹?
|
||||
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?
|
||||
鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹<EFBFBD>攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?
|
||||
鈹?Left Panel (flex-1) 鈹?Right Sidebar (w-[420px]) 鈹?
|
||||
鈹? 鈹? 鈹?
|
||||
鈹?鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?
|
||||
鈹?鈹?Toolbar (h-16) 鈹?鈹?鈹?Tab: [Chat] [Insights] 鈹?鈹?
|
||||
鈹?鈹?7涓<37>揩鎹锋寜閽?+ 鎼滅储 鈹?鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?
|
||||
鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 鈹?
|
||||
鈹? 鈹?鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?
|
||||
鈹?鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?鈹?Chat Messages Area 鈹?鈹?
|
||||
鈹?鈹? 鈹?鈹?鈹?- 鐢ㄦ埛娑堟伅 鈹?鈹?
|
||||
鈹?鈹? AG Grid Table 鈹?鈹?鈹?- AI娑堟伅锛堝惈浠g爜鍧楋級 鈹?鈹?
|
||||
鈹?鈹? (Excel椋庢牸) 鈹?鈹?鈹?- 绯荤粺娑堟伅 鈹?鈹?
|
||||
鈹?鈹? 鈹?鈹?鈹? 鈹?鈹?
|
||||
鈹?鈹? 21鍒?x 300+琛? 鈹?鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?
|
||||
鈹?鈹? 鈹?鈹? 鈹?
|
||||
鈹?鈹? 鏀<>寔锛? 鈹?鈹?鈹屸攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?
|
||||
鈹?鈹? - 鍒楁帓搴? 鈹?鈹?鈹?Input + Send Button 鈹?鈹?
|
||||
鈹?鈹? - 鍒楀<E98D92>璋冩暣 鈹?鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹?
|
||||
鈹?鈹? - 鍗曞厓鏍肩紪杈戯紙鍚庢湡锛?鈹?鈹? 鈹?
|
||||
鈹?鈹? - 閫夋嫨楂樹寒 鈹?鈹? 鈹?
|
||||
鈹?鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?鈹? 鈹?
|
||||
鈹斺攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹粹攢鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹€鈹?
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Header (h-14) │
|
||||
│ [🟢 科研数据编辑器] lung_cancer.csv [撤销/重做] [导出] │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────┬─────────────────────────────────┐
|
||||
│ Left Panel (flex-1) │ Right Sidebar (w-[420px]) │
|
||||
│ │ │
|
||||
│ ┌──────────────────────┐ │ ┌───────────────────────────┐ │
|
||||
│ │ Toolbar (h-16) │ │ │ Tab: [Chat] [Insights] │ │
|
||||
│ │ 7个快捷按钮 + 搜索 │ │ └───────────────────────────┘ │
|
||||
│ └──────────────────────┘ │ │
|
||||
│ │ ┌───────────────────────────┐ │
|
||||
│ ┌──────────────────────┐ │ │ Chat Messages Area │ │
|
||||
│ │ │ │ │ - 用户消息 │ │
|
||||
│ │ AG Grid Table │ │ │ - AI消息(含代码块) │ │
|
||||
│ │ (Excel风格) │ │ │ - 系统消息 │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ 21列 x 300+行 │ │ └───────────────────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ 支持: │ │ ┌───────────────────────────┐ │
|
||||
│ │ - 列排序 │ │ │ Input + Send Button │ │
|
||||
│ │ - 列宽调整 │ │ └───────────────────────────┘ │
|
||||
│ │ - 单元格编辑(后期) │ │ │
|
||||
│ │ - 选择高亮 │ │ │
|
||||
│ └──────────────────────┘ │ │
|
||||
└──────────────────────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 馃梻锔?鏂囦欢缁撴瀯瑙勫垝
|
||||
## 🗂️ 文件结构规划
|
||||
|
||||
```
|
||||
frontend-v2/src/modules/dc/pages/tool-c/
|
||||
鈹溾攢鈹€ index.tsx # 涓诲叆鍙o紙鐘舵€佺<EFBFBD>鐞?甯冨眬锛?
|
||||
├── index.tsx # 主入口(状态管理+布局)
|
||||
├── components/
|
||||
鈹? 鈹溾攢鈹€ Header.tsx # 椤堕儴鏍?
|
||||
鈹? 鈹溾攢鈹€ Toolbar.tsx # 宸ュ叿鏍忥紙7涓<EFBFBD>寜閽<EFBFBD>級
|
||||
鈹? 鈹溾攢鈹€ DataGrid.tsx # AG Grid琛ㄦ牸锛堟牳蹇冿級
|
||||
鈹? 鈹溾攢鈹€ Sidebar.tsx # 鍙充晶鏍忓<EFBFBD>鍣?
|
||||
鈹? 鈹溾攢鈹€ ChatPanel.tsx # Chat闈㈡澘
|
||||
鈹? 鈹溾攢鈹€ InsightsPanel.tsx # Insights闈㈡澘
|
||||
鈹? 鈹溾攢鈹€ MessageList.tsx # 娑堟伅鍒楄〃
|
||||
鈹? 鈹溾攢鈹€ MessageItem.tsx # 鍗曟潯娑堟伅
|
||||
鈹? 鈹溾攢鈹€ CodeBlock.tsx # 浠g爜鍧楁覆鏌?
|
||||
鈹? 鈹斺攢鈹€ InputArea.tsx # 杈撳叆妗?
|
||||
│ ├── Header.tsx # 顶部栏
|
||||
│ ├── Toolbar.tsx # 工具栏(7个按钮)
|
||||
│ ├── DataGrid.tsx # AG Grid表格(核心)
|
||||
│ ├── Sidebar.tsx # 右侧栏容器
|
||||
│ ├── ChatPanel.tsx # Chat面板
|
||||
│ ├── InsightsPanel.tsx # Insights面板
|
||||
│ ├── MessageList.tsx # 消息列表
|
||||
│ ├── MessageItem.tsx # 单条消息
|
||||
│ ├── CodeBlock.tsx # 代码块渲染
|
||||
│ └── InputArea.tsx # 输入框
|
||||
├── hooks/
|
||||
鈹? 鈹溾攢鈹€ useToolC.ts # 鏍稿績鐘舵€佺<EFBFBD>鐞咹ook
|
||||
鈹? 鈹溾攢鈹€ useSession.ts # Session绠$悊
|
||||
鈹? 鈹溾攢鈹€ useChat.ts # AI瀵硅瘽閫昏緫
|
||||
鈹? 鈹斺攢鈹€ useDataGrid.ts # 琛ㄦ牸鏁版嵁绠$悊
|
||||
│ ├── useToolC.ts # 核心状态管理Hook
|
||||
│ ├── useSession.ts # Session管理
|
||||
│ ├── useChat.ts # AI对话逻辑
|
||||
│ └── useDataGrid.ts # 表格数据管理
|
||||
└── types/
|
||||
└── index.ts # TypeScript类型定义
|
||||
|
||||
frontend-v2/src/modules/dc/api/
|
||||
鈹斺攢鈹€ toolC.ts # API灏佽<EFBFBD>锛?涓<>柟娉曪級
|
||||
└── toolC.ts # API封装(6个方法)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 鈴憋笍 Day 4 寮€鍙戣<EFBFBD>鍒掞紙6-8灏忔椂锛?
|
||||
## ⏱️ Day 4 开发计划(6-8小时)
|
||||
|
||||
### 闃舵<EFBFBD>1锛氶」鐩<EFBFBD>垵濮嬪寲锛?灏忔椂锛?
|
||||
### 阶段1:项目初始化(1小时)
|
||||
|
||||
#### 1.1 安装依赖
|
||||
```bash
|
||||
@@ -127,32 +127,32 @@ touch src/modules/dc/pages/tool-c/index.tsx
|
||||
|
||||
#### 1.3 更新路由配置
|
||||
```typescript
|
||||
// src/App.tsx 鎴栬矾鐢遍厤缃<EFBFBD>枃浠?
|
||||
// src/App.tsx 或路由配置文件
|
||||
<Route path="/data-cleaning/tool-c" element={<ToolC />} />
|
||||
```
|
||||
|
||||
#### 1.4 更新Portal页面
|
||||
```typescript
|
||||
// src/modules/dc/pages/Portal.tsx
|
||||
// 灏員ool C鐨剆tatus浠?disabled'鏀逛负'ready'
|
||||
// 将Tool C的status从'disabled'改为'ready'
|
||||
{
|
||||
id: 'tool-c',
|
||||
title: '绉戠爺鏁版嵁缂栬緫鍣?,
|
||||
status: 'ready', // 猸?淇<>敼杩欓噷
|
||||
title: '科研数据编辑器',
|
||||
status: 'ready', // ⭐ 修改这里
|
||||
route: '/data-cleaning/tool-c'
|
||||
}
|
||||
```
|
||||
|
||||
**浜や粯鐗?*锛?
|
||||
- 鉁?渚濊禆瀹夎<E780B9>瀹屾垚
|
||||
- 鉁?鏂囦欢缁撴瀯鍒涘缓
|
||||
- 鉁?Portal鍙<EFBFBD>偣鍑昏繘鍏<EFBFBD>ool C
|
||||
**交付物**:
|
||||
- ✅ 依赖安装完成
|
||||
- ✅ 文件结构创建
|
||||
- ✅ Portal可点击进入Tool C
|
||||
|
||||
---
|
||||
|
||||
### 闃舵<EFBFBD>2锛氶〉闈㈡<EFBFBD>鏋舵惌寤猴紙2灏忔椂锛?
|
||||
### 阶段2:页面框架搭建(2小时)
|
||||
|
||||
#### 2.1 鍒涘缓涓诲叆鍙?(index.tsx)
|
||||
#### 2.1 创建主入口 (index.tsx)
|
||||
```typescript
|
||||
import { useState } from 'react';
|
||||
import Header from './components/Header';
|
||||
@@ -229,7 +229,7 @@ const Header: React.FC<HeaderProps> = ({ fileName, onUndo, onRedo, onExport }) =
|
||||
<Table2 size={20} />
|
||||
</div>
|
||||
<span className="font-bold text-lg text-slate-900">
|
||||
绉戠爺鏁版嵁缂栬緫鍣?
|
||||
科研数据编辑器
|
||||
<span className="text-emerald-600 text-xs px-1.5 py-0.5 bg-emerald-50 rounded-full ml-1">
|
||||
Pro
|
||||
</span>
|
||||
@@ -283,8 +283,8 @@ const ToolbarButton = ({ icon: Icon, label, colorClass }: any) => (
|
||||
const Toolbar = () => {
|
||||
return (
|
||||
<div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-1 overflow-x-auto flex-none shadow-sm z-10">
|
||||
<ToolbarButton icon={Calculator} label="鐢熸垚鏂板彉閲? colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100" />
|
||||
<ToolbarButton icon={CalendarClock} label="鏃堕棿宸? colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100" />
|
||||
<ToolbarButton icon={Calculator} label="生成新变量" colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100" />
|
||||
<ToolbarButton icon={CalendarClock} label="时间差" colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100" />
|
||||
<ToolbarButton icon={ArrowLeftRight} label="横纵转换" colorClass="text-cyan-600 bg-cyan-50 hover:bg-cyan-100" />
|
||||
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
|
||||
<ToolbarButton icon={FileSearch} label="查重" colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100" />
|
||||
@@ -296,7 +296,7 @@ const Toolbar = () => {
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
className="pl-9 pr-4 py-1.5 text-sm bg-slate-100 border-none rounded-full w-48 focus:w-64 transition-all outline-none focus:ring-2 focus:ring-emerald-500/20"
|
||||
placeholder="鎼滅储鍊?.."
|
||||
placeholder="搜索值..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,14 +306,14 @@ const Toolbar = () => {
|
||||
export default Toolbar;
|
||||
```
|
||||
|
||||
**浜や粯鐗?*锛?
|
||||
- 鉁?Header瀹屾垚锛堝甫杩斿洖鎸夐挳锛?
|
||||
- 鉁?Toolbar瀹屾垚锛?涓<>寜閽<E5AF9C>級
|
||||
- 鉁?鍩虹<E98DA9>甯冨眬鍙<E79CAC><E98D99>
|
||||
**交付物**:
|
||||
- ✅ Header完成(带返回按钮)
|
||||
- ✅ Toolbar完成(7个按钮)
|
||||
- ✅ 基础布局可见
|
||||
|
||||
---
|
||||
|
||||
### 闃舵<EFBFBD>3锛欰G Grid闆嗘垚锛?-4灏忔椂锛?
|
||||
### 阶段3:AG Grid集成(3-4小时)
|
||||
|
||||
#### 3.1 创建DataGrid组件
|
||||
```typescript
|
||||
@@ -352,7 +352,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, columns, onCellValueChanged }
|
||||
}));
|
||||
}, [columns]);
|
||||
|
||||
// 榛樿<EFBFBD>鍒楅厤缃?
|
||||
// 默认列配置
|
||||
const defaultColDef: ColDef = {
|
||||
flex: 0,
|
||||
sortable: true,
|
||||
@@ -360,13 +360,13 @@ const DataGrid: React.FC<DataGridProps> = ({ data, columns, onCellValueChanged }
|
||||
resizable: true,
|
||||
};
|
||||
|
||||
// 濡傛灉娌℃湁鏁版嵁锛屾樉绀虹┖鐘舵€?
|
||||
// 如果没有数据,显示空状态
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 shadow-sm rounded-xl p-12 text-center">
|
||||
<div className="text-slate-400 text-sm">
|
||||
<p className="mb-2">暂无数据</p>
|
||||
<p>璇峰湪鍙充晶AI鍔╂墜涓<EFBFBD>笂浼犳枃浠?/p>
|
||||
<p>请在右侧AI助手中上传文件</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -421,9 +421,9 @@ export default DataGrid;
|
||||
```typescript
|
||||
// 临时测试:在index.tsx中硬编码测试数据
|
||||
const mockData = [
|
||||
{ id: 'P001', age: 27, sex: '濂?, bmi: 23.1, smoke: '鍚? },
|
||||
{ id: 'P002', age: 24, sex: '鐢?, bmi: 16.7, smoke: '鏄? },
|
||||
{ id: 'P003', age: null, sex: '鐢?, bmi: null, smoke: '鏄? },
|
||||
{ id: 'P001', age: 27, sex: '女', bmi: 23.1, smoke: '否' },
|
||||
{ id: 'P002', age: 24, sex: '男', bmi: 16.7, smoke: '是' },
|
||||
{ id: 'P003', age: null, sex: '男', bmi: null, smoke: '是' },
|
||||
];
|
||||
|
||||
const mockColumns = [
|
||||
@@ -431,21 +431,21 @@ const mockColumns = [
|
||||
{ id: 'age', name: '年龄', type: 'number' },
|
||||
{ id: 'sex', name: '性别', type: 'category' },
|
||||
{ id: 'bmi', name: 'BMI', type: 'number' },
|
||||
{ id: 'smoke', name: '鍚哥儫鍙?, type: 'category' },
|
||||
{ id: 'smoke', name: '吸烟史', type: 'category' },
|
||||
];
|
||||
```
|
||||
|
||||
**浜や粯鐗?*锛?
|
||||
- 鉁?AG Grid鎴愬姛娓叉煋
|
||||
- 鉁?缂哄け鍊奸珮浜<E78FAE>樉绀?
|
||||
- 鉁?鍒楁帓搴?杩囨护鍙<E68AA4>敤
|
||||
- 鉁?鍒楀<E98D92>鍙<EFBFBD>皟鏁?
|
||||
**交付物**:
|
||||
- ✅ AG Grid成功渲染
|
||||
- ✅ 缺失值高亮显示
|
||||
- ✅ 列排序/过滤可用
|
||||
- ✅ 列宽可调整
|
||||
|
||||
---
|
||||
|
||||
## 鈴憋笍 Day 5 寮€鍙戣<EFBFBD>鍒掞紙6-8灏忔椂锛?
|
||||
## ⏱️ Day 5 开发计划(6-8小时)
|
||||
|
||||
### 闃舵<EFBFBD>4锛欰I Chat闈㈡澘锛?灏忔椂锛?
|
||||
### 阶段4:AI Chat面板(3小时)
|
||||
|
||||
#### 4.1 创建Sidebar组件
|
||||
```typescript
|
||||
@@ -549,8 +549,8 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ messages, onSendMessage, isLoadin
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="text-center text-slate-400 text-sm py-12">
|
||||
<p className="mb-2">馃憢 鎮ㄥソ锛佹垜鏄<EFBFBD>偍鐨凙I鏁版嵁鍒嗘瀽甯?/p>
|
||||
<p>璇曡瘯璇达細"鎶婂勾榫勫ぇ浜?0鐨勬爣璁颁负鑰佸勾缁?</p>
|
||||
<p className="mb-2">👋 您好!我是您的AI数据分析师</p>
|
||||
<p>试试说:"把年龄大于60的标记为老年组"</p>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg) => (
|
||||
@@ -559,7 +559,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ messages, onSendMessage, isLoadin
|
||||
{isLoading && (
|
||||
<div className="flex items-center gap-2 text-slate-400 text-sm">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-emerald-500 border-t-transparent"></div>
|
||||
<span>AI姝e湪鎬濊€?..</span>
|
||||
<span>AI正在思考...</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
@@ -653,7 +653,7 @@ const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
|
||||
<Play size={12} /> 执行代码
|
||||
</>
|
||||
)}
|
||||
{code.status === 'running' && '鎵ц<EFBFBD>涓?..'}
|
||||
{code.status === 'running' && '执行中...'}
|
||||
{code.status === 'success' && (
|
||||
<>
|
||||
<CheckCircle2 size={12} /> 执行成功
|
||||
@@ -715,7 +715,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = 'python' }) => {
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={12} /> 宸插<EFBFBD>鍒?
|
||||
<Check size={12} /> 已复制
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -771,7 +771,7 @@ const InputArea: React.FC<InputAreaProps> = ({ onSend, disabled }) => {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="鍛婅瘔AI浣犳兂鍋氫粈涔?.."
|
||||
placeholder="告诉AI你想做什么..."
|
||||
className="flex-1 resize-none border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500"
|
||||
rows={3}
|
||||
disabled={disabled}
|
||||
@@ -785,7 +785,7 @@ const InputArea: React.FC<InputAreaProps> = ({ onSend, disabled }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-2">
|
||||
鎸?<kbd className="px-1 py-0.5 bg-slate-100 rounded">Enter</kbd> 鍙戦€侊紝
|
||||
按 <kbd className="px-1 py-0.5 bg-slate-100 rounded">Enter</kbd> 发送,
|
||||
<kbd className="px-1 py-0.5 bg-slate-100 rounded">Shift + Enter</kbd> 换行
|
||||
</div>
|
||||
</div>
|
||||
@@ -795,15 +795,15 @@ const InputArea: React.FC<InputAreaProps> = ({ onSend, disabled }) => {
|
||||
export default InputArea;
|
||||
```
|
||||
|
||||
**浜や粯鐗?*锛?
|
||||
- 鉁?Chat闈㈡澘瀹屾暣UI
|
||||
- 鉁?娑堟伅鍒楄〃娓叉煋
|
||||
- 鉁?浠g爜鍧楄<E98DA7>娉曢珮浜?
|
||||
- 鉁?杈撳叆妗嗕氦浜?
|
||||
**交付物**:
|
||||
- ✅ Chat面板完整UI
|
||||
- ✅ 消息列表渲染
|
||||
- ✅ 代码块语法高亮
|
||||
- ✅ 输入框交互
|
||||
|
||||
---
|
||||
|
||||
### 闃舵<EFBFBD>5锛欰PI闆嗘垚锛?-3灏忔椂锛?
|
||||
### 阶段5:API集成(2-3小时)
|
||||
|
||||
#### 5.1 创建API封装
|
||||
```typescript
|
||||
@@ -911,7 +911,7 @@ export const useToolC = () => {
|
||||
setMessages([{
|
||||
id: Date.now(),
|
||||
role: 'system',
|
||||
content: `鉁?鏂囦欢涓婁紶鎴愬姛锛佸叡${preview.data.totalRows}琛?x ${preview.data.totalCols}鍒梎,
|
||||
content: `✅ 文件上传成功!共${preview.data.totalRows}行 x ${preview.data.totalCols}列`,
|
||||
}]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -919,14 +919,14 @@ export const useToolC = () => {
|
||||
setMessages([{
|
||||
id: Date.now(),
|
||||
role: 'system',
|
||||
content: `鉂?涓婁紶澶辫触: ${error.message}`,
|
||||
content: `❌ 上传失败: ${error.message}`,
|
||||
}]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 鍙戦€丄I娑堟伅锛堜竴姝ュ埌浣嶏細鐢熸垚+鎵ц<E98EB5>锛?
|
||||
// 发送AI消息(一步到位:生成+执行)
|
||||
const handleSendMessage = useCallback(async (message: string) => {
|
||||
if (!sessionId) {
|
||||
alert('请先上传文件');
|
||||
@@ -943,7 +943,7 @@ export const useToolC = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 璋冪敤process鎺ュ彛锛堢敓鎴?鎵ц<E98EB5>锛屼竴姝ュ埌浣嶏級
|
||||
// 调用process接口(生成+执行,一步到位)
|
||||
const result = await api.processMessage(sessionId, message);
|
||||
|
||||
if (result.success) {
|
||||
@@ -958,20 +958,20 @@ export const useToolC = () => {
|
||||
},
|
||||
}]);
|
||||
|
||||
// 濡傛灉鎵ц<EFBFBD>鎴愬姛锛屾洿鏂拌〃鏍兼暟鎹?
|
||||
// 如果执行成功,更新表格数据
|
||||
if (result.data.executeResult.success && result.data.executeResult.newDataPreview) {
|
||||
setData(result.data.executeResult.newDataPreview);
|
||||
|
||||
setMessages(prev => [...prev, {
|
||||
id: Date.now() + 2,
|
||||
role: 'system',
|
||||
content: `鉁?浠g爜鎵ц<EFBFBD>鎴愬姛锛佽〃鏍煎凡鏇存柊銆?{result.data.retryCount > 0 ? `锛堥噸璇?{result.data.retryCount}娆″悗鎴愬姛锛塦 : ''}`,
|
||||
content: `✅ 代码执行成功!表格已更新。${result.data.retryCount > 0 ? `(重试${result.data.retryCount}次后成功)` : ''}`,
|
||||
}]);
|
||||
} else if (!result.data.executeResult.success) {
|
||||
setMessages(prev => [...prev, {
|
||||
id: Date.now() + 2,
|
||||
role: 'system',
|
||||
content: `鉂?鎵ц<E98EB5>澶辫触: ${result.data.executeResult.error}`,
|
||||
content: `❌ 执行失败: ${result.data.executeResult.error}`,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
@@ -980,7 +980,7 @@ export const useToolC = () => {
|
||||
setMessages(prev => [...prev, {
|
||||
id: Date.now() + 1,
|
||||
role: 'system',
|
||||
content: `鉂?澶勭悊澶辫触: ${error.response?.data?.error || error.message}`,
|
||||
content: `❌ 处理失败: ${error.response?.data?.error || error.message}`,
|
||||
}]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -1038,14 +1038,14 @@ const ToolC = () => {
|
||||
};
|
||||
```
|
||||
|
||||
**浜や粯鐗?*锛?
|
||||
- 鉁?6涓狝PI鏂规硶灏佽<E7818F>
|
||||
- 鉁?useToolC鏍稿績Hook
|
||||
- 鉁?绔<>埌绔<E59F8C>祦绋嬪彲鐢?
|
||||
**交付物**:
|
||||
- ✅ 6个API方法封装
|
||||
- ✅ useToolC核心Hook
|
||||
- ✅ 端到端流程可用
|
||||
|
||||
---
|
||||
|
||||
### 闃舵<EFBFBD>6锛氭枃浠朵笂浼?+ InsightsPanel锛?灏忔椂锛?
|
||||
### 阶段6:文件上传 + InsightsPanel(1小时)
|
||||
|
||||
#### 6.1 添加文件上传UI
|
||||
```typescript
|
||||
@@ -1072,7 +1072,7 @@ const ToolC = () => {
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">
|
||||
鏀<EFBFBD>寔鏍煎紡锛欳SV, XLSX, XLS锛堟渶澶?0MB锛?
|
||||
支持格式:CSV, XLSX, XLS(最大10MB)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1110,11 +1110,11 @@ const InsightsPanel: React.FC<InsightsPanelProps> = ({ dataStats }) => {
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-600">鎬昏<EFBFBD>鏁?/span>
|
||||
<span className="text-slate-600">总行数</span>
|
||||
<span className="font-medium">{dataStats.totalRows}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-600">鎬诲垪鏁?/span>
|
||||
<span className="text-slate-600">总列数</span>
|
||||
<span className="font-medium">{dataStats.totalCols}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1123,7 +1123,7 @@ const InsightsPanel: React.FC<InsightsPanelProps> = ({ dataStats }) => {
|
||||
<div className="bg-red-50 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<AlertCircle size={16} className="text-red-600" />
|
||||
<h3 className="font-semibold text-sm text-red-900">缂哄け鍊?/h3>
|
||||
<h3 className="font-semibold text-sm text-red-900">缺失值</h3>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
@@ -1131,7 +1131,7 @@ const InsightsPanel: React.FC<InsightsPanelProps> = ({ dataStats }) => {
|
||||
<span className="font-medium text-red-900">{dataStats.missingCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-red-700">缂哄け鐜?/span>
|
||||
<span className="text-red-700">缺失率</span>
|
||||
<span className="font-medium text-red-900">{dataStats.missingRate.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1143,26 +1143,26 @@ const InsightsPanel: React.FC<InsightsPanelProps> = ({ dataStats }) => {
|
||||
export default InsightsPanel;
|
||||
```
|
||||
|
||||
**浜や粯鐗?*锛?
|
||||
- 鉁?鏂囦欢涓婁紶鍔熻兘
|
||||
- 鉁?Insights闈㈡澘
|
||||
**交付物**:
|
||||
- ✅ 文件上传功能
|
||||
- ✅ Insights面板
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Day 4-5 验收标准
|
||||
|
||||
### 蹇呰揪鐩<EFBFBD>爣锛圡VP锛?
|
||||
- [x] **椤甸潰鍙<EFBFBD><EFBFBD>闂?*锛歅ortal鍙<6C>偣鍑昏繘鍏<E7B998>ool C
|
||||
### 必达目标(MVP)
|
||||
- [x] **页面可访问**:Portal可点击进入Tool C
|
||||
- [x] **文件上传**:支持CSV/Excel上传
|
||||
- [x] **琛ㄦ牸灞曠ず**锛欰G Grid娓叉煋鐪熷疄鏁版嵁锛坈qol-demo.csv锛?
|
||||
- [x] **AI瀵硅瘽**锛氬彸渚<E5BDB8>hat闈㈡澘鍙<E6BE98>彂閫佹秷鎭?
|
||||
- [x] **浠g爜鐢熸垚**锛欰I鐢熸垚Python浠g爜骞舵樉绀?
|
||||
- [x] **表格展示**:AG Grid渲染真实数据(cqol-demo.csv)
|
||||
- [x] **AI对话**:右侧Chat面板可发送消息
|
||||
- [x] **代码生成**:AI生成Python代码并显示
|
||||
- [x] **代码执行**:点击执行按钮,表格数据更新
|
||||
- [x] **瀵硅瘽鍘嗗彶**锛氬<E9949B>杞<EFBFBD><E69D9E>璇濇<E79287>甯告樉绀?
|
||||
- [x] **对话历史**:多轮对话正常显示
|
||||
|
||||
### 鍙<EFBFBD>€夌洰鏍囷紙Day 6浼樺寲锛?
|
||||
- [ ] 鍗曞厓鏍兼墜鍔ㄧ紪杈?
|
||||
- [ ] 宸ュ叿鏍忔寜閽<EFBFBD>姛鑳?
|
||||
### 可选目标(Day 6优化)
|
||||
- [ ] 单元格手动编辑
|
||||
- [ ] 工具栏按钮功能
|
||||
- [ ] 撤销/重做
|
||||
- [ ] 导出Excel
|
||||
- [ ] 数据洞察卡片完善
|
||||
@@ -1172,40 +1172,40 @@ export default InsightsPanel;
|
||||
|
||||
## 📝 测试场景
|
||||
|
||||
### 鍦烘櫙1锛氫笂浼犳枃浠?
|
||||
### 场景1:上传文件
|
||||
1. 访问 `/data-cleaning/tool-c`
|
||||
2. 点击"上传CSV/Excel文件"
|
||||
3. 选择 `cqol-demo.csv`
|
||||
4. 楠岃瘉锛氳〃鏍兼樉绀?1鍒梮300+琛屾暟鎹?
|
||||
4. 验证:表格显示21列x300+行数据
|
||||
|
||||
### 鍦烘櫙2锛欰I缂哄け鍊煎<EFBFBD>鐞?
|
||||
1. 鍦–hat杈撳叆锛?鎶妔ex鍒楃殑缂哄け鍊煎~琛ヤ负浼楁暟"
|
||||
### 场景2:AI缺失值处理
|
||||
1. 在Chat输入:"把sex列的缺失值填补为众数"
|
||||
2. 验证:AI生成代码
|
||||
3. 点击"执行代码"
|
||||
4. 楠岃瘉锛氳〃鏍兼暟鎹<EFBFBD>洿鏂帮紝缂哄け鍊兼秷澶?
|
||||
4. 验证:表格数据更新,缺失值消失
|
||||
|
||||
### 场景3:AI年龄分组
|
||||
1. 鍦–hat杈撳叆锛?鎶奱ge鍒楁寜18銆?0鍒嗕负鏈<E8B49F>垚骞淬€佹垚骞淬€佽€佸勾涓夌粍"
|
||||
1. 在Chat输入:"把age列按18、60分为未成年、成年、老年三组"
|
||||
2. 验证:AI生成代码
|
||||
3. 点击"执行代码"
|
||||
4. 楠岃瘉锛氳〃鏍兼柊澧瀉ge_group鍒?
|
||||
4. 验证:表格新增age_group列
|
||||
|
||||
### 鍦烘櫙4锛氬<EFBFBD>璇濆巻鍙?
|
||||
1. 鍙戦€佸<EFBFBD>鏉℃秷鎭?
|
||||
2. 楠岃瘉锛氭墍鏈夊<EFBFBD>璇濅繚鐣?
|
||||
### 场景4:对话历史
|
||||
1. 发送多条消息
|
||||
2. 验证:所有对话保留
|
||||
3. 刷新页面
|
||||
4. 楠岃瘉锛氬<EFBFBD>璇濆巻鍙插姞杞芥<EFBFBD>甯?
|
||||
4. 验证:对话历史加载正常
|
||||
|
||||
---
|
||||
|
||||
## 馃毃 椋庨櫓涓庡簲瀵?
|
||||
## 🚨 风险与应对
|
||||
|
||||
| 风险 | 概率 | 影响 | 应对措施 |
|
||||
|------|------|------|---------|
|
||||
| AG Grid瀛︿範鎴愭湰楂?| 涓?| 涓?| Day 4涓婂崍闆嗕腑瀛︿範瀹樻柟鏂囨。 |
|
||||
| API璋冭瘯鏃堕棿闀?| 楂?| 涓?| 鍏堢敤Mock鏁版嵁锛屽啀瀵规帴鐪熷疄API |
|
||||
| 浠g爜璇<EFBFBD>硶楂樹寒闂<EFBFBD><EFBFBD> | 浣?| 浣?| Prism.js閰嶇疆涓嶅<E6B693>鏉傦紝鏈夌幇鎴愭柟妗?|
|
||||
| 鎬ц兘闂<EFBFBD><EFBFBD>锛?00+琛屾暟鎹<E69A9F>級| 浣?| 浣?| AG Grid鑷<64>甫铏氭嫙婊氬姩锛屾棤闇€浼樺寲 |
|
||||
| AG Grid学习成本高 | 中 | 中 | Day 4上午集中学习官方文档 |
|
||||
| API调试时间长 | 高 | 中 | 先用Mock数据,再对接真实API |
|
||||
| 代码语法高亮问题 | 低 | 低 | Prism.js配置不复杂,有现成方案 |
|
||||
| 性能问题(300+行数据)| 低 | 低 | AG Grid自带虚拟滚动,无需优化 |
|
||||
|
||||
---
|
||||
|
||||
@@ -1221,7 +1221,7 @@ npm install prismjs @types/prismjs
|
||||
# Markdown渲染(可选)
|
||||
npm install react-markdown
|
||||
|
||||
# 宸叉湁渚濊禆锛堟棤闇€瀹夎<EFBFBD>锛?
|
||||
# 已有依赖(无需安装)
|
||||
# - react
|
||||
# - react-router-dom
|
||||
# - lucide-react
|
||||
@@ -1231,7 +1231,7 @@ npm install react-markdown
|
||||
|
||||
---
|
||||
|
||||
## 馃摎 鍙傝€冭祫婧?
|
||||
## 📚 参考资源
|
||||
|
||||
- [AG Grid React Documentation](https://www.ag-grid.com/react-data-grid/)
|
||||
- [Prism.js Syntax Highlighting](https://prismjs.com/)
|
||||
@@ -1240,13 +1240,13 @@ npm install react-markdown
|
||||
|
||||
---
|
||||
|
||||
## 鉁?鏈€缁堜氦浠樼墿娓呭崟
|
||||
## ✅ 最终交付物清单
|
||||
|
||||
### Day 4
|
||||
- [x] 鏂囦欢缁撴瀯鍒涘缓锛?0涓<30>枃浠讹級
|
||||
- [x] 文件结构创建(10个文件)
|
||||
- [x] Header组件
|
||||
- [x] Toolbar组件
|
||||
- [x] DataGrid缁勪欢锛圓G Grid锛?
|
||||
- [x] DataGrid组件(AG Grid)
|
||||
- [x] 基础布局完成
|
||||
|
||||
### Day 5
|
||||
@@ -1256,20 +1256,20 @@ npm install react-markdown
|
||||
- [x] CodeBlock组件
|
||||
- [x] InputArea组件
|
||||
- [x] InsightsPanel组件
|
||||
- [x] API灏佽<EFBFBD>锛坱oolC.ts锛?
|
||||
- [x] API封装(toolC.ts)
|
||||
- [x] useToolC Hook
|
||||
- [x] 端到端流程测试通过
|
||||
|
||||
### 文档
|
||||
- [x] 鏈<EFBFBD>紑鍙戣<EFBFBD>鍒?
|
||||
- [x] 本开发计划
|
||||
- [ ] Day 4-5开发完成总结(Day 5结束后)
|
||||
- [ ] API对接文档(Day 5结束后)
|
||||
|
||||
---
|
||||
|
||||
**鍒跺畾浜?*: AI Assistant
|
||||
**纭<EFBFBD><EFBFBD>浜?*: 鐢ㄦ埛
|
||||
**寮€濮嬫椂闂?*: Day 4涓婂崍
|
||||
**制定人**: AI Assistant
|
||||
**确认人**: 用户
|
||||
**开始时间**: Day 4上午
|
||||
**预期完成**: Day 5下午
|
||||
|
||||
---
|
||||
@@ -1326,6 +1326,5 @@ npm install react-markdown
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user