Repository: Tencent/tdesign-vue-next
Pull request #5510 (2025-05-28 05:54:34)

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


tdesign-bot: # TDesign Component Site Preview [Open](https://github.com/Tencent/tdesign-vue-next/actions/runs/15292615971)
  | 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-5510-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-5510-tdesign-vue-next-chat.surge.sh) | 

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

Patch:
diff --git a/packages/common b/packages/common
index 8cf20889c..d44143875 160000
--- a/packages/common
+++ b/packages/common
@@ -1,1 +1,1 @@
-Subproject commit 8cf20889c0d2ff21dc64db0523c747a432351f4a
+Subproject commit d44143875fd3e081efe0f42d1d3bfe8168db9bda
diff --git a/packages/components/config-provider/config-provider.en-US.md b/packages/components/config-provider/config-provider.en-US.md
index 2821eb5a2..5ee843133 100644
--- a/packages/components/config-provider/config-provider.en-US.md
+++ b/packages/components/config-provider/config-provider.en-US.md
@@ -224,6 +224,7 @@ name | type | default | description | required
 colonText | String | - | colon on the right of label ":" | N
 errorMessage | Object | - | Typescript：`FormErrorMessage`，[Form API Documents](./form?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/config-provider/type.ts) | N
 requiredMark | Boolean | true | \- | N
+requiredMarkPosition | String | left | Display position of required symbols。options: left/right | N
 
 ### GuideConfig
 
diff --git a/packages/components/config-provider/config-provider.md b/packages/components/config-provider/config-provider.md
index 5b2b02e29..099863e74 100644
--- a/packages/components/config-provider/config-provider.md
+++ b/packages/components/config-provider/config-provider.md
@@ -225,6 +225,7 @@ titleText | Object | - | 空状态组件各类型的标题文本配置。TS 类
 colonText | String | - | 字段旁边的冒号，中文为“：” | N
 errorMessage | Object | - | 表单错误信息配置，示例：`{ idcard: '请输入正确的身份证号码', max: '字符长度不能超过 ${max}' }`。TS 类型：`FormErrorMessage`，[Form API Documents](./form?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/config-provider/type.ts) | N
 requiredMark | Boolean | true | 是否显示必填符号（*），默认显示 | N
+requiredMarkPosition | String | left | 表单必填符号（*）显示位置。可选项：left/right | N
 
 ### GuideConfig
 
