feat: 更新图像资源和引导对话,提升学习体验

- 替换多个场景中的图像资源,确保视觉内容的准确性和一致性。
- 更新信件组件中的输入校验功能,提升用户填写体验。
- 调整引导对话文本,增强互动性和信息传达的清晰度。
- 引入结束窗口,鼓励用户深入了解鲁迅和孙中山的历史背景。
This commit is contained in:
zhongyidian committed 2026-01-18 01:41:53 +08:00
1 parent 5bdfdadede
commit ac7bb420fd
69 files changed
+1166 -442

No files matched your search

+346 -95
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image } from "@/components";
import { Image, Select } from "@/components";
export interface LetterToLuxunProps {
/** 是否可见 */
@@ -23,15 +23,88 @@ const LetterToLuxun = ({
}: LetterToLuxunProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([]);
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
// 填空类型定义:0=填空1(限填),1=填空2(限填),其他=任填
const BLANK_1_ANSWER = "首在立人";
const BLANK_2_ANSWER = "批判求真";
// 填空1的选项
const BLANK_1_OPTIONS = [
"首在立人",
// 可以添加其他选项
];
// 填空2的选项
const BLANK_2_OPTIONS = [
"批判求真",
// 可以添加其他选项
];
// 校验输入:确保所有填空符合标准
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
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();
if (i === 0) {
// 填空1:限填,必须选择"首在立人"
if (!v || v !== BLANK_1_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
} else if (i === 1) {
// 填空2:限填,必须选择"批判求真"
if (!v || v !== BLANK_2_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
} else {
// 其他填空:任填,不能为空,不超过 10 个字
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
}
}
setBlankValues(nextValues);
setBlankErrors(nextErrors);
return !hasError;
};
// 弹窗关闭后重置内容,方便下次重新填写
useEffect(() => {
if (!visible) {
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
}
}, [visible]);
// 信件内容模板(根据设计稿)
const letterContent = `敬爱的鲁迅先生:
您好!
今天,我怀着崇敬的心情走进大陆新村9号,看着您亲手撰写的《呐喊》《且介亭杂文》《故事新编》等作品,您编辑的《萌芽》《前哨》等杂志,您翻译的《艺术论》《死魂灵》等译文,仿佛能看到您"舌将上下而求索"的身影。在这场跨越时空的对话中,我终于明白您坚守的"____",不是抽象的理念,而是我们安身立命的根本;您秉持的"____",不是无谓的锋芒,而是我们直面现实的勇气。
今天,我怀着崇敬的心情走进大陆新村9号,看着您亲手撰写的《呐喊》《且介亭杂文》《故事新编》等作品,您编辑的《萌芽》《前哨》等杂志,您翻译的《艺术论》《死魂灵》等译文,仿佛能看到您"吾将上下而求索"的身影。在这场跨越时空的对话中,我终于明白您坚守的"____",不是抽象的理念,而是我们安身立命的根本;您秉持的"____",不是无谓的锋芒,而是我们直面现实的勇气。
我是____专业的学生,今后,我会以您"____干夫指"的骨气,在是非面前坚守立场,不随波逐流;我会以您"俯首甘为____"的热忱,参与____活动,用微光温暖他人;我更会记住您"____,走的人多了,便成了路"的箴言,在专业领域勇于探索,努力用____为民族复兴、文化繁荣添砖加瓦。
我是____专业的学生,今后,我会以您"____千夫指"的骨气,在是非面前坚守立场,不随波逐流;我会以您"俯首甘为____"的热忱,参与____活动,用微光温暖他人;我更会记住您"____,走的人多了,便成了路"的箴言,在专业领域勇于探索,努力用____为民族复兴、文化繁荣添砖加瓦。
请您放心,您播下的"立人"火种,我们会让它在新时代燎原;您毕生追求的"人国"愿景,我们会用青春和奋斗去守护!`;
@@ -65,112 +138,290 @@ const LetterToLuxun = ({
<div
className="relative w-[1200px] h-full pointer-events-auto"
onClick={(e) => e.stopPropagation()}
style={{ perspective: "1200px" }}
>
{/* 信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
alt="信纸"
className="absolute inset-0 w-full h-full object-contain"
/>
{/* 标题 - 垂直显示在左侧 */}
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
{/* 编辑态 / 只读态: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" }}
>
{/* 前面:编辑态 */}
<div
className="text-white text-6xl font-bold tracking-[20px]"
className="absolute inset-0"
style={{
writingMode: "vertical-rl",
textOrientation: "upright",
backfaceVisibility: "hidden",
// 编辑态展示时才响应事件,避免与背面点击冲突
pointerEvents: submitted ? "none" : "auto",
}}
>
写给鲁迅先生的信
</div>
</div>
{/* 编辑态:信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
alt="信纸"
className="absolute inset-0 w-full h-full object-contain"
/>
{/* 信件内容区域 */}
<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;
}
{/* 标题 - 垂直显示在左侧 */}
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
<div
className="text-white text-6xl font-bold tracking-[20px]"
style={{
writingMode: "vertical-rl",
textOrientation: "upright",
}}
>
写给鲁迅先生的信
</div>
</div>
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
style={needsIndent ? { textIndent: "2em" } : {}}
>
{segments.map((part, partIndex) => {
const isLast = partIndex === segments.length - 1;
if (isLast) {
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
</span>
);
{/* 信件内容区域(可编辑) */}
<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;
}
const currentIndex = placeholderIndex++;
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const segments = line.split("____");
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
<input
className="inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed border-[#4e402e] text-[24px] text-center outline-none"
value={blankValues[currentIndex] ?? ""}
onChange={(e) => {
const next = [...blankValues];
next[currentIndex] = e.target.value;
setBlankValues(next);
}}
/>
</span>
<div
key={lineIndex}
className="mb-2"
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 === 1; // 填空1和填空2为限填
const options =
currentIndex === 0
? BLANK_1_OPTIONS
: BLANK_2_OPTIONS;
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);
}
}}
placeholder="请选择"
error={blankErrors[currentIndex]}
zIndex={10000}
fitContent={true}
/>
) : (
// 任填:文本输入
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? "border-red-500"
: "border-[#4e402e]"
}`}
value={
blankValues[currentIndex] ?? ""
}
onChange={(e) => {
const next = [...blankValues];
next[currentIndex] = e.target.value;
setBlankValues(next);
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
/>
)}
</span>
);
})}
</div>
);
})}
</div>
);
});
})()}
});
})()}
</div>
</div>
{/* 笔的图片 */}
<div className="absolute bottom-[150px] -right-30">
<img
src={getStaticImage("components/letter/笔")}
alt="笔"
className="w-full h-full object-contain"
/>
</div>
{/* 提交按钮:切换到只读信件态 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
alt="可提交状态"
onClick={() => {
if (!validateBlanks()) return;
setSubmitted(true);
}}
button
/>
</div>
{/* 寄信图标(可选,如果需要的话) */}
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
<img
src={getStaticImage("components/letter/寄信图标")}
alt="寄信"
className="w-full h-full object-contain"
/>
</div>
</div>
</div>
{/* 笔的图片 */}
<div className="absolute bottom-[150px] -right-30">
<img
src={getStaticImage("components/letter/笔")}
alt="笔"
className="w-full h-full object-contain"
/>
</div>
{/* 背面:只读态 */}
<div
className="absolute inset-0"
style={{
transform: "rotateY(180deg)",
backfaceVisibility: "hidden",
// 只读态展示时才响应事件
pointerEvents: submitted ? "auto" : "none",
}}
>
{/* 只读态:新背景 */}
<img
src={getStaticImage("components/letter/信")}
alt="信件"
className="absolute inset-0 w-full h-full object-contain"
/>
<Image
src={getStaticImage("components/close")}
alt="信纸"
className="absolute right-38 top-0"
button
onClick={() => {
setSubmitted(false);
}}
/>
{/* 提交按钮 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
alt="可提交状态"
onClick={() => onSubmit?.(blankValues)}
button
/>
</div>
{/* 信件内容区域(只读) */}
<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;
}
{/* 寄信图标(可选,如果需要的话) */}
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
<img
src={getStaticImage("components/letter/寄信图标")}
alt="寄信"
className="w-full h-full object-contain"
/>
</div>
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
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] ?? "";
return (
<span
key={`part-${lineIndex}-${partIndex}`}
>
{part}
{value || "  "}
</span>
);
})}
</div>
);
});
})()}
</div>
</div>
{/* 寄出按钮:关闭窗口 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/寄出")}
alt="寄出"
onClick={() => {
onSubmit?.(blankValues);
}}
button
/>
</div>
</div>
</motion.div>
</div>
</motion.div>
</>
+261 -96
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image } from "@/components";
@@ -23,6 +23,47 @@ const LetterToSunzhongshan = ({
}: LetterToSunzhongshanProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([]);
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
// 校验输入:不能为空、每个不超过 10 个字符
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
const nextErrors: boolean[] = Array(placeholderCount).fill(false);
const nextValues: string[] = [...blankValues];
let hasError = false;
for (let i = 0; i < placeholderCount; i++) {
const raw = blankValues[i] || "";
const v = raw.trim();
// 为空 或 超过 10 个字,都视为不符合要求:清空并标红
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
}
setBlankValues(nextValues);
setBlankErrors(nextErrors);
return !hasError;
};
// 弹窗关闭后重置内容,方便下次重新填写
useEffect(() => {
if (!visible) {
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
}
}, [visible]);
// 信件内容模板(根据设计稿)
const letterContent = `敬爱的孙中山先生:
@@ -65,112 +106,236 @@ const LetterToSunzhongshan = ({
<div
className="relative w-[1200px] h-full pointer-events-auto"
onClick={(e) => e.stopPropagation()}
style={{ perspective: "1200px" }}
>
{/* 信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
alt="信纸"
className="absolute inset-0 w-full h-full object-contain"
/>
{/* 标题 - 垂直显示在左侧 */}
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
{/* 编辑态 / 只读态: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" }}
>
{/* 前面:编辑态 */}
<div
className="text-white text-6xl font-bold tracking-[20px]"
className="absolute inset-0"
style={{
writingMode: "vertical-rl",
textOrientation: "upright",
backfaceVisibility: "hidden",
// 编辑态展示时才响应事件,避免与背面点击冲突
pointerEvents: submitted ? "none" : "auto",
}}
>
写给孙中山先生的信
{/* 编辑态:信纸背景 */}
<img
src={getStaticImage("components/letter/信底")}
alt="信纸"
className="absolute inset-0 w-full h-full object-contain"
/>
{/* 标题 - 垂直显示在左侧 */}
<div className="absolute left-[160px] top-20 bottom-0 [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
<div
className="text-white text-6xl font-bold tracking-[20px]"
style={{
writingMode: "vertical-rl",
textOrientation: "upright",
}}
>
写给孙中山先生的信
</div>
</div>
{/* 信件内容区域(可编辑) */}
<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;
}
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
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++;
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
<input
name={`blank-${currentIndex}`}
className={`inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed [font-size:26px_!important] text-center outline-none ${
blankErrors[currentIndex]
? "border-red-500"
: "border-[#4e402e]"
}`}
value={blankValues[currentIndex] ?? ""}
onChange={(e) => {
const next = [...blankValues];
next[currentIndex] = e.target.value;
setBlankValues(next);
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
/>
</span>
);
})}
</div>
);
});
})()}
</div>
</div>
{/* 笔的图片 */}
<div className="absolute bottom-[150px] -right-30">
<img
src={getStaticImage("components/letter/笔")}
alt="笔"
className="w-full h-full object-contain"
/>
</div>
{/* 提交按钮:切换到只读信件态 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
alt="可提交状态"
onClick={() => {
if (!validateBlanks()) return;
setSubmitted(true);
}}
button
/>
</div>
{/* 寄信图标(可选,如果需要的话) */}
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
<img
src={getStaticImage("components/letter/寄信图标")}
alt="寄信"
className="w-full h-full object-contain"
/>
</div>
</div>
</div>
{/* 信件内容区域 */}
<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;
}
{/* 背面:只读态 */}
<div
className="absolute inset-0"
style={{
transform: "rotateY(180deg)",
backfaceVisibility: "hidden",
// 只读态展示时才响应事件
pointerEvents: submitted ? "auto" : "none",
}}
>
{/* 只读态:新背景 */}
<img
src={getStaticImage("components/letter/信")}
alt="信件"
className="absolute inset-0 w-full h-full object-contain"
/>
<Image
src={getStaticImage("components/close")}
alt="信纸"
className="absolute right-38 top-0"
button
onClick={() => {
setSubmitted(false);
}}
/>
// "您好"行之后的非空行需要首行缩进
const needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
{/* 信件内容区域(只读) */}
<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;
}
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
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 needsIndent =
foundNiHao &&
line.trim() !== "" &&
!line.includes("您好");
const currentIndex = placeholderIndex++;
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
<input
className="inline-block w-[100px] h-[36px] mx-2 align-middle bg-transparent border-b-2 border-dashed border-[#4e402e] text-[24px] text-center outline-none"
value={blankValues[currentIndex] ?? ""}
onChange={(e) => {
const next = [...blankValues];
next[currentIndex] = e.target.value;
setBlankValues(next);
}}
/>
</span>
);
})}
</div>
);
});
})()}
const segments = line.split("____");
return (
<div
key={lineIndex}
className="mb-2"
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] ?? "";
return (
<span key={`part-${lineIndex}-${partIndex}`}>
{part}
{value || "  "}
</span>
);
})}
</div>
);
});
})()}
</div>
</div>
{/* 寄出按钮:关闭窗口 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/寄出")}
alt="寄出"
onClick={() => {
onSubmit?.(blankValues);
}}
button
/>
</div>
</div>
</div>
{/* 笔的图片 */}
<div className="absolute bottom-[150px] -right-30">
<img
src={getStaticImage("components/letter/笔")}
alt="笔"
className="w-full h-full object-contain"
/>
</div>
{/* 提交按钮 */}
<div className="absolute bottom-[80px] left-1/2 transform -translate-x-1/2 z-99">
<Image
src={getStaticImage("components/letter/可提交状态")}
alt="可提交状态"
onClick={() => onSubmit?.(blankValues)}
button
/>
</div>
{/* 寄信图标(可选,如果需要的话) */}
<div className="absolute top-[50px] right-[50px] w-[100px] h-[100px] opacity-80">
<img
src={getStaticImage("components/letter/寄信图标")}
alt="寄信"
className="w-full h-full object-contain"
/>
</div>
</motion.div>
</div>
</motion.div>
</>