/** * 全站「时间只显示年月日」工具(需求 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` 属性即可跳过(如: * `{{ row.createTime }}`)。 */ 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) }, }