refactor: 重构实训页面底部导航,统一步骤按钮样式并移除重复代码

将各实训页面分散的步骤操作按钮整合为统一的工具栏式导航组件,移除重复的重置按钮和样式代码,新增上一步、保存进度、下一步按钮及步骤进度显示,优化响应式布局适配不同屏幕尺寸,提升界面一致性和可维护性。
fix/exam-question-access-material-excel-delete-draft
qinzhenpen 2 weeks ago
parent 3d83fefbce
commit 55ae2f3789

@ -22,7 +22,7 @@
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</section>
<section class="task-card">
@ -306,11 +306,6 @@
</div>
</div>
<div class="training-actions">
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>

@ -22,7 +22,7 @@
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</section>
<section class="consumer-scenario-workbench">
@ -254,11 +254,6 @@
</div>
</div>
<div class="training-actions">
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>

@ -22,7 +22,7 @@
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining" :on-export="exportOutcome" />
</section>
<section class="task-card">
@ -248,11 +248,6 @@
</div>
</div>
<div class="training-actions">
<el-button class="training-action training-action--reset" @click="resetTraining">
清空填写
</el-button>
</div>
</section>
</main>

@ -346,11 +346,6 @@
</div>
</div>
<div class="training-actions">
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
</div>
</section>
</section>
</main>

