first commit
This commit is contained in:
40
uni_modules/tdesign-uniapp/components/qrcode/README.en-US.md
Normal file
40
uni_modules/tdesign-uniapp/components/qrcode/README.en-US.md
Normal 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 | -
|
||||
97
uni_modules/tdesign-uniapp/components/qrcode/README.md
Normal file
97
uni_modules/tdesign-uniapp/components/qrcode/README.md
Normal 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 | -
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.t-qrcode__canvas-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
.t-qrcode__canvas {
|
||||
display: block;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
66
uni_modules/tdesign-uniapp/components/qrcode/props.ts
Normal file
66
uni_modules/tdesign-uniapp/components/qrcode/props.ts
Normal 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: () => ({}),
|
||||
},
|
||||
};
|
||||
31
uni_modules/tdesign-uniapp/components/qrcode/qrcode.css
Normal file
31
uni_modules/tdesign-uniapp/components/qrcode/qrcode.css
Normal 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));
|
||||
}
|
||||
144
uni_modules/tdesign-uniapp/components/qrcode/qrcode.vue
Normal file
144
uni_modules/tdesign-uniapp/components/qrcode/qrcode.vue
Normal 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>
|
||||
66
uni_modules/tdesign-uniapp/components/qrcode/type.ts
Normal file
66
uni_modules/tdesign-uniapp/components/qrcode/type.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user