兰宝车间质量管理系统-前端
zhuguifei
14 小时以前 26cd65ace0c8787b5ff6feff3e6270fb371e1a9c
添加质量预测性维护两个页面
已修改1个文件
已添加8个文件
已重命名1个文件
已删除1个文件
3071 ■■■■ 文件已修改
src/api/qms/index/index.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/quailty/order.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/quailty/product.jpg 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/quailty/q1.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/quailty/q2.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/quailty/q3.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/quailty/q4.png 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 926 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qms/ai/detail.vue 856 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qms/quality/detail.vue 1244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qms/quality/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qms/index/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { BatchQuery, BatchVO } from '@/api/qms/batch/types';
/**
 * æŸ¥è¯¢è´¨é‡å¥åº·åº¦
 * @param query
 * @returns {*}
 */
export const queryQualityHealth = (): AxiosPromise<any> => {
  return request({
    url: '/qms/quality/health',
    method: 'get'
  });
};
// æŸ¥è¯¢æ‰¹æ¬¡
export const queryBatchList = (query?: BatchQuery): AxiosPromise<BatchVO[]> => {
  return request({
    url: '/qms/quality/batch',
    method: 'get',
    params: query
  });
};
export const queryPwbatchList = (): AxiosPromise<any> => {
  return request({
    url: '/qms/quality/pwbatch',
    method: 'get'
  });
};
export const queryNgrank = (): AxiosPromise<any> => {
  return request({
    url: '/qms/quality/ngrank',
    method: 'get'
  });
};
src/assets/images/quailty/order.png
src/assets/images/quailty/product.jpg
src/assets/images/quailty/q1.png
src/assets/images/quailty/q2.png
src/assets/images/quailty/q3.png
src/assets/images/quailty/q4.png
src/views/index.vue
@@ -1,85 +1,905 @@
<template>
  <div class="app-container home">
    <el-row :gutter="20">
      <el-col :sm="24" :lg="12" style="padding-left: 20px">
        <h2>兰宝车间质量管理系统</h2>
    <div class="p-5">
      <div style="display: flex" class="gap-4">
      </el-col>
        <el-card style="width: 100%;flex: 1;" class="mb-4">
      <el-col :sm="24" :lg="12" style="padding-left: 20px">
       <div style="display: flex">
         <div>
           <el-statistic  :value="batchScore" :suffix="'分'" :precision="1" :value-style="{fontSize: '30px',fontWeight: 'bold'}">
             <template #title>
               <div style="display: inline-flex; align-items: center;font-size: 18px">
                 ç»¼åˆè´¨é‡è¯„分
                 <el-tooltip effect="dark" content="" placement="top">
                   <el-icon style="margin-left: 4px" :size="12">
                     <Warning />
                   </el-icon>
                 </el-tooltip>
               </div>
             </template>
           </el-statistic>
           <div class="statistic-footer">
             <div class="footer-item">
               <span>较昨天</span>
               <span :class="{ 'green': batchScore > 9.4, 'red': batchScore <= 9.4 }">
                <el-icon v-if="batchScore > 9.4">
                  <CaretTop />
                </el-icon>
                <el-icon v-else>
                  <CaretBottom />
                </el-icon>
              </span>
             </div>
           </div>
         </div>
         <div style="flex: 1;display: flex;justify-content: center;align-items: center">
           <el-image style="width: 60px; height: 60px" :src="q1" fit="contain" />
         </div>
       </div>
      </el-col>
    </el-row>
    <el-divider />
        </el-card>
        <el-card style="flex: 1" class="mb-4">
          <div style="display: flex">
            <div>
              <el-statistic :value="qualityRate" :suffix="'%'" :value-style="{fontSize: '30px',fontWeight: 'bold'}">
                <template #title>
                  <div style="display: inline-flex; align-items: center;font-size: 18px">
                    è‰¯å“çއ
                    <el-tooltip effect="dark" content="" placement="top">
                      <el-icon style="margin-left: 4px" :size="12">
                        <Warning />
                      </el-icon>
                    </el-tooltip>
                  </div>
                </template>
              </el-statistic>
              <div class="statistic-footer">
                <div class="footer-item">
                  <span>较昨天</span>
                  <span :class="{ 'green': qualityRate >= 95, 'red': qualityRate < 95 }">
                <el-icon v-if="qualityRate >= 95">
                  <CaretTop />
                </el-icon>
                <el-icon v-else>
                  <CaretBottom />
                </el-icon>
              </span>
                </div>
              </div>
            </div>
            <div style="flex: 1;display: flex;justify-content: center;align-items: center">
              <el-image style="width: 60px; height: 60px" :src="q2" fit="contain" />
            </div>
          </div>
        </el-card>
        <el-card style="flex: 1" class="mb-4">
          <div style="display: flex">
            <div>
              <el-statistic :value="checkNum" :suffix="'pcs'" :value-style="{fontSize: '30px',fontWeight: 'bold'}">
                <template #title>
                  <div style="display: inline-flex; align-items: center;font-size: 18px">
                    æ£€æµ‹æ•°é‡
                    <el-tooltip effect="dark" content="" placement="top">
                      <el-icon style="margin-left: 4px" :size="12">
                        <Warning />
                      </el-icon>
                    </el-tooltip>
                  </div>
                </template>
              </el-statistic>
              <div class="statistic-footer">
                <div class="footer-item">
                  <span>较昨天</span>
                  <span :class="{ 'green': batchScore > 1, 'red': batchScore <= 1 }">
                <el-icon v-if="batchScore > 1">
                  <CaretTop />
                </el-icon>
                <el-icon v-else>
                  <CaretBottom />
                </el-icon>
              </span>
                </div>
              </div>
            </div>
            <div style="flex: 1;display: flex;justify-content: center;align-items: center">
              <el-image style="width: 60px; height: 60px" :src="q3" fit="contain" />
            </div>
          </div>
        </el-card>
        <el-card style="flex: 1" class="mb-4">
          <div style="display: flex">
            <div>
              <el-statistic :value="ngCountNum" :suffix="'pcs'"  :value-style="{fontSize: '30px',fontWeight: 'bold'}">
                <template #title>
                  <div style="display: inline-flex; align-items: center;font-size: 18px">
                    å¼‚常数量
                    <el-tooltip effect="dark" content="" placement="top">
                      <el-icon style="margin-left: 4px" :size="12">
                        <Warning />
                      </el-icon>
                    </el-tooltip>
                  </div>
                </template>
              </el-statistic>
              <div class="statistic-footer">
                <div class="footer-item">
                  <span>较昨天</span>
                  <span :class="{ 'green': ngCountNum < 50, 'red': ngCountNum  >= 50 }">
                <el-icon v-if="ngCountNum >= 50">
                  <CaretTop />
                </el-icon>
                <el-icon v-else>
                  <CaretBottom />
                </el-icon>
              </span>
                  <span style="margin-left: 10px">预测异常率:</span>
                  <span  class="red" v-if="qualityRate > 80"> {{(((100-qualityRate)/ 3)+0.1).toFixed(1) }}% </span>
                  <span v-else > 3.7%</span>
                </div>
              </div>
            </div>
            <div style="flex: 1;display: flex;justify-content: center;align-items: center">
              <el-image style="width: 60px; height: 60px" :src="q4" fit="contain" />
            </div>
          </div>
        </el-card>
      </div>
      <!-- è®¾å¤‡å¥åº·çŠ¶æ€å¯è§†åŒ– -->
      <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
        <el-card shadow="hover" class="h-420" >
          <template #header>
            <div class="card-header">
              <span>健康度评分</span>
            </div>
          </template>
          <div ref="healthChartRef" class="h-400 w-full"></div>
        </el-card>
        <el-card shadow="hover" class="h-420">
          <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">39.00 V â‰¤ result â‰¤ 49.00 V</p>-->
