feat(report): 报表图表管理
1.报表管理(样品进度报表,检测项目数据,样品领样记录,设备使用记录)
2.数字化语音看板
3.智能图表
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ°ååè¯é³çæ¿ - è¯éªå¤§å
æ¥å£ |
| | | 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 |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 设å¤ä½¿ç¨è®°å½æ¥å£ |
| | | 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' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ£æåå¸å¾æ¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | // æ£æåå¸åæ |
| | | export function normalDistributionAnalyze(data) { |
| | | return request({ |
| | | url: '/chart/normalDistribution/analyze', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // 导åºåææ°æ® |
| | | export function exportNormalDistribution(query) { |
| | | return request({ |
| | | url: '/chart/normalDistribution/export', |
| | | method: 'get', |
| | | params: query, |
| | | responseType: 'blob' |
| | | }) |
| | | } |
| | | |
| | | // è·åæ£æµé¡¹ç®å表 |
| | | export function getProjectList(query) { |
| | | return request({ |
| | | url: '/chart/normalDistribution/projectList', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | | |
| | | // è·åæ£æµåæ°å表 |
| | | export function getParamList(projectId) { |
| | | return request({ |
| | | url: '/chart/normalDistribution/paramList', |
| | | method: 'get', |
| | | params: { projectId } |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // åæ ¼çç»è®¡æ¥å£ |
| | | 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' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ ·åè¿åº¦æ¥è¡¨æ¥å£ |
| | | 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 |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ ·å颿 ·è®°å½æ¥å£ |
| | | 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' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 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 } |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ£æµé¡¹ç®æ°æ®æ¥å£ |
| | | 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' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // å·¥ä½ç»è®¡æ¥å£ |
| | | 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' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- åæé
置表å --> |
| | | <el-card shadow="hover" style="margin-bottom: 20px;"> |
| | | <div slot="header">æ£æåå¸åæé
ç½®</div> |
| | | <el-form ref="analysisForm" :model="analysisParams" :inline="true" size="small" label-width="100px"> |
| | | <el-form-item label="æ£æµé¡¹ç®" prop="projectId"> |
| | | <el-select v-model="analysisParams.projectId" placeholder="è¯·éæ©æ£æµé¡¹ç®" style="width: 200px" @change="handleProjectChange"> |
| | | <el-option v-for="item in projectList" :key="item.id" :label="item.projectName" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ£æµåæ°" prop="paramName"> |
| | | <el-select v-model="analysisParams.paramName" placeholder="è¯·éæ©æ£æµåæ°" style="width: 200px"> |
| | | <el-option v-for="item in paramList" :key="item.paramName" :label="item.paramName" :value="item.paramName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¶é´èå´" prop="timeRange"> |
| | | <el-date-picker |
| | | v-model="analysisParams.timeRange" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="å¼å§æ¶é´" |
| | | end-placeholder="ç»ææ¶é´" |
| | | value-format="yyyy-MM-dd" |
| | | style="width: 240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-data-analysis" @click="handleAnalysis">å¼å§åæ</el-button> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">å¯¼åºæ°æ®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <el-row :gutter="20" v-if="analysisResult"> |
| | | <!-- æ£æåå¸å¾ --> |
| | | <el-col :span="16"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header">æ£æåå¸å¾</div> |
| | | <Echart |
| | | :xAxis="distributionXAxis" |
| | | :yAxis="distributionYAxis" |
| | | :series="distributionSeries" |
| | | :tooltip="{ trigger: 'axis' }" |
| | | :legend="{ data: ['颿°', 'æ£ææ²çº¿'] }" |
| | | :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }" |
| | | :chartStyle="{ height: '400px' }" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | <!-- ç»è®¡ä¿¡æ¯ --> |
| | | <el-col :span="8"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header">ç»è®¡ä¿¡æ¯</div> |
| | | <el-descriptions :column="1" border> |
| | | <el-descriptions-item label="æ ·æ¬æ°é">{{ analysisResult.statistics.sampleCount || 0 }}</el-descriptions-item> |
| | | <el-descriptions-item label="åå¼(μ)">{{ (analysisResult.statistics.mean || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ åå·®(Ï)">{{ (analysisResult.statistics.stdDev || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æå°å¼">{{ (analysisResult.statistics.min || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æå¤§å¼">{{ (analysisResult.statistics.max || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æå·®">{{ (analysisResult.statistics.range || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸ä½æ°">{{ (analysisResult.statistics.median || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="å度">{{ (analysisResult.statistics.skewness || 0).toFixed(4) }}</el-descriptions-item> |
| | | <el-descriptions-item label="峰度">{{ (analysisResult.statistics.kurtosis || 0).toFixed(4) }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- ç´æ¹å¾æ°æ®è¡¨æ ¼ --> |
| | | <el-row :gutter="20" style="margin-top: 20px;" v-if="analysisResult"> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header">颿°åå¸è¡¨</div> |
| | | <el-table :data="analysisResult.histogramData" border style="width: 100%"> |
| | | <el-table-column prop="interval" label="åºé´" /> |
| | | <el-table-column prop="frequency" label="颿°" /> |
| | | <el-table-column prop="relativeFreq" label="ç¸å¯¹é¢ç"> |
| | | <template slot-scope="scope"> |
| | | {{ (scope.row.relativeFreq * 100).toFixed(2) }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="cumulativeFreq" label="累计é¢ç"> |
| | | <template slot-scope="scope"> |
| | | {{ (scope.row.cumulativeFreq * 100).toFixed(2) }}% |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header">è¿ç¨è½å</div> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="capability-item"> |
| | | <div class="capability-label">è§æ ¼ä¸é(USL)</div> |
| | | <el-input-number v-model="analysisParams.usl" :precision="4" style="width: 100%;" @change="calculateCpk" /> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="capability-item"> |
| | | <div class="capability-label">è§æ ¼ä¸é(LSL)</div> |
| | | <el-input-number v-model="analysisParams.lsl" :precision="4" style="width: 100%;" @change="calculateCpk" /> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-divider /> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="capability-result"> |
| | | <div class="capability-label">Cp</div> |
| | | <div class="capability-value" :style="{ color: getCapabilityColor(calculatedCpk.cp) }"> |
| | | {{ calculatedCpk.cp ? calculatedCpk.cp.toFixed(4) : '--' }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="capability-result"> |
| | | <div class="capability-label">Cpk</div> |
| | | <div class="capability-value" :style="{ color: getCapabilityColor(calculatedCpk.cpk) }"> |
| | | {{ calculatedCpk.cpk ? calculatedCpk.cpk.toFixed(4) : '--' }} |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- åå§æ°æ® --> |
| | | <el-card shadow="hover" style="margin-top: 20px;" v-if="analysisResult"> |
| | | <div slot="header">åå§æ°æ®</div> |
| | | <el-table :data="rawDataTable" border style="width: 100%" max-height="300"> |
| | | <el-table-column type="index" label="åºå·" width="60" /> |
| | | <el-table-column prop="value" label="æ£æµå¼"> |
| | | <template slot-scope="scope"> |
| | | {{ scope.row.value ? scope.row.value.toFixed(4) : '' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="sampleCode" label="æ ·åç¼å·" /> |
| | | <el-table-column prop="testTime" label="æ£æµæ¶é´" /> |
| | | <el-table-column prop="tester" label="æ£æµäºº" /> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import Echart from '@/components/echarts/echarts.vue' |
| | | import { normalDistributionAnalyze, getProjectList, getParamList, exportNormalDistribution } from '@/api/report/normalDistribution' |
| | | |
| | | export default { |
| | | name: 'NormalDistribution', |
| | | components: { Echart }, |
| | | data() { |
| | | return { |
| | | projectList: [], |
| | | paramList: [], |
| | | analysisParams: { |
| | | projectId: null, |
| | | paramName: null, |
| | | timeRange: [], |
| | | usl: null, |
| | | lsl: null |
| | | }, |
| | | analysisResult: null, |
| | | rawDataTable: [], |
| | | calculatedCpk: { |
| | | cp: null, |
| | | cpk: null |
| | | }, |
| | | // æ£æåå¸å¾ |
| | | distributionXAxis: [{ type: 'category', data: [] }], |
| | | distributionYAxis: [{ type: 'value' }], |
| | | distributionSeries: [ |
| | | { name: '颿°', type: 'bar', data: [], barWidth: '60%' }, |
| | | { name: 'æ£ææ²çº¿', type: 'line', data: [], smooth: true } |
| | | ] |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.getProjectList() |
| | | }, |
| | | methods: { |
| | | getProjectList() { |
| | | getProjectList().then(res => { |
| | | this.projectList = res.data || [] |
| | | }) |
| | | }, |
| | | handleProjectChange(projectId) { |
| | | this.analysisParams.paramName = null |
| | | getParamList(projectId).then(res => { |
| | | this.paramList = res.data || [] |
| | | }) |
| | | }, |
| | | handleAnalysis() { |
| | | if (!this.analysisParams.projectId) { |
| | | this.$message.warning('è¯·éæ©æ£æµé¡¹ç®') |
| | | return |
| | | } |
| | | if (!this.analysisParams.paramName) { |
| | | this.$message.warning('è¯·éæ©æ£æµåæ°') |
| | | return |
| | | } |
| | | const params = { |
| | | projectId: this.analysisParams.projectId, |
| | | paramName: this.analysisParams.paramName |
| | | } |
| | | if (this.analysisParams.timeRange && this.analysisParams.timeRange.length === 2) { |
| | | params.startDate = this.analysisParams.timeRange[0] |
| | | params.endDate = this.analysisParams.timeRange[1] |
| | | } |
| | | normalDistributionAnalyze(params).then(res => { |
| | | this.analysisResult = res.data |
| | | this.renderCharts(res.data) |
| | | this.rawDataTable = (res.data.rawData || []).map(item => ({ |
| | | value: item.value, |
| | | sampleCode: item.sampleCode, |
| | | testTime: item.testTime, |
| | | tester: item.tester |
| | | })) |
| | | this.$message.success('åæå®æ') |
| | | }) |
| | | }, |
| | | renderCharts(data) { |
| | | const histogramData = data.histogramData || [] |
| | | const normalCurve = data.normalCurve || [] |
| | | // ç´æ¹å¾æ°æ® |
| | | this.distributionXAxis[0].data = histogramData.map(item => item.interval) |
| | | this.distributionSeries[0].data = histogramData.map(item => item.frequency) |
| | | // æ£ææ²çº¿æ°æ® |
| | | this.distributionSeries[1].data = normalCurve |
| | | }, |
| | | calculateCpk() { |
| | | if (!this.analysisResult || !this.analysisParams.usl || !this.analysisParams.lsl) { |
| | | return |
| | | } |
| | | const stats = this.analysisResult.statistics |
| | | const mean = stats.mean |
| | | const stdDev = stats.stdDev |
| | | const usl = this.analysisParams.usl |
| | | const lsl = this.analysisParams.lsl |
| | | // Cp = (USL - LSL) / (6Ï) |
| | | this.calculatedCpk.cp = (usl - lsl) / (6 * stdDev) |
| | | // Cpk = min[(USL - μ) / (3Ï), (μ - LSL) / (3Ï)] |
| | | const cpu = (usl - mean) / (3 * stdDev) |
| | | const cpl = (mean - lsl) / (3 * stdDev) |
| | | this.calculatedCpk.cpk = Math.min(cpu, cpl) |
| | | }, |
| | | handleExport() { |
| | | if (!this.analysisResult) { |
| | | this.$message.warning('请å
è¿è¡åæ') |
| | | return |
| | | } |
| | | const params = { |
| | | projectId: this.analysisParams.projectId, |
| | | paramName: this.analysisParams.paramName |
| | | } |
| | | exportNormalDistribution(params).then(res => { |
| | | this.downloadFile(res, 'æ£æåå¸åææ°æ®.xlsx') |
| | | }) |
| | | }, |
| | | getCapabilityColor(val) { |
| | | if (!val) return '#909399' |
| | | if (val >= 1.33) return '#67C23A' |
| | | if (val >= 1) return '#E6A23C' |
| | | return '#F56C6C' |
| | | }, |
| | | downloadFile(data, fileName) { |
| | | const blob = new Blob([data]) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = fileName |
| | | link.click() |
| | | window.URL.revokeObjectURL(url) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .capability-item { |
| | | margin-bottom: 15px; |
| | | } |
| | | .capability-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 8px; |
| | | } |
| | | .capability-result { |
| | | text-align: center; |
| | | padding: 15px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | } |
| | | .capability-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- åæé
置表å --> |
| | | <el-card shadow="hover" style="margin-bottom: 20px;"> |
| | | <div slot="header">SPCåæé
ç½®</div> |
| | | <el-form ref="analysisForm" :model="analysisParams" :inline="true" size="small" label-width="100px"> |
| | | <el-form-item label="æ£æµé¡¹ç®" prop="projectId"> |
| | | <el-select v-model="analysisParams.projectId" placeholder="è¯·éæ©æ£æµé¡¹ç®" style="width: 200px" @change="handleProjectChange"> |
| | | <el-option v-for="item in projectList" :key="item.id" :label="item.projectName" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ£æµåæ°" prop="paramName"> |
| | | <el-select v-model="analysisParams.paramName" placeholder="è¯·éæ©æ£æµåæ°" style="width: 200px"> |
| | | <el-option v-for="item in paramList" :key="item.paramName" :label="item.paramName" :value="item.paramName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¶é´èå´" prop="timeRange"> |
| | | <el-date-picker |
| | | v-model="analysisParams.timeRange" |
| | | type="daterange" |
| | | range-separator="-" |
| | | start-placeholder="å¼å§æ¶é´" |
| | | end-placeholder="ç»ææ¶é´" |
| | | value-format="yyyy-MM-dd" |
| | | style="width: 240px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="åç»å¤§å°" prop="subgroupSize"> |
| | | <el-input-number v-model="analysisParams.subgroupSize" :min="2" :max="25" style="width: 150px" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ§å¶ä¸éUCL" prop="ucl"> |
| | | <el-input-number v-model="analysisParams.ucl" :precision="4" style="width: 150px" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ§å¶ä¸éLCL" prop="lcl"> |
| | | <el-input-number v-model="analysisParams.lcl" :precision="4" style="width: 150px" /> |
| | | </el-form-item> |
| | | <el-form-item label="ç®æ å¼CL" prop="targetValue"> |
| | | <el-input-number v-model="analysisParams.targetValue" :precision="4" style="width: 150px" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-data-analysis" @click="handleAnalysis">å¼å§åæ</el-button> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">å¯¼åºæ°æ®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- SPCå¾è¡¨åºå --> |
| | | <el-row :gutter="20" v-if="analysisResult"> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header">X-Baræ§å¶å¾</div> |
| | | <Echart |
| | | :xAxis="xBarXAxis" |
| | | :yAxis="xBarYAxis" |
| | | :series="xBarSeries" |
| | | :tooltip="{ trigger: 'axis' }" |
| | | :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }" |
| | | :chartStyle="{ height: '350px' }" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card shadow="hover"> |
| | | <div slot="header">Ræ§å¶å¾</div> |
| | | <Echart |
| | | :xAxis="rChartXAxis" |
| | | :yAxis="rChartYAxis" |
| | | :series="rChartSeries" |
| | | :tooltip="{ trigger: 'axis' }" |
| | | :grid="{ left: '3%', right: '4%', bottom: '3%', containLabel: true }" |
| | | :chartStyle="{ height: '350px' }" |
| | | /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å¶ç¨è½ååæ --> |
| | | <el-card shadow="hover" style="margin-top: 20px;" v-if="analysisResult"> |
| | | <div slot="header">å¶ç¨è½ååæ</div> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <div class="capability-card"> |
| | | <div class="capability-label">Cp (å¶ç¨ç²¾å¯åº¦)</div> |
| | | <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.cp) }"> |
| | | {{ analysisResult.capability.cp || '--' }} |
| | | </div> |
| | | <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.cp) }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="capability-card"> |
| | | <div class="capability-label">Cpk (å¶ç¨ç²¾ç¡®åº¦)</div> |
| | | <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.cpk) }"> |
| | | {{ analysisResult.capability.cpk || '--' }} |
| | | </div> |
| | | <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.cpk) }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="capability-card"> |
| | | <div class="capability-label">Pp (è¿ç¨ç²¾å¯åº¦)</div> |
| | | <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.pp) }"> |
| | | {{ analysisResult.capability.pp || '--' }} |
| | | </div> |
| | | <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.pp) }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="capability-card"> |
| | | <div class="capability-label">Ppk (è¿ç¨ç²¾ç¡®åº¦)</div> |
| | | <div class="capability-value" :style="{ color: getCapabilityColor(analysisResult.capability.ppk) }"> |
| | | {{ analysisResult.capability.ppk || '--' }} |
| | | </div> |
| | | <div class="capability-status">{{ getCapabilityStatus(analysisResult.capability.ppk) }}</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <!-- åææ°æ®è¡¨æ ¼ --> |
| | | <el-card shadow="hover" style="margin-top: 20px;" v-if="analysisResult"> |
| | | <div slot="header">åææ°æ®æç»</div> |
| | | <el-table :data="analysisDataTable" border style="width: 100%" max-height="400"> |
| | | <el-table-column prop="subgroupNo" label="åç»å·" width="80" /> |
| | | <el-table-column prop="xBar" label="Xåå¼" width="100" /> |
| | | <el-table-column prop="range" label="æå·®R" width="100" /> |
| | | <el-table-column v-for="(item, index) in subgroupColumns" :key="index" :prop="`value${index + 1}`" :label="`æ°æ®${index + 1}`" width="100" /> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.status === 'æ£å¸¸' ? 'success' : 'danger'">{{ scope.row.status }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import Echart from '@/components/echarts/echarts.vue' |
| | | import { spcAnalyze, getProjectList, getParamList, exportSpcData } from '@/api/report/spcChart' |
| | | |
| | | export default { |
| | | name: 'SpcChart', |
| | | components: { Echart }, |
| | | data() { |
| | | return { |
| | | projectList: [], |
| | | paramList: [], |
| | | analysisParams: { |
| | | projectId: null, |
| | | paramName: null, |
| | | timeRange: [], |
| | | subgroupSize: 5, |
| | | ucl: null, |
| | | lcl: null, |
| | | targetValue: null |
| | | }, |
| | | analysisResult: null, |
| | | analysisDataTable: [], |
| | | subgroupColumns: [], |
| | | // X-Barå¾è¡¨ |
| | | xBarXAxis: [{ type: 'category', data: [] }], |
| | | xBarYAxis: [{ type: 'value' }], |
| | | xBarSeries: [ |
| | | { name: 'Xåå¼', type: 'line', data: [], markLine: { data: [] } } |
| | | ], |
| | | // Rå¾è¡¨ |
| | | rChartXAxis: [{ type: 'category', data: [] }], |
| | | rChartYAxis: [{ type: 'value' }], |
| | | rChartSeries: [ |
| | | { name: 'æå·®R', type: 'line', data: [], markLine: { data: [] } } |
| | | ] |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.getProjectList() |
| | | }, |
| | | methods: { |
| | | getProjectList() { |
| | | getProjectList().then(res => { |
| | | this.projectList = res.data || [] |
| | | }) |
| | | }, |
| | | handleProjectChange(projectId) { |
| | | this.analysisParams.paramName = null |
| | | getParamList(projectId).then(res => { |
| | | this.paramList = res.data || [] |
| | | }) |
| | | }, |
| | | handleAnalysis() { |
| | | if (!this.analysisParams.projectId) { |
| | | this.$message.warning('è¯·éæ©æ£æµé¡¹ç®') |
| | | return |
| | | } |
| | | if (!this.analysisParams.paramName) { |
| | | this.$message.warning('è¯·éæ©æ£æµåæ°') |
| | | return |
| | | } |
| | | const params = { |
| | | projectId: this.analysisParams.projectId, |
| | | paramName: this.analysisParams.paramName, |
| | | subgroupSize: this.analysisParams.subgroupSize, |
| | | ucl: this.analysisParams.ucl, |
| | | lcl: this.analysisParams.lcl, |
| | | targetValue: this.analysisParams.targetValue |
| | | } |
| | | if (this.analysisParams.timeRange && this.analysisParams.timeRange.length === 2) { |
| | | params.startDate = this.analysisParams.timeRange[0] |
| | | params.endDate = this.analysisParams.timeRange[1] |
| | | } |
| | | spcAnalyze(params).then(res => { |
| | | this.analysisResult = res.data |
| | | this.renderCharts(res.data) |
| | | this.renderDataTable(res.data) |
| | | this.$message.success('åæå®æ') |
| | | }) |
| | | }, |
| | | renderCharts(data) { |
| | | const xBarData = data.xBar || {} |
| | | const rChartData = data.rChart || {} |
| | | const subgroupLabels = (xBarData.data || []).map((_, i) => `ç»${i + 1}`) |
| | | // X-Barå¾ |
| | | this.xBarXAxis[0].data = subgroupLabels |
| | | this.xBarSeries[0].data = xBarData.data || [] |
| | | this.xBarSeries[0].markLine = { |
| | | data: [ |
| | | { yAxis: xBarData.ucl, name: 'UCL', lineStyle: { color: '#F56C6C' } }, |
| | | { yAxis: xBarData.lcl, name: 'LCL', lineStyle: { color: '#F56C6C' } }, |
| | | { yAxis: xBarData.cl, name: 'CL', lineStyle: { color: '#409EFF' } } |
| | | ] |
| | | } |
| | | // Rå¾ |
| | | this.rChartXAxis[0].data = subgroupLabels |
| | | this.rChartSeries[0].data = rChartData.data || [] |
| | | this.rChartSeries[0].markLine = { |
| | | data: [ |
| | | { yAxis: rChartData.ucl, name: 'UCL', lineStyle: { color: '#F56C6C' } }, |
| | | { yAxis: rChartData.lcl, name: 'LCL', lineStyle: { color: '#F56C6C' } }, |
| | | { yAxis: rChartData.cl, name: 'CL', lineStyle: { color: '#409EFF' } } |
| | | ] |
| | | } |
| | | }, |
| | | renderDataTable(data) { |
| | | const subgroupSize = this.analysisParams.subgroupSize |
| | | this.subgroupColumns = [] |
| | | for (let i = 1; i <= subgroupSize; i++) { |
| | | this.subgroupColumns.push({ prop: `value${i}` }) |
| | | } |
| | | const rawData = data.rawData || [] |
| | | const xBarData = data.xBar?.data || [] |
| | | const rChartData = data.rChart?.data || [] |
| | | const ucl = data.xBar?.ucl |
| | | const lcl = data.xBar?.lcl |
| | | this.analysisDataTable = rawData.map((group, index) => { |
| | | const xBar = xBarData[index] |
| | | const status = (ucl && lcl) ? (xBar >= lcl && xBar <= ucl ? 'æ£å¸¸' : 'å¼å¸¸') : 'æ£å¸¸' |
| | | const row = { |
| | | subgroupNo: index + 1, |
| | | xBar: xBar?.toFixed(4), |
| | | range: rChartData[index]?.toFixed(4), |
| | | status |
| | | } |
| | | group.forEach((val, i) => { |
| | | row[`value${i + 1}`] = val?.toFixed(4) |
| | | }) |
| | | return row |
| | | }) |
| | | }, |
| | | handleExport() { |
| | | if (!this.analysisResult) { |
| | | this.$message.warning('请å
è¿è¡SPCåæ') |
| | | return |
| | | } |
| | | const params = { |
| | | projectId: this.analysisParams.projectId, |
| | | paramName: this.analysisParams.paramName |
| | | } |
| | | exportSpcData(params).then(res => { |
| | | this.downloadFile(res, 'SPCåææ°æ®.xlsx') |
| | | }) |
| | | }, |
| | | getCapabilityColor(val) { |
| | | if (val >= 1.33) return '#67C23A' |
| | | if (val >= 1) return '#E6A23C' |
| | | return '#F56C6C' |
| | | }, |
| | | getCapabilityStatus(val) { |
| | | if (val >= 1.33) return 'å¶ç¨è½åä¼ç§' |
| | | if (val >= 1) return 'å¶ç¨è½ååæ ¼' |
| | | return 'å¶ç¨è½åä¸è¶³' |
| | | }, |
| | | downloadFile(data, fileName) { |
| | | const blob = new Blob([data]) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = fileName |
| | | link.click() |
| | | window.URL.revokeObjectURL(url) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .capability-card { |
| | | text-align: center; |
| | | padding: 20px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | } |
| | | .capability-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | .capability-value { |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | margin-top: 10px; |
| | | } |
| | | .capability-status { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-top: 5px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |