< template >
< div class = "product-standard-page student-training-shell" >
< main class = "training-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 >
< / section >
< section class = "task-card" >
< div class = "workbench-head workbench-head--actions-only" >
< div class = "workbench-actions" >
< TrainingMaterialButton
: 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"
/ >
< button type = "button" class = "outline-action" @click ="exportOutcome" > < el -icon > < Download / > < / e l - i c o n > 导 出 实 训 成 果 < / b u t t o n >
< / div >
< / div >
< nav v-if ="hasConfiguredSteps" class="step-indicator" :aria-label="`${taskTitle}步骤`" >
< template v-for ="(step, index) in configuredSteps" :key="`${step}-${index}`" >
< button type = "button" : class = "['step-tab', { active: activeStep === index + 1, complete: activeStep > index + 1 }]" : aria -current = " activeStep = = = index + 1 ? ' step ' : undefined " : aria -label = " ` 第 $ { index + 1 } 步 : $ { step } ` " @ click = "setCurrentStep(index + 1)" >
< span class = "step-marker" > { { String ( index + 1 ) . padStart ( 2 , '0' ) } } < / span >
< span class = "step-copy" > < strong class = "step-name" :title ="step" > { { step } } < / strong > < span class = "step-action" > { { activeStep === index + 1 ? "当前步骤" : "点击切换" } } < span aria -hidden = " true " > → < / span > < / span > < / span >
< / button >
< span v-if ="index < configuredSteps.length - 1" class="step-connector" aria-hidden="true" > < / span >
< / template >
< / nav >
< section class = "step-card" >
< div class = "step-title-row" >
< div >
< p v-if ="hasConfiguredSteps" class="section-eyebrow" > Step {{ String ( activeStep ) .padStart ( 2 , " 0 " ) }} < / p >
< h3 > { { activeStepName } } < / h3 >
< / div >
< / div >
< div class = "step-content" >
< slot v -if = " hasCustomContent " name = "step-content" :active-step ="activeStep" :active-step-name ="activeStepName" :has-configured-steps ="hasConfiguredSteps" / >
< template v-else >
< p v-if ="page.keyPoints?.length && isFirstStep" class="step-desc" > 请 围 绕 以 下 内 容 组 织 需 求 说 明 书 , 确 保 文 档 能 够 支 撑 设 计 、 开 发 、 测 试 和 验 收 协 作 。 < / p >
< div v-if ="page.keyPoints?.length && isFirstStep" class="key-point-grid" >
< span v-for ="item in page.keyPoints" :key ="item" > {{ item }} < / span >
< / div >
< label v-for ="field in visibleFields" :key="field.key" class="field-block" >
< span > { { field . label } } < / span >
< el -input v -model = " form [ field.key ] " : type = "field.type === 'textarea' ? 'textarea' : 'text'" : autosize = "field.type === 'textarea' ? { minRows: 6, maxRows: 11 } : undefined" :placeholder ="field.placeholder" / >
< / label >
< div v-if ="showWorkTable" class="table-panel" >
< div class = "panel-title" > < h4 > { { page . tableTitle || "实训工作表" } } < / h4 > < p > 请结合产品对象填写对应内容 , 形成可提交的实训成果 。 < / p > < / div >
< div class = "table-wrap" >
< table >
< thead > < tr > < th > 项目 < / th > < th > 说明 < / th > < th > 学生填写 < / th > < / tr > < / thead >
< tbody > < tr v-for ="row in workRows" :key="row.name"><th scope="row" > {{ row.name }} < / th > < td > { { row . description } } < / td > < td > < el -input v -model = " row.answer " type = "textarea" : autosize = "{ minRows: 3, maxRows: 6 }" placeholder = "填写本项分析内容" / > < / td > < / tr > < / tbody >
< / table >
< / div >
< / div >
< p v-if ="!visibleFields.length && !showWorkTable" class="step-desc" > 请 根 据 本 步 骤 要 求 梳 理 前 述 内 容 , 确 认 信 息 完 整 后 继 续 下 一 步 。 < / p >
< / template >
< / div >
< / section >
< div v-if ="hasConfiguredSteps" class="step-navigation" >
< button type = "button" class = "btn-nav" : disabled = "activeStep === 1" @click ="goPreviousStep" > < el -icon > < ArrowLeft / > < / e l - i c o n > 上 一 步 < / b u t t o n >
< button type = "button" class = "btn-nav btn-save" :disabled ="saving" @click ="saveCurrentProgress" > < el -icon > < CircleCheck / > < / e l - i c o n > 保 存 当 前 进 度 < / b u t t o n >
< button type = "button" class = "btn-nav btn-primary" : disabled = "activeStep === configuredSteps.length" @click ="goNextStep" > 下 一 步 < el -icon > < ArrowRight / > < / e l - i c o n > < / b u t t o n >
< / div >
< div class = "training-actions" >
< el -button class = "training-action training-action--submit" :loading ="saving" @click ="submitTask" > 提 交 任 务 < / el -button >
< el -button class = "training-action training-action--reset" :loading ="saving" @click ="resetTask" > 清 空 填 写 < / el -button >
< / div >
< / section >
< / main >
< TrainingAiSidebar :study-tip ="`当前任务建议:围绕“${taskTitle}”先明确产品对象,再按配置步骤补全实训内容。`" :progress-items ="progressItems" / >
< / div >
< / template >
< script setup >
import { computed , getCurrentInstance , onMounted , ref , useSlots } from "vue" ;
import { ArrowLeft , ArrowRight , CircleCheck , Document , Download } from "@element-plus/icons-vue" ;
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue" ;
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue" ;
import { getTrainingTaskByKey } from "@/api/trainingTask" ;
import { getStudentTrainingAnswer , saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer" ;
import { getStudentTrainingPage } from "@/views/training/studentTrainingPages" ;
import { parseTrainingArray } from "@/views/training/taskKeyMap" ;
const props = defineProps ( { taskKey : { type : String , required : true } , title : { type : String , default : "" } , background : { type : String , default : "" } , requirement : { type : String , default : "" } , goals : { type : Array , default : ( ) => [ ] } , badgeText : { type : String , default : "核心实训 · 产品规划与设计" } } ) ;
const emit = defineEmits ( [ "save" , "submit" , "reset" ] ) ;
const slots = useSlots ( ) ;
const { proxy } = getCurrentInstance ( ) ;
const taskConfig = ref ( null ) ;
const briefOpen = ref ( false ) ;
const saving = ref ( false ) ;
const activeStep = ref ( 1 ) ;
const page = computed ( ( ) => getStudentTrainingPage ( props . taskKey ) ) ;
const form = ref ( createForm ( ) ) ;
const workRows = ref ( createRows ( ) ) ;
const hasCustomContent = computed ( ( ) => Boolean ( slots [ "step-content" ] ) ) ;
const taskTitle = computed ( ( ) => taskConfig . value ? . taskName || props . title || page . value . title ) ;
const taskBackground = computed ( ( ) => taskConfig . value ? . background || props . background || page . value . background ) ;
const taskRequirement = computed ( ( ) => taskConfig . value ? . requirements || props . requirement || page . value . task ) ;
const taskGoals = computed ( ( ) => parseTrainingArray ( taskConfig . value ? . objectives , props . goals . length ? props . goals : page . value . goals ) ) ;
const configuredSteps = computed ( ( ) => parseTrainingArray ( taskConfig . value ? . steps ) ) ;
const hasConfiguredSteps = computed ( ( ) => configuredSteps . value . length > 0 ) ;
const activeStepName = computed ( ( ) => configuredSteps . value [ activeStep . value - 1 ] || page . value . title ) ;
const fieldsPerStep = computed ( ( ) => hasConfiguredSteps . value ? Math . max ( 1 , Math . ceil ( ( page . value . fields ? . length || 0 ) / configuredSteps . value . length ) ) : page . value . fields ? . length || 0 ) ;
const visibleFields = computed ( ( ) => {
const fields = page . value . fields || [ ] ;
if ( ! hasConfiguredSteps . value ) return fields ;
const start = ( activeStep . value - 1 ) * fieldsPerStep . value ;
return fields . slice ( start , start + fieldsPerStep . value ) ;
} ) ;
const showWorkTable = computed ( ( ) => Boolean ( workRows . value . length ) && ( ! hasConfiguredSteps . value || activeStep . value === configuredSteps . value . length ) ) ;
const isFirstStep = computed ( ( ) => ! hasConfiguredSteps . value || activeStep . value === 1 ) ;
const progressItems = computed ( ( ) => configuredSteps . value . map ( ( text , index ) => ( { text , status : index + 1 < activeStep . value ? "done" : index + 1 === activeStep . value ? "doing" : "" } ) ) ) ;
function createForm ( ) { return Object . fromEntries ( ( page . value . fields || [ ] ) . map ( ( field ) => [ field . key , "" ] ) ) ; }
function createRows ( ) { return ( page . value . tableRows || [ ] ) . map ( ( [ name , description ] ) => ( { name , description , answer : "" } ) ) ; }
function normalizeActiveStep ( value ) { if ( ! configuredSteps . value . length ) return 1 ; return Math . min ( Math . max ( Number ( value ) || 1 , 1 ) , configuredSteps . value . length ) ; }
function buildProgress ( ) { return { activeStep : activeStep . value , form : form . value , workRows : workRows . value , updatedAt : new Date ( ) . toISOString ( ) } ; }
function applyProgress ( value ) { if ( ! value || typeof value !== "object" ) return ; activeStep . value = normalizeActiveStep ( value . activeStep ) ; form . value = { ... createForm ( ) , ... ( value . form || { } ) } ; workRows . value = createRows ( ) . map ( ( row , index ) => ( { ... row , answer : value . workRows ? . [ index ] ? . answer || "" } ) ) ; }
onMounted ( async ( ) => { await loadTaskConfig ( ) ; await loadSavedProgress ( ) ; } ) ;
async function loadTaskConfig ( ) { try { const res = await getTrainingTaskByKey ( props . taskKey ) ; taskConfig . value = res ? . data || null ; activeStep . value = normalizeActiveStep ( activeStep . value ) ; } catch ( error ) { taskConfig . value = null ; activeStep . value = 1 ; } }
async function loadSavedProgress ( ) { try { const res = await getStudentTrainingAnswer ( props . taskKey ) ; const raw = res ? . data ? . step1Answer ; if ( raw ) applyProgress ( JSON . parse ( raw ) ) ; } catch ( error ) { /* The training task remains available without previous progress. */ } }
async function persistProgress ( saveAction = "SAVE" , silent = false ) { await saveStudentTrainingAnswer ( props . taskKey , { saveAction , currentStep : activeStep . value , step1Answer : JSON . stringify ( buildProgress ( ) ) } ) ; if ( ! silent ) proxy ? . $modal ? . msgSuccess ( saveAction === "SUBMIT" ? "提交成功" : "保存成功" ) ; }
function setCurrentStep ( step ) { activeStep . value = normalizeActiveStep ( step ) ; persistProgress ( "SAVE" , true ) . catch ( ( ) => { } ) ; }
function goPreviousStep ( ) { setCurrentStep ( activeStep . value - 1 ) ; }
function goNextStep ( ) { setCurrentStep ( activeStep . value + 1 ) ; }
async function saveCurrentProgress ( ) { if ( saving . value ) return ; saving . value = true ; try { if ( hasCustomContent . value ) { emit ( "save" ) ; await persistProgress ( "SAVE" , true ) ; } else { await persistProgress ( ) ; } } finally { saving . value = false ; } }
async function submitTask ( ) { if ( saving . value ) return ; saving . value = true ; try { if ( hasCustomContent . value ) { emit ( "submit" ) ; await persistProgress ( "SAVE" , true ) ; } else { await persistProgress ( "SUBMIT" ) ; } } finally { saving . value = false ; } }
async function resetTask ( ) { if ( saving . value ) return ; saving . value = true ; try { if ( hasCustomContent . value ) emit ( "reset" ) ; form . value = createForm ( ) ; workRows . value = createRows ( ) ; activeStep . value = 1 ; await persistProgress ( "RESET" , hasCustomContent . value ) ; if ( ! hasCustomContent . value ) proxy ? . $modal ? . msgSuccess ( "清空填写成功" ) ; } finally { saving . value = false ; } }
function exportOutcome ( ) { const blob = new Blob ( [ JSON . stringify ( buildProgress ( ) , null , 2 ) ] , { type : "application/json;charset=utf-8" } ) ; const url = URL . createObjectURL ( blob ) ; const link = document . createElement ( "a" ) ; link . href = url ; link . download = ` ${ taskTitle . value } -实训成果.json ` ; link . click ( ) ; URL . revokeObjectURL ( url ) ; }
< / script >
< style scoped lang = "scss" >
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 教育蓝极简 · 产品标准实训页
* === === === === === === === === === === === === === === === === === === === === * /
. product - standard - page {
/* 主色:教育蓝 */
-- student - accent : # 2563 eb ;
-- student - accent - deep : # 1 d4ed8 ;
-- student - accent - light : # 3 b82f6 ;
-- student - accent - soft : # eff6ff ;
/* 语义色 */
-- student - danger : # ef4444 ;
-- student - success : # 2 ba471 ;
/* 三层次标识(仅小色条) */
-- student - layer - 1 : # 1 d4ed8 ;
-- student - layer - 2 : # 3 b82f6 ;
-- student - layer - 3 : # 93 c5fd ;
/* 结构色 */
-- student - panel - border : # e5e7eb ;
-- student - card - bg : # ffffff ;
-- student - panel - bg : # fafbfc ;
-- student - text : # 1 f2937 ;
-- student - text - secondary : # 6 b7280 ;
/* 字号梯度:紧凑 */
-- student - fs - display : 17 px ;
-- student - fs - h1 : 15 px ;
-- student - fs - h2 : 14 px ;
-- student - fs - h3 : 13 px ;
-- student - fs - body : 13 px ;
-- student - fs - body - sm : 12 px ;
-- student - fs - caption : 11 px ;
-- student - fs - button : 13 px ;
-- student - fs - button - lg : 13 px ;
-- student - fs - data : 14 px ;
display : grid ;
grid - template - columns : minmax ( 720 px , 1 fr ) 340 px ;
gap : 24 px ;
min - height : calc ( 100 vh - 76 px ) ;
padding : 24 px 24 px 32 px ;
color : var ( -- student - text ) ;
background : # ffffff ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 训练核心容器
* === === === === === === === === === === === === === === === === === === === === * /
. training - core {
display : grid ;
gap : 12 px ;
align - content : start ;
min - width : 0 ;
padding : 20 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : # ffffff ;
box - shadow : none ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 任务头 : 白卡 + 左侧教育蓝竖条
* === === === === === === === === === === === === === === === === === === === === * /
. task - header {
position : relative ;
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 16 px ;
margin : 0 ;
padding : 18 px 20 px 18 px 24 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : var ( -- student - card - bg ) ;
overflow : hidden ;
& : : before {
content : "" ;
position : absolute ;
top : 0 ;
bottom : 0 ;
left : 0 ;
width : 4 px ;
background : var ( -- student - accent ) ;
}
}
. task - head - main {
display : grid ;
justify - items : start ;
gap : 8 px ;
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.4 ;
letter - spacing : - 0.01 em ;
}
. task - brief {
margin : 0 ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.65 ;
}
. task - brief - text {
display : - webkit - box ;
overflow : hidden ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 1 ;
}
. task - brief - toggle {
margin - left : 6 px ;
padding : 0 ;
border : 0 ;
color : var ( -- student - accent ) ;
background : transparent ;
font : inherit ;
font - weight : 600 ;
cursor : pointer ;
& : hover {
color : var ( -- student - accent - deep ) ;
text - decoration : underline ;
}
}
. task - brief - more {
display : grid ;
gap : 6 px ;
margin - top : 8 px ;
padding - top : 8 px ;
border - top : 1 px dashed # e5e7eb ;
b {
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body - sm ) ;
}
ul {
display : grid ;
gap : 3 px ;
margin : 0 ;
padding - left : 18 px ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.6 ;
}
p {
margin : 0 ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.6 ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 工作台容器
* === === === === === === === === === === === === === === === === === === === === * /
. task - card {
padding : 0 ;
border : 0 ;
border - radius : 0 ;
background : transparent ;
display : grid ;
gap : 10 px ;
}
. workbench - head -- actions - only {
justify - content : flex - end ;
margin - bottom : 0 ;
padding - bottom : 0 ;
border - bottom : 0 ;
}
. workbench - head {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 18 px ;
margin - bottom : 12 px ;
padding - bottom : 12 px ;
border - bottom : 1 px solid var ( -- student - panel - border ) ;
}
. workbench - actions {
display : flex ;
flex - wrap : wrap ;
justify - content : flex - end ;
gap : 8 px ;
flex - shrink : 0 ;
}
. section - eyebrow {
margin : 0 ;
color : var ( -- student - accent ) ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 700 ;
letter - spacing : 0.1 em ;
text - transform : uppercase ;
}
. step - title - row {
margin - bottom : 10 px ;
}
. step - title - row h3 {
margin : 4 px 0 0 ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h1 ) ;
font - weight : 700 ;
line - height : 1.35 ;
}
. step - desc {
margin : 0 0 14 px ;
color : # 4 b5563 ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.7 ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 按钮 ( 教育蓝极简 )
* === === === === === === === === === === === === === === === === === === === === * /
. outline - action ,
. btn - nav {
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
min - height : 36 px ;
height : 36 px ;
padding : 0 14 px ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 8 px ;
color : var ( -- student - accent ) ;
background : # ffffff ;
font - size : var ( -- student - fs - button ) ;
font - weight : 600 ;
font - family : inherit ;
cursor : pointer ;
transition : border - color 0.16 s ease , background 0.16 s ease ,
color 0.16 s ease , transform 0.16 s ease ,
box - shadow 0.16 s ease ;
}
. outline - action : hover : not ( : disabled ) ,
. btn - nav : hover : not ( : disabled ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
color : var ( -- student - accent ) ;
transform : translateY ( - 1 px ) ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ;
}
. btn - primary {
border - color : var ( -- student - accent ) ;
color : # ffffff ;
background : var ( -- student - accent ) ;
box - shadow : 0 1 px 2 px rgba ( 37 , 99 , 235 , 0.2 ) , 0 2 px 8 px rgba ( 37 , 99 , 235 , 0.15 ) ;
}
. btn - primary : hover : not ( : disabled ) {
border - color : var ( -- student - accent - deep ) ;
background : var ( -- student - accent - deep ) ;
color : # ffffff ;
transform : translateY ( - 1 px ) ;
box - shadow : 0 2 px 6 px rgba ( 29 , 78 , 216 , 0.3 ) , 0 4 px 12 px rgba ( 29 , 78 , 216 , 0.22 ) ;
}
. btn - save {
border : 1 px solid # e5e7eb ;
color : # 374151 ;
background : # f9fafb ;
}
. btn - save : hover : not ( : disabled ) {
border - color : # d1d5db ;
background : # f3f4f6 ;
color : # 374151 ;
transform : none ;
box - shadow : none ;
}
. btn - nav : disabled {
cursor : not - allowed ;
opacity : 0.5 ;
transform : none ;
box - shadow : none ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 步骤导航 ( 紧凑 )
* === === === === === === === === === === === === === === === === === === === === * /
. step - indicator {
display : flex ;
align - items : stretch ;
gap : 0 ;
width : 100 % ;
margin : 0 0 16 px ;
padding : 0 ;
border : none ;
border - radius : 0 ;
background : transparent ;
box - shadow : none ;
overflow - x : auto ;
scrollbar - width : thin ;
}
. step - tab {
display : flex ;
flex : 1 0 145 px ;
align - items : center ;
min - width : 0 ;
min - height : 40 px ;
padding : 8 px 12 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - right : none ;
border - radius : 0 ;
color : # 6 b7280 ;
background : # ffffff ;
font - size : var ( -- student - fs - body ) ;
font - weight : 600 ;
text - align : left ;
cursor : pointer ;
transition : all 0.16 s ease ;
& : first - child {
border - radius : 8 px 0 0 8 px ;
}
& : last - child {
border - radius : 0 8 px 8 px 0 ;
border - right : 1 px solid var ( -- student - panel - border ) ;
}
& : hover : not ( . active ) : not ( . complete ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent - soft ) ;
color : var ( -- student - accent ) ;
z - index : 1 ;
}
& . complete : not ( . active ) {
color : # 374151 ;
. step - marker {
background : rgba ( 43 , 164 , 113 , 0.12 ) ;
color : # 16915 f ;
}
& : hover {
border - color : var ( -- student - success ) ;
background : # f0fdf6 ;
. step - marker {
background : var ( -- student - success ) ;
color : # ffffff ;
}
}
}
& . active {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent ) ;
color : # ffffff ;
font - weight : 700 ;
z - index : 2 ;
. step - marker {
background : # ffffff ;
color : var ( -- student - accent ) ;
}
. step - action {
color : rgba ( 255 , 255 , 255 , 0.65 ) ;
}
& : hover {
background : var ( -- student - accent - deep ) ;
border - color : var ( -- student - accent - deep ) ;
}
}
}
. step - marker {
display : inline - flex ;
flex : 0 0 22 px ;
align - items : center ;
justify - content : center ;
width : 22 px ;
height : 22 px ;
border - radius : 6 px ;
background : # f3f4f6 ;
color : # 6 b7280 ;
font - family : ui - monospace , "SF Mono" , Consolas , monospace ;
font - size : 11 px ;
font - weight : 700 ;
line - height : 1 ;
transition : all 0.16 s ease ;
}
. step - copy {
display : grid ;
min - width : 0 ;
gap : 2 px ;
margin - left : 8 px ;
}
. step - name {
overflow : hidden ;
display : - webkit - box ;
- webkit - box - orient : vertical ;
- webkit - line - clamp : 2 ;
font - size : var ( -- student - fs - body ) ;
font - weight : 600 ;
line - height : 1.25 ;
color : inherit ;
}
. step - action {
color : var ( -- student - accent ) ;
font - size : var ( -- student - fs - caption ) ;
font - weight : 600 ;
letter - spacing : 0.02 em ;
line - height : 1 ;
}
. step - connector {
display : none ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 步骤卡片
* === === === === === === === === === === === === === === === === === === === === * /
. step - card {
padding : 18 px 20 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 12 px ;
background : # ffffff ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 内容区
* === === === === === === === === === === === === === === === === === === === === * /
. step - content {
display : grid ;
gap : 12 px ;
}
. key - point - grid {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
margin : 0 ;
}
. key - point - grid span {
padding : 12 px 14 px ;
border : 1 px solid # bfdbfe ;
border - radius : 8 px ;
background : var ( -- student - accent - soft ) ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body ) ;
font - weight : 600 ;
line - height : 1.55 ;
}
. field - block {
display : block ;
margin : 8 px 0 0 ;
}
. field - block > span {
display : block ;
margin - bottom : 6 px ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body ) ;
font - weight : 600 ;
line - height : 1.6 ;
}
. field - block : deep ( . el - input _ _wrapper ) ,
. field - block : deep ( . el - textarea _ _inner ) ,
. table - wrap : deep ( . el - textarea _ _inner ) {
border : 1 px solid # d1d5db ;
border - radius : 8 px ;
box - shadow : none ! important ;
background : # ffffff ;
}
. field - block : deep ( . el - input _ _wrapper ) {
min - height : 40 px ;
padding : 0 12 px ;
}
. field - block : deep ( . el - input _ _inner ) ,
. field - block : deep ( . el - textarea _ _inner ) ,
. table - wrap : deep ( . el - textarea _ _inner ) {
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body ) ;
}
. field - block : deep ( . el - textarea _ _inner ) ,
. table - wrap : deep ( . el - textarea _ _inner ) {
min - height : 110 px ! important ;
padding : 10 px 12 px ;
line - height : 1.65 ;
}
. field - block : deep ( . el - input _ _wrapper : hover ) ,
. field - block : deep ( . el - textarea _ _inner : hover ) {
border - color : var ( -- student - accent - light ) ;
}
. field - block : deep ( . el - input _ _wrapper . is - focus ) ,
. field - block : deep ( . el - textarea _ _inner : focus ) {
border - color : var ( -- student - accent ) ;
box - shadow : 0 0 0 3 px rgba ( 37 , 99 , 235 , 0.1 ) ! important ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 表格
* === === === === === === === === === === === === === === === === === === === === * /
. table - panel {
margin - top : 14 px ;
padding : 16 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
background : # ffffff ;
}
. panel - title {
margin - bottom : 10 px ;
}
. panel - title h4 {
margin : 0 0 4 px ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 700 ;
}
. panel - title p {
margin : 0 ;
color : var ( -- student - text - secondary ) ;
font - size : var ( -- student - fs - body - sm ) ;
line - height : 1.6 ;
}
. table - wrap {
overflow - x : auto ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 8 px ;
}
. table - wrap table {
width : 100 % ;
min - width : 760 px ;
border - collapse : collapse ;
}
. table - wrap th ,
. table - wrap td {
padding : 10 px 12 px ;
border - right : 1 px solid var ( -- student - panel - border ) ;
border - bottom : 1 px solid var ( -- student - panel - border ) ;
vertical - align : top ;
}
. table - wrap thead th {
color : var ( -- student - text - secondary ) ;
background : # f9fafb ;
font - size : var ( -- student - fs - body - sm ) ;
font - weight : 700 ;
text - align : left ;
}
. table - wrap tbody th {
width : 18 % ;
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body - sm ) ;
font - weight : 700 ;
}
. table - wrap td {
color : var ( -- student - text ) ;
font - size : var ( -- student - fs - body ) ;
line - height : 1.6 ;
}
. table - wrap tr : last - child th ,
. table - wrap tr : last - child td {
border - bottom : 0 ;
}
. table - wrap th : last - child ,
. table - wrap td : last - child {
border - right : 0 ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 底部导航
* === === === === === === === === === === === === === === === === === === === === * /
. step - navigation {
display : flex ;
justify - content : center ;
gap : 10 px ;
margin - top : 14 px ;
padding : 12 px 16 px ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 10 px ;
background : # ffffff ;
}
. training - actions {
display : flex ;
flex - wrap : wrap ;
justify - content : center ;
gap : 12 px ;
margin - top : 20 px ;
}
. training - actions : deep ( . training - action ) {
display : inline - flex ;
align - items : center ;
justify - content : center ;
min - width : 120 px ;
height : 40 px ;
padding : 0 20 px ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ;
border - radius : 10 px ;
color : var ( -- student - accent ) ;
background : # ffffff ;
font - size : var ( -- student - fs - h2 ) ;
font - weight : 600 ;
cursor : pointer ;
transition : all 0.16 s ease ;
}
. training - actions : deep ( . training - action -- submit ) {
border - color : var ( -- student - accent ) ;
background : var ( -- student - accent ) ;
color : # ffffff ;
box - shadow : 0 1 px 2 px rgba ( 37 , 99 , 235 , 0.2 ) , 0 2 px 8 px rgba ( 37 , 99 , 235 , 0.15 ) ;
}
. training - actions : deep ( . training - action -- submit : hover ) {
background : var ( -- student - accent - deep ) ;
border - color : var ( -- student - accent - deep ) ;
box - shadow : 0 2 px 6 px rgba ( 29 , 78 , 216 , 0.3 ) , 0 4 px 12 px rgba ( 29 , 78 , 216 , 0.22 ) ;
}
. training - actions : deep ( . training - action -- reset ) {
border - color : var ( -- student - panel - border ) ;
background : # f9fafb ;
color : var ( -- student - text - secondary ) ;
}
. training - actions : deep ( . training - action -- reset : hover ) {
border - color : # d1d5db ;
background : # f3f4f6 ;
color : var ( -- student - text ) ;
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 响应式
* === === === === === === === === === === === === === === === === === === === === * /
@ media ( max - width : 1100 px ) {
. product - standard - page {
grid - template - columns : 1 fr ;
}
}
@ media ( max - width : 900 px ) {
. product - standard - page {
padding : 14 px ;
}
. training - core {
padding : 14 px ;
border - radius : 10 px ;
}
. step - indicator {
align - items : stretch ;
flex - direction : column ;
gap : 8 px ;
width : 100 % ;
overflow : visible ;
}
. step - tab {
flex : 0 0 auto ;
width : 100 % ;
border : 1 px solid var ( -- student - panel - border ) ;
border - radius : 8 px ! important ;
}
. step - tab : first - child ,
. step - tab : last - child {
border - radius : 8 px ! important ;
}
. step - connector {
display : none ;
}
. workbench - head ,
. workbench - actions ,
. step - navigation {
flex - direction : column ;
align - items : stretch ;
}
. workbench - actions ,
. outline - action ,
. btn - nav ,
. training - actions : deep ( . training - action ) {
width : 100 % ;
}
. key - point - grid {
grid - template - columns : 1 fr ;
}
}
< / style >
< style lang = "scss" >
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 非 scoped : 步骤导航覆盖 ( 教育蓝主题 )
* === === === === === === === === === === === === === === === === === === === === * /
# app . studentLayout section . app - main : has ( . product - standard - page ) . step - indicator {
display : flex ! important ;
flex - wrap : nowrap ! important ;
align - items : stretch ! important ;
justify - content : center ! important ;
gap : 0 ! important ;
width : 100 % ! important ;
margin : 0 0 16 px ! 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 : 240 px ! important ;
display : flex ! important ;
flex - direction : row ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 8 px ! important ;
min - width : 0 ! important ;
height : auto ! important ;
min - height : 40 px ! important ;
margin : 0 ! important ;
padding : 8 px 12 px ! important ;
border : 1 px solid # e5e7eb ! important ;
border - right : none ! important ;
border - radius : 0 ! important ;
background : # ffffff ! important ;
box - shadow : none ! important ;
color : # 6 b7280 ! important ;
font - size : 13 px ! important ;
font - weight : 600 ! important ;
line - height : 1.3 ! important ;
white - space : normal ! important ;
text - align : center ! important ;
cursor : pointer ;
transition : all 0.16 s ease ! important ;
& : first - child {
border - radius : 8 px 0 0 8 px ! important ;
}
& : last - child {
border - radius : 0 8 px 8 px 0 ! important ;
border - right : 1 px solid # e5e7eb ! important ;
}
. step - marker {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
flex : 0 0 auto ! important ;
min - width : 22 px ! important ;
height : 22 px ! important ;
padding : 0 5 px ! important ;
border : none ! important ;
border - radius : 6 px ! important ;
background : # f3f4f6 ! important ;
color : # 6 b7280 ! important ;
font - family : ui - monospace , "SF Mono" , Consolas , monospace ! important ;
font - size : 12 px ! important ;
font - weight : 700 ! important ;
line - height : 1 ! important ;
transition : all 0.16 s ease ! important ;
}
. step - copy {
display : flex ! important ;
flex - direction : column ! important ;
gap : 2 px ! important ;
min - width : 0 ! important ;
}
. step - action { display : none ! important ; }
& : hover : not ( . active ) : not ( . complete ) {
border - color : # 2563 eb ! important ;
background : # eff6ff ! important ;
color : # 2563 eb ! important ;
z - index : 1 ! important ;
. step - marker { background : # 2563 eb ! 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 : # 16915 f ! important ; }
& : hover {
border - color : # 2 ba471 ! important ;
background : # f0fdf6 ! important ;
z - index : 1 ! important ;
. step - marker { background : # 2 ba471 ! important ; color : # ffffff ! important ; }
}
}
& . active {
border - color : # 2563 eb ! important ;
background : # 2563 eb ! important ;
color : # ffffff ! important ;
font - weight : 700 ! important ;
z - index : 2 ! important ;
. step - marker { background : # ffffff ! important ; color : # 2563 eb ! important ; }
. step - action { color : rgba ( 255 , 255 , 255 , 0.65 ) ! important ; }
& : hover {
background : # 1 d4ed8 ! important ;
border - color : # 1 d4ed8 ! important ;
}
}
}
}
@ media ( max - width : 900 px ) {
# app . studentLayout section . app - main : has ( . product - standard - page ) . step - indicator {
justify - content : flex - start ! important ;
gap : 8 px ! important ;
overflow - x : auto ! important ;
flex - wrap : nowrap ! important ;
}
# app . studentLayout section . app - main : has ( . product - standard - page ) . step - indicator . step - tab {
flex : 0 0 140 px ! important ;
max - width : none ! important ;
border - radius : 8 px ! important ;
border : 1 px solid # e5e7eb ! important ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 共用组件换肤 : TrainingMaterialButton → 教育蓝
* === === === === === === === === === === === === === === === === === === === === * /
. product - standard - page . training - material - button . el - button {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 6 px ! important ;
min - height : 34 px ! important ;
height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ! important ;
border - radius : 8 px ! important ;
color : # 2563 eb ! important ;
background : # ffffff ! important ;
font - size : 13 px ! important ;
font - weight : 600 ! important ;
box - shadow : none ! important ;
transition :
border - color 0.16 s ease ,
background 0.16 s ease ,
color 0.16 s ease ,
box - shadow 0.16 s ease ! important ;
. el - icon {
font - size : 14 px ! important ;
color : inherit ! important ;
}
& : hover ,
& : focus {
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
}
}
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
* 顶部导出按钮 ( outline - action ) → 教育蓝描边
* === === === === === === === === === === === === === === === === === === === === * /
# app . studentLayout section . app - main : has ( . product - standard - page ) . outline - action {
display : inline - flex ! important ;
align - items : center ! important ;
justify - content : center ! important ;
gap : 6 px ! important ;
min - height : 34 px ! important ;
height : 34 px ! important ;
padding : 0 14 px ! important ;
border : 1 px solid rgba ( 37 , 99 , 235 , 0.35 ) ! important ;
border - radius : 8 px ! important ;
color : # 2563 eb ! important ;
background : # ffffff ! important ;
font - size : 13 px ! important ;
font - weight : 600 ! important ;
box - shadow : none ! important ;
transition :
border - color 0.16 s ease ,
background 0.16 s ease ,
color 0.16 s ease ,
box - shadow 0.16 s ease ! important ;
. el - icon {
font - size : 14 px ! important ;
color : inherit ! important ;
}
& : hover {
border - color : # 2563 eb ! important ;
color : # 2563 eb ! important ;
background : # eff6ff ! important ;
box - shadow : 0 2 px 6 px rgba ( 37 , 99 , 235 , 0.12 ) ! important ;
transform : translateY ( - 1 px ) ! important ;
}
}
< / style >