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,1267 @@
1
+ <template>
2
+ <div class="qiu-upload-crop" :class="{ 'is-disabled': disabled }">
3
+ <!-- 触发区 -->
4
+ <div
5
+ v-if="!hasImage && !cropping"
6
+ class="qiu-upload-crop__trigger"
7
+ :style="boxStyle"
8
+ @click="openPicker"
9
+ @dragover.prevent="onDragOver"
10
+ @dragleave.prevent="onDragLeave"
11
+ @drop.prevent="onDrop"
12
+ >
13
+ <input
14
+ ref="inputRef"
15
+ class="qiu-upload-crop__input"
16
+ type="file"
17
+ :accept="accept"
18
+ :disabled="disabled"
19
+ @change="onInputChange"
20
+ />
21
+ <qiu-icon icon="image-add-line" class="qiu-upload-crop__trigger-icon" />
22
+ <p class="qiu-upload-crop__trigger-text">{{ buttonText }}</p>
23
+ <p v-if="tip" class="qiu-upload-crop__tip">{{ tip }}</p>
24
+ </div>
25
+
26
+ <!-- 结果预览区 -->
27
+ <div
28
+ v-else-if="!cropping && result"
29
+ class="qiu-upload-crop__result"
30
+ :style="boxStyle"
31
+ @mouseenter="hover = true"
32
+ @mouseleave="hover = false"
33
+ >
34
+ <img :src="result" class="qiu-upload-crop__result-img" alt="" :style="resultImgStyle" />
35
+ <div v-show="hover" class="qiu-upload-crop__mask">
36
+ <button type="button" :disabled="disabled" title="重新裁切" @click="cropping = true">
37
+ <qiu-icon icon="crop-line" />
38
+ </button>
39
+ <button type="button" :disabled="disabled" title="查看原图" @click="previewVisible = true">
40
+ <qiu-icon icon="zoom-in-line" />
41
+ </button>
42
+ <button type="button" :disabled="disabled" title="删除" @click="remove">
43
+ <qiu-icon icon="delete-bin-line" />
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- 裁剪弹窗 -->
49
+ <teleport to="body">
50
+ <div v-if="cropping" class="qiu-upload-crop__dialog" @click.self="onShadeClick">
51
+ <div class="qiu-upload-crop__dialog-box">
52
+ <div class="qiu-upload-crop__dialog-head">
53
+ <span>图片裁剪</span>
54
+ <button type="button" class="qiu-upload-crop__close" @click="cancel">×</button>
55
+ </div>
56
+
57
+ <div
58
+ ref="stageRef"
59
+ class="qiu-upload-crop__stage"
60
+ @wheel.prevent="onWheel"
61
+ @mousedown="onStageDown"
62
+ >
63
+ <img
64
+ ref="imgRef"
65
+ :src="source"
66
+ class="qiu-upload-crop__source"
67
+ :style="sourceStyle"
68
+ alt=""
69
+ draggable="false"
70
+ />
71
+ <div
72
+ class="qiu-upload-crop__crop"
73
+ :class="{ 'is-round': shape === 'round' }"
74
+ :style="cropBoxStyle"
75
+ @mousedown.stop="onCropDown"
76
+ >
77
+ <span
78
+ v-for="h in handles"
79
+ :key="h"
80
+ class="qiu-upload-crop__handle"
81
+ :class="`qiu-upload-crop__handle--${h}`"
82
+ @mousedown.stop="onHandleDown($event, h)"
83
+ />
84
+ </div>
85
+ </div>
86
+
87
+ <div class="qiu-upload-crop__toolbar">
88
+ <button
89
+ v-if="showRotate"
90
+ type="button"
91
+ title="左转"
92
+ @click="rotateBy(-90)"
93
+ >
94
+ <qiu-icon icon="anticlockwise-line" />
95
+ </button>
96
+ <button
97
+ v-if="showRotate"
98
+ type="button"
99
+ title="右转"
100
+ @click="rotateBy(90)"
101
+ >
102
+ <qiu-icon icon="clockwise-line" />
103
+ </button>
104
+
105
+ <button v-if="showFlip" type="button" title="水平翻转" @click="flipX = -flipX">
106
+ <qiu-icon icon="flip-h-line" />
107
+ </button>
108
+ <button v-if="showFlip" type="button" title="垂直翻转" @click="flipY = -flipY">
109
+ <qiu-icon icon="flip-v-line" />
110
+ </button>
111
+ <button
112
+ v-if="showZoom"
113
+ type="button"
114
+ title="放大"
115
+ @click="scale = Math.min(5, +(scale + 0.1).toFixed(2))"
116
+ >
117
+ <qiu-icon icon="zoom-in-line" />
118
+ </button>
119
+ <button
120
+ v-if="showZoom"
121
+ type="button"
122
+ title="缩小"
123
+ @click="scale = Math.max(0.1, +(scale - 0.1).toFixed(2))"
124
+ >
125
+ <qiu-icon icon="zoom-out-line" />
126
+ </button>
127
+ <button type="button" title="重置" @click="reset">
128
+ <qiu-icon icon="refresh-line" />
129
+ </button>
130
+
131
+ <template v-if="showSizeInputs">
132
+ <span class="qiu-upload-crop__divider" />
133
+
134
+ <label class="qiu-upload-crop__field">
135
+ <span>宽</span>
136
+ <input
137
+ v-model.number="cropWInput"
138
+ type="number"
139
+ min="20"
140
+ @change="applyCropSize('w')"
141
+ />
142
+ </label>
143
+ <label class="qiu-upload-crop__field">
144
+ <span>高</span>
145
+ <input
146
+ v-model.number="cropHInput"
147
+ type="number"
148
+ min="20"
149
+ @change="applyCropSize('h')"
150
+ />
151
+ </label>
152
+ <button type="button" title="应用尺寸" @click="applyCropSize()">应用</button>
153
+ </template>
154
+
155
+ <span class="qiu-upload-crop__divider" />
156
+
157
+ <label class="qiu-upload-crop__quality">
158
+ <span>质量 {{ quality }}</span>
159
+ <input v-model.number="quality" type="range" min="0.1" max="1" step="0.1" />
160
+ </label>
161
+
162
+ <slot name="toolbar" />
163
+
164
+ <span class="qiu-upload-crop__divider" />
165
+
166
+ <button type="button" class="qiu-upload-crop__confirm" @click="confirm">确定</button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <div v-if="previewVisible" class="qiu-upload-crop__preview" @click="previewVisible = false">
172
+ <img :src="result || source" alt="" />
173
+ </div>
174
+ </teleport>
175
+ </div>
176
+ </template>
177
+
178
+ <script lang="ts" setup>
179
+ /**
180
+ * QiuUploadCrop — 图片上传 + Canvas 裁剪(自研,无第三方裁剪依赖)
181
+ * 输出 base64(v-model)与 cropper-file 事件(可拿 File 直接上传)
182
+ * 支持 fixedAspect 比例锁定、90° 旋转预览并在确认时烘焙进画布
183
+ *
184
+ * @author qiuye
185
+ * @email yjk150@qq.com
186
+ * @date 2026-08-16
187
+ */
188
+ defineOptions({ name: 'QiuUploadCrop' })
189
+
190
+ const props = withDefaults(
191
+ defineProps<{
192
+ modelValue?: string
193
+ /** 组件展示宽高(px) */
194
+ width?: number
195
+ height?: number
196
+ /** 允许上传的图片类型后缀 */
197
+ type?: string[]
198
+ /** 文件大小上限(MB) */
199
+ size?: number
200
+ disabled?: boolean
201
+ buttonText?: string
202
+ tip?: string
203
+ /** 裁剪框形状 */
204
+ shape?: 'rect' | 'round'
205
+ /** 是否允许 round 时输出圆形(裁切透明背景) */
206
+ circleOutput?: boolean
207
+ /** 点击遮罩是否关闭(不保存) */
208
+ shadeClose?: boolean
209
+ /** 输出图片格式 */
210
+ outputType?: 'image/png' | 'image/jpeg' | 'image/webp'
211
+ accept?: string
212
+ /** 输出图片质量 0.1~1(控制体积,便于存储) */
213
+ outputQuality?: number
214
+ /** 输出原图裁切结果的目标宽高(px);0 表示按裁剪框自然尺寸 */
215
+ outputWidth?: number
216
+ outputHeight?: number
217
+ /** 是否同时输出缩略图 */
218
+ withThumbnail?: boolean
219
+ /** 缩略图边长(px) */
220
+ thumbnailSize?: number
221
+ /** 是否同时输出圆形头像 */
222
+ withAvatar?: boolean
223
+ /** 头像边长(px) */
224
+ avatarSize?: number
225
+ /** 初始裁剪框宽(px),0 表示自适应 */
226
+ cropInitWidth?: number
227
+ /** 初始裁剪框高(px),0 表示自适应 */
228
+ cropInitHeight?: number
229
+ /** 裁剪框缩放时是否锁定宽高比 */
230
+ fixedAspect?: boolean
231
+ /**
232
+ * 宽/高比。支持 number(如 3.2)或字符串 `"16:5"` / `"16/5"`。
233
+ * 为 0 且 fixedAspect 时从 cropInit / output 尺寸推导,否则为 1。
234
+ */
235
+ aspectRatio?: number | string
236
+ /** 工具栏:旋转按钮 */
237
+ showRotate?: boolean
238
+ /** 工具栏:翻转按钮 */
239
+ showFlip?: boolean
240
+ /** 工具栏:缩放按钮 */
241
+ showZoom?: boolean
242
+ /** 工具栏:宽高输入 */
243
+ showSizeInputs?: boolean
244
+ }>(),
245
+ {
246
+ modelValue: '',
247
+ width: 178,
248
+ height: 178,
249
+ type: () => ['png', 'jpg', 'jpeg'],
250
+ size: 5,
251
+ disabled: false,
252
+ buttonText: '上传图片',
253
+ tip: '',
254
+ shape: 'rect',
255
+ circleOutput: false,
256
+ shadeClose: true,
257
+ outputType: 'image/jpeg',
258
+ accept: 'image/*',
259
+ outputQuality: 0.92,
260
+ outputWidth: 0,
261
+ outputHeight: 0,
262
+ withThumbnail: false,
263
+ thumbnailSize: 120,
264
+ withAvatar: false,
265
+ avatarSize: 160,
266
+ cropInitWidth: 0,
267
+ cropInitHeight: 0,
268
+ fixedAspect: false,
269
+ aspectRatio: 0,
270
+ showRotate: true,
271
+ showFlip: true,
272
+ showZoom: true,
273
+ showSizeInputs: true,
274
+ }
275
+ )
276
+
277
+ const emit = defineEmits<{
278
+ 'update:modelValue': [string]
279
+ change: [string]
280
+ /** 裁剪原图结果 base64 */
281
+ 'cropper-image': [string]
282
+ /** 缩略图 base64 */
283
+ 'cropper-thumbnail': [string]
284
+ /** 圆形头像 base64 */
285
+ 'cropper-avatar': [string]
286
+ /** 一次性返回全部结果 */
287
+ 'cropper-result': [{
288
+ image: string
289
+ thumbnail: string
290
+ avatar: string
291
+ }]
292
+ 'cropper-file': [File]
293
+ remove: []
294
+ error: [string]
295
+ }>()
296
+
297
+ const inputRef = ref<HTMLInputElement | null>(null)
298
+ const stageRef = ref<HTMLElement | null>(null)
299
+ const imgRef = ref<HTMLImageElement | null>(null)
300
+
301
+ const hover = ref(false)
302
+ const cropping = ref(false)
303
+ const previewVisible = ref(false)
304
+
305
+ const source = ref('') // 原图 base64
306
+ const result = ref(props.modelValue)
307
+ const hasImage = computed(() => !!result.value)
308
+
309
+ // 画布内图片变换
310
+ const scale = ref(1)
311
+ const flipX = ref(1)
312
+ const flipY = ref(1)
313
+ /** 预览旋转角(0 / 90 / 180 / 270),确认时烘焙进 canvas */
314
+ const rotateDeg = ref(0)
315
+ const offsetX = ref(0)
316
+ const offsetY = ref(0)
317
+
318
+ // 裁剪框(相对 stage 坐标)
319
+ const cropX = ref(0)
320
+ const cropY = ref(0)
321
+ const cropW = ref(0)
322
+ const cropH = ref(0)
323
+ const cropWInput = ref(0)
324
+ const cropHInput = ref(0)
325
+
326
+ const quality = ref(props.outputQuality)
327
+
328
+ /** 解析 aspectRatio:number 或 `"16:5"` / `"16/5"` */
329
+ const parseAspectRatio = (v: number | string | undefined): number => {
330
+ if (v === undefined || v === null || v === '') return 0
331
+ if (typeof v === 'number') return Number.isFinite(v) && v > 0 ? v : 0
332
+ const s = String(v).trim()
333
+ if (!s) return 0
334
+ const sep = s.includes(':') ? ':' : s.includes('/') ? '/' : ''
335
+ if (sep) {
336
+ const [a, b] = s.split(sep).map((x) => Number(x.trim()))
337
+ if (Number.isFinite(a) && Number.isFinite(b) && b !== 0) return a / b
338
+ return 0
339
+ }
340
+ const n = Number(s)
341
+ return Number.isFinite(n) && n > 0 ? n : 0
342
+ }
343
+
344
+ /** 解析后的 W/H;fixedAspect 且未显式给出时按 cropInit → output → 1 回退 */
345
+ const resolvedAspect = computed(() => {
346
+ const parsed = parseAspectRatio(props.aspectRatio)
347
+ if (parsed > 0) return parsed
348
+ if (props.cropInitWidth > 0 && props.cropInitHeight > 0) {
349
+ return props.cropInitWidth / props.cropInitHeight
350
+ }
351
+ if (props.outputWidth > 0 && props.outputHeight > 0) {
352
+ return props.outputWidth / props.outputHeight
353
+ }
354
+ return 1
355
+ })
356
+
357
+ const boxStyle = computed(() => ({
358
+ width: `${props.width}px`,
359
+ height: `${props.height}px`,
360
+ }))
361
+
362
+ const resultImgStyle = computed(() => {
363
+ if (props.shape === 'round' || props.circleOutput) return { borderRadius: '50%' }
364
+ return {}
365
+ })
366
+
367
+ const sourceStyle = computed(() => ({
368
+ transform: `translate(${offsetX.value}px, ${offsetY.value}px) scale(${scale.value}) rotate(${rotateDeg.value}deg) scaleX(${flipX.value}) scaleY(${flipY.value})`,
369
+ }))
370
+
371
+ const cropBoxStyle = computed(() => ({
372
+ left: `${cropX.value}px`,
373
+ top: `${cropY.value}px`,
374
+ width: `${cropW.value}px`,
375
+ height: `${cropH.value}px`,
376
+ }))
377
+
378
+ const handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']
379
+
380
+ const fileExt = (name: string) => (name.split('.').pop() || '').toLowerCase()
381
+ const isAllowed = (name: string) => {
382
+ const ext = fileExt(name)
383
+ return props.type.includes(ext)
384
+ }
385
+
386
+ const openPicker = () => {
387
+ if (props.disabled) return
388
+ inputRef.value?.click()
389
+ }
390
+ const onDragOver = () => {
391
+ if (!props.disabled) stageRef.value?.classList.add('is-dragover')
392
+ }
393
+ const onDragLeave = () => stageRef.value?.classList.remove('is-dragover')
394
+ const onDrop = (e: DragEvent) => {
395
+ stageRef.value?.classList.remove('is-dragover')
396
+ if (props.disabled) return
397
+ const file = e.dataTransfer?.files?.[0]
398
+ if (file) handleFile(file)
399
+ }
400
+
401
+ const onInputChange = (e: Event) => {
402
+ const input = e.target as HTMLInputElement
403
+ const file = input.files?.[0]
404
+ input.value = ''
405
+ if (file) handleFile(file)
406
+ }
407
+
408
+ const handleFile = (file: File) => {
409
+ if (!file.type.startsWith('image/')) {
410
+ emit('error', '仅支持图片文件')
411
+ return
412
+ }
413
+ if (!isAllowed(file.name)) {
414
+ emit('error', `不支持的格式,仅允许:${props.type.join('/')}`)
415
+ return
416
+ }
417
+ if (file.size > props.size * 1024 * 1024) {
418
+ emit('error', `文件超过 ${props.size}MB 限制`)
419
+ return
420
+ }
421
+ const reader = new FileReader()
422
+ reader.onload = () => {
423
+ source.value = reader.result as string
424
+ cropping.value = true
425
+ nextTick(initCrop)
426
+ }
427
+ reader.readAsDataURL(file)
428
+ }
429
+
430
+ /** 在 stage 内按宽高比生成居中裁剪框尺寸 */
431
+ const fitAspectBox = (stageW: number, stageH: number, ratio: number, preferW: number, preferH: number) => {
432
+ let w = preferW
433
+ let h = preferH
434
+ if (w <= 0 && h <= 0) {
435
+ const maxW = stageW * 0.7
436
+ const maxH = stageH * 0.7
437
+ if (maxW / maxH > ratio) {
438
+ h = maxH
439
+ w = h * ratio
440
+ } else {
441
+ w = maxW
442
+ h = w / ratio
443
+ }
444
+ } else if (w > 0 && h <= 0) {
445
+ w = Math.min(w, stageW)
446
+ h = w / ratio
447
+ if (h > stageH) {
448
+ h = stageH
449
+ w = h * ratio
450
+ }
451
+ } else if (h > 0 && w <= 0) {
452
+ h = Math.min(h, stageH)
453
+ w = h * ratio
454
+ if (w > stageW) {
455
+ w = stageW
456
+ h = w / ratio
457
+ }
458
+ } else {
459
+ // 两者都给了:以宽为基准再夹紧
460
+ w = Math.min(w, stageW)
461
+ h = w / ratio
462
+ if (h > stageH) {
463
+ h = Math.min(preferH, stageH)
464
+ w = h * ratio
465
+ if (w > stageW) {
466
+ w = stageW
467
+ h = w / ratio
468
+ }
469
+ }
470
+ }
471
+ w = Math.max(20, Math.min(w, stageW))
472
+ h = Math.max(20, Math.min(w / ratio, stageH))
473
+ w = Math.max(20, Math.min(h * ratio, stageW))
474
+ h = w / ratio
475
+ return { w, h }
476
+ }
477
+
478
+ const initCrop = () => {
479
+ const stage = stageRef.value
480
+ if (!stage) return
481
+ const rect = stage.getBoundingClientRect()
482
+ // 让原图适配 stage
483
+ const img = new Image()
484
+ img.onload = () => {
485
+ const fit = Math.min(rect.width / img.width, rect.height / img.height, 1)
486
+ const w = img.width * fit
487
+ const h = img.height * fit
488
+ scale.value = fit
489
+ flipX.value = 1
490
+ flipY.value = 1
491
+ rotateDeg.value = 0
492
+ offsetX.value = (rect.width - w) / 2
493
+ offsetY.value = (rect.height - h) / 2
494
+
495
+ if (props.fixedAspect) {
496
+ const ratio = resolvedAspect.value
497
+ const preferW = props.cropInitWidth > 0 ? props.cropInitWidth : 0
498
+ const preferH = props.cropInitHeight > 0 ? props.cropInitHeight : 0
499
+ const box = fitAspectBox(rect.width, rect.height, ratio, preferW, preferH)
500
+ cropW.value = box.w
501
+ cropH.value = box.h
502
+ } else {
503
+ // 裁剪框默认取中心正方形 / 自由矩形
504
+ const side = Math.min(rect.width, rect.height) * 0.7
505
+ const initW = props.cropInitWidth > 0 ? Math.min(props.cropInitWidth, rect.width) : side
506
+ const initH = props.cropInitHeight > 0 ? Math.min(props.cropInitHeight, rect.height) : side
507
+ cropW.value = props.shape === 'round' ? Math.min(initW, initH) : Math.min(initW, w)
508
+ cropH.value = props.shape === 'round' ? Math.min(initW, initH) : Math.min(initH, h)
509
+ }
510
+ cropX.value = (rect.width - cropW.value) / 2
511
+ cropY.value = (rect.height - cropH.value) / 2
512
+ syncCropInput()
513
+ }
514
+ img.src = source.value
515
+ }
516
+
517
+ const syncCropInput = () => {
518
+ cropWInput.value = Math.round(cropW.value)
519
+ cropHInput.value = Math.round(cropH.value)
520
+ }
521
+
522
+ const clampCropBox = (nx: number, ny: number, nw: number, nh: number, stageW: number, stageH: number) => {
523
+ let w = Math.max(20, Math.min(nw, stageW))
524
+ let h = Math.max(20, Math.min(nh, stageH))
525
+ let x = Math.max(0, Math.min(nx, stageW - w))
526
+ let y = Math.max(0, Math.min(ny, stageH - h))
527
+ if (x + w > stageW) {
528
+ w = stageW - x
529
+ }
530
+ if (y + h > stageH) {
531
+ h = stageH - y
532
+ }
533
+ return { x, y, w: Math.max(20, w), h: Math.max(20, h) }
534
+ }
535
+
536
+ /** dim: 用户改了宽或高时,fixedAspect 下联动另一边 */
537
+ const applyCropSize = (dim?: 'w' | 'h') => {
538
+ const stage = stageRef.value
539
+ if (!stage) return
540
+ const rect = stage.getBoundingClientRect()
541
+ let w = Math.max(20, cropWInput.value || cropW.value)
542
+ let h = Math.max(20, cropHInput.value || cropH.value)
543
+
544
+ if (props.fixedAspect) {
545
+ const ratio = resolvedAspect.value
546
+ if (dim === 'h') {
547
+ w = h * ratio
548
+ } else {
549
+ // 改宽或点「应用」:以宽为准
550
+ h = w / ratio
551
+ }
552
+ // 夹紧进 stage,保持比例
553
+ if (w > rect.width) {
554
+ w = rect.width
555
+ h = w / ratio
556
+ }
557
+ if (h > rect.height) {
558
+ h = rect.height
559
+ w = h * ratio
560
+ }
561
+ w = Math.max(20, w)
562
+ h = Math.max(20, h)
563
+ } else {
564
+ w = Math.max(20, Math.min(w, rect.width))
565
+ h = Math.max(20, Math.min(h, rect.height))
566
+ }
567
+
568
+ const clamped = clampCropBox(cropX.value, cropY.value, w, h, rect.width, rect.height)
569
+ if (props.fixedAspect) {
570
+ const ratio = resolvedAspect.value
571
+ let cw = clamped.w
572
+ let ch = cw / ratio
573
+ if (ch > rect.height) {
574
+ ch = Math.min(clamped.h, rect.height)
575
+ cw = ch * ratio
576
+ }
577
+ const final = clampCropBox(clamped.x, clamped.y, cw, ch, rect.width, rect.height)
578
+ // 再一次比例校正
579
+ let fw = final.w
580
+ let fh = fw / ratio
581
+ if (final.y + fh > rect.height) {
582
+ fh = rect.height - final.y
583
+ fw = fh * ratio
584
+ }
585
+ if (final.x + fw > rect.width) {
586
+ fw = rect.width - final.x
587
+ fh = fw / ratio
588
+ }
589
+ cropX.value = Math.max(0, Math.min(final.x, rect.width - fw))
590
+ cropY.value = Math.max(0, Math.min(final.y, rect.height - fh))
591
+ cropW.value = Math.max(20, fw)
592
+ cropH.value = Math.max(20, fh)
593
+ } else {
594
+ cropX.value = clamped.x
595
+ cropY.value = clamped.y
596
+ cropW.value = clamped.w
597
+ cropH.value = clamped.h
598
+ }
599
+ syncCropInput()
600
+ }
601
+
602
+ const reset = () => initCrop()
603
+
604
+ const rotateBy = (delta: number) => {
605
+ rotateDeg.value = (((rotateDeg.value + delta) % 360) + 360) % 360
606
+ }
607
+
608
+ const onWheel = (e: WheelEvent) => {
609
+ scale.value = Math.max(0.1, Math.min(5, +(scale.value - e.deltaY * 0.001).toFixed(3)))
610
+ }
611
+
612
+ // 拖拽图片
613
+ const onStageDown = (e: MouseEvent) => {
614
+ const startX = e.clientX
615
+ const startY = e.clientY
616
+ const ox = offsetX.value
617
+ const oy = offsetY.value
618
+ const move = (ev: MouseEvent) => {
619
+ offsetX.value = ox + (ev.clientX - startX)
620
+ offsetY.value = oy + (ev.clientY - startY)
621
+ }
622
+ const up = () => {
623
+ window.removeEventListener('mousemove', move)
624
+ window.removeEventListener('mouseup', up)
625
+ }
626
+ window.addEventListener('mousemove', move)
627
+ window.addEventListener('mouseup', up)
628
+ }
629
+
630
+ // 拖拽裁剪框
631
+ const onCropDown = (e: MouseEvent) => {
632
+ const startX = e.clientX
633
+ const startY = e.clientY
634
+ const ox = cropX.value
635
+ const oy = cropY.value
636
+ const stage = stageRef.value!.getBoundingClientRect()
637
+ const move = (ev: MouseEvent) => {
638
+ let nx = ox + (ev.clientX - startX)
639
+ let ny = oy + (ev.clientY - startY)
640
+ nx = Math.max(0, Math.min(nx, stage.width - cropW.value))
641
+ ny = Math.max(0, Math.min(ny, stage.height - cropH.value))
642
+ cropX.value = nx
643
+ cropY.value = ny
644
+ }
645
+ const up = () => {
646
+ window.removeEventListener('mousemove', move)
647
+ window.removeEventListener('mouseup', up)
648
+ }
649
+ window.addEventListener('mousemove', move)
650
+ window.addEventListener('mouseup', up)
651
+ }
652
+
653
+ /** 固定比例时,将 (nx,ny,nw,nh) 按 ratio 夹紧进 stage */
654
+ const lockAspectResize = (
655
+ dir: string,
656
+ bx: number,
657
+ by: number,
658
+ bw: number,
659
+ bh: number,
660
+ dx: number,
661
+ dy: number,
662
+ stageW: number,
663
+ stageH: number,
664
+ ratio: number
665
+ ) => {
666
+ let nx = bx
667
+ let ny = by
668
+ let nw = bw
669
+ let nh = bh
670
+
671
+ const applySizeFromWidth = (w: number, anchorRight: boolean, anchorBottom: boolean, centerY: boolean) => {
672
+ nw = Math.max(20, w)
673
+ nh = nw / ratio
674
+ if (anchorRight) nx = bx + bw - nw
675
+ if (centerY) ny = by + (bh - nh) / 2
676
+ else if (anchorBottom) ny = by + bh - nh
677
+ }
678
+
679
+ const applySizeFromHeight = (h: number, anchorBottom: boolean, anchorRight: boolean, centerX: boolean) => {
680
+ nh = Math.max(20, h)
681
+ nw = nh * ratio
682
+ if (anchorBottom) ny = by + bh - nh
683
+ if (centerX) nx = bx + (bw - nw) / 2
684
+ else if (anchorRight) nx = bx + bw - nw
685
+ }
686
+
687
+ if (dir === 'e' || dir === 'w') {
688
+ const rawW = dir === 'e' ? bw + dx : bw - dx
689
+ applySizeFromWidth(rawW, dir === 'w', false, true)
690
+ } else if (dir === 'n' || dir === 's') {
691
+ const rawH = dir === 's' ? bh + dy : bh - dy
692
+ applySizeFromHeight(rawH, dir === 'n', false, true)
693
+ } else {
694
+ // 角点:取拖动更大的边主导
695
+ const rawW = dir.includes('e') ? bw + dx : bw - dx
696
+ const rawH = dir.includes('s') ? bh + dy : bh - dy
697
+ if (Math.abs(dx) * ratio >= Math.abs(dy)) {
698
+ applySizeFromWidth(rawW, dir.includes('w'), dir.includes('n'), false)
699
+ if (dir.includes('n')) ny = by + bh - nh
700
+ } else {
701
+ applySizeFromHeight(rawH, dir.includes('n'), dir.includes('w'), false)
702
+ if (dir.includes('w')) nx = bx + bw - nw
703
+ }
704
+ }
705
+
706
+ // 夹紧:先限制尺寸,再修正锚点位置
707
+ if (nw > stageW) {
708
+ nw = stageW
709
+ nh = nw / ratio
710
+ }
711
+ if (nh > stageH) {
712
+ nh = stageH
713
+ nw = nh * ratio
714
+ }
715
+ nw = Math.max(20, nw)
716
+ nh = Math.max(20, nh)
717
+
718
+ if (dir.includes('w')) nx = bx + bw - nw
719
+ if (dir.includes('n') && dir !== 'n' && dir !== 's') {
720
+ // 角点已设 ny;边缘 n/s 在上面处理
721
+ }
722
+ if (dir === 'n') ny = by + bh - nh
723
+ if (dir.includes('n') && dir.length === 2) ny = by + bh - nh
724
+
725
+ nx = Math.max(0, Math.min(nx, stageW - nw))
726
+ ny = Math.max(0, Math.min(ny, stageH - nh))
727
+
728
+ // 贴边后再缩一次,保证比例仍成立
729
+ if (nx + nw > stageW) {
730
+ nw = stageW - nx
731
+ nh = nw / ratio
732
+ if (dir.includes('n')) ny = by + bh - nh
733
+ ny = Math.max(0, Math.min(ny, stageH - nh))
734
+ }
735
+ if (ny + nh > stageH) {
736
+ nh = stageH - ny
737
+ nw = nh * ratio
738
+ if (dir.includes('w')) nx = bx + bw - nw
739
+ nx = Math.max(0, Math.min(nx, stageW - nw))
740
+ }
741
+
742
+ return { nx, ny, nw: Math.max(20, nw), nh: Math.max(20, nh) }
743
+ }
744
+
745
+ // 调整裁剪框大小
746
+ const onHandleDown = (e: MouseEvent, dir: string) => {
747
+ const startX = e.clientX
748
+ const startY = e.clientY
749
+ const bx = cropX.value
750
+ const by = cropY.value
751
+ const bw = cropW.value
752
+ const bh = cropH.value
753
+ const stage = stageRef.value!.getBoundingClientRect()
754
+ const move = (ev: MouseEvent) => {
755
+ const dx = ev.clientX - startX
756
+ const dy = ev.clientY - startY
757
+ let nx = bx
758
+ let ny = by
759
+ let nw = bw
760
+ let nh = bh
761
+
762
+ if (props.fixedAspect) {
763
+ const locked = lockAspectResize(dir, bx, by, bw, bh, dx, dy, stage.width, stage.height, resolvedAspect.value)
764
+ nx = locked.nx
765
+ ny = locked.ny
766
+ nw = locked.nw
767
+ nh = locked.nh
768
+ } else {
769
+ if (dir.includes('e')) nw = Math.max(20, Math.min(bw + dx, stage.width - bx))
770
+ if (dir.includes('s')) nh = Math.max(20, Math.min(bh + dy, stage.height - by))
771
+ if (dir.includes('w')) {
772
+ nx = Math.max(0, bx + dx)
773
+ nw = bx + bw - nx
774
+ }
775
+ if (dir.includes('n')) {
776
+ ny = Math.max(0, by + dy)
777
+ nh = by + bh - ny
778
+ }
779
+ if (nw < 20) {
780
+ nw = 20
781
+ if (dir.includes('w')) nx = bx + bw - 20
782
+ }
783
+ if (nh < 20) {
784
+ nh = 20
785
+ if (dir.includes('n')) ny = by + bh - 20
786
+ }
787
+ }
788
+
789
+ cropX.value = nx
790
+ cropY.value = ny
791
+ cropW.value = nw
792
+ cropH.value = nh
793
+ syncCropInput()
794
+ }
795
+ const up = () => {
796
+ window.removeEventListener('mousemove', move)
797
+ window.removeEventListener('mouseup', up)
798
+ }
799
+ window.addEventListener('mousemove', move)
800
+ window.addEventListener('mouseup', up)
801
+ }
802
+
803
+ const onShadeClick = () => {
804
+ if (props.shadeClose) cancel()
805
+ }
806
+
807
+ const cancel = () => {
808
+ cropping.value = false
809
+ }
810
+
811
+ /**
812
+ * 将原图按当前 rotate + flip 烘焙到中间画布,再按裁剪框取样。
813
+ * 预览用 CSS transform;确认时必须 bake,否则输出不含旋转/翻转。
814
+ *
815
+ * Caveat:裁剪框坐标按「旋转后外接矩形」映射(90° 时 W/H 互换),
816
+ * 与 CSS transform-origin:center 的视觉大致对齐;极端 offset/scale 下可能有像素级偏差。
817
+ */
818
+ const buildTransformedSource = (img: HTMLImageElement) => {
819
+ const naturalW = img.naturalWidth
820
+ const naturalH = img.naturalHeight
821
+ const deg = ((rotateDeg.value % 360) + 360) % 360
822
+ const rad = (deg * Math.PI) / 180
823
+ const swapped = deg === 90 || deg === 270
824
+ const outW = swapped ? naturalH : naturalW
825
+ const outH = swapped ? naturalW : naturalH
826
+ const canvas = document.createElement('canvas')
827
+ canvas.width = Math.max(1, outW)
828
+ canvas.height = Math.max(1, outH)
829
+ const ctx = canvas.getContext('2d')!
830
+ ctx.translate(outW / 2, outH / 2)
831
+ ctx.rotate(rad)
832
+ ctx.scale(flipX.value, flipY.value)
833
+ ctx.drawImage(img, -naturalW / 2, -naturalH / 2)
834
+ return { canvas, outW, outH, swapped }
835
+ }
836
+
837
+ const confirm = () => {
838
+ const img = imgRef.value
839
+ const stage = stageRef.value
840
+ if (!img || !stage) return
841
+
842
+ const { canvas: srcCanvas, outW: baseW, outH: baseH } = buildTransformedSource(img)
843
+
844
+ // stage 内显示尺寸:以旋转后外接尺寸 * scale 估算
845
+ const dispW = baseW * scale.value
846
+ const dispH = baseH * scale.value
847
+ const ratio = baseW / dispW
848
+
849
+ const imgLeft = offsetX.value + (stage.clientWidth - dispW) / 2
850
+ const imgTop = offsetY.value + (stage.clientHeight - dispH) / 2
851
+
852
+ const sx = (cropX.value - imgLeft) * ratio
853
+ const sy = (cropY.value - imgTop) * ratio
854
+ const sw = cropW.value * ratio
855
+ const sh = cropH.value * ratio
856
+
857
+ const outW = props.outputWidth > 0 ? props.outputWidth : Math.max(1, Math.round(sw))
858
+ const outH = props.outputHeight > 0 ? props.outputHeight : Math.max(1, Math.round(sh))
859
+
860
+ const imageURL = drawCrop(srcCanvas, sx, sy, sw, sh, outW, outH, props.outputType, quality.value, false)
861
+
862
+ const thumbURL = props.withThumbnail
863
+ ? drawCrop(srcCanvas, sx, sy, sw, sh, props.thumbnailSize, props.thumbnailSize, props.outputType, quality.value, false)
864
+ : ''
865
+
866
+ const avatarType = props.outputType === 'image/jpeg' ? 'image/png' : props.outputType
867
+ const avatarURL = props.withAvatar
868
+ ? drawCrop(srcCanvas, sx, sy, sw, sh, props.avatarSize, props.avatarSize, avatarType, 1, true)
869
+ : ''
870
+
871
+ result.value = imageURL
872
+ emit('update:modelValue', imageURL)
873
+ emit('change', imageURL)
874
+ emit('cropper-image', imageURL)
875
+ if (thumbURL) emit('cropper-thumbnail', thumbURL)
876
+ if (avatarURL) emit('cropper-avatar', avatarURL)
877
+ emit('cropper-result', { image: imageURL, thumbnail: thumbURL, avatar: avatarURL })
878
+ emit('cropper-file', dataURLtoFile(imageURL, `crop.${extFromType(props.outputType)}`))
879
+ cropping.value = false
880
+ }
881
+
882
+ /**
883
+ * 将 source 中 (sx,sy,sw,sh) 区域绘制到目标尺寸画布并返回 dataURL
884
+ * @param circle 是否裁为圆形(透明背景)
885
+ */
886
+ const drawCrop = (
887
+ img: CanvasImageSource,
888
+ sx: number,
889
+ sy: number,
890
+ sw: number,
891
+ sh: number,
892
+ targetW: number,
893
+ targetH: number,
894
+ type: string,
895
+ q: number,
896
+ circle: boolean
897
+ ) => {
898
+ const canvas = document.createElement('canvas')
899
+ canvas.width = Math.max(1, Math.round(targetW))
900
+ canvas.height = Math.max(1, Math.round(targetH))
901
+ const ctx = canvas.getContext('2d')!
902
+ if (type === 'image/jpeg' || type === 'image/webp') {
903
+ ctx.fillStyle = '#fff'
904
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
905
+ }
906
+ if (circle) {
907
+ ctx.beginPath()
908
+ ctx.arc(canvas.width / 2, canvas.height / 2, Math.min(canvas.width, canvas.height) / 2, 0, Math.PI * 2)
909
+ ctx.closePath()
910
+ ctx.clip()
911
+ }
912
+ // 裁剪区域可能因旋转映射略越界,做最小夹紧
913
+ const srcW = 'width' in img && typeof (img as HTMLCanvasElement).width === 'number'
914
+ ? (img as HTMLCanvasElement).width
915
+ : (img as HTMLImageElement).naturalWidth || 0
916
+ const srcH = 'height' in img && typeof (img as HTMLCanvasElement).height === 'number'
917
+ ? (img as HTMLCanvasElement).height
918
+ : (img as HTMLImageElement).naturalHeight || 0
919
+ let csx = sx
920
+ let csy = sy
921
+ let csw = sw
922
+ let csh = sh
923
+ if (srcW > 0 && srcH > 0) {
924
+ csx = Math.max(0, Math.min(sx, srcW - 1))
925
+ csy = Math.max(0, Math.min(sy, srcH - 1))
926
+ csw = Math.max(1, Math.min(sw, srcW - csx))
927
+ csh = Math.max(1, Math.min(sh, srcH - csy))
928
+ }
929
+ ctx.drawImage(img, csx, csy, csw, csh, 0, 0, canvas.width, canvas.height)
930
+ return canvas.toDataURL(type, q)
931
+ }
932
+
933
+ const extFromType = (t: string) => (t.includes('png') ? 'png' : t.includes('webp') ? 'webp' : 'jpg')
934
+
935
+ const dataURLtoFile = (dataUrl: string, filename: string) => {
936
+ const arr = dataUrl.split(',')
937
+ const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg'
938
+ const bstr = atob(arr[1])
939
+ const n = bstr.length
940
+ const u8 = new Uint8Array(n)
941
+ for (let i = 0; i < n; i++) u8[i] = bstr.charCodeAt(i)
942
+ return new File([u8], filename, { type: mime })
943
+ }
944
+
945
+ const remove = () => {
946
+ result.value = ''
947
+ source.value = ''
948
+ emit('update:modelValue', '')
949
+ emit('remove')
950
+ }
951
+
952
+ watch(
953
+ () => props.modelValue,
954
+ (v) => {
955
+ if (v !== result.value) result.value = v
956
+ }
957
+ )
958
+ </script>
959
+
960
+ <style lang="scss" scoped>
961
+ .qiu-upload-crop {
962
+ display: inline-block;
963
+ font-size: 13px;
964
+ color: var(--qiu-text-color-regular, #606266);
965
+
966
+ &.is-disabled {
967
+ opacity: 0.6;
968
+ pointer-events: none;
969
+ }
970
+ }
971
+
972
+ .qiu-upload-crop__input {
973
+ display: none;
974
+ }
975
+
976
+ .qiu-upload-crop__trigger {
977
+ box-sizing: border-box;
978
+ display: flex;
979
+ flex-direction: column;
980
+ align-items: center;
981
+ justify-content: center;
982
+ gap: 6px;
983
+ cursor: pointer;
984
+ background: var(--qiu-fill-color-blank, #fff);
985
+ border: 1px dashed var(--qiu-border-color, #dcdfe6);
986
+ border-radius: 8px;
987
+ transition: border-color 0.2s ease, background 0.2s ease;
988
+
989
+ &:hover {
990
+ color: var(--qiu-color-primary, #0f766e);
991
+ background: color-mix(in srgb, var(--qiu-color-primary, #0f766e) 6%, #fff);
992
+ border-color: var(--qiu-color-primary, #0f766e);
993
+ }
994
+
995
+ &.is-dragover {
996
+ color: var(--qiu-color-primary, #0f766e);
997
+ background: color-mix(in srgb, var(--qiu-color-primary, #0f766e) 10%, #fff);
998
+ border-color: var(--qiu-color-primary, #0f766e);
999
+ }
1000
+
1001
+ &-icon {
1002
+ font-size: 28px;
1003
+ }
1004
+
1005
+ &-text {
1006
+ margin: 0;
1007
+ font-size: 13px;
1008
+ }
1009
+ }
1010
+
1011
+ .qiu-upload-crop__tip {
1012
+ margin: 2px 0 0;
1013
+ font-size: 12px;
1014
+ color: var(--qiu-text-color-secondary, #909399);
1015
+ }
1016
+
1017
+ .qiu-upload-crop__result {
1018
+ position: relative;
1019
+ box-sizing: border-box;
1020
+ overflow: hidden;
1021
+ background: var(--qiu-fill-color-lighter, #fafafa);
1022
+ border: 1px solid var(--qiu-border-color-lighter, #ebeef5);
1023
+ border-radius: 8px;
1024
+ }
1025
+
1026
+ .qiu-upload-crop__result-img {
1027
+ width: 100%;
1028
+ height: 100%;
1029
+ object-fit: cover;
1030
+ }
1031
+
1032
+ .qiu-upload-crop__mask {
1033
+ position: absolute;
1034
+ inset: 0;
1035
+ display: flex;
1036
+ gap: 6px;
1037
+ align-items: center;
1038
+ justify-content: center;
1039
+ background: rgba(0, 0, 0, 0.45);
1040
+
1041
+ button {
1042
+ display: inline-flex;
1043
+ align-items: center;
1044
+ justify-content: center;
1045
+ width: 28px;
1046
+ height: 28px;
1047
+ color: #fff;
1048
+ cursor: pointer;
1049
+ background: rgba(255, 255, 255, 0.15);
1050
+ border: 0;
1051
+ border-radius: 6px;
1052
+
1053
+ &:hover {
1054
+ background: rgba(255, 255, 255, 0.3);
1055
+ }
1056
+
1057
+ i {
1058
+ font-size: 16px;
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ .qiu-upload-crop__dialog {
1064
+ position: fixed;
1065
+ inset: 0;
1066
+ z-index: 2000;
1067
+ display: flex;
1068
+ align-items: center;
1069
+ justify-content: center;
1070
+ background: rgba(0, 0, 0, 0.5);
1071
+ }
1072
+
1073
+ .qiu-upload-crop__dialog-box {
1074
+ display: flex;
1075
+ flex-direction: column;
1076
+ width: min(720px, 92vw);
1077
+ max-height: 90vh;
1078
+ overflow: hidden;
1079
+ background: #fff;
1080
+ border-radius: 10px;
1081
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
1082
+ }
1083
+
1084
+ .qiu-upload-crop__dialog-head {
1085
+ display: flex;
1086
+ align-items: center;
1087
+ justify-content: space-between;
1088
+ padding: 12px 16px;
1089
+ font-size: 15px;
1090
+ font-weight: 600;
1091
+ border-bottom: 1px solid var(--qiu-border-color-lighter, #ebeef5);
1092
+ }
1093
+
1094
+ .qiu-upload-crop__close {
1095
+ padding: 0 6px;
1096
+ font-size: 20px;
1097
+ color: var(--qiu-text-color-secondary, #909399);
1098
+ cursor: pointer;
1099
+ background: transparent;
1100
+ border: 0;
1101
+ }
1102
+
1103
+ .qiu-upload-crop__stage {
1104
+ position: relative;
1105
+ height: min(460px, 56vh);
1106
+ overflow: hidden;
1107
+ background:
1108
+ linear-gradient(45deg, #f0f0f0 25%, transparent 25%) -10px 0 / 20px 20px,
1109
+ linear-gradient(-45deg, #f0f0f0 25%, transparent 25%) -10px 0 / 20px 20px,
1110
+ linear-gradient(45deg, transparent 75%, #f0f0f0 75%) -10px 0 / 20px 20px,
1111
+ linear-gradient(-45deg, transparent 75%, #f0f0f0 75%) -10px 0 / 20px 20px,
1112
+ #fff;
1113
+ user-select: none;
1114
+ }
1115
+
1116
+ .qiu-upload-crop__source {
1117
+ position: absolute;
1118
+ top: 50%;
1119
+ left: 50%;
1120
+ max-width: none;
1121
+ transform-origin: center center;
1122
+ transform: translate(-50%, -50%);
1123
+ will-change: transform;
1124
+ }
1125
+
1126
+ .qiu-upload-crop__crop {
1127
+ position: absolute;
1128
+ box-sizing: border-box;
1129
+ border: 1px solid var(--qiu-color-primary, #0f766e);
1130
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
1131
+ cursor: move;
1132
+
1133
+ &.is-round {
1134
+ border-radius: 50%;
1135
+ }
1136
+
1137
+ &::before,
1138
+ &::after {
1139
+ position: absolute;
1140
+ content: '';
1141
+ background: rgba(255, 255, 255, 0.6);
1142
+ }
1143
+
1144
+ &::before {
1145
+ top: 50%;
1146
+ left: 0;
1147
+ width: 100%;
1148
+ height: 1px;
1149
+ transform: translateY(-50%);
1150
+ }
1151
+
1152
+ &::after {
1153
+ top: 0;
1154
+ left: 50%;
1155
+ width: 1px;
1156
+ height: 100%;
1157
+ transform: translateX(-50%);
1158
+ }
1159
+ }
1160
+
1161
+ .qiu-upload-crop__handle {
1162
+ position: absolute;
1163
+ width: 10px;
1164
+ height: 10px;
1165
+ background: #fff;
1166
+ border: 1px solid var(--qiu-color-primary, #0f766e);
1167
+ border-radius: 2px;
1168
+
1169
+ &--nw { top: -6px; left: -6px; cursor: nwse-resize; }
1170
+ &--n { top: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
1171
+ &--ne { top: -6px; right: -6px; cursor: nesw-resize; }
1172
+ &--e { top: 50%; right: -6px; transform: translateY(-50%); cursor: ew-resize; }
1173
+ &--se { right: -6px; bottom: -6px; cursor: nwse-resize; }
1174
+ &--s { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
1175
+ &--sw { bottom: -6px; left: -6px; cursor: nesw-resize; }
1176
+ &--w { top: 50%; left: -6px; transform: translateY(-50%); cursor: ew-resize; }
1177
+ }
1178
+
1179
+ .qiu-upload-crop__toolbar {
1180
+ display: flex;
1181
+ flex-wrap: wrap;
1182
+ gap: 8px;
1183
+ align-items: center;
1184
+ padding: 12px 16px;
1185
+ border-top: 1px solid var(--qiu-border-color-lighter, #ebeef5);
1186
+
1187
+ button {
1188
+ display: inline-flex;
1189
+ align-items: center;
1190
+ justify-content: center;
1191
+ min-width: 30px;
1192
+ height: 30px;
1193
+ padding: 0 8px;
1194
+ color: var(--qiu-text-color-regular, #606266);
1195
+ cursor: pointer;
1196
+ background: var(--qiu-fill-color-lighter, #fafafa);
1197
+ border: 1px solid var(--qiu-border-color-lighter, #ebeef5);
1198
+ border-radius: 6px;
1199
+
1200
+ &:hover {
1201
+ color: var(--qiu-color-primary, #0f766e);
1202
+ border-color: var(--qiu-color-primary, #0f766e);
1203
+ }
1204
+
1205
+ i {
1206
+ font-size: 16px;
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ .qiu-upload-crop__divider {
1212
+ width: 1px;
1213
+ height: 20px;
1214
+ background: var(--qiu-border-color-lighter, #ebeef5);
1215
+ }
1216
+
1217
+ .qiu-upload-crop__field {
1218
+ display: inline-flex;
1219
+ gap: 4px;
1220
+ align-items: center;
1221
+ font-size: 12px;
1222
+ color: var(--qiu-text-color-secondary, #909399);
1223
+
1224
+ input {
1225
+ width: 56px;
1226
+ height: 28px;
1227
+ padding: 0 6px;
1228
+ border: 1px solid var(--qiu-border-color, #dcdfe6);
1229
+ border-radius: 4px;
1230
+ }
1231
+ }
1232
+
1233
+ .qiu-upload-crop__quality {
1234
+ display: inline-flex;
1235
+ gap: 6px;
1236
+ align-items: center;
1237
+ font-size: 12px;
1238
+ color: var(--qiu-text-color-secondary, #909399);
1239
+
1240
+ input {
1241
+ width: 90px;
1242
+ }
1243
+ }
1244
+
1245
+ .qiu-upload-crop__confirm {
1246
+ margin-left: auto;
1247
+ color: #fff !important;
1248
+ background: var(--qiu-color-primary, #0f766e) !important;
1249
+ border-color: var(--qiu-color-primary, #0f766e) !important;
1250
+ }
1251
+
1252
+ .qiu-upload-crop__preview {
1253
+ position: fixed;
1254
+ inset: 0;
1255
+ z-index: 2100;
1256
+ display: flex;
1257
+ align-items: center;
1258
+ justify-content: center;
1259
+ padding: 24px;
1260
+ background: rgba(0, 0, 0, 0.7);
1261
+
1262
+ img {
1263
+ max-width: 90vw;
1264
+ max-height: 90vh;
1265
+ }
1266
+ }
1267
+ </style>