|
|
<template>
|
|
|
<div class="consumer-behavior-page student-training-shell">
|
|
|
<main class="training-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<p class="task-brief">
|
|
|
<span class="task-brief-text">{{ taskBackground }}</span>
|
|
|
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
|
|
|
{{ briefOpen ? "收起" : "展开目标与要求" }}
|
|
|
</button>
|
|
|
<span v-if="briefOpen" class="task-brief-more">
|
|
|
<b>实训目标</b>
|
|
|
<ul>
|
|
|
<li v-for="goal in taskGoals" :key="goal">{{ goal }}</li>
|
|
|
</ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-action-strip">
|
|
|
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
|
|
|
</section>
|
|
|
|
|
|
<section class="task-card">
|
|
|
|
|
|
<nav v-if="flowSteps.length" class="step-indicator" aria-label="消费者需求与行为步骤">
|
|
|
<button v-for="(step, index) in flowSteps" :key="step" type="button"
|
|
|
:class="['step-tab', { active: form.activeStep === index, complete: form.activeStep > index }]"
|
|
|
:aria-label="`第 ${index + 1} 步:${step}`"
|
|
|
:aria-current="form.activeStep === index ? 'step' : undefined" :title="step"
|
|
|
@click="setActiveStep(index)">
|
|
|
<span class="step-bar" aria-hidden="true"></span>
|
|
|
<span class="step-no" aria-hidden="true">{{ String(index + 1).padStart(2, '0') }}</span>
|
|
|
<span class="step-name">{{ step }}</span>
|
|
|
</button>
|
|
|
</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><el-input v-model="hypothesis.targetGroup" class="hypothesis-input"
|
|
|
placeholder="例如:职场白领(25-35岁)" /></td>
|
|
|
<td><el-input v-model="hypothesis.keyFinding" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" class="hypothesis-textarea"
|
|
|
placeholder="例如:高压工作、入睡困难、看重效率" /></td>
|
|
|
<td><el-input v-model="hypothesis.coreHypothesis" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" 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><el-input v-model="need.feature" type="textarea" :autosize="{ minRows: 2 }"
|
|
|
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><el-input v-model="need.featureDirection" type="textarea"
|
|
|
:autosize="{ minRows: 2 }" 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 五级量表对步骤二提到的功能进行需求度评价)、开放建议(用户自由表达对案例产品的期望与顾虑)。
|
|
|
</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>
|
|
|
<div class="questionnaire-editor-launcher__actions">
|
|
|
<el-button class="questionnaire-editor-launcher__action"
|
|
|
@click="questionnaireEditorVisible = true">进入问卷编辑器</el-button>
|
|
|
<el-button class="questionnaire-editor-launcher__action"
|
|
|
:disabled="!hasEditableQuestionnaire" @click="openQuestionnairePreview">预览问卷</el-button>
|
|
|
<el-button class="questionnaire-editor-launcher__action"
|
|
|
:disabled="!hasEditableQuestionnaire" @click="downloadQuestionnaireWord">下载问卷(Word)</el-button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<p class="questionnaire-editor-summary">
|
|
|
已编辑 {{ form.questionnaireQuestions.length }} 题;{{
|
|
|
form.questionnaireImport?.name ? "已导入 1 份 Word 问卷,可随时预览。" : "也可下载规范模板,在 Word 中编好题目后导入。"
|
|
|
}}
|
|
|
</p>
|
|
|
|
|
|
<div class="questionnaire-template-guide">
|
|
|
<div>
|
|
|
<h5>推荐工具及方法如下,自行选择:</h5>
|
|
|
<p>1. 使用本系统问卷编辑器,自行编辑问卷。</p>
|
|
|
<p>2. 下载规范导入模板,按模板格式在 Word 中编好题目后导入,系统自动识别为题目。</p>
|
|
|
<p>3. 编辑或导入完成后,可在线预览问卷并下载 Word 文件。</p>
|
|
|
</div>
|
|
|
<el-button class="questionnaire-template-action" @click="downloadQuestionnaireImportTemplate">
|
|
|
<el-icon>
|
|
|
<Download />
|
|
|
</el-icon>
|
|
|
下载导入模板
|
|
|
</el-button>
|
|
|
</div>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<el-dialog v-model="showQuestionnaireComposePreview" title="问卷预览" width="min(860px, 94vw)"
|
|
|
class="questionnaire-compose-dialog" append-to-body>
|
|
|
<div class="questionnaire-compose-preview">
|
|
|
<h3 class="questionnaire-compose-preview__title">消费者需求调研问卷</h3>
|
|
|
<template v-for="section in questionnaireSections" :key="section.value">
|
|
|
<div v-if="questionsBySection(section.value).length"
|
|
|
class="questionnaire-compose-preview__section">
|
|
|
<h4>{{ section.label }}</h4>
|
|
|
<ol>
|
|
|
<li v-for="question in questionsBySection(section.value)" :key="question.id">
|
|
|
<p class="question-title">{{ question.title }}<span
|
|
|
class="question-type">({{ questionTypeLabel(question.type)
|
|
|
}})</span></p>
|
|
|
<ul v-if="questionOptions(question).length" class="question-options">
|
|
|
<li v-for="(option, optIndex) in questionOptions(question)"
|
|
|
:key="optIndex">{{ optionLetter(optIndex) }}. {{ option }}</li>
|
|
|
</ul>
|
|
|
<p v-else-if="question.type === 'TEXT'" class="question-open-line">________________________</p>
|
|
|
</li>
|
|
|
</ol>
|
|
|
</div>
|
|
|
</template>
|
|
|
<p v-if="!hasEditableQuestionnaire" class="questionnaire-compose-preview__empty">暂无可预览的题目,请先编辑或导入问卷。</p>
|
|
|
</div>
|
|
|
<template #footer>
|
|
|
<el-button @click="showQuestionnaireComposePreview = false">关闭</el-button>
|
|
|
<el-button type="primary" :disabled="!hasEditableQuestionnaire"
|
|
|
@click="downloadQuestionnaireWord">下载问卷(Word)</el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
</section>
|
|
|
|
|
|
<section v-else class="step-panel">
|
|
|
<p class="task-desc survey-report-desc">
|
|
|
将步骤三设计的调查问卷导入问卷星等在线问卷调查平台,开展问卷调查。基于收集到的数据,填写问卷调查报告。
|
|
|
</p>
|
|
|
<div class="survey-report-actions">
|
|
|
<el-button :disabled="form.surveyReportRows.length >= 12" @click="addSurveyReportRow">
|
|
|
<el-icon>
|
|
|
<Plus />
|
|
|
</el-icon>
|
|
|
增加一行
|
|
|
</el-button>
|
|
|
<el-button :disabled="form.surveyReportRows.length <= 1" @click="removeSurveyReportRow">
|
|
|
<el-icon>
|
|
|
<Minus />
|
|
|
</el-icon>
|
|
|
删除一行
|
|
|
</el-button>
|
|
|
</div>
|
|
|
<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>
|
|
|
<el-input v-model="row.title" class="survey-report-title-input"
|
|
|
:placeholder="surveyReportTitlePlaceholder(index)"
|
|
|
:aria-label="`第${index + 1}个报告模块标题`" />
|
|
|
</td>
|
|
|
<td>
|
|
|
<el-input v-model="row.content" type="textarea" :autosize="{ minRows: 3 }"
|
|
|
class="survey-report-content-input"
|
|
|
:placeholder="surveyReportContentPlaceholder(index)"
|
|
|
:aria-label="`第${index + 1}个报告模块内容`" />
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<div class="step-navigation">
|
|
|
<div class="step-nav-left">
|
|
|
<button v-if="form.activeStep > 0" type="button" class="btn-nav btn-ghost"
|
|
|
:disabled="saving" @click="goPrev">
|
|
|
<el-icon>
|
|
|
<ArrowLeft />
|
|
|
</el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving"
|
|
|
@click="saveCurrentProgress">
|
|
|
<el-icon>
|
|
|
<CircleCheck />
|
|
|
</el-icon>
|
|
|
保存
|
|
|
</button>
|
|
|
</div>
|
|
|
<div class="step-nav-right">
|
|
|
<span class="step-progress">Step {{ String(form.activeStep + 1).padStart(2, "0") }} / {{
|
|
|
String(flowSteps.length).padStart(2, "0") }}</span>
|
|
|
<button v-if="form.activeStep < flowSteps.length - 1" type="button"
|
|
|
class="btn-nav btn-primary" :disabled="saving" @click="goNext">
|
|
|
下一步
|
|
|
<el-icon>
|
|
|
<ArrowRight />
|
|
|
</el-icon>
|
|
|
</button>
|
|
|
</div>
|
|
|
</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 { useRouter } from "vue-router"
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Minus, Plus, Upload } from "@element-plus/icons-vue"
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
|
|
|
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask"
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap"
|
|
|
import { checkConsumerBehaviorStepOne, checkConsumerBehaviorStepTwo, checkConsumerBehaviorStepThree, checkConsumerBehaviorStepFour, getStudentTrainingAnswer, previewConsumerBehaviorQuestionnaireWord, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
|
|
|
import JSZip from "jszip"
|
|
|
|
|
|
const TASK_KEY = "consumer-behavior"
|
|
|
const KANO_NEED_LIMIT = 20
|
|
|
let questionnaireQuestionSequence = 0
|
|
|
const { proxy } = getCurrentInstance()
|
|
|
const router = useRouter()
|
|
|
|
|
|
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: "开放题" },
|
|
|
]
|
|
|
const LIKERT_SCALE_TEXT = "非常不重要、较不重要、一般、较重要、非常重要"
|
|
|
const showQuestionnaireComposePreview = ref(false)
|
|
|
const hasEditableQuestionnaire = computed(() => form.value.questionnaireQuestions.some((item) => String(item?.title || "").trim()))
|
|
|
|
|
|
function questionTypeLabel(type) { return questionnaireQuestionTypes.find((item) => item.value === type)?.label || "单选题" }
|
|
|
function questionOptions(question) {
|
|
|
if (question.type === "LIKERT") return LIKERT_SCALE_TEXT.split("、")
|
|
|
if (question.type === "TEXT") return []
|
|
|
return String(question.options || "").split(/[,,、\n]/).map((item) => item.trim()).filter(Boolean)
|
|
|
}
|
|
|
function questionsBySection(section) { return form.value.questionnaireQuestions.filter((item) => item.section === section && String(item?.title || "").trim()) }
|
|
|
function optionLetter(index) { return String.fromCharCode(65 + index) }
|
|
|
function openQuestionnairePreview() { showQuestionnaireComposePreview.value = true }
|
|
|
|
|
|
// ===== 规范模板导入:从 Word 解析文本中按模板格式识别题目 =====
|
|
|
function parseQuestionnaireContent(content) {
|
|
|
const sectionNames = questionnaireSections.map((item) => item.value)
|
|
|
const typeKeywords = [["多选", "MULTIPLE"], ["量表", "LIKERT"], ["开放", "TEXT"], ["单选", "SINGLE"]]
|
|
|
const questions = []
|
|
|
let section = "基本信息"
|
|
|
const lines = String(content || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
|
for (const line of lines) {
|
|
|
const sectionMatch = line.match(/^【(.+?)】$/)
|
|
|
if (sectionMatch) {
|
|
|
const name = sectionNames.find((item) => sectionMatch[1].includes(item))
|
|
|
if (name) section = name
|
|
|
continue
|
|
|
}
|
|
|
const questionMatch = line.match(/^(\d+)\s*[.、.]\s*(.+)$/)
|
|
|
if (questionMatch) {
|
|
|
let title = questionMatch[2].trim()
|
|
|
let type = "SINGLE"
|
|
|
const typeMatch = title.match(/[((]([^))]{1,8})[))]\s*$/)
|
|
|
if (typeMatch) {
|
|
|
const hit = typeKeywords.find(([keyword]) => typeMatch[1].includes(keyword))
|
|
|
if (hit) {
|
|
|
type = hit[1]
|
|
|
title = title.slice(0, typeMatch.index).trim()
|
|
|
}
|
|
|
}
|
|
|
questions.push({ id: `question-${Date.now()}-${questions.length + 1}`, section, type, title, options: type === "LIKERT" ? LIKERT_SCALE_TEXT : "" })
|
|
|
continue
|
|
|
}
|
|
|
const optionMatch = line.match(/^([A-Z])\s*[.、.]\s*(.+)$/)
|
|
|
if (optionMatch && questions.length) {
|
|
|
const target = questions[questions.length - 1]
|
|
|
if (target.type === "SINGLE" || target.type === "MULTIPLE") {
|
|
|
target.options = target.options ? `${target.options},${optionMatch[2].trim()}` : optionMatch[2].trim()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return questions.filter((item) => item.title)
|
|
|
}
|
|
|
|
|
|
// ===== 问卷 Word 生成(最小可用 .docx,兼容 Word 与后端 POI 解析) =====
|
|
|
function escapeXmlValue(value) { return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """) }
|
|
|
async function buildDocxBlob(paragraphs) {
|
|
|
const body = paragraphs.map((item) => {
|
|
|
const props = `${item.align === "center" ? '<w:jc w:val="center"/>' : ""}`
|
|
|
const runProps = `${item.bold ? "<w:b/>" : ""}<w:sz w:val="${item.size || 24}"/>`
|
|
|
return `<w:p><w:pPr>${props}</w:pPr><w:r><w:rPr>${runProps}</w:rPr><w:t xml:space="preserve">${escapeXmlValue(item.text)}</w:t></w:r></w:p>`
|
|
|
}).join("")
|
|
|
const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${body}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/></w:sectPr></w:body></w:document>`
|
|
|
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`
|
|
|
const rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`
|
|
|
const zip = new JSZip()
|
|
|
zip.file("[Content_Types].xml", contentTypes)
|
|
|
zip.folder("_rels").file(".rels", rels)
|
|
|
zip.folder("word").file("document.xml", documentXml)
|
|
|
return zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" })
|
|
|
}
|
|
|
function downloadBlob(blob, fileName) {
|
|
|
const link = document.createElement("a")
|
|
|
link.href = URL.createObjectURL(blob)
|
|
|
link.download = fileName
|
|
|
document.body.appendChild(link)
|
|
|
link.click()
|
|
|
document.body.removeChild(link)
|
|
|
setTimeout(() => URL.revokeObjectURL(link.href), 2000)
|
|
|
}
|
|
|
function questionnaireDocxParagraphs() {
|
|
|
const paragraphs = [{ text: "消费者需求调研问卷", bold: true, size: 32, align: "center" }]
|
|
|
questionnaireSections.forEach((section) => {
|
|
|
const items = questionsBySection(section.value)
|
|
|
if (!items.length) return
|
|
|
paragraphs.push({ text: section.label, bold: true, size: 28 })
|
|
|
items.forEach((item) => {
|
|
|
paragraphs.push({ text: `${items.indexOf(item) + 1}. ${item.title}(${questionTypeLabel(item.type)})`, size: 24 })
|
|
|
if (["SINGLE", "MULTIPLE"].includes(item.type)) {
|
|
|
questionOptions(item).forEach((option, optIndex) => paragraphs.push({ text: `${optionLetter(optIndex)}. ${option}`, size: 24 }))
|
|
|
} else if (item.type === "LIKERT") {
|
|
|
paragraphs.push({ text: questionOptions(item).map((option, optIndex) => `${optionLetter(optIndex)}. ${option}`).join(" "), size: 24 })
|
|
|
} else {
|
|
|
paragraphs.push({ text: "________________________", size: 24 })
|
|
|
}
|
|
|
})
|
|
|
})
|
|
|
return paragraphs
|
|
|
}
|
|
|
async function downloadQuestionnaireWord() {
|
|
|
if (!hasEditableQuestionnaire.value) return
|
|
|
downloadBlob(await buildDocxBlob(questionnaireDocxParagraphs()), "消费者需求调研问卷.docx")
|
|
|
proxy?.$modal?.msgSuccess("问卷已导出为 Word 文件")
|
|
|
}
|
|
|
async function downloadQuestionnaireImportTemplate() {
|
|
|
const paragraphs = [
|
|
|
{ text: "消费者需求调研问卷 · 导入模板", bold: true, size: 32, align: "center" },
|
|
|
{ text: "填写说明:", bold: true, size: 24 },
|
|
|
{ text: "1. 分组行用【】标注,仅支持:基本信息、购买行为、需求偏好、开放建议。", size: 21 },
|
|
|
{ text: "2. 题目行以数字加“.”开头,题干末尾用括号标注题型:(单选)(多选)(量表)(开放题),不填默认单选。", size: 21 },
|
|
|
{ text: "3. 选项行以“A.”“B.”开头,每行一个选项;(量表)题型无需选项,系统固定五级量表。", size: 21 },
|
|
|
{ text: "4. 编辑完成后,通过“导入问卷(Word 文档)”上传本文件,系统自动识别题目并可在线预览、下载。", size: 21 },
|
|
|
{ text: "【基本信息】", bold: true, size: 24 },
|
|
|
{ text: "1. 您的年龄段是?(单选)", size: 24 },
|
|
|
{ text: "A. 18-25岁", size: 24 }, { text: "B. 26-35岁", size: 24 }, { text: "C. 36-45岁", size: 24 }, { text: "D. 46岁及以上", size: 24 },
|
|
|
{ text: "2. 您所在的城市属于?(单选)", size: 24 },
|
|
|
{ text: "A. 一线城市", size: 24 }, { text: "B. 新一线或二线城市", size: 24 }, { text: "C. 三线及以下城市", size: 24 },
|
|
|
{ text: "【购买行为】", bold: true, size: 24 },
|
|
|
{ text: "3. 您是否购买过本案例产品?(单选)", size: 24 },
|
|
|
{ text: "A. 购买过", size: 24 }, { text: "B. 未购买过", size: 24 },
|
|
|
{ text: "【需求偏好】", bold: true, size: 24 },
|
|
|
{ text: "4. 请对步骤二提到的功能进行需求度评价(量表)", size: 24 },
|
|
|
{ text: "【开放建议】", bold: true, size: 24 },
|
|
|
{ text: "5. 您对本案例产品的期望与顾虑?(开放题)", size: 24 },
|
|
|
]
|
|
|
downloadBlob(await buildDocxBlob(paragraphs), "消费者需求调研问卷-导入模板.docx")
|
|
|
proxy?.$modal?.msgSuccess("导入模板已下载,按格式编辑后可直接导入")
|
|
|
}
|
|
|
|
|
|
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 briefOpen = ref(true)
|
|
|
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(() => parseTrainingArray(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, "&")
|
|
|
.replace(/</g, "<")
|
|
|
.replace(/>/g, ">")
|
|
|
.replace(/"/g, """)
|
|
|
.replace(/'/g, "'")
|
|
|
}
|
|
|
|
|
|
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() {
|
|
|
router.push({ path: "/demand/people" })
|
|
|
}
|
|
|
|
|
|
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 || ""
|
|
|
const parsed = parseQuestionnaireContent(previewRes?.data?.content)
|
|
|
if (parsed.length) {
|
|
|
while (parsed.length < 4) parsed.push(createQuestionnaireQuestion())
|
|
|
form.value.questionnaireQuestions = parsed
|
|
|
}
|
|
|
form.value.questionnaireImport = {
|
|
|
name: file.name,
|
|
|
url,
|
|
|
previewContent: String(previewRes?.data?.content || ""),
|
|
|
}
|
|
|
if (parsed.length) {
|
|
|
showQuestionnaireComposePreview.value = true
|
|
|
proxy?.$modal?.msgSuccess(`已按规范模板识别 ${parsed.length} 道题目,可在线预览与下载`)
|
|
|
} else {
|
|
|
showQuestionnairePreview.value = true
|
|
|
proxy?.$modal?.msgSuccess("问卷导入成功(未识别到模板题目,已按普通 Word 问卷导入)")
|
|
|
}
|
|
|
} 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() {
|
|
|
const filled = form.value.surveyReportRows.filter((item) => String(item?.title || "").trim() || String(item?.content || "").trim())
|
|
|
return filled.length > 0 && filled.every((item) => String(item?.title || "").trim() && String(item?.content || "").trim())
|
|
|
}
|
|
|
|
|
|
function addSurveyReportRow() {
|
|
|
if (form.value.surveyReportRows.length >= 12) return
|
|
|
form.value.surveyReportRows.push({ title: "", content: "" })
|
|
|
}
|
|
|
|
|
|
function removeSurveyReportRow() {
|
|
|
if (form.value.surveyReportRows.length <= 1) return
|
|
|
form.value.surveyReportRows.pop()
|
|
|
}
|
|
|
|
|
|
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.filter((item) => String(item?.title || "").trim() || String(item?.content || "").trim()) })
|
|
|
}
|
|
|
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 downloadBuiltInFile(url, fileName) {
|
|
|
const link = document.createElement("a")
|
|
|
link.href = url
|
|
|
link.download = fileName
|
|
|
document.body.appendChild(link)
|
|
|
link.click()
|
|
|
document.body.removeChild(link)
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
/*
|
|
|
* 页面级令牌(教育蓝极简主题):纯白画布 + 教育蓝主操作 + 中性灰层次。
|
|
|
*/
|
|
|
.consumer-behavior-page {
|
|
|
/* 主色:教育蓝 */
|
|
|
--student-accent: #2563eb;
|
|
|
--student-accent-deep: #1d4ed8;
|
|
|
--student-accent-light: #3b82f6;
|
|
|
--student-accent-soft: #eff6ff;
|
|
|
/* 语义色 */
|
|
|
--student-danger: #ef4444;
|
|
|
--student-success: #2ba471;
|
|
|
/* 三层次标识色:深蓝 / 中蓝 / 浅蓝 */
|
|
|
--student-layer-1: #1d4ed8;
|
|
|
--student-layer-2: #3b82f6;
|
|
|
--student-layer-3: #93c5fd;
|
|
|
/* 结构色 */
|
|
|
--student-panel-border: #e5e7eb;
|
|
|
--student-card-bg: #ffffff;
|
|
|
--student-panel-bg: #fafbfc;
|
|
|
--student-text: #1f2937;
|
|
|
--student-text-secondary: #6b7280;
|
|
|
/* Element Plus 主色:教育蓝 */
|
|
|
--el-color-primary: #2563eb;
|
|
|
--el-color-primary-light-3: #60a5fa;
|
|
|
--el-color-primary-light-5: #93c5fd;
|
|
|
--el-color-primary-light-7: #bfdbfe;
|
|
|
--el-color-primary-light-8: #dbeafe;
|
|
|
--el-color-primary-light-9: #eff6ff;
|
|
|
--el-color-primary-dark-2: #1d4ed8;
|
|
|
--el-input-hover-border-color: #3b82f6;
|
|
|
--el-input-focus-border-color: #2563eb;
|
|
|
--el-input-focus-ring-color: rgba(59, 130, 246, 0.15);
|
|
|
--el-textarea-hover-border-color: #3b82f6;
|
|
|
--el-textarea-focus-border-color: #2563eb;
|
|
|
/* 字号梯度统一继承 .student-training-shell,页面不再重复声明 */
|
|
|
|
|
|
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: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.training-core {
|
|
|
min-width: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
backdrop-filter: none;
|
|
|
}
|
|
|
|
|
|
.task-header {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin: 0;
|
|
|
padding: 18px 20px 18px 24px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: var(--student-card-bg);
|
|
|
overflow: hidden;
|
|
|
|
|
|
&::before {
|
|
|
content: '';
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
bottom: 0;
|
|
|
left: 0;
|
|
|
width: 4px;
|
|
|
background: var(--student-accent);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-head-main {
|
|
|
display: grid;
|
|
|
justify-items: start;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-display);
|
|
|
line-height: 1.35;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
.task-brief-toggle {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover {
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-brief-more {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 8px;
|
|
|
|
|
|
b {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
|
|
|
ul {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
margin: 0;
|
|
|
padding-left: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-card {
|
|
|
margin-bottom: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
.workbench-actions {
|
|
|
display: flex;
|
|
|
flex: 0 0 auto;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 操作工具栏横条:内联布局,去掉悬浮和边框
|
|
|
* ============================================================ */
|
|
|
.task-action-strip {
|
|
|
margin: 0;
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
:deep(.training-task-toolbar) {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 800;
|
|
|
line-height: 1.4;
|
|
|
letter-spacing: 0.12em;
|
|
|
}
|
|
|
|
|
|
.outline-action {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 8px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease, box-shadow 0.16s ease;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
transform: translateY(-1px);
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.step-card {
|
|
|
margin-bottom: 0;
|
|
|
padding: 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 16px;
|
|
|
background: var(--student-card-bg);
|
|
|
}
|
|
|
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 18px;
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
h3 {
|
|
|
margin: 2px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h2);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.task-desc {
|
|
|
margin: 0 0 14px;
|
|
|
color: #374151;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.hypothesis-head {
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
|
align-items: center;
|
|
|
gap: 10px 18px;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.hypothesis-desc,
|
|
|
.kano-desc,
|
|
|
.questionnaire-design-desc {
|
|
|
margin: 0;
|
|
|
color: #374151;
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 600;
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.training-eight-action {
|
|
|
grid-column: 2;
|
|
|
grid-row: 1;
|
|
|
justify-self: end;
|
|
|
min-width: 124px;
|
|
|
height: 36px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
}
|
|
|
|
|
|
.hypothesis-actions,
|
|
|
.kano-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.hypothesis-actions {
|
|
|
grid-column: 1 / -1;
|
|
|
grid-row: 2;
|
|
|
}
|
|
|
|
|
|
:deep(.hypothesis-action),
|
|
|
:deep(.training-eight-action) {
|
|
|
min-width: 120px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
:deep(.hypothesis-action:hover:not(.is-disabled)),
|
|
|
:deep(.training-eight-action:hover:not(.is-disabled)) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
:deep(.hypothesis-action--quiet) {
|
|
|
border-color: var(--student-panel-border);
|
|
|
color: var(--student-text-secondary);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
:deep(.hypothesis-action--quiet:hover:not(.is-disabled)) {
|
|
|
border-color: var(--student-text-secondary);
|
|
|
background: #eff6ff;
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.survey-report-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: flex-end;
|
|
|
gap: 10px;
|
|
|
margin: 0 0 10px;
|
|
|
}
|
|
|
|
|
|
.target-hypothesis-table-wrap,
|
|
|
.kano-table-wrap,
|
|
|
.survey-report-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.target-hypothesis-table,
|
|
|
.kano-table,
|
|
|
.survey-report-table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
|
|
|
th,
|
|
|
td {
|
|
|
padding: 10px 12px;
|
|
|
border-right: 1px solid var(--student-panel-border);
|
|
|
border-bottom: 1px solid var(--student-panel-border);
|
|
|
vertical-align: top;
|
|
|
text-align: left;
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
th:last-child,
|
|
|
td:last-child {
|
|
|
border-right: 0;
|
|
|
}
|
|
|
|
|
|
tr:last-child td {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
th {
|
|
|
color: var(--student-text);
|
|
|
background: #eff6ff;
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 800;
|
|
|
text-align: left;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.target-hypothesis-table {
|
|
|
min-width: 920px;
|
|
|
|
|
|
th:nth-child(1) {
|
|
|
width: 28%;
|
|
|
}
|
|
|
|
|
|
th:nth-child(2) {
|
|
|
width: 28%;
|
|
|
}
|
|
|
|
|
|
th:nth-child(3) {
|
|
|
width: 44%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.kano-table {
|
|
|
min-width: 900px;
|
|
|
|
|
|
th:nth-child(1) {
|
|
|
width: 9%;
|
|
|
}
|
|
|
|
|
|
th:nth-child(2) {
|
|
|
width: 42%;
|
|
|
}
|
|
|
|
|
|
th:nth-child(3) {
|
|
|
width: 25%;
|
|
|
}
|
|
|
|
|
|
th:nth-child(4) {
|
|
|
width: 24%;
|
|
|
}
|
|
|
|
|
|
th {
|
|
|
position: sticky;
|
|
|
top: 0;
|
|
|
z-index: 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.kano-index {
|
|
|
color: var(--student-accent-light);
|
|
|
font-weight: 700;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.survey-report-table {
|
|
|
min-width: 760px;
|
|
|
|
|
|
th:first-child,
|
|
|
td:first-child {
|
|
|
width: 22%;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.hypothesis-input,
|
|
|
.hypothesis-textarea {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.hypothesis-input :deep(.el-input__wrapper) {
|
|
|
min-height: 36px;
|
|
|
border: 1px solid #d6dae0;
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.hypothesis-input :deep(.el-input__wrapper.is-focus) {
|
|
|
border-color: var(--student-accent-light);
|
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
|
|
|
.hypothesis-input :deep(.el-input__inner),
|
|
|
.hypothesis-textarea :deep(.el-textarea__inner) {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
}
|
|
|
|
|
|
.hypothesis-textarea :deep(.el-textarea__inner) {
|
|
|
min-height: 72px !important;
|
|
|
border: 1px solid #d6dae0;
|
|
|
border-radius: 10px;
|
|
|
padding: 8px 12px;
|
|
|
background: #ffffff;
|
|
|
box-sizing: border-box;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
resize: vertical;
|
|
|
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
|
|
|
|
|
&::placeholder {
|
|
|
color: #9ca3af;
|
|
|
}
|
|
|
|
|
|
&:focus {
|
|
|
border-color: var(--student-accent-light);
|
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.kano-textarea :deep(.el-textarea__inner) {
|
|
|
min-height: 44px !important;
|
|
|
}
|
|
|
|
|
|
.kano-select {
|
|
|
width: min(100%, 190px);
|
|
|
}
|
|
|
|
|
|
.kano-select :deep(.el-select__wrapper) {
|
|
|
min-height: 36px;
|
|
|
border: 1px solid #d6dae0;
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.kano-select :deep(.el-select__placeholder) {
|
|
|
color: #9ca3af;
|
|
|
}
|
|
|
|
|
|
.kano-select-cell {
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.hypothesis-tip {
|
|
|
margin: 10px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.kano-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px 18px;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.questionnaire-design-head {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px 18px;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.questionnaire-import-action,
|
|
|
.questionnaire-editor-launcher__action,
|
|
|
.questionnaire-template-action,
|
|
|
|
|
|
.questionnaire-imported-file {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
max-width: 100%;
|
|
|
min-height: 36px;
|
|
|
padding: 0 12px;
|
|
|
margin-bottom: 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
color: #374151;
|
|
|
background: #fafbfc;
|
|
|
|
|
|
span {
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
button {
|
|
|
flex: 0 0 auto;
|
|
|
border: 0;
|
|
|
color: var(--student-accent);
|
|
|
background: transparent;
|
|
|
cursor: pointer;
|
|
|
font-weight: 600;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
|
|
|
&:hover {
|
|
|
text-decoration: underline;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-launcher {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 24px;
|
|
|
min-height: auto;
|
|
|
padding: 16px 20px;
|
|
|
margin-bottom: 10px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #fafbfc;
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-launcher__copy {
|
|
|
display: grid;
|
|
|
gap: 4px;
|
|
|
|
|
|
h4 {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-launcher__actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
flex: 0 0 auto;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: flex-end;
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-launcher__action {
|
|
|
flex: 0 0 auto;
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-summary {
|
|
|
margin: 0 0 12px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
|
|
|
.questionnaire-template-guide {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 18px;
|
|
|
margin-top: 10px;
|
|
|
padding: 12px 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
color: #374151;
|
|
|
background: #fafbfc;
|
|
|
|
|
|
h5,
|
|
|
p {
|
|
|
margin: 0 0 4px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
h5 {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
:deep(.questionnaire-editor-dialog .el-dialog),
|
|
|
:deep(.questionnaire-preview-dialog .el-dialog) {
|
|
|
border-radius: 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* ===== 结构化问卷预览弹窗 ===== */
|
|
|
.questionnaire-compose-preview {
|
|
|
max-height: 60vh;
|
|
|
padding: 18px 26px;
|
|
|
overflow-y: auto;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
|
|
|
&__title {
|
|
|
margin: 0 0 16px;
|
|
|
color: var(--student-text);
|
|
|
font-size: 20px;
|
|
|
font-weight: 800;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
&__section {
|
|
|
margin: 0 0 14px;
|
|
|
|
|
|
h4 {
|
|
|
margin: 0 0 8px;
|
|
|
padding-left: 8px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
ol {
|
|
|
margin: 0;
|
|
|
padding-left: 22px;
|
|
|
}
|
|
|
|
|
|
li {
|
|
|
margin: 8px 0;
|
|
|
}
|
|
|
|
|
|
.question-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.question-type {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 400;
|
|
|
}
|
|
|
|
|
|
.question-options {
|
|
|
margin: 6px 0 0;
|
|
|
padding: 0;
|
|
|
list-style: none;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
|
|
|
li {
|
|
|
margin: 4px 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.question-open-line {
|
|
|
margin: 6px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
letter-spacing: 2px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&__empty {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
text-align: center;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
:deep(.questionnaire-preview-dialog .el-dialog) {
|
|
|
border-radius: 12px;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
:deep(.questionnaire-editor-dialog .el-dialog__title),
|
|
|
:deep(.questionnaire-preview-dialog .el-dialog__title) {
|
|
|
color: var(--student-text);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-dialog__intro {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 16px;
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.questionnaire-editor-add {
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.questionnaire-question-list {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
max-height: min(58vh, 700px);
|
|
|
padding-right: 5px;
|
|
|
overflow: auto;
|
|
|
}
|
|
|
|
|
|
.questionnaire-question-card {
|
|
|
display: grid;
|
|
|
grid-template-columns: 36px minmax(0, 1fr) auto;
|
|
|
gap: 10px;
|
|
|
align-items: start;
|
|
|
padding: 12px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 12px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.questionnaire-question-card__number {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
width: 28px;
|
|
|
height: 28px;
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text-secondary);
|
|
|
background: var(--student-accent-soft);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.questionnaire-question-card__fields {
|
|
|
display: grid;
|
|
|
grid-template-columns: 160px 160px minmax(0, 1fr);
|
|
|
gap: 8px;
|
|
|
|
|
|
:deep(.el-select__wrapper),
|
|
|
:deep(.el-input__wrapper) {
|
|
|
min-height: 34px;
|
|
|
border: 1px solid #d6dae0;
|
|
|
box-shadow: none;
|
|
|
background: #ffffff;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
:deep(.el-input__inner) {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.likert-tip {
|
|
|
grid-column: 1 / -1;
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
|
|
|
.questionnaire-question-card__remove {
|
|
|
padding: 6px 10px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-text-secondary);
|
|
|
background: #ffffff;
|
|
|
cursor: pointer;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 600;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
color: var(--student-accent);
|
|
|
}
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.45;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.questionnaire-word-preview {
|
|
|
max-height: min(62vh, 760px);
|
|
|
padding: 16px;
|
|
|
overflow: auto;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
white-space: pre-wrap;
|
|
|
line-height: 1.75;
|
|
|
}
|
|
|
|
|
|
.questionnaire-preview-empty {
|
|
|
color: var(--student-text-secondary);
|
|
|
}
|
|
|
|
|
|
.survey-report-desc {
|
|
|
max-width: 1050px;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.survey-report-title-input,
|
|
|
.survey-report-content-input {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.survey-report-title-input :deep(.el-input__wrapper) {
|
|
|
min-height: 36px;
|
|
|
border: 1px solid #d6dae0;
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.survey-report-title-input :deep(.el-input__inner) {
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.survey-report-content-input :deep(.el-textarea__inner) {
|
|
|
min-height: 72px !important;
|
|
|
border: 1px solid #d6dae0;
|
|
|
border-radius: 10px;
|
|
|
padding: 8px 12px;
|
|
|
background: #ffffff;
|
|
|
box-sizing: border-box;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
resize: vertical;
|
|
|
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
|
|
|
|
|
&::placeholder {
|
|
|
color: #9ca3af;
|
|
|
}
|
|
|
|
|
|
&:focus {
|
|
|
border-color: var(--student-accent-light);
|
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
.file-input {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
/* ============ 底部步骤导航 ============ */
|
|
|
.step-navigation {
|
|
|
position: relative;
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
padding: 12px 16px;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
flex-wrap: nowrap;
|
|
|
}
|
|
|
|
|
|
.step-nav-right {
|
|
|
margin-left: auto;
|
|
|
}
|
|
|
|
|
|
.btn-primary {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid var(--student-accent);
|
|
|
border-radius: 8px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent-deep);
|
|
|
background: var(--student-accent-deep);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
|
|
|
}
|
|
|
|
|
|
&:active:not(:disabled) {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-ghost {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35);
|
|
|
border-radius: 8px;
|
|
|
color: var(--student-accent);
|
|
|
background: #ffffff;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: var(--student-accent);
|
|
|
background: var(--student-accent-soft);
|
|
|
transform: translateY(-1px);
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-save {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 6px;
|
|
|
min-height: 36px;
|
|
|
height: 36px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid #e5e7eb;
|
|
|
border-radius: 8px;
|
|
|
color: #4b5563;
|
|
|
background: #f9fafb;
|
|
|
font-family: inherit;
|
|
|
font-size: var(--student-fs-button);
|
|
|
font-weight: 600;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease;
|
|
|
white-space: nowrap;
|
|
|
|
|
|
.el-icon {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #d1d5db;
|
|
|
background: #f3f4f6;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.btn-primary:disabled,
|
|
|
.btn-ghost:disabled,
|
|
|
.btn-save:disabled,
|
|
|
.btn-nav:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.5;
|
|
|
transform: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.step-progress {
|
|
|
color: var(--student-text-secondary);
|
|
|
font-family: var(--student-font-data);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.05em;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.training-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 14px;
|
|
|
margin-top: 14px;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
.consumer-behavior-page {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.consumer-behavior-page {
|
|
|
padding: 14px 14px 18px;
|
|
|
}
|
|
|
|
|
|
.workbench-actions,
|
|
|
.step-title-row,
|
|
|
.step-navigation,
|
|
|
.training-actions {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
|
|
|
.step-nav-left,
|
|
|
.step-nav-right,
|
|
|
.btn-nav {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.hypothesis-head {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.training-eight-action {
|
|
|
grid-column: auto;
|
|
|
grid-row: auto;
|
|
|
justify-self: stretch;
|
|
|
}
|
|
|
|
|
|
.hypothesis-actions,
|
|
|
.kano-actions {
|
|
|
justify-content: stretch;
|
|
|
}
|
|
|
|
|
|
.hypothesis-actions :deep(.hypothesis-action),
|
|
|
.kano-actions :deep(.hypothesis-action) {
|
|
|
flex: 1;
|
|
|
}
|
|
|
|
|
|
.kano-head,
|
|
|
.questionnaire-design-head,
|
|
|
.questionnaire-editor-launcher,
|
|
|
.questionnaire-template-guide {
|
|
|
align-items: stretch;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
|
|
|
.questionnaire-editor-dialog__intro {
|
|
|
flex-direction: column;
|
|
|
align-items: stretch;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 760px) {
|
|
|
.consumer-behavior-page {
|
|
|
padding: 10px 10px 14px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* === 步骤导航:分段连接胶囊(SKILL.md 规格) === */
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .step-indicator {
|
|
|
display: flex !important;
|
|
|
flex-wrap: nowrap !important;
|
|
|
align-items: stretch !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 0 !important;
|
|
|
width: 100% !important;
|
|
|
margin: 0 0 20px !important;
|
|
|
padding: 0 !important;
|
|
|
border: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
.step-tab {
|
|
|
|
|
|
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
min-width: 0 !important;
|
|
|
height: auto !important;
|
|
|
min-height: 40px !important;
|
|
|
margin: 0 !important;
|
|
|
padding: 8px 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-right: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
white-space: nowrap !important;
|
|
|
text-align: center !important;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&:first-child {
|
|
|
border-radius: 8px 0 0 8px !important;
|
|
|
}
|
|
|
|
|
|
&:last-child {
|
|
|
border-radius: 0 8px 8px 0 !important;
|
|
|
border-right: 1px solid #e5e7eb !important;
|
|
|
}
|
|
|
|
|
|
.step-bar {
|
|
|
display: none !important;
|
|
|
}
|
|
|
|
|
|
.step-no {
|
|
|
position: static !important;
|
|
|
flex: 0 0 auto !important;
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
min-width: 22px !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: var(--student-fs-body-sm) !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&::after {
|
|
|
display: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.step-name {
|
|
|
color: inherit !important;
|
|
|
}
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: rgba(43, 164, 113, 0.12) !important;
|
|
|
color: #16915f !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #2ba471 !important;
|
|
|
color: #ffffff !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
z-index: 2 !important;
|
|
|
|
|
|
.step-no {
|
|
|
background: #ffffff !important;
|
|
|
color: #2563eb !important;
|
|
|
}
|
|
|
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 窄屏:横向滑动 */
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .step-indicator {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .step-indicator .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* === 全页单选教育蓝 === */
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-radio__input.is-checked .el-radio__inner {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-radio__input.is-checked .el-radio__inner::after {
|
|
|
background: #ffffff !important;
|
|
|
}
|
|
|
|
|
|
/* === training-action === */
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions .el-button.training-action,
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions button.training-action {
|
|
|
min-width: 140px !important;
|
|
|
min-height: 34px !important;
|
|
|
height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: var(--student-accent) !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
letter-spacing: 0 !important;
|
|
|
text-shadow: none !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions .el-button.training-action--submit,
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions button.training-action--submit {
|
|
|
border-color: var(--student-accent) !important;
|
|
|
color: #ffffff !important;
|
|
|
background: var(--student-accent) !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions .el-button.training-action:hover:not(.is-disabled),
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions button.training-action:hover:not(.is-disabled) {
|
|
|
transform: translateY(-1px) !important;
|
|
|
border-color: var(--student-accent) !important;
|
|
|
background: var(--student-accent-soft) !important;
|
|
|
color: var(--student-accent) !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions .el-button.training-action--submit:hover:not(.is-disabled),
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .training-actions button.training-action--submit:hover:not(.is-disabled) {
|
|
|
border-color: var(--student-accent-deep) !important;
|
|
|
background: var(--student-accent-deep) !important;
|
|
|
color: #ffffff !important;
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
|
|
|
}
|
|
|
|
|
|
/* === nav-action (step-navigation):原生 button,样式见 scoped 块 === */
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部工具栏:TrainingMaterialButton 资料按钮 → 教育蓝
|
|
|
* ============================================================ */
|
|
|
.consumer-behavior-page .workbench-actions .material-button.el-button {
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
&.is-plain {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 顶部导出按钮(原生 <button.outline-action>)
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .workbench-actions>button.outline-action {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 6px !important;
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
padding: 0 14px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: var(--student-fs-body) !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .workbench-actions>button.outline-action:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
transform: none !important;
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 内容区次级 el-button:假设行操作 / 查看实训8
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-button.hypothesis-action,
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-button.training-eight-action {
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
&:hover:not(.is-disabled) {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-button.hypothesis-action--quiet {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #6b7280 !important;
|
|
|
background: #ffffff !important;
|
|
|
|
|
|
&:hover:not(.is-disabled) {
|
|
|
border-color: rgba(37, 99, 235, 0.35) !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 内容区次级 el-button:问卷导入 / 编辑器入口 / 模板下载
|
|
|
* ============================================================ */
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-button.questionnaire-import-action,
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-button.questionnaire-editor-launcher__action,
|
|
|
#app .studentLayout section.app-main:has(.consumer-behavior-page) .el-button.questionnaire-template-action {
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
&:hover:not(.is-disabled) {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* teleport 弹窗(append-to-body)内「新增题目」按钮:
|
|
|
* 以 body.student-light-mode 默认按钮选择器 +1 类特异性覆盖
|
|
|
* ============================================================ */
|
|
|
body.student-light-mode .el-button:not(.is-text):not(.is-link):not(.el-button--primary):not(.el-button--success):not(.el-button--danger):not(.el-button--warning):not(.task-toolbar-button):not(.material-button).questionnaire-editor-add {
|
|
|
min-height: 36px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 10px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
}
|
|
|
}
|
|
|
</style>
|