|
|
|
@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
|
|
<div class="log-dashboard">
|
|
|
|
|
|
|
|
<h1 class="page-title">日志看板</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section class="filters card">
|
|
|
|
|
|
|
|
<div class="filters-row">
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
|
|
机床
|
|
|
|
|
|
|
|
<select v-model="filters.machineId">
|
|
|
|
|
|
|
|
<option value="">全部</option>
|
|
|
|
|
|
|
|
<option v-for="m in machines" :key="m" :value="m">{{ m }}</option>
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
|
|
程序名
|
|
|
|
|
|
|
|
<input v-model="filters.programName" placeholder="过滤加工程序" />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label>
|
|
|
|
|
|
|
|
时间范围
|
|
|
|
|
|
|
|
<input type="date" v-model="filters.startDate" />
|
|
|
|
|
|
|
|
<span>—</span>
|
|
|
|
|
|
|
|
<input type="date" v-model="filters.endDate" />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<button @click="loadDashboard">刷新</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section class="summary card" v-if="data">
|
|
|
|
|
|
|
|
<div class="summary-item" v-for="(count, key) in data.counts" :key="key">
|
|
|
|
|
|
|
|
<div class="label">{{ key }}</div>
|
|
|
|
|
|
|
|
<div class="value">{{ count }}</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="summary-analyses" v-if="data.analysis">
|
|
|
|
|
|
|
|
<h3>分析摘要</h3>
|
|
|
|
|
|
|
|
<p>{{ data.analysis }}</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<section class="logs card" v-if="data?.logs?.length">
|
|
|
|
|
|
|
|
<table class="logs-table">
|
|
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
|
|
<th>时间</th>
|
|
|
|
|
|
|
|
<th>机床</th>
|
|
|
|
|
|
|
|
<th>程序</th>
|
|
|
|
|
|
|
|
<th>等级</th>
|
|
|
|
|
|
|
|
<th>日志摘要</th>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
|
|
<tr v-for="log in data.logs" :key="log.id">
|
|
|
|
|
|
|
|
<td>{{ log.timestamp }}</td>
|
|
|
|
|
|
|
|
<td>{{ log.machineId }}</td>
|
|
|
|
|
|
|
|
<td>{{ log.programName }}</td>
|
|
|
|
|
|
|
|
<td>{{ log.level }}</td>
|
|
|
|
|
|
|
|
<td class="message" :title="log.message">{{ log.messageSnippet }}</td>
|
|
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
|
|
import { ref, onMounted, computed } from 'vue'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type LogItem = {
|
|
|
|
|
|
|
|
id: string
|
|
|
|
|
|
|
|
timestamp: string
|
|
|
|
|
|
|
|
machineId: string
|
|
|
|
|
|
|
|
programName: string
|
|
|
|
|
|
|
|
level: string
|
|
|
|
|
|
|
|
message: string
|
|
|
|
|
|
|
|
messageSnippet: string
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type DashboardData = {
|
|
|
|
|
|
|
|
total: number
|
|
|
|
|
|
|
|
counts: Record<string, number>
|
|
|
|
|
|
|
|
logs: LogItem[]
|
|
|
|
|
|
|
|
analysis?: string
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const data = ref<DashboardData | null>(null)
|
|
|
|
|
|
|
|
const machines = ref<string[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const filters = ref({ machineId: '', programName: '', startDate: '', endDate: '' })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const loadDashboard = async () => {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const query = new URLSearchParams({}).toString()
|
|
|
|
|
|
|
|
const res = await fetch('/api/logs/dashboard?' + query)
|
|
|
|
|
|
|
|
if (!res.ok) throw new Error('网络错误')
|
|
|
|
|
|
|
|
const json = await res.json()
|
|
|
|
|
|
|
|
// 兼容后端返回不同字段名的容错处理
|
|
|
|
|
|
|
|
data.value = {
|
|
|
|
|
|
|
|
total: json.total ?? json.logs?.length ?? 0,
|
|
|
|
|
|
|
|
counts: json.counts ?? {},
|
|
|
|
|
|
|
|
logs: (json.logs ?? []).map((l: any) => ({
|
|
|
|
|
|
|
|
id: l.id ?? l.timestamp + '-' + l.machineId,
|
|
|
|
|
|
|
|
timestamp: l.timestamp,
|
|
|
|
|
|
|
|
machineId: l.machineId,
|
|
|
|
|
|
|
|
programName: l.programName,
|
|
|
|
|
|
|
|
level: l.level,
|
|
|
|
|
|
|
|
message: l.message,
|
|
|
|
|
|
|
|
messageSnippet: (l.message ?? '').slice(0, 100)
|
|
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
} as DashboardData
|
|
|
|
|
|
|
|
// 机床列表用于筛选
|
|
|
|
|
|
|
|
if (Array.isArray(json.machines)) machines.value = json.machines
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.error('加载看板失败', e)
|
|
|
|
|
|
|
|
data.value = null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
|
|
loadDashboard()
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 计算用:将日志中的摘要截断后显示
|
|
|
|
|
|
|
|
const normalizeLogs = computed(() => {
|
|
|
|
|
|
|
|
if (!data.value) return []
|
|
|
|
|
|
|
|
return data.value.logs.map((l) => ({ ...l, messageSnippet: (l.message ?? '').slice(0, 120) }))
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
.log-dashboard { padding: 16px; }
|
|
|
|
|
|
|
|
.page-title { font-size: 28px; margin-bottom: 12px; }
|
|
|
|
|
|
|
|
.card { background: #fff; border: 1px solid #eee; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
|
|
|
|
|
|
|
|
.filters-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
|
|
|
|
|
|
|
|
.filters label { display: flex; flex-direction: column; font-size: 12px; color: #555; }
|
|
|
|
|
|
|
|
.filters input, .filters select { padding: 6px 8px; border: 1px solid #ddd; border-radius: 4px; min-width: 180px; }
|
|
|
|
|
|
|
|
.filters button { padding: 6px 12px; border: none; background: #409eff; color: white; border-radius: 4px; cursor: pointer; }
|
|
|
|
|
|
|
|
.summary { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; }
|
|
|
|
|
|
|
|
.summary-item { background: #f9f9f9; padding: 12px; border-radius: 6px; text-align: center; }
|
|
|
|
|
|
|
|
.summary-item .label { font-size: 12px; color: #666; }
|
|
|
|
|
|
|
|
.summary-item .value { font-size: 20px; font-weight: bold; margin-top: 6px; }
|
|
|
|
|
|
|
|
.logs-table { width: 100%; border-collapse: collapse; }
|
|
|
|
|
|
|
|
.logs-table th, .logs-table td { border-bottom: 1px solid #eee; padding: 8px; text-align: left; font-family: sans-serif; font-size: 12px; }
|
|
|
|
|
|
|
|
.logs-table .message { max-width: 420px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
|
|
|
</style>
|