Jelajahi Sumber

fix(question-card): 点输入框就算选中它(高亮移过来、并取消右边选项)

用户追加反馈(附截图):「左边可以输入,但是边框没有高亮。点击右边,右边边框高亮之后,
我再点击左边输入框,右边依旧高亮」——上一版只做到「填了内容才算选中」,过头了:
点了输入框却不亮、右边还亮着,同样不对。

修法:
- 常驻输入框(freeformInputOnTop)的 focus 与 click 都接 handleAlwaysOnFreeformInteract:
  记下"输入框已激活"(于是它高亮),**单选卡片顺手清掉已选选项** → 一次只有一个选项亮
- isFreeformSelected 对常驻输入框改为「被点/聚焦过 **或** 填了内容」才算选中;
  多选仍允许选项与自由输入同时成立;普通「其他」输入框行为不变

验证:verify-question-cards 18 → 22/22(新增 4 条源码级断言:focus/click 都接同一处理函数、
处理函数会置激活标志、单选时清空已选选项、选中判定包含 chosenByInteraction);
变异测试:去掉 click 绑定 + 去掉清空选项 → 2 项立刻 FAIL,还原后 22/22。
回归:内联脚本 6/6、NLS 44/44、toast 27/27、build 通过、vue-tsc 无新错。

⚠️ 仍需用户在浏览器确认:点左边输入框 → 左边亮、右边灭;点右边 → 右边亮、左边灭。

Co-Authored-By: Claude Code <noreply@anthropic.com>
gongtianxiao 1 bulan lalu
induk
melakukan
45860b2bd1

+ 13 - 0
harness/tools/verify-question-cards.mjs

@@ -118,6 +118,19 @@ console.log('\n【4】接线(源码级:这两处都在 Vue 组件 / hook 里
     !/const isFreeformSelected = \(cardIdx: number\): boolean => \{\s*return isCardFreeformAlwaysOn\(cardIdx\) \|\|/.test(card) &&
       /if \(!isCardFreeformAlwaysOn\(cardIdx\)\) \{/.test(card));
   check('单选时"选了别的选项就不算它选中"', /if \(card\?\.multiSelect\) return true;/.test(card));
+
+  // ⚠️ 第二轮修正(用户 2026-09-24 追加反馈):「点了输入框应该把高亮移过来、并取消右边的选中」。
+  // 只按"填了内容才算选中"是不够的 —— 点了输入框却不亮、右边还亮着,同样是不对的。
+  check('常驻输入框的 focus 与 click 都接同一处理函数(点一下就等于选了它)',
+    (card.match(/@focus="handleAlwaysOnFreeformInteract\(cardIdx\)"/g) || []).length === 1 &&
+      (card.match(/@click="handleAlwaysOnFreeformInteract\(cardIdx\)"/g) || []).length === 1);
+  check('该处理函数:记下"输入框已激活"(高亮它)',
+    /const handleAlwaysOnFreeformInteract[\s\S]{0,400}isFreeformActive\.value\[cardIdx\] = true/.test(card) ||
+      /const handleAlwaysOnFreeformInteract[\s\S]{0,400}isFreeformActive\.value = \{ \.\.\.isFreeformActive\.value, \[cardIdx\]: true \}/.test(card));
+  check('该处理函数:**单选时清掉已选选项**(高亮只跟一个走)',
+    /const handleAlwaysOnFreeformInteract[\s\S]{0,600}!card\.multiSelect &&[\s\S]{0,200}new Set\(\)/.test(card));
+  check('选中判定里包含"被交互过"这一项(chosenByInteraction)',
+    /const chosenByInteraction = !!isFreeformActive\.value\[cardIdx\]/.test(card));
 }
 
 console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);

+ 32 - 9
src/components/Chat/QuestionCard.vue

@@ -99,11 +99,15 @@
               :class="{ selected: isFreeformSelected(cardIdx) }">
               <span class="qc-option-label">{{ getOtherOptionLabel(card) }}</span>
               <div class="qc-option-content">
