@puresnake/snake-ui 0.0.0-stage → 0.0.2

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 (279) hide show
  1. package/LICENSE +21 -0
  2. package/changelog.md +83 -0
  3. package/components/snake-activity-list/snake-activity-list.vue +365 -0
  4. package/components/snake-activity-list/types.ts +29 -0
  5. package/components/snake-auxiliary-segmentation/snake-auxiliary-segmentation.vue +44 -0
  6. package/components/snake-auxiliary-segmentation/types.ts +5 -0
  7. package/components/snake-button/snake-button.vue +357 -0
  8. package/components/snake-button/types.ts +103 -0
  9. package/components/snake-button-sort/snake-button-sort.vue +72 -0
  10. package/components/snake-button-sort/types.ts +22 -0
  11. package/components/snake-calendar/snake-calendar.vue +998 -0
  12. package/components/snake-calendar/types.ts +189 -0
  13. package/components/snake-cell-group/snake-cell-group.vue +55 -0
  14. package/components/snake-cell-group/types.ts +19 -0
  15. package/components/snake-cell-item/snake-cell-item.vue +159 -0
  16. package/components/snake-cell-item/types.ts +66 -0
  17. package/components/snake-checkbox/snake-checkbox.vue +264 -0
  18. package/components/snake-checkbox/types.ts +70 -0
  19. package/components/snake-checkbox-group/snake-checkbox-group.vue +154 -0
  20. package/components/snake-checkbox-group/types.ts +52 -0
  21. package/components/snake-cms/blocks/v2/cms-v2-carousel.vue +87 -0
  22. package/components/snake-cms/blocks/v2/cms-v2-coupon.vue +25 -0
  23. package/components/snake-cms/blocks/v2/cms-v2-custom.vue +12 -0
  24. package/components/snake-cms/blocks/v2/cms-v2-divider.vue +33 -0
  25. package/components/snake-cms/blocks/v2/cms-v2-floatbutton.vue +28 -0
  26. package/components/snake-cms/blocks/v2/cms-v2-goods-list.vue +150 -0
  27. package/components/snake-cms/blocks/v2/cms-v2-hotzone.vue +41 -0
  28. package/components/snake-cms/blocks/v2/cms-v2-images.vue +41 -0
  29. package/components/snake-cms/blocks/v2/cms-v2-magiccube.vue +93 -0
  30. package/components/snake-cms/blocks/v2/cms-v2-notice.vue +80 -0
  31. package/components/snake-cms/blocks/v2/cms-v2-popup.vue +46 -0
  32. package/components/snake-cms/blocks/v2/cms-v2-richtext.vue +18 -0
  33. package/components/snake-cms/blocks/v2/cms-v2-search.vue +115 -0
  34. package/components/snake-cms/blocks/v2/cms-v2-video.vue +25 -0
  35. package/components/snake-cms/blocks/v2/shared.ts +84 -0
  36. package/components/snake-cms/snake-cms.vue +409 -0
  37. package/components/snake-cms/types.ts +50 -0
  38. package/components/snake-commodity-search/snake-commodity-search.vue +234 -0
  39. package/components/snake-commodity-search/types.ts +11 -0
  40. package/components/snake-config-provider/snake-config-provider.vue +61 -0
  41. package/components/snake-config-provider/types.ts +22 -0
  42. package/components/snake-count-down/snake-count-down.vue +231 -0
  43. package/components/snake-count-down/types.ts +103 -0
  44. package/components/snake-coupon/snake-coupon.vue +209 -0
  45. package/components/snake-coupon/types.ts +21 -0
  46. package/components/snake-custom-module/snake-custom-module.vue +25 -0
  47. package/components/snake-custom-module/types.ts +11 -0
  48. package/components/snake-drop-menu/snake-drop-menu.vue +123 -0
  49. package/components/snake-drop-menu/types.ts +95 -0
  50. package/components/snake-drop-menu-item/snake-drop-menu-item.vue +181 -0
  51. package/components/snake-drop-menu-item/types.ts +48 -0
  52. package/components/snake-empty/snake-empty.vue +100 -0
  53. package/components/snake-empty/types.ts +48 -0
  54. package/components/snake-filter-sorts/filter-page.vue +695 -0
  55. package/components/snake-filter-sorts/snake-filter-sorts.vue +185 -0
  56. package/components/snake-filter-sorts/types.ts +57 -0
  57. package/components/snake-floating-bubble/snake-floating-bubble.vue +157 -0
  58. package/components/snake-floating-bubble/types.ts +43 -0
  59. package/components/snake-follow/snake-follow.vue +18 -0
  60. package/components/snake-follow/types.ts +2 -0
  61. package/components/snake-form/snake-form.vue +124 -0
  62. package/components/snake-form/types.ts +96 -0
  63. package/components/snake-form-item/snake-form-item.vue +331 -0
  64. package/components/snake-form-item/types.ts +70 -0
  65. package/components/snake-goods-list/snake-goods-list.vue +792 -0
  66. package/components/snake-goods-list/types.ts +55 -0
  67. package/components/snake-graphic-navigation/snake-graphic-navigation.vue +198 -0
  68. package/components/snake-graphic-navigation/types.ts +43 -0
  69. package/components/snake-icon/icon-names.ts +222 -0
  70. package/components/snake-icon/snake-icon.vue +220 -0
  71. package/components/snake-icon/types.ts +45 -0
  72. package/components/snake-image/snake-image.vue +230 -0
  73. package/components/snake-image/types.ts +70 -0
  74. package/components/snake-images/snake-images.vue +80 -0
  75. package/components/snake-images/types.ts +5 -0
  76. package/components/snake-index-anchor/snake-index-anchor.vue +83 -0
  77. package/components/snake-index-anchor/types.ts +14 -0
  78. package/components/snake-index-list/anchor-registry.ts +80 -0
  79. package/components/snake-index-list/snake-index-list.vue +259 -0
  80. package/components/snake-index-list/types.ts +115 -0
  81. package/components/snake-index-list-panel/snake-index-list-panel.vue +277 -0
  82. package/components/snake-index-list-panel/types.ts +10 -0
  83. package/components/snake-input/snake-input.vue +225 -0
  84. package/components/snake-input/types.ts +99 -0
  85. package/components/snake-jump-applet/snake-jump-applet.vue +99 -0
  86. package/components/snake-jump-applet/types.ts +17 -0
  87. package/components/snake-line/snake-line.vue +42 -0
  88. package/components/snake-line/types.ts +30 -0
  89. package/components/snake-line-progress/snake-line-progress.vue +105 -0
  90. package/components/snake-line-progress/types.ts +26 -0
  91. package/components/snake-link/snake-link.vue +74 -0
  92. package/components/snake-link/types.ts +30 -0
  93. package/components/snake-list-switching/components/tree-select/index.vue +156 -0
  94. package/components/snake-list-switching/snake-list-switching.vue +1285 -0
  95. package/components/snake-list-switching/types.ts +11 -0
  96. package/components/snake-loading/snake-loading.vue +241 -0
  97. package/components/snake-loading/types.ts +34 -0
  98. package/components/snake-loadmore/snake-loadmore.vue +103 -0
  99. package/components/snake-loadmore/types.ts +56 -0
  100. package/components/snake-magic-cube/snake-magic-cube.vue +622 -0
  101. package/components/snake-magic-cube/types.ts +5 -0
  102. package/components/snake-modal/snake-modal.vue +434 -0
  103. package/components/snake-modal/types.ts +71 -0
  104. package/components/snake-navbar/snake-navbar.vue +194 -0
  105. package/components/snake-navbar/types.ts +54 -0
  106. package/components/snake-no-network/snake-no-network.vue +125 -0
  107. package/components/snake-no-network/types.ts +27 -0
  108. package/components/snake-notice/snake-notice.vue +44 -0
  109. package/components/snake-notice/types.ts +5 -0
  110. package/components/snake-notice-bar/snake-notice-bar.vue +336 -0
  111. package/components/snake-notice-bar/types.ts +82 -0
  112. package/components/snake-overlay/snake-overlay.vue +73 -0
  113. package/components/snake-overlay/types.ts +35 -0
  114. package/components/snake-page-popup/snake-page-popup.vue +146 -0
  115. package/components/snake-page-popup/types.ts +28 -0
  116. package/components/snake-parse/audio/audio.vue +286 -0
  117. package/components/snake-parse/audio/context.js +11 -0
  118. package/components/snake-parse/audio/index.js +34 -0
  119. package/components/snake-parse/node/node.vue +830 -0
  120. package/components/snake-parse/parser.js +1480 -0
  121. package/components/snake-parse/snake-parse.vue +548 -0
  122. package/components/snake-parse/static/app-plus/snake-parse/js/handler.js +109 -0
  123. package/components/snake-parse/static/app-plus/snake-parse/js/uni.webview.js +0 -0
  124. package/components/snake-parse/static/app-plus/snake-parse/js/uni.webview.min.js +1 -0
  125. package/components/snake-parse/static/app-plus/snake-parse/local.html +3 -0
  126. package/components/snake-parse/style/index.js +132 -0
  127. package/components/snake-parse/style/parser.js +175 -0
  128. package/components/snake-parse/types.ts +49 -0
  129. package/components/snake-picture-ads/snake-picture-ads.vue +292 -0
  130. package/components/snake-picture-ads/types.ts +5 -0
  131. package/components/snake-popup/snake-popup.vue +348 -0
  132. package/components/snake-popup/types.ts +88 -0
  133. package/components/snake-qr-code/qrcode.ts +1214 -0
  134. package/components/snake-qr-code/snake-qr-code.vue +408 -0
  135. package/components/snake-qr-code/types.ts +78 -0
  136. package/components/snake-radio/snake-radio.vue +271 -0
  137. package/components/snake-radio/types.ts +64 -0
  138. package/components/snake-radio-group/snake-radio-group.vue +98 -0
  139. package/components/snake-radio-group/types.ts +48 -0
  140. package/components/snake-read-more/snake-read-more.vue +140 -0
  141. package/components/snake-read-more/types.ts +53 -0
  142. package/components/snake-rich-text/snake-rich-text.vue +24 -0
  143. package/components/snake-rich-text/types.ts +5 -0
  144. package/components/snake-root-portal/snake-root-portal.vue +92 -0
  145. package/components/snake-root-portal/types.ts +9 -0
  146. package/components/snake-search/snake-search.vue +202 -0
  147. package/components/snake-search/types.ts +84 -0
  148. package/components/snake-select/snake-select.vue +371 -0
  149. package/components/snake-select/types.ts +75 -0
  150. package/components/snake-skeleton/snake-skeleton.vue +104 -0
  151. package/components/snake-skeleton/types.ts +32 -0
  152. package/components/snake-skeleton-item/snake-skeleton-item.vue +90 -0
  153. package/components/snake-skeleton-item/types.ts +30 -0
  154. package/components/snake-slider/snake-slider.vue +249 -0
  155. package/components/snake-slider/types.ts +59 -0
  156. package/components/snake-sticky/snake-sticky.vue +178 -0
  157. package/components/snake-sticky/types.ts +48 -0
  158. package/components/snake-subsection/snake-subsection.vue +188 -0
  159. package/components/snake-subsection/types.ts +54 -0
  160. package/components/snake-swiper/snake-swiper.vue +229 -0
  161. package/components/snake-swiper/types.ts +79 -0
  162. package/components/snake-switch/snake-switch.vue +100 -0
  163. package/components/snake-switch/types.ts +41 -0
  164. package/components/snake-tabbar/snake-tabbar.vue +306 -0
  165. package/components/snake-tabbar/types.ts +93 -0
  166. package/components/snake-tabs/snake-tabs.vue +305 -0
  167. package/components/snake-tabs/types.ts +79 -0
  168. package/components/snake-text/snake-text.vue +292 -0
  169. package/components/snake-text/types.ts +100 -0
  170. package/components/snake-textarea/snake-textarea.vue +171 -0
  171. package/components/snake-textarea/types.ts +79 -0
  172. package/components/snake-toast/snake-toast.vue +330 -0
  173. package/components/snake-toast/types.ts +55 -0
  174. package/components/snake-tooltip/position.ts +33 -0
  175. package/components/snake-tooltip/snake-tooltip.vue +339 -0
  176. package/components/snake-tooltip/types.ts +80 -0
  177. package/components/snake-transition/snake-transition.vue +315 -0
  178. package/components/snake-transition/types.ts +65 -0
  179. package/components/snake-upload/snake-upload.vue +1801 -0
  180. package/components/snake-upload/types.ts +256 -0
  181. package/components/snake-videos/snake-videos.vue +122 -0
  182. package/components/snake-videos/types.ts +7 -0
  183. package/iconfont.css +881 -0
  184. package/index.d.ts +7 -0
  185. package/index.scss +26 -0
  186. package/index.ts +94 -0
  187. package/libs/cms/blocks.ts +103 -0
  188. package/libs/cms/config.ts +25 -0
  189. package/libs/cms/index.ts +7 -0
  190. package/libs/cms/normalize.ts +135 -0
  191. package/libs/cms/resolve.ts +12 -0
  192. package/libs/cms/types.ts +164 -0
  193. package/libs/cms/use-commerce.ts +385 -0
  194. package/libs/cms/v2.ts +193 -0
  195. package/libs/config/index.ts +4 -0
  196. package/libs/config/modal.ts +93 -0
  197. package/libs/config/state.ts +33 -0
  198. package/libs/config/theme.ts +351 -0
  199. package/libs/config/toast.ts +71 -0
  200. package/libs/config/types.ts +135 -0
  201. package/libs/config/zIndex.ts +44 -0
  202. package/libs/css/color.scss +105 -0
  203. package/libs/css/common.scss +171 -0
  204. package/libs/css/style.h5.scss +7 -0
  205. package/libs/css/style.mp.scss +7 -0
  206. package/libs/css/style.theme.scss +70 -0
  207. package/libs/css/style.vue.scss +140 -0
  208. package/libs/form/async-validator.min.d.ts +75 -0
  209. package/libs/form/async-validator.min.js +3 -0
  210. package/libs/form/index.ts +3 -0
  211. package/libs/form/path.ts +40 -0
  212. package/libs/form/types.ts +51 -0
  213. package/libs/form/validate.ts +149 -0
  214. package/libs/foundation.ts +96 -0
  215. package/libs/function/$parent.ts +33 -0
  216. package/libs/function/clipboard.ts +13 -0
  217. package/libs/function/color.ts +55 -0
  218. package/libs/function/debounce.ts +34 -0
  219. package/libs/function/deepClone.ts +30 -0
  220. package/libs/function/deepMerge.ts +25 -0
  221. package/libs/function/emitter.ts +30 -0
  222. package/libs/function/format.ts +27 -0
  223. package/libs/function/getParent.ts +53 -0
  224. package/libs/function/getRect.ts +29 -0
  225. package/libs/function/guid.ts +11 -0
  226. package/libs/function/index.ts +31 -0
  227. package/libs/function/queryParams.ts +73 -0
  228. package/libs/function/random.ts +6 -0
  229. package/libs/function/randomArray.ts +14 -0
  230. package/libs/function/route.ts +155 -0
  231. package/libs/function/sleep.ts +7 -0
  232. package/libs/function/style.ts +58 -0
  233. package/libs/function/styleUtils.ts +83 -0
  234. package/libs/function/system.ts +14 -0
  235. package/libs/function/test.ts +202 -0
  236. package/libs/function/throttle.ts +36 -0
  237. package/libs/function/timeFormat.ts +27 -0
  238. package/libs/function/timeFrom.ts +27 -0
  239. package/libs/function/toast.ts +10 -0
  240. package/libs/function/trim.ts +11 -0
  241. package/libs/function/type2icon.ts +11 -0
  242. package/libs/function/unit.ts +45 -0
  243. package/libs/hooks/index.ts +11 -0
  244. package/libs/hooks/useChildren.ts +125 -0
  245. package/libs/hooks/useColor.ts +5 -0
  246. package/libs/hooks/useDebounce.ts +5 -0
  247. package/libs/hooks/useLocale.ts +13 -0
  248. package/libs/hooks/useModal.ts +131 -0
  249. package/libs/hooks/useParent.ts +62 -0
  250. package/libs/hooks/useRect.ts +44 -0
  251. package/libs/hooks/useRouter.ts +5 -0
  252. package/libs/hooks/useTheme.ts +31 -0
  253. package/libs/hooks/useThrottle.ts +5 -0
  254. package/libs/hooks/useToast.ts +116 -0
  255. package/libs/index.ts +18 -0
  256. package/libs/request/auto-http.ts +1 -0
  257. package/libs/request/index.ts +343 -0
  258. package/libs/runtime/accessors.ts +55 -0
  259. package/libs/runtime/context.ts +40 -0
  260. package/libs/runtime/index.ts +18 -0
  261. package/libs/runtime/types.ts +49 -0
  262. package/libs/util/calendar.ts +6 -0
  263. package/locale/index.ts +130 -0
  264. package/locale/lang/en-US.ts +67 -0
  265. package/locale/lang/zh-CN.ts +67 -0
  266. package/package.json +134 -5
  267. package/plugins/index.ts +1 -0
  268. package/plugins/root/README.md +86 -0
  269. package/plugins/root/index.ts +123 -0
  270. package/plugins/root/page.ts +77 -0
  271. package/plugins/root/root.ts +53 -0
  272. package/plugins/root/utils.ts +137 -0
  273. package/readme.md +79 -0
  274. package/theme.scss +118 -0
  275. package/types/components.d.ts +236 -0
  276. package/types/global.d.ts +21 -0
  277. package/types/index.d.ts +6 -0
  278. package/types/uni-app.d.ts +16 -0
  279. package/README.md +0 -3
