npmapps 1.1.3 → 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/html-note(1).zip +0 -0
@@ -0,0 +1,1551 @@
1
+ <template>
2
+ <div class="doc">
3
+ <!-- ============================ 页头 ============================ -->
4
+ <header class="doc__hero">
5
+ <h1 class="doc__hero-title">EventCalendar 日程日历</h1>
6
+ <p class="doc__hero-desc">
7
+ 零第三方依赖的月历组件:月份自动补全、下拉切换、自动轮播、可配字段、插槽定制、骨架屏。
8
+ 整个 <code>EventCalendar</code> 文件夹可直接拷贝到任意 Vue 2 项目使用。
9
+ </p>
10
+ <div class="doc__hero-tags">
11
+ <span v-for="t in heroTags" :key="t" class="doc__chip">{{ t }}</span>
12
+ </div>
13
+ </header>
14
+
15
+ <div class="doc__body">
16
+ <!-- ============================ 侧边目录 ============================ -->
17
+ <nav class="doc__toc">
18
+ <p class="doc__toc-title">示例目录</p>
19
+ <a
20
+ v-for="s in sections"
21
+ :key="s.id"
22
+ class="doc__toc-link"
23
+ :class="{ 'is-active': activeSection === s.id }"
24
+ href="javascript:void(0)"
25
+ @click="scrollTo(s.id)"
26
+ >{{ s.title }}</a>
27
+ </nav>
28
+
29
+ <!-- ============================ 示例主体 ============================ -->
30
+ <main class="doc__main">
31
+ <!-- ------------------------------------------------------------ -->
32
+ <!-- 1. 基础用法 -->
33
+ <!-- ------------------------------------------------------------ -->
34
+ <section id="basic" class="demo">
35
+ <h2 class="demo__title">1. 基础用法</h2>
36
+ <p class="demo__desc">
37
+ 传入 <code>data</code> 即可渲染。默认一周从<strong>周日</strong>开始,
38
+ 当月之外的补位日期自动置灰;每格里的条目就是设计稿中的「学校 + 角标」。
39
+ </p>
40
+ <div class="demo__stage">
41
+ <EventCalendar
42
+ :data="basicData"
43
+ title="宣讲日历"
44
+ subtitle="更多场次陆续上线中,宣讲进程以实际安排为准"
45
+ active-month="2026-09"
46
+ />
47
+ </div>
48
+ <CodeBlock :code="code.basic" />
49
+ </section>
50
+
51
+ <!-- ------------------------------------------------------------ -->
52
+ <!-- 2. 异步数据 + 骨架屏 -->
53
+ <!-- ------------------------------------------------------------ -->
54
+ <section id="async" class="demo">
55
+ <h2 class="demo__title">2. 异步数据与骨架屏</h2>
56
+ <p class="demo__desc">
57
+ 数据由 <code>mockData.js</code> 通过 <code>setTimeout</code> 异步返回。
58
+ 返回前 <code>loading</code> 为 <code>true</code>,组件渲染骨架屏,避免白屏和布局跳动。
59
+ </p>
60
+ <div class="demo__toolbar">
61
+ <button class="doc__btn" :disabled="asyncLoading" @click="loadAsync">
62
+ {{ asyncLoading ? '加载中…' : '重新加载(1.5s)' }}
63
+ </button>
64
+ <span class="doc__hint">骨架屏行数由 <code>skeleton-rows</code> 控制</span>
65
+ </div>
66
+ <div class="demo__stage">
67
+ <EventCalendar
68
+ :data="asyncData"
69
+ :loading="asyncLoading"
70
+ :skeleton-rows="5"
71
+ title="宣讲日历"
72
+ subtitle="数据加载完成前展示骨架屏"
73
+ />
74
+ </div>
75
+ <CodeBlock :code="code.async" />
76
+ </section>
77
+
78
+ <!-- ------------------------------------------------------------ -->
79
+ <!-- 3. 多月份与下拉切换 -->
80
+ <!-- ------------------------------------------------------------ -->
81
+ <section id="months" class="demo">
82
+ <h2 class="demo__title">3. 多月份与下拉切换</h2>
83
+ <p class="demo__desc">
84
+ 后端返回多个月份时,右上角下拉框自动列出所有月份(下拉框为自研实现,支持键盘上下键与回车)。
85
+ 打开 <code>show-arrows</code> 还能得到左右翻月按钮。当前月份可用
86
+ <code>v-model</code> 或 <code>:active-month.sync</code> 双向绑定。
87
+ </p>
88
+ <div class="demo__toolbar">
89
+ <span class="doc__hint">
90
+ 当前绑定值:<code class="doc__code-inline">{{ monthsActive || '—' }}</code>
91
+ </span>
92
+ <button class="doc__btn doc__btn--ghost" @click="monthsActive = '2026-11'">
93
+ 外部跳到 2026-11
94
+ </button>
95
+ </div>
96
+ <div class="demo__stage">
97
+ <EventCalendar
98
+ v-model="monthsActive"
99
+ :data="multiData"
100
+ :loading="multiLoading"
101
+ show-arrows
102
+ title="宣讲日历"
103
+ subtitle="下拉 / 箭头均可切换月份"
104
+ @month-change="onMonthChange"
105
+ />
106
+ </div>
107
+ <CodeBlock :code="code.months" />
108
+ </section>
109
+
110
+ <!-- ------------------------------------------------------------ -->
111
+ <!-- 4. 自动轮播 -->
112
+ <!-- ------------------------------------------------------------ -->
113
+ <section id="autoplay" class="demo">
114
+ <h2 class="demo__title">4. 自动轮播</h2>
115
+ <p class="demo__desc">
116
+ <code>autoplay</code> 开启轮播,<code>autoplay-interval</code> 配置间隔(毫秒)。
117
+ 顶部细进度条展示距下次切换的剩余时间。以下情况会<strong>自动暂停</strong>并保留剩余时间:
118
+ 鼠标悬停(<code>pause-on-hover</code>)、月份下拉展开、浏览器切到后台、点击暂停按钮。
119
+ </p>
120
+ <div class="demo__toolbar">
121
+ <span class="doc__hint">切换间隔:</span>
122
+ <button
123
+ v-for="opt in intervalOptions"
124
+ :key="opt.value"
125
+ class="doc__seg"
126
+ :class="{ 'is-active': autoplayInterval === opt.value }"
127
+ @click="autoplayInterval = opt.value"
128
+ >{{ opt.label }}</button>
129
+ <label class="doc__check">
130
+ <input v-model="autoplayLoop" type="checkbox" />
131
+ <span>循环(关闭则播到最后一个月停止)</span>
132
+ </label>
133
+ </div>
134
+ <div class="demo__stage">
135
+ <EventCalendar
136
+ :data="multiData"
137
+ :loading="multiLoading"
138
+ autoplay
139
+ :autoplay-interval="autoplayInterval"
140
+ :loop="autoplayLoop"
141
+ pause-on-hover
142
+ title="宣讲日历"
143
+ subtitle="鼠标移上来试试,轮播会暂停"
144
+ />
145
+ </div>
146
+ <CodeBlock :code="code.autoplay" />
147
+ </section>
148
+
149
+ <!-- ------------------------------------------------------------ -->
150
+ <!-- 5. 切换动效 -->
151
+ <!-- ------------------------------------------------------------ -->
152
+ <section id="effect" class="demo">
153
+ <h2 class="demo__title">5. 切换动效</h2>
154
+ <p class="demo__desc">
155
+ 提供 4 种月份切换动效,外加「单元格错峰入场」。动效可通过
156
+ <code>transition-effect="none"</code> 完全关闭,时长由
157
+ <code>transition-duration</code> 控制。
158
+ 切换月份时行数可能从 5 行变 6 行,组件会同步做高度过渡,不会突然跳动。
159
+ </p>
160
+ <div class="demo__toolbar">
161
+ <span class="doc__hint">动效:</span>
162
+ <button
163
+ v-for="opt in effectOptions"
164
+ :key="opt.value"
165
+ class="doc__seg"
166
+ :class="{ 'is-active': effect === opt.value }"
167
+ @click="effect = opt.value"
168
+ >{{ opt.label }}</button>
169
+ </div>
170
+ <div class="demo__toolbar">
171
+ <label class="doc__check">
172
+ <input v-model="stagger" type="checkbox" />
173
+ <span>单元格错峰入场</span>
174
+ </label>
175
+ <span class="doc__hint">时长 {{ duration }}ms</span>
176
+ <input v-model.number="duration" type="range" min="0" max="1200" step="20" class="doc__range" />
177
+ </div>
178
+ <div class="demo__stage">
179
+ <EventCalendar
180
+ :data="multiData"
181
+ :loading="multiLoading"
182
+ :transition-effect="effect"
183
+ :transition-duration="duration"
184
+ :cell-stagger="stagger"
185
+ show-arrows
186
+ autoplay
187
+ :autoplay-interval="6000"
188
+ title="动效预览"
189
+ subtitle="每 6 秒自动切换一次,也可以点箭头手动切"
190
+ />
191
+ </div>
192
+ <CodeBlock :code="code.effect" />
193
+ </section>
194
+
195
+ <!-- ------------------------------------------------------------ -->
196
+ <!-- 6. 月份补全与跨年 -->
197
+ <!-- ------------------------------------------------------------ -->
198
+ <section id="boundary" class="demo">
199
+ <h2 class="demo__title">6. 月份补全与跨年边界</h2>
200
+ <p class="demo__desc">
201
+ 每月天数与「1 号是星期几」都不固定,组件会自动补齐首尾:
202
+ 2026 年 2 月 1 号是<strong>周日</strong>,首行无需补位;2 月 28 号是<strong>周六</strong>,
203
+ 末行也刚好排满;而 2026 年 1 月要往前补 <strong>2025 年 12 月</strong>的日期,
204
+ 12 月则要往后补 <strong>2026 年 1 月</strong>——跨年由原生 <code>Date</code> 的进位机制保证正确。
205
+ 切到 <code>2025-12</code> / <code>2026-01</code> 看看灰色补位格。
206
+ </p>
207
+ <div class="demo__toolbar">
208
+ <label class="doc__check">
209
+ <input v-model="showAdjacentItems" type="checkbox" />
210
+ <span>补位日期也显示条目(关闭后只留灰色日期)</span>
211
+ </label>
212
+ <label class="doc__check">
213
+ <input v-model="mondayFirst" type="checkbox" />
214
+ <span>一周从周一开始(first-day-of-week=1)</span>
215
+ </label>
216
+ </div>
217
+ <div class="demo__stage">
218
+ <EventCalendar
219
+ :data="crossYearData"
220
+ :loading="crossYearLoading"
221
+ :active-month.sync="crossYearMonth"
222
+ :show-adjacent-items="showAdjacentItems"
223
+ :first-day-of-week="mondayFirst ? 1 : 0"
224
+ show-arrows
225
+ title="跨年场次"
226
+ subtitle="2025-12 / 2026-01 / 2026-02"
227
+ />
228
+ </div>
229
+ <CodeBlock :code="code.boundary" />
230
+ </section>
231
+
232
+ <!-- ------------------------------------------------------------ -->
233
+ <!-- 7. 自定义字段映射 -->
234
+ <!-- ------------------------------------------------------------ -->
235
+ <section id="schema" class="demo">
236
+ <h2 class="demo__title">7. 自定义字段映射</h2>
237
+ <p class="demo__desc">
238
+ 后端字段名和组件默认约定不一致时,<strong>不需要改数据</strong>,
239
+ 传一个 <code>field-names</code> 即可。下面这份数据用的是
240
+ <code>ym / dateList / dayStr / activityList / schoolName / activityTypeName</code>,
241
+ 字段路径还支持 <code>'a.b.c'</code> 形式。
242
+ </p>
243
+ <div class="demo__cols">
244
+ <div class="demo__col">
245
+ <p class="demo__col-title">后端返回(片段)</p>
246
+ <CodeBlock :code="code.schemaData" always-open />
247
+ </div>
248
+ <div class="demo__col">
249
+ <p class="demo__col-title">映射配置</p>
250
+ <CodeBlock :code="code.schemaMap" always-open />
251
+ </div>
252
+ </div>
253
+ <div class="demo__stage">
254
+ <EventCalendar
255
+ :data="schemaData"
256
+ :loading="schemaLoading"
257
+ :field-names="altFieldNames"
258
+ title="字段映射示例"
259
+ subtitle="数据字段全都不一样,组件照样认识"
260
+ />
261
+ </div>
262
+ </section>
263
+
264
+ <!-- ------------------------------------------------------------ -->
265
+ <!-- 8. 条目溢出与行高 -->
266
+ <!-- ------------------------------------------------------------ -->
267
+ <section id="overflow" class="demo">
268
+ <h2 class="demo__title">8. 条目溢出与行高</h2>
269
+ <p class="demo__desc">
270
+ 每天的条目数量不固定。<strong>行高由该行条目最多的那一天决定</strong>(CSS Grid 天然等高,无需 JS 计算)。
271
+ 超过 <code>max-visible-items</code> 后有两种处理:
272
+ <code>scroll</code> 在格内出现细滚动条,<code>collapse</code> 折叠并显示「还有 N 项」(可监听
273
+ <code>more-click</code>)。
274
+ </p>
275
+ <div class="demo__toolbar">
276
+ <span class="doc__hint">溢出方式:</span>
277
+ <button
278
+ v-for="opt in overflowOptions"
279
+ :key="opt.value"
280
+ class="doc__seg"
281
+ :class="{ 'is-active': overflowMode === opt.value }"
282
+ @click="overflowMode = opt.value"
283
+ >{{ opt.label }}</button>
284
+ <span class="doc__hint">最多显示 {{ maxVisibleItems }} 条</span>
285
+ <input v-model.number="maxVisibleItems" type="range" min="1" max="6" step="1" class="doc__range" />
286
+ </div>
287
+ <div class="demo__stage">
288
+ <EventCalendar
289
+ :data="overflowData"
290
+ :loading="overflowLoading"
291
+ :max-visible-items="maxVisibleItems"
292
+ :overflow-mode="overflowMode"
293
+ :min-row-height="120"
294
+ title="溢出演示"
295
+ subtitle="部分日期故意放了 6 条数据"
296
+ @more-click="onMoreClick"
297
+ />
298
+ </div>
299
+ <CodeBlock :code="code.overflow" />
300
+ </section>
301
+
302
+ <!-- ------------------------------------------------------------ -->
303
+ <!-- 9. 插槽定制 -->
304
+ <!-- ------------------------------------------------------------ -->
305
+ <section id="slots" class="demo">
306
+ <h2 class="demo__title">9. 插槽定制</h2>
307
+ <p class="demo__desc">
308
+ 左上角 <code>header-left</code>、单条 <code>item</code>、整格 <code>day</code>、
309
+ 当天无数据时的 <code>day-empty</code> 等都可以自定义。
310
+ 下面这个例子把左上角换成了统计卡片,把条目换成了「时间 + 学校 + 地点」的两行布局。
311
+ </p>
312
+ <div class="demo__stage">
313
+ <EventCalendar
314
+ :data="multiData"
315
+ :loading="multiLoading"
316
+ :min-row-height="140"
317
+ :item-height="44"
318
+ :max-visible-items="3"
319
+ >
320
+ <!-- 左上角完全自定义 -->
321
+ <template #header-left="{ monthLabel, monthIndex, monthCount }">
322
+ <div class="custom-head">
323
+ <div class="custom-head__badge">{{ monthLabel }}</div>
324
+ <div class="custom-head__meta">
325
+ <strong>校园招聘日程</strong>
326
+ <span>第 {{ monthIndex + 1 }} / {{ monthCount }} 个月 · 共 {{ monthItemCount }} 场</span>
327
+ </div>
328
+ </div>
329
+ </template>
330
+
331
+ <!-- 单条条目自定义 -->
332
+ <template #item="{ item, label, badge, type }">
333
+ <div class="custom-item" :class="'is-' + type">
334
+ <div class="custom-item__row">
335
+ <span class="custom-item__time">{{ item.startTime || '—' }}</span>
336
+ <span class="custom-item__name">{{ label }}</span>
337
+ </div>
338
+ <div class="custom-item__row custom-item__row--sub">
339
+ <span class="custom-item__addr">{{ item.address || '地点待定' }}</span>
340
+ <span class="custom-item__tag">{{ badge }}</span>
341
+ </div>
342
+ </div>
343
+ </template>
344
+
345
+ <!-- 当天没有数据时的占位 -->
346
+ <template #day-empty="{ isCurrentMonth }">
347
+ <div v-if="isCurrentMonth" class="custom-empty">暂无安排</div>
348
+ </template>
349
+ </EventCalendar>
350
+ </div>
351
+ <CodeBlock :code="code.slots" />
352
+ </section>
353
+
354
+ <!-- ------------------------------------------------------------ -->
355
+ <!-- 10. 事件 -->
356
+ <!-- ------------------------------------------------------------ -->
357
+ <section id="events" class="demo">
358
+ <h2 class="demo__title">10. 事件</h2>
359
+ <p class="demo__desc">
360
+ 点击<strong>格子空白处</strong>触发 <code>day-click</code>,点击<strong>条目</strong>触发
361
+ <code>item-click</code>,点击条目右上角<strong>角标</strong>触发 <code>tag-click</code>
362
+ (未监听 <code>tag-click</code> 时会退化成 <code>item-click</code>,避免「点了没反应」)。
363
+ 回调都会带上完整的原始数据对象。
364
+ </p>
365
+ <div class="demo__cols demo__cols--events">
366
+ <div class="demo__stage demo__stage--flex">
367
+ <EventCalendar
368
+ :data="basicData"
369
+ active-month="2026-09"
370
+ title="点点看"
371
+ subtitle="日期格 / 条目 / 角标 都可点"
372
+ @day-click="onDayClick"
373
+ @item-click="onItemClick"
374
+ @tag-click="onTagClick"
375
+ />
376
+ </div>
377
+ <div class="event-log">
378
+ <div class="event-log__head">
379
+ <span>事件日志</span>
380
+ <button class="doc__btn doc__btn--ghost doc__btn--sm" @click="logs = []">清空</button>
381
+ </div>
382
+ <ul v-if="logs.length" class="event-log__list">
383
+ <li v-for="(log, i) in logs" :key="i" class="event-log__item">
384
+ <span class="event-log__type" :class="'is-' + log.type">{{ log.type }}</span>
385
+ <span class="event-log__text">{{ log.text }}</span>
386
+ </li>
387
+ </ul>
388
+ <p v-else class="event-log__empty">还没有事件,点左侧日历试试</p>
389
+ </div>
390
+ </div>
391
+ <CodeBlock :code="code.events" />
392
+ </section>
393
+
394
+ <!-- ------------------------------------------------------------ -->
395
+ <!-- 11. 主题与样式变体 -->
396
+ <!-- ------------------------------------------------------------ -->
397
+ <section id="theme" class="demo">
398
+ <h2 class="demo__title">11. 主题与样式变体</h2>
399
+ <p class="demo__desc">
400
+ 所有颜色走 CSS 变量,用 <code>theme</code> 传入即可换肤;
401
+ <code>type-colors</code> 可为业务类型指定配色;
402
+ <code>:colorful="false"</code> 会让角标回到中性灰(更贴近原始设计稿)。
403
+ </p>
404
+ <div class="demo__toolbar">
405
+ <button
406
+ v-for="t in themePresets"
407
+ :key="t.name"
408
+ class="doc__seg"
409
+ :class="{ 'is-active': themeName === t.name }"
410
+ @click="themeName = t.name"
411
+ >{{ t.label }}</button>
412
+ <label class="doc__check">
413
+ <input v-model="colorful" type="checkbox" />
414
+ <span>角标彩色(关闭 = 还原设计稿灰色)</span>
415
+ </label>
416
+ </div>
417
+ <div class="demo__stage">
418
+ <EventCalendar
419
+ :data="basicData"
420
+ active-month="2026-09"
421
+ :theme="currentTheme"
422
+ :colorful="colorful"
423
+ :type-colors="typeColors"
424
+ title="主题预览"
425
+ :subtitle="'当前:' + themeName"
426
+ />
427
+ </div>
428
+ <CodeBlock :code="code.theme" />
429
+ </section>
430
+
431
+ <!-- ------------------------------------------------------------ -->
432
+ <!-- 12. 空状态 -->
433
+ <!-- ------------------------------------------------------------ -->
434
+ <section id="empty" class="demo">
435
+ <h2 class="demo__title">12. 空状态与无头部</h2>
436
+ <p class="demo__desc">
437
+ 没有任何月份数据时展示空状态,文案由 <code>empty-text</code> 控制,
438
+ 也可以用 <code>empty</code> 插槽整体替换。
439
+ <code>:show-header="false"</code> 可以隐藏整个顶部区域,把日历当作纯展示块嵌进别的容器。
440
+ </p>
441
+ <div class="demo__cols">
442
+ <div class="demo__col">
443
+ <p class="demo__col-title">空数据</p>
444
+ <div class="demo__stage">
445
+ <EventCalendar :data="[]" empty-text="本月暂无宣讲安排" :show-header="false" />
446
+ </div>
447
+ </div>
448
+ <div class="demo__col">
449
+ <p class="demo__col-title">持续加载中</p>
450
+ <div class="demo__stage">
451
+ <EventCalendar :data="[]" loading :skeleton-rows="4" :show-header="false" />
452
+ </div>
453
+ </div>
454
+ </div>
455
+ </section>
456
+
457
+ <!-- ------------------------------------------------------------ -->
458
+ <!-- API -->
459
+ <!-- ------------------------------------------------------------ -->
460
+ <section id="api" class="demo">
461
+ <h2 class="demo__title">API</h2>
462
+
463
+ <h3 class="demo__sub">Props</h3>
464
+ <div class="doc__table-wrap">
465
+ <table class="doc__table">
466
+ <thead>
467
+ <tr><th>参数</th><th>说明</th><th>类型</th><th>默认值</th></tr>
468
+ </thead>
469
+ <tbody>
470
+ <tr v-for="row in apiProps" :key="row[0]">
471
+ <td><code>{{ row[0] }}</code></td>
472
+ <td>{{ row[1] }}</td>
473
+ <td><span class="doc__type">{{ row[2] }}</span></td>
474
+ <td><code>{{ row[3] }}</code></td>
475
+ </tr>
476
+ </tbody>
477
+ </table>
478
+ </div>
479
+
480
+ <h3 class="demo__sub">Events</h3>
481
+ <div class="doc__table-wrap">
482
+ <table class="doc__table">
483
+ <thead>
484
+ <tr><th>事件名</th><th>说明</th><th>回调参数</th></tr>
485
+ </thead>
486
+ <tbody>
487
+ <tr v-for="row in apiEvents" :key="row[0]">
488
+ <td><code>{{ row[0] }}</code></td>
489
+ <td>{{ row[1] }}</td>
490
+ <td><span class="doc__type">{{ row[2] }}</span></td>
491
+ </tr>
492
+ </tbody>
493
+ </table>
494
+ </div>
495
+
496
+ <h3 class="demo__sub">Slots</h3>
497
+ <div class="doc__table-wrap">
498
+ <table class="doc__table">
499
+ <thead>
500
+ <tr><th>名称</th><th>说明</th><th>作用域参数</th></tr>
501
+ </thead>
502
+ <tbody>
503
+ <tr v-for="row in apiSlots" :key="row[0]">
504
+ <td><code>{{ row[0] }}</code></td>
505
+ <td>{{ row[1] }}</td>
506
+ <td><span class="doc__type">{{ row[2] }}</span></td>
507
+ </tr>
508
+ </tbody>
509
+ </table>
510
+ </div>
511
+
512
+ <h3 class="demo__sub">Methods(通过 ref 调用)</h3>
513
+ <div class="doc__table-wrap">
514
+ <table class="doc__table">
515
+ <thead>
516
+ <tr><th>方法</th><th>说明</th></tr>
517
+ </thead>
518
+ <tbody>
519
+ <tr v-for="row in apiMethods" :key="row[0]">
520
+ <td><code>{{ row[0] }}</code></td>
521
+ <td>{{ row[1] }}</td>
522
+ </tr>
523
+ </tbody>
524
+ </table>
525
+ </div>
526
+ </section>
527
+ </main>
528
+ </div>
529
+ </div>
530
+ </template>
531
+
532
+ <script>
533
+ /**
534
+ * EventCalendar 示例集合
535
+ * ------------------------------------------------------------------
536
+ * 按 UI 组件库文档的形式,把组件的每一项能力都单独做成一个可交互示例。
537
+ * 本文件同样不依赖第三方 UI 库,跟随组件文件夹一起拷走即可运行。
538
+ */
539
+ import EventCalendar from './index.vue'
540
+ import CodeBlock from './CodeBlock.vue'
541
+ import {
542
+ SIMPLE_DATA,
543
+ ALT_FIELD_NAMES,
544
+ fetchCalendarData,
545
+ fetchCalendarDataV2,
546
+ fetchCrossYearData
547
+ } from './mockData'
548
+
549
+ export default {
550
+ name: 'EventCalendarExample',
551
+
552
+ components: { EventCalendar, CodeBlock },
553
+
554
+ data() {
555
+ return {
556
+ heroTags: ['零依赖', '自动补月', '轮播动效', '字段可配', '插槽定制', '骨架屏'],
557
+
558
+ sections: [
559
+ { id: 'basic', title: '1. 基础用法' },
560
+ { id: 'async', title: '2. 异步与骨架屏' },
561
+ { id: 'months', title: '3. 多月份切换' },
562
+ { id: 'autoplay', title: '4. 自动轮播' },
563
+ { id: 'effect', title: '5. 切换动效' },
564
+ { id: 'boundary', title: '6. 补全与跨年' },
565
+ { id: 'schema', title: '7. 字段映射' },
566
+ { id: 'overflow', title: '8. 溢出与行高' },
567
+ { id: 'slots', title: '9. 插槽定制' },
568
+ { id: 'events', title: '10. 事件' },
569
+ { id: 'theme', title: '11. 主题定制' },
570
+ { id: 'empty', title: '12. 空状态' },
571
+ { id: 'api', title: 'API' }
572
+ ],
573
+ activeSection: 'basic',
574
+
575
+ /* -------- 1. 基础 -------- */
576
+ basicData: SIMPLE_DATA,
577
+
578
+ /* -------- 2. 异步 -------- */
579
+ asyncData: [],
580
+ asyncLoading: true,
581
+
582
+ /* -------- 3/4/5/9. 多月份(共用一份数据) -------- */
583
+ multiData: [],
584
+ multiLoading: true,
585
+ monthsActive: '',
586
+
587
+ /* -------- 4. 轮播 -------- */
588
+ autoplayInterval: 10000,
589
+ autoplayLoop: true,
590
+ intervalOptions: [
591
+ { label: '5 秒', value: 5000 },
592
+ { label: '10 秒', value: 10000 },
593
+ { label: '30 秒', value: 30000 },
594
+ { label: '1 分钟', value: 60000 }
595
+ ],
596
+
597
+ /* -------- 5. 动效 -------- */
598
+ effect: 'slide',
599
+ duration: 420,
600
+ stagger: true,
601
+ effectOptions: [
602
+ { label: '滑动 slide', value: 'slide' },
603
+ { label: '淡入 fade', value: 'fade' },
604
+ { label: '缩放 zoom', value: 'zoom' },
605
+ { label: '翻页 flip', value: 'flip' },
606
+ { label: '关闭 none', value: 'none' }
607
+ ],
608
+
609
+ /* -------- 6. 跨年 -------- */
610
+ crossYearData: [],
611
+ crossYearLoading: true,
612
+ crossYearMonth: '2026-01',
613
+ showAdjacentItems: true,
614
+ mondayFirst: false,
615
+
616
+ /* -------- 7. 字段映射 -------- */
617
+ schemaData: [],
618
+ schemaLoading: true,
619
+ altFieldNames: ALT_FIELD_NAMES,
620
+
621
+ /* -------- 8. 溢出 -------- */
622
+ overflowData: [],
623
+ overflowLoading: true,
624
+ maxVisibleItems: 4,
625
+ overflowMode: 'scroll',
626
+ overflowOptions: [
627
+ { label: '滚动 scroll', value: 'scroll' },
628
+ { label: '折叠 collapse', value: 'collapse' }
629
+ ],
630
+
631
+ /* -------- 10. 事件 -------- */
632
+ logs: [],
633
+
634
+ /* -------- 11. 主题 -------- */
635
+ themeName: 'default',
636
+ colorful: true,
637
+ themePresets: [
638
+ { name: 'default', label: '默认蓝', vars: {} },
639
+ {
640
+ name: 'green',
641
+ label: '清新绿',
642
+ vars: {
643
+ primary: '#10a37f',
644
+ primaryGhost: 'rgba(16, 163, 127, 0.12)',
645
+ itemBg: '#f2f9f6',
646
+ cellHover: '#f7fcfa',
647
+ radius: '10px'
648
+ }
649
+ },
650
+ {
651
+ name: 'violet',
652
+ label: '典雅紫',
653
+ vars: {
654
+ primary: '#7c3aed',
655
+ primaryGhost: 'rgba(124, 58, 237, 0.12)',
656
+ itemBg: '#f6f4fd',
657
+ cellHover: '#faf9fe',
658
+ radius: '12px'
659
+ }
660
+ },
661
+ {
662
+ name: 'plain',
663
+ label: '素净灰',
664
+ vars: {
665
+ primary: '#4b5563',
666
+ primaryGhost: 'rgba(75, 85, 99, 0.1)',
667
+ itemBg: '#f4f4f5',
668
+ cellHover: '#fafafa',
669
+ radius: '4px'
670
+ }
671
+ }
672
+ ],
673
+ typeColors: {
674
+ talk: { color: '#3d6cf0', bg: 'rgba(61, 108, 240, 0.10)' },
675
+ fair: { color: '#e0821b', bg: 'rgba(224, 130, 27, 0.12)' },
676
+ live: { color: '#10a37f', bg: 'rgba(16, 163, 127, 0.12)' },
677
+ exam: { color: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.12)' }
678
+ },
679
+
680
+ /* -------- 代码片段(纯字符串,不参与模板编译) -------- */
681
+ code: {
682
+ basic: `<EventCalendar
683
+ :data="data"
684
+ title="宣讲日历"
685
+ subtitle="更多场次陆续上线中,宣讲进程以实际安排为准"
686
+ active-month="2026-09"
687
+ />
688
+
689
+ // data 结构
690
+ [{
691
+ month: '2026-09',
692
+ monthLabel: '2026年9月',
693
+ days: [{
694
+ date: '2026-09-09',
695
+ items: [
696
+ { id: 'a1', label: '香港中文大学(深圳)', badge: '宣讲', type: 'talk' }
697
+ ]
698
+ }]
699
+ }]`,
700
+
701
+ async: `<EventCalendar :data="data" :loading="loading" :skeleton-rows="5" />
702
+
703
+ import { fetchCalendarData } from '@/components/EventCalendar/mockData'
704
+
705
+ async created() {
706
+ this.loading = true
707
+ try {
708
+ this.data = await fetchCalendarData({ delay: 1500 })
709
+ } finally {
710
+ this.loading = false // 放 finally,接口失败也要收掉骨架屏
711
+ }
712
+ }`,
713
+
714
+ months: `<!-- v-model 与 :active-month.sync 等价,二选一 -->
715
+ <EventCalendar
716
+ v-model="activeMonth"
717
+ :data="data"
718
+ show-arrows
719
+ @month-change="onMonthChange"
720
+ />
721
+
722
+ onMonthChange(month, info) {
723
+ // info = { index, source, option, raw }
724
+ // source: 'select' | 'arrow' | 'autoplay' | 'prop' | 'api'
725
+ console.log(month, info.source)
726
+ }`,
727
+
728
+ autoplay: `<EventCalendar
729
+ :data="data"
730
+ autoplay
731
+ :autoplay-interval="10000"
732
+ :loop="true"
733
+ pause-on-hover
734
+ :show-progress="true"
735
+ :show-autoplay-toggle="true"
736
+ />`,
737
+
738
+ effect: `<EventCalendar
739
+ :data="data"
740
+ transition-effect="slide"
741
+ :transition-duration="420"
742
+ :cell-stagger="true"
743
+ />
744
+
745
+ <!-- 完全关闭动效 -->
746
+ <EventCalendar :data="data" transition-effect="none" :cell-stagger="false" />`,
747
+
748
+ boundary: `<!-- 补位日期自动置灰;跨年由原生 Date 进位保证正确 -->
749
+ <EventCalendar
750
+ :data="data"
751
+ :active-month.sync="month"
752
+ :show-adjacent-items="true"
753
+ :first-day-of-week="0"
754
+ />`,
755
+
756
+ schemaData: `[{
757
+ ym: '2026-09',
758
+ ymText: '9 月场次',
759
+ dateList: [{
760
+ dayStr: '2026-09-09',
761
+ activityList: [{
762
+ activityId: 1,
763
+ schoolName: '南方科技大学',
764
+ activityTypeName: '宣讲',
765
+ activityType: 'talk',
766
+ expired: false
767
+ }]
768
+ }]
769
+ }]`,
770
+
771
+ schemaMap: `const fieldNames = {
772
+ month: 'ym',
773
+ monthLabel: 'ymText',
774
+ days: 'dateList',
775
+ date: 'dayStr',
776
+ items: 'activityList',
777
+ itemKey: 'activityId',
778
+ itemLabel: 'schoolName',
779
+ itemBadge: 'activityTypeName',
780
+ itemType: 'activityType',
781
+ itemDisabled: 'expired'
782
+ }
783
+
784
+ <EventCalendar :data="data" :field-names="fieldNames" />`,
785
+
786
+ overflow: `<!-- 行高自动取该行条目最多的那一天,超出按 overflow-mode 处理 -->
787
+ <EventCalendar
788
+ :data="data"
789
+ :max-visible-items="4"
790
+ overflow-mode="scroll"
791
+ :item-height="28"
792
+ :item-gap="6"
793
+ :min-row-height="120"
794
+ @more-click="onMoreClick"
795
+ />`,
796
+
797
+ slots: `<EventCalendar :data="data" :item-height="44" :max-visible-items="3">
798
+ <!-- 左上角:设计稿里的标题区,可放任意内容 -->
799
+ <template #header-left="{ monthLabel, monthIndex, monthCount }">
800
+ <div class="custom-head">…</div>
801
+ </template>
802
+
803
+ <!-- 单条条目:不传则用默认「学校名 + 右上角角标」 -->
804
+ <template #item="{ item, label, badge, type, day, index }">
805
+ <div class="custom-item">…</div>
806
+ </template>
807
+
808
+ <!-- 当天没有数据时 -->
809
+ <template #day-empty="{ isCurrentMonth }">
810
+ <div v-if="isCurrentMonth">暂无安排</div>
811
+ </template>
812
+ </EventCalendar>`,
813
+
814
+ events: `<EventCalendar
815
+ :data="data"
816
+ @day-click="onDayClick"
817
+ @item-click="onItemClick"
818
+ @tag-click="onTagClick"
819
+ />
820
+
821
+ onDayClick(scope, event) {
822
+ // scope = { day, date, dateKey, items, raw, isCurrentMonth, isToday, ... }
823
+ }
824
+ onItemClick(scope, event) {
825
+ // scope = { item, label, badge, type, index, day, dateKey }
826
+ // item 是后端返回的原始对象,字段齐全
827
+ }
828
+ onTagClick(scope, event) {
829
+ // 点角标;未监听时会自动退化成 item-click
830
+ }`,
831
+
832
+ theme: `<EventCalendar
833
+ :data="data"
834
+ :theme="{
835
+ primary: '#10a37f',
836
+ primaryGhost: 'rgba(16,163,127,.12)',
837
+ itemBg: '#f2f9f6',
838
+ radius: '10px'
839
+ }"
840
+ :type-colors="{ talk: { color: '#3d6cf0', bg: 'rgba(61,108,240,.1)' } }"
841
+ :colorful="true"
842
+ />`
843
+ },
844
+
845
+ /* -------- API 表格 -------- */
846
+ apiProps: [
847
+ ['data', '日历数据,支持「按月分组」和「扁平日期数组」两种结构', 'Array', '[]'],
848
+ ['field-names', '字段映射,覆盖默认字段名,支持 a.b.c 点路径', 'Object', '{}'],
849
+ ['loading', '加载中,为 true 时渲染骨架屏', 'Boolean', 'false'],
850
+ ['skeleton-rows', '骨架屏行数', 'Number', '5'],
851
+ ['active-month', '当前月份,支持 v-model / .sync', 'String', "''"],
852
+ ['show-month-select', '是否显示月份下拉框', 'Boolean', 'true'],
853
+ ['show-arrows', '是否显示左右翻月箭头', 'Boolean', 'false'],
854
+ ['autoplay', '是否开启自动轮播', 'Boolean', 'false'],
855
+ ['autoplay-interval', '轮播间隔(毫秒)', 'Number', '60000'],
856
+ ['loop', '轮播到最后一个月后是否回到第一个', 'Boolean', 'true'],
857
+ ['pause-on-hover', '鼠标悬停时暂停轮播', 'Boolean', 'true'],
858
+ ['show-progress', '显示轮播倒计时进度条', 'Boolean', 'true'],
859
+ ['show-autoplay-toggle', '显示播放/暂停按钮', 'Boolean', 'true'],
860
+ ['transition-effect', '切换动效:slide / fade / zoom / flip / none', 'String', "'slide'"],
861
+ ['transition-duration', '动效时长(毫秒)', 'Number', '420'],
862
+ ['cell-stagger', '单元格错峰入场动画', 'Boolean', 'false'],
863
+ ['show-header', '是否显示顶部区域', 'Boolean', 'true'],
864
+ ['title / subtitle', '默认标题与副标题(header-left 未传时生效)', 'String', "''"],
865
+ ['first-day-of-week', '一周起始日,0=周日 1=周一', 'Number', '0'],
866
+ ['week-labels', '星期文案,下标对应 Date.getDay()', 'Array', "['周日'…'周六']"],
867
+ ['max-visible-items', '单日最多显示条目数', 'Number', '4'],
868
+ ['overflow-mode', '溢出处理:scroll / collapse', 'String', "'scroll'"],
869
+ ['item-height', '单条条目高度(px)', 'Number', '28'],
870
+ ['item-gap', '条目间距(px)', 'Number', '6'],
871
+ ['min-row-height', '单元格最小高度(px)', 'Number', '118'],
872
+ ['show-adjacent-items', '补位日期是否也渲染条目', 'Boolean', 'true'],
873
+ ['highlight-today', '高亮今天', 'Boolean', 'true'],
874
+ ['colorful', '角标是否按 type 上色', 'Boolean', 'true'],
875
+ ['type-colors', '类型配色 { type: { color, bg } }', 'Object', '内置 4 种'],
876
+ ['day-formatter', '日期文案格式化 (cell) => string', 'Function', "'9月7日'"],
877
+ ['month-formatter', '月份下拉文案格式化 (monthKey, raw) => string', 'Function', 'null'],
878
+ ['empty-text', '空数据文案', 'String', "'暂无日程数据'"],
879
+ ['cell-focusable', '单元格可聚焦(键盘无障碍)', 'Boolean', 'false'],
880
+ ['theme', '主题变量覆盖,如 { primary, radius, itemBg }', 'Object', '{}']
881
+ ],
882
+ apiEvents: [
883
+ ['day-click', '点击日期格子', '(scope, event)'],
884
+ ['item-click', '点击条目', '(scope, event)'],
885
+ ['tag-click', '点击条目右上角角标;未监听时退化为 item-click', '(scope, event)'],
886
+ ['more-click', 'collapse 模式下点击「还有 N 项」', '(scope, event)'],
887
+ ['month-change', '月份变化', '(month, { index, source, option, raw })'],
888
+ ['update:activeMonth', '配合 v-model / .sync', '(month)'],
889
+ ['autoplay-toggle', '点击播放/暂停按钮', '(playing: Boolean)'],
890
+ ['autoplay-end', 'loop=false 时播到最后一个月', '—'],
891
+ ['transition-end', '月份切换动画结束', '(month)']
892
+ ],
893
+ apiSlots: [
894
+ ['header-left', '顶部左侧(设计稿的标题区)', '{ month, monthLabel, monthIndex, monthCount, prev, next, goTo }'],
895
+ ['header-right', '顶部右侧,整体替换默认的按钮 + 下拉', '同上'],
896
+ ['weekday', '星期表头单元', '{ label, weekday, isWeekend, index }'],
897
+ ['day', '整个日期格子(覆盖所有默认内容)', '{ day, dateKey, items, raw, isCurrentMonth, isToday, label }'],
898
+ ['day-header', '格子里的日期行', '同 day'],
899
+ ['items', '格子里的条目列表区域', '同 day'],
900
+ ['item', '单个条目', '{ item, label, badge, type, disabled, index, day, dateKey }'],
901
+ ['day-empty', '当天无数据时的占位', '同 day'],
902
+ ['loading', '自定义骨架屏', '{ rows }'],
903
+ ['empty', '整体空状态', '—']
904
+ ],
905
+ apiMethods: [
906
+ ['prev()', '切到上一个月'],
907
+ ['next()', '切到下一个月'],
908
+ ['goTo(index)', '切到第 index 个月(从 0 开始)'],
909
+ ['setMonth(month)', '切到指定月份,如 setMonth("2026-10")'],
910
+ ['restartAutoplay()', '重置轮播计时']
911
+ ]
912
+ }
913
+ },
914
+
915
+ computed: {
916
+ currentTheme() {
917
+ const hit = this.themePresets.find((t) => t.name === this.themeName)
918
+ return hit ? hit.vars : {}
919
+ },
920
+
921
+ /** 插槽示例里用到的当月场次总数 */
922
+ monthItemCount() {
923
+ const month = this.multiData[0]
924
+ if (!month) return 0
925
+ return (month.days || []).reduce((sum, d) => sum + (d.items || []).length, 0)
926
+ }
927
+ },
928
+
929
+ created() {
930
+ this.loadAsync()
931
+ this.loadMulti()
932
+ this.loadCrossYear()
933
+ this.loadSchema()
934
+ this.loadOverflow()
935
+ },
936
+
937
+ mounted() {
938
+ window.addEventListener('scroll', this.handleScroll, true)
939
+ },
940
+
941
+ beforeDestroy() {
942
+ window.removeEventListener('scroll', this.handleScroll, true)
943
+ },
944
+
945
+ methods: {
946
+ /* ------------------------- 数据加载 ------------------------- */
947
+ async loadAsync() {
948
+ this.asyncLoading = true
949
+ try {
950
+ this.asyncData = await fetchCalendarData({ months: ['2026-09'], delay: 1500 })
951
+ } finally {
952
+ // 放 finally:接口失败也要收掉骨架屏,否则页面会一直卡在加载态
953
+ this.asyncLoading = false
954
+ }
955
+ },
956
+
957
+ async loadMulti() {
958
+ this.multiLoading = true
959
+ try {
960
+ this.multiData = await fetchCalendarData({
961
+ months: ['2026-09', '2026-10', '2026-11'],
962
+ delay: 1200
963
+ })
964
+ } finally {
965
+ this.multiLoading = false
966
+ }
967
+ },
968
+
969
+ async loadCrossYear() {
970
+ this.crossYearLoading = true
971
+ try {
972
+ this.crossYearData = await fetchCrossYearData({ delay: 1000 })
973
+ } finally {
974
+ this.crossYearLoading = false
975
+ }
976
+ },
977
+
978
+ async loadSchema() {
979
+ this.schemaLoading = true
980
+ try {
981
+ this.schemaData = await fetchCalendarDataV2({ months: ['2026-09', '2026-10'], delay: 1400 })
982
+ } finally {
983
+ this.schemaLoading = false
984
+ }
985
+ },
986
+
987
+ async loadOverflow() {
988
+ this.overflowLoading = true
989
+ try {
990
+ // density=1 + maxItems=6:让几乎每天都有数据,且部分日期超过 4 条
991
+ this.overflowData = await fetchCalendarData({
992
+ months: ['2026-09'],
993
+ delay: 800,
994
+ maxItems: 6,
995
+ density: 0.95
996
+ })
997
+ } finally {
998
+ this.overflowLoading = false
999
+ }
1000
+ },
1001
+
1002
+ /* ------------------------- 事件回调 ------------------------- */
1003
+ onMonthChange(month, info) {
1004
+ this.pushLog('month', `切到 ${month}(来源:${info.source})`)
1005
+ },
1006
+
1007
+ onDayClick(scope) {
1008
+ this.pushLog('day', `${scope.dateKey} · ${scope.items.length} 场`)
1009
+ },
1010
+
1011
+ onItemClick(scope) {
1012
+ this.pushLog('item', `${scope.dateKey} · ${scope.label}`)
1013
+ },
1014
+
1015
+ onTagClick(scope) {
1016
+ this.pushLog('tag', `${scope.label} 的角标「${scope.badge}」`)
1017
+ },
1018
+
1019
+ onMoreClick(scope) {
1020
+ this.pushLog('more', `${scope.dateKey} 共 ${scope.items.length} 场`)
1021
+ },
1022
+
1023
+ pushLog(type, text) {
1024
+ this.logs.unshift({ type, text })
1025
+ if (this.logs.length > 12) this.logs.pop()
1026
+ },
1027
+
1028
+ /* ------------------------- 目录导航 ------------------------- */
1029
+ scrollTo(id) {
1030
+ const el = document.getElementById(id)
1031
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
1032
+ this.activeSection = id
1033
+ },
1034
+
1035
+ /** 滚动时同步高亮左侧目录 */
1036
+ handleScroll() {
1037
+ const offset = 120
1038
+ let current = this.sections[0].id
1039
+ for (const s of this.sections) {
1040
+ const el = document.getElementById(s.id)
1041
+ if (el && el.getBoundingClientRect().top <= offset) current = s.id
1042
+ }
1043
+ this.activeSection = current
1044
+ }
1045
+ }
1046
+ }
1047
+ </script>
1048
+
1049
+ <style scoped>
1050
+ /* ============================ 文档页外壳 ============================ */
1051
+ .doc {
1052
+ padding: 24px;
1053
+ background: #fafbfc;
1054
+ color: #303133;
1055
+ font-size: 14px;
1056
+ line-height: 1.6;
1057
+ }
1058
+
1059
+ .doc code {
1060
+ padding: 1px 5px;
1061
+ border-radius: 4px;
1062
+ background: #eef0f4;
1063
+ color: #476582;
1064
+ font-family: Consolas, Monaco, 'Courier New', monospace;
1065
+ font-size: 0.92em;
1066
+ }
1067
+
1068
+ /* ------------------------------ 页头 ------------------------------ */
1069
+ .doc__hero {
1070
+ padding: 28px 32px;
1071
+ margin-bottom: 24px;
1072
+ border-radius: 14px;
1073
+ background: linear-gradient(120deg, #eef3ff 0%, #f7f9ff 45%, #fdf6ff 100%);
1074
+ border: 1px solid #e8ecf6;
1075
+ }
1076
+
1077
+ .doc__hero-title {
1078
+ margin: 0 0 10px;
1079
+ font-size: 26px;
1080
+ font-weight: 700;
1081
+ }
1082
+
1083
+ .doc__hero-desc {
1084
+ max-width: 860px;
1085
+ margin: 0 0 16px;
1086
+ color: #5c6478;
1087
+ }
1088
+
1089
+ .doc__hero-tags {
1090
+ display: flex;
1091
+ flex-wrap: wrap;
1092
+ gap: 8px;
1093
+ }
1094
+
1095
+ .doc__chip {
1096
+ padding: 3px 12px;
1097
+ border-radius: 999px;
1098
+ background: rgba(61, 108, 240, 0.1);
1099
+ color: #3d6cf0;
1100
+ font-size: 12px;
1101
+ font-weight: 500;
1102
+ }
1103
+
1104
+ /* ------------------------------ 主体布局 ------------------------------ */
1105
+ .doc__body {
1106
+ display: flex;
1107
+ align-items: flex-start;
1108
+ gap: 24px;
1109
+ }
1110
+
1111
+ .doc__toc {
1112
+ position: sticky;
1113
+ top: 16px;
1114
+ flex: none;
1115
+ width: 172px;
1116
+ padding: 14px;
1117
+ border-radius: 12px;
1118
+ background: #fff;
1119
+ border: 1px solid #ebeef5;
1120
+ }
1121
+
1122
+ .doc__toc-title {
1123
+ margin: 0 0 8px;
1124
+ padding-left: 10px;
1125
+ color: #909399;
1126
+ font-size: 12px;
1127
+ }
1128
+
1129
+ .doc__toc-link {
1130
+ display: block;
1131
+ padding: 6px 10px;
1132
+ border-radius: 6px;
1133
+ color: #606266;
1134
+ font-size: 13px;
1135
+ text-decoration: none;
1136
+ transition: background-color 0.16s, color 0.16s;
1137
+ }
1138
+
1139
+ .doc__toc-link:hover {
1140
+ background: #f5f7fa;
1141
+ color: #3d6cf0;
1142
+ }
1143
+
1144
+ .doc__toc-link.is-active {
1145
+ background: rgba(61, 108, 240, 0.1);
1146
+ color: #3d6cf0;
1147
+ font-weight: 600;
1148
+ }
1149
+
1150
+ .doc__main {
1151
+ flex: 1;
1152
+ min-width: 0;
1153
+ }
1154
+
1155
+ /* ------------------------------ 示例卡片 ------------------------------ */
1156
+ .demo {
1157
+ padding: 24px;
1158
+ margin-bottom: 24px;
1159
+ border-radius: 12px;
1160
+ background: #fff;
1161
+ border: 1px solid #ebeef5;
1162
+ scroll-margin-top: 16px;
1163
+ }
1164
+
1165
+ .demo__title {
1166
+ margin: 0 0 8px;
1167
+ font-size: 18px;
1168
+ font-weight: 600;
1169
+ }
1170
+
1171
+ .demo__sub {
1172
+ margin: 22px 0 10px;
1173
+ font-size: 15px;
1174
+ font-weight: 600;
1175
+ color: #303133;
1176
+ }
1177
+
1178
+ .demo__desc {
1179
+ margin: 0 0 16px;
1180
+ color: #5c6478;
1181
+ }
1182
+
1183
+ .demo__stage {
1184
+ padding: 16px;
1185
+ border-radius: 10px;
1186
+ border: 1px solid #ebeef5;
1187
+ background: #fff;
1188
+ }
1189
+
1190
+ .demo__stage--flex {
1191
+ flex: 1;
1192
+ min-width: 0;
1193
+ }
1194
+
1195
+ .demo__toolbar {
1196
+ display: flex;
1197
+ align-items: center;
1198
+ flex-wrap: wrap;
1199
+ gap: 10px;
1200
+ margin-bottom: 14px;
1201
+ }
1202
+
1203
+ .demo__cols {
1204
+ display: flex;
1205
+ gap: 16px;
1206
+ flex-wrap: wrap;
1207
+ }
1208
+
1209
+ .demo__cols--events {
1210
+ align-items: stretch;
1211
+ flex-wrap: nowrap;
1212
+ }
1213
+
1214
+ .demo__col {
1215
+ flex: 1;
1216
+ min-width: 280px;
1217
+ }
1218
+
1219
+ .demo__col-title {
1220
+ margin: 0 0 8px;
1221
+ color: #909399;
1222
+ font-size: 13px;
1223
+ }
1224
+
1225
+ /* ------------------------------ 控件 ------------------------------ */
1226
+ .doc__btn {
1227
+ height: 32px;
1228
+ padding: 0 14px;
1229
+ border: 1px solid #3d6cf0;
1230
+ border-radius: 8px;
1231
+ background: #3d6cf0;
1232
+ color: #fff;
1233
+ font-size: 13px;
1234
+ cursor: pointer;
1235
+ transition: opacity 0.18s, background-color 0.18s;
1236
+ }
1237
+
1238
+ .doc__btn:hover:not(:disabled) {
1239
+ opacity: 0.88;
1240
+ }
1241
+
1242
+ .doc__btn:disabled {
1243
+ cursor: not-allowed;
1244
+ opacity: 0.55;
1245
+ }
1246
+
1247
+ .doc__btn--ghost {
1248
+ background: #fff;
1249
+ color: #3d6cf0;
1250
+ }
1251
+
1252
+ .doc__btn--ghost:hover {
1253
+ background: rgba(61, 108, 240, 0.08);
1254
+ }
1255
+
1256
+ .doc__btn--sm {
1257
+ height: 24px;
1258
+ padding: 0 10px;
1259
+ font-size: 12px;
1260
+ }
1261
+
1262
+ .doc__seg {
1263
+ height: 30px;
1264
+ padding: 0 12px;
1265
+ border: 1px solid #dcdfe6;
1266
+ border-radius: 8px;
1267
+ background: #fff;
1268
+ color: #606266;
1269
+ font-size: 13px;
1270
+ cursor: pointer;
1271
+ transition: all 0.18s;
1272
+ }
1273
+
1274
+ .doc__seg:hover {
1275
+ border-color: #3d6cf0;
1276
+ color: #3d6cf0;
1277
+ }
1278
+
1279
+ .doc__seg.is-active {
1280
+ border-color: #3d6cf0;
1281
+ background: #3d6cf0;
1282
+ color: #fff;
1283
+ }
1284
+
1285
+ .doc__check {
1286
+ display: inline-flex;
1287
+ align-items: center;
1288
+ gap: 6px;
1289
+ color: #606266;
1290
+ font-size: 13px;
1291
+ cursor: pointer;
1292
+ user-select: none;
1293
+ }
1294
+
1295
+ .doc__check input {
1296
+ cursor: pointer;
1297
+ }
1298
+
1299
+ .doc__range {
1300
+ width: 130px;
1301
+ cursor: pointer;
1302
+ }
1303
+
1304
+ .doc__hint {
1305
+ color: #909399;
1306
+ font-size: 13px;
1307
+ }
1308
+
1309
+ .doc__code-inline {
1310
+ color: #3d6cf0 !important;
1311
+ }
1312
+
1313
+ /* ------------------------------ 事件日志 ------------------------------ */
1314
+ .event-log {
1315
+ flex: none;
1316
+ width: 280px;
1317
+ display: flex;
1318
+ flex-direction: column;
1319
+ border: 1px solid #ebeef5;
1320
+ border-radius: 10px;
1321
+ background: #fbfcfe;
1322
+ overflow: hidden;
1323
+ }
1324
+
1325
+ .event-log__head {
1326
+ display: flex;
1327
+ align-items: center;
1328
+ justify-content: space-between;
1329
+ padding: 10px 12px;
1330
+ border-bottom: 1px solid #ebeef5;
1331
+ font-size: 13px;
1332
+ font-weight: 600;
1333
+ color: #303133;
1334
+ }
1335
+
1336
+ .event-log__list {
1337
+ flex: 1;
1338
+ margin: 0;
1339
+ padding: 8px;
1340
+ list-style: none;
1341
+ overflow-y: auto;
1342
+ max-height: 420px;
1343
+ }
1344
+
1345
+ .event-log__item {
1346
+ display: flex;
1347
+ align-items: flex-start;
1348
+ gap: 8px;
1349
+ padding: 6px 4px;
1350
+ border-bottom: 1px dashed #eef0f4;
1351
+ font-size: 12px;
1352
+ }
1353
+
1354
+ .event-log__item:last-child {
1355
+ border-bottom: none;
1356
+ }
1357
+
1358
+ .event-log__type {
1359
+ flex: none;
1360
+ padding: 1px 6px;
1361
+ border-radius: 4px;
1362
+ font-size: 11px;
1363
+ background: #eef0f4;
1364
+ color: #606266;
1365
+ }
1366
+
1367
+ .event-log__type.is-day { background: rgba(61, 108, 240, 0.12); color: #3d6cf0; }
1368
+ .event-log__type.is-item { background: rgba(16, 163, 127, 0.12); color: #10a37f; }
1369
+ .event-log__type.is-tag { background: rgba(224, 130, 27, 0.14); color: #e0821b; }
1370
+ .event-log__type.is-month { background: rgba(139, 92, 246, 0.12); color: #8b5cf6; }
1371
+ .event-log__type.is-more { background: rgba(107, 114, 128, 0.14); color: #6b7280; }
1372
+
1373
+ .event-log__text {
1374
+ color: #5c6478;
1375
+ word-break: break-all;
1376
+ }
1377
+
1378
+ .event-log__empty {
1379
+ margin: 0;
1380
+ padding: 40px 16px;
1381
+ color: #c0c4cc;
1382
+ font-size: 13px;
1383
+ text-align: center;
1384
+ }
1385
+
1386
+ /* ------------------------------ API 表格 ------------------------------ */
1387
+ .doc__table-wrap {
1388
+ overflow-x: auto;
1389
+ border: 1px solid #ebeef5;
1390
+ border-radius: 10px;
1391
+ }
1392
+
1393
+ .doc__table {
1394
+ width: 100%;
1395
+ border-collapse: collapse;
1396
+ font-size: 13px;
1397
+ }
1398
+
1399
+ .doc__table th,
1400
+ .doc__table td {
1401
+ padding: 10px 14px;
1402
+ text-align: left;
1403
+ border-bottom: 1px solid #f0f2f5;
1404
+ vertical-align: top;
1405
+ }
1406
+
1407
+ .doc__table th {
1408
+ background: #fafbfc;
1409
+ color: #303133;
1410
+ font-weight: 600;
1411
+ white-space: nowrap;
1412
+ }
1413
+
1414
+ .doc__table tbody tr:last-child td {
1415
+ border-bottom: none;
1416
+ }
1417
+
1418
+ .doc__table tbody tr:hover {
1419
+ background: #fafbfd;
1420
+ }
1421
+
1422
+ .doc__type {
1423
+ color: #909399;
1424
+ font-family: Consolas, Monaco, monospace;
1425
+ font-size: 12px;
1426
+ }
1427
+
1428
+ /* ==================== 插槽示例里的自定义样式 ====================
1429
+ * 注意:scoped 样式默认不作用于插槽内容的子组件根节点之外,
1430
+ * 但这些 DOM 是在本组件模板里写的,所以带的是本组件的 scopeId,样式生效。
1431
+ * ============================================================== */
1432
+ .custom-head {
1433
+ display: flex;
1434
+ align-items: center;
1435
+ gap: 12px;
1436
+ }
1437
+
1438
+ .custom-head__badge {
1439
+ padding: 6px 14px;
1440
+ border-radius: 10px;
1441
+ background: linear-gradient(135deg, #3d6cf0, #6c8ff5);
1442
+ color: #fff;
1443
+ font-size: 14px;
1444
+ font-weight: 600;
1445
+ white-space: nowrap;
1446
+ }
1447
+
1448
+ .custom-head__meta {
1449
+ display: flex;
1450
+ flex-direction: column;
1451
+ line-height: 1.4;
1452
+ }
1453
+
1454
+ .custom-head__meta strong {
1455
+ font-size: 15px;
1456
+ }
1457
+
1458
+ .custom-head__meta span {
1459
+ color: #909399;
1460
+ font-size: 12px;
1461
+ }
1462
+
1463
+ .custom-item {
1464
+ width: 100%;
1465
+ min-width: 0;
1466
+ padding: 2px 0;
1467
+ }
1468
+
1469
+ .custom-item__row {
1470
+ display: flex;
1471
+ align-items: center;
1472
+ gap: 6px;
1473
+ min-width: 0;
1474
+ }
1475
+
1476
+ .custom-item__row--sub {
1477
+ margin-top: 2px;
1478
+ }
1479
+
1480
+ .custom-item__time {
1481
+ flex: none;
1482
+ color: #3d6cf0;
1483
+ font-size: 11px;
1484
+ font-weight: 600;
1485
+ }
1486
+
1487
+ .custom-item.is-fair .custom-item__time { color: #e0821b; }
1488
+ .custom-item.is-live .custom-item__time { color: #10a37f; }
1489
+ .custom-item.is-exam .custom-item__time { color: #8b5cf6; }
1490
+
1491
+ .custom-item__name {
1492
+ flex: 1;
1493
+ min-width: 0;
1494
+ overflow: hidden;
1495
+ white-space: nowrap;
1496
+ text-overflow: ellipsis;
1497
+ font-size: 12px;
1498
+ color: #303133;
1499
+ }
1500
+
1501
+ .custom-item__addr {
1502
+ flex: 1;
1503
+ min-width: 0;
1504
+ overflow: hidden;
1505
+ white-space: nowrap;
1506
+ text-overflow: ellipsis;
1507
+ color: #a8abb2;
1508
+ font-size: 11px;
1509
+ }
1510
+
1511
+ .custom-item__tag {
1512
+ flex: none;
1513
+ padding: 0 5px;
1514
+ border-radius: 3px;
1515
+ background: rgba(0, 0, 0, 0.05);
1516
+ color: #909399;
1517
+ font-size: 10px;
1518
+ }
1519
+
1520
+ .custom-empty {
1521
+ padding: 6px 0;
1522
+ color: #dcdfe6;
1523
+ font-size: 12px;
1524
+ text-align: center;
1525
+ }
1526
+
1527
+ /* ------------------------------ 响应式 ------------------------------ */
1528
+ @media (max-width: 1100px) {
1529
+ .doc__toc {
1530
+ display: none;
1531
+ }
1532
+ .demo__cols--events {
1533
+ flex-wrap: wrap;
1534
+ }
1535
+ .event-log {
1536
+ width: 100%;
1537
+ }
1538
+ }
1539
+
1540
+ @media (max-width: 768px) {
1541
+ .doc {
1542
+ padding: 12px;
1543
+ }
1544
+ .doc__hero {
1545
+ padding: 20px;
1546
+ }
1547
+ .demo {
1548
+ padding: 16px;
1549
+ }
1550
+ }
1551
+ </style>