Skip to content

Commit 2e339a0

Browse files
committed
refactor(ui): 拆 SessionListPanel 第 5 步 — 列表区组件
1 parent 96dc41c commit 2e339a0

3 files changed

Lines changed: 149 additions & 60 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* SessionListBody — 会话列表区视图(批 0-C2 自 SessionListPanel 拆出)。
3+
*
4+
* @ai-context: 列表容器(flex:1 可滚动)+ 全部分支渲染:加载态 / 空态 / 检索命中
5+
* (委托 SessionSearchHits)/ 课程分组折叠 / 筛选无结果(含「清除筛选」
6+
* 入口)/ 平铺行。`renderRow` 行工厂一并下沉——行点击语义由父层的
7+
* useSessionSelection 决定(onRowOpen / onRowModifier 原样转发),
8+
* 行内编辑与多选视觉仍在 SessionListRow。
9+
* @ai-context 边界(分支顺序=优先级,不得重排):
10+
* ① 加载态与空态是**独立**的 `&&` 块,先于分支链;空态抑制条件含
11+
* `!hits`(检索中不显示"暂无会话");
12+
* ② 命中视图(hits 或 ocrHits)**接管**整个列表区——分组与平铺都不渲染;
13+
* ③ 分组分支要求 `grouped && groupedView` 同时成立(groups=null 时回落平铺);
14+
* ④ 无匹配提示要求 `itemCount > 0 && filtered.length === 0`
15+
* (items 为空时走空态,不显示"无匹配会话");
16+
* ⑤ canConvert 走 utils/sessionEligibility 纯函数(与批量栏同口径)。
17+
*/
18+
import type { CourseGroup, OcrBlockHit, SegmentHit, SessionListItem } from "../types";
19+
import { isSessionConvertible } from "../utils/sessionEligibility";
20+
import SessionListRow from "./SessionListRow";
21+
import type { SessionRenameRequest } from "./SessionListRow";
22+
import SessionSearchHits from "./SessionSearchHits";
23+
24+
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 };
25+
26+
interface Props {
27+
loading: boolean;
28+
/** items.length(空态判定——不传整个列表,避免视图层再遍历) */
29+
itemCount: number;
30+
hits: SegmentHit[] | null;
31+
ocrHits: OcrBlockHit[] | null;
32+
searchKw: string;
33+
grouped: boolean;
34+
groupedView: CourseGroup[] | null;
35+
collapsed: Record<string, boolean>;
36+
onToggleCollapse: (course: string) => void;
37+
filtered: SessionListItem[];
38+
onClearFilters: () => void;
39+
onOpenDetail: (id: number, targetSegId?: number) => void;
40+
// ── 行渲染下发项(原面板 renderRow 的入参) ──
41+
openSessionId: number | null;
42+
selected: ReadonlySet<number>;
43+
renameReq: SessionRenameRequest | null;
44+
onRenameEnd: () => void;
45+
onSessionRenamed: (id: number) => void;
46+
showToast: (msg: string, kind: "ok" | "err") => void;
47+
onRowOpen: (item: SessionListItem) => void;
48+
onRowModifier: (item: SessionListItem, ctrl: boolean, shift: boolean) => void;
49+
onRowContextMenu: (e: React.MouseEvent, item: SessionListItem) => void;
50+
onConvert: (item: SessionListItem) => void;
51+
onOpenNote: (noteId: number) => void;
52+
}
53+
54+
export default function SessionListBody({
55+
loading, itemCount, hits, ocrHits, searchKw, grouped, groupedView, collapsed, onToggleCollapse,
56+
filtered, onClearFilters, onOpenDetail, openSessionId, selected, renameReq, onRenameEnd,
57+
onSessionRenamed, showToast, onRowOpen, onRowModifier, onRowContextMenu, onConvert, onOpenNote,
58+
}: Props) {
59+
/** 行渲染(平铺/分组共用);行内编辑与多选视觉在 SessionListRow */
60+
const renderRow = (item: SessionListItem) => (
61+
<SessionListRow
62+
key={item.session.id}
63+
item={item}
64+
isOpen={openSessionId === item.session.id}
65+
multiSelected={selected.has(item.session.id)}
66+
canConvert={isSessionConvertible(item)}
67+
renameRequest={renameReq}
68+
onRenameEnd={onRenameEnd}
69+
onRenamed={(id) => onSessionRenamed(id)}
70+
showToast={showToast}
71+
onOpen={onRowOpen}
72+
onModifierClick={onRowModifier}
73+
onContextMenu={onRowContextMenu}
74+
onConvert={(it) => onConvert(it)}
75+
onOpenNote={onOpenNote}
76+
/>
77+
);
78+
79+
return (
80+
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
81+
{loading && itemCount === 0 && (
82+
<p style={{ fontSize: 12, color: "#9ca3af", padding: 16, textAlign: "center" }}>加载中…</p>
83+
)}
84+
{!loading && itemCount === 0 && !hits && (
85+
<p style={{ fontSize: 12, color: "#9ca3af", padding: 16, textAlign: "center" }}>
86+
暂无会话,去「课堂助手」开始实时捕获
87+
</p>
88+
)}
89+
{hits || ocrHits ? (
90+
<SessionSearchHits hits={hits} ocrHits={ocrHits} searchKw={searchKw} onOpenDetail={onOpenDetail} />
91+
) : grouped && groupedView ? (
92+
/* 课程分组(折叠 + 组内筛选排序) */
93+
groupedView.map((g) => (
94+
<div key={g.course}>
95+
<div
96+
onClick={() => onToggleCollapse(g.course)}
97+
style={{ padding: "8px 14px", fontSize: 12, fontWeight: 600, color: "#0f766e", cursor: "pointer", background: "#f8fafc", borderBottom: "1px solid #e5e7eb" }}
98+
>
99+
{collapsed[g.course] ? "▸" : "▾"} {g.course}({g.sessions.length})
100+
</div>
101+
{!collapsed[g.course] && g.sessions.map(renderRow)}
102+
</div>
103+
))
104+
) : itemCount > 0 && filtered.length === 0 ? (
105+
/* 筛选无结果:给出清除入口 */
106+
<p style={{ fontSize: 12, color: "#9ca3af", padding: 16, textAlign: "center" }}>
107+
无匹配会话{" "}
108+
<button style={{ ...btn, fontSize: 11 }} onClick={onClearFilters}>
109+
清除筛选
110+
</button>
111+
</p>
112+
) : (
113+
filtered.map(renderRow)
114+
)}
115+
</div>
116+
);
117+
}

