Skip to content

Commit 7ded915

Browse files
kieranwvwzc520pyfm
andauthored
feat(sender): add speech custom icon support (#374)
* feat(sender): add speech custom icon support and docs demo * docs enhance SenderProps with audio icon customization options * chore: remove console * feat(sender): use the allowSpeech field to customize the icon * fix: fix SenderProps types * fix(sender): fix allow-speech type --------- Co-authored-by: wzc520pyfm <1528857653@qq.com>
1 parent 18b23af commit 7ded915

File tree

7 files changed

+151
-49
lines changed

7 files changed

+151
-49
lines changed

docs/component/sender.md

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# Sender 输入框
32

43
用于聊天的输入框组件。
@@ -45,6 +44,18 @@ sender/speech
4544

4645
</ClientOnly>
4746

47+
### 语音输入图标
48+
49+
<ClientOnly>
50+
51+
:::demo 调整语音输入显示的图标,包含正在录音的图标。
52+
53+
sender/speechIcon
54+
55+
:::
56+
57+
</ClientOnly>
58+
4859
### 自定义语音输入
4960

5061
<ClientOnly>
@@ -143,35 +154,38 @@ sender/focus
143154

144155
### SenderProps
145156

146-
| 属性 | 说明 | 类型 | 默认值 | 版本 |
147-
| --- | --- | --- | --- | --- |
148-
| actions | 自定义按钮,当不需要默认操作按钮时,可以设为 `actions={false}` | VNode \| (oriNode, info: \{ components: ActionsComponents \}) => VNode | - | - |
149-
| allowSpeech | 是否允许语音输入 | boolean \| SpeechConfig | false | - |
150-
| classNames | 样式类名 | [见下](#semantic-dom) | - | - |
151-
| components | 自定义组件 | Record<'input', ComponentType> | - | - |
152-
| defaultValue | 输入框默认值 | string | - | - |
153-
| disabled | 是否禁用 | boolean | false | - |
154-
| loading | 是否加载中 | boolean | false | - |
155-
| header | 头部面板 | VNode \| () => VNode | - | - |
156-
| prefix | 前缀内容 | VNode \| () => VNode | - | - |
157-
| footer | 底部内容 | ReactNode \| (info: \{ components: ActionsComponents \}) => ReactNode | - | - |
158-
| readOnly | 是否让输入框只读 | boolean | false | - |
159-
| rootClassName | 根元素样式类 | string | - | - |
160-
| styles | 语义化定义样式 | [见下](#semantic-dom) | - | - |
161-
| submitType | 提交模式 | SubmitType | `enter` \| `shiftEnter` | - |
162-
| value(v-model) | 输入框值 | string | - | - |
163-
| onSubmit | 点击发送按钮的回调 | (message: string) => void | - | - |
164-
| onChange | 输入框值改变的回调 | (value: string, event?: FormEvent \| ChangeEvent ) => void | - | - |
165-
| onCancel | 点击取消按钮的回调 | () => void | - | - |
166-
| onPasteFile | 黏贴文件的回调 | (firstFile: File, files: FileList) => void | - | - |
167-
| autoSize | 自适应内容高度,可设置为 true \| false 或对象:\{ minRows: 2, maxRows: 6 \} | boolean \| \{ minRows?: number; maxRows?: number \} | \{ maxRows: 8 \} | - |
157+
| 属性 | 说明 | 类型 | 默认值 | 版本 |
158+
| -------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------- | ---- |
159+
| actions | 自定义按钮,当不需要默认操作按钮时,可以设为 `actions={false}` | VNode \| (oriNode, info: \{ components: ActionsComponents \}) => VNode | - | - |
160+
| allowSpeech | 是否允许语音输入 | boolean \| SpeechConfig | false | - |
161+
| classNames | 样式类名 | [见下](#semantic-dom) | - | - |
162+
| components | 自定义组件 | Record<'input', ComponentType> | - | - |
163+
| defaultValue | 输入框默认值 | string | - | - |
164+
| disabled | 是否禁用 | boolean | false | - |
165+
| loading | 是否加载中 | boolean | false | - |
166+
| header | 头部面板 | VNode \| () => VNode | - | - |
167+
| prefix | 前缀内容 | VNode \| () => VNode | - | - |
168+
| footer | 底部内容 | ReactNode \| (info: \{ components: ActionsComponents \}) => ReactNode | - | - |
169+
| readOnly | 是否让输入框只读 | boolean | false | - |
170+
| rootClassName | 根元素样式类 | string | - | - |
171+
| styles | 语义化定义样式 | [见下](#semantic-dom) | - | - |
172+
| submitType | 提交模式 | SubmitType | `enter` \| `shiftEnter` | - |
173+
| value(v-model) | 输入框值 | string | - | - |
174+
| onSubmit | 点击发送按钮的回调 | (message: string) => void | - | - |
175+
| onChange | 输入框值改变的回调 | (value: string, event?: FormEvent \| ChangeEvent ) => void | - | - |
176+
| onCancel | 点击取消按钮的回调 | () => void | - | - |
177+
| onPasteFile | 黏贴文件的回调 | (firstFile: File, files: FileList) => void | - | - |
178+
| autoSize | 自适应内容高度,可设置为 true \| false 或对象:\{ minRows: 2, maxRows: 6 \} | boolean \| \{ minRows?: number; maxRows?: number \} | \{ maxRows: 8 \} | - |
168179

169180
```typescript | pure
170181
type SpeechConfig = {
171182
// 当设置 `recording` 时,内置的语音输入功能将会被禁用。
172183
// 交由开发者实现三方语音输入的功能。
173184
recording?: boolean;
174185
onRecordingChange?: (recording: boolean) => void;
186+
audioIcon?: ButtonProps['icon'] | VNode;
187+
audioDisabledIcon?: ButtonProps['icon'] | VNode;
188+
audioRecordingIcon?: ButtonProps['icon'] | VNode;
175189
};
176190
```
177191

@@ -186,31 +200,31 @@ type ActionsComponents = {
186200

187201
### Sender Slots
188202

189-
| 插槽名 | 说明 | 类型 |
190-
| ------- | ------ | ---- |
191-
| header | 头部面板 | - |
192-
| prefix | 前缀内容 | _ |
203+
| 插槽名 | 说明 | 类型 |
204+
| ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
205+
| header | 头部面板 | - |
206+
| prefix | 前缀内容 | _ |
193207
| actions | 操作按钮 | \{ ori: VNode; info: \{ components: \{ SendButton: InstanceType\<Button\>; ClearButton: InstanceType\<Button\>; LoadingButton: InstanceType\<Button\>; SpeechButton: InstanceType\<Button\>; \} \} \} |
194-
| footer | 底部内容 | \{ info: \{ components: \{ SendButton: InstanceType\<Button\>; ClearButton: InstanceType\<Button\>; LoadingButton: InstanceType\<Button\>; SpeechButton: InstanceType\<Button\>; \} \} \} |
208+
| footer | 底部内容 | \{ info: \{ components: \{ SendButton: InstanceType\<Button\>; ClearButton: InstanceType\<Button\>; LoadingButton: InstanceType\<Button\>; SpeechButton: InstanceType\<Button\>; \} \} \} |
195209

196210
#### Sender Ref
197211

198-
| 属性 | 说明 | 类型 | 默认值 | 版本 |
199-
| --- | --- | --- | --- | --- |
200-
| nativeElement | 外层容器 | `HTMLDivElement` | - | - |
201-
| focus | 获取焦点 | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | - | - |
202-
| blur | 取消焦点 | () => void | - | - |
212+
| 属性 | 说明 | 类型 | 默认值 | 版本 |
213+
| ------------- | -------- | -------------------------------------------------------------------------- | ------ | ---- |
214+
| nativeElement | 外层容器 | `HTMLDivElement` | - | - |
215+
| focus | 获取焦点 | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | - | - |
216+
| blur | 取消焦点 | () => void | - | - |
203217

204218
### Sender.Header
205219

206-
| 属性 | 说明 | 类型 | 默认值 | 版本 |
207-
| --- | --- | --- | --- | --- |
208-
| children | 面板内容 | VNode | - | - |
209-
| closable | 是否可关闭 | boolean | true | - |
210-
| forceRender | 强制渲染,在初始化便需要 ref 内部元素时使用 | boolean | false | - |
211-
| open | 是否展开 | boolean | - | - |
212-
| title | 标题 | VNode | - | - |
213-
| onOpenChange | 展开状态改变的回调 | (open: boolean) => void | - | - |
220+
| 属性 | 说明 | 类型 | 默认值 | 版本 |
221+
| ------------ | ------------------------------------------- | ----------------------- | ------ | ---- |
222+
| children | 面板内容 | VNode | - | - |
223+
| closable | 是否可关闭 | boolean | true | - |
224+
| forceRender | 强制渲染,在初始化便需要 ref 内部元素时使用 | boolean | false | - |
225+
| open | 是否展开 | boolean | - | - |
226+
| title | 标题 | VNode | - | - |
227+
| onOpenChange | 展开状态改变的回调 | (open: boolean) => void | - | - |
214228

215229
## Semantic DOM
216230

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script setup lang="ts">
2+
import { h } from 'vue';
3+
import { App, message } from 'ant-design-vue';
4+
import { Sender } from 'ant-design-x-vue';
5+
import { SoundOutlined, EllipsisOutlined } from '@ant-design/icons-vue';
6+
7+
defineOptions({ name: 'AXSenderSpeechIconSetup' });
8+
9+
const [messageApi, contextHolder] = message.useMessage();
10+
11+
const submit = () => {
12+
messageApi.success('Send message successfully!');
13+
};
14+
</script>
15+
<template>
16+
<App>
17+
<context-holder />
18+
<Sender
19+
:allow-speech="{
20+
audioIcon: h(SoundOutlined),
21+
audioDisabledIcon: h(SoundOutlined, { style: { color: 'gray' } }),
22+
audioRecordingIcon: h(EllipsisOutlined)
23+
}"
24+
:on-submit="submit"
25+
/>
26+
</App>
27+
</template>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script setup lang="tsx">
2+
import { h } from 'vue';
3+
import { App } from 'ant-design-vue';
4+
import { Sender } from 'ant-design-x-vue';
5+
import { SoundOutlined, EllipsisOutlined } from '@ant-design/icons-vue';
6+
7+
defineOptions({ name: 'AXSenderSpeechIcon' });
8+
9+
const Demo = () => {
10+
const { message } = App.useApp();
11+
12+
return (
13+
<Sender
14+
allowSpeech={{
15+
audioIcon: h(SoundOutlined),
16+
audioDisabledIcon: h(SoundOutlined, { style: { color: 'gray' } }),
17+
audioRecordingIcon: h(EllipsisOutlined)
18+
}}
19+
onSubmit={() => {
20+
message.success('Send message successfully!');
21+
}}
22+
/>
23+
);
24+
};
25+
26+
defineRender(() => {
27+
return (
28+
<App>
29+
<Demo />
30+
</App>
31+
)
32+
});
33+
34+
</script>

src/sender/Sender.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,17 @@ const onContentMouseDown: MouseEventHandler = (e) => {
245245
const actionNode = computed(() => {
246246
let _actionNode: VNode | false = (
247247
<Flex class={`${actionListCls.value}-presets`}>
248-
{allowSpeech && <SpeechButton />}
248+
{allowSpeech && (
249+
<SpeechButton
250+
{...(typeof allowSpeech === 'object' ?
251+
{
252+
audioIcon: allowSpeech.audioIcon,
253+
audioDisabledIcon: allowSpeech.audioDisabledIcon,
254+
audioRecordingIcon: allowSpeech.audioRecordingIcon
255+
} : {}
256+
)}
257+
/>
258+
)}
249259
{/* Loading or Send */}
250260
{loading ? <LoadingButton /> : <SendButton />}
251261
</Flex>

src/sender/components/SpeechButton/index.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,36 @@ import { AudioMutedOutlined, AudioOutlined } from '@ant-design/icons-vue';
44
import type { AntdButtonProps } from '../../interface';
55
import ActionButton from '../ActionButton/index.vue';
66
import { useActionButtonContextInject } from '../ActionButton/context';
7-
87
import RecordingIcon from './RecordingIcon.vue';
98
import { computed } from 'vue';
109
1110
defineOptions({ name: 'AXSenderSpeechButton' });
1211
13-
const { type = 'text', disabled = undefined, ...restProps} = defineProps<AntdButtonProps>();
12+
const { type = 'text',
13+
disabled = undefined,
14+
audioIcon = (<AudioOutlined />),
15+
audioDisabledIcon = (<AudioMutedOutlined />),
16+
audioRecordingIcon = undefined,
17+
...restProps
18+
} = defineProps<AntdButtonProps>();
1419
1520
const context = useActionButtonContextInject();
21+
1622
const { token } = theme.useToken();
1723
1824
const speechRecording = computed(() => context.value.speechRecording);
25+
1926
const prefixCls = computed(() => context.value.prefixCls);
2027
2128
const icon = computed(() => {
2229
2330
let myIcon;
2431
if (speechRecording.value) {
25-
myIcon = <RecordingIcon className={`${prefixCls.value}-recording-icon`} />;
32+
myIcon = audioRecordingIcon ? (audioRecordingIcon) : (<RecordingIcon className={`${prefixCls.value}-recording-icon`} />)
2633
} else if (context.value.onSpeechDisabled) {
27-
myIcon = <AudioMutedOutlined />;
34+
myIcon = audioDisabledIcon
2835
} else {
29-
myIcon = <AudioOutlined />;
36+
myIcon = audioIcon
3037
}
3138
return myIcon
3239
})

src/sender/interface.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type ClipboardEventHandler = (e: ClipboardEvent) => void;
1717

1818
export type ChangeEvent = Event & {
1919
target: {
20-
value?: string | undefined;
20+
value?: string | undefined;
2121
};
2222
}
2323

@@ -118,6 +118,9 @@ export interface SenderHeaderProps {
118118

119119
export interface RecordingIconProps {
120120
className?: string;
121+
audioIcon?: ButtonProps['icon'];
122+
audioDisabledIcon?: ButtonProps['icon'];
123+
audioRecordingIcon?: ButtonProps['icon'];
121124
}
122125

123126
export interface ActionButtonContextProps {
@@ -152,6 +155,9 @@ export interface AntdButtonProps {
152155
title?: ButtonProps['title'];
153156
onClick?: ButtonProps['onClick'];
154157
onMousedown?: ButtonProps['onMousedown'];
158+
audioIcon?: ButtonProps['icon'] | VNode;
159+
audioDisabledIcon?: ButtonProps['icon'] | VNode
160+
audioRecordingIcon?: ButtonProps['icon'] | VNode;
155161
}
156162

157163
export interface ActionButtonProps extends AntdButtonProps {

src/sender/useSpeech.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ButtonProps } from 'ant-design-vue';
12
import useMergedState from '../_util/hooks/useMergedState';
23
import { computed, ref, watchEffect, type MaybeRefOrGetter, toValue, onWatcherCleanup, type ComputedRef, type Ref } from 'vue';
34

@@ -10,7 +11,10 @@ if (!SpeechRecognition && typeof window !== 'undefined') {
1011

1112
export type ControlledSpeechConfig = {
1213
recording?: boolean;
13-
onRecordingChange: (recording: boolean) => void;
14+
onRecordingChange?: (recording: boolean) => void;
15+
audioIcon?: ButtonProps['icon'];
16+
audioDisabledIcon?: ButtonProps['icon']
17+
audioRecordingIcon?: ButtonProps['icon'];
1418
};
1519

1620
export type AllowSpeech = boolean | ControlledSpeechConfig;

0 commit comments

Comments
 (0)