Explain invite vs. generating state using MemoirState slots, surface remaining characters to the display gate, refresh memoir-state on pull, and sync i18n. Co-authored-by: Cursor <cursoragent@cursor.com>
64 lines
1.6 KiB
TypeScript
64 lines
1.6 KiB
TypeScript
import type { ChapterViewModel } from './types';
|
||
|
||
/**
|
||
* 与后端 `CHAPTER_ORDER` / `CHAPTER_CATEGORIES` 顺序一致;对应 i18n `memoir.frameworkChapters.*`。
|
||
*/
|
||
export const FRAMEWORK_CHAPTER_KEYS = [
|
||
'chapter1',
|
||
'chapter2',
|
||
'chapter3',
|
||
'chapter4',
|
||
'chapter5',
|
||
'chapter6',
|
||
'chapter7',
|
||
'chapter8',
|
||
] as const;
|
||
|
||
export type FrameworkChapterKey = (typeof FRAMEWORK_CHAPTER_KEYS)[number];
|
||
|
||
/** 与后端 `CHAPTER_ORDER` 一致(用于占位章节缺省 category) */
|
||
export const CHAPTER_CATEGORY_BY_ORDER_INDEX = [
|
||
'childhood',
|
||
'education',
|
||
'career_early',
|
||
'career_achievement',
|
||
'career_challenge',
|
||
'family',
|
||
'beliefs',
|
||
'summary',
|
||
] as const;
|
||
|
||
export function buildFrameworkChapterPlaceholders(
|
||
tr: (key: string) => string,
|
||
): ChapterViewModel[] {
|
||
return FRAMEWORK_CHAPTER_KEYS.map((key, orderIndex) => ({
|
||
id: `framework:${key}`,
|
||
title: tr(`frameworkChapters.${key}`),
|
||
category: '',
|
||
orderIndex,
|
||
isEmpty: true,
|
||
isNew: false,
|
||
hasImages: false,
|
||
allImagesReady: false,
|
||
pendingImageCount: 0,
|
||
failedImageCount: 0,
|
||
coverImageUrl: null,
|
||
updatedAt: null,
|
||
wordCount: 0,
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 列表页始终展示 8 个框架槽位:已有章节用接口数据,其余槽位仍用框架占位(与「零章节」时一致)。
|
||
*/
|
||
export function mergeFrameworkChaptersWithFetched(
|
||
placeholders: ChapterViewModel[],
|
||
fetched: ChapterViewModel[],
|
||
): ChapterViewModel[] {
|
||
const byOrder = new Map<number, ChapterViewModel>();
|
||
for (const vm of fetched) {
|
||
byOrder.set(vm.orderIndex, vm);
|
||
}
|
||
return placeholders.map((p) => byOrder.get(p.orderIndex) ?? p);
|
||
}
|