feat: 更新 ESLint 和 Prettier 配置,增强代码质量管理
- 在 .gitignore 中添加 public/audio/* 以忽略音频文件。 - 更新 bun.lock,添加 eslint-config-prettier 和 eslint-plugin-prettier 依赖,确保 ESLint 与 Prettier 的兼容性。 - 修改 eslint.config.ts,整合 Prettier 规则,优化代码风格一致性。 - 在 package.json 中添加 lint:fix 和 format 脚本,简化代码格式化和修复流程。 - 更新 tailwind.config.ts 和其他组件文件,统一代码风格,提升可读性。
This commit is contained in:
1 parent
5d00adc1a5
commit
76c67b492e
216 files changed
+12454
-13976
No files matched your search
@@ -1,22 +1,22 @@
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||
import { Image, Video, DragFill } from "@/components";
|
||||
import type { DragFillResult } from "@/components";
|
||||
import { useScale } from "@/contexts/ScaleContext";
|
||||
import type { VideoRef } from "@/components/video";
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { cn, getStaticImage, getPublicPath } from '@/lib/utils'
|
||||
import { Image, Video, DragFill } from '@/components'
|
||||
import type { DragFillResult } from '@/components'
|
||||
import { useScale } from '@/contexts/ScaleContext'
|
||||
import type { VideoRef } from '@/components/video'
|
||||
|
||||
export interface ViseAssemblyDialogProps {
|
||||
/** 是否可见 */
|
||||
visible?: boolean;
|
||||
visible?: boolean
|
||||
/** 关闭回调 */
|
||||
onClose?: () => void;
|
||||
onClose?: () => void
|
||||
/** 完成回调 */
|
||||
onComplete?: () => void;
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
type PartType = "A" | "B" | "C";
|
||||
type PartType = 'A' | 'B' | 'C'
|
||||
|
||||
/**
|
||||
* 组装台钳互动弹窗
|
||||
@@ -24,151 +24,141 @@ type PartType = "A" | "B" | "C";
|
||||
* - 底部三个零件(A、B、C)可以选择
|
||||
* - 答题成功后播放视频
|
||||
*/
|
||||
const ViseAssemblyDialog = ({
|
||||
visible = true,
|
||||
onClose,
|
||||
onComplete,
|
||||
}: ViseAssemblyDialogProps) => {
|
||||
const maskSrc = getStaticImage("components/蒙版遮罩");
|
||||
const backSrc = getStaticImage("components/关闭");
|
||||
const ViseAssemblyDialog = ({ visible = true, onClose, onComplete }: ViseAssemblyDialogProps) => {
|
||||
const maskSrc = getStaticImage('components/蒙版遮罩')
|
||||
const backSrc = getStaticImage('components/关闭')
|
||||
|
||||
// 零件背景提示素材
|
||||
const partHintA = getStaticImage("huangyanpei/reflection/scene3/11_005");
|
||||
const partHintB = getStaticImage("huangyanpei/reflection/scene3/11_006");
|
||||
const partHintC = getStaticImage("huangyanpei/reflection/scene3/c");
|
||||
const partHintA = getStaticImage('huangyanpei/reflection/scene3/11_005')
|
||||
const partHintB = getStaticImage('huangyanpei/reflection/scene3/11_006')
|
||||
const partHintC = getStaticImage('huangyanpei/reflection/scene3/c')
|
||||
|
||||
// 零件素材
|
||||
const partA = getStaticImage("huangyanpei/reflection/scene3/11_010");
|
||||
const partB = getStaticImage("huangyanpei/reflection/scene3/11_011");
|
||||
const partC = getStaticImage("huangyanpei/reflection/scene3/11_012");
|
||||
const partA = getStaticImage('huangyanpei/reflection/scene3/11_010')
|
||||
const partB = getStaticImage('huangyanpei/reflection/scene3/11_011')
|
||||
const partC = getStaticImage('huangyanpei/reflection/scene3/11_012')
|
||||
|
||||
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [value, setValue] = useState<DragFillResult[]>([]);
|
||||
const [videoVisible, setVideoVisible] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
const completedRef = useRef(false);
|
||||
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [value, setValue] = useState<DragFillResult[]>([])
|
||||
const [videoVisible, setVideoVisible] = useState(false)
|
||||
const videoRef = useRef<VideoRef>(null)
|
||||
const completedRef = useRef(false)
|
||||
|
||||
// SSR 兼容
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
// 重置游戏状态
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setValue([]);
|
||||
completedRef.current = false;
|
||||
setVideoVisible(false);
|
||||
setValue([])
|
||||
completedRef.current = false
|
||||
setVideoVisible(false)
|
||||
}
|
||||
}, [visible]);
|
||||
}, [visible])
|
||||
|
||||
const handleClose = useCallback(
|
||||
(e: React.MouseEvent<HTMLImageElement>) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation()
|
||||
if (!videoVisible) {
|
||||
onClose?.();
|
||||
onClose?.()
|
||||
}
|
||||
},
|
||||
[onClose, videoVisible]
|
||||
);
|
||||
)
|
||||
|
||||
// 转换为 DragFill 的 options
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{ value: "A", label: "丝杠和丝杆" },
|
||||
{ value: "B", label: "活动钳口" },
|
||||
{ value: "C", label: "固定钳口和基座" },
|
||||
{ value: 'A', label: '丝杠和丝杆' },
|
||||
{ value: 'B', label: '活动钳口' },
|
||||
{ value: 'C', label: '固定钳口和基座' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
)
|
||||
|
||||
// 转换为 DragFill 的 blanks
|
||||
const blanks = useMemo(
|
||||
() => [
|
||||
{ id: "slot0", label: "位置1" },
|
||||
{ id: "slot1", label: "位置2" },
|
||||
{ id: "slot2", label: "位置3" },
|
||||
{ id: 'slot0', label: '位置1' },
|
||||
{ id: 'slot1', label: '位置2' },
|
||||
{ id: 'slot2', label: '位置3' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
)
|
||||
|
||||
// 处理拖拽结果变化
|
||||
const handleValueChange = useCallback((newValue: DragFillResult[]) => {
|
||||
// 验证:只允许 A->slot0, B->slot1, C->slot2
|
||||
const filteredValue = newValue.filter((result) => {
|
||||
const expectedSlot =
|
||||
result.optionValue === "A"
|
||||
? "slot0"
|
||||
: result.optionValue === "B"
|
||||
? "slot1"
|
||||
: "slot2";
|
||||
return result.blankId === expectedSlot;
|
||||
});
|
||||
setValue(filteredValue);
|
||||
}, []);
|
||||
result.optionValue === 'A' ? 'slot0' : result.optionValue === 'B' ? 'slot1' : 'slot2'
|
||||
return result.blankId === expectedSlot
|
||||
})
|
||||
setValue(filteredValue)
|
||||
}, [])
|
||||
|
||||
// 处理"拼接台钳"按钮点击
|
||||
const handleAssemble = useCallback(() => {
|
||||
if (value.length < 3) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const slot0 = value.find((r) => r.blankId === "slot0");
|
||||
const slot1 = value.find((r) => r.blankId === "slot1");
|
||||
const slot2 = value.find((r) => r.blankId === "slot2");
|
||||
const slot0 = value.find((r) => r.blankId === 'slot0')
|
||||
const slot1 = value.find((r) => r.blankId === 'slot1')
|
||||
const slot2 = value.find((r) => r.blankId === 'slot2')
|
||||
|
||||
// 验证:A->slot0, B->slot1, C->slot2
|
||||
const isCorrect =
|
||||
slot0?.optionValue === "A" &&
|
||||
slot1?.optionValue === "B" &&
|
||||
slot2?.optionValue === "C";
|
||||
slot0?.optionValue === 'A' && slot1?.optionValue === 'B' && slot2?.optionValue === 'C'
|
||||
|
||||
if (isCorrect) {
|
||||
completedRef.current = true;
|
||||
completedRef.current = true
|
||||
// 答题成功,播放视频
|
||||
setVideoVisible(true);
|
||||
setVideoVisible(true)
|
||||
} else {
|
||||
// 答题失败,清除错误的填充
|
||||
setTimeout(() => {
|
||||
setValue([]);
|
||||
}, 1500);
|
||||
alert("组装顺序不正确,请重新尝试!");
|
||||
setValue([])
|
||||
}, 1500)
|
||||
alert('组装顺序不正确,请重新尝试!')
|
||||
}
|
||||
}, [value]);
|
||||
}, [value])
|
||||
|
||||
const handleVideoEnd = useCallback(() => {
|
||||
setVideoVisible(false);
|
||||
onComplete?.();
|
||||
onClose?.();
|
||||
}, [onComplete, onClose]);
|
||||
setVideoVisible(false)
|
||||
onComplete?.()
|
||||
onClose?.()
|
||||
}, [onComplete, onClose])
|
||||
|
||||
// 样式
|
||||
const overlayStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
position: "fixed",
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
zIndex: 9999,
|
||||
pointerEvents: "none",
|
||||
pointerEvents: 'none',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
)
|
||||
|
||||
const scaledContainerStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
width: `${DESIGN_WIDTH}px`,
|
||||
height: `${DESIGN_HEIGHT}px`,
|
||||
left: `${offsetX}px`,
|
||||
top: `${offsetY}px`,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
overflow: "hidden",
|
||||
transformOrigin: 'top left',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
[scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT]
|
||||
);
|
||||
)
|
||||
|
||||
if (!mounted) return null;
|
||||
if (!mounted) return null
|
||||
|
||||
const content = (
|
||||
<AnimatePresence>
|
||||
@@ -179,15 +169,15 @@ const ViseAssemblyDialog = ({
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
>
|
||||
<div style={scaledContainerStyle}>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-0 flex flex-col items-center justify-center",
|
||||
"bg-black/70"
|
||||
'absolute inset-0 flex flex-col items-center justify-center',
|
||||
'bg-black/70'
|
||||
)}
|
||||
style={{ pointerEvents: "auto" }}
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
{/* 背景蒙版 */}
|
||||
{maskSrc && (
|
||||
@@ -214,7 +204,7 @@ const ViseAssemblyDialog = ({
|
||||
<div className="fixed inset-0 z-[10000] bg-black">
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={getPublicPath("video/huangyanpei/huangyanpei_11.mp4")}
|
||||
src={getPublicPath('video/huangyanpei/huangyanpei_11.mp4')}
|
||||
className="w-full h-full"
|
||||
autoPlay
|
||||
playsInline
|
||||
@@ -230,22 +220,18 @@ const ViseAssemblyDialog = ({
|
||||
{/* 顶部三个位置 - 更大的框 */}
|
||||
<div className="w-full flex items-center justify-between">
|
||||
{[0, 1, 2].map((index) => {
|
||||
const blankId = `slot${index}`;
|
||||
const filled = value.find((r) => r.blankId === blankId);
|
||||
const partType = filled
|
||||
? (filled.optionValue as PartType)
|
||||
: null;
|
||||
const hasPart = partType !== null;
|
||||
const blankId = `slot${index}`
|
||||
const filled = value.find((r) => r.blankId === blankId)
|
||||
const partType = filled ? (filled.optionValue as PartType) : null
|
||||
const hasPart = partType !== null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
data-blank-id={blankId}
|
||||
className={cn(
|
||||
"relative w-140 h-90 border-4 border-white rounded-lg transition-all duration-200",
|
||||
hasPart
|
||||
? "bg-white/30"
|
||||
: "bg-white/20 hover:bg-white/30"
|
||||
'relative w-140 h-90 border-4 border-white rounded-lg transition-all duration-200',
|
||||
hasPart ? 'bg-white/30' : 'bg-white/20 hover:bg-white/30'
|
||||
)}
|
||||
>
|
||||
{/* 背景提示 */}
|
||||
@@ -271,21 +257,21 @@ const ViseAssemblyDialog = ({
|
||||
/>
|
||||
)}
|
||||
{/* 已放置的零件 */}
|
||||
{partType === "A" && (
|
||||
{partType === 'A' && (
|
||||
<img
|
||||
src={partA}
|
||||
alt="零件A"
|
||||
className="absolute inset-0 w-full h-full object-contain p-4"
|
||||
/>
|
||||
)}
|
||||
{partType === "B" && (
|
||||
{partType === 'B' && (
|
||||
<img
|
||||
src={partB}
|
||||
alt="零件B"
|
||||
className="absolute inset-0 w-full h-full object-contain p-4"
|
||||
/>
|
||||
)}
|
||||
{partType === "C" && (
|
||||
{partType === 'C' && (
|
||||
<img
|
||||
src={partC}
|
||||
alt="零件C"
|
||||
@@ -293,7 +279,7 @@ const ViseAssemblyDialog = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -308,9 +294,7 @@ const ViseAssemblyDialog = ({
|
||||
|
||||
{/* 底部零件选择区域 - 使用 DragFill */}
|
||||
<div className="w-full gap-16">
|
||||
<div className="text-[#432800] text-5xl font-bold mr-4 my-10">
|
||||
零件区
|
||||
</div>
|
||||
<div className="text-[#432800] text-5xl font-bold mr-4 my-10">零件区</div>
|
||||
<div className="flex justify-between">
|
||||
<DragFill
|
||||
options={options}
|
||||
@@ -324,19 +308,15 @@ const ViseAssemblyDialog = ({
|
||||
className="w-full"
|
||||
renderOption={(option, isUsed) => {
|
||||
const partSrc =
|
||||
option.value === "A"
|
||||
? partA
|
||||
: option.value === "B"
|
||||
? partB
|
||||
: partC;
|
||||
option.value === 'A' ? partA : option.value === 'B' ? partB : partC
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center transition-all duration-200",
|
||||
"cursor-move select-none",
|
||||
'flex items-center justify-center transition-all duration-200',
|
||||
'cursor-move select-none',
|
||||
isUsed
|
||||
? "opacity-30 cursor-not-allowed"
|
||||
: "hover:scale-110 opacity-100"
|
||||
? 'opacity-30 cursor-not-allowed'
|
||||
: 'hover:scale-110 opacity-100'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
@@ -358,15 +338,11 @@ const ViseAssemblyDialog = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}}
|
||||
renderDragging={(option) => {
|
||||
const partSrc =
|
||||
option.value === "A"
|
||||
? partA
|
||||
: option.value === "B"
|
||||
? partB
|
||||
: partC;
|
||||
option.value === 'A' ? partA : option.value === 'B' ? partB : partC
|
||||
return (
|
||||
<div className="w-56 h-56">
|
||||
<img
|
||||
@@ -376,11 +352,11 @@ const ViseAssemblyDialog = ({
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}}
|
||||
renderBlank={() => {
|
||||
// 返回隐藏元素,实际的空白区域在页面中通过 data-blank-id 定位
|
||||
return <div className="hidden" />;
|
||||
return <div className="hidden" />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -392,9 +368,9 @@ const ViseAssemblyDialog = ({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
)
|
||||
|
||||
return createPortal(content, document.body);
|
||||
};
|
||||
return createPortal(content, document.body)
|
||||
}
|
||||
|
||||
export default ViseAssemblyDialog;
|
||||
export default ViseAssemblyDialog
|
||||
Reference in new issue
Block a user