Files
pqs-9100_client/frontend/src/views/machine/sntp/index.vue
caozehui 27b593ba01 微调
2026-06-02 11:21:55 +08:00

369 lines
8.8 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 class="sntp-page">
<section class="sntp-panel">
<el-row :gutter="16" class="time-list">
<el-col :xs="24" :md="12">
<div class="time-item">
<div class="time-label">当前电脑时间</div>
<div class="time-content">{{ computerTime }}</div>
</div>
</el-col>
<el-col :xs="24" :md="12">
<div class="time-item">
<div class="time-label">装置返回时间</div>
<div class="time-content">{{ deviceTime }}</div>
</div>
</el-col>
</el-row>
<div class="action-row">
<el-button type="primary" :loading="starting" :disabled="running || stopping" @click="handleStart">
启动SNTP对时服务
</el-button>
<el-button type="danger" plain :loading="stopping" :disabled="!running || starting" @click="handleStop">
停止SNTP对时服务
</el-button>
</div>
<div class="history-section">
<div class="history-header">
<span class="history-title">历史记录</span>
<el-button
plain
type="danger"
:disabled="historyList.length === 0"
@click="clearHistory"
>
清空
</el-button>
</div>
<div class="history-table">
<div class="history-table__head history-row">
<div class="col-order">序号</div>
<div>当前电脑时间</div>
<div>装置返回时间</div>
<div>误差ms</div>
</div>
<div v-if="historyList.length === 0" class="history-empty">
<span>暂无数据</span>
</div>
<div v-else class="history-table__body">
<div v-for="(item, index) in historyList" :key="item.id" class="history-row">
<div class="col-order">{{ index + 1 }}</div>
<div>{{ item.computerTime }}</div>
<div>{{ item.deviceTime }}</div>
<div>{{ formatErrorMs(item.errorMs) }}</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts" name="sntp">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { Delete } from '@element-plus/icons-vue'
import socketClient from '@/utils/webSocketClient'
import { startSntpService, stopSntpService } from '@/api/system/sntp'
interface SntpTimeMessage {
type: string
computerTime?: string
deviceTime?: string
computerTimestampMs?: number
deviceTimestampMs?: number
errorMs?: number
}
interface SntpHistoryItem {
id: string
computerTime: string
deviceTime: string
computerTimestampMs: number | null
deviceTimestampMs: number | null
errorMs: number | null
}
defineOptions({
name: 'sntp'
})
const messageType = 'sntp_time_update'
const maxHistoryCount = 50
const running = ref(false)
const starting = ref(false)
const stopping = ref(false)
const computerTimeValue = ref('--')
const deviceTimeValue = ref('--')
const historyList = ref<SntpHistoryItem[]>([])
const computerTime = computed(() => computerTimeValue.value)
const deviceTime = computed(() => deviceTimeValue.value)
const formatErrorMs = (errorMs: number | null) => {
if (errorMs === null || Number.isNaN(errorMs))
return '--'
if (errorMs > 0)
return `+${errorMs}`
return `${errorMs}`
}
const appendHistory = (
computerTimeText: string,
deviceTimeText: string,
computerTimestampMs: number | null,
deviceTimestampMs: number | null,
errorMs: number | null
) => {
const nextItem: SntpHistoryItem = {
id: `${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
computerTime: computerTimeText,
deviceTime: deviceTimeText,
computerTimestampMs,
deviceTimestampMs,
errorMs
}
historyList.value = [nextItem, ...historyList.value].slice(0, maxHistoryCount)
}
const handleTimeUpdate = (message: SntpTimeMessage) => {
const nextComputerTime = message.computerTime || '--'
const nextDeviceTime = message.deviceTime || '--'
const nextComputerTimestampMs = typeof message.computerTimestampMs === 'number' ? message.computerTimestampMs : null
const nextDeviceTimestampMs = typeof message.deviceTimestampMs === 'number' ? message.deviceTimestampMs : null
const nextErrorMs = typeof message.errorMs === 'number' ? message.errorMs : null
computerTimeValue.value = nextComputerTime
deviceTimeValue.value = nextDeviceTime
appendHistory(
nextComputerTime,
nextDeviceTime,
nextComputerTimestampMs,
nextDeviceTimestampMs,
nextErrorMs
)
}
const ensureSocketConnection = () => {
socketClient.Instance.connect()
socketClient.Instance.registerCallBack(messageType, (message: SntpTimeMessage) => {
handleTimeUpdate(message)
})
}
const clearHistory = () => {
historyList.value = []
}
const handleStart = async () => {
starting.value = true
try {
await startSntpService()
running.value = true
} finally {
starting.value = false
}
}
const handleStop = async () => {
stopping.value = true
try {
await stopSntpService()
running.value = false
} finally {
stopping.value = false
}
}
onMounted(() => {
ensureSocketConnection()
})
onBeforeUnmount(() => {
socketClient.Instance.unRegisterCallBack(messageType)
})
</script>
<style scoped lang="scss">
.sntp-page {
height: 100%;
min-height: 100%;
padding: 16px;
background: #f5f7fa;
display: flex;
}
.sntp-panel {
flex: 1;
min-height: 0;
background: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
display: flex;
flex-direction: column;
gap: 20px;
}
.time-list {
flex-shrink: 0;
}
.time-item {
min-height: 168px;
padding: 18px 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #ffffff;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.time-label {
font-size: 14px;
color: #606266;
}
.time-content {
font-size: 26px;
line-height: 1.35;
color: #303133;
word-break: break-word;
}
.action-row {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.history-section {
flex: 1;
min-height: 0;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
background: #ffffff;
}
.history-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid #e5e7eb;
background: #fafafa;
flex-shrink: 0;
}
.history-title {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.history-empty {
flex: 1;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
}
.history-table {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.history-table__head {
background: #fafafa;
flex-shrink: 0;
}
.history-table__body {
flex: 1;
min-height: 0;
overflow: auto;
}
.history-row {
display: grid;
grid-template-columns: 88px minmax(0, 1fr) minmax(0, 1fr) 200px;
}
.history-row > div {
padding: 14px 16px;
border-bottom: 1px solid #f0f2f5;
color: #303133;
word-break: break-word;
}
.history-row > div + div {
border-left: 1px solid #f0f2f5;
}
.history-table__head > div {
font-size: 13px;
color: #606266;
font-weight: 500;
}
.history-table__body .history-row:last-child > div {
border-bottom: none;
}
.col-order {
text-align: center;
}
@media (max-width: 900px) {
.sntp-page {
padding: 12px;
}
.sntp-panel {
padding: 16px;
gap: 16px;
}
.time-item {
min-height: 132px;
margin-bottom: 16px;
}
.time-content {
font-size: 22px;
}
.action-row {
flex-direction: column;
align-items: stretch;
}
.history-row {
grid-template-columns: 1fr;
}
.history-row > div + div {
border-left: none;
border-top: 1px solid #f0f2f5;
}
.col-order {
text-align: left;
}
}
</style>