|
|
@@ -12,7 +12,7 @@ const getNow = (): number => {
|
|
|
// #ifdef MP-WEIXIN
|
|
|
return Date.now();
|
|
|
// #endif
|
|
|
-
|
|
|
+
|
|
|
// #ifndef MP-WEIXIN
|
|
|
if (typeof performance !== 'undefined' && performance.now) {
|
|
|
return performance.now();
|
|
|
@@ -31,7 +31,7 @@ const requestAnimFrame = (() => {
|
|
|
return setTimeout(() => callback(Date.now()), 1000 / 60) as unknown as number;
|
|
|
};
|
|
|
// #endif
|
|
|
-
|
|
|
+
|
|
|
// #ifndef MP-WEIXIN
|
|
|
if (typeof requestAnimationFrame !== 'undefined') {
|
|
|
return requestAnimationFrame;
|
|
|
@@ -52,7 +52,7 @@ const cancelAnimFrame = (() => {
|
|
|
clearTimeout(id as unknown as NodeJS.Timeout);
|
|
|
};
|
|
|
// #endif
|
|
|
-
|
|
|
+
|
|
|
// #ifndef MP-WEIXIN
|
|
|
if (typeof cancelAnimationFrame !== 'undefined') {
|
|
|
return cancelAnimationFrame;
|
|
|
@@ -71,7 +71,7 @@ const cancelAnimFrame = (() => {
|
|
|
export const decodeHtmlEntities = (str: string): string => {
|
|
|
if (!str) return str;
|
|
|
|
|
|
- // 音标和常用 HTML 实体映射表
|
|
|
+ // 标准 HTML 实体映射表
|
|
|
const entityMap: Record<string, string> = {
|
|
|
// 音标相关 - 锐音符 (acute)
|
|
|
'aacute': 'á',
|
|
|
@@ -101,10 +101,9 @@ export const decodeHtmlEntities = (str: string): string => {
|
|
|
'ntilde': 'ñ',
|
|
|
'atilde': 'ã',
|
|
|
'otilde': 'õ',
|
|
|
+ 'aelig': 'æ',
|
|
|
+ 'eth': 'ð',
|
|
|
// 其他常用实体
|
|
|
- 'amp': '&',
|
|
|
- 'lt': '<',
|
|
|
- 'gt': '>',
|
|
|
'quot': '"',
|
|
|
'apos': "'",
|
|
|
'nbsp': '\u00A0',
|
|
|
@@ -114,29 +113,188 @@ export const decodeHtmlEntities = (str: string): string => {
|
|
|
'mdash': '—',
|
|
|
'ndash': '–',
|
|
|
'hellip': '…',
|
|
|
+ 'darr': '↓',
|
|
|
+ 'uarr': '↑',
|
|
|
+ 'rarr': '→',
|
|
|
+ 'larr': '←',
|
|
|
+ 'harr': '↔',
|
|
|
+ 'crarr': '↵',
|
|
|
+ 'lArr': '⇐',
|
|
|
+ 'rArr': '⇒',
|
|
|
+ 'hArr': '⇔',
|
|
|
+ 'laquo': '«',
|
|
|
+ 'raquo': '»',
|
|
|
+ 'ldquo': '“',
|
|
|
+ 'rdquo': '”',
|
|
|
+ 'lsquo': '‘',
|
|
|
+ 'rsquo': '’',
|
|
|
+ 'lsaquo': '‹',
|
|
|
+ 'rsaquo': '›',
|
|
|
// 数学符号
|
|
|
+ 'amp': '&',
|
|
|
+ 'minus': '−',
|
|
|
+ 'plus': '+',
|
|
|
'times': '×',
|
|
|
'divide': '÷',
|
|
|
'plusmn': '±',
|
|
|
+ 'deg': '°',
|
|
|
+ 'radic': '√',
|
|
|
+ 'isin': '∈',
|
|
|
+ 'infin': '∞',
|
|
|
+ 'there4': '∴',
|
|
|
+ 'because': '∵',
|
|
|
+ 'forall': '∀',
|
|
|
+ 'exists': '∃',
|
|
|
+ 'nexists': '∄',
|
|
|
+ 'notin': '∉',
|
|
|
+ 'ni': '∋',
|
|
|
+ 'empty': '∅',
|
|
|
+ 'prod': '∏',
|
|
|
+ 'sum': '∑',
|
|
|
+ 'prop': '∝',
|
|
|
+ 'sim': '∼',
|
|
|
+ 'simeq': '≃',
|
|
|
+ 'approx': '≈',
|
|
|
+ 'asymp': '≈',
|
|
|
+ 'neq': '≠',
|
|
|
+ 'equiv': '≡',
|
|
|
+ 'ne': '≠',
|
|
|
+ 'le': '≤',
|
|
|
+ 'ge': '≥',
|
|
|
+ 'll': '≪',
|
|
|
+ 'gg': '≫',
|
|
|
+ 'prec': '≺',
|
|
|
+ 'succ': '≻',
|
|
|
+ 'preceq': '≼',
|
|
|
+ 'succeq': '≽',
|
|
|
+ 'sub': '⊂',
|
|
|
+ 'sube': '⊆',
|
|
|
+ 'subset': '⊂',
|
|
|
+ 'subseteq': '⊆',
|
|
|
+ 'sup': '⊃',
|
|
|
+ 'supe': '⊇',
|
|
|
+ 'supset': '⊃',
|
|
|
+ 'supseteq': '⊇',
|
|
|
+ 'perp': '⊥',
|
|
|
+ 'parallel': '∥',
|
|
|
+ 'wedge': '∧',
|
|
|
+ 'vee': '∨',
|
|
|
+ 'cap': '∩',
|
|
|
+ 'cup': '∪',
|
|
|
+ 'int': '∫',
|
|
|
+ 'oint': '∮',
|
|
|
+ 'angle': '∠',
|
|
|
+ 'ang': '∠',
|
|
|
+ 'mid': '∣',
|
|
|
+ 'vdash': '⊢',
|
|
|
+ 'dashv': '⊣',
|
|
|
+ 'models': '⊨',
|
|
|
+ 'vDash': '⊩',
|
|
|
+ 'Vdash': '⊫',
|
|
|
+ 'VDash': '⊬',
|
|
|
+ 'Oslash': 'Ø',
|
|
|
+ 'ordm': 'º',
|
|
|
+ 'not': '¬',
|
|
|
+ 'prime': '′',
|
|
|
+ // 排版符号
|
|
|
+ 'middot': '·',
|
|
|
+ 'bull': '•',
|
|
|
+ 'sect': '§',
|
|
|
+ 'para': '¶',
|
|
|
+ 'macr': '¯',
|
|
|
+ 'micro': 'µ',
|
|
|
+ // 货币符号
|
|
|
+ 'yen': '¥',
|
|
|
+ 'euro': '€',
|
|
|
+ 'pound': '£',
|
|
|
+ 'cent': '¢',
|
|
|
+ // 希腊字母
|
|
|
+ 'alpha': 'α',
|
|
|
+ 'beta': 'β',
|
|
|
+ 'gamma': 'γ',
|
|
|
+ 'delta': 'δ',
|
|
|
+ 'epsilon': 'ε',
|
|
|
+ 'zeta': 'ζ',
|
|
|
+ 'eta': 'η',
|
|
|
+ 'theta': 'θ',
|
|
|
+ 'iota': 'ι',
|
|
|
+ 'kappa': 'κ',
|
|
|
+ 'lambda': 'λ',
|
|
|
+ 'mu': 'μ',
|
|
|
+ 'nu': 'ν',
|
|
|
+ 'xi': 'ξ',
|
|
|
+ 'omicron': 'ο',
|
|
|
+ 'pi': 'π',
|
|
|
+ 'rho': 'ρ',
|
|
|
+ 'sigma': 'σ',
|
|
|
+ 'tau': 'τ',
|
|
|
+ 'upsilon': 'υ',
|
|
|
+ 'phi': 'φ',
|
|
|
+ 'chi': 'χ',
|
|
|
+ 'psi': 'ψ',
|
|
|
+ 'omega': 'ω',
|
|
|
+ 'Omega': 'Ω',
|
|
|
+ 'thetasym': 'ϑ',
|
|
|
+ 'upsih': 'ϒ',
|
|
|
+ 'piv': 'ϖ',
|
|
|
};
|
|
|
|
|
|
- // 处理命名实体(如 í)
|
|
|
- // 使用 [a-z0-9] 以支持包含数字的实体名称
|
|
|
- let result = str.replace(/&([a-z0-9]+);/gi, (match, entity) => {
|
|
|
+ let result = str;
|
|
|
+
|
|
|
+ // 清除所有的特殊空格实体
|
|
|
+ result = result.replaceAll("‍", "")
|
|
|
+ .replaceAll(" ", "")
|
|
|
+ .replaceAll("‌", "")
|
|
|
+ .replaceAll(" ", "")
|
|
|
+ .replaceAll(" ", "")
|
|
|
+ .replaceAll(" ", "")
|
|
|
+ .replaceAll("­", "")
|
|
|
+ .replaceAll("​", "");
|
|
|
+
|
|
|
+ // 0. 保护真正的 HTML 标签,转义文本中的 < 和 >
|
|
|
+ // 策略:< 后面如果不是字母或 /(标签开始),则转义为 <
|
|
|
+ // 这样可以处理 x<2、1<x<5 等数学表达式
|
|
|
+ // result = result.replace(/<(?![a-zA-Z/])/g, '<'); // < 后面不是字母或 / 的转义
|
|
|
+ // result = result.replace(/(?<![a-zA-Z0-9])>/g, '>'); // > 前面不是字母数字的转义(避免破坏标签)
|
|
|
+
|
|
|
+ // 1. 处理非标准的上标实体:&sup数字; → <sup>数字</sup>
|
|
|
+ // 例如:² → <sup>2</sup>, ¹23; → <sup>123</sup>
|
|
|
+ result = result.replace(/&sup(\d+);/gi, (match, digits) => {
|
|
|
+ return `<sup>${digits}</sup>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 处理非标准的下标实体:&sub数字; → <sub>数字</sub>
|
|
|
+ // 例如:&sub2; → <sub>2</sub>, &sub123; → <sub>123</sub>
|
|
|
+ result = result.replace(/&sub(\d+);/gi, (match, digits) => {
|
|
|
+ return `<sub>${digits}</sub>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 处理标准命名实体(如 í、∴)
|
|
|
+ // 注意:正则需要支持字母和数字的组合
|
|
|
+ result = result.replace(/&([a-z0-9]+);/gi, (match, entity) => {
|
|
|
const lowerEntity = entity.toLowerCase();
|
|
|
if (entityMap[lowerEntity]) {
|
|
|
return entityMap[lowerEntity];
|
|
|
}
|
|
|
- return match; // 如果找不到映射,保持原样
|
|
|
+ return match;
|
|
|
});
|
|
|
|
|
|
- // 处理数字实体(如 í 或 í)
|
|
|
+ // 4. 处理十进制数字实体(如 ² → ²)
|
|
|
result = result.replace(/&#(\d+);/g, (match, num) => {
|
|
|
- return String.fromCharCode(parseInt(num, 10));
|
|
|
+ const code = parseInt(num, 10);
|
|
|
+ if (!isNaN(code)) {
|
|
|
+ return String.fromCharCode(code);
|
|
|
+ }
|
|
|
+ return match;
|
|
|
});
|
|
|
|
|
|
+ // 5. 处理十六进制数字实体(如 ² → ²)
|
|
|
result = result.replace(/&#x([0-9a-f]+);/gi, (match, hex) => {
|
|
|
- return String.fromCharCode(parseInt(hex, 16));
|
|
|
+ const code = parseInt(hex, 16);
|
|
|
+ if (!isNaN(code)) {
|
|
|
+ return String.fromCharCode(code);
|
|
|
+ }
|
|
|
+ return match;
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
@@ -384,7 +542,7 @@ export const useExam = () => {
|
|
|
answer1 = answer1 || '';
|
|
|
answer2 = answer2 || '';
|
|
|
if ([EnumQuestionType.SINGLE_CHOICE, EnumQuestionType.JUDGMENT].includes(typeId)) {
|
|
|
- return answer1.includes(answers[0]);
|
|
|
+ return answer1.trim() === answers[0];
|
|
|
} else if ([EnumQuestionType.MULTIPLE_CHOICE].includes(typeId)) {
|
|
|
return answers.length === answer1.length && answers.every(item => answer1.includes(item));
|
|
|
} else {
|
|
|
@@ -430,7 +588,9 @@ export const useExam = () => {
|
|
|
if (currentQuestion.value.activeSubIndex < currentQuestion.value.subQuestions.length - 1) {
|
|
|
currentQuestion.value.activeSubIndex++;
|
|
|
} else {
|
|
|
- currentIndex.value++;
|
|
|
+ const lastIndex = currentIndex.value + 1;
|
|
|
+ questionList.value[lastIndex].activeSubIndex = 0;
|
|
|
+ currentIndex.value = lastIndex;
|
|
|
}
|
|
|
}
|
|
|
// 上一题
|
|
|
@@ -442,7 +602,9 @@ export const useExam = () => {
|
|
|
if (currentQuestion.value.activeSubIndex > 0) {
|
|
|
currentQuestion.value.activeSubIndex--;
|
|
|
} else {
|
|
|
- currentIndex.value--;
|
|
|
+ const prevIndex = currentIndex.value - 1;
|
|
|
+ questionList.value[prevIndex].activeSubIndex = questionList.value[prevIndex].subQuestions.length - 1;
|
|
|
+ currentIndex.value = prevIndex;
|
|
|
}
|
|
|
} else {
|
|
|
if (currentIndex.value > 0) {
|
|
|
@@ -494,12 +656,12 @@ export const useExam = () => {
|
|
|
// 开始计时
|
|
|
const startTiming = () => {
|
|
|
startCount();
|
|
|
-
|
|
|
+
|
|
|
// 记录开始时间戳(毫秒)
|
|
|
if (practiceStartTime === 0) {
|
|
|
practiceStartTime = getNow();
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 使用 requestAnimFrame 更新显示,更流畅且性能更好
|
|
|
// 兼容微信小程序环境
|
|
|
const updatePracticeDuration = () => {
|
|
|
@@ -511,20 +673,20 @@ export const useExam = () => {
|
|
|
animationFrameId = requestAnimFrame(updatePracticeDuration);
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
// 开始动画帧循环
|
|
|
animationFrameId = requestAnimFrame(updatePracticeDuration);
|
|
|
}
|
|
|
// 停止计时
|
|
|
const stopTiming = () => {
|
|
|
stopCount();
|
|
|
-
|
|
|
+
|
|
|
// 取消动画帧
|
|
|
if (animationFrameId !== null) {
|
|
|
cancelAnimFrame(animationFrameId);
|
|
|
animationFrameId = null;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 如果正在计时,累加经过的时间
|
|
|
if (practiceStartTime > 0) {
|
|
|
const elapsed = (getNow() - practiceStartTime) / 1000;
|
|
|
@@ -625,6 +787,7 @@ export const useExam = () => {
|
|
|
const transerQuestion = (item: Study.ExamineeQuestion, index: number): Study.Question => {
|
|
|
return {
|
|
|
...item,
|
|
|
+ title: decodeHtmlEntities(item.title || ''),
|
|
|
// 处理没有题型的大题,统一作为阅读题
|
|
|
typeId: (item.typeId === null || item.typeId === undefined) ? EnumQuestionType.OTHER : item.typeId,
|
|
|
answers: item.answers || [],
|
|
|
@@ -647,7 +810,8 @@ export const useExam = () => {
|
|
|
virtualIndex: 0,
|
|
|
duration: 0,
|
|
|
activeSubIndex: 0,
|
|
|
- hasSubQuestions: item.subQuestions?.length > 0
|
|
|
+ hasSubQuestions: item.subQuestions && item.subQuestions.length > 0,
|
|
|
+ typeTitle: item.typeTitle
|
|
|
} as Study.Question
|
|
|
}
|
|
|
questionList.value = transerQuestions(list.map((item, index) => transerQuestion(item, index)));
|
|
|
@@ -712,7 +876,6 @@ export const useExam = () => {
|
|
|
watch([() => currentIndex.value, () => currentQuestion.value?.activeSubIndex], (val) => {
|
|
|
const qs = questionList.value[val[0]];
|
|
|
virtualCurrentIndex.value = qs.index + qs.offset + val[1];
|
|
|
- console.log(virtualCurrentIndex.value, 777)
|
|
|
}, {
|
|
|
immediate: false
|
|
|
});
|