Vue3 + Element Plus 项目中日期时间处理的最佳实践与数据库设计规范
前言
在开发企业级管理系统过程中,遇到了一个看似简单但实际复杂的问题:日期时间处理。不同模块使用不同的日期格式,前后端交互时出现格式不匹配,数据库设计也不够规范。经过深入分析和实践,总结出了一套完整的解决方案。
🚨 问题分析
1. 前端日期格式不统一
在我们的项目中,不同模块使用了不同的日期格式:
vue
<!-- 订单管理模块 -->
<el-date-picker
v-model="formData.orderDate"
type="date"
value-format="YYYY-MM-DD"
/>
<!-- 发货管理模块 -->
<el-date-picker
v-model="queryParams.shippingDate"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
/>
<!-- 用户管理模块 -->
<el-date-picker
v-model="formData.registerDate"
type="date"
value-format="x" <!-- 时间戳格式 -->
/>
2. 表格显示格式混乱
vue
<!-- 不同表格使用不同的格式化函数 -->
<el-table-column
label="订单日期"
:formatter="dateFormatter2" <!-- YYYY-MM-DD -->
/>
<el-table-column
label="创建时间"
:formatter="dateFormatter" <!-- YYYY-MM-DD HH:mm:ss -->
/>
3. 前后端交互问题
typescript
// 前端发送格式
const data = {
orderDate: "2024-01-15", // yyyy-MM-dd
deliveryDate: "2024-01-15 14:30:00" // yyyy-MM-dd HH:mm:ss
}
// 后端期望格式不一致,导致解析错误
🎯 解决方案
1. 统一日期格式标准
首先,建立统一的日期格式标准:
typescript
// utils/dateConstants.ts
export const DATE_FORMATS = {
DATE_ONLY: 'YYYY-MM-DD', // 仅日期
DATETIME: 'YYYY-MM-DD HH:mm:ss', // 日期时间
TIMESTAMP: 'x', // 时间戳
ISO: 'YYYY-MM-DDTHH:mm:ss.SSSZ' // ISO格式
}
// 使用场景定义
export const DATE_USAGE = {
// 业务日期字段(只需要日期)
BUSINESS_DATE: DATE_FORMATS.DATE_ONLY,
// 系统时间字段(需要精确到秒)
SYSTEM_TIME: DATE_FORMATS.DATETIME,
// 时间戳字段
TIMESTAMP: DATE_FORMATS.TIMESTAMP
}
2. 创建统一的日期处理工具
typescript
// utils/dateHandler.ts
import dayjs from 'dayjs'
import { DATE_FORMATS } from './dateConstants'
export class DateHandler {
/**
* 统一日期格式化
* @param date 日期值
* @param format 格式类型
* @returns 格式化后的字符串
*/
static formatDate(date: any, format: string = DATE_FORMATS.DATE_ONLY): string {
if (!date) return ''
return dayjs(date).format(format)
}
/**
* 统一日期范围处理
* @param dates 日期范围数组
* @param format 格式类型
* @returns 格式化后的日期范围
*/
static formatDateRange(dates: [string, string], format: string = DATE_FORMATS.DATE_ONLY): [string, string] {
if (!dates || dates.length !== 2) return ['', '']
return [dayjs(dates[0]).format(format), dayjs(dates[1]).format(format)]
}
/**
* 统一日期验证
* @param date 日期值
* @returns 是否有效
*/
static isValidDate(date: any): boolean {
return dayjs(date).isValid()
}
/**
* 获取日期范围查询参数
* @param dateRange 日期范围
* @returns 查询参数
*/
static getDateRangeQuery(dateRange: [string, string]): { startTime: string, endTime: string } {
if (!dateRange || dateRange.length !== 2) {
return { startTime: '', endTime: '' }
}
return {
startTime: dayjs(dateRange[0]).startOf('day').format(DATE_FORMATS.DATETIME),
endTime: dayjs(dateRange[1]).endOf('day').format(DATE_FORMATS.DATETIME)
}
}
}
3. 统一表格日期格式化
typescript
// utils/tableFormatters.ts
import { DateHandler } from './dateHandler'
import { DATE_FORMATS } from './dateConstants'
export const tableDateFormatters = {
/**
* 仅显示日期
*/
dateOnly: (row: any, column: any, cellValue: any): string => {
return DateHandler.formatDate(cellValue, DATE_FORMATS.DATE_ONLY) || '-'
},
/**
* 显示日期时间
*/
dateTime: (row: any, column: any, cellValue: any): string => {
return DateHandler.formatDate(cellValue, DATE_FORMATS.DATETIME) || '-'
},
/**
* 相对时间显示
*/
relative: (row: any, column: any, cellValue: any): string => {
if (!cellValue) return '-'
return dayjs(cellValue).fromNow()
}
}
4. 统一日期选择器配置
vue
<!-- components/DatePicker/index.vue -->
<template>
<div class="date-picker-wrapper">
<!-- 仅日期选择 -->
<el-date-picker
v-if="type === 'date'"
v-model="modelValue"
type="date"
:value-format="DATE_FORMATS.DATE_ONLY"
:placeholder="placeholder"
:disabled="disabled"
@change="handleChange"
/>
<!-- 日期时间选择 -->
<el-date-picker
v-else-if="type === 'datetime'"
v-model="modelValue"
type="datetime"
:value-format="DATE_FORMATS.DATETIME"
:placeholder="placeholder"
:disabled="disabled"
@change="handleChange"
/>
<!-- 日期范围选择 -->
<el-date-picker
v-else-if="type === 'daterange'"
v-model="modelValue"
type="daterange"
:value-format="DATE_FORMATS.DATE_ONLY"
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
:disabled="disabled"
@change="handleChange"
/>
</div>
</template>
<script setup lang="ts">
import { DATE_FORMATS } from '@/utils/dateConstants'
interface Props {
modelValue: string | [string, string]
type: 'date' | 'datetime' | 'daterange'
placeholder?: string
startPlaceholder?: string
endPlaceholder?: string
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择日期',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期',
disabled: false
})
const emit = defineEmits<{
'update:modelValue': [value: string | [string, string]]
'change': [value: string | [string, string]]
}>()
const handleChange = (value: string | [string, string]) => {
emit('update:modelValue', value)
emit('change', value)
}
</script>
🗄️ 数据库设计规范
1. 时间字段类型选择
sql
-- ✅ 推荐:系统字段使用 timestamp(6)
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- ✅ 推荐:业务时间字段使用 timestamp(6)
"delivery_date" timestamp(6),
"shipping_date" timestamp(6),
"processing_time" timestamp(6),
-- ✅ 推荐:纯日期业务字段使用 date
"order_date" date,
"effective_date" date,
"expire_date" date,
2. 完整的表结构设计
sql
-- 订单表
CREATE TABLE "sys_order" (
"id" int8 NOT NULL DEFAULT nextval('sys_order_id_seq'::regclass),
"order_no" varchar(50) NOT NULL,
"order_name" varchar(100) NOT NULL,
"order_type" int2 NOT NULL,
"status" int2 NOT NULL DEFAULT 0,
-- 业务日期字段(只需要日期)
"order_date" date,
"effective_date" date,
"expire_date" date,
-- 业务金额字段
"order_amount" numeric(15,2) DEFAULT 0,
"paid_amount" numeric(15,2) DEFAULT 0,
"unpaid_amount" numeric(15,2) DEFAULT 0,
-- 系统字段
"creator" varchar(64) DEFAULT '',
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bool NOT NULL DEFAULT false,
"tenant_id" int8 NOT NULL DEFAULT 0,
CONSTRAINT "sys_order_pkey" PRIMARY KEY ("id")
);
-- 发货记录表
CREATE TABLE "sys_shipping_record" (
"id" int8 NOT NULL DEFAULT nextval('sys_shipping_record_id_seq'::regclass),
"order_id" int8 NOT NULL,
"shipping_no" varchar(50) NOT NULL,
-- 业务时间字段(需要精确到秒)
"shipping_date" timestamp(6) NOT NULL,
"delivery_date" timestamp(6),
"arrival_date" timestamp(6),
-- 业务数据字段
"shipping_quantity" int4 DEFAULT 0,
"logistics_company" varchar(100),
"logistics_no" varchar(100),
-- 系统字段
"creator" varchar(64) DEFAULT '',
"create_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bool NOT NULL DEFAULT false,
"tenant_id" int8 NOT NULL DEFAULT 0,
CONSTRAINT "sys_shipping_record_pkey" PRIMARY KEY ("id")
);
3. 索引优化
sql
-- 为常用查询字段添加索引
CREATE INDEX "idx_sys_order_order_date" ON "sys_order" ("order_date");
CREATE INDEX "idx_sys_order_create_time" ON "sys_order" ("create_time");
CREATE INDEX "idx_sys_order_status" ON "sys_order" ("status");
CREATE INDEX "idx_sys_shipping_shipping_date" ON "sys_shipping_record" ("shipping_date");
CREATE INDEX "idx_sys_shipping_order_id" ON "sys_shipping_record" ("order_id");
后端Java实体类设计
1. 实体类映射
java
// Order.java
@Data
@TableName("sys_order")
public class OrderDO {
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private String orderName;
private Integer orderType;
private Integer status;
// 业务日期字段 - 对应数据库 date 类型
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate orderDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate effectiveDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate expireDate;
// 业务金额字段
private BigDecimal orderAmount;
private BigDecimal paidAmount;
private BigDecimal unpaidAmount;
// 系统字段 - 对应数据库 timestamp(6) 类型
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private String creator;
private String updater;
private Boolean deleted;
private Long tenantId;
}
// ShippingRecord.java
@Data
@TableName("sys_shipping_record")
public class ShippingRecordDO {
@TableId(type = IdType.AUTO)
private Long id;
private Long orderId;
private String shippingNo;
// 业务时间字段 - 对应数据库 timestamp(6) 类型
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime shippingDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime deliveryDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime arrivalDate;
// 业务数据字段
private Integer shippingQuantity;
private String logisticsCompany;
private String logisticsNo;
// 系统字段
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private String creator;
private String updater;
private Boolean deleted;
private Long tenantId;
}
2. VO类设计
java
// OrderVO.java
@Data
public class OrderVO {
private Long id;
private String orderNo;
private String orderName;
private Integer orderType;
private Integer status;
// 业务日期字段
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate orderDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate effectiveDate;
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate expireDate;
// 业务金额字段
private BigDecimal orderAmount;
private BigDecimal paidAmount;
private BigDecimal unpaidAmount;
// 系统字段
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
private String creatorName;
private String updaterName;
}
🚀 实际应用示例
1. 订单表单组件
vue
<!-- OrderForm.vue -->
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1400px">
<el-form ref="formRef" :model="formData" :rules="formRules">
<!-- 订单基本信息 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="订单日期" prop="orderDate">
<DatePicker
v-model="formData.orderDate"
type="date"
placeholder="请选择订单日期"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="到期日期" prop="expireDate">
<DatePicker
v-model="formData.expireDate"
type="date"
placeholder="请选择到期日期"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</Dialog>
</template>
<script setup lang="ts">
import { DateHandler } from '@/utils/dateHandler'
import { DATE_FORMATS } from '@/utils/dateConstants'
const formData = ref({
orderDate: '',
expireDate: '',
// ... 其他字段
})
// 提交表单时的数据处理
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 处理日期字段
const data = {
...formData.value,
// 确保日期格式正确
orderDate: DateHandler.formatDate(formData.value.orderDate, DATE_FORMATS.DATE_ONLY),
expireDate: DateHandler.formatDate(formData.value.expireDate, DATE_FORMATS.DATE_ONLY),
}
// 提交到后端
if (formType.value === 'create') {
await OrderApi.createOrder(data)
} else {
await OrderApi.updateOrder(data)
}
}
</script>
2. 列表查询组件
vue
<!-- OrderList.vue -->
<template>
<ContentWrap>
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
<el-form-item label="订单日期" prop="orderDate">
<DatePicker
v-model="queryParams.orderDate"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
</el-form>
<el-table :data="list" :stripe="true">
<el-table-column
label="订单日期"
prop="orderDate"
:formatter="tableDateFormatters.dateOnly"
/>
<el-table-column
label="创建时间"
prop="createTime"
:formatter="tableDateFormatters.dateTime"
/>
</el-table>
</ContentWrap>
</template>
<script setup lang="ts">
import { DateHandler } from '@/utils/dateHandler'
import { tableDateFormatters } from '@/utils/tableFormatters'
const queryParams = reactive({
orderDate: [] as [string, string],
// ... 其他查询参数
})
// 查询列表
const getList = async () => {
const params = {
...queryParams,
// 处理日期范围查询
...DateHandler.getDateRangeQuery(queryParams.orderDate)
}
const data = await OrderApi.getOrderPage(params)
list.value = data.list || []
total.value = data.total || 0
}
</script>
📊 数据迁移方案
1. 现有数据迁移脚本
sql
-- 迁移订单表的订单日期字段
-- 步骤1:添加新字段
ALTER TABLE "sys_order"
ADD COLUMN "order_date_new" date;
-- 步骤2:迁移数据
UPDATE "sys_order"
SET "order_date_new" = "order_date"::date
WHERE "order_date" IS NOT NULL AND "order_date" != '';
-- 步骤3:删除旧字段
ALTER TABLE "sys_order"
DROP COLUMN "order_date";
-- 步骤4:重命名新字段
ALTER TABLE "sys_order"
RENAME COLUMN "order_date_new" TO "order_date";
-- 步骤5:添加约束
ALTER TABLE "sys_order"
ALTER COLUMN "order_date" SET NOT NULL;
2. 兼容性处理
java
// 在实体类中添加兼容性处理
@Data
public class OrderDO {
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate orderDate;
// 兼容性方法:处理历史数据
@JsonSetter
public void setOrderDate(String dateStr) {
if (StringUtils.isNotBlank(dateStr)) {
try {
this.orderDate = LocalDate.parse(dateStr);
} catch (Exception e) {
// 处理格式不正确的历史数据
log.warn("Invalid date format: {}", dateStr);
this.orderDate = null;
}
}
}
}
⚡ 性能优化建议
1. 查询优化
sql
-- 使用日期范围查询时,确保索引生效
SELECT * FROM sys_order
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'
AND status = 1;
-- 时间戳查询优化
SELECT * FROM sys_shipping_record
WHERE shipping_date >= '2024-01-01 00:00:00'
AND shipping_date <= '2024-12-31 23:59:59'
AND order_id = 123;
2. 分区表设计
sql
-- 对于大数据量的时间相关表,考虑按时间分区
CREATE TABLE sys_shipping_record_2024 PARTITION OF sys_shipping_record
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE sys_shipping_record_2025 PARTITION OF sys_shipping_record
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
🎯 实施效果
1. 开发效率提升
- 统一标准:所有开发者使用相同的日期处理规范
- 减少错误:避免格式不匹配导致的问题
- 代码复用:统一的工具类和组件
2. 用户体验改善
- 界面一致:所有页面的日期选择器行为一致
- 数据准确:避免时区转换导致的时间错误
- 查询高效:优化的索引和查询语句
3. 维护成本降低
- 代码清晰:统一的命名和格式规范
- 问题定位:标准化的错误处理
- 扩展容易:新功能可以复用现有规范
🔮 未来改进方向
1. 技术升级
- 时区支持:考虑多时区业务场景
- 国际化:支持不同地区的日期格式
- 性能优化:大数据量下的查询优化
2. 功能扩展
- 日期计算:业务日期的自动计算
- 提醒功能:基于日期的业务提醒
- 报表统计:时间维度的数据分析
总结
通过建立统一的日期处理规范,我们解决了以下问题:
- 格式不统一:建立了标准化的日期格式
- 交互问题:规范了前后端数据交互
- 数据库设计:优化了时间字段的类型选择
- 开发效率:提供了可复用的工具和组件
这套方案不仅解决了当前的问题,也为未来的功能扩展打下了良好的基础。建议在项目中逐步实施,先从核心模块开始,然后推广到整个系统。