ui:工作台界面优化及张力心电图bug修复#83
Conversation
This reverts commit 1e413c5.
- 工作台左右面板支持拖拽调整宽度与折叠/展开 - 中间内容区滚动条默认隐藏,悬停时显示 - 故事结构树窄屏时隐藏节点后缀 - 统一各面板标题行布局(标题+标签+按钮同行) - 监控网格张力图表与实时日志高度一致 - 张力心电图刷新后重新初始化,新增 ResizeObserver 自适应 - 关系图工具栏按钮防换行布局 - 修复 Naive UI 按钮属性冲突(dashed/secondary)
📝 WalkthroughWalkthroughMultiple frontend components received layout reorganization and responsive behavior enhancements. Changes include header restructuring across several panels, addition of resize observers for responsive width-based rendering, refactoring of the main Workbench layout from Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
frontend/src/components/panels/BiblePanel.vue (1)
537-547:bible-roles的单列切换阈值建议上调。
max-width: 320px对可拖拽侧栏偏低,320~500px 区间仍双列会明显拥挤。建议回调到更实用阈值(如 480/520)。💡 Suggested tweak
-@media (max-width: 320px) { +@media (max-width: 520px) { .bible-roles { grid-template-columns: 1fr; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/panels/BiblePanel.vue` around lines 537 - 547, The responsive breakpoint for the .bible-roles grid is too small (max-width: 320px) causing two columns to be cramped on narrow screens; update the media query controlling .bible-roles to use a larger threshold (e.g., max-width: 480px or 520px) so it switches to a single column earlier. Locate the .bible-roles CSS and replace the existing `@media` (max-width: 320px) rule with the chosen larger max-width value to force grid-template-columns: 1fr at that breakpoint.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/autopilot/RealtimeLogStream.vue`:
- Around line 522-524: The CSS rule in RealtimeLogStream.vue sets a hard
min-width on the .realtime-log-stream class which causes overflow on narrow
layouts; remove the min-width: 368px (or replace it with a responsive rule such
as max-width, width: 100%, or a media-query that only applies a min-width at
larger breakpoints) so the RealtimeLogStream component can shrink with the
center pane (update the .realtime-log-stream declaration in
RealtimeLogStream.vue accordingly).
In `@frontend/src/components/autopilot/TensionChart.vue`:
- Around line 154-163: The chart instance reuse bug occurs because we only check
chartInstance.isDisposed() before reusing it; update the initialization logic
(around chartInstance, chartRef, resizeObserver) to also detect a stale DOM by
comparing chartInstance.getDom() !== chartRef.value and force a
re-initialization (dispose and init) when they differ, and in refreshChart()
ensure you call resizeObserver.disconnect() (and set resizeObserver = null)
before clearing/letting the DOM unmount so the observer is not left attached to
a destroyed element; finally, when creating a new chart instance
(init(chartRef.value)), recreate and reattach a new ResizeObserver that observes
chartRef.value.
In `@frontend/src/components/knowledge/KnowledgePanel.vue`:
- Around line 4-6: The header action buttons in KnowledgePanel.vue can overflow
when the split-pane is very narrow because .kp-header-row is a single-line flex
container and the action area (the n-space shown when sideTab === 'narrative')
doesn't wrap or shrink; update the header layout to allow wrapping and
shrinking: make the .kp-header-row flex container enable flex-wrap and ensure
the title (kp-title) and the action container (the n-space for sideTab
'narrative') have appropriate flex properties (e.g., title flex: 0 1 auto and
action area flex: 0 1 auto or 1 1 auto) or set the action area to wrap its items
so buttons move to a new line at narrow widths; apply the same fix to the other
header instance noted (lines 829-836).
In `@frontend/src/components/panels/BiblePanel.vue`:
- Around line 5-13: The .bible-title-row layout can cause the action buttons to
overflow when the panel is narrow; update the CSS for .bible-title-row and
.bible-hero-actions (used around the n-button elements and props generating/save
methods) to allow wrapping: make .bible-title-row display:flex with
align-items:center and flex-wrap:wrap, give the title (.bible-title) flex:1 1
auto and .bible-hero-actions flex:0 0 auto with a max-width or min-width clamp
on its child buttons to allow them to wrap to the next line when space is
constrained; ensure the Vue methods generateBible and save and reactive flags
generating/saving remain unchanged.
In `@frontend/src/components/workbench/ForeshadowLedgerPanel.vue`:
- Around line 8-14: The title row currently uses a fixed single-line layout
which causes action buttons (n-button) to overflow when the workbench is dragged
narrower; update the CSS for the .title-row/.panel-title/.header-actions block
to support a narrow-width fallback by enabling flex-wrap on .title-row and
allowing .panel-title to shrink/grow (e.g., flex: 1 1 auto) while making
.header-actions wrap to a new line at small widths (e.g., flex-basis: 100% or
width: 100% with margin-top) so the + 添加伏笔 button (openCreateModal) and 刷新
button (load, bound to loading) move under the title instead of overflowing;
apply the same style changes to the other similar blocks referenced (around the
other title rows).
In `@frontend/src/views/Workbench.vue`:
- Around line 128-137: Initial left/right pane widths can exceed the viewport,
leaving the center pane unusable; on mount and on window resize clamp them so
they never reserve more space than available. Implement an onMounted() and
resize listener that reads window.innerWidth, computes the max allowed side
width (e.g. availableWidth = window.innerWidth; maxSides = Math.max(0,
availableWidth - MIN_CENTER_WIDTH)), then if leftWidth.value + rightWidth.value
> maxSides reduce them (either clamp each with LEFT_MIN/LEFT_MAX and
RIGHT_MIN/RIGHT_MAX or scale them proportionally) and write back to
leftWidth.value and rightWidth.value; use the existing constants LEFT_MIN,
LEFT_MAX, RIGHT_MIN, RIGHT_MAX and refs leftWidth/rightWidth, and update/cleanup
the resize listener on unmount.
---
Nitpick comments:
In `@frontend/src/components/panels/BiblePanel.vue`:
- Around line 537-547: The responsive breakpoint for the .bible-roles grid is
too small (max-width: 320px) causing two columns to be cramped on narrow
screens; update the media query controlling .bible-roles to use a larger
threshold (e.g., max-width: 480px or 520px) so it switches to a single column
earlier. Locate the .bible-roles CSS and replace the existing `@media` (max-width:
320px) rule with the chosen larger max-width value to force
grid-template-columns: 1fr at that breakpoint.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e274b4ee-66e8-4f45-8f13-1c0edf9bd617
📒 Files selected for processing (13)
frontend/src/components/StoryStructureTree.vuefrontend/src/components/autopilot/AutopilotDashboard.vuefrontend/src/components/autopilot/AutopilotPanel.vuefrontend/src/components/autopilot/RealtimeLogStream.vuefrontend/src/components/autopilot/TensionChart.vuefrontend/src/components/graphs/CastGraphCompact.vuefrontend/src/components/graphs/LocationGraphCompact.vuefrontend/src/components/knowledge/KnowledgePanel.vuefrontend/src/components/panels/BiblePanel.vuefrontend/src/components/workbench/ForeshadowLedgerPanel.vuefrontend/src/components/workbench/HolographicChroniclesPanel.vuefrontend/src/components/workbench/WorkArea.vuefrontend/src/views/Workbench.vue
| .realtime-log-stream { | ||
| height: 100%; | ||
| min-width: 368px; |
There was a problem hiding this comment.
Drop the hard min-width here.
Below 900px, frontend/src/components/autopilot/AutopilotDashboard.vue switches to a single-column grid, and frontend/src/views/Workbench.vue now reserves ~900px for the two side panes by default. This min-width: 368px forces the log card to overflow/clamp as soon as the center pane is narrower than 368px, which is easy to hit on smaller windows.
Suggested fix
.realtime-log-stream {
height: 100%;
- min-width: 368px;
+ min-width: 0;
+ width: 100%;
position: relative;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .realtime-log-stream { | |
| height: 100%; | |
| min-width: 368px; | |
| .realtime-log-stream { | |
| height: 100%; | |
| min-width: 0; | |
| width: 100%; | |
| position: relative; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/autopilot/RealtimeLogStream.vue` around lines 522 -
524, The CSS rule in RealtimeLogStream.vue sets a hard min-width on the
.realtime-log-stream class which causes overflow on narrow layouts; remove the
min-width: 368px (or replace it with a responsive rule such as max-width, width:
100%, or a media-query that only applies a min-width at larger breakpoints) so
the RealtimeLogStream component can shrink with the center pane (update the
.realtime-log-stream declaration in RealtimeLogStream.vue accordingly).
| // 如果实例已被销毁或不存在,重新初始化 | ||
| if (!chartInstance || chartInstance.isDisposed()) { | ||
| chartInstance = init(chartRef.value) | ||
| // 在 chartRef 确保存在后才初始化 ResizeObserver(解决 onMounted 时 chartRef 为 null 的问题) | ||
| if (!resizeObserver && chartRef.value) { | ||
| resizeObserver = new ResizeObserver(() => { | ||
| chartInstance?.resize() | ||
| }) | ||
| resizeObserver.observe(chartRef.value) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f TensionChart.vueRepository: shenminglinyi/PlotPilot
Length of output: 117
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | head -100Repository: shenminglinyi/PlotPilot
Length of output: 3923
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '150,170p'Repository: shenminglinyi/PlotPilot
Length of output: 853
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '305,325p'Repository: shenminglinyi/PlotPilot
Length of output: 656
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '1,40p'Repository: shenminglinyi/PlotPilot
Length of output: 1844
🏁 Script executed:
rg -t ts "getDom|dispose|isDisposed" frontend/src/components/autopilot/TensionChart.vueRepository: shenminglinyi/PlotPilot
Length of output: 215
🌐 Web query:
ECharts TypeScript getDom method API
💡 Result:
The ECharts instance method getDom returns the DOM element (HTMLElement) that serves as the container for the ECharts chart. It is available on the EChartsType interface, which extends the ECharts class and is the return type of echarts.init. Signature (from ECharts 4+ documentation, consistent across versions including latest ECharts 6): getDom: HTMLCanvasElement | HTMLDivElement Usage example in TypeScript: typescript import * as echarts from 'echarts'; const chart = echarts.init(document.getElementById('main') as HTMLElement); const domContainer = chart.getDom; // Returns the original DOM element passed to init console.log(domContainer); // HTMLElement (typically div or canvas) This method retrieves the root DOM container where the chart was initialized, useful for accessing the container element after initialization (e.g., for resizing or DOM manipulation). It is listed under Dimensions methods alongside getWidth and getHeight in instance API documentation. Note: Official API docs at echarts.apache.org/en/api.html redirect to the main site; detailed method docs are preserved in source repositories and third-party mirrors like Mintlify. Source code confirms getDom is public on EChartsType.
Citations:
- 1: https://mintlify.com/apache/echarts/api/echarts-instance
- 2: https://apache.googlesource.com/echarts-doc/+/refs/heads/v4/en/api/echarts-instance.md
- 3: https://www.mintlify.com/apache/echarts/api/echarts-instance
- 4: https://github.com/apache/echarts/blob/master/src/core/ExtensionAPI.ts
🏁 Script executed:
rg -t ts "from 'echarts" frontend/src/components/autopilot/TensionChart.vue | head -10Repository: shenminglinyi/PlotPilot
Length of output: 347
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '100,145p'Repository: shenminglinyi/PlotPilot
Length of output: 1702
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '240,280p'Repository: shenminglinyi/PlotPilot
Length of output: 1884
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '322,350p'Repository: shenminglinyi/PlotPilot
Length of output: 940
🏁 Script executed:
cat -n frontend/src/components/autopilot/TensionChart.vue | sed -n '112,145p'Repository: shenminglinyi/PlotPilot
Length of output: 1269
Detect when the ECharts instance is bound to a stale DOM node, and disconnect the ResizeObserver during refresh.
The template conditionally renders the chart container (line 27). When refreshChart() clears tensionData (line 312), the template switches to the empty state (line 17), unmounting the old DOM element. When loadTensionData() completes with new data, line 27 renders a new DOM element. However, the check at line 155 only tests isDisposed(), so the old instance—still alive and bound to the destroyed element—is reused instead of re-initialized on the new DOM.
Additionally, resizeObserver is never disconnected in refreshChart() (lines 310–320), leaving it attached to the destroyed element.
Suggested fix
let chartInstance: ECharts | null = null
let resizeObserver: ResizeObserver | null = null
+function bindResizeObserver(el: HTMLElement) {
+ resizeObserver?.disconnect()
+ resizeObserver = new ResizeObserver(() => {
+ chartInstance?.resize()
+ })
+ resizeObserver.observe(el)
+}
+
function renderChart() {
if (!chartRef.value || tensionData.value.length === 0) return
const rect = chartRef.value.getBoundingClientRect()
if (rect.width < 10 || rect.height < 10) {
setTimeout(() => renderChart(), 200)
return
}
- if (!chartInstance || chartInstance.isDisposed()) {
- chartInstance = init(chartRef.value)
- if (!resizeObserver && chartRef.value) {
- resizeObserver = new ResizeObserver(() => {
- chartInstance?.resize()
- })
- resizeObserver.observe(chartRef.value)
- }
+ if (
+ !chartInstance ||
+ chartInstance.isDisposed() ||
+ chartInstance.getDom() !== chartRef.value
+ ) {
+ chartInstance?.dispose()
+ chartInstance = init(chartRef.value)
+ bindResizeObserver(chartRef.value)
}
// ...
}
async function refreshChart() {
tensionData.value = []
+ resizeObserver?.disconnect()
+ resizeObserver = null
if (chartInstance) {
chartInstance.dispose()
+ chartInstance = null
}
await nextTick()
await loadTensionData()
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/autopilot/TensionChart.vue` around lines 154 - 163,
The chart instance reuse bug occurs because we only check
chartInstance.isDisposed() before reusing it; update the initialization logic
(around chartInstance, chartRef, resizeObserver) to also detect a stale DOM by
comparing chartInstance.getDom() !== chartRef.value and force a
re-initialization (dispose and init) when they differ, and in refreshChart()
ensure you call resizeObserver.disconnect() (and set resizeObserver = null)
before clearing/letting the DOM unmount so the observer is not left attached to
a destroyed element; finally, when creating a new chart instance
(init(chartRef.value)), recreate and reattach a new ResizeObserver that observes
chartRef.value.
| <div class="kp-header-row"> | ||
| <h3 class="kp-title">侧栏资料</h3> | ||
| <n-space v-show="sideTab === 'narrative'" :size="8" align="center" style="flex-shrink:0"> |
There was a problem hiding this comment.
Header actions may be clipped in narrow split-pane widths.
当前标题行固定单行且操作区不收缩,面板被拖窄时按钮可能溢出并影响点击。建议给窄宽度加换行兜底。
💡 Suggested fix
.kp-header-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 10px 12px 6px;
flex-shrink: 0;
}
+
+@media (max-width: 560px) {
+ .kp-header-row {
+ flex-wrap: wrap;
+ align-items: flex-start;
+ }
+}Also applies to: 829-836
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/knowledge/KnowledgePanel.vue` around lines 4 - 6, The
header action buttons in KnowledgePanel.vue can overflow when the split-pane is
very narrow because .kp-header-row is a single-line flex container and the
action area (the n-space shown when sideTab === 'narrative') doesn't wrap or
shrink; update the header layout to allow wrapping and shrinking: make the
.kp-header-row flex container enable flex-wrap and ensure the title (kp-title)
and the action container (the n-space for sideTab 'narrative') have appropriate
flex properties (e.g., title flex: 0 1 auto and action area flex: 0 1 auto or 1
1 auto) or set the action area to wrap its items so buttons move to a new line
at narrow widths; apply the same fix to the other header instance noted (lines
829-836).
| <div class="bible-title-row"> | ||
| <h3 class="bible-title">作品设定</h3> | ||
| <n-tag size="small" round :bordered="false" class="bible-badge">Story Bible</n-tag> | ||
| <div class="bible-hero-actions"> | ||
| <n-button size="small" secondary :loading="generating" @click="generateBible" title="用 AI 根据小说标题重新生成设定"> | ||
| ✦ AI 生成 | ||
| </n-button> | ||
| <n-button size="small" type="primary" :loading="saving" @click="save">保存设定</n-button> | ||
| </div> |
There was a problem hiding this comment.
标题行在窄面板下可能挤压操作按钮。
当前一行布局 + 操作区不收缩,在分栏拖窄时可能出现按钮溢出。建议加窄宽度换行兜底。
💡 Suggested fix
.bible-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
@@
.bible-hero-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
+
+@media (max-width: 560px) {
+ .bible-title-row {
+ flex-wrap: wrap;
+ }
+}Also applies to: 493-506
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/panels/BiblePanel.vue` around lines 5 - 13, The
.bible-title-row layout can cause the action buttons to overflow when the panel
is narrow; update the CSS for .bible-title-row and .bible-hero-actions (used
around the n-button elements and props generating/save methods) to allow
wrapping: make .bible-title-row display:flex with align-items:center and
flex-wrap:wrap, give the title (.bible-title) flex:1 1 auto and
.bible-hero-actions flex:0 0 auto with a max-width or min-width clamp on its
child buttons to allow them to wrap to the next line when space is constrained;
ensure the Vue methods generateBible and save and reactive flags
generating/saving remain unchanged.
| <div class="title-row"> | ||
| <h3 class="panel-title">伏笔账本</h3> | ||
| <n-tag size="small" round :bordered="false">Foreshadow Ledger</n-tag> | ||
| <div class="header-actions"> | ||
| <n-button size="small" secondary @click="openCreateModal">+ 添加伏笔</n-button> | ||
| <n-button size="small" type="primary" :loading="loading" @click="load">刷新</n-button> | ||
| </div> |
There was a problem hiding this comment.
Title row needs a narrow-width fallback.
在可拖拽缩窄的工作台中,这里固定单行 + 右侧操作区不收缩,容易出现按钮溢出。建议补一个小屏换行规则。
💡 Suggested fix
.title-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
@@
.header-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
+
+@media (max-width: 560px) {
+ .title-row {
+ flex-wrap: wrap;
+ }
+}Also applies to: 400-405, 424-430
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/workbench/ForeshadowLedgerPanel.vue` around lines 8 -
14, The title row currently uses a fixed single-line layout which causes action
buttons (n-button) to overflow when the workbench is dragged narrower; update
the CSS for the .title-row/.panel-title/.header-actions block to support a
narrow-width fallback by enabling flex-wrap on .title-row and allowing
.panel-title to shrink/grow (e.g., flex: 1 1 auto) while making .header-actions
wrap to a new line at small widths (e.g., flex-basis: 100% or width: 100% with
margin-top) so the + 添加伏笔 button (openCreateModal) and 刷新 button (load, bound to
loading) move under the title instead of overflowing; apply the same style
changes to the other similar blocks referenced (around the other title rows).
| // ━━━ 侧栏宽度 & 折叠 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| const LEFT_MIN = 238 | ||
| const LEFT_MAX = 600 | ||
| const RIGHT_MIN = 260 | ||
| const RIGHT_MAX = 520 | ||
|
|
||
| const leftWidth = ref(388) | ||
| const rightWidth = ref(512) | ||
| const leftCollapsed = ref(false) | ||
| const rightCollapsed = ref(false) |
There was a problem hiding this comment.
Clamp the initial pane widths to the available viewport.
These defaults reserve ~900px for the side panes before the center pane gets any room. Since .main-pane is allowed to shrink to 0, the editor can render nearly unusable on smaller windows until the user manually collapses or drags a pane.
A responsive fallback on mount/resize would avoid that first-render regression.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/views/Workbench.vue` around lines 128 - 137, Initial left/right
pane widths can exceed the viewport, leaving the center pane unusable; on mount
and on window resize clamp them so they never reserve more space than available.
Implement an onMounted() and resize listener that reads window.innerWidth,
computes the max allowed side width (e.g. availableWidth = window.innerWidth;
maxSides = Math.max(0, availableWidth - MIN_CENTER_WIDTH)), then if
leftWidth.value + rightWidth.value > maxSides reduce them (either clamp each
with LEFT_MIN/LEFT_MAX and RIGHT_MIN/RIGHT_MAX or scale them proportionally) and
write back to leftWidth.value and rightWidth.value; use the existing constants
LEFT_MIN, LEFT_MAX, RIGHT_MIN, RIGHT_MAX and refs leftWidth/rightWidth, and
update/cleanup the resize listener on unmount.
|
你好, 非常感谢 @yanxiaofei395118 这次对工作台 UI 的用心打磨:可拖拽侧栏、布局统一、张力图与监控区高度对齐等,都是实实在在提升体验的细节。合并时与上游 PR 在张力图上有少量冲突,已在本地合并处理并推上 若你本地还有旧分支,拉一下最新 再次感谢你的时间与贡献,祝顺利 🙏 |
变更类型
feat新功能fixBug 修复refactor重构(不影响功能)变更说明
工作台 UI 布局与交互优化:
面板标题区域布局统一:
监控网格高度一致性:
图表修复:
工具栏按钮防换行:
Bug 修复:
架构影响
frontend测试
风险说明
Git 提交信息
Summary by CodeRabbit
Release Notes
New Features
Improvements