feat(report): 报表图表管理
1.报表管理(样品进度报表,检测项目数据,样品领样记录,设备使用记录)
2.数字化语音看板
3.智能图表
已添加18个文件
3060 ■■■■■ 文件已修改
src/api/report/dashboard.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/deviceRecord.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/normalDistribution.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/passRate.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/sampleProgress.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/sampleRecord.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/spcChart.js 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/testItemData.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/workStatistics.js 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/dashboard/index.vue 496 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/deviceRecord/index.vue 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/normalDistribution/index.vue 303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/passRate/index.vue 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/sampleProgress/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/sampleRecord/index.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/spcChart/index.vue 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/testItemData/index.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/report/workStatistics/index.vue 326 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/report/dashboard.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
// æ•°å­—化语音看板 - è¯•验大厅接口
import request from '@/utils/request'
// èŽ·å–çœ‹æ¿æ¦‚è§ˆæ•°æ®
export function getOverview(query) {
  return request({
    url: '/report/dashboard/overview',
    method: 'get',
    params: query
  })
}
// åŽ†å²15天检测任务数据
export function getHistory15Days(query) {
  return request({
    url: '/report/dashboard/history15Days',
    method: 'get',
    params: query
  })
}
// æœªæ¥15天任务
export function getFuture15Days(query) {
  return request({
    url: '/report/dashboard/future15Days',
    method: 'get',
    params: query
  })
}
// è¿‘15天提交排行
export function getRanking(query) {
  return request({
    url: '/report/dashboard/ranking',
    method: 'get',
    params: query
  })
}
// è¿‘30天检验结果统计
export function getInsResult(query) {
  return request({
    url: '/report/dashboard/insResult',
    method: 'get',
    params: query
  })
}
// èŽ·å–è¯­éŸ³æ’­æŠ¥é˜Ÿåˆ—
export function getVoiceQueue(query) {
  return request({
    url: '/report/dashboard/voiceQueue',
    method: 'get',
    params: query
  })
}
src/api/report/deviceRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
// è®¾å¤‡ä½¿ç”¨è®°å½•接口
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢è®¾å¤‡ä½¿ç”¨è®°å½•
export function pageDeviceRecord(query) {
  return request({
    url: '/report/deviceRecord/page',
    method: 'get',
    params: query
  })
}
// è®¾å¤‡ä½¿ç”¨ç»Ÿè®¡
export function getDeviceStatistics(query) {
  return request({
    url: '/report/deviceRecord/statistics',
    method: 'get',
    params: query
  })
}
// å¯¼å‡ºè®¾å¤‡ä½¿ç”¨è®°å½•
export function exportDeviceRecord(query) {
  return request({
    url: '/report/deviceRecord/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
src/api/report/normalDistribution.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
// æ­£æ€åˆ†å¸ƒå›¾æŽ¥å£
import request from '@/utils/request'
// æ­£æ€åˆ†å¸ƒåˆ†æž
export function normalDistributionAnalyze(data) {
  return request({
    url: '/chart/normalDistribution/analyze',
    method: 'post',
    data: data
  })
}
// å¯¼å‡ºåˆ†æžæ•°æ®
export function exportNormalDistribution(query) {
  return request({
    url: '/chart/normalDistribution/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
// èŽ·å–æ£€æµ‹é¡¹ç›®åˆ—è¡¨
export function getProjectList(query) {
  return request({
    url: '/chart/normalDistribution/projectList',
    method: 'get',
    params: query
  })
}
// èŽ·å–æ£€æµ‹å‚æ•°åˆ—è¡¨
export function getParamList(projectId) {
  return request({
    url: '/chart/normalDistribution/paramList',
    method: 'get',
    params: { projectId }
  })
}
src/api/report/passRate.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
// åˆæ ¼çŽ‡ç»Ÿè®¡æŽ¥å£
import request from '@/utils/request'
// åŽŸææ–™åˆæ ¼çŽ‡
export function getRawMaterialPassRate(query) {
  return request({
    url: '/chart/passRate/rawMaterial',
    method: 'get',
    params: query
  })
}
// ä¾›åº”商不合格统计
export function getSupplierUnqualified(query) {
  return request({
    url: '/chart/passRate/supplier',
    method: 'get',
    params: query
  })
}
// å¸•累托图数据
export function getParetoData(query) {
  return request({
    url: '/chart/passRate/pareto',
    method: 'get',
    params: query
  })
}
// å·¥åºåˆæ ¼çއ
export function getProcessPassRate(query) {
  return request({
    url: '/chart/passRate/process',
    method: 'get',
    params: query
  })
}
// æœºå°ä¸åˆæ ¼ç»Ÿè®¡
export function getMachineUnqualified(query) {
  return request({
    url: '/chart/passRate/machine',
    method: 'get',
    params: query
  })
}
// å¯¼å‡ºåˆæ ¼çŽ‡ç»Ÿè®¡
export function exportPassRate(query) {
  return request({
    url: '/chart/passRate/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
src/api/report/sampleProgress.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
// æ ·å“è¿›åº¦æŠ¥è¡¨æŽ¥å£
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢æ ·å“è¿›åº¦
export function pageSampleProgress(query) {
  return request({
    url: '/report/sampleProgress/page',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢æ ·å“è¿›åº¦ç»Ÿè®¡
export function getStatistics(query) {
  return request({
    url: '/report/sampleProgress/statistics',
    method: 'get',
    params: query
  })
}
// å¯¼å‡ºæ ·å“è¿›åº¦æŠ¥è¡¨
export function exportSampleProgress(query) {
  return request({
    url: '/report/sampleProgress/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
// æŸ¥è¯¢è¿›åº¦å¯è§†åŒ–数据
export function getChartData(query) {
  return request({
    url: '/report/sampleProgress/chart',
    method: 'get',
    params: query
  })
}
src/api/report/sampleRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
// æ ·å“é¢†æ ·è®°å½•接口
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢é¢†æ ·è®°å½•
export function pageSampleRecord(query) {
  return request({
    url: '/report/sampleRecord/page',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢æ ·å“æµè½¬è®°å½•
export function getSampleFlow(id) {
  return request({
    url: '/report/sampleRecord/flow',
    method: 'get',
    params: { id }
  })
}
// å¯¼å‡ºé¢†æ ·è®°å½•
export function exportSampleRecord(query) {
  return request({
    url: '/report/sampleRecord/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
src/api/report/spcChart.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
// SPC控制图接口
import request from '@/utils/request'
// SPC分析
export function spcAnalyze(data) {
  return request({
    url: '/chart/spc/analyze',
    method: 'post',
    data: data
  })
}
// åˆ¶ç¨‹èƒ½åŠ›åˆ†æž
export function getCapability(query) {
  return request({
    url: '/chart/spc/capability',
    method: 'get',
    params: query
  })
}
// å¯¼å‡ºåˆ†æžæ•°æ®
export function exportSpcData(query) {
  return request({
    url: '/chart/spc/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
// èŽ·å–æ£€æµ‹é¡¹ç›®åˆ—è¡¨
export function getProjectList(query) {
  return request({
    url: '/chart/spc/projectList',
    method: 'get',
    params: query
  })
}
// èŽ·å–æ£€æµ‹å‚æ•°åˆ—è¡¨
export function getParamList(projectId) {
  return request({
    url: '/chart/spc/paramList',
    method: 'get',
    params: { projectId }
  })
}
src/api/report/testItemData.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
// æ£€æµ‹é¡¹ç›®æ•°æ®æŽ¥å£
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢æ£€æµ‹é¡¹ç›®æ•°æ®
export function pageTestItemData(query) {
  return request({
    url: '/report/testItemData/page',
    method: 'get',
    params: query
  })
}
// æŸ¥è¯¢æ£€æµ‹é¡¹ç›®è¯¦æƒ…
export function getTestItemDetail(id) {
  return request({
    url: '/report/testItemData/detail',
    method: 'get',
    params: { id }
  })
}
// æ•°æ®æ¨ªå‘比较
export function compareTestItem(query) {
  return request({
    url: '/report/testItemData/compare',
    method: 'get',
    params: query
  })
}
// å¯¼å‡ºæ£€æµ‹é¡¹ç›®æ•°æ®
export function exportTestItemData(query) {
  return request({
    url: '/report/testItemData/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
src/api/report/workStatistics.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
// å·¥ä½œç»Ÿè®¡æŽ¥å£
import request from '@/utils/request'
// æŒ‰äººå‘˜ç»Ÿè®¡
export function getStatisticsByUser(query) {
  return request({
    url: '/chart/workStatistics/byUser',
    method: 'get',
    params: query
  })
}
// åŠæ—¶çŽ‡ç»Ÿè®¡
export function getTimelyRate(query) {
  return request({
    url: '/chart/workStatistics/timelyRate',
    method: 'get',
    params: query
  })
}
// å·¥ä½œè¶‹åŠ¿å›¾
export function getWorkTrend(query) {
  return request({
    url: '/chart/workStatistics/trend',
    method: 'get',
    params: query
  })
}
// å¯¼å‡ºå·¥ä½œç»Ÿè®¡
export function exportWorkStatistics(query) {
  return request({
    url: '/chart/workStatistics/export',
    method: 'get',
    params: query,
    responseType: 'blob'
  })
}
src/views/report/dashboard/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,496 @@
<template>
  <div class="app-container dashboard-container">
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <el-row :gutter="20" style="margin-bottom: 20px;">
      <el-col :span="4">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #409EFF;">
              <i class="el-icon-s-claim" />
            </div>
            <div class="stat-content">
              <div class="stat-title">待领样品</div>
              <div class="stat-value">{{ overviewData.waitReceive || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="4">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #E6A23C;">
              <i class="el-icon-time" />
            </div>
            <div class="stat-content">
              <div class="stat-title">待检样品</div>
              <div class="stat-value">{{ overviewData.waitInspection || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="4">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #909399;">
              <i class="el-icon-document-checked" />
            </div>
            <div class="stat-content">
              <div class="stat-title">待审核</div>
              <div class="stat-value">{{ overviewData.waitAudit || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="4">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #F56C6C;">
              <i class="el-icon-document" />
            </div>
            <div class="stat-content">
              <div class="stat-title">待编制报告</div>
              <div class="stat-value">{{ overviewData.waitReport || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="4">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #67C23A;">
              <i class="el-icon-circle-check" />
            </div>
            <div class="stat-content">
              <div class="stat-title">今日完成</div>
              <div class="stat-value">{{ overviewData.todayFinished || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="4">
        <el-card shadow="hover" class="voice-control" @click.native="toggleVoice">
          <div class="voice-content">
            <i :class="voiceEnabled ? 'el-icon-microphone' : 'el-icon-turn-off-microphone'" />
            <span>{{ voiceEnabled ? '语音播报中' : '语音已关闭' }}</span>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- å›¾è¡¨åŒºåŸŸ -->
    <el-row :gutter="20" style="margin-bottom: 20px;">
      <!-- åŽ†å²15天检测任务 -->
      <el-col :span="16">
        <el-card shadow="hover">
          <div slot="header">
            <span>近15天检测任务</span>
            <el-tag type="info" size="mini" style="margin-left: 10px;">实时更新</el-tag>
          </div>
          <Echart
            :xAxis="historyXAxis"
            :yAxis="historyYAxis"
            :series="historySeries"
            :tooltip="{ trigger: 'axis' }"
            :legend="{ data: ['检测任务', '完成任务'] }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '280px' }"
          />
        </el-card>
      </el-col>
      <!-- æœªæ¥15天任务预览 -->
      <el-col :span="8">
        <el-card shadow="hover" class="future-task-card">
          <div slot="header">未来15天任务</div>
          <div class="future-task-list">
            <div v-for="(item, index) in futureTasks" :key="index" class="future-task-item">
              <span class="task-date">{{ item.date }}</span>
              <el-progress :percentage="item.progress" :stroke-width="10" style="flex: 1; margin: 0 10px;" />
              <span class="task-count">{{ item.taskCount }}个任务</span>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <el-row :gutter="20" style="margin-bottom: 20px;">
      <!-- æäº¤æŽ’行榜 -->
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">
            <span>近15天提交排行</span>
            <el-radio-group v-model="rankingType" size="mini" style="float: right;" @change="getRankingData">
              <el-radio-button label="record">原始记录</el-radio-button>
              <el-radio-button label="report">报告</el-radio-button>
            </el-radio-group>
          </div>
          <div class="ranking-list">
            <div v-for="(item, index) in rankingData" :key="index" class="ranking-item">
              <span class="ranking-index" :class="getRankingClass(index)">{{ index + 1 }}</span>
              <span class="ranking-name">{{ item.userName }}</span>
              <el-progress :percentage="item.percentage" :stroke-width="15" :show-text="false" style="flex: 1; margin: 0 15px;" />
              <span class="ranking-count">{{ item.count }}份</span>
            </div>
          </div>
        </el-card>
      </el-col>
      <!-- æ£€éªŒç»“果统计 -->
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">近30天检验结果</div>
          <el-row :gutter="20">
            <el-col :span="8">
              <div class="result-card">
                <div class="result-title">原材料</div>
                <Echart
                  :series="rawMaterialSeries"
                  :tooltip="{ trigger: 'item', formatter: '{b}: {c} ({d}%)' }"
                  :chartStyle="{ height: '200px' }"
                />
              </div>
            </el-col>
            <el-col :span="8">
              <div class="result-card">
                <div class="result-title">半成品</div>
                <Echart
                  :series="semiProductSeries"
                  :tooltip="{ trigger: 'item', formatter: '{b}: {c} ({d}%)' }"
                  :chartStyle="{ height: '200px' }"
                />
              </div>
            </el-col>
            <el-col :span="8">
              <div class="result-card">
                <div class="result-title">成品</div>
                <Echart
                  :series="finishedProductSeries"
                  :tooltip="{ trigger: 'item', formatter: '{b}: {c} ({d}%)' }"
                  :chartStyle="{ height: '200px' }"
                />
              </div>
            </el-col>
          </el-row>
        </el-card>
      </el-col>
    </el-row>
    <!-- ç´§æ€¥äº‹é¡¹æ’­æŠ¥ -->
    <el-card shadow="hover">
      <div slot="header">
        <span>紧急事项</span>
        <el-button type="text" style="float: right;" @click="refreshVoiceQueue">
          <i class="el-icon-refresh" /> åˆ·æ–°
        </el-button>
      </div>
      <el-table :data="urgentItems" border style="width: 100%" max-height="200">
        <el-table-column prop="type" label="类型" width="120" />
        <el-table-column prop="content" label="内容" />
        <el-table-column prop="time" label="时间" width="180" />
        <el-table-column prop="level" label="级别" width="100">
          <template slot-scope="scope">
            <el-tag :type="getLevelType(scope.row.level)">{{ scope.row.level }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="100">
          <template slot-scope="scope">
            <el-button type="text" size="mini" @click="speakContent(scope.row.content)">播报</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import {
  getOverview,
  getHistory15Days,
  getFuture15Days,
  getRanking,
  getInsResult,
  getVoiceQueue
} from '@/api/report/dashboard'
export default {
  name: 'TestHall',
  components: { Echart },
  data() {
    return {
      overviewData: {},
      futureTasks: [],
      rankingData: [],
      rankingType: 'record',
      urgentItems: [],
      voiceEnabled: true,
      refreshTimer: null,
      // åŽ†å²15天图表配置
      historyXAxis: [{ type: 'category', data: [] }],
      historyYAxis: [{ type: 'value' }],
      historySeries: [
        { name: '检测任务', type: 'bar', data: [] },
        { name: '完成任务', type: 'line', data: [] }
      ],
      // æ£€éªŒç»“果饼图配置
      rawMaterialSeries: [{
        type: 'pie',
        radius: ['50%', '70%'],
        data: [
          { name: '合格', value: 0, itemStyle: { color: '#67C23A' } },
          { name: '不合格', value: 0, itemStyle: { color: '#F56C6C' } }
        ]
      }],
      semiProductSeries: [{
        type: 'pie',
        radius: ['50%', '70%'],
        data: [
          { name: '合格', value: 0, itemStyle: { color: '#67C23A' } },
          { name: '不合格', value: 0, itemStyle: { color: '#F56C6C' } }
        ]
      }],
      finishedProductSeries: [{
        type: 'pie',
        radius: ['50%', '70%'],
        data: [
          { name: '合格', value: 0, itemStyle: { color: '#67C23A' } },
          { name: '不合格', value: 0, itemStyle: { color: '#F56C6C' } }
        ]
      }]
    }
  },
  mounted() {
    this.initDashboard()
    this.startAutoRefresh()
  },
  beforeDestroy() {
    this.stopAutoRefresh()
  },
  methods: {
    // åˆå§‹åŒ–看板数据
    async initDashboard() {
      await Promise.all([
        this.getOverviewData(),
        this.getHistoryData(),
        this.getFutureData(),
        this.getRankingData(),
        this.getInsResultData(),
        this.refreshVoiceQueue()
      ])
    },
    // èŽ·å–æ¦‚è§ˆæ•°æ®
    getOverviewData() {
      return getOverview().then(res => {
        this.overviewData = res.data || {}
      })
    },
    // èŽ·å–åŽ†å²15天数据
    getHistoryData() {
      return getHistory15Days().then(res => {
        this.historyXAxis[0].data = res.data.dates || []
        this.historySeries[0].data = res.data.taskCounts || []
        this.historySeries[1].data = res.data.finishCounts || []
      })
    },
    // èŽ·å–æœªæ¥15天任务
    getFutureData() {
      return getFuture15Days().then(res => {
        this.futureTasks = res.data || []
      })
    },
    // èŽ·å–æäº¤æŽ’è¡Œ
    getRankingData() {
      return getRanking({ type: this.rankingType }).then(res => {
        this.rankingData = res.data || []
      })
    },
    // èŽ·å–æ£€éªŒç»“æžœç»Ÿè®¡
    getInsResultData() {
      return getInsResult().then(res => {
        if (res.data.rawMaterial) {
          this.rawMaterialSeries[0].data[0].value = res.data.rawMaterial.passCount || 0
          this.rawMaterialSeries[0].data[1].value = res.data.rawMaterial.unpassCount || 0
        }
        if (res.data.semiProduct) {
          this.semiProductSeries[0].data[0].value = res.data.semiProduct.passCount || 0
          this.semiProductSeries[0].data[1].value = res.data.semiProduct.unpassCount || 0
        }
        if (res.data.finishedProduct) {
          this.finishedProductSeries[0].data[0].value = res.data.finishedProduct.passCount || 0
          this.finishedProductSeries[0].data[1].value = res.data.finishedProduct.unpassCount || 0
        }
      })
    },
    // åˆ·æ–°è¯­éŸ³æ’­æŠ¥é˜Ÿåˆ—
    refreshVoiceQueue() {
      return getVoiceQueue().then(res => {
        this.urgentItems = res.data || []
      })
    },
    // åˆ‡æ¢è¯­éŸ³æ’­æŠ¥çŠ¶æ€
    toggleVoice() {
      this.voiceEnabled = !this.voiceEnabled
      if (this.voiceEnabled) {
        this.$message.success('语音播报已开启')
      } else {
        this.$message.info('语音播报已关闭')
      }
    },
    // è¯­éŸ³æ’­æŠ¥å†…容
    speakContent(content) {
      if ('speechSynthesis' in window) {
        const utterance = new SpeechSynthesisUtterance(content)
        utterance.lang = 'zh-CN'
        speechSynthesis.speak(utterance)
      } else {
        this.$message.warning('当前浏览器不支持语音播报')
      }
    },
    // å¼€å¯è‡ªåŠ¨åˆ·æ–°
    startAutoRefresh() {
      this.refreshTimer = setInterval(() => {
        this.initDashboard()
      }, 30000)
    },
    // åœæ­¢è‡ªåŠ¨åˆ·æ–°
    stopAutoRefresh() {
      if (this.refreshTimer) {
        clearInterval(this.refreshTimer)
        this.refreshTimer = null
      }
    },
    // èŽ·å–æŽ’è¡Œæ ·å¼ç±»
    getRankingClass(index) {
      if (index === 0) return 'first'
      if (index === 1) return 'second'
      if (index === 2) return 'third'
      return ''
    },
    // èŽ·å–çº§åˆ«æ ‡ç­¾ç±»åž‹
    getLevelType(level) {
      const map = { '紧急': 'danger', '重要': 'warning', '普通': 'info' }
      return map[level] || 'info'
    }
  }
}
</script>
<style scoped>
.dashboard-container {
  background: #f0f2f5;
  min-height: calc(100vh - 84px);
}
.stat-card-wrapper {
  cursor: pointer;
}
.stat-card {
  display: flex;
  align-items: center;
  padding: 10px;
}
.stat-icon {
  width: 50px;
  height: 50px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.stat-icon i {
  font-size: 24px;
  color: #fff;
}
.stat-content {
  margin-left: 10px;
}
.stat-title {
  font-size: 12px;
  color: #909399;
}
.stat-value {
  font-size: 22px;
  font-weight: bold;
  color: #303133;
  margin-top: 5px;
}
.voice-control {
  cursor: pointer;
}
.voice-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 15px;
  color: #409EFF;
}
.voice-content i {
  font-size: 32px;
}
.voice-content span {
  font-size: 12px;
  margin-top: 8px;
}
.future-task-card {
  height: 380px;
  overflow: auto;
}
.future-task-list {
  padding: 10px;
}
.future-task-item {
  display: flex;
  align-items: center;
  margin-bottom: 15px;
}
.task-date {
  width: 80px;
  font-size: 12px;
  color: #606266;
}
.task-count {
  width: 60px;
  text-align: right;
  font-size: 12px;
  color: #909399;
}
.ranking-list {
  padding: 10px;
}
.ranking-item {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
}
.ranking-index {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #909399;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
}
.ranking-index.first { background: #FFD700; }
.ranking-index.second { background: #C0C0C0; }
.ranking-index.third { background: #CD7F32; }
.ranking-name {
  width: 80px;
  margin-left: 10px;
  font-size: 14px;
}
.ranking-count {
  width: 50px;
  text-align: right;
  font-size: 14px;
  color: #606266;
}
.result-card {
  text-align: center;
}
.result-title {
  font-size: 14px;
  color: #606266;
  margin-bottom: 10px;
}
</style>
src/views/report/deviceRecord/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,235 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="queryForm" :model="queryParams" :inline="true" size="small">
      <el-form-item label="设备编号" prop="deviceCode">
        <el-input v-model="queryParams.deviceCode" placeholder="请输入设备编号" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="设备名称" prop="deviceName">
        <el-input v-model="queryParams.deviceName" placeholder="请输入设备名称" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="使用人" prop="useUser">
        <el-input v-model="queryParams.useUser" placeholder="请输入使用人" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="使用周期" prop="timeRange">
        <el-date-picker
          v-model="timeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          value-format="yyyy-MM-dd"
          style="width: 240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </el-form-item>
    </el-form>
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <el-row :gutter="20" style="margin-bottom: 20px;">
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-title">设备总数</div>
            <div class="stat-value">{{ statistics.totalDevices || 0 }}</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-title">使用次数</div>
            <div class="stat-value">{{ statistics.useCount || 0 }}</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-title">使用时长(h)</div>
            <div class="stat-value">{{ statistics.useHours || 0 }}</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover">
          <div class="stat-card">
            <div class="stat-title">利用率</div>
            <div class="stat-value">{{ statistics.utilization || 0 }}%</div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- ä½¿ç”¨é¢‘率图表 -->
    <el-row :gutter="20" style="margin-bottom: 20px;">
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">设备使用频率TOP10</div>
          <Echart
            :xAxis="useFrequencyXAxis"
            :yAxis="useFrequencyYAxis"
            :series="useFrequencySeries"
            :tooltip="{ trigger: 'axis' }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '300px' }"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">设备使用趋势</div>
          <Echart
            :xAxis="useTrendXAxis"
            :yAxis="useTrendYAxis"
            :series="useTrendSeries"
            :tooltip="{ trigger: 'axis' }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '300px' }"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <lims-table
      :tableData="tableData"
      :column="tableColumn"
      :page="page"
      :tableLoading="tableLoading"
      @pagination="handlePagination"
    />
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import limsTable from '@/components/Table/lims-table.vue'
import { pageDeviceRecord, getDeviceStatistics, exportDeviceRecord } from '@/api/report/deviceRecord'
export default {
  name: 'DeviceRecord',
  components: { Echart, limsTable },
  data() {
    return {
      queryParams: {},
      timeRange: [],
      tableData: [],
      tableLoading: false,
      statistics: {},
      page: { total: 0, size: 10, current: 1 },
      tableColumn: [
        { label: '设备编号', prop: 'deviceCode', minWidth: '120px' },
        { label: '设备名称', prop: 'deviceName', minWidth: '150px' },
        { label: '规格型号', prop: 'specModel', minWidth: '120px' },
        { label: '使用人', prop: 'useUser', minWidth: '80px' },
        { label: '开始时间', prop: 'startTime', minWidth: '160px' },
        { label: '结束时间', prop: 'endTime', minWidth: '160px' },
        { label: '使用时长(h)', prop: 'useHours', minWidth: '100px' },
        { label: '关联样品', prop: 'sampleCode', minWidth: '140px' },
        { label: '检测项目', prop: 'testItem', minWidth: '120px' },
        { label: '使用状态', prop: 'status', minWidth: '100px', dataType: 'tag', formatData: (val) => val === 1 ? '使用中' : '已结束', formatType: (val) => val === 1 ? 'warning' : 'success' }
      ],
      // ä½¿ç”¨é¢‘率图表
      useFrequencyXAxis: [{ type: 'category', data: [], axisLabel: { rotate: 30 } }],
      useFrequencyYAxis: [{ type: 'value' }],
      useFrequencySeries: [{ name: '使用次数', type: 'bar', data: [] }],
      // ä½¿ç”¨è¶‹åŠ¿å›¾è¡¨
      useTrendXAxis: [{ type: 'category', data: [] }],
      useTrendYAxis: [{ type: 'value' }],
      useTrendSeries: [{ name: '使用时长', type: 'line', data: [] }]
    }
  },
  mounted() {
    this.getList()
    this.getStatisticsData()
  },
  methods: {
    getList() {
      this.tableLoading = true
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      pageDeviceRecord({ ...params, ...this.page })
        .then(res => {
          this.tableData = res.data.records || []
          this.page.total = res.data.total || 0
        })
        .finally(() => (this.tableLoading = false))
    },
    getStatisticsData() {
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      getDeviceStatistics(params).then(res => {
        this.statistics = res.data.summary || {}
        // ä½¿ç”¨é¢‘率图表数据
        this.useFrequencyXAxis[0].data = (res.data.frequencyData || []).map(item => item.deviceName)
        this.useFrequencySeries[0].data = (res.data.frequencyData || []).map(item => item.count)
        // ä½¿ç”¨è¶‹åŠ¿å›¾è¡¨æ•°æ®
        this.useTrendXAxis[0].data = (res.data.trendData || []).map(item => item.date)
        this.useTrendSeries[0].data = (res.data.trendData || []).map(item => item.hours)
      })
    },
    handleQuery() {
      this.page.current = 1
      this.getList()
      this.getStatisticsData()
    },
    resetQuery() {
      this.queryParams = {}
      this.timeRange = []
      this.handleQuery()
    },
    handleExport() {
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      exportDeviceRecord(params).then(res => {
        this.downloadFile(res, '设备使用记录.xlsx')
      })
    },
    handlePagination({ page, limit }) {
      this.page.current = page
      this.page.size = limit
      this.getList()
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
<style scoped>
.stat-card {
  text-align: center;
  padding: 15px 0;
}
.stat-title {
  font-size: 14px;
  color: #909399;
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  color: #303133;
  margin-top: 10px;
}
</style>
src/views/report/normalDistribution/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,303 @@
<template>
  <div class="app-container">
    <!-- åˆ†æžé…ç½®è¡¨å• -->
    <el-card shadow="hover" style="margin-bottom: 20px;">
      <div slot="header">正态分布分析配置</div>
      <el-form ref="analysisForm" :model="analysisParams" :inline="true" size="small" label-width="100px">
        <el-form-item label="检测项目" prop="projectId">
          <el-select v-model="analysisParams.projectId" placeholder="请选择检测项目" style="width: 200px" @change="handleProjectChange">
            <el-option v-for="item in projectList" :key="item.id" :label="item.projectName" :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="检测参数" prop="paramName">
          <el-select v-model="analysisParams.paramName" placeholder="请选择检测参数" style="width: 200px">
            <el-option v-for="item in paramList" :key="item.paramName" :label="item.paramName" :value="item.paramName" />
          </el-select>
        </el-form-item>
        <el-form-item label="时间范围" prop="timeRange">
          <el-date-picker
            v-model="analysisParams.timeRange"
            type="daterange"
            range-separator="-"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            value-format="yyyy-MM-dd"
            style="width: 240px"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-data-analysis" @click="handleAnalysis">开始分析</el-button>
          <el-button type="success" icon="el-icon-download" @click="handleExport">导出数据</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <el-row :gutter="20" v-if="analysisResult">
      <!-- æ­£æ€åˆ†å¸ƒå›¾ -->
      <el-col :span="16">
        <el-card shadow="hover">
          <div slot="header">正态分布图</div>
          <Echart
            :xAxis="distributionXAxis"
            :yAxis="distributionYAxis"
            :series="distributionSeries"
            :tooltip="{ trigger: 'axis' }"
            :legend="{ data: ['频数', '正态曲线'] }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '400px' }"
          />
        </el-card>
      </el-col>
      <!-- ç»Ÿè®¡ä¿¡æ¯ -->
      <el-col :span="8">
        <el-card shadow="hover">
          <div slot="header">统计信息</div>
          <el-descriptions :column="1" border>
            <el-descriptions-item label="样本数量">{{ analysisResult.statistics.sampleCount || 0 }}</el-descriptions-item>
            <el-descriptions-item label="均值(μ)">{{ (analysisResult.statistics.mean || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="标准差(σ)">{{ (analysisResult.statistics.stdDev || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="最小值">{{ (analysisResult.statistics.min || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="最大值">{{ (analysisResult.statistics.max || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="极差">{{ (analysisResult.statistics.range || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="中位数">{{ (analysisResult.statistics.median || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="偏度">{{ (analysisResult.statistics.skewness || 0).toFixed(4) }}</el-descriptions-item>
            <el-descriptions-item label="峰度">{{ (analysisResult.statistics.kurtosis || 0).toFixed(4) }}</el-descriptions-item>
          </el-descriptions>
        </el-card>
      </el-col>
    </el-row>
    <!-- ç›´æ–¹å›¾æ•°æ®è¡¨æ ¼ -->
    <el-row :gutter="20" style="margin-top: 20px;" v-if="analysisResult">
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">频数分布表</div>
          <el-table :data="analysisResult.histogramData" border style="width: 100%">
            <el-table-column prop="interval" label="区间" />
            <el-table-column prop="frequency" label="频数" />
            <el-table-column prop="relativeFreq" label="相对频率">
              <template slot-scope="scope">
                {{ (scope.row.relativeFreq * 100).toFixed(2) }}%
              </template>
            </el-table-column>
            <el-table-column prop="cumulativeFreq" label="累计频率">
              <template slot-scope="scope">
                {{ (scope.row.cumulativeFreq * 100).toFixed(2) }}%
              </template>
            </el-table-column>
          </el-table>
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">过程能力</div>
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="capability-item">
                <div class="capability-label">规格上限(USL)</div>
                <el-input-number v-model="analysisParams.usl" :precision="4" style="width: 100%;" @change="calculateCpk" />
              </div>
            </el-col>
            <el-col :span="12">
              <div class="capability-item">
                <div class="capability-label">规格下限(LSL)</div>
                <el-input-number v-model="analysisParams.lsl" :precision="4" style="width: 100%;" @change="calculateCpk" />
              </div>
            </el-col>
          </el-row>
          <el-divider />
          <el-row :gutter="20">
            <el-col :span="12">
              <div class="capability-result">
                <div class="capability-label">Cp</div>
                <div class="capability-value" :style="{ color: getCapabilityColor(calculatedCpk.cp) }">
                  {{ calculatedCpk.cp ? calculatedCpk.cp.toFixed(4) : '--' }}
                </div>
              </div>
            </el-col>
            <el-col :span="12">
              <div class="capability-result">
                <div class="capability-label">Cpk</div>
                <div class="capability-value" :style="{ color: getCapabilityColor(calculatedCpk.cpk) }">
                  {{ calculatedCpk.cpk ? calculatedCpk.cpk.toFixed(4) : '--' }}
                </div>
              </div>
            </el-col>
          </el-row>
        </el-card>
      </el-col>
    </el-row>
    <!-- åŽŸå§‹æ•°æ® -->
    <el-card shadow="hover" style="margin-top: 20px;" v-if="analysisResult">
      <div slot="header">原始数据</div>
      <el-table :data="rawDataTable" border style="width: 100%" max-height="300">
        <el-table-column type="index" label="序号" width="60" />
        <el-table-column prop="value" label="检测值">
          <template slot-scope="scope">
            {{ scope.row.value ? scope.row.value.toFixed(4) : '' }}
          </template>
        </el-table-column>
        <el-table-column prop="sampleCode" label="样品编号" />
        <el-table-column prop="testTime" label="检测时间" />
        <el-table-column prop="tester" label="检测人" />
      </el-table>
    </el-card>
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import { normalDistributionAnalyze, getProjectList, getParamList, exportNormalDistribution } from '@/api/report/normalDistribution'
export default {
  name: 'NormalDistribution',
  components: { Echart },
  data() {
    return {
      projectList: [],
      paramList: [],
      analysisParams: {
        projectId: null,
        paramName: null,
        timeRange: [],
        usl: null,
        lsl: null
      },
      analysisResult: null,
      rawDataTable: [],
      calculatedCpk: {
        cp: null,
        cpk: null
      },
      // æ­£æ€åˆ†å¸ƒå›¾
      distributionXAxis: [{ type: 'category', data: [] }],
      distributionYAxis: [{ type: 'value' }],
      distributionSeries: [
        { name: '频数', type: 'bar', data: [], barWidth: '60%' },
        { name: '正态曲线', type: 'line', data: [], smooth: true }
      ]
    }
  },
  mounted() {
    this.getProjectList()
  },
  methods: {
    getProjectList() {
      getProjectList().then(res => {
        this.projectList = res.data || []
      })
    },
    handleProjectChange(projectId) {
      this.analysisParams.paramName = null
      getParamList(projectId).then(res => {
        this.paramList = res.data || []
      })
    },
    handleAnalysis() {
      if (!this.analysisParams.projectId) {
        this.$message.warning('请选择检测项目')
        return
      }
      if (!this.analysisParams.paramName) {
        this.$message.warning('请选择检测参数')
        return
      }
      const params = {
        projectId: this.analysisParams.projectId,
        paramName: this.analysisParams.paramName
      }
      if (this.analysisParams.timeRange && this.analysisParams.timeRange.length === 2) {
        params.startDate = this.analysisParams.timeRange[0]
        params.endDate = this.analysisParams.timeRange[1]
      }
      normalDistributionAnalyze(params).then(res => {
        this.analysisResult = res.data
        this.renderCharts(res.data)
        this.rawDataTable = (res.data.rawData || []).map(item => ({
          value: item.value,
          sampleCode: item.sampleCode,
          testTime: item.testTime,
          tester: item.tester
        }))
        this.$message.success('分析完成')
      })
    },
    renderCharts(data) {
      const histogramData = data.histogramData || []
      const normalCurve = data.normalCurve || []
      // ç›´æ–¹å›¾æ•°æ®
      this.distributionXAxis[0].data = histogramData.map(item => item.interval)
      this.distributionSeries[0].data = histogramData.map(item => item.frequency)
      // æ­£æ€æ›²çº¿æ•°æ®
      this.distributionSeries[1].data = normalCurve
    },
    calculateCpk() {
      if (!this.analysisResult || !this.analysisParams.usl || !this.analysisParams.lsl) {
        return
      }
      const stats = this.analysisResult.statistics
      const mean = stats.mean
      const stdDev = stats.stdDev
      const usl = this.analysisParams.usl
      const lsl = this.analysisParams.lsl
      // Cp = (USL - LSL) / (6σ)
      this.calculatedCpk.cp = (usl - lsl) / (6 * stdDev)
      // Cpk = min[(USL - Î¼) / (3σ), (μ - LSL) / (3σ)]
      const cpu = (usl - mean) / (3 * stdDev)
      const cpl = (mean - lsl) / (3 * stdDev)
      this.calculatedCpk.cpk = Math.min(cpu, cpl)
    },
    handleExport() {
      if (!this.analysisResult) {
        this.$message.warning('请先进行分析')
        return
      }
      const params = {
        projectId: this.analysisParams.projectId,
        paramName: this.analysisParams.paramName
      }
      exportNormalDistribution(params).then(res => {
        this.downloadFile(res, '正态分布分析数据.xlsx')
      })
    },
    getCapabilityColor(val) {
      if (!val) return '#909399'
      if (val >= 1.33) return '#67C23A'
      if (val >= 1) return '#E6A23C'
      return '#F56C6C'
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
<style scoped>
.capability-item {
  margin-bottom: 15px;
}
.capability-label {
  font-size: 14px;
  color: #909399;
  margin-bottom: 8px;
}
.capability-result {
  text-align: center;
  padding: 15px;
  background: #f5f7fa;
  border-radius: 8px;
}
.capability-value {
  font-size: 24px;
  font-weight: bold;
  margin-top: 10px;
}
</style>
src/views/report/passRate/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,325 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="queryForm" :model="queryParams" :inline="true" size="small">
      <el-form-item label="检验类型" prop="insType">
        <el-select v-model="queryParams.insType" placeholder="请选择检验类型" clearable style="width: 150px">
          <el-option label="原材料" value="rawMaterial" />
          <el-option label="半成品" value="semiProduct" />
          <el-option label="成品" value="finishedProduct" />
        </el-select>
      </el-form-item>
      <el-form-item label="供应商" prop="supplier">
        <el-input v-model="queryParams.supplier" placeholder="请输入供应商" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="工序" prop="process">
        <el-input v-model="queryParams.process" placeholder="请输入工序" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="时间范围" prop="timeRange">
        <el-date-picker
          v-model="timeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          value-format="yyyy-MM-dd"
          style="width: 240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </el-form-item>
    </el-form>
    <!-- Tab切换 -->
    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
      <el-tab-pane label="原材料合格率" name="rawMaterial">
        <el-card shadow="hover">
          <div slot="header">原材料不同批次检验合格率</div>
          <Echart
            :xAxis="rawMaterialXAxis"
            :yAxis="rawMaterialYAxis"
            :series="rawMaterialSeries"
            :tooltip="{ trigger: 'axis' }"
            :legend="{ data: ['合格率', '批次数'] }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '350px' }"
          />
        </el-card>
        <lims-table
          :tableData="rawMaterialTableData"
          :column="rawMaterialTableColumn"
          :page="rawMaterialPage"
          :tableLoading="rawMaterialLoading"
          @pagination="handleRawMaterialPagination"
        />
      </el-tab-pane>
      <el-tab-pane label="供应商统计" name="supplier">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-card shadow="hover">
              <div slot="header">供应商不合格次数统计</div>
              <Echart
                :xAxis="supplierXAxis"
                :yAxis="supplierYAxis"
                :series="supplierSeries"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
                :chartStyle="{ height: '350px' }"
              />
            </el-card>
          </el-col>
          <el-col :span="12">
            <el-card shadow="hover">
              <div slot="header">不合格项目帕累托图</div>
              <Echart
                :xAxis="paretoXAxis"
                :yAxis="paretoYAxis"
                :series="paretoSeries"
                :tooltip="{ trigger: 'axis' }"
                :legend="{ data: ['不合格次数', '累计占比'] }"
                :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
                :chartStyle="{ height: '350px' }"
              />
            </el-card>
          </el-col>
        </el-row>
      </el-tab-pane>
      <el-tab-pane label="工序合格率" name="process">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-card shadow="hover">
              <div slot="header">各工序合格率</div>
              <Echart
                :xAxis="processXAxis"
                :yAxis="processYAxis"
                :series="processSeries"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
                :chartStyle="{ height: '350px' }"
              />
            </el-card>
          </el-col>
          <el-col :span="12">
            <el-card shadow="hover">
              <div slot="header">机台不合格次数统计</div>
              <Echart
                :xAxis="machineXAxis"
                :yAxis="machineYAxis"
                :series="machineSeries"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
                :chartStyle="{ height: '350px' }"
              />
            </el-card>
          </el-col>
        </el-row>
        <lims-table
          :tableData="processTableData"
          :column="processTableColumn"
          :page="processPage"
          :tableLoading="processLoading"
          @pagination="handleProcessPagination"
        />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import limsTable from '@/components/Table/lims-table.vue'
import {
  getRawMaterialPassRate,
  getSupplierUnqualified,
  getParetoData,
  getProcessPassRate,
  getMachineUnqualified,
  exportPassRate
} from '@/api/report/passRate'
export default {
  name: 'PassRate',
  components: { Echart, limsTable },
  data() {
    return {
      queryParams: {},
      timeRange: [],
      activeTab: 'rawMaterial',
      // åŽŸææ–™åˆæ ¼çŽ‡
      rawMaterialLoading: false,
      rawMaterialTableData: [],
      rawMaterialPage: { total: 0, size: 10, current: 1 },
      rawMaterialTableColumn: [
        { label: '批次号', prop: 'batchCode', minWidth: '140px' },
        { label: '物料名称', prop: 'materialName', minWidth: '150px' },
        { label: '供应商', prop: 'supplier', minWidth: '150px' },
        { label: '检验数量', prop: 'totalCount', minWidth: '100px' },
        { label: '合格数量', prop: 'passCount', minWidth: '100px' },
        { label: '不合格数量', prop: 'unpassCount', minWidth: '100px' },
        { label: '合格率', prop: 'passRate', minWidth: '100px', dataType: 'tag', formatData: (val) => `${val}%`, formatType: (val) => val >= 90 ? 'success' : val >= 70 ? 'warning' : 'danger' }
      ],
      // åŽŸææ–™å›¾è¡¨
      rawMaterialXAxis: [{ type: 'category', data: [], axisLabel: { rotate: 30 } }],
      rawMaterialYAxis: [{ type: 'value', max: 100 }, { type: 'value', position: 'right' }],
      rawMaterialSeries: [
        { name: '合格率', type: 'bar', data: [] },
        { name: '批次数', type: 'line', yAxisIndex: 1, data: [] }
      ],
      // ä¾›åº”商图表
      supplierXAxis: [{ type: 'category', data: [], axisLabel: { rotate: 30 } }],
      supplierYAxis: [{ type: 'value' }],
      supplierSeries: [{ name: '不合格次数', type: 'bar', data: [] }],
      // å¸•累托图
      paretoXAxis: [{ type: 'category', data: [] }],
      paretoYAxis: [{ type: 'value' }, { type: 'value', max: 100, position: 'right' }],
      paretoSeries: [
        { name: '不合格次数', type: 'bar', data: [] },
        { name: '累计占比', type: 'line', yAxisIndex: 1, data: [] }
      ],
      // å·¥åºå›¾è¡¨
      processLoading: false,
      processTableData: [],
      processPage: { total: 0, size: 10, current: 1 },
      processTableColumn: [
        { label: '工序名称', prop: 'processName', minWidth: '120px' },
        { label: '检验数量', prop: 'totalCount', minWidth: '100px' },
        { label: '合格数量', prop: 'passCount', minWidth: '100px' },
        { label: '不合格数量', prop: 'unpassCount', minWidth: '100px' },
        { label: '合格率', prop: 'passRate', minWidth: '100px', dataType: 'tag', formatData: (val) => `${val}%`, formatType: (val) => val >= 90 ? 'success' : val >= 70 ? 'warning' : 'danger' }
      ],
      processXAxis: [{ type: 'category', data: [] }],
      processYAxis: [{ type: 'value', max: 100 }],
      processSeries: [{ name: '合格率', type: 'bar', data: [] }],
      // æœºå°å›¾è¡¨
      machineXAxis: [{ type: 'category', data: [] }],
      machineYAxis: [{ type: 'value' }],
      machineSeries: [{ name: '不合格次数', type: 'bar', data: [] }]
    }
  },
  mounted() {
    this.getRawMaterialData()
  },
  methods: {
    handleTabChange(tab) {
      if (tab.name === 'rawMaterial') {
        this.getRawMaterialData()
      } else if (tab.name === 'supplier') {
        this.getSupplierData()
        this.getParetoData()
      } else if (tab.name === 'process') {
        this.getProcessData()
        this.getMachineData()
      }
    },
    getRawMaterialData() {
      this.rawMaterialLoading = true
      const params = this.buildParams()
      getRawMaterialPassRate({ ...params, ...this.rawMaterialPage })
        .then(res => {
          this.rawMaterialTableData = res.data.records || []
          this.rawMaterialPage.total = res.data.total || 0
          // å›¾è¡¨æ•°æ®
          const chartData = (res.data.chartData || []).slice(0, 15)
          this.rawMaterialXAxis[0].data = chartData.map(item => item.batchCode)
          this.rawMaterialSeries[0].data = chartData.map(item => item.passRate)
          this.rawMaterialSeries[1].data = chartData.map(item => item.batchCount)
        })
        .finally(() => (this.rawMaterialLoading = false))
    },
    getSupplierData() {
      const params = this.buildParams()
      getSupplierUnqualified(params).then(res => {
        const data = res.data || []
        this.supplierXAxis[0].data = data.map(item => item.supplier)
        this.supplierSeries[0].data = data.map(item => item.unpassCount)
      })
    },
    getParetoData() {
      const params = this.buildParams()
      getParetoData(params).then(res => {
        this.paretoXAxis[0].data = res.data.categories || []
        this.paretoSeries[0].data = res.data.values || []
        this.paretoSeries[1].data = res.data.cumulativePercent || []
      })
    },
    getProcessData() {
      this.processLoading = true
      const params = this.buildParams()
      getProcessPassRate({ ...params, ...this.processPage })
        .then(res => {
          this.processTableData = res.data.records || []
          this.processPage.total = res.data.total || 0
          // å›¾è¡¨æ•°æ®
          const chartData = res.data.chartData || []
          this.processXAxis[0].data = chartData.map(item => item.processName)
          this.processSeries[0].data = chartData.map(item => item.passRate)
        })
        .finally(() => (this.processLoading = false))
    },
    getMachineData() {
      const params = this.buildParams()
      getMachineUnqualified(params).then(res => {
        const data = res.data || []
        this.machineXAxis[0].data = data.map(item => item.machineCode)
        this.machineSeries[0].data = data.map(item => item.unpassCount)
      })
    },
    buildParams() {
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      return params
    },
    handleQuery() {
      if (this.activeTab === 'rawMaterial') {
        this.rawMaterialPage.current = 1
        this.getRawMaterialData()
      } else if (this.activeTab === 'supplier') {
        this.getSupplierData()
        this.getParetoData()
      } else {
        this.processPage.current = 1
        this.getProcessData()
        this.getMachineData()
      }
    },
    resetQuery() {
      this.queryParams = {}
      this.timeRange = []
      this.handleQuery()
    },
    handleExport() {
      const params = { ...this.buildParams(), type: this.activeTab }
      exportPassRate(params).then(res => {
        this.downloadFile(res, '合格率统计.xlsx')
      })
    },
    handleRawMaterialPagination({ page, limit }) {
      this.rawMaterialPage.current = page
      this.rawMaterialPage.size = limit
      this.getRawMaterialData()
    },
    handleProcessPagination({ page, limit }) {
      this.processPage.current = page
      this.processPage.size = limit
      this.getProcessData()
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
src/views/report/sampleProgress/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,300 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="queryForm" :model="queryParams" :inline="true" size="small">
      <el-form-item label="委托编号" prop="entrustCode">
        <el-input v-model="queryParams.entrustCode" placeholder="请输入委托编号" clearable style="width: 200px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="样品编号" prop="sampleCode">
        <el-input v-model="queryParams.sampleCode" placeholder="请输入样品编号" clearable style="width: 200px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="样品名称" prop="sampleName">
        <el-input v-model="queryParams.sampleName" placeholder="请输入样品名称" clearable style="width: 200px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="检测状态" prop="insState">
        <el-select v-model="queryParams.insState" placeholder="请选择检测状态" clearable style="width: 150px">
          <el-option label="待检" :value="0" />
          <el-option label="检验中" :value="1" />
          <el-option label="已检验" :value="2" />
          <el-option label="待审核" :value="3" />
          <el-option label="审核未通过" :value="4" />
          <el-option label="审核通过" :value="5" />
        </el-select>
      </el-form-item>
      <el-form-item label="时间范围" prop="timeRange">
        <el-date-picker
          v-model="timeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          value-format="yyyy-MM-dd"
          style="width: 240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </el-form-item>
    </el-form>
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <el-row :gutter="20" style="margin-bottom: 20px;">
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #409EFF;">
              <i class="el-icon-time" />
            </div>
            <div class="stat-content">
              <div class="stat-title">待检样品</div>
              <div class="stat-value">{{ statistics.waitInspection || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #E6A23C;">
              <i class="el-icon-loading" />
            </div>
            <div class="stat-content">
              <div class="stat-title">检验中</div>
              <div class="stat-value">{{ statistics.inspecting || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #909399;">
              <i class="el-icon-document-checked" />
            </div>
            <div class="stat-content">
              <div class="stat-title">待审核</div>
              <div class="stat-value">{{ statistics.waitAudit || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="hover" class="stat-card-wrapper">
          <div class="stat-card">
            <div class="stat-icon" style="background: #67C23A;">
              <i class="el-icon-circle-check" />
            </div>
            <div class="stat-content">
              <div class="stat-title">已完成</div>
              <div class="stat-value">{{ statistics.finished || 0 }}</div>
            </div>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- è¿›åº¦å›¾è¡¨ -->
    <el-card shadow="hover" style="margin-bottom: 20px;">
      <div slot="header">
        <span>检测进度趋势</span>
        <el-radio-group v-model="chartTimeType" size="mini" style="float: right;" @change="getChart">
          <el-radio-button label="week">近一周</el-radio-button>
          <el-radio-button label="month">近一月</el-radio-button>
        </el-radio-group>
      </div>
      <Echart
        ref="progressChart"
        :xAxis="chartXAxis"
        :yAxis="chartYAxis"
        :series="chartSeries"
        :tooltip="chartTooltip"
        :grid="chartGrid"
        :chartStyle="{ height: '300px' }"
      />
    </el-card>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <lims-table
      :tableData="tableData"
      :column="tableColumn"
      :page="page"
      :tableLoading="tableLoading"
      @pagination="handlePagination"
    />
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import limsTable from '@/components/Table/lims-table.vue'
import { pageSampleProgress, getStatistics, getChartData, exportSampleProgress } from '@/api/report/sampleProgress'
export default {
  name: 'SampleProgress',
  components: { Echart, limsTable },
  data() {
    return {
      queryParams: {},
      timeRange: [],
      tableData: [],
      tableLoading: false,
      statistics: {},
      page: { total: 0, size: 10, current: 1 },
      chartTimeType: 'week',
      tableColumn: [
        { label: '委托编号', prop: 'entrustCode', minWidth: '140px' },
        { label: '样品编号', prop: 'sampleCode', minWidth: '140px' },
        { label: '样品名称', prop: 'sampleName', minWidth: '150px' },
        { label: '报告编号', prop: 'reportCode', minWidth: '140px' },
        {
          label: '检测状态',
          prop: 'insState',
          minWidth: '100px',
          dataType: 'tag',
          formatData: (val) => this.formatInsState(val),
          formatType: (val) => this.formatInsStateType(val)
        },
        { label: '进度', prop: 'progressPercent', minWidth: '120px', dataType: 'progress' },
        {
          label: '已完成/总数',
          prop: 'itemCount',
          minWidth: '100px',
          formatData: (val, row) => `${row.finishedItems || 0}/${row.totalItems || 0}`
        },
        { label: '负责人', prop: 'chargeUser', minWidth: '80px' },
        { label: '客户名称', prop: 'custom', minWidth: '150px' },
        { label: '创建时间', prop: 'createTime', minWidth: '160px' }
      ],
      // å›¾è¡¨é…ç½®
      chartXAxis: [{ type: 'category', data: [] }],
      chartYAxis: [{ type: 'value' }],
      chartSeries: [
        { name: '样品数量', type: 'bar', data: [] },
        { name: '完成数量', type: 'line', data: [] }
      ],
      chartTooltip: { trigger: 'axis' },
      chartGrid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }
    }
  },
  mounted() {
    this.getList()
    this.getStatisticsData()
    this.getChart()
  },
  methods: {
    getList() {
      this.tableLoading = true
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      pageSampleProgress({ ...params, ...this.page })
        .then(res => {
          this.tableData = res.data.records || []
          this.page.total = res.data.total || 0
        })
        .finally(() => (this.tableLoading = false))
    },
    getStatisticsData() {
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      getStatistics(params).then(res => {
        this.statistics = res.data || {}
      })
    },
    getChart() {
      const params = { timeType: this.chartTimeType }
      getChartData(params).then(res => {
        this.chartXAxis[0].data = res.data.dates || []
        this.chartSeries[0].data = res.data.totalCounts || []
        this.chartSeries[1].data = res.data.finishedCounts || []
      })
    },
    handleQuery() {
      this.page.current = 1
      this.getList()
      this.getStatisticsData()
      this.getChart()
    },
    resetQuery() {
      this.queryParams = {}
      this.timeRange = []
      this.handleQuery()
    },
    handleExport() {
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      exportSampleProgress(params).then(res => {
        this.downloadFile(res, '样品进度报表.xlsx')
      })
    },
    handlePagination({ page, limit }) {
      this.page.current = page
      this.page.size = limit
      this.getList()
    },
    formatInsState(val) {
      const map = { 0: '待检', 1: '检验中', 2: '已检验', 3: '待审核', 4: '审核未通过', 5: '审核通过' }
      return map[val] || ''
    },
    formatInsStateType(val) {
      const map = { 0: 'warning', 1: 'primary', 2: 'info', 3: 'warning', 4: 'danger', 5: 'success' }
      return map[val] || ''
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
<style scoped>
.stat-card-wrapper {
  cursor: pointer;
}
.stat-card {
  display: flex;
  align-items: center;
  padding: 10px;
}
.stat-icon {
  width: 60px;
  height: 60px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.stat-icon i {
  font-size: 28px;
  color: #fff;
}
.stat-content {
  margin-left: 15px;
}
.stat-title {
  font-size: 14px;
  color: #909399;
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  color: #303133;
  margin-top: 5px;
}
</style>
src/views/report/sampleRecord/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="queryForm" :model="queryParams" :inline="true" size="small">
      <el-form-item label="样品编号" prop="sampleCode">
        <el-input v-model="queryParams.sampleCode" placeholder="请输入样品编号" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="样品名称" prop="sampleName">
        <el-input v-model="queryParams.sampleName" placeholder="请输入样品名称" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="客户名称" prop="custom">
        <el-input v-model="queryParams.custom" placeholder="请输入客户名称" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="领用人" prop="receiveUser">
        <el-input v-model="queryParams.receiveUser" placeholder="请输入领用人" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="时间范围" prop="timeRange">
        <el-date-picker
          v-model="timeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          value-format="yyyy-MM-dd"
          style="width: 240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </el-form-item>
    </el-form>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <lims-table
      :tableData="tableData"
      :column="tableColumn"
      :page="page"
      :tableLoading="tableLoading"
      @pagination="handlePagination"
    >
      <template #operation="{ row }">
        <el-button type="text" size="mini" @click="handleFlow(row)">流转记录</el-button>
      </template>
    </lims-table>
    <!-- æµè½¬è®°å½•弹窗 -->
    <el-dialog title="样品流转记录" :visible.sync="flowVisible" width="70%">
      <el-timeline>
        <el-timeline-item
          v-for="(item, index) in flowData"
          :key="index"
          :timestamp="item.operateTime"
          placement="top"
          :color="getTimelineColor(item.status)"
        >
          <el-card>
            <h4>{{ item.operateType }}</h4>
            <p>操作人:{{ item.operator }}</p>
            <p>状态:{{ item.statusName }}</p>
            <p v-if="item.remark">备注:{{ item.remark }}</p>
          </el-card>
        </el-timeline-item>
      </el-timeline>
    </el-dialog>
  </div>
</template>
<script>
import limsTable from '@/components/Table/lims-table.vue'
import { pageSampleRecord, getSampleFlow, exportSampleRecord } from '@/api/report/sampleRecord'
export default {
  name: 'SampleRecord',
  components: { limsTable },
  data() {
    return {
      queryParams: {},
      timeRange: [],
      tableData: [],
      tableLoading: false,
      page: { total: 0, size: 10, current: 1 },
      flowVisible: false,
      flowData: [],
      tableColumn: [
        { label: '样品编号', prop: 'sampleCode', minWidth: '140px' },
        { label: '样品名称', prop: 'sampleName', minWidth: '150px' },
        { label: '委托编号', prop: 'entrustCode', minWidth: '140px' },
        { label: '领用人', prop: 'receiveUser', minWidth: '80px' },
        { label: '领用时间', prop: 'receiveTime', minWidth: '160px' },
        { label: '领用数量', prop: 'receiveNum', minWidth: '100px' },
        { label: '领用用途', prop: 'purpose', minWidth: '120px' },
        { label: '客户名称', prop: 'custom', minWidth: '150px' },
        { label: '存放位置', prop: 'location', minWidth: '120px' },
        { label: '当前状态', prop: 'status', minWidth: '100px', dataType: 'tag', formatData: (val) => this.formatStatus(val), formatType: (val) => this.formatStatusType(val) },
        {
          label: '操作',
          dataType: 'slot',
          slot: 'operation',
          minWidth: '100px'
        }
      ]
    }
  },
  mounted() {
    this.getList()
  },
  methods: {
    getList() {
      this.tableLoading = true
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      pageSampleRecord({ ...params, ...this.page })
        .then(res => {
          this.tableData = res.data.records || []
          this.page.total = res.data.total || 0
        })
        .finally(() => (this.tableLoading = false))
    },
    handleQuery() {
      this.page.current = 1
      this.getList()
    },
    resetQuery() {
      this.queryParams = {}
      this.timeRange = []
      this.handleQuery()
    },
    handleExport() {
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      exportSampleRecord(params).then(res => {
        this.downloadFile(res, '样品领样记录.xlsx')
      })
    },
    handleFlow(row) {
      getSampleFlow(row.id).then(res => {
        this.flowData = res.data || []
        this.flowVisible = true
      })
    },
    handlePagination({ page, limit }) {
      this.page.current = page
      this.page.size = limit
      this.getList()
    },
    formatStatus(val) {
      const map = { 0: '在库', 1: '已领用', 2: '已归还', 3: '已处理' }
      return map[val] || ''
    },
    formatStatusType(val) {
      const map = { 0: 'success', 1: 'warning', 2: 'info', 3: 'danger' }
      return map[val] || ''
    },
    getTimelineColor(status) {
      const map = { 0: '#67C23A', 1: '#E6A23C', 2: '#909399', 3: '#F56C6C' }
      return map[status] || '#409EFF'
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
src/views/report/spcChart/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,326 @@
<template>
  <div class="app-container">
    <!-- åˆ†æžé…ç½®è¡¨å• -->
    <el-card shadow="hover" style="margin-bottom: 20px;">
      <div slot="header">SPC分析配置</div>
      <el-form ref="analysisForm" :model="analysisParams" :inline="true" size="small" label-width="100px">
        <el-form-item label="检测项目" prop="projectId">
          <el-select v-model="analysisParams.projectId" placeholder="请选择检测项目" style="width: 200px" @change="handleProjectChange">
            <el-option v-for="item in projectList" :key="item.id" :label="item.projectName" :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="检测参数" prop="paramName">
          <el-select v-model="analysisParams.paramName" placeholder="请选择检测参数" style="width: 200px">
            <el-option v-for="item in paramList" :key="item.paramName" :label="item.paramName" :value="item.paramName" />
          </el-select>
        </el-form-item>
        <el-form-item label="时间范围" prop="timeRange">
          <el-date-picker
            v-model="analysisParams.timeRange"
            type="daterange"
            range-separator="-"
            start-placeholder="开始时间"
            end-placeholder="结束时间"
            value-format="yyyy-MM-dd"
            style="width: 240px"
          />
        </el-form-item>
        <el-form-item label="子组大小" prop="subgroupSize">
          <el-input-number v-model="analysisParams.subgroupSize" :min="2" :max="25" style="width: 150px" />
        </el-form-item>
        <el-form-item label="控制上限UCL" prop="ucl">
          <el-input-number v-model="analysisParams.ucl" :precision="4" style="width: 150px" />
        </el-form-item>
        <el-form-item label="控制下限LCL" prop="lcl">
          <el-input-number v-model="analysisParams.lcl" :precision="4" style="width: 150px" />
        </el-form-item>
        <el-form-item label="目标值CL" prop="targetValue">
          <el-input-number v-model="analysisParams.targetValue" :precision="4" style="width: 150px" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="el-icon-data-analysis" @click="handleAnalysis">开始分析</el-button>
          <el-button type="success" icon="el-icon-download" @click="handleExport">导出数据</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- SPC图表区域 -->
    <el-row :gutter="20" v-if="analysisResult">
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">X-Bar控制图</div>
          <Echart
            :xAxis="xBarXAxis"
            :yAxis="xBarYAxis"
            :series="xBarSeries"
            :tooltip="{ trigger: 'axis' }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '350px' }"
          />
        </el-card>
      </el-col>
      <el-col :span="12">
        <el-card shadow="hover">
          <div slot="header">R控制图</div>
          <Echart
            :xAxis="rChartXAxis"
            :yAxis="rChartYAxis"
            :series="rChartSeries"
            :tooltip="{ trigger: 'axis' }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '350px' }"
          />
        </el-card>
      </el-col>
    </el-row>
    <!-- åˆ¶ç¨‹èƒ½åŠ›åˆ†æž -->
    <el-card shadow="hover" style="margin-top: 20px;" v-if="analysisResult">
      <div slot="header">制程能力分析</div>
      <el-row :gutter="20">
        <el-col :span="6">
          <div class="capability-card">
            <div class="capability-label">Cp (制程精密度)</div>
            <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.cp) }">
              {{ analysisResult.capability.cp || '--' }}
            </div>
            <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.cp) }}</div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="capability-card">
            <div class="capability-label">Cpk (制程精确度)</div>
            <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.cpk) }">
              {{ analysisResult.capability.cpk || '--' }}
            </div>
            <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.cpk) }}</div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="capability-card">
            <div class="capability-label">Pp (过程精密度)</div>
            <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.pp) }">
              {{ analysisResult.capability.pp || '--' }}
            </div>
            <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.pp) }}</div>
          </div>
        </el-col>
        <el-col :span="6">
          <div class="capability-card">
            <div class="capability-label">Ppk (过程精确度)</div>
            <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.ppk) }">
              {{ analysisResult.capability.ppk || '--' }}
            </div>
            <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.ppk) }}</div>
          </div>
        </el-col>
      </el-row>
    </el-card>
    <!-- åˆ†æžæ•°æ®è¡¨æ ¼ -->
    <el-card shadow="hover" style="margin-top: 20px;" v-if="analysisResult">
      <div slot="header">分析数据明细</div>
      <el-table :data="analysisDataTable" border style="width: 100%" max-height="400">
        <el-table-column prop="subgroupNo" label="子组号" width="80" />
        <el-table-column prop="xBar" label="X均值" width="100" />
        <el-table-column prop="range" label="极差R" width="100" />
        <el-table-column v-for="(item, index) in subgroupColumns" :key="index" :prop="`value${index + 1}`" :label="`数据${index + 1}`" width="100" />
        <el-table-column prop="status" label="状态" width="100">
          <template slot-scope="scope">
            <el-tag :type="scope.row.status === '正常' ? 'success' : 'danger'">{{ scope.row.status }}</el-tag>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import { spcAnalyze, getProjectList, getParamList, exportSpcData } from '@/api/report/spcChart'
export default {
  name: 'SpcChart',
  components: { Echart },
  data() {
    return {
      projectList: [],
      paramList: [],
      analysisParams: {
        projectId: null,
        paramName: null,
        timeRange: [],
        subgroupSize: 5,
        ucl: null,
        lcl: null,
        targetValue: null
      },
      analysisResult: null,
      analysisDataTable: [],
      subgroupColumns: [],
      // X-Bar图表
      xBarXAxis: [{ type: 'category', data: [] }],
      xBarYAxis: [{ type: 'value' }],
      xBarSeries: [
        { name: 'X均值', type: 'line', data: [], markLine: { data: [] } }
      ],
      // R图表
      rChartXAxis: [{ type: 'category', data: [] }],
      rChartYAxis: [{ type: 'value' }],
      rChartSeries: [
        { name: '极差R', type: 'line', data: [], markLine: { data: [] } }
      ]
    }
  },
  mounted() {
    this.getProjectList()
  },
  methods: {
    getProjectList() {
      getProjectList().then(res => {
        this.projectList = res.data || []
      })
    },
    handleProjectChange(projectId) {
      this.analysisParams.paramName = null
      getParamList(projectId).then(res => {
        this.paramList = res.data || []
      })
    },
    handleAnalysis() {
      if (!this.analysisParams.projectId) {
        this.$message.warning('请选择检测项目')
        return
      }
      if (!this.analysisParams.paramName) {
        this.$message.warning('请选择检测参数')
        return
      }
      const params = {
        projectId: this.analysisParams.projectId,
        paramName: this.analysisParams.paramName,
        subgroupSize: this.analysisParams.subgroupSize,
        ucl: this.analysisParams.ucl,
        lcl: this.analysisParams.lcl,
        targetValue: this.analysisParams.targetValue
      }
      if (this.analysisParams.timeRange && this.analysisParams.timeRange.length === 2) {
        params.startDate = this.analysisParams.timeRange[0]
        params.endDate = this.analysisParams.timeRange[1]
      }
      spcAnalyze(params).then(res => {
        this.analysisResult = res.data
        this.renderCharts(res.data)
        this.renderDataTable(res.data)
        this.$message.success('分析完成')
      })
    },
    renderCharts(data) {
      const xBarData = data.xBar || {}
      const rChartData = data.rChart || {}
      const subgroupLabels = (xBarData.data || []).map((_, i) => `组${i + 1}`)
      // X-Bar图
      this.xBarXAxis[0].data = subgroupLabels
      this.xBarSeries[0].data = xBarData.data || []
      this.xBarSeries[0].markLine = {
        data: [
          { yAxis: xBarData.ucl, name: 'UCL', lineStyle: { color: '#F56C6C' } },
          { yAxis: xBarData.lcl, name: 'LCL', lineStyle: { color: '#F56C6C' } },
          { yAxis: xBarData.cl, name: 'CL', lineStyle: { color: '#409EFF' } }
        ]
      }
      // R图
      this.rChartXAxis[0].data = subgroupLabels
      this.rChartSeries[0].data = rChartData.data || []
      this.rChartSeries[0].markLine = {
        data: [
          { yAxis: rChartData.ucl, name: 'UCL', lineStyle: { color: '#F56C6C' } },
          { yAxis: rChartData.lcl, name: 'LCL', lineStyle: { color: '#F56C6C' } },
          { yAxis: rChartData.cl, name: 'CL', lineStyle: { color: '#409EFF' } }
        ]
      }
    },
    renderDataTable(data) {
      const subgroupSize = this.analysisParams.subgroupSize
      this.subgroupColumns = []
      for (let i = 1; i <= subgroupSize; i++) {
        this.subgroupColumns.push({ prop: `value${i}` })
      }
      const rawData = data.rawData || []
      const xBarData = data.xBar?.data || []
      const rChartData = data.rChart?.data || []
      const ucl = data.xBar?.ucl
      const lcl = data.xBar?.lcl
      this.analysisDataTable = rawData.map((group, index) => {
        const xBar = xBarData[index]
        const status = (ucl && lcl) ? (xBar >= lcl && xBar <= ucl ? '正常' : '异常') : '正常'
        const row = {
          subgroupNo: index + 1,
          xBar: xBar?.toFixed(4),
          range: rChartData[index]?.toFixed(4),
          status
        }
        group.forEach((val, i) => {
          row[`value${i + 1}`] = val?.toFixed(4)
        })
        return row
      })
    },
    handleExport() {
      if (!this.analysisResult) {
        this.$message.warning('请先进行SPC分析')
        return
      }
      const params = {
        projectId: this.analysisParams.projectId,
        paramName: this.analysisParams.paramName
      }
      exportSpcData(params).then(res => {
        this.downloadFile(res, 'SPC分析数据.xlsx')
      })
    },
    getCapabilityColor(val) {
      if (val >= 1.33) return '#67C23A'
      if (val >= 1) return '#E6A23C'
      return '#F56C6C'
    },
    getCapabilityStatus(val) {
      if (val >= 1.33) return '制程能力优秀'
      if (val >= 1) return '制程能力合格'
      return '制程能力不足'
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
<style scoped>
.capability-card {
  text-align: center;
  padding: 20px;
  background: #f5f7fa;
  border-radius: 8px;
}
.capability-label {
  font-size: 14px;
  color: #909399;
}
.capability-value {
  font-size: 32px;
  font-weight: bold;
  margin-top: 10px;
}
.capability-status {
  font-size: 12px;
  color: #909399;
  margin-top: 5px;
}
</style>
src/views/report/testItemData/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,195 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="queryForm" :model="queryParams" :inline="true" size="small">
      <el-form-item label="生产订单" prop="orderCode">
        <el-input v-model="queryParams.orderCode" placeholder="请输入生产订单" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="批次号" prop="batchCode">
        <el-input v-model="queryParams.batchCode" placeholder="请输入批次号" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="样品编号" prop="sampleCode">
        <el-input v-model="queryParams.sampleCode" placeholder="请输入样品编号" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="样品名称" prop="sampleName">
        <el-input v-model="queryParams.sampleName" placeholder="请输入样品名称" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="检测项目" prop="testItem">
        <el-input v-model="queryParams.testItem" placeholder="请输入检测项目" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
        <el-button type="warning" icon="el-icon-sort" @click="handleCompare">横向比较</el-button>
      </el-form-item>
    </el-form>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <lims-table
      :tableData="tableData"
      :column="tableColumn"
      :page="page"
      :tableLoading="tableLoading"
      @pagination="handlePagination"
    >
      <template #operation="{ row }">
        <el-button type="text" size="mini" @click="handleDetail(row)">查看详情</el-button>
      </template>
    </lims-table>
    <!-- è¯¦æƒ…弹窗 -->
    <el-dialog title="检测项目详情" :visible.sync="detailVisible" width="80%" top="5vh">
      <el-descriptions :column="3" border>
        <el-descriptions-item label="样品编号">{{ detailData.sampleCode }}</el-descriptions-item>
        <el-descriptions-item label="样品名称">{{ detailData.sampleName }}</el-descriptions-item>
        <el-descriptions-item label="委托编号">{{ detailData.entrustCode }}</el-descriptions-item>
        <el-descriptions-item label="检测项目">{{ detailData.testItem }}</el-descriptions-item>
        <el-descriptions-item label="检测结果">{{ detailData.testResult }}</el-descriptions-item>
        <el-descriptions-item label="标准值">{{ detailData.standardValue }}</el-descriptions-item>
        <el-descriptions-item label="检测人">{{ detailData.tester }}</el-descriptions-item>
        <el-descriptions-item label="检测时间">{{ detailData.testTime }}</el-descriptions-item>
        <el-descriptions-item label="检测设备">{{ detailData.deviceName }}</el-descriptions-item>
      </el-descriptions>
      <div style="margin-top: 20px;">
        <h4>检测数据明细</h4>
        <el-table :data="detailData.dataList" border style="width: 100%">
          <el-table-column prop="paramName" label="参数名称" />
          <el-table-column prop="standardValue" label="标准值" />
          <el-table-column prop="actualValue" label="实测值" />
          <el-table-column prop="unit" label="单位" />
          <el-table-column prop="result" label="判定结果">
            <template slot-scope="scope">
              <el-tag :type="scope.row.result === '合格' ? 'success' : 'danger'">{{ scope.row.result }}</el-tag>
            </template>
          </el-table-column>
        </el-table>
      </div>
    </el-dialog>
    <!-- æ¨ªå‘比较弹窗 -->
    <el-dialog title="检测数据横向比较" :visible.sync="compareVisible" width="90%" top="5vh">
      <el-table :data="compareData" border style="width: 100%" max-height="500">
        <el-table-column prop="sampleCode" label="样品编号" fixed width="140" />
        <el-table-column prop="sampleName" label="样品名称" fixed width="150" />
        <el-table-column v-for="item in compareColumns" :key="item.prop" :prop="item.prop" :label="item.label" min-width="100">
          <template slot-scope="scope">
            <span :style="{ color: getCompareColor(scope.row[item.prop], item.standardValue) }">
              {{ scope.row[item.prop] }}
            </span>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
  </div>
</template>
<script>
import limsTable from '@/components/Table/lims-table.vue'
import { pageTestItemData, getTestItemDetail, compareTestItem, exportTestItemData } from '@/api/report/testItemData'
export default {
  name: 'TestItemData',
  components: { limsTable },
  data() {
    return {
      queryParams: {},
      tableData: [],
      tableLoading: false,
      page: { total: 0, size: 10, current: 1 },
      detailVisible: false,
      detailData: {},
      compareVisible: false,
      compareData: [],
      compareColumns: [],
      tableColumn: [
        { label: '委托编号', prop: 'entrustCode', minWidth: '140px' },
        { label: '生产订单', prop: 'orderCode', minWidth: '140px' },
        { label: '批次号', prop: 'batchCode', minWidth: '120px' },
        { label: '样品编号', prop: 'sampleCode', minWidth: '140px' },
        { label: '样品名称', prop: 'sampleName', minWidth: '150px' },
        { label: '检测项目', prop: 'testItem', minWidth: '120px' },
        {
          label: '检测结果',
          prop: 'testResult',
          minWidth: '100px',
          dataType: 'tag',
          formatData: (val) => val === 1 ? '合格' : '不合格',
          formatType: (val) => val === 1 ? 'success' : 'danger'
        },
        { label: '检测人', prop: 'tester', minWidth: '80px' },
        { label: '检测时间', prop: 'testTime', minWidth: '160px' },
        { label: '报告编号', prop: 'reportCode', minWidth: '140px' },
        {
          label: '操作',
          dataType: 'slot',
          slot: 'operation',
          minWidth: '100px'
        }
      ]
    }
  },
  mounted() {
    this.getList()
  },
  methods: {
    getList() {
      this.tableLoading = true
      pageTestItemData({ ...this.queryParams, ...this.page })
        .then(res => {
          this.tableData = res.data.records || []
          this.page.total = res.data.total || 0
        })
        .finally(() => (this.tableLoading = false))
    },
    handleQuery() {
      this.page.current = 1
      this.getList()
    },
    resetQuery() {
      this.queryParams = {}
      this.handleQuery()
    },
    handleExport() {
      exportTestItemData(this.queryParams).then(res => {
        this.downloadFile(res, '检测项目数据.xlsx')
      })
    },
    handleDetail(row) {
      getTestItemDetail(row.id).then(res => {
        this.detailData = res.data || {}
        this.detailVisible = true
      })
    },
    handleCompare() {
      compareTestItem(this.queryParams).then(res => {
        this.compareData = res.data.dataList || []
        this.compareColumns = (res.data.columns || []).map(col => ({
          prop: col.field,
          label: col.name,
          standardValue: col.standardValue
        }))
        this.compareVisible = true
      })
    },
    handlePagination({ page, limit }) {
      this.page.current = page
      this.page.size = limit
      this.getList()
    },
    getCompareColor(value, standard) {
      if (!standard) return ''
      return value >= standard.min && value <= standard.max ? '' : '#F56C6C'
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
src/views/report/workStatistics/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,326 @@
<template>
  <div class="app-container">
    <!-- æœç´¢è¡¨å• -->
    <el-form ref="queryForm" :model="queryParams" :inline="true" size="small">
      <el-form-item label="人员姓名" prop="userName">
        <el-input v-model="queryParams.userName" placeholder="请输入人员姓名" clearable style="width: 150px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="部门" prop="dept">
        <el-input v-model="queryParams.dept" placeholder="请输入部门" clearable style="width: 180px" @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="时间范围" prop="timeRange">
        <el-date-picker
          v-model="timeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始时间"
          end-placeholder="结束时间"
          value-format="yyyy-MM-dd"
          style="width: 240px"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
        <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button>
      </el-form-item>
    </el-form>
    <!-- Tab切换 -->
    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
      <el-tab-pane label="人员工作统计" name="user">
        <!-- ç»Ÿè®¡å¡ç‰‡ -->
        <el-row :gutter="20" style="margin-bottom: 20px;">
          <el-col :span="6">
            <el-card shadow="hover">
              <div class="stat-card">
                <div class="stat-title">检测样品总数</div>
                <div class="stat-value">{{ userStatistics.totalSamples || 0 }}</div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="6">
            <el-card shadow="hover">
              <div class="stat-card">
                <div class="stat-title">检测项目总数</div>
                <div class="stat-value">{{ userStatistics.totalItems || 0 }}</div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="6">
            <el-card shadow="hover">
              <div class="stat-card">
                <div class="stat-title">平均检测及时率</div>
                <div class="stat-value">{{ userStatistics.avgTimelyRate || 0 }}%</div>
              </div>
            </el-card>
          </el-col>
          <el-col :span="6">
            <el-card shadow="hover">
              <div class="stat-card">
                <div class="stat-title">参与人员数</div>
                <div class="stat-value">{{ userStatistics.userCount || 0 }}</div>
              </div>
            </el-card>
          </el-col>
        </el-row>
        <!-- æ•°æ®è¡¨æ ¼ -->
        <lims-table
          :tableData="userTableData"
          :column="userTableColumn"
          :page="page"
          :tableLoading="tableLoading"
          @pagination="handlePagination"
        />
      </el-tab-pane>
      <el-tab-pane label="及时率统计" name="timely">
        <!-- åŠæ—¶çŽ‡å›¾è¡¨ -->
        <el-row :gutter="20" style="margin-bottom: 20px;">
          <el-col :span="12">
            <el-card shadow="hover">
              <div slot="header">样品负责人及时率</div>
              <Echart
                :xAxis="chargeTimelyXAxis"
                :yAxis="chargeTimelyYAxis"
                :series="chargeTimelySeries"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
                :chartStyle="{ height: '350px' }"
              />
            </el-card>
          </el-col>
          <el-col :span="12">
            <el-card shadow="hover">
              <div slot="header">试验员及时率</div>
              <Echart
                :xAxis="testerTimelyXAxis"
                :yAxis="testerTimelyYAxis"
                :series="testerTimelySeries"
                :tooltip="{ trigger: 'axis' }"
                :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
                :chartStyle="{ height: '350px' }"
              />
            </el-card>
          </el-col>
        </el-row>
        <!-- åŠæ—¶çŽ‡æ•°æ®è¡¨æ ¼ -->
        <lims-table
          :tableData="timelyTableData"
          :column="timelyTableColumn"
          :page="timelyPage"
          :tableLoading="timelyTableLoading"
          @pagination="handleTimelyPagination"
        />
      </el-tab-pane>
      <el-tab-pane label="工作趋势" name="trend">
        <el-card shadow="hover">
          <div slot="header">
            <span>工作趋势图</span>
            <el-radio-group v-model="trendType" size="mini" style="float: right;" @change="getTrendData">
              <el-radio-button label="week">近一周</el-radio-button>
              <el-radio-button label="month">近一月</el-radio-button>
              <el-radio-button label="year">近一年</el-radio-button>
            </el-radio-group>
          </div>
          <Echart
            :xAxis="trendXAxis"
            :yAxis="trendYAxis"
            :series="trendSeries"
            :tooltip="{ trigger: 'axis' }"
            :legend="{ data: ['样品数', '项目数', '及时率'] }"
            :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }"
            :chartStyle="{ height: '400px' }"
          />
        </el-card>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>
<script>
import Echart from '@/components/echarts/echarts.vue'
import limsTable from '@/components/Table/lims-table.vue'
import { getStatisticsByUser, getTimelyRate, getWorkTrend, exportWorkStatistics } from '@/api/report/workStatistics'
export default {
  name: 'WorkStatistics',
  components: { Echart, limsTable },
  data() {
    return {
      queryParams: {},
      timeRange: [],
      activeTab: 'user',
      tableLoading: false,
      userTableData: [],
      userStatistics: {},
      page: { total: 0, size: 10, current: 1 },
      userTableColumn: [
        { label: '人员姓名', prop: 'userName', minWidth: '100px' },
        { label: '部门', prop: 'dept', minWidth: '120px' },
        { label: '检测样品数', prop: 'sampleCount', minWidth: '100px' },
        { label: '检测项目数', prop: 'itemCount', minWidth: '100px' },
        { label: '及时完成数', prop: 'timelyCount', minWidth: '100px' },
        { label: '超期完成数', prop: 'overdueCount', minWidth: '100px' },
        { label: '及时率', prop: 'timelyRate', minWidth: '100px', formatData: (val) => `${val}%` },
        { label: '平均用时(h)', prop: 'avgTime', minWidth: '100px' }
      ],
      // åŠæ—¶çŽ‡æ•°æ®
      timelyTableLoading: false,
      timelyTableData: [],
      timelyPage: { total: 0, size: 10, current: 1 },
      timelyTableColumn: [
        { label: '人员姓名', prop: 'userName', minWidth: '100px' },
        { label: '角色', prop: 'roleType', minWidth: '100px' },
        { label: '负责样品数', prop: 'sampleCount', minWidth: '100px' },
        { label: '按时完成数', prop: 'timelyCount', minWidth: '100px' },
        { label: '超期数', prop: 'overdueCount', minWidth: '100px' },
        { label: '及时率', prop: 'timelyRate', minWidth: '100px', dataType: 'tag', formatData: (val) => `${val}%`, formatType: (val) => val >= 90 ? 'success' : val >= 70 ? 'warning' : 'danger' }
      ],
      trendType: 'week',
      // å›¾è¡¨é…ç½®
      chargeTimelyXAxis: [{ type: 'category', data: [] }],
      chargeTimelyYAxis: [{ type: 'value', max: 100 }],
      chargeTimelySeries: [{ name: '及时率', type: 'bar', data: [] }],
      testerTimelyXAxis: [{ type: 'category', data: [] }],
      testerTimelyYAxis: [{ type: 'value', max: 100 }],
      testerTimelySeries: [{ name: '及时率', type: 'bar', data: [] }],
      trendXAxis: [{ type: 'category', data: [] }],
      trendYAxis: [{ type: 'value' }, { type: 'value', max: 100, position: 'right' }],
      trendSeries: [
        { name: '样品数', type: 'bar', data: [] },
        { name: '项目数', type: 'bar', data: [] },
        { name: '及时率', type: 'line', yAxisIndex: 1, data: [] }
      ]
    }
  },
  mounted() {
    this.getUserData()
  },
  methods: {
    handleTabChange(tab) {
      if (tab.name === 'user') {
        this.getUserData()
      } else if (tab.name === 'timely') {
        this.getTimelyData()
      } else if (tab.name === 'trend') {
        this.getTrendData()
      }
    },
    getUserData() {
      this.tableLoading = true
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      getStatisticsByUser({ ...params, ...this.page })
        .then(res => {
          this.userTableData = res.data.records || []
          this.page.total = res.data.total || 0
          this.userStatistics = res.data.statistics || {}
        })
        .finally(() => (this.tableLoading = false))
    },
    getTimelyData() {
      this.timelyTableLoading = true
      const params = { ...this.queryParams }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      getTimelyRate({ ...params, ...this.timelyPage })
        .then(res => {
          this.timelyTableData = res.data.records || []
          this.timelyPage.total = res.data.total || 0
          // å›¾è¡¨æ•°æ®
          const chargeData = (res.data.chargeList || []).slice(0, 10)
          this.chargeTimelyXAxis[0].data = chargeData.map(item => item.userName)
          this.chargeTimelySeries[0].data = chargeData.map(item => item.timelyRate)
          const testerData = (res.data.testerList || []).slice(0, 10)
          this.testerTimelyXAxis[0].data = testerData.map(item => item.userName)
          this.testerTimelySeries[0].data = testerData.map(item => item.timelyRate)
        })
        .finally(() => (this.timelyTableLoading = false))
    },
    getTrendData() {
      const params = { timeType: this.trendType }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      getWorkTrend(params).then(res => {
        this.trendXAxis[0].data = res.data.dates || []
        this.trendSeries[0].data = res.data.sampleCounts || []
        this.trendSeries[1].data = res.data.itemCounts || []
        this.trendSeries[2].data = res.data.timelyRates || []
      })
    },
    handleQuery() {
      if (this.activeTab === 'user') {
        this.page.current = 1
        this.getUserData()
      } else if (this.activeTab === 'timely') {
        this.timelyPage.current = 1
        this.getTimelyData()
      } else {
        this.getTrendData()
      }
    },
    resetQuery() {
      this.queryParams = {}
      this.timeRange = []
      this.handleQuery()
    },
    handleExport() {
      const params = { ...this.queryParams, type: this.activeTab }
      if (this.timeRange && this.timeRange.length === 2) {
        params.startTime = this.timeRange[0]
        params.endTime = this.timeRange[1]
      }
      exportWorkStatistics(params).then(res => {
        this.downloadFile(res, '工作统计.xlsx')
      })
    },
    handlePagination({ page, limit }) {
      this.page.current = page
      this.page.size = limit
      this.getUserData()
    },
    handleTimelyPagination({ page, limit }) {
      this.timelyPage.current = page
      this.timelyPage.size = limit
      this.getTimelyData()
    },
    downloadFile(data, fileName) {
      const blob = new Blob([data])
      const url = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = fileName
      link.click()
      window.URL.revokeObjectURL(url)
    }
  }
}
</script>
<style scoped>
.stat-card {
  text-align: center;
  padding: 15px 0;
}
.stat-title {
  font-size: 14px;
  color: #909399;
}
.stat-value {
  font-size: 28px;
  font-weight: bold;
  color: #303133;
  margin-top: 10px;
}
</style>