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>