diff --git a/.github/workflows/typos-config.toml b/.github/workflows/typos-config.toml
index 6d6610f829..560822f017 100644
--- a/.github/workflows/typos-config.toml
+++ b/.github/workflows/typos-config.toml
@@ -7,6 +7,8 @@ ded = "ded"
destory ="destory"
nd = "nd"
loosing = "loosing"
+ba = "ba"
+Impossibile = "Impossibile"
[files]
extend-exclude = [
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e8a73f7184..505e7cae2f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -46,12 +46,29 @@
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
- "cSpell.words": [
- "stylelint",
- "cascader",
- "tdesign",
- "uniapp",
- "backtop",
- "miniprogram"
- ]
+ "cSpell.words": ["stylelint", "cascader", "tdesign", "uniapp", "backtop", "miniprogram"],
+ "terminal.integrated.profiles.linux": {
+ "bash": {
+ "path": "bash",
+ "icon": "terminal-bash"
+ },
+ "zsh": {
+ "path": "zsh"
+ },
+ "fish": {
+ "path": "fish"
+ },
+ "tmux": {
+ "path": "tmux",
+ "icon": "terminal-tmux"
+ },
+ "pwsh": {
+ "path": "pwsh",
+ "icon": "terminal-powershell"
+ },
+ "supercode-sh": {
+ "path": "/bin/zsh",
+ "args": ["-l"]
+ }
+ }
}
diff --git a/packages/common b/packages/common
index 3372792cb9..57a70cc3a9 160000
--- a/packages/common
+++ b/packages/common
@@ -1 +1 @@
-Subproject commit 3372792cb9a863c4d7e2046fac53ecee440c4f24
+Subproject commit 57a70cc3a9159ae487ea3283ac515df7dcf82d65
diff --git a/packages/components/config-provider/__test__/__snapshots__/demo.test.js.snap b/packages/components/config-provider/__test__/__snapshots__/demo.test.js.snap
index 5a8ad6d38a..7dcbd65559 100644
--- a/packages/components/config-provider/__test__/__snapshots__/demo.test.js.snap
+++ b/packages/components/config-provider/__test__/__snapshots__/demo.test.js.snap
@@ -58,6 +58,27 @@ exports[`ConfigProvider ConfigProvider chat-en demo works fine 1`] = `
"share": "share",
},
},
+ "chatRecord": Object {
+ "authSettingFail": "Failed to get recording permission settings",
+ "busyTip": "Recognizing, please wait...",
+ "cancelText": "Cancel",
+ "holdToTalk": "Hold to talk",
+ "missingPluginTip": "Missing voice recognition plugin WechatSI",
+ "openSettingFail": "Failed to open settings",
+ "recognizeFailTip": "Voice recognition failed, please try again",
+ "releaseToCancel": "Release to cancel",
+ "releaseToSend": "Release to send, swipe up to cancel",
+ "requestAuth": "Please authorize microphone permission",
+ "sendText": "Send",
+ "systemMicContent": "Detected that the phone system has disabled the microphone permission for \\"WeChat\\".
+
+Please enable it in system settings:
+- iOS: Settings > WeChat > Microphone
+- Android: Settings > App Management > WeChat > Permissions > Microphone
+
+After enabling, return to the Mini Program and try again.",
+ "systemMicTitle": "Cannot use microphone",
+ },
"chatSender": Object {
"placeholder": "please enter message...",
"sendText": "send",
@@ -75,7 +96,7 @@ exports[`ConfigProvider ConfigProvider chat-en demo works fine 1`] = `
"confirm": "Confirm",
"dateLabel": "",
"dayjsLocale": "en",
- "format": "YYYY-MM-DD",
+ "format": "YYYY-MM-DD HH:mm:ss",
"hourLabel": "",
"minuteLabel": "",
"monthLabel": "",
@@ -168,7 +189,12 @@ exports[`ConfigProvider ConfigProvider chat-en demo works fine 1`] = `
"newsAriaLabel": "There is new news",
},
"table": Object {
+ "clearFilterResultButtonText": "Clear",
+ "confirmText": "Confirm",
"empty": "Empty Data",
+ "pullUpLoadingText": "Loading...",
+ "resetText": "Reset",
+ "searchResultText": "Search \\"{result}\\". Found no items. | Search \\"{result}\\". Found 1 item. | Search \\"{result}\\". Found {count} items.",
},
"typography": Object {
"collapseText": "collapse",
@@ -335,6 +361,27 @@ exports[`ConfigProvider ConfigProvider upload-en demo works fine 1`] = `
"share": "share",
},
},
+ "chatRecord": Object {
+ "authSettingFail": "Failed to get recording permission settings",
+ "busyTip": "Recognizing, please wait...",
+ "cancelText": "Cancel",
+ "holdToTalk": "Hold to talk",
+ "missingPluginTip": "Missing voice recognition plugin WechatSI",
+ "openSettingFail": "Failed to open settings",
+ "recognizeFailTip": "Voice recognition failed, please try again",
+ "releaseToCancel": "Release to cancel",
+ "releaseToSend": "Release to send, swipe up to cancel",
+ "requestAuth": "Please authorize microphone permission",
+ "sendText": "Send",
+ "systemMicContent": "Detected that the phone system has disabled the microphone permission for \\"WeChat\\".
+
+Please enable it in system settings:
+- iOS: Settings > WeChat > Microphone
+- Android: Settings > App Management > WeChat > Permissions > Microphone
+
+After enabling, return to the Mini Program and try again.",
+ "systemMicTitle": "Cannot use microphone",
+ },
"chatSender": Object {
"placeholder": "please enter message...",
"sendText": "send",
@@ -352,7 +399,7 @@ exports[`ConfigProvider ConfigProvider upload-en demo works fine 1`] = `
"confirm": "Confirm",
"dateLabel": "",
"dayjsLocale": "en",
- "format": "YYYY-MM-DD",
+ "format": "YYYY-MM-DD HH:mm:ss",
"hourLabel": "",
"minuteLabel": "",
"monthLabel": "",
@@ -445,7 +492,12 @@ exports[`ConfigProvider ConfigProvider upload-en demo works fine 1`] = `
"newsAriaLabel": "There is new news",
},
"table": Object {
+ "clearFilterResultButtonText": "Clear",
+ "confirmText": "Confirm",
"empty": "Empty Data",
+ "pullUpLoadingText": "Loading...",
+ "resetText": "Reset",
+ "searchResultText": "Search \\"{result}\\". Found no items. | Search \\"{result}\\". Found 1 item. | Search \\"{result}\\". Found {count} items.",
},
"typography": Object {
"collapseText": "collapse",
diff --git a/packages/components/locale/ar_KW.ts b/packages/components/locale/ar_KW.ts
index 1b7c1af9e2..8688019983 100644
--- a/packages/components/locale/ar_KW.ts
+++ b/packages/components/locale/ar_KW.ts
@@ -10,7 +10,7 @@ export default {
confirm: 'أكد',
title: 'انتقِ التاريخ',
weekdays: ['يوم الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'],
- monthTitle: '{شهر واحد} {سنة واحدة}',
+ monthTitle: '{month} {year}',
months: [
'يناير',
'فبراير',
@@ -41,7 +41,7 @@ export default {
title: 'انتقِ التاريخ',
cancel: 'الإلغاء',
confirm: 'أكد',
- format: 'DD-MM-YYYY',
+ format: 'DD-MM-YYYY HH:mm:ss',
months: [
'يناير',
'فبراير',
@@ -80,6 +80,7 @@ export default {
boolean: '${name} ليس منطقيًا',
number: 'يجب أن يكون ${name} رقمًا',
},
+ colonText: ':',
},
picker: {
cancel: 'الإلغاء',
@@ -90,17 +91,22 @@ export default {
},
rate: {
texts: ['سيء', 'مخيب', 'عادي', 'جيد', 'ممتاز'],
- valueText: '{القيمة} نتيجة',
+ valueText: '{value} نقطة',
noValueText: 'لا توجد نقاط',
},
tabBar: {
newsAriaLabel: 'هناك أخبار جديدة',
- moreNewsAriaLabel: 'هناك الكثير من الأخبار السيئة',
- haveMoreNewsAriaLabel: 'هناك { قيمة }+ أخبار',
- haveNewsAriaLabel: 'هناك { قيمة } أخبار',
+ moreNewsAriaLabel: 'هناك الكثير من الأخبار',
+ haveMoreNewsAriaLabel: 'هناك {value}+ أخبار',
+ haveNewsAriaLabel: 'هناك {value} أخبار',
},
table: {
empty: 'البيانات الفارغة',
+ confirmText: 'نعم',
+ resetText: 'إعادة ضبط',
+ searchResultText: "تم العثور على بحث '{result}' و{count} من العناصر.",
+ clearFilterResultButtonText: 'صافي',
+ pullUpLoadingText: 'جاري التحميل',
},
list: {
loading: 'التحميل...',
@@ -115,6 +121,7 @@ export default {
waitingText: 'الانتظار',
failText: 'فشل',
successText: 'النجاح',
+ reloadText: 'إعادة الرفع',
},
},
guide: {
@@ -149,6 +156,26 @@ export default {
quote: 'اقتباس',
},
},
+ chatRecord: {
+ holdToTalk: 'اضغط مع الاستمرار للتحدث',
+ requestAuth: 'يرجى منح إذن الميكروفون',
+ releaseToCancel: 'اترك للإلغاء',
+ releaseToSend: 'اترك للإرسال، اسحب للأعلى للإلغاء',
+ cancelText: 'إلغاء',
+ sendText: 'إرسال',
+ busyTip: 'جارٍ التعرّف، يرجى الانتظار…',
+ recognizeFailTip: 'فشل التعرّف على التسجيل، يرجى المحاولة مرة أخرى',
+ missingPluginTip: 'إضافة التعرّف على الصوت WechatSI مفقودة',
+ authSettingFail: 'فشل الحصول على إعدادات إذن التسجيل',
+ openSettingFail: 'فشل فتح الإعدادات',
+ systemMicTitle: 'لا يمكن استخدام الميكروفون',
+ systemMicContent:
+ 'تم اكتشاف أن نظام الهاتف قد أوقف إذن الميكروفون لتطبيق "WeChat".\n\n' +
+ 'يرجى تفعيله من إعدادات النظام:\n' +
+ '- iOS: الإعدادات > WeChat > الميكروفون\n' +
+ '- Android: الإعدادات > إدارة التطبيقات > WeChat > الأذونات > الميكروفون\n\n' +
+ 'بعد التفعيل، عد إلى البرنامج المصغّر وحاول مرة أخرى.',
+ },
chatSender: {
placeholder: 'الرجاء إدخال الرسالة...',
sendText: 'إرسال',
diff --git a/packages/components/locale/en_US.ts b/packages/components/locale/en_US.ts
index 8fd3ec0398..06b995fcfe 100644
--- a/packages/components/locale/en_US.ts
+++ b/packages/components/locale/en_US.ts
@@ -41,7 +41,7 @@ export default {
title: 'Select Date',
cancel: 'Cancel',
confirm: 'Confirm',
- format: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD HH:mm:ss',
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
yearLabel: '',
monthLabel: '',
@@ -89,6 +89,12 @@ export default {
},
table: {
empty: 'Empty Data',
+ confirmText: 'Confirm',
+ resetText: 'Reset',
+ searchResultText:
+ 'Search "{result}". Found no items. | Search "{result}". Found 1 item. | Search "{result}". Found {count} items.',
+ clearFilterResultButtonText: 'Clear',
+ pullUpLoadingText: 'Loading...',
},
list: {
loading: 'Loading...',
@@ -138,6 +144,26 @@ export default {
quote: 'quote',
},
},
+ chatRecord: {
+ holdToTalk: 'Hold to talk',
+ requestAuth: 'Please authorize microphone permission',
+ releaseToCancel: 'Release to cancel',
+ releaseToSend: 'Release to send, swipe up to cancel',
+ cancelText: 'Cancel',
+ sendText: 'Send',
+ busyTip: 'Recognizing, please wait...',
+ recognizeFailTip: 'Voice recognition failed, please try again',
+ missingPluginTip: 'Missing voice recognition plugin WechatSI',
+ authSettingFail: 'Failed to get recording permission settings',
+ openSettingFail: 'Failed to open settings',
+ systemMicTitle: 'Cannot use microphone',
+ systemMicContent:
+ 'Detected that the phone system has disabled the microphone permission for "WeChat".\n\n' +
+ 'Please enable it in system settings:\n' +
+ '- iOS: Settings > WeChat > Microphone\n' +
+ '- Android: Settings > App Management > WeChat > Permissions > Microphone\n\n' +
+ 'After enabling, return to the Mini Program and try again.',
+ },
chatSender: {
placeholder: 'please enter message...',
sendText: 'send',
diff --git a/packages/components/locale/it_IT.ts b/packages/components/locale/it_IT.ts
index 58151a03a4..6b900e40d2 100644
--- a/packages/components/locale/it_IT.ts
+++ b/packages/components/locale/it_IT.ts
@@ -10,7 +10,7 @@ export default {
confirm: 'Conferma',
title: 'Seleziona Data',
weekdays: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
- monthTitle: '{mese} {anno}',
+ monthTitle: '{month} {year}',
months: [
'Gennaio',
'Febbraio',
@@ -41,7 +41,7 @@ export default {
title: 'Seleziona Data',
cancel: 'Annulla',
confirm: 'Conferma',
- format: 'DD-MM-YYYY',
+ format: 'DD-MM-YYYY HH:mm:ss',
months: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
yearLabel: 'Anno',
monthLabel: 'Mese',
@@ -78,17 +78,22 @@ export default {
},
rate: {
texts: ['Pessimo', 'Scarso', 'Normale', 'Buono', 'Eccellente'],
- valueText: '{valore} punteggio',
+ valueText: '{value} punteggio',
noValueText: 'Nessun punteggio',
},
tabBar: {
newsAriaLabel: 'Ci sono nuove notizie',
moreNewsAriaLabel: 'Ci sono molte notizie',
- haveMoreNewsAriaLabel: 'Ci sono {valore}+ notizie',
- haveNewsAriaLabel: 'Ci sono {valore} notizie',
+ haveMoreNewsAriaLabel: 'Ci sono {value}+ notizie',
+ haveNewsAriaLabel: 'Ci sono {value} notizie',
},
table: {
empty: 'Dati Vuoti',
+ confirmText: 'Conferma',
+ resetText: 'Ripristina',
+ searchResultText: 'Ricerca "{result}", trovati {count} risultati',
+ clearFilterResultButtonText: 'Pulisci il filtro',
+ pullUpLoadingText: 'In carica',
},
list: {
loading: 'Caricamento...',
@@ -103,6 +108,7 @@ export default {
waitingText: 'Attesa',
failText: 'Fallito',
successText: 'Successo',
+ reloadText: 'Ricarica',
},
},
guide: {
@@ -137,6 +143,26 @@ export default {
quote: 'Cita',
},
},
+ chatRecord: {
+ holdToTalk: 'Tieni premuto per parlare',
+ requestAuth: 'Autorizza il permesso del microfono',
+ releaseToCancel: 'Rilascia per annullare',
+ releaseToSend: 'Rilascia per inviare, scorri su per annullare',
+ cancelText: 'Annulla',
+ sendText: 'Invia',
+ busyTip: 'Riconoscimento in corso, attendere…',
+ recognizeFailTip: 'Riconoscimento vocale non riuscito, riprova',
+ missingPluginTip: 'Plugin di riconoscimento vocale WechatSI mancante',
+ authSettingFail: 'Impossibile ottenere le impostazioni del permesso di registrazione',
+ openSettingFail: 'Impossibile aprire le impostazioni',
+ systemMicTitle: 'Impossibile usare il microfono',
+ systemMicContent:
+ 'È stato rilevato che il sistema del telefono ha disabilitato il permesso del microfono per "WeChat".\n\n' +
+ 'Abilitalo nelle impostazioni di sistema:\n' +
+ '- iOS: Impostazioni > WeChat > Microfono\n' +
+ '- Android: Impostazioni > Gestione app > WeChat > Autorizzazioni > Microfono\n\n' +
+ 'Dopo averlo abilitato, torna al Mini Program e riprova.',
+ },
chatSender: {
placeholder: 'Inserisci il messaggio...',
sendText: 'Invia',
diff --git a/packages/components/locale/ja_JP.ts b/packages/components/locale/ja_JP.ts
index 90eaa5a10e..459883d689 100644
--- a/packages/components/locale/ja_JP.ts
+++ b/packages/components/locale/ja_JP.ts
@@ -28,7 +28,7 @@ export default {
title: '日付を選択',
cancel: 'キャンセル',
confirm: '確認',
- format: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD HH:mm:ss',
months: ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'],
yearLabel: '年',
monthLabel: '月',
@@ -76,6 +76,11 @@ export default {
},
table: {
empty: 'データがありません',
+ confirmText: '確認事項',
+ resetText: 'リセット',
+ searchResultText: '検索“{result}”,探す {count} 記事結果',
+ clearFilterResultButtonText: 'クリアフィルター',
+ pullUpLoadingText: '読み込み中',
},
list: {
loading: '読み込み中…',
@@ -90,6 +95,7 @@ export default {
waitingText: '待機中',
failText: '失敗しました',
successText: '成功しました',
+ reloadText: '再アップロード',
},
},
guide: {
@@ -124,6 +130,26 @@ export default {
quote: '引用',
},
},
+ chatRecord: {
+ holdToTalk: '押しながら話す',
+ requestAuth: 'マイクの権限を許可してください',
+ releaseToCancel: '離してキャンセル',
+ releaseToSend: '離して送信、上にスワイプでキャンセル',
+ cancelText: 'キャンセル',
+ sendText: '送信',
+ busyTip: '認識中です。しばらくお待ちください…',
+ recognizeFailTip: '録音の認識に失敗しました。もう一度お試しください',
+ missingPluginTip: '音声認識プラグイン WechatSI がありません',
+ authSettingFail: '録音権限の設定の取得に失敗しました',
+ openSettingFail: '設定を開けませんでした',
+ systemMicTitle: 'マイクを使用できません',
+ systemMicContent:
+ '携帯電話のシステムで「WeChat」のマイク権限がオフになっていることが検出されました。\n\n' +
+ 'システム設定で有効にしてください:\n' +
+ '- iOS:設定 > WeChat > マイク\n' +
+ '- Android:設定 > アプリ管理 > WeChat > 権限 > マイク\n\n' +
+ '有効にした後、ミニプログラムに戻って再度お試しください。',
+ },
chatSender: {
placeholder: 'メッセージを入力してください...',
sendText: '送信',
diff --git a/packages/components/locale/ko_KR.ts b/packages/components/locale/ko_KR.ts
index 668d27313f..1c832cf1f8 100644
--- a/packages/components/locale/ko_KR.ts
+++ b/packages/components/locale/ko_KR.ts
@@ -28,7 +28,7 @@ export default {
title: '날짜 선택',
cancel: '취소',
confirm: '확인',
- format: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD HH:mm:ss',
months: ['1 월', '2 월', '3 월', '4 월', '5 월', '6 월', '7 월', '8 월', '9 월', '10 월', '11 월', '12 월'],
yearLabel: '년',
monthLabel: '월',
@@ -76,6 +76,11 @@ export default {
},
table: {
empty: '빈 데이터',
+ confirmText: '확인하다',
+ resetText: '초기화',
+ searchResultText: '검색“{result}”,일어나 {count} 결과',
+ clearFilterResultButtonText: '클리어 필터',
+ pullUpLoadingText: '로딩 중입니다',
},
list: {
loading: '로딩 중...',
@@ -90,6 +95,7 @@ export default {
waitingText: '대기 중',
failText: '실패했습니다',
successText: '성공했습니다',
+ reloadText: '다시 업로드',
},
},
guide: {
@@ -124,6 +130,26 @@ export default {
quote: '인용',
},
},
+ chatRecord: {
+ holdToTalk: '길게 눌러 말하기',
+ requestAuth: '마이크 권한을 허용해 주세요',
+ releaseToCancel: '놓으면 취소',
+ releaseToSend: '놓으면 전송, 위로 밀면 취소',
+ cancelText: '취소',
+ sendText: '전송',
+ busyTip: '인식 중입니다. 잠시만 기다려 주세요…',
+ recognizeFailTip: '음성 인식에 실패했습니다. 다시 시도해 주세요',
+ missingPluginTip: '음성 인식 플러그인 WechatSI가 없습니다',
+ authSettingFail: '녹음 권한 설정을 가져오지 못했습니다',
+ openSettingFail: '설정을 열지 못했습니다',
+ systemMicTitle: '마이크를 사용할 수 없습니다',
+ systemMicContent:
+ '휴대폰 시스템에서 "WeChat"의 마이크 권한이 꺼져 있는 것으로 감지되었습니다.\n\n' +
+ '시스템 설정에서 활성화해 주세요:\n' +
+ '- iOS: 설정 > WeChat > 마이크\n' +
+ '- Android: 설정 > 앱 관리 > WeChat > 권한 > 마이크\n\n' +
+ '활성화한 후 미니 프로그램으로 돌아가 다시 시도해 주세요.',
+ },
chatSender: {
placeholder: '메시지를 입력하세요...',
sendText: '보내기',
diff --git a/packages/components/locale/ru_RU.ts b/packages/components/locale/ru_RU.ts
index 1d751d4b23..0271cbb278 100644
--- a/packages/components/locale/ru_RU.ts
+++ b/packages/components/locale/ru_RU.ts
@@ -40,7 +40,7 @@ export default {
title: 'Выберите время',
cancel: 'Отмена',
confirm: 'Подтвердить',
- format: 'DD.MM.YYYY',
+ format: 'DD.MM.YYYY HH:mm:ss',
months: [
'Январь',
'Февраль',
@@ -101,6 +101,11 @@ export default {
},
table: {
empty: 'Нет данных',
+ confirmText: 'Подтвердить',
+ resetText: 'Сбросить',
+ searchResultText: 'Поиск «{result}», найдено {count} результатов',
+ clearFilterResultButtonText: 'Очистить фильтр',
+ pullUpLoadingText: 'Загрузка',
},
list: {
loading: 'Загрузка...',
@@ -115,6 +120,7 @@ export default {
waitingText: 'Ожидание загрузки',
failText: 'Ошибка загрузки',
successText: 'Загрузка завершена',
+ reloadText: 'Загрузить повторно',
},
},
guide: {
@@ -149,6 +155,27 @@ export default {
quote: 'Цитировать',
},
},
+ chatRecord: {
+ holdToTalk: 'Нажмите и удерживайте, чтобы говорить',
+ requestAuth: 'Пожалуйста, предоставьте разрешение на использование микрофона',
+ releaseToCancel: 'Отпустите для отмены',
+ releaseToSend: 'Отпустите для отправки, проведите вверх для отмены',
+ cancelText: 'Отмена',
+ sendText: 'Отправить',
+ busyTip: 'Распознавание, пожалуйста, подождите…',
+ recognizeFailTip: 'Не удалось распознать запись, попробуйте снова',
+ missingPluginTip: 'Отсутствует плагин распознавания речи WechatSI',
+ authSettingFail: 'Не удалось получить настройки разрешения на запись',
+ openSettingFail: 'Не удалось открыть настройки',
+ systemMicTitle: 'Невозможно использовать микрофон',
+ systemMicContent:
+ 'Обнаружено, что в системе телефона отключено разрешение ' +
+ 'на использование микрофона для "WeChat".\n\n' +
+ 'Пожалуйста, включите его в настройках системы:\n' +
+ '- iOS: Настройки > WeChat > Микрофон\n' +
+ '- Android: Настройки > Управление приложениями > WeChat > Разрешения > Микрофон\n\n' +
+ 'После включения вернитесь в мини-программу и повторите попытку.',
+ },
chatSender: {
placeholder: 'Введите сообщение...',
sendText: 'Отправить',
diff --git a/packages/components/locale/zh_CN.ts b/packages/components/locale/zh_CN.ts
index eb276cec81..4c9989756d 100644
--- a/packages/components/locale/zh_CN.ts
+++ b/packages/components/locale/zh_CN.ts
@@ -28,7 +28,7 @@ export default {
title: '选择时间',
cancel: '取消',
confirm: '确定',
- format: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD HH:mm:ss',
months: ['1 月', '2 月', '3 月', '4 月', '5 月', '6 月', '7 月', '8 月', '9 月', '10 月', '11 月', '12 月'],
yearLabel: '年',
monthLabel: '月',
@@ -76,6 +76,11 @@ export default {
},
table: {
empty: '暂无数据',
+ confirmText: '确认',
+ resetText: '重置',
+ searchResultText: '搜索“{result}”,找到 {count} 条结果',
+ clearFilterResultButtonText: '清空筛选',
+ pullUpLoadingText: '加载中...',
},
list: {
loading: '加载中...',
@@ -125,6 +130,26 @@ export default {
quote: '引用',
},
},
+ chatRecord: {
+ holdToTalk: '按住说话',
+ requestAuth: '请授权麦克风权限',
+ releaseToCancel: '松开取消',
+ releaseToSend: '松开发送,上滑取消',
+ cancelText: '取消',
+ sendText: '发送',
+ busyTip: '识别中,请稍候…',
+ recognizeFailTip: '录音识别失败,请重试',
+ missingPluginTip: '缺少语音识别插件 WechatSI',
+ authSettingFail: '获取录音权限设置失败',
+ openSettingFail: '打开设置失败',
+ systemMicTitle: '无法使用麦克风',
+ systemMicContent:
+ '检测到手机系统已关闭"微信"的麦克风权限。\n\n' +
+ '请到系统设置中开启:\n' +
+ '- iOS:设置 > 微信 > 麦克风\n' +
+ '- Android:设置 > 应用管理 > 微信 > 权限 > 麦克风\n\n' +
+ '开启后返回小程序再试。',
+ },
chatSender: {
placeholder: '请输入消息...',
sendText: '发送',
diff --git a/packages/components/locale/zh_TW.ts b/packages/components/locale/zh_TW.ts
index eec001643a..b5619dfce0 100644
--- a/packages/components/locale/zh_TW.ts
+++ b/packages/components/locale/zh_TW.ts
@@ -28,7 +28,7 @@ export default {
title: '選擇時間',
cancel: '取消',
confirm: '確定',
- format: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD HH:mm:ss',
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
yearLabel: '年',
monthLabel: '月',
@@ -76,6 +76,11 @@ export default {
},
table: {
empty: '暫無數據',
+ confirmText: '確認',
+ resetText: '重置',
+ searchResultText: '搜尋"{result}",找到{count}項結果',
+ clearFilterResultButtonText: '清空篩選',
+ pullUpLoadingText: '加載中...',
},
list: {
loading: '加載中...',
@@ -90,6 +95,7 @@ export default {
waitingText: '待上傳',
failText: '上傳失敗',
successText: '上傳成功',
+ reloadText: '重新上傳',
},
},
guide: {
@@ -101,7 +107,7 @@ export default {
typography: {
expandText: '展開',
collapseText: '收起',
- copiedText: '復製成功',
+ copiedText: '複製成功',
},
qrcode: {
expiredText: '二維碼過期',
@@ -124,6 +130,26 @@ export default {
quote: '引用',
},
},
+ chatRecord: {
+ holdToTalk: '按住說話',
+ requestAuth: '請授權麥克風權限',
+ releaseToCancel: '鬆開取消',
+ releaseToSend: '鬆開發送,上滑取消',
+ cancelText: '取消',
+ sendText: '發送',
+ busyTip: '識別中,請稍候…',
+ recognizeFailTip: '錄音識別失敗,請重試',
+ missingPluginTip: '缺少語音識別外掛 WechatSI',
+ authSettingFail: '獲取錄音權限設置失敗',
+ openSettingFail: '打開設置失敗',
+ systemMicTitle: '無法使用麥克風',
+ systemMicContent:
+ '檢測到手機系統已關閉「微信」的麥克風權限。\n\n' +
+ '請到系統設置中開啟:\n' +
+ '- iOS:設置 > 微信 > 麥克風\n' +
+ '- Android:設置 > 應用管理 > 微信 > 權限 > 麥克風\n\n' +
+ '開啟後返回小程序再試。',
+ },
chatSender: {
placeholder: '請輸入消息...',
sendText: '發送',
diff --git a/packages/pro-components/chat/attachments/attachments.less b/packages/pro-components/chat/attachments/attachments.less
index 3ff937047e..b36d6ab7b8 100644
--- a/packages/pro-components/chat/attachments/attachments.less
+++ b/packages/pro-components/chat/attachments/attachments.less
@@ -142,7 +142,7 @@
align-items: center;
justify-content: center;
- image {
+ .attachment-image {
width: 100%;
height: 100%;
}
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..027ca4339d
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/README.en-US.md
@@ -0,0 +1,93 @@
+---
+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 | - | CSS(Cascading Style Sheets) | N
+custom-style | Object | - | CSS(Cascading Style Sheets),used to set style on virtual component | N
+auto-send | Boolean | false | \- | N
+bottom-height | Number | 0 | \- | N
+duration | Number | 60000 | \- | N
+lang | String | zh_CN | \- | N
+
+### ChatRecord Events
+
+name | params | description
+-- | -- | --
+cancel | \- | \-
+error | `(err: any)` | \-
+recognize | `(detail: { voicePath: string, voiceText: string, duration: number })` | \-
+
+### ChatRecord Slots
+
+name | Description
+-- | --
+speech-input | \-
+speech-no-auth | \-
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..9d6ba70323
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/README.md
@@ -0,0 +1,94 @@
+---
+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
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+style | Object | - | 样式 | N
+custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
+auto-send | Boolean | false | 是否自动发送(预留扩展) | N
+bottom-height | Number | 0 | 底部高度,用于适配键盘弹出时的布局 | N
+duration | Number | 60000 | 最大录音时长(ms) | N
+lang | String | zh_CN | 识别语言(WechatSI 插件参数) | N
+
+### ChatRecord Events
+
+名称 | 参数 | 描述
+-- | -- | --
+cancel | \- | 取消录音时触发
+error | `(err: any)` | 录音识别错误时触发
+recognize | `(detail: { voicePath: string, voiceText: string, duration: number })` | 语音识别完成时触发
+
+### ChatRecord Slots
+
+名称 | 描述
+-- | --
+speech-input | 语音输入按钮插槽,自定义语音输入按钮区域内容
+speech-no-auth | 语音授权按钮插槽,自定义语音授权按钮区域内容
diff --git a/packages/pro-components/chat/chat-record/_example/base/index.js b/packages/pro-components/chat/chat-record/_example/base/index.js
new file mode 100644
index 0000000000..4028c131de
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/base/index.js
@@ -0,0 +1,319 @@
+import Toast from 'tdesign-miniprogram/toast';
+
+let uniqueId = 0;
+const getUniqueKey = () => {
+ uniqueId += 1;
+ return `key-${uniqueId}`;
+};
+
+const mockData = `南极的自动提款机并没有一个特定的专属名称,但历史上确实有一台ATM机曾短暂存在于南极的**麦克默多站**(McMurdo Station)。这台ATM由美国**富兰克林国家银行**(Wells Fargo)于1998年安装,主要供驻扎在该站的科研人员使用。不过,由于南极的极端环境和极低的人口密度,这台ATM机并未长期运行,最终被移除。
+
+**背景补充:**
+- **麦克默多站**是美国在南极最大的科研基地,夏季人口可达约1,000人,冬季约200人。
+- 该ATM机更多是作为一种象征性服务存在,实际使用频率极低,因为南极科考人员通常依靠预支资金或电子支付。
+- 目前南极已无长期运行的ATM机,现代科考站更多依赖非现金交易方式。
+
+南极作为非主权领土,其基础设施以科研和生活支持为主,商业金融服务非常有限。若有类似设施,通常是临时或实验性质的。`;
+
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+const fetchStream = async (str, options) => {
+ const { success, complete, delay = 100 } = options;
+
+ const arr = str.split('');
+
+ for (let i = 0; i < arr.length; i += 1) {
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(delay);
+ success(arr[i]);
+ }
+
+ complete();
+};
+
+const getNavigationBarHeight = () => {
+ try {
+ const systemInfo = wx.getSystemInfoSync();
+
+ // 获取状态栏高度
+ const statusBarHeight = systemInfo.statusBarHeight || 0;
+
+ // 获取胶囊按钮位置信息
+ const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
+
+ if (menuButtonInfo) {
+ // 计算导航栏高度:胶囊按钮底部位置 + 胶囊按钮与底部的间距
+ const navigationBarHeight = menuButtonInfo.bottom + (menuButtonInfo.top - statusBarHeight);
+ return navigationBarHeight;
+ }
+ // 如果无法获取胶囊按钮信息,使用默认计算方式
+ // 一般情况下,导航栏高度 = 状态栏高度 + 44px(标准导航栏高度)
+ const navigationBarHeight = statusBarHeight + 44;
+
+ return navigationBarHeight;
+ } catch (error) {
+ console.error('获取导航栏高度失败:', error);
+ return 44;
+ }
+};
+
+Component({
+ options: {
+ styleIsolation: 'shared',
+ },
+ data: {
+ renderPresets: [{ name: 'send', type: 'icon' }],
+ chatList: [
+ {
+ avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png',
+ role: 'assistant',
+ status: 'complete',
+ content: [
+ {
+ type: 'text',
+ data: '它叫 McMurdo Station ATM,是美国富国银行安装在南极洲最大科学中心麦克默多站的一台自动提款机。',
+ },
+ ],
+ chatId: getUniqueKey(),
+ comment: '',
+ },
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ data: '牛顿第一定律是否适用于所有参考系?',
+ },
+ ],
+ chatId: getUniqueKey(),
+ },
+ ],
+ value: '', // 输入框的值
+ loading: false, // 加载状态
+ disabled: false, // 禁用状态
+ inputStyle: '', // 输入框样式
+ contentHeight: '100vh', // 内容高度
+ animation: 'dots',
+ activePopoverId: '', // 当前打开悬浮actionbar的chatId
+ longPressPosition: null, // 长按位置对象
+ keyboardHeight: 0, // 键盘高度(px)
+ },
+
+ methods: {
+ // 调用chatList的滚动到底部方法
+ scrollToBottom() {
+ const chatListComponent = this.selectComponent('#chatList');
+ if (chatListComponent && typeof chatListComponent.scrollToBottom === 'function') {
+ chatListComponent.scrollToBottom();
+ }
+ },
+ onScroll(e) {
+ console.log('监听滚动', e);
+ },
+ /**
+ * 监听键盘高度变化,弹出键盘时将 chat-sender 往上推
+ */
+ onKeyboardHeightChange(e) {
+ const height = (e && e.detail && e.detail.height) || 0;
+ this.setData({
+ keyboardHeight: height,
+ });
+ },
+ /**
+ * 语音识别回调
+ * @param {Object} e - 事件对象
+ */
+ handleRecognize(e) {
+ const voiceMsg = e.detail;
+ console.log('语音识别结果:', voiceMsg);
+
+ if (voiceMsg.voiceText) {
+ // 自动发送识别到的文本
+ this.onSend({ detail: { value: voiceMsg.voiceText } });
+ }
+ },
+ // 发送消息事件处理
+ onSend(e) {
+ const { value } = e.detail;
+ if (!value || value.trim() === '') return;
+
+ // 创建用户消息对象
+ const userMessage = {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ data: value.trim(),
+ },
+ ],
+ chatId: getUniqueKey(),
+ };
+
+ // 将用户消息插入到chatList的开头(因为reverse为true,所以用unshift)
+ this.setData({
+ chatList: [userMessage, ...this.data.chatList],
+ value: '', // 清空输入框
+ });
+
+ // 模拟助手回复(可选)
+ this.simulateAssistantReply(value.trim());
+ },
+
+ // 停止事件处理
+ onStop() {
+ this.setData({
+ loading: false,
+ });
+ },
+
+ // 聚焦事件处理
+ onFocus() {
+ console.log('输入框聚焦');
+ },
+ // 获取当前时间
+ getCurrentTime() {
+ const now = new Date();
+ const hours = now.getHours().toString().padStart(2, '0');
+ const minutes = now.getMinutes().toString().padStart(2, '0');
+ return `${hours}:${minutes}`;
+ },
+
+ // 模拟助手回复
+ simulateAssistantReply() {
+ this.setData({ loading: true });
+ // 请求中
+ const assistantMessage = {
+ role: 'assistant',
+ content: [
+ {
+ type: 'markdown',
+ data: '',
+ },
+ ],
+ avatar: 'https://tdesign.gtimg.com/site/chat-avatar.png',
+ status: 'pending',
+ chatId: getUniqueKey(),
+ comment: '',
+ };
+ this.setData({
+ chatList: [assistantMessage, ...this.data.chatList],
+ });
+ const that = this;
+ wx.nextTick(() => {
+ fetchStream(mockData, {
+ success(result) {
+ // 生文中
+ if (!that.data.loading) return;
+ that.setData({
+ 'chatList[0].status': 'streaming',
+ 'chatList[0].content[0].data': that.data.chatList[0].content[0].data + result,
+ });
+ },
+ complete() {
+ that.setData({
+ 'chatList[0].status': 'complete',
+ loading: false,
+ });
+ },
+ });
+ });
+ },
+ handleAction(e) {
+ const { name, active, data, chatId } = e.detail;
+
+ let message = '';
+ switch (name) {
+ case 'replay':
+ message = '重新生成';
+ break;
+ case 'copy':
+ console.log(data);
+ message = '复制成功';
+ break;
+ case 'good':
+ message = active ? '点赞成功' : '取消点赞';
+ break;
+ case 'bad':
+ message = active ? '点踩成功' : '取消点踩';
+ break;
+ case 'share':
+ message = '分享功能';
+ break;
+ default:
+ message = `执行了${name}操作`;
+ }
+
+ Toast({
+ context: this,
+ selector: '#t-toast',
+ message,
+ theme: 'success',
+ });
+
+ if (name === 'good' || name === 'bad') {
+ this.data.chatList.forEach((item) => {
+ if (item.chatId === chatId) {
+ item.comment = active ? name : '';
+ }
+ });
+ this.setData({
+ chatList: this.data.chatList,
+ });
+ }
+ },
+ showPopover(e) {
+ const { id, longPressPosition } = e.detail;
+
+ let role = '';
+ this.data.chatList.forEach((item) => {
+ if (item.chatId === id) {
+ role = item.role;
+ }
+ });
+
+ // 仅当 role 为 user 时才显示 popover
+ if (role !== 'user') {
+ return;
+ }
+
+ this.setData({
+ activePopoverId: id,
+ longPressPosition,
+ });
+ },
+ handlePopoverAction(e) {
+ e.detail.chatId = this.data.activePopoverId;
+ this.handleAction(e);
+ },
+ onClick(e) {
+ const { node } = e.detail;
+ console.log('点击节点', node);
+ },
+ },
+ lifetimes: {
+ attached: function () {
+ /**
+ * 计算内容区域高度
+ * 生成CSS calc表达式:calc(100vh - 导航高度 - 底部安全区域高度)
+ */
+ try {
+ // 获取当前的导航栏高度和安全区域高度
+ const navigationBarHeight = getNavigationBarHeight() || 0;
+
+ // 生成CSS calc表达式字符串
+ const contentHeight = `calc(100vh - ${navigationBarHeight}px)`;
+
+ this.setData({
+ contentHeight: contentHeight,
+ });
+
+ console.log('内容区域高度CSS表达式:', contentHeight);
+ } catch (error) {
+ console.error('生成内容高度表达式失败:', error);
+ this.setData({
+ contentHeight: 'calc(100vh - 96rpx)',
+ });
+ }
+ },
+ },
+});
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..f78024b39b
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/base/index.json
@@ -0,0 +1,15 @@
+{
+ "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",
+ "t-icon": "tdesign-miniprogram/icon/icon",
+ "t-chat": "tdesign-miniprogram/chat-list/chat-list",
+ "t-chat-actionbar": "tdesign-miniprogram/chat-actionbar/chat-actionbar",
+ "t-toast": "tdesign-miniprogram/toast/toast",
+ "t-chat-message": "tdesign-miniprogram/chat-message/chat-message",
+ "t-chat-content": "tdesign-miniprogram/chat-content/chat-content"
+ }
+}
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..f2c4ef2f24
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/base/index.wxml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+ 按住说话
+ 请授权麦克风权限
+
+
+
+
+
+
+
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..e1671a9d29
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/base/index.wxss
@@ -0,0 +1,68 @@
+.chat-box {
+ padding-top: 32rpx;
+ box-sizing: border-box;
+}
+
+.t-chat__list {
+ padding: 0 0 0 32rpx;
+ box-sizing: border-box;
+}
+
+.t-chat-message {
+ padding: 0 32rpx;
+}
+
+.speech-slot-btn,
+.speech-btn-error {
+ /* 不设置 height,让 chat-sender 父容器的 min-height(134rpx) 自然撑起,
+ 避免与 t-chat-record-hook 的 padding/line-height 叠加导致总高度超过 keyboard 模式 */
+ width: 100%;
+ text-align: center;
+ gap: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ line-height: 48rpx;
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.speech-btn-error {
+ color: red;
+}
+
+.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;
+}
+
+/* 语音输入按钮样式 */
+.icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ width: 64rpx;
+ height: 64rpx;
+ background-color: #fff;
+ border: 1px solid #dcdcdc;
+}
+.voice-input-button {
+ font-size: 36rpx;
+ color: #000;
+}
+
+wx-button:after {
+ border: none;
+}
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..d39ebf4c54
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/chat-record.json
@@ -0,0 +1,6 @@
+{
+ "navigationBarTitleText": "ChatRecord",
+ "usingComponents": {
+ "base": "./base"
+ }
+}
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..21d248cfb1
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/chat-record.less
@@ -0,0 +1,25 @@
+: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;
+}
+
+page {
+ background-color: var(--td-bg-color-container);
+}
+
+.demo {
+ padding-bottom: 0;
+}
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..fa936ad7d8
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/_example/chat-record.wxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
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..1dc0036ace
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/chat-record.json
@@ -0,0 +1,5 @@
+{
+ "component": true,
+ "usingComponents": {},
+ "styleIsolation": "apply-shared"
+}
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..0ebcda2df6
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/chat-record.less
@@ -0,0 +1,383 @@
+@import '../../../components/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-bg: #f5f5f5;
+
+// 遮罩层
+@chat-record-mask-z-index: 5100;
+
+// 主容器
+@chat-record-main-z-index: 5101;
+
+// 气泡颜色
+@chat-record-color-blue: #2979ff;
+@chat-record-color-red: #ff5252;
+@chat-record-color-white: #ffffff;
+
+// 波浪条
+@chat-record-wave-height: 28rpx;
+@chat-record-wave-gap: 2rpx;
+@chat-record-wave-item-width: 8rpx;
+@chat-record-wave-item-height: 14rpx;
+@chat-record-wave-item-radius: 90rpx;
+
+// 底部操作区
+@chat-record-footer-height: 260rpx;
+@chat-record-tips-font-size: 28rpx;
+@chat-record-tips-line-height: 44rpx;
+
+// 全宽录音大按钮
+@chat-record-main-btn-bg-record: #2979ff;
+@chat-record-main-btn-bg-cancel: #ff5252;
+
+// 确认按钮区域
+@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;
+ background: @chat-record-hook-bg;
+ }
+
+ // ==================== 遮罩层和主容器 ====================
+
+ &-audio-input {
+ &.show {
+ .@{chat-record}-audio-input__mask,
+ .@{chat-record}-audio-input__main {
+ opacity: 1;
+ pointer-events: auto;
+ }
+ }
+
+ // 遮罩层:设计稿为纯白背景,遮住 chat-sender 上半部分输入区域
+ &__mask {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ min-height: 420rpx;
+ z-index: @chat-record-mask-z-index;
+ background: #ffffff;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity @chat-record-transition-opacity;
+ }
+
+ // 主容器
+ &__main {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 140rpx;
+ 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;
+ }
+
+ // 音量波浪条
+ .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 循环生成 27 个波浪条的动画延迟
+ .generate-wave-delays(@n, @i: 1) when (@i =< @n) {
+ .wave-item:nth-child(@{i}) {
+ animation-delay: unit((-1.5 + (@i - 1) * 0.05), s);
+ }
+
+ .generate-wave-delays(@n, (@i + 1));
+ }
+
+ .generate-wave-delays(27);
+ }
+
+ // ==================== 底部操作区 ====================
+
+ &__ft {
+ position: relative;
+ width: 100%;
+ height: @chat-record-footer-height;
+ opacity: 1;
+ transition: opacity 0.3s ease;
+ animation: fadeIn 0.3s ease;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ padding-bottom: 48rpx;
+ box-sizing: border-box;
+
+ // 外部音波动画(独立于按钮):宽度略小于按钮(按钮 calc(100% - 64rpx),此处 calc(100% - 128rpx))
+ // 波浪条均匀分布以"撑满"音波区域,视觉上比之前 280rpx 长很多
+ .audio-wave-outer {
+ justify-content: space-between;
+ width: calc(100% - 158rpx);
+ margin-bottom: 12rpx;
+ pointer-events: none;
+ // 覆盖基类的 gap(基类用 gap,此处用 space-between 自动分布)
+ gap: 0;
+
+ &.wave-blue .wave-item {
+ background: @chat-record-color-blue;
+ }
+
+ &.wave-red .wave-item {
+ background: @chat-record-color-red;
+ }
+ }
+
+ // 提示文字容器:固定高度,作为"滚轮"舞台
+ .tips-text {
+ position: relative;
+ width: 100%;
+ height: @chat-record-tips-line-height;
+ margin-bottom: 24rpx;
+ pointer-events: none;
+ overflow: hidden;
+
+ // 单条提示文字:绝对定位,通过 transform/opacity 做上下切换
+ .tips-item {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ width: 100%;
+ text-align: center;
+ font-size: @chat-record-tips-font-size;
+ font-weight: 500;
+ line-height: @chat-record-tips-line-height;
+ will-change: transform, opacity;
+ transition:
+ transform @chat-record-transition-ease,
+ opacity @chat-record-transition-opacity;
+
+ // 默认状态(在视图中)
+ &.is-in {
+ transform: translate3d(0, 0, 0);
+ opacity: 1;
+ }
+
+ // 向上滑出(被上一条挤上去)
+ &.is-out-up {
+ transform: translate3d(0, -100%, 0);
+ opacity: 0;
+ }
+
+ // 在下方等待(从下方将要滑入)
+ &.is-out-down {
+ transform: translate3d(0, 100%, 0);
+ opacity: 0;
+ }
+ }
+
+ // 文案颜色:发送态黑色 60%,取消态红色
+ .tips-send {
+ color: rgba(0, 0, 0, 0.6);
+ }
+
+ .tips-cancel {
+ color: @chat-record-color-red;
+ }
+ }
+
+ // 取消状态保留兼容(其它地方若有依赖):不再统一改色,颜色由 .tips-cancel 自带
+ &.release_cancel .tips-text {
+ color: inherit;
+ }
+ .record-main-btn {
+ width: calc(100% - 64rpx);
+ height: 100rpx;
+ border-radius: @chat-record-hook-border-radius;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color @chat-record-transition-ease;
+
+ &.is-record {
+ background-color: @chat-record-main-btn-bg-record;
+ }
+
+ &.is-cancel {
+ background-color: @chat-record-main-btn-bg-cancel;
+ }
+ }
+ // ==================== 确认按钮区域 ====================
+
+ .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/pro-components/chat/chat-record/chat-record.ts b/packages/pro-components/chat/chat-record/chat-record.ts
new file mode 100644
index 0000000000..ab5f48e3a7
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/chat-record.ts
@@ -0,0 +1,583 @@
+import { SuperComponent, wxComponent } from '../../../components/common/src/index';
+import config from '../../../components/common/config';
+import usingConfig from '../../../components/mixins/using-config';
+import props from './props';
+
+const { prefix } = config;
+const componentName = 'chat-record';
+const name = `${prefix}-${componentName}`;
+
+// 交互阈值配置(单位:px)
+const MOVE_THRESHOLD_Y = 60;
+
+// 录音启动延时定时器(模拟长按)
+let startRecordTimer: number | null = null;
+
+type VoiceInfo = {
+ voicePath: string;
+ voiceText: string;
+ duration: number; // 秒
+};
+
+@wxComponent()
+export default class ChatRecord extends SuperComponent {
+ behaviors = [usingConfig({ componentName })];
+
+ options = {
+ multipleSlots: true,
+ };
+
+ properties = props;
+
+ data = {
+ classPrefix: name,
+
+ // 权限
+ recordAuthSetting: false,
+ // 是否已授权 scope.record(未授权时应展示去设置/授权引导)
+ recordAuthStatus: false,
+ // 是否已向用户申请过授权但被拒绝
+ recordAuthDenied: false,
+
+ // UI 状态
+ showMask: false,
+ activeBtnCancel: false,
+ activeBtnSend: false,
+
+ // 流程状态
+ processStatus: 'idle' as 'idle' | 'recording' | 'processing' | 'confirm' | 'error',
+ interactStatus: 'normal' as 'normal' | 'release_cancel' | 'release_convert',
+
+ // 录音数据
+ voiceInfo: {
+ voicePath: '',
+ voiceText: '',
+ duration: 0,
+ } as VoiceInfo,
+
+ // 转文字结果
+ translateResult: '',
+ startTouch: { x: 0, y: 0 },
+ isStarted: false,
+ isManagerBusy: false,
+ ignoreNextOnStop: false,
+ managerRecording: false,
+ waveList: Array.from({ length: 27 }).map((_, i) => i + 1),
+ };
+
+ private manager: any = null;
+
+ methods = {
+ // 清理长按延时启动定时器
+ clearStartRecordTimer(timer: number | null) {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ startRecordTimer = null;
+ },
+
+ initRecordManager() {
+ try {
+ // WechatSI 插件需要你在 app.json 里配置 plugins: { WechatSI: { ... } }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const plugin = requirePlugin('WechatSI');
+ this.manager = plugin?.getRecordRecognitionManager?.();
+ } catch (e) {
+ this.manager = null;
+ }
+
+ const { manager } = this;
+ if (!manager) return;
+
+ manager.onStart = () => {
+ this.setData({
+ managerRecording: true,
+ processStatus: 'recording',
+ });
+ };
+
+ manager.onRecognize = (res: any) => {
+ // res.result 实时识别结果;res.end 表示结束
+ if (res?.result && !res?.end) {
+ const voiceText = res.result;
+ this.setData({
+ voiceInfo: {
+ ...this.data.voiceInfo,
+ voiceText,
+ },
+ });
+ if (this.data.interactStatus === 'release_convert') {
+ this.setData({ translateResult: voiceText });
+ }
+ }
+ };
+
+ manager.onStop = (res: any) => {
+ this.setData({
+ managerRecording: false,
+ isManagerBusy: false,
+ });
+
+ if (this.data.ignoreNextOnStop) {
+ this.setData({ ignoreNextOnStop: false });
+ return;
+ }
+
+ if (this.data.processStatus === 'error') return;
+
+ const tempFilePath = res?.tempFilePath || '';
+ const durationMs = res?.duration || 0;
+ const duration = Math.floor(durationMs / 1000) || 1;
+ const voiceText = res?.result || '';
+
+ // 直接发送,不进入 confirm 状态
+ this.setData({
+ voiceInfo: {
+ voicePath: tempFilePath,
+ voiceText,
+ duration,
+ },
+ translateResult: voiceText,
+ });
+
+ this.triggerEvent('recognize', {
+ voicePath: tempFilePath,
+ voiceText,
+ duration,
+ });
+
+ // 发送后重置状态
+ setTimeout(() => {
+ this.resetState();
+ }, 100);
+ };
+
+ manager.onError = (err: any) => {
+ wx.showToast({
+ icon: 'none',
+ title: this.data.globalConfig?.recognizeFailTip || '录音识别失败,请重试',
+ duration: 2000,
+ });
+ this.setData({
+ isManagerBusy: false,
+ managerRecording: false,
+ processStatus: 'error',
+ interactStatus: 'normal',
+ translateResult: '',
+ activeBtnCancel: false,
+ activeBtnSend: false,
+ showMask: false,
+ });
+
+ // 清理长按延时启动定时器,避免错误期间仍触发录音
+ this.clearStartRecordTimer(startRecordTimer);
+
+ this.triggerEvent('error', err);
+ };
+ },
+
+ // 键盘高度监听已移至示例页面处理
+ async getVoiceAuthSetting() {
+ return new Promise((resolve, reject) => {
+ wx.getSetting({
+ success: (res) => {
+ const authSetting = res.authSetting || {};
+ const hasRequested = Object.prototype.hasOwnProperty.call(authSetting, 'scope.record');
+ const recordAuthStatus = !!authSetting['scope.record'];
+ const recordAuthSetting = recordAuthStatus;
+ const recordAuthDenied = hasRequested && !recordAuthStatus;
+
+ this.setData({ recordAuthSetting, recordAuthStatus, recordAuthDenied });
+ resolve(recordAuthSetting);
+ },
+ fail: () => reject(new Error(this.data.globalConfig?.authSettingFail || '获取录音权限设置失败')),
+ });
+ });
+ },
+
+ async requestRecordAuth() {
+ // 系统层检测:如果系统把“微信-麦克风”关了,这里会提示去系统设置
+ const sysOk = await this.checkSystemMicPermission();
+ if (!sysOk) return;
+
+ try {
+ // 先尝试直接弹授权框(必须在用户点击事件里调用)
+ await wx.authorize({ scope: 'scope.record' });
+
+ // 授权成功后刷新状态
+ await this.getVoiceAuthSetting();
+ } catch (e) {
+ // 用户拒绝 / 或已拒绝不再询问 -> 才引导去设置页
+ this.openVoiceSetting();
+ }
+ },
+
+ /**
+ * 申请小程序录音权限(scope.record)
+ * 同时检测"系统层面是否禁用了微信麦克风"。
+ */
+ async applyAuth() {
+ // 先检测系统层面的麦克风权限(如果系统禁用,openSetting 里通常也不会出现开关)
+ const sysOk = await this.checkSystemMicPermission();
+ if (!sysOk) {
+ this.setData({ recordAuthSetting: false, recordAuthStatus: false });
+ return false;
+ }
+
+ return new Promise((resolve, reject) => {
+ wx.authorize({
+ scope: 'scope.record',
+ success: () => {
+ this.setData({ recordAuthSetting: true, recordAuthStatus: true });
+ resolve(true);
+ },
+ fail: (err: any) => {
+ // 这里通常是用户拒绝(或已拒绝且不再询问)
+ this.setData({ recordAuthSetting: false, recordAuthStatus: false });
+ reject(err);
+ },
+ });
+ });
+ },
+
+ /**
+ * 检测系统层面的麦克风权限是否允许"微信"使用。
+ * - iOS/Android 在系统设置里关闭微信麦克风时:
+ * openSetting 页面可能没有"录音"开关,因此必须给出明确引导。
+ */
+ async checkSystemMicPermission() {
+ // 低版本基础库可能没有该 API,缺失时默认继续走授权流程
+ // @ts-ignore
+ if (typeof wx.getAppAuthorizeSetting !== 'function') return true;
+
+ try {
+ // @ts-ignore
+ const res = wx.getAppAuthorizeSetting();
+ const mic = res?.microphoneAuthorized; // 'authorized' | 'denied' | 'not determined'
+
+ if (mic === 'denied') {
+ this.showSystemMicGuide();
+ return false;
+ }
+ return true;
+ } catch (e) {
+ return true;
+ }
+ },
+
+ /** 系统麦克风被关闭时的明确引导 */
+ showSystemMicGuide() {
+ const { globalConfig } = this.data;
+ wx.showModal({
+ title: globalConfig?.systemMicTitle || '无法使用麦克风',
+ content:
+ globalConfig?.systemMicContent ||
+ '检测到手机系统已关闭"微信"的麦克风权限。\n\n请到系统设置中开启:\n- iOS:设置 > 微信 > 麦克风\n- Android:设置 > 应用管理 > 微信 > 权限 > 麦克风\n\n开启后返回小程序再试。',
+ showCancel: false,
+ });
+ },
+
+ async openVoiceSetting() {
+ // 先判断是否系统层禁用了微信麦克风;如果是,直接提示去系统设置
+ const sysOk = await this.checkSystemMicPermission();
+ if (!sysOk) return;
+
+ wx.openSetting({
+ success: (r) => {
+ const recordAuthSetting = !!r.authSetting?.['scope.record'];
+ const recordAuthStatus = !!r.authSetting?.['scope.record'];
+ this.setData({ recordAuthSetting, recordAuthStatus });
+ },
+ fail: () => {
+ wx.showToast({
+ icon: 'none',
+ title: this.data.globalConfig?.openSettingFail || '打开设置失败',
+ });
+ },
+ });
+ },
+
+ // ==================== 授权设置回调 ====================
+ onOpenSetting(e: WechatMiniprogram.OpenSettingSuccessCallbackResult) {
+ const { authSetting } = e;
+ if (authSetting?.['scope.record']) {
+ // 用户授权了录音权限
+ this.setData({
+ recordAuthSetting: true,
+ recordAuthStatus: true,
+ });
+ } else {
+ // 用户未授权录音权限
+ this.setData({
+ recordAuthSetting: false,
+ recordAuthStatus: false,
+ });
+ }
+ },
+
+ // ==================== 录音流程 ====================
+ async startRecord(e: WechatMiniprogram.TouchEvent) {
+ // 避免识别回调/stop 尚未完成又开始
+ if (this.data.isManagerBusy) {
+ wx.showToast({
+ icon: 'none',
+ title: this.data.globalConfig?.busyTip || '识别中,请稍候…',
+ });
+ return;
+ }
+
+ if (this.data.processStatus === 'error') {
+ this.resetState();
+ }
+
+ this.setData({ isStarted: true });
+
+ // 授权检查
+ try {
+ await this.getVoiceAuthSetting();
+ if (!this.data.recordAuthSetting) {
+ const ok = await this.applyAuth();
+ if (!ok) {
+ this.setData({ isStarted: false });
+ return;
+ }
+ this.setData({ isStarted: false });
+ return;
+ }
+ } catch (error) {
+ this.setData({ isStarted: false });
+ return;
+ }
+
+ // 记录起始触摸点
+ const touch = e?.changedTouches?.[0];
+ if (touch) {
+ this.setData({ startTouch: { x: touch.clientX, y: touch.clientY } });
+ }
+
+ this.setData({
+ showMask: true,
+ processStatus: 'recording',
+ interactStatus: 'normal',
+ translateResult: '',
+ voiceInfo: { voicePath: '', voiceText: '', duration: 0 },
+ });
+
+ // 100ms 后开始录音(与原组件一致)
+ this.clearStartRecordTimer(startRecordTimer);
+ startRecordTimer = setTimeout(() => {
+ if (!this.data.isStarted) return;
+
+ if (!this.manager) {
+ this.initRecordManager();
+ }
+
+ if (this.manager) {
+ this.setData({ isManagerBusy: true });
+ // 从 properties 取 lang/duration
+ // @ts-ignore
+ const duration = this.properties?.duration ?? 60000;
+ // @ts-ignore
+ const lang = this.properties?.lang ?? 'zh_CN';
+ this.manager.start({ duration, lang });
+ } else {
+ // 插件不可用
+ wx.showToast({
+ icon: 'none',
+ title: this.data.globalConfig?.missingPluginTip || '缺少语音识别插件 WechatSI',
+ });
+ this.setData({ processStatus: 'error' });
+ }
+ }, 100) as unknown as number;
+ },
+
+ touchmove(e: WechatMiniprogram.TouchEvent) {
+ if (!this.data.isStarted || !this.data.showMask) return;
+ if (this.data.processStatus !== 'recording') return;
+
+ const touch = (e.touches && e.touches[0]) || (e.changedTouches && e.changedTouches[0]);
+ if (!touch) return;
+
+ const deltaY = touch.clientY - this.data.startTouch.y;
+
+ // 只有上滑取消,没有转文字
+ let interactStatus: typeof this.data.interactStatus = 'normal';
+ if (deltaY < -MOVE_THRESHOLD_Y) {
+ interactStatus = 'release_cancel';
+ }
+
+ if (interactStatus !== this.data.interactStatus) {
+ this.setData({ interactStatus });
+ }
+ },
+
+ stopRecord() {
+ if (this.data.processStatus !== 'recording') return;
+
+ this.setData({ isStarted: false });
+
+ this.clearStartRecordTimer(startRecordTimer);
+
+ // 停止录音
+ if (this.manager) {
+ if (this.data.managerRecording) {
+ this.manager.stop();
+ } else {
+ // 录音尚未开始,取消启动
+ this.setData({ ignoreNextOnStop: true });
+ }
+ }
+
+ if (this.data.interactStatus === 'release_cancel') {
+ // 取消时不让 onStop 把 UI 推到 confirm
+ this.setData({
+ ignoreNextOnStop: true,
+ activeBtnCancel: true,
+ });
+ this.cancelRecord();
+ }
+ // 正常松手:等待 onStop 回调,自动发送
+ },
+
+ touchcancel() {
+ this.cancelRecord();
+ },
+
+ // ==================== 业务逻辑 ====================
+ cancelRecord() {
+ // 关闭弹窗
+ this.setData({ showMask: false });
+ // 延迟重置状态,确保事件能够正确触发
+ setTimeout(() => {
+ this.resetState();
+ }, 100);
+ this.triggerEvent('cancel');
+ },
+
+ convertToText() {
+ this.setData({
+ processStatus: 'confirm',
+ interactStatus: 'normal',
+ translateResult: this.data.voiceInfo.voiceText,
+ });
+ this.updateBubbleClass();
+ },
+
+ sendVoice() {
+ // 发送语音原始信息(按原组件逻辑保留)
+ this.triggerEvent('recognize', {
+ voicePath: this.data.voiceInfo.voicePath,
+ voiceText: this.data.voiceInfo.voiceText,
+ duration: this.data.voiceInfo.duration,
+ });
+ // 延迟重置状态,确保事件能够正确触发
+ setTimeout(() => {
+ this.resetState();
+ }, 100);
+ },
+
+ handleSendVoiceMsg() {
+ if (this.data.processStatus === 'error') {
+ return;
+ }
+
+ // 发送语音消息,包含语音文件和转文字结果
+ this.triggerEvent('recognize', {
+ voicePath: this.data.voiceInfo.voicePath,
+ voiceText: this.data.translateResult || this.data.voiceInfo.voiceText,
+ duration: this.data.voiceInfo.duration,
+ });
+
+ // 关闭弹窗
+ this.setData({ showMask: false });
+ // 延迟重置状态,确保事件能够正确触发
+ setTimeout(() => {
+ this.resetState();
+ }, 100);
+ },
+
+ handleCancelSend() {
+ if (this.data.processStatus === 'error') {
+ this.resetState();
+ return;
+ }
+ this.cancelRecord();
+ },
+
+ onTranslateInput(e: WechatMiniprogram.Input) {
+ this.setData({
+ translateResult: (e.detail?.value ?? '') as string,
+ activeBtnCancel: !!e.detail?.value,
+ activeBtnSend: !!e.detail?.value,
+ });
+ },
+
+ // ==================== 状态管理 ====================
+ resetState() {
+ this.clearStartRecordTimer(startRecordTimer);
+
+ if (this.manager && this.data.managerRecording) {
+ try {
+ this.manager.stop();
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ this.setData({
+ isStarted: false,
+ managerRecording: false,
+ showMask: false,
+ processStatus: 'idle',
+ interactStatus: 'normal',
+ translateResult: '',
+ voiceInfo: { voicePath: '', voiceText: '', duration: 0 },
+ activeBtnCancel: false,
+ activeBtnSend: false,
+ isManagerBusy: false,
+ ignoreNextOnStop: false,
+ });
+ },
+ };
+
+ lifetimes = {
+ created() {
+ // 绑定方法到 this.data(对齐 chat-sender.ts 的写法)
+ this.data.initRecordManager = this.initRecordManager.bind(this);
+ this.data.getVoiceAuthSetting = this.getVoiceAuthSetting.bind(this);
+ this.data.applyAuth = this.applyAuth.bind(this);
+ this.data.openVoiceSetting = this.openVoiceSetting.bind(this);
+ this.data.startRecord = this.startRecord.bind(this);
+ this.data.touchmove = this.touchmove.bind(this);
+ this.data.stopRecord = this.stopRecord.bind(this);
+ this.data.touchcancel = this.touchcancel.bind(this);
+ this.data.cancelRecord = this.cancelRecord.bind(this);
+ this.data.convertToText = this.convertToText.bind(this);
+ this.data.sendVoice = this.sendVoice.bind(this);
+ this.data.handleSendVoiceMsg = this.handleSendVoiceMsg.bind(this);
+ this.data.handleCancelSend = this.handleCancelSend.bind(this);
+ this.data.onTranslateInput = this.onTranslateInput.bind(this);
+ this.data.onOpenSetting = this.onOpenSetting.bind(this);
+ this.data.resetState = this.resetState.bind(this);
+ this.data.requestRecordAuth = this.requestRecordAuth.bind(this);
+
+ this.initRecordManager();
+ },
+
+ attached() {
+ // 进入页面时检查一次权限
+ this.getVoiceAuthSetting().catch(() => null);
+ },
+
+ detached() {
+ if (this.manager) {
+ try {
+ this.manager.stop();
+ } catch (e) {
+ // ignore
+ }
+ }
+ this.resetState();
+ },
+ };
+}
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..abe5e0dc9a
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/chat-record.wxml
@@ -0,0 +1,88 @@
+
+
+
+
+ {{globalConfig.holdToTalk}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{globalConfig.releaseToSend}}
+
+
+ {{globalConfig.releaseToCancel}}
+
+
+
+
+
+
+
+
+
+
+
+
+ ↩
+
+ {{globalConfig.cancelText}}
+
+
+ {{globalConfig.sendText}}
+
+
+
+
+
+
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..58425049a8
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/props.ts
@@ -0,0 +1,31 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdChatRecordProps } from './type';
+const props: TdChatRecordProps = {
+ /** 是否自动发送(预留扩展) */
+ autoSend: {
+ type: Boolean,
+ value: false,
+ },
+ /** 底部高度,用于适配键盘弹出时的布局 */
+ bottomHeight: {
+ type: Number,
+ value: 0,
+ },
+ /** 最大录音时长(ms) */
+ duration: {
+ type: Number,
+ value: 60000,
+ },
+ /** 识别语言(WechatSI 插件参数) */
+ lang: {
+ type: String,
+ value: 'zh_CN',
+ },
+};
+
+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..47b26435c9
--- /dev/null
+++ b/packages/pro-components/chat/chat-record/type.ts
@@ -0,0 +1,40 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+export interface TdChatRecordProps {
+ /**
+ * 是否自动发送(预留扩展)
+ * @default false
+ */
+ autoSend?: {
+ type: BooleanConstructor;
+ value?: boolean;
+ };
+ /**
+ * 底部高度,用于适配键盘弹出时的布局
+ * @default 0
+ */
+ bottomHeight?: {
+ type: NumberConstructor;
+ value?: number;
+ };
+ /**
+ * 最大录音时长(ms)
+ * @default 60000
+ */
+ duration?: {
+ type: NumberConstructor;
+ value?: number;
+ };
+ /**
+ * 识别语言(WechatSI 插件参数)
+ * @default zh_CN
+ */
+ lang?: {
+ type: StringConstructor;
+ value?: string;
+ };
+}
diff --git a/packages/pro-components/chat/chat-sender/README.en-US.md b/packages/pro-components/chat/chat-sender/README.en-US.md
index 51a417f1ca..6ca38718fb 100644
--- a/packages/pro-components/chat/chat-sender/README.en-US.md
+++ b/packages/pro-components/chat/chat-sender/README.en-US.md
@@ -9,6 +9,7 @@ name | type | default | description | required
style | Object | - | CSS(Cascading Style Sheets) | N
custom-style | Object | - | CSS(Cascading Style Sheets),used to set style on virtual component | N
adjust-position | Boolean | false | \- | N
+allow-speech | Boolean | false | `1.16.0` | N
attachments-props | Object | - | Typescript: `AttachmentsProps`,[Attachments API Documents](./attachments?tab=api)。[see more ts definition](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
diff --git a/packages/pro-components/chat/chat-sender/README.md b/packages/pro-components/chat/chat-sender/README.md
index 13077887ab..ccd8f1650f 100644
--- a/packages/pro-components/chat/chat-sender/README.md
+++ b/packages/pro-components/chat/chat-sender/README.md
@@ -56,6 +56,7 @@ isComponent: true
style | Object | - | 样式 | N
custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N
adjust-position | Boolean | false | 默认键盘弹起不会把页面顶起来 | N
+allow-speech | Boolean | false | `1.16.0`。是否允许语音输入。为 true 时显示语音切换按钮,语音输入 UI 由开发者通过 speech 插槽自行提供 | N
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
diff --git a/packages/pro-components/chat/chat-sender/_example/chat-sender.json b/packages/pro-components/chat/chat-sender/_example/chat-sender.json
index 374658b373..62cb83748d 100644
--- a/packages/pro-components/chat/chat-sender/_example/chat-sender.json
+++ b/packages/pro-components/chat/chat-sender/_example/chat-sender.json
@@ -6,6 +6,7 @@
"base": "./base",
"attachments": "./attachments",
"content-citation": "./content-citation",
- "file-citation": "./file-citation"
+ "file-citation": "./file-citation",
+ "record": "./record"
}
}
diff --git a/packages/pro-components/chat/chat-sender/_example/chat-sender.wxml b/packages/pro-components/chat/chat-sender/_example/chat-sender.wxml
index 19738427a5..09eb5fd6c6 100644
--- a/packages/pro-components/chat/chat-sender/_example/chat-sender.wxml
+++ b/packages/pro-components/chat/chat-sender/_example/chat-sender.wxml
@@ -17,4 +17,7 @@
+
+
+
diff --git a/packages/pro-components/chat/chat-sender/_example/record/index.js b/packages/pro-components/chat/chat-sender/_example/record/index.js
new file mode 100644
index 0000000000..2199872862
--- /dev/null
+++ b/packages/pro-components/chat/chat-sender/_example/record/index.js
@@ -0,0 +1,100 @@
+Page({
+ /**
+ * 页面的初始数据
+ */
+ data: {
+ query: '', // 输入框内容
+ placeholder: '请输入内容', // 输入框占位符
+ loading: false, // 发送按钮加载状态
+ allowSpeech: true,
+ keyboardHeight: 0, // 键盘高度(px)
+ },
+
+ /**
+ * 监听键盘高度变化,弹出键盘时将 chat-sender 往上推
+ */
+ onKeyboardHeightChange(e) {
+ const height = (e && e.detail && e.detail.height) || 0;
+ this.setData({
+ keyboardHeight: height,
+ });
+ },
+
+ /**
+ * 返回上一页
+ */
+ navigateBack() {
+ wx.navigateBack({
+ delta: 1,
+ });
+ },
+
+ /**
+ * 处理输入框内容变化
+ */
+ handleInput(e) {
+ this.setData({
+ query: e.detail.value,
+ });
+ },
+
+ /**
+ * 语音识别回调
+ * @param {Object} e - 事件对象
+ */
+ handleRecognize(e) {
+ const voiceMsg = e.detail;
+ console.log('语音识别结果:', voiceMsg);
+
+ // 将语音识别结果设置到输入框中
+ if (voiceMsg.voiceText) {
+ this.setData({
+ query: voiceMsg.voiceText,
+ });
+ }
+
+ // 提示用户
+ // 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-sender/_example/record/index.json b/packages/pro-components/chat/chat-sender/_example/record/index.json
new file mode 100644
index 0000000000..da83d41dbd
--- /dev/null
+++ b/packages/pro-components/chat/chat-sender/_example/record/index.json
@@ -0,0 +1,10 @@
+{
+ "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",
+ "t-icon": "tdesign-miniprogram/icon/icon"
+ }
+}
diff --git a/packages/pro-components/chat/chat-sender/_example/record/index.wxml b/packages/pro-components/chat/chat-sender/_example/record/index.wxml
new file mode 100644
index 0000000000..2bedee7c2c
--- /dev/null
+++ b/packages/pro-components/chat/chat-sender/_example/record/index.wxml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ 按住说话
+ 请授权麦克风权限
+
+
+
+
+
+
diff --git a/packages/pro-components/chat/chat-sender/_example/record/index.wxss b/packages/pro-components/chat/chat-sender/_example/record/index.wxss
new file mode 100644
index 0000000000..b88dea9dca
--- /dev/null
+++ b/packages/pro-components/chat/chat-sender/_example/record/index.wxss
@@ -0,0 +1,80 @@
+.demo-base-container {
+ padding: 56rpx 0 0 0;
+ background-color: var(--td-bg-color-container);
+ height: 57vh;
+ position: relative;
+}
+
+/* 聊天发送器包装器 */
+.chat-sender-demo-wrapper {
+ margin-bottom: 32rpx;
+ border-radius: 8rpx;
+ overflow: hidden;
+}
+
+.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);
+ transition: bottom 0.2s ease-out;
+}
+
+.speech-slot-btn,
+.speech-btn-error {
+ /* 不设置 height,让 chat-sender 父容器的 min-height(134rpx) 自然撑起,
+ 避免与 t-chat-record-hook 的 padding/line-height 叠加导致总高度超过 keyboard 模式 */
+ width: 100%;
+ text-align: center;
+ gap: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ line-height: 48rpx;
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.speech-btn-error {
+ color: red;
+}
+
+.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;
+}
+
+/* 语音输入按钮样式 */
+.icon-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ width: 64rpx;
+ height: 64rpx;
+ background-color: #fff;
+ border: 1px solid #dcdcdc;
+}
+.voice-input-button {
+ font-size: 36rpx;
+ color: #000;
+}
diff --git a/packages/pro-components/chat/chat-sender/chat-sender.less b/packages/pro-components/chat/chat-sender/chat-sender.less
index 445272129c..258508dd3d 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;
// 输入框
@@ -140,6 +142,24 @@
z-index: @chat-sender-footer-z-index;
}
+ // 模式切换区域(底部左侧)
+ &__mode {
+ display: flex;
+ align-items: center;
+ }
+
+ // 语音/键盘切换按钮
+ &__speech-toggle {
+ width: @chat-sender-send-btn-size;
+ height: @chat-sender-send-btn-size;
+ border-radius: 50%;
+ border: @chat-sender-btn-func-border;
+ background-color: @chat-sender-btn-func-bg;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
// 上传区域
&__upload {
display: flex;
diff --git a/packages/pro-components/chat/chat-sender/chat-sender.ts b/packages/pro-components/chat/chat-sender/chat-sender.ts
index bc8f630f89..d4c1aa8ff5 100644
--- a/packages/pro-components/chat/chat-sender/chat-sender.ts
+++ b/packages/pro-components/chat/chat-sender/chat-sender.ts
@@ -46,6 +46,9 @@ export default class ChatSender extends SuperComponent {
},
},
uploadNames: [],
+
+ // 当前输入模式:keyboard-键盘输入,speech-语音输入
+ inputMode: 'keyboard',
};
observers = {
@@ -64,6 +67,18 @@ export default class ChatSender extends SuperComponent {
};
methods = {
+ /**
+ * 切换语音/键盘输入模式
+ * 切换前先收起键盘,避免 textarea 销毁失焦与模式切换叠加导致闪烁
+ */
+ handleSpeechToggle() {
+ // 切换前先收起键盘,避免 textarea 销毁失焦与模式切换叠加导致 chat-sender 闪烁
+ wx.hideKeyboard && wx.hideKeyboard();
+ this.setData({
+ inputMode: this.data.inputMode === 'keyboard' ? 'speech' : 'keyboard',
+ });
+ },
+
onkeyboardheightchange(e) {
// 业务侧控制键盘顶起高度,如果用fix布局,不需要监听键盘高度变化
this.triggerEvent('keyboardheightchange', e.detail);
@@ -275,6 +290,7 @@ export default class ChatSender extends SuperComponent {
this.data.handleImageUpload = this.handleImageUpload.bind(this);
this.data.handleWechatFileUpload = this.handleWechatFileUpload.bind(this);
this.data.handleUploadEntryClick = this.handleUploadEntryClick.bind(this);
+ this.data.handleSpeechToggle = this.handleSpeechToggle.bind(this);
},
attached() {},
detached() {
diff --git a/packages/pro-components/chat/chat-sender/chat-sender.wxml b/packages/pro-components/chat/chat-sender/chat-sender.wxml
index 069d7a5a11..4b62bdfbdc 100644
--- a/packages/pro-components/chat/chat-sender/chat-sender.wxml
+++ b/packages/pro-components/chat/chat-sender/chat-sender.wxml
@@ -29,33 +29,46 @@
-
- {{placeholder || globalConfig.placeholder}}
+
+
+
+ {{placeholder || globalConfig.placeholder}}
+
+
+
+
+