Files
bj_power/bj_power_wms_client/frontend/src/pages/Display.vue
T
SunYF a2afd2f6dc feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE
- WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置
- WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页
- 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面
- Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理
- 清理各项目球形磨遗留代码,新增部署手册.md
2026-08-27 19:50:57 +08:00

164 lines
5.7 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.
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import request from '../utils/request'
import { fmtNum } from '../utils/format'
const now = ref(new Date())
let clockTimer = null
let pollTimer = null
const overview = ref({})
const maxZoneQty = computed(() => {
const vals = Object.values(zoneList.value).map(v => Number(v.qty) || 0)
return Math.max(...vals, 1)
})
const zoneList = computed(() => {
const detail = overview.value.zoneDetail || {}
return Object.entries(detail)
.map(([zone, qty]) => ({ zone, qty: Number(qty) || 0 }))
.sort((a, b) => b.qty - a.qty)
})
const kpis = computed(() => [
{ key: 'totalQty', label: '在库总量', value: fmtNum(overview.value.totalQty) },
{ key: 'lockedQty', label: '锁定量', value: fmtNum(overview.value.lockedQty) },
{ key: 'snInStock', label: 'SN 在库', value: fmtNum(overview.value.snInStock) },
{ key: 'semiInStock', label: '半成品在库', value: fmtNum(overview.value.semiInStock) },
{ key: 'packageCount', label: '包装箱数', value: fmtNum(overview.value.packageCount) },
{ key: 'inToday', label: '今日入库单', value: fmtNum(overview.value.inToday) },
{ key: 'outToday', label: '今日出库单', value: fmtNum(overview.value.outToday) }
])
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const clock = computed(() => {
const d = now.value
const p = (n) => String(n).padStart(2, '0')
return {
date: `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`,
time: `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`,
week: `星期${weekdays[d.getDay()].replace('周', '')}`
}
})
async function load() {
try {
overview.value = await request.get('/display/overview')
} catch { /* 大屏轮询失败静默重试 */ }
}
onMounted(() => {
clockTimer = setInterval(() => { now.value = new Date() }, 1000)
load()
pollTimer = setInterval(load, 5000)
})
onBeforeUnmount(() => {
clearInterval(clockTimer)
clearInterval(pollTimer)
})
</script>
<template>
<div class="screen">
<header class="head">
<div class="side left">北自所 · 库房监控</div>
<h1 class="title">库存动态大屏</h1>
<div class="side right">
<span class="date">{{ clock.date }}</span>
<span class="time">{{ clock.time }}</span>
<span>{{ clock.week }}</span>
</div>
</header>
<section class="kpi-grid">
<div v-for="k in kpis" :key="k.key" class="kpi">
<div class="kpi-value">{{ k.value }}</div>
<div class="kpi-label">{{ k.label }}</div>
</div>
</section>
<section class="zones">
<div class="zone-title">区域明细区域 / 质量状态</div>
<div class="zone-list">
<div v-for="z in zoneList" :key="z.zone" class="zone-row">
<div class="zone-name">{{ z.zone }}</div>
<div class="bar-wrap">
<div class="bar" :style="{ width: `${(z.qty / maxZoneQty) * 100}%` }"></div>
</div>
<div class="zone-qty">{{ z.qty.toLocaleString('zh-CN') }}</div>
</div>
<div v-if="!zoneList.length" class="empty">暂无区域数据</div>
</div>
</section>
<footer class="foot">数据来源WMS 后端 · 5 秒自动刷新</footer>
</div>
</template>
<style scoped>
.screen {
min-height: 100vh;
background:
radial-gradient(ellipse at 50% 0%, rgba(0, 170, 255, 0.18), transparent 55%),
linear-gradient(180deg, #04102a 0%, #071e42 60%, #04102a 100%);
color: #d7ecff;
display: flex;
flex-direction: column;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
.head {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 36px 10px;
border-bottom: 1px solid rgba(64, 158, 255, 0.35);
}
.title {
margin: 0; font-size: clamp(20px, 2.6vw, 34px); letter-spacing: 8px;
color: #fff; text-shadow: 0 0 14px rgba(64, 190, 255, 0.9);
}
.side { font-size: 13px; color: #7fb8ff; }
.right { text-align: right; }
.date { margin-right: 12px; }
.time {
font-size: 22px; font-weight: 700; color: #4fd7ff; margin-right: 8px;
font-variant-numeric: tabular-nums;
}
.kpi-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 14px; padding: 22px 36px 6px;
}
.kpi {
border: 1px solid rgba(64, 158, 255, 0.45);
background: linear-gradient(180deg, rgba(13, 47, 96, 0.75), rgba(7, 26, 58, 0.85));
border-radius: 8px; padding: 18px 10px; text-align: center;
box-shadow: inset 0 0 24px rgba(35, 130, 255, 0.25);
}
.kpi-value {
font-size: clamp(28px, 3vw, 46px); font-weight: 800; color: #61e6ff;
text-shadow: 0 0 16px rgba(80, 220, 255, 0.7);
font-variant-numeric: tabular-nums;
}
.kpi-label { margin-top: 6px; font-size: 14px; color: #9cc7ff; letter-spacing: 2px; }
.zones { flex: 1; padding: 16px 36px 8px; overflow: auto; }
.zone-title {
font-size: 16px; letter-spacing: 3px; color: #bfe0ff;
border-left: 4px solid #409eff; padding-left: 10px; margin-bottom: 12px;
}
.zone-row {
display: flex; align-items: center; gap: 12px;
padding: 7px 0; border-bottom: 1px dashed rgba(64, 158, 255, 0.15);
}
.zone-name { width: 200px; text-align: right; color: #cfe6ff; font-size: 14px; }
.bar-wrap { flex: 1; height: 16px; background: rgba(19, 56, 110, 0.6); border-radius: 9px; overflow: hidden; }
.bar {
height: 100%; border-radius: 9px;
background: linear-gradient(90deg, #1673ff, #37e2ff);
box-shadow: 0 0 10px rgba(55, 210, 255, 0.65);
transition: width 0.6s ease;
}
.zone-qty { width: 90px; color: #61e6ff; font-weight: 700; font-variant-numeric: tabular-nums; }
.empty { color: #5f86b8; padding: 30px 0; text-align: center; }
.foot { padding: 12px 36px 16px; text-align: center; color: #47709f; font-size: 12px; }
</style>