fix(plan): P2 React 降级18 + 功能域代码扩展 + debounce提取
This commit is contained in:
parent
0b9b8cb4b0
commit
f012772158
@ -6,7 +6,7 @@
|
||||
|
||||
**架构:** Vite + React + TypeScript SPA,TanStack Query 数据层,Zustand 状态管理,Tiptap/ProseMirror 编辑器,MSW Mock API,SSE 双通道实时通信,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): 市场资源浏览 + 个人中心 + 用量统计"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user