Skip to content

Commit e579df6

Browse files
committed
refactor(notes): move derivations to colorPalette
NotesPage 300/300 (C9 余量 0) 拆件:两个 useMemo 的派生逻辑逐字搬入 utils/colorPalette(已是 resolveNoteColor 四档解析的真源),页面侧改为调用 => 300 -> 285;补 3 条纯函数判据(groupFilter / filterSealed / 四档色板优先级),既有用例零改动。
1 parent ad9d80d commit e579df6

3 files changed

Lines changed: 98 additions & 19 deletions

File tree

‎app/src/pages/NotesPage.tsx‎

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1717
import { invoke } from "@tauri-apps/api/core";
1818
import type { Note } from "../types";
19-
import { resolveNoteColor } from "../utils/colorPalette";
19+
import { buildNoteColorMap, visibleNotesOf } from "../utils/colorPalette";
2020
// 批 8(REQ-317):模型卡对话框槽(拆件)与选区行动类编排(hook 收敛)
2121
import { useNoteSelectionActions } from "../hooks/useNoteSelectionActions";
2222
import NotesGroupsColumn from "../components/notes/NotesGroupsColumn";
@@ -173,24 +173,9 @@ export default function NotesPage({ focusNoteId, focusNoteSearch, focusGroupId,
173173
setEditing(false);
174174
};
175175

176-
// v0.11.0:组过滤在客户端生效(列表已全量加载;组切换零请求)
177-
// v0.20.3(REQ-301)+2026-09-06 审查(TD-E):SE 封存默认不可见——#树洞
178-
// tag 精确匹配(防“树洞XX”子串误滤);除显式显隐开关外任何视图态均排除
179-
const visibleNotes = useMemo(() => {
180-
const filtered = groupFilter === null ? list.notes : list.notes.filter((n) => n.group_id === groupFilter);
181-
return filterSealed(filtered);
182-
}, [list.notes, groupFilter, filterSealed]);
183-
184-
// v0.14 B:组映射(noteId → 组,resolveNoteColor 组继承档用)
185-
const groupMap = useMemo(() => new Map(list.groups.map((g) => [g.id, g])), [list.groups]);
186-
// v0.14 B:笔记色板 id 映射(四档优先级解析——笔记显式 > 组继承 > 标签 > 默认灰)
187-
const noteColors = useMemo(() => {
188-
const m: Record<number, string | null> = {};
189-
for (const n of visibleNotes) {
190-
m[n.id] = resolveNoteColor(n, n.group_id != null ? groupMap.get(n.group_id) : null, list.tagColors);
191-
}
192-
return m;
193-
}, [visibleNotes, groupMap, list.tagColors]);
176+
// 批 5 T4:可见笔记(组过滤 + SE 封存)与色板映射的派生搬进 utils/colorPalette
177+
const visibleNotes = useMemo(() => visibleNotesOf(list.notes, groupFilter, filterSealed), [list.notes, groupFilter, filterSealed]);
178+
const noteColors = useMemo(() => buildNoteColorMap(visibleNotes, list.groups, list.tagColors), [visibleNotes, list.groups, list.tagColors]);
194179

195180
return (
196181
<div style={{ display: "flex", height: "calc(100vh - var(--ed-nav-h))", minHeight: 0 }}>

‎app/src/utils/colorPalette.test.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import { describe, expect, it } from "vitest";
88
import {
99
COLOR_IDS,
1010
COLOR_PALETTE,
11+
buildNoteColorMap,
1112
contrastRatio,
1213
isColorId,
1314
isThemeSafe,
1415
onColorText,
1516
paletteHex,
1617
parseNoteProperties,
1718
resolveNoteColor,
19+
visibleNotesOf,
1820
} from "./colorPalette";
21+
import type { Note, NoteGroup } from "../types";
1922

2023
describe("colorPalette 对比度基础", () => {
2124
it("黑白对比度为 21:1", () => {
@@ -118,3 +121,43 @@ describe("resolveNoteColor 四档优先级", () => {
118121
expect(resolveNoteColor(note, null, {})).toBe("neon");
119122
});
120123
});
124+
125+
// ── 批 5 T4:自 NotesPage 搬入的两个派生(判据随搬入新增;既有用例未改)──
126+
const mkNote = (id: number, group_id: number | null, tags = "[]", properties: string | null = null): Note =>
127+
({ id, title: `n${id}`, content: "", source: "manual", tags, properties, pin: 0, group_id, created_at: 1, updated_at: 2 });
128+
129+
const mkGroup = (id: number, color: string | null): NoteGroup =>
130+
({ id, name: `g${id}`, terrain: "container", kind: "topic", domainTag: null, source: "manual",
131+
seriesKey: null, routeReason: null, routeOverridden: 0, color, noteCount: 1, createdAt: 1, updatedAt: 2 });
132+
133+
describe("visibleNotesOf 组过滤 + 封存过滤(批 5 T4 搬入)", () => {
134+
it("① groupFilter 生效:只留该组;null = 全量(含未归组)", () => {
135+
const notes = [mkNote(1, 7), mkNote(2, 8), mkNote(3, null)];
136+
const keep = (n: Note[]) => n;
137+
expect(visibleNotesOf(notes, 7, keep).map((n) => n.id)).toEqual([1]);
138+
expect(visibleNotesOf(notes, null, keep).map((n) => n.id)).toEqual([1, 2, 3]);
139+
});
140+
141+
it("② filterSealed 生效:恒真桩原样透传(同一引用,不复制)/ 恒假桩滤空", () => {
142+
const notes = [mkNote(1, 7)];
143+
const keep = (n: Note[]) => n;
144+
const drop = (): Note[] => [];
145+
expect(visibleNotesOf(notes, null, keep)).toBe(notes);
146+
expect(visibleNotesOf(notes, null, drop)).toEqual([]);
147+
});
148+
});
149+
150+
describe("buildNoteColorMap 色板优先级(批 5 T4 搬入)", () => {
151+
it("③ 笔记显式 > 组继承 > 标签 > 默认灰;未归组/组不存在都不查组表", () => {
152+
const groups = [mkGroup(7, "green")];
153+
const tagColors = { 编程: "blue" };
154+
const notes = [
155+
mkNote(1, 7, '["编程"]', '{"color":"purple"}'), // 显式优先
156+
mkNote(2, 7, '["编程"]'), // 组继承次之
157+
mkNote(3, null, '["编程"]'), // 未归组 → 标签档
158+
mkNote(4, 9, "[]"), // 组不存在 → 默认灰
159+
mkNote(5, null, "[]"), // 全无量 → 默认灰
160+
];
161+
expect(buildNoteColorMap(notes, groups, tagColors)).toEqual({ 1: "purple", 2: "green", 3: "blue", 4: null, 5: null });
162+
});
163+
});

‎app/src/utils/colorPalette.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
* isThemeSafe/onColorText 决定色块上的文字颜色(黑/白取高对比),
77
* resolveNoteColor 统一卡片/列表/侧栏的颜色来源(spec §4.3)。
88
* 未知色板 id 一律回退默认灰(数据损坏防御,不崩溃)。
9+
* @ai-context: 批 5 T4——`NotesPage`(300/300 贴边)的「可见笔记派生」与「色板
10+
* id 映射」两个 useMemo 搬到这里(visibleNotesOf / buildNoteColorMap);
11+
* 归属理由:noteColors 被列表列与阅读列**共用**,且本文件已是
12+
* resolveNoteColor(四级解析)的真源 ⇒ 不制造第二份派生。
913
*/
14+
import type { Note, NoteGroup } from "../types";
1015

1116
/** 色板 id(12 色;null=未设置/默认灰) */
1217
export type ColorId =
@@ -122,3 +127,49 @@ export function resolveNoteColor(
122127
}
123128
return null;
124129
}
130+
131+
/**
132+
* 可见笔记派生:组过滤 → SE 封存过滤(v0.11.0 + v0.20.3 REQ-301;批 5 T4 自
133+
* `NotesPage` 逐字搬入)。
134+
*
135+
* @ai-context: 组过滤在客户端生效(列表已全量加载 ⇒ 组切换零请求);SE 封存默认
136+
* 不可见由注入的 filterSealed 承载(#树洞 tag 精确匹配,防「树洞XX」
137+
* 子串误滤,见 `hooks/useNotesSealedFilter`)——本函数只把「先组过滤、
138+
* 后封存过滤」的既有串接顺序固化在一处,供页面 memo 调用。
139+
* @ai-context: 纯函数——不读 localStorage、不调 invoke、不改入参;groupFilter===null
140+
* 时把 notes **原样**交给 filterSealed(不复制 ⇒ 保持下游 memo 的引用
141+
* 语义,与拆前 `NotesPage.tsx:179-182` 逐字等价)。
142+
* @ai-context: 形参 filterSealed 用 `(n: Note[]) => Note[]`(非 readonly)——消费者
143+
* `useNotesSealedFilter` 的实参是可变数组;readonly 形参在
144+
* `strictFunctionTypes` 下不可赋值(批 5 T4 实测 tsc TS2345)。
145+
*/
146+
export function visibleNotesOf(
147+
notes: Note[],
148+
groupFilter: number | null,
149+
filterSealed: (n: Note[]) => Note[],
150+
): Note[] {
151+
const filtered = groupFilter === null ? notes : notes.filter((n) => n.group_id === groupFilter);
152+
return filterSealed(filtered);
153+
}
154+
155+
/**
156+
* 笔记色板 id 映射(v0.14 B;批 5 T4 自 `NotesPage` 逐字搬入)。
157+
*
158+
* @ai-context: 四档优先级解析的真源是 resolveNoteColor(笔记显式 > 组继承 > 标签 >
159+
* 默认灰);本函数只做「组表 → noteId → 组色」的查表与逐笔记求值。
160+
* @ai-context: 纯函数——不读 localStorage、不调 invoke、不改入参(返回新对象);
161+
* 未归组笔记(group_id == null)传 null 组、不查表(防组 id 缺省时
162+
* 误继承其它组色)。
163+
*/
164+
export function buildNoteColorMap(
165+
visibleNotes: readonly Note[],
166+
groups: readonly NoteGroup[],
167+
tagColors: Readonly<Record<string, string>>,
168+
): Record<number, string | null> {
169+
const groupMap = new Map(groups.map((g): [number, NoteGroup] => [g.id, g]));
170+
const m: Record<number, string | null> = {};
171+
for (const n of visibleNotes) {
172+
m[n.id] = resolveNoteColor(n, n.group_id != null ? groupMap.get(n.group_id) : null, tagColors);
173+
}
174+
return m;
175+
}

0 commit comments

Comments
 (0)