-
-
- 重新实训
-
-
diff --git a/src/views/product/differentiation.vue b/src/views/product/differentiation.vue
index 3169922..e8c53d6 100644
--- a/src/views/product/differentiation.vue
+++ b/src/views/product/differentiation.vue
@@ -22,7 +22,8 @@
-
-
-
-
-
-
-
清空填写
+
+
+
+
+
+
Step {{ String(form.activeStep).padStart(2, "0") }} / {{
+ String(configuredSteps.length).padStart(2, "0") }}
+
+
@@ -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-nav-left,
+.step-nav-right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: nowrap;
}
-.step-navigation .btn-nav {
+.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) {
- 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);
+ .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);
+ }
}
-.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 {
- 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);
+ &:hover:not(:disabled) {
+ border-color: var(--student-accent);
+ 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;
- justify-content: center;
- gap: 14px;
- margin-top: 20px;
+.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%;
+ }
}
/* ===== 响应式 ===== */
@@ -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;
-}
diff --git a/src/views/product/market-opportunity-selection.vue b/src/views/product/market-opportunity-selection.vue
index 1919e3a..ff6da41 100644
--- a/src/views/product/market-opportunity-selection.vue
+++ b/src/views/product/market-opportunity-selection.vue
@@ -69,11 +69,6 @@
-
@@ -125,12 +120,6 @@
-
@@ -175,12 +164,6 @@
@click="downloadInstructions">下载实训操作说明
-
@@ -222,9 +205,6 @@
class="btn primary" type="button"
@click="downloadVisualizationTemplate">下载自动化模板
-
@@ -272,12 +252,6 @@
提示词:请计算 Excel 表中产品评论数据的好评率,提炼用户痛点和未被满足的需求。
-
@@ -293,15 +267,41 @@
id="market-opportunity-conclusion" v-model.trim="developmentConclusion" type="textarea"
:autosize="{ minRows: 4 }" :disabled="!stepFivePassed"
placeholder="请结合综合得分、评论痛点与未被满足的需求,说明建议开发的产品及具体原因。" />
-
第 {{ currentStep }} 步内容待配置
已保存的步骤可从上方步骤栏返回查看。
+
+
+
+
+
+
+
+
Step {{ String(currentStep).padStart(2, "0") }} / {{
+ String(trainingSteps.length).padStart(2, "0") }}
+
+
+
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;