feat: add market opportunity selection step two

dev-QQq
chenyuan 3 weeks ago
parent 3f4763bc0f
commit a3649d9f5e

@ -109,6 +109,20 @@ export function checkMarketOpportunitySelectionStepOne(items) {
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-1/check", method: "post", data: { items }, headers: { repeatSubmit: false } }); return request({ url: "/api/student-training-answers/market-opportunity-selection/step-1/check", method: "post", data: { items }, headers: { repeatSubmit: false } });
} }
export function checkMarketOpportunitySelectionStepTwo(payload) {
const weights = Array.isArray(payload?.weights) ? payload.weights : [];
const expectedIds = ["search-count", "transaction-amount", "transaction-growth", "category-transaction-growth", "demand-supply-ratio", "online-products", "online-merchants", "organic-traffic-ratio"];
if (isStudentDemo()) {
const ids = new Set(weights.map((item) => item?.id));
const total = weights.reduce((sum, item) => sum + Number(item?.weight || 0), 0);
if (weights.length !== expectedIds.length || expectedIds.some((id) => !ids.has(id)) || weights.some((item) => !Number.isInteger(Number(item?.weight)) || Number(item.weight) < 1 || Number(item.weight) > 100) || total !== 100) {
return Promise.reject(new Error("二级指标权重需为 1 至 100 的整数,且合计必须为 100%"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({ url: "/api/student-training-answers/market-opportunity-selection/step-2/validate", method: "post", data: { weights }, headers: { repeatSubmit: false } });
}
function validateNewProductSurveyStepTwoDemo(form) { function validateNewProductSurveyStepTwoDemo(form) {
const value = form || {}; const value = form || {};
const images = Array.isArray(value.images) ? value.images : []; const images = Array.isArray(value.images) ? value.images : [];

@ -13,7 +13,15 @@
<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="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> <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>
<div v-else class="placeholder"><p> {{ currentStep }} 步内容待配置</p><span> 1 步指标分类已保存可返回查看</span></div> <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>
<div v-else class="placeholder"><p> {{ currentStep }} 步内容待配置</p><span>已保存的步骤可从上方步骤栏返回查看</span></div>
</section> </section>
</section> </section>
</main> </main>
@ -23,7 +31,7 @@
<script setup> <script setup>
import { Download, RefreshLeft } from "@element-plus/icons-vue"; import { Download, RefreshLeft } from "@element-plus/icons-vue";
import { checkMarketOpportunitySelectionStepOne, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer"; import { checkMarketOpportunitySelectionStepOne, checkMarketOpportunitySelectionStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask"; import { getTrainingTaskByKey } from "@/api/trainingTask";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"; import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"; import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
@ -33,26 +41,32 @@ const TASK_KEY = "market-opportunity-selection";
const DEFAULT_STEPS = ["指标分类", "市场机会分析", "选品方案设计", "选品结论"]; const DEFAULT_STEPS = ["指标分类", "市场机会分析", "选品方案设计", "选品结论"];
const categories = ["市场规模", "市场潜力", "竞争强度", "运营难度"]; const categories = ["市场规模", "市场潜力", "竞争强度", "运营难度"];
const indicators = [ const indicators = [
{ id: "search-count", name: "搜索次数" }, { id: "demand-supply-ratio", name: "需供比" }, { id: "transaction-amount", name: "成交金额(元)" }, { id: "transaction-growth", name: "成交金额增速" }, { id: "search-count", name: "搜索次数", group: "市场规模" }, { id: "transaction-amount", name: "成交金额(元)", group: "市场规模" }, { id: "transaction-growth", name: "成交金额增速", group: "市场潜力" }, { id: "category-transaction-growth", name: "类目成交增速", group: "市场潜力" },
{ id: "category-transaction-growth", name: "类目成交增速" }, { id: "online-products", name: "在线商品数" }, { id: "online-merchants", name: "在线商家数" }, { id: "organic-traffic-ratio", name: "自然流占比" }, { id: "demand-supply-ratio", name: "需供比", group: "竞争强度" }, { id: "online-products", name: "在线商品数", group: "竞争强度" }, { id: "online-merchants", name: "在线商家数", group: "竞争强度" }, { id: "organic-traffic-ratio", name: "自然流占比", group: "运营难度" },
]; ];
const defaultTask = { title: "市场机会识别与选品", background: "基于实训数据中的市场、供给与流量指标,完成市场机会识别和选品判断。", goals: ["理解选品数据指标的业务含义", "掌握市场规模、潜力、竞争和运营难度的分类方法", "为后续选品分析建立指标基础"], requirement: "阅读案例资料,完成 8 项指标的分类,并依据判题反馈修改。" }; const defaultTask = { title: "市场机会识别与选品", background: "基于实训数据中的市场、供给与流量指标,完成市场机会识别和选品判断。", goals: ["理解选品数据指标的业务含义", "掌握市场规模、潜力、竞争和运营难度的分类方法", "为后续选品分析建立指标基础"], requirement: "阅读案例资料,完成 8 项指标的分类,并依据判题反馈修改。" };
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const stepOnePassed = ref(false); const answers = ref(createAnswers()); const taskConfig = ref(null); const currentStep = ref(1); const saving = ref(false); const stepOnePassed = ref(false); const stepTwoPassed = ref(false); const answers = ref(createAnswers()); const weights = ref(createWeights());
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(() => parseArray(taskConfig.value?.steps, DEFAULT_STEPS)); 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(() => parseArray(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 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(); }); onMounted(async () => { await loadTaskConfig(); await loadSavedAnswer(); });
function createAnswers() { return Object.fromEntries(indicators.map((item) => [item.id, ""])); } function createAnswers() { return Object.fromEntries(indicators.map((item) => [item.id, ""])); }
function createWeights() { return Object.fromEntries(indicators.map((item) => [item.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; } } 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 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 (Number(answer?.currentStep) > 0) currentStep.value = Number(answer.currentStep); } catch (error) { /* Keep the page available when history cannot be restored. */ } } 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 (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 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)])) }; }
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; } } 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 switchStep(step) { if (step > 1 && !stepOnePassed.value) { proxy?.$modal?.msgWarning("请先完成第 1 步指标分类"); return; } currentStep.value = step; } 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; } 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; } }
function resetStepOne() { answers.value = createAnswers(); proxy?.$modal?.msgSuccess("已清空第 1 步填写内容"); } function resetStepOne() { answers.value = createAnswers(); proxy?.$modal?.msgSuccess("已清空第 1 步填写内容"); }
function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: taskTitle.value, step1: buildStepOneOutcome(), 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); } function resetStepTwo() { weights.value = createWeights(); proxy?.$modal?.msgSuccess("已清空第 2 步填写内容"); }
function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: taskTitle.value, step1: buildStepOneOutcome(), step2: buildStepTwoOutcome(), 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> </script>
<style lang="scss" scoped> <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; }.table-wrap { overflow-x:auto; }.table-wrap table { width:100%; min-width:720px; border-collapse:collapse; }.table-wrap th,.table-wrap td { border:1px solid rgba(139,213,241,.45); }.table-wrap th { padding:14px; color:#fff; background:#079ed9; font-size:16px; }.table-wrap th:first-child,.table-wrap td:first-child { width:15%; text-align:center; }.table-wrap th:nth-child(2),.metric { width:26%; text-align:center; font-weight:700; }.table-wrap td { padding:10px 18px; }.table-wrap select { 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; }.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; } } .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 { overflow-x:auto; }.table-wrap table { width:100%; min-width:720px; border-collapse:collapse; }.table-wrap th,.table-wrap td { border:1px solid rgba(139,213,241,.45); }.table-wrap 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 { 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 { position:relative; text-align:center; }.weight-table input { display:inline-block; width:150px; margin:0; text-align:center; }.weight-table .percent { margin-left:8px; color:#78d9f7; }.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; } }
</style> </style>

Loading…
Cancel
Save