|
|
|
|
@ -13,31 +13,43 @@
|
|
|
|
|
:show-material="false"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<nav class="step-navigation" aria-label="智能硬件成本估算步骤">
|
|
|
|
|
<button type="button" class="step-nav-item" :class="{ active: currentStep === 1 }" @click="currentStep = 1">
|
|
|
|
|
<span>01</span> BOM 物料选型
|
|
|
|
|
<section class="task-card 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" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<nav class="step-indicator" aria-label="智能硬件成本估算步骤">
|
|
|
|
|
<button type="button" :class="['step-tab', { active: currentStep === 1, complete: currentStep > 1 }]" aria-label="切换至第 1 步:BOM 物料选型" :aria-current="currentStep === 1 ? 'step' : undefined" @click="currentStep = 1">
|
|
|
|
|
<span class="step-marker" aria-hidden="true">01</span>
|
|
|
|
|
<span class="step-copy"><strong class="step-name" title="BOM 物料选型">BOM 物料选型</strong><span class="step-action">{{ currentStep === 1 ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span></span>
|
|
|
|
|
</button>
|
|
|
|
|
<i class="step-connector"></i>
|
|
|
|
|
<button type="button" class="step-nav-item" :class="{ active: currentStep === 2 }" @click="openStepTwo">
|
|
|
|
|
<span>02</span> 批量成本核算
|
|
|
|
|
<span class="step-connector" aria-hidden="true" />
|
|
|
|
|
<button type="button" :class="['step-tab', { active: currentStep === 2, complete: currentStep > 2 }]" aria-label="切换至第 2 步:批量成本核算" :aria-current="currentStep === 2 ? 'step' : undefined" @click="openStepTwo">
|
|
|
|
|
<span class="step-marker" aria-hidden="true">02</span>
|
|
|
|
|
<span class="step-copy"><strong class="step-name" title="批量成本核算">批量成本核算</strong><span class="step-action">{{ currentStep === 2 ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span></span>
|
|
|
|
|
</button>
|
|
|
|
|
<i class="step-connector"></i>
|
|
|
|
|
<button type="button" class="step-nav-item" :class="{ active: currentStep === 3 }" @click="openStepThree">
|
|
|
|
|
<span>03</span> 制造成本核算
|
|
|
|
|
<span class="step-connector" aria-hidden="true" />
|
|
|
|
|
<button type="button" :class="['step-tab', { active: currentStep === 3, complete: currentStep > 3 }]" aria-label="切换至第 3 步:制造成本核算" :aria-current="currentStep === 3 ? 'step' : undefined" @click="openStepThree">
|
|
|
|
|
<span class="step-marker" aria-hidden="true">03</span>
|
|
|
|
|
<span class="step-copy"><strong class="step-name" title="制造成本核算">制造成本核算</strong><span class="step-action">{{ currentStep === 3 ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span></span>
|
|
|
|
|
</button>
|
|
|
|
|
<i class="step-connector"></i>
|
|
|
|
|
<button type="button" class="step-nav-item" :class="{ active: currentStep === 4 }" @click="openStepFour">
|
|
|
|
|
<span>04</span> 成本优化决策
|
|
|
|
|
<span class="step-connector" aria-hidden="true" />
|
|
|
|
|
<button type="button" :class="['step-tab', { active: currentStep === 4, complete: currentStep > 4 }]" aria-label="切换至第 4 步:成本优化决策" :aria-current="currentStep === 4 ? 'step' : undefined" @click="openStepFour">
|
|
|
|
|
<span class="step-marker" aria-hidden="true">04</span>
|
|
|
|
|
<span class="step-copy"><strong class="step-name" title="成本优化决策">成本优化决策</strong><span class="step-action">{{ currentStep === 4 ? '当前步骤' : '点击切换' }} <span aria-hidden="true">→</span></span></span>
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<section v-if="currentStep === 1" class="task-card">
|
|
|
|
|
<section v-if="currentStep === 1" class="step-card hardware-step-card">
|
|
|
|
|
<div class="workbench-head">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="section-eyebrow">Step 01 · BOM selection</p>
|
|
|
|
|
<h2>核心元器件选型组合</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<TrainingMaterialButton :material-name="taskConfig?.materialName" :material-url="taskConfig?.materialUrl" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<section class="cost-brief">
|
|
|
|
|
@ -90,7 +102,7 @@
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-else-if="currentStep === 2" class="task-card">
|
|
|
|
|
<section v-else-if="currentStep === 2" class="step-card hardware-step-card">
|
|
|
|
|
<div class="workbench-head">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="section-eyebrow">Step 02 · Volume costing</p>
|
|
|
|
|
@ -153,7 +165,7 @@
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-else-if="currentStep === 3" class="task-card">
|
|
|
|
|
<section v-else-if="currentStep === 3" class="step-card hardware-step-card">
|
|
|
|
|
<div class="workbench-head">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="section-eyebrow">Step 03 · Manufacturing costing</p>
|
|
|
|
|
@ -212,7 +224,7 @@
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section v-else class="task-card">
|
|
|
|
|
<section v-else class="step-card hardware-step-card">
|
|
|
|
|
<div class="workbench-head">
|
|
|
|
|
<div>
|
|
|
|
|
<p class="section-eyebrow">Step 04 · Cost optimisation</p>
|
|
|
|
|
@ -281,12 +293,24 @@
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<TrainingAiSidebar
|
|
|
|
|
:task-key="TASK_KEY"
|
|
|
|
|
:has-saved-answer="hasSavedAnswer"
|
|
|
|
|
:progress-items="progressItems"
|
|
|
|
|
:study-tip="currentStudyTip"
|
|
|
|
|
reference-title="硬件成本核算提示"
|
|
|
|
|
reference-text="· BOM 金额 = 单价 × 数量<br />· 小批量和大批量成本需分别核算<br />· 优化方案应说明可执行的降本措施"
|
|
|
|
|
assist-text="建议先检查物料数量、单价和金额关系,再比较批量成本差异并形成可执行的成本优化结论。"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { computed, getCurrentInstance, onMounted, reactive, ref } from "vue";
|
|
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
|
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
|
|
|
|
|
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
|
|
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
|
|
@ -328,8 +352,24 @@ const stepFour = reactive({
|
|
|
|
|
|
|
|
|
|
const taskTitle = computed(() => taskConfig.value?.taskName || "智能硬件成本估算");
|
|
|
|
|
const taskBackground = computed(() => taskConfig.value?.background || "围绕智能硬件产品的核心元器件进行 BOM 选型与成本核算。");
|
|
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.goals, ["理解元器件性能、成本与产品定位之间的平衡", "完成可追溯的 BOM 选型明细"]));
|
|
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirement || "基于案例资料中的元器件选型库完成 BOM 物料明细,并通过系统校验后保存。");
|
|
|
|
|
const taskGoals = computed(() => parseTrainingArray(taskConfig.value?.objectives ?? taskConfig.value?.goals, ["理解元器件性能、成本与产品定位之间的平衡", "完成可追溯的 BOM 选型明细"]));
|
|
|
|
|
const taskRequirement = computed(() => taskConfig.value?.requirements || taskConfig.value?.requirement || "基于案例资料中的元器件选型库完成 BOM 物料明细,并通过系统校验后保存。");
|
|
|
|
|
const hasSavedAnswer = computed(() => savedStepOneItems.value.length > 0 || stepTwoSaved.value || stepThreeSaved.value || Boolean(stepFour.optimizationPlan || stepFour.smallBatchChart.url || stepFour.largeBatchChart.url));
|
|
|
|
|
const currentStudyTip = computed(() => {
|
|
|
|
|
const tips = {
|
|
|
|
|
1: "先完成 BOM 物料选型,确保物料名称、规格、单价和数量之间的关系准确。",
|
|
|
|
|
2: "分别核算小批量和大批量的物料成本,重点比较批量差异对单件成本的影响。",
|
|
|
|
|
3: "结合制造环节成本与 BOM 成本,核算不同批量下的完整单件成本。",
|
|
|
|
|
4: "根据成本核算结果,给出有依据、可执行的硬件成本优化方案。",
|
|
|
|
|
};
|
|
|
|
|
return tips[currentStep.value] || tips[1];
|
|
|
|
|
});
|
|
|
|
|
const progressItems = computed(() => [
|
|
|
|
|
{ text: "BOM 物料选型", status: savedStepOneItems.value.length ? "done" : currentStep.value === 1 ? "doing" : "" },
|
|
|
|
|
{ text: "物料 BOM 成本核算", status: stepTwoSaved.value ? "done" : currentStep.value === 2 ? "doing" : "" },
|
|
|
|
|
{ text: "制造成本核算", status: stepThreeSaved.value ? "done" : currentStep.value === 3 ? "doing" : "" },
|
|
|
|
|
{ text: "成本优化决策", status: currentStep.value === 4 ? "doing" : "" },
|
|
|
|
|
]);
|
|
|
|
|
const canAdd = computed(() => rows.value.length < MAX_ROWS);
|
|
|
|
|
const canDelete = computed(() => rows.value.length > 1);
|
|
|
|
|
const totalAmount = computed(() => rows.value.reduce((total, row) => total + rowAmount(row), 0));
|
|
|
|
|
@ -759,4 +799,185 @@ onMounted(async () => {
|
|
|
|
|
.manufacturing-table{min-width:820px}.manufacturing-table th:nth-child(1){width:8%}.manufacturing-table th:nth-child(2){width:32%}.manufacturing-table th:nth-child(3),.manufacturing-table th:nth-child(4){width:30%}.reference-amount{text-align:center;color:#a6d8ee;font-size:16px;font-weight:800;background:rgba(8,71,102,.3)!important}
|
|
|
|
|
.step-four-summary{margin:0 0 18px}.chart-upload-section,.optimization-section{margin-top:18px}.chart-upload-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}.chart-upload-card{padding:18px;border:1px solid rgba(75,185,230,.27);border-radius:13px;background:rgba(3,29,45,.68)}.chart-upload-card h4{margin:0;color:#e9f9ff;font-size:17px}.chart-upload-card p{min-height:40px;margin:7px 0 15px;color:#95b9ca;font-size:13px;line-height:1.65}.chart-upload-trigger{display:flex;align-items:center;justify-content:center;min-height:42px;border:1px dashed rgba(102,221,255,.58);border-radius:8px;color:#c8f5ff;font-weight:800;cursor:pointer;background:rgba(12,112,158,.18);transition:.18s ease}.chart-upload-trigger:hover{border-color:#75e4ff;background:rgba(15,144,195,.31)}.chart-upload-trigger.uploading{pointer-events:none;opacity:.65}.chart-upload-trigger input{display:none}.chart-preview{display:grid;grid-template-columns:104px 1fr auto;gap:12px;align-items:center;margin-top:14px;padding:10px;border:1px solid rgba(105,190,222,.26);border-radius:9px;background:rgba(2,19,31,.58)}.chart-preview :deep(.el-image){width:104px;height:72px;border-radius:6px;cursor:zoom-in;background:#061827}.chart-preview>div{display:grid;gap:4px;min-width:0}.chart-preview span{overflow:hidden;color:#cbe9f5;font-size:13px;font-weight:700;text-overflow:ellipsis;white-space:nowrap}.chart-preview small{color:#8fb4c7;font-size:12px}.chart-preview button{border:0;color:#ffc6ce;font-size:13px;cursor:pointer;background:transparent}.decision-hint{margin:8px 0 0;color:#91b8ca;font-size:13px}.reference-downloads{display:flex;flex-wrap:wrap;gap:10px;margin-top:18px}.reference-downloads .btn-nav{display:inline-flex;align-items:center;gap:7px}.reference-downloads .btn-nav::before{content:'↓';font-size:16px}.reference-downloads .btn-nav:hover{color:#e4faff}
|
|
|
|
|
@media(max-width:780px){.chart-upload-grid{grid-template-columns:1fr}.chart-preview{grid-template-columns:86px 1fr}.chart-preview :deep(.el-image){width:86px;height:62px}.chart-preview button{grid-column:2;text-align:left;padding:0}.reference-downloads .btn-nav{flex:1;justify-content:center}}
|
|
|
|
|
/* 与 foundation/new-product-survey 保持同一套步骤条局部细节;公共规则仍由 student-training.scss 提供。 */
|
|
|
|
|
/* AI 侧栏启用后,主内容必须收敛到所属网格列,避免固定宽度侵入侧栏区域。 */
|
|
|
|
|
.hardware-cost-page .training-core {
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 28px;
|
|
|
|
|
border: 1px solid rgba(0, 180, 255, 0.25);
|
|
|
|
|
border-radius: 36px;
|
|
|
|
|
background: rgba(8, 18, 28, 0.6);
|
|
|
|
|
box-shadow: inset 0 1px 0 rgba(169, 229, 255, 0.08), 0 22px 48px rgba(0, 0, 0, 0.22);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .task-header {
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .task-badge {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
min-height: 28px;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 4px 14px;
|
|
|
|
|
border: 1px solid rgba(99, 217, 255, 0.55);
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
color: #dff5ff;
|
|
|
|
|
background: rgba(17, 126, 178, 0.36);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .task-title {
|
|
|
|
|
margin: 12px 0 0;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 30px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .hardware-workbench {
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
padding: 18px 22px 22px;
|
|
|
|
|
border: 1px solid rgba(82, 174, 226, 0.42);
|
|
|
|
|
border-radius: 26px;
|
|
|
|
|
background: rgba(0, 25, 42, 0.64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .hardware-workbench-head {
|
|
|
|
|
margin-bottom: 22px;
|
|
|
|
|
padding-bottom: 18px;
|
|
|
|
|
border-bottom: 1px solid rgba(0, 180, 255, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .hardware-workbench-head h2 {
|
|
|
|
|
margin: 5px 0 0;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .hardware-workbench > .step-indicator {
|
|
|
|
|
margin: 0 0 30px !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hardware-cost-page .hardware-step-card {
|
|
|
|
|
padding: 24px;
|
|
|
|
|
border: 1px solid rgba(82, 174, 226, 0.42);
|
|
|
|
|
border-radius: 26px;
|
|
|
|
|
background: rgba(0, 25, 42, 0.64);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-marker {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
flex: 0 0 38px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 38px;
|
|
|
|
|
height: 38px;
|
|
|
|
|
border: 1px solid rgba(101, 176, 208, 0.6);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
color: #a6c4d9;
|
|
|
|
|
background: rgba(3, 25, 40, 0.8);
|
|
|
|
|
box-shadow: inset 0 0 12px rgba(38, 174, 233, 0.08);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-copy { display: grid; min-width: 0; gap: 4px; margin-left: 10px; }
|
|
|
|
|
.step-name { display: -webkit-box; overflow: hidden; color: inherit; font-size: 14px; font-weight: 800; line-height: 1.25; text-overflow: ellipsis; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
|
|
|
|
.step-action { color: #66b6d4; font-size: 10px; font-weight: 800; letter-spacing: 0.06em; line-height: 1; transition: color 0.2s ease; }
|
|
|
|
|
|
|
|
|
|
.step-tab:hover {
|
|
|
|
|
border-color: rgba(89, 223, 255, 0.82);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
background: rgba(8, 92, 131, 0.52);
|
|
|
|
|
box-shadow: 0 7px 18px rgba(0, 0, 0, 0.24), inset 0 1px 0 rgba(152, 239, 255, 0.16);
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-tab:hover .step-action { color: #9bf4ff; }
|
|
|
|
|
.step-tab:focus-visible { outline: 2px solid #72e8ff; outline-offset: 4px; }
|
|
|
|
|
|
|
|
|
|
.step-tab.active .step-marker {
|
|
|
|
|
border-color: #72e8ff;
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
background: linear-gradient(135deg, #08b7e8, #1375d0);
|
|
|
|
|
box-shadow: 0 0 0 5px rgba(25, 179, 237, 0.14), 0 0 18px rgba(46, 208, 255, 0.58);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-tab.complete { color: #dffbff; }
|
|
|
|
|
.step-tab.complete .step-marker {
|
|
|
|
|
border-color: rgba(84, 230, 221, 0.85);
|
|
|
|
|
color: #062b37;
|
|
|
|
|
background: #6ef0e1;
|
|
|
|
|
box-shadow: 0 0 12px rgba(79, 231, 218, 0.35);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-connector {
|
|
|
|
|
position: relative;
|
|
|
|
|
flex: 0 1 40px;
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
height: 2px;
|
|
|
|
|
margin: 0 8px;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
background: rgba(98, 169, 201, 0.34);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-connector::after {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
right: -1px;
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
border-top: 1px solid #73b1ce;
|
|
|
|
|
border-right: 1px solid #73b1ce;
|
|
|
|
|
content: '';
|
|
|
|
|
transform: translateY(-50%) rotate(45deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-tab.complete + .step-connector {
|
|
|
|
|
background: linear-gradient(90deg, #6ef0e1, #1cb8e9);
|
|
|
|
|
box-shadow: 0 0 9px rgba(53, 218, 235, 0.46);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-tab.complete + .step-connector::after { border-color: #63e6f1; }
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
.step-indicator {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 0 0 32px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
overflow-x: visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-tab {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-connector {
|
|
|
|
|
flex: 0 0 20px;
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
width: 2px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
height: 20px;
|
|
|
|
|
margin: 4px 0 4px 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.step-connector::after {
|
|
|
|
|
top: auto;
|
|
|
|
|
right: auto;
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%) rotate(135deg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|