| .gitignore | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/router/index.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/BomDemo.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/CombinedDemo.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/FactoryDemo.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/ProcessRouteDemo.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/components/DemoControls.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/components/ProcessNode.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/components/TreeNode.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/teachingDemo/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
.gitignore
@@ -21,5 +21,8 @@ *.sln *.local # Claude Code .claude/ package-lock.json yarn.lock src/router/index.js
@@ -97,6 +97,44 @@ }, ], }, // æå¦æ¼ç¤ºæ¨¡å { path: "/productionManagement/teachingDemo", component: Layout, hidden: true, children: [ { path: "", component: () => import("@/views/productionManagement/teachingDemo/index.vue"), name: "TeachingDemo", meta: { title: "å·¥èºè·¯çº¿ä¸BOMæå¦æ¼ç¤º", icon: "education" }, }, { path: "bom", component: () => import("@/views/productionManagement/teachingDemo/BomDemo.vue"), name: "BomDemo", meta: { title: "BOMç»ææ¼ç¤º", activeMenu: "/productionManagement/teachingDemo" }, }, { path: "processRoute", component: () => import("@/views/productionManagement/teachingDemo/ProcessRouteDemo.vue"), name: "ProcessRouteDemo", meta: { title: "å·¥èºè·¯çº¿æ¼ç¤º", activeMenu: "/productionManagement/teachingDemo" }, }, { path: "combined", component: () => import("@/views/productionManagement/teachingDemo/CombinedDemo.vue"), name: "CombinedDemo", meta: { title: "è卿¼ç¤º", activeMenu: "/productionManagement/teachingDemo" }, }, { path: "factory", component: () => import("@/views/productionManagement/teachingDemo/FactoryDemo.vue"), name: "FactoryDemo", meta: { title: "å¨æå·¥åæ¼ç¤º", activeMenu: "/productionManagement/teachingDemo" }, }, ], }, { path: "/user", component: Layout, src/views/productionManagement/teachingDemo/BomDemo.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,430 @@ <template> <div class="app-container bom-demo"> <PageHeader :content="`BOMç»ææ¼ç¤º - ${bomInfo.productName || ''}`"> <template #right-button> <el-button @click="goBack">è¿å</el-button> </template> </PageHeader> <DemoControls ref="controlsRef" :steps="demoSteps" :explanations="demoExplanations" @play="handlePlay" @pause="handlePause" @reset="handleReset" @stepChange="handleStepChange" @speedChange="handleSpeedChange" /> <div class="demo-container" v-loading="loading"> <div class="bom-visualization"> <div class="tree-container" v-if="bomData.length > 0"> <TreeNode v-for="(item, index) in bomData" :key="item.tempId || item.id || index" :node="item" :level="0" :delay="0" :animation-speed="animationSpeed" :auto-expand="autoExpand" :show-lines="true" :highlight-ids="highlightIds" :active-id="activeNodeId" :animating="isAnimating" @node-click="handleNodeClick" @expand="handleExpand" /> </div> <el-empty v-else description="ææ BOMæ°æ®" /> <div class="visualization-sidebar"> <el-card class="info-card" shadow="hover"> <template #header> <span>å½åèç¹ä¿¡æ¯</span> </template> <el-descriptions :column="1" border size="small" v-if="activeNode"> <el-descriptions-item label="产ååç§°">{{ activeNode.productName }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åå·">{{ activeNode.model || '-' }}</el-descriptions-item> <el-descriptions-item label="åä½ç¨é">{{ activeNode.unitQuantity || 1 }}</el-descriptions-item> <el-descriptions-item label="åä½">{{ activeNode.unit || '-' }}</el-descriptions-item> <el-descriptions-item label="æ¶èå·¥åº">{{ activeNode.processName || '-' }}</el-descriptions-item> <el-descriptions-item label="å±çº§æ·±åº¦">{{ getNodeDepth(activeNode) }}</el-descriptions-item> <el-descriptions-item label="å项æ°é">{{ activeNode.children?.length || 0 }}</el-descriptions-item> </el-descriptions> <div v-else class="empty-info">ç¹å»èç¹æ¥ç详ç»ä¿¡æ¯</div> </el-card> <el-card class="legend-card" shadow="hover"> <template #header> <span>å¾ä¾è¯´æ</span> </template> <div class="legend-item"> <span class="legend-icon root"></span> <span class="legend-text">æ ¹èç¹ï¼æåï¼</span> </div> <div class="legend-item"> <span class="legend-icon child"></span> <span class="legend-text">åèç¹ï¼é¶é¨ä»¶ï¼</span> </div> <div class="legend-item"> <span class="legend-icon leaf"></span> <span class="legend-text">å¶åèç¹ï¼åææï¼</span> </div> <div class="legend-item"> <span class="legend-icon highlight"></span> <span class="legend-text">é«äº®ç¶æ</span> </div> <div class="legend-item"> <span class="legend-icon active"></span> <span class="legend-text">éä¸ç¶æ</span> </div> </el-card> </div> </div> </div> </div> </template> <script setup> import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue' import { useRouter, useRoute } from 'vue-router' import TreeNode from './components/TreeNode.vue' import DemoControls from './components/DemoControls.vue' import { queryList } from '@/api/productionManagement/productStructure.js' const router = useRouter() const route = useRoute() const loading = ref(false) const bomData = ref([]) const controlsRef = ref(null) const animationSpeed = ref(1) const autoExpand = ref(false) const isAnimating = ref(false) const highlightIds = ref([]) const activeNodeId = ref(null) const activeNode = ref(null) const playTimer = ref(null) const bomId = computed(() => route.query.id) const bomInfo = computed(() => ({ bomNo: route.query.bomNo || '', productName: route.query.productName || '', productModelName: route.query.productModelName || '' })) const demoSteps = [ { title: 'æ ¹èç¹', description: 'å±ç¤ºæå' }, { title: 'å±å¼å项', description: 'éå±å±å¼' }, { title: 'ç©æç»æ', description: 'æ¾ç¤ºå ³ç³»' }, { title: 'ç¨éä¿¡æ¯', description: 'æ°éå ³ç³»' }, { title: 'å·¥åºå ³è', description: 'æ¶èå·¥åº' } ] const demoExplanations = [ { title: '第1æ¥ï¼è®¤è¯æåæ ¹èç¹', content: 'BOMçæ ¹èç¹ä»£è¡¨æç»æåãè¿éæ¯æä»¬è¦ç产ç产åï¼ææåèç¹é½æ¯ä¸ºäºç产è¿ä¸ªæåæéè¦çç©æã' }, { title: '第2æ¥ï¼å±å¼æ¥çå项', content: 'ç¹å»èç¹å¯ä»¥å±å¼æ¥çå ¶å项ãåé¡¹ä»£è¡¨ç»æç¶é¡¹çé¶é¨ä»¶æåææãBOMæ¯ä¸ä¸ªæ å½¢ç»æï¼ä½ç°äºäº§åçç»æå±æ¬¡ã' }, { title: '第3æ¥ï¼çè§£ç©æç»æå ³ç³»', content: 'è¿çº¿è¡¨ç¤ºç¶åå ³ç³»ãæ¯ä¸ªåèç¹é½æ¯å ¶ç¶èç¹çç»æé¨åãä¾å¦ï¼ä¸ä¸ªçµèç±ä¸»æ¿ãCPUãå åçç»æã' }, { title: '第4æ¥ï¼æ¥çç¨éä¿¡æ¯', content: 'æ¯ä¸ªèç¹ä¸çæ°å表示"åä½äº§åºæéæ°é"ï¼å³ç产ä¸ä¸ªç¶é¡¹éè¦å¤å°å项ãä¾å¦ï¼ç产ä¸å°çµèéè¦1å主æ¿ã' }, { title: '第5æ¥ï¼äºè§£å·¥åºå ³è', content: 'åèç¹ä¸çå·¥åºåç§°è¡¨ç¤ºè¯¥ç©æå¨åªéå·¥åºä¸è¢«æ¶èãè¿è¿æ¥äºBOMä¸å·¥èºè·¯çº¿ï¼è¯´æç©æå¨ä½æ¶è¢«ä½¿ç¨ã' } ] const fetchBomData = async () => { if (!bomId.value) return loading.value = true try { const res = await queryList(bomId.value) bomData.value = res?.data || [] normalizeBomData(bomData.value) } catch (error) { console.error('è·åBOMæ°æ®å¤±è´¥:', error) } finally { loading.value = false } } const normalizeBomData = (data) => { data.forEach(item => { item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}` if (item.children && item.children.length > 0) { normalizeBomData(item.children) } }) } const goBack = () => { router.push('/productionManagement/teachingDemo') } const handleNodeClick = (node) => { activeNode.value = node activeNodeId.value = node.tempId || node.id highlightIds.value = getRelatedIds(node) } const handleExpand = (node, expanded) => { // å¤çå±å¼äºä»¶ } const getRelatedIds = (node) => { const ids = [] // æ·»å ææç¶èç¹ID const addParentIds = (currentNode, targetNode) => { if (currentNode.children) { for (const child of currentNode.children) { if (child.tempId === targetNode.tempId || child.id === targetNode.id) { ids.push(currentNode.tempId || currentNode.id) return true } if (addParentIds(child, targetNode)) { ids.push(currentNode.tempId || currentNode.id) return true } } } return false } for (const root of bomData.value) { addParentIds(root, node) } // æ·»å ææåèç¹ID const addChildIds = (currentNode) => { ids.push(currentNode.tempId || currentNode.id) if (currentNode.children) { for (const child of currentNode.children) { addChildIds(child) } } } if (node.children) { for (const child of node.children) { addChildIds(child) } } return ids } const getNodeDepth = (node) => { let depth = 0 const findDepth = (currentNode, targetNode, currentDepth) => { if (currentNode.tempId === targetNode.tempId || currentNode.id === targetNode.id) { depth = currentDepth return true } if (currentNode.children) { for (const child of currentNode.children) { if (findDepth(child, targetNode, currentDepth + 1)) { return true } } } return false } for (const root of bomData.value) { findDepth(root, node, 0) } return depth } const handlePlay = () => { isAnimating.value = true autoExpand.value = true runAnimation() } const handlePause = () => { isAnimating.value = false autoExpand.value = false if (playTimer.value) { clearTimeout(playTimer.value) } } const handleReset = () => { isAnimating.value = false autoExpand.value = false activeNodeId.value = null activeNode.value = null highlightIds.value = [] if (playTimer.value) { clearTimeout(playTimer.value) } } const handleStepChange = (step) => { // æ ¹æ®æ¥éª¤æ§è¡ç¸åºæä½ switch (step) { case 0: // å±ç¤ºæ ¹èç¹ if (bomData.value.length > 0) { activeNode.value = bomData.value[0] activeNodeId.value = bomData.value[0].tempId || bomData.value[0].id } break case 1: // å±å¼å项 autoExpand.value = true break case 2: // æ¾ç¤ºå ³ç³» if (activeNode.value) { highlightIds.value = getRelatedIds(activeNode.value) } break case 3: // ç¨éä¿¡æ¯ break case 4: // å·¥åºå ³è break } } const handleSpeedChange = (speed) => { animationSpeed.value = speed } const runAnimation = () => { if (!isAnimating.value) return const totalSteps = demoSteps.length let currentStep = controlsRef.value?.currentStep || 0 const animate = () => { if (!isAnimating.value) return if (currentStep < totalSteps) { controlsRef.value?.setStep(currentStep) handleStepChange(currentStep) currentStep++ playTimer.value = setTimeout(animate, 3000 / animationSpeed.value) } else { isAnimating.value = false controlsRef.value?.setIsPlaying(false) } } animate() } onMounted(() => { fetchBomData() }) onUnmounted(() => { if (playTimer.value) { clearTimeout(playTimer.value) } }) </script> <style scoped lang="scss"> .bom-demo { .demo-container { background: #f5f7fa; padding: 20px; border-radius: 12px; min-height: 400px; .bom-visualization { display: flex; gap: 20px; .tree-container { flex: 1; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); overflow: auto; max-height: 600px; } .visualization-sidebar { width: 300px; display: flex; flex-direction: column; gap: 16px; .info-card { .empty-info { color: #909399; text-align: center; padding: 20px; } } .legend-card { .legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; .legend-icon { width: 16px; height: 16px; border-radius: 4px; border: 2px solid; &.root { background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); border-color: #409eff; } &.child { background: #fff; border-color: #e4e7ed; } &.leaf { background: #fff; border-color: #67c23a; } &.highlight { background: #fff; border-color: #e6a23c; box-shadow: 0 0 8px rgba(230, 162, 60, 0.4); } &.active { background: #fff; border-color: #409eff; box-shadow: 0 0 12px rgba(64, 158, 255, 0.5); } } .legend-text { font-size: 12px; color: #666; } } } } } } } </style> src/views/productionManagement/teachingDemo/CombinedDemo.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,518 @@ <template> <div class="app-container combined-demo"> <PageHeader content="BOMä¸å·¥èºè·¯çº¿è卿¼ç¤º"> <template #right-button> <el-button @click="goBack">è¿å</el-button> </template> </PageHeader> <DemoControls ref="controlsRef" :steps="demoSteps" :explanations="demoExplanations" @play="handlePlay" @pause="handlePause" @reset="handleReset" @stepChange="handleStepChange" @speedChange="handleSpeedChange" /> <div class="selection-panel" v-if="!showDemo"> <el-card shadow="hover"> <el-form label-width="100px"> <el-form-item label="éæ©BOM"> <el-select v-model="selectedBomId" placeholder="è¯·éæ©BOM" filterable style="width: 100%"> <el-option v-for="item in bomList" :key="item.id" :label="`${item.bomNo} - ${item.productName}`" :value="item.id" /> </el-select> </el-form-item> <el-form-item label="鿩工èºè·¯çº¿"> <el-select v-model="selectedRouteId" placeholder="è¯·éæ©å·¥èºè·¯çº¿" filterable style="width: 100%"> <el-option v-for="item in routeList" :key="item.id" :label="`${item.processRouteCode} - ${item.productName}`" :value="item.id" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" :disabled="!selectedBomId || !selectedRouteId" @click="startDemo"> å¼å§æ¼ç¤º </el-button> </el-form-item> </el-form> </el-card> </div> <div class="demo-container" v-loading="loading" v-if="showDemo"> <div class="combined-visualization"> <div class="bom-panel"> <div class="panel-header"> <h4>BOM ç©æç»æ</h4> <el-button size="small" @click="toggleBomExpand">{{ bomExpanded ? 'å ¨é¨æå ' : 'å ¨é¨å±å¼' }}</el-button> </div> <div class="tree-container"> <TreeNode v-for="(item, index) in bomData" :key="item.tempId || item.id || index" :node="item" :level="0" :delay="0" :animation-speed="animationSpeed" :auto-expand="bomExpanded" :show-lines="true" :highlight-ids="highlightBomIds" :active-id="activeBomId" :animating="isAnimating" @node-click="handleBomClick" /> </div> </div> <div class="connection-panel"> <svg class="connection-svg" ref="connectionSvg"> <!-- 卿ç»å¶è¿æ¥çº¿ --> </svg> <div class="connection-info" v-if="connectionInfo"> <el-tag type="success" effect="plain"> {{ connectionInfo }} </el-tag> </div> </div> <div class="route-panel"> <div class="panel-header"> <h4>å·¥èºè·¯çº¿æµç¨</h4> </div> <div class="process-container"> <ProcessNode v-for="(process, index) in processList" :key="process.id || index" :process="process" :index="index" :delay="index * 200" :animation-speed="animationSpeed" :active="activeProcessId === process.id" :completed="completedProcesses.includes(process.id)" :current="currentProcessId === process.id" :animating="isAnimating && currentProcessId === process.id" :show-arrow="index < processList.length - 1" @click="handleProcessClick" /> </div> </div> </div> <div class="demo-explanation"> <el-card shadow="hover"> <div class="explanation-content"> <h5>èå¨è¯´æ</h5> <p>ç¹å»å³ä¾§çå·¥åºèç¹ï¼å·¦ä¾§ä¼é«äº®æ¾ç¤ºè¯¥å·¥åºæ¶èçç©æã</p> <p>ç¹å»å·¦ä¾§çç©æèç¹ï¼å³ä¾§ä¼é«äº®æ¾ç¤ºæ¶èè¯¥ç©æçå·¥åºã</p> <p>绿è²è¿çº¿è¡¨ç¤ºç©æå¨å¯¹åºå·¥åºä¸è¢«æ¶èçå ³ç³»ã</p> </div> </el-card> </div> </div> </div> </template> <script setup> import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { useRouter, useRoute } from 'vue-router' import TreeNode from './components/TreeNode.vue' import ProcessNode from './components/ProcessNode.vue' import DemoControls from './components/DemoControls.vue' import { listPage as getBomList } from '@/api/productionManagement/productBom.js' import { listPage as getRouteList } from '@/api/productionManagement/processRoute.js' import { queryList } from '@/api/productionManagement/productStructure.js' import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js' const router = useRouter() const route = useRoute() const loading = ref(false) const showDemo = ref(false) const bomList = ref([]) const routeList = ref([]) const selectedBomId = ref(null) const selectedRouteId = ref(null) const bomData = ref([]) const processList = ref([]) const controlsRef = ref(null) const animationSpeed = ref(1) const isAnimating = ref(false) const bomExpanded = ref(true) const highlightBomIds = ref([]) const activeBomId = ref(null) const activeProcessId = ref(null) const activeProcess = ref(null) const completedProcesses = ref([]) const currentProcessId = ref(null) const connectionInfo = ref('') const playTimer = ref(null) const demoSteps = [ { title: 'BOMç»æ', description: 'æ¥çç©æç»æ' }, { title: 'å·¥èºè·¯çº¿', description: 'æ¥çå·¥åºæµç¨' }, { title: 'ç©ææ¶è', description: 'å ³èå ³ç³»' }, { title: 'ç产è¿ç¨', description: 'æ¨¡ææµç¨' }, { title: 'è卿¼ç¤º', description: '交äºä½éª' } ] const demoExplanations = [ { title: '第1æ¥ï¼è®¤è¯BOMç»æ', content: '左侧å±ç¤ºäºäº§åçBOMç»æï¼æ 形徿¾ç¤ºäºäº§åç±åªäºç©æç»æãæ ¹èç¹æ¯æåï¼åèç¹æ¯ç»ææåçé¶é¨ä»¶ååææã' }, { title: '第2æ¥ï¼äºè§£å·¥èºè·¯çº¿', content: 'å³ä¾§å±ç¤ºäºå·¥èºè·¯çº¿ï¼æ¨ªåæµç¨å¾æ¾ç¤ºäºäº§åéè¦ç»è¿åªäºå·¥åºãæ¯ä¸ªå·¥åºèç¹ä»£è¡¨ä¸ä¸ªç¬ç«çå å·¥æ¥éª¤ã' }, { title: '第3æ¥ï¼çè§£ç©ææ¶èå ³ç³»', content: 'BOMä¸çæ¯ä¸ªç©æèç¹é½æä¸ä¸ª"æ¶èå·¥åº"屿§ï¼è¡¨ç¤ºè¯¥ç©æå¨åªéå·¥åºä¸è¢«ä½¿ç¨ãè¿æ¯BOMä¸å·¥èºè·¯çº¿çæ ¸å¿å ³èã' }, { title: '第4æ¥ï¼è§çç产è¿ç¨æ¨¡æ', content: 'ç¹å»ææ¾å¯ä»¥è§ç产åå¨åå·¥åºé´æµè½¬çè¿ç¨ãéçå·¥åºçè¿è¡ï¼BOMä¸çç©æè¢«éæ¥æ¶èã' }, { title: '第5æ¥ï¼ä½éªèå¨äº¤äº', content: 'ç¹å»å·¥åºèç¹ï¼BOMä¸å¯¹åºç©æä¼é«äº®ï¼ç¹å»ç©æèç¹ï¼å¯¹åºå·¥åºä¼é«äº®ãè¿ç§èå¨å¸®å©çè§£ç©æå¨ä½æ¶ä½å°è¢«æ¶èã' } ] const fetchBomList = async () => { try { const res = await getBomList({ current: 1, size: 100 }) bomList.value = res?.data?.records || [] // å¦æè·¯ç±æé¢è®¾bomIdï¼èªå¨éæ© if (route.query.bomId) { selectedBomId.value = Number(route.query.bomId) } } catch (error) { console.error('è·åBOMå表失败:', error) } } const fetchRouteList = async () => { try { const res = await getRouteList({ current: 1, size: 100 }) routeList.value = res?.data?.records || [] // å¦æè·¯ç±æé¢è®¾routeIdï¼èªå¨éæ© if (route.query.routeId) { selectedRouteId.value = Number(route.query.routeId) } } catch (error) { console.error('è·åå·¥èºè·¯çº¿å表失败:', error) } } const fetchBomData = async () => { if (!selectedBomId.value) return try { const res = await queryList(selectedBomId.value) bomData.value = res?.data || [] normalizeBomData(bomData.value) } catch (error) { console.error('è·åBOMæ°æ®å¤±è´¥:', error) } } const fetchProcessList = async () => { if (!selectedRouteId.value) return try { const res = await findProcessRouteItemList({ routeId: selectedRouteId.value }) processList.value = res?.data || [] processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0)) } catch (error) { console.error('è·åå·¥èºè·¯çº¿æ°æ®å¤±è´¥:', error) } } const normalizeBomData = (data) => { data.forEach(item => { item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}` if (item.children && item.children.length > 0) { normalizeBomData(item.children) } }) } const startDemo = async () => { loading.value = true try { await Promise.all([fetchBomData(), fetchProcessList()]) showDemo.value = true } finally { loading.value = false } } const goBack = () => { router.push('/productionManagement/teachingDemo') } const toggleBomExpand = () => { bomExpanded.value = !bomExpanded.value } const handleBomClick = (node) => { activeBomId.value = node.tempId || node.id // æ¾å°æ¶èè¯¥ç©æçå·¥åº const processName = node.processName if (processName) { const matchingProcess = processList.value.find(p => p.technologyOperationName === processName || p.operationName === processName ) if (matchingProcess) { activeProcessId.value = matchingProcess.id connectionInfo.value = `${node.productName} å¨ "${processName}" å·¥åºä¸æ¶è` } } } const handleProcessClick = (process) => { activeProcess.value = process activeProcessId.value = process.id // æ¾å°è¯¥å·¥åºæ¶èçææç©æ const processName = process.technologyOperationName || process.operationName highlightBomIds.value = findBomIdsByProcess(processName) if (highlightBomIds.value.length > 0) { connectionInfo.value = `"${processName}" å·¥åºæ¶èäº ${highlightBomIds.value.length} ç§ç©æ` } } const findBomIdsByProcess = (processName) => { const ids = [] const traverse = (nodes) => { for (const node of nodes) { if (node.processName === processName) { ids.push(node.tempId || node.id) } if (node.children) { traverse(node.children) } } } traverse(bomData.value) return ids } const handlePlay = () => { isAnimating.value = true bomExpanded.value = true runAnimation() } const handlePause = () => { isAnimating.value = false if (playTimer.value) { clearTimeout(playTimer.value) } } const handleReset = () => { isAnimating.value = false highlightBomIds.value = [] activeBomId.value = null activeProcessId.value = null activeProcess.value = null currentProcessId.value = null completedProcesses.value = [] connectionInfo.value = '' if (playTimer.value) { clearTimeout(playTimer.value) } } const handleStepChange = (step) => { switch (step) { case 0: // BOMç»æ break case 1: // å·¥èºè·¯çº¿ break case 2: // ç©ææ¶è if (processList.value.length > 0) { const firstProcess = processList.value[0] handleProcessClick(firstProcess) } break case 3: // ç产è¿ç¨æ¨¡æ simulateProduction() break case 4: // è卿¼ç¤º break } } const handleSpeedChange = (speed) => { animationSpeed.value = speed } const simulateProduction = () => { completedProcesses.value = [] currentProcessId.value = null let index = 0 const interval = 1500 / animationSpeed.value playTimer.value = setInterval(() => { if (index >= processList.value.length) { clearInterval(playTimer.value) currentProcessId.value = null isAnimating.value = false return } if (index > 0) { completedProcesses.value.push(processList.value[index - 1].id) } currentProcessId.value = processList.value[index].id activeProcessId.value = processList.value[index].id // é«äº®å¯¹åºç©æ const processName = processList.value[index].technologyOperationName || processList.value[index].operationName highlightBomIds.value = findBomIdsByProcess(processName) connectionInfo.value = `æ£å¨æ§è¡ "${processName}" å·¥åº` index++ }, interval) } const runAnimation = () => { if (!isAnimating.value) return const totalSteps = demoSteps.length let currentStep = controlsRef.value?.currentStep || 0 const animate = () => { if (!isAnimating.value) return if (currentStep < totalSteps) { controlsRef.value?.setStep(currentStep) handleStepChange(currentStep) currentStep++ playTimer.value = setTimeout(animate, 4000 / animationSpeed.value) } else { isAnimating.value = false controlsRef.value?.setIsPlaying(false) } } animate() } onMounted(() => { fetchBomList() fetchRouteList() // 妿é¢è®¾äºæ°æ®ï¼èªå¨å¼å§æ¼ç¤º if (route.query.bomId && route.query.routeId) { nextTick(() => { startDemo() }) } }) onUnmounted(() => { if (playTimer.value) { clearTimeout(playTimer.value) } }) </script> <style scoped lang="scss"> .combined-demo { .selection-panel { margin-bottom: 20px; } .demo-container { background: #f5f7fa; padding: 20px; border-radius: 12px; min-height: 400px; .combined-visualization { display: flex; gap: 16px; min-height: 300px; .bom-panel, .route-panel { flex: 1; background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; h4 { color: #303133; font-size: 16px; font-weight: 600; margin: 0; } } .tree-container, .process-container { overflow: auto; max-height: 400px; } .process-container { display: flex; align-items: center; gap: 0; padding: 10px 0; } } .connection-panel { width: 200px; background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); display: flex; flex-direction: column; justify-content: center; align-items: center; .connection-svg { width: 100%; height: 100px; } .connection-info { text-align: center; margin-top: 16px; } } } .demo-explanation { margin-top: 20px; .explanation-content { h5 { color: #409eff; margin-bottom: 12px; } p { color: #666; line-height: 1.8; margin-bottom: 8px; } } } } } </style> src/views/productionManagement/teachingDemo/FactoryDemo.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,1153 @@ <template> <div class="app-container factory-demo"> <PageHeader content="卿工åç产线æ¼ç¤º"> <template #right-button> <el-button @click="goBack">è¿å</el-button> </template> </PageHeader> <div class="control-panel"> <el-row :gutter="20" align="middle"> <el-col :span="6"> <el-select v-model="selectedRouteId" placeholder="鿩工èºè·¯çº¿" filterable style="width: 100%" @change="handleRouteChange"> <el-option v-for="item in routeList" :key="item.id" :label="`${item.processRouteCode} - ${item.productName}`" :value="item.id" /> </el-select> </el-col> <el-col :span="6"> <el-button-group> <el-button :type="isPlaying ? 'warning' : 'primary'" @click="togglePlay"> <el-icon v-if="isPlaying"><VideoPause /></el-icon> <el-icon v-else><VideoPlay /></el-icon> {{ isPlaying ? 'æå' : 'ææ¾' }} </el-button> <el-button @click="handleReset"> <el-icon><RefreshRight /></el-icon> éç½® </el-button> </el-button-group> </el-col> <el-col :span="6"> <div class="speed-control"> <span>é度ï¼</span> <el-radio-group v-model="speed" size="small"> <el-radio-button label="0.5">æ ¢</el-radio-button> <el-radio-button label="1">æ£å¸¸</el-radio-button> <el-radio-button label="2">å¿«</el-radio-button> </el-radio-group> </div> </el-col> <el-col :span="6"> <div class="progress-info"> <span>è¿åº¦ï¼</span> <el-progress :percentage="productionProgress" :stroke-width="8" style="width: 120px" /> </div> </el-col> </el-row> </div> <div class="factory-container" v-loading="loading"> <div class="factory-scene"> <svg class="factory-svg" viewBox="0 0 1100 500" preserveAspectRatio="xMidYMid meet"> <defs> <!-- æ¸åå®ä¹ --> <linearGradient id="beltGradient" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="#409eff" stop-opacity="0.8" /> <stop offset="50%" stop-color="#66b1ff" stop-opacity="0.6" /> <stop offset="100%" stop-color="#409eff" stop-opacity="0.8" /> </linearGradient> <linearGradient id="stationGradient" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" stop-color="#f8f9fa" /> <stop offset="100%" stop-color="#e9ecef" /> </linearGradient> <linearGradient id="activeGradient" x1="0%" y1="0%" x2="0%" y2="100%"> <stop offset="0%" stop-color="#ecf5ff" /> <stop offset="100%" stop-color="#d9ecff" /> </linearGradient> <filter id="stationShadow" x="-20%" y="-20%" width="140%" height="140%"> <feDropShadow dx="0" dy="4" stdDeviation="6" flood-color="#000" flood-opacity="0.15" /> </filter> <filter id="glowEffect" x="-50%" y="-50%" width="200%" height="200%"> <feGaussianBlur stdDeviation="4" result="blur" /> <feMerge> <feMergeNode in="blur" /> <feMergeNode in="SourceGraphic" /> </feMerge> </filter> </defs> <!-- èæ¯å°æ¿ --> <rect x="0" y="380" width="1100" height="120" fill="#f5f7fa" /> <rect x="0" y="380" width="1100" height="2" fill="#e4e7ed" /> <!-- 履带 --> <g class="conveyor-system"> <!-- å±¥å¸¦ä¸»ä½ --> <rect x="40" y="320" width="1020" height="60" rx="8" fill="url(#beltGradient)" opacity="0.3" /> <!-- å±¥å¸¦è¾¹ç¼ --> <rect x="40" y="320" width="1020" height="4" rx="2" fill="#409eff" opacity="0.5" /> <rect x="40" y="376" width="1020" height="4" rx="2" fill="#409eff" opacity="0.5" /> <!-- æµå¨çº¿æ¡ --> <path class="belt-flow" d="M50 350 H1050" stroke="#fff" stroke-width="3" stroke-dasharray="15 20" stroke-linecap="round" opacity="0.8" /> <!-- 履带æ»è½® --> <circle v-for="i in 12" :key="'roller'+i" :cx="50 + i * 85" cy="350" r="8" fill="#fff" opacity="0.6" /> </g> <!-- å ¥åºåº --> <g class="input-area" @click="showInputDialog"> <rect x="20" y="200" width="80" height="80" rx="12" fill="#f0f9eb" stroke="#67c23a" stroke-width="2" filter="url(#stationShadow)" /> <text x="60" y="245" text-anchor="middle" font-size="14" fill="#67c23a" font-weight="600">åææ</text> <text x="60" y="265" text-anchor="middle" font-size="12" fill="#67c23a">å ¥åº</text> <!-- å ¥åºå¾æ --> <g transform="translate(45, 215)"> <rect width="30" height="20" rx="4" fill="#67c23a" opacity="0.8" /> <path d="M15 -8 L15 24" stroke="#67c23a" stroke-width="3" stroke-dasharray="4 4" /> </g> </g> <!-- åºåºåº --> <g class="output-area" @click="showOutputDialog"> <rect x="1000" y="200" width="80" height="80" rx="12" fill="#ecf5ff" stroke="#409eff" stroke-width="2" filter="url(#stationShadow)" /> <text x="1040" y="245" text-anchor="middle" font-size="14" fill="#409eff" font-weight="600">æå</text> <text x="1040" y="265" text-anchor="middle" font-size="12" fill="#409eff">åºåº</text> <!-- åºåºå¾æ --> <g transform="translate(1015, 215)"> <rect width="30" height="20" rx="4" fill="#409eff" opacity="0.8" /> <path d="M15 28 L15 0" stroke="#409eff" stroke-width="3" stroke-dasharray="4 4" /> <path d="M8 -5 L15 -12 L22 -5" stroke="#409eff" stroke-width="2" fill="none" /> </g> </g> <!-- å·¥åºå·¥ä½ç« --> <g v-for="(process, index) in processList" :key="process.id || index" class="work-station" :class="{ 'is-active': currentProcessIndex === index, 'is-completed': completedIndex > index }" :transform="`translate(${getStationX(index)}, 0)`" @click="showProcessDetail(process)" > <!-- æºå¨å¤å£³ --> <rect x="0" y="80" width="120" height="140" rx="16" :fill="currentProcessIndex === index ? 'url(#activeGradient)' : 'url(#stationGradient)'" :stroke="completedIndex > index ? '#67c23a' : currentProcessIndex === index ? '#409eff' : '#e4e7ed'" stroke-width="2" filter="url(#stationShadow)" /> <!-- å·¥åºåºå· --> <circle cx="60" cy="60" r="24" :fill="completedIndex > index ? '#67c23a' : currentProcessIndex === index ? '#409eff' : '#909399'" /> <text x="60" y="65" text-anchor="middle" font-size="14" fill="#fff" font-weight="bold">{{ index + 1 }}</text> <!-- å·¥åºåç§° --> <text x="60" y="110" text-anchor="middle" font-size="13" :fill="currentProcessIndex === index ? '#409eff' : '#303133'" font-weight="600" class="station-name"> {{ truncateText(process.technologyOperationName || process.operationName, 8) }} </text> <!-- 产åä¿¡æ¯ --> <text x="60" y="130" text-anchor="middle" font-size="10" fill="#909399"> {{ truncateText(process.productName, 10) || 'æ 产å' }} </text> <!-- ç¶ææ ç¾ --> <g transform="translate(20, 145)"> <rect width="80" height="20" rx="4" :fill="process.isQuality ? '#fdf6ec' : '#f5f7fa'" /> <text x="40" y="14" text-anchor="middle" font-size="10" :fill="process.isQuality ? '#e6a23c' : '#909399'"> {{ process.isQuality ? 'è´¨æ£ç¹' : 'å å·¥' }} </text> </g> <!-- ææå£ --> <g class="feed-point" :transform="`translate(50, 218)`"> <circle r="10" fill="#fff" stroke="#409eff" stroke-width="2" :class="{ 'is-feeding': feedingIndex === index }" /> <text y="4" text-anchor="middle" font-size="10" fill="#409eff">æ</text> </g> <!-- ç¶ææç¤ºç¯ --> <circle cx="110" cy="90" r="6" :fill="completedIndex > index ? '#67c23a' : currentProcessIndex === index ? '#e6a23c' : '#c0c4cc'" :class="{ 'is-pulsing': currentProcessIndex === index }" /> <!-- ææå¨ç»å°ç --> <g v-if="feedingIndex === index" class="feed-ball"> <circle cx="60" cy="200" r="8" fill="#67c23a"> <animate attributeName="cy" values="200;280" dur="0.3s" fill="freeze" /> <animate attributeName="opacity" values="1;0" dur="0.3s" fill="freeze" /> </circle> </g> </g> <!-- ç§»å¨ç产åç®±å --> <g class="product-boxes"> <g v-for="(box, bIndex) in visibleBoxes" :key="bIndex" class="product-box" :transform="`translate(${box.x}, ${box.y})`"> <!-- ç®±åä¸»ä½ - æ¾å¤§å°ºå¯¸ --> <rect x="0" y="0" width="70" height="45" rx="8" :fill="box.color" opacity="0.95" /> <rect x="0" y="15" width="70" height="3" fill="#fff" opacity="0.3" /> <!-- 产ååç§° --> <text x="35" y="35" text-anchor="middle" font-size="12" fill="#fff" font-weight="600">{{ box.label }}</text> <!-- æ¬åæç¤ºæ¡ --> <title>{{ box.fullName || box.label }}</title> </g> </g> <!-- ææå¨ç»ï¼åæä»å·¥åºèç¹è½ä¸å°äº§åä¸ --> <g v-if="showFeedingAnimation && feedingProcessIndex >= 0" class="feeding-animation"> <!-- æå ¥çåæå°ç --> <circle :cx="getStationX(feedingProcessIndex) + 60" :cy="feedingAnimY" r="12" fill="#67c23a" opacity="0.9" > <animate attributeName="cy" :values="`${feedingStartY};${feedingEndY}`" :dur="`${0.4 / parseFloat(speed)}s`" fill="freeze" /> <animate attributeName="opacity" values="1;0.6" :dur="`${0.4 / parseFloat(speed)}s`" fill="freeze" /> </circle> <!-- åæåç§° --> <text :x="getStationX(feedingProcessIndex) + 60" :y="feedingAnimY - 18" text-anchor="middle" font-size="10" fill="#67c23a" font-weight="600" opacity="0.9" > {{ truncateText(feedingMaterialName, 5) || 'åæ' }} </text> </g> <!-- 产åºå¨ç»ï¼å 工宿åçéªå ææ --> <g v-if="showOutputAnimation && outputProcessIndex >= 0" class="output-animation"> <circle :cx="getStationX(outputProcessIndex) + 60" cy="285" r="30" fill="none" stroke="#409eff" stroke-width="3" opacity="0.8" > <animate attributeName="r" values="20;40" dur="0.5s" fill="freeze" /> <animate attributeName="opacity" values="0.8;0" dur="0.5s" fill="freeze" /> </circle> <!-- 产åºåç§°æç¤º --> <text :x="getStationX(outputProcessIndex) + 60" y="340" text-anchor="middle" font-size="11" fill="#409eff" font-weight="600" > 产åº: {{ truncateText(outputProductName, 6) || '产å' }} </text> </g> <!-- è¿åº¦æç¤ºçº¿ --> <path class="progress-line" :d="getProgressPath()" stroke="#67c23a" stroke-width="3" fill="none" opacity="0.6" /> <!-- 说ææååºå --> <g class="info-text" v-if="currentProcess"> <rect x="200" y="10" width="300" height="50" rx="8" fill="#ecf5ff" stroke="#409eff" stroke-width="1" opacity="0.9" /> <text x="350" y="35" text-anchor="middle" font-size="14" fill="#409eff" font-weight="600"> å½åå·¥åºï¼{{ currentProcess.technologyOperationName || currentProcess.operationName }} </text> <text x="350" y="50" text-anchor="middle" font-size="11" fill="#606266"> {{ feedingIndex >= 0 ? 'æ£å¨ææ...' : 'äº§åæµè½¬ä¸...' }} </text> </g> </svg> </div> <div class="factory-sidebar"> <el-card class="info-card" shadow="hover"> <template #header> <span>çäº§çº¿ç¶æ</span> </template> <div class="status-info"> <div class="status-item"> <span class="status-label">å·¥åºæ»æ°</span> <span class="status-value">{{ processList.length }}</span> </div> <div class="status-item"> <span class="status-label">已宿</span> <span class="status-value">{{ completedIndex }} / {{ processList.length }}</span> </div> <div class="status-item"> <span class="status-label">å½åå·¥åº</span> <span class="status-value highlight">{{ currentProcess?.technologyOperationName || currentProcess?.operationName || '-' }}</span> </div> <div class="status-item"> <span class="status-label">çäº§ç¶æ</span> <el-tag :type="isPlaying ? 'success' : 'info'" size="small">{{ isPlaying ? 'è¿è¡ä¸' : 'å·²æå' }}</el-tag> </div> </div> </el-card> <el-card class="legend-card" shadow="hover"> <template #header> <span>ç¶æè¯´æ</span> </template> <div class="legend-item"> <span class="legend-dot completed"></span> <span class="legend-text">已宿</span> </div> <div class="legend-item"> <span class="legend-dot current"></span> <span class="legend-text">å½åå·¥åº</span> </div> <div class="legend-item"> <span class="legend-dot pending"></span> <span class="legend-text">å¾ å å·¥</span> </div> <div class="legend-item"> <span class="legend-dot quality"></span> <span class="legend-text">è´¨æ£ç¹</span> </div> </el-card> <el-card class="tips-card" shadow="hover"> <template #header> <span>æä½æç¤º</span> </template> <div class="tips-content"> <p>ç¹å»å·¥åºèç¹æ¥ç详ç»ä¿¡æ¯</p> <p>ç¹å»"åææå ¥åº"æ¥çæææ¸ å</p> <p>ç¹å»"æååºåº"æ¥ç产åºä¿¡æ¯</p> <p>ä½¿ç¨æ§å¶é¢æ¿è°èæ¼ç¤ºé度</p> </div> </el-card> </div> </div> <!-- å·¥åºè¯¦æ å¯¹è¯æ¡ --> <el-dialog v-model="processDialogVisible" :title="`å·¥åºè¯¦æ - ${selectedProcess?.technologyOperationName || selectedProcess?.operationName}`" width="650px"> <el-descriptions :column="2" border size="small" v-if="selectedProcess"> <el-descriptions-item label="å·¥åºåç§°">{{ selectedProcess.technologyOperationName || selectedProcess.operationName }}</el-descriptions-item> <el-descriptions-item label="å·¥åºåºå·">{{ selectedProcess.dragSort || selectedProcessIndex + 1 }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ selectedProcess.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åå·">{{ selectedProcess.model || '-' }}</el-descriptions-item> <el-descriptions-item label="åä½">{{ selectedProcess.unit || '-' }}</el-descriptions-item> <el-descriptions-item label="计费类å"> <el-tag :type="selectedProcess.type === 1 ? 'primary' : 'success'" size="small"> {{ selectedProcess.type === 1 ? '计件' : '计æ¶' }} </el-tag> </el-descriptions-item> <el-descriptions-item label="æ¯å¦è´¨æ£"> <el-tag :type="selectedProcess.isQuality ? 'warning' : 'info'" size="small">{{ selectedProcess.isQuality ? 'æ¯' : 'å¦' }}</el-tag> </el-descriptions-item> <el-descriptions-item label="æ¯å¦ç产"> <el-tag :type="selectedProcess.isProduction ? 'success' : 'info'" size="small">{{ selectedProcess.isProduction ? 'æ¯' : 'å¦' }}</el-tag> </el-descriptions-item> </el-descriptions> <!-- æå ¥ç©æ --> <div class="material-section" v-if="inputMaterials.length > 0"> <div class="section-header"> <el-icon style="color: #67c23a;"><Bottom /></el-icon> <h4>éè¦æå ¥çç©æï¼{{ inputMaterials.length }} ç§ï¼</h4> </div> <el-table :data="inputMaterials" border size="small" max-height="200"> <el-table-column prop="productName" label="ç©æåç§°" min-width="120" /> <el-table-column prop="model" label="è§æ ¼åå·" min-width="100" /> <el-table-column prop="unitQuantity" label="åä½ç¨é" width="90" align="right"> <template #default="{ row }"> <span class="quantity-value">{{ row.unitQuantity }}</span> </template> </el-table-column> <el-table-column prop="unit" label="åä½" width="70" align="center" /> </el-table> <div class="material-summary"> <el-tag type="success" size="small" effect="plain"> ç产1个åä½äº§åéè¦æå ¥ä»¥ä¸ {{ inputMaterials.length }} ç§ç©æ </el-tag> </div> </div> <div class="material-section empty-section" v-else> <el-empty description="è¯¥å·¥åºææ éè¦æå ¥çç©æ" :image-size="60" /> </div> <!-- 产åºäº§å --> <div class="material-section" v-if="outputProduct"> <div class="section-header"> <el-icon style="color: #409eff;"><Top /></el-icon> <h4>产åºäº§åï¼æ¬å·¥åºäº§åºï¼</h4> </div> <el-descriptions :column="2" border size="small"> <el-descriptions-item label="产ååç§°">{{ outputProduct.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åå·">{{ outputProduct.model || '-' }}</el-descriptions-item> <el-descriptions-item label="åä½">{{ outputProduct.unit || '-' }}</el-descriptions-item> <el-descriptions-item label="产åºç±»å">{{ selectedProcess?.isProduction ? 'ç产产åº' : 'å 工产åº' }}</el-descriptions-item> </el-descriptions> </div> <!-- ä¸ä¸æ¸¸å·¥åº --> <div class="material-section" v-if="selectedProcessIndex >= 0"> <div class="section-header"> <el-icon style="color: #909399;"><Sort /></el-icon> <h4>å·¥åºæµè½¬</h4> </div> <div class="process-flow"> <div class="flow-item" v-if="prevProcess"> <span class="flow-label">ä¸ä¸éå·¥åº</span> <el-tag type="info" size="small">{{ prevProcess.technologyOperationName || prevProcess.operationName }}</el-tag> </div> <div class="flow-item current"> <span class="flow-label">å½åå·¥åº</span> <el-tag type="primary">{{ selectedProcess?.technologyOperationName || selectedProcess?.operationName }}</el-tag> </div> <div class="flow-item" v-if="nextProcess"> <span class="flow-label">ä¸ä¸éå·¥åº</span> <el-tag type="info" size="small">{{ nextProcess.technologyOperationName || nextProcess.operationName }}</el-tag> </div> </div> </div> <template #footer> <el-button @click="processDialogVisible = false">å ³é</el-button> </template> </el-dialog> </div> </template> <script setup> import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { useRouter } from 'vue-router' import { VideoPlay, VideoPause, RefreshRight, Bottom, Top, Sort } from '@element-plus/icons-vue' import { listPage as getRouteList } from '@/api/productionManagement/processRoute.js' import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js' import { queryList } from '@/api/productionManagement/productStructure.js' const router = useRouter() const loading = ref(false) const routeList = ref([]) const selectedRouteId = ref(null) const selectedBomId = ref(null) const processList = ref([]) const bomData = ref([]) const isPlaying = ref(false) const speed = ref('1') const currentProcessIndex = ref(-1) const completedIndex = ref(0) const feedingIndex = ref(-1) const processDialogVisible = ref(false) const selectedProcess = ref(null) const selectedProcessIndex = ref(-1) const boxPosition = ref(0) const animationTimer = ref(null) const boxColors = ['#ffb15f', '#28d9cd', '#8b5cf6', '#409eff', '#67c23a'] // ææå¨ç»ç¸å ³ const showFeedingAnimation = ref(false) const feedingProcessIndex = ref(-1) const feedingMaterialName = ref('') const feedingAnimY = ref(200) const feedingStartY = 200 const feedingEndY = 285 // 产åºå¨ç»ç¸å ³ const showOutputAnimation = ref(false) const outputProcessIndex = ref(-1) const outputProductName = ref('') // ä¸ä¸æ¬¡å®æçå·¥åºç´¢å¼ï¼ç¨äºå¤ææ¯å¦éè¦è§¦å产åºå¨ç»ï¼ const lastCompletedIndex = ref(-1) const productionProgress = computed(() => { if (processList.value.length === 0) return 0 return Math.round((completedIndex.value / processList.value.length) * 100) }) const currentProcess = computed(() => { if (currentProcessIndex.value >= 0 && currentProcessIndex.value < processList.value.length) { return processList.value[currentProcessIndex.value] } return null }) const processMaterials = computed(() => { if (!selectedProcess.value) return [] const processName = selectedProcess.value.technologyOperationName || selectedProcess.value.operationName const materials = [] const traverse = (nodes) => { for (const node of nodes) { if (node.processName === processName) { materials.push(node) } if (node.children) traverse(node.children) } } traverse(bomData.value) return materials }) // æå ¥ç©æï¼éè¦æå ¥çç©æ - ä»BOMä¸å¹é å·¥åºï¼ const inputMaterials = computed(() => { if (!selectedProcess.value) return [] const processId = selectedProcess.value.technologyOperationId || selectedProcess.value.operationId const processName = selectedProcess.value.technologyOperationName || selectedProcess.value.operationName const materials = [] const traverse = (nodes) => { for (const node of nodes) { // ä¼å ç¨IDå¹é ï¼å ¶æ¬¡ç¨åç§°å¹é const nodeProcessId = node.processId || node.operationId || node.technologyOperationId const nodeProcessName = node.processName || node.operationName || node.technologyOperationName const isMatch = (processId && nodeProcessId === processId) || (processName && nodeProcessName === processName) if (isMatch) { materials.push({ productName: node.productName || 'æªå½åç©æ', model: node.model || '-', unitQuantity: node.unitQuantity || 1, unit: node.unit || '-', processName: nodeProcessName || '-' }) } if (node.children && node.children.length > 0) { traverse(node.children) } } } traverse(bomData.value) return materials }) // 产åºäº§åï¼å½åå·¥åºå ³èç产åä¿¡æ¯ï¼ const outputProduct = computed(() => { if (!selectedProcess.value) return null return { productName: selectedProcess.value.productName || null, model: selectedProcess.value.model || null, unit: selectedProcess.value.unit || null } }) // ä¸ä¸éå·¥åº const prevProcess = computed(() => { if (selectedProcessIndex.value <= 0) return null return processList.value[selectedProcessIndex.value - 1] }) // ä¸ä¸éå·¥åº const nextProcess = computed(() => { if (selectedProcessIndex.value < 0 || selectedProcessIndex.value >= processList.value.length - 1) return null return processList.value[selectedProcessIndex.value + 1] }) const visibleBoxes = computed(() => { const boxes = [] const baseX = 60 + boxPosition.value // 计ç®äº§åå½åæå¨çå·¥åºç´¢å¼ const totalWidth = 900 const progressRatio = Math.min(1, boxPosition.value / totalWidth) const currentStep = Math.floor(progressRatio * processList.value.length) // å½åå·¥åº const currentProc = processList.value[currentStep] || null // ä¸ä¸éå·¥åºï¼å·²å®æï¼ const prevProc = processList.value[currentStep - 1] || null // ç¡®å®äº§ååç§° let productLabel = 'åæ' let fullName = 'åææï¼å¾ å å·¥ï¼' if (currentStep > 0 && prevProc) { // å·²ç»è¿äºä¸äºå·¥åºï¼æ¾ç¤ºä¸ä¸ä¸ªå·¥åºçäº§åº productLabel = truncateText(prevProc.productName, 5) || 'åæå' fullName = prevProc.productName || 'åæå' } if (currentStep >= processList.value.length) { // å ¨é¨å®æ const lastProc = processList.value[processList.value.length - 1] productLabel = truncateText(lastProc?.productName, 5) || 'æå' fullName = lastProc?.productName || 'æå' } if (baseX > 40) { boxes.push({ x: baseX, y: 275, // è°æ´ä½ç½®ï¼è®©ç®±åå¨å±¥å¸¦ä¸ color: '#409eff', label: productLabel, fullName: fullName, processIndex: currentStep, isFeeding: feedingIndex.value === currentStep && currentStep < processList.value.length }) } return boxes }) // å½åå·¥åºçæå ¥ç©æåç§° const currentInputMaterial = computed(() => { if (currentProcessIndex.value < 0 || currentProcessIndex.value >= processList.value.length) return null const process = processList.value[currentProcessIndex.value] if (!process) return null const processId = process.technologyOperationId || process.operationId const processName = process.technologyOperationName || process.operationName // ä»BOM䏿¾ç¬¬ä¸ä¸ªå¹é çç©æ let material = null const traverse = (nodes) => { for (const node of nodes) { const nodeProcessId = node.processId || node.operationId || node.technologyOperationId const nodeProcessName = node.processName || node.operationName || node.technologyOperationName if ((processId && nodeProcessId === processId) || (processName && nodeProcessName === processName)) { material = node return true } if (node.children && node.children.length > 0) { if (traverse(node.children)) return true } } return false } traverse(bomData.value) return material }) const getStationX = (index) => { const totalStations = processList.value.length if (totalStations === 0) return 0 const startX = 140 const spacing = Math.min(180, (900 - startX) / totalStations) return startX + index * spacing } const truncateText = (text, maxLen) => { if (!text) return '' return text.length > maxLen ? text.substring(0, maxLen) + '..' : text } const getProgressPath = () => { if (completedIndex.value === 0) return '' const endX = getStationX(Math.min(completedIndex.value, processList.value.length) - 1) + 60 return `M60 350 L${endX} 350` } const fetchRouteList = async () => { try { const res = await getRouteList({ current: 1, size: 100 }) routeList.value = res?.data?.records || [] } catch (error) { console.error('è·åå·¥èºè·¯çº¿å表失败:', error) } } const fetchProcessList = async () => { if (!selectedRouteId.value) return loading.value = true try { const res = await findProcessRouteItemList({ routeId: selectedRouteId.value }) processList.value = res?.data || [] processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0)) // è·åå ³èçBOMæ°æ® const routeInfo = routeList.value.find(r => r.id === selectedRouteId.value) if (routeInfo?.bomId) { selectedBomId.value = routeInfo.bomId const bomRes = await queryList(routeInfo.bomId) bomData.value = bomRes?.data || [] } } catch (error) { console.error('è·åå·¥èºè·¯çº¿æ°æ®å¤±è´¥:', error) } finally { loading.value = false } } const handleRouteChange = () => { handleReset() fetchProcessList() } const togglePlay = () => { isPlaying.value = !isPlaying.value if (isPlaying.value) { startAnimation() } else { stopAnimation() } } const handleReset = () => { stopAnimation() isPlaying.value = false currentProcessIndex.value = -1 completedIndex.value = 0 feedingIndex.value = -1 boxPosition.value = 0 showFeedingAnimation.value = false showOutputAnimation.value = false feedingProcessIndex.value = -1 outputProcessIndex.value = -1 lastCompletedIndex.value = -1 } const startAnimation = () => { const speedFactor = parseFloat(speed.value) const interval = 2000 / speedFactor const moveStep = 20 / speedFactor animationTimer.value = setInterval(() => { if (!isPlaying.value) return // æ´æ°ç®±åä½ç½® boxPosition.value += moveStep // 计ç®å½ååºè¯¥å¨åªä¸ªå·¥åº const totalWidth = 900 const progressRatio = boxPosition.value / totalWidth const newIndex = Math.floor(progressRatio * processList.value.length) // è¿å ¥æ°å·¥åºæ¶è§¦åææå¨ç» if (newIndex !== currentProcessIndex.value && newIndex < processList.value.length) { // å 触å产åºå¨ç»ï¼ä¸ä¸éå·¥åºå®æï¼ if (currentProcessIndex.value >= 0 && currentProcessIndex.value < processList.value.length) { const prevProcess = processList.value[currentProcessIndex.value] triggerOutputAnimation(currentProcessIndex.value, prevProcess?.productName || '产å') } currentProcessIndex.value = newIndex // è§¦åææå¨ç» const process = processList.value[newIndex] const inputMaterial = getInputMaterialForProcess(process) triggerFeedingAnimation(newIndex, inputMaterial?.productName || 'åæ') feedingIndex.value = newIndex setTimeout(() => { feedingIndex.value = -1 }, 500 / speedFactor) } // 夿æ¯å¦å®æ if (boxPosition.value >= totalWidth) { // æåä¸ä¸ªå·¥åºå®æï¼è§¦å产åºå¨ç» if (currentProcessIndex.value >= 0 && currentProcessIndex.value < processList.value.length) { const lastProcess = processList.value[currentProcessIndex.value] triggerOutputAnimation(currentProcessIndex.value, lastProcess?.productName || 'æå') } completedIndex.value = processList.value.length currentProcessIndex.value = -1 // å¾ªç¯ææ¾ setTimeout(() => { boxPosition.value = 0 completedIndex.value = 0 currentProcessIndex.value = -1 lastCompletedIndex.value = -1 showFeedingAnimation.value = false showOutputAnimation.value = false }, 1500 / speedFactor) } else if (newIndex >= 0) { completedIndex.value = newIndex } }, interval) } // è·åå·¥åºéè¦æå ¥çç©æ const getInputMaterialForProcess = (process) => { if (!process) return null const processId = process.technologyOperationId || process.operationId const processName = process.technologyOperationName || process.operationName let material = null const traverse = (nodes) => { for (const node of nodes) { const nodeProcessId = node.processId || node.operationId || node.technologyOperationId const nodeProcessName = node.processName || node.operationName || node.technologyOperationName if ((processId && nodeProcessId === processId) || (processName && nodeProcessName === processName)) { material = node return true } if (node.children && node.children.length > 0) { if (traverse(node.children)) return true } } return false } traverse(bomData.value) return material } // è§¦åææå¨ç» const triggerFeedingAnimation = (processIndex, materialName) => { showFeedingAnimation.value = true feedingProcessIndex.value = processIndex feedingMaterialName.value = materialName feedingAnimY.value = feedingStartY setTimeout(() => { showFeedingAnimation.value = false }, 600 / parseFloat(speed.value)) } // 触å产åºå¨ç» const triggerOutputAnimation = (processIndex, productName) => { showOutputAnimation.value = true outputProcessIndex.value = processIndex outputProductName.value = productName setTimeout(() => { showOutputAnimation.value = false }, 800 / parseFloat(speed.value)) } const stopAnimation = () => { if (animationTimer.value) { clearInterval(animationTimer.value) animationTimer.value = null } } const showProcessDetail = (process) => { selectedProcess.value = process // æ¥æ¾å·¥åºç´¢å¼ selectedProcessIndex.value = processList.value.findIndex(p => p.id === process.id || (p.technologyOperationName === process.technologyOperationName && p.productName === process.productName) ) processDialogVisible.value = true } const showInputDialog = () => { // å¯ä»¥å±ç¤ºåææå ¥åºä¿¡æ¯ } const showOutputDialog = () => { // å¯ä»¥å±ç¤ºæååºåºä¿¡æ¯ } const goBack = () => { router.push('/productionManagement/teachingDemo') } watch(speed, () => { if (isPlaying.value) { stopAnimation() startAnimation() } }) onMounted(() => { fetchRouteList() }) onUnmounted(() => { stopAnimation() }) </script> <style scoped lang="scss"> .factory-demo { .control-panel { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); margin-bottom: 20px; .speed-control { display: flex; align-items: center; gap: 8px; } .progress-info { display: flex; align-items: center; gap: 8px; } } .factory-container { display: flex; gap: 20px; min-height: 500px; .factory-scene { flex: 1; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 16px; padding: 20px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); overflow: hidden; .factory-svg { width: 100%; height: auto; min-height: 450px; .conveyor-system { .belt-flow { animation: beltFlow 0.8s linear infinite; } } .work-station { cursor: pointer; &:hover { filter: brightness(1.1) drop-shadow(0 0 12px rgba(64, 158, 255, 0.4)); } &.is-active { .station-name { animation: textGlow 1s ease-in-out infinite; } } .feed-point { transition: all 0.3s ease; &.is-feeding { animation: feedPulse 0.3s ease-out; } } .is-pulsing { animation: statusPulse 1s ease-in-out infinite; } } .product-box { transition: transform 0.1s linear; } .product-boxes { animation: boxesMove calc(8s / var(--speed, 1)) linear infinite; } .info-text { animation: fadeInOut 0.5s ease; } } } .factory-sidebar { width: 280px; display: flex; flex-direction: column; gap: 16px; .info-card { .status-info { .status-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; &:last-child { border-bottom: none; } .status-label { color: #909399; font-size: 13px; } .status-value { color: #303133; font-weight: 600; &.highlight { color: #409eff; } } } } } .legend-card { .legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; .legend-dot { width: 12px; height: 12px; border-radius: 50%; &.completed { background: #67c23a; } &.current { background: #e6a23c; animation: legendPulse 1s ease-in-out infinite; } &.pending { background: #c0c4cc; } &.quality { background: #fdf6ec; border: 2px solid #e6a23c; } } .legend-text { font-size: 12px; color: #666; } } } .tips-card { .tips-content { p { font-size: 12px; color: #909399; margin-bottom: 8px; line-height: 1.6; &:before { content: '⢠'; color: #409eff; } } } } } } .material-section { margin-top: 16px; .section-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; h4 { color: #303133; font-size: 14px; margin: 0; font-weight: 600; } } &.empty-section { margin-top: 12px; } .quantity-value { font-weight: 600; color: #67c23a; } .material-summary { margin-top: 8px; } .process-flow { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; .flow-item { display: flex; align-items: center; gap: 8px; .flow-label { font-size: 12px; color: #909399; } &.current { .flow-label { color: #409eff; font-weight: 600; } } } } } } @keyframes beltFlow { to { stroke-dashoffset: -35; } } @keyframes statusPulse { 0%, 100% { opacity: 0.6; transform: scale(1); } 50% { opacity: 1; transform: scale(1.2); } } @keyframes feedPulse { 0% { transform: scale(1); } 50% { transform: scale(1.3); fill: #67c23a; } 100% { transform: scale(1); } } @keyframes textGlow { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } } @keyframes legendPulse { 0%, 100% { box-shadow: 0 0 4px rgba(230, 162, 60, 0.4); } 50% { box-shadow: 0 0 8px rgba(230, 162, 60, 0.6); } } @keyframes fadeInOut { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } </style> src/views/productionManagement/teachingDemo/ProcessRouteDemo.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,535 @@ <template> <div class="app-container process-route-demo"> <PageHeader :content="`å·¥èºè·¯çº¿æ¼ç¤º - ${routeInfo.productName || ''}`"> <template #right-button> <el-button @click="goBack">è¿å</el-button> </template> </PageHeader> <DemoControls ref="controlsRef" :steps="demoSteps" :explanations="demoExplanations" @play="handlePlay" @pause="handlePause" @reset="handleReset" @stepChange="handleStepChange" @speedChange="handleSpeedChange" /> <div class="demo-container" v-loading="loading"> <div class="route-visualization"> <div class="process-flow-container" v-if="processList.length > 0"> <div class="flow-wrapper"> <ProcessNode v-for="(process, index) in processList" :key="process.id || index" :process="process" :index="index" :delay="index * 200" :animation-speed="animationSpeed" :active="activeProcessId === process.id" :completed="completedProcesses.includes(process.id)" :current="currentProcessId === process.id" :animating="isAnimating && currentProcessId === process.id" :show-arrow="index < processList.length - 1" :show-progress="showProgress" :progress="getProcessProgress(process)" @click="handleProcessClick" /> </div> <div class="flow-start-end"> <div class="flow-node start"> <el-icon><Goods /></el-icon> <span>åææ</span> </div> <div class="flow-connector"> <svg width="60" height="24"> <line x1="0" y1="12" x2="50" y2="12" stroke="#67c23a" stroke-width="2" stroke-dasharray="5,5" /> <polygon points="50,8 60,12 50,16" fill="#67c23a" /> </svg> </div> <div class="flow-node end"> <el-icon><Box /></el-icon> <span>æå</span> </div> </div> </div> <el-empty v-else description="ææ å·¥èºè·¯çº¿æ°æ®" /> <div class="visualization-sidebar"> <el-card class="info-card" shadow="hover"> <template #header> <span>å½åå·¥åºä¿¡æ¯</span> </template> <el-descriptions :column="1" border size="small" v-if="activeProcess"> <el-descriptions-item label="å·¥åºåç§°">{{ activeProcess.technologyOperationName || activeProcess.operationName }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ activeProcess.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åå·">{{ activeProcess.model || '-' }}</el-descriptions-item> <el-descriptions-item label="åä½">{{ activeProcess.unit || '-' }}</el-descriptions-item> <el-descriptions-item label="计费类å">{{ activeProcess.type === 1 ? '计件' : '计æ¶' }}</el-descriptions-item> <el-descriptions-item label="æ¯å¦è´¨æ£">{{ activeProcess.isQuality ? 'æ¯' : 'å¦' }}</el-descriptions-item> <el-descriptions-item label="æ¯å¦ç产">{{ activeProcess.isProduction ? 'æ¯' : 'å¦' }}</el-descriptions-item> <el-descriptions-item label="å·¥åºåºå·">{{ activeProcess.dragSort || getProcessIndex(activeProcess) + 1 }}</el-descriptions-item> </el-descriptions> <div v-else class="empty-info">ç¹å»å·¥åºèç¹æ¥ç详ç»ä¿¡æ¯</div> </el-card> <el-card class="progress-card" shadow="hover"> <template #header> <span>ç产è¿åº¦æ¨¡æ</span> </template> <div class="progress-info"> <div class="progress-stat"> <span class="stat-label">已宿工åº</span> <span class="stat-value">{{ completedProcesses.length }} / {{ processList.length }}</span> </div> <el-progress :percentage="processList.length > 0 ? (completedProcesses.length / processList.length) * 100 : 0" :stroke-width="10" status="success" /> </div> <div class="progress-actions"> <el-button type="primary" size="small" @click="simulateProgress"> 模æè¿åº¦ </el-button> <el-button size="small" @click="resetProgress"> éç½® </el-button> </div> </el-card> <el-card class="legend-card" shadow="hover"> <template #header> <span>ç¶æè¯´æ</span> </template> <div class="legend-item"> <span class="legend-icon default"></span> <span class="legend-text">å¾ å å·¥</span> </div> <div class="legend-item"> <span class="legend-icon current"></span> <span class="legend-text">å½åå·¥åº</span> </div> <div class="legend-item"> <span class="legend-icon completed"></span> <span class="legend-text">已宿</span> </div> <div class="legend-item"> <span class="legend-icon active"></span> <span class="legend-text">éä¸ç¶æ</span> </div> </el-card> </div> </div> </div> </div> </template> <script setup> import { ref, computed, onMounted, onUnmounted } from 'vue' import { useRouter, useRoute } from 'vue-router' import { Goods, Box } from '@element-plus/icons-vue' import ProcessNode from './components/ProcessNode.vue' import DemoControls from './components/DemoControls.vue' import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js' const router = useRouter() const route = useRoute() const loading = ref(false) const processList = ref([]) const controlsRef = ref(null) const animationSpeed = ref(1) const isAnimating = ref(false) const activeProcessId = ref(null) const activeProcess = ref(null) const currentProcessId = ref(null) const completedProcesses = ref([]) const showProgress = ref(false) const playTimer = ref(null) const progressTimer = ref(null) const routeId = computed(() => route.query.id) const routeInfo = computed(() => ({ processRouteCode: route.query.processRouteCode || '', productName: route.query.productName || '', model: route.query.model || '', bomId: route.query.bomId || '' })) const demoSteps = [ { title: 'å·¥åºæ¦è§', description: 'æ¥çå ¨é¨å·¥åº' }, { title: 'å·¥åºé¡ºåº', description: 'äºè§£æµç¨' }, { title: 'å·¥åºè¯¦æ ', description: 'æ¥çåæ°' }, { title: 'è¿åº¦æ¨¡æ', description: 'ç产æµç¨' }, { title: 'å®æç¶æ', description: 'çäº§å®æ' } ] const demoExplanations = [ { title: '第1æ¥ï¼å·¥åºæ¦è§', content: 'å·¥èºè·¯çº¿å±ç¤ºäºäº§åä»åææå°æåéè¦ç»è¿çææå·¥åºãæ¯ä¸ªèç¹ä»£è¡¨ä¸éç¬ç«çå å·¥æ¥éª¤ã' }, { title: '第2æ¥ï¼å·¥åºé¡ºåº', content: 'ç®å¤´è¡¨ç¤ºå·¥åºçæ§è¡é¡ºåºã产åæç §ç®å¤´æ¹å便¬¡ç»è¿æ¯éå·¥åºï¼ç´å°æç»å®æã' }, { title: '第3æ¥ï¼å·¥åºè¯¦æ ', content: 'ç¹å»å·¥åºèç¹å¯ä»¥æ¥ç详ç»ä¿¡æ¯ï¼å æ¬äº§ååç§°ãè§æ ¼ã计费类åãæ¯å¦éè¦è´¨æ£çã' }, { title: '第4æ¥ï¼è¿åº¦æ¨¡æ', content: 'ç¹å»"模æè¿åº¦"å¯ä»¥è§ç产åå¨åå·¥åºé´æµè½¬çè¿ç¨ã绿è²è¡¨ç¤ºå·²å®æï¼é»è²è¡¨ç¤ºå½åå·¥åºã' }, { title: '第5æ¥ï¼çäº§å®æ', content: '彿æå·¥åºé½å®æåï¼äº§åå°±çäº§åºæ¥äºãå®é ç产ä¸ï¼æ¯éå·¥åºå¯è½ä¼ææ´è¯¦ç»çåæ°åæä½è¦æ±ã' } ] const fetchProcessList = async () => { if (!routeId.value) return loading.value = true try { const res = await findProcessRouteItemList({ routeId: routeId.value }) processList.value = res?.data || [] // æåºå·æåº processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0)) } catch (error) { console.error('è·åå·¥èºè·¯çº¿æ°æ®å¤±è´¥:', error) } finally { loading.value = false } } const goBack = () => { router.push('/productionManagement/teachingDemo') } const handleProcessClick = (process) => { activeProcess.value = process activeProcessId.value = process.id } const getProcessIndex = (process) => { return processList.value.findIndex(p => p.id === process.id) } const getProcessProgress = (process) => { if (completedProcesses.value.includes(process.id)) { return 100 } if (currentProcessId.value === process.id) { return 50 } return 0 } const handlePlay = () => { isAnimating.value = true runAnimation() } const handlePause = () => { isAnimating.value = false if (playTimer.value) { clearTimeout(playTimer.value) } } const handleReset = () => { isAnimating.value = false activeProcessId.value = null activeProcess.value = null currentProcessId.value = null completedProcesses.value = [] showProgress.value = false if (playTimer.value) { clearTimeout(playTimer.value) } if (progressTimer.value) { clearInterval(progressTimer.value) } } const handleStepChange = (step) => { switch (step) { case 0: // å·¥åºæ¦è§ break case 1: // å·¥åºé¡ºåº if (processList.value.length > 0) { activeProcess.value = processList.value[0] activeProcessId.value = processList.value[0].id } break case 2: // å·¥åºè¯¦æ break case 3: // è¿åº¦æ¨¡æ simulateProgress() break case 4: // å®æç¶æ completedProcesses.value = processList.value.map(p => p.id) currentProcessId.value = null break } } const handleSpeedChange = (speed) => { animationSpeed.value = speed } const simulateProgress = () => { showProgress.value = true completedProcesses.value = [] currentProcessId.value = null let index = 0 const interval = 1500 / animationSpeed.value progressTimer.value = setInterval(() => { if (index >= processList.value.length) { clearInterval(progressTimer.value) currentProcessId.value = null isAnimating.value = false controlsRef.value?.setIsPlaying(false) return } if (index > 0) { completedProcesses.value.push(processList.value[index - 1].id) } currentProcessId.value = processList.value[index].id activeProcess.value = processList.value[index] activeProcessId.value = processList.value[index].id index++ }, interval) } const resetProgress = () => { if (progressTimer.value) { clearInterval(progressTimer.value) } completedProcesses.value = [] currentProcessId.value = null showProgress.value = false } const runAnimation = () => { if (!isAnimating.value) return const totalSteps = demoSteps.length let currentStep = controlsRef.value?.currentStep || 0 const animate = () => { if (!isAnimating.value) return if (currentStep < totalSteps) { controlsRef.value?.setStep(currentStep) handleStepChange(currentStep) currentStep++ playTimer.value = setTimeout(animate, 4000 / animationSpeed.value) } else { isAnimating.value = false controlsRef.value?.setIsPlaying(false) } } animate() } onMounted(() => { fetchProcessList() }) onUnmounted(() => { if (playTimer.value) { clearTimeout(playTimer.value) } if (progressTimer.value) { clearInterval(progressTimer.value) } }) </script> <style scoped lang="scss"> .process-route-demo { .demo-container { background: #f5f7fa; padding: 20px; border-radius: 12px; min-height: 400px; .route-visualization { display: flex; gap: 20px; .process-flow-container { flex: 1; background: #fff; padding: 30px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); .flow-wrapper { display: flex; align-items: center; gap: 0; padding: 20px 0; overflow-x: auto; min-height: 200px; } .flow-start-end { display: flex; align-items: center; justify-content: center; margin-top: 30px; padding-top: 20px; border-top: 1px dashed #e4e7ed; .flow-node { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 16px 24px; border-radius: 12px; &.start { background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%); border: 2px solid #67c23a; .el-icon { color: #67c23a; font-size: 24px; } span { color: #67c23a; font-weight: 600; } } &.end { background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%); border: 2px solid #409eff; .el-icon { color: #409eff; font-size: 24px; } span { color: #409eff; font-weight: 600; } } } .flow-connector { margin: 0 20px; } } } .visualization-sidebar { width: 300px; display: flex; flex-direction: column; gap: 16px; .info-card, .progress-card, .legend-card { .empty-info { color: #909399; text-align: center; padding: 20px; } } .progress-card { .progress-info { margin-bottom: 16px; .progress-stat { display: flex; justify-content: space-between; margin-bottom: 8px; .stat-label { color: #666; font-size: 14px; } .stat-value { color: #409eff; font-weight: 600; } } } .progress-actions { display: flex; gap: 8px; } } .legend-card { .legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; .legend-icon { width: 20px; height: 20px; border-radius: 4px; border: 2px solid; &.default { background: #fff; border-color: #e4e7ed; } &.current { background: linear-gradient(135deg, #fdf6ec 0%, #fff 100%); border-color: #e6a23c; box-shadow: 0 0 8px rgba(230, 162, 60, 0.4); } &.completed { background: linear-gradient(135deg, #f0f9eb 0%, #fff 100%); border-color: #67c23a; } &.active { background: #fff; border-color: #409eff; box-shadow: 0 0 12px rgba(64, 158, 255, 0.5); } } .legend-text { font-size: 12px; color: #666; } } } } } } } </style> src/views/productionManagement/teachingDemo/components/DemoControls.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,186 @@ <template> <div class="demo-controls"> <div class="control-buttons"> <el-button-group> <el-button :type="isPlaying ? 'warning' : 'primary'" @click="togglePlay"> <el-icon v-if="isPlaying"><VideoPause /></el-icon> <el-icon v-else><VideoPlay /></el-icon> {{ isPlaying ? 'æå' : 'ææ¾' }} </el-button> <el-button @click="handleReset"> <el-icon><RefreshRight /></el-icon> éç½® </el-button> </el-button-group> <div class="speed-control"> <span class="speed-label">é度ï¼</span> <el-radio-group v-model="speed" size="small" @change="handleSpeedChange"> <el-radio-button label="0.5">æ ¢é</el-radio-button> <el-radio-button label="1">æ£å¸¸</el-radio-button> <el-radio-button label="2">å¿«é</el-radio-button> </el-radio-group> </div> </div> <div class="step-control" v-if="steps.length > 0"> <span class="step-label">æ¥éª¤ï¼</span> <el-steps :active="currentStep" align-center> <el-step v-for="(step, index) in steps" :key="index" :title="step.title" :description="step.description" @click.native="handleStepClick(index)" class="clickable-step" /> </el-steps> </div> <div class="explanation-box" v-if="currentExplanation"> <el-alert :title="currentExplanation.title" type="success" :closable="false" show-icon > <template #default> {{ currentExplanation.content }} </template> </el-alert> </div> </div> </template> <script setup> import { ref, computed, watch } from 'vue' import { VideoPlay, VideoPause, RefreshRight } from '@element-plus/icons-vue' const props = defineProps({ steps: { type: Array, default: () => [] }, explanations: { type: Array, default: () => [] } }) const emit = defineEmits(['play', 'pause', 'reset', 'stepChange', 'speedChange']) const isPlaying = ref(false) const speed = ref('1') const currentStep = ref(0) const currentExplanation = computed(() => { if (props.explanations && props.explanations[currentStep.value]) { return props.explanations[currentStep.value] } return null }) const togglePlay = () => { isPlaying.value = !isPlaying.value if (isPlaying.value) { emit('play') } else { emit('pause') } } const handleReset = () => { isPlaying.value = false currentStep.value = 0 emit('reset') } const handleStepClick = (index) => { currentStep.value = index emit('stepChange', index) } const handleSpeedChange = (val) => { emit('speedChange', parseFloat(val)) } const nextStep = () => { if (currentStep.value < props.steps.length - 1) { currentStep.value++ emit('stepChange', currentStep.value) } } const setStep = (index) => { currentStep.value = index } const setIsPlaying = (val) => { isPlaying.value = val } defineExpose({ nextStep, setStep, setIsPlaying, currentStep }) </script> <style scoped lang="scss"> .demo-controls { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); margin-bottom: 20px; .control-buttons { display: flex; align-items: center; gap: 24px; margin-bottom: 20px; .speed-control { display: flex; align-items: center; gap: 8px; .speed-label { color: #666; font-size: 14px; } } } .step-control { margin-bottom: 20px; .step-label { display: block; color: #666; font-size: 14px; margin-bottom: 12px; } .clickable-step { cursor: pointer; } } .explanation-box { animation: fadeIn 0.3s ease; } } @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } </style> src/views/productionManagement/teachingDemo/components/ProcessNode.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,354 @@ <template> <div class="process-node" :class="{ 'is-active': active, 'is-completed': completed, 'is-current': current, 'is-animating': animating }" :style="{ '--delay': delay + 'ms', '--speed': animationSpeed }" > <div class="node-badge"> <span class="badge-number">{{ index + 1 }}</span> </div> <div class="node-card" @click="handleClick"> <div class="node-header"> <div class="node-icon"> <el-icon><SetUp /></el-icon> </div> <div class="node-title">{{ process.technologyOperationName || process.operationName || 'æªå½åå·¥åº' }}</div> </div> <div class="node-body"> <div class="node-info-item" v-if="process.productName"> <span class="info-label">产åï¼</span> <span class="info-value">{{ process.productName }}</span> </div> <div class="node-info-item" v-if="process.model"> <span class="info-label">è§æ ¼ï¼</span> <span class="info-value">{{ process.model }}</span> </div> <div class="node-tags"> <el-tag :type="process.type === 1 ? 'primary' : 'success'" size="small"> {{ process.type === 1 ? '计件' : '计æ¶' }} </el-tag> <el-tag v-if="process.isQuality" type="warning" size="small">è´¨æ£</el-tag> <el-tag v-if="process.isProduction" type="info" size="small">ç产</el-tag> </div> </div> <div class="node-progress" v-if="showProgress"> <el-progress :percentage="progress" :status="completed ? 'success' : current ? '' : ''" :stroke-width="6" /> </div> </div> <div class="node-arrow" v-if="showArrow"> <svg width="40" height="24" viewBox="0 0 40 24"> <defs> <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto" > <polygon points="0 0, 10 3.5, 0 7" fill="#409eff" /> </marker> </defs> <line x1="0" y1="12" x2="30" y2="12" stroke="#409eff" stroke-width="2" marker-end="url(#arrowhead)" :class="{ 'is-animating': animating }" /> <circle v-if="animating" r="4" fill="#67c23a" class="flow-dot" > <animate attributeName="cx" values="0;30" :dur="`${1 / animationSpeed}s`" repeatCount="indefinite" /> <animate attributeName="cy" values="12;12" dur="1s" repeatCount="indefinite" /> </circle> </svg> </div> </div> </template> <script setup> import { computed } from 'vue' import { SetUp } from '@element-plus/icons-vue' const props = defineProps({ process: { type: Object, required: true }, index: { type: Number, default: 0 }, delay: { type: Number, default: 0 }, animationSpeed: { type: Number, default: 1 }, active: { type: Boolean, default: false }, completed: { type: Boolean, default: false }, current: { type: Boolean, default: false }, animating: { type: Boolean, default: false }, showArrow: { type: Boolean, default: true }, showProgress: { type: Boolean, default: false }, progress: { type: Number, default: 0 } }) const emit = defineEmits(['click']) const handleClick = () => { emit('click', props.process) } </script> <style scoped lang="scss"> .process-node { display: flex; align-items: center; animation: nodeSlideIn calc(var(--delay) * var(--speed)) ease-out both; .node-badge { position: absolute; top: -12px; left: 50%; transform: translateX(-50%); z-index: 1; .badge-number { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); color: #fff; font-weight: bold; font-size: 14px; border-radius: 50%; box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4); } } .node-card { position: relative; width: 180px; background: #fff; border-radius: 12px; border: 2px solid #e4e7ed; padding: 20px 16px 16px; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); &:hover { transform: translateY(-4px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); border-color: #409eff; } .node-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; .node-icon { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); border-radius: 6px; .el-icon { color: #fff; font-size: 16px; } } .node-title { font-weight: 600; color: #303133; font-size: 13px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } .node-body { .node-info-item { display: flex; font-size: 12px; margin-bottom: 4px; .info-label { color: #909399; margin-right: 4px; } .info-value { color: #606266; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } } .node-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; } } .node-progress { margin-top: 12px; } } .node-arrow { margin: 0 8px; line { transition: stroke 0.3s; &.is-animating { animation: arrowGlow 0.5s ease-in-out infinite alternate; } } .flow-dot { filter: drop-shadow(0 0 4px rgba(103, 194, 58, 0.6)); } } &.is-active { .node-card { border-color: #409eff; box-shadow: 0 4px 16px rgba(64, 158, 255, 0.3); } } &.is-completed { .node-card { border-color: #67c23a; background: linear-gradient(135deg, #f0f9eb 0%, #fff 100%); .node-badge .badge-number { background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); } } } &.is-current { .node-card { border-color: #e6a23c; animation: currentPulse 1.5s ease-in-out infinite; } } &.is-animating { .node-card { animation: cardPulse calc(1s * var(--speed)) ease-in-out infinite; } } } @keyframes nodeSlideIn { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } } @keyframes currentPulse { 0%, 100% { box-shadow: 0 4px 16px rgba(230, 162, 60, 0.3); } 50% { box-shadow: 0 4px 24px rgba(230, 162, 60, 0.5); } } @keyframes cardPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } } @keyframes arrowGlow { from { stroke: #409eff; filter: drop-shadow(0 0 2px rgba(64, 158, 255, 0.4)); } to { stroke: #66b1ff; filter: drop-shadow(0 0 6px rgba(64, 158, 255, 0.6)); } } </style> src/views/productionManagement/teachingDemo/components/TreeNode.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,423 @@ <template> <div class="tree-node" :class="{ 'is-root': isRoot, 'is-expanded': expanded, 'is-highlighted': highlighted, 'is-active': active, 'is-animating': animating }" :style="{ '--delay': delay + 'ms', '--speed': animationSpeed }" > <div class="node-content" @click="handleClick"> <div class="node-icon"> <el-icon v-if="hasChildren"><FolderOpened v-if="expanded" /><Folder v-else /></el-icon> <el-icon v-else><Document /></el-icon> </div> <div class="node-info"> <div class="node-name">{{ node.productName || 'æªå½å产å' }}</div> <div class="node-model" v-if="node.model">{{ node.model }}</div> <div class="node-meta"> <span class="node-quantity"> <el-icon><Goods /></el-icon> {{ node.unitQuantity || 1 }} {{ node.unit || '' }} </span> <span class="node-process" v-if="node.processName && !isRoot"> <el-icon><Setting /></el-icon> {{ node.processName }} </span> </div> </div> <div class="node-expand-btn" v-if="hasChildren" @click.stop="toggleExpand"> <el-icon> <ArrowDown v-if="expanded" /> <ArrowRight v-else /> </el-icon> </div> </div> <div class="node-children" v-if="hasChildren && expanded"> <div class="children-container"> <svg class="connection-lines" v-if="showLines"> <path v-for="(child, index) in node.children" :key="child.tempId || child.id || index" :d="getLinePath(index)" class="connection-line" :class="{ 'is-animating': animating }" :style="{ '--line-delay': (delay + index * 100) + 'ms' }" /> </svg> <TreeNode v-for="(child, index) in node.children" :key="child.tempId || child.id || index" :node="child" :level="level + 1" :delay="delay + 200 + index * 100" :animation-speed="animationSpeed" :auto-expand="autoExpand" :show-lines="showLines" :highlight-ids="highlightIds" :active-id="activeId" :animating="animating" @node-click="(n) => emit('node-click', n)" @expand="(n, exp) => emit('expand', n, exp)" /> </div> </div> <div class="material-flow-ball" v-if="animating && !hasChildren"> <div class="ball"></div> </div> </div> </template> <script setup> import { ref, computed, watch, onMounted, nextTick } from 'vue' import { Folder, FolderOpened, Document, ArrowDown, ArrowRight, Goods, Setting } from '@element-plus/icons-vue' const props = defineProps({ node: { type: Object, required: true }, level: { type: Number, default: 0 }, delay: { type: Number, default: 0 }, animationSpeed: { type: Number, default: 1 }, autoExpand: { type: Boolean, default: false }, showLines: { type: Boolean, default: true }, highlightIds: { type: Array, default: () => [] }, activeId: { type: [String, Number], default: null }, animating: { type: Boolean, default: false } }) const emit = defineEmits(['node-click', 'expand']) const expanded = ref(false) const nodeWidth = ref(200) const nodeHeight = ref(60) const isRoot = computed(() => props.level === 0) const hasChildren = computed(() => props.node.children && props.node.children.length > 0) const highlighted = computed(() => { const nodeId = props.node.tempId || props.node.id return props.highlightIds.includes(nodeId) }) const active = computed(() => { const nodeId = props.node.tempId || props.node.id return props.activeId === nodeId }) const toggleExpand = () => { expanded.value = !expanded.value emit('expand', props.node, expanded.value) } const handleClick = () => { emit('node-click', props.node) } const getLinePath = (index) => { const startX = 20 const startY = 0 const endX = 20 const endY = 30 + index * 70 return `M ${startX} ${startY} L ${startX} ${endY}` } watch(() => props.autoExpand, (val) => { if (val && hasChildren.value) { setTimeout(() => { expanded.value = true }, props.delay) } }) onMounted(() => { if (props.autoExpand && isRoot.value && hasChildren.value) { expanded.value = true } }) </script> <style scoped lang="scss"> .tree-node { position: relative; margin-left: 20px; animation: nodeAppear calc(var(--delay) * var(--speed)) ease-out both; &.is-root { margin-left: 0; .node-content { background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); color: #fff; .node-name { color: #fff; } .node-model { color: rgba(255, 255, 255, 0.85); } .node-meta { color: rgba(255, 255, 255, 0.9); .node-quantity, .node-process { background: rgba(255, 255, 255, 0.2); padding: 2px 8px; border-radius: 4px; } } } } &.is-highlighted { .node-content { border-color: #e6a23c; box-shadow: 0 0 12px rgba(230, 162, 60, 0.4); } } &.is-active { .node-content { border-color: #409eff; box-shadow: 0 0 16px rgba(64, 158, 255, 0.5); animation: activePulse 1.5s ease-in-out infinite; } } &.is-animating { .node-content { animation: nodeFlow calc(2s * var(--speed)) ease-in-out infinite; } } .node-content { display: flex; align-items: center; padding: 12px 16px; background: #fff; border-radius: 12px; border: 2px solid #e4e7ed; min-width: 200px; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); &:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); border-color: #409eff; } .node-icon { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 8px; margin-right: 12px; .el-icon { color: #409eff; font-size: 18px; } } .node-info { flex: 1; .node-name { font-weight: 600; color: #303133; font-size: 14px; } .node-model { color: #909399; font-size: 12px; margin-top: 2px; } .node-meta { display: flex; gap: 8px; margin-top: 6px; font-size: 12px; color: #606266; .node-quantity, .node-process { display: flex; align-items: center; gap: 4px; } } } .node-expand-btn { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: #f5f7fa; border-radius: 4px; transition: background 0.2s; &:hover { background: #409eff; .el-icon { color: #fff; } } .el-icon { font-size: 14px; color: #909399; } } } .node-children { margin-top: 8px; .children-container { position: relative; padding-left: 20px; .connection-lines { position: absolute; top: 0; left: 0; width: 40px; height: 100%; overflow: visible; .connection-line { stroke: #c0c4cc; stroke-width: 2; fill: none; stroke-dasharray: 100; stroke-dashoffset: 100; transition: stroke-dashoffset calc(var(--line-delay) * var(--speed)) ease-out; &.is-animating { stroke-dashoffset: 0; stroke: #409eff; animation: lineFlow 1s ease-in-out infinite; } } } } } .material-flow-ball { position: absolute; right: -20px; top: 50%; transform: translateY(-50%); .ball { width: 12px; height: 12px; background: #67c23a; border-radius: 50%; box-shadow: 0 0 8px rgba(103, 194, 58, 0.6); animation: ballPulse 0.8s ease-in-out infinite; } } } @keyframes nodeAppear { from { opacity: 0; transform: translateY(-20px) scale(0.9); } to { opacity: 1; transform: translateY(0) scale(1); } } @keyframes activePulse { 0%, 100% { box-shadow: 0 0 16px rgba(64, 158, 255, 0.5); } 50% { box-shadow: 0 0 24px rgba(64, 158, 255, 0.8); } } @keyframes nodeFlow { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } } @keyframes lineFlow { 0% { stroke-dashoffset: 100; } 100% { stroke-dashoffset: 0; } } @keyframes ballPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.3); opacity: 0.8; } } </style> src/views/productionManagement/teachingDemo/index.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,333 @@ <template> <div class="app-container teaching-demo-index"> <PageHeader content="å·¥èºè·¯çº¿ä¸BOMå¨ç»æå¦æ¼ç¤º"> <template #right-button> <el-button type="success" @click="goToFactoryDemo"> <el-icon><Monitor /></el-icon> å·¥åæ¼ç¤º </el-button> <el-button type="primary" @click="goToCombinedDemo"> è卿¼ç¤º </el-button> </template> </PageHeader> <div class="demo-intro"> <el-alert title="æå¦æ¼ç¤ºè¯´æ" type="info" :closable="false" show-icon > <template #default> æ¬æ¨¡åéè¿å¨ç»æ¼ç¤ºå¸®å©æ¨çè§£ <strong>BOMï¼ç©ææ¸ åï¼</strong> å <strong>å·¥èºè·¯çº¿</strong> çæ¦å¿µã 鿩䏿¹çæ°æ®åç¹å»è¿å ¥æ¼ç¤ºï¼å³å¯è§çå¨ç»è®²è§£ã </template> </el-alert> </div> <el-row :gutter="20"> <el-col :span="12"> <el-card class="demo-card" shadow="hover"> <template #header> <div class="card-header"> <span class="card-title"> <el-icon><List /></el-icon> BOM ç»ææ¼ç¤º </span> <el-button type="primary" :disabled="!selectedBom" @click="goToBomDemo"> è¿å ¥æ¼ç¤º </el-button> </div> </template> <div class="card-content"> <p class="card-desc">å±ç¤ºç©ææ¸ åçæ å½¢å±çº§ç»æï¼ç解产åç±åªäºé¶é¨ä»¶ç»æã</p> <el-select v-model="selectedBom" placeholder="è¯·éæ©ä¸ä¸ªBOM" filterable style="width: 100%" @change="handleBomChange" > <el-option v-for="item in bomList" :key="item.id" :label="`${item.bomNo} - ${item.productName}`" :value="item.id" /> </el-select> <div v-if="selectedBomInfo" class="selected-info"> <el-descriptions :column="1" border size="small"> <el-descriptions-item label="BOMç¼å·">{{ selectedBomInfo.bomNo }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ selectedBomInfo.productName }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åå·">{{ selectedBomInfo.productModelName }}</el-descriptions-item> <el-descriptions-item label="çæ¬å·">{{ selectedBomInfo.version }}</el-descriptions-item> </el-descriptions> </div> </div> </el-card> </el-col> <el-col :span="12"> <el-card class="demo-card" shadow="hover"> <template #header> <div class="card-header"> <span class="card-title"> <el-icon><Connection /></el-icon> å·¥èºè·¯çº¿æ¼ç¤º </span> <el-button type="primary" :disabled="!selectedRoute" @click="goToRouteDemo"> è¿å ¥æ¼ç¤º </el-button> </div> </template> <div class="card-content"> <p class="card-desc">å±ç¤ºäº§åå å·¥çå·¥åºæµç¨ï¼çè§£äº§åæ¯å¦ä½ä¸æ¥æ¥çäº§åºæ¥çã</p> <el-select v-model="selectedRoute" placeholder="è¯·éæ©ä¸ä¸ªå·¥èºè·¯çº¿" filterable style="width: 100%" @change="handleRouteChange" > <el-option v-for="item in routeList" :key="item.id" :label="`${item.processRouteCode} - ${item.productName}`" :value="item.id" /> </el-select> <div v-if="selectedRouteInfo" class="selected-info"> <el-descriptions :column="1" border size="small"> <el-descriptions-item label="路线ç¼å·">{{ selectedRouteInfo.processRouteCode }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ selectedRouteInfo.productName }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼åç§°">{{ selectedRouteInfo.model }}</el-descriptions-item> <el-descriptions-item label="BOMç¼å·">{{ selectedRouteInfo.bomNo }}</el-descriptions-item> </el-descriptions> </div> </div> </el-card> </el-col> </el-row> <el-card class="concept-card" shadow="hover"> <template #header> <span class="card-title"> <el-icon><Reading /></el-icon> æ¦å¿µè¯´æ </span> </template> <el-row :gutter="20"> <el-col :span="12"> <div class="concept-item"> <h4>BOMï¼ç©ææ¸ åï¼</h4> <p> BOMï¼Bill of Materialsï¼æ¯äº§åç»æçææ¯æä»¶ï¼å®è¯¦ç»è®°å½äºäº§åç±åªäºé¶é¨ä»¶ãåææç»æï¼ ä»¥ååç»æé¨åä¹é´çå±çº§å ³ç³»åæ°éå ³ç³»ã </p> <ul> <li><strong>ç¶é¡¹ï¼</strong>ä¸å±äº§åæé¨ä»¶</li> <li><strong>å项ï¼</strong>ç»æç¶é¡¹çé¶é¨ä»¶æåææ</li> <li><strong>ç¨éï¼</strong>ç产ä¸ä¸ªç¶é¡¹éè¦å¤å°å项</li> </ul> </div> </el-col> <el-col :span="12"> <div class="concept-item"> <h4>å·¥èºè·¯çº¿</h4> <p> å·¥èºè·¯çº¿æ¯æè¿°äº§åå å·¥è¿ç¨çæä»¶ï¼å®è§å®äºäº§åä»åææå°æåéè¦ç»è¿åªäºå·¥åºï¼ 以ååå·¥åºçå å顺åºåæä½è¦æ±ã </p> <ul> <li><strong>å·¥åºï¼</strong>ä¸ä¸ªç¬ç«çå å·¥æ¥éª¤</li> <li><strong>顺åºï¼</strong>å·¥åºæ§è¡çå 忬¡åº</li> <li><strong>åæ°ï¼</strong>å·¥åºçå ·ä½æä½è¦æ±</li> </ul> </div> </el-col> </el-row> </el-card> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' import { useRouter } from 'vue-router' import { List, Connection, Reading, Monitor } from '@element-plus/icons-vue' import { listPage as getBomList } from '@/api/productionManagement/productBom.js' import { listPage as getRouteList } from '@/api/productionManagement/processRoute.js' const router = useRouter() const bomList = ref([]) const routeList = ref([]) const selectedBom = ref(null) const selectedRoute = ref(null) const selectedBomInfo = computed(() => { return bomList.value.find(item => item.id === selectedBom.value) }) const selectedRouteInfo = computed(() => { return routeList.value.find(item => item.id === selectedRoute.value) }) const fetchBomList = async () => { try { const res = await getBomList({ current: 1, size: 100 }) bomList.value = res?.data?.records || [] } catch (error) { console.error('è·åBOMå表失败:', error) } } const fetchRouteList = async () => { try { const res = await getRouteList({ current: 1, size: 100 }) routeList.value = res?.data?.records || [] } catch (error) { console.error('è·åå·¥èºè·¯çº¿å表失败:', error) } } const handleBomChange = (val) => { selectedBom.value = val } const handleRouteChange = (val) => { selectedRoute.value = val } const goToBomDemo = () => { if (!selectedBom.value) return const info = selectedBomInfo.value router.push({ path: '/productionManagement/teachingDemo/bom', query: { id: selectedBom.value, bomNo: info?.bomNo || '', productName: info?.productName || '', productModelName: info?.productModelName || '' } }) } const goToRouteDemo = () => { if (!selectedRoute.value) return const info = selectedRouteInfo.value router.push({ path: '/productionManagement/teachingDemo/processRoute', query: { id: selectedRoute.value, processRouteCode: info?.processRouteCode || '', productName: info?.productName || '', model: info?.model || '', bomId: info?.bomId || '' } }) } const goToCombinedDemo = () => { router.push({ path: '/productionManagement/teachingDemo/combined', query: { bomId: selectedBom.value || '', routeId: selectedRoute.value || '' } }) } const goToFactoryDemo = () => { router.push({ path: '/productionManagement/teachingDemo/factory', query: { routeId: selectedRoute.value || '' } }) } onMounted(() => { fetchBomList() fetchRouteList() }) </script> <style scoped lang="scss"> .teaching-demo-index { .demo-intro { margin-bottom: 20px; } .demo-card { margin-bottom: 20px; .card-header { display: flex; justify-content: space-between; align-items: center; .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 16px; } } .card-content { .card-desc { color: #666; margin-bottom: 16px; line-height: 1.6; } .selected-info { margin-top: 16px; } } } .concept-card { .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 16px; } .concept-item { h4 { color: #409eff; margin-bottom: 12px; font-size: 15px; } p { color: #666; line-height: 1.8; margin-bottom: 12px; } ul { padding-left: 20px; color: #888; line-height: 1.8; li { margin-bottom: 4px; } } } } } </style>