fix: 修复开发/测试包模拟进度被易班同步覆盖的问题
进度模拟面板写入 puzzleStore 后,App 启动时易班同步会将其覆盖; 现在在同步完成后按 sessionStorage 中保存的模式重放模拟进度, 确保测试包的"到达某故居/全流程完成"调试入口生效。 同时纳入近期未提交的页面、组件、资源与环境变量改动。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
No files matched your search
@@ -58,7 +58,8 @@
|
|||||||
"Bash(git config *)",
|
"Bash(git config *)",
|
||||||
"WebSearch",
|
"WebSearch",
|
||||||
"Bash(grep -v \"^--$\")",
|
"Bash(grep -v \"^--$\")",
|
||||||
"Bash(grep -E \"^\\\\S+.*>.*</GuideStepsItem>$\")"
|
"Bash(grep -E \"^\\\\S+.*>.*</GuideStepsItem>$\")",
|
||||||
|
"Bash(git -C \"/Users/zhongyidian/工作/育享/沪上又见故人来/shanghai-residences/.claude/worktrees/agent-ab7540fc239364906\" status --short)"
|
||||||
],
|
],
|
||||||
"deny": [
|
"deny": [
|
||||||
"Read(public/**)",
|
"Read(public/**)",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_DEBUG_ENTRY=false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_DEBUG_ENTRY=false
|
||||||
@@ -5,7 +5,15 @@ import { ScaleProvider, useScale } from './contexts/ScaleContext'
|
|||||||
import { RotateHint } from './components/RotateHint'
|
import { RotateHint } from './components/RotateHint'
|
||||||
import { HintHost } from './components'
|
import { HintHost } from './components'
|
||||||
import { useUserStore } from './store'
|
import { useUserStore } from './store'
|
||||||
import { getUserDefine } from '@/lib/utils/api-common'
|
import { getUserDefineResult } from '@/lib/utils/api-common'
|
||||||
|
import {
|
||||||
|
SyncLoadingHost,
|
||||||
|
SyncPromptHost,
|
||||||
|
hideSyncLoading,
|
||||||
|
showSyncLoading,
|
||||||
|
showYibanSyncErrorPrompt,
|
||||||
|
showYibanUnauthenticatedPrompt,
|
||||||
|
} from '@/components'
|
||||||
import {
|
import {
|
||||||
usePuzzleStore,
|
usePuzzleStore,
|
||||||
deserializePuzzles,
|
deserializePuzzles,
|
||||||
@@ -13,6 +21,13 @@ import {
|
|||||||
PUZZLE_KEY,
|
PUZZLE_KEY,
|
||||||
} from '@/store/puzzleStore'
|
} from '@/store/puzzleStore'
|
||||||
|
|
||||||
|
const applyStoredProgressionDebugMode = async () => {
|
||||||
|
if (import.meta.env.VITE_DEBUG_ENTRY !== 'true') return false
|
||||||
|
|
||||||
|
const debug = await import('@/debug/progressionDebug')
|
||||||
|
return debug.applyStoredProgressionDebugMode()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用内容组件
|
* 应用内容组件
|
||||||
* 使用 useScale 获取缩放样式
|
* 使用 useScale 获取缩放样式
|
||||||
@@ -36,20 +51,39 @@ function AppContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
showSyncLoading()
|
||||||
try {
|
try {
|
||||||
const raw = await getUserDefine(PUZZLE_LESSON_ID, PUZZLE_KEY)
|
const result = await getUserDefineResult(PUZZLE_LESSON_ID, PUZZLE_KEY)
|
||||||
if (raw) {
|
|
||||||
const data = JSON.parse(raw) as { puzzles?: Record<string, number[]> }
|
if (result.status === 'success') {
|
||||||
if (data.puzzles) {
|
const data = JSON.parse(result.content) as { puzzles?: Record<string, number[]> }
|
||||||
usePuzzleStore.setState((prev) => ({
|
usePuzzleStore.setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
puzzles: deserializePuzzles(data.puzzles),
|
puzzles: deserializePuzzles(data.puzzles),
|
||||||
}))
|
}))
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
usePuzzleStore.setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
puzzles: {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (result.status === 'unauthenticated') {
|
||||||
|
showYibanUnauthenticatedPrompt()
|
||||||
|
} else if (result.status === 'error') {
|
||||||
|
showYibanSyncErrorPrompt()
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
usePuzzleStore.setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
puzzles: {},
|
||||||
|
}))
|
||||||
|
showYibanSyncErrorPrompt()
|
||||||
console.error('Failed to load puzzle store', e)
|
console.error('Failed to load puzzle store', e)
|
||||||
} finally {
|
} finally {
|
||||||
|
await applyStoredProgressionDebugMode()
|
||||||
|
hideSyncLoading()
|
||||||
usePuzzleStore.getState().setLoaded(true)
|
usePuzzleStore.getState().setLoaded(true)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -67,6 +101,8 @@ function AppContent() {
|
|||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
<HintHost />
|
<HintHost />
|
||||||
|
<SyncPromptHost />
|
||||||
|
<SyncLoadingHost />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 3.7 MiB After Width: | Height: | Size: 439 KiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 398 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 491 KiB |
|
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 446 KiB After Width: | Height: | Size: 486 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 398 KiB After Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 443 KiB After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 357 KiB |
|
Before Width: | Height: | Size: 372 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 503 KiB After Width: | Height: | Size: 494 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 537 KiB After Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 777 KiB After Width: | Height: | Size: 796 KiB |
|
Before Width: | Height: | Size: 798 KiB After Width: | Height: | Size: 817 KiB |
|
Before Width: | Height: | Size: 972 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 780 KiB After Width: | Height: | Size: 921 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 3.1 MiB After Width: | Height: | Size: 3.1 MiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1733.87 467.43"><defs><linearGradient id="guide-fill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#a98d80"/><stop offset="100%" stop-color="#c5523d"/></linearGradient></defs><path fill="url(#guide-fill)" d="M1733.54,376.29s-33.78-11.21-64.39,21.27-11.5,69.57-11.5,69.57l-1581.76.3S95,430.35,64.39,397.86,0,376.59,0,376.59L.33,90.84s33.78,11.21,64.39-21.27S76.22,0,76.22,0L1658,.3s-19.12,37.08,11.5,69.57,64.39,21.27,64.39,21.27Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 505 B |
@@ -1,5 +1,5 @@
|
|||||||
import type { CSSProperties, ReactNode } from 'react'
|
import type { CSSProperties, ReactNode } from 'react'
|
||||||
import { Children, useEffect, useState } from 'react'
|
import { Children, useEffect, useRef, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Image } from '@/components'
|
import { Image } from '@/components'
|
||||||
import { cn, getStaticImage } from '@/lib/utils'
|
import { cn, getStaticImage } from '@/lib/utils'
|
||||||
@@ -95,6 +95,8 @@ export const StudyGuideModal = ({
|
|||||||
audioChannelId,
|
audioChannelId,
|
||||||
}: StudyGuideModalProps) => {
|
}: StudyGuideModalProps) => {
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
|
const frameRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [frameHeight, setFrameHeight] = useState(0)
|
||||||
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale()
|
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale()
|
||||||
const videoContext = useVideoContextSafe()
|
const videoContext = useVideoContextSafe()
|
||||||
const { currentSrc, play, stop } = useAudioManager(
|
const { currentSrc, play, stop } = useAudioManager(
|
||||||
@@ -141,6 +143,18 @@ export const StudyGuideModal = ({
|
|||||||
}
|
}
|
||||||
}, [mounted, canPlayAudio, audioSrc, currentSrc, stop])
|
}, [mounted, canPlayAudio, audioSrc, currentSrc, stop])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mounted || !visible || !frameRef.current) return
|
||||||
|
|
||||||
|
const updateFrameHeight = () => setFrameHeight(frameRef.current?.offsetHeight ?? 0)
|
||||||
|
updateFrameHeight()
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(updateFrameHeight)
|
||||||
|
observer.observe(frameRef.current)
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [mounted, visible, children])
|
||||||
|
|
||||||
if (!visible || !mounted) return null
|
if (!visible || !mounted) return null
|
||||||
|
|
||||||
const handleMaskClick = () => {
|
const handleMaskClick = () => {
|
||||||
@@ -158,55 +172,144 @@ export const StudyGuideModal = ({
|
|||||||
|
|
||||||
const contentChildren = renderIndentedContent(children, firstLineIndent)
|
const contentChildren = renderIndentedContent(children, firstLineIndent)
|
||||||
|
|
||||||
|
const fillHeight = Math.max(467.43, frameHeight - 74)
|
||||||
|
const fillPath = [
|
||||||
|
'M76.22,0',
|
||||||
|
'L1658,0.3',
|
||||||
|
'C1658,0.3 1638.88,37.38 1669.5,69.87',
|
||||||
|
'C1700.12,102.36 1733.89,91.14 1733.89,91.14',
|
||||||
|
`L1733.54,${fillHeight - 91.14}`,
|
||||||
|
`C1733.54,${fillHeight - 91.14} 1699.76,${fillHeight - 102.35} 1669.15,${fillHeight - 69.87}`,
|
||||||
|
`C1638.54,${fillHeight - 37.38} 1657.65,${fillHeight - 0.3} 1657.65,${fillHeight - 0.3}`,
|
||||||
|
`L75.89,${fillHeight}`,
|
||||||
|
`C75.89,${fillHeight} 95,${fillHeight - 37.08} 64.39,${fillHeight - 69.57}`,
|
||||||
|
`C33.78,${fillHeight - 102.06} 0,${fillHeight - 90.84} 0,${fillHeight - 90.84}`,
|
||||||
|
'L0.33,90.84',
|
||||||
|
'C0.33,90.84 34.11,102.05 64.72,69.57',
|
||||||
|
'C95.34,37.08 76.22,0 76.22,0',
|
||||||
|
'Z',
|
||||||
|
].join(' ')
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div
|
<div className="fixed inset-0" style={{ zIndex, pointerEvents: 'auto' }}>
|
||||||
className="fixed inset-0 flex items-center justify-center"
|
|
||||||
style={{ zIndex, pointerEvents: 'auto' }}
|
|
||||||
>
|
|
||||||
{/* 背景遮罩 */}
|
{/* 背景遮罩 */}
|
||||||
<div className="absolute inset-0 bg-black/60" onClick={handleMaskClick} />
|
<div className="absolute inset-0 bg-black/60" onClick={handleMaskClick} />
|
||||||
|
|
||||||
{/* 引导内容 */}
|
{/* 引导内容 */}
|
||||||
<div className="relative w-[1700px] h-[760px] pointer-events-none pt-65">
|
<div className={cn('absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2')}>
|
||||||
{/* 引导框背景 */}
|
{/* 标题 */}
|
||||||
|
{_title &&
|
||||||
|
(_title === '温馨提示' ? (
|
||||||
|
<img
|
||||||
|
src={getStaticImage('components/guide/title')}
|
||||||
|
alt={_title}
|
||||||
|
className="pointer-events-none absolute left-1/2 -top-5 z-20 -translate-x-1/2"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="pointer-events-none absolute left-1/2 top-6 z-20 -translate-x-1/2 text-5xl font-bold text-[#ffe7b2] tracking-[12px]">
|
||||||
|
{_title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* 叶子 */}
|
||||||
<img
|
<img
|
||||||
src={getStaticImage('components/guide/引导框')}
|
src={getStaticImage('components/guide/竹叶')}
|
||||||
alt="学习引导背景"
|
alt=""
|
||||||
className="absolute inset-0 w-full h-full object-contain"
|
className="pointer-events-none absolute left-10 top-16 z-20"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 文本内容 */}
|
{/* 星星 */}
|
||||||
<div
|
<img
|
||||||
className={cn(
|
src={getStaticImage('components/guide/star')}
|
||||||
'relative z-10 w-[1480px] flex items-center justify-center mx-auto text-white pointer-events-auto',
|
alt=""
|
||||||
ailgn === 'left' && 'justify-start',
|
className="pointer-events-none absolute left-1/2 top-20 z-20 -translate-x-1/2"
|
||||||
ailgn === 'right' && 'justify-end'
|
/>
|
||||||
)}
|
|
||||||
>
|
{/* 内容 */}
|
||||||
{/* {title && (
|
<div ref={frameRef} className="relative z-10 w-[1818px] text-white pointer-events-auto">
|
||||||
<div className="mt-40 text-5xl font-bold text-[#ffe7b2] tracking-[12px]">
|
{/* 中间不规则渐变填充 */}
|
||||||
{title}
|
<div
|
||||||
</div>
|
className="absolute z-0 pointer-events-none"
|
||||||
)} */}
|
style={{
|
||||||
{children && (
|
left: '38px',
|
||||||
<div
|
top: '28px',
|
||||||
className={cn(
|
width: 'calc(100% - 80px)',
|
||||||
'text-4xl leading-relaxed whitespace-pre-line overflow-y-auto custom-scrollbar max-h-[300px]',
|
height: 'calc(100% - 54px)',
|
||||||
'[text-shadow:_0px_0px_10px_rgba(193,85,64,0.4),_0px_0px_10px_rgba(193,85,64,0.4),_0px_0px_10px_rgba(193,85,64,0.4),_0px_0px_10px_rgba(193,85,64,0.4)]'
|
}}
|
||||||
)}
|
>
|
||||||
style={{
|
<svg
|
||||||
marginTop: verticalAlign === 'center' ? '85px' : '',
|
className="absolute left-0 top-0 h-full w-full"
|
||||||
paddingLeft:
|
viewBox={`0 0 1733.87 ${fillHeight}`}
|
||||||
contentPaddingLeft === undefined ? undefined : toCssSize(contentPaddingLeft),
|
preserveAspectRatio="none"
|
||||||
}}
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{contentChildren}
|
<defs>
|
||||||
</div>
|
<linearGradient
|
||||||
)}
|
id="guide-fill"
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="0"
|
||||||
|
y2={fillHeight}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0%" stopColor="#a98d80" />
|
||||||
|
<stop offset="100%" stopColor="#c5523d" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="url(#guide-fill)" d={fillPath} />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 边框层 */}
|
||||||
|
<div className="absolute inset-0 z-10 pointer-events-none">
|
||||||
|
<img
|
||||||
|
src={getStaticImage('components/guide/frame-top')}
|
||||||
|
alt=""
|
||||||
|
className="absolute left-0 top-0 w-full h-[135px]"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={getStaticImage('components/guide/frame-middle')}
|
||||||
|
alt=""
|
||||||
|
className="absolute left-[32px] top-[134px]"
|
||||||
|
style={{
|
||||||
|
width: 'calc(100% - 68px)',
|
||||||
|
height: 'calc(100% - 268px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={getStaticImage('components/guide/frame-bottom')}
|
||||||
|
alt=""
|
||||||
|
className="absolute left-0 bottom-0 w-full h-[135px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文本内容 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative z-20 w-[1520px] flex items-center justify-center mx-auto text-white px-0 py-[130px]',
|
||||||
|
ailgn === 'left' && 'justify-start',
|
||||||
|
ailgn === 'right' && 'justify-end'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-4xl leading-relaxed whitespace-pre-line overflow-visible custom-scrollbar',
|
||||||
|
'[text-shadow:_0px_0px_10px_rgba(193,85,64,0.4),_0px_0px_10px_rgba(193,85,64,0.4),_0px_0px_10px_rgba(193,85,64,0.4),_0px_0px_10px_rgba(193,85,64,0.4)]'
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
paddingLeft:
|
||||||
|
contentPaddingLeft === undefined ? undefined : toCssSize(contentPaddingLeft),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contentChildren}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 继续按钮 */}
|
{/* 继续按钮 */}
|
||||||
<div className="absolute right-15 bottom-8 z-20 pointer-events-auto">
|
<div className="text-right mr-16">
|
||||||
<Image
|
<Image
|
||||||
src={getStaticImage('components/guide/继续')}
|
src={getStaticImage('components/guide/继续')}
|
||||||
alt="继续"
|
alt="继续"
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
|
||||||
|
interface SyncErrorPromptOptions {
|
||||||
|
title?: string
|
||||||
|
message: string
|
||||||
|
onConfirm?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYNC_ERROR_PROMPT_EVENT = 'sync-error-prompt'
|
||||||
|
const shouldShowSyncPrompt = import.meta.env.MODE === 'production'
|
||||||
|
|
||||||
|
const dispatchSyncPrompt = (options: SyncErrorPromptOptions) => {
|
||||||
|
if (!shouldShowSyncPrompt) return
|
||||||
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent<SyncErrorPromptOptions>(SYNC_ERROR_PROMPT_EVENT, { detail: options })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showSyncErrorPrompt = (options: SyncErrorPromptOptions) => {
|
||||||
|
dispatchSyncPrompt(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showYibanUnauthenticatedPrompt = () => {
|
||||||
|
showSyncErrorPrompt({ message: '易班登录状态异常,请先登录易班平台后继续学习。' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showYibanSyncErrorPrompt = () => {
|
||||||
|
showSyncErrorPrompt({ message: '学习进度获取失败,请稍后刷新重试。' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyncPromptHost = () => {
|
||||||
|
const [prompt, setPrompt] = useState<SyncErrorPromptOptions | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePrompt = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<SyncErrorPromptOptions>
|
||||||
|
setPrompt(customEvent.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(SYNC_ERROR_PROMPT_EVENT, handlePrompt)
|
||||||
|
return () => window.removeEventListener(SYNC_ERROR_PROMPT_EVENT, handlePrompt)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!prompt) return
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
const onConfirm = prompt.onConfirm
|
||||||
|
setPrompt(null)
|
||||||
|
onConfirm?.()
|
||||||
|
}, 4500)
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
|
}, [prompt])
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
const onConfirm = prompt?.onConfirm
|
||||||
|
setPrompt(null)
|
||||||
|
onConfirm?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none fixed inset-0 z-[10000]">
|
||||||
|
<AnimatePresence>
|
||||||
|
{prompt && (
|
||||||
|
<motion.div
|
||||||
|
key={prompt.message}
|
||||||
|
className="pointer-events-auto absolute bottom-12 right-12 w-[520px]"
|
||||||
|
initial={{ opacity: 0, x: 40, y: 12, filter: 'blur(2px)' }}
|
||||||
|
animate={{ opacity: 1, x: 0, y: 0, filter: 'blur(0px)' }}
|
||||||
|
exit={{ opacity: 0, x: 32, y: 8, filter: 'blur(2px)' }}
|
||||||
|
transition={{ duration: 0.24, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden rounded-[18px] border-[3px] border-[#c44535] bg-[#f3e8c9]/95 px-8 py-6 shadow-[0_10px_30px_rgba(114,48,35,0.28)]">
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 h-full w-3 bg-[#c44535]" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-4 top-3 text-3xl leading-none text-[#9b2f25] transition-opacity hover:opacity-70"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="关闭提示"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<div className="pr-8">
|
||||||
|
<div className="mb-2 text-3xl font-bold tracking-[4px] text-[#b8322b]">
|
||||||
|
{prompt.title ?? '温馨提示'}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-semibold leading-snug tracking-wide text-[#6d4a24]">
|
||||||
|
{prompt.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
SYNC_LOADING_EVENT,
|
||||||
|
SYNC_LOADING_HIDE_EVENT,
|
||||||
|
type SyncLoadingOptions,
|
||||||
|
} from './syncLoadingApi'
|
||||||
|
|
||||||
|
export const SyncLoadingHost = () => {
|
||||||
|
const [text, setText] = useState('正在同步学习进度...')
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleShow = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent<SyncLoadingOptions>
|
||||||
|
setText(customEvent.detail.text ?? '正在同步学习进度...')
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHide = () => {
|
||||||
|
setVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(SYNC_LOADING_EVENT, handleShow)
|
||||||
|
window.addEventListener(SYNC_LOADING_HIDE_EVENT, handleHide)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(SYNC_LOADING_EVENT, handleShow)
|
||||||
|
window.removeEventListener(SYNC_LOADING_HIDE_EVENT, handleHide)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{visible && (
|
||||||
|
<motion.div
|
||||||
|
key="sync-loading"
|
||||||
|
className="fixed inset-0 z-10000 flex items-center justify-center bg-black/60"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center justify-center gap-8"
|
||||||
|
initial={{ opacity: 0, y: 12, scale: 0.96 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 8, scale: 0.98 }}
|
||||||
|
transition={{ duration: 0.24, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<svg className="h-20 w-20 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="#f4e4bd"
|
||||||
|
strokeWidth="3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-90"
|
||||||
|
fill="#f4e4bd"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="text-3xl font-semibold tracking-[6px] text-[#f4e4bd] drop-shadow-[0_2px_8px_rgba(0,0,0,0.7)]">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,2 +1,11 @@
|
|||||||
export { GuideSteps, GuideStepsItem } from './GuideSteps'
|
export { GuideSteps, GuideStepsItem } from './GuideSteps'
|
||||||
export { StudyGuideModal } from './StudyGuideModal'
|
export { StudyGuideModal } from './StudyGuideModal'
|
||||||
|
export { SyncLoadingHost } from './SyncLoadingOverlay'
|
||||||
|
export { showSyncLoading, hideSyncLoading } from './syncLoadingApi'
|
||||||
|
export type { SyncLoadingOptions } from './syncLoadingApi'
|
||||||
|
export {
|
||||||
|
SyncPromptHost,
|
||||||
|
showSyncErrorPrompt,
|
||||||
|
showYibanUnauthenticatedPrompt,
|
||||||
|
showYibanSyncErrorPrompt,
|
||||||
|
} from './SyncErrorPrompt'
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export interface SyncLoadingOptions {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SYNC_LOADING_EVENT = 'sync-loading'
|
||||||
|
export const SYNC_LOADING_HIDE_EVENT = 'sync-loading-hide'
|
||||||
|
|
||||||
|
const dispatchSyncLoading = (detail: SyncLoadingOptions) => {
|
||||||
|
window.dispatchEvent(new CustomEvent<SyncLoadingOptions>(SYNC_LOADING_EVENT, { detail }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const showSyncLoading = (options?: SyncLoadingOptions) => {
|
||||||
|
dispatchSyncLoading(options ?? {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hideSyncLoading = () => {
|
||||||
|
window.dispatchEvent(new Event(SYNC_LOADING_HIDE_EVENT))
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
import { cn, getBackgroundImage } from '@/lib/utils'
|
import { cn, getBackgroundImage } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export interface ShowHintOptions {
|
|||||||
persistent?: boolean
|
persistent?: boolean
|
||||||
/** 相对中心点微调位置 */
|
/** 相对中心点微调位置 */
|
||||||
offset?: HintPositionOffset
|
offset?: HintPositionOffset
|
||||||
/** 自定义层级(默认 60) */
|
/** 自定义层级(默认 9999) */
|
||||||
zIndex?: number
|
zIndex?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ const DEFAULT_OPTIONS: HintState['options'] = {
|
|||||||
durationMs: 2000,
|
durationMs: 2000,
|
||||||
persistent: false,
|
persistent: false,
|
||||||
offset: { x: 0, y: 0 },
|
offset: { x: 0, y: 0 },
|
||||||
zIndex: 60,
|
zIndex: 9999,
|
||||||
}
|
}
|
||||||
|
|
||||||
let current: HintState = {
|
let current: HintState = {
|
||||||
|
|||||||
@@ -14,7 +14,18 @@ export type { ImageProps } from './image'
|
|||||||
export { Title } from './title'
|
export { Title } from './title'
|
||||||
|
|
||||||
// Guide components
|
// Guide components
|
||||||
export { GuideSteps, GuideStepsItem, StudyGuideModal } from './guide'
|
export {
|
||||||
|
GuideSteps,
|
||||||
|
GuideStepsItem,
|
||||||
|
StudyGuideModal,
|
||||||
|
SyncLoadingHost,
|
||||||
|
SyncPromptHost,
|
||||||
|
showSyncLoading,
|
||||||
|
hideSyncLoading,
|
||||||
|
showSyncErrorPrompt,
|
||||||
|
showYibanUnauthenticatedPrompt,
|
||||||
|
showYibanSyncErrorPrompt,
|
||||||
|
} from './guide'
|
||||||
|
|
||||||
// Exhibit overlay
|
// Exhibit overlay
|
||||||
export { ExhibitOverlay } from './exhibit'
|
export { ExhibitOverlay } from './exhibit'
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export interface AlertProps {
|
|||||||
const Alert = ({ title, subtitle, visible = true, onMaskClick, onClose }: AlertProps) => {
|
const Alert = ({ title, subtitle, visible = true, onMaskClick, onClose }: AlertProps) => {
|
||||||
if (!visible) return null
|
if (!visible) return null
|
||||||
|
|
||||||
|
const titleMatch = title.match(/^(第[一二三四五六七八九十]+幕)[::]["“]?(.+?)["”]?(?:--|——)(.+)$/)
|
||||||
|
const titlePrefix = titleMatch ? `${titleMatch[1]} ${titleMatch[2]}` : undefined
|
||||||
|
const titleContent = titleMatch?.[3] ?? title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center">
|
<div className="absolute inset-0 z-50 flex items-center justify-center">
|
||||||
{/* 蒙版背景 */}
|
{/* 蒙版背景 */}
|
||||||
@@ -39,12 +43,22 @@ const Alert = ({ title, subtitle, visible = true, onMaskClick, onClose }: AlertP
|
|||||||
{/* 标题 */}
|
{/* 标题 */}
|
||||||
<div
|
<div
|
||||||
className={cn([
|
className={cn([
|
||||||
'absolute -top-10',
|
'absolute -top-40 flex flex-col items-center gap-3',
|
||||||
'text-white text-7xl font-bold',
|
'text-white font-bold',
|
||||||
'[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]',
|
'[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]',
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{title}
|
{titlePrefix && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-[4.8rem] leading-none mb-8'
|
||||||
|
// '[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{titlePrefix}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-6xl leading-none">{titleContent}</span>
|
||||||
</div>
|
</div>
|
||||||
{/* 副标题 */}
|
{/* 副标题 */}
|
||||||
<div className="text-white text-5xl font-medium tracking-widest">{subtitle}</div>
|
<div className="text-white text-5xl font-medium tracking-widest">{subtitle}</div>
|
||||||
|
|||||||
@@ -257,8 +257,8 @@ const MatchQuizPanel = ({
|
|||||||
leftItemRefs.current[item.id] = el
|
leftItemRefs.current[item.id] = el
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200',
|
'relative px-6 py-4 rounded-xl border-2 transition-all duration-200',
|
||||||
'text-[20px] md:text-[28px] leading-relaxed cursor-pointer',
|
'text-[28px] leading-relaxed cursor-pointer',
|
||||||
'touch-manipulation',
|
'touch-manipulation',
|
||||||
disabled || isMatched ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
|
disabled || isMatched ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer',
|
||||||
isError
|
isError
|
||||||
@@ -283,9 +283,9 @@ const MatchQuizPanel = ({
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
leftDotRefs.current[item.id] = el
|
leftDotRefs.current[item.id] = el
|
||||||
}}
|
}}
|
||||||
className="absolute -right-2 md:-right-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-red-400 rounded-full flex items-center justify-center shadow-[0_0_10px_rgba(248,113,113,0.8)]"
|
className="absolute -right-4 top-1/2 -translate-y-1/2 w-6 h-6 bg-red-400 rounded-full flex items-center justify-center shadow-[0_0_10px_rgba(248,113,113,0.8)]"
|
||||||
>
|
>
|
||||||
<span className="text-white text-[10px] md:text-xs">✕</span>
|
<span className="text-white text-xs">✕</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isError && isMatched && (
|
{!isError && isMatched && (
|
||||||
@@ -293,9 +293,9 @@ const MatchQuizPanel = ({
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
leftDotRefs.current[item.id] = el
|
leftDotRefs.current[item.id] = el
|
||||||
}}
|
}}
|
||||||
className="absolute -right-2 md:-right-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-green-500 rounded-full flex items-center justify-center"
|
className="absolute -right-4 top-1/2 -translate-y-1/2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span className="text-white text-xs md:text-sm">✓</span>
|
<span className="text-white text-sm">✓</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -313,8 +313,8 @@ const MatchQuizPanel = ({
|
|||||||
rightItemRefs.current[item.id] = el
|
rightItemRefs.current[item.id] = el
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative px-4 md:px-6 py-3 md:py-4 rounded-xl border-2 transition-all duration-200',
|
'relative px-6 py-4 rounded-xl border-2 transition-all duration-200',
|
||||||
'text-[20px] md:text-[28px] leading-relaxed',
|
'text-[28px] leading-relaxed',
|
||||||
'touch-manipulation',
|
'touch-manipulation',
|
||||||
disabled || isMatched
|
disabled || isMatched
|
||||||
? 'opacity-60 cursor-not-allowed'
|
? 'opacity-60 cursor-not-allowed'
|
||||||
@@ -345,9 +345,9 @@ const MatchQuizPanel = ({
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
rightDotRefs.current[item.id] = el
|
rightDotRefs.current[item.id] = el
|
||||||
}}
|
}}
|
||||||
className="absolute -left-2 md:-left-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-red-400 rounded-full flex items-center justify-center shadow-[0_0_10px_rgba(248,113,113,0.8)]"
|
className="absolute -left-4 top-1/2 -translate-y-1/2 w-6 h-6 bg-red-400 rounded-full flex items-center justify-center shadow-[0_0_10px_rgba(248,113,113,0.8)]"
|
||||||
>
|
>
|
||||||
<span className="text-white text-[10px] md:text-xs">✕</span>
|
<span className="text-white text-xs">✕</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isError && isMatched && (
|
{!isError && isMatched && (
|
||||||
@@ -355,9 +355,9 @@ const MatchQuizPanel = ({
|
|||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
rightDotRefs.current[item.id] = el
|
rightDotRefs.current[item.id] = el
|
||||||
}}
|
}}
|
||||||
className="absolute -left-2 md:-left-4 top-1/2 -translate-y-1/2 w-4 h-4 md:w-6 md:h-6 bg-green-500 rounded-full flex items-center justify-center"
|
className="absolute -left-4 top-1/2 -translate-y-1/2 w-6 h-6 bg-green-500 rounded-full flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<span className="text-white text-xs md:text-sm">✓</span>
|
<span className="text-white text-sm">✓</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -372,25 +372,14 @@ const MatchQuizPanel = ({
|
|||||||
style={{ zIndex }}
|
style={{ zIndex }}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<div className="relative w-full max-w-[1728px] h-full max-h-[972px] flex flex-col items-center justify-center p-4 md:p-8">
|
<div className="relative w-full max-w-[1728px] h-full max-h-[972px] flex flex-col items-center justify-center p-8">
|
||||||
<div className="w-full max-w-[1700px] rounded-[32px] bg-black/90 px-6 md:px-20 py-6 md:py-8 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9] mb-4 md:mb-8">
|
<div className="w-full max-w-[1700px] rounded-[32px] bg-black/90 px-20 py-8 shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9] mb-8">
|
||||||
<div className="text-[24px] md:text-[34px] leading-relaxed text-white">{title}</div>
|
<div className="text-[34px] leading-relaxed text-white">{title}</div>
|
||||||
|
|
||||||
{error && (
|
{error && <div className="mt-5 text-center text-[26px] text-[#fcbe53]">{error}</div>}
|
||||||
<div className="mt-4 md:mt-5 text-center text-[20px] md:text-[26px] text-[#fcbe53]">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端 */}
|
<div ref={quizAreaRef} className="w-full max-w-[1600px] relative">
|
||||||
<div className="w-full max-w-[1600px] md:hidden flex flex-col gap-4 items-start justify-center">
|
|
||||||
<div className="w-full flex flex-col gap-3">{leftItems.map(renderLeftItem)}</div>
|
|
||||||
<div className="w-full flex flex-col gap-3">{rightItems.map(renderRightItem)}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 桌面端 */}
|
|
||||||
<div ref={quizAreaRef} className="hidden md:block w-full max-w-[1600px] relative">
|
|
||||||
{/* SVG 覆盖整个题目区域,用绝对像素坐标画线 */}
|
{/* SVG 覆盖整个题目区域,用绝对像素坐标画线 */}
|
||||||
<svg
|
<svg
|
||||||
className="absolute inset-0 pointer-events-none overflow-visible z-10"
|
className="absolute inset-0 pointer-events-none overflow-visible z-10"
|
||||||
@@ -454,10 +443,6 @@ const MatchQuizPanel = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:hidden mt-4 text-center text-[18px] text-white/80">
|
|
||||||
请先点击左侧日期,再点击右侧对应的事件进行连线
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const LetterTemplate = ({
|
|||||||
const [signatureName, setSignatureName] = useState('')
|
const [signatureName, setSignatureName] = useState('')
|
||||||
const [signatureError, setSignatureError] = useState(false)
|
const [signatureError, setSignatureError] = useState(false)
|
||||||
const [submitted, setSubmitted] = useState(false)
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
const [letterSent, setLetterSent] = useState(false)
|
||||||
const [showSendVideo, setShowSendVideo] = useState(false)
|
const [showSendVideo, setShowSendVideo] = useState(false)
|
||||||
const [showReplyVideo, setShowReplyVideo] = useState(false)
|
const [showReplyVideo, setShowReplyVideo] = useState(false)
|
||||||
const [showScene, setShowScene] = useState(false)
|
const [showScene, setShowScene] = useState(false)
|
||||||
@@ -167,6 +168,7 @@ const LetterTemplate = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
setSubmitted(false)
|
setSubmitted(false)
|
||||||
|
setLetterSent(false)
|
||||||
setBlankValues([])
|
setBlankValues([])
|
||||||
setBlankErrors([])
|
setBlankErrors([])
|
||||||
setSignatureName('')
|
setSignatureName('')
|
||||||
@@ -417,7 +419,7 @@ const LetterTemplate = ({
|
|||||||
<audio ref={audioRef} src={getPublicPath(audioSrc)} loop className="hidden" />
|
<audio ref={audioRef} src={getPublicPath(audioSrc)} loop className="hidden" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showSendVideo && (
|
{!showSendVideo && !letterSent && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed inset-0 bg-black/50 z-100"
|
className="fixed inset-0 bg-black/50 z-100"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@@ -428,7 +430,7 @@ const LetterTemplate = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!showSendVideo && (
|
{!showSendVideo && !letterSent && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed inset-0 z-101 flex items-center justify-center pointer-events-none"
|
className="fixed inset-0 z-101 flex items-center justify-center pointer-events-none"
|
||||||
initial={{ opacity: 0, scale: 0.5, y: 100 }}
|
initial={{ opacity: 0, scale: 0.5, y: 100 }}
|
||||||
@@ -560,6 +562,7 @@ const LetterTemplate = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const nextValues = [...blankValues]
|
const nextValues = [...blankValues]
|
||||||
onSubmit?.(nextValues)
|
onSubmit?.(nextValues)
|
||||||
|
setLetterSent(true)
|
||||||
setShowSendVideo(true)
|
setShowSendVideo(true)
|
||||||
}}
|
}}
|
||||||
button
|
button
|
||||||
@@ -596,13 +599,19 @@ const LetterTemplate = ({
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{showScene && !showReplyImage && replyImageSrc && (
|
{(showScene || showSendVideo) && replyImageSrc && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed inset-0 z-102"
|
className="fixed inset-0 z-102"
|
||||||
initial={{ opacity: 0, y: 50 }}
|
initial={false}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={
|
||||||
exit={{ opacity: 0 }}
|
showScene && !showSendVideo && !showReplyImage
|
||||||
|
? { opacity: 1, y: 0 }
|
||||||
|
: { opacity: 0, y: 50 }
|
||||||
|
}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
|
style={{
|
||||||
|
pointerEvents: showScene && !showSendVideo && !showReplyImage ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={getStaticImage(sceneImageSrc)}
|
src={getStaticImage(sceneImageSrc)}
|
||||||
@@ -636,20 +645,20 @@ const LetterTemplate = ({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showReplyImage && replyImageSrc && (
|
{(showReplyImage || showScene) && replyImageSrc && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="fixed inset-0 z-103 flex items-center justify-center bg-black/80"
|
className="fixed inset-0 z-103 flex items-center justify-center bg-black/80"
|
||||||
initial={{ opacity: 0 }}
|
initial={false}
|
||||||
animate={{ opacity: 1 }}
|
animate={showReplyImage ? { opacity: 1 } : { opacity: 0 }}
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
|
style={{ pointerEvents: showReplyImage ? 'auto' : 'none' }}
|
||||||
>
|
>
|
||||||
<motion.img
|
<motion.img
|
||||||
src={getStaticImage(replyImageSrc)}
|
src={getStaticImage(replyImageSrc)}
|
||||||
alt="回信"
|
alt="回信"
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
initial={{ scale: 0.8, opacity: 0 }}
|
initial={false}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={showReplyImage ? { scale: 1, opacity: 1 } : { scale: 0.8, opacity: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const LetterToHuangyanpei = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onAnimationEnd={onAnimationEnd}
|
onAnimationEnd={onAnimationEnd}
|
||||||
title="写给黄炎培先生"
|
title="写给黄炎培先生的信"
|
||||||
letterContent={letterContent}
|
letterContent={letterContent}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
contentClassName="!text-[25px]"
|
contentClassName="!text-[25px]"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
...commonSelectField,
|
...commonSelectField,
|
||||||
correctValue: '首在立人',
|
correctValue: '首在立人',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '首在立人',
|
readOnlyFallback: '首在立人',
|
||||||
},
|
},
|
||||||
@@ -46,7 +46,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
...commonSelectField,
|
...commonSelectField,
|
||||||
correctValue: '批判求真',
|
correctValue: '批判求真',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '批判求真',
|
readOnlyFallback: '批判求真',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
...commonSelectField,
|
...commonSelectField,
|
||||||
correctValue: '实事求是',
|
correctValue: '实事求是',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '实事求是',
|
readOnlyFallback: '实事求是',
|
||||||
},
|
},
|
||||||
@@ -53,7 +53,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
...commonSelectField,
|
...commonSelectField,
|
||||||
correctValue: '群众路线',
|
correctValue: '群众路线',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '群众路线',
|
readOnlyFallback: '群众路线',
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
...commonSelectField,
|
...commonSelectField,
|
||||||
correctValue: '独立自主',
|
correctValue: '独立自主',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '独立自主',
|
readOnlyFallback: '独立自主',
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
],
|
],
|
||||||
correctValue: '实事求是',
|
correctValue: '实事求是',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '实事求是',
|
readOnlyFallback: '实事求是',
|
||||||
},
|
},
|
||||||
@@ -91,7 +91,7 @@ const fields: Record<number, LetterFieldConfig> = {
|
|||||||
],
|
],
|
||||||
correctValue: '群众路线',
|
correctValue: '群众路线',
|
||||||
hints: {
|
hints: {
|
||||||
error: '请你结合上下文和所学在想想吧!',
|
error: '请你结合上下文和所学再想想吧!',
|
||||||
},
|
},
|
||||||
readOnlyFallback: '群众路线',
|
readOnlyFallback: '群众路线',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -367,12 +367,8 @@ const PuzzleSystem = ({
|
|||||||
'[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]'
|
'[text-shadow:_-4px_-4px_0_#ae6400,_4px_-4px_0_#ae6400,_-4px_4px_0_#ae6400,_4px_4px_0_#ae6400,_-5px_0_0_#ae6400,_5px_0_0_#ae6400,_0_-5px_0_#ae6400,_0_5px_0_#ae6400,_0_0_20px_rgba(255,215,0,1),_0_0_40px_rgba(255,215,0,0.8),_0_0_60px_rgba(255,215,0,0.5)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 空格换行 */}
|
{selectedPieceData.title.split('\n').map((line: string, index: number) => (
|
||||||
{selectedPieceData.title.split(' ').map((word: string, index: number) => (
|
<div key={index}>{line}</div>
|
||||||
<div key={index}>
|
|
||||||
{word}
|
|
||||||
{index < selectedPieceData.title.split(' ').length - 1 && ' '}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,15 +3,6 @@ import { PUZZLE_RESIDENCES } from './puzzleResidences'
|
|||||||
|
|
||||||
export type PuzzleProgress = Record<string, Set<number>>
|
export type PuzzleProgress = Record<string, Set<number>>
|
||||||
|
|
||||||
const BUILD_UNLOCK_ALL = import.meta.env.VITE_PROGRESS_UNLOCK_ALL === 'true'
|
|
||||||
const UNLOCK_ALL_STORAGE_KEY = 'debug:progression-unlock-all'
|
|
||||||
|
|
||||||
const isUnlockAllEnabled = () => {
|
|
||||||
if (!BUILD_UNLOCK_ALL) return false
|
|
||||||
if (typeof window === 'undefined') return true
|
|
||||||
return window.sessionStorage.getItem(UNLOCK_ALL_STORAGE_KEY) !== 'false'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getResidenceIndex = (residence: string) =>
|
const getResidenceIndex = (residence: string) =>
|
||||||
PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence)
|
PUZZLE_RESIDENCES.findIndex((item) => item.residence === residence)
|
||||||
|
|
||||||
@@ -33,13 +24,10 @@ export const isResidenceStarted = (residence: string, puzzles: PuzzleProgress) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isResidenceComplete = (residence: string, puzzles: PuzzleProgress) => {
|
export const isResidenceComplete = (residence: string, puzzles: PuzzleProgress) => {
|
||||||
if (isUnlockAllEnabled()) return true
|
|
||||||
return arePiecesUnlocked(residence, puzzles, getConfiguredPieceIds(residence))
|
return arePiecesUnlocked(residence, puzzles, getConfiguredPieceIds(residence))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isResidenceUnlocked = (residence: string, puzzles: PuzzleProgress) => {
|
export const isResidenceUnlocked = (residence: string, puzzles: PuzzleProgress) => {
|
||||||
if (isUnlockAllEnabled()) return true
|
|
||||||
|
|
||||||
const index = getResidenceIndex(residence)
|
const index = getResidenceIndex(residence)
|
||||||
if (index === 0) return true
|
if (index === 0) return true
|
||||||
if (index < 0) return false
|
if (index < 0) return false
|
||||||
@@ -62,8 +50,6 @@ export const isMenuModuleStarted = (moduleName: string, puzzles: PuzzleProgress)
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isMenuModuleComplete = (moduleName: string, puzzles: PuzzleProgress) => {
|
export const isMenuModuleComplete = (moduleName: string, puzzles: PuzzleProgress) => {
|
||||||
if (isUnlockAllEnabled()) return true
|
|
||||||
|
|
||||||
const residences = PUZZLE_RESIDENCES.filter((item) => item.moduleName === moduleName)
|
const residences = PUZZLE_RESIDENCES.filter((item) => item.moduleName === moduleName)
|
||||||
if (residences.length === 0) return false
|
if (residences.length === 0) return false
|
||||||
return residences.every((item) => isResidenceComplete(item.residence, puzzles))
|
return residences.every((item) => isResidenceComplete(item.residence, puzzles))
|
||||||
@@ -79,14 +65,6 @@ const getPieceIdsByRoute = (residence: string, routePrefix: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getResidenceStageUnlocks = (residence: string, puzzles: PuzzleProgress) => {
|
export const getResidenceStageUnlocks = (residence: string, puzzles: PuzzleProgress) => {
|
||||||
if (isUnlockAllEnabled()) {
|
|
||||||
return {
|
|
||||||
dialogueUnlocked: true,
|
|
||||||
reflectionUnlocked: true,
|
|
||||||
internalizationUnlocked: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const residenceUnlocked = isResidenceUnlocked(residence, puzzles)
|
const residenceUnlocked = isResidenceUnlocked(residence, puzzles)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片2
|
// 碎片2
|
||||||
2: {
|
2: {
|
||||||
title: '陈云就读 青浦县立乙种商业学校',
|
title: '陈云就读\n青浦县立乙种商业学校',
|
||||||
detailImage: 'chenyun/details/2',
|
detailImage: 'chenyun/details/2',
|
||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
@@ -38,7 +38,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片4
|
// 碎片4
|
||||||
4: {
|
4: {
|
||||||
title: '陈云在 上海商务印书馆当学徒',
|
title: '陈云在\n上海商务印书馆当学徒',
|
||||||
detailImage: 'chenyun/details/4',
|
detailImage: 'chenyun/details/4',
|
||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
@@ -50,19 +50,19 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片6
|
// 碎片6
|
||||||
6: {
|
6: {
|
||||||
title: '陈云被选为 中央政治局委员、常委',
|
title: '陈云被选为\n中央政治局委员、常委',
|
||||||
detailImage: 'chenyun/details/6',
|
detailImage: 'chenyun/details/6',
|
||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
// 碎片7
|
// 碎片7
|
||||||
7: {
|
7: {
|
||||||
title: '陈云任 西北财经办事处副主任',
|
title: '陈云任\n西北财经办事处副主任',
|
||||||
detailImage: 'chenyun/details/7',
|
detailImage: 'chenyun/details/7',
|
||||||
route: 'place5',
|
route: 'place5',
|
||||||
},
|
},
|
||||||
// 碎片8
|
// 碎片8
|
||||||
8: {
|
8: {
|
||||||
title: '陈云主持 陕甘宁边区财政工作',
|
title: '陈云主持\n陕甘宁边区财政工作',
|
||||||
detailImage: 'chenyun/details/8',
|
detailImage: 'chenyun/details/8',
|
||||||
route: 'place5',
|
route: 'place5',
|
||||||
},
|
},
|
||||||
@@ -80,7 +80,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片11
|
// 碎片11
|
||||||
11: {
|
11: {
|
||||||
title: '陈云起草的关于发展国民 经济“一五”计划报告的手稿',
|
title: '陈云起草的关于发展\n国民经济“一五”计划\n报告的手稿',
|
||||||
detailImage: 'chenyun/details/11',
|
detailImage: 'chenyun/details/11',
|
||||||
route: 'place8',
|
route: 'place8',
|
||||||
},
|
},
|
||||||
@@ -110,7 +110,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片16
|
// 碎片16
|
||||||
16: {
|
16: {
|
||||||
title: '陈云精神',
|
title: '陈云给青年的精神指引',
|
||||||
detailImage: 'chenyun/details/16',
|
detailImage: 'chenyun/details/16',
|
||||||
route: 'internalization',
|
route: 'internalization',
|
||||||
},
|
},
|
||||||
@@ -148,7 +148,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片6
|
// 碎片6
|
||||||
6: {
|
6: {
|
||||||
title: '职业教育 和中华职业学校',
|
title: '职业教育\n和中华职业学校',
|
||||||
detailImage: 'huangyanpei/details/6',
|
detailImage: 'huangyanpei/details/6',
|
||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
@@ -172,7 +172,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片10
|
// 碎片10
|
||||||
10: {
|
10: {
|
||||||
title: '黄炎培与荣氏兄弟 合作工人教育',
|
title: '黄炎培与荣氏兄弟\n合作工人教育',
|
||||||
detailImage: 'huangyanpei/details/10',
|
detailImage: 'huangyanpei/details/10',
|
||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
@@ -196,7 +196,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
// 碎片14
|
// 碎片14
|
||||||
14: {
|
14: {
|
||||||
title: '黄炎培对浦东中学 新生的教导',
|
title: '黄炎培对浦东中学\n新生的教导',
|
||||||
detailImage: 'huangyanpei/details/14',
|
detailImage: 'huangyanpei/details/14',
|
||||||
route: 'reflection2',
|
route: 'reflection2',
|
||||||
},
|
},
|
||||||
@@ -225,7 +225,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place1',
|
route: 'place1',
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
title: '《申报》刊登孙中山就职 临时大总统消息',
|
title: '《申报》刊登孙中山就职\n临时大总统消息',
|
||||||
detailImage: 'sunzhongshan/details/3',
|
detailImage: 'sunzhongshan/details/3',
|
||||||
route: 'place1',
|
route: 'place1',
|
||||||
},
|
},
|
||||||
@@ -260,7 +260,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
10: {
|
10: {
|
||||||
title: '中国国民党第一次 全国代表大会宣言',
|
title: '中国国民党第一次\n全国代表大会宣言',
|
||||||
detailImage: 'sunzhongshan/details/10',
|
detailImage: 'sunzhongshan/details/10',
|
||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
@@ -285,7 +285,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'reflection3',
|
route: 'reflection3',
|
||||||
},
|
},
|
||||||
15: {
|
15: {
|
||||||
title: '振兴中华对新时代 青年的意义',
|
title: '振兴中华对新时代\n青年的意义',
|
||||||
detailImage: 'sunzhongshan/details/15',
|
detailImage: 'sunzhongshan/details/15',
|
||||||
route: 'internalization',
|
route: 'internalization',
|
||||||
},
|
},
|
||||||
@@ -297,7 +297,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
luxun: {
|
luxun: {
|
||||||
1: {
|
1: {
|
||||||
title: '鲁迅1927年南下 广州时的照片',
|
title: '鲁迅1927年南下\n广州时的照片',
|
||||||
detailImage: 'luxun/details/1',
|
detailImage: 'luxun/details/1',
|
||||||
route: 'place1',
|
route: 'place1',
|
||||||
},
|
},
|
||||||
@@ -414,7 +414,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
8: {
|
8: {
|
||||||
title: '毛泽东签发的两份 中共四大通知',
|
title: '毛泽东签发的两份\n中共四大通知',
|
||||||
detailImage: 'maozedong/details/8',
|
detailImage: 'maozedong/details/8',
|
||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
@@ -526,7 +526,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place6',
|
route: 'place6',
|
||||||
},
|
},
|
||||||
14: {
|
14: {
|
||||||
title: '唱国家一定要站起来',
|
title: '唱国歌一定要站起来',
|
||||||
detailImage: 'nieer/details/14',
|
detailImage: 'nieer/details/14',
|
||||||
route: 'reflection',
|
route: 'reflection',
|
||||||
},
|
},
|
||||||
@@ -543,7 +543,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
},
|
},
|
||||||
zhangleping: {
|
zhangleping: {
|
||||||
1: {
|
1: {
|
||||||
title: '童年的回忆 (张乐平,1983)',
|
title: '童年的回忆\n(张乐平,1983)',
|
||||||
detailImage: 'zhangleping/details/1',
|
detailImage: 'zhangleping/details/1',
|
||||||
route: 'place1',
|
route: 'place1',
|
||||||
},
|
},
|
||||||
@@ -553,17 +553,17 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place1',
|
route: 'place1',
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
title: '穷凶极恶的结果 (张乐平,1937)',
|
title: '穷凶极恶的结果\n(张乐平,1937)',
|
||||||
detailImage: 'zhangleping/details/3',
|
detailImage: 'zhangleping/details/3',
|
||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
title: '长沙大火前夕 张乐平在街头墙上挥笔作画',
|
title: '长沙大火前夕\n张乐平在街头墙上挥笔作画',
|
||||||
detailImage: 'zhangleping/details/4',
|
detailImage: 'zhangleping/details/4',
|
||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
5: {
|
5: {
|
||||||
title: '侵略者的坟墓 (张乐平,1938)',
|
title: '侵略者的坟墓\n(张乐平,1938)',
|
||||||
detailImage: 'zhangleping/details/5',
|
detailImage: 'zhangleping/details/5',
|
||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
@@ -645,7 +645,7 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
5: {
|
5: {
|
||||||
title: '上海申新第九纺织厂门楼(1933年旧照)',
|
title: '上海申新第九纺织厂门楼\n(1933年旧照)',
|
||||||
detailImage: 'rongdesheng/details/5',
|
detailImage: 'rongdesheng/details/5',
|
||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
@@ -655,22 +655,22 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
7: {
|
7: {
|
||||||
title: '国际劳工局赞美申新三纺织厂劳工自治区',
|
title: '国际劳工局赞美\n申新三纺织厂劳工自治区',
|
||||||
detailImage: 'rongdesheng/details/7',
|
detailImage: 'rongdesheng/details/7',
|
||||||
route: 'place2',
|
route: 'place2',
|
||||||
},
|
},
|
||||||
8: {
|
8: {
|
||||||
title: '上海申新一厂和八厂于1937年被日本飞机炸毁',
|
title: '上海申新一厂和八厂于\n1937年被日本飞机炸毁',
|
||||||
detailImage: 'rongdesheng/details/8',
|
detailImage: 'rongdesheng/details/8',
|
||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
9: {
|
9: {
|
||||||
title: '《人报》报道 乐农氏再度捐助面粉一万包',
|
title: '《人报》报道\n乐农氏再度捐助面粉一万包',
|
||||||
detailImage: 'rongdesheng/details/9',
|
detailImage: 'rongdesheng/details/9',
|
||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
10: {
|
10: {
|
||||||
title: '心正思无邪 意诚言必中题字',
|
title: '心正思无邪\n意诚言必中 题字',
|
||||||
detailImage: 'rongdesheng/details/10',
|
detailImage: 'rongdesheng/details/10',
|
||||||
route: 'place3',
|
route: 'place3',
|
||||||
},
|
},
|
||||||
@@ -680,19 +680,19 @@ export const puzzleDataConfig: Record<string, Record<number, PuzzlePieceData>> =
|
|||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
12: {
|
12: {
|
||||||
title: '荣德生拒绝荣氏工业结构外迁的报道',
|
title: '荣德生拒绝荣氏工业结构\n外迁的报道',
|
||||||
detailImage: 'rongdesheng/details/12',
|
detailImage: 'rongdesheng/details/12',
|
||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
13: {
|
13: {
|
||||||
title: '毛泽东主席、陈毅副总理在荣毅仁陪同下,参观公私合营上海申新第九纺织厂',
|
title: '毛泽东主席、陈毅副总理在\n荣毅仁陪同下,参观公私合\n营上海申新第九纺织厂',
|
||||||
detailImage: 'rongdesheng/details/13',
|
detailImage: 'rongdesheng/details/13',
|
||||||
route: 'place4',
|
route: 'place4',
|
||||||
},
|
},
|
||||||
14: {
|
14: {
|
||||||
title: '荣德生与张 一九四九年九月,荣德生致函第一届全国政协謇的合影',
|
title: '民生坚守',
|
||||||
detailImage: 'rongdesheng/details/14',
|
detailImage: 'rongdesheng/details/14',
|
||||||
route: 'place4',
|
route: 'reflection',
|
||||||
},
|
},
|
||||||
15: {
|
15: {
|
||||||
title: '坚韧不拔',
|
title: '坚韧不拔',
|
||||||
|
|||||||
@@ -1,42 +1,238 @@
|
|||||||
const STORAGE_KEY = 'debug:progression-unlock-all'
|
import { puzzleDataConfig } from '@/config/puzzleData'
|
||||||
const BADGE_ID = 'progression-debug-badge'
|
import { PUZZLE_RESIDENCES } from '@/config/puzzleResidences'
|
||||||
|
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||||
|
|
||||||
const isUnlockAllEnabled = () => {
|
const MODE_KEY = 'debug:progression-mode'
|
||||||
return window.sessionStorage.getItem(STORAGE_KEY) !== 'false'
|
const BACKUP_KEY = 'debug:progression-real-puzzles-backup'
|
||||||
|
const BADGE_ID = 'progression-debug-badge'
|
||||||
|
const PANEL_ID = 'progression-debug-panel'
|
||||||
|
|
||||||
|
type DebugMode =
|
||||||
|
| { type: 'real' }
|
||||||
|
| { type: 'arrived'; residence: string; label: string }
|
||||||
|
| { type: 'allCompleted' }
|
||||||
|
|
||||||
|
const serializePuzzles = (puzzles: Record<string, Set<number>>) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(puzzles).map(([residence, pieces]) => [residence, [...pieces]])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBadgeText = () => {
|
const deserializePuzzles = (puzzles: Record<string, number[]> | undefined) => {
|
||||||
return isUnlockAllEnabled() ? '测试包:全解锁' : '测试包:真实进度'
|
if (!puzzles) return {}
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(puzzles).map(([residence, pieces]) => [residence, new Set(pieces)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllPieceIds = (residence: string) => {
|
||||||
|
return Object.keys(puzzleDataConfig[residence] ?? {}).map(Number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildArrivedPuzzles = (targetResidence: string) => {
|
||||||
|
const targetIndex = PUZZLE_RESIDENCES.findIndex((item) => item.residence === targetResidence)
|
||||||
|
const puzzles: Record<string, Set<number>> = {}
|
||||||
|
|
||||||
|
if (targetIndex <= 0) return puzzles
|
||||||
|
|
||||||
|
for (let index = 0; index < targetIndex; index++) {
|
||||||
|
const residence = PUZZLE_RESIDENCES[index]?.residence
|
||||||
|
if (!residence) continue
|
||||||
|
puzzles[residence] = new Set(getAllPieceIds(residence))
|
||||||
|
}
|
||||||
|
|
||||||
|
return puzzles
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildAllCompletedPuzzles = () => {
|
||||||
|
const puzzles: Record<string, Set<number>> = {}
|
||||||
|
|
||||||
|
for (const { residence } of PUZZLE_RESIDENCES) {
|
||||||
|
puzzles[residence] = new Set(getAllPieceIds(residence))
|
||||||
|
}
|
||||||
|
|
||||||
|
return puzzles
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStoredMode = (): DebugMode => {
|
||||||
|
const raw = window.sessionStorage.getItem(MODE_KEY)
|
||||||
|
if (!raw) return { type: 'real' }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const mode = JSON.parse(raw) as DebugMode
|
||||||
|
if (mode.type === 'arrived' || mode.type === 'allCompleted') return mode
|
||||||
|
} catch {
|
||||||
|
return { type: 'real' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'real' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStoredMode = (mode: DebugMode) => {
|
||||||
|
if (mode.type === 'real') {
|
||||||
|
window.sessionStorage.removeItem(MODE_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.sessionStorage.setItem(MODE_KEY, JSON.stringify(mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModeLabel = (mode = getStoredMode()) => {
|
||||||
|
if (mode.type === 'arrived') return `到达:${mode.label}`
|
||||||
|
if (mode.type === 'allCompleted') return '全流程完成'
|
||||||
|
return '真实进度'
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupRealPuzzles = () => {
|
||||||
|
if (window.sessionStorage.getItem(BACKUP_KEY)) return
|
||||||
|
|
||||||
|
const puzzles = usePuzzleStore.getState().puzzles
|
||||||
|
window.sessionStorage.setItem(BACKUP_KEY, JSON.stringify(serializePuzzles(puzzles)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreRealPuzzles = () => {
|
||||||
|
const raw = window.sessionStorage.getItem(BACKUP_KEY)
|
||||||
|
if (!raw) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw) as Record<string, number[]>
|
||||||
|
usePuzzleStore.setState({ puzzles: deserializePuzzles(data) })
|
||||||
|
} catch {
|
||||||
|
window.sessionStorage.removeItem(BACKUP_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyDebugModePuzzles = (mode: Exclude<DebugMode, { type: 'real' }>) => {
|
||||||
|
const puzzles =
|
||||||
|
mode.type === 'allCompleted' ? buildAllCompletedPuzzles() : buildArrivedPuzzles(mode.residence)
|
||||||
|
|
||||||
|
usePuzzleStore.setState((prev) => ({ ...prev, puzzles }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applyStoredProgressionDebugMode = () => {
|
||||||
|
const mode = getStoredMode()
|
||||||
|
if (mode.type === 'real') return false
|
||||||
|
|
||||||
|
applyDebugModePuzzles(mode)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyMode = (mode: DebugMode) => {
|
||||||
|
if (mode.type === 'real') {
|
||||||
|
restoreRealPuzzles()
|
||||||
|
setStoredMode(mode)
|
||||||
|
window.sessionStorage.removeItem(BACKUP_KEY)
|
||||||
|
window.location.reload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
backupRealPuzzles()
|
||||||
|
applyDebugModePuzzles(mode)
|
||||||
|
setStoredMode(mode)
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
const createButton = (label: string, onClick: () => void) => {
|
||||||
|
const button = document.createElement('button')
|
||||||
|
button.type = 'button'
|
||||||
|
button.textContent = label
|
||||||
|
button.style.width = '100%'
|
||||||
|
button.style.border = '1px solid rgba(255,255,255,0.2)'
|
||||||
|
button.style.borderRadius = '6px'
|
||||||
|
button.style.background = 'rgba(255,255,255,0.1)'
|
||||||
|
button.style.color = '#fff'
|
||||||
|
button.style.padding = '6px 8px'
|
||||||
|
button.style.textAlign = 'left'
|
||||||
|
button.style.cursor = 'pointer'
|
||||||
|
button.addEventListener('click', onClick)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePanel = () => {
|
||||||
|
document.getElementById(PANEL_ID)?.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePanel = () => {
|
||||||
|
const existingPanel = document.getElementById(PANEL_ID)
|
||||||
|
if (existingPanel) {
|
||||||
|
existingPanel.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = document.createElement('div')
|
||||||
|
panel.id = PANEL_ID
|
||||||
|
panel.style.position = 'fixed'
|
||||||
|
panel.style.right = '12px'
|
||||||
|
panel.style.top = '72px'
|
||||||
|
panel.style.zIndex = '99999'
|
||||||
|
panel.style.width = '260px'
|
||||||
|
panel.style.maxHeight = '70vh'
|
||||||
|
panel.style.overflowY = 'auto'
|
||||||
|
panel.style.padding = '10px'
|
||||||
|
panel.style.borderRadius = '10px'
|
||||||
|
panel.style.background = 'rgba(0, 0, 0, 0.84)'
|
||||||
|
panel.style.color = '#fff'
|
||||||
|
panel.style.fontSize = '12px'
|
||||||
|
panel.style.boxShadow = '0 8px 24px rgba(0,0,0,0.35)'
|
||||||
|
|
||||||
|
const title = document.createElement('div')
|
||||||
|
title.textContent = '进度模拟'
|
||||||
|
title.style.fontWeight = 'bold'
|
||||||
|
title.style.marginBottom = '8px'
|
||||||
|
panel.appendChild(title)
|
||||||
|
|
||||||
|
const description = document.createElement('div')
|
||||||
|
description.textContent = '只写本地 puzzleStore,不调用易班保存。'
|
||||||
|
description.style.opacity = '0.75'
|
||||||
|
description.style.marginBottom = '8px'
|
||||||
|
panel.appendChild(description)
|
||||||
|
|
||||||
|
const list = document.createElement('div')
|
||||||
|
list.style.display = 'flex'
|
||||||
|
list.style.flexDirection = 'column'
|
||||||
|
list.style.gap = '6px'
|
||||||
|
|
||||||
|
list.appendChild(createButton('真实进度', () => applyMode({ type: 'real' })))
|
||||||
|
|
||||||
|
for (const residence of PUZZLE_RESIDENCES) {
|
||||||
|
list.appendChild(
|
||||||
|
createButton(`到达:${residence.displayName}`, () =>
|
||||||
|
applyMode({ type: 'arrived', residence: residence.residence, label: residence.displayName })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
list.appendChild(createButton('全流程完成', () => applyMode({ type: 'allCompleted' })))
|
||||||
|
|
||||||
|
panel.appendChild(list)
|
||||||
|
document.body.appendChild(panel)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateBadge = () => {
|
const updateBadge = () => {
|
||||||
let badge = document.getElementById(BADGE_ID)
|
let badge = document.getElementById(BADGE_ID)
|
||||||
if (!badge) {
|
if (!badge) {
|
||||||
badge = document.createElement('div')
|
badge = document.createElement('button')
|
||||||
badge.id = BADGE_ID
|
badge.id = BADGE_ID
|
||||||
badge.style.position = 'fixed'
|
badge.style.position = 'fixed'
|
||||||
badge.style.right = '12px'
|
badge.style.right = '12px'
|
||||||
badge.style.top = '12px'
|
badge.style.top = '12px'
|
||||||
badge.style.zIndex = '99999'
|
badge.style.zIndex = '99999'
|
||||||
badge.style.padding = '8px 10px'
|
badge.style.padding = '8px 10px'
|
||||||
|
badge.style.border = '0'
|
||||||
badge.style.borderRadius = '8px'
|
badge.style.borderRadius = '8px'
|
||||||
badge.style.background = 'rgba(0, 0, 0, 0.72)'
|
badge.style.background = 'rgba(0, 0, 0, 0.72)'
|
||||||
badge.style.color = '#fff'
|
badge.style.color = '#fff'
|
||||||
badge.style.fontSize = '12px'
|
badge.style.fontSize = '12px'
|
||||||
badge.style.lineHeight = '1.5'
|
badge.style.lineHeight = '1.5'
|
||||||
badge.style.pointerEvents = 'none'
|
badge.style.cursor = 'pointer'
|
||||||
badge.style.whiteSpace = 'pre-line'
|
badge.style.whiteSpace = 'pre-line'
|
||||||
|
badge.addEventListener('click', togglePanel)
|
||||||
document.body.appendChild(badge)
|
document.body.appendChild(badge)
|
||||||
}
|
}
|
||||||
|
|
||||||
badge.textContent = `${getBadgeText()}\nCtrl+Shift+U 切换`
|
const mode = getStoredMode()
|
||||||
}
|
const title = mode.type === 'real' ? '测试包:真实进度' : '测试包:模拟进度'
|
||||||
|
badge.textContent = `${title}\n当前:${getModeLabel(mode)}\nCtrl+Shift+U 打开面板`
|
||||||
const toggleUnlockAll = () => {
|
|
||||||
const nextValue = isUnlockAllEnabled() ? 'false' : 'true'
|
|
||||||
window.sessionStorage.setItem(STORAGE_KEY, nextValue)
|
|
||||||
updateBadge()
|
|
||||||
window.location.reload()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setupProgressionDebug = () => {
|
export const setupProgressionDebug = () => {
|
||||||
@@ -46,6 +242,14 @@ export const setupProgressionDebug = () => {
|
|||||||
if (!event.ctrlKey || !event.shiftKey || event.key.toLowerCase() !== 'u') return
|
if (!event.ctrlKey || !event.shiftKey || event.key.toLowerCase() !== 'u') return
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
toggleUnlockAll()
|
togglePanel()
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('click', (event) => {
|
||||||
|
const target = event.target as Node | null
|
||||||
|
if (!target) return
|
||||||
|
if (document.getElementById(PANEL_ID)?.contains(target)) return
|
||||||
|
if (document.getElementById(BADGE_ID)?.contains(target)) return
|
||||||
|
removePanel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useUserStore } from '@/store/userStore'
|
import { useUserStore } from '@/store/userStore'
|
||||||
|
|
||||||
|
//超时时间
|
||||||
|
const TIMEOUT_MS = 1000 * 10
|
||||||
|
|
||||||
// 声明外部依赖(由运行环境提供)
|
// 声明外部依赖(由运行环境提供)
|
||||||
declare const $: any
|
declare const $: any
|
||||||
declare const iscallyb: boolean
|
declare const iscallyb: boolean
|
||||||
@@ -9,11 +12,6 @@ declare function getFutureDa(days: number): string
|
|||||||
declare function getCurrentDa(str: string): void
|
declare function getCurrentDa(str: string): void
|
||||||
declare function formatDateFromMilliseconds(date: Date): string
|
declare function formatDateFromMilliseconds(date: Date): string
|
||||||
|
|
||||||
// 本地或非易班环境下,外部全局变量可能未注入;统一用安全访问
|
|
||||||
const isYibanRuntime = (): boolean => {
|
|
||||||
return typeof iscallyb !== 'undefined' && iscallyb === true
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserDefineParam {
|
interface UserDefineParam {
|
||||||
lessonId: string
|
lessonId: string
|
||||||
key?: string | number
|
key?: string | number
|
||||||
@@ -27,11 +25,21 @@ interface VideoAjaxResponseItem {
|
|||||||
|
|
||||||
interface VideoAjaxResponse {
|
interface VideoAjaxResponse {
|
||||||
code: number
|
code: number
|
||||||
|
msg?: string
|
||||||
data: {
|
data: {
|
||||||
value?: VideoAjaxResponseItem[] | string
|
value?: VideoAjaxResponseItem[] | string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserDefineFetchStatus = 'success' | 'empty' | 'unauthenticated' | 'error'
|
||||||
|
|
||||||
|
export interface UserDefineFetchResult {
|
||||||
|
status: UserDefineFetchStatus
|
||||||
|
content: string
|
||||||
|
code?: number
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface VideoAjax {
|
interface VideoAjax {
|
||||||
setHost(host: string): void
|
setHost(host: string): void
|
||||||
addUserDefine(param: UserDefineParam): void
|
addUserDefine(param: UserDefineParam): void
|
||||||
@@ -77,17 +85,12 @@ $.extend({
|
|||||||
|
|
||||||
const LESSON_PREFIX = 'hsgrl'
|
const LESSON_PREFIX = 'hsgrl'
|
||||||
|
|
||||||
const videoajax: VideoAjax | null = typeof videoAjax !== 'undefined' ? videoAjax() : null
|
const videoajax: VideoAjax = videoAjax()
|
||||||
videoajax?.setHost('/')
|
videoajax.setHost('/')
|
||||||
|
|
||||||
function normalizeLessonId(lessonId: string | number): string {
|
|
||||||
const raw = String(lessonId)
|
|
||||||
return raw.startsWith(`${LESSON_PREFIX}_`) ? raw : `${LESSON_PREFIX}_${raw}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置我的自定义数据(所有的自定义数据都设置在第一个课时ID上,以key来区分不同的类型)
|
// 设置我的自定义数据(所有的自定义数据都设置在第一个课时ID上,以key来区分不同的类型)
|
||||||
function setLtDefine(key: string | number, value: string): void {
|
function setLtDefine(key: string | number, value: string): void {
|
||||||
if (isYibanRuntime()) {
|
if (iscallyb) {
|
||||||
setUserDefine(1, key, value)
|
setUserDefine(1, key, value)
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.setItem(`${LESSON_PREFIX}_${key}`, value)
|
sessionStorage.setItem(`${LESSON_PREFIX}_${key}`, value)
|
||||||
@@ -98,7 +101,7 @@ let mycstr9 = ''
|
|||||||
|
|
||||||
// 获取定义数据(由于底层是异步接口,这里返回 Promise)
|
// 获取定义数据(由于底层是异步接口,这里返回 Promise)
|
||||||
async function getLtDefine(key: string | number): Promise<string> {
|
async function getLtDefine(key: string | number): Promise<string> {
|
||||||
if (isYibanRuntime()) {
|
if (iscallyb) {
|
||||||
if (mycstr9 === '') {
|
if (mycstr9 === '') {
|
||||||
const all = await getUsersDefine(1, emykey)
|
const all = await getUsersDefine(1, emykey)
|
||||||
if (all === undefined || all === null || all === 'null' || all === '') {
|
if (all === undefined || all === null || all === 'null' || all === '') {
|
||||||
@@ -142,53 +145,105 @@ export function setUserDefine(
|
|||||||
key: string | number,
|
key: string | number,
|
||||||
value: string
|
value: string
|
||||||
): void {
|
): void {
|
||||||
if (!isYibanRuntime() || !videoajax) {
|
|
||||||
sessionStorage.setItem(`${normalizeLessonId(lessonId)}_${key}`, value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const param: UserDefineParam = {
|
const param: UserDefineParam = {
|
||||||
lessonId: normalizeLessonId(lessonId),
|
lessonId: `${LESSON_PREFIX}_${lessonId}`,
|
||||||
key,
|
key,
|
||||||
content: value,
|
content: value,
|
||||||
}
|
}
|
||||||
|
console.log(param)
|
||||||
videoajax.addUserDefine(param)
|
videoajax.addUserDefine(param)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内部:将回调形式封装为 Promise
|
// 内部:将回调形式封装为 Promise
|
||||||
function getUserDefineAsyncInternal(
|
function getUserDefineResultAsyncInternal(
|
||||||
lessonId: string | number,
|
lessonId: string | number,
|
||||||
key: string | number
|
key: string | number
|
||||||
): Promise<string> {
|
): Promise<UserDefineFetchResult> {
|
||||||
if (!isYibanRuntime() || !videoajax) {
|
if (!iscallyb) {
|
||||||
const value = sessionStorage.getItem(`${normalizeLessonId(lessonId)}_${key}`)
|
const itemValue = sessionStorage.getItem(`${LESSON_PREFIX}_${lessonId}_${key}`)
|
||||||
return Promise.resolve(value === null || value === 'null' ? '' : value)
|
return Promise.resolve(
|
||||||
|
itemValue === undefined || itemValue === null || itemValue === 'null' || itemValue === ''
|
||||||
|
? { status: 'empty', content: '' }
|
||||||
|
: { status: 'success', content: itemValue }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const param: UserDefineParam = {
|
const param: UserDefineParam = {
|
||||||
lessonId: normalizeLessonId(lessonId),
|
lessonId: `${LESSON_PREFIX}_${lessonId}`,
|
||||||
order: key,
|
order: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ajax = videoajax
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let content = ''
|
let settled = false
|
||||||
ajax.getUserDefineInfoByUser(param, (res) => {
|
const done = (result: UserDefineFetchResult) => {
|
||||||
// 保留原有 console 行为
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.warn(`getUserDefine timeout: lessonId=${lessonId}, key=${key}`)
|
||||||
|
done({ status: 'error', content: '', message: '学习进度获取超时' })
|
||||||
|
}, TIMEOUT_MS)
|
||||||
|
|
||||||
|
videoajax.getUserDefineInfoByUser(param, (res) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
console.log(res)
|
console.log(res)
|
||||||
if (res && res.code === 200) {
|
|
||||||
const json = (res.data.value || []) as VideoAjaxResponseItem[]
|
if (res === undefined || res === null || res === ('null' as unknown)) {
|
||||||
const contents = json[0]?.content || ''
|
done({ status: 'empty', content: '' })
|
||||||
content = contents
|
return
|
||||||
} else {
|
|
||||||
content = ''
|
|
||||||
}
|
}
|
||||||
resolve(content)
|
|
||||||
|
if (res.code === 202) {
|
||||||
|
done({ status: 'unauthenticated', content: '', code: res.code, message: res.msg })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.code !== 200) {
|
||||||
|
done({
|
||||||
|
status: 'error',
|
||||||
|
content: '',
|
||||||
|
code: res.code,
|
||||||
|
message: res.msg || '学习进度获取失败',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = res.data?.value
|
||||||
|
if (value === undefined || value === null || value === 'null' || value === '') {
|
||||||
|
done({ status: 'empty', content: '' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = Array.isArray(value) ? value : []
|
||||||
|
const content = json[0]?.content || ''
|
||||||
|
done(
|
||||||
|
content === '' || content === 'null'
|
||||||
|
? { status: 'empty', content: '' }
|
||||||
|
: { status: 'success', content }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserDefineAsyncInternal(
|
||||||
|
lessonId: string | number,
|
||||||
|
key: string | number
|
||||||
|
): Promise<string> {
|
||||||
|
return getUserDefineResultAsyncInternal(lessonId, key).then((result) =>
|
||||||
|
result.status === 'success' ? result.content : ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Promise 版本:获取用户自定义数据
|
// Promise 版本:获取用户自定义数据
|
||||||
|
export async function getUserDefineResult(
|
||||||
|
lessonId: string | number,
|
||||||
|
key: string | number
|
||||||
|
): Promise<UserDefineFetchResult> {
|
||||||
|
return getUserDefineResultAsyncInternal(lessonId, key)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUserDefine(
|
export async function getUserDefine(
|
||||||
lessonId: string | number,
|
lessonId: string | number,
|
||||||
key: string | number
|
key: string | number
|
||||||
@@ -201,19 +256,27 @@ function getUsersDefineAsyncInternal(
|
|||||||
lessonId: string | number,
|
lessonId: string | number,
|
||||||
key: string | number
|
key: string | number
|
||||||
): Promise<string | VideoAjaxResponseItem[]> {
|
): Promise<string | VideoAjaxResponseItem[]> {
|
||||||
if (!isYibanRuntime() || !videoajax) {
|
|
||||||
return Promise.resolve('')
|
|
||||||
}
|
|
||||||
|
|
||||||
const param: UserDefineParam = {
|
const param: UserDefineParam = {
|
||||||
lessonId: normalizeLessonId(lessonId),
|
lessonId: `${LESSON_PREFIX}_${lessonId}`,
|
||||||
order: key,
|
order: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ajax = videoajax
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let content: string | VideoAjaxResponseItem[] = ''
|
let settled = false
|
||||||
ajax.getUserDefineInfo(param, (data) => {
|
const done = (content: string | VideoAjaxResponseItem[]) => {
|
||||||
|
if (settled) return
|
||||||
|
settled = true
|
||||||
|
resolve(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.warn(`getUsersDefine timeout: lessonId=${lessonId}, key=${key}`)
|
||||||
|
done('')
|
||||||
|
}, TIMEOUT_MS)
|
||||||
|
|
||||||
|
videoajax.getUserDefineInfo(param, (data) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
let content: string | VideoAjaxResponseItem[] = ''
|
||||||
if (data && data.code === 200) {
|
if (data && data.code === 200) {
|
||||||
const json = (data.data.value || []) as VideoAjaxResponseItem[]
|
const json = (data.data.value || []) as VideoAjaxResponseItem[]
|
||||||
if (json.length > 1) {
|
if (json.length > 1) {
|
||||||
@@ -231,10 +294,8 @@ function getUsersDefineAsyncInternal(
|
|||||||
} else {
|
} else {
|
||||||
content = json[0]?.content || ''
|
content = json[0]?.content || ''
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
content = ''
|
|
||||||
}
|
}
|
||||||
resolve(content)
|
done(content)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -253,13 +314,7 @@ export async function setCourseTimeProgress(order: number): Promise<void> {
|
|||||||
// var courseid = getCookie(lesson+"_course_id");//获取url链接的课程ID
|
// var courseid = getCookie(lesson+"_course_id");//获取url链接的课程ID
|
||||||
// const courseid = await getLtDefine('10000')
|
// const courseid = await getLtDefine('10000')
|
||||||
console.info(`courseid:${courseid} order:${order}`)
|
console.info(`courseid:${courseid} order:${order}`)
|
||||||
if (
|
if (iscallyb && courseid !== undefined && courseid !== null && courseid !== '') {
|
||||||
isYibanRuntime() &&
|
|
||||||
videoajax &&
|
|
||||||
courseid !== undefined &&
|
|
||||||
courseid !== null &&
|
|
||||||
courseid !== ''
|
|
||||||
) {
|
|
||||||
if (order > 0) {
|
if (order > 0) {
|
||||||
const param2 = { order, co: courseid, process: 100 }
|
const param2 = { order, co: courseid, process: 100 }
|
||||||
videoajax.finishCourse(param2)
|
videoajax.finishCourse(param2)
|
||||||
|
|||||||
@@ -5,11 +5,20 @@
|
|||||||
|
|
||||||
// 敏感词库(可根据实际需求扩展)
|
// 敏感词库(可根据实际需求扩展)
|
||||||
const SENSITIVE_WORDS = [
|
const SENSITIVE_WORDS = [
|
||||||
'敏感词1',
|
|
||||||
'敏感词2',
|
|
||||||
'测试',
|
|
||||||
'垃圾',
|
'垃圾',
|
||||||
// 可以根据实际需求添加更多敏感词
|
'脏话',
|
||||||
|
'骂人',
|
||||||
|
'暴力',
|
||||||
|
'恐怖',
|
||||||
|
'色情',
|
||||||
|
'赌博',
|
||||||
|
'诈骗',
|
||||||
|
'毒品',
|
||||||
|
'邪教',
|
||||||
|
'反动',
|
||||||
|
'辱骂',
|
||||||
|
'仇恨',
|
||||||
|
'sb',
|
||||||
]
|
]
|
||||||
|
|
||||||
type FileSaveHandle = {
|
type FileSaveHandle = {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { StrictMode } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { useChenyunStore, useHuangyanpeiStore, usePuzzleStore, useSunzhongshanStore } from './store'
|
|
||||||
|
|
||||||
if (import.meta.env.VITE_DEBUG_ENTRY === 'true' && typeof window !== 'undefined') {
|
if (import.meta.env.VITE_DEBUG_ENTRY === 'true' && typeof window !== 'undefined') {
|
||||||
import('@/debug/progressionDebug').then(({ setupProgressionDebug }) => {
|
import('@/debug/progressionDebug').then(({ setupProgressionDebug }) => {
|
||||||
@@ -29,43 +28,6 @@ if (import.meta.env.DEV && typeof window !== 'undefined') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局键盘快捷键:Ctrl+Shift+R 重置所有缓存
|
|
||||||
const setupGlobalKeyboardShortcuts = () => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
document.addEventListener('keydown', (event) => {
|
|
||||||
// Ctrl+Shift+R: 重置所有缓存
|
|
||||||
if (event.ctrlKey && event.shiftKey && event.key === 'R') {
|
|
||||||
event.preventDefault() // 阻止默认的页面刷新行为
|
|
||||||
|
|
||||||
// 显示确认对话框
|
|
||||||
const confirmed = window.confirm(
|
|
||||||
'确定要重置所有缓存吗?这将清除所有已收集的拼图碎片、已完成的展品和线索。'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
try {
|
|
||||||
// 重置所有store
|
|
||||||
useChenyunStore.getState().resetAll()
|
|
||||||
useHuangyanpeiStore.getState().resetAll()
|
|
||||||
usePuzzleStore.getState().resetAll()
|
|
||||||
useSunzhongshanStore.getState().resetAll()
|
|
||||||
|
|
||||||
// 显示成功消息
|
|
||||||
alert('所有缓存已重置!页面将刷新。')
|
|
||||||
window.location.reload()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('重置缓存时出错:', error)
|
|
||||||
alert('重置缓存失败,请查看控制台错误信息。')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置全局键盘快捷键
|
|
||||||
setupGlobalKeyboardShortcuts()
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('root')
|
const rootElement = document.getElementById('root')
|
||||||
if (!rootElement) {
|
if (!rootElement) {
|
||||||
throw new Error('Root element not found')
|
throw new Error('Root element not found')
|
||||||
|
|||||||
@@ -1,14 +1,37 @@
|
|||||||
import { getBackgroundImage, getStaticImage } from '@/lib/utils'
|
import { useState } from 'react'
|
||||||
import { Image } from '@/components'
|
import { AnimatePresence, motion } from 'framer-motion'
|
||||||
|
import { getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
|
||||||
|
import { Image, Video } from '@/components'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const [showIntroVideo, setShowIntroVideo] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||||
style={{ backgroundImage: getBackgroundImage('home/首页底图') }}
|
style={{ backgroundImage: getBackgroundImage('home/首页底图') }}
|
||||||
>
|
>
|
||||||
|
{/* 课程简介 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{!showIntroVideo && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute right-0 top-0"
|
||||||
|
initial={{ opacity: 0, scale: 0.92, x: 24 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.92, x: 24 }}
|
||||||
|
transition={{ duration: 0.22, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={getStaticImage('home/课程简介')}
|
||||||
|
button
|
||||||
|
onClick={() => setShowIntroVideo(true)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/menu')
|
navigate('/menu')
|
||||||
@@ -17,6 +40,46 @@ const Home = () => {
|
|||||||
button
|
button
|
||||||
src={getStaticImage('home/首页-进入')}
|
src={getStaticImage('home/首页-进入')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showIntroVideo && (
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 z-50 flex items-center justify-center bg-black/65"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.28, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="relative w-full h-full overflow-hidden bg-black"
|
||||||
|
initial={{ opacity: 0, scale: 1.04 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 1.02 }}
|
||||||
|
transition={{ duration: 0.32, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||||
|
>
|
||||||
|
<Video
|
||||||
|
className="w-full h-full"
|
||||||
|
src={getPublicPath('video/intro.mp4')}
|
||||||
|
autoPlay
|
||||||
|
controls
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute right-0 top-0 z-10"
|
||||||
|
initial={{ opacity: 0, scale: 0.92, x: 24 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.92, x: 24 }}
|
||||||
|
transition={{ duration: 0.22, ease: 'easeOut', delay: 0.08 }}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={getStaticImage('home/点击关闭')}
|
||||||
|
button
|
||||||
|
onClick={() => setShowIntroVideo(false)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useNavigate, useParams } from 'react-router-dom'
|
|||||||
import { Image } from '@/components'
|
import { Image } from '@/components'
|
||||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||||
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
|
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
|
||||||
import { getUserDefine } from '@/lib/utils/api-common'
|
import { getUserDefineResult } from '@/lib/utils/api-common'
|
||||||
|
import { showYibanSyncErrorPrompt, showYibanUnauthenticatedPrompt } from '@/components'
|
||||||
import { useChenyunStore, CHENYUN_LESSON_ID, CHENYUN_KEY } from '@/store/chenyunStore'
|
import { useChenyunStore, CHENYUN_LESSON_ID, CHENYUN_KEY } from '@/store/chenyunStore'
|
||||||
|
|
||||||
const deserializeVisitedExhibits = (
|
const deserializeVisitedExhibits = (
|
||||||
@@ -43,10 +44,26 @@ const Chenyun = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const raw = await getUserDefine(CHENYUN_LESSON_ID, CHENYUN_KEY)
|
const result = await getUserDefineResult(CHENYUN_LESSON_ID, CHENYUN_KEY)
|
||||||
if (!raw) return
|
|
||||||
|
|
||||||
const data = JSON.parse(raw) as {
|
if (result.status !== 'success') {
|
||||||
|
useChenyunStore.setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
visitedExhibits: {},
|
||||||
|
isYinyuanSolved: false,
|
||||||
|
isMimianSolved: false,
|
||||||
|
lastVisitedPlace: null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (result.status === 'unauthenticated') {
|
||||||
|
showYibanUnauthenticatedPrompt()
|
||||||
|
} else if (result.status === 'error') {
|
||||||
|
showYibanSyncErrorPrompt()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(result.content) as {
|
||||||
visitedExhibits?: Record<string, number[]>
|
visitedExhibits?: Record<string, number[]>
|
||||||
isYinyuanSolved?: boolean
|
isYinyuanSolved?: boolean
|
||||||
isMimianSolved?: boolean
|
isMimianSolved?: boolean
|
||||||
@@ -56,14 +73,19 @@ const Chenyun = () => {
|
|||||||
useChenyunStore.setState((prev) => ({
|
useChenyunStore.setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
visitedExhibits: deserializeVisitedExhibits(data.visitedExhibits),
|
visitedExhibits: deserializeVisitedExhibits(data.visitedExhibits),
|
||||||
isYinyuanSolved:
|
isYinyuanSolved: data.isYinyuanSolved ?? false,
|
||||||
data.isYinyuanSolved !== undefined ? data.isYinyuanSolved : prev.isYinyuanSolved,
|
isMimianSolved: data.isMimianSolved ?? false,
|
||||||
isMimianSolved:
|
lastVisitedPlace: data.lastVisitedPlace ?? null,
|
||||||
data.isMimianSolved !== undefined ? data.isMimianSolved : prev.isMimianSolved,
|
|
||||||
lastVisitedPlace:
|
|
||||||
data.lastVisitedPlace !== undefined ? data.lastVisitedPlace : prev.lastVisitedPlace,
|
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
useChenyunStore.setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
visitedExhibits: {},
|
||||||
|
isYinyuanSolved: false,
|
||||||
|
isMimianSolved: false,
|
||||||
|
lastVisitedPlace: null,
|
||||||
|
}))
|
||||||
|
showYibanSyncErrorPrompt()
|
||||||
console.error('Failed to load Chenyun store', error)
|
console.error('Failed to load Chenyun store', error)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quiz-3',
|
id: 'quiz-3',
|
||||||
time: 16 * 60 + 31, //16:26
|
time: 15 * 60 + 56, //15:56
|
||||||
title: '题目3',
|
title: '题目3',
|
||||||
prompt: '新时代的青年可以从陈云身上学习到',
|
prompt: '新时代的青年可以从陈云身上学习到',
|
||||||
type: 'drag',
|
type: 'drag',
|
||||||
@@ -117,7 +117,7 @@ const QUIZ_ANCHORS: QuizAnchor[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quiz-5',
|
id: 'quiz-5',
|
||||||
time: 60 * 16 + 23, // 16:23
|
time: 60 * 15 + 48, // 15:48
|
||||||
title: '题目5',
|
title: '题目5',
|
||||||
prompt: '以下哪些陈云的生活小事值得我们学习?',
|
prompt: '以下哪些陈云的生活小事值得我们学习?',
|
||||||
type: 'multiple',
|
type: 'multiple',
|
||||||
@@ -219,7 +219,7 @@ const ChenyunInternalization = () => {
|
|||||||
/>
|
/>
|
||||||
<SuccessAlert
|
<SuccessAlert
|
||||||
visible={activeSuccess === item.id}
|
visible={activeSuccess === item.id}
|
||||||
title="恭喜你, 选择正确!"
|
title="恭喜你, 选择正确!"
|
||||||
content=""
|
content=""
|
||||||
onContinue={() => {
|
onContinue={() => {
|
||||||
setActiveSuccess(null)
|
setActiveSuccess(null)
|
||||||
@@ -291,7 +291,7 @@ const ChenyunInternalization = () => {
|
|||||||
/>
|
/>
|
||||||
<SuccessAlert
|
<SuccessAlert
|
||||||
visible={activeSuccess === item.id}
|
visible={activeSuccess === item.id}
|
||||||
// title="恭喜你, 选择正确!"
|
// title="恭喜你, 选择正确!"
|
||||||
// content="回答正确,请继续探索。"
|
// content="回答正确,请继续探索。"
|
||||||
onContinue={() => {
|
onContinue={() => {
|
||||||
setActiveSuccess(null)
|
setActiveSuccess(null)
|
||||||
@@ -496,7 +496,7 @@ const ChenyunInternalization = () => {
|
|||||||
onContinue={() => {
|
onContinue={() => {
|
||||||
setShowStudyGuide(false)
|
setShowStudyGuide(false)
|
||||||
setCourseTimeProgress(24)
|
setCourseTimeProgress(24)
|
||||||
navigate('/menu')
|
navigate('/learning-map', { state: { completedResidence: 'chenyun' } })
|
||||||
}}
|
}}
|
||||||
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_40.mp3')}
|
audioSrc={getPublicPath('audio/chenyun/mp3_chenyun_40.mp3')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { ClueData } from '../clue/types'
|
|||||||
// 银元之战视频配置
|
// 银元之战视频配置
|
||||||
export const YINYUAN_VIDEO_CONFIG = {
|
export const YINYUAN_VIDEO_CONFIG = {
|
||||||
src: getPublicPath('video/chenyun/chenyun_15.mp4'),
|
src: getPublicPath('video/chenyun/chenyun_15.mp4'),
|
||||||
narration: `1949年5月,上海解放,此时上海经济秩序混乱,一些资本家甚至说“共产党是军事一百分,政治八十分,财经打零分。“上海经济旧势力利用人们长期以来形成的担心钞票贬值的心理,大肆贩卖银元,掀起投机风潮。上海证券交易所是投机商的大本营,而大街小巷的银元贩子却更多。原来的一块银元值100人民币,到了6月4日已经涨到1100元,而涨势仍在继续。事实上,银元已经是上海市场的主要货币。有些商号只认银元,把人民币排斥到市场之外。上海,这个全国的经济心脏,能否在新政权手中恢复跳动?一场没有硝烟的经济战争即将打响。面对这硝烟弥漫的经济战线,陈云同志亲自参与指导了同旧经济势力间的“银元之战”。”你将化身陈云同志的特派员,直面这场“银元之战”。`,
|
narration: `1949年5月,上海解放,此时上海经济秩序混乱,一些资本家甚至说“共产党是军事一百分,政治八十分,财经打零分。”上海经济旧势力利用人们长期以来形成的担心钞票贬值的心理,大肆贩卖银元,掀起投机风潮。上海证券交易所是投机商的大本营,而大街小巷的银元贩子却更多。原来的一块银元值100人民币,到了6月4日已经涨到1100元,而涨势仍在继续。事实上,银元已经是上海市场的主要货币。有些商号只认银元,把人民币排斥到市场之外。上海,这个全国的经济心脏,能否在新政权手中恢复跳动?一场没有硝烟的经济战争即将打响。面对这硝烟弥漫的经济战线,陈云同志亲自参与指导了同旧经济势力间的“银元之战”。你将化身陈云同志的特派员,直面这场“银元之战”。`,
|
||||||
alert: {
|
alert: {
|
||||||
title: '第一幕:“银元之战”--人民币的阵地争夺战',
|
title: '第一幕:“银元之战”--人民币的阵地争夺战',
|
||||||
subtitle: '上海目前出现大肆贩卖银元的情况,请你了解情况!',
|
subtitle: '上海目前出现大肆贩卖银元的情况,请你了解情况!',
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ const ChenyunReflection = () => {
|
|||||||
// ========== 顶层状态管理 ==========
|
// ========== 顶层状态管理 ==========
|
||||||
|
|
||||||
/** 当前展开的文件 */
|
/** 当前展开的文件 */
|
||||||
const [expandedFile, setExpandedFile] = useState<FileType | null>(() =>
|
const [expandedFile, setExpandedFile] = useState<FileType | null>(() => {
|
||||||
useChenyunStore.getState().isYinyuanSolved ? 'mimian' : null
|
const { isYinyuanSolved, isMimianSolved } = useChenyunStore.getState()
|
||||||
)
|
return isYinyuanSolved && !isMimianSolved ? 'mimian' : null
|
||||||
|
})
|
||||||
|
|
||||||
/** 当前播放的视频 */
|
/** 当前播放的视频 */
|
||||||
const [playingVideo, setPlayingVideo] = useState<FileType | null>(null)
|
const [playingVideo, setPlayingVideo] = useState<FileType | null>(null)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const MimianBattle = ({
|
|||||||
state.setShowMimianQuiz2(false)
|
state.setShowMimianQuiz2(false)
|
||||||
state.setShowMimianSuccessAlert(true)
|
state.setShowMimianSuccessAlert(true)
|
||||||
state.setMimianSuccessAlertContent(
|
state.setMimianSuccessAlertContent(
|
||||||
`结合当时市场情况,陈云同志在会上做出了"全国一盘棋"的指示。全国统筹调运粮食、棉花、煤炭填补缺口,筑牢物资基础。关键节点集中抛售,让跟风囤货的投机商无力承接,因亏本和高利贷纷纷破产,快速平抑物价、稳固人民币地位。`
|
`结合当时市场情况,陈云同志在会上做出了“全国一盘棋”的指示。全国统筹调运粮食、棉花、煤炭填补缺口,筑牢物资基础。关键节点集中抛售,让跟风囤货的投机商无力承接,因亏本和高利贷纷纷破产,快速平抑物价、稳固人民币地位。`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 选错时,错误信息已经在题目面板中通过 onWrongSelect 显示,不需要额外的 SuccessAlert
|
// 选错时,错误信息已经在题目面板中通过 onWrongSelect 显示,不需要额外的 SuccessAlert
|
||||||
@@ -125,7 +125,7 @@ export const MimianBattle = ({
|
|||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
state.setShowMimianQuiz3(false)
|
state.setShowMimianQuiz3(false)
|
||||||
state.setShowMimianSuccessAlert(true)
|
state.setShowMimianSuccessAlert(true)
|
||||||
state.setMimianSuccessAlertContent('地方配合,调运按时推进。')
|
state.setMimianSuccessAlertContent('地方配合,调运按时推进。')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ export const MimianBattle = ({
|
|||||||
if (isCorrect) {
|
if (isCorrect) {
|
||||||
state.setShowMimianQuiz4(false)
|
state.setShowMimianQuiz4(false)
|
||||||
state.setShowMimianSuccessAlert(true)
|
state.setShowMimianSuccessAlert(true)
|
||||||
state.setMimianSuccessAlertContent('棉农支持,调运量足额完成。')
|
state.setMimianSuccessAlertContent('棉农支持,调运量足额完成。')
|
||||||
} else {
|
} else {
|
||||||
// state.setShowMimianQuiz4(false);
|
// state.setShowMimianQuiz4(false);
|
||||||
// state.setShowMimianSuccessAlert(true);
|
// state.setShowMimianSuccessAlert(true);
|
||||||
|
|||||||
@@ -176,16 +176,16 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题3(图5) */}
|
{/* 米棉之战 - 单选题3(图5) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz3}
|
visible={showQuiz3}
|
||||||
title="【单选】某地方粮站反馈“本地冬储粮不足,不愿外运”。"
|
title="【单选】华东某地方粮站反馈“本地冬储粮不足,不愿外运”。"
|
||||||
options={[
|
options={[
|
||||||
{ value: 'A', label: 'A. 强制调运,否则追责' },
|
{ value: 'A', label: 'A. 强制调运,否则追责' },
|
||||||
{
|
{
|
||||||
value: 'B',
|
value: 'B',
|
||||||
label: 'B. 中央已协调东北补充常州冬储粮,先保障全国物价稳定',
|
label: 'B. 中央已协调东北补充华东地方冬储粮,先保障全国物价稳定',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
correctAnswer="B"
|
correctAnswer="B"
|
||||||
errorMessage="地方抵触,调运延误多天。请你再思考一下吧!"
|
errorMessage="地方抵触,调运延误多天。请你再思考一下吧!"
|
||||||
onSubmit={onQuiz3Submit}
|
onSubmit={onQuiz3Submit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export const MimianQuizzes = ({
|
|||||||
{/* 米棉之战 - 单选题5(图8) */}
|
{/* 米棉之战 - 单选题5(图8) */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz5}
|
visible={showQuiz5}
|
||||||
title="【单选】此时,以下哪种举措能够一举获得胜利?"
|
title="【单选】此时,以下哪种举措能够一举获得胜利?"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 'A',
|
value: 'A',
|
||||||
@@ -254,7 +254,7 @@ export const MimianQuizzes = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'B',
|
value: 'B',
|
||||||
label: `B.逐步降价,配合舆论引导,每日小幅降价,并通过报纸宣传政府物资充足,试图"劝退"投机商`,
|
label: `B.逐步降价,配合舆论引导,每日小幅降价,并通过报纸宣传政府物资充足,试图“劝退”投机商`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
correctAnswer="A"
|
correctAnswer="A"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const entranceActionConfig: Record<
|
|||||||
locked: {
|
locked: {
|
||||||
image: 'entrance/暂未解锁',
|
image: 'entrance/暂未解锁',
|
||||||
position: {
|
position: {
|
||||||
left: 'absolute left-95 bottom-58',
|
left: 'absolute left-108 bottom-58',
|
||||||
right: 'absolute right-106 bottom-58',
|
right: 'absolute right-106 bottom-58',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -47,7 +47,7 @@ const entranceActionConfig: Record<
|
|||||||
image: 'entrance/该故居已访问',
|
image: 'entrance/该故居已访问',
|
||||||
position: {
|
position: {
|
||||||
left: 'absolute left-95 bottom-58',
|
left: 'absolute left-95 bottom-58',
|
||||||
right: 'absolute right-102 bottom-58',
|
right: 'absolute right-98 bottom-58',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useNavigate, useParams } from 'react-router-dom'
|
|||||||
import { Image } from '@/components'
|
import { Image } from '@/components'
|
||||||
import { usePuzzleStore } from '@/store/puzzleStore'
|
import { usePuzzleStore } from '@/store/puzzleStore'
|
||||||
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
|
import { getResidenceStageUnlocks, isResidenceUnlocked } from '@/config/progression'
|
||||||
import { getUserDefine } from '@/lib/utils/api-common'
|
import { getUserDefineResult } from '@/lib/utils/api-common'
|
||||||
|
import { showYibanSyncErrorPrompt, showYibanUnauthenticatedPrompt } from '@/components'
|
||||||
import {
|
import {
|
||||||
useHuangyanpeiStore,
|
useHuangyanpeiStore,
|
||||||
HUANGYANPEI_LESSON_ID,
|
HUANGYANPEI_LESSON_ID,
|
||||||
@@ -37,28 +38,63 @@ const Huangyanpei = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const raw = await getUserDefine(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
|
const result = await getUserDefineResult(HUANGYANPEI_LESSON_ID, HUANGYANPEI_KEY)
|
||||||
if (!raw) return
|
|
||||||
|
|
||||||
const data = JSON.parse(raw) as {
|
if (result.status !== 'success') {
|
||||||
|
useHuangyanpeiStore.setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
visitedExhibits: {},
|
||||||
|
lastVisitedPlace: null,
|
||||||
|
reflectionRoute: 'reflection',
|
||||||
|
shownReflectionCompletionGuides: {},
|
||||||
|
reflectionScene2Choice: null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (result.status === 'unauthenticated') {
|
||||||
|
showYibanUnauthenticatedPrompt()
|
||||||
|
} else if (result.status === 'error') {
|
||||||
|
showYibanSyncErrorPrompt()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(result.content) as {
|
||||||
visitedExhibits?: Record<string, number[]>
|
visitedExhibits?: Record<string, number[]>
|
||||||
lastVisitedPlace?: string | null
|
lastVisitedPlace?: string | null
|
||||||
reflectionRoute?: string
|
reflectionRoute?: string
|
||||||
|
shownReflectionCompletionGuides?: Record<string, boolean>
|
||||||
|
reflectionScene2Choice?: '铁工科' | '商科' | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validReflectionRoute =
|
||||||
|
data.reflectionRoute &&
|
||||||
|
['reflection', 'reflection2', 'reflection3', 'reflection4'].includes(data.reflectionRoute)
|
||||||
|
? data.reflectionRoute
|
||||||
|
: 'reflection'
|
||||||
|
|
||||||
useHuangyanpeiStore.setState((prev) => ({
|
useHuangyanpeiStore.setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
lastVisitedPlace:
|
visitedExhibits: Object.fromEntries(
|
||||||
data.lastVisitedPlace !== undefined ? data.lastVisitedPlace : prev.lastVisitedPlace,
|
Object.entries(data.visitedExhibits ?? {}).map(([place, exhibits]) => [
|
||||||
reflectionRoute:
|
place,
|
||||||
data.reflectionRoute &&
|
new Set(Array.isArray(exhibits) ? exhibits : []),
|
||||||
['reflection', 'reflection2', 'reflection3', 'reflection4'].includes(
|
])
|
||||||
data.reflectionRoute
|
),
|
||||||
)
|
lastVisitedPlace: data.lastVisitedPlace ?? null,
|
||||||
? data.reflectionRoute
|
reflectionRoute: validReflectionRoute,
|
||||||
: (prev.reflectionRoute ?? 'reflection'),
|
shownReflectionCompletionGuides: data.shownReflectionCompletionGuides ?? {},
|
||||||
|
reflectionScene2Choice: data.reflectionScene2Choice ?? null,
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
useHuangyanpeiStore.setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
visitedExhibits: {},
|
||||||
|
lastVisitedPlace: null,
|
||||||
|
reflectionRoute: 'reflection',
|
||||||
|
shownReflectionCompletionGuides: {},
|
||||||
|
reflectionScene2Choice: null,
|
||||||
|
}))
|
||||||
|
showYibanSyncErrorPrompt()
|
||||||
console.error('Failed to load Huangyanpei store', error)
|
console.error('Failed to load Huangyanpei store', error)
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ const HuangyanpeiInternalization = () => {
|
|||||||
onContinue={() => {
|
onContinue={() => {
|
||||||
setShowStudyGuide(false)
|
setShowStudyGuide(false)
|
||||||
setCourseTimeProgress(12)
|
setCourseTimeProgress(12)
|
||||||
navigate('/menu')
|
navigate('/learning-map', { state: { completedResidence: 'huangyanpei' } })
|
||||||
}}
|
}}
|
||||||
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_31.mp3')}
|
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_31.mp3')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ const HuangyanpeiPlace3 = () => {
|
|||||||
speaker="student"
|
speaker="student"
|
||||||
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_07.mp3')}
|
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_07.mp3')}
|
||||||
>
|
>
|
||||||
这所川沙小学堂就是黄炎培先生“教育救国”的第一个试验田吧?我们现在学习的课程有专业课、实习实训课都提倡技能培养,当年他是不是也很注重动手能力呀?
|
这所川沙小学堂就是黄炎培先生“教育救国”的第一个试验田吧?我们现在的专业课、实训课都提倡技能培养,当年他是不是也很注重动手能力呀?
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
<GuideStepsItem
|
<GuideStepsItem
|
||||||
speaker="guide"
|
speaker="guide"
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ const HuangyanpeiPlace4 = () => {
|
|||||||
{/* 题目 */}
|
{/* 题目 */}
|
||||||
<SingleChoiceQuizPanel
|
<SingleChoiceQuizPanel
|
||||||
visible={showQuiz}
|
visible={showQuiz}
|
||||||
title={`【单选】中华职业学校首批招生78人,凭借____口号和“免学费、包分配”的优待,开创了中国近代职业教育的先河。`}
|
title={`【单选】中华职业学校首批招生78人,凭借____口号和“免学费、包分配”的优待,开创了中国近代职业教育的先河。`}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'A', label: 'A. 学而优则仕' },
|
{ value: 'A', label: 'A. 学而优则仕' },
|
||||||
{ value: 'B', label: 'B. 双手万能、手脑并用' },
|
{ value: 'B', label: 'B. 双手万能、手脑并用' },
|
||||||
|
|||||||
@@ -150,13 +150,13 @@ const HuangyanpeiPlace6 = () => {
|
|||||||
speaker="guide"
|
speaker="guide"
|
||||||
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_21_2.mp3')}
|
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_21_2.mp3')}
|
||||||
>
|
>
|
||||||
第一次把”中等专业学校”正式纳入国家学制,让职业教育有了明确的国家定位。1952年,他又推动建立了上海机械学校、南京航空学校等五所中专,这些学校为国家”一五”计划输送了6.3万名技术工人,为工业发展提供了坚实的人才支撑。
|
第一次把“中等专业学校”正式纳入国家学制,让职业教育有了明确的国家定位。1952年,他又推动建立了上海机械学校、南京航空学校等五所中专,这些学校为国家“一五”计划输送了6.3万名技术工人,为工业发展提供了坚实的人才支撑。
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
<GuideStepsItem
|
<GuideStepsItem
|
||||||
speaker="guide"
|
speaker="guide"
|
||||||
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_21_3.mp3')}
|
audioSrc={getPublicPath('audio/huangyanpei/mp3_huangyanpei_21_3.mp3')}
|
||||||
>
|
>
|
||||||
黄炎培先生曾说过:”我要把最后一滴血,滴在职业教育的讲台上。”他用一生践行了这句话,而我们现在中职教育强调的”工学结合、德技并修”,正是对他”手脑并用、注重实践”理念的传承和发展。
|
黄炎培先生曾说过:“我要把最后一滴血,滴在职业教育的讲台上。”他用一生践行了这句话,而我们现在中职教育强调的“工学结合、德技并修”,正是对他“手脑并用、注重实践”理念的传承和发展。
|
||||||
</GuideStepsItem>
|
</GuideStepsItem>
|
||||||
</GuideSteps>
|
</GuideSteps>
|
||||||
|
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ const HuangyanpeiReflectionScene2 = () => {
|
|||||||
<SuccessAlert
|
<SuccessAlert
|
||||||
visible={showQuizSuccess}
|
visible={showQuizSuccess}
|
||||||
title=""
|
title=""
|
||||||
content="恭喜你!选择正确。不愧是从川沙小学堂走出来!"
|
content="恭喜你!选择正确。不愧是从川沙小学堂走出来!"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowQuizSuccess(false)
|
setShowQuizSuccess(false)
|
||||||
}}
|
}}
|
||||||
@@ -246,7 +246,7 @@ const HuangyanpeiReflectionScene2 = () => {
|
|||||||
onContinue={handleCompleteGuideContinue}
|
onContinue={handleCompleteGuideContinue}
|
||||||
verticalAlign="center"
|
verticalAlign="center"
|
||||||
>
|
>
|
||||||
第二关完成!“职业方向锁定!”去新学校了解你的专业吧。
|
第二关完成!职业方向锁定!去新学校了解你的专业吧。
|
||||||
</StudyGuideModal>
|
</StudyGuideModal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ const ChoiceDialog2 = ({ visible = true, onClose, onSelect }: ChoiceDialog2Props
|
|||||||
<SuccessAlert
|
<SuccessAlert
|
||||||
visible={showSuccessAlert}
|
visible={showSuccessAlert}
|
||||||
title="选得好!"
|
title="选得好!"
|
||||||
content="不管是做工匠还是做买卖,学好本事就能报效国家,这个选择会影响你在中华职业学校的专业哦!"
|
content="不管是做工匠还是做买卖,学好本事就能报效国家,这个选择会影响你在中华职业学校的专业哦!"
|
||||||
onClose={handleSuccessAlertContinue}
|
onClose={handleSuccessAlertContinue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||