npmapps 1.1.4 → 1.1.6

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 (293) hide show
  1. package/app/wujie-wk/dev.log +1979 -0
  2. package/app/wujie-wk/t.html +90 -0
  3. package/app/wujie-wk/wujie-react-child/README.md +126 -0
  4. package/app/wujie-wk/wujie-react-child/eslint.config.js +28 -0
  5. package/app/wujie-wk/wujie-react-child/index.html +13 -0
  6. package/app/wujie-wk/wujie-react-child/package-lock.json +4344 -0
  7. package/app/wujie-wk/wujie-react-child/package.json +32 -0
  8. package/app/wujie-wk/wujie-react-child/public/vite.svg +1 -0
  9. package/app/wujie-wk/wujie-react-child/src/App.css +0 -0
  10. package/app/wujie-wk/wujie-react-child/src/App.tsx +72 -0
  11. package/app/wujie-wk/wujie-react-child/src/assets/react.svg +1 -0
  12. package/app/wujie-wk/wujie-react-child/src/components/FeatureCards.css +63 -0
  13. package/app/wujie-wk/wujie-react-child/src/components/FeatureCards.tsx +112 -0
  14. package/app/wujie-wk/wujie-react-child/src/components/LightCard/index.tsx +36 -0
  15. package/app/wujie-wk/wujie-react-child/src/components/LightCard/style.css +20 -0
  16. package/app/wujie-wk/wujie-react-child/src/hooks/use3DCard.ts +53 -0
  17. package/app/wujie-wk/wujie-react-child/src/hooks/useLightCard.ts +141 -0
  18. package/app/wujie-wk/wujie-react-child/src/index.css +0 -0
  19. package/app/wujie-wk/wujie-react-child/src/main.tsx +8 -0
  20. package/app/wujie-wk/wujie-react-child/src/pages/ChildToParent/index.css +55 -0
  21. package/app/wujie-wk/wujie-react-child/src/pages/ChildToParent/index.tsx +88 -0
  22. package/app/wujie-wk/wujie-react-child/src/pages/Css.tsx +28 -0
  23. package/app/wujie-wk/wujie-react-child/src/pages/Home.tsx +72 -0
  24. package/app/wujie-wk/wujie-react-child/src/pages/ParentToChild.css +85 -0
  25. package/app/wujie-wk/wujie-react-child/src/pages/ParentToChild.tsx +119 -0
  26. package/app/wujie-wk/wujie-react-child/src/pages/RouterJump/index.css +61 -0
  27. package/app/wujie-wk/wujie-react-child/src/pages/RouterJump/index.tsx +87 -0
  28. package/app/wujie-wk/wujie-react-child/src/router/index.tsx +63 -0
  29. package/app/wujie-wk/wujie-react-child/src/types/wujie.d.ts +26 -0
  30. package/app/wujie-wk/wujie-react-child/src/vite-env.d.ts +1 -0
  31. package/app/wujie-wk/wujie-react-child/tsconfig.app.json +26 -0
  32. package/app/wujie-wk/wujie-react-child/tsconfig.json +7 -0
  33. package/app/wujie-wk/wujie-react-child/tsconfig.node.json +24 -0
  34. package/app/wujie-wk/wujie-react-child/vite.config.ts +10 -0
  35. package/app/wujie-wk/wujie-vue2-child/.eslintrc.js +19 -0
  36. package/app/wujie-wk/wujie-vue2-child/README.md +24 -0
  37. package/app/wujie-wk/wujie-vue2-child/babel.config.js +5 -0
  38. package/app/wujie-wk/wujie-vue2-child/docs/superpowers/plans/2026-05-29-quill-editor.md +836 -0
  39. package/app/wujie-wk/wujie-vue2-child/docs/superpowers/specs/2026-05-29-quill-editor-design.md +210 -0
  40. package/app/wujie-wk/wujie-vue2-child/jsconfig.json +19 -0
  41. package/app/wujie-wk/wujie-vue2-child/package-lock.json +21333 -0
  42. package/app/wujie-wk/wujie-vue2-child/package.json +63 -0
  43. package/app/wujie-wk/wujie-vue2-child/postcss.config.js +10 -0
  44. package/app/wujie-wk/wujie-vue2-child/public/favicon.ico +0 -0
  45. package/app/wujie-wk/wujie-vue2-child/public/index.html +17 -0
  46. package/app/wujie-wk/wujie-vue2-child/public/rili.jpg +0 -0
  47. package/app/wujie-wk/wujie-vue2-child/public//344/270/200/351/224/256/351/273/221/346/232/227.html +136 -0
  48. package/app/wujie-wk/wujie-vue2-child/src/App.vue +110 -0
  49. package/app/wujie-wk/wujie-vue2-child/src/assets/bpmn-camunda.jpg +0 -0
  50. package/app/wujie-wk/wujie-vue2-child/src/assets/css/diagram.less +17 -0
  51. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/Icon.less +31 -0
  52. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app-codes.css +26 -0
  53. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.eot +0 -0
  54. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.svg +60 -0
  55. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.ttf +0 -0
  56. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.woff +0 -0
  57. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/app.woff2 +0 -0
  58. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/config.json +248 -0
  59. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-bottom-tool.svg +30 -0
  60. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-horizontal-center-tool.svg +85 -0
  61. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-left-tool.svg +84 -0
  62. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-right-tool.svg +80 -0
  63. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-top-tool.svg +84 -0
  64. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/align-vertical-center-tool.svg +89 -0
  65. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/distribute-horizontally-tool.svg +95 -0
  66. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/distribute-vertically-tool.svg +99 -0
  67. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/raw/set-color-tool.svg +111 -0
  68. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-bottom-tool.svg +30 -0
  69. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-horizontal-center-tool.svg +30 -0
  70. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-left-tool.svg +30 -0
  71. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-right-tool.svg +30 -0
  72. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-top-tool.svg +30 -0
  73. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/align-vertical-center-tool.svg +30 -0
  74. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/distribute-horizontally-tool.svg +30 -0
  75. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/distribute-vertically-tool.svg +30 -0
  76. package/app/wujie-wk/wujie-vue2-child/src/assets/icon/font/source/symbols/set-color-tool.svg +63 -0
  77. package/app/wujie-wk/wujie-vue2-child/src/assets/logo.png +0 -0
  78. package/app/wujie-wk/wujie-vue2-child/src/components/EllTable/README.md +70 -0
  79. package/app/wujie-wk/wujie-vue2-child/src/components/EllTable/article.md +184 -0
  80. package/app/wujie-wk/wujie-vue2-child/src/components/EllTable/index.js +213 -0
  81. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/CalendarSkeleton.vue +147 -0
  82. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/CodeBlock.vue +199 -0
  83. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/MonthSelect.vue +376 -0
  84. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/README.md +485 -0
  85. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/example.vue +1747 -0
  86. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/index.vue +2720 -0
  87. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/mockData.js +475 -0
  88. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/recruitmentAdapter.js +51 -0
  89. package/app/wujie-wk/wujie-vue2-child/src/components/EventCalendar/utils.js +202 -0
  90. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/FunctionSelector.vue +123 -0
  91. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/OperatorSelector.vue +184 -0
  92. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/ParameterSelector.vue +123 -0
  93. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/api.js +69 -0
  94. package/app/wujie-wk/wujie-vue2-child/src/components/FormulaEditor/index.vue +435 -0
  95. package/app/wujie-wk/wujie-vue2-child/src/components/HelloWorld.vue +58 -0
  96. package/app/wujie-wk/wujie-vue2-child/src/components/PageHeader/index.vue +158 -0
  97. package/app/wujie-wk/wujie-vue2-child/src/components/Splitter/README.md +144 -0
  98. package/app/wujie-wk/wujie-vue2-child/src/components/Splitter/example.vue +84 -0
  99. package/app/wujie-wk/wujie-vue2-child/src/components/Splitter/index.vue +203 -0
  100. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/ToolBar.vue +357 -0
  101. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/customTranslate/customTranslate.js +12 -0
  102. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/customTranslate/translationsGerman.js +241 -0
  103. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/index.vue +261 -0
  104. package/app/wujie-wk/wujie-vue2-child/src/components/diagram/xmlData.js +29 -0
  105. package/app/wujie-wk/wujie-vue2-child/src/directives/filldown.js +155 -0
  106. package/app/wujie-wk/wujie-vue2-child/src/directives/filldownTable.js +291 -0
  107. package/app/wujie-wk/wujie-vue2-child/src/main.js +40 -0
  108. package/app/wujie-wk/wujie-vue2-child/src/router/index.js +72 -0
  109. package/app/wujie-wk/wujie-vue2-child/src/store/index.js +23 -0
  110. package/app/wujie-wk/wujie-vue2-child/src/utils/winBox.js +23 -0
  111. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/A.vue +12 -0
  112. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/B.vue +10 -0
  113. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/MagicalComponentsForELFormItem.vue +78 -0
  114. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/index.vue +59 -0
  115. package/app/wujie-wk/wujie-vue2-child/src/views/Extend/tableMouseHorizontalWheel.vue +184 -0
  116. package/app/wujie-wk/wujie-vue2-child/src/views/Home.vue +27 -0
  117. package/app/wujie-wk/wujie-vue2-child/src/views/RouterJump.vue +145 -0
  118. package/app/wujie-wk/wujie-vue2-child/src/views/calendar/index.vue +19 -0
  119. package/app/wujie-wk/wujie-vue2-child/src/views/css.vue +57 -0
  120. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/EllipsisText.vue +73 -0
  121. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/HoverCard.vue +69 -0
  122. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/TableHover.vue +130 -0
  123. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/inputSlo.vue +42 -0
  124. package/app/wujie-wk/wujie-vue2-child/src/views/cssComponents/tableFixed.vue +148 -0
  125. package/app/wujie-wk/wujie-vue2-child/src/views/echarts/echart-dome.vue +73 -0
  126. package/app/wujie-wk/wujie-vue2-child/src/views/echarts/index.vue +118 -0
  127. package/app/wujie-wk/wujie-vue2-child/src/views/echarts/pei3d.vue +658 -0
  128. package/app/wujie-wk/wujie-vue2-child/src/views/element/bpmn/index.vue +15 -0
  129. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/attendanceCycle/index.vue +131 -0
  130. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/attendanceGroup/index.vue +147 -0
  131. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/attendancePersonnel/index.vue +158 -0
  132. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/companyCalendar/index.vue +147 -0
  133. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/shift/index.vue +147 -0
  134. package/app/wujie-wk/wujie-vue2-child/src/views/element/components/shiftRotationSystem/index.vue +147 -0
  135. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/columnManagement.vue +340 -0
  136. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/dialogInput.vue +71 -0
  137. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/elTableJsx.vue +1826 -0
  138. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/formTable.vue +595 -0
  139. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/index.vue +29 -0
  140. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx/simpleTable.vue +189 -0
  141. package/app/wujie-wk/wujie-vue2-child/src/views/element/elTableJsx.zip +0 -0
  142. package/app/wujie-wk/wujie-vue2-child/src/views/element/index.vue +39 -0
  143. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/README.md +163 -0
  144. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/example.vue +307 -0
  145. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/index.vue +409 -0
  146. package/app/wujie-wk/wujie-vue2-child/src/views/element/quillEditor/toolbar.js +122 -0
  147. package/app/wujie-wk/wujie-vue2-child/vue.config.js +15 -0
  148. package/app/wujie-wk/wujie-vue3-child/.claude/settings.local.json +8 -0
  149. package/app/wujie-wk/wujie-vue3-child/.vscode/extensions.json +3 -0
  150. package/app/wujie-wk/wujie-vue3-child/PROJECT_MEMORY.md +427 -0
  151. package/app/wujie-wk/wujie-vue3-child/README.md +5 -0
  152. package/app/wujie-wk/wujie-vue3-child/index.html +13 -0
  153. package/app/wujie-wk/wujie-vue3-child/package-lock.json +5744 -0
  154. package/app/wujie-wk/wujie-vue3-child/package.json +28 -0
  155. package/app/wujie-wk/wujie-vue3-child/public/vite.svg +1 -0
  156. package/app/wujie-wk/wujie-vue3-child/src/App.vue +130 -0
  157. package/app/wujie-wk/wujie-vue3-child/src/assets/vue.svg +1 -0
  158. package/app/wujie-wk/wujie-vue3-child/src/components/HelloWorld.vue +43 -0
  159. package/app/wujie-wk/wujie-vue3-child/src/components/SmartAnchorTabs/index.jsx +224 -0
  160. package/app/wujie-wk/wujie-vue3-child/src/components/SmartAnchorTabs/style.css +154 -0
  161. package/app/wujie-wk/wujie-vue3-child/src/components/tags-view.vue +193 -0
  162. package/app/wujie-wk/wujie-vue3-child/src/components/tags-view1.vue +131 -0
  163. package/app/wujie-wk/wujie-vue3-child/src/directives/aiLoading.js +182 -0
  164. package/app/wujie-wk/wujie-vue3-child/src/hooks/useClickOutside.js +11 -0
  165. package/app/wujie-wk/wujie-vue3-child/src/hooks/useTableDragSort.js +28 -0
  166. package/app/wujie-wk/wujie-vue3-child/src/main.js +18 -0
  167. package/app/wujie-wk/wujie-vue3-child/src/router/index.js +104 -0
  168. package/app/wujie-wk/wujie-vue3-child/src/store/tagsViewStroe.js +34 -0
  169. package/app/wujie-wk/wujie-vue3-child/src/style.css +171 -0
  170. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/collapseExpand/index.jsx +108 -0
  171. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/collapseExpand/index.module.scss +97 -0
  172. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/README.md +836 -0
  173. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/REFLEX_EXAMPLES.md +728 -0
  174. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentPersonnelSelector.jsx +687 -0
  175. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentPersonnelSelector.module.scss +560 -0
  176. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelector.jsx +570 -0
  177. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelector.module.scss +330 -0
  178. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelectorV2.jsx +378 -0
  179. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/DepartmentSelectorV2.module.scss +228 -0
  180. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/OptionsSelector.jsx +399 -0
  181. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/OptionsSelector.module.scss +252 -0
  182. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PersonnelSelector.jsx +585 -0
  183. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PersonnelSelector.module.scss +331 -0
  184. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PopoverSelector.jsx +392 -0
  185. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/PopoverSelector.module.scss +39 -0
  186. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/README.md +248 -0
  187. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/components/SelectorTrigger.jsx +194 -0
  188. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/index.jsx +1459 -0
  189. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/departmentPersonnel/mockData.js +301 -0
  190. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/dialogueSegment/index.jsx +182 -0
  191. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/dialogueSegment/index.module.scss +28 -0
  192. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/index.jsx +375 -0
  193. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ChartsPanel/index.jsx +121 -0
  194. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ChartsPanel/index.module.scss +76 -0
  195. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/DonutChart/index.jsx +104 -0
  196. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/PracticeTable/index.jsx +75 -0
  197. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/PracticeTable/index.module.scss +12 -0
  198. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankBarChart/index.jsx +62 -0
  199. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankBarChart/index.module.scss +43 -0
  200. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingGroup/index.jsx +29 -0
  201. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingGroup/index.module.scss +5 -0
  202. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingList/index.jsx +58 -0
  203. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/RankingList/index.module.scss +85 -0
  204. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ScriptStatsPanel/index.jsx +92 -0
  205. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/ScriptStatsPanel/index.module.scss +56 -0
  206. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/StatCardsRow/index.jsx +40 -0
  207. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/StatCardsRow/index.module.scss +53 -0
  208. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/echarts/EchartsDonut.jsx +106 -0
  209. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/components/echarts/EchartsRankBar.jsx +132 -0
  210. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/index.jsx +176 -0
  211. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/practiceStatus/index.module.scss +96 -0
  212. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/CoachReport/index.jsx +162 -0
  213. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/CoachReport/index.module.scss +16 -0
  214. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ComprehensiveEvaluation/index.jsx +29 -0
  215. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ComprehensiveEvaluation/index.module.scss +25 -0
  216. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueBubble/index.jsx +106 -0
  217. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueBubble/index.module.scss +164 -0
  218. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueRecord/index.jsx +182 -0
  219. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DialogueRecord/index.module.scss +203 -0
  220. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionDetail/index.jsx +145 -0
  221. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionDetail/index.module.scss +126 -0
  222. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionScores/index.jsx +67 -0
  223. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/DimensionScores/index.module.scss +105 -0
  224. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ReportHeader/index.jsx +81 -0
  225. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ReportHeader/index.module.scss +47 -0
  226. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/RoleInfo/index.jsx +64 -0
  227. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/RoleInfo/index.module.scss +85 -0
  228. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ScoreBadge/index.jsx +39 -0
  229. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/ScoreBadge/index.module.scss +44 -0
  230. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/SubDimensionItem/index.jsx +83 -0
  231. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/components/SubDimensionItem/index.module.scss +101 -0
  232. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/index.jsx +50 -0
  233. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/reportDetail/index.module.scss +25 -0
  234. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/scriptTable/index.jsx +196 -0
  235. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/scriptTable/index.module.scss +41 -0
  236. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/scriptTable/inputColumn/index.jsx +183 -0
  237. package/app/wujie-wk/wujie-vue3-child/src/views/aiCoach/scriptTable/inputColumn/index.module.scss +115 -0
  238. package/app/wujie-wk/wujie-vue3-child/src/views/child-to-parent.vue +117 -0
  239. package/app/wujie-wk/wujie-vue3-child/src/views/home.vue +53 -0
  240. package/app/wujie-wk/wujie-vue3-child/src/views/jsx/btnSelect/btnSelect.vue +169 -0
  241. package/app/wujie-wk/wujie-vue3-child/src/views/jsx/btnSelect/index.vue +69 -0
  242. package/app/wujie-wk/wujie-vue3-child/src/views/jsx/com.vue +44 -0
  243. package/app/wujie-wk/wujie-vue3-child/src/views/jsx/dialog.jsx +66 -0
  244. package/app/wujie-wk/wujie-vue3-child/src/views/jsx/index.vue +72 -0
  245. package/app/wujie-wk/wujie-vue3-child/src/views/jsx/props.vue +33 -0
  246. package/app/wujie-wk/wujie-vue3-child/src/views/parent-to-child.vue +225 -0
  247. package/app/wujie-wk/wujie-vue3-child/src/views/phone-code.vue +318 -0
  248. package/app/wujie-wk/wujie-vue3-child/src/views/router-jump.vue +123 -0
  249. package/app/wujie-wk/wujie-vue3-child/src/views/test.vue +192 -0
  250. package/app/wujie-wk/wujie-vue3-child/vite.config.js +15 -0
  251. package/app/wujie-wk/wujie-vue3-child copy/.vscode/extensions.json +3 -0
  252. package/app/wujie-wk/wujie-vue3-child copy/README.md +5 -0
  253. package/app/wujie-wk/wujie-vue3-child copy/index.html +13 -0
  254. package/app/wujie-wk/wujie-vue3-child copy/package-lock.json +4617 -0
  255. package/app/wujie-wk/wujie-vue3-child copy/package.json +22 -0
  256. package/app/wujie-wk/wujie-vue3-child copy/public/vite.svg +1 -0
  257. package/app/wujie-wk/wujie-vue3-child copy/src/App.vue +124 -0
  258. package/app/wujie-wk/wujie-vue3-child copy/src/assets/vue.svg +1 -0
  259. package/app/wujie-wk/wujie-vue3-child copy/src/components/HelloWorld.vue +43 -0
  260. package/app/wujie-wk/wujie-vue3-child copy/src/main.js +12 -0
  261. package/app/wujie-wk/wujie-vue3-child copy/src/router/index.js +84 -0
  262. package/app/wujie-wk/wujie-vue3-child copy/src/style.css +4 -0
  263. package/app/wujie-wk/wujie-vue3-child copy/src/views/child-to-parent.vue +117 -0
  264. package/app/wujie-wk/wujie-vue3-child copy/src/views/home.vue +53 -0
  265. package/app/wujie-wk/wujie-vue3-child copy/src/views/parent-to-child.vue +225 -0
  266. package/app/wujie-wk/wujie-vue3-child copy/src/views/phone-code.vue +318 -0
  267. package/app/wujie-wk/wujie-vue3-child copy/src/views/router-jump.vue +115 -0
  268. package/app/wujie-wk/wujie-vue3-child copy/src/views/test.vue +192 -0
  269. package/app/wujie-wk/wujie-vue3-child copy/vite.config.js +10 -0
  270. package/app/wujie-wk/wujie-vue3-main/.vscode/extensions.json +3 -0
  271. package/app/wujie-wk/wujie-vue3-main/README.md +154 -0
  272. package/app/wujie-wk/wujie-vue3-main/index.html +13 -0
  273. package/app/wujie-wk/wujie-vue3-main/package-lock.json +4736 -0
  274. package/app/wujie-wk/wujie-vue3-main/package.json +25 -0
  275. package/app/wujie-wk/wujie-vue3-main/public/vite.svg +1 -0
  276. package/app/wujie-wk/wujie-vue3-main/src/App.vue +879 -0
  277. package/app/wujie-wk/wujie-vue3-main/src/assets/vue.svg +1 -0
  278. package/app/wujie-wk/wujie-vue3-main/src/components/HelloWorld.vue +43 -0
  279. package/app/wujie-wk/wujie-vue3-main/src/components/WujieReact/index.vue +20 -0
  280. package/app/wujie-wk/wujie-vue3-main/src/components/WujieVue2/index.vue +8 -0
  281. package/app/wujie-wk/wujie-vue3-main/src/components/WujieVue3/index.vue +8 -0
  282. package/app/wujie-wk/wujie-vue3-main/src/components/WujieVue3Copy/index.vue +8 -0
  283. package/app/wujie-wk/wujie-vue3-main/src/main.js +23 -0
  284. package/app/wujie-wk/wujie-vue3-main/src/router/index.js +88 -0
  285. package/app/wujie-wk/wujie-vue3-main/src/store/useMicroAppStore.js +66 -0
  286. package/app/wujie-wk/wujie-vue3-main/src/style.css +3 -0
  287. package/app/wujie-wk/wujie-vue3-main/src/views/home.vue +103 -0
  288. package/app/wujie-wk/wujie-vue3-main/src/views/router-jump.vue +159 -0
  289. package/app/wujie-wk/wujie-vue3-main/src/views/test.vue +125 -0
  290. package/app/wujie-wk/wujie-vue3-main/src/views/vue3-child-msg.vue +108 -0
  291. package/app/wujie-wk/wujie-vue3-main/vite.config.js +12 -0
  292. package/package.json +1 -1
  293. package/app/check-import-case.js +0 -344
@@ -0,0 +1,2720 @@
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>