You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/product/price.vue

840 lines
40 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2025-04-28 15:05:56
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-08 17:17:01
* @FilePath: \e-commerce-internet\src\views\product\price.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="price student-training-shell">
<main class="training-core">
<section class="task-header"><div class="task-badge"><el-icon><Money /></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 workbench-head--actions-only"><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="hasConfiguredSteps" class="step-indicator" aria-label="产品定价步骤"><template v-for="(step,index) in configuredSteps" :key="`${step}-${index}`"><button type="button" :class="['step-tab',{active:activeStep===index+1,complete:activeStep>index+1}]" :aria-current="activeStep===index+1?'step':undefined" :aria-label="`第${index+1}步:${step}`" @click="setCurrentStep(index+1)"><span class="step-marker">{{ String(index+1).padStart(2,'0') }}</span><span class="step-copy"><strong class="step-name" :title="step">{{ step }}</strong><span class="step-action">{{ activeStep===index+1?'当前步骤':'点击切换' }} <span aria-hidden="true">→</span></span></span></button><span v-if="index<configuredSteps.length-1" class="step-connector" aria-hidden="true"></span></template></nav>
<section v-show="isSectionVisible(1)" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<div class="caseBacktitle">
<img src="@/assets/images/步骤.png" alt="" />
<span>一、知识学习</span>
</div>
<p>
PSM分析技术的理论基础
<br />
在开发新产品时,厂商对产品价格的看法和消费者对产品价格的知觉经常会不一致,因此厂商往往不愿意事先设定产品的价格,而希望能通过调查从消费者那里得到产品价格的可接受范围。
<br />
价格敏感度测试(Price Sensitivity Measure ment,PSM)不需要预先给定好价格,而是让每位被访者自己表示他们的可接受价格范围。
<br />
目的:可用于新产品开发、产品线的扩张(产品线内产品项目的增加)、产品范围的扩张(增加产品线)等情况下,确定产品价格的可接受值或范围。
<br />
样本的选择:对于产品的开发,必须从所有可能使用新产品的消费总体中抽取有代表性的样本。对于产品线或产品范围的扩张,样本可以从原来的品牌使用者中抽取。
<br />
</p>
<div class="caseBacktitle">
<img src="@/assets/images/步骤.png" alt="" />
<span>二、实验实训</span>
</div>
<p>假如淘宝某店铺计划推出一款针对大学生群体的洗面奶,您对下列价格区间有何想法?</p>
<el-table
:data="priceList1"
border
style="width: 60%"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column label="价格区间" align="center" prop="label"></el-table-column>
<el-table-column label="太便宜,可能有陷阱" align="center" prop="value">
<template #default="{ row }">
<el-radio v-model="row.value" label="1" class="hidden-label"></el-radio>
</template>
</el-table-column>
<el-table-column label="很划算,肯定会购买" align="center" prop="value">
<template #default="{ row }">
<el-radio v-model="row.value" label="2" class="hidden-label"></el-radio>
</template>
</el-table-column>
<el-table-column label="有点贵,还可以接受" align="center" prop="value">
<template #default="{ row }">
<el-radio v-model="row.value" label="3" class="hidden-label"></el-radio>
</template>
</el-table-column>
<el-table-column label="太贵,直接放弃" align="center" prop="value">
<template #default="{ row }">
<el-radio v-model="row.value" label="4" class="hidden-label"></el-radio>
</template>
</el-table-column>
</el-table>
</section><section v-show="isSectionVisible(2)" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<p>洗面奶价格梯度表,累计百分比用百分数表示,保留两位小数。</p>
<el-table
:data="priceList2"
border
style="width: 80%"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column label="价格区间" align="center" prop="label"></el-table-column>
<el-table-column label="太便宜,可能有陷阱" align="center">
<el-table-column label="" align="center" prop="pricevalue1"></el-table-column>
<el-table-column label="累计总和" align="center" prop="toocheapsum1">
<template #default="{ row }">
<el-input v-model="row.toocheapsum1" placeholder="" />
</template>
</el-table-column>
<el-table-column label="累计百分比" align="center" prop="percentage1">
<template #default="{ row }">
<el-input v-model="row.percentage1" placeholder="">
<template #suffix>%</template>
</el-input>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="很划算,肯定会购买" align="center">
<el-table-column label="" align="center" prop="pricevalue2"></el-table-column>
<el-table-column label="累计总和" align="center" prop="toocheapsum2">
<template #default="{ row }">
<el-input v-model="row.toocheapsum2" placeholder="" />
</template>
</el-table-column>
<el-table-column label="累计百分比" align="center" prop="percentage2">
<template #default="{ row }">
<el-input v-model="row.percentage2" placeholder="">
<template #suffix>%</template>
</el-input>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="有点贵,还可以接受" align="center">
<el-table-column label="" align="center" prop="pricevalue3"></el-table-column>
<el-table-column label="累计总和" align="center" prop="toocheapsum3">
<template #default="{ row }">
<el-input v-model="row.toocheapsum3" placeholder="" />
</template>
</el-table-column>
<el-table-column label="累计百分比" align="center" prop="percentage3">
<template #default="{ row }">
<el-input v-model="row.percentage3" placeholder="">
<template #suffix>%</template>
</el-input>
</template>
</el-table-column>
</el-table-column>
<el-table-column label="太贵,直接放弃" align="center">
<el-table-column label="" align="center" prop="pricevalue4"></el-table-column>
<el-table-column label="累计总和" align="center" prop="toocheapsum4">
<template #default="{ row }">
<el-input v-model="row.toocheapsum4" placeholder="" />
</template>
</el-table-column>
<el-table-column label="累计百分比" align="center" prop="percentage4">
<template #default="{ row }">
<el-input v-model="row.percentage4" placeholder="">
<template #suffix>%</template>
</el-input>
</template>
</el-table-column>
</el-table-column>
</el-table>
</section><section v-show="isSectionVisible(3)" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div>
<p>太便宜和很划算向下累积,有点贵和太贵向上累积,画出每一个价格下开始觉得太便宜、便宜划算,开始觉得贵、太贵的累积人数百分比曲线图。</p>
<div class="cnetent">
<div class="left">
<el-button @click="draw" style="margin-bottom: 20px">绘制</el-button>
<div id="main" ref="main" style="width: 100%; height: 50vh"></div>
</div>
<div class="right">
<div style="display: flex">
<div class="online">
<span>最优价格点:</span>
<el-input style="width: 100px" type="text" v-model="formInline.optimalPrice" />
</div>
<div class="online ml20">
<span>可接受价格点:</span>
<el-input style="width: 100px" type="text" v-model="formInline.acceptablePrice" />
</div>
</div>
<div class="online mt20">
<span>可接受价格范围:</span>
<el-input style="width: 100px" type="text" v-model="formInline.range1" />
<span class="p10">----------</span>
<el-input style="width: 100px" type="text" v-model="formInline.range2" />
</div>
<p>PSM模型在产品定价过程中的作用以及不足:</p>
<el-input v-model="textarea2" :autosize="{ minRows: 11, maxRows: 11 }" type="textarea" placeholder="" />
</div>
</div>
</section><section v-show="hasConfiguredSteps && activeStep > 3" class="step-card"><div class="step-title-row"><div><p class="section-eyebrow">Step {{ String(activeStep).padStart(2,'0') }}</p><h3>{{ activeStepName }}</h3></div></div><p>请复核价格梯度、PSM 曲线、最优价格点和可接受价格范围后,保存或提交实训成果。</p></section>
<div v-if="hasConfiguredSteps" class="step-navigation"><button type="button" class="btn-nav" :disabled="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="activeStep===configuredSteps.length" @click="goNextStep">下一步<el-icon><ArrowRight /></el-icon></button></div>
<div class="taskfooter"><el-button @click="submitTask" :loading="saving">提交任务</el-button><el-button @click="resetTask" :loading="saving">清空填写</el-button></div>
</section>
</main>
<TrainingAiSidebar study-tip="当前任务建议:完成价格敏感度数据整理,绘制 PSM 曲线并给出最优与可接受价格范围。" :progress-items="progressItems" />
</div>
</template>
<script setup>
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import * as echarts from "echarts";
import * as analysisAPI from "@/api/market-demand-mining.js";
import { ArrowLeft, ArrowRight, CircleCheck, Download, Money } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
const { proxy } = getCurrentInstance();
import useUserStore from "@/store/modules/user";
import popModel from "@/views/components/popModal.vue";
const userStore = useUserStore();
const TASK_KEY = "product-pricing";
const taskConfig = ref(null);
const activeStep = ref(1);
const saving = ref(false);
const taskTitle = computed(() => taskConfig.value?.taskName || "产品定价");
const taskBackground = computed(() => taskConfig.value?.background || "PSM 价格敏感度分析通过价格区间问卷数据,帮助产品团队确定合理的定价区间。" );
const taskRequirement = computed(() => taskConfig.value?.requirements || "使用 PSM 模型完成价格梯度整理、曲线分析与定价结论输出。" );
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, ["理解 PSM 价格敏感度模型", "能够完成价格梯度与累计百分比计算", "能够给出合理定价区间"]));
const configuredSteps = computed(() => parseTrainingArray(taskConfig.value?.steps));
const hasConfiguredSteps = computed(() => configuredSteps.value.length > 0);
const activeStepName = computed(() => configuredSteps.value[activeStep.value - 1] || "产品定价");
const progressItems = computed(() => configuredSteps.value.map((text, index) => ({ text, status: index + 1 < activeStep.value ? "done" : index + 1 === activeStep.value ? "doing" : "" })));
const formInline = ref({
optimalPrice: "",
acceptablePrice: "",
range1: "",
range2: "",
});
const textarea2 = ref("");
const dialogVisible = ref(false);
const reTraining = () => {
proxy.$message.success("重新实训");
};
onMounted(() => {});
const main = ref(null);
const priceList1 = ref([
{
label: "15元以内",
value: "",
},
{
label: "15-20元",
value: "",
},
{
label: "20-30元",
value: "",
},
{
label: "30-50元",
value: "",
},
{
label: "50-70元",
value: "",
},
{
label: "70-100元",
value: "",
},
{
label: "100-200元",
value: "",
},
{
label: "200元以上",
value: "",
},
]);
const priceList2 = ref([
{
label: "15元以内",
pricevalue1: "126",
pricevalue2: "56",
pricevalue3: "0",
pricevalue4: "0",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "15-20元",
pricevalue1: "85",
pricevalue2: "102",
pricevalue3: "0",
pricevalue4: "0",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "20-30元",
pricevalue1: "36",
pricevalue2: "198",
pricevalue3: "5",
pricevalue4: "0",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "30-50元",
pricevalue1: "2",
pricevalue2: "85",
pricevalue3: "30",
pricevalue4: "0",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "50-70元",
pricevalue1: "0",
pricevalue2: "25",
pricevalue3: "98",
pricevalue4: "8",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "70-100元",
pricevalue1: "0",
pricevalue2: "2",
pricevalue3: "126",
pricevalue4: "35",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "100-200元",
pricevalue1: "0",
pricevalue2: "0",
pricevalue3: "159",
pricevalue4: "89",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
{
label: "200元以上",
pricevalue1: "0",
pricevalue2: "0",
pricevalue3: "109",
pricevalue4: "192",
toocheapsum1: "",
toocheapsum2: "",
toocheapsum3: "",
toocheapsum4: "",
percentage1: "",
percentage2: "",
percentage3: "",
percentage4: "",
},
]);
// 校验空值并只提示一次
function validateAndAlertOnce() {
let hasEmptyValue = false; // 标记是否发现空值
// 遍历数组检查空值
priceList2.value.forEach((item) => {
Object.keys(item).forEach((key) => {
if (item[key] === "") {
hasEmptyValue = true; // 发现空值,标记为 true
// 可选择替换空值(如果需要)
item[key] = ""; // 替换为空值时的默认值
}
});
});
// 只在发现空值时提示一次
if (hasEmptyValue) {
// alert("数据中存在空值,已自动替换为默认值 0。");
proxy.$modal.msgWarning("请填写完整数据!");
return false; // 返回 false,表示存在空值
}
return true; // 没有空值,返回 true
}
const draw = () => {
const isValid = validateAndAlertOnce();
if (!isValid) return;
const minPicture = priceList2.value.map((item) => item.percentage1);
const calculatePrice = priceList2.value.map((item) => item.percentage2);
const minexpensive = priceList2.value.map((item) => item.percentage3);
const expensive = priceList2.value.map((item) => item.percentage4);
const priceRanges = ['15元以内',"15-20元", "20-30元", "30-50元", "50-70元", "70-100元", "100-200元", "200元以上"];
var myChart = echarts.init(main.value);
myChart.setOption({
title: {
text: "PSM价格数据分析",
left: "center",
},
tooltip: {
trigger: "axis",
},
legend: {
data: ["太便宜", "很划算", "有点贵", "太贵了"],
top: 30,
},
xAxis: {
type: "category",
name: "价格区间",
data: priceRanges,
},
yAxis: {
type: "value",
name: "销售数量 (件)",
min: 0,
max: 100,
},
series: [
{
name: "太便宜",
type: "line",
data: minPicture,
color: "#1f77b4",
symbol: "circle",
symbolSize: 8,
},
{
name: "太贵了",
type: "line",
data: expensive,
color: "#d62728",
symbol: "circle",
symbolSize: 8,
},
{
name: "有点贵",
type: "line",
data: minexpensive,
color: "#2ca02c",
symbol: "circle",
symbolSize: 8,
},
{
name: "很划算",
type: "line",
data: calculatePrice,
color: "#ff7f0e",
symbol: "circle",
symbolSize: 8,
},
],
grid: {
left: "2%",
right: "15%",
bottom: "10%",
containLabel: true,
},
graphic: [
{
type: "text",
left: 391, // 或 "60%" 等
top: 334,
z: 100, // 设置更高的图层层级
style: {
text: "P4",
font: "bold 14px sans-serif",
fill: "#2ca02c",
// 显示在线条墙上面
distance: 10,
},
},
{
type: "text",
left: 300, // 或 "60%" 等
top: 380,
z: 100, // 设置更高的图层层级
symbol: "circle",
style: {
text: "P1",
font: "bold 14px sans-serif",
fill: "#1f77b4",
// 显示在线条墙上面
distance: 10,
},
},
{
type: "text",
left: 350, // 或 "60%" 等
top: 388,
z: 100, // 设置更高的图层层级
symbol: "circle",
style: {
text: "P2",
font: "bold 14px sans-serif",
fill: "#d62728",
// 显示在线条墙上面
distance: 10,
},
},
{
type: "text",
left: 430, // 或 "60%" 等
top: 380,
z: 100, // 设置更高的图层层级
symbol: "circle",
style: {
text: "P3",
font: "bold 14px sans-serif",
fill: "#ff7f0e",
// 显示在线条墙上面
distance: 10,
},
},
],
});
};
function normalizeActiveStep(value) { if (!configuredSteps.value.length) return 1; return Math.min(Math.max(Number(value) || 1, 1), configuredSteps.value.length); }
function isSectionVisible(section) { return !hasConfiguredSteps.value || activeStep.value === section; }
function buildProgress() { return { activeStep: activeStep.value, formInline: formInline.value, textarea2: textarea2.value, priceList1: priceList1.value, priceList2: priceList2.value, updatedAt: new Date().toISOString() }; }
function applyProgress(value) { if (!value || typeof value !== "object") return; activeStep.value = normalizeActiveStep(value.activeStep); formInline.value = { ...formInline.value, ...(value.formInline || {}) }; textarea2.value = value.textarea2 || ""; if (Array.isArray(value.priceList1)) priceList1.value = value.priceList1; if (Array.isArray(value.priceList2)) priceList2.value = value.priceList2; }
async function persistProgress(saveAction = "SAVE", silent = false) { await saveStudentTrainingAnswer(TASK_KEY, { saveAction, currentStep: activeStep.value, step1Answer: JSON.stringify(buildProgress()) }); if (!silent) proxy?.$modal?.msgSuccess(saveAction === "SUBMIT" ? "提交成功" : "保存成功"); }
function setCurrentStep(step) { activeStep.value = normalizeActiveStep(step); persistProgress("SAVE", true).catch(() => {}); }
function goPreviousStep() { setCurrentStep(activeStep.value - 1); }
function goNextStep() { setCurrentStep(activeStep.value + 1); }
async function saveCurrentProgress() { if (saving.value) return; saving.value = true; try { await persistProgress(); } finally { saving.value = false; } }
async function loadTaskConfig() { try { const res = await getTrainingTaskByKey(TASK_KEY); taskConfig.value = res?.data || null; activeStep.value = normalizeActiveStep(activeStep.value); } catch (error) { taskConfig.value = null; activeStep.value = 1; } }
async function loadSavedProgress() { try { const res = await getStudentTrainingAnswer(TASK_KEY); const raw = res?.data?.step1Answer; if (raw) applyProgress(JSON.parse(raw)); } catch (error) { /* Historical progress is optional. */ } }
function exportOutcome() { const blob = new Blob([JSON.stringify(buildProgress(), 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); }
const submitTask = async () => {
const minPicture = priceList2.value.map((item) => item.percentage1);
const minPriceValue = priceList2.value.map((item) => item.toocheapsum1);
const calculatePrice = priceList2.value.map((item) => item.percentage2);
const calculatevalue = priceList2.value.map((item) => item.toocheapsum2);
const minexpensive = priceList2.value.map((item) => item.percentage3);
const minexpensivevalue = priceList2.value.map((item) => item.toocheapsum3);
const expensive = priceList2.value.map((item) => item.percentage4);
const tooSum = priceList2.value.map((item) => item.toocheapsum4);
// 判断数组是否为空
if (minPicture.includes("") || minPriceValue.includes("") || calculatePrice.includes("") || calculatevalue.includes("") || minexpensive.includes("") || minexpensivevalue.includes("") || expensive.includes("") || priceList1.value.some((item) => !item.value)) {
return proxy.$modal.msgWarning("请填写完整数据");
}
// 判断formInline的值是否为空
if (formInline.value.optimalPrice == "" || formInline.value.acceptablePrice == "" || formInline.value.range1 == "" || formInline.value.range2 == "") {
return proxy.$modal.msgWarning("请填写完整数据");
}
const params = {
acceptablePrice: formInline.value.acceptablePrice,
acceptablePriceRange: formInline.value.range1 + "," + formInline.value.range2,
optimalPrice: formInline.value.optimalPrice,
cheapPercentage: minPicture.join(","),
cheapSum: minPriceValue.join(","),
context: textarea2.value,
costEffectivePercentage: calculatePrice.join(","),
costEffectiveSum: calculatevalue.join(","),
expensivePercentage: minexpensive.join(","),
expensiveSum: minexpensivevalue.join(","),
tooExpensivePercentage: expensive.join(","),
tooSum: tooSum.join(","),
priceRange: JSON.stringify(priceList1.value),
displayStatus: 1,
};
if (saving.value) return;
saving.value = true;
try { await analysisAPI.productPricingSubmit({ stuAnswer: params, userId: userStore.userInfo.userId }); await persistProgress("SUBMIT"); } finally { saving.value = false; }
};
// 回显接口
const loadTaskInfo = async () => {
try {
const res = await analysisAPI.productPricingEcho({ userId: userStore.userInfo.userId });
if (!res.data) return;
formInline.value.optimalPrice = res.data.optimalPrice;
formInline.value.acceptablePrice = res.data.acceptablePrice;
formInline.value.range1 = res.data.acceptablePriceRange.split(",")[0];
formInline.value.range2 = res.data.acceptablePriceRange.split(",")[1];
textarea2.value = res.data.context;
priceList1.value = JSON.parse(res.data.priceRange !== "" ? res.data.priceRange : priceList1.value);
priceList2.value.forEach((item, index) => {
item.toocheapsum1 = res.data.cheapSum.split(",")[index];
item.toocheapsum2 = res.data.costEffectiveSum.split(",")[index];
item.toocheapsum3 = res.data.expensiveSum.split(",")[index];
item.percentage1 = res.data.cheapPercentage.split(",")[index];
item.percentage2 = res.data.costEffectivePercentage.split(",")[index];
item.percentage3 = res.data.expensivePercentage.split(",")[index];
item.percentage4 = res.data.tooExpensivePercentage.split(",")[index];
item.toocheapsum4 = res.data.tooSum.split(",")[index];
});
draw();
} catch (error) { /* Existing submission is optional. */ }
};
// 重新实训
const resetTask = async () => {
if (saving.value) return;
saving.value = true;
try {
await analysisAPI.productPricingReTrain({ userId: userStore.userInfo.userId, module: "产品定价" });
proxy.$modal.msgSuccess("重新实训成功");
// 清楚数据
formInline.value.optimalPrice = "";
formInline.value.acceptablePrice = "";
formInline.value.range1 = "";
formInline.value.range2 = "";
textarea2.value = "";
// 清除数据
priceList1.value.forEach((item) => {
item.value = "";
});
priceList2.value.forEach((item) => {
item.toocheapsum1 = "";
item.toocheapsum2 = "";
item.toocheapsum3 = "";
item.toocheapsum4 = "";
item.percentage1 = "";
item.percentage2 = "";
item.percentage3 = "";
item.percentage4 = "";
});
activeStep.value = 1;
await persistProgress("RESET", true);
proxy.$modal.msgSuccess("清空填写成功");
} finally { saving.value = false; }
};
onMounted(async () => {
await loadTaskConfig();
await loadTaskInfo();
await loadSavedProgress();
});
</script>
<style lang="scss" scoped>
.price {
--price-panel: rgba(4, 31, 46, 0.78);
--price-panel-strong: rgba(3, 20, 31, 0.95);
--price-line: rgba(54, 174, 221, 0.42);
min-height: calc(100vh - 84px);
padding: 0 0 32px;
color: #d7ecff;
background:
radial-gradient(circle at 10% 0%, rgba(15, 129, 189, 0.16), transparent 34%),
linear-gradient(180deg, rgba(3, 17, 27, 0.94), rgba(5, 19, 30, 0.98));
:deep(.training-step-area) {
margin-top: 0 !important;
border-color: var(--price-line) !important;
background: var(--price-panel) !important;
box-shadow: inset 0 1px 0 rgba(129, 231, 255, 0.12), 0 18px 38px rgba(0, 0, 0, 0.2) !important;
}
:deep(.el-table) {
--el-table-border-color: rgba(63, 164, 207, 0.36);
--el-table-header-bg-color: #0b577f;
--el-table-header-text-color: #edfbff;
--el-table-row-hover-bg-color: rgba(15, 107, 149, 0.32);
--el-table-bg-color: rgba(2, 20, 31, 0.82);
--el-table-tr-bg-color: rgba(2, 20, 31, 0.82);
--el-table-text-color: #d7ecff;
overflow: hidden;
margin-top: 12px;
border: 1px solid var(--price-line);
border-radius: 12px;
background: var(--price-panel-strong);
}
:deep(.el-table::before),
:deep(.el-table__inner-wrapper::before) {
background-color: transparent;
}
:deep(.el-table th.el-table__cell) {
color: #edfbff !important;
background: linear-gradient(180deg, #0f76a9, #0a567e) !important;
font-weight: 800;
}
:deep(.el-table td.el-table__cell),
:deep(.el-table tr) {
color: #d7ecff !important;
background: rgba(2, 20, 31, 0.82) !important;
}
:deep(.el-table .cell) {
line-height: 1.65;
}
:deep(.el-input__wrapper),
:deep(.el-textarea__inner) {
border: 1px solid rgba(76, 169, 211, 0.48);
border-radius: 7px;
color: #ebf8ff;
box-shadow: none !important;
background: var(--price-panel-strong);
}
:deep(.el-input__wrapper) {
min-height: 36px;
}
:deep(.el-input__inner),
:deep(.el-textarea__inner) {
color: #ebf8ff;
}
:deep(.el-input__inner::placeholder),
:deep(.el-textarea__inner::placeholder) {
color: #7194a8;
}
:deep(.el-input__wrapper.is-focus),
:deep(.el-textarea__inner:focus) {
border-color: #4edbff;
box-shadow: 0 0 0 3px rgba(50, 205, 255, 0.12) !important;
}
:deep(.el-radio__inner) {
border-color: #48bfe8;
background: rgba(3, 20, 31, 0.9);
}
:deep(.el-radio__input.is-checked .el-radio__inner) {
border-color: #4edbff;
background: #159ac9;
}
:deep(.el-button) {
border-color: rgba(55, 174, 221, 0.72) !important;
color: #e9faff !important;
background: linear-gradient(180deg, #168ab9, #0c628d) !important;
}
:deep(.el-button:hover) {
border-color: #65e5ff !important;
color: #ffffff !important;
background: linear-gradient(180deg, #1b9ece, #0f709f) !important;
}
:deep(.hidden-label) {
.el-radio__label {
display: none;
}
}
.online {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1px solid rgba(44, 141, 182, 0.3);
border-radius: 10px;
background: rgba(1, 17, 27, 0.3);
}
.cnetent {
display: flex;
gap: 24px;
align-items: stretch;
padding: 18px;
border: 1px solid rgba(44, 141, 182, 0.3);
border-radius: 12px;
background: rgba(1, 17, 27, 0.24);
.left {
width: 50%;
height: 100%;
}
.right {
width: 50%;
height: 100%;
}
}
.taskfooter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 28px;
margin-top: 32px;
padding-top: 28px;
border-top: 1px solid rgba(55, 174, 221, 0.24);
}
.taskfooter :deep(.el-button) {
min-width: 172px;
height: 56px;
border-radius: 8px;
font-size: 18px;
font-weight: 800;
}
@media (max-width: 960px) {
.cnetent {
flex-direction: column;
.left,
.right {
width: 100%;
}
}
}
}
.price {
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,.25); border-radius: 36px; background: rgba(8,18,28,.6); box-shadow: inset 0 1px 0 rgba(169,229,255,.08),0 22px 48px rgba(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,.55); border-radius: 999px; color: #dff5ff; background: rgba(17,126,178,.36); font-size: 12px; font-weight: 700; }.task-title { margin: 12px 0 0; color: #fff; font-size: 30px; font-weight: 900; line-height: 1.3; }.task-card,.step-card { border: 1px solid rgba(82,174,226,.42); border-radius: 26px; background: rgba(0,25,42,.64); }.task-card { margin-top: 28px; padding: 18px 22px 22px; }.workbench-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid rgba(0,180,255,.2); }.section-eyebrow { margin: 0; color: #71dfff; font-size: 12px; font-weight: 800; line-height: 1.4; }.workbench-head h3 { margin: 4px 0 8px; color: #fff; font-size: 26px; font-weight: 800; line-height: 1.3; }.workbench-description { 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,.55); border-radius: 999px; color: #e9fbff; background: rgba(17,126,178,.36); font: inherit; font-size: 14px; font-weight: 800; cursor: pointer; }.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,.3); border-radius: 999px; background: rgba(0,20,35,.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.active { border-color: rgba(92,224,255,.7); color: #fff; background: linear-gradient(95deg,#0a6b9e,#0a5c86); }.step-card { margin-top: 32px; padding: 18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top: 0; }.step-title-row { margin-bottom: 14px; }.step-title-row h3 { margin: 4px 0 0; color: #fff; font-size: 22px; font-weight: 900; line-height: 1.3; }.caseBacktitle { display: flex; align-items: center; gap: 8px; margin: 20px 0 10px; color: #dff5ff; font-size: 16px; font-weight: 800; }.caseBacktitle img { display: none; }.step-card > p { color: #d7ecff; font-size: 15px; line-height: 1.8; }:deep(.el-table) { width: 100% !important; max-width: 100%; border-radius: 18px; }:deep(.el-table th.el-table__cell) { background: rgba(8,55,82,.98) !important; }:deep(.el-input__wrapper),:deep(.el-textarea__inner) { border-radius: 14px; background: #0f1a24; }.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,.7); color: #fff; background: linear-gradient(95deg,#0b84bd,#096491); }.btn-nav:disabled { cursor: not-allowed; opacity: .45; }.taskfooter { gap: 52px; margin-top: 36px; padding-top: 0; border-top: 0; }.taskfooter :deep(.el-button) { min-width: 172px; height: 64px; border: 1px solid #2c83aa !important; border-radius: 6px; color: #dff7ff !important; background: rgba(13,64,88,.62) !important; font-size: 24px; font-weight: 900; }
}
.price {
.workbench-actions { margin-left: auto; }
.step-indicator { display:flex; align-items:center; gap:12px; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid rgba(0,180,255,.3); border-radius:18px; background:linear-gradient(110deg,rgba(4,39,58,.9),rgba(2,20,33,.94)); box-shadow:inset 0 1px 0 rgba(169,229,255,.08); }
.step-tab { display:flex; flex:1 0 145px; align-items:center; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; transition:transform .2s,color .2s,background .2s,border-color .2s,box-shadow .2s; }
.step-tab:hover,.step-tab:focus-visible { transform:translateY(-2px); border-color:rgba(89,223,255,.82); color:#fff; background:rgba(8,92,131,.52); box-shadow:0 8px 18px rgba(0,0,0,.2); outline:none; }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(95deg,#0a6b9e,#0a5c86); box-shadow:0 8px 20px rgba(0,120,170,.22); }
.step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid rgba(113,223,255,.55); border-radius:50%; color:#bdefff; background:rgba(3,27,43,.72); font-size:13px; font-weight:900; }
.step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.step-copy span { color:#7ec7e7; font-size:12px; font-weight:700; }.step-tab.active .step-marker { border-color:#a8f3ff; color:#fff; background:linear-gradient(135deg,#0bb2ed,#0a6b9e); box-shadow:0 0 0 4px rgba(77,215,255,.13),0 0 18px rgba(77,215,255,.4); }.step-tab.complete .step-marker { border-color:#59e0ca; color:#dffbff; background:rgba(13,132,119,.5); }.step-connector { flex:0 0 28px; height:2px; background:rgba(77,167,220,.54); color:#78cfe9; font-size:16px; line-height:0; text-align:right; }.step-connector.complete { background:#55ddcc; color:#75f0df; }
}
@media (max-width:1100px) { .price { grid-template-columns:1fr; } } @media (max-width:900px) { .price { 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,.taskfooter :deep(.el-button) { width:100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; }.taskfooter { gap:14px; }.cnetent { flex-direction:column; .left,.right { width:100%; } } } }
.step-tab.active { border-color:rgba(89,226,255,.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,.78),rgba(5,62,102,.64)); box-shadow:0 0 18px rgba(24,183,235,.22),inset 0 1px 0 rgba(176,246,255,.2); }.step-tab.active .step-marker { border-color:#72e8ff; color:#fff; background:linear-gradient(135deg,#08b7e8,#1375d0); box-shadow:0 0 0 5px rgba(25,179,237,.14),0 0 18px rgba(46,208,255,.58); }.step-tab.complete { color:#dffbff; }.step-tab.complete .step-marker { border-color:rgba(84,230,221,.85); color:#062b37; background:#6ef0e1; box-shadow:0 0 12px rgba(79,231,218,.35); }.step-connector.complete { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); color:#63e6f1; box-shadow:0 0 9px rgba(53,218,235,.46); }
.price .step-indicator { position:relative; display:flex; align-items:center; gap:0; width:calc(100% + 24px); margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border-radius:18px; background:linear-gradient(90deg,rgba(6,42,61,.58),rgba(1,18,31,.42)),rgba(0,0,0,.3); }
.price .step-tab { display:flex; flex:1 0 145px; align-items:center; min-width:0; min-height:66px; padding:8px 10px; border:1px solid rgba(68,141,177,.35); border-radius:8px; color:#9ab3d0; background:rgba(3,27,43,.52); text-align:left; }
.price .step-tab.active { border-color:rgba(89,226,255,0.85); color:#fff; background:linear-gradient(115deg,rgba(4,109,155,0.78),rgba(5,62,102,0.64)); box-shadow:0 0 18px rgba(24,183,235,.22); }
.price .step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.price .step-name { display:-webkit-box; overflow:hidden; font-size:14px; line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.price .step-action { color:#66b6d4; font-size:10px; font-weight:800; }.price .step-connector { position:relative; flex:0 1 40px; min-width:18px; height:2px; margin:0 8px; overflow:visible; background:rgba(98,169,201,.34); }.price .step-connector::after { position:absolute; top:50%; right:-1px; width:6px; height:6px; border-top:1px solid #73b1ce; border-right:1px solid #73b1ce; content:""; transform:translateY(-50%) rotate(45deg); }.price .step-tab.complete + .step-connector { background:linear-gradient(90deg,#6ef0e1,#1cb8e9); }
@media (max-width:900px) { .price .step-indicator { flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; }.price .step-tab { flex:0 0 auto; width:100%; }.price .step-connector { flex:0 0 20px; min-width:0; width:2px; height:20px; margin-left:29px; } }
.workbench-head--actions-only {
justify-content: flex-end;
margin-bottom: 18px;
padding-bottom: 0;
border-bottom: 0;
}
</style>