Repository: Tencent/tdesign-vue-next
Pull request #5742 (2025-07-15 03:26:17)

fix(table): fix calculation oldTotalWidth error
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [ ] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->
- https://github.com/Tencent/tdesign-vue-next/issues/5741
### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->

### 📝 更新日志

- [x] 本条 PR 不需要纳入 Changelog

#### tdesign-vue-next
<!--
- feat(组件名称): 处理问题或特性描述
--> 

#### @tdesign-vue-next/chat
<!--
- feat(组件名称): 处理问题或特性描述
--> 

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
- [ ] Changelog 已提供或无须提供


tdesign-bot: # TDesign Component Site Preview [Open](https://github.com/Tencent/tdesign-vue-next/actions/runs/16283302534)
  | Component | Preview |
  | --------- | :--: |
  | tdesign-vue-next | [<img width="300" alt="完成" src="https://user-images.githubusercontent.com/15634204/150816437-9f5bb788-cd67-4cbc-9897-b82d74e9aa65.png" />](https://preview-pr-5742-tdesign-vue-next.surge.sh) | 
| @tdesign-vue-next/chat | [<img width="300" alt="完成" src="https://user-images.githubusercontent.com/15634204/150816437-9f5bb788-cd67-4cbc-9897-b82d74e9aa65.png" />](https://preview-pr-5742-tdesign-vue-next-chat.surge.sh) | 

  <!-- AUTO_PREVIEW_HOOK -->
<!-- AUTO_PREVIEW_HOOK -->

Patch:
diff --git a/.github/workflows/project-issue-trigger.yml b/.github/workflows/project-issue-trigger.yml
new file mode 100644
index 000000000..4bf99f374
--- /dev/null
+++ b/.github/workflows/project-issue-trigger.yml
@@ -0,0 +1,16 @@
+# https://github.com/TDesignOteam/tdesign-projects-action
+
+name: project-issue-trigger
+on:
+  issues:
+    types: [opened, closed]
+jobs:
+  project-issue-trigger:
+    runs-on: ubuntu-latest
+    steps:
+      - name: issue to project
+        uses: TDesignOteam/tdesign-projects-action@main
+        env:
+          GH_TOKEN: ${{ secrets.TDESIGN_BOT_TOKEN }}
+          PROJECT_TYPE: ISSUE2TRIGGER
+          PROJECT_ID: 1
diff --git a/.github/workflows/project-label-trigger.yml b/.github/workflows/project-label-trigger.yml
new file mode 100644
index 000000000..bca7c39e2
--- /dev/null
+++ b/.github/workflows/project-label-trigger.yml
@@ -0,0 +1,18 @@
+# https://github.com/TDesignOteam/tdesign-projects-action
+
+name: projects-label-trigger
+
+on:
+  issues:
+    types: [labeled, unlabeled]
+
+jobs:
+  projects-label-trigger:
+    runs-on: ubuntu-latest
+    steps:
+      - name: label to project
+        uses: TDesignOteam/tdesign-projects-action@main
+        env:
+          GH_TOKEN: ${{ secrets.TDESIGN_BOT_TOKEN }}
+          PROJECT_TYPE: LABEL2TRIGGER
+          PROJECT_ID: 1
diff --git a/.github/workflows/project-pr-trigger.yml b/.github/workflows/project-pr-trigger.yml
new file mode 100644
index 000000000..9614b1a06
--- /dev/null
+++ b/.github/workflows/project-pr-trigger.yml
@@ -0,0 +1,18 @@
+# https://github.com/TDesignOteam/tdesign-projects-action
+
+name: project-pr-trigger
+
+on:
+  pull_request_target:
+    types: [opened, reopened, closed]
+
+jobs:
+  project-pr-trigger:
+    runs-on: ubuntu-latest
+    steps:
+      - name: pr to project
+        uses: TDesignOteam/tdesign-projects-action@main
+        env:
+          GH_TOKEN: ${{ secrets.TDESIGN_BOT_TOKEN }}
+          PROJECT_TYPE: PR2TRIGGER
+          PROJECT_ID: 1
diff --git a/packages/common b/packages/common
index d56150f2e..a27c913bc 160000
--- a/packages/common
+++ b/packages/common
@@ -1,1 +1,1 @@
-Subproject commit d56150f2ee85c87fcd26710a0c3fb599ecd78183
+Subproject commit a27c913bc6ae659038e58974549cf6f669b78806
diff --git a/packages/components/components.ts b/packages/components/components.ts
index e2db45e9b..dfaec9e7e 100644
--- a/packages/components/components.ts
+++ b/packages/components/components.ts
@@ -67,6 +67,7 @@ export * from './collapse';
 export * from './watermark';
 export * from './rate';
 export * from './typography';
+export * from './qrcode';
 // 消息提醒
 
 export * from './alert';
diff --git a/packages/components/config-provider/config-provider.en-US.md b/packages/components/config-provider/config-provider.en-US.md
index 5ee843133..c9c8419f1 100644
--- a/packages/components/config-provider/config-provider.en-US.md
+++ b/packages/components/config-provider/config-provider.en-US.md
@@ -89,6 +89,7 @@ list | Object | - | List global configs。Typescript：`ListConfig` | N
 message | Object | - | Message Component global configs。Typescript：`MessageConfig` | N
 pagination | Object | - | Pagination global configs。Typescript：`PaginationConfig` | N
 popconfirm | Object | - | Popconfirm global configs。Typescript：`PopconfirmConfig` | N
+qrcode | Object | - | QRCode global configs。Typescript：`QRCodeConfig` | N
 rate | Object | - | Rate global configs。Typescript：`RateConfig` | N
 select | Object | - | Select global configs。Typescript：`SelectConfig` | N
 steps | Object | - | Steps global configs。Typescript：`StepsConfig` | N
@@ -289,6 +290,14 @@ cancel | String / Object | - | Typescript：`string \| ButtonProps`，[Button AP
 confirm | String / Object | - | Typescript：`string \| ButtonProps` | N
 confirmBtnTheme | Object | - | Typescript：`{ default: string; warning: string; danger: string; }` | N
 
+### QRCodeConfig
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+expiredText | String | - | Language configuration, "QR code expired" description text | N
+refreshText | String | - | Language configuration, "QR code refresh" description text | N
+scannedText | String | - | Language configuration, "QR code scanned" description text | N
+
 ### RateConfig
 
 name | type | default | description | required
diff --git a/packages/components/config-provider/config-provider.md b/packages/components/config-provider/config-provider.md
index 099863e74..f2b8dbc06 100644
--- a/packages/components/config-provider/config-provider.md
+++ b/packages/components/config-provider/config-provider.md
@@ -90,6 +90,7 @@ list | Object | - | 列表组件全局配置。TS 类型：`ListConfig` | N
 message | Object | - | 消息组件全局配置。TS 类型：`MessageConfig` | N
 pagination | Object | - | 分页组件全局配置。TS 类型：`PaginationConfig` | N
 popconfirm | Object | - | 气泡确认框全局配置。TS 类型：`PopconfirmConfig` | N
+qrcode | Object | - | 二维码全局配置。TS 类型：`QRCodeConfig` | N
 rate | Object | - | 评分全局配置。TS 类型：`RateConfig` | N
 select | Object | - | 选择器组件全局配置。TS 类型：`SelectConfig` | N
 steps | Object | - | 步骤条组件全局配置。TS 类型：`StepsConfig` | N
@@ -290,6 +291,14 @@ cancel | String / Object | - | 语言配置，“取消”描述文本。TS 类
 confirm | String / Object | - | 语言配置，“确定”描述文本。TS 类型：`string \| ButtonProps` | N
 confirmBtnTheme | Object | - | 确认按钮主题色，即 Popconfirm 的 `theme` 和 确认按钮的 `theme` 映射关系。示例：{ danger: 'danger' }。TS 类型：`{ default: string; warning: string; danger: string; }` | N
 
+### QRCodeConfig
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+expiredText | String | - | 语言配置，“二维码过期”描述文本 | N
+refreshText | String | - | 语言配置，“点击刷新”描述文本 | N
+scannedText | String | - | 语言配置，“已扫描”描述文本 | N
+
 ### RateConfig
 
 名称 | 类型 | 默认值 | 描述 | 必传
diff --git a/packages/components/config-provider/type.ts b/packages/components/config-provider/type.ts
index 3fe8b9bd5..eee886a56 100644
--- a/packages/components/config-provider/type.ts
+++ b/packages/components/config-provider/type.ts
@@ -118,6 +118,10 @@ export interface GlobalConfigProvider {
    * 气泡确认框全局配置
    */
   popconfirm?: PopconfirmConfig;
+  /**
+   * 二维码全局配置
+   */
+  qrcode?: QRCodeConfig;
   /**
    * 评分全局配置
    */
@@ -720,6 +724,24 @@ export interface PopconfirmConfig {
   confirmBtnTheme?: { default: string; warning: string; danger: string };
 }
 
+export interface QRCodeConfig {
+  /**
+   * 语言配置，“二维码过期”描述文本
+   * @default ''
+   */
+  expiredText?: string;
+  /**
+   * 语言配置，“点击刷新”描述文本
+   * @default ''
+   */
+  refreshText?: string;
+  /**
+   * 语言配置，“已扫描”描述文本
+   * @default ''
+   */
+  scannedText?: string;
+}
+
 export interface RateConfig {
   /**
    * 评分描述，默认值：['极差', '失望', '一般', '满意', '惊喜']
diff --git a/packages/components/dialog/dialog.md b/packages/components/dialog/dialog.md
index 3b2bf5eb5..ac8c70e8f 100644
--- a/packages/components/dialog/dialog.md
+++ b/packages/components/dialog/dialog.md
@@ -70,6 +70,16 @@
 
 如果您不确定问题是否是由该规则引起的，或者确定该规则不是问题的根本原因，请在 `GitHub` 上提出一个 `issue`，并提供可以重现问题的代码。这将有助于我们更好地了解您的问题并提供更好的帮助。
 
+### 多层 Dialog 嵌套时，外层 Dialog 开启 destroy-on-close 后出现异常的情况？
+
+使用嵌套多层使用 dialog 且最外层开启了 `destroy-on-close` ，会出现框架层的报错：
+
+> Uncaught (in promise) NotFoundError: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
+
+解决方法： 除最后一层 dialog，外层的所有 dialog 都设置 `lazy` 为`true`；
+
+详情见 [issue](https://github.com/Tencent/tdesign-vue-next/pull/5659)，可通过此[链接](https://stackblitz.com/edit/vitejs-vite-vnywg115?file=src%2FApp.vue)体验。
+
 ## API
 
 ### DialogCard Props
diff --git a/packages/components/pagination/pagination-mini.tsx b/packages/components/pagination/pagination-mini.tsx
index 52582eaf6..808315fe0 100644
--- a/packages/components/pagination/pagination-mini.tsx
+++ b/packages/components/pagination/pagination-mini.tsx
@@ -12,6 +12,7 @@ import props from './pagination-mini-props';
 import { useGlobalIcon, usePrefixClass } from '@tdesign/shared-hooks';
 
 import TButton from '../button';
+import TTooltip from '../tooltip';
 
 export default defineComponent({
   name: 'TPaginationMini',
@@ -46,6 +47,15 @@ export default defineComponent({
       return { prev: false, current: false, next: false };
     });
 
+    const renderWithTooltip = (content: string | undefined, node: JSX.Element) => {
+      if (!content) return node;
+      return (
+        <TTooltip content={content} showArrow={false}>
+          {node}
+        </TTooltip>
+      );
+    };
+
     return () => {
       const jumperClass = [
         COMPONENT_NAME.value,
@@ -56,40 +66,45 @@ export default defineComponent({
 
       return (
         <div class={jumperClass}>
-          <TButton
-            title={titleConfig.value.prev}
-            variant={props.variant}
-            size={props.size}
-            shape="square"
-            onClick={(e) => props.onChange?.({ e, trigger: 'prev' })}
-            icon={props.layout === 'horizontal' ? () => <ChevronLeftIcon /> : () => <ChevronUpIcon />}
-            class={`${COMPONENT_NAME.value}__prev`}
-            disabled={disabledConfig.value.prev}
-          />
-
-          {props.showCurrent && (
+          {renderWithTooltip(
+            titleConfig.value.prev,
             <TButton
-              title={titleConfig.value.current}
               variant={props.variant}
               size={props.size}
               shape="square"
-              onClick={(e) => props.onChange?.({ e, trigger: 'current' })}
-              icon={() => <RoundIcon />}
-              class={`${COMPONENT_NAME.value}__current`}
-              disabled={disabledConfig.value.current}
-            />
+              onClick={(e) => props.onChange?.({ e, trigger: 'prev' })}
+              icon={props.layout === 'horizontal' ? () => <ChevronLeftIcon /> : () => <ChevronUpIcon />}
+              class={`${COMPONENT_NAME.value}__prev`}
+              disabled={disabledConfig.value.prev}
+            />,
           )}
 
-          <TButton
-            title={titleConfig.value.next}
-            variant={props.variant}
-            size={props.size}
-            shape="square"
-            onClick={(e) => props.onChange?.({ e, trigger: 'next' })}
-            icon={props.layout === 'horizontal' ? () => <ChevronRightIcon /> : () => <ChevronDownIcon />}
-            class={`${COMPONENT_NAME.value}__next`}
-            disabled={disabledConfig.value.next}
-          />
+          {props.showCurrent &&
+            renderWithTooltip(
+              titleConfig.value.current,
+              <TButton
+                variant={props.variant}
+                size={props.size}
+                shape="square"
+                onClick={(e) => props.onChange?.({ e, trigger: 'current' })}
+                icon={() => <RoundIcon />}
+                class={`${COMPONENT_NAME.value}__current`}
+                disabled={disabledConfig.value.current}
+              />,
+            )}
+
+          {renderWithTooltip(
+            titleConfig.value.next,
+            <TButton
+              variant={props.variant}
+              size={props.size}
+              shape="square"
+              onClick={(e) => props.onChange?.({ e, trigger: 'next' })}
+              icon={props.layout === 'horizontal' ? () => <ChevronRightIcon /> : () => <ChevronDownIcon />}
+              class={`${COMPONENT_NAME.value}__next`}
+              disabled={disabledConfig.value.next}
+            />,
+          )}
         </div>
       );
     };
diff --git a/packages/components/qrcode/_example-ts/base.vue b/packages/components/qrcode/_example-ts/base.vue
new file mode 100644
index 000000000..602177a62
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/base.vue
@@ -0,0 +1,13 @@
+<template>
+  <div style="width: 100%">
+    <t-space direction="vertical" align="center">
+      <t-qrcode :value="text || '-'" />
+      <t-input v-model="text" placeholder="-" />
+    </t-space>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+const text = ref('https://tdesign.tencent.com/');
+</script>
diff --git a/packages/components/qrcode/_example-ts/customColor.vue b/packages/components/qrcode/_example-ts/customColor.vue
new file mode 100644
index 000000000..e640babb7
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/customColor.vue
@@ -0,0 +1,6 @@
+<template>
+  <t-space>
+    <t-qrcode value="https://tdesign.tencent.com/" color="#0052D9" bg-color="#fff" />
+    <t-qrcode value="https://tdesign.tencent.com/" color="#0052D9" bg-color="#D9E1FF" />
+  </t-space>
+</template>
diff --git a/packages/components/qrcode/_example-ts/customSize.vue b/packages/components/qrcode/_example-ts/customSize.vue
new file mode 100644
index 000000000..ec34c1107
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/customSize.vue
@@ -0,0 +1,36 @@
+<template>
+  <t-space direction="vertical">
+    <t-space>
+      <t-button :disabled="size <= minSize" @click="size -= 10">
+        <template #icon>
+          <MinusIcon />
+        </template>
+        Smaller
+      </t-button>
+      <t-button :disabled="size >= maxSize" @click="size += 10">
+        <template #icon>
+          <AddIcon />
+        </template>
+        Larger
+      </t-button>
+    </t-space>
+    <t-qrcode
+      level="H"
+      :size="size"
+      :icon-size="size / 4"
+      value="https://tdesign.tencent.com/"
+      icon="https://tdesign.gtimg.com/site/tdesign-logo.png"
+    />
+  </t-space>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { MinusIcon, AddIcon } from 'tdesign-icons-vue-next';
+import { QRCodeProps } from 'tdesign-vue-next';
+
+const minSize = 40;
+const maxSize = 300;
+
+const size = ref<QRCodeProps['level']>(160);
+</script>
diff --git a/packages/components/qrcode/_example-ts/customStatusRender.vue b/packages/components/qrcode/_example-ts/customStatusRender.vue
new file mode 100644
index 000000000..b07378a14
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/customStatusRender.vue
@@ -0,0 +1,70 @@
+<template>
+  <t-space>
+    <!-- 自定义状态-使用Function -->
+    <t-qrcode value="https://tdesign.tencent.com/" status="loading" :status-render="renderStatus" />
+    <!-- 自定义状态-使用插槽 -->
+    <t-qrcode value="https://tdesign.tencent.com/" status="expired">
+      <template #status-render="{ status }">
+        <div v-if="status === 'expired'" class="tdesign-demo-qrcode__status-render">
+          <p class="tdesign-demo-qrcode__status-render-title">
+            <CloseCircleFilledIcon size="16" />
+            <span>二维码过期</span>
+          </p>
+          <p class="tdesign-demo-qrcode__status-render-action" @click="handleRefresh">
+            <RefreshIcon size="16" />
+            <span>点击刷新</span>
+          </p>
+        </div>
+      </template>
+    </t-qrcode>
+    <!-- 默认状态 -->
+    <t-qrcode value="https://tdesign.tencent.com/" status="scanned" />
+  </t-space>
+</template>
+
+<script lang="tsx" setup>
+import { CloseCircleFilledIcon, RefreshIcon } from 'tdesign-icons-vue-next';
+import { QRCodeProps } from 'tdesign-vue-next';
+
+const renderStatus: QRCodeProps['statusRender'] = (_h, { status }) => {
+  return (
+    status === 'loading' && (
+      <t-space direction="vertical" size="4px">
+        <t-loading size="32px" />
+        <p>加载中...</p>
+      </t-space>
+    )
+  );
+};
+
+const handleRefresh: QRCodeProps['onRefresh'] = () => {
+  console.log('Click Refresh');
+};
+</script>
+
+<style>
+.tdesign-demo-qrcode__status-render {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+}
+
+.tdesign-demo-qrcode__status-render-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.tdesign-demo-qrcode__status-render-title .t-icon {
+  color: var(--td-error-color);
+}
+
+.tdesign-demo-qrcode__status-render-action {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: var(--td-brand-color);
+  cursor: pointer;
+  line-height: 32px;
+}
+</style>
diff --git a/packages/components/qrcode/_example-ts/download.vue b/packages/components/qrcode/_example-ts/download.vue
new file mode 100644
index 000000000..432ca2755
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/download.vue
@@ -0,0 +1,58 @@
+<template>
+  <t-space direction="vertical">
+    <t-radio-group v-model="type" variant="default-filled">
+      <t-radio-button value="canvas">canvas</t-radio-button>
+      <t-radio-button value="svg">svg</t-radio-button>
+    </t-radio-group>
+    <t-qrcode
+      id="QRCode"
+      :type="type"
+      value="https://tdesign.tencent.com/"
+      icon="https://tdesign.gtimg.com/site/tdesign-logo.png"
+    />
+    <t-button @click="handleDownload">Download</t-button>
+  </t-space>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { QRCodeProps } from 'tdesign-vue-next';
+
+const type = ref<QRCodeProps['type']>('canvas');
+
+function downloadFile(url: string, fileName: string) {
+  const a = document.createElement('a');
+  a.download = fileName;
+  a.href = url;
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+}
+
+const downloadCanvas = () => {
+  const canvas = document.getElementById('QRCode').querySelector('canvas');
+  if (canvas) {
+    const url = canvas.toDataURL();
+    console.log(url);
+    downloadFile(url, 'TDesign-QRCode.png');
+  }
+};
+
+const downloadSvg = () => {
+  const svg = document.getElementById('QRCode').querySelector('svg');
+  const svgData = new XMLSerializer().serializeToString(svg);
+  const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
+  const url = URL.createObjectURL(blob);
+  console.log(url);
+  downloadFile(url, 'TDesign-QRCode.svg');
+};
+
+const handleDownload = () => {
+  if (type.value === 'canvas') {
+    downloadCanvas();
+  }
+  if (type.value === 'svg') {
+    downloadSvg();
+  }
+};
+</script>
diff --git a/packages/components/qrcode/_example-ts/icon.vue b/packages/components/qrcode/_example-ts/icon.vue
new file mode 100644
index 000000000..92ed88e72
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/icon.vue
@@ -0,0 +1,5 @@
+<template>
+  <div style="width: 100%">
+    <t-qrcode value="https://tdesign.tencent.com/" icon="https://tdesign.gtimg.com/site/tdesign-logo.png" />
+  </div>
+</template>
diff --git a/packages/components/qrcode/_example-ts/level.vue b/packages/components/qrcode/_example-ts/level.vue
new file mode 100644
index 000000000..505b2e9f4
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/level.vue
@@ -0,0 +1,18 @@
+<template>
+  <t-space direction="vertical">
+    <t-radio-group v-model="level" variant="default-filled">
+      <t-radio-button value="L">L</t-radio-button>
+      <t-radio-button value="M">M</t-radio-button>
+      <t-radio-button value="Q">Q</t-radio-button>
+      <t-radio-button value="H">H</t-radio-button>
+    </t-radio-group>
+    <t-qrcode :level="level" value="https://tdesign.gtimg.com/site/tdesign-logo.png" />
+  </t-space>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import { QRCodeProps } from 'tdesign-vue-next';
+
+const level = ref<QRCodeProps['level']>('M');
+</script>
diff --git a/packages/components/qrcode/_example-ts/popover.vue b/packages/components/qrcode/_example-ts/popover.vue
new file mode 100644
index 000000000..cb798aac0
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/popover.vue
@@ -0,0 +1,15 @@
+<template>
+  <div style="width: 100%">
+    <t-popup trigger="hover">
+      <t-button>Hover me</t-button>
+      <template #content>
+        <t-qrcode
+          value="https://tdesign.tencent.com/"
+          :size="146"
+          borderless
+          style="padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-s)"
+        />
+      </template>
+    </t-popup>
+  </div>
+</template>
diff --git a/packages/components/qrcode/_example-ts/status.vue b/packages/components/qrcode/_example-ts/status.vue
new file mode 100644
index 000000000..f75c435a3
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/status.vue
@@ -0,0 +1,7 @@
+<template>
+  <t-space>
+    <t-qrcode value="https://tdesign.tencent.com/" status="loading" />
+    <t-qrcode value="https://tdesign.tencent.com/" status="expired" :on-refresh="() => console.log('Click Refresh')" />
+    <t-qrcode value="https://tdesign.tencent.com/" status="scanned" />
+  </t-space>
+</template>
diff --git a/packages/components/qrcode/_example-ts/type.vue b/packages/components/qrcode/_example-ts/type.vue
new file mode 100644
index 000000000..aa637e5de
--- /dev/null
+++ b/packages/components/qrcode/_example-ts/type.vue
@@ -0,0 +1,6 @@
+<template>
+  <t-space>
+    <t-qrcode value="https://tdesign.tencent.com/" type="canvas" />
+    <t-qrcode value="https://tdesign.tencent.com/" type="svg" />
+  </t-space>
+</template>
diff --git a/packages/components/qrcode/_example/base.vue b/packages/components/qrcode/_example/base.vue
new file mode 100644
index 000000000..83598be2a
--- /dev/null
+++ b/packages/components/qrcode/_example/base.vue
@@ -0,0 +1,13 @@
+<template>
+  <div style="width: 100%">
+    <t-space direction="vertical" align="center">
+      <t-qrcode :value="text || '-'" />
+      <t-input v-model="text" placeholder="-" />
+    </t-space>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+const text = ref('https://tdesign.tencent.com/');
+</script>
diff --git a/packages/components/qrcode/_example/customColor.vue b/packages/components/qrcode/_example/customColor.vue
new file mode 100644
index 000000000..e640babb7
--- /dev/null
+++ b/packages/components/qrcode/_example/customColor.vue
@@ -0,0 +1,6 @@
+<template>
+  <t-space>
+    <t-qrcode value="https://tdesign.tencent.com/" color="#0052D9" bg-color="#fff" />
+    <t-qrcode value="https://tdesign.tencent.com/" color="#0052D9" bg-color="#D9E1FF" />
+  </t-space>
+</template>
diff --git a/packages/components/qrcode/_example/customSize.vue b/packages/components/qrcode/_example/customSize.vue
new file mode 100644
index 000000000..cb4f95991
--- /dev/null
+++ b/packages/components/qrcode/_example/customSize.vue
@@ -0,0 +1,35 @@
+<template>
+  <t-space direction="vertical">
+    <t-space>
+      <t-button :disabled="size <= minSize" @click="size -= 10">
+        <template #icon>
+          <MinusIcon />
+        </template>
+        Smaller
+      </t-button>
+      <t-button :disabled="size >= maxSize" @click="size += 10">
+        <template #icon>
+          <AddIcon />
+        </template>
+        Larger
+      </t-button>
+    </t-space>
+    <t-qrcode
+      level="H"
+      :size="size"
+      :icon-size="size / 4"
+      value="https://tdesign.tencent.com/"
+      icon="https://tdesign.gtimg.com/site/tdesign-logo.png"
+    />
+  </t-space>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { MinusIcon, AddIcon } from 'tdesign-icons-vue-next';
+
+const minSize = 40;
+const maxSize = 300;
+
+const size = ref(160);
+</script>
diff --git a/packages/components/qrcode/_example/customStatusRender.vue b/packages/components/qrcode/_example/customStatusRender.vue
new file mode 100644
index 000000000..51297ee8f
--- /dev/null
+++ b/packages/components/qrcode/_example/customStatusRender.vue
@@ -0,0 +1,69 @@
+<template>
+  <t-space>
+    <!-- 自定义状态-使用Function -->
+    <t-qrcode value="https://tdesign.tencent.com/" status="loading" :status-render="renderStatus" />
+    <!-- 自定义状态-使用插槽 -->
+    <t-qrcode value="https://tdesign.tencent.com/" status="expired">
+      <template #status-render="{ status }">
+        <div v-if="status === 'expired'" class="tdesign-demo-qrcode__status-render">
+          <p class="tdesign-demo-qrcode__status-render-title">
+            <CloseCircleFilledIcon size="16" />
+            <span>二维码过期</span>
+          </p>
+          <p class="tdesign-demo-qrcode__status-render-action" @click="handleRefresh">
+            <RefreshIcon size="16" />
+            <span>点击刷新</span>
+          </p>
+        </div>
+      </template>
+    </t-qrcode>
+    <!-- 默认状态 -->
+    <t-qrcode value="https://tdesign.tencent.com/" status="scanned" />
+  </t-space>
+</template>
+
+<script lang="jsx" setup>
+import { CloseCircleFilledIcon, RefreshIcon } from 'tdesign-icons-vue-next';
+
+const renderStatus = (_h, { status }) => {
+  return (
+    status === 'loading' && (
+      <t-space direction="vertical" size="4px">
+        <t-loading size="32px" />
+        <p>加载中...</p>
+      </t-space>
+    )
+  );
+};
+
+const handleRefresh = () => {
+  console.log('Click Refresh');
+};
+</script>
+
+<style>
+.tdesign-demo-qrcode__status-render {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+}
+
+.tdesign-demo-qrcode__status-render-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.tdesign-demo-qrcode__status-render-title .t-icon {
+  color: var(--td-error-color);
+}
+
+.tdesign-demo-qrcode__status-render-action {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: var(--td-brand-color);
+  cursor: pointer;
+  line-height: 32px;
+}
+</style>
diff --git a/packages/components/qrcode/_example/download.vue b/packages/components/qrcode/_example/download.vue
new file mode 100644
index 000000000..93acfda36
--- /dev/null
+++ b/packages/components/qrcode/_example/download.vue
@@ -0,0 +1,57 @@
+<template>
+  <t-space direction="vertical">
+    <t-radio-group v-model="type" variant="default-filled">
+      <t-radio-button value="canvas">canvas</t-radio-button>
+      <t-radio-button value="svg">svg</t-radio-button>
+    </t-radio-group>
+    <t-qrcode
+      id="QRCode"
+      :type="type"
+      value="https://tdesign.tencent.com/"
+      icon="https://tdesign.gtimg.com/site/tdesign-logo.png"
+    />
+    <t-button @click="handleDownload">Download</t-button>
+  </t-space>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+
+const type = ref('canvas');
+
+function downloadFile(url, fileName) {
+  const a = document.createElement('a');
+  a.download = fileName;
+  a.href = url;
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+}
+
+const downloadCanvas = () => {
+  const canvas = document.getElementById('QRCode').querySelector('canvas');
+  if (canvas) {
+    const url = canvas.toDataURL();
+    console.log(url);
+    downloadFile(url, 'TDesign-QRCode.png');
+  }
+};
+
+const downloadSvg = () => {
+  const svg = document.getElementById('QRCode').querySelector('svg');
+  const svgData = new XMLSerializer().serializeToString(svg);
+  const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
+  const url = URL.createObjectURL(blob);
+  console.log(url);
+  downloadFile(url, 'TDesign-QRCode.svg');
+};
+
+const handleDownload = () => {
+  if (type.value === 'canvas') {
+    downloadCanvas();
+  }
+  if (type.value === 'svg') {
+    downloadSvg();
+  }
+};
+</script>
diff --git a/packages/components/qrcode/_example/icon.vue b/packages/components/qrcode/_example/icon.vue
new file mode 100644
index 000000000..92ed88e72
--- /dev/null
+++ b/packages/components/qrcode/_example/icon.vue
@@ -0,0 +1,5 @@
+<template>
+  <div style="width: 100%">
+    <t-qrcode value="https://tdesign.tencent.com/" icon="https://tdesign.gtimg.com/site/tdesign-logo.png" />
+  </div>
+</template>
diff --git a/packages/components/qrcode/_example/level.vue b/packages/components/qrcode/_example/level.vue
new file mode 100644
index 000000000..8653bab2f
--- /dev/null
+++ b/packages/components/qrcode/_example/level.vue
@@ -0,0 +1,17 @@
+<template>
+  <t-space direction="vertical">
+    <t-radio-group v-model="level" variant="default-filled">
+      <t-radio-button value="L">L</t-radio-button>
+      <t-radio-button value="M">M</t-radio-button>
+      <t-radio-button value="Q">Q</t-radio-button>
+      <t-radio-button value="H">H</t-radio-button>
+    </t-radio-group>
+    <t-qrcode :level="level" value="https://tdesign.gtimg.com/site/tdesign-logo.png" />
+  </t-space>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+
+const level = ref('M');
+</script>
diff --git a/packages/components/qrcode/_example/popover.vue b/packages/components/qrcode/_example/popover.vue
new file mode 100644
index 000000000..cb798aac0
--- /dev/null
+++ b/packages/components/qrcode/_example/popover.vue
@@ -0,0 +1,15 @@
+<template>
+  <div style="width: 100%">
+    <t-popup trigger="hover">
+      <t-button>Hover me</t-button>
+      <template #content>
+        <t-qrcode
+          value="https://tdesign.tencent.com/"
+          :size="146"
+          borderless
+          style="padding: var(--td-comp-paddingTB-m) var(--td-comp-paddingLR-s)"
+        />
+      </template>
+    </t-popup>
+  </div>
+</template>
diff --git a/packages/components/qrcode/_example/status.vue b/packages/components/qrcode/_example/status.vue
new file mode 100644
index 000000000..f75c435a3
--- /dev/null
+++ b/packages/components/qrcode/_example/status.vue
@@ -0,0 +1,7 @@
+<template>
+  <t-space>
+    <t-qrcode value="https://tdesign.tencent.com/" status="loading" />
+    <t-qrcode value="https://tdesign.tencent.com/" status="expired" :on-refresh="() => console.log('Click Refresh')" />
+    <t-qrcode value="https://tdesign.tencent.com/" status="scanned" />
+  </t-space>
+</template>
diff --git a/packages/components/qrcode/_example/type.vue b/packages/components/qrcode/_example/type.vue
new file mode 100644
index 000000000..aa637e5de
--- /dev/null
+++ b/packages/components/qrcode/_example/type.vue
@@ -0,0 +1,6 @@
+<template>
+  <t-space>
+    <t-qrcode value="https://tdesign.tencent.com/" type="canvas" />
+    <t-qrcode value="https://tdesign.tencent.com/" type="svg" />
+  </t-space>
+</template>
diff --git a/packages/components/qrcode/_usage/index.vue b/packages/components/qrcode/_usage/index.vue
new file mode 100644
index 000000000..b2dd2975e
--- /dev/null
+++ b/packages/components/qrcode/_usage/index.vue
@@ -0,0 +1,30 @@
+<!-- 该脚本为自动生成，如有需要请在 /script/generate-usage/index.js 中调整 -->
+<template>
+  <base-usage :code="usageCode" :config-list="configList" :panel-list="panelList" @panel-change="onPanelChange">
+    <template #qrcode="{ configProps }">
+      <t-qrcode value="https://tdesign.gtimg.com/site/tdesign-logo.png" v-bind="configProps" />
+    </template>
+  </base-usage>
+</template>
+
+<script setup lang="jsx">
+/* eslint-disable */
+import { ref, onMounted } from 'vue';
+import configJson from './props.json';
+
+const configList = ref(configJson);
+const panelList = [{ label: 'QRCode', value: 'qrcode' }];
+
+const usageCodeMap = {
+  qrcode: '<t-qrcode value="https://tdesign.gtimg.com/site/tdesign-logo.png" v-bind="configProps"/>',
+};
+const usageCode = ref(`<template>${usageCodeMap[panelList[0].value].trim()}</template>`);
+
+function onPanelChange(panel) {
+  usageCode.value = `<template>${usageCodeMap[panel].trim()}</template>`;
+}
+
+onMounted(() => {
+  console.log();
+});
+</script>
diff --git a/packages/components/qrcode/_usage/props.json b/packages/components/qrcode/_usage/props.json
new file mode 100644
index 000000000..a407f360a
--- /dev/null
+++ b/packages/components/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/packages/components/qrcode/components/props.ts b/packages/components/qrcode/components/props.ts
new file mode 100644
index 000000000..0ca9577e6
--- /dev/null
+++ b/packages/components/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<QRCodeSubComponent['level']>,
+    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<QRCodeSubComponent['style']>,
+    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<QRCodeSubComponent['imageSettings']>,
+    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<QRCodeStatus['locale']>,
+    default: () => ({} as QRCodeStatus['locale']),
+  },
+  onRefresh: {
+    type: Function as PropType<QRCodeStatus['onRefresh']>,
+  },
+  statusRender: {
+    type: Function as PropType<QRCodeStatus['statusRender']>,
+  },
+  status: {
+    type: String as PropType<QRCodeStatus['status']>,
+    default: 'active' as QRCodeStatus['status'],
+  },
+};
diff --git a/packages/components/qrcode/components/qrcode-canvas.tsx b/packages/components/qrcode/components/qrcode-canvas.tsx
new file mode 100644
index 000000000..5e1d567da
--- /dev/null
+++ b/packages/components/qrcode/components/qrcode-canvas.tsx
@@ -0,0 +1,123 @@
+import { computed, defineComponent, ref, watchEffect, onMounted } from 'vue';
+import { QRCodeSubComponentProps } from './props';
+import {
+  DEFAULT_NEED_MARGIN,
+  DEFAULT_MINVERSION,
+  isSupportPath2d,
+  excavateModules,
+  generatePath,
+} from '@tdesign/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 imageRef = ref<HTMLImageElement>(null);
+
+    const canvasRef = ref<HTMLCanvasElement>(null);
+
+    const imgCrossOrigin = ref('');
+
+    const renderQRCode = () => {
+      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,
+      });
+
+      if (!canvasRef.value) {
+        return;
+      }
+
+      const canvas = canvasRef.value;
+      const ctx = canvas.getContext('2d');
+
+      if (!ctx) {
+        return;
+      }
+
+      imgCrossOrigin.value = calculatedImageSettings.value?.crossOrigin;
+
+      let cellsToDraw = cells;
+      const image = imageRef.value;
+
+      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 = props.size * pixelRatio;
+      canvas.width = props.size * pixelRatio;
+      const scale = (props.size / numCells.value) * pixelRatio;
+      ctx.scale(scale, scale);
+
+      ctx.fillStyle = props.bgColor;
+      ctx.fillRect(0, 0, numCells.value, numCells.value);
+
+      ctx.fillStyle = props.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) {
+        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,
+        );
+      }
+    };
+
+    watchEffect(() => {
+      renderQRCode();
+    });
+
+    onMounted(() => {
+      renderQRCode();
+    });
+
+    return () => {
+      return (
+        <>
+          <canvas ref={canvasRef} role="img" />
+          {imgSrc.value != null ? (
+            <img ref={imageRef} onLoad={renderQRCode} style={{ display: 'none' }} src={imgSrc.value} />
+          ) : null}
+        </>
+      );
+    };
+  },
+});
diff --git a/packages/components/qrcode/components/qrcode-status.tsx b/packages/components/qrcode/components/qrcode-status.tsx
new file mode 100644
index 000000000..561f4b2e0
--- /dev/null
+++ b/packages/components/qrcode/components/qrcode-status.tsx
@@ -0,0 +1,60 @@
+import { computed, defineComponent } from 'vue';
+import { QRCodeStatusProps } from './props';
+import { CheckCircleFilledIcon, RefreshIcon } from 'tdesign-icons-vue-next';
+import Loading from '../../loading';
+import type { StatusRenderInfo } from '../type';
+import { usePrefixClass } from '@tdesign/shared-hooks';
+
+export default defineComponent({
+  name: 'QRCodeStatus',
+  props: QRCodeStatusProps,
+  setup(props) {
+    const classPrefix = usePrefixClass();
+
+    const defaultSpin = <Loading size="32px" />;
+
+    const defaultExpiredNode = computed(() => (
+      <>
+        <p class={`${classPrefix.value}-expired__text`}>{props.locale?.expiredText}</p>
+        {props?.onRefresh && (
+          <p class={`${classPrefix.value}-expired__button`} onClick={props?.onRefresh}>
+            <RefreshIcon size="16" />
+            {props.locale?.refreshText}
+          </p>
+        )}
+      </>
+    ));
+
+    const defaultScannedNode = (
+      <p class={`${classPrefix.value}-scanned`}>
+        <CheckCircleFilledIcon size="16" class={`${classPrefix.value}-scanned__icon`} />
+        {props.locale?.scannedText}
+      </p>
+    );
+
+    const defaultNodes = computed(() => {
+      return {
+        expired: defaultExpiredNode.value,
+        loading: defaultSpin,
+        scanned: defaultScannedNode,
+        active: null as any,
+      };
+    });
+
+    const renderStatus = (info: StatusRenderInfo) => {
+      return defaultNodes.value[info.status];
+    };
+
+    return () => {
+      return (
+        <>
+          {props.statusRender ||
+            renderStatus({
+              status: props.status,
+              onRefresh: props.onRefresh,
+            })}
+        </>
+      );
+    };
+  },
+});
diff --git a/packages/components/qrcode/components/qrcode-svg.tsx b/packages/components/qrcode/components/qrcode-svg.tsx
new file mode 100644
index 000000000..071c25c20
--- /dev/null
+++ b/packages/components/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 '@tdesign/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;
+    });
+
+    const imageNode = computed(() => {
+      if (!props.imageSettings || !calculatedImageSettings.value) return null;
+
+      return (
+        <image
+          href={props.imageSettings.src}
+          height={calculatedImageSettings.value.h}
+          width={calculatedImageSettings.value.w}
+          x={calculatedImageSettings.value.x + margin.value}
+          y={calculatedImageSettings.value.y + margin.value}
+          crossOrigin={calculatedImageSettings.value.crossOrigin}
+        />
+      );
+    });
+
+    return () => {
+      const fgPath = generatePath(cellsToDraw.value, margin.value);
+      return (
+        <svg
+          height={props.size}
+          width={props.size}
+          viewBox={`0 0 ${numCells.value} ${numCells.value}`}
+          role="img"
+          style={props.style}
+        >
+          {!!props.title && <title>{props.title}</title>}
+          <path fill={props.bgColor} d={`M0,0 h${numCells.value}v${numCells.value}H0z`} shape-rendering="crispEdges" />
+          <path fill={props.fgColor} d={fgPath} shape-rendering="crispEdges" />
+          {imageNode.value}
+        </svg>
+      );
+    };
+  },
+});
diff --git a/packages/components/qrcode/components/style/css.js b/packages/components/qrcode/components/style/css.js
new file mode 100644
index 000000000..f388879ac
--- /dev/null
+++ b/packages/components/qrcode/components/style/css.js
@@ -0,0 +1,1 @@
+import '../../style/index.css';
diff --git a/packages/components/qrcode/components/style/index.js b/packages/components/qrcode/components/style/index.js
new file mode 100644
index 000000000..4a2e7d583
--- /dev/null
+++ b/packages/components/qrcode/components/style/index.js
@@ -0,0 +1,1 @@
+import '../../style/index.js';
diff --git a/packages/components/qrcode/components/type.ts b/packages/components/qrcode/components/type.ts
new file mode 100644
index 000000000..56df4d758
--- /dev/null
+++ b/packages/components/qrcode/components/type.ts
@@ -0,0 +1,81 @@
+import { CanvasHTMLAttributes, CSSProperties, SVGAttributes } from 'vue';
+import type { ErrorCorrectionLevel, ImageSettings } from '@tdesign/common-js/qrcode/types';
+import { QRCodeConfig } from '@tdesign/components/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?: CSSProperties;
+  /**
+   * 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 & CanvasHTMLAttributes;
+
+export type QRCodeSVG = QRCodeSubComponent & SVGAttributes;
+
+export interface QRCodeStatus {
+  locale: QRCodeConfig;
+  classPrefix: string;
+  onRefresh?: TdQRCodeProps['onRefresh'];
+  statusRender?: TdQRCodeProps['statusRender'];
+  status: StatusRenderInfo['status'];
+}
diff --git a/packages/components/qrcode/hooks/useQRCode.ts b/packages/components/qrcode/hooks/useQRCode.ts
new file mode 100644
index 000000000..01349558c
--- /dev/null
+++ b/packages/components/qrcode/hooks/useQRCode.ts
@@ -0,0 +1,53 @@
+import { QrCode, QrSegment } from '@tdesign/common-js/qrcode/qrcodegen';
+import type { CrossOrigin, ErrorCorrectionLevel, Excavation, ImageSettings } from '@tdesign/common-js/qrcode/types';
+import { ERROR_LEVEL_MAP, getImageSettings, getMarginSize } from '@tdesign/common-js/qrcode/utils';
+import { computed, ComputedRef } from 'vue';
+
+interface Options {
+  value: string;
+  level: ErrorCorrectionLevel;
+  minVersion: number;
+  includeMargin: boolean;
+  marginSize?: number;
+  imageSettings?: ImageSettings;
+  size: number;
+}
+
+interface QRCodeResult {
+  cells: ComputedRef<boolean[][]>;
+  margin: ComputedRef<number>;
+  numCells: ComputedRef<number>;
+  calculatedImageSettings: ComputedRef<{
+    x: number;
+    y: number;
+    h: number;
+    w: number;
+    excavation: Excavation | null;
+    opacity: number;
+    crossOrigin: CrossOrigin;
+  }>;
+  qrcode: ComputedRef<QrCode>;
+}
+
+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/packages/components/qrcode/index.ts b/packages/components/qrcode/index.ts
new file mode 100644
index 000000000..ef7266207
--- /dev/null
+++ b/packages/components/qrcode/index.ts
@@ -0,0 +1,12 @@
+import _QRCode from './qrcode';
+import { withInstall } from '@tdesign/shared-utils';
+import { TdQRCodeProps } from './type';
+
+import './style';
+
+export type QRCodeProps = TdQRCodeProps;
+export * from './type';
+
+export const QRCode = withInstall(_QRCode, 'TQrcode');
+
+export default QRCode;
diff --git a/packages/components/qrcode/props.ts b/packages/components/qrcode/props.ts
new file mode 100644
index 000000000..2005ede54
--- /dev/null
+++ b/packages/components/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<TdQRCodeProps['iconSize']>,
+    default: 40 as TdQRCodeProps['iconSize'],
+  },
+  /** 二维码纠错等级 */
+  level: {
+    type: String as PropType<TdQRCodeProps['level']>,
+    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<TdQRCodeProps['status']>,
+    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<TdQRCodeProps['statusRender']>,
+  },
+  /** 渲染类型 */
+  type: {
+    type: String as PropType<TdQRCodeProps['type']>,
+    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<TdQRCodeProps['onRefresh']>,
+};
diff --git a/packages/components/qrcode/qrcode.en-US.md b/packages/components/qrcode/qrcode.en-US.md
new file mode 100644
index 000000000..e945e4f77
--- /dev/null
+++ b/packages/components/qrcode/qrcode.en-US.md
@@ -0,0 +1,26 @@
+:: 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 or custom icon 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-next/blob/develop/packages/components/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-next/blob/develop/packages/components/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/qrcode/type.ts) | N
+type | String | canvas | render type。options: canvas/svg | N
+value | String | - | scanned text | N
+onRefresh | Function |  | Typescript：`() => void`<br/>Click the "Click to refresh" callback | N
+
+### QRCode Events
+
+name | params | description
+-- | -- | --
+refresh | \- | Click the "Click to refresh" callback
diff --git a/packages/components/qrcode/qrcode.md b/packages/components/qrcode/qrcode.md
new file mode 100644
index 000000000..2fed2f42a
--- /dev/null
+++ b/packages/components/qrcode/qrcode.md
@@ -0,0 +1,26 @@
+:: 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-next/blob/develop/packages/components/qrcode/type.ts) | N
+statusRender | Slot / Function | - | 自定义状态渲染器。TS 类型：`(info:StatusRenderInfo) => TNode` `type StatusRenderInfo = {status:QRStatus;onRefresh?: () => void;}`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/qrcode/type.ts) | N
+type | String | canvas | 渲染类型。可选项：canvas/svg | N
+value | String | - | 扫描后的文本 | N
+onRefresh | Function |  | TS 类型：`() => void`<br/>点击"点击刷新"的回调 | N
+
+### QRCode Events
+
+名称 | 参数 | 描述
+-- | -- | --
+refresh | \- | 点击"点击刷新"的回调
diff --git a/packages/components/qrcode/qrcode.tsx b/packages/components/qrcode/qrcode.tsx
new file mode 100644
index 000000000..9820dc671
--- /dev/null
+++ b/packages/components/qrcode/qrcode.tsx
@@ -0,0 +1,103 @@
+import { computed, defineComponent } from 'vue';
+import props from './props';
+import { DEFAULT_FRONT_COLOR } from '@tdesign/common-js/qrcode/utils';
+import { usePrefixClass, useConfig, useTNodeJSX, useVariables } from '@tdesign/shared-hooks';
+
+import QRCodeCanvas from './components/qrcode-canvas';
+import QRCodeSVG from './components/qrcode-svg';
+import QRcodeStatus from './components/qrcode-status';
+
+import type { ImageSettings } from '@tdesign/common-js/qrcode/types';
+import { isNumber } from 'lodash-es';
+
+export default defineComponent({
+  name: 'TQRCode',
+  props,
+  setup(props) {
+    const classPrefix = usePrefixClass();
+    const renderTNodeJSX = useTNodeJSX();
+    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<ImageSettings>(() => {
+      return {
+        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(() => {
+      return [
+        `${classPrefix.value}-qrcode`,
+        {
+          [`${classPrefix.value}-borderless`]: props.borderless,
+          [`${classPrefix.value}-qrcode-svg`]: props.type === 'svg',
+        },
+      ];
+    });
+
+    const mergedStyle = computed(() => {
+      return {
+        backgroundColor: finalBgColor.value,
+        width: `${props.size}px`,
+        height: `${props.size}px`,
+      };
+    });
+
+    return () => {
+      const QRCodeProps = {
+        value: props.value,
+        size: props.size,
+        bgColor: finalBgColor.value,
+        fgColor: finalFgColor.value,
+        imageSettings: props.icon ? imageSettings.value : undefined,
+        level: props.level,
+      };
+
+      return (
+        <div class={classes.value} style={mergedStyle.value} {...{ level: props.level }}>
+          {props.status !== 'active' && (
+            <div
+              class={[
+                `${classPrefix.value}-mask`,
+                { [`${classPrefix.value}-${props.status}`]: props.status !== 'loading' },
+              ]}
+            >
+              <QRcodeStatus
+                locale={globalConfig.value}
+                status={props.status}
+                onRefresh={props.onRefresh}
+                statusRender={renderTNodeJSX('statusRender', {
+                  params: { status: props.status, onRefresh: props.onRefresh },
+                })}
+              />
+            </div>
+          )}
+          {props.type === 'canvas' ? (
+            <QRCodeCanvas {...QRCodeProps} size={props.size} />
+          ) : (
+            <QRCodeSVG {...QRCodeProps} size={props.size} />
+          )}
+        </div>
+      );
+    };
+  },
+});
diff --git a/packages/components/qrcode/style/css.js b/packages/components/qrcode/style/css.js
new file mode 100644
index 000000000..6a9a4b132
--- /dev/null
+++ b/packages/components/qrcode/style/css.js
@@ -0,0 +1,1 @@
+import './index.css';
diff --git a/packages/components/qrcode/style/index.js b/packages/components/qrcode/style/index.js
new file mode 100644
index 000000000..c11526a73
--- /dev/null
+++ b/packages/components/qrcode/style/index.js
@@ -0,0 +1,1 @@
+import '@tdesign/common-style/web/components/qrcode/_index.less';
diff --git a/packages/components/qrcode/type.ts b/packages/components/qrcode/type.ts
new file mode 100644
index 000000000..2bfad5e9e
--- /dev/null
+++ b/packages/components/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?: TNode<StatusRenderInfo>;
+  /**
+   * 渲染类型
+   * @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/packages/components/table/hooks/useFixed.ts b/packages/components/table/hooks/useFixed.ts
index 266853cf5..ab1fc47cd 100644
--- a/packages/components/table/hooks/useFixed.ts
+++ b/packages/components/table/hooks/useFixed.ts
@@ -518,7 +518,7 @@ export default function useFixed(
         reduceWidth += thWidthList[key];
       });
       const rootThWidthList = pick(thWidthList, preColKeys);
-      const oldTotalWidth = Object.values(rootThWidthList).reduce((r = 0, n) => r + n);
+      const oldTotalWidth = Object.values(rootThWidthList).reduce((r = 0, n) => r + n, 0);
       // 保留原有可能编辑过的列宽度，但是当剩余列过小时，表头小于内容宽，需要缩放回内容宽度
       // 使用不包含滚动条的可视化区域宽度，意味着当不再溢出的时候，将宽度设置回完整宽度
       const contentWidth = tableContentRef.value.clientWidth;
diff --git a/packages/shared/hooks/index.ts b/packages/shared/hooks/index.ts
index da8b12aba..f1c9f76d1 100644
--- a/packages/shared/hooks/index.ts
+++ b/packages/shared/hooks/index.ts
@@ -22,3 +22,5 @@ export * from './useTeleport';
 export * from './useVModel';
 export * from './useVirtualScroll';
 export * from './useVirtualScrollNew';
+export * from './useMutationObservable';
+export * from './useVariables';
diff --git a/packages/shared/hooks/useMutationObservable/index.md b/packages/shared/hooks/useMutationObservable/index.md
new file mode 100644
index 000000000..5f5acb401
--- /dev/null
+++ b/packages/shared/hooks/useMutationObservable/index.md
@@ -0,0 +1,1 @@
+useMutationObservable
\ No newline at end of file
diff --git a/packages/shared/hooks/useMutationObservable/index.ts b/packages/shared/hooks/useMutationObservable/index.ts
new file mode 100644
index 000000000..84a64be55
--- /dev/null
+++ b/packages/shared/hooks/useMutationObservable/index.ts
@@ -0,0 +1,65 @@
+import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import { debounce, isEqual } from 'lodash-es';
+
+export interface MutationCallback {
+  (mutations: MutationRecord[]): void;
+}
+
+interface Options {
+  debounceTime?: number;
+  config?: MutationObserverInit;
+}
+
+const DEFAULT_OPTIONS: Options = {
+  debounceTime: 0,
+  config: {
+    attributes: true,
+    childList: true,
+    characterData: true,
+    subtree: true,
+  },
+};
+
+/**
+ * useMutationObservable
+ * @param targetEl 监听对象
+ * @param callback 回调方法
+ * @param options 配置项
+ */
+export function useMutationObservable(
+  targetEl: HTMLElement | null,
+  callback: MutationCallback,
+  options: Options = DEFAULT_OPTIONS,
+) {
+  const mergedOptions = ref<Options>({ ...DEFAULT_OPTIONS, ...options });
+  let observer: MutationObserver | null = null;
+
+  const initObserver = () => {
+    if (!targetEl) return;
+
+    const { debounceTime = 0, config = DEFAULT_OPTIONS.config } = mergedOptions.value;
+    const handler: MutationCallback = (...args) => {
+      callback?.(...args);
+    };
+
+    observer?.disconnect();
+    observer = new MutationObserver(debounceTime > 0 ? debounce(handler, debounceTime) : handler);
+    observer.observe(targetEl, config);
+  };
+
+  watch(
+    () => options,
+    (newOptions) => {
+      if (!isEqual(newOptions, mergedOptions.value)) {
+        mergedOptions.value = { ...DEFAULT_OPTIONS, ...newOptions };
+        initObserver();
+      }
+    },
+    { deep: true },
+  );
+
+  onMounted(initObserver);
+  onBeforeUnmount(() => observer?.disconnect());
+}
+
+export default useMutationObservable;
diff --git a/packages/shared/hooks/useVariables/index.md b/packages/shared/hooks/useVariables/index.md
new file mode 100644
index 000000000..50c9b27ab
--- /dev/null
+++ b/packages/shared/hooks/useVariables/index.md
@@ -0,0 +1,1 @@
+useVariables
\ No newline at end of file
diff --git a/packages/shared/hooks/useVariables/index.ts b/packages/shared/hooks/useVariables/index.ts
new file mode 100644
index 000000000..5d6bc06c9
--- /dev/null
+++ b/packages/shared/hooks/useVariables/index.ts
@@ -0,0 +1,50 @@
+import { ref } from 'vue';
+import type { Ref } from 'vue';
+import { isString } from 'lodash-es';
+import { THEME_MODE } from '@tdesign/common-js/common';
+import { getColorTokenColor } from '@tdesign/shared-utils/dom';
+import useMutationObservable from '../useMutationObservable';
+
+/**
+ * useVariables
+ * @param variable CSS 变量名
+ * @example
+ *   const { textColor, brandColor } = useVariables({
+ *      textColor: '--td-color-primary',
+ *      brandColor: '--td-brand-color',
+ *   });
+ */
+export function useVariables<T extends Record<string, string>>(variables: T): Record<keyof T, Ref<string>> {
+  const values = {} as Record<keyof T, Ref<string>>;
+  let varsArray: string[] = [];
+
+  varsArray = Object.values(variables);
+  Object.entries(variables).forEach(([key, varName]) => {
+    values[key as keyof T] = ref(getColorTokenColor(varName));
+  });
+
+  varsArray.forEach((varName) => {
+    values[varName as keyof T] = ref(getColorTokenColor(varName));
+  });
+
+  const targetElement = document?.documentElement;
+  useMutationObservable(targetElement, (mutationsList) => {
+    mutationsList.some((mutation) => {
+      if (mutation.type === 'attributes' && mutation.attributeName === THEME_MODE) {
+        if (isString(variables) || Array.isArray(variables)) {
+          varsArray.forEach((varName) => {
+            values[varName].value = getColorTokenColor(varName);
+          });
+        } else {
+          Object.entries(variables).forEach(([key, varName]) => {
+            values[key].value = getColorTokenColor(varName);
+          });
+        }
+        return true;
+      }
+      return false;
+    });
+  });
+
+  return values;
+}
diff --git a/packages/shared/utils/dom.ts b/packages/shared/utils/dom.ts
index 1f2cc8fbe..38635679d 100644
--- a/packages/shared/utils/dom.ts
+++ b/packages/shared/utils/dom.ts
@@ -384,3 +384,14 @@ export function getWindowSize(): { width: number; height: number } {
 export const isCommentVNode = (node: unknown): node is VNode => {
   return isVNode(node) && node.type === Comment;
 };
+
+/**
+ * 获取颜色token的色值
+ * @example getColorTokenColor('--td-brand-color')
+ * @returns string
+ */
+export const getColorTokenColor = (token: string): string => {
+  const targetElement = document?.documentElement;
+  const styles = getComputedStyle(targetElement);
+  return styles.getPropertyValue(token).trim() ?? '';
+};
diff --git a/packages/tdesign-vue-next/.changelog/pr-5670.md b/packages/tdesign-vue-next/.changelog/pr-5670.md
new file mode 100644
index 000000000..4512abb22
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5670.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5670
+contributor: baozjj
+---
+
+- feat(Pagination): 优化 `PaginationMini`  的显示提示文案展示 @baozjj ([#5670](https://github.com/Tencent/tdesign-vue-next/pull/5670))
diff --git a/packages/tdesign-vue-next/.changelog/pr-5719.md b/packages/tdesign-vue-next/.changelog/pr-5719.md
new file mode 100644
index 000000000..4dbbea1bc
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5719.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5719
+contributor: Wesley-0808
+---
+
+- feat(QRCode): 新增 `QRCode` 二维码组件 @Wesley-0808 ([#5719](https://github.com/Tencent/tdesign-vue-next/pull/5719))
diff --git a/packages/tdesign-vue-next/site/src/router/site.config.ts b/packages/tdesign-vue-next/site/src/router/site.config.ts
index e8160e0e6..24baf29c4 100644
--- a/packages/tdesign-vue-next/site/src/router/site.config.ts
+++ b/packages/tdesign-vue-next/site/src/router/site.config.ts
@@ -538,6 +538,14 @@ export const docs = [
         component: () => import('@tdesign/components/progress/progress.md'),
         componentEn: () => import('@tdesign/components/progress/progress.en-US.md'),
       },
+      {
+        title: 'QRCode 二维码',
+        titleEn: 'QRCode',
+        name: 'qrcode',
+        path: '/vue-next/components/qrcode',
+        component: () => import('@tdesign/components/qrcode/qrcode.md'),
+        componentEn: () => import('@tdesign/components/qrcode/qrcode.en-US.md'),
+      },
       {
         title: 'Skeleton 骨架屏',
         titleEn: 'Skeleton',


Test patch:
diff --git a/packages/components/pagination/__tests__/pagination.test.tsx b/packages/components/pagination/__tests__/pagination.test.tsx
index ccbe7592c..9282cb30e 100644
--- a/packages/components/pagination/__tests__/pagination.test.tsx
+++ b/packages/components/pagination/__tests__/pagination.test.tsx
@@ -2,6 +2,7 @@
 import { mount } from '@vue/test-utils';
 import { vi } from 'vitest';
 import { Select, Pagination } from '@tdesign/components';
+import TPaginationMini from '../pagination-mini';
 
 const defaultPaginationProps = {
   total: 100,
@@ -371,3 +372,44 @@ describe('Pagination', () => {
     });
   });
 });
+
+describe('TPaginationMini with Tooltip', () => {
+  it('should render Tooltip with correct content when tips enabled', () => {
+    const wrapper = mount(TPaginationMini, {
+      props: {
+        tips: { prev: '上一页', current: '当前页', next: '下一页' },
+        showCurrent: true,
+      },
+    });
+
+    const tooltips = wrapper.findAllComponents({ name: 'TTooltip' });
+    expect(tooltips).toHaveLength(3);
+    expect(tooltips[0].props('content')).toBe('上一页');
+    expect(tooltips[1].props('content')).toBe('当前页');
+    expect(tooltips[2].props('content')).toBe('下一页');
+  });
+
+  it('should not render Tooltip when tips is false', () => {
+    const wrapper = mount(TPaginationMini, {
+      props: {
+        tips: false,
+        showCurrent: true,
+      },
+    });
+
+    const tooltips = wrapper.findAllComponents({ name: 'TTooltip' });
+    expect(tooltips.length).toBe(0);
+  });
+
+  it('should pass showArrow=false to Tooltip', () => {
+    const wrapper = mount(TPaginationMini, {
+      props: {
+        tips: { prev: '上一页' },
+      },
+    });
+
+    const tooltip = wrapper.findComponent({ name: 'TTooltip' });
+    expect(tooltip.exists()).toBe(true);
+    expect(tooltip.props('showArrow')).toBe(false);
+  });
+});
diff --git a/packages/components/qrcode/__tests__/qrcode.test.tsx b/packages/components/qrcode/__tests__/qrcode.test.tsx
new file mode 100644
index 000000000..c13bca9c1
--- /dev/null
+++ b/packages/components/qrcode/__tests__/qrcode.test.tsx
@@ -0,0 +1,96 @@
+import { mount } from '@vue/test-utils';
+import { QRCode, TdQRCodeProps } from '@tdesign/components';
+import type { VueWrapper } from '@vue/test-utils';
+
+describe('QRCode', () => {
+  describe('props', () => {
+    let wrapper: VueWrapper<InstanceType<typeof QRCode>> | null = null;
+    // 因单测环境下无法获取主题色，若未定义颜色，组件将由默认颜色兜底。所以单测的颜色要为默认颜色。
+    // 颜色优先级如下：
+    // bgColor：自定义颜色 > 主题色适配 > 透明[transparent]
+    // color[fgColor]：自定义颜色 > 主题色适配 > 默认颜色[#000000]
+    const defaultBgColor = 'transparent'; // 实际使用时为 rgb(255, 255, 255)
+    const defaultFgColor = '#000000'; // 实际使用时为 rgba(0, 0, 0, .9)
+    const defaultSize = 160;
+    beforeEach(() => {
+      wrapper = mount(<QRCode value="https://tdesign.tencent.com/"></QRCode>) as VueWrapper<
+        InstanceType<typeof QRCode>
+      >;
+    });
+
+    it(':bgColor[string]', async () => {
+      expect(wrapper.find('.t-qrcode').attributes('style')).eq(
+        `background-color: ${defaultBgColor}; width: 160px; height: 160px;`,
+      );
+      const bgColor = 'rgb(7, 193, 96)';
+      await wrapper.setProps({ bgColor: bgColor });
+      expect(wrapper.find('.t-qrcode').attributes('style')).eq(
+        `background-color: ${bgColor}; width: 160px; height: 160px;`,
+      );
+    });
+
+    it(':borderless[boolean]', async () => {
+      expect(wrapper.find('.t-qrcode').classes('t-borderless')).eq(false);
+      await wrapper.setProps({ borderless: true });
+      expect(wrapper.find('.t-qrcode').classes('t-borderless')).eq(true);
+    });
+
+    // color只能测试svg模式下
+    it(':color[string]', async () => {
+      await wrapper.setProps({ type: 'svg' });
+      // [0] 是背景
+      expect(wrapper.find('.t-qrcode').findAll('path')[1].attributes('fill')).eq(defaultFgColor);
+      const color = 'rgb(7, 193, 96)';
+      await wrapper.setProps({ color });
+      expect(wrapper.find('.t-qrcode').findAll('path')[1].attributes('fill')).eq(color);
+    });
+
+    it(':icon[string]-canvas', async () => {
+      const iconSrc = 'https://tdesign.gtimg.com/site/tdesign-logo.png';
+      await wrapper.setProps({ icon: iconSrc });
+      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';
+      await wrapper.setProps({ icon: iconSrc, type: 'svg' });
+      expect(wrapper.find('.t-qrcode').find('image').attributes('href')).eq(iconSrc);
+    });
+
+    it(':iconSize[number|object]-svg', async () => {});
+
+    const level: TdQRCodeProps['level'][] = ['L', 'M', 'Q', 'H'];
+    level.forEach((item) => {
+      it(`:level[string]-[${item}]`, async () => {
+        await wrapper.setProps({ level: item });
+        expect(wrapper.find('.t-qrcode').attributes('level')).eq(item);
+      });
+    });
+
+    it(':size[number]', async () => {
+      expect(wrapper.find('.t-qrcode').attributes('style')).eq(
+        `background-color: ${defaultBgColor}; width: ${defaultSize}px; height: ${defaultSize}px;`,
+      );
+      const size = 380;
+      await wrapper.setProps({ size });
+      expect(wrapper.find('.t-qrcode').attributes('style')).eq(
+        `background-color: ${defaultBgColor}; width: ${size}px; height: ${size}px;`,
+      );
+    });
+
+    const status: TdQRCodeProps['status'][] = ['expired', 'loading', 'scanned'];
+    status.forEach((item) => {
+      it(`:status[string]-[${item}]`, async () => {
+        await wrapper.setProps({ status: item });
+        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();
+      await wrapper.setProps({ status: 'expired', statusRender });
+      expect(statusRender).toBeCalled();
+    });
+  });
+});
diff --git a/packages/tdesign-vue-next/site/configs/test-coverage.ts b/packages/tdesign-vue-next/site/configs/test-coverage.ts
index fde8bb02a..756468dab 100644
--- a/packages/tdesign-vue-next/site/configs/test-coverage.ts
+++ b/packages/tdesign-vue-next/site/configs/test-coverage.ts
@@ -6,10 +6,10 @@ export default {
     lines: '86.16%',
   },
   alert: {
-    statements: '95.58%',
-    branches: '90.47%',
+    statements: '94.81%',
+    branches: '83.33%',
     functions: '90.9%',
-    lines: '95.58%',
+    lines: '94.81%',
   },
   anchor: {
     statements: '84.9%',
@@ -120,10 +120,10 @@ export default {
     lines: '100%',
   },
   dialog: {
-    statements: '78.12%',
+    statements: '76.37%',
     branches: '87.73%',
     functions: '65%',
-    lines: '78.12%',
+    lines: '76.37%',
   },
   divider: {
     statements: '95.38%',
@@ -132,10 +132,10 @@ export default {
     lines: '95.38%',
   },
   drawer: {
-    statements: '65.91%',
+    statements: '62.42%',
     branches: '79.41%',
     functions: '75%',
-    lines: '65.91%',
+    lines: '62.42%',
   },
   dropdown: {
     statements: '51.25%',
@@ -150,10 +150,10 @@ export default {
     lines: '96.12%',
   },
   form: {
-    statements: '96.81%',
-    branches: '92.76%',
+    statements: '96.8%',
+    branches: '93.16%',
     functions: '97.29%',
-    lines: '96.81%',
+    lines: '96.8%',
   },
   grid: {
     statements: '100%',
@@ -222,10 +222,10 @@ export default {
     lines: '96.44%',
   },
   loading: {
-    statements: '61.73%',
+    statements: '59.44%',
     branches: '94.59%',
     functions: '30.76%',
-    lines: '61.73%',
+    lines: '59.44%',
   },
   menu: {
     statements: '61.26%',
@@ -234,16 +234,16 @@ export default {
     lines: '61.26%',
   },
   message: {
-    statements: '61.67%',
-    branches: '76%',
-    functions: '35%',
-    lines: '61.67%',
+    statements: '90.44%',
+    branches: '82.45%',
+    functions: '62.06%',
+    lines: '90.44%',
   },
   notification: {
-    statements: '90.41%',
-    branches: '88.46%',
+    statements: '89.7%',
+    branches: '85.18%',
     functions: '78.57%',
-    lines: '90.41%',
+    lines: '89.7%',
   },
   pagination: {
     statements: '83.11%',
@@ -258,10 +258,10 @@ export default {
     lines: '97.1%',
   },
   popup: {
-    statements: '83.85%',
-    branches: '84.52%',
-    functions: '81.25%',
-    lines: '83.85%',
+    statements: '83.69%',
+    branches: '83.9%',
+    functions: '82%',
+    lines: '83.69%',
   },
   progress: {
     statements: '92.71%',
@@ -269,6 +269,12 @@ export default {
     functions: '80%',
     lines: '92.71%',
   },
+  qrcode: {
+    statements: '98.61%',
+    branches: '76%',
+    functions: '100%',
+    lines: '98.61%',
+  },
   radio: {
     statements: '95.34%',
     branches: '89.41%',
@@ -288,10 +294,10 @@ export default {
     lines: '90.97%',
   },
   select: {
-    statements: '83.26%',
-    branches: '69.09%',
-    functions: '63.15%',
-    lines: '83.26%',
+    statements: '86%',
+    branches: '69.1%',
+    functions: '65%',
+    lines: '86%',
   },
   selectInput: {
     statements: '98.29%',
@@ -336,10 +342,10 @@ export default {
     lines: '37.87%',
   },
   swiper: {
-    statements: '82.54%',
-    branches: '69.79%',
+    statements: '83.62%',
+    branches: '71.27%',
     functions: '67.85%',
-    lines: '82.54%',
+    lines: '83.62%',
   },
   switch: {
     statements: '97.07%',
@@ -372,16 +378,16 @@ export default {
     lines: '97.48%',
   },
   textarea: {
-    statements: '89.52%',
+    statements: '89.49%',
     branches: '66.66%',
     functions: '76.47%',
-    lines: '89.52%',
+    lines: '89.49%',
   },
   timePicker: {
-    statements: '61.09%',
+    statements: '60.46%',
     branches: '57.14%',
     functions: '44.44%',
-    lines: '61.09%',
+    lines: '60.46%',
   },
   timeline: {
     statements: '98.13%',
diff --git a/packages/tdesign-vue-next/test/scripts/generate-coverage.ts b/packages/tdesign-vue-next/test/scripts/generate-coverage.ts
index 02dfedd5e..c38b58dfb 100644
--- a/packages/tdesign-vue-next/test/scripts/generate-coverage.ts
+++ b/packages/tdesign-vue-next/test/scripts/generate-coverage.ts
@@ -1,7 +1,8 @@
 import fs from 'fs';
 import { camelCase } from 'lodash-es';
 import { parseFromString } from 'dom-parser';
-import { joinTdesignVueNextRoot } from '@tdesign/internal-utils';
+import { joinTdesignVueNextRoot, getWorkspaceRoot } from '@tdesign/internal-utils';
+import { execSync } from 'child_process';
 
 fs.readFile(joinTdesignVueNextRoot('test/coverage/index.html'), 'utf8', (err, html) => {
   if (err) {
@@ -42,7 +43,9 @@ fs.readFile(joinTdesignVueNextRoot('test/coverage/index.html'), 'utf8', (err, ht
 
     const finalRes = `export default ${JSON.stringify(resultCoverage, null, 2)};\n`;
     fs.writeFileSync(joinTdesignVueNextRoot('site/configs/test-coverage.ts'), finalRes);
-
+    const prettierConfig = getWorkspaceRoot() + '/.prettierrc.js';
+    const formatFile = joinTdesignVueNextRoot('site/configs/test-coverage.ts');
+    execSync(`pnpm exec prettier --config ${prettierConfig} --write ${formatFile}`);
     // eslint-disable-next-line
     console.log('successful re-generate coverage');
   }