@@ -0,0 +1,408 @@
1
+ <template>
2
+ <view class="snake-qr-code" :class="customClass" :style="customStyle" @click="handleClick">
3
+ <!-- #ifdef MP-WEIXIN -->
4
+ <canvas type="2d" :id="canvasId" :style="canvasStyle" />
5
+ <!-- #endif -->
6
+ <!-- #ifndef MP-WEIXIN -->
7
+ <canvas :id="canvasId" :canvas-id="canvasId" :width="canvasSize" :height="canvasSize" :style="canvasStyle" />
8
+ <!-- #endif -->
9
+ </view>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ /**
14
+ * @component QrCode
15
+ * @description 二维码组件,把文本或链接渲染为 canvas 二维码,支持纠错级别、颜色定制、中心 Logo 与导出临时图片。
16
+ * @see /components/qr-code
17
+ */
18
+
19
+ import { computed, getCurrentInstance, nextTick, onBeforeMount, onMounted, ref, toRaw, watch } from 'vue';
20
+ import { addUnit, getSystemInfo, guid } from '../../libs/function';
21
+ import { generateQRCode, QRErrorCorrectLevel } from './qrcode';
22
+ import { qrCodeProps } from './types';
23
+ import type { QrCodeEmits, QrCodeExpose } from './types';
24
+
25
+ defineOptions({ name: 'QrCode', options: { virtualHost: true, styleIsolation: 'shared' } });
26
+
27
+ const props = defineProps(qrCodeProps);
28
+ const emit = defineEmits<QrCodeEmits>();
29
+
30
+ const instance = getCurrentInstance();
31
+ const proxy = instance?.proxy;
32
+
33
+ /** 二维码绘制所需的画布方法子集,覆盖旧版 CanvasContext 与微信 2d 上下文两条通道。 */
34
+ interface QrCanvasContext {
35
+ clearRect(x: number, y: number, width: number, height: number): void;
36
+ setFillStyle(style: string): void;
37
+ fillRect(x: number, y: number, width: number, height: number): void;
38
+ drawImage(image: unknown, x: number, y: number, width: number, height: number): void;
39
+ draw(reserve?: boolean, callback?: () => void): void;
40
+ }
41
+
42
+ /** 微信 2d canvas 节点(按需声明用到的能力,其余平台走旧版上下文)。 */
43
+ interface WechatCanvasNode {
44
+ width: number;
45
+ height: number;
46
+ getContext: (type: '2d') => CanvasRenderingContext2D | null;
47
+ createImage?: () => WechatCanvasImage;
48
+ }
49
+
50
+ /** 微信 canvas.createImage 返回的图片对象。 */
51
+ interface WechatCanvasImage {
52
+ crossOrigin?: string;
53
+ onload?: () => void;
54
+ onerror?: (error: unknown) => void;
55
+ src: string;
56
+ }
57
+
58
+ const canvasStyle = computed(() => ({
59
+ width: addUnit(props.size, 'px'),
60
+ height: addUnit(props.size, 'px')
61
+ }));
62
+
63
+ /** Canvas 标识:同页多实例时由业务显式传入,否则自动生成。 */
64
+ const canvasId = props.canvasId || guid('snake-qr');
65
+ const canvasNode = ref<WechatCanvasNode | null>(null);
66
+ const nativeCtx = ref<CanvasRenderingContext2D | null>(null);
67
+ const canvasCtx = ref<QrCanvasContext | null>(null);
68
+ const pixelRatio = ref(1);
69
+ const isCanvasReady = ref(false);
70
+ let drawTask: Promise<void> = Promise.resolve();
71
+
72
+ /** 旧版 canvas 的位图尺寸:支付宝需要按像素比放大,其余平台与逻辑尺寸一致。 */
73
+ const canvasSize = computed(() => {
74
+ let size = props.size;
75
+ // #ifdef MP-ALIPAY
76
+ size *= pixelRatio.value;
77
+ // #endif
78
+ return size;
79
+ });
80
+
81
+ watch(
82
+ [
83
+ () => props.text,
84
+ () => props.size,
85
+ () => props.correctLevel,
86
+ () => props.colorDark,
87
+ () => props.colorLight,
88
+ () => props.margin,
89
+ () => props.logo,
90
+ () => props.logoWidth,
91
+ () => props.logoHeight
92
+ ],
93
+ (values, oldValues) => {
94
+ if (!isCanvasReady.value) return;
95
+ // #ifdef MP-ALIPAY
96
+ if (values[1] !== oldValues[1]) {
97
+ canvasCtx.value = null;
98
+ }
99
+ // #endif
100
+ requestDraw();
101
+ },
102
+ { flush: 'post' }
103
+ );
104
+
105
+ onBeforeMount(() => {
106
+ // #ifdef H5 || MP-ALIPAY
107
+ pixelRatio.value = getSystemInfo().pixelRatio || 1;
108
+ // #endif
109
+ });
110
+
111
+ onMounted(() => {
112
+ nextTick(initCanvas);
113
+ });
114
+
115
+ /** 初始化画布:微信走 2d 节点查询,其余平台绘制时按需创建旧版上下文。 */
116
+ function initCanvas() {
117
+ // #ifdef MP-WEIXIN
118
+ uni.createSelectorQuery()
119
+ .in(proxy)
120
+ .select(`#${canvasId}`)
121
+ .node((res: any) => {
122
+ if (!res?.node) {
123
+ emit('error', new Error('Canvas node not found'));
124
+ return;
125
+ }
126
+ const canvas = res.node as WechatCanvasNode;
127
+ const ctx = canvas.getContext('2d');
128
+ if (!ctx) {
129
+ emit('error', new Error('Failed to get canvas context'));
130
+ return;
131
+ }
132
+ pixelRatio.value = getSystemInfo().pixelRatio || 1;
133
+ canvasNode.value = canvas;
134
+ nativeCtx.value = ctx;
135
+ canvasCtx.value = toQrContext(ctx);
136
+ isCanvasReady.value = true;
137
+ requestDraw();
138
+ })
139
+ .exec();
140
+ // #endif
141
+
142
+ // #ifndef MP-WEIXIN
143
+ isCanvasReady.value = true;
144
+ requestDraw();
145
+ // #endif
146
+ }
147
+
148
+ /** 把微信 2d 原生上下文适配为统一绘制接口(对齐旧版 setFillStyle 语义)。 */
149
+ function toQrContext(ctx: CanvasRenderingContext2D): QrCanvasContext {
150
+ return {
151
+ clearRect: (x, y, width, height) => ctx.clearRect(x, y, width, height),
152
+ setFillStyle: style => {
153
+ ctx.fillStyle = style;
154
+ },
155
+ fillRect: (x, y, width, height) => ctx.fillRect(x, y, width, height),
156
+ drawImage: (image, x, y, width, height) => ctx.drawImage(image as CanvasImageSource, x, y, width, height),
157
+ draw: () => undefined
158
+ };
159
+ }
160
+
161
+ function requestDraw() {
162
+ drawTask = draw();
163
+ }
164
+
165
+ async function draw() {
166
+ if (!isCanvasReady.value) return;
167
+
168
+ // #ifdef MP-WEIXIN
169
+ syncWechatCanvas();
170
+ // #endif
171
+
172
+ let ctx = canvasCtx.value;
173
+ // #ifndef MP-WEIXIN
174
+ if (!ctx) {
175
+ const alipay = (globalThis as Record<string, any>).my;
176
+ const raw = alipay?.createCanvasContext
177
+ ? alipay.createCanvasContext(canvasId)
178
+ : uni.createCanvasContext(canvasId, proxy);
179
+ // #ifdef MP-ALIPAY
180
+ (raw as UniApp.CanvasContext).scale(pixelRatio.value, pixelRatio.value);
181
+ // #endif
182
+ ctx = raw as unknown as QrCanvasContext;
183
+ canvasCtx.value = ctx;
184
+ }
185
+ // #endif
186
+
187
+ if (!ctx) {
188
+ emit('error', new Error('Canvas context not available'));
189
+ return;
190
+ }
191
+
192
+ try {
193
+ ctx.clearRect(0, 0, props.size, props.size);
194
+ ctx.setFillStyle(props.colorLight);
195
+ ctx.fillRect(0, 0, props.size, props.size);
196
+
197
+ if (!props.text) {
198
+ // #ifndef MP-WEIXIN
199
+ await flushCanvas(ctx);
200
+ // #endif
201
+ return;
202
+ }
203
+
204
+ let modules: boolean[][];
205
+ try {
206
+ const level = QRErrorCorrectLevel[props.correctLevel] ?? QRErrorCorrectLevel.H;
207
+ modules = generateQRCode(props.text, { errorCorrectLevel: level }).modules;
208
+ } catch (error) {
209
+ emit('error', error);
210
+ return;
211
+ }
212
+
213
+ ctx.setFillStyle(props.colorDark);
214
+ drawSquareModules(ctx, modules);
215
+
216
+ if (props.logo) {
217
+ await drawLogo(ctx);
218
+ }
219
+
220
+ // #ifndef MP-WEIXIN
221
+ await flushCanvas(ctx);
222
+ // #endif
223
+ } catch (error) {
224
+ emit('error', error);
225
+ }
226
+ }
227
+
228
+ /** 逐行合并连续深色模块,减少 fillRect 调用次数。 */
229
+ function drawSquareModules(ctx: QrCanvasContext, modules: boolean[][]) {
230
+ const count = modules.length;
231
+ const tile = (props.size - props.margin * 2) / count;
232
+ for (let row = 0; row < count; row++) {
233
+ let runStart = -1;
234
+ for (let col = 0; col <= count; col++) {
235
+ if (col < count && modules[row][col]) {
236
+ if (runStart === -1) runStart = col;
237
+ continue;
238
+ }
239
+ if (runStart === -1) continue;
240
+ const x = Math.ceil(runStart * tile) + props.margin;
241
+ const y = Math.ceil(row * tile) + props.margin;
242
+ const width = Math.ceil(col * tile) - Math.ceil(runStart * tile);
243
+ const height = Math.ceil((row + 1) * tile) - Math.ceil(row * tile);
244
+ ctx.fillRect(x, y, width, height);
245
+ runStart = -1;
246
+ }
247
+ }
248
+ }
249
+
250
+ /** 绘制中心 Logo:微信优先用 canvas 2d 的 createImage,其余走 getImageInfo。 */
251
+ async function drawLogo(ctx: QrCanvasContext) {
252
+ const logoWidth = props.logoWidth || props.size * 0.2;
253
+ const logoHeight = props.logoHeight || logoWidth;
254
+ const x = (props.size - logoWidth) / 2;
255
+ const y = (props.size - logoHeight) / 2;
256
+
257
+ // #ifdef MP-WEIXIN
258
+ if (canvasNode.value?.createImage) {
259
+ try {
260
+ const image = await loadImageWithCreateImage(props.logo);
261
+ drawLogoContent(ctx, image, x, y, logoWidth, logoHeight);
262
+ return;
263
+ } catch (error) {
264
+ void error; // 落到 getImageInfo 兜底
265
+ }
266
+ }
267
+ // #endif
268
+
269
+ try {
270
+ const path = await loadImageWithGetImageInfo(props.logo);
271
+ drawLogoContent(ctx, path, x, y, logoWidth, logoHeight);
272
+ } catch (error) {
273
+ // Logo 加载失败时保留原矩阵不覆盖,二维码仍可扫描。
274
+ emit('error', error);
275
+ }
276
+ }
277
+
278
+ function drawLogoContent(ctx: QrCanvasContext, image: unknown, x: number, y: number, width: number, height: number) {
279
+ const padding = 4;
280
+ ctx.setFillStyle('#ffffff');
281
+ ctx.fillRect(x - padding, y - padding, width + padding * 2, height + padding * 2);
282
+ ctx.drawImage(image, x, y, width, height);
283
+ }
284
+
285
+ // #ifdef MP-WEIXIN
286
+ function loadImageWithCreateImage(src: string): Promise<WechatCanvasImage> {
287
+ return new Promise((resolve, reject) => {
288
+ const canvas = toRaw(canvasNode.value);
289
+ const createImage = canvas?.createImage;
290
+ if (!createImage) {
291
+ reject(new Error('canvas.createImage is unavailable'));
292
+ return;
293
+ }
294
+ const image = createImage();
295
+ image.crossOrigin = 'anonymous';
296
+ image.onload = () => resolve(image);
297
+ image.onerror = error => reject(error);
298
+ image.src = src;
299
+ });
300
+ }
301
+ // #endif
302
+
303
+ function loadImageWithGetImageInfo(src: string): Promise<string> {
304
+ return new Promise((resolve, reject) => {
305
+ uni.getImageInfo({ src, success: res => resolve(res.path), fail: reject });
306
+ });
307
+ }
308
+
309
+ // #ifndef MP-WEIXIN
310
+ /** 旧版画布需要显式 draw 提交;H5 上回调可能不触发,按帧兜底放行。 */
311
+ function flushCanvas(ctx: QrCanvasContext) {
312
+ return new Promise<void>(resolve => {
313
+ let settled = false;
314
+ const done = () => {
315
+ if (settled) return;
316
+ settled = true;
317
+ resolve();
318
+ };
319
+ try {
320
+ ctx.draw(false, done);
321
+ } catch (error) {
322
+ void error;
323
+ ctx.draw(false);
324
+ }
325
+ setTimeout(done, 16);
326
+ });
327
+ }
328
+ // #endif
329
+
330
+ // #ifdef MP-WEIXIN
331
+ /** 同步微信 2d 画布位图尺寸并按像素比缩放坐标系,保证清晰度。 */
332
+ function syncWechatCanvas() {
333
+ const canvas = toRaw(canvasNode.value);
334
+ const ctx = nativeCtx.value;
335
+ if (!canvas || !ctx) return;
336
+
337
+ const dpr = getSystemInfo().pixelRatio || 1;
338
+ const width = props.size * dpr;
339
+ const height = props.size * dpr;
340
+ pixelRatio.value = dpr;
341
+
342
+ if (canvas.width !== width) canvas.width = width;
343
+ if (canvas.height !== height) canvas.height = height;
344
+
345
+ if (typeof ctx.setTransform === 'function') {
346
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
347
+ } else if (typeof ctx.resetTransform === 'function') {
348
+ ctx.resetTransform();
349
+ } else {
350
+ canvas.width = width;
351
+ canvas.height = height;
352
+ }
353
+
354
+ ctx.scale(dpr, dpr);
355
+ }
356
+ // #endif
357
+
358
+ /** 等待当前绘制完成后导出二维码临时图片路径。 */
359
+ async function exportImage(): Promise<string> {
360
+ await drawTask;
361
+
362
+ return new Promise((resolve, reject) => {
363
+ const exportSize = props.size * pixelRatio.value;
364
+ let sourceSize = exportSize;
365
+ // #ifdef H5
366
+ sourceSize = props.size;
367
+ // #endif
368
+ const options = {
369
+ canvasId: canvasId,
370
+ width: sourceSize,
371
+ height: sourceSize,
372
+ destWidth: exportSize,
373
+ destHeight: exportSize,
374
+ success: (res: UniApp.CanvasToTempFilePathRes) => resolve(res.tempFilePath),
375
+ fail: reject
376
+ } as UniApp.CanvasToTempFilePathOptions & { canvas?: unknown };
377
+
378
+ // #ifdef MP-WEIXIN
379
+ if (canvasNode.value) {
380
+ options.canvas = toRaw(canvasNode.value);
381
+ }
382
+ // #endif
383
+
384
+ // #ifndef MP-ALIPAY
385
+ uni.canvasToTempFilePath(options, proxy);
386
+ // #endif
387
+ // #ifdef MP-ALIPAY
388
+ uni.canvasToTempFilePath(options);
389
+ // #endif
390
+ });
391
+ }
392
+
393
+ function handleClick(event: Event) {
394
+ emit('click', event);
395
+ }
396
+
397
+ defineExpose<QrCodeExpose>({
398
+ exportImage
399
+ });
400
+ </script>
401
+
402
+ <style scoped>
403
+ .snake-qr-code {
404
+ display: inline-block;
405
+ line-height: 0;
406
+ font-size: 0;
407
+ }
408
+ </style>
@@ -0,0 +1,78 @@
1
+ import type { ExtractPropTypes, PropType } from 'vue';
2
+ import type { StyleValue } from '../../types/global';
3
+
4
+ /** 二维码纠错级别:L 约 7%、M 约 15%、Q 约 25%、H 约 30%。 */
5
+ export type QrCodeCorrectLevel = 'L' | 'M' | 'Q' | 'H';
6
+
7
+ /** qrCodeProps 运行时属性及默认值。 */
8
+ export const qrCodeProps = {
9
+ /** 根节点自定义类名。 */
10
+ customClass: { type: String, default: '' },
11
+ /** 根节点自定义样式。 */
12
+ customStyle: { type: [String, Object, Array] as PropType<StyleValue>, default: '' },
13
+ /** 二维码内容,UTF-8 文本或链接。 */
14
+ text: { type: String, default: '' },
15
+ /** 二维码边长,单位 px。 */
16
+ size: { type: Number, default: 200 },
17
+ /** 纠错级别,带 Logo 时建议使用 H。 */
18
+ correctLevel: { type: String as PropType<QrCodeCorrectLevel>, default: 'H' },
19
+ /** 深色模块颜色。 */
20
+ colorDark: { type: String, default: '#000000' },
21
+ /** 浅色背景颜色。 */
22
+ colorLight: { type: String, default: '#FFFFFF' },
23
+ /** 二维码四周留白,单位 px。 */
24
+ margin: { type: Number, default: 0 },
25
+ /** 中心 Logo 图片地址,为空时不绘制。 */
26
+ logo: { type: String, default: '' },
27
+ /** Logo 宽度,单位 px,传 0 时取 size 的 20%。 */
28
+ logoWidth: { type: Number, default: 0 },
29
+ /** Logo 高度,单位 px,传 0 时同 logoWidth。 */
30
+ logoHeight: { type: Number, default: 0 },
31
+ /** Canvas 标识,不传时自动生成;同页多实例时建议显式指定。 */
32
+ canvasId: { type: String, default: '' }
33
+ } as const;
34
+
35
+ /** QrCodeResolvedProps 组件的解析属性。 */
36
+ export type QrCodeResolvedProps = ExtractPropTypes<typeof qrCodeProps>;
37
+
38
+ /** QrCode 组件的公开属性。 */
39
+ export interface QrCodeProps {
40
+ /** 根节点自定义类名。 */
41
+ customClass?: string;
42
+ /** 根节点自定义样式。 */
43
+ customStyle?: StyleValue;
44
+ /** 二维码内容,UTF-8 文本或链接。 */
45
+ text?: string;
46
+ /** 二维码边长,单位 px。 */
47
+ size?: number;
48
+ /** 纠错级别,带 Logo 时建议使用 H。 */
49
+ correctLevel?: QrCodeCorrectLevel;
50
+ /** 深色模块颜色。 */
51
+ colorDark?: string;
52
+ /** 浅色背景颜色。 */
53
+ colorLight?: string;
54
+ /** 二维码四周留白,单位 px。 */
55
+ margin?: number;
56
+ /** 中心 Logo 图片地址,为空时不绘制。 */
57
+ logo?: string;
58
+ /** Logo 宽度,单位 px,传 0 时取 size 的 20%。 */
59
+ logoWidth?: number;
60
+ /** Logo 高度,单位 px,传 0 时同 logoWidth。 */
61
+ logoHeight?: number;
62
+ /** Canvas 标识,不传时自动生成;同页多实例时建议显式指定。 */
63
+ canvasId?: string;
64
+ }
65
+
66
+ /** QrCode 组件的事件契约。 */
67
+ export type QrCodeEmits = {
68
+ /** 点击二维码时触发。 */
69
+ click: [event: Event];
70
+ /** 内容编码或画布绘制失败时触发。 */
71
+ error: [error: unknown];
72
+ };
73
+
74
+ /** QrCode 组件暴露的方法。 */
75
+ export interface QrCodeExpose {
76
+ /** 等待当前绘制完成后导出二维码临时图片路径。 */
77
+ exportImage: () => Promise<string>;
78
+ }