first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
:: BASE_DOC ::
## API
### QRCode Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
bg-color | String | - | QR code background color | N
borderless | Boolean | false | Is there a border | N
color | String | - | QR code color | N
icon | String | - | The address of the picture in the QR code | N
icon-size | Number / Object | 40 | The size of the picture in the QR code。Typescript`number \| { width: number; height: number }` | N
level | String | M | QR code error correction level。options: L/M/Q/H | N
size | Number | 160 | QR code size | N
status | String | active | QR code status。options: active/expired/loading/scanned。Typescript`QRStatus` `type QRStatus = "active" \| "expired" \| "loading" \| "scanned"`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/qrcode/type.ts) | N
status-render | Boolean | false | should use custom status slot | N
value | String | - | scanned text | N
### QRCode Events
name | params | description
-- | -- | --
refresh | \- | Click the "Click to refresh" callback
### QRCode Slots
name | Description
-- | --
status-render | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-font-size-title-small | --td-font-size-title-small | -
--td-brand-color-hover | --td-brand-color-hover | -
--td-success-color | --td-success-color | -

View File

@@ -0,0 +1,97 @@
---
title: QRCode 二维码
description: 二维码能够将文本转换生成二维码的组件,支持自定义配色和 Logo 配置。
spline: message
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TQRCode from '@tdesign/uniapp/qrcode/qrcode.vue';
```
### 01 组件类型
#### 基本用法
{{ base }}
#### 带 Icon 的二维码
{{ icon }}
#### 二维码纠错等级
{{ level }}
### 02 组件状态
{{ status }}
### 03 组件样式
#### 二维码颜色
{{ color }}
#### 二维码尺寸
{{ size }}
### FAQ
#### 关于二维码纠错等级
纠错等级也叫纠错率,就是指二维码可以被遮挡后还能正常扫描,而这个能被遮挡的最大面积就是纠错率。
通常情况下二维码分为 4 个纠错级别:`L级` 可纠正约 `7%` 错误、`M级` 可纠正约 `15%` 错误、`Q级` 可纠正约 `25%` 错误、`H级` 可纠正约 `30%` 错误。但并不是所有位置都可以缺损,像最明显的三个角上的方框,直接影响初始定位。中间零散的部分是内容编码,可以容忍缺损。当二维码的内容编码携带信息比较少的时候,也就是链接比较短的时候,设置不同的纠错等级,生成的图片不会发生变化。
有关更多信息,可参阅[官方文档](https://www.qrcode.com/zh/about/error_correction)的相关资料
#### 生成的二维码无法扫描?
若二维码无法扫码识别,可能是因为链接地址过长导致像素过于密集,可以通过 `size` 配置二维码更大,或者通过短链接服务等方式将链接变短。
##
## API
### QRCode Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
bg-color | String | - | 二维码背景颜色 | N
borderless | Boolean | false | 是否有边框 | N
color | String | - | 二维码颜色 | N
icon | String | - | 二维码中图片的地址 | N
icon-size | Number / Object | 40 | 二维码中图片的大小。TS 类型:`number \| { width: number; height: number }` | N
level | String | M | 二维码纠错等级。可选项L/M/Q/H | N
size | Number | 160 | 二维码大小 | N
status | String | active | 二维码状态。可选项active/expired/loading/scanned。TS 类型:`QRStatus` `type QRStatus = "active" \| "expired" \| "loading" \| "scanned"`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/qrcode/type.ts) | N
status-render | Boolean | false | 是否启用自定义渲染 | N
value | String | - | 扫描后的文本 | N
### QRCode Events
名称 | 参数 | 描述
-- | -- | --
refresh | \- | 点击"点击刷新"的回调
### QRCode Slots
名称 | 描述
-- | --
status-render | 自定义状态渲染器
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-font-size-title-small | --td-font-size-title-small | -
--td-brand-color-hover | --td-brand-color-hover | -
--td-success-color | --td-success-color | -

View File

@@ -0,0 +1,48 @@
export default {
// 二维码内容
value: {
type: String,
default: '',
},
// 中心图标路径
icon: {
type: String,
default: '',
},
// 二维码大小单位rpx
size: {
type: Number,
default: 160,
},
// 中心图标大小单位px
iconSize: {
type: [Number, Object],
default: 40,
},
// 纠错等级
level: {
type: String,
default: 'M',
validator: (value: string) => ['L', 'M', 'Q', 'H'].includes(value),
},
// 背景色
bgColor: {
type: String,
default: '#FFFFFF',
},
// 二维码颜色
color: {
type: String,
default: '#000000',
},
// 是否包含边距
includeMargin: {
type: Boolean,
default: false,
},
// 边距大小单位rpx
marginSize: {
type: Number,
default: 0,
},
};

View File

@@ -0,0 +1,7 @@
.t-qrcode__canvas-wrapper {
display: inline-block;
position: relative;
}
.t-qrcode__canvas {
display: block;
}

View File

@@ -0,0 +1,413 @@
<template>
<view
class="t-qrcode__canvas-wrapper"
:class="tClass"
>
<canvas
:id="canvasId"
ref="qrcodeCanvas"
type="2d"
:canvas-id="canvasId"
class="t-qrcode__canvas"
:style="`width: ${size}px; height: ${size}px;`"
/>
</view>
</template>
<script>
import props from './props';
import useQRCode from '../../hooks/useQRCode';
import { DEFAULT_MINVERSION, excavateModules, isSupportPath2d, generatePath } from '../../../common/shared/qrcode/utils';
import { uniComponent } from '../../../common/src/index';
import { prefix } from '../../../common/config';
import { loadImage } from '../../../common/canvas/index';
import { getWindowInfo, nextTick } from '../../../common/utils';
export default uniComponent({
name: 'QrcodeCanvas',
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
],
props: {
...props,
},
emits: ['drawCompleted', 'drawError'],
data() {
return {
canvas: null,
ctx: null,
canvasId: `qrcode-canvas-${Math.random().toString(36)
.slice(2, 11)}`,
isWeb: false,
};
},
computed: {
// 使用计算属性确保有默认值
actualBgColor() {
return this.bgColor || '#FFFFFF';
},
actualColor() {
return this.color || '#000000';
},
},
watch: {
value() {
this.renderQRCode();
},
icon() {
this.renderQRCode();
},
size() {
let interval = 0;
// #ifdef APP-PLUS
interval = 33;
// #endif
setTimeout(() => {
this.renderQRCode();
}, interval);
},
iconSize() {
this.renderQRCode();
},
level() {
this.renderQRCode();
},
bgColor() {
this.renderQRCode();
},
color() {
this.renderQRCode();
},
},
created() {
// 小程序不能使用响应式的this.canvas
this._canvas = null;
this._ctx = null;
},
mounted() {
// 判断是否为小程序环境,否则默认为 H5
// #ifdef MP
this.isWeb = false;
// #endif
// #ifndef MP
this.isWeb = true;
// #endif
this.initCanvas();
},
methods: {
async initCanvas() {
await nextTick();
// #ifndef H5
this.initMiniProgramCanvas();
// #endif
// #ifdef H5
this.initH5Canvas();
// #endif
},
// H5 环境初始化
async initH5Canvas() {
// 在 uniapp H5 环境中canvas 会被包裹在 uni-canvas 内
const uniCanvasElement = document.querySelector(`#${this.canvasId}`);
let canvasElement = null;
// 如果获取到的是 uni-canvas需要找到内部的 canvas
if (uniCanvasElement && uniCanvasElement.tagName === 'UNI-CANVAS') {
canvasElement = uniCanvasElement.querySelector('canvas');
// 设置 uni-canvas 的样式
uniCanvasElement.style.width = `${this.size}px`;
uniCanvasElement.style.height = `${this.size}px`;
uniCanvasElement.style.overflow = 'visible';
// 设置 wrapper 的样式
const wrapper = uniCanvasElement.parentElement;
if (wrapper) {
wrapper.style.width = `${this.size}px`;
wrapper.style.height = `${this.size}px`;
wrapper.style.overflow = 'visible';
}
} else {
canvasElement = uniCanvasElement;
}
if (canvasElement) {
// 在初始化时设置 Canvas 的物理尺寸和显示尺寸
const pixelRatio = window.devicePixelRatio || 1;
const canvasSize = this.size * pixelRatio;
// 设置物理尺寸(实际像素)
canvasElement.width = canvasSize;
canvasElement.height = canvasSize;
// 设置显示尺寸CSS 像素)
canvasElement.style.width = `${this.size}px`;
canvasElement.style.height = `${this.size}px`;
// 添加 willReadFrequently 属性以优化性能并消除警告
const ctx = canvasElement.getContext('2d', { willReadFrequently: true });
this.canvas = canvasElement;
this.ctx = ctx;
await this.renderQRCode();
} else {
console.error('无法获取 canvas 元素');
}
},
// 小程序环境初始化
async initMiniProgramCanvas() {
if (typeof uni !== 'undefined' && uni.createSelectorQuery) {
const query = uni.createSelectorQuery().in(this);
query
.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec(async (res) => {
if (!res || !res[0] || !res[0].node) {
console.error('获取 canvas 节点失败');
return;
}
const canvas = res[0].node;
// 小程序环境也添加 willReadFrequently 属性
try {
let ctx;
// #ifdef MP
ctx = canvas.getContext('2d', { willReadFrequently: true });
// #endif
if (!ctx) {
ctx = uni.createCanvasContext(this.canvasId, this);
}
this._canvas = canvas;
this._ctx = ctx;
} catch (e) {
console.warn('获取 ctx 失败', e);
}
await this.renderQRCode();
});
}
},
async renderQRCode() {
const canvas = this._canvas || this.canvas;
const ctx = this._ctx || this.ctx;
if (!canvas || !ctx) {
return;
}
const sizeProp = this.getSizeProp(this.iconSize);
try {
const qrData = useQRCode({
value: this.value,
level: this.level,
minVersion: DEFAULT_MINVERSION,
includeMargin: this.includeMargin,
marginSize: this.marginSize,
size: this.size,
imageSettings: this.icon
? {
src: this.icon,
width: sizeProp.width,
height: sizeProp.height,
excavate: true,
}
: undefined,
});
// 获取设备像素比
let pixelRatio = 1;
let canvasSize;
let scale;
// #ifndef H5
// 小程序环境:获取真实的设备像素比并设置 Canvas 尺寸
// 使用 getWindowInfo 替代已废弃的 getSystemInfoSync
const windowInfo = getWindowInfo();
pixelRatio = windowInfo.pixelRatio || 1;
canvasSize = this.size * pixelRatio;
canvas.width = canvasSize;
canvas.height = canvasSize;
// 小程序环境scale 计算方式(参考 TS 实现)
scale = canvasSize / qrData.numCells;
// #ifdef APP-PLUS
scale /= pixelRatio;
// #endif
// #endif
// #ifdef H5
// H5 环境:每次渲染时重新设置 Canvas 尺寸(因为 size 可能变化)
pixelRatio = window.devicePixelRatio || 1;
canvasSize = this.size * pixelRatio;
// 重新设置 Canvas 物理尺寸(会重置 canvas 状态)
canvas.width = canvasSize;
canvas.height = canvasSize;
// 重新设置 Canvas 显示尺寸
canvas.style.width = `${this.size}px`;
canvas.style.height = `${this.size}px`;
// H5 环境scale 计算方式(基于物理尺寸)
scale = canvasSize / qrData.numCells;
// #endif
// 重置变换矩阵并应用缩放
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(scale, scale);
// 绘制背景
ctx.fillStyle = this.actualBgColor;
ctx.fillRect(0, 0, qrData.numCells, qrData.numCells);
// 处理需要挖空的区域(如果有图标)
let cellsToDraw = qrData.cells;
if (this.icon && qrData.calculatedImageSettings?.excavation) {
cellsToDraw = excavateModules(qrData.cells, qrData.calculatedImageSettings.excavation);
}
// 绘制二维码
ctx.fillStyle = this.actualColor;
// Web 环境优先使用 Path2D性能更好
if (this.isWeb && isSupportPath2d) {
ctx.fill(new Path2D(generatePath(cellsToDraw, qrData.margin)));
} else {
// 小程序环境或不支持 Path2D 时使用逐个绘制
cellsToDraw.forEach((row, y) => {
row.forEach((cell, x) => {
if (cell) {
ctx.fillRect(x + qrData.margin, y + qrData.margin, 1, 1);
}
});
});
}
// 绘制中心图标
if (this.icon && qrData.calculatedImageSettings) {
await this.drawIcon(qrData, pixelRatio);
}
// #ifdef APP-PLUS
ctx.draw();
// #endif
this.$emit('drawCompleted');
} catch (err) {
console.error('二维码绘制失败:', err);
this.$emit('drawError', { error: err });
}
},
async drawIcon(qrData, pixelRatio) {
const ctx = this._ctx || this.ctx;
const { calculatedImageSettings, margin } = qrData;
if (!calculatedImageSettings) {
return;
}
try {
// 加载图标图片
const img = await this.loadIconImage();
if (!img) {
console.error('无法加载图标图片');
return;
}
const drawX = calculatedImageSettings.x + margin;
const drawY = calculatedImageSettings.y + margin;
// 设置透明度
if (calculatedImageSettings.opacity !== null && calculatedImageSettings.opacity !== undefined) {
ctx.globalAlpha = calculatedImageSettings.opacity;
}
// #ifdef H5
ctx.scale(1 / pixelRatio, 1 / pixelRatio); // H5 环境:需要调整缩放
// #endif
// 绘制图标
ctx.drawImage(
img,
drawX,
drawY,
calculatedImageSettings.w,
calculatedImageSettings.h,
);
// 恢复透明度
ctx.globalAlpha = 1;
} catch (err) {
console.error('图标绘制失败:', err);
}
},
// 加载图标图片
// 参考 TSX (H5) 和 TS (小程序) 的实现
async loadIconImage() {
const canvas = this._canvas || this.canvas;
if (!this.icon || !canvas) {
return null;
}
return loadImage({
canvas,
src: this.icon,
});
},
getSizeProp(iconSize) {
if (!iconSize) return { width: 0, height: 0 };
if (typeof iconSize === 'number') {
return {
width: iconSize,
height: iconSize,
};
}
return {
width: iconSize.width,
height: iconSize.height,
};
},
// 暴露 canvas 节点给父组件
getCanvasNode() {
let result;
// #ifndef H5
result = new Promise((resolve) => {
if (typeof uni !== 'undefined' && uni.createSelectorQuery) {
const query = uni.createSelectorQuery().in(this);
query
.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
resolve(res[0]?.node);
});
} else {
resolve(null);
}
});
// #endif
// #ifdef H5
result = Promise.resolve(document.querySelector(`#${this.canvasId}`));
// #endif
return result;
},
},
});
</script>
<style lang="less" scoped>
@import './qrcode-canvas.css';
</style>

