npmapps 1.1.4 → 1.1.5

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 (114) hide show
  1. package/app/wujie-vue2-child/.eslintrc.js +19 -0
  2. package/app/wujie-vue2-child/README.md +24 -0
  3. package/app/wujie-vue2-child/babel.config.js +5 -0
  4. package/app/wujie-vue2-child/docs/superpowers/plans/2026-05-29-quill-editor.md +836 -0
  5. package/app/wujie-vue2-child/docs/superpowers/specs/2026-05-29-quill-editor-design.md +210 -0
  6. package/app/wujie-vue2-child/jsconfig.json +19 -0
  7. package/app/wujie-vue2-child/package-lock.json +21333 -0
  8. package/app/wujie-vue2-child/package.json +63 -0
  9. package/app/wujie-vue2-child/postcss.config.js +10 -0
  10. package/app/wujie-vue2-child/public/favicon.ico +0 -0
  11. package/app/wujie-vue2-child/public/index.html +17 -0
  12. package/app/wujie-vue2-child/public/rili.jpg +0 -0
  13. package/app/wujie-vue2-child/public//344/270/200/351/224/256/351/273/221/346/232/227.html +136 -0
  14. package/app/wujie-vue2-child/src/App.vue +110 -0
  15. package/app/wujie-vue2-child/src/assets/bpmn-camunda.jpg +0 -0
  16. package/app/wujie-vue2-child/src/assets/css/diagram.less +17 -0
  17. package/app/wujie-vue2-child/src/assets/icon/Icon.less +31 -0
  18. package/app/wujie-vue2-child/src/assets/icon/font/app-codes.css +26 -0
  19. package/app/wujie-vue2-child/src/assets/icon/font/app.eot +0 -0
  20. package/app/wujie-vue2-child/src/assets/icon/font/app.svg +60 -0
  21. package/app/wujie-vue2-child/src/assets/icon/font/app.ttf +0 -0
  22. package/app/wujie-vue2-child/src/assets/icon/font/app.woff +0 -0
  23. package/app/wujie-vue2-child/src/assets/icon/font/app.woff2 +0 -0
  24. package/app/wujie-vue2-child/src/assets/icon/font/config.json +248 -0
  25. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/align-bottom-tool.svg +30 -0
  26. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/align-horizontal-center-tool.svg +85 -0
  27. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/align-left-tool.svg +84 -0
  28. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/align-right-tool.svg +80 -0
  29. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/align-top-tool.svg +84 -0
  30. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/align-vertical-center-tool.svg +89 -0
  31. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/distribute-horizontally-tool.svg +95 -0
  32. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/distribute-vertically-tool.svg +99 -0
  33. package/app/wujie-vue2-child/src/assets/icon/font/source/raw/set-color-tool.svg +111 -0
  34. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/align-bottom-tool.svg +30 -0
  35. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/align-horizontal-center-tool.svg +30 -0
  36. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/align-left-tool.svg +30 -0
  37. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/align-right-tool.svg +30 -0
  38. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/align-top-tool.svg +30 -0
  39. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/align-vertical-center-tool.svg +30 -0
  40. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/distribute-horizontally-tool.svg +30 -0
  41. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/distribute-vertically-tool.svg +30 -0
  42. package/app/wujie-vue2-child/src/assets/icon/font/source/symbols/set-color-tool.svg +63 -0
  43. package/app/wujie-vue2-child/src/assets/logo.png +0 -0
  44. package/app/wujie-vue2-child/src/components/EllTable/README.md +70 -0
  45. package/app/wujie-vue2-child/src/components/EllTable/article.md +184 -0
  46. package/app/wujie-vue2-child/src/components/EllTable/index.js +213 -0
  47. package/app/wujie-vue2-child/src/components/EventCalendar/CalendarSkeleton.vue +147 -0
  48. package/app/wujie-vue2-child/src/components/EventCalendar/CodeBlock.vue +199 -0
  49. package/app/wujie-vue2-child/src/components/EventCalendar/MonthSelect.vue +376 -0
  50. package/app/wujie-vue2-child/src/components/EventCalendar/README.md +368 -0
  51. package/app/wujie-vue2-child/src/components/EventCalendar/example.vue +1551 -0
  52. package/app/wujie-vue2-child/src/components/EventCalendar/index.vue +1646 -0
  53. package/app/wujie-vue2-child/src/components/EventCalendar/mockData.js +349 -0
  54. package/app/wujie-vue2-child/src/components/EventCalendar/utils.js +201 -0
  55. package/app/wujie-vue2-child/src/components/FormulaEditor/FunctionSelector.vue +123 -0
  56. package/app/wujie-vue2-child/src/components/FormulaEditor/OperatorSelector.vue +184 -0
  57. package/app/wujie-vue2-child/src/components/FormulaEditor/ParameterSelector.vue +123 -0
  58. package/app/wujie-vue2-child/src/components/FormulaEditor/api.js +69 -0
  59. package/app/wujie-vue2-child/src/components/FormulaEditor/index.vue +435 -0
  60. package/app/wujie-vue2-child/src/components/HelloWorld.vue +58 -0
  61. package/app/wujie-vue2-child/src/components/PageHeader/index.vue +158 -0
  62. package/app/wujie-vue2-child/src/components/Splitter/README.md +144 -0
  63. package/app/wujie-vue2-child/src/components/Splitter/example.vue +84 -0
  64. package/app/wujie-vue2-child/src/components/Splitter/index.vue +203 -0
  65. package/app/wujie-vue2-child/src/components/diagram/ToolBar.vue +357 -0
  66. package/app/wujie-vue2-child/src/components/diagram/customTranslate/customTranslate.js +12 -0
  67. package/app/wujie-vue2-child/src/components/diagram/customTranslate/translationsGerman.js +241 -0
  68. package/app/wujie-vue2-child/src/components/diagram/index.vue +261 -0
  69. package/app/wujie-vue2-child/src/components/diagram/xmlData.js +29 -0
  70. package/app/wujie-vue2-child/src/directives/filldown.js +155 -0
  71. package/app/wujie-vue2-child/src/directives/filldownTable.js +291 -0
  72. package/app/wujie-vue2-child/src/main.js +40 -0
  73. package/app/wujie-vue2-child/src/router/index.js +72 -0
  74. package/app/wujie-vue2-child/src/store/index.js +23 -0
  75. package/app/wujie-vue2-child/src/utils/winBox.js +23 -0
  76. package/app/wujie-vue2-child/src/views/Extend/A.vue +12 -0
  77. package/app/wujie-vue2-child/src/views/Extend/B.vue +10 -0
  78. package/app/wujie-vue2-child/src/views/Extend/MagicalComponentsForELFormItem.vue +78 -0
  79. package/app/wujie-vue2-child/src/views/Extend/index.vue +59 -0
  80. package/app/wujie-vue2-child/src/views/Extend/tableMouseHorizontalWheel.vue +184 -0
  81. package/app/wujie-vue2-child/src/views/Home.vue +27 -0
  82. package/app/wujie-vue2-child/src/views/RouterJump.vue +145 -0
  83. package/app/wujie-vue2-child/src/views/calendar/index.vue +19 -0
  84. package/app/wujie-vue2-child/src/views/css.vue +57 -0
  85. package/app/wujie-vue2-child/src/views/cssComponents/EllipsisText.vue +73 -0
  86. package/app/wujie-vue2-child/src/views/cssComponents/HoverCard.vue +69 -0
  87. package/app/wujie-vue2-child/src/views/cssComponents/TableHover.vue +130 -0
  88. package/app/wujie-vue2-child/src/views/cssComponents/inputSlo.vue +42 -0
  89. package/app/wujie-vue2-child/src/views/cssComponents/tableFixed.vue +148 -0
  90. package/app/wujie-vue2-child/src/views/echarts/echart-dome.vue +73 -0
  91. package/app/wujie-vue2-child/src/views/echarts/index.vue +118 -0
  92. package/app/wujie-vue2-child/src/views/echarts/pei3d.vue +658 -0
  93. package/app/wujie-vue2-child/src/views/element/bpmn/index.vue +15 -0
  94. package/app/wujie-vue2-child/src/views/element/components/attendanceCycle/index.vue +131 -0
  95. package/app/wujie-vue2-child/src/views/element/components/attendanceGroup/index.vue +147 -0
  96. package/app/wujie-vue2-child/src/views/element/components/attendancePersonnel/index.vue +158 -0
  97. package/app/wujie-vue2-child/src/views/element/components/companyCalendar/index.vue +147 -0
  98. package/app/wujie-vue2-child/src/views/element/components/shift/index.vue +147 -0
  99. package/app/wujie-vue2-child/src/views/element/components/shiftRotationSystem/index.vue +147 -0
  100. package/app/wujie-vue2-child/src/views/element/elTableJsx/columnManagement.vue +340 -0
  101. package/app/wujie-vue2-child/src/views/element/elTableJsx/dialogInput.vue +71 -0
  102. package/app/wujie-vue2-child/src/views/element/elTableJsx/elTableJsx.vue +1826 -0
  103. package/app/wujie-vue2-child/src/views/element/elTableJsx/formTable.vue +595 -0
  104. package/app/wujie-vue2-child/src/views/element/elTableJsx/index.vue +29 -0
  105. package/app/wujie-vue2-child/src/views/element/elTableJsx/simpleTable.vue +189 -0
  106. package/app/wujie-vue2-child/src/views/element/elTableJsx.zip +0 -0
  107. package/app/wujie-vue2-child/src/views/element/index.vue +39 -0
  108. package/app/wujie-vue2-child/src/views/element/quillEditor/README.md +163 -0
  109. package/app/wujie-vue2-child/src/views/element/quillEditor/example.vue +307 -0
  110. package/app/wujie-vue2-child/src/views/element/quillEditor/index.vue +409 -0
  111. package/app/wujie-vue2-child/src/views/element/quillEditor/toolbar.js +122 -0
  112. package/app/wujie-vue2-child/vue.config.js +15 -0
  113. package/package.json +1 -1
  114. package/app/check-import-case.js +0 -344