@ -22,7 +22,8 @@
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTraining"
:on-export="exportOutcome" />
</section>
<section class="task-card">
@ -306,7 +307,9 @@
</section>
<div v-if="hasConfiguredSteps" class="step-navigation">
<button type="button" class="btn-nav" :disabled="form.activeStep === 1" @click="goPreviousStep">
<div class="step-nav-left">
<button v-if="form.activeStep > 1" type="button" class="btn-nav btn-ghost"
@click="goPreviousStep">
<el-icon>
<ArrowLeft />
</el-icon>
@ -318,17 +321,18 @@
</el-icon>
保存当前进度
</button>
<button type="button" class="btn-nav btn-primary"
:disabled="form.activeStep === configuredSteps.length" @click="goNextStep">
</div>
<div class="step-nav-right">
<span class="step-progress">Step {{ String(form.activeStep).padStart(2, "0") }} / {{
String(configuredSteps.length).padStart(2, "0") }}</span>
<button v-if="form.activeStep < configuredSteps.length" type="button"
class="btn-nav btn-primary" :disabled="saving" @click="goNextStep">
下一步
<el-icon>
<ArrowRight />
</el-icon>
</button>
</div>
<div class="training-actions">
<el-button class="training-action training-action--reset" @click="resetTraining">清空填写</el-button>
</div>
</section>
</main>
@ -1532,15 +1536,36 @@ async function handleValueCanvasUpload(event) {
color: var(--student-success);
}
/* ===== 底部步骤导航按钮 ===== */
/* ============================================================
* 底部导航:工具栏式
* ============================================================ */
.step-navigation {
position: relative;
display: flex;
justify-content: center;
gap: 14px;
margin-top: 20px;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 16px;
padding: 12px 16px;
border: 1px solid var(--student-panel-border);
border-radius: 10px;
background: #ffffff;
}
.step-navigation .btn-nav {
.step-nav-left,
.step-nav-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.step-nav-right {
margin-left: auto;
}
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
@ -1552,61 +1577,125 @@ async function handleValueCanvasUpload(event) {
border-radius: 8px;
color: var(--student-accent);
background: #ffffff;
font-family: inherit;
font-size: var(--student-fs-button);
font-weight: 600;
cursor: pointer;
text-shadow: none;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease, transform 0.16s ease, box-shadow 0.16s ease;
}
transition: all 0.16s ease;
white-space: nowrap;
.step-navigation .btn-nav:hover:not(:disabled) {
.el-icon {
font-size: 18px;
}
&:hover:not(:disabled) {
border-color: var(--student-accent);
background: var(--student-accent-soft);
color: var(--student-accent);
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12);
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
}
}
.step-navigation .btn-nav.btn-save {
border-color: #e5e7eb;
color: #374151;
background: #f3f4f6;
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid var(--student-accent);
border-radius: 8px;
color: #ffffff;
background: var(--student-accent);
font-family: inherit;
font-size: var(--student-fs-button);
font-weight: 600;
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
cursor: pointer;
transition: all 0.16s ease;
white-space: nowrap;
.el-icon {
font-size: 18px;
}
&:hover:not(:disabled) {
border-color: var(--student-accent-deep);
background: var(--student-accent-deep);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
}
&:active:not(:disabled) {
transform: translateY(0);
}
}
.step-navigation .btn-nav.btn-save:hover:not(:disabled) {
border-color: var(--student-accent);
background: #e9ecef;
.btn-ghost {
border-color: rgba(37, 99, 235, 0.35);
color: var(--student-accent);
}
background: #ffffff;
.step-navigation .btn-nav.btn-primary {
&:hover:not(:disabled) {
border-color: var(--student-accent);
color: #ffffff;
background: var(--student-accent);
font-weight: 700;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28);
background: var(--student-accent-soft);
}
}
.step-navigation .btn-nav.btn-primary:hover:not(:disabled) {
background: var(--student-accent-deep);
border-color: var(--student-accent-deep);
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32);
.btn-save {
border-color: #e5e7eb;
color: #4b5563;
background: #f9fafb;
&:hover:not(:disabled) {
border-color: #d1d5db;
background: #f3f4f6;
transform: none;
box-shadow: none;
}
}
.step-navigation .btn-nav:disabled {
.btn-primary:disabled,
.btn-ghost:disabled,
.btn-save:disabled,
.btn-nav:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
box-shadow: none;
}
/* training-actions:容器保留,el-button 由全局接管 */
.training-actions {
display: flex;
flex-wrap: wrap;
.step-progress {
color: var(--student-text-secondary);
font-family: var(--student-font-data);
font-size: var(--student-fs-caption);
font-weight: 700;
letter-spacing: 0.05em;
white-space: nowrap;
}
@media (max-width: 1100px) {
.step-nav-right {
margin-left: 0;
}
}
@media (max-width: 900px) {
.step-navigation {
flex-direction: column;
align-items: stretch;
}
.step-nav-left,
.step-nav-right {
justify-content: center;
gap: 14px;
margin-top: 20px;
}
.btn-nav,
.btn-primary {
width: 100%;
}
}
/* ===== 响应式 ===== */
@ -1627,15 +1716,13 @@ async function handleValueCanvasUpload(event) {
}
.workbench-head,
.workbench-actions,
.step-navigation {
.workbench-actions {
flex-direction: column;
align-items: stretch;
}
.workbench-actions,
.outline-action,
.btn-nav {
.outline-action {
width: 100%;
}
@ -1873,58 +1960,4 @@ async function handleValueCanvasUpload(event) {
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12) !important;
transform: none !important;
}
/* ============================================================
* 底部提交/清空按钮:training-actions
* ============================================================ */
#app .studentLayout section.app-main:has(.product-positioning-page) .training-actions .el-button.training-action {
min-width: 140px !important;
min-height: 34px !important;
height: 34px !important;
padding: 0 14px !important;
border: 1px solid rgba(37, 99, 235, 0.35) !important;
border-radius: 8px !important;
color: #2563eb !important;
background: #ffffff !important;
font-size: var(--student-fs-body) !important;
font-weight: 700 !important;
line-height: 1 !important;
letter-spacing: 0 !important;
text-shadow: none !important;
box-shadow: none !important;
}
#app .studentLayout section.app-main:has(.product-positioning-page) .training-actions .el-button.training-action:hover:not(.is-disabled) {
transform: translateY(-1px) !important;
border-color: #2563eb !important;
background: #eff6ff !important;
color: #2563eb !important;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.12) !important;
}
#app .studentLayout section.app-main:has(.product-positioning-page) .training-actions .el-button.training-action--submit {
border-color: #2563eb !important;
color: #ffffff !important;
background: #2563eb !important;
box-shadow: 0 3px 10px rgba(37, 99, 235, 0.28) !important;
}
#app .studentLayout section.app-main:has(.product-positioning-page) .training-actions .el-button.training-action--submit:hover:not(.is-disabled) {
border-color: #1d4ed8 !important;
background: #1d4ed8 !important;
color: #ffffff !important;
box-shadow: 0 5px 14px rgba(29, 78, 216, 0.32) !important;
}
#app .studentLayout section.app-main:has(.product-positioning-page) .training-actions .el-button.training-action--reset {
border-color: #e5e7eb !important;
color: #374151 !important;
background: #f3f4f6 !important;
}
#app .studentLayout section.app-main:has(.product-positioning-page) .training-actions .el-button.training-action--reset:hover:not(.is-disabled) {
border-color: #2563eb !important;
color: #2563eb !important;
background: #eff6ff !important;
}
</style>

