add jose
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 统一时间格式化工具
|
||||
*
|
||||
* 背景:后端用 datetime.utcnow() 写入 UTC naive datetime。
|
||||
* 修复后端后,schema 输出会带 +08:00 或 Z(Asia/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}`
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user