@epxing/utils 0.0.0-stage → 5.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/API.md +602 -0
  2. package/README.md +22 -2
  3. package/dist/_vendor/.pnpm/jsencrypt@3.5.4/_vendor/jsencrypt/bin/jsencrypt.min.js +1691 -0
  4. package/dist/_virtual/_commonjsHelpers.js +6 -0
  5. package/dist/_virtual/jsencrypt.min.js +31 -0
  6. package/dist/_virtual/jsencrypt.min2.js +4 -0
  7. package/dist/_virtual/preload-helper.js +88 -0
  8. package/dist/assets/ai-BBFn3QRy.png +0 -0
  9. package/dist/assets/ai_import-BxQ7Ohn6.png +0 -0
  10. package/dist/assets/app-DBNIokDP.png +0 -0
  11. package/dist/assets/approval-DGcBv-re.png +0 -0
  12. package/dist/assets/create_market_system-BH__UVbt.png +0 -0
  13. package/dist/assets/create_system-irg--ePh.png +0 -0
  14. package/dist/assets/custom-C-ujlHz1.png +0 -0
  15. package/dist/assets/default-icon-2193FMjv.png +0 -0
  16. package/dist/assets/ex_link-4fYPd9nh.png +0 -0
  17. package/dist/assets/excel-BYqRm2Y8.png +0 -0
  18. package/dist/assets/folder-CFVsNbjj.png +0 -0
  19. package/dist/assets/form-DD9HV-jN.png +0 -0
  20. package/dist/assets/import-tfKWbWVS.png +0 -0
  21. package/dist/assets/meta-Qorpi70B.png +0 -0
  22. package/dist/assets/module-DQrs7VAS.png +0 -0
  23. package/dist/assets/null-B2An29h4.png +0 -0
  24. package/dist/assets/option-CG02tIgh.svg +1 -0
  25. package/dist/assets/photo-uscRzRXF.png +0 -0
  26. package/dist/assets/process-Dcndu9m2.png +0 -0
  27. package/dist/assets/rule-DcYhYse0.png +0 -0
  28. package/dist/assets/service-CLYLpPl3.png +0 -0
  29. package/dist/assets/single_file-B3TJtJli.png +0 -0
  30. package/dist/assets/snapshot-history-BsVY9cVj.png +0 -0
  31. package/dist/assets/snapshot-manual-DBRfFHYI.png +0 -0
  32. package/dist/assets/snapshot-realtime-DjHnJI6C.png +0 -0
  33. package/dist/assets/staff-Bn_-_00l.png +0 -0
  34. package/dist/assets/sync_append-Bm8mtvZs.png +0 -0
  35. package/dist/assets/sync_append_update-DzjxqJ_U.png +0 -0
  36. package/dist/assets/sync_loading-D7LoNPFP.png +0 -0
  37. package/dist/assets/sync_replace-Cbbzz327.png +0 -0
  38. package/dist/assets/sync_update-Birao6Cr.png +0 -0
  39. package/dist/assets/userRole-D1LAdM6N.png +0 -0
  40. package/dist/assets/user_bg-BFHtaZjz.png +0 -0
  41. package/dist/src/Config.js +25 -0
  42. package/dist/src/asset-images.js +125 -0
  43. package/dist/src/assets/images/add.png.js +4 -0
  44. package/dist/src/assets/images/ai-button.png.js +4 -0
  45. package/dist/src/assets/images/ai.png.js +4 -0
  46. package/dist/src/assets/images/ai_import.png.js +4 -0
  47. package/dist/src/assets/images/app.png.js +4 -0
  48. package/dist/src/assets/images/approval.png.js +4 -0
  49. package/dist/src/assets/images/book.png.js +4 -0
  50. package/dist/src/assets/images/canvas-editor/alignment.svg.js +4 -0
  51. package/dist/src/assets/images/canvas-editor/arrow-left.svg.js +4 -0
  52. package/dist/src/assets/images/canvas-editor/arrow-right.svg.js +4 -0
  53. package/dist/src/assets/images/canvas-editor/block.svg.js +4 -0
  54. package/dist/src/assets/images/canvas-editor/bold.svg.js +4 -0
  55. package/dist/src/assets/images/canvas-editor/catalog.svg.js +4 -0
  56. package/dist/src/assets/images/canvas-editor/center.svg.js +4 -0
  57. package/dist/src/assets/images/canvas-editor/checkbox.svg.js +4 -0
  58. package/dist/src/assets/images/canvas-editor/close.svg.js +4 -0
  59. package/dist/src/assets/images/canvas-editor/codeblock.svg.js +4 -0
  60. package/dist/src/assets/images/canvas-editor/color.svg.js +4 -0
  61. package/dist/src/assets/images/canvas-editor/control.svg.js +4 -0
  62. package/dist/src/assets/images/canvas-editor/date.svg.js +4 -0
  63. package/dist/src/assets/images/canvas-editor/exit-fullscreen.svg.js +4 -0
  64. package/dist/src/assets/images/canvas-editor/format.svg.js +4 -0
  65. package/dist/src/assets/images/canvas-editor/highlight.svg.js +4 -0
  66. package/dist/src/assets/images/canvas-editor/hyperlink.svg.js +4 -0
  67. package/dist/src/assets/images/canvas-editor/image.svg.js +4 -0
  68. package/dist/src/assets/images/canvas-editor/italic.svg.js +4 -0
  69. package/dist/src/assets/images/canvas-editor/justify.svg.js +4 -0
  70. package/dist/src/assets/images/canvas-editor/latex.svg.js +4 -0
  71. package/dist/src/assets/images/canvas-editor/left.svg.js +4 -0
  72. package/dist/src/assets/images/canvas-editor/line-dash-dot-dot.svg.js +4 -0
  73. package/dist/src/assets/images/canvas-editor/line-dash-dot.svg.js +4 -0
  74. package/dist/src/assets/images/canvas-editor/line-dash-large-gap.svg.js +4 -0
  75. package/dist/src/assets/images/canvas-editor/line-dash-small-gap.svg.js +4 -0
  76. package/dist/src/assets/images/canvas-editor/line-dot.svg.js +4 -0
  77. package/dist/src/assets/images/canvas-editor/line-double.svg.js +4 -0
  78. package/dist/src/assets/images/canvas-editor/line-single.svg.js +4 -0
  79. package/dist/src/assets/images/canvas-editor/line-wavy.svg.js +4 -0
  80. package/dist/src/assets/images/canvas-editor/list.svg.js +4 -0
  81. package/dist/src/assets/images/canvas-editor/option.svg.js +4 -0
  82. package/dist/src/assets/images/canvas-editor/page-break.svg.js +4 -0
  83. package/dist/src/assets/images/canvas-editor/page-mode.svg.js +4 -0
  84. package/dist/src/assets/images/canvas-editor/page-scale-add.svg.js +4 -0
  85. package/dist/src/assets/images/canvas-editor/page-scale-minus.svg.js +4 -0
  86. package/dist/src/assets/images/canvas-editor/painter.svg.js +4 -0
  87. package/dist/src/assets/images/canvas-editor/paper-direction.svg.js +4 -0
  88. package/dist/src/assets/images/canvas-editor/paper-margin.svg.js +4 -0
  89. package/dist/src/assets/images/canvas-editor/paper-size.svg.js +4 -0
  90. package/dist/src/assets/images/canvas-editor/print.svg.js +4 -0
  91. package/dist/src/assets/images/canvas-editor/radio.svg.js +4 -0
  92. package/dist/src/assets/images/canvas-editor/redo.svg.js +4 -0
  93. package/dist/src/assets/images/canvas-editor/request-fullscreen.svg.js +4 -0
  94. package/dist/src/assets/images/canvas-editor/right.svg.js +4 -0
  95. package/dist/src/assets/images/canvas-editor/row-margin.svg.js +4 -0
  96. package/dist/src/assets/images/canvas-editor/search.svg.js +4 -0
  97. package/dist/src/assets/images/canvas-editor/separator.svg.js +4 -0
  98. package/dist/src/assets/images/canvas-editor/signature-undo.svg.js +4 -0
  99. package/dist/src/assets/images/canvas-editor/signature.svg.js +4 -0
  100. package/dist/src/assets/images/canvas-editor/size-add.svg.js +4 -0
  101. package/dist/src/assets/images/canvas-editor/size-minus.svg.js +4 -0
  102. package/dist/src/assets/images/canvas-editor/strikeout.svg.js +4 -0
  103. package/dist/src/assets/images/canvas-editor/subscript.svg.js +4 -0
  104. package/dist/src/assets/images/canvas-editor/superscript.svg.js +4 -0
  105. package/dist/src/assets/images/canvas-editor/table.svg.js +4 -0
  106. package/dist/src/assets/images/canvas-editor/title.svg.js +4 -0
  107. package/dist/src/assets/images/canvas-editor/trash.svg.js +4 -0
  108. package/dist/src/assets/images/canvas-editor/underline.svg.js +4 -0
  109. package/dist/src/assets/images/canvas-editor/undo.svg.js +4 -0
  110. package/dist/src/assets/images/canvas-editor/watermark.svg.js +4 -0
  111. package/dist/src/assets/images/canvas-editor/word-tool.svg.js +4 -0
  112. package/dist/src/assets/images/clear.png.js +4 -0
  113. package/dist/src/assets/images/create_market_system.png.js +4 -0
  114. package/dist/src/assets/images/create_system.png.js +4 -0
  115. package/dist/src/assets/images/custom.png.js +4 -0
  116. package/dist/src/assets/images/default-icon.png.js +4 -0
  117. package/dist/src/assets/images/ex_link.png.js +4 -0
  118. package/dist/src/assets/images/excel.png.js +4 -0
  119. package/dist/src/assets/images/file.png.js +4 -0
  120. package/dist/src/assets/images/folder.png.js +4 -0
  121. package/dist/src/assets/images/hall/form.png.js +4 -0
  122. package/dist/src/assets/images/hall/meta.png.js +4 -0
  123. package/dist/src/assets/images/hall/process.png.js +4 -0
  124. package/dist/src/assets/images/hall/rule.png.js +4 -0
  125. package/dist/src/assets/images/hall/service.png.js +4 -0
  126. package/dist/src/assets/images/hall/userRole.png.js +4 -0
  127. package/dist/src/assets/images/hr.png.js +4 -0
  128. package/dist/src/assets/images/import.png.js +4 -0
  129. package/dist/src/assets/images/jian.png.js +4 -0
  130. package/dist/src/assets/images/module.png.js +4 -0
  131. package/dist/src/assets/images/null.png.js +4 -0
  132. package/dist/src/assets/images/photo.png.js +4 -0
  133. package/dist/src/assets/images/qq.png.js +4 -0
  134. package/dist/src/assets/images/robot.png.js +4 -0
  135. package/dist/src/assets/images/single_file.png.js +4 -0
  136. package/dist/src/assets/images/snapshot-history.png.js +4 -0
  137. package/dist/src/assets/images/snapshot-manual.png.js +4 -0
  138. package/dist/src/assets/images/snapshot-realtime.png.js +4 -0
  139. package/dist/src/assets/images/staff.png.js +4 -0
  140. package/dist/src/assets/images/switch_app.png.js +4 -0
  141. package/dist/src/assets/images/sync_append.png.js +4 -0
  142. package/dist/src/assets/images/sync_append_update.png.js +4 -0
  143. package/dist/src/assets/images/sync_loading.png.js +4 -0
  144. package/dist/src/assets/images/sync_replace.png.js +4 -0
  145. package/dist/src/assets/images/sync_update.png.js +4 -0
  146. package/dist/src/assets/images/user_bg.png.js +4 -0
  147. package/dist/src/assets/images/weixin.png.js +4 -0
  148. package/dist/src/chart-config-update.js +17 -0
  149. package/dist/src/chart-data-adapters.js +112 -0
  150. package/dist/src/chart-option-override.js +128 -0
  151. package/dist/src/chart-option-paths.js +64 -0
  152. package/dist/src/chart-registry.js +46 -0
  153. package/dist/src/chart-render-host.js +57 -0
  154. package/dist/src/chart-request-broker.js +176 -0
  155. package/dist/src/chartTheme.js +683 -0
  156. package/dist/src/collection.js +25 -0
  157. package/dist/src/date-helper.js +60 -0
  158. package/dist/src/dom-helper.js +22 -0
  159. package/dist/src/eval-with.js +14 -0
  160. package/dist/src/fieldFactory.js +5 -0
  161. package/dist/src/formula.js +365 -0
  162. package/dist/src/hljs-lite.js +29 -0
  163. package/dist/src/index.js +24 -0
  164. package/dist/src/lazy-jsencrypt.js +13 -0
  165. package/dist/src/legacy.js +25 -0
  166. package/dist/src/locale.js +88 -0
  167. package/dist/src/menu-open.js +43 -0
  168. package/dist/src/mermaid.js +79 -0
  169. package/dist/src/meta.js +4 -0
  170. package/dist/src/mobile-chart-appearance.js +148 -0
  171. package/dist/src/moment-compat.js +558 -0
  172. package/dist/src/nav-history.js +184 -0
  173. package/dist/src/page.js +116 -0
  174. package/dist/src/pdf.viewer.js +4 -0
  175. package/dist/src/placeholder.js +64 -0
  176. package/dist/src/public.js +49 -0
  177. package/dist/src/security.js +26 -0
  178. package/dist/src/storage.js +107 -0
  179. package/dist/src/token-storage.js +108 -0
  180. package/dist/src/util.js +4 -0
  181. package/package.json +109 -3
  182. package/src/Config.js +25 -0
  183. package/src/asset-images.js +16 -0
  184. package/src/chart-config-update.js +16 -0
  185. package/src/chart-data-adapters.js +106 -0
  186. package/src/chart-option-override.js +126 -0
  187. package/src/chart-option-paths.js +63 -0
  188. package/src/chart-registry.js +41 -0
  189. package/src/chart-render-host.js +56 -0
  190. package/src/chart-request-broker.js +171 -0
  191. package/src/chartTheme.js +711 -0
  192. package/src/collection.js +28 -0
  193. package/src/date-helper.js +72 -0
  194. package/src/dom-helper.js +26 -0
  195. package/src/eval-with.js +21 -0
  196. package/src/fieldFactory.js +3 -0
  197. package/src/formula.js +362 -0
  198. package/src/hljs-lite.js +41 -0
  199. package/src/index.js +2 -0
  200. package/src/lazy-jsencrypt.js +36 -0
  201. package/src/legacy.js +29 -0
  202. package/src/locale.js +104 -0
  203. package/src/menu-open.js +72 -0
  204. package/src/mermaid.js +115 -0
  205. package/src/meta.js +2 -0
  206. package/src/mobile-chart-appearance.js +147 -0
  207. package/src/moment-compat.d.ts +91 -0
  208. package/src/moment-compat.js +744 -0
  209. package/src/nav-history.js +274 -0
  210. package/src/page.js +129 -0
  211. package/src/pdf.viewer.js +2 -0
  212. package/src/placeholder.js +107 -0
  213. package/src/public.js +11 -0
  214. package/src/security.js +29 -0
  215. package/src/storage.js +135 -0
  216. package/src/token-storage.js +120 -0
  217. package/src/util.js +2 -0
  218. package/types/Config.d.ts +4 -0
  219. package/types/collection.d.ts +40 -0
  220. package/types/date-helper.d.ts +11 -0
  221. package/types/date-time.d.ts +2 -0
  222. package/types/date.d.ts +1 -0
  223. package/types/dom-helper.d.ts +3 -0
  224. package/types/dom.d.ts +1 -0
  225. package/types/index.d.ts +1 -0
  226. package/types/moment-compat.d.ts +91 -0
  227. package/types/page.d.ts +56 -0
  228. package/types/placeholder.d.ts +42 -0
  229. package/types/public.d.ts +10 -0
  230. package/types/storage.d.ts +31 -0
