/**
|
* 全站「时间只显示年月日」工具(需求 14)
|
* ---------------------------------------------------------------
|
* 客户口径:页面上所有时间都不显示时分秒,只保留 `YYYY-MM-DD`。
|
*
|
* 三种接入方式(覆盖全部显示路径,均**不影响数据、不影响导出/接口参数**):
|
* 1. 表格列:`main.js` 给 Element Plus 的 `el-table-column` 的 `formatter` 设了默认值
|
* → 所有「没有自定义插槽」的列自动只显示日期;
|
* 2. 共享表格组件:`PIMTable.vue` 的默认展示分支调用 `dateOnly()`(PIMTable 的列都带插槽,
|
* 拿不到上面的默认 formatter);
|
* 3. 其它任何插值(detail 弹窗、el-descriptions、span、slot 单元格…):
|
* 全局 mixin `dateOnlyTextMixin` 在组件挂载/更新后,把**纯日期时间文本节点**截断成日期。
|
*
|
* 只处理「日期 + 时分秒」这种完整形态的字符串:
|
* ✅ '2026-10-09 14:20:00' -> '2026-10-09' ✅ '2026-10-09T14:20:00.000+08:00' -> '2026-10-09'
|
* ⛔ '08:00:00'(用电时段/班次这类纯时间,没有年月日)保持原样
|
* ⛔ '2026-10-09'(本来就是日期)、'2026-10'、数字金额等保持原样
|
*
|
* 需要保留完整时间的局部,给元素加 `data-keep-time` 属性即可跳过(如:
|
* `<span data-keep-time>{{ row.createTime }}</span>`)。
|
*/
|
|
const DATE_PART = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?)(?:Z|[+-]\d{2}:?\d{2})?$/
|
const TEXT_NODE_RE = /^(\s*)(\d{4}-\d{2}-\d{2})[ T]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:\s*)$/
|
const HAS_DATETIME = /\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/
|
|
/** Date / 时间戳 → 'YYYY-MM-DD'(本地时区) */
|
function fromDateObject(d) {
|
const p = (n) => String(n).padStart(2, '0')
|
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
}
|
|
// 只把「像时间戳」的数字当年月日看待(2000-01-01 ~ 2100-01-01),
|
// 否则数量/金额/序号里的 0、1、123 会被误渲染成 1970-01-01
|
const TS_MS_MIN = 946684800000
|
const TS_MS_MAX = 4102444800000
|
const TS_S_MIN = 946684800
|
const TS_S_MAX = 4102444800
|
|
function fromTimestamp(n) {
|
let ms
|
if (Number.isInteger(n) && n >= TS_MS_MIN && n <= TS_MS_MAX) ms = n
|
else if (Number.isInteger(n) && n >= TS_S_MIN && n <= TS_S_MAX) ms = n * 1000
|
else return undefined
|
const d = new Date(ms)
|
return Number.isNaN(d.getTime()) ? undefined : fromDateObject(d)
|
}
|
|
/**
|
* 把「日期 + 时分秒」截断成「年月日」;其它值原样返回。
|
* 返回 null/undefined/'' 时也原样返回(保留页面上的 '-' / 空态处理)。
|
*/
|
export function dateOnly(value) {
|
if (value === null || value === undefined || value === '') return value
|
if (value instanceof Date) return Number.isNaN(value.getTime()) ? value : fromDateObject(value)
|
if (typeof value === 'number') return fromTimestamp(value) ?? value
|
if (typeof value !== 'string') return value
|
const m = DATE_PART.exec(value.trim())
|
return m ? m[1] : value
|
}
|
|
/** el-table-column 的默认 formatter(无自定义插槽的列生效) */
|
export function dateOnlyFormatter(row, column, cellValue) {
|
const v = dateOnly(cellValue)
|
return v === null || v === undefined ? '' : String(v)
|
}
|
|
/** 把一个文本节点截断成日期;返回是否改动 */
|
function truncateTextNode(node) {
|
const m = TEXT_NODE_RE.exec(node.nodeValue || '')
|
if (!m) return false
|
node.nodeValue = `${m[1]}${m[2]}`
|
return true
|
}
|
|
function walk(el) {
|
if (!el || el.nodeType !== 1) return
|
// 快速判断:整棵子树没有「日期+时分」就不用逐个文本节点看了
|
if (!HAS_DATETIME.test(el.textContent || '')) return
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT)
|
let n = walker.nextNode()
|
while (n) {
|
const next = walker.nextNode()
|
if (n.parentElement && !n.parentElement.closest('[data-keep-time]')) truncateTextNode(n)
|
n = next
|
}
|
}
|
|
/**
|
* 全局 mixin:组件挂载/更新后把纯日期时间文本截断成日期。
|
* 只改 DOM 文本,不动响应式数据 —— 表单回显、日期选择器、导出、接口参数都不受影响。
|
*/
|
export const dateOnlyTextMixin = {
|
mounted() {
|
walk(this.$el)
|
},
|
updated() {
|
walk(this.$el)
|
},
|
}
|