feat:大屏对接接口

liutao_branch
黄伟杰 3 months ago
parent 4292e5cf86
commit fae45959ed

@ -1,47 +1,89 @@
import request from '@/config/axios' import request from '@/config/axios'
export interface DashboardProductVO { export interface DashboardProductVO {
taskItems: ItemVO[] taskItems: ItemVO[]
planItems: ItemVO[] planItems: ItemVO[]
} }
export interface ItemVO { export interface ItemVO {
key: string key: string
label: string label: string
value: number value: number
} }
export interface DeviceStatusVO { export interface DeviceStatusVO {
key: string key: string
label: string label: string
value: number value: number
level: string level: string
} }
export interface TaskStatisticsData {
deviceInspection: number
deviceInspectionProportion: string
moldInspection: number
moldInspectionProportion: string
deviceMaintenance: number
deviceMaintenanceProportion: string
moldMaintenance: number
moldMaintenanceProportion: string
deviceRepair: number
deviceRepairProportion: string
moldRepair: number
moldRepairProportion: string
}
export interface TaskStatisticsResponse {
code: number
status: number
data: TaskStatisticsData
msg: string
}
export interface DashboardTaskItem {
code: string
name: string
type: string
finishStatus: string
resultStatus: number
}
export interface DashboardTaskListResponse {
code: number
status: number
data: DashboardTaskItem[]
msg: string
}
// 编码生成记录 API // 编码生成记录 API
export const DashboardApi = { export const DashboardApi = {
// 查询编码生成记录分页 // 查询编码生成记录分页
getProduction: async (params: any) => { getProduction: async (params: any) => {
return await request.get({ url: `/mes/dashboard/getProduction`, params }) return await request.get({ url: `/mes/dashboard/getProduction`, params })
}, },
getPlan: async () => { getPlan: async () => {
return await request.get({ url: `/mes/dashboard/getPlan` }) return await request.get({ url: `/mes/dashboard/getPlan` })
}, },
getDevice: async () => { getDevice: async () => {
return await request.get({ url: `/mes/dashboard/getDevice` }) return await request.get({ url: `/mes/dashboard/getDevice` })
}, },
getMold: async () => { getMold: async () => {
return await request.get({ url: `/mes/dashboard/getMold` }) return await request.get({ url: `/mes/dashboard/getMold` })
}, },
getTodoList: async () => { getTodoList: async () => {
return await request.get({ url: `/mes/dashboard/getTodoList` }) return await request.get({ url: `/mes/dashboard/getTodoList` })
}, },
getDeviceOperationalStatus: async () => { getDeviceOperationalStatus: async () => {
return await request.get({ url: `/iot/device/getDeviceOperationalStatus` }) return await request.get({ url: `/iot/device/getDeviceOperationalStatus` })
}, },
getDeviceRepairLineOptions: async () => { getDeviceRepairLineOptions: async () => {
return await request.get({ url: `/mes/dashboard/getDeviceRepairLineOptions` }) return await request.get({ url: `/mes/dashboard/getDeviceRepairLineOptions` })
}, },
getTaskStatistics: async () => {
return await request.get<TaskStatisticsResponse>({ url: `/mes/dashboard/getTaskStatistics` })
},
getAllTaskList: async () => {
return await request.get<DashboardTaskListResponse>({ url: `/mes/dashboard/getAllTaskList` })
}
} }

