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

+180 -224
View File
@@ -1,108 +1,104 @@
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image, Select } from "@/components";
import { filterInput } from "@/lib/utils/sensitiveWords";
import { useEffect, useState } from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import { getStaticImage } from '@/lib/utils'
import { Image, Select } from '@/components'
import { filterInput } from '@/lib/utils/sensitiveWords'
export interface LetterToHuangyanpeiProps {
/** 是否可见 */
visible?: boolean;
visible?: boolean
/** 关闭回调 */
onClose?: () => void;
onClose?: () => void
/** 提交回调 */
onSubmit?: (values: string[]) => void;
onSubmit?: (values: string[]) => void
}
/**
* LetterToHuangyanpei 组件 - 写给黄炎培先生的信
* 包含飞入和放大动画
*/
const LetterToHuangyanpei = ({
visible = false,
onClose,
onSubmit,
}: LetterToHuangyanpeiProps) => {
const LetterToHuangyanpei = ({ visible = false, onClose, onSubmit }: LetterToHuangyanpeiProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([]);
const [blankValues, setBlankValues] = useState<string[]>([])
// 每个空的校验状态:true 表示该空当前不符合要求
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
const [blankErrors, setBlankErrors] = useState<boolean[]>([])
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
const [submitted, setSubmitted] = useState(false)
// 填空类型定义:0=填空1(限填),1=填空2(任填),2=填空3(任填),3=填空4(限填)
const BLANK_1_ANSWER = "手脑并用";
const BLANK_4_ANSWER = "有业者乐业";
const BLANK_1_ANSWER = '手脑并用'
const BLANK_4_ANSWER = '有业者乐业'
// 填空1的选项
const BLANK_1_OPTIONS = [
"手脑并用",
"有业者乐业",
'手脑并用',
'有业者乐业',
// "知行合一",
// "学以致用",
// "理论实践"
];
]
// 填空4的选项
const BLANK_4_OPTIONS = [
"手脑并用",
"有业者乐业",
'手脑并用',
'有业者乐业',
// "有业者敬业",
// "有业者精业",
// "有业者成业",
];
]
// 校验输入:确保所有填空符合标准
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
const placeholderCount = (letterContent.match(/____/g) || []).length
const nextErrors: boolean[] = Array(placeholderCount).fill(false);
const nextValues: string[] = [...blankValues];
let hasError = false;
const nextErrors: boolean[] = Array(placeholderCount).fill(false)
const nextValues: string[] = [...blankValues]
let hasError = false
for (let i = 0; i < placeholderCount; i++) {
const raw = nextValues[i] || "";
const v = raw.trim();
const raw = nextValues[i] || ''
const v = raw.trim()
if (i === 0) {
// 填空1:限填,必须选择"手脑并用"
if (!v || v !== BLANK_1_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
nextValues[i] = ''
nextErrors[i] = true
hasError = true
}
} else if (i === 1 || i === 2) {
// 填空2、3:任填,不能为空,不超过20个字
if (!v || v.length > 20) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
nextValues[i] = ''
nextErrors[i] = true
hasError = true
}
} else if (i === 3) {
// 填空4:限填,必须选择"有业者乐业"
if (!v || v !== BLANK_4_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
nextValues[i] = ''
nextErrors[i] = true
hasError = true
}
}
}
setBlankValues(nextValues);
setBlankErrors(nextErrors);
setBlankValues(nextValues)
setBlankErrors(nextErrors)
return !hasError;
};
return !hasError
}
// 弹窗关闭后重置内容,方便下次重新填写
useEffect(() => {
if (!visible) {
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
setSubmitted(false)
setBlankValues([])
setBlankErrors([])
}
}, [visible]);
}, [visible])
// 信件内容模板(根据设计稿)
const letterContent = `敬爱的黄炎培先生:
@@ -115,7 +111,7 @@ const LetterToHuangyanpei = ({
我是____专业的学生,今后,我会在实训课堂上精益求精,像当年中华职校的学长学姐那样,把技能练到极致;我会在志愿服务中践行担当,用所学知识为身边人、为社会做实事;我更会紧跟时代步伐,努力成为____领域的技能人才,为中国制造升级、为民族产业振兴添砖加瓦!
请您放心,您播下的职教火种,我们会让它在新时代燃烧得更旺;您毕生追求的"无业者有业、____"的愿景,我们会用青春和汗水去实现!`;
请您放心,您播下的职教火种,我们会让它在新时代燃烧得更旺;您毕生追求的"无业者有业、____"的愿景,我们会用青春和汗水去实现!`
return (
<AnimatePresence>
@@ -138,7 +134,7 @@ const LetterToHuangyanpei = ({
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.5, y: 100 }}
transition={{
type: "spring",
type: 'spring',
stiffness: 300,
damping: 30,
duration: 0.6,
@@ -147,28 +143,28 @@ const LetterToHuangyanpei = ({
<div
className="relative w-[1200px] h-full pointer-events-auto"
onClick={(e) => e.stopPropagation()}
style={{ perspective: "1200px" }}
style={{ perspective: '1200px' }}
>
{/* 编辑态 / 只读态:Y 轴翻转切换 */}
<motion.div
className="relative w-full h-full"
initial={false}
animate={{ rotateY: submitted ? 180 : 0 }}
transition={{ duration: 0.7, ease: "easeInOut" }}
style={{ transformStyle: "preserve-3d" }}
transition={{ duration: 0.7, ease: 'easeInOut' }}
style={{ transformStyle: 'preserve-3d' }}
>
{/* 前面:编辑态 */}
<div
className="absolute inset-0"
style={{
backfaceVisibility: "hidden",
backfaceVisibility: 'hidden',
// 编辑态展示时才响应事件,避免与背面点击冲突
pointerEvents: submitted ? "none" : "auto",
pointerEvents: submitted ? 'none' : 'auto',
}}
>
{/* 编辑态:信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
src={getStaticImage('components/letter/信底')}
alt="信纸"
className="absolute inset-0 w-full h-full object-contain"
/>
@@ -178,8 +174,8 @@ const LetterToHuangyanpei = ({
<div
className="text-white text-6xl font-bold tracking-[20px]"
style={{
writingMode: "vertical-rl",
textOrientation: "upright",
writingMode: 'vertical-rl',
textOrientation: 'upright',
}}
>
写给黄炎培先生的信
@@ -190,121 +186,97 @@ const LetterToHuangyanpei = ({
<div className="absolute w-[700px] left-[320px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent
.split("\n")
.map((line, lineIndex) => {
// 检测是否已经找到"您好"行
if (line.includes("您好")) {
foundNiHao = true;
}
let placeholderIndex = 0
let foundNiHao = false
return letterContent.split('\n').map((line, lineIndex) => {
// 检测是否已经找到"您好"行
if (line.includes('您好')) {
foundNiHao = true
}
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao && line.trim() !== '' && !line.includes('您好')
const segments = line.split("____");
return (
<div
key={lineIndex}
style={needsIndent ? { textIndent: "2em" } : {}}
>
{segments.map((part, partIndex) => {
const isLast =
partIndex === segments.length - 1;
if (isLast) {
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
</span>
);
}
const segments = line.split('____')
return (
<div key={lineIndex} style={needsIndent ? { textIndent: '2em' } : {}}>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1
if (isLast) {
return <span key={`part-${lineIndex}-${partIndex}`}>{part}</span>
}
const currentIndex = placeholderIndex++;
const isLimitedFill =
currentIndex === 0 || currentIndex === 3; // 填空1和填空4为限填
const options =
currentIndex === 0
? BLANK_1_OPTIONS
: BLANK_4_OPTIONS;
const currentIndex = placeholderIndex++
const isLimitedFill = currentIndex === 0 || currentIndex === 3 // 填空1和填空4为限填
const options =
currentIndex === 0 ? BLANK_1_OPTIONS : BLANK_4_OPTIONS
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{isLimitedFill ? (
// 限填:下拉选择
<Select
className="inline"
options={options.map((opt) => ({
value: opt,
label: opt,
}))}
value={
blankValues[currentIndex] ?? ""
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
{isLimitedFill ? (
// 限填:下拉选择
<Select
className="inline"
options={options.map((opt) => ({
value: opt,
label: opt,
}))}
value={blankValues[currentIndex] ?? ''}
onChange={(selectedValue) => {
const next = [...blankValues]
next[currentIndex] = selectedValue
setBlankValues(next)
// 用户再次选择时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors]
nextErrs[currentIndex] = false
setBlankErrors(nextErrs)
}
onChange={(selectedValue) => {
const next = [...blankValues];
next[currentIndex] = selectedValue;
setBlankValues(next);
}}
placeholder="请选择"
error={blankErrors[currentIndex]}
zIndex={10000}
fitContent={true}
/>
) : (
// 任填:文本输入,限制20个字,敏感词过滤
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? 'border-red-500'
: 'border-[#4e402e]'
}`}
autoComplete="off"
value={blankValues[currentIndex] ?? ''}
maxLength={20}
onChange={(e) => {
const rawValue = e.target.value
// 应用敏感词过滤
const filteredValue = filterInput(rawValue)
// 用户再次选择时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
placeholder="请选择"
error={blankErrors[currentIndex]}
zIndex={10000}
fitContent={true}
/>
) : (
// 任填:文本输入,限制20个字,敏感词过滤
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? "border-red-500"
: "border-[#4e402e]"
}`}
autoComplete="off"
value={
blankValues[currentIndex] ?? ""
const next = [...blankValues]
next[currentIndex] = filteredValue
setBlankValues(next)
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors]
nextErrs[currentIndex] = false
setBlankErrors(nextErrs)
}
maxLength={20}
onChange={(e) => {
const rawValue = e.target.value;
// 应用敏感词过滤
const filteredValue =
filterInput(rawValue);
const next = [...blankValues];
next[currentIndex] = filteredValue;
setBlankValues(next);
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
/>
)}
</span>
);
})}
</div>
);
});
}}
/>
)}
</span>
)
})}
</div>
)
})
})()}
</div>
</div>
@@ -312,7 +284,7 @@ const LetterToHuangyanpei = ({
{/* 笔的图片 */}
<div className="absolute bottom-[150px] -right-30">
<img
src={getStaticImage("components/letter/笔")}
src={getStaticImage('components/letter/笔')}
alt="笔"
className="w-full h-full object-contain"
/>
@@ -321,11 +293,11 @@ const LetterToHuangyanpei = ({
{/* 提交按钮:切换到只读信件态 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
src={getStaticImage('components/letter/可提交状态')}
alt="可提交状态"
onClick={() => {
if (!validateBlanks()) return;
setSubmitted(true);
if (!validateBlanks()) return
setSubmitted(true)
}}
button
/>
@@ -336,25 +308,25 @@ const LetterToHuangyanpei = ({
<div
className="absolute inset-0"
style={{
transform: "rotateY(180deg)",
backfaceVisibility: "hidden",
transform: 'rotateY(180deg)',
backfaceVisibility: 'hidden',
// 只读态展示时才响应事件
pointerEvents: submitted ? "auto" : "none",
pointerEvents: submitted ? 'auto' : 'none',
}}
>
{/* 只读态:新背景 */}
<img
src={getStaticImage("components/letter/信")}
src={getStaticImage('components/letter/信')}
alt="信件"
className="absolute inset-0 w-full h-full object-contain"
/>
<Image
src={getStaticImage("components/close")}
src={getStaticImage('components/close')}
alt="信纸"
className="absolute right-38 top-0"
button
onClick={() => {
setSubmitted(false);
setSubmitted(false)
}}
/>
@@ -362,54 +334,38 @@ const LetterToHuangyanpei = ({
<div className="absolute w-[700px] left-[260px] right-[180px] top-[80px] bottom-[180px] z-99">
<div className="text-[#4e402e] text-[26px] leading-relaxed">
{(() => {
let placeholderIndex = 0;
let foundNiHao = false;
return letterContent
.split("\n")
.map((line, lineIndex) => {
if (line.includes("您好")) {
foundNiHao = true;
}
let placeholderIndex = 0
let foundNiHao = false
return letterContent.split('\n').map((line, lineIndex) => {
if (line.includes('您好')) {
foundNiHao = true
}
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const needsIndent =
foundNiHao && line.trim() !== '' && !line.includes('您好')
const segments = line.split("____");
return (
<div
key={lineIndex}
style={needsIndent ? { textIndent: "2em" } : {}}
>
{segments.map((part, partIndex) => {
const isLast =
partIndex === segments.length - 1;
if (isLast) {
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
</span>
);
}
const segments = line.split('____')
return (
<div key={lineIndex} style={needsIndent ? { textIndent: '2em' } : {}}>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1
if (isLast) {
return <span key={`part-${lineIndex}-${partIndex}`}>{part}</span>
}
const currentIndex = placeholderIndex++;
const value = blankValues[currentIndex] ?? "";
const currentIndex = placeholderIndex++
const value = blankValues[currentIndex] ?? ''
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{value || "  "}
</span>
);
})}
</div>
);
});
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
{value || '  '}
</span>
)
})}
</div>
)
})
})()}
</div>
</div>
@@ -417,12 +373,12 @@ const LetterToHuangyanpei = ({
{/* 寄出按钮:关闭窗口 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/寄出")}
src={getStaticImage('components/letter/寄出')}
alt="寄出"
onClick={() => {
// 寄出前再次校验,确保所有填空符合标准
if (validateBlanks()) {
onSubmit?.(blankValues);
onSubmit?.(blankValues)
}
}}
button
@@ -435,7 +391,7 @@ const LetterToHuangyanpei = ({
</>
)}
</AnimatePresence>
);
};
)
}
export default LetterToHuangyanpei;
export default LetterToHuangyanpei