foucui 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/README.md +43 -0
  2. package/components/QiuAffix/index.vue +186 -0
  3. package/components/QiuAlert/index.vue +208 -0
  4. package/components/QiuAnchor/context.ts +18 -0
  5. package/components/QiuAnchor/index.vue +180 -0
  6. package/components/QiuAnchorLink/index.vue +67 -0
  7. package/components/QiuApp/index.vue +9 -0
  8. package/components/QiuAutocomplete/index.vue +408 -0
  9. package/components/QiuAvatar/index.vue +140 -0
  10. package/components/QiuBacktop/index.vue +121 -0
  11. package/components/QiuBadge/api/index.ts +389 -0
  12. package/components/QiuBadge/components/BadgeImagePickerDialog.vue +243 -0
  13. package/components/QiuBadge/components/BadgeLibraryManagerDialog.vue +30 -0
  14. package/components/QiuBadge/components/BadgeLibraryPanel.vue +182 -0
  15. package/components/QiuBadge/composables/useTableHostHeight.ts +53 -0
  16. package/components/QiuBadge/index.ts +18 -0
  17. package/components/QiuBadge/shared/BadgeTemplateRenderer.vue +32 -0
  18. package/components/QiuBadge/shared/assetUrl.ts +26 -0
  19. package/components/QiuBadge/shared/badgeTemplate.ts +309 -0
  20. package/components/QiuBadge/utils/chinaAreaData.ts +44 -0
  21. package/components/QiuBadge/utils/hostRegion.ts +22 -0
  22. package/components/QiuBadge/utils/pca-code.json +1 -0
  23. package/components/QiuBadge/utils/pdfFromElement.ts +348 -0
  24. package/components/QiuBadge/views/BadgeLibraryView.vue +32 -0
  25. package/components/QiuBadge/views/BadgeManageView.vue +1364 -0
  26. package/components/QiuBadge/views/BadgeTemplateEditorView.vue +10 -0
  27. package/components/QiuBadge/views/BadgeTemplateListView.vue +229 -0
  28. package/components/QiuBadge/views/DeskLabelTemplateListView.vue +227 -0
  29. package/components/QiuBadge/views/DeskLabelView.vue +683 -0
  30. package/components/QiuBadgeDot/index.vue +87 -0
  31. package/components/QiuButton/index.vue +247 -0
  32. package/components/QiuButtonGroup/index.vue +63 -0
  33. package/components/QiuCanvasKit/QiuCanvasEditor.vue +5503 -0
  34. package/components/QiuCanvasKit/QiuCanvasRenderer.vue +348 -0
  35. package/components/QiuCanvasKit/assetUrl.ts +11 -0
  36. package/components/QiuCanvasKit/components/DataModulesPanel.vue +352 -0
  37. package/components/QiuCanvasKit/export/html.ts +132 -0
  38. package/components/QiuCanvasKit/export/pdf.ts +48 -0
  39. package/components/QiuCanvasKit/export/png.ts +50 -0
  40. package/components/QiuCanvasKit/index.ts +20 -0
  41. package/components/QiuCanvasKit/modules/badgeCompat.ts +84 -0
  42. package/components/QiuCanvasKit/modules/dataRegistry.ts +207 -0
  43. package/components/QiuCanvasKit/modules/merge.ts +71 -0
  44. package/components/QiuCanvasKit/presets/badge.ts +67 -0
  45. package/components/QiuCanvasKit/presets/banner.ts +79 -0
  46. package/components/QiuCanvasKit/types.ts +138 -0
  47. package/components/QiuCard/index.vue +104 -0
  48. package/components/QiuCarousel/context.ts +16 -0
  49. package/components/QiuCarousel/index.vue +343 -0
  50. package/components/QiuCarouselItem/index.vue +47 -0
  51. package/components/QiuCascader/index.vue +879 -0
  52. package/components/QiuCascader/types.ts +48 -0
  53. package/components/QiuCheckbox/QiuCheckboxGroup.vue +78 -0
  54. package/components/QiuCheckbox/index.vue +231 -0
  55. package/components/QiuCheckbox/types.ts +8 -0
  56. package/components/QiuCode/index.vue +64 -0
  57. package/components/QiuCol/col.scss +55 -0
  58. package/components/QiuCol/index.vue +64 -0
  59. package/components/QiuCollapse/context.ts +10 -0
  60. package/components/QiuCollapse/index.vue +70 -0
  61. package/components/QiuCollapseItem/index.vue +167 -0
  62. package/components/QiuColorField/index.vue +296 -0
  63. package/components/QiuColorPicker/README.md +118 -0
  64. package/components/QiuColorPicker/index.vue +60 -0
  65. package/components/QiuColorfulCard/index.vue +101 -0
  66. package/components/QiuCombobox/index.vue +277 -0
  67. package/components/QiuConfigProvider/index.vue +31 -0
  68. package/components/QiuContextMenu/index.vue +209 -0
  69. package/components/QiuDate/index.vue +1134 -0
  70. package/components/QiuDescriptions/context.ts +20 -0
  71. package/components/QiuDescriptions/index.vue +115 -0
  72. package/components/QiuDescriptionsItem/index.vue +103 -0
  73. package/components/QiuDialog/index.vue +828 -0
  74. package/components/QiuDigitalCard/index.vue +159 -0
  75. package/components/QiuDivider/index.vue +185 -0
  76. package/components/QiuDot/index.vue +83 -0
  77. package/components/QiuDrawer/index.vue +350 -0
  78. package/components/QiuDropdown/QiuDropdownItem.vue +74 -0
  79. package/components/QiuDropdown/QiuDropdownMenu.vue +22 -0
  80. package/components/QiuDropdown/context.ts +9 -0
  81. package/components/QiuDropdown/index.vue +189 -0
  82. package/components/QiuEmpty/index.vue +73 -0
  83. package/components/QiuForm/index.vue +172 -0
  84. package/components/QiuForm/types.ts +39 -0
  85. package/components/QiuFormItem/index.vue +276 -0
  86. package/components/QiuFullscreen/index.vue +11 -0
  87. package/components/QiuGrid/index.vue +408 -0
  88. package/components/QiuHoverCard/index.vue +192 -0
  89. package/components/QiuIcon/index.vue +117 -0
  90. package/components/QiuIconPicker/iconify.libraries.ts +182 -0
  91. package/components/QiuIconPicker/icons.catalog.ts +3072 -0
  92. package/components/QiuIconPicker/index.vue +488 -0
  93. package/components/QiuImage/index.vue +177 -0
  94. package/components/QiuInput/index.vue +328 -0
  95. package/components/QiuInputNumber/index.vue +295 -0
  96. package/components/QiuInputOtp/index.vue +190 -0
  97. package/components/QiuKbd/index.vue +57 -0
  98. package/components/QiuLayoutContainer/index.vue +50 -0
  99. package/components/QiuLinkPreview/index.vue +224 -0
  100. package/components/QiuLoading/index.ts +176 -0
  101. package/components/QiuLoading/spinner.vue +820 -0
  102. package/components/QiuLoading/types.ts +44 -0
  103. package/components/QiuMention/index.vue +299 -0
  104. package/components/QiuMenu/QiuMenuRoot.vue +241 -0
  105. package/components/QiuMenu/components/QiuMenuItem.vue +340 -0
  106. package/components/QiuMenu/components/QiuSubMenu.vue +504 -0
  107. package/components/QiuMenu/context.ts +21 -0
  108. package/components/QiuMenu/index.vue +43 -0
  109. package/components/QiuMenuNav/QiuMenuNavItem.vue +100 -0
  110. package/components/QiuMenuNav/QiuMenuNavSub.vue +120 -0
  111. package/components/QiuMenuNav/context.ts +14 -0
  112. package/components/QiuMenuNav/index.vue +148 -0
  113. package/components/QiuMsg/QiuMsgItem.vue +180 -0
  114. package/components/QiuMsg/index.ts +158 -0
  115. package/components/QiuMsg/types.ts +23 -0
  116. package/components/QiuMsgBox/index.ts +147 -0
  117. package/components/QiuNotification/QiuNotificationItem.vue +168 -0
  118. package/components/QiuNotification/index.ts +155 -0
  119. package/components/QiuNotification/types.ts +14 -0
  120. package/components/QiuPage/index.vue +12 -0
  121. package/components/QiuPageHeader/index.vue +115 -0
  122. package/components/QiuPageMain/index.vue +78 -0
  123. package/components/QiuPagination/index.vue +421 -0
  124. package/components/QiuPasswordStrength/index.vue +101 -0
  125. package/components/QiuPopconfirm/index.vue +196 -0
  126. package/components/QiuPopover/index.vue +277 -0
  127. package/components/QiuProgress/index.vue +235 -0
  128. package/components/QiuQrcode/index.vue +302 -0
  129. package/components/QiuQueryForm/components/QiuQueryFormBottomPanel.vue +13 -0
  130. package/components/QiuQueryForm/components/QiuQueryFormLeftPanel.vue +37 -0
  131. package/components/QiuQueryForm/components/QiuQueryFormRightPanel.vue +37 -0
  132. package/components/QiuQueryForm/components/QiuQueryFormTopPanel.vue +13 -0
  133. package/components/QiuQueryForm/index.vue +117 -0
  134. package/components/QiuRadio/QiuRadioGroup.vue +75 -0
  135. package/components/QiuRadio/index.vue +191 -0
  136. package/components/QiuRadio/types.ts +9 -0
  137. package/components/QiuRate/index.vue +184 -0
  138. package/components/QiuRefresh/index.vue +32 -0
  139. package/components/QiuResult/index.vue +103 -0
  140. package/components/QiuRow/index.vue +60 -0
  141. package/components/QiuRow/types.ts +5 -0
  142. package/components/QiuScrollbar/index.vue +107 -0
  143. package/components/QiuSearchBar/index.vue +154 -0
  144. package/components/QiuSegmented/index.vue +120 -0
  145. package/components/QiuSelect/QiuOption.vue +93 -0
  146. package/components/QiuSelect/index.vue +452 -0
  147. package/components/QiuSelect/types.ts +15 -0
  148. package/components/QiuSkeleton/index.vue +78 -0
  149. package/components/QiuSlider/index.vue +266 -0
  150. package/components/QiuSpace/index.vue +95 -0
  151. package/components/QiuSparkline/index.vue +92 -0
  152. package/components/QiuStatistic/index.vue +4 -0
  153. package/components/QiuStatistics/index.vue +123 -0
  154. package/components/QiuStep/index.vue +220 -0
  155. package/components/QiuSteps/context.ts +25 -0
  156. package/components/QiuSteps/index.vue +84 -0
  157. package/components/QiuSwitch/index.vue +145 -0
  158. package/components/QiuTabBar/index.vue +218 -0
  159. package/components/QiuTabBar/types.ts +19 -0
  160. package/components/QiuTabPane/index.vue +82 -0
  161. package/components/QiuTable/columns.ts +16 -0
  162. package/components/QiuTable/export.ts +102 -0
  163. package/components/QiuTable/index.ts +24 -0
  164. package/components/QiuTable/index.vue +3503 -0
  165. package/components/QiuTable/registry.ts +75 -0
  166. package/components/QiuTable/styles/table.scss +1230 -0
  167. package/components/QiuTable/types.ts +413 -0
  168. package/components/QiuTable/useArea.ts +202 -0
  169. package/components/QiuTable/useColumns.ts +325 -0
  170. package/components/QiuTable/useEdit.ts +136 -0
  171. package/components/QiuTable/useExtras.ts +115 -0
  172. package/components/QiuTable/useHistory.ts +77 -0
  173. package/components/QiuTable/useMerge.ts +105 -0
  174. package/components/QiuTable/useResize.ts +86 -0
  175. package/components/QiuTable/useScrollSync.ts +152 -0
  176. package/components/QiuTable/useVirtual.ts +97 -0
  177. package/components/QiuTable/utils.ts +165 -0
  178. package/components/QiuTableColumn/index.vue +101 -0
  179. package/components/QiuTag/index.vue +143 -0
  180. package/components/QiuText/index.vue +99 -0
  181. package/components/QiuTime/index.vue +548 -0
  182. package/components/QiuTimeAgo/index.vue +103 -0
  183. package/components/QiuTimeline/index.vue +54 -0
  184. package/components/QiuTimelineItem/index.vue +140 -0
  185. package/components/QiuTips/index.vue +323 -0
  186. package/components/QiuTooltip/index.vue +237 -0
  187. package/components/QiuTransfer/index.vue +277 -0
  188. package/components/QiuTree/QiuTreeNode.vue +146 -0
  189. package/components/QiuTree/index.vue +143 -0
  190. package/components/QiuTree/types.ts +30 -0
  191. package/components/QiuTreeSelect/index.vue +259 -0
  192. package/components/QiuTrend/index.vue +92 -0
  193. package/components/QiuUpload/index.vue +628 -0
  194. package/components/QiuUploadCrop/index.vue +1267 -0
  195. package/components/QiuWatermark/index.vue +121 -0
  196. package/components/useBodyScrollLock.ts +28 -0
  197. package/dist/components/QiuAnchor/context.d.ts +15 -0
  198. package/dist/components/QiuAnchorLink/index.vue.d.ts +23 -0
  199. package/dist/components/QiuBadge/api/index.d.ts +110 -0
  200. package/dist/components/QiuBadge/components/BadgeImagePickerDialog.vue.d.ts +16 -0
  201. package/dist/components/QiuBadge/components/BadgeLibraryManagerDialog.vue.d.ts +9 -0
  202. package/dist/components/QiuBadge/components/BadgeLibraryPanel.vue.d.ts +5 -0
  203. package/dist/components/QiuBadge/composables/useTableHostHeight.d.ts +7 -0
  204. package/dist/components/QiuBadge/index.d.ts +18 -0
  205. package/dist/components/QiuBadge/shared/BadgeTemplateRenderer.vue.d.ts +14 -0
  206. package/dist/components/QiuBadge/shared/assetUrl.d.ts +1 -0
  207. package/dist/components/QiuBadge/shared/badgeTemplate.d.ts +132 -0
  208. package/dist/components/QiuBadge/utils/chinaAreaData.d.ts +9 -0
  209. package/dist/components/QiuBadge/utils/hostRegion.d.ts +3 -0
  210. package/dist/components/QiuBadge/utils/pca-code.json.d.ts +3 -0
  211. package/dist/components/QiuBadge/utils/pdfFromElement.d.ts +14 -0
  212. package/dist/components/QiuBadge/views/BadgeLibraryView.vue.d.ts +2 -0
  213. package/dist/components/QiuBadge/views/BadgeManageView.vue.d.ts +7 -0
  214. package/dist/components/QiuBadge/views/BadgeTemplateListView.vue.d.ts +4 -0
  215. package/dist/components/QiuBadge/views/DeskLabelTemplateListView.vue.d.ts +4 -0
  216. package/dist/components/QiuBadge/views/DeskLabelView.vue.d.ts +4 -0
  217. package/dist/components/QiuButton/index.vue.d.ts +52 -0
  218. package/dist/components/QiuCanvasKit/QiuCanvasEditor.vue.d.ts +24 -0
  219. package/dist/components/QiuCanvasKit/QiuCanvasRenderer.vue.d.ts +13 -0
  220. package/dist/components/QiuCanvasKit/assetUrl.d.ts +7 -0
  221. package/dist/components/QiuCanvasKit/components/DataModulesPanel.vue.d.ts +23 -0
  222. package/dist/components/QiuCanvasKit/export/html.d.ts +12 -0
  223. package/dist/components/QiuCanvasKit/export/pdf.d.ts +9 -0
  224. package/dist/components/QiuCanvasKit/export/png.d.ts +8 -0
  225. package/dist/components/QiuCanvasKit/index.d.ts +19 -0
  226. package/dist/components/QiuCanvasKit/modules/badgeCompat.d.ts +8 -0
  227. package/dist/components/QiuCanvasKit/modules/dataRegistry.d.ts +23 -0
  228. package/dist/components/QiuCanvasKit/modules/merge.d.ts +12 -0
  229. package/dist/components/QiuCanvasKit/presets/badge.d.ts +12 -0
  230. package/dist/components/QiuCanvasKit/presets/banner.d.ts +4 -0
  231. package/dist/components/QiuCanvasKit/types.d.ts +119 -0
  232. package/dist/components/QiuCarousel/context.d.ts +13 -0
  233. package/dist/components/QiuCarouselItem/index.vue.d.ts +20 -0
  234. package/dist/components/QiuCascader/types.d.ts +25 -0
  235. package/dist/components/QiuCheckbox/QiuCheckboxGroup.vue.d.ts +33 -0
  236. package/dist/components/QiuCheckbox/index.vue.d.ts +38 -0
  237. package/dist/components/QiuCheckbox/types.d.ts +7 -0
  238. package/dist/components/QiuCode/index.vue.d.ts +15 -0
  239. package/dist/components/QiuCollapse/context.d.ts +8 -0
  240. package/dist/components/QiuCollapseItem/index.vue.d.ts +30 -0
  241. package/dist/components/QiuColorField/index.vue.d.ts +23 -0
  242. package/dist/components/QiuColorPicker/index.vue.d.ts +21 -0
  243. package/dist/components/QiuCombobox/index.vue.d.ts +54 -0
  244. package/dist/components/QiuContextMenu/index.vue.d.ts +46 -0
  245. package/dist/components/QiuDescriptions/context.d.ts +16 -0
  246. package/dist/components/QiuDescriptionsItem/index.vue.d.ts +25 -0
  247. package/dist/components/QiuDigitalCard/index.vue.d.ts +45 -0
  248. package/dist/components/QiuDropdown/QiuDropdownItem.vue.d.ts +30 -0
  249. package/dist/components/QiuDropdown/QiuDropdownMenu.vue.d.ts +17 -0
  250. package/dist/components/QiuDropdown/context.d.ts +7 -0
  251. package/dist/components/QiuDropdown/index.vue.d.ts +46 -0
  252. package/dist/components/QiuForm/types.d.ts +31 -0
  253. package/dist/components/QiuHoverCard/index.vue.d.ts +44 -0
  254. package/dist/components/QiuIcon/index.vue.d.ts +18 -0
  255. package/dist/components/QiuIconPicker/iconify.libraries.d.ts +18 -0
  256. package/dist/components/QiuIconPicker/icons.catalog.d.ts +12 -0
  257. package/dist/components/QiuInput/index.vue.d.ts +71 -0
  258. package/dist/components/QiuInputNumber/index.vue.d.ts +38 -0
  259. package/dist/components/QiuInputOtp/index.vue.d.ts +28 -0
  260. package/dist/components/QiuKbd/index.vue.d.ts +22 -0
  261. package/dist/components/QiuLayoutContainer/index.vue.d.ts +29 -0
  262. package/dist/components/QiuLinkPreview/index.vue.d.ts +43 -0
  263. package/dist/components/QiuLoading/index.d.ts +26 -0
  264. package/dist/components/QiuLoading/spinner.vue.d.ts +14 -0
  265. package/dist/components/QiuLoading/types.d.ts +2 -0
  266. package/dist/components/QiuMention/index.vue.d.ts +35 -0
  267. package/dist/components/QiuMenu/QiuMenuRoot.vue.d.ts +36 -0
  268. package/dist/components/QiuMenu/context.d.ts +19 -0
  269. package/dist/components/QiuMenuNav/QiuMenuNavItem.vue.d.ts +26 -0
  270. package/dist/components/QiuMenuNav/QiuMenuNavSub.vue.d.ts +25 -0
  271. package/dist/components/QiuMenuNav/context.d.ts +12 -0
  272. package/dist/components/QiuMenuNav/index.vue.d.ts +35 -0
  273. package/dist/components/QiuMsg/QiuMsgItem.vue.d.ts +15 -0
  274. package/dist/components/QiuMsg/index.d.ts +30 -0
  275. package/dist/components/QiuMsg/types.d.ts +12 -0
  276. package/dist/components/QiuMsgBox/index.d.ts +14 -0
  277. package/dist/components/QiuNotification/QiuNotificationItem.vue.d.ts +16 -0
  278. package/dist/components/QiuNotification/index.d.ts +30 -0
  279. package/dist/components/QiuNotification/types.d.ts +12 -0
  280. package/dist/components/QiuPageMain/index.vue.d.ts +27 -0
  281. package/dist/components/QiuPagination/index.vue.d.ts +44 -0
  282. package/dist/components/QiuPasswordStrength/index.vue.d.ts +9 -0
  283. package/dist/components/QiuQrcode/index.vue.d.ts +20 -0
  284. package/dist/components/QiuRadio/QiuRadioGroup.vue.d.ts +33 -0
  285. package/dist/components/QiuRadio/types.d.ts +8 -0
  286. package/dist/components/QiuRow/types.d.ts +4 -0
  287. package/dist/components/QiuSearchBar/index.vue.d.ts +38 -0
  288. package/dist/components/QiuSelect/QiuOption.vue.d.ts +24 -0
  289. package/dist/components/QiuSelect/index.vue.d.ts +52 -0
  290. package/dist/components/QiuSelect/types.d.ts +13 -0
  291. package/dist/components/QiuSparkline/index.vue.d.ts +18 -0
  292. package/dist/components/QiuStep/index.vue.d.ts +31 -0
  293. package/dist/components/QiuSteps/context.d.ts +21 -0
  294. package/dist/components/QiuSwitch/index.vue.d.ts +25 -0
  295. package/dist/components/QiuTabBar/index.vue.d.ts +32 -0
  296. package/dist/components/QiuTabBar/types.d.ts +16 -0
  297. package/dist/components/QiuTabPane/index.vue.d.ts +25 -0
  298. package/dist/components/QiuTable/columns.d.ts +1 -0
  299. package/dist/components/QiuTable/export.d.ts +18 -0
  300. package/dist/components/QiuTable/index.d.ts +3 -0
  301. package/dist/components/QiuTable/index.vue.d.ts +322 -0
  302. package/dist/components/QiuTable/registry.d.ts +39 -0
  303. package/dist/components/QiuTable/types.d.ts +353 -0
  304. package/dist/components/QiuTable/useArea.d.ts +51 -0
  305. package/dist/components/QiuTable/useColumns.d.ts +67 -0
  306. package/dist/components/QiuTable/useEdit.d.ts +36 -0
  307. package/dist/components/QiuTable/useExtras.d.ts +38 -0
  308. package/dist/components/QiuTable/useHistory.d.ts +44 -0
  309. package/dist/components/QiuTable/useMerge.d.ts +28 -0
  310. package/dist/components/QiuTable/useResize.d.ts +19 -0
  311. package/dist/components/QiuTable/useScrollSync.d.ts +25 -0
  312. package/dist/components/QiuTable/useVirtual.d.ts +30 -0
  313. package/dist/components/QiuTable/utils.d.ts +54 -0
  314. package/dist/components/QiuTime/index.vue.d.ts +43 -0
  315. package/dist/components/QiuTimeAgo/index.vue.d.ts +35 -0
  316. package/dist/components/QiuTimelineItem/index.vue.d.ts +35 -0
  317. package/dist/components/QiuTooltip/index.vue.d.ts +38 -0
  318. package/dist/components/QiuTree/types.d.ts +13 -0
  319. package/dist/components/QiuTrend/index.vue.d.ts +34 -0
  320. package/dist/components/QiuUpload/index.vue.d.ts +86 -0
  321. package/dist/components/QiuUploadCrop/index.vue.d.ts +136 -0
  322. package/dist/components/useBodyScrollLock.d.ts +4 -0
  323. package/dist/foucui.css +1 -0
  324. package/dist/foucui.js +26509 -0
  325. package/dist/foucui.umd.cjs +125 -0
  326. package/dist/index.d.ts +46 -0
  327. package/dist/src/bridge/@/api/media.d.ts +7 -0
  328. package/dist/src/bridge/@/api/notice.d.ts +5 -0
  329. package/dist/src/bridge/@/config/index.d.ts +29 -0
  330. package/dist/src/bridge/@/config/media.config.d.ts +15 -0
  331. package/dist/src/bridge/@/config/security.config.d.ts +7 -0
  332. package/dist/src/bridge/@/i18n/index.d.ts +7 -0
  333. package/dist/src/bridge/@/store/modules/bing.d.ts +3 -0
  334. package/dist/src/bridge/@/store/modules/errorLog.d.ts +3 -0
  335. package/dist/src/bridge/@/store/modules/media.d.ts +4 -0
  336. package/dist/src/bridge/@/store/modules/routes.d.ts +3 -0
  337. package/dist/src/bridge/@/store/modules/settings.d.ts +3 -0
  338. package/dist/src/bridge/@/store/modules/tabs.d.ts +3 -0
  339. package/dist/src/bridge/@/store/modules/user.d.ts +3 -0
  340. package/dist/src/bridge/@/utils/convertToCamelCase.d.ts +2 -0
  341. package/dist/src/bridge/@/utils/index.d.ts +5 -0
  342. package/dist/src/bridge/@/utils/pageTitle.d.ts +1 -0
  343. package/dist/src/bridge/@/utils/routes.d.ts +11 -0
  344. package/dist/src/bridge/@/utils/validate.d.ts +13 -0
  345. package/dist/src/bridge/@qiu/plugins/shortcuts.d.ts +2 -0
  346. package/dist/src/i18n.d.ts +2 -0
  347. package/package.json +83 -0
  348. package/plugins/qiu.ts +83 -0
  349. package/plugins/shortcuts.ts +69 -0
  350. package/src/bridge/@/api/media.ts +8 -0
  351. package/src/bridge/@/api/notice.ts +5 -0
  352. package/src/bridge/@/config/index.ts +38 -0
  353. package/src/bridge/@/config/media.config.ts +37 -0
  354. package/src/bridge/@/config/security.config.ts +8 -0
  355. package/src/bridge/@/i18n/index.ts +6 -0
  356. package/src/bridge/@/store/modules/bing.ts +9 -0
  357. package/src/bridge/@/store/modules/errorLog.ts +9 -0
  358. package/src/bridge/@/store/modules/media.ts +10 -0
  359. package/src/bridge/@/store/modules/routes.ts +10 -0
  360. package/src/bridge/@/store/modules/settings.ts +9 -0
  361. package/src/bridge/@/store/modules/tabs.ts +9 -0
  362. package/src/bridge/@/store/modules/user.ts +9 -0
  363. package/src/bridge/@/utils/convertToCamelCase.ts +11 -0
  364. package/src/bridge/@/utils/index.ts +9 -0
  365. package/src/bridge/@/utils/pageTitle.ts +4 -0
  366. package/src/bridge/@/utils/routes.ts +14 -0
  367. package/src/bridge/@/utils/validate.ts +17 -0
  368. package/src/bridge/@qiu/plugins/shortcuts.ts +3 -0
  369. package/src/i18n.ts +8 -0
  370. package/src/icons/qiu-logo.svg +1 -0
  371. package/styles/dark.scss +194 -0
  372. package/styles/normalize.css +349 -0
  373. package/styles/plain.scss +423 -0
  374. package/styles/qiu-tokens.scss +40 -0
  375. package/styles/qiu.scss +1149 -0
  376. package/styles/technology.scss +452 -0
  377. package/styles/var.scss +382 -0
  378. package/styles/variables.scss +77 -0
