Repository: Tencent/tdesign-vue-next
Pull request #5488 (2025-05-22 05:15:07)

fix(Select): Even if set `disabled` options `Tag` still can be deleted
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [x] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

- https://github.com/Tencent/tdesign-vue-next/issues/5473

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->

### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->

### 📝 更新日志

- [ ] 本条 PR 不需要纳入 Changelog

#### tdesign-vue-next
<!--
- feat(组件名称): 处理问题或特性描述
--> 

- fix(Select): 修复多选模式下，存在不可选的选项已在选中项中时，仍可通过标签按钮和键盘删除的缺陷

#### @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/15178722792)
  | 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-5488-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-5488-tdesign-vue-next-chat.surge.sh) | 

  <!-- AUTO_PREVIEW_HOOK -->
<!-- AUTO_PREVIEW_HOOK -->
pkg-pr-new[bot]: 
[tdesign-vue-next-demo](https://pkg.pr.new/template/eef35e5b-7045-48f8-91ff-2637025967f7)


```
npm i https://pkg.pr.new/tdesign-vue-next@5488
```


_commit: <a href="https://github.com/Tencent/tdesign-vue-next/runs/42683852519"><code>ae72648</code></a>_

Wesley-0808: /update-snapshot
github-actions[bot]: ⏳ 正在运行快照更新。。。 CI: [Open](https://github.com//Tencent/tdesign-vue-next/actions/runs/15178866270)

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/components/select/_example/multiple.vue b/packages/components/select/_example/multiple.vue
index 52cef040d..682ca3afb 100644
--- a/packages/components/select/_example/multiple.vue
+++ b/packages/components/select/_example/multiple.vue
@@ -55,7 +55,7 @@ const options2 = [
   { label: '低代码平台', value: '6' },
 ];
 
-const value1 = ref(['1', '3']);
+const value1 = ref(['1', '3', '4']);
 const value2 = ref(['1', '2', '3', '4', '5', '6']);
 const value3 = ref(['3', '5', '6', '2']);
 
diff --git a/packages/components/select/select.md b/packages/components/select/select.md
index 9e82c39f0..607a0dd5c 100644
--- a/packages/components/select/select.md
+++ b/packages/components/select/select.md
@@ -48,7 +48,7 @@ tagProps | Object | - | 透传 Tag 标签组件全部属性。TS 类型：`TagPr
 tips | String / Slot / Function | - | 输入框下方提示文本，会根据不同的 `status` 呈现不同的样式。TS 类型：`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
 value | String / Number / Boolean / Object / Array | - | 选中值。支持语法糖 `v-model` 或 `v-model:value`。TS 类型：`SelectValue` `type SelectValue<T extends SelectOption = SelectOption> = string \| number \| boolean \| T \| Array<SelectValue<T>>`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/select/type.ts) | N
 defaultValue | String / Number / Boolean / Object / Array | - | 选中值。非受控属性。TS 类型：`SelectValue` `type SelectValue<T extends SelectOption = SelectOption> = string \| number \| boolean \| T \| Array<SelectValue<T>>`。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/select/type.ts) | N
