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/channel/supplier.vue

1163 lines
47 KiB
Vue

<!--
2 months ago
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2025-05-03 17:28:31
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-23 14:33:52
* @FilePath: \e-commerce-internet\src\views\channel\supplier.vue
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
-->
<template>
<div class="supplier student-training-shell">
<main class="training-core">
<section class="task-header">
<div class="task-badge"><el-icon><Connection /></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>
2 months ago
<div class="caseBacktitle">
<img src="@/assets/images/步骤.png" alt="" />
<span>任务目标</span>
</div>
<p>掌握供应商渠道评估的要素评分办法的设置学会使用线性权重法计算各供应渠道综合得分确定供应渠道级别并给出综合评估意见</p>
<div class="caseBacktitle">
<img src="@/assets/images/步骤.png" alt="" />
<span>任务背景</span>
</div>
<p>电商选品中常见的供应渠道包括以下类型品牌直供直接与品牌方或总代理合作确保正品授权和稳定供应批发市场如义乌小商品市场1688平台适合小批量采购和多样化选品代工厂/OEM定制化生产适合有独特设计需求的产品跨境电商平台如亚马逊全球开店速卖通提供国际供应链资源垂直类供应商专注于特定品类如母婴3C的供应商具备专业性和深度服务能力</p>
<el-table
:data="channelComparisonData"
border
style="width: 90%"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column prop="item" label="项目" align="center" width="120"></el-table-column>
<el-table-column prop="channelA" label="渠道A品牌直供" align="center"></el-table-column>
<el-table-column prop="channelB" label="渠道B批发市场" align="center"></el-table-column>
<el-table-column prop="channelC" label="渠道C代工厂/OEM" align="center"></el-table-column>
<el-table-column prop="channelD" label="渠道D跨境电商平台" align="center"></el-table-column>
<el-table-column prop="channelE" label="渠道E垂直类供应商" align="center"></el-table-column>
</el-table>
<div class="caseBacktitle">
<img src="@/assets/images/步骤.png" alt="" />
<span>任务要求</span>
</div>
<div class="caseBacktitle">
<span>任务1设置评估维度及权重</span>
</div>
<div>
<div style="display: flex; margin-top: 10px">
<el-table
:data="evaluationWeights"
border
style="width: 30%"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column prop="dimension" label="评估维度" align="center" width="120"></el-table-column>
<el-table-column prop="currentWeight" label="权重设置" align="center">
<template #default="{ row }">
<el-input v-if="row.dimension !== '合计'" v-model="row.currentWeight" placeholder="请输入内容"></el-input>
<span v-else>{{ total }}</span>
</template>
</el-table-column>
<el-table-column prop="recommendedRange" label="推荐设置" align="center"></el-table-column>
</el-table>
<el-button @click="onClick" class="ml50" type="primary">提交</el-button>
</div>
</div>
<div class="caseBacktitle">
<span>任务2设置评分标准</span>
</div>
<el-table
:data="evaluationCriteria"
border
style="width: 50%; margin-top: 10px"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column prop="category" label="项目" align="center" width="120"></el-table-column>
<el-table-column label="评分标准" align="left">
<template #default="{ row }">
<div v-for="standard in row.standards" :key="standard.score" class="standard-item">
<span>{{ standard.score }}{{ standard.description }}</span>
</div>
</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>
2 months ago
<div class="caseBacktitle">
<span>任务3供应渠道评分</span>
</div>
<div style="display: flex">
<el-table
:data="supplierEvaluationData"
border
style="width: 80%"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column prop="supplierType" label="供应渠道" align="center" width="120"></el-table-column>
<el-table-column prop="quality" label="产品质量(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.quality" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="priceCompetitiveness" label="价格竞争力(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.priceCompetitiveness" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="supplyStability" label="供货稳定性(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.supplyStability" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="reputation" label="合作信誉(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.reputation" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="afterSales" label="售后支持(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.afterSales" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="minOrderQuantity" label="最小订单量(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.minOrderQuantity" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="productDiversity" label="产品多样性(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.productDiversity" placeholder=""></el-input>
</template>
</el-table-column>
<el-table-column prop="logistics" label="物流配送(分)" align="center">
<template #default="{ row }">
<el-input v-model="row.logistics" placeholder=""></el-input>
</template>
</el-table-column>
</el-table>
<el-button class="ml50" type="primary" @click="onSubmit"></el-button>
</div>
</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>
2 months ago
<div class="caseBacktitle">
<span>任务4使用线性权重法计算各供应渠道综合得分</span>
</div>
<el-table
:data="supplierEvaluationData1"
border
style="width: 100%; margin-top: 10px"
:header-cell-style="{
color: '#fff',
fontSize: '14px',
backgroundColor: '#6A7DE9!important',
}"
>
<el-table-column prop="supplierType" label="供应渠道" align="center" width="120"></el-table-column>
<el-table-column label="产品质量" align="center">
<el-table-column prop="quality" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("产品质量") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="价格竞争力" align="center">
<el-table-column prop="priceCompetitiveness" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("价格竞争力") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="供货稳定性" align="center">
<el-table-column prop="supplyStability" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("供货稳定性") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="合作信誉" align="center">
<el-table-column prop="reputation" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("合作信誉") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="售后支持" align="center">
<el-table-column prop="afterSales" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("售后支持") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="最小订单量" align="center">
<el-table-column prop="minOrderQuantity" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("最小订单量") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="产品多样性" align="center">
<el-table-column prop="productDiversity" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("产品多样性") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="物流配送" align="center">
<el-table-column prop="logistics" label="得分" align="center"></el-table-column>
<el-table-column label="权重" align="center" prop="weight">
<template #default="{ row }">
{{ getWeightByDimension("物流配送") }}
</template>
</el-table-column>
</el-table-column>
<el-table-column label="综合得分" align="center" prop="weight">
<template #default="{ row }">
<el-input v-model="row.comprehensiveScore" placeholder=""></el-input>
</template>
</el-table-column>
</el-table>
</section>
<section v-show="isSectionVisible(4)" 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>
2 months ago
<div class="caseBacktitle">
<span>任务5选择供应渠道级别</span>
</div>
<div style="display: flex; gap: 5vw">
<div style="padding-left: 1vw">
<span>
根据综合得分可以将供应渠道分为以下级别
<br />
8分以上优秀供应渠道优先合作
<br />
7-8良好供应渠道可考虑合作
<br />
6-7一般供应渠道需谨慎合作
<br />
6分以下较差供应渠道不建议合作
<br />
</span>
</div>
<div>
<span>根据计算结果</span>
<div class="online">
<span>品牌直供{{ supplierEvaluationData1[0].comprehensiveScore || 0 }}</span>
<el-select placeholder="请选择" v-model="form.option1" @change="onChange">
<el-option v-for="item in option" :key="item" :value="item" :label="item"></el-option>
</el-select>
</div>
<div class="online mt10">
<span>垂直类供应商{{ supplierEvaluationData1[1].comprehensiveScore || 0 }}</span>
<el-select placeholder="请选择" v-model="form.option2" @change="onChange">
<el-option v-for="item in option" :key="item" :value="item" :label="item"></el-option>
</el-select>
</div>
<div class="online mt10">
<span>代工厂/OEM{{ supplierEvaluationData1[2].comprehensiveScore || 0 }}</span>
<el-select placeholder="请选择" v-model="form.option3" @change="onChange">
<el-option v-for="item in option" :key="item" :value="item" :label="item"></el-option>
</el-select>
</div>
<div class="online mt10">
<span>批发市场{{ supplierEvaluationData1[3].comprehensiveScore || 0 }}</span>
<el-select placeholder="请选择" v-model="form.option4" @change="onChange">
<el-option v-for="item in option" :key="item" :value="item" :label="item"></el-option>
</el-select>
</div>
<div class="online mt10">
<span>跨境电商平台{{ supplierEvaluationData1[4].comprehensiveScore || 0 }}</span>
<el-select placeholder="请选择" v-model="form.option5" @change="onChange">
<el-option v-for="item in option" :key="item" :value="item" :label="item"></el-option>
</el-select>
</div>
</div>
</div>
<div class="caseBacktitle">
<span>任务6给出综合评估意见</span>
</div>
<p style="padding-left: 1vw">
品牌直供综合得分最高产品质量和合作信誉表现优秀建议优先合作
<br />
垂直类供应商综合得分较高售后支持表现突出可作为主要合作渠道之一
<br />
代工厂/OEM综合得分中等产品多样性表现较好但价格竞争力一般可作为补充渠道
<br />
批发市场综合得分中等价格竞争力较强但产品质量和供货稳定性一般可作为备选渠道
<br />
跨境电商平台综合得分较低物流配送和供货稳定性表现一般建议谨慎合作或作为短期应急渠道
<br />
</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="" :progress-items="progressItems" />
2 months ago
</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";
2 months ago
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
import * as analysisAPI from "@/api/market-demand-mining.js";
import { ArrowLeft, ArrowRight, CircleCheck, Connection, Download } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
2 months ago
const { proxy } = getCurrentInstance();
let currentStep = ref(0);
const TASK_KEY = "supplier-evaluation";
const taskConfig = ref(null);
const activeStep = ref(1);
const saving = ref(false);
const taskTitle = computed(() => taskConfig.value?.taskName || "供应商评估与选择");
const taskBackground = computed(() => taskConfig.value?.background || "电商选品中存在品牌直供、批发市场、代工厂、跨境电商平台、垂直类供应商等多种供应渠道,需要综合评估各渠道能力。");
const taskRequirement = computed(() => taskConfig.value?.requirements || "掌握供应商渠道评估要素和评分办法,使用线性权重法计算各供应渠道综合得分,确定供应渠道级别并给出综合评估意见。");
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, ["掌握供应商渠道评估要素和评分办法", "能够使用线性权重法计算供应渠道综合得分", "形成供应渠道级别与综合评估意见"]));
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" : "" })));
2 months ago
const form = ref({
option1: "",
option2: "",
option3: "",
option4: "",
option5: "",
});
const option = ["优秀供应渠道", "良好供应渠道", "一般供应渠道", "较差供应渠道"];
const channelComparisonData = [
{
item: "产品质量",
channelA: "通常提供高质量产品,符合品牌标准",
channelB: "产品质量参差不齐,需仔细甄别",
channelC: "产品质量可靠,符合定制要求",
channelD: "产品质量一般,需仔细甄别",
channelE: "产品质量较高,专业领域优势明显",
},
{
item: "价格竞争力",
channelA: "价格较高,但具有一定市场竞争力",
channelB: "价格较低,具有明显竞争力",
channelC: "定制产品价格较高",
channelD: "价格具有一定的竞争力,但受关税影响",
channelE: "价格具有竞争力,专业采购渠道",
},
{
item: "供应链稳定性",
channelA: "通常有稳定的供应链",
channelB: "供应链稳定性一般,受市场因素影响较大",
channelC: "供应链稳定性较好,但受生产计划影响",
channelD: "国际运输存在不确定性",
channelE: "供应链稳定性高,专业供应链管理",
},
{
item: "合作信誉",
channelA: "品牌方信誉良好,长期合作关系",
channelB: "信誉一般,需谨慎选择供应商",
channelC: "信誉良好,专业生产定制产品",
channelD: "平台信誉较好,但供应商个体差异大",
channelE: "信誉良好,专业服务",
},
{
item: "售后支持",
channelA: "提供全面的售后服务",
channelB: "售后服务一般,部分供应商提供有限支持",
channelC: "提供一定的售后服务,但可能有限",
channelD: "提供一定的售后服务,但受国际贸易影响",
channelE: "提供全面的售后服务",
},
{
item: "最小订单量",
channelA: "通常要求一定的最小订单量",
channelB: "支持小批量采购",
channelC: "通常要求较高的最小订单量",
channelD: "支持一件代发",
channelE: "最小订单量适中",
},
{
item: "产品多样性",
channelA: "产品种类相对有限,专注于品牌产品",
channelB: "产品种类丰富,选择多样",
channelC: "可根据需求定制多样化产品",
channelD: "产品种类丰富",
channelE: "专注于特定品类,种类较为丰富",
},
{
item: "物流配送",
channelA: "物流服务较为高效",
channelB: "物流服务较为便捷,但运费可能较高",
channelC: "物流服务一般,需自行安排",
channelD: "国际物流时间长,运费高",
channelE: "物流服务较为高效",
},
];
const evaluationWeights = ref([
{
dimension: "产品质量",
currentWeight: null,
recommendedRange: "0.15~0.25",
min: 0.15,
max: 0.25,
},
{
dimension: "价格竞争力",
currentWeight: null,
recommendedRange: "0.10~0.20",
min: 0.1,
max: 0.2,
},
{
dimension: "供货稳定性",
currentWeight: null,
recommendedRange: "0.10~0.20",
min: 0.1,
max: 0.2,
},
{
dimension: "合作信誉",
currentWeight: null,
recommendedRange: "0.10~0.20",
min: 0.1,
max: 0.2,
},
{
dimension: "售后支持",
currentWeight: null,
recommendedRange: "0.05~0.15",
min: 0.05,
max: 0.15,
},
{
dimension: "最小订单量",
currentWeight: null,
recommendedRange: "0.05~0.15",
min: 0.05,
max: 0.15,
},
{
dimension: "产品多样性",
currentWeight: null,
recommendedRange: "0.05~0.15",
min: 0.05,
max: 0.15,
},
{
dimension: "物流配送",
currentWeight: null,
recommendedRange: "0.05~0.15",
min: 0.05,
max: 0.15,
},
// 合计行(特殊标记)
{
dimension: "合计",
currentWeight: 1.0,
recommendedRange: "(权重合计为1)",
isTotal: true,
},
]);
const evaluationCriteria = [
{
category: "产品质量",
standards: [
{ score: 10, description: "产品符合或超过行业标准,质量稳定,无缺陷" },
{ score: 9, description: "产品质量良好,偶尔有小问题,但不影响使用" },
{ score: 7, description: "产品质量一般,可能存在小问题,但总体可接受" },
{ score: 5, description: "产品质量较差,存在较多问题,但基本可用" },
{ score: 3, description: "产品质量差,存在严重问题,不建议使用" },
],
},
{
category: "价格竞争力",
standards: [
{ score: 10, description: "价格远低于市场平均水平,竞争力极高" },
{ score: 9, description: "价格低于市场平均水平,具有一定竞争力" },
{ score: 7, description: "价格与市场平均水平持平,竞争力一般" },
{ score: 5, description: "价格高于市场平均水平,竞争力较低" },
],
},
{
category: "供货稳定性",
standards: [
{ score: 10, description: "供货非常稳定,从未出现延迟或缺货" },
{ score: 9, description: "供货较为稳定,偶尔有轻微延迟" },
{ score: 7, description: "供货稳定性一般,有时会出现延迟或缺货" },
{ score: 5, description: "供货不稳定,经常出现延迟或缺货" },
],
},
{
category: "合作信誉",
standards: [
{ score: 10, description: "销售表现优秀,客户评价极高" },
{ score: 9, description: "销售良好,偶尔有小问题,但总体可靠" },
{ score: 7, description: "销售一般,存在一些客户投诉,但问题不大" },
{ score: 5, description: "销售较差,存在较多客户投诉或不良记录" },
],
},
{
category: "售后支持",
standards: [
{ score: 10, description: "售后服务优秀,响应及时,解决问题迅速" },
{ score: 9, description: "售后服务较好,响应及时,但可能有轻微不足" },
{ score: 7, description: "售后服务一般,响应时间较长,但基本满足需求" },
{ score: 5, description: "售后服务较差,响应缓慢,客户满意度低" },
],
},
{
category: "最小订单量",
standards: [
{ score: 10, description: "无最低要求,支持单件代发" },
{ score: 9, description: "最小订单量较低如10-20件" },
{ score: 7, description: "最小订单量适中如30-50件" },
{ score: 5, description: "最小订单量较高如100-200件" },
],
},
{
category: "产品多样性",
standards: [
{ score: 10, description: "产品种类丰富,涵盖多个类别和款式" },
{ score: 9, description: "产品种类较多,但主要集中在某一类别" },
{ score: 7, description: "产品种类一般,有一定多样性,但不够丰富" },
{ score: 5, description: "产品种类较少,缺乏多样性" },
],
},
{
category: "物流配送",
standards: [
{ score: 10, description: "物流配送速度快,运费低,覆盖范围广" },
{ score: 9, description: "物流配送较快,运费适中,覆盖范围较广" },
{ score: 7, description: "物流配送一般,运费适中,覆盖范围有限" },
{ score: 5, description: "物流配送较慢,运费较高,覆盖范围有限" },
{ score: 3, description: "物流配送非常慢,运费高,覆盖范围小" },
],
},
];
const supplierEvaluationData = ref([
{
supplierType: "品牌直供",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
},
{
supplierType: "批发市场",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
},
{
supplierType: "代工厂/OEM",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
},
{
supplierType: "跨境电商平台",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
},
{
supplierType: "垂直类供应商",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
},
]);
// 映射字段字典
const weightFieldMapping = {
产品质量: "qualityWeight",
价格竞争力: "priceWeight",
供货稳定性: "stabilityWeight",
合作信誉: "reputationWeight",
售后支持: "supportWeight",
最小订单量: "moqWeight",
产品多样性: "varietyWeight",
物流配送: "logisticsWeight",
};
const scoreFieldMapping = {
qualityWeight: "产品质量",
priceWeight: "价格竞争力",
stabilityWeight: "供货稳定性",
reputationWeight: "合作信誉",
supportWeight: "售后支持",
moqWeight: "最小订单量",
varietyWeight: "产品多样性",
logisticsWeight: "物流配送",
};
const comprehensiveScore = {
crossborderScore: null,
directSupplyScore: null,
oemScore: null,
verticalScore: null,
wholesaleScore: null,
};
const mapping = {
跨境电商平台: "crossborderScore",
品牌直供: "directSupplyScore",
"代工厂/OEM": "oemScore",
垂直类供应商: "verticalScore",
批发市场: "wholesaleScore",
};
const mappingkey = {
crossborderScore: "跨境电商平台",
directSupplyScore: "品牌直供",
oemScore: "代工厂/OEM",
verticalScore: "垂直类供应商",
wholesaleScore: "批发市场",
};
// 提取权重
const getWeightMap = () => {
const map = {};
evaluationWeights.value.forEach((item) => {
if (!item.isTotal) {
const key = weightFieldMapping[item.dimension];
if (key) {
map[key] = item.currentWeight;
}
}
});
return map;
};
const supplierEvaluationData1 = ref([
{
supplierType: "品牌直供",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
comprehensiveScore: "",
weight: "",
type: "",
},
{
supplierType: "批发市场",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
comprehensiveScore: "",
weight: "",
},
{
supplierType: "代工厂/OEM",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
comprehensiveScore: "",
weight: "",
},
{
supplierType: "跨境电商平台",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
comprehensiveScore: "",
weight: "",
},
{
supplierType: "垂直类供应商",
quality: null,
priceCompetitiveness: null,
supplyStability: null,
reputation: null,
afterSales: null,
minOrderQuantity: null,
productDiversity: null,
logistics: null,
comprehensiveScore: "",
weight: "",
},
]);
const total = computed(() => {
return evaluationWeights.value.filter((item) => !item.isTotal).reduce((sum, item) => sum + parseFloat(Number(item.currentWeight).toFixed(2)), 0);
});
// 权重提交
const onClick = () => {
// 需要判断权重是否在推荐范围内
const isValid = evaluationWeights.value.every((item) => {
if (item.dimension === "合计") return true;
const weight = parseFloat(item.currentWeight);
return weight >= item.min && weight <= item.max;
});
if (isValid && total.value === 1) {
proxy.$modal.msgSuccess("权重设置成功");
supplierEvaluationData1.value.forEach((item, index) => {
item.weight = evaluationWeights.value[index].currentWeight;
});
currentStep.value = 1;
} else {
proxy.$modal.msgWarning("权重设置超出范围,请重新设置");
}
};
// 计算综合得分
const onSubmit = () => {
if (currentStep.value < 1) {
proxy.$modal.msgWarning("请先完成权重设置");
return;
}
// 查看全部填写完毕了吗
const isAllFilled = supplierEvaluationData.value.every((item) => {
return item.quality !== "" && item.priceCompetitiveness !== "" && item.supplyStability !== "" && item.reputation !== "" && item.afterSales !== "" && item.minOrderQuantity !== "" && item.productDiversity !== "" && item.logistics !== "";
});
if (!isAllFilled) {
proxy.$modal.msgWarning("请填写完整");
return;
}
proxy.$modal.msgSuccess("提交成功");
currentStep.value = 3;
syncData();
};
const onInput = (index) => {
if (currentStep.value < 3) {
proxy.$modal.msgWarning("请先供应渠道评分");
supplierEvaluationData1.value[index].comprehensiveScore = "";
return;
}
currentStep.value = 4;
};
const onChange = (index) => {
if (currentStep.value < 3) {
proxy.$modal.msgWarning("请先供应渠道综合得分计算");
for (const key in form.value) {
form.value[key] = "";
}
return;
}
};
// 同步数据方法
const syncData = () => {
supplierEvaluationData1.value.forEach((item1) => {
const matchingItem = supplierEvaluationData.value.find((item2) => item2.supplierType === item1.supplierType);
if (matchingItem) {
item1.quality = matchingItem.quality;
item1.priceCompetitiveness = matchingItem.priceCompetitiveness;
item1.supplyStability = matchingItem.supplyStability;
item1.reputation = matchingItem.reputation;
item1.afterSales = matchingItem.afterSales;
item1.minOrderQuantity = matchingItem.minOrderQuantity;
item1.productDiversity = matchingItem.productDiversity;
item1.logistics = matchingItem.logistics;
}
});
};
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,
form: form.value,
evaluationWeights: evaluationWeights.value,
supplierEvaluationData: supplierEvaluationData.value,
supplierEvaluationData1: supplierEvaluationData1.value,
currentStep: currentStep.value,
updatedAt: new Date().toISOString(),
};
}
function applyProgress(value) {
if (!value || typeof value !== "object") return;
activeStep.value = normalizeActiveStep(value.activeStep);
form.value = { ...form.value, ...(value.form || {}) };
if (Array.isArray(value.evaluationWeights)) evaluationWeights.value = value.evaluationWeights;
if (Array.isArray(value.supplierEvaluationData)) supplierEvaluationData.value = value.supplierEvaluationData;
if (Array.isArray(value.supplierEvaluationData1)) supplierEvaluationData1.value = value.supplierEvaluationData1;
currentStep.value = Number(value.currentStep) || currentStep.value;
}
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 () => {
2 months ago
// 判断form是否全部填写
const isFormFilled = Object.values(form.value).every((value) => value !== "");
if (!isFormFilled) {
proxy.$modal.msgWarning("请先完成实训!");
return;
}
supplierEvaluationData1.value.forEach((item) => {
const key = mapping[item.supplierType];
if (key) {
comprehensiveScore[key] = item.comprehensiveScore;
}
});
let params = {
...getWeightMap(),
...comprehensiveScore,
userId: userStore.userInfo.userId,
channelScores: JSON.stringify({
list: supplierEvaluationData.value,
form: form.value,
}),
};
if (saving.value) return;
saving.value = true;
try {
await analysisAPI.supplierEvaluationAndSelectionSubmit(params);
await persistProgress("SUBMIT");
} finally { saving.value = false; }
2 months ago
};
// 回显方法
const echo = async () => {
try {
const res = await analysisAPI.supplierEvaluationAndSelectionEcho({ userId: userStore.userInfo.userId });
2 months ago
if (!res.data) return;
let data = JSON.parse(res.data.channelScores);
supplierEvaluationData.value = data.list;
form.value = data.form;
Object.entries(scoreFieldMapping).forEach(([key, value]) => {
const match = evaluationWeights.value.find((item) => item.dimension === value);
if (match) {
match.currentWeight = res.data[key];
}
});
Object.entries(mappingkey).forEach(([key, value]) => {
const match = supplierEvaluationData1.value.find((item) => item.supplierType === value);
if (match) {
match.comprehensiveScore = res.data[key];
}
});
supplierEvaluationData1.value.forEach((item, index) => {
item.weight = evaluationWeights.value[index].currentWeight;
});
currentStep.value = 5;
} catch (error) {
// Existing task data is optional when no prior submission exists.
}
2 months ago
};
//重新实训
const resetTask = async () => {
if (saving.value) return;
saving.value = true;
try {
await analysisAPI.supplierEvaluationAndSelectionReTrain({ userId: userStore.userInfo.userId });
2 months ago
proxy.$modal.msgSuccess("重新实训成功");
// 清空数据
evaluationWeights.value.forEach((item) => {
item.currentWeight = "";
});
supplierEvaluationData1.value.forEach((item) => {
item.quality = "";
item.priceCompetitiveness = "";
item.supplyStability = "";
item.reputation = "";
item.afterSales = "";
item.minOrderQuantity = "";
item.productDiversity = "";
item.logistics = "";
item.comprehensiveScore = "";
item.weight = "";
item.type = "";
});
supplierEvaluationData.value.forEach((item) => {
item.quality = "";
item.priceCompetitiveness = "";
item.supplyStability = "";
item.reputation = "";
item.afterSales = "";
item.minOrderQuantity = "";
item.productDiversity = "";
item.logistics = "";
});
for (const key in form.value) {
form.value[key] = "";
}
currentStep.value = 0;
activeStep.value = 1;
await persistProgress("RESET", true);
proxy.$modal.msgSuccess("清空填写成功");
} finally { saving.value = false; }
2 months ago
};
// 获取权重的函数
function getWeightByDimension(label) {
if (!currentStep.value == 2) return;
const match = evaluationWeights.value.find((item) => item.dimension === label);
return match ? match.currentWeight : "-";
}
onMounted(async () => {
await loadTaskConfig();
await echo();
await loadSavedProgress();
2 months ago
});
</script>
<style lang="scss" scoped>
.supplier {
--supplier-panel: rgba(4, 31, 46, 0.78);
--supplier-panel-strong: rgba(3, 20, 31, 0.95);
--supplier-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(--supplier-line) !important;
background: var(--supplier-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(--supplier-line);
border-radius: 12px;
background: var(--supplier-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) {
min-height: 36px;
border: 1px solid rgba(76, 169, 211, 0.48);
border-radius: 7px;
box-shadow: none !important;
background: var(--supplier-panel-strong);
}
:deep(.el-input__inner) {
color: #ebf8ff;
}
:deep(.el-input__inner::placeholder) {
color: #7194a8;
}
:deep(.el-input__wrapper.is-focus) {
border-color: #4edbff;
box-shadow: 0 0 0 3px rgba(50, 205, 255, 0.12) !important;
}
: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;
}
2 months ago
.online {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid rgba(44, 141, 182, 0.3);
border-radius: 10px;
background: rgba(1, 17, 27, 0.3);
}
.online span {
width: auto;
color: #d7ecff;
}
.online :deep(.el-select) {
min-width: 180px;
flex: 1 1 180px;
}
.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;
2 months ago
}
}
.supplier {
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%; margin-top: 12px; border-radius: 18px; }
:deep(.el-table th.el-table__cell) { background: rgba(8, 55, 82, .98) !important; }
:deep(.el-input__wrapper) { min-height: 42px; border-radius: 14px; background: #0f1a24; }
:deep(.el-select__wrapper) { min-height: 42px; border: 1px solid rgba(45, 95, 126, .95); border-radius: 14px; color: #eef5ff; 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) { .supplier { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .supplier { 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; } } }
2 months ago
</style>