@@ -0,0 +1,744 @@
1
+ // `$moment` 兼容层:对外是 moment 的用法,对内是 dayjs。
2
+ // 设计与实测依据:docs/superpowers/specs/2026-09-03-moment-to-dayjs-design.md
3
+ //
4
+ // 为什么需要一层包装而不是直接给 dayjs:**moment 可变、dayjs 不可变**。
5
+ // `const m = $moment(); m.add(1,'d')` 在 moment 下改的是 m 自己,在 dayjs 下这句是无效语句
6
+ // (实测:docs 里的 D1/D2/D3 三条)。包装类让写方法改内部实例并 return this,
7
+ // 于是「当语句用」和「链式用」两种写法同时成立,与 moment 一致。
8
+ // ⚠️ 说明符里的 `.js` 不是可有可无的装饰:dayjs 的 package.json **没有 exports 映射**
9
+ // (只有 main + types),所以子路径就是裸文件路径。Vite / vitest 会替你补扩展名,但纯
10
+ // Node ESM 不会 —— 实测 `import('dayjs/plugin/relativeTime')` 在 node 下直接
11
+ // ERR_MODULE_NOT_FOUND。产品项目若以 Node ESM 方式消费发布出去的 @epxing/utils 就会挂。
12
+ import dayjs from 'dayjs'
13
+ import 'dayjs/locale/zh-cn.js'
14
+ import relativeTime from 'dayjs/plugin/relativeTime.js'
15
+ import duration from 'dayjs/plugin/duration.js'
16
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js'
17
+ import advancedFormat from 'dayjs/plugin/advancedFormat.js'
18
+ import localizedFormat from 'dayjs/plugin/localizedFormat.js'
19
+ import calendarPlugin from 'dayjs/plugin/calendar.js'
20
+ import objectSupport from 'dayjs/plugin/objectSupport.js'
21
+ import arraySupport from 'dayjs/plugin/arraySupport.js'
22
+ import utcPlugin from 'dayjs/plugin/utc.js'
23
+ import minMax from 'dayjs/plugin/minMax.js'
24
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js'
25
+ import isSameOrAfter from 'dayjs/plugin/isSameOrAfter.js'
26
+ import isBetween from 'dayjs/plugin/isBetween.js'
27
+ import isLeapYear from 'dayjs/plugin/isLeapYear.js'
28
+ import weekOfYear from 'dayjs/plugin/weekOfYear.js'
29
+ import isoWeek from 'dayjs/plugin/isoWeek.js'
30
+ import quarterOfYear from 'dayjs/plugin/quarterOfYear.js'
31
+ import dayOfYear from 'dayjs/plugin/dayOfYear.js'
32
+ import weekday from 'dayjs/plugin/weekday.js'
33
+ import toArray from 'dayjs/plugin/toArray.js'
34
+ import toObject from 'dayjs/plugin/toObject.js'
35
+
36
+ // ⚠️ customParseFormat / objectSupport / arraySupport 当前仓库用不到,但 moment 支持
37
+ // `$moment(str, fmt)` / `$moment({y,M,d})` / `$moment([y,M,d])` 这三种写法。不装的话
38
+ // dayjs 会**静默**忽略第二参 / 得到 Invalid Date —— 是错值不是报错,产品项目最可能踩这个。
39
+ //
40
+ // ⚠️⚠️ **下面这个数组的顺序是语义相关的,别按字母序或"看着顺眼"重排。**
41
+ // dayjs 插件是「取出当前的 proto.X,换成一个包着它的新函数」——后 extend 的包在外层,
42
+ // 先被调用。两个插件包同一个方法时,谁在外面决定行为:
43
+ //
44
+ // ① `objectSupport` **必须排在最后**(至少要在 `quarterOfYear` 之后)。它给 add/set/
45
+ // subtract 加的是「参数是对象就按 key 逐个转发」这一层;而 `quarterOfYear` 也包
46
+ // `add`,且首句是无条件的 `e = Number(e)`(见 node_modules/dayjs/plugin/quarterOfYear.js)。
47
+ // quarterOfYear 若在外层,对象参数会先被 Number() 变成 NaN,objectSupport 的分支
48
+ // 永远走不到 ⇒ `$moment(x).add({days:1})` 全形态返回 `Invalid date`。
49
+ // (最终评审实测:set/subtract 不受影响——quarterOfYear 只包了 add——所以这个 bug
50
+ // 躲过了「测了 set(object) 就以为 add(object) 也行」的抽查。)
51
+ // ② `localizedFormat` 排在 `advancedFormat` **之后**,于是它在外层、先跑:L/LL/LT/…
52
+ // 先按 locale 展开成普通 token 串,再交给 advancedFormat 与核心 format 处理。
53
+ for (const p of [relativeTime, duration, customParseFormat, advancedFormat, localizedFormat,
54
+ calendarPlugin, arraySupport, utcPlugin, minMax, isSameOrBefore, isSameOrAfter, isBetween,
55
+ isLeapYear, weekOfYear, isoWeek, quarterOfYear, dayOfYear, weekday, toArray, toObject,
56
+ objectSupport]) {
57
+ dayjs.extend(p)
58
+ }
59
+ // locale 只设这一处。moment 时代散在 install.js 与 plugins/frame.js 各写了一遍,
60
+ // 而其中一遍还因为 moment 的双实例问题静默失效过(见 spec §1)。
61
+ //
62
+ // ⚠️⚠️ **这一句改的是 dayjs 的全局默认 locale,而 dayjs 与 Element Plus 是同一个实例**
63
+ // (node_modules 里只有一份 dayjs@1.11.21,EP 的 `dependencies.dayjs` 指向它)。已知的
64
+ // 两条外溢,裁定(R9)是**保留现行为 + 文档化,不做隔离**——`dayjs.locale('zh-cn')` 是
65
+ // 本 shim 中文输出的前提,隔离掉会让 `locale()` 的对拍断言(必须返回 'zh-cn')失效,
66
+ // 而且中文平台上周一起始本来就更合理:
67
+ // ① `el-calendar` 的每周首日由**周日变周一**:EP 的
68
+ // element-plus/es/components/calendar/src/use-date-table.mjs:12 读的是
69
+ // `dayjs.localeData().firstDayOfWeek()`,取的正是这个全局默认 locale(zh-cn 的
70
+ // weekStart = 1)。
71
+ // ② `createMoment.locale('en')`(下面静态面那一行)会**全局翻掉** dayjs 的默认 locale,
72
+ // 于是数据库里的一行页面 JS 就能改到 EP 的日期显示——这是 moment 时代没有的触达范围。
73
+ // 同步记录在 docs/WINDOW-FRAME-ABI.md。
74
+ dayjs.locale('zh-cn')
75
+
76
+ // ⚠️ dayjs 内置 zh-cn 的 future 模板是 "%s内",moment 是 "%s后"(node_modules 里两家
77
+ // 的 locale 源码逐字节核对过,past 模板 "%s前" 一致,只有 future 不同)——受影响的不只
78
+ // duration.humanize(true),MomentLike 转发的 from/to/fromNow/toNow 在"未来方向"上全部
79
+ // 会说错。全仓只有这一处 `dayjs.extend(relativeTime)`(grep 过,没有第二处),且 Element
80
+ // Plus 自身的产物里搜不到 "relativeTime" 这个字符串(它不装这个插件,读不到这个字段),
81
+ // 所以这里直接覆盖 dayjs 内置的 zh-cn 表,不会波及除本 shim 外的任何 dayjs 消费方。
82
+ // 不改注册一份新 locale 的原因:`createMoment.locale()` / `m.locale()` 的对拍断言钉的是
83
+ // 名字必须还是 'zh-cn'。
84
+ dayjs.Ls['zh-cn'].relativeTime.future = '%s后'
85
+
86
+ // ⚠️ 同上一处的做法(裁定 R11),补齐另外两张 dayjs 与 moment 内容不同的 zh-cn 子表。
87
+ //
88
+ // ① `meridiem`(A/a token):dayjs 内置 zh-cn 的分界是 1100/1300,moment 是 **1130/1230**
89
+ // (node_modules/moment/locale/zh-cn.js:56,60 逐行核对)。差集实测:11:00–11:29 moment
90
+ // 说「上午」dayjs 说「中午」、12:30–12:59 moment 说「下午」dayjs 说「中午」。这是一整段
91
+ // 时间里的静默错字,`LLL`/`LLLL` 也会跟着错(它们的模板里含 A)。
92
+ // 爆炸半径(本轮独立复核):element-plus 全包对 "meridiem" 的引用 **0 次**;EP 自带的
93
+ // 默认格式串是 HH:mm / HH:mm:ss / YYYY-MM-DD / gggg[w]ww,**没有一条含 A 或 a**;EP 里
94
+ // 带字面量的 `.format()` 只有 `.format('M')` 三处。本仓自身的 13 种格式串也全部不含 A/a。
95
+ // ② `calendar`:dayjs 的 zh-cn locale **根本没有这张表**,calendar 插件会退回它内置的英文
96
+ // 默认值(`[Today at] h:mm A`)。不补的话 `$moment().calendar()` 输出英文——又是一个
97
+ // 「看起来像样的错值」。表内容逐字抄自 moment zh-cn(moment/locale/zh-cn.js:66-83)。
98
+ // ⚠️ 两家对**函数型**取值的处理不同:moment 拿函数的返回值当格式串再 `format()` 一次,
99
+ // dayjs 是 `l.call(this, dayjs())` 后**直接返回**、不再 format。所以这里的两个函数必须
100
+ // 自己调用 `this.format(...)`,不能照抄 moment 那样只返回格式串。
101
+ dayjs.Ls['zh-cn'].meridiem = (hour, minute) => {
102
+ const hm = hour * 100 + minute
103
+ if (hm < 600) return '凌晨'
104
+ if (hm < 900) return '早上'
105
+ if (hm < 1130) return '上午'
106
+ if (hm < 1230) return '中午'
107
+ if (hm < 1800) return '下午'
108
+ return '晚上'
109
+ }
110
+ dayjs.Ls['zh-cn'].calendar = {
111
+ sameDay: '[今天]LT',
112
+ nextDay: '[明天]LT',
113
+ nextWeek (now) { return this.format(now.week() !== this.week() ? '[下]dddLT' : '[本]dddLT') },
114
+ lastDay: '[昨天]LT',
115
+ lastWeek (now) { return this.format(this.week() !== now.week() ? '[上]dddLT' : '[本]dddLT') },
116
+ sameElse: 'L'
117
+ }
118
+
119
+ // 只读方法:直接转发。除 clone 外返回的都不是日期对象,不需要再包一层。
120
+ export const READ_METHODS = ['format', 'valueOf', 'unix', 'toDate', 'toISOString', 'toJSON',
121
+ 'toString', 'toArray', 'toObject', 'diff', 'isValid', 'isBefore', 'isAfter', 'isSame',
122
+ 'isSameOrBefore', 'isSameOrAfter', 'isBetween', 'isLeapYear', 'daysInMonth', 'clone',
123
+ 'get', 'from', 'to', 'fromNow', 'toNow', 'utcOffset', 'weekday', 'isoWeekday',
124
+ // `calendar` 是本仓自己的页面 JS 编写指南(.claude/skills/epx-frame-web/tools/Moment日期.md)
125
+ // 教过的写法,漏了它等于指南里的示例一跑就抛错。
126
+ 'calendar']
127
+
128
+ // 写方法:改内部 dayjs 实例并 return this —— 这就是"模拟可变"的全部内容。
129
+ export const WRITE_METHODS = ['add', 'subtract', 'startOf', 'endOf', 'set', 'local', 'utc',
130
+ 'locale', 'year', 'month', 'date', 'day', 'hour', 'minute', 'second', 'millisecond',
131
+ 'quarter', 'week', 'isoWeek', 'dayOfYear']
132
+
133
+ // ⚠️ 这些是 **getter/setter 双身份**:无参取值(返回 number/string),有参赋值(返回 this)。
134
+ // moment 就是这个语义。按 arguments.length 分支,写错一边是静默错值、没有任何报错。
135
+ export const DUAL_METHODS = new Set(['locale', 'year', 'month', 'date', 'day', 'hour',
136
+ 'minute', 'second', 'millisecond', 'quarter', 'week', 'isoWeek', 'dayOfYear'])
137
+
138
+ // ⚠️ 框架探针放行名单。**漏了会白屏**:Vue 3 的 reactive/isRef 会读 `__v_*`,
139
+ // Promise 解析会读 `then`,模板与 JSON 序列化会读 Symbol.toPrimitive / toJSON。
140
+ // 这些必须返回 undefined 或真实值,绝不能抛错。
141
+ export const PASSTHROUGH = new Set(['then', 'nodeType', 'tagName', 'length', 'name',
142
+ 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
143
+ '__v_isRef', '__v_isReactive', '__v_isReadonly', '__v_raw', '__v_skip',
144
+ '_isAMomentObject', '_d', '$$typeof'])
145
+
146
+ class MomentLike {
147
+ constructor (d) {
148
+ this._d = d
149
+ // 让 Vue 的 reactive() 整体跳过本对象:日期对象没有需要被追踪的内部字段,
150
+ // 包进响应式只有开销(本仓已有 meta markRaw 的同类处置)。
151
+ this.__v_skip = true
152
+ // 存量代码可能用 moment 的这个内部标记判型
153
+ this._isAMomentObject = true
154
+ }
155
+ }
156
+
157
+ function wrapMaybe (v) {
158
+ return dayjs.isDayjs(v) ? createMoment(v) : v
159
+ }
160
+
161
+ // 转发只读方法前,把参数里混入的 MomentLike 换成它内部的裸 dayjs 实例(比如
162
+ // a.diff(b)、a.from(b) 的 b):dayjs 的 diff/from 等实现会直接读参数上的内部字段
163
+ // (如 $u、$isDayjsObject),不会先规整成 dayjs()。MomentLike 是 Proxy,这些字段
164
+ // 不在白名单里,直接传给 dayjs 会被当成「不在覆盖范围」抛错(实测:
165
+ // createMoment(a).diff(createMoment(b)) 会命中)。判据与上面 createMoment 里
166
+ // `input._d` 同一个套路。
167
+ function unwrapMaybe (v) {
168
+ return (v && v._d) ? v._d : v
169
+ }
170
+
171
+ for (const name of READ_METHODS) {
172
+ MomentLike.prototype[name] = function (...args) {
173
+ return wrapMaybe(this._d[name](...args.map(unwrapMaybe)))
174
+ }
175
+ }
176
+
177
+ // moment 对无效日期输出 "Invalid date"(小写 d),dayjs 输出 "Invalid Date"。
178
+ // 这个串可能被直接显示或参与比较,必须对齐到 moment 那一侧。
179
+ const INVALID = 'Invalid date'
180
+
181
+ // ⚠️ **无效日期的兜底值**。数据库里的日期字段可空,`$moment(row.SOME_DATE)` 拿到
182
+ // null/''/非法串是这个平台的日常,所以这条路径不是边角。moment 对无效日期有一套统一兜底
183
+ // (数值 getter → NaN、字符串输出 → 'Invalid date'、toISOString/toJSON → null);dayjs 大部分
184
+ // 对得上(year()/month()/diff()/valueOf() 都给 NaN,toJSON 给 null),但下面这 6 个没有——
185
+ // 生成式矩阵(方法 × 入参形态)扫出来的,逐条都是"看起来像样的错值"或读不懂的内部错:
186
+ // toISOString moment: null dayjs: **抛 RangeError: Invalid time value**
187
+ // isoWeekday moment: NaN dayjs: 7(实现是 `$W || 7`,NaN 落进了 || 分支)
188
+ // from/to moment: 'Invalid date' dayjs: '1 个月前'(拿 NaN 去做差,档位随机)
189
+ // fromNow/toNow 同上
190
+ // calendar moment: 'Invalid date' dayjs: 'Invalid Date'(大小写,插件内部直接 format)
191
+ const INVALID_FALLBACKS = {
192
+ toISOString: null,
193
+ isoWeekday: NaN,
194
+ calendar: INVALID,
195
+ from: INVALID,
196
+ to: INVALID,
197
+ fromNow: INVALID,
198
+ toNow: INVALID
199
+ }
200
+ // from/to 的**参照物**也可能无效(`ref.from(null)`),moment 同样给 'Invalid date'。
201
+ const CHECKS_ARG = new Set(['from', 'to'])
202
+
203
+ for (const [name, fallback] of Object.entries(INVALID_FALLBACKS)) {
204
+ const raw = MomentLike.prototype[name]
205
+ MomentLike.prototype[name] = function (...args) {
206
+ if (!this._d.isValid()) return fallback
207
+ if (CHECKS_ARG.has(name) && args[0] !== undefined) {
208
+ const other = unwrapMaybe(args[0])
209
+ if (!dayjs(other).isValid()) return fallback
210
+ }
211
+ return raw.apply(this, args)
212
+ }
213
+ }
214
+
215
+ // ⚠️ moment 的 68 个 format token 里,有 16 个是 dayjs + 本文件所装插件**给不出同样输出**的。
216
+ // 它们分三类,共同点是**全都不报错**:
217
+ // · 静默错值:`DDD`/`DDDD`(年内第几天)在 dayjs 下会被当成 D 系 token,2026-09-02 输出
218
+ // "022"/"0202",moment 是 "245"——像模像样,肉眼查不出来;
219
+ // · 原样吐字面量:`Qo` `Mo` `do` `Wo` `e` `E` `gg` `GG` `N` `NN` 输出的就是 token 本身;
220
+ // · 抛一个读不懂的内部错:`gggg` → `t.weekYear is not a function`,`z`/`zz` → `t.offsetName
221
+ // is not a function`(advancedFormat 会去调 weekYear/timezone 插件的方法,我们没装)。
222
+ // 按用户裁定的绑定姿态(厚 shim、不留逃生开关、覆盖面外**响亮抛错**),这里在入口就把这批
223
+ // token 拦下抛错。**这比逐个实现它们更便宜也更正确**:一整类静默错值换成一条自解释的报错。
224
+ //
225
+ // ⚠️ `y`(连同 `yy`/`yyy`/`yyyy`)**曾经也在这张表里**:第一轮修复把它当成「原样吐字面量」
226
+ // 一类一并拦下抛错,随后评审抓到这是个**新引入的一等回归**——
227
+ // packages/components/src/form-item/Date.vue:622 的 isDateNow 分支里有一句 moment 时代遗留的
228
+ // `format('yyyy')`(历史写法,意为"当年 4 位数字"),shim 改成抛错后会整段打断 Vue 的
229
+ // created() 钩子,isDateNow 之后的 pickerOptions/rangePickerOptions 设置也一并丢失(本仓无
230
+ // app.config.errorHandler,Vue 只会打日志、不会让钩子继续跑)。这个 token 组便宜到可以直接
231
+ // 实现(见下方 preprocessFormat 里紧挨 S+ 的那段),犯不着让"响亮抛错"这个姿态去背一个自己
232
+ // 填得平的坑,于是把它从这张「拦下抛错」表里挪走,改走计算路径。
233
+ //
234
+ // 不在这张表里、经逐 token 对拍确认与 moment 逐字节一致的(别顺手加进来):
235
+ // YYYY YY M MM MMM MMMM D DD Do d dd ddd dddd H HH h hh k kk m mm s ss SSS Z ZZ A a X x
236
+ // Q w ww wo W WW **GGGG**(isoWeek 插件提供 isoWeekYear,实测两边都是 "2026")
237
+ // L LL LLL LLLL l ll lll llll LT LTS(localizedFormat 插件 + 上面补的 zh-cn formats)
238
+ // ⚠️ 顺序即语义:长的必须排在短的前面(见下面正则的说明)。
239
+ export const UNSUPPORTED_FORMAT_TOKENS = ['DDDo', 'DDDD', 'DDD', 'Mo', 'Qo', 'do', 'Wo',
240
+ 'gggg', 'gg', 'GG', 'NN', 'N', 'zz', 'z', 'E', 'e']
241
+
242
+ // moment 的亚秒 token:S=十分秒、SS=百分秒、SSS=毫秒。dayjs 只认 SSS,
243
+ // 遇到 S / SS 会**原样吐字面量**(实测 `format('YYYYMMDDHHMMSS')` → "202609021309SS")。
244
+ // 这里把 S / SS(以及 moment 支持的 4 位以上)现算成数字字面量再交给 dayjs。
245
+ //
246
+ // `y`/`yy`/`yyy`/`yyyy` 同款处理(不走上面的抛错表,见该表旁边的说明):这四个 token
247
+ // 在 moment 里都是 eraYear(本仓未启用自定义 era,等价于 `this.year()`),左补零到至少
248
+ // token 长度那么多位——不是「取后 N 位」(那是 `YY` 的语义)。逐字节核对自
249
+ // node_modules/moment/moment.js 的 `zeroFill` + `addFormatToken('y', ['yyyy', 4], ...)`。
250
+ // 本仓年份都是公元后 4 位数,补零目标位数(1~4)从不超过实际位数,所以 y/yy/yyy/yyyy 四种
251
+ // 写法输出永远相同(都是完整的 4 位年份)。dayjs 核心 format 正则本就不认识小写 `y`
252
+ // (node_modules/dayjs/dayjs.min.js 的 REGEX_FORMAT 没有这个分支),原生会把它当字面量原样
253
+ // 吐出——这正是这个坑过去是"静默错值"而不是"抛错"的原因。
254
+ //
255
+ // 扫描用的这条正则同时承担三件事,**分支顺序是语义相关的**(JS 的 `|` 是有序的,
256
+ // 同一位置先匹配上的赢),照 moment 自己的 formattingTokens 那套「长的排前面」写法:
257
+ // · `\[[^\]]*\]` 排最前——方括号转义段必须整体跳过(`format('[SS]')` 要输出 "SS",
258
+ // `format('[yyyy]')` 同理要输出字面量 "yyyy");
259
+ // · `LTS` 必须排在 `S+` 前面,否则 `format('LTS')` 会被当成「LT + 十分秒」吐出 "LT0";
260
+ // · `GGGG` 必须排在 `GG` 前面,否则一个**受支持**的 token 会被误判成不支持而抛错;
261
+ // · `DDDo`>`DDDD`>`DDD`、`gggg`>`gg`、`NN`>`N` 同理,保证报错信息指的是真正写的那个 token。
262
+ // 反过来,`Mo`/`Qo`/`do`/`Wo` 都以字面 'o' 收尾,`MMMM`/`Q`/`dddd`/`WW` 这些受支持的写法
263
+ // 匹配不上它们,不会误伤(大小写敏感:受支持的 `Do` 与不支持的 `do` 是两个 token)。
264
+ // `y{1,4}` 自带的量词上限就是 4:`yyyyy`(5 个 y)第一轮贪婪吃掉前 4 个算一个 token,
265
+ // 剩下 1 个在下一轮匹配里再算一个 token,两段各自算出同一个年份再拼接
266
+ // (2026 年实测:`"2026" + "2026"` = "20262026"),与 moment 自己的 formattingTokens
267
+ // 正则(同样只认到 4 个连续 y)行为一致。
268
+ // 正则的大部分由上面那张表**推导**而来,不手写第二份——两份会漂;`y{1,4}` 是唯一手写的
269
+ // 例外,因为它不再属于「拦下抛错」表,没有现成的数组可供 spread。
270
+ const FORMAT_SCAN_RE = new RegExp(
271
+ ['\\[[^\\]]*\\]', 'LTS', 'GGGG', ...UNSUPPORTED_FORMAT_TOKENS, 'S+', 'y{1,4}'].join('|'), 'g')
272
+
273
+ export function preprocessFormat (fmt, ms, year) {
274
+ if (typeof fmt !== 'string') return fmt
275
+ return fmt.replace(FORMAT_SCAN_RE, (seg) => {
276
+ // 转义段与「含 S/GG 但其实受支持」的两个 token:原样交给 dayjs
277
+ if (seg[0] === '[' || seg === 'LTS' || seg === 'GGGG') return seg
278
+ if (seg[0] === 'S') {
279
+ if (seg.length === 3) return seg // SSS:dayjs 原生支持,别碰
280
+ if (seg.length === 1) return String(Math.floor(ms / 100))
281
+ if (seg.length === 2) return String(Math.floor(ms / 10)).padStart(2, '0')
282
+ return String(ms).padStart(3, '0').padEnd(seg.length, '0')
283
+ }
284
+ if (seg[0] === 'y') {
285
+ // moment 的 zeroFill:负数保留符号位、对绝对值左补零,不能对带符号的字符串直接
286
+ // padStart(会把 0 垫进符号和数字之间,比如 "-5" 变成 "00-5")。本仓实际年份恒正,
287
+ // 这里只是照 zeroFill 的算法补全,不额外增加行为面。
288
+ const sign = year < 0 ? '-' : ''
289
+ return sign + String(Math.abs(year)).padStart(seg.length, '0')
290
+ }
291
+ throw new Error(
292
+ `[epx-moment] format 串里的 \`${seg}\` 不在兼容层覆盖范围内。\n` +
293
+ ' 本平台已从 moment 迁移到 dayjs,$moment 是一层兼容 shim。\n' +
294
+ ' dayjs 对这个 token 会静默给出错值(或抛一个读不懂的内部错),所以这里主动拦下——\n' +
295
+ ` 想输出字面量 "${seg}" 请写成 [${seg}](方括号是 moment/dayjs 通用的转义)。\n` +
296
+ ' 确实需要这个 token 请补进 packages/utils/src/moment-compat.js;\n' +
297
+ ' 临时可用 $moment(x).toDate() 拿到原生 Date 自行处理。'
298
+ )
299
+ })
300
+ }
301
+
302
+ MomentLike.prototype.format = function (fmt) {
303
+ if (!this._d.isValid()) return INVALID
304
+ return this._d.format(preprocessFormat(fmt, this._d.millisecond(), this._d.year()))
305
+ }
306
+
307
+ // ⚠️ 必须覆盖(不能走 READ_METHODS 那条通用转发):**两家的 toString 输出完全不同**。
308
+ // moment:`Wed Sep 02 2026 13:45:06 GMT+0800`(本地时区、英文、逐字节抄自
309
+ // moment/src/lib/moment/prototype.js 的 `this.clone().locale('en')
310
+ // .format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ')`)
311
+ // dayjs :`Wed, 02 Sep 2026 05:45:06 GMT`(它的 toString 就是 `$d.toUTCString()`——
312
+ // **连时区都换成 UTC 了**)
313
+ // 这条是线上可见的:Vue 的 toDisplayString 对「带自定义 toString 的对象」走 String(val),
314
+ // 所以模板里的 `{{ dateObj }}`、字符串拼接、以及公式引擎把 DATE_ADD/DATE_SUBTRACT 的返回值
315
+ // 当结果流出去(util.js:958)时,出来的都是这个串。
316
+ // ⚠️ `_d.locale('en')` 返回的是新实例(dayjs 不可变),不会污染本对象的 locale。
317
+ MomentLike.prototype.toString = function () {
318
+ if (!this._d.isValid()) return INVALID
319
+ return this._d.locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ')
320
+ }
321
+
322
+ for (const name of WRITE_METHODS) {
323
+ MomentLike.prototype[name] = function (...args) {
324
+ if (DUAL_METHODS.has(name) && args.length === 0) return this._d[name]()
325
+ // 参数同样过一遍 unwrapMaybe:查过 dayjs 核心与这里装的插件(utc/duration/
326
+ // quarterOfYear/weekOfYear/isoWeek/dayOfYear)的源码,20 个写方法目前都没有
327
+ // 像 diff/isSame 那样直读参数内部字段的写法,此刻还搭不出复现用例;但对非
328
+ // MomentLike 的参数它是纯 no-op,跟只读转发保持同一防线不吃亏。
329
+ this._d = this._d[name](...args.map(unwrapMaybe))
330
+ return this
331
+ }
332
+ }
333
+
334
+ // —— set(object):自己归一化 + 排序 + 逐个下发,不交给 dayjs 的 objectSupport ——
335
+ //
336
+ // moment 的 stringSet 是「normalizeObjectUnits 归一化键名 → getPrioritizedUnits 按
337
+ // priorities 表升序排 → 逐个 this[unit](value)」。dayjs 的 objectSupport 两点都不同:
338
+ // 按字面键序下发,且它的 set 只认核心那几个单位。实测两类分歧(改前 6/13 不一致):
339
+ // ① 键序敏感:`set({date:31, month:1})` moment 先设 2 月再设 31 日 ⇒ 2 月 31 日**溢出**
340
+ // 成 2026-03-03;dayjs 按字面序先设 31 日再设 2 月 ⇒ 2026-02-01。
341
+ // ② 单位面:quarter / week / isoWeek / dayOfYear 这四个 dayjs 的 set 不认,**静默忽略**
342
+ // (`set({year:2030, quarter:2})` 只有年生效)。静默错值正是本 shim 要消灭的失败模式。
343
+ // 下面两张表逐字抄自 moment 2.30.1 的 src/lib/units/{aliases,priorities}.js。
344
+ const UNIT_ALIASES = {
345
+ D: 'date',
346
+ dates: 'date',
347
+ date: 'date',
348
+ d: 'day',
349
+ days: 'day',
350
+ day: 'day',
351
+ e: 'weekday',
352
+ weekdays: 'weekday',
353
+ weekday: 'weekday',
354
+ E: 'isoWeekday',
355
+ isoweekdays: 'isoWeekday',
356
+ isoweekday: 'isoWeekday',
357
+ DDD: 'dayOfYear',
358
+ dayofyears: 'dayOfYear',
359
+ dayofyear: 'dayOfYear',
360
+ h: 'hour',
361
+ hours: 'hour',
362
+ hour: 'hour',
363
+ ms: 'millisecond',
364
+ milliseconds: 'millisecond',
365
+ millisecond: 'millisecond',
366
+ m: 'minute',
367
+ minutes: 'minute',
368
+ minute: 'minute',
369
+ M: 'month',
370
+ months: 'month',
371
+ month: 'month',
372
+ Q: 'quarter',
373
+ quarters: 'quarter',
374
+ quarter: 'quarter',
375
+ s: 'second',
376
+ seconds: 'second',
377
+ second: 'second',
378
+ w: 'week',
379
+ weeks: 'week',
380
+ week: 'week',
381
+ W: 'isoWeek',
382
+ isoweeks: 'isoWeek',
383
+ isoweek: 'isoWeek',
384
+ y: 'year',
385
+ years: 'year',
386
+ year: 'year'
387
+ }
388
+ const UNIT_PRIORITY = {
389
+ year: 1,
390
+ dayOfYear: 4,
391
+ week: 5,
392
+ isoWeek: 5,
393
+ quarter: 7,
394
+ month: 8,
395
+ date: 9,
396
+ day: 11,
397
+ weekday: 11,
398
+ isoWeekday: 11,
399
+ hour: 13,
400
+ minute: 14,
401
+ second: 15,
402
+ millisecond: 16
403
+ }
404
+ // 归一化后的单位 → 怎么在裸 dayjs 实例上设它。都走各自的 setter 而不是 `d.set(unit,v)`,
405
+ // 因为后者认不得 quarter/week/isoWeek/dayOfYear(这四个是插件加在原型上的方法)。
406
+ const UNIT_SETTER = {
407
+ year: (d, v) => d.year(v),
408
+ month: (d, v) => d.month(v),
409
+ date: (d, v) => d.date(v),
410
+ day: (d, v) => d.day(v),
411
+ weekday: (d, v) => d.weekday(v),
412
+ isoWeekday: (d, v) => d.isoWeekday(v),
413
+ hour: (d, v) => d.hour(v),
414
+ minute: (d, v) => d.minute(v),
415
+ second: (d, v) => d.second(v),
416
+ millisecond: (d, v) => d.millisecond(v),
417
+ quarter: (d, v) => d.quarter(v),
418
+ week: (d, v) => d.week(v),
419
+ isoWeek: (d, v) => d.isoWeek(v),
420
+ dayOfYear: (d, v) => d.dayOfYear(v)
421
+ }
422
+
423
+ MomentLike.prototype.set = function (unit, value) {
424
+ if (unit === null || typeof unit !== 'object') {
425
+ this._d = this._d.set(unit, value)
426
+ return this
427
+ }
428
+ const pending = []
429
+ for (const key of Object.keys(unit)) {
430
+ // 与 moment 一致:认不出的键**静默丢弃**,不抛错(normalizeObjectUnits 就是这么做的)
431
+ const u = UNIT_ALIASES[key] || UNIT_ALIASES[String(key).toLowerCase()]
432
+ if (u) pending.push([u, unit[key]])
433
+ }
434
+ // moment 的 weekYear/isoWeekYear(priority 1)本 shim 覆盖不到——dayjs 没有对应能力。
435
+ // 上面的别名表里就没有它们,所以会走「静默丢弃」;这与 moment 不一致,但这两个单位
436
+ // 连单独调用都不在白名单里,此处保持与 shim 其余部分同一覆盖面。
437
+ pending.sort((a, b) => UNIT_PRIORITY[a[0]] - UNIT_PRIORITY[b[0]])
438
+ for (const [u, v] of pending) this._d = UNIT_SETTER[u](this._d, v)
439
+ return this
440
+ }
441
+
442
+ // ⚠️ 必须在两个安装循环都跑完之后再构造:READ_METHODS 装完时 WRITE_METHODS
443
+ // 还是空数组,若在此提前构造,所有写方法都会被这份白名单挡下抛错。
444
+ const ALLOWED = new Set([...READ_METHODS, ...WRITE_METHODS])
445
+
446
+ /**
447
+ * 造一个 moment 风格的日期对象。入参形态与 moment 一致(无参=当前时间、
448
+ * 字符串、时间戳、Date、dayjs 对象、MomentLike、对象/数组字面量)。
449
+ */
450
+ function createMoment (input, ...rest) {
451
+ let d
452
+ if (arguments.length === 0) d = dayjs()
453
+ // ⚠️ 已经是 dayjs / MomentLike 的,**直接取用内部实例,别再过一次 dayjs()**:
454
+ // 再过一次会丢掉 UTC 模式等内部状态(createMoment.utc(...) 会因此按本地时区
455
+ // 格式化、跨时区差一天)。dayjs 不可变,共享内部实例是安全的——本包装类的写方法
456
+ // 是「替换 _d」而不是「原地改 _d」,两个包装对象共享初始实例互不影响。
457
+ //
458
+ // ⚠️ MomentLike 分支必须排在 dayjs.isDayjs(input) 前面(实测踩坑):MomentLike 是
459
+ // Proxy,dayjs.isDayjs 内部会读 `$isDayjsObject` 这类不在白名单里的字段,对它调用
460
+ // 会被当成「不在覆盖范围」抛错——createMoment(既有的 MomentLike) 直接命中。
461
+ // `input._d` 是安全探测:_d 在 PASSTHROUGH 里,裸 dayjs 实例上则没有这个字段。
462
+ else if (input && input._d) d = input._d
463
+ else if (dayjs.isDayjs(input)) d = input
464
+ else d = dayjs(input, ...rest)
465
+ const inst = new MomentLike(d)
466
+ return new Proxy(inst, {
467
+ get (target, prop, receiver) {
468
+ if (typeof prop === 'symbol') return Reflect.get(target, prop, receiver)
469
+ // ⚠️ 判据是「**自有**字段」(_d / __v_skip / _isAMomentObject),**不能**写成
470
+ // `prop in target`——`in` 会一路走到 Object.prototype,于是 toLocaleString /
471
+ // __defineGetter__ / __lookupGetter__ 这类继承成员全部被当成"在覆盖范围内"静默放行。
472
+ // `toLocaleString` 尤其阴:moment 那边它等价于 toString(),dayjs 这边它同样落到
473
+ // toString 上,但两家 toString 的输出本来就不同(见上面的覆盖),放行等于把一个错值
474
+ // 伪装成"支持"。原型上的方法一律由 ALLOWED 白名单负责。
475
+ // ⚠️ 用 `hasOwnProperty.call` 而不是 `Object.hasOwn`:后者是 ES2022,要 iOS 15.4+,
476
+ // 而本仓的目标面里有更老的 iOS(曾因正则 lookbehind 需要 iOS 16.4 而整块白屏,见
477
+ // 那次事故记录)。语义完全一样,兼容面白赚。
478
+ if (ALLOWED.has(prop) || PASSTHROUGH.has(prop) ||
479
+ Object.prototype.hasOwnProperty.call(target, prop)) {
480
+ return Reflect.get(target, prop, receiver)
481
+ }
482
+ throw new Error(
483
+ `[epx-moment] \`$moment(...).${prop}\` 不在兼容层覆盖范围内。\n` +
484
+ ' 本平台已从 moment 迁移到 dayjs,$moment 是一层兼容 shim。\n' +
485
+ ' 需要这个 API 请补进 packages/utils/src/moment-compat.js;\n' +
486
+ ' 临时可用 $moment(x).toDate() 拿到原生 Date 自行处理。'
487
+ )
488
+ }
489
+ })
490
+ }
491
+
492
+ // moment 的 duration.humanize() 默认阈值 + 天↔月固定换算常数,逐字节抄自
493
+ // node_modules/moment/src/lib/duration/{humanize,bubble}.js(本仓没有任何
494
+ // moment.relativeTimeThreshold() 调用改过默认阈值)。dayjs 自己的 duration.humanize()
495
+ // 有两个问题:① 内部靠 `dayjs().add(ms,'ms').fromNow()` 读两次「真实现在」做差再分类,
496
+ // 卡在四舍五入边界的值(比如 90 分钟=1.5 小时)会被两次取值间的执行耗时污染,几毫秒的
497
+ // 调度延迟就能把「2 小时」翻成「1 小时」;② 月/年分类靠 dayjs 的 `.diff()`,那是**日历
498
+ // 感知**的(按相邻月份的真实天数做除法),而 moment 用的是固定比例 `days*4800/146097`
499
+ // (400 个公历年=146097 天=4800 个月,摊平闰年后的常数)——两者在月/年边界上会给出不同
500
+ // 档位(实测 46/75/546 天这三个值两套算法各不相同,且哪边"更接近真值"不重要,重要的是
501
+ // 必须和 moment 一致)。这里干脆不用 dayjs 的日期做差,直接从 duration 的毫秒值按 moment
502
+ // 的算法分类——时钟依赖和日历依赖一起消失。
503
+ const DURATION_THRESHOLDS = { ss: 44, s: 45, m: 45, h: 22, d: 26, M: 11 }
504
+ const daysToMonths = (days) => (days * 4800) / 146097
505
+ const monthsToDays = (months) => (months * 146097) / 4800
506
+
507
+ // ── duration 的「三桶」还原 ────────────────────────────────────────────────────
508
+ // moment 的 Duration 内部是**三个互不折算的桶**:`_milliseconds` / `_days` / `_months`
509
+ // (构造时写进去就定死,见 moment/src/lib/duration/constructor.js),`as()` 与 `humanize()`
510
+ // 都按这三个桶算。dayjs 把一切塌缩成一个 `$ms`,桶信息**原则上已经丢了**——
511
+ // 但它派生出来的 `$d` 用的月长是固定的 365/12 天(= 2628000000ms),所以按月/季/年单位
512
+ // 构造时 `$ms` 恰好是这个常数的整数倍,`$d.years*12 + $d.months` 能把当初写进去的月数
513
+ // **原样取回**。于是判据是「$d 里比月更小的位是否全为 0」:
514
+ // · 是(且确有年或月)⇒ 认定「按月/季/年构造」,还原成 `_months`,`_ms` 归 0;
515
+ // · 否 ⇒ 一律当成纯毫秒(`_days` = `_months` = 0)。这条与 dayjs 自己的算法等价,
516
+ // 不改变现状(本仓 6 个调用点全走这条)。
517
+ // ⚠️ 已知残留(实测无法消除):`duration(47304000000)` 与 `duration(18,'M')` 在 dayjs 里
518
+ // 是**同一个对象**(前者正好是 18 个 dayjs 月的毫秒数),而 moment 分得清——前者 `_ms`
519
+ // 桶、后者 `_months` 桶,`humanize()` 一个给「1 年」一个给「2 年」。要消除只能自己重写
520
+ // moment 的三桶模型。命中它需要写一个 2628000000 的整数倍毫秒值。
521
+ function durationBuckets (d) {
522
+ // ⚠️ `$d` 的字段在不同构造形态下并不齐全(`duration({months:18})` 只有 months 一个键、
523
+ // `duration('abc')` 是空对象、ISO 串还会多出 weeks),所以逐个 `|| 0` 兜底,别解构带默认值。
524
+ const b = d.$d || {}
525
+ const ms = d.$ms || 0
526
+ const exact = !b.days && !b.hours && !b.minutes && !b.seconds && !b.milliseconds &&
527
+ !b.weeks && (b.years || b.months)
528
+ return exact ? { ms: 0, months: (b.years || 0) * 12 + (b.months || 0) } : { ms, months: 0 }
529
+ }
530
+
531
+ // moment 的 `as(units)`,逐行抄自 moment/src/lib/duration/as.js。
532
+ // 'month'/'quarter'/'year' 与其余单位是**两个独立分支**(前者按月折算、后者按天折算),
533
+ // 且后者会先把月数 `Math.round` 成整天——这一步是两家差异的主要来源
534
+ // (`duration(1,'M').asHours()`:moment 走 round(30.436875)=30 天 ⇒ 720,dayjs 直接
535
+ // $ms/36e5 ⇒ 730)。生成式矩阵实测 9720 组里 4872 组不一致,全部落在含年/月/季的换算上。
536
+ function durationAs (d, unit) {
537
+ const { ms, months } = durationBuckets(d)
538
+ if (unit === 'month' || unit === 'quarter' || unit === 'year') {
539
+ const m = months + daysToMonths(ms / 864e5)
540
+ return unit === 'month' ? m : unit === 'quarter' ? m / 3 : m / 12
541
+ }
542
+ const days = Math.round(monthsToDays(months))
543
+ switch (unit) {
544
+ case 'week': return days / 7 + ms / 6048e5
545
+ case 'day': return days + ms / 864e5
546
+ case 'hour': return days * 24 + ms / 36e5
547
+ case 'minute': return days * 1440 + ms / 6e4
548
+ case 'second': return days * 86400 + ms / 1e3
549
+ case 'millisecond': return Math.floor(days * 864e5) + ms
550
+ // ⚠️ 不要把这个 default 写成"当毫秒处理"——那就是本次要消灭的那种静默错值
551
+ // (`as('bogus')` 会安静地返回一个毫秒数)。moment 在这里也是抛 `Unknown unit`。
552
+ default: throw new Error(
553
+ `[epx-moment] \`$moment.duration(...).as('${unit}')\` 的单位无法识别。\n` +
554
+ ' 可用单位:ms/s/m/h/d/w/M/Q/y(或它们的完整名,大小写敏感:M=月、m=分钟)。'
555
+ )
556
+ }
557
+ }
558
+
559
+ // ⚠️ 大小写敏感:`M` 是月、`m` 是分钟。所以先原样查表,查不到再小写查一次
560
+ // (与 moment 的 normalizeUnits 同款:`aliases[u] || aliases[u.toLowerCase()]`)。
561
+ const DURATION_UNITS = {
562
+ ms: 'millisecond',
563
+ millisecond: 'millisecond',
564
+ milliseconds: 'millisecond',
565
+ s: 'second',
566
+ second: 'second',
567
+ seconds: 'second',
568
+ m: 'minute',
569
+ minute: 'minute',
570
+ minutes: 'minute',
571
+ h: 'hour',
572
+ hour: 'hour',
573
+ hours: 'hour',
574
+ d: 'day',
575
+ day: 'day',
576
+ days: 'day',
577
+ w: 'week',
578
+ week: 'week',
579
+ weeks: 'week',
580
+ M: 'month',
581
+ month: 'month',
582
+ months: 'month',
583
+ Q: 'quarter',
584
+ quarter: 'quarter',
585
+ quarters: 'quarter',
586
+ y: 'year',
587
+ year: 'year',
588
+ years: 'year'
589
+ }
590
+ const normalizeDurationUnit = (u) =>
591
+ (typeof u === 'string' && (DURATION_UNITS[u] || DURATION_UNITS[u.toLowerCase()])) || u
592
+
593
+ // dayjs 的 Duration 原型里**没有 asQuarters**(实测 Object.getOwnPropertyNames 里查无此名),
594
+ // moment 有——不补的话是一句读不懂的 `d.asQuarters is not a function`。
595
+ const AS_METHODS = {
596
+ asMilliseconds: 'millisecond',
597
+ asSeconds: 'second',
598
+ asMinutes: 'minute',
599
+ asHours: 'hour',
600
+ asDays: 'day',
601
+ asWeeks: 'week',
602
+ asMonths: 'month',
603
+ asQuarters: 'quarter',
604
+ asYears: 'year'
605
+ }
606
+
607
+ // 分档表本身与 moment 的 humanize.js 一致(见上面 DURATION_THRESHOLDS 那段),
608
+ // 月/年两档的月数改走 durationAs('month') ⇒ 三桶还原(裁定 R10):
609
+ // `duration(18,'M')` 从「17.99/12=1.499 → 1 年」修正成「18/12=1.5 → 2 年」,与 moment 一致。
610
+ function classifyDuration (d) {
611
+ // ⚠️ 不可解析的输入(如 dayjs.duration('abc'))内部 $ms 是 undefined,兜底成 0——
612
+ // moment 对同样的无效输入按空 duration 处理,humanize() 给"几秒";不兜底的话
613
+ // `dayjs(undefined)` 会被当成"现在",算出一个跟输入毫无关系的"57 年"。
614
+ const abs = Math.abs(d.$ms || 0)
615
+ const t = DURATION_THRESHOLDS
616
+ const seconds = Math.round(abs / 1e3)
617
+ const minutes = Math.round(abs / 6e4)
618
+ const hours = Math.round(abs / 36e5)
619
+ const days = Math.round(abs / 864e5)
620
+ // 年份必须用**未取整**的月数算,不能拿下面已取整的 months 再除 12——moment 的 as.js 里
621
+ // 'month' 与 'year' 是两个独立分支,各算各的。月数走三桶还原(见 durationBuckets)。
622
+ const monthsExact = Math.abs(durationAs(d, 'month'))
623
+ const months = Math.round(monthsExact)
624
+ const years = Math.round(monthsExact / 12)
625
+ return (
626
+ (seconds <= t.ss && ['s', seconds]) ||
627
+ (seconds < t.s && ['ss', seconds]) ||
628
+ (minutes <= 1 && ['m', 1]) ||
629
+ (minutes < t.m && ['mm', minutes]) ||
630
+ (hours <= 1 && ['h', 1]) ||
631
+ (hours < t.h && ['hh', hours]) ||
632
+ (days <= 1 && ['d', 1]) ||
633
+ (days < t.d && ['dd', days]) ||
634
+ (months <= 1 && ['M', 1]) ||
635
+ (months < t.M && ['MM', months]) ||
636
+ (years <= 1 && ['y', 1]) || ['yy', years]
637
+ )
638
+ }
639
+
640
+ function humanizeDuration (d, withSuffix) {
641
+ const [key, count] = classifyDuration(d)
642
+ const rt = dayjs.Ls[d.$l].relativeTime
643
+ const text = rt[key].replace('%d', count)
644
+ if (!withSuffix) return text
645
+ // moment 的 pastFuture(diff, output):diff > 0 ? future : past(逐字节核对过
646
+ // node_modules/moment/src/lib/locale/relative.js),diff 用的是**带符号**的原值,
647
+ // 不是 classifyDuration 里已经取绝对值那份。
648
+ return ((d.$ms || 0) > 0 ? rt.future : rt.past).replace('%s', text)
649
+ }
650
+
651
+ // dayjs 的 add/subtract/clone/locale 都经内部工厂返回全新的 Duration 实例,带的是
652
+ // 未打补丁的原型 humanize——不重新套一遍的话,`$moment.duration(60,'m').add(30,'m')
653
+ // .humanize()` 又会掉回本节要消灭的那个实现。这四个是 dayjs duration 原型里*仅有的*
654
+ // 会返回新 Duration 实例的方法(其余方法要么返回数字,要么就是 humanize 本身)。
655
+ // __patched 短路:subtract 内部会调用 add、locale 内部会调用 clone,两条路径下这个
656
+ // 补丁函数天然会在同一个新实例上被再调一次,短路成本很低,但能防止链式调用
657
+ // (.subtract().subtract()...)让包装层数无限堆叠。
658
+ function patchDuration (d) {
659
+ if (d.__patched) return d
660
+ d.__patched = true
661
+ d.humanize = (withSuffix) => humanizeDuration(d, withSuffix)
662
+ // as* 同样要按 moment 的算法重来一遍(不是 humanize 的附属,是独立的一族输出)。
663
+ d.as = (unit) => durationAs(d, normalizeDurationUnit(unit))
664
+ for (const [name, unit] of Object.entries(AS_METHODS)) d[name] = () => durationAs(d, unit)
665
+ for (const name of ['add', 'subtract', 'clone', 'locale']) {
666
+ const raw = d[name].bind(d)
667
+ d[name] = (...args) => patchDuration(raw(...args))
668
+ }
669
+ return d
670
+ }
671
+
672
+ // ⚠️ 两处入参规整,都是"静默错值"级别的坑:
673
+ // ① `args[0] ?? 0`:`dayjs.duration(null)` 会抛 `Cannot convert undefined or null to object`,
674
+ // moment 的 `duration(null)` 返回零时长。duration 的入参最常见的来源就是可空的数据库字段,
675
+ // 而 §2.2 的消费方 ①③④(页面 JS / 产品项目 / 公式引擎)都直接够得着这个静态面。
676
+ // (本仓那 2 个调用点紧挨的上一行有 `if (!duration) return null`,所以仓库自身撞不到。)
677
+ // ② **季度单位**:dayjs 的 duration 插件压根不认 'Q'/'quarter',`dayjs.duration(1,'Q').$ms`
678
+ // 实测是 **0** —— `humanize()` 说"几秒"、`asDays()` 说 0,而 moment 是 3 个月 / 91 天。
679
+ // 一整个单位静默归零。这里按 moment 的定义(1 季 = 3 个月)折成月再交给 dayjs。
680
+ const QUARTER_KEYS = ['Q', 'quarter', 'quarters']
681
+ function normalizeDurationArgs (args) {
682
+ const [input, unit] = args
683
+ if (normalizeDurationUnit(unit) === 'quarter') return [(input ?? 0) * 3, 'M']
684
+ if (input && typeof input === 'object' && !(input instanceof Date) && !Array.isArray(input)) {
685
+ const q = QUARTER_KEYS.reduce((sum, k) => sum + (input[k] || 0), 0)
686
+ if (q) {
687
+ const rest = { ...input }
688
+ for (const k of QUARTER_KEYS) delete rest[k]
689
+ return [{ ...rest, months: (rest.months || 0) + q * 3 }, ...args.slice(1)]
690
+ }
691
+ }
692
+ return [input ?? 0, ...args.slice(1)]
693
+ }
694
+ createMoment.duration = (...args) => patchDuration(dayjs.duration(...normalizeDurationArgs(args)))
695
+ // ⚠️ 这一行会改掉 **dayjs 的全局默认 locale**,进而改到 Element Plus(el-calendar 的每周
696
+ // 首日等),见文件顶部 `dayjs.locale('zh-cn')` 处的长注释与裁定 R9。moment 时代没有这个触达范围。
697
+ createMoment.locale = (...args) => dayjs.locale(...args)
698
+ createMoment.isMoment = (v) => !!(v && v._isAMomentObject === true)
699
+ createMoment.unix = (sec) => createMoment(dayjs.unix(sec))
700
+ createMoment.utc = (...args) => createMoment(dayjs.utc(...args))
701
+ createMoment.max = (...args) => createMoment(dayjs.max(args.flat().map(unwrapMaybe)))
702
+ createMoment.min = (...args) => createMoment(dayjs.min(args.flat().map(unwrapMaybe)))
703
+
704
+ // 静态面的 7 个方法(与 spec §4.3 的清单一致)。
705
+ const STATIC_ALLOWED = new Set(['duration', 'locale', 'isMoment', 'unix', 'max', 'min', 'utc'])
706
+
707
+ // ⚠️ 函数自身的固有成员 + 框架探针,必须放行、不能抛错。理由与实例面的 PASSTHROUGH 同源:
708
+ // `JSON.stringify({ m: $moment })` 会先读 `toJSON`(规范里对**任何**对象——函数也算——都先取
709
+ // 一次 toJSON)、ESM/CJS 互操作垫片会读 `default` / `__esModule`、Vue 的响应式探针读 `__v_*`。
710
+ // 这些读到 undefined 与真 moment 的行为一致(moment 上它们同样不存在),抛错则会白屏。
711
+ const STATIC_PASSTHROUGH = new Set(['name', 'length', 'prototype', 'call', 'apply', 'bind',
712
+ 'toString', 'valueOf', 'constructor', 'hasOwnProperty', 'isPrototypeOf',
713
+ 'propertyIsEnumerable', 'then', 'toJSON', 'default', '__esModule', 'nodeType', 'tagName',
714
+ '$$typeof', '__v_isRef', '__v_isReactive', '__v_isReadonly', '__v_raw', '__v_skip',
715
+ '_isAMomentObject'])
716
+
717
+ // ⚠️ 静态面也必须响亮抛错——这是 spec §4.3 明写的绑定姿态,而在此之前它是**没有的**:
718
+ // `createMoment` 是个普通函数,7 个静态方法之外的一切(`isDuration` `isDate` `months`
719
+ // `weekdays` `localeData` `ISO_8601` `parseZone` `invalid` `now` `updateLocale`
720
+ // `defaultFormat`……moment 全都有)读出来都是 `undefined`,于是
721
+ // `$moment.isDuration(x)` 直接 TypeError、`$moment.ISO_8601` 当格式串传下去变成静默错值。
722
+ // 实例面早就抛错了,静态面不抛,正是 §4.3 要消灭的那种失败模式。
723
+ // 只拦 get 的 miss 分支(命中白名单的走 Reflect.get,零额外开销);Symbol 一律放行。
724
+ const momentCompat = new Proxy(createMoment, {
725
+ get (target, prop, receiver) {
726
+ if (typeof prop === 'symbol') return Reflect.get(target, prop, receiver)
727
+ if (STATIC_ALLOWED.has(prop) || STATIC_PASSTHROUGH.has(prop)) {
728
+ return Reflect.get(target, prop, receiver)
729
+ }
730
+ throw new Error(
731
+ `[epx-moment] \`$moment.${prop}\` 不在兼容层覆盖范围内。\n` +
732
+ ' 本平台已从 moment 迁移到 dayjs,$moment 是一层兼容 shim。\n' +
733
+ ` 静态面只覆盖 ${[...STATIC_ALLOWED].join(' / ')}。\n` +
734
+ ' 需要这个 API 请补进 packages/utils/src/moment-compat.js;\n' +
735
+ ' 临时可用 $moment(x).toDate() 拿到原生 Date 自行处理。'
736
+ )
737
+ }
738
+ })
739
+
740
+ // 具名与默认导出**必须是同一个(被代理的)对象**:产品项目的构建产物里两种写法都出现过
741
+ // (packages/utils/dist/src/formula.js 走的就是具名 `{ createMoment }`),只代理其中一个
742
+ // 等于留了半扇后门。
743
+ export { momentCompat as createMoment }
744
+ export default momentCompat