|
|
|
|
@ -48,20 +48,31 @@
|
|
|
|
|
<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 class="core-pain-intro">
|
|
|
|
|
<p>价值主张的起点不是产品功能,而是用户痛点。不要试图满足所有人,只抓占 80% 的核心用户最痛的 3 个问题。</p>
|
|
|
|
|
<p>参考实训 11 的需求优先级评估结果,整理出 Top3 核心痛点,同时识别弱需求和伪需求——它们不作为核心价值主张,只作为加分项。</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="core-pain-table-wrap">
|
|
|
|
|
<table class="core-pain-table">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>序号</th>
|
|
|
|
|
<th>需求类型</th>
|
|
|
|
|
<th>需求描述</th>
|
|
|
|
|
<th>场景描述</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="(row, index) in form.corePainRows" :key="row.id">
|
|
|
|
|
<td>{{ index + 1 }}</td>
|
|
|
|
|
<td class="pain-type-cell">{{ row.type }}</td>
|
|
|
|
|
<td><textarea v-model="row.description" :placeholder="index === 0 ? '示例:入睡困难' : '填写需求描述'" :aria-label="`${row.type}需求描述`" /></td>
|
|
|
|
|
<td><textarea v-model="row.scene" :placeholder="index === 0 ? '示例:高压职场人群睡前大脑兴奋难以切换到放松状态,传统吸顶灯只有冷白光切换,缺乏渐进式助眠的光环境引导,导致入睡时间平均超过40分钟' : '填写场景描述'" :aria-label="`${row.type}场景描述`" /></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-if="corePainValidationMessage" class="core-pain-validation-message" role="status">{{ corePainValidationMessage }}</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(2)" class="step-card">
|
|
|
|
|
@ -202,14 +213,18 @@ 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 { checkProductValuePositioningStepOne, 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 checkingCorePain = ref(false);
|
|
|
|
|
const corePainValidationMessage = ref("");
|
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
|
|
|
|
|
|
const corePainTypes = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
|
|
|
|
|
|
|
|
|
|
const defaultRequirement =
|
|
|
|
|
"产品价值主张及定位需要在市场调研和用户洞察基础上,明确目标用户、核心价值、差异化优势和定位表达。";
|
|
|
|
|
const defaultBackground =
|
|
|
|
|
@ -224,6 +239,7 @@ const defaultGoals = [
|
|
|
|
|
function createForm() {
|
|
|
|
|
return {
|
|
|
|
|
activeStep: 1,
|
|
|
|
|
corePainRows: corePainTypes.map((type, index) => ({ id: index + 1, type, description: "", scene: "" })),
|
|
|
|
|
research: { marketAnalysis: "", userResearch: "", persona: "" },
|
|
|
|
|
positionCanvas: { targetUsers: "", needsAndPains: "", competitorOpportunity: "" },
|
|
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
@ -277,20 +293,22 @@ function normalizeActiveStep(value) {
|
|
|
|
|
return Math.min(Math.max(step, 1), configuredSteps.value.length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setCurrentStep(step) {
|
|
|
|
|
form.value.activeStep = normalizeActiveStep(step);
|
|
|
|
|
async function setCurrentStep(step) {
|
|
|
|
|
const targetStep = normalizeActiveStep(step);
|
|
|
|
|
if (targetStep > form.value.activeStep && form.value.activeStep === 1 && !(await ensureCorePainValid())) return;
|
|
|
|
|
form.value.activeStep = targetStep;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSectionVisible(section) {
|
|
|
|
|
return !hasConfiguredSteps.value || form.value.activeStep === section;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goPreviousStep() {
|
|
|
|
|
setCurrentStep(form.value.activeStep - 1);
|
|
|
|
|
async function goPreviousStep() {
|
|
|
|
|
await setCurrentStep(form.value.activeStep - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goNextStep() {
|
|
|
|
|
setCurrentStep(form.value.activeStep + 1);
|
|
|
|
|
async function goNextStep() {
|
|
|
|
|
await setCurrentStep(form.value.activeStep + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applySavedAnswer(value) {
|
|
|
|
|
@ -309,6 +327,7 @@ function normalizeForm(value = {}) {
|
|
|
|
|
...base,
|
|
|
|
|
...value,
|
|
|
|
|
activeStep: normalizeActiveStep(value.activeStep),
|
|
|
|
|
corePainRows: normalizeCorePainRows(value.corePainRows),
|
|
|
|
|
research: { ...base.research, ...(value.research || {}) },
|
|
|
|
|
positionCanvas: { ...base.positionCanvas, ...(value.positionCanvas || {}) },
|
|
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
|
|
@ -316,6 +335,32 @@ function normalizeForm(value = {}) {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeCorePainRows(rows = []) {
|
|
|
|
|
const savedRows = Array.isArray(rows) ? rows : [];
|
|
|
|
|
return corePainTypes.map((type, index) => ({
|
|
|
|
|
id: index + 1,
|
|
|
|
|
type,
|
|
|
|
|
description: String(savedRows[index]?.description || ""),
|
|
|
|
|
scene: String(savedRows[index]?.scene || ""),
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureCorePainValid() {
|
|
|
|
|
if (checkingCorePain.value) return false;
|
|
|
|
|
checkingCorePain.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await checkProductValuePositioningStepOne({ rows: form.value.corePainRows });
|
|
|
|
|
const result = response?.data || {};
|
|
|
|
|
corePainValidationMessage.value = result.message || (result.valid ? "核心痛点分类填写完整。" : "请完成 3 个核心痛点、弱需求和伪需求的描述与场景。");
|
|
|
|
|
return result.valid === true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
corePainValidationMessage.value = error?.message || "核心痛点校验失败,请稍后重试。";
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
|
|
|
|
checkingCorePain.value = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildOutcome() {
|
|
|
|
|
return {
|
|
|
|
|
taskKey: TASK_KEY,
|
|
|
|
|
@ -323,6 +368,7 @@ function buildOutcome() {
|
|
|
|
|
taskName: taskTitle.value,
|
|
|
|
|
currentStep: form.value.activeStep,
|
|
|
|
|
configuredSteps: configuredSteps.value,
|
|
|
|
|
corePainRows: form.value.corePainRows,
|
|
|
|
|
research: form.value.research,
|
|
|
|
|
positionCanvas: form.value.positionCanvas,
|
|
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
|
|
@ -341,10 +387,12 @@ function buildAnswerPayload(status) {
|
|
|
|
|
|
|
|
|
|
async function persist(status = "IN_PROGRESS") {
|
|
|
|
|
if (saving.value) return;
|
|
|
|
|
if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureCorePainValid())) return false;
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
|
|
|
|
|
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
|
|
|
|
|
return true;
|
|
|
|
|
} finally {
|
|
|
|
|
saving.value = false;
|
|
|
|
|
}
|
|
|
|
|
@ -723,6 +771,97 @@ function exportOutcome() {
|
|
|
|
|
|
|
|
|
|
.value-grid .field-block { margin: 0; }
|
|
|
|
|
|
|
|
|
|
.core-pain-intro {
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
padding: 16px 18px;
|
|
|
|
|
border-left: 3px solid #55ddff;
|
|
|
|
|
border-radius: 0 14px 14px 0;
|
|
|
|
|
background: linear-gradient(90deg, rgba(7, 92, 132, 0.28), rgba(5, 34, 51, 0.12));
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
margin: 0;
|
|
|
|
|
color: #cae7f2;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p + p { margin-top: 6px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.core-pain-table-wrap { overflow-x: auto; }
|
|
|
|
|
|
|
|
|
|
.core-pain-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 820px;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
table-layout: fixed;
|
|
|
|
|
|
|
|
|
|
th,
|
|
|
|
|
td {
|
|
|
|
|
border: 1px solid rgba(77, 167, 220, 0.28);
|
|
|
|
|
text-align: center;
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
th {
|
|
|
|
|
padding: 11px 10px;
|
|
|
|
|
color: #f5fcff;
|
|
|
|
|
background: linear-gradient(100deg, #0787bd, #075276);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
td {
|
|
|
|
|
height: 84px;
|
|
|
|
|
padding: 9px;
|
|
|
|
|
color: #d7f1ff;
|
|
|
|
|
background: rgba(1, 14, 26, 0.48);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
th:nth-child(1),
|
|
|
|
|
td:nth-child(1) { width: 7%; }
|
|
|
|
|
th:nth-child(2),
|
|
|
|
|
td:nth-child(2) { width: 15%; }
|
|
|
|
|
th:nth-child(3),
|
|
|
|
|
td:nth-child(3) { width: 32%; }
|
|
|
|
|
|
|
|
|
|
.pain-type-cell {
|
|
|
|
|
color: #b9e7f6;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
background: rgba(7, 62, 88, 0.46);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
textarea {
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 76px;
|
|
|
|
|
min-height: 52px;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
border: 1px solid rgba(45, 95, 126, 0.95);
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
outline: none;
|
|
|
|
|
color: #eef5ff;
|
|
|
|
|
background: rgba(4, 24, 38, 0.72);
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
font: inherit;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
|
|
|
|
&:focus {
|
|
|
|
|
border-color: rgba(92, 224, 255, 0.7);
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.13);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.core-pain-validation-message {
|
|
|
|
|
margin: 12px 0 0;
|
|
|
|
|
color: #ffab9d;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-navigation {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|