oxy-uni-ui 1.2.3 → 2.1.0

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 (246) hide show
  1. package/attributes.json +1 -1
  2. package/components/common/abstracts/variable.scss +512 -343
  3. package/components/common/util.ts +185 -32
  4. package/components/composables/index.ts +1 -0
  5. package/components/composables/usePopover.ts +24 -20
  6. package/components/composables/useVirtualScroll.ts +48 -21
  7. package/components/composables/useWindowResize.ts +35 -0
  8. package/components/oxy-action-sheet/index.scss +24 -11
  9. package/components/oxy-action-sheet/oxy-action-sheet.vue +27 -19
  10. package/components/oxy-action-sheet/types.ts +7 -0
  11. package/components/oxy-backtop/index.scss +4 -4
  12. package/components/oxy-backtop/oxy-backtop.vue +9 -6
  13. package/components/oxy-backtop/types.ts +7 -7
  14. package/components/oxy-badge/index.scss +4 -4
  15. package/components/oxy-badge/oxy-badge.vue +3 -3
  16. package/components/oxy-badge/types.ts +2 -2
  17. package/components/oxy-button/index.scss +5 -5
  18. package/components/oxy-button/oxy-button.vue +5 -1
  19. package/components/oxy-calendar/index.scss +15 -15
  20. package/components/oxy-calendar/oxy-calendar.vue +1 -0
  21. package/components/oxy-calendar/types.ts +5 -0
  22. package/components/oxy-calendar-view/month/index.scss +4 -4
  23. package/components/oxy-calendar-view/month/types.ts +36 -0
  24. package/components/oxy-calendar-view/monthPanel/index.scss +7 -8
  25. package/components/oxy-calendar-view/monthPanel/month-panel.vue +14 -8
  26. package/components/oxy-calendar-view/year/index.scss +5 -5
  27. package/components/oxy-calendar-view/yearPanel/index.scss +4 -4
  28. package/components/oxy-calendar-view/yearPanel/year-panel.vue +21 -5
  29. package/components/oxy-card/index.scss +2 -2
  30. package/components/oxy-cell/index.scss +8 -8
  31. package/components/oxy-checkbox/index.scss +12 -12
  32. package/components/oxy-checkbox-group/index.scss +2 -2
  33. package/components/oxy-circle/oxy-circle.vue +10 -7
  34. package/components/oxy-circle/types.ts +5 -5
  35. package/components/oxy-col/oxy-col.vue +2 -2
  36. package/components/oxy-col-picker/index.scss +4 -4
  37. package/components/oxy-col-picker/oxy-col-picker.vue +6 -5
  38. package/components/oxy-col-picker/types.ts +7 -2
  39. package/components/oxy-collapse/index.scss +2 -2
  40. package/components/oxy-collapse-item/oxy-collapse-item.vue +3 -3
  41. package/components/oxy-corner/index.scss +33 -33
  42. package/components/oxy-count-to/oxy-count-to.vue +3 -3
  43. package/components/oxy-curtain/index.scss +15 -15
  44. package/components/oxy-curtain/oxy-curtain.vue +4 -2
  45. package/components/oxy-curtain/types.ts +6 -1
  46. package/components/oxy-date-strip/oxy-date-strip.vue +2 -2
  47. package/components/oxy-date-strip/types.ts +1 -1
  48. package/components/oxy-date-strip-item/index.scss +3 -3
  49. package/components/oxy-datetime-picker/index.scss +11 -11
  50. package/components/oxy-datetime-picker/oxy-datetime-picker.vue +1 -0
  51. package/components/oxy-datetime-picker/types.ts +5 -0
  52. package/components/oxy-drop-menu/index.scss +5 -5
  53. package/components/oxy-drop-menu/oxy-drop-menu.vue +3 -3
  54. package/components/oxy-drop-menu-item/index.scss +3 -3
  55. package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +4 -3
  56. package/components/oxy-drop-menu-item/types.ts +5 -0
  57. package/components/oxy-echarts/types.ts +6 -0
  58. package/components/oxy-fab/index.scss +8 -8
  59. package/components/oxy-fab/oxy-fab.vue +22 -3
  60. package/components/oxy-file-list/index.scss +30 -29
  61. package/components/oxy-file-list/oxy-file-list.vue +2 -2
  62. package/components/oxy-floating-panel/oxy-floating-panel.vue +13 -9
  63. package/components/oxy-floating-panel/{type.ts → types.ts} +8 -8
  64. package/components/oxy-footer/index.scss +19 -0
  65. package/components/oxy-footer/oxy-footer.vue +78 -0
  66. package/components/oxy-footer/types.ts +17 -0
  67. package/components/oxy-form-item/types.ts +22 -1
  68. package/components/oxy-gap/oxy-gap.vue +2 -2
  69. package/components/oxy-gap/types.ts +2 -2
  70. package/components/oxy-grid/oxy-grid.vue +1 -1
  71. package/components/oxy-grid/types.ts +1 -1
  72. package/components/oxy-grid-item/index.scss +1 -1
  73. package/components/oxy-grid-item/oxy-grid-item.vue +7 -5
  74. package/components/oxy-grid-item/types.ts +1 -1
  75. package/components/oxy-guidance/index.scss +75 -0
  76. package/components/oxy-guidance/oxy-guidance.vue +201 -0
  77. package/components/oxy-guidance/types.ts +33 -0
  78. package/components/oxy-icon/oxy-icon.vue +2 -2
  79. package/components/oxy-icon/types.ts +1 -1
  80. package/components/oxy-img/oxy-img.vue +4 -4
  81. package/components/oxy-img/types.ts +3 -3
  82. package/components/oxy-img-cropper/index.scss +23 -23
  83. package/components/oxy-img-cropper/oxy-img-cropper.vue +97 -52
  84. package/components/oxy-img-cropper/types.ts +2 -2
  85. package/components/oxy-img-lazy/oxy-img-lazy.vue +3 -3
  86. package/components/oxy-img-lazy/types.ts +3 -3
  87. package/components/oxy-index-anchor/index.scss +2 -2
  88. package/components/oxy-index-anchor/oxy-index-anchor.vue +2 -2
  89. package/components/oxy-index-anchor/{type.ts → types.ts} +3 -0
  90. package/components/oxy-index-bar/index.scss +3 -3
  91. package/components/oxy-index-bar/oxy-index-bar.vue +3 -3
  92. package/components/oxy-index-bar/{type.ts → types.ts} +2 -2
  93. package/components/oxy-input/index.scss +1 -1
  94. package/components/oxy-input-number/index.scss +5 -5
  95. package/components/oxy-input-number/oxy-input-number.vue +2 -2
  96. package/components/oxy-input-number/types.ts +3 -2
  97. package/components/oxy-keyboard/index.scss +5 -5
  98. package/components/oxy-keyboard/key/index.scss +3 -3
  99. package/components/oxy-keyboard/key/index.vue +2 -2
  100. package/components/oxy-keyboard/key/types.ts +15 -0
  101. package/components/oxy-keyboard/oxy-keyboard.vue +1 -0
  102. package/components/oxy-keyboard/types.ts +5 -0
  103. package/components/oxy-link/index.scss +2 -2
  104. package/components/oxy-list/oxy-list.vue +4 -3
  105. package/components/oxy-loading/oxy-loading.vue +8 -4
  106. package/components/oxy-loading/types.ts +1 -1
  107. package/components/oxy-loadmore/index.scss +3 -3
  108. package/components/oxy-long-press-menu/index.scss +93 -0
  109. package/components/oxy-long-press-menu/oxy-long-press-menu.vue +338 -0
  110. package/components/oxy-long-press-menu/types.ts +34 -0
  111. package/components/oxy-message-box/index.scss +12 -11
  112. package/components/oxy-message-box/oxy-message-box.vue +9 -2
  113. package/components/oxy-message-box/types.ts +9 -0
  114. package/components/oxy-navbar/index.scss +2 -2
  115. package/components/oxy-navbar/oxy-navbar.vue +58 -13
  116. package/components/oxy-navbar/types.ts +8 -1
  117. package/components/oxy-navbar-capsule/types.ts +3 -0
  118. package/components/oxy-notice-bar/index.scss +3 -3
  119. package/components/oxy-notice-bar/oxy-notice-bar.vue +9 -5
  120. package/components/oxy-notice-bar/types.ts +3 -3
  121. package/components/oxy-notify/index.ts +1 -0
  122. package/components/oxy-notify/oxy-notify.vue +3 -2
  123. package/components/oxy-notify/types.ts +7 -0
  124. package/components/oxy-pagination/index.scss +6 -5
  125. package/components/oxy-password-input/oxy-password-input.vue +2 -2
  126. package/components/oxy-password-input/types.ts +1 -1
  127. package/components/oxy-picker/index.scss +45 -2
  128. package/components/oxy-picker/oxy-picker.vue +100 -14
  129. package/components/oxy-picker/types.ts +29 -1
  130. package/components/oxy-picker-view/index.scss +4 -4
  131. package/components/oxy-picker-view/oxy-picker-view.vue +4 -4
  132. package/components/oxy-popover/index.scss +13 -13
  133. package/components/oxy-popup/index.scss +4 -4
  134. package/components/oxy-popup/oxy-popup.vue +35 -2
  135. package/components/oxy-popup/types.ts +8 -1
  136. package/components/oxy-progress/index.scss +3 -3
  137. package/components/oxy-qrcode/draw.ts +398 -0
  138. package/components/oxy-qrcode/index.scss +2 -0
  139. package/components/oxy-qrcode/oxy-qrcode.vue +124 -0
  140. package/components/oxy-qrcode/qrcode.ts +936 -0
  141. package/components/oxy-qrcode/types.ts +42 -0
  142. package/components/oxy-radio/index.scss +25 -19
  143. package/components/oxy-radio-group/index.scss +2 -2
  144. package/components/oxy-rate/types.ts +4 -4
  145. package/components/oxy-resize/index.scss +2 -2
  146. package/components/oxy-resize/oxy-resize.vue +4 -4
  147. package/components/oxy-resize/types.ts +3 -0
  148. package/components/oxy-rich-text/index.scss +37 -36
  149. package/components/oxy-rich-text/mp-html/card/card.vue +3 -3
  150. package/components/oxy-rich-text/mp-html/mp-html.vue +33 -24
  151. package/components/oxy-rich-text/mp-html/node/node.vue +30 -19
  152. package/components/oxy-rich-text/oxy-rich-text.vue +31 -31
  153. package/components/oxy-rich-text/types.ts +6 -1
  154. package/components/oxy-row/oxy-row.vue +3 -3
  155. package/components/oxy-row/types.ts +1 -1
  156. package/components/oxy-search/index.scss +7 -7
  157. package/components/oxy-segmented/index.scss +19 -16
  158. package/components/oxy-segmented/oxy-segmented.vue +23 -3
  159. package/components/oxy-select/index.scss +213 -89
  160. package/components/oxy-select/oxy-select.vue +106 -58
  161. package/components/oxy-select/types.ts +13 -1
  162. package/components/oxy-select-picker/index.scss +7 -7
  163. package/components/oxy-select-picker/oxy-select-picker.vue +1 -0
  164. package/components/oxy-select-picker/types.ts +2 -0
  165. package/components/oxy-sidebar-item/index.scss +2 -2
  166. package/components/oxy-signature/oxy-signature.vue +18 -10
  167. package/components/oxy-signature/types.ts +106 -13
  168. package/components/oxy-skeleton/index.scss +1 -1
  169. package/components/oxy-skeleton/oxy-skeleton.vue +6 -6
  170. package/components/oxy-skeleton/types.ts +1 -1
  171. package/components/oxy-slider/index.scss +6 -6
  172. package/components/oxy-sort-button/index.scss +8 -8
  173. package/components/oxy-splitter/index.scss +19 -0
  174. package/components/oxy-splitter/oxy-splitter.vue +409 -0
  175. package/components/oxy-splitter/types.ts +75 -0
  176. package/components/oxy-splitter-panel/index.scss +366 -0
  177. package/components/oxy-splitter-panel/oxy-splitter-panel.vue +432 -0
  178. package/components/oxy-splitter-panel/types.ts +63 -0
  179. package/components/oxy-status-tip/index.scss +4 -4
  180. package/components/oxy-status-tip/oxy-status-tip.vue +5 -5
  181. package/components/oxy-status-tip/types.ts +3 -3
  182. package/components/oxy-step/index.scss +16 -16
  183. package/components/oxy-sticky/oxy-sticky.vue +6 -6
  184. package/components/oxy-stream-render/oxy-stream-render.vue +230 -4
  185. package/components/oxy-stream-render/types.ts +4 -1
  186. package/components/oxy-swipe-action/oxy-swipe-action.vue +27 -2
  187. package/components/oxy-swiper/oxy-swiper.vue +6 -6
  188. package/components/oxy-swiper/types.ts +5 -5
  189. package/components/oxy-swiper-nav/index.scss +3 -3
  190. package/components/oxy-switch/index.scss +10 -10
  191. package/components/oxy-switch/oxy-switch.vue +2 -2
  192. package/components/oxy-switch/types.ts +1 -1
  193. package/components/oxy-tab/index.scss +11 -1
  194. package/components/oxy-tabbar/index.scss +2 -2
  195. package/components/oxy-tabbar/oxy-tabbar.vue +39 -10
  196. package/components/oxy-table/index.scss +8 -8
  197. package/components/oxy-table/oxy-table.vue +8 -6
  198. package/components/oxy-table/types.ts +2 -2
  199. package/components/oxy-table-col/index.scss +3 -3
  200. package/components/oxy-table-col/oxy-table-col.vue +3 -3
  201. package/components/oxy-table-col/types.ts +2 -2
  202. package/components/oxy-tabs/index.scss +52 -22
  203. package/components/oxy-tabs/oxy-tabs.vue +53 -19
  204. package/components/oxy-tabs/types.ts +15 -3
  205. package/components/oxy-tag/index.scss +111 -36
  206. package/components/oxy-text/index.scss +5 -1
  207. package/components/oxy-text/oxy-text.vue +76 -7
  208. package/components/oxy-text/types.ts +12 -0
  209. package/components/oxy-textarea/index.scss +6 -6
  210. package/components/oxy-toast/oxy-toast.vue +24 -8
  211. package/components/oxy-tooltip/index.scss +9 -9
  212. package/components/oxy-tree/index.scss +51 -15
  213. package/components/oxy-tree/oxy-tree.vue +13 -9
  214. package/components/oxy-tree/types.ts +12 -9
  215. package/components/oxy-upload/index.scss +23 -23
  216. package/components/oxy-upload/types.ts +2 -2
  217. package/components/oxy-verification-code/index.scss +6 -0
  218. package/components/oxy-verification-code/oxy-verification-code.vue +187 -0
  219. package/components/oxy-verification-code/types.ts +82 -0
  220. package/components/oxy-video-preview/index.scss +4 -4
  221. package/components/oxy-virtual-scroll/index.scss +5 -5
  222. package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +11 -7
  223. package/components/oxy-virtual-scroll/types.ts +14 -14
  224. package/components/oxy-voice-player/index.scss +937 -0
  225. package/components/oxy-voice-player/oxy-voice-player.vue +821 -0
  226. package/components/oxy-voice-player/types.ts +567 -0
  227. package/components/oxy-waterfall/oxy-waterfall.vue +6 -6
  228. package/components/oxy-waterfall/types.ts +6 -6
  229. package/components/oxy-watermark/oxy-watermark.vue +35 -13
  230. package/components/oxy-watermark/types.ts +14 -14
  231. package/global.d.ts +4 -0
  232. package/locale/lang/ar-SA.ts +3 -0
  233. package/locale/lang/en-US.ts +3 -0
  234. package/locale/lang/zh-CN.ts +3 -0
  235. package/package.json +97 -1
  236. package/tags.json +1 -1
  237. package/web-types.json +1 -1
  238. package/components/oxy-number-keyboard/index.scss +0 -78
  239. package/components/oxy-number-keyboard/key/index.scss +0 -81
  240. package/components/oxy-number-keyboard/key/index.vue +0 -78
  241. package/components/oxy-number-keyboard/key/types.ts +0 -11
  242. package/components/oxy-number-keyboard/oxy-number-keyboard.vue +0 -151
  243. package/components/oxy-number-keyboard/types.ts +0 -83
  244. package/components/oxy-tree/components/tree-node-content.vue +0 -72
  245. package/components/oxy-tree/index.ts +0 -51
  246. package/oxy-uni-ui.zip +0 -0
