npmapps 1.2.0 → 1.3.1

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 (120) hide show
  1. package/README.md +41 -0
  2. package/files/part-002 +4 -0
  3. package/get.js +193 -0
  4. package/manifest.json +28 -0
  5. package/package.json +17 -10
  6. package/PUBLISH.md +0 -268
  7. package/app/wujie-wk/t.html +0 -90
  8. package/app/wujie-wk/wujie-vue2-child/.eslintrc.js +0 -19
  9. package/app/wujie-wk/wujie-vue2-child/README.md +0 -24
  10. package/app/wujie-wk/wujie-vue2-child/babel.config.js +0 -5
  11. package/app/wujie-wk/wujie-vue2-child/docs/superpowers/plans/2026-05-29-quill-editor.md +0 -836
  12. package/app/wujie-wk/wujie-vue2-child/docs/superpowers/specs/2026-05-29-quill-editor-design.md +0 -210
  13. package/app/wujie-wk/wujie-vue2-child/jsconfig.json +0 -19
  14. package/app/wujie-wk/wujie-vue2-child/package-lock.json +0 -21333
  15. package/app/wujie-wk/wujie-vue2-child/package.json +0 -63
  16. package/app/wujie-wk/wujie-vue2-child/postcss.config.js +0 -10
  17. package/app/wujie-wk/wujie-vue2-child/public/favicon.ico +0 -0
  18. package/app/wujie-wk/wujie-vue2-child/public/index.html +0 -17
  19. package/app/wujie-wk/wujie-vue2-child/public/rili.jpg +0 -0
  20. package/app/wujie-wk/wujie-vue2-child/public//344/270/200/351/224/256/351/273/221/346/232/227.html +0 -136
  21. package/app/wujie-wk/wujie-vue2-child/src/App.vue +0 -110
  22. package/app/wujie-wk/wujie-vue2-child/src/assets/bpmn-camunda.jpg +0 -0
  23. package/app/wujie-wk/wujie-vue2-child/src/assets/css/diagram.less +0 -17
  24. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/Icon.less +0 -31
  25. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app-codes.css +0 -26
  26. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.eot +0 -0
  27. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.svg +0 -60
  28. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.ttf +0 -0
  29. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.woff +0 -0
  30. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.woff2 +0 -0
  31. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/config.json +0 -248
  32. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-bottom-tool.svg +0 -30
  33. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-horizontal-center-tool.svg +0 -85
  34. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-left-tool.svg +0 -84
  35. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-right-tool.svg +0 -80
  36. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-top-tool.svg +0 -84
  37. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-vertical-center-tool.svg +0 -89
  38. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/distribute-horizontally-tool.svg +0 -95
  39. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/distribute-vertically-tool.svg +0 -99
  40. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/set-color-tool.svg +0 -111
  41. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-bottom-tool.svg +0 -30
  42. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-horizontal-center-tool.svg +0 -30
  43. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-left-tool.svg +0 -30
  44. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-right-tool.svg +0 -30
  45. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-top-tool.svg +0 -30
  46. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-vertical-center-tool.svg +0 -30
  47. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/distribute-horizontally-tool.svg +0 -30
  48. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/distribute-vertically-tool.svg +0 -30
  49. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/set-color-tool.svg +0 -63
  50. package/app/wujie-wk/wujie-vue2-child/src/assets/logo.png +0 -0
  51. package/app/wujie-wk/wujie-vue2-child/src/components/EllTable/README.md +0 -70
  52. package/app/wujie-wk/wujie-vue2-child/src/components/EllTable/article.md +0 -184
  53. package/app/wujie-wk/wujie-vue2-child/src/components/EllTable/index.js +0 -213
  54. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/CalendarSkeleton.vue +0 -147
  55. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/CodeBlock.vue +0 -199
  56. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/MonthSelect.vue +0 -376
  57. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/README.md +0 -485
  58. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/example.vue +0 -1747
  59. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/index.vue +0 -2720
  60. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/mockData.js +0 -475
  61. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/recruitmentAdapter.js +0 -51
  62. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/utils.js +0 -202
  63. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/FunctionSelector.vue +0 -123
  64. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/OperatorSelector.vue +0 -184
  65. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/ParameterSelector.vue +0 -123
  66. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/api.js +0 -69
  67. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/index.vue +0 -435
  68. package/app/wujie-wk/wujie-vue2-child/src/components/HelloWorld.vue +0 -58
  69. package/app/wujie-wk/wujie-vue2-child/src/components/PageHeader/index.vue +0 -158
  70. package/app/wujie-wk/wujie-vue2-child/src/components/Splitter/README.md +0 -144
  71. package/app/wujie-wk/wujie-vue2-child/src/components/Splitter/example.vue +0 -84
  72. package/app/wujie-wk/wujie-vue2-child/src/components/Splitter/index.vue +0 -203
  73. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/ToolBar.vue +0 -357
  74. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/customTranslate/customTranslate.js +0 -12
  75. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/customTranslate/translationsGerman.js +0 -241
  76. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/index.vue +0 -261
  77. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/xmlData.js +0 -29
  78. package/app/wujie-wk/wujie-vue2-child/src/directives/filldown.js +0 -155
  79. package/app/wujie-wk/wujie-vue2-child/src/directives/filldownTable.js +0 -291
  80. package/app/wujie-wk/wujie-vue2-child/src/main.js +0 -40
  81. package/app/wujie-wk/wujie-vue2-child/src/router/index.js +0 -72
  82. package/app/wujie-wk/wujie-vue2-child/src/store/index.js +0 -23
  83. package/app/wujie-wk/wujie-vue2-child/src/utils/winBox.js +0 -23
  84. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/A.vue +0 -12
  85. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/B.vue +0 -10
  86. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/MagicalComponentsForELFormItem.vue +0 -78
  87. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/index.vue +0 -59
  88. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/tableMouseHorizontalWheel.vue +0 -184
  89. package/app/wujie-wk/wujie-vue2-child/src/views/Home.vue +0 -27
  90. package/app/wujie-wk/wujie-vue2-child/src/views/RouterJump.vue +0 -145
  91. package/app/wujie-wk/wujie-vue2-child/src/views/calendar/index.vue +0 -19
  92. package/app/wujie-wk/wujie-vue2-child/src/views/css.vue +0 -57
  93. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/EllipsisText.vue +0 -73
  94. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/HoverCard.vue +0 -69
  95. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/TableHover.vue +0 -130
  96. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/inputSlo.vue +0 -42
  97. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/tableFixed.vue +0 -148
  98. package/app/wujie-wk/wujie-vue2-child/src/views/echarts/echart-dome.vue +0 -73
  99. package/app/wujie-wk/wujie-vue2-child/src/views/echarts/index.vue +0 -118
  100. package/app/wujie-wk/wujie-vue2-child/src/views/echarts/pei3d.vue +0 -658
  101. package/app/wujie-wk/wujie-vue2-child/src/views/element/bpmn/index.vue +0 -15
  102. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/attendanceCycle/index.vue +0 -131
  103. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/attendanceGroup/index.vue +0 -147
  104. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/attendancePersonnel/index.vue +0 -158
  105. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/companyCalendar/index.vue +0 -147
  106. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/shift/index.vue +0 -147
  107. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/shiftRotationSystem/index.vue +0 -147
  108. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/columnManagement.vue +0 -340
  109. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/dialogInput.vue +0 -71
  110. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/elTableJsx.vue +0 -1826
  111. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/formTable.vue +0 -595
  112. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/index.vue +0 -29
  113. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/simpleTable.vue +0 -189
  114. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx.zip +0 -0
  115. package/app/wujie-wk/wujie-vue2-child/src/views/element/index.vue +0 -39
  116. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/README.md +0 -163
  117. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/example.vue +0 -307
  118. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/index.vue +0 -409
  119. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/toolbar.js +0 -122
  120. package/app/wujie-wk/wujie-vue2-child/vue.config.js +0 -15
