From 5a02effd9d34ddffdcd7655bfdcb70c51338d909 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sat, 6 Jun 2026 22:17:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8E=A5=E5=85=A5=20BangDream=20?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E5=8C=BA=E5=9D=97=E6=9E=84=E5=BB=BA=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot-bangdream-tsugu-global-refactor-plan.md | 2 + package.json | 1 + .../tsugu/command-renderers/event-detail.ts | 88 +++++++------------ .../render-blocks/detail-block-builder.ts | 44 ++++++++++ .../tsugu/detail-block-builder.spec.ts | 26 ++++++ 5 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder.ts create mode 100644 test/qqbot/plugins/bangDream/tsugu/detail-block-builder.spec.ts diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index e1c63c6..3c2eb63 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -345,6 +345,8 @@ export interface TsuguHook { - 已新增 `render-blocks/layout-spec.ts`,统一横向/纵向虚线分割规格,歌曲列表、活动列表、活动详情和通用列表框架不再重复维护分割线宽高/颜色。 - 已新增 `runtime/asset-manifest.ts`,收口本地 `BG`、`Card`、`Skill`、`SongChart`、字体和标题资源路径;`canvas/text.ts`、`canvas/rect.ts`、`canvas/output.ts`、`canvas/background.ts`、`card-art.ts`、`list-rarity.ts`、`skill-text.ts`、`title.ts`、`song-chart-preview.ts` 已改走 manifest。 - 已生成查曲、查活动和查谱面 smoke 图片,验证 theme/spec/manifest 第一段迁移后本地图片输出非空,谱面预览资源路径未断。 +- 已新增 `render-blocks/detail-block-builder.ts`,活动详情页先接入 `DetailBlockBuilder` 统一 section、spacer 和 data block 拼装,去掉局部 `pushSection` 和手动分割线维护;`detail-block-builder.spec.ts` 覆盖 section 分割线与 spacer/data block 输出。 +- 本地验证时发现 Jest 无法解析 `skia-canvas/lib/v6/index.node`,根因是 `moduleFileExtensions` 未包含 `node`;已把 `.node` 原生扩展解析固化进 Jest 配置,避免后续渲染单测重复卡在同类问题。 - 线上 `/qqbot/command/test` smoke 已固化为先按 `operationKey` 查询启用命令、传 `commandId`,并保留完整命令文本;避免默认 `preview` selfId 未绑定命令时误报“未匹配到命令”。 ### Phase 6:策略 policy 和时间/档线规则 diff --git a/package.json b/package.json index 3dc9c73..41ba4c4 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "moduleFileExtensions": [ "js", "json", + "node", "ts" ], "rootDir": ".", diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts index cc389cd..319285e 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts @@ -2,7 +2,6 @@ import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event'; import { Card } from '@/qqbot/plugins/bangDream/tsugu/models/card'; import { drawList, - line, drawListByServerList, drawListMerge, } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame'; @@ -37,7 +36,7 @@ import { } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song'; import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line'; import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; -import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; +import { DetailBlockBuilder } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder'; const songSeparatorLine = drawDottedLine( createHorizontalSeparatorSpec({ height: 10 }), @@ -59,17 +58,6 @@ async function drawSongListDataBlock(songList: Song[], topLeftText?: string) { return drawDataBlock({ list, topLeftText }); } -/** - * 在QQBot 图片视图层中推入区块。 - * - * @param list - 待处理列表。 - * @param section - 区块参数。 - */ -function pushSection(list: Array, section: Image | Canvas) { - list.push(section); - list.push(line); -} - /** * 在QQBot 图片视图层中判断对象是否包含指定自有属性。 * @@ -84,21 +72,20 @@ function hasOwn(source: object, key: string): boolean { /** * 在QQBot 图片视图层中追加活动加成区块列表。 * - * @param list - 待处理列表。 + * @param builder - 详情区块构建器。 * @param event - 活动参数。 */ async function appendEventBonusSections( - list: Array, + builder: DetailBlockBuilder, event: Event, ): Promise { - pushSection(list, drawList({ key: '活动加成' })); + builder.addSection(drawList({ key: '活动加成' })); const attributeList = event.getAttributeList(); for (const percent in attributeList) { if (!hasOwn(attributeList, percent)) { continue; } - pushSection( - list, + builder.addSection( await drawAttributeInList({ content: attributeList[percent], text: ` +${percent}%`, @@ -106,14 +93,13 @@ async function appendEventBonusSections( ); } - pushSection(list, drawList({ key: '活动角色加成' })); + builder.addSection(drawList({ key: '活动角色加成' })); const characterList = event.getCharacterList(); for (const percent in characterList) { if (!hasOwn(characterList, percent)) { continue; } - pushSection( - list, + builder.addSection( await drawCharacterInList({ content: characterList[percent], text: ` +${percent}%`, @@ -146,16 +132,18 @@ function getEventStatBonusText(event: Event): string { /** * 在QQBot 图片视图层中追加活动数值加成。 * - * @param list - 待处理列表。 + * @param builder - 详情区块构建器。 * @param event - 活动参数。 */ -function appendEventStatBonus(list: Array, event: Event): void { +function appendEventStatBonus( + builder: DetailBlockBuilder, + event: Event, +): void { const statText = getEventStatBonusText(event); if (!statText) { return; } - pushSection( - list, + builder.addSection( drawList({ key: '活动偏科加成', text: statText, @@ -166,19 +154,18 @@ function appendEventStatBonus(list: Array, event: Event): void { /** * 在QQBot 图片视图层中追加活动奖励区块列表。 * - * @param list - 待处理列表。 + * @param builder - 详情区块构建器。 * @param event - 活动参数。 * @param displayedServerList - 允许展示或下载资源的服务器优先级列表。 */ async function appendEventRewardSections( - list: Array, + builder: DetailBlockBuilder, event: Event, displayedServerList: Server[], ): Promise { const decoImage = await event.getRewardDeco(displayedServerList[0]); if (decoImage) { - pushSection( - list, + builder.addSection( await drawList({ key: '活动装饰', content: [decoImage], @@ -188,12 +175,11 @@ async function appendEventRewardSections( ); } - pushSection(list, await drawDegreeListOfEvent(event, displayedServerList)); + builder.addSection(await drawDegreeListOfEvent(event, displayedServerList)); const stampImage = await event.getRewardStamp(displayedServerList[0]); if (stampImage) { - pushSection( - list, + builder.addSection( await drawList({ key: '活动表情', content: [stampImage], @@ -204,8 +190,7 @@ async function appendEventRewardSections( } const rewardCardList = event.rewardCards.map((cardId) => new Card(cardId)); - pushSection( - list, + builder.addSection( await drawCardListInList({ key: '奖励卡牌', cardList: rewardCardList, @@ -231,12 +216,12 @@ function getEventMusicServer(event: Event, displayedServerList: Server[]) { /** * 在QQBot 图片视图层中追加活动音乐区块。 * - * @param list - 待处理列表。 + * @param builder - 详情区块构建器。 * @param event - 活动参数。 * @param displayedServerList - 允许展示或下载资源的服务器优先级列表。 */ async function appendEventMusicSection( - list: Array, + builder: DetailBlockBuilder, event: Event, displayedServerList: Server[], ): Promise { @@ -253,7 +238,7 @@ async function appendEventMusicSection( const songs = event.musics[musicServer].map( (music) => new Song(music.musicId), ); - pushSection(list, await drawSongListInList(songs)); + builder.addSection(await drawSongListInList(songs)); } interface EventGachaSections { @@ -381,23 +366,21 @@ export async function drawEventDetail( return ['错误: 活动不存在']; } await event.initFull(); - const list: Array = []; + const builder = new DetailBlockBuilder(); //bannner const eventBannerImage = await event.getBannerImage(); const eventBannerImageCanvas = drawBannerImageCanvas(eventBannerImage); - list.push(eventBannerImageCanvas); - list.push(new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 30)); + builder.add(eventBannerImageCanvas).addSpacer(30); //标题 - list.push( + builder.addSection( await drawListByServerList( event.eventName, '活动名称', displayedServerList, ), ); - list.push(line); //类型 const typeImage = drawList({ @@ -411,11 +394,10 @@ export async function drawEventDetail( text: event.eventId.toString(), }); - list.push(drawListMerge([typeImage, idImage])); - list.push(line); + builder.addSection(drawListMerge([typeImage, idImage])); //开始时间 - list.push( + builder.addSection( await drawTimeInList({ key: '开始时间', content: event.startAt, @@ -423,35 +405,33 @@ export async function drawEventDetail( estimateCNTime: true, }), ); - list.push(line); //结束时间 - list.push( + builder.addSection( await drawTimeInList({ key: '结束时间', content: event.endAt, }), ); - list.push(line); //活动属性加成 - await appendEventBonusSections(list, event); + await appendEventBonusSections(builder, event); //活动偏科加成(stat) - appendEventStatBonus(list, event); + appendEventStatBonus(builder, event); //有歌榜活动的歌榜歌曲 - await appendEventMusicSection(list, event, displayedServerList); + await appendEventMusicSection(builder, event, displayedServerList); // 活动装饰、牌子、表情和奖励卡牌 - await appendEventRewardSections(list, event, displayedServerList); + await appendEventRewardSections(builder, event, displayedServerList); const { gachaCardList, gachaImageList } = await collectEventGachaSections( event, displayedServerList, ); - list.push( + builder.add( await drawCardListInList({ key: '活动期间卡池卡牌', cardList: gachaCardList, @@ -462,7 +442,7 @@ export async function drawEventDetail( }), ); - const listImage = drawDataBlock({ list }); + const listImage = builder.toDataBlock(); //创建最终输出数组 const all = []; diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder.ts new file mode 100644 index 0000000..ead4ec5 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder.ts @@ -0,0 +1,44 @@ +import { Canvas, Image } from 'skia-canvas'; +import { drawDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/data-block'; +import { line } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; + +export interface DetailBlockDataOptions { + BG?: boolean; + opacity?: number; + topLeftText?: string; +} + +/** + * 按详情页顺序收集区块,并统一处理区块分割线。 + */ +export class DetailBlockBuilder { + private readonly list: Array = []; + + add(block: Canvas | Image): this { + this.list.push(block); + return this; + } + + addSection(block: Canvas | Image): this { + this.list.push(block); + this.list.push(line); + return this; + } + + addSpacer( + height: number, + width: number = BANGDREAM_RENDER_THEME.layout.contentWidth, + ): this { + this.list.push(new Canvas(width, height)); + return this; + } + + toList(): Array { + return [...this.list]; + } + + toDataBlock(options: DetailBlockDataOptions = {}): Canvas { + return drawDataBlock({ ...options, list: this.list }); + } +} diff --git a/test/qqbot/plugins/bangDream/tsugu/detail-block-builder.spec.ts b/test/qqbot/plugins/bangDream/tsugu/detail-block-builder.spec.ts new file mode 100644 index 0000000..9c4bf5a --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/detail-block-builder.spec.ts @@ -0,0 +1,26 @@ +import { Canvas } from 'skia-canvas'; +import { DetailBlockBuilder } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder'; +import { line } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame'; + +describe('DetailBlockBuilder', () => { + it('keeps section separators centralized', () => { + const first = new Canvas(10, 20); + const second = new Canvas(30, 40); + const list = new DetailBlockBuilder() + .addSection(first) + .add(second) + .toList(); + + expect(list).toEqual([first, line, second]); + }); + + it('creates spacers and data blocks from collected sections', () => { + const dataBlock = new DetailBlockBuilder() + .add(new Canvas(100, 50)) + .addSpacer(30, 100) + .toDataBlock(); + + expect(dataBlock.width).toBeGreaterThan(0); + expect(dataBlock.height).toBeGreaterThan(0); + }); +});