@@ -77,11 +77,12 @@ export default {
77
77
  </script>
78
78
 
79
79
  <script lang="ts" setup>
80
- import { computed, onBeforeMount, ref } from 'vue'
80
+ import { computed, onBeforeMount, ref, watch } from 'vue'
81
81
  import OxyIcon from '../oxy-icon/oxy-icon.vue'
82
82
  import OxyOverlay from '../oxy-overlay/oxy-overlay.vue'
83
83
  import OxyTransition from '../oxy-transition/oxy-transition.vue'
84
84
  import OxyRootPortal from '../oxy-root-portal/oxy-root-portal.vue'
85
+ import { resolveSizeWithScreenWidth, unitConvert } from '../common/util'
85
86
  import { popupProps } from './types'
86
87
  import type { TransitionName } from '../oxy-transition/types'
87
88
 
@@ -124,9 +125,41 @@ const transitionName = computed<TransitionName | TransitionName[]>(() => {
124
125
  })
125
126
 
126
127
  const safeBottom = ref<number>(0)
128
+ const maxWidthStyleWhenOpen = ref<string>('')
129
+
130
+ function getMaxWidthStyle() {
131
+ if (props.maxWidth === '' || props.maxWidth === undefined || props.maxWidth === null) {
132
+ return ''
133
+ }
134
+ const maxWidthValue = resolveSizeWithScreenWidth(props.maxWidth, { defaultUnit: 'rpx' })
135
+ const widthStyle = props.position === 'center' ? 'width:100%;' : ''
136
+ return `${widthStyle}max-width:${maxWidthValue};margin-left:auto;margin-right:auto;`
137
+ }
138
+
139
+ watch(
140
+ () => props.modelValue,
141
+ (newValue) => {
142
+ if (newValue) {
143
+ maxWidthStyleWhenOpen.value = getMaxWidthStyle()
144
+ }
145
+ },
146
+ { immediate: true }
147
+ )
148
+
149
+ watch(
150
+ [() => props.maxWidth, () => props.position],
151
+ () => {
152
+ if (props.modelValue) {
153
+ maxWidthStyleWhenOpen.value = getMaxWidthStyle()
154
+ }
155
+ },
156
+ { deep: true }
157
+ )
127
158
 
128
159
  const style = computed(() => {
129
- return `z-index:${props.zIndex}; padding-bottom: ${safeBottom.value}px;${props.customStyle}`
160
+ return `z-index:${props.zIndex};padding-bottom:${unitConvert(safeBottom.value, 0, { output: 'px' })};${maxWidthStyleWhenOpen.value}${
161
+ props.customStyle
162
+ }`
130
163
  })
131
164
 
132
165
  const rootClass = computed(() => {
@@ -1,5 +1,5 @@
1
1
  import type { PropType } from 'vue'
2
- import { baseProps, makeBooleanProp, makeNumberProp, makeStringProp } from '../common/props'
2
+ import { baseProps, makeBooleanProp, makeNumberProp, makeNumericProp, makeStringProp } from '../common/props'
3
3
  import type { TransitionName } from '../oxy-transition/types'
4
4
 
5
5
  export type PopupType = 'center' | 'top' | 'right' | 'bottom' | 'left'
@@ -52,6 +52,13 @@ export const popupProps = {
52
52
  * 默认值:10
53
53
  */
54
54
  zIndex: makeNumberProp(10),
55
+ /**
56
+ * 弹出层最大宽度,支持 `rpx`、`px`、`%` 等单位;
57
+ * 数值和纯数字字符串按 `rpx` 处理,`%` 在打开时按屏幕宽度换算为 `rpx`
58
+ * 类型:number | string
59
+ * 默认值:''
60
+ */
61
+ maxWidth: makeNumericProp(''),
55
62
  /**
56
63
  * 是否当关闭时将弹出层隐藏(display: none)
57
64
  * 类型:boolean
@@ -47,8 +47,8 @@
47
47
  }
48
48
  }
49
49
  @include edeep(label) {
50
- width: 30px;
51
- margin-left: 9px;
50
+ width: 60rpx;
51
+ margin-left: 20rpx;
52
52
  color: $-progress-label-color;
53
53
  font-size: $-progress-label-fs;
54
54
  }
@@ -65,4 +65,4 @@
65
65
  color: $-progress-warning-color;
66
66
  }
67
67
  }
68
- }
68
+ }
@@ -0,0 +1,398 @@
1
+ /**
2
+ * 导入所需的工具函数和依赖
3
+ */
4
+ import { generateFrame } from './qrcode'
5
+ import type { ClQrcodeMode } from './types'
6
+ import { uuid } from '@/uni_modules/oxy-uni-ui/components/common/util'
7
+ import CanvasContext = UniNamespace.CanvasContext
8
+
9
+ declare type Image = HTMLImageElement
10
+ /**
11
+ * 二维码生成配置选项接口
12
+ * 定义了生成二维码所需的所有参数
13
+ */
14
+ export type QrcodeOptions = {
15
+ ecc: string // 纠错级别,可选 L/M/Q/H,纠错能力依次增强
16
+ text: string // 二维码内容,要编码的文本
17
+ size: number // 二维码尺寸,单位px
18
+ foreground: string // 前景色,二维码数据点的颜色
19
+ background: string // 背景色,二维码背景的颜色
20
+ padding: number // 内边距,二维码四周留白的距离
21
+ logo: string // logo图片地址,可以在二维码中心显示logo
22
+ logoSize: number // logo尺寸,logo图片的显示大小
23
+ mode: ClQrcodeMode // 二维码样式模式,支持矩形、圆形、线条、小方块
24
+ pdColor: string | null // 定位点颜色,三个角上定位图案的颜色,为null时使用前景色
25
+ pdRadius: number // 定位图案圆角半径,为0时绘制直角矩形
26
+ }
27
+
28
+ /**
29
+ * 绘制圆角矩形
30
+ * 兼容不同平台的圆角矩形绘制方法
31
+ * @param ctx Canvas上下文
32
+ * @param x 矩形左上角x坐标
33
+ * @param y 矩形左上角y坐标
34
+ * @param width 矩形宽度
35
+ * @param height 矩形高度
36
+ * @param radius 圆角半径
37
+ */
38
+ function drawRoundedRect(ctx: CanvasContext, x: number, y: number, width: number, height: number, radius: number) {
39
+ if (radius <= 0) {
40
+ // 圆角半径为0时直接绘制矩形
41
+ ctx.fillRect(x, y, width, height)
42
+ return
43
+ }
44
+
45
+ // 限制圆角半径不超过矩形的一半
46
+ const maxRadius = Math.min(width, height) / 2
47
+ const r = Math.min(radius, maxRadius)
48
+
49
+ ctx.beginPath()
50
+ ctx.moveTo(x + r, y)
51
+ ctx.lineTo(x + width - r, y)
52
+ ctx.arcTo(x + width, y, x + width, y + r, r)
53
+ ctx.lineTo(x + width, y + height - r)
54
+ ctx.arcTo(x + width, y + height, x + width - r, y + height, r)
55
+ ctx.lineTo(x + r, y + height)
56
+ ctx.arcTo(x, y + height, x, y + height - r, r)
57
+ ctx.lineTo(x, y + r)
58
+ ctx.arcTo(x, y, x + r, y, r)
59
+ ctx.closePath()
60
+ ctx.fill()
61
+ }
62
+
63
+ /**
64
+ * 绘制定位图案
65
+ * 绘制7x7的定位图案,包含外框、内框和中心点
66
+ * @param ctx Canvas上下文
67
+ * @param startX 定位图案起始X坐标
68
+ * @param startY 定位图案起始Y坐标
69
+ * @param px 单个像素点大小
70
+ * @param pdColor 定位图案颜色
71
+ * @param background 背景颜色
72
+ * @param radius 圆角半径
73
+ */
74
+ function drawPositionPattern(ctx: CanvasContext, startX: number, startY: number, px: number, pdColor: string, background: string, radius: number) {
75
+ const patternSize = px * 7 // 定位图案总尺寸 7x7
76
+
77
+ // 绘制外层边框 (7x7)
78
+ ctx.fillStyle = pdColor
79
+ drawRoundedRect(ctx, startX, startY, patternSize, patternSize, radius)
80
+
81
+ // 绘制内层空心区域 (5x5)
82
+ ctx.fillStyle = background
83
+ const innerStartX = startX + px
84
+ const innerStartY = startY + px
85
+ const innerSize = px * 5
86
+ const innerRadius = Math.max(0, radius - px) // 内层圆角适当减小
87
+ drawRoundedRect(ctx, innerStartX, innerStartY, innerSize, innerSize, innerRadius)
88
+
89
+ // 绘制中心实心区域 (3x3)
90
+ ctx.fillStyle = pdColor
91
+ const centerStartX = startX + px * 2
92
+ const centerStartY = startY + px * 2
93
+ const centerSize = px * 3
94
+ const centerRadius = Math.max(0, radius - px * 2) // 中心圆角适当减小
95
+ drawRoundedRect(ctx, centerStartX, centerStartY, centerSize, centerSize, centerRadius)
96
+ }
97
+
98
+ /**
99
+ * 绘制二维码到Canvas上下文
100
+ * 主要的二维码绘制函数,使用统一的Canvas 2D API
101
+ * @param context Canvas 2D绘图上下文对象
102
+ * @param options 二维码配置选项
103
+ */
104
+ export function drawQrcode(context: CanvasContext, options: QrcodeOptions) {
105
+ const ctx: CanvasContext = context
106
+
107
+ // 生成二维码数据矩阵
108
+ const frame = generateFrame(options.text, options.ecc)
109
+ const points = frame.frameBuffer // 点阵数据
110
+ const width = frame.width // 矩阵宽度
111
+
112
+ // 计算二维码内容区域大小(减去四周的padding)
113
+ const contentSize = options.size - options.padding * 2
114
+ // 计算每个数据点的实际像素大小
115
+ const px = contentSize / width
116
+ // 二维码内容的起始位置(考虑padding)
117
+ const offsetX = options.padding
118
+ const offsetY = options.padding
119
+
120
+ // 绘制整个画布背景
121
+ ctx.fillStyle = options.background
122
+ ctx.fillRect(0, 0, options.size, options.size)
123
+
124
+ /**
125
+ * 判断坐标点是否在定位图案区域内
126
+ * 二维码三个角上的定位图案是7x7的方块
127
+ * @param i 横坐标
128
+ * @param j 纵坐标
129
+ * @param width 二维码宽度
130
+ * @returns 是否是定位点
131
+ */
132
+ function isPositionDetectionPattern(i: number, j: number, width: number): boolean {
133
+ // 判断三个角的定位图案(7x7)
134
+ if (i < 7 && j < 7) return true // 左上角
135
+ if (i > width - 8 && j < 7) return true // 右上角
136
+ if (i < 7 && j > width - 8) return true // 左下角
137
+ return false
138
+ }
139
+
140
+ /**
141
+ * 判断坐标点是否在Logo区域内(包含缓冲区)
142
+ * @param i 横坐标
143
+ * @param j 纵坐标
144
+ * * @param width 二维码宽度
145
+ * @param logoSize logo尺寸(像素)
146
+ * @param px 单个数据点像素大小
147
+ * @returns 是否在logo区域内
148
+ */
149
+ function isInLogoArea(i: number, j: number, width: number, logoSize: number, px: number): boolean {
150
+ if (logoSize <= 0) return false
151
+
152
+ // 计算logo在矩阵中占用的点数,限制最大不超过二维码总宽度的25%
153
+ // 根据二维码标准,中心区域最多可以遮挡约30%的数据,但为了确保识别率,我们限制在20%
154
+ const maxLogoRatio = 0.2 // 20%的区域用于logo
155
+ const maxLogoPoints = Math.floor(width * maxLogoRatio)
156
+ const logoPoints = Math.min(Math.ceil(logoSize / px), maxLogoPoints)
157
+
158
+ // 减少缓冲区,只保留必要的边距,避免过度遮挡数据
159
+ // 当logo较小时不需要缓冲区,当logo较大时才添加最小缓冲区
160
+ const buffer = logoPoints > width * 0.1 ? 1 : 0
161
+ const totalLogoPoints = logoPoints + buffer * 2
162
+
163
+ // 计算logo区域在矩阵中的中心位置
164
+ const centerI = Math.floor(width / 2)
165
+ const centerJ = Math.floor(width / 2)
166
+
167
+ // 计算logo区域的边界
168
+ const halfSize = Math.floor(totalLogoPoints / 2)
169
+ const minI = centerI - halfSize
170
+ const maxI = centerI + halfSize
171
+ const minJ = centerJ - halfSize
172
+ const maxJ = centerJ + halfSize
173
+
174
+ // 判断当前点是否在logo区域内
175
+ return i >= minI && i <= maxI && j >= minJ && j <= maxJ
176
+ }
177
+
178
+ // 先绘制定位图案
179
+ const pdColor = options.pdColor ?? options.foreground
180
+ const radius = options.pdRadius
181
+
182
+ // 绘制三个定位图案
183
+ // 左上角 (0, 0)
184
+ drawPositionPattern(ctx, offsetX, offsetY, px, pdColor, options.background, radius)
185
+ // 右上角 (width-7, 0)
186
+ drawPositionPattern(ctx, offsetX + (width - 7) * px, offsetY, px, pdColor, options.background, radius)
187
+ // 左下角 (0, width-7)
188
+ drawPositionPattern(ctx, offsetX, offsetY + (width - 7) * px, px, pdColor, options.background, radius)
189
+
190
+ // 点的间距,用于圆形和小方块模式
191
+ const dot = px * 0.1
192
+
193
+ // 遍历绘制数据点(跳过定位图案区域和logo区域)
194
+ for (let i = 0; i < width; i++) {
195
+ for (let j = 0; j < width; j++) {
196
+ if (points[j * width + i] > 0) {
197
+ // 跳过定位图案区域
198
+ if (isPositionDetectionPattern(i, j, width)) {
199
+ continue
200
+ }
201
+
202
+ // 跳过logo区域(包含缓冲区)
203
+ if (options.logo != '' && isInLogoArea(i, j, width, options.logoSize, px)) {
204
+ continue
205
+ }
206
+
207
+ // 绘制数据点
208
+ ctx.fillStyle = options.foreground
209
+ const x = offsetX + px * i
210
+ const y = offsetY + px * j
211
+
212
+ // 根据不同模式绘制数据点
213
+ switch (options.mode) {
214
+ case 'line': // 线条模式 - 绘制水平线条
215
+ ctx.fillRect(x, y, px, px / 2)
216
+ break
217
+
218
+ case 'circular': // 圆形模式 - 绘制圆点
219
+ ctx.beginPath()
220
+ ctx.arc(x + px / 2 - dot, y + px / 2 - dot, px / 2 - dot, 0, 2 * Math.PI)
221
+ ctx.fill()
222
+ ctx.closePath()
223
+ break
224
+
225
+ case 'rectSmall': // 小方块模式 - 绘制小一号的方块
226
+ ctx.fillRect(x + dot, y + dot, px - dot * 2, px - dot * 2)
227
+ break
228
+
229
+ default: // 默认实心方块模式
230
+ ctx.fillRect(x, y, px, px)
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+ // 绘制 Logo
237
+ if (options.logo != '') {
238
+ let img: Image
239
+
240
+ // 微信小程序和鸿蒙环境创建图片
241
+ // #ifdef MP-WEIXIN || APP-HARMONY
242
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
243
+ // @ts-ignore
244
+ img = context.canvas.createImage()
245
+ // #endif
246
+
247
+ // 其他环境创建图片
248
+ // #ifndef MP-WEIXIN || APP-HARMONY
249
+ img = new Image(options.logoSize, options.logoSize)
250
+ // #endif
251
+
252
+ // 设置图片加载完成后的回调,然后设置图片源
253
+ img.onload = () => {
254
+ drawLogo(ctx, options, img)
255
+ }
256
+ img.src = options.logo
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 在二维码中心绘制Logo
262
+ * 在二维码中心位置绘制Logo图片,优化背景处理以减少对二维码数据的影响
263
+ * @param ctx Canvas上下文
264
+ * @param options 二维码配置
265
+ * @param img Logo图片对象
266
+ */
267
+ function drawLogo(ctx: CanvasContext, options: QrcodeOptions, img: Image) {
268
+ ctx.save() // 保存当前绘图状态
269
+
270
+ // 计算二维码内容区域的中心位置(考虑padding)
271
+ const contentSize = options.size - options.padding * 2
272
+ const contentCenterX = options.padding + contentSize / 2
273
+ const contentCenterY = options.padding + contentSize / 2
274
+
275
+ // 优化背景处理:减少背景边距,最小化对二维码数据的影响
276
+ // 背景边距从6px减少到3px,降低对数据点的遮挡
277
+ const backgroundPadding = 3 // 背景比logo大3px
278
+ const backgroundSize = options.logoSize + backgroundPadding * 2
279
+
280
+ // 绘制白色背景作为Logo的底色(适当大于logo以确保可读性)
281
+ ctx.fillStyle = options.background // 使用二维码背景色而不是固定白色,保持一致性
282
+ const backgroundX = contentCenterX - backgroundSize / 2
283
+ const backgroundY = contentCenterY - backgroundSize / 2
284
+
285
+ // 绘制圆角背景,让logo与二维码更好融合
286
+ const cornerRadius = Math.min(backgroundSize * 0.1, 6) // 背景圆角半径
287
+ drawRoundedRect(ctx, backgroundX, backgroundY, backgroundSize, backgroundSize, cornerRadius)
288
+
289
+ // 获取图片信息后绘制Logo
290
+ uni.getImageInfo({
291
+ src: options.logo,
292
+ success: (imgInfo) => {
293
+ // 计算logo的精确位置
294
+ const logoX = contentCenterX - options.logoSize / 2
295
+ const logoY = contentCenterY - options.logoSize / 2
296
+
297
+ // 绘制Logo图片,减少边距从3px到1.5px,让logo更大一些
298
+ const logoPadding = 1.5
299
+ const actualLogoSize = options.logoSize - logoPadding * 2
300
+
301
+ // #ifdef APP-HARMONY
302
+ ctx.drawImage(img as any, logoX + logoPadding, logoY + logoPadding, actualLogoSize, actualLogoSize, 0, 0, imgInfo.width, imgInfo.height)
303
+ // #endif
304
+
305
+ // #ifndef APP-HARMONY
306
+ ctx.drawImage(img as any, logoX + logoPadding, logoY + logoPadding, actualLogoSize, actualLogoSize)
307
+ // #endif
308
+
309
+ ctx.restore() // 恢复之前的绘图状态
310
+ },
311
+ fail(err) {
312
+ console.error(err)
313
+ }
314
+ })
315
+ }
316
+
317
+ /**
318
+ * 检查是否为鸿蒙环境
319
+ * @returns 是否为鸿蒙环境
320
+ */
321
+ export const isHarmony = (): boolean => {
322
+ // #ifdef APP-HARMONY
323
+ return true
324
+ // #endif
325
+
326
+ return false
327
+ }
328
+ /**
329
+ * 检查是否为App-IOS环境
330
+ * @returns 是否为App-IOS环境
331
+ */
332
+ export const isAppIOS = (): boolean => {
333
+ // #ifdef APP-IOS
334
+ return true
335
+ // #endif
336
+ return false
337
+ }
338
+ /**
339
+ * 将base64转换为blob
340
+ * @param data base64数据
341
+ * @returns blob数据
342
+ */
343
+ export function base64ToBlob(data: string, type: string = 'image/jpeg'): Blob {
344
+ // #ifdef H5
345
+ const bytes = window.atob(data.split(',')[1])
346
+ const ab = new ArrayBuffer(bytes.length)
347
+ const ia = new Uint8Array(ab)
348
+ for (let i = 0; i < bytes.length; i++) {
349
+ ia[i] = bytes.charCodeAt(i)
350
+ }
351
+ return new Blob([ab], { type })
352
+ // #endif
353
+ }
354
+ /**
355
+ * 将canvas转换为png图片
356
+ * @param canvas canvas元素
357
+ * @returns 图片路径
358
+ */
359
+ export function canvasToPng(canvas: HTMLCanvasElement): Promise<string> {
360
+ return new Promise((resolve) => {
361
+ // #ifdef APP
362
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
363
+ // @ts-ignore
364
+ canvas.parentElement!.takeSnapshot({
365
+ success(res: { tempFilePath: string; errMsg?: string }) {
366
+ resolve(res.tempFilePath)
367
+ },
368
+ fail(err: { errMsg: string }) {
369
+ console.error(err)
370
+ resolve('')
371
+ }
372
+ })
373
+ // #endif
374
+
375
+ // #ifdef H5
376
+ const url = URL.createObjectURL(base64ToBlob(canvas.toDataURL('image/png', 1) ?? ''))
377
+ resolve(url)
378
+ // #endif
379
+
380
+ // #ifdef MP
381
+ const data = canvas.toDataURL('image/png', 1)
382
+ const fileMg = uni.getFileSystemManager()
383
+ const filepath = `${wx.env.USER_DATA_PATH}/${uuid()}.png`
384
+ fileMg.writeFile({
385
+ filePath: filepath,
386
+ data: data.split(',')[1],
387
+ encoding: 'base64',
388
+ success() {
389
+ resolve(filepath)
390
+ },
391
+ fail(err) {
392
+ console.error(err)
393
+ resolve('')
394
+ }
395
+ })
396
+ // #endif
397
+ })
398
+ }
@@ -0,0 +1,2 @@
1
+ @import "./../common/abstracts/_mixin.scss";
2
+ @import "./../common/abstracts/variable.scss";
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <view :class="['qrcode', customClass]" :style="customStyle">
3
+ <canvas :canvas-id="qrcodeId" type="2d" :id="qrcodeId" :style="{ width: getUnit(width), height: getUnit(height) }"></canvas>
4
+ </view>
5
+ </template>
6
+ <script lang="ts">
7
+ export default {
8
+ name: 'oxy-qrcode',
9
+ options: {
10
+ virtualHost: true,
11
+ addGlobalClass: true,
12
+ styleIsolation: 'shared'
13
+ }
14
+ }
15
+ </script>
16
+ <script lang="ts" setup>
17
+ import { ref, watch, onMounted, getCurrentInstance, nextTick, computed, onUnmounted, shallowRef } from 'vue'
18
+ import { qrcodeProps } from './types'
19
+ import { canvasToPng, drawQrcode, isAppIOS, isHarmony, type QrcodeOptions } from './draw'
20
+ import { unitConvert, unitConvertWithDefault, withDefaultUnit, uuid } from '../common/util'
21
+ import { canvas2dAdapter } from '../common/canvasHelper'
22
+ const props = defineProps(qrcodeProps)
23
+
24
+ // 二维码组件id
25
+ const qrcodeId = ref<string>('oxy-qrcode-' + uuid())
26
+
27
+ /**
28
+ * 主绘制方法,根据当前 props 生成二维码并绘制到 canvas。
29
+ * 支持多平台(APP、H5、微信小程序),自动适配高分屏。
30
+ * 内部调用 drawQrcode 进行二维码点阵绘制。
31
+ */
32
+ function getUnitNumber(value: number | string) {
33
+ return unitConvertWithDefault(value, { defaultUnit: 'rpx' })
34
+ }
35
+
36
+ function drawer() {
37
+ const data = {
38
+ text: props.text,
39
+ size: getUnitNumber(props.width),
40
+ foreground: props.foreground,
41
+ background: props.background,
42
+ padding: getUnitNumber(props.padding),
43
+ logo: props.logo,
44
+ logoSize: getUnitNumber(props.logoSize),
45
+ ecc: props.ecc,
46
+ mode: props.mode,
47
+ pdColor: props.pdColor,
48
+ pdRadius: getUnitNumber(props.pdRadius)
49
+ } as QrcodeOptions
50
+
51
+ nextTick(() => {
52
+ uni
53
+ .createSelectorQuery()
54
+ .select('#' + qrcodeId.value)
55
+ .fields({ node: true, size: true }, () => {})
56
+ .exec((res) => {
57
+ if (res[0]) {
58
+ const canvas = res[0].node
59
+ const ctx = canvas2dAdapter(canvas.getContext('2d') as CanvasRenderingContext2D)
60
+
61
+ // 获取设备像素比,用于高清屏适配
62
+ const dpr = uni.getSystemInfoSync().pixelRatio || 1
63
+ // 设置canvas的物理尺寸为CSS尺寸乘以像素比
64
+ canvas.width = res[0].width * dpr
65
+ canvas.height = res[0].height * dpr
66
+ // 缩放绘图上下文,使绘制逻辑使用CSS尺寸
67
+ ctx.scale(1, 1)
68
+ drawQrcode(ctx, data)
69
+ }
70
+ })
71
+ })
72
+ }
73
+ /**
74
+ * 获取当前二维码图片的临时文件地址
75
+ * @returns Promise返回图片路径,失败返回空字符串
76
+ */
77
+ function toPng(): Promise<string> {
78
+ return new Promise((resolve) => {
79
+ uni
80
+ .createSelectorQuery()
81
+ .select('#' + qrcodeId.value)
82
+ .fields({ node: true }, () => {})
83
+ .exec((res) => {
84
+ if (res[0] && res[0].node) {
85
+ canvasToPng(res[0].node).then(resolve)
86
+ } else {
87
+ resolve('')
88
+ }
89
+ })
90
+ })
91
+ }
92
+
93
+ // 自动重绘
94
+ const stopWatch = watch(
95
+ computed(() => [props.foreground, props.background, props.text, props.logo, props.logoSize, props.mode, props.padding, props.pdRadius]),
96
+ () => {
97
+ drawer()
98
+ }
99
+ )
100
+
101
+ function getUnit(value: number | string) {
102
+ return unitConvert(withDefaultUnit(value, 'rpx'), 0, { output: 'px' })
103
+ }
104
+
105
+ onMounted(() => {
106
+ setTimeout(
107
+ () => {
108
+ drawer()
109
+ },
110
+ isHarmony() || isAppIOS() ? 50 : 0
111
+ )
112
+ })
113
+
114
+ onUnmounted(() => {
115
+ stopWatch()
116
+ })
117
+
118
+ defineExpose({
119
+ toPng
120
+ })
121
+ </script>
122
+ <style lang="scss" scoped>
123
+ @import './index.scss';
124
+ </style>