feat: 移除PDI和订单号字段,新增设备巡检模块

- 从物料跟踪页面移除订单号列和表单字段
- 从导航菜单移除PDI管理,添加设备巡检
- 新增InspectionLocation和InspectionRecord后端模型和API
- 新增设备巡检前端页面(左侧点位列表,右侧设备和历史记录)
This commit is contained in:
2026-05-27 16:38:40 +08:00
commit 193da0018f
86 changed files with 11379 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
<template>
<div>
<div class="card">
<div class="card-body" style="padding:10px 14px;">
<div class="flex-row" style="flex-wrap:wrap;gap:12px;">
<div class="flex-row">
<span class="kv-label">报文类型</span>
<input v-model="query.msg_type" class="kv-input" style="width:90px;" placeholder="PC01..." />
</div>
<div class="flex-row">
<span class="kv-label">方向</span>
<select v-model="query.direction" class="kv-input" style="width:90px;">
<option value="">全部</option>
<option value="recv">接收</option>
<option value="send">发送</option>
</select>
</div>
<div class="flex-row">
<span class="kv-label">状态</span>
<select v-model="query.status" class="kv-input" style="width:90px;">
<option value="">全部</option>
<option value="success">成功</option>
<option value="error">失败</option>
</select>
</div>
<div class="flex-row">
<button class="btn btn-primary" @click="fetchData">查询</button>
<button class="btn btn-outline" @click="fetchData"> 刷新</button>
</div>
<div style="margin-left:auto;" class="flex-row">
<span class="kv-label">
成功 <span class="kv-value" style="color:var(--accent-green)">{{ successCount }}</span>
&nbsp; 失败 <span class="kv-value" style="color:var(--accent-red)">{{ errorCount }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
报文监控日志
<span class="ch-badge" :style="{ color: connected ? 'var(--accent-green)' : 'var(--accent-red)' }">
{{ connected ? '● UDP在线' : '○ 未连接' }}
</span>
</div>
<div class="table-scroll" v-loading="loading">
<table class="data-table">
<thead>
<tr>
<th>报文ID</th><th>类型</th><th>方向</th><th>来源</th>
<th>状态</th><th>耗时(ms)</th><th>错误信息</th><th>接收时间</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td class="td-num">{{ row.msg_id }}</td>
<td><span class="badge badge-blue">{{ row.msg_type }}</span></td>
<td>
<span :class="['badge', row.direction==='recv' ? 'badge-green' : 'badge-blue']">
{{ row.direction === 'recv' ? '↓ 接收' : '↑ 发送' }}
</span>
</td>
<td class="td-muted">{{ row.source }}</td>
<td>
<span :class="['badge', row.status==='success' ? 'badge-green' : 'badge-red']">
{{ row.status === 'success' ? '成功' : '失败' }}
</span>
</td>
<td :class="row.process_time > 100 ? 'td-warn' : 'td-num'">{{ row.process_time || '—' }}</td>
<td class="td-err">{{ row.error_msg || '—' }}</td>
<td class="td-muted">{{ fmtTime(row.received_at) }}</td>
<td><span class="action-link" @click="viewDetail(row)">详情</span></td>
</tr>
<tr v-if="!tableData.length && !loading">
<td colspan="9" class="td-muted" style="text-align:center;padding:24px;">暂无报文记录</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 详情 Modal -->
<div v-if="detailVisible" class="modal-mask" @click.self="detailVisible=false">
<div class="modal-box" style="width:760px;">
<div class="modal-header">
报文详情 <span style="color:var(--sms-highlight)">{{ currentLog && currentLog.msg_type }}</span>
<span class="modal-close" @click="detailVisible=false"></span>
</div>
<div class="modal-body" v-if="currentLog">
<div class="kv-grid" style="margin-bottom:14px;">
<span class="kv-label">报文类型</span><span class="kv-value">{{ currentLog.msg_type }}</span>
<span class="kv-label">接收时间</span><span class="kv-value">{{ fmtTime(currentLog.received_at) }}</span>
<span class="kv-label">状态</span>
<span><span :class="['badge', currentLog.status==='success' ? 'badge-green' : 'badge-red']">{{ currentLog.status }}</span></span>
</div>
<div class="sec-title">原始报文HEX</div>
<pre class="raw-box">{{ currentLog.raw_data || '—' }}</pre>
<div class="sec-title" style="margin-top:12px;">解析结果JSON</div>
<pre class="raw-box">{{ formatJson(currentLog.parsed_data) }}</pre>
</div>
<div class="modal-footer">
<button class="btn btn-outline" @click="detailVisible=false">关闭</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { getMessageLogs, getMessageLog } from '@/api'
export default {
name: 'Message',
data() {
return {
loading: false,
tableData: [], total: 0,
query: { page: 1, page_size: 50, msg_type: '', direction: '', status: '' },
connected: true,
detailVisible: false, currentLog: null,
}
},
computed: {
successCount() { return this.tableData.filter(r => r.status === 'success').length },
errorCount() { return this.tableData.filter(r => r.status === 'error').length },
},
created() { this.fetchData() },
methods: {
async fetchData() {
this.loading = true
try {
const res = await getMessageLogs(this.query)
this.tableData = res.data.items
this.total = res.data.total
} finally { this.loading = false }
},
fmtTime(t) { return t ? t.replace('T',' ').slice(0,19) : '—' },
async viewDetail(row) {
const res = await getMessageLog(row.id)
this.currentLog = res.data
this.detailVisible = true
},
formatJson(s) {
if (!s) return '—'
try { return JSON.stringify(JSON.parse(s), null, 2) } catch { return s }
}
}
}
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables';
.action-link { color: $sms-highlight; cursor: pointer; font-size: 12px; &:hover { text-decoration: underline; } }
.raw-box {
background: #0a0f18; color: #d4d4d4;
padding: 12px; border-radius: 4px; border: 1px solid $border;
font-size: 11px; font-family: $font-mono;
max-height: 180px; overflow-y: auto;
white-space: pre-wrap; word-break: break-all;
}
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.modal-box { background: $bg-card; border: 1px solid $border; border-radius: 6px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; }
.modal-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: $bg-panel; border-bottom: 1px solid $border; font-size: 13px; font-weight: 600; color: $sms-highlight; .modal-close { cursor: pointer; color: $text-muted; &:hover { color: $text-primary; } } }
.modal-body { padding: 16px; overflow-y: auto; }
.modal-footer { padding: 10px 16px; background: $bg-panel; border-top: 1px solid $border; display: flex; justify-content: flex-end; gap: 10px; }
</style>