|
|
|
|
|
<!--
|
|
|
|
|
|
* @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"><div><p class="section-eyebrow">Training Task</p><h3>任务实训</h3><p class="workbench-description">{{ taskRequirement }}</p></div><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="产品定价步骤"><button v-for="(step,index) in configuredSteps" :key="`${step}-${index}`" type="button" class="step-tab" :class="{active:activeStep===index+1}" :aria-current="activeStep===index+1?'step':undefined" @click="setCurrentStep(index+1)">{{ step }}</button></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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@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 { flex-direction:column; border-radius:26px; }.step-tab { flex:none; }.taskfooter { gap:14px; }.cnetent { flex-direction:column; .left,.right { width:100%; } } } }
|
|
|
|
|
|
</style>
|