feat:大屏对接接口

main
黄伟杰 1 week ago
parent 4292e5cf86
commit fae45959ed

@ -1,47 +1,89 @@
import request from '@/config/axios'
export interface DashboardProductVO {
taskItems: ItemVO[]
planItems: ItemVO[]
taskItems: ItemVO[]
planItems: ItemVO[]
}
export interface ItemVO {
key: string
label: string
value: number
key: string
label: string
value: number
}
export interface DeviceStatusVO {
key: string
label: string
value: number
level: string
key: string
label: string
value: number
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
export const DashboardApi = {
// 查询编码生成记录分页
getProduction: async (params: any) => {
return await request.get({ url: `/mes/dashboard/getProduction`, params })
},
getPlan: async () => {
return await request.get({ url: `/mes/dashboard/getPlan` })
},
getDevice: async () => {
return await request.get({ url: `/mes/dashboard/getDevice` })
},
getMold: async () => {
return await request.get({ url: `/mes/dashboard/getMold` })
},
getTodoList: async () => {
return await request.get({ url: `/mes/dashboard/getTodoList` })
},
getDeviceOperationalStatus: async () => {
return await request.get({ url: `/iot/device/getDeviceOperationalStatus` })
},
getDeviceRepairLineOptions: async () => {
return await request.get({ url: `/mes/dashboard/getDeviceRepairLineOptions` })
},
// 查询编码生成记录分页
getProduction: async (params: any) => {
return await request.get({ url: `/mes/dashboard/getProduction`, params })
},
getPlan: async () => {
return await request.get({ url: `/mes/dashboard/getPlan` })
},
getDevice: async () => {
return await request.get({ url: `/mes/dashboard/getDevice` })
},
getMold: async () => {
return await request.get({ url: `/mes/dashboard/getMold` })
},
getTodoList: async () => {
return await request.get({ url: `/mes/dashboard/getTodoList` })
},
getDeviceOperationalStatus: async () => {
return await request.get({ url: `/iot/device/getDeviceOperationalStatus` })
},
getDeviceRepairLineOptions: async () => {
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>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { colors } from '../utils'
import { DashboardApi } from '@/api/dashboard'
const overviewItems = [
{ key: 'device', label: '设备数量', value: '987,965', percent: 78, color: colors.cyan },
{ key: 'running', label: '运行数量', value: '30', percent: 66, color: colors.blue },
{ key: 'idle', label: '待机数量', value: '2', percent: 42, color: colors.warn },
{ key: 'alarm', label: '报警数量', value: '10', percent: 58, color: colors.danger }
]
type OverviewItemKey = 'device' | 'running' | 'idle' | 'alarm' | 'utilization' | 'faultRate'
interface OverviewItem {
key: OverviewItemKey
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 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)`
}
}
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>
<style scoped>
@ -105,7 +205,7 @@ const getGaugeStyle = (percent: number, color: string) => {
.overview-body {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}

@ -1,9 +1,17 @@
<template>
<div class="card">
<div class="panel-title">
<span class="title-dot"></span>
<span>事件提醒</span>
</div>
<div class="panel-title">
<div class="panel-title-left">
<span class="title-dot"></span>
<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="event-list">
<div v-for="item in eventItems" :key="item.key" class="event-row">
@ -24,77 +32,142 @@
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import * as echarts from 'echarts'
import { DashboardApi, TaskStatisticsData } from '@/api/dashboard'
import { colors } from '../utils'
interface EventItem {
key: string
name: string
count: number
percent: number
color: string
}
const chartRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
const eventItems = [
{ key: 'check', name: '点检', count: 1, percent: 30, color: colors.cyan },
{ key: 'maintain', name: '保养', count: 1, percent: 42, color: colors.warn },
{ key: 'repair', name: '维修', count: 1, percent: 20, color: colors.danger }
]
const mode = ref<'device' | 'mold'>('device')
const eventItems = ref<EventItem[]>([])
const rawStats = ref<TaskStatisticsData | null>(null)
let switchTimer: number | undefined
const render = () => {
if (!chart) return
const percentMap = Object.fromEntries(eventItems.map((i) => [i.name, i.percent])) as Record<string, number>
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item' },
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
itemGap: 14,
selectedMode: false,
textStyle: {
rich: {
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}}`
},
series: [
{
type: 'pie',
radius: ['48%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
label: { show: false },
labelLine: { show: false },
padAngle: 2,
itemStyle: { borderRadius: 8, borderWidth: 6, borderColor: 'rgba(2,6,23,0.9)' },
emphasis: { scale: false },
data: [
{ value: 30, name: '点检', itemStyle: { color: colors.cyan } },
{ value: 42, name: '保养', itemStyle: { color: colors.warn } },
{ value: 20, name: '维修', itemStyle: { color: colors.danger } }
]
}
]
})
if (!chart) return
const items = eventItems.value
const percentMap = Object.fromEntries(items.map((i) => [i.name, i.percent])) as Record<string, number>
const seriesData = items.map((i) => ({ value: i.count, name: i.name, itemStyle: { color: i.color } }))
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'item' },
legend: {
orient: 'vertical',
right: 10,
top: 'center',
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
itemGap: 14,
textStyle: {
rich: {
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}}`
},
series: [
{
type: 'pie',
radius: ['48%', '70%'],
center: ['35%', '50%'],
avoidLabelOverlap: true,
label: { show: false },
labelLine: { show: false },
padAngle: 2,
itemStyle: { borderRadius: 8, borderWidth: 6, borderColor: 'rgba(2,6,23,0.9)' },
emphasis: { scale: false },
data: seriesData
}
]
})
}
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 = () => {
chart?.resize()
chart?.resize()
}
onMounted(() => {
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
window.addEventListener('resize', resize)
if (!chartRef.value) return
chart = echarts.init(chartRef.value, 'dark', { renderer: 'canvas' })
render()
loadTaskStatistics()
switchTimer = window.setInterval(() => {
if (!rawStats.value) return
mode.value = mode.value === 'device' ? 'mold' : 'device'
}, 10000)
window.addEventListener('resize', resize)
})
onUnmounted(() => {
window.removeEventListener('resize', resize)
chart?.dispose()
window.removeEventListener('resize', resize)
if (switchTimer) {
clearInterval(switchTimer)
switchTimer = undefined
}
chart?.dispose()
})
</script>
@ -142,14 +215,51 @@ onUnmounted(() => {
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
font-weight: 900;
color: #e5f0ff;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid rgba(41, 54, 95, 0.9);
font-size: 16px;
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 {

@ -8,21 +8,46 @@
<table class="task-table">
<thead>
<tr>
<th>时间</th>
<th>类型</th>
<th>设备</th>
<th>责任人</th>
<th>状态</th>
<th>编号</th>
<th>名称</th>
<th>类型</th>
<th>完成状态</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr v-for="row in taskRows" :key="row.id" :class="row.status">
<td>{{ row.time }}</td>
<td>{{ row.type }}</td>
<td>{{ row.device }}</td>
<td>{{ row.owner }}</td>
<td class="status-cell" :style="{ color: row.color }">{{ row.statusLabel }}</td>
</tr>
<tbody ref="tbodyRef">
<tr v-for="row in taskRows" :key="row.id">
<td>{{ row.code }}</td>
<td>{{ row.name }}</td>
<td>{{ row.type }}</td>
<td>
<el-tag
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>
</table>
</div>
@ -30,16 +55,106 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { DashboardApi, DashboardTaskItem } from '@/api/dashboard'
import { colors } from '../utils'
const taskRows = [
{ id: 1, time: '10:20', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan },
{ id: 2, time: '10:25', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan },
{ id: 3, time: '10:30', type: '维修', device: '1号装配机', owner: 'XXX', status: 'danger', statusLabel: '维修', color: colors.danger },
{ id: 4, time: '10:35', type: '保养', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '保养', color: colors.warn },
{ id: 5, time: '10:40', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan },
{ id: 6, time: '10:45', type: '点检', device: '1号装配机', owner: 'XXX', status: 'ok', statusLabel: '正常', color: colors.cyan }
]
interface TaskRow {
id: string
code: string
name: string
type: string
finishStatusLabel: string
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>
<style scoped>
@ -149,4 +264,11 @@ const taskRows = [
.status-cell {
font-weight: 800;
}
.status-tag {
min-width: 60px;
text-align: center;
border-radius: 999px;
padding: 0 10px;
}
</style>

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