| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="quality-dashboard"> |
| | | <el-row :gutter="16"> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-card shadow="hover" class="panel"> |
| | | <template #header> |
| | | <div class="panel-title"> |
| | | æ£æµæ ·åå¨æç¶æ |
| | | <div class="actions"> |
| | | <el-switch v-model="voiceEnabled" active-text="è¯é³é¢è¦" inactive-text="éé³" /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="status-list"> |
| | | <div v-for="item in sampleStatus" :key="item.id" class="status-item" :class="item.status"> |
| | | <div class="left"> |
| | | <span class="dot" :class="item.status"></span> |
| | | <span class="name">{{ item.name }}</span> |
| | | </div> |
| | | <div class="right"> |
| | | <el-tag :type="statusTagType(item.status)" size="small">{{ statusLabel(item.status) }}</el-tag> |
| | | <span class="time">{{ item.time }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="12"> |
| | | <el-card shadow="hover" class="panel"> |
| | | <template #header> |
| | | <div class="panel-title">任塿è¡ï¼Top 10ï¼</div> |
| | | </template> |
| | | <EChart :xAxis="tasksXAxis" :yAxis="[{ type: 'value' }]" :series="tasksSeries" :grid="{ left: 40, right: 20, top: 20, bottom: 40 }" :tooltip="{ trigger: 'axis' }" :barColors="['#3b82f6']" :chartStyle="{ height: '320px', width: '100%' }" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="16" style="margin-top: 16px;"> |
| | | <el-col :xs="24" :sm="14"> |
| | | <el-card shadow="hover" class="panel"> |
| | | <template #header> |
| | | <div class="panel-title">åå²è¶å¿</div> |
| | | </template> |
| | | <EChart :xAxis="[{ type: 'category', data: trendXAxis }]" :yAxis="[{ type: 'value', name: 'æ°é' }]" :series="trendSeries" :tooltip="{ trigger: 'axis' }" :legend="{ top: 0 }" :lineColors="['#10b981', '#f59e0b']" :chartStyle="{ height: '340px', width: '100%' }" /> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :xs="24" :sm="10"> |
| | | <el-card shadow="hover" class="panel"> |
| | | <template #header> |
| | | <div class="panel-title">åæ ¼çåæ</div> |
| | | </template> |
| | | <EChart :series="passRateSeries" :legend="{ show: false }" :chartStyle="{ height: '340px', width: '100%' }" /> |
| | | <div class="passrate-text"> |
| | | å½ååæ ¼çï¼<b>{{ (passRate * 100).toFixed(1) }}%</b> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="16" style="margin-top: 16px;"> |
| | | <el-col :xs="24"> |
| | | <el-card shadow="hover" class="panel"> |
| | | <template #header> |
| | | <div class="panel-title">SPC æ§å¶å¾ï¼Xbarï¼</div> |
| | | </template> |
| | | <EChart :xAxis="[{ type: 'category', data: spcXAxis }]" :yAxis="[{ type: 'value', name: 'æµéå¼' }]" :series="spcSeries" :legend="{ top: 0 }" :tooltip="{ trigger: 'axis' }" :lineColors="['#2563eb', '#ef4444', '#f97316', '#22c55e']" :chartStyle="{ height: '380px', width: '100%' }" /> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, onBeforeUnmount, reactive, ref } from 'vue' |
| | | import EChart from '@/components/Echarts/echarts.vue' |
| | | |
| | | const voiceEnabled = ref(true) |
| | | let dataTimer = null |
| | | |
| | | // 1) æ ·åå¨æç¶æï¼æ»å¨æ´æ°ï¼ |
| | | const sampleStatus = ref([]) |
| | | const statusPool = ['processing', 'warning', 'error', 'success'] |
| | | function statusLabel(s) { |
| | | return s === 'processing' ? 'æ£æµä¸' : s === 'warning' ? 'é¢è¦' : s === 'error' ? 'ä¸åæ ¼' : 'åæ ¼' |
| | | } |
| | | function statusTagType(s) { |
| | | return s === 'processing' ? 'info' : s === 'warning' ? 'warning' : s === 'error' ? 'danger' : 'success' |
| | | } |
| | | function randomSample() { |
| | | const id = Math.random().toString(36).slice(2, 8) |
| | | const status = statusPool[Math.floor(Math.random() * statusPool.length)] |
| | | const name = `æ ·å-${Math.floor(Math.random() * 900 + 100)}` |
| | | const time = new Date().toLocaleTimeString('zh-CN', { hour12: false }) |
| | | return { id, name, status, time } |
| | | } |
| | | |
| | | // 2) 任塿è¡ï¼æ±ç¶å¾ï¼ |
| | | const tasksXAxis = reactive([{ type: 'category', data: [] }]) |
| | | const tasksSeries = ref([ |
| | | { |
| | | type: 'bar', |
| | | data: [], |
| | | label: { show: true, position: 'inside', align: 'center', verticalAlign: 'middle', color: '#fff' }, |
| | | encode: undefined, |
| | | }, |
| | | ]) |
| | | |
| | | // 3) åå²è¶å¿ï¼æçº¿ï¼ |
| | | const trendXAxis = ref([]) |
| | | const trendSeries = ref([ |
| | | { name: 'æ¥æ ·æ°', type: 'line', smooth: true, data: [] }, |
| | | { name: '宿æ°', type: 'line', smooth: true, data: [] }, |
| | | ]) |
| | | |
| | | // 4) åæ ¼çåæï¼ä»ªè¡¨çï¼ |
| | | const passRate = ref(0.92) |
| | | const passRateSeries = ref([ |
| | | { |
| | | type: 'gauge', |
| | | progress: { show: true, width: 12 }, |
| | | axisLine: { lineStyle: { width: 12 } }, |
| | | pointer: { show: true }, |
| | | detail: { valueAnimation: true, formatter: (v) => `${(v * 100).toFixed(1)}%` }, |
| | | data: [{ value: passRate.value }], |
| | | }, |
| | | ]) |
| | | |
| | | // 5) SPC æ§å¶å¾ |
| | | const spcXAxis = ref([]) |
| | | const spcData = ref([]) // å®é
æµéå¼ |
| | | const CL = ref(50) |
| | | const UCL = ref(55) |
| | | const LCL = ref(45) |
| | | const spcSeries = ref([ |
| | | { |
| | | name: 'æµéåå¼', |
| | | type: 'line', |
| | | smooth: false, |
| | | symbol: 'circle', |
| | | data: [], |
| | | markLine: { |
| | | symbol: 'none', |
| | | lineStyle: { type: 'dashed', color: '#999' }, |
| | | data: [ |
| | | { yAxis: () => UCL.value, name: 'UCL' }, |
| | | { yAxis: () => CL.value, name: 'CL' }, |
| | | { yAxis: () => LCL.value, name: 'LCL' }, |
| | | ], |
| | | label: { formatter: ({ name }) => name }, |
| | | }, |
| | | }, |
| | | { name: 'UCL', type: 'line', data: [], symbol: 'none', lineStyle: { type: 'dashed', color: '#ef4444' } }, |
| | | { name: 'CL', type: 'line', data: [], symbol: 'none', lineStyle: { type: 'dashed', color: '#f97316' } }, |
| | | { name: 'LCL', type: 'line', data: [], symbol: 'none', lineStyle: { type: 'dashed', color: '#22c55e' } }, |
| | | ]) |
| | | |
| | | // è¯é³ææ¥ |
| | | function speak(text) { |
| | | if (!voiceEnabled.value) return |
| | | if (!('speechSynthesis' in window)) return |
| | | const utter = new SpeechSynthesisUtterance(text) |
| | | utter.lang = 'zh-CN' |
| | | try { |
| | | window.speechSynthesis.cancel() |
| | | window.speechSynthesis.speak(utter) |
| | | } catch (e) { |
| | | // ignore |
| | | } |
| | | } |
| | | |
| | | function refreshFakeData() { |
| | | // æ ·åç¶ææ»å¨ |
| | | const next = randomSample() |
| | | sampleStatus.value = [next, ...sampleStatus.value].slice(0, 8) |
| | | |
| | | // ä»»å¡æè¡ |
| | | const tasks = Array.from({ length: 10 }).map((_, i) => ({ name: `ä»»å¡-${i + 1}`, count: Math.floor(Math.random() * 100 + 20) })) |
| | | tasks.sort((a, b) => a.count - b.count) |
| | | tasksXAxis.data = tasks.map(t => t.name) |
| | | tasksSeries.value[0].data = tasks.map(t => t.count) |
| | | |
| | | // åå²è¶å¿ï¼è¿½å ç¹ï¼ |
| | | const nowLabel = new Date().toLocaleTimeString('zh-CN', { minute: '2-digit', second: '2-digit' }) |
| | | if (trendXAxis.value.length > 15) { |
| | | trendXAxis.value.shift() |
| | | trendSeries.value[0].data.shift() |
| | | trendSeries.value[1].data.shift() |
| | | } |
| | | trendXAxis.value.push(nowLabel) |
| | | const incoming = Math.floor(Math.random() * 30 + 20) |
| | | const finished = Math.max(0, incoming - Math.floor(Math.random() * 10)) |
| | | trendSeries.value[0].data.push(incoming) |
| | | trendSeries.value[1].data.push(finished) |
| | | |
| | | // åæ ¼çï¼è½»å¾®æ³¢å¨ï¼ |
| | | const delta = (Math.random() - 0.5) * 0.02 |
| | | passRate.value = Math.min(0.99, Math.max(0.6, passRate.value + delta)) |
| | | passRateSeries.value[0].data[0].value = passRate.value |
| | | |
| | | // SPC æ°æ®ï¼çªå£ç§»å¨ï¼ |
| | | const nextVal = CL.value + (Math.random() - 0.5) * 8 // æ³¢å¨ |
| | | if (spcXAxis.value.length > 30) { |
| | | spcXAxis.value.shift() |
| | | spcData.value.shift() |
| | | } |
| | | spcXAxis.value.push(`${spcXAxis.value.length + 1}`) |
| | | spcData.value.push(parseFloat(nextVal.toFixed(2))) |
| | | spcSeries.value[0].data = [...spcData.value] |
| | | spcSeries.value[1].data = new Array(spcData.value.length).fill(UCL.value) |
| | | spcSeries.value[2].data = new Array(spcData.value.length).fill(CL.value) |
| | | spcSeries.value[3].data = new Array(spcData.value.length).fill(LCL.value) |
| | | |
| | | // è§¦åææ¥ï¼åæ ¼çè¿ä½æ SPC è¶
é |
| | | if (passRate.value < 0.8) { |
| | | speak(`é¢è¦ï¼å½ååæ ¼ç为 ${(passRate.value * 100).toFixed(0)}%ï¼ä½äº 80% éå¼`) |
| | | } |
| | | const last = spcData.value[spcData.value.length - 1] |
| | | if (last > UCL.value) { |
| | | speak(`é¢è¦ï¼ææ°æµéå¼ ${last.toFixed(2)} è¶
è¿ä¸é`) |
| | | } |
| | | if (last < LCL.value) { |
| | | speak(`é¢è¦ï¼ææ°æµéå¼ ${last.toFixed(2)} ä½äºä¸é`) |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | // åå§åå æ¡åæ°æ® |
| | | sampleStatus.value = Array.from({ length: 5 }).map(() => randomSample()) |
| | | for (let i = 0; i < 10; i++) { |
| | | trendXAxis.value.push(`T-${i}`) |
| | | trendSeries.value[0].data.push(Math.floor(Math.random() * 30 + 20)) |
| | | trendSeries.value[1].data.push(Math.floor(Math.random() * 25 + 15)) |
| | | } |
| | | for (let i = 0; i < 20; i++) { |
| | | spcXAxis.value.push(`${i + 1}`) |
| | | const v = CL.value + (Math.random() - 0.5) * 6 |
| | | spcData.value.push(parseFloat(v.toFixed(2))) |
| | | } |
| | | spcSeries.value[0].data = [...spcData.value] |
| | | spcSeries.value[1].data = new Array(spcData.value.length).fill(UCL.value) |
| | | spcSeries.value[2].data = new Array(spcData.value.length).fill(CL.value) |
| | | spcSeries.value[3].data = new Array(spcData.value.length).fill(LCL.value) |
| | | |
| | | dataTimer = setInterval(refreshFakeData, 10000) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (dataTimer) clearInterval(dataTimer) |
| | | try { window.speechSynthesis && window.speechSynthesis.cancel() } catch (e) {} |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .quality-dashboard { |
| | | padding: 8px; |
| | | } |
| | | .panel-title { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | font-weight: 600; |
| | | } |
| | | .status-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | max-height: 320px; |
| | | overflow: auto; |
| | | } |
| | | .status-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 8px 10px; |
| | | border-radius: 6px; |
| | | background: var(--el-fill-color-light); |
| | | } |
| | | .status-item .left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .status-item .right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | .status-item .name { font-weight: 500; } |
| | | .status-item .time { color: var(--el-text-color-secondary); font-size: 12px; } |
| | | .dot { |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | display: inline-block; |
| | | } |
| | | .dot.processing { background: #60a5fa; } |
| | | .dot.warning { background: #f59e0b; } |
| | | .dot.error { background: #ef4444; } |
| | | .dot.success { background: #10b981; } |
| | | .passrate-text { |
| | | text-align: center; |
| | | margin-top: 8px; |
| | | } |
| | | </style> |
| | | |
| | | |