Files
shanghai-residences/src/pages/huangyanpei/reflection/scene4/TowelSalesDialog.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

349 lines
12 KiB
TypeScript

import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { cn, getStaticImage } from '@/lib/utils'
import { Image, DragFill } from '@/components'
import type { DragFillResult } from '@/components'
import { ErrorAlert, SuccessAlert } from '@/components/layout'
export interface TowelSalesDialogProps {
/** 是否可见 */
visible?: boolean
/** 关闭回调 */
onClose?: () => void
/** 完成回调 */
onComplete?: () => void
}
/**
* 毛巾推销语拖拽弹窗
* - 左边显示4个可选话术
* - 右边显示3个毛巾(堆叠显示)
* - 拖拽话术到毛巾上,点亮对应的毛巾
* - 3个都点亮后完成
*/
const TowelSalesDialog = ({ visible = true, onClose, onComplete }: TowelSalesDialogProps) => {
const maskSrc = getStaticImage('components/蒙版遮罩')
const backSrc = getStaticImage('components/关闭')
// 毛巾图片资源(未点亮)
const towel1 = getStaticImage('huangyanpei/reflection/scene4/12_002')
const towel2 = getStaticImage('huangyanpei/reflection/scene4/12_003')
const towel3 = getStaticImage('huangyanpei/reflection/scene4/12_004')
// 毛巾图片资源(点亮)
const towel1Lit = getStaticImage('huangyanpei/reflection/scene4/12_002_02')
const towel2Lit = getStaticImage('huangyanpei/reflection/scene4/12_003_02')
const towel3Lit = getStaticImage('huangyanpei/reflection/scene4/12_004_03')
const [mounted, setMounted] = useState(false)
const [value, setValue] = useState<DragFillResult[]>([])
const [errorVisible, setErrorVisible] = useState(false)
const [successVisible, setSuccessVisible] = useState(false)
const completedRef = useRef(false)
// SSR 兼容
useEffect(() => {
setMounted(true)
}, [])
// 重置游戏状态
useEffect(() => {
if (visible) {
setValue([])
completedRef.current = false
setErrorVisible(false)
setSuccessVisible(false)
}
}, [visible])
// 检查是否完成(3个毛巾都点亮)
useEffect(() => {
if (value.length === 3 && !completedRef.current) {
completedRef.current = true
// 显示成功提示
setSuccessVisible(true)
}
}, [value])
const handleClose = useCallback(
(e: React.MouseEvent<HTMLImageElement>) => {
e.stopPropagation()
onClose?.()
},
[onClose]
)
// 关闭错误提示
const handleErrorClose = useCallback(() => {
setErrorVisible(false)
}, [])
// 关闭成功提示
const handleSuccessContinue = useCallback(() => {
setSuccessVisible(false)
// 关闭提示后,完成并关闭弹窗
onComplete?.()
onClose?.()
}, [onComplete, onClose])
// 可选话术选项
const options = useMemo(
() => [
{
value: 'option1',
label: 'A.这是中华职校生产的国货,质量比洋货还好!',
},
{
value: 'option2',
label: 'B.价格便宜,量大从优,诚信经营绝不掺假。',
},
{
value: 'option3',
label: 'C.买国货就是支持实业救国,为国家出一份力!',
},
{
value: 'option4',
label: 'D.我只管赚钱,东西好不好不管哦。',
},
],
[]
)
// 转换为 DragFill 的 blanks
const blanks = useMemo(
() => [
{ id: 'towel1', label: '毛巾1' },
{ id: 'towel2', label: '毛巾2' },
{ id: 'towel3', label: '毛巾3' },
],
[]
)
// 处理拖拽结果变化
const handleValueChange = useCallback((newValue: DragFillResult[]) => {
// 检查是否选择了错误的选项(option4)
const hasOption4 = newValue.some((result) => result.optionValue === 'option4')
if (hasOption4) {
// 显示错误提示
setErrorVisible(true)
// 移除 option4,保留其他选项
const filteredValue = newValue.filter((result) => result.optionValue !== 'option4')
setValue(filteredValue)
} else {
// 限制最多3个
if (newValue.length <= 3) {
setValue(newValue)
}
}
}, [])
// 检查毛巾是否被点亮
const isTowelLit = useCallback(
(towelId: string) => {
return value.some((result) => result.blankId === towelId)
},
[value]
)
const getTowelText = useCallback(
(towelId: string) => {
const result = value.find((item) => item.blankId === towelId)
return options.find((option) => option.value === result?.optionValue)?.label
},
[options, value]
)
// 样式
const overlayStyle = useMemo<React.CSSProperties>(
() => ({
position: 'fixed',
inset: 0,
zIndex: 9999,
pointerEvents: 'none',
}),
[]
)
if (!mounted) return null
const content = (
<AnimatePresence>
{visible && (
<motion.div
key="towel-sales-dialog"
style={overlayStyle}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<div
className={cn('absolute inset-0 flex flex-col', 'bg-black/70')}
style={{ pointerEvents: 'auto' }}
>
{/* 背景蒙版 */}
{maskSrc && (
<Image
src={maskSrc}
className="absolute inset-0 w-full h-full object-cover select-none pointer-events-none"
aria-hidden
/>
)}
{/* 返回按钮 */}
{onClose && (
<Image
src={backSrc}
onClick={handleClose}
alt="返回"
className="absolute top-5 right-5 z-20"
button
/>
)}
{/* 内容区域 */}
<div className="relative z-10 w-full h-full flex flex-col px-30 py-16 pt-30 text-[#432800]">
<div className="w-full h-full flex flex-col">
{/* 顶部:题目文本和毛巾区域 */}
<div className="flex justify-between gap-10 mb-8">
{/* 题目文本 */}
<div className="flex-1">
<div className="text-4xl leading-relaxed">
【拖拽】从以下话术里,挑选三句组成你的推销语,放置在国货毛巾上。
</div>
</div>
{/* 右侧:毛巾区域 */}
<div className="w-180 flex flex-col items-center">
{/* 毛巾堆叠区域 */}
<div className="relative w-full flex flex-col">
{/* 毛巾1 */}
<div
data-blank-id="towel1"
className="relative w-full flex items-center justify-center transition-all duration-300 rounded-2xl"
>
<img
src={isTowelLit('towel1') ? towel1Lit : towel1}
draggable={false}
alt="毛巾1"
className="w-full h-full object-contain"
/>
{getTowelText('towel1') && (
<div className="absolute inset-0 flex items-center justify-center text-center text-3xl font-bold leading-snug text-[#432800] pointer-events-none">
{getTowelText('towel1')}
</div>
)}
</div>
{/* 毛巾2 */}
<div
data-blank-id="towel2"
className="relative w-full flex items-center justify-center transition-all duration-300 rounded-2xl"
>
<img
src={isTowelLit('towel2') ? towel2Lit : towel2}
draggable={false}
alt="毛巾2"
className="w-full h-full object-contain"
/>
{getTowelText('towel2') && (
<div className="absolute inset-0 flex items-center justify-center text-center text-3xl font-bold leading-snug text-[#432800] pointer-events-none">
{getTowelText('towel2')}
</div>
)}
</div>
{/* 毛巾3 */}
<div
data-blank-id="towel3"
className="relative w-full flex items-center justify-center transition-all duration-300 rounded-2xl"
>
<img
src={isTowelLit('towel3') ? towel3Lit : towel3}
draggable={false}
alt="毛巾3"
className="w-full h-full object-contain"
/>
{getTowelText('towel3') && (
<div className="absolute inset-0 flex items-center justify-center text-center text-3xl font-bold leading-snug text-[#432800] pointer-events-none">
{getTowelText('towel3')}
</div>
)}
</div>
</div>
</div>
</div>
{/* 底部:可选话术区域 - 使用 DragFill */}
<div className="flex-1">
<div className="text-5xl font-bold mb-6">答题区</div>
<DragFill
options={options}
blanks={blanks}
value={value}
onChange={handleValueChange}
allowDuplicate={false}
layout="horizontal-options"
optionClassName="grid grid-cols-2 gap-10"
hideLabels={true}
showSubmitButton={false}
className="w-full"
renderOption={(option, isUsed) => {
// const index = options.findIndex((opt) => opt.value === option.value)
return (
<div
className={cn(
'px-10 py-10 rounded-2xl border-3 text-3xl font-medium transition-all select-none',
isUsed
? 'border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50'
: 'border-[#79471f] bg-black/20 text-white cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)]'
)}
>
{option.label}
</div>
)
}}
renderDragging={(option) => {
return (
<div className="w-[638px] h-[90px] flex items-center justify-center rounded-2xl border-3 text-2xl font-medium border-[#f0be75] bg-black/40 text-white shadow-[0_12px_40px_rgba(240,190,117,0.7)]">
{option.label}
</div>
)
}}
renderBlank={() => {
// 返回隐藏元素,实际的空白区域在页面中通过 data-blank-id 定位
return <div className="hidden" />
}}
/>
</div>
</div>
</div>
{/* 错误提示 */}
<ErrorAlert
text="哎呀!经商要讲诚信,这句话不符合黄先生倡导的商科精神。"
visible={errorVisible}
onClose={handleErrorClose}
onMaskClick={handleErrorClose}
zIndex={10001}
/>
{/* 成功提示 */}
<SuccessAlert
title="恭喜你, 推销语设计成功!"
content="既突出了国货优势,又传递了爱国情怀,当年的商科学生也是这么做的。"
visible={successVisible}
onContinue={handleSuccessContinue}
/>
</div>
</motion.div>
)}
</AnimatePresence>
)
return content
}
export default TowelSalesDialog