新增功能: - 统一续学逻辑:8个故居首页接入自动续学提示弹窗 - 完成判定:基于拼图碎片统一判断对话/体悟/内化是否完成 - 断点续学:对话回lastVisitedPlace、体悟回reflectionRoute、内化回internalization - 触发条件:会话内每故居仅提示一次 + 首页视频播完后 + store加载成功/空时 - 安全防护:组件卸载后不再setState、同步失败不弹窗避免用旧状态误导 新增组件: - ContinueLearningModal:纯CSS哑光米纸+朱红古籍风弹窗 - useResidenceContinue:封装触发逻辑+跳转+去重的共享hook 涉及文件: - src/config/progression.ts:getResidenceStageCompletion、resolveResidenceContinueTarget - src/hooks/useResidenceContinue.ts:统一续学hook - src/components/layout/alert/continue-learning-modal.tsx:弹窗组件 - src/pages/*/home.tsx:8个故居首页接入 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
155 lines
5.1 KiB
TypeScript
155 lines
5.1 KiB
TypeScript
import { puzzleDataConfig } from './puzzleData'
|
|
import { PUZZLE_RESIDENCES } from './puzzleResidences'
|
|
|
|
export type PuzzleProgress = Record<string, Set<number>>
|
|
|
|
const getResidenceIndex = (residence: string) =>
|
|
PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence)
|
|
|
|
const getConfiguredPieceIds = (residence: string) => {
|
|
const pieces = puzzleDataConfig[residence]
|
|
if (!pieces) return []
|
|
return Object.keys(pieces).map(Number)
|
|
}
|
|
|
|
const arePiecesUnlocked = (residence: string, puzzles: PuzzleProgress, pieceIds: number[]) => {
|
|
if (pieceIds.length === 0) return false
|
|
const unlockedPieces = puzzles[residence]
|
|
if (!unlockedPieces) return false
|
|
return pieceIds.every((pieceId) => unlockedPieces.has(pieceId))
|
|
}
|
|
|
|
export const isResidenceStarted = (residence: string, puzzles: PuzzleProgress) => {
|
|
return (puzzles[residence]?.size ?? 0) > 0
|
|
}
|
|
|
|
export const isResidenceComplete = (residence: string, puzzles: PuzzleProgress) => {
|
|
return arePiecesUnlocked(residence, puzzles, getConfiguredPieceIds(residence))
|
|
}
|
|
|
|
export const isResidenceUnlocked = (residence: string, puzzles: PuzzleProgress) => {
|
|
const index = getResidenceIndex(residence)
|
|
if (index === 0) return true
|
|
if (index < 0) return false
|
|
|
|
const previousResidence = PUZZLE_RESIDENCES[index - 1]
|
|
if (!previousResidence) return false
|
|
return isResidenceComplete(previousResidence.residence, puzzles)
|
|
}
|
|
|
|
export const isMenuModuleUnlocked = (moduleName: string, puzzles: PuzzleProgress) => {
|
|
return PUZZLE_RESIDENCES.some(
|
|
(item) => item.moduleName === moduleName && isResidenceUnlocked(item.residence, puzzles)
|
|
)
|
|
}
|
|
|
|
export const isMenuModuleStarted = (moduleName: string, puzzles: PuzzleProgress) => {
|
|
return PUZZLE_RESIDENCES.some(
|
|
(item) => item.moduleName === moduleName && isResidenceStarted(item.residence, puzzles)
|
|
)
|
|
}
|
|
|
|
export const isMenuModuleComplete = (moduleName: string, puzzles: PuzzleProgress) => {
|
|
const residences = PUZZLE_RESIDENCES.filter((item) => item.moduleName === moduleName)
|
|
if (residences.length === 0) return false
|
|
return residences.every((item) => isResidenceComplete(item.residence, puzzles))
|
|
}
|
|
|
|
const getPieceIdsByRoute = (residence: string, routePrefix: string) => {
|
|
const pieces = puzzleDataConfig[residence]
|
|
if (!pieces) return []
|
|
|
|
return Object.entries(pieces)
|
|
.filter(([, piece]) => piece.route.startsWith(routePrefix))
|
|
.map(([pieceId]) => Number(pieceId))
|
|
}
|
|
|
|
export const getResidenceStageUnlocks = (residence: string, puzzles: PuzzleProgress) => {
|
|
const residenceUnlocked = isResidenceUnlocked(residence, puzzles)
|
|
|
|
return {
|
|
dialogueUnlocked: residenceUnlocked,
|
|
reflectionUnlocked:
|
|
residenceUnlocked &&
|
|
arePiecesUnlocked(residence, puzzles, getPieceIdsByRoute(residence, 'place')),
|
|
internalizationUnlocked:
|
|
residenceUnlocked &&
|
|
arePiecesUnlocked(residence, puzzles, getPieceIdsByRoute(residence, 'reflection')),
|
|
}
|
|
}
|
|
|
|
export interface ResidenceStageCompletion {
|
|
dialogueComplete: boolean
|
|
reflectionComplete: boolean
|
|
internalizationComplete: boolean
|
|
residenceComplete: boolean
|
|
}
|
|
|
|
/**
|
|
* 基于拼图碎片统一判定每个故居三阶段是否完成。
|
|
* 每个故居 16 块碎片天然按 route 分为 place* / reflection* / internalization 三段,
|
|
* 收齐对应碎片即视为该阶段完成。
|
|
*/
|
|
export const getResidenceStageCompletion = (
|
|
residence: string,
|
|
puzzles: PuzzleProgress
|
|
): ResidenceStageCompletion => {
|
|
const dialogueComplete = arePiecesUnlocked(
|
|
residence,
|
|
puzzles,
|
|
getPieceIdsByRoute(residence, 'place')
|
|
)
|
|
const reflectionComplete = arePiecesUnlocked(
|
|
residence,
|
|
puzzles,
|
|
getPieceIdsByRoute(residence, 'reflection')
|
|
)
|
|
const internalizationComplete = arePiecesUnlocked(
|
|
residence,
|
|
puzzles,
|
|
getPieceIdsByRoute(residence, 'internalization')
|
|
)
|
|
|
|
return {
|
|
dialogueComplete,
|
|
reflectionComplete,
|
|
internalizationComplete,
|
|
residenceComplete: dialogueComplete && reflectionComplete && internalizationComplete,
|
|
}
|
|
}
|
|
|
|
export interface ResidenceContinueInput {
|
|
completion: ResidenceStageCompletion
|
|
/** 对话模块断点(place1~placeN),各故居 store 均有 */
|
|
lastVisitedPlace?: string | null
|
|
/** 体悟模块断点路由,仅部分多场景故居(孙中山/鲁迅/黄炎培)有 */
|
|
reflectionRoute?: string | null
|
|
}
|
|
|
|
/**
|
|
* 计算「继续学习」应跳转到的目标路由——取第一个未完成的阶段。
|
|
* - 对话未完成 → lastVisitedPlace || 'place1'
|
|
* - 体悟未完成 → reflectionRoute || 'reflection'
|
|
* - 内化未完成 → 'internalization'
|
|
* - 全部完成 → null(无需续学)
|
|
*
|
|
* 这些路由都是各故居 home/场景现有逻辑已经在用的跳转目标,
|
|
* 单场景体悟(毛泽东/陈云/聂耳/荣德生)统一落到 'reflection' 由页面自管进度。
|
|
*/
|
|
export const resolveResidenceContinueTarget = ({
|
|
completion,
|
|
lastVisitedPlace,
|
|
reflectionRoute,
|
|
}: ResidenceContinueInput): string | null => {
|
|
if (!completion.dialogueComplete) {
|
|
return lastVisitedPlace || 'place1'
|
|
}
|
|
if (!completion.reflectionComplete) {
|
|
return reflectionRoute || 'reflection'
|
|
}
|
|
if (!completion.internalizationComplete) {
|
|
return 'internalization'
|
|
}
|
|
return null
|
|
}
|