@ -18,14 +18,28 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import { colors } from '../utils' import { colors } from '../utils'
import { DashboardApi } from '@/api/dashboard'
const overviewItems = [ type OverviewItemKey = 'device' | 'running' | 'idle' | 'alarm' | 'utilization' | 'faultRate'
{ key: 'device', label: '设备数量', value: '987,965', percent: 78, color: colors.cyan },
{ key: 'running', label: '运行数量', value: '30', percent: 66, color: colors.blue }, interface OverviewItem {
{ key: 'idle', label: '待机数量', value: '2', percent: 42, color: colors.warn }, key: OverviewItemKey
{ key: 'alarm', label: '报警数量', value: '10', percent: 58, color: colors.danger } label: string
] value: string
percent: number
color: string
}
const overviewItems = ref<OverviewItem[]>([
{ key: 'device', label: '设备数量', value: '0', percent: 0, color: colors.cyan },
{ key: 'running', label: '运行数量', value: '0', percent: 0, color: colors.blue },
{ key: 'idle', label: '待机数量', value: '0', percent: 0, color: colors.warn },
{ key: 'alarm', label: '报警数量', value: '0', percent: 0, color: colors.danger },
{ key: 'utilization', label: '稼动率', value: '0%', percent: 0, color: colors.green },
{ key: 'faultRate', label: '故障率', value: '0%', percent: 0, color: colors.purple }
])
const getGaugeStyle = (percent: number, color: string) => { const getGaugeStyle = (percent: number, color: string) => {
const p = Math.max(0, Math.min(100, percent)) const p = Math.max(0, Math.min(100, percent))
@ -33,6 +47,92 @@ const getGaugeStyle = (percent: number, color: string) => {
background: `conic-gradient(${color} ${p * 3.6}deg, rgba(148,163,184,0.18) 0deg)` background: `conic-gradient(${color} ${p * 3.6}deg, rgba(148,163,184,0.18) 0deg)`
} }
} }
const formatNumber = (value: number | string | undefined | null) => {
const n = Number(value ?? 0)
if (!Number.isFinite(n)) return '0'
return n.toLocaleString('zh-CN')
}
const calcPercent = (part: number, total: number) => {
if (!total || total <= 0 || !Number.isFinite(part)) return 0
const raw = (part / total) * 100
return Math.max(0, Math.min(100, Math.round(raw)))
}
const loadOverview = async () => {
try {
const res: any = await DashboardApi.getDeviceOperationalStatus()
const data = (res && typeof res === 'object' ? (res.data ?? res) : {}) as any
const totalDevices = Number(data.totalDevices ?? 0)
const runningCount = Number(data.runningCount ?? 0)
const standbyCount = Number(data.standbyCount ?? 0)
const faultCount = Number(data.faultCount ?? 0)
const warningCount = Number(data.warningCount ?? 0)
const utilizationRateRaw = String(data.utilizationRate ?? '0')
const faultRateRaw = String(data.faultRate ?? '0')
const utilizationPercent = Number.parseFloat(utilizationRateRaw.replace('%', '')) || 0
const faultPercent = Number.parseFloat(faultRateRaw.replace('%', '')) || 0
const alarmCount = faultCount + warningCount
overviewItems.value = overviewItems.value.map((item) => {
if (item.key === 'device') {
return {
...item,
value: formatNumber(totalDevices),
percent: calcPercent(totalDevices, totalDevices || 1)
}
}
if (item.key === 'running') {
return {
...item,
value: formatNumber(runningCount),
percent: calcPercent(runningCount, totalDevices)
}
}
if (item.key === 'idle') {
return {
...item,
value: formatNumber(standbyCount),
percent: calcPercent(standbyCount, totalDevices)
}
}
if (item.key === 'alarm') {
return {
...item,
value: formatNumber(alarmCount),
percent: calcPercent(alarmCount, totalDevices)
}
}
if (item.key === 'utilization') {
const p = Math.max(0, Math.min(100, Math.round(utilizationPercent)))
return {
...item,
value: utilizationRateRaw.includes('%') ? utilizationRateRaw : `${utilizationPercent.toFixed(2)}%`,
percent: p
}
}
if (item.key === 'faultRate') {
const p = Math.max(0, Math.min(100, Math.round(faultPercent)))
return {
...item,
value: faultRateRaw.includes('%') ? faultRateRaw : `${faultPercent.toFixed(2)}%`,
percent: p
}
}
return item
})
} catch (e) {
}
}
onMounted(() => {
loadOverview()
})
</script> </script>
<style scoped> <style scoped>
@ -105,7 +205,7 @@ const getGaugeStyle = (percent: number, color: string) => {
.overview-body { .overview-body {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px; gap: 12px;
} }

@ -1,9 +1,17 @@
<template> <template>
<div class="card"> <div class="card">
<div class="panel-title"> <div class="panel-title">
<span class="title-dot"></span> <div class="panel-title-left">
<span>事件提醒</span> <span class="title-dot"></span>
</div> <span>事件提醒</span>
</div>
<div class="panel-title-right">
<el-radio-group v-model="mode" size="small">
<el-radio-button label="device">设备</el-radio-button>
<el-radio-button label="mold">模具</el-radio-button>
</el-radio-group>
</div>
</div>
<div class="panel-body body"> <div class="panel-body body">
<div class="event-list"> <div class="event-list">
<div v-for="item in eventItems" :key="item.key" class="event-row"> <div v-for="item in eventItems" :key="item.key" class="event-row">
@ -24,77 +32,142 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref, watch } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { DashboardApi, TaskStatisticsData } from '@/api/dashboard'
import { colors } from '../utils' import { colors } from '../utils'
interface EventItem {
key: string
name: string
count: number
percent: number
color: string
}
const chartRef = ref<HTMLElement | null>(null) const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null let chart: echarts.ECharts | null = null
const mode = ref<'device' | 'mold'>('device')
const eventItems = [ const eventItems = ref<EventItem[]>([])
{ key: 'check', name: '点检', count: 1, percent: 30, color: colors.cyan }, const rawStats = ref<TaskStatisticsData | null>(null)
{ key: 'maintain', name: '保养', count: 1, percent: 42, color: colors.warn }, let switchTimer: number | undefined
{ key: 'repair', name: '维修', count: 1, percent: 20, color: colors.danger }
]
const render = () => { const render = () => {
if (!chart) return if (!chart) return
const percentMap = Object.fromEntries(eventItems.map((i) => [i.name, i.percent])) as Record<string, number> const items = eventItems.value
chart.setOption({ const percentMap = Object.fromEntries(items.map((i) => [i.name, i.percent])) as Record<string, number>
backgroundColor: 'transparent', const seriesData = items.map((i) => ({ value: i.count, name: i.name, itemStyle: { color: i.color } }))
tooltip: { trigger: 'item' }, chart.setOption({
legend: { backgroundColor: 'transparent',
orient: 'vertical', tooltip: { trigger: 'item' },
right: 10, legend: {
top: 'center', orient: 'vertical',
icon: 'circle', right: 10,
itemWidth: 10, top: 'center',
itemHeight: 10, icon: 'circle',
itemGap: 14, itemWidth: 10,
selectedMode: false, itemHeight: 10,
textStyle: { itemGap: 14,
rich: { textStyle: {
percent: { fontSize: 18, fontWeight: 900, color: '#e5f0ff', lineHeight: 20 }, rich: {
name: { fontSize: 12, fontWeight: 700, color: 'rgba(148,163,184,0.95)', lineHeight: 16 } percent: { fontSize: 18, fontWeight: 900, color: '#e5f0ff', lineHeight: 20 },
} name: { fontSize: 12, fontWeight: 700, color: 'rgba(148,163,184,0.95)', lineHeight: 16 }
}, }
formatter: (name: string) => `{percent|${percentMap[name] ?? 0}%}\n{name|${name}}` },
}, formatter: (name: string) => `{percent|${percentMap[name] ?? 0}%}\n{name|${name}}`
series: [ },
{ series: [
type: 'pie', {
radius: ['48%', '70%'], type: 'pie',
center: ['35%', '50%'], radius: ['48%', '70%'],
avoidLabelOverlap: true, center: ['35%', '50%'],
label: { show: false }, avoidLabelOverlap: true,
labelLine: { show: false }, label: { show: false },
padAngle: 2, labelLine: { show: false },
itemStyle: { borderRadius: 8, borderWidth: 6, borderColor: 'rgba(2,6,23,0.9)' }, padAngle: 2,
emphasis: { scale: false }, itemStyle: { borderRadius: 8, borderWidth: 6, borderColor: 'rgba(2,6,23,0.9)' },
data: [ emphasis: { scale: false },
{ value: 30, name: '点检', itemStyle: { color: colors.cyan } }, data: seriesData
{ value: 42, name: '保养', itemStyle: { color: colors.warn } }, }
{ value: 20, name: '维修', itemStyle: { color: colors.danger } } ]
] })
}
]
})
} }
const applyTaskStatistics = (data: TaskStatisticsData) => {
rawStats.value = data
const isDevice = mode.value === 'device'
const inspection = Number(isDevice ? data.deviceInspection : data.moldInspection) || 0
const maintenance = Number(isDevice ? data.deviceMaintenance : data.moldMaintenance) || 0
const repair = Number(isDevice ? data.deviceRepair : data.moldRepair) || 0
const total = inspection + maintenance + repair
const toPercent = (value: number) => (total > 0 ? Math.round((value / total) * 100) : 0)
eventItems.value = [
{
key: 'check',
name: '点检',
count: inspection,
percent: toPercent(inspection),
color: colors.cyan
},
{
key: 'maintain',
name: '保养',
count: maintenance,
percent: toPercent(maintenance),
color: colors.warn
},
{
key: 'repair',
name: '维修',
count: repair,
percent: toPercent(repair),
color: colors.danger
}
]
render()
}
const loadTaskStatistics = async () => {
try {
const res = await DashboardApi.getTaskStatistics()
const payload = (res && typeof res === 'object' && 'data' in res ? (res as any).data : res) as
| TaskStatisticsData
| null
if (!payload) return
applyTaskStatistics(payload)
} catch (error) {
console.error(error)
}
}
watch(mode, () => {
if (!rawStats.value) return
applyTaskStatistics(rawStats.value)
})
const resize = () => { const resize = () => {
chart?.resize() chart?.resize()
} }
onMounted(() => { onMounted(() => {
if (!chartRef.value) return if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' }) chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render() render()
window.addEventListener('resize', resize) loadTaskStatistics()
switchTimer = window.setInterval(() => {
if (!rawStats.value) return
mode.value = mode.value === 'device' ? 'mold' : 'device'
}, 10000)
window.addEventListener('resize', resize)
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', resize) window.removeEventListener('resize', resize)
chart?.dispose() if (switchTimer) {
clearInterval(switchTimer)
switchTimer = undefined
}
chart?.dispose()
}) })
</script> </script>
@ -142,14 +215,51 @@ onUnmounted(() => {
} }
.panel-title { .panel-title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; justify-content: space-between;
padding: 10px 12px; gap: 10px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9); padding: 10px 12px;
font-size: 16px; border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-weight: 900; font-size: 16px;
color: #e5f0ff; font-weight: 900;
color: #e5f0ff;
}
.panel-title-left {
display: inline-flex;
align-items: center;
gap: 10px;
}
.panel-title-right {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 4px;
border-radius: 999px;
border: 1px solid rgba(56, 189, 248, 0.5);
background: radial-gradient(circle at 0 0, rgba(56, 189, 248, 0.18), transparent 70%);
}
.panel-title-right :deep(.el-radio-group) {
background: transparent;
}
.panel-title-right :deep(.el-radio-button__inner) {
border: none;
box-shadow: none;
background: transparent;
color: rgba(148, 163, 184, 0.95);
padding: 4px 10px;
font-size: 12px;
}
.panel-title-right :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
background: rgba(15, 23, 42, 0.9);
color: #e5f0ff;
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.85);
border-radius: 999px;
} }
.title-dot { .title-dot {

@ -8,21 +8,46 @@
<table class="task-table"> <table class="task-table">
<thead> <thead>
<tr> <tr>
<th>时间</th> <th>编号</th>
<th>类型</th> <th>名称</th>
<th>设备</th> <th>类型</th>
<th>责任人</th> <th>完成状态</th>
<th>状态</th> <th>结果</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody ref="tbodyRef">
<tr v-for="row in taskRows" :key="row.id" :class="row.status"> <tr v-for="row in taskRows" :key="row.id">
<td>{{ row.time }}</td> <td>{{ row.code }}</td>
<td>{{ row.type }}</td> <td>{{ row.name }}</td>
<td>{{ row.device }}</td> <td>{{ row.type }}</td>
<td>{{ row.owner }}</td> <td>
<td class="status-cell" :style="{ color: row.color }">{{ row.statusLabel }}</td> <el-tag
</tr> v-if="row.finishStatusLabel"
class="status-tag"
size="small"
:disable-transitions="true"
:hit="false"
:round="true"
:type="row.finishStatusType || 'info'"
>
{{ row.finishStatusLabel }}
</el-tag>
</td>
<td class="status-cell">
<el-tag
v-if="row.resultStatusLabel !== '-'"
class="status-tag"
size="small"
:disable-transitions="true"
:hit="false"
:round="true"
:type="row.resultStatusType || 'info'"
>
{{ row.resultStatusLabel }}
</el-tag>
<span v-else>-</span>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -30,16 +55,106 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { DashboardApi, DashboardTaskItem } from '@/api/dashboard'
import { colors } from '../utils' import { colors } from '../utils'
const taskRows = [ interface TaskRow {
{ id: 1, time: '10:20', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan }, id: string
{ id: 2, time: '10:25', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan }, code: string
{ id: 3, time: '10:30', type: '维修', device: '1号装配机', owner: 'XXX', status: 'danger', statusLabel: '维修', color: colors.danger }, name: string
{ id: 4, time: '10:35', type: '保养', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '保养', color: colors.warn }, type: string
{ id: 5, time: '10:40', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan }, finishStatusLabel: string
{ id: 6, time: '10:45', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan } resultStatusLabel: string
] finishStatusType: '' | 'success' | 'warning' | 'danger' | 'info'
resultStatusType: '' | 'success' | 'warning' | 'danger' | 'info'
}
const taskRows = ref<TaskRow[]>([])
const tbodyRef = ref<HTMLElement | null>(null)
let scrollTimer: number | undefined
const mapItemToRow = (item: DashboardTaskItem, index: number): TaskRow => {
const resultStatus = Number(item.resultStatus) || 0
const finishCode = String(item.finishStatus ?? '').trim()
let finishStatusLabel = item.finishStatus
let finishStatusType: TaskRow['finishStatusType'] = ''
if (finishCode === '0') finishStatusLabel = '待完成'
else if (finishCode === '1') finishStatusLabel = '已完成'
else if (finishCode === '2') finishStatusLabel = '已取消'
if (finishCode === '0') finishStatusType = 'warning'
else if (finishCode === '1') finishStatusType = 'success'
else if (finishCode === '2') finishStatusType = 'info'
let resultStatusLabel = '-'
let resultStatusType: TaskRow['resultStatusType'] = ''
if (resultStatus === 1) {
resultStatusLabel = '通过'
resultStatusType = 'success'
} else if (resultStatus === 2) {
resultStatusLabel = '不通过'
resultStatusType = 'danger'
}
return {
id: `${item.code || 'task'}-${index}`,
code: item.code,
name: item.name,
type: item.type,
finishStatusLabel,
resultStatusLabel,
finishStatusType,
resultStatusType
}
}
const loadTaskList = async () => {
try {
const res = await DashboardApi.getAllTaskList()
const payload = (res && typeof res === 'object' && 'data' in res ? (res as any).data : res) as
| DashboardTaskItem[]
| null
if (!payload || !Array.isArray(payload)) {
taskRows.value = []
return
}
taskRows.value = payload.map(mapItemToRow)
} catch (error) {
console.error(error)
taskRows.value = []
}
}
const startAutoScroll = () => {
if (!tbodyRef.value) return
const rows = tbodyRef.value.querySelectorAll('tr')
if (!rows || rows.length <= 8) return
scrollTimer = window.setInterval(() => {
if (!tbodyRef.value) return
const first = tbodyRef.value.firstElementChild as HTMLElement | null
if (!first) return
first.style.transition = 'all 0.35s'
first.style.transform = 'translateY(-36px)'
first.style.opacity = '0'
window.setTimeout(() => {
if (!tbodyRef.value) return
first.style.transition = 'none'
first.style.transform = 'translateY(0)'
first.style.opacity = '1'
tbodyRef.value.appendChild(first)
}, 360)
}, 5000)
}
onMounted(async () => {
await loadTaskList()
startAutoScroll()
})
onUnmounted(() => {
if (scrollTimer) {
clearInterval(scrollTimer)
scrollTimer = undefined
}
})
</script> </script>
<style scoped> <style scoped>
@ -149,4 +264,11 @@ const taskRows = [
.status-cell { .status-cell {
font-weight: 800; font-weight: 800;
} }
.status-tag {
min-width: 60px;
text-align: center;
border-radius: 999px;
padding: 0 10px;
}
</style> </style>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save