| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922 |
- <template>
- <div v-if="isReadonly && contentAnswers.length > 0" class="question-card-shell readonly">
- <div v-for="(answer, idx) in contentAnswers" :key="idx" class="question-card">
- <div class="qc-body">
- <div class="qc-question">
- <span v-if="contentAnswers.length > 1" class="qc-question-seq">{{ idx + 1 }}. </span>
- {{ answer.header }}
- </div>
- <div class="qc-options">
- <div v-if="answer.freeText" class="qc-option selected">
- <div class="qc-option-content">
- <span class="qc-option-text">{{ answer.freeText }}</span>
- </div>
- </div>
- <template v-else-if="(answer.selected || []).length > 0">
- <div v-for="(label, labelIdx) in (answer.selected || [])" :key="labelIdx" class="qc-option selected">
- <span class="qc-option-label">{{ getOptionLabel(labelIdx) }}</span>
- <div class="qc-option-content">
- <span class="qc-option-text">{{ label }}</span>
- </div>
- </div>
- </template>
- <div v-else class="qc-option readonly-empty">
- <div class="qc-option-content">
- <span class="qc-option-text">未选择</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-else-if="questions.length > 0" class="question-card-shell" :class="{ submitted }">
- <!-- Progress dots outside questions -->
- <div class="qc-dots-row">
- <div v-for="(card, cardIdx) in questions" :key="cardIdx" class="qc-dot"
- :class="{ active: isQuestionAnswered(cardIdx) }" @click="scrollToQuestion(cardIdx)" />
- </div>
- <div v-for="(card, cardIdx) in questions" :key="cardIdx" class="question-card"
- :ref="(el) => setQuestionRef(cardIdx, el)" :class="{ highlighted: highlightCardIdx === cardIdx }">
- <!-- Question -->
- <div class="qc-body">
- <div class="qc-question" v-if="card.question">
- <span v-if="questions.length > 1" class="qc-question-seq">{{ cardIdx + 1 }}. </span>
- <span class="qc-question-type">{{ card.multiSelect ? '(多选)' : '(单选)' }}</span>
- {{ card.question }}
- </div>
- <!-- Options -->
- <div class="qc-options">
- <!-- Custom single-select dropdown for >= 6 options -->
- <div v-if="isDropdownCard(card)" class="qc-dropdown-wrapper">
- <div class="qc-option qc-custom-dropdown"
- :class="{ selected: isDropdownSelected(cardIdx), open: openDropdowns[cardIdx] }"
- @click.stop="toggleDropdown(cardIdx)">
- <span class="qc-option-label">{{ getSelectedDropdownLabel(cardIdx) || '?' }}</span>
- <div class="qc-option-content">
- <span class="qc-option-text">{{ getSelectedDropdownText(cardIdx) || '未选择' }}</span>
- </div>
- <span class="qc-dropdown-arrow" :class="{ open: openDropdowns[cardIdx] }">▾</span>
- </div>
- <div v-show="openDropdowns[cardIdx]" class="qc-dropdown-menu" @wheel.stop>
- <div v-for="(opt, optIdx) in card.options" :key="optIdx"
- class="qc-dropdown-item" :class="{ selected: isOptionSelected(cardIdx, optIdx) }"
- @click.stop="selectDropdownOption(cardIdx, optIdx)">
- <span class="qc-dropdown-item-label">{{ getCardOptionLabel(card, optIdx) }}</span>
- <span class="qc-dropdown-item-text">{{ opt.label }}</span>
- <span v-if="opt.description" class="qc-dropdown-item-desc">{{ opt.description }}</span>
- </div>
- </div>
- </div>
- <!-- Custom multi-select dropdown for >= 6 options -->
- <div v-else-if="isMultiDropdownCard(card)" class="qc-dropdown-wrapper">
- <div class="qc-option qc-custom-dropdown"
- :class="{ selected: isMultiDropdownSelected(cardIdx), open: openDropdowns[cardIdx] }"
- @click.stop="toggleDropdown(cardIdx)">
- <span class="qc-option-label">{{ getMultiDropdownLabel(cardIdx) || '?' }}</span>
- <div class="qc-option-content">
- <span class="qc-option-text">{{ getMultiDropdownText(cardIdx) || '未选择' }}</span>
- </div>
- <span class="qc-dropdown-arrow" :class="{ open: openDropdowns[cardIdx] }">▾</span>
- </div>
- <div v-show="openDropdowns[cardIdx]" class="qc-dropdown-menu" @wheel.stop>
- <div v-for="(opt, optIdx) in card.options" :key="optIdx"
- class="qc-dropdown-item" :class="{ selected: isOptionSelected(cardIdx, optIdx) }"
- @click.stop="toggleMultiDropdownOption(cardIdx, optIdx)">
- <span class="qc-dropdown-check">{{ isOptionSelected(cardIdx, optIdx) ? '✓' : '' }}</span>
- <span class="qc-dropdown-item-text">{{ opt.label }}</span>
- <span v-if="opt.description" class="qc-dropdown-item-desc">{{ opt.description }}</span>
- </div>
- </div>
- </div>
- <!-- Regular buttons -->
- <template v-else>
- <!-- 输入框常驻且在最上:用于补充信息(如公司未查到时补公司名/信用代码) -->
- <div v-if="card.freeformInputOnTop" class="qc-option"
- :class="{ selected: isFreeformSelected(cardIdx) }">
- <span class="qc-option-label">{{ getOtherOptionLabel(card) }}</span>
- <div class="qc-option-content">
- <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>
- </div>
- </div>
- <button v-for="(opt, optIdx) in card.options" :key="optIdx" type="button" class="qc-option"
- :class="{ selected: isOptionSelected(cardIdx, optIdx), recommended: opt.recommended }"
- @click="toggleOption(cardIdx, optIdx)">
- <span class="qc-option-label">{{ getCardOptionLabel(card, optIdx) }}</span>
- <div class="qc-option-content">
- <span class="qc-option-text">{{ opt.label }}</span>
- <span v-if="opt.description" class="qc-option-desc">{{ opt.description }}</span>
- </div>
- </button>
- <!-- "其他" freeform option(freeformInputOnTop 时输入框已在上方,不再重复渲染) -->
- <div v-if="!card.freeformInputOnTop && card.allowFreeformInput !== false" class="qc-option"
- :class="{ selected: isFreeformSelected(cardIdx) }" @click="toggleFreeform(cardIdx)">
- <span class="qc-option-label">{{ getOtherOptionLabel(card) }}</span>
- <div class="qc-option-content">
- <span v-if="!isFreeformSelected(cardIdx)"
- class="qc-option-text qc-other-text" :class="{ 'has-text': freeformTexts[cardIdx] }">{{ freeformTexts[cardIdx] || "其他" }}</span>
- <textarea v-if="isFreeformSelected(cardIdx)" :ref="(el) => setFreeformRef(cardIdx, el)"
- class="qc-freeform-input inline" :placeholder="'请输入补充信息...'" v-model="freeformTexts[cardIdx]" rows="1"
- @focus="handleFreeformFocus" @blur="handleFreeformBlur" @click.stop></textarea>
- </div>
- </div>
- </template>
- </div>
- </div>
- </div>
- <!-- Submit -->
- <div class="qc-footer">
- <div class="qc-divider" />
- <div class="qc-footer-row">
- <span v-if="unansweredCount > 0 && canSubmit" class="qc-footer-hint" @click="scrollToFirstUnanswered">还有{{ unansweredCount }}道题未填</span>
- <span v-else />
- <button type="button" class="qc-submit-btn" :disabled="!canSubmit || isSubmitting" @click="handleSubmit">
- {{ isSubmitting ? '提交中...' : '提交' }}
- </button>
- </div>
- </div>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
- import { FREEFORM_INPUT_FOCUS_KEY } from "../business-assistant/freeform-focus-control";
- interface QuestionOption {
- label: string;
- description?: string;
- recommended?: boolean;
- }
- interface Question {
- header: string;
- question: string;
- multiSelect?: boolean;
- allowFreeformInput?: boolean;
- message?: string;
- /** 输入框常驻并排在最上(用于补充信息),默认 false 保持原有"其他"行为 */
- freeformInputOnTop?: boolean;
- /** 上述常驻输入框的 placeholder */
- freeformPlaceholder?: string;
- options?: QuestionOption[];
- }
- function parseQuestions(content: string): Question[] {
- try {
- const data = JSON.parse(content);
- return data.questions || [];
- } catch {
- // Try to unwrap <question-cards>...</question-cards>
- const match = String(content || "").match(/<question-cards\b[^>]*>([\s\S]*?)<\/question-cards>/i);
- if (match) {
- try {
- const data = JSON.parse(match[1].trim());
- return data.questions || [];
- } catch {
- return [];
- }
- }
- return [];
- }
- }
- const props = defineProps<{
- content: string;
- recordMode?: boolean;
- sessionId?: string;
- }>();
- const questions = computed(() => parseQuestions(props.content));
- // Selected option indices per card (for single select, store one value; for multi, store set)
- const selectedOptions = ref<Record<number, Set<number>>>({});
- // Freeform selected per card
- const isFreeformActive = ref<Record<number, boolean>>({});
- const freeformTexts = ref<Record<number, string>>({});
- const freeformRefs: Record<number, HTMLTextAreaElement | null> = {};
- const questionRefs: Record<number, HTMLElement | null> = {};
- const highlightCardIdx = ref(-1);
- let highlightTimer: ReturnType<typeof setTimeout> | null = null;
- const isSubmitting = ref(false);
- const submitted = ref(false);
- const openDropdowns = ref<Record<number, boolean>>({});
- const freeformFocusOwner = Symbol("question-card-freeform");
- const freeformInputFocusControl = inject(FREEFORM_INPUT_FOCUS_KEY, null);
- function parseAnswersFromContent(content: string) {
- try {
- const data = JSON.parse(content);
- return Array.isArray(data.answers) ? data.answers : null;
- } catch {
- return null;
- }
- }
- const contentAnswers = computed(() => parseAnswersFromContent(props.content));
- const isReadonly = computed(() => !!contentAnswers.value);
- function handleFreeformFocus() {
- if (submitted.value) return;
- freeformInputFocusControl?.focus(freeformFocusOwner);
- }
- function handleFreeformBlur() {
- freeformInputFocusControl?.blur(freeformFocusOwner);
- }
- function setFreeformRef(cardIdx: number, el: any) {
- freeformRefs[cardIdx] = el as HTMLTextAreaElement | null;
- }
- function setQuestionRef(cardIdx: number, el: any) {
- questionRefs[cardIdx] = el as HTMLElement | null;
- }
- function scrollToQuestion(cardIdx: number) {
- const el = questionRefs[cardIdx];
- if (!el) return;
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- triggerHighlight(cardIdx);
- }
- function scrollToFirstUnanswered() {
- const idx = questions.value.findIndex((_card, cardIdx) => {
- const selected = selectedOptions.value[cardIdx];
- const hasOption = selected && selected.size > 0;
- const hasFreeform = isFreeformActive.value[cardIdx] && (freeformTexts.value[cardIdx] || '').trim().length > 0;
- return !hasOption && !hasFreeform;
- });
- if (idx >= 0) {
- scrollToQuestion(idx);
- }
- }
- function triggerHighlight(cardIdx: number) {
- highlightCardIdx.value = cardIdx;
- if (highlightTimer) clearTimeout(highlightTimer);
- highlightTimer = setTimeout(() => {
- highlightCardIdx.value = -1;
- highlightTimer = null;
- }, 1200);
- }
- const injectSubmit = inject<(answers: any[]) => void>('submitQuestionAnswers', undefined);
- // PC keeps its existing selection-driven input visibility behavior.
- const legacyFreeformInputActive = inject<{ value: boolean } | undefined>('isFreeformInputActive', undefined);
- watch(isFreeformActive, () => {
- if (!legacyFreeformInputActive) return;
- legacyFreeformInputActive.value = Object.values(isFreeformActive.value).some(Boolean);
- }, { deep: true });
- const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'];
- const getOptionLabel = (idx: number) => {
- return OPTION_LABELS[idx] || String(idx + 1);
- };
- /**
- * 卡片内某个选项的序号。
- * freeformInputOnTop 时输入框排在最上、占用 A,因此选项序号整体后移一位。
- */
- const getCardOptionLabel = (card: Question | undefined, optIdx: number) =>
- getOptionLabel(card?.freeformInputOnTop ? optIdx + 1 : optIdx);
- const getOtherOptionLabel = (card: Question) => {
- // 输入框常驻在最上时它自己是 A;否则跟在所有选项之后
- const idx = card.freeformInputOnTop ? 0 : (card.options?.length || 0);
- return OPTION_LABELS[idx] || '';
- };
- const isDropdownCard = (card: Question): boolean => {
- return !card.multiSelect && (card.options?.length || 0) >= 6;
- };
- const isMultiDropdownCard = (card: Question): boolean => {
- return card.multiSelect === true && (card.options?.length || 0) >= 6;
- };
- // --- Custom dropdown helpers ---
- function closeAllDropdowns() {
- openDropdowns.value = {};
- }
- function toggleDropdown(cardIdx: number) {
- if (submitted.value) return;
- const isOpen = openDropdowns.value[cardIdx];
- closeAllDropdowns();
- if (!isOpen) {
- openDropdowns.value = { [cardIdx]: true };
- }
- }
- function onDocumentClick() {
- closeAllDropdowns();
- }
- onMounted(() => {
- document.addEventListener('click', onDocumentClick);
- });
- onUnmounted(() => {
- handleFreeformBlur();
- document.removeEventListener('click', onDocumentClick);
- });
- // Single-select dropdown
- const isDropdownSelected = (cardIdx: number): boolean => {
- const selected = selectedOptions.value[cardIdx];
- return !!(selected && selected.size > 0);
- };
- const getSelectedDropdownLabel = (cardIdx: number): string => {
- const selected = selectedOptions.value[cardIdx];
- if (!selected || selected.size === 0) return '';
- const optIdx = selected.values().next().value;
- return getOptionLabel(optIdx);
- };
- const getSelectedDropdownText = (cardIdx: number): string => {
- const selected = selectedOptions.value[cardIdx];
- if (!selected || selected.size === 0) return '';
- const optIdx = selected.values().next().value;
- const card = questions.value[cardIdx];
- const opt = card?.options?.[optIdx];
- if (!opt) return '';
- return opt.description ? `${opt.label} - ${opt.description}` : opt.label;
- };
- function selectDropdownOption(cardIdx: number, optIdx: number) {
- if (submitted.value) return;
- const card = questions.value[cardIdx];
- if (!card) return;
- isFreeformActive.value[cardIdx] = false;
- const selected = new Set<number>();
- selected.add(optIdx);
- selectedOptions.value = { ...selectedOptions.value, [cardIdx]: selected };
- closeAllDropdowns();
- }
- // Multi-select dropdown
- const isMultiDropdownSelected = (cardIdx: number): boolean => {
- const selected = selectedOptions.value[cardIdx];
- return !!(selected && selected.size > 0);
- };
- const getMultiDropdownLabel = (cardIdx: number): string => {
- const selected = selectedOptions.value[cardIdx];
- if (!selected || selected.size === 0) return '';
- return String(selected.size);
- };
- const getMultiDropdownText = (cardIdx: number): string => {
- const selected = selectedOptions.value[cardIdx];
- if (!selected || selected.size === 0) return '';
- const card = questions.value[cardIdx];
- if (!card) return '';
- const labels: string[] = [];
- selected.forEach((optIdx) => {
- const opt = card.options?.[optIdx];
- if (opt) labels.push(opt.label);
- });
- return labels.join(', ');
- };
- function toggleMultiDropdownOption(cardIdx: number, optIdx: number) {
- if (submitted.value) return;
- const card = questions.value[cardIdx];
- if (!card) return;
- isFreeformActive.value[cardIdx] = false;
- const selected = ensureSelected(cardIdx);
- if (selected.has(optIdx)) {
- selected.delete(optIdx);
- } else {
- selected.add(optIdx);
- }
- selectedOptions.value = { ...selectedOptions.value, [cardIdx]: new Set(selected) };
- }
- // --- End custom dropdown helpers ---
- function ensureSelected(cardIdx: number): Set<number> {
- if (!selectedOptions.value[cardIdx]) {
- selectedOptions.value[cardIdx] = new Set();
- }
- return selectedOptions.value[cardIdx];
- }
- const isOptionSelected = (cardIdx: number, optIdx: number): boolean => {
- return ensureSelected(cardIdx).has(optIdx);
- };
- // 输入框常驻的卡片(freeformInputOnTop)不需要点击激活,直接视为自由输入已启用
- const isCardFreeformAlwaysOn = (cardIdx: number): boolean =>
- questions.value[cardIdx]?.freeformInputOnTop === true;
- /**
- * 自由输入那一行是否算"选中"(同时决定它的边框样式、以及答案是否算作出自输入框)。
- *
- * ⚠️ 2026-09-24 修(用户报「一次应该只有一个选项是选中样式」):
- * 以前这里是 `常驻输入框 || 被点开` —— **常驻输入框恒为 true**,
- * 于是那一行永远带着选中边框,点旁边的选项也不会变。
- * 现在按"答案出自谁"判定:
- * · 常驻输入框(freeformInputOnTop):**填了内容**、且(单选时)**没有选中别的选项** → 才算选中
- * · 普通「其他」输入框:被点开就算选中(原行为不变)
- */
- const isFreeformSelected = (cardIdx: number): boolean => {
- const card = questions.value[cardIdx];
- if (!isCardFreeformAlwaysOn(cardIdx)) {
- return !!isFreeformActive.value[cardIdx];
- }
- const hasText = (freeformTexts.value[cardIdx] || '').trim().length > 0;
- if (!hasText) return false;
- if (card?.multiSelect) return true; // 多选:选项与自由输入可以同时成立
- const selected = selectedOptions.value[cardIdx];
- return !selected || selected.size === 0;
- };
- /** 自由输入是否算作"已作答":需已启用且确实填了内容 */
- const hasFreeformAnswer = (cardIdx: number): boolean =>
- isFreeformSelected(cardIdx) && (freeformTexts.value[cardIdx] || '').trim().length > 0;
- const isQuestionAnswered = (cardIdx: number): boolean => {
- const selected = selectedOptions.value[cardIdx];
- const hasOption = selected && selected.size > 0;
- return hasOption || hasFreeformAnswer(cardIdx);
- };
- const toggleOption = (cardIdx: number, optIdx: number) => {
- if (submitted.value) return;
- const card = questions.value[cardIdx];
- if (!card) return;
- // Clear freeform when selecting options
- isFreeformActive.value[cardIdx] = false;
- handleFreeformBlur();
- const selected = ensureSelected(cardIdx);
- if (card.multiSelect) {
- // Multi-select: toggle
- if (selected.has(optIdx)) {
- selected.delete(optIdx);
- } else {
- selected.add(optIdx);
- }
- // Trigger reactivity
- selectedOptions.value = { ...selectedOptions.value, [cardIdx]: new Set(selected) };
- } else {
- // Single select: toggle or replace
- if (selected.has(optIdx)) {
- selected.clear();
- } else {
- selected.clear();
- selected.add(optIdx);
- }
- selectedOptions.value = { ...selectedOptions.value, [cardIdx]: new Set(selected) };
- }
- };
- const toggleFreeform = (cardIdx: number) => {
- if (submitted.value) return;
- const card = questions.value[cardIdx];
- if (!card) return;
- // Clear options when selecting "其他"
- isFreeformActive.value[cardIdx] = !isFreeformActive.value[cardIdx];
- isFreeformActive.value = { ...isFreeformActive.value };
- if (isFreeformActive.value[cardIdx]) {
- selectedOptions.value[cardIdx]?.clear();
- selectedOptions.value = { ...selectedOptions.value, [cardIdx]: new Set() };
- void nextTick(() => {
- freeformRefs[cardIdx]?.focus();
- });
- } else {
- handleFreeformBlur();
- }
- };
- const canSubmit = computed(() => {
- if (submitted.value) return false;
- return questions.value.some((_card, cardIdx) => {
- const selected = selectedOptions.value[cardIdx];
- const hasOption = selected && selected.size > 0;
- return hasOption || hasFreeformAnswer(cardIdx);
- });
- });
- const unansweredCount = computed(() => {
- return questions.value.filter((_card, cardIdx) => {
- const selected = selectedOptions.value[cardIdx];
- const hasOption = selected && selected.size > 0;
- const hasFreeform = isFreeformActive.value[cardIdx] && (freeformTexts.value[cardIdx] || '').trim().length > 0;
- return !hasOption && !hasFreeform;
- }).length;
- });
- const handleSubmit = () => {
- if (!canSubmit.value || isSubmitting.value) return;
- handleFreeformBlur();
- Object.values(freeformRefs).forEach((textarea) => textarea?.blur());
- closeAllDropdowns();
- const answers = questions.value.map((card, cardIdx) => {
- const selected = selectedOptions.value[cardIdx] || new Set();
- const selectedLabels: string[] = [];
- selected.forEach((optIdx) => {
- const opt = card.options?.[optIdx];
- if (opt) {
- selectedLabels.push(opt.label);
- }
- });
- return {
- header: card.header,
- selected: selectedLabels,
- // 常驻输入框不需要点击激活,输入的内容照样要带上
- freeText: isFreeformSelected(cardIdx) ? (freeformTexts.value[cardIdx] || '') : '',
- skipped: false,
- };
- });
- isSubmitting.value = true;
- submitted.value = true;
- if (injectSubmit) {
- injectSubmit(answers);
- } else {
- console.warn('[QuestionCard] No submit handler provided via inject');
- }
- isSubmitting.value = false;
- };
- </script>
- <style lang="scss" scoped>
- .question-card-shell {
- border: 1px solid rgba(199, 196, 216, 0.2);
- border-radius: 16px;
- background: #ffffff;
- box-shadow: 0 1px 1.75px rgba(0, 0, 0, 0.05);
- padding: 0 12px;
- display: flex;
- flex-direction: column;
- gap: 0;
- margin-top: 12px;
- margin-bottom: 12px;
- max-width: 700px;
- }
- .question-card-shell.readonly .qc-option {
- pointer-events: none;
- }
- .question-card-shell.readonly .qc-option.readonly-empty .qc-option-text {
- color: rgba(70, 69, 85, 0.5);
- }
- .question-card {
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 16px 12px;
- border-radius: 8px;
- transition: background 0.3s ease;
- &.highlighted {
- background: rgba(53, 37, 205, 0.06);
- }
- }
- .qc-dots-row {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- padding: 16px 12px 0;
- }
- .qc-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: rgba(199, 196, 216, 0.4);
- transition: background 0.2s ease;
- cursor: pointer;
- &.active {
- background: #3525cd;
- }
- }
- .qc-body {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .qc-question {
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 16px;
- font-weight: 500;
- color: #141b2b;
- line-height: 1.5;
- }
- .qc-question-seq {
- color: rgba(70, 69, 85, 0.5);
- font-weight: 400;
- }
- .qc-question-type {
- font-size: 12px;
- font-weight: 400;
- color: rgba(53, 37, 205, 0.6);
- margin-right: 4px;
- }
- .qc-options {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
- .qc-option {
- flex: 1 1 calc(50% - 4px);
- min-width: 220px;
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 12px 16px;
- border: 1px solid rgba(199, 196, 216, 0.3);
- border-radius: 12px;
- background: #ffffff;
- cursor: pointer;
- transition: all 0.15s ease;
- text-align: left;
- &.freeform {
- flex: 1 1 100%;
- }
- &:hover {
- border-color: rgba(53, 37, 205, 0.3);
- background: rgba(53, 37, 205, 0.02);
- }
- &.selected {
- border-color: #3525cd;
- background: rgba(53, 37, 205, 0.04);
- }
- }
- // Custom dropdown
- .qc-dropdown-wrapper {
- flex: 1 1 calc(50% - 4px);
- min-width: 220px;
- position: relative;
- }
- .qc-custom-dropdown {
- &.open {
- border-color: #3525cd;
- background: rgba(53, 37, 205, 0.04);
- }
- }
- .qc-dropdown-arrow {
- font-size: 12px;
- color: rgba(70, 69, 85, 0.4);
- transition: transform 0.2s ease;
- flex-shrink: 0;
- margin-left: auto;
- &.open {
- transform: rotate(180deg);
- }
- }
- .qc-dropdown-menu {
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- max-height: 240px;
- overflow-y: auto;
- overscroll-behavior: contain;
- background: #ffffff;
- border: 1px solid rgba(199, 196, 216, 0.2);
- border-radius: 12px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
- z-index: 100;
- padding: 4px;
- }
- .qc-dropdown-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 12px;
- border-radius: 8px;
- cursor: pointer;
- transition: background 0.15s ease;
- &:hover {
- background: rgba(53, 37, 205, 0.04);
- }
- &.selected {
- background: rgba(53, 37, 205, 0.06);
- }
- }
- .qc-dropdown-item-label {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- background: rgba(53, 37, 205, 0.08);
- color: #3525cd;
- font-size: 12px;
- font-weight: 500;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- .qc-dropdown-check {
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- font-size: 14px;
- color: #3525cd;
- font-weight: 600;
- }
- .qc-dropdown-item-text {
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 14px;
- font-weight: 500;
- color: #141b2b;
- flex: 1;
- line-height: 1.4;
- }
- .qc-dropdown-item-desc {
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 12px;
- color: rgba(70, 69, 85, 0.5);
- line-height: 1.3;
- flex-shrink: 0;
- }
- .question-card-shell.submitted .qc-option {
- cursor: default;
- &:hover {
- border-color: rgba(199, 196, 216, 0.3);
- background: #ffffff;
- }
- }
- .qc-option-label {
- width: 24px;
- height: 24px;
- border-radius: 50%;
- background: rgba(53, 37, 205, 0.08);
- color: #3525cd;
- font-size: 12px;
- font-weight: 500;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- }
- .qc-option-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
- }
- .qc-option-text {
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 14px;
- font-weight: 500;
- color: #141b2b;
- line-height: 1.4;
- }
- .qc-other-text.has-text {
- color: #141b2b;
- }
- .qc-option-desc {
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 12px;
- color: rgba(70, 69, 85, 0.5);
- line-height: 1.3;
- }
- .qc-option.freeform {
- cursor: pointer;
- color: rgba(70, 69, 85, 0.4);
- &.selected {
- border-color: #3525cd;
- background: rgba(53, 37, 205, 0.04);
- }
- }
- .qc-freeform-input.inline {
- width: 100%;
- padding: 4px 0;
- border: none;
- border-bottom: 1px solid rgba(53, 37, 205, 0.4);
- border-radius: 0;
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 14px;
- color: #141b2b;
- resize: none;
- overflow: hidden;
- outline: none;
- background: transparent;
- box-sizing: border-box;
- transition: border-color 0.15s ease;
- &::placeholder {
- color: rgba(70, 69, 85, 0.4);
- }
- &:focus {
- border-bottom-color: #3525cd;
- }
- }
- .qc-footer {
- display: flex;
- flex-direction: column;
- }
- .qc-divider {
- height: 1px;
- background: rgba(199, 196, 216, 0.1);
- margin: 0;
- }
- .qc-footer-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 12px;
- width: 100%;
- padding: 0 12px 16px;
- }
- .qc-footer-hint {
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 12px;
- color: rgba(70, 69, 85, 0.5);
- margin-right: 16px;
- cursor: pointer;
- flex-shrink: 0;
- }
- .qc-submit-btn {
- flex-grow: 1;
- padding: 8px 24px;
- border: none;
- border-radius: 8px;
- background: #3525cd;
- color: #ffffff;
- font-family: "HarmonyOS Sans SC", sans-serif;
- font-size: 15px;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.2s ease;
- &:hover:not(:disabled) {
- background: #2b1ea8;
- }
- &:disabled {
- background: rgba(53, 37, 205, 0.3);
- cursor: not-allowed;
- }
- }
- </style>
|