Files
pickling-mes/frontend/src/views/Dashboard.vue
wangyu 193da0018f feat: 移除PDI和订单号字段,新增设备巡检模块
- 从物料跟踪页面移除订单号列和表单字段
- 从导航菜单移除PDI管理,添加设备巡检
- 新增InspectionLocation和InspectionRecord后端模型和API
- 新增设备巡检前端页面(左侧点位列表,右侧设备和历史记录)
2026-05-27 16:38:40 +08:00

247 lines
9.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- 指标卡 -->
<div class="grid-5" style="gap:10px;">
<div class="metric-box" v-for="c in statCards" :key="c.label">
<div class="mb-label">{{ c.label }}</div>
<div class="mb-value" :style="{ color: c.color || 'var(--sms-highlight)' }">{{ c.value }}</div>
<div class="mb-unit">{{ c.unit }}</div>
</div>
</div>
<!-- 当前在线卷 -->
<div class="card">
<div class="card-header">
当前在线钢卷状态
<span class="ch-badge">实时</span>
</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>卷号</th><th>钢种</th><th>厚度(mm)</th><th>宽度(mm)</th>
<th>当前位置</th><th>速度(m/min)</th><th>入线时间</th><th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="r in onlineCoils" :key="r.coil_no">
<td class="td-num">{{ r.coil_no }}</td>
<td>{{ r.steel_grade || '—' }}</td>
<td class="td-num">{{ r.spec_thickness || '—' }}</td>
<td class="td-num">{{ r.spec_width || '—' }}</td>
<td>{{ r.position || '酸洗段' }}</td>
<td class="td-num">{{ r.speed || '85.3' }}</td>
<td class="td-muted">{{ formatTime(r.created_at) }}</td>
<td><span class="badge badge-green">在线</span></td>
</tr>
<tr v-if="!onlineCoils.length">
<td colspan="8" class="td-muted" style="text-align:center;padding:20px;">暂无在线钢卷</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 趋势 + 停机统计 -->
<div class="section-row">
<div class="card" style="flex:2;">
<div class="card-header">今日产量趋势/小时</div>
<div class="card-body">
<canvas ref="trendChart" style="width:100%;height:140px;display:block;"></canvas>
</div>
</div>
<div class="card" style="flex:1;">
<div class="card-header">今日停机统计</div>
<div class="card-body" style="padding:0;">
<table class="data-table">
<thead><tr><th>停机类别</th><th>次数</th><th>时长(min)</th></tr></thead>
<tbody>
<tr v-for="d in downtimeStat" :key="d.name">
<td>{{ d.name }}</td>
<td class="td-num">{{ d.count }}</td>
<td class="td-warn">{{ d.duration }}</td>
</tr>
<tr v-if="!downtimeStat.length">
<td colspan="3" class="td-muted" style="text-align:center;padding:16px;">今日无停机记录</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 班次实绩 -->
<div class="card">
<div class="card-header">今日班次实绩汇总</div>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr><th>班次</th><th>计划()</th><th>实际()</th><th>完成率</th><th>产量(t)</th><th>平均速度</th><th>停机时长</th></tr>
</thead>
<tbody>
<tr v-for="s in shiftSummary" :key="s.shift">
<td>{{ s.shift }}</td>
<td class="td-num">{{ s.plan }}</td>
<td class="td-num">{{ s.actual }}</td>
<td>
<div class="prog-bar-wrap" style="width:80px;display:inline-block;vertical-align:middle;margin-right:6px;">
<div class="prog-bar-fill" :style="{ width: s.rate + '%', background: s.rate >= 90 ? 'var(--accent-green)' : s.rate >= 70 ? 'var(--accent-yellow)' : 'var(--accent-red)' }"></div>
</div>
<span :class="s.rate >= 90 ? 'td-ok' : s.rate >= 70 ? 'td-warn' : 'td-err'">{{ s.rate }}%</span>
</td>
<td class="td-num">{{ s.weight }}</td>
<td class="td-num">{{ s.speed }} m/min</td>
<td :class="s.downtime > 30 ? 'td-warn' : 'td-ok'">{{ s.downtime }} min</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
import { getDashboardSummary, getCoils, getDowntimeRecords, getProductionRecords } from '@/api'
export default {
name: 'Dashboard',
data() {
return {
summary: null,
onlineCoils: [],
downtimeStat: [],
shiftSummary: [],
chart: null,
}
},
computed: {
statCards() {
const s = this.summary
return [
{ label: '今日产量', value: s?.today_production?.coil_count ?? 0, unit: '卷', color: 'var(--sms-highlight)' },
{ label: '今日产量', value: s ? (s.today_production.weight_kg / 1000).toFixed(1) : '0', unit: '吨' },
{ label: '在线钢卷', value: s?.online_coils ?? 0, unit: '卷', color: 'var(--accent-green)' },
{ label: '今日停机', value: s?.today_downtime_min?.toFixed(0) ?? '0', unit: 'min', color: s?.today_downtime_min > 60 ? 'var(--accent-red)' : 'var(--accent-yellow)' },
{ label: '机组速度', value: '85.3', unit: 'm/min' },
]
}
},
async mounted() {
await this.fetchData()
},
methods: {
async fetchData() {
try {
const [summaryRes, coilRes, downtimeRes, prodRes] = await Promise.all([
getDashboardSummary(),
getCoils({ status: 'on_line', page_size: 10 }),
getDowntimeRecords({ page_size: 50 }),
getProductionRecords({ page_size: 100 }),
])
this.summary = summaryRes.data
this.onlineCoils = coilRes.data?.items || []
// 停机统计(按类别汇总)
const dtItems = downtimeRes.data?.items || []
const dtMap = {}
dtItems.forEach(d => {
const k = d.category_name || d.category_code || '其他'
if (!dtMap[k]) dtMap[k] = { name: k, count: 0, duration: 0 }
dtMap[k].count++
dtMap[k].duration += d.duration_min || 0
})
this.downtimeStat = Object.values(dtMap).sort((a, b) => b.duration - a.duration).slice(0, 6)
// 班次汇总(按班次聚合产量)
const prods = prodRes.data?.items || []
const shiftMap = {}
prods.forEach(p => {
const k = p.shift || '—'
if (!shiftMap[k]) shiftMap[k] = { shift: k, count: 0, weight: 0, speeds: [] }
shiftMap[k].count++
shiftMap[k].weight += (p.coil_weight_t || 0)
if (p.avg_speed) shiftMap[k].speeds.push(p.avg_speed)
})
this.shiftSummary = Object.values(shiftMap).map(s => ({
shift: s.shift,
actual: s.count,
weight: s.weight.toFixed(1),
speed: s.speeds.length ? (s.speeds.reduce((a,b)=>a+b,0)/s.speeds.length).toFixed(1) : '—',
}))
// 图表数据:按小时统计产量卷数
const hourMap = Array(12).fill(0)
prods.forEach(p => {
if (p.start_time) {
const h = Math.floor(new Date(p.start_time).getHours() / 2)
if (h >= 0 && h < 12) hourMap[h]++
}
})
this.$nextTick(() => this.drawChart(hourMap))
} catch (e) {
this.$nextTick(() => this.drawChart(Array(12).fill(0)))
}
},
formatTime(t) {
if (!t) return '—'
return t.replace('T', ' ').slice(0, 16)
},
drawChart(data) {
const canvas = this.$refs.trendChart
if (!canvas) return
const ctx = canvas.getContext('2d')
canvas.width = canvas.offsetWidth * devicePixelRatio
canvas.height = canvas.offsetHeight * devicePixelRatio
ctx.scale(devicePixelRatio, devicePixelRatio)
const W = canvas.offsetWidth, H = canvas.offsetHeight
data = data || Array(12).fill(0)
const labels = Array.from({length:12}, (_,i) => `${i*2}:00`)
const max = Math.max(...data) + 2
ctx.clearRect(0,0,W,H)
// 网格线
ctx.strokeStyle = '#30363d'
ctx.lineWidth = .5
for (let i = 0; i <= 4; i++) {
const y = H - (i / 4) * (H - 20) - 10
ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(W - 10, y); ctx.stroke()
}
// 折线
const stepX = (W - 50) / (data.length - 1)
const toY = v => H - (v / max) * (H - 30) - 10
const pts = data.map((v, i) => [40 + i * stepX, toY(v)])
// 填充
ctx.beginPath()
ctx.moveTo(pts[0][0], H - 10)
pts.forEach(([x, y]) => ctx.lineTo(x, y))
ctx.lineTo(pts[pts.length-1][0], H - 10)
ctx.closePath()
const grad = ctx.createLinearGradient(0, 0, 0, H)
grad.addColorStop(0, 'rgba(0,200,255,.25)')
grad.addColorStop(1, 'rgba(0,200,255,0)')
ctx.fillStyle = grad
ctx.fill()
// 线条
ctx.beginPath()
ctx.strokeStyle = '#00c8ff'
ctx.lineWidth = 1.5
pts.forEach(([x,y], i) => i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y))
ctx.stroke()
// 数据点
ctx.fillStyle = '#00c8ff'
pts.forEach(([x,y]) => { ctx.beginPath(); ctx.arc(x,y,3,0,Math.PI*2); ctx.fill() })
// X轴标签
ctx.fillStyle = '#6e7681'
ctx.font = '10px Consolas'
ctx.textAlign = 'center'
labels.forEach((l, i) => ctx.fillText(l, 40 + i * stepX, H))
}
}
}
</script>