) : null}
{/* 尾页按钮 */}
{this.showFirstAndLastPageBtn ? (
-
this.toPage(this.pageCount)}
- // @ts-ignore
- disabled={this.disabled || this.current === this.pageCount}
- >
+
this.toPage(this.pageCount)}>
) : null}
diff --git a/src/popup/popup.tsx b/src/popup/popup.tsx
index 150c874bb..7e5ef5ee7 100644
--- a/src/popup/popup.tsx
+++ b/src/popup/popup.tsx
@@ -170,6 +170,14 @@ export default mixins(classPrefixMixins, getAttachConfigMixins('popup')).extend(
};
updateTrigger();
this.$watch('trigger', updateTrigger);
+
+ // 当初始 visible 为 true 时,添加 document 事件监听
+ if (this.visible) {
+ if (!this.hasDocumentEvent) {
+ on(document, 'mousedown', this.handleDocumentClick, true);
+ this.hasDocumentEvent = true;
+ }
+ }
},
updated() {
(this.$refs.container as any)?.updateContent();
@@ -310,6 +318,8 @@ export default mixins(classPrefixMixins, getAttachConfigMixins('popup')).extend(
// ignore document event if popper panel clicked
const popperEl = this.$refs.popper as HTMLDivElement;
if (popperEl?.contains(ev.target as Node)) return;
+
+ // 确保点击外部区域能正确关闭
this.visibleState = 0;
this.emitPopVisible(false, { trigger: 'document', e: ev });
});
diff --git a/src/qrcode/__tests__/index.test.jsx b/src/qrcode/__tests__/index.test.jsx
new file mode 100644
index 000000000..3a80be4b3
--- /dev/null
+++ b/src/qrcode/__tests__/index.test.jsx
@@ -0,0 +1,115 @@
+import { mount } from '@vue/test-utils';
+import { QRCode } from '../index';
+
+describe('QRCode', () => {
+ describe('props', () => {
+ // 因单测环境下无法获取主题色,若未定义颜色,组件将由默认颜色兜底。所以单测的颜色要为默认颜色。
+ // 颜色优先级如下:
+ // bgColor:自定义颜色 > 主题色适配 > 透明[transparent]
+ // color[fgColor]:自定义颜色 > 主题色适配 > 默认颜色[#000000]
+ const defaultBgColor = 'transparent'; // 实际使用时为 rgb(255, 255, 255)
+
+ it(':bgColor[string]', async () => {
+ const bgColor = 'rgb(7, 193, 96)';
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ expect(wrapper.find('.t-qrcode').attributes('style')).eq(
+ `background-color: ${bgColor}; width: 160px; height: 160px;`,
+ );
+ });
+
+ it(':borderless[boolean]', async () => {
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ expect(wrapper.find('.t-qrcode').classes('t-borderless')).eq(true);
+ });
+
+ // color只能测试svg模式下
+ it(':color[string]', async () => {
+ const color = 'rgb(7, 193, 96)';
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ // [0] 是背景
+ expect(wrapper.find('.t-qrcode').findAll('path').at(1).attributes('fill')).eq(color);
+ });
+
+ it(':icon[string]-canvas', async () => {
+ const iconSrc = 'https://tdesign.gtimg.com/site/tdesign-logo.png';
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ expect(wrapper.find('.t-qrcode').find('img').attributes('src')).eq(iconSrc);
+ });
+
+ it(':icon[string]-svg', async () => {
+ const iconSrc = 'https://tdesign.gtimg.com/site/tdesign-logo.png';
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ expect(wrapper.find('.t-qrcode').find('image').attributes('href')).eq(iconSrc);
+ });
+
+ // it(':iconSize[number|object]-svg', async () => {});
+
+ // const level = ['L', 'M', 'Q', 'H'];
+ // level.forEach((item) => {
+ // it(`:level[string]-[${item}]`, async () => {
+ // const wrapper = mount({
+ // render() {
+ // return
;
+ // },
+ // });
+ // expect(wrapper.find('.t-qrcode').attributes('level')).eq(item);
+ // });
+ // });
+
+ it(':size[number]', async () => {
+ const size = 380;
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ expect(wrapper.find('.t-qrcode').attributes('style')).eq(
+ `background-color: ${defaultBgColor}; width: ${size}px; height: ${size}px;`,
+ );
+ });
+
+ const status = ['expired', 'loading', 'scanned'];
+ status.forEach((item) => {
+ it(`:status[string]-[${item}]`, async () => {
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ expect(wrapper.find('.t-qrcode').find('.t-mask').exists()).eq(true);
+ expect(wrapper.find('.t-qrcode').find(`.t-${item}`).exists()).eq(true);
+ });
+ });
+
+ it(':statusRender[Function]', async () => {
+ const statusRender = vi.fn();
+ const wrapper = mount({
+ render() {
+ return
;
+ },
+ });
+ await wrapper.setProps({ status: 'expired', statusRender });
+ expect(statusRender).toBeCalled();
+ });
+ });
+});
diff --git a/src/qrcode/_example-composition/base.vue b/src/qrcode/_example-composition/base.vue
new file mode 100644
index 000000000..0ab2984fe
--- /dev/null
+++ b/src/qrcode/_example-composition/base.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/customColor.vue b/src/qrcode/_example-composition/customColor.vue
new file mode 100644
index 000000000..e640babb7
--- /dev/null
+++ b/src/qrcode/_example-composition/customColor.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/customSize.vue b/src/qrcode/_example-composition/customSize.vue
new file mode 100644
index 000000000..46863b430
--- /dev/null
+++ b/src/qrcode/_example-composition/customSize.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ Smaller
+
+
+
+
+
+ Larger
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/customStatusRender.vue b/src/qrcode/_example-composition/customStatusRender.vue
new file mode 100644
index 000000000..7a996532e
--- /dev/null
+++ b/src/qrcode/_example-composition/customStatusRender.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+ 二维码过期
+
+
+
+ 点击刷新
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/download.vue b/src/qrcode/_example-composition/download.vue
new file mode 100644
index 000000000..93acfda36
--- /dev/null
+++ b/src/qrcode/_example-composition/download.vue
@@ -0,0 +1,57 @@
+
+
+
+ canvas
+ svg
+
+
+ Download
+
+
+
+
diff --git a/src/qrcode/_example-composition/icon.vue b/src/qrcode/_example-composition/icon.vue
new file mode 100644
index 000000000..92ed88e72
--- /dev/null
+++ b/src/qrcode/_example-composition/icon.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/level.vue b/src/qrcode/_example-composition/level.vue
new file mode 100644
index 000000000..8653bab2f
--- /dev/null
+++ b/src/qrcode/_example-composition/level.vue
@@ -0,0 +1,17 @@
+
+
+
+ L
+ M
+ Q
+ H
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/popover.vue b/src/qrcode/_example-composition/popover.vue
new file mode 100644
index 000000000..6902a2a1c
--- /dev/null
+++ b/src/qrcode/_example-composition/popover.vue
@@ -0,0 +1,10 @@
+
+
+
+ Hover me
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/status.vue b/src/qrcode/_example-composition/status.vue
new file mode 100644
index 000000000..f75c435a3
--- /dev/null
+++ b/src/qrcode/_example-composition/status.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example-composition/type.vue b/src/qrcode/_example-composition/type.vue
new file mode 100644
index 000000000..aa637e5de
--- /dev/null
+++ b/src/qrcode/_example-composition/type.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/qrcode/_example/base.vue b/src/qrcode/_example/base.vue
new file mode 100644
index 000000000..7d83c1058
--- /dev/null
+++ b/src/qrcode/_example/base.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example/customColor.vue b/src/qrcode/_example/customColor.vue
new file mode 100644
index 000000000..e640babb7
--- /dev/null
+++ b/src/qrcode/_example/customColor.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/qrcode/_example/customSize.vue b/src/qrcode/_example/customSize.vue
new file mode 100644
index 000000000..b842ff567
--- /dev/null
+++ b/src/qrcode/_example/customSize.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+ Smaller
+
+
+
+
+
+ Larger
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example/customStatusRender.vue b/src/qrcode/_example/customStatusRender.vue
new file mode 100644
index 000000000..f07b43aec
--- /dev/null
+++ b/src/qrcode/_example/customStatusRender.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+ 二维码过期
+
+
+
+ 点击刷新
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example/download.vue b/src/qrcode/_example/download.vue
new file mode 100644
index 000000000..4038fe9db
--- /dev/null
+++ b/src/qrcode/_example/download.vue
@@ -0,0 +1,74 @@
+
+
+
+ canvas
+ svg
+
+
+ Download
+
+
+
+
diff --git a/src/qrcode/_example/icon.vue b/src/qrcode/_example/icon.vue
new file mode 100644
index 000000000..ecdf48cf2
--- /dev/null
+++ b/src/qrcode/_example/icon.vue
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/qrcode/_example/level.vue b/src/qrcode/_example/level.vue
new file mode 100644
index 000000000..63423aeda
--- /dev/null
+++ b/src/qrcode/_example/level.vue
@@ -0,0 +1,21 @@
+
+
+
+ L
+ M
+ Q
+ H
+
+
+
+
+
+
diff --git a/src/qrcode/_example/popover.vue b/src/qrcode/_example/popover.vue
new file mode 100644
index 000000000..6902a2a1c
--- /dev/null
+++ b/src/qrcode/_example/popover.vue
@@ -0,0 +1,10 @@
+
+
+
+ Hover me
+
+
+
+
+
+
diff --git a/src/qrcode/_example/status.vue b/src/qrcode/_example/status.vue
new file mode 100644
index 000000000..965dd70fb
--- /dev/null
+++ b/src/qrcode/_example/status.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/qrcode/_example/type.vue b/src/qrcode/_example/type.vue
new file mode 100644
index 000000000..aa637e5de
--- /dev/null
+++ b/src/qrcode/_example/type.vue
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/src/qrcode/_usage/index.vue b/src/qrcode/_usage/index.vue
new file mode 100644
index 000000000..56e2d8d52
--- /dev/null
+++ b/src/qrcode/_usage/index.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/qrcode/_usage/props.json b/src/qrcode/_usage/props.json
new file mode 100644
index 000000000..a407f360a
--- /dev/null
+++ b/src/qrcode/_usage/props.json
@@ -0,0 +1,69 @@
+[
+ {
+ "name": "borderless",
+ "type": "Boolean",
+ "defaultValue": false,
+ "options": []
+ },
+ {
+ "name": "level",
+ "type": "enum",
+ "defaultValue": "M",
+ "options": [
+ {
+ "label": "L",
+ "value": "L"
+ },
+ {
+ "label": "M",
+ "value": "M"
+ },
+ {
+ "label": "Q",
+ "value": "Q"
+ },
+ {
+ "label": "H",
+ "value": "H"
+ }
+ ]
+ },
+ {
+ "name": "status",
+ "type": "enum",
+ "defaultValue": "active",
+ "options": [
+ {
+ "label": "active",
+ "value": "active"
+ },
+ {
+ "label": "expired",
+ "value": "expired"
+ },
+ {
+ "label": "loading",
+ "value": "loading"
+ },
+ {
+ "label": "scanned",
+ "value": "scanned"
+ }
+ ]
+ },
+ {
+ "name": "type",
+ "type": "enum",
+ "defaultValue": "canvas",
+ "options": [
+ {
+ "label": "canvas",
+ "value": "canvas"
+ },
+ {
+ "label": "svg",
+ "value": "svg"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/qrcode/components/props.ts b/src/qrcode/components/props.ts
new file mode 100644
index 000000000..d4b317405
--- /dev/null
+++ b/src/qrcode/components/props.ts
@@ -0,0 +1,117 @@
+import { PropType } from 'vue';
+import { QRCodeSubComponent, QRCodeStatus } from './type';
+
+export const QRCodeSubComponentProps = {
+ /**
+ * The value to encode into the QR Code. An array of strings can be passed in
+ * to represent multiple segments to further optimize the QR Code.
+ */
+ value: {
+ type: String,
+ default: '',
+ },
+ /**
+ * The size, in pixels, to render the QR Code.
+ * @defaultValue 128
+ */
+ size: {
+ type: Number,
+ default: 128,
+ },
+ /**
+ * The Error Correction Level to use.
+ * @see https://www.qrcode.com/en/about/error_correction.html
+ * @defaultValue L
+ */
+ level: {
+ type: String as PropType
,
+ default: 'L',
+ },
+ /**
+ * The background color used to render the QR Code.
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
+ * @defaultValue #FFFFFF
+ */
+ bgColor: {
+ type: String,
+ default: '#FFFFFF',
+ },
+ /**
+ * The foregtound color used to render the QR Code.
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
+ * @defaultValue #000000
+ */
+ fgColor: {
+ type: String,
+ default: '#000000',
+ },
+ /**
+ * The style to apply to the QR Code.
+ */
+ // style: {
+ // type: Object as PropType,
+ // default: () => ({} as QRCodeSubComponent['style']),
+ // },
+ /**
+ * Whether or not a margin of 4 modules should be rendered as a part of the
+ * QR Code.
+ * @deprecated Use `marginSize` instead.
+ * @defaultValue false
+ */
+ includeMargin: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * The number of _modules_ to use for margin. The QR Code specification
+ * requires `4`, however you can specify any number. Values will be turned to
+ * integers with `Math.floor`. Overrides `includeMargin` when both are specified.
+ * @defaultValue 0
+ */
+ marginSize: {
+ type: Number,
+ default: 0,
+ },
+ /**
+ * The settings for the embedded image.
+ */
+ imageSettings: {
+ type: Object as PropType,
+ default: () => ({}),
+ },
+ /**
+ * The title to assign to the QR Code. Used for accessibility reasons.
+ */
+ title: {
+ type: String,
+ default: '',
+ },
+ /**
+ * The minimum version used when encoding the QR Code. Valid values are 1-40
+ * with higher values resulting in more complex QR Codes. The optimal
+ * (lowest) version is determined for the `value` provided, using `minVersion`
+ * as the lower bound.
+ * @defaultValue 1
+ */
+ minVersion: {
+ type: Number,
+ default: 1,
+ },
+};
+
+export const QRCodeStatusProps = {
+ locale: {
+ type: Object as PropType,
+ default: () => ({} as QRCodeStatus['locale']),
+ },
+ refresh: {
+ type: Function as PropType,
+ },
+ statusRender: {
+ type: Function,
+ },
+ status: {
+ type: String as PropType,
+ default: 'active' as QRCodeStatus['status'],
+ },
+};
diff --git a/src/qrcode/components/qrcode-canvas.tsx b/src/qrcode/components/qrcode-canvas.tsx
new file mode 100644
index 000000000..ad73e8a87
--- /dev/null
+++ b/src/qrcode/components/qrcode-canvas.tsx
@@ -0,0 +1,142 @@
+import { computed, defineComponent, ref } from 'vue';
+import { QRCodeSubComponentProps } from './props';
+import {
+ DEFAULT_NEED_MARGIN,
+ DEFAULT_MINVERSION,
+ isSupportPath2d,
+ excavateModules,
+ generatePath,
+} from '../../_common/js/qrcode/utils';
+import { useQRCode } from '../hooks/useQRCode';
+
+export default defineComponent({
+ name: 'QRCodeCanvas',
+ props: QRCodeSubComponentProps,
+ setup(props) {
+ const imgSrc = computed(() => props.imageSettings?.src);
+ const imgCrossOrigin = ref('');
+
+ return {
+ imgSrc,
+ imgCrossOrigin,
+ };
+ },
+ methods: {
+ renderQRCode() {
+ const {
+ margin, cells, numCells, calculatedImageSettings,
+ } = useQRCode({
+ value: this.value,
+ level: this.level,
+ minVersion: DEFAULT_MINVERSION,
+ includeMargin: DEFAULT_NEED_MARGIN,
+ marginSize: this.marginSize,
+ imageSettings: this.imageSettings,
+ size: this.size,
+ });
+
+ if (!this.$refs.canvasRef) {
+ return;
+ }
+
+ const canvas = (this.$refs.canvasRef as HTMLCanvasElement) ?? null;
+ const ctx = canvas?.getContext('2d');
+
+ if (!ctx) {
+ return;
+ }
+
+ this.imgCrossOrigin = calculatedImageSettings.value?.crossOrigin;
+
+ let cellsToDraw = cells;
+ const image = (this.$refs.imageRef as HTMLImageElement) ?? null;
+
+ if (image) {
+ image.crossOrigin = calculatedImageSettings.value.crossOrigin;
+ }
+
+ const haveImageToRender = calculatedImageSettings.value
+ && image !== null
+ && image.complete
+ && image.naturalHeight !== 0
+ && image.naturalWidth !== 0;
+
+ if (haveImageToRender && calculatedImageSettings.value.excavation != null) {
+ cellsToDraw = computed(() => excavateModules(cells.value, calculatedImageSettings.value.excavation));
+ }
+
+ const pixelRatio = window.devicePixelRatio || 1;
+ canvas.height = this.size * pixelRatio;
+ canvas.width = this.size * pixelRatio;
+ const scale = (this.size / numCells.value) * pixelRatio;
+ ctx.scale(scale, scale);
+
+ ctx.fillStyle = this.bgColor;
+ ctx.fillRect(0, 0, numCells.value, numCells.value);
+
+ ctx.fillStyle = this.fgColor;
+ if (isSupportPath2d) {
+ ctx.fill(new Path2D(generatePath(cellsToDraw.value, margin.value)));
+ } else {
+ cells.value.forEach((row, rdx) => {
+ row.forEach((cell, cdx) => {
+ if (cell) {
+ ctx.fillRect(cdx + margin.value, rdx + margin.value, 1, 1);
+ }
+ });
+ });
+ }
+
+ if (calculatedImageSettings.value) {
+ ctx.globalAlpha = calculatedImageSettings.value.opacity;
+ }
+
+ if (haveImageToRender) {
+ ctx.globalAlpha = calculatedImageSettings.value.opacity;
+ ctx.drawImage(
+ image,
+ calculatedImageSettings.value.x + margin.value,
+ calculatedImageSettings.value.y + margin.value,
+ calculatedImageSettings.value.w,
+ calculatedImageSettings.value.h,
+ );
+ }
+ },
+ },
+ mounted() {
+ this.renderQRCode();
+ },
+ watch: {
+ value() {
+ this.renderQRCode();
+ },
+ size() {
+ this.renderQRCode();
+ },
+ level() {
+ this.renderQRCode();
+ },
+ bgColor() {
+ this.renderQRCode();
+ },
+ fgColor() {
+ this.renderQRCode();
+ },
+ },
+ render() {
+ const styles = {
+ display: 'flex',
+ alignSelf: 'stretch',
+ flex: 'auto',
+ minWidth: '0',
+ };
+ return (
+
+
+ {this.imgSrc != null ? (
+

+ ) : null}
+
+ );
+ },
+});
diff --git a/src/qrcode/components/qrcode-status.tsx b/src/qrcode/components/qrcode-status.tsx
new file mode 100644
index 000000000..7cf505966
--- /dev/null
+++ b/src/qrcode/components/qrcode-status.tsx
@@ -0,0 +1,59 @@
+import { defineComponent } from 'vue';
+import { CheckCircleFilledIcon, RefreshIcon } from 'tdesign-icons-vue';
+import Loading from '../../loading';
+import { usePrefixClass } from '../../hooks';
+import type { StatusRenderInfo } from '../type';
+import { QRCodeStatusProps } from './props';
+
+export default defineComponent({
+ name: 'QRCodeStatus',
+ props: QRCodeStatusProps,
+ setup() {
+ const classPrefix = usePrefixClass();
+
+ return {
+ classPrefix,
+ };
+ },
+ methods: {
+ renderStatus(info: StatusRenderInfo) {
+ const defaultSpin = ;
+ const defaultExpiredNode = (
+
+
{this.locale?.expiredText}
+ {this?.refresh && (
+
+
+ {this.locale?.refreshText}
+
+ )}
+
+ );
+ const defaultScannedNode = (
+
+
+ {this.locale?.scannedText}
+
+ );
+
+ const defaultNodes = {
+ expired: defaultExpiredNode,
+ loading: defaultSpin,
+ scanned: defaultScannedNode,
+ active: null as any,
+ };
+ return defaultNodes[info.status];
+ },
+ },
+ render() {
+ return (
+
+ {this?.statusRender?.()
+ || this.renderStatus({
+ status: this.status,
+ onRefresh: this.refresh,
+ })}
+
+ );
+ },
+});
diff --git a/src/qrcode/components/qrcode-svg.tsx b/src/qrcode/components/qrcode-svg.tsx
new file mode 100644
index 000000000..20f85a3ff
--- /dev/null
+++ b/src/qrcode/components/qrcode-svg.tsx
@@ -0,0 +1,65 @@
+import { computed, defineComponent } from 'vue';
+import { QRCodeSubComponentProps } from './props';
+import {
+ DEFAULT_NEED_MARGIN, DEFAULT_MINVERSION, excavateModules, generatePath,
+} from '../../_common/js/qrcode/utils';
+import { useQRCode } from '../hooks/useQRCode';
+
+export default defineComponent({
+ name: 'QRCodeSVG',
+ props: QRCodeSubComponentProps,
+ setup(props) {
+ const {
+ margin, cells, numCells, calculatedImageSettings,
+ } = useQRCode({
+ value: props.value,
+ level: props.level,
+ minVersion: DEFAULT_MINVERSION,
+ includeMargin: DEFAULT_NEED_MARGIN,
+ marginSize: props.marginSize,
+ imageSettings: props.imageSettings,
+ size: props.size,
+ });
+
+ const cellsToDraw = computed(() => {
+ if (props.imageSettings && calculatedImageSettings.value?.excavation != null) {
+ return excavateModules(cells.value, calculatedImageSettings.value.excavation);
+ }
+ return cells.value;
+ });
+
+ return {
+ cellsToDraw,
+ calculatedImageSettings,
+ margin,
+ numCells,
+ };
+ },
+ render() {
+ const fgPath = generatePath(this.cellsToDraw, this.margin);
+
+ const imageNode = () => {
+ if (!this.imageSettings || !this.calculatedImageSettings) return null;
+
+ return (
+
+ );
+ };
+
+ return (
+
+ );
+ },
+});
diff --git a/src/qrcode/components/style/css.js b/src/qrcode/components/style/css.js
new file mode 100644
index 000000000..f388879ac
--- /dev/null
+++ b/src/qrcode/components/style/css.js
@@ -0,0 +1 @@
+import '../../style/index.css';
diff --git a/src/qrcode/components/style/index.js b/src/qrcode/components/style/index.js
new file mode 100644
index 000000000..4a2e7d583
--- /dev/null
+++ b/src/qrcode/components/style/index.js
@@ -0,0 +1 @@
+import '../../style/index.js';
diff --git a/src/qrcode/components/type.ts b/src/qrcode/components/type.ts
new file mode 100644
index 000000000..8e4b9c1bf
--- /dev/null
+++ b/src/qrcode/components/type.ts
@@ -0,0 +1,80 @@
+import type { ErrorCorrectionLevel, ImageSettings } from '../../_common/js/qrcode/types';
+import { QRCodeConfig } from '../../config-provider/type';
+import { StatusRenderInfo, TdQRCodeProps } from '../type';
+
+export interface QRCodeSubComponent {
+ /**
+ * The value to encode into the QR Code. An array of strings can be passed in
+ * to represent multiple segments to further optimize the QR Code.
+ */
+ value: string;
+ /**
+ * The size, in pixels, to render the QR Code.
+ * @defaultValue 128
+ */
+ size?: number;
+ /**
+ * The Error Correction Level to use.
+ * @see https://www.qrcode.com/en/about/error_correction.html
+ * @defaultValue L
+ */
+ level?: ErrorCorrectionLevel;
+ /**
+ * The background color used to render the QR Code.
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
+ * @defaultValue #FFFFFF
+ */
+ bgColor?: string;
+ /**
+ * The foregtound color used to render the QR Code.
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
+ * @defaultValue #000000
+ */
+ fgColor?: string;
+ /**
+ * The style to apply to the QR Code.
+ */
+ style?: any;
+ /**
+ * Whether or not a margin of 4 modules should be rendered as a part of the
+ * QR Code.
+ * @deprecated Use `marginSize` instead.
+ * @defaultValue false
+ */
+ includeMargin?: boolean;
+ /**
+ * The number of _modules_ to use for margin. The QR Code specification
+ * requires `4`, however you can specify any number. Values will be turned to
+ * integers with `Math.floor`. Overrides `includeMargin` when both are specified.
+ * @defaultValue 0
+ */
+ marginSize?: number;
+ /**
+ * The settings for the embedded image.
+ */
+ imageSettings?: ImageSettings;
+ /**
+ * The title to assign to the QR Code. Used for accessibility reasons.
+ */
+ title?: string;
+ /**
+ * The minimum version used when encoding the QR Code. Valid values are 1-40
+ * with higher values resulting in more complex QR Codes. The optimal
+ * (lowest) version is determined for the `value` provided, using `minVersion`
+ * as the lower bound.
+ * @defaultValue 1
+ */
+ minVersion?: number;
+}
+
+export type QRCodeCanvas = QRCodeSubComponent & any;
+
+export type QRCodeSVG = QRCodeSubComponent & any;
+
+export interface QRCodeStatus {
+ locale: QRCodeConfig;
+ classPrefix: string;
+ onRefresh?: TdQRCodeProps['onRefresh'];
+ statusRender?: TdQRCodeProps['statusRender'];
+ status: StatusRenderInfo['status'];
+}
diff --git a/src/qrcode/hooks/useQRCode.ts b/src/qrcode/hooks/useQRCode.ts
new file mode 100644
index 000000000..0e65eba66
--- /dev/null
+++ b/src/qrcode/hooks/useQRCode.ts
@@ -0,0 +1,57 @@
+import { computed, ComputedRef } from 'vue';
+import type {
+ CrossOrigin, ErrorCorrectionLevel, Excavation, ImageSettings,
+} from '../../_common/js/qrcode/types';
+import { ERROR_LEVEL_MAP, getImageSettings, getMarginSize } from '../../_common/js/qrcode/utils';
+import { QrCode, QrSegment } from '../../_common/js/qrcode/qrcodegen';
+
+interface Options {
+ value: string;
+ level: ErrorCorrectionLevel;
+ minVersion: number;
+ includeMargin: boolean;
+ marginSize?: number;
+ imageSettings?: ImageSettings;
+ size: number;
+}
+
+interface QRCodeResult {
+ cells: ComputedRef;
+ margin: ComputedRef;
+ numCells: ComputedRef;
+ calculatedImageSettings: ComputedRef<{
+ x: number;
+ y: number;
+ h: number;
+ w: number;
+ excavation: Excavation | null;
+ opacity: number;
+ crossOrigin: CrossOrigin;
+ }>;
+ qrcode: ComputedRef;
+}
+
+export const useQRCode = (opt: Options): QRCodeResult => {
+ const {
+ value, level, minVersion, includeMargin, marginSize, imageSettings, size,
+ } = opt;
+
+ const memoizedQrcode = computed(() => {
+ const segments = QrSegment.makeSegments(value);
+ return QrCode.encodeSegments(segments, ERROR_LEVEL_MAP[level], minVersion);
+ });
+
+ const cs = computed(() => memoizedQrcode.value.getModules());
+ const mg = computed(() => getMarginSize(includeMargin, marginSize));
+ const cis = computed(() => getImageSettings(cs.value, size, mg.value, imageSettings));
+
+ return {
+ cells: cs,
+ margin: mg,
+ numCells: computed(() => cs.value.length + mg.value * 2),
+ calculatedImageSettings: cis,
+ qrcode: memoizedQrcode,
+ };
+};
+
+export default useQRCode;
diff --git a/src/qrcode/index.ts b/src/qrcode/index.ts
new file mode 100644
index 000000000..387fe22fc
--- /dev/null
+++ b/src/qrcode/index.ts
@@ -0,0 +1,12 @@
+import _QRCode from './qrcode';
+import withInstall from '../utils/withInstall';
+import { TdQRCodeProps } from './type';
+
+import './style';
+
+export type QRCodeProps = TdQRCodeProps;
+export * from './type';
+
+export const QRCode = withInstall(_QRCode, null, null, 'TQrcode');
+
+export default QRCode;
diff --git a/src/qrcode/props.ts b/src/qrcode/props.ts
new file mode 100644
index 000000000..6f78252d5
--- /dev/null
+++ b/src/qrcode/props.ts
@@ -0,0 +1,76 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdQRCodeProps } from './type';
+import { PropType } from 'vue';
+
+export default {
+ /** 二维码背景颜色 */
+ bgColor: {
+ type: String,
+ default: '',
+ },
+ /** 是否有边框 */
+ borderless: Boolean,
+ /** 二维码颜色 */
+ color: {
+ type: String,
+ default: '',
+ },
+ /** 二维码中图片的地址 */
+ icon: {
+ type: String,
+ default: '',
+ },
+ /** 二维码中图片的大小 */
+ iconSize: {
+ type: [Number, Object] as PropType,
+ default: 40,
+ },
+ /** 二维码纠错等级 */
+ level: {
+ type: String as PropType,
+ 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 as PropType,
+ default: 'active' as TdQRCodeProps['status'],
+ validator(val: TdQRCodeProps['status']): boolean {
+ if (!val) return true;
+ return ['active', 'expired', 'loading', 'scanned'].includes(val);
+ },
+ },
+ /** 自定义状态渲染器 */
+ statusRender: {
+ type: Function as PropType,
+ },
+ /** 渲染类型 */
+ type: {
+ type: String as PropType,
+ default: 'canvas' as TdQRCodeProps['type'],
+ validator(val: TdQRCodeProps['type']): boolean {
+ if (!val) return true;
+ return ['canvas', 'svg'].includes(val);
+ },
+ },
+ /** 扫描后的文本 */
+ value: {
+ type: String,
+ default: '',
+ },
+ /** 点击"点击刷新"的回调 */
+ onRefresh: Function as PropType,
+};
diff --git a/src/qrcode/qrcode.en-US.md b/src/qrcode/qrcode.en-US.md
new file mode 100644
index 000000000..1a8465be4
--- /dev/null
+++ b/src/qrcode/qrcode.en-US.md
@@ -0,0 +1,27 @@
+:: BASE_DOC ::
+
+## API
+
+
+### QRCode Props
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+bgColor | 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
+iconSize | 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-vue/tree/develop/src/qrcode/type.ts) | N
+statusRender | Slot / Function | - | Custom state renderer。Typescript:`(info:StatusRenderInfo) => TNode` `type StatusRenderInfo = {status:QRStatus;onRefresh?: () => void;}`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/qrcode/type.ts) | N
+type | String | canvas | render type。options: canvas/svg | N
+value | String | - | scanned text | N
+onRefresh | Function | | Typescript:`() => void`
Click the "Click to refresh" callback | N
+
+### QRCode Events
+
+name | params | description
+-- | -- | --
+refresh | \- | Click the "Click to refresh" callback
diff --git a/src/qrcode/qrcode.md b/src/qrcode/qrcode.md
new file mode 100644
index 000000000..89eaf85be
--- /dev/null
+++ b/src/qrcode/qrcode.md
@@ -0,0 +1,27 @@
+:: BASE_DOC ::
+
+## API
+
+
+### QRCode Props
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+bgColor | String | - | 二维码背景颜色 | N
+borderless | Boolean | false | 是否有边框 | N
+color | String | - | 二维码颜色 | N
+icon | String | - | 二维码中图片的地址 | N
+iconSize | 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-vue/tree/develop/src/qrcode/type.ts) | N
+statusRender | Slot / Function | - | 自定义状态渲染器。TS 类型:`(info:StatusRenderInfo) => TNode` `type StatusRenderInfo = {status:QRStatus;onRefresh?: () => void;}`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/qrcode/type.ts) | N
+type | String | canvas | 渲染类型。可选项:canvas/svg | N
+value | String | - | 扫描后的文本 | N
+onRefresh | Function | | TS 类型:`() => void`
点击"点击刷新"的回调 | N
+
+### QRCode Events
+
+名称 | 参数 | 描述
+-- | -- | --
+refresh | \- | 点击"点击刷新"的回调
diff --git a/src/qrcode/qrcode.tsx b/src/qrcode/qrcode.tsx
new file mode 100644
index 000000000..351e6c101
--- /dev/null
+++ b/src/qrcode/qrcode.tsx
@@ -0,0 +1,112 @@
+import { computed, defineComponent } from 'vue';
+import { isNumber } from 'lodash-es';
+import { usePrefixClass, useConfig, useVariables } from '../hooks';
+import { renderTNodeJSX } from '../utils/render-tnode';
+import { DEFAULT_FRONT_COLOR } from '../_common/js/qrcode/utils';
+import props from './props';
+
+import QRCodeCanvas from './components/qrcode-canvas';
+import QRCodeSVG from './components/qrcode-svg';
+import QRcodeStatus from './components/qrcode-status';
+
+import type { ImageSettings } from '../_common/js/qrcode/types';
+
+export default defineComponent({
+ name: 'TQRCode',
+ props,
+ setup(props) {
+ const classPrefix = usePrefixClass();
+ const { globalConfig } = useConfig('qrcode');
+
+ const { themeFgColor, themeBgColor } = useVariables({
+ themeFgColor: '--td-text-color-primary',
+ themeBgColor: '--td-bg-color-specialcomponent',
+ });
+
+ // bgColor:自定义颜色 > 主题色适配 > 透明[transparent]
+ const finalBgColor = computed(() => props.bgColor || themeBgColor.value || 'transparent');
+ // color[fgColor]:自定义颜色 > 主题色适配 > 默认颜色[#000000]
+ const finalFgColor = computed(() => props.color || themeFgColor.value || DEFAULT_FRONT_COLOR);
+
+ if (!props.value) {
+ return null;
+ }
+
+ const imageSettings = computed(() => ({
+ src: props.icon,
+ x: undefined,
+ y: undefined,
+ height: isNumber(props.iconSize) ? props.iconSize : props.iconSize?.height ?? 40,
+ width: isNumber(props.iconSize) ? props.iconSize : props.iconSize?.width ?? 40,
+ excavate: true,
+ crossOrigin: 'anonymous',
+ }));
+
+ const classes = computed(() => [
+ `${classPrefix.value}-qrcode`,
+ {
+ [`${classPrefix.value}-borderless`]: props.borderless,
+ [`${classPrefix.value}-qrcode-svg`]: props.type === 'svg',
+ },
+ ]);
+
+ const mergedStyle = computed(() => ({
+ backgroundColor: finalBgColor.value,
+ width: `${props.size}px`,
+ height: `${props.size}px`,
+ }));
+
+ return {
+ classes,
+ mergedStyle,
+ globalConfig,
+ finalBgColor,
+ finalFgColor,
+ classPrefix,
+ imageSettings,
+ };
+ },
+ render() {
+ return (
+
+ {this.status !== 'active' && (
+
+ renderTNodeJSX(this, 'statusRender', {
+ params: {
+ status: this.status,
+ onRefresh: this.onRefresh,
+ },
+ })
+ }
+ refresh={this.onRefresh}
+ />
+
+ )}
+ {this.type === 'canvas' ? (
+
+ ) : (
+
+ )}
+
+ );
+ },
+});
diff --git a/src/qrcode/style/css.js b/src/qrcode/style/css.js
new file mode 100644
index 000000000..6a9a4b132
--- /dev/null
+++ b/src/qrcode/style/css.js
@@ -0,0 +1 @@
+import './index.css';
diff --git a/src/qrcode/style/index.js b/src/qrcode/style/index.js
new file mode 100644
index 000000000..6714f08d3
--- /dev/null
+++ b/src/qrcode/style/index.js
@@ -0,0 +1 @@
+import '../../_common/style/web/components/qrcode/_index.less';
diff --git a/src/qrcode/type.ts b/src/qrcode/type.ts
new file mode 100644
index 000000000..e2e200fd9
--- /dev/null
+++ b/src/qrcode/type.ts
@@ -0,0 +1,72 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TNode } from '../common';
+
+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;
+ /**
+ * 自定义状态渲染器
+ */
+ statusRender?: (info: StatusRenderInfo) => TNode;
+ /**
+ * 渲染类型
+ * @default canvas
+ */
+ type?: 'canvas' | 'svg';
+ /**
+ * 扫描后的文本
+ * @default ''
+ */
+ value?: string;
+ /**
+ * 点击"点击刷新"的回调
+ */
+ onRefresh?: () => void;
+}
+
+export type QRStatus = 'active' | 'expired' | 'loading' | 'scanned';
+
+export type StatusRenderInfo = { status: QRStatus; onRefresh?: () => void };
diff --git a/src/statistic/__tests__/index.test.jsx b/src/statistic/__tests__/index.test.jsx
index 6e57dfceb..c646cf3fd 100644
--- a/src/statistic/__tests__/index.test.jsx
+++ b/src/statistic/__tests__/index.test.jsx
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import Statistic from '@/src/statistic/index.ts';
+import { COLOR_MAP } from '../../_common/js/statistic/utils';
describe('Statistic', () => {
describe(':props', () => {
@@ -110,5 +111,48 @@ describe('Statistic', () => {
expect(trendIconElement.exists()).toBe(true);
expect(trendIconElement.is('svg')).toBe(true);
});
+
+ it('color:#ff0000', () => {
+ const wrapper = mount(Statistic, {
+ propsData: {
+ title: 'Total Sales',
+ value: 1000,
+ color: '#ff0000',
+ },
+ });
+
+ const contentElement = wrapper.find('.t-statistic-content');
+ expect(contentElement.exists()).toBe(true);
+ expect(contentElement.attributes('style')).toContain('color: rgb(255, 0, 0)');
+ });
+
+ it('color:yellow', () => {
+ const wrapper = mount(Statistic, {
+ propsData: {
+ title: 'Total Sales',
+ value: 1000,
+ color: 'yellow',
+ },
+ });
+
+ const contentElement = wrapper.find('.t-statistic-content');
+ expect(contentElement.exists()).toBe(true);
+ expect(contentElement.attributes('style')).toContain('color: yellow');
+ });
+
+ it('colors: colorKeys', async () => {
+ Object.keys(COLOR_MAP).forEach((color) => {
+ const wrapper = mount(Statistic, {
+ propsData: {
+ value: 1000,
+ color,
+ },
+ });
+
+ const contentElement = wrapper.find('.t-statistic-content');
+ expect(contentElement.exists()).toBe(true);
+ expect(contentElement.attributes('style')).toContain(`color: ${COLOR_MAP[color]}`);
+ });
+ });
});
});
diff --git a/src/statistic/props.ts b/src/statistic/props.ts
index d70a7d031..478db159d 100644
--- a/src/statistic/props.ts
+++ b/src/statistic/props.ts
@@ -14,13 +14,10 @@ export default {
},
/** 是否开始动画 */
animationStart: Boolean,
- /** 颜色风格,依次为 TDesign 风格的黑色、蓝色、红色、橙色、绿色。也可以为任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持的 RGB 等值 */
+ /** 颜色风格,预设五个 TDesign 颜色风格:黑色(black)、蓝色(blue)、红色(red)、橙色(orange)、绿色(green)支持深浅色模式切换。也可以自定义任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持颜色值,深浅色模式切换需自行适配 */
color: {
- type: String as PropType,
- validator(val: TdStatisticProps['color']): boolean {
- if (!val) return true;
- return ['black', 'blue', 'red', 'orange', 'green'].includes(val);
- },
+ type: String,
+ default: '',
},
/** 小数保留位数 */
decimalPlaces: {
@@ -40,7 +37,7 @@ export default {
prefix: {
type: [String, Function] as PropType,
},
- /** 默认展示进位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时隐藏分隔符 */
+ /** 默认展示千位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时展示默认分隔符 */
separator: {
type: String,
default: ',',
diff --git a/src/statistic/statistic.en-US.md b/src/statistic/statistic.en-US.md
index 17649bfbe..5aff808d9 100644
--- a/src/statistic/statistic.en-US.md
+++ b/src/statistic/statistic.en-US.md
@@ -1,22 +1,29 @@
:: BASE_DOC ::
## API
+
### Statistic Props
name | type | default | description | required
-- | -- | -- | -- | --
animation | Object | - | Animation effect control, `duration` refers to the transition time of the animation `unit: millisecond`, `valueFrom` refers to the initial value of the animation. `{ duration, valueFrom }`。Typescript:`animation` `interface animation { duration: number; valueFrom: number; }`。[see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/statistic/type.ts) | N
animationStart | Boolean | false | Whether to start animation | N
-color | String | - | Color style, followed by TDesign style black, blue, red, orange, green.Can also be any RGB equivalent supported by [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value)。options:black/blue/red/orange/green | N
+color | String | - | The color style can support TDesign's light and dark modes with the following options: black, blue, red, orange, and green. Alternatively, it can be any [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) value, but in this case, TDesign's light and dark modes will not be supported. | N
decimalPlaces | Number | - | Decimal places | N
extra | String / Slot / Function | - | Additional display content。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
format | Function | - | Format numeric display value。Typescript:`(value: number) => number` | N
loading | Boolean | false | Loading | N
prefix | String / Slot / Function | - | Prefix content, display priority is higher than trend。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
-separator | String | , | The carry separator is displayed by default, and can be customized to other content. When `separator = ''` is set to an empty string/null/undefined, the separator is hidden | N
+separator | String | , | Thousands separator is displayed by default, and can be customized to other content, and the default separator is displayed when `separator = ''` is set to an empty string/null/undefined | N
suffix | String / Slot / Function | - | Suffix content, display priority is higher than trend。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
title | String / Slot / Function | - | The title of Statistic。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
-trend | String | - | trend。options:increase/decrease | N
-trendPlacement | String | left | Position of trending placements。options:left/right | N
+trend | String | - | trend。options: increase/decrease | N
+trendPlacement | String | left | Position of trending placements。options: left/right | N
unit | String / Slot / Function | - | Unit content。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
value | Number | - | The value of Statistic | N
+
+### StatisticInstanceFunctions 组件实例方法
+
+name | params | return | description
+-- | -- | -- | --
+start | \- | \- | required。start animation
diff --git a/src/statistic/statistic.md b/src/statistic/statistic.md
index b8bc87063..2be74ce0d 100644
--- a/src/statistic/statistic.md
+++ b/src/statistic/statistic.md
@@ -1,22 +1,29 @@
:: BASE_DOC ::
## API
+
### Statistic Props
-名称 | 类型 | 默认值 | 说明 | 必传
+名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
animation | Object | - | 动画效果控制,`duration` 指动画的过渡时间`单位:毫秒`,`valueFrom` 指动画的起始数值。`{ duration, valueFrom }`。TS 类型:`animation` `interface animation { duration: number; valueFrom: number; }`。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/statistic/type.ts) | N
animationStart | Boolean | false | 是否开始动画 | N
-color | String | - | 颜色风格,依次为 TDesign 风格的黑色、蓝色、红色、橙色、绿色。也可以为任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持的 RGB 等值。可选项:black/blue/red/orange/green | N
+color | String | - | 颜色风格,预设五个 TDesign 颜色风格:黑色(black)、蓝色(blue)、红色(red)、橙色(orange)、绿色(green)支持深浅色模式切换。也可以自定义任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持颜色值,深浅色模式切换需自行适配 | N
decimalPlaces | Number | - | 小数保留位数 | N
extra | String / Slot / Function | - | 额外的显示内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
format | Function | - | 格式化数值显示值。TS 类型:`(value: number) => number` | N
loading | Boolean | false | 是否加载中 | N
prefix | String / Slot / Function | - | 前缀内容,展示优先级高于 trend。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
-separator | String | , | 默认展示进位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时隐藏分隔符 | N
+separator | String | , | 默认展示千位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时展示默认分隔符 | N
suffix | String / Slot / Function | - | 后缀内容,展示优先级高于 trend。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
title | String / Slot / Function | - | 数值显示的标题。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
trend | String | - | 趋势。可选项:increase/decrease | N
trendPlacement | String | left | 趋势展示位置。可选项:left/right | N
unit | String / Slot / Function | - | 单位内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts) | N
value | Number | - | 数值显示的值 | N
+
+### StatisticInstanceFunctions 组件实例方法
+
+名称 | 参数 | 返回值 | 描述
+-- | -- | -- | --
+start | \- | \- | 必需。开始动画
diff --git a/src/statistic/statistic.tsx b/src/statistic/statistic.tsx
index 6d6f594c8..7478e02e5 100644
--- a/src/statistic/statistic.tsx
+++ b/src/statistic/statistic.tsx
@@ -30,7 +30,7 @@ export default defineComponent({
const tween = ref(null);
const numberValue = computed(() => (isNumber(props.value) ? props.value : 0));
- const valueStyle = computed(() => ({ color: COLOR_MAP[color.value] || color.value }));
+ const valueStyle = computed(() => ({ color: COLOR_MAP[color.value as keyof typeof COLOR_MAP] || color.value }));
const innerDecimalPlaces = computed(
() => decimalPlaces.value ?? numberValue.value.toString().split('.')[1]?.length ?? 0,
);
diff --git a/src/statistic/type.ts b/src/statistic/type.ts
index 06d43d29f..e12180efe 100644
--- a/src/statistic/type.ts
+++ b/src/statistic/type.ts
@@ -17,9 +17,10 @@ export interface TdStatisticProps {
*/
animationStart?: boolean;
/**
- * 颜色风格,依次为 TDesign 风格的黑色、蓝色、红色、橙色、绿色。也可以为任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持的 RGB 等值
+ * 颜色风格,预设五个 TDesign 颜色风格:黑色(black)、蓝色(blue)、红色(red)、橙色(orange)、绿色(green)支持深浅色模式切换。也可以自定义任何 [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 支持颜色值,深浅色模式切换需自行适配
+ * @default ''
*/
- color?: 'black' | 'blue' | 'red' | 'orange' | 'green';
+ color?: string;
/**
* 小数保留位数
*/
@@ -42,7 +43,7 @@ export interface TdStatisticProps {
*/
prefix?: string | TNode;
/**
- * 默认展示进位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时隐藏分隔符
+ * 默认展示千位分隔符,可以自定义为其他内容,`separator = ''` 设置为空字符串/null/undefined 时展示默认分隔符
* @default ,
*/
separator?: string;
@@ -73,6 +74,14 @@ export interface TdStatisticProps {
value?: number;
}
+/** 组件实例方法 */
+export interface StatisticInstanceFunctions {
+ /**
+ * 开始动画
+ */
+ start: () => void;
+}
+
export interface animation {
duration: number;
valueFrom: number;
diff --git a/src/switch/switch.tsx b/src/switch/switch.tsx
index 2eb1e257c..a513188a1 100755
--- a/src/switch/switch.tsx
+++ b/src/switch/switch.tsx
@@ -130,7 +130,7 @@ export default mixins(classPrefixMixins).extend({
},
render(): VNode {
const {
- loading, disabled, content, nodeClasses, classes, toggle, contentClasses,
+ loading, content, nodeClasses, classes, toggle, contentClasses,
} = this;
let switchContent: TNodeReturnValue;
@@ -143,8 +143,7 @@ export default mixins(classPrefixMixins).extend({
}
return (
- // @ts-ignore
-
+
{loadingContent}
{switchContent}
diff --git a/src/tree/hooks/useRenderLabel.tsx b/src/tree/hooks/useRenderLabel.tsx
index d48a36b58..910b69f67 100644
--- a/src/tree/hooks/useRenderLabel.tsx
+++ b/src/tree/hooks/useRenderLabel.tsx
@@ -74,6 +74,7 @@ export default function useRenderLabel(state: TypeTreeItemState) {
stopLabelTrigger={shouldStopLabelTrigger.value}
ignore={treeProps.expandOnClickNode ? 'active' : 'expand,active'}
props={itemCheckProps}
+ title={node.label}
>
{labelNode}
diff --git a/src/upload/themes/dragger-file.tsx b/src/upload/themes/dragger-file.tsx
index 7056a8b81..a9f25d418 100644
--- a/src/upload/themes/dragger-file.tsx
+++ b/src/upload/themes/dragger-file.tsx
@@ -74,6 +74,13 @@ export default defineComponent({
};
},
+ computed: {
+ hasActiveFile() {
+ const file = this.displayFiles[0];
+ return file && (['progress', 'success', 'fail', 'waiting'].includes(file.status) || !file.status);
+ },
+ },
+
methods: {
renderImage() {
if (!this.displayFiles.length) return;
@@ -208,16 +215,20 @@ export default defineComponent({
},
getContent() {
- const file = this.displayFiles[0];
- if (file && (['progress', 'success', 'fail', 'waiting'].includes(file.status) || !file.status)) {
+ if (this.hasActiveFile) {
return this.renderMainPreview();
}
return (
-
+
{this.$scopedSlots.default?.(null) || this.renderDefaultDragElement()}
);
},
+ handleDraggerClick(e: MouseEvent) {
+ if (!this.hasActiveFile) {
+ this.triggerUpload?.(e);
+ }
+ },
},
render() {
@@ -229,6 +240,7 @@ export default defineComponent({
onDragenter={this.drag.handleDragenter}
onDragover={this.drag.handleDragover}
onDragleave={this.drag.handleDragleave}
+ onClick={this.handleDraggerClick}
>
{this.trigger?.(h, { files: this.displayFiles, dragActive: this.dragActive }) || this.getContent()}
diff --git a/src/utils/withInstall.ts b/src/utils/withInstall.ts
index 2adaf6985..52d924c6e 100644
--- a/src/utils/withInstall.ts
+++ b/src/utils/withInstall.ts
@@ -1,7 +1,12 @@
import Vue, { VueConstructor, PluginObject } from 'vue';
import { capitalize } from 'lodash-es';
-export function withInstall
(comp: T, dep?: PluginObject, directive?: { name: string; comp: unknown }) {
+export function withInstall(
+ comp: T,
+ dep?: PluginObject,
+ directive?: { name: string; comp: unknown },
+ alias?: string,
+) {
const c = comp as any;
const name = c?.options?.name || c.name;
@@ -11,8 +16,9 @@ export function withInstall(comp: T, dep?: PluginObject, directive?: { n
const installConfig = { ...defaults, ...config };
/// 为保证组件名称简洁,前缀保持为一个单词,首字母大写
const defaultPrefix = capitalize(defaults.prefix);
+ const compName = alias || name;
// mapprops component is original component
- let componentName = name.replace(defaultPrefix, '').replace('-mapprops', '');
+ let componentName = compName.replace(defaultPrefix, '').replace('-mapprops', '');
componentName = capitalize(installConfig.prefix) + componentName;
Vue.component(componentName, comp);
diff --git a/test/snap/__snapshots__/csr.test.js.snap b/test/snap/__snapshots__/csr.test.js.snap
index 794688313..24796bd77 100644
--- a/test/snap/__snapshots__/csr.test.js.snap
+++ b/test/snap/__snapshots__/csr.test.js.snap
@@ -394,7 +394,13 @@ exports[`csr snapshot test > csr test ./src/alert/_example/close.vue 1`] = `
- Slot Close
+
+
+ 关闭
+
+
@@ -28970,7 +28976,7 @@ exports[`csr snapshot test > csr test ./src/color-picker/_example/panel.vue 1`]
class="t-space-item"
>