2026-06-02 09:25:17 +08:00
|
|
|
|
<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>
|
2026-06-02 11:21:55 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
plain
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
:disabled="historyList.length === 0"
|
|
|
|
|
|
@click="clearHistory"
|
|
|
|
|
|
>
|
|
|
|
|
|
清空
|
|
|
|
|
|
</el-button>
|
2026-06-02 09:25:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-06-02 11:21:55 +08:00
|
|
|
|
<div class="history-table">
|
2026-06-02 09:25:17 +08:00
|
|
|
|
<div class="history-table__head history-row">
|
|
|
|
|
|
<div class="col-order">序号</div>
|
|
|
|
|
|
<div>当前电脑时间</div>
|
|
|
|
|
|
<div>装置返回时间</div>
|
2026-06-02 11:21:55 +08:00
|
|
|
|
<div>误差(ms)</div>
|
2026-06-02 09:25:17 +08:00
|
|
|
|
</div>
|
2026-06-02 11:21:55 +08:00
|
|
|
|
<div v-if="historyList.length === 0" class="history-empty">
|
|
|
|
|
|
<span>暂无数据</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="history-table__body">
|
2026-06-02 09:25:17 +08:00
|
|
|
|
<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>
|
2026-06-02 11:21:55 +08:00
|
|
|
|
<div>{{ formatErrorMs(item.errorMs) }}</div>
|
2026-06-02 09:25:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts" name="sntp">
|
|
|
|
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
2026-06-02 11:21:55 +08:00
|
|
|
|
import { Delete } from '@element-plus/icons-vue'
|
2026-06-02 09:25:17 +08:00
|
|
|
|
import socketClient from '@/utils/webSocketClient'
|
|
|
|
|
|
import { startSntpService, stopSntpService } from '@/api/system/sntp'
|
|
|
|
|
|
|
|
|
|
|
|
interface SntpTimeMessage {
|
|
|
|
|
|
type: string
|
|
|
|
|
|
computerTime?: string
|
|
|
|
|
|
deviceTime?: string
|
2026-06-02 11:21:55 +08:00
|
|
|
|
computerTimestampMs?: number
|
|
|
|
|
|
deviceTimestampMs?: number
|
|
|
|
|
|
errorMs?: number
|
2026-06-02 09:25:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface SntpHistoryItem {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
computerTime: string
|
|
|
|
|
|
deviceTime: string
|
2026-06-02 11:21:55 +08:00
|
|
|
|
computerTimestampMs: number | null
|
|
|
|
|
|
deviceTimestampMs: number | null
|
|
|
|
|
|
errorMs: number | null
|
2026-06-02 09:25:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2026-06-02 11:21:55 +08:00
|
|
|
|
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
|
|
|
|
|
|
) => {
|
2026-06-02 09:25:17 +08:00
|
|
|
|
const nextItem: SntpHistoryItem = {
|
|
|
|
|
|
id: `${Date.now()}_${Math.random().toString(16).slice(2, 8)}`,
|
|
|
|
|
|
computerTime: computerTimeText,
|
2026-06-02 11:21:55 +08:00
|
|
|
|
deviceTime: deviceTimeText,
|
|
|
|
|
|
computerTimestampMs,
|
|
|
|
|
|
deviceTimestampMs,
|
|
|
|
|
|
errorMs
|
2026-06-02 09:25:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
historyList.value = [nextItem, ...historyList.value].slice(0, maxHistoryCount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleTimeUpdate = (message: SntpTimeMessage) => {
|
|
|
|
|
|
const nextComputerTime = message.computerTime || '--'
|
|
|
|
|
|
const nextDeviceTime = message.deviceTime || '--'
|
2026-06-02 11:21:55 +08:00
|
|
|
|
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
|
2026-06-02 09:25:17 +08:00
|
|
|
|
computerTimeValue.value = nextComputerTime
|
|
|
|
|
|
deviceTimeValue.value = nextDeviceTime
|
2026-06-02 11:21:55 +08:00
|
|
|
|
appendHistory(
|
|
|
|
|
|
nextComputerTime,
|
|
|
|
|
|
nextDeviceTime,
|
|
|
|
|
|
nextComputerTimestampMs,
|
|
|
|
|
|
nextDeviceTimestampMs,
|
|
|
|
|
|
nextErrorMs
|
|
|
|
|
|
)
|
2026-06-02 09:25:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-02 11:21:55 +08:00
|
|
|
|
grid-template-columns: 88px minmax(0, 1fr) minmax(0, 1fr) 200px;
|
2026-06-02 09:25:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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>
|