feat: 日期时间国际化 - formatDate/formatDateTime 统一 YYYY-MM-DD HH:mm:ss 格式,修复7个详情页 UTC 原始时间串显示,AntdDemo 增加日期时间演示

This commit is contained in:
bootx
2026-06-13 10:37:46 +08:00
parent 63da01e68e
commit f9acfbe965
28 changed files with 156 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
# 端口号
VITE_PORT=5999
VITE_PORT=6999
VITE_BASE=/

View File

@@ -5,8 +5,8 @@ import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antdv-next';
import { useTitle } from '@vueuse/core';
import { formatDate, formatDateTime } from '@vben/utils';
import Antd from 'antdv-next';
import VxeUI from 'vxe-pc-ui';
import VxeUITable from 'vxe-table';
@@ -40,6 +40,18 @@ async function bootstrap(namespace: string) {
// 注册 VxeUI
app.use(VxeUI);
// 注册全局 vxe-table 日期格式化器, 使 formatter="formatDateTime" 生效
VxeUI.formats.add('formatDate', {
tableCellFormatMethod({ cellValue }) {
return formatDate(cellValue);
},
});
VxeUI.formats.add('formatDateTime', {
tableCellFormatMethod({ cellValue }) {
return formatDateTime(cellValue);
},
});
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称也可以明确提供false表示不注册这个指令

View File

@@ -4,6 +4,7 @@
import { computed } from 'vue';
import { $t } from '@vben/locales';
import { formatDate } from '@vben/utils';
import { BOOLEAN, DATE, DATE_RANGE, DATE_TIME, DATE_TIME_RANGE, LIST, NUMBER, STRING, TIME } from './query';
@@ -130,13 +131,6 @@
},
];
function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function query() {
emits('enterQuery');
}

View File

@@ -1,7 +1,6 @@
{
"title": "Antd Next Demo",
"description": "Support multi-language, theme switching and more",
"button": "Button",
"message": "Message",
"notification": "Notification",
"info": "Info",
@@ -13,5 +12,12 @@
"confirmContent": "This is a confirm dialog triggered by useMessage.",
"confirmed": "Confirmed",
"dialog": "Dialog",
"openConfirm": "Open Confirm Dialog"
"openConfirm": "Open Confirm Dialog",
"currentTime": "Current Time",
"formatDateDemo": "Date Formatting",
"formatDateTimeDemo": "Date Time Formatting",
"inputDemo": "Time Input (with Echo)",
"datePicker": "Date Picker",
"timePicker": "Time Picker",
"dateTimePicker": "Date Time Picker"
}

View File

@@ -1,7 +1,6 @@
{
"title": "Antd Next演示",
"description": "支持多语言,主题功能集成切换等",
"button": "按钮",
"message": "Message",
"notification": "Notification",
"info": "信息",
@@ -13,5 +12,12 @@
"confirmContent": "这是一个通过 useMessage 调起的确认框示例。",
"confirmed": "已确认",
"dialog": "Dialog",
"openConfirm": "打开确认框"
"openConfirm": "打开确认框",
"currentTime": "当前时间",
"formatDateDemo": "日期格式化",
"formatDateTimeDemo": "日期时间格式化",
"inputDemo": "时间输入组件(含回显)",
"datePicker": "日期选择",
"timePicker": "时间选择",
"dateTimePicker": "日期时间选择"
}

View File

@@ -40,6 +40,6 @@ export const overridesPreferences = defineOverridesPreferences({
colorPrimary: 'hsl(212 100% 45%)',
},
widget: {
timezone: false,
timezone: true,
},
});

View File