<!--            </div>-->
<!--            <div class="p-2 bg-yellow-50 rounded">-->
<!--              <p class="text-sm text-gray-600">距离</p>-->
<!--              <p class="text-xl">4.75 mm â‰¤ result â‰¤ 5.15 mm</p>-->
<!--            </div>-->
<!--            <div class="p-2 bg-red-50 rounded">-->
<!--              <p class="text-sm text-gray-600">压降(黑)1</p>-->
<!--              <p class="text-xl">0.30 V â‰¤ result â‰¤ 3.00 V</p>-->
<!--            </div>-->
<!--            <div class="p-2 bg-green-50 rounded">-->
<!--              <p class="text-sm text-gray-600">回差</p>-->
<!--              <p class="text-xl">0.05 mm â‰¤ result â‰¤ 1.00 mm</p>-->
<!--            </div>-->
<!--            <div class="p-2 bg-blue-50 rounded">-->
<!--              <p class="text-sm text-gray-600">漏电流(黑)1</p>-->
<!--              <p class="text-xl">result â‰¤ 800.00 uA</p>-->
<!--            </div>-->
<!--            <div class="p-2 bg-yellow-50 rounded">-->
<!--              <p class="text-sm text-gray-600"></p>-->
<!--              <p class="text-xl font-bold"></p>-->
<!--            </div>-->
<!--          </div>-->
          <el-table size="large"   :data="warnBatchList" style="width: 100%" :border="true" stripe>
            <el-table-column prop="batchTime" label="预警时间" />
            <el-table-column prop="batchCode" label="批次号"  width="140" />
            <el-table-column prop="gw" label="设备/工位"  />
            <el-table-column prop="yc" label="异常类型"   />
            <el-table-column prop="cd" label="严重程度">
              <template #default="{ row }">
                <el-tag :type="getStatusTagType(row.cd)">{{ row.cd }}</el-tag>
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </div>
      <el-card shadow="hover" class="mb-6">
        <template #header>
          <div class="card-header">
            <span>检测批次</span>
            <el-date-picker
              v-model="batchDate"
              type="date"
              placeholder="选择日期"
              :disabled-date="disabledDate"
              :shortcuts="shortcuts"
              @change="changeBatchDate"
            />
          </div>
        </template>
        <div class="grid grid-cols-1 gap-4">
          <!-- è¿™é‡Œå°†æ”¾ç½®é¢„警表格 -->
          <el-table v-loading="loading" :data="batchList" style="width: 100%" :border="true" stripe>
            <el-table-column prop="batchCode" label="批次号" width="180" />
            <el-table-column prop="prodModel" label="产品型号" width="180" />
            <el-table-column prop="num" label="检测数量" width="120" />
            <el-table-column prop="okNum" label="良品数量" width="120" />
            <el-table-column prop="ngNum" label="不良数量" width="120" />
            <el-table-column prop="lpv" label="良品率" width="120">
              <template #default="{ row }">
                <el-tag :type="goodProductsRate(row) >= 95 ? 'success' : 'warning'"> {{ goodProductsRate(row) }} % </el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="whjy" label="维护建议">
              <template #default="{ row }">
                <el-tag :type="goodProductsRate(row) >= 95 ? 'success' : 'warning'">
                  {{ goodProductsRate(row) >= 95 ? '良品率指标已优异,请关注设备综合效率' : '预警,检查工艺参数是否符合标准,优化关键参数' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="180">
              <template #default="{ row }">
                <el-button link type="primary" @click="handleDetail(row)">详情</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-420">
          <template #header>
            <div class="card-header">
              <span>批次合格率趋势</span>
            </div>
          </template>
          <div ref="batchRateTrendRef" class="h-400 w-full"></div>
        </el-card>
        <el-card shadow="hover" class="h-420">
          <template #header>
            <div class="card-header">
              <span>近一周NG项TOP5</span>
            </div>
          </template>
          <div ref="ngChartRef" class="h-400 w-full"></div>
        </el-card>
      </div>
    </div>
  </div>
</template>
<script setup name="Index" lang="ts">
const goTarget = (url: string) => {
  window.open(url, '__blank');
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import * as echarts from 'echarts';
import q1 from '@/assets/images/quailty/q1.png';
import q2 from '@/assets/images/quailty/q2.png';
import q3 from '@/assets/images/quailty/q3.png';
import q4 from '@/assets/images/quailty/q4.png';
import { queryQualityHealth, queryBatchList,queryPwbatchList ,queryNgrank} from '@/api/qms/index';
import { BatchVO } from '@/api/qms/batch/types';
import { listBatch } from '@/api/qms/batch';
const batchScore = ref(0);
const qualityRate = ref(0);
const checkNum = ref(0);
const ngCountNum = ref(0);
const batchDate = ref('');
const healthChartRef = ref<HTMLElement | null>(null);
const batchRateTrendRef = ref<HTMLElement | null>(null);
const ngChartRef = ref<HTMLElement | null>(null);
const healthChart = ref();
const batchRateChart = ref();
const ngChart = ref();
const router = useRouter();
const loading = ref(false);
const batchList = ref<BatchVO[]>([]);
const warnBatchList = ref<any>([]);
const gwList = [
  "微机电系统蚀刻机",
  "晶圆键合机",
  "传感器校准站",
  "薄膜沉积设备",
  "光刻机",
  "离子注入机",
  "化学机械抛光设备",
  "热处理炉",
  "封装测试站",
  "激光修调设备",
  "电性能测试台",
  "光学检测仪",
  "真空镀膜机",
  "超声波清洗机",
  "X射线检测仪"
]
const ycList =  [
  "蚀刻深度偏差",
  "键合压力异常",
  "校准数据偏移",
  "沉积速率波动",
  "光刻对准偏差",
  "离子注入剂量异常",
  "抛光厚度不均",
  "温度稳定性异常",
  "封装密封性不良",
  "电阻值偏差",
  "灵敏度异常",
  "响应时间超限",
  "信号漂移异常",
  "线性度偏差",
  "零点漂移异常",
  "过载恢复异常",
  "绝缘电阻不足",
  "介质耐压不合格",
  "频率响应异常",
  "信噪比不达标"
];
const cdList = [
  "高", "高", "中", "中", "高",
  "中", "中", "高", "高", "中",
  "高", "中", "中", "中", "低",
  "中", "高", "高", "中", "低"
];
const getStatusTagType = (status: string) => {
  switch (status) {
    case '高': return 'danger';
    case '中': return 'warning';
    case '低': return 'info';
  }
};
function changeBatchDate(date: Date){
  getBatch(date ? date : new Date())
}
async function queryWarnBatchList(){
  const queryParams = {
    pageNum: 1,
    pageSize: 6
  };
  const res:any = await listBatch(queryParams);
  if(res && res.rows){
    warnBatchList.value = [];
    res.rows.forEach((item,index) => {
      item.gw = gwList[index];
      item.yc = ycList[index];
      item.cd = cdList[index];
      item.batchTime = item.batchTime.substring(0,10);
      warnBatchList.value.push(item);
    })
  }
}
const shortcuts = [
  {
    text: '今天',
    value: new Date(),
  },
  {
    text: '昨天',
    value: () => {
      const date = new Date()
      date.setTime(date.getTime() - 3600 * 1000 * 24)
      return date
    },
  },
  {
    text: '一周前',
    value: () => {
      const date = new Date()
      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
      return date
    },
  },
]
const disabledDate = (time: Date) => {
  return time.getTime() > Date.now()
}
const goodProductsRate = (row) => {
  if (row.num > 0 && row.okNum > 0) {
    return Math.round((row.okNum / row.num) * 100);
  }
};
const generateWorkOrder = (record) => {
  // å·¥å•生成逻辑
  ElMessage.success(`已为 ${record.name} ç”Ÿæˆå·¥å•`);
};
const handleDetail = (record) => {
  router.push({ path: '/quality/detail', query: { id: record.id } });
};
const initHealthChart = () => {
  if (healthChartRef.value) {
    healthChart.value = echarts.init(healthChartRef.value);
    const option = {
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'shadow'
        }
      },
      xAxis: {
        type: 'category',
        data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
      },
      yAxis: {
        type: 'value',
        name: '得分',
        minInterval: 1
      },
      series: [
        {
          name: '质量数据',
          type: 'bar',
          barWidth: '50%',
          data: [
            { value: 9.8, itemStyle: { color: '#52c41a' } },
            { value: 9.9, itemStyle: { color: '#52c41a' } },
            { value: 9.7, itemStyle: { color: '#52c41a' } },
            { value: 9.8, itemStyle: { color: '#52c41a' } },
            { value: 9.9, itemStyle: { color: '#52c41a' } },
            { value: 9.8, itemStyle: { color: '#52c41a' } },
            { value: 9.9, itemStyle: { color: '#52c41a' } },
            { value: 9.8, itemStyle: { color: '#52c41a' } },
            { value: 9.8, itemStyle: { color: '#52c41a' } }
          ],
          label: {
            show: true,
            position: 'top',
            formatter: '{c}分'
          }
        }
      ]
    };
    healthChart.value.setOption(option);
    window.addEventListener('resize', () => {
      healthChart.value.resize();
    });
  }
};
const initBatchRateChart = () => {
  if (batchRateTrendRef.value) {
    batchRateChart.value = echarts.init(batchRateTrendRef.value);
    const option = {
      color: ['#FFBF00'],
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'cross',
          label: {
            backgroundColor: '#6a7985'
          }
        }
      },
      xAxis: [
        {
          splitLine: { show: false },
          type: 'category',
          boundaryGap: false,
          data: ['08-01', '08-02', '08-03', '08-04', '08-05', '08-06', '08-07']
        }
      ],
      yAxis: [
        {
          splitLine: { show: false },
          type: 'value',
          name: '检测合格率',
          min: 80, // è®¾ç½®æœ€å°å€¼ä¸º90
          max: 100, // è®¾ç½®æœ€å¤§å€¼ä¸º100
          interval: 5, // è®¾ç½®åˆ»åº¦é—´éš”为1
          axisLabel: {
            formatter: '{value}%' // æ·»åŠ ç™¾åˆ†æ¯”ç¬¦å·
          }
        }
      ],
      series: [
        {
          name: '',
          type: 'line',
          stack: 'Total',
          smooth: true,
          lineStyle: {
            width: 0
          },
          showSymbol: false,
          areaStyle: {
            opacity: 0.8,
            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
              {
                offset: 0,
                color: 'rgb(128, 255, 165)'
              },
              {
                offset: 1,
                color: 'rgb(1, 191, 236)'
              }
            ])
          },
          emphasis: {
            focus: 'series'
          },
          data: [99, 98, 95, 99, 92, 98, 96],
        }
      ]
    };
    option && batchRateChart.value.setOption(option);
    window.addEventListener('resize', () => {
      batchRateChart.value.resize();
    });
  }
};
const initNgChart = () => {
  if (ngChartRef.value) {
    ngChart.value = echarts.init(ngChartRef.value);
    const option = {
      tooltip: {
        trigger: 'axis',
        axisPointer: {
          type: 'shadow'
        }
      },
      grid: {
        left: '120px', // å¢žåŠ å·¦ä¾§è¾¹è·ï¼Œä¸ºæ ‡ç­¾ç•™å‡ºæ›´å¤šç©ºé—´
        top: '0px',
      },
      xAxis: {
        type: 'value',
        boundaryGap: [0, 0.01],
        splitLine: { show: false }
      },
      yAxis: {
        splitLine: { show: false },
        type: 'category',
        data: [
          '静态消耗电流2',
          '回差',
          '短路保护消耗电流(黑)2',
          '动态消耗电流2',
          '距离',
          '压降(黑)2'
        ]
      },
      series: [
        {
          name: '',
          type: 'bar',
          barWidth: '60%',
          data: [100, 90, 80, 70, 60, 10],
          itemStyle: {
            color: '#faad14'  // ä¿®æ”¹æŸ±çŠ¶å›¾é¢œè‰²
          },
          label: {
            show: true,
            position: 'right',
            formatter: '{c}次'
          }
        }
      ]
    };
    option && ngChart.value.setOption(option);
    window.addEventListener('resize', () => {
      ngChart.value.resize();
    });
  }
};
const getHealth = async () => {
  const res: any = await queryQualityHealth();
  if (res && res.yAxis) {
    const xAxisData = [];
    const minValue = res.yAxis.reduce((min, item) => {
      return min === null ? item.value : Math.min(min, item.value);
    }, null);
    res.yAxis.forEach((item, index) => {
      if (item.value >= 9.5) {
        item.itemStyle = { color: '#52c41a' };
      } else if (item.value < 9.5 && item.value >= 9.0) {
        item.itemStyle = { color: '#faad14' };
      } else {
        item.itemStyle = { color: '#f5222d' };
      }
      if (item.value == minValue) {
        item.itemStyle = { color: '#faad14' };
      }
      xAxisData.push(item);
    });
    updateHealthData(res.xAxis, xAxisData);
  }
};
const getBatch = async (date = new Date()) => {
  loading.value = true;
  const res: any = await queryBatchList({
    pageNum: 1,
    pageSize: 10,
    params: {
      startTime: formatDate(date) + ' 00:00:00',
      endTime: formatDate(date) + ' 23:59:59'
    }
  });
  if (!res || !res.rows) {
    return;
  }
  batchList.value = res.rows;
  checkNum.value = res.rows.reduce((sum, item) => sum + (item.num || 0), 0);
  ngCountNum.value = res.rows.reduce((sum, item) => sum + (item.ngNum || 0), 0);
  // è®¡ç®—oknum总和
  const totalOkNum = res.rows.reduce((sum, item) => sum + (item.okNum || 0), 0);
  // è®¡ç®—良品率 (避免除以0的情况)
  const yieldRate = checkNum.value > 0 ? (totalOkNum / checkNum.value) * 100 : 0;
  qualityRate.value = Number(yieldRate.toFixed(2));
  batchScore.value =  Number(((yieldRate/10 )-0.1).toFixed(1));
  if(batchScore.value == -0.1){
    batchScore.value = 0;
  }
  loading.value = false;
};
const getPwbatch = async () => {
  const res: any = await queryPwbatchList();
  if (res && res.yAxis) {
    updatePwbatchData(res.xAxis, res.yAxis);
  }
};
const getNgrank = async () => {
  const res: any = await queryNgrank();
  if (res && res.yAxis) {
    updateNgrankData(res.xAxis, res.yAxis);
  }
};
const formatDate = (date) => {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
};
function updateHealthData(xAxis, yAxis) {
  healthChart.value.setOption({
    xAxis: {
      type: 'category',
      data: xAxis
    },
    series: [
      {
        data: yAxis
      }
    ]
  });
}
function updatePwbatchData(xAxis, yAxis) {
  batchRateChart.value.setOption({
    xAxis: {
      type: 'category',
      data: xAxis
    },
    series: [
      {
        data: yAxis
      }
    ]
  });
}
function updateNgrankData(xAxis, yAxis) {
  ngChart.value.setOption({
    yAxis: {
      type: 'category',
      data: xAxis,
      inverse: true,
    },
    series: [
      {
        data: yAxis
      }
    ]
  });
}
onMounted(() => {
  queryWarnBatchList();
  initHealthChart();
  initBatchRateChart();
  initNgChart();
  getHealth();
  getBatch();
  getPwbatch();
  getNgrank();
});
</script>
<style scoped lang="scss">
.home {
  blockquote {
    padding: 10px 20px;
    margin: 0 0 20px;
    font-size: 17.5px;
    border-left: 5px solid #eee;
  }
  hr {
    margin-top: 20px;
    margin-bottom: 20px;
    border: 0;
    border-top: 1px solid #eee;
  }
  .col-item {
    margin-bottom: 20px;
.h-500 {
  height: 500px;
  }
  ul {
    padding: 0;
    margin: 0;
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: bold;
  }
  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-size: 13px;
  color: #676a6c;
  overflow-x: hidden;
  ul {
    list-style-type: none;
.grid {
  display: grid;
  }
  h4 {
    margin-top: 0px;
.grid-cols-1 {
  grid-template-columns: repeat(1, minmax(0, 1fr));
  }
  h2 {
    margin-top: 10px;
    font-size: 26px;
    font-weight: 100;
.grid-cols-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  p {
    margin-top: 10px;
.gap-4 {
  gap: 1rem;
}
    b {
.mb-6 {
  margin-bottom: 1.5rem;
}
.p-5 {
  padding: 1.25rem;
}
.w-full {
  width: 100%;
}
.h-400 {
  height: 400px;
}
.h-420 {
  height: 420px;
}
.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:14px;
}
.text-xl {
  font-size: 14px;
}
.font-bold {
      font-weight: 700;
    }
.text-gray-600 {
  color: #4b5563;
  }
  .update-log {
    ol {
      display: block;
      list-style-type: decimal;
      margin-block-start: 1em;
      margin-block-end: 1em;
      margin-inline-start: 0;
      margin-inline-end: 0;
      padding-inline-start: 40px;
@media (min-width: 768px) {
  .md\:grid-cols-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
.statistic-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  font-size: 12px;
  color: var(--el-text-color-regular);
  margin-top: 16px;
}
.statistic-footer .footer-item {
  font-size: 14px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.statistic-footer .footer-item span:last-child {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
}
.green {
  color: var(--el-color-success);
}
.red {
  color: var(--el-color-error);
}
</style>
src/views/qms/ai/detail.vue
ÎļþÒÑɾ³ý
src/views/qms/quality/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1244 @@
<template>
  <div class="device-detail-container">
    <el-page-header @back="goBack">
      <template #content>
        <span class="text-large font-600 mr-3"> æ‰¹æ¬¡è¯¦æƒ…分析</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="img" alt="设备图片" class="device-image" />
              </div>
            </el-col>
            <el-col :span="12">
              <el-descriptions border :column="1"  class="custom-descriptions">
                <el-descriptions-item label="批次编号">{{ batch.batchCode }}</el-descriptions-item>
                <el-descriptions-item label="产品型号">{{ batch.prodModel }}</el-descriptions-item>
                <el-descriptions-item label="生产线">A线</el-descriptions-item>
                <el-descriptions-item label="生产时间">{{ batch.batchTime }}</el-descriptions-item>
                <el-descriptions-item label="生产状态">
                  <el-tag v-if="batchIsToday(batch.batchTime)" type="primary">生产中</el-tag>
                  <el-tag v-else type="success">已完成</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="healthRate"
                :precision="0"
                :suffix="'%'"
                :value-style="{ color: healthData.healthColor }"
              />
            </el-col>
            <el-col :span="8">
              <el-statistic
                title="过程能力指数 (Cpk)"
                :precision="2"
                :value="healthData.predictedLife"
              />
            </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="8" style="display: flex;justify-content: center;align-items: center">
              <el-statistic title="本批良率" :value="healthRate" :precision="2" :suffix="'%'">
                <template #prefix>
<!--                  <el-icon  style="vertical-align: -0.125em"><TrendCharts /></el-icon>-->
                </template>
              </el-statistic>
            </el-col>
            <el-col :span="8" style="display: flex;justify-content: center;align-items: center">
              <el-statistic title="不良品数" :value="batch.ngNum"   :suffix="'pcs'">
                <template #prefix>
<!--                  <el-icon  style="vertical-align: -0.125em"><TrendCharts /></el-icon>-->
                </template>
              </el-statistic>
            </el-col>
            <el-col :span="8" style="display: flex;justify-content: center;align-items: center">
              <el-statistic title="低于平均水平" :value="belowRate" :precision="2" :suffix="'%'">
                <template #prefix>
<!--                  <el-icon  style="vertical-align: -0.125em"><TrendCharts /></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>
              <el-tooltip content="基于设备运行数据和传感器监测的预测性维护分析" placement="top">
                <el-icon><Warning /></el-icon>
              </el-tooltip>
            </div>
          </template>
          <div class="health-summary">
            <el-row :gutter="16">
              <el-col :span="8" class="summary-item">
                <div class="summary-value" style="color: #67C23A;">{{ healthyCount }}</div>
                <div class="summary-label">健康</div>
              </el-col>
              <el-col :span="8" class="summary-item">
                <div class="summary-value" style="color: #E6A23C;">{{ warningCount }}</div>
                <div class="summary-label">预警</div>
              </el-col>
              <el-col :span="8" class="summary-item">
                <div class="summary-value" style="color: #F56C6C;">{{ criticalCount }}</div>
                <div class="summary-label">紧急</div>
              </el-col>
            </el-row>
          </div>
          <el-table
            :data="sensorComponentData"
            stripe
            :row-class-name="tableRowClassName"
          >
            <el-table-column prop="name" label="部件名称"   />
            <el-table-column label="健康状态"  >
              <template #default="{ row }">
                <el-tag
                  :type="getHealthStatusType(row.healthStatus)"
                  size="small"
                >
                  {{ row.healthStatus }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="剩余寿命" >
              <template #default="{ row }">
                <div class="life-progress">
                  <el-progress
                    :percentage="row.remainingPercentage"
                    :color="getProgressColor(row.remainingPercentage)"
                    :show-text="false"
                    :stroke-width="12"
                  />
                  <span class="life-text">{{ row.remainingLife }}</span>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="预测更新时间" width="120">
              <template #default="{ row }">
                {{ row.lastUpdate }}
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80">
              <template #default="{ row }">
                <el-button
                  link
                  type="primary"
                  size="small"
                  @click="showComponentDetail(row)"
                >
                  è¯¦æƒ…
                </el-button>
              </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 setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
import { useRouter,useRoute } 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/quailty/product.jpg'
import { getBatch } from '@/api/qms/batch';
import { BatchVO } from '@/api/qms/batch/types';
const batch= ref<BatchVO>({});
const router = useRouter();
const route = useRoute();
const batchId = route.query.id
const healthRate = ref(0);
const belowRate = ref(0);
const goBack = () => {
  router.go(-1);
};
function getBatchDetail(){
  getBatch(batchId).then(res => {
    if(res){
      batch.value = res.data;
      initCharts2();
     if(batch.value && batch.value.num > 0 && batch.value.okNum > 0){
       const yieldRate =   (batch.value.okNum / batch.value.num) * 100;
       healthRate.value =  Number(yieldRate.toFixed(1));
       if(healthRate.value < 80 ){
         belowRate.value =  10.11;
         healthData.riskLevel = "高风险"
         healthData.riskColor = "#f56c6c"
       }else if(healthRate.value>=80&& healthRate.value<90){
         healthData.riskLevel = "中风险"
         healthData.riskColor = "#faad14"
         belowRate.value =  6.37;
       } else if(healthRate.value>90&& healthRate.value<95){
         belowRate.value =  3.15;
       }  else if(healthRate.value>95&& healthRate.value<=100){
         belowRate.value = 0.32 ;
       }else {
         belowRate.value = 0;
       }
     }
    }
  })
}
const healthData = reactive({
  overallHealth: 82,
  healthColor: '#52c41a',
  predictedLife: 1.31,
  riskLevel: '低风险',
  riskColor: '#52c41a',
  xAxisTravel: 300.179,
  yAxisTravel: 233.39,
  tapeJamCount: 6,
  materialJamCount: 15,
  panelCount: 2480,
  downtime: 4.5,
});
// ç”Ÿæˆç›¸å¯¹æ—¶é—´å‡½æ•°
const generateRelativeTime = (daysAgo) => {
  const date = new Date();
  date.setDate(date.getDate() - daysAgo);
  return date.toISOString().split('T')[0];
};
const maintenanceData = reactive([
  {
    key: '1',
    type: '1号贴装系统维护',
    content: '吸嘴真空压力偏低,需要清洁或更换吸嘴',
    suggestedTime: generateRelativeTime(3),
    urgency: '中等'
  },
  {
    key: '2',
    type: '2号回流焊炉维护',
    content: '温区温度波动超过标准范围,需要校准温度传感器',
    suggestedTime: generateRelativeTime(1),
    urgency: '低'
  },
  {
    key: '3',
    type: '3号AOI检测仪维护',
    content: '相机镜头有灰尘,影响检测精度,需要清洁镜头',
    suggestedTime: generateRelativeTime(2),
    urgency: '低'
  },
  {
    key: '4',
    type: '4号锡膏印刷机维护',
    content: '刮刀压力不均匀,需要调整或更换刮刀',
    suggestedTime: generateRelativeTime(3),
    urgency: '中等'
  },
  {
    key: '5',
    type: '5号SPI检测仪维护',
    content: '激光测量模块需要重新校准',
    suggestedTime: generateRelativeTime(5),
    urgency: '中等'
  },
  {
    key: '6',
    type: '6号X-Ray检测仪维护',
    content: 'X射线源工作时间接近维护周期,需要预防性维护',
    suggestedTime: generateRelativeTime(4),
    urgency: '低'
  }
]);
const getUrgencyTagType = (urgency: string) => {
  switch (urgency) {
    case '高': return 'danger';
    case '中等': return 'warning';
    case '低': return 'success';
    default: return 'info';
  }
};
const batchIsToday = (date: string) => {
  const today = new Date();
  const inputDate = new Date(date);
  return inputDate.toDateString() === today.toDateString();
};
const getTimelineItemType = (color: string) => {
  switch (color) {
    case 'green': return 'success';
    case 'red': return 'danger';
    default: return 'primary';
  }
};
const sparePartData = reactive([
  {
    key: '1',
    name: '1号贴装系统T轴伺服电机',
    currentLife: '15000小时',
    remainingLife: '1451小时',
    status: '预警'
  },
  {
    key: '5',
    name: '1号贴装系统Z轴伺服电机',
    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号贴装系统T轴伺服电机',
    currentLife: '15000小时',
    remainingLife: '7540小时',
    status: '良好'
  },
  {
    key: '7',
    name: '2号贴装系统Z轴伺服电机',
    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-09-12 14:25:36',
    type: '蚀刻深度异常',
    description: '微机电系统蚀刻机检测到蚀刻深度异常:2.8μm (正常范围: 2.3-2.5μm)',
    color: 'red'
  },
  {
    id: '2',
    date: '2025-08-21 13:40:22',
    type: '温度稳定性异常',
    description: '热处理炉温度波动超过允许范围±0.5°C,达到±0.8°C',
    color: 'orange'
  },
  {
    id: '3',
    date: '2025-08-03 12:15:48',
    type: '电阻值偏差',
    description: '检测到传感器电阻值超出公差范围±5%,实际偏差为+7.2%',
    color: 'red'
  },
  {
    id: '4',
    date: '2025-07-12 11:30:15',
    type: '沉积速率波动',
    description: '薄膜沉积设备沉积速率不稳定,波动幅度达到±8%',
    color: 'orange'
  },
  {
    id: '5',
    date: '2025-06-01 10:45:33',
    type: '校准数据偏移',
    description: '传感器校准站检测到零点漂移0.3mV,需要重新校准',
    color: 'yellow'
  },
  {
    id: '6',
    date: '2025-04-17 09:20:57',
    type: '键合压力异常',
    description: '晶圆键合机压力传感器读数异常,实际压力比设定值低12%',
    color: 'orange'
  },
  {
    id: '7',
    date: '2025-03-26 08:55:12',
    type: '光学检测异常',
    description: '光学检测仪发现3个传感器表面存在微小划痕',
    color: 'yellow'
  },
  {
    id: '8',
    date: '2025-01-3 08:30:45',
    type: '真空度不足',
    description: '真空镀膜机真空度下降至0.005Pa,低于标准0.001Pa',
    color: 'red'
  },
]);
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(() => {
  getBatchDetail()
  nextTick(() => {
    startAutoScroll(); // å¯åŠ¨è‡ªåŠ¨æ»šåŠ¨
  });
});
onUnmounted(() => {
  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' }
  ]);
};
const initCharts2 = () => {
  // ç”Ÿæˆæ—¶é—´è½´æ•°æ®
  const generateTimeAxis = (isToday) => {
    const baseTimes = ['08:30', '09:30', '10:30', '11:30', '12:30', '13:30', '14:30', '15:30', '16:30', '17:00'];
    if (!isToday) {
      return baseTimes;
    }
    const now = new Date();
    const currentHour = now.getHours();
    const currentMinute = now.getMinutes();
    const currentTime = `${currentHour}:${currentMinute.toString().padStart(2, '0')}`;
    // æ‰¾åˆ°å½“前时间在基础时间数组中的位置
    const currentIndex = baseTimes.findIndex(time => {
      const [hour, minute] = time.split(':').map(Number);
      const timeInMinutes = hour * 60 + minute;
      const currentInMinutes = currentHour * 60 + currentMinute;
      return timeInMinutes >= currentInMinutes;
    });
    // å¦‚果当前时间早于8:30,返回完整数组
    if (currentIndex === -1) {
      return baseTimes;
    }
    // æˆªå–到当前时间之前的时间点
    return baseTimes.slice(0, currentIndex + 1);
  };
  // æˆªå–数据以匹配时间轴长度
  const truncateData = (data, targetLength) => {
    return data.slice(0, targetLength);
  };
  // å›¾è¡¨åˆå§‹åŒ–代码
  const initChart = (chartId, title, seriesConfig, isToday) => {
    const chart = echarts.init(document.getElementById(chartId));
    const timeAxis = generateTimeAxis(isToday);
    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: timeAxis
      },
      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: truncateData(s.data, timeAxis.length),
        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);
    window.addEventListener('resize', () => {
      chart.resize();
    });
  };
  // å‡è®¾æœ‰ä¸€ä¸ªå˜é‡è¡¨ç¤ºæ˜¯å¦æ˜¯ä»Šå¤©
  const isToday = batchIsToday(batch.value.batchTime); // å¯ä»¥æ ¹æ®å®žé™…情况设置为true或false
  // åˆå§‹åŒ–各个图表
  initChart('motorTemperatureChart', '蚀刻深度(μm)', [
    {
      name: '蚀刻深度',
      data: [2.35, 2.38, 2.42, 2.45, 2.51, 2.48, 2.52, 2.55, 2.58, 2.60],
      unit: 'μm',
      color: '#5470C6'
    },
  ], isToday);
  initChart('motorVibrationChart', '键合压力(MPa)', [
    {
      name: '键合压力',
      data: [15.2, 15.5, 15.8, 16.1, 15.9, 16.2, 16.0, 16.3, 16.1, 16.4],
      unit: 'MPa',
      color: '#91cc75'
    },
  ], isToday);
  initChart('nozzleVacuumChart', '沉积速率(Å/min)', [
    {
      name: '沉积速率',
      data: [120, 118, 122, 125, 128, 126, 124, 127, 129, 131],
      unit: 'Å/min',
      color: '#fac858'
    },
  ], isToday);
  initChart('nozzleFlowChart', '温度控制(°C)', [
    {
      name: '温度',
      data: [350, 352, 355, 358, 356, 354, 351, 353, 357, 359],
      unit: '°C',
      color: '#ee6666'
    },
  ], isToday);
  initChart('placementSpeedChart', '真空度(Pa)', [
    {
      name: '真空度',
      data: [0.0012, 0.0015, 0.0018, 0.0021, 0.0019, 0.0020, 0.0017, 0.0022, 0.0020, 0.0018],
      unit: 'Pa',
      color: '#73c0de'
    },
  ], isToday);
  initChart('ambientTemperatureHumidityChart', '电流密度(A/cm²)', [
    {
      name: '电流密度',
      data: [2.1, 2.2, 2.3, 2.4, 2.35, 2.38, 2.32, 2.36, 2.39, 2.42],
      unit: 'A/cm²',
      color: '#3ba272'
    }
  ], isToday);
};
// èŽ·å–éšæœºå¤©æ•°ï¼ˆç”¨äºŽæ¨¡æ‹Ÿä¸åŒçš„æ›´æ–°æ—¶é—´ï¼‰
const getRandomDaysAgo = (min, max) => {
  return Math.floor(Math.random() * (max - min + 1)) + min;
};
const sensorComponentData = reactive([
  {
    id: '1',
    name: '蚀刻反应腔室',
    healthStatus: '良好',
    remainingLife: '285天',
    remainingPercentage: 85,
    totalLife: '5å¹´',
    currentUsage: '2.5å¹´',
    lastUpdate: generateRelativeTime(3),
    maintenanceHistory: '上次维护: 2025-06-15'
  },
  {
    id: '2',
    name: '晶圆传输机械臂',
    healthStatus: '预警',
    remainingLife: '45天',
    remainingPercentage: 25,
    totalLife: '80万次',
    currentUsage: '60万次',
    lastUpdate: generateRelativeTime(7),
    maintenanceHistory: '上次维护: 2025-07-20'
  },
  {
    id: '3',
    name: '真空泵系统',
    healthStatus: '紧急',
    remainingLife: '7天',
    remainingPercentage: 5,
    totalLife: '20000小时',
    currentUsage: '19500小时',
    lastUpdate: generateRelativeTime(2),
    maintenanceHistory: '上次维护: 2025-03-10'
  },
  {
    id: '4',
    name: '温度控制系统',
    healthStatus: '良好',
    remainingLife: '180天',
    remainingPercentage: 70,
    totalLife: '3å¹´',
    currentUsage: '1.5å¹´',
    lastUpdate: generateRelativeTime(4),
    maintenanceHistory: '上次维护: 2025-05-22'
  },
  {
    id: '5',
    name: '化学沉积喷头',
    healthStatus: '预警',
    remainingLife: '30天',
    remainingPercentage: 20,
    totalLife: '1000批次',
    currentUsage: '850批次',
    lastUpdate: generateRelativeTime(3),
    maintenanceHistory: '上次维护: 2025-04-18'
  },
  {
    id: '6',
    name: '光学检测镜头',
    healthStatus: '良好',
    remainingLife: '365天',
    remainingPercentage: 90,
    totalLife: '4å¹´',
    currentUsage: '1å¹´',
    lastUpdate: generateRelativeTime(18),
    maintenanceHistory: '上次维护: 2025-01-15'
  },
  {
    id: '7',
    name: '离子注入源',
    healthStatus: '紧急',
    remainingLife: '14天',
    remainingPercentage: 8,
    totalLife: '15000小时',
    currentUsage: '14500小时',
    lastUpdate: generateRelativeTime(2),
    maintenanceHistory: '上次维护: 2024-12-05'
  }
]);
// è®¡ç®—健康状态统计
const healthyCount = computed(() =>
  sensorComponentData.filter(item => item.healthStatus === '良好').length
);
const warningCount = computed(() =>
  sensorComponentData.filter(item => item.healthStatus === '预警').length
);
const criticalCount = computed(() =>
  sensorComponentData.filter(item => item.healthStatus === '紧急').length
);
// è¡¨æ ¼è¡Œæ ·å¼
const tableRowClassName = ({ row }) => {
  if (row.healthStatus === '紧急') {
    return 'warning-row';
  } else if (row.healthStatus === '预警') {
    return 'warning-row-light';
  }
  return '';
};
// èŽ·å–å¥åº·çŠ¶æ€æ ‡ç­¾ç±»åž‹
const getHealthStatusType = (status) => {
  const statusMap = {
    '良好': 'success',
    '预警': 'warning',
    '紧急': 'danger'
  };
  return statusMap[status] || 'info';
};
// èŽ·å–è¿›åº¦æ¡é¢œè‰²
const getProgressColor = (percentage) => {
  if (percentage > 60) return '#67C23A';
  if (percentage > 30) return '#E6A23C';
  return '#F56C6C';
};
// æ˜¾ç¤ºéƒ¨ä»¶è¯¦æƒ…
const showComponentDetail = (component) => {
  ElMessageBox.confirm(
    `部件名称: ${component.name}
健康状态: ${component.healthStatus}
剩余寿命: ${component.remainingLife}
总设计寿命: ${component.totalLife}
当前使用: ${component.currentUsage}
${component.maintenanceHistory}`,
    '部件详情',
    {
      confirmButtonText: '确定',
      cancelButtonText: '关闭',
      type: 'info'
    }
  );
};
</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部分添加以下样式 */
.life-progress {
  display: flex;
  align-items: center;
  gap: 8px;
}
.life-text {
  font-size: 12px;
  color: #606266;
  min-width: 40px;
}
.health-summary {
  padding: 10px 0;
}
.summary-item {
  text-align: center;
  padding: 8px 0;
}
.summary-value {
  font-size: 24px;
  font-weight: bold;
  margin-bottom: 4px;
}
.summary-label {
  font-size: 12px;
  color: #909399;
}
:deep(.warning-row) {
  --el-table-tr-bg-color: var(--el-color-danger-light-9);
}
:deep(.warning-row-light) {
  --el-table-tr-bg-color: var(--el-color-warning-light-9);
}
:deep(.el-table .warning-row:hover > td) {
  background-color: var(--el-color-danger-light-7) !important;
}
:deep(.el-table .warning-row-light:hover > td) {
  background-color: var(--el-color-warning-light-7) !important;
}
</style>
src/views/qms/quality/index.vue
ÎļþÃû´Ó src/views/qms/ai/index.vue ÐÞ¸Ä
@@ -6,7 +6,7 @@
        <el-card shadow="hover" class="h-full">
          <template #header>
            <div class="card-header">
              <span>设备健康度评分</span>
              <span>质量健康度评分</span>
            </div>
          </template>
          <!-- è¿™é‡Œå°†æ”¾ç½®å¥åº·åº¦åˆ†å¸ƒå›¾è¡¨ -->