|
|
|
|
@ -174,24 +174,35 @@
|
|
|
|
|
<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 class="mrd-intro">
|
|
|
|
|
基于前述步骤的分析成果,撰写一份完整的产品市场需求说明书(MRD)。MRD 文档全面分析产品的市场机会、用户需求及竞争格局,明确产品核心定位与功能方向,为后续产品开发、资源投入及战略决策提供数据支撑与逻辑依据。
|
|
|
|
|
</div>
|
|
|
|
|
<section v-for="section in mrdSections" :key="section.key" class="mrd-section">
|
|
|
|
|
<h4>{{ section.order }}. {{ section.title }}</h4>
|
|
|
|
|
<div class="mrd-table-wrap">
|
|
|
|
|
<table class="mrd-table">
|
|
|
|
|
<thead><tr><th>序号</th><th v-if="section.hasModule">模块</th><th>标题</th><th>内容</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="(row, index) in form.mrd[section.key]" :key="row.id">
|
|
|
|
|
<td>{{ index + 1 }}</td>
|
|
|
|
|
<td v-if="section.hasModule" class="mrd-module">{{ row.module }}</td>
|
|
|
|
|
<td class="mrd-row-title">{{ row.title }}</td>
|
|
|
|
|
<td><textarea v-model="row.content" :aria-label="`${section.title}${row.title}内容`" :placeholder="`填写${row.title}内容`" /></td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
<section class="mrd-positioning-section">
|
|
|
|
|
<h4>6. 产品定位</h4>
|
|
|
|
|
<textarea v-model="form.mrd.productPositioning" aria-label="产品定位" placeholder="综合市场、用户、竞品和需求分析,填写产品定位" />
|
|
|
|
|
</section>
|
|
|
|
|
<div class="mrd-guidance">
|
|
|
|
|
<p>根据以上结构框架及信息,结合实训 8-11,撰写 MRD 文档。</p>
|
|
|
|
|
<p>1. 根据报告模板,自行分析撰写 MRD 文档。</p>
|
|
|
|
|
<p>2. 使用通用 AI 大模型辅助完成报告,参考步骤提示词见案例资料文档。</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-if="mrdValidationMessage" class="core-pain-validation-message" role="status">{{ mrdValidationMessage }}</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-show="hasConfiguredSteps && form.activeStep > 4" class="step-card completion-card">
|
|
|
|
|
@ -247,7 +258,7 @@ 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 { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, checkProductValuePositioningStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
|
|
import { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, checkProductValuePositioningStepThree, checkProductValuePositioningStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
|
import { isStudentDemo } from "@/utils/studentDemo";
|
|
|
|
|
|
|
|
|
|
@ -260,17 +271,34 @@ const checkingValueCanvas = ref(false);
|
|
|
|
|
const valueCanvasValidationMessage = ref("");
|
|
|
|
|
const checkingPositioning = ref(false);
|
|
|
|
|
const positioningValidationMessage = ref("");
|
|
|
|
|
const checkingMrd = ref(false);
|
|
|
|
|
const mrdValidationMessage = ref("");
|
|
|
|
|
const uploadingValueCanvas = ref(false);
|
|
|
|
|
const valueCanvasInput = ref(null);
|
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
|
|
|
|
|
|
const corePainTypes = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
|
|
|
|
|
const positioningStatementFields = ["targetUser", "productName", "productCategory", "coreValue", "differentiationReason"];
|
|
|
|
|
const mrdSections = [
|
|
|
|
|
{ key: "marketInsights", order: 1, title: "市场问题与机会", rows: [{ title: "市场痛点" }, { title: "市场机会" }, { title: "发展趋势" }] },
|
|
|
|
|
{ key: "marketOverview", order: 2, title: "市场概述", rows: [{ title: "目标市场" }, { title: "市场特征" }] },
|
|
|
|
|
{ key: "userAnalysis", order: 3, title: "用户与购买者分析", rows: [{ title: "目标用户画像" }, { title: "用户场景与需求" }, { title: "购买者与决策者" }] },
|
|
|
|
|
{ key: "competitorAnalysis", order: 4, title: "竞品分析", rows: [{ title: "竞品对比分析" }, { title: "差异化机会" }] },
|
|
|
|
|
{ key: "productRequirements", order: 5, title: "产品需求说明", hasModule: true, rows: [{ module: "功能需求", title: "核心功能" }, { module: "功能需求", title: "辅助功能" }, { module: "非功能需求", title: "性能需求" }, { module: "非功能需求", title: "安全需求" }, { module: "非功能需求", title: "兼容性" }] },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function createPositioningStatement(id) {
|
|
|
|
|
return { id, targetUser: "", productName: "", productCategory: "", coreValue: "", differentiationReason: "" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createMrd() {
|
|
|
|
|
const mrd = { productPositioning: "" };
|
|
|
|
|
mrdSections.forEach((section) => {
|
|
|
|
|
mrd[section.key] = section.rows.map((row, index) => ({ id: index + 1, module: row.module || "", title: row.title, content: "" }));
|
|
|
|
|
});
|
|
|
|
|
return mrd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultRequirement =
|
|
|
|
|
"产品价值主张及定位需要在市场调研和用户洞察基础上,明确目标用户、核心价值、差异化优势和定位表达。";
|
|
|
|
|
const defaultBackground =
|
|
|
|
|
@ -292,6 +320,7 @@ function createForm() {
|
|
|
|
|
valueMap: { products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
valuePropositionCanvas: { url: "", name: "" },
|
|
|
|
|
positioningStatements: [createPositioningStatement(1), createPositioningStatement(2)],
|
|
|
|
|
mrd: createMrd(),
|
|
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
statement: { targetUsers: "", valueProposition: "", differentiation: "", positioning: "" },
|
|
|
|
|
};
|
|
|
|
|
@ -349,6 +378,7 @@ async function setCurrentStep(step) {
|
|
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return;
|
|
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return;
|
|
|
|
|
if (form.value.activeStep === 3 && !(await ensurePositioningValid())) return;
|
|
|
|
|
if (form.value.activeStep === 4 && !(await ensureMrdValid())) return;
|
|
|
|
|
}
|
|
|
|
|
form.value.activeStep = targetStep;
|
|
|
|
|
}
|
|
|
|
|
@ -388,6 +418,7 @@ function normalizeForm(value = {}) {
|
|
|
|
|
valueMap: { ...base.valueMap, ...(value.valueMap || {}) },
|
|
|
|
|
valuePropositionCanvas: { ...base.valuePropositionCanvas, ...(value.valuePropositionCanvas || {}) },
|
|
|
|
|
positioningStatements: normalizePositioningStatements(value.positioningStatements),
|
|
|
|
|
mrd: normalizeMrd(value.mrd),
|
|
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
|
|
statement: { ...base.statement, ...(value.statement || {}) },
|
|
|
|
|
};
|
|
|
|
|
@ -411,6 +442,21 @@ function normalizePositioningStatements(statements = []) {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeMrd(mrd = {}) {
|
|
|
|
|
const base = createMrd();
|
|
|
|
|
mrdSections.forEach((section) => {
|
|
|
|
|
const savedRows = Array.isArray(mrd?.[section.key]) ? mrd[section.key] : [];
|
|
|
|
|
base[section.key] = section.rows.map((row, index) => ({
|
|
|
|
|
id: index + 1,
|
|
|
|
|
module: row.module || "",
|
|
|
|
|
title: row.title,
|
|
|
|
|
content: String(savedRows[index]?.content || ""),
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
base.productPositioning = String(mrd?.productPositioning || "");
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureCorePainValid() {
|
|
|
|
|
if (checkingCorePain.value) return false;
|
|
|
|
|
checkingCorePain.value = true;
|
|
|
|
|
@ -459,6 +505,20 @@ async function ensurePositioningValid() {
|
|
|
|
|
} finally { checkingPositioning.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureMrdValid() {
|
|
|
|
|
if (checkingMrd.value) return false;
|
|
|
|
|
checkingMrd.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await checkProductValuePositioningStepFour(form.value.mrd);
|
|
|
|
|
const result = response?.data || {};
|
|
|
|
|
mrdValidationMessage.value = result.message || (result.valid ? "MRD 文档填写完整。" : "请完整填写 MRD 的五组分析内容和产品定位。");
|
|
|
|
|
return result.valid === true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
mrdValidationMessage.value = error?.message || "MRD 校验失败,请稍后重试。";
|
|
|
|
|
return false;
|
|
|
|
|
} finally { checkingMrd.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildOutcome() {
|
|
|
|
|
return {
|
|
|
|
|
taskKey: TASK_KEY,
|
|
|
|
|
@ -473,6 +533,7 @@ function buildOutcome() {
|
|
|
|
|
valueMap: form.value.valueMap,
|
|
|
|
|
valuePropositionCanvas: form.value.valuePropositionCanvas,
|
|
|
|
|
positioningStatements: form.value.positioningStatements,
|
|
|
|
|
mrd: form.value.mrd,
|
|
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
|
|
statement: form.value.statement,
|
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
@ -493,6 +554,7 @@ async function persist(status = "IN_PROGRESS") {
|
|
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return false;
|
|
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return false;
|
|
|
|
|
if (form.value.activeStep === 3 && !(await ensurePositioningValid())) return false;
|
|
|
|
|
if (form.value.activeStep === 4 && !(await ensureMrdValid())) return false;
|
|
|
|
|
}
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
@ -1199,6 +1261,64 @@ async function handleValueCanvasUpload(event) {
|
|
|
|
|
|
|
|
|
|
.positioning-fields__wide { grid-column: 1 / -1; }
|
|
|
|
|
|
|
|
|
|
.mrd-intro {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
padding: 15px 18px;
|
|
|
|
|
border: 1px solid rgba(78, 183, 227, .3);
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
color: #c9e6f1;
|
|
|
|
|
background: linear-gradient(100deg, rgba(5, 78, 112, .28), rgba(3, 26, 42, .18));
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.85;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mrd-section { margin-top: 24px; }
|
|
|
|
|
.mrd-section h4,
|
|
|
|
|
.mrd-positioning-section h4 { margin: 0 0 10px; color: #ffffff; font-size: 16px; font-weight: 900; }
|
|
|
|
|
.mrd-table-wrap { overflow-x: auto; }
|
|
|
|
|
|
|
|
|
|
.mrd-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 760px;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
table-layout: fixed;
|
|
|
|
|
|
|
|
|
|
th, td { border: 1px solid rgba(77, 167, 220, .3); }
|
|
|
|
|
th { padding: 9px; color: #f5fcff; background: linear-gradient(100deg, #0787bd, #075276); font-size: 13px; font-weight: 800; }
|
|
|
|
|
td { min-height: 42px; padding: 7px; color: #d5edf7; background: rgba(1, 14, 26, .48); font-size: 13px; text-align: center; vertical-align: middle; }
|
|
|
|
|
th:nth-child(1), td:nth-child(1) { width: 8%; }
|
|
|
|
|
th:nth-child(2), td:nth-child(2) { width: 17%; }
|
|
|
|
|
.mrd-row-title { color: #bbebf6; font-weight: 800; }
|
|
|
|
|
.mrd-module { color: #a6d7e6; }
|
|
|
|
|
textarea {
|
|
|
|
|
display: block; width: 100%; min-height: 50px; padding: 8px 10px; border: 1px solid rgba(45, 95, 126, .95); border-radius: 8px; color: #eef5ff; background: rgba(4, 24, 38, .72); box-sizing: border-box; font: inherit; font-size: 13px; line-height: 1.55; resize: vertical;
|
|
|
|
|
&:focus { border-color: rgba(92, 224, 255, .7); box-shadow: 0 0 0 3px rgba(0, 174, 255, .13); outline: none; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mrd-positioning-section {
|
|
|
|
|
margin-top: 25px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border: 1px solid rgba(74, 178, 223, .34);
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
background: rgba(1, 31, 48, .35);
|
|
|
|
|
|
|
|
|
|
textarea {
|
|
|
|
|
display: block; width: 100%; min-height: 120px; padding: 12px; border: 1px solid rgba(45, 95, 126, .95); border-radius: 10px; color: #eef5ff; background: rgba(4, 24, 38, .72); box-sizing: border-box; font: inherit; font-size: 14px; line-height: 1.65; resize: vertical;
|
|
|
|
|
&:focus { border-color: rgba(92, 224, 255, .7); box-shadow: 0 0 0 3px rgba(0, 174, 255, .13); outline: none; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mrd-guidance {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
padding: 13px 16px;
|
|
|
|
|
border-left: 3px solid #4fdbf5;
|
|
|
|
|
border-radius: 0 12px 12px 0;
|
|
|
|
|
background: rgba(3, 46, 67, .26);
|
|
|
|
|
p { margin: 0; color: #bfdeea; font-size: 13px; line-height: 1.75; }
|
|
|
|
|
p + p { margin-top: 4px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-navigation {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|