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

110 lines
28 KiB
Vue

This file contains ambiguous Unicode characters!

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

<template>
<div class="opportunity-page student-training-shell">
<main class="opportunity-core">
<header class="task-header"><span>产品规划与设计 · MARKET SIGNALS</span><h1>{{ taskTitle }}</h1></header>
<TrainingTaskBrief :background="taskBackground" :goals="taskGoals" :requirements="taskRequirement" :show-material="false" />
<section class="workbench">
<div class="workbench-actions"><TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" /><button class="btn" type="button" @click="exportOutcome"><el-icon><Download /></el-icon> </button></div>
<nav class="step-indicator" aria-label=""><template v-for="(step, index) in trainingSteps" :key="step"><button type="button" :class="['step-tab', { active: currentStep === index + 1, complete: currentStep > index + 1 }]" @click="switchStep(index + 1)"><b>{{ String(index + 1).padStart(2, '0') }}</b><span><strong>{{ step }}</strong><small>{{ currentStep === index + 1 ? '当前步骤' : '点击切换' }} →</small></span></button><i v-if="index < trainingSteps.length - 1" /></template></nav>
<section class="step-card">
<template v-if="currentStep === 1">
<div class="step-heading"><div><p>Step 01</p><h2>{{ trainingSteps[0] }}</h2></div><em>指标分类练习</em></div>
<p class="intro">请对实训数据表中的各项指标,按照市场规模、市场潜力、竞争强度、运营难度 4 个类别进行分类。</p>
<div class="table-wrap"><table><thead><tr><th>序号</th><th>指标</th><th>分类</th></tr></thead><tbody><tr v-for="(item, index) in indicators" :key="item.id"><td>{{ index + 1 }}</td><td class="metric">{{ item.name }}</td><td><select v-model="answers[item.id]" :aria-label="`${item.name} 的分类`"><option value="">请选择</option><option v-for="category in categories" :key="category" :value="category">{{ category }}</option></select></td></tr></tbody></table></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving" @click="checkAndContinue"> 2 </button><button class="btn" type="button" :disabled="saving" @click="resetStepOne"><el-icon><RefreshLeft /></el-icon> </button></div>
</template>
<template v-else-if="currentStep === 2">
<div class="step-heading"><div><p>Step 02</p><h2>{{ trainingSteps[1] }}</h2></div><em>指标权重设定</em></div>
<p class="intro">根据案例信息为各二级指标设定权重系统将自动汇总一级指标权重</p>
<p v-if="!stepOnePassed" class="warning">请先完成并保存第 1 步的指标分类,再填写本步骤。</p>
<div class="weight-summary"><span>权重合计</span><strong :class="{ valid: totalWeight === 100 }">{{ totalWeight }}%</strong><small>每项建议填写 1100合计必须为 100%</small></div>
<div class="table-wrap"><table class="weight-table"><thead><tr><th>一级指标</th><th>二级指标</th><th>权重(%</th></tr></thead><tbody><tr v-for="item in indicators" :key="item.id"><td class="first-level">{{ item.group }}{{ groupWeight(item.group) }}%</td><td class="metric">{{ item.name }}</td><td><input v-model.number="weights[item.id]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${item.name} 的权重`" /><span class="percent">%</span></td></tr></tbody></table></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepOnePassed" @click="saveAndGoToStepThree">保存并进入第 3 步</button><button class="btn" type="button" :disabled="saving || !stepOnePassed" @click="resetStepTwo"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
</template>
<template v-else-if="currentStep === 3">
<div class="step-heading"><div><p>Step 03</p><h2>{{ trainingSteps[2] }}</h2></div><em>加权评分计算</em></div>
<p class="intro">请根据数据表及第 2 步权重,使用 Min-Max 归一法计算各二级指标得分,再采用加权评分法填写产品综合得分。</p>
<p v-if="!stepTwoPassed" class="warning">请先完成并保存第 2 步的指标权重设定,再填写本步骤。</p>
<div class="score-table-wrap"><table class="score-table"><thead><tr><th>序号</th><th>产品品类</th><th>综合得分</th></tr></thead><tbody><tr v-for="(product, index) in products" :key="product.id"><td>{{ index + 1 }}</td><td class="metric">{{ product.name }}</td><td><input v-model.number="scores[product.id]" :disabled="!stepTwoPassed" type="number" min="0" max="100" step="0.01" :aria-label="`${product.name} 的综合得分`" /><span class="percent">分</span></td></tr></tbody></table></div>
<div class="calculation-guide"><div><h3>推荐工具及方法如下,自行选择:</h3><p>1. Excel/WPS 表格,自行操作计算,可参考《实训操作说明》完成。</p><p>2. 下载 Excel 模板,填写关键数据,完成指标计算。</p><p>3. 导出实训数据表,上传到通用 AI 大模型,参考提示词完成计算。</p><p>请根据数据表及各指标权重设定表,通过 Min-Max 归一法计算各二级指标得分,采用加权评分法计算综合得分。</p></div><div class="download-stack"><button class="btn" type="button" @click="downloadInstructions">下载实训操作说明</button><button class="btn primary" type="button" @click="downloadTemplate">下载自动化模板</button></div></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepTwoPassed" @click="saveAndGoToStepFour">保存并进入第 4 步</button><button class="btn" type="button" :disabled="saving || !stepTwoPassed" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
</template>
<template v-else-if="currentStep === 4">
<div class="step-heading"><div><p>Step 04</p><h2>{{ trainingSteps[3] }}</h2></div><em>可视化结果提交</em></div>
<p class="intro">计算完各产品综合得分后,请按照智能健身镜、智能卫浴镜和智能美妆镜,计算各品类产品的平均得分、最高分、最低分,并进行可视化处理。</p>
<p v-if="!stepThreePassed" class="warning">请先完成并保存第 3 步的产品综合得分,再上传可视化结果图。</p>
<div class="visualization-upload">
<input ref="visualizationInput" class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change="handleVisualizationUpload" />
<button class="upload-trigger" type="button" :disabled="saving || uploading || !stepThreePassed" @click="openVisualizationPicker"><el-icon><UploadFilled /></el-icon>{{ uploading ? "上传中..." : visualization.url ? "更换数据可视化结果图" : "上传数据可视化结果图" }}</button>
<p>支持 PNG、JPG、WEBP 格式,文件不超过 5MB。</p>
<div v-if="visualization.url" class="visualization-preview"><el-image :src="visualization.url" :preview-src-list="[visualization.url]" fit="cover" preview-teleported /><div><strong>{{ visualization.name || "数据可视化结果图" }}</strong><span>点击缩略图可放大查看</span><button class="text-button" type="button" @click="clearVisualization">移除图片</button></div></div>
</div>
<div class="visual-guide"><h3>推荐工具及方法如下,自行选择:</h3><p>1. 使用 Excel/WPS 表格计算三类产品的平均得分、最高分和最低分,并生成图表。</p><p>2. 下载 Excel 模板,填写关键数据,完成指标计算和可视化。</p><p>3. 导出实训数据表,上传到通用 AI 大模型,参考提示词生成可视化结果。</p></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || uploading || !stepThreePassed" @click="saveAndCompleteTraining">保存并完成实训</button></div>
</template>
<div v-else class="placeholder"><p> {{ currentStep }} 步内容待配置</p><span>已保存的步骤可从上方步骤栏返回查看</span></div>
</section>
</section>
</main>
<TrainingAiSidebar study-tip="" reference-title="" reference-text="· <br />· 市场潜力反映增长速度与扩张空间<br />· 竞争强度反映供给数量与竞争关系<br />· 运营难度反映获取流量与转化的门槛" :progress-items="progressItems" />
</div>
</template>
<script setup>
import { Download, RefreshLeft, UploadFilled } from "@element-plus/icons-vue";
import { checkMarketOpportunitySelectionStepOne, checkMarketOpportunitySelectionStepTwo, checkMarketOpportunitySelectionStepThree, checkMarketOpportunitySelectionStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
const TASK_KEY = "market-opportunity-selection";
const DEFAULT_STEPS = ["指标分类", "市场机会分析", "选品方案设计", "选品结论"];
const categories = ["市场规模", "市场潜力", "竞争强度", "运营难度"];
const indicators = [
{ id: "search-count", name: "搜索次数", group: "市场规模" }, { id: "transaction-amount", name: "成交金额()", group: "市场规模" }, { id: "transaction-growth", name: "成交金额增速", group: "市场潜力" }, { id: "category-transaction-growth", name: "类目成交增速", group: "市场潜力" },
{ id: "demand-supply-ratio", name: "需供比", group: "竞争强度" }, { id: "online-products", name: "在线商品数", group: "竞争强度" }, { id: "online-merchants", name: "在线商家数", group: "竞争强度" }, { id: "organic-traffic-ratio", name: "自然流占比", group: "运营难度" },
];
const products = [{ id: "smart-fitness-mirror", name: "智能健身镜" }, { id: "smart-bathroom-mirror", name: "智能卫浴镜" }, { id: "smart-beauty-mirror", name: "智能美妆镜" }];
const defaultTask = { title: "市场机会识别与选品", background: "基于实训数据中的市场供给与流量指标完成市场机会识别和选品判断", goals: ["理解选品数据指标的业务含义", "掌握市场规模潜力竞争和运营难度的分类方法", "为后续选品分析建立指标基础"], requirement: "阅读案例资料完成 8 项指标的分类并依据判题反馈修改" };
const { proxy } = getCurrentInstance();
const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const uploading = ref(false); const visualizationInput = ref(null); const visualization = ref({ url: "", name: "" }); const stepOnePassed = ref(false); const stepTwoPassed = ref(false); const stepThreePassed = ref(false); const answers = ref(createAnswers()); const weights = ref(createWeights()); const scores = ref(createScores());
const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title); const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background); const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals)); const taskRequirement = computed(() => taskConfig.value?.requirements || defaultTask.requirement); const trainingSteps = computed(() => parseTrainingArray(taskConfig.value?.steps, DEFAULT_STEPS));
const progressItems = computed(() => indicators.map((item) => ({ text: `${answers.value[item.id] ? "已分类" : "待分类"}${item.name}`, status: answers.value[item.id] ? "done" : "" })));
const totalWeight = computed(() => indicators.reduce((total, item) => total + (Number(weights.value[item.id]) || 0), 0));
onMounted(async () => { await loadTaskConfig(); await loadSavedAnswer(); });
function createAnswers() { return Object.fromEntries(indicators.map((item) => [item.id, ""])); }
function createWeights() { return Object.fromEntries(indicators.map((item) => [item.id, null])); }
function createScores() { return Object.fromEntries(products.map((product) => [product.id, null])); }
function parseArray(value, fallback) { if (Array.isArray(value)) return value.length ? value : fallback; if (!value) return fallback; try { const parsed = JSON.parse(value); return Array.isArray(parsed) && parsed.length ? parsed.map((item) => String(item?.name ?? item).trim()).filter(Boolean) : fallback; } catch (error) { const list = String(value).split(/[;|]/).map((item) => item.trim()).filter(Boolean); return list.length ? list : fallback; } }
async function loadTaskConfig() { try { taskConfig.value = (await getTrainingTaskByKey(TASK_KEY))?.data || null; } catch (error) { taskConfig.value = null; } }
async function loadSavedAnswer() { try { const answer = (await getStudentTrainingAnswer(TASK_KEY))?.data; if (answer?.step1Answer) { const parsed = JSON.parse(answer.step1Answer); answers.value = { ...createAnswers(), ...(parsed?.answers || parsed || {}) }; stepOnePassed.value = true; } if (answer?.step2Answer) { const parsed = JSON.parse(answer.step2Answer); weights.value = { ...createWeights(), ...(parsed?.weights || parsed || {}) }; stepTwoPassed.value = true; } if (answer?.step3Answer) { const parsed = JSON.parse(answer.step3Answer); scores.value = { ...createScores(), ...(parsed?.scores || parsed || {}) }; stepThreePassed.value = true; } if (answer?.step4Answer) { const parsed = JSON.parse(answer.step4Answer); visualization.value = { url: parsed?.visualization?.url || "", name: parsed?.visualization?.name || "" }; } if (Number(answer?.currentStep) > 0) currentStep.value = Number(answer.currentStep); } catch (error) { /* Keep the page available when history cannot be restored. */ } }
function buildStepOneOutcome() { return { title: taskTitle.value, answers: { ...answers.value } }; }
function buildStepTwoOutcome() { return { title: taskTitle.value, weights: { ...weights.value }, primaryWeights: Object.fromEntries(["市场规模", "市场潜力", "竞争强度", "运营难度"].map((group) => [group, groupWeight(group)])) }; }
function buildStepThreeOutcome() { return { title: taskTitle.value, scores: { ...scores.value } }; }
function buildStepFourOutcome() { return { title: taskTitle.value, visualization: { ...visualization.value } }; }
async function checkAndContinue() { if (saving.value) return; saving.value = true; try { const result = await checkMarketOpportunitySelectionStepOne(indicators.map((item) => ({ id: item.id, category: answers.value[item.id] }))); if (!result?.data?.allCorrect) throw new Error(`当前答对 ${result?.data?.correctCount || 0}/${indicators.length} 项,请根据提示修改`); await saveStudentTrainingAnswer(TASK_KEY, { step1Answer: JSON.stringify(buildStepOneOutcome()), currentStep: 2, submitted: false, saveAction: "SAVE" }); stepOnePassed.value = true; currentStep.value = 2; proxy?.$modal?.msgSuccess("分类正确已进入第 2 "); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请完成指标分类并根据提示修改"); } finally { saving.value = false; } }
function groupWeight(group) { return indicators.filter((item) => item.group === group).reduce((total, item) => total + (Number(weights.value[item.id]) || 0), 0); }
function switchStep(step) { if (step > 1 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成第 1 步指标分类"); return; } if (step > 2 && !stepTwoPassed.value) { proxy?.$modal?.msgWarning("请先完成第 2 步指标权重设定"); return; } if (step > 3 && !stepThreePassed.value) { proxy?.$modal?.msgWarning("请先完成第 3 步综合得分计算"); return; } currentStep.value = step; }
async function saveAndGoToStepThree() { if (saving.value || !stepOnePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepTwo({ weights: indicators.map((item) => ({ id: item.id, weight: weights.value[item.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step2Answer: JSON.stringify(buildStepTwoOutcome()), currentStep: 3, submitted: false, saveAction: "SAVE" }); stepTwoPassed.value = true; currentStep.value = 3; proxy?.$modal?.msgSuccess("已保存已进入第 3 "); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写合理的二级指标权重且合计为 100%"); } finally { saving.value = false; } }
async function saveAndGoToStepFour() { if (saving.value || !stepTwoPassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepThree({ scores: products.map((product) => ({ id: product.id, score: scores.value[product.id] })) }); await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" }); stepThreePassed.value = true; currentStep.value = 4; proxy?.$modal?.msgSuccess("已保存已进入第 4 "); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请填写 0 100 分的综合得分"); } finally { saving.value = false; } }
function resetStepOne() { answers.value = createAnswers(); proxy?.$modal?.msgSuccess("已清空第 1 步填写内容"); }
function resetStepTwo() { weights.value = createWeights(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
function resetStepThree() { scores.value = createScores(); proxy?.$modal?.msgSuccess("已清空第 3 步填写内容"); }
function openVisualizationPicker() { visualizationInput.value?.click(); }
function clearVisualization() { if (visualization.value.url.startsWith("blob:")) URL.revokeObjectURL(visualization.value.url); visualization.value = { url: "", name: "" }; }
async function handleVisualizationUpload(event) { const file = event.target?.files?.[0]; if (!file) return; const acceptedTypes = ["image/jpeg", "image/png", "image/webp"]; if (!acceptedTypes.includes(file.type)) { proxy?.$modal?.msgError?.("请上传 PNGJPG WEBP 格式的图片"); event.target.value = ""; return; } if (file.size > 5 * 1024 * 1024) { proxy?.$modal?.msgError?.("图片大小不能超过 5MB"); event.target.value = ""; return; } uploading.value = true; try { if (isStudentDemo()) { clearVisualization(); visualization.value = { url: URL.createObjectURL(file), name: file.name }; } else { const formData = new FormData(); formData.append("file", file); const result = await uploadStudentTrainingFile(formData); const url = result?.url || result?.data?.url; if (!url) throw new Error("图片上传失败请重试"); visualization.value = { url, name: file.name }; } proxy?.$modal?.msgSuccess("可视化结果图上传成功"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "图片上传失败请重试"); } finally { uploading.value = false; event.target.value = ""; } }
async function saveAndCompleteTraining() { if (saving.value || !stepThreePassed.value) return; saving.value = true; try { await checkMarketOpportunitySelectionStepFour({ visualizationUrl: visualization.value.url }); await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" }); proxy?.$modal?.msgSuccess("实训成果已保存恭喜完成本次实训"); } catch (error) { proxy?.$modal?.msgError?.(error?.message || "请上传数据可视化结果图"); } finally { saving.value = false; } }
function downloadFile(content, fileName, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); }
function downloadInstructions() { downloadFile("市场机会识别与选品 - 实训操作说明\n\n1. 汇总三类产品的原始数据\n2. 对正向指标计算 (x-min)/(max-min)对逆向指标计算 (max-x)/(max-min)\n3. 将归一化得分乘以第 2 步二级指标权重后求和\n4. 将综合得分填写回第 3 步表格保留两位小数", "市场机会识别与选品-实训操作说明.txt", "text/plain;charset=utf-8"); }
function downloadTemplate() { const header = ["产品品类", ...indicators.map((item) => item.name), "综合得分"]; const rows = products.map((product) => [product.name, ...indicators.map(() => ""), ""]); const weightRow = ["二级指标权重(%)", ...indicators.map((item) => weights.value[item.id] ?? ""), ""]; downloadFile([header, weightRow, ...rows].map((row) => row.join(",")).join("\n"), "市场机会识别与选品-自动化模板.csv", "text/csv;charset=utf-8"); }
function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: taskTitle.value, step1: buildStepOneOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), currentStep: currentStep.value }, 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); }
</script>
<style lang="scss" scoped>
.opportunity-page { display:grid; grid-template-columns:minmax(720px,1fr) 340px; gap:24px; min-height:calc(100vh - 106px); padding:24px; color:#e9f8ff; background:#06111d; }.opportunity-core { min-width:0; border:1px solid rgba(0,180,255,.25); background:rgba(8,18,28,.64); box-shadow:0 22px 48px rgba(0,0,0,.22); }.task-header { padding:24px 28px 4px; }.task-header span,.step-heading p { color:#66dcff; font-size:13px; font-weight:800; letter-spacing:.08em; }.task-header h1 { margin:9px 0 0; color:#fff; font-size:29px; }.workbench { margin:24px; padding:22px; border:1px solid rgba(33,194,255,.35); border-radius:20px; background:rgba(1,24,39,.72); }.workbench-actions { display:flex; justify-content:flex-end; gap:12px; }.btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid #2cc9ff; border-radius:999px; padding:10px 18px; color:#dff8ff; background:rgba(8,87,121,.55); cursor:pointer; }.primary { border-color:#36d9ff; background:linear-gradient(110deg,#0eaae4,#0572ac); font-weight:700; }.btn:disabled { cursor:not-allowed; opacity:.6; }.step-indicator { display:flex; align-items:center; gap:12px; margin:28px 0; padding:12px; border:1px solid rgba(30,190,255,.28); border-radius:22px; }.step-tab { display:flex; align-items:center; flex:1; gap:10px; min-width:0; padding:10px; border:0; border-radius:16px; color:#96c6d8; background:transparent; text-align:left; cursor:pointer; }.step-tab.active { color:#fff; background:linear-gradient(110deg,#148fc6,#0879aa); }.step-tab b { display:grid; place-items:center; flex:0 0 42px; width:42px; height:42px; border:1px solid #4ed7ff; border-radius:50%; }.step-tab span { display:grid; gap:4px; min-width:0; }.step-tab strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }.step-tab small { color:#5ec9ed; }.step-indicator i { flex:0 0 24px; height:2px; background:rgba(83,204,255,.4); }.step-card { padding:24px; border:1px solid rgba(31,185,249,.32); border-radius:18px; background:rgba(1,21,34,.65); }.step-heading { display:flex; justify-content:space-between; align-items:start; }.step-heading p { margin:0; }.step-heading h2 { margin:5px 0 0; color:#fff; font-size:24px; }.step-heading em { border:1px solid #38caff; border-radius:999px; padding:7px 12px; color:#9aeaff; font-style:normal; }.intro { margin:24px 0; color:#c5e4ef; font-size:16px; line-height:1.8; }.warning { margin:0 0 16px; color:#ffd58e; }.weight-summary { display:flex; align-items:baseline; gap:10px; margin:10px 0 18px; padding:13px 16px; border:1px solid rgba(57,207,255,.28); border-radius:11px; color:#c5e4ef; background:rgba(11,81,113,.18); }.weight-summary strong { color:#ffcf77; font-size:23px; }.weight-summary strong.valid { color:#67e8a9; }.weight-summary small { margin-left:auto; color:#8fc9dc; }.table-wrap,.score-table-wrap { overflow-x:auto; }.table-wrap table,.score-table { width:100%; min-width:720px; border-collapse:collapse; }.table-wrap th,.table-wrap td,.score-table th,.score-table td { border:1px solid rgba(139,213,241,.45); }.table-wrap th,.score-table th { padding:14px; color:#fff; background:#079ed9; font-size:16px; }.table-wrap th:first-child,.table-wrap td:first-child { width:22%; text-align:center; }.table-wrap th:nth-child(2),.metric { width:34%; text-align:center; font-weight:700; }.table-wrap td { padding:10px 18px; }.table-wrap select,.weight-table input,.score-table input { display:block; width:min(600px,100%); margin:auto; border:1px solid rgba(115,220,255,.58); border-radius:6px; padding:9px 12px; color:#e9faff; background:#09293c; font:inherit; }.weight-table .first-level { color:#67dcff; font-weight:800; }.weight-table td:last-child,.score-table td:last-child { position:relative; text-align:center; }.weight-table input,.score-table input { display:inline-block; width:150px; margin:0; text-align:center; }.weight-table .percent,.score-table .percent { margin-left:8px; color:#78d9f7; }.score-table th:first-child,.score-table td:first-child { width:16%; text-align:center; }.score-table th:nth-child(2) { width:45%; }.score-table td { padding:12px 18px; }.calculation-guide { display:grid; grid-template-columns:minmax(0,1fr) 210px; gap:22px; margin-top:26px; padding:19px 21px; border:1px solid rgba(64,206,255,.27); border-radius:14px; color:#c9e9f4; background:rgba(9,67,94,.18); line-height:1.75; }.calculation-guide h3 { margin:0 0 8px; color:#fff; font-size:16px; }.calculation-guide p { margin:6px 0; }.download-stack { display:grid; align-content:center; gap:12px; }.download-stack .btn { border-radius:8px; }.step-actions { display:flex; justify-content:center; gap:12px; margin-top:26px; }.placeholder { padding:94px 20px; color:#a8cfdd; text-align:center; }.placeholder p { color:#fff; font-size:20px; } @media (max-width:1280px) { .opportunity-page { grid-template-columns:1fr; }.step-indicator { overflow-x:auto; }.step-tab { min-width:180px; } } @media (max-width:720px) { .opportunity-page { padding:12px; }.workbench { margin:12px; padding:14px; }.task-header { padding:18px; }.workbench-actions { justify-content:flex-start; flex-wrap:wrap; }.step-heading em { display:none; }.weight-summary { flex-wrap:wrap; }.weight-summary small { width:100%; margin-left:0; }.calculation-guide { grid-template-columns:1fr; } }
.visualization-upload { display:grid; justify-items:center; gap:12px; min-height:280px; padding:48px 24px; border:1px dashed rgba(77,214,255,.68); border-radius:16px; background:radial-gradient(circle at 50% 25%,rgba(16,143,193,.23),rgba(5,31,48,.3) 58%,rgba(1,21,34,.5)); text-align:center; }.upload-trigger { display:inline-flex; align-items:center; gap:8px; border:1px solid #41d8ff; border-radius:8px; padding:15px 23px; color:#fff; background:linear-gradient(110deg,#0faee6,#0874af); font:inherit; font-weight:800; cursor:pointer; }.upload-trigger:disabled { cursor:not-allowed; opacity:.6; }.visualization-upload > p { margin:0; color:#8dc9dd; }.visually-hidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; }.visualization-preview { display:flex; align-items:center; width:min(620px,100%); gap:16px; padding:12px; border:1px solid rgba(91,218,255,.4); border-radius:12px; text-align:left; background:rgba(2,17,29,.55); }.visualization-preview .el-image { flex:0 0 164px; height:104px; border-radius:8px; cursor:zoom-in; overflow:hidden; }.visualization-preview div { display:grid; gap:8px; }.visualization-preview strong { color:#fff; }.visualization-preview span { color:#9bcbdc; font-size:13px; }.text-button { justify-self:start; border:0; padding:0; color:#ffb4a5; background:transparent; cursor:pointer; font:inherit; }.visual-guide { margin-top:24px; padding:18px 22px; border-left:3px solid #31d1ff; color:#c9e9f4; background:rgba(8,78,108,.18); line-height:1.75; }.visual-guide h3 { margin:0 0 7px; color:#fff; font-size:16px; }.visual-guide p { margin:5px 0; }
</style>