|
|
<template>
|
|
|
<div class="release-channel-page student-training-shell">
|
|
|
<main class="release-channel-core">
|
|
|
<section class="task-header">
|
|
|
<div class="task-head-main">
|
|
|
<h1 class="task-title">{{ taskTitle }}</h1>
|
|
|
<p class="task-brief">
|
|
|
<span class="task-brief-text">{{ taskBackground }}</span>
|
|
|
<button type="button" class="task-brief-toggle" @click="briefOpen = !briefOpen">
|
|
|
{{ briefOpen ? "收起" : "展开目标与要求" }}
|
|
|
</button>
|
|
|
<span v-if="briefOpen" class="task-brief-more">
|
|
|
<b>实训目标</b>
|
|
|
<ul><li v-for="goal in taskGoals" :key="goal">{{ goal }}</li></ul>
|
|
|
<b>实训要求</b>
|
|
|
<p>{{ taskRequirement }}</p>
|
|
|
</span>
|
|
|
</p>
|
|
|
</div>
|
|
|
<TrainingMaterialButton
|
|
|
class="release-channel-toolbar"
|
|
|
:material-name="taskConfig?.materialName"
|
|
|
:material-url="taskConfig?.materialUrl"
|
|
|
:training-data-name="taskConfig?.trainingDataName"
|
|
|
:training-data-url="taskConfig?.trainingDataUrl"
|
|
|
:guide-manual-name="taskConfig?.guideManualName"
|
|
|
:guide-manual-url="taskConfig?.guideManualUrl"
|
|
|
:ai-manual-name="taskConfig?.aiManualName"
|
|
|
:ai-manual-url="taskConfig?.aiManualUrl"
|
|
|
/>
|
|
|
</section>
|
|
|
|
|
|
<section class="task-card">
|
|
|
<header class="workbench-head">
|
|
|
<div>
|
|
|
<p class="section-eyebrow">STEP {{ String(activeStep).padStart(2, "0") }}</p>
|
|
|
<h2>{{ stepTitle }}</h2>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<nav class="step-tabs" aria-label="产品发布渠道评估步骤">
|
|
|
<button type="button" :class="['step-tab', { active: activeStep === 1, complete: activeStep > 1 }]" :aria-current="activeStep === 1 ? 'step' : undefined" @click="activeStep = 1">
|
|
|
<span class="step-marker">01</span><span class="step-copy"><b>权重设置</b><small>{{ activeStep === 1 ? '当前步骤 →' : '点击切换 →' }}</small></span>
|
|
|
</button>
|
|
|
<span class="step-connector" aria-hidden="true" />
|
|
|
<button type="button" :class="['step-tab', { active: activeStep === 2, complete: activeStep > 2 }]" :aria-current="activeStep === 2 ? 'step' : undefined" @click="activeStep = 2">
|
|
|
<span class="step-marker">02</span><span class="step-copy"><b>渠道数据</b><small>{{ activeStep === 2 ? '当前步骤 →' : '点击切换 →' }}</small></span>
|
|
|
</button>
|
|
|
<span class="step-connector" aria-hidden="true" />
|
|
|
<button type="button" :class="['step-tab', { active: activeStep === 3, complete: activeStep > 3 }]" :aria-current="activeStep === 3 ? 'step' : undefined" @click="openStepThree">
|
|
|
<span class="step-marker">03</span><span class="step-copy"><b>归一化评分</b><small>{{ activeStep === 3 ? '当前步骤 →' : '点击切换 →' }}</small></span>
|
|
|
</button>
|
|
|
<span class="step-connector" aria-hidden="true" />
|
|
|
<button type="button" :class="['step-tab', { active: activeStep === 4 }]" :aria-current="activeStep === 4 ? 'step' : undefined" @click="openStepFour">
|
|
|
<span class="step-marker">04</span><span class="step-copy"><b>渠道排名</b><small>{{ activeStep === 4 ? '当前步骤 →' : '点击切换 →' }}</small></span>
|
|
|
</button>
|
|
|
</nav>
|
|
|
|
|
|
<section v-if="activeStep === 1">
|
|
|
<p class="lead">不同维度对新品渠道效果的影响不一样,请给以下核心评估维度设置权重,按照重要性分配权重,总权重合计 100%。</p>
|
|
|
<div class="table-actions">
|
|
|
<el-button type="primary" @click="addWeightRow"><el-icon><Plus /></el-icon>增加一行</el-button>
|
|
|
<el-button @click="weightRows.pop()" :disabled="!weightRows.length"><el-icon><Minus /></el-icon>删除一行</el-button>
|
|
|
</div>
|
|
|
<div class="table-wrap">
|
|
|
<table class="weight-table">
|
|
|
<thead><tr><th>评估维度</th><th>维度说明</th><th>权重占比(%)</th><th class="operation-column">操作</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in weightRows" :key="row.id">
|
|
|
<td><el-input v-model="row.dimension" maxlength="50" placeholder="填写评估维度" /></td>
|
|
|
<td><el-input v-model="row.description" maxlength="200" placeholder="填写维度说明" /></td>
|
|
|
<td><el-input-number v-model="row.weight" :min="0" :max="100" :precision="2" :step="1" controls-position="right" /><small v-if="row.suggestion">建议权重:{{ row.suggestion }}</small></td>
|
|
|
<td class="operation-column"><el-button link type="danger" @click="weightRows.splice(index, 1)">删除本行</el-button></td>
|
|
|
</tr>
|
|
|
<tr v-if="!weightRows.length"><td colspan="4" class="empty-row">暂无评估维度,请增加一行后填写。</td></tr>
|
|
|
</tbody>
|
|
|
<tfoot><tr><th colspan="2">当前权重合计</th><td :class="{ 'total-error': weightTotal !== 100 }">{{ displayWeightTotal }}%</td><td></td></tr></tfoot>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p class="hint">默认展示建议的评估维度和建议权重范围,具体权重需由学生自行填写。可修改任意单元格,也可逐行新增、删除;点击下一步时系统校验所有权重之和必须等于 100%。</p>
|
|
|
<footer class="footer-actions">
|
|
|
<el-button :loading="saving" @click="saveWeightStep">保存当前进度</el-button>
|
|
|
<el-button type="primary" :loading="validating" @click="goStepTwo">下一步<el-icon><ArrowRight /></el-icon></el-button>
|
|
|
</footer>
|
|
|
</section>
|
|
|
|
|
|
<section v-else-if="activeStep === 2">
|
|
|
<p class="lead">根据案例资料提供的 5 大渠道近 3 个月数据,汇总填写下表。</p>
|
|
|
<div class="table-actions">
|
|
|
<el-button type="primary" @click="addChannelRow"><el-icon><Plus /></el-icon>增加一行</el-button>
|
|
|
<el-button @click="channelRows.pop()" :disabled="!channelRows.length"><el-icon><Minus /></el-icon>删除一行</el-button>
|
|
|
<el-button type="primary" plain @click="exportChannelRows"><el-icon><Download /></el-icon>导出</el-button>
|
|
|
</div>
|
|
|
<div class="table-wrap">
|
|
|
<table class="channel-table">
|
|
|
<thead><tr><th>渠道</th><th>日均总搜索量</th><th>供需比</th><th>平均点击率(%)</th><th>平均CPC(元)</th><th>平均转化率(%)</th><th>现有相关访客数</th><th>匹配度(1-100)</th><th class="operation-column">操作</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in channelRows" :key="row.id">
|
|
|
<td><el-input v-model="row.channel" maxlength="30" placeholder="填写渠道" /></td>
|
|
|
<td><el-input-number v-model="row.dailySearchVolume" :min="0" :precision="0" controls-position="right" /></td>
|
|
|
<td><el-input-number v-model="row.supplyDemandRatio" :min="0" :precision="4" :step="0.1" controls-position="right" /></td>
|
|
|
<td><el-input-number v-model="row.averageClickRate" :min="0" :max="100" :precision="2" controls-position="right" /></td>
|
|
|
<td><el-input-number v-model="row.averageCpc" :min="0" :precision="2" controls-position="right" /></td>
|
|
|
<td><el-input-number v-model="row.averageConversionRate" :min="0" :max="100" :precision="2" controls-position="right" /></td>
|
|
|
<td><el-input-number v-model="row.existingVisitorCount" :min="0" :precision="0" controls-position="right" /></td>
|
|
|
<td><el-input-number v-model="row.matchingScore" :min="1" :max="100" :precision="0" controls-position="right" /></td>
|
|
|
<td class="operation-column"><el-button link type="danger" @click="channelRows.splice(index, 1)">删除本行</el-button></td>
|
|
|
</tr>
|
|
|
<tr v-if="!channelRows.length"><td colspan="9" class="empty-row">暂无渠道数据,请增加一行后填写。</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p class="hint">默认显示天猫、京东、抖音、拼多多、唯品会 5 个渠道;可继续新增或删除任意渠道。保存当前进度不拦截,点击下一步时由系统校验全部指标。</p>
|
|
|
<footer class="footer-actions">
|
|
|
<el-button @click="activeStep = 1">上一步</el-button>
|
|
|
<el-button :loading="saving" @click="saveChannelStep">保存当前进度</el-button>
|
|
|
<el-button type="primary" :loading="validating" @click="validateChannelStep">下一步<el-icon><ArrowRight /></el-icon></el-button>
|
|
|
</footer>
|
|
|
</section>
|
|
|
|
|
|
<section v-else-if="activeStep === 3">
|
|
|
<p class="scoring-guide">通过 Min-Max 归一法将各维度原始数据转换为 0–10 分标准化得分。正向指标(搜索量、点击率、转化率、访客数、匹配度)按“渠道值 − 最小值”计算;逆向指标(供需比、CPC)按“最大值 − 渠道值”计算。请根据前两步数据手工填写得分与加权得分。</p>
|
|
|
<div class="table-actions"><el-button type="primary" plain @click="exportScoreRows"><el-icon><Download /></el-icon>导出</el-button></div>
|
|
|
<div class="table-wrap scoring-table-wrap">
|
|
|
<table class="scoring-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th rowspan="2">渠道</th>
|
|
|
<th v-for="metric in scoringMetrics" :key="metric.key" colspan="2">{{ metric.label }}</th>
|
|
|
<th rowspan="2">加权得分</th>
|
|
|
</tr>
|
|
|
<tr>
|
|
|
<template v-for="metric in scoringMetrics" :key="`${metric.key}-sub`"><th>得分</th><th>权重</th></template>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
<tr v-for="row in scoreRows" :key="row.id">
|
|
|
<th scope="row">{{ row.channel }}</th>
|
|
|
<template v-for="metric in row.dimensions" :key="metric.key">
|
|
|
<td><el-input-number v-model="metric.score" :min="0" :max="10" :precision="1" :step="0.1" controls-position="right" /></td>
|
|
|
<td class="weight-cell">{{ metric.weight ?? "—" }}%</td>
|
|
|
</template>
|
|
|
<td><el-input-number v-model="row.weightedScore" :min="0" :precision="2" :step="0.01" controls-position="right" /></td>
|
|
|
</tr>
|
|
|
<tr v-if="!scoreRows.length"><td :colspan="2 + scoringMetrics.length * 2" class="empty-row">请先完成并保存第二步渠道数据。</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<p class="hint">渠道名称同步第二步,权重同步第一步。每项得分乘以对应权重(按百分比折算)并相加得到加权得分;系统会依据已保存的前两步数据检测计算结果。</p>
|
|
|
<p class="step-three-notice">说明:渠道来自第二步,权重来自第一步;每个维度得分乘以权重后相加即为加权得分。计算结果不一致时仍可进入下一步,系统会记录检测结果,供 AI 助评扣分参考。</p>
|
|
|
<section class="step-three-materials" aria-label="归一化评分参考资料">
|
|
|
<p>参考方法及工具如下:下载案例数据表,使用 Excel 按 Min-Max 公式计算各渠道归一化得分,可参考《实训操作说明》完成。</p>
|
|
|
<el-button type="primary" class="step-three-materials__button" @click="downloadPracticeGuide"><el-icon><Download /></el-icon>下载实训操作说明</el-button>
|
|
|
</section>
|
|
|
<footer class="footer-actions">
|
|
|
<el-button @click="activeStep = 2">上一步</el-button>
|
|
|
<el-button :loading="saving" @click="saveScoreStep">保存当前进度</el-button>
|
|
|
<el-button type="primary" :loading="validating" @click="validateScoreStep">下一步<el-icon><ArrowRight /></el-icon></el-button>
|
|
|
</footer>
|
|
|
</section>
|
|
|
|
|
|
<section v-else>
|
|
|
<p class="lead">根据步骤一设置的权重和步骤三的归一化得分,填写各渠道加权综合得分并排序,明确前两位核心渠道。</p>
|
|
|
<div class="table-wrap ranking-table-wrap">
|
|
|
<table class="ranking-table">
|
|
|
<thead><tr><th>排名</th><th>渠道</th><th>加权综合得分</th><th>是否选为核心渠道</th></tr></thead>
|
|
|
<tbody>
|
|
|
<tr v-for="(row, index) in rankingRows" :key="row.id">
|
|
|
<td class="rank-cell">{{ index + 1 }}</td>
|
|
|
<td><el-input v-model="row.channel" maxlength="30" placeholder="填写渠道" /></td>
|
|
|
<td><el-input-number v-model="row.weightedScore" :min="0" :precision="2" :step="0.01" controls-position="right" /></td>
|
|
|
<td class="core-cell"><span :class="{ core: index < 2 }">{{ index < 2 ? "是" : "否" }}</span></td>
|
|
|
</tr>
|
|
|
<tr v-if="!rankingRows.length"><td colspan="4" class="empty-row">请先完成并保存第三步归一化评分。</td></tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
<label class="analysis-field"><span>请根据以上计算结果,结合案例文档内容,分析所选核心渠道的优势及运营建议:</span><el-input v-model="coreChannelAnalysis" type="textarea" :autosize="{ minRows: 7, maxRows: 14 }" placeholder="填写核心渠道优势、资源投入重点和运营建议" /></label>
|
|
|
<p class="hint">渠道数量同步第三步,排名自动从 1 往下排列。渠道与加权综合得分需由学生填写,并与第三步数据逐项一致;得分必须按从高到低排序,前两名自动显示为核心渠道。</p>
|
|
|
<footer class="footer-actions">
|
|
|
<el-button @click="activeStep = 3">上一步</el-button>
|
|
|
<el-button :loading="saving" @click="saveRankingStep">保存当前进度</el-button>
|
|
|
<el-button type="primary" :loading="validating" @click="validateRankingStep">提交任务</el-button>
|
|
|
</footer>
|
|
|
</section>
|
|
|
</section>
|
|
|
</main>
|
|
|
<TrainingAiSidebar :study-tip="'先确定可解释的评估权重,再完整汇总各渠道近三个月数据。'" :progress-items="progressItems" />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
import { computed, onMounted, ref } from "vue";
|
|
|
import { ArrowRight, Document, Download, Minus, Plus } from "@element-plus/icons-vue";
|
|
|
import { ElMessage } from "element-plus";
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, validateReleaseChannelEvaluationStepOne, validateReleaseChannelEvaluationStepTwo, validateReleaseChannelEvaluationStepThree, validateReleaseChannelEvaluationStepFour } from "@/api/studentTrainingAnswer";
|
|
|
import { parseTrainingArray } from "@/views/training/taskKeyMap";
|
|
|
|
|
|
const TASK_KEY = "release-channel-evaluation";
|
|
|
const RELEASE_CHANNEL_GUIDE_URL = "/file/training-materials/release-channel-evaluation/21.实训操作说明.docx";
|
|
|
const activeStep = ref(1);
|
|
|
const briefOpen = ref(false);
|
|
|
const taskConfig = ref(null);
|
|
|
const saving = ref(false);
|
|
|
const validating = ref(false);
|
|
|
let nextId = 1;
|
|
|
const makeRow = (values = {}) => ({ id: nextId++, ...values });
|
|
|
const defaultWeightRows = () => [
|
|
|
["市场规模潜力", "对应关键词日均总搜索量,代表渠道用户需求大盘空间", "20~25"], ["竞争激烈程度", "供需比(在线商品数/日均搜索量),数值越低渠道竞争越宽松", "15~20"], ["流量点击效率", "品类平均点击率,代表渠道用户对该品类的点击意愿强弱", "12~18"], ["流量获取成本", "平均CPC,数值越低获取精准用户的推广成本越低", "15~20"], ["流量转化效率", "品类平均转化率,代表渠道用户的最终下单意愿", "12~18"], ["现有用户基础", "店铺现有该品类相关访客数,代表品牌存量用户承接能力", "5~10"], ["匹配度", "渠道属性与产品定位的适配度打分(1-10分)", "5~10"],
|
|
|
].map(([dimension, description, suggestion]) => makeRow({ dimension, description, weight: undefined, suggestion }));
|
|
|
const defaultChannelRows = () => ["天猫", "京东", "抖音", "拼多多", "唯品会"].map((channel) => makeRow({ channel }));
|
|
|
const weightSuggestions = Object.fromEntries(defaultWeightRows().map((row) => [row.dimension, row.suggestion]));
|
|
|
const weightRows = ref(defaultWeightRows());
|
|
|
const channelRows = ref(defaultChannelRows());
|
|
|
const scoreRows = ref([]);
|
|
|
const rankingRows = ref([]);
|
|
|
const coreChannelAnalysis = ref("");
|
|
|
const scoringMetrics = [
|
|
|
{ key: "marketScale", dimension: "市场规模潜力", label: "市场规模得分" },
|
|
|
{ key: "competition", dimension: "竞争激烈程度", label: "竞争程度得分" },
|
|
|
{ key: "click", dimension: "流量点击效率", label: "点击效率得分" },
|
|
|
{ key: "cost", dimension: "流量获取成本", label: "获客成本得分" },
|
|
|
{ key: "conversion", dimension: "流量转化效率", label: "转化效率得分" },
|
|
|
{ key: "userBase", dimension: "现有用户基础", label: "用户基础得分" },
|
|
|
{ key: "match", dimension: "匹配度", label: "匹配度得分" },
|
|
|
];
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "产品发布渠道评估");
|
|
|
const stepTitle = computed(() => ["评估维度与权重设置", "渠道基础数据汇总", "Min-Max 归一化与加权评分", "渠道综合排名与核心渠道选择"][activeStep.value - 1] || "产品发布渠道评估");
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || "结合新品定位与渠道特征,为产品发布选择更合适的投放渠道。");
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || "设置渠道评估维度与权重,并汇总渠道基础数据。");
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives, ["理解渠道评估的核心维度", "能够按重要性配置评估权重", "汇总渠道数据并完成评估基础"]));
|
|
|
const weightTotal = computed(() => Number(weightRows.value.reduce((sum, row) => sum + (Number(row.weight) || 0), 0).toFixed(6)));
|
|
|
const displayWeightTotal = computed(() => Number.isInteger(weightTotal.value) ? String(weightTotal.value) : weightTotal.value.toFixed(2));
|
|
|
const progressItems = computed(() => parseTrainingArray(taskConfig.value?.steps).map((text, index) => ({ text, status: index + 1 < activeStep.value ? "done" : index + 1 === activeStep.value ? "doing" : "" })));
|
|
|
const weightPayload = () => ({ rows: weightRows.value.map(({ dimension, description, weight }) => ({ dimension, description, weight })) });
|
|
|
const channelPayload = () => ({ rows: channelRows.value.map(({ channel, dailySearchVolume, supplyDemandRatio, averageClickRate, averageCpc, averageConversionRate, existingVisitorCount, matchingScore }) => ({ channel, dailySearchVolume, supplyDemandRatio, averageClickRate, averageCpc, averageConversionRate, existingVisitorCount, matchingScore })) });
|
|
|
const scorePayload = () => ({ rows: scoreRows.value.map((row) => ({ channel: row.channel, weightedScore: row.weightedScore, dimensions: row.dimensions.map(({ dimension, score, weight }) => ({ dimension, score, weight })) })) });
|
|
|
const scoreAnswerPayload = (calculationCheck = {}) => ({ ...scorePayload(), calculationCheck: {
|
|
|
calculationCorrect: calculationCheck.calculationCorrect === true,
|
|
|
totalItemCount: Number(calculationCheck.totalItemCount) || 0,
|
|
|
incorrectItemCount: Number(calculationCheck.incorrectItemCount) || 0,
|
|
|
incorrectItems: Array.isArray(calculationCheck.incorrectItems) ? calculationCheck.incorrectItems : [],
|
|
|
} });
|
|
|
const rankingPayload = () => ({ rows: rankingRows.value.map(({ channel, weightedScore }) => ({ channel, weightedScore })), coreChannelAnalysis: coreChannelAnalysis.value });
|
|
|
function addWeightRow() { weightRows.value.push(makeRow({ dimension: "", description: "", weight: undefined, suggestion: "" })); }
|
|
|
function addChannelRow() { channelRows.value.push(makeRow({ channel: "" })); }
|
|
|
async function saveAnswer(currentStep, fields) { await saveStudentTrainingAnswer(TASK_KEY, { saveAction: "SAVE", currentStep, ...fields }); }
|
|
|
async function saveWeightStep() { if (saving.value) return; saving.value = true; try { await saveAnswer(1, { step1Answer: JSON.stringify(weightPayload()) }); ElMessage.success("当前进度已保存"); } finally { saving.value = false; } }
|
|
|
async function goStepTwo() { if (validating.value) return; validating.value = true; try { await validateReleaseChannelEvaluationStepOne(weightPayload()); await saveAnswer(2, { step1Answer: JSON.stringify(weightPayload()) }); activeStep.value = 2; ElMessage.success("权重校验通过,已进入第二步"); } catch (error) { ElMessage.error(error?.message || "请检查评估维度和权重设置"); } finally { validating.value = false; } }
|
|
|
async function saveChannelStep() { if (saving.value) return; saving.value = true; try { await saveAnswer(2, { step2Answer: JSON.stringify(channelPayload()) }); ElMessage.success("当前进度已保存"); } finally { saving.value = false; } }
|
|
|
async function validateChannelStep() { if (validating.value) return; validating.value = true; try { await validateReleaseChannelEvaluationStepTwo(channelPayload()); await saveAnswer(3, { step2Answer: JSON.stringify(channelPayload()) }); syncScoreRows(); activeStep.value = 3; ElMessage.success("渠道数据校验通过,已进入第三步"); } catch (error) { ElMessage.error(error?.message || "请检查渠道数据填写情况"); } finally { validating.value = false; } }
|
|
|
function syncScoreRows(savedRows = []) { const savedByChannel = new Map(savedRows.map((row) => [row?.channel, row])); const weights = Object.fromEntries(weightRows.value.map((row) => [row.dimension, row.weight])); scoreRows.value = channelRows.value.map((channelRow) => { const saved = savedByChannel.get(channelRow.channel); const savedScores = new Map((saved?.dimensions || []).map((dimension) => [dimension?.dimension, dimension])); return makeRow({ channel: channelRow.channel, weightedScore: saved?.weightedScore, dimensions: scoringMetrics.map((metric) => makeRow({ key: metric.key, dimension: metric.dimension, weight: weights[metric.dimension], score: savedScores.get(metric.dimension)?.score })) }); }); }
|
|
|
function openStepThree() { syncScoreRows(scoreRows.value); activeStep.value = 3; }
|
|
|
async function saveScoreStep() { if (saving.value) return; saving.value = true; try { await saveAnswer(3, { step3Answer: JSON.stringify(scorePayload()) }); ElMessage.success("当前进度已保存"); } finally { saving.value = false; } }
|
|
|
async function validateScoreStep() { if (validating.value) return; validating.value = true; try { const result = await validateReleaseChannelEvaluationStepThree(scorePayload()); const calculationCheck = result?.data || {}; await saveAnswer(4, { step3Answer: JSON.stringify(scoreAnswerPayload(calculationCheck)) }); syncRankingRows(); activeStep.value = 4; ElMessage[calculationCheck.calculationCorrect ? "success" : "warning"](calculationCheck.message || "已保存并进入第四步"); } catch (error) { ElMessage.error(error?.message || "请完整填写归一化得分表"); } finally { validating.value = false; } }
|
|
|
function syncRankingRows(savedRows = []) { const savedByChannel = new Map(savedRows.map((row) => [row?.channel, row])); rankingRows.value = scoreRows.value.map((scoreRow) => { const saved = savedByChannel.get(scoreRow.channel); return makeRow({ channel: saved?.channel ?? "", weightedScore: saved?.weightedScore }); }); }
|
|
|
function openStepFour() { syncRankingRows(rankingRows.value); activeStep.value = 4; }
|
|
|
async function saveRankingStep() { if (saving.value) return; saving.value = true; try { await saveAnswer(4, { step4Answer: JSON.stringify(rankingPayload()) }); ElMessage.success("当前进度已保存"); } finally { saving.value = false; } }
|
|
|
async function validateRankingStep() { if (validating.value) return; validating.value = true; try { await validateReleaseChannelEvaluationStepFour(rankingPayload()); await saveStudentTrainingAnswer(TASK_KEY, { saveAction: "SUBMIT", currentStep: 4, step4Answer: JSON.stringify(rankingPayload()) }); ElMessage.success("渠道排名校验通过,任务已提交"); } catch (error) { ElMessage.error(error?.message || "请检查渠道排名和核心渠道运营建议"); } finally { validating.value = false; } }
|
|
|
function downloadPracticeGuide() {
|
|
|
downloadBuiltInFile(RELEASE_CHANNEL_GUIDE_URL, "21.实训操作说明.docx");
|
|
|
}
|
|
|
function downloadBuiltInFile(url, fileName) {
|
|
|
const link = document.createElement("a");
|
|
|
link.href = url;
|
|
|
link.download = fileName;
|
|
|
document.body.appendChild(link);
|
|
|
link.click();
|
|
|
document.body.removeChild(link);
|
|
|
}
|
|
|
function downloadCsv(headers, rows, fileName) { const escape = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`; const lines = [headers, ...rows].map((row) => row.map(escape).join(",")); const url = URL.createObjectURL(new Blob([`\uFEFF${lines.join("\n")}`], { type: "text/csv;charset=utf-8" })); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); }
|
|
|
function exportChannelRows() { downloadCsv(["渠道", "日均总搜索量", "供需比", "平均点击率(%)", "平均CPC(元)", "平均转化率(%)", "现有相关访客数", "匹配度(1-100)"], channelRows.value.map((row) => [row.channel, row.dailySearchVolume, row.supplyDemandRatio, row.averageClickRate, row.averageCpc, row.averageConversionRate, row.existingVisitorCount, row.matchingScore]), "产品发布渠道评估-渠道数据.csv"); }
|
|
|
function exportScoreRows() { const headers = ["渠道", ...scoringMetrics.flatMap((metric) => [`${metric.dimension}得分`, `${metric.dimension}权重(%)`]), "加权得分"]; const rows = scoreRows.value.map((row) => [row.channel, ...row.dimensions.flatMap((dimension) => [dimension.score, dimension.weight]), row.weightedScore]); downloadCsv(headers, rows, "产品发布渠道评估-归一化评分.csv"); }
|
|
|
function restoreRows(raw, target, defaults, fields) { try { const saved = typeof raw === "string" ? JSON.parse(raw || "{}") : raw; if (!Array.isArray(saved?.rows)) return; target.value = saved.rows.map((row) => { const value = Object.fromEntries(fields.map((field) => [field, row?.[field]])); return makeRow(target === weightRows ? { ...value, suggestion: weightSuggestions[value.dimension] || "" } : value); }); } catch (error) { target.value = defaults(); } }
|
|
|
async function loadProgress() { try { const result = await getStudentTrainingAnswer(TASK_KEY); const answer = result?.data; restoreRows(answer?.step1Answer, weightRows, defaultWeightRows, ["dimension", "description", "weight"]); restoreRows(answer?.step2Answer, channelRows, defaultChannelRows, ["channel", "dailySearchVolume", "supplyDemandRatio", "averageClickRate", "averageCpc", "averageConversionRate", "existingVisitorCount", "matchingScore"]); let savedScores = []; try { const saved = typeof answer?.step3Answer === "string" ? JSON.parse(answer.step3Answer || "{}") : answer?.step3Answer; savedScores = Array.isArray(saved?.rows) ? saved.rows : []; } catch (error) { savedScores = []; } syncScoreRows(savedScores); let savedRanking = []; try { const saved = typeof answer?.step4Answer === "string" ? JSON.parse(answer.step4Answer || "{}") : answer?.step4Answer; savedRanking = Array.isArray(saved?.rows) ? saved.rows : []; coreChannelAnalysis.value = String(saved?.coreChannelAnalysis || ""); } catch (error) { savedRanking = []; coreChannelAnalysis.value = ""; } syncRankingRows(savedRanking); activeStep.value = Math.min(Math.max(Number(answer?.currentStep) || 1, 1), 4); } catch (error) { syncScoreRows(); syncRankingRows(); } }
|
|
|
onMounted(async () => { try { const result = await getTrainingTaskByKey(TASK_KEY); taskConfig.value = result?.data || null; } catch (error) { taskConfig.value = null; } await loadProgress(); });
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
/* ===== 页面级令牌(墨黑极简主题) ===== */
|
|
|
.release-channel-page {
|
|
|
--student-accent: #2563eb;
|
|
|
--student-accent-deep: #1d4ed8;
|
|
|
--student-accent-light: #3b82f6;
|
|
|
--student-accent-soft: #eff6ff;
|
|
|
--student-danger: #ef4444;
|
|
|
--student-success: #2ba471;
|
|
|
--student-layer-1: #1d4ed8;
|
|
|
--student-layer-2: #3b82f6;
|
|
|
--student-layer-3: #93c5fd;
|
|
|
--student-panel-border: #e5e7eb;
|
|
|
--student-card-bg: #ffffff;
|
|
|
--student-panel-bg: #fafbfc;
|
|
|
--student-text: #1f2937;
|
|
|
--student-text-secondary: #6b7280;
|
|
|
--student-fs-display: 17px;
|
|
|
--student-fs-h1: 15px;
|
|
|
--student-fs-h2: 14px;
|
|
|
--student-fs-h3: 13px;
|
|
|
--student-fs-body: 13px;
|
|
|
--student-fs-body-sm: 12px;
|
|
|
--student-fs-caption: 11px;
|
|
|
--student-fs-button: 13px;
|
|
|
--student-fs-button-lg: 13px;
|
|
|
--student-fs-data: 14px;
|
|
|
|
|
|
min-width: 0;
|
|
|
color: var(--student-text);
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
/* 主容器:只设 gap,无边框/背景/阴影/双列 grid */
|
|
|
.release-channel-core {
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
align-content: start;
|
|
|
min-width: 0;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* ===== task-header ===== */
|
|
|
.task-header {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin: 0;
|
|
|
padding: 16px;
|
|
|
border: 1px solid #e7e9ed;
|
|
|
border-radius: 16px;
|
|
|
background: #ffffff;
|
|
|
}
|
|
|
|
|
|
.task-head-main {
|
|
|
display: grid;
|
|
|
justify-items: start;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
flex: 1 1 auto;
|
|
|
}
|
|
|
|
|
|
|
|
|
.task-title {
|
|
|
margin: 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-display);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
.task-brief {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 12px;
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.task-brief-text {
|
|
|
display: -webkit-box;
|
|
|
overflow: hidden;
|
|
|
-webkit-box-orient: vertical;
|
|
|
-webkit-line-clamp: 1;
|
|
|
}
|
|
|
|
|
|
.task-brief-toggle {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
color: var(--student-accent-deep);
|
|
|
background: transparent;
|
|
|
font: inherit;
|
|
|
font-weight: 700;
|
|
|
cursor: pointer;
|
|
|
&:hover { text-decoration: underline; }
|
|
|
}
|
|
|
|
|
|
.task-brief-more {
|
|
|
display: grid;
|
|
|
gap: 6px;
|
|
|
margin-top: 8px;
|
|
|
padding-top: 8px;
|
|
|
border-top: 1px solid var(--student-panel-border);
|
|
|
b { color: var(--student-text); font-size: 12px; }
|
|
|
ul { display: grid; gap: 4px; margin: 0; padding-left: 18px; }
|
|
|
p { margin: 0; }
|
|
|
}
|
|
|
|
|
|
/* ===== 工具栏:一行不换行 ===== */
|
|
|
.task-header :deep(.material-actions) {
|
|
|
flex: 0 0 auto;
|
|
|
display: flex;
|
|
|
flex-wrap: nowrap;
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
.task-header :deep(.material-button) {
|
|
|
flex: 0 0 auto;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
/* ===== task-card:透明容器 ===== */
|
|
|
.task-card {
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
display: grid;
|
|
|
gap: 10px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
/* grid 子项默认 min-width:auto 会被宽表格(评分表 1780px)撑开,
|
|
|
导致 .table-wrap 的 overflow-x:auto 失效、整页横向溢出 */
|
|
|
.task-card > * {
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.workbench-head {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
padding: 0 0 10px;
|
|
|
border: 0;
|
|
|
border-bottom: 1px solid var(--student-panel-border);
|
|
|
}
|
|
|
|
|
|
.section-eyebrow {
|
|
|
margin: 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: 11px;
|
|
|
font-weight: 700;
|
|
|
letter-spacing: 0.12em;
|
|
|
text-transform: uppercase;
|
|
|
}
|
|
|
|
|
|
.workbench-head h2 {
|
|
|
margin: 4px 0 0;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-h1);
|
|
|
font-weight: 700;
|
|
|
line-height: 1.35;
|
|
|
}
|
|
|
|
|
|
/* ===== 步骤条:容器样式 ===== */
|
|
|
.step-tabs {
|
|
|
display: flex;
|
|
|
align-items: stretch;
|
|
|
gap: 10px;
|
|
|
margin: 4px 0 16px;
|
|
|
padding: 0;
|
|
|
border: 0;
|
|
|
border-radius: 0;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
/* ===== 步骤说明 & 引导区 ===== */
|
|
|
.lead {
|
|
|
margin: 14px 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.scoring-guide {
|
|
|
margin: 14px 0 12px;
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.step-three-materials {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 20px;
|
|
|
margin-top: 18px;
|
|
|
padding: 12px 14px;
|
|
|
border-left: 3px solid var(--student-accent);
|
|
|
border-radius: 0 8px 8px 0;
|
|
|
background: var(--student-accent-soft);
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
.step-three-materials p { margin: 0; }
|
|
|
|
|
|
.step-three-notice {
|
|
|
margin: 20px 0 0;
|
|
|
padding: 10px 14px;
|
|
|
border: 1px solid rgba(217, 85, 76, 0.28);
|
|
|
border-radius: 8px;
|
|
|
color: #9b3b34;
|
|
|
background: rgba(217, 85, 76, 0.06);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
/* ===== 表格 ===== */
|
|
|
.table-actions {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
gap: 12px;
|
|
|
margin: 0 0 16px;
|
|
|
}
|
|
|
|
|
|
.table-wrap {
|
|
|
overflow-x: auto;
|
|
|
min-width: 0;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 10px;
|
|
|
}
|
|
|
|
|
|
.table-wrap table {
|
|
|
width: 100%;
|
|
|
border-collapse: collapse;
|
|
|
}
|
|
|
|
|
|
.weight-table { min-width: 900px; }
|
|
|
.channel-table { min-width: 1320px; }
|
|
|
.scoring-table { min-width: 1780px; }
|
|
|
.ranking-table { min-width: 880px; }
|
|
|
|
|
|
.table-wrap th,
|
|
|
.table-wrap td {
|
|
|
padding: 12px;
|
|
|
border-right: 1px solid var(--student-panel-border);
|
|
|
border-bottom: 1px solid var(--student-panel-border);
|
|
|
vertical-align: middle;
|
|
|
}
|
|
|
|
|
|
.table-wrap th {
|
|
|
white-space: nowrap;
|
|
|
color: var(--student-text);
|
|
|
background: #fafbfc;
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
font-weight: 800;
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
.table-wrap td { background: #ffffff; }
|
|
|
|
|
|
.scoring-table thead tr:nth-child(2) th { background: #fafbfc; }
|
|
|
|
|
|
.channel-table td,
|
|
|
.scoring-table td { min-width: 108px; }
|
|
|
.channel-table td:first-child { min-width: 110px; }
|
|
|
.scoring-table th:first-child { min-width: 108px; }
|
|
|
.ranking-table td { min-width: 170px; }
|
|
|
.ranking-table td:first-child { min-width: 100px; }
|
|
|
|
|
|
.rank-cell,
|
|
|
.core-cell { text-align: center; font-weight: 800; }
|
|
|
|
|
|
.core-cell .core {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
min-width: 36px;
|
|
|
height: 24px;
|
|
|
padding: 0 8px;
|
|
|
border-radius: 6px;
|
|
|
color: #ffffff;
|
|
|
background: var(--student-accent);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.weight-cell {
|
|
|
color: var(--student-accent);
|
|
|
text-align: center;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
|
|
|
.table-wrap small {
|
|
|
display: block;
|
|
|
margin-top: 6px;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-caption);
|
|
|
}
|
|
|
|
|
|
.operation-column { width: 100px; text-align: center; }
|
|
|
.empty-row { text-align: center; color: var(--student-text-secondary); }
|
|
|
|
|
|
.table-wrap tfoot th,
|
|
|
.table-wrap tfoot td {
|
|
|
border-bottom: 0;
|
|
|
background: var(--student-panel-bg);
|
|
|
}
|
|
|
|
|
|
.total-error { color: var(--student-danger); font-weight: 700; }
|
|
|
|
|
|
/* ===== 表单输入 ===== */
|
|
|
.analysis-field {
|
|
|
display: block;
|
|
|
margin-top: 14px;
|
|
|
}
|
|
|
.analysis-field > span {
|
|
|
display: block;
|
|
|
margin-bottom: 8px;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
/* 表格内输入框:透明底 + outline focus */
|
|
|
.table-wrap :deep(.el-input__wrapper),
|
|
|
.table-wrap :deep(.el-input-number) {
|
|
|
width: 100%;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
.table-wrap :deep(.el-input__inner) {
|
|
|
color: var(--student-text);
|
|
|
}
|
|
|
.table-wrap :deep(.el-textarea__inner) {
|
|
|
border: 1px solid transparent;
|
|
|
background: transparent;
|
|
|
box-shadow: none;
|
|
|
color: var(--student-text);
|
|
|
font-size: var(--student-fs-body);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
.analysis-field :deep(.el-textarea__inner) {
|
|
|
min-height: 180px !important;
|
|
|
border: 1px solid var(--student-panel-border);
|
|
|
border-radius: 8px;
|
|
|
background: #ffffff;
|
|
|
box-shadow: none;
|
|
|
color: var(--student-text);
|
|
|
line-height: 1.55;
|
|
|
}
|
|
|
.table-wrap :deep(.el-textarea__inner:focus),
|
|
|
.analysis-field :deep(.el-textarea__inner:focus),
|
|
|
.table-wrap :deep(.el-input__wrapper.is-focus) {
|
|
|
outline: 2px solid rgba(37, 99, 235, 0.2);
|
|
|
outline-offset: -2px;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
/* ===== 底部说明 & 按钮 ===== */
|
|
|
.hint {
|
|
|
margin: 14px 0 0;
|
|
|
color: var(--student-text-secondary);
|
|
|
font-size: var(--student-fs-body-sm);
|
|
|
line-height: 1.6;
|
|
|
}
|
|
|
|
|
|
.footer-actions {
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
gap: 12px;
|
|
|
margin-top: 20px;
|
|
|
}
|
|
|
|
|
|
/* ===== 响应式 ===== */
|
|
|
@media (max-width: 720px) {
|
|
|
.task-header { flex-direction: column; align-items: flex-start; }
|
|
|
.task-header :deep(.material-actions) { width: 100%; }
|
|
|
.workbench-head { flex-direction: column; }
|
|
|
.table-actions,
|
|
|
.footer-actions { align-items: stretch; flex-direction: column; }
|
|
|
.table-actions :deep(.el-button),
|
|
|
.footer-actions :deep(.el-button) { width: 100%; }
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<style lang="scss">
|
|
|
/* ===== 步骤条:分段连接胶囊(教育蓝极简) ===== */
|
|
|
#app .studentLayout section.app-main:has(.release-channel-page) .step-tabs {
|
|
|
display: flex !important;
|
|
|
flex-wrap: nowrap !important;
|
|
|
align-items: stretch !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 0 !important;
|
|
|
width: 100% !important;
|
|
|
margin: 0 0 20px !important;
|
|
|
padding: 0 !important;
|
|
|
border: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: transparent !important;
|
|
|
box-shadow: none !important;
|
|
|
|
|
|
.step-connector { display: none !important; }
|
|
|
|
|
|
.step-tab {
|
|
|
flex: 1 1 0 !important;
|
|
|
max-width: 240px !important;
|
|
|
display: flex !important;
|
|
|
flex-direction: row !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
gap: 8px !important;
|
|
|
min-width: 0 !important;
|
|
|
height: auto !important;
|
|
|
min-height: 40px !important;
|
|
|
margin: 0 !important;
|
|
|
padding: 8px 14px !important;
|
|
|
border: 1px solid #e5e7eb !important;
|
|
|
border-right: none !important;
|
|
|
border-radius: 0 !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
line-height: 1.3 !important;
|
|
|
letter-spacing: 0 !important;
|
|
|
white-space: normal !important;
|
|
|
text-align: center !important;
|
|
|
cursor: pointer;
|
|
|
transition: all 0.16s ease !important;
|
|
|
|
|
|
&:first-child {
|
|
|
border-radius: 8px 0 0 8px !important;
|
|
|
}
|
|
|
&:last-child {
|
|
|
border-radius: 0 8px 8px 0 !important;
|
|
|
border-right: 1px solid #e5e7eb !important;
|
|
|
}
|
|
|
|
|
|
.step-marker {
|
|
|
display: inline-flex !important;
|
|
|
align-items: center !important;
|
|
|
justify-content: center !important;
|
|
|
flex: 0 0 auto !important;
|
|
|
min-width: 22px !important;
|
|
|
width: auto !important;
|
|
|
height: 22px !important;
|
|
|
padding: 0 5px !important;
|
|
|
border: none !important;
|
|
|
border-radius: 6px !important;
|
|
|
background: #f3f4f6 !important;
|
|
|
color: #6b7280 !important;
|
|
|
font-family: var(--student-font-data), ui-monospace, "SF Mono", Consolas, monospace !important;
|
|
|
font-size: 12px !important;
|
|
|
font-weight: 700 !important;
|
|
|
line-height: 1 !important;
|
|
|
transition: all 0.16s ease !important;
|
|
|
}
|
|
|
|
|
|
.step-copy {
|
|
|
display: flex !important;
|
|
|
flex-direction: column !important;
|
|
|
gap: 0 !important;
|
|
|
min-width: 0 !important;
|
|
|
margin-left: 0 !important;
|
|
|
}
|
|
|
|
|
|
.step-copy b {
|
|
|
overflow: hidden !important;
|
|
|
text-overflow: ellipsis !important;
|
|
|
white-space: normal !important;
|
|
|
line-height: 1.3 !important;
|
|
|
color: inherit !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
}
|
|
|
|
|
|
.step-copy small { display: none !important; }
|
|
|
|
|
|
&:hover:not(.active):not(.complete) {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
color: #2563eb !important;
|
|
|
z-index: 1 !important;
|
|
|
.step-marker { background: #2563eb !important; color: #ffffff !important; }
|
|
|
}
|
|
|
|
|
|
&.complete:not(.active) {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #374151 !important;
|
|
|
.step-marker { background: rgba(43, 164, 113, 0.12) !important; color: #16915f !important; }
|
|
|
&:hover {
|
|
|
border-color: #2ba471 !important;
|
|
|
background: #f0fdf6 !important;
|
|
|
z-index: 1 !important;
|
|
|
.step-marker { background: #2ba471 !important; color: #ffffff !important; }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.active {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
font-weight: 700 !important;
|
|
|
box-shadow: none !important;
|
|
|
z-index: 2 !important;
|
|
|
.step-marker { background: #ffffff !important; color: #2563eb !important; }
|
|
|
&:hover {
|
|
|
background: #1d4ed8 !important;
|
|
|
border-color: #1d4ed8 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 窄屏横向滑动 ===== */
|
|
|
@media (max-width: 900px) {
|
|
|
#app .studentLayout section.app-main:has(.release-channel-page) .step-tabs {
|
|
|
justify-content: flex-start !important;
|
|
|
overflow-x: auto !important;
|
|
|
}
|
|
|
#app .studentLayout section.app-main:has(.release-channel-page) .step-tabs .step-tab {
|
|
|
flex: 0 0 140px !important;
|
|
|
max-width: none !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ============================================================
|
|
|
* 共用组件换肤:TrainingMaterialButton 资料按钮 → 教育蓝
|
|
|
* ============================================================ */
|
|
|
.release-channel-page .task-header .material-button.el-button {
|
|
|
height: 34px !important;
|
|
|
min-height: 34px !important;
|
|
|
border: 1px solid rgba(37, 99, 235, 0.35) !important;
|
|
|
border-radius: 8px !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #ffffff !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 600 !important;
|
|
|
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease !important;
|
|
|
|
|
|
&:hover {
|
|
|
border-color: #2563eb !important;
|
|
|
color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
|
|
|
}
|
|
|
|
|
|
&.is-plain {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
&:hover {
|
|
|
border-color: #e5e7eb !important;
|
|
|
color: #9ca3af !important;
|
|
|
background: #ffffff !important;
|
|
|
box-shadow: none !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* ===== 表格操作按钮换肤 ===== */
|
|
|
.release-channel-page .table-actions .el-button,
|
|
|
.release-channel-page .footer-actions .el-button {
|
|
|
border-radius: 8px !important;
|
|
|
font-size: 13px !important;
|
|
|
font-weight: 700 !important;
|
|
|
}
|
|
|
.release-channel-page .table-actions .el-button--primary,
|
|
|
.release-channel-page .footer-actions .el-button--primary {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #2563eb !important;
|
|
|
color: #ffffff !important;
|
|
|
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
|
|
|
}
|
|
|
.release-channel-page .table-actions .el-button--primary:hover,
|
|
|
.release-channel-page .footer-actions .el-button--primary:hover {
|
|
|
border-color: #1d4ed8 !important;
|
|
|
background: #1d4ed8 !important;
|
|
|
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
|
|
|
}
|
|
|
.release-channel-page .footer-actions .el-button:not(.el-button--primary) {
|
|
|
border-color: rgba(37, 99, 235, 0.35) !important;
|
|
|
background: #ffffff !important;
|
|
|
color: #2563eb !important;
|
|
|
}
|
|
|
.release-channel-page .footer-actions .el-button:not(.el-button--primary):hover {
|
|
|
border-color: #2563eb !important;
|
|
|
background: #eff6ff !important;
|
|
|
}
|
|
|
</style>
|