From ab4a5e4724d41b9e5914b1ee02f9fc690d59369f Mon Sep 17 00:00:00 2001
From: GBcui <1760326146@qq.com>
Date: Wed, 10 Sep 2025 12:00:52 +0800
Subject: [PATCH] feat(MentionSender, Sender): add openHeader prop for
controlling header visibility
---
.gitignore | 1 +
.../components/mentionSender/demos/header.vue | 53 ++++-----
.../docs/en/components/mentionSender/index.md | 1 +
.../en/components/sender/demos/header.vue | 53 ++++-----
apps/docs/en/components/sender/index.md | 2 +-
.../components/mentionSender/demos/header.vue | 53 ++++-----
.../docs/zh/components/mentionSender/index.md | 1 +
.../zh/components/sender/demos/header.vue | 53 ++++-----
apps/docs/zh/components/sender/index.md | 1 +
packages/core/components.d.ts | 2 -
.../src/components/MentionSender/index.vue | 88 +++++++++------
.../src/components/MentionSender/types.d.ts | 4 +
packages/core/src/components/Sender/index.vue | 103 +++++++++++-------
.../core/src/components/Sender/types.d.ts | 4 +
.../src/stories/MentionSender/CustomSolt.vue | 28 ++---
.../MentionSender/MentionSender.stories.ts | 9 ++
.../core/src/stories/Sender/CustomSolt.vue | 28 ++---
.../core/src/stories/Sender/Sender.stories.ts | 10 ++
18 files changed, 257 insertions(+), 237 deletions(-)
diff --git a/.gitignore b/.gitignore
index 2b1fdcb6..60c53fb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,4 @@ yarn-error.log*
.oxlintrc.json.bak
*/**/auto-imports.d.ts
*/**/components.d.ts
+.cunzhi*
diff --git a/apps/docs/en/components/mentionSender/demos/header.vue b/apps/docs/en/components/mentionSender/demos/header.vue
index d726b27e..618cf3b1 100644
--- a/apps/docs/en/components/mentionSender/demos/header.vue
+++ b/apps/docs/en/components/mentionSender/demos/header.vue
@@ -5,39 +5,36 @@ title: Header Slot
Use the `#header` slot to customize the input header content.
-::: info
-Control header container expand/collapse through component instance
+::: warning Deprecation Warning
+The following methods will be removed in the next major version. It is recommended to use `v-model:openHeader` for two-way binding control:
-- `senderRef.value.openHeader()` Open header container
-- `senderRef.value.closeHeader()` Close header container
+- `senderRef.value.openHeader()` Open header container (deprecated)
+- `senderRef.value.closeHeader()` Close header container (deprecated)
+:::
+
+::: tip Recommended Usage
+Use `v-model:openHeader` for two-way binding to control header display state:
+
+```vue
+
+ Header content
+
+```
:::
@@ -51,16 +48,14 @@ function closeHeader() {
justify-content: space-between;
"
>
-
- {{ showHeaderFlog ? 'Close Header' : 'Open Header' }}
+
+ {{ openHeader ? 'Close Header' : 'Open Header' }}
-
+
diff --git a/apps/docs/en/components/mentionSender/index.md b/apps/docs/en/components/mentionSender/index.md
index 53b45536..4cbd93b6 100644
--- a/apps/docs/en/components/mentionSender/index.md
+++ b/apps/docs/en/components/mentionSender/index.md
@@ -142,6 +142,7 @@ This warm tip was last updated: `2025-04-16`
| ------------------------- | -------------------- | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `v-model` | String | No | '' | Bound value of the input box, use `v-model` for two-way binding. |
| `placeholder` | String | No | '' | Placeholder text for the input box. |
+| `openHeader` | Boolean | No | false | Whether to open the custom header of the input box, using `v-model:openHeader` for two-way binding |
| `auto-size` | Object | No | \{ minRows:1, maxRows:6 \} | Set the minimum and maximum number of visible rows for the input box. |
| `read-only` | Boolean | No | false | Whether the input box is read-only. |
| `disabled` | Boolean | No | false | Whether the input box is disabled. |
diff --git a/apps/docs/en/components/sender/demos/header.vue b/apps/docs/en/components/sender/demos/header.vue
index 33ae33c8..404bc867 100644
--- a/apps/docs/en/components/sender/demos/header.vue
+++ b/apps/docs/en/components/sender/demos/header.vue
@@ -5,39 +5,36 @@ title: Header Slot
Use the `#header` slot to customize the input header content.
-::: info
-Control header container expand/collapse through component instance
+::: warning Deprecation Warning
+The following methods will be removed in the next major version. It is recommended to use `v-model:openHeader` for two-way binding control:
-- `senderRef.value.openHeader()` Open header container
-- `senderRef.value.closeHeader()` Close header container
+- `senderRef.value.openHeader()` Open header container (deprecated)
+- `senderRef.value.closeHeader()` Close header container (deprecated)
+:::
+
+::: tip Recommended Usage
+Use `v-model:openHeader` for two-way binding to control header display state:
+
+```vue
+
+ Header content
+
+```
:::
@@ -51,16 +48,14 @@ function closeHeader() {
justify-content: space-between;
"
>
-
- {{ showHeaderFlog ? 'Close Header' : 'Open Header' }}
+
+ {{ openHeader ? 'Close Header' : 'Open Header' }}
-
+
diff --git a/apps/docs/en/components/sender/index.md b/apps/docs/en/components/sender/index.md
index 683ce246..5732a952 100644
--- a/apps/docs/en/components/sender/index.md
+++ b/apps/docs/en/components/sender/index.md
@@ -2,7 +2,6 @@
title: Sender
---
-
::: warning
`Added in version 1.1.6`
@@ -101,6 +100,7 @@ Built-in browser speech recognition API. You can use the [`useRecord`](https://e
| ------------------------- | -------------------- | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `v-model` | String | No | '' | Bound value of the input box, use `v-model` for two-way binding. |
| `placeholder` | String | No | '' | Placeholder text for the input box. |
+| `openHeader` | Boolean | No | false | Whether to open the custom header of the input box, using `v-model:openHeader` for two-way binding |
| `auto-size` | Object | No | \{ minRows:1, maxRows:6 \} | Set the minimum and maximum number of visible rows for the input box. |
| `read-only` | Boolean | No | false | Whether the input box is read-only. |
| `disabled` | Boolean | No | false | Whether the input box is disabled. |
diff --git a/apps/docs/zh/components/mentionSender/demos/header.vue b/apps/docs/zh/components/mentionSender/demos/header.vue
index f97381c0..4054f1c9 100644
--- a/apps/docs/zh/components/mentionSender/demos/header.vue
+++ b/apps/docs/zh/components/mentionSender/demos/header.vue
@@ -5,39 +5,36 @@ title: 头部插槽
通过 `#header` 插槽用于自定义输入框的头部内容。
-::: info
-通过组件实例控制 头部容器 展开收起
+::: warning 废弃警告
+以下方法将在下个大版本中移除,推荐使用 `v-model:openHeader` 进行双向绑定控制:
-- `senderRef.value.openHeader()` 打开头部容器
-- `senderRef.value.closeHeader()` 关闭头部容器
+- `senderRef.value.openHeader()` 打开头部容器 (已废弃)
+- `senderRef.value.closeHeader()` 关闭头部容器 (已废弃)
+:::
+
+::: tip 推荐用法
+使用 `v-model:openHeader` 进行双向绑定控制头部显示状态:
+
+```vue
+
+ 头部内容
+
+```
:::
@@ -51,16 +48,14 @@ function closeHeader() {
justify-content: space-between;
"
>
-
- {{ showHeaderFlog ? '关闭头部' : '打开头部' }}
+
+ {{ openHeader ? '关闭头部' : '打开头部' }}
-
+
diff --git a/apps/docs/zh/components/mentionSender/index.md b/apps/docs/zh/components/mentionSender/index.md
index edca877e..ecfe84b6 100644
--- a/apps/docs/zh/components/mentionSender/index.md
+++ b/apps/docs/zh/components/mentionSender/index.md
@@ -142,6 +142,7 @@ title: 'MentionSender'
| ------------------------- | -------------------- | -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `v-model` | String | 否 | '' | 输入框的绑定值,使用 `v-model` 进行双向绑定。 |
| `placeholder` | String | 否 | '' | 输入框的提示语文本。 |
+| `openHeader` | Boolean | 否 | false | 输入框自定义头部是否打开,使用 `v-model:openHeader` 进行双向绑定 |
| `auto-size` | Object | 否 | \{ minRows:1, maxRows:6 \} | 设置输入框的最小展示行数和最大展示行数。 |
| `read-only` | Boolean | 否 | false | 输入框是否为只读状态。 |
| `disabled` | Boolean | 否 | false | 输入框是否为禁用状态。 |
diff --git a/apps/docs/zh/components/sender/demos/header.vue b/apps/docs/zh/components/sender/demos/header.vue
index d1fa7c52..ad8b87ee 100644
--- a/apps/docs/zh/components/sender/demos/header.vue
+++ b/apps/docs/zh/components/sender/demos/header.vue
@@ -5,39 +5,36 @@ title: 头部插槽
通过 `#header` 插槽用于自定义输入框的头部内容。
-::: info
-通过组件实例控制 头部容器 展开收起
+::: warning 废弃警告
+以下方法将在下个大版本中移除,推荐使用 `v-model:openHeader` 进行双向绑定控制:
-- `senderRef.value.openHeader()` 打开头部容器
-- `senderRef.value.closeHeader()` 关闭头部容器
+- `senderRef.value.openHeader()` 打开头部容器 (已废弃)
+- `senderRef.value.closeHeader()` 关闭头部容器 (已废弃)
+:::
+
+::: tip 推荐用法
+使用 `v-model:openHeader` 进行双向绑定控制头部显示状态:
+
+```vue
+
+ 头部内容
+
+```
:::
@@ -51,16 +48,14 @@ function closeHeader() {
justify-content: space-between;
"
>
-
- {{ showHeaderFlog ? '关闭头部' : '打开头部' }}
+
+ {{ openHeader ? '关闭头部' : '打开头部' }}
-
+
diff --git a/apps/docs/zh/components/sender/index.md b/apps/docs/zh/components/sender/index.md
index 5582181d..37660eb1 100644
--- a/apps/docs/zh/components/sender/index.md
+++ b/apps/docs/zh/components/sender/index.md
@@ -100,6 +100,7 @@ title: 'Sender'
| ------------------------- | -------------------- | -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `v-model` | String | 否 | '' | 输入框的绑定值,使用 `v-model` 进行双向绑定。 |
| `placeholder` | String | 否 | '' | 输入框的提示语文本。 |
+| `openHeader` | Boolean | 否 | false | 输入框自定义头部是否打开,使用 `v-model:openHeader` 进行双向绑定本。 |
| `auto-size` | Object | 否 | \{ minRows:1, maxRows:6 \} | 设置输入框的最小展示行数和最大展示行数。 |
| `read-only` | Boolean | 否 | false | 输入框是否为只读状态。 |
| `disabled` | Boolean | 否 | false | 输入框是否为禁用状态。 |
diff --git a/packages/core/components.d.ts b/packages/core/components.d.ts
index 829d2fd7..eeb83ad7 100644
--- a/packages/core/components.d.ts
+++ b/packages/core/components.d.ts
@@ -35,7 +35,6 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
- ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMention: typeof import('element-plus/es')['ElMention']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
@@ -54,7 +53,6 @@ declare module 'vue' {
Image: typeof import('./src/components/FilesCard/fileSvg/image.vue')['default']
IndexAttachments: typeof import('./src/components/Attachments/index-attachments.vue')['default']
IndexFileList: typeof import('./src/components/Attachments/index-file-list.vue')['default']
- IndexOri: typeof import('./src/components/XMarkdownCore/components/CodeBlock/index-ori.vue')['default']
Item: typeof import('./src/components/Conversations/components/item.vue')['default']
Link: typeof import('./src/components/FilesCard/fileSvg/link.vue')['default']
Loading: typeof import('./src/components/BubbleList/loading.vue')['default']
diff --git a/packages/core/src/components/MentionSender/index.vue b/packages/core/src/components/MentionSender/index.vue
index 5348856c..547258a5 100644
--- a/packages/core/src/components/MentionSender/index.vue
+++ b/packages/core/src/components/MentionSender/index.vue
@@ -30,6 +30,9 @@ const props = withDefaults(defineProps(), {
// el-input 属性透传
inputStyle: '',
+ // 头部显示控制
+ openHeader: false,
+
// el-mention 属性透传
options: () => [],
filterOption: () => true,
@@ -51,8 +54,7 @@ const internalValue = computed({
return props.modelValue;
},
set(val) {
- if (props.readOnly || props.disabled)
- return;
+ if (props.readOnly || props.disabled) return;
emits('update:modelValue', val);
}
});
@@ -95,22 +97,52 @@ function onContentMouseDown(e: MouseEvent) {
/* 内容容器聚焦 结束 */
/* 头部显示隐藏 开始 */
-const visiableHeader = ref(false);
+// 内部状态,用于没有外部绑定时的状态管理
+const internalHeaderOpen = ref(props.openHeader);
+
+// 监听 props.openHeader 变化,同步到内部状态
+watch(
+ () => props.openHeader,
+ newValue => {
+ internalHeaderOpen.value = newValue;
+ }
+);
+
+const headerOpenState = computed({
+ get() {
+ return internalHeaderOpen.value;
+ },
+ set(value) {
+ if (props.readOnly || props.disabled) return;
+
+ internalHeaderOpen.value = value;
+ // 始终触发更新事件,让外部可以监听状态变化
+ emits('update:openHeader', value);
+ }
+});
+
+/**
+ * 打开头部容器
+ * @deprecated 此方法将在下个大版本中移除,请使用 v-model:openHeader 代替
+ * @returns {boolean} 是否成功打开
+ */
function openHeader() {
- if (!slots.header)
- return false;
+ if (!slots.header) return false;
- if (props.readOnly)
- return false;
+ if (props.readOnly) return false;
- visiableHeader.value = true;
+ headerOpenState.value = true;
+ return true;
}
+
+/**
+ * 关闭头部容器
+ * @deprecated 此方法将在下个大版本中移除,请使用 v-model:openHeader 代替
+ */
function closeHeader() {
- if (!slots.header)
- return;
- if (props.readOnly)
- return;
- visiableHeader.value = false;
+ if (!slots.header) return;
+ if (props.readOnly) return;
+ headerOpenState.value = false;
}
/* 头部显示隐藏 结束 */
@@ -119,8 +151,7 @@ const recognition = ref(null);
const speechLoading = ref(false);
function startRecognition() {
- if (props.readOnly)
- return; // 直接返回,不执行后续逻辑
+ if (props.readOnly) return; // 直接返回,不执行后续逻辑
if (hasOnRecordingChangeListener.value) {
speechLoading.value = true;
emits('recordingChange', true);
@@ -151,8 +182,7 @@ function startRecognition() {
speechLoading.value = false;
};
recognition.value.start();
- }
- else {
+ } else {
console.error('浏览器不支持 Web Speech API');
}
}
@@ -185,22 +215,19 @@ function submit() {
}
// 取消按钮
function cancel() {
- if (props.readOnly)
- return;
+ if (props.readOnly) return;
emits('cancel', internalValue.value);
}
function clear() {
- if (props.readOnly)
- return; // 直接返回,不执行后续逻辑
+ if (props.readOnly) return; // 直接返回,不执行后续逻辑
inputRef.value.input.clear();
internalValue.value = '';
}
// 在这判断组合键的回车键 (目前支持四种模式)
function handleKeyDown(e: { target: HTMLTextAreaElement } & KeyboardEvent) {
- if (props.readOnly)
- return; // 直接返回,不执行后续逻辑
+ if (props.readOnly) return; // 直接返回,不执行后续逻辑
const _resetSelectionRange = () => {
const cursorPosition = e.target.selectionStart; // 获取光标位置
const textBeforeCursor = internalValue.value.slice(0, cursorPosition); // 光标前的文本
@@ -231,8 +258,7 @@ function handleKeyDown(e: { target: HTMLTextAreaElement } & KeyboardEvent) {
e.preventDefault();
if (props.submitType === 'enter') {
_isComKeyDown ? _resetSelectionRange() : submit();
- }
- else {
+ } else {
_isComKeyDown ? submit() : _resetSelectionRange();
}
}
@@ -253,11 +279,9 @@ function focus(type = 'all') {
}
if (type === 'all') {
inputRef.value.input.select();
- }
- else if (type === 'start') {
+ } else if (type === 'start') {
focusToStart();
- }
- else if (type === 'end') {
+ } else if (type === 'end') {
focusToEnd();
}
}
@@ -309,8 +333,8 @@ function handleInternalPaste(e: ClipboardEvent) {
}
defineExpose({
- openHeader, // 打开头部
- closeHeader, // 关闭头部
+ openHeader, // 打开头部 (已废弃,请使用 v-model:openHeader)
+ closeHeader, // 关闭头部 (已废弃,请使用 v-model:openHeader)
clear, // 清空输入框
blur, // 失去焦点
focus, // 获取焦点
@@ -345,7 +369,7 @@ defineExpose({
>
-