diff --git a/packages/components/config-provider/type.ts b/packages/components/config-provider/type.ts
index 02c64c198..3fe8b9bd5 100644
--- a/packages/components/config-provider/type.ts
+++ b/packages/components/config-provider/type.ts
@@ -588,6 +588,11 @@ export interface FormConfig {
    * @default true
    */
   requiredMark?: boolean;
+  /**
+   * 表单必填符号（*）显示位置
+   * @default left
+   */
+  requiredMarkPosition?: 'left' | 'right';
 }
 
 export interface GuideConfig {
diff --git a/packages/components/form/form-item.tsx b/packages/components/form/form-item.tsx
index 68359d2ab..663a61d44 100644
--- a/packages/components/form/form-item.tsx
+++ b/packages/components/form/form-item.tsx
@@ -86,7 +86,7 @@ export default defineComponent({
     });
 
     const requiredMarkPosition = computed(() => {
-      return form?.requiredMarkPosition ?? 'left';
+      return form?.requiredMarkPosition ?? globalConfig.value.requiredMarkPosition;
     });
 
     const hasLabel = computed(() => slots.label || props.label);
diff --git a/packages/components/form/props.ts b/packages/components/form/props.ts
index ac854f590..0e44a0d9a 100644
--- a/packages/components/form/props.ts
+++ b/packages/components/form/props.ts
@@ -74,7 +74,6 @@ export default {
   /** 表单必填符号（*）显示位置 */
   requiredMarkPosition: {
     type: String as PropType<TdFormProps['requiredMarkPosition']>,
-    default: 'left' as TdFormProps['requiredMarkPosition'],
     validator(val: TdFormProps['requiredMarkPosition']): boolean {
       if (!val) return true;
       return ['left', 'right'].includes(val);
diff --git a/packages/components/form/type.ts b/packages/components/form/type.ts
index 30a951631..38527a83b 100644
--- a/packages/components/form/type.ts
+++ b/packages/components/form/type.ts
@@ -65,7 +65,6 @@ export interface TdFormProps<FormData extends Data = Data> {
   requiredMark?: boolean;
   /**
    * 表单必填符号（*）显示位置
-   * @default left
    */
   requiredMarkPosition?: 'left' | 'right';
   /**
diff --git a/packages/components/message/plugin.tsx b/packages/components/message/plugin.tsx
index 433e498b3..e2c4035c6 100644
--- a/packages/components/message/plugin.tsx
+++ b/packages/components/message/plugin.tsx
@@ -66,7 +66,7 @@ const MessageFunction = (props: MessageOptions): Promise<MessageInstance> => {
   }
   const p = instanceMap.get(attachDom)[placement];
   let mgKey: number;
-  if (!p) {
+  if (!p || !attachDom.contains(p.$el)) {
     const wrapper = document.createElement('div');
 
     const instance = createApp(MessageList, {
diff --git a/packages/components/progress/progress.tsx b/packages/components/progress/progress.tsx
index f311ce156..d2c5755cb 100644
--- a/packages/components/progress/progress.tsx
+++ b/packages/components/progress/progress.tsx
@@ -1,4 +1,4 @@
-import { defineComponent, VNode, computed, CSSProperties } from 'vue';
+import { defineComponent, VNode, computed, CSSProperties, ref, nextTick } from 'vue';
 import {
   CloseCircleFilledIcon as TdCloseCircleFilledIcon,
   CheckCircleFilledIcon as TdCheckCircleFilledIcon,
@@ -11,7 +11,8 @@ import {
 import { getBackgroundColor } from '@tdesign/common-js/utils/helper';
 import { PRO_THEME, CIRCLE_SIZE, CIRCLE_SIZE_PX, STATUS_ICON, CIRCLE_FONT_SIZE_RATIO } from './consts';
 import props from './props';
-import { useTNodeJSX, useGlobalIcon, usePrefixClass } from '@tdesign/hooks';
+
+import { useTNodeJSX, useGlobalIcon, usePrefixClass, useResizeObserver } from '@tdesign/hooks';
 
 import { isObject, isString } from 'lodash-es';
 
@@ -31,6 +32,17 @@ export default defineComponent({
         ErrorIcon: TdErrorIcon,
       });
 
+    const CIRCLE_ICONS_MAP = {
+      success: CheckIcon,
+      warning: ErrorIcon,
+      error: CloseIcon,
+    };
+    const NORMAL_ICONS_MAP = {
+      success: CheckCircleFilledIcon,
+      warning: ErrorCircleFilledIcon,
+      error: CloseCircleFilledIcon,
+    };
+
     const statusStyle = computed(() => {
       if (!props.status && props.percentage >= 100) {
         return 'success';
@@ -74,26 +86,27 @@ export default defineComponent({
 
     // theme=circle 获取直径
     const diameter = computed(() => {
-      let diameter = CIRCLE_SIZE_PX.MEDIUM;
+      let diameterValue = CIRCLE_SIZE_PX.MEDIUM;
       if (!props.size) {
-        return diameter;
+        return diameterValue;
       }
       const { SMALL, LARGE, MEDIUM } = CIRCLE_SIZE;
       switch (props.size) {
         case SMALL:
-          diameter = CIRCLE_SIZE_PX.SMALL;
+          diameterValue = CIRCLE_SIZE_PX.SMALL;
           break;
         case MEDIUM:
-          diameter = CIRCLE_SIZE_PX.MEDIUM;
+          diameterValue = CIRCLE_SIZE_PX.MEDIUM;
           break;
         case LARGE:
-          diameter = CIRCLE_SIZE_PX.LARGE;
+          diameterValue = CIRCLE_SIZE_PX.LARGE;
           break;
         default:
-          diameter = Number(props.size);
+          const customSize = Number(props.size);
+          diameterValue = !isNaN(customSize) && customSize > 0 ? customSize : CIRCLE_SIZE_PX.MEDIUM;
           break;
       }
-      return diameter;
+      return diameterValue;
     });
 
     const rPoints = computed(() => {
@@ -135,40 +148,47 @@ export default defineComponent({
       return `${perimeter * percent}  ${perimeter * (1 - percent)}`;
     });
 
-    const getIconMap = () => {
-      const CIRCLE_ICONS = {
-        success: CheckIcon,
-        warning: ErrorIcon,
-        error: CloseIcon,
-      };
-      const NORMAL_ICONS = {
-        success: CheckCircleFilledIcon,
-        warning: ErrorCircleFilledIcon,
-        error: CloseCircleFilledIcon,
-      };
-      return props.theme === PRO_THEME.CIRCLE ? CIRCLE_ICONS : NORMAL_ICONS;
-    };
+    const getIconMap = computed(() => {
+      return props.theme === PRO_THEME.CIRCLE ? CIRCLE_ICONS_MAP : NORMAL_ICONS_MAP;
+    });
+
     const getLabelContent = () => {
-      let labelContent: string | VNode = `${props.percentage}%`;
+      let labelContentRender: string | VNode = `${props.percentage}%`;
       const status = props.status || '';
       if (STATUS_ICON.includes(status) && props.theme !== PRO_THEME.PLUMP) {
-        const components = getIconMap();
-        const component = components[status as keyof typeof components];
+        const component = getIconMap.value[status as keyof typeof CIRCLE_ICONS_MAP];
         if (component) {
-          labelContent = <component class={[`${COMPONENT_NAME.value}__icon`]}></component>;
+          labelContentRender = <component class={[`${COMPONENT_NAME.value}__icon`]}></component>;
         }
       }
-      return labelContent;
+      return labelContentRender;
     };
 
+    // 当文字小于进度条宽度时，文字在进度条外部，否则在内部
+    const infoIsOut = ref(true);
+    const infoRef = ref<HTMLDivElement>(null);
+
+    const separateClasses = computed(() => {
+      return infoIsOut.value ? `${COMPONENT_NAME.value}--over-ten` : `${COMPONENT_NAME.value}--under-ten`;
+    });
+
+    async function updateInfoIsOut() {
+      if (props.theme === PRO_THEME.PLUMP) {
+        if (!infoRef.value || props.label === false) return;
+        await nextTick();
+        const infoEl =
+          infoRef.value.querySelector(`.${COMPONENT_NAME.value}__info`) || infoRef.value.nextElementSibling;
+        infoIsOut.value = infoRef.value.clientWidth > infoEl?.clientWidth + 10;
+      }
+    }
+
+    useResizeObserver(infoRef, updateInfoIsOut);
+
     return () => {
       const labelContent = (
         <div class={`${COMPONENT_NAME.value}__info`}>{renderTNodeJSX('label', getLabelContent())}</div>
       );
-      // 进度大于 10 ，进度百分比显示在内部；进度百分比小于 10 进度显示在外部
-      const PLUMP_SEPARATE = 10;
-      const separateClasses =
-        props.percentage > PLUMP_SEPARATE ? `${COMPONENT_NAME.value}--over-ten` : `${COMPONENT_NAME.value}--under-ten`;
+
       return (
         <div class={COMPONENT_NAME.value}>
           {props.theme === PRO_THEME.LINE && (
@@ -183,15 +203,15 @@ export default defineComponent({
           {props.theme === PRO_THEME.PLUMP && (
             <div
               class={[
-                `${COMPONENT_NAME.value}__bar ${COMPONENT_NAME.value}--plump ${separateClasses}`,
+                `${COMPONENT_NAME.value}__bar ${COMPONENT_NAME.value}--plump ${separateClasses.value}`,
                 { [`${COMPONENT_NAME.value}--status--${statusStyle.value}`]: statusStyle.value },
               ]}
               style={trackBgStyle.value}
             >
-              <div class={`${COMPONENT_NAME.value}__inner`} style={barStyle.value}>
-                {props.percentage > PLUMP_SEPARATE && labelContent}
+              <div class={`${COMPONENT_NAME.value}__inner`} ref={infoRef} style={barStyle.value}>
+                {infoIsOut.value && labelContent}
               </div>
-              {props.percentage <= PLUMP_SEPARATE && labelContent}
+              {!infoIsOut.value && labelContent}
             </div>
           )}
 
diff --git a/packages/pro-components/chat/_example/base.vue b/packages/pro-components/chat/_example/base.vue
index 98ee58c90..2568110be 100644
--- a/packages/pro-components/chat/_example/base.vue
+++ b/packages/pro-components/chat/_example/base.vue
@@ -2,7 +2,7 @@
   <div class="chat-box">
     <t-chat
       ref="chatRef"
-      clear-history
+      :clear-history="chatList.length > 0 && !isStreamLoad"
       :data="chatList"
       :text-loading="loading"
       :is-stream-load="isStreamLoad"
@@ -14,7 +14,7 @@
       <template #content="{ item, index }">
         <t-chat-reasoning v-if="item.reasoning?.length > 0" expand-icon-placement="right">
           <template #header>
-            <t-chat-loading v-if="isStreamLoad" text="思考中..." />
+            <t-chat-loading v-if="isStreamLoad && item.content.length === 0" text="思考中..." />
             <div v-else style="display: flex; align-items: center">
               <CheckCircleIcon style="color: var(--td-success-color-5); font-size: 20px; margin-right: 8px" />
               <span>已深度思考</span>
diff --git a/packages/pro-components/chat/_example/chat-sender-slot.vue b/packages/pro-components/chat/_example/chat-sender-slot.vue
index c7fea7016..56d2386df 100644
--- a/packages/pro-components/chat/_example/chat-sender-slot.vue
+++ b/packages/pro-components/chat/_example/chat-sender-slot.vue
@@ -1,6 +1,7 @@
 <template>
   <t-chat-sender
     ref="chatSenderRef"
+    v-model="inputValue"
     class="chat-sender"
     :stop-disabled="loading"
     :textarea-props="{
@@ -9,7 +10,8 @@
     @send="inputEnter"
   >
     <template #suffix>
-      <t-button theme="default" variant="text" size="large" class="btn"> 发送 </t-button>
+      <!-- 监听键盘回车发送事件需要在sender组件监听 -->
+      <t-button theme="default" variant="text" size="large" class="btn" @click="inputEnter"> 发送 </t-button>
     </template>
     <template #prefix>
       <div class="model-select">
@@ -35,6 +37,7 @@ import { SystemSumIcon } from 'tdesign-icons-vue-next';
 const loading = ref(false);
 const allowToolTip = ref(false);
 const chatSenderRef = ref(null);
+const inputValue = ref('');
 const selectOptions = [
   {
     label: '默认模型',
@@ -58,11 +61,12 @@ const checkClick = () => {
   isChecked.value = !isChecked.value;
 };
 // 模拟消息发送
-const inputEnter = function (inputValue: string) {
+const inputEnter = function () {
   if (loading.value) {
     return;
   }
-  if (!inputValue) return;
+  if (!inputValue.value) return;
+  inputValue.value = '';
   loading.value = true;
   setTimeout(() => {
     loading.value = false;
diff --git a/packages/pro-components/chat/_example/chat-sender.vue b/packages/pro-components/chat/_example/chat-sender.vue
index 97a419411..79ae8e734 100644
--- a/packages/pro-components/chat/_example/chat-sender.vue
+++ b/packages/pro-components/chat/_example/chat-sender.vue
@@ -27,11 +27,12 @@ import { ref } from 'vue';
 const query = ref('');
 const loading = ref(false);
 // 模拟消息发送
-const inputEnter = function (inputValue: string) {
+const inputEnter = function () {
   if (loading.value) {
     return;
   }
-  if (!inputValue) return;
+  if (!query.value) return;
+  query.value = '';
   loading.value = true;
   setTimeout(() => {
     loading.value = false;
diff --git a/packages/pro-components/chat/_example/reasoning-drag.vue b/packages/pro-components/chat/_example/reasoning-drag.vue
index 66bdb83a2..cdc478a0f 100644
--- a/packages/pro-components/chat/_example/reasoning-drag.vue
+++ b/packages/pro-components/chat/_example/reasoning-drag.vue
@@ -30,7 +30,7 @@
               expandIconPlacement: 'right',
               onExpandChange: handleChange(value, { index }),
               collapsePanelProps: {
-                header: renderHeader(index === 0 && isStreamLoad, item),
+                header: renderHeader(index === 0 && isStreamLoad && !item.content, item),
                 content: renderReasoningContent(item.reasoning),
               },
             }"
@@ -39,6 +39,7 @@
         </template>
         <template #footer>
           <t-chat-sender
+            v-model="inputValue"
             :stop-disabled="isStreamLoad"
             :textarea-props="{
               placeholder: '请输入消息...',
@@ -79,6 +80,7 @@ const loading = ref(false);
 // 流式数据加载中
 const isStreamLoad = ref(false);
 const visibleModelessDrag = ref(false);
+const inputValue = ref('');
 // 滚动到底部
 const operation = function (type, options) {
   console.log(type, options);
@@ -191,16 +193,16 @@ const onStop = function () {
     isStreamLoad.value = false;
   }
 };
-const inputEnter = function (inputValue) {
+const inputEnter = function () {
   if (isStreamLoad.value) {
     return;
   }
-  if (!inputValue) return;
+  if (!inputValue.value) return;
   const params = {
     avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
     name: '自己',
     datetime: new Date().toDateString(),
-    content: inputValue,
+    content: inputValue.value,
     role: 'user',
   };
   chatList.value.unshift(params);
@@ -214,7 +216,8 @@ const inputEnter = function (inputValue) {
     role: 'assistant',
   };
   chatList.value.unshift(params2);
-  handleData(inputValue);
+  handleData(inputValue.value);
+  inputValue.value = '';
 };
 const fetchSSE = async (fetchFn, options) => {
   const response = await fetchFn();
diff --git a/packages/pro-components/chat/_example/reasoning-drawer.vue b/packages/pro-components/chat/_example/reasoning-drawer.vue
index 990033708..3e208073c 100644
--- a/packages/pro-components/chat/_example/reasoning-drawer.vue
+++ b/packages/pro-components/chat/_example/reasoning-drawer.vue
@@ -23,7 +23,7 @@
             expandIconPlacement: 'right',
             onExpandChange: handleChange(value, { index }),
             collapsePanelProps: {
-              header: renderHeader(index === 0 && isStreamLoad, item),
+              header: renderHeader(index === 0 && isStreamLoad && !item.content, item),
               content: renderReasoningContent(item.reasoning),
             },
           }"
@@ -32,6 +32,7 @@
       </template>
       <template #footer>
         <t-chat-sender
+          v-model="inputValue"
           :stop-disabled="isStreamLoad"
           :textarea-props="{
             placeholder: '请输入消息...',
@@ -68,6 +69,7 @@ import { CheckCircleIcon } from 'tdesign-icons-vue-next';
 
 const fetchCancel = ref(null);
 const loading = ref(false);
+const inputValue = ref('');
 // 流式数据加载中
 const isStreamLoad = ref(false);
 const visible = ref(false);
@@ -196,16 +198,16 @@ const onStop = function () {
   }
 };
 
-const inputEnter = function (inputValue) {
+const inputEnter = function () {
   if (isStreamLoad.value) {
     return;
   }
-  if (!inputValue) return;
+  if (!inputValue.value) return;
   const params = {
     avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
     name: '自己',
     datetime: new Date().toDateString(),
-    content: inputValue,
+    content: inputValue.value,
     role: 'user',
   };
   chatList.value.unshift(params);
@@ -219,7 +221,8 @@ const inputEnter = function (inputValue) {
     role: 'assistant',
   };
   chatList.value.unshift(params2);
-  handleData(inputValue);
+  handleData(inputValue.value);
+  inputValue.value = '';
 };
 const fetchSSE = async (fetchFn, options) => {
   const response = await fetchFn();
diff --git a/packages/pro-components/chat/_example/reasoning.vue b/packages/pro-components/chat/_example/reasoning.vue
index 6d5b518c1..d40f68521 100644
--- a/packages/pro-components/chat/_example/reasoning.vue
+++ b/packages/pro-components/chat/_example/reasoning.vue
@@ -21,7 +21,7 @@
             expandIconPlacement: 'right',
             onExpandChange: handleChange(value, { index }),
             collapsePanelProps: {
-              header: renderHeader(index === 0 && isStreamLoad, item),
+              header: renderHeader(index === 0 && isStreamLoad && !item.content, item),
               content: renderReasoningContent(item.reasoning),
             },
           }"
@@ -30,6 +30,7 @@
       </template>
       <template #footer>
         <t-chat-sender
+          v-model="inputValue"
           :stop-disabled="isStreamLoad"
           :textarea-props="{
             placeholder: '请输入消息...',
@@ -73,7 +74,7 @@ const fetchCancel = ref(null);
 const loading = ref(false);
 // 流式数据加载中
 const isStreamLoad = ref(false);
-
+const inputValue = ref('');
 const chatRef = ref(null);
 const isShowToBottom = ref(false);
 // 滚动到底部
@@ -185,16 +186,17 @@ const handleChatScroll = function ({ e }) {
   isShowToBottom.value = scrollTop < 0;
 };
 
-const inputEnter = function (inputValue) {
+const inputEnter = function () {
+  console.log('inputEnter', inputValue.value);
   if (isStreamLoad.value) {
     return;
   }
-  if (!inputValue) return;
+  if (!inputValue.value) return;
   const params = {
     avatar: 'https://tdesign.gtimg.com/site/avatar.jpg',
     name: '自己',
     datetime: new Date().toDateString(),
-    content: inputValue,
+    content: inputValue.value,
     role: 'user',
   };
   chatList.value.unshift(params);
@@ -208,7 +210,8 @@ const inputEnter = function (inputValue) {
     role: 'assistant',
   };
   chatList.value.unshift(params2);
-  handleData(inputValue);
+  handleData(inputValue.value);
+  inputValue.value = '';
 };
 const fetchSSE = async (fetchFn, options) => {
   const response = await fetchFn();
diff --git a/packages/pro-components/chat/chat-sender.tsx b/packages/pro-components/chat/chat-sender.tsx
index ffbbd6ad8..e1075dc53 100644
--- a/packages/pro-components/chat/chat-sender.tsx
+++ b/packages/pro-components/chat/chat-sender.tsx
@@ -33,11 +33,11 @@ export default defineComponent({
       if (textValue.value && !disabled.value) {
         emit('send', textValue.value, { e });
         loading.value = true;
-        textValue.value = '';
       }
     };
     // 点击了停止按钮
     const handleStop = (e: MouseEvent) => {
+      e.stopPropagation(); // 阻止事件冒泡
       loading.value = false;
       emit('stop', textValue.value, {
         e,
@@ -189,18 +189,25 @@ export default defineComponent({
                 item.name === 'uploadAttachment' || item.name === 'uploadImage',
             )
             .map((item) => buttonComponents[item.name])}
-          <Button
-            theme="default"
-            size="small"
-            variant="text"
-            class={[
-              `${COMPONENT_NAME.value}-sender__button__default`,
-              textValue.value ? '' : `${COMPONENT_NAME.value}-sender__button--disabled`,
-            ]}
-            disabled={disabled.value || showStopBtn.value || !textValue.value}
-          >
-            <SendFilledIcon />
-          </Button>
+          {!showStopBtn.value ? (
+            <Button
+              theme="default"
+              size="small"
+              variant="text"
+              class={[
+                `${COMPONENT_NAME.value}-sender__button__default`,
+                textValue.value ? '' : `${COMPONENT_NAME.value}-sender__button--disabled`,
+              ]}
+              onClick={sendClick}
+              disabled={disabled.value || showStopBtn.value || !textValue.value}
+            >
+              <SendFilledIcon />
+            </Button>
+          ) : (
+            <Button variant="text" class={`${COMPONENT_NAME.value}-sender__button__default`} onClick={handleStop}>
+              <div class={`${COMPONENT_NAME.value}-sender__button__stopicon`} />
+            </Button>
+          )}
         </Fragment>
       );
       // }
@@ -243,19 +250,7 @@ export default defineComponent({
             <div class={`${COMPONENT_NAME.value}-sender__mode`}>{renderContent('default', 'prefix')}</div>
             <div class={`${COMPONENT_NAME.value}-sender__button`}>
               {/* 发送按钮 */}
-              {!showStopBtn.value && (
-                <div class={`${COMPONENT_NAME.value}-sender__button__sendbtn`} onClick={sendClick}>
-                  {renderSuffixIcon()}
-                </div>
-              )}
-              {/* 停止按钮 */}
-              {showStopBtn.value && (
-                <div class={`${COMPONENT_NAME.value}-sender__button__stopbtn`}>
-                  <Button variant="text" class={`${COMPONENT_NAME.value}-sender__button__default`} onClick={handleStop}>
-                    <div class={`${COMPONENT_NAME.value}-sender__button__stopicon`} />
-                  </Button>
-                </div>
-              )}
+              <div class={`${COMPONENT_NAME.value}-sender__button__sendbtn`}>{renderSuffixIcon()}</div>
             </div>
           </div>
         </div>
diff --git a/packages/pro-components/chat/index.ts b/packages/pro-components/chat/index.ts
index 68ffc2d70..589d5b2fb 100644
--- a/packages/pro-components/chat/index.ts
+++ b/packages/pro-components/chat/index.ts
@@ -56,6 +56,7 @@ export default {
     app.use(ChatReasoning, config);
     app.use(ChatAction, config);
     app.use(ChatLoading, config);
+    app.use(ChatSender, config);
   },
   version: typeof PKG_VERSION === 'undefined' ? '' : PKG_VERSION,
 };
diff --git a/packages/tdesign-vue-next-chat/site/plugin-doc/md-to-vue.js b/packages/tdesign-vue-next-chat/site/plugin-doc/md-to-vue.js
index 7546ce57b..299e3246b 100644
--- a/packages/tdesign-vue-next-chat/site/plugin-doc/md-to-vue.js
+++ b/packages/tdesign-vue-next-chat/site/plugin-doc/md-to-vue.js
@@ -170,7 +170,7 @@ async function customRender({ source, file, md }) {
   };
 
   // 渲染 live demo
-  if (pageData.usage && pageData.isComponent) {
+  if (pageData.usage && pageData.isComponent && pageData.title === 'Chat') {
     const usageObj = compileUsage({
       componentName,
       usage: pageData.usage,
diff --git a/packages/tdesign-vue-next/.changelog/pr-5460.md b/packages/tdesign-vue-next/.changelog/pr-5460.md
new file mode 100644
index 000000000..5a9c35ab6
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5460.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5460
+contributor: Soya-xy
+---
+
+- feat(Progress): 当 `theme=plump` 时，当进度条填色区域大小足够容下百分比内容时，百分比内容将显示于进度条填色区域内，否则将显示于进度条填色区域的右侧，具体呈现请参考文档示例 @RSS1102 @Soya-xy ([#5460](https://github.com/Tencent/tdesign-vue-next/pull/5460))
diff --git a/packages/tdesign-vue-next/.changelog/pr-5477.md b/packages/tdesign-vue-next/.changelog/pr-5477.md
new file mode 100644
index 000000000..bee397489
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5477.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5477
+contributor: MrElvin
+---
+
+- fix(message): 修复插件调用场景下， `attach` 所在Dom被清空后，新的 `message` 无法显示的问题 @MrElvin ([#5477](https://github.com/Tencent/tdesign-vue-next/pull/5477))
diff --git a/packages/tdesign-vue-next/.changelog/pr-5510.md b/packages/tdesign-vue-next/.changelog/pr-5510.md
new file mode 100644
index 000000000..24f205a6f
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5510.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5510
+contributor: Wesley-0808
+---
+
+- feat(GlobalConfig): `FormConfig` 新增 `requiredMaskPosition` @Wesley-0808 ([#5510](https://github.com/Tencent/tdesign-vue-next/pull/5510))
diff --git a/packages/tdesign-vue-next/.changelog/pr-5517.md b/packages/tdesign-vue-next/.changelog/pr-5517.md
new file mode 100644
index 000000000..a3f9eac38
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5517.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5517
+contributor: tdesign-bot
+---
+
+- fix(Slider): 修复 Slider 最后一个 label 换行的问题 @l123wx  ([#5517](https://github.com/Tencent/tdesign-vue-next/pull/5517))


Test patch:
diff --git a/packages/components/progress/__tests__/index.test.jsx b/packages/components/progress/__tests__/index.test.jsx
index 59c70edde..738eab09d 100644
--- a/packages/components/progress/__tests__/index.test.jsx
+++ b/packages/components/progress/__tests__/index.test.jsx
@@ -28,13 +28,52 @@ describe('Progress', () => {
       expect(getComputedStyle(inner.element, null).background).toBe('red');
     });
 
-    it(':label', () => {
+    it(':label:string', () => {
       const wrapper = mount(() => <Progress percentage={30} label="label" />);
       const info = wrapper.find('.t-progress__info');
       expect(info.exists()).toBeTruthy();
       expect(info.text()).toBe('label');
     });
 
+    it(':label:boolean', () => {
+      const wrapper = mount(() => <Progress percentage={30} label />);
+      const info = wrapper.find('.t-progress__info');
+      expect(info.exists()).toBeTruthy();
+      expect(info.text()).toBe('30%');
+    });
+
+    it(':label:slot', () => {
+      const customSlotLabel = 'Custom Label';
+      const wrapper = mount(() => (
+        <Progress
+          percentage={30}
+          v-slots={{
+            label: () => <span>{customSlotLabel}</span>,
+          }}
+        />
+      ));
+      const info = wrapper.find('.t-progress__info');
+      expect(info.exists()).toBeTruthy();
+      expect(info.text()).toBe(customSlotLabel);
+    });
+
+    it(':label:boolean-slot', () => {
+      const customLabel = '50%';
+      const customSlotLabel = 'Custom Label';
+      const wrapper = mount(() => (
+        <Progress
+          percentage={30}
+          label={customLabel}
+          v-slots={{
+            label: () => <span>{customSlotLabel}</span>,
+          }}
+        />
+      ));
+      const info = wrapper.find('.t-progress__info');
+      expect(info.exists()).toBeTruthy();
+      expect(info.text()).toBe(customLabel);
+    });
+
     it(':trackColor', () => {
       const wrapper = mount(() => <Progress percentage={30} trackColor="red" />);
       const bar = wrapper.find('.t-progress__bar');