@@ -0,0 +1,1646 @@
1
+ <template>
2
+ <div
3
+ class="ec"
4
+ :class="rootClass"
5
+ :style="cssVars"
6
+ @mouseenter="handleMouseEnter"
7
+ @mouseleave="handleMouseLeave"
8
+ >
9
+ <!-- ==================== 顶部区域 ==================== -->
10
+ <div v-if="showHeader" class="ec__header">
11
+ <!-- 左侧:完全交给使用者的插槽,未传时回落到 title / subtitle -->
12
+ <div class="ec__header-left">
13
+ <slot name="header-left" v-bind="headerScope">
14
+ <div v-if="title || subtitle" class="ec__title-group">
15
+ <h3 v-if="title" class="ec__title">{{ title }}</h3>
16
+ <span v-if="subtitle" class="ec__subtitle">{{ subtitle }}</span>
17
+ </div>
18
+ </slot>
19
+ </div>
20
+
21
+ <!-- 右侧:默认是「轮播开关 + 翻月箭头 + 月份下拉」 -->
22
+ <div class="ec__header-right">
23
+ <slot name="header-right" v-bind="headerScope">
24
+ <button
25
+ v-if="autoplay && showAutoplayToggle"
26
+ type="button"
27
+ class="ec__icon-btn"
28
+ :title="manualPaused ? '继续轮播' : '暂停轮播'"
29
+ :aria-label="manualPaused ? '继续轮播' : '暂停轮播'"
30
+ @click="toggleAutoplay"
31
+ >
32
+ <svg viewBox="0 0 16 16" aria-hidden="true">
33
+ <path v-if="manualPaused" d="M5 3.5 L12 8 L5 12.5 Z" fill="currentColor" />
34
+ <g v-else fill="currentColor">
35
+ <rect x="4.5" y="3.5" width="2.4" height="9" rx="0.8" />
36
+ <rect x="9.1" y="3.5" width="2.4" height="9" rx="0.8" />
37
+ </g>
38
+ </svg>
39
+ </button>
40
+
41
+ <template v-if="showArrows">
42
+ <button
43
+ type="button"
44
+ class="ec__icon-btn"
45
+ title="上一个月"
46
+ aria-label="上一个月"
47
+ :disabled="!canGoPrev"
48
+ @click="prev()"
49
+ >
50
+ <svg viewBox="0 0 16 16" aria-hidden="true">
51
+ <path d="M10 3 L5 8 L10 13" fill="none" stroke="currentColor" stroke-width="1.7"
52
+ stroke-linecap="round" stroke-linejoin="round" />
53
+ </svg>
54
+ </button>
55
+ <button
56
+ type="button"
57
+ class="ec__icon-btn"
58
+ title="下一个月"
59
+ aria-label="下一个月"
60
+ :disabled="!canGoNext"
61
+ @click="next()"
62
+ >
63
+ <svg viewBox="0 0 16 16" aria-hidden="true">
64
+ <path d="M6 3 L11 8 L6 13" fill="none" stroke="currentColor" stroke-width="1.7"
65
+ stroke-linecap="round" stroke-linejoin="round" />
66
+ </svg>
67
+ </button>
68
+ </template>
69
+
70
+ <MonthSelect
71
+ v-if="showMonthSelect"
72
+ :value="innerMonth"
73
+ :options="monthOptions"
74
+ :disabled="loading || !monthOptions.length"
75
+ placeholder="选择月份"
76
+ @change="handleSelectChange"
77
+ @visible-change="handleSelectVisibleChange"
78
+ />
79
+ </slot>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- 轮播进度条:直观体现「下一次自动切换还有多久」 -->
84
+ <div v-if="showProgressBar" class="ec__progress">
85
+ <div
86
+ :key="progressKey"
87
+ class="ec__progress-inner"
88
+ :style="progressStyle"
89
+ ></div>
90
+ </div>
91
+
92
+ <!-- ==================== 星期表头 ==================== -->
93
+ <div class="ec__weekdays" role="row">
94
+ <div
95
+ v-for="(w, index) in weekHeaders"
96
+ :key="w.weekday"
97
+ class="ec__weekday"
98
+ :class="{ 'is-weekend': w.isWeekend }"
99
+ role="columnheader"
100
+ >
101
+ <slot name="weekday" v-bind="{ ...w, index }">{{ w.label }}</slot>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- ==================== 主体(骨架屏 / 日历网格) ==================== -->
106
+ <div class="ec__stage" :style="stageStyle">
107
+ <transition
108
+ :name="transitionName"
109
+ @before-enter="handleBeforeEnter"
110
+ @enter="handleEnter"
111
+ @after-enter="handleAfterEnter"
112
+ >
113
+ <!-- 加载中:骨架屏 -->
114
+ <div v-if="loading" key="__skeleton__" class="ec__panel">
115
+ <slot name="loading" v-bind="{ rows: skeletonRows }">
116
+ <CalendarSkeleton
117
+ :rows="skeletonRows"
118
+ :min-row-height="minRowHeight"
119
+ :item-height="itemHeight"
120
+ />
121
+ </slot>
122
+ </div>
123
+
124
+ <!-- 无任何月份数据:空状态 -->
125
+ <div v-else-if="!monthOptions.length" key="__empty__" class="ec__panel">
126
+ <slot name="empty">
127
+ <div class="ec__empty">
128
+ <svg class="ec__empty-icon" viewBox="0 0 64 64" aria-hidden="true">
129
+ <rect x="8" y="14" width="48" height="42" rx="6" fill="none"
130
+ stroke="currentColor" stroke-width="2.5" />
131
+ <path d="M8 26 H56" stroke="currentColor" stroke-width="2.5" />
132
+ <path d="M20 8 V18 M44 8 V18" stroke="currentColor" stroke-width="2.5"
133
+ stroke-linecap="round" />
134
+ </svg>
135
+ <p class="ec__empty-text">{{ emptyText }}</p>
136
+ </div>
137
+ </slot>
138
+ </div>
139
+
140
+ <!-- 正常渲染 -->
141
+ <div v-else :key="innerMonth" class="ec__panel">
142
+ <div class="ec__grid" role="grid">
143
+ <div
144
+ v-for="(cell, index) in renderCells"
145
+ :key="cell.dateKey"
146
+ class="ec__cell"
147
+ :class="cellClass(cell)"
148
+ :style="{ '--ec-i': index }"
149
+ role="gridcell"
150
+ :tabindex="cellFocusable ? 0 : -1"
151
+ :aria-label="cell.dateKey"
152
+ @click="handleDayClick(cell, $event)"
153
+ @keydown.enter.self="handleDayClick(cell, $event)"
154
+ @keydown.space.prevent.self="handleDayClick(cell, $event)"
155
+ >
156
+ <!-- 整格自定义 -->
157
+ <slot name="day" v-bind="cellScope(cell)">
158
+ <!-- 日期行 -->
159
+ <div class="ec__day-head">
160
+ <slot name="day-header" v-bind="cellScope(cell)">
161
+ <span class="ec__day-label">{{ dayFormatter(cell) }}</span>
162
+ <span v-if="cell.isToday && highlightToday" class="ec__today-dot"></span>
163
+ </slot>
164
+ </div>
165
+
166
+ <!-- 条目列表 -->
167
+ <div class="ec__items-wrap">
168
+ <slot name="items" v-bind="cellScope(cell)">
169
+ <div
170
+ v-if="cell.items.length"
171
+ class="ec__items"
172
+ :class="{ 'is-scrollable': cell.scrollable }"
173
+ :style="itemsStyle"
174
+ >
175
+ <div
176
+ v-for="(entry, i) in cell.visibleItems"
177
+ :key="entry.key"
178
+ class="ec__item"
179
+ :class="{
180
+ 'is-disabled': entry.disabled,
181
+ 'has-badge': !!entry.badge,
182
+ 'is-colorful': colorful && entry.type
183
+ }"
184
+ :style="itemStyle(entry)"
185
+ :title="entry.label"
186
+ @click.stop="handleItemClick(entry, cell, i, $event)"
187
+ >
188
+ <!-- 单条自定义 -->
189
+ <slot name="item" v-bind="itemScope(entry, cell, i)">
190
+ <span class="ec__item-label">{{ entry.label }}</span>
191
+ <span
192
+ v-if="entry.badge"
193
+ class="ec__item-badge"
194
+ :style="badgeStyle(entry)"
195
+ @click.stop="handleBadgeClick(entry, cell, i, $event)"
196
+ >{{ entry.badge }}</span>
197
+ </slot>
198
+ </div>
199
+
200
+ <!-- overflowMode='collapse' 时的「更多」提示 -->
201
+ <div
202
+ v-if="cell.moreCount > 0"
203
+ class="ec__more"
204
+ @click.stop="handleMoreClick(cell, $event)"
205
+ >还有 {{ cell.moreCount }} 项</div>
206
+ </div>
207
+
208
+ <!-- 当天无数据 -->
209
+ <slot v-else name="day-empty" v-bind="cellScope(cell)"></slot>
210
+ </slot>
211
+ </div>
212
+ </slot>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </transition>
217
+ </div>
218
+ </div>
219
+ </template>
220
+
221
+ <script>
222
+ /**
223
+ * EventCalendar —— 月历 / 日程日历组件
224
+ * ==================================================================
225
+ * 特性一览:
226
+ * 1. 月份网格自动补全:上个月末尾 + 下个月开头,跨年正确,非当月置灰;
227
+ * 2. 月份下拉切换 + 自动轮播(间隔可配,支持 hover/下拉/切后台自动暂停);
228
+ * 3. 4 种切换动效(slide / fade / zoom / flip)与逐格入场,均可关闭;
229
+ * 4. 字段名全部可映射(field-names),不用为后端字段改数据;
230
+ * 5. 行高按「当行条目最多的那天」自动撑开,超过上限出现滚动条;
231
+ * 6. 骨架屏、空状态、日期点击、条目点击、角标点击、丰富插槽。
232
+ *
233
+ * 组件不依赖任何第三方库,整个文件夹可直接拷贝到其它项目使用。
234
+ */
235
+ import MonthSelect from './MonthSelect.vue'
236
+ import CalendarSkeleton from './CalendarSkeleton.vue'
237
+ import {
238
+ DEFAULT_FIELD_NAMES,
239
+ DEFAULT_WEEK_LABELS,
240
+ buildMonthMatrix,
241
+ defaultDayFormatter,
242
+ defaultMonthFormatter,
243
+ formatMonthKey,
244
+ getByPath
245
+ } from './utils'
246
+
247
+ /** 内置类型配色,可被 typeColors prop 覆盖或扩展 */
248
+ const BUILTIN_TYPE_COLORS = {
249
+ talk: { color: '#3d6cf0', bg: 'rgba(61, 108, 240, 0.10)' },
250
+ fair: { color: '#e0821b', bg: 'rgba(224, 130, 27, 0.12)' },
251
+ live: { color: '#10a37f', bg: 'rgba(16, 163, 127, 0.12)' },
252
+ exam: { color: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.12)' },
253
+ default: { color: '#6b7280', bg: 'rgba(107, 114, 128, 0.10)' }
254
+ }
255
+
256
+ export default {
257
+ name: 'EventCalendar',
258
+
259
+ components: { MonthSelect, CalendarSkeleton },
260
+
261
+ // 同时支持 v-model 与 :active-month.sync
262
+ model: {
263
+ prop: 'activeMonth',
264
+ event: 'update:activeMonth'
265
+ },
266
+
267
+ props: {
268
+ /* ----------------------------- 数据 ----------------------------- */
269
+ /**
270
+ * 月份数据数组,默认结构:
271
+ * [{ month: '2026-09', monthLabel: '2026年9月',
272
+ * days: [{ date: '2026-09-07', items: [{ id, label, badge, type }] }] }]
273
+ * 也支持直接传扁平的日期数组(自动按日期归月)。
274
+ */
275
+ data: {
276
+ type: Array,
277
+ default: () => []
278
+ },
279
+ /** 字段映射,覆盖 DEFAULT_FIELD_NAMES 中的任意项,支持 'a.b' 点路径 */
280
+ fieldNames: {
281
+ type: Object,
282
+ default: () => ({})
283
+ },
284
+ /** 加载中(显示骨架屏) */
285
+ loading: {
286
+ type: Boolean,
287
+ default: false
288
+ },
289
+ /** 骨架屏行数 */
290
+ skeletonRows: {
291
+ type: Number,
292
+ default: 5
293
+ },
294
+
295
+ /* --------------------------- 月份与轮播 --------------------------- */
296
+ /** 当前月份 'YYYY-MM',支持 v-model / .sync;不传则取第一个月 */
297
+ activeMonth: {
298
+ type: String,
299
+ default: ''
300
+ },
301
+ /** 是否展示月份下拉 */
302
+ showMonthSelect: {
303
+ type: Boolean,
304
+ default: true
305
+ },
306
+ /** 是否展示左右翻月箭头 */
307
+ showArrows: {
308
+ type: Boolean,
309
+ default: false
310
+ },
311
+ /** 开启自动轮播 */
312
+ autoplay: {
313
+ type: Boolean,
314
+ default: false
315
+ },
316
+ /** 轮播间隔(毫秒),如 10000=10 秒、60000=1 分钟 */
317
+ autoplayInterval: {
318
+ type: Number,
319
+ default: 60000
320
+ },
321
+ /** 轮播到最后一个月后是否回到第一个 */
322
+ loop: {
323
+ type: Boolean,
324
+ default: true
325
+ },
326
+ /** 鼠标悬停时暂停轮播 */
327
+ pauseOnHover: {
328
+ type: Boolean,
329
+ default: true
330
+ },
331
+ /** 展示轮播倒计时进度条 */
332
+ showProgress: {
333
+ type: Boolean,
334
+ default: true
335
+ },
336
+ /** 展示轮播播放/暂停按钮 */
337
+ showAutoplayToggle: {
338
+ type: Boolean,
339
+ default: true
340
+ },
341
+
342
+ /* ----------------------------- 动效 ----------------------------- */
343
+ /** 月份切换动效:slide | fade | zoom | flip | none */
344
+ transitionEffect: {
345
+ type: String,
346
+ default: 'slide',
347
+ validator: (v) => ['slide', 'fade', 'zoom', 'flip', 'none'].includes(v)
348
+ },
349
+ /** 动效时长(毫秒) */
350
+ transitionDuration: {
351
+ type: Number,
352
+ default: 420
353
+ },
354
+ /** 单元格逐个入场的错峰动画 */
355
+ cellStagger: {
356
+ type: Boolean,
357
+ default: false
358
+ },
359
+
360
+ /* ----------------------------- 渲染 ----------------------------- */
361
+ /** 顶部区域是否显示 */
362
+ showHeader: {
363
+ type: Boolean,
364
+ default: true
365
+ },
366
+ /** 默认标题(header-left 插槽未传时生效) */
367
+ title: {
368
+ type: String,
369
+ default: ''
370
+ },
371
+ /** 默认副标题 */
372
+ subtitle: {
373
+ type: String,
374
+ default: ''
375
+ },
376
+ /** 一周起始日:0=周日(设计图默认)1=周一 */
377
+ firstDayOfWeek: {
378
+ type: Number,
379
+ default: 0,
380
+ validator: (v) => v >= 0 && v <= 6
381
+ },
382
+ /** 星期文案,下标对应 Date.getDay() */
383
+ weekLabels: {
384
+ type: Array,
385
+ default: () => DEFAULT_WEEK_LABELS
386
+ },
387
+ /** 单日最多显示的条目数,超出按 overflowMode 处理 */
388
+ maxVisibleItems: {
389
+ type: Number,
390
+ default: 4
391
+ },
392
+ /** 超出处理:scroll=出现滚动条 | collapse=折叠并显示「还有 N 项」 */
393
+ overflowMode: {
394
+ type: String,
395
+ default: 'scroll',
396
+ validator: (v) => ['scroll', 'collapse'].includes(v)
397
+ },
398
+ /** 单个条目高度(px),用于精确计算滚动区域高度 */
399
+ itemHeight: {
400
+ type: Number,
401
+ default: 28
402
+ },
403
+ /** 条目间距(px) */
404
+ itemGap: {
405
+ type: Number,
406
+ default: 6
407
+ },
408
+ /** 单元格最小高度(px),保证空行不塌陷 */
409
+ minRowHeight: {
410
+ type: Number,
411
+ default: 118
412
+ },
413
+ /** 相邻月份补位格子是否也渲染条目 */
414
+ showAdjacentItems: {
415
+ type: Boolean,
416
+ default: true
417
+ },
418
+ /** 高亮今天 */
419
+ highlightToday: {
420
+ type: Boolean,
421
+ default: true
422
+ },
423
+ /** 条目角标是否按 type 上色,false 则统一中性灰(更贴近原设计稿) */
424
+ colorful: {
425
+ type: Boolean,
426
+ default: true
427
+ },
428
+ /** 类型配色扩展/覆盖:{ talk: { color, bg } } */
429
+ typeColors: {
430
+ type: Object,
431
+ default: () => ({})
432
+ },
433
+ /** 日期文案格式化函数 (cell) => string */
434
+ dayFormatter: {
435
+ type: Function,
436
+ default: defaultDayFormatter
437
+ },
438
+ /** 月份下拉文案格式化函数 (monthKey, rawMonthItem) => string */
439
+ monthFormatter: {
440
+ type: Function,
441
+ default: null
442
+ },
443
+ /** 空数据文案 */
444
+ emptyText: {
445
+ type: String,
446
+ default: '暂无日程数据'
447
+ },
448
+ /** 单元格是否可获得焦点(键盘无障碍) */
449
+ cellFocusable: {
450
+ type: Boolean,
451
+ default: false
452
+ },
453
+ /** 主题变量覆盖,如 { primary: '#10a37f', radius: '12px' } */
454
+ theme: {
455
+ type: Object,
456
+ default: () => ({})
457
+ }
458
+ },
459
+
460
+ data() {
461
+ return {
462
+ /** 组件内部维护的当前月份 */
463
+ innerMonth: '',
464
+ /** 切换方向:next / prev,决定动画走向 */
465
+ direction: 'next',
466
+ /** 舞台高度,切换月份时用于高度过渡(行数可能 5 行变 6 行) */
467
+ stageHeight: '',
468
+ /** 用户主动点了暂停 */
469
+ manualPaused: false,
470
+ /** 鼠标悬停中 */
471
+ hovering: false,
472
+ /** 月份下拉展开中 */
473
+ selectOpen: false,
474
+ /** 页面切到后台 */
475
+ pageHidden: false,
476
+ /** 每次切换 +1,用于重置进度条 CSS 动画 */
477
+ progressKey: 0,
478
+ /** 骨架屏 -> 内容 的首次切换用淡入,避免莫名其妙的滑动 */
479
+ useFadeOnce: true
480
+ }
481
+ },
482
+
483
+ computed: {
484
+ /* --------------------------- 字段映射 --------------------------- */
485
+ fields() {
486
+ return { ...DEFAULT_FIELD_NAMES, ...this.fieldNames }
487
+ },
488
+
489
+ /* --------------------------- 数据归一化 --------------------------- */
490
+ /**
491
+ * 把外部数据统一整理成 [{ month, label, raw }],
492
+ * 同时兼容「扁平日期数组」的写法。
493
+ */
494
+ normalizedMonths() {
495
+ const f = this.fields
496
+ const list = Array.isArray(this.data) ? this.data : []
497
+ if (!list.length) return []
498
+
499
+ // 扁平结构判定:第一项没有 days 字段但有 date 字段
500
+ const isFlat = getByPath(list[0], f.days) === undefined &&
501
+ getByPath(list[0], f.date) !== undefined
502
+
503
+ if (isFlat) {
504
+ const monthSet = []
505
+ list.forEach((day) => {
506
+ const monthKey = formatMonthKey(getByPath(day, f.date))
507
+ if (monthKey && !monthSet.includes(monthKey)) monthSet.push(monthKey)
508
+ })
509
+ return monthSet.sort().map((monthKey) => ({
510
+ month: monthKey,
511
+ label: this.resolveMonthLabel(monthKey, null),
512
+ raw: null
513
+ }))
514
+ }
515
+
516
+ return list
517
+ .map((item) => {
518
+ const monthKey = String(getByPath(item, f.month) || '')
519
+ if (!monthKey) return null
520
+ return {
521
+ month: monthKey,
522
+ label: this.resolveMonthLabel(monthKey, item),
523
+ raw: item
524
+ }
525
+ })
526
+ .filter(Boolean)
527
+ },
528
+
529
+ /** 月份下拉选项 */
530
+ monthOptions() {
531
+ return this.normalizedMonths.map((m) => ({ label: m.label, value: m.month }))
532
+ },
533
+
534
+ /** 'YYYY-MM-DD' -> 该日的条目数组(扁平化整份数据,便于补位格子取数) */
535
+ dateItemsMap() {
536
+ const f = this.fields
537
+ const map = Object.create(null)
538
+ const list = Array.isArray(this.data) ? this.data : []
539
+ if (!list.length) return map
540
+
541
+ const collectDay = (day) => {
542
+ const dateKey = String(getByPath(day, f.date) || '').slice(0, 10)
543
+ if (!dateKey) return
544
+ const items = getByPath(day, f.items)
545
+ if (!map[dateKey]) map[dateKey] = { items: [], raw: day }
546
+ if (Array.isArray(items)) map[dateKey].items.push(...items)
547
+ }
548
+
549
+ const isFlat = getByPath(list[0], f.days) === undefined &&
550
+ getByPath(list[0], f.date) !== undefined
551
+
552
+ if (isFlat) {
553
+ list.forEach(collectDay)
554
+ } else {
555
+ list.forEach((monthItem) => {
556
+ const days = getByPath(monthItem, f.days)
557
+ if (Array.isArray(days)) days.forEach(collectDay)
558
+ })
559
+ }
560
+ return map
561
+ },
562
+
563
+ /** 当前月份在下拉列表中的索引 */
564
+ currentIndex() {
565
+ return this.monthOptions.findIndex((o) => o.value === this.innerMonth)
566
+ },
567
+
568
+ /* --------------------------- 网格构建 --------------------------- */
569
+ /** 表头星期,按 firstDayOfWeek 旋转 */
570
+ weekHeaders() {
571
+ const labels = this.weekLabels.length === 7 ? this.weekLabels : DEFAULT_WEEK_LABELS
572
+ return Array.from({ length: 7 }, (_, i) => {
573
+ const weekday = (this.firstDayOfWeek + i) % 7
574
+ return {
575
+ weekday,
576
+ label: labels[weekday],
577
+ isWeekend: weekday === 0 || weekday === 6
578
+ }
579
+ })
580
+ },
581
+
582
+ /**
583
+ * 最终渲染的格子:日期矩阵 + 当天条目(已按 maxVisibleItems 处理溢出)
584
+ */
585
+ renderCells() {
586
+ if (!this.innerMonth) return []
587
+ const f = this.fields
588
+ const cells = buildMonthMatrix(this.innerMonth, this.firstDayOfWeek)
589
+
590
+ return cells.map((cell) => {
591
+ const hit = this.dateItemsMap[cell.dateKey]
592
+ const showItems = cell.isCurrentMonth || this.showAdjacentItems
593
+ const rawItems = showItems && hit ? hit.items : []
594
+
595
+ // 归一化条目,模板里直接用,避免反复取值
596
+ const items = rawItems.map((raw, i) => ({
597
+ key: getByPath(raw, f.itemKey) != null
598
+ ? String(getByPath(raw, f.itemKey))
599
+ : `${cell.dateKey}-${i}`,
600
+ label: getByPath(raw, f.itemLabel),
601
+ badge: getByPath(raw, f.itemBadge),
602
+ type: getByPath(raw, f.itemType),
603
+ disabled: !!getByPath(raw, f.itemDisabled),
604
+ raw
605
+ }))
606
+
607
+ const overflowed = items.length > this.maxVisibleItems
608
+ const collapse = this.overflowMode === 'collapse' && overflowed
609
+ // collapse 模式留一行给「还有 N 项」
610
+ const visibleItems = collapse ? items.slice(0, this.maxVisibleItems - 1) : items
611
+
612
+ return {
613
+ ...cell,
614
+ dayRaw: hit ? hit.raw : null,
615
+ items,
616
+ visibleItems,
617
+ moreCount: collapse ? items.length - visibleItems.length : 0,
618
+ /** scroll 模式下是否真的会出现滚动条 */
619
+ scrollable: this.overflowMode === 'scroll' && overflowed
620
+ }
621
+ })
622
+ },
623
+
624
+ /* --------------------------- 样式相关 --------------------------- */
625
+ rootClass() {
626
+ return {
627
+ 'ec--loading': this.loading,
628
+ 'ec--stagger': this.cellStagger && !this.loading,
629
+ [`ec--effect-${this.transitionEffect}`]: true
630
+ }
631
+ },
632
+
633
+ /** 主题:全部走 CSS 变量,方便外部覆盖 */
634
+ cssVars() {
635
+ const t = this.theme || {}
636
+ const n = this.maxVisibleItems
637
+ return {
638
+ '--ec-primary': t.primary || '#3d6cf0',
639
+ '--ec-primary-ghost': t.primaryGhost || 'rgba(61, 108, 240, 0.12)',
640
+ '--ec-border': t.border || '#e4e7ed',
641
+ '--ec-border-light': t.borderLight || '#eef0f4',
642
+ '--ec-text': t.text || '#303133',
643
+ '--ec-text-secondary': t.textSecondary || '#606266',
644
+ '--ec-text-placeholder': t.textPlaceholder || '#c0c4cc',
645
+ '--ec-muted': t.muted || '#c8ccd4',
646
+ '--ec-item-bg': t.itemBg || '#f5f7fa',
647
+ '--ec-cell-hover': t.cellHover || '#fafbfd',
648
+ '--ec-radius': t.radius || '8px',
649
+ '--ec-duration': this.transitionDuration + 'ms',
650
+ '--ec-item-h': this.itemHeight + 'px',
651
+ '--ec-item-gap': this.itemGap + 'px',
652
+ '--ec-min-row-h': this.minRowHeight + 'px',
653
+ // 滚动区域最大高度 = n 个条目 + (n-1) 个间距
654
+ '--ec-items-max-h': `${n * this.itemHeight + (n - 1) * this.itemGap}px`
655
+ }
656
+ },
657
+
658
+ stageStyle() {
659
+ return this.stageHeight ? { height: this.stageHeight } : {}
660
+ },
661
+
662
+ itemsStyle() {
663
+ // collapse 模式不限制高度(本来就已经截断了),scroll 模式限高触发滚动条
664
+ return this.overflowMode === 'scroll'
665
+ ? { maxHeight: 'var(--ec-items-max-h)' }
666
+ : {}
667
+ },
668
+
669
+ /** 当前生效的过渡名 */
670
+ transitionName() {
671
+ if (this.transitionEffect === 'none') return 'ec-none'
672
+ if (this.useFadeOnce) return 'ec-fade'
673
+ if (this.transitionEffect === 'slide' || this.transitionEffect === 'flip') {
674
+ return `ec-${this.transitionEffect}-${this.direction}`
675
+ }
676
+ return `ec-${this.transitionEffect}`
677
+ },
678
+
679
+ /* --------------------------- 轮播状态 --------------------------- */
680
+ /** 任一条件成立即暂停 */
681
+ isPaused() {
682
+ return (
683
+ this.manualPaused ||
684
+ this.pageHidden ||
685
+ this.selectOpen ||
686
+ (this.pauseOnHover && this.hovering)
687
+ )
688
+ },
689
+
690
+ /** 轮播真正可运行的条件 */
691
+ autoplayRunnable() {
692
+ return this.autoplay && !this.loading && this.monthOptions.length > 1
693
+ },
694
+
695
+ showProgressBar() {
696
+ return this.autoplay && this.showProgress && this.autoplayRunnable
697
+ },
698
+
699
+ progressStyle() {
700
+ return {
701
+ animationDuration: this.autoplayInterval + 'ms',
702
+ animationPlayState: this.isPaused ? 'paused' : 'running'
703
+ }
704
+ },
705
+
706
+ canGoPrev() {
707
+ return this.loop ? this.monthOptions.length > 1 : this.currentIndex > 0
708
+ },
709
+
710
+ canGoNext() {
711
+ return this.loop
712
+ ? this.monthOptions.length > 1
713
+ : this.currentIndex > -1 && this.currentIndex < this.monthOptions.length - 1
714
+ },
715
+
716
+ /** 顶部插槽可用的上下文 */
717
+ headerScope() {
718
+ return {
719
+ month: this.innerMonth,
720
+ monthLabel: (this.monthOptions[this.currentIndex] || {}).label || '',
721
+ monthIndex: this.currentIndex,
722
+ monthCount: this.monthOptions.length,
723
+ monthOptions: this.monthOptions,
724
+ loading: this.loading,
725
+ paused: this.isPaused,
726
+ prev: this.prev,
727
+ next: this.next,
728
+ goTo: this.goTo,
729
+ toggleAutoplay: this.toggleAutoplay
730
+ }
731
+ }
732
+ },
733
+
734
+ watch: {
735
+ /** 外部改月份(v-model / .sync) */
736
+ activeMonth(val) {
737
+ if (val && val !== this.innerMonth) this.setMonth(val, 'prop')
738
+ },
739
+
740
+ /** 数据变化后校正当前月份(比如接口返回了新的月份列表) */
741
+ monthOptions: {
742
+ immediate: true,
743
+ handler() {
744
+ this.ensureValidMonth()
745
+ }
746
+ },
747
+
748
+ loading(val) {
749
+ // 加载结束后重新起播,并让「骨架屏 -> 内容」用淡入
750
+ if (val) {
751
+ this.useFadeOnce = true
752
+ this.stopAutoplay()
753
+ } else {
754
+ this.restartAutoplay()
755
+ }
756
+ },
757
+
758
+ isPaused(val) {
759
+ if (val) this.pauseAutoplay()
760
+ else this.resumeAutoplay()
761
+ },
762
+
763
+ autoplay() {
764
+ this.restartAutoplay()
765
+ },
766
+
767
+ autoplayInterval() {
768
+ this.restartAutoplay()
769
+ }
770
+ },
771
+
772
+ created() {
773
+ // 计时器相关的中间状态不需要响应式,挂在实例上即可
774
+ this._timer = null
775
+ this._tickStart = 0
776
+ this._remaining = 0
777
+ this._ro = null
778
+ },
779
+
780
+ mounted() {
781
+ document.addEventListener('visibilitychange', this.handleVisibilityChange)
782
+ window.addEventListener('resize', this.syncStageHeight)
783
+ this.$nextTick(() => {
784
+ this.observeStage(this.$el.querySelector('.ec__panel'))
785
+ })
786
+ this.restartAutoplay()
787
+ },
788
+
789
+ beforeDestroy() {
790
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange)
791
+ window.removeEventListener('resize', this.syncStageHeight)
792
+ this.stopAutoplay()
793
+ this.disconnectObserver()
794
+ },
795
+
796
+ methods: {
797
+ /* ========================= 月份切换 ========================= */
798
+ /**
799
+ * 切换到指定月份
800
+ * @param {String} month 'YYYY-MM'
801
+ * @param {String} source 触发来源:select / arrow / autoplay / prop / api
802
+ */
803
+ setMonth(month, source = 'api') {
804
+ if (!month || month === this.innerMonth) return
805
+
806
+ const from = this.currentIndex
807
+ const to = this.monthOptions.findIndex((o) => o.value === month)
808
+
809
+ // 决定动画方向:轮播从末尾回到开头时仍按「向后」处理,视觉更连贯
810
+ if (from > -1 && to > -1) {
811
+ const wrapForward = source === 'autoplay' && to < from
812
+ this.direction = to > from || wrapForward ? 'next' : 'prev'
813
+ }
814
+
815
+ // 第一次从骨架屏切到内容用淡入,之后才用配置的动效
816
+ if (this.innerMonth) this.useFadeOnce = false
817
+
818
+ this.innerMonth = month
819
+ this.progressKey++
820
+
821
+ this.$emit('update:activeMonth', month)
822
+ this.$emit('month-change', month, {
823
+ index: to,
824
+ source,
825
+ option: this.monthOptions[to] || null,
826
+ raw: (this.normalizedMonths[to] || {}).raw || null
827
+ })
828
+
829
+ // 用户手动操作后重新计时,避免刚点完就被轮播切走
830
+ if (source !== 'autoplay') this.restartAutoplay()
831
+ },
832
+
833
+ /** 上一个月 */
834
+ prev(source = 'arrow') {
835
+ const len = this.monthOptions.length
836
+ if (len < 2) return
837
+ let idx = this.currentIndex - 1
838
+ if (idx < 0) {
839
+ if (!this.loop) return
840
+ idx = len - 1
841
+ }
842
+ this.setMonth(this.monthOptions[idx].value, source)
843
+ },
844
+
845
+ /** 下一个月 */
846
+ next(source = 'arrow') {
847
+ const len = this.monthOptions.length
848
+ if (len < 2) return
849
+ let idx = this.currentIndex + 1
850
+ if (idx >= len) {
851
+ if (!this.loop) {
852
+ this.stopAutoplay()
853
+ this.$emit('autoplay-end')
854
+ return
855
+ }
856
+ idx = 0
857
+ }
858
+ this.setMonth(this.monthOptions[idx].value, source)
859
+ },
860
+
861
+ /** 跳到第 n 个月份(0 开始) */
862
+ goTo(index, source = 'api') {
863
+ const opt = this.monthOptions[index]
864
+ if (opt) this.setMonth(opt.value, source)
865
+ },
866
+
867
+ /** 当前月份失效时(数据变更/首次加载)自动落到一个合法值 */
868
+ ensureValidMonth() {
869
+ const options = this.monthOptions
870
+ if (!options.length) {
871
+ this.innerMonth = ''
872
+ return
873
+ }
874
+ const exists = options.some((o) => o.value === this.innerMonth)
875
+ if (exists) return
876
+
877
+ const preferred = options.some((o) => o.value === this.activeMonth)
878
+ ? this.activeMonth
879
+ : options[0].value
880
+
881
+ this.innerMonth = preferred
882
+ if (preferred !== this.activeMonth) this.$emit('update:activeMonth', preferred)
883
+ },
884
+
885
+ resolveMonthLabel(monthKey, rawItem) {
886
+ if (typeof this.monthFormatter === 'function') {
887
+ return this.monthFormatter(monthKey, rawItem)
888
+ }
889
+ const custom = rawItem ? getByPath(rawItem, this.fields.monthLabel) : null
890
+ return custom || defaultMonthFormatter(monthKey)
891
+ },
892
+
893
+ /* ========================= 轮播控制 ========================= */
894
+ /** 重新开始计时(完整间隔) */
895
+ restartAutoplay() {
896
+ this.stopAutoplay()
897
+ if (!this.autoplayRunnable) return
898
+ this._remaining = this.autoplayInterval
899
+ this.progressKey++
900
+ if (!this.isPaused) this.scheduleTick(this._remaining)
901
+ },
902
+
903
+ /** 挂一个定时器,到点切下一个月 */
904
+ scheduleTick(delay) {
905
+ this.clearTimer()
906
+ this._tickStart = Date.now()
907
+ this._timer = setTimeout(() => {
908
+ this._timer = null
909
+ this.next('autoplay')
910
+ // next() 内部不会重置计时(source=autoplay),这里续上下一轮
911
+ this._remaining = this.autoplayInterval
912
+ this.progressKey++
913
+ if (!this.isPaused && this.autoplayRunnable) this.scheduleTick(this._remaining)
914
+ }, delay)
915
+ },
916
+
917
+ /** 暂停:把剩余时间记下来,恢复时接着走,进度条不会跳 */
918
+ pauseAutoplay() {
919
+ if (!this._timer) return
920
+ const elapsed = Date.now() - this._tickStart
921
+ this._remaining = Math.max(0, this._remaining - elapsed)
922
+ this.clearTimer()
923
+ },
924
+
925
+ resumeAutoplay() {
926
+ if (this._timer || !this.autoplayRunnable || this.isPaused) return
927
+ this.scheduleTick(this._remaining > 0 ? this._remaining : this.autoplayInterval)
928
+ },
929
+
930
+ stopAutoplay() {
931
+ this.clearTimer()
932
+ this._remaining = this.autoplayInterval
933
+ },
934
+
935
+ clearTimer() {
936
+ if (this._timer) {
937
+ clearTimeout(this._timer)
938
+ this._timer = null
939
+ }
940
+ },
941
+
942
+ /** 播放 / 暂停按钮 */
943
+ toggleAutoplay() {
944
+ this.manualPaused = !this.manualPaused
945
+ this.$emit('autoplay-toggle', !this.manualPaused)
946
+ },
947
+
948
+ handleMouseEnter() {
949
+ this.hovering = true
950
+ },
951
+
952
+ handleMouseLeave() {
953
+ this.hovering = false
954
+ },
955
+
956
+ handleVisibilityChange() {
957
+ this.pageHidden = document.hidden
958
+ },
959
+
960
+ handleSelectChange(value) {
961
+ this.setMonth(value, 'select')
962
+ },
963
+
964
+ handleSelectVisibleChange(visible) {
965
+ this.selectOpen = visible
966
+ },
967
+
968
+ /* ========================= 舞台高度 ========================= */
969
+ /**
970
+ * 月份行数可能是 5 行或 6 行,直接切换会导致高度突变。
971
+ * 这里显式把舞台高度锁到当前面板高度,配合 CSS transition 平滑过渡。
972
+ */
973
+ observeStage(el) {
974
+ this.disconnectObserver()
975
+ if (!el) return
976
+
977
+ this.syncStageHeightOf(el)
978
+
979
+ if (typeof ResizeObserver !== 'undefined') {
980
+ this._ro = new ResizeObserver(() => this.syncStageHeightOf(el))
981
+ this._ro.observe(el)
982
+ }
983
+ },
984
+
985
+ disconnectObserver() {
986
+ if (this._ro) {
987
+ this._ro.disconnect()
988
+ this._ro = null
989
+ }
990
+ },
991
+
992
+ syncStageHeightOf(el) {
993
+ if (!el || !el.offsetParent) return
994
+ const h = el.offsetHeight
995
+ if (h > 0) this.stageHeight = h + 'px'
996
+ },
997
+
998
+ /** 窗口尺寸变化时重新测量(文字换行会改变行高) */
999
+ syncStageHeight() {
1000
+ const el = this.$el && this.$el.querySelector('.ec__panel:not([class*="-leave-"])')
1001
+ this.syncStageHeightOf(el)
1002
+ },
1003
+
1004
+ /* ========================= 过渡钩子 ========================= */
1005
+ handleBeforeEnter(el) {
1006
+ // 进场元素先量一次高度,让 stage 的高度动画和内容切换同步开始
1007
+ this.$nextTick(() => this.syncStageHeightOf(el))
1008
+ },
1009
+
1010
+ handleEnter(el) {
1011
+ // 面板是新 DOM,需要重新观察
1012
+ this.observeStage(el)
1013
+ },
1014
+
1015
+ handleAfterEnter(el) {
1016
+ this.syncStageHeightOf(el)
1017
+ this.$emit('transition-end', this.innerMonth)
1018
+ },
1019
+
1020
+ /* ========================= 交互事件 ========================= */
1021
+ /** 单元格样式类 */
1022
+ cellClass(cell) {
1023
+ return {
1024
+ 'is-other-month': !cell.isCurrentMonth,
1025
+ 'is-today': cell.isToday && this.highlightToday,
1026
+ 'is-weekend': cell.isWeekend,
1027
+ 'has-items': cell.items.length > 0
1028
+ }
1029
+ },
1030
+
1031
+ /** 传给 day / day-header / items / day-empty 插槽的上下文 */
1032
+ cellScope(cell) {
1033
+ return {
1034
+ day: cell,
1035
+ date: cell.date,
1036
+ dateKey: cell.dateKey,
1037
+ items: cell.items,
1038
+ visibleItems: cell.visibleItems,
1039
+ raw: cell.dayRaw,
1040
+ isCurrentMonth: cell.isCurrentMonth,
1041
+ isToday: cell.isToday,
1042
+ isWeekend: cell.isWeekend,
1043
+ label: this.dayFormatter(cell)
1044
+ }
1045
+ },
1046
+
1047
+ /** 传给 item 插槽的上下文 */
1048
+ itemScope(entry, cell, index) {
1049
+ return {
1050
+ item: entry.raw,
1051
+ raw: entry.raw,
1052
+ label: entry.label,
1053
+ badge: entry.badge,
1054
+ type: entry.type,
1055
+ disabled: entry.disabled,
1056
+ index,
1057
+ day: cell,
1058
+ dateKey: cell.dateKey
1059
+ }
1060
+ },
1061
+
1062
+ handleDayClick(cell, event) {
1063
+ this.$emit('day-click', this.cellScope(cell), event)
1064
+ },
1065
+
1066
+ handleItemClick(entry, cell, index, event) {
1067
+ if (entry.disabled) return
1068
+ this.$emit('item-click', this.itemScope(entry, cell, index), event)
1069
+ },
1070
+
1071
+ handleBadgeClick(entry, cell, index, event) {
1072
+ if (entry.disabled) return
1073
+ // 未监听 tag-click 时,点角标退化成点条目,避免「点了没反应」
1074
+ if (this.$listeners['tag-click']) {
1075
+ this.$emit('tag-click', this.itemScope(entry, cell, index), event)
1076
+ } else {
1077
+ this.handleItemClick(entry, cell, index, event)
1078
+ }
1079
+ },
1080
+
1081
+ handleMoreClick(cell, event) {
1082
+ this.$emit('more-click', this.cellScope(cell), event)
1083
+ },
1084
+
1085
+ /* ========================= 配色 ========================= */
1086
+ resolveTypeColor(type) {
1087
+ const merged = { ...BUILTIN_TYPE_COLORS, ...this.typeColors }
1088
+ return merged[type] || merged.default || BUILTIN_TYPE_COLORS.default
1089
+ },
1090
+
1091
+ itemStyle(entry) {
1092
+ if (!this.colorful || !entry.type) return {}
1093
+ const c = this.resolveTypeColor(entry.type)
1094
+ // 只把颜色写进 CSS 变量,具体怎么用交给样式表,方便外部再覆盖
1095
+ return { '--ec-item-accent': c.color, '--ec-item-accent-bg': c.bg }
1096
+ },
1097
+
1098
+ badgeStyle(entry) {
1099
+ if (!this.colorful || !entry.type) return {}
1100
+ const c = this.resolveTypeColor(entry.type)
1101
+ return { color: c.color, backgroundColor: c.bg }
1102
+ }
1103
+ }
1104
+ }
1105
+ </script>
1106
+
1107
+ <style scoped>
1108
+ /* ==================================================================
1109
+ * 布局:整体用 CSS Grid,7 列等宽。
1110
+ * 同一行的格子会自动拉伸到该行最高格子的高度,
1111
+ * 因此「行高取决于条目最多的那一天」是天然满足的,无需 JS 计算。
1112
+ * ================================================================== */
1113
+ .ec {
1114
+ position: relative;
1115
+ box-sizing: border-box;
1116
+ width: 100%;
1117
+ color: var(--ec-text);
1118
+ font-size: 14px;
1119
+ line-height: 1.5;
1120
+ -webkit-font-smoothing: antialiased;
1121
+ }
1122
+
1123
+ .ec *,
1124
+ .ec *::before,
1125
+ .ec *::after {
1126
+ box-sizing: border-box;
1127
+ }
1128
+
1129
+ /* ------------------------------ 顶部 ------------------------------ */
1130
+ .ec__header {
1131
+ display: flex;
1132
+ align-items: center;
1133
+ justify-content: space-between;
1134
+ gap: 16px;
1135
+ padding: 4px 4px 16px;
1136
+ }
1137
+
1138
+ .ec__header-left {
1139
+ flex: 1;
1140
+ min-width: 0;
1141
+ }
1142
+
1143
+ .ec__header-right {
1144
+ display: flex;
1145
+ align-items: center;
1146
+ gap: 8px;
1147
+ flex: none;
1148
+ }
1149
+
1150
+ .ec__title-group {
1151
+ display: flex;
1152
+ align-items: baseline;
1153
+ gap: 10px;
1154
+ flex-wrap: wrap;
1155
+ }
1156
+
1157
+ .ec__title {
1158
+ margin: 0;
1159
+ font-size: 20px;
1160
+ font-weight: 600;
1161
+ color: var(--ec-text);
1162
+ }
1163
+
1164
+ .ec__subtitle {
1165
+ font-size: 13px;
1166
+ color: var(--ec-text-secondary);
1167
+ }
1168
+
1169
+ .ec__icon-btn {
1170
+ display: inline-flex;
1171
+ align-items: center;
1172
+ justify-content: center;
1173
+ width: 34px;
1174
+ height: 34px;
1175
+ padding: 0;
1176
+ border: 1px solid var(--ec-border);
1177
+ border-radius: var(--ec-radius);
1178
+ background: #fff;
1179
+ color: var(--ec-text-secondary);
1180
+ cursor: pointer;
1181
+ transition: color 0.2s, border-color 0.2s, background-color 0.2s;
1182
+ }
1183
+
1184
+ .ec__icon-btn svg {
1185
+ width: 16px;
1186
+ height: 16px;
1187
+ }
1188
+
1189
+ .ec__icon-btn:hover:not(:disabled) {
1190
+ color: var(--ec-primary);
1191
+ border-color: var(--ec-primary);
1192
+ background: var(--ec-primary-ghost);
1193
+ }
1194
+
1195
+ .ec__icon-btn:disabled {
1196
+ cursor: not-allowed;
1197
+ opacity: 0.45;
1198
+ }
1199
+
1200
+ /* ---------------------------- 轮播进度条 ---------------------------- */
1201
+ .ec__progress {
1202
+ height: 2px;
1203
+ margin-bottom: 8px;
1204
+ border-radius: 2px;
1205
+ background: var(--ec-border-light);
1206
+ overflow: hidden;
1207
+ }
1208
+
1209
+ .ec__progress-inner {
1210
+ height: 100%;
1211
+ width: 0;
1212
+ border-radius: 2px;
1213
+ background: var(--ec-primary);
1214
+ animation-name: ec-progress;
1215
+ animation-timing-function: linear;
1216
+ animation-fill-mode: forwards;
1217
+ }
1218
+
1219
+ @keyframes ec-progress {
1220
+ from { width: 0; }
1221
+ to { width: 100%; }
1222
+ }
1223
+
1224
+ /* ---------------------------- 星期表头 ---------------------------- */
1225
+ .ec__weekdays {
1226
+ display: grid;
1227
+ grid-template-columns: repeat(7, minmax(0, 1fr));
1228
+ border-bottom: 1px solid var(--ec-border);
1229
+ }
1230
+
1231
+ .ec__weekday {
1232
+ padding: 10px 8px;
1233
+ text-align: center;
1234
+ font-size: 14px;
1235
+ font-weight: 500;
1236
+ color: var(--ec-text-secondary);
1237
+ }
1238
+
1239
+ .ec__weekday.is-weekend {
1240
+ color: var(--ec-text-placeholder);
1241
+ }
1242
+
1243
+ /* ---------------------------- 舞台 / 面板 ---------------------------- */
1244
+ .ec__stage {
1245
+ position: relative;
1246
+ overflow: hidden;
1247
+ transition: height var(--ec-duration) cubic-bezier(0.4, 0, 0.2, 1);
1248
+ }
1249
+
1250
+ .ec__panel {
1251
+ width: 100%;
1252
+ }
1253
+
1254
+ .ec__grid {
1255
+ display: grid;
1256
+ grid-template-columns: repeat(7, minmax(0, 1fr));
1257
+ /* 行高下限固定,上限交给内容撑开 —— 同行等高由 grid 保证 */
1258
+ grid-auto-rows: minmax(var(--ec-min-row-h), auto);
1259
+ }
1260
+
1261
+ /* ------------------------------ 单元格 ------------------------------ */
1262
+ .ec__cell {
1263
+ display: flex;
1264
+ flex-direction: column;
1265
+ min-width: 0;
1266
+ padding: 12px 10px;
1267
+ border-right: 1px solid var(--ec-border-light);
1268
+ border-bottom: 1px solid var(--ec-border-light);
1269
+ transition: background-color 0.2s;
1270
+ }
1271
+
1272
+ .ec__cell:nth-child(7n) {
1273
+ border-right: none;
1274
+ }
1275
+
1276
+ /* 最后一行去掉底边框 */
1277
+ .ec__grid > .ec__cell:nth-last-child(-n + 7) {
1278
+ border-bottom: none;
1279
+ }
1280
+
1281
+ .ec__cell:hover {
1282
+ background: var(--ec-cell-hover);
1283
+ }
1284
+
1285
+ .ec__cell:focus-visible {
1286
+ outline: 2px solid var(--ec-primary);
1287
+ outline-offset: -2px;
1288
+ }
1289
+
1290
+ /* 非当月日期整体置灰 */
1291
+ .ec__cell.is-other-month {
1292
+ background: #fcfcfd;
1293
+ }
1294
+
1295
+ .ec__cell.is-other-month .ec__day-label {
1296
+ color: var(--ec-muted);
1297
+ }
1298
+
1299
+ .ec__cell.is-other-month .ec__item {
1300
+ opacity: 0.55;
1301
+ }
1302
+
1303
+ /* ------------------------------ 日期行 ------------------------------ */
1304
+ .ec__day-head {
1305
+ display: flex;
1306
+ align-items: center;
1307
+ justify-content: center;
1308
+ gap: 5px;
1309
+ margin-bottom: 10px;
1310
+ min-height: 20px;
1311
+ }
1312
+
1313
+ .ec__day-label {
1314
+ font-size: 14px;
1315
+ color: var(--ec-text);
1316
+ }
1317
+
1318
+ .ec__cell.is-today .ec__day-label {
1319
+ color: var(--ec-primary);
1320
+ font-weight: 600;
1321
+ }
1322
+
1323
+ .ec__today-dot {
1324
+ width: 5px;
1325
+ height: 5px;
1326
+ border-radius: 50%;
1327
+ background: var(--ec-primary);
1328
+ }
1329
+
1330
+ /* ------------------------------ 条目列表 ------------------------------ */
1331
+ .ec__items-wrap {
1332
+ flex: 1;
1333
+ min-height: 0;
1334
+ }
1335
+
1336
+ .ec__items {
1337
+ display: flex;
1338
+ flex-direction: column;
1339
+ gap: var(--ec-item-gap);
1340
+ overflow-y: auto;
1341
+ overflow-x: hidden;
1342
+ /* 滚动到边界时不把滚动传递给外层页面 */
1343
+ overscroll-behavior: contain;
1344
+ }
1345
+
1346
+ /* 出现滚动条时右侧留一点空隙,避免内容被压住 */
1347
+ .ec__items.is-scrollable {
1348
+ padding-right: 4px;
1349
+ }
1350
+
1351
+ .ec__items::-webkit-scrollbar {
1352
+ width: 4px;
1353
+ }
1354
+
1355
+ .ec__items::-webkit-scrollbar-thumb {
1356
+ background: #d8dce4;
1357
+ border-radius: 2px;
1358
+ }
1359
+
1360
+ .ec__items::-webkit-scrollbar-thumb:hover {
1361
+ background: #c0c4cc;
1362
+ }
1363
+
1364
+ .ec__items::-webkit-scrollbar-track {
1365
+ background: transparent;
1366
+ }
1367
+
1368
+ /* ------------------------------ 单个条目 ------------------------------ */
1369
+ .ec__item {
1370
+ position: relative;
1371
+ flex: none;
1372
+ display: flex;
1373
+ align-items: center;
1374
+ min-height: var(--ec-item-h);
1375
+ padding: 4px 8px;
1376
+ border-radius: 4px;
1377
+ background: var(--ec-item-bg);
1378
+ color: var(--ec-text-secondary);
1379
+ font-size: 13px;
1380
+ cursor: pointer;
1381
+ transition: background-color 0.18s, color 0.18s, box-shadow 0.18s;
1382
+ }
1383
+
1384
+ /* 有角标时右侧让位,没有角标则占满,避免留白 */
1385
+ .ec__item.has-badge {
1386
+ padding-right: 30px;
1387
+ }
1388
+
1389
+ /* 注意:外层 .ec__items 有 overflow-x: hidden(滚动条需要),
1390
+ * 所以 hover 不能用水平位移,否则会被裁切;这里改用内阴影描边。 */
1391
+ .ec__item:hover {
1392
+ color: var(--ec-text);
1393
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
1394
+ }
1395
+
1396
+ .ec__item.is-colorful:hover {
1397
+ background: var(--ec-item-accent-bg, var(--ec-item-bg));
1398
+ color: var(--ec-item-accent, var(--ec-text));
1399
+ box-shadow: inset 0 0 0 1px var(--ec-item-accent-bg, transparent);
1400
+ }
1401
+
1402
+ .ec__item.is-disabled {
1403
+ cursor: not-allowed;
1404
+ opacity: 0.5;
1405
+ }
1406
+
1407
+ .ec__item.is-disabled:hover {
1408
+ box-shadow: none;
1409
+ }
1410
+
1411
+ .ec__item-label {
1412
+ flex: 1;
1413
+ min-width: 0;
1414
+ overflow: hidden;
1415
+ white-space: nowrap;
1416
+ text-overflow: ellipsis;
1417
+ }
1418
+
1419
+ /* 角标:贴在条目右上角,与设计图一致 */
1420
+ .ec__item-badge {
1421
+ position: absolute;
1422
+ top: 0;
1423
+ right: 0;
1424
+ padding: 0 4px;
1425
+ border-radius: 0 4px 0 4px;
1426
+ background: rgba(0, 0, 0, 0.06);
1427
+ color: var(--ec-text-placeholder);
1428
+ font-size: 11px;
1429
+ line-height: 16px;
1430
+ white-space: nowrap;
1431
+ cursor: pointer;
1432
+ transition: filter 0.18s;
1433
+ }
1434
+
1435
+ .ec__item-badge:hover {
1436
+ filter: brightness(0.92);
1437
+ }
1438
+
1439
+ /* 「还有 N 项」 */
1440
+ .ec__more {
1441
+ flex: none;
1442
+ padding: 2px 8px;
1443
+ color: var(--ec-primary);
1444
+ font-size: 12px;
1445
+ text-align: center;
1446
+ cursor: pointer;
1447
+ border-radius: 4px;
1448
+ }
1449
+
1450
+ .ec__more:hover {
1451
+ background: var(--ec-primary-ghost);
1452
+ }
1453
+
1454
+ /* ------------------------------ 空状态 ------------------------------ */
1455
+ .ec__empty {
1456
+ display: flex;
1457
+ flex-direction: column;
1458
+ align-items: center;
1459
+ justify-content: center;
1460
+ gap: 12px;
1461
+ padding: 60px 20px;
1462
+ color: var(--ec-text-placeholder);
1463
+ }
1464
+
1465
+ .ec__empty-icon {
1466
+ width: 56px;
1467
+ height: 56px;
1468
+ }
1469
+
1470
+ .ec__empty-text {
1471
+ margin: 0;
1472
+ font-size: 14px;
1473
+ }
1474
+
1475
+ /* ==================================================================
1476
+ * 切换动效
1477
+ * 离场元素统一 absolute 脱离文档流,
1478
+ * 这样舞台高度只由进场元素决定,两者可以同时播放动画。
1479
+ * ================================================================== */
1480
+ .ec-fade-leave-active,
1481
+ .ec-zoom-leave-active,
1482
+ .ec-none-leave-active,
1483
+ .ec-slide-next-leave-active,
1484
+ .ec-slide-prev-leave-active,
1485
+ .ec-flip-next-leave-active,
1486
+ .ec-flip-prev-leave-active {
1487
+ position: absolute;
1488
+ top: 0;
1489
+ left: 0;
1490
+ right: 0;
1491
+ }
1492
+
1493
+ .ec-fade-enter-active,
1494
+ .ec-fade-leave-active,
1495
+ .ec-zoom-enter-active,
1496
+ .ec-zoom-leave-active,
1497
+ .ec-slide-next-enter-active,
1498
+ .ec-slide-next-leave-active,
1499
+ .ec-slide-prev-enter-active,
1500
+ .ec-slide-prev-leave-active,
1501
+ .ec-flip-next-enter-active,
1502
+ .ec-flip-next-leave-active,
1503
+ .ec-flip-prev-enter-active,
1504
+ .ec-flip-prev-leave-active {
1505
+ transition:
1506
+ transform var(--ec-duration) cubic-bezier(0.4, 0, 0.2, 1),
1507
+ opacity var(--ec-duration) ease;
1508
+ will-change: transform, opacity;
1509
+ }
1510
+
1511
+ /* fade */
1512
+ .ec-fade-enter,
1513
+ .ec-fade-leave-to {
1514
+ opacity: 0;
1515
+ }
1516
+
1517
+ /* slide:向后翻从右侧进、向前翻从左侧进 */
1518
+ .ec-slide-next-enter {
1519
+ opacity: 0;
1520
+ transform: translateX(6%);
1521
+ }
1522
+ .ec-slide-next-leave-to {
1523
+ opacity: 0;
1524
+ transform: translateX(-6%);
1525
+ }
1526
+ .ec-slide-prev-enter {
1527
+ opacity: 0;
1528
+ transform: translateX(-6%);
1529
+ }
1530
+ .ec-slide-prev-leave-to {
1531
+ opacity: 0;
1532
+ transform: translateX(6%);
1533
+ }
1534
+
1535
+ /* zoom */
1536
+ .ec-zoom-enter {
1537
+ opacity: 0;
1538
+ transform: scale(0.97);
1539
+ }
1540
+ .ec-zoom-leave-to {
1541
+ opacity: 0;
1542
+ transform: scale(1.03);
1543
+ }
1544
+
1545
+ /* flip:带透视的 3D 翻页 */
1546
+ .ec--effect-flip .ec__stage {
1547
+ perspective: 1600px;
1548
+ }
1549
+ .ec-flip-next-enter,
1550
+ .ec-flip-prev-enter,
1551
+ .ec-flip-next-leave-to,
1552
+ .ec-flip-prev-leave-to {
1553
+ opacity: 0;
1554
+ }
1555
+ .ec-flip-next-enter {
1556
+ transform: rotateY(9deg) translateX(5%);
1557
+ }
1558
+ .ec-flip-next-leave-to {
1559
+ transform: rotateY(-9deg) translateX(-5%);
1560
+ }
1561
+ .ec-flip-prev-enter {
1562
+ transform: rotateY(-9deg) translateX(-5%);
1563
+ }
1564
+ .ec-flip-prev-leave-to {
1565
+ transform: rotateY(9deg) translateX(5%);
1566
+ }
1567
+
1568
+ /* ------------------------- 单元格错峰入场 -------------------------
1569
+ * 面板每次切月都是全新 DOM,CSS 动画会自动重放,无需额外 JS 触发。
1570
+ * ---------------------------------------------------------------- */
1571
+ .ec--stagger .ec__cell {
1572
+ animation: ec-cell-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
1573
+ /* 按格子序号递增延迟,形成从左上到右下的波浪 */
1574
+ animation-delay: calc(var(--ec-i, 0) * 12ms);
1575
+ }
1576
+
1577
+ @keyframes ec-cell-in {
1578
+ from {
1579
+ opacity: 0;
1580
+ transform: translateY(10px);
1581
+ }
1582
+ to {
1583
+ opacity: 1;
1584
+ transform: none;
1585
+ }
1586
+ }
1587
+
1588
+ /* ------------------------- 无动效 / 降级 ------------------------- */
1589
+ .ec--effect-none .ec__stage {
1590
+ transition: none;
1591
+ }
1592
+
1593
+ /* 系统开启「减少动态效果」时关闭全部动画 */
1594
+ @media (prefers-reduced-motion: reduce) {
1595
+ .ec__stage,
1596
+ .ec__item,
1597
+ .ec__cell,
1598
+ .ec__icon-btn,
1599
+ .ec-fade-enter-active,
1600
+ .ec-fade-leave-active,
1601
+ .ec-zoom-enter-active,
1602
+ .ec-zoom-leave-active,
1603
+ .ec-slide-next-enter-active,
1604
+ .ec-slide-next-leave-active,
1605
+ .ec-slide-prev-enter-active,
1606
+ .ec-slide-prev-leave-active,
1607
+ .ec-flip-next-enter-active,
1608
+ .ec-flip-next-leave-active,
1609
+ .ec-flip-prev-enter-active,
1610
+ .ec-flip-prev-leave-active {
1611
+ transition: none !important;
1612
+ }
1613
+ .ec--stagger .ec__cell,
1614
+ .ec__progress-inner {
1615
+ animation: none !important;
1616
+ }
1617
+ }
1618
+
1619
+ /* ------------------------------ 响应式 ------------------------------ */
1620
+ @media (max-width: 768px) {
1621
+ .ec__header {
1622
+ flex-direction: column;
1623
+ align-items: stretch;
1624
+ gap: 10px;
1625
+ }
1626
+ .ec__header-right {
1627
+ justify-content: flex-end;
1628
+ }
1629
+ .ec__cell {
1630
+ padding: 8px 6px;
1631
+ }
1632
+ .ec__item {
1633
+ font-size: 12px;
1634
+ }
1635
+ /* 窄屏下角标改为常规内联,不再绝对定位,因此收回右侧让位的内边距。
1636
+ * 这里必须带 .has-badge,否则选择器权重低于上面的 .ec__item.has-badge。 */
1637
+ .ec__item.has-badge {
1638
+ padding-right: 8px;
1639
+ }
1640
+ .ec__item-badge {
1641
+ position: static;
1642
+ margin-left: 6px;
1643
+ border-radius: 3px;
1644
+ }
1645
+ }
1646
+ </style>