yuan
4 天以前 79548559809eec7c4fb8c666314605d688dffeb4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
 * 全站「时间只显示年月日」工具(需求 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)
  },
}