You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/demand/consumer-behavior.vue

2312 lines
69 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="consumer-behavior-page student-training-shell">
<main class="training-core">
<section class="task-header">
<div class="task-badge">市场洞察与需求分析</div>
<h1 class="task-title">{{ taskTitle }}</h1>
</section>
<TrainingTaskBrief
:background="taskBackground"
:goals="taskGoals"
:requirements="taskRequirement"
:show-material="false"
/>
<section class="task-card">
<div class="workbench-head workbench-head--actions-only">
<div class="workbench-actions">
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
<button type="button" class="outline-action" @click="exportOutcome">
<el-icon><Download /></el-icon>
导出实训成果
</button>
</div>
</div>
<nav v-if="flowSteps.length" class="step-indicator" aria-label="消费者需求与行为步骤">
<template v-for="(step, index) in flowSteps" :key="step">
<button
type="button"
:class="['step-tab', { active: form.activeStep === index, complete: form.activeStep > index }]"
:aria-label="`第${index + 1}步:${step}`"
@click="setActiveStep(index)"
>
<span class="step-marker">{{ String(index + 1).padStart(2, '0') }}</span>
<span class="step-copy">
<strong class="step-name" :title="step">{{ step }}</strong>
<span class="step-action">{{ form.activeStep === index ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < flowSteps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<input
ref="fileInputRef"
type="file"
class="file-input"
:accept="uploadAccept"
@change="handleFileChange"
/>
<input
ref="questionnaireImportInputRef"
type="file"
class="file-input"
accept=".docx"
@change="handleQuestionnaireImport"
/>
<section class="step-card consumer-behavior-step-card">
<div v-if="activeStepName" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(form.activeStep + 1).padStart(2, '0') }}</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<section v-if="form.activeStep === 0" class="step-panel">
<div class="hypothesis-head">
<p class="task-desc hypothesis-desc">
基于实训 8 的用户画像结论,明确本次问卷调查的目标人群与核心假设,填写下表。
</p>
<div class="hypothesis-actions">
<el-button class="hypothesis-action" @click="addTargetHypothesis">增加一行</el-button>
<el-button class="hypothesis-action hypothesis-action--quiet" :disabled="form.targetHypotheses.length <= 1" @click="removeTargetHypothesis">删除一行</el-button>
</div>
<el-button class="training-eight-action" @click="viewTargetUserProfile">查看实训8</el-button>
</div>
<div class="target-hypothesis-table-wrap">
<table class="target-hypothesis-table">
<thead>
<tr>
<th>目标人群</th>
<th>实训8关键发现</th>
<th>本次问卷需验证的核心假设</th>
</tr>
</thead>
<tbody>
<tr v-for="(hypothesis, index) in form.targetHypotheses" :key="index">
<td><input v-model="hypothesis.targetGroup" class="hypothesis-input" placeholder="例如职场白领25-35岁" /></td>
<td><textarea v-model="hypothesis.keyFinding" class="hypothesis-textarea" placeholder="例如:高压工作、入睡困难、看重效率" /></td>
<td><textarea v-model="hypothesis.coreHypothesis" class="hypothesis-textarea" placeholder="例如:是否更关注白噪声质量与智能场景联动?对价格的敏感度如何?" /></td>
</tr>
</tbody>
</table>
</div>
<p class="hypothesis-tip">可编辑表格内容;请基于实训 8 的结论明确每类目标人群的问卷验证方向。</p>
</section>
<section v-else-if="form.activeStep === 1" class="step-panel">
<div class="kano-head">
<p class="task-desc kano-desc">
在正式设计问卷之前,需要理解消费者需求的层次结构。不同类型的需求对购买决策的影响方式截然不同。结合案例资料,通过 KANO 模型进行用户需求分类,填写下表。
</p>
<div class="kano-actions">
<el-button class="hypothesis-action" :disabled="form.kanoNeeds.length >= KANO_NEED_LIMIT" @click="addKanoNeed">增加一行</el-button>
<el-button class="hypothesis-action hypothesis-action--quiet" :disabled="form.kanoNeeds.length <= 1" @click="removeKanoNeed">删除一行</el-button>
</div>
</div>
<div class="kano-table-wrap">
<table class="kano-table">
<thead>
<tr>
<th>编号</th>
<th>功能</th>
<th>对应KANO类型</th>
<th>对应功能方向</th>
</tr>
</thead>
<tbody>
<tr v-for="(need, index) in form.kanoNeeds" :key="index">
<td class="kano-index">{{ index + 1 }}</td>
<td><textarea v-model="need.feature" class="hypothesis-textarea kano-textarea" placeholder="示例:灯光亮度可无级调节" aria-label="功能" /></td>
<td class="kano-select-cell">
<el-select v-model="need.kanoType" class="kano-select" placeholder="请选择类型" aria-label="对应KANO类型">
<el-option v-for="option in kanoTypeOptions" :key="option" :label="option" :value="option" />
</el-select>
</td>
<td><textarea v-model="need.featureDirection" class="hypothesis-textarea kano-textarea" placeholder="示例:照明系统" aria-label="对应功能方向" /></td>
</tr>
</tbody>
</table>
</div>
<p class="hypothesis-tip">功能与功能方向均可填写KANO 类型请从下拉菜单选择。最多可填写 {{ KANO_NEED_LIMIT }} 行。</p>
</section>
<section v-else-if="form.activeStep === 2" class="step-panel">
<div class="questionnaire-design-head">
<p class="task-desc questionnaire-design-desc">
结合前面的实训分析,设计一份完整的消费者需求调研问卷,包含基本信息、购买行为、需求偏好和开放建议四部分。需求偏好部分请用 Likert 五级量表评价第 2 步归类的功能。
</p>
<el-button class="questionnaire-import-action" :loading="questionnaireImporting" @click="triggerQuestionnaireImport">
<el-icon><Upload /></el-icon>
导入问卷Word 文档)
</el-button>
</div>
<div v-if="form.questionnaireImport?.name" class="questionnaire-imported-file">
<el-icon><Document /></el-icon>
<span>{{ form.questionnaireImport.name }}</span>
<button type="button" @click="showQuestionnairePreview = true">预览问卷</button>
</div>
<div class="questionnaire-editor-launcher">
<div class="questionnaire-editor-launcher__copy">
<span class="section-eyebrow">Questionnaire Studio</span>
<h4>在线编写消费者需求调研问卷</h4>
<p>通过分区、题型与选项设置,快速完成可执行的问卷结构。</p>
</div>
<el-button class="questionnaire-editor-launcher__action" @click="questionnaireEditorVisible = true">进入问卷编辑器</el-button>
</div>
<p class="questionnaire-editor-summary">
已编辑 {{ form.questionnaireQuestions.length }} 题;{{ form.questionnaireImport?.name ? "已导入 1 份 Word 问卷,可随时预览。" : "也可导入已完成的 .docx 问卷。" }}
</p>
<el-dialog v-model="questionnaireEditorVisible" title="消费者需求调研问卷编辑器" width="min(1120px, 94vw)" class="questionnaire-editor-dialog" append-to-body>
<div class="questionnaire-editor-dialog__intro">
<p>请覆盖基本信息、购买行为、需求偏好和开放建议四部分;需求偏好至少设置一题 Likert 五级量表。</p>
<el-button class="questionnaire-editor-add" @click="addQuestionnaireQuestion">新增题目</el-button>
</div>
<div class="questionnaire-question-list">
<article v-for="(question, index) in form.questionnaireQuestions" :key="question.id" class="questionnaire-question-card">
<span class="questionnaire-question-card__number">{{ String(index + 1).padStart(2, "0") }}</span>
<div class="questionnaire-question-card__fields">
<el-select v-model="question.section" aria-label="问卷分区">
<el-option v-for="section in questionnaireSections" :key="section.value" :label="section.label" :value="section.value" />
</el-select>
<el-select v-model="question.type" aria-label="题目类型">
<el-option v-for="type in questionnaireQuestionTypes" :key="type.value" :label="type.label" :value="type.value" />
</el-select>
<el-input v-model="question.title" :placeholder="questionPlaceholder(question.section)" aria-label="题目内容" />
<el-input v-if="question.type === 'SINGLE' || question.type === 'MULTIPLE'" v-model="question.options" placeholder="选项以中文逗号或换行分隔" aria-label="题目选项" />
<p v-else-if="question.type === 'LIKERT'" class="likert-tip">量表固定为:非常不重要、较不重要、一般、较重要、非常重要</p>
</div>
<button type="button" class="questionnaire-question-card__remove" :disabled="form.questionnaireQuestions.length <= 4" @click="removeQuestionnaireQuestion(index)">删除</button>
</article>
</div>
</el-dialog>
<el-dialog v-model="showQuestionnairePreview" title="导入问卷预览" width="min(900px, 94vw)" class="questionnaire-preview-dialog" append-to-body>
<div v-if="form.questionnaireImport?.previewContent" class="questionnaire-word-preview">{{ form.questionnaireImport.previewContent }}</div>
<p v-else class="questionnaire-preview-empty">暂未解析到可预览内容。</p>
</el-dialog>
</section>
<section v-else class="step-panel">
<p class="task-desc survey-report-desc">
将步骤三设计的调查问卷导入问卷星等在线问卷调查平台,开展问卷调查。基于收集到的数据,填写问卷调查报告。
</p>
<div class="survey-report-table-wrap">
<table class="survey-report-table">
<thead>
<tr>
<th>报告模块标题</th>
<th>内容</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in form.surveyReportRows" :key="index">
<td>
<input v-model="row.title" class="survey-report-title-input" :placeholder="surveyReportTitlePlaceholder(index)" :aria-label="`第${index + 1}个报告模块标题`" />
</td>
<td>
<textarea v-model="row.content" class="survey-report-content-input" :placeholder="surveyReportContentPlaceholder(index)" :aria-label="`第${index + 1}个报告模块内容`" />
</td>
</tr>
</tbody>
</table>
</div>
<div class="survey-report-guide">
<div>
<h5>可参考以下方法</h5>
<p>1. 根据报告模板,自行分析撰写问卷调查报告。</p>
<p>2. 使用通用 AI 大模型辅助完成报告:请根据用户调研数据,从用户购买核心决策因素、不同群体决策因素差异、未购买用户的顾虑与市场机会、信息获取渠道与购买场景等方面撰写报告。</p>
<p>3. 如问卷调查收集的数据不理想,可使用本实训提供的数据进行分析,生成调查报告。</p>
</div>
<el-button class="survey-report-template-action" @click="downloadSurveyReportTemplate">
<el-icon><Download /></el-icon>
下载报告模板
</el-button>
</div>
</section>
<div class="step-actions">
<el-button class="nav-action nav-action--previous" :disabled="form.activeStep === 0" @click="goPrev">
<el-icon><ArrowLeft /></el-icon>
上一步
</el-button>
<el-button class="nav-action nav-action--save" :loading="saving" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</el-button>
<el-button class="nav-action nav-action--next" :disabled="form.activeStep === flowSteps.length - 1" @click="goNext">
下一步
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>
<TrainingAiSidebar
study-tip="当前步骤建议:先确认问卷能覆盖用户基本信息、购买行为、决策因素和未购买顾虑,再用数据分析支撑结论。"
reference-title="消费者行为分析参考"
reference-text="· 需求类型:功能需求、价格需求、服务需求、隐私安全需求<br />· 决策因素:价格、品牌、功能、质量、便捷性、售后<br />· 分析方法:分群对比、渠道分析、购买场景分析、未购买原因归因"
assist-text="报告中尽量把数据结论写成“现象 + 人群差异 + 产品启示”的结构,避免只罗列比例。"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { computed, getCurrentInstance, nextTick, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Upload } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkConsumerBehaviorStepOne, checkConsumerBehaviorStepTwo, checkConsumerBehaviorStepThree, checkConsumerBehaviorStepFour, getStudentTrainingAnswer, previewConsumerBehaviorQuestionnaireWord, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
const TASK_KEY = "consumer-behavior";
const KANO_NEED_LIMIT = 20;
let questionnaireQuestionSequence = 0;
const { proxy } = getCurrentInstance();
const defaultRequirement = "消费者需求与行为分析用于识别目标用户的购买动机、决策因素、渠道偏好和使用场景,为智能产品功能设计与市场切入提供依据。";
const defaultBackground =
"SX是一家专注于智能健康生活产品的科技公司。公司产品开发团队经过市场分析发现光线及噪音对睡眠具有较大影响市场上针对助眠产品主要集中在保健品及药物较少产品是通过优化睡眠环境来达成助眠效果因此初步判断开发一款智能声光伴眠吸顶灯具有市场机会。\n产品开发前团队进行了目标用户画像进行了初步的用户分析现在需要通过问卷调查收集足够多的数据分析用户购买智能产品的决策因素为后续的产品关键功能开发提供市场需求数据支撑与决策依据。";
const defaultGoals = [
"掌握消费者需求类型划分及特征。",
"熟悉消费者行为理论与决策机制。",
"挖掘影响消费者需求及行为的关键因素。",
];
const questionnaireTemplate = `<h4>智能产品购买决策因素调查问卷</h4>
<p><strong>(一)基本信息</strong></p>
<p>1.您的性别: A. 男 B. 女</p>
<p>2.您的年龄: A. 18岁及以下 B. 19-25岁 C. 26-35岁 D. 36-45岁 E. 46岁及以上</p>
<p>3.您的月收入: A. 3000元及以下 B. 3001-5000元 C. 5001-8000元 D. 8001-12000元 E. 12001元及以上</p>
<p>4.您居住的城市级别: A. 一线城市 B. 新一线城市 C. 二线城市 D. 三线及以下城市</p>
<p><strong>(二)购买行为与决策因素</strong></p>
<p>5.您是否购买过智能产品? A. 是 B. 否若选B跳转至第12题</p>
<p>6.您购买过的智能产品类型可多选: A. 智能手机 B. 智能家电(如扫地机器人、智能冰箱等) C. 智能穿戴设备(如智能手表、手环等) D. 智能玩具 E. 智能健身设备(如智能健身镜) F. 其他请注明____</p>
<p>7.在购买智能产品时,您优先考虑的因素可多选: A. 价格 B. 品牌知名度 C. 功能实用性 D. 产品质量与稳定性 E. 操作便捷性 F. 数据安全与隐私保护 G. 外观设计 H. 售后服务 I. 生态兼容性(如能否与其他智能设备联动) J. 其他请注明____</p>
<p>8.请您根据实际感受,评价以下因素对您购买智能产品的重要程度,请在对应选项中打“√”</p>
<table>
<thead>
<tr>
<th>因素</th>
<th>非常不重要</th>
<th>不重要</th>
<th>一般</th>
<th>重要</th>
<th>非常重要</th>
</tr>
</thead>
<tbody>
<tr><td>价格</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>品牌知名度</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>功能实用性</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>产品质量与稳定性</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>操作便捷性</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>数据安全与隐私保护</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>外观设计</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>售后服务</td><td></td><td></td><td></td><td></td><td></td></tr>
<tr><td>生态兼容性</td><td></td><td></td><td></td><td></td><td></td></tr>
</tbody>
</table>
<p>9.您是否会因为智能产品具备独特创新功能而愿意支付更高价格? A. 一定会 B. 可能会 C. 不确定 D. 可能不会 E. 一定不会</p>
<p>10.您获取智能产品信息的主要渠道可多选: A. 电商平台(如淘宝、京东等) B. 社交媒体(如抖音、小红书等) C. 朋友、家人推荐 D. 线下实体店体验 E. 科技媒体评测 F. 其他请注明____</p>
<p>11.您购买智能产品的主要场景是: A. 自己使用 B. 赠送家人 C. 赠送朋友 D. 商务礼品 E. 其他请注明____</p>
<p><strong>(三)未购买情况与顾虑</strong></p>
<p>12.您未购买智能产品的主要原因可多选: A. 价格过高 B. 对智能产品功能不了解 C. 担心产品质量与稳定性 D. 担心数据安全与隐私泄露 E. 觉得操作复杂,难以上手 F. 没有合适的产品满足需求 G. 其他请注明____</p>
<p>13.如果您计划购买智能产品,您最期待的功能是: A. 更强大的智能化功能 B. 更便捷的操作方式 C. 更好的数据安全与隐私保护 D. 更丰富的生态兼容性 E. 其他请注明____</p>
<p><strong>(四)开放性问题</strong></p>
<p>14.对于智能产品的发展,您有哪些建议或期望?</p>`;
const surveyStats = `(一)基本信息
1.性别男235人47%女265人53%
2.年龄18岁及以下30人6%19-25岁120人24%26-35岁180人36%36-45岁100人20%46岁及以上70人14%
3.月收入3000元及以下80人16%3001-5000元120人24%5001-8000元150人30%8001-12000元100人20%12001元及以上50人10%
4.城市级别一线城市100人20%新一线城市150人30%二线城市120人24%三线及以下城市130人26%
(二)购买行为与决策因素
5.是否购买过智能产品是420人84%否80人16%
6.购买过的智能产品类型智能手机380人76%智能家电220人44%智能穿戴设备180人36%智能玩具60人12%智能健身设备40人8%其他20人4%
7.优先考虑的因素价格250人50%品牌知名度180人36%功能实用性320人64%产品质量与稳定性280人56%操作便捷性220人44%数据安全与隐私保护150人30%外观设计120人24%售后服务100人20%生态兼容性80人16%其他30人6%
8.各因素影响程度统计价格65%品牌知名度50%功能实用性80%产品质量与稳定性75%操作便捷性60%数据安全与隐私保护45%外观设计35%售后服务40%生态兼容性30%
9.是否为创新功能支付更高价格一定会80人16%可能会220人44%不确定100人20%可能不会70人14%一定不会30人6%
10.获取信息渠道电商平台300人60%社交媒体250人50%朋友、家人推荐200人40%线下实体店体验150人30%科技媒体评测120人24%其他30人6%
11.购买场景自己使用350人70%赠送家人80人16%赠送朋友40人8%商务礼品20人4%其他10人2%
(三)未购买情况与顾虑
12.未购买原因价格过高35人43.75%对功能不了解20人25%担心质量与稳定性15人18.75%担心数据安全与隐私泄露8人10%觉得操作复杂5人6.25%没有合适产品3人3.75%其他2人2.5%
13.期待的功能更强大智能化功能25人31.25%更便捷操作方式20人25%更好数据安全与隐私保护15人18.75%更丰富生态兼容性12人15%其他8人10%
(四)开放性问题
部分用户建议:希望智能产品价格更加亲民;简化操作流程,适合各年龄段人群;加强数据安全保护;丰富产品功能,满足多样化需求;提升售后服务质量等。`;
const visualGroups = [
{
title: "1.基本信息",
items: [
{ key: "gender", label: "性别" },
{ key: "age", label: "年龄" },
{ key: "income", label: "月收入" },
{ key: "city", label: "城市级别" },
],
},
{
title: "2.购买行为与决策因素",
items: [
{ key: "hasPurchased", label: "是否购买过智能产品" },
{ key: "productTypes", label: "购买过的智能产品类型" },
{ key: "priorityFactors", label: "优先考虑的因素" },
{ key: "factorImportance", label: "各因素影响程度统计" },
{ key: "innovationPremium", label: "是否为创新功能支付更高价格" },
{ key: "infoChannels", label: "获取信息渠道" },
{ key: "purchaseScenario", label: "购买场景" },
],
},
{
title: "3.未购买情况与顾虑",
items: [
{ key: "nonPurchaseReasons", label: "未购买原因" },
{ key: "expectedFunctions", label: "期待的功能" },
],
},
];
const taskConfig = ref(null);
const saving = ref(false);
const uploading = ref(false);
const questionnaireImporting = ref(false);
const fileInputRef = ref(null);
const questionnaireImportInputRef = ref(null);
const questionnaireEditorRef = ref(null);
const uploadContext = ref({ type: "", key: "" });
const questionnaireEditorVisible = ref(false);
const showQuestionnairePreview = ref(false);
const uploadAccept = computed(() => {
if (uploadContext.value.type === "questionnaire") return ".pdf";
if (uploadContext.value.type === "dataExcel") return ".xls,.xlsx";
if (uploadContext.value.type === "chart") return ".png,.jpg,.jpeg,.webp";
return ".png,.jpg,.jpeg,.webp,.pdf,.doc,.docx";
});
function createForm() {
return {
activeStep: 0,
targetHypotheses: createTargetHypotheses(),
kanoNeeds: createKanoNeeds(),
questionnaireQuestions: createQuestionnaireQuestions(),
questionnaireImport: null,
surveyReportRows: createSurveyReportRows(),
questionnaireText: questionnaireTemplate,
questionnaireFileName: "",
questionnaireFileUrl: "",
dataExcelFileName: "",
dataExcelFileUrl: "",
chartFiles: {},
};
}
function createTargetHypotheses() {
return [
{ targetGroup: "职场白领25-35岁", keyFinding: "", coreHypothesis: "" },
{ targetGroup: "精致妈妈28-38岁", keyFinding: "", coreHypothesis: "" },
{ targetGroup: "银发人群50-65岁", keyFinding: "", coreHypothesis: "" },
];
}
function createEmptyTargetHypothesis() {
return { targetGroup: "", keyFinding: "", coreHypothesis: "" };
}
const kanoTypeOptions = ["基本型", "期望型", "兴奋型", "无差异型", "反向型"];
const questionnaireSections = [
{ value: "基本信息", label: "基本信息" },
{ value: "购买行为", label: "购买行为" },
{ value: "需求偏好", label: "需求偏好" },
{ value: "开放建议", label: "开放建议" },
];
const questionnaireQuestionTypes = [
{ value: "SINGLE", label: "单选题" },
{ value: "MULTIPLE", label: "多选题" },
{ value: "LIKERT", label: "Likert 五级量表" },
{ value: "TEXT", label: "开放题" },
];
function createKanoNeeds() {
return Array.from({ length: 3 }, createEmptyKanoNeed);
}
function createEmptyKanoNeed() {
return { feature: "", kanoType: "", featureDirection: "" };
}
function createQuestionnaireQuestions() {
return [
createQuestionnaireQuestion("基本信息", "SINGLE"),
createQuestionnaireQuestion("购买行为", "SINGLE"),
createQuestionnaireQuestion("需求偏好", "LIKERT"),
createQuestionnaireQuestion("开放建议", "TEXT"),
];
}
function createQuestionnaireQuestion(section = "基本信息", type = "SINGLE") {
questionnaireQuestionSequence += 1;
return { id: `question-${Date.now()}-${questionnaireQuestionSequence}`, section, type, title: "", options: "" };
}
function createSurveyReportRows() {
return Array.from({ length: 5 }, () => ({ title: "", content: "" }));
}
const form = ref(createForm());
const taskTitle = computed(() => taskConfig.value?.taskName || "消费者需求与行为");
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
const flowSteps = computed(() => parseArray(taskConfig.value?.steps, []).slice(0, 4));
const activeStepName = computed(() => flowSteps.value[form.value.activeStep] || "");
const progressItems = computed(() => [
{ text: hasQuestionnaireDesign() ? "已完成:问卷设计" : "待完成:问卷设计", status: hasQuestionnaireDesign() ? "done" : "" },
{ text: form.value.dataExcelFileName ? "已完成:数据整理上传" : "待完成整理并上传Excel", status: form.value.dataExcelFileName ? "done" : "" },
{ text: Object.keys(form.value.chartFiles).length ? "进行中:数据可视化截图" : "待完成:数据可视化截图", status: Object.keys(form.value.chartFiles).length ? "done" : "" },
{ text: hasSurveyReport() ? "已完成:问卷调研报告" : "待完成:问卷调研报告", status: hasSurveyReport() ? "done" : "" },
]);
onMounted(async () => {
await loadTaskConfig();
await loadSavedAnswer();
await nextTick();
renderQuestionnaireEditor();
});
async function loadTaskConfig() {
try {
const res = await getTrainingTaskByKey(TASK_KEY);
taskConfig.value = res?.data || null;
} catch (error) {
taskConfig.value = null;
}
}
async function loadSavedAnswer() {
try {
const res = await getStudentTrainingAnswer(TASK_KEY);
const answer = res?.data;
if (answer?.step1Answer) {
applySavedAnswer(answer.step1Answer);
}
} catch (error) {
// 回显失败不阻断页面填写。
}
}
function parseArray(value, fallback) {
if (Array.isArray(value)) return value.length ? value : fallback;
if (!value) return fallback;
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) && parsed.length ? parsed : fallback;
} catch (error) {
const list = String(value)
.split(/[;|,\n]/)
.map((item) => item.trim())
.filter(Boolean);
return list.length ? list : fallback;
}
}
function applySavedAnswer(value) {
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object") return;
form.value = {
...createForm(),
...parsed,
activeStep: Number.isInteger(parsed.activeStep) ? parsed.activeStep : 0,
questionnaireText: normalizeQuestionnaireContent(parsed.questionnaireText || questionnaireTemplate),
targetHypotheses: normalizeTargetHypotheses(parsed.targetHypotheses),
kanoNeeds: normalizeKanoNeeds(parsed.kanoNeeds),
questionnaireQuestions: normalizeQuestionnaireQuestions(parsed.questionnaireQuestions),
questionnaireImport: normalizeQuestionnaireImport(parsed.questionnaireImport),
surveyReportRows: normalizeSurveyReportRows(parsed.surveyReportRows),
chartFiles: parsed.chartFiles || {},
};
nextTick(renderQuestionnaireEditor);
} catch (error) {
// 忽略历史异常数据。
}
}
function normalizeTargetHypotheses(rows) {
if (!Array.isArray(rows) || !rows.length) return createTargetHypotheses();
return rows.map((row) => ({
targetGroup: String(row?.targetGroup || ""),
keyFinding: String(row?.keyFinding || ""),
coreHypothesis: String(row?.coreHypothesis || ""),
}));
}
function normalizeKanoNeeds(rows) {
if (!Array.isArray(rows) || !rows.length) return createKanoNeeds();
return rows.slice(0, KANO_NEED_LIMIT).map((row) => ({
feature: String(row?.feature || ""),
kanoType: kanoTypeOptions.includes(row?.kanoType) ? row.kanoType : "",
featureDirection: String(row?.featureDirection || ""),
}));
}
function normalizeQuestionnaireQuestions(rows) {
if (!Array.isArray(rows) || !rows.length) return createQuestionnaireQuestions();
return rows.map((row, index) => ({
id: String(row?.id || `question-restored-${index}`),
section: questionnaireSections.some((item) => item.value === row?.section) ? row.section : "基本信息",
type: questionnaireQuestionTypes.some((item) => item.value === row?.type) ? row.type : "SINGLE",
title: String(row?.title || ""),
options: String(row?.options || ""),
}));
}
function normalizeQuestionnaireImport(value) {
if (!value || typeof value !== "object" || !value.name) return null;
return {
name: String(value.name),
url: String(value.url || ""),
previewContent: String(value.previewContent || ""),
};
}
function normalizeSurveyReportRows(rows) {
if (!Array.isArray(rows) || !rows.length) return createSurveyReportRows();
return Array.from({ length: 5 }, (_, index) => ({
title: String(rows[index]?.title || ""),
content: String(rows[index]?.content || ""),
}));
}
function syncQuestionnaireText() {
if (!questionnaireEditorRef.value) return;
form.value.questionnaireText = questionnaireEditorRef.value.innerHTML;
}
function renderQuestionnaireEditor() {
if (!questionnaireEditorRef.value) return;
if (questionnaireEditorRef.value.innerHTML !== form.value.questionnaireText) {
questionnaireEditorRef.value.innerHTML = form.value.questionnaireText;
}
}
function normalizeQuestionnaireContent(value) {
const content = String(value || "").trim();
if (!content) return questionnaireTemplate;
return /<\/?[a-z][\s\S]*>/i.test(content) ? content : textToHtml(content);
}
function textToHtml(value) {
return String(value)
.split(/\n{2,}/)
.map((block) => `<p>${escapeHtml(block).replace(/\n/g, "<br>")}</p>`)
.join("");
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function questionnaireTextForExport() {
syncQuestionnaireText();
const wrapper = document.createElement("div");
wrapper.innerHTML = form.value.questionnaireText || "";
return wrapper.innerText.trim();
}
function triggerFileSelect(type, key = "") {
uploadContext.value = { type, key };
fileInputRef.value?.click();
}
function setActiveStep(index) {
form.value.activeStep = index;
if (index === 0) {
nextTick(renderQuestionnaireEditor);
}
}
function addTargetHypothesis() {
form.value.targetHypotheses.push(createEmptyTargetHypothesis());
}
function removeTargetHypothesis() {
if (form.value.targetHypotheses.length <= 1) return;
form.value.targetHypotheses.pop();
}
function addKanoNeed() {
if (form.value.kanoNeeds.length >= KANO_NEED_LIMIT) {
proxy?.$modal?.msgWarning(`最多可填写 ${KANO_NEED_LIMIT} 行`);
return;
}
form.value.kanoNeeds.push(createEmptyKanoNeed());
}
function removeKanoNeed() {
if (form.value.kanoNeeds.length <= 1) return;
form.value.kanoNeeds.pop();
}
function questionPlaceholder(section) {
const placeholders = {
基本信息: "例如:您的年龄处于以下哪个区间?",
购买行为: "例如:您通常通过什么渠道购买智能助眠产品?",
需求偏好: "例如:白噪声可定时关闭对您有多重要?",
开放建议: "例如:您希望智能助眠产品增加哪些功能?",
};
return placeholders[section] || "请输入题目内容";
}
function addQuestionnaireQuestion() {
form.value.questionnaireQuestions.push(createQuestionnaireQuestion());
}
function removeQuestionnaireQuestion(index) {
if (form.value.questionnaireQuestions.length <= 4) return;
form.value.questionnaireQuestions.splice(index, 1);
}
function triggerQuestionnaireImport() {
questionnaireImportInputRef.value?.click();
}
function viewTargetUserProfile() {
window.open("/demand/people", "_blank", "noopener");
}
async function handleFileChange(event) {
const file = event.target.files?.[0];
if (!file) return;
const data = new FormData();
data.append("file", file);
uploading.value = true;
try {
const res = await uploadStudentTrainingFile(data);
const url = res?.url || res?.fileName || res?.data?.url || res?.data?.fileName || "";
const { type, key } = uploadContext.value;
if (type === "questionnaire") {
form.value.questionnaireFileName = file.name;
form.value.questionnaireFileUrl = url;
} else if (type === "dataExcel") {
form.value.dataExcelFileName = file.name;
form.value.dataExcelFileUrl = url;
} else if (type === "chart" && key) {
form.value.chartFiles = {
...form.value.chartFiles,
[key]: { name: file.name, url },
};
}
proxy?.$modal?.msgSuccess("上传成功");
} finally {
uploading.value = false;
event.target.value = "";
}
}
async function handleQuestionnaireImport(event) {
const file = event.target.files?.[0];
if (!file) return;
if (!/\.docx$/i.test(file.name)) {
proxy?.$modal?.msgWarning("请导入 .docx 格式的 Word 问卷,以便在线预览");
event.target.value = "";
return;
}
const data = new FormData();
data.append("file", file);
questionnaireImporting.value = true;
try {
const previewRes = await previewConsumerBehaviorQuestionnaireWord(data);
const uploadRes = await uploadStudentTrainingFile(data);
const url = uploadRes?.url || uploadRes?.fileName || uploadRes?.data?.url || uploadRes?.data?.fileName || "";
form.value.questionnaireImport = {
name: file.name,
url,
previewContent: String(previewRes?.data?.content || ""),
};
showQuestionnairePreview.value = true;
proxy?.$modal?.msgSuccess("问卷导入成功");
} finally {
questionnaireImporting.value = false;
event.target.value = "";
}
}
function goPrev() {
if (form.value.activeStep > 0) setActiveStep(form.value.activeStep - 1);
}
async function goNext() {
if (form.value.activeStep <= 2) await persist("IN_PROGRESS");
if (form.value.activeStep < flowSteps.value.length - 1) setActiveStep(form.value.activeStep + 1);
}
function hasSurveyReport() {
return form.value.surveyReportRows.every((item) => String(item?.title || "").trim() && String(item?.content || "").trim());
}
function hasQuestionnaireDesign() {
return Boolean(form.value.questionnaireImport?.name)
|| form.value.questionnaireQuestions.some((item) => String(item?.title || "").trim());
}
function buildAnswerPayload(status) {
syncQuestionnaireText();
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
saveAction,
currentStep: form.value.activeStep + 1,
step1Answer: JSON.stringify({
...form.value,
taskKey: TASK_KEY,
taskId: taskConfig.value?.id || "",
taskName: taskTitle.value,
flowSteps: flowSteps.value,
updatedAt: new Date().toISOString(),
}),
};
}
async function persist(status = "IN_PROGRESS") {
saving.value = true;
try {
if (status !== "RESET" && (form.value.activeStep === 0 || status === "SUBMITTED")) {
await checkConsumerBehaviorStepOne({ hypotheses: form.value.targetHypotheses });
}
if (status !== "RESET" && (form.value.activeStep === 1 || status === "SUBMITTED")) {
await checkConsumerBehaviorStepTwo({ needs: form.value.kanoNeeds });
}
if (status !== "RESET" && (form.value.activeStep === 2 || status === "SUBMITTED")) {
await checkConsumerBehaviorStepThree({
questions: form.value.questionnaireQuestions,
importedDocumentUrl: form.value.questionnaireImport?.url || "",
});
}
if (status !== "RESET" && (form.value.activeStep === 3 || status === "SUBMITTED")) {
await checkConsumerBehaviorStepFour({ rows: form.value.surveyReportRows });
}
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
} finally {
saving.value = false;
}
}
function saveCurrentProgress() {
return persist("IN_PROGRESS");
}
function submitTask() {
return persist("SUBMITTED");
}
async function resetTraining() {
form.value = createForm();
await nextTick();
renderQuestionnaireEditor();
await persist("RESET");
}
function exportOutcome() {
syncQuestionnaireText();
const lines = [
`任务:${taskTitle.value}`,
"",
`当前步骤:${flowSteps.value[form.value.activeStep] || ""}`,
"",
"问卷调查目标人群与核心假设:",
...form.value.targetHypotheses.map((item, index) => `${index + 1}. 目标人群:${item.targetGroup || ""}\n实训8关键发现${item.keyFinding || ""}\n核心假设${item.coreHypothesis || ""}`),
"",
"KANO 用户需求分类:",
...form.value.kanoNeeds.map((item, index) => `${index + 1}. 功能:${item.feature || ""}\nKANO类型${item.kanoType || ""}\n功能方向${item.featureDirection || ""}`),
"",
"消费者需求调研问卷:",
form.value.questionnaireImport?.name ? `导入问卷:${form.value.questionnaireImport.name}` : "",
...form.value.questionnaireQuestions.map((item, index) => `${index + 1}. [${item.section}] ${item.title || ""}${questionnaireQuestionTypes.find((type) => type.value === item.type)?.label || ""}`),
"",
"问卷设计:",
questionnaireTextForExport(),
"",
"问卷PDF文件",
form.value.questionnaireFileName || "未上传",
"",
"数据收集文件:",
form.value.dataExcelFileName || "未上传",
"",
"问卷调研报告:",
...form.value.surveyReportRows.map((item, index) => `${index + 1}. ${item.title || "未命名模块"}\n${item.content || ""}`),
];
const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${taskTitle.value}-实训结果.txt`;
link.click();
URL.revokeObjectURL(url);
}
function surveyReportTitlePlaceholder(index) {
return ["例如:调研背景与目的", "例如:样本与调研方法", "例如:用户购买决策因素", "例如:不同群体需求差异", "例如:产品建议与结论"][index] || "请输入报告模块标题";
}
function surveyReportContentPlaceholder(index) {
return ["说明本次问卷调研的背景、目标用户与调研目的。", "说明样本数量、调研时间、调研渠道与数据来源。", "结合问卷数据,分析影响用户购买决策的核心因素。", "比较不同人群在需求、价格、渠道或购买场景上的差异。", "总结调研结论,并提出产品功能、定价或营销建议。"][index] || "请输入报告内容";
}
function downloadSurveyReportTemplate() {
const rows = Array.from({ length: 5 }, (_, index) => `<tr><td>${surveyReportTitlePlaceholder(index)}</td><td>${surveyReportContentPlaceholder(index)}</td></tr>`).join("");
const html = `<!doctype html><html><head><meta charset="utf-8"><title>消费者需求调研报告模板</title><style>body{font-family:SimSun,serif;padding:32px;color:#222}h1{font-size:22px;text-align:center}p{line-height:1.75}table{width:100%;border-collapse:collapse;margin-top:24px}th,td{border:1px solid #777;padding:14px;vertical-align:top}th{background:#14a3df;color:#fff}td:first-child{width:22%}</style></head><body><h1>消费者需求调研报告</h1><p>请将步骤三设计的调查问卷导入在线问卷调查平台开展调研,并基于收集到的数据完成本报告。</p><table><thead><tr><th>报告模块标题</th><th>内容</th></tr></thead><tbody>${rows}</tbody></table></body></html>`;
const blob = new Blob([html], { type: "application/msword;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "消费者需求调研报告模板.doc";
link.click();
URL.revokeObjectURL(url);
}
</script>
<style scoped lang="scss">
.consumer-behavior-page {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: minmax(720px, 1fr) 340px;
gap: 24px;
width: 100%;
max-width: 100%;
min-height: calc(100vh - 106px);
margin: 0;
padding: 24px 24px 32px;
color: #e8f7ff;
background: #06111d;
}
.training-core {
min-width: 0;
padding: 28px;
border: 1px solid rgba(0, 180, 255, 0.25);
border-radius: 36px;
background: rgba(8, 18, 28, 0.6);
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(169, 229, 255, 0.08);
backdrop-filter: blur(8px);
}
.task-header {
margin-bottom: 24px;
}
.task-badge {
display: inline-flex;
margin-bottom: 12px;
padding: 4px 14px;
border: 1px solid rgba(0, 170, 255, 0.5);
border-radius: 30px;
color: #d6f6ff;
background: rgba(0, 170, 255, 0.2);
font-size: var(--student-fs-caption);
font-weight: 700;
}
.task-title {
margin: 0;
color: #ffffff;
font-size: var(--student-fs-display);
line-height: 1.3;
font-weight: 900;
}
.goal-bg,
.task-card,
.step-card {
margin-bottom: 28px;
padding: 18px 22px;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.64);
}
.goal-bg {
p,
li {
color: #d7ecff;
font-size: var(--student-fs-body);
line-height: 1.8;
}
p,
ol {
margin: 0;
}
ol {
padding-left: 18px;
}
}
.subtitle-icon {
display: inline-flex;
align-items: center;
gap: 8px;
color: #eafaff;
font-size: var(--student-fs-h1);
font-weight: 900;
}
.subtitle-icon--spaced {
margin-top: 16px;
}
.task-card {
margin-top: 0;
}
.workbench-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 22px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
}
.section-eyebrow {
margin: 0;
color: #71dfff;
font-size: var(--student-fs-caption);
font-weight: 800;
line-height: 1.4;
}
.workbench-head h3 {
margin: 4px 0 8px;
color: #ffffff;
font-size: var(--student-fs-h1);
font-weight: 800;
line-height: 1.3;
}
.workbench-actions {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
margin-left: auto;
gap: 10px;
}
.workbench-description {
margin: 0;
color: #d7ecff;
font-size: var(--student-fs-body);
font-weight: 400;
line-height: 1.8;
}
.outline-action {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(99, 217, 255, 0.55);
border-radius: 999px;
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
font-size: var(--student-fs-button);
font-weight: 800;
&:hover {
transform: translateY(-1px);
border-color: rgba(116, 235, 255, 0.85);
background: rgba(18, 146, 204, 0.5);
}
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
padding: 18px 22px;
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.3);
border-radius: 18px;
background:
linear-gradient(90deg, rgba(6, 42, 61, 0.58), rgba(1, 18, 31, 0.42)),
rgba(0, 0, 0, 0.3);
box-shadow: inset 0 1px 0 rgba(145, 233, 255, 0.08);
scrollbar-width: thin;
}
.step-tab {
display: flex;
flex: 1 0 145px;
align-items: center;
min-width: 0;
min-height: 66px;
padding: 8px 10px;
border: 1px solid rgba(68, 141, 177, 0.35);
border-radius: 8px;
color: #9ab3d0;
background: rgba(3, 27, 43, 0.52);
box-shadow: inset 0 1px 0 rgba(135, 221, 255, 0.04);
font-family: inherit;
font-size: var(--student-fs-body);
font-weight: 800;
text-align: left;
cursor: pointer;
transition: transform 0.2s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
border-color: rgba(89, 223, 255, 0.82);
color: #ffffff;
background: rgba(8, 92, 131, 0.52);
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(152, 239, 255, 0.16);
.step-action {
color: #9bf4ff;
}
}
&.active {
border-color: rgba(89, 226, 255, 0.85);
color: #ffffff;
background: linear-gradient(115deg, rgba(4, 109, 155, 0.78), rgba(5, 62, 102, 0.64));
box-shadow: 0 0 18px rgba(24, 183, 235, 0.22), inset 0 1px 0 rgba(176, 246, 255, 0.2);
.step-marker {
border-color: #72e8ff;
color: #ffffff;
background: linear-gradient(135deg, #08b7e8, #1375d0);
box-shadow: 0 0 0 5px rgba(25, 179, 237, 0.14), 0 0 18px rgba(46, 208, 255, 0.58);
}
}
&.complete {
color: #dffbff;
.step-marker {
border-color: rgba(84, 230, 221, 0.85);
color: #062b37;
background: #6ef0e1;
box-shadow: 0 0 12px rgba(79, 231, 218, 0.35);
}
}
}
.step-marker {
display: inline-flex;
flex: 0 0 38px;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border: 1px solid rgba(101, 176, 208, 0.6);
border-radius: 50%;
color: #a6c4d9;
background: rgba(3, 25, 40, 0.8);
box-shadow: inset 0 0 12px rgba(38, 174, 233, 0.08);
font-size: var(--student-fs-caption);
font-weight: 900;
letter-spacing: 0.04em;
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.step-copy {
display: grid;
min-width: 0;
gap: 4px;
margin-left: 10px;
}
.step-name {
display: -webkit-box;
overflow: hidden;
color: inherit;
font-size: var(--student-fs-body);
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.step-action {
color: #66b6d4;
font-size: var(--student-fs-caption);
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
transition: color 0.2s ease;
}
.step-connector {
position: relative;
flex: 0 1 40px;
min-width: 18px;
height: 2px;
margin: 0 8px;
overflow: visible;
background: rgba(98, 169, 201, 0.34);
}
.step-connector::after {
position: absolute;
top: 50%;
right: -1px;
width: 6px;
height: 6px;
border-top: 1px solid #73b1ce;
border-right: 1px solid #73b1ce;
content: "";
transform: translateY(-50%) rotate(45deg);
}
.step-tab.complete + .step-connector {
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
&::after {
border-color: #63e6f1;
}
}
.step-panel {
min-width: 0;
}
.step-card {
margin-bottom: 0;
}
.step-title-row {
display: flex;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: var(--student-fs-h2);
font-weight: 900;
line-height: 1.3;
}
}
.panel-title {
padding-bottom: 14px;
margin-bottom: 18px;
border-bottom: 1px solid rgba(51, 169, 217, 0.38);
}
.panel-title span {
color: #6eeaff;
font-weight: 900;
}
.panel-title h4 {
margin: 8px 0 0;
color: #f8fcff;
font-size: var(--student-fs-h1);
line-height: 1.2;
}
.task-desc {
margin: 14px 0 18px;
color: #c7d8e6;
font-size: var(--student-fs-body);
line-height: 1.8;
}
.questionnaire-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
margin-bottom: 12px;
}
.hypothesis-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 18px 28px;
margin-bottom: 20px;
}
.hypothesis-desc {
margin: 0;
color: #d9eaf4;
font-size: var(--student-fs-h2);
font-weight: 600;
}
.hypothesis-actions,
.kano-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
}
.hypothesis-actions :deep(.hypothesis-action),
.kano-actions :deep(.hypothesis-action),
.training-eight-action {
min-width: 146px;
height: 42px;
border: 1px solid rgba(92, 224, 255, 0.7);
border-radius: 10px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
font-size: var(--student-fs-button-lg);
font-weight: 800;
}
.hypothesis-actions :deep(.hypothesis-action--quiet),
.kano-actions :deep(.hypothesis-action--quiet) {
border-color: rgba(115, 191, 220, 0.58);
color: #d9f4ff;
background: rgba(13, 85, 117, 0.72);
}
.training-eight-action {
grid-column: 2;
grid-row: 1;
min-width: 136px;
}
.target-hypothesis-table-wrap {
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.24);
border-radius: 18px;
background: rgba(1, 14, 26, 0.72);
}
.target-hypothesis-table {
width: 100%;
min-width: 920px;
border-collapse: collapse;
table-layout: fixed;
th,
td {
padding: 12px;
border-right: 1px solid rgba(77, 167, 220, 0.26);
border-bottom: 1px solid rgba(77, 167, 220, 0.26);
vertical-align: middle;
}
th {
color: #ecfbff;
background: linear-gradient(100deg, #158fc4, #08739f);
font-size: var(--student-fs-h2);
font-weight: 800;
}
th:nth-child(1) { width: 28%; }
th:nth-child(2) { width: 28%; }
th:nth-child(3) { width: 44%; }
tr:last-child td { border-bottom: 0; }
th:last-child,
td:last-child { border-right: 0; }
}
.hypothesis-input,
.hypothesis-textarea {
box-sizing: border-box;
width: 100%;
border: 1px solid rgba(67, 134, 166, 0.8);
outline: none;
color: #eaf7ff;
background: rgba(7, 29, 43, 0.92);
font-family: inherit;
font-size: var(--student-fs-body);
line-height: 1.65;
&::placeholder { color: #8da5b2; opacity: 1; }
&:focus {
border-color: #5fe7ff;
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.hypothesis-input {
height: 42px;
padding: 0 12px;
border-radius: 10px;
}
.hypothesis-textarea {
min-height: 86px;
padding: 10px 12px;
border-radius: 10px;
resize: vertical;
}
.hypothesis-tip {
margin: 12px 0 0;
color: #75dff7;
font-size: var(--student-fs-body-sm);
}
.kano-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px 28px;
margin-bottom: 20px;
}
.kano-desc {
max-width: 780px;
margin: 0;
color: #d9eaf4;
font-size: var(--student-fs-h2);
font-weight: 600;
}
.kano-table-wrap {
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.24);
border-radius: 18px;
background: rgba(1, 14, 26, 0.72);
}
.kano-table {
width: 100%;
min-width: 980px;
border-collapse: collapse;
table-layout: fixed;
th,
td {
padding: 10px 12px;
border-right: 1px solid rgba(77, 167, 220, 0.26);
border-bottom: 1px solid rgba(77, 167, 220, 0.26);
vertical-align: middle;
}
th {
color: #ecfbff;
background: linear-gradient(100deg, #158fc4, #08739f);
font-size: var(--student-fs-h2);
font-weight: 800;
}
th:nth-child(1) { width: 9%; }
th:nth-child(2) { width: 42%; }
th:nth-child(3) { width: 25%; }
th:nth-child(4) { width: 24%; }
tr:last-child td { border-bottom: 0; }
th:last-child,
td:last-child { border-right: 0; }
}
.kano-index {
color: #83e9ff;
font-weight: 800;
text-align: center;
}
.kano-textarea {
min-height: 58px;
}
.kano-select-cell {
text-align: center;
}
.kano-select {
width: min(100%, 190px);
}
.kano-select :deep(.el-select__wrapper) {
min-height: 42px;
border: 1px solid rgba(67, 134, 166, 0.8);
border-radius: 10px;
box-shadow: none;
color: #eaf7ff;
background: rgba(7, 29, 43, 0.92);
}
.kano-select :deep(.el-select__placeholder) {
color: #8da5b2;
}
.questionnaire-design-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px 28px;
margin-bottom: 20px;
}
.questionnaire-design-desc {
max-width: 820px;
margin: 0;
color: #d9eaf4;
font-size: var(--student-fs-h2);
font-weight: 600;
}
.questionnaire-import-action,
.questionnaire-editor-launcher__action {
min-width: 178px;
height: 44px;
border: 1px solid rgba(92, 224, 255, 0.7);
border-radius: 10px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
font-size: var(--student-fs-button-lg);
font-weight: 800;
}
.questionnaire-imported-file {
display: inline-flex;
align-items: center;
gap: 10px;
max-width: 100%;
min-height: 42px;
padding: 0 13px;
margin-bottom: 18px;
border: 1px solid rgba(87, 215, 255, 0.4);
border-radius: 12px;
color: #dff9ff;
background: rgba(10, 97, 137, 0.28);
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
button {
flex: 0 0 auto;
border: 0;
color: #75e5ff;
background: transparent;
cursor: pointer;
font-weight: 800;
}
}
.questionnaire-editor-launcher {
display: flex;
align-items: center;
justify-content: space-between;
gap: 32px;
min-height: 190px;
padding: 32px 40px;
overflow: hidden;
border: 1px solid rgba(54, 195, 243, 0.36);
border-radius: 20px;
background:
radial-gradient(circle at 86% 18%, rgba(31, 207, 255, 0.22), transparent 34%),
linear-gradient(115deg, rgba(7, 59, 87, 0.94), rgba(2, 20, 34, 0.9));
}
.questionnaire-editor-launcher__copy {
position: relative;
z-index: 1;
h4 {
margin: 7px 0;
color: #ffffff;
font-size: 23px;
}
p {
max-width: 600px;
margin: 0;
color: #b6d1df;
line-height: 1.7;
}
}
.questionnaire-editor-launcher__action {
flex: 0 0 auto;
min-width: 170px;
height: 52px;
}
.questionnaire-editor-summary {
margin: 13px 0 0;
color: #72deff;
font-size: var(--student-fs-body-sm);
}
:deep(.questionnaire-editor-dialog .el-dialog),
:deep(.questionnaire-preview-dialog .el-dialog) {
overflow: hidden;
border: 1px solid rgba(70, 200, 247, 0.35);
border-radius: 18px;
color: #e7f8ff;
background: #061724;
}
:deep(.questionnaire-editor-dialog .el-dialog__title),
:deep(.questionnaire-preview-dialog .el-dialog__title) {
color: #ffffff;
font-weight: 800;
}
.questionnaire-editor-dialog__intro {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 18px;
p {
margin: 0;
color: #b9d3e0;
line-height: 1.7;
}
}
.questionnaire-editor-add {
flex: 0 0 auto;
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.questionnaire-question-list {
display: grid;
gap: 12px;
max-height: min(58vh, 700px);
padding-right: 5px;
overflow: auto;
}
.questionnaire-question-card {
display: grid;
grid-template-columns: 36px minmax(0, 1fr) auto;
gap: 12px;
align-items: start;
padding: 14px;
border: 1px solid rgba(67, 144, 184, 0.34);
border-radius: 14px;
background: rgba(13, 45, 63, 0.66);
}
.questionnaire-question-card__number {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 1px solid rgba(80, 222, 255, 0.45);
border-radius: 50%;
color: #74e5ff;
font-size: var(--student-fs-caption);
font-weight: 800;
}
.questionnaire-question-card__fields {
display: grid;
grid-template-columns: 180px 180px minmax(0, 1fr);
gap: 10px;
:deep(.el-select__wrapper),
:deep(.el-input__wrapper) {
min-height: 38px;
border: 1px solid rgba(67, 134, 166, 0.8);
box-shadow: none;
color: #eaf7ff;
background: rgba(5, 26, 39, 0.9);
}
:deep(.el-input__inner) { color: #eaf7ff; }
}
.likert-tip {
grid-column: 1 / -1;
margin: 0;
color: #7ce0f8;
font-size: var(--student-fs-caption);
}
.questionnaire-question-card__remove {
padding: 8px 10px;
border: 1px solid rgba(104, 161, 187, 0.5);
border-radius: 8px;
color: #bddfeb;
background: transparent;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: 0.45;
}
}
.questionnaire-word-preview {
max-height: min(62vh, 760px);
padding: 20px;
overflow: auto;
border: 1px solid rgba(77, 167, 220, 0.28);
border-radius: 12px;
color: #e2f3fa;
background: rgba(3, 25, 38, 0.86);
white-space: pre-wrap;
line-height: 1.85;
}
.questionnaire-preview-empty {
color: #b9d3e0;
}
.survey-report-desc {
max-width: 1050px;
margin-bottom: 24px;
}
.survey-report-table-wrap {
overflow-x: auto;
border: 1px solid rgba(65, 169, 214, 0.52);
border-radius: 16px;
background: rgba(2, 23, 36, 0.56);
}
.survey-report-table {
width: 100%;
min-width: 760px;
border-collapse: collapse;
th,
td {
border: 1px solid rgba(77, 148, 183, 0.5);
}
th {
padding: 14px 18px;
color: #e9fbff;
background: rgba(9, 111, 157, 0.68);
font-size: var(--student-fs-body);
letter-spacing: 0.04em;
}
th:first-child,
td:first-child {
width: 22%;
}
td {
padding: 10px;
background: rgba(5, 31, 47, 0.42);
}
}
.survey-report-title-input,
.survey-report-content-input {
display: block;
width: 100%;
border: 0;
outline: 0;
color: #e7f8ff;
background: transparent;
font: inherit;
&::placeholder {
color: #668a9b;
}
}
.survey-report-title-input {
min-height: 88px;
padding: 8px;
text-align: center;
font-weight: 700;
}
.survey-report-content-input {
min-height: 88px;
padding: 8px 12px;
resize: vertical;
line-height: 1.75;
}
.survey-report-guide {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 28px;
margin-top: 26px;
padding: 20px 22px;
border: 1px solid rgba(69, 164, 207, 0.27);
border-radius: 14px;
color: #b8d2de;
background: linear-gradient(105deg, rgba(6, 48, 69, 0.75), rgba(5, 24, 38, 0.52));
h5,
p {
margin: 0;
line-height: 1.85;
}
h5 {
color: #ffffff;
font-size: var(--student-fs-body);
}
}
.survey-report-template-action {
flex: 0 0 auto;
min-width: 146px;
min-height: 42px;
}
.questionnaire-desc {
flex: 1;
margin: 0;
}
.questionnaire-upload {
min-width: 0;
}
.questionnaire-file {
margin-bottom: 10px;
}
.questionnaire-field {
margin-top: 0;
}
.large-textarea.questionnaire-textarea {
height: 920px;
min-height: 920px;
white-space: pre-wrap;
overflow: auto;
}
.questionnaire-textarea[contenteditable="true"] {
resize: none;
white-space: normal;
}
.questionnaire-textarea[contenteditable="true"]:empty::before {
color: #8fa8b8;
content: attr(data-placeholder);
}
.questionnaire-textarea :deep(h4) {
margin: 0 0 26px;
color: #f4fbff;
font-size: var(--student-fs-h1);
font-weight: 900;
}
.questionnaire-textarea :deep(p) {
margin: 0 0 10px;
}
.questionnaire-textarea :deep(strong) {
display: inline-block;
margin-top: 16px;
color: #f4fbff;
font-size: var(--student-fs-h2);
}
.questionnaire-textarea :deep(table) {
width: min(820px, 100%);
margin: 10px 0 24px;
border-collapse: collapse;
color: #e7f5ff;
background: rgba(1, 14, 26, 0.72);
font-size: var(--student-fs-body);
table-layout: fixed;
}
.questionnaire-textarea :deep(th),
.questionnaire-textarea :deep(td) {
height: 38px;
padding: 6px 10px;
border: 1px solid rgba(0, 180, 255, 0.22);
text-align: left;
vertical-align: middle;
}
.questionnaire-textarea :deep(th) {
color: #dff5ff;
background: rgba(8, 55, 82, 0.98);
font-weight: 900;
}
.questionnaire-textarea :deep(th:first-child),
.questionnaire-textarea :deep(td:first-child) {
width: 180px;
font-weight: 800;
}
.field-block {
display: block;
margin-top: 18px;
}
.field-block > span {
display: block;
margin-bottom: 10px;
color: #f1f8ff;
font-size: var(--student-fs-body);
font-weight: 900;
}
.large-textarea {
width: 100%;
min-height: 180px;
padding: 16px 18px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #eef5ff;
background: #0f1a24;
font-size: var(--student-fs-body);
line-height: 1.7;
transition: box-shadow 0.16s ease, border-color 0.16s ease;
}
.large-textarea::placeholder {
color: #8fa8b8;
opacity: 0.9;
}
.large-textarea:focus {
border-color: rgba(102, 235, 255, 0.9);
box-shadow: inset 0 0 0 1px rgba(102, 235, 255, 0.42), 0 0 20px rgba(0, 159, 220, 0.16);
}
.file-input {
display: none;
}
.upload-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 18px;
}
.upload-action,
.mini-upload {
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(92, 224, 255, 0.7);
border-radius: 999px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
font-weight: 800;
}
.file-chip {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 100%;
padding: 8px 12px;
border: 1px solid rgba(95, 232, 255, 0.38);
border-radius: 999px;
color: #9fe8ff;
background: rgba(1, 14, 26, 0.72);
font-size: var(--student-fs-body-sm);
font-weight: 700;
span {
overflow-wrap: anywhere;
}
}
.data-board {
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.data-board pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: #d7ecff;
font-family: inherit;
font-size: var(--student-fs-body);
line-height: 1.78;
}
.visual-group {
margin-top: 22px;
}
.visual-group h5 {
margin: 0 0 12px;
color: #f1f8ff;
font-size: var(--student-fs-h2);
}
.analysis-heading {
margin: 28px 0 16px;
color: #f1f8ff;
font-size: var(--student-fs-h2);
font-weight: 900;
line-height: 1.8;
}
:deep(.report-fields) {
display: grid;
gap: 18px;
}
:deep(.report-fields .field-block) {
display: block;
width: 100%;
margin-top: 0;
}
:deep(.report-fields .field-block > span) {
display: block;
margin-bottom: 8px;
color: #f1f8ff;
font-size: var(--student-fs-body);
font-weight: 900;
}
:deep(.report-fields .large-textarea) {
display: block;
width: 100%;
min-height: 132px;
padding: 14px 16px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: var(--student-fs-body);
line-height: 1.7;
box-sizing: border-box;
}
:deep(.report-fields .large-textarea:placeholder-shown) {
color: #cbd4dc;
}
:deep(.report-fields .large-textarea:focus) {
border-color: rgba(102, 235, 255, 0.9);
box-shadow: inset 0 0 0 1px rgba(102, 235, 255, 0.42), 0 0 20px rgba(0, 159, 220, 0.16);
}
:deep(.report-fields .large-textarea::placeholder) {
color: #cbd4dc;
opacity: 1;
}
.visual-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.visual-card {
min-height: 120px;
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 20px;
background: rgba(1, 14, 26, 0.72);
}
.visual-card span {
display: block;
margin-bottom: 14px;
color: #e8f8ff;
font-weight: 900;
}
.visual-card small {
display: block;
margin-top: 10px;
color: #9fdced;
word-break: break-all;
}
.step-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 22px;
margin-bottom: 36px;
}
.nav-action {
min-height: 40px;
padding: 0 18px;
border-color: rgba(99, 217, 255, 0.55);
border-radius: 999px;
color: #e9fbff;
background: rgba(17, 126, 178, 0.36);
font-size: var(--student-fs-button);
font-weight: 800;
}
.nav-action--next {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.nav-action--save {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.nav-action.is-disabled {
opacity: 0.45;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
}
.training-actions :deep(.training-action) {
min-width: 172px;
height: 64px;
padding: 0 28px;
border: 1px solid #2c83aa;
border-radius: 6px;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
box-shadow: none;
font-size: var(--student-fs-h1);
font-weight: 900;
line-height: 1;
letter-spacing: 0;
text-shadow: none;
}
.training-actions :deep(.training-action span) {
display: inline-flex;
align-items: center;
color: inherit;
font-size: inherit;
line-height: 1;
}
.training-actions :deep(.training-action--submit),
.training-actions :deep(.training-action--reset) {
border-color: #2c83aa;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
}
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #46b5e7;
color: #ffffff;
background: rgba(18, 85, 116, 0.78);
}
@media (max-width: 1200px) {
.consumer-behavior-page {
grid-template-columns: 1fr;
}
.visual-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.consumer-behavior-page {
padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
.workbench-actions :deep(.material-download),
.workbench-actions .outline-action {
width: 100%;
}
.hypothesis-head {
grid-template-columns: 1fr;
}
.training-eight-action {
grid-column: auto;
grid-row: auto;
width: 100%;
}
.hypothesis-actions,
.kano-actions {
justify-content: stretch;
}
.hypothesis-actions :deep(.hypothesis-action),
.kano-actions :deep(.hypothesis-action) {
flex: 1;
}
.kano-head {
align-items: stretch;
flex-direction: column;
}
.questionnaire-design-head,
.questionnaire-editor-launcher,
.questionnaire-editor-dialog__intro {
align-items: stretch;
flex-direction: column;
}
.questionnaire-import-action,
.questionnaire-editor-launcher__action {
width: 100%;
}
.questionnaire-editor-launcher {
padding: 26px;
}
.questionnaire-question-card {
grid-template-columns: 32px minmax(0, 1fr);
}
.questionnaire-question-card__fields {
grid-template-columns: 1fr;
}
.questionnaire-question-card__remove {
grid-column: 2;
justify-self: end;
}
.survey-report-guide {
flex-direction: column;
}
.survey-report-template-action {
width: 100%;
}
.step-indicator {
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 22px;
width: 2px;
height: 22px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.step-actions :deep(.nav-action),
.training-actions :deep(.training-action) {
width: 100%;
}
}
@media (max-width: 760px) {
.training-core,
.task-card,
.step-card {
padding: 18px;
}
.questionnaire-head {
flex-direction: column;
}
.questionnaire-upload {
width: 100%;
}
.training-actions {
gap: 14px;
}
.training-actions :deep(.training-action) {
width: 100%;
}
.visual-grid {
grid-template-columns: 1fr;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>