387 lines
14 KiB
TypeScript
387 lines
14 KiB
TypeScript
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { AnimatePresence, motion } from "framer-motion";
|
|
import { cn, getStaticImage } from "@/lib/utils";
|
|
import { Image } from "@/components";
|
|
import type { DragFillResult } from "@/components";
|
|
import { useScale } from "@/contexts/ScaleContext";
|
|
|
|
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 { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale();
|
|
const [mounted, setMounted] = useState(false);
|
|
const [value, setValue] = useState<DragFillResult[]>([]);
|
|
const completedRef = useRef(false);
|
|
|
|
// SSR 兼容
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
// 重置游戏状态
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setValue([]);
|
|
completedRef.current = false;
|
|
}
|
|
}, [visible]);
|
|
|
|
// 检查是否完成(3个毛巾都点亮)
|
|
useEffect(() => {
|
|
if (value.length === 3 && !completedRef.current) {
|
|
completedRef.current = true;
|
|
// 延迟一点时间让用户看到效果,然后完成
|
|
setTimeout(() => {
|
|
onComplete?.();
|
|
onClose?.();
|
|
}, 500);
|
|
}
|
|
}, [value, onComplete, onClose]);
|
|
|
|
const handleClose = useCallback(
|
|
(e: React.MouseEvent<HTMLImageElement>) => {
|
|
e.stopPropagation();
|
|
onClose?.();
|
|
},
|
|
[onClose]
|
|
);
|
|
|
|
// 可选话术选项
|
|
const options = useMemo(
|
|
() => [
|
|
{
|
|
value: "option1",
|
|
label: "这是中华职校生产的国货,质量比洋货还好!",
|
|
},
|
|
{
|
|
value: "option2",
|
|
label: "价格便宜,量大从优,诚信经营绝不掺假",
|
|
},
|
|
{
|
|
value: "option3",
|
|
label: "买国货就是支持实业救国,为国家出一份力!",
|
|
},
|
|
{
|
|
value: "option4",
|
|
label: "我只管赚钱,东西好不好不管哦",
|
|
},
|
|
],
|
|
[]
|
|
);
|
|
|
|
// 处理拖拽结果变化
|
|
const handleValueChange = useCallback((newValue: DragFillResult[]) => {
|
|
// 限制最多3个
|
|
if (newValue.length <= 3) {
|
|
setValue(newValue);
|
|
}
|
|
}, []);
|
|
|
|
// 检查毛巾是否被点亮
|
|
const isTowelLit = useCallback(
|
|
(towelId: string) => {
|
|
return value.some((result) => result.blankId === towelId);
|
|
},
|
|
[value]
|
|
);
|
|
|
|
// 拖拽悬停状态
|
|
const [hoveredTowel, setHoveredTowel] = useState<string | null>(null);
|
|
|
|
// 样式
|
|
const overlayStyle = useMemo<React.CSSProperties>(
|
|
() => ({
|
|
position: "fixed",
|
|
inset: 0,
|
|
zIndex: 9999,
|
|
pointerEvents: "none",
|
|
}),
|
|
[]
|
|
);
|
|
|
|
const scaledContainerStyle = useMemo<React.CSSProperties>(
|
|
() => ({
|
|
position: "absolute",
|
|
width: `${DESIGN_WIDTH}px`,
|
|
height: `${DESIGN_HEIGHT}px`,
|
|
left: `${offsetX}px`,
|
|
top: `${offsetY}px`,
|
|
transform: `scale(${scale})`,
|
|
transformOrigin: "top left",
|
|
overflow: "hidden",
|
|
}),
|
|
[scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT]
|
|
);
|
|
|
|
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 style={scaledContainerStyle}>
|
|
<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 items-center justify-center px-30 py-16 pt-30 text-[#432800]">
|
|
<div className="w-full h-full">
|
|
{/* 左侧:题目和可选话术 */}
|
|
<div className="flex justify-between gap-10">
|
|
{/* 题目文本 */}
|
|
<div className="mb-8">
|
|
<div className="text-5xl font-bold mb-6">题目文本</div>
|
|
<div className="text-4xl leading-relaxed">
|
|
从以下话术里,挑选3句组成你的推销语 (拖拽至对话框)
|
|
</div>
|
|
</div>
|
|
|
|
{/* 右侧:毛巾区域 */}
|
|
<div className="w-180 flex flex-col items-center">
|
|
{/* 答题区标签 */}
|
|
{/* <div className="text-4xl font-bold text-white mb-8">
|
|
答题区
|
|
</div> */}
|
|
|
|
{/* 毛巾堆叠区域 */}
|
|
<div className="relative w-full flex flex-col ">
|
|
{/* 毛巾1 */}
|
|
<div
|
|
className={cn(
|
|
"relative w-full flex items-center justify-center transition-all duration-300 rounded-2xl"
|
|
// isTowelLit("towel1")
|
|
// ? "opacity-100 border-[#f0be75]"
|
|
// : hoveredTowel === "towel1"
|
|
// ? "opacity-100 border-[#f0be75] bg-[#f0be75]/20"
|
|
// : "opacity-100 border-transparent"
|
|
)}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "move";
|
|
if (!isTowelLit("towel1")) {
|
|
setHoveredTowel("towel1");
|
|
}
|
|
}}
|
|
onDragLeave={() => {
|
|
setHoveredTowel(null);
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
setHoveredTowel(null);
|
|
const optionValue =
|
|
e.dataTransfer.getData("text/plain");
|
|
if (optionValue && !isTowelLit("towel1")) {
|
|
// 检查是否已经使用过这个选项
|
|
const alreadyUsed = value.some(
|
|
(r) => r.optionValue === optionValue
|
|
);
|
|
if (!alreadyUsed) {
|
|
handleValueChange([
|
|
...value,
|
|
{ blankId: "towel1", optionValue },
|
|
]);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<img
|
|
src={isTowelLit("towel1") ? towel1Lit : towel1}
|
|
alt="毛巾1"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
|
|
{/* 毛巾2 */}
|
|
<div
|
|
className={cn(
|
|
"relative w-full flex items-center justify-center transition-all duration-300 rounded-2xl"
|
|
)}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "move";
|
|
if (!isTowelLit("towel2")) {
|
|
setHoveredTowel("towel2");
|
|
}
|
|
}}
|
|
onDragLeave={() => {
|
|
setHoveredTowel(null);
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
setHoveredTowel(null);
|
|
const optionValue =
|
|
e.dataTransfer.getData("text/plain");
|
|
if (optionValue && !isTowelLit("towel2")) {
|
|
// 检查是否已经使用过这个选项
|
|
const alreadyUsed = value.some(
|
|
(r) => r.optionValue === optionValue
|
|
);
|
|
if (!alreadyUsed) {
|
|
handleValueChange([
|
|
...value,
|
|
{ blankId: "towel2", optionValue },
|
|
]);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<img
|
|
src={isTowelLit("towel2") ? towel2Lit : towel2}
|
|
alt="毛巾2"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
|
|
{/* 毛巾3 */}
|
|
<div
|
|
className={cn(
|
|
"relative w-full flex items-center justify-center transition-all duration-300 rounded-2xl"
|
|
)}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = "move";
|
|
if (!isTowelLit("towel3")) {
|
|
setHoveredTowel("towel3");
|
|
}
|
|
}}
|
|
onDragLeave={() => {
|
|
setHoveredTowel(null);
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
setHoveredTowel(null);
|
|
const optionValue =
|
|
e.dataTransfer.getData("text/plain");
|
|
if (optionValue && !isTowelLit("towel3")) {
|
|
// 检查是否已经使用过这个选项
|
|
const alreadyUsed = value.some(
|
|
(r) => r.optionValue === optionValue
|
|
);
|
|
if (!alreadyUsed) {
|
|
handleValueChange([
|
|
...value,
|
|
{ blankId: "towel3", optionValue },
|
|
]);
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
<img
|
|
src={isTowelLit("towel3") ? towel3Lit : towel3}
|
|
alt="毛巾3"
|
|
className="w-full h-full object-contain"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 可选话术区域 */}
|
|
<div className="flex-1">
|
|
<div className="text-5xl font-bold mb-6">答题区</div>
|
|
<div className="grid grid-cols-2 gap-y-8 gap-x-26 mt-12">
|
|
{options.map((option, index) => {
|
|
const isUsed = value.some(
|
|
(r) => r.optionValue === option.value
|
|
);
|
|
return (
|
|
<div
|
|
key={option.value}
|
|
className={cn(
|
|
" px-10 py-10 rounded-2xl border-3 text-3xl font-medium transition-all",
|
|
isUsed
|
|
? "border-[#8b7355] bg-[#3d3329] text-[#a89378] cursor-not-allowed opacity-50"
|
|
: "border-[#79471f] bg-black/20 to-[#3a2f25] text-white cursor-move hover:border-[#f0be75] hover:shadow-[0_12px_40px_rgba(240,190,117,0.7)]"
|
|
)}
|
|
draggable={!isUsed}
|
|
onDragStart={(e) => {
|
|
if (isUsed) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
e.dataTransfer.setData(
|
|
"text/plain",
|
|
option.value
|
|
);
|
|
}}
|
|
>
|
|
{index + 1}. {option.label}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
|
|
return createPortal(content, document.body);
|
|
};
|
|
|
|
export default TowelSalesDialog;
|