Files
shanghai-residences/src/pages/zhangleping/draw/draw1.tsx
T
zhongyidianandClaude Opus 4.7 49ecabbf3a fix: 修复开发/测试包模拟进度被易班同步覆盖的问题
进度模拟面板写入 puzzleStore 后,App 启动时易班同步会将其覆盖;
现在在同步完成后按 sessionStorage 中保存的模式重放模拟进度,
确保测试包的"到达某故居/全流程完成"调试入口生效。

同时纳入近期未提交的页面、组件、资源与环境变量改动。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 10:19:36 +08:00

289 lines
10 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react'
import { ExhibitFlipOverlay, FlipActionButton, Image, SceneHint } from '@/components'
import { cn, getBackgroundImage, getPublicPath, getStaticImage } from '@/lib/utils'
import { filterInput } from '@/lib/utils/sensitiveWords'
import { useNavigate, useParams } from 'react-router-dom'
import { saveZhanglepingStore, useZhanglepingStore } from '@/store/zhanglepingStore'
import {
type BackgroundKey,
type ClothesKey,
type MaterialKey,
type SidebarBlockKey,
STAR_NAME,
backgroundToAllowedMaterials,
buildCompositeDataUrl,
getBackgroundAssetKey,
materialConflicts,
materialOptions,
sidebarBlocks,
} from './draw1.config'
import { TopMenuPanel } from './TopMenuPanel'
import { SanmaoDialogueItem, SanmaoDialogueSteps } from '@/pages/zhangleping/components'
const COMPLETION_DIALOGUE =
'太好啦!我的记忆都回来啦,还拥有了新时代的模样!爷爷画的我,藏着善良、勇敢与热爱;你画的我,让这些美好品质在今天继续发光!谢谢你,让三毛精神一直传承下去!'
const ZhanglepingDraw1 = () => {
const navigate = useNavigate()
const { moduleName } = useParams<{ moduleName: string }>()
const { setDrawArtwork } = useZhanglepingStore()
const [activeBlock, setActiveBlock] = useState<SidebarBlockKey>('衣柜')
const [selectedClothes, setSelectedClothes] = useState<ClothesKey>('默认')
const [selectedBackground, setSelectedBackground] = useState<BackgroundKey>('城际高铁')
const [selectedMaterials, setSelectedMaterials] = useState<Set<MaterialKey>>(new Set())
const [showCaptionInput, setShowCaptionInput] = useState(false)
const [captionText, setCaptionText] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [createdArtworkSrc, setCreatedArtworkSrc] = useState('')
const [showCreatedOverlay, setShowCreatedOverlay] = useState(false)
const [showCompletionDialogue, setShowCompletionDialogue] = useState(false)
const [submitErrorHintKey, setSubmitErrorHintKey] = useState(0)
const captionInputRef = useRef<HTMLInputElement>(null)
const compositionRef = useRef<HTMLDivElement>(null)
const allowedMaterialKeys = useMemo(
() => backgroundToAllowedMaterials[selectedBackground] ?? [],
[selectedBackground]
)
const backgroundAssetKey = getBackgroundAssetKey(selectedBackground)
const scenePreviewSrc = `${STAR_NAME}/reflection/draw/布景/${backgroundAssetKey}`
const clothingSrc = `${STAR_NAME}/reflection/draw/衣柜/${selectedClothes}`
const boardBgSrc = `${STAR_NAME}/reflection/draw/画板底图`
// 切换布景后:移除不允许的素材
useEffect(() => {
setSelectedMaterials((prev) => {
const next = new Set([...prev].filter((k) => allowedMaterialKeys.includes(k)))
return next.size === prev.size ? prev : next
})
}, [allowedMaterialKeys])
useEffect(() => {
if (showCaptionInput) captionInputRef.current?.focus()
}, [showCaptionInput])
const handleToggleMaterial = (key: MaterialKey) => {
setSelectedMaterials((prev) => {
const next = new Set(prev)
if (next.has(key)) {
next.delete(key)
return next
}
const conflicts = materialConflicts[key] ?? []
for (const c of conflicts) next.delete(c)
if (next.size < 3) next.add(key)
return next
})
}
const handleSubmitArtwork = async () => {
if (!showCaptionInput) {
setShowCaptionInput(true)
return
}
if (isSubmitting || !compositionRef.current) return
const filteredCaptionText = filterInput(captionText.trim())
if (Array.from(filteredCaptionText).length > 15) {
setCaptionText(Array.from(filteredCaptionText).slice(0, 15).join(''))
setSubmitErrorHintKey((key) => key + 1)
return
}
setIsSubmitting(true)
try {
const dataUrl = await buildCompositeDataUrl(compositionRef.current)
setDrawArtwork(dataUrl, filteredCaptionText)
saveZhanglepingStore()
setCreatedArtworkSrc(dataUrl)
setShowCreatedOverlay(true)
} catch (error) {
console.error('Failed to submit draw artwork', error)
setSubmitErrorHintKey((key) => key + 1)
} finally {
setIsSubmitting(false)
}
}
const handleCollectArtwork = () => {
navigate(`/${moduleName}/${STAR_NAME}/reflection5`)
}
const isPreviewVisible = activeBlock !== '任务提示'
if (!moduleName) return null
return (
<div
className="w-full h-full relative overflow-hidden bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: getBackgroundImage(boardBgSrc) }}
>
{/* 中央预览区 */}
<div
ref={compositionRef}
className="absolute left-20 bottom-[86px] w-[1605px] h-[904px] bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: getBackgroundImage(scenePreviewSrc) }}
>
<div className="relative w-full h-full">
{isPreviewVisible && (
<div className="absolute left-111 bottom-0 z-10">
<Image src={getStaticImage(clothingSrc)} alt="衣柜选项" />
</div>
)}
{isPreviewVisible &&
materialOptions
.filter((opt) => selectedMaterials.has(opt.key))
.map((opt) => (
<div
key={opt.key}
className={cn('absolute', opt.key === '车' ? 'z-8' : 'z-20')}
data-material-key={opt.key}
style={{
top: opt.position.top,
left: opt.position.left,
bottom: opt.position.bottom,
right: opt.position.right,
}}
>
<Image src={getStaticImage(opt.assetSrc)} alt={opt.key} />
</div>
))}
</div>
</div>
{/* 顶部菜单 */}
<div className="absolute left-0 top-4 z-30 w-full">
<TopMenuPanel
activeBlock={activeBlock}
selectedClothes={selectedClothes}
selectedBackground={selectedBackground}
selectedMaterials={selectedMaterials}
allowedMaterialKeys={allowedMaterialKeys}
onSelectClothes={setSelectedClothes}
onSelectBackground={setSelectedBackground}
onToggleMaterial={handleToggleMaterial}
/>
</div>
{/* 任务提示窗口 */}
{activeBlock === '任务提示' && (
<div
className="absolute left-8 top-[26%] z-30 w-[1724px] h-[424px] bg-no-repeat"
style={{
backgroundImage: getBackgroundImage(`${STAR_NAME}/reflection/draw/创作任务面板`),
backgroundSize: '100% 100%',
}}
>
<div className="flex items-center justify-center pt-28">
<div className="w-full text-center text-[#8B6914] font-medium leading-tight">
<div className="text-3xl space-y-8">
<div>①为三毛换装</div>
<div>②搭配1个新时代场景</div>
<div>③融入1-3个新时代元素</div>
<div>④配1句简短正能量文字(不超过15个字)</div>
</div>
</div>
</div>
</div>
)}
{/* 右侧侧边栏 */}
<div className="absolute right-[4px] top-[162px] z-40 w-[162px]">
<div className="flex flex-col gap-4">
{sidebarBlocks.map((blk) => (
<button key={blk.key} type="button" onClick={() => setActiveBlock(blk.key)}>
<Image
className={cn(activeBlock === blk.key ? 'opacity-100' : 'opacity-60')}
src={getStaticImage(blk.iconSrc)}
alt={blk.key}
/>
</button>
))}
</div>
</div>
{/* 底部输入框 */}
{showCaptionInput && (
<div
className="absolute left-20 bottom-2.5 bg-cover bg-center bg-no-repeat w-[1724px] h-[100px] z-40"
style={{ backgroundImage: getBackgroundImage(`${STAR_NAME}/reflection/draw/写字底板`) }}
>
<div className="flex items-center justify-center">
<input
ref={captionInputRef}
type="text"
id="textInput"
name="textInput"
maxLength={15}
value={captionText}
onChange={(e) =>
setCaptionText(Array.from(filterInput(e.target.value)).slice(0, 15).join(''))
}
className={cn(
'w-260 h-24 bg-transparent',
'text-4xl! text-[#8B6914] leading-none placeholder:text-3xl!'
)}
autoComplete="off"
placeholder="提交之前,请给你创作的我配一句简短的正能量文字吧!(不超过15个字)"
/>
</div>
</div>
)}
{/* 底部按钮 */}
<div className="absolute -left-10 bottom-0 z-50">
<Image
src={getStaticImage(`${STAR_NAME}/reflection/draw/返回`)}
button
onClick={() => navigate(`/${moduleName}/${STAR_NAME}/draw`)}
/>
</div>
<div className="absolute -right-10 bottom-0 z-50">
<Image
src={getStaticImage(`${STAR_NAME}/reflection/draw/提交作品`)}
button
className={isSubmitting ? 'opacity-70 pointer-events-none' : undefined}
onClick={handleSubmitArtwork}
/>
</div>
<SceneHint when={submitErrorHintKey > 0} triggerKey={submitErrorHintKey} durationMs={3200}>
作品生成失败,请稍后再试
</SceneHint>
{createdArtworkSrc && (
<ExhibitFlipOverlay
src={createdArtworkSrc}
visible={showCreatedOverlay}
// footerClassName="bottom-120"
onEnterComplete={() => {
setShowCompletionDialogue(true)
}}
onClose={() => {
setShowCreatedOverlay(false)
}}
footer={<FlipActionButton onClick={handleCollectArtwork}>收藏</FlipActionButton>}
/>
)}
<SanmaoDialogueSteps
visible={showCompletionDialogue}
onVisibleChange={setShowCompletionDialogue}
showMask={false}
audioChannelId="zhangleping-draw-completion"
zIndex={10000}
>
<SanmaoDialogueItem audioSrc={getPublicPath('audio/zhangleping/mp3_zhangleping_30_1.mp3')}>
{COMPLETION_DIALOGUE}
</SanmaoDialogueItem>
</SanmaoDialogueSteps>
</div>
)
}
export default ZhanglepingDraw1