@@ -1,2720 +0,0 @@
1
- <template>
2
- <div
3
- class="ec"
4
- :class="rootClass"
5
- :style="cssVars"
6
- @mouseenter="handleMouseEnter"
7
- @mouseleave="handleMouseLeave"
8
- >
9
- <!-- 开发/业务配置工具栏:默认关闭;改完点「复制配置」,把 JSON 发给研发即可 -->
10
- <section
11
- v-if="showConfigToolbar"
12
- class="ec__config"
13
- aria-label="日历布局配置"
14
- >
15
- <div class="ec__config-head">
16
- <div>
17
- <strong class="ec__config-title">布局配置</strong>
18
- <span class="ec__config-caption">调整后点「复制配置」,粘贴发给研发即可</span>
19
- </div>
20
- <div class="ec__config-actions">
21
- <button
22
- type="button"
23
- class="ec__config-btn is-primary"
24
- @click="copyLayoutConfig"
25
- >复制配置</button>
26
- </div>
27
- </div>
28
-
29
- <div class="ec__config-list">
30
- <div
31
- v-for="field in configFields"
32
- :key="field.key"
33
- class="ec__config-block"
34
- :class="'ec__config-block--' + field.kind"
35
- :title="field.kind === 'spacing' ? '当前简写:' + spacingShorthand(field.key) : undefined"
36
- >
37
- <!-- 分段选项:条目对齐 / 溢出方式 -->
38
- <template v-if="field.kind === 'seg'">
39
- <p class="ec__config-label">
40
- <span class="ec__config-priority">P{{ field.priority }}</span>{{ field.label }}
41
- <button
42
- type="button"
43
- class="ec__config-reset"
44
- :title="'还原「' + field.label + '」为初始值'"
45
- @click="resetField(field.key)"
46
- >还原</button>
47
- </p>
48
- <div class="ec__config-segment">
49
- <button
50
- v-for="option in field.options"
51
- :key="option.value"
52
- type="button"
53
- :class="{ 'is-active': configDraft[field.key] === option.value }"
54
- :aria-pressed="configDraft[field.key] === option.value ? 'true' : 'false'"
55
- @click="updateLayoutConfig(field.key, option.value)"
56
- >{{ option.label }}</button>
57
- </div>
58
- </template>
59
-
60
- <!-- 间距:上下 / 左右两块步进 -->
61
- <template v-else-if="field.kind === 'spacing'">
62
- <p class="ec__config-label">
63
- <span class="ec__config-priority">P{{ field.priority }}</span>{{ field.label }}
64
- <code class="ec__config-shorthand">{{ spacingShorthand(field.key) }}</code>
65
- <button
66
- type="button"
67
- class="ec__config-reset"
68
- :title="'还原「' + field.label + '」为初始值'"
69
- @click="resetField(field.key)"
70
- >还原</button>
71
- </p>
72
- <div class="ec__spacing">
73
- <div v-for="axis in spacingAxes" :key="axis.key" class="ec__spacing-row">
74
- <span class="ec__spacing-row-label">{{ axis.label }}</span>
75
- <span class="ec__stepper">
76
- <button
77
- type="button"
78
- class="ec__step-btn"
79
- :aria-label="field.label + ',' + axis.label + '减 1'"
80
- title="减 1(按住 Shift ±5)"
81
- @click="nudgeSpacingAxis(field.key, axis.key, -1, $event)"
82
- >−</button>
83
- <label class="ec__stepper-input">
84
- <input
85
- type="number"
86
- min="0"
87
- step="1"
88
- :value="spacingAxisValue(field.key, axis.key)"
89
- :aria-label="field.label + ',' + axis.label + '(px)'"
90
- @change="updateSpacingAxis(field.key, axis.key, $event)"
91
- />
92
- <span class="ec__stepper-unit" aria-hidden="true">px</span>
93
- </label>
94
- <button
95
- type="button"
96
- class="ec__step-btn"
97
- :aria-label="field.label + ',' + axis.label + '加 1'"
98
- title="加 1(按住 Shift ±5)"
99
- @click="nudgeSpacingAxis(field.key, axis.key, 1, $event)"
100
- >+</button>
101
- </span>
102
- </div>
103
- </div>
104
- </template>
105
-
106
- <!-- 单值数值:输入框左右包 −/+ -->
107
- <template v-else-if="field.kind === 'number'">
108
- <p class="ec__config-label">
109
- <span class="ec__config-priority">P{{ field.priority }}</span>{{ field.label }}
110
- <button
111
- type="button"
112
- class="ec__config-reset"
113
- :title="'还原「' + field.label + '」为初始值'"
114
- @click="resetField(field.key)"
115
- >还原</button>
116
- </p>
117
- <span class="ec__stepper">
118
- <button
119
- type="button"
120
- class="ec__step-btn"
121
- :aria-label="field.label + '减 1'"
122
- :title="'减 ' + (field.step || 1) + '(按住 Shift ×5)'"
123
- @click="nudgeNumber(field.key, -1, $event)"
124
- >−</button>
125
- <label class="ec__stepper-input">
126
- <input
127
- type="number"
128
- :min="typeof field.min === 'number' ? field.min : 0"
129
- :step="field.step || 1"
130
- :value="configDraft[field.key]"
131
- :aria-label="field.label + '(' + field.unit + ')'"
132
- @change="updateLayoutConfig(field.key, $event.target.value, 'number')"
133
- />
134
- <span v-if="field.unit" class="ec__stepper-unit" aria-hidden="true">{{ field.unit }}</span>
135
- </label>
136
- <button
137
- type="button"
138
- class="ec__step-btn"
139
- :aria-label="field.label + '加 1'"
140
- :title="'加 ' + (field.step || 1) + '(按住 Shift ×5)'"
141
- @click="nudgeNumber(field.key, 1, $event)"
142
- >+</button>
143
- </span>
144
- </template>
145
-
146
- <!-- 颜色 -->
147
- <template v-else-if="field.kind === 'color'">
148
- <label class="ec__color-swatch" :title="field.label">
149
- <input
150
- type="color"
151
- :value="colorValue(field.key)"
152
- :aria-label="field.label"
153
- @input="updateLayoutConfig(field.key, $event.target.value)"
154
- />
155
- </label>
156
- <span class="ec__color-meta">
157
- <span class="ec__config-label">
158
- <span class="ec__config-priority">P{{ field.priority }}</span>{{ field.label }}
159
- </span>
160
- <code class="ec__color-hex">{{ colorValue(field.key) }}</code>
161
- </span>
162
- <button
163
- type="button"
164
- class="ec__reset-btn"
165
- title="还原为默认配色"
166
- :disabled="!configDraft[field.key]"
167
- @click="resetColor(field.key)"
168
- >还原</button>
169
- </template>
170
- </div>
171
- </div>
172
-
173
- <div class="ec__config-output">
174
- <pre class="ec__config-json">{{ configJsonText }}</pre>
175
- </div>
176
-
177
- <div class="ec__config-feedback" role="status" aria-live="polite">
178
- {{ copyStatus }}
179
- </div>
180
- </section>
181
-
182
- <!-- ==================== 顶部区域 ==================== -->
183
- <div v-if="showHeader" class="ec__header">
184
- <!-- 左侧:完全交给使用者的插槽,未传时回落到 title / subtitle -->
185
- <div class="ec__header-left">
186
- <slot name="header-left" v-bind="headerScope">
187
- <div v-if="title || subtitle" class="ec__title-group">
188
- <h3 v-if="title" class="ec__title">{{ title }}</h3>
189
- <span v-if="subtitle" class="ec__subtitle">{{ subtitle }}</span>
190
- </div>
191
- </slot>
192
- </div>
193
-
194
- <!-- 右侧:默认是「轮播开关 + 翻月箭头 + 月份下拉」 -->
195
- <div class="ec__header-right">
196
- <slot name="header-right" v-bind="headerScope">
197
- <button
198
- v-if="autoplay && showAutoplayToggle"
199
- type="button"
200
- class="ec__icon-btn"
201
- :title="manualPaused ? '继续轮播' : '暂停轮播'"
202
- :aria-label="manualPaused ? '继续轮播' : '暂停轮播'"
203
- @click="toggleAutoplay"
204
- >
205
- <svg viewBox="0 0 16 16" aria-hidden="true">
206
- <path v-if="manualPaused" d="M5 3.5 L12 8 L5 12.5 Z" fill="currentColor" />
207
- <g v-else fill="currentColor">
208
- <rect x="4.5" y="3.5" width="2.4" height="9" rx="0.8" />
209
- <rect x="9.1" y="3.5" width="2.4" height="9" rx="0.8" />
210
- </g>
211
- </svg>
212
- </button>
213
-
214
- <template v-if="showArrows">
215
- <button
216
- type="button"
217
- class="ec__icon-btn"
218
- title="上一个月"
219
- aria-label="上一个月"
220
- :disabled="!canGoPrev"
221
- @click="prev()"
222
- >
223
- <svg viewBox="0 0 16 16" aria-hidden="true">
224
- <path d="M10 3 L5 8 L10 13" fill="none" stroke="currentColor" stroke-width="1.7"
225
- stroke-linecap="round" stroke-linejoin="round" />
226
- </svg>
227
- </button>
228
- <button
229
- type="button"
230
- class="ec__icon-btn"
231
- title="下一个月"
232
- aria-label="下一个月"
233
- :disabled="!canGoNext"
234
- @click="next()"
235
- >
236
- <svg viewBox="0 0 16 16" aria-hidden="true">
237
- <path d="M6 3 L11 8 L6 13" fill="none" stroke="currentColor" stroke-width="1.7"
238
- stroke-linecap="round" stroke-linejoin="round" />
239
- </svg>
240
- </button>
241
- </template>
242
-
243
- <MonthSelect
244
- v-if="showMonthSelect"
245
- :value="innerMonth"
246
- :options="monthOptions"
247
- :disabled="loading || !monthOptions.length"
248
- placeholder="选择月份"
249
- @change="handleSelectChange"
250
- @visible-change="handleSelectVisibleChange"
251
- />
252
- </slot>
253
- </div>
254
- </div>
255
-
256
- <!-- 轮播进度条:直观体现「下一次自动切换还有多久」 -->
257
- <div v-if="showProgressBar" class="ec__progress">
258
- <div
259
- :key="progressKey"
260
- class="ec__progress-inner"
261
- :style="progressStyle"
262
- ></div>
263
- </div>
264
-
265
- <!-- ==================== 星期表头 ==================== -->
266
- <div class="ec__weekdays" role="row">
267
- <div
268
- v-for="(w, index) in weekHeaders"
269
- :key="w.weekday"
270
- class="ec__weekday"
271
- :class="{ 'is-weekend': w.isWeekend }"
272
- role="columnheader"
273
- >
274
- <slot name="weekday" v-bind="{ ...w, index }">{{ w.label }}</slot>
275
- </div>
276
- </div>
277
-
278
- <!-- ==================== 主体(骨架屏 / 日历网格) ==================== -->
279
- <div class="ec__stage" :style="stageStyle">
280
- <transition
281
- :name="transitionName"
282
- @before-enter="handleBeforeEnter"
283
- @enter="handleEnter"
284
- @after-enter="handleAfterEnter"
285
- >
286
- <!-- 加载中:骨架屏 -->
287
- <div v-if="loading" key="__skeleton__" class="ec__panel">
288
- <slot name="loading" v-bind="{ rows: skeletonRows }">
289
- <CalendarSkeleton
290
- :rows="skeletonRows"
291
- :min-row-height="cfgMinRowHeight"
292
- :item-height="cfgItemHeight"
293
- />
294
- </slot>
295
- </div>
296
-
297
- <!-- 无任何月份数据:空状态 -->
298
- <div v-else-if="!monthOptions.length" key="__empty__" class="ec__panel">
299
- <slot name="empty">
300
- <div class="ec__empty">
301
- <svg class="ec__empty-icon" viewBox="0 0 64 64" aria-hidden="true">
302
- <rect x="8" y="14" width="48" height="42" rx="6" fill="none"
303
- stroke="currentColor" stroke-width="2.5" />
304
- <path d="M8 26 H56" stroke="currentColor" stroke-width="2.5" />
305
- <path d="M20 8 V18 M44 8 V18" stroke="currentColor" stroke-width="2.5"
306
- stroke-linecap="round" />
307
- </svg>
308
- <p class="ec__empty-text">{{ emptyText }}</p>
309
- </div>
310
- </slot>
311
- </div>
312
-
313
- <!-- 正常渲染 -->
314
- <div v-else :key="innerMonth" class="ec__panel">
315
- <div class="ec__grid" role="grid">
316
- <div
317
- v-for="(cell, index) in renderCells"
318
- :key="cell.dateKey"
319
- class="ec__cell"
320
- :class="cellClass(cell)"
321
- :style="{ '--ec-i': index }"
322
- role="gridcell"
323
- :tabindex="cellFocusable ? 0 : -1"
324
- :aria-label="cell.dateKey"
325
- @click="handleDayClick(cell, $event)"
326
- @keydown.enter.self="handleDayClick(cell, $event)"
327
- @keydown.space.prevent.self="handleDayClick(cell, $event)"
328
- >
329
- <!-- 整格自定义 -->
330
- <slot name="day" v-bind="cellScope(cell)">
331
- <!-- 日期行 -->
332
- <div class="ec__day-head">
333
- <slot name="day-header" v-bind="cellScope(cell)">
334
- <span class="ec__day-label">{{ dayFormatter(cell) }}</span>
335
- <span v-if="cell.isToday && highlightToday" class="ec__today-dot"></span>
336
- </slot>
337
- </div>
338
-
339
- <!-- 条目列表 -->
340
- <div class="ec__items-wrap">
341
- <slot name="items" v-bind="cellScope(cell)">
342
- <div
343
- v-if="cell.items.length"
344
- class="ec__items"
345
- :class="{ 'is-scrollable': cell.scrollable }"
346
- :style="itemsStyle"
347
- >
348
- <div
349
- v-for="(entry, i) in cell.visibleItems"
350
- :key="entry.key"
351
- class="ec__item"
352
- :class="{
353
- 'is-disabled': entry.disabled,
354
- 'has-badge': !!entry.badge,
355
- 'is-colorful': colorful && entry.type && !cfgItemBgColor && cfgItemBgHover,
356
- 'is-bg-gradient': cfgItemBgGradient,
357
- 'is-bg-gradient-h': cfgItemBgGradient && cfgGradientDir === 'to-right'
358
- }"
359
- :style="itemStyle(entry)"
360
- :title="itemTitle(entry)"
361
- @click.stop="handleItemClick(entry, cell, i, $event)"
362
- >
363
- <!-- 单条自定义 -->
364
- <slot name="item" v-bind="itemScope(entry, cell, i)">
365
- <div class="ec__item-content">
366
- <span
367
- class="ec__item-label"
368
- :class="{ 'is-long': isLongItemLabel(entry.label) }"
369
- >{{ entry.label }}</span>
370
- <span
371
- v-if="hasText(entry.address)"
372
- class="ec__item-address"
373
- >{{ entry.address }}</span>
374
- </div>
375
- <span
376
- v-if="entry.badge"
377
- class="ec__item-badge"
378
- :style="badgeStyle(entry)"
379
- @click.stop="handleBadgeClick(entry, cell, i, $event)"
380
- >{{ entry.badge }}</span>
381
- </slot>
382
- </div>
383
-
384
- <!-- overflowMode='collapse' 时的「更多」提示 -->
385
- <div
386
- v-if="cell.moreCount > 0"
387
- class="ec__more"
388
- @click.stop="handleMoreClick(cell, $event)"
389
- >还有 {{ cell.moreCount }} 项</div>
390
- </div>
391
-
392
- <!-- 当天无数据 -->
393
- <slot v-else name="day-empty" v-bind="cellScope(cell)"></slot>
394
- </slot>
395
- </div>
396
- </slot>
397
- </div>
398
- </div>
399
- </div>
400
- </transition>
401
- </div>
402
- </div>
403
- </template>
404
-
405
- <script>
406
- /**
407
- * EventCalendar —— 月历 / 日程日历组件
408
- * ==================================================================
409
- * 特性一览:
410
- * 1. 月份网格自动补全:上个月末尾 + 下个月开头,跨年正确,非当月置灰;
411
- * 2. 月份下拉切换 + 自动轮播(间隔可配,支持 hover/下拉/切后台自动暂停);
412
- * 3. 4 种切换动效(slide / fade / zoom / flip)与逐格入场,均可关闭;
413
- * 4. 字段名全部可映射(field-names),不用为后端字段改数据;
414
- * 5. 行高按「当行条目最多的那天」自动撑开,超过上限出现滚动条;
415
- * 6. 骨架屏、空状态、日期点击、条目点击、角标点击、丰富插槽。
416
- *
417
- * 组件不依赖任何第三方库,整个文件夹可直接拷贝到其它项目使用。
418
- */
419
- import MonthSelect from './MonthSelect.vue'
420
- import CalendarSkeleton from './CalendarSkeleton.vue'
421
- import {
422
- DEFAULT_FIELD_NAMES,
423
- DEFAULT_WEEK_LABELS,
424
- buildMonthMatrix,
425
- defaultDayFormatter,
426
- defaultMonthFormatter,
427
- formatMonthKey,
428
- getByPath
429
- } from './utils'
430
-
431
- /** 内置类型配色,可被 typeColors prop 覆盖或扩展 */
432
- const BUILTIN_TYPE_COLORS = {
433
- talk: { color: '#3d6cf0', bg: 'rgba(61, 108, 240, 0.10)' },
434
- fair: { color: '#e0821b', bg: 'rgba(224, 130, 27, 0.12)' },
435
- live: { color: '#10a37f', bg: 'rgba(16, 163, 127, 0.12)' },
436
- exam: { color: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.12)' },
437
- default: { color: '#6b7280', bg: 'rgba(107, 114, 128, 0.10)' }
438
- }
439
-
440
- /** 布局配置全部对应 EventCalendar 原生 Props,可直接用于 v-bind。 */
441
- const DEFAULT_LAYOUT_CONFIG = Object.freeze({
442
- itemAlign: 'left',
443
- calendarMargin: 0,
444
- calendarPadding: 0,
445
- headerPadding: '4px 4px 16px',
446
- cellPadding: '12px 10px',
447
- mobileCellPadding: '8px 6px',
448
- itemHorizontalPadding: 8,
449
- itemVerticalPadding: 4,
450
- itemGap: 6,
451
- // 溢出 / 行高 / 角标缩放:同样是对应原生 Props
452
- overflowMode: 'scroll',
453
- maxVisibleItems: 4,
454
- itemHeight: 28,
455
- minRowHeight: 118,
456
- badgeScale: 0.75,
457
- // 条目内部排版与外观
458
- itemLineGap: 2, // 名称与地址之间的行间距(px)
459
- itemBgGradient: false, // 条目背景是否启用渐变效果
460
- itemBgGradientDir: 'to-bottom', // 渐变方向:to-bottom 上到下 | to-right 左到右
461
- itemBgHover: true, // 悬浮时是否在底色上自动加深
462
- // 条目(.ec__item)颜色覆盖:空串 = 按 type/主题配色走
463
- itemTextColor: '',
464
- itemBgColor: ''
465
- })
466
-
467
- /**
468
- * 工具栏统一字段表:按展示顺序排列。
469
- * kind: seg=分段选项 | spacing=上下/左右两块 | number=单值步进 | color=取色
470
- */
471
- const CONFIG_FIELDS = Object.freeze([
472
- { kind: 'seg', key: 'itemAlign', label: '条目对齐', priority: 1, options: [
473
- { label: '靠左', value: 'left' },
474
- { label: '居中', value: 'center' }
475
- ] },
476
-
477
- { kind: 'spacing', key: 'cellPadding', label: '日期格内边距', priority: 2 },
478
- { kind: 'spacing', key: 'mobileCellPadding', label: '窄屏日期格内边距', priority: 3 },
479
-
480
- { kind: 'number', key: 'itemHorizontalPadding', label: '条目左右内边距', priority: 4, unit: 'px', step: 1 },
481
- { kind: 'number', key: 'itemVerticalPadding', label: '条目上下内边距', priority: 5, unit: 'px', step: 1 },
482
- { kind: 'number', key: 'itemGap', label: '条目间距', priority: 6, unit: 'px', step: 1 },
483
-
484
- { kind: 'number', key: 'maxVisibleItems', label: '单格最多显示', priority: 7, unit: '条', step: 1, min: 1 },
485
- { kind: 'number', key: 'itemHeight', label: '条目高度', priority: 8, unit: 'px', step: 1 },
486
- { kind: 'number', key: 'itemLineGap', label: '名称地址间距', priority: 9, unit: 'px', step: 1 },
487
-
488
- { kind: 'seg', key: 'itemBgGradient', label: '条目背景效果', priority: 10, options: [
489
- { label: '纯色', value: false },
490
- { label: '渐变', value: true }
491
- ] },
492
- { kind: 'seg', key: 'itemBgGradientDir', label: '渐变方向', priority: 11, options: [
493
- { label: '上到下', value: 'to-bottom' },
494
- { label: '左到右', value: 'to-right' }
495
- ] },
496
- { kind: 'seg', key: 'itemBgHover', label: '悬浮加深', priority: 12, options: [
497
- { label: '开启', value: true },
498
- { label: '关闭', value: false }
499
- ] },
500
-
501
- { kind: 'color', key: 'itemTextColor', label: '条目文字色', priority: 13, default: '#606266' },
502
- { kind: 'color', key: 'itemBgColor', label: '条目背景色', priority: 14, default: '#f5f7fa' }
503
- ])
504
-
505
- /** 间距控件的两个轴:vertical=上下(top/bottom),horizontal=左右(left/right) */
506
- const CONFIG_SPACING_AXES = Object.freeze([
507
- { key: 'vertical', label: '上下', sides: ['top', 'bottom'] },
508
- { key: 'horizontal', label: '左右', sides: ['left', 'right'] }
509
- ])
510
-
511
- function createLayoutConfig(vm) {
512
- return Object.keys(DEFAULT_LAYOUT_CONFIG).reduce((config, key) => {
513
- config[key] = vm[key]
514
- return config
515
- }, {})
516
- }
517
-
518
- function toCssSpacing(value, fallback) {
519
- if (typeof value === 'number' && Number.isFinite(value)) return `${Math.max(0, value)}px`
520
- if (typeof value === 'string' && value.trim()) return value.trim()
521
- return typeof fallback === 'number' ? `${fallback}px` : fallback
522
- }
523
-
524
- /**
525
- * 把 CSS 简写(或数字)解析成 { top, right, bottom, left } 的 px 数值。
526
- * 支持 1~4 个值,如 '12px 10px' -> { top:12, right:10, bottom:12, left:10 }。
527
- * 解析失败返回全 0,避免方向控件拿不到数。
528
- */
529
- function parseSpacingToSides(value) {
530
- const zero = { top: 0, right: 0, bottom: 0, left: 0 }
531
- let nums = null
532
- if (typeof value === 'number' && Number.isFinite(value)) {
533
- nums = [value, value, value, value]
534
- } else if (typeof value === 'string') {
535
- const tokens = value.trim().split(/\s+/).filter(Boolean)
536
- const parsed = tokens.map((token) => {
537
- const m = /^(-?\d+(?:\.\d+)?)(?:px)?$/i.exec(token)
538
- return m ? Number(m[1]) : null
539
- })
540
- if (parsed.length && parsed.every((n) => n != null && Number.isFinite(n))) nums = parsed
541
- }
542
- if (!nums) return zero
543
- const top = nums[0]
544
- const right = nums.length >= 2 ? nums[1] : top
545
- const bottom = nums.length >= 3 ? nums[2] : top
546
- const left = nums.length >= 4 ? nums[3] : right
547
- return { top, right, bottom, left }
548
- }
549
-
550
- /** 4 个方向的 px 合成 CSS 简写:全等/上下左右两两相等时尽量压缩成 1 项或 2 项 */
551
- function spacingFromSides(sides) {
552
- const px = (v) => `${Math.max(0, Math.round(v))}px`
553
- const top = sides.top
554
- const right = sides.right
555
- const bottom = sides.bottom
556
- const left = sides.left
557
- if (top === right && right === bottom && bottom === left) return px(top)
558
- if (top === bottom && right === left) return `${px(top)} ${px(right)}`
559
- return `${px(top)} ${px(right)} ${px(bottom)} ${px(left)}`
560
- }
561
-
562
- export default {
563
- name: 'EventCalendar',
564
-
565
- components: { MonthSelect, CalendarSkeleton },
566
-
567
- // 同时支持 v-model 与 :active-month.sync
568
- model: {
569
- prop: 'activeMonth',
570
- event: 'update:activeMonth'
571
- },
572
-
573
- props: {
574
- /* ----------------------------- 数据 ----------------------------- */
575
- /**
576
- * 月份数据数组,默认结构:
577
- * [{ month: '2026-09', monthLabel: '2026年9月',
578
- * days: [{ date: '2026-09-07', items: [{ id, label, badge, type }] }] }]
579
- * 也支持直接传扁平的日期数组(自动按日期归月)。
580
- */
581
- data: {
582
- type: Array,
583
- default: () => []
584
- },
585
- /** 字段映射,覆盖 DEFAULT_FIELD_NAMES 中的任意项,支持 'a.b' 点路径 */
586
- fieldNames: {
587
- type: Object,
588
- default: () => ({})
589
- },
590
- /** 加载中(显示骨架屏) */
591
- loading: {
592
- type: Boolean,
593
- default: false
594
- },
595
- /** 骨架屏行数 */
596
- skeletonRows: {
597
- type: Number,
598
- default: 5
599
- },
600
-
601
- /* --------------------------- 月份与轮播 --------------------------- */
602
- /** 当前月份 'YYYY-MM',支持 v-model / .sync;不传则取第一个月 */
603
- activeMonth: {
604
- type: String,
605
- default: ''
606
- },
607
- /** 是否展示月份下拉 */
608
- showMonthSelect: {
609
- type: Boolean,
610
- default: true
611
- },
612
- /** 是否展示左右翻月箭头 */
613
- showArrows: {
614
- type: Boolean,
615
- default: false
616
- },
617
- /** 开启自动轮播 */
618
- autoplay: {
619
- type: Boolean,
620
- default: false
621
- },
622
- /** 轮播间隔(毫秒),如 10000=10 秒、60000=1 分钟 */
623
- autoplayInterval: {
624
- type: Number,
625
- default: 60000
626
- },
627
- /** 轮播到最后一个月后是否回到第一个 */
628
- loop: {
629
- type: Boolean,
630
- default: true
631
- },
632
- /** 鼠标悬停时暂停轮播 */
633
- pauseOnHover: {
634
- type: Boolean,
635
- default: true
636
- },
637
- /** 展示轮播倒计时进度条 */
638
- showProgress: {
639
- type: Boolean,
640
- default: true
641
- },
642
- /** 展示轮播播放/暂停按钮 */
643
- showAutoplayToggle: {
644
- type: Boolean,
645
- default: true
646
- },
647
-
648
- /* ----------------------------- 动效 ----------------------------- */
649
- /** 月份切换动效:slide | fade | zoom | flip | none */
650
- transitionEffect: {
651
- type: String,
652
- default: 'slide',
653
- validator: (v) => ['slide', 'fade', 'zoom', 'flip', 'none'].includes(v)
654
- },
655
- /** 动效时长(毫秒) */
656
- transitionDuration: {
657
- type: Number,
658
- default: 420
659
- },
660
- /** 单元格逐个入场的错峰动画 */
661
- cellStagger: {
662
- type: Boolean,
663
- default: false
664
- },
665
-
666
- /* ----------------------------- 渲染 ----------------------------- */
667
- /** 顶部区域是否显示 */
668
- showHeader: {
669
- type: Boolean,
670
- default: true
671
- },
672
- /** 默认标题(header-left 插槽未传时生效) */
673
- title: {
674
- type: String,
675
- default: ''
676
- },
677
- /** 默认副标题 */
678
- subtitle: {
679
- type: String,
680
- default: ''
681
- },
682
- /** 一周起始日:0=周日(设计图默认)1=周一 */
683
- firstDayOfWeek: {
684
- type: Number,
685
- default: 0,
686
- validator: (v) => v >= 0 && v <= 6
687
- },
688
- /** 星期文案,下标对应 Date.getDay() */
689
- weekLabels: {
690
- type: Array,
691
- default: () => DEFAULT_WEEK_LABELS
692
- },
693
- /** 单日最多显示的条目数,超出按 overflowMode 处理 */
694
- maxVisibleItems: {
695
- type: Number,
696
- default: 4
697
- },
698
- /** 超出处理:scroll=出现滚动条 | collapse=折叠并显示「还有 N 项」 */
699
- overflowMode: {
700
- type: String,
701
- default: 'scroll',
702
- validator: (v) => ['scroll', 'collapse'].includes(v)
703
- },
704
- /** 单个条目高度(px),用于精确计算滚动区域高度 */
705
- itemHeight: {
706
- type: Number,
707
- default: 28
708
- },
709
- /** 条目间距(px) */
710
- itemGap: {
711
- type: Number,
712
- default: 6
713
- },
714
- /** 条目内容对齐方式 */
715
- itemAlign: {
716
- type: String,
717
- default: 'left',
718
- validator: (v) => ['left', 'center'].includes(v)
719
- },
720
- /** 日历根节点外边距;Number 按 px 处理,String 支持 CSS 简写 */
721
- calendarMargin: {
722
- type: [Number, String],
723
- default: DEFAULT_LAYOUT_CONFIG.calendarMargin
724
- },
725
- /** 日历根节点内边距;Number 按 px 处理,String 支持 CSS 简写 */
726
- calendarPadding: {
727
- type: [Number, String],
728
- default: DEFAULT_LAYOUT_CONFIG.calendarPadding
729
- },
730
- /** 顶部区域内边距;支持 CSS 简写 */
731
- headerPadding: {
732
- type: [Number, String],
733
- default: DEFAULT_LAYOUT_CONFIG.headerPadding
734
- },
735
- /** 日期格内边距;支持 CSS 简写 */
736
- cellPadding: {
737
- type: [Number, String],
738
- default: DEFAULT_LAYOUT_CONFIG.cellPadding
739
- },
740
- /** 768px 及以下日期格内边距;支持 CSS 简写 */
741
- mobileCellPadding: {
742
- type: [Number, String],
743
- default: DEFAULT_LAYOUT_CONFIG.mobileCellPadding
744
- },
745
- /** 条目左右内边距(px),传 0 可去除 */
746
- itemHorizontalPadding: {
747
- type: Number,
748
- default: 8,
749
- validator: (v) => v >= 0
750
- },
751
- /** 条目上下内边距(px) */
752
- itemVerticalPadding: {
753
- type: Number,
754
- default: DEFAULT_LAYOUT_CONFIG.itemVerticalPadding,
755
- validator: (v) => v >= 0
756
- },
757
- /** 显示内置布局配置工具栏;生产环境默认隐藏 */
758
- showConfigToolbar: {
759
- type: Boolean,
760
- default: false
761
- },
762
- /** 单元格最小高度(px),保证空行不塌陷 */
763
- minRowHeight: {
764
- type: Number,
765
- default: 118
766
- },
767
- /** 右上角角标整体缩放倍率(transform scale,越小越紧凑) */
768
- badgeScale: {
769
- type: Number,
770
- default: 0.75
771
- },
772
- /** 条目内名称与地址之间的行间距(px) */
773
- itemLineGap: {
774
- type: Number,
775
- default: 2,
776
- validator: (v) => v >= 0
777
- },
778
- /** 条目背景是否启用渐变效果(在所选底色上叠柔和渐变) */
779
- itemBgGradient: {
780
- type: Boolean,
781
- default: false
782
- },
783
- /** 渐变方向:to-bottom 上到下 | to-right 左到右(仅渐变开启时生效) */
784
- itemBgGradientDir: {
785
- type: String,
786
- default: 'to-bottom',
787
- validator: (v) => ['to-bottom', 'to-right'].includes(v)
788
- },
789
- /** 悬浮时是否自动把条目背景调淡一档(false 则保持原色) */
790
- itemBgHover: {
791
- type: Boolean,
792
- default: true
793
- },
794
- /** 相邻月份补位格子是否也渲染条目 */
795
- showAdjacentItems: {
796
- type: Boolean,
797
- default: true
798
- },
799
- /** 高亮今天 */
800
- highlightToday: {
801
- type: Boolean,
802
- default: true
803
- },
804
- /** 条目角标是否按 type 上色,false 则统一中性灰(更贴近原设计稿) */
805
- colorful: {
806
- type: Boolean,
807
- default: true
808
- },
809
- /** 类型配色扩展/覆盖:{ talk: { color, bg } } */
810
- typeColors: {
811
- type: Object,
812
- default: () => ({})
813
- },
814
- /** 日期文案格式化函数 (cell) => string */
815
- dayFormatter: {
816
- type: Function,
817
- default: defaultDayFormatter
818
- },
819
- /** 月份下拉文案格式化函数 (monthKey, rawMonthItem) => string */
820
- monthFormatter: {
821
- type: Function,
822
- default: null
823
- },
824
- /** 空数据文案 */
825
- emptyText: {
826
- type: String,
827
- default: '暂无日程数据'
828
- },
829
- /** 单元格是否可获得焦点(键盘无障碍) */
830
- cellFocusable: {
831
- type: Boolean,
832
- default: false
833
- },
834
- /** 条目(.ec__item)文字色覆盖,空串 = 按 type/主题配色 */
835
- itemTextColor: {
836
- type: String,
837
- default: ''
838
- },
839
- /** 条目(.ec__item)背景色覆盖,空串 = 按 type/主题配色 */
840
- itemBgColor: {
841
- type: String,
842
- default: ''
843
- },
844
- /** 主题变量覆盖,如 { primary: '#10a37f', radius: '12px' } */
845
- theme: {
846
- type: Object,
847
- default: () => ({})
848
- }
849
- },
850
-
851
- data() {
852
- return {
853
- /** 组件内部维护的当前月份 */
854
- innerMonth: '',
855
- /** 切换方向:next / prev,决定动画走向 */
856
- direction: 'next',
857
- /** 舞台高度,切换月份时用于高度过渡(行数可能 5 行变 6 行) */
858
- stageHeight: '',
859
- /** 用户主动点了暂停 */
860
- manualPaused: false,
861
- /** 鼠标悬停中 */
862
- hovering: false,
863
- /** 月份下拉展开中 */
864
- selectOpen: false,
865
- /** 页面切到后台 */
866
- pageHidden: false,
867
- /** 每次切换 +1,用于重置进度条 CSS 动画 */
868
- progressKey: 0,
869
- /** 骨架屏 -> 内容 的首次切换用淡入,避免莫名其妙的滑动 */
870
- useFadeOnce: true,
871
- /** 配置工具栏只编辑本地草稿,还原时以此为初始快照 */
872
- configDraft: createLayoutConfig(this),
873
- /** 初始配置快照:每个「还原」按钮回到这里 */
874
- configDefaults: createLayoutConfig(this),
875
- /** 复制配置按钮的即时反馈文案 */
876
- copyStatus: ''
877
- }
878
- },
879
-
880
- computed: {
881
- /* --------------------------- 字段映射 --------------------------- */
882
- fields() {
883
- return { ...DEFAULT_FIELD_NAMES, ...this.fieldNames }
884
- },
885
-
886
- externalLayoutConfig() {
887
- return createLayoutConfig(this)
888
- },
889
-
890
- effectiveLayoutConfig() {
891
- return this.showConfigToolbar ? this.configDraft : this.externalLayoutConfig
892
- },
893
-
894
- /** 工具栏统一字段表(seg / spacing / number / color) */
895
- configFields() {
896
- // 渐变方向只在启用渐变时才显示
897
- return CONFIG_FIELDS.filter((f) => !(f.key === 'itemBgGradientDir' && !this.configDraft.itemBgGradient))
898
- },
899
-
900
- /** 间距控件的「上下 / 左右」两组方向定义 */
901
- spacingAxes() {
902
- return CONFIG_SPACING_AXES
903
- },
904
-
905
- /** 实时生成的配置 JSON 文本(复制给用户 / 供展示) */
906
- configJsonText() {
907
- return JSON.stringify(this.getLayoutConfig(), null, 2)
908
- },
909
-
910
- /** 下面几个值统一从布局配置取:工具栏可见时用草稿,否则回落 Props */
911
- cfgItemHeight() {
912
- return this.effectiveLayoutConfig.itemHeight
913
- },
914
-
915
- cfgMaxVisibleItems() {
916
- return this.effectiveLayoutConfig.maxVisibleItems
917
- },
918
-
919
- cfgOverflowMode() {
920
- return this.effectiveLayoutConfig.overflowMode
921
- },
922
-
923
- cfgMinRowHeight() {
924
- return this.effectiveLayoutConfig.minRowHeight
925
- },
926
-
927
- cfgItemBgColor() {
928
- return this.effectiveLayoutConfig.itemBgColor
929
- },
930
-
931
- cfgItemBgGradient() {
932
- return this.effectiveLayoutConfig.itemBgGradient
933
- },
934
-
935
- cfgGradientDir() {
936
- return this.effectiveLayoutConfig.itemBgGradientDir
937
- },
938
-
939
- cfgItemBgHover() {
940
- return this.effectiveLayoutConfig.itemBgHover
941
- },
942
-
943
- /* --------------------------- 数据归一化 --------------------------- */
944
- /**
945
- * 把外部数据统一整理成 [{ month, label, raw }],
946
- * 同时兼容「扁平日期数组」的写法。
947
- */
948
- normalizedMonths() {
949
- const f = this.fields
950
- const list = Array.isArray(this.data) ? this.data : []
951
- if (!list.length) return []
952
-
953
- // 扁平结构判定:第一项没有 days 字段但有 date 字段
954
- const isFlat = getByPath(list[0], f.days) === undefined &&
955
- getByPath(list[0], f.date) !== undefined
956
-
957
- if (isFlat) {
958
- const monthSet = []
959
- list.forEach((day) => {
960
- const monthKey = formatMonthKey(getByPath(day, f.date))
961
- if (monthKey && !monthSet.includes(monthKey)) monthSet.push(monthKey)
962
- })
963
- return monthSet.sort().map((monthKey) => ({
964
- month: monthKey,
965
- label: this.resolveMonthLabel(monthKey, null),
966
- raw: null
967
- }))
968
- }
969
-
970
- return list
971
- .map((item) => {
972
- const monthKey = String(getByPath(item, f.month) || '')
973
- if (!monthKey) return null
974
- return {
975
- month: monthKey,
976
- label: this.resolveMonthLabel(monthKey, item),
977
- raw: item
978
- }
979
- })
980
- .filter(Boolean)
981
- },
982
-
983
- /** 月份下拉选项 */
984
- monthOptions() {
985
- return this.normalizedMonths.map((m) => ({ label: m.label, value: m.month }))
986
- },
987
-
988
- /** 'YYYY-MM-DD' -> 该日的条目数组(扁平化整份数据,便于补位格子取数) */
989
- dateItemsMap() {
990
- const f = this.fields
991
- const map = Object.create(null)
992
- const list = Array.isArray(this.data) ? this.data : []
993
- if (!list.length) return map
994
-
995
- const collectDay = (day) => {
996
- const dateKey = String(getByPath(day, f.date) || '').slice(0, 10)
997
- if (!dateKey) return
998
- const items = getByPath(day, f.items)
999
- if (!map[dateKey]) map[dateKey] = { items: [], raw: day }
1000
- if (Array.isArray(items)) map[dateKey].items.push(...items)
1001
- }
1002
-
1003
- const isFlat = getByPath(list[0], f.days) === undefined &&
1004
- getByPath(list[0], f.date) !== undefined
1005
-
1006
- if (isFlat) {
1007
- list.forEach(collectDay)
1008
- } else {
1009
- list.forEach((monthItem) => {
1010
- const days = getByPath(monthItem, f.days)
1011
- if (Array.isArray(days)) days.forEach(collectDay)
1012
- })
1013
- }
1014
- return map
1015
- },
1016
-
1017
- /** 当前月份在下拉列表中的索引 */
1018
- currentIndex() {
1019
- return this.monthOptions.findIndex((o) => o.value === this.innerMonth)
1020
- },
1021
-
1022
- /* --------------------------- 网格构建 --------------------------- */
1023
- /** 表头星期,按 firstDayOfWeek 旋转 */
1024
- weekHeaders() {
1025
- const labels = this.weekLabels.length === 7 ? this.weekLabels : DEFAULT_WEEK_LABELS
1026
- return Array.from({ length: 7 }, (_, i) => {
1027
- const weekday = (this.firstDayOfWeek + i) % 7
1028
- return {
1029
- weekday,
1030
- label: labels[weekday],
1031
- isWeekend: weekday === 0 || weekday === 6
1032
- }
1033
- })
1034
- },
1035
-
1036
- /**
1037
- * 最终渲染的格子:日期矩阵 + 当天条目(已按 maxVisibleItems 处理溢出)
1038
- */
1039
- renderCells() {
1040
- if (!this.innerMonth) return []
1041
- const f = this.fields
1042
- const cells = buildMonthMatrix(this.innerMonth, this.firstDayOfWeek)
1043
-
1044
- return cells.map((cell) => {
1045
- const hit = this.dateItemsMap[cell.dateKey]
1046
- const showItems = cell.isCurrentMonth || this.showAdjacentItems
1047
- const rawItems = showItems && hit ? hit.items : []
1048
-
1049
- // 归一化条目,模板里直接用,避免反复取值
1050
- const items = rawItems.map((raw, i) => ({
1051
- key: getByPath(raw, f.itemKey) != null
1052
- ? String(getByPath(raw, f.itemKey))
1053
- : `${cell.dateKey}-${i}`,
1054
- label: getByPath(raw, f.itemLabel),
1055
- address: getByPath(raw, f.itemAddress),
1056
- badge: getByPath(raw, f.itemBadge),
1057
- type: getByPath(raw, f.itemType),
1058
- disabled: !!getByPath(raw, f.itemDisabled),
1059
- raw
1060
- }))
1061
-
1062
- const overflowed = items.length > this.cfgMaxVisibleItems
1063
- const collapse = this.cfgOverflowMode === 'collapse' && overflowed
1064
- // collapse 模式留一行给「还有 N 项」
1065
- const visibleItems = collapse ? items.slice(0, this.cfgMaxVisibleItems - 1) : items
1066
-
1067
- return {
1068
- ...cell,
1069
- dayRaw: hit ? hit.raw : null,
1070
- items,
1071
- visibleItems,
1072
- moreCount: collapse ? items.length - visibleItems.length : 0,
1073
- /** scroll 模式下是否真的会出现滚动条 */
1074
- scrollable: this.cfgOverflowMode === 'scroll' && overflowed
1075
- }
1076
- })
1077
- },
1078
-
1079
- /* --------------------------- 样式相关 --------------------------- */
1080
- rootClass() {
1081
- return {
1082
- 'ec--loading': this.loading,
1083
- 'ec--stagger': this.cellStagger && !this.loading,
1084
- 'ec--item-center': this.effectiveLayoutConfig.itemAlign === 'center',
1085
- [`ec--effect-${this.transitionEffect}`]: true
1086
- }
1087
- },
1088
-
1089
- /** 主题:全部走 CSS 变量,方便外部覆盖 */
1090
- cssVars() {
1091
- const t = this.theme || {}
1092
- const n = this.cfgMaxVisibleItems
1093
- const layout = this.effectiveLayoutConfig
1094
- const vars = {
1095
- '--ec-primary': t.primary || '#3d6cf0',
1096
- '--ec-primary-ghost': t.primaryGhost || 'rgba(61, 108, 240, 0.12)',
1097
- '--ec-border': t.border || '#e4e7ed',
1098
- '--ec-border-light': t.borderLight || '#eef0f4',
1099
- '--ec-text': t.text || '#303133',
1100
- '--ec-text-secondary': t.textSecondary || '#606266',
1101
- '--ec-text-placeholder': t.textPlaceholder || '#c0c4cc',
1102
- '--ec-muted': t.muted || '#c8ccd4',
1103
- // 条目颜色:工具栏配置的条目色优先于 theme,theme 优先于默认
1104
- '--ec-item-bg': layout.itemBgColor || t.itemBg || '#f5f7fa',
1105
- '--ec-cell-hover': t.cellHover || '#fafbfd',
1106
- '--ec-radius': t.radius || '8px',
1107
- '--ec-duration': this.transitionDuration + 'ms',
1108
- '--ec-item-h': this.cfgItemHeight + 'px',
1109
- '--ec-badge-scale': Number.isFinite(Number(layout.badgeScale)) ? Number(layout.badgeScale) : 0.75,
1110
- '--ec-calendar-margin': toCssSpacing(layout.calendarMargin, DEFAULT_LAYOUT_CONFIG.calendarMargin),
1111
- '--ec-calendar-padding': toCssSpacing(layout.calendarPadding, DEFAULT_LAYOUT_CONFIG.calendarPadding),
1112
- '--ec-header-padding': toCssSpacing(layout.headerPadding, DEFAULT_LAYOUT_CONFIG.headerPadding),
1113
- '--ec-cell-padding': toCssSpacing(layout.cellPadding, DEFAULT_LAYOUT_CONFIG.cellPadding),
1114
- '--ec-cell-padding-mobile': toCssSpacing(layout.mobileCellPadding, DEFAULT_LAYOUT_CONFIG.mobileCellPadding),
1115
- '--ec-item-gap': layout.itemGap + 'px',
1116
- '--ec-item-padding-x': layout.itemHorizontalPadding + 'px',
1117
- '--ec-item-padding-y': layout.itemVerticalPadding + 'px',
1118
- '--ec-item-line-gap': layout.itemLineGap + 'px',
1119
- // 悬浮自动调淡一档(白色叠层);关闭则为透明
1120
- '--ec-item-hover-mask': layout.itemBgHover === false ? 'transparent' : 'rgba(255, 255, 255, 0.18)',
1121
- '--ec-min-row-h': this.cfgMinRowHeight + 'px',
1122
- // 滚动区域最大高度 = n 个条目 + (n-1) 个间距
1123
- '--ec-items-max-h': `${n * this.cfgItemHeight + (n - 1) * layout.itemGap}px`
1124
- }
1125
- // 条目文字色只在显式配置时覆盖;未配置时走 --ec-text-secondary(hover 走 --ec-text)
1126
- if (layout.itemTextColor) vars['--ec-item-text'] = layout.itemTextColor
1127
- return vars
1128
- },
1129
-
1130
- stageStyle() {
1131
- return this.stageHeight ? { height: this.stageHeight } : {}
1132
- },
1133
-
1134
- itemsStyle() {
1135
- // collapse 模式不限制高度(本来就已经截断了),scroll 模式限高触发滚动条
1136
- return this.cfgOverflowMode === 'scroll'
1137
- ? { maxHeight: 'var(--ec-items-max-h)' }
1138
- : {}
1139
- },
1140
-
1141
- /** 当前生效的过渡名 */
1142
- transitionName() {
1143
- if (this.transitionEffect === 'none') return 'ec-none'
1144
- if (this.useFadeOnce) return 'ec-fade'
1145
- if (this.transitionEffect === 'slide' || this.transitionEffect === 'flip') {
1146
- return `ec-${this.transitionEffect}-${this.direction}`
1147
- }
1148
- return `ec-${this.transitionEffect}`
1149
- },
1150
-
1151
- /* --------------------------- 轮播状态 --------------------------- */
1152
- /** 任一条件成立即暂停 */
1153
- isPaused() {
1154
- return (
1155
- this.manualPaused ||
1156
- this.pageHidden ||
1157
- this.selectOpen ||
1158
- (this.pauseOnHover && this.hovering)
1159
- )
1160
- },
1161
-
1162
- /** 轮播真正可运行的条件 */
1163
- autoplayRunnable() {
1164
- return this.autoplay && !this.loading && this.monthOptions.length > 1
1165
- },
1166
-
1167
- showProgressBar() {
1168
- return this.autoplay && this.showProgress && this.autoplayRunnable
1169
- },
1170
-
1171
- progressStyle() {
1172
- return {
1173
- animationDuration: this.autoplayInterval + 'ms',
1174
- animationPlayState: this.isPaused ? 'paused' : 'running'
1175
- }
1176
- },
1177
-
1178
- canGoPrev() {
1179
- return this.loop ? this.monthOptions.length > 1 : this.currentIndex > 0
1180
- },
1181
-
1182
- canGoNext() {
1183
- return this.loop
1184
- ? this.monthOptions.length > 1
1185
- : this.currentIndex > -1 && this.currentIndex < this.monthOptions.length - 1
1186
- },
1187
-
1188
- /** 顶部插槽可用的上下文 */
1189
- headerScope() {
1190
- return {
1191
- month: this.innerMonth,
1192
- monthLabel: (this.monthOptions[this.currentIndex] || {}).label || '',
1193
- monthIndex: this.currentIndex,
1194
- monthCount: this.monthOptions.length,
1195
- monthOptions: this.monthOptions,
1196
- loading: this.loading,
1197
- paused: this.isPaused,
1198
- prev: this.prev,
1199
- next: this.next,
1200
- goTo: this.goTo,
1201
- toggleAutoplay: this.toggleAutoplay
1202
- }
1203
- }
1204
- },
1205
-
1206
- watch: {
1207
- /** 外部改月份(v-model / .sync) */
1208
- activeMonth(val) {
1209
- if (val && val !== this.innerMonth) this.setMonth(val, 'prop')
1210
- },
1211
-
1212
- /** 父组件通过 v-bind 更新已保存配置时,同步到工具栏草稿。 */
1213
- externalLayoutConfig: {
1214
- deep: true,
1215
- handler(val) {
1216
- this.configDraft = { ...val }
1217
- }
1218
- },
1219
-
1220
- /** 数据变化后校正当前月份(比如接口返回了新的月份列表) */
1221
- monthOptions: {
1222
- immediate: true,
1223
- handler() {
1224
- this.ensureValidMonth()
1225
- }
1226
- },
1227
-
1228
- loading(val) {
1229
- // 加载结束后重新起播,并让「骨架屏 -> 内容」用淡入
1230
- if (val) {
1231
- this.useFadeOnce = true
1232
- this.stopAutoplay()
1233
- } else {
1234
- this.restartAutoplay()
1235
- }
1236
- },
1237
-
1238
- isPaused(val) {
1239
- if (val) this.pauseAutoplay()
1240
- else this.resumeAutoplay()
1241
- },
1242
-
1243
- autoplay() {
1244
- this.restartAutoplay()
1245
- },
1246
-
1247
- autoplayInterval() {
1248
- this.restartAutoplay()
1249
- }
1250
- },
1251
-
1252
- created() {
1253
- // 计时器相关的中间状态不需要响应式,挂在实例上即可
1254
- this._timer = null
1255
- this._tickStart = 0
1256
- this._remaining = 0
1257
- this._ro = null
1258
- },
1259
-
1260
- mounted() {
1261
- document.addEventListener('visibilitychange', this.handleVisibilityChange)
1262
- window.addEventListener('resize', this.syncStageHeight)
1263
- this.$nextTick(() => {
1264
- this.observeStage(this.$el.querySelector('.ec__panel'))
1265
- })
1266
- this.restartAutoplay()
1267
- },
1268
-
1269
- beforeDestroy() {
1270
- document.removeEventListener('visibilitychange', this.handleVisibilityChange)
1271
- window.removeEventListener('resize', this.syncStageHeight)
1272
- this.stopAutoplay()
1273
- this.disconnectObserver()
1274
- },
1275
-
1276
- methods: {
1277
- /* ========================= 布局配置 ========================= */
1278
- /** 间距字段某个轴的当前 px 值(vertical 取上下、horizontal 取左右) */
1279
- spacingAxisValue(key, axisKey) {
1280
- const sides = parseSpacingToSides(this.configDraft[key])
1281
- const axis = this.spacingAxes.find((a) => a.key === axisKey)
1282
- return axis ? sides[axis.sides[0]] : 0
1283
- },
1284
-
1285
- /** 间距字段的可读简写,如 12px 10px(也用于标题 tooltip) */
1286
- spacingShorthand(key) {
1287
- return spacingFromSides(parseSpacingToSides(this.configDraft[key]))
1288
- },
1289
-
1290
- /** 按「上下 / 左右」整轴步进 ±delta px */
1291
- nudgeSpacingAxis(key, axisKey, delta, event) {
1292
- const step = event && event.shiftKey ? 5 : 1
1293
- const sides = parseSpacingToSides(this.configDraft[key])
1294
- const axis = this.spacingAxes.find((a) => a.key === axisKey)
1295
- if (!axis) return
1296
- const current = sides[axis.sides[0]] || 0
1297
- const next = Math.max(0, Math.round(current + delta * step))
1298
- if (next === current) return
1299
- axis.sides.forEach((side) => { sides[side] = next })
1300
- this.updateLayoutConfig(key, spacingFromSides(sides))
1301
- },
1302
-
1303
- /** 直接输入间距字段某个轴(上下/左右)的值,两端一起生效 */
1304
- updateSpacingAxis(key, axisKey, event) {
1305
- const raw = Number(event.target.value)
1306
- const sides = parseSpacingToSides(this.configDraft[key])
1307
- const axis = this.spacingAxes.find((a) => a.key === axisKey)
1308
- if (!axis) return
1309
- const next = Number.isFinite(raw) ? Math.max(0, Math.round(raw)) : 0
1310
- axis.sides.forEach((side) => { sides[side] = next })
1311
- this.updateLayoutConfig(key, spacingFromSides(sides))
1312
- },
1313
-
1314
- /** 数值字段步进:步长取字段 step(整数 ±1、角标缩放 ±0.05),按住 Shift ×5 */
1315
- nudgeNumber(key, delta, event) {
1316
- const field = CONFIG_FIELDS.find((f) => f.key === key)
1317
- const unit = field && Number(field.step) ? Number(field.step) : 1
1318
- const multi = event && event.shiftKey ? 5 : 1
1319
- const current = Number(this.configDraft[key])
1320
- const base = Number.isFinite(current) ? current : 0
1321
- this.updateLayoutConfig(key, base + delta * unit * multi, 'number')
1322
- },
1323
-
1324
- /** 取色器展示值:未配置(空串)时给一个与默认主题一致的兜底色 */
1325
- colorValue(key) {
1326
- const hit = CONFIG_FIELDS.find((f) => f.key === key && f.kind === 'color')
1327
- return this.configDraft[key] || (hit ? hit.default : '#000000')
1328
- },
1329
-
1330
- /** 还原颜色为默认配色(存空串) */
1331
- resetColor(key) {
1332
- this.updateLayoutConfig(key, '')
1333
- },
1334
-
1335
- /** 还原某个配置项到初始快照(configDefaults) */
1336
- resetField(key) {
1337
- if (!Object.prototype.hasOwnProperty.call(this.configDefaults, key)) return
1338
- this.$set(this.configDraft, key, this.configDefaults[key])
1339
- this.copyStatus = ''
1340
- this.$emit('config-change', this.getLayoutConfig())
1341
- },
1342
-
1343
- updateLayoutConfig(key, value, inputType) {
1344
- let nextValue = value
1345
- if (inputType === 'number') {
1346
- const parsed = Number(value)
1347
- const field = CONFIG_FIELDS.find((f) => f.key === key)
1348
- const min = field && typeof field.min === 'number' ? field.min : 0
1349
- nextValue = Number.isFinite(parsed)
1350
- ? Math.max(min, Math.round(parsed * 100) / 100)
1351
- : min
1352
- }
1353
- this.$set(this.configDraft, key, nextValue)
1354
- this.copyStatus = ''
1355
- this.$emit('config-change', this.getLayoutConfig())
1356
- },
1357
-
1358
- /** 返回可直接 <EventCalendar v-bind="config" /> 的配置对象。 */
1359
- getLayoutConfig() {
1360
- return { ...this.effectiveLayoutConfig }
1361
- },
1362
-
1363
- /** 复制当前配置 JSON 到剪贴板(用户直接粘贴发回,研发贴到组件上即可) */
1364
- copyLayoutConfig() {
1365
- const json = this.configJsonText
1366
- const fallback = () => {
1367
- if (typeof document === 'undefined') {
1368
- this.copyStatus = '已复制,可粘贴发送给研发。'
1369
- return
1370
- }
1371
- const ta = document.createElement('textarea')
1372
- ta.value = json
1373
- ta.setAttribute('readonly', '')
1374
- ta.style.position = 'fixed'
1375
- ta.style.opacity = '0'
1376
- document.body.appendChild(ta)
1377
- ta.select()
1378
- let ok = false
1379
- try { ok = document.execCommand('copy') } catch (e) { ok = false }
1380
- document.body.removeChild(ta)
1381
- this.copyStatus = ok
1382
- ? '已复制,可粘贴发送给研发。'
1383
- : '复制失败,请手动全选下方 JSON 后复制。'
1384
- }
1385
- if (typeof navigator !== 'undefined' && navigator.clipboard && window.isSecureContext) {
1386
- navigator.clipboard.writeText(json).then(() => {
1387
- this.copyStatus = '已复制,可粘贴发送给研发。'
1388
- }, fallback)
1389
- } else {
1390
- fallback()
1391
- }
1392
- },
1393
-
1394
- /* ========================= 月份切换 ========================= */
1395
- /**
1396
- * 切换到指定月份
1397
- * @param {String} month 'YYYY-MM'
1398
- * @param {String} source 触发来源:select / arrow / autoplay / prop / api
1399
- */
1400
- setMonth(month, source = 'api') {
1401
- if (!month || month === this.innerMonth) return
1402
-
1403
- const from = this.currentIndex
1404
- const to = this.monthOptions.findIndex((o) => o.value === month)
1405
-
1406
- // 决定动画方向:轮播从末尾回到开头时仍按「向后」处理,视觉更连贯
1407
- if (from > -1 && to > -1) {
1408
- const wrapForward = source === 'autoplay' && to < from
1409
- this.direction = to > from || wrapForward ? 'next' : 'prev'
1410
- }
1411
-
1412
- // 第一次从骨架屏切到内容用淡入,之后才用配置的动效
1413
- if (this.innerMonth) this.useFadeOnce = false
1414
-
1415
- this.innerMonth = month
1416
- this.progressKey++
1417
-
1418
- this.$emit('update:activeMonth', month)
1419
- this.$emit('month-change', month, {
1420
- index: to,
1421
- source,
1422
- option: this.monthOptions[to] || null,
1423
- raw: (this.normalizedMonths[to] || {}).raw || null
1424
- })
1425
-
1426
- // 用户手动操作后重新计时,避免刚点完就被轮播切走
1427
- if (source !== 'autoplay') this.restartAutoplay()
1428
- },
1429
-
1430
- /** 上一个月 */
1431
- prev(source = 'arrow') {
1432
- const len = this.monthOptions.length
1433
- if (len < 2) return
1434
- let idx = this.currentIndex - 1
1435
- if (idx < 0) {
1436
- if (!this.loop) return
1437
- idx = len - 1
1438
- }
1439
- this.setMonth(this.monthOptions[idx].value, source)
1440
- },
1441
-
1442
- /** 下一个月 */
1443
- next(source = 'arrow') {
1444
- const len = this.monthOptions.length
1445
- if (len < 2) return
1446
- let idx = this.currentIndex + 1
1447
- if (idx >= len) {
1448
- if (!this.loop) {
1449
- this.stopAutoplay()
1450
- this.$emit('autoplay-end')
1451
- return
1452
- }
1453
- idx = 0
1454
- }
1455
- this.setMonth(this.monthOptions[idx].value, source)
1456
- },
1457
-
1458
- /** 跳到第 n 个月份(0 开始) */
1459
- goTo(index, source = 'api') {
1460
- const opt = this.monthOptions[index]
1461
- if (opt) this.setMonth(opt.value, source)
1462
- },
1463
-
1464
- /** 当前月份失效时(数据变更/首次加载)自动落到一个合法值 */
1465
- ensureValidMonth() {
1466
- const options = this.monthOptions
1467
- if (!options.length) {
1468
- this.innerMonth = ''
1469
- return
1470
- }
1471
- const exists = options.some((o) => o.value === this.innerMonth)
1472
- if (exists) return
1473
-
1474
- const preferred = options.some((o) => o.value === this.activeMonth)
1475
- ? this.activeMonth
1476
- : options[0].value
1477
-
1478
- this.innerMonth = preferred
1479
- if (preferred !== this.activeMonth) this.$emit('update:activeMonth', preferred)
1480
- },
1481
-
1482
- resolveMonthLabel(monthKey, rawItem) {
1483
- if (typeof this.monthFormatter === 'function') {
1484
- return this.monthFormatter(monthKey, rawItem)
1485
- }
1486
- const custom = rawItem ? getByPath(rawItem, this.fields.monthLabel) : null
1487
- return custom || defaultMonthFormatter(monthKey)
1488
- },
1489
-
1490
- /* ========================= 轮播控制 ========================= */
1491
- /** 重新开始计时(完整间隔) */
1492
- restartAutoplay() {
1493
- this.stopAutoplay()
1494
- if (!this.autoplayRunnable) return
1495
- this._remaining = this.autoplayInterval
1496
- this.progressKey++
1497
- if (!this.isPaused) this.scheduleTick(this._remaining)
1498
- },
1499
-
1500
- /** 挂一个定时器,到点切下一个月 */
1501
- scheduleTick(delay) {
1502
- this.clearTimer()
1503
- this._tickStart = Date.now()
1504
- this._timer = setTimeout(() => {
1505
- this._timer = null
1506
- this.next('autoplay')
1507
- // next() 内部不会重置计时(source=autoplay),这里续上下一轮
1508
- this._remaining = this.autoplayInterval
1509
- this.progressKey++
1510
- if (!this.isPaused && this.autoplayRunnable) this.scheduleTick(this._remaining)
1511
- }, delay)
1512
- },
1513
-
1514
- /** 暂停:把剩余时间记下来,恢复时接着走,进度条不会跳 */
1515
- pauseAutoplay() {
1516
- if (!this._timer) return
1517
- const elapsed = Date.now() - this._tickStart
1518
- this._remaining = Math.max(0, this._remaining - elapsed)
1519
- this.clearTimer()
1520
- },
1521
-
1522
- resumeAutoplay() {
1523
- if (this._timer || !this.autoplayRunnable || this.isPaused) return
1524
- this.scheduleTick(this._remaining > 0 ? this._remaining : this.autoplayInterval)
1525
- },
1526
-
1527
- stopAutoplay() {
1528
- this.clearTimer()
1529
- this._remaining = this.autoplayInterval
1530
- },
1531
-
1532
- clearTimer() {
1533
- if (this._timer) {
1534
- clearTimeout(this._timer)
1535
- this._timer = null
1536
- }
1537
- },
1538
-
1539
- /** 播放 / 暂停按钮 */
1540
- toggleAutoplay() {
1541
- this.manualPaused = !this.manualPaused
1542
- this.$emit('autoplay-toggle', !this.manualPaused)
1543
- },
1544
-
1545
- handleMouseEnter() {
1546
- this.hovering = true
1547
- },
1548
-
1549
- handleMouseLeave() {
1550
- this.hovering = false
1551
- },
1552
-
1553
- handleVisibilityChange() {
1554
- this.pageHidden = document.hidden
1555
- },
1556
-
1557
- handleSelectChange(value) {
1558
- this.setMonth(value, 'select')
1559
- },
1560
-
1561
- handleSelectVisibleChange(visible) {
1562
- this.selectOpen = visible
1563
- },
1564
-
1565
- /* ========================= 舞台高度 ========================= */
1566
- /**
1567
- * 月份行数可能是 5 行或 6 行,直接切换会导致高度突变。
1568
- * 这里显式把舞台高度锁到当前面板高度,配合 CSS transition 平滑过渡。
1569
- */
1570
- observeStage(el) {
1571
- this.disconnectObserver()
1572
- if (!el) return
1573
-
1574
- this.syncStageHeightOf(el)
1575
-
1576
- if (typeof ResizeObserver !== 'undefined') {
1577
- this._ro = new ResizeObserver(() => this.syncStageHeightOf(el))
1578
- this._ro.observe(el)
1579
- }
1580
- },
1581
-
1582
- disconnectObserver() {
1583
- if (this._ro) {
1584
- this._ro.disconnect()
1585
- this._ro = null
1586
- }
1587
- },
1588
-
1589
- syncStageHeightOf(el) {
1590
- if (!el || !el.offsetParent) return
1591
- const h = el.offsetHeight
1592
- if (h > 0) this.stageHeight = h + 'px'
1593
- },
1594
-
1595
- /** 窗口尺寸变化时重新测量(文字换行会改变行高) */
1596
- syncStageHeight() {
1597
- const el = this.$el && this.$el.querySelector('.ec__panel:not([class*="-leave-"])')
1598
- this.syncStageHeightOf(el)
1599
- },
1600
-
1601
- /* ========================= 过渡钩子 ========================= */
1602
- handleBeforeEnter(el) {
1603
- // 进场元素先量一次高度,让 stage 的高度动画和内容切换同步开始
1604
- this.$nextTick(() => this.syncStageHeightOf(el))
1605
- },
1606
-
1607
- handleEnter(el) {
1608
- // 面板是新 DOM,需要重新观察
1609
- this.observeStage(el)
1610
- },
1611
-
1612
- handleAfterEnter(el) {
1613
- this.syncStageHeightOf(el)
1614
- this.$emit('transition-end', this.innerMonth)
1615
- },
1616
-
1617
- /* ========================= 交互事件 ========================= */
1618
- /** 单元格样式类 */
1619
- cellClass(cell) {
1620
- return {
1621
- 'is-other-month': !cell.isCurrentMonth,
1622
- 'is-today': cell.isToday && this.highlightToday,
1623
- 'is-weekend': cell.isWeekend,
1624
- 'has-items': cell.items.length > 0
1625
- }
1626
- },
1627
-
1628
- /** 传给 day / day-header / items / day-empty 插槽的上下文 */
1629
- cellScope(cell) {
1630
- return {
1631
- day: cell,
1632
- date: cell.date,
1633
- dateKey: cell.dateKey,
1634
- items: cell.items,
1635
- visibleItems: cell.visibleItems,
1636
- raw: cell.dayRaw,
1637
- isCurrentMonth: cell.isCurrentMonth,
1638
- isToday: cell.isToday,
1639
- isWeekend: cell.isWeekend,
1640
- label: this.dayFormatter(cell)
1641
- }
1642
- },
1643
-
1644
- /** 传给 item 插槽的上下文 */
1645
- itemScope(entry, cell, index) {
1646
- return {
1647
- item: entry.raw,
1648
- raw: entry.raw,
1649
- label: entry.label,
1650
- address: entry.address,
1651
- badge: entry.badge,
1652
- type: entry.type,
1653
- disabled: entry.disabled,
1654
- index,
1655
- day: cell,
1656
- dateKey: cell.dateKey
1657
- }
1658
- },
1659
-
1660
- handleDayClick(cell, event) {
1661
- this.$emit('day-click', this.cellScope(cell), event)
1662
- },
1663
-
1664
- handleItemClick(entry, cell, index, event) {
1665
- if (entry.disabled) return
1666
- this.$emit('item-click', this.itemScope(entry, cell, index), event)
1667
- },
1668
-
1669
- handleBadgeClick(entry, cell, index, event) {
1670
- if (entry.disabled) return
1671
- // 未监听 tag-click 时,点角标退化成点条目,避免「点了没反应」
1672
- if (this.$listeners['tag-click']) {
1673
- this.$emit('tag-click', this.itemScope(entry, cell, index), event)
1674
- } else {
1675
- this.handleItemClick(entry, cell, index, event)
1676
- }
1677
- },
1678
-
1679
- handleMoreClick(cell, event) {
1680
- this.$emit('more-click', this.cellScope(cell), event)
1681
- },
1682
-
1683
- hasText(value) {
1684
- return value !== undefined && value !== null && String(value).trim() !== ''
1685
- },
1686
-
1687
- itemTitle(entry) {
1688
- const label = this.hasText(entry.label) ? String(entry.label) : ''
1689
- const address = this.hasText(entry.address) ? `\n${entry.address}` : ''
1690
- return label + address
1691
- },
1692
-
1693
- /** 大学名称超过 7 个字符时使用 12px,否则使用 14px。 */
1694
- isLongItemLabel(label) {
1695
- const text = label == null ? '' : String(label)
1696
- return text.length > 7
1697
- },
1698
-
1699
- /* ========================= 配色 ========================= */
1700
- resolveTypeColor(type) {
1701
- const merged = { ...BUILTIN_TYPE_COLORS, ...this.typeColors }
1702
- return merged[type] || merged.default || BUILTIN_TYPE_COLORS.default
1703
- },
1704
-
1705
- itemStyle(entry) {
1706
- if (!this.colorful || !entry.type) return {}
1707
- const c = this.resolveTypeColor(entry.type)
1708
- // 只把颜色写进 CSS 变量,具体怎么用交给样式表,方便外部再覆盖
1709
- return { '--ec-item-accent': c.color, '--ec-item-accent-bg': c.bg }
1710
- },
1711
-
1712
- badgeStyle(entry) {
1713
- if (!this.colorful || !entry.type) return {}
1714
- const c = this.resolveTypeColor(entry.type)
1715
- return { color: c.color, backgroundColor: c.bg }
1716
- }
1717
- }
1718
- }
1719
- </script>
1720
-
1721
- <style scoped>
1722
- /* ==================================================================
1723
- * 布局:整体用 CSS Grid,7 列等宽。
1724
- * 同一行的格子会自动拉伸到该行最高格子的高度,
1725
- * 因此「行高取决于条目最多的那一天」是天然满足的,无需 JS 计算。
1726
- * ================================================================== */
1727
- .ec {
1728
- position: relative;
1729
- box-sizing: border-box;
1730
- width: auto;
1731
- margin: var(--ec-calendar-margin);
1732
- padding: var(--ec-calendar-padding);
1733
- color: var(--ec-text);
1734
- font-size: 14px;
1735
- line-height: 1.5;
1736
- -webkit-font-smoothing: antialiased;
1737
- }
1738
-
1739
- /* -------------------------- 布局配置工具栏 -------------------------- */
1740
- .ec__config {
1741
- margin-bottom: 16px;
1742
- padding: 14px;
1743
- border: 1px solid var(--ec-border);
1744
- border-radius: calc(var(--ec-radius) + 2px);
1745
- background: #f8fafc;
1746
- }
1747
-
1748
- .ec__config-head,
1749
- .ec__config-actions,
1750
- .ec__config-segment {
1751
- display: flex;
1752
- align-items: center;
1753
- }
1754
-
1755
- .ec__config-head {
1756
- justify-content: space-between;
1757
- gap: 12px;
1758
- margin-bottom: 12px;
1759
- }
1760
-
1761
- .ec__config-title {
1762
- color: var(--ec-text);
1763
- font-size: 14px;
1764
- }
1765
-
1766
- .ec__config-caption {
1767
- margin-left: 8px;
1768
- color: var(--ec-text-secondary);
1769
- font-size: 12px;
1770
- }
1771
-
1772
- .ec__config-actions {
1773
- gap: 8px;
1774
- }
1775
-
1776
- .ec__config-btn,
1777
- .ec__config-segment button {
1778
- min-height: 34px;
1779
- border: 1px solid var(--ec-border);
1780
- border-radius: 6px;
1781
- background: #fff;
1782
- color: var(--ec-text-secondary);
1783
- font: inherit;
1784
- font-size: 12px;
1785
- cursor: pointer;
1786
- transition: color 0.18s, border-color 0.18s, background-color 0.18s;
1787
- }
1788
-
1789
- .ec__config-btn {
1790
- padding: 0 12px;
1791
- }
1792
-
1793
- .ec__config-btn.is-primary,
1794
- .ec__config-segment button.is-active {
1795
- border-color: var(--ec-primary);
1796
- background: var(--ec-primary);
1797
- color: #fff;
1798
- }
1799
-
1800
- .ec__config-btn:hover,
1801
- .ec__config-segment button:hover {
1802
- border-color: var(--ec-primary);
1803
- color: var(--ec-primary);
1804
- }
1805
-
1806
- .ec__config-btn.is-primary:hover,
1807
- .ec__config-segment button.is-active:hover {
1808
- color: #fff;
1809
- opacity: 0.9;
1810
- }
1811
-
1812
- .ec__config-btn:focus-visible,
1813
- .ec__config-segment button:focus-visible,
1814
- .ec__stepper:focus-within,
1815
- .ec__reset-btn:focus-visible,
1816
- .ec__config input[type='color']:focus-visible {
1817
- outline: 2px solid var(--ec-primary);
1818
- outline-offset: 2px;
1819
- }
1820
-
1821
- /* 自带 +/− 步进按钮,隐藏数字框原生上下箭头 */
1822
- .ec__config input[type='number'] {
1823
- -moz-appearance: textfield;
1824
- appearance: textfield;
1825
- }
1826
-
1827
- .ec__config input[type='number']::-webkit-outer-spin-button,
1828
- .ec__config input[type='number']::-webkit-inner-spin-button {
1829
- -webkit-appearance: none;
1830
- margin: 0;
1831
- }
1832
-
1833
- /* 取色器只保留色块 */
1834
- .ec__config input[type='color'] {
1835
- appearance: none;
1836
- -webkit-appearance: none;
1837
- width: 34px;
1838
- height: 34px;
1839
- padding: 0;
1840
- border: 0;
1841
- border-radius: 8px;
1842
- background: transparent;
1843
- cursor: pointer;
1844
- }
1845
-
1846
- .ec__config input[type='color']::-webkit-color-swatch-wrapper {
1847
- padding: 0;
1848
- }
1849
-
1850
- .ec__config input[type='color']::-webkit-color-swatch,
1851
- .ec__config input[type='color']::-moz-color-swatch {
1852
- border: 1px solid var(--ec-border);
1853
- border-radius: 8px;
1854
- }
1855
-
1856
- /* 字段网格:默认 5 列一排,卡片更小更矮 */
1857
- .ec__config-list {
1858
- display: grid;
1859
- grid-template-columns: repeat(5, minmax(0, 1fr));
1860
- gap: 8px;
1861
- }
1862
-
1863
- /* 每个字段一张白色小卡片 */
1864
- .ec__config-block {
1865
- min-width: 0;
1866
- margin: 0;
1867
- padding: 8px;
1868
- border: 1px solid var(--ec-border);
1869
- border-radius: 8px;
1870
- background: #fff;
1871
- }
1872
-
1873
- .ec__config-block--seg {
1874
- display: flex;
1875
- flex-direction: column;
1876
- }
1877
-
1878
- .ec__config-label {
1879
- display: flex;
1880
- align-items: baseline;
1881
- gap: 3px;
1882
- flex-wrap: wrap;
1883
- margin: 0 0 6px;
1884
- color: var(--ec-text-secondary);
1885
- font-size: 11px;
1886
- line-height: 16px;
1887
- }
1888
-
1889
- .ec__config-block--color .ec__config-label {
1890
- margin: 0;
1891
- }
1892
-
1893
- .ec__config-priority {
1894
- color: var(--ec-primary);
1895
- font-weight: 600;
1896
- }
1897
-
1898
- .ec__config-shorthand {
1899
- margin-left: 6px;
1900
- max-width: 55%;
1901
- overflow: hidden;
1902
- color: var(--ec-text-placeholder);
1903
- font-family: Consolas, Monaco, monospace;
1904
- font-size: 10px;
1905
- font-weight: 400;
1906
- text-overflow: ellipsis;
1907
- white-space: nowrap;
1908
- }
1909
-
1910
- .ec__config-segment button {
1911
- min-height: 28px;
1912
- flex: 1;
1913
- padding: 0 8px;
1914
- }
1915
-
1916
- .ec__config-segment button + button {
1917
- margin-left: 6px;
1918
- }
1919
-
1920
- /* ------------ 间距控件:每个字段只拆「上下」「左右」两大块 ------------ */
1921
- .ec__spacing {
1922
- display: flex;
1923
- flex-direction: column;
1924
- gap: 4px;
1925
- }
1926
-
1927
- .ec__spacing-row {
1928
- display: flex;
1929
- align-items: center;
1930
- gap: 6px;
1931
- }
1932
-
1933
- .ec__spacing-row-label {
1934
- flex: none;
1935
- width: 30px;
1936
- color: var(--ec-text-secondary);
1937
- font-size: 11px;
1938
- }
1939
-
1940
- .ec__spacing-row .ec__stepper {
1941
- flex: 1;
1942
- min-width: 0;
1943
- }
1944
-
1945
- /* ------------ 单值数值字段:输入框左右包 −/+ ------------ */
1946
- .ec__stepper {
1947
- display: flex;
1948
- align-items: stretch;
1949
- height: 30px;
1950
- border: 1px solid var(--ec-border);
1951
- border-radius: 6px;
1952
- background: #fff;
1953
- }
1954
-
1955
- .ec__step-btn {
1956
- flex: none;
1957
- width: 27px;
1958
- border: 0;
1959
- background: transparent;
1960
- color: var(--ec-text-secondary);
1961
- font-size: 15px;
1962
- line-height: 1;
1963
- cursor: pointer;
1964
- transition: color 0.16s, background-color 0.16s;
1965
- }
1966
-
1967
- .ec__step-btn:first-child {
1968
- border-right: 1px solid var(--ec-border-light);
1969
- }
1970
-
1971
- .ec__step-btn:last-child {
1972
- border-left: 1px solid var(--ec-border-light);
1973
- }
1974
-
1975
- .ec__step-btn:hover {
1976
- color: var(--ec-primary);
1977
- background: var(--ec-primary-ghost);
1978
- }
1979
-
1980
- .ec__stepper-input {
1981
- position: relative;
1982
- flex: 1;
1983
- min-width: 0;
1984
- }
1985
-
1986
- .ec__stepper-input input {
1987
- width: 100%;
1988
- height: 100%;
1989
- border: 0;
1990
- outline: 0;
1991
- padding: 0 22px 0 4px;
1992
- background: transparent;
1993
- color: var(--ec-text);
1994
- font: inherit;
1995
- font-size: 11px;
1996
- text-align: center;
1997
- }
1998
-
1999
- .ec__stepper-unit {
2000
- position: absolute;
2001
- top: 50%;
2002
- right: 6px;
2003
- transform: translateY(-50%);
2004
- color: var(--ec-text-placeholder);
2005
- font-size: 10px;
2006
- pointer-events: none;
2007
- }
2008
-
2009
- /* ------------ 颜色控件(并入字段网格的小卡片) ------------ */
2010
- .ec__config-block--color {
2011
- display: flex;
2012
- align-items: center;
2013
- gap: 8px;
2014
- }
2015
-
2016
- .ec__config-block--color input[type='color'] {
2017
- width: 28px;
2018
- height: 28px;
2019
- border-radius: 6px;
2020
- }
2021
-
2022
- .ec__color-swatch {
2023
- flex: none;
2024
- width: 30px;
2025
- height: 30px;
2026
- }
2027
-
2028
- .ec__color-meta {
2029
- display: flex;
2030
- flex: 1;
2031
- flex-direction: column;
2032
- min-width: 0;
2033
- }
2034
-
2035
- .ec__color-hex {
2036
- margin-top: 1px;
2037
- color: var(--ec-text);
2038
- font-family: Consolas, Monaco, monospace;
2039
- font-size: 11px;
2040
- }
2041
-
2042
- .ec__reset-btn {
2043
- flex: none;
2044
- height: 24px;
2045
- padding: 0 7px;
2046
- border: 1px solid var(--ec-border);
2047
- border-radius: 6px;
2048
- background: #fff;
2049
- color: var(--ec-text-secondary);
2050
- font-size: 10px;
2051
- cursor: pointer;
2052
- }
2053
-
2054
- .ec__reset-btn:disabled {
2055
- cursor: not-allowed;
2056
- opacity: 0.45;
2057
- }
2058
-
2059
- .ec__reset-btn:hover:not(:disabled) {
2060
- border-color: var(--ec-primary);
2061
- color: var(--ec-primary);
2062
- }
2063
-
2064
- /* 卡片标题行右上角的「还原」 */
2065
- .ec__config-reset {
2066
- flex: none;
2067
- align-self: center;
2068
- margin-left: auto;
2069
- padding: 0 6px;
2070
- height: 20px;
2071
- border: 1px solid transparent;
2072
- border-radius: 5px;
2073
- background: transparent;
2074
- color: var(--ec-text-placeholder);
2075
- font-size: 10px;
2076
- line-height: 1;
2077
- cursor: pointer;
2078
- transition: color 0.16s, border-color 0.16s, background-color 0.16s;
2079
- }
2080
-
2081
- .ec__config-reset:hover {
2082
- border-color: var(--ec-border);
2083
- background: #fff;
2084
- color: var(--ec-primary);
2085
- }
2086
-
2087
- /* ------------ 配置 JSON 输出区 ------------ */
2088
- .ec__config-output {
2089
- margin-top: 10px;
2090
- }
2091
-
2092
- .ec__config-json {
2093
- max-height: 170px;
2094
- margin: 0;
2095
- padding: 10px 12px;
2096
- overflow: auto;
2097
- border-radius: 8px;
2098
- background: #111827;
2099
- color: #e5e7eb;
2100
- font: 11px/1.6 Consolas, Monaco, monospace;
2101
- white-space: pre-wrap;
2102
- word-break: break-word;
2103
- }
2104
-
2105
- .ec__config-feedback {
2106
- min-height: 16px;
2107
- margin-top: 6px;
2108
- color: #059669;
2109
- font-size: 12px;
2110
- }
2111
-
2112
- @media (max-width: 1100px) {
2113
- .ec__config-list {
2114
- grid-template-columns: repeat(4, minmax(0, 1fr));
2115
- }
2116
- }
2117
-
2118
- @media (max-width: 900px) {
2119
- .ec__config-list {
2120
- grid-template-columns: repeat(3, minmax(0, 1fr));
2121
- }
2122
- }
2123
-
2124
- @media (max-width: 640px) {
2125
- .ec__config-list {
2126
- grid-template-columns: repeat(2, minmax(0, 1fr));
2127
- }
2128
- .ec__spacing-row-label {
2129
- width: 26px;
2130
- }
2131
- }
2132
-
2133
- @media (max-width: 420px) {
2134
- .ec__config-list {
2135
- grid-template-columns: 1fr;
2136
- }
2137
- }
2138
-
2139
- .ec *,
2140
- .ec *::before,
2141
- .ec *::after {
2142
- box-sizing: border-box;
2143
- }
2144
-
2145
- /* ------------------------------ 顶部 ------------------------------ */
2146
- .ec__header {
2147
- display: flex;
2148
- align-items: center;
2149
- justify-content: space-between;
2150
- gap: 16px;
2151
- padding: var(--ec-header-padding);
2152
- }
2153
-
2154
- .ec__header-left {
2155
- flex: 1;
2156
- min-width: 0;
2157
- }
2158
-
2159
- .ec__header-right {
2160
- display: flex;
2161
- align-items: center;
2162
- gap: 8px;
2163
- flex: none;
2164
- }
2165
-
2166
- .ec__title-group {
2167
- display: flex;
2168
- align-items: baseline;
2169
- gap: 10px;
2170
- flex-wrap: wrap;
2171
- }
2172
-
2173
- .ec__title {
2174
- margin: 0;
2175
- font-size: 20px;
2176
- font-weight: 600;
2177
- color: var(--ec-text);
2178
- }
2179
-
2180
- .ec__subtitle {
2181
- font-size: 13px;
2182
- color: var(--ec-text-secondary);
2183
- }
2184
-
2185
- .ec__icon-btn {
2186
- display: inline-flex;
2187
- align-items: center;
2188
- justify-content: center;
2189
- width: 34px;
2190
- height: 34px;
2191
- padding: 0;
2192
- border: 1px solid var(--ec-border);
2193
- border-radius: var(--ec-radius);
2194
- background: #fff;
2195
- color: var(--ec-text-secondary);
2196
- cursor: pointer;
2197
- transition: color 0.2s, border-color 0.2s, background-color 0.2s;
2198
- }
2199
-
2200
- .ec__icon-btn svg {
2201
- width: 16px;
2202
- height: 16px;
2203
- }
2204
-
2205
- .ec__icon-btn:hover:not(:disabled) {
2206
- color: var(--ec-primary);
2207
- border-color: var(--ec-primary);
2208
- background: var(--ec-primary-ghost);
2209
- }
2210
-
2211
- .ec__icon-btn:disabled {
2212
- cursor: not-allowed;
2213
- opacity: 0.45;
2214
- }
2215
-
2216
- /* ---------------------------- 轮播进度条 ---------------------------- */
2217
- .ec__progress {
2218
- height: 2px;
2219
- margin-bottom: 8px;
2220
- border-radius: 2px;
2221
- background: var(--ec-border-light);
2222
- overflow: hidden;
2223
- }
2224
-
2225
- .ec__progress-inner {
2226
- height: 100%;
2227
- width: 0;
2228
- border-radius: 2px;
2229
- background: var(--ec-primary);
2230
- animation-name: ec-progress;
2231
- animation-timing-function: linear;
2232
- animation-fill-mode: forwards;
2233
- }
2234
-
2235
- @keyframes ec-progress {
2236
- from { width: 0; }
2237
- to { width: 100%; }
2238
- }
2239
-
2240
- /* ---------------------------- 星期表头 ---------------------------- */
2241
- .ec__weekdays {
2242
- display: grid;
2243
- grid-template-columns: repeat(7, minmax(0, 1fr));
2244
- border-bottom: 1px solid var(--ec-border);
2245
- }
2246
-
2247
- .ec__weekday {
2248
- padding: 10px 8px;
2249
- text-align: center;
2250
- font-size: 14px;
2251
- font-weight: 500;
2252
- color: var(--ec-text-secondary);
2253
- }
2254
-
2255
- .ec__weekday.is-weekend {
2256
- color: var(--ec-text-placeholder);
2257
- }
2258
-
2259
- /* ---------------------------- 舞台 / 面板 ---------------------------- */
2260
- .ec__stage {
2261
- position: relative;
2262
- overflow: hidden;
2263
- transition: height var(--ec-duration) cubic-bezier(0.4, 0, 0.2, 1);
2264
- }
2265
-
2266
- .ec__panel {
2267
- width: 100%;
2268
- }
2269
-
2270
- .ec__grid {
2271
- display: grid;
2272
- grid-template-columns: repeat(7, minmax(0, 1fr));
2273
- /* 行高下限固定,上限交给内容撑开 —— 同行等高由 grid 保证 */
2274
- grid-auto-rows: minmax(var(--ec-min-row-h), auto);
2275
- }
2276
-
2277
- /* ------------------------------ 单元格 ------------------------------ */
2278
- .ec__cell {
2279
- display: flex;
2280
- flex-direction: column;
2281
- min-width: 0;
2282
- padding: var(--ec-cell-padding);
2283
- border-right: 1px solid var(--ec-border-light);
2284
- border-bottom: 1px solid var(--ec-border-light);
2285
- transition: background-color 0.2s;
2286
- }
2287
-
2288
- .ec__cell:nth-child(7n) {
2289
- border-right: none;
2290
- }
2291
-
2292
- /* 最后一行去掉底边框 */
2293
- .ec__grid > .ec__cell:nth-last-child(-n + 7) {
2294
- border-bottom: none;
2295
- }
2296
-
2297
- .ec__cell:hover {
2298
- background: var(--ec-cell-hover);
2299
- }
2300
-
2301
- .ec__cell:focus-visible {
2302
- outline: 2px solid var(--ec-primary);
2303
- outline-offset: -2px;
2304
- }
2305
-
2306
- /* 非当月日期整体置灰 */
2307
- .ec__cell.is-other-month {
2308
- background: #fcfcfd;
2309
- }
2310
-
2311
- .ec__cell.is-other-month .ec__day-label {
2312
- color: var(--ec-muted);
2313
- }
2314
-
2315
- .ec__cell.is-other-month .ec__item {
2316
- opacity: 0.55;
2317
- }
2318
-
2319
- /* ------------------------------ 日期行 ------------------------------ */
2320
- .ec__day-head {
2321
- display: flex;
2322
- align-items: center;
2323
- justify-content: center;
2324
- gap: 5px;
2325
- margin-bottom: 10px;
2326
- min-height: 20px;
2327
- }
2328
-
2329
- .ec__day-label {
2330
- font-size: 14px;
2331
- color: var(--ec-text);
2332
- }
2333
-
2334
- .ec__cell.is-today .ec__day-label {
2335
- color: var(--ec-primary);
2336
- font-weight: 600;
2337
- }
2338
-
2339
- .ec__today-dot {
2340
- width: 5px;
2341
- height: 5px;
2342
- border-radius: 50%;
2343
- background: var(--ec-primary);
2344
- }
2345
-
2346
- /* ------------------------------ 条目列表 ------------------------------ */
2347
- .ec__items-wrap {
2348
- flex: 1;
2349
- min-height: 0;
2350
- }
2351
-
2352
- .ec__items {
2353
- display: flex;
2354
- flex-direction: column;
2355
- gap: var(--ec-item-gap);
2356
- overflow-y: auto;
2357
- overflow-x: hidden;
2358
- /* 滚动到边界时不把滚动传递给外层页面 */
2359
- overscroll-behavior: contain;
2360
- }
2361
-
2362
- /* 出现滚动条时右侧留一点空隙,避免内容被压住 */
2363
- .ec__items.is-scrollable {
2364
- padding-right: 4px;
2365
- }
2366
-
2367
- .ec__items::-webkit-scrollbar {
2368
- width: 4px;
2369
- }
2370
-
2371
- .ec__items::-webkit-scrollbar-thumb {
2372
- background: #d8dce4;
2373
- border-radius: 2px;
2374
- }
2375
-
2376
- .ec__items::-webkit-scrollbar-thumb:hover {
2377
- background: #c0c4cc;
2378
- }
2379
-
2380
- .ec__items::-webkit-scrollbar-track {
2381
- background: transparent;
2382
- }
2383
-
2384
- /* ------------------------------ 单个条目 ------------------------------ */
2385
- .ec__item {
2386
- position: relative;
2387
- flex: none;
2388
- display: flex;
2389
- align-items: center;
2390
- min-height: var(--ec-item-h);
2391
- padding: var(--ec-item-padding-y) var(--ec-item-padding-x);
2392
- border-radius: 4px;
2393
- background: var(--ec-item-bg);
2394
- /* 未显式配置文字色时沿用次要文字色;hover 回落到主文字色 */
2395
- color: var(--ec-item-text, var(--ec-text-secondary));
2396
- cursor: pointer;
2397
- transition: background-color 0.18s, color 0.18s, box-shadow 0.18s;
2398
- }
2399
-
2400
- /* 注意:外层 .ec__items 有 overflow-x: hidden(滚动条需要),
2401
- * 所以 hover 不能用水平位移,否则会被裁切;这里改用内阴影。
2402
- * 悬浮背景 = 在底色上叠一层半透明白(--ec-item-hover-mask),自动调淡一档;
2403
- * 文字画在阴影之上,不受影响。item-bg-hover=false 时 mask 为透明。 */
2404
- .ec__item:hover {
2405
- color: var(--ec-item-text, var(--ec-text));
2406
- box-shadow:
2407
- inset 0 0 0 9999px var(--ec-item-hover-mask, rgba(255, 255, 255, 0.18)),
2408
- inset 0 0 0 1px rgba(0, 0, 0, 0.08);
2409
- }
2410
-
2411
- /* 渐变背景:纯色底上叠一层白色,默认上→下 */
2412
- .ec__item.is-bg-gradient {
2413
- background-image: linear-gradient(
2414
- 180deg,
2415
- rgba(255, 255, 255, 0.55) 0%,
2416
- rgba(255, 255, 255, 0.12) 46%,
2417
- rgba(255, 255, 255, 0) 74%
2418
- );
2419
- }
2420
-
2421
- /* 渐变方向:左→右 */
2422
- .ec__item.is-bg-gradient.is-bg-gradient-h {
2423
- background-image: linear-gradient(
2424
- 90deg,
2425
- rgba(255, 255, 255, 0.55) 0%,
2426
- rgba(255, 255, 255, 0.12) 46%,
2427
- rgba(255, 255, 255, 0) 74%
2428
- );
2429
- }
2430
-
2431
- .ec__item.is-colorful:hover {
2432
- background: var(--ec-item-accent-bg, var(--ec-item-bg));
2433
- color: var(--ec-item-text, var(--ec-item-accent, var(--ec-text)));
2434
- box-shadow:
2435
- inset 0 0 0 9999px rgba(255, 255, 255, 0.15),
2436
- inset 0 0 0 1px var(--ec-item-accent-bg, transparent);
2437
- }
2438
-
2439
- .ec__item.is-disabled {
2440
- cursor: not-allowed;
2441
- opacity: 0.5;
2442
- }
2443
-
2444
- .ec__item.is-disabled:hover {
2445
- box-shadow: none;
2446
- }
2447
-
2448
- .ec__item-content {
2449
- display: flex;
2450
- flex: 1;
2451
- flex-direction: column;
2452
- min-width: 0;
2453
- }
2454
-
2455
- .ec--item-center .ec__item {
2456
- justify-content: center;
2457
- }
2458
-
2459
- .ec--item-center .ec__item-content {
2460
- align-items: center;
2461
- text-align: center;
2462
- }
2463
-
2464
- .ec__item-label,
2465
- .ec__item-address {
2466
- display: block;
2467
- min-width: 0;
2468
- max-width: 100%;
2469
- overflow: hidden;
2470
- white-space: nowrap;
2471
- text-overflow: ellipsis;
2472
- }
2473
-
2474
- .ec__item-label {
2475
- font-size: 14px;
2476
- line-height: 16px;
2477
- }
2478
-
2479
- .ec__item-label.is-long {
2480
- font-size: 12px;
2481
- }
2482
-
2483
- .ec__item-address {
2484
- /* 名称与地址的行间距可配(item-line-gap) */
2485
- margin-top: var(--ec-item-line-gap, 2px);
2486
- font-size: 12px;
2487
- line-height: 16px;
2488
- }
2489
-
2490
- /* 角标:贴在条目右上角,与设计图一致 */
2491
- .ec__item-badge {
2492
- position: absolute;
2493
- top: 0;
2494
- right: 0;
2495
- padding: 0 4px;
2496
- border-radius: 0 4px 0 4px;
2497
- background: rgba(0, 0, 0, 0.06);
2498
- color: var(--ec-text-placeholder);
2499
- font-size: 12px;
2500
- line-height: 16px;
2501
- white-space: nowrap;
2502
- cursor: pointer;
2503
- /* 字号保持 12px 渲染,scale 倍率由配置 badge-scale 控制(默认 0.75);
2504
- * 锚点在右上角,缩放后仍紧贴条目右上角,不占内容宽度 */
2505
- transform: scale(var(--ec-badge-scale, 0.75));
2506
- transform-origin: top right;
2507
- transition: filter 0.18s;
2508
- }
2509
-
2510
- .ec__item-badge:hover {
2511
- filter: brightness(0.92);
2512
- }
2513
-
2514
- /* 「还有 N 项」 */
2515
- .ec__more {
2516
- flex: none;
2517
- padding: 2px 8px;
2518
- color: var(--ec-primary);
2519
- font-size: 12px;
2520
- text-align: center;
2521
- cursor: pointer;
2522
- border-radius: 4px;
2523
- }
2524
-
2525
- .ec__more:hover {
2526
- background: var(--ec-primary-ghost);
2527
- }
2528
-
2529
- /* ------------------------------ 空状态 ------------------------------ */
2530
- .ec__empty {
2531
- display: flex;
2532
- flex-direction: column;
2533
- align-items: center;
2534
- justify-content: center;
2535
- gap: 12px;
2536
- padding: 60px 20px;
2537
- color: var(--ec-text-placeholder);
2538
- }
2539
-
2540
- .ec__empty-icon {
2541
- width: 56px;
2542
- height: 56px;
2543
- }
2544
-
2545
- .ec__empty-text {
2546
- margin: 0;
2547
- font-size: 14px;
2548
- }
2549
-
2550
- /* ==================================================================
2551
- * 切换动效
2552
- * 离场元素统一 absolute 脱离文档流,
2553
- * 这样舞台高度只由进场元素决定,两者可以同时播放动画。
2554
- * ================================================================== */
2555
- .ec-fade-leave-active,
2556
- .ec-zoom-leave-active,
2557
- .ec-none-leave-active,
2558
- .ec-slide-next-leave-active,
2559
- .ec-slide-prev-leave-active,
2560
- .ec-flip-next-leave-active,
2561
- .ec-flip-prev-leave-active {
2562
- position: absolute;
2563
- top: 0;
2564
- left: 0;
2565
- right: 0;
2566
- }
2567
-
2568
- .ec-fade-enter-active,
2569
- .ec-fade-leave-active,
2570
- .ec-zoom-enter-active,
2571
- .ec-zoom-leave-active,
2572
- .ec-slide-next-enter-active,
2573
- .ec-slide-next-leave-active,
2574
- .ec-slide-prev-enter-active,
2575
- .ec-slide-prev-leave-active,
2576
- .ec-flip-next-enter-active,
2577
- .ec-flip-next-leave-active,
2578
- .ec-flip-prev-enter-active,
2579
- .ec-flip-prev-leave-active {
2580
- transition:
2581
- transform var(--ec-duration) cubic-bezier(0.4, 0, 0.2, 1),
2582
- opacity var(--ec-duration) ease;
2583
- will-change: transform, opacity;
2584
- }
2585
-
2586
- /* fade */
2587
- .ec-fade-enter,
2588
- .ec-fade-leave-to {
2589
- opacity: 0;
2590
- }
2591
-
2592
- /* slide:向后翻从右侧进、向前翻从左侧进 */
2593
- .ec-slide-next-enter {
2594
- opacity: 0;
2595
- transform: translateX(6%);
2596
- }
2597
- .ec-slide-next-leave-to {
2598
- opacity: 0;
2599
- transform: translateX(-6%);
2600
- }
2601
- .ec-slide-prev-enter {
2602
- opacity: 0;
2603
- transform: translateX(-6%);
2604
- }
2605
- .ec-slide-prev-leave-to {
2606
- opacity: 0;
2607
- transform: translateX(6%);
2608
- }
2609
-
2610
- /* zoom */
2611
- .ec-zoom-enter {
2612
- opacity: 0;
2613
- transform: scale(0.97);
2614
- }
2615
- .ec-zoom-leave-to {
2616
- opacity: 0;
2617
- transform: scale(1.03);
2618
- }
2619
-
2620
- /* flip:带透视的 3D 翻页 */
2621
- .ec--effect-flip .ec__stage {
2622
- perspective: 1600px;
2623
- }
2624
- .ec-flip-next-enter,
2625
- .ec-flip-prev-enter,
2626
- .ec-flip-next-leave-to,
2627
- .ec-flip-prev-leave-to {
2628
- opacity: 0;
2629
- }
2630
- .ec-flip-next-enter {
2631
- transform: rotateY(9deg) translateX(5%);
2632
- }
2633
- .ec-flip-next-leave-to {
2634
- transform: rotateY(-9deg) translateX(-5%);
2635
- }
2636
- .ec-flip-prev-enter {
2637
- transform: rotateY(-9deg) translateX(-5%);
2638
- }
2639
- .ec-flip-prev-leave-to {
2640
- transform: rotateY(9deg) translateX(5%);
2641
- }
2642
-
2643
- /* ------------------------- 单元格错峰入场 -------------------------
2644
- * 面板每次切月都是全新 DOM,CSS 动画会自动重放,无需额外 JS 触发。
2645
- * ---------------------------------------------------------------- */
2646
- .ec--stagger .ec__cell {
2647
- animation: ec-cell-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
2648
- /* 按格子序号递增延迟,形成从左上到右下的波浪 */
2649
- animation-delay: calc(var(--ec-i, 0) * 12ms);
2650
- }
2651
-
2652
- @keyframes ec-cell-in {
2653
- from {
2654
- opacity: 0;
2655
- transform: translateY(10px);
2656
- }
2657
- to {
2658
- opacity: 1;
2659
- transform: none;
2660
- }
2661
- }
2662
-
2663
- /* ------------------------- 无动效 / 降级 ------------------------- */
2664
- .ec--effect-none .ec__stage {
2665
- transition: none;
2666
- }
2667
-
2668
- /* 系统开启「减少动态效果」时关闭全部动画 */
2669
- @media (prefers-reduced-motion: reduce) {
2670
- .ec__stage,
2671
- .ec__item,
2672
- .ec__cell,
2673
- .ec__icon-btn,
2674
- .ec-fade-enter-active,
2675
- .ec-fade-leave-active,
2676
- .ec-zoom-enter-active,
2677
- .ec-zoom-leave-active,
2678
- .ec-slide-next-enter-active,
2679
- .ec-slide-next-leave-active,
2680
- .ec-slide-prev-enter-active,
2681
- .ec-slide-prev-leave-active,
2682
- .ec-flip-next-enter-active,
2683
- .ec-flip-next-leave-active,
2684
- .ec-flip-prev-enter-active,
2685
- .ec-flip-prev-leave-active {
2686
- transition: none !important;
2687
- }
2688
- .ec--stagger .ec__cell,
2689
- .ec__progress-inner {
2690
- animation: none !important;
2691
- }
2692
- }
2693
-
2694
- /* ------------------------------ 响应式 ------------------------------ */
2695
- @media (max-width: 768px) {
2696
- .ec__config-head {
2697
- align-items: flex-start;
2698
- flex-direction: column;
2699
- }
2700
- .ec__config-actions {
2701
- width: 100%;
2702
- }
2703
- .ec__config-btn {
2704
- flex: 1;
2705
- min-height: 40px;
2706
- }
2707
- .ec__header {
2708
- flex-direction: column;
2709
- align-items: stretch;
2710
- gap: 10px;
2711
- }
2712
- .ec__header-right {
2713
- justify-content: flex-end;
2714
- }
2715
- .ec__cell {
2716
- padding: var(--ec-cell-padding-mobile);
2717
- }
2718
- /* 角标在任何屏宽下都绝对定位、悬浮于条目右上角,不占内容宽度 */
2719
- }
2720
- </style>