+                <!-- ⚠️ focus / click 都要接管:点一下输入框就等于"选了 A",
+                     否则点了输入框却不高亮、右边的 B 还一直亮着(用户 2026-09-24 反馈) -->
                 <textarea :ref="(el) => setFreeformRef(cardIdx, el)"
                   class="qc-freeform-input inline"
                   :placeholder="card.freeformPlaceholder || '请输入补充信息...'"
-                  v-model="freeformTexts[cardIdx]" rows="1" @focus="handleFreeformFocus"
-                  @blur="handleFreeformBlur" @click.stop></textarea>
+                  v-model="freeformTexts[cardIdx]" rows="1"
+                  @focus="handleAlwaysOnFreeformInteract(cardIdx)"
+                  @click="handleAlwaysOnFreeformInteract(cardIdx)"
+                  @blur="handleFreeformBlur"></textarea>
               </div>
             </div>
 
@@ -427,25 +431,44 @@ const isCardFreeformAlwaysOn = (cardIdx: number): boolean =>
 /**
  * 自由输入那一行是否算"选中"(同时决定它的边框样式、以及答案是否算作出自输入框)。
  *
- * ⚠️ 2026-09-24 修(用户报「一次应该只有一个选项是选中样式」):
- * 以前这里是 `常驻输入框 || 被点开` —— **常驻输入框恒为 true**,
- * 于是那一行永远带着选中边框,点旁边的选项也不会变。
- * 现在按"答案出自谁"判定:
- *   · 常驻输入框(freeformInputOnTop):**填了内容**、且(单选时)**没有选中别的选项** → 才算选中
- *   · 普通「其他」输入框:被点开就算选中(原行为不变)
+ * ⚠️ 2026-09-24 两次修正,别改回去:
+ * ① 最早写的是 `常驻输入框 || 被点开` —— **常驻输入框恒为 true**,那一行永远带选中边框;
+ * ② 改成"只有填了内容才算"又过头了:**点了输入框却没有高亮**,而且点了它右边的选项还亮着
+ *    (用户反馈「左边可以输入,但是边框没有高亮;点击右边高亮后,再点左边右边依旧高亮」)。
+ * 现在:**被点/聚焦过(isFreeformActive)或填了内容**都算选中它;
+ * 单选卡片在 `handleAlwaysOnFreeformInteract` 里会把别的选项清掉,所以高亮仍然只跟一个走。
  */
 const isFreeformSelected = (cardIdx: number): boolean => {
   const card = questions.value[cardIdx];
   if (!isCardFreeformAlwaysOn(cardIdx)) {
     return !!isFreeformActive.value[cardIdx];
   }
+  const chosenByInteraction = !!isFreeformActive.value[cardIdx];
   const hasText = (freeformTexts.value[cardIdx] || '').trim().length > 0;
-  if (!hasText) return false;
+  if (!chosenByInteraction && !hasText) return false;
   if (card?.multiSelect) return true; // 多选:选项与自由输入可以同时成立
   const selected = selectedOptions.value[cardIdx];
   return !selected || selected.size === 0;
 };
 
+/**
+ * 点/聚焦"常驻输入框"这一行 = 选了 A:
+ *   · 记下"自由输入已激活"(高亮它)
+ *   · **单选卡片顺手清掉已选选项** —— 这正是"一次只有一个选项是选中样式"
+ */
+const handleAlwaysOnFreeformInteract = (cardIdx: number) => {
+  if (submitted.value) return;
+  const card = questions.value[cardIdx];
+  if (!card) return;
+  if (!isFreeformActive.value[cardIdx]) {
+    isFreeformActive.value = { ...isFreeformActive.value, [cardIdx]: true };
+  }
+  if (!card.multiSelect && (selectedOptions.value[cardIdx]?.size || 0) > 0) {
+    selectedOptions.value = { ...selectedOptions.value, [cardIdx]: new Set() };
+  }
+  handleFreeformFocus();
+};
+
 /** 自由输入是否算作"已作答":需已启用且确实填了内容 */
 const hasFreeformAnswer = (cardIdx: number): boolean =>
   isFreeformSelected(cardIdx) && (freeformTexts.value[cardIdx] || '').trim().length > 0;