@ -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 / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< 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 / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / 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 > 每项建议填写 1 – 100 , 合计必须为 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 / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / 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 ( 720 px , 1 fr ) 340 px ; gap : 24 px ; min - height : calc ( 100 vh - 106 px ) ; padding : 24 px ; color : # e9f8ff ; background : # 06111 d ; } . opportunity - core { min - width : 0 ; border : 1 px solid rgba ( 0 , 180 , 255 , .25 ) ; background : rgba ( 8 , 18 , 28 , .64 ) ; box - shadow : 0 22 px 48 px rgba ( 0 , 0 , 0 , .22 ) ; } . task - header { padding : 24 px 28 px 4 px ; } . task - header span , . step - heading p { color : # 66 dcff ; font - size : 13 px ; font - weight : 800 ; letter - spacing : .08 em ; } . task - header h1 { margin : 9 px 0 0 ; color : # fff ; font - size : 29 px ; } . workbench { margin : 24 px ; padding : 22 px ; border : 1 px solid rgba ( 33 , 194 , 255 , .35 ) ; border - radius : 20 px ; background : rgba ( 1 , 24 , 39 , .72 ) ; } . workbench - actions { display : flex ; justify - content : flex - end ; gap : 12 px ; } . btn { display : inline - flex ; align - items : center ; justify - content : center ; gap : 6 px ; border : 1 px solid # 2 cc9ff ; border - radius : 999 px ; padding : 10 px 18 px ; color : # dff8ff ; background : rgba ( 8 , 87 , 121 , .55 ) ; cursor : pointer ; } . primary { border - color : # 36 d9ff ; background : linear - gradient ( 110 deg , # 0 eaae4 , # 0572 ac ) ; font - weight : 700 ; } . btn : disabled { cursor : not - allowed ; opacity : .6 ; } . step - indicator { display : flex ; align - items : center ; gap : 12 px ; margin : 28 px 0 ; padding : 12 px ; border : 1 px solid rgba ( 30 , 190 , 255 , .28 ) ; border - radius : 22 px ; } . step - tab { display : flex ; align - items : center ; flex : 1 ; gap : 10 px ; min - width : 0 ; padding : 10 px ; border : 0 ; border - radius : 16 px ; color : # 96 c6d8 ; background : transparent ; text - align : left ; cursor : pointer ; } . step - tab . active { color : # fff ; background : linear - gradient ( 110 deg , # 148 fc6 , # 0879 aa ) ; } . step - tab b { display : grid ; place - items : center ; flex : 0 0 42 px ; width : 42 px ; height : 42 px ; border : 1 px solid # 4 ed7ff ; border - radius : 50 % ; } . step - tab span { display : grid ; gap : 4 px ; min - width : 0 ; } . step - tab strong { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; } . step - tab small { color : # 5 ec9ed ; } . step - indicator i { flex : 0 0 24 px ; height : 2 px ; background : rgba ( 83 , 204 , 255 , .4 ) ; } . step - card { padding : 24 px ; border : 1 px solid rgba ( 31 , 185 , 249 , .32 ) ; border - radius : 18 px ; 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 : 5 px 0 0 ; color : # fff ; font - size : 24 px ; } . step - heading em { border : 1 px solid # 38 caff ; border - radius : 999 px ; padding : 7 px 12 px ; color : # 9 aeaff ; font - style : normal ; } . intro { margin : 24 px 0 ; color : # c5e4ef ; font - size : 16 px ; line - height : 1.8 ; } . table- wrap { overflow - x : auto ; } . table - wrap table { width : 100 % ; min - width : 720 px ; border - collapse : collapse ; } . table - wrap th , . table - wrap td { border : 1 px solid rgba ( 139 , 213 , 241 , .45 ) ; } . table - wrap th { padding : 14 px ; color : # fff ; background : # 079 ed9 ; font - size : 16 px ; } . 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 : 10 px 18 px ; } . table - wrap select { display : block ; width : min ( 600 px , 100 % ) ; margin : auto ; border : 1 px solid rgba ( 115 , 220 , 255 , .58 ) ; border - radius : 6 px ; padding : 9 px 12 px ; color : # e9faff ; background : # 09293 c ; font : inherit ; } . step- actions { display : flex ; justify - content : center ; gap : 12 px ; margin - top : 26 px ; } . placeholder { padding : 94 px 20 px ; color : # a8cfdd ; text - align : center ; } . placeholder p { color : # fff ; font - size : 20 px ; } @ media ( max - width : 1280 px ) { . opportunity - page { grid - template - columns : 1 fr ; } . step - indicator { overflow - x : auto ; } . step - tab { min - width : 180 px ; } } @ media ( max - width : 720 px ) { . opportunity - page { padding : 12 px ; } . workbench { margin : 12 px ; padding : 14 px ; } . task - header { padding : 18 px ; } . workbench - actions { justify - content : flex - start ; flex - wrap : wrap ; } . step - heading em { display : none ; } }
. opportunity - page { display : grid ; grid - template - columns : minmax ( 720 px , 1 fr ) 340 px ; gap : 24 px ; min - height : calc ( 100 vh - 106 px ) ; padding : 24 px ; color : # e9f8ff ; background : # 06111 d ; } . opportunity - core { min - width : 0 ; border : 1 px solid rgba ( 0 , 180 , 255 , .25 ) ; background : rgba ( 8 , 18 , 28 , .64 ) ; box - shadow : 0 22 px 48 px rgba ( 0 , 0 , 0 , .22 ) ; } . task - header { padding : 24 px 28 px 4 px ; } . task - header span , . step - heading p { color : # 66 dcff ; font - size : 13 px ; font - weight : 800 ; letter - spacing : .08 em ; } . task - header h1 { margin : 9 px 0 0 ; color : # fff ; font - size : 29 px ; } . workbench { margin : 24 px ; padding : 22 px ; border : 1 px solid rgba ( 33 , 194 , 255 , .35 ) ; border - radius : 20 px ; background : rgba ( 1 , 24 , 39 , .72 ) ; } . workbench - actions { display : flex ; justify - content : flex - end ; gap : 12 px ; } . btn { display : inline - flex ; align - items : center ; justify - content : center ; gap : 6 px ; border : 1 px solid # 2 cc9ff ; border - radius : 999 px ; padding : 10 px 18 px ; color : # dff8ff ; background : rgba ( 8 , 87 , 121 , .55 ) ; cursor : pointer ; } . primary { border - color : # 36 d9ff ; background : linear - gradient ( 110 deg , # 0 eaae4 , # 0572 ac ) ; font - weight : 700 ; } . btn : disabled { cursor : not - allowed ; opacity : .6 ; } . step - indicator { display : flex ; align - items : center ; gap : 12 px ; margin : 28 px 0 ; padding : 12 px ; border : 1 px solid rgba ( 30 , 190 , 255 , .28 ) ; border - radius : 22 px ; } . step - tab { display : flex ; align - items : center ; flex : 1 ; gap : 10 px ; min - width : 0 ; padding : 10 px ; border : 0 ; border - radius : 16 px ; color : # 96 c6d8 ; background : transparent ; text - align : left ; cursor : pointer ; } . step - tab . active { color : # fff ; background : linear - gradient ( 110 deg , # 148 fc6 , # 0879 aa ) ; } . step - tab b { display : grid ; place - items : center ; flex : 0 0 42 px ; width : 42 px ; height : 42 px ; border : 1 px solid # 4 ed7ff ; border - radius : 50 % ; } . step - tab span { display : grid ; gap : 4 px ; min - width : 0 ; } . step - tab strong { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; } . step - tab small { color : # 5 ec9ed ; } . step - indicator i { flex : 0 0 24 px ; height : 2 px ; background : rgba ( 83 , 204 , 255 , .4 ) ; } . step - card { padding : 24 px ; border : 1 px solid rgba ( 31 , 185 , 249 , .32 ) ; border - radius : 18 px ; 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 : 5 px 0 0 ; color : # fff ; font - size : 24 px ; } . step - heading em { border : 1 px solid # 38 caff ; border - radius : 999 px ; padding : 7 px 12 px ; color : # 9 aeaff ; font - style : normal ; } . intro { margin : 24 px 0 ; color : # c5e4ef ; font - size : 16 px ; line - height : 1.8 ; } . warning { margin : 0 0 16 px ; color : # ffd58e ; } . weight - summary { display : flex ; align - items : baseline ; gap : 10 px ; margin : 10 px 0 18 px ; padding : 13 px 16 px ; border : 1 px solid rgba ( 57 , 207 , 255 , .28 ) ; border - radius : 11 px ; color : # c5e4ef ; background : rgba ( 11 , 81 , 113 , .18 ) ; } . weight - summary strong { color : # ffcf77 ; font - size : 23 px ; } . weight - summary strong . valid { color : # 67 e8a9 ; } . weight - summary small { margin - left : auto ; color : # 8 fc9dc ; } . table- wrap { overflow - x : auto ; } . table - wrap table { width : 100 % ; min - width : 720 px ; border - collapse : collapse ; } . table - wrap th , . table - wrap td { border : 1 px solid rgba ( 139 , 213 , 241 , .45 ) ; } . table - wrap th { padding : 14 px ; color : # fff ; background : # 079 ed9 ; font - size : 16 px ; } . 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 : 10 px 18 px ; } . table - wrap select , . weight - table input { display : block ; width : min ( 600 px , 100 % ) ; margin : auto ; border : 1 px solid rgba ( 115 , 220 , 255 , .58 ) ; border - radius : 6 px ; padding : 9 px 12 px ; color : # e9faff ; background : # 09293 c ; font : inherit ; } . weight- table . first - level { color : # 67 dcff ; font - weight : 800 ; } . weight - table td : last - child { position : relative ; text - align : center ; } . weight - table input { display : inline - block ; width : 150 px ; margin : 0 ; text - align : center ; } . weight - table . percent { margin - left : 8 px ; color : # 78 d9f7 ; } . step- actions { display : flex ; justify - content : center ; gap : 12 px ; margin - top : 26 px ; } . placeholder { padding : 94 px 20 px ; color : # a8cfdd ; text - align : center ; } . placeholder p { color : # fff ; font - size : 20 px ; } @ media ( max - width : 1280 px ) { . opportunity - page { grid - template - columns : 1 fr ; } . step - indicator { overflow - x : auto ; } . step - tab { min - width : 180 px ; } } @ media ( max - width : 720 px ) { . opportunity - page { padding : 12 px ; } . workbench { margin : 12 px ; padding : 14 px ; } . task - header { padding : 18 px ; } . 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 >