oxy-uni-ui 1.2.3 → 2.0.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.
- package/attributes.json +1 -1
- package/components/common/abstracts/variable.scss +353 -328
- package/components/common/util.ts +185 -32
- package/components/composables/index.ts +1 -0
- package/components/composables/usePopover.ts +24 -20
- package/components/composables/useVirtualScroll.ts +10 -9
- package/components/composables/useWindowResize.ts +35 -0
- package/components/oxy-action-sheet/index.scss +24 -11
- package/components/oxy-action-sheet/oxy-action-sheet.vue +27 -19
- package/components/oxy-action-sheet/types.ts +7 -0
- package/components/oxy-backtop/index.scss +3 -3
- package/components/oxy-backtop/oxy-backtop.vue +9 -6
- package/components/oxy-backtop/types.ts +7 -7
- package/components/oxy-badge/index.scss +4 -4
- package/components/oxy-badge/oxy-badge.vue +3 -3
- package/components/oxy-badge/types.ts +2 -2
- package/components/oxy-button/index.scss +5 -5
- package/components/oxy-button/oxy-button.vue +5 -1
- package/components/oxy-calendar/index.scss +11 -11
- package/components/oxy-calendar/oxy-calendar.vue +1 -0
- package/components/oxy-calendar/types.ts +5 -0
- package/components/oxy-calendar-view/month/index.scss +4 -4
- package/components/oxy-calendar-view/month/types.ts +36 -0
- package/components/oxy-calendar-view/monthPanel/index.scss +7 -7
- package/components/oxy-calendar-view/monthPanel/month-panel.vue +14 -8
- package/components/oxy-calendar-view/year/index.scss +4 -4
- package/components/oxy-calendar-view/yearPanel/index.scss +4 -4
- package/components/oxy-calendar-view/yearPanel/year-panel.vue +21 -5
- package/components/oxy-card/index.scss +2 -2
- package/components/oxy-cell/index.scss +8 -8
- package/components/oxy-checkbox/index.scss +7 -7
- package/components/oxy-checkbox-group/index.scss +2 -2
- package/components/oxy-circle/oxy-circle.vue +10 -7
- package/components/oxy-circle/types.ts +5 -5
- package/components/oxy-col/oxy-col.vue +2 -2
- package/components/oxy-col-picker/index.scss +4 -4
- package/components/oxy-col-picker/oxy-col-picker.vue +6 -5
- package/components/oxy-col-picker/types.ts +7 -2
- package/components/oxy-collapse/index.scss +2 -2
- package/components/oxy-collapse-item/oxy-collapse-item.vue +3 -3
- package/components/oxy-corner/index.scss +32 -32
- package/components/oxy-count-to/oxy-count-to.vue +3 -3
- package/components/oxy-curtain/index.scss +15 -15
- package/components/oxy-curtain/oxy-curtain.vue +4 -2
- package/components/oxy-curtain/types.ts +6 -1
- package/components/oxy-date-strip/oxy-date-strip.vue +2 -2
- package/components/oxy-date-strip/types.ts +1 -1
- package/components/oxy-date-strip-item/index.scss +3 -3
- package/components/oxy-datetime-picker/index.scss +11 -11
- package/components/oxy-datetime-picker/oxy-datetime-picker.vue +1 -0
- package/components/oxy-datetime-picker/types.ts +5 -0
- package/components/oxy-drop-menu/index.scss +3 -3
- package/components/oxy-drop-menu/oxy-drop-menu.vue +3 -3
- package/components/oxy-drop-menu-item/index.scss +1 -1
- package/components/oxy-drop-menu-item/oxy-drop-menu-item.vue +4 -3
- package/components/oxy-drop-menu-item/types.ts +5 -0
- package/components/oxy-echarts/types.ts +6 -0
- package/components/oxy-fab/index.scss +8 -8
- package/components/oxy-fab/oxy-fab.vue +22 -3
- package/components/oxy-file-list/index.scss +24 -23
- package/components/oxy-file-list/oxy-file-list.vue +2 -2
- package/components/oxy-floating-panel/oxy-floating-panel.vue +13 -9
- package/components/oxy-floating-panel/{type.ts → types.ts} +8 -8
- package/components/oxy-footer/index.scss +19 -0
- package/components/oxy-footer/oxy-footer.vue +78 -0
- package/components/oxy-footer/types.ts +17 -0
- package/components/oxy-form-item/types.ts +22 -1
- package/components/oxy-gap/oxy-gap.vue +2 -2
- package/components/oxy-gap/types.ts +2 -2
- package/components/oxy-grid/oxy-grid.vue +1 -1
- package/components/oxy-grid/types.ts +1 -1
- package/components/oxy-grid-item/index.scss +1 -1
- package/components/oxy-grid-item/oxy-grid-item.vue +7 -5
- package/components/oxy-grid-item/types.ts +1 -1
- package/components/oxy-guidance/index.scss +75 -0
- package/components/oxy-guidance/oxy-guidance.vue +201 -0
- package/components/oxy-guidance/types.ts +33 -0
- package/components/oxy-icon/oxy-icon.vue +2 -2
- package/components/oxy-icon/types.ts +1 -1
- package/components/oxy-img/oxy-img.vue +4 -4
- package/components/oxy-img/types.ts +3 -3
- package/components/oxy-img-cropper/index.scss +12 -12
- package/components/oxy-img-cropper/oxy-img-cropper.vue +97 -52
- package/components/oxy-img-cropper/types.ts +2 -2
- package/components/oxy-img-lazy/oxy-img-lazy.vue +3 -3
- package/components/oxy-img-lazy/types.ts +3 -3
- package/components/oxy-index-anchor/index.scss +2 -2
- package/components/oxy-index-anchor/oxy-index-anchor.vue +2 -2
- package/components/oxy-index-anchor/{type.ts → types.ts} +3 -0
- package/components/oxy-index-bar/index.scss +3 -3
- package/components/oxy-index-bar/oxy-index-bar.vue +3 -3
- package/components/oxy-index-bar/{type.ts → types.ts} +2 -2
- package/components/oxy-input/index.scss +1 -1
- package/components/oxy-input-number/index.scss +5 -5
- package/components/oxy-input-number/oxy-input-number.vue +2 -2
- package/components/oxy-input-number/types.ts +3 -2
- package/components/oxy-keyboard/index.scss +5 -5
- package/components/oxy-keyboard/key/index.scss +3 -3
- package/components/oxy-keyboard/key/index.vue +2 -2
- package/components/oxy-keyboard/key/types.ts +15 -0
- package/components/oxy-keyboard/oxy-keyboard.vue +1 -0
- package/components/oxy-keyboard/types.ts +5 -0
- package/components/oxy-link/index.scss +2 -2
- package/components/oxy-list/oxy-list.vue +4 -3
- package/components/oxy-loading/oxy-loading.vue +8 -4
- package/components/oxy-loading/types.ts +1 -1
- package/components/oxy-loadmore/index.scss +3 -3
- package/components/oxy-long-press-menu/index.scss +93 -0
- package/components/oxy-long-press-menu/oxy-long-press-menu.vue +338 -0
- package/components/oxy-long-press-menu/types.ts +34 -0
- package/components/oxy-message-box/index.scss +12 -11
- package/components/oxy-message-box/oxy-message-box.vue +11 -3
- package/components/oxy-message-box/types.ts +14 -0
- package/components/oxy-navbar/index.scss +2 -2
- package/components/oxy-navbar/oxy-navbar.vue +58 -13
- package/components/oxy-navbar/types.ts +8 -1
- package/components/oxy-navbar-capsule/types.ts +3 -0
- package/components/oxy-notice-bar/index.scss +3 -3
- package/components/oxy-notice-bar/oxy-notice-bar.vue +9 -5
- package/components/oxy-notice-bar/types.ts +3 -3
- package/components/oxy-notify/index.ts +1 -0
- package/components/oxy-notify/oxy-notify.vue +3 -2
- package/components/oxy-notify/types.ts +7 -0
- package/components/oxy-pagination/index.scss +1 -1
- package/components/oxy-password-input/oxy-password-input.vue +2 -2
- package/components/oxy-password-input/types.ts +1 -1
- package/components/oxy-picker/index.scss +45 -2
- package/components/oxy-picker/oxy-picker.vue +100 -14
- package/components/oxy-picker/types.ts +29 -1
- package/components/oxy-picker-view/index.scss +3 -3
- package/components/oxy-picker-view/oxy-picker-view.vue +4 -4
- package/components/oxy-popover/index.scss +9 -9
- package/components/oxy-popup/index.scss +2 -2
- package/components/oxy-popup/oxy-popup.vue +35 -2
- package/components/oxy-popup/types.ts +8 -1
- package/components/oxy-progress/index.scss +3 -3
- package/components/oxy-qrcode/draw.ts +398 -0
- package/components/oxy-qrcode/index.scss +2 -0
- package/components/oxy-qrcode/oxy-qrcode.vue +124 -0
- package/components/oxy-qrcode/qrcode.ts +936 -0
- package/components/oxy-qrcode/types.ts +42 -0
- package/components/oxy-radio/index.scss +10 -10
- package/components/oxy-radio-group/index.scss +2 -2
- package/components/oxy-rate/types.ts +4 -4
- package/components/oxy-resize/index.scss +2 -2
- package/components/oxy-resize/oxy-resize.vue +4 -4
- package/components/oxy-resize/types.ts +3 -0
- package/components/oxy-rich-text/index.scss +30 -29
- package/components/oxy-rich-text/mp-html/mp-html.vue +33 -24
- package/components/oxy-rich-text/mp-html/node/node.vue +30 -19
- package/components/oxy-rich-text/oxy-rich-text.vue +31 -31
- package/components/oxy-rich-text/types.ts +6 -1
- package/components/oxy-row/oxy-row.vue +3 -3
- package/components/oxy-row/types.ts +1 -1
- package/components/oxy-search/index.scss +3 -3
- package/components/oxy-segmented/index.scss +16 -16
- package/components/oxy-segmented/oxy-segmented.vue +23 -3
- package/components/oxy-select/index.scss +144 -68
- package/components/oxy-select/oxy-select.vue +85 -50
- package/components/oxy-select/types.ts +13 -1
- package/components/oxy-select-picker/index.scss +7 -7
- package/components/oxy-select-picker/oxy-select-picker.vue +1 -0
- package/components/oxy-select-picker/types.ts +2 -0
- package/components/oxy-sidebar-item/index.scss +1 -1
- package/components/oxy-signature/oxy-signature.vue +18 -10
- package/components/oxy-signature/types.ts +106 -13
- package/components/oxy-skeleton/oxy-skeleton.vue +6 -6
- package/components/oxy-skeleton/types.ts +1 -1
- package/components/oxy-slider/index.scss +3 -3
- package/components/oxy-sort-button/index.scss +8 -8
- package/components/oxy-status-tip/index.scss +4 -4
- package/components/oxy-status-tip/oxy-status-tip.vue +5 -5
- package/components/oxy-status-tip/types.ts +3 -3
- package/components/oxy-step/index.scss +14 -14
- package/components/oxy-sticky/oxy-sticky.vue +6 -6
- package/components/oxy-stream-render/types.ts +4 -1
- package/components/oxy-swipe-action/oxy-swipe-action.vue +27 -2
- package/components/oxy-swiper/oxy-swiper.vue +6 -6
- package/components/oxy-swiper/types.ts +5 -5
- package/components/oxy-switch/index.scss +8 -8
- package/components/oxy-switch/oxy-switch.vue +2 -2
- package/components/oxy-switch/types.ts +1 -1
- package/components/oxy-tab/index.scss +11 -1
- package/components/oxy-tabbar/index.scss +1 -1
- package/components/oxy-tabbar/oxy-tabbar.vue +39 -10
- package/components/oxy-table/index.scss +5 -5
- package/components/oxy-table/oxy-table.vue +8 -6
- package/components/oxy-table/types.ts +2 -2
- package/components/oxy-table-col/oxy-table-col.vue +3 -3
- package/components/oxy-table-col/types.ts +2 -2
- package/components/oxy-tabs/index.scss +43 -15
- package/components/oxy-tabs/oxy-tabs.vue +53 -19
- package/components/oxy-tabs/types.ts +15 -3
- package/components/oxy-tag/index.scss +15 -15
- package/components/oxy-text/index.scss +5 -1
- package/components/oxy-text/oxy-text.vue +76 -7
- package/components/oxy-text/types.ts +12 -0
- package/components/oxy-textarea/index.scss +6 -6
- package/components/oxy-toast/oxy-toast.vue +24 -8
- package/components/oxy-tooltip/index.scss +9 -9
- package/components/oxy-tree/index.scss +51 -15
- package/components/oxy-tree/oxy-tree.vue +13 -9
- package/components/oxy-tree/types.ts +12 -9
- package/components/oxy-upload/index.scss +21 -21
- package/components/oxy-upload/types.ts +2 -2
- package/components/oxy-verification-code/index.scss +6 -0
- package/components/oxy-verification-code/oxy-verification-code.vue +187 -0
- package/components/oxy-verification-code/types.ts +82 -0
- package/components/oxy-video-preview/index.scss +4 -4
- package/components/oxy-virtual-scroll/index.scss +4 -4
- package/components/oxy-virtual-scroll/oxy-virtual-scroll.vue +11 -7
- package/components/oxy-virtual-scroll/types.ts +14 -14
- package/components/oxy-voice-player/index.scss +908 -0
- package/components/oxy-voice-player/oxy-voice-player.vue +821 -0
- package/components/oxy-voice-player/types.ts +567 -0
- package/components/oxy-waterfall/oxy-waterfall.vue +6 -6
- package/components/oxy-waterfall/types.ts +6 -6
- package/components/oxy-watermark/oxy-watermark.vue +35 -13
- package/components/oxy-watermark/types.ts +14 -14
- package/global.d.ts +2 -0
- package/locale/lang/ar-SA.ts +3 -0
- package/locale/lang/en-US.ts +3 -0
- package/locale/lang/zh-CN.ts +3 -0
- package/package.json +97 -1
- package/tags.json +1 -1
- package/web-types.json +1 -1
- package/components/oxy-number-keyboard/index.scss +0 -78
- package/components/oxy-number-keyboard/key/index.scss +0 -81
- package/components/oxy-number-keyboard/key/index.vue +0 -78
- package/components/oxy-number-keyboard/key/types.ts +0 -11
- package/components/oxy-number-keyboard/oxy-number-keyboard.vue +0 -151
- package/components/oxy-number-keyboard/types.ts +0 -83
- package/components/oxy-tree/components/tree-node-content.vue +0 -72
- package/components/oxy-tree/index.ts +0 -51
- package/oxy-uni-ui.zip +0 -0
|
@@ -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,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>
|