feat: 更新组件和引导对话,增强用户体验

- 在多个组件中添加新的选择和填空功能,提升互动性。
- 更新引导对话文本,确保信息准确性和一致性。
- 引入新的选择组件,优化用户填写体验。
- 清理冗余代码,提升可维护性。
This commit is contained in:
zhongyidian committed 2026-01-16 17:54:51 +08:00
1 parent a8723ab4aa
commit 5bdfdadede
27 files changed
+880 -1252

No files matched your search

+157 -31
View File
@@ -1,7 +1,8 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { getStaticImage } from "@/lib/utils";
import { Image } from "@/components";
import { Image, Select } from "@/components";
import { filterInput } from "@/lib/utils/sensitiveWords";
export interface LetterToHuangyanpeiProps {
/** 是否可见 */
@@ -23,13 +24,71 @@ const LetterToHuangyanpei = ({
}: LetterToHuangyanpeiProps) => {
// 下划线可填写内容
const [blankValues, setBlankValues] = useState<string[]>([]);
// 每个空的校验状态:true 表示该空当前不符合要求(为空或超过 10 个字)
// 每个空的校验状态:true 表示该空当前不符合要求
const [blankErrors, setBlankErrors] = useState<boolean[]>([]);
// 是否已提交:提交后隐藏编辑信件,展示只读信件
const [submitted, setSubmitted] = useState(false);
// 校验输入:不能为空、每个不超过 10 个字符
// 用于跟踪是否已经自动填入过答案,避免重复填入
const autoFilledRef = useRef(false);
// 填空类型定义:0=填空1(限填),1=填空2(任填),2=填空3(任填),3=填空4(限填)
const BLANK_1_ANSWER = "手脑并用";
const BLANK_4_ANSWER = "有业者乐业";
// 填空1的选项
const BLANK_1_OPTIONS = [
"手脑并用",
// "知行合一",
// "学以致用",
// "理论实践"
];
// 填空4的选项
const BLANK_4_OPTIONS = [
"有业者乐业",
// "有业者敬业",
// "有业者精业",
// "有业者成业",
];
// 自动浮现答案:填空1、4空白时自动填入正确答案
useEffect(() => {
// 只在组件可见且未自动填入过时检查
if (!visible) {
autoFilledRef.current = false;
return;
}
if (autoFilledRef.current) return;
setBlankValues((prevValues) => {
const nextValues = [...prevValues];
let changed = false;
// 填空1(索引0):如果为空,自动填入正确答案
if (!nextValues[0] || nextValues[0].trim() === "") {
nextValues[0] = BLANK_1_ANSWER;
changed = true;
}
// 填空4(索引3):如果为空,自动填入正确答案
if (!nextValues[3] || nextValues[3].trim() === "") {
nextValues[3] = BLANK_4_ANSWER;
changed = true;
}
if (changed || nextValues.length > 0) {
// 如果已经有值或已更改,标记为已处理,避免重复检查
autoFilledRef.current = true;
}
return nextValues;
});
}, [visible]);
// 校验输入:确保所有填空符合标准
const validateBlanks = () => {
// 计算占位总数,防止有些没被输入过而是 undefined
const placeholderCount = (letterContent.match(/____/g) || []).length;
@@ -39,14 +98,30 @@ const LetterToHuangyanpei = ({
let hasError = false;
for (let i = 0; i < placeholderCount; i++) {
const raw = blankValues[i] || "";
const raw = nextValues[i] || "";
const v = raw.trim();
// 为空 或 超过 10 个字,都视为不符合要求:清空并标红
if (!v || v.length > 10) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
if (i === 0) {
// 填空1:限填,必须选择"手脑并用"
if (!v || v !== BLANK_1_ANSWER) {
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;
}
} else if (i === 3) {
// 填空4:限填,必须选择"有业者乐业"
if (!v || v !== BLANK_4_ANSWER) {
nextValues[i] = "";
nextErrors[i] = true;
hasError = true;
}
}
}
@@ -62,6 +137,7 @@ const LetterToHuangyanpei = ({
setSubmitted(false);
setBlankValues([]);
setBlankErrors([]);
autoFilledRef.current = false; // 重置自动填入标记
}
}, [visible]);
@@ -187,32 +263,79 @@ const LetterToHuangyanpei = ({
}
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}
<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]"
}`}
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);
{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}
/>
) : (
// 任填:文本输入,限制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);
const next = [...blankValues];
next[currentIndex] = filteredValue;
setBlankValues(next);
// 用户再次输入时,清除该空的错误高亮
if (blankErrors[currentIndex]) {
const nextErrs = [...blankErrors];
nextErrs[currentIndex] = false;
setBlankErrors(nextErrs);
}
}}
/>
)}
</span>
);
})}
@@ -334,7 +457,10 @@ const LetterToHuangyanpei = ({
src={getStaticImage("components/letter/寄出")}
alt="寄出"
onClick={() => {
onSubmit?.(blankValues);
// 寄出前再次校验,确保所有填空符合标准
if (validateBlanks()) {
onSubmit?.(blankValues);
}
}}
button
/>