From a550342eb41dd0b5e42ad529b806a2a77ba66fcb Mon Sep 17 00:00:00 2001 From: zydemail <1054055589@qq.com> Date: Mon, 9 Feb 2026 11:40:11 +0800 Subject: [PATCH 01/42] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81ChatThinking?= =?UTF-8?q?=E7=BB=84=E4=BB=B6content=E6=8F=92=E6=A7=BD=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E6=80=9D=E8=80=83=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/pro-components/chat/chat-thinking/README.md | 8 +++++++- .../chat/chat-thinking/_example/layout/index.js | 4 ++++ .../chat/chat-thinking/_example/layout/index.wxml | 6 +++++- .../chat/chat-thinking/chat-thinking.less | 5 ++++- .../chat/chat-thinking/chat-thinking.wxml | 5 ++++- .../chat/chat-thinking/README.md | 8 +++++++- .../chat/chat-thinking/_example/layout/index.vue | 12 ++++++++++-- .../chat/chat-thinking/chat-thinking.less | 5 ++++- .../chat/chat-thinking/chat-thinking.vue | 5 ++++- 9 files changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/pro-components/chat/chat-thinking/README.md b/packages/pro-components/chat/chat-thinking/README.md index 91c874a24d..fb130713cc 100644 --- a/packages/pro-components/chat/chat-thinking/README.md +++ b/packages/pro-components/chat/chat-thinking/README.md @@ -44,7 +44,7 @@ isComponent: true ### 03 组件样式 支持通过`layout`来设置思考过程的布局方式 - +支持通过`content`插槽自定义思考过程显示内容 {{ layout }} ## API @@ -67,3 +67,9 @@ status | String | pending | 必需。思考状态。可选项:complete/stop/er 名称 | 参数 | 描述 -- | -- | -- collapsed-change | `(value: Boolean)` | 切换折叠面板时触发 + +### ChatThinking Slots + +名称 | 描述 +-- | -- +content | 自定义 `content` 显示内容 diff --git a/packages/pro-components/chat/chat-thinking/_example/layout/index.js b/packages/pro-components/chat/chat-thinking/_example/layout/index.js index 3a359f0d0b..f7698f5223 100644 --- a/packages/pro-components/chat/chat-thinking/_example/layout/index.js +++ b/packages/pro-components/chat/chat-thinking/_example/layout/index.js @@ -6,5 +6,9 @@ Component({ text: '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说, 保持原来的运动状态。', title: '已深度思考(用时19秒)', }, + title2: { + title: '已深度思考(用时19秒)', + }, + text2: '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说, 保持原来的运动状态。' }, }); diff --git a/packages/pro-components/chat/chat-thinking/_example/layout/index.wxml b/packages/pro-components/chat/chat-thinking/_example/layout/index.wxml index f0c241b0f8..8745be306f 100644 --- a/packages/pro-components/chat/chat-thinking/_example/layout/index.wxml +++ b/packages/pro-components/chat/chat-thinking/_example/layout/index.wxml @@ -1,7 +1,11 @@ block 样式 - + + + {{text2}} + + border 样式 diff --git a/packages/pro-components/chat/chat-thinking/chat-thinking.less b/packages/pro-components/chat/chat-thinking/chat-thinking.less index 314f6b5ea7..fe7c90e57d 100644 --- a/packages/pro-components/chat/chat-thinking/chat-thinking.less +++ b/packages/pro-components/chat/chat-thinking/chat-thinking.less @@ -56,9 +56,12 @@ font-size: @font-size-base; line-height: 170%; margin-top: @spacer-1; - white-space: pre-wrap; overflow-y: auto; + &__inner { + white-space: pre-wrap; + } + &--border { border-left: 4rpx solid @component-stroke; padding-left: @spacer; diff --git a/packages/pro-components/chat/chat-thinking/chat-thinking.wxml b/packages/pro-components/chat/chat-thinking/chat-thinking.wxml index 7d412d4619..4017dc232e 100644 --- a/packages/pro-components/chat/chat-thinking/chat-thinking.wxml +++ b/packages/pro-components/chat/chat-thinking/chat-thinking.wxml @@ -22,6 +22,9 @@ /> - + diff --git a/packages/uniapp-pro-components/chat/chat-thinking/README.md b/packages/uniapp-pro-components/chat/chat-thinking/README.md index 88a61f3b56..14c510de62 100644 --- a/packages/uniapp-pro-components/chat/chat-thinking/README.md +++ b/packages/uniapp-pro-components/chat/chat-thinking/README.md @@ -34,7 +34,7 @@ import TChatThinking from '@tdesign/uniapp-chat/chat-thinking/chat-thinking.vue' ### 03 组件样式 支持通过`layout`来设置思考过程的布局方式 - +支持通过`content`插槽自定义思考过程显示内容 {{ layout }} ## API @@ -57,6 +57,12 @@ status | String | pending | 必需。思考状态。可选项:complete/stop/er -- | -- | -- collapsed-change | `(value: Boolean)` | 切换折叠面板时触发 +### ChatThinking Slots + +名称 | 描述 +-- | -- +content | 自定义 `content` 显示内容 + ### CSS Variables 组件提供了下列 CSS 变量,可用于自定义样式。 diff --git a/packages/uniapp-pro-components/chat/chat-thinking/_example/layout/index.vue b/packages/uniapp-pro-components/chat/chat-thinking/_example/layout/index.vue index 7935c1666e..0ad14cdfa6 100644 --- a/packages/uniapp-pro-components/chat/chat-thinking/_example/layout/index.vue +++ b/packages/uniapp-pro-components/chat/chat-thinking/_example/layout/index.vue @@ -6,10 +6,14 @@ + > + + @@ -42,6 +46,10 @@ export default { text: '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说, 保持原来的运动状态。', title: '已深度思考(用时19秒)', }, + title2: { + title: '已深度思考(用时19秒)', + }, + text2: '嗯,用户问牛顿第一定律是不是适用于所有参考系。首先,我得先回忆一下牛顿第一定律的内容。牛顿第一定律,也就是惯性定律,说物体在没有外力作用时会保持静止或匀速直线运动。也就是说, 保持原来的运动状态。', }; }, created() {}, diff --git a/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.less b/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.less index fc22efa9c8..2b57ec84db 100644 --- a/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.less +++ b/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.less @@ -56,9 +56,12 @@ font-size: @font-size-base; line-height: 170%; margin-top: @spacer-1; - white-space: pre-wrap; overflow-y: auto; + &__inner { + white-space: pre-wrap; + } + &--border { border-left: 4rpx solid @component-stroke; padding-left: @spacer; diff --git a/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue b/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue index c1cbe70048..7440cc0179 100644 --- a/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue +++ b/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue @@ -31,7 +31,10 @@ :class="tools.cls(classPrefix + '__bd', [layout])" :style="contentStyle" > - {{ content.text || '' }} + + {{ content.text }} + + From c98f5e41c959b60f52c922084a24a8245ba1b3b6 Mon Sep 17 00:00:00 2001 From: zydemail <1054055589@qq.com> Date: Mon, 9 Feb 2026 14:24:56 +0800 Subject: [PATCH 02/42] =?UTF-8?q?refactor:=20=E4=B8=BA=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E5=86=85=E9=83=A8=E8=A7=86=E5=9B=BE=E6=B7=BB=E5=8A=A0=E7=A9=BA?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E7=B1=BB=E5=90=8D=E5=8F=82=E6=95=B0=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E7=BB=93=E6=9E=84=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uniapp-pro-components/chat/chat-thinking/chat-thinking.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue b/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue index 4acc11735f..3cf1e7a678 100644 --- a/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue +++ b/packages/uniapp-pro-components/chat/chat-thinking/chat-thinking.vue @@ -31,7 +31,7 @@ :class="tools.cls(classPrefix + '__bd', [layout])" :style="contentStyle" > - + {{ content.text }} From 9703e491e82c614489540281d15b2377634b8f69 Mon Sep 17 00:00:00 2001 From: CodeCarfter <43510057+zhangjiaoalice@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:24:24 +0800 Subject: [PATCH 03/42] Feature/add chat record (#4281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add chat-record component 新增chat-record 语音输入组件 * feat: 新增语音书输入插件 BREAKING CHANGE: 新增语音输入组件 * feat: 新增语音输入组件 * feat(chat-record 组件样式调整): chat-record 组件样式调整 * feat(chat-record 组件样式优化): chat-record 组件样式优化 * feat: 更新chat-record 组件文档 * feat: 更新chat-record 组件文档 --- .../chat/chat-record/README.en-US.md | 110 +++ .../pro-components/chat/chat-record/README.md | 93 +++ .../chat/chat-record/_example/base/index.json | 9 + .../chat/chat-record/_example/base/index.ts | 97 +++ .../chat/chat-record/_example/base/index.wxml | 45 ++ .../chat/chat-record/_example/base/index.wxss | 84 +++ .../chat-record/_example/chat-record.json | 6 + .../chat-record/_example/chat-record.less | 0 .../chat/chat-record/_example/chat-record.ts | 1 + .../chat-record/_example/chat-record.wxml | 11 + .../chat/chat-record/chat-record.json | 4 + .../chat/chat-record/chat-record.less | 637 ++++++++++++++++++ .../chat/chat-record/chat-record.ts | 622 +++++++++++++++++ .../chat/chat-record/chat-record.wxml | 109 +++ .../chat/chat-record/chat-record.wxs | 14 + .../pro-components/chat/chat-record/index.ts | 2 + .../pro-components/chat/chat-record/props.ts | 24 + .../pro-components/chat/chat-record/type.ts | 55 ++ .../pro-components/chat/chat-sender/README.md | 1 + .../chat/chat-sender/chat-sender.less | 2 + .../chat/chat-sender/chat-sender.wxml | 49 +- .../pro-components/chat/chat-sender/props.ts | 5 + .../pro-components/chat/chat-sender/type.ts | 8 + .../site/site.config.mjs | 9 + packages/tdesign-miniprogram/example/app.json | 103 ++- .../example/pages/home/data/chat.ts | 4 + .../example/project.config.json | 14 +- .../example/src/pages/home/data/chat.json | 4 + 28 files changed, 2081 insertions(+), 41 deletions(-) create mode 100644 packages/pro-components/chat/chat-record/README.en-US.md create mode 100644 packages/pro-components/chat/chat-record/README.md create mode 100644 packages/pro-components/chat/chat-record/_example/base/index.json create mode 100644 packages/pro-components/chat/chat-record/_example/base/index.ts create mode 100644 packages/pro-components/chat/chat-record/_example/base/index.wxml create mode 100644 packages/pro-components/chat/chat-record/_example/base/index.wxss create mode 100644 packages/pro-components/chat/chat-record/_example/chat-record.json create mode 100644 packages/pro-components/chat/chat-record/_example/chat-record.less create mode 100644 packages/pro-components/chat/chat-record/_example/chat-record.ts create mode 100644 packages/pro-components/chat/chat-record/_example/chat-record.wxml create mode 100644 packages/pro-components/chat/chat-record/chat-record.json create mode 100644 packages/pro-components/chat/chat-record/chat-record.less create mode 100644 packages/pro-components/chat/chat-record/chat-record.ts create mode 100644 packages/pro-components/chat/chat-record/chat-record.wxml create mode 100644 packages/pro-components/chat/chat-record/chat-record.wxs create mode 100644 packages/pro-components/chat/chat-record/index.ts create mode 100644 packages/pro-components/chat/chat-record/props.ts create mode 100644 packages/pro-components/chat/chat-record/type.ts diff --git a/packages/pro-components/chat/chat-record/README.en-US.md b/packages/pro-components/chat/chat-record/README.en-US.md new file mode 100644 index 0000000000..eac998ec83 --- /dev/null +++ b/packages/pro-components/chat/chat-record/README.en-US.md @@ -0,0 +1,110 @@ +--- +title: ChatRecord +description: A component for displaying chat conversation history with features like time grouping, scroll loading, and message interactions. +spline: base +isComponent: true +--- + +## Import + +For global import, configure in `app.json` in the miniprogram root directory. For local import, configure in the `index.json` of the page or component where you need to import. + +```json +"usingComponents": { + "t-chat-record": "tdesign-miniprogram/chat-record/chat-record" +} +``` + +## Usage + +### 01 Component Types + +#### Basic Type + +Display a basic chat record list. + +```xml + +``` + +#### With Time Grouping + +Support automatic time grouping display based on time intervals. + +```xml + +``` + +#### Scroll Load More + +Support loading more history when scrolling to the top. + +```xml + +``` + +#### Custom Message Rendering + +Support custom message rendering through slots. + +```xml + + + + + +``` + +## API + +### ChatRecord Props + +Name | Type | Default | Description | Required +-- | -- | -- | -- | -- +style | Object | - | Style | N +custom-style | Object | - | Style, generally used for virtualized component node scenarios | N +records | Array | [] | Chat record data list. TS Type: `ChatRecordItem[]` | N +loading | Boolean | false | Whether to show loading state | N +finished | Boolean | false | Whether all data has been loaded | N +loading-text | String | Loading... | Loading prompt text | N +finished-text | String | No more data | Finished loading prompt text | N +empty-text | String | No chat records | Empty state prompt text | N +show-time-group | Boolean | true | Whether to show time grouping | N +time-group-interval | Number | 5 | Time grouping interval (minutes) | N +virtual-scroll | Boolean | false | Whether to enable virtual scrolling | N +scroll-view-height | String | 100vh | Scroll view height | N +auto-scroll-to-bottom | Boolean | true | Whether to auto scroll to bottom | N + +### ChatRecord Events + +Name | Parameters | Description +-- | -- | -- +loadmore | - | Triggered when scrolling to the top, used to load more history +scroll | `(detail: ScrollDetail)` | Triggered when scrolling +message-click | `(detail: { record: ChatRecordItem })` | Triggered when clicking a message +message-long-press | `(detail: { record: ChatRecordItem })` | Triggered when long-pressing a message + +### ChatRecord Slots + +Name | Description +-- | -- +empty | Custom empty state content +message | Custom message content, receives record parameter + +### ChatRecord External Style Classes + +Class Name | Description +-- | -- +t-class | Root node style class +t-class-empty | Empty state style class +t-class-time | Time grouping style class +t-class-message | Message item style class diff --git a/packages/pro-components/chat/chat-record/README.md b/packages/pro-components/chat/chat-record/README.md new file mode 100644 index 0000000000..bffa4bc888 --- /dev/null +++ b/packages/pro-components/chat/chat-record/README.md @@ -0,0 +1,93 @@ +--- +title: ChatRecord 语音输入 +description: 用于聊天场景的语音输入组件,支持语音转文字、录音时长控制等功能。 +spline: base +isComponent: true +--- + +## 引入 + +全局引入,在 miniprogram 根目录下的`app.json`中配置,局部引入,在需要引入的页面或组件的`index.json`中配置。 + +```json +"usingComponents": { + "t-chat-record": "tdesign-miniprogram/chat-record/chat-record" +} +``` + +## 前置配置 + +### 1. 添加插件声明 + +在 `app.json` 中声明微信同声传译插件: + +```json +{ + "plugins": { + "WechatSI": { + "version": "0.3.6", + "provider": "wx069ba97219f66d99" + } + } +} +``` + +### 2. 麦克风权限 + +使用语音输入需要用户授权麦克风权限。组件会自动处理授权流程,但开发者需要了解以下场景: + +#### 首次使用 +- 组件会自动调用 `wx.authorize` 申请麦克风权限 +- 用户同意后即可正常使用 + +#### 用户拒绝授权 +- 如果用户点击拒绝,会显示"请授权麦克风权限"提示 +- 点击提示区域会引导用户前往设置页面开启权限 + +#### 权限问题排查 + +如果在小程序设置页面看不到麦克风权限选项: + +1. **检查微信 App 权限** + - 进入手机系统设置 > 应用管理 > 微信 + - 确保微信有麦克风权限 + +2. **检查小程序授权** + - 微信中下拉打开最近使用小程序列表 + - 长按目标小程序 > 关于小程序 > 设置 + - 查看是否有麦克风权限选项 + +3. **重新授权** + - 删除小程序后重新搜索进入 + - 首次点击语音输入时会重新触发授权弹窗 + +4. **真机调试** + - 模拟器无法测试录音功能 + - 必须使用真机预览或体验版测试 + +## API + +### ChatRecord Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +useSpeechNoAuthSlot | Boolean | false | 是否启用语音输入无权限时显示插槽 | N +useSpeechInputSlot | Boolean | false | 是否启用语音输入时显示插槽 | N +autoSendHeight| Number | 0 | 自动发送消息的高度 | N +speechInput | slot | false | 语音输入按钮插槽 | N +speechNoAuth | slot | false | 语音授权按钮插槽 | N + + +### ChatRecord Events + +名称 | 参数 | 描述 +-- | -- | -- +recognize | msg: string | 识别到的文本内容 + +### ChatRecord Slots + +名称 | 描述 +-- | -- +speechInput | 语音输入按钮插槽 +speechNoAuth | 语音授权按钮插槽 + diff --git a/packages/pro-components/chat/chat-record/_example/base/index.json b/packages/pro-components/chat/chat-record/_example/base/index.json new file mode 100644 index 0000000000..5e834db8a6 --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/base/index.json @@ -0,0 +1,9 @@ +{ + "component": true, + "styleIsolation": "shared", + "usingComponents": { + "t-navbar": "tdesign-miniprogram/navbar/navbar", + "t-chat-sender": "tdesign-miniprogram/chat-sender/chat-sender", + "t-chat-record": "tdesign-miniprogram/chat-record/chat-record" + } +} diff --git a/packages/pro-components/chat/chat-record/_example/base/index.ts b/packages/pro-components/chat/chat-record/_example/base/index.ts new file mode 100644 index 0000000000..1a031ee2ce --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/base/index.ts @@ -0,0 +1,97 @@ +Page({ + /** + * 页面的初始数据 + */ + data: { + query: '', // 输入框内容 + placeholder: '请输入内容', // 输入框占位符 + loading: false, // 发送按钮加载状态 + showVoice: false, // 是否显示语音输入组件 + }, + + /** + * 返回上一页 + */ + navigateBack() { + wx.navigateBack({ + delta: 1, + }); + }, + + /** + * 处理输入框内容变化 + */ + handleInput(e) { + this.setData({ + query: e.detail.value, + }); + }, + + /** + * 切换语音输入显示状态 + */ + handleVoice() { + this.setData({ + showVoice: !this.data.showVoice, + }); + }, + + /** + * 语音识别回调 + * @param {Object} e - 事件对象 + */ + handleRecognize(e) { + const voiceMsg = e.detail.msg; + console.log('语音识别结果:', voiceMsg); + + // 将语音识别结果设置到输入框中 + this.setData({ + query: voiceMsg, + showVoice: false, // 识别完成后隐藏语音输入组件 + }); + + // 提示用户 + wx.showToast({ + title: '识别成功', + icon: 'success', + duration: 1500, + }); + }, + + /** + * 发送消息 + */ + handleSend() { + const { query, loading } = this.data; + + // 如果正在加载或内容为空,不处理 + if (loading || !query.trim()) { + if (!query.trim()) { + wx.showToast({ + title: '请输入内容', + icon: 'none', + duration: 1500, + }); + } + return; + } + // 设置加载状态 + this.setData({ + loading: true, + }); + + // 模拟发送请求 + setTimeout(() => { + this.setData({ + loading: false, + query: '', // 清空输入框 + }); + + wx.showToast({ + title: '发送成功', + icon: 'success', + duration: 1500, + }); + }, 1000); + }, +}); diff --git a/packages/pro-components/chat/chat-record/_example/base/index.wxml b/packages/pro-components/chat/chat-record/_example/base/index.wxml new file mode 100644 index 0000000000..9b907e6da3 --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/base/index.wxml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + 按住说话 + + 请授权麦克风权限 + + + + + + + + + + + + + diff --git a/packages/pro-components/chat/chat-record/_example/base/index.wxss b/packages/pro-components/chat/chat-record/_example/base/index.wxss new file mode 100644 index 0000000000..51c640ae36 --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/base/index.wxss @@ -0,0 +1,84 @@ +.demo-base-container { + padding: 56rpx 0 0 0; + background-color: var(--td-bg-color-container); + height: 488rpx; + position: relative; +} + +/* 聊天发送器包装器 */ +.chat-sender-demo-wrapper { + margin-bottom: 32rpx; + /* border: 2rpx solid #e5e5e5; */ + border-radius: 8rpx; + overflow: hidden; +} + +.chat-sender-height-limit { + height: 72rpx; + padding: 0 24rpx; + display: flex; + justify-content: space-between; + align-items: center; +} + +.chat-sender-height-left-limit { + height: 70rpx; + width: 70rpx; + border-top: 1px var(--td-component-stroke) dashed; + border-left: 1px var(--td-component-stroke) dashed; + border-top-left-radius: 32rpx; +} +.chat-sender-height-right-limit { + height: 70rpx; + width: 70rpx; + border-top: 1px var(--td-component-stroke) dashed; + border-right: 1px var(--td-component-stroke) dashed; + border-top-right-radius: 32rpx; +} +.chat-sender-placeholder { + font-size: 32rpx; + font-weight: 600; + color: var(--demo-chat-sender-placeholder); + text-align: center; + height: 48rpx; +} + +.chat-sender-wrapper { + position: absolute; + width: 100%; + bottom: 0rpx; + background-color: var(--td-bg-color-container); +} + +.demo-footer { + height: 32rpx; + width: 100%; + text-align: center; + font-size: 20rpx; + line-height: 32rpx; + color: var(--td-text-color-placeholder); + position: absolute; + bottom: 32rpx; +} + +.demo-footer-prefix { + display: flex; + align-items: center; +} + +/* 语音输入按钮样式 */ +.voice-input-button { + width: 100%; + height: 96rpx; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--td-bg-color-container); + border-radius: 8rpx; + color: var(--td-text-color-primary); + font-size: 28rpx; +} + +.voice-input-text { + margin-left: 8rpx; +} diff --git a/packages/pro-components/chat/chat-record/_example/chat-record.json b/packages/pro-components/chat/chat-record/_example/chat-record.json new file mode 100644 index 0000000000..36c6e4ce93 --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/chat-record.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "ChatRecord", + "usingComponents": { + "base": "./base" + } +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-record/_example/chat-record.less b/packages/pro-components/chat/chat-record/_example/chat-record.less new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/pro-components/chat/chat-record/_example/chat-record.ts b/packages/pro-components/chat/chat-record/_example/chat-record.ts new file mode 100644 index 0000000000..560d44d430 --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/chat-record.ts @@ -0,0 +1 @@ +Page({}); diff --git a/packages/pro-components/chat/chat-record/_example/chat-record.wxml b/packages/pro-components/chat/chat-record/_example/chat-record.wxml new file mode 100644 index 0000000000..364a909f7e --- /dev/null +++ b/packages/pro-components/chat/chat-record/_example/chat-record.wxml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/pro-components/chat/chat-record/chat-record.json b/packages/pro-components/chat/chat-record/chat-record.json new file mode 100644 index 0000000000..66165257fc --- /dev/null +++ b/packages/pro-components/chat/chat-record/chat-record.json @@ -0,0 +1,4 @@ +{ + "component": true, + "styleIsolation": "apply-shared" +} \ No newline at end of file diff --git a/packages/pro-components/chat/chat-record/chat-record.less b/packages/pro-components/chat/chat-record/chat-record.less new file mode 100644 index 0000000000..dd10d18a35 --- /dev/null +++ b/packages/pro-components/chat/chat-record/chat-record.less @@ -0,0 +1,637 @@ +@import '../../../components/common/style/base.less'; + +@chat-record: ~'@{prefix}-chat-record'; + +@chat-record-primary-color: #126dff; +@chat-record-primary-color-light: rgba(18, 109, 255, 0.6); +@chat-record-primary-gradient: linear-gradient( + 180deg, + @chat-record-primary-color 0%, + @chat-record-primary-color-light 100% +); + +@chat-record-error-color: #ff5729; +@chat-record-error-color-light: rgba(255, 87, 41, 0.6); +@chat-record-error-gradient: linear-gradient(180deg, @chat-record-error-color 0%, @chat-record-error-color-light 100%); + +@chat-record-text-primary: rgba(0, 0, 0, 0.9); +@chat-record-text-secondary: rgba(0, 0, 0, 0.4); +@chat-record-text-tertiary: rgba(0, 0, 0, 0.26); +@chat-record-text-white: #fff; + +@chat-record-bg-white: #fff; +@chat-record-bg-mask: rgba(255, 255, 255, 0.54); +@chat-record-bg-dark: rgba(0, 0, 0, 0.03); + +@chat-record-padding-vertical: 24rpx; +@chat-record-padding-horizontal: 40rpx; +@chat-record-margin-bottom: 60rpx; +@chat-record-margin-bottom-small: 48rpx; + +@chat-record-border-radius-small: 16px; +@chat-record-border-radius-medium: 48rpx; +@chat-record-border-radius-large: 64rpx; +@chat-record-border-radius-full: 50%; + +@chat-record-border-width: 2rpx; +@chat-record-border-color: @chat-record-bg-white; + +@chat-record-font-size-large: 34rpx; +@chat-record-font-size-medium: 28rpx; +@chat-record-line-height: 42rpx; +@chat-record-line-height-large: 58rpx; +@chat-record-letter-spacing: 1.02rpx; + +@chat-record-btn-height: 96rpx; +@chat-record-btn-width-small: 64rpx; +@chat-record-btn-icon-size: 48rpx; + +@chat-record-ani-size: 256rpx; +@chat-record-ani-inner-size: 160rpx; +@chat-record-ani-main-size: 176rpx; + +@chat-record-footer-height: 348rpx; +@chat-record-footer-bg-height: 300rpx; + +@chat-record-transition-fast: 0.25s linear; +@chat-record-transition-normal: 0.6s; +@chat-record-transition-slow: 1s; +@chat-record-animation-duration: 1.5s; + +@chat-record-box-shadow: 0 8rpx 32rpx 0 rgba(18, 109, 255, 0.12); + +@chat-record-asset-base-url: 'https://static.wecity.qq.com/webrebuildmysscard-mini/250702-aigc-audio'; + +.@{chat-record} { + padding: @chat-record-padding-vertical 0; + width: -webkit-fill-available; + + .@{chat-record}-hook { + width: 100%; + color: @chat-record-text-primary; + text-align: center; + font-size: @chat-record-font-size-large; + font-style: normal; + font-weight: 500; + line-height: @chat-record-line-height; + letter-spacing: @chat-record-letter-spacing; + border-radius: @chat-record-border-radius-small; + border: @chat-record-border-width solid @chat-record-border-color; + } + + .@{chat-record}-audio-input { + &.show { + .@{chat-record}-audio-input__mask { + opacity: 1; + pointer-events: auto; + } + + .@{chat-record}-audio-input__main { + opacity: 1; + pointer-events: auto; + } + } + + &.unknow { + .@{chat-record}-audio-input__ft__tips { + color: @chat-record-primary-color; + } + + .@{chat-record}-audio-input__ft__tips__inner { + display: none; + } + + .speak-btn { + display: flex; + } + + .speak-close-btn { + display: flex; + } + + .audio-loading-icon { + display: none; + } + } + + &.cancel { + .@{chat-record}-audio-input__ft__tips__inner { + color: @chat-record-error-color; + } + + .@{chat-record}-audio-input__ft__tips { + color: @chat-record-error-color; + } + + .@{chat-record}-audio-input__ft__bg { + background: url('@{chat-record-asset-base-url}/img-ctrl-bg-red.png') no-repeat center; + background-size: cover; + } + + .audio-loading-icon { + .audio-loading-dot { + background: @chat-record-error-gradient; + } + } + } + + &.complete { + .@{chat-record}-audio-input__ft__tips__inner { + display: none; + } + + .@{chat-record}-audio-input__ft__bg { + display: none; + } + + .@{chat-record}-audio-input__ft__ct { + display: flex; + } + + .audio-loading-icon { + display: none; + } + + .@{chat-record}-audio-inputt__ft__tips { + display: none; + } + } + + &.cover-ng-bar { + position: fixed; + left: 0; + top: 0; + z-index: 99999999; + } + + &__mask { + width: 100vw; + height: 100vh; + position: fixed; + left: 0; + top: 0; + z-index: 98; + background: @chat-record-bg-mask; + backdrop-filter: blur(12px); + opacity: 0; + pointer-events: none; + transition: opacity @chat-record-transition-fast; + } + + &__main { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 99; + opacity: 0; + pointer-events: none; + transition: opacity @chat-record-transition-fast; + } + + &-ani { + display: flex; + align-items: center; + width: @chat-record-ani-size; + height: @chat-record-ani-size; + margin: 0 auto @chat-record-margin-bottom; + position: relative; + overflow: hidden; + + .ani-cir { + width: @chat-record-ani-size; + height: @chat-record-ani-size; + position: relative; + z-index: 3; + + &.diff-start { + .ani-cir-2 { + animation: diff @chat-record-transition-slow linear infinite; + } + + .ani-cir-3 { + animation: diff @chat-record-transition-slow linear 0.5s infinite; + } + + .ani-cir-4 { + animation: diff @chat-record-transition-slow linear 1s infinite; + } + } + + .ani-cir-1, + .ani-cir-2, + .ani-cir-3, + .ani-cir-4 { + position: absolute; + left: 48rpx; + top: 48rpx; + width: @chat-record-ani-inner-size; + height: @chat-record-ani-inner-size; + background: url('@{chat-record-asset-base-url}/img-border.png') no-repeat center; + background-size: contain; + } + + .ani-cir-1 { + animation: bg-turn 3s linear infinite; + } + + @keyframes diff { + 0% { + transform: scale(1); + opacity: 0.33; + } + + 100% { + transform: scale(1.61); + opacity: 0; + } + } + } + + .ani-wrap { + width: @chat-record-ani-inner-size; + height: @chat-record-ani-inner-size; + z-index: 2; + position: absolute; + left: 48rpx; + top: 48rpx; + overflow: hidden; + border-radius: @chat-record-border-radius-full; + + .ani-inner { + width: @chat-record-ani-inner-size; + height: @chat-record-ani-inner-size; + position: absolute; + left: 0; + top: 0; + border-radius: @chat-record-border-radius-full; + z-index: 2; + background: url('@{chat-record-asset-base-url}/img-clr-bg.png') no-repeat center; + background-size: contain; + animation: bg-turn 5s linear infinite; + } + + @keyframes bg-turn { + 0% { + transform: rotateZ(0deg); + } + + 100% { + transform: rotateZ(360deg); + } + } + + .ani-mask { + width: @chat-record-ani-inner-size; + height: @chat-record-ani-inner-size; + position: absolute; + left: 0; + top: 0; + z-index: 3; + background: url('@{chat-record-asset-base-url}/img-ani-mask.png') no-repeat center; + background-size: contain; + } + + .ani-main { + width: @chat-record-ani-main-size; + height: @chat-record-ani-main-size; + position: absolute; + left: -8rpx; + top: 11rpx; + z-index: 4; + background: url('@{chat-record-asset-base-url}/img-ani-spirit-new.png') no-repeat center 0; + background-size: 100% auto; + + &.ani-start { + animation: spirit-start @chat-record-transition-normal steps(14) forwards; + } + + &.ani-end { + background: url('@{chat-record-asset-base-url}/img-ani-spirit-new.png') no-repeat center 100%; + background-size: @chat-record-ani-main-size auto; + animation: spirit-end @chat-record-transition-normal steps(14) forwards; + } + } + + @keyframes spirit-start { + 0% { + background-position: center 0; + } + + 100% { + background-position: center -2464rpx; + } + } + + @keyframes spirit-end { + 0% { + background-position: center -2464rpx; + } + + 100% { + background-position: center 0; + } + } + + @keyframes spirit-start-1 { + 0% { + background-position: center 0; + } + + 100% { + background-position: center -2436rpx; + } + } + + @keyframes spirit-end-1 { + 0% { + background-position: center -2436rpx; + } + + 100% { + background-position: center 0; + } + } + } + } + + &__con { + margin: 0 @chat-record-padding-horizontal 88rpx; + overflow-y: scroll; + + &.disabled { + .@{chat-record}-audio-input__con__inner { + color: @chat-record-text-secondary; + } + + .@{chat-record}-audio-input__con__ta { + color: @chat-record-text-secondary; + } + } + + &__inner { + color: @chat-record-text-primary; + font-size: @chat-record-font-size-large; + font-weight: 500; + line-height: @chat-record-line-height-large; + min-height: 116rpx; + text-align: center; + } + + &__ta { + color: @chat-record-text-primary; + font-size: @chat-record-font-size-large; + font-weight: 500; + line-height: @chat-record-line-height-large; + min-height: 116rpx; + text-align: left; + display: block; + width: 100%; + overflow: scroll; + + // height: auto; + &.txt-limit-5 { + max-height: 120rpx; + } + + &.txt-limit-9 { + max-height: 522rpx; + } + } + } + + .audio-loading-icon { + display: flex; + align-items: center; + height: 24rpx; + position: absolute; + top: 218rpx; + left: 50%; + transform: translateX(-50%); + } + + .audio-loading-dot { + height: 10rpx; + width: 7rpx; + background: @chat-record-primary-gradient; + border-radius: 4rpx; + + & + .audio-loading-dot { + margin-left: 7rpx; + } + + &.dot-1 { + animation: dot-scale 1.5s linear infinite; + } + + &.dot-2 { + transform: scaleY(1.6); + animation: dot-scale-2 1.5s linear infinite; + } + + &.dot-3 { + transform: scaleY(2.4); + animation: dot-scale-3 1.5s linear infinite; + } + } + + @keyframes dot-scale { + 0% { + transform: scaleY(1); + } + + 50% { + transform: scaleY(2.4); + } + + 100% { + transform: scaleY(1); + } + } + + @keyframes dot-scale-2 { + 0% { + transform: scaleY(1.6); + } + + 25% { + transform: scaleY(2.4); + } + + 75% { + transform: scaleY(1); + } + + 100% { + transform: scaleY(1.6); + } + } + + @keyframes dot-scale-3 { + 0% { + transform: scaleY(2.4); + } + + 50% { + transform: scaleY(1); + } + + 100% { + transform: scaleY(2.4); + } + } + + &__ft { + position: relative; + + &__bg { + width: 100%; + height: @chat-record-footer-height; + display: block; + background: url('@{chat-record-asset-base-url}/img-ctrl-bg-new.png') no-repeat center; + background-size: cover; + } + + &__tips { + color: @chat-record-text-tertiary; + text-align: center; + font-size: @chat-record-font-size-medium; + line-height: 36rpx; + } + + &__tips__inner { + position: absolute; + left: 3rpx; + right: 3rpx; + top: 146rpx; + color: @chat-record-primary-color; + text-align: center; + font-size: @chat-record-font-size-medium; + line-height: 36rpx; + } + + &__ct { + display: flex; + align-items: center; + height: 300rpx; + margin-top: 48rpx; + padding: 0 40rpx; + display: none; + + &.keyboard-cover { + height: fit-content; + padding: 0 32rpx 32rpx; + } + } + + &__btn { + border-radius: @chat-record-border-radius-medium; + height: @chat-record-btn-height; + background: @chat-record-bg-white; + box-shadow: @chat-record-box-shadow; + display: flex; + align-items: center; + justify-content: center; + + & + .@{chat-record}-audio-input__ft__btn { + margin-left: 24rpx; + } + + &.btn-send { + flex: 1; + font-size: 40rpx; + font-weight: 500; + background: @chat-record-primary-gradient; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + &.btn-close { + width: @chat-record-btn-height; + flex-shrink: 0; + + .close-icon { + width: @chat-record-btn-icon-size; + height: @chat-record-btn-icon-size; + mask: url('@{chat-record-asset-base-url}/icon-close.svg') no-repeat center; + mask-size: contain; + background: @chat-record-text-tertiary; + } + } + } + + .speak-close-btn { + position: absolute; + left: @chat-record-padding-horizontal; + top: 202rpx; + width: @chat-record-btn-width-small; + height: @chat-record-btn-width-small; + border-radius: @chat-record-border-radius-medium; + border: @chat-record-border-width solid @chat-record-border-color; + background: @chat-record-bg-dark; + box-sizing: border-box; + display: none; + align-items: center; + justify-content: center; + + .close-icon { + width: @chat-record-btn-icon-size; + height: @chat-record-btn-icon-size; + border-radius: @chat-record-border-radius-full; + mask: url('@{chat-record-asset-base-url}/icon-close.svg') no-repeat center; + mask-size: contain; + background: @chat-record-text-tertiary; + } + } + + .speak-btn { + position: absolute; + left: 50%; + top: 210rpx; + transform: translateX(-50%); + display: flex; + align-items: center; + justify-content: center; + display: none; + // pointer-events: none; + + .speak-icon { + width: @chat-record-btn-icon-size; + height: @chat-record-btn-icon-size; + background: url('@{chat-record-asset-base-url}/icon-vol.svg') no-repeat center; + background-size: contain; + margin-right: 4rpx; + } + + .tips-txt { + font-size: @chat-record-font-size-large; + font-weight: 500; + background: @chat-record-primary-gradient; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + } + + .audio-volume-icon { + width: @chat-record-btn-icon-size; + height: @chat-record-btn-icon-size; + position: absolute; + top: 60rpx; + left: 50%; + z-index: 2; + transform: translateX(-50%); + mask: url('@{chat-record-asset-base-url}/icon-play.svg') no-repeat center; + mask-size: contain; + background: @chat-record-text-secondary; + + &.volume-w { + mask: url('@{chat-record-asset-base-url}/icon-play.svg') no-repeat center; + mask-size: contain; + background: @chat-record-text-white; + } + } + } + } +} diff --git a/packages/pro-components/chat/chat-record/chat-record.ts b/packages/pro-components/chat/chat-record/chat-record.ts new file mode 100644 index 0000000000..618e7e7219 --- /dev/null +++ b/packages/pro-components/chat/chat-record/chat-record.ts @@ -0,0 +1,622 @@ +import { SuperComponent, wxComponent } from '../../../components/common/src/index'; +import config from '../../../components/common/config'; +import props from './props'; +import { TouchStatus, RecordStatus, ComponentStatus, CustomFocusEvent, CustomTouchEvent } from './type'; + +declare const wx: any; +declare function requirePlugin(name: string): any; + +const { prefix } = config; +const name = `${prefix}-chat-record`; + +// 插件管理器(延迟加载,错误处理) +let manager: any = null; +const getManager = () => { + if (!manager) { + try { + const plugin = requirePlugin('WechatSI'); + manager = plugin?.getRecordRecognitionManager?.() || null; + } catch (e) { + console.error('WechatSI 插件加载失败:', e); + } + } + return manager; +}; + +@wxComponent() +export default class ChatRecord extends SuperComponent { + options = { + multipleSlots: true, + }; + + properties = props; + + // 实例变量,避免多组件间共享 + private startRecordTimer: ReturnType | null = null; + + private recordTimer: ReturnType | null = null; + + data = { + classPrefix: name, + showMask: false, + touchStatus: 'bottom', + startTime: 0, + recordCountDown: -1, + translateResult: '', + voiceInfo: { + voicePath: '', + duration: 0, + }, + recordStatus: '', + recordAuthSetting: false, + recordAuthStatus: true, + isStarted: false, + bottomHeight: 0, + autoSendHeight: true, + windowHeight: 0, + translateSuccess: false, + showRecordCountDown: false, + status: 'normal' as ComponentStatus, + hasSpeechInputSlot: true, + hasSpeechNoAuthSlot: true, + }; + + observers = { + translateResult() { + this.setData({ + translateSuccess: this.data.translateResult !== '-1' && !!this.data.translateResult, + }); + }, + recordCountDown() { + this.setData({ + showRecordCountDown: this.data.recordCountDown >= 0, + }); + }, + 'touchStatus, recordStatus, translateResult'() { + const translateSuccess = this.data.translateResult !== '-1' && !!this.data.translateResult; + let status: ComponentStatus = 'normal'; + + if (this.data.touchStatus === 'top') { + status = 'cancel'; + } else if (this.data.recordStatus === 'recording') { + status = 'recording'; + } else if (this.data.recordStatus === 'thinking') { + status = 'thinking'; + } else if (this.data.recordStatus === 'stop' && !translateSuccess) { + status = 'unknow'; + } else if (this.data.recordStatus === 'stop' && translateSuccess) { + status = 'complete'; + } else if (this.data.recordStatus === 'error') { + status = 'error'; + } + + this.setData({ status }); + }, + }; + + lifetimes = { + attached() { + this.initRecorderManager(); + this.getWindowHeight(); + }, + detached() { + if (this.recordTimer) { + clearInterval(this.recordTimer); + this.recordTimer = null; + } + if (this.startRecordTimer) { + clearTimeout(this.startRecordTimer); + this.startRecordTimer = null; + } + const mgr = getManager(); + if (mgr) { + mgr.stop(); + } + }, + }; + + /** + * 初始化同声传译插件 + */ + initRecorderManager(): void { + const mgr = getManager(); + if (!mgr) { + console.warn('语音插件未加载,语音功能不可用'); + return; + } + + mgr.onStop = (res): void => { + const { tempFilePath, duration } = res; + + if (this.data.touchStatus === 'top') { + this.resetRecordState(); + return; + } + + this.setData({ + 'voiceInfo.voicePath': tempFilePath, + 'voiceInfo.duration': Math.floor(duration / 1000) || 1, + recordStatus: 'stop' as RecordStatus, + touchStatus: '' as TouchStatus, + }); + }; + + mgr.onStart = (): void => { + this.setData({ + recordStatus: 'thinking' as RecordStatus, + }); + }; + + mgr.onRecognize = (res): void => { + if (res.result && !res.end) { + this.setData({ + recordStatus: 'recording' as RecordStatus, + translateResult: res.result, + }); + } + }; + + mgr.onError = (res): void => { + this.setData({ + recordStatus: 'error' as RecordStatus, + touchStatus: '' as TouchStatus, + translateResult: '-1', + }); + + wx.showToast({ + icon: 'none', + title: res.msg || '录音识别失败,请重试', + duration: 2000, + }); + }; + } + + /** + * 重置录音状态 + */ + resetRecordState(): void { + this.setData({ + showMask: false, + translateResult: '', + recordStatus: '' as RecordStatus, + touchStatus: '' as TouchStatus, + recordCountDown: -1, + startTime: 0, + }); + } + + /** + * 获取窗口高度 + */ + getWindowHeight(): void { + wx.getSystemInfo({ + success: (res) => { + this.setData({ + windowHeight: res.windowHeight, + }); + }, + }); + } + + /** + * 打开设置页授权 + */ + openVoiceSetting(): void { + wx.getSetting({ + success: (res) => { + const authSetting = res.authSetting['scope.record']; + if (authSetting === true) { + // 已授权 + this.setData({ + recordAuthSetting: true, + recordAuthStatus: true, + }); + wx.showToast({ + title: '已授权麦克风', + icon: 'success', + }); + return; + } + + if (authSetting === false) { + // 用户曾经拒绝过,需要引导去设置页面 + wx.showModal({ + title: '需要麦克风权限', + content: '麦克风权限被拒绝,请前往微信设置开启', + confirmText: '去设置', + cancelText: '取消', + success: (modalRes) => { + if (modalRes.confirm) { + wx.openSetting({ + success: (settingRes) => { + const newAuthStatus = !!settingRes.authSetting['scope.record']; + this.setData({ + recordAuthSetting: newAuthStatus, + recordAuthStatus: newAuthStatus, + }); + if (newAuthStatus) { + wx.showToast({ + title: '授权成功', + icon: 'success', + }); + } + }, + fail: (err) => { + console.error('打开设置失败:', err); + wx.showToast({ + title: '打开设置失败', + icon: 'none', + }); + }, + }); + } + }, + }); + } else { + // 未申请过,尝试直接申请 + this.applyAuthWithFallback(); + } + }, + fail: (err) => { + console.error('获取设置失败:', err); + // 降级处理:直接尝试申请 + this.applyAuthWithFallback(); + }, + }); + } + + /** + * 带降级的授权申请 + */ + applyAuthWithFallback(): void { + wx.authorize({ + scope: 'scope.record', + success: () => { + this.setData({ + recordAuthSetting: true, + recordAuthStatus: true, + }); + wx.showToast({ + title: '授权成功', + icon: 'success', + }); + }, + fail: (err) => { + console.error('授权申请失败:', err); + this.handleAuthFail(err); + }, + }); + } + + /** + * 处理授权失败 + */ + handleAuthFail(err: any): void { + // 判断是否是系统权限问题 + const errMsg = err.errMsg || ''; + + if (errMsg.includes('system') || errMsg.includes('denied')) { + // 系统级权限被拒绝 + wx.showModal({ + title: '需要系统麦克风权限', + content: + '请检查手机系统设置:\n\n1. 进入手机「设置」>「应用管理」>「微信」\n2. 开启「麦克风」权限\n3. 返回小程序重试', + showCancel: false, + confirmText: '我知道了', + }); + } else { + // 普通拒绝 + wx.showModal({ + title: '授权失败', + content: '无法使用语音输入功能。请在「设置」中允许小程序使用麦克风。', + confirmText: '去设置', + cancelText: '取消', + success: (res) => { + if (res.confirm) { + wx.openSetting(); + } + }, + }); + } + + this.setData({ + recordAuthSetting: false, + recordAuthStatus: false, + }); + } + + /** + * 获取语音授权状态 + */ + getVoiceAuthSetting(): Promise { + return new Promise((resolve, reject) => { + wx.getSetting({ + success: (res) => { + const authSettings = Object.keys(res.authSetting); + const recordAuthSetting = authSettings.includes('scope.record'); + const recordAuthStatus = !!res.authSetting['scope.record']; + this.setData({ + recordAuthSetting, + recordAuthStatus, + }); + resolve(recordAuthSetting); + }, + fail: () => { + reject(new Error('获取语音授权设置失败')); + }, + }); + }); + } + + /** + * 申请授权 + */ + applyAuth(): Promise { + return new Promise((resolve, reject) => { + wx.authorize({ + scope: 'scope.record', + success: () => { + this.setData({ + recordAuthSetting: true, + recordAuthStatus: true, + }); + resolve(true); + }, + fail: () => { + this.setData({ + recordAuthSetting: false, + recordAuthStatus: false, + }); + reject(new Error('语音授权申请失败')); + }, + }); + }); + } + + /** + * 修改语音转文字结果 + */ + onTranslateResultChange(value: string): void { + this.setData({ + translateResult: value, + }); + } + + /** + * 直接发送语音消息 + */ + handleSendVoiceMsg(): void { + if (this.data.translateResult && this.data.translateResult !== '-1') { + this.sendVoiceMsg(this.data.translateResult); + } + this.resetRecordState(); + } + + /** + * 取消发送语音 + */ + handleCancelSend(): void { + this.resetRecordState(); + } + + /** + * 开始录音 + */ + async startRecord(e?: CustomTouchEvent): Promise { + if (this.data.isStarted) { + return; + } + + // 阻止默认行为,防止冒泡 + if (e?.preventDefault) { + e.preventDefault(); + } + + const mgr = getManager(); + if (!mgr) { + wx.showToast({ + title: '语音功能暂不可用', + icon: 'none', + }); + return; + } + + this.setData({ isStarted: true }); + + try { + await this.getVoiceAuthSetting(); + if (!this.data.recordAuthSetting) { + await this.applyAuth(); + this.setData({ isStarted: false }); + return; + } + } catch (error) { + console.error('授权检查失败', error); + this.setData({ isStarted: false }); + return; + } + + this.setData({ + touchStatus: 'bottom' as TouchStatus, + startTime: new Date().getTime(), + }); + + this.startRecordTimer = setTimeout(() => { + if (!this.data.isStarted) { + return; + } + + this.setData({ showMask: true }); + + const currentMgr = getManager(); + if (!currentMgr) { + this.setData({ isStarted: false }); + return; + } + + currentMgr.start({ duration: 30000, lang: 'zh_CN' }); + + this.recordTimer = setInterval(() => { + const recordTime = new Date().getTime() - this.data.startTime; + if (recordTime > 50000) { + if (this.data.recordCountDown === -1) { + this.setData({ recordCountDown: 10 }); + } else { + this.setData({ recordCountDown: this.data.recordCountDown - 1 }); + } + } + if (recordTime > 60000) { + this.stopRecord(); + } + }, 1000); + }, 500); + } + + /** + * 结束录音 + */ + stopRecord(): void { + this.setData({ isStarted: false }); + + if (this.recordTimer) { + clearInterval(this.recordTimer); + this.recordTimer = null; + } + if (this.startRecordTimer) { + clearTimeout(this.startRecordTimer); + this.startRecordTimer = null; + } + + this.setData({ recordCountDown: -1 }); + + if (!this.data.recordAuthStatus) { + this.setData({ + showMask: false, + startTime: 0, + }); + return; + } + + if (this.data.startTime === 0) { + return; + } + + const recordTime = new Date().getTime() - this.data.startTime; + const mgr = getManager(); + + if (this.data.touchStatus === 'top') { + if (mgr) { + mgr.stop(); + } + this.resetRecordState(); + this.setData({ startTime: 0 }); + wx.showToast({ + icon: 'none', + title: '已取消发送', + duration: 1500, + }); + return; + } + + if (recordTime > 500) { + this.setData({ + 'voiceInfo.duration': Math.floor(recordTime / 1000) || 1, + startTime: 0, + }); + + if (mgr) { + mgr.stop(); + } + } else { + this.setData({ + showMask: false, + startTime: 0, + }); + if (mgr) { + mgr.stop(); + } + wx.showToast({ + icon: 'none', + title: '说话时间太短', + }); + } + } + + /** + * 录音过程中手指移动事件 + */ + touchmove(e: CustomTouchEvent): void { + if (!this.data.isStarted || !this.data.showMask) { + return; + } + + const bottomHeight = 150; + const { changedTouches } = e; + if (!changedTouches || !changedTouches[0]) { + return; + } + + const { clientY } = changedTouches[0]; + const oldStatus = this.data.touchStatus; + + let newTouchStatus: TouchStatus; + if (clientY > this.data.windowHeight - bottomHeight) { + newTouchStatus = 'bottom'; + } else { + newTouchStatus = 'top'; + } + + if (oldStatus !== newTouchStatus) { + this.setData({ touchStatus: newTouchStatus }); + } + } + + /** + * 录音过程中被系统事件打断,结束录音 + */ + touchcancel(): void { + const mgr = getManager(); + if (mgr) { + mgr.stop(); + } + + if (this.recordTimer) { + clearInterval(this.recordTimer); + this.recordTimer = null; + } + if (this.startRecordTimer) { + clearTimeout(this.startRecordTimer); + this.startRecordTimer = null; + } + + this.resetRecordState(); + this.setData({ isStarted: false }); + } + + /** + * 发送语音消息,触发recognize事件 + */ + sendVoiceMsg(voiceMsg: string): void { + this.triggerEvent('recognize', voiceMsg); + } + + /** + * textarea获得焦点时 + */ + focusTextarea(e: CustomFocusEvent): void { + if (this.data.autoSendHeight) { + this.setData({ + bottomHeight: e.detail?.height || 0, + }); + } + } + + /** + * textarea失去焦点时 + */ + blurTextarea(): void { + this.setData({ + bottomHeight: 0, + }); + } +} diff --git a/packages/pro-components/chat/chat-record/chat-record.wxml b/packages/pro-components/chat/chat-record/chat-record.wxml new file mode 100644 index 0000000000..08a2d4135d --- /dev/null +++ b/packages/pro-components/chat/chat-record/chat-record.wxml @@ -0,0 +1,109 @@ + + + + + + 按住 说话 + + + + 请授权麦克风权限 + + + + + + + + + + + + + + + + + + + + + + + {{status === 'unknow' ? '不好意思,未能识别您的语音' : '我在听,请说话'}} + + + + + + + 松开手指 即可取消 + + 点击下方重新开始说话 + + 松手完成 上滑取消 + + + {{recordCountDown}}秒后停止语音输入 + + + + + + + + + + + + + + + + + + + + + + + + 按住说话 + + + + + + + + 发送 + + + + + + diff --git a/packages/pro-components/chat/chat-record/chat-record.wxs b/packages/pro-components/chat/chat-record/chat-record.wxs new file mode 100644 index 0000000000..2cc862d61d --- /dev/null +++ b/packages/pro-components/chat/chat-record/chat-record.wxs @@ -0,0 +1,14 @@ +var utils = require('../../../components/common/utils.wxs'); + +function getRecordClass(classPrefix, record) { + var classes = [classPrefix + '__record']; + if (record.placement) { + classes.push(classPrefix + '__record--' + record.placement); + } + return classes.join(' '); +} + +module.exports = { + getRecordClass: getRecordClass, + _style: utils._style, +}; diff --git a/packages/pro-components/chat/chat-record/index.ts b/packages/pro-components/chat/chat-record/index.ts new file mode 100644 index 0000000000..c1a91eadf4 --- /dev/null +++ b/packages/pro-components/chat/chat-record/index.ts @@ -0,0 +1,2 @@ +export * from './type'; +export { default as ChatRecord } from './chat-record'; diff --git a/packages/pro-components/chat/chat-record/props.ts b/packages/pro-components/chat/chat-record/props.ts new file mode 100644 index 0000000000..b6278d18c8 --- /dev/null +++ b/packages/pro-components/chat/chat-record/props.ts @@ -0,0 +1,24 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + */ + +import { TdChatRecordProps } from './type'; + +const props: TdChatRecordProps = { + useSpeechInputSlot: { + type: Boolean, + value: false, + }, + useSpeechNoAuthSlot: { + type: Boolean, + value: false, + }, + autoSendHeight: { + type: Boolean, + value: true, + }, +}; + +export default props; diff --git a/packages/pro-components/chat/chat-record/type.ts b/packages/pro-components/chat/chat-record/type.ts new file mode 100644 index 0000000000..96b0f148f3 --- /dev/null +++ b/packages/pro-components/chat/chat-record/type.ts @@ -0,0 +1,55 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + */ +export interface TdChatRecordProps { + /** + * 是否使用自定义语音输入插槽 + * @default false + */ + useSpeechInputSlot?: { + type: BooleanConstructor; + value: boolean; + }; + /** + * 是否使用自定义无权限插槽 + * @default false + */ + useSpeechNoAuthSlot?: { + type: BooleanConstructor; + value: boolean; + }; + /** + * 是否自动抬升发送按钮高度 + * @default true + */ + autoSendHeight?: { + type: BooleanConstructor; + value: boolean; + }; +} + +/** 触摸状态类型 */ +export type TouchStatus = 'top' | 'bottom' | ''; + +/** 录音状态类型 */ +export type RecordStatus = 'recording' | 'thinking' | 'stop' | 'error' | ''; + +/** 组件状态类型 */ +export type ComponentStatus = 'normal' | 'cancel' | 'recording' | 'thinking' | 'unknow' | 'complete' | 'error'; + +/** 触摸事件 */ +export interface CustomTouchEvent { + changedTouches?: Array<{ + clientY: number; + }>; + preventDefault?: () => void; +} + +/** 焦点事件 */ +export interface CustomFocusEvent { + detail?: { + height?: number; + }; +} diff --git a/packages/pro-components/chat/chat-sender/README.md b/packages/pro-components/chat/chat-sender/README.md index 46893c51ef..5511fa887e 100644 --- a/packages/pro-components/chat/chat-sender/README.md +++ b/packages/pro-components/chat/chat-sender/README.md @@ -59,6 +59,7 @@ adjust-position | Boolean | false | 默认键盘弹起不会把页面顶起来 | attachments-props | Object | - | 附件列表属性。TS 类型:`AttachmentsProps`,[Attachments API Documents](./attachments?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/pro-components/chat/chat-sender/type.ts) | N auto-rise-with-keyboard | Boolean | false | 键盘弹起时自动顶起来输入框 | N disabled | Boolean | false | 是否禁用输入框 | N +input-mode | String | text | 输入模式:text-文本输入模式(显示textarea),voice-语音输入模式(显示语音按钮)。可选项:text/voice | N file-list | Array | [] | 附件文件列表。TS 类型:`FileItem[]` | N loading | Boolean | false | 发送按钮是否处于加载状态 | N placeholder | String | 请输入消息... | 输入框默认文案 | N diff --git a/packages/pro-components/chat/chat-sender/chat-sender.less b/packages/pro-components/chat/chat-sender/chat-sender.less index 445272129c..c1d9961600 100644 --- a/packages/pro-components/chat/chat-sender/chat-sender.less +++ b/packages/pro-components/chat/chat-sender/chat-sender.less @@ -19,6 +19,7 @@ @chat-sender-textarea-gap: 10rpx; @chat-sender-textarea-padding: 0 24rpx; @chat-sender-textarea-hook-padding-bottom: 16rpx; +@chat-sender-textarea-hook-min-height: 134rpx; @chat-sender-textarea-hook-max-height: 280rpx; @chat-sender-input-max-height: 264rpx; @@ -98,6 +99,7 @@ position: relative; flex: 1; padding-bottom: @chat-sender-textarea-hook-padding-bottom; + min-height: @chat-sender-textarea-hook-min-height; max-height: @chat-sender-textarea-hook-max-height; // 输入框 diff --git a/packages/pro-components/chat/chat-sender/chat-sender.wxml b/packages/pro-components/chat/chat-sender/chat-sender.wxml index 61c7a18ff0..c811a9922b 100644 --- a/packages/pro-components/chat/chat-sender/chat-sender.wxml +++ b/packages/pro-components/chat/chat-sender/chat-sender.wxml @@ -30,27 +30,34 @@ - - {{placeholder}} + + + + {{placeholder}} + + + + + diff --git a/packages/pro-components/chat/chat-sender/props.ts b/packages/pro-components/chat/chat-sender/props.ts index f7da999148..3fcbfd5afb 100644 --- a/packages/pro-components/chat/chat-sender/props.ts +++ b/packages/pro-components/chat/chat-sender/props.ts @@ -25,6 +25,11 @@ const props: TdChatSenderProps = { type: Boolean, value: false, }, + /** 输入模式:text-文本输入模式(显示textarea),voice-语音输入模式(显示语音按钮) */ + inputMode: { + type: String, + value: 'text', + }, /** 附件文件列表 */ fileList: { type: Array, diff --git a/packages/pro-components/chat/chat-sender/type.ts b/packages/pro-components/chat/chat-sender/type.ts index 2f5dc0386a..b4ac5c6916 100644 --- a/packages/pro-components/chat/chat-sender/type.ts +++ b/packages/pro-components/chat/chat-sender/type.ts @@ -38,6 +38,14 @@ export interface TdChatSenderProps { type: BooleanConstructor; value?: boolean; }; + /** + * 输入模式:text-文本输入模式(显示textarea),voice-语音输入模式(显示语音按钮) + * @default text + */ + inputMode?: { + type: StringConstructor; + value?: 'text' | 'voice'; + }; /** * 附件文件列表 * @default [] diff --git a/packages/tdesign-miniprogram-chat/site/site.config.mjs b/packages/tdesign-miniprogram-chat/site/site.config.mjs index acac7e71f5..b3f559eb28 100644 --- a/packages/tdesign-miniprogram-chat/site/site.config.mjs +++ b/packages/tdesign-miniprogram-chat/site/site.config.mjs @@ -42,6 +42,15 @@ export const docs = [ component: () => import('@/chat/chat-sender/README.md'), // componentEn: () => import('@/chat/chat-sender/README.en-US.md'), }, + { + title: 'ChatRecord 语音输入', + titleEn: 'ChatRecord', + name: 'ChatRecord 语音输入', + meta: { docType: 'base' }, + path: '/miniprogram-chat/components/chat-record', + component: () => import('@/chat/chat-record/README.md'), + // componentEn: () => import('@/chat/chat-record/README.en-US.md'), + }, { title: 'ChatMessage 对话消息体', titleEn: 'ChatMessage', diff --git a/packages/tdesign-miniprogram/example/app.json b/packages/tdesign-miniprogram/example/app.json index 3445ff2d53..496b82f202 100644 --- a/packages/tdesign-miniprogram/example/app.json +++ b/packages/tdesign-miniprogram/example/app.json @@ -98,79 +98,133 @@ "subpackages": [ { "root": "pages/chat-list/", - "pages": ["chat-list"] + "pages": [ + "chat-list" + ] }, { "root": "pages/chat-content/", - "pages": ["chat-content"] + "pages": [ + "chat-content" + ] }, { "root": "pages/chat-actionbar/", - "pages": ["chat-actionbar"] + "pages": [ + "chat-actionbar" + ] }, { "root": "pages/chat-loading/", - "pages": ["chat-loading"] + "pages": [ + "chat-loading" + ] }, { "root": "pages/chat-thinking/", - "pages": ["chat-thinking"] + "pages": [ + "chat-thinking" + ] }, { "root": "pages/attachments/", - "pages": ["attachments"] + "pages": [ + "attachments" + ] }, { "root": "pages/chat-markdown/", - "pages": ["chat-markdown"] + "pages": [ + "chat-markdown" + ] }, { "root": "pages/chat-sender/", - "pages": ["chat-sender"] + "pages": [ + "chat-sender" + ] + }, + { + "root": "pages/chat-record/", + "pages": [ + "chat-record" + ] }, { "root": "pages/side-bar/", - "pages": ["side-bar", "base/index", "switch/index", "custom/index", "with-icon/index"] + "pages": [ + "side-bar", + "base/index", + "switch/index", + "custom/index", + "with-icon/index" + ] }, { "root": "pages/action-sheet/", - "pages": ["action-sheet"] + "pages": [ + "action-sheet" + ] }, { "root": "pages/avatar/", - "pages": ["avatar", "skyline/avatar"] + "pages": [ + "avatar", + "skyline/avatar" + ] }, { "root": "pages/calendar/", - "pages": ["calendar"] + "pages": [ + "calendar" + ] }, { "root": "pages/dialog/", - "pages": ["dialog", "skyline/dialog"] + "pages": [ + "dialog", + "skyline/dialog" + ] }, { "root": "pages/picker/", - "pages": ["picker", "skyline/picker"] + "pages": [ + "picker", + "skyline/picker" + ] }, { "root": "pages/rate/", - "pages": ["rate"] + "pages": [ + "rate" + ] }, { "root": "pages/swiper/", - "pages": ["swiper", "skyline/swiper"] + "pages": [ + "swiper", + "skyline/swiper" + ] }, { "root": "pages/swipe-cell/", - "pages": ["swipe-cell"] + "pages": [ + "swipe-cell" + ] }, { "root": "pages/tree-select/", - "pages": ["tree-select"] + "pages": [ + "tree-select" + ] }, { "root": "pages/indexes/", - "pages": ["indexes", "base/index", "custom/index"] + "pages": [ + "indexes", + "base/index", + "custom/index" + ] } ], "themeLocation": "theme.json", @@ -189,6 +243,17 @@ "resolveAlias": { "@behaviors/*": "behaviors/*" }, + "plugins": { + "WechatSI": { + "version": "0.3.6", + "provider": "wx069ba97219f66d99" + } + }, + "permission": { + "scope.record": { + "desc": "你的信息将用于录音功能的效果展示" + } + }, "sitemapLocation": "sitemap.json", "lazyCodeLoading": "requiredComponents", "rendererOptions": { diff --git a/packages/tdesign-miniprogram/example/pages/home/data/chat.ts b/packages/tdesign-miniprogram/example/pages/home/data/chat.ts index f9e8b7b77f..c50739734f 100644 --- a/packages/tdesign-miniprogram/example/pages/home/data/chat.ts +++ b/packages/tdesign-miniprogram/example/pages/home/data/chat.ts @@ -14,6 +14,10 @@ const chat = { name: 'ChatSender', label: '对话输入框', }, + { + name: 'ChatRecord', + label: '语音输入', + }, { name: 'ChatMarkdown', label: '对话 Markdown', diff --git a/packages/tdesign-miniprogram/example/project.config.json b/packages/tdesign-miniprogram/example/project.config.json index 2ade883a57..0bee7016a1 100644 --- a/packages/tdesign-miniprogram/example/project.config.json +++ b/packages/tdesign-miniprogram/example/project.config.json @@ -489,8 +489,20 @@ "name": "chat-sender", "pathName": "pages/chat-sender/chat-sender", "scene": null + }, + { + "id": -1, + "name": "chat-record", + "pathName": "pages/chat-record/chat-record", + "scene": null + } + ], + "plugins": { + "WechatSI": { + "version": "0.3.6", + "provider": "wx069ba97219f66d99" } - ] + } } } } diff --git a/packages/tdesign-uniapp/example/src/pages/home/data/chat.json b/packages/tdesign-uniapp/example/src/pages/home/data/chat.json index 17623a9e05..555720f1f0 100644 --- a/packages/tdesign-uniapp/example/src/pages/home/data/chat.json +++ b/packages/tdesign-uniapp/example/src/pages/home/data/chat.json @@ -15,6 +15,10 @@ "name": "ChatSender", "label": "对话输入框" }, + { + "name": "ChatRecord", + "label": "语音输入" + }, { "name": "ChatMarkdown", "label": "对话 Markdown" From 0bc4adc62f467f2191748aa404c11e363a866989 Mon Sep 17 00:00:00 2001 From: CodeCarfter <43510057+zhangjiaoalice@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:42:56 +0800 Subject: [PATCH 04/42] Feature/add uni record (#4318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: tdesign-uniapp-chat 新增 chat-record 组件 tdesign-uniapp-chat 新增 chat-record 组件 * feat: chat-record 组件demo 补充 * feat: chat-record demo 样式调整 * feat(还原setting.json 文件配置,删除快码插件配置): 还原setting文件,删除快码插件配置 --------- Co-authored-by: Miya <1054055589@qq.com> --- packages/tdesign-uniapp-chat/global.d.ts | 19 +- packages/tdesign-uniapp-chat/package.json | 7 +- .../tdesign-uniapp-chat/site/docs.config.js | 8 + .../tdesign-uniapp/example/src/manifest.json | 8 +- .../tdesign-uniapp/example/src/pages.json | 13 + .../chat/chat-record/README.md | 24 + .../chat/chat-record/_example/base/index.vue | 186 +++++ .../chat-record/_example/chat-record.less | 17 + .../chat/chat-record/_example/chat-record.vue | 36 + .../chat/chat-record/chat-record.less | 699 ++++++++++++++++++ .../chat/chat-record/chat-record.vue | 622 ++++++++++++++++ .../chat/chat-record/props.ts | 6 + .../chat/chat-record/type.ts | 12 + .../chat/chat-sender/README.md | 1 + .../chat/chat-sender/chat-sender.vue | 117 ++- .../chat/chat-sender/props.ts | 12 +- .../chat/chat-sender/type.ts | 5 + .../uniapp-pro-components/chat/package.json | 7 +- .../chat/types/chat-record.d.ts | 17 + .../chat/types/index.d.ts | 1 + 20 files changed, 1735 insertions(+), 82 deletions(-) create mode 100644 packages/uniapp-pro-components/chat/chat-record/README.md create mode 100644 packages/uniapp-pro-components/chat/chat-record/_example/base/index.vue create mode 100644 packages/uniapp-pro-components/chat/chat-record/_example/chat-record.less create mode 100644 packages/uniapp-pro-components/chat/chat-record/_example/chat-record.vue create mode 100644 packages/uniapp-pro-components/chat/chat-record/chat-record.less create mode 100644 packages/uniapp-pro-components/chat/chat-record/chat-record.vue create mode 100644 packages/uniapp-pro-components/chat/chat-record/props.ts create mode 100644 packages/uniapp-pro-components/chat/chat-record/type.ts create mode 100644 packages/uniapp-pro-components/chat/types/chat-record.d.ts diff --git a/packages/tdesign-uniapp-chat/global.d.ts b/packages/tdesign-uniapp-chat/global.d.ts index 0d80187e96..25648a7187 100644 --- a/packages/tdesign-uniapp-chat/global.d.ts +++ b/packages/tdesign-uniapp-chat/global.d.ts @@ -1,14 +1,15 @@ declare module 'vue' { export interface GlobalComponents { - TAttachments: typeof import('@tdesign/uniapp-chat/attachments/attachments.vue').default; - TChatActionbar: typeof import('@tdesign/uniapp-chat/chat-actionbar/chat-actionbar.vue').default; - TChatContent: typeof import('@tdesign/uniapp-chat/chat-content/chat-content.vue').default; - TChatList: typeof import('@tdesign/uniapp-chat/chat-list/chat-list.vue').default; - TChatLoading: typeof import('@tdesign/uniapp-chat/chat-loading/chat-loading.vue').default; - TChatMarkdown: typeof import('@tdesign/uniapp-chat/chat-markdown/chat-markdown.vue').default; - TChatMessage: typeof import('@tdesign/uniapp-chat/chat-message/chat-message.vue').default; - TChatSender: typeof import('@tdesign/uniapp-chat/chat-sender/chat-sender.vue').default; - TChatThinking: typeof import('@tdesign/uniapp-chat/chat-thinking/chat-thinking.vue').default; + TAttachments: typeof import('tdesign-uniapp-chat/attachments/attachments.vue').default; + TChatActionbar: typeof import('tdesign-uniapp-chat/chat-actionbar/chat-actionbar.vue').default; + TChatContent: typeof import('tdesign-uniapp-chat/chat-content/chat-content.vue').default; + TChatList: typeof import('tdesign-uniapp-chat/chat-list/chat-list.vue').default; + TChatLoading: typeof import('tdesign-uniapp-chat/chat-loading/chat-loading.vue').default; + TChatMarkdown: typeof import('tdesign-uniapp-chat/chat-markdown/chat-markdown.vue').default; + TChatMessage: typeof import('tdesign-uniapp-chat/chat-message/chat-message.vue').default; + TChatRecord: typeof import('tdesign-uniapp-chat/chat-record/chat-record.vue').default; + TChatSender: typeof import('tdesign-uniapp-chat/chat-sender/chat-sender.vue').default; + TChatThinking: typeof import('tdesign-uniapp-chat/chat-thinking/chat-thinking.vue').default; } } diff --git a/packages/tdesign-uniapp-chat/package.json b/packages/tdesign-uniapp-chat/package.json index 2289bad7af..d47aaac2ff 100644 --- a/packages/tdesign-uniapp-chat/package.json +++ b/packages/tdesign-uniapp-chat/package.json @@ -50,6 +50,11 @@ "import": "./dist/chat-message/chat-message.vue", "default": "./dist/chat-message/chat-message.vue" }, + "./chat-record/chat-record.vue": { + "types": "./dist/types/chat-record.d.ts", + "import": "./dist/chat-record/chat-record.vue", + "default": "./dist/chat-record/chat-record.vue" + }, "./chat-sender/chat-sender.vue": { "types": "./dist/types/chat-sender.d.ts", "import": "./dist/chat-sender/chat-sender.vue", @@ -135,4 +140,4 @@ "eslint --fix" ] } -} \ No newline at end of file +} diff --git a/packages/tdesign-uniapp-chat/site/docs.config.js b/packages/tdesign-uniapp-chat/site/docs.config.js index 7caadf9d7b..fde80a0324 100644 --- a/packages/tdesign-uniapp-chat/site/docs.config.js +++ b/packages/tdesign-uniapp-chat/site/docs.config.js @@ -85,6 +85,14 @@ export const docs = [ path: '/uniapp-chat/components/chat-sender', component: () => import('@/chat-sender/README.md'), }, + { + title: 'ChatRecord 对话记录', + titleEn: 'ChatRecord', + name: 'ChatRecord 对话记录', + meta: { docType: 'base' }, + path: '/tdesign-uniapp-chat/components/chat-record', + component: () => import('@/chat-record/README.md'), + }, { title: 'ChatMessage 对话消息体', titleEn: 'ChatMessage', diff --git a/packages/tdesign-uniapp/example/src/manifest.json b/packages/tdesign-uniapp/example/src/manifest.json index 89747bb918..8b07a6f63d 100644 --- a/packages/tdesign-uniapp/example/src/manifest.json +++ b/packages/tdesign-uniapp/example/src/manifest.json @@ -80,6 +80,12 @@ "sdkVersionBegin": "3.4.3", "sdkVersionEnd": "15.255.255" } + }, + "plugins": { + "WechatSI": { + "version": "0.3.6", + "provider": "wx069ba97219f66d99" + } } }, "mp-qq": { @@ -123,4 +129,4 @@ "enable": false }, "vueVersion": "3" -} +} \ No newline at end of file diff --git a/packages/tdesign-uniapp/example/src/pages.json b/packages/tdesign-uniapp/example/src/pages.json index 0a6aa12134..be3fa8392c 100644 --- a/packages/tdesign-uniapp/example/src/pages.json +++ b/packages/tdesign-uniapp/example/src/pages.json @@ -304,6 +304,14 @@ } ] }, + { + "root": "pages-more/chat-record", + "pages": [ + { + "path": "chat-record" + } + ] + }, { "root": "pages-more/chat-markdown", "pages": [ @@ -623,6 +631,10 @@ "name": "chat-sender", "pathName": "pages-more/chat-sender/chat-sender" }, + { + "name": "chat-record", + "pathName": "pages-more/chat-record/chat-record" + }, { "name": "chat-markdown", "pathName": "pages-more/chat-markdown/chat-markdown" @@ -669,6 +681,7 @@ "pages-more/chat-list", "pages-more/chat-message", "pages-more/chat-sender", + "pages-more/chat-record", "pages-more/chat-markdown", "pages-more/chat-content", "pages-more/chat-thinking", diff --git a/packages/uniapp-pro-components/chat/chat-record/README.md b/packages/uniapp-pro-components/chat/chat-record/README.md new file mode 100644 index 0000000000..66e6f2aad7 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-record/README.md @@ -0,0 +1,24 @@ +## API +### ChatRecord Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +### 事件 + +事件名 | 说明 | 回调参数 +-- | -- | -- +recognize | 发送消息 | msg:string + + + +### 插槽 + +插槽名 | 说明 +-- | -- +speechInput | 自定义语音输入按钮内容 +speechNoAuth | 自定义未授权麦克风时的提示内容 + +### 示例用法 +#### 基础类型 + +{{ base }} diff --git a/packages/uniapp-pro-components/chat/chat-record/_example/base/index.vue b/packages/uniapp-pro-components/chat/chat-record/_example/base/index.vue new file mode 100644 index 0000000000..612174e7c3 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-record/_example/base/index.vue @@ -0,0 +1,186 @@ + + + + diff --git a/packages/uniapp-pro-components/chat/chat-record/_example/chat-record.less b/packages/uniapp-pro-components/chat/chat-record/_example/chat-record.less new file mode 100644 index 0000000000..63805ab1d1 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-record/_example/chat-record.less @@ -0,0 +1,17 @@ +:deep(.chat-sender-placeholder) { + --demo-chat-sender-placeholder: rgba(0, 0, 0, 0.1); +} + +@media (prefers-color-scheme: dark) { + :deep(.chat-sender-placeholder) { + --demo-chat-sender-placeholder: rgba(255, 255, 255, 0.1); + } +} + +.t-button { + margin-right: 32rpx; +} + +.t-button:last-child { + margin-right: 0; +} diff --git a/packages/uniapp-pro-components/chat/chat-record/_example/chat-record.vue b/packages/uniapp-pro-components/chat/chat-record/_example/chat-record.vue new file mode 100644 index 0000000000..ddd03a5127 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-record/_example/chat-record.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/uniapp-pro-components/chat/chat-record/chat-record.less b/packages/uniapp-pro-components/chat/chat-record/chat-record.less new file mode 100644 index 0000000000..b68c02c587 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-record/chat-record.less @@ -0,0 +1,699 @@ +@import 'tdesign-uniapp/common/style/base.less'; + +@chat-record: ~'@{prefix}-chat-record'; + +// ==================== 动画定义 ==================== + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes wave { + 0%, + 100% { + transform: scaleY(1); + } + + 50% { + transform: scaleY(1.857); + } +} + +// ==================== 变量配置 ==================== + +// 录音按钮 +@chat-record-hook-padding: 24rpx 0; +@chat-record-hook-font-size: 34rpx; +@chat-record-hook-line-height: 42rpx; +@chat-record-hook-letter-spacing: 1.02rpx; +@chat-record-hook-border-radius: 16px; +@chat-record-hook-border: 2rpx solid #fff; +@chat-record-hook-bg: #f5f5f5; + +// 遮罩层 +@chat-record-mask-z-index: 5100; +@chat-record-mask-bg: rgba(0, 0, 0, 0.8); +@chat-record-mask-blur: blur(5px); + +// 主容器 +@chat-record-main-z-index: 5101; + +// 气泡样式 +@chat-record-bubble-border-radius: 24rpx; +@chat-record-bubble-padding: 40rpx 48rpx; +@chat-record-bubble-min-width: 300rpx; +@chat-record-bubble-min-height: 120rpx; +@chat-record-bubble-max-height: 584rpx; +@chat-record-bubble-margin-bottom: 268rpx; +@chat-record-bubble-padding-horizontal: 48rpx; + +// 气泡颜色 +@chat-record-color-blue: #2979ff; +@chat-record-color-red: #ff5252; +@chat-record-color-white: #ffffff; + +@chat-record-cancel-font-size: 32rpx; + +// 红色气泡(取消) +@chat-record-bubble-red-min-width: 160rpx; +@chat-record-bubble-red-padding: 24rpx 32rpx; +@chat-record-bubble-red-translate-x: -230rpx; + +// 宽气泡(转文字) +@chat-record-bubble-wide-min-width: calc(100% - 96rpx); +@chat-record-bubble-tail-right: calc(100% - 66rpx); + +// 波浪条 +@chat-record-wave-height: 28rpx; +@chat-record-wave-gap: 8rpx; +@chat-record-wave-item-width: 8rpx; +@chat-record-wave-item-height: 14rpx; +@chat-record-wave-item-radius: 90rpx; + +// 省略号圆点 +@chat-record-dot-size: 8rpx; +@chat-record-dot-gap: 6rpx; +@chat-record-dot-shadow-offset: 14rpx; +@chat-record-dot-margin-right: 56rpx; + +// 转文字输入框 +@chat-record-textarea-min-height: 112rpx; +@chat-record-textarea-max-height: 504rpx; +@chat-record-textarea-font-size: 40rpx; +@chat-record-textarea-line-height: 56rpx; + +// 底部操作区 +@chat-record-footer-height: 400rpx; +@chat-record-tips-top: 148rpx; +@chat-record-tips-font-size: 32rpx; +@chat-record-tips-line-height: 48rpx; + +// 大圆背景 +@chat-record-circle-size: 2000rpx; +@chat-record-circle-border-radius: 2000rpx; +@chat-record-circle-bottom: -1700rpx; +@chat-record-circle-border: 4rpx solid #ffffff; +@chat-record-circle-bg-default: #dcdcdc; +@chat-record-circle-bg-active: #4a4a4a; + +// 异形按钮 +@chat-record-shape-btn-width: 340rpx; +@chat-record-shape-btn-height: 338rpx; +@chat-record-shape-btn-bottom: 120rpx; +@chat-record-shape-btn-padding-bottom: 180rpx; +@chat-record-shape-btn-hint-top: -50rpx; +@chat-record-shape-btn-hint-font-size: 24rpx; +@chat-record-shape-btn-hint-color: #a3a3a3; +@chat-record-shape-btn-label-font-size: 28rpx; +@chat-record-shape-btn-label-line-height: 44rpx; +@chat-record-shape-btn-label-color: #fff; +@chat-record-shape-btn-label-color-active: #000; + +// 确认按钮区域 +@chat-record-confirm-bottom: 120rpx; +@chat-record-confirm-height: 300rpx; +@chat-record-confirm-padding: 20rpx 48rpx 40rpx; +@chat-record-cancel-icon-size: 144rpx; +@chat-record-cancel-icon-margin: 16rpx; +@chat-record-cancel-text-font-size: 24rpx; +@chat-record-cancel-text-line-height: 40rpx; +@chat-record-send-btn-width: 224rpx; +@chat-record-send-btn-height: 144rpx; +@chat-record-send-btn-radius: 1998rpx; +@chat-record-send-btn-bg: #e0e0e0; +@chat-record-send-btn-bg-active: #b0b0b0; +@chat-record-send-btn-font-size: 28rpx; +@chat-record-send-btn-line-height: 44rpx; + +// 过渡动画 +@chat-record-transition-ease: 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); +@chat-record-transition-fast: 0.15s ease; +@chat-record-transition-opacity: 0.25s linear; + +// ==================== 主容器 ==================== + +.@{chat-record} { + // 录音按钮 + &-hook { + width: 100%; + padding: @chat-record-hook-padding; + color: rgba(0, 0, 0, 0.9); + text-align: center; + font-size: @chat-record-hook-font-size; + font-weight: 500; + line-height: @chat-record-hook-line-height; + letter-spacing: @chat-record-hook-letter-spacing; + border-radius: @chat-record-hook-border-radius; + border: @chat-record-hook-border; + background: @chat-record-hook-bg; + } + + // ==================== 遮罩层和主容器 ==================== + + &-audio-input { + &.show { + .@{chat-record}-audio-input__mask, + .@{chat-record}-audio-input__main { + opacity: 1; + pointer-events: auto; + } + } + + // 遮罩层 + &__mask { + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + z-index: @chat-record-mask-z-index; + background: @chat-record-mask-bg; + backdrop-filter: @chat-record-mask-blur; + opacity: 0; + pointer-events: none; + transition: opacity @chat-record-transition-opacity; + } + + // 主容器 + &__main { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: @chat-record-main-z-index; + display: flex; + flex-direction: column; + justify-content: flex-end; + opacity: 0; + pointer-events: none; + transition: opacity @chat-record-transition-opacity; + } + + // ==================== 气泡动画容器 ==================== + + &-ani { + display: flex; + justify-content: center; + width: 100%; + margin-bottom: @chat-record-bubble-margin-bottom; + padding: 0 @chat-record-bubble-padding-horizontal; + box-sizing: border-box; + opacity: 1; + transition: opacity 0.3s ease; + animation: fadeIn 0.3s ease; + + // 气泡容器 + .bubble-container { + position: relative; + display: flex; + align-items: center; + justify-content: center; + padding: @chat-record-bubble-padding; + border-radius: @chat-record-bubble-border-radius; + min-width: @chat-record-bubble-min-width; + min-height: @chat-record-bubble-min-height; + width: auto; + // 只对特定属性做过渡,避免 height 变化时闪烁 + transition: background-color @chat-record-transition-ease, transform @chat-record-transition-ease, + min-width @chat-record-transition-ease, padding @chat-record-transition-ease; + + // 气泡尾巴 mixin + .bubble-tail(@color, @left: 50%, @transform: translateX(-50%)) { + content: ''; + position: absolute; + bottom: -14rpx; + left: @left; + transform: @transform; + border-left: 16rpx solid transparent; + border-right: 16rpx solid transparent; + border-top: 16rpx solid @color; + transition: all @chat-record-transition-ease; + } + + // 蓝色气泡(默认) + &.bubble-blue { + background-color: @chat-record-color-blue; + color: @chat-record-color-white; + + &::after { + .bubble-tail(@chat-record-color-blue); + } + } + + // 红色气泡(取消状态) + &.bubble-red { + background-color: @chat-record-color-red; + color: @chat-record-color-white; + min-width: @chat-record-bubble-red-min-width; + padding: @chat-record-bubble-red-padding; + transform: translateX(@chat-record-bubble-red-translate-x); + + &::after { + .bubble-tail(@chat-record-color-red); + } + } + + // 宽气泡(转文字状态) + &.bubble-wide { + background-color: @chat-record-color-blue; + color: @chat-record-color-white; + min-width: @chat-record-bubble-wide-min-width; + max-width: none; + justify-content: flex-start; + + &::after { + .bubble-tail(@chat-record-color-blue, @chat-record-bubble-tail-right); + } + } + } + } + + // ==================== 气泡内容 ==================== + + // 音量波浪条 + .audio-wave { + display: flex; + align-items: center; + height: @chat-record-wave-height; + gap: @chat-record-wave-gap; + + .wave-item { + width: @chat-record-wave-item-width; + height: @chat-record-wave-item-height; + background: @chat-record-color-white; + border-radius: @chat-record-wave-item-radius; + transform-origin: center; + will-change: transform; + animation: wave 1.5s infinite ease-in-out; + } + + // LESS 循环生成 13 个波浪条的动画延迟 + .generate-wave-delays(@n, @i: 1) when (@i =< @n) { + .wave-item:nth-child(@{i}) { + animation-delay: unit((-1.2 + (@i - 1) * 0.1), s); + } + + .generate-wave-delays(@n, (@i + 1)); + } + + .generate-wave-delays(13); + } + + // 取消图标(省略号) + .cancel-icon { + display: flex; + align-items: center; + gap: @chat-record-dot-gap; + font-size: 0; + + &::before { + content: ''; + display: block; + width: @chat-record-dot-size; + height: @chat-record-dot-size; + background: @chat-record-color-white; + border-radius: 50%; + box-shadow: @chat-record-dot-shadow-offset 0 0 @chat-record-color-white, + (@chat-record-dot-shadow-offset * 2) 0 0 @chat-record-color-white, + (@chat-record-dot-shadow-offset * 3) 0 0 @chat-record-color-white, + (@chat-record-dot-shadow-offset * 4) 0 0 @chat-record-color-white; + margin-right: @chat-record-dot-margin-right; + } + } + + .cancel-text { + color: @chat-record-color-white; + font-size: @chat-record-cancel-font-size; + font-weight: 600; + } + + // 转文字内容 + .convert-content { + width: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + + .editable-textarea { + width: 100%; + height: @chat-record-textarea-min-height; + background: transparent; + border: none; + color: @chat-record-color-white; + font-family: 'PingFang SC'; + font-size: @chat-record-textarea-font-size; + font-weight: 600; + line-height: @chat-record-textarea-line-height; + outline: none; + resize: none; + overflow-y: auto; + -webkit-appearance: none; + } + + .convert-dots { + align-self: flex-end; + display: flex; + align-items: center; + gap: @chat-record-dot-gap; + font-size: 0; + + &::before { + content: ''; + display: block; + width: @chat-record-dot-size; + height: @chat-record-dot-size; + background: @chat-record-color-white; + border-radius: 50%; + box-shadow: @chat-record-dot-shadow-offset 0 0 @chat-record-color-white, + (@chat-record-dot-shadow-offset * 2) 0 0 @chat-record-color-white, + (@chat-record-dot-shadow-offset * 3) 0 0 @chat-record-color-white, + (@chat-record-dot-shadow-offset * 4) 0 0 @chat-record-color-white; + margin-right: @chat-record-dot-margin-right; + } + } + } + + // ==================== 底部操作区 ==================== + + &__ft { + position: relative; + width: 100%; + height: @chat-record-footer-height; + opacity: 1; + transition: opacity 0.3s ease; + animation: fadeIn 0.3s ease; + + // 提示文字 + .tips-text { + position: absolute; + top: @chat-record-tips-top; + width: 100%; + text-align: center; + color: rgba(0, 0, 0, 0.9); + font-size: @chat-record-tips-font-size; + font-weight: 600; + line-height: @chat-record-tips-line-height; + z-index: 10; + pointer-events: none; + } + + // 状态变化:文字变白色 + &.release_cancel .tips-text, + &.release_convert .tips-text { + color: @chat-record-color-white; + } + + // 大圆弧背景 + &__bg { + width: 100%; + height: 100%; + position: absolute; + bottom: 0; + pointer-events: none; + + &::before { + content: ''; + position: absolute; + left: 50%; + bottom: @chat-record-circle-bottom; + transform: translateX(-50%); + width: @chat-record-circle-size; + height: @chat-record-circle-size; + border-radius: @chat-record-circle-border-radius; + background: @chat-record-circle-bg-default; + border: @chat-record-circle-border; + transition: background-color 0.3s ease; + } + } + + // 状态变化:大圆变深色 + &.release_cancel .@{chat-record}-audio-input__ft__bg::before, + &.release_convert .@{chat-record}-audio-input__ft__bg::before { + background-color: @chat-record-circle-bg-active; + border-color: @chat-record-circle-bg-active; + } + + // ==================== 异形按钮 ==================== + + .shape-btn { + position: absolute; + bottom: @chat-record-shape-btn-bottom; + width: @chat-record-shape-btn-width; + height: @chat-record-shape-btn-height; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-bottom: @chat-record-shape-btn-padding-bottom; + box-sizing: border-box; + transition: all 0.3s ease; + pointer-events: none; + z-index: 1; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: contain; + background-repeat: no-repeat; + background-position: center center; + z-index: -1; + transition: all 0.3s ease; + transform-origin: center 20%; + } + + .btn-hint { + position: absolute; + top: @chat-record-shape-btn-hint-top; + font-size: @chat-record-shape-btn-hint-font-size; + font-weight: 400; + color: @chat-record-shape-btn-hint-color; + white-space: nowrap; + + .word { + display: inline-block; + margin-right: 8rpx; + + &:last-child { + margin-right: 0; + } + } + } + + .btn-label { + font-family: 'PingFang SC'; + font-size: @chat-record-shape-btn-label-font-size; + font-weight: 600; + line-height: @chat-record-shape-btn-label-line-height; + color: @chat-record-shape-btn-label-color; + + .word { + display: inline-block; + margin-right: 8rpx; + + &:last-child { + margin-right: 0; + } + } + } + + &.active { + .btn-label { + color: @chat-record-shape-btn-label-color-active; + } + + &::before { + transform: scale(1.1); + } + } + } + + // 左侧按钮 + .left-btn { + left: 0; + + &::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='169' viewBox='0 0 170 169' fill='none'%3E%3Cpath d='M142.225 59.8918C158.733 58.4737 170.966 43.9418 169.548 27.4343C168.13 10.9265 153.598 -1.30612 137.09 0.111923C25.1861 9.72467 -81.5199 51.6215 -170.069 120.715L-133.159 168.019C-53.7697 106.073 41.8976 68.5101 142.225 59.8918Z' fill='%234B4B4B'/%3E%3C/svg%3E"); + } + + // btn-hint: "Release" + "to cancel" + .btn-hint { + transform: translateZ(0); // 触发重绘防止文字残影 + + .word-1 { + transform: rotate(-10deg) translateY(6rpx); + } + + .word-2 { + transform: rotate(-7deg) translateY(-10rpx); + } + } + + // btn-label: "Cancel" + .btn-label { + transform: rotate(-12deg); + } + + &.active::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='169' viewBox='0 0 170 169' fill='none'%3E%3Cpath d='M142.225 59.8918C158.733 58.4737 170.966 43.9418 169.548 27.4343C168.13 10.9265 153.598 -1.30612 137.09 0.111923C25.1861 9.72467 -81.5199 51.6215 -170.069 120.715L-133.159 168.019C-53.7697 106.073 41.8976 68.5101 142.225 59.8918Z' fill='%23DCDCDC'/%3E%3C/svg%3E"); + transform: scale(1.1); + } + } + + // 右侧按钮 + .right-btn { + right: 0; + transform: scaleX(-1); + + &::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='169' viewBox='0 0 170 169' fill='none'%3E%3Cpath d='M142.225 59.8918C158.733 58.4737 170.966 43.9418 169.548 27.4343C168.13 10.9265 153.598 -1.30612 137.09 0.111923C25.1861 9.72467 -81.5199 51.6215 -170.069 120.715L-133.159 168.019C-53.7697 106.073 41.8976 68.5101 142.225 59.8918Z' fill='%234B4B4B'/%3E%3C/svg%3E"); + } + + // btn-hint: "Release" + "to edit" + "text" + .btn-hint { + transform: scaleX(-1) translateZ(0); // 触发重绘防止文字残影 + + .word-1 { + transform: rotate(7deg) translateY(-10rpx); + } + + .word-2 { + transform: rotate(12deg) translateY(5rpx); + } + + .word-3 { + transform: rotate(17deg) translateY(20rpx); + } + } + + // btn-label: "Convert" + "to Text" + .btn-label { + transform: scaleX(-1); + + .word-1 { + transform: rotate(9deg) translateY(-12rpx); + } + + .word-2 { + transform: rotate(16deg) translateY(10rpx); + } + } + + &.active::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='169' viewBox='0 0 170 169' fill='none'%3E%3Cpath d='M142.225 59.8918C158.733 58.4737 170.966 43.9418 169.548 27.4343C168.13 10.9265 153.598 -1.30612 137.09 0.111923C25.1861 9.72467 -81.5199 51.6215 -170.069 120.715L-133.159 168.019C-53.7697 106.073 41.8976 68.5101 142.225 59.8918Z' fill='%23DCDCDC'/%3E%3C/svg%3E"); + transform: scale(1.1); + } + } + + // ==================== 确认按钮区域 ==================== + + .confirm-actions { + position: absolute; + left: 0; + right: 0; + bottom: @chat-record-confirm-bottom; + height: @chat-record-confirm-height; + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: @chat-record-confirm-padding; + z-index: 20; + + .action-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + transition: all @chat-record-transition-fast; + + // Cancel 按钮 + &.btn-cancel { + .icon-wrapper { + width: @chat-record-cancel-icon-size; + height: @chat-record-cancel-icon-size; + border-radius: 50%; + background-color: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: @chat-record-cancel-icon-margin; + transition: background-color @chat-record-transition-fast; + } + + .btn-text { + color: #fff; + text-align: center; + font-family: 'PingFang SC'; + font-size: @chat-record-cancel-text-font-size; + font-weight: 600; + line-height: @chat-record-cancel-text-line-height; + } + + &.active .icon-wrapper { + background-color: rgba(255, 255, 255, 0.4); + } + } + + // Send 按钮 + &.btn-send { + width: @chat-record-send-btn-width; + height: @chat-record-send-btn-height; + background-color: @chat-record-send-btn-bg; + border-radius: @chat-record-send-btn-radius; + display: flex; + align-items: center; + justify-content: center; + transition: background-color @chat-record-transition-fast; + + text { + color: rgba(0, 0, 0, 0.9); + font-family: 'PingFang SC'; + font-size: @chat-record-send-btn-font-size; + font-weight: 600; + line-height: @chat-record-send-btn-line-height; + } + + &.active { + background-color: @chat-record-send-btn-bg-active; + } + + // 禁用状态 + &.disabled { + background-color: #999; + opacity: 0.5; + pointer-events: none; + + text { + color: rgba(255, 255, 255, 0.6); + } + } + } + + // Cancel 按钮禁用状态 + &.btn-cancel.disabled { + opacity: 0.5; + pointer-events: none; + + .icon-wrapper { + background-color: rgba(255, 255, 255, 0.1); + } + + .btn-text { + color: rgba(255, 255, 255, 0.6); + } + } + } + } + } + } +} diff --git a/packages/uniapp-pro-components/chat/chat-record/chat-record.vue b/packages/uniapp-pro-components/chat/chat-record/chat-record.vue new file mode 100644 index 0000000000..6804de64f6 --- /dev/null +++ b/packages/uniapp-pro-components/chat/chat-record/chat-record.vue @@ -0,0 +1,622 @@ +