fix(plan): P2 React 降级18 + 功能域代码扩展 + debounce提取

This commit is contained in:
zizi 2026-05-24 19:05:13 +08:00
parent 0b9b8cb4b0
commit f012772158

View File

@ -6,7 +6,7 @@
**架构:** Vite + React + TypeScript SPATanStack Query 数据层Zustand 状态管理Tiptap/ProseMirror 编辑器MSW Mock APISSE 双通道实时通信IndexedDB 安全网。
**技术栈:** React 19, TypeScript 5, Vite 6, TanStack Query 5, Zustand 4, React Hook Form 7 + Zod 3, Tiptap 2, Tailwind CSS 3, MSW 2, Vitest + Testing Library + Playwright
**技术栈:** React 18, TypeScript 5, Vite 6, TanStack Query 5, Zustand 4, React Hook Form 7 + Zod 3, Tiptap 2, Tailwind CSS 3, MSW 2, Vitest + Testing Library + Playwright
---
@ -26,7 +26,7 @@ cd muse-studio
- [ ] **Step 2: 安装核心依赖**
```bash
pnpm add react@19 react-dom@19 react-router-dom@7
pnpm add react@18 react-dom@18 react-router-dom@6
pnpm add @tanstack/react-query@5
pnpm add zustand@4
pnpm add react-hook-form@7 zod@3 @hookform/resolvers@3
@ -680,6 +680,8 @@ import Placeholder from '@tiptap/extension-placeholder';
import { useEffect, useCallback } from 'react';
import { useEditorStore } from '@/stores/editorStore';
import { saveBlockDraft, getBlockDraft, removeBlockDraft } from '@/lib/indexed-db';
import { debounce } from '@/lib/debounce';
import { api } from '@/lib/api';
interface MuseEditorProps {
blockId: string;
@ -739,10 +741,15 @@ export default function MuseEditor({ blockId, initialContent, revision }: MuseEd
</div>
);
}
```
function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): T {
**关键共享模块:** `src/lib/debounce.ts`
```typescript
// src/lib/debounce.ts — 通用去抖工具函数
export function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): T {
let timer: ReturnType<typeof setTimeout>;
return ((...args) => {
return ((...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
}) as T;
@ -759,20 +766,287 @@ function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): T {
git add -A && git commit -m "feat(editor): Tiptap 编辑器集成 + IndexedDB 安全网 + 自动保存"
```
### Feature 2-6: 其余功能域
### Feature 2: 我的作品 + 工作台
每个功能域按相同 TDD 模式开发:
1. 编写组件测试Vitest + Testing Library
2. 实现组件和 Hook
3. 编写 E2E 测试Playwright
4. 测试通过
5. 提交
**涉及的 API 端点:** `GET /works``POST /works``GET /works/:workId``DELETE /works/:workId``GET /works/:workId/chapters``POST /works/:workId/chapters``PATCH /works/:workId/chapters/:chapterId``DELETE /works/:workId/chapters/:chapterId`
优先级顺序:
- **F2 我的作品 + 工作台**: 作品列表/创建/删除、章节管理、Block 分割
- **F3 知识库工作台**: 知识面板、实体/关系可视化、知识草稿管理
- **F4 智能体工作台**: 智能体列表/创建/配置/槽位绑定
- **F5 市场 + 个人中心**: 市场浏览/安装/绑定、个人中心
**文件:**
- 创建: `src/features/editor/components/WorkListPage.tsx`
- 创建: `src/features/editor/components/CreateWorkModal.tsx`
- 创建: `src/features/editor/components/ChapterPanel.tsx`
- 创建: `src/features/editor/hooks/useWorks.ts`
**关键组件与 Props:**
| 组件 | Props | 说明 |
|------|-------|------|
| WorkListPage | — | 作品列表页,使用 TanStack Query 拉取列表,展示卡片网格 |
| CreateWorkModal | `open: boolean; onClose: () => void; onCreated: (work: WorkVO) => void` | 创建作品弹窗React Hook Form + Zod 校验 |
| ChapterPanel | `workId: string; chapters: ChapterVO[]; activeChapterId?: string; onSelect: (chapterId: string) => void; onDelete: (chapterId: string) => void` | 章节管理侧面板,支持拖拽排序 |
**代码示例 — `useWorks` Hook:**
```typescript
// src/features/editor/hooks/useWorks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type { WorkVO, WorkCreateDTO } from '@/types/openapi';
export function useWorks(pageNo = 1, pageSize = 20) {
return useQuery({
queryKey: ['works', pageNo, pageSize],
queryFn: () => api.get<{ total: number; list: WorkVO[] }>(`/works?pageNo=${pageNo}&pageSize=${pageSize}`),
});
}
export function useCreateWork() {
const qc = useQueryClient();
return useMutation({
mutationFn: (dto: WorkCreateDTO) => api.post<WorkVO>('/works', dto),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['works'] }); },
});
}
export function useDeleteWork() {
const qc = useQueryClient();
return useMutation({
mutationFn: (workId: string) => api.delete(`/works/${workId}`),
onSuccess: () => { qc.invalidateQueries({ queryKey: ['works'] }); },
});
}
```
**测试场景:**
1. 列表页加载展示 3 个 Mock 作品卡片
2. 点击「创建作品」按钮,弹窗打开,填写表单,提交后列表刷新
3. 删除按钮点击后弹出确认框,确认后列表移除该条目
4. 点击作品卡片进入章节面板,展示章节列表
- [ ] **Task: 提交**
```bash
git add -A && git commit -m "feat(workspace): 作品列表/创建/删除 + 章节管理面板"
```
---
### Feature 3: 知识库工作台
**涉及的 API 端点:** `GET /knowledge/:workId/entities``POST /knowledge/:workId/entities``PATCH /knowledge/:workId/entities/:entityId``GET /knowledge/:workId/drafts``POST /knowledge/:workId/drafts/:draftId/confirm``POST /knowledge/:workId/drafts/:draftId/reject``GET /knowledge/:workId/graph`
**文件:**
- 创建: `src/features/knowledge/components/EntityList.tsx`
- 创建: `src/features/knowledge/components/DraftPanel.tsx`
- 创建: `src/features/knowledge/components/GraphStub.tsx`
- 创建: `src/features/knowledge/hooks/useKnowledge.ts`
**关键组件与 Props:**
| 组件 | Props | 说明 |
|------|-------|------|
| EntityList | `workId: string` | 知识实体列表,支持按类型筛选、搜索关键字 |
| DraftPanel | `workId: string; drafts: KnowledgeDraftVO[]; onConfirm: (draftId: string) => void; onReject: (draftId: string, reason: string) => void` | 知识草稿确认/拒绝面板,拒绝需填写原因 |
| GraphStub | `workId: string; width?: number; height?: number` | 知识图谱可视化占位,后续接入 RAGFlow GraphRAG API |
**代码示例 — `useKnowledgeDrafts` Hook:**
```typescript
// src/features/knowledge/hooks/useKnowledge.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type { KnowledgeDraftVO, KnowledgeEntityVO } from '@/types/openapi';
export function useKnowledgeEntities(workId: string) {
return useQuery({
queryKey: ['knowledge', workId, 'entities'],
queryFn: () => api.get<KnowledgeEntityVO[]>(`/knowledge/${workId}/entities`),
enabled: !!workId,
});
}
export function useKnowledgeDrafts(workId: string) {
return useQuery({
queryKey: ['knowledge', workId, 'drafts'],
queryFn: () => api.get<KnowledgeDraftVO[]>(`/knowledge/${workId}/drafts`),
enabled: !!workId,
});
}
export function useConfirmDraft() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ workId, draftId }: { workId: string; draftId: string }) =>
api.post(`/knowledge/${workId}/drafts/${draftId}/confirm`),
onSuccess: (_, vars) => {
qc.invalidateQueries({ queryKey: ['knowledge', vars.workId] });
},
});
}
export function useRejectDraft() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ workId, draftId, reason }: { workId: string; draftId: string; reason: string }) =>
api.post(`/knowledge/${workId}/drafts/${draftId}/reject`, { reason }),
onSuccess: (_, vars) => {
qc.invalidateQueries({ queryKey: ['knowledge', vars.workId] });
},
});
}
```
**测试场景:**
1. 实体列表加载展示,按类型(人物/地点/事件)筛选
2. 草稿面板展示待确认列表,点击「确认」更新状态
3. 点击「拒绝」,弹出拒绝原因输入框,提交后刷新
4. 知识图谱占位区域正确渲染,显示 "Graph visualization coming soon" 占位文本
- [ ] **Task: 提交**
```bash
git add -A && git commit -m "feat(knowledge): 知识实体列表 + 草稿确认/拒绝面板 + 图谱占位"
```
---
### Feature 4: 智能体工作台
**涉及的 API 端点:** `GET /agents``POST /agents``GET /agents/:agentId``PATCH /agents/:agentId``DELETE /agents/:agentId``GET /agents/:agentId/slots``PATCH /agents/:agentId/slots`
**文件:**
- 创建: `src/features/agent/components/AgentList.tsx`
- 创建: `src/features/agent/components/AgentCreateForm.tsx`
- 创建: `src/features/agent/components/SlotBindingPanel.tsx`
- 创建: `src/features/agent/hooks/useAgents.ts`
**关键组件与 Props:**
| 组件 | Props | 说明 |
|------|-------|------|
| AgentList | `onSelect: (agentId: string) => void` | 智能体列表卡片,展示名称、模型、状态 |
| AgentCreateForm | `onCreated: (agent: AgentVO) => void; onCancel: () => void` | 创建智能体表单名称、system prompt、模型选择、温度等 |
| SlotBindingPanel | `agentId: string; slots: AgentSlotVO[]; onBind: (slotId: string, toolId: string) => void` | 槽位-工具绑定面板,支持搜索可用工具 |
**代码示例 — `AgentList` 组件:**
```tsx
// src/features/agent/components/AgentList.tsx
import { useAgents } from '../hooks/useAgents';
interface AgentListProps {
onSelect: (agentId: string) => void;
}
export default function AgentList({ onSelect }: AgentListProps) {
const { data, isLoading, error } = useAgents();
if (isLoading) return <div className="p-4">加载中...</div>;
if (error) return <div className="p-4 text-red-500">加载失败</div>;
return (
<div className="grid grid-cols-3 gap-4 p-4">
{data?.list.map((agent) => (
<div
key={agent.id}
className="border rounded-lg p-4 cursor-pointer hover:shadow-md transition-shadow"
onClick={() => onSelect(agent.id)}
>
<h3 className="font-bold text-lg">{agent.name}</h3>
<p className="text-sm text-gray-500 mt-1">{agent.model}</p>
<span className={"inline-block mt-2 px-2 py-1 rounded text-xs " +
(agent.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500')
}>
{agent.status === 'active' ? '运行中' : '已停用'}
</span>
</div>
))}
</div>
);
}
```
**测试场景:**
1. 智能体列表加载展示,每个卡片显示名称、模型、状态标签
2. 点击「创建智能体」打开表单,填写 system prompt、选择模型提交后列表刷新
3. 点击卡片进入槽位绑定面板,为智能体绑定/解绑工具槽位
4. 删除智能体,确认后列表中移除
- [ ] **Task: 提交**
```bash
git add -A && git commit -m "feat(agent): 智能体列表 + 创建表单 + 槽位绑定面板"
```
---
### Feature 5: 市场 + 个人中心
**涉及的 API 端点:** `GET /market/assets``GET /market/assets/:assetId``POST /market/assets/:assetId/install``GET /account/profile``PATCH /account/profile``GET /account/usage-stats``GET /account/entitlements`
**文件:**
- 创建: `src/features/market/components/MarketBrowse.tsx`
- 创建: `src/features/market/hooks/useMarket.ts`
- 创建: `src/features/account/components/PersonalCenter.tsx`
- 创建: `src/features/account/components/UsageStats.tsx`
- 创建: `src/features/account/hooks/useAccount.ts`
**关键组件与 Props:**
| 组件 | Props | 说明 |
|------|-------|------|
| MarketBrowse | `onInstall: (assetId: string) => void` | 市场资源浏览页,支持按类型/标签筛选,展示资产卡片网格 |
| PersonalCenter | — | 个人中心:头像、昵称、配额详情、使用统计概览 |
| UsageStats | `stats: UsageStatsVO` | 用量统计面板Token 消耗、存储用量、API 调用次数 |
**代码示例 — `UsageStats` 组件:**
```tsx
// src/features/account/components/UsageStats.tsx
import type { UsageStatsVO } from '@/types/openapi';
interface UsageStatsProps {
stats: UsageStatsVO;
}
export default function UsageStats({ stats }: UsageStatsProps) {
const items: { label: string; value: string; used: number; total: number }[] = [
{ label: 'Token 消耗', value: `${(stats.tokenUsed / 1000).toFixed(1)}K`, used: stats.tokenUsed, total: stats.tokenQuota },
{ label: '存储用量', value: `${(stats.storageUsed / 1024 / 1024).toFixed(0)} MB`, used: stats.storageUsed, total: stats.storageQuota },
{ label: 'API 调用', value: `${stats.apiCalls}`, used: stats.apiCalls, total: stats.apiQuota },
];
return (
<div className="space-y-3 p-4">
{items.map((item) => (
<div key={item.label}>
<div className="flex justify-between text-sm mb-1">
<span className="text-gray-600">{item.label}</span>
<span className="text-gray-900">{item.value} / {item.total > 0 ? item.total : '无限制'}</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: item.total > 0 ? `${Math.min((item.used / item.total) * 100, 100)}%` : '0%' }}
/>
</div>
</div>
))}
</div>
);
}
```
**测试场景:**
1. 市场页面加载展示资产卡片,筛选器切换有效
2. 点击「安装」按钮,调用安装 API按钮变为「已安装」
3. 个人中心显示用户信息、配额用量进度条
4. 用量统计各指标数据正确渲染,百分比进度条与实际数据成比例
- [ ] **Task: 提交**
```bash
git add -A && git commit -m "feat(market+account): 市场资源浏览 + 个人中心 + 用量统计"
```
---