@@ -0,0 +1,1364 @@
1
+ <template>
2
+ <div class="page-container badge-manage">
3
+ <qiu-card shadow="never" class="tpl-entry-card tpl-fill-card">
4
+ <template #header>
5
+ <div class="card-head">
6
+ <span class="toolbar-title">证件模板</span>
7
+ <span class="card-head-desc">按比赛设计版面;保存后学校端按模板套打。</span>
8
+ </div>
9
+ </template>
10
+ <div class="tpl-list-layout">
11
+ <aside class="tpl-region-aside">
12
+ <div class="tree-title">赛区分类</div>
13
+ <qiu-input v-model="treeFilterText" class="tree-filter" clearable size="small" placeholder="筛选赛区" />
14
+ <qiu-scrollbar class="tpl-region-scroll">
15
+ <qiu-tree
16
+ ref="treeRef"
17
+ :data="treeData"
18
+ highlight-current
19
+ :default-expanded-keys="['all']"
20
+ :props="{ label: 'label', children: 'children', value: 'id' }"
21
+ :filter-node-method="filterTreeNode"
22
+ @node-click="onRegionNodeClick"
23
+ />
24
+ </qiu-scrollbar>
25
+ </aside>
26
+ <section class="tpl-list-main">
27
+ <p class="hint tpl-hint tpl-hint-shrink">
28
+ 在列表中选择比赛进入<strong>管理模板</strong>,或与下方「证件生成任务」配合:先选比赛制作模板,再用任务生成预览。
29
+ <qiu-button link type="primary" @click="goBadgeLibrary">证件图片库</qiu-button>
30
+ </p>
31
+ <div class="tpl-filter-bar">
32
+ <qiu-input v-model="tplKeyword" clearable placeholder="关键字:比赛名称/赛区码/场地" style="width: 240px" @keyup.enter="searchTemplates" />
33
+ <qiu-date
34
+ v-model="tplDateRange"
35
+ type="daterange"
36
+ value-format="YYYY-MM-DD"
37
+ start-placeholder="开始日期"
38
+ end-placeholder="结束日期"
39
+ range-separator="至"
40
+ style="width: 260px"
41
+ />
42
+ <qiu-button type="primary" @click="searchTemplates">查询</qiu-button>
43
+ <qiu-button @click="resetTemplateFilters">重置</qiu-button>
44
+ <qiu-tag v-if="regionTagText" type="info" closable @close="clearRegionFilter">{{ regionTagText }}</qiu-tag>
45
+ </div>
46
+ <div ref="tplHostRef" class="table-host">
47
+ <qiu-table
48
+ v-qiu-loading="tplTableLoading"
49
+ :data="competitions"
50
+ stripe
51
+ :height="tplTableHeight"
52
+ empty-text="暂无比赛数据"
53
+ size="small"
54
+ class="tpl-table"
55
+ >
56
+ <qiu-table-column type="index" label="#" width="52" align="center" :index="templateRowIndex" />
57
+ <qiu-table-column prop="name" label="比赛名称" min-width="200" show-overflow-tooltip />
58
+ <qiu-table-column prop="area_code" label="赛区码" width="82" />
59
+ <qiu-table-column label="举办地" min-width="150" show-overflow-tooltip>
60
+ <template #default="{ row }">{{ formatHostRegionLabel(row.host_region, codeToText as Record<string, string>) || '—' }}</template>
61
+ </qiu-table-column>
62
+ <qiu-table-column prop="start_date" label="开始日期" width="118" />
63
+ <qiu-table-column label="状态" width="96">
64
+ <template #default="{ row }">
65
+ <qiu-tag size="small" :type="(['info', 'success', 'primary', 'danger'] as const)[Number(row.status)] ?? 'info'">
66
+ {{ ['草稿', '报名中', '进行中', '已结束'][Number(row.status)] ?? '—' }}
67
+ </qiu-tag>
68
+ </template>
69
+ </qiu-table-column>
70
+ <qiu-table-column label="模板" width="220" fixed="right">
71
+ <template #default="{ row }">
72
+ <qiu-button link type="primary" @click="goEditBadgeTemplate(row.id)">管理模板</qiu-button>
73
+ <qiu-button link type="primary" @click="goCompetitionDetail(row.id)">比赛详情</qiu-button>
74
+ </template>
75
+ </qiu-table-column>
76
+ </qiu-table>
77
+ </div>
78
+ <div class="pagination-wrapper tpl-pagination">
79
+ <qiu-pagination
80
+ v-model:current-page="tplPagination.page"
81
+ v-model:page-size="tplPagination.pageSize"
82
+ :total="tplPagination.total"
83
+ :page-sizes="[10, 20, 50]"
84
+ layout="total, sizes, prev, pager, next"
85
+ @change="loadCompetitions"
86
+ />
87
+ </div>
88
+ </section>
89
+ </div>
90
+ </qiu-card>
91
+
92
+ <qiu-card shadow="never" class="task-fill-card">
93
+ <div class="toolbar">
94
+ <span class="toolbar-title">证件生成任务</span>
95
+ <qiu-button type="primary" @click="openDialog">添加任务</qiu-button>
96
+ </div>
97
+ <p class="hint task-hint-shrink">任务保存在本浏览器;点击「生成预览」将先选择 A4/A3 纸张,再打开整版预览并可下载 PDF(无座位号的学生会被跳过或禁止)。</p>
98
+ <div ref="taskHostRef" class="table-host">
99
+ <qiu-table :data="tasks" stripe empty-text="暂无任务,请点击添加" :height="taskTableHeight">
100
+ <qiu-table-column prop="title" label="任务名称" min-width="140" />
101
+ <qiu-table-column prop="competitionName" label="比赛" min-width="160" show-overflow-tooltip />
102
+ <qiu-table-column label="模板" min-width="140" show-overflow-tooltip>
103
+ <template #default="{ row }">{{ row.templateName || '默认模板' }}</template>
104
+ </qiu-table-column>
105
+ <qiu-table-column label="学校范围" width="140">
106
+ <template #default="{ row }">
107
+ <span v-if="row.scopeAllSchools">全部学校</span>
108
+ <span v-else>{{ row.schoolIds.length }} 所学校</span>
109
+ </template>
110
+ </qiu-table-column>
111
+ <qiu-table-column label="学生范围" width="120">
112
+ <template #default="{ row }">
113
+ <span v-if="!row.studentIds?.length">范围内全部(无座位号跳过)</span>
114
+ <span v-else>指定 {{ row.studentIds.length }} 人</span>
115
+ </template>
116
+ </qiu-table-column>
117
+ <qiu-table-column label="生成方式" width="120" align="center">
118
+ <template #default="{ row }">
119
+ <span>{{ row.generateMode === 'school' ? '按学校' : '按顺序' }}</span>
120
+ </template>
121
+ </qiu-table-column>
122
+ <qiu-table-column label="创建时间" width="175">
123
+ <template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
124
+ </qiu-table-column>
125
+ <qiu-table-column label="操作" width="360" fixed="right">
126
+ <template #default="{ row }">
127
+ <qiu-button link type="primary" @click="goEditBadgeTemplate(row.competitionId)">编辑模板</qiu-button>
128
+ <qiu-button link type="success" @click="generateTaskNow(row)">立即生成证件</qiu-button>
129
+ <qiu-button link type="primary" @click="openPaperForPreview(row)">生成预览</qiu-button>
130
+ <qiu-button link type="danger" @click="removeTask(row.id)">删除</qiu-button>
131
+ </template>
132
+ </qiu-table-column>
133
+ </qiu-table>
134
+ </div>
135
+ </qiu-card>
136
+
137
+ <qiu-card v-if="previewBadges.length || previewSkipped.length" class="preview-card" shadow="never">
138
+ <template #header>
139
+ <div class="preview-head">
140
+ <span>预览{{ previewTaskTitle ? ` — ${previewTaskTitle}` : '' }}</span>
141
+ <div class="preview-head-actions">
142
+ <qiu-button
143
+ v-if="pendingPreviewTask && (previewBadges.length || previewSkipped.length)"
144
+ link
145
+ type="primary"
146
+ @click="reopenPdfPreview"
147
+ >
148
+ 返回整版预览
149
+ </qiu-button>
150
+ <qiu-button link type="danger" @click="clearInlinePreview">关闭预览</qiu-button>
151
+ </div>
152
+ </div>
153
+ </template>
154
+ <qiu-alert
155
+ v-if="previewSkipped.length"
156
+ type="warning"
157
+ show-icon
158
+ :closable="false"
159
+ :title="`已跳过 ${previewSkipped.length} 名学生(未编排座位号,无法制作证件)`"
160
+ style="margin-bottom: 12px"
161
+ />
162
+ <div v-if="previewBadges.length" class="badge-grid">
163
+ <div class="badge-card" v-for="b in previewBadges" :key="`${b.competition_no}-${b.seat_no}`">
164
+ <div class="badge-header">智慧奇乐</div>
165
+ <div class="badge-name">{{ b.student_name }}</div>
166
+ <div class="badge-school">{{ b.school_name }}</div>
167
+ <div class="badge-no">参赛编号:{{ b.competition_no }}</div>
168
+ <div class="badge-room">{{ b.competition_room }} {{ b.seat_no }}号</div>
169
+ </div>
170
+ </div>
171
+ <qiu-empty v-else-if="!previewSkipped.length" description="无符合条件的证件数据" />
172
+ </qiu-card>
173
+
174
+ <qiu-dialog
175
+ v-model="dialogVisible"
176
+ title="添加证件任务"
177
+ width="580px"
178
+ align-center
179
+ draggable
180
+ destroy-on-close
181
+ @closed="resetDialogForm"
182
+ >
183
+ <qiu-form label-width="108px">
184
+ <qiu-form-item label="任务名称">
185
+ <qiu-input v-model="dialogTitle" placeholder="可选,便于区分" clearable />
186
+ </qiu-form-item>
187
+ <qiu-form-item label="比赛" required>
188
+ <qiu-select
189
+ v-model="dialogCompetitionId"
190
+ placeholder="选择比赛"
191
+ style="width: 100%"
192
+ :options="taskCompetitionSelectOptions"
193
+ @change="onDialogCompetitionChange"
194
+ />
195
+ </qiu-form-item>
196
+ <qiu-form-item label="学校范围">
197
+ <qiu-radio-group v-model="dialogScopeAll">
198
+ <qiu-radio :label="true">全部学校</qiu-radio>
199
+ <qiu-radio :label="false">指定学校</qiu-radio>
200
+ </qiu-radio-group>
201
+ </qiu-form-item>
202
+ <qiu-form-item v-if="!dialogScopeAll" label="选择学校">
203
+ <qiu-input
204
+ v-model="schoolFilterText"
205
+ clearable
206
+ size="small"
207
+ placeholder="筛选学校"
208
+ :disabled="!dialogCompetitionId"
209
+ style="width: 100%; margin-bottom: 8px"
210
+ />
211
+ <div class="checkbox-scroll" :class="{ 'is-disabled': !dialogCompetitionId }">
212
+ <qiu-checkbox-group v-model="dialogSchoolIds" :disabled="!dialogCompetitionId">
213
+ <qiu-checkbox
214
+ v-for="s in filteredBadgeSchoolOptions"
215
+ :key="s.id"
216
+ :label="s.id"
217
+ >
218
+ {{ s.school_name }}({{ s.student_count }}人)
219
+ </qiu-checkbox>
220
+ </qiu-checkbox-group>
221
+ <div v-if="!filteredBadgeSchoolOptions.length" class="checkbox-empty">请先选择比赛</div>
222
+ </div>
223
+ </qiu-form-item>
224
+ <qiu-form-item v-if="dialogCompetitionId" label="模板">
225
+ <qiu-select
226
+ v-model="dialogTemplateId"
227
+ placeholder="请选择证件模板"
228
+ style="width: 100%"
229
+ :options="badgeTemplateSelectOptions"
230
+ />
231
+ <qiu-button link type="primary" @click="goEditBadgeTemplate(dialogCompetitionId)">编辑该比赛的证件模板</qiu-button>
232
+ </qiu-form-item>
233
+ <qiu-form-item label="指定学生">
234
+ <div class="inline-stack">
235
+ <qiu-switch v-model="dialogPickStudents" active-text="仅所选学生" inactive-text="范围内全部" />
236
+ </div>
237
+ <template v-if="dialogPickStudents">
238
+ <qiu-input
239
+ v-model="studentFilterText"
240
+ clearable
241
+ size="small"
242
+ placeholder="筛选学生"
243
+ :disabled="!dialogCompetitionId"
244
+ style="width: 100%; margin-top: 8px"
245
+ />
246
+ <div class="checkbox-scroll" :class="{ 'is-disabled': !dialogCompetitionId }" style="margin-top: 8px">
247
+ <qiu-checkbox-group v-model="dialogStudentIds" :disabled="!dialogCompetitionId">
248
+ <qiu-checkbox
249
+ v-for="opt in filteredStudentSelectOptions"
250
+ :key="opt.value"
251
+ :label="opt.value"
252
+ :disabled="opt.disabled"
253
+ >
254
+ {{ opt.label }}
255
+ </qiu-checkbox>
256
+ </qiu-checkbox-group>
257
+ </div>
258
+ </template>
259
+ </qiu-form-item>
260
+ <qiu-form-item label="生成方式">
261
+ <qiu-radio-group v-model="dialogGenerateMode">
262
+ <qiu-radio label="sequence">按顺序生成</qiu-radio>
263
+ <qiu-radio label="school">按学校生成</qiu-radio>
264
+ </qiu-radio-group>
265
+ <div class="dialog-tip">预览、打印和下载 PDF 都会按这个顺序输出。</div>
266
+ </qiu-form-item>
267
+ </qiu-form>
268
+ <template #footer>
269
+ <qiu-button @click="dialogVisible = false">取消</qiu-button>
270
+ <qiu-button type="primary" @click="saveTask">保存任务</qiu-button>
271
+ </template>
272
+ </qiu-dialog>
273
+
274
+ <BadgeLibraryManagerDialog v-model="badgeLibraryDialogOpen" />
275
+
276
+ <qiu-dialog v-model="paperPickVisible" title="预览排版设置" width="520px" destroy-on-close align-center>
277
+ <p class="paper-tip">生成整版证件预览前,请选择纸张模式。可按 A4/A3 排版,也可直接按模板实际尺寸生成。预览、下载 PDF、打印都会使用这套排版设置。</p>
278
+ <div class="paper-config">
279
+ <div class="paper-config-block">
280
+ <div class="paper-config-title">纸张尺寸</div>
281
+ <qiu-radio-group v-model="paperSize">
282
+ <qiu-radio label="a4">A4</qiu-radio>
283
+ <qiu-radio label="a3">A3</qiu-radio>
284
+ <qiu-radio label="custom">按模板尺寸</qiu-radio>
285
+ </qiu-radio-group>
286
+ </div>
287
+ <div class="paper-config-block">
288
+ <div class="paper-config-title">打印方向</div>
289
+ <qiu-radio-group v-model="paperOrientation" :disabled="isDirectSizeMode">
290
+ <qiu-radio label="portrait">纵向</qiu-radio>
291
+ <qiu-radio label="landscape">横向</qiu-radio>
292
+ </qiu-radio-group>
293
+ <div v-if="isDirectSizeMode" class="paper-config-desc">按模板尺寸生成时,页面宽高直接取模板设置,方向无需单独选择。</div>
294
+ </div>
295
+ <div class="paper-config-block">
296
+ <div class="paper-config-title">每行显示</div>
297
+ <div class="paper-inline-row">
298
+ <qiu-radio-group v-model="paperColumnsPreset" :disabled="isDirectSizeMode">
299
+ <qiu-radio :label="1">1个</qiu-radio>
300
+ <qiu-radio :label="2">2个</qiu-radio>
301
+ <qiu-radio :label="3">3个</qiu-radio>
302
+ <qiu-radio :label="0">自定义</qiu-radio>
303
+ </qiu-radio-group>
304
+ <qiu-input-number
305
+ v-model="paperColumns"
306
+ :min="1"
307
+ :max="12"
308
+ controls-position="right"
309
+ style="width: 120px"
310
+ :disabled="isDirectSizeMode"
311
+ />
312
+ </div>
313
+ <div class="paper-config-desc">
314
+ {{ isDirectSizeMode ? '按模板尺寸生成时固定为每页 1 个证件。' : '按当前模板尺寸自动计算每页行数;只控制每行显示几个证件。' }}
315
+ </div>
316
+ </div>
317
+ </div>
318
+ <template #footer>
319
+ <qiu-button @click="paperPickVisible = false">取消</qiu-button>
320
+ <qiu-button type="primary" @click="confirmPaperAndPreview">下一步:整版预览</qiu-button>
321
+ </template>
322
+ </qiu-dialog>
323
+
324
+ <qiu-dialog
325
+ v-model="pdfDialogVisible"
326
+ :title="`证件整版预览 — ${pdfPreviewTaskTitle}`"
327
+ width="96%"
328
+ top="2vh"
329
+ destroy-on-close
330
+ class="pdf-preview-dialog"
331
+ >
332
+ <qiu-alert
333
+ v-if="pdfSkipped.length"
334
+ type="warning"
335
+ show-icon
336
+ :closable="false"
337
+ :title="`已跳过 ${pdfSkipped.length} 名学生(未编排座位号)`"
338
+ style="margin-bottom: 12px"
339
+ />
340
+ <div v-qiu-loading="pdfLoading" class="pdf-dialog-body">
341
+ <div ref="pdfExportRoot" class="pdf-export-root" :style="pdfExportRootStyle">
342
+ <template v-if="pdfTemplateDoc?.canvas && pdfMergeList.length">
343
+ <div v-for="(page, pageIndex) in pdfPages" :key="pageIndex" class="pdf-page" :style="pdfPageStyle">
344
+ <div class="pdf-page-grid" :style="pdfPageGridStyleFor(page.length)">
345
+ <div v-for="(m, i) in page" :key="`${pageIndex}-${i}`" class="pdf-cell" :style="pdfCellStyle">
346
+ <BadgeTemplateRenderer :doc="pdfTemplateDoc" :merge-data="m" :screen-scale="1" />
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </template>
351
+ <template v-else-if="pdfMergeList.length">
352
+ <div v-for="(b, i) in previewBadges" :key="i" class="pdf-cell simple-badge">
353
+ <div class="sb-h">智慧奇乐</div>
354
+ <div class="sb-name">{{ b.student_name }}</div>
355
+ <div class="sb-school">{{ b.school_name }}</div>
356
+ <div class="sb-no">参赛编号:{{ b.competition_no }}</div>
357
+ <div class="sb-room">{{ b.competition_room }} {{ b.seat_no }}号</div>
358
+ </div>
359
+ </template>
360
+ </div>
361
+ </div>
362
+ <template #footer>
363
+ <qiu-button :disabled="!pdfMergeList.length" @click="onPrintBadgePdf">打印整版</qiu-button>
364
+ <qiu-button @click="pdfDialogVisible = false">关闭</qiu-button>
365
+ <qiu-button type="primary" :loading="pdfDownloading" :disabled="!pdfMergeList.length" @click="onDownloadBadgePdf">
366
+ 下载 PDF
367
+ </qiu-button>
368
+ </template>
369
+ </qiu-dialog>
370
+ </div>
371
+ </template>
372
+
373
+ <script setup lang="ts">
374
+ import { ref, computed, onMounted, nextTick, watch, onActivated } from 'vue'
375
+ import { useRouter } from 'vue-router'
376
+ import { qiuMsg } from '../../QiuMsg'
377
+ import { qiuAlert } from '../../QiuMsgBox'
378
+ import { regionData, codeToText } from '../utils/chinaAreaData'
379
+ import BadgeLibraryManagerDialog from '../components/BadgeLibraryManagerDialog.vue'
380
+ import BadgeTemplateRenderer from '../shared/BadgeTemplateRenderer.vue'
381
+ import { getCompetitions, getBadgeTemplate, getBadgeTemplateOne, listBadgeTemplates } from '../api/index'
382
+ import { getBadgeSchools, getBadges, getStudents, finalizeSeatAssignmentLock } from '../api/index'
383
+ import type { BadgeTemplateDoc } from '../shared/badgeTemplate'
384
+ import { flattenBadgeForMerge } from '../shared/badgeTemplate'
385
+ import {
386
+ downloadPdfPagesFromElements,
387
+ printPdfPagesFromElements,
388
+ type PaperKind,
389
+ type PaperOrientation,
390
+ } from '../utils/pdfFromElement'
391
+ import { useTableHostHeight } from '../composables/useTableHostHeight'
392
+ import { formatHostRegionLabel } from '../utils/hostRegion'
393
+
394
+ const { hostRef: tplHostRef, tableHeight: tplTableHeight, recalcTableHeight: recalcTplTable } = useTableHostHeight(140)
395
+ const { hostRef: taskHostRef, tableHeight: taskTableHeight, recalcTableHeight: recalcTaskTable } = useTableHostHeight(160)
396
+
397
+ const router = useRouter()
398
+ const badgeLibraryDialogOpen = ref(false)
399
+ const STORAGE_KEY = 'zhihui_admin_badge_tasks'
400
+ const tplTableLoading = ref(false)
401
+
402
+ interface RegionTreeNode {
403
+ id: string
404
+ label: string
405
+ code: string
406
+ children?: RegionTreeNode[]
407
+ }
408
+
409
+ interface BadgeTask {
410
+ id: string
411
+ title: string
412
+ competitionId: number
413
+ competitionName: string
414
+ templateId?: number
415
+ templateName?: string
416
+ scopeAllSchools: boolean
417
+ schoolIds: number[]
418
+ studentIds: number[] | null
419
+ generateMode: 'sequence' | 'school'
420
+ createdAt: string
421
+ }
422
+
423
+ function newId(): string {
424
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()
425
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
426
+ }
427
+
428
+ const competitions = ref<any[]>([])
429
+ const taskCompetitionOptions = ref<any[]>([])
430
+ const tasks = ref<BadgeTask[]>([])
431
+ const tplKeyword = ref('')
432
+ const tplDateRange = ref<[string, string] | []>([])
433
+ const tplPagination = ref({ page: 1, pageSize: 10, total: 0 })
434
+ const treeRef = ref<{ filter: (v: string) => void; setCurrentKey: (k: string) => void } | null>(null)
435
+ const treeFilterText = ref('')
436
+ const selectedRegionCode = ref('')
437
+
438
+ const dialogVisible = ref(false)
439
+ const dialogTitle = ref('')
440
+ const dialogCompetitionId = ref<number | undefined>(undefined)
441
+ const dialogTemplateId = ref<number | undefined>(undefined)
442
+ const dialogScopeAll = ref(true)
443
+ const dialogSchoolIds = ref<number[]>([])
444
+ const dialogPickStudents = ref(false)
445
+ const dialogStudentIds = ref<number[]>([])
446
+ const dialogGenerateMode = ref<'sequence' | 'school'>('sequence')
447
+ const badgeSchoolOptions = ref<{ id: number; school_name: string; student_count: number }[]>([])
448
+ const badgeTemplateOptions = ref<{ id: number; name: string; is_default?: boolean }[]>([])
449
+ const dialogStudentsRaw = ref<any[]>([])
450
+ const schoolFilterText = ref('')
451
+ const studentFilterText = ref('')
452
+
453
+ const previewBadges = ref<any[]>([])
454
+ const previewSkipped = ref<any[]>([])
455
+ const previewTaskTitle = ref('')
456
+
457
+ const paperPickVisible = ref(false)
458
+ const paperSize = ref<PaperKind>('a4')
459
+ const paperOrientation = ref<PaperOrientation>('portrait')
460
+ const paperColumnsPreset = ref<number>(2)
461
+ const paperColumns = ref<number>(2)
462
+ const pendingPreviewTask = ref<BadgeTask | null>(null)
463
+ const pdfDialogVisible = ref(false)
464
+ const pdfLoading = ref(false)
465
+ const pdfDownloading = ref(false)
466
+ const pdfExportRoot = ref<HTMLElement | null>(null)
467
+ const pdfTemplateDoc = ref<BadgeTemplateDoc | null>(null)
468
+ const pdfMergeList = ref<Record<string, string>[]>([])
469
+ const pdfSkipped = ref<any[]>([])
470
+ const pdfPreviewTaskTitle = ref('')
471
+
472
+ const PAPER_SIZE_MM: Record<'a4' | 'a3', { w: number; h: number }> = {
473
+ a4: { w: 210, h: 297 },
474
+ a3: { w: 297, h: 420 },
475
+ }
476
+ const PDF_PAGE_MARGIN_MM = 8
477
+ const isDirectSizeMode = computed(() => paperSize.value === 'custom')
478
+
479
+ const pdfPaperSize = computed(() => {
480
+ if (isDirectSizeMode.value) {
481
+ return {
482
+ w: Number(pdfTemplateDoc.value?.canvas?.widthMm ?? 90),
483
+ h: Number(pdfTemplateDoc.value?.canvas?.heightMm ?? 130),
484
+ }
485
+ }
486
+ const base = PAPER_SIZE_MM[paperSize.value === 'a3' ? 'a3' : 'a4']
487
+ return paperOrientation.value === 'landscape'
488
+ ? { w: base.h, h: base.w }
489
+ : base
490
+ })
491
+ const pdfBadgeSize = computed(() => ({
492
+ w: Number(pdfTemplateDoc.value?.canvas?.widthMm ?? 90),
493
+ h: Number(pdfTemplateDoc.value?.canvas?.heightMm ?? 130),
494
+ }))
495
+ const pdfGridMetrics = computed(() => {
496
+ const page = pdfPaperSize.value
497
+ const badge = pdfBadgeSize.value
498
+ if (isDirectSizeMode.value) {
499
+ return {
500
+ requestedCols: 1,
501
+ maxCols: 1,
502
+ cols: 1,
503
+ rows: 1,
504
+ perPage: 1,
505
+ blockW: badge.w,
506
+ blockH: badge.h,
507
+ }
508
+ }
509
+ const usableW = Math.max(1, page.w - PDF_PAGE_MARGIN_MM * 2)
510
+ const usableH = Math.max(1, page.h - PDF_PAGE_MARGIN_MM * 2)
511
+ const maxCols = Math.max(1, Math.floor(usableW / badge.w))
512
+ const requestedCols = Math.max(1, Number(paperColumns.value || 1))
513
+ const cols = Math.min(requestedCols, maxCols)
514
+ const rows = Math.max(1, Math.floor(usableH / badge.h))
515
+ const perPage = Math.max(1, cols * rows)
516
+ return {
517
+ requestedCols,
518
+ maxCols,
519
+ cols,
520
+ rows,
521
+ perPage,
522
+ blockW: cols * badge.w,
523
+ blockH: rows * badge.h,
524
+ }
525
+ })
526
+ const pdfPages = computed(() => {
527
+ const perPage = pdfGridMetrics.value.perPage
528
+ const out: Record<string, string>[][] = []
529
+ for (let i = 0; i < pdfMergeList.value.length; i += perPage) {
530
+ out.push(pdfMergeList.value.slice(i, i + perPage))
531
+ }
532
+ return out
533
+ })
534
+ const pdfExportRootStyle = computed(() => ({
535
+ width: `${pdfPaperSize.value.w}mm`,
536
+ }))
537
+ const pdfPageStyle = computed(() => ({
538
+ width: `${pdfPaperSize.value.w}mm`,
539
+ height: `${pdfPaperSize.value.h}mm`,
540
+ padding: `${isDirectSizeMode.value ? 0 : PDF_PAGE_MARGIN_MM}mm`,
541
+ }))
542
+ function pdfPageGridStyleFor(itemCount: number) {
543
+ const cols = Math.max(1, Math.min(pdfGridMetrics.value.cols, itemCount || 1))
544
+ const rows = Math.max(1, Math.ceil((itemCount || 1) / Math.max(1, pdfGridMetrics.value.cols)))
545
+ return {
546
+ width: `${cols * pdfBadgeSize.value.w}mm`,
547
+ minHeight: `${rows * pdfBadgeSize.value.h}mm`,
548
+ gridTemplateColumns: `repeat(${cols}, ${pdfBadgeSize.value.w}mm)`,
549
+ gridAutoRows: `${pdfBadgeSize.value.h}mm`,
550
+ }
551
+ }
552
+ const pdfCellStyle = computed(() => ({
553
+ width: `${pdfBadgeSize.value.w}mm`,
554
+ height: `${pdfBadgeSize.value.h}mm`,
555
+ }))
556
+
557
+ const studentSelectOptions = computed(() => {
558
+ return dialogStudentsRaw.value.map((s) => {
559
+ const seat = String(s.seat_no ?? '').trim()
560
+ const disabled = !seat
561
+ return {
562
+ value: s.id as number,
563
+ label: `${s.student_name}(座位:${seat || '无'})`,
564
+ disabled,
565
+ }
566
+ })
567
+ })
568
+
569
+ const taskCompetitionSelectOptions = computed(() =>
570
+ taskCompetitionOptions.value.map((c) => ({ label: c.name, value: c.id }))
571
+ )
572
+
573
+ const badgeTemplateSelectOptions = computed(() =>
574
+ badgeTemplateOptions.value.map((tpl) => ({
575
+ label: `${tpl.name || '未命名模板'}${tpl.is_default ? '(默认)' : ''}`,
576
+ value: tpl.id,
577
+ }))
578
+ )
579
+
580
+ const filteredBadgeSchoolOptions = computed(() => {
581
+ const kw = schoolFilterText.value.trim().toLowerCase()
582
+ if (!kw) return badgeSchoolOptions.value
583
+ return badgeSchoolOptions.value.filter((s) =>
584
+ `${s.school_name}`.toLowerCase().includes(kw)
585
+ )
586
+ })
587
+
588
+ const filteredStudentSelectOptions = computed(() => {
589
+ const kw = studentFilterText.value.trim().toLowerCase()
590
+ if (!kw) return studentSelectOptions.value
591
+ return studentSelectOptions.value.filter((opt) => opt.label.toLowerCase().includes(kw))
592
+ })
593
+
594
+ const formatTime = (iso: string) => {
595
+ if (!iso) return ''
596
+ const d = new Date(iso)
597
+ if (Number.isNaN(d.getTime())) return iso
598
+ return d.toLocaleString()
599
+ }
600
+
601
+ const mapRegionToTreeNodes = (nodes: any[]): RegionTreeNode[] => {
602
+ if (!Array.isArray(nodes)) return []
603
+ return nodes.map((node: any) => ({
604
+ id: `r-${node.value}`,
605
+ label: node.label,
606
+ code: String(node.value || ''),
607
+ children: mapRegionToTreeNodes(node.children || []),
608
+ }))
609
+ }
610
+
611
+ const treeData = computed<RegionTreeNode[]>(() => [
612
+ {
613
+ id: 'all',
614
+ label: '全国',
615
+ code: '',
616
+ children: mapRegionToTreeNodes(regionData),
617
+ },
618
+ ])
619
+
620
+ const regionTagText = computed(() => {
621
+ if (!selectedRegionCode.value) return ''
622
+ return codeToText[selectedRegionCode.value] || selectedRegionCode.value
623
+ })
624
+
625
+ const filterTreeNode = (value: string, data: RegionTreeNode) => {
626
+ const text = value.trim()
627
+ if (!text) return true
628
+ return data.label.includes(text)
629
+ }
630
+
631
+ const onRegionNodeClick = (node: RegionTreeNode) => {
632
+ selectedRegionCode.value = node.code || ''
633
+ tplPagination.value.page = 1
634
+ loadCompetitions()
635
+ }
636
+
637
+ const clearRegionFilter = () => {
638
+ selectedRegionCode.value = ''
639
+ treeRef.value?.setCurrentKey('all')
640
+ tplPagination.value.page = 1
641
+ loadCompetitions()
642
+ }
643
+
644
+ const searchTemplates = () => {
645
+ tplPagination.value.page = 1
646
+ loadCompetitions()
647
+ }
648
+
649
+ const resetTemplateFilters = () => {
650
+ tplKeyword.value = ''
651
+ tplDateRange.value = []
652
+ selectedRegionCode.value = ''
653
+ treeFilterText.value = ''
654
+ treeRef.value?.setCurrentKey('all')
655
+ tplPagination.value.page = 1
656
+ loadCompetitions()
657
+ }
658
+
659
+ const templateRowIndex = (index: number) => {
660
+ return (tplPagination.value.page - 1) * tplPagination.value.pageSize + index + 1
661
+ }
662
+
663
+ const persistTasks = () => {
664
+ try {
665
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks.value))
666
+ } catch {}
667
+ }
668
+
669
+ const loadTasksFromStorage = () => {
670
+ try {
671
+ const raw = localStorage.getItem(STORAGE_KEY)
672
+ if (!raw) return
673
+ const parsed = JSON.parse(raw) as Partial<BadgeTask>[]
674
+ if (Array.isArray(parsed)) {
675
+ tasks.value = parsed.map((item) => ({
676
+ id: String(item.id || newId()),
677
+ title: String(item.title || '证件任务'),
678
+ competitionId: Number(item.competitionId || 0),
679
+ competitionName: String(item.competitionName || ''),
680
+ templateId: item.templateId ? Number(item.templateId) : undefined,
681
+ templateName: item.templateName ? String(item.templateName) : undefined,
682
+ scopeAllSchools: Boolean(item.scopeAllSchools ?? true),
683
+ schoolIds: Array.isArray(item.schoolIds) ? item.schoolIds.map((id) => Number(id)) : [],
684
+ studentIds: Array.isArray(item.studentIds) ? item.studentIds.map((id) => Number(id)) : null,
685
+ generateMode: item.generateMode === 'school' ? 'school' : 'sequence',
686
+ createdAt: String(item.createdAt || new Date().toISOString()),
687
+ }))
688
+ }
689
+ } catch {}
690
+ }
691
+
692
+ const loadCompetitions = async () => {
693
+ tplTableLoading.value = true
694
+ try {
695
+ const params: Record<string, unknown> = {
696
+ page: tplPagination.value.page,
697
+ page_size: tplPagination.value.pageSize,
698
+ }
699
+ const keyword = tplKeyword.value.trim()
700
+ if (keyword) params.keyword = keyword
701
+ if (tplDateRange.value.length === 2) {
702
+ params.start_date = tplDateRange.value[0]
703
+ params.end_date = tplDateRange.value[1]
704
+ }
705
+ if (selectedRegionCode.value) params.host_region_code = selectedRegionCode.value
706
+ const res: any = await getCompetitions(params)
707
+ competitions.value = res.data?.data || []
708
+ tplPagination.value.total = res.data?.total || 0
709
+ } catch {
710
+ competitions.value = []
711
+ tplPagination.value.total = 0
712
+ } finally {
713
+ tplTableLoading.value = false
714
+ nextTick(() => recalcTplTable())
715
+ }
716
+ }
717
+
718
+ const loadTaskCompetitionOptions = async () => {
719
+ try {
720
+ const res: any = await getCompetitions({ page: 1, page_size: 500 })
721
+ taskCompetitionOptions.value = res.data?.data || []
722
+ } catch {
723
+ taskCompetitionOptions.value = []
724
+ }
725
+ }
726
+
727
+ const goEditBadgeTemplate = (competitionId: number) => {
728
+ router.push(`/qiuui/badge/competition/${competitionId}/badge-templates`)
729
+ }
730
+
731
+ const goCompetitionDetail = (competitionId: number) => {
732
+ router.push(`/qiuui/badge/competition/${competitionId}`)
733
+ }
734
+
735
+ const goBadgeLibrary = () => {
736
+ badgeLibraryDialogOpen.value = true
737
+ }
738
+
739
+ const openDialog = () => {
740
+ resetDialogForm()
741
+ dialogVisible.value = true
742
+ }
743
+
744
+ const resetDialogForm = () => {
745
+ dialogTitle.value = ''
746
+ dialogCompetitionId.value = undefined
747
+ dialogTemplateId.value = undefined
748
+ dialogScopeAll.value = true
749
+ dialogSchoolIds.value = []
750
+ dialogPickStudents.value = false
751
+ dialogStudentIds.value = []
752
+ dialogGenerateMode.value = 'sequence'
753
+ badgeSchoolOptions.value = []
754
+ badgeTemplateOptions.value = []
755
+ dialogStudentsRaw.value = []
756
+ schoolFilterText.value = ''
757
+ studentFilterText.value = ''
758
+ }
759
+
760
+ const onDialogCompetitionChange = async (cid: number) => {
761
+ dialogSchoolIds.value = []
762
+ dialogTemplateId.value = undefined
763
+ dialogStudentIds.value = []
764
+ badgeSchoolOptions.value = []
765
+ badgeTemplateOptions.value = []
766
+ dialogStudentsRaw.value = []
767
+ if (!cid) return
768
+ try {
769
+ const tplRes: any = await listBadgeTemplates(cid)
770
+ badgeTemplateOptions.value = tplRes.data?.list ?? []
771
+ const first = badgeTemplateOptions.value[0]
772
+ if (first?.id) dialogTemplateId.value = Number(first.id)
773
+ } catch {
774
+ badgeTemplateOptions.value = []
775
+ }
776
+ try {
777
+ const rs: any = await getBadgeSchools(cid)
778
+ badgeSchoolOptions.value = rs.data?.schools ?? []
779
+ } catch {
780
+ badgeSchoolOptions.value = []
781
+ }
782
+ try {
783
+ const st: any = await getStudents(cid, { page: 1, page_size: 5000 })
784
+ dialogStudentsRaw.value = st.data?.data ?? st.data ?? []
785
+ } catch {
786
+ dialogStudentsRaw.value = []
787
+ }
788
+ }
789
+
790
+ const saveTask = () => {
791
+ const cid = dialogCompetitionId.value
792
+ if (!cid) {
793
+ qiuMsg.warning('请选择比赛')
794
+ return
795
+ }
796
+ if (!dialogTemplateId.value) {
797
+ qiuMsg.warning('请选择证件模板')
798
+ return
799
+ }
800
+ if (!dialogScopeAll.value && !dialogSchoolIds.value.length) {
801
+ qiuMsg.warning('请选择至少一所学校,或改为「全部学校」')
802
+ return
803
+ }
804
+ if (dialogPickStudents.value) {
805
+ if (!dialogStudentIds.value.length) {
806
+ qiuMsg.warning('请勾选指定学生,或关闭「指定学生」')
807
+ return
808
+ }
809
+ }
810
+ const comp = taskCompetitionOptions.value.find((c) => c.id === cid) || competitions.value.find((c) => c.id === cid)
811
+ const tpl = badgeTemplateOptions.value.find((t) => Number(t.id) === Number(dialogTemplateId.value))
812
+ const task: BadgeTask = {
813
+ id: newId(),
814
+ title: dialogTitle.value.trim() || `证件任务 ${tasks.value.length + 1}`,
815
+ competitionId: cid,
816
+ competitionName: comp?.name ?? `#${cid}`,
817
+ templateId: Number(dialogTemplateId.value),
818
+ templateName: tpl?.name || '未命名模板',
819
+ scopeAllSchools: dialogScopeAll.value,
820
+ schoolIds: dialogScopeAll.value ? [] : [...dialogSchoolIds.value],
821
+ studentIds: dialogPickStudents.value && dialogStudentIds.value.length ? [...dialogStudentIds.value] : null,
822
+ generateMode: dialogGenerateMode.value,
823
+ createdAt: new Date().toISOString(),
824
+ }
825
+ tasks.value = [task, ...tasks.value]
826
+ persistTasks()
827
+ dialogVisible.value = false
828
+ qiuMsg.success('任务已保存')
829
+ }
830
+
831
+ const openPaperForPreview = (row: BadgeTask) => {
832
+ pendingPreviewTask.value = row
833
+ paperSize.value = 'a4'
834
+ paperOrientation.value = 'portrait'
835
+ paperColumnsPreset.value = 2
836
+ paperColumns.value = 2
837
+ paperPickVisible.value = true
838
+ }
839
+
840
+ const generateTaskNow = async (row: BadgeTask) => {
841
+ pendingPreviewTask.value = row
842
+ paperSize.value = 'a4'
843
+ paperOrientation.value = 'portrait'
844
+ paperColumnsPreset.value = 2
845
+ paperColumns.value = 2
846
+ await confirmPaperAndPreview()
847
+ }
848
+
849
+ const confirmPaperAndPreview = async () => {
850
+ const row = pendingPreviewTask.value
851
+ if (!row) {
852
+ paperPickVisible.value = false
853
+ return
854
+ }
855
+ paperPickVisible.value = false
856
+ previewTaskTitle.value = row.title
857
+ pdfPreviewTaskTitle.value = row.title
858
+ previewBadges.value = []
859
+ previewSkipped.value = []
860
+ pdfTemplateDoc.value = null
861
+ pdfMergeList.value = []
862
+ pdfSkipped.value = []
863
+ const params: Record<string, string> = {}
864
+ if (!row.scopeAllSchools && row.schoolIds.length) params.school_ids = row.schoolIds.join(',')
865
+ if (row.studentIds?.length) params.student_ids = row.studentIds.join(',')
866
+ pdfLoading.value = true
867
+ pdfDialogVisible.value = true
868
+ try {
869
+ const res: any = await getBadges(row.competitionId, params)
870
+ previewBadges.value = sortBadgesForTask(res.data?.badges ?? [], row.generateMode)
871
+ previewSkipped.value = sortSkippedForTask(res.data?.skipped_no_seat ?? [], row.generateMode)
872
+ pdfSkipped.value = previewSkipped.value
873
+ if (!previewBadges.value.length && !previewSkipped.value.length) {
874
+ pdfDialogVisible.value = false
875
+ void qiuAlert(
876
+ '没有可生成的证件(请检查是否已按赛事场次上传学生、已添加教室并自动编排座位号)',
877
+ '提示'
878
+ )
879
+ return
880
+ }
881
+ pdfMergeList.value = previewBadges.value.map((b) => flattenBadgeForMerge(b as Record<string, unknown>))
882
+ try {
883
+ const tplRes: any = row.templateId
884
+ ? await getBadgeTemplateOne(row.competitionId, row.templateId)
885
+ : await getBadgeTemplate(row.competitionId)
886
+ const tpl = tplRes.data?.template_json
887
+ if (tpl && typeof tpl === 'object' && Array.isArray((tpl as BadgeTemplateDoc).elements) && (tpl as BadgeTemplateDoc).elements!.length) {
888
+ pdfTemplateDoc.value = tpl as BadgeTemplateDoc
889
+ await nextTick()
890
+ if (pdfGridMetrics.value.requestedCols > pdfGridMetrics.value.maxCols) {
891
+ pdfDialogVisible.value = false
892
+ qiuMsg.warning(
893
+ `${paperSize.value.toUpperCase()}${paperOrientation.value === 'landscape' ? '横向' : '纵向'} 最多每行显示 ${pdfGridMetrics.value.maxCols} 个证件,请调整后重试`
894
+ )
895
+ return
896
+ }
897
+ }
898
+ } catch {
899
+ pdfTemplateDoc.value = null
900
+ }
901
+ } catch {
902
+ pdfDialogVisible.value = false
903
+ } finally {
904
+ pdfLoading.value = false
905
+ }
906
+ }
907
+
908
+ const clearInlinePreview = () => {
909
+ previewBadges.value = []
910
+ previewSkipped.value = []
911
+ previewTaskTitle.value = ''
912
+ }
913
+
914
+ const reopenPdfPreview = () => {
915
+ if (!pendingPreviewTask.value) return
916
+ pdfDialogVisible.value = true
917
+ }
918
+
919
+ const markSeatAssignmentLocked = async (source: 'badge_print' | 'badge_pdf') => {
920
+ const row = pendingPreviewTask.value
921
+ if (!row?.competitionId) return
922
+ try {
923
+ await finalizeSeatAssignmentLock(row.competitionId, { source })
924
+ } catch {
925
+ qiuMsg.warning('参赛证已输出,但定档状态写入失败,请刷新后重试')
926
+ }
927
+ }
928
+
929
+ const onDownloadBadgePdf = async () => {
930
+ const el = pdfExportRoot.value
931
+ if (!el || !pdfMergeList.value.length) return
932
+ pdfDownloading.value = true
933
+ try {
934
+ await nextTick()
935
+ await new Promise((r) => setTimeout(r, 200))
936
+ const safe = (pdfPreviewTaskTitle.value || 'export').replace(/[/\\?%*:|"<>]/g, '_')
937
+ const pages = Array.from(el.querySelectorAll<HTMLElement>('.pdf-page'))
938
+ await downloadPdfPagesFromElements(pages, `证件_${safe}.pdf`, {
939
+ size: paperSize.value,
940
+ orientation: paperOrientation.value,
941
+ widthMm: pdfPaperSize.value.w,
942
+ heightMm: pdfPaperSize.value.h,
943
+ })
944
+ await markSeatAssignmentLocked('badge_pdf')
945
+ } finally {
946
+ pdfDownloading.value = false
947
+ }
948
+ }
949
+
950
+ const onPrintBadgePdf = async () => {
951
+ const el = pdfExportRoot.value
952
+ if (!el || !pdfMergeList.value.length) return
953
+ try {
954
+ await nextTick()
955
+ await new Promise((r) => setTimeout(r, 200))
956
+ const pages = Array.from(el.querySelectorAll<HTMLElement>('.pdf-page'))
957
+ const safe = (pdfPreviewTaskTitle.value || '证件预览').replace(/[/\\?%*:|"<>]/g, '_')
958
+ await printPdfPagesFromElements(pages, `证件_${safe}`, {
959
+ size: paperSize.value,
960
+ orientation: paperOrientation.value,
961
+ widthMm: pdfPaperSize.value.w,
962
+ heightMm: pdfPaperSize.value.h,
963
+ })
964
+ await markSeatAssignmentLocked('badge_print')
965
+ } catch (error) {
966
+ qiuMsg.error(error instanceof Error ? error.message : '打印失败')
967
+ }
968
+ }
969
+
970
+ const removeTask = (id: string) => {
971
+ tasks.value = tasks.value.filter((t) => t.id !== id)
972
+ persistTasks()
973
+ }
974
+
975
+ onMounted(async () => {
976
+ loadTasksFromStorage()
977
+ await Promise.all([loadCompetitions(), loadTaskCompetitionOptions()])
978
+ nextTick(() => {
979
+ recalcTplTable()
980
+ recalcTaskTable()
981
+ })
982
+ })
983
+ watch(treeFilterText, (value) => {
984
+ treeRef.value?.filter(value)
985
+ })
986
+ watch(
987
+ () => tasks.value.length,
988
+ () => nextTick(() => recalcTaskTable())
989
+ )
990
+ onActivated(() =>
991
+ nextTick(() => {
992
+ recalcTplTable()
993
+ recalcTaskTable()
994
+ })
995
+ )
996
+ watch(paperColumnsPreset, (value) => {
997
+ if (isDirectSizeMode.value) return
998
+ if (value > 0) {
999
+ paperColumns.value = value
1000
+ }
1001
+ })
1002
+ watch(paperColumns, (value) => {
1003
+ if (isDirectSizeMode.value) {
1004
+ if (paperColumns.value !== 1) paperColumns.value = 1
1005
+ if (paperColumnsPreset.value !== 1) paperColumnsPreset.value = 1
1006
+ return
1007
+ }
1008
+ const normalized = Math.max(1, Number(value || 1))
1009
+ if (normalized !== value) {
1010
+ paperColumns.value = normalized
1011
+ return
1012
+ }
1013
+ paperColumnsPreset.value = [1, 2, 3].includes(normalized) ? normalized : 0
1014
+ })
1015
+ watch(paperSize, (value) => {
1016
+ if (value === 'custom') {
1017
+ paperOrientation.value = 'portrait'
1018
+ paperColumnsPreset.value = 1
1019
+ paperColumns.value = 1
1020
+ }
1021
+ })
1022
+
1023
+ function sortBadgesForTask(list: any[], mode: 'sequence' | 'school'): any[] {
1024
+ const rows = Array.isArray(list) ? [...list] : []
1025
+ if (mode === 'school') {
1026
+ rows.sort((a, b) => {
1027
+ const schoolCompare = compareText(a?.school_name, b?.school_name)
1028
+ if (schoolCompare !== 0) return schoolCompare
1029
+ const roomCompare = compareText(a?.competition_room, b?.competition_room)
1030
+ if (roomCompare !== 0) return roomCompare
1031
+ const seatCompare = compareSeat(a?.seat_no, b?.seat_no)
1032
+ if (seatCompare !== 0) return seatCompare
1033
+ return compareText(a?.student_name, b?.student_name)
1034
+ })
1035
+ return rows
1036
+ }
1037
+ rows.sort((a, b) => {
1038
+ const roomCompare = compareText(a?.competition_room, b?.competition_room)
1039
+ if (roomCompare !== 0) return roomCompare
1040
+ const seatCompare = compareSeat(a?.seat_no, b?.seat_no)
1041
+ if (seatCompare !== 0) return seatCompare
1042
+ const noCompare = compareText(a?.competition_no, b?.competition_no)
1043
+ if (noCompare !== 0) return noCompare
1044
+ return compareText(a?.student_name, b?.student_name)
1045
+ })
1046
+ return rows
1047
+ }
1048
+
1049
+ function sortSkippedForTask(list: any[], mode: 'sequence' | 'school'): any[] {
1050
+ const rows = Array.isArray(list) ? [...list] : []
1051
+ rows.sort((a, b) => {
1052
+ if (mode === 'school') {
1053
+ const schoolCompare = compareText(a?.school_name, b?.school_name)
1054
+ if (schoolCompare !== 0) return schoolCompare
1055
+ }
1056
+ return compareText(a?.student_name, b?.student_name)
1057
+ })
1058
+ return rows
1059
+ }
1060
+
1061
+ function compareText(a: unknown, b: unknown): number {
1062
+ return String(a || '').localeCompare(String(b || ''), 'zh-CN')
1063
+ }
1064
+
1065
+ function compareSeat(a: unknown, b: unknown): number {
1066
+ const sa = String(a || '').trim()
1067
+ const sb = String(b || '').trim()
1068
+ const na = Number(sa)
1069
+ const nb = Number(sb)
1070
+ const aValid = sa !== '' && !Number.isNaN(na)
1071
+ const bValid = sb !== '' && !Number.isNaN(nb)
1072
+ if (aValid && bValid && na !== nb) return na - nb
1073
+ return sa.localeCompare(sb, 'zh-CN', { numeric: true })
1074
+ }
1075
+ </script>
1076
+
1077
+ <style scoped>
1078
+ .toolbar {
1079
+ display: flex;
1080
+ align-items: center;
1081
+ justify-content: space-between;
1082
+ margin-bottom: 8px;
1083
+ }
1084
+ .toolbar-title {
1085
+ font-size: 16px;
1086
+ font-weight: 600;
1087
+ color: #303133;
1088
+ }
1089
+ .hint {
1090
+ font-size: 13px;
1091
+ color: #909399;
1092
+ margin: 0 0 12px;
1093
+ }
1094
+ .preview-card {
1095
+ margin-top: 16px;
1096
+ }
1097
+ .preview-head {
1098
+ display: flex;
1099
+ align-items: center;
1100
+ justify-content: space-between;
1101
+ gap: 12px;
1102
+ }
1103
+ .preview-head-actions {
1104
+ display: flex;
1105
+ align-items: center;
1106
+ gap: 12px;
1107
+ flex-wrap: wrap;
1108
+ }
1109
+ .badge-grid {
1110
+ display: grid;
1111
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
1112
+ gap: 16px;
1113
+ }
1114
+ .badge-card {
1115
+ border: 2px solid #409eff;
1116
+ border-radius: 8px;
1117
+ padding: 16px;
1118
+ text-align: center;
1119
+ }
1120
+ .badge-header {
1121
+ font-size: 12px;
1122
+ color: #909399;
1123
+ }
1124
+ .badge-name {
1125
+ font-size: 20px;
1126
+ font-weight: bold;
1127
+ margin: 8px 0;
1128
+ }
1129
+ .badge-school {
1130
+ color: #606266;
1131
+ }
1132
+ .badge-no {
1133
+ color: #409eff;
1134
+ font-weight: bold;
1135
+ margin-top: 8px;
1136
+ }
1137
+ .badge-room {
1138
+ color: #909399;
1139
+ font-size: 13px;
1140
+ margin-top: 4px;
1141
+ }
1142
+ .inline-stack {
1143
+ width: 100%;
1144
+ }
1145
+ .dialog-tip {
1146
+ width: 100%;
1147
+ margin-top: 6px;
1148
+ color: #909399;
1149
+ font-size: 12px;
1150
+ line-height: 1.5;
1151
+ }
1152
+ .badge-manage {
1153
+ display: flex;
1154
+ flex-direction: column;
1155
+ flex: 1;
1156
+ min-height: 0;
1157
+ overflow: hidden;
1158
+ gap: 0;
1159
+ }
1160
+ .tpl-fill-card,
1161
+ .task-fill-card {
1162
+ flex: 1;
1163
+ min-height: 0;
1164
+ display: flex;
1165
+ flex-direction: column;
1166
+ overflow: hidden;
1167
+ margin-bottom: 16px;
1168
+ }
1169
+ .tpl-fill-card :deep(.qiu-card__body),
1170
+ .task-fill-card :deep(.qiu-card__body) {
1171
+ flex: 1;
1172
+ min-height: 0;
1173
+ display: flex;
1174
+ flex-direction: column;
1175
+ overflow: hidden;
1176
+ box-sizing: border-box;
1177
+ }
1178
+ .tpl-hint-shrink,
1179
+ .task-hint-shrink {
1180
+ flex-shrink: 0;
1181
+ }
1182
+ .card-head {
1183
+ display: flex;
1184
+ flex-direction: column;
1185
+ gap: 4px;
1186
+ }
1187
+ .card-head-desc {
1188
+ font-size: 13px;
1189
+ font-weight: normal;
1190
+ color: #909399;
1191
+ }
1192
+ .tpl-hint strong {
1193
+ color: #409eff;
1194
+ font-weight: 600;
1195
+ }
1196
+ .tpl-list-layout {
1197
+ flex: 1;
1198
+ min-height: 0;
1199
+ display: flex;
1200
+ gap: 14px;
1201
+ overflow: hidden;
1202
+ }
1203
+ .tpl-region-aside {
1204
+ width: 240px;
1205
+ flex-shrink: 0;
1206
+ display: flex;
1207
+ flex-direction: column;
1208
+ min-height: 0;
1209
+ padding: 12px;
1210
+ border: 1px solid #ebeef5;
1211
+ border-radius: 8px;
1212
+ background: #fafafa;
1213
+ box-sizing: border-box;
1214
+ }
1215
+ .tree-title {
1216
+ font-size: 14px;
1217
+ font-weight: 700;
1218
+ color: #303133;
1219
+ margin-bottom: 10px;
1220
+ }
1221
+ .tree-filter {
1222
+ margin-bottom: 10px;
1223
+ }
1224
+ .tpl-region-scroll {
1225
+ flex: 1;
1226
+ min-height: 0;
1227
+ }
1228
+ .tpl-list-main {
1229
+ flex: 1;
1230
+ min-width: 0;
1231
+ display: flex;
1232
+ flex-direction: column;
1233
+ overflow: hidden;
1234
+ }
1235
+ .tpl-filter-bar {
1236
+ flex-shrink: 0;
1237
+ display: flex;
1238
+ align-items: center;
1239
+ gap: 8px;
1240
+ flex-wrap: wrap;
1241
+ margin-bottom: 12px;
1242
+ }
1243
+ .tpl-table {
1244
+ width: 100%;
1245
+ }
1246
+ .tpl-pagination {
1247
+ flex-shrink: 0;
1248
+ }
1249
+ .paper-tip {
1250
+ margin: 0 0 12px;
1251
+ color: #606266;
1252
+ font-size: 14px;
1253
+ }
1254
+ .paper-config {
1255
+ display: flex;
1256
+ flex-direction: column;
1257
+ gap: 14px;
1258
+ }
1259
+ .paper-config-block {
1260
+ display: flex;
1261
+ flex-direction: column;
1262
+ gap: 8px;
1263
+ }
1264
+ .paper-inline-row {
1265
+ display: flex;
1266
+ align-items: center;
1267
+ gap: 12px;
1268
+ flex-wrap: wrap;
1269
+ }
1270
+ .paper-config-title {
1271
+ font-size: 13px;
1272
+ color: #606266;
1273
+ font-weight: 600;
1274
+ }
1275
+ .paper-config-desc {
1276
+ font-size: 12px;
1277
+ color: #909399;
1278
+ }
1279
+ .pdf-dialog-body {
1280
+ max-height: 78vh;
1281
+ overflow: auto;
1282
+ background: #f5f7fa;
1283
+ padding: 12px;
1284
+ border-radius: 8px;
1285
+ }
1286
+ .pdf-export-root {
1287
+ margin: 0 auto;
1288
+ }
1289
+ .pdf-page {
1290
+ margin: 0 auto 12px;
1291
+ background: #fff;
1292
+ box-sizing: border-box;
1293
+ display: flex;
1294
+ align-items: center;
1295
+ justify-content: center;
1296
+ break-after: page;
1297
+ }
1298
+ .pdf-page-grid {
1299
+ display: grid;
1300
+ gap: 0;
1301
+ align-content: start;
1302
+ justify-content: start;
1303
+ }
1304
+ .pdf-cell {
1305
+ background: #fff;
1306
+ box-sizing: border-box;
1307
+ overflow: hidden;
1308
+ }
1309
+ .simple-badge {
1310
+ min-width: 200px;
1311
+ border: 2px solid #409eff;
1312
+ border-radius: 8px;
1313
+ padding: 12px;
1314
+ text-align: center;
1315
+ }
1316
+ .sb-h {
1317
+ font-size: 12px;
1318
+ color: #909399;
1319
+ }
1320
+ .sb-name {
1321
+ font-size: 18px;
1322
+ font-weight: 700;
1323
+ margin: 6px 0;
1324
+ }
1325
+ .sb-school {
1326
+ font-size: 13px;
1327
+ color: #606266;
1328
+ }
1329
+ .sb-no {
1330
+ color: #409eff;
1331
+ font-weight: 600;
1332
+ margin-top: 8px;
1333
+ font-size: 13px;
1334
+ }
1335
+ .sb-room {
1336
+ font-size: 12px;
1337
+ color: #909399;
1338
+ margin-top: 4px;
1339
+ }
1340
+ .checkbox-scroll {
1341
+ max-height: 220px;
1342
+ overflow-y: auto;
1343
+ padding: 8px;
1344
+ border: 1px solid #dcdfe6;
1345
+ border-radius: 6px;
1346
+ background: #fafafa;
1347
+ }
1348
+ .checkbox-scroll.is-disabled {
1349
+ opacity: 0.6;
1350
+ pointer-events: none;
1351
+ }
1352
+ .checkbox-scroll :deep(.qiu-checkbox-group) {
1353
+ display: flex;
1354
+ flex-direction: column;
1355
+ align-items: flex-start;
1356
+ gap: 6px;
1357
+ }
1358
+ .checkbox-empty {
1359
+ font-size: 13px;
1360
+ color: #909399;
1361
+ text-align: center;
1362
+ padding: 8px 0;
1363
+ }
1364
+ </style>