This commit is contained in:
Your Name
2026-07-23 13:13:31 +08:00
parent 1a3c2d15c8
commit 5b93e4536f
16 changed files with 302 additions and 68 deletions
+124
View File
@@ -0,0 +1,124 @@
/**
* 统一时间格式化工具
*
* 背景:后端用 datetime.utcnow() 写入 UTC naive datetime。
* 修复后端后,schema 输出会带 +08:00 或 ZAsia/Shanghai 或 UTC)时区标签。
* 本 helper
* 1. 收到的字符串如果带时区标签(Z 或 +HH:MM),按字面解析
* 2. 如果无时区标签,假定为 UTC 解析(兼容老 endpoint 内联 dict 序列化)
* 3. 转 Asia/Shanghai (+08:00) 显示
* 4. 显示成 'YYYY-MM-DD HH:MM:SS' 字符串
*/
const BUSINESS_TZ = 'Asia/Shanghai'
export function parseToBusiness (input) {
if (input == null || input === '') return null
let d
if (input instanceof Date) {
d = input
} else if (typeof input === 'number') {
d = new Date(input)
} else {
// 字符串:优先按 ISO 解析(含 Z / +HH:MM 则按对应时区)
// JS 原生 new Date() 对 'YYYY-MM-DD HH:MM:SS'(无 T,无时区)按本地时区解析,
// 对 'YYYY-MM-DDTHH:MM:SSZ' 等标准 ISO 8601 则按 UTC 解析。
const s = String(input).trim()
// 后端 schema 修复后输出 'YYYY-MM-DDTHH:MM:SS+08:00'aware),
// 老 endpoint 内联 dict 输出 'YYYY-MM-DD HH:MM:SS'naive UTC)。
// JS Date 无法区分后者的'naive UTC'和'naive 本地时间'。
// 但本项目所有 datetime 后端都是 datetime.utcnow() 写的,
// 所以 naive 也按 UTC 处理。
d = new Date(s.includes('T') ? s : s.replace(' ', 'T') + 'Z')
}
if (isNaN(d.getTime())) return null
return d
}
/**
* 把任意 datetime 输入格式化成 Asia/Shanghai 时区的 'YYYY-MM-DD HH:MM:SS'。
* 不做时区转换的纯字符串渲染请用 formatUtc。
*/
export function formatDateTime (input) {
if (input == null || input === '') return '-'
const d = parseToBusiness(input)
if (!d) return '-'
const opts = {
timeZone: BUSINESS_TZ,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false,
}
try {
return new Intl.DateTimeFormat('zh-CN', opts).format(d)
// zh-CN 格式默认是 "2026/07/23 14:31:19",替换成 "2026-07-23 14:31:19"
.replace(/\//g, '-')
} catch {
// 浏览器不支持 Intl 时区时回退:用本地时区手动格式化
return formatLocal(d)
}
}
/**
* 仅显示日期 'YYYY-MM-DD'。
*/
export function formatDate (input) {
if (input == null || input === '') return '-'
const d = parseToBusiness(input)
if (!d) return '-'
try {
return new Intl.DateTimeFormat('zh-CN', {
timeZone: BUSINESS_TZ,
year: 'numeric', month: '2-digit', day: '2-digit',
}).format(d).replace(/\//g, '-')
} catch {
return formatLocal(d).substring(0, 10)
}
}
/**
* 仅显示时间 'HH:MM' 或 'HH:MM:SS'(带 seconds 参数控制)。
*/
export function formatTime (input, { seconds = false } = {}) {
if (input == null || input === '') return '-'
const d = parseToBusiness(input)
if (!d) return '-'
const opts = {
timeZone: BUSINESS_TZ,
hour: '2-digit', minute: '2-digit', hour12: false,
}
if (seconds) opts.second = '2-digit'
try {
return new Intl.DateTimeFormat('zh-CN', opts).format(d)
} catch {
return formatLocal(d).substring(11)
}
}
/**
* 用户友好的相对时间描述("3 分钟前")。
*/
export function formatRelative (input) {
if (input == null || input === '') return '-'
const d = parseToBusiness(input)
if (!d) return '-'
const diff = (Date.now() - d.getTime()) / 1000
if (diff < 60) return '刚刚'
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`
if (diff < 604800) return `${Math.floor(diff / 86400)} 天前`
return formatDateTime(input)
}
// 内部:浏览器不支持时区时回退到本地格式化(业务时区手动转)
function formatLocal (d) {
// 用本地方法拿到 YYYY-MM-DD HH:MM:SS(按浏览器本地时区,可能错 8 小时但作为兜底)
const pad = n => String(n).padStart(2, '0')
const y = d.getFullYear()
const mo = pad(d.getMonth() + 1)
const da = pad(d.getDate())
const h = pad(d.getHours())
const mi = pad(d.getMinutes())
const s = pad(d.getSeconds())
return `${y}-${mo}-${da} ${h}:${mi}:${s}`
}
+4 -6
View File
@@ -141,7 +141,7 @@
</el-table-column>
<el-table-column prop="created_at" label="时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
{{ formatDateTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center" fixed="right">
@@ -194,7 +194,7 @@
<el-table-column prop="owner" label="所有者" width="120" />
<el-table-column prop="created_at" label="添加时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
{{ formatDateTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
@@ -238,6 +238,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { alertApi } from '@/api'
import { formatDateTime, formatDate, formatRelative } from '@/utils/datetime'
const loading = ref(false)
const detecting = ref(false)
@@ -460,10 +461,7 @@ const getAlertTypeText = (type) => {
return texts[type] || type
}
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
// 旧 formatDate 已由 @/utils/datetime 统一替代
onMounted(() => {
loadStats()
+3 -5
View File
@@ -107,7 +107,7 @@
</el-table-column>
<el-table-column prop="last_seen" label="最后发现" width="170">
<template #default="scope">
{{ scope.row.last_seen ? formatDate(scope.row.last_seen) : '-' }}
{{ scope.row.last_seen ? formatDateTime(scope.row.last_seen) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" align="center" fixed="right">
@@ -174,6 +174,7 @@ import { ref, reactive, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ipApi, networkApi, scanApi } from '@/api'
import { formatDateTime, formatDate, formatRelative } from '@/utils/datetime'
const route = useRoute()
@@ -350,10 +351,7 @@ const getStatusText = (status) => {
return texts[status] || status
}
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
// 旧 formatDate 已由 @/utils/datetime 统一替代
onMounted(() => {
loadNetworks()
+3 -5
View File
@@ -58,7 +58,7 @@
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
{{ formatDateTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center" fixed="right">
@@ -139,6 +139,7 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { networkApi, scanApi } from '@/api'
import { formatDateTime, formatDate } from '@/utils/datetime'
const router = useRouter()
@@ -304,10 +305,7 @@ const getGroupTagType = (group) => {
return types[group] || 'info'
}
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
// 旧 formatDate 已由 @/utils/datetime 统一替代
onMounted(() => {
loadNetworks()
+5 -7
View File
@@ -39,7 +39,7 @@
</el-table-column>
<el-table-column prop="last_polled_at" label="最后轮询" width="180">
<template #default="scope">
{{ scope.row.last_polled_at ? formatDate(scope.row.last_polled_at) : '-' }}
{{ scope.row.last_polled_at ? formatDateTime(scope.row.last_polled_at) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="300" align="center" fixed="right">
@@ -98,7 +98,7 @@
<el-table-column prop="retries" label="重试次数" width="100" align="center" />
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
{{ formatDateTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center" fixed="right">
@@ -152,7 +152,7 @@
<el-table-column prop="interface" label="接口" width="120" />
<el-table-column prop="last_seen" label="最后发现" width="180">
<template #default="scope">
{{ formatDate(scope.row.last_seen) }}
{{ formatDateTime(scope.row.last_seen) }}
</template>
</el-table-column>
</el-table>
@@ -275,6 +275,7 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { snmpApi } from '@/api'
import { formatDateTime, formatDate, formatRelative } from '@/utils/datetime'
const activeTab = ref('devices')
const loading = ref(false)
@@ -530,10 +531,7 @@ const deleteCredential = async (id) => {
}
}
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
// 旧 formatDate 已由 @/utils/datetime 统一替代
onMounted(() => {
loading.value = true
+3 -10
View File
@@ -58,7 +58,7 @@
</el-table-column>
<el-table-column label="最后登录" width="170">
<template #default="scope">
{{ scope.row.last_login_at ? formatDate(scope.row.last_login_at) : '-' }}
{{ scope.row.last_login_at ? formatDateTime(scope.row.last_login_at) : '-' }}
</template>
</el-table-column>
<el-table-column label="操作" width="280" align="center" fixed="right">
@@ -162,6 +162,7 @@ import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Search, Refresh } from '@element-plus/icons-vue'
import { userApi } from '@/api/auth'
import { formatDateTime, formatDate, formatRelative } from '@/utils/datetime'
import { authStore } from '@/utils/auth'
const loading = ref(false)
@@ -206,15 +207,7 @@ function roleTagType(r) { return ROLE_MAP[r]?.tagType || '' }
function statusLabel(s) { return STATUS_MAP[s]?.label || s || '-' }
function statusTagType(s) { return STATUS_MAP[s]?.tagType || '' }
function formatDate(d) {
if (!d) return '-'
try {
const dt = new Date(d)
if (isNaN(dt.getTime())) return d
const pad = n => String(n).padStart(2, '0')
return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}`
} catch { return d }
}
// 旧 formatDate 已由 @/utils/datetime 统一替代
async function loadUsers() {
loading.value = true