|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="product-positioning-page student-training-shell">
|
|
|
|
|
|
<main class="training-core">
|
|
|
|
|
|
<section class="task-header">
|
|
|
|
|
|
<div class="task-badge">
|
|
|
|
|
|
<el-icon><Operation /></el-icon>
|
|
|
|
|
|
市场洞察与需求分析
|
|
|
|
|
|
</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">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="section-eyebrow">Training Task</p>
|
|
|
|
|
|
<h3>任务实训</h3>
|
|
|
|
|
|
<p v-if="taskRequirement" class="workbench-description">{{ taskRequirement }}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<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="configuredSteps.length" class="step-indicator" aria-label="产品价值主张及定位步骤">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="(step, index) in configuredSteps"
|
|
|
|
|
|
:key="`${step}-${index}`"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="step-tab"
|
|
|
|
|
|
:class="{ active: form.activeStep === index + 1 }"
|
|
|
|
|
|
:aria-current="form.activeStep === index + 1 ? 'step' : undefined"
|
|
|
|
|
|
@click="setCurrentStep(index + 1)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ step }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(1)" class="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>
|
|
|
|
|
|
<div class="step-content">
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>收集并分析当前智能健身镜市场的主要品牌、产品特点、用户评价:</span>
|
|
|
|
|
|
<textarea v-model="form.research.marketAnalysis" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>开展初步访谈或问卷调研,梳理潜在用户对智能健身设备的需求与痛点:</span>
|
|
|
|
|
|
<textarea v-model="form.research.userResearch" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>整理用户画像,明确目标用户群体特征:</span>
|
|
|
|
|
|
<textarea v-model="form.research.persona" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(2)" class="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>
|
|
|
|
|
|
<div class="step-content">
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>目标用户群体:</span>
|
|
|
|
|
|
<textarea v-model="form.positionCanvas.targetUsers" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>用户核心需求与痛点:</span>
|
|
|
|
|
|
<textarea v-model="form.positionCanvas.needsAndPains" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>竞品定位与差异化机会:</span>
|
|
|
|
|
|
<textarea v-model="form.positionCanvas.competitorOpportunity" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(3)" class="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>
|
|
|
|
|
|
<div class="step-content value-grid">
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>客户任务:</span>
|
|
|
|
|
|
<textarea v-model="form.valueCanvas.customerJobs" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>用户痛点:</span>
|
|
|
|
|
|
<textarea v-model="form.valueCanvas.pains" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>用户收益:</span>
|
|
|
|
|
|
<textarea v-model="form.valueCanvas.gains" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>产品与服务:</span>
|
|
|
|
|
|
<textarea v-model="form.valueCanvas.products" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>痛点缓解方案:</span>
|
|
|
|
|
|
<textarea v-model="form.valueCanvas.painRelievers" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>收益创造方案:</span>
|
|
|
|
|
|
<textarea v-model="form.valueCanvas.gainCreators" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(4)" class="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>
|
|
|
|
|
|
<div class="step-content">
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>目标用户:</span>
|
|
|
|
|
|
<textarea v-model="form.statement.targetUsers" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>核心价值主张:</span>
|
|
|
|
|
|
<textarea v-model="form.statement.valueProposition" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>差异化优势:</span>
|
|
|
|
|
|
<textarea v-model="form.statement.differentiation" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label class="field-block">
|
|
|
|
|
|
<span>产品定位陈述:</span>
|
|
|
|
|
|
<textarea v-model="form.statement.positioning" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="hasConfiguredSteps && form.activeStep > 4" class="step-card completion-card">
|
|
|
|
|
|
<div class="step-title-row">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p class="section-eyebrow">Step {{ String(form.activeStep).padStart(2, '0') }}</p>
|
|
|
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="step-desc">请检查已填写的调研、定位画布、价值主张和定位陈述,再保存或提交实训成果。</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="hasConfiguredSteps" class="step-navigation">
|
|
|
|
|
|
<button type="button" class="btn-nav" :disabled="form.activeStep === 1" @click="goPreviousStep">
|
|
|
|
|
|
<el-icon><ArrowLeft /></el-icon>
|
|
|
|
|
|
上一步
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress">
|
|
|
|
|
|
<el-icon><CircleCheck /></el-icon>
|
|
|
|
|
|
保存当前进度
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="btn-nav btn-primary"
|
|
|
|
|
|
:disabled="form.activeStep === configuredSteps.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>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<TrainingAiSidebar
|
|
|
|
|
|
study-tip="当前任务建议:先明确目标人群和核心痛点,再把定位画布、价值主张画布和定位陈述串成一条完整逻辑。"
|
|
|
|
|
|
reference-title="定位分析参考"
|
|
|
|
|
|
reference-text="· 市场调研:竞品、用户评价、需求痛点<br />· 定位画布:用户、场景、差异化机会<br />· 价值主张:客户任务、痛点、收益与解决方案<br />· 定位陈述:目标用户 + 核心价值 + 差异化理由"
|
|
|
|
|
|
:progress-items="progressItems"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
|
|
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, Operation } 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 } from "@/api/studentTrainingAnswer";
|
|
|
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
|
|
|
|
|
|
|
|
const TASK_KEY = "product-value-positioning";
|
|
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const defaultRequirement =
|
|
|
|
|
|
"产品价值主张及定位需要在市场调研和用户洞察基础上,明确目标用户、核心价值、差异化优势和定位表达。";
|
|
|
|
|
|
const defaultBackground =
|
|
|
|
|
|
"某智能家居公司计划推出一款面向家庭用户的 AI 智能健身镜,希望通过该产品进入家庭健身市场。作为产品经理,需要基于市场调研与用户洞察完成系统化定位。";
|
|
|
|
|
|
const defaultGoals = [
|
|
|
|
|
|
"理解产品定位的概念、作用与核心要素。",
|
|
|
|
|
|
"掌握定位画布与价值主张画布的系统化工具。",
|
|
|
|
|
|
"运用产品定位方法完成一次 AI 智能健身镜的定位设计。",
|
|
|
|
|
|
"撰写清晰、有力的产品定位陈述。",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function createForm() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
activeStep: 1,
|
|
|
|
|
|
research: { marketAnalysis: "", userResearch: "", persona: "" },
|
|
|
|
|
|
positionCanvas: { targetUsers: "", needsAndPains: "", competitorOpportunity: "" },
|
|
|
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
|
statement: { targetUsers: "", valueProposition: "", differentiation: "", positioning: "" },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const form = ref(createForm());
|
|
|
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "产品价值主张及定位");
|
|
|
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || defaultBackground);
|
|
|
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || defaultRequirement);
|
|
|
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, defaultGoals));
|
|
|
|
|
|
const configuredSteps = computed(() => parseTrainingArray(taskConfig.value?.steps));
|
|
|
|
|
|
const hasConfiguredSteps = computed(() => configuredSteps.value.length > 0);
|
|
|
|
|
|
const activeStepName = computed(() => configuredSteps.value[form.value.activeStep - 1] || "");
|
|
|
|
|
|
const progressItems = computed(() =>
|
|
|
|
|
|
configuredSteps.value.map((step, index) => ({
|
|
|
|
|
|
text: step,
|
|
|
|
|
|
status: index + 1 < form.value.activeStep ? "done" : index + 1 === form.value.activeStep ? "doing" : "",
|
|
|
|
|
|
}))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadTaskConfig();
|
|
|
|
|
|
await loadSavedAnswer();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function loadTaskConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getTrainingTaskByKey(TASK_KEY);
|
|
|
|
|
|
taskConfig.value = res?.data || null;
|
|
|
|
|
|
form.value.activeStep = normalizeActiveStep(form.value.activeStep);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
taskConfig.value = null;
|
|
|
|
|
|
form.value.activeStep = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSavedAnswer() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getStudentTrainingAnswer(TASK_KEY);
|
|
|
|
|
|
if (res?.data?.step1Answer) applySavedAnswer(res.data.step1Answer);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Keep the local form when no historical answer is available.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeActiveStep(value) {
|
|
|
|
|
|
if (!configuredSteps.value.length) return 1;
|
|
|
|
|
|
const step = Number(value) || 1;
|
|
|
|
|
|
return Math.min(Math.max(step, 1), configuredSteps.value.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setCurrentStep(step) {
|
|
|
|
|
|
form.value.activeStep = normalizeActiveStep(step);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSectionVisible(section) {
|
|
|
|
|
|
return !hasConfiguredSteps.value || form.value.activeStep === section;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goPreviousStep() {
|
|
|
|
|
|
setCurrentStep(form.value.activeStep - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goNextStep() {
|
|
|
|
|
|
setCurrentStep(form.value.activeStep + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applySavedAnswer(value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(value);
|
|
|
|
|
|
if (!parsed || typeof parsed !== "object") return;
|
|
|
|
|
|
form.value = normalizeForm(parsed);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Ignore malformed historical answers.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeForm(value = {}) {
|
|
|
|
|
|
const base = createForm();
|
|
|
|
|
|
return {
|
|
|
|
|
|
...base,
|
|
|
|
|
|
...value,
|
|
|
|
|
|
activeStep: normalizeActiveStep(value.activeStep),
|
|
|
|
|
|
research: { ...base.research, ...(value.research || {}) },
|
|
|
|
|
|
positionCanvas: { ...base.positionCanvas, ...(value.positionCanvas || {}) },
|
|
|
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
|
|
|
statement: { ...base.statement, ...(value.statement || {}) },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildOutcome() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
taskKey: TASK_KEY,
|
|
|
|
|
|
taskId: taskConfig.value?.id || "",
|
|
|
|
|
|
taskName: taskTitle.value,
|
|
|
|
|
|
currentStep: form.value.activeStep,
|
|
|
|
|
|
configuredSteps: configuredSteps.value,
|
|
|
|
|
|
research: form.value.research,
|
|
|
|
|
|
positionCanvas: form.value.positionCanvas,
|
|
|
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
|
|
|
statement: form.value.statement,
|
|
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildAnswerPayload(status) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
saveAction: status === "SUBMITTED" ? "SUBMIT" : status === "RESET" ? "RESET" : "SAVE",
|
|
|
|
|
|
currentStep: form.value.activeStep,
|
|
|
|
|
|
step1Answer: JSON.stringify(buildOutcome()),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function persist(status = "IN_PROGRESS") {
|
|
|
|
|
|
if (saving.value) return;
|
|
|
|
|
|
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();
|
|
|
|
|
|
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">
|
|
|
|
|
|
.product-positioning-page {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(720px, 1fr) 340px;
|
|
|
|
|
|
gap: 24px;
|
|
|
|
|
|
min-height: calc(100vh - 76px);
|
|
|
|
|
|
padding: 24px 24px 32px;
|
|
|
|
|
|
color: #eef5ff;
|
|
|
|
|
|
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: inset 0 1px 0 rgba(169, 229, 255, 0.08), 0 22px 48px rgba(0, 0, 0, 0.22);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-header { margin-bottom: 24px; }
|
|
|
|
|
|
|
|
|
|
|
|
.task-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
min-height: 28px;
|
|
|
|
|
|
padding: 4px 14px;
|
|
|
|
|
|
border: 1px solid rgba(99, 217, 255, 0.55);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
color: #dff5ff;
|
|
|
|
|
|
background: rgba(17, 126, 178, 0.36);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
|
|
|
margin: 12px 0 0;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-size: 30px;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-card,
|
|
|
|
|
|
.step-card {
|
|
|
|
|
|
border: 1px solid rgba(82, 174, 226, 0.42);
|
|
|
|
|
|
border-radius: 26px;
|
|
|
|
|
|
background: rgba(0, 25, 42, 0.64);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-card {
|
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
|
padding: 18px 22px 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.workbench-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
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;
|
|
|
|
|
|
letter-spacing: 0.02em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.workbench-head h3 {
|
|
|
|
|
|
margin: 4px 0 8px;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-size: 26px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.workbench-description,
|
|
|
|
|
|
.step-desc {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: #d7ecff;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.workbench-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.outline-action,
|
|
|
|
|
|
.btn-nav {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
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: inherit;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.outline-action:hover,
|
|
|
|
|
|
.outline-action:focus-visible,
|
|
|
|
|
|
.btn-nav:hover:not(:disabled),
|
|
|
|
|
|
.btn-nav:focus-visible {
|
|
|
|
|
|
border-color: rgba(116, 235, 255, 0.85);
|
|
|
|
|
|
background: rgba(18, 146, 204, 0.5);
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-indicator {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
min-height: 58px;
|
|
|
|
|
|
margin: 0 0 32px;
|
|
|
|
|
|
padding: 6px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
border: 1px solid rgba(0, 180, 255, 0.3);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: rgba(0, 20, 35, 0.65);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-tab {
|
|
|
|
|
|
flex: 1 0 max-content;
|
|
|
|
|
|
min-height: 46px;
|
|
|
|
|
|
padding: 0 18px;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
color: #9ab3d0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-tab:hover,
|
|
|
|
|
|
.step-tab:focus-visible {
|
|
|
|
|
|
border-color: rgba(99, 217, 255, 0.55);
|
|
|
|
|
|
color: #e9fbff;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-tab.active {
|
|
|
|
|
|
border-color: rgba(92, 224, 255, 0.7);
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
background: linear-gradient(95deg, #0a6b9e, #0a5c86);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-card {
|
|
|
|
|
|
margin-top: 32px;
|
|
|
|
|
|
padding: 18px 22px 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-indicator + .step-card { margin-top: 0; }
|
|
|
|
|
|
.workbench-head + .step-card { margin-top: 0; }
|
|
|
|
|
|
.step-card + .step-card { margin-top: 16px; }
|
|
|
|
|
|
|
|
|
|
|
|
.step-title-row { margin-bottom: 14px; }
|
|
|
|
|
|
.step-title-row h3 {
|
|
|
|
|
|
margin: 4px 0 0;
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-content { color: #d7ecff; }
|
|
|
|
|
|
|
|
|
|
|
|
.field-block {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-block:last-child { margin-bottom: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
.field-block span {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
color: #dff5ff;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-block textarea {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 110px;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
border: 1px solid rgba(45, 95, 126, 0.95);
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
color: #eef5ff;
|
|
|
|
|
|
background: #0f1a24;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-block textarea:focus {
|
|
|
|
|
|
border-color: rgba(92, 224, 255, 0.7);
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.13);
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.value-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.value-grid .field-block { margin: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
.step-navigation {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
margin-top: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-nav.btn-save,
|
|
|
|
|
|
.btn-nav.btn-primary {
|
|
|
|
|
|
border-color: rgba(92, 224, 255, 0.7);
|
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
background: linear-gradient(95deg, #0b84bd, #096491);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-nav:disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
|
transform: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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);
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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) {
|
|
|
|
|
|
.product-positioning-page { grid-template-columns: 1fr; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.product-positioning-page { padding: 14px; }
|
|
|
|
|
|
.training-core { padding: 18px; border-radius: 24px; }
|
|
|
|
|
|
.workbench-head,
|
|
|
|
|
|
.workbench-actions,
|
|
|
|
|
|
.step-navigation { flex-direction: column; align-items: stretch; }
|
|
|
|
|
|
.workbench-actions,
|
|
|
|
|
|
.outline-action,
|
|
|
|
|
|
.btn-nav,
|
|
|
|
|
|
.training-actions :deep(.training-action) { width: 100%; }
|
|
|
|
|
|
.step-indicator { flex-direction: column; border-radius: 26px; }
|
|
|
|
|
|
.step-tab { flex: none; }
|
|
|
|
|
|
.value-grid { grid-template-columns: 1fr; }
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|