|
|
|
|
@ -149,32 +149,22 @@
|
|
|
|
|
<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 class="positioning-intro">
|
|
|
|
|
<p>根据前面实训分析,使用经典定位陈述格式撰写简洁有力的产品价值定位陈述。</p>
|
|
|
|
|
<p>经典定位陈述格式:对于【目标市场/用户】,我们的【产品名称】是一个【产品类别】,它能提供【核心价值/关键收益】,因为它具有【主要差异化理由】。</p>
|
|
|
|
|
</div>
|
|
|
|
|
<article v-for="(statement, index) in form.positioningStatements" :key="statement.id" class="positioning-version-card">
|
|
|
|
|
<header><span>版本{{ numberToChinese(index + 1) }}</span><small>至少设计两个不同目标用户版本</small></header>
|
|
|
|
|
<p class="statement-preview">对于 <strong>{{ statement.targetUser || '【目标市场/用户】' }}</strong>,我们的 <strong>{{ statement.productName || '【产品名称】' }}</strong> 是一个 <strong>{{ statement.productCategory || '【产品类别】' }}</strong>,它能提供 <strong>{{ statement.coreValue || '【核心价值/关键收益】' }}</strong>,因为它具有 <strong>{{ statement.differentiationReason || '【主要差异化理由】' }}</strong>。</p>
|
|
|
|
|
<div class="positioning-fields">
|
|
|
|
|
<label><span>目标市场/用户</span><input v-model="statement.targetUser" :aria-label="`版本${index + 1}目标市场或用户`" placeholder="填写目标市场/用户" /></label>
|
|
|
|
|
<label><span>产品名称</span><input v-model="statement.productName" :aria-label="`版本${index + 1}产品名称`" placeholder="填写产品名称" /></label>
|
|
|
|
|
<label><span>产品类别</span><input v-model="statement.productCategory" :aria-label="`版本${index + 1}产品类别`" placeholder="填写产品类别" /></label>
|
|
|
|
|
<label><span>核心价值/关键收益</span><input v-model="statement.coreValue" :aria-label="`版本${index + 1}核心价值或关键收益`" placeholder="填写核心价值/关键收益" /></label>
|
|
|
|
|
<label class="positioning-fields__wide"><span>主要差异化理由</span><textarea v-model="statement.differentiationReason" :aria-label="`版本${index + 1}主要差异化理由`" placeholder="填写主要差异化理由" /></label>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
<p v-if="positioningValidationMessage" class="core-pain-validation-message" role="status">{{ positioningValidationMessage }}</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(4)" class="step-card">
|
|
|
|
|
@ -257,7 +247,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, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
|
|
import { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, checkProductValuePositioningStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
|
import { isStudentDemo } from "@/utils/studentDemo";
|
|
|
|
|
|
|
|
|
|
@ -268,11 +258,18 @@ const checkingCorePain = ref(false);
|
|
|
|
|
const corePainValidationMessage = ref("");
|
|
|
|
|
const checkingValueCanvas = ref(false);
|
|
|
|
|
const valueCanvasValidationMessage = ref("");
|
|
|
|
|
const checkingPositioning = ref(false);
|
|
|
|
|
const positioningValidationMessage = 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"];
|
|
|
|
|
|
|
|
|
|
function createPositioningStatement(id) {
|
|
|
|
|
return { id, targetUser: "", productName: "", productCategory: "", coreValue: "", differentiationReason: "" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultRequirement =
|
|
|
|
|
"产品价值主张及定位需要在市场调研和用户洞察基础上,明确目标用户、核心价值、差异化优势和定位表达。";
|
|
|
|
|
@ -294,6 +291,7 @@ function createForm() {
|
|
|
|
|
customerProfile: { jobs: "", pains: "", gains: "" },
|
|
|
|
|
valueMap: { products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
valuePropositionCanvas: { url: "", name: "" },
|
|
|
|
|
positioningStatements: [createPositioningStatement(1), createPositioningStatement(2)],
|
|
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
statement: { targetUsers: "", valueProposition: "", differentiation: "", positioning: "" },
|
|
|
|
|
};
|
|
|
|
|
@ -350,6 +348,7 @@ async function setCurrentStep(step) {
|
|
|
|
|
if (targetStep > form.value.activeStep) {
|
|
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return;
|
|
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return;
|
|
|
|
|
if (form.value.activeStep === 3 && !(await ensurePositioningValid())) return;
|
|
|
|
|
}
|
|
|
|
|
form.value.activeStep = targetStep;
|
|
|
|
|
}
|
|
|
|
|
@ -388,6 +387,7 @@ function normalizeForm(value = {}) {
|
|
|
|
|
customerProfile: { ...base.customerProfile, ...(value.customerProfile || {}) },
|
|
|
|
|
valueMap: { ...base.valueMap, ...(value.valueMap || {}) },
|
|
|
|
|
valuePropositionCanvas: { ...base.valuePropositionCanvas, ...(value.valuePropositionCanvas || {}) },
|
|
|
|
|
positioningStatements: normalizePositioningStatements(value.positioningStatements),
|
|
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
|
|
statement: { ...base.statement, ...(value.statement || {}) },
|
|
|
|
|
};
|
|
|
|
|
@ -403,6 +403,14 @@ function normalizeCorePainRows(rows = []) {
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePositioningStatements(statements = []) {
|
|
|
|
|
const savedStatements = Array.isArray(statements) ? statements : [];
|
|
|
|
|
return [1, 2].map((id, index) => {
|
|
|
|
|
const saved = savedStatements[index] || {};
|
|
|
|
|
return positioningStatementFields.reduce((result, field) => ({ ...result, [field]: String(saved[field] || "") }), { id });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureCorePainValid() {
|
|
|
|
|
if (checkingCorePain.value) return false;
|
|
|
|
|
checkingCorePain.value = true;
|
|
|
|
|
@ -437,6 +445,20 @@ async function ensureValueCanvasValid() {
|
|
|
|
|
} finally { checkingValueCanvas.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensurePositioningValid() {
|
|
|
|
|
if (checkingPositioning.value) return false;
|
|
|
|
|
checkingPositioning.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const response = await checkProductValuePositioningStepThree({ statements: form.value.positioningStatements });
|
|
|
|
|
const result = response?.data || {};
|
|
|
|
|
positioningValidationMessage.value = result.message || (result.valid ? "两版定位陈述填写完整。" : "请完整填写两个产品价值定位陈述版本。");
|
|
|
|
|
return result.valid === true;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
positioningValidationMessage.value = error?.message || "定位陈述校验失败,请稍后重试。";
|
|
|
|
|
return false;
|
|
|
|
|
} finally { checkingPositioning.value = false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildOutcome() {
|
|
|
|
|
return {
|
|
|
|
|
taskKey: TASK_KEY,
|
|
|
|
|
@ -450,6 +472,7 @@ function buildOutcome() {
|
|
|
|
|
customerProfile: form.value.customerProfile,
|
|
|
|
|
valueMap: form.value.valueMap,
|
|
|
|
|
valuePropositionCanvas: form.value.valuePropositionCanvas,
|
|
|
|
|
positioningStatements: form.value.positioningStatements,
|
|
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
|
|
statement: form.value.statement,
|
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
@ -469,6 +492,7 @@ async function persist(status = "IN_PROGRESS") {
|
|
|
|
|
if (status !== "RESET") {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
saving.value = true;
|
|
|
|
|
try {
|
|
|
|
|
@ -504,6 +528,8 @@ function exportOutcome() {
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function numberToChinese(number) { return ["一", "二", "三", "四"][number - 1] || String(number); }
|
|
|
|
|
|
|
|
|
|
const customerProfileExportContent = computed(() => `# 用户档案(Customer Profile)\n\n## 用户目标(Jobs)\n${form.value.customerProfile.jobs}\n\n## 痛点(Pains)\n${form.value.customerProfile.pains}\n\n## 收益期望(Gains)\n${form.value.customerProfile.gains}\n`);
|
|
|
|
|
const valueMapExportContent = computed(() => `# 产品价值地图(Value Map)\n\n## 产品与服务\n${form.value.valueMap.products}\n\n## 痛点缓释器(Pain Relievers)\n${form.value.valueMap.painRelievers}\n\n## 增益创造器(Gain Creators)\n${form.value.valueMap.gainCreators}\n`);
|
|
|
|
|
|
|
|
|
|
@ -1112,6 +1138,67 @@ async function handleValueCanvasUpload(event) {
|
|
|
|
|
.template-download-button { position: absolute; top: 44px; left: 18px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.positioning-intro {
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
padding: 15px 18px;
|
|
|
|
|
border-left: 3px solid #55ddff;
|
|
|
|
|
border-radius: 0 14px 14px 0;
|
|
|
|
|
background: linear-gradient(90deg, rgba(7, 92, 132, .28), rgba(5, 34, 51, .12));
|
|
|
|
|
|
|
|
|
|
p { margin: 0; color: #c9e7f3; font-size: 14px; line-height: 1.8; }
|
|
|
|
|
p + p { margin-top: 5px; color: #a9ccda; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.positioning-version-card {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
border: 1px solid rgba(74, 178, 223, .36);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
background: linear-gradient(120deg, rgba(5, 55, 81, .42), rgba(1, 17, 29, .5));
|
|
|
|
|
|
|
|
|
|
header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 14px;
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
border-bottom: 1px solid rgba(75, 174, 218, .24);
|
|
|
|
|
background: rgba(4, 86, 125, .35);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
header span { color: #f2fdff; font-size: 16px; font-weight: 900; }
|
|
|
|
|
header small { color: #8ecadd; font-size: 12px; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.statement-preview {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 15px 16px;
|
|
|
|
|
color: #afd2df;
|
|
|
|
|
background: rgba(0, 16, 29, .36);
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
line-height: 1.85;
|
|
|
|
|
|
|
|
|
|
strong { color: #64daf8; font-weight: 800; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.positioning-fields {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
|
|
|
|
label { display: grid; gap: 6px; min-width: 0; }
|
|
|
|
|
label span { color: #d8f2fb; font-size: 12px; font-weight: 800; }
|
|
|
|
|
input,
|
|
|
|
|
textarea {
|
|
|
|
|
width: 100%; padding: 10px 11px; border: 1px solid rgba(45, 95, 126, .95); border-radius: 9px; color: #eef5ff; background: rgba(4, 24, 38, .72); box-sizing: border-box; font: inherit; font-size: 13px; line-height: 1.55;
|
|
|
|
|
&:focus { border-color: rgba(92, 224, 255, .7); box-shadow: 0 0 0 3px rgba(0, 174, 255, .13); outline: none; }
|
|
|
|
|
}
|
|
|
|
|
textarea { min-height: 66px; resize: vertical; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.positioning-fields__wide { grid-column: 1 / -1; }
|
|
|
|
|
|
|
|
|
|
.step-navigation {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
@ -1192,6 +1279,8 @@ async function handleValueCanvasUpload(event) {
|
|
|
|
|
writing-mode: vertical-rl;
|
|
|
|
|
}
|
|
|
|
|
.value-grid { grid-template-columns: 1fr; }
|
|
|
|
|
.positioning-fields { grid-template-columns: 1fr; }
|
|
|
|
|
.positioning-fields__wide { grid-column: auto; }
|
|
|
|
|
.canvas-guidance { padding: 15px; }
|
|
|
|
|
.canvas-guidance .template-download-button { position: static; margin: 8px 0; }
|
|
|
|
|
}
|
|
|
|
|
|