feat:黄炎培对话对接中
This commit is contained in:
1 parent
3611a0857a
commit
6ab184b328
125 files changed
+6617
-2391
No files matched your search
@@ -1,10 +1,9 @@
|
||||
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 { Image, DragFill } from "@/components";
|
||||
import type { DragFillResult } from "@/components";
|
||||
import { useScale } from "@/contexts/ScaleContext";
|
||||
import { ErrorAlert, SuccessAlert } from "@/components/layout";
|
||||
|
||||
export interface TowelSalesDialogProps {
|
||||
/** 是否可见 */
|
||||
@@ -40,9 +39,10 @@ const TowelSalesDialog = ({
|
||||
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 [errorVisible, setErrorVisible] = useState(false);
|
||||
const [successVisible, setSuccessVisible] = useState(false);
|
||||
const completedRef = useRef(false);
|
||||
|
||||
// SSR 兼容
|
||||
@@ -55,6 +55,8 @@ const TowelSalesDialog = ({
|
||||
if (visible) {
|
||||
setValue([]);
|
||||
completedRef.current = false;
|
||||
setErrorVisible(false);
|
||||
setSuccessVisible(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
@@ -62,13 +64,10 @@ const TowelSalesDialog = ({
|
||||
useEffect(() => {
|
||||
if (value.length === 3 && !completedRef.current) {
|
||||
completedRef.current = true;
|
||||
// 延迟一点时间让用户看到效果,然后完成
|
||||
setTimeout(() => {
|
||||
onComplete?.();
|
||||
onClose?.();
|
||||
}, 500);
|
||||
// 显示成功提示
|
||||
setSuccessVisible(true);
|
||||
}
|
||||
}, [value, onComplete, onClose]);
|
||||
}, [value]);
|
||||
|
||||
const handleClose = useCallback(
|
||||
(e: React.MouseEvent<HTMLImageElement>) => {
|
||||
@@ -78,6 +77,19 @@ const TowelSalesDialog = ({
|
||||
[onClose]
|
||||
);
|
||||
|
||||
// 关闭错误提示
|
||||
const handleErrorClose = useCallback(() => {
|
||||
setErrorVisible(false);
|
||||
}, []);
|
||||
|
||||
// 关闭成功提示
|
||||
const handleSuccessContinue = useCallback(() => {
|
||||
setSuccessVisible(false);
|
||||
// 关闭提示后,完成并关闭弹窗
|
||||
onComplete?.();
|
||||
onClose?.();
|
||||
}, [onComplete, onClose]);
|
||||
|
||||
// 可选话术选项
|
||||
const options = useMemo(
|
||||
() => [
|
||||
@@ -101,11 +113,36 @@ const TowelSalesDialog = ({
|
||||
[]
|
||||
);
|
||||
|
||||
// 转换为 DragFill 的 blanks
|
||||
const blanks = useMemo(
|
||||
() => [
|
||||
{ id: "towel1", label: "毛巾1" },
|
||||
{ id: "towel2", label: "毛巾2" },
|
||||
{ id: "towel3", label: "毛巾3" },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// 处理拖拽结果变化
|
||||
const handleValueChange = useCallback((newValue: DragFillResult[]) => {
|
||||
// 限制最多3个
|
||||
if (newValue.length <= 3) {
|
||||
setValue(newValue);
|
||||
// 检查是否选择了错误的选项(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);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -117,9 +154,6 @@ const TowelSalesDialog = ({
|
||||
[value]
|
||||
);
|
||||
|
||||
// 拖拽悬停状态
|
||||
const [hoveredTowel, setHoveredTowel] = useState<string | null>(null);
|
||||
|
||||
// 样式
|
||||
const overlayStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
@@ -131,20 +165,6 @@ const TowelSalesDialog = ({
|
||||
[]
|
||||
);
|
||||
|
||||
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 = (
|
||||
@@ -158,229 +178,159 @@ const TowelSalesDialog = ({
|
||||
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
|
||||
/>
|
||||
)}
|
||||
<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
|
||||
/>
|
||||
)}
|
||||
{/* 返回按钮 */}
|
||||
{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 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-5xl font-bold mb-6">题目文本</div>
|
||||
<div className="text-4xl leading-relaxed">
|
||||
从以下话术里,挑选3句组成你的推销语 (拖拽至对话框)
|
||||
</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 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"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</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)]"
|
||||
)}
|
||||
>
|
||||
{index + 1}. {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 createPortal(content, document.body);
|
||||
return content;
|
||||
};
|
||||
|
||||
export default TowelSalesDialog;
|
||||
Reference in new issue
Block a user