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

1655 lines
46 KiB
Vue

This file contains ambiguous Unicode characters!

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

<template>
<div class="target-profile-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="steps.length" class="step-indicator" aria-label="目标用户画像步骤">
<template v-for="(step, index) in steps" :key="step.no">
<button
type="button"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
:aria-label="`第${step.no}步:${step.name}`"
@click="currentStep = step.no"
>
<span class="step-marker">{{ String(step.no).padStart(2, '0') }}</span>
<span class="step-copy">
<strong class="step-name" :title="step.name">{{ step.name }}</strong>
<span class="step-action">{{ currentStep === step.no ? "当前步骤" : "点击切换" }} <span aria-hidden="true">→</span></span>
</span>
</button>
<span v-if="index < steps.length - 1" class="step-connector" aria-hidden="true"></span>
</template>
</nav>
<section class="step-card target-profile-step-card">
<div v-if="activeStepName" class="step-title-row">
<div>
<p class="section-eyebrow">Step {{ String(currentStep).padStart(2, '0') }}</p>
<h3>{{ activeStepName }}</h3>
</div>
</div>
<section v-show="currentStep === 1" class="step-panel">
<p class="task-desc">
通过电商数据平台,搜索“智能吸顶灯”、助眠产品等产品的客户群画像。如没有电商数据平台账号,可通过百度指数进行搜索,并将搜索结果截图,进行分析。
</p>
<div class="upload-panel">
<input ref="fileInputRef" type="file" class="file-input" accept=".png,.jpg,.jpeg,.webp,.pdf,.doc,.docx,.xls,.xlsx" @change="handleFileChange" />
<el-button class="upload-action" :loading="uploading" @click="triggerFileSelect">
<el-icon><Upload /></el-icon>
上传客户群画像搜索结果
</el-button>
<div v-if="form.profileFileName" class="file-chip">
<el-icon><Document /></el-icon>
<span>{{ form.profileFileName }}</span>
</div>
</div>
<label class="field-block">
<span>智能吸顶灯、助眠产品等产品客户群画像分析:</span>
<textarea
v-model="form.profileAnalysis"
class="profile-textarea"
placeholder="结合搜索结果填写客户群画像,例如年龄、性别、地域、消费能力、核心需求、典型场景、内容偏好和购买顾虑。"
/>
</label>
</section>
<section v-show="currentStep === 2" class="step-panel">
<p class="task-desc task-desc--compact">
重点用户画像开发团队已经访谈了3位重点用户并撰写了访谈记录请根据访谈记录从用户的行为特征、需求痛点、消费心理等三个维度进行重点用户画像。
</p>
<div class="interview-tabs" aria-label="重点用户访谈记录">
<button
v-for="(record, index) in form.keyUserRecords"
:key="record.code"
type="button"
:class="['interview-tab', { active: activeInterviewIndex === index }]"
@click="handleInterviewClick(index)"
>
{{ record.title }}{{ record.code }}
</button>
</div>
<div v-if="interviewDialogVisible" class="interview-dialog-layer" role="dialog" aria-modal="true">
<div class="interview-dialog">
<button type="button" class="interview-dialog-close" @click="interviewDialogVisible = false">关闭</button>
<div class="interview-dialog-body">
<p class="interview-line">访谈编号:{{ currentInterviewTranscript.code }}</p>
<p class="interview-line">基本属性:{{ currentInterviewTranscript.basicAttributes }}</p>
<p class="interview-title">访谈内容:</p>
<div v-for="item in currentInterviewTranscript.content" :key="item.question" class="interview-qa">
<p>Q{{ item.question }}</p>
<p>A{{ item.answer }}</p>
</div>
</div>
</div>
</div>
<div class="portrait-table-wrap">
<section v-for="(record, index) in form.keyUserRecords" :key="record.code" class="portrait-record">
<div class="portrait-user">
<p v-if="index === 0" class="portrait-example">示例</p>
<label>
<span>用户:</span>
<input v-model="record.user" class="portrait-input" />
</label>
<label>
<span>基础属性:</span>
<input v-model="record.basicAttributes" class="portrait-input portrait-input--wide" />
</label>
<label>
<span>标签:</span>
<input v-model="record.tags" class="portrait-input portrait-input--wide" />
</label>
</div>
<div class="portrait-grid">
<div class="portrait-head">行为特征</div>
<div class="portrait-head">需求痛点</div>
<div class="portrait-head">消费心理</div>
<textarea v-model="record.behavior" class="portrait-cell" />
<textarea v-model="record.painPoints" class="portrait-cell" />
<textarea v-model="record.psychology" class="portrait-cell" />
</div>
</section>
</div>
</section>
<section v-show="currentStep === 3" class="step-panel">
<div class="segmentation-intro">
<p>用户细分与市场选择。结合上述分析,进一步细分市场,寻找产品开发机会,填写下表。</p>
<p>1确定细分变量和细分市场往往采用多变量组合进行市场细分。</p>
<p>2评估每个细分市场的吸引力可以不用细分用户的行为和需求痛点分析市场的吸引力。</p>
<p>3选择目标细分市场每个目标细分市场定位。</p>
</div>
<div class="segmentation-table-wrap">
<table class="segmentation-table">
<thead>
<tr>
<th>细分市场</th>
<th>客户分析</th>
<th>产品机会</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in form.segmentationRows" :key="index">
<td>
<input v-model="row.market" class="segmentation-market-input" />
</td>
<td>
<textarea v-model="row.customerAnalysis" class="segmentation-cell-input" />
</td>
<td>
<textarea v-model="row.productOpportunity" class="segmentation-cell-input" />
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section v-show="currentStep === 4" class="step-panel">
<div class="product-plan-layout">
<div class="product-plan-form">
<p class="product-plan-desc">结合上述分析,请说明如果你来开发相关的产品,计划设计那些核心功能,预计产品的价格区间是多少。</p>
<div class="product-plan-label">计划设计的核心功能:</div>
<textarea
v-for="(_, index) in form.coreFunctions"
:key="index"
v-model="form.coreFunctions[index]"
class="core-function-input"
:placeholder="`核心功能${index + 1}`"
/>
<div class="product-plan-label product-plan-label--price">预计产品价格区间(元):</div>
<div class="price-range-row">
<input v-model="form.priceMin" class="price-input" />
<span></span>
<input v-model="form.priceMax" class="price-input" />
</div>
<label class="analysis-conclusion-block">
<span>分析结论:</span>
<textarea v-model="form.conclusion" class="analysis-conclusion-input" />
</label>
</div>
<div class="product-reference">
<img :src="productReferenceImage" alt="小米有品三雄极光天域声光伴眠吸顶灯参考图" />
</div>
</div>
</section>
<div v-if="steps.length" class="step-actions">
<el-button class="step-action step-action--previous" :disabled="currentStep <= 1" @click="currentStep -= 1">
<el-icon><ArrowLeft /></el-icon>
上一步
</el-button>
<el-button class="step-action step-action--save" :loading="saving" @click="saveCurrentProgress">
<el-icon><CircleCheck /></el-icon>
保存当前进度
</el-button>
<el-button class="step-action step-action--next" :disabled="currentStep >= steps.length" @click="currentStep += 1">
下一步
<el-icon><ArrowRight /></el-icon>
</el-button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
</el-button>
</div>
</section>
</section>
</main>
<TrainingAiSidebar
study-tip=""
reference-title=""
reference-text="· <br />· 消费特征价格敏感度购买渠道品牌偏好<br />· 场景需求家庭照明睡眠改善健康管理<br />· 决策阻碍安装成本效果可信度售后保障"
assist-text="如果画像分析较散可以先列出目标人群标签再补充证据来源最后总结最值得切入的重点用户群"
:progress-items="progressItems"
/>
</div>
</template>
<script setup>
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Upload } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import productReferenceImage from "@/assets/images/target-user-profile-u1344.png";
const TASK_KEY = "target-user-profile";
const { proxy } = getCurrentInstance();
const taskConfig = ref(null);
const saving = ref(false);
const uploading = ref(false);
const fileInputRef = ref(null);
const currentStep = ref(1);
const activeInterviewIndex = ref(0);
const interviewDialogVisible = ref(false);
const defaultRequirement = "目标用户画像用于把市场数据转化为具体用户认知帮助判断产品应该优先服务哪类人群以及围绕哪些场景设计卖点";
const defaultBackground =
"以智能吸顶灯助眠产品等产品为例通过电商平台或百度指数等数据来源收集客户群画像信息分析目标人群特征需求场景和购买决策因素";
const defaultGoals = [
"掌握通过平台数据识别目标用户群的方法",
"能够从搜索结果中提炼用户属性需求和行为特征",
"能够形成面向产品定位和后续选品判断的用户画像结论",
];
const interviewTranscripts = [
{
code: "U01",
basicAttributes: "李伟29男性上海互联网公司程序员月收入约2.8万元",
content: [
{
question: "您平时的睡眠质量如何通常因为什么失眠",
answer:
"非常差几乎每天都熬夜不是因为想玩是脑子停不下来一直在想工作上的bug和方案即使躺下也觉得心神不宁入睡特别困难有时候心脏会有点不舒服知道是压力大和缺觉搞的",
},
{
question: "您尝试过哪些方法来改善睡眠",
answer:
"买过褪黑素吃了一段时间感觉效果不明显而且怕有依赖也用过一些冥想App听着白噪音确实有点用但手机亮着屏幕放在旁边总觉得不舒服而且容易被消息通知打断",
},
{
question: "如果有一款智能助眠灯您最希望它有什么功能",
answer:
"首先光一定要柔和能让我放松下来其次最好能无缝衔接我用的冥想App的内容别让我再来回切换还有就是我挺想知道自己每晚到底睡了多久醒了多少次有个数据报告会让我觉得这钱花得值如果能在我睡着后自动关灯调低空调温度那就太智能了",
},
{
question: "在购买这类产品时您最看重什么",
answer:
"效果和科技感价格不是最敏感的因素只要它真的能帮我解决问题我愿意为专业和便捷付费品牌最好有点调性别显得太廉价",
},
],
},
{
code: "U02",
basicAttributes: "赵静31女性杭州中学教师月收入约1.5万元",
content: [
{
question: "您目前的睡眠状况是怎样的",
answer:
"碎片化非常碎片化宝宝现在一岁多夜里还是会醒从她出生后我就没睡过一个整觉现在问题是即使宝宝睡了我自己也因为长期疲劳变得神经衰弱很难深度睡眠有点声音就醒",
},
{
question: "在您的睡前时光通常有什么习惯或需求",
answer:
"我需要一段属于自己的缓冲时间会看会儿手机但知道蓝光不好也希望有一些温和的声音能盖掉家里的其他杂音帮助我放松另外夜里起床照顾宝宝需要一盏能瞬间提供柔和不刺眼照明的小灯现在开大灯太晃眼了",
},
{
question: "对于智能助眠灯您有什么期待",
answer:
"操作一定要简单我可没时间研究复杂的APP最好能语音控制比如我说小眠开夜灯它就亮起最暗的光灯光颜色最好能适合哺乳内容上我希望有一些针对成人的睡前放松引导或者温和的轻音乐最好也能播放一些给宝宝听的摇篮曲",
},
{
question: "购买决策时哪些因素会打动您",
answer: "安全实用高性价比材质必须安全无害因为宝宝可能会碰到功能要真正解决我的痛点而不是花里胡哨",
},
],
},
{
code: "U03",
basicAttributes: "王悦22女性广州设计专业大学生月生活费约3000元",
content: [
{
question: "您关注睡眠健康吗",
answer:
"关注我们寝室经常聊朋克养生就是一边熬夜做设计作业一边泡枸杞我知道睡眠很重要但有时候是功课多有时候就是单纯不想睡刷短视频停不下来",
},
{
question: "您的睡前仪式通常包括什么",
answer:
"我很喜欢营造氛围感会在宿舍点香薰蜡烛但不太安全也喜欢用手机放ASMR或者助眠视频但同样需要开着屏幕我觉得我的睡眠环境需要更有仪式感科技感",
},
{
question: "一款什么样的助眠灯会吸引您购买",
answer:
"颜值是第一位的设计必须好看有质感能融入我的宿舍装饰风格灯光效果要多能切换不同颜色和模式满足我学习休息自拍打光等不同场景可以没有深入的睡眠监测但一定要能语音控制和我互动比如问问天气设定闹钟最好还有一些有趣的小众的白噪音比如雨声篝火声",
},
{
question: "在消费这类产品时您是怎样的心理",
answer:
"我愿意为情绪价值社交价值买单它不仅要好用还要能成为我的一个社交货币就是可以拍照分享到小红书的那种价格不能太贵四五百元我觉得是上限了但如果它足够好看好玩我会非常心动",
},
],
},
];
const currentInterviewTranscript = computed(() => interviewTranscripts.find((item) => item.code === form.value.keyUserRecords[activeInterviewIndex.value]?.code) || interviewTranscripts[0]);
function createKeyUserRecords() {
return [
{
title: "访谈记录一",
code: "U01",
user: "U01",
basicAttributes: "29男性职业为程序员月收入2.8万元居住于一线城市上海",
tags: "高压力职场人高压力高收入",
behavior: "1.工作侵占生活作息不规律\n2.积极寻求科技解决方案\n3.碎片化与无缝衔接的需求\n4.数据驱动的自我量化倾向",
painPoints: "1.精神紧张与生理不适2.现有解决方案有缺陷药物担心依赖性软件App体验被干扰设备手机本身与睡眠环境冲突\n3.智能化有高阶需求\n4.对专业性与有效性的渴求",
psychology: "1.功效优先的理性决策者\n2.效率便捷付费的意愿强烈3.科技感品牌调性是重要加分项\n4.投资健康的长期主义者",
},
{
title: "访谈记录二",
code: "U02",
user: "",
basicAttributes: "",
tags: "",
behavior: "",
painPoints: "",
psychology: "",
},
{
title: "访谈记录三",
code: "U03",
user: "",
basicAttributes: "",
tags: "",
behavior: "",
painPoints: "",
psychology: "",
},
];
}
function createSegmentationRows() {
return ["新锐白领", "精致妈妈", "都市银发", "继续补充", "继续补充"].map((market) => ({
market,
customerAnalysis: "",
productOpportunity: "",
}));
}
function createForm() {
return {
profileFileName: "",
profileFileUrl: "",
profileAnalysis: "",
keyUserProfile: "",
keyUserRecords: createKeyUserRecords(),
segmentationStrategy: "",
segmentationRows: createSegmentationRows(),
coreFunctions: ["", "", ""],
priceMin: "",
priceMax: "",
conclusion: "",
};
}
const form = ref(createForm());
const taskTitle = computed(() => taskConfig.value?.taskName || "目标用户画像");
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultGoals));
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
const flowSteps = computed(() => parseArray(taskConfig.value?.steps, []));
const steps = computed(() =>
flowSteps.value.map((name, index) => ({
no: index + 1,
name,
}))
);
const activeStepName = computed(() => steps.value.find((step) => step.no === currentStep.value)?.name || "");
const progressItems = computed(() => [
...steps.value.map((step) => ({
text: `${currentStep.value > step.no ? "已完成" : currentStep.value === step.no ? "进行中" : "待完成"}${step.name}`,
status: currentStep.value > step.no ? "done" : currentStep.value === step.no ? "doing" : "",
})),
]);
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) {
// 保持页面可填写,不因回显失败阻断使用。
}
}
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(/[;|]/)
.map((item) => item.trim())
.filter(Boolean);
return list.length ? list : fallback;
}
}
function normalizeKeyUserRecords(records, legacyText = "") {
const defaults = createKeyUserRecords();
if (Array.isArray(records)) {
return defaults.map((item, index) => ({
...item,
...(records[index] || {}),
}));
}
if (legacyText) {
defaults[1].behavior = legacyText;
}
return defaults;
}
function normalizeSegmentationRows(rows, legacyText = "") {
const defaults = createSegmentationRows();
if (Array.isArray(rows)) {
return defaults.map((item, index) => ({
...item,
...(rows[index] || {}),
}));
}
if (legacyText) {
defaults[0].customerAnalysis = legacyText;
}
return defaults;
}
function applySavedAnswer(value) {
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object") return;
form.value = {
...createForm(),
profileFileName: parsed.profileFileName || parsed.file?.name || "",
profileFileUrl: parsed.profileFileUrl || parsed.file?.url || "",
profileAnalysis: parsed.profileAnalysis || "",
keyUserProfile: parsed.keyUserProfile || "",
keyUserRecords: normalizeKeyUserRecords(parsed.keyUserRecords, parsed.keyUserProfile),
segmentationStrategy: parsed.segmentationStrategy || "",
segmentationRows: normalizeSegmentationRows(parsed.segmentationRows, parsed.segmentationStrategy),
coreFunctions: Array.isArray(parsed.coreFunctions) ? ["", "", ""].map((item, index) => parsed.coreFunctions[index] || item) : ["", "", ""],
priceMin: parsed.priceMin || "",
priceMax: parsed.priceMax || "",
conclusion: parsed.conclusion || "",
};
currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), 4);
} catch (error) {
// 忽略历史异常数据。
}
}
function triggerFileSelect() {
fileInputRef.value?.click();
}
function handleInterviewClick(index) {
activeInterviewIndex.value = index;
const record = form.value.keyUserRecords[index];
if (interviewTranscripts.some((item) => item.code === record?.code)) {
interviewDialogVisible.value = true;
}
}
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);
form.value.profileFileName = file.name;
form.value.profileFileUrl = res?.url || res?.fileName || res?.data?.url || res?.data?.fileName || "";
proxy?.$modal?.msgSuccess("上传成功");
} finally {
uploading.value = false;
event.target.value = "";
}
}
function buildAnswerPayload(status) {
const saveAction = status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE";
return {
saveAction,
currentStep: currentStep.value,
step1Answer: JSON.stringify({
taskKey: TASK_KEY,
taskName: taskTitle.value,
flowSteps: flowSteps.value,
currentStep: currentStep.value,
profileFileName: form.value.profileFileName,
profileFileUrl: form.value.profileFileUrl,
file: {
name: form.value.profileFileName,
url: form.value.profileFileUrl,
},
profileAnalysis: form.value.profileAnalysis,
keyUserProfile: form.value.keyUserProfile,
keyUserRecords: form.value.keyUserRecords,
segmentationStrategy: form.value.segmentationStrategy,
segmentationRows: form.value.segmentationRows,
coreFunctions: form.value.coreFunctions,
priceMin: form.value.priceMin,
priceMax: form.value.priceMax,
conclusion: form.value.conclusion,
}),
};
}
async function persist(status = "IN_PROGRESS") {
saving.value = true;
try {
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();
currentStep.value = 1;
await persist("RESET");
}
function exportOutcome() {
const keyUserRecordsText = form.value.keyUserRecords
.map(
(record) =>
`${record.title}${record.code}\n用户${record.user || ""}\n基础属性${record.basicAttributes || ""}\n标签${record.tags || ""}\n行为特征\n${record.behavior || ""}\n需求痛点\n${record.painPoints || ""}\n消费心理\n${record.psychology || ""}`,
)
.join("\n\n");
const segmentationText = form.value.segmentationRows
.map((row) => `${row.market || ""}\n客户分析\n${row.customerAnalysis || ""}\n产品机会\n${row.productOpportunity || ""}`)
.join("\n\n");
const coreFunctionsText = form.value.coreFunctions.map((item, index) => `核心功能${index + 1}${item || ""}`).join("\n");
const content = `目标用户画像\n\n任务流程${flowSteps.value.join(" > ")}\n\n上传文件${form.value.profileFileName || "未上传"}\n文件地址${form.value.profileFileUrl || ""}\n\n客户群画像分析\n${form.value.profileAnalysis || ""}\n\n重点用户画像\n${keyUserRecordsText}\n\n用户细分与市场选择\n${segmentationText}\n\n计划设计的核心功能\n${coreFunctionsText}\n\n预计产品价格区间${form.value.priceMin || ""} - ${form.value.priceMax || ""}\n\n分析结论\n${form.value.conclusion || ""}`;
const blob = new Blob([content], { 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);
}
</script>
<style lang="scss" scoped>
.target-profile-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 {
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: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
color: #b9e2ff;
font-weight: 800;
}
.subtitle-icon--spaced {
margin-top: 12px;
}
.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);
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;
}
.section-eyebrow {
margin: 0;
color: #71dfff;
font-size: 12px;
font-weight: 800;
line-height: 1.4;
}
.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:not(.is-disabled) {
transform: translateY(-1px);
border-color: rgba(116, 235, 255, 0.85);
background: rgba(18, 146, 204, 0.5);
}
}
.step-card {
margin-bottom: 0;
}
.step-indicator {
position: relative;
display: flex;
align-items: center;
gap: 0;
width: calc(100% + 24px);
margin: 0 -12px 32px;
overflow-x: auto;
padding: 18px 22px;
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;
}
}
&.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-title-row {
display: flex;
justify-content: space-between;
gap: 18px;
margin-bottom: 14px;
h3 {
margin: 4px 0 0;
color: #ffffff;
font-size: 22px;
font-weight: 900;
line-height: 1.3;
}
}
.task-desc {
margin: 0 0 38px;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.task-desc--compact {
margin-bottom: 26px;
}
.interview-tabs {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 22px;
}
.interview-tab {
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;
transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease;
&.active {
border-color: rgba(0, 170, 255, 0.55);
color: #ffffff;
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
.interview-dialog-layer {
position: fixed;
inset: 0;
z-index: 2100;
display: flex;
align-items: center;
justify-content: center;
padding: 44px;
background: rgba(0, 0, 0, 0.52);
}
.interview-dialog {
position: relative;
width: min(880px, calc(100vw - 40px));
max-height: min(720px, 78vh);
overflow: hidden;
border: 1px solid rgba(82, 174, 226, 0.42);
border-radius: 26px;
background: rgba(0, 25, 42, 0.96);
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(169, 229, 255, 0.08);
}
.interview-dialog-close {
position: absolute;
top: 18px;
right: 18px;
min-width: 0;
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;
}
.interview-dialog-body {
max-height: min(720px, 78vh);
overflow-y: auto;
padding: 24px 96px 24px 24px;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.interview-line,
.interview-title,
.interview-qa p {
margin: 0;
}
.interview-title {
margin-top: 18px;
color: #ffffff;
font-weight: 800;
}
.interview-qa {
margin-top: 12px;
}
.portrait-table-wrap {
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.portrait-record {
min-width: 780px;
& + & {
border-top: 1px solid rgba(77, 167, 220, 0.2);
}
}
.portrait-user {
min-height: 110px;
padding: 12px;
border-bottom: 1px solid rgba(77, 167, 220, 0.2);
label,
.portrait-example {
display: flex;
align-items: center;
min-height: 38px;
margin: 0;
padding: 0;
color: #d7ecff;
font-size: 15px;
line-height: 1.6;
}
span {
flex: 0 0 auto;
color: #d7ecff;
}
}
.portrait-input {
flex: 1;
min-width: 120px;
height: 38px;
padding: 0 10px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.portrait-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.portrait-head {
display: flex;
align-items: center;
justify-content: center;
min-height: 52px;
padding: 12px;
border-bottom: 1px solid rgba(77, 167, 220, 0.2);
color: #eaffff;
background: rgba(8, 55, 82, 0.98);
font-size: 14px;
font-weight: 900;
& + & {
border-left: 1px solid rgba(77, 167, 220, 0.2);
}
}
.portrait-cell {
min-height: 190px;
padding: 12px;
border: 0;
border-left: 1px solid rgba(77, 167, 220, 0.2);
outline: none;
resize: vertical;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
line-height: 1.7;
&:nth-of-type(1) {
border-left: 0;
}
&:focus {
background: rgba(10, 31, 46, 0.98);
box-shadow: inset 0 0 0 1px #5fe7ff, 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.segmentation-intro {
margin-bottom: 24px;
color: #d7ecff;
font-size: 15px;
line-height: 1.7;
p {
margin: 0;
}
}
.segmentation-table-wrap {
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
}
.segmentation-table {
width: 100%;
min-width: 840px;
border-collapse: collapse;
table-layout: fixed;
th,
td {
border: 1px solid rgba(77, 167, 220, 0.2);
}
th {
height: 52px;
padding: 12px;
color: #eaffff;
background: rgba(8, 55, 82, 0.98);
font-size: 14px;
font-weight: 900;
text-align: center;
}
th:first-child,
td:first-child {
width: 190px;
}
td {
height: 160px;
padding: 12px;
color: #d7f1ff;
background: rgba(1, 14, 26, 0.48);
}
}
.segmentation-market-input,
.segmentation-cell-input {
width: 100%;
min-height: 38px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.segmentation-market-input {
text-align: center;
font-weight: 800;
}
.segmentation-cell-input {
min-height: 160px;
padding: 12px 14px;
resize: vertical;
line-height: 1.7;
}
.product-plan-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);
gap: 34px;
align-items: start;
}
.product-plan-desc {
margin: 0 0 26px;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.product-plan-label {
margin-bottom: 10px;
color: #ffffff;
font-size: 15px;
font-weight: 800;
}
.product-plan-label--price {
margin-top: 38px;
}
.core-function-input {
display: block;
width: 100%;
min-height: 110px;
margin-bottom: 28px;
padding: 12px 14px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
line-height: 1.7;
&::placeholder {
color: #91aabc;
}
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.price-range-row {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 34px;
span {
width: 34px;
height: 1px;
background: rgba(215, 236, 255, 0.72);
}
}
.price-input {
width: 132px;
height: 38px;
padding: 0 10px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
}
}
.analysis-conclusion-block {
display: block;
span {
display: block;
margin-bottom: 10px;
color: #ffffff;
font-size: 15px;
font-weight: 800;
}
}
.analysis-conclusion-input {
width: 100%;
min-height: 210px;
padding: 14px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #eef5ff;
background: #0f1a24;
font-family: inherit;
font-size: 15px;
line-height: 1.7;
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.product-reference {
display: flex;
justify-content: center;
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 22px;
background: rgba(1, 14, 26, 0.72);
img {
width: min(100%, 320px);
height: auto;
border-radius: 14px;
box-shadow: none;
}
}
.upload-panel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
gap: 14px;
margin: 22px 0;
}
.file-input {
display: none;
}
.upload-action {
min-width: 0;
height: 40px;
padding: 0 18px;
border: 1px solid rgba(92, 224, 255, 0.7);
border-radius: 999px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
box-shadow: none;
font-size: 14px;
font-weight: 800;
}
.file-chip {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: min(520px, 100%);
padding: 8px 14px;
border: 1px solid rgba(93, 213, 255, 0.42);
border-radius: 999px;
color: #9fe8ff;
background: rgba(1, 14, 26, 0.72);
font-size: 13px;
font-weight: 700;
span {
overflow-wrap: anywhere;
}
}
.field-block {
display: block;
span {
display: block;
margin-bottom: 10px;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
}
.profile-textarea {
width: 100%;
min-height: 110px;
padding: 12px 14px;
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
outline: none;
resize: vertical;
color: #eef5ff;
background: #0f1a24;
font-size: 15px;
line-height: 1.7;
transition: border-color 0.16s ease, box-shadow 0.16s ease, background 0.16s ease;
&::placeholder {
color: #8fa8b8;
opacity: 0.9;
}
&:focus {
border-color: #5fe7ff;
background: rgba(10, 31, 46, 0.98);
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.step-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
margin-top: 22px;
margin-bottom: 36px;
}
.step-actions :deep(.step-action) {
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;
}
.step-actions :deep(.step-action--save),
.step-actions :deep(.step-action--next) {
border-color: rgba(92, 224, 255, 0.7);
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
}
.step-actions :deep(.step-action.is-disabled) {
opacity: 0.45;
}
.training-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 52px;
margin-top: 36px;
}
.training-actions :deep(.training-action) {
min-width: 172px;
height: 64px;
padding: 0 28px;
border: 1px solid #2c83aa;
border-radius: 6px;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
box-shadow: none;
font-size: 24px;
font-weight: 900;
line-height: 1;
letter-spacing: 0;
text-shadow: none;
}
.training-actions :deep(.training-action span) {
display: inline-flex;
align-items: center;
color: inherit;
font-size: inherit;
line-height: 1;
}
.training-actions :deep(.training-action--submit),
.training-actions :deep(.training-action--reset),
.training-actions :deep(.training-action--export) {
border-color: #2c83aa;
color: #dff7ff;
background: rgba(13, 64, 88, 0.62);
}
.training-actions :deep(.training-action:hover:not(.is-disabled)) {
border-color: #46b5e7;
color: #ffffff;
background: rgba(18, 85, 116, 0.78);
}
@media (max-width: 1100px) {
.target-profile-page {
grid-template-columns: 1fr;
}
.product-plan-layout {
grid-template-columns: 1fr;
}
.product-reference {
justify-content: flex-start;
}
}
@media (max-width: 900px) {
.target-profile-page {
padding: 14px 14px 18px;
}
.training-core {
padding: 18px;
border-radius: 24px;
}
.workbench-head,
.workbench-actions,
.upload-panel,
.step-title-row,
.step-actions,
.training-actions {
flex-direction: column;
align-items: stretch;
}
.workbench-actions :deep(.material-download),
.workbench-actions .outline-action,
.upload-action,
.step-actions :deep(.step-action),
.training-actions :deep(.training-action) {
width: 100%;
}
.step-indicator {
align-items: stretch;
flex-direction: column;
width: 100%;
margin-right: 0;
margin-left: 0;
overflow: visible;
border-radius: 20px;
}
.step-tab {
flex: 0 0 auto;
width: 100%;
}
.step-connector {
flex: 0 0 22px;
width: 2px;
height: 22px;
margin-left: 29px;
text-align: center;
writing-mode: vertical-rl;
}
.product-plan-layout {
gap: 20px;
}
}
@media (max-width: 760px) {
.interview-tabs {
flex-direction: column;
gap: 12px;
}
.interview-tab {
width: 100%;
}
.interview-dialog-layer {
padding: 20px;
}
.interview-dialog {
width: calc(100vw - 40px);
}
.interview-dialog-close {
top: 16px;
right: 16px;
min-width: 78px;
height: 40px;
font-size: 15px;
}
.interview-dialog-body {
padding: 78px 20px 28px;
font-size: 17px;
}
}
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>