|
|
<template>
|
|
|
<div class="consumer-scenario-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="trainingSteps.length" class="step-indicator" aria-label="消费场景解构步骤">
|
|
|
<template v-for="(step, index) in trainingSteps" :key="`${index}-${step}`">
|
|
|
<button
|
|
|
type="button"
|
|
|
:class="['step-tab', { active: form.activeStep === index + 1, complete: form.activeStep > index + 1 }]"
|
|
|
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
|
|
|
:aria-label="`第${index + 1}步:${step}`"
|
|
|
@click="setCurrentStep(index + 1)"
|
|
|
>
|
|
|
<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 + 1 ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
|
|
|
</span>
|
|
|
</button>
|
|
|
<span v-if="index < trainingSteps.length - 1" class="step-connector" aria-hidden="true"></span>
|
|
|
</template>
|
|
|
</nav>
|
|
|
|
|
|
<section class="step-card consumer-scenario-step-card">
|
|
|
<div v-if="activeStepName" class="step-title-row">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, "0") }}</p>
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<section v-show="isSectionVisible(1)" class="scenario-step-section">
|
|
|
<div class="journey-sort-head">
|
|
|
<div>
|
|
|
<p class="journey-sort-kicker">Customer Journey</p>
|
|
|
<h4>将购买旅程卡片按正确顺序排列</h4>
|
|
|
<p>从需求触发到购买后的使用反馈,共五个阶段。拖动卡片,或使用卡片右侧的上下移动按钮调整顺序。</p>
|
|
|
</div>
|
|
|
<button type="button" class="journey-check-action" :disabled="checkingJourney" @click="checkJourneyOrder">
|
|
|
{{ checkingJourney ? "正在校验…" : "检查排序" }}
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="journey-sort-list" role="list" aria-label="消费者购买旅程排序">
|
|
|
<template v-for="(stage, index) in orderedJourneyStages" :key="stage.id">
|
|
|
<article
|
|
|
class="journey-sort-card"
|
|
|
:class="{ dragging: draggedStageId === stage.id }"
|
|
|
draggable="true"
|
|
|
role="listitem"
|
|
|
@dragstart="startJourneyDrag(stage.id)"
|
|
|
@dragover.prevent
|
|
|
@drop="dropJourneyStage(stage.id)"
|
|
|
@dragend="endJourneyDrag"
|
|
|
>
|
|
|
<span class="journey-card-order">{{ String(index + 1).padStart(2, "0") }}</span>
|
|
|
<div class="journey-card-copy">
|
|
|
<strong>{{ stage.title }}</strong>
|
|
|
<span>{{ stage.description }}</span>
|
|
|
</div>
|
|
|
<div class="journey-card-actions" aria-label="调整卡片顺序">
|
|
|
<button type="button" :disabled="index === 0" :aria-label="`将${stage.title}上移`" @click="moveJourneyStage(index, -1)">↑</button>
|
|
|
<button type="button" :disabled="index === orderedJourneyStages.length - 1" :aria-label="`将${stage.title}下移`" @click="moveJourneyStage(index, 1)">↓</button>
|
|
|
</div>
|
|
|
</article>
|
|
|
<span v-if="index < orderedJourneyStages.length - 1" class="journey-sort-arrow" aria-hidden="true">→</span>
|
|
|
</template>
|
|
|
</div>
|
|
|
|
|
|
<p v-if="journeyCheckMessage" :class="['journey-check-message', { success: journeyCheckedCorrectly }]" role="status">
|
|
|
{{ journeyCheckMessage }}
|
|
|
</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="isSectionVisible(2)" class="scenario-step-section">
|
|
|
<div class="journey-analysis-intro">
|
|
|
<p class="journey-sort-kicker">Journey evidence map</p>
|
|
|
<h4>分析购买旅程中的行为与体验</h4>
|
|
|
<p>请根据访谈记录,分析用户在购买旅程各阶段的行为表现,识别关键触点,并评估用户的情感体验水平。</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="journey-analysis-wrap">
|
|
|
<table class="journey-analysis-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>序号</th>
|
|
|
<th>旅程阶段</th>
|
|
|
<th>用户行为描述</th>
|
|
|
<th>关键触点</th>
|
|
|
<th>情感体验</th>
|
|
|
<th>关键决策因素</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in form.journeyAnalysis" :key="row.stageId">
|
|
|
<td class="journey-analysis-index">{{ index + 1 }}</td>
|
|
|
<th class="journey-analysis-stage">{{ stageTitle(row.stageId) }}</th>
|
|
|
<td>
|
|
|
<textarea v-model="row.behaviorDescription" placeholder="描述用户在此阶段的行为表现"></textarea>
|
|
|
</td>
|
|
|
<td>
|
|
|
<textarea v-model="row.keyTouchpoints" placeholder="填写平台、内容、服务或产品触点"></textarea>
|
|
|
</td>
|
|
|
<td>
|
|
|
<select v-model="row.emotionExperience" aria-label="选择情感体验">
|
|
|
<option value="" disabled>请选择</option>
|
|
|
<option v-for="emotion in emotionOptions" :key="emotion" :value="emotion">{{ emotion }}</option>
|
|
|
</select>
|
|
|
</td>
|
|
|
<td>
|
|
|
<textarea v-model="row.decisionFactors" placeholder="填写推动或阻碍决策的关键因素"></textarea>
|
|
|
</td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p v-if="journeyAnalysisMessage" class="journey-check-message" role="status">{{ journeyAnalysisMessage }}</p>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="isSectionVisible(3)" class="scenario-step-section">
|
|
|
<div class="journey-map-intro">
|
|
|
<span>Step 03</span>
|
|
|
<h4>绘制用户购买旅程图</h4>
|
|
|
<p>基于步骤二的分析结果,绘制完整的用户购买旅程图,呈现旅程阶段、情感体验曲线、关键触点、痛点和机会点。</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="journey-map-upload-panel">
|
|
|
<input
|
|
|
ref="journeyMapInput"
|
|
|
class="hidden-file-input"
|
|
|
type="file"
|
|
|
accept="image/png,image/jpeg,image/webp"
|
|
|
@change="handleJourneyMapUpload"
|
|
|
/>
|
|
|
<template v-if="form.journeyMap.url">
|
|
|
<el-image
|
|
|
class="journey-map-preview"
|
|
|
:src="form.journeyMap.url"
|
|
|
:preview-src-list="[form.journeyMap.url]"
|
|
|
preview-teleported
|
|
|
fit="contain"
|
|
|
/>
|
|
|
<div class="journey-map-file-info">
|
|
|
<el-icon><UploadFilled /></el-icon>
|
|
|
<span>{{ form.journeyMap.name || "用户购买旅程图" }}</span>
|
|
|
</div>
|
|
|
<div class="journey-map-actions">
|
|
|
<el-button type="primary" @click="openJourneyMapPicker">更换用户购买旅程图</el-button>
|
|
|
<el-button @click="clearJourneyMap">移除图片</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
<template v-else>
|
|
|
<el-icon class="journey-map-upload-icon"><UploadFilled /></el-icon>
|
|
|
<strong>上传用户购买旅程图</strong>
|
|
|
<p>支持 PNG、JPG、JPEG、WEBP 格式,单个文件不超过 5MB</p>
|
|
|
<el-button type="primary" :loading="uploadingJourneyMap" @click="openJourneyMapPicker">上传用户购买旅程图</el-button>
|
|
|
</template>
|
|
|
</div>
|
|
|
|
|
|
<div class="journey-map-helper">
|
|
|
<div>
|
|
|
<h5>推荐工具及方法如下,自行选择:</h5>
|
|
|
<p>1. 下载模板,填写各阶段数据,完成用户购买旅程图。</p>
|
|
|
<p>2. 下载步骤二用户购买旅程表,使用通用 AI 大模型辅助生成。</p>
|
|
|
<p class="journey-map-prompt">参考提示词:请根据用户购买旅程表文件,生成一个美观的用户购买旅程图。</p>
|
|
|
</div>
|
|
|
<el-button plain @click="downloadJourneyMapTemplate"><el-icon><Download /></el-icon>下载用户购买旅程模板</el-button>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section v-show="isSectionVisible(4)" class="scenario-step-section">
|
|
|
<div class="journey-optimization-intro">
|
|
|
<span>Step 04</span>
|
|
|
<h4>识别旅程痛点,形成优化建议</h4>
|
|
|
<p>结合用户购买旅程图,分析各阶段存在的痛点及未满足的需求,提出可落地的产品优化建议。</p>
|
|
|
</div>
|
|
|
<div class="journey-optimization-wrap">
|
|
|
<table class="journey-optimization-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>旅程阶段</th>
|
|
|
<th>识别的痛点</th>
|
|
|
<th>优化建议</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in form.journeyOptimizations" :key="row.stageId">
|
|
|
<th>{{ stageShortTitle(row.stageId) }}</th>
|
|
|
<td><textarea v-model="row.painPoint" placeholder="填写该阶段用户遇到的痛点或未满足需求"></textarea></td>
|
|
|
<td><textarea v-model="row.optimizationSuggestion" placeholder="填写针对痛点的产品、服务或体验优化建议"></textarea></td>
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p v-if="journeyOptimizationMessage" class="journey-check-message" role="status">{{ journeyOptimizationMessage }}</p>
|
|
|
</section>
|
|
|
|
|
|
<div v-if="hasConfiguredSteps" class="step-actions">
|
|
|
<button type="button" class="nav-action nav-action--previous" :disabled="form.activeStep === 1" @click="goPreviousStep">
|
|
|
<el-icon><ArrowLeft /></el-icon>
|
|
|
上一步
|
|
|
</button>
|
|
|
<button type="button" class="nav-action nav-action--save" :disabled="saving" @click="saveCurrentProgress">
|
|
|
<el-icon><CircleCheck /></el-icon>
|
|
|
保存当前进度
|
|
|
</button>
|
|
|
<button type="button" class="nav-action nav-action--next" :disabled="form.activeStep >= trainingSteps.length" @click="goNextStep">
|
|
|
下一步
|
|
|
<el-icon><ArrowRight /></el-icon>
|
|
|
</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="可让 AI 帮你把一段用户故事拆成旅程阶段,再检查每个阶段是否有明确触点和痛点。"
|
|
|
:progress-items="progressItems"
|
|
|
/>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, UploadFilled } 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 { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsumerScenarioStepThree, checkConsumerScenarioStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
import { isStudentDemo } from "@/utils/studentDemo";
|
|
|
|
|
|
const TASK_KEY = "consumer-scenario";
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
|
const defaultRequirement =
|
|
|
"消费场景解构用于把抽象需求落到具体时间、地点、人物、任务和情绪中,找到用户真正发生问题的瞬间,从而发现产品机会和功能设计切入点。";
|
|
|
const defaultBackground =
|
|
|
"很多智能产品失败并不是功能少,而是没有进入用户真实生活场景。通过拆解场景,可以识别触发因素、使用阻碍、情绪变化和可被产品改善的关键节点。";
|
|
|
const defaultGoals = [
|
|
|
"能够描述目标用户的典型消费场景。",
|
|
|
"识别场景中的触发点、痛点和阻碍因素。",
|
|
|
"将场景洞察转化为产品机会。",
|
|
|
];
|
|
|
|
|
|
const journeyStages = [
|
|
|
{
|
|
|
id: "problem-awareness",
|
|
|
title: "问题感知(需求触发)",
|
|
|
description: "用户意识到自身存在某个问题或需求",
|
|
|
},
|
|
|
{
|
|
|
id: "information-search",
|
|
|
title: "信息搜索(产品探索)",
|
|
|
description: "用户主动搜索相关信息,了解可选方案",
|
|
|
},
|
|
|
{
|
|
|
id: "evaluation-decision",
|
|
|
title: "评估决策(对比选择)",
|
|
|
description: "用户对比不同产品的功能、价格、口碑",
|
|
|
},
|
|
|
{
|
|
|
id: "purchase-conversion",
|
|
|
title: "购买转化(下单支付)",
|
|
|
description: "用户完成购买决策并下单",
|
|
|
},
|
|
|
{
|
|
|
id: "post-purchase",
|
|
|
title: "购后行为(使用反馈)",
|
|
|
description: "用户使用产品后形成体验评价,决定是否推荐或复购",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
const emotionOptions = ["负面", "中性", "正面"];
|
|
|
|
|
|
const phases = [
|
|
|
{ key: "problem", label: "问题感知(需求触发)" },
|
|
|
{ key: "search", label: "信息搜索(产品探索)" },
|
|
|
{ key: "evaluate", label: "评估决策(对比选择)" },
|
|
|
{ key: "purchase", label: "购买转化(下单支付)" },
|
|
|
{ key: "post", label: "购后行为(使用反馈)" },
|
|
|
];
|
|
|
|
|
|
const scenarioStatements = [
|
|
|
{
|
|
|
key: "needTrigger",
|
|
|
text: "用户刷小红书看到寝室智能做出好多美味的营养饮品和养生美汤,也想要自己尝试来做,这种行为属于用户购买旅程的",
|
|
|
defaultPhase: "problem",
|
|
|
},
|
|
|
{
|
|
|
key: "productSearch",
|
|
|
text: "用户在小红书、抖音、京东、淘宝等平台关注破壁机核心参数(转速、分贝值)、性价比、使用场景、演示等内容,属于用户购买旅程的",
|
|
|
defaultPhase: "search",
|
|
|
},
|
|
|
{
|
|
|
key: "compareDecision",
|
|
|
text: "用户选定了3款不同品牌破壁机,针对自动清洁、预约等实用功能以及价格、售后服务进行全方位比对,评估性价比,属于用户购买旅程的",
|
|
|
defaultPhase: "evaluate",
|
|
|
},
|
|
|
{
|
|
|
key: "orderPayment",
|
|
|
text: "用户心仪的破壁机在810期间大促,而且支持先用后付,用户最终以原价八折的八百多下单购买,属于用户购买旅程的",
|
|
|
defaultPhase: "purchase",
|
|
|
},
|
|
|
{
|
|
|
key: "usageReview",
|
|
|
text: "用户在使用该破壁机后给出好评,并在小红书上晒单分享使用过程,属于用户购买旅程的",
|
|
|
defaultPhase: "post",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
const journeyRows = [
|
|
|
{ key: "goal", label: "用户目标", placeholder: "填写该阶段用户想达成什么" },
|
|
|
{ key: "behavior", label: "用户行为", placeholder: "填写用户具体动作" },
|
|
|
{ key: "touchpoint", label: "接触点", placeholder: "填写平台、页面、人员或设备触点" },
|
|
|
{ key: "emotion", label: "情绪曲线", placeholder: "填写情绪变化和原因" },
|
|
|
{ key: "pain", label: "痛点/机会点", placeholder: "填写阻碍、痛点和产品机会" },
|
|
|
];
|
|
|
|
|
|
const referencePhases = [
|
|
|
{
|
|
|
name: "了解产品",
|
|
|
goals: ["有哪些特色功能?", "有哪些利益点?"],
|
|
|
touchpoint: "小红书、抖音、豆瓣、官网、百度、应用市场",
|
|
|
pain: "不知道从哪里获取可靠信息",
|
|
|
},
|
|
|
{
|
|
|
name: "找房",
|
|
|
goals: ["性价比高、干净、氛围好", "交通便利", "距离景点近"],
|
|
|
touchpoint: "首页、搜索、推荐页、详情页、聊天页",
|
|
|
pain: "房型好多,看不完;有过踩雷经历",
|
|
|
},
|
|
|
{
|
|
|
name: "订房",
|
|
|
goals: ["下单方便快捷", "可以正常通订"],
|
|
|
touchpoint: "收藏、付款页",
|
|
|
pain: "详情页广告多,且介绍文本过长",
|
|
|
},
|
|
|
{
|
|
|
name: "订房后/住前",
|
|
|
goals: ["办理入住简单", "与房东联系方便"],
|
|
|
touchpoint: "详情页、线下",
|
|
|
pain: "客服回复慢或答非所问",
|
|
|
},
|
|
|
{
|
|
|
name: "住中",
|
|
|
goals: ["房间干净、安全、噪声低", "设施完善,24h热水"],
|
|
|
touchpoint: "详情页、线下、客服电话",
|
|
|
pain: "卫生不过关,联系流程复杂",
|
|
|
},
|
|
|
{
|
|
|
name: "住后",
|
|
|
goals: ["顺利完成退房", "返还押金"],
|
|
|
touchpoint: "我的订单、评价",
|
|
|
pain: "没有想找用户、做意见问卷",
|
|
|
},
|
|
|
];
|
|
|
|
|
|
const behaviorNodes = ["了解同类产品", "浏览房源信息", "查看房间详情", "与客服沟通咨询", "下单付款", "联系酒店,确认入住时间", "办理入住", "游玩", "退房", "发布入住评价"];
|
|
|
const emotionPoints = [
|
|
|
{ icon: "🙂", label: "期待", left: "4%", top: "58%" },
|
|
|
{ icon: "😍", label: "满意", left: "17%", top: "30%" },
|
|
|
{ icon: "🙂", label: "犹豫", left: "30%", top: "60%" },
|
|
|
{ icon: "🙁", label: "担心", left: "43%", top: "74%" },
|
|
|
{ icon: "😍", label: "高兴", left: "56%", top: "34%" },
|
|
|
{ icon: "🙁", label: "焦虑", left: "70%", top: "74%" },
|
|
|
{ icon: "😍", label: "满意", left: "82%", top: "52%" },
|
|
|
{ icon: "🙂", label: "平稳", left: "93%", top: "44%" },
|
|
|
];
|
|
|
|
|
|
const taskConfig = ref(null);
|
|
|
const saving = ref(false);
|
|
|
const checkingJourney = ref(false);
|
|
|
const checkingJourneyAnalysis = ref(false);
|
|
|
const checkingJourneyOptimization = ref(false);
|
|
|
const journeyMapInput = ref(null);
|
|
|
const uploadingJourneyMap = ref(false);
|
|
|
const draggedStageId = ref("");
|
|
|
const journeyCheckMessage = ref("");
|
|
|
const journeyCheckedCorrectly = ref(false);
|
|
|
const journeyAnalysisMessage = ref("");
|
|
|
const journeyOptimizationMessage = ref("");
|
|
|
|
|
|
function createMatrix() {
|
|
|
return journeyRows.reduce((matrix, row) => {
|
|
|
matrix[row.key] = phases.reduce((phaseMap, phase) => {
|
|
|
phaseMap[phase.key] = "";
|
|
|
return phaseMap;
|
|
|
}, {});
|
|
|
return matrix;
|
|
|
}, {});
|
|
|
}
|
|
|
|
|
|
function createClassifications() {
|
|
|
return scenarioStatements.reduce((map, item) => {
|
|
|
map[item.key] = item.defaultPhase;
|
|
|
return map;
|
|
|
}, {});
|
|
|
}
|
|
|
|
|
|
function createJourneyAnalysis() {
|
|
|
return journeyStages.map((stage) => ({
|
|
|
stageId: stage.id,
|
|
|
behaviorDescription: "",
|
|
|
keyTouchpoints: "",
|
|
|
emotionExperience: "",
|
|
|
decisionFactors: "",
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
function createJourneyOptimizations() {
|
|
|
return journeyStages.map((stage) => ({
|
|
|
stageId: stage.id,
|
|
|
painPoint: "",
|
|
|
optimizationSuggestion: "",
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
function createForm() {
|
|
|
return {
|
|
|
activeStep: 1,
|
|
|
journeyOrder: createShuffledJourneyOrder(),
|
|
|
journeyAnalysis: createJourneyAnalysis(),
|
|
|
journeyMap: { url: "", name: "" },
|
|
|
journeyOptimizations: createJourneyOptimizations(),
|
|
|
classifications: createClassifications(),
|
|
|
matrix: createMatrix(),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
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 trainingSteps = computed(() => parseArray(taskConfig.value?.steps, []));
|
|
|
const hasConfiguredSteps = computed(() => trainingSteps.value.length > 0);
|
|
|
const activeStepName = computed(() => trainingSteps.value[form.value.activeStep - 1] || "");
|
|
|
const orderedJourneyStages = computed(() =>
|
|
|
form.value.journeyOrder
|
|
|
.map((stageId) => journeyStages.find((stage) => stage.id === stageId))
|
|
|
.filter(Boolean)
|
|
|
);
|
|
|
|
|
|
const progressItems = computed(() => [
|
|
|
{ text: journeyCheckedCorrectly.value ? "已完成:购买旅程排序" : "进行中:购买旅程排序", status: journeyCheckedCorrectly.value ? "done" : "doing" },
|
|
|
{ text: hasJourneyAnalysisContent.value ? "进行中:用户旅程分析" : "待完成:用户旅程分析", status: hasJourneyAnalysisContent.value ? "doing" : "" },
|
|
|
{ text: "待完成:提交实训成果", status: "" },
|
|
|
]);
|
|
|
|
|
|
function createShuffledJourneyOrder() {
|
|
|
const order = journeyStages.map((stage) => stage.id);
|
|
|
for (let index = order.length - 1; index > 0; index -= 1) {
|
|
|
const randomIndex = Math.floor(Math.random() * (index + 1));
|
|
|
[order[index], order[randomIndex]] = [order[randomIndex], order[index]];
|
|
|
}
|
|
|
if (order.every((stageId, index) => stageId === journeyStages[index].id)) {
|
|
|
[order[0], order[1]] = [order[1], order[0]];
|
|
|
}
|
|
|
return order;
|
|
|
}
|
|
|
|
|
|
const isClassifiedCorrectly = computed(() =>
|
|
|
scenarioStatements.every((item) => form.value.classifications[item.key] === item.defaultPhase)
|
|
|
);
|
|
|
|
|
|
const hasMatrixContent = computed(() =>
|
|
|
Object.values(form.value.matrix).some((row) =>
|
|
|
Object.values(row).some((value) => String(value || "").trim())
|
|
|
)
|
|
|
);
|
|
|
|
|
|
const hasJourneyAnalysisContent = computed(() =>
|
|
|
form.value.journeyAnalysis.some((row) =>
|
|
|
[row.behaviorDescription, row.keyTouchpoints, row.emotionExperience, row.decisionFactors]
|
|
|
.some((value) => String(value || "").trim())
|
|
|
)
|
|
|
);
|
|
|
|
|
|
onMounted(async () => {
|
|
|
await loadTaskConfig();
|
|
|
await loadSavedAnswer();
|
|
|
});
|
|
|
|
|
|
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) {
|
|
|
// Keep local defaults when history cannot be loaded.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function parseArray(value, fallback = []) {
|
|
|
if (Array.isArray(value)) return value.filter(Boolean);
|
|
|
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 = {
|
|
|
activeStep: normalizeActiveStep(parsed.currentStep),
|
|
|
journeyOrder: normalizeJourneyOrder(parsed.journeyOrder),
|
|
|
journeyAnalysis: normalizeJourneyAnalysis(parsed.journeyAnalysis),
|
|
|
journeyMap: normalizeJourneyMap(parsed.journeyMap),
|
|
|
journeyOptimizations: normalizeJourneyOptimizations(parsed.journeyOptimizations),
|
|
|
classifications: {
|
|
|
...createClassifications(),
|
|
|
...(parsed.classifications || {}),
|
|
|
},
|
|
|
matrix: normalizeMatrix(parsed.matrix),
|
|
|
};
|
|
|
journeyCheckedCorrectly.value = parsed.journeyCheckedCorrectly === true;
|
|
|
journeyCheckMessage.value = journeyCheckedCorrectly.value ? "排序正确,已完成本步骤。" : "";
|
|
|
} catch (error) {
|
|
|
// Ignore malformed historical answers.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function normalizeJourneyMap(value) {
|
|
|
return {
|
|
|
url: typeof value?.url === "string" ? value.url : "",
|
|
|
name: typeof value?.name === "string" ? value.name : "",
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function normalizeJourneyAnalysis(value) {
|
|
|
const savedRows = Array.isArray(value) ? value : [];
|
|
|
return createJourneyAnalysis().map((row) => {
|
|
|
const savedRow = savedRows.find((item) => item?.stageId === row.stageId) || {};
|
|
|
return {
|
|
|
...row,
|
|
|
behaviorDescription: savedRow.behaviorDescription || "",
|
|
|
keyTouchpoints: savedRow.keyTouchpoints || "",
|
|
|
emotionExperience: emotionOptions.includes(savedRow.emotionExperience) ? savedRow.emotionExperience : "",
|
|
|
decisionFactors: savedRow.decisionFactors || "",
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function normalizeJourneyOptimizations(value) {
|
|
|
const savedRows = Array.isArray(value) ? value : [];
|
|
|
return createJourneyOptimizations().map((row) => {
|
|
|
const savedRow = savedRows.find((item) => item?.stageId === row.stageId) || {};
|
|
|
return {
|
|
|
...row,
|
|
|
painPoint: savedRow.painPoint || "",
|
|
|
optimizationSuggestion: savedRow.optimizationSuggestion || "",
|
|
|
};
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function normalizeJourneyOrder(value) {
|
|
|
if (!Array.isArray(value) || value.length !== journeyStages.length) return createShuffledJourneyOrder();
|
|
|
const uniqueOrder = [...new Set(value)];
|
|
|
const validIds = journeyStages.map((stage) => stage.id);
|
|
|
if (uniqueOrder.length !== validIds.length || uniqueOrder.some((stageId) => !validIds.includes(stageId))) {
|
|
|
return createShuffledJourneyOrder();
|
|
|
}
|
|
|
return uniqueOrder;
|
|
|
}
|
|
|
|
|
|
function normalizeMatrix(value = {}) {
|
|
|
const matrix = createMatrix();
|
|
|
journeyRows.forEach((row) => {
|
|
|
phases.forEach((phase) => {
|
|
|
matrix[row.key][phase.key] = value?.[row.key]?.[phase.key] || "";
|
|
|
});
|
|
|
});
|
|
|
return matrix;
|
|
|
}
|
|
|
|
|
|
function normalizeActiveStep(value) {
|
|
|
const step = Number(value);
|
|
|
const total = trainingSteps.value.length;
|
|
|
if (!Number.isInteger(step) || step < 1) return 1;
|
|
|
return total ? Math.min(step, total) : 1;
|
|
|
}
|
|
|
|
|
|
async function setCurrentStep(step) {
|
|
|
if (!hasConfiguredSteps.value) return;
|
|
|
const targetStep = normalizeActiveStep(step);
|
|
|
if (targetStep > form.value.activeStep && [1, 2, 3].includes(form.value.activeStep) && !(await persist("IN_PROGRESS"))) return;
|
|
|
form.value.activeStep = targetStep;
|
|
|
}
|
|
|
|
|
|
function isSectionVisible(section) {
|
|
|
if (!hasConfiguredSteps.value) return true;
|
|
|
return Math.min(form.value.activeStep, 4) === section;
|
|
|
}
|
|
|
|
|
|
function goPreviousStep() {
|
|
|
setCurrentStep(form.value.activeStep - 1);
|
|
|
}
|
|
|
|
|
|
async function goNextStep() {
|
|
|
await setCurrentStep(form.value.activeStep + 1);
|
|
|
}
|
|
|
|
|
|
function startJourneyDrag(stageId) {
|
|
|
draggedStageId.value = stageId;
|
|
|
}
|
|
|
|
|
|
function endJourneyDrag() {
|
|
|
draggedStageId.value = "";
|
|
|
}
|
|
|
|
|
|
function dropJourneyStage(targetStageId) {
|
|
|
const sourceStageId = draggedStageId.value;
|
|
|
endJourneyDrag();
|
|
|
if (!sourceStageId || sourceStageId === targetStageId) return;
|
|
|
const sourceIndex = form.value.journeyOrder.indexOf(sourceStageId);
|
|
|
const targetIndex = form.value.journeyOrder.indexOf(targetStageId);
|
|
|
if (sourceIndex < 0 || targetIndex < 0) return;
|
|
|
const nextOrder = [...form.value.journeyOrder];
|
|
|
nextOrder.splice(sourceIndex, 1);
|
|
|
nextOrder.splice(targetIndex, 0, sourceStageId);
|
|
|
updateJourneyOrder(nextOrder);
|
|
|
}
|
|
|
|
|
|
function moveJourneyStage(index, offset) {
|
|
|
const targetIndex = index + offset;
|
|
|
if (targetIndex < 0 || targetIndex >= form.value.journeyOrder.length) return;
|
|
|
const nextOrder = [...form.value.journeyOrder];
|
|
|
[nextOrder[index], nextOrder[targetIndex]] = [nextOrder[targetIndex], nextOrder[index]];
|
|
|
updateJourneyOrder(nextOrder);
|
|
|
}
|
|
|
|
|
|
function updateJourneyOrder(nextOrder) {
|
|
|
form.value.journeyOrder = nextOrder;
|
|
|
journeyCheckedCorrectly.value = false;
|
|
|
journeyCheckMessage.value = "";
|
|
|
}
|
|
|
|
|
|
async function checkJourneyOrder() {
|
|
|
await ensureJourneyOrderValid(true);
|
|
|
}
|
|
|
|
|
|
async function ensureJourneyOrderValid(showSuccess = false) {
|
|
|
if (checkingJourney.value) return false;
|
|
|
checkingJourney.value = true;
|
|
|
try {
|
|
|
const response = await checkConsumerScenarioStepOne({ stageOrder: form.value.journeyOrder });
|
|
|
const result = response?.data || {};
|
|
|
journeyCheckedCorrectly.value = result.valid === true;
|
|
|
journeyCheckMessage.value = result.message || (result.valid ? "排序正确,已完成本步骤。" : "排序还不正确,请继续调整。");
|
|
|
if (result.valid && showSuccess) proxy?.$modal?.msgSuccess("排序正确,可以进入下一步");
|
|
|
return result.valid === true;
|
|
|
} catch (error) {
|
|
|
journeyCheckedCorrectly.value = false;
|
|
|
journeyCheckMessage.value = error?.message || "排序校验失败,请稍后重试。";
|
|
|
return false;
|
|
|
} finally {
|
|
|
checkingJourney.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function stageTitle(stageId) {
|
|
|
return journeyStages.find((stage) => stage.id === stageId)?.title || "未知阶段";
|
|
|
}
|
|
|
|
|
|
function stageShortTitle(stageId) {
|
|
|
return stageTitle(stageId).split("(")[0];
|
|
|
}
|
|
|
|
|
|
async function ensureJourneyAnalysisValid() {
|
|
|
if (checkingJourneyAnalysis.value) return false;
|
|
|
checkingJourneyAnalysis.value = true;
|
|
|
try {
|
|
|
const response = await checkConsumerScenarioStepTwo({ rows: form.value.journeyAnalysis });
|
|
|
const result = response?.data || {};
|
|
|
journeyAnalysisMessage.value = result.message || (result.valid ? "旅程分析填写完整。" : "请完整填写五个阶段的旅程分析。");
|
|
|
return result.valid === true;
|
|
|
} catch (error) {
|
|
|
journeyAnalysisMessage.value = error?.message || "旅程分析校验失败,请稍后重试。";
|
|
|
return false;
|
|
|
} finally {
|
|
|
checkingJourneyAnalysis.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function openJourneyMapPicker() {
|
|
|
journeyMapInput.value?.click();
|
|
|
}
|
|
|
|
|
|
function revokeBlobUrl(url) {
|
|
|
if (typeof url === "string" && url.startsWith("blob:")) URL.revokeObjectURL(url);
|
|
|
}
|
|
|
|
|
|
async function handleJourneyMapUpload(event) {
|
|
|
const file = event.target?.files?.[0];
|
|
|
if (!file) return;
|
|
|
const allowedTypes = ["image/png", "image/jpeg", "image/webp"];
|
|
|
if (!allowedTypes.includes(file.type)) {
|
|
|
proxy?.$modal?.msgError("请上传 PNG、JPG、JPEG 或 WEBP 图片");
|
|
|
event.target.value = "";
|
|
|
return;
|
|
|
}
|
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
|
proxy?.$modal?.msgError("图片大小不能超过 5MB");
|
|
|
event.target.value = "";
|
|
|
return;
|
|
|
}
|
|
|
uploadingJourneyMap.value = true;
|
|
|
try {
|
|
|
let url = "";
|
|
|
if (isStudentDemo()) {
|
|
|
url = URL.createObjectURL(file);
|
|
|
} else {
|
|
|
const formData = new FormData();
|
|
|
formData.append("file", file);
|
|
|
const response = await uploadStudentTrainingFile(formData);
|
|
|
url = response?.url || response?.data?.url || "";
|
|
|
}
|
|
|
if (!url) throw new Error("上传失败,请稍后重试");
|
|
|
revokeBlobUrl(form.value.journeyMap.url);
|
|
|
form.value.journeyMap = { url, name: file.name };
|
|
|
proxy?.$modal?.msgSuccess("用户购买旅程图上传成功");
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError(error?.message || "上传失败,请稍后重试");
|
|
|
} finally {
|
|
|
uploadingJourneyMap.value = false;
|
|
|
event.target.value = "";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function clearJourneyMap() {
|
|
|
revokeBlobUrl(form.value.journeyMap.url);
|
|
|
form.value.journeyMap = { url: "", name: "" };
|
|
|
}
|
|
|
|
|
|
async function ensureJourneyMapValid() {
|
|
|
try {
|
|
|
const response = await checkConsumerScenarioStepThree({ journeyMapUrl: form.value.journeyMap.url });
|
|
|
const result = response?.data || {};
|
|
|
if (!result.valid) proxy?.$modal?.msgError(result.message || "请上传用户购买旅程图");
|
|
|
return result.valid === true;
|
|
|
} catch (error) {
|
|
|
proxy?.$modal?.msgError(error?.message || "请先上传用户购买旅程图");
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function ensureJourneyOptimizationValid() {
|
|
|
if (checkingJourneyOptimization.value) return false;
|
|
|
checkingJourneyOptimization.value = true;
|
|
|
try {
|
|
|
const response = await checkConsumerScenarioStepFour({ rows: form.value.journeyOptimizations });
|
|
|
const result = response?.data || {};
|
|
|
journeyOptimizationMessage.value = result.message || (result.valid ? "五个阶段的优化分析填写完整。" : "请完整填写五个阶段的痛点和优化建议。");
|
|
|
return result.valid === true;
|
|
|
} catch (error) {
|
|
|
journeyOptimizationMessage.value = error?.message || "优化建议校验失败,请稍后重试。";
|
|
|
return false;
|
|
|
} finally {
|
|
|
checkingJourneyOptimization.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function downloadJourneyMapTemplate() {
|
|
|
const escapeCsvValue = (value) => `"${String(value || "").replaceAll('"', '""')}"`;
|
|
|
const header = ["序号", "旅程阶段", "用户行为描述", "关键触点", "情感体验", "关键决策因素", "痛点标记", "机会点标记"];
|
|
|
const rows = form.value.journeyAnalysis.map((row, index) => [
|
|
|
index + 1,
|
|
|
stageTitle(row.stageId),
|
|
|
row.behaviorDescription,
|
|
|
row.keyTouchpoints,
|
|
|
row.emotionExperience,
|
|
|
row.decisionFactors,
|
|
|
"",
|
|
|
"",
|
|
|
]);
|
|
|
const csv = [header, ...rows].map((row) => row.map(escapeCsvValue).join(",")).join("\r\n");
|
|
|
const url = URL.createObjectURL(new Blob([`\uFEFF${csv}`], { type: "text/csv;charset=utf-8" }));
|
|
|
const link = document.createElement("a");
|
|
|
link.href = url;
|
|
|
link.download = "用户购买旅程图模板.csv";
|
|
|
link.click();
|
|
|
URL.revokeObjectURL(url);
|
|
|
}
|
|
|
|
|
|
function buildOutcome() {
|
|
|
return {
|
|
|
taskKey: TASK_KEY,
|
|
|
taskId: taskConfig.value?.id || "",
|
|
|
taskName: taskTitle.value,
|
|
|
currentStep: form.value.activeStep,
|
|
|
journeyOrder: form.value.journeyOrder,
|
|
|
journeyCheckedCorrectly: journeyCheckedCorrectly.value,
|
|
|
journeyAnalysis: form.value.journeyAnalysis,
|
|
|
journeyMap: form.value.journeyMap,
|
|
|
journeyOptimizations: form.value.journeyOptimizations,
|
|
|
phases,
|
|
|
classifications: form.value.classifications,
|
|
|
matrix: form.value.matrix,
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function buildAnswerPayload(status) {
|
|
|
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
|
|
|
return {
|
|
|
saveAction,
|
|
|
currentStep: form.value.activeStep,
|
|
|
step1Answer: JSON.stringify(buildOutcome()),
|
|
|
};
|
|
|
}
|
|
|
|
|
|
async function persist(status = "IN_PROGRESS") {
|
|
|
if (saving.value) return;
|
|
|
if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureJourneyOrderValid())) return false;
|
|
|
if (status !== "RESET" && form.value.activeStep === 2 && !(await ensureJourneyAnalysisValid())) return false;
|
|
|
if (status !== "RESET" && form.value.activeStep === 3 && !(await ensureJourneyMapValid())) return false;
|
|
|
if (status !== "RESET" && form.value.activeStep === 4 && !(await ensureJourneyOptimizationValid())) return false;
|
|
|
saving.value = true;
|
|
|
try {
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
|
|
|
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
|
|
|
return true;
|
|
|
} finally {
|
|
|
saving.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function saveCurrentProgress() {
|
|
|
return persist("IN_PROGRESS");
|
|
|
}
|
|
|
|
|
|
function submitTask() {
|
|
|
return persist("SUBMITTED");
|
|
|
}
|
|
|
|
|
|
async function resetTraining() {
|
|
|
revokeBlobUrl(form.value.journeyMap.url);
|
|
|
form.value = createForm();
|
|
|
await persist("RESET");
|
|
|
}
|
|
|
|
|
|
function exportOutcome() {
|
|
|
const blob = new Blob([JSON.stringify(buildOutcome(), null, 2)], { type: "application/json;charset=utf-8" });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const link = document.createElement("a");
|
|
|
link.href = url;
|
|
|
link.download = `${taskTitle.value}-实训结果.json`;
|
|
|
link.click();
|
|
|
URL.revokeObjectURL(url);
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.consumer-scenario-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: 12px;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: #ffffff;
|
|
|
font-size: 30px;
|
|
|
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: 14px;
|
|
|
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: 18px;
|
|
|
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: 12px;
|
|
|
font-weight: 800;
|
|
|
line-height: 1.4;
|
|
|
}
|
|
|
|
|
|
.workbench-head h3 {
|
|
|
margin: 4px 0 8px;
|
|
|
color: #ffffff;
|
|
|
font-size: 26px;
|
|
|
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: 15px;
|
|
|
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: 14px;
|
|
|
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: 14px;
|
|
|
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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&:focus-visible {
|
|
|
outline: 2px solid rgba(92, 224, 255, 0.7);
|
|
|
outline-offset: 2px;
|
|
|
}
|
|
|
|
|
|
&.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: 12px;
|
|
|
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: 14px;
|
|
|
font-weight: 800;
|
|
|
line-height: 1.25;
|
|
|
text-overflow: ellipsis;
|
|
|
-webkit-box-orient: vertical;
|
|
|
-webkit-line-clamp: 2;
|
|
|
}
|
|
|
|
|
|
.step-action {
|
|
|
color: #66b6d4;
|
|
|
font-size: 10px;
|
|
|
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-card {
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.step-title-row {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: flex-start;
|
|
|
gap: 18px;
|
|
|
margin-bottom: 18px;
|
|
|
padding-bottom: 14px;
|
|
|
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
|
|
|
|
|
|
h3 {
|
|
|
margin: 4px 0 0;
|
|
|
color: #ffffff;
|
|
|
font-size: 22px;
|
|
|
font-weight: 900;
|
|
|
line-height: 1.3;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.scenario-step-section + .scenario-step-section {
|
|
|
margin-top: 0;
|
|
|
}
|
|
|
|
|
|
.journey-head {
|
|
|
margin: 0;
|
|
|
padding-bottom: 18px;
|
|
|
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
|
|
|
}
|
|
|
|
|
|
.journey-head h4 {
|
|
|
margin: 0;
|
|
|
color: #ffffff;
|
|
|
font-size: 22px;
|
|
|
font-weight: 900;
|
|
|
}
|
|
|
|
|
|
.journey-sort-head {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 20px;
|
|
|
margin-bottom: 22px;
|
|
|
padding: 20px;
|
|
|
border: 1px solid rgba(91, 218, 255, 0.28);
|
|
|
border-radius: 18px;
|
|
|
background: linear-gradient(120deg, rgba(7, 94, 132, 0.26), rgba(2, 26, 43, 0.72));
|
|
|
|
|
|
h4 {
|
|
|
margin: 5px 0 8px;
|
|
|
color: #ffffff;
|
|
|
font-size: 21px;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
max-width: 700px;
|
|
|
margin: 0;
|
|
|
color: #b9d8e9;
|
|
|
font-size: 14px;
|
|
|
line-height: 1.75;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-sort-kicker {
|
|
|
color: #69e3ff !important;
|
|
|
font-size: 12px !important;
|
|
|
font-weight: 900;
|
|
|
letter-spacing: 0.08em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.journey-check-action {
|
|
|
flex: 0 0 auto;
|
|
|
min-height: 42px;
|
|
|
padding: 0 18px;
|
|
|
border: 1px solid rgba(112, 235, 255, 0.74);
|
|
|
border-radius: 10px;
|
|
|
color: #ecfdff;
|
|
|
background: linear-gradient(135deg, #078ac2, #07558a);
|
|
|
box-shadow: 0 8px 20px rgba(0, 145, 203, 0.2);
|
|
|
font: inherit;
|
|
|
font-size: 14px;
|
|
|
font-weight: 800;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
transform: translateY(-1px);
|
|
|
background: linear-gradient(135deg, #0ba9df, #0870aa);
|
|
|
}
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.64;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-sort-list {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(9, minmax(0, 1fr));
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
}
|
|
|
|
|
|
.journey-sort-card {
|
|
|
position: relative;
|
|
|
display: grid;
|
|
|
grid-column: span 1;
|
|
|
min-height: 162px;
|
|
|
min-width: 0;
|
|
|
grid-template-rows: auto 1fr auto;
|
|
|
padding: 16px;
|
|
|
border: 1px solid rgba(63, 174, 222, 0.48);
|
|
|
border-radius: 14px;
|
|
|
color: #ecfbff;
|
|
|
background: linear-gradient(150deg, rgba(8, 74, 109, 0.82), rgba(3, 24, 42, 0.9));
|
|
|
box-shadow: inset 0 1px 0 rgba(201, 247, 255, 0.1), 0 12px 24px rgba(0, 0, 0, 0.16);
|
|
|
cursor: grab;
|
|
|
transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
|
|
|
|
|
|
&:hover {
|
|
|
transform: translateY(-3px);
|
|
|
border-color: rgba(106, 235, 255, 0.94);
|
|
|
box-shadow: inset 0 1px 0 rgba(201, 247, 255, 0.16), 0 16px 30px rgba(0, 0, 0, 0.28), 0 0 20px rgba(15, 199, 242, 0.14);
|
|
|
}
|
|
|
|
|
|
&.dragging {
|
|
|
opacity: 0.45;
|
|
|
cursor: grabbing;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-card-order {
|
|
|
color: #78e9ff;
|
|
|
font-size: 12px;
|
|
|
font-weight: 900;
|
|
|
letter-spacing: 0.08em;
|
|
|
}
|
|
|
|
|
|
.journey-card-copy {
|
|
|
align-self: center;
|
|
|
|
|
|
strong,
|
|
|
span {
|
|
|
display: block;
|
|
|
}
|
|
|
|
|
|
strong {
|
|
|
margin-bottom: 8px;
|
|
|
color: #ffffff;
|
|
|
font-size: 15px;
|
|
|
line-height: 1.45;
|
|
|
}
|
|
|
|
|
|
span {
|
|
|
color: #b6d9e9;
|
|
|
font-size: 12px;
|
|
|
line-height: 1.65;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-card-actions {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
gap: 6px;
|
|
|
|
|
|
button {
|
|
|
display: inline-grid;
|
|
|
width: 26px;
|
|
|
height: 26px;
|
|
|
place-items: center;
|
|
|
border: 1px solid rgba(111, 220, 246, 0.45);
|
|
|
border-radius: 7px;
|
|
|
color: #dffbff;
|
|
|
background: rgba(4, 41, 65, 0.76);
|
|
|
font: inherit;
|
|
|
font-weight: 900;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:hover:not(:disabled) {
|
|
|
border-color: #72e8ff;
|
|
|
background: rgba(9, 119, 160, 0.9);
|
|
|
}
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.28;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-sort-arrow {
|
|
|
justify-self: center;
|
|
|
color: #42cfff;
|
|
|
font-size: 20px;
|
|
|
font-weight: 900;
|
|
|
}
|
|
|
|
|
|
.journey-check-message {
|
|
|
margin: 20px 0 0;
|
|
|
padding: 11px 14px;
|
|
|
border: 1px solid rgba(255, 136, 106, 0.7);
|
|
|
border-radius: 10px;
|
|
|
color: #ffd2c7;
|
|
|
background: rgba(127, 45, 30, 0.25);
|
|
|
font-size: 14px;
|
|
|
font-weight: 700;
|
|
|
|
|
|
&.success {
|
|
|
border-color: rgba(90, 238, 198, 0.65);
|
|
|
color: #d6fff3;
|
|
|
background: rgba(16, 122, 98, 0.25);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-analysis-intro {
|
|
|
margin-bottom: 18px;
|
|
|
padding: 20px;
|
|
|
border-left: 3px solid #3bd8ff;
|
|
|
border-radius: 0 16px 16px 0;
|
|
|
background: linear-gradient(90deg, rgba(7, 95, 137, 0.28), rgba(4, 31, 49, 0.18));
|
|
|
|
|
|
h4 {
|
|
|
margin: 4px 0 8px;
|
|
|
color: #ffffff;
|
|
|
font-size: 21px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: #c3dfec;
|
|
|
font-size: 14px;
|
|
|
line-height: 1.8;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-analysis-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid rgba(72, 182, 228, 0.46);
|
|
|
border-radius: 16px;
|
|
|
background: rgba(2, 20, 33, 0.72);
|
|
|
}
|
|
|
|
|
|
.journey-analysis-table {
|
|
|
width: 100%;
|
|
|
min-width: 1120px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
|
|
|
th,
|
|
|
td {
|
|
|
border: 1px solid rgba(77, 167, 220, 0.28);
|
|
|
}
|
|
|
|
|
|
thead th {
|
|
|
padding: 13px 10px;
|
|
|
color: #effcff;
|
|
|
background: linear-gradient(90deg, rgba(8, 128, 178, 0.94), rgba(5, 94, 143, 0.9));
|
|
|
font-size: 14px;
|
|
|
font-weight: 900;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
tbody tr {
|
|
|
background: rgba(2, 17, 29, 0.48);
|
|
|
|
|
|
&:nth-child(even) {
|
|
|
background: rgba(9, 47, 67, 0.34);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
td,
|
|
|
tbody th {
|
|
|
padding: 10px;
|
|
|
vertical-align: top;
|
|
|
}
|
|
|
|
|
|
th:nth-child(1) { width: 54px; }
|
|
|
th:nth-child(2) { width: 132px; }
|
|
|
th:nth-child(3), th:nth-child(4), th:nth-child(6) { width: 27%; }
|
|
|
th:nth-child(5) { width: 118px; }
|
|
|
|
|
|
textarea,
|
|
|
select {
|
|
|
width: 100%;
|
|
|
border: 1px solid transparent;
|
|
|
border-radius: 8px;
|
|
|
color: #eaf9ff;
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-size: 13px;
|
|
|
line-height: 1.65;
|
|
|
outline: none;
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
&:focus {
|
|
|
border-color: rgba(92, 224, 255, 0.76);
|
|
|
background: rgba(8, 64, 92, 0.42);
|
|
|
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
textarea {
|
|
|
min-height: 112px;
|
|
|
padding: 8px;
|
|
|
resize: vertical;
|
|
|
}
|
|
|
|
|
|
textarea::placeholder {
|
|
|
color: #7897a8;
|
|
|
}
|
|
|
|
|
|
select {
|
|
|
min-height: 38px;
|
|
|
margin-top: 30px;
|
|
|
padding: 0 9px;
|
|
|
border-color: rgba(99, 177, 211, 0.6);
|
|
|
background: rgba(6, 38, 58, 0.78);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-analysis-index,
|
|
|
.journey-analysis-stage {
|
|
|
color: #dff7ff;
|
|
|
font-weight: 800;
|
|
|
text-align: center;
|
|
|
vertical-align: middle !important;
|
|
|
}
|
|
|
|
|
|
.journey-analysis-stage {
|
|
|
color: #9defff;
|
|
|
line-height: 1.65;
|
|
|
}
|
|
|
|
|
|
.journey-map-intro {
|
|
|
margin-bottom: 18px;
|
|
|
padding: 20px 22px;
|
|
|
border: 1px solid rgba(69, 202, 249, 0.36);
|
|
|
border-radius: 18px;
|
|
|
background: linear-gradient(112deg, rgba(9, 99, 145, 0.28), rgba(2, 22, 38, 0.48));
|
|
|
|
|
|
span {
|
|
|
color: #51d8ff;
|
|
|
font-size: 13px;
|
|
|
font-weight: 900;
|
|
|
letter-spacing: 0.08em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
h4 {
|
|
|
margin: 6px 0 8px;
|
|
|
color: #ffffff;
|
|
|
font-size: 22px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: #bed8e6;
|
|
|
font-size: 14px;
|
|
|
line-height: 1.8;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.hidden-file-input {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.journey-map-upload-panel {
|
|
|
min-height: 330px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
gap: 12px;
|
|
|
padding: 28px;
|
|
|
border: 1px dashed rgba(78, 210, 255, 0.64);
|
|
|
border-radius: 20px;
|
|
|
color: #e9fbff;
|
|
|
background: radial-gradient(circle at 50% 32%, rgba(13, 119, 171, 0.22), rgba(2, 18, 31, 0.52) 62%);
|
|
|
|
|
|
strong {
|
|
|
font-size: 18px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0 0 8px;
|
|
|
color: #9abac9;
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-map-upload-icon {
|
|
|
width: 52px;
|
|
|
height: 52px;
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
border: 1px solid rgba(94, 222, 255, 0.62);
|
|
|
border-radius: 50%;
|
|
|
color: #71e5ff;
|
|
|
background: rgba(8, 100, 148, 0.3);
|
|
|
font-size: 25px;
|
|
|
}
|
|
|
|
|
|
.journey-map-preview {
|
|
|
width: min(100%, 760px);
|
|
|
max-height: 360px;
|
|
|
overflow: hidden;
|
|
|
border: 1px solid rgba(90, 214, 255, 0.45);
|
|
|
border-radius: 12px;
|
|
|
background: #03111c;
|
|
|
cursor: zoom-in;
|
|
|
|
|
|
:deep(img) {
|
|
|
object-fit: contain;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-map-file-info {
|
|
|
max-width: 100%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
color: #cbf5ff;
|
|
|
font-size: 14px;
|
|
|
|
|
|
span {
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-map-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.journey-map-helper {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 24px;
|
|
|
margin-top: 20px;
|
|
|
padding: 18px 20px;
|
|
|
border: 1px solid rgba(61, 162, 210, 0.28);
|
|
|
border-radius: 14px;
|
|
|
color: #b9d4df;
|
|
|
background: rgba(3, 27, 43, 0.46);
|
|
|
|
|
|
h5,
|
|
|
p {
|
|
|
margin: 0;
|
|
|
font-size: 13px;
|
|
|
line-height: 1.8;
|
|
|
}
|
|
|
|
|
|
h5 {
|
|
|
color: #e5faff;
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.journey-map-prompt {
|
|
|
margin-top: 4px;
|
|
|
color: #80dff8;
|
|
|
}
|
|
|
|
|
|
.el-button {
|
|
|
flex: 0 0 auto;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-optimization-intro {
|
|
|
margin-bottom: 18px;
|
|
|
padding: 20px 22px;
|
|
|
border-left: 3px solid #41d9ff;
|
|
|
border-radius: 0 16px 16px 0;
|
|
|
background: linear-gradient(90deg, rgba(7, 98, 143, 0.3), rgba(3, 27, 42, 0.12));
|
|
|
|
|
|
span {
|
|
|
color: #51d8ff;
|
|
|
font-size: 13px;
|
|
|
font-weight: 900;
|
|
|
letter-spacing: 0.08em;
|
|
|
}
|
|
|
|
|
|
h4 {
|
|
|
margin: 6px 0 8px;
|
|
|
color: #ffffff;
|
|
|
font-size: 21px;
|
|
|
}
|
|
|
|
|
|
p {
|
|
|
margin: 0;
|
|
|
color: #c4dfe9;
|
|
|
font-size: 14px;
|
|
|
line-height: 1.8;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-optimization-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid rgba(72, 182, 228, 0.46);
|
|
|
border-radius: 16px;
|
|
|
background: rgba(2, 20, 33, 0.72);
|
|
|
}
|
|
|
|
|
|
.journey-optimization-table {
|
|
|
width: 100%;
|
|
|
min-width: 900px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
|
|
|
th,
|
|
|
td {
|
|
|
border: 1px solid rgba(77, 167, 220, 0.28);
|
|
|
}
|
|
|
|
|
|
thead th {
|
|
|
padding: 14px 12px;
|
|
|
color: #f0fcff;
|
|
|
background: linear-gradient(90deg, rgba(8, 128, 178, 0.94), rgba(5, 94, 143, 0.9));
|
|
|
font-size: 14px;
|
|
|
font-weight: 900;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
thead th:first-child {
|
|
|
width: 170px;
|
|
|
}
|
|
|
|
|
|
tbody th {
|
|
|
padding: 18px 14px;
|
|
|
color: #a6ecff;
|
|
|
background: rgba(3, 36, 55, 0.58);
|
|
|
font-size: 14px;
|
|
|
font-weight: 800;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
td {
|
|
|
padding: 10px;
|
|
|
background: rgba(1, 17, 29, 0.45);
|
|
|
}
|
|
|
|
|
|
textarea {
|
|
|
width: 100%;
|
|
|
min-height: 92px;
|
|
|
padding: 10px 12px;
|
|
|
border: 1px solid transparent;
|
|
|
border-radius: 9px;
|
|
|
outline: none;
|
|
|
resize: vertical;
|
|
|
color: #ebfaff;
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-size: 13px;
|
|
|
line-height: 1.65;
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
&:focus {
|
|
|
border-color: rgba(92, 224, 255, 0.72);
|
|
|
background: rgba(8, 64, 92, 0.42);
|
|
|
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
textarea::placeholder {
|
|
|
color: #7696a8;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-flow {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
|
|
align-items: center;
|
|
|
gap: 28px;
|
|
|
margin-top: 18px;
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.phase-node {
|
|
|
position: relative;
|
|
|
min-height: 78px;
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
padding: 14px 16px;
|
|
|
border: 1px solid rgba(0, 180, 255, 0.22);
|
|
|
border-radius: 14px;
|
|
|
color: #dff8ff;
|
|
|
background: rgba(1, 14, 26, 0.72);
|
|
|
font-size: 14px;
|
|
|
font-weight: 800;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.phase-arrow {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.phase-node:not(:last-child)::after {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
right: -29px;
|
|
|
width: 28px;
|
|
|
height: 2px;
|
|
|
background: #2bb8ff;
|
|
|
transform: translateY(-50%);
|
|
|
}
|
|
|
|
|
|
.phase-node:not(:last-child)::before {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
right: -31px;
|
|
|
width: 8px;
|
|
|
height: 8px;
|
|
|
border-top: 2px solid #2bb8ff;
|
|
|
border-right: 2px solid #2bb8ff;
|
|
|
transform: translateY(-50%) rotate(45deg);
|
|
|
}
|
|
|
|
|
|
.requirement {
|
|
|
margin: 20px 0 16px;
|
|
|
color: #d7ecff;
|
|
|
font-size: 15px;
|
|
|
font-weight: 400;
|
|
|
line-height: 1.8;
|
|
|
}
|
|
|
|
|
|
.scenario-list {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
margin-bottom: 18px;
|
|
|
}
|
|
|
|
|
|
.scenario-row {
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(0, 1fr) 220px;
|
|
|
gap: 12px;
|
|
|
align-items: center;
|
|
|
color: #d7ecff;
|
|
|
font-size: 14px;
|
|
|
line-height: 1.7;
|
|
|
}
|
|
|
|
|
|
.phase-select {
|
|
|
width: 100%;
|
|
|
min-height: 42px;
|
|
|
padding: 0 14px;
|
|
|
border: 1px solid rgba(45, 95, 126, 0.95);
|
|
|
border-radius: 14px;
|
|
|
color: #eef5ff;
|
|
|
background: #0f1a24;
|
|
|
font: inherit;
|
|
|
|
|
|
&:focus {
|
|
|
border-color: #5fe7ff;
|
|
|
background: rgba(10, 31, 46, 0.98);
|
|
|
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.journey-table-wrap {
|
|
|
overflow-x: auto;
|
|
|
border: 1px solid rgba(0, 180, 255, 0.22);
|
|
|
border-radius: 22px;
|
|
|
background: rgba(1, 14, 26, 0.72);
|
|
|
}
|
|
|
|
|
|
.journey-table {
|
|
|
width: 100%;
|
|
|
min-width: 980px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
}
|
|
|
|
|
|
.journey-table th,
|
|
|
.journey-table td {
|
|
|
border: 1px solid rgba(77, 167, 220, 0.2);
|
|
|
}
|
|
|
|
|
|
.journey-table thead th {
|
|
|
height: 46px;
|
|
|
padding: 12px;
|
|
|
color: #eaffff;
|
|
|
background: rgba(8, 55, 82, 0.98);
|
|
|
font-weight: 900;
|
|
|
}
|
|
|
|
|
|
.journey-table tbody th {
|
|
|
width: 120px;
|
|
|
padding: 12px;
|
|
|
color: #eafaff;
|
|
|
background: rgba(1, 14, 26, 0.72);
|
|
|
font-weight: 900;
|
|
|
}
|
|
|
|
|
|
.row-head {
|
|
|
width: 120px;
|
|
|
}
|
|
|
|
|
|
.journey-table td {
|
|
|
height: 108px;
|
|
|
padding: 12px;
|
|
|
background: rgba(1, 14, 26, 0.48);
|
|
|
}
|
|
|
|
|
|
.table-input {
|
|
|
display: block;
|
|
|
width: 100%;
|
|
|
min-height: 110px;
|
|
|
padding: 12px 14px;
|
|
|
border: 1px solid rgba(45, 95, 126, 0.95);
|
|
|
border-radius: 14px;
|
|
|
outline: none;
|
|
|
resize: none;
|
|
|
color: #eef5ff;
|
|
|
background: #0f1a24;
|
|
|
font-family: inherit;
|
|
|
font-size: 13px;
|
|
|
line-height: 1.6;
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
.table-input::placeholder {
|
|
|
color: #8299aa;
|
|
|
}
|
|
|
|
|
|
.table-input:focus {
|
|
|
border-color: #5fe7ff;
|
|
|
background: rgba(10, 31, 46, 0.98);
|
|
|
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
|
|
|
}
|
|
|
|
|
|
.reference-map {
|
|
|
margin-top: 28px;
|
|
|
padding: 16px;
|
|
|
border: 1px solid rgba(0, 180, 255, 0.22);
|
|
|
border-radius: 22px;
|
|
|
background: rgba(1, 14, 26, 0.72);
|
|
|
}
|
|
|
|
|
|
.step-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 14px;
|
|
|
margin-top: 22px;
|
|
|
margin-bottom: 36px;
|
|
|
}
|
|
|
|
|
|
.nav-action {
|
|
|
min-width: 112px;
|
|
|
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-family: inherit;
|
|
|
font-size: 14px;
|
|
|
font-weight: 800;
|
|
|
cursor: pointer;
|
|
|
|
|
|
&:disabled {
|
|
|
cursor: not-allowed;
|
|
|
opacity: 0.45;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.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);
|
|
|
}
|
|
|
|
|
|
.reference-title {
|
|
|
margin-bottom: 14px;
|
|
|
color: #dff5ff;
|
|
|
font-size: 15px;
|
|
|
font-weight: 800;
|
|
|
}
|
|
|
|
|
|
.reference-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: 120px repeat(6, minmax(0, 1fr));
|
|
|
gap: 8px;
|
|
|
margin-top: 8px;
|
|
|
}
|
|
|
|
|
|
.reference-side,
|
|
|
.stage-chip,
|
|
|
.reference-cell,
|
|
|
.behavior-lane,
|
|
|
.emotion-chart {
|
|
|
border: 1px solid rgba(0, 180, 255, 0.22);
|
|
|
background: rgba(1, 14, 26, 0.48);
|
|
|
}
|
|
|
|
|
|
.reference-side {
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
min-height: 54px;
|
|
|
color: #dff5ff;
|
|
|
font-weight: 900;
|
|
|
}
|
|
|
|
|
|
.stage-chip {
|
|
|
min-height: 34px;
|
|
|
display: grid;
|
|
|
place-items: center;
|
|
|
color: #eaffff;
|
|
|
background: rgba(8, 55, 82, 0.98);
|
|
|
font-weight: 900;
|
|
|
}
|
|
|
|
|
|
.reference-cell {
|
|
|
min-height: 58px;
|
|
|
padding: 10px;
|
|
|
color: #d7ecff;
|
|
|
font-size: 12px;
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
|
|
|
.reference-cell p {
|
|
|
margin: 0;
|
|
|
}
|
|
|
|
|
|
.reference-grid--behavior,
|
|
|
.reference-grid--emotion {
|
|
|
grid-template-columns: 120px minmax(0, 1fr);
|
|
|
}
|
|
|
|
|
|
.behavior-lane {
|
|
|
min-height: 130px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 22px;
|
|
|
padding: 16px;
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.behavior-node {
|
|
|
position: relative;
|
|
|
min-width: 82px;
|
|
|
color: #d9f1ff;
|
|
|
font-size: 12px;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.behavior-node::before {
|
|
|
content: "";
|
|
|
display: block;
|
|
|
width: 30px;
|
|
|
height: 30px;
|
|
|
margin: 0 auto 8px;
|
|
|
border-radius: 50%;
|
|
|
background: linear-gradient(180deg, #38c8ef, #0b84bd);
|
|
|
box-shadow: 0 0 16px rgba(0, 174, 255, 0.24);
|
|
|
}
|
|
|
|
|
|
.behavior-node:not(:last-child)::after {
|
|
|
content: "";
|
|
|
position: absolute;
|
|
|
top: 15px;
|
|
|
left: calc(50% + 26px);
|
|
|
width: 56px;
|
|
|
height: 2px;
|
|
|
background: rgba(92, 224, 255, 0.5);
|
|
|
}
|
|
|
|
|
|
.emotion-chart {
|
|
|
position: relative;
|
|
|
min-height: 210px;
|
|
|
padding: 10px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.emotion-chart svg {
|
|
|
width: 100%;
|
|
|
height: 190px;
|
|
|
}
|
|
|
|
|
|
.emotion-points span {
|
|
|
position: absolute;
|
|
|
display: grid;
|
|
|
gap: 2px;
|
|
|
color: #f5fbff;
|
|
|
font-size: 18px;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.emotion-points small {
|
|
|
color: #d7ecff;
|
|
|
font-size: 11px;
|
|
|
}
|
|
|
|
|
|
.training-actions {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
justify-content: center;
|
|
|
gap: 52px;
|
|
|
margin-top: 36px;
|
|
|
}
|
|
|
|
|
|
.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: 24px;
|
|
|
font-weight: 900;
|
|
|
line-height: 1;
|
|
|
letter-spacing: 0;
|
|
|
text-shadow: none;
|
|
|
}
|
|
|
|
|
|
.training-action:hover:not(.is-disabled) {
|
|
|
border-color: #46b5e7;
|
|
|
color: #ffffff;
|
|
|
background: rgba(18, 85, 116, 0.78);
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1100px) {
|
|
|
.consumer-scenario-page {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
.consumer-scenario-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%;
|
|
|
}
|
|
|
|
|
|
.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 20px;
|
|
|
min-width: 0;
|
|
|
width: 2px;
|
|
|
height: 20px;
|
|
|
margin-left: 29px;
|
|
|
text-align: center;
|
|
|
writing-mode: vertical-rl;
|
|
|
}
|
|
|
|
|
|
.journey-flow,
|
|
|
.scenario-row {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.journey-sort-head {
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.journey-check-action {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.journey-sort-list {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.journey-sort-card {
|
|
|
min-height: 126px;
|
|
|
}
|
|
|
|
|
|
.journey-sort-arrow {
|
|
|
transform: rotate(90deg);
|
|
|
}
|
|
|
|
|
|
.journey-map-helper {
|
|
|
align-items: stretch;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.journey-map-helper .el-button {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.nav-action,
|
|
|
.training-action {
|
|
|
width: 100%;
|
|
|
}
|
|
|
|
|
|
.phase-node::before,
|
|
|
.phase-node::after {
|
|
|
display: none;
|
|
|
}
|
|
|
|
|
|
.reference-grid,
|
|
|
.reference-grid--behavior,
|
|
|
.reference-grid--emotion {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
}
|
|
|
.workbench-head--actions-only {
|
|
|
justify-content: flex-end;
|
|
|
margin-bottom: 18px;
|
|
|
padding-bottom: 0;
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
|