|
|
@@ -0,0 +1,110 @@
|
|
|
+<template>
|
|
|
+ <div class="doubao-voice-input" :class="{ 'is-recording': isRecording }">
|
|
|
+ <VoiceInputApi
|
|
|
+ ref="voiceInputRef"
|
|
|
+ :input-selector="inputSelector"
|
|
|
+ :lang="lang"
|
|
|
+ :max-duration="maxDuration"
|
|
|
+ @voice-recognized="handleVoiceRecognized"
|
|
|
+ @recording-status-changed="handleRecordingStatusChanged"
|
|
|
+ />
|
|
|
+ <span class="voice-status" aria-live="polite">{{ isRecording ? '正在聆听,点击结束' : '语音输入' }}</span>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref } from 'vue'
|
|
|
+import VoiceInputApi from './VoiceInput_Api.vue'
|
|
|
+
|
|
|
+defineProps({
|
|
|
+ lang: { type: String, default: 'zh-CN' },
|
|
|
+ maxDuration: { type: Number, default: 30 },
|
|
|
+ inputSelector: { type: String, default: 'textarea' }
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['voiceRecognized', 'recordingStatusChanged'])
|
|
|
+const voiceInputRef = ref(null)
|
|
|
+const isRecording = ref(false)
|
|
|
+
|
|
|
+const handleVoiceRecognized = (result) => emit('voiceRecognized', result)
|
|
|
+const handleRecordingStatusChanged = (recording) => {
|
|
|
+ isRecording.value = recording
|
|
|
+ emit('recordingStatusChanged', recording)
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({
|
|
|
+ toggleSpeechInput: () => voiceInputRef.value?.toggleSpeechInput(),
|
|
|
+ isRecording
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.doubao-voice-input {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.voice-input-container) { display: flex; }
|
|
|
+
|
|
|
+:deep(.speech-btn) {
|
|
|
+ display: grid !important;
|
|
|
+ width: 38px;
|
|
|
+ height: 38px;
|
|
|
+ padding: 0 !important;
|
|
|
+ place-items: center;
|
|
|
+ border: 0 !important;
|
|
|
+ border-radius: 11px !important;
|
|
|
+ color: #a9c6ff;
|
|
|
+ background: transparent !important;
|
|
|
+ box-shadow: none !important;
|
|
|
+ transition: color .2s ease, background .2s ease, transform .2s ease !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.speech-btn:hover:not(:disabled)) {
|
|
|
+ color: #edf4ff;
|
|
|
+ background: rgba(137, 180, 255, .16) !important;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.speech-btn .el-icon) { color: currentColor !important; font-size: 18px !important; }
|
|
|
+
|
|
|
+:deep(.speech-btn.recording) {
|
|
|
+ display: flex !important;
|
|
|
+ width: 86px;
|
|
|
+ padding: 0 9px !important;
|
|
|
+ gap: 5px !important;
|
|
|
+ color: #fff;
|
|
|
+ background: linear-gradient(135deg, #e44250, #b91f47) !important;
|
|
|
+ box-shadow: 0 7px 20px rgba(209, 45, 70, .3) !important;
|
|
|
+ animation: voice-breath 1.35s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.speech-btn.recording .waveform-container) { max-width: 43px !important; min-width: 43px; }
|
|
|
+:deep(.speech-btn.recording .countdown-text) { color: #fff !important; font-size: 12px !important; }
|
|
|
+:deep(.speech-btn.loading) { color: #c7dbff; background: rgba(137, 180, 255, .12) !important; }
|
|
|
+:deep(.loading-spinner) { border-color: rgba(194, 218, 255, .28) !important; border-top-color: #c6dcff !important; }
|
|
|
+
|
|
|
+.voice-status {
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ bottom: calc(100% + 9px);
|
|
|
+ padding: 5px 8px;
|
|
|
+ border: 1px solid rgba(167, 198, 250, .24);
|
|
|
+ border-radius: 7px;
|
|
|
+ color: #dce9ff;
|
|
|
+ background: rgba(7, 25, 59, .92);
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1;
|
|
|
+ white-space: nowrap;
|
|
|
+ pointer-events: none;
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(3px);
|
|
|
+ transition: .2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.doubao-voice-input:hover .voice-status,
|
|
|
+.doubao-voice-input.is-recording .voice-status { opacity: 1; transform: translateY(0); }
|
|
|
+.doubao-voice-input.is-recording .voice-status { border-color: rgba(255, 160, 172, .36); }
|
|
|
+
|
|
|
+@keyframes voice-breath { 50% { transform: translateY(-1px); box-shadow: 0 9px 26px rgba(222, 58, 83, .5) !important; } }
|
|
|
+</style>
|