| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 |
- /**
- * 问卷(`<question-cards>` 行)**作答后**的内容改写 —— 纯函数,便于 harness 直接测。
- *
- * 背景(2026-09-24 用户反馈):「如果下方已经新对话了,上方的问卷应该是只读状态」。
- * `QuestionCard.vue` 本来就有只读渲染分支(显示已选项 / 已填文本),但它的判定是
- * `isReadonly = 解析 content 得到 answers` —— 而**此前没有任何代码把答案写回 content**,
- * 于是只读分支永远走不到:提交过的问卷在新一轮对话出现后仍然可点可填。
- *
- * 这里负责「把提交的答案写回那一行」:把内容里**最后一个**
- * `<question-cards>…</question-cards>` 块的内部替换成 `{answers:[…]}`(只读分支认的形状)。
- * 写回后:
- * · 卡片立刻渲染成只读(内容变了 → 渲染层整体重解析,见 BusinessRecord 的 content watcher)
- * · `saveHistory()` 落盘后,刷新页面仍是已作答态
- */
- import type { QuestionCardAnswer } from "./question-cards-content";
- export type { QuestionCardAnswer };
- const OPEN_TAG = "<question-cards>";
- const CLOSE_TAG = "</question-cards>";
- /** 归一化一条答案(只保留只读渲染需要的字段,避免把别的状态写进内容) */
- const normalizeAnswer = (answer: QuestionCardAnswer | null | undefined) => ({
- header: String(answer?.header ?? ""),
- selected: Array.isArray(answer?.selected)
- ? answer.selected.map((item) => String(item ?? "").trim()).filter(Boolean)
- : [],
- freeText: String(answer?.freeText ?? ""),
- skipped: Boolean(answer?.skipped),
- });
- /** 生成只读渲染所需的内容(`QuestionCard` 的 readonly 分支按这个形状读) */
- export const buildAnsweredQuestionCardsContent = (answers: QuestionCardAnswer[]): string =>
- JSON.stringify({ answers: (Array.isArray(answers) ? answers : []).map(normalizeAnswer) });
- /**
- * 把内容里**最后一个** `<question-cards>…</question-cards>` 改成"已作答"的内容。
- *
- * ⚠️ **保留原有字段(尤其 `questions`),只补/换 `answers`**:
- * 只读态要求"选完还是问卷本来的样子",而那份样式(`(单选)` 标签、选项 A/B、提交按钮…)
- * 全靠 `questions` 渲染出来 —— 上一版把整块替换成只有 `answers` 的 JSON,
- * 结果只读渲染只能退化成一个简陋的小框(用户 2026-09-24:「问卷长什么样选完后还是长什么样」)。
- *
- * 找不到标记(或标记不成对)时**原样返回** —— 宁可不动,也不要破坏内容里的其它行。
- */
- export const applyQuestionCardsAnswers = (
- content: string,
- answers: QuestionCardAnswer[]
- ): string => {
- const text = String(content ?? "");
- const closeIdx = text.lastIndexOf(CLOSE_TAG);
- if (closeIdx < 0) return text;
- const openIdx = text.lastIndexOf(OPEN_TAG, closeIdx);
- if (openIdx < 0) return text;
- const inner = text.slice(openIdx + OPEN_TAG.length, closeIdx);
- let payload: Record<string, unknown> = {};
- try {
- const parsed = JSON.parse(inner);
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
- payload = parsed as Record<string, unknown>;
- }
- } catch {
- // 原内容不是 JSON(理论上不会发生):只写 answers,至少让只读态能渲染出答案
- }
- const next = JSON.stringify({
- ...payload,
- answers: (Array.isArray(answers) ? answers : []).map(normalizeAnswer),
- });
- return text.slice(0, openIdx + OPEN_TAG.length) + next + text.slice(closeIdx);
- };
- /** 内容里是否已经有作答结果(用于断言 / 幂等判断) */
- export const hasAnsweredQuestionCards = (content: string): boolean => {
- const text = String(content ?? "");
- const closeIdx = text.lastIndexOf(CLOSE_TAG);
- const openIdx = closeIdx >= 0 ? text.lastIndexOf(OPEN_TAG, closeIdx) : -1;
- if (openIdx < 0) return false;
- const inner = text.slice(openIdx + OPEN_TAG.length, closeIdx);
- try {
- const parsed = JSON.parse(inner);
- return Array.isArray(parsed?.answers) && parsed.answers.length > 0;
- } catch {
- return false;
- }
- };
|