diff --git a/backend/src/components/CommonHeader.vue b/backend/src/components/CommonHeader.vue index 6e89dc4..cf24c46 100644 --- a/backend/src/components/CommonHeader.vue +++ b/backend/src/components/CommonHeader.vue @@ -120,6 +120,7 @@ import { User, SwitchButton, Sunny, Moon, Refresh, Bell, HomeFilled, Loading, Me import { ElMessage } from 'element-plus'; import { getMySiteReminders, readAllSiteReminders } from "@/api/sitereminder"; import MessageDetailDialog from "./MessageDetailDialog.vue"; +import { formatTime } from "@/utils/datetime"; const router = useRouter(); const route = useRoute(); @@ -181,12 +182,6 @@ const loadingMessages = ref(false); const detailVisible = ref(false); const currentReminder = ref({}); -const formatTime = (timeStr: any) => { - if (!timeStr) return "-"; - const date = new Date(timeStr); - return date.toLocaleString(); -}; - const fetchUnreadCount = async () => { if (!authStore.token) return; try { diff --git a/backend/src/components/MessageDetailDialog.vue b/backend/src/components/MessageDetailDialog.vue index 32e367a..9818445 100644 --- a/backend/src/components/MessageDetailDialog.vue +++ b/backend/src/components/MessageDetailDialog.vue @@ -32,6 +32,7 @@ import { computed, ref, watch } from "vue"; import { User, Calendar } from "@element-plus/icons-vue"; import { readSiteReminder } from "@/api/sitereminder"; +import { formatTime } from "@/utils/datetime"; const props = defineProps({ modelValue: Boolean, @@ -62,12 +63,6 @@ const getSenderName = (item: any) => { return item.sender_type; }; -const formatTime = (timeStr: any) => { - if (!timeStr) return "-"; - const date = new Date(timeStr); - return date.toLocaleString(); -}; - watch(() => props.modelValue, async (val) => { if (val && props.reminder && !props.viewOnly && props.reminder.is_read === 0) { loading.value = true; diff --git a/backend/src/utils/datetime.ts b/backend/src/utils/datetime.ts new file mode 100644 index 0000000..6cf5ba8 --- /dev/null +++ b/backend/src/utils/datetime.ts @@ -0,0 +1,55 @@ +/** + * 统一日期时间格式化工具(backend 端) + * + * 后端(Go)返回的时间字段为 RFC3339 / ISO 8601 字符串, + * 例如 "2026-03-31T23:34:07+08:00",直接渲染不友好。 + * 页面上显示时间统一使用本工具,输出: + * formatDateTime / formatTime -> 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss) + * formatDate / formatDateOnly -> 2026-03-31(YYYY-MM-DD) + */ + +const pad = (n: number) => String(n).padStart(2, "0"); + +function toDate(value: any): Date | null { + if (value === null || value === undefined || value === "") return null; + if (value instanceof Date) return isNaN(value.getTime()) ? null : value; + // 兼容秒级 / 毫秒级时间戳 + if (typeof value === "number") { + const d = new Date(value < 1e12 ? value * 1000 : value); + return isNaN(d.getTime()) ? null : d; + } + // "2026-03-31 23:34:07" 形式在部分浏览器解析异常,统一先替换为 ISO 形式 + const str = String(value).trim().replace(" ", "T"); + const d = new Date(str); + return isNaN(d.getTime()) ? null : d; +} + +/** 2026-03-31 23:34:07(YYYY-MM-DD HH:mm:ss);非法值返回 "-" */ +export function formatDateTime(value: any): string { + const d = toDate(value); + if (!d) return "-"; + return ( + `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` + + `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` + ); +} + +/** 与 formatDateTime 相同(兼容旧调用名) */ +export function formatTime(value: any): string { + return formatDateTime(value); +} + +/** 2026-03-31(YYYY-MM-DD);非法值返回 "-";纯日期串不做时区换算 */ +export function formatDate(value: any): string { + if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value.trim())) { + return value.trim(); + } + const d = toDate(value); + if (!d) return "-"; + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + +/** 与 formatDate 相同(兼容旧调用名) */ +export function formatDateOnly(value: any): string { + return formatDate(value); +} diff --git a/backend/src/views/apps/cms/article/index/components/preview.vue b/backend/src/views/apps/cms/article/index/components/preview.vue index ef5977a..66f5c6b 100644 --- a/backend/src/views/apps/cms/article/index/components/preview.vue +++ b/backend/src/views/apps/cms/article/index/components/preview.vue @@ -30,7 +30,7 @@ - 创建日期:{{ model?.create_time }} + 创建日期:{{ formatDateTime(model?.create_time) }} @@ -52,6 +52,7 @@ diff --git a/platform/src/views/basicSettings/sitereminder/index.vue b/platform/src/views/basicSettings/sitereminder/index.vue index 1750fc4..666cbb6 100644 --- a/platform/src/views/basicSettings/sitereminder/index.vue +++ b/platform/src/views/basicSettings/sitereminder/index.vue @@ -105,6 +105,7 @@ import { } from "@/api/sitereminder"; import MessageComposeDialog from "./components/edit.vue"; import MessageDetailDialog from "./components/detail.vue"; +import { formatTime } from "@/utils/datetime"; const loading = ref(false); const reminderList = ref([]); @@ -124,41 +125,65 @@ const currentReminder = ref({}); // 接收目标标签类型 const getTargetTagType = (type: string) => { switch (type) { + case "all": + return "danger"; case "platform": return "primary"; case "tenant_all": return "success"; - case "role": + case "tenant_role": return "warning"; case "tenant": - return "danger"; + return "warning"; + case "tenant_user": + return "info"; + case "role": + return "warning"; default: return "info"; } }; +// 解析接收目标多选ID数量(兼容数组 / JSON 字符串) +const getTargetIdsCount = (row: any) => { + const raw = row?.target_ids; + if (!raw) return 0; + if (Array.isArray(raw)) return raw.length; + if (typeof raw === "string") { + try { + const arr = JSON.parse(raw); + return Array.isArray(arr) ? arr.length : 0; + } catch { + return 0; + } + } + return 0; +}; + // 接收目标显示文字 const getTargetLabel = (row: any) => { + const count = getTargetIdsCount(row); switch (row.target_type) { + case "all": + return "全平台(所有人)"; case "platform": - return "平台端"; + return "平台端(所有人)"; case "tenant_all": - return "管理端 (所有租户)"; + return "租户端(所有租户)"; + case "tenant_role": + return count > 0 ? `租户角色(${count}个)` : "租户角色"; + case "tenant": + if (count > 0) return `租户(${count}个)`; + return row.target_tenant_id ? `租户(ID: ${row.target_tenant_id})` : "租户"; + case "tenant_user": + return count > 0 ? `租户用户(${count}人)` : "租户用户"; case "role": return `平台角色 (ID: ${row.target_role_id})`; - case "tenant": - return `特定租户 (ID: ${row.target_tenant_id})`; default: return row.target_type || "未知"; } }; -const formatTime = (timeStr: any) => { - if (!timeStr) return "-"; - const date = new Date(timeStr); - return date.toLocaleString(); -}; - // 获取已发送列表 const fetchList = async () => { loading.value = true; diff --git a/platform/src/views/basicSettings/tenants/components/detail.vue b/platform/src/views/basicSettings/tenants/components/detail.vue index 3fc0336..bc92dea 100644 --- a/platform/src/views/basicSettings/tenants/components/detail.vue +++ b/platform/src/views/basicSettings/tenants/components/detail.vue @@ -36,7 +36,7 @@ - {{ detailData.create_time || '-' }} + {{ detailData.create_time ? formatDateTime(detailData.create_time) : '-' }} @@ -68,6 +68,7 @@ import { ref } from "vue"; import { getTenantDetail } from "@/api/tenant"; import TenantUsersTab from "./TenantUsersTab.vue"; +import { formatDateTime } from "@/utils/datetime"; const visible = ref(false); const loading = ref(false); diff --git a/platform/src/views/basicSettings/tenants/domain.vue b/platform/src/views/basicSettings/tenants/domain.vue index 975cd1b..3cd2243 100644 --- a/platform/src/views/basicSettings/tenants/domain.vue +++ b/platform/src/views/basicSettings/tenants/domain.vue @@ -104,7 +104,9 @@ - + + +