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,3503 @@
1
+ <template>
2
+ <div
3
+ ref="rootRef"
4
+ class="qiu-table"
5
+ tabindex="0"
6
+ :class="[
7
+ {
8
+ 'qiu-table--border': border === true || border === 'full' || border === 'default',
9
+ 'qiu-table--border-full': border === 'full' || border === true,
10
+ 'qiu-table--border-outer': border === 'outer',
11
+ 'qiu-table--border-inner': border === 'inner',
12
+ 'qiu-table--border-none': border === 'none' || border === false,
13
+ 'qiu-table--stripe': stripe,
14
+ 'qiu-table--round': round,
15
+ 'qiu-table--fullscreen': isFullscreen,
16
+ 'qiu-table--toolbar': showToolbarArea,
17
+ 'qiu-table--hover': resolveRowHover(rowConfig),
18
+ 'qiu-table--fixed-height': height != null || maxHeight != null,
19
+ 'qiu-table--scroll-x': hasScrollX,
20
+ 'qiu-table--scroll-y': scrollbarSize > 0,
21
+ },
22
+ size ? `qiu-table--${size}` : '',
23
+ ]"
24
+ :style="rootTableStyle"
25
+ @keydown="onTableKeydown"
26
+ >
27
+ <div v-if="showToolbarArea" class="qiu-table__toolbar">
28
+ <div class="qiu-table__toolbar-left">
29
+ <slot name="toolbar-left" />
30
+ <slot name="toolbar" />
31
+ <div v-if="toolsPlacement === 'left'" class="qiu-table__toolbar-builtin">
32
+ <template v-for="tool in toolButtonList" :key="tool.name">
33
+ <button
34
+ v-if="tool.show"
35
+ type="button"
36
+ class="qiu-table__toolbar-icon"
37
+ :title="tool.title"
38
+ @click="tool.onClick"
39
+ >
40
+ <qiu-icon :icon="tool.icon" />
41
+ </button>
42
+ </template>
43
+ <qiu-dropdown v-if="copyEnabled" class="qiu-table__toolbar-dropdown">
44
+ <button type="button" class="qiu-table__toolbar-icon" :title="translate('复制')">
45
+ <qiu-icon icon="file-copy-line" />
46
+ </button>
47
+ <template #dropdown>
48
+ <qiu-dropdown-menu>
49
+ <qiu-dropdown-item @click="copyTableData('vertical')">
50
+ {{ translate('纵向复制') }}
51
+ </qiu-dropdown-item>
52
+ <qiu-dropdown-item @click="copyTableData('horizontal')">
53
+ {{ translate('横向复制') }}
54
+ </qiu-dropdown-item>
55
+ </qiu-dropdown-menu>
56
+ </template>
57
+ </qiu-dropdown>
58
+ </div>
59
+ <component
60
+ :is="tool.component"
61
+ v-for="tool in customToolsByPosition('left')"
62
+ :key="tool.name"
63
+ v-bind="tool.props"
64
+ />
65
+ </div>
66
+ <div class="qiu-table__toolbar-center">
67
+ <slot name="toolbar-center" />
68
+ <div v-if="toolsPlacement === 'center'" class="qiu-table__toolbar-builtin">
69
+ <template v-for="tool in toolButtonList" :key="tool.name">
70
+ <button
71
+ v-if="tool.show"
72
+ type="button"
73
+ class="qiu-table__toolbar-icon"
74
+ :title="tool.title"
75
+ @click="tool.onClick"
76
+ >
77
+ <qiu-icon :icon="tool.icon" />
78
+ </button>
79
+ </template>
80
+ <qiu-dropdown v-if="copyEnabled" class="qiu-table__toolbar-dropdown">
81
+ <button type="button" class="qiu-table__toolbar-icon" :title="translate('复制')">
82
+ <qiu-icon icon="file-copy-line" />
83
+ </button>
84
+ <template #dropdown>
85
+ <qiu-dropdown-menu>
86
+ <qiu-dropdown-item @click="copyTableData('vertical')">
87
+ {{ translate('纵向复制') }}
88
+ </qiu-dropdown-item>
89
+ <qiu-dropdown-item @click="copyTableData('horizontal')">
90
+ {{ translate('横向复制') }}
91
+ </qiu-dropdown-item>
92
+ </qiu-dropdown-menu>
93
+ </template>
94
+ </qiu-dropdown>
95
+ </div>
96
+ <component
97
+ :is="tool.component"
98
+ v-for="tool in customToolsByPosition('center')"
99
+ :key="tool.name"
100
+ v-bind="tool.props"
101
+ />
102
+ </div>
103
+ <div class="qiu-table__toolbar-right">
104
+ <slot name="toolbar-tools" />
105
+ <slot name="toolbar-right" />
106
+ <div v-if="toolsPlacement === 'right'" class="qiu-table__toolbar-builtin">
107
+ <template v-for="tool in toolButtonList" :key="tool.name">
108
+ <button
109
+ v-if="tool.show"
110
+ type="button"
111
+ class="qiu-table__toolbar-icon"
112
+ :title="tool.title"
113
+ @click="tool.onClick"
114
+ >
115
+ <qiu-icon :icon="tool.icon" />
116
+ </button>
117
+ </template>
118
+ <qiu-dropdown v-if="copyEnabled" class="qiu-table__toolbar-dropdown">
119
+ <button type="button" class="qiu-table__toolbar-icon" :title="translate('复制')">
120
+ <qiu-icon icon="file-copy-line" />
121
+ </button>
122
+ <template #dropdown>
123
+ <qiu-dropdown-menu>
124
+ <qiu-dropdown-item @click="copyTableData('vertical')">
125
+ {{ translate('纵向复制') }}
126
+ </qiu-dropdown-item>
127
+ <qiu-dropdown-item @click="copyTableData('horizontal')">
128
+ {{ translate('横向复制') }}
129
+ </qiu-dropdown-item>
130
+ </qiu-dropdown-menu>
131
+ </template>
132
+ </qiu-dropdown>
133
+ </div>
134
+ <component
135
+ :is="tool.component"
136
+ v-for="tool in customToolsByPosition('right')"
137
+ :key="tool.name"
138
+ v-bind="tool.props"
139
+ />
140
+ </div>
141
+ </div>
142
+
143
+ <div v-if="customPanelOpen" class="qiu-table__custom-panel">
144
+ <label v-for="col in customizableColumns" :key="col._key" class="qiu-table__custom-item">
145
+ <input
146
+ type="checkbox"
147
+ :checked="columnVisibleMap[col._key] !== false"
148
+ @change="toggleColumnVisible(col._key, ($event.target as HTMLInputElement).checked)"
149
+ />
150
+ <span>{{ col.label || col._key }}</span>
151
+ </label>
152
+ </div>
153
+
154
+ <qiu-dialog
155
+ v-model="exportDialogVisible"
156
+ :title="translate('导出数据')"
157
+ width="480px"
158
+ align-center
159
+ :show-minimize="false"
160
+ :show-fullscreen="false"
161
+ >
162
+ <div class="qiu-table__export-dialog">
163
+ <div class="qiu-table__export-section">
164
+ <div class="qiu-table__export-label">{{ translate('导出字段') }}</div>
165
+ <div class="qiu-table__export-fields">
166
+ <label class="qiu-table__export-field">
167
+ <input
168
+ type="checkbox"
169
+ :checked="exportAllChecked"
170
+ :indeterminate.prop="exportIndeterminate"
171
+ @change="toggleExportAll(($event.target as HTMLInputElement).checked)"
172
+ />
173
+ <span>{{ translate('全选') }}</span>
174
+ </label>
175
+ <label
176
+ v-for="col in exportableColumns"
177
+ :key="col._key"
178
+ class="qiu-table__export-field"
179
+ >
180
+ <input
181
+ type="checkbox"
182
+ :checked="exportFieldKeys.includes(col._key)"
183
+ @change="
184
+ toggleExportField(col._key, ($event.target as HTMLInputElement).checked)
185
+ "
186
+ />
187
+ <span>{{ col.label || resolveColumnProp(col) || col._key }}</span>
188
+ </label>
189
+ </div>
190
+ </div>
191
+ <div class="qiu-table__export-section">
192
+ <div class="qiu-table__export-label">{{ translate('文件格式') }}</div>
193
+ <div class="qiu-table__export-formats">
194
+ <label
195
+ v-for="fmt in exportFormats"
196
+ :key="fmt.value"
197
+ class="qiu-table__export-format"
198
+ :class="{ 'is-active': exportFormat === fmt.value }"
199
+ >
200
+ <input v-model="exportFormat" type="radio" :value="fmt.value" />
201
+ <span>{{ fmt.label }}</span>
202
+ </label>
203
+ </div>
204
+ </div>
205
+ <div class="qiu-table__export-section">
206
+ <div class="qiu-table__export-label">{{ translate('导出范围') }}</div>
207
+ <div class="qiu-table__export-modes">
208
+ <label
209
+ v-for="m in exportModeOptions"
210
+ :key="m.value"
211
+ class="qiu-table__export-mode"
212
+ :class="{ 'is-active': exportMode === m.value }"
213
+ >
214
+ <input v-model="exportMode" type="radio" :value="m.value" />
215
+ <span>{{ m.label }}</span>
216
+ </label>
217
+ </div>
218
+ <div v-if="exportMode === 'range'" class="qiu-table__export-range">
219
+ <span>{{ translate('页码') }}</span>
220
+ <input
221
+ v-model.number="exportStartPage"
222
+ type="number"
223
+ :min="1"
224
+ :max="pageCount"
225
+ class="qiu-table__export-range-input"
226
+ />
227
+ <span>-</span>
228
+ <input
229
+ v-model.number="exportEndPage"
230
+ type="number"
231
+ :min="exportStartPage"
232
+ :max="pageCount"
233
+ class="qiu-table__export-range-input"
234
+ />
235
+ <span>{{ translate('(共') }} {{ pageCount }} {{ translate('页)') }}</span>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ <template #footer>
240
+ <qiu-button @click="exportDialogVisible = false">{{ translate('取消') }}</qiu-button>
241
+ <qiu-button type="primary" :disabled="!exportFieldKeys.length" @click="confirmExport">
242
+ {{ translate('下载') }}
243
+ </qiu-button>
244
+ </template>
245
+ </qiu-dialog>
246
+
247
+ <input
248
+ ref="importInputRef"
249
+ type="file"
250
+ accept=".csv,.xls,.xlsx,text/csv,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
251
+ class="qiu-table__import-input"
252
+ @change="onImportFile"
253
+ />
254
+
255
+ <div class="qiu-table__main" :style="mainStyle">
256
+ <QiuLoadingSpinner
257
+ v-if="loading"
258
+ class="qiu-table__loading"
259
+ :text="loadingText || `${translate('加载中')}...`"
260
+ :effect="loadingEffect"
261
+ :transparent="loadingTransparent"
262
+ />
263
+ <!-- Header -->
264
+ <div
265
+ ref="headerWrapRef"
266
+ class="qiu-table__header-wrap"
267
+ >
268
+ <table class="qiu-table__inner qiu-table__header-table" :style="tableMinWidthStyle">
269
+ <colgroup>
270
+ <col v-if="resolvedShowExpand" style="width: 48px" />
271
+ <col v-if="resolvedShowRadio" style="width: 48px" />
272
+ <col v-if="resolvedShowSelection" style="width: 48px" />
273
+ <col v-if="resolvedShowIndex" style="width: 55px" />
274
+ <col
275
+ v-if="virtualXEnabled && virtualXPad.left > 0"
276
+ :style="{ width: `${virtualXPad.left}px` }"
277
+ />
278
+ <col
279
+ v-for="col in displayLeafColumns"
280
+ :key="`hcol-${col._key}`"
281
+ :style="resolvedColStyle(col)"
282
+ />
283
+ <col
284
+ v-if="virtualXEnabled && virtualXPad.right > 0"
285
+ :style="{ width: `${virtualXPad.right}px` }"
286
+ />
287
+ </colgroup>
288
+ <thead>
289
+ <tr v-for="(row, ri) in headerRows" :key="`hr-${ri}`">
290
+ <th
291
+ v-if="resolvedShowExpand && ri === 0"
292
+ class="qiu-table__cell qiu-table__cell--header qiu-table__cell--expand qiu-table__cell--fixed-left"
293
+ :class="specialLeftLastClass('expand')"
294
+ :style="{ left: '0px' }"
295
+ :rowspan="headerRows.length"
296
+ />
297
+ <th
298
+ v-if="resolvedShowRadio && ri === 0"
299
+ class="qiu-table__cell qiu-table__cell--header qiu-table__cell--radio qiu-table__cell--fixed-left"
300
+ :class="specialLeftLastClass('radio')"
301
+ :style="{ left: `${fixedLeftOffset('radio')}px` }"
302
+ :rowspan="headerRows.length"
303
+ />
304
+ <th
305
+ v-if="resolvedShowSelection && ri === 0"
306
+ class="qiu-table__cell qiu-table__cell--header qiu-table__cell--selection qiu-table__cell--fixed-left"
307
+ :class="specialLeftLastClass('selection')"
308
+ :style="{ left: `${fixedLeftOffset('selection')}px` }"
309
+ :rowspan="headerRows.length"
310
+ >
311
+ <qiu-checkbox
312
+ :model-value="isAllSelected"
313
+ :indeterminate="isIndeterminate"
314
+ @change="(v) => toggleSelectAll(!!v)"
315
+ />
316
+ </th>
317
+ <th
318
+ v-if="resolvedShowIndex && ri === 0"
319
+ class="qiu-table__cell qiu-table__cell--header qiu-table__cell--index qiu-table__cell--fixed-left"
320
+ :class="specialLeftLastClass('index')"
321
+ :style="{ left: `${fixedLeftOffset('index')}px` }"
322
+ :rowspan="headerRows.length"
323
+ >
324
+ {{ translate('序号') }}
325
+ </th>
326
+ <th
327
+ v-if="virtualXEnabled && virtualXPad.left > 0 && ri === 0"
328
+ :rowspan="headerRows.length"
329
+ class="qiu-table__cell qiu-table__cell--header"
330
+ />
331
+ <th
332
+ v-for="(cell, ci) in row"
333
+ :key="`th-${ri}-${ci}-${cell.column._key}`"
334
+ class="qiu-table__cell qiu-table__cell--header"
335
+ :class="[
336
+ cell.column.className,
337
+ cell.column.headerClassName,
338
+ cell.column.headerAlign
339
+ ? `is-align-${cell.column.headerAlign}`
340
+ : `is-align-${cell.column.align || headerAlign || align || 'left'}`,
341
+ {
342
+ 'qiu-table__cell--sortable': cell.isLeaf && !!cell.column.sortable,
343
+ 'qiu-table__cell--col-drag': cell.isLeaf && columnDragEnabled,
344
+ ...(cell.isLeaf ? fixedCellClass(cell.column as any) : {}),
345
+ },
346
+ ]"
347
+ :colspan="cell.colspan"
348
+ :rowspan="cell.rowspan"
349
+ :style="headerCellStyle(cell)"
350
+ :data-col-key="cell.isLeaf ? cell.column._key : undefined"
351
+ @click="cell.isLeaf && onHeaderClick(cell.column as any, $event)"
352
+ >
353
+ <div class="qiu-table__th">
354
+ <qiu-icon
355
+ v-if="cell.isLeaf && getShowIcon() && canEditCol(cell.column as any)"
356
+ icon="edit-line"
357
+ class="qiu-table__edit-icon"
358
+ title="edit"
359
+ />
360
+ <span class="qiu-table__label">{{ cell.label }}</span>
361
+ <span
362
+ v-if="cell.isLeaf && cell.column.sortable"
363
+ class="qiu-table__sort"
364
+ :class="sortClass(cell.column as any)"
365
+ >
366
+ <i class="qiu-table__caret qiu-table__caret--up" />
367
+ <i class="qiu-table__caret qiu-table__caret--down" />
368
+ <em v-if="sortPriority(cell.column as any)" class="qiu-table__sort-num">
369
+ {{ sortPriority(cell.column as any) }}
370
+ </em>
371
+ </span>
372
+ <button
373
+ v-if="cell.isLeaf && isFilterable(cell.column as any)"
374
+ type="button"
375
+ class="qiu-table__filter-btn"
376
+ :class="{
377
+ 'is-active':
378
+ isFilterActive(cell.column as any) || openFilterKey === cell.column._key,
379
+ }"
380
+ @click.stop="toggleFilterPanel(cell.column as any, $event)"
381
+ >
382
+ <svg viewBox="0 0 1024 1024" width="12" height="12">
383
+ <path
384
+ fill="currentColor"
385
+ d="M128 192h768l-288 352v256l-192 64V544L128 192z"
386
+ />
387
+ </svg>
388
+ </button>
389
+ <span
390
+ v-if="cell.isLeaf && isColResizable(cell.column as any)"
391
+ class="qiu-table__resizer"
392
+ @mousedown="onColResizeStart(cell.column as any, $event)"
393
+ />
394
+ </div>
395
+ </th>
396
+ <th
397
+ v-if="virtualXEnabled && virtualXPad.right > 0 && ri === 0"
398
+ :rowspan="headerRows.length"
399
+ class="qiu-table__cell qiu-table__cell--header"
400
+ />
401
+ </tr>
402
+ </thead>
403
+ </table>
404
+ </div>
405
+
406
+ <!-- Body -->
407
+ <div
408
+ ref="bodyWrapRef"
409
+ class="qiu-table__body-wrap"
410
+ @scroll="onBodyScroll"
411
+ @contextmenu.prevent="onBodyContextMenu"
412
+ >
413
+ <table ref="tableRef" class="qiu-table__inner qiu-table__body-table" :style="tableMinWidthStyle">
414
+ <colgroup>
415
+ <col v-if="resolvedShowExpand" style="width: 48px" />
416
+ <col v-if="resolvedShowRadio" style="width: 48px" />
417
+ <col v-if="resolvedShowSelection" style="width: 48px" />
418
+ <col v-if="resolvedShowIndex" style="width: 55px" />
419
+ <col
420
+ v-if="virtualXEnabled && virtualXPad.left > 0"
421
+ :style="{ width: `${virtualXPad.left}px` }"
422
+ />
423
+ <col
424
+ v-for="col in displayLeafColumns"
425
+ :key="`bcol-${col._key}`"
426
+ :style="resolvedColStyle(col)"
427
+ />
428
+ <col
429
+ v-if="virtualXEnabled && virtualXPad.right > 0"
430
+ :style="{ width: `${virtualXPad.right}px` }"
431
+ />
432
+ </colgroup>
433
+ <tbody ref="tbodyRef" class="qiu-table__body">
434
+ <tr
435
+ v-if="virtualEnabled && virtualRange.padTop > 0"
436
+ class="qiu-table__virtual-pad"
437
+ aria-hidden="true"
438
+ >
439
+ <td :colspan="totalColspan">
440
+ <div
441
+ class="qiu-table__virtual-pad-space"
442
+ :style="{ height: `${virtualRange.padTop}px` }"
443
+ />
444
+ </td>
445
+ </tr>
446
+ <template v-for="item in displayRowItems" :key="getRowId(item.flat.row) || item.flat.flatIndex">
447
+ <tr
448
+ class="qiu-table__row"
449
+ :class="[
450
+ {
451
+ 'qiu-table__row--striped': stripe && item.flat.flatIndex % 2 === 1,
452
+ 'is-selected': isRowSelected(item.flat.row),
453
+ 'is-current': currentRowId === getRowId(item.flat.row),
454
+ 'is-radio-checked': isRadioChecked(item.flat.row),
455
+ 'is-dirty': keepSourceEnabled && isUpdateByRow(item.flat.row),
456
+ },
457
+ resolveRowClassName(item.flat.row, item.rowIndex),
458
+ ]"
459
+ :data-row-key="getRowId(item.flat.row)"
460
+ @click="onRowClick(item.flat.row)"
461
+ >
462
+ <td
463
+ v-if="resolvedShowExpand"
464
+ class="qiu-table__cell qiu-table__cell--expand qiu-table__cell--fixed-left"
465
+ :class="specialLeftLastClass('expand')"
466
+ :style="{ left: '0px' }"
467
+ @click.stop="toggleRowExpand(item.flat.row)"
468
+ >
469
+ <div class="qiu-table__cell-inner">
470
+ <button type="button" class="qiu-table__tree-toggle">
471
+ <qiu-icon
472
+ :icon="isRowExpand(item.flat.row) ? 'arrow-down-s-fill' : 'arrow-right-s-fill'"
473
+ />
474
+ </button>
475
+ </div>
476
+ </td>
477
+ <td
478
+ v-if="resolvedShowRadio"
479
+ class="qiu-table__cell qiu-table__cell--radio qiu-table__cell--fixed-left"
480
+ :class="specialLeftLastClass('radio')"
481
+ :style="{ left: `${fixedLeftOffset('radio')}px` }"
482
+ @click.stop="onRadioChange(item.flat.row)"
483
+ >
484
+ <div class="qiu-table__cell-inner">
485
+ <input
486
+ type="radio"
487
+ class="qiu-table__radio"
488
+ :checked="isRadioChecked(item.flat.row)"
489
+ :name="radioGroupName"
490
+ @change="onRadioChange(item.flat.row)"
491
+ />
492
+ </div>
493
+ </td>
494
+ <td
495
+ v-if="resolvedShowSelection"
496
+ class="qiu-table__cell qiu-table__cell--selection qiu-table__cell--fixed-left"
497
+ :class="specialLeftLastClass('selection')"
498
+ :style="{ left: `${fixedLeftOffset('selection')}px` }"
499
+ @click.stop
500
+ >
501
+ <div class="qiu-table__cell-inner">
502
+ <qiu-checkbox
503
+ :model-value="isRowSelected(item.flat.row)"
504
+ @change="(v) => toggleRowSelection(item.flat.row, !!v)"
505
+ />
506
+ </div>
507
+ </td>
508
+ <td
509
+ v-if="resolvedShowIndex"
510
+ class="qiu-table__cell qiu-table__cell--index qiu-table__cell--fixed-left"
511
+ :class="specialLeftLastClass('index')"
512
+ :style="{ left: `${fixedLeftOffset('index')}px` }"
513
+ >
514
+ <div class="qiu-table__cell-inner">{{ item.flat.flatIndex + 1 }}</div>
515
+ </td>
516
+ <td v-if="virtualXEnabled && virtualXPad.left > 0" class="qiu-table__cell" />
517
+ <template
518
+ v-for="(col, colIndex) in displayLeafColumns"
519
+ :key="`td-${col._key}-${getRowId(item.flat.row)}`"
520
+ >
521
+ <td
522
+ v-if="cellSpan(item.flat, item.rowIndex, col, realColIndex(col)).visible"
523
+ class="qiu-table__cell"
524
+ :class="[
525
+ col.className,
526
+ resolveCellClassName(item.flat.row, col, item.rowIndex, realColIndex(col)),
527
+ `is-align-${col.align || align || 'left'}`,
528
+ {
529
+ 'qiu-table__cell--ellipsis': shouldEllipsis(col),
530
+ 'is-editing': isEditing(item.flat.row, col),
531
+ 'is-input-fill': isInputFill(item.flat.row, col),
532
+ 'is-editable': canEditCol(col),
533
+ 'is-valid-error': !!cellError(item.flat.row, col),
534
+ 'is-active-cell': isActiveCell(item.flat.row, col),
535
+ 'is-area-cell': isCellInArea(item.flat.row, col),
536
+ ...fixedCellClass(col),
537
+ },
538
+ ]"
539
+ :style="bodyCellStyle(col)"
540
+ :title="cellError(item.flat.row, col) || undefined"
541
+ :rowspan="cellSpan(item.flat, item.rowIndex, col, realColIndex(col)).rowspan"
542
+ :colspan="cellSpan(item.flat, item.rowIndex, col, realColIndex(col)).colspan"
543
+ @click="onCellClick(item.flat.row, col, $event)"
544
+ @dblclick="onCellDblclick(item.flat.row, col)"
545
+ >
546
+ <div
547
+ class="qiu-table__cell-inner"
548
+ :class="{
549
+ 'qiu-table__cell-inner--input-fill': isInputFill(item.flat.row, col),
550
+ }"
551
+ >
552
+ <span v-if="col.dragSort" class="qiu-table__drag-handle">
553
+ <qiu-icon icon="drag-move-2-line" />
554
+ </span>
555
+
556
+ <span
557
+ v-else-if="tree && col.treeNode"
558
+ class="qiu-table__tree-cell"
559
+ :style="{ paddingLeft: `${item.flat.level * 18}px` }"
560
+ >
561
+ <button
562
+ v-if="item.flat.hasChildren"
563
+ type="button"
564
+ class="qiu-table__tree-toggle"
565
+ @click.stop="toggleExpand(item.flat.row)"
566
+ >
567
+ <qiu-icon
568
+ :icon="item.flat.expanded ? 'arrow-down-s-fill' : 'arrow-right-s-fill'"
569
+ />
570
+ </button>
571
+ <span v-else class="qiu-table__tree-placeholder" />
572
+ <cell-content
573
+ :row="item.flat.row"
574
+ :col="col"
575
+ :index="item.flat.flatIndex"
576
+ />
577
+ </span>
578
+
579
+ <slot
580
+ v-else-if="col.slot && $slots[col.slot]"
581
+ :name="col.slot"
582
+ v-bind="
583
+ mapSlotScope(
584
+ item.flat.row,
585
+ col,
586
+ item.flat.flatIndex,
587
+ item.flat.level
588
+ )
589
+ "
590
+ />
591
+
592
+ <component
593
+ :is="col._defaultSlot"
594
+ v-else-if="col._defaultSlot"
595
+ v-bind="
596
+ mapSlotScope(
597
+ item.flat.row,
598
+ col,
599
+ item.flat.flatIndex,
600
+ item.flat.level
601
+ )
602
+ "
603
+ />
604
+
605
+ <div
606
+ v-else
607
+ class="qiu-table__editor-host"
608
+ :class="{
609
+ 'qiu-table__editor-host--input-fill': isInputFill(item.flat.row, col),
610
+ }"
611
+ >
612
+ <cell-content
613
+ :row="item.flat.row"
614
+ :col="col"
615
+ :index="item.flat.flatIndex"
616
+ />
617
+ </div>
618
+ </div>
619
+ </td>
620
+ </template>
621
+ <td v-if="virtualXEnabled && virtualXPad.right > 0" class="qiu-table__cell" />
622
+ </tr>
623
+ <tr v-if="resolvedShowExpand && isRowExpand(item.flat.row)" class="qiu-table__expand-row">
624
+ <td :colspan="totalColspan" class="qiu-table__cell qiu-table__cell--expand-content">
625
+ <div class="qiu-table__expand-content">
626
+ <slot name="expand" :row="item.flat.row" :$index="item.flat.flatIndex">
627
+ <pre class="qiu-table__expand-json">{{ JSON.stringify(item.flat.row, null, 2) }}</pre>
628
+ </slot>
629
+ </div>
630
+ </td>
631
+ </tr>
632
+ </template>
633
+ <tr
634
+ v-if="virtualEnabled && virtualRange.padBottom > 0"
635
+ class="qiu-table__virtual-pad"
636
+ aria-hidden="true"
637
+ >
638
+ <td :colspan="totalColspan">
639
+ <div
640
+ class="qiu-table__virtual-pad-space"
641
+ :style="{ height: `${virtualRange.padBottom}px` }"
642
+ />
643
+ </td>
644
+ </tr>
645
+ <tr v-if="!pagedRows.length" class="qiu-table__row qiu-table__row--empty">
646
+ <td class="qiu-table__cell qiu-table__cell--empty" :colspan="totalColspan">
647
+ <slot name="empty">{{ emptyText || translate('暂无数据') }}</slot>
648
+ </td>
649
+ </tr>
650
+ </tbody>
651
+ </table>
652
+ </div>
653
+
654
+ <!-- Footer -->
655
+ <div
656
+ v-if="showFooter && footerRows.length"
657
+ ref="footerWrapRef"
658
+ class="qiu-table__footer-wrap"
659
+ >
660
+ <table class="qiu-table__inner qiu-table__footer-table" :style="tableMinWidthStyle">
661
+ <colgroup>
662
+ <col v-if="resolvedShowExpand" style="width: 48px" />
663
+ <col v-if="resolvedShowRadio" style="width: 48px" />
664
+ <col v-if="resolvedShowSelection" style="width: 48px" />
665
+ <col v-if="resolvedShowIndex" style="width: 55px" />
666
+ <col
667
+ v-if="virtualXEnabled && virtualXPad.left > 0"
668
+ :style="{ width: `${virtualXPad.left}px` }"
669
+ />
670
+ <col
671
+ v-for="col in displayLeafColumns"
672
+ :key="`fcol-${col._key}`"
673
+ :style="resolvedColStyle(col)"
674
+ />
675
+ <col
676
+ v-if="virtualXEnabled && virtualXPad.right > 0"
677
+ :style="{ width: `${virtualXPad.right}px` }"
678
+ />
679
+ </colgroup>
680
+ <tbody>
681
+ <tr v-for="(frow, fi) in footerRows" :key="`fr-${fi}`" class="qiu-table__row">
682
+ <td
683
+ v-if="resolvedShowExpand"
684
+ class="qiu-table__cell qiu-table__cell--footer qiu-table__cell--fixed-left"
685
+ :class="specialLeftLastClass('expand')"
686
+ :style="{ left: '0px' }"
687
+ >
688
+ <div class="qiu-table__cell-inner" />
689
+ </td>
690
+ <td
691
+ v-if="resolvedShowRadio"
692
+ class="qiu-table__cell qiu-table__cell--footer qiu-table__cell--fixed-left"
693
+ :class="specialLeftLastClass('radio')"
694
+ :style="{ left: `${fixedLeftOffset('radio')}px` }"
695
+ >
696
+ <div class="qiu-table__cell-inner" />
697
+ </td>
698
+ <td
699
+ v-if="resolvedShowSelection"
700
+ class="qiu-table__cell qiu-table__cell--footer qiu-table__cell--selection qiu-table__cell--fixed-left"
701
+ :class="specialLeftLastClass('selection')"
702
+ :style="{ left: `${fixedLeftOffset('selection')}px` }"
703
+ >
704
+ <div class="qiu-table__cell-inner" />
705
+ </td>
706
+ <td
707
+ v-if="resolvedShowIndex"
708
+ class="qiu-table__cell qiu-table__cell--footer qiu-table__cell--index qiu-table__cell--fixed-left"
709
+ :class="specialLeftLastClass('index')"
710
+ :style="{ left: `${fixedLeftOffset('index')}px` }"
711
+ >
712
+ <div class="qiu-table__cell-inner" />
713
+ </td>
714
+ <td
715
+ v-for="(text, ci) in frow"
716
+ :key="`fd-${fi}-${ci}`"
717
+ class="qiu-table__cell qiu-table__cell--footer"
718
+ :class="[
719
+ `is-align-${leafColumns[ci]?.footerAlign || leafColumns[ci]?.align || footerAlign || align || 'left'}`,
720
+ leafColumns[ci]?.footerClassName,
721
+ fixedCellClass(leafColumns[ci]),
722
+ ]"
723
+ :style="bodyCellStyle(leafColumns[ci])"
724
+ >
725
+ <div class="qiu-table__cell-inner">{{ text }}</div>
726
+ </td>
727
+ </tr>
728
+ </tbody>
729
+ </table>
730
+ </div>
731
+ </div>
732
+
733
+ <div v-if="pagerEnabled" class="qiu-table__pager">
734
+ <qiu-pagination
735
+ v-model:current-page="innerPage"
736
+ v-model:page-size="innerPageSize"
737
+ :total="pagerTotal"
738
+ :page-sizes="pagerConfig?.pageSizes || [10, 20, 50, 100]"
739
+ :background="pagerConfig?.background !== false"
740
+ :small="pagerConfig?.small"
741
+ layout="total, sizes, prev, pager, next, jumper"
742
+ @current-change="onPageChange"
743
+ @size-change="onSizeChange"
744
+ />
745
+ </div>
746
+
747
+ <Teleport to="body">
748
+ <div v-if="openFilterKey" class="qiu-table__filter-mask" @mousedown="closeFilterPanel" />
749
+ <div
750
+ v-if="openFilterKey && filterPanelPos"
751
+ class="qiu-table__filter-panel"
752
+ :style="{ left: `${filterPanelPos.left}px`, top: `${filterPanelPos.top}px` }"
753
+ @mousedown.stop
754
+ >
755
+ <div class="qiu-table__filter-head">
756
+ <span class="qiu-table__filter-title">{{ translate('筛选') }}</span>
757
+ <div v-if="openFilterOptions.length" class="qiu-table__filter-checkall">
758
+ <qiu-checkbox
759
+ :model-value="isFilterAllChecked"
760
+ :indeterminate="isFilterIndeterminate"
761
+ @change="(v) => toggleFilterAll(!!v)"
762
+ >
763
+ {{ translate('全选') }}
764
+ </qiu-checkbox>
765
+ </div>
766
+ </div>
767
+ <div class="qiu-table__filter-list">
768
+ <div
769
+ v-for="opt in openFilterOptions"
770
+ :key="String(opt.value)"
771
+ class="qiu-table__filter-item"
772
+ >
773
+ <qiu-checkbox
774
+ :model-value="isFilterOptionChecked(openFilterKey, opt.value)"
775
+ @change="(v) => toggleFilterOption(openFilterKey!, opt.value, !!v)"
776
+ >
777
+ {{ opt.text }}
778
+ </qiu-checkbox>
779
+ </div>
780
+ <div v-if="!openFilterOptions.length" class="qiu-table__filter-empty">
781
+ {{ translate('无筛选项') }}
782
+ </div>
783
+ </div>
784
+ <div class="qiu-table__filter-actions">
785
+ <button type="button" class="qiu-table__filter-action is-plain" @click="resetOpenFilter">
786
+ {{ translate('重置') }}
787
+ </button>
788
+ <button type="button" class="qiu-table__filter-action" @click="confirmFilter">
789
+ {{ translate('确定') }}
790
+ </button>
791
+ </div>
792
+ </div>
793
+
794
+ <div v-if="ctxMenu.visible" class="qiu-table__filter-mask" @mousedown="closeCtxMenu" />
795
+ <div
796
+ v-if="ctxMenu.visible"
797
+ class="qiu-table__ctx-menu"
798
+ :style="{ left: `${ctxMenu.x}px`, top: `${ctxMenu.y}px` }"
799
+ @mousedown.stop
800
+ >
801
+ <template v-for="(group, gi) in ctxMenu.options" :key="gi">
802
+ <div v-if="gi > 0" class="qiu-table__ctx-divider" />
803
+ <button
804
+ v-for="item in group"
805
+ :key="item.code"
806
+ type="button"
807
+ class="qiu-table__ctx-item"
808
+ :disabled="item.disabled"
809
+ @click="onMenuItemClick(item)"
810
+ >
811
+ {{ item.name }}
812
+ </button>
813
+ </template>
814
+ </div>
815
+ </Teleport>
816
+
817
+ <slot />
818
+ </div>
819
+ </template>
820
+
821
+ <script lang="ts" setup>
822
+ /**
823
+ * QiuTable — 方案 A 自研,零 vxe 运行时
824
+ * Phase1–3: 编辑/多级头/表尾/分页 + 列宽/合并 + Grid级导入打印/虚拟滚动/菜单/校验
825
+ */
826
+ import Sortable from 'sortablejs'
827
+ import { translate } from '../../src/i18n'
828
+ import QiuCheckbox from '../QiuCheckbox/index.vue'
829
+ import QiuInput from '../QiuInput/index.vue'
830
+ import QiuInputNumber from '../QiuInputNumber/index.vue'
831
+ import QiuLoadingSpinner from '../QiuLoading/spinner.vue'
832
+ import type { QiuLoadingEffect } from '../QiuLoading/types'
833
+ import QiuPagination from '../QiuPagination/index.vue'
834
+ import QiuSelect from '../QiuSelect/index.vue'
835
+ import QiuSwitch from '../QiuSwitch/index.vue'
836
+ import QiuTime from '../QiuTime/index.vue'
837
+ import QiuTooltip from '../QiuTooltip/index.vue'
838
+ import { QiuTableEditRenderer, QiuTableFormats, QiuTableRenderer } from './registry'
839
+ import type {
840
+ QiuTableClipboardConfig,
841
+ QiuTableColumnConfig,
842
+ QiuTableColumnConfigExtra,
843
+ QiuTableEditConfig,
844
+ QiuTableExportConfig,
845
+ QiuTableFilterConfig,
846
+ QiuTableFlatRow,
847
+ QiuTableFooterMethod,
848
+ QiuTableHistoryConfig,
849
+ QiuTableKeyboardConfig,
850
+ QiuTableMergeCell,
851
+ QiuTablePagerConfig,
852
+ QiuTableRegisterContext,
853
+ QiuTableRegisteredColumn,
854
+ QiuTableRowConfig,
855
+ QiuTableSortConfig,
856
+ QiuTableSpanMethod,
857
+ QiuTableToolbarConfig,
858
+ QiuTableTooltipConfig,
859
+ } from './types'
860
+ import {
861
+ QIU_TABLE_REGISTER_KEY,
862
+ resolveColumnProp,
863
+ resolveEditRender,
864
+ resolveRowCurrent,
865
+ resolveRowHover,
866
+ } from './types'
867
+ import QiuDropdown from '../QiuDropdown/index.vue'
868
+ import QiuDropdownMenu from '../QiuDropdown/QiuDropdownMenu.vue'
869
+ import QiuDropdownItem from '../QiuDropdown/QiuDropdownItem.vue'
870
+ import { qiuMsg } from '../QiuMsg'
871
+ import {
872
+ buildHeaderRows,
873
+ computeRenderWidths,
874
+ computeTableMinWidth,
875
+ flattenLeafColumns,
876
+ getColumnSizeHints,
877
+ isFirstFixedRight,
878
+ isLastFixedLeft,
879
+ leftOffsetForColumn,
880
+ makeColumnKey,
881
+ parseWidth,
882
+ rightOffsetForColumn,
883
+ type LeafColumn,
884
+ } from './columns'
885
+ import { useTableArea } from './useArea'
886
+ import { useTableEdit } from './useEdit'
887
+ import {
888
+ compareRows,
889
+ useExpandRows,
890
+ useKeepSource,
891
+ useRadioRow,
892
+ type SortItem,
893
+ } from './useExtras'
894
+ import { useTableHistory } from './useHistory'
895
+ import { applyMergeHeaderCells, buildMergeCoveredSet, resolveCellSpan } from './useMerge'
896
+ import { bindAutoClearEdit, useColumnResize } from './useResize'
897
+ import { useScrollSync } from './useScrollSync'
898
+ import { useVirtualY } from './useVirtual'
899
+ import type {
900
+ QiuTableCustomConfig,
901
+ QiuTableEditRules,
902
+ QiuTableMenuConfig,
903
+ QiuTableMenuItem,
904
+ QiuTableValidConfig,
905
+ } from './utils'
906
+ import { csvToObjects, validateCell } from './utils'
907
+ import {
908
+ buildSheetMatrix,
909
+ exportSheet,
910
+ parseImportFile,
911
+ type QiuTableExportFormat,
912
+ } from './export'
913
+
914
+ defineOptions({ name: 'QiuTable' })
915
+
916
+ type FilterOption = { text: string; value: any }
917
+
918
+ const props = withDefaults(
919
+ defineProps<{
920
+ data: any[]
921
+ columns?: QiuTableColumnConfig[]
922
+ editable?: boolean
923
+ editTrigger?: 'click' | 'dblclick' | 'manual' | 'always'
924
+ editConfig?: QiuTableEditConfig
925
+ keyboardConfig?: QiuTableKeyboardConfig
926
+ clipboardConfig?: QiuTableClipboardConfig
927
+ tooltipConfig?: QiuTableTooltipConfig
928
+ exportConfig?: QiuTableExportConfig
929
+ historyConfig?: QiuTableHistoryConfig | boolean
930
+ sortConfig?: QiuTableSortConfig
931
+ filterConfig?: QiuTableFilterConfig
932
+ rowConfig?: QiuTableRowConfig
933
+ columnConfig?: QiuTableColumnConfigExtra
934
+ spanMethod?: QiuTableSpanMethod
935
+ mergeCells?: QiuTableMergeCell[]
936
+ /** 合并表头单元格(扁平表头额外合并;多级表头优先用 children) */
937
+ mergeHeaderCells?: QiuTableMergeCell[]
938
+ border?: boolean | 'full' | 'outer' | 'inner' | 'none' | 'default'
939
+ stripe?: boolean
940
+ round?: boolean
941
+ height?: string | number
942
+ maxHeight?: string | number
943
+ rowKey?: string | ((row: any) => string)
944
+ showIndex?: boolean
945
+ showSelection?: boolean
946
+ showRadio?: boolean
947
+ showExpand?: boolean
948
+ radioConfig?: import('./types').QiuTableRadioConfig
949
+ expandConfig?: import('./types').QiuTableExpandConfig
950
+ columnDragConfig?: import('./types').QiuTableColumnDragConfig
951
+ keepSource?: boolean
952
+ virtualXConfig?: boolean | { enabled?: boolean; gt?: number }
953
+ tree?: boolean
954
+ treeProps?: { children?: string; hasChildren?: string }
955
+ virtualY?: boolean
956
+ showToolbar?: boolean
957
+ showRefresh?: boolean
958
+ showExport?: boolean
959
+ showImport?: boolean
960
+ showPrint?: boolean
961
+ showCopy?: boolean
962
+ showFullscreen?: boolean
963
+ toolbarConfig?: QiuTableToolbarConfig | boolean
964
+ showFooter?: boolean
965
+ footerMethod?: QiuTableFooterMethod
966
+ footerData?: string[][]
967
+ pagerConfig?: QiuTablePagerConfig | boolean
968
+ gridOptions?: Record<string, any>
969
+ dragSort?: boolean
970
+ defaultExpandAll?: boolean
971
+ size?: 'large' | 'default' | 'small'
972
+ emptyText?: string
973
+ loading?: boolean
974
+ /** loading 文案 */
975
+ loadingText?: string
976
+ /** loading 动画,见 QIU_LOADING_EFFECTS(20 种) */
977
+ loadingEffect?: QiuLoadingEffect
978
+ /** loading 遮罩更透明 */
979
+ loadingTransparent?: boolean
980
+ /** 是否将剩余宽度分配给仅 minWidth 的弹性列(对齐 vxe fit) */
981
+ fit?: boolean
982
+ /** 表格级默认对齐 */
983
+ align?: 'left' | 'center' | 'right'
984
+ headerAlign?: 'left' | 'center' | 'right'
985
+ footerAlign?: 'left' | 'center' | 'right'
986
+ minHeight?: string | number
987
+ rowClassName?: string | ((params: { row: any; rowIndex: number }) => string)
988
+ cellClassName?:
989
+ | string
990
+ | ((params: { row: any; column: QiuTableColumnConfig; rowIndex: number; columnIndex: number }) => string)
991
+ customConfig?: QiuTableCustomConfig
992
+ menuConfig?: QiuTableMenuConfig
993
+ editRules?: QiuTableEditRules
994
+ validConfig?: QiuTableValidConfig
995
+ virtualYConfig?: boolean | { enabled?: boolean; gt?: number; oSize?: number; rowHeight?: number }
996
+ }>(),
997
+ {
998
+ columns: () => [],
999
+ editable: false,
1000
+ editTrigger: 'click',
1001
+ border: true,
1002
+ stripe: false,
1003
+ round: false,
1004
+ showIndex: false,
1005
+ showSelection: false,
1006
+ showRadio: false,
1007
+ showExpand: false,
1008
+ keepSource: false,
1009
+ fit: true,
1010
+ tree: false,
1011
+ treeProps: () => ({ children: 'children', hasChildren: 'hasChildren' }),
1012
+ virtualY: false,
1013
+ showToolbar: false,
1014
+ showRefresh: true,
1015
+ showExport: false,
1016
+ showImport: false,
1017
+ showPrint: false,
1018
+ showCopy: false,
1019
+ showFullscreen: true,
1020
+ showFooter: false,
1021
+ dragSort: false,
1022
+ defaultExpandAll: true,
1023
+ loading: false,
1024
+ loadingText: '',
1025
+ loadingEffect: 'spinner',
1026
+ loadingTransparent: false,
1027
+ mergeCells: () => [],
1028
+ mergeHeaderCells: () => [],
1029
+ }
1030
+ )
1031
+
1032
+ const emit = defineEmits<{
1033
+ edit: [payload: { row: any; column: QiuTableColumnConfig; value: any }]
1034
+ 'cell-edit-start': [payload: { row: any; column: QiuTableColumnConfig }]
1035
+ 'cell-edit-end': [payload: { row: any; column: QiuTableColumnConfig; value: any }]
1036
+ 'row-dragend': [payload: any]
1037
+ 'checkbox-change': [payload: any]
1038
+ 'checkbox-all': [payload: any]
1039
+ 'selection-change': [payload: any[]]
1040
+ 'filter-change': [payload: Record<string, any[]>]
1041
+ 'sort-change': [payload: { prop: string; order: 'asc' | 'desc' | null }]
1042
+ 'page-change': [payload: { currentPage: number; pageSize: number }]
1043
+ 'current-change': [payload: { row: any | null }]
1044
+ 'resizable-change': [payload: { column: QiuTableColumnConfig; width: number }]
1045
+ 'menu-click': [payload: { menu: QiuTableMenuItem; row?: any; column?: QiuTableColumnConfig }]
1046
+ 'import-data': [payload: { rows: any[] }]
1047
+ 'radio-change': [payload: { row: any | null }]
1048
+ 'toggle-row-expand': [payload: { row: any; expanded: boolean }]
1049
+ 'column-dragend': [payload: { oldIndex: number; newIndex: number; columnKeys: string[] }]
1050
+ refresh: []
1051
+ }>()
1052
+
1053
+ const rootRef = ref<HTMLElement | null>(null)
1054
+ const tableRef = ref<HTMLTableElement | null>(null)
1055
+ const tbodyRef = ref<HTMLTableSectionElement | null>(null)
1056
+ const headerWrapRef = ref<HTMLDivElement | null>(null)
1057
+ const bodyWrapRef = ref<HTMLDivElement | null>(null)
1058
+ const footerWrapRef = ref<HTMLDivElement | null>(null)
1059
+ const selectionRows = ref<any[]>([])
1060
+ const registeredColumns = ref<QiuTableRegisteredColumn[]>([])
1061
+ const expandedKeys = ref<Set<string>>(new Set())
1062
+ const sortState = ref<{ prop: string; order: 'asc' | 'desc' | null }>({ prop: '', order: null })
1063
+ const sortList = ref<SortItem[]>([])
1064
+ const columnOrderKeys = ref<string[]>([])
1065
+ const scrollLeftX = ref(0)
1066
+ const hasScrollX = ref(false)
1067
+ const updateHasScrollX = () => {
1068
+ const el = bodyWrapRef.value
1069
+ hasScrollX.value = !!el && el.scrollWidth > el.clientWidth
1070
+ }
1071
+ const radioGroupName = `qiu-table-radio-${Math.random().toString(36).slice(2, 8)}`
1072
+ const filterValues = reactive<Record<string, any[]>>({})
1073
+ const filterDraft = reactive<Record<string, any[]>>({})
1074
+ const openFilterKey = ref<string | null>(null)
1075
+ const filterPanelPos = ref<{ left: number; top: number } | null>(null)
1076
+ const isFullscreen = ref(false)
1077
+ const innerPage = ref(1)
1078
+ const innerPageSize = ref(10)
1079
+ const currentRowId = ref<string | null>(null)
1080
+ const customPanelOpen = ref(false)
1081
+ const exportDialogVisible = ref(false)
1082
+ const exportFormat = ref<QiuTableExportFormat>('xlsx')
1083
+ const exportFieldKeys = ref<string[]>([])
1084
+ const exportMode = ref<QiuTableExportConfig['mode']>('all')
1085
+ const exportStartPage = ref(1)
1086
+ const exportEndPage = ref(1)
1087
+ const exportFormats = [
1088
+ { value: 'xlsx' as const, label: 'Excel (.xlsx)' },
1089
+ { value: 'xls' as const, label: 'Excel (.xls)' },
1090
+ { value: 'csv' as const, label: 'CSV (.csv)' },
1091
+ ]
1092
+ const exportModeOptions = [
1093
+ { value: 'all' as const, label: translate('全部数据') },
1094
+ { value: 'current' as const, label: translate('当前页') },
1095
+ { value: 'selected' as const, label: translate('选中行') },
1096
+ { value: 'range' as const, label: translate('指定页码范围') },
1097
+ ]
1098
+ const columnVisibleMap = reactive<Record<string, boolean>>({})
1099
+ const importInputRef = ref<HTMLInputElement | null>(null)
1100
+ const validErrors = reactive<Record<string, string>>({})
1101
+ const ctxMenu = reactive<{
1102
+ visible: boolean
1103
+ x: number
1104
+ y: number
1105
+ options: QiuTableMenuItem[][]
1106
+ row?: any
1107
+ column?: QiuTableColumnConfig
1108
+ }>({ visible: false, x: 0, y: 0, options: [] })
1109
+
1110
+ let sortableInst: Sortable | null = null
1111
+ let expandInitialized = false
1112
+
1113
+ const layoutTick = ref(0)
1114
+
1115
+ const { scrollbarSize, measureScrollbar, syncFromBody, scheduleLayout } = useScrollSync({
1116
+ headerWrapRef,
1117
+ bodyWrapRef,
1118
+ footerWrapRef,
1119
+ onLayoutChange: () => {
1120
+ layoutTick.value++
1121
+ updateHasScrollX()
1122
+ },
1123
+ })
1124
+
1125
+ const { columnWidths, getWidth, onResizeStart, setColumnWidth } = useColumnResize()
1126
+
1127
+ const columnWidthDefaults = computed(() => ({
1128
+ minWidth: props.columnConfig?.minWidth ?? 80,
1129
+ maxWidth: props.columnConfig?.maxWidth ?? Number.POSITIVE_INFINITY,
1130
+ }))
1131
+
1132
+ /** 无稳定主键时用对象身份兜底,避免多行 rowId 同为 '' 导致整列同时进入编辑 */
1133
+ let rowUidSeq = 0
1134
+ const rowUidMap = new WeakMap<object, string>()
1135
+
1136
+ const getStableObjectId = (row: object) => {
1137
+ let uid = rowUidMap.get(row)
1138
+ if (!uid) {
1139
+ uid = `__qiu_row_${++rowUidSeq}`
1140
+ rowUidMap.set(row, uid)
1141
+ }
1142
+ return uid
1143
+ }
1144
+
1145
+ const getRowId = (row: any): string => {
1146
+ if (row == null) return ''
1147
+ if (typeof props.rowKey === 'function') {
1148
+ const v = props.rowKey(row)
1149
+ if (v != null && String(v) !== '') return String(v)
1150
+ } else {
1151
+ const key =
1152
+ props.rowConfig?.keyField ||
1153
+ (typeof props.rowKey === 'string' && props.rowKey ? props.rowKey : '') ||
1154
+ 'id'
1155
+ const v = row?.[key]
1156
+ if (v != null && String(v) !== '') return String(v)
1157
+ }
1158
+ if (typeof row === 'object') return getStableObjectId(row)
1159
+ return String(row)
1160
+ }
1161
+
1162
+ const {
1163
+ isRowExpand,
1164
+ toggleRowExpand: toggleRowExpandRaw,
1165
+ setRowExpand,
1166
+ clearRowExpand,
1167
+ } = useExpandRows({ getRowId })
1168
+
1169
+ const toggleRowExpand = (row: any) => {
1170
+ if (props.expandConfig?.accordion) {
1171
+ clearRowExpand()
1172
+ }
1173
+ const before = isRowExpand(row)
1174
+ toggleRowExpandRaw(row)
1175
+ emit('toggle-row-expand', { row, expanded: !before })
1176
+ }
1177
+
1178
+ const { isRadioChecked, setRadioRow, clearRadioRow, radioRowId } = useRadioRow({ getRowId })
1179
+
1180
+ const keepSourceEnabled = computed(() => !!props.keepSource)
1181
+ const {
1182
+ snapshot: snapshotSource,
1183
+ isUpdateByRow,
1184
+ getUpdateRecords,
1185
+ getInsertRecords,
1186
+ reloadRow,
1187
+ } = useKeepSource({
1188
+ enabled: () => keepSourceEnabled.value,
1189
+ getRowId,
1190
+ data: () => props.data,
1191
+ })
1192
+
1193
+ const onRadioChange = (row: any) => {
1194
+ setRadioRow(row)
1195
+ emit('radio-change', { row })
1196
+ if (props.radioConfig?.highlight !== false) {
1197
+ currentRowId.value = getRowId(row)
1198
+ }
1199
+ }
1200
+
1201
+ const {
1202
+ editing,
1203
+ getColEditor,
1204
+ getEditRender,
1205
+ getProp,
1206
+ canEditCol,
1207
+ hasAlwaysEditor,
1208
+ isEditing,
1209
+ isInputFill,
1210
+ startEdit,
1211
+ clearEdit,
1212
+ commitEdit: baseCommitEdit,
1213
+ inEditGuard,
1214
+ getTrigger,
1215
+ getShowIcon,
1216
+ getAutoClear,
1217
+ } = useTableEdit({
1218
+ editable: () => props.editable,
1219
+ editTrigger: () => props.editTrigger,
1220
+ editConfig: () => props.editConfig,
1221
+ getRowId,
1222
+ emit: (event, payload) => emit(event as any, payload),
1223
+ })
1224
+
1225
+ const historyEnabled = () => {
1226
+ const h = props.historyConfig
1227
+ if (h === true) return true
1228
+ if (h && typeof h === 'object') return h.enabled !== false
1229
+ return false
1230
+ }
1231
+
1232
+ const {
1233
+ push: pushHistory,
1234
+ undo: undoEdit,
1235
+ redo: redoEdit,
1236
+ clear: clearHistory,
1237
+ canUndo,
1238
+ canRedo,
1239
+ } = useTableHistory({
1240
+ enabled: historyEnabled,
1241
+ size: () =>
1242
+ typeof props.historyConfig === 'object' && props.historyConfig?.size
1243
+ ? props.historyConfig.size
1244
+ : 50,
1245
+ findRow: (id) => findRowById(props.data, id),
1246
+ })
1247
+
1248
+ const {
1249
+ setActive,
1250
+ clearArea,
1251
+ isCellInArea,
1252
+ isActiveCell,
1253
+ moveActive,
1254
+ buildTsv,
1255
+ pasteTsv,
1256
+ clearAreaValues,
1257
+ activeCell,
1258
+ areaBounds,
1259
+ } = useTableArea({
1260
+ getRows: () => pagedRows.value.map((f) => f.row),
1261
+ getColumns: () => leafColumns.value,
1262
+ getRowId,
1263
+ })
1264
+
1265
+ const kb = () => props.keyboardConfig || {}
1266
+ const clip = () => props.clipboardConfig || {}
1267
+
1268
+ const shouldEllipsis = (col: QiuTableColumnConfig) => {
1269
+ if (col.showOverflowTooltip === false) return false
1270
+ if (props.tooltipConfig?.showAll === false) return false
1271
+ return true
1272
+ }
1273
+
1274
+ const resolveTooltipContent = (row: any, col: QiuTableColumnConfig) => {
1275
+ const cellValue = getProp(col) != null ? row?.[getProp(col)!] : undefined
1276
+ if (props.tooltipConfig?.contentMethod) {
1277
+ const r = props.tooltipConfig.contentMethod({ row, column: col, cellValue })
1278
+ if (r === null) return ''
1279
+ if (r !== undefined) return r
1280
+ }
1281
+ return formatCell(row, col)
1282
+ }
1283
+
1284
+ const commitEdit = async (row: any, col: QiuTableColumnConfig, value: any) => {
1285
+ if (inEditGuard()) return
1286
+ const prop = getProp(col)
1287
+ if (prop && props.editRules?.[prop]) {
1288
+ const err = await validateCell({
1289
+ rules: props.editRules[prop],
1290
+ cellValue: value,
1291
+ row,
1292
+ column: col,
1293
+ })
1294
+ const key = `${getRowId(row)}:${prop}`
1295
+ if (err) {
1296
+ validErrors[key] = err
1297
+ return
1298
+ }
1299
+ delete validErrors[key]
1300
+ }
1301
+ if (prop != null) {
1302
+ const oldValue = row[prop]
1303
+ if (oldValue !== value) {
1304
+ pushHistory({
1305
+ rowId: getRowId(row),
1306
+ prop,
1307
+ oldValue,
1308
+ newValue: value,
1309
+ })
1310
+ }
1311
+ }
1312
+ baseCommitEdit(row, col, value)
1313
+ }
1314
+
1315
+ const cellError = (row: any, col: QiuTableColumnConfig) => {
1316
+ const prop = getProp(col)
1317
+ if (!prop) return ''
1318
+ return validErrors[`${getRowId(row)}:${prop}`] || ''
1319
+ }
1320
+
1321
+ bindAutoClearEdit({
1322
+ rootRef,
1323
+ enabled: () => props.editable && getAutoClear(),
1324
+ clearEdit,
1325
+ })
1326
+
1327
+ watch(
1328
+ () => props.sortConfig?.defaultSort,
1329
+ (ds) => {
1330
+ if (!ds) return
1331
+ const list = Array.isArray(ds) ? ds : [ds]
1332
+ const mapped = list
1333
+ .map((d) => ({
1334
+ prop: d.prop || d.field || '',
1335
+ order: (d.order || 'asc') as 'asc' | 'desc',
1336
+ }))
1337
+ .filter((d) => d.prop)
1338
+ sortList.value = mapped
1339
+ if (mapped[0]) sortState.value = { prop: mapped[0].prop, order: mapped[0].order }
1340
+ },
1341
+ { immediate: true }
1342
+ )
1343
+
1344
+ watch(
1345
+ () => props.data,
1346
+ () => snapshotSource(),
1347
+ { deep: true, immediate: true }
1348
+ )
1349
+
1350
+ watch(
1351
+ () => props.expandConfig?.expandAll,
1352
+ (v) => {
1353
+ if (v && resolvedShowExpand.value) setRowExpand(props.data || [], true)
1354
+ },
1355
+ { immediate: true }
1356
+ )
1357
+
1358
+ watch(
1359
+ () => props.radioConfig?.checkRowKey,
1360
+ (key) => {
1361
+ if (key == null) return
1362
+ const row = (props.data || []).find((r) => getRowId(r) === String(key))
1363
+ if (row) setRadioRow(row)
1364
+ },
1365
+ { immediate: true }
1366
+ )
1367
+
1368
+ const normalizeSize = (v: string | number) => {
1369
+ if (typeof v === 'number' && Number.isFinite(v)) return `${v}px`
1370
+ const s = String(v ?? '').trim()
1371
+ if (!s) return s
1372
+ // height="300" 这类无单位数字字符串需补 px,否则高度不生效、虚拟滚动失效
1373
+ if (/^\d+(\.\d+)?$/.test(s)) return `${s}px`
1374
+ return s
1375
+ }
1376
+
1377
+ const resolvedTreeProps = computed(() => ({
1378
+ children: props.treeProps?.children || 'children',
1379
+ hasChildren: props.treeProps?.hasChildren || 'hasChildren',
1380
+ }))
1381
+
1382
+ const configColumns = computed(() => {
1383
+ const walk = (cols: QiuTableColumnConfig[]): QiuTableColumnConfig[] =>
1384
+ cols.map((col) => ({
1385
+ ...col,
1386
+ label: col.label || col.title,
1387
+ align: col.align || props.align || 'left',
1388
+ headerAlign: col.headerAlign || props.headerAlign || col.align || props.align || 'left',
1389
+ footerAlign: col.footerAlign || props.footerAlign || col.align || props.align || 'left',
1390
+ children: col.children?.length ? walk(col.children) : undefined,
1391
+ }))
1392
+ return walk(props.columns || [])
1393
+ })
1394
+
1395
+ const slotColumnsNormalized = computed((): LeafColumn[] =>
1396
+ registeredColumns.value.map((reg, i) => {
1397
+ const p = reg.props
1398
+ const fixed = p.fixed === true ? ('left' as const) : p.fixed || undefined
1399
+ return {
1400
+ label: p.label || p.title || '',
1401
+ title: p.title,
1402
+ prop: p.prop,
1403
+ field: p.field,
1404
+ width: p.width,
1405
+ minWidth: p.minWidth,
1406
+ maxWidth: p.maxWidth,
1407
+ fixed,
1408
+ sortable: p.sortable,
1409
+ filterable: p.filterable,
1410
+ align: p.align || props.align || 'left',
1411
+ headerAlign: p.headerAlign || props.headerAlign,
1412
+ footerAlign: p.footerAlign || props.footerAlign,
1413
+ editor: p.editor,
1414
+ editorProps: p.editorProps,
1415
+ options: p.options,
1416
+ formatter: p.formatter,
1417
+ editable: p.editable,
1418
+ filters: p.filters,
1419
+ filterMethod: p.filterMethod,
1420
+ type: p.type,
1421
+ treeNode: p.treeNode,
1422
+ dragSort: p.dragSort,
1423
+ showOverflowTooltip: p.showOverflowTooltip,
1424
+ className: p.className,
1425
+ headerClassName: p.headerClassName || p.labelClassName,
1426
+ children: p.children,
1427
+ aggFunc: p.aggFunc,
1428
+ _key: p.columnKey || p.prop || p.field || `slot-col-${i}`,
1429
+ _defaultSlot: reg.slots.default,
1430
+ _headerSlot: reg.slots.header,
1431
+ }
1432
+ })
1433
+ )
1434
+
1435
+ const mergedSourceColumns = computed((): QiuTableColumnConfig[] => {
1436
+ const fromSlots = slotColumnsNormalized.value.filter(
1437
+ (c) => c.type !== 'selection' && c.type !== 'index' && c.type !== 'seq'
1438
+ )
1439
+ return [...configColumns.value, ...fromSlots]
1440
+ })
1441
+
1442
+ const allLeafColumns = computed((): LeafColumn[] => {
1443
+ const leaves = flattenLeafColumns(mergedSourceColumns.value)
1444
+ return leaves.filter(
1445
+ (c) =>
1446
+ c.type !== 'selection' &&
1447
+ c.type !== 'checkbox' &&
1448
+ c.type !== 'index' &&
1449
+ c.type !== 'seq' &&
1450
+ c.type !== 'radio' &&
1451
+ c.type !== 'expand'
1452
+ )
1453
+ })
1454
+
1455
+ const leafColumns = computed((): LeafColumn[] => {
1456
+ const order = columnOrderKeys.value
1457
+ let leaves = allLeafColumns.value.filter((c) => {
1458
+ if (c.visible === false) return false
1459
+ if (columnVisibleMap[c._key] === false) return false
1460
+ return true
1461
+ })
1462
+ if (order.length) {
1463
+ const map = new Map(leaves.map((c) => [c._key, c]))
1464
+ const ordered: LeafColumn[] = []
1465
+ order.forEach((k) => {
1466
+ const col = map.get(k)
1467
+ if (col) {
1468
+ ordered.push(col)
1469
+ map.delete(k)
1470
+ }
1471
+ })
1472
+ map.forEach((c) => ordered.push(c))
1473
+ leaves = ordered
1474
+ }
1475
+ return leaves
1476
+ })
1477
+
1478
+ watch(
1479
+ allLeafColumns,
1480
+ (cols) => {
1481
+ const keys = cols.map((c) => c._key)
1482
+ if (!columnOrderKeys.value.length) {
1483
+ columnOrderKeys.value = keys
1484
+ return
1485
+ }
1486
+ const set = new Set(columnOrderKeys.value)
1487
+ keys.forEach((k) => {
1488
+ if (!set.has(k)) columnOrderKeys.value.push(k)
1489
+ })
1490
+ columnOrderKeys.value = columnOrderKeys.value.filter((k) => keys.includes(k))
1491
+ },
1492
+ { immediate: true }
1493
+ )
1494
+
1495
+ const customizableColumns = computed(() =>
1496
+ allLeafColumns.value.filter((c) => {
1497
+ if (props.customConfig?.checkMethod) return props.customConfig.checkMethod(c)
1498
+ return true
1499
+ })
1500
+ )
1501
+
1502
+ const customEnabled = computed(() => props.customConfig?.enabled === true)
1503
+
1504
+ const resolvedToolbarConfig = computed((): Required<QiuTableToolbarConfig> => {
1505
+ if (props.toolbarConfig === false) {
1506
+ return {
1507
+ enabled: false,
1508
+ refresh: false,
1509
+ export: false,
1510
+ import: false,
1511
+ print: false,
1512
+ custom: false,
1513
+ zoom: false,
1514
+ copy: false,
1515
+ toolsPlacement: 'right',
1516
+ customTools: [],
1517
+ }
1518
+ }
1519
+ const cfg = (props.toolbarConfig && typeof props.toolbarConfig === 'object'
1520
+ ? props.toolbarConfig
1521
+ : {}) as QiuTableToolbarConfig
1522
+ return {
1523
+ enabled: cfg.enabled !== false,
1524
+ refresh: cfg.refresh !== false,
1525
+ export: cfg.export !== false,
1526
+ import: cfg.import !== false,
1527
+ print: cfg.print !== false,
1528
+ custom: cfg.custom !== false,
1529
+ zoom: cfg.zoom !== false,
1530
+ copy: cfg.copy === true,
1531
+ toolsPlacement: cfg.toolsPlacement || 'right',
1532
+ customTools: cfg.customTools || [],
1533
+ }
1534
+ })
1535
+
1536
+ const slots = useSlots()
1537
+ const refreshEnabled = computed(() => resolvedToolbarConfig.value.refresh && props.showRefresh !== false)
1538
+ const exportEnabled = computed(() => resolvedToolbarConfig.value.export && props.showExport !== false)
1539
+ const importEnabled = computed(() => resolvedToolbarConfig.value.import && props.showImport === true)
1540
+ const printEnabled = computed(() => resolvedToolbarConfig.value.print && props.showPrint === true)
1541
+ const zoomEnabled = computed(() => resolvedToolbarConfig.value.zoom && props.showFullscreen !== false)
1542
+ const copyEnabled = computed(
1543
+ () => resolvedToolbarConfig.value.copy || (props.showCopy === true && props.toolbarConfig !== false)
1544
+ )
1545
+ const hasToolbarCustom = computed(
1546
+ () =>
1547
+ !!slots.toolbar ||
1548
+ !!slots['toolbar-left'] ||
1549
+ !!slots['toolbar-center'] ||
1550
+ !!slots['toolbar-right'] ||
1551
+ resolvedToolbarConfig.value.customTools.length > 0
1552
+ )
1553
+ const hasAnyToolbarContent = computed(
1554
+ () =>
1555
+ refreshEnabled.value ||
1556
+ exportEnabled.value ||
1557
+ importEnabled.value ||
1558
+ printEnabled.value ||
1559
+ copyEnabled.value ||
1560
+ zoomEnabled.value ||
1561
+ customEnabled.value ||
1562
+ hasToolbarCustom.value
1563
+ )
1564
+ const showToolbarArea = computed(
1565
+ () => props.showToolbar !== false && (props.showToolbar === true || hasAnyToolbarContent.value)
1566
+ )
1567
+ const toolsPlacement = computed(() => resolvedToolbarConfig.value.toolsPlacement)
1568
+
1569
+ const customToolsByPosition = (position: 'left' | 'center' | 'right') =>
1570
+ resolvedToolbarConfig.value.customTools
1571
+ .filter((t) => (t.position || 'right') === position)
1572
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
1573
+
1574
+ const toggleColumnVisible = (key: string, visible: boolean) => {
1575
+ columnVisibleMap[key] = visible
1576
+ }
1577
+
1578
+ const columnDragEnabled = computed(() => props.columnDragConfig?.enabled === true)
1579
+
1580
+ let colSortableInst: Sortable | null = null
1581
+ const initColumnSortable = () => {
1582
+ colSortableInst?.destroy()
1583
+ colSortableInst = null
1584
+ if (!columnDragEnabled.value) return
1585
+ nextTick(() => {
1586
+ const row = headerWrapRef.value?.querySelector('thead tr:last-child') as HTMLElement | null
1587
+ if (!row) return
1588
+ colSortableInst = Sortable.create(row, {
1589
+ draggable: 'th.qiu-table__cell--col-drag',
1590
+ animation: 150,
1591
+ onEnd: (evt) => {
1592
+ const keys = Array.from(row.querySelectorAll('th[data-col-key]'))
1593
+ .map((el) => el.getAttribute('data-col-key') || '')
1594
+ .filter(Boolean)
1595
+ if (keys.length) {
1596
+ columnOrderKeys.value = keys
1597
+ emit('column-dragend', {
1598
+ oldIndex: evt.oldIndex ?? 0,
1599
+ newIndex: evt.newIndex ?? 0,
1600
+ columnKeys: keys,
1601
+ })
1602
+ }
1603
+ },
1604
+ })
1605
+ })
1606
+ }
1607
+
1608
+ watch(
1609
+ () => [columnDragEnabled.value, leafColumns.value.map((c) => c._key).join(',')],
1610
+ () => initColumnSortable()
1611
+ )
1612
+
1613
+ const resolvedShowRadio = computed(
1614
+ () =>
1615
+ props.showRadio || mergedSourceColumns.value.some((c) => c.type === 'radio')
1616
+ )
1617
+
1618
+ const resolvedShowExpand = computed(
1619
+ () =>
1620
+ props.showExpand || mergedSourceColumns.value.some((c) => c.type === 'expand')
1621
+ )
1622
+
1623
+ const fixedLeftOffset = ( whichtype: 'radio' | 'selection' | 'index') => {
1624
+ let left = 0
1625
+ if (resolvedShowExpand.value) left += 48
1626
+ if (whichtype === 'radio') return left
1627
+ if (resolvedShowRadio.value) left += 48
1628
+ if (whichtype === 'selection') return left
1629
+ if (resolvedShowSelection.value) left += 48
1630
+ return left
1631
+ }
1632
+
1633
+ const hasFixedLeftDataCol = computed(() =>
1634
+ leafColumns.value.some((c) => c.fixed === 'left' || c.fixed === true)
1635
+ )
1636
+
1637
+ /** 无数据左固定列时,特殊列最后一列需要阴影 */
1638
+ const lastSpecialLeftKind = computed(() => {
1639
+ if (hasFixedLeftDataCol.value) return null
1640
+ if (resolvedShowIndex.value) return 'index'
1641
+ if (resolvedShowSelection.value) return 'selection'
1642
+ if (resolvedShowRadio.value) return 'radio'
1643
+ if (resolvedShowExpand.value) return 'expand'
1644
+ return null
1645
+ })
1646
+
1647
+ const specialLeftLastClass = (kind: 'expand' | 'radio' | 'selection' | 'index') => ({
1648
+ 'qiu-table__cell--fixed-left-last': lastSpecialLeftKind.value === kind,
1649
+ })
1650
+
1651
+ const headerSourceColumns = computed(() => {
1652
+ const visibleKeys = new Set(leafColumns.value.map((c) => c._key))
1653
+ const walk = (cols: QiuTableColumnConfig[], prefix = ''): QiuTableColumnConfig[] => {
1654
+ const out: QiuTableColumnConfig[] = []
1655
+ cols.forEach((col, i) => {
1656
+ if (
1657
+ col.type === 'selection' ||
1658
+ col.type === 'checkbox' ||
1659
+ col.type === 'index' ||
1660
+ col.type === 'seq'
1661
+ ) {
1662
+ return
1663
+ }
1664
+ const key = makeColumnKey(col, i, prefix)
1665
+ if (col.children?.length) {
1666
+ const children = walk(col.children, key)
1667
+ if (children.length) out.push({ ...col, children })
1668
+ return
1669
+ }
1670
+ if (visibleKeys.has(key)) out.push(col)
1671
+ })
1672
+ return out
1673
+ }
1674
+ return walk(mergedSourceColumns.value)
1675
+ })
1676
+
1677
+ const headerRows = computed(() => {
1678
+ if (virtualXEnabled.value) {
1679
+ return [
1680
+ displayLeafColumns.value.map((col, i) => ({
1681
+ column: col,
1682
+ label: col.label || '',
1683
+ colspan: 1,
1684
+ rowspan: 1,
1685
+ isLeaf: true,
1686
+ colIndex: i,
1687
+ })),
1688
+ ]
1689
+ }
1690
+ const rows = buildHeaderRows(headerSourceColumns.value)
1691
+ const base = rows.length ? rows : [[]]
1692
+ return applyMergeHeaderCells(base, props.mergeHeaderCells)
1693
+ })
1694
+
1695
+ const resolvedShowSelection = computed(
1696
+ () =>
1697
+ props.showSelection ||
1698
+ mergedSourceColumns.value.some((c) => c.type === 'selection' || c.type === 'checkbox')
1699
+ )
1700
+
1701
+ const resolvedShowIndex = computed(
1702
+ () =>
1703
+ props.showIndex ||
1704
+ mergedSourceColumns.value.some((c) => c.type === 'index' || c.type === 'seq')
1705
+ )
1706
+
1707
+ const renderWidthMap = computed(() => {
1708
+ void layoutTick.value
1709
+ for (const k of Object.keys(columnWidths)) void columnWidths[k]
1710
+ const containerWidth = bodyWrapRef.value?.clientWidth || 0
1711
+ return computeRenderWidths(leafColumns.value, {
1712
+ containerWidth,
1713
+ showSelection: resolvedShowSelection.value,
1714
+ showIndex: resolvedShowIndex.value,
1715
+ showRadio: resolvedShowRadio.value,
1716
+ showExpand: resolvedShowExpand.value,
1717
+ fit: props.fit !== false,
1718
+ resized: columnWidths,
1719
+ defaults: columnWidthDefaults.value,
1720
+ })
1721
+ })
1722
+
1723
+ const tableMinWidthStyle = computed(() => {
1724
+ // 依赖拖拽列宽与布局 tick,保证三张表总宽始终一致
1725
+ void layoutTick.value
1726
+ for (const k of Object.keys(columnWidths)) void columnWidths[k]
1727
+ const total = Math.max(
1728
+ 1,
1729
+ computeTableMinWidth(
1730
+ leafColumns.value,
1731
+ {
1732
+ showSelection: resolvedShowSelection.value,
1733
+ showIndex: resolvedShowIndex.value,
1734
+ showRadio: resolvedShowRadio.value,
1735
+ showExpand: resolvedShowExpand.value,
1736
+ },
1737
+ (col) => renderWidthMap.value[col._key] || getWidth(col._key),
1738
+ columnWidthDefaults.value
1739
+ )
1740
+ )
1741
+ // 100% 铺满窄容器;minWidth 保证列宽之和,超出时由 body-wrap 横向滚动
1742
+ return {
1743
+ width: '100%',
1744
+ minWidth: `${total}px`,
1745
+ }
1746
+ })
1747
+
1748
+ const rootSizeStyle = computed(() => {
1749
+ const style: Record<string, string> = {}
1750
+ if (props.height != null) {
1751
+ style.height = normalizeSize(props.height)
1752
+ style.minHeight = '0'
1753
+ }
1754
+ if (props.maxHeight != null) {
1755
+ style.maxHeight = normalizeSize(props.maxHeight)
1756
+ style.minHeight = '0'
1757
+ }
1758
+ if (props.minHeight != null) {
1759
+ style.minHeight = normalizeSize(props.minHeight)
1760
+ }
1761
+ if (props.rowConfig?.height != null) {
1762
+ style['--qiu-table-row-height'] = `${props.rowConfig.height}px`
1763
+ }
1764
+ return style
1765
+ })
1766
+
1767
+ /** 含纵向滚动条占位,供表头/表尾补齐右侧竖线 */
1768
+ const rootTableStyle = computed(() => ({
1769
+ ...rootSizeStyle.value,
1770
+ '--qiu-table-v-scrollbar': `${scrollbarSize.value}px`,
1771
+ }))
1772
+
1773
+ /** main 在固定高度模式下吃掉剩余空间,自身不再写死 height,避免和 toolbar/pager 叠高 */
1774
+ const mainStyle = computed(() => {
1775
+ const style: Record<string, string> = {}
1776
+ if (props.rowConfig?.height != null) {
1777
+ style['--qiu-table-row-height'] = `${props.rowConfig.height}px`
1778
+ }
1779
+ return style
1780
+ })
1781
+
1782
+ const virtualXEnabled = computed(() => {
1783
+ const c = props.virtualXConfig
1784
+ if (c === true) return true
1785
+ if (c && typeof c === 'object') return c.enabled !== false
1786
+ return false
1787
+ })
1788
+
1789
+ const colWidthOf = (col: LeafColumn) =>
1790
+ renderWidthMap.value[col._key] ||
1791
+ getWidth(col._key) ||
1792
+ parseWidth(col.width) ||
1793
+ parseWidth(col.minWidth) ||
1794
+ columnWidthDefaults.value.minWidth ||
1795
+ 100
1796
+
1797
+ const virtualXPad = computed(() => {
1798
+ void layoutTick.value
1799
+ if (!virtualXEnabled.value) return { left: 0, right: 0, start: 0, end: leafColumns.value.length }
1800
+ const cols = leafColumns.value
1801
+ const viewW = bodyWrapRef.value?.clientWidth || 800
1802
+ let acc = 0
1803
+ let start = 0
1804
+ const x = Math.max(0, scrollLeftX.value)
1805
+ for (let i = 0; i < cols.length; i++) {
1806
+ const w = colWidthOf(cols[i])
1807
+ if (acc + w >= x - 120) {
1808
+ start = i
1809
+ break
1810
+ }
1811
+ acc += w
1812
+ start = i + 1
1813
+ }
1814
+ let end = start
1815
+ let used = 0
1816
+ for (let i = start; i < cols.length; i++) {
1817
+ used += colWidthOf(cols[i])
1818
+ end = i + 1
1819
+ if (used > viewW + 240) break
1820
+ }
1821
+ let left = 0
1822
+ for (let i = 0; i < start; i++) left += colWidthOf(cols[i])
1823
+ let right = 0
1824
+ for (let i = end; i < cols.length; i++) right += colWidthOf(cols[i])
1825
+ return { left, right, start, end }
1826
+ })
1827
+
1828
+ const displayLeafColumns = computed(() => {
1829
+ if (!virtualXEnabled.value) return leafColumns.value
1830
+ return leafColumns.value.slice(virtualXPad.value.start, virtualXPad.value.end)
1831
+ })
1832
+
1833
+ const realColIndex = (col: LeafColumn) => leafColumns.value.findIndex((c) => c._key === col._key)
1834
+
1835
+ const totalColspan = computed(() => {
1836
+ let n = displayLeafColumns.value.length
1837
+ if (resolvedShowExpand.value) n += 1
1838
+ if (resolvedShowRadio.value) n += 1
1839
+ if (resolvedShowSelection.value) n += 1
1840
+ if (resolvedShowIndex.value) n += 1
1841
+ if (virtualXEnabled.value && virtualXPad.value.left > 0) n += 1
1842
+ if (virtualXEnabled.value && virtualXPad.value.right > 0) n += 1
1843
+ return Math.max(n, 1)
1844
+ })
1845
+
1846
+ const pagerEnabled = computed(() => {
1847
+ if (props.pagerConfig === true) return true
1848
+ if (props.pagerConfig && typeof props.pagerConfig === 'object') {
1849
+ return props.pagerConfig.enabled !== false
1850
+ }
1851
+ return false
1852
+ })
1853
+
1854
+ const pagerConfig = computed(() =>
1855
+ typeof props.pagerConfig === 'object' ? props.pagerConfig : undefined
1856
+ )
1857
+
1858
+ watch(
1859
+ () => pagerConfig.value,
1860
+ (cfg) => {
1861
+ if (cfg?.currentPage) innerPage.value = cfg.currentPage
1862
+ if (cfg?.pageSize) innerPageSize.value = cfg.pageSize
1863
+ },
1864
+ { immediate: true }
1865
+ )
1866
+
1867
+ const dragEnabled = computed(
1868
+ () =>
1869
+ props.dragSort ||
1870
+ !!props.gridOptions?.rowConfig?.drag ||
1871
+ leafColumns.value.some((c) => c.dragSort)
1872
+ )
1873
+
1874
+ const getChildren = (row: any) => row?.[resolvedTreeProps.value.children] || []
1875
+ const hasRowChildren = (row: any) => {
1876
+ const children = getChildren(row)
1877
+ if (children.length) return true
1878
+ return !!row?.[resolvedTreeProps.value.hasChildren]
1879
+ }
1880
+
1881
+ const walkRows = (rows: any[], fn: (row: any) => void) => {
1882
+ const childrenKey = resolvedTreeProps.value.children
1883
+ for (const row of rows || []) {
1884
+ fn(row)
1885
+ if (row?.[childrenKey]?.length) walkRows(row[childrenKey], fn)
1886
+ }
1887
+ }
1888
+
1889
+ const initExpandState = () => {
1890
+ if (!props.tree) return
1891
+ const next = new Set<string>()
1892
+ if (props.defaultExpandAll) {
1893
+ walkRows(props.data, (row) => {
1894
+ if (hasRowChildren(row)) next.add(getRowId(row))
1895
+ })
1896
+ }
1897
+ expandedKeys.value = next
1898
+ expandInitialized = true
1899
+ }
1900
+
1901
+ const isExpanded = (row: any) => expandedKeys.value.has(getRowId(row))
1902
+ const toggleExpand = (row: any) => {
1903
+ const id = getRowId(row)
1904
+ const next = new Set(expandedKeys.value)
1905
+ if (next.has(id)) next.delete(id)
1906
+ else next.add(id)
1907
+ expandedKeys.value = next
1908
+ }
1909
+
1910
+ const flattenRows = (rows: any[], level = 0, acc: QiuTableFlatRow[] = []): QiuTableFlatRow[] => {
1911
+ for (const row of rows || []) {
1912
+ const hasChildren = hasRowChildren(row)
1913
+ const expanded = isExpanded(row)
1914
+ acc.push({ row, level, hasChildren, expanded, flatIndex: acc.length })
1915
+ if (hasChildren && expanded) flattenRows(getChildren(row), level + 1, acc)
1916
+ }
1917
+ return acc
1918
+ }
1919
+
1920
+ const rawFlatRows = computed(() => {
1921
+ if (props.tree) return flattenRows(props.data)
1922
+ return (props.data || []).map((row, i) => ({
1923
+ row,
1924
+ level: 0,
1925
+ hasChildren: false,
1926
+ expanded: false,
1927
+ flatIndex: i,
1928
+ }))
1929
+ })
1930
+
1931
+ const formatCell = (row: any, col: QiuTableColumnConfig) => {
1932
+ const prop = resolveColumnProp(col)
1933
+ const value = prop != null ? row?.[prop] : undefined
1934
+ if (col.formatter) return String(col.formatter(row, col, value) ?? '')
1935
+ if (col.format && QiuTableFormats.has(col.format)) {
1936
+ return QiuTableFormats.get(col.format)!({ cellValue: value, row, column: col })
1937
+ }
1938
+ const er = getEditRender(col)
1939
+ const options = er?.options || col.options
1940
+ const editor = er?.name || getColEditor(col)
1941
+ if (editor === 'select' && options?.length) {
1942
+ const hit = options.find((o) => o.value === value)
1943
+ return String(hit?.label ?? value ?? '')
1944
+ }
1945
+ if (value == null) return ''
1946
+ return String(value)
1947
+ }
1948
+
1949
+ const isFilterable = (col: LeafColumn) => !!col.filterable || !!(col.filters && col.filters.length)
1950
+
1951
+ const getFilterOptions = (col: LeafColumn): FilterOption[] => {
1952
+ if (col.filters?.length) return col.filters
1953
+ const prop = getProp(col)
1954
+ if (!prop || !col.filterable) return []
1955
+ const seen = new Set<string>()
1956
+ const opts: FilterOption[] = []
1957
+ for (const { row } of rawFlatRows.value) {
1958
+ const v = row?.[prop]
1959
+ const key = String(v)
1960
+ if (seen.has(key)) continue
1961
+ seen.add(key)
1962
+ opts.push({ text: formatCell(row, col) || key, value: v })
1963
+ }
1964
+ return opts
1965
+ }
1966
+
1967
+ const isFilterActive = (col: LeafColumn) => (filterValues[col._key]?.length ?? 0) > 0
1968
+
1969
+ const openFilterOptions = computed(() => {
1970
+ const key = openFilterKey.value
1971
+ if (!key) return [] as FilterOption[]
1972
+ const col = leafColumns.value.find((c) => c._key === key)
1973
+ return col ? getFilterOptions(col) : []
1974
+ })
1975
+
1976
+ const applyFilters = (rows: QiuTableFlatRow[]) => {
1977
+ if (props.filterConfig?.remote) return rows
1978
+ const active = leafColumns.value.filter((c) => (filterValues[c._key]?.length ?? 0) > 0)
1979
+ if (!active.length) return rows
1980
+ return rows.filter(({ row }) =>
1981
+ active.every((col) => {
1982
+ const vals = filterValues[col._key] || []
1983
+ if (col.filterMethod) return vals.some((v) => col.filterMethod!(v, row, col))
1984
+ const prop = getProp(col)
1985
+ if (!prop) return true
1986
+ return vals.some((v) => row[prop] === v)
1987
+ })
1988
+ )
1989
+ }
1990
+
1991
+ const applySort = (rows: QiuTableFlatRow[]) => {
1992
+ if (props.sortConfig?.remote) return rows
1993
+ const sorts =
1994
+ props.sortConfig?.multiple && sortList.value.length
1995
+ ? sortList.value
1996
+ : sortState.value.prop && sortState.value.order
1997
+ ? [{ prop: sortState.value.prop, order: sortState.value.order }]
1998
+ : []
1999
+ if (!sorts.length) return rows
2000
+ const sorted = [...rows]
2001
+ sorted.sort((a, b) => compareRows(a.row, b.row, sorts))
2002
+ return sorted.map((r, i) => ({ ...r, flatIndex: i }))
2003
+ }
2004
+
2005
+ const visibleRows = computed(() => applySort(applyFilters(rawFlatRows.value)))
2006
+
2007
+ const pagerTotal = computed(() => pagerConfig.value?.total ?? visibleRows.value.length)
2008
+
2009
+ const pageCount = computed(() => Math.max(1, Math.ceil(pagerTotal.value / innerPageSize.value)))
2010
+
2011
+ const pagedRows = computed(() => {
2012
+ if (!pagerEnabled.value) return visibleRows.value
2013
+ if (pagerConfig.value?.total != null) {
2014
+ return visibleRows.value.map((r, i) => ({ ...r, flatIndex: i }))
2015
+ }
2016
+ const start = (innerPage.value - 1) * innerPageSize.value
2017
+ return visibleRows.value
2018
+ .slice(start, start + innerPageSize.value)
2019
+ .map((r, i) => ({ ...r, flatIndex: start + i }))
2020
+ })
2021
+
2022
+ const virtualConfig = computed(() => {
2023
+ if (props.virtualYConfig != null) return props.virtualYConfig
2024
+ if (props.virtualY) return true
2025
+ return false
2026
+ })
2027
+
2028
+ const {
2029
+ enabled: virtualEnabled,
2030
+ range: virtualRange,
2031
+ onScroll: onVirtualScroll,
2032
+ measure: measureVirtual,
2033
+ } = useVirtualY({
2034
+ bodyWrapRef,
2035
+ rows: () => pagedRows.value,
2036
+ config: () => virtualConfig.value,
2037
+ defaultRowHeight: props.rowConfig?.height || 48,
2038
+ })
2039
+
2040
+ const displayRowItems = computed(() => {
2041
+ const rows = pagedRows.value
2042
+ if (!virtualEnabled.value) {
2043
+ return rows.map((flat, rowIndex) => ({ flat, rowIndex }))
2044
+ }
2045
+ const { start, end } = virtualRange.value
2046
+ return rows.slice(start, end).map((flat, i) => ({
2047
+ flat,
2048
+ rowIndex: start + i,
2049
+ }))
2050
+ })
2051
+
2052
+ const onBodyScroll = () => {
2053
+ syncFromBody()
2054
+ onVirtualScroll()
2055
+ scrollLeftX.value = bodyWrapRef.value?.scrollLeft || 0
2056
+ updateHasScrollX()
2057
+ }
2058
+
2059
+ const buildDefaultFooter = (): string[][] => {
2060
+ // 必须用当前页数据:翻页后合计/平均应随之变化
2061
+ const data = pagedRows.value.map((f) => f.row)
2062
+ const sumRow: string[] = []
2063
+ const avgRow: string[] = []
2064
+
2065
+ leafColumns.value.forEach((col, i) => {
2066
+ const prop = getProp(col)
2067
+ if (i === 0) {
2068
+ sumRow.push(translate('合计'))
2069
+ avgRow.push(translate('平均'))
2070
+ return
2071
+ }
2072
+
2073
+ const nums = data
2074
+ .map((r) => Number(prop != null ? r[prop] : NaN))
2075
+ .filter((n) => !Number.isNaN(n))
2076
+
2077
+ const fmtMoney = (n: number) =>
2078
+ `¥ ${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
2079
+
2080
+ if (col.aggFunc === 'count') {
2081
+ sumRow.push(String(data.length))
2082
+ avgRow.push('')
2083
+ return
2084
+ }
2085
+
2086
+ if (col.aggFunc === 'sum' || col.aggFunc === 'avg' || (nums.length > 0 && nums.length === data.length)) {
2087
+ const s = nums.reduce((a, b) => a + b, 0)
2088
+ const avg = nums.length ? s / nums.length : 0
2089
+ const useMoney = col.aggFunc != null || prop === 'jan' || prop === 'feb'
2090
+ if (col.aggFunc === 'avg') {
2091
+ sumRow.push('')
2092
+ avgRow.push(useMoney ? fmtMoney(avg) : avg.toFixed(2))
2093
+ return
2094
+ }
2095
+ sumRow.push(useMoney ? fmtMoney(s) : String(Number.isInteger(s) ? s : Number(s.toFixed(2))))
2096
+ avgRow.push(useMoney ? fmtMoney(avg) : avg.toFixed(2))
2097
+ return
2098
+ }
2099
+
2100
+ sumRow.push('')
2101
+ avgRow.push('')
2102
+ })
2103
+
2104
+ return [sumRow, avgRow]
2105
+ }
2106
+
2107
+ const footerRows = computed(() => {
2108
+ if (!props.showFooter) return [] as string[][]
2109
+ if (props.footerData?.length) return props.footerData
2110
+ const pageData = pagedRows.value.map((f) => f.row)
2111
+ if (props.footerMethod) {
2112
+ return props.footerMethod({
2113
+ columns: leafColumns.value,
2114
+ data: pageData,
2115
+ })
2116
+ }
2117
+ return buildDefaultFooter()
2118
+ })
2119
+
2120
+ const mapSlotScope = (row: any, col: QiuTableColumnConfig, index: number, level = 0) => ({
2121
+ row,
2122
+ column: col,
2123
+ $index: index,
2124
+ cellValue: getProp(col) != null ? row?.[getProp(col)!] : undefined,
2125
+ level,
2126
+ })
2127
+
2128
+ const editorSize = computed(() => props.size || 'small')
2129
+
2130
+ const toDateInputValue = (val: any, datetime: boolean) => {
2131
+ if (!val) return ''
2132
+ const s = String(val)
2133
+ if (datetime) return s.replace(' ', 'T').slice(0, 16)
2134
+ return s.slice(0, 10)
2135
+ }
2136
+
2137
+ const renderEditor = (row: any, col: LeafColumn) => {
2138
+ const prop = getProp(col)!
2139
+ const val = row[prop]
2140
+ const onDone = (v: any) => commitEdit(row, col, v)
2141
+ const scheduleBlurCommit = (v: any) => {
2142
+ // 延后到下一帧,避开挂载/聚焦瞬间的伪 blur,以及下拉面板抢焦点
2143
+ requestAnimationFrame(() => {
2144
+ if (inEditGuard()) return
2145
+ if (!isEditing(row, col)) return
2146
+ onDone(v)
2147
+ })
2148
+ }
2149
+ const editRender = getEditRender(col) || resolveEditRender(col)
2150
+ const editor = editRender?.name || getColEditor(col)
2151
+ const editorProps = {
2152
+ ...(col.editorProps || {}),
2153
+ ...(editRender?.props || {}),
2154
+ ...(editRender?.attrs || {}),
2155
+ }
2156
+ const editorOptions = editRender?.options || col.options || []
2157
+ const bindEvents = (base: Record<string, any>) => {
2158
+ const ev = editRender?.events
2159
+ if (!ev) return base
2160
+ const next = { ...base }
2161
+ Object.keys(ev).forEach((k) => {
2162
+ const userFn = ev[k]
2163
+ const prev = next[k]
2164
+ next[k] = (...args: any[]) => {
2165
+ userFn?.(...args)
2166
+ if (typeof prev === 'function') prev(...args)
2167
+ }
2168
+ })
2169
+ return next
2170
+ }
2171
+ const fill = editor === 'input'
2172
+
2173
+ // 自定义 editRender
2174
+ if (editor && QiuTableEditRenderer.has(editor)) {
2175
+ const vnode = QiuTableEditRenderer.get(editor)!({
2176
+ cellValue: val,
2177
+ row,
2178
+ column: col,
2179
+ editRender: editRender!,
2180
+ commit: onDone,
2181
+ update: (v) => {
2182
+ row[prop] = v
2183
+ },
2184
+ })
2185
+ if (vnode) return vnode
2186
+ }
2187
+
2188
+ if (editor === 'switch') {
2189
+ return h(
2190
+ QiuSwitch,
2191
+ bindEvents({
2192
+ modelValue: !!val,
2193
+ size: editorSize.value === 'large' ? 'default' : 'small',
2194
+ ...editorProps,
2195
+ 'onUpdate:modelValue': onDone,
2196
+ onChange: onDone,
2197
+ })
2198
+ )
2199
+ }
2200
+ if (editor === 'select') {
2201
+ return h(
2202
+ QiuSelect,
2203
+ bindEvents({
2204
+ modelValue: val,
2205
+ options: editorOptions,
2206
+ size: editorSize.value,
2207
+ fit: true,
2208
+ ...editorProps,
2209
+ 'onUpdate:modelValue': (v: any) => {
2210
+ row[prop] = v
2211
+ },
2212
+ onChange: onDone,
2213
+ onVisibleChange: (open: boolean) => {
2214
+ if (!open) onDone(row[prop])
2215
+ },
2216
+ })
2217
+ )
2218
+ }
2219
+ if (editor === 'number') {
2220
+ return h(
2221
+ QiuInputNumber,
2222
+ bindEvents({
2223
+ modelValue: val,
2224
+ size: editorSize.value,
2225
+ controlsPosition: 'right',
2226
+ style: { width: '110px', maxWidth: '100%' },
2227
+ ...editorProps,
2228
+ 'onUpdate:modelValue': (v: number | null) => {
2229
+ row[prop] = v
2230
+ },
2231
+ onChange: onDone,
2232
+ onBlur: () => scheduleBlurCommit(row[prop]),
2233
+ })
2234
+ )
2235
+ }
2236
+ if (editor === 'date' || editor === 'datetime') {
2237
+ return h(
2238
+ 'input',
2239
+ bindEvents({
2240
+ class: 'qiu-table__date-input',
2241
+ type: editor === 'datetime' ? 'datetime-local' : 'date',
2242
+ value: toDateInputValue(val, editor === 'datetime'),
2243
+ ...editorProps,
2244
+ onChange: (e: Event) => onDone((e.target as HTMLInputElement).value),
2245
+ onKeydown: (e: KeyboardEvent) => {
2246
+ if (e.key === 'Enter') {
2247
+ e.preventDefault()
2248
+ e.stopPropagation()
2249
+ onDone((e.target as HTMLInputElement).value)
2250
+ }
2251
+ },
2252
+ onBlur: (e: FocusEvent) => scheduleBlurCommit((e.target as HTMLInputElement).value),
2253
+ })
2254
+ )
2255
+ }
2256
+ if (editor === 'time') {
2257
+ return h(
2258
+ QiuTime,
2259
+ bindEvents({
2260
+ modelValue: val ?? null,
2261
+ size: editorSize.value,
2262
+ fit: true,
2263
+ format: (editorProps as any)?.format || 'HH:mm:ss',
2264
+ ...editorProps,
2265
+ 'onUpdate:modelValue': (v: string | null) => {
2266
+ row[prop] = v
2267
+ },
2268
+ onChange: onDone,
2269
+ onVisibleChange: (open: boolean) => {
2270
+ if (!open && row[prop] != null) onDone(row[prop])
2271
+ },
2272
+ })
2273
+ )
2274
+ }
2275
+ return h(
2276
+ QiuInput,
2277
+ bindEvents({
2278
+ modelValue: val ?? '',
2279
+ size: fill ? undefined : editorSize.value,
2280
+ class: fill ? 'qiu-table__input-fill' : undefined,
2281
+ style: fill
2282
+ ? { width: '100%', height: '100%', borderRadius: '0' }
2283
+ : { width: '100%' },
2284
+ ...editorProps,
2285
+ 'onUpdate:modelValue': (v: string | number) => {
2286
+ row[prop] = v
2287
+ },
2288
+ onKeydown: (e: KeyboardEvent) => {
2289
+ if (e.key === 'Enter') {
2290
+ e.preventDefault()
2291
+ e.stopPropagation()
2292
+ onDone(row[prop])
2293
+ } else if (e.key === 'Escape') {
2294
+ e.preventDefault()
2295
+ e.stopPropagation()
2296
+ clearEdit()
2297
+ }
2298
+ },
2299
+ onBlur: () => scheduleBlurCommit(row[prop]),
2300
+ })
2301
+ )
2302
+ }
2303
+
2304
+ const onCellClick = (row: any, col: QiuTableColumnConfig, e?: MouseEvent) => {
2305
+ setActive(row, col, !!e?.shiftKey)
2306
+ onRowClick(row)
2307
+ if (!props.editable) return
2308
+ const trigger = getTrigger()
2309
+ if (trigger === 'click') startEdit(row, col, bodyWrapRef.value)
2310
+ }
2311
+
2312
+ const onCellDblclick = (row: any, col: QiuTableColumnConfig) => {
2313
+ setActive(row, col)
2314
+ if (!props.editable) return
2315
+ const trigger = getTrigger()
2316
+ if (trigger === 'dblclick') startEdit(row, col, bodyWrapRef.value)
2317
+ }
2318
+
2319
+ const isRowSelected = (row: any) =>
2320
+ selectionRows.value.some((r) => getRowId(r) === getRowId(row))
2321
+
2322
+ const toggleRowSelection = (row: any, checked: boolean) => {
2323
+ const id = getRowId(row)
2324
+ let next = selectionRows.value.slice()
2325
+ if (checked) {
2326
+ if (!next.some((r) => getRowId(r) === id)) next.push(row)
2327
+ } else next = next.filter((r) => getRowId(r) !== id)
2328
+ selectionRows.value = next
2329
+ emit('selection-change', next)
2330
+ emit('checkbox-change', { records: next, row, checked })
2331
+ }
2332
+
2333
+ const isAllSelected = computed(() => {
2334
+ const rows = pagedRows.value.map((f) => f.row)
2335
+ return rows.length > 0 && rows.every((r) => isRowSelected(r))
2336
+ })
2337
+
2338
+ const isIndeterminate = computed(() => {
2339
+ const rows = pagedRows.value.map((f) => f.row)
2340
+ const count = rows.filter((r) => isRowSelected(r)).length
2341
+ return count > 0 && count < rows.length
2342
+ })
2343
+
2344
+ const toggleSelectAll = (checked: boolean) => {
2345
+ const rows = pagedRows.value.map((f) => f.row)
2346
+ selectionRows.value = checked ? rows.slice() : []
2347
+ emit('selection-change', selectionRows.value)
2348
+ emit('checkbox-all', { records: selectionRows.value, checked })
2349
+ }
2350
+
2351
+ const onHeaderClick = (col: LeafColumn, e?: MouseEvent) => {
2352
+ if (!col.sortable || col.sortable === 'custom') return
2353
+ const prop = getProp(col)
2354
+ if (!prop) return
2355
+ const multi = !!props.sortConfig?.multiple && !!(e?.shiftKey || e?.ctrlKey || e?.metaKey)
2356
+
2357
+ if (multi) {
2358
+ const idx = sortList.value.findIndex((s) => s.prop === prop)
2359
+ if (idx < 0) sortList.value = [...sortList.value, { prop, order: 'asc' }]
2360
+ else if (sortList.value[idx].order === 'asc') {
2361
+ const next = sortList.value.slice()
2362
+ next[idx] = { prop, order: 'desc' }
2363
+ sortList.value = next
2364
+ } else {
2365
+ sortList.value = sortList.value.filter((s) => s.prop !== prop)
2366
+ }
2367
+ const last = sortList.value[sortList.value.length - 1]
2368
+ sortState.value = last
2369
+ ? { prop: last.prop, order: last.order }
2370
+ : { prop: '', order: null }
2371
+ emit('sort-change', {
2372
+ prop: sortState.value.prop,
2373
+ order: sortState.value.order,
2374
+ sorts: sortList.value.slice(),
2375
+ } as any)
2376
+ return
2377
+ }
2378
+
2379
+ sortList.value = []
2380
+ if (sortState.value.prop !== prop) sortState.value = { prop, order: 'asc' }
2381
+ else if (sortState.value.order === 'asc') sortState.value = { prop, order: 'desc' }
2382
+ else sortState.value = { prop: '', order: null }
2383
+ if (sortState.value.prop && sortState.value.order) {
2384
+ sortList.value = [{ prop: sortState.value.prop, order: sortState.value.order }]
2385
+ }
2386
+ emit('sort-change', { ...sortState.value, sorts: sortList.value.slice() } as any)
2387
+ }
2388
+
2389
+ const sortPriority = (col: LeafColumn) => {
2390
+ if (!props.sortConfig?.multiple || sortList.value.length < 2) return 0
2391
+ const prop = getProp(col)
2392
+ const idx = sortList.value.findIndex((s) => s.prop === prop)
2393
+ return idx >= 0 ? idx + 1 : 0
2394
+ }
2395
+
2396
+ const sortClass = (col: LeafColumn) => {
2397
+ const prop = getProp(col)
2398
+ if (props.sortConfig?.multiple && sortList.value.length) {
2399
+ const hit = sortList.value.find((s) => s.prop === prop)
2400
+ if (!hit) return ''
2401
+ return hit.order === 'asc' ? 'is-asc' : 'is-desc'
2402
+ }
2403
+ if (sortState.value.prop !== prop) return ''
2404
+ return sortState.value.order === 'asc' ? 'is-asc' : 'is-desc'
2405
+ }
2406
+
2407
+ const onRowClick = (row: any) => {
2408
+ if (!resolveRowCurrent(props.rowConfig)) return
2409
+ currentRowId.value = getRowId(row)
2410
+ emit('current-change', { row })
2411
+ }
2412
+
2413
+ const mergeCovered = computed(() => buildMergeCoveredSet(props.mergeCells))
2414
+
2415
+ const cellSpan = (
2416
+ flat: QiuTableFlatRow,
2417
+ rowIndex: number,
2418
+ col: LeafColumn,
2419
+ columnIndex: number
2420
+ ) =>
2421
+ resolveCellSpan({
2422
+ row: flat.row,
2423
+ column: col,
2424
+ rowIndex,
2425
+ columnIndex,
2426
+ data: pagedRows.value.map((f) => f.row),
2427
+ spanMethod: props.spanMethod,
2428
+ mergeCells: props.mergeCells,
2429
+ covered: mergeCovered.value,
2430
+ })
2431
+
2432
+ const isColResizable = (col: LeafColumn) => {
2433
+ if (col.resizable === false) return false
2434
+ if (col.resizable === true) return true
2435
+ return !!props.columnConfig?.resizable
2436
+ }
2437
+
2438
+ const resolvedColStyle = (col: LeafColumn) => {
2439
+ void columnWidths[col._key]
2440
+ const w = colWidthOf(col)
2441
+ const hints = getColumnSizeHints(col, bodyWrapRef.value?.clientWidth || 0, columnWidthDefaults.value)
2442
+ return {
2443
+ width: `${w}px`,
2444
+ minWidth: `${hints.isFlex ? hints.minWidth : w}px`,
2445
+ ...(hints.maxWidth < Number.POSITIVE_INFINITY ? { maxWidth: `${hints.maxWidth}px` } : {}),
2446
+ }
2447
+ }
2448
+
2449
+ const onColResizeStart = (col: LeafColumn, e: MouseEvent) => {
2450
+ const cur = colWidthOf(col)
2451
+ const hints = getColumnSizeHints(col, bodyWrapRef.value?.clientWidth || 0, columnWidthDefaults.value)
2452
+ onResizeStart(col._key, cur, e, {
2453
+ minWidth: hints.minWidth,
2454
+ maxWidth: hints.maxWidth < Number.POSITIVE_INFINITY ? hints.maxWidth : undefined,
2455
+ })
2456
+ const onUp = () => {
2457
+ emit('resizable-change', {
2458
+ column: col,
2459
+ width: columnWidths[col._key] || cur,
2460
+ })
2461
+ document.removeEventListener('mouseup', onUp)
2462
+ }
2463
+ document.addEventListener('mouseup', onUp)
2464
+ }
2465
+
2466
+ const toggleFilterPanel = (col: LeafColumn, e: MouseEvent) => {
2467
+ if (openFilterKey.value === col._key) {
2468
+ closeFilterPanel()
2469
+ return
2470
+ }
2471
+ filterDraft[col._key] = [...(filterValues[col._key] || [])]
2472
+ openFilterKey.value = col._key
2473
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
2474
+ filterPanelPos.value = {
2475
+ left: Math.min(rect.left, window.innerWidth - 220),
2476
+ top: rect.bottom + 6,
2477
+ }
2478
+ }
2479
+
2480
+ const closeFilterPanel = () => {
2481
+ openFilterKey.value = null
2482
+ filterPanelPos.value = null
2483
+ }
2484
+
2485
+ const isFilterOptionChecked = (key: string, value: any) =>
2486
+ (filterDraft[key] || []).some((v) => v === value)
2487
+
2488
+ const toggleFilterOption = (key: string, value: any, checked: boolean) => {
2489
+ const cur = [...(filterDraft[key] || [])]
2490
+ const idx = cur.findIndex((v) => v === value)
2491
+ if (checked && idx < 0) cur.push(value)
2492
+ if (!checked && idx >= 0) cur.splice(idx, 1)
2493
+ filterDraft[key] = cur
2494
+ }
2495
+
2496
+ const isFilterAllChecked = computed(() => {
2497
+ const key = openFilterKey.value
2498
+ if (!key || !openFilterOptions.value.length) return false
2499
+ return openFilterOptions.value.every((opt) => isFilterOptionChecked(key, opt.value))
2500
+ })
2501
+
2502
+ const isFilterIndeterminate = computed(() => {
2503
+ const key = openFilterKey.value
2504
+ if (!key || !openFilterOptions.value.length) return false
2505
+ const n = (filterDraft[key] || []).length
2506
+ return n > 0 && n < openFilterOptions.value.length
2507
+ })
2508
+
2509
+ const toggleFilterAll = (checked: boolean) => {
2510
+ const key = openFilterKey.value
2511
+ if (!key) return
2512
+ filterDraft[key] = checked ? openFilterOptions.value.map((o) => o.value) : []
2513
+ }
2514
+
2515
+ const confirmFilter = () => {
2516
+ const key = openFilterKey.value
2517
+ if (key) {
2518
+ filterValues[key] = [...(filterDraft[key] || [])]
2519
+ emit('filter-change', { ...filterValues })
2520
+ }
2521
+ closeFilterPanel()
2522
+ }
2523
+
2524
+ const resetOpenFilter = () => {
2525
+ const key = openFilterKey.value
2526
+ if (key) {
2527
+ filterDraft[key] = []
2528
+ filterValues[key] = []
2529
+ emit('filter-change', { ...filterValues })
2530
+ }
2531
+ closeFilterPanel()
2532
+ }
2533
+
2534
+ const headerCellStyle = (cell: { column: LeafColumn; isLeaf: boolean }) => {
2535
+ const col = cell.column
2536
+ // 分组表头不写 width,避免 colspan 单元格与 colgroup 抢宽度导致错线
2537
+ const style: Record<string, string> = cell.isLeaf ? {} : {}
2538
+ if (!cell.isLeaf) return style
2539
+ if (col.fixed === 'left' || col.fixed === true) {
2540
+ style.left = `${leftOffsetForColumn(
2541
+ leafColumns.value,
2542
+ col._key,
2543
+ {
2544
+ showSelection: resolvedShowSelection.value,
2545
+ showIndex: resolvedShowIndex.value,
2546
+ showRadio: resolvedShowRadio.value,
2547
+ showExpand: resolvedShowExpand.value,
2548
+ },
2549
+ colWidthOf
2550
+ )}px`
2551
+ }
2552
+ if (col.fixed === 'right') {
2553
+ style.right = `${rightOffsetForColumn(leafColumns.value, col._key, colWidthOf)}px`
2554
+ }
2555
+ return style
2556
+ }
2557
+
2558
+ const bodyCellStyle = (col?: LeafColumn) => {
2559
+ if (!col) return {}
2560
+ // 列宽只走 colgroup,避免 th/td 与 col 双重指定在缩放后分叉
2561
+ const style: Record<string, string> = {}
2562
+ if (col.fixed === 'left' || col.fixed === true) {
2563
+ style.left = `${leftOffsetForColumn(
2564
+ leafColumns.value,
2565
+ col._key,
2566
+ {
2567
+ showSelection: resolvedShowSelection.value,
2568
+ showIndex: resolvedShowIndex.value,
2569
+ showRadio: resolvedShowRadio.value,
2570
+ showExpand: resolvedShowExpand.value,
2571
+ },
2572
+ colWidthOf
2573
+ )}px`
2574
+ }
2575
+ if (col.fixed === 'right') {
2576
+ style.right = `${rightOffsetForColumn(leafColumns.value, col._key, colWidthOf)}px`
2577
+ }
2578
+ return style
2579
+ }
2580
+
2581
+ const fixedCellClass = (col?: LeafColumn) => {
2582
+ if (!col) return {}
2583
+ const isLeft = col.fixed === 'left' || col.fixed === true
2584
+ const isRight = col.fixed === 'right'
2585
+ return {
2586
+ 'qiu-table__cell--fixed-left': isLeft,
2587
+ 'qiu-table__cell--fixed-right': isRight,
2588
+ 'qiu-table__cell--fixed-left-last':
2589
+ isLeft && isLastFixedLeft(leafColumns.value, col._key),
2590
+ 'qiu-table__cell--fixed-right-first':
2591
+ isRight && isFirstFixedRight(leafColumns.value, col._key),
2592
+ }
2593
+ }
2594
+
2595
+ const toggleFullscreen = async () => {
2596
+ isFullscreen.value = !isFullscreen.value
2597
+ await nextTick()
2598
+ scheduleLayout()
2599
+ }
2600
+
2601
+ const onPageChange = (page: number) => {
2602
+ innerPage.value = page
2603
+ emit('page-change', { currentPage: page, pageSize: innerPageSize.value })
2604
+ }
2605
+
2606
+ const onSizeChange = (size: number) => {
2607
+ innerPageSize.value = size
2608
+ innerPage.value = 1
2609
+ emit('page-change', { currentPage: 1, pageSize: size })
2610
+ }
2611
+
2612
+ const setAllTreeExpand = (checked: boolean) => {
2613
+ const next = new Set<string>()
2614
+ if (checked) {
2615
+ walkRows(props.data, (row) => {
2616
+ if (hasRowChildren(row)) next.add(getRowId(row))
2617
+ })
2618
+ }
2619
+ expandedKeys.value = next
2620
+ }
2621
+
2622
+ const getCheckboxRecords = () => selectionRows.value.slice()
2623
+
2624
+ const exportableColumns = computed(() =>
2625
+ leafColumns.value.filter((c) => getProp(c) && !c.dragSort && c.type !== 'selection' && c.type !== 'index' && c.type !== 'seq' && c.type !== 'radio' && c.type !== 'expand')
2626
+ )
2627
+
2628
+ const exportAllChecked = computed(
2629
+ () =>
2630
+ exportableColumns.value.length > 0 &&
2631
+ exportableColumns.value.every((c) => exportFieldKeys.value.includes(c._key))
2632
+ )
2633
+
2634
+ const exportIndeterminate = computed(() => {
2635
+ const n = exportFieldKeys.value.length
2636
+ return n > 0 && n < exportableColumns.value.length
2637
+ })
2638
+
2639
+ const openExportDialog = () => {
2640
+ const cfg = props.exportConfig
2641
+ const all = exportableColumns.value.filter((c) =>
2642
+ cfg?.columnFilterMethod ? cfg.columnFilterMethod({ column: c }) !== false : true
2643
+ )
2644
+ exportFieldKeys.value = all.map((c) => c._key)
2645
+ exportFormat.value = (cfg?.type as QiuTableExportFormat) || 'xlsx'
2646
+ exportMode.value = cfg?.mode || 'all'
2647
+ exportStartPage.value = Math.max(1, cfg?.startPage ?? innerPage.value)
2648
+ exportEndPage.value = Math.max(
2649
+ exportStartPage.value,
2650
+ Math.min(pageCount.value, cfg?.endPage ?? pageCount.value)
2651
+ )
2652
+ exportDialogVisible.value = true
2653
+ }
2654
+
2655
+ const toggleExportAll = (checked: boolean) => {
2656
+ exportFieldKeys.value = checked ? exportableColumns.value.map((c) => c._key) : []
2657
+ }
2658
+
2659
+ const toggleExportField = (key: string, checked: boolean) => {
2660
+ if (checked) {
2661
+ if (!exportFieldKeys.value.includes(key)) exportFieldKeys.value = [...exportFieldKeys.value, key]
2662
+ } else {
2663
+ exportFieldKeys.value = exportFieldKeys.value.filter((k) => k !== key)
2664
+ }
2665
+ }
2666
+
2667
+ const collectExportRows = (mode: 'current' | 'all' | 'selected' | 'range') => {
2668
+ if (mode === 'selected') return selectionRows.value.slice()
2669
+ if (mode === 'current') return pagedRows.value.map((f) => f.row)
2670
+ if (mode === 'range') {
2671
+ const start = Math.max(0, (exportStartPage.value - 1) * innerPageSize.value)
2672
+ const end = exportEndPage.value * innerPageSize.value
2673
+ return visibleRows.value.slice(start, end).map((r) => r.row)
2674
+ }
2675
+ const flat: any[] = []
2676
+ walkRows(props.data, (row) => flat.push(row))
2677
+ return flat
2678
+ }
2679
+
2680
+ const confirmExport = async () => {
2681
+ const cfg = { ...(props.exportConfig || {}) }
2682
+ cfg.type = exportFormat.value
2683
+ if (cfg.beforeExportMethod) {
2684
+ const ok = await cfg.beforeExportMethod({ options: cfg })
2685
+ if (ok === false) return
2686
+ }
2687
+ const cols = exportableColumns.value.filter((c) => exportFieldKeys.value.includes(c._key))
2688
+ if (!cols.length) return
2689
+ cfg.mode = exportMode.value || 'all'
2690
+ cfg.startPage = exportStartPage.value
2691
+ cfg.endPage = exportEndPage.value
2692
+ const rows = collectExportRows(cfg.mode)
2693
+ const matrix = buildSheetMatrix(cols, rows, (row, col) => formatCell(row, col))
2694
+ const base = cfg.filename || 'export'
2695
+ await exportSheet({
2696
+ format: exportFormat.value,
2697
+ filename: base,
2698
+ matrix,
2699
+ })
2700
+ exportDialogVisible.value = false
2701
+ cfg.afterExportMethod?.({ options: cfg })
2702
+ }
2703
+
2704
+ const exportData = async (opts?: {
2705
+ filename?: string
2706
+ format?: QiuTableExportFormat
2707
+ columns?: string[]
2708
+ mode?: 'current' | 'all' | 'selected' | 'range'
2709
+ startPage?: number
2710
+ endPage?: number
2711
+ }) => {
2712
+ const cfg = { ...(props.exportConfig || {}), ...opts }
2713
+ if (cfg.beforeExportMethod) {
2714
+ const ok = await cfg.beforeExportMethod({ options: cfg as QiuTableExportConfig })
2715
+ if (ok === false) return
2716
+ }
2717
+ const all = exportableColumns.value.filter((c) =>
2718
+ cfg.columnFilterMethod ? cfg.columnFilterMethod({ column: c }) !== false : true
2719
+ )
2720
+ const cols = opts?.columns?.length
2721
+ ? all.filter((c) => opts.columns!.includes(c._key) || opts.columns!.includes(getProp(c) || ''))
2722
+ : all
2723
+ if (!cols.length) {
2724
+ openExportDialog()
2725
+ return
2726
+ }
2727
+ const exportModeValue = opts?.mode || cfg.mode || 'all'
2728
+ if (exportModeValue === 'range') {
2729
+ exportStartPage.value = Math.max(1, opts?.startPage ?? cfg.startPage ?? innerPage.value)
2730
+ exportEndPage.value = Math.max(
2731
+ exportStartPage.value,
2732
+ Math.min(pageCount.value, opts?.endPage ?? cfg.endPage ?? pageCount.value)
2733
+ )
2734
+ }
2735
+ const rows = collectExportRows(exportModeValue)
2736
+ const matrix = buildSheetMatrix(cols, rows, (row, col) => formatCell(row, col))
2737
+ await exportSheet({
2738
+ format: (opts?.format || cfg.type || 'xlsx') as QiuTableExportFormat,
2739
+ filename: opts?.filename || cfg.filename || 'export',
2740
+ matrix,
2741
+ })
2742
+ cfg.afterExportMethod?.({ options: cfg as QiuTableExportConfig })
2743
+ }
2744
+
2745
+ const triggerImport = () => importInputRef.value?.click()
2746
+
2747
+ const onImportFile = async (e: Event) => {
2748
+ const file = (e.target as HTMLInputElement).files?.[0]
2749
+ ;(e.target as HTMLInputElement).value = ''
2750
+ if (!file) return
2751
+ try {
2752
+ const rows = await parseImportFile(file)
2753
+ const objects = csvToObjects(rows, leafColumns.value)
2754
+ emit('import-data', { rows: objects })
2755
+ } catch (err) {
2756
+ console.error(err)
2757
+ }
2758
+ }
2759
+
2760
+ const escapePrintHtml = (s: any) =>
2761
+ String(s ?? '')
2762
+ .replace(/&/g, '&amp;')
2763
+ .replace(/</g, '&lt;')
2764
+ .replace(/>/g, '&gt;')
2765
+ .replace(/"/g, '&quot;')
2766
+
2767
+ /** 构建可打印表格:含完整表头;thead 在每页重复 */
2768
+ const printTable = () => {
2769
+ const cols = leafColumns.value
2770
+ const dataRows = visibleRows.value
2771
+ const built = buildHeaderRows(headerSourceColumns.value)
2772
+ const headRows = built.filter((r) => r.length)
2773
+ const depth = Math.max(headRows.length, 1)
2774
+
2775
+ const specialTh = (label = '') =>
2776
+ `<th class="qiu-print-special" rowspan="${depth}">${escapePrintHtml(label)}</th>`
2777
+
2778
+ let theadHtml = '<thead>'
2779
+ if (headRows.length) {
2780
+ headRows.forEach((row, ri) => {
2781
+ theadHtml += '<tr>'
2782
+ if (ri === 0) {
2783
+ if (resolvedShowExpand.value) theadHtml += specialTh('')
2784
+ if (resolvedShowRadio.value) theadHtml += specialTh('')
2785
+ if (resolvedShowSelection.value) theadHtml += specialTh('')
2786
+ if (resolvedShowIndex.value) theadHtml += specialTh(translate('序号'))
2787
+ }
2788
+ row.forEach((cell) => {
2789
+ theadHtml += `<th colspan="${cell.colspan || 1}" rowspan="${cell.rowspan || 1}">${escapePrintHtml(cell.label)}</th>`
2790
+ })
2791
+ theadHtml += '</tr>'
2792
+ })
2793
+ } else {
2794
+ theadHtml += '<tr>'
2795
+ if (resolvedShowExpand.value) theadHtml += specialTh('')
2796
+ if (resolvedShowRadio.value) theadHtml += specialTh('')
2797
+ if (resolvedShowSelection.value) theadHtml += specialTh('')
2798
+ if (resolvedShowIndex.value) theadHtml += specialTh(translate('序号'))
2799
+ cols.forEach((col) => {
2800
+ theadHtml += `<th>${escapePrintHtml(col.label || '')}</th>`
2801
+ })
2802
+ theadHtml += '</tr>'
2803
+ }
2804
+ theadHtml += '</thead>'
2805
+
2806
+ let tbodyHtml = '<tbody>'
2807
+ dataRows.forEach((item, i) => {
2808
+ tbodyHtml += '<tr>'
2809
+ if (resolvedShowExpand.value) tbodyHtml += '<td></td>'
2810
+ if (resolvedShowRadio.value) tbodyHtml += '<td></td>'
2811
+ if (resolvedShowSelection.value) tbodyHtml += '<td></td>'
2812
+ if (resolvedShowIndex.value) tbodyHtml += `<td>${item.flatIndex + 1}</td>`
2813
+ cols.forEach((col) => {
2814
+ const align = col.align || 'left'
2815
+ tbodyHtml += `<td style="text-align:${align}">${escapePrintHtml(formatCell(item.row, col))}</td>`
2816
+ })
2817
+ tbodyHtml += '</tr>'
2818
+ })
2819
+ tbodyHtml += '</tbody>'
2820
+
2821
+ let tfootHtml = ''
2822
+ if (props.showFooter && footerRows.value.length) {
2823
+ tfootHtml = '<tfoot>'
2824
+ footerRows.value.forEach((frow) => {
2825
+ tfootHtml += '<tr>'
2826
+ if (resolvedShowExpand.value) tfootHtml += '<td></td>'
2827
+ if (resolvedShowRadio.value) tfootHtml += '<td></td>'
2828
+ if (resolvedShowSelection.value) tfootHtml += '<td></td>'
2829
+ if (resolvedShowIndex.value) tfootHtml += '<td></td>'
2830
+ frow.forEach((text, ci) => {
2831
+ const align = cols[ci]?.align || 'left'
2832
+ tfootHtml += `<td style="text-align:${align}">${escapePrintHtml(text)}</td>`
2833
+ })
2834
+ tfootHtml += '</tr>'
2835
+ })
2836
+ tfootHtml += '</tfoot>'
2837
+ }
2838
+
2839
+ const win = window.open('', '_blank')
2840
+ if (!win) return
2841
+ win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"/><title>${escapePrintHtml(translate('打印'))}</title>
2842
+ <style>
2843
+ @page { margin: 12mm; }
2844
+ * { box-sizing: border-box; }
2845
+ body { margin: 0; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; color: #303133; }
2846
+ table.qiu-print-table {
2847
+ width: 100%;
2848
+ border-collapse: collapse;
2849
+ table-layout: auto;
2850
+ font-size: 12px;
2851
+ }
2852
+ .qiu-print-table th,
2853
+ .qiu-print-table td {
2854
+ border: 1px solid #dcdfe6;
2855
+ padding: 6px 8px;
2856
+ vertical-align: middle;
2857
+ word-break: break-word;
2858
+ }
2859
+ .qiu-print-table thead {
2860
+ display: table-header-group; /* 每一打印页重复表头 */
2861
+ }
2862
+ .qiu-print-table tfoot {
2863
+ display: table-footer-group;
2864
+ }
2865
+ .qiu-print-table tr {
2866
+ page-break-inside: avoid;
2867
+ }
2868
+ .qiu-print-table th {
2869
+ background: #f5f7fa !important;
2870
+ font-weight: 600;
2871
+ text-align: left;
2872
+ -webkit-print-color-adjust: exact;
2873
+ print-color-adjust: exact;
2874
+ }
2875
+ .qiu-print-table tfoot td {
2876
+ background: #fafafa !important;
2877
+ font-weight: 600;
2878
+ -webkit-print-color-adjust: exact;
2879
+ print-color-adjust: exact;
2880
+ }
2881
+ @media print {
2882
+ .qiu-print-table thead { display: table-header-group; }
2883
+ .qiu-print-table tfoot { display: table-footer-group; }
2884
+ }
2885
+ </style></head><body>
2886
+ <table class="qiu-print-table">${theadHtml}${tbodyHtml}${tfootHtml}</table>
2887
+ </body></html>`)
2888
+ win.document.close()
2889
+ win.focus()
2890
+ // 等样式/字体就绪再打
2891
+ setTimeout(() => {
2892
+ win.print()
2893
+ }, 50)
2894
+ }
2895
+
2896
+ const toolButtonList = computed(() => [
2897
+ {
2898
+ name: 'refresh',
2899
+ show: refreshEnabled.value,
2900
+ title: translate('刷新'),
2901
+ icon: 'refresh-line',
2902
+ onClick: () => emit('refresh'),
2903
+ },
2904
+ {
2905
+ name: 'zoom',
2906
+ show: zoomEnabled.value,
2907
+ title: translate(isFullscreen.value ? '退出全屏' : '全屏'),
2908
+ icon: isFullscreen.value ? 'fullscreen-exit-line' : 'fullscreen-line',
2909
+ onClick: toggleFullscreen,
2910
+ },
2911
+ {
2912
+ name: 'import',
2913
+ show: importEnabled.value,
2914
+ title: translate('导入'),
2915
+ icon: 'upload-2-line',
2916
+ onClick: triggerImport,
2917
+ },
2918
+ {
2919
+ name: 'print',
2920
+ show: printEnabled.value,
2921
+ title: translate('打印'),
2922
+ icon: 'printer-line',
2923
+ onClick: printTable,
2924
+ },
2925
+ {
2926
+ name: 'custom',
2927
+ show: customEnabled.value,
2928
+ title: translate('列设置'),
2929
+ icon: 'settings-3-line',
2930
+ onClick: () => (customPanelOpen.value = !customPanelOpen.value),
2931
+ },
2932
+ {
2933
+ name: 'export',
2934
+ show: exportEnabled.value || !!props.exportConfig,
2935
+ title: translate('导出'),
2936
+ icon: 'download-2-line',
2937
+ onClick: openExportDialog,
2938
+ },
2939
+ ])
2940
+
2941
+ const isSwitchColumn = (col: LeafColumn) => {
2942
+ if (col.editor === 'switch' || col.type === 'switch') return true
2943
+ if ((col.editRender as any)?.name === 'switch' || (col.cellRender as any)?.name === 'switch') return true
2944
+ return false
2945
+ }
2946
+
2947
+ const copyToClipboard = async (text: string) => {
2948
+ if (navigator.clipboard && window.isSecureContext) {
2949
+ await navigator.clipboard.writeText(text)
2950
+ return
2951
+ }
2952
+ const textarea = document.createElement('textarea')
2953
+ textarea.value = text
2954
+ textarea.style.position = 'fixed'
2955
+ textarea.style.opacity = '0'
2956
+ document.body.appendChild(textarea)
2957
+ textarea.select()
2958
+ try {
2959
+ document.execCommand('copy')
2960
+ } finally {
2961
+ textarea.remove()
2962
+ }
2963
+ }
2964
+
2965
+ const copyTableData = async (
2966
+ direction: 'vertical' | 'horizontal',
2967
+ scope: 'all' | 'selected' = 'selected'
2968
+ ) => {
2969
+ let cols = leafColumns.value
2970
+ let rows = visibleRows.value.map((f) => f.row)
2971
+ const bounds = areaBounds.value
2972
+ if (scope === 'selected' && bounds) {
2973
+ cols = cols.slice(bounds.c0, bounds.c1 + 1)
2974
+ rows = rows.slice(bounds.r0, bounds.r1 + 1)
2975
+ }
2976
+ if (!cols.length || !rows.length) {
2977
+ qiuMsg.warning(translate('没有可复制的数据'))
2978
+ return
2979
+ }
2980
+ if (direction === 'horizontal' && cols.some(isSwitchColumn)) {
2981
+ qiuMsg.warning(translate('横向复制暂不支持包含开关/自定义渲染的列'))
2982
+ return
2983
+ }
2984
+ const escapeCell = (v: any) => String(v ?? '').replace(/\t/g, ' ').replace(/\r?\n/g, ' ')
2985
+ const cellText = (row: any, col: LeafColumn) => escapeCell(formatCell(row, col))
2986
+ const headerText = (col: LeafColumn) => escapeCell(col.label || col.title || col.field || col._key)
2987
+ let text = ''
2988
+ if (direction === 'vertical') {
2989
+ const lines: string[] = [cols.map(headerText).join('\t')]
2990
+ rows.forEach((row) => lines.push(cols.map((col) => cellText(row, col)).join('\t')))
2991
+ text = lines.join('\n')
2992
+ } else {
2993
+ const matrix: string[][] = []
2994
+ matrix.push([''].concat(rows.map((_, i) => String(i + 1))))
2995
+ cols.forEach((col) => {
2996
+ matrix.push([headerText(col)].concat(rows.map((row) => cellText(row, col))))
2997
+ })
2998
+ text = matrix.map((line) => line.join('\t')).join('\n')
2999
+ }
3000
+ try {
3001
+ await copyToClipboard(text)
3002
+ qiuMsg.success(translate('已复制到剪贴板'))
3003
+ } catch {
3004
+ qiuMsg.error(translate('复制失败,请检查浏览器剪贴板权限'))
3005
+ }
3006
+ }
3007
+
3008
+ const closeCtxMenu = () => {
3009
+ ctxMenu.visible = false
3010
+ }
3011
+
3012
+ const onBodyContextMenu = (e: MouseEvent) => {
3013
+ if (props.menuConfig?.enabled === false) return
3014
+ const options = props.menuConfig?.body?.options
3015
+ if (!options?.length) return
3016
+ const tr = (e.target as HTMLElement).closest('tr') as HTMLElement | null
3017
+ const rowKey = tr?.getAttribute('data-row-key') || ''
3018
+ const row = rowKey ? findRowById(props.data, rowKey) : undefined
3019
+ if (
3020
+ props.menuConfig?.visibleMethod &&
3021
+ props.menuConfig.visibleMethod({
3022
+ type: 'body',
3023
+ row,
3024
+ options,
3025
+ }) === false
3026
+ ) {
3027
+ return
3028
+ }
3029
+ ctxMenu.visible = true
3030
+ ctxMenu.x = e.clientX
3031
+ ctxMenu.y = e.clientY
3032
+ ctxMenu.options = options
3033
+ ctxMenu.row = row
3034
+ }
3035
+
3036
+ const onMenuItemClick = (item: QiuTableMenuItem) => {
3037
+ if (item.code === 'copy' || item.code === 'COPY') {
3038
+ void copyArea()
3039
+ } else if (item.code === 'cut' || item.code === 'CUT') {
3040
+ void cutArea()
3041
+ } else if (item.code === 'paste' || item.code === 'PASTE') {
3042
+ void pasteArea()
3043
+ }
3044
+ emit('menu-click', { menu: item, row: ctxMenu.row, column: ctxMenu.column })
3045
+ closeCtxMenu()
3046
+ }
3047
+
3048
+ const copyArea = async () => {
3049
+ if (clip().isCopy === false) return
3050
+ const text = buildTsv((row, prop) => {
3051
+ const col = leafColumns.value.find((c) => getProp(c) === prop)
3052
+ return col ? formatCell(row, col) : String(row?.[prop] ?? '')
3053
+ })
3054
+ if (!text) return
3055
+ try {
3056
+ await navigator.clipboard.writeText(text)
3057
+ } catch {
3058
+ // ignore
3059
+ }
3060
+ }
3061
+
3062
+ const applyClipboardValue = (row: any, prop: string, value: string) => {
3063
+ const col = leafColumns.value.find((c) => getProp(c) === prop)
3064
+ if (!col || !canEditCol(col)) {
3065
+ row[prop] = value
3066
+ return
3067
+ }
3068
+ void commitEdit(row, col, value)
3069
+ }
3070
+
3071
+ const cutArea = async () => {
3072
+ if (clip().isCut === false) return
3073
+ await copyArea()
3074
+ clearAreaValues((row, prop, value) => applyClipboardValue(row, prop, value))
3075
+ }
3076
+
3077
+ const pasteArea = async () => {
3078
+ if (clip().isPaste === false) return
3079
+ try {
3080
+ const text = await navigator.clipboard.readText()
3081
+ pasteTsv(text, (row, prop, value) => applyClipboardValue(row, prop, value))
3082
+ } catch {
3083
+ // ignore
3084
+ }
3085
+ }
3086
+
3087
+ const focusCurrentRow = (row: any) => {
3088
+ onRowClick(row)
3089
+ nextTick(() => scrollToRow(row))
3090
+ }
3091
+
3092
+ const onTableKeydown = (e: KeyboardEvent) => {
3093
+ const allowCurrent = resolveRowCurrent(props.rowConfig)
3094
+ const allowArrow = kb().isArrow !== false
3095
+ const allowEnter = kb().isEnter !== false
3096
+ const allowEsc = kb().isEsc !== false
3097
+ const allowTab = kb().isTab === true
3098
+ const allowUndo = kb().isUndo !== false && historyEnabled()
3099
+
3100
+ if (!props.editable && !allowCurrent && !allowUndo) return
3101
+
3102
+ const el = e.target as HTMLElement | null
3103
+ const tag = el?.tagName
3104
+ const inEditor =
3105
+ tag === 'INPUT' ||
3106
+ tag === 'TEXTAREA' ||
3107
+ tag === 'SELECT' ||
3108
+ !!el?.isContentEditable ||
3109
+ !!el?.closest?.('.qiu-input, .qiu-select, .qiu-input-number, .qiu-time, .qiu-time-panel')
3110
+
3111
+ const mod = e.ctrlKey || e.metaKey
3112
+
3113
+ if (mod && (e.key === 'z' || e.key === 'Z') && allowUndo && !e.shiftKey) {
3114
+ e.preventDefault()
3115
+ undoEdit()
3116
+ return
3117
+ }
3118
+ if (mod && ((e.key === 'y' || e.key === 'Y') || ((e.key === 'z' || e.key === 'Z') && e.shiftKey)) && allowUndo) {
3119
+ e.preventDefault()
3120
+ redoEdit()
3121
+ return
3122
+ }
3123
+ if (mod && (e.key === 'c' || e.key === 'C') && clip().isCopy !== false) {
3124
+ if (!inEditor) {
3125
+ e.preventDefault()
3126
+ void copyArea()
3127
+ }
3128
+ return
3129
+ }
3130
+ if (mod && (e.key === 'x' || e.key === 'X') && clip().isCut !== false) {
3131
+ if (!inEditor) {
3132
+ e.preventDefault()
3133
+ void cutArea()
3134
+ }
3135
+ return
3136
+ }
3137
+ if (mod && (e.key === 'v' || e.key === 'V') && clip().isPaste !== false) {
3138
+ if (!inEditor) {
3139
+ e.preventDefault()
3140
+ void pasteArea()
3141
+ }
3142
+ return
3143
+ }
3144
+
3145
+ if (inEditor && kb().isEdit !== true) return
3146
+
3147
+ const rows = pagedRows.value.map((f) => f.row)
3148
+ if (!rows.length) return
3149
+ const idx = currentRowId.value
3150
+ ? rows.findIndex((r) => getRowId(r) === currentRowId.value)
3151
+ : -1
3152
+ const extend = e.shiftKey
3153
+
3154
+ if (allowArrow && e.key === 'ArrowDown') {
3155
+ e.preventDefault()
3156
+ const moved = moveActive(1, 0, extend)
3157
+ if (moved) focusCurrentRow(moved.row)
3158
+ else if (allowCurrent) {
3159
+ const next = rows[Math.min(rows.length - 1, Math.max(0, idx + 1))]
3160
+ if (next) focusCurrentRow(next)
3161
+ }
3162
+ } else if (allowArrow && e.key === 'ArrowUp') {
3163
+ e.preventDefault()
3164
+ const moved = moveActive(-1, 0, extend)
3165
+ if (moved) focusCurrentRow(moved.row)
3166
+ else if (allowCurrent) {
3167
+ const prev = rows[Math.max(0, idx <= 0 ? 0 : idx - 1)]
3168
+ if (prev) focusCurrentRow(prev)
3169
+ }
3170
+ } else if (allowArrow && e.key === 'ArrowLeft') {
3171
+ e.preventDefault()
3172
+ const moved = moveActive(0, -1, extend)
3173
+ if (moved) focusCurrentRow(moved.row)
3174
+ } else if (allowArrow && e.key === 'ArrowRight') {
3175
+ e.preventDefault()
3176
+ const moved = moveActive(0, 1, extend)
3177
+ if (moved) focusCurrentRow(moved.row)
3178
+ } else if (allowTab && e.key === 'Tab') {
3179
+ e.preventDefault()
3180
+ const moved = moveActive(0, e.shiftKey ? -1 : 1, false)
3181
+ if (moved) focusCurrentRow(moved.row)
3182
+ } else if (allowEnter && e.key === 'Enter' && idx >= 0 && props.editable) {
3183
+ if (editing.value) return
3184
+ e.preventDefault()
3185
+ const row = rows[idx]
3186
+ const prop = activeCell.value?.prop
3187
+ const col =
3188
+ (prop && leafColumns.value.find((c) => getProp(c) === prop && canEditCol(c))) ||
3189
+ leafColumns.value.find((c) => canEditCol(c) && getColEditor(c) !== 'switch')
3190
+ if (row && col) startEdit(row, col, bodyWrapRef.value)
3191
+ } else if (allowEsc && e.key === 'Escape') {
3192
+ clearEdit()
3193
+ clearArea()
3194
+ closeCtxMenu()
3195
+ }
3196
+ }
3197
+
3198
+ const getGrid = () => ({
3199
+ getTableData: () => ({ fullData: props.data }),
3200
+ getCheckboxRecords,
3201
+ clearEdit,
3202
+ exportData,
3203
+ setAllTreeExpand,
3204
+ importData: triggerImport,
3205
+ print: printTable,
3206
+ })
3207
+
3208
+ const findRowById = (rows: any[], id: string): any | null => {
3209
+ const childrenKey = resolvedTreeProps.value.children
3210
+ for (const row of rows || []) {
3211
+ if (getRowId(row) === id) return row
3212
+ if (row?.[childrenKey]?.length) {
3213
+ const found = findRowById(row[childrenKey], id)
3214
+ if (found) return found
3215
+ }
3216
+ }
3217
+ return null
3218
+ }
3219
+
3220
+ const initSortable = () => {
3221
+ sortableInst?.destroy()
3222
+ sortableInst = null
3223
+ if (!dragEnabled.value) return
3224
+ nextTick(() => {
3225
+ const el = tbodyRef.value
3226
+ if (!el) return
3227
+ sortableInst = Sortable.create(el, {
3228
+ handle: '.qiu-table__drag-handle',
3229
+ animation: 150,
3230
+ draggable: '.qiu-table__row:not(.qiu-table__row--empty)',
3231
+ onEnd: (evt) => {
3232
+ const dragId = (evt.item as HTMLElement)?.getAttribute('data-row-key') || ''
3233
+ emit('row-dragend', {
3234
+ dragRow: findRowById(props.data, dragId),
3235
+ oldIndex: evt.oldIndex,
3236
+ newIndex: evt.newIndex,
3237
+ })
3238
+ },
3239
+ })
3240
+ })
3241
+ }
3242
+
3243
+ const registerContext: QiuTableRegisterContext = {
3244
+ register(col) {
3245
+ registeredColumns.value = [...registeredColumns.value.filter((c) => c.id !== col.id), col]
3246
+ },
3247
+ unregister(id) {
3248
+ registeredColumns.value = registeredColumns.value.filter((c) => c.id !== id)
3249
+ },
3250
+ update(col) {
3251
+ const idx = registeredColumns.value.findIndex((c) => c.id === col.id)
3252
+ if (idx >= 0) {
3253
+ const next = registeredColumns.value.slice()
3254
+ next[idx] = col
3255
+ registeredColumns.value = next
3256
+ } else registerContext.register(col)
3257
+ },
3258
+ }
3259
+
3260
+ provide(QIU_TABLE_REGISTER_KEY, registerContext)
3261
+
3262
+ watch(
3263
+ () => [props.tree, props.defaultExpandAll, props.data],
3264
+ () => {
3265
+ if (!expandInitialized || props.defaultExpandAll) initExpandState()
3266
+ },
3267
+ { deep: true, immediate: true }
3268
+ )
3269
+
3270
+ watch(
3271
+ () => [dragEnabled.value, props.data.length, pagedRows.value.length],
3272
+ () => initSortable()
3273
+ )
3274
+
3275
+ watch(
3276
+ () => [pagedRows.value.length, props.height, props.maxHeight, leafColumns.value.length],
3277
+ () => nextTick(scheduleLayout)
3278
+ )
3279
+
3280
+ onMounted(() => {
3281
+ initSortable()
3282
+ nextTick(() => {
3283
+ scheduleLayout()
3284
+ measureVirtual()
3285
+ updateHasScrollX()
3286
+ })
3287
+ })
3288
+
3289
+ onBeforeUnmount(() => {
3290
+ sortableInst?.destroy()
3291
+ sortableInst = null
3292
+ colSortableInst?.destroy()
3293
+ colSortableInst = null
3294
+ })
3295
+
3296
+ const CellContent = defineComponent({
3297
+ name: 'QiuTableCellContent',
3298
+ props: {
3299
+ row: { type: Object, required: true },
3300
+ col: { type: Object, required: true },
3301
+ index: { type: Number, required: true },
3302
+ },
3303
+ setup(cellProps) {
3304
+ return () => {
3305
+ const row = cellProps.row as any
3306
+ const col = cellProps.col as LeafColumn
3307
+ const index = cellProps.index as number
3308
+ const editor = getColEditor(col)
3309
+ const prop = getProp(col)
3310
+ const cellValue = prop != null ? row[prop] : undefined
3311
+
3312
+ if (hasAlwaysEditor(col) || isEditing(row, col)) {
3313
+ return renderEditor(row, col)
3314
+ }
3315
+
3316
+ const renderName =
3317
+ typeof col.cellRender === 'string' ? col.cellRender : col.cellRender?.name
3318
+ if (renderName && QiuTableRenderer.has(renderName)) {
3319
+ const vnode = QiuTableRenderer.get(renderName)!({
3320
+ cellValue,
3321
+ row,
3322
+ column: col,
3323
+ $index: index,
3324
+ })
3325
+ if (vnode == null) return null
3326
+ if (typeof vnode === 'string') return h('span', { class: 'qiu-table__text' }, vnode)
3327
+ return vnode
3328
+ }
3329
+
3330
+ let text = ''
3331
+ if (editor === 'select' && col.options?.length) {
3332
+ const hit = col.options.find((o) => o.value === cellValue)
3333
+ text = hit?.label ?? formatCell(row, col)
3334
+ } else {
3335
+ text = formatCell(row, col)
3336
+ }
3337
+
3338
+ const tip = resolveTooltipContent(row, col)
3339
+ const useTip =
3340
+ shouldEllipsis(col) &&
3341
+ tip &&
3342
+ !isEditing(row, col) &&
3343
+ props.tooltipConfig?.showAll !== false
3344
+
3345
+ if (useTip) {
3346
+ return h(
3347
+ QiuTooltip,
3348
+ {
3349
+ content: tip,
3350
+ placement: 'top',
3351
+ showAfter: props.tooltipConfig?.enterDelay ?? 200,
3352
+ hideAfter: props.tooltipConfig?.leaveDelay ?? 100,
3353
+ },
3354
+ {
3355
+ default: () => h('span', { class: 'qiu-table__text' }, text),
3356
+ }
3357
+ )
3358
+ }
3359
+ return h('span', { class: 'qiu-table__text' }, text)
3360
+ }
3361
+ },
3362
+ })
3363
+
3364
+ const resolveRowClassName = (row: any, rowIndex: number) => {
3365
+ const fn = props.rowClassName
3366
+ if (!fn) return ''
3367
+ return typeof fn === 'function' ? fn({ row, rowIndex }) : fn
3368
+ }
3369
+
3370
+ const resolveCellClassName = (
3371
+ row: any,
3372
+ column: QiuTableColumnConfig,
3373
+ rowIndex: number,
3374
+ columnIndex: number
3375
+ ) => {
3376
+ const fn = props.cellClassName
3377
+ if (!fn) return ''
3378
+ return typeof fn === 'function' ? fn({ row, column, rowIndex, columnIndex }) : fn
3379
+ }
3380
+
3381
+ const setCurrentRow = (row: any | null) => {
3382
+ if (row == null) {
3383
+ currentRowId.value = null
3384
+ emit('current-change', { row: null })
3385
+ return
3386
+ }
3387
+ if (!resolveRowCurrent(props.rowConfig)) return
3388
+ currentRowId.value = getRowId(row)
3389
+ emit('current-change', { row })
3390
+ }
3391
+
3392
+ const clearCurrentRow = () => {
3393
+ currentRowId.value = null
3394
+ clearArea()
3395
+ emit('current-change', { row: null })
3396
+ }
3397
+
3398
+ const clearSort = () => {
3399
+ sortState.value = { prop: '', order: null }
3400
+ sortList.value = []
3401
+ emit('sort-change', { prop: '', order: null, sorts: [] } as any)
3402
+ }
3403
+
3404
+ const clearFilter = (field?: string) => {
3405
+ if (field) {
3406
+ const col = leafColumns.value.find((c) => getProp(c) === field || c._key === field)
3407
+ const key = col?._key
3408
+ if (key) {
3409
+ filterValues[key] = []
3410
+ filterDraft[key] = []
3411
+ }
3412
+ } else {
3413
+ Object.keys(filterValues).forEach((k) => {
3414
+ filterValues[k] = []
3415
+ filterDraft[k] = []
3416
+ })
3417
+ }
3418
+ emit('filter-change', { ...filterValues })
3419
+ }
3420
+
3421
+ const scrollTo = (opts: { top?: number; left?: number }) => {
3422
+ const el = bodyWrapRef.value
3423
+ if (!el) return
3424
+ if (opts.top != null) el.scrollTop = opts.top
3425
+ if (opts.left != null) el.scrollLeft = opts.left
3426
+ }
3427
+
3428
+ const scrollToRow = (row: any) => {
3429
+ const id = getRowId(row)
3430
+ const el = bodyWrapRef.value?.querySelector(
3431
+ `[data-row-key="${String(id).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"]`
3432
+ )
3433
+ if (el instanceof HTMLElement) {
3434
+ el.scrollIntoView({ block: 'nearest' })
3435
+ }
3436
+ }
3437
+
3438
+ const getColumnWidth = (fieldOrKey: string) => {
3439
+ const col = leafColumns.value.find(
3440
+ (c) => c._key === fieldOrKey || getProp(c) === fieldOrKey
3441
+ )
3442
+ return col ? colWidthOf(col) : undefined
3443
+ }
3444
+
3445
+ const setColumnWidthByField = (fieldOrKey: string, width: number) => {
3446
+ const col = leafColumns.value.find(
3447
+ (c) => c._key === fieldOrKey || getProp(c) === fieldOrKey
3448
+ )
3449
+ if (col) setColumnWidth(col._key, width)
3450
+ }
3451
+
3452
+ const recalculate = () => {
3453
+ layoutTick.value++
3454
+ nextTick(scheduleLayout)
3455
+ }
3456
+
3457
+ defineExpose({
3458
+ tableRef,
3459
+ gridRef: tableRef,
3460
+ getGrid,
3461
+ clearEdit,
3462
+ exportData,
3463
+ getCheckboxRecords,
3464
+ setAllTreeExpand,
3465
+ importData: triggerImport,
3466
+ print: printTable,
3467
+ getRadioRecord: () => {
3468
+ if (!radioRowId.value) return null
3469
+ return findRowById(props.data, radioRowId.value)
3470
+ },
3471
+ clearRadioRow,
3472
+ setRowExpand,
3473
+ clearRowExpand,
3474
+ getUpdateRecords,
3475
+ getInsertRecords,
3476
+ isUpdateByRow,
3477
+ reloadRow,
3478
+ reloadColumn: () => {
3479
+ layoutTick.value++
3480
+ },
3481
+ setCurrentRow,
3482
+ clearCurrentRow,
3483
+ clearSort,
3484
+ clearFilter,
3485
+ scrollTo,
3486
+ scrollToRow,
3487
+ getColumnWidth,
3488
+ setColumnWidth: setColumnWidthByField,
3489
+ recalculate,
3490
+ getColumns: () => leafColumns.value.slice(),
3491
+ undo: undoEdit,
3492
+ redo: redoEdit,
3493
+ clearHistory,
3494
+ canUndo,
3495
+ canRedo,
3496
+ copy: copyArea,
3497
+ cut: cutArea,
3498
+ paste: pasteArea,
3499
+ copyData: copyTableData,
3500
+ })
3501
+ </script>
3502
+
3503
+ <style lang="scss" src="./styles/table.scss"></style>