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 000000000..eac998ec8 --- /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 000000000..bffa4bc88 --- /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 000000000..5e834db8a --- /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 000000000..1a031ee2c --- /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 000000000..9b907e6da --- /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 000000000..51c640ae3 --- /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 000000000..36c6e4ce9 --- /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 000000000..e69de29bb 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 000000000..560d44d43 --- /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 000000000..364a909f7 --- /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 000000000..66165257f --- /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 000000000..dd10d18a3 --- /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 000000000..618e7e721 --- /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 000000000..08a2d4135 --- /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 000000000..2cc862d61 --- /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 000000000..c1a91eadf --- /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 000000000..b6278d18c --- /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 000000000..96b0f148f --- /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 46893c51e..5511fa887 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 445272129..c1d996160 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 02c2bf628..034a3cb0f 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 f7da99914..3fcbfd5af 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 2f5dc0386..b4ac5c691 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 acac7e71f..b3f559eb2 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 3445ff2d5..496b82f20 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 f9e8b7b77..c50739734 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 2ade883a5..0bee7016a 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 17623a9e0..555720f1f 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"