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:
zhongyidian committed 2026-03-05 13:50:47 +08:00
1 parent 5d00adc1a5
commit 76c67b492e
216 files changed
+12454 -13976

No files matched your search

+76 -95
View File
@@ -1,15 +1,9 @@
import {
ReactNode,
useState,
Children,
isValidElement,
useEffect,
} from "react";
import { AnimatePresence, motion } from "framer-motion";
import { cn, getBackgroundImage } from "@/lib/utils";
import { Overlay } from "@/components/overlay";
import { useVideoContextSafe } from "@/contexts/VideoContext";
import { useAudioManager } from "@/hooks/useAudioManager";
import { ReactNode, useState, Children, isValidElement, useEffect } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { cn, getBackgroundImage } from '@/lib/utils'
import { Overlay } from '@/components/overlay'
import { useVideoContextSafe } from '@/contexts/VideoContext'
import { useAudioManager } from '@/hooks/useAudioManager'
/**
* 使用示例:
@@ -20,29 +14,29 @@ import { useAudioManager } from "@/hooks/useAudioManager";
* </GuideSteps>
*/
interface GuideStepsProps {
children: ReactNode;
className?: string;
visible?: boolean;
onVisibleChange?: (visible: boolean) => void;
currentStep?: number;
onStepChange?: (step: number) => void;
children: ReactNode
className?: string
visible?: boolean
onVisibleChange?: (visible: boolean) => void
currentStep?: number
onStepChange?: (step: number) => void
/** 每个步骤对应的高亮选择器数组,索引对应步骤序号 */
highlightSelectors?: (string | string[])[];
highlightSelectors?: (string | string[])[]
/** 控制是否显示遮罩层(包括背景蒙版和 Overlay 遮罩,默认:true,保持向后兼容) */
showMask?: boolean;
showMask?: boolean
/** 自定义 z-index,用于控制层级(默认:50) */
zIndex?: number;
zIndex?: number
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
audioChannelId?: string;
audioChannelId?: string
}
interface GuideStepsItemProps {
children: ReactNode;
speaker?: "guide" | "student";
children: ReactNode
speaker?: 'guide' | 'student'
/** 当前步骤对应的音频地址 */
audioSrc?: string;
audioSrc?: string
/** 进入该步骤时是否自动播放音频(有 audioSrc 且未显式设置时,默认自动播放) */
autoPlayOnEnter?: boolean;
autoPlayOnEnter?: boolean
}
const GuideSteps = ({
@@ -57,125 +51,117 @@ const GuideSteps = ({
zIndex = 50,
audioChannelId,
}: GuideStepsProps) => {
const [internalStep, setInternalStep] = useState(0);
const [internalVisible, setInternalVisible] = useState(true);
const [internalStep, setInternalStep] = useState(0)
const [internalVisible, setInternalVisible] = useState(true)
// 安全地获取视频上下文(如果不在 VideoProvider 中,返回 null)
const videoContext = useVideoContextSafe();
const videoContext = useVideoContextSafe()
// 音频管理:默认全局互斥,如果传了 audioChannelId 则按 channel 互斥
const { play, stop } = useAudioManager(
audioChannelId
? { mode: "channel", channelId: audioChannelId }
: { mode: "global" }
);
audioChannelId ? { mode: 'channel', channelId: audioChannelId } : { mode: 'global' }
)
// 收集每个步骤的内容和 speaker 信息
const stepsData = Children.toArray(children).map((child) => {
if (
isValidElement<GuideStepsItemProps>(child) &&
child.type === GuideStepsItem
) {
const hasAudio = !!child.props.audioSrc;
if (isValidElement<GuideStepsItemProps>(child) && child.type === GuideStepsItem) {
const hasAudio = !!child.props.audioSrc
return {
content: child.props.children,
speaker: child.props.speaker || "guide", // 默认为 "guide" 保持向后兼容
speaker: child.props.speaker || 'guide', // 默认为 "guide" 保持向后兼容
audioSrc: child.props.audioSrc,
// 如果没有显式设置 autoPlayOnEnter,则在有 audioSrc 时默认自动播放
autoPlayOnEnter:
child.props.autoPlayOnEnter !== undefined
? child.props.autoPlayOnEnter
: hasAudio,
};
child.props.autoPlayOnEnter !== undefined ? child.props.autoPlayOnEnter : hasAudio,
}
}
// 如果不是 GuideStepsItem,保持原有行为
return {
content: child,
speaker: "guide" as const,
speaker: 'guide' as const,
audioSrc: undefined,
autoPlayOnEnter: false,
};
});
}
})
const steps = stepsData.map((step) => step.content);
const totalSteps = steps.length;
const steps = stepsData.map((step) => step.content)
const totalSteps = steps.length
// 使用受控或非受控模式
const isControlled = controlledStep !== undefined;
const currentStep = isControlled ? controlledStep : internalStep;
const baseVisible = visible !== undefined ? visible : internalVisible;
const isControlled = controlledStep !== undefined
const currentStep = isControlled ? controlledStep : internalStep
const baseVisible = visible !== undefined ? visible : internalVisible
// 如果视频正在播放或内容不可见,则不显示 GuideSteps
// 如果不在 VideoProvider 中(videoContext 为 null),则正常显示
const isVisible =
baseVisible &&
(!videoContext ||
(!videoContext.isVideoPlaying && videoContext.isContentVisible));
(!videoContext || (!videoContext.isVideoPlaying && videoContext.isContentVisible))
// 获取当前步骤的 speaker
const currentSpeaker = stepsData[currentStep]?.speaker || "guide";
const currentStepData = stepsData[currentStep];
const currentAudioSrc = currentStepData?.audioSrc;
const currentAutoPlayOnEnter = currentStepData?.autoPlayOnEnter ?? false;
const currentSpeaker = stepsData[currentStep]?.speaker || 'guide'
const currentStepData = stepsData[currentStep]
const currentAudioSrc = currentStepData?.audioSrc
const currentAutoPlayOnEnter = currentStepData?.autoPlayOnEnter ?? false
// 步骤变化时,按需自动播放音频(依赖稳定值,避免循环触发)
useEffect(() => {
if (!isVisible) return;
if (!isVisible) return
if (currentAutoPlayOnEnter && currentAudioSrc) {
play(currentAudioSrc);
play(currentAudioSrc)
}
}, [isVisible, currentStep, currentAutoPlayOnEnter, currentAudioSrc, play]);
}, [isVisible, currentStep, currentAutoPlayOnEnter, currentAudioSrc, play])
const updateStep = (newStep: number) => {
if (!isControlled) {
setInternalStep(newStep);
setInternalStep(newStep)
}
onStepChange?.(newStep);
};
onStepChange?.(newStep)
}
const updateVisible = (newVisible: boolean) => {
if (visible === undefined) {
setInternalVisible(newVisible);
setInternalVisible(newVisible)
}
// 隐藏引导时,停止当前 channel 的音频
if (!newVisible) {
stop();
stop()
}
onVisibleChange?.(newVisible);
};
onVisibleChange?.(newVisible)
}
// 受控 visible 场景:当外部把 visible 置为 false 时,也需要停止音频
useEffect(() => {
if (!isVisible) {
stop();
stop()
}
}, [isVisible, stop]);
}, [isVisible, stop])
const handleNext = (e: React.MouseEvent) => {
e.stopPropagation(); // 阻止事件冒泡
e.stopPropagation() // 阻止事件冒泡
if (currentStep < totalSteps - 1) {
updateStep(currentStep + 1);
updateStep(currentStep + 1)
}
};
}
const handleTextClick = () => {
if (currentStep < totalSteps - 1) {
updateStep(currentStep + 1);
updateStep(currentStep + 1)
} else {
// 最后一步,隐藏组件
updateVisible(false);
updateVisible(false)
}
};
}
// 获取当前步骤的高亮选择器
const currentHighlightSelectors =
highlightSelectors && highlightSelectors[currentStep]
? highlightSelectors[currentStep]
: undefined;
: undefined
// 根据 speaker 选择背景图片
const currentBackgroundPath =
currentSpeaker === "student" ? "components/引导板2" : "components/引导板";
currentSpeaker === 'student' ? 'components/引导板2' : 'components/引导板'
// GuideSteps 内容
const guideStepsContent = (
@@ -183,15 +169,12 @@ const GuideSteps = ({
{isVisible && (
<motion.div
key="guide-steps-container"
className={cn(
"fixed inset-0 flex items-center justify-center",
className
)}
className={cn('fixed inset-0 flex items-center justify-center', className)}
style={{ zIndex: zIndex + 1 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
onClick={handleTextClick}
>
{/* 引导板容器和背景 - 作为整体切换,确保 padding 和背景同步 */}
@@ -201,13 +184,13 @@ const GuideSteps = ({
<motion.div
key={`container-${currentSpeaker}`}
className={cn(
"absolute bottom-0 w-[1764px] h-[656px] pt-75 pb-10",
currentSpeaker === "student" ? "pl-50 pr-40" : "pr-80"
'absolute bottom-0 w-[1764px] h-[656px] pt-75 pb-10',
currentSpeaker === 'student' ? 'pl-50 pr-40' : 'pr-80'
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
{/* 背景层 */}
<div
@@ -226,7 +209,7 @@ const GuideSteps = ({
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<div className="text-[#4e402e] text-4xl font-bold leading-15">
{steps[currentStep]}
@@ -241,9 +224,7 @@ const GuideSteps = ({
>
继续
</span>
<span className="text-[#8B7355] text-2xl animate-pulse">
→
</span>
<span className="text-[#8B7355] text-2xl animate-pulse">→</span>
</div>
)}
</motion.div>
@@ -254,7 +235,7 @@ const GuideSteps = ({
</motion.div>
)}
</AnimatePresence>
);
)
// 始终使用 Overlay 包裹,showMask 由 Overlay 处理
return (
@@ -266,11 +247,11 @@ const GuideSteps = ({
>
{guideStepsContent}
</Overlay>
);
};
)
}
const GuideStepsItem = ({ children }: GuideStepsItemProps) => {
return <>{children}</>;
};
return <>{children}</>
}
export { GuideSteps, GuideStepsItem };
export { GuideSteps, GuideStepsItem }