|
|
|
@ -82,20 +82,64 @@
|
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
|
<h3>{{ activeStepName }}</h3>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="step-content">
|
|
|
|
<div class="value-proposition-intro">
|
|
|
|
<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>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="canvas-section-heading">
|
|
|
|
|
|
|
|
<h4>(一)用户档案 <small>Customer Profile</small></h4>
|
|
|
|
|
|
|
|
<button type="button" class="canvas-export-button" @click="exportCanvasSection('用户档案', customerProfileExportContent)">
|
|
|
|
|
|
|
|
<el-icon><Download /></el-icon>导出
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="value-canvas-table-wrap">
|
|
|
|
|
|
|
|
<table class="value-canvas-table">
|
|
|
|
|
|
|
|
<thead><tr><th>维度</th><th>说明</th><th>内容</th></tr></thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
<tr><td>用户目标(Jobs)</td><td>用户在相关场景中试图完成的任务</td><td><textarea v-model="form.customerProfile.jobs" placeholder="示例:快速入睡、提升睡眠质量、温和唤醒不犯困、起夜不刺眼" /></td></tr>
|
|
|
|
|
|
|
|
<tr><td>痛点(Pains)</td><td>用户遇到的障碍、风险和负面体验</td><td><textarea v-model="form.customerProfile.pains" placeholder="填写用户痛点" /></td></tr>
|
|
|
|
|
|
|
|
<tr><td>收益期望(Gains)</td><td>用户期望的结果和额外收益</td><td><textarea v-model="form.customerProfile.gains" placeholder="填写用户收益期望" /></td></tr>
|
|
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="canvas-section-heading canvas-section-heading--spaced">
|
|
|
|
|
|
|
|
<h4>(二)产品价值地图 <small>Value Map</small></h4>
|
|
|
|
|
|
|
|
<button type="button" class="canvas-export-button" @click="exportCanvasSection('产品价值地图', valueMapExportContent)">
|
|
|
|
|
|
|
|
<el-icon><Download /></el-icon>导出
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="value-canvas-table-wrap">
|
|
|
|
|
|
|
|
<table class="value-canvas-table">
|
|
|
|
|
|
|
|
<thead><tr><th>维度</th><th>说明</th><th>内容</th></tr></thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
<tr><td>产品与服务</td><td>我们提供给用户的产品和服务清单</td><td><textarea v-model="form.valueMap.products" placeholder="示例:智能声光伴眠吸顶灯(含 App 控制)、白噪音声库、睡眠监测服务、节律光算法" /></td></tr>
|
|
|
|
|
|
|
|
<tr><td>痛点缓释器(Pain Relievers)</td><td>产品如何消除或缓解用户痛点</td><td><textarea v-model="form.valueMap.painRelievers" placeholder="填写痛点缓释器" /></td></tr>
|
|
|
|
|
|
|
|
<tr><td>增益创造器(Gain Creators)</td><td>产品如何创造额外价值和惊喜体验</td><td><textarea v-model="form.valueMap.gainCreators" placeholder="填写增益创造器" /></td></tr>
|
|
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="canvas-drawing-section">
|
|
|
|
|
|
|
|
<h4>(三)绘制产品价值主张画布</h4>
|
|
|
|
|
|
|
|
<p>检查“产品价值地图”与“用户档案”是否实现匹配(Fit):每个痛点应有对应的痛点缓释器,每个收益期望应有对应的增益创造器。根据上表绘制产品价值主张画布。</p>
|
|
|
|
|
|
|
|
<input ref="valueCanvasInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleValueCanvasUpload" />
|
|
|
|
|
|
|
|
<div v-if="form.valuePropositionCanvas.url" class="value-canvas-preview">
|
|
|
|
|
|
|
|
<el-image :src="form.valuePropositionCanvas.url" :preview-src-list="[form.valuePropositionCanvas.url]" fit="cover" preview-teleported />
|
|
|
|
|
|
|
|
<div><strong>{{ form.valuePropositionCanvas.name || '产品价值主张画布' }}</strong><span>点击缩略图可放大预览</span><button type="button" class="text-button" @click="clearValueCanvas">移除图片</button></div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<button v-else type="button" class="canvas-upload-trigger" :disabled="uploadingValueCanvas" @click="openValueCanvasPicker">
|
|
|
|
|
|
|
|
<el-icon><UploadFilled /></el-icon>{{ uploadingValueCanvas ? '正在上传...' : '上传产品价值主张画布' }}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="canvas-guidance">
|
|
|
|
|
|
|
|
<p>推荐工具及方法如下,自行选择:</p>
|
|
|
|
|
|
|
|
<p>1. 下载模板,填写用户档案及产品价值地图,完成产品价值主张画布。</p>
|
|
|
|
|
|
|
|
<button type="button" class="template-download-button" @click="downloadCanvasTemplate"><el-icon><Download /></el-icon>模板下载</button>
|
|
|
|
|
|
|
|
<p>2. 导出用户档案和价值主张地图两张表,使用通用 AI 大模型辅助生成画布。</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<p v-if="valueCanvasValidationMessage" class="core-pain-validation-message" role="status">{{ valueCanvasValidationMessage }}</p>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-show="isSectionVisible(3)" class="step-card">
|
|
|
|
<section v-show="isSectionVisible(3)" class="step-card">
|
|
|
|
@ -208,19 +252,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
<script setup>
|
|
|
|
import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
|
|
|
import { computed, getCurrentInstance, onMounted, ref } from "vue";
|
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, Operation } from "@element-plus/icons-vue";
|
|
|
|
import { ArrowLeft, ArrowRight, CircleCheck, Download, Operation, UploadFilled } from "@element-plus/icons-vue";
|
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
|
|
|
|
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
|
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
|
import { checkProductValuePositioningStepOne, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
|
|
|
|
import { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
|
|
|
|
import { isStudentDemo } from "@/utils/studentDemo";
|
|
|
|
|
|
|
|
|
|
|
|
const TASK_KEY = "product-value-positioning";
|
|
|
|
const TASK_KEY = "product-value-positioning";
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
const { proxy } = getCurrentInstance();
|
|
|
|
const saving = ref(false);
|
|
|
|
const saving = ref(false);
|
|
|
|
const checkingCorePain = ref(false);
|
|
|
|
const checkingCorePain = ref(false);
|
|
|
|
const corePainValidationMessage = ref("");
|
|
|
|
const corePainValidationMessage = ref("");
|
|
|
|
|
|
|
|
const checkingValueCanvas = ref(false);
|
|
|
|
|
|
|
|
const valueCanvasValidationMessage = ref("");
|
|
|
|
|
|
|
|
const uploadingValueCanvas = ref(false);
|
|
|
|
|
|
|
|
const valueCanvasInput = ref(null);
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const corePainTypes = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
|
|
|
|
const corePainTypes = ["核心痛点1", "核心痛点2", "核心痛点3", "弱需求", "伪需求"];
|
|
|
|
@ -242,6 +291,9 @@ function createForm() {
|
|
|
|
corePainRows: corePainTypes.map((type, index) => ({ id: index + 1, type, description: "", scene: "" })),
|
|
|
|
corePainRows: corePainTypes.map((type, index) => ({ id: index + 1, type, description: "", scene: "" })),
|
|
|
|
research: { marketAnalysis: "", userResearch: "", persona: "" },
|
|
|
|
research: { marketAnalysis: "", userResearch: "", persona: "" },
|
|
|
|
positionCanvas: { targetUsers: "", needsAndPains: "", competitorOpportunity: "" },
|
|
|
|
positionCanvas: { targetUsers: "", needsAndPains: "", competitorOpportunity: "" },
|
|
|
|
|
|
|
|
customerProfile: { jobs: "", pains: "", gains: "" },
|
|
|
|
|
|
|
|
valueMap: { products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
|
|
|
|
valuePropositionCanvas: { url: "", name: "" },
|
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
valueCanvas: { customerJobs: "", pains: "", gains: "", products: "", painRelievers: "", gainCreators: "" },
|
|
|
|
statement: { targetUsers: "", valueProposition: "", differentiation: "", positioning: "" },
|
|
|
|
statement: { targetUsers: "", valueProposition: "", differentiation: "", positioning: "" },
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -295,7 +347,10 @@ function normalizeActiveStep(value) {
|
|
|
|
|
|
|
|
|
|
|
|
async function setCurrentStep(step) {
|
|
|
|
async function setCurrentStep(step) {
|
|
|
|
const targetStep = normalizeActiveStep(step);
|
|
|
|
const targetStep = normalizeActiveStep(step);
|
|
|
|
if (targetStep > form.value.activeStep && form.value.activeStep === 1 && !(await ensureCorePainValid())) return;
|
|
|
|
if (targetStep > form.value.activeStep) {
|
|
|
|
|
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return;
|
|
|
|
|
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return;
|
|
|
|
|
|
|
|
}
|
|
|
|
form.value.activeStep = targetStep;
|
|
|
|
form.value.activeStep = targetStep;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -330,6 +385,9 @@ function normalizeForm(value = {}) {
|
|
|
|
corePainRows: normalizeCorePainRows(value.corePainRows),
|
|
|
|
corePainRows: normalizeCorePainRows(value.corePainRows),
|
|
|
|
research: { ...base.research, ...(value.research || {}) },
|
|
|
|
research: { ...base.research, ...(value.research || {}) },
|
|
|
|
positionCanvas: { ...base.positionCanvas, ...(value.positionCanvas || {}) },
|
|
|
|
positionCanvas: { ...base.positionCanvas, ...(value.positionCanvas || {}) },
|
|
|
|
|
|
|
|
customerProfile: { ...base.customerProfile, ...(value.customerProfile || {}) },
|
|
|
|
|
|
|
|
valueMap: { ...base.valueMap, ...(value.valueMap || {}) },
|
|
|
|
|
|
|
|
valuePropositionCanvas: { ...base.valuePropositionCanvas, ...(value.valuePropositionCanvas || {}) },
|
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
|
valueCanvas: { ...base.valueCanvas, ...(value.valueCanvas || {}) },
|
|
|
|
statement: { ...base.statement, ...(value.statement || {}) },
|
|
|
|
statement: { ...base.statement, ...(value.statement || {}) },
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -361,6 +419,24 @@ async function ensureCorePainValid() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureValueCanvasValid() {
|
|
|
|
|
|
|
|
if (checkingValueCanvas.value) return false;
|
|
|
|
|
|
|
|
checkingValueCanvas.value = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const response = await checkProductValuePositioningStepTwo({
|
|
|
|
|
|
|
|
customerProfile: form.value.customerProfile,
|
|
|
|
|
|
|
|
valueMap: form.value.valueMap,
|
|
|
|
|
|
|
|
canvasUrl: form.value.valuePropositionCanvas.url,
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = response?.data || {};
|
|
|
|
|
|
|
|
valueCanvasValidationMessage.value = result.message || (result.valid ? "价值主张画布填写完整。" : "请完整填写两张画布表并上传产品价值主张画布。");
|
|
|
|
|
|
|
|
return result.valid === true;
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
valueCanvasValidationMessage.value = error?.message || "价值主张画布校验失败,请稍后重试。";
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
} finally { checkingValueCanvas.value = false; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildOutcome() {
|
|
|
|
function buildOutcome() {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
taskKey: TASK_KEY,
|
|
|
|
taskKey: TASK_KEY,
|
|
|
|
@ -371,6 +447,9 @@ function buildOutcome() {
|
|
|
|
corePainRows: form.value.corePainRows,
|
|
|
|
corePainRows: form.value.corePainRows,
|
|
|
|
research: form.value.research,
|
|
|
|
research: form.value.research,
|
|
|
|
positionCanvas: form.value.positionCanvas,
|
|
|
|
positionCanvas: form.value.positionCanvas,
|
|
|
|
|
|
|
|
customerProfile: form.value.customerProfile,
|
|
|
|
|
|
|
|
valueMap: form.value.valueMap,
|
|
|
|
|
|
|
|
valuePropositionCanvas: form.value.valuePropositionCanvas,
|
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
|
valueCanvas: form.value.valueCanvas,
|
|
|
|
statement: form.value.statement,
|
|
|
|
statement: form.value.statement,
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
@ -387,7 +466,10 @@ function buildAnswerPayload(status) {
|
|
|
|
|
|
|
|
|
|
|
|
async function persist(status = "IN_PROGRESS") {
|
|
|
|
async function persist(status = "IN_PROGRESS") {
|
|
|
|
if (saving.value) return;
|
|
|
|
if (saving.value) return;
|
|
|
|
if (status !== "RESET" && form.value.activeStep === 1 && !(await ensureCorePainValid())) return false;
|
|
|
|
if (status !== "RESET") {
|
|
|
|
|
|
|
|
if (form.value.activeStep === 1 && !(await ensureCorePainValid())) return false;
|
|
|
|
|
|
|
|
if (form.value.activeStep === 2 && !(await ensureValueCanvasValid())) return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
saving.value = true;
|
|
|
|
saving.value = true;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
|
|
|
|
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
|
|
|
|
@ -407,6 +489,7 @@ function submitTask() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function resetTraining() {
|
|
|
|
async function resetTraining() {
|
|
|
|
|
|
|
|
clearValueCanvas();
|
|
|
|
form.value = createForm();
|
|
|
|
form.value = createForm();
|
|
|
|
await persist("RESET");
|
|
|
|
await persist("RESET");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -420,6 +503,55 @@ function exportOutcome() {
|
|
|
|
link.click();
|
|
|
|
link.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function downloadText(content, fileName) {
|
|
|
|
|
|
|
|
const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
|
|
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
const link = document.createElement("a");
|
|
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
|
|
link.download = fileName;
|
|
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function exportCanvasSection(name, content) { downloadText(content, `${taskTitle.value}-${name}.md`); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function downloadCanvasTemplate() {
|
|
|
|
|
|
|
|
downloadText("# 产品价值主张画布\n\n## 用户档案\n- 用户目标(Jobs):\n- 痛点(Pains):\n- 收益期望(Gains):\n\n## 产品价值地图\n- 产品与服务:\n- 痛点缓释器(Pain Relievers):\n- 增益创造器(Gain Creators):\n\n## 匹配(Fit)说明\n", "产品价值主张画布模板.md");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function openValueCanvasPicker() { valueCanvasInput.value?.click(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function clearValueCanvas() {
|
|
|
|
|
|
|
|
if (form.value?.valuePropositionCanvas?.url?.startsWith("blob:")) URL.revokeObjectURL(form.value.valuePropositionCanvas.url);
|
|
|
|
|
|
|
|
if (form.value) form.value.valuePropositionCanvas = { url: "", name: "" };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function handleValueCanvasUpload(event) {
|
|
|
|
|
|
|
|
const file = event.target?.files?.[0];
|
|
|
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
if (!["image/jpeg", "image/png", "image/webp"].includes(file.type)) {
|
|
|
|
|
|
|
|
proxy?.$modal?.msgError?.("请上传 PNG、JPG 或 WEBP 格式的图片"); event.target.value = ""; return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("图片大小不能超过 5MB"); event.target.value = ""; return; }
|
|
|
|
|
|
|
|
uploadingValueCanvas.value = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (isStudentDemo()) { clearValueCanvas(); form.value.valuePropositionCanvas = { url: URL.createObjectURL(file), name: file.name }; }
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
const formData = new FormData(); formData.append("file", file);
|
|
|
|
|
|
|
|
const result = await uploadStudentTrainingFile(formData);
|
|
|
|
|
|
|
|
const url = result?.url || result?.data?.url;
|
|
|
|
|
|
|
|
if (!url) throw new Error("图片上传失败,请重试");
|
|
|
|
|
|
|
|
form.value.valuePropositionCanvas = { url, name: file.name };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
valueCanvasValidationMessage.value = "";
|
|
|
|
|
|
|
|
proxy?.$modal?.msgSuccess?.("产品价值主张画布上传成功");
|
|
|
|
|
|
|
|
} catch (error) { proxy?.$modal?.msgError?.(error?.message || "图片上传失败,请重试"); }
|
|
|
|
|
|
|
|
finally { uploadingValueCanvas.value = false; event.target.value = ""; }
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
<style scoped lang="scss">
|
|
|
|
@ -862,6 +994,124 @@ function exportOutcome() {
|
|
|
|
font-weight: 800;
|
|
|
|
font-weight: 800;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.value-proposition-intro {
|
|
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
|
|
border: 1px solid rgba(71, 183, 232, 0.28);
|
|
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
|
|
color: #c9e9f5;
|
|
|
|
|
|
|
|
background: rgba(4, 48, 71, 0.28);
|
|
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
line-height: 1.8;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-section-heading {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
|
|
margin: 18px 0 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
h4 { margin: 0; color: #ffffff; font-size: 17px; }
|
|
|
|
|
|
|
|
small { margin-left: 6px; color: #8dc8df; font-size: 12px; font-weight: 600; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-section-heading--spaced { margin-top: 28px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-export-button,
|
|
|
|
|
|
|
|
.template-download-button,
|
|
|
|
|
|
|
|
.canvas-upload-trigger {
|
|
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
gap: 7px;
|
|
|
|
|
|
|
|
border: 1px solid rgba(94, 219, 255, 0.72);
|
|
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
|
|
color: #effcff;
|
|
|
|
|
|
|
|
background: linear-gradient(105deg, #078bbf, #095c89);
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
|
|
transition: .2s ease;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-export-button { min-width: 92px; height: 34px; padding: 0 14px; font-size: 13px; }
|
|
|
|
|
|
|
|
.template-download-button { min-width: 116px; height: 36px; padding: 0 14px; font-size: 13px; }
|
|
|
|
|
|
|
|
.canvas-upload-trigger { min-width: 218px; min-height: 54px; padding: 0 22px; font-size: 15px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-export-button:hover,
|
|
|
|
|
|
|
|
.template-download-button:hover,
|
|
|
|
|
|
|
|
.canvas-upload-trigger:hover:not(:disabled) {
|
|
|
|
|
|
|
|
border-color: #9bf1ff;
|
|
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
|
|
box-shadow: 0 7px 18px rgba(0, 158, 213, 0.2);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-upload-trigger:disabled { cursor: not-allowed; opacity: .65; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.value-canvas-table-wrap { overflow-x: auto; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.value-canvas-table {
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
min-width: 820px;
|
|
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
|
|
table-layout: fixed;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
th,
|
|
|
|
|
|
|
|
td { border: 1px solid rgba(77, 167, 220, 0.3); }
|
|
|
|
|
|
|
|
th { padding: 10px; color: #f6fdff; background: linear-gradient(100deg, #0787bd, #075276); font-size: 14px; font-weight: 800; }
|
|
|
|
|
|
|
|
td { padding: 10px; color: #d7f1ff; background: rgba(1, 14, 26, 0.48); font-size: 13px; line-height: 1.65; vertical-align: middle; }
|
|
|
|
|
|
|
|
th:nth-child(1), td:nth-child(1) { width: 19%; color: #c8edfa; font-weight: 800; text-align: center; }
|
|
|
|
|
|
|
|
th:nth-child(2), td:nth-child(2) { width: 24%; color: #9fc4d4; }
|
|
|
|
|
|
|
|
textarea {
|
|
|
|
|
|
|
|
display: block; width: 100%; min-height: 72px; padding: 10px; 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; resize: vertical;
|
|
|
|
|
|
|
|
&:focus { border-color: rgba(92, 224, 255, .7); box-shadow: 0 0 0 3px rgba(0, 174, 255, .13); outline: none; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-drawing-section {
|
|
|
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
|
|
|
padding: 18px;
|
|
|
|
|
|
|
|
border: 1px solid rgba(84, 185, 228, 0.32);
|
|
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
|
|
background: linear-gradient(110deg, rgba(8, 66, 94, .32), rgba(0, 19, 34, .26));
|
|
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
h4 { margin: 0; color: #ffffff; font-size: 17px; text-align: left; }
|
|
|
|
|
|
|
|
p { margin: 9px 0 18px; color: #bfdeeb; font-size: 13px; line-height: 1.75; text-align: left; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.value-canvas-preview {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-image) { width: 116px; height: 76px; border: 1px solid rgba(92, 224, 255, .66); border-radius: 10px; cursor: zoom-in; }
|
|
|
|
|
|
|
|
div { display: grid; gap: 5px; }
|
|
|
|
|
|
|
|
strong { color: #eefcff; font-size: 14px; }
|
|
|
|
|
|
|
|
span { color: #8fb9c9; font-size: 12px; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.text-button { width: fit-content; padding: 0; border: 0; color: #77e4ff; background: transparent; cursor: pointer; font: inherit; font-size: 12px; text-decoration: underline; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.canvas-guidance {
|
|
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
margin-top: 22px;
|
|
|
|
|
|
|
|
padding: 15px 18px 15px 152px;
|
|
|
|
|
|
|
|
border-left: 3px solid #38bee7;
|
|
|
|
|
|
|
|
border-radius: 0 12px 12px 0;
|
|
|
|
|
|
|
|
background: rgba(3, 43, 65, .24);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
p { margin: 0; color: #bedce8; font-size: 13px; line-height: 1.75; }
|
|
|
|
|
|
|
|
p + p { margin-top: 4px; }
|
|
|
|
|
|
|
|
.template-download-button { position: absolute; top: 44px; left: 18px; }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-navigation {
|
|
|
|
.step-navigation {
|
|
|
|
display: flex;
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
justify-content: center;
|
|
|
|
@ -942,6 +1192,8 @@ function exportOutcome() {
|
|
|
|
writing-mode: vertical-rl;
|
|
|
|
writing-mode: vertical-rl;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.value-grid { grid-template-columns: 1fr; }
|
|
|
|
.value-grid { grid-template-columns: 1fr; }
|
|
|
|
|
|
|
|
.canvas-guidance { padding: 15px; }
|
|
|
|
|
|
|
|
.canvas-guidance .template-download-button { position: static; margin: 8px 0; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.workbench-head--actions-only {
|
|
|
|
.workbench-head--actions-only {
|
|
|
|
justify-content: flex-end;
|
|
|
|
justify-content: flex-end;
|
|
|
|
|