View File

@@ -0,0 +1,22 @@
export default {
// 二维码状态
status: {
type: String,
default: 'active',
validator: (value: string) => ['active', 'expired', 'loading', 'scanned'].includes(value),
},
// 本地化文本配置
locale: {
type: Object,
default: () => ({
expiredText: '二维码过期',
refreshText: '点击刷新',
scannedText: '已扫描',
}),
},
// 是否启用自定义渲染
statusRender: {
type: Boolean,
default: false,
},
};

View File

@@ -0,0 +1,25 @@
.t-expired__text {
color: var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9)));
font-weight: 600;
}
.t-expired__button {
display: flex;
color: var(--td-brand-color, var(--td-primary-color-7, #0052d9));
box-shadow: none;
cursor: pointer;
column-gap: 8px;
align-items: center;
height: 32px;
transition: all 0.2s cubic-bezier(0.215, 0.61, 0.355, 1);
}
.t-expired__button:hover {
color: var(--td-brand-color-hover);
}
.t-scanned {
display: flex;
column-gap: 8px;
align-items: center;
}
.t-scanned__icon {
color: var(--td-success-color);
}

View File

@@ -0,0 +1,94 @@
<template>
<div>
<slot
v-if="statusRender"
name="statusRender"
/>
<template v-else>
<view
v-if="status === 'expired'"
:class="`${prefix}-expired`"
>
<view :class="`${prefix}-expired__text`">
{{ locale.expiredText }}
<view
:class="`${prefix}-expired__button`"
@click="handleRefresh"
>
<t-icon
name="refresh"
size="36rpx"
/>
{{ locale.refreshText }}
</view>
</view>
</view>
<view
v-else-if="status === 'loading'"
:class="`${prefix}-loading-container`"
>
<t-loading
size="64rpx"
:theme="isSkyline ? 'spinner' : 'circular'"
/>
</view>
<view
v-else-if="status === 'scanned'"
:class="`${prefix}-scanned`"
>
<t-icon
name="check-circle-filled"
:class="`${prefix}-scanned__icon`"
size="44rpx"
/>
{{ locale.scannedText }}
</view>
</template>
</div>
</template>
<script>
import TIcon from '../../../icon/icon';
import TLoading from '../../../loading/loading';
import props from './props';
import { prefix } from '../../../common/config';
const name = `${prefix}-qrcode`;
export default {
name: 'QrcodeStatus',
options: {
styleIsolation: 'shared',
},
components: {
TIcon,
TLoading,
},
props: {
...props,
},
data() {
return {
prefix,
classPrefix: name,
isSkyline: false,
};
},
mounted() {
// 暂时忽略 skyline
// this.isSkyline = false;
},
methods: {
handleRefresh() {
this.$emit('refresh');
},
},
};
</script>
<style lang="less" scoped>
@import './qrcode-status.css';
</style>

View File

@@ -0,0 +1,25 @@
export interface QRCodeStatusProps {
/**
* 二维码状态
* @default 'active'
*/
status?: 'active' | 'expired' | 'loading' | 'scanned';
/**
* 本地化文本配置
*/
locale?: {
/** 过期提示文本 */
expiredText?: string;
/** 刷新按钮文本 */
refreshText?: string;
/** 已扫描提示文本 */
scannedText?: string;
};
/**
* 是否启用自定义渲染
* @default false
*/
statusRender?: boolean;
}

View File

@@ -0,0 +1,25 @@
import { QrCode, QrSegment } from '../../common/shared/qrcode/qrcodegen';
import { ERROR_LEVEL_MAP, getImageSettings, getMarginSize } from '../../common/shared/qrcode/utils';
const useQRCode = (opt) => {
const { value, level, minVersion, includeMargin, marginSize, imageSettings, size } = opt;
const qrcode = (() => {
const segments = QrSegment.makeSegments(value);
return QrCode.encodeSegments(segments, ERROR_LEVEL_MAP[level], minVersion);
})();
const cells = qrcode.getModules();
const margin = getMarginSize(includeMargin, marginSize);
const calculatedImageSettings = getImageSettings(cells, size, margin, imageSettings);
return {
cells,
margin,
numCells: cells.length + margin * 2,
calculatedImageSettings,
qrcode,
};
};
export default useQRCode;

View File

@@ -0,0 +1,66 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
import type { TdQRCodeProps } from './type';
export default {
/** 二维码背景颜色 */
bgColor: {
type: String,
default: '',
},
/** 是否有边框 */
borderless: Boolean,
/** 二维码颜色 */
color: {
type: String,
default: '',
},
/** 二维码中图片的地址 */
icon: {
type: String,
default: '',
},
/** 二维码中图片的大小 */
iconSize: {
type: [Number, Object],
default: 40 as TdQRCodeProps['iconSize'],
},
/** 二维码纠错等级 */
level: {
type: String,
default: 'M' as TdQRCodeProps['level'],
validator(val: TdQRCodeProps['level']): boolean {
if (!val) return true;
return ['L', 'M', 'Q', 'H'].includes(val);
},
},
/** 二维码大小 */
size: {
type: Number,
default: 160,
},
/** 二维码状态 */
status: {
type: String,
default: 'active' as TdQRCodeProps['status'],
validator(val: TdQRCodeProps['status']): boolean {
if (!val) return true;
return ['active', 'expired', 'loading', 'scanned'].includes(val);
},
},
/** 是否启用自定义渲染 */
statusRender: Boolean,
/** 扫描后的文本 */
value: {
type: String,
default: '',
},
/** 点击"点击刷新"的回调 */
onRefresh: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,31 @@
.t-qrcode {
position: relative;
display: flex;
box-sizing: border-box;
background-color: var(--td-bg-color-container, var(--td-font-white-1, #ffffff));
padding: 24rpx;
border-radius: 12rpx;
border: 1px solid var(--td-component-border, var(--td-gray-color-4, #dcdcdc));
}
.t-qrcode.t-borderless {
border-color: transparent;
}
.t-qrcode .t-mask {
left: 0;
top: 0;
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
z-index: 300;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
color: var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9)));
background-color: var(--td-mask-background, rgba(255, 255, 255, 0.96));
text-align: center;
border-radius: 12rpx;
font: var(--td-font-body-medium, 28rpx / 44rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular));
}

View File

@@ -0,0 +1,144 @@
<template>
<view
:style="`${tools._style([customStyle])}; width:${containerSize}px; height: ${containerSize}px; background-color: ${bgColor};`"
:class="`${classPrefix} ${borderless ? prefix + '-' + 'borderless' : ''} ${tClass}`"
>
<QrcodeCanvas
ref="qrcodeCanvas"
:t-class="tClassCanvas"
:size="size"
:value="value"
:level="level"
:color="color"
:bg-color="bgColor"
:icon="icon"
:icon-size="iconSize"
@drawError="handleDrawError"
@drawCompleted="handleDrawCompleted"
/>
<view
v-if="showMask && canvasReady"
:class="`${prefix}-mask`"
>
<QrcodeStatus
:status="status"
:status-render="statusRender"
@refresh="handleRefresh"
>
<template #statusRender>
<slot name="statusRender" />
</template>
</QrcodeStatus>
</view>
</view>
</template>
<script>
import QrcodeCanvas from './components/qrcode-canvas/qrcode-canvas.vue';
import QrcodeStatus from './components/qrcode-status/qrcode-status.vue';
import { prefix } from '../common/config';
import props from './props';
import { uniComponent } from '../common/src/index';
import tools from '../common/utils.wxs';
const name = `${prefix}-qrcode`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
externalClasses: [
`${prefix}-class`,
`${prefix}-class-canvas`,
],
components: {
QrcodeCanvas,
QrcodeStatus,
},
props: {
...props,
},
data() {
return {
prefix,
tools,
showMask: false,
classPrefix: name,
canvasReady: false,
canvasNode: null,
};
},
computed: {
// 容器尺寸 = Canvas 尺寸 + padding * 2
// padding 为 12px所以容器需要额外 24px
containerSize() {
return this.size + 24;
},
},
watch: {
status: {
handler(newVal) {
this.showMask = newVal !== 'active';
},
immediate: true,
},
},
mounted() {
this.initCanvas();
},
methods: {
async initCanvas() {
// 获取 canvas 实例
const canvasComp = this.$refs.qrcodeCanvas;
if (canvasComp) {
const canvas = await canvasComp.getCanvasNode();
this.canvasNode = canvas;
}
},
// 用于外部调用,重新绘制二维码
init() {
const canvasComp = this.$refs.qrcodeCanvas;
if (canvasComp) {
canvasComp.initCanvas();
}
},
handleDrawCompleted() {
this.canvasReady = true;
},
handleDrawError(err) {
console.error('二维码绘制失败', err);
},
handleRefresh() {
this.$emit('refresh');
},
// 二维码下载方法
async handleDownload() {
if (!this.canvasNode) {
console.error('未找到 canvas 节点');
return;
}
// 注意:此方法在非小程序环境需要适配
if (typeof wx !== 'undefined' && uni.canvasToTempFilePath) {
uni.canvasToTempFilePath({
canvas: this.canvasNode,
success: (res) => {
uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath });
},
fail: (err) => {
console.error('canvasToTempFilePath failed', err);
},
});
}
},
},
});
</script>
<style lang="less" scoped>
@import './qrcode.css';
</style>

View File

@@ -0,0 +1,66 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdQRCodeProps {
/**
* 二维码背景颜色
* @default ''
*/
bgColor?: string;
/**
* 是否有边框
* @default false
*/
borderless?: boolean;
/**
* 二维码颜色
* @default ''
*/
color?: string;
/**
* 二维码中图片的地址
* @default ''
*/
icon?: string;
/**
* 二维码中图片的大小
* @default 40
*/
iconSize?: number | { width: number; height: number };
/**
* 二维码纠错等级
* @default M
*/
level?: 'L' | 'M' | 'Q' | 'H';
/**
* 二维码大小
* @default 160
*/
size?: number;
/**
* 二维码状态
* @default active
*/
status?: QRStatus;
/**
* 是否启用自定义渲染
* @default false
*/
statusRender?: boolean;
/**
* 扫描后的文本
* @default ''
*/
value?: string;
/**
* 点击"点击刷新"的回调
*/
onRefresh?: () => void;
}
export type QRStatus = 'active' | 'expired' | 'loading' | 'scanned';
export type StatusRenderInfo = { status: QRStatus; onRefresh?: () => void };