@ -69,11 +69,6 @@
</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">
@ -125,12 +120,6 @@
</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">
@ -175,12 +164,6 @@
@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">
@ -222,9 +205,6 @@
class="btn primary" type="button"
@click="downloadVisualizationTemplate">下载自动化模板</button></div>
</div>
<div class="step-actions"><button class="btn primary" type="button"
:disabled="saving || uploading || !stepThreePassed" @click="saveAndGoToStepFive">保存并进入第
5 步</button></div>
</template>
<template v-else-if="currentStep === 5">
<div class="step-heading">
@ -272,12 +252,6 @@
<p>提示词:请计算 Excel 表中产品评论数据的好评率,提炼用户痛点和未被满足的需求。</p>
</div>
</div>
<div class="step-actions"><button class="btn primary" type="button"
:disabled="saving || !stepFourPassed" @click="saveAndGoToStepSix">保存并进入第 6
步</button><button class="btn" type="button" :disabled="saving || !stepFourPassed"
@click="resetStepFive"><el-icon>
<RefreshLeft />
</el-icon> 清空填写</button></div>
</template>
<template v-else-if="currentStep === 6">
<div class="step-heading">
@ -293,15 +267,41 @@
id="market-opportunity-conclusion" v-model.trim="developmentConclusion" type="textarea"
:autosize="{ minRows: 4 }" :disabled="!stepFivePassed"
placeholder="请结合综合得分、评论痛点与未被满足的需求,说明建议开发的产品及具体原因。" /></div>
<div class="step-actions"><button class="btn" type="button"
:disabled="saving || !stepFivePassed" @click="resetStepSix"><el-icon>
<RefreshLeft />
</el-icon> 清空填写</button></div>
</template>
<div v-else class="placeholder">
<p>第 {{ currentStep }} 步内容待配置</p><span>已保存的步骤可从上方步骤栏返回查看。</span>
</div>
</section>
<div class="step-navigation">
<div class="step-nav-left">
<button v-if="currentStep > 1" type="button" class="btn-nav btn-ghost" :disabled="saving"
@click="prevStep">
<el-icon>
<ArrowLeft />
</el-icon>
上一步
</button>
<button type="button" class="btn-nav btn-save" :disabled="saving"
@click="saveCurrentProgress">
<el-icon>
<CircleCheck />
</el-icon>
保存当前进度
</button>
</div>
<div class="step-nav-right">
<span class="step-progress">Step {{ String(currentStep).padStart(2, "0") }} / {{
String(trainingSteps.length).padStart(2, "0") }}</span>
<button v-if="currentStep < trainingSteps.length" type="button" class="btn-nav btn-primary"
:disabled="saving" @click="nextStep">
下一步
<el-icon>
<ArrowRight />
</el-icon>
</button>
</div>
</div>
</section>
</main>
<TrainingAiSidebar study-tip="先辨别指标描述的是规模、增长潜力、供给竞争,还是流量获取难度,再进行归类。" reference-title="指标分类提示"
@ -312,7 +312,7 @@
<script setup>
import * as XLSX from "xlsx"
import { Download, RefreshLeft, UploadFilled } from "@element-plus/icons-vue"
import { ArrowLeft, ArrowRight, CircleCheck, Download, RefreshLeft, UploadFilled } from "@element-plus/icons-vue"
import { checkMarketOpportunitySelectionStepOne, checkMarketOpportunitySelectionStepTwo, checkMarketOpportunitySelectionStepThree, checkMarketOpportunitySelectionStepFour, checkMarketOpportunitySelectionStepFive, checkMarketOpportunitySelectionStepSix, deleteStudentTrainingAnswer, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
@ -358,6 +358,39 @@ function buildStepFourOutcome() { return { title: taskTitle.value, visualization
function buildStepFiveOutcome() { return { title: taskTitle.value, reviewInsights: Object.fromEntries(products.map((product) => [product.id, { ...reviewInsights.value[product.id] }])) } }
function buildStepSixOutcome() { return { title: taskTitle.value, developmentConclusion: developmentConclusion.value.trim() } }
function buildDynamicStepAnswers(overrides) { return { ...dynamicStepAnswers.value, ...overrides } }
/* ===== 底部步骤导航 ===== */
function prevStep() { if (currentStep.value > 1) switchStep(currentStep.value - 1) }
async function saveCurrentProgress() {
if (saving.value) return
saving.value = true
try {
const step = currentStep.value
const payload = { currentStep: step, submitted: false, saveAction: "SAVE" }
if (step === 1) payload.step1Answer = JSON.stringify(buildStepOneOutcome())
if (step === 2) payload.step2Answer = JSON.stringify(buildStepTwoOutcome())
if (step === 3) payload.step3Answer = JSON.stringify(buildStepThreeOutcome())
if (step === 4) payload.step4Answer = JSON.stringify(buildStepFourOutcome())
if (step === 5) { dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome() }); payload.dynamicStepAnswers = JSON.stringify(dynamicStepAnswers.value) }
if (step === 6) { dynamicStepAnswers.value = buildDynamicStepAnswers({ step5: buildStepFiveOutcome(), step6: buildStepSixOutcome() }); payload.dynamicStepAnswers = JSON.stringify(dynamicStepAnswers.value) }
await saveStudentTrainingAnswer(TASK_KEY, payload)
proxy?.$modal?.msgSuccess("进度已保存")
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "保存失败,请重试")
} finally {
saving.value = false
}
}
function nextStep() {
const step = currentStep.value
if (step === 1) checkAndContinue()
else if (step === 2) saveAndGoToStepThree()
else if (step === 3) saveAndGoToStepFour()
else if (step === 4) saveAndGoToStepFive()
else if (step === 5) saveAndGoToStepSix()
else if (step === 6) saveAndCompleteTraining()
}
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] }))); stepOneCheckResults.value = Object.fromEntries((result?.data?.items || []).map((item) => [item.id, item])); 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 isIntegerWeight(value) { return value !== null && value !== "" && Number.isInteger(Number(value)) && Number(value) >= 1 && Number(value) <= 100 }
function dimensionWeightTotal(group) { return calculateDimensionWeightTotal(group, weights.value) }
@ -986,14 +1019,6 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: tas
outline-offset: -2px;
}
/* ===== 底部按钮 ===== */
.step-actions {
display: flex;
justify-content: center;
gap: 10px;
margin-top: 18px;
}
.btn {
display: inline-flex;
align-items: center;
@ -1043,6 +1068,168 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: tas
box-shadow: none;
}
/* ============================================================
* 底部导航:工具栏式
* ============================================================ */
.step-navigation {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 16px;
padding: 12px 16px;
border: 1px solid var(--student-panel-border);
border-radius: 10px;
background: #ffffff;
}
.step-nav-left,
.step-nav-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.step-nav-right {
margin-left: auto;
}
.btn-nav {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid rgba(37, 99, 235, 0.35);
border-radius: 8px;
color: var(--student-accent);
background: #ffffff;
font-family: inherit;
font-size: var(--student-fs-button);
font-weight: 600;
cursor: pointer;
transition: all 0.16s ease;
white-space: nowrap;
.el-icon {
font-size: 18px;
}
&:hover:not(:disabled) {
border-color: var(--student-accent);
background: var(--student-accent-soft);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.12);
}
}
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid var(--student-accent);
border-radius: 8px;
color: #ffffff;
background: var(--student-accent);
font-family: inherit;
font-size: var(--student-fs-button);
font-weight: 600;
box-shadow: 0 1px 2px rgba(37, 99, 235, 0.2), 0 2px 8px rgba(37, 99, 235, 0.15);
cursor: pointer;
transition: all 0.16s ease;
white-space: nowrap;
.el-icon {
font-size: 18px;
}
&:hover:not(:disabled) {
border-color: var(--student-accent-deep);
background: var(--student-accent-deep);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.3), 0 4px 12px rgba(29, 78, 216, 0.22);
}
&:active:not(:disabled) {
transform: translateY(0);
}
}
.btn-ghost {
border-color: rgba(37, 99, 235, 0.35);
color: var(--student-accent);
background: #ffffff;
&:hover:not(:disabled) {
border-color: var(--student-accent);
background: var(--student-accent-soft);
}
}
.btn-save {
border-color: #e5e7eb;
color: #4b5563;
background: #f9fafb;
&:hover:not(:disabled) {
border-color: #d1d5db;
background: #f3f4f6;
transform: none;
box-shadow: none;
}
}
.btn-primary:disabled,
.btn-ghost:disabled,
.btn-save:disabled,
.btn-nav:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
box-shadow: none;
}
.step-progress {
color: var(--student-text-secondary);
font-family: var(--student-font-data);
font-size: var(--student-fs-caption);
font-weight: 700;
letter-spacing: 0.05em;
white-space: nowrap;
}
@media (max-width: 1100px) {
.step-nav-right {
margin-left: 0;
}
}
@media (max-width: 900px) {
.step-navigation {
flex-direction: column;
align-items: stretch;
}
.step-nav-left,
.step-nav-right {
justify-content: center;
}
.btn-nav,
.btn-primary {
width: 100%;
}
}
.placeholder {
padding: 60px 20px;
text-align: center;

Loading…
Cancel
Save