From 53fb2076bc80d88c722d210aebf15565bfd5b94e Mon Sep 17 00:00:00 2001 From: lili Date: Sun, 21 Jun 2026 04:14:08 -0700 Subject: [PATCH] =?UTF-8?q?feat(studio):=20B5c-1=20planning=20=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E9=9B=86=E6=88=90=20WorkspacePage[P1]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlanningEditorPanel.tsx:列 work 的 meta-projection→选→内嵌 PlanningEditor;真后端 Meta owner 未接入投影时优雅降级(空态提示不崩,anti-false-green 不假装有字段)。 - WorkspacePage.tsx:右侧面板 Tab 化(AI 创作助手/作品规划);planning 为 work 级不依赖 activeBlock 故面板始终挂载;AI tab 保持原 activeBlock 挂载语义+空态提示。 验证:vitest PlanningEditorPanel 2/2(列 projection 默认渲染字段、unavailable server.use override 降级)、全量 66/66、tsc 0、eslint clean。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/PlanningEditorPanel.test.tsx | 36 +++++++++++ .../editor/components/PlanningEditorPanel.tsx | 63 +++++++++++++++++++ muse-studio/src/pages/WorkspacePage.tsx | 57 +++++++++++++---- 3 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 muse-studio/src/features/editor/components/PlanningEditorPanel.test.tsx create mode 100644 muse-studio/src/features/editor/components/PlanningEditorPanel.tsx diff --git a/muse-studio/src/features/editor/components/PlanningEditorPanel.test.tsx b/muse-studio/src/features/editor/components/PlanningEditorPanel.test.tsx new file mode 100644 index 00000000..5b658a9e --- /dev/null +++ b/muse-studio/src/features/editor/components/PlanningEditorPanel.test.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { http, HttpResponse } from 'msw'; +import { server } from '@/test/setup'; +import PlanningEditorPanel from './PlanningEditorPanel'; +import { resetMockDb } from '@/api/mocks/handlers/content'; +import React from 'react'; + +const renderWithClient = (ui: React.ReactElement) => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }); + return render({ui}); +}; + +describe('PlanningEditorPanel 组件', () => { + beforeEach(() => resetMockDb()); + + it('应列出 projection 并默认渲染第一个的字段', async () => { + renderWithClient(); + // projection 选择条出现「故事设定」。 + expect(await screen.findByRole('button', { name: '故事设定' })).toBeInTheDocument(); + // 默认选中第一个 → 内嵌 PlanningEditor 渲染其字段。 + expect(await screen.findByLabelText('时代背景')).toBeInTheDocument(); + }); + + it('meta-projection unavailable 时优雅降级(不崩)', async () => { + // 模拟真后端 Meta owner 未接入:meta-projections 返回 unavailable(code 非 0 → api client 抛错)。 + server.use( + http.get('/app-api/muse/works/:workId/meta-projections', () => + HttpResponse.json({ code: 500, msg: 'external owner unavailable', data: null }), + ), + ); + renderWithClient(); + expect(await screen.findByText('暂无可编辑的规划投影')).toBeInTheDocument(); + }); +}); diff --git a/muse-studio/src/features/editor/components/PlanningEditorPanel.tsx b/muse-studio/src/features/editor/components/PlanningEditorPanel.tsx new file mode 100644 index 00000000..f27116cc --- /dev/null +++ b/muse-studio/src/features/editor/components/PlanningEditorPanel.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { useMetaProjections } from '../hooks/usePlanning'; +import PlanningEditor from './PlanningEditor'; + +interface PlanningEditorPanelProps { + /** 当前作品 ID */ + workId: string; +} + +/** + * 作品规划面板:列出 work 绑定 schema 的所有 meta-projection,选中后按字段编辑。 + * + *

projection 由 Meta owner 投影(GET /works/{workId}/meta-projections)。真后端若 Meta owner + * 未接入投影实现,端口返回 unavailable → 此处优雅降级为空态提示,不崩溃(anti-false-green: + * 不假装有字段可填)。投影就绪后即可按 schema 字段填值并落字段用量快照。

+ */ +export default function PlanningEditorPanel({ workId }: PlanningEditorPanelProps) { + const { data: projections, isLoading, isError } = useMetaProjections(workId); + const [selectedKey, setSelectedKey] = useState(null); + + if (isLoading) { + return
加载规划投影…
; + } + + // meta-projection 端口在真后端可能 unavailable(Meta owner 未接入)或作品未绑 schema → 优雅降级。 + if (isError || !projections || projections.length === 0) { + return ( +
+

暂无可编辑的规划投影

+

该作品未绑定已激活的 Meta Schema,或投影服务尚未就绪。

+
+ ); + } + + // 当前选中 projection:用户未选时默认第一个(渲染期计算,无 effect setState)。 + const activeKey = selectedKey ?? projections[0]?.projectionKey ?? null; + + return ( +
+ {/* projection 选择条 */} +
+ {projections.map((p) => ( + + ))} +
+ {/* 选中 projection 的字段编辑器(key=activeKey:切换时重挂载清本地编辑) */} +
+ {activeKey && } +
+
+ ); +} diff --git a/muse-studio/src/pages/WorkspacePage.tsx b/muse-studio/src/pages/WorkspacePage.tsx index da43c0f7..ad8e163a 100644 --- a/muse-studio/src/pages/WorkspacePage.tsx +++ b/muse-studio/src/pages/WorkspacePage.tsx @@ -11,6 +11,7 @@ import MuseEditor from '../features/editor/components/MuseEditor'; import type { MuseEditorInstance } from '../features/editor/components/MuseEditor'; import AIPanel from '../features/editor/components/AIPanel'; import CandidatePanel from '../features/editor/components/CandidatePanel'; +import PlanningEditorPanel from '../features/editor/components/PlanningEditorPanel'; import type { WorkVO } from '@/types/openapi'; /** @@ -55,6 +56,8 @@ export default function WorkspacePage() { const [candidateSuggestionId, setCandidateSuggestionId] = useState(null); const [originalText, setOriginalText] = useState(''); const [showCompare, setShowCompare] = useState(false); + // 右侧面板 Tab:AI 创作助手 / 作品规划(planning 为 work 级,不依赖 activeBlock,故面板始终挂载)。 + const [rightTab, setRightTab] = useState<'ai' | 'planning'>('ai'); // 本地 revision 覆盖:采纳成功后后端返回 newRevision,先用它即时校准本地乐观锁,避免连续采纳因 revision 滞后误触 409。 const [revisionOverride, setRevisionOverride] = useState(null); @@ -235,22 +238,50 @@ export default function WorkspacePage() { )} - {/* 右侧 AI 助手 / 候选对比面板:仅在已激活 Block 时挂载(AI 生成与采纳都需要目标 Block) */} - {activeBlock && ( -
- {showCompare ? ( - + {/* 右侧面板:Tab 切换「AI 创作助手」(需激活 Block) 与「作品规划」(work 级,始终可用)。 */} +
+ {/* Tab 导航栏 */} +
+ + +
+ {/* Tab 内容区:planning 始终可用;AI 须有激活 Block(生成与采纳都需要目标 Block)。 */} +
+ {rightTab === 'planning' ? ( + + ) : activeBlock ? ( + showCompare ? ( + + ) : ( + + ) ) : ( - +
请在左侧大纲选择章节,以使用 AI 创作助手。
)}
- )} +
);