From 12c98fc512070fc52d5956be1da9ce099b40f350 Mon Sep 17 00:00:00 2001 From: zhuguifei <zhuguifei@zhuguifeideiMac.local> Date: 星期一, 08 九月 2025 12:36:52 +0800 Subject: [PATCH] 添加预测性维护两个页面 --- src/assets/images/JUKI.png | 0 src/views/qms/ai/detail.vue | 856 +++++++++++++++++++++++++++++++++++++ src/views/qms/ai/index.vue | 515 ++++++++++++++++++++++ 3 files changed, 1,371 insertions(+), 0 deletions(-) diff --git a/src/assets/images/JUKI.png b/src/assets/images/JUKI.png new file mode 100644 index 0000000..27224a2 --- /dev/null +++ b/src/assets/images/JUKI.png Binary files differ diff --git a/src/views/qms/ai/detail.vue b/src/views/qms/ai/detail.vue new file mode 100644 index 0000000..60d491e --- /dev/null +++ b/src/views/qms/ai/detail.vue @@ -0,0 +1,856 @@ +<template> + <div class="device-detail-container"> + <el-page-header @back="goBack"> + <template #content> + <span class="text-large font-600 mr-3"> SMT璐寸墖鏈洪娴嬫�х淮鎶よ鎯�</span> + </template> + </el-page-header> + + <el-row :gutter="16" class="mt-4"> + <!-- 璁惧鍥剧墖鍜岃澶囧熀鏈俊鎭悎骞� --> + <el-col :span="10"> + <el-card class="mb-4" :style="{ height: '440px' }"> + <template #header> + <div class="card-header"> + <span>璁惧淇℃伅</span> + </div> + </template> + <el-row :gutter="16"> + <el-col :span="12"> + <div class="device-image-container"> + <img :src="deviceInfo.imageUrl" alt="璁惧鍥剧墖" class="device-image" /> + </div> + </el-col> + <el-col :span="12"> + <el-descriptions border :column="1" class="custom-descriptions"> + <el-descriptions-item label="璁惧鍚嶇О">{{ deviceInfo.deviceName }}</el-descriptions-item> + <el-descriptions-item label="璁惧绫诲瀷">{{ deviceInfo.deviceType }}</el-descriptions-item> + <el-descriptions-item label="璁惧缂栧彿">{{ deviceInfo.deviceId }}</el-descriptions-item> + <el-descriptions-item label="瀹夎鏃ユ湡">{{ deviceInfo.installDate }}</el-descriptions-item> + <el-descriptions-item label="浣跨敤骞撮檺">{{ deviceInfo.serviceLife }}骞�</el-descriptions-item> + <el-descriptions-item label="褰撳墠鐘舵��"> + <el-tag :type="getStatusTagType(deviceInfo.status)">{{ deviceInfo.status }}</el-tag> + </el-descriptions-item> + </el-descriptions> + </el-col> + </el-row> + </el-card> + </el-col> + + <!-- 璁惧鍋ュ悍鐘舵�佷笌缁存姢寤鸿 --> + <el-col :span="14"> + <el-card class="mb-4" :style="{ height: '440px' }"> + <template #header> + <div class="card-header"> + <span>璁惧鍋ュ悍鐘舵�佷笌缁存姢寤鸿</span> + </div> + </template> + <el-row :gutter="16"> + <el-col :span="8"> + <el-statistic + title="鏁翠綋鍋ュ悍搴�" + :value="healthData.overallHealth" + :precision="0" + :suffix="'%'" + :value-style="{ color: healthData.healthColor }" + /> + </el-col> + <el-col :span="8"> + <el-statistic + title="棰勬祴鍓╀綑瀵垮懡" + :value="healthData.predictedLife" + :suffix="'澶�'" + /> + </el-col> + <el-col :span="8"> + <el-statistic + title="鏁呴殰椋庨櫓绛夌骇" + :value="healthData.riskLevel" + :value-style="{ color: healthData.riskColor }" + /> + </el-col> + </el-row> + <div class="mt-4"> + <el-progress + :percentage="healthData.overallHealth" + :color="healthData.healthColor" + :show-text="false" + /> + </div> + + <el-divider content-position="left">棰勬祴鎬х淮鎶ゅ缓璁�</el-divider> + <div class="table-container"> + + <el-table + ref="maintenanceTable" + :data="displayMaintenanceData" + height="246" + size="large" + stripe + > + <el-table-column prop="type" label="缁存姢绫诲瀷" /> + <el-table-column prop="content" label="缁存姢鍐呭" /> + <el-table-column prop="suggestedTime" label="寤鸿鏃堕棿" /> + <el-table-column prop="urgency" label="绱ф�ョ▼搴�"> + <template #default="{ row }"> + <el-tag :type="getUrgencyTagType(row.urgency)">{{ row.urgency }}</el-tag> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="80"> + <template #default="{ row }"> + <el-button link type="primary" size="small" @click="handleMaintenance(row)">澶勭悊</el-button> + </template> + </el-table-column> + </el-table> + + </div> + </el-card> + </el-col> + + <el-col :span="24"> + <el-card class="mb-4"> + <template #header> + <div class="card-header"> + <span>璁惧鏁版嵁</span> + </div> + </template> + <el-row :gutter="16"> + <el-col :span="4"> + <el-statistic title="X杞存�荤Щ鍔ㄨ窛绂�" :value="healthData.xAxisTravel" :precision="2" :suffix="'km'"> + <template #prefix> + <el-icon style="vertical-align: -0.125em"><TrendCharts /></el-icon> + </template> + </el-statistic> + </el-col> + <el-col :span="4"> + <el-statistic title="Y杞存�荤Щ鍔ㄨ窛绂�" :value="healthData.yAxisTravel" :precision="2" :suffix="'km'"> + <template #prefix> + <el-icon style="vertical-align: -0.125em"><TrendCharts /></el-icon> + </template> + </el-statistic> + </el-col> + <el-col :span="4"> + <el-statistic title="鍗″甫娆℃暟" :value="healthData.tapeJamCount" :suffix="'娆�'"> + <template #prefix> + <el-icon style="vertical-align: -0.125em"><Warning /></el-icon> + </template> + </el-statistic> + </el-col> + <el-col :span="4"> + <el-statistic title="鍗℃枡娆℃暟" :value="healthData.materialJamCount" :suffix="'娆�'"> + <template #prefix> + <el-icon style="vertical-align: -0.125em"><Warning /></el-icon> + </template> + </el-statistic> + </el-col> + <el-col :span="4"> + <el-statistic title="鎷兼澘鏁�" :value="healthData.panelCount" :suffix="'鍧�'"> + <template #prefix> + <el-icon style="vertical-align: -0.125em"><Grid /></el-icon> + </template> + </el-statistic> + </el-col> + <el-col :span="4"> + <el-statistic title="鍑洪敊鍋滄満鏃堕棿" :value="healthData.downtime" :suffix="'绉�'"> + <template #prefix> + <el-icon style="vertical-align: -0.125em"><Clock /></el-icon> + </template> + </el-statistic> + </el-col> + </el-row> + </el-card> + </el-col> + </el-row> + + <!-- 瀹炴椂鏁版嵁瓒嬪娍鍥� --> + <el-card class="mb-4"> + <template #header> + <div class="card-header"> + <span>瀹炴椂鏁版嵁瓒嬪娍鍥�</span> + </div> + </template> + <el-row :gutter="16" class="mt-4"> + <el-col :span="8"> + <div id="ambientTemperatureHumidityChart" style="height: 300px;"></div> + </el-col> + <el-col :span="8"> + <div id="motorTemperatureChart" style="height: 300px;"></div> + </el-col> + <el-col :span="8"> + <div id="motorVibrationChart" style="height: 300px;"></div> + </el-col> + </el-row> + + <!-- 璐磋澶�/鍚稿槾 --> + <el-row :gutter="16" class="mt-4"> + <el-col :span="8"> + <div id="nozzleVacuumChart" style="height: 300px;"></div> + </el-col> + <el-col :span="8"> + <div id="nozzleFlowChart" style="height: 300px;"></div> + </el-col> + <el-col :span="8"> + <div id="placementSpeedChart" style="height: 300px;"></div> + </el-col> + </el-row> + </el-card> + + <el-row :gutter="16"> + <!-- 閮ㄤ欢瀵垮懡棰勬祴 --> + <el-col :span="12"> + <el-card class="mb-4" :style="{ height: '560px' }"> + <template #header> + <div class="card-header"> + <span>閮ㄤ欢瀵垮懡棰勬祴</span> + </div> + </template> + <el-table + :data="sparePartData" + size="large" + stripe + > + <el-table-column prop="name" label="閮ㄤ欢鍚嶇О" /> + <el-table-column prop="currentLife" label="鐞嗚瀵垮懡" /> + <el-table-column prop="remainingLife" label="棰勬祴鍓╀綑瀵垮懡" /> + <el-table-column prop="status" label="鐘舵��"> + <template #default="{ row }"> + <el-tag :type="getStatusTagType(row.status)">{{ row.status }}</el-tag> + </template> + </el-table-column> + </el-table> + </el-card> + </el-col> + + <!-- 鍘嗗彶缁存姢璁板綍 --> + <el-col :span="12"> + <el-card class="mb-4" :style="{ height: '560px' }"> + <template #header> + <div class="card-header"> + <span>鍘嗗彶缁存姢璁板綍</span> + </div> + </template> + <el-timeline> + <el-timeline-item + v-for="item in historyData" + :key="item.id" + :type="getTimelineItemType(item.color)" + :timestamp="item.date" + > + {{ item.type }}: {{ item.description }} + </el-timeline-item> + </el-timeline> + </el-card> + </el-col> + </el-row> + </div> +</template> + +<script lang="ts"> +import { defineComponent, reactive, onMounted, onUnmounted, ref, nextTick } from 'vue'; +import { useRouter } from 'vue-router'; +import * as echarts from 'echarts'; +import { + TrendCharts, + Warning, + Grid, + Clock +} from '@element-plus/icons-vue'; +import { ElMessage } from 'element-plus'; +import img from '@/assets/images/JUKI.png'; + + +export default defineComponent({ + name: 'SmtMachineDetail', + components: { + TrendCharts, + Warning, + Grid, + Clock + }, + setup() { + const router = useRouter(); + + const goBack = () => { + router.go(-1); + }; + + // 妯℃嫙鏁版嵁 + const deviceInfo = reactive({ + deviceName: 'SMT璐寸墖鏈�', + deviceType: '璐寸墖鏈�', + deviceId: 'GPC2012A101', + installDate: '2012-06-08', + serviceLife: 15, + status: '杩愯涓�', + statusColor: '#52c41a', + imageUrl: img + }); + + const healthData = reactive({ + overallHealth: 82, + healthColor: '#52c41a', + predictedLife: 635, + riskLevel: '浣庨闄�', + riskColor: '#1a7ac4', + xAxisTravel: 300.179, + yAxisTravel: 233.39, + tapeJamCount: 6, + materialJamCount: 15, + panelCount: 2480, + downtime: 4.5, + }); + + const maintenanceData = reactive([ + { + key: '1', + type: '1鍙疯创瑁呯郴缁熺淮鎶�', + content: '鍚稿槾鐪熺┖鍘嬪姏鍋忎綆', + suggestedTime: '2025-07-05', + urgency: '涓瓑' + }, + { + key: '6', + type: '4鍙疯创瑁呯郴缁熺淮鎶�', + content: 'T杞撮┈杈惧鍛藉憡鎬�', + suggestedTime: '2025-07-05', + urgency: '涓瓑' + }, + { + key: '2', + type: '杩愬姩绯荤粺淇濆吇', + content: 'X/Y杞翠己鏈嶇數鏈烘鼎婊戞鏌�', + suggestedTime: '2025-06-20', + urgency: '浣�' + }, + { + key: '3', + type: '渚涙枡绯荤粺妫�鏌�', + content: '椋炶揪鍗″甫/鍗℃枡娆℃暟娓呴浂', + suggestedTime: '2025-06-10', + urgency: '浣�' + }, + { + key: '4', + type: '2鍙疯创瑁呯郴缁熺淮鎶�', + content: '鐪熺┖鍘嬪姏涓嶇ǔ瀹�', + suggestedTime: '2025-07-05', + urgency: '浣�' + }, + { + key: '5', + type: '3鍙疯创瑁呯郴缁熺淮鎶�', + content: 'Z杞撮┈杈剧數娴佸紓甯�', + suggestedTime: '2025-07-05', + urgency: '浣�' + }, + { + key: '7', + type: '5鍙疯创瑁呭ご缁存姢', + content: '鐪熺┖鍘嬪姏涓嶇ǔ瀹�', + suggestedTime: '2025-07-05', + urgency: '浣�' + }, + { + key: '8', + type: '6鍙疯创瑁呭ご缁存姢', + content: '鐪熺┖鍘嬪姏涓嶇ǔ瀹�', + suggestedTime: '2025-07-05', + urgency: '浣�' + }, + ]); + + const getUrgencyTagType = (urgency: string) => { + switch (urgency) { + case '楂�': return 'danger'; + case '涓瓑': return 'warning'; + case '浣�': return 'success'; + default: return 'info'; + } + }; + + const getStatusTagType = (status: string) => { + switch (status) { + case '杩愯涓�': return 'success'; + case '棰勮': return 'warning'; + case '鍗遍櫓': return 'danger'; + case '鑹ソ': return 'success'; + default: return 'info'; + } + }; + + const getTimelineItemType = (color: string) => { + switch (color) { + case 'green': return 'success'; + case 'red': return 'danger'; + default: return 'primary'; + } + }; + + // const fetchDeviceData = async () => { + // try { + // const res = await getDeviceDataSmt(); + // Object.assign(healthData, res); + // } catch (error) { + // console.error('鑾峰彇璁惧鏁版嵁澶辫触:', error); + // } + // }; + + let deviceDataInterval: number | undefined; + // fetchDeviceData(); + // deviceDataInterval = setInterval(fetchDeviceData, 3000); + + const sparePartData = reactive([ + { + key: '1', + name: '1鍙疯创瑁呯郴缁烼杞翠己鏈嶇數鏈�', + currentLife: '15000灏忔椂', + remainingLife: '1451灏忔椂', + status: '棰勮' + }, + { + key: '5', + name: '1鍙疯创瑁呯郴缁焃杞翠己鏈嶇數鏈�', + currentLife: '15000灏忔椂', + remainingLife: '7521灏忔椂', + status: '鑹ソ' + }, + { + key: '9', + name: '1鍙疯创瑁呯郴缁熺湡绌虹數纾侀榾', + currentLife: '10000灏忔椂', + remainingLife: '2154灏忔椂', + status: '鑹ソ' + }, + { + key: '2', + name: '1鍙疯创瑁呭ご', + currentLife: '1000000娆�', + remainingLife: '425542娆�', + status: '鑹ソ' + }, + { + key: '6', + name: '2鍙疯创瑁呯郴缁烼杞翠己鏈嶇數鏈�', + currentLife: '15000灏忔椂', + remainingLife: '7540灏忔椂', + status: '鑹ソ' + }, + { + key: '7', + name: '2鍙疯创瑁呯郴缁焃杞翠己鏈嶇數鏈�', + currentLife: '15000灏忔椂', + remainingLife: '7521灏忔椂', + status: '鑹ソ' + }, + { + key: '9', + name: '2鍙疯创瑁呯郴缁熺湡绌虹數纾侀榾', + currentLife: '10000灏忔椂', + remainingLife: '2154灏忔椂', + status: '鑹ソ' + }, + { + key: '8', + name: '2鍙疯创瑁呭ご', + currentLife: '1000000娆�', + remainingLife: '751251娆�', + status: '鑹ソ' + }, + { + key: '3', + name: '椋炶揪', + currentLife: '96涓湀', + remainingLife: '43涓湀', + status: '鑹ソ' + }, + ]); + + const historyData = reactive([ + { + id: '1', + date: '2025-07-22', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚鏈堝害淇濆吇锛屼笣鏉嗗杞ㄦ敞娌癸紝鎶涙枡娓呯悊', + color: 'green' + }, + { + id: '1', + date: '2025-06-18', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚鏈堝害淇濆吇锛屾槗鎹熶欢鏇存崲', + color: 'green' + }, + { + id: '1', + date: '2025-05-15', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚瀛e害淇濆吇锛屾鏌ヨ繍鍔ㄧ郴缁熸鼎婊�', + color: 'green' + }, + { + id: '1', + date: '2025-04-20', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚鏈堝害淇濆吇锛岄槻灏樿繃婊ょ綉娓呯悊锛屽杞ㄦ敞娌�', + color: 'green' + }, + { + id: '1', + date: '2025-03-16', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚鏈堝害淇濆吇锛岀湡绌哄�兼牎鍑�', + color: 'green' + }, + { + id: '1', + date: '2025-02-13', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚鏈堝害淇濆吇锛屽惛鍢存鏌ユ洿鎹紝鎶涙枡娓呯悊锛岀浉鏈哄弬鏁版牎鍑�', + color: 'green' + }, + { + id: '2', + date: '2025-01-20', + type: '鏁呴殰缁翠慨', + description: '淇鍚稿槾鍫靛闂', + color: 'red' + }, + { + id: '3', + date: '2025-01-15', + type: '瀹氭湡淇濆吇', + description: '瀹屾垚骞村害淇濆吇锛岃创瑁呯郴缁熸牎鍑嗭紝鏄撴崯浠舵洿鎹紝鏍″噯瑙嗚绯荤粺', + color: 'green' + } + ]); + + const handleMaintenance = (record: any) => { + console.log('澶勭悊缁存姢寤鸿:', record); + ElMessage.info(`澶勭悊缁存姢寤鸿: ${record.type}`); + }; + + const maintenanceTable = ref(); + + // 鐢ㄤ簬鏄剧ず鐨勭淮鎶ゆ暟鎹紙鏀寔鏃犻檺婊氬姩锛� + const displayMaintenanceData = ref([...maintenanceData]); + let scrollInterval: ReturnType<typeof setInterval> | undefined; + + // 鍚姩鑷姩婊氬姩 + const startAutoScroll = () => { + scrollInterval = setInterval(() => { + if (displayMaintenanceData.value.length > 0) { + // 灏嗙涓�椤圭Щ鍒版渶鍚� + const firstItem = displayMaintenanceData.value.shift(); + if (firstItem) { + displayMaintenanceData.value.push(firstItem); + } + } + }, 1000); // 姣�3绉掓粴鍔ㄤ竴娆� + }; + + onMounted(() => { + nextTick(() => { + initCharts(); + startAutoScroll(); // 鍚姩鑷姩婊氬姩 + }); + }); + + onUnmounted(() => { + if (deviceDataInterval) { + clearInterval(deviceDataInterval); + } + if (scrollInterval) { + clearInterval(scrollInterval); + } + }); + + const initCharts = () => { + // 鍥捐〃鍒濆鍖栦唬鐮侊紙涓庝箣鍓嶇浉鍚岋級 + // 鍒濆鍖栧浘琛� + const initChart = (chartId: string, title: string, seriesConfig: Array<{ name: string, data: any[], unit: string, baseValue: number, fluctuation: number, color: string }>) => { + const chart = echarts.init(document.getElementById(chartId)); + + const updateChart = () => { + // 鐢熸垚鏂扮殑鏁版嵁鐐� + const now = new Date(); + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; + + seriesConfig.forEach(s => { + const newValue = (s.baseValue + (Math.random() * 2 - 1) * s.fluctuation).toFixed(2); + s.data.push({ + time, + value: newValue + }); + if (s.data.length > 24) { + s.data.shift(); + } + }); + + const option = { + title: { + text: title, + left: 'center' + }, + tooltip: { + trigger: 'axis', + formatter: (params) => { + let result = `${params[0].axisValueLabel}<br/>`; + params.forEach(param => { + result += `${param.marker} ${param.seriesName}: ${param.data}${seriesConfig[param.seriesIndex].unit}<br/>`; + }); + return result; + } + }, + grid: { + left: '8%', // 澧炲姞宸︿晶杈硅窛 + right: '5%', + bottom: '10%', + containLabel: true + }, + xAxis: { + type: 'category', + data: seriesConfig[0].data.map(item => item.time) + }, + yAxis: seriesConfig.map((s, index) => ({ + type: 'value', + name: s.name, + position: index === 0 ? 'left' : 'right', + axisLine: { + show: true, + }, + axisLabel: { + formatter: (value) => `${value}${s.unit}` + } + })), + series: seriesConfig.map((s, index) => ({ + name: s.name, + data: s.data.map(item => parseFloat(item.value)), + type: 'line', + smooth: true, + yAxisIndex: index, + lineStyle: { + width: 2, + color: s.color + }, + areaStyle: { + opacity: 0.8, + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: 'rgba(84,112,198,0.3)' + }, + { + offset: 1, + color: 'rgba(84,112,198,0)' + } + ]) + }, + } + )), + }; + chart.setOption(option); + }; + + seriesConfig.forEach(s => { + s.data = []; + // 鐢熸垚鍒濆鏁版嵁鐐癸紙60涓偣锛�5鍒嗛挓鏁版嵁锛� + for (let i = 0; i < 24; i++) { + const now = new Date(Date.now() - (24 - i) * 5000); // 鐢熸垚杩囧幓5鍒嗛挓鐨勬暟鎹� + const time = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}`; + const newValue = (s.baseValue + (Math.random() * 2 - 1) * s.fluctuation).toFixed(2); + s.data.push({ + time, + value: newValue + }); + } + }) + + + // 鍒濆娓叉煋 + updateChart(); + + // 姣�5绉掓洿鏂颁竴娆℃暟鎹� + const intervalId = setInterval(updateChart, 5000); + + window.addEventListener('resize', () => { + chart.resize(); + }); + + + }; + + // 鍒濆鍖栧悇涓浘琛� + initChart('motorTemperatureChart', '浜ч噺', [ + { name: '浜ч噺', data: [], unit: 'pcs/h', baseValue: 175, fluctuation: 25, color: '#5470C6' }, + ]); + + initChart('motorVibrationChart', '鎶涙枡鐜�', [ + { name: '鎶涙枡鐜�', data: [], unit: '%', baseValue: 0.15, fluctuation: 0.01, color: '#5470C6' }, + ]); + + initChart('nozzleVacuumChart', '鍚稿槾鐪熺┖鍘嬪姏', [ + { name: '鍘嬪姏', data: [], unit: 'kPa', baseValue: -45, fluctuation: 5, color: '#5470C6' }, + ]); + + initChart('nozzleFlowChart', '鍚稿槾鍚规皵鍘嬪姏', [ + { name: '鍘嬪姏', data: [], unit: 'kPa', baseValue: 20, fluctuation: 5, color: '#5470C6' }, + ]); + + initChart('placementSpeedChart', '璐磋閫熷害', [ + { name: '閫熷害', data: [], unit: 'chips/h', baseValue: 9000, fluctuation: 1500, color: '#5470C6' }, + ]); + + const ambientTemperatureData: any[] = []; + const ambientHumidityData: any[] = []; + initChart('ambientTemperatureHumidityChart', '鐜娓╂箍搴�', [ + { name: '娓╁害', data: ambientTemperatureData, unit: '掳C', baseValue: 25, fluctuation: 1, color: '#5470C6' }, + { name: '婀垮害', data: ambientHumidityData, unit: '%', baseValue: 60, fluctuation: 5, color: '#91cc75' } + ]); + }; + + return { + deviceInfo, + healthData, + maintenanceData, + displayMaintenanceData, + sparePartData, + historyData, + goBack, + getUrgencyTagType, + getStatusTagType, + getTimelineItemType, + handleMaintenance, + maintenanceTable, + + }; + } +}); +</script> + +<style scoped> +.device-detail-container { + padding: 16px; + background: #f0f2f5; +} + +.page-header-content { + display: flex; + flex-direction: column; +} + +.page-header-title { + font-size: 18px; + font-weight: bold; +} + +.page-header-subtitle { + font-size: 14px; + color: #666; + margin-top: 4px; +} + +.card-header { + font-weight: bold; +} + +.device-image-container { + display: flex; + justify-content: center; + align-items: center; + height: 330px; + overflow: hidden; +} + +.device-image { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.mt-4 { + margin-top: 1rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + + +.custom-descriptions { + height: 357px; /* 璁剧疆鏁翠釜鎻忚堪鍒楄〃鐨勯珮搴� */ +} + +.custom-descriptions :deep(.el-descriptions__body) { + height: 100%; +} + +.custom-descriptions :deep(.el-descriptions__table) { + height: 100%; +} + +.custom-descriptions :deep(.el-descriptions__table tbody) { + height: 100%; + display: flex; + flex-direction: column; +} + +.custom-descriptions :deep(.el-descriptions__table tr) { + flex: 1; + display: flex; +} + +.custom-descriptions :deep(.el-descriptions__table td) { + flex: 1; + display: flex; + align-items: center; +} + +.custom-descriptions :deep(.el-descriptions__table .el-descriptions__label) { + display: flex; + align-items: center; + justify-content: flex-start; +} + +.custom-descriptions :deep(.el-descriptions__table .el-descriptions__content) { + display: flex; + align-items: center; + justify-content: flex-start; +} + +/* +.table-container { + position: relative; + height: 246px; + overflow: hidden; +} + +.table-container :deep(.el-table__body-wrapper) { + overflow: hidden; +} + +.table-container :deep(.el-table__body) { + animation: scrollTable 20s linear infinite; +} + +.table-container:hover :deep(.el-table__body) { + animation-play-state: paused; +} + +@keyframes scrollTable { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-100%); + } +} + + +.table-container :deep(.el-table__header-wrapper) { + position: sticky; + top: 0; + background: white; + z-index: 10; +} +*/ +</style> diff --git a/src/views/qms/ai/index.vue b/src/views/qms/ai/index.vue new file mode 100644 index 0000000..d010fb5 --- /dev/null +++ b/src/views/qms/ai/index.vue @@ -0,0 +1,515 @@ +<template> + <div class="app-container"> + <div class="p-5"> + <!-- 璁惧鍋ュ悍鐘舵�佸彲瑙嗗寲 --> + <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> + <el-card shadow="hover" class="h-full"> + <template #header> + <div class="card-header"> + <span>璁惧鍋ュ悍搴﹁瘎鍒�</span> + </div> + </template> + <!-- 杩欓噷灏嗘斁缃仴搴峰害鍒嗗竷鍥捐〃 --> + <div ref="chartRef" class="h-400 w-full"></div> + </el-card> + + <el-card shadow="hover" class="h-full"> + <template #header> + <div class="card-header"> + <span>鍏抽敭鎸囨爣</span> + </div> + </template> + <div class="grid grid-cols-2 gap-2"> + <div class="p-2 bg-blue-50 rounded"> + <p class="text-sm text-gray-600">鍋ュ悍璁惧</p> + <p class="text-xl font-bold">85%</p> + </div> + <div class="p-2 bg-yellow-50 rounded"> + <p class="text-sm text-gray-600">棰勮璁惧</p> + <p class="text-xl font-bold">12%</p> + </div> + <div class="p-2 bg-red-50 rounded"> + <p class="text-sm text-gray-600">鏁呴殰璁惧</p> + <p class="text-xl font-bold">3%</p> + </div> + <div class="p-2 bg-green-50 rounded"> + <p class="text-sm text-gray-600">缁存姢瀹屾垚</p> + <p class="text-xl font-bold">92%</p> + </div> + <div class="p-2 bg-blue-50 rounded"> + <p class="text-sm text-gray-600">寰呭鐞嗗伐鍗�</p> + <p class="text-xl font-bold">{{ healthData.pendingWorkOrders }}</p> + </div> + <div class="p-2 bg-yellow-50 rounded"> + <p class="text-sm text-gray-600">澶囦欢搴撳瓨棰勮</p> + <p class="text-xl font-bold">{{ healthData.sparePartWarnings }}</p> + </div> + </div> + </el-card> + </div> + + <!-- 鏁呴殰棰勬祴涓庨璀� --> + <el-card shadow="hover" class="mb-6"> + <template #header> + <div class="card-header"> + <span>鏁呴殰棰勬祴涓庨璀�</span> + </div> + </template> + <div class="grid grid-cols-1 gap-4"> + <!-- 杩欓噷灏嗘斁缃璀﹁〃鏍� --> + <el-table + :data="warningData" + style="width: 100%" + :border="true" + stripe + > + <el-table-column prop="name" label="璁惧鍚嶇О" /> + <el-table-column prop="component" label="鍏抽敭閮ㄤ欢" /> + <el-table-column prop="indicator" label="椋庨櫓鎸囨爣" /> + <el-table-column prop="value" label="褰撳墠鍊�" /> + <el-table-column prop="threshold" label="闃堝��" /> + <el-table-column prop="status" label="鐘舵��"> + <template #default="{ row }"> + <el-tag :type="getStatusTagType(row.status)"> + {{ row.status }} + </el-tag> + </template> + </el-table-column> + <el-table-column prop="maintenanceSuggestion" label="缁存姢寤鸿" /> + <el-table-column label="鎿嶄綔" width="180"> + <template #default="{ row }"> + <el-button link type="primary" @click="handleDetail(row)">璇︽儏</el-button> + <el-button + link + type="primary" + @click="generateWorkOrder(row)" + :disabled="row.maintenanceSuggestion === '鏆傛棤寤鸿'" + > + 鐢熸垚宸ュ崟 + </el-button> + </template> + </el-table-column> + </el-table> + </div> + </el-card> + + <!-- 澶囦欢淇℃伅 --> + <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> + <!-- 璁惧閮ㄤ欢瀵垮懡棰勬祴 --> + <el-card shadow="hover" class="h-500"> + <template #header> + <div class="card-header"> + <span>璁惧閮ㄤ欢瀵垮懡棰勬祴</span> + </div> + </template> + <el-table + :data="lifePredictionData" + style="width: 100%" + :border="true" + stripe + height="410" + > + <el-table-column prop="deviceName" label="璁惧鍚嶇О" /> + <el-table-column prop="name" label="閮ㄤ欢鍚嶇О" /> + <el-table-column prop="predictedLife" label="棰勬祴瀵垮懡 (澶�)" /> + <el-table-column prop="remainingDays" label="鍓╀綑瀵垮懡 (澶�)" /> + <el-table-column prop="lifeStatus" label="鐘舵��"> + <template #default="{ row }"> + <el-tag :type="getLifeStatusTagType(row.remainingDays)"> + {{ getLifeStatus(row.remainingDays) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="80"> + <template #default="{ row }"> + <el-button link type="primary" @click="showLifePredictionDetail(row)">璇︽儏</el-button> + </template> + </el-table-column> + </el-table> + </el-card> + + <!-- 澶囦欢搴撳瓨涓庨璀� --> + <el-card shadow="hover" class="h-500"> + <template #header> + <div class="card-header"> + <span>澶囦欢搴撳瓨涓庨璀�</span> + </div> + </template> + <el-table + :data="sparePartData" + style="width: 100%" + :border="true" + stripe + height="410" + > + <el-table-column prop="name" label="澶囦欢鍚嶇О" /> + <el-table-column prop="currentStock" label="褰撳墠搴撳瓨" /> + <el-table-column prop="safetyStock" label="瀹夊叏搴撳瓨" /> + <el-table-column prop="predictedDemand" label="棰勬祴闇�姹�" /> + <el-table-column prop="stockStatus" label="搴撳瓨鐘舵��"> + <template #default="{ row }"> + <el-tag :type="getStockStatusTagType(row.currentStock, row.safetyStock)"> + {{ getStockStatus(row.currentStock, row.safetyStock) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="80"> + <template #default="{ row }"> + <el-button link type="primary" @click="showSparePartDetail(row)">璇︽儏</el-button> + </template> + </el-table-column> + </el-table> + </el-card> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from 'vue'; +import { useRouter } from 'vue-router'; +import { ElMessage } from 'element-plus'; +import * as echarts from 'echarts'; + +const chartRef = ref<HTMLElement | null>(null); +const router = useRouter(); + +const healthData = { + healthy: 85, + warning: 12, + critical: 3, + maintained: 92, + pendingWorkOrders: 5, + sparePartWarnings: 2, +}; + +// 妯℃嫙楂橀闄╄澶囨暟鎹� +const warningData = ref([ + { + key: '1', + name: 'SMT璐寸墖鏈�', + type: 'SMT鏈哄櫒', + component: '宸�3-Head', + indicator: '鐪熺┖鍘嬪姏', + value: 32, + threshold: 35, + status: '浣庨闄�', + maintenanceSuggestion: '鐪熺┖鍘嬪姏鍊间綆浜庤瀹氬�硷紝寤鸿妫�鏌ユ垨鏇存崲鍚稿槾', + maintenanceType: '棰勯槻鎬х淮鎶�', + }, + { + key: '2', + name: 'CNC鍔犲伐涓績', + type: 'CNC', + component: '涓昏酱', + indicator: '娓╁害', + value: 85, + threshold: 80, + status: '浣庨闄�', + maintenanceSuggestion: '寤鸿瀵逛富杞磋繘琛屾鼎婊戜繚鍏诲苟妫�鏌ュ喎鍗寸郴缁�', + maintenanceType: '娑︽粦缁存姢', + }, + { + key: '3', + name: '娉ㄥ鏈�', + type: '娉ㄥ璁惧', + component: '娑插帇绯荤粺', + indicator: '灏勫嚭鍘嬪姏涓嶇ǔ瀹�', + value: 28, + threshold: 30, + status: '浣庨闄�', + maintenanceSuggestion: '寤鸿妫�鏌ユ恫鍘嬬郴缁熷瘑灏佷欢', + maintenanceType: '妫�鏌ョ淮鎶�', + }, +]); + +// 妯℃嫙澶囦欢鏁版嵁 +const sparePartData = ref([ + { + key: '1', + name: '浼犻�佸甫杞存壙', + currentStock: 10, + safetyStock: 15, + predictedDemand: 8, + }, + { + key: '2', + name: '涓昏酱娑︽粦娌�', + currentStock: 2, + safetyStock: 3, + predictedDemand: 1, + }, + { + key: '3', + name: '娑插帇绯荤粺瀵嗗皝浠�', + currentStock: 20, + safetyStock: 25, + predictedDemand: 5, + }, + { + key: '4', + name: '鐢垫満纰冲埛', + currentStock: 8, + safetyStock: 10, + predictedDemand: 2, + }, + { + key: '5', + name: '鍠风爜鏈烘补澧�', + currentStock: 1, + safetyStock: 1, + predictedDemand: 1, + }, + { + key: '6', + name: '閽㈢綉娓呮礂娑�', + currentStock: 2, + safetyStock: 1, + predictedDemand: 1, + }, + { + key: '7', + name: '鍗佸绮惧瘑婊よ姱', + currentStock: 20, + safetyStock: 4, + predictedDemand: 0, + }, + { + key: '8', + name: '鍗佸绮惧瘑婊よ姱', + currentStock: 20, + safetyStock: 4, + predictedDemand: 0, + }, + { + key: '9', + name: '鍗佸绮惧瘑婊よ姱', + currentStock: 20, + safetyStock: 4, + predictedDemand: 0, + }, + { + key: '10', + name: '鍗佸绮惧瘑婊よ姱', + currentStock: 20, + safetyStock: 4, + predictedDemand: 0, + }, +]); + +const lifePredictionData = ref([ + { key: '1', deviceName: 'SMT璐寸墖鏈�', componentName: '浼犻�佸甫', name: '鐪熺┖鍙戠敓鍣�', predictedLife: 700, remainingDays: 83 }, + { key: '2', deviceName: '鍖呰鏈�', componentName: '浼犻�佸甫', name: '鍙戠儹涓�', predictedLife: 260, remainingDays: 61 }, + { key: '3', deviceName: '婵�鍏夋墦鏍囨満', componentName: '浼犻�佸甫', name: '鍐峰嵈姘�', predictedLife: 180, remainingDays: 43 }, + { key: '4', deviceName: '閽㈢綉娓呮礂鏈�', componentName: '浼犻�佸甫', name: '娓呮礂娑�', predictedLife: 180, remainingDays: 95 }, + { key: '5', deviceName: '閽㈢綉娓呮礂鏈�', componentName: '浼犻�佸甫', name: '婊よ姱', predictedLife: 180, remainingDays: 95 }, + { key: '6', deviceName: '绔瓙鏈�', componentName: '浼犻�佸甫', name: '鍒�鐗�', predictedLife: 180, remainingDays: 112 }, + { key: '7', deviceName: '鐢佃剳鍓ョ嚎鏈�', componentName: '浼犻�佸甫', name: '鍒�鐗�', predictedLife: 180, remainingDays: 107 }, + { key: '8', deviceName: 'CNC鍔犲伐涓績', componentName: '涓昏酱', name: '鍒�绛掑す', predictedLife: 1100, remainingDays: 130 }, + { key: '9', deviceName: '绌哄帇鏈�', componentName: '瀹夊叏闃�', name: '瀹夊叏闃�', predictedLife: 365, remainingDays: 130 }, + { key: '10', deviceName: '鍠风爜鏈�', componentName: '浼犻�佸甫', name: '澧ㄦ按', predictedLife: 365, remainingDays: 184 }, + { key: '11', deviceName: '娉ㄥ鏈�', componentName: '娑插帇绯荤粺', name: '娑插帇娌�', predictedLife: 1100, remainingDays: 395 }, + { key: '12', deviceName: '鐒婃帴鏈哄櫒浜�', componentName: '鐒婃灙', name: '鐑欓搧鑺�', predictedLife: 1000, remainingDays: 512 } +]); + +const getStatusTagType = (status) => { + switch (status) { + case '楂橀闄�': return 'danger'; + case '涓闄�': return 'warning'; + case '浣庨闄�': return 'primary'; + default: return 'success'; + } +}; + +const getLifeStatus = (remainingDays) => { + if (remainingDays <= 90) { + return '鍗冲皢鍒版湡'; + } else if (remainingDays <= 150) { + return '涓湡棰勮'; + } else { + return '瀵垮懡鍏呰冻'; + } +}; + +const getLifeStatusTagType = (remainingDays) => { + if (remainingDays <= 90) { + return 'warning'; + } else if (remainingDays <= 150) { + return 'primary'; + } else { + return 'success'; + } +}; + +const showLifePredictionDetail = (record) => { + ElMessage.info(`澶囦欢鍚嶇О: ${record.name}, 棰勬祴瀵垮懡: ${record.predictedLife}澶�, 鍓╀綑瀵垮懡: ${record.remainingDays}澶ー); +}; + +const getStockStatus = (currentStock, safetyStock) => { + if (currentStock < safetyStock) { + return '搴撳瓨棰勮'; + } else { + return '搴撳瓨鍏呰冻'; + } +}; + +const getStockStatusTagType = (currentStock, safetyStock) => { + if (currentStock < safetyStock) { + return 'warning'; + } else { + return 'success'; + } +}; + +const showSparePartDetail = (record) => { + ElMessage.info(`澶囦欢鍚嶇О: ${record.name}, 褰撳墠搴撳瓨: ${record.currentStock}, 瀹夊叏搴撳瓨: ${record.safetyStock}, 棰勬祴闇�姹�: ${record.predictedDemand}`); +}; + +const generateWorkOrder = (record) => { + // 宸ュ崟鐢熸垚閫昏緫 + ElMessage.success(`宸蹭负 ${record.name} 鐢熸垚宸ュ崟`); +}; + +const handleDetail = (record) => { + router.push({ path: '/ai/detail', query: { deviceId: 15 } }); +}; + +onMounted(() => { + if (chartRef.value) { + const myChart = echarts.init(chartRef.value); + const option = { + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + xAxis: { + type: 'category', + data: ['10鍒�', '9鍒�', '8鍒�', '7鍒�', '6鍒�', '5鍒�', '4鍒�', '3鍒�', '2鍒�', '1鍒�'], + }, + yAxis: { + type: 'value', + name: '璁惧鏁伴噺', + minInterval: 1 + }, + series: [ + { + name: '璁惧鏁伴噺', + type: 'bar', + barWidth: '60%', + data: [ + { value: 48, itemStyle: { color: '#52c41a' } }, + { value: 53, itemStyle: { color: '#52c41a' } }, + { value: 37, itemStyle: { color: '#52c41a' } }, + { value: 29, itemStyle: { color: '#52c41a' } }, + { value: 21, itemStyle: { color: '#52c41a' } }, + { value: 8, itemStyle: { color: '#faad14' } }, + { value: 5, itemStyle: { color: '#faad14' } }, + ], + label: { + show: true, + position: 'top', + formatter: '{c}鍙�' + } + } + ] + }; + myChart.setOption(option); + + window.addEventListener('resize', () => { + myChart.resize(); + }); + } +}); +</script> + +<style scoped> +.app-container { + padding: 0; +} + +.h-500 { + height: 500px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.grid { + display: grid; +} + +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.gap-4 { + gap: 1rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.p-5 { + padding: 1.25rem; +} + +.w-full { + width: 100%; +} + +.h-400 { + height: 400px; +} + +.bg-blue-50 { + background-color: #eff6ff; +} + +.bg-yellow-50 { + background-color: #fffbeb; +} + +.bg-red-50 { + background-color: #fef2f2; +} + +.bg-green-50 { + background-color: #f0fdf4; +} + +.rounded { + border-radius: 0.25rem; +} + +.text-sm { + font-size: 0.875rem; +} + +.text-xl { + font-size: 1.25rem; +} + +.font-bold { + font-weight: 700; +} + +.text-gray-600 { + color: #4b5563; +} + +@media (min-width: 768px) { + .md\:grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +</style> -- Gitblit v1.9.3