refactor: 统一将工具栏替换为共用的 TrainingTaskToolbar 组件

将多个页面的独立工具栏(包含资料按钮和导出按钮)替换为共用的 TrainingTaskToolbar 组件,新增统一的 task-action-strip 样式类,简化代码结构,提升组件复用性。
fix/exam-question-access-material-excel-delete-draft
qinzhenpen 2 weeks ago
parent 0053e509dc
commit b37cb241ee

@ -1,7 +1,7 @@
#app {
.main-container {
height: calc(100% - 50px);
transition: margin-left .28s;
transition: margin-left 0.28s;
margin-left: $base-sidebar-width;
position: relative;
}
@ -9,10 +9,10 @@
margin-left: 0 !important;
}
.sidebar-container {
-webkit-transition: width .28s;
-webkit-transition: width 0.28s;
transition: width 0.28s;
width: 239px !important;
background: url('../images/sidebar-background.webp') no-repeat center;
background: url("../images/sidebar-background.webp") no-repeat center;
// background-color: $base-menu-background;
background-size: 100% 100%;
height: 100%;
@ -23,11 +23,14 @@
left: 0;
z-index: 1001;
overflow: hidden;
-webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
-webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
transition:
0s width ease-in-out,
0s padding-left ease-in-out,
0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
@ -72,7 +75,7 @@
.el-sub-menu__title {
height: 37px;
line-height: 37px;
background: url('../images/sidebar-normal.png') no-repeat;
background: url("../images/sidebar-normal.png") no-repeat;
background-size: 100% 100% !important;
}
.el-menu {
@ -90,7 +93,7 @@
}
}
}
.is-opened{
.is-opened {
margin-bottom: 0px !important;
}
.el-menu-item {
@ -100,25 +103,24 @@
height: 37px;
line-height: 37px;
color: #fff;
background: url('../images/sidebar-normal.png') no-repeat;
background: url("../images/sidebar-normal.png") no-repeat;
background-size: 100% 100% !important;
}
.is-active {
background: url('../images/sidebar-active.png') no-repeat !important;
.el-menu{
.nest-menu{
.is-active{
background: url("../images/sidebar-active.png") no-repeat !important;
.el-menu {
.nest-menu {
.is-active {
color: #008efe !important;
}
}
}
.el-sub-menu__title {
background: url('../images/sidebar-active.png') no-repeat !important;
background: url("../images/sidebar-active.png") no-repeat !important;
background-size: 100% 100% !important;
}
}
}
.el-menu-item,
@ -140,11 +142,11 @@
}
}
& .theme-dark .is-active>.el-sub-menu__title {
& .theme-dark .is-active > .el-sub-menu__title {
color: $base-menu-color-active !important;
}
& .nest-menu .el-sub-menu>.el-sub-menu__title,
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: $base-sidebar-width !important;
@ -153,7 +155,7 @@
}
}
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background !important;
@ -187,20 +189,19 @@
.el-sub-menu {
overflow: hidden;
&>.el-sub-menu__title {
& > .el-sub-menu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
}
}
.el-menu--collapse {
.el-sub-menu {
&>.el-sub-menu__title {
&>span {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
@ -208,7 +209,7 @@
display: inline-block;
}
&>i {
& > i {
height: 0;
width: 0;
overflow: hidden;
@ -229,7 +230,7 @@
}
.sidebar-container {
transition: transform .28s;
transition: transform 0.28s;
width: $base-sidebar-width !important;
}
@ -251,10 +252,7 @@
.platform-admin-main {
height: calc(100vh - 50px);
overflow: auto !important;
background:
radial-gradient(circle at 12% 18%, rgba(14, 74, 112, 0.45), transparent 34%),
radial-gradient(circle at 78% 6%, rgba(25, 92, 170, 0.3), transparent 28%),
linear-gradient(135deg, #03070e 0%, #061827 48%, #02060d 100%) !important;
background: radial-gradient(circle at 12% 18%, rgba(14, 74, 112, 0.45), transparent 34%), radial-gradient(circle at 78% 6%, rgba(25, 92, 170, 0.3), transparent 28%), linear-gradient(135deg, #03070e 0%, #061827 48%, #02060d 100%) !important;
&::-webkit-scrollbar {
width: 5px;
@ -274,17 +272,11 @@
.studentLayout,
.teacherLayout {
background:
radial-gradient(circle at 10% 20%, rgba(11, 42, 74, 0.74), transparent 34%),
radial-gradient(circle at 88% 8%, rgba(0, 136, 210, 0.24), transparent 28%),
linear-gradient(135deg, #02050c 0%, #07101d 52%, #02060d 100%);
background: radial-gradient(circle at 10% 20%, rgba(11, 42, 74, 0.74), transparent 34%), radial-gradient(circle at 88% 8%, rgba(0, 136, 210, 0.24), transparent 28%), linear-gradient(135deg, #02050c 0%, #07101d 52%, #02060d 100%);
.main-container {
margin-left: 304px;
background:
radial-gradient(circle at 10% 20%, rgba(11, 42, 74, 0.72), transparent 34%),
radial-gradient(circle at 88% 8%, rgba(0, 136, 210, 0.22), transparent 28%),
linear-gradient(135deg, #02050c 0%, #07101d 52%, #02060d 100%);
background: radial-gradient(circle at 10% 20%, rgba(11, 42, 74, 0.72), transparent 34%), radial-gradient(circle at 88% 8%, rgba(0, 136, 210, 0.22), transparent 28%), linear-gradient(135deg, #02050c 0%, #07101d 52%, #02060d 100%);
}
.app-main,
@ -292,10 +284,7 @@
height: calc(100vh - 50px);
padding: 28px 32px 40px 24px;
overflow: auto;
background:
radial-gradient(circle at 12% 18%, rgba(14, 74, 112, 0.45), transparent 34%),
radial-gradient(circle at 78% 6%, rgba(25, 92, 170, 0.3), transparent 28%),
linear-gradient(135deg, #03070e 0%, #061827 48%, #02060d 100%) !important;
background: radial-gradient(circle at 12% 18%, rgba(14, 74, 112, 0.45), transparent 34%), radial-gradient(circle at 78% 6%, rgba(25, 92, 170, 0.3), transparent 28%), linear-gradient(135deg, #03070e 0%, #061827 48%, #02060d 100%) !important;
&::-webkit-scrollbar {
width: 5px;
@ -350,7 +339,9 @@
border: 1px solid rgba(0, 180, 255, 0.25);
border-radius: 32px;
background: rgba(8, 18, 28, 0.7);
box-shadow: 0 22px 48px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(169, 229, 255, 0.08);
box-shadow:
0 22px 48px rgba(0, 0, 0, 0.22),
inset 0 1px 0 rgba(169, 229, 255, 0.08);
backdrop-filter: blur(12px);
}
@ -408,7 +399,12 @@
white-space: normal;
overflow: hidden;
cursor: pointer;
transition: color 0.2s ease, transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
transition:
color 0.2s ease,
transform 0.2s ease,
border-color 0.2s ease,
background 0.2s ease,
box-shadow 0.2s ease;
}
.student-course-icon {
@ -423,10 +419,10 @@
border: 1px solid rgba(0, 170, 255, 0.26);
border-radius: 10px;
color: #7ee9ff;
background:
linear-gradient(135deg, rgba(0, 170, 255, 0.24), rgba(43, 110, 255, 0.08)),
rgba(1, 24, 36, 0.82);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 0 10px rgba(0, 170, 255, 0.08);
background: linear-gradient(135deg, rgba(0, 170, 255, 0.24), rgba(43, 110, 255, 0.08)), rgba(1, 24, 36, 0.82);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 0 10px rgba(0, 170, 255, 0.08);
svg {
width: 15px;
@ -487,7 +483,9 @@
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: background 0.2s ease, box-shadow 0.2s ease;
transition:
background 0.2s ease,
box-shadow 0.2s ease;
&:hover,
&.active {
@ -557,10 +555,10 @@
box-sizing: border-box;
border: 0;
border-radius: 0;
background:
radial-gradient(circle at 18% 10%, rgba(0, 170, 255, 0.12), transparent 34%),
rgba(8, 18, 28, 0.82);
box-shadow: inset -1px 0 0 rgba(169, 229, 255, 0.06), 12px 0 32px rgba(0, 5, 16, 0.2);
background: radial-gradient(circle at 18% 10%, rgba(0, 170, 255, 0.12), transparent 34%), rgba(8, 18, 28, 0.82);
box-shadow:
inset -1px 0 0 rgba(169, 229, 255, 0.06),
12px 0 32px rgba(0, 5, 16, 0.2);
}
.student-course-list {
@ -589,7 +587,10 @@
font-size: 24px;
line-height: 1;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
transition:
background 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
&:hover {
border-color: #70e4ff;
@ -637,7 +638,6 @@
margin-left: 64px;
}
}
}
/* 仅学生实训任务页提供课程导航的图标轨道,离开任务页不改变原有学生布局。 */
@ -666,7 +666,10 @@
font-size: 24px;
line-height: 1;
cursor: pointer;
transition: background .2s ease, border-color .2s ease, transform .2s ease;
transition:
background 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
&:hover,
&:focus-visible {
@ -690,9 +693,8 @@
padding: 58px 11px 24px;
border: 0;
border-radius: 0;
background:
linear-gradient(180deg, rgba(8, 31, 47, .92), rgba(3, 16, 28, .98));
box-shadow: inset -1px 0 0 rgba(132, 228, 255, .1);
background: linear-gradient(180deg, rgba(8, 31, 47, 0.92), rgba(3, 16, 28, 0.98));
box-shadow: inset -1px 0 0 rgba(132, 228, 255, 0.1);
}
.student-course-heading,
@ -718,7 +720,7 @@
min-height: 42px;
margin: 0 0 8px;
border-radius: 13px;
background: rgba(255, 255, 255, .025);
background: rgba(255, 255, 255, 0.025);
}
.student-course-list {
@ -735,9 +737,9 @@
}
.student-course-link.active {
border: 1px solid rgba(0, 190, 255, .85);
background: linear-gradient(135deg, rgba(0, 174, 255, .28), rgba(18, 95, 190, .14));
box-shadow: 0 0 14px rgba(0, 178, 255, .25);
border: 1px solid rgba(0, 190, 255, 0.85);
background: linear-gradient(135deg, rgba(0, 174, 255, 0.28), rgba(18, 95, 190, 0.14));
box-shadow: 0 0 14px rgba(0, 178, 255, 0.25);
transform: none;
}
@ -756,13 +758,13 @@
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
& > .el-menu {
.svg-icon {
margin-right: 16px;
}
}
.nest-menu .el-sub-menu>.el-sub-menu__title,
.nest-menu .el-sub-menu > .el-sub-menu__title,
.el-menu-item {
&:hover {
// you can use $sub-menuHover
@ -771,7 +773,7 @@
}
// the scroll bar appears when the sub-menu is too long
>.el-menu--popup {
> .el-menu--popup {
max-height: 100vh;
overflow-y: auto;
@ -798,24 +800,15 @@
#app .studentLayout,
#app .teacherLayout {
// 整体浅色背景 + 蓝色点缀,营造明亮的科技感
background:
radial-gradient(circle at 10% 20%, rgba(30, 136, 229, 0.07), transparent 34%),
radial-gradient(circle at 88% 8%, rgba(66, 165, 245, 0.05), transparent 28%),
linear-gradient(135deg, #f5f8fc 0%, #eaf2fb 52%, #f3f8ff 100%) !important;
background: radial-gradient(circle at 10% 20%, rgba(30, 136, 229, 0.07), transparent 34%), radial-gradient(circle at 88% 8%, rgba(66, 165, 245, 0.05), transparent 28%), linear-gradient(135deg, #f5f8fc 0%, #eaf2fb 52%, #f3f8ff 100%) !important;
.main-container {
background:
radial-gradient(circle at 10% 20%, rgba(30, 136, 229, 0.06), transparent 34%),
radial-gradient(circle at 88% 8%, rgba(66, 165, 245, 0.04), transparent 28%),
linear-gradient(135deg, #f5f8fc 0%, #eaf2fb 52%, #f3f8ff 100%) !important;
background: radial-gradient(circle at 10% 20%, rgba(30, 136, 229, 0.06), transparent 34%), radial-gradient(circle at 88% 8%, rgba(66, 165, 245, 0.04), transparent 28%), linear-gradient(135deg, #f5f8fc 0%, #eaf2fb 52%, #f3f8ff 100%) !important;
}
.app-main,
.teacher-main {
background:
radial-gradient(circle at 12% 18%, rgba(30, 136, 229, 0.05), transparent 34%),
radial-gradient(circle at 78% 6%, rgba(66, 165, 245, 0.04), transparent 28%),
linear-gradient(135deg, #f7fafd 0%, #eef4fb 48%, #f5f8fc 100%) !important;
background: radial-gradient(circle at 12% 18%, rgba(30, 136, 229, 0.05), transparent 34%), radial-gradient(circle at 78% 6%, rgba(66, 165, 245, 0.04), transparent 28%), linear-gradient(135deg, #f7fafd 0%, #eef4fb 48%, #f5f8fc 100%) !important;
&::-webkit-scrollbar {
width: 6px;
@ -878,7 +871,9 @@
margin: 0 0 2px !important;
min-height: 40px !important;
padding: 0 12px !important;
transition: background-color 0.15s ease, color 0.15s ease;
transition:
background-color 0.15s ease,
color 0.15s ease;
&:hover {
color: #1f2937 !important;
@ -954,10 +949,16 @@
color: #9ca3af !important;
box-shadow: -2px 0 6px rgba(17, 24, 39, 0.08) !important;
cursor: pointer;
transition: background-color 0.15s ease, color 0.15s ease;
transition:
background-color 0.15s ease,
color 0.15s ease;
.toggle-arrow { font-size: 10px; }
.toggle-arrow--right { display: none; }
.toggle-arrow {
font-size: 10px;
}
.toggle-arrow--right {
display: none;
}
&:hover,
&:focus-visible {
@ -970,8 +971,31 @@
// 折叠态:左箭头隐藏、右箭头显示(与 .sidebar-container 同级,避免嵌套出双容器)
.sidebar-container.is-collapse .course-sidebar-toggle {
.toggle-arrow--left { display: none; }
.toggle-arrow--right { display: inline-flex; }
.toggle-arrow--left {
display: none;
}
.toggle-arrow--right {
display: inline-flex;
}
}
}
/*
* 教师端 sidebar 宽度单独缩减 50%(304 → 152px)
* 教师端使用 el-menu 直显式布局,无需学生端的内嵌卡片空间,原 304px 造成大量空白
* 学生端保持 304px 不变(内部 .student-course-nav 卡片需要空间)
* 同时隐藏教师端唯一的分组标题"教学管理"(标识),仅保留菜单项
*/
#app .teacherLayout {
.main-container {
margin-left: 180px !important;
}
.sidebar-container {
width: 180px !important;
}
// 隐藏教师端顶部分组标题("教学管理"标识),不影响学生端多分组标题
.student-course-nav > .student-course-heading {
display: none !important;
}
}
@ -1001,7 +1025,9 @@
border-right: 1px solid #e5e7eb !important;
box-shadow: none !important;
&::after { display: none !important; }
&::after {
display: none !important;
}
/* 清除遗留背景图:sidebar-normal.png / sidebar-active.png / sidebar-background.webp
这些 !important 老规则在 L15/L75/L103/L107 会给 li/sub-menu-title/menu-item
@ -1045,13 +1071,19 @@
/* 侧边栏滚动条中性化 */
.el-scrollbar__wrap {
&::-webkit-scrollbar { width: 5px; }
&::-webkit-scrollbar-track { background: transparent; }
&::-webkit-scrollbar {
width: 5px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 10px;
}
&::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
&::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
}
.el-scrollbar__bar.is-vertical {
@ -1060,7 +1092,9 @@
transition: opacity 0.2s ease;
}
&:hover .el-scrollbar__bar.is-vertical { opacity: 1; }
&:hover .el-scrollbar__bar.is-vertical {
opacity: 1;
}
}
/* el-menu 菜单容器:白底、紧凑行高 */
@ -1091,9 +1125,14 @@
background: transparent !important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.15s ease, color 0.15s ease;
transition:
background-color 0.15s ease,
color 0.15s ease;
.student-menu-icon { color: #1f2937 !important; transition: color 0.15s ease; }
.student-menu-icon {
color: #1f2937 !important;
transition: color 0.15s ease;
}
&:hover,
&:focus {
@ -1101,7 +1140,9 @@
background: #f9fafb !important;
outline: none;
.student-menu-icon { color: #1f2937 !important; }
.student-menu-icon {
color: #1f2937 !important;
}
}
}
@ -1119,14 +1160,18 @@
background: #f3f4f6 !important;
box-shadow: none;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
&:hover,
&:focus {
color: #111827 !important;
background: #e5e7eb !important;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
}
}
@ -1138,14 +1183,18 @@
background: transparent !important;
box-shadow: none;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
&:hover,
&:focus {
color: #2563eb !important;
background: #f9fafb !important;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
}
}
@ -1157,14 +1206,18 @@
background: transparent !important;
box-shadow: none;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
&:hover,
&:focus {
color: #2563eb !important;
background: #f9fafb !important;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
}
}
@ -1187,7 +1240,9 @@
font-size: 13px;
font-weight: 400;
background: transparent;
transition: background-color 0.15s ease, color 0.15s ease;
transition:
background-color 0.15s ease,
color 0.15s ease;
.student-menu-icon {
color: #9ca3af !important;
@ -1201,7 +1256,9 @@
color: #1f2937;
outline: none;
.student-menu-icon { color: #6b7280 !important; }
.student-menu-icon {
color: #6b7280 !important;
}
}
&.is-active {
@ -1210,7 +1267,9 @@
font-weight: 600;
box-shadow: none;
.student-menu-icon { color: #2563eb !important; }
.student-menu-icon {
color: #2563eb !important;
}
&:hover {
background: #e5e7eb !important;
@ -1270,7 +1329,9 @@
padding: 0;
}
.student-menu-icon { margin-right: 0; }
.student-menu-icon {
margin-right: 0;
}
/* 恢复折叠模式下 sub-menu title 里的 icon 尺寸 ——
Element Plus 默认把 .hideSidebar 里 sub-menu title 的 <i> 也压成 0 宽高,
@ -1310,7 +1371,9 @@
font-size: 13px;
font-weight: 400;
background: transparent;
transition: background-color 0.15s ease, color 0.15s ease;
transition:
background-color 0.15s ease,
color 0.15s ease;
&:hover,
&:focus {
@ -1341,26 +1404,38 @@
#app .teacherLayout {
background: #ffffff !important;
.main-container { background: #ffffff !important; }
.main-container {
background: #ffffff !important;
}
.app-main,
.teacher-main {
background: #ffffff !important;
&::-webkit-scrollbar-track { background: transparent !important; }
&::-webkit-scrollbar-thumb { background: #d1d5db !important; }
&::-webkit-scrollbar-thumb:hover { background: #9ca3af !important; }
&::-webkit-scrollbar-track {
background: transparent !important;
}
&::-webkit-scrollbar-thumb {
background: #d1d5db !important;
}
&::-webkit-scrollbar-thumb:hover {
background: #9ca3af !important;
}
}
// 实训页背景一并收敛为纯白
.app-main:has(.student-training-shell) { background: #ffffff !important; }
.app-main:has(.student-training-shell) {
background: #ffffff !important;
}
.sidebar-container {
background: #ffffff !important;
border-right: 1px solid #e5e7eb !important;
box-shadow: none !important;
&::after { display: none !important; }
&::after {
display: none !important;
}
// 导航容器:去掉大圆角玻璃卡片,铺满侧栏
.student-course-nav {
@ -1385,10 +1460,14 @@
letter-spacing: 0;
text-transform: none;
&:hover { color: #1f2937 !important; }
&:hover {
color: #1f2937 !important;
}
}
.student-course-list { margin: 0 0 16px; }
.student-course-list {
margin: 0 0 16px;
}
// 菜单项:纯文字行、38px 高、8px 圆角
.student-course-link {
@ -1438,7 +1517,11 @@
background: #eff6ff !important;
}
.student-course-link:hover .student-course-icon { color: #4b5563 !important; }
.student-course-link.active .student-course-icon { color: #3b82f6 !important; }
.student-course-link:hover .student-course-icon {
color: #4b5563 !important;
}
.student-course-link.active .student-course-icon {
color: #3b82f6 !important;
}
}
}

@ -17,19 +17,9 @@
</span>
</p>
</div>
<div class="channel-toolbar">
<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 /></el-icon>导出实训成果</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-reset="resetTask" :on-export="exportOutcome" />
</section>
<section class="task-card">
<nav v-if="hasConfiguredSteps" class="step-indicator" aria-label="采购需求预测步骤"><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>
@ -148,7 +138,6 @@
<div id="forecastChart" ref="forecastChart" style="width: 80%; height: 400px"></div>
</section>
<div v-if="hasConfiguredSteps" class="step-navigation"><button type="button" class="btn-nav" :disabled="activeStep===1" @click="goPreviousStep"><el-icon><ArrowLeft /></el-icon>上一步</button><button type="button" class="btn-nav btn-save" :disabled="saving" @click="saveCurrentProgress"><el-icon><CircleCheck /></el-icon>保存当前进度</button><button type="button" class="btn-nav btn-primary" :disabled="activeStep===configuredSteps.length" @click="goNextStep">下一步<el-icon><ArrowRight /></el-icon></button></div>
<div class="taskfooter"><el-button @click="submitTask" :loading="saving">提交任务</el-button><el-button @click="resetTask" :loading="saving">清空填写</el-button></div>
<pop-model :showModel="runResultShow" title="设置代码" @closePop="closeCode">
<template v-slot:content>
<div>
@ -165,11 +154,11 @@
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import * as analysisAPI from "@/api/market-demand-mining.js";
import * as echarts from "echarts";
import useUserStore from "@/store/modules/user";
import { ArrowLeft, ArrowRight, CircleCheck, Download, TrendCharts } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck, TrendCharts } from "@element-plus/icons-vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile, validatePurchaseDemandForecastStepOne } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
@ -946,23 +935,17 @@ onMounted(async () => {
color: var(--student-text);
}
.taskfooter {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 28px;
margin-top: 32px;
padding-top: 28px;
border-top: 1px solid var(--student-panel-border);
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.taskfooter :deep(.el-button) {
min-width: 140px;
height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: var(--student-fs-h1);
font-weight: 800;
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
@ -976,13 +959,12 @@ onMounted(async () => {
.task-brief-text { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 1; }
.task-brief-toggle { padding: 0; border: 0; color: var(--student-accent-deep); background: transparent; font: inherit; font-weight: 700; cursor: pointer; &:hover { text-decoration: underline; } }
.task-brief-more { display: grid; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--student-panel-border); b { color: var(--student-text); font-size: 12px; } ul { display: grid; gap: 4px; margin: 0; padding-left: 18px; } p { margin: 0; } }
.channel-toolbar { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; flex-shrink: 0; }
.task-card,.step-card { border: 1px solid var(--student-panel-border); border-radius: 26px; background: var(--student-card-bg); }.task-card { margin-top: 28px; padding: 18px 22px 22px; }.workbench-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 18px; margin-bottom: 22px; padding-bottom: 18px; border-bottom: 1px solid var(--student-panel-border); }.section-eyebrow { margin: 0; color: var(--student-accent); font-size: var(--student-fs-caption); font-weight: 800; line-height: 1.4; }.workbench-head h3 { margin: 4px 0 8px; color: var(--student-text); font-size: var(--student-fs-h1); font-weight: 800; line-height: 1.3; }.workbench-description { margin: 0; color: var(--student-text-secondary); font-size: var(--student-fs-body); line-height: 1.8; }.workbench-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; gap: 10px; flex-shrink: 0; }
.outline-action,.btn-nav { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 36px; height: 36px; padding: 0 14px; border: 1px solid var(--student-panel-border); border-radius: 8px; color: var(--student-text); background: var(--student-accent-soft); font: inherit; font-size: var(--student-fs-button); font-weight: 800; cursor: pointer; }.step-indicator { display:flex; align-items:center; gap:12px; width:calc(100% + 24px); min-height:0; margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid var(--student-panel-border); border-radius:18px; background:var(--student-card-bg); box-shadow:inset 0 1px 0 rgba(31, 41, 55, 0.04); }.step-tab { display:flex; flex:1 0 145px; align-items:center; min-height:66px; padding:8px 10px; border:1px solid var(--student-panel-border); border-radius:8px; color:var(--student-text-secondary); background:var(--student-card-bg); font:inherit; font-size:var(--student-fs-body); font-weight:800; text-align:left; cursor:pointer; transition:transform .2s,color .2s,background .2s,border-color .2s,box-shadow .2s; }.step-tab:hover,.step-tab:focus-visible { transform:translateY(-2px); border-color:var(--student-accent); color:#ffffff; background:var(--student-accent-light); box-shadow:0 8px 18px rgba(0,0,0,.1); outline:none; }.step-tab.active { border-color:var(--student-accent); color:#ffffff; background:var(--student-accent); box-shadow:0 8px 20px rgba(31, 41, 55, 0.18); }.step-marker { display:inline-flex; flex:0 0 38px; align-items:center; justify-content:center; width:38px; height:38px; border:1px solid var(--student-panel-border); border-radius:50%; color:var(--student-text-secondary); background:var(--student-card-bg); font-size:var(--student-fs-body-sm); font-weight:900; }.step-copy { display:grid; min-width:0; gap:4px; margin-left:10px; }.step-copy strong { display:-webkit-box; overflow:hidden; color:inherit; font-size:var(--student-fs-body); line-height:1.25; -webkit-box-orient:vertical; -webkit-line-clamp:2; }.step-copy span { color:var(--student-text-secondary); font-size:var(--student-fs-caption); font-weight:700; }.step-tab.active .step-marker { border-color:var(--student-accent); color:#ffffff; background:var(--student-accent); box-shadow:0 0 0 4px rgba(31, 41, 55, 0.1); }.step-tab.complete .step-marker { border-color:var(--student-success); color:#ffffff; background:var(--student-success); }.step-connector { flex:0 0 28px; height:2px; background:var(--student-panel-border); color:var(--student-text-secondary); font-size:var(--student-fs-h2); line-height:0; text-align:right; }.step-connector.complete { background:var(--student-success); color:var(--student-success); }.step-card { margin-top: 32px; padding: 18px 22px 22px; }.step-indicator + .step-card,.workbench-head + .step-card { margin-top: 0; }.step-title-row { margin-bottom: 14px; }.step-title-row h3 { margin: 4px 0 0; color: var(--student-text); font-size: var(--student-fs-h2); font-weight: 900; line-height: 1.3; }.caseBacktitle { display: flex; align-items: center; gap: 8px; margin: 20px 0 10px; color: var(--student-text-secondary); font-size: var(--student-fs-h2); font-weight: 800; }.caseBacktitle img { display: none; }.step-card > p { color: var(--student-text-secondary); font-size: var(--student-fs-body); line-height: 1.8; }
:deep(.el-table) { width: 100% !important; max-width: 100%; border-radius: 18px; }:deep(.el-table th.el-table__cell) { background: #fafbfc !important; }:deep(.el-input__wrapper) { min-height: 42px; border-radius: 14px; background: #ffffff; }.online { border-radius: 14px; background: var(--student-panel-bg); }.step-navigation { display: flex; justify-content: center; gap: 14px; margin-top: 22px; }.btn-nav.btn-save,.btn-nav.btn-primary { border-color: var(--student-accent); color: #ffffff; background: var(--student-accent); min-height: 36px; height: 36px; padding: 0 14px; border-radius: 8px; }.btn-nav:disabled { cursor: not-allowed; opacity: .45; }.taskfooter { gap: 52px; margin-top: 36px; padding-top: 0; border-top: 0; }.taskfooter :deep(.el-button) { min-width: 112px; height: 34px; padding: 0 16px; border: 1px solid rgba(37, 99, 235, 0.35) !important; border-radius: 8px; color: #2563eb !important; background: #ffffff !important; font-size: 13px; font-weight: 600; }
:deep(.el-table) { width: 100% !important; max-width: 100%; border-radius: 18px; }:deep(.el-table th.el-table__cell) { background: #fafbfc !important; }:deep(.el-input__wrapper) { min-height: 42px; border-radius: 14px; background: #ffffff; }.online { border-radius: 14px; background: var(--student-panel-bg); }.step-navigation { display: flex; justify-content: center; gap: 14px; margin-top: 22px; }.btn-nav.btn-save,.btn-nav.btn-primary { border-color: var(--student-accent); color: #ffffff; background: var(--student-accent); min-height: 36px; height: 36px; padding: 0 14px; border-radius: 8px; }.btn-nav:disabled { cursor: not-allowed; opacity: .45; }
}
@media (max-width: 1100px) { .channel { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .channel { padding: 14px; .training-core { padding: 18px; border-radius: 24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction: column; align-items: stretch; }.workbench-actions,.outline-action,.btn-nav,.taskfooter :deep(.el-button) { width: 100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; }.taskfooter { gap: 14px; } } }
@media (max-width: 900px) { .channel { padding: 14px; .training-core { padding: 18px; border-radius: 24px; }.workbench-head,.workbench-actions,.step-navigation { flex-direction: column; align-items: stretch; }.workbench-actions,.outline-action,.btn-nav { width: 100%; }.step-indicator { align-items:stretch; flex-direction:column; width:100%; margin-right:0; margin-left:0; overflow:visible; border-radius:20px; }.step-tab { flex:0 0 auto; width:100%; }.step-connector { flex:0 0 22px; width:2px; height:22px; margin-left:29px; text-align:center; writing-mode:vertical-rl; } } }
.step-tab.active { border-color:var(--student-accent); color:#ffffff; background:var(--student-accent); box-shadow:0 0 14px rgba(31, 41, 55, 0.12); }.step-tab.active .step-marker { border-color:var(--student-accent); color:#ffffff; background:var(--student-accent); box-shadow:0 0 0 5px rgba(31, 41, 55, 0.1); }.step-tab.complete { color:var(--student-text-secondary); }.step-tab.complete .step-marker { border-color:var(--student-success); color:#ffffff; background:var(--student-success); }.step-connector.complete { background:var(--student-success); color:var(--student-success); }
/* Standard page step navigation */
.step-indicator { position:relative; display:flex; align-items:center; gap:0; width:calc(100% + 24px); margin:0 -12px 32px; padding:18px 22px; overflow-x:auto; border:1px solid var(--student-panel-border); border-radius:18px; background:var(--student-card-bg); box-shadow:inset 0 1px 0 rgba(31, 41, 55, 0.04); scrollbar-width:thin; }

@ -17,18 +17,9 @@
</span>
</p>
</div>
<div class="supplier-toolbar">
<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"
/>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveStepThree" />
</section>
<section class="supplier-workbench">
@ -217,7 +208,7 @@ import { computed, onMounted, ref } from "vue";
import { CircleCheck, DataAnalysis, Delete, Plus } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import {
@ -672,11 +663,17 @@ onMounted(async () => {
p { margin: 0; }
}
.supplier-toolbar {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* ===== supplier-workbench(透明容器) ===== */
@ -988,7 +985,6 @@ onMounted(async () => {
@media (max-width: 720px) {
.supplier-page { padding: 12px; }
.task-header { flex-direction: column; align-items: flex-start; }
.supplier-toolbar { width: 100%; justify-content: flex-start; }
.risk-heading { align-items: flex-start; flex-direction: column; gap: 8px; }
.split-actions { gap: 12px; flex-wrap: wrap; }
}

@ -19,16 +19,10 @@
</span>
</p>
</div>
<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 class="action-button" type="button" @click="exportOutcome"><el-icon>
<Download />
</el-icon>导出实训成果</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveStepThree" :on-export="exportOutcome" />
</section>
<section class="workbench">
@ -378,7 +372,7 @@ import { checkBusinessFeasibilityStepOne, checkBusinessFeasibilityStepTwo, check
import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
const TASK_KEY = "business-feasibility"
const DEFAULT_STEPS = ["商业可行性论证", "项目初步决策", "可行性论证报告", "决策结论"]
@ -546,6 +540,19 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ task: taskTit
min-width: 0;
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.task-title {
margin: 0;

@ -17,19 +17,10 @@
</span>
</p>
</div>
<div class="analysis-toolbar">
<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 /></el-icon> 导出实训成果</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveAndCompleteTraining" :on-reset="resetStepFour" :on-export="exportOutcome" />
</section>
<section class="analysis-workbench">
@ -62,7 +53,6 @@
<section class="decision-section"><h3>(1)优先策略排序</h3><p class="section-tip">策略内容由第 3 步填写的“具体策略建议”自动带入;请完成优先级排序并说明理由。</p><div class="priority-table-wrap"><table class="priority-table"><thead><tr><th>优先级排序</th><th>策略</th><th>理由</th></tr></thead><tbody><tr v-for="row in strategyDefinitions" :key="row.key"><td><el-select v-model="finalForm.priorities[row.key].priority" :disabled="!stepThreePassed" :aria-label="`${row.label} 的优先级排序`" placeholder="请选择" style="width:100%"><el-option :value="null" label="请选择" /><el-option v-for="rank in 4" :key="rank" :value="rank" :label="`${rank}`" /></el-select></td><td class="priority-strategy"><strong>{{ row.label }}</strong><p>{{ strategyForm[row.key].recommendation || '请先在第 3 步填写该策略建议' }}</p></td><td><el-input v-model.trim="finalForm.priorities[row.key].reason" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="说明该策略的排序理由" /></td></tr></tbody></table></div></section>
<section class="decision-section"><h3>(2)需关注的风险点</h3><p>请至少指出 2 个风险点。</p><div class="risk-grid"><label><span>核心风险点 1</span><el-input v-model.trim="finalForm.coreRiskOne" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>核心风险点 2</span><el-input v-model.trim="finalForm.coreRiskTwo" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="填写核心风险点" /></label><label><span>其他风险点</span><el-input v-model.trim="finalForm.otherRisk" type="textarea" :autosize="{ minRows: 2 }" :disabled="!stepThreePassed" placeholder="可补充其他风险点" /></label></div></section>
<section class="decision-section"><h3>(3)总结 SWOT 竞争环境评估结论</h3><el-input v-model.trim="finalForm.conclusion" class="conclusion-textarea" type="textarea" :autosize="{ minRows: 3 }" :disabled="!stepThreePassed" placeholder="综合说明优先策略、风险应对方向及最终结论" /></section>
<div class="task-actions"><button type="button" class="btn btn-primary" :disabled="saving || !stepThreePassed" @click="saveAndCompleteTraining">保存并完成实训</button><button type="button" class="btn" :disabled="saving || !stepThreePassed" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon> 清空填写</button></div>
</template>
<div v-else class="step-placeholder"><p>第 {{ currentStep }} 步内容待配置</p><span>第 1 步 SWOT 分类已保存,可从上方步骤栏返回查看。</span></div>
</section>
@ -73,13 +63,13 @@
</template>
<script setup>
import { Download, RefreshLeft, TrendCharts } from "@element-plus/icons-vue";
import { RefreshLeft, TrendCharts } from "@element-plus/icons-vue";
import { checkCompetitiveEnvironmentAnalysisStepOne, checkCompetitiveEnvironmentAnalysisStepTwo, checkCompetitiveEnvironmentAnalysisStepThree, checkCompetitiveEnvironmentAnalysisStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "competitive-environment-analysis";
const { proxy } = getCurrentInstance();
@ -289,6 +279,19 @@ function exportOutcome() { const blob = new Blob([JSON.stringify(buildDraft(), n
justify-content: flex-end;
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* workbench 层:透明容器 */
.analysis-workbench {
margin: 0;

@ -1,4 +1,4 @@
<template>
<template>
<div class="consumer-behavior-page student-training-shell">
<main class="training-core">
<section class="task-header">
@ -17,22 +17,10 @@
</span>
</p>
</div>
<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 /></el-icon>
导出实训成果
</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
</section>
<section class="task-card">
@ -272,10 +260,6 @@
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
<el-icon><CircleCheck /></el-icon>
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
@ -299,7 +283,7 @@ import { computed, getCurrentInstance, nextTick, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Upload } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkConsumerBehaviorStepOne, checkConsumerBehaviorStepTwo, checkConsumerBehaviorStepThree, checkConsumerBehaviorStepFour, getStudentTrainingAnswer, previewConsumerBehaviorQuestionnaireWord, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
@ -1105,6 +1089,22 @@ function downloadSurveyReportTemplate() {
gap: 8px;
}
/* ============================================================
* 操作工具栏横条:内联布局,去掉悬浮和边框
* ============================================================ */
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
.section-eyebrow {
margin: 0;
color: var(--student-text-secondary);

@ -19,26 +19,11 @@
</div>
</section>
<section class="consumer-scenario-workbench">
<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 /></el-icon>
导出实训成果
</button>
</div>
</div>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
</section>
<section class="consumer-scenario-workbench">
<nav v-if="trainingSteps.length" class="step-indicator" aria-label="消费场景解构步骤">
<template v-for="(step, index) in trainingSteps" :key="`${index}-${step}`">
<button
@ -248,10 +233,6 @@
</div>
<div class="training-actions">
<el-button class="training-action training-action--submit" :loading="saving" @click="submitTask">
提交任务
<el-icon><CircleCheck /></el-icon>
</el-button>
<el-button class="training-action training-action--reset" @click="resetTraining">
重新实训
</el-button>
@ -274,7 +255,7 @@
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download, UploadFilled } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkConsumerScenarioStepOne, checkConsumerScenarioStepTwo, checkConsumerScenarioStepThree, checkConsumerScenarioStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
@ -1059,6 +1040,19 @@ function exportOutcome() {
p { margin: 0; }
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* 顶部任务工具栏(保留 DOM,按钮由全局接管) */
.workbench-head--actions-only {
flex: 0 0 auto;

@ -19,26 +19,11 @@
</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 /></el-icon>
导出实训成果
</button>
</div>
</div>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
</section>
<section class="task-card">
<nav v-if="flowSteps.length" class="step-indicator" aria-label="需求转化为功能步骤">
<template v-for="(step, index) in flowSteps" :key="`${step}-${index}`">
<button
@ -218,9 +203,6 @@
</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" @click="resetTraining">
清空填写
</el-button>
@ -240,9 +222,9 @@
<script setup>
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, CircleCheck } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { checkFeatureConversionStepOne, checkFeatureConversionStepTwo, checkFeatureConversionStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
@ -790,6 +772,19 @@ function exportOutcome() {
}
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.task-head-main {
display: grid;
justify-items: start;

@ -17,7 +17,10 @@
</span>
</p>
</div>
<TrainingTaskToolbar class="industry-demand-toolbar" :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTraining" :on-reset="resetTraining" :on-export="exportOutcome" />
</section>
<section class="industry-workbench">
@ -360,10 +363,17 @@ function exportOutcome() { const blob = new Blob([JSON.stringify(buildDraft(), n
p { margin: 0; }
}
.industry-demand-toolbar {
flex: 0 0 auto;
justify-content: flex-end;
max-width: 100%;
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* step-card 层:透明容器,去掉旧的 border/bg(对齐 foundation 的 .task-card) */
@ -668,7 +678,6 @@ function exportOutcome() { const blob = new Blob([JSON.stringify(buildDraft(), n
@media (max-width: 720px) {
.task-header { flex-direction: column; align-items: flex-start; }
.industry-demand-toolbar { width: 100%; justify-content: flex-start; }
.step-chip { display: none; }
.step-navigation { flex-direction: column; }
.step-navigation .btn-nav { width: 100%; }

@ -21,24 +21,11 @@
</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 />
</el-icon>
导出实训成果
</button>
</div>
</div>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" :on-reset="resetTraining" />
</section>
<section class="task-card">
<nav v-if="steps.length" class="step-indicator" aria-label="目标用户画像步骤">
<button v-for="(step, index) in steps" :key="step.no" type="button"
:class="['step-tab', { active: currentStep === step.no, complete: currentStep > step.no }]"
@ -352,10 +339,6 @@
</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" @click="resetTraining">
重新实训
</el-button>
@ -374,7 +357,7 @@
import { ArrowLeft, ArrowRight, CircleCheck, Document, Download, Upload } from "@element-plus/icons-vue"
import * as XLSX from "xlsx"
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue"
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue"
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue"
import { getTrainingTaskByKey } from "@/api/trainingTask"
import { parseTrainingArray } from "@/views/training/taskKeyMap"
import { checkTargetUserProfileStepFive, checkTargetUserProfileStepFour, checkTargetUserProfileStepOne, checkTargetUserProfileStepThree, checkTargetUserProfileStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer"
@ -926,6 +909,19 @@ function exportOutcome() {
box-shadow: none;
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
/* 任务头:白卡 + 左侧教育蓝竖条 + 12px 圆角 */
.task-header {
position: relative;

@ -17,19 +17,10 @@
</span>
</p>
</div>
<div class="after-sales-toolbar">
<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 /></el-icon>导出实训成果</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="validateAndSaveStepThree" :on-export="exportOutcome" />
</section>
<section class="after-sales-workbench">
@ -225,7 +216,6 @@
<p v-if="stepThreeResult" class="result-message" :class="{ success: stepThreeResult.valid }">{{ stepThreeResult.message }}</p>
<div class="task-actions">
<button type="button" class="btn-nav" :disabled="savingStepThree" @click="switchStep(2)"><el-icon><ArrowLeft /></el-icon>返回第 2 步</button>
<button type="button" class="btn-nav btn-submit" :disabled="savingStepThree" @click="validateAndSaveStepThree">{{ savingStepThree ? '提交中…' : '校验并提交任务' }}<el-icon><ArrowRight /></el-icon></button>
</div>
</section>
@ -249,9 +239,9 @@
<script setup>
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, Download, RefreshLeft, Service } from "@element-plus/icons-vue";
import { ArrowLeft, ArrowRight, RefreshLeft, Service } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { checkAfterSalesServiceStepOne, checkAfterSalesServiceStepTwo, checkAfterSalesServiceStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingPage } from "@/views/training/studentTrainingPages";
@ -599,14 +589,17 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ currentStep:
.after-sales-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
flex: 0 0 auto;
justify-content: flex-end;
max-width: 100%;
.task-action-strip {
margin: 0;
padding: 10px 16px;
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* ===== workbench:透明化 ===== */

@ -19,26 +19,11 @@
</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 /></el-icon>
导出实训成果
</button>
</div>
</div>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
</section>
<section class="task-card">
<nav v-if="configuredSteps.length" class="step-indicator" aria-label="产品价值主张及定位步骤">
<template v-for="(step, index) in configuredSteps" :key="`${step}-${index}`">
<button
@ -253,7 +238,6 @@
</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" @click="resetTraining">清空填写</el-button>
</div>
</section>
@ -272,7 +256,7 @@
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowLeft, ArrowRight, CircleCheck, Download, Operation, UploadFilled } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { checkProductValuePositioningStepOne, checkProductValuePositioningStepTwo, checkProductValuePositioningStepThree, checkProductValuePositioningStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
@ -817,6 +801,19 @@ async function handleValueCanvasUpload(event) {
p { margin: 0; }
}
/* task-action-strip:TrainingTaskToolbar 横条 */
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
/* task-card / step-card 层:透明容器 */
.task-card,
.step-card {

@ -19,22 +19,16 @@
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveStepFour" />
</section>
<section class="hardware-workbench">
<div class="workbench-head hardware-workbench-head">
<div>
<p class="section-eyebrow">BOM cost workspace</p>
<h2>智能硬件成本估算</h2>
</div>
<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"
/>
</div>
<nav class="step-indicator" aria-label="智能硬件成本估算步骤">
@ -327,7 +321,7 @@
import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
import { DataAnalysis } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile, validateHardwareCostStepOne, validateHardwareCostStepTwo, validateHardwareCostStepThree, validateHardwareCostStepFour } from "@/api/studentTrainingAnswer";
@ -1433,6 +1427,19 @@ onMounted(async () => {
.step-chip { display: none; }
.bom-table-wrap .bom-table { min-width: 720px; }
}
/* ===== 操作工具栏横条 ===== */
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
</style>
<style lang="scss">

@ -17,19 +17,9 @@
</span>
</p>
</div>
<div class="opportunity-toolbar">
<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 class="btn" type="button" @click="exportOutcome"><el-icon><Download /></el-icon> 导出实训成果</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveAndCompleteTraining" />
</section>
<section class="workbench">
@ -83,7 +73,7 @@
<p class="intro">结合前面的数据分析,请说明应该进行开发什么产品,具体原因是什么。</p>
<p v-if="!stepFivePassed" class="warning">请先完成并保存第 5 步的用户需求洞察,再填写本步骤。</p>
<div class="conclusion-field"><label for="market-opportunity-conclusion">开发建议与原因</label><el-input id="market-opportunity-conclusion" v-model.trim="developmentConclusion" type="textarea" :autosize="{ minRows: 4 }" :disabled="!stepFivePassed" placeholder="请结合综合得分、评论痛点与未被满足的需求,说明建议开发的产品及具体原因。" /></div>
<div class="step-actions"><button class="btn primary" type="button" :disabled="saving || !stepFivePassed" @click="saveAndCompleteTraining">保存并完成实训</button><button class="btn" type="button" :disabled="saving || !stepFivePassed" @click="resetStepSix"><el-icon><RefreshLeft /></el-icon> 清空填写</button></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>
@ -101,7 +91,7 @@ import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import { isStudentDemo } from "@/utils/studentDemo";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
const TASK_KEY = "market-opportunity-selection";
const SCORE_CALCULATION_GUIDE_URL = "/file/training-materials/market-opportunity-selection/6.综合得分计算操作说明.docx";
@ -273,6 +263,18 @@ function exportOutcome() { const blob = new Blob([JSON.stringify({ taskName: tas
box-shadow: none;
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
/* 任务头:独立卡片 + 左侧教育蓝竖条 */
.task-header {
position: relative;

@ -17,21 +17,10 @@
</span>
</p>
</div>
<div class="price-toolbar">
<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="exportPriceOutcome">
<el-icon><Download /></el-icon>导出实训成果
</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="saveStepFive" :on-export="exportPriceOutcome" />
</section>
<section class="price-workbench">
@ -348,11 +337,6 @@
</section>
<p v-if="validationMessage" class="validation-message">{{ validationMessage }}</p>
<div class="step-footer">
<button type="button" class="primary-button" :disabled="saving" @click="saveStepFive">
{{ saving ? labels.finalPricingSaving : labels.finalPricingSave }}
</button>
</div>
</section>
</template>
@ -468,10 +452,9 @@
</template>
<script setup>
import { Download } from "@element-plus/icons-vue";
import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import {
getStudentTrainingAnswer,
saveStudentTrainingAnswer,
@ -1683,11 +1666,18 @@ onMounted(async () => {
p { margin: 0; }
}
.price-toolbar {
display: flex;
flex: 0 0 auto;
gap: 8px;
align-items: center;
.task-action-strip {
margin: 0;
padding: 10px 16px;
// 让 TrainingTaskToolbar 内部按钮在横条内右对齐
:deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
}
/* workbench:透明容器,去掉旧 border/bg */
@ -1741,34 +1731,7 @@ onMounted(async () => {
line-height: 1.6;
}
/* ===== 按钮(白底灰边 / outline-action) ===== */
.outline-action {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 36px;
height: 36px;
padding: 0 14px;
border: 1px solid rgba(31, 41, 55, 0.4);
border-radius: 8px;
color: var(--student-accent);
background: #ffffff;
font: inherit;
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease,
transform 0.16s ease, box-shadow 0.16s ease;
&:hover, &:focus-visible {
border-color: var(--student-accent);
background: var(--student-accent-soft);
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(31, 41, 55, 0.12);
}
}
/* ===== 按钮 ===== */
.primary-button {
display: inline-flex;
align-items: center;
@ -2333,10 +2296,6 @@ onMounted(async () => {
.psm-point-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.price-toolbar { width: 100%; }
}
</style>
<style lang="scss">
@ -2495,7 +2454,6 @@ onMounted(async () => {
/* ============================================================
* 共用组件换肤:TrainingMaterialButton 资料按钮
* ============================================================ */
.price-page .price-toolbar .material-button.el-button,
.price-page .workbench-actions .material-button.el-button {
height: 34px !important;
min-height: 34px !important;
@ -2533,7 +2491,6 @@ onMounted(async () => {
/* ============================================================
* 顶部导出按钮(outline-action)
* ============================================================ */
.price-page .price-toolbar > button.outline-action,
.price-page .workbench-actions > button.outline-action {
display: inline-flex !important;
align-items: center !important;

@ -17,19 +17,10 @@
</span>
</p>
</div>
<div class="requirements-toolbar">
<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 /></el-icon>{{ currentStep === 3 ? '预览产品需求说明书' : '导出实训成果' }}</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="submitTask" :on-export="exportOutcome" />
</section>
<section class="task-card">
@ -193,7 +184,6 @@
<div class="task-actions">
<button type="button" class="btn-nav" :disabled="!isDocumentReady || saving || submitting || documentUploading" @click="saveStepThree">{{ saving ? '保存中…' : '保存文档' }}</button>
<button type="button" class="btn-nav btn-submit" :disabled="!isDocumentReady || documentUploading" @click="showDocumentPreview = true"><el-icon><View /></el-icon>预览文档</button>
<button type="button" class="btn-nav btn-primary" :disabled="!canSubmitTask || saving || submitting || documentUploading" @click="submitTask">{{ submitting ? '提交中…' : '提交任务' }}</button>
</div>
</section>
@ -227,7 +217,7 @@
import { computed, getCurrentInstance, onMounted, ref } from "vue";
import { ArrowRight, Close, Delete, Document, Download, InfoFilled, RefreshLeft, UploadFilled, View } from "@element-plus/icons-vue";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { checkRequirementsDocumentStepOne, getStudentTrainingAnswer, previewRequirementsDocumentFile, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
@ -1406,6 +1396,19 @@ function exportOutcome() { if (currentStep.value === 2) { exportStepTwoTable();
.purpose-bank { padding: 14px; }
.example-bar { flex-direction: column; }
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
</style>
<style lang="scss">

@ -17,19 +17,10 @@
</span>
</p>
</div>
<div class="architecture-toolbar">
<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 /></el-icon>导出实训成果</button>
</div>
</section>
<section class="task-action-strip">
<TrainingTaskToolbar :task-key="TASK_KEY" :on-submit="checkAndSaveStepFour" :on-export="exportOutcome" />
</section>
<section class="architecture-workbench">
@ -155,7 +146,7 @@
</section>
<section class="architecture-guide"><h4>参考工具及方案</h4><ol><li>下载模板,按应用功能层、算法处理层、驱动感知层、硬件物理层补全逻辑调用关系。</li><li>每张图围绕一个核心功能绘制,使用箭头清晰标注信号流向。</li></ol><button type="button" @click="downloadArchitectureTemplate">下载模板</button></section>
<p v-if="stepFourCheckResult" class="result-message" :class="{ success: stepFourCheckResult.valid }">{{ stepFourCheckResult.message }}</p>
<div class="task-actions"><button type="button" class="btn-nav" :disabled="uploadingArchitectureImage !== null || checking" @click="saveStepFour">保存进度</button><button type="button" class="btn-nav btn-submit" :disabled="uploadingArchitectureImage !== null || checking" @click="checkAndSaveStepFour">{{ checking ? '校验中…' : '校验并完成实训' }}</button></div>
<div class="task-actions"><button type="button" class="btn-nav" :disabled="uploadingArchitectureImage !== null || checking" @click="saveStepFour">保存进度</button></div>
</section>
<section v-else class="architecture-step-card placeholder-card"><p class="section-eyebrow">Step {{ String(currentStep).padStart(2, '0') }}</p><h3>{{ trainingSteps[currentStep - 1] }}</h3></section>
</section>
@ -171,7 +162,7 @@ import { checkProductArchitectureDesignStepOne, checkProductArchitectureDesignSt
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { parseTrainingArray } from "@/views/training/taskKeyMap";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskToolbar from "@/views/components/TrainingTaskToolbar.vue";
import { SCENARIO_FIELDS, getCompletedScenarioCount, getRowsForScenarioValidation, getScenarioValidationMessage } from "./productArchitectureScenarioValidation.mjs";
const { proxy } = getCurrentInstance();
@ -1260,6 +1251,18 @@ button:disabled {
.task-header { flex-direction: column; align-items: flex-start; }
.architecture-toolbar { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
}
.task-action-strip {
margin: 0;
padding: 10px 16px;
}
.task-action-strip :deep(.training-task-toolbar) {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
</style>
<style lang="scss">

Loading…
Cancel
Save