Merge remote-tracking branch 'origin/master'

master
suixy 2 months ago
commit 7304ce2df0

@ -0,0 +1,10 @@
import request from '@/utils/request'
export function viewAndonBoard(boardCode) {
return request({
url: '/production/andonBoard/view',
method: 'get',
params: { boardCode },
headers: { isToken: false }
})
}

@ -34,7 +34,10 @@ export default {
getDateIntervalFun = setInterval(getDate, 1000)
},
beforeDestroy() {
getDateIntervalFun = null
if (getDateIntervalFun) {
clearInterval(getDateIntervalFun)
getDateIntervalFun = null
}
}
}
</script>

@ -8,7 +8,7 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/board1', '/board2', '/board3', '/board5', '/board6', '/register', '/liner', '/caseShell2', '/foaming', '/foaming2', '/pourInto', '/finalAssembly', '/scanDown', '/week', '/week2', '/model']
const whiteList = ['/login', '/board1', '/board2', '/board3', '/board5', '/board6', '/register', '/liner', '/caseShell2', '/foaming', '/foaming2', '/pourInto', '/finalAssembly', '/scanDown', '/week', '/week2', '/model', '/andonBoard']
router.beforeEach((to, from, next) => {
NProgress.start()

@ -197,6 +197,12 @@ export const constantRoutes = [
component: () => import('@/views/board/week2/index'),
meta: {title: '综合看板', icon: 'dashboard',}
},
{
path: 'andonBoard',
name: 'AndonBoard',
component: () => import('@/views/board/andonBoard/index'),
meta: {title: '安灯看板', icon: 'dashboard',}
},
]
},
{

@ -297,6 +297,15 @@ export default {
form: {},
//
rules: {
paramCode: [
{ required: true, message: "参数编号不能为空", trigger: "blur" }
],
paramName: [
{ required: true, message: "参数名称不能为空", trigger: "blur" }
],
deviceCode: [
{ required: true, message: "设备名称不能为空", trigger: "change" }
],
},
columns: [
{ key: 0, label: `主键标识`, visible: false },

@ -0,0 +1,681 @@
<template>
<div class="app-container">
<div class="headTitle">{{ boardTitle }}</div>
<div class="subTitle">
<span v-if="boardCode">boardCode: {{ boardCode }}</span>
<span v-else> boardCode</span>
<span class="split">|</span>
<span>刷新: {{ refreshIntervalSec }}s</span>
<span class="split">|</span>
<span>最近更新: {{ lastUpdateText }}</span>
</div>
<div v-if="errorMsg" class="error">{{ errorMsg }}</div>
<div class="content" v-else>
<div class="stats">
<div class="stat stat-pending">
<div class="stat-label">待处理</div>
<div class="stat-value">{{ stats.pending || 0 }}</div>
</div>
<div class="stat stat-processing">
<div class="stat-label">处理中</div>
<div class="stat-value">{{ stats.processing || 0 }}</div>
</div>
<div class="stat stat-resolved">
<div class="stat-label">已解决</div>
<div class="stat-value">{{ stats.resolved || 0 }}</div>
</div>
<div class="stat stat-cancelled">
<div class="stat-label">已取消</div>
<div class="stat-value">{{ stats.cancelled || 0 }}</div>
</div>
<div class="stat stat-total">
<div class="stat-label">总数</div>
<div class="stat-value">{{ stats.total || 0 }}</div>
</div>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">待处理 / 处理中 ({{ stats.active || 0 }})</div>
<div class="table">
<div class="grid-header" :style="gridStyle(activeColumns)">
<div
v-for="col in activeColumns"
:key="col.field"
class="cell header"
:style="cellStyle(col)"
>
{{ col.label }}
</div>
</div>
<div class="grid-body">
<div
v-for="(row, rowIndex) in activeEvents"
:key="row.eventId || rowIndex"
class="grid-row"
:style="gridStyle(activeColumns)"
>
<div
v-for="col in activeColumns"
:key="col.field"
class="cell"
:style="cellStyle(col)"
>
<span
v-if="col.field === 'eventStatus'"
:class="['status', statusClass(row.eventStatus)]"
>
{{ statusText(row.eventStatus) }}
</span>
<span v-else>
{{ formatValue(row, col.field, rowIndex) }}
</span>
</div>
</div>
<div v-if="!activeEvents || activeEvents.length === 0" class="empty"></div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-title">已解决 / 已取消 ({{ closedEvents.length }})</div>
<div class="table">
<div class="grid-header" :style="gridStyle(closedColumns)">
<div
v-for="col in closedColumns"
:key="col.field"
class="cell header"
:style="cellStyle(col)"
>
{{ col.label }}
</div>
</div>
<div class="grid-body">
<div
v-for="(row, rowIndex) in closedEvents"
:key="row.eventId || rowIndex"
class="grid-row"
:style="gridStyle(closedColumns)"
>
<div
v-for="col in closedColumns"
:key="col.field"
class="cell"
:style="cellStyle(col)"
>
<span
v-if="col.field === 'eventStatus'"
:class="['status', statusClass(row.eventStatus)]"
>
{{ statusText(row.eventStatus) }}
</span>
<span v-else>
{{ formatValue(row, col.field, rowIndex) }}
</span>
</div>
</div>
<div v-if="!closedEvents || closedEvents.length === 0" class="empty"></div>
</div>
</div>
</div>
</div>
<div v-if="loading" class="loading">...</div>
</div>
</div>
</template>
<script>
import { viewAndonBoard } from '@/api/production/andonBoard'
import { parseTime } from '@/utils/ruoyi'
export default {
name: 'AndonBoard',
data() {
return {
boardCode: '',
loading: false,
fetching: false,
errorMsg: '',
config: {},
stats: {},
activeEvents: [],
closedEvents: [],
activeColumns: [],
closedColumns: [],
refreshIntervalSec: 10,
lastServerTime: null,
refreshTimer: null,
refreshTimerSec: null,
}
},
computed: {
boardTitle() {
const name = this.config && this.config.boardName ? this.config.boardName : '安灯看板'
return name
},
lastUpdateText() {
return this.lastServerTime ? parseTime(this.lastServerTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-'
}
},
created() {
this.boardCode = this.getBoardCode()
this.reload()
},
watch: {
'$route.query.boardCode': function () {
const nextCode = this.getBoardCode()
if (nextCode !== this.boardCode) {
this.boardCode = nextCode
this.reload()
}
}
},
beforeDestroy() {
this.clearRefreshTimer()
},
methods: {
getBoardCode() {
return (this.$route.query && this.$route.query.boardCode) || (this.$route.params && this.$route.params.boardCode) || ''
},
reload() {
this.clearRefreshTimer()
if (!this.boardCode) {
this.errorMsg = '请在URL中传入参数 boardCode例如/andonBoard?boardCode=AD-001'
return
}
this.errorMsg = ''
this.lastServerTime = null
this.fetchBoardData()
},
fetchBoardData() {
if (this.fetching) {
return Promise.resolve()
}
const showLoading = !this.lastServerTime
this.fetching = true
if (showLoading) {
this.loading = true
}
return viewAndonBoard(this.boardCode)
.then((res) => {
const data = (res && res.data) || {}
this.errorMsg = ''
this.config = data.config || {}
this.stats = data.stats || {}
this.activeEvents = data.activeEvents || []
this.closedEvents = data.closedEvents || []
this.lastServerTime = data.serverTime || null
const interval = Number(this.config && this.config.refreshIntervalSec)
this.refreshIntervalSec = interval && interval > 0 ? interval : 10
const cols = this.parseDisplayFields(this.config && this.config.displayFields)
this.activeColumns = cols.active
this.closedColumns = cols.closed
this.resetRefreshTimer()
})
.catch((e) => {
this.errorMsg = (e && e.message) || '加载失败'
})
.finally(() => {
this.fetching = false
if (showLoading) {
this.loading = false
}
this.resetRefreshTimer()
})
},
clearRefreshTimer() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
this.refreshTimer = null
this.refreshTimerSec = null
}
},
resetRefreshTimer() {
const sec = this.refreshIntervalSec
if (!sec || sec <= 0) {
return
}
if (this.refreshTimer && this.refreshTimerSec === sec) {
return
}
this.clearRefreshTimer()
this.refreshTimerSec = sec
this.refreshTimer = setInterval(() => {
this.fetchBoardData()
}, sec * 1000)
},
parseDisplayFields(raw) {
const fallbackActive = ['callCode', 'callTypeCode', 'stationCode', 'deviceCode', 'eventStatus', 'priority', 'createTime', 'description']
const fallbackClosed = ['callCode', 'callTypeCode', 'stationCode', 'deviceCode', 'eventStatus', 'responseEndTime', 'resolution', 'cancelReason']
if (!raw) {
return {
active: this.normalizeColumns(fallbackActive),
closed: this.normalizeColumns(fallbackClosed),
}
}
if (typeof raw === 'string') {
const trimmed = raw.trim()
if (trimmed && trimmed[0] !== '[' && trimmed[0] !== '{') {
const fields = trimmed.split(/[,;\s]+/).filter(Boolean)
if (fields.length) {
const cols = this.normalizeColumns(fields)
return { active: cols, closed: cols }
}
}
}
let parsed
try {
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
} catch (e) {
return {
active: this.normalizeColumns(fallbackActive),
closed: this.normalizeColumns(fallbackClosed),
}
}
if (Array.isArray(parsed)) {
const cols = this.normalizeColumns(parsed)
return { active: cols, closed: cols }
}
if (parsed && typeof parsed === 'object') {
const activeRaw = parsed.activeFields || parsed.active || parsed.fields || parsed
const closedRaw = parsed.closedFields || parsed.closed || parsed.fields || parsed
const activeCols = Array.isArray(activeRaw) || typeof activeRaw === 'object' ? this.normalizeColumns(activeRaw) : this.normalizeColumns(fallbackActive)
const closedCols = Array.isArray(closedRaw) || typeof closedRaw === 'object' ? this.normalizeColumns(closedRaw) : this.normalizeColumns(fallbackClosed)
return {
active: activeCols && activeCols.length ? activeCols : this.normalizeColumns(fallbackActive),
closed: closedCols && closedCols.length ? closedCols : this.normalizeColumns(fallbackClosed),
}
}
return {
active: this.normalizeColumns(fallbackActive),
closed: this.normalizeColumns(fallbackClosed),
}
},
normalizeColumns(input) {
if (!input) return []
if (Array.isArray(input)) {
return input
.map((item) => {
if (!item) return null
if (typeof item === 'string') {
return { field: item, label: this.fieldLabel(item) }
}
if (typeof item === 'object') {
const field = item.field || item.prop || item.key
if (!field) return null
return {
field,
label: item.label || item.title || this.fieldLabel(field),
width: item.width,
align: item.align,
}
}
return null
})
.filter(Boolean)
}
if (input && typeof input === 'object') {
return Object.keys(input).map((field) => {
const val = input[field]
if (typeof val === 'string') {
return { field, label: val }
}
if (typeof val === 'object' && val) {
return {
field,
label: val.label || val.title || this.fieldLabel(field),
width: val.width,
align: val.align,
}
}
return { field, label: this.fieldLabel(field) }
})
}
return []
},
fieldLabel(field) {
const map = {
index: '序号',
eventId: '事件ID',
callCode: '呼叫单号',
callTypeCode: '呼叫类型',
sourceType: '触发源类型',
sourceRefId: '触发源',
productLineCode: '产线',
stationCode: '工位',
teamCode: '班组',
orderCode: '工单号',
materialCode: '物料编码',
deviceCode: '设备编码',
priority: '优先级',
eventStatus: '状态',
description: '描述',
ackBy: '确认人',
ackTime: '确认时间',
responseStartTime: '开始处理时间',
responseEndTime: '完成时间',
resolution: '解决措施',
cancelReason: '取消原因',
escalateLevel: '升级级别',
escalateTime: '升级时间',
ackDeadline: '确认截止',
resolveDeadline: '解决截止',
createTime: '创建时间',
updateTime: '更新时间',
}
return map[field] || field
},
formatValue(row, field, rowIndex) {
if (field === 'index') {
return rowIndex + 1
}
const v = row ? row[field] : null
if (v === null || v === undefined) return ''
if (field === 'eventStatus') {
return this.statusText(v)
}
if (field === 'sourceType') {
return this.sourceTypeText(v)
}
if (/(Time|Deadline)$/i.test(field)) {
const text = parseTime(v, '{y}-{m}-{d} {h}:{i}:{s}')
return text || ''
}
return String(v)
},
statusText(status) {
if (status === '0') return '待处理'
if (status === '1') return '处理中'
if (status === '2') return '已解决'
if (status === '3') return '已取消'
return String(status || '')
},
statusClass(status) {
if (status === '0') return 'pending'
if (status === '1') return 'processing'
if (status === '2') return 'resolved'
if (status === '3') return 'cancelled'
return ''
},
sourceTypeText(v) {
if (v === '0') return '工位'
if (v === '1') return '设备'
if (v === '2') return '报警'
if (v === '3') return '手动'
return String(v || '')
},
gridStyle(cols) {
const list = Array.isArray(cols) ? cols : []
const template = list
.map((c) => {
if (c && c.width) {
if (typeof c.width === 'number') return c.width + 'px'
return String(c.width)
}
return '1fr'
})
.join(' ')
return {
gridTemplateColumns: template || '1fr',
}
},
cellStyle(col) {
const style = {}
if (col && col.align) {
style.textAlign = col.align
}
return style
},
}
}
</script>
<style scoped lang="less">
.app-container {
background: #0f2740;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
padding: 4.2vw 2vw 1.2vw 2vw;
box-sizing: border-box;
color: #d6eaed;
overflow: hidden;
}
.headTitle {
position: absolute;
top: 5%;
left: 50%;
transform: translate(-50%, -100%);
font-size: 1.6vw;
letter-spacing: 0.3vw;
white-space: nowrap;
}
.subTitle {
position: absolute;
top: 8.1%;
left: 50%;
transform: translate(-50%, -100%);
font-size: 0.85vw;
color: #a9d5e8;
white-space: nowrap;
}
.split {
display: inline-block;
margin: 0 0.6vw;
color: rgba(255, 255, 255, 0.35);
}
.content {
height: 100%;
display: flex;
flex-direction: column;
}
.stats {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1vw;
margin-bottom: 1.2vw;
}
.stat {
height: 5.6vw;
border-radius: 0.6vw;
padding: 0.8vw 1vw;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.12);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stat-label {
font-size: 0.95vw;
opacity: 0.9;
}
.stat-value {
font-size: 2.1vw;
font-weight: 700;
line-height: 1;
}
.stat-pending .stat-value {
color: #ff5f5f;
}
.stat-processing .stat-value {
color: #ffb020;
}
.stat-resolved .stat-value {
color: #16ceb9;
}
.stat-cancelled .stat-value {
color: #9aa3ad;
}
.stat-total .stat-value {
color: #ffffff;
}
.panels {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.2vw;
min-height: 0;
}
.panel {
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 0.6vw;
padding: 0.8vw;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 0;
}
.panel-title {
font-size: 1.05vw;
font-weight: 700;
margin-bottom: 0.6vw;
letter-spacing: 0.1vw;
}
.table {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.grid-header {
display: grid;
gap: 0;
background: rgba(9, 65, 112, 0.75);
border-radius: 0.4vw;
overflow: hidden;
}
.grid-body {
flex: 1;
min-height: 0;
overflow: auto;
margin-top: 0.4vw;
}
.grid-body::-webkit-scrollbar {
width: 0;
height: 0;
}
.grid-row {
display: grid;
background: rgba(3, 45, 87, 0.55);
}
.grid-row:nth-child(2n) {
background: rgba(5, 52, 96, 0.55);
}
.cell {
padding: 0.55vw 0.6vw;
font-size: 0.85vw;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid rgba(255, 255, 255, 0.06);
}
.cell:last-child {
border-right: none;
}
.cell.header {
font-size: 0.85vw;
font-weight: 700;
color: #ffffff;
}
.status {
font-weight: 700;
}
.status.pending {
color: #ff5f5f;
}
.status.processing {
color: #ffb020;
}
.status.resolved {
color: #16ceb9;
}
.status.cancelled {
color: #9aa3ad;
}
.empty {
padding: 1vw;
text-align: center;
color: rgba(255, 255, 255, 0.55);
font-size: 0.9vw;
}
.error {
margin-top: 6vw;
padding: 1vw;
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 95, 95, 0.6);
border-radius: 0.6vw;
font-size: 1vw;
color: #ffbdbd;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 1vw;
color: rgba(255, 255, 255, 0.8);
background: rgba(0, 0, 0, 0.35);
padding: 0.6vw 1.2vw;
border-radius: 0.4vw;
}
</style>

@ -47,12 +47,8 @@
</el-form-item>
<el-form-item label="是否有效" prop="isFlag">
<el-select v-model="queryParams.isFlag" placeholder="请选择是否有效" clearable>
<el-option
v-for="dict in dict.type.is_flag"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
<el-option label="有效" value="1" />
<el-option label="无效" value="0" />
</el-select>
</el-form-item>-->
<el-form-item>
@ -118,7 +114,7 @@
<el-table-column label="刷新间隔" align="center" prop="refreshIntervalSec" v-if="columns[6].visible"/>
<el-table-column label="是否有效" align="center" prop="isFlag" v-if="columns[7].visible">
<template slot-scope="scope">
<dict-tag :options="dict.type.is_flag" :value="scope.row.isFlag"/>
<span>{{ scope.row.isFlag === '1' ? '有效' : '无效' }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" v-if="columns[8].visible"/>
@ -179,16 +175,19 @@
/>
</el-select>
</el-form-item>
<el-form-item label="展示字段配置" prop="displayFields" label-width="120px">
<el-input type="textarea" v-model="form.displayFields" placeholder="请输入展示字段配置" :rows="3" />
<div style="margin-top: 6px;">
<el-button type="info" size="mini" plain @click="helpVisible = true">操作说明</el-button>
</div>
</el-form-item>
<el-form-item label="刷新间隔" prop="refreshIntervalSec">
<el-input v-model="form.refreshIntervalSec" placeholder="请输入刷新间隔" />
</el-form-item>
<el-form-item label="是否有效" prop="isFlag">
<el-radio-group v-model="form.isFlag">
<el-radio
v-for="dict in dict.type.is_flag"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
<el-radio label="1">有效</el-radio>
<el-radio label="0">无效</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
@ -200,6 +199,37 @@
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 展示字段配置操作说明 -->
<el-dialog title="展示字段配置说明" :visible.sync="helpVisible" width="640px" append-to-body>
<div class="help-block">
<p><strong>作用</strong>控制大屏进行中/已关闭列表显示哪些列顺序与标题可单独设置列宽与对齐</p>
<p><strong>写法示例</strong></p>
<ol>
<li>逗号分隔字符串两张表共用同列<br><code>callCode,stationCode,eventStatus,priority,createTime</code></li>
<li>JSON 数组可带 label/width/align<br><pre style="margin:4px 0;">[
"callCode",
{ "field": "eventStatus", "label": "状态", "align": "center" }
]</pre></li>
<li>JSON 对象区分进行中/已关闭<br><pre style="margin:4px 0;">{
"activeFields": ["callCode","priority"],
"closedFields": ["callCode","responseEndTime","resolution"]
}</pre></li>
</ol>
<p><strong>可用字段</strong></p>
<div class="field-tags">
<ul style="padding-left: 18px; margin: 6px 0;">
<li v-for="f in availableFields" :key="f.field" style="margin: 2px 0;">
<code>{{ f.field }}</code> - {{ f.label }}
</li>
</ul>
</div>
<p style="margin-top: 8px; color: #888;">未填写或格式错误时会回退默认列label/width/align 未设置则自动使用字段默认标题</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="helpVisible = false">知道了</el-button>
</span>
</el-dialog>
</div>
</template>
@ -210,7 +240,7 @@ import { listProcessStation } from "@/api/base/processStation";
export default {
name: "AndonBoardConfig",
dicts: ['is_flag'],
dicts: [],
data() {
return {
//
@ -235,6 +265,8 @@ export default {
title: "",
//
open: false,
//
helpVisible: false,
//
queryParams: {
pageNum: 1,
@ -276,6 +308,37 @@ export default {
{ key: 11, label: `更新人`, visible: true },
{ key: 12, label: `更新时间`, visible: true },
]
,
//
availableFields: [
{ field: "index", label: "序号(前端行号)" },
{ field: "eventId", label: "事件ID" },
{ field: "callCode", label: "呼叫单号" },
{ field: "callTypeCode", label: "呼叫类型" },
{ field: "sourceType", label: "触发源类型(工位/设备/报警/手动)" },
{ field: "sourceRefId", label: "触发源编码" },
{ field: "productLineCode", label: "产线编码" },
{ field: "stationCode", label: "工位/工序编码" },
{ field: "teamCode", label: "班组编码" },
{ field: "orderCode", label: "工单号" },
{ field: "materialCode", label: "物料编码" },
{ field: "deviceCode", label: "设备编码" },
{ field: "priority", label: "优先级" },
{ field: "eventStatus", label: "状态(待处理/处理中/已解决/已取消)" },
{ field: "description", label: "呼叫描述" },
{ field: "ackBy", label: "确认人" },
{ field: "ackTime", label: "确认时间" },
{ field: "responseStartTime", label: "开始处理时间" },
{ field: "responseEndTime", label: "完成时间" },
{ field: "resolution", label: "解决措施" },
{ field: "cancelReason", label: "取消原因" },
{ field: "escalateLevel", label: "升级级别" },
{ field: "escalateTime", label: "升级时间" },
{ field: "ackDeadline", label: "确认截止时间" },
{ field: "resolveDeadline", label: "解决截止时间" },
{ field: "createTime", label: "创建时间" },
{ field: "updateTime", label: "更新时间" }
]
};
},
created() {
@ -337,6 +400,7 @@ export default {
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.form.isFlag = '1'; //
this.open = true;
this.title = "添加安灯看板配置";
},

Loading…
Cancel
Save