@@ -1,6 +1,9 @@
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { formatDate, formatDateTime } from '@vben/utils';
import { useMessage } from '#/hooks/useMessage';
@@ -8,6 +11,20 @@
const { confirm, message, notification } = useMessage();
// 当前时间(每秒更新)
const now = ref(new Date());
let timer: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
timer = setInterval(() => { now.value = new Date(); }, 1000);
});
onBeforeUnmount(() => { if (timer) clearInterval(timer); });
// 日期选择器值
const selectedDate = ref<string>('');
const selectedTime = ref<string>('');
const selectedDateTime = ref<string>('');
function info() {
message.info('How many roads must a man walk down');
}
@@ -55,15 +72,62 @@
<template>
<!-- 国际化支持多语言主题功能集成切换等 -->
<Page :description="$t('demos.antd.description')" :title="$t('demos.antd.title')">
<!-- 国际化按钮 -->
<a-card class="mb-5" :title="$t('demos.antd.button')">
<!-- 国际化当前时间 -->
<a-card class="mb-5" :title="$t('demos.antd.currentTime')">
<a-typography-title :level="4" class="font-mono">
{{ formatDateTime(now) }}
</a-typography-title>
<a-space>
<a-button>Default</a-button>
<a-button type="primary"> Primary </a-button>
<a-button> Info </a-button>
<a-button danger> Error </a-button>
<a-tag>{{ formatDate(now) }}</a-tag>
<a-tag>{{ $t('common.createTime') }}: {{ formatDateTime(now) }}</a-tag>
</a-space>
</a-card>
<!-- 国际化日期格式化 -->
<a-card class="mb-5" :title="$t('demos.antd.formatDateDemo')">
<a-descriptions :column="2" size="small" bordered>
<a-descriptions-item :label="$t('demos.antd.formatDateDemo')">
{{ formatDate('2026-06-13T08:30:00Z') }}
</a-descriptions-item>
<a-descriptions-item :label="$t('demos.antd.formatDateTimeDemo')">
{{ formatDateTime('2026-06-13T08:30:00Z') }}
</a-descriptions-item>
<a-descriptions-item :label="$t('demos.antd.formatDateDemo')">
{{ formatDate('2026-06-13T16:30:00+08:00') }}
</a-descriptions-item>
<a-descriptions-item :label="$t('demos.antd.formatDateTimeDemo')">
{{ formatDateTime('2026-06-13T16:30:00+08:00') }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 国际化时间输入组件 -->
<a-card class="mb-5" :title="$t('demos.antd.inputDemo')">
<a-form layout="inline">
<a-form-item :label="$t('demos.antd.datePicker')">
<a-date-picker
v-model:value="selectedDate"
:placeholder="$t('demos.antd.datePicker')"
value-format="YYYY-MM-DD"
/>
</a-form-item>
<a-form-item :label="$t('demos.antd.timePicker')">
<a-time-picker
v-model:value="selectedTime"
:placeholder="$t('demos.antd.timePicker')"
value-format="HH:mm:ss"
/>
</a-form-item>
<a-form-item :label="$t('demos.antd.dateTimePicker')">
<a-date-picker
v-model:value="selectedDateTime"
:placeholder="$t('demos.antd.dateTimePicker')"
show-time
value-format="YYYY-MM-DD HH:mm:ss"
/>
</a-form-item>
</a-form>
</a-card>
<!-- 国际化Message -->
<a-card class="mb-5" :title="$t('demos.antd.message')">
<a-space>

View File

@@ -236,7 +236,7 @@
<!-- 备注 -->
<vxe-column field="remark" :title="$t('iam.role.remark')" :min-width="150" />
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('common.createTime')" :min-width="160" />
<vxe-column field="createTime" :title="$t('common.createTime')" :min-width="160" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="180" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -482,7 +482,7 @@
</template>
</vxe-column>
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('common.createTime')" min-width="160" />
<vxe-column field="createTime" :title="$t('common.createTime')" min-width="160" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="180" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue';
import { $t, i18n } from '@vben/locales';
import { formatDateTime } from '@vben/utils';
import { type User, UserApi, type UserRole, UserRoleApi } from '#/api/iam/user.api';
import { clientCodeColorMap, clientCodeI18nMap } from '#/enums/clientCode';
@@ -142,11 +143,11 @@
</a-descriptions-item>
<!-- 创建时间 -->
<a-descriptions-item :label="$t('common.createTime')">
{{ userInfo.createTime }}
{{ formatDateTime(userInfo.createTime) }}
</a-descriptions-item>
<!-- 最后登录时间 -->
<a-descriptions-item :label="$t('iam.user.field.lastLoginTime')">
{{ userInfo.lastLoginTime || '-' }}
{{ formatDateTime(userInfo.lastLoginTime) || '-' }}
</a-descriptions-item>
</a-descriptions>
</a-spin>

View File

@@ -173,7 +173,7 @@
</template>
</vxe-column>
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('payment.isv.base.field.createTime')" :min-width="180" />
<vxe-column field="createTime" :title="$t('payment.isv.base.field.createTime')" :min-width="180" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="100" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -393,7 +393,7 @@
</a-tag>
</template>
</vxe-column>
<vxe-column field="createTime" :title="$t('common.createTime')" min-width="160" />
<vxe-column field="createTime" :title="$t('common.createTime')" min-width="160" formatter="formatDateTime" />
<vxe-column fixed="right" width="180" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">
<a-space :size="2">

View File

@@ -155,7 +155,7 @@
<a-tag v-else color="red">{{ $t('payment.merchant.base.status.disabled') }}</a-tag>
</template>
</vxe-column>
<vxe-column field="createTime" :title="$t('payment.merchant.base.field.createTime')" :min-width="180" />
<vxe-column field="createTime" :title="$t('payment.merchant.base.field.createTime')" :min-width="180" formatter="formatDateTime" />
<vxe-column fixed="right" width="100" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">
<a

View File

@@ -394,7 +394,7 @@
</a-tag>
</template>
</vxe-column>
<vxe-column field="createTime" :title="$t('common.createTime')" min-width="160" />
<vxe-column field="createTime" :title="$t('common.createTime')" min-width="160" formatter="formatDateTime" />
<vxe-column fixed="right" width="180" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">
<a-space :size="2">

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue';
import { $t } from '@vben/locales';
import { formatDateTime } from '@vben/utils';
import { type MerchantUserDetail, MerchantUserApi } from '#/api/payment/merchantUser.api';
import { useMessage } from '#/hooks/useMessage';
@@ -114,11 +115,11 @@
</a-descriptions-item>
<!-- 注册时间 -->
<a-descriptions-item :label="$t('iam.user.field.registerTime')">
{{ userInfo.createTime || '-' }}
{{ formatDateTime(userInfo.createTime) || '-' }}
</a-descriptions-item>
<!-- 最后登录时间 -->
<a-descriptions-item :label="$t('iam.user.field.lastLoginTime')">
{{ userInfo.lastLoginTime || '-' }}
{{ formatDateTime(userInfo.lastLoginTime) || '-' }}
</a-descriptions-item>
<!-- 登录次数 -->
<a-descriptions-item :label="$t('iam.user.field.loginCount')">

View File

@@ -228,7 +228,7 @@
</template>
</vxe-column>
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('risk.modelInstance.base.field.createTime')" :min-width="170" />
<vxe-column field="createTime" :title="$t('risk.modelInstance.base.field.createTime')" :min-width="170" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="180" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -175,7 +175,7 @@
<!-- 备注 -->
<vxe-column field="remark" :title="$t('system.dict.remark')" :min-width="150" />
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('system.dict.createTime')" :min-width="160" />
<vxe-column field="createTime" :title="$t('system.dict.createTime')" :min-width="160" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="150" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -191,7 +191,7 @@
<!-- 备注 -->
<vxe-column field="remark" :title="$t('system.dict.remark')" :min-width="150" />
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('system.dict.createTime')" :min-width="160" />
<vxe-column field="createTime" :title="$t('system.dict.createTime')" :min-width="160" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="200" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue';
import { $t } from '@vben/locales';
import { formatDateTime } from '@vben/utils';
import { type PlatformFile, PlatformFileApi } from '#/api/system/platform-file.api';
import { useApiPrefix } from '#/hooks/useApiPrefix';
@@ -116,7 +117,7 @@
</a-descriptions-item>
<!-- 创建时间 -->
<a-descriptions-item :label="$t('system.file.platform.field.createTime')">
{{ data.createTime }}
{{ formatDateTime(data.createTime) }}
</a-descriptions-item>
</a-descriptions>
</a-spin>

View File

@@ -205,7 +205,7 @@
</template>
</vxe-column>
<!-- 创建时间 -->
<vxe-column field="createTime" :title="$t('system.file.platform.field.createTime')" :min-width="180" />
<vxe-column field="createTime" :title="$t('system.file.platform.field.createTime')" :min-width="180" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" :width="160" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue';
import { $t } from '@vben/locales';
import { formatDateTime } from '@vben/utils';
import { type LoginLog, LoginLogApi } from '#/api/system/log/login-log.api';
@@ -77,7 +78,7 @@
</a-descriptions-item>
<!-- 登录时间 -->
<a-descriptions-item :label="$t('system.log.login-log.loginTime')">
{{ data.loginTime }}
{{ formatDateTime(data.loginTime) }}
</a-descriptions-item>
</a-descriptions>
</a-spin>

View File

@@ -201,7 +201,7 @@
<!-- 提示消息 -->
<vxe-column field="msg" :title="$t('system.log.login-log.msg')" :min-width="170" />
<!-- 访问时间 -->
<vxe-column field="loginTime" :title="$t('system.log.login-log.loginTime')" :min-width="170" />
<vxe-column field="loginTime" :title="$t('system.log.login-log.loginTime')" :min-width="170" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="60" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue';
import { $t } from '@vben/locales';
import { formatDateTime } from '@vben/utils';
import { type OperateLog, OperateLogApi } from '#/api/system/log/operate-log.api';
@@ -101,7 +102,7 @@
</a-descriptions-item>
<!-- 操作时间 -->
<a-descriptions-item :label="$t('system.log.operate-log.operateTime')" :span="2">
{{ data.operateTime }}
{{ formatDateTime(data.operateTime) }}
</a-descriptions-item>
</a-descriptions>
</a-spin>

View File

@@ -208,7 +208,7 @@
<!-- 错误提示 -->
<vxe-column field="errorMsg" :title="$t('system.log.operate-log.errorMsg')" :min-width="150" />
<!-- 操作时间 -->
<vxe-column field="operateTime" :title="$t('system.log.operate-log.operateTime')" :min-width="170" />
<vxe-column field="operateTime" :title="$t('system.log.operate-log.operateTime')" :min-width="170" formatter="formatDateTime" />
<!-- 操作 -->
<vxe-column fixed="right" width="60" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -228,7 +228,12 @@
</template>
</vxe-column>
<!-- 登录时间 -->
<vxe-column field="loginTime" :title="$t('system.monitor.online-user.loginTime')" min-width="160" />
<vxe-column
field="loginTime"
:title="$t('system.monitor.online-user.loginTime')"
min-width="160"
formatter="formatDateTime"
/>
<!-- 操作 -->
<vxe-column fixed="right" width="100" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">

View File

@@ -10,7 +10,7 @@ export default defineConfig(async () => {
'/api': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
target: 'http://localhost:12121',
target: 'http://localhost:9999',
ws: true,
},
},

View File

@@ -19,12 +19,32 @@ type Format =
| 'YYYY-MM-DD HH:mm:ss'
| (string & {});
/**
* 格式化日期时间, 支持多种输入类型, 自动按用户时区转换显示
* 后端返回的 UTC 时间 (如 "2026-06-13T08:30:00Z") 会被解析为 UTC 时间,
* 然后通过 dayjs.tz() 转换为用户选中的时区后格式化输出
* @param time 日期时间, 支持 Date、dayjs 对象、时间戳 (number)、字符串
* @param format 输出格式, 默认 "YYYY-MM-DD"
* @returns 格式化后的日期字符串
*/
export function formatDate(time?: FormatDate, format: Format = 'YYYY-MM-DD') {
try {
const date = dayjs.isDayjs(time) ? time : dayjs(time);
let date: dayjs.Dayjs;
// 已经是 dayjs 对象, 直接复用
if (dayjs.isDayjs(time)) {
date = time;
// ISO 8601 格式字符串 (末尾带 Z、含偏移 + 或 T 标记), 以 UTC 方式解析
} else if (typeof time === 'string' && (time.endsWith('Z') || time.includes('+') || time.includes('T'))) {
date = dayjs.utc(time);
// 其他格式 (Date、时间戳、普通日期字符串等), 由 dayjs 默认解析
} else {
date = dayjs(time);
}
// 日期无效时抛出异常, 进入 catch 返回原始值
if (!date.isValid()) {
throw new Error('Invalid date');
}
// 按用户时区转换后格式化
return date.tz().format(format);
} catch (error) {
console.error(`Error formatting date: ${error}`);

View File

@@ -1,4 +1,6 @@
// @ts-ignore
import Vue from '@vitejs/plugin-vue';
// @ts-ignore
import VueJsx from '@vitejs/plugin-vue-jsx';
import { configDefaults, defineConfig } from 'vitest/config';