From 1a7678abd99b9bdc5682e3ea27e49d65e91f37d0 Mon Sep 17 00:00:00 2001
From: liding <756868258@qq.com>
Date: 星期四, 04 六月 2026 17:12:33 +0800
Subject: [PATCH] feat(report): 报表图表管理 1.报表管理(样品进度报表,检测项目数据,样品领样记录,设备使用记录) 2.数字化语音看板 3.智能图表
---
src/api/report/workStatistics.js | 39
src/views/report/workStatistics/index.vue | 326 ++++++
src/views/report/sampleRecord/index.vue | 177 +++
src/views/report/spcChart/index.vue | 326 ++++++
src/api/report/testItemData.js | 39
src/api/report/passRate.js | 57 +
src/api/report/normalDistribution.js | 39
src/api/report/spcChart.js | 48
src/views/report/normalDistribution/index.vue | 303 +++++
src/views/report/testItemData/index.vue | 195 +++
src/views/report/deviceRecord/index.vue | 235 ++++
src/views/report/passRate/index.vue | 325 ++++++
src/api/report/deviceRecord.js | 30
src/views/report/dashboard/index.vue | 496 +++++++++
src/api/report/sampleRecord.js | 30
src/api/report/dashboard.js | 56 +
src/api/report/sampleProgress.js | 39
src/views/report/sampleProgress/index.vue | 300 +++++
18 files changed, 3,060 insertions(+), 0 deletions(-)
diff --git a/src/api/report/dashboard.js b/src/api/report/dashboard.js
new file mode 100644
index 0000000..f45db38
--- /dev/null
+++ b/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
+ })
+}
\ No newline at end of file
diff --git a/src/api/report/deviceRecord.js b/src/api/report/deviceRecord.js
new file mode 100644
index 0000000..a2b803c
--- /dev/null
+++ b/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'
+ })
+}
diff --git a/src/api/report/normalDistribution.js b/src/api/report/normalDistribution.js
new file mode 100644
index 0000000..e8d1fd9
--- /dev/null
+++ b/src/api/report/normalDistribution.js
@@ -0,0 +1,39 @@
+// 姝f�佸垎甯冨浘鎺ュ彛
+import request from '@/utils/request'
+
+// 姝f�佸垎甯冨垎鏋�
+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 }
+ })
+}
diff --git a/src/api/report/passRate.js b/src/api/report/passRate.js
new file mode 100644
index 0000000..96d58f3
--- /dev/null
+++ b/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'
+ })
+}
diff --git a/src/api/report/sampleProgress.js b/src/api/report/sampleProgress.js
new file mode 100644
index 0000000..f9b6119
--- /dev/null
+++ b/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
+ })
+}
diff --git a/src/api/report/sampleRecord.js b/src/api/report/sampleRecord.js
new file mode 100644
index 0000000..2db4c74
--- /dev/null
+++ b/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'
+ })
+}
diff --git a/src/api/report/spcChart.js b/src/api/report/spcChart.js
new file mode 100644
index 0000000..170744f
--- /dev/null
+++ b/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 }
+ })
+}
diff --git a/src/api/report/testItemData.js b/src/api/report/testItemData.js
new file mode 100644
index 0000000..353eb2e
--- /dev/null
+++ b/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'
+ })
+}
diff --git a/src/api/report/workStatistics.js b/src/api/report/workStatistics.js
new file mode 100644
index 0000000..67f377c
--- /dev/null
+++ b/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'
+ })
+}
diff --git a/src/views/report/dashboard/index.vue b/src/views/report/dashboard/index.vue
new file mode 100644
index 0000000..a0e3b7c
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/views/report/deviceRecord/index.vue b/src/views/report/deviceRecord/index.vue
new file mode 100644
index 0000000..3641878
--- /dev/null
+++ b/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>
diff --git a/src/views/report/normalDistribution/index.vue b/src/views/report/normalDistribution/index.vue
new file mode 100644
index 0000000..2386063
--- /dev/null
+++ b/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">姝f�佸垎甯冨垎鏋愰厤缃�</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">
+ <!-- 姝f�佸垎甯冨浘 -->
+ <el-col :span="16">
+ <el-card shadow="hover">
+ <div slot="header">姝f�佸垎甯冨浘</div>
+ <Echart
+ :xAxis="distributionXAxis"
+ :yAxis="distributionYAxis"
+ :series="distributionSeries"
+ :tooltip="{ trigger: 'axis' }"
+ :legend="{ data: ['棰戞暟', '姝f�佹洸绾�'] }"
+ :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
+ },
+ // 姝f�佸垎甯冨浘
+ distributionXAxis: [{ type: 'category', data: [] }],
+ distributionYAxis: [{ type: 'value' }],
+ distributionSeries: [
+ { name: '棰戞暟', type: 'bar', data: [], barWidth: '60%' },
+ { name: '姝f�佹洸绾�', 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)
+ // 姝f�佹洸绾挎暟鎹�
+ 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, '姝f�佸垎甯冨垎鏋愭暟鎹�.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>
\ No newline at end of file
diff --git a/src/views/report/passRate/index.vue b/src/views/report/passRate/index.vue
new file mode 100644
index 0000000..2b5a6a0
--- /dev/null
+++ b/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>
diff --git a/src/views/report/sampleProgress/index.vue b/src/views/report/sampleProgress/index.vue
new file mode 100644
index 0000000..3506409
--- /dev/null
+++ b/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>
diff --git a/src/views/report/sampleRecord/index.vue b/src/views/report/sampleRecord/index.vue
new file mode 100644
index 0000000..4cbafd4
--- /dev/null
+++ b/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>
diff --git a/src/views/report/spcChart/index.vue b/src/views/report/spcChart/index.vue
new file mode 100644
index 0000000..66c7015
--- /dev/null
+++ b/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="鐩爣鍊糃L" 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 === '姝e父' ? '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 ? '姝e父' : '寮傚父') : '姝e父'
+ 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>
\ No newline at end of file
diff --git a/src/views/report/testItemData/index.vue b/src/views/report/testItemData/index.vue
new file mode 100644
index 0000000..3040919
--- /dev/null
+++ b/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>
diff --git a/src/views/report/workStatistics/index.vue b/src/views/report/workStatistics/index.vue
new file mode 100644
index 0000000..0209a5b
--- /dev/null
+++ b/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>
--
Gitblit v1.9.3