‎app/src/components/SessionListPanel.tsx‎

Lines changed: 30 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,10 @@ import {
2626
} from "../hooks/useSessionListView";
2727
import { useSessionSearch } from "../hooks/useSessionSearch";
2828
import { useSessionSelection } from "../hooks/useSessionSelection";
29-
import SessionListRow from "./SessionListRow";
3029
import type { SessionRenameRequest } from "./SessionListRow";
3130
import SessionRowContextMenu from "./SessionRowContextMenu";
31+
import SessionListBody from "./SessionListBody";
3232
import SessionSearchBar from "./SessionSearchBar";
33-
import SessionSearchHits from "./SessionSearchHits";
3433

3534
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 };
3635
const selectStyle: React.CSSProperties = {
@@ -124,6 +123,9 @@ export default function SessionListPanel({
124123
setKeyword("");
125124
};
126125

126+
/** 分组折叠切换(列表区组头点击——原内联 setCollapsed 收敛于此,语义不变) */
127+
const toggleCollapse = (course: string) => setCollapsed((c) => ({ ...c, [course]: !c[course] }));
128+
127129
/** 批量转:先过滤出可转化集合(已转/进行中/无内容不在其中);pending 防连点
128130
*(P3-2——onBatchConvert 允许返回 Promise:父层 invoke 完成前按钮保持禁用) */
129131
const runBatchConvert = async () => {
@@ -150,26 +152,6 @@ export default function SessionListPanel({
150152
}
151153
};
152154

153-
/** 行渲染(平铺/分组共用);行内编辑与多选视觉在 SessionListRow */
154-
const renderRow = (item: SessionListItem) => (
155-
<SessionListRow
156-
key={item.session.id}
157-
item={item}
158-
isOpen={openSessionId === item.session.id}
159-
multiSelected={selected.has(item.session.id)}
160-
canConvert={isSessionConvertible(item)}
161-
renameRequest={renameReq}
162-
onRenameEnd={() => setRenameReq(null)}
163-
onRenamed={(id) => onSessionRenamed(id)}
164-
showToast={showToast}
165-
onOpen={rowOpen}
166-
onModifierClick={rowModifier}
167-
onContextMenu={(e, item) => setContextMenu({ item, x: e.clientX, y: e.clientY })}
168-
onConvert={(it) => onConvert(it)}
169-
onOpenNote={onOpenNote}
170-
/>
171-
);
172-
173155
const selectModeBtn = (on: boolean): React.CSSProperties => ({
174156
...btn,
175157
fontSize: 11,
@@ -260,43 +242,32 @@ export default function SessionListPanel({
260242
</div>
261243
)}
262244

263-
{/* 列表区 */}
264-
<div style={{ flex: 1, minHeight: 0, overflowY: "auto" }}>
265-
{loading && items.length === 0 && (
266-
<p style={{ fontSize: 12, color: "#9ca3af", padding: 16, textAlign: "center" }}>加载中…</p>
267-
)}
268-
{!loading && items.length === 0 && !hits && (
269-
<p style={{ fontSize: 12, color: "#9ca3af", padding: 16, textAlign: "center" }}>
270-
暂无会话,去「课堂助手」开始实时捕获
271-
</p>
272-
)}
273-
{hits || ocrHits ? (
274-
<SessionSearchHits hits={hits} ocrHits={ocrHits} searchKw={searchKw} onOpenDetail={onOpenDetail} />
275-
) : grouped && groupedView ? (
276-
/* 课程分组(折叠 + 组内筛选排序) */
277-
groupedView.map((g) => (
278-
<div key={g.course}>
279-
<div
280-
onClick={() => setCollapsed((c) => ({ ...c, [g.course]: !c[g.course] }))}
281-
style={{ padding: "8px 14px", fontSize: 12, fontWeight: 600, color: "#0f766e", cursor: "pointer", background: "#f8fafc", borderBottom: "1px solid #e5e7eb" }}
282-
>
283-
{collapsed[g.course] ? "▸" : "▾"} {g.course}({g.sessions.length})
284-
</div>
285-
{!collapsed[g.course] && g.sessions.map(renderRow)}
286-
</div>
287-
))
288-
) : items.length > 0 && filtered.length === 0 ? (
289-
/* 筛选无结果:给出清除入口 */
290-
<p style={{ fontSize: 12, color: "#9ca3af", padding: 16, textAlign: "center" }}>
291-
无匹配会话{" "}
292-
<button style={{ ...btn, fontSize: 11 }} onClick={clearFilters}>
293-
清除筛选
294-
</button>
295-
</p>
296-
) : (
297-
filtered.map(renderRow)
298-
)}
299-
</div>
245+
{/* 列表区(批 0-C2 拆至 components/SessionListBody) */}
246+
<SessionListBody
247+
loading={loading}
248+
itemCount={items.length}
249+
hits={hits}
250+
ocrHits={ocrHits}
251+
searchKw={searchKw}
252+
grouped={grouped}
253+
groupedView={groupedView}
254+
collapsed={collapsed}
255+
onToggleCollapse={toggleCollapse}
256+
filtered={filtered}
257+
onClearFilters={clearFilters}
258+
onOpenDetail={onOpenDetail}
259+
openSessionId={openSessionId}
260+
selected={selected}
261+
renameReq={renameReq}
262+
onRenameEnd={() => setRenameReq(null)}
263+
onSessionRenamed={onSessionRenamed}
264+
showToast={showToast}
265+
onRowOpen={rowOpen}
266+
onRowModifier={rowModifier}
267+
onRowContextMenu={(e, item) => setContextMenu({ item, x: e.clientX, y: e.clientY })}
268+
onConvert={onConvert}
269+
onOpenNote={onOpenNote}
270+
/>
300271

301272
{/* 批量操作栏(出现后出现;段搜索命中视图隐藏——避免对不可见列表误操作) */}
302273
{!hits && !ocrHits && selected.size > 0 && (

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@
9696
| app/src-tauri/src/commands_fragments.rs | 374 | 碎片命令域 + REQ-316(批 7)结果契约与组域条件广播(登记值缺失为漏登——HEAD 基线已 347,本次补) | 若再增长:碎片组操作命令族拆至 commands_fragments_group.rs |
9797
| app/src-tauri/src/analysis_tests.rs | 371 | 分析编排测试域(档案门控矩阵 + REQ-108 事件消费 + M2 三字段)单模块 #[path] 挂载 | 若再增长:事件消费组拆至 analysis_events_tests.rs |
9898
| app/src-tauri/src/commands_web_inbox.rs | 371 | v0.20.4(REQ-304)扩展收件命令域(起停/状态/HTTP 小循环/投递收口/图落盘)内聚;2026-09-06 实测登记(TD-2026-09-06-G) | 若再增长:HTTP 连接处理拆至 web_inbox_http.rs |
99-
| app/src/components/SessionListPanel.tsx | 371 | 超硬限(>600 行),不允许豁免 —— v0.20.9 批 4(REQ-313)列表交互重写(选择模式/行右键/行内改名/批量栏)+ 审查修复轮 3(全选可见行基准/pending)净增——2026-09-09 实测纠偏(登记值 491 过期);**超 600 硬限随 TD-2026-09-09-D 登记** | **批 0-C2 Task 5 八文件边界**(原计划「批量栏 + 选择模式」只减 166 行 ⇒ 仍 ≈468 >300,不足):`utils/sessionEligibility.ts`(可转化纯函数)· `hooks/useSessionSelection.ts`(选择态机 + visibleOrderRef + 裁剪 effect;**Esc 监听留面板**)· `hooks/useSessionListView.ts`(matchFilters/sorted/filtered/groupedView/visibleOrder)· `hooks/useSessionSearch.ts`(三模式搜索 + 两个 invoke 落点)· `components/SessionSearchBar.tsx`(三模式单输入框)· `components/SessionSearchHits.tsx`(段/画面命中视图)· `components/SessionListBody.tsx`(列表容器含 renderRow)· `components/SessionSelectionToolbar.tsx`(头部选择控件 + 底部批量栏);列表行已拆 SessionListRow.tsx |
10099
| app/src-tauri/src/live_keyframes.rs | 370 | ① handle_full_frame:全帧画面要点落库 + 关键帧样本收集与(自动摘取,待细化) | 若再增长:按职责拆分 |
101100
| app/src-tauri/src/windows.rs | 370 | 窗口枚举/评分域 + v0.19.2(REQ-271/272):CaptureWindow.zOrder/systemWindow 字段 + 抖音/快手/B站客户端评分表(+28)——枚举系统副作用与评分纯函数同域便于单测(既有模式) | 若再增长:z 序/系统标记纯函数拆至 windows_meta.rs |
102101
| app/src-tauri/src/commands_window.rs | 369 | 浮窗窗口命令域(v0.12.3 架构升级计划:全部窗口操作集中单文件)+ v0.12.6(ADR-025)显隐链路与全局快捷键三态切换核心(open/close/locked/topmost/toggle 核心 fn + 命令薄包装 + 状态机/序列化单测内联);拆分需跨 fn 传递 AppHandle/State,内聚性优先 | 若再增长:浮窗核心逻辑拆至 float_core.rs(命令薄包装保留本文件) |
@@ -111,6 +110,7 @@
111110
| app/src/components/NoteLinkToSystem.tsx | 351 | v0.19.7(REQ-286)重构:挂体系选择器(体系下拉 + 三 tab + LinkEntityPicker 搜索树列表 + 三类内联轻建编排 + 既有反查/撤链/钳制语义保持)——实体选择/创建交互已下沉 LinkEntityPicker.tsx(134 行),本文件保留编排与数据装载内聚 | 若再增长:树行构建(flattenNodeRows)与实体装载拆至 useSystemEntities.ts hook |
112111
| app/src-tauri/src/commands_ai_settings.rs | 349 | AI 设置命令域(视图/密钥/授权/目标 AI) + v0.19.1 ai_set_kb_qa 最小面命令(+28)——read-modify-write 同域先例(ai_set_goal_plan)内聚 | 若再增长:kb/goal 最小面命令拆至 commands_ai_settings_extra.rs |
113112
| app/src-tauri/src/structure_models.rs | 343 | v0.5.0 模型版:模型清单/独立状态机下载器(进度事件/.part 原子写/按需启用三分类)+ 磁盘就绪判定(disk_done)内聚 | 若再增长:download_one 拆至 structure_download.rs |
113+
| app/src/components/SessionListPanel.tsx | 342 | 超硬限(>600 行),不允许豁免 —— v0.20.9 批 4(REQ-313)列表交互重写(选择模式/行右键/行内改名/批量栏)+ 审查修复轮 3(全选可见行基准/pending)净增——2026-09-09 实测纠偏(登记值 491 过期);**超 600 硬限随 TD-2026-09-09-D 登记** | **批 0-C2 Task 5 八文件边界**(原计划「批量栏 + 选择模式」只减 166 行 ⇒ 仍 ≈468 >300,不足):`utils/sessionEligibility.ts`(可转化纯函数)· `hooks/useSessionSelection.ts`(选择态机 + visibleOrderRef + 裁剪 effect;**Esc 监听留面板**)· `hooks/useSessionListView.ts`(matchFilters/sorted/filtered/groupedView/visibleOrder)· `hooks/useSessionSearch.ts`(三模式搜索 + 两个 invoke 落点)· `components/SessionSearchBar.tsx`(三模式单输入框)· `components/SessionSearchHits.tsx`(段/画面命中视图)· `components/SessionListBody.tsx`(列表容器含 renderRow)· `components/SessionSelectionToolbar.tsx`(头部选择控件 + 底部批量栏);列表行已拆 SessionListRow.tsx |
114114
| app/src-tauri/src/db_ai_tasks.rs | 340 | 任务注册表在 AppState 内存(HashMap)——重启即失、未采纳(自动摘取,待细化) | 若再增长:按职责拆分 |
115115
| app/src-tauri/src/screen_merge_tests.rs | 339 | 屏级聚合/净化纯函数测试域(聚类/行合并/零跨度/图去重回归)单模块 #[path] 挂载 | 若再增长:净化组拆至 screen_merge_purify_tests.rs |
116116
| app/src-tauri/src/db_notes_tests.rs | 337 | db_notes.rs 单测域(15 例:笔记 CRUD/updated 倒序/搜索通配符转义/会话关联与旧库 ensure_column 迁移;全部走内存库,环境隔离铁律)——测试模块由 `#[cfg(test)] #[path]` 单点挂载(db_notes.rs:365),H3 硬拆时由原 db.rs 的 tests 模块整体迁入(语义不变)。**本条目由生成器补登(该文件无 @ai-context 头注释),理由为 2026-09-11 重建时人工补写** | 若再增长:会话关联与迁移用例拆至 db_notes_link_tests.rs |
@@ -151,6 +151,7 @@
151151
| app/src-tauri/src/commands_knowledge_systems.rs | 306 | 本层只做参数校验、调用数据层、错误映射(AGENTS.md §6);编排逻辑(自动摘取,待细化) | 若再增长:按职责拆分 |
152152
| app/src-tauri/src/db_session_refine.rs | 306 | 原料 session_segments 不可变(ADR-030 决策 5 可逆契约延续)——(自动摘取,待细化) | 若再增长:按职责拆分 |
153153
| app/src-tauri/src/note_diff.rs | 304 | 纯函数:按行(块)比较 before/after,三态标记 unchanged/(自动摘取,待细化) | 若再增长:按职责拆分 |
154+
| app/src/components/ClassroomCapturePanel.tsx | 303 | 纯展示适配器 —— 采集生命周期(active/starting/pending/sessionId/(自动摘取,待细化) | 若再增长:按职责拆分 |
154155
| app/src/components/SecondPassPanel.tsx | 303 | 会话结束后把 S4 落盘音频全窗重跑 SenseVoice(后端 second_pass_* 命令),(自动摘取,待细化) | 若再增长:按职责拆分 |
155156
| app/src-tauri/src/region_ocr.rs | 302 | 原始帧 → LayoutAnalyzer → 区域列表 → 逐区域裁剪(内存 crop + 边距)(自动摘取,待细化) | 若再增长:按职责拆分 |
156157
| app/src-tauri/src/series_detect.rs | 301 | 从窗口标题/文件名剥离"系列名 + 集号"(B站分P `P3`/`第3集`/`EP03`/(自动摘取,待细化) | 若再增长:按职责拆分 |

0 commit comments

Comments
 (0)