-valueDisplay | String / Slot / Function | - | 自定义选中项呈现的内容。TS 类型：`string \| TNode<{ value: SelectValue; onClose: (index: number) => void; displayValue?: SelectValue; label?: string }>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
+valueDisplay | String / Slot / Function | - | 自定义选中项呈现的内容。TS 类型：`string \| TNode<{ value: SelectValue; onClose: (index: number) => void; displayValue?: SelectValue; } \| SelectValue>`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/common.ts) | N
 valueType | String | value | 用于控制选中值的类型。假设数据选项为：`[{ label: '姓名', value: 'name' }]`，value 表示值仅返回数据选项中的 value， object 表示值返回全部数据。可选项：value/object | N
 onBlur | Function |  | TS 类型：`(context: { value: SelectValue; e: FocusEvent \| KeyboardEvent }) => void`<br/>输入框失去焦点时触发 | N
 onChange | Function |  | TS 类型：`(value: SelectValue, context: { option?: T, selectedOptions: T[], trigger: SelectValueChangeTrigger; e?: MouseEvent \| KeyboardEvent }) => void`<br/>选中值变化时触发。`context.trigger` 表示触发变化的来源；`context.selectedOptions` 表示选中值的完整对象，数组长度一定和 `value` 相同；`context.option` 表示当前操作的选项，不一定存在。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/packages/components/select/type.ts)。<br/>`type SelectValueChangeTrigger = 'clear' \| 'tag-remove' \| 'backspace' \| 'check' \| 'uncheck' \| 'default'`<br/> | N
diff --git a/packages/components/select/select.tsx b/packages/components/select/select.tsx
index 8785f2717..37be26d18 100644
--- a/packages/components/select/select.tsx
+++ b/packages/components/select/select.tsx
@@ -4,6 +4,7 @@ import { get, isArray, debounce, cloneDeep, isFunction, intersection, pick as pi
 import FakeArrow from '../common-components/fake-arrow';
 import SelectInput from '../select-input';
 import SelectPanel from './select-panel';
+import Tag from '../tag';
 import props from './props';
 // hooks
 import {
@@ -21,7 +22,7 @@ import { selectInjectKey } from './consts';
 import { useSelectOptions, useKeyboardControl } from './hooks';
 import type { PopupProps, PopupVisibleChangeContext } from '../popup';
 import type { SelectInputChangeContext, SelectInputValueChangeContext } from '../select-input';
-import type { TdSelectProps, SelectValue } from './type';
+import type { TdSelectProps, SelectValue, TdOptionProps } from './type';
 import { SelectInputValueDisplayOptions } from '../select-input/hooks/useSingle';
 import { TagInputTriggerSource } from '../tag-input';
 
@@ -139,25 +140,24 @@ export default defineComponent({
 
     // valueDisplayParams参数
     const valueDisplayParams = computed(() => {
-      const val =
-        props.multiple && isArray(innerValue.value)
-          ? (innerValue.value as SelectValue[]).map((value) => ({
-              value,
-              label: optionsMap.value.get(value)?.label,
-            }))
-          : innerValue.value;
+      if (!props.multiple) {
+        return {
+          ...optionsMap.value.get(innerValue.value),
+          value: innerValue.value,
+          label: displayText.value,
+        };
+      }
 
+      const val = isArray(innerValue.value) ? innerValue.value.map((value) => optionsMap.value.get(value)) : [];
       const params = {
         value: val,
         onClose: props.multiple ? (index: number) => removeTag(index) : () => {},
       };
 
-      if (!props.multiple) Object.assign(params, { label: displayText.value });
-
-      if (props.minCollapsedNum && props.multiple) {
+      if (props.minCollapsedNum && isArray(innerValue.value)) {
         return {
           ...params,
-          displayValue: val?.slice?.(0, props.minCollapsedNum),
+          displayValue: Array.isArray(val) ? val.slice(0, props.minCollapsedNum) : [],
         };
       }
       return params;
@@ -185,6 +185,40 @@ export default defineComponent({
 
       selectValue.splice(index, 1);
 
+      if (trigger === 'backspace') {
+        // 如果最后一个为disabled，则应删除前一项（非disabled的）
+        let closest = -1;
+        let len = index;
+        const currentSelected = getCurrentSelectedOptions();
+        while (len >= 0) {
+          if (!currentSelected[len]?.disabled) {
+            closest = len;
+            break;
+          }
+          len -= 1;
+        }
+        // 只剩下disabled的情况，不做任何操作
+        if (closest < 0) return;
+
+        // 前面不是disabled的option
+        const values = currentSelected[closest];
+
+        const currentSelectedOptions = currentSelected.filter((item) => item.value !== values.value);
+
+        setInnerValue(
+          currentSelectedOptions.map((item) => item.value),
+          { selectedOptions: currentSelectedOptions, trigger, e },
+        );
+
+        props.onRemove?.({
+          value: values.value as string | number,
+          data: values,
+          e,
+        });
+
+        return;
+      }
+
       if (trigger !== 'clear') {
         setInnerValue(selectValue, { selectedOptions: getSelectedOptions(selectValue), trigger, e });
       }
@@ -229,6 +263,19 @@ export default defineComponent({
       });
     };
 
+    //  获取当前选中的选项，和 getSelectedOptions 的区别是 这个会保持选择的先后顺序
+    const getCurrentSelectedOptions = (selectValue: SelectValue[] | SelectValue = innerValue.value) => {
+      const options: TdOptionProps[] = [];
+      const values = isArray(selectValue) ? selectValue : [selectValue];
+
+      values.forEach((value) => {
+        const option = optionsMap.value.get(value);
+        if (option) options.push(option);
+      });
+
+      return options;
+    };
+
     /*
      * 全选逻辑：
      * 根据 checked 的值计算最终选中的值：
@@ -421,6 +468,42 @@ export default defineComponent({
         }
       });
     };
+
+    const renderValueDisplay = () => {
+      const renderTag = () => {
+        if (!props.multiple) {
+          return undefined;
+        }
+        const currentSelectedOptions = getCurrentSelectedOptions(innerValue.value);
+        return currentSelectedOptions
+          .slice(0, props.minCollapsedNum ? props.minCollapsedNum : currentSelectedOptions.length)
+          .map((v: TdOptionProps, key: number) => {
+            return (
+              <Tag
+                key={key}
+                closable={!v?.disabled && !props.disabled && !props.readonly}
+                size={props.size}
+                {...props.tagProps}
+                onClose={({ e }: { e: MouseEvent }) => {
+                  e.stopPropagation();
+                  const index = currentSelectedOptions.findIndex((item) => item.value === v.value);
+                  props.tagProps?.onClose?.({ e });
+                  removeTag(index);
+                }}
+              >
+                {v?.label ?? v?.value}
+              </Tag>
+            );
+          });
+      };
+
+      return (
+        renderTNodeJSX('valueDisplay', {
+          params: valueDisplayParams.value,
+        }) || renderTag()
+      );
+    };
+
     provide('updateScrollTop', updateScrollTop);
     return () => {
       const { overlayClassName, ...restPopupProps } = (props.popupProps || {}) as TdSelectProps['popupProps'];
@@ -486,11 +569,7 @@ export default defineComponent({
                 )
               );
             }}
-            valueDisplay={() =>
-              renderTNodeJSX('valueDisplay', {
-                params: valueDisplayParams.value,
-              })
-            }
+            valueDisplay={renderValueDisplay}
             onPopupVisibleChange={handlerPopupVisibleChange}
             onInputChange={handlerInputChange}
             onClear={({ e }) => {
diff --git a/packages/components/select/type.ts b/packages/components/select/type.ts
index 5b04aefbc..35ddb2638 100644
--- a/packages/components/select/type.ts
+++ b/packages/components/select/type.ts
@@ -209,7 +209,7 @@ export interface TdSelectProps<T extends SelectOption = SelectOption> {
    */
   valueDisplay?:
     | string
-    | TNode<{ value: SelectValue; onClose: (index: number) => void; displayValue?: SelectValue; label?: string }>;
+    | TNode<{ value: SelectValue; onClose: (index: number) => void; displayValue?: SelectValue } | SelectValue>;
   /**
    * 用于控制选中值的类型。假设数据选项为：`[{ label: '姓名', value: 'name' }]`，value 表示值仅返回数据选项中的 value， object 表示值返回全部数据。
    * @default value
diff --git a/packages/hooks/tnode/index.ts b/packages/hooks/tnode/index.ts
index 4718583d6..0b153714d 100644
--- a/packages/hooks/tnode/index.ts
+++ b/packages/hooks/tnode/index.ts
@@ -22,10 +22,10 @@ function handleSlots(instance: ComponentInternalInstance, name: string, params:
 
   // 检查是否存在 驼峰命名 的插槽（过滤注释节点）
   let node = instance.slots[camelCase(name)]?.(params);
-  if (node && node.filter((t) => t.type.toString() !== 'Symbol(v-cmt)').length) return node;
+  if (node && node.filter((t) => t.type.toString?.() !== 'Symbol(v-cmt)').length) return node;
   // 检查是否存在 中划线命名 的插槽
   node = instance.slots[kebabCase(name)]?.(params);
-  if (node && node.filter((t) => t.type.toString() !== 'Symbol(v-cmt)').length) return node;
+  if (node && node.filter((t) => t.type.toString?.() !== 'Symbol(v-cmt)').length) return node;
   return null;
 }
 
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-5488.md b/packages/tdesign-vue-next/.changelog/pr-5488.md
new file mode 100644
index 000000000..5e4f5b15c
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5488.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5488
+contributor: Wesley-0808
+---
+
+- fix(Select): 修复多选模式下，存在不可选的选项已在选中项中时，仍可通过标签按钮和键盘删除的缺陷 @Wesley-0808 ([#5488](https://github.com/Tencent/tdesign-vue-next/pull/5488))
diff --git a/packages/tdesign-vue-next/.changelog/pr-5509.md b/packages/tdesign-vue-next/.changelog/pr-5509.md
new file mode 100644
index 000000000..f39b04997
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5509.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5509
+contributor: RSS1102
+---
+
+- feat(select):  `valueDisplay` 参数 `value` 返回完整option内容，用于使用其他option参数进行其他展示定制的场景 @RSS1102 ([#5509](https://github.com/Tencent/tdesign-vue-next/pull/5509))
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))
diff --git a/packages/tdesign-vue-next/.changelog/pr-5521.md b/packages/tdesign-vue-next/.changelog/pr-5521.md
new file mode 100644
index 000000000..59efe46b6
--- /dev/null
+++ b/packages/tdesign-vue-next/.changelog/pr-5521.md
@@ -0,0 +1,6 @@
+---
+pr_number: 5521
+contributor: uyarn
+---
+
+- fix: 优化插槽方法的判断兼容更多组件类型的使用 @uyarn ([#5521](https://github.com/Tencent/tdesign-vue-next/pull/5521))


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');
diff --git a/packages/components/select/__tests__/__snapshots__/index.test.jsx.snap b/packages/components/select/__tests__/__snapshots__/index.test.jsx.snap
index bccdeac52..479c7f210 100644
--- a/packages/components/select/__tests__/__snapshots__/index.test.jsx.snap
+++ b/packages/components/select/__tests__/__snapshots__/index.test.jsx.snap
@@ -368,6 +368,8 @@ exports[`Select > :props > :multiple 1`] = `
         >
           
           
+          
+          
         </div>
         <input
           class="t-input__inner"