|
@@ -11,6 +11,16 @@
|
|
|
@keyup.enter.exact="handleSubmit"
|
|
@keyup.enter.exact="handleSubmit"
|
|
|
></textarea>
|
|
></textarea>
|
|
|
<div class="input-actions">
|
|
<div class="input-actions">
|
|
|
|
|
+ <!-- 语音输入按钮 -->
|
|
|
|
|
+ <div class="voice-input-outer" :class="{ 'recording': isRecording }">
|
|
|
|
|
+ <VoiceInput
|
|
|
|
|
+ inputSelector=".user-input-textarea"
|
|
|
|
|
+ lang="zh-CN"
|
|
|
|
|
+ maxDuration="10"
|
|
|
|
|
+ @voiceRecognized="handleVoiceRecognized"
|
|
|
|
|
+ @recordingStatusChanged="handleRecordingStatusChanged"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
<button class="cancel-btn" @click="handleCancel">清空</button>
|
|
<button class="cancel-btn" @click="handleCancel">清空</button>
|
|
|
<button class="submit-btn" @click="handleSubmit">发送</button>
|
|
<button class="submit-btn" @click="handleSubmit">发送</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -20,6 +30,7 @@
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref } from 'vue';
|
|
import { ref } from 'vue';
|
|
|
|
|
+import VoiceInput from '@/components/ai/voice/VoiceInput_Api.vue';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* UserInputCard - 用户文本输入卡片组件
|
|
* UserInputCard - 用户文本输入卡片组件
|
|
@@ -29,10 +40,13 @@ import { ref } from 'vue';
|
|
|
/**
|
|
/**
|
|
|
* Emits 定义
|
|
* Emits 定义
|
|
|
* @event submit - 用户提交输入内容
|
|
* @event submit - 用户提交输入内容
|
|
|
|
|
+ * @event voice-recognized - 语音识别完成
|
|
|
|
|
+ * @event recording-status-changed - 录音状态变化
|
|
|
*/
|
|
*/
|
|
|
-const emit = defineEmits(['submit']);
|
|
|
|
|
|
|
+const emit = defineEmits(['submit', 'voice-recognized', 'recording-status-changed']);
|
|
|
|
|
|
|
|
const userInput = ref('');
|
|
const userInput = ref('');
|
|
|
|
|
+const isRecording = ref(false);
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 处理提交
|
|
* 处理提交
|
|
@@ -53,6 +67,30 @@ const handleSubmit = () => {
|
|
|
const handleCancel = () => {
|
|
const handleCancel = () => {
|
|
|
userInput.value = '';
|
|
userInput.value = '';
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理语音识别完成
|
|
|
|
|
+ * @param {Object} result - 语音识别结果对象
|
|
|
|
|
+ * @param {string} result.originalText - 原始识别文本
|
|
|
|
|
+ * @param {string} result.processedText - 处理后的文本(包含原有内容)
|
|
|
|
|
+ * @param {number} result.cursorPos - 光标位置
|
|
|
|
|
+ */
|
|
|
|
|
+const handleVoiceRecognized = (result) => {
|
|
|
|
|
+ if (result && typeof result === 'object') {
|
|
|
|
|
+ userInput.value = result.processedText || result.originalText || '';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ userInput.value = String(result || '');
|
|
|
|
|
+ }
|
|
|
|
|
+ emit('voice-recognized', result);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 处理录音状态变化
|
|
|
|
|
+ */
|
|
|
|
|
+const handleRecordingStatusChanged = (status) => {
|
|
|
|
|
+ isRecording.value = status === 'recording';
|
|
|
|
|
+ emit('recording-status-changed', status);
|
|
|
|
|
+};
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
@@ -161,12 +199,145 @@ const handleCancel = () => {
|
|
|
|
|
|
|
|
.input-actions {
|
|
.input-actions {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
justify-content: flex-end;
|
|
justify-content: flex-end;
|
|
|
- gap: rpx(6);
|
|
|
|
|
|
|
+ gap: rpx(8);
|
|
|
margin-top: rpx(6);
|
|
margin-top: rpx(6);
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.voice-input-outer {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: rpx(28);
|
|
|
|
|
+ height: rpx(28);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 85%;
|
|
|
|
|
+ height: 85%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: rpx(1.5) solid rgba(80, 190, 240, 0.6);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.recording {
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ animation: pulse 2s infinite;
|
|
|
|
|
+ border-color: rgba(0, 100, 192, 0.6);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::after {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 85%;
|
|
|
|
|
+ height: 85%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: rpx(1.5) solid rgba(0, 100, 192, 0.4);
|
|
|
|
|
+ animation: pulse 2s infinite 0.5s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.voice-input-container) {
|
|
|
|
|
+ .speech-btn {
|
|
|
|
|
+ background: linear-gradient(135deg, #A0DCF0, #50BEF0);
|
|
|
|
|
+ border-color: rgba(0, 100, 192, 1);
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ color: #0064BE;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.voice-input-container) {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+
|
|
|
|
|
+ .speech-btn {
|
|
|
|
|
+ width: rpx(22);
|
|
|
|
|
+ height: rpx(22);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ border: rpx(1.5) solid rgba(0, 100, 192);
|
|
|
|
|
+ background: linear-gradient(135deg, #A0DCF0, #50BEF0);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ gap: 0;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ box-shadow: 0 rpx(3) rpx(8) rgba(0, 0, 0, 0.3);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:active {
|
|
|
|
|
+ transform: scale(0.95);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.5);
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ transition: width 0.6s, height 0.6s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:active::before {
|
|
|
|
|
+ width: rpx(50);
|
|
|
|
|
+ height: rpx(50);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ font-size: rpx(12);
|
|
|
|
|
+ color: #0064BE;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .countdown-text {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ font-size: rpx(6);
|
|
|
|
|
+ color: #0064BE;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: rpx(3);
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .waveform-container {
|
|
|
|
|
+ .live-waveform {
|
|
|
|
|
+ .bar {
|
|
|
|
|
+ background: rgba(0, 100, 190, 0.5) !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes pulse {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: scale(1.15);
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.cancel-btn, .submit-btn {
|
|
.cancel-btn, .submit-btn {
|
|
|
padding: rpx(2.5) rpx(10);
|
|
padding: rpx(2.5) rpx(10);
|
|
|
border: none;
|
|
border: none;
|