vue_zhongyou 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/275/225/351/237/263/send.svg +1 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/275/225/351/237/263/sendText.svg +1 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/275/225/351/237/263/voice.svg +1 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/275/225/351/237/263/voice.vue +453 -0
- package//346/217/222/344/273/266/mkcert-v1.4.4-linux-amd64 +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766727537085" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1891" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 85.333c234.667 0 426.667 192 426.667 426.667S746.667 938.667 512 938.667 85.333 746.667 85.333 512 277.333 85.333 512 85.333zM505.6 320c-4.267 2.133-6.4 2.133-12.8 8.533l-153.6 153.6c-12.8 12.8-12.8 32 0 44.8 12.8 12.8 32 12.8 44.8 0l96-96v251.734c0 17.066 14.933 32 32 32s32-14.934 32-32V430.933l96 96c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8l-153.6-153.6c-6.4-6.4-8.533-8.533-12.8-8.533s-8.533-2.133-12.8 0z" p-id="1892" fill="#0061b0"></path></svg>
|
package//345/212/237/350/203/275/344/273/243/347/240/201//345/275/225/351/237/263/sendText.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766728091085" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9111" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 1012.8c-68.8 0-132.8-12.8-195.2-40-59.2-25.6-112-62.4-158.4-107.2-46.4-46.4-81.6-100.8-107.2-158.4C25.6 644.8 11.2 580.8 11.2 512s12.8-132.8 40-195.2c25.6-59.2 62.4-112 107.2-158.4 46.4-46.4 100.8-83.2 158.4-107.2C379.2 25.6 443.2 11.2 512 11.2s132.8 12.8 195.2 40c60.8 25.6 112 62.4 158.4 107.2 46.4 46.4 83.2 100.8 107.2 158.4 25.6 62.4 40 126.4 40 195.2s-12.8 132.8-40 195.2c-25.6 60.8-62.4 112-107.2 158.4-46.4 46.4-100.8 83.2-158.4 107.2-62.4 27.2-126.4 40-195.2 40zM512 80C273.6 80 80 273.6 80 512s193.6 432 432 432 432-193.6 432-432S750.4 80 512 80z" fill="#515151" p-id="9112"></path><path d="M284.8 424h-9.6c-20.8 0-36.8-16-36.8-36.8V336c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c0 20.8-17.6 36.8-36.8 36.8zM441.6 424H432c-20.8 0-36.8-16-36.8-36.8V336c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c-1.6 20.8-17.6 36.8-36.8 36.8zM596.8 424h-9.6c-20.8 0-36.8-16-36.8-36.8V336c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c0 20.8-16 36.8-36.8 36.8zM753.6 424h-9.6c-20.8 0-36.8-16-36.8-36.8V336c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c0 20.8-16 36.8-36.8 36.8zM280 598.4h-9.6c-20.8 0-36.8-16-36.8-36.8v-51.2c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c0 20.8-16 36.8-36.8 36.8zM436.8 598.4h-9.6c-20.8 0-36.8-16-36.8-36.8v-51.2c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c0 20.8-16 36.8-36.8 36.8zM593.6 598.4h-9.6c-20.8 0-36.8-16-36.8-36.8v-51.2c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c-1.6 20.8-17.6 36.8-36.8 36.8zM748.8 598.4h-9.6c-20.8 0-36.8-16-36.8-36.8v-51.2c0-20.8 16-36.8 36.8-36.8h9.6c20.8 0 36.8 16 36.8 36.8v51.2c0 20.8-16 36.8-36.8 36.8zM721.6 740.8H308.8c-17.6 0-33.6-14.4-33.6-33.6v-9.6c0-17.6 14.4-33.6 33.6-33.6h412.8c17.6 0 33.6 14.4 33.6 33.6v9.6c0 19.2-16 33.6-33.6 33.6z" fill="#515151" p-id="9113"></path></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766727960981" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6671" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 981.3504A469.3504 469.3504 0 1 0 512 42.6496a469.3504 469.3504 0 0 0 0 938.7008z m0-861.9008a392.5504 392.5504 0 1 1 0 785.1008 392.5504 392.5504 0 0 1 0-785.1008z" fill="#515151" p-id="6672"></path><path d="M552.704 513.9456q0-79.5136-56.2176-135.7824a38.4 38.4 0 1 0-54.272 54.272q33.6896 33.792 33.6896 81.5104t-33.7408 81.408a38.4 38.4 0 1 0 54.3232 54.3744q56.2176-56.2688 56.2176-135.7824zM645.8368 271.5136q101.0176 101.0176 101.0176 243.9168 0 142.848-101.0176 243.8656a38.4 38.4 0 1 1-54.3232-54.272q78.5408-78.592 78.5408-189.5936 0-111.104-78.5408-189.5936a38.4 38.4 0 1 1 54.272-54.3232z" fill="#515151" p-id="6673"></path><path d="M371.2 526.9504q0 2.816-0.256 5.632-0.3072 2.816-0.8704 5.632-0.512 2.7136-1.3312 5.4272t-1.9456 5.3248q-1.024 2.6112-2.4064 5.12-1.3312 2.5088-2.9184 4.864-1.536 2.3552-3.328 4.5056-1.792 2.2016-3.84 4.1984-1.9968 2.048-4.1472 3.7888-2.2016 1.792-4.5568 3.3792-2.3552 1.536-4.864 2.9184-2.4576 1.3312-5.12 2.4064-2.56 1.0752-5.2736 1.8944-2.7136 0.8192-5.5296 1.3824-2.7648 0.512-5.5808 0.8192t-5.632 0.3072q-2.816 0-5.632-0.3072-2.816-0.256-5.632-0.8192-2.7648-0.512-5.4784-1.3824-2.6624-0.8192-5.3248-1.8944-2.56-1.0752-5.12-2.4064-2.4576-1.3312-4.8128-2.9184-2.3552-1.536-4.5568-3.3792-2.1504-1.792-4.1472-3.7888-2.048-1.9968-3.84-4.1984-1.792-2.1504-3.328-4.5056-1.5872-2.3552-2.9184-4.864-1.3312-2.5088-2.4064-5.12-1.1264-2.56-1.9456-5.3248-0.8192-2.7136-1.3312-5.4784-0.5632-2.7648-0.8704-5.632Q256 529.8176 256 526.9504q0-2.816 0.256-5.632 0.3072-2.816 0.8704-5.5808 0.512-2.7648 1.3312-5.4784t1.9456-5.3248q1.024-2.56 2.4064-5.12 1.3312-2.4576 2.9184-4.8128 1.536-2.3552 3.328-4.5568t3.84-4.1984q1.9968-1.9968 4.1472-3.7888 2.2016-1.792 4.5568-3.3792 2.3552-1.536 4.864-2.8672 2.4576-1.3824 5.12-2.4576 2.56-1.024 5.2736-1.8944 2.7136-0.8192 5.5296-1.3824 2.7648-0.512 5.5808-0.8192 2.816-0.256 5.632-0.256t5.632 0.256q2.816 0.3072 5.632 0.8192 2.7648 0.5632 5.4784 1.3824 2.6624 0.8192 5.3248 1.8944 2.56 1.0752 5.12 2.4576 2.4576 1.3312 4.8128 2.8672 2.3552 1.5872 4.5568 3.3792 2.1504 1.792 4.1472 3.7888 2.048 2.048 3.84 4.1984 1.792 2.2016 3.328 4.5568 1.5872 2.3552 2.9184 4.8128 1.3312 2.56 2.4064 5.12 1.1264 2.6112 1.9456 5.3248t1.3312 5.4784q0.5632 2.7648 0.8704 5.632 0.256 2.7648 0.256 5.632z" fill="#515151" p-id="6674"></path></svg>
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="voice-container">
|
|
3
|
+
<div class="btn-tip" v-show="isRecording">{{ isTouchInside ? '松手发送,上移取消' : '松手取消' }}</div>
|
|
4
|
+
<button
|
|
5
|
+
class="voice-btn"
|
|
6
|
+
:class="{
|
|
7
|
+
recording: isRecording
|
|
8
|
+
}"
|
|
9
|
+
@touchstart.prevent="startRecord" @touchend="handleTouchEnd"
|
|
10
|
+
@touchmove="handleTouchMove"
|
|
11
|
+
@touchcancel="cancelRecord"
|
|
12
|
+
@mousedown.prevent="startRecord"
|
|
13
|
+
@mouseup="handleMouseUp"
|
|
14
|
+
@mouseleave="handleMouseLeave"
|
|
15
|
+
ref="voiceButtonRef">
|
|
16
|
+
<div v-if="isRecording" class="waveform-container" ref="waveformRef"></div>
|
|
17
|
+
<span v-else class="btn-text">按住说话</span>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<audio v-if="audioUrl" :src="audioUrl" controls autoplay preload="auto" class="audio-player" />
|
|
22
|
+
<p class="tip">{{ recordTip }}</p>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup>
|
|
26
|
+
import { ref, onUnmounted, nextTick } from 'vue'
|
|
27
|
+
import WaveSurfer from 'wavesurfer.js'
|
|
28
|
+
|
|
29
|
+
const isRecording = ref(false)
|
|
30
|
+
const audioUrl = ref('')
|
|
31
|
+
const recordTip = ref('')
|
|
32
|
+
const isTouchInside = ref(true) // 跟踪触摸是否在按钮内
|
|
33
|
+
const voiceButtonRef = ref(null)
|
|
34
|
+
const waveformRef = ref(null)
|
|
35
|
+
let mediaRecorder = null // 媒体录制器实例
|
|
36
|
+
let stream = null // 音频流
|
|
37
|
+
let recordTimer = null // 录音定时器
|
|
38
|
+
const audioChunks = ref([]) // 存储录音二进制数据
|
|
39
|
+
const waveSurferInstance = ref(null) // 波形图实例
|
|
40
|
+
const audioContext = ref(null) // 音频上下文
|
|
41
|
+
const analyser = ref(null) // 分析器
|
|
42
|
+
const source = ref(null) // 音频源
|
|
43
|
+
const animationFrame = ref(null) // 动画帧
|
|
44
|
+
const dataArray = ref(null) // 音频数据数组
|
|
45
|
+
|
|
46
|
+
onUnmounted(() => {
|
|
47
|
+
cancelRecord()
|
|
48
|
+
// 确保清理所有资源
|
|
49
|
+
if (animationFrame.value) {
|
|
50
|
+
cancelAnimationFrame(animationFrame.value);
|
|
51
|
+
}
|
|
52
|
+
if (waveSurferInstance.value) {
|
|
53
|
+
waveSurferInstance.value.destroy();
|
|
54
|
+
waveSurferInstance.value = null;
|
|
55
|
+
}
|
|
56
|
+
if (audioContext.value) {
|
|
57
|
+
audioContext.value.close();
|
|
58
|
+
audioContext.value = null;
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// 开始录音(使用原生MediaRecorder)
|
|
63
|
+
const startRecord = async () => {
|
|
64
|
+
try {
|
|
65
|
+
// 判断浏览器是否支持录音API
|
|
66
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
67
|
+
recordTip.value = '当前浏览器不支持录音功能'
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 获取麦克风权限,开启音频流
|
|
72
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
|
73
|
+
|
|
74
|
+
// 检测浏览器支持的音频格式
|
|
75
|
+
let options = { mimeType: 'audio/webm' }
|
|
76
|
+
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
|
|
77
|
+
options = { mimeType: 'audio/webm;codecs=opus' }
|
|
78
|
+
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
|
|
79
|
+
options = { mimeType: 'audio/webm' }
|
|
80
|
+
} else if (MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')) {
|
|
81
|
+
options = { mimeType: 'audio/ogg;codecs=opus' }
|
|
82
|
+
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
83
|
+
options = { mimeType: 'audio/mp4' }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
mediaRecorder = new MediaRecorder(stream, options)
|
|
87
|
+
audioChunks.value = []
|
|
88
|
+
recordTip.value = ''
|
|
89
|
+
|
|
90
|
+
// 实时收集录音数据
|
|
91
|
+
mediaRecorder.ondataavailable = (e) => {
|
|
92
|
+
audioChunks.value.push(e.data)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 开始录音
|
|
96
|
+
mediaRecorder.start()
|
|
97
|
+
isRecording.value = true
|
|
98
|
+
recordTip.value = '正在录音...'
|
|
99
|
+
|
|
100
|
+
// 设置最长录音时间为1分钟
|
|
101
|
+
recordTimer = setTimeout(() => {
|
|
102
|
+
stopRecord();
|
|
103
|
+
recordTip.value = '录音已超时(最长1分钟)';
|
|
104
|
+
}, 60 * 1000); // 60秒
|
|
105
|
+
|
|
106
|
+
// 创建实时波形图
|
|
107
|
+
await createRealTimeWaveform();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('录音开启失败:', err)
|
|
110
|
+
const errMap = {
|
|
111
|
+
'NotAllowedError': '麦克风权限被拒绝!请开启',
|
|
112
|
+
'NotFoundError': '未检测到麦克风',
|
|
113
|
+
'SecurityError': '请在HTTPS环境下使用'
|
|
114
|
+
};
|
|
115
|
+
recordTip.value = errMap[err.name] || `录音失败:${err.message}`;
|
|
116
|
+
isRecording.value = false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 停止录音
|
|
121
|
+
const stopRecord = () => {
|
|
122
|
+
if (!isRecording.value || !mediaRecorder) return
|
|
123
|
+
if (mediaRecorder.state !== 'recording') return
|
|
124
|
+
|
|
125
|
+
// 清除录音定时器
|
|
126
|
+
if (recordTimer) {
|
|
127
|
+
clearTimeout(recordTimer);
|
|
128
|
+
recordTimer = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 停止录音并释放麦克风
|
|
132
|
+
mediaRecorder.stop()
|
|
133
|
+
stream.getTracks().forEach(track => track.stop())
|
|
134
|
+
isRecording.value = false
|
|
135
|
+
|
|
136
|
+
// 录音结束的回调 - 生成音频文件
|
|
137
|
+
mediaRecorder.onstop = () => {
|
|
138
|
+
// 使用录制时的实际MIME类型
|
|
139
|
+
const audioBlob = new Blob(audioChunks.value, { type: mediaRecorder.mimeType })
|
|
140
|
+
audioUrl.value = URL.createObjectURL(audioBlob)
|
|
141
|
+
recordTip.value = '录音完成,已自动播放'
|
|
142
|
+
|
|
143
|
+
// ✅ 核心:这里把 audioBlob 传给后端做【语音转文字】即可
|
|
144
|
+
// 传参格式:new FormData().append('file', audioBlob, 'voice.webm') - 根据实际格式调整文件名
|
|
145
|
+
console.log('录音文件Blob对象:', audioBlob)
|
|
146
|
+
console.log('录音文件MIME类型:', mediaRecorder.mimeType)
|
|
147
|
+
console.log('录音文件大小:', audioBlob.size)
|
|
148
|
+
}
|
|
149
|
+
// 停止波形图
|
|
150
|
+
stopRealTimeWaveform();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 检查触摸点是否在元素内
|
|
154
|
+
const isTouchWithinElement = (event, element) => {
|
|
155
|
+
const touch = event.touches[0] || event.changedTouches[0];
|
|
156
|
+
const rect = element.getBoundingClientRect();
|
|
157
|
+
return (
|
|
158
|
+
touch.clientX >= rect.left &&
|
|
159
|
+
touch.clientX <= rect.right &&
|
|
160
|
+
touch.clientY >= rect.top &&
|
|
161
|
+
touch.clientY <= rect.bottom
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 检查鼠标是否在元素内
|
|
166
|
+
const isMouseWithinElement = (event, element) => {
|
|
167
|
+
const rect = element.getBoundingClientRect();
|
|
168
|
+
return (
|
|
169
|
+
event.clientX >= rect.left &&
|
|
170
|
+
event.clientX <= rect.right &&
|
|
171
|
+
event.clientY >= rect.top &&
|
|
172
|
+
event.clientY <= rect.bottom
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// 触摸移动事件处理
|
|
177
|
+
const handleTouchMove = (event) => {
|
|
178
|
+
const button = event.currentTarget;
|
|
179
|
+
isTouchInside.value = isTouchWithinElement(event, button);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// 触摸结束事件处理
|
|
183
|
+
const handleTouchEnd = (event) => {
|
|
184
|
+
if (!isRecording.value) return;
|
|
185
|
+
|
|
186
|
+
const button = event.currentTarget;
|
|
187
|
+
const isWithin = isTouchWithinElement(event, button);
|
|
188
|
+
|
|
189
|
+
if (isWithin) {
|
|
190
|
+
// 手指在按钮内松开,停止录音(发送)
|
|
191
|
+
stopRecord();
|
|
192
|
+
} else {
|
|
193
|
+
// 手指在按钮外松开,取消录音
|
|
194
|
+
cancelRecord();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// 鼠标抬起事件处理
|
|
199
|
+
const handleMouseUp = (event) => {
|
|
200
|
+
if (!isRecording.value) return;
|
|
201
|
+
|
|
202
|
+
const button = event.currentTarget;
|
|
203
|
+
const isWithin = isMouseWithinElement(event, button);
|
|
204
|
+
|
|
205
|
+
if (isWithin) {
|
|
206
|
+
// 鼠标在按钮内抬起,停止录音(发送)
|
|
207
|
+
stopRecord();
|
|
208
|
+
} else {
|
|
209
|
+
// 鼠标在按钮外抬起,取消录音
|
|
210
|
+
cancelRecord();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// 鼠标离开事件处理
|
|
215
|
+
const handleMouseLeave = (event) => {
|
|
216
|
+
if (isRecording.value) {
|
|
217
|
+
// 鼠标离开按钮区域,取消录音
|
|
218
|
+
cancelRecord();
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// 创建实时波形图
|
|
223
|
+
const createRealTimeWaveform = async () => {
|
|
224
|
+
if (!mediaRecorder) return
|
|
225
|
+
|
|
226
|
+
// 确保 DOM 已更新
|
|
227
|
+
await nextTick();
|
|
228
|
+
|
|
229
|
+
// 如果已有波形图实例,先销毁
|
|
230
|
+
if (waveSurferInstance.value) {
|
|
231
|
+
waveSurferInstance.value.destroy();
|
|
232
|
+
waveSurferInstance.value = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 创建音频上下文和分析器
|
|
236
|
+
audioContext.value = new (window.AudioContext || window.webkitAudioContext)();
|
|
237
|
+
analyser.value = audioContext.value.createAnalyser();
|
|
238
|
+
source.value = audioContext.value.createMediaStreamSource(stream);
|
|
239
|
+
source.value.connect(analyser.value);
|
|
240
|
+
|
|
241
|
+
// 配置分析器
|
|
242
|
+
analyser.value.fftSize = 256;
|
|
243
|
+
const bufferLength = analyser.value.frequencyBinCount;
|
|
244
|
+
dataArray.value = new Uint8Array(bufferLength);
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
// 使用 canvas 手动绘制波形
|
|
248
|
+
const container = waveformRef.value;
|
|
249
|
+
if (!container) return;
|
|
250
|
+
|
|
251
|
+
// 确保容器干净
|
|
252
|
+
container.innerHTML = '';
|
|
253
|
+
|
|
254
|
+
// 创建 canvas 元素
|
|
255
|
+
const canvas = document.createElement('canvas');
|
|
256
|
+
canvas.width = container.clientWidth || 160;
|
|
257
|
+
canvas.height = container.clientHeight || 50;
|
|
258
|
+
|
|
259
|
+
canvas.style.width = '100%';
|
|
260
|
+
canvas.style.height = '100%';
|
|
261
|
+
container.appendChild(canvas);
|
|
262
|
+
|
|
263
|
+
const ctx = canvas.getContext('2d');
|
|
264
|
+
|
|
265
|
+
// 实时绘制波形
|
|
266
|
+
const draw = () => {
|
|
267
|
+
if (!isRecording.value || !analyser.value || !ctx) {
|
|
268
|
+
if (animationFrame.value) {
|
|
269
|
+
cancelAnimationFrame(animationFrame.value);
|
|
270
|
+
animationFrame.value = null;
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 获取频率数据
|
|
276
|
+
analyser.value.getByteFrequencyData(dataArray.value);
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
// 清空画布
|
|
280
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
281
|
+
|
|
282
|
+
// 设置波形颜色
|
|
283
|
+
ctx.fillStyle = '#fff';
|
|
284
|
+
|
|
285
|
+
// 计算每个条的宽度和间距
|
|
286
|
+
const barWidth = 2;
|
|
287
|
+
const barSpacing = 2;
|
|
288
|
+
const totalBarWidth = barWidth + barSpacing;
|
|
289
|
+
|
|
290
|
+
// 绘制波形条
|
|
291
|
+
for (let i = 0; i < dataArray.value.length; i++) {
|
|
292
|
+
const value = dataArray.value[i];
|
|
293
|
+
const percent = value / 255; // 转换为0到1的范围
|
|
294
|
+
|
|
295
|
+
// 设置最小高度为canvas高度的5%,最大高度为canvas高度的80%
|
|
296
|
+
const minHeight = canvas.height * 0.1;
|
|
297
|
+
const maxHeight = canvas.height * 0.6;
|
|
298
|
+
const height = Math.max(minHeight, Math.min(canvas.height * percent, maxHeight));
|
|
299
|
+
const offset = (canvas.height - height) / 2;
|
|
300
|
+
const x = i * totalBarWidth;
|
|
301
|
+
|
|
302
|
+
// 绘制条形
|
|
303
|
+
ctx.fillRect(x, offset, barWidth, height);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
animationFrame.value = requestAnimationFrame(draw);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
draw();
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// 停止实时波形图
|
|
313
|
+
const stopRealTimeWaveform = () => {
|
|
314
|
+
if (animationFrame.value) {
|
|
315
|
+
cancelAnimationFrame(animationFrame.value);
|
|
316
|
+
animationFrame.value = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (audioContext.value) {
|
|
320
|
+
audioContext.value.close();
|
|
321
|
+
audioContext.value = null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (analyser.value) {
|
|
325
|
+
analyser.value = null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (source.value) {
|
|
329
|
+
source.value = null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 清空波形图容器
|
|
333
|
+
const container = waveformRef.value;
|
|
334
|
+
if (container) {
|
|
335
|
+
container.innerHTML = '';
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// 取消录音(手指滑出按钮、页面销毁时触发)
|
|
340
|
+
const cancelRecord = () => {
|
|
341
|
+
if (isRecording.value && mediaRecorder) {
|
|
342
|
+
// 清除录音定时器
|
|
343
|
+
if (recordTimer) {
|
|
344
|
+
clearTimeout(recordTimer);
|
|
345
|
+
recordTimer = null;
|
|
346
|
+
}
|
|
347
|
+
mediaRecorder.stop()
|
|
348
|
+
stream?.getTracks().forEach(track => track.stop())
|
|
349
|
+
audioChunks.value = []
|
|
350
|
+
isRecording.value = false
|
|
351
|
+
recordTip.value = '已取消录音'
|
|
352
|
+
}
|
|
353
|
+
// 停止波形图
|
|
354
|
+
stopRealTimeWaveform();
|
|
355
|
+
// 重置触摸状态
|
|
356
|
+
isTouchInside.value = true;
|
|
357
|
+
}
|
|
358
|
+
</script>
|
|
359
|
+
|
|
360
|
+
<style scoped>
|
|
361
|
+
/* 样式和之前一致,无需修改 */
|
|
362
|
+
.voice-container {
|
|
363
|
+
width: 100%;
|
|
364
|
+
display: flex;
|
|
365
|
+
flex-direction: column;
|
|
366
|
+
align-items: center;
|
|
367
|
+
margin-top: 50px;
|
|
368
|
+
position: relative;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.btn-tip{
|
|
372
|
+
position: absolute;
|
|
373
|
+
top: 0px;
|
|
374
|
+
transform: translateY(-100%);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.voice-btn {
|
|
378
|
+
width: 200px;
|
|
379
|
+
height: 40px;
|
|
380
|
+
background: #d2d3de;
|
|
381
|
+
color: #3f3e3e;
|
|
382
|
+
font-size: 16px;
|
|
383
|
+
border: none;
|
|
384
|
+
cursor: pointer;
|
|
385
|
+
transition: all 0.2s;
|
|
386
|
+
position: relative;
|
|
387
|
+
overflow: hidden;
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
justify-content: center;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.voice-btn.recording {
|
|
394
|
+
background: #2217eb;
|
|
395
|
+
/* animation: breath 1.2s infinite ease-in-out; */
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@keyframes breath {
|
|
399
|
+
0% {
|
|
400
|
+
transform: scale(1);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
50% {
|
|
404
|
+
transform: scale(1.05);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
100% {
|
|
408
|
+
transform: scale(1);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.waveform-container {
|
|
413
|
+
width: 160px;
|
|
414
|
+
/* height: 60px; */
|
|
415
|
+
height: 100%;
|
|
416
|
+
display: flex;
|
|
417
|
+
align-items: center;
|
|
418
|
+
justify-content: center;
|
|
419
|
+
position: relative;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.btn-text {
|
|
423
|
+
position: absolute;
|
|
424
|
+
top: 50%;
|
|
425
|
+
left: 50%;
|
|
426
|
+
transform: translate(-50%, -50%);
|
|
427
|
+
z-index: 2;
|
|
428
|
+
pointer-events: none;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.btn-text-overlay {
|
|
432
|
+
position: absolute;
|
|
433
|
+
top: 50%;
|
|
434
|
+
left: 50%;
|
|
435
|
+
transform: translate(-50%, -50%);
|
|
436
|
+
z-index: 2;
|
|
437
|
+
pointer-events: none;
|
|
438
|
+
font-size: 14px;
|
|
439
|
+
text-align: center;
|
|
440
|
+
width: 100%;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.audio-player {
|
|
444
|
+
margin-top: 20px;
|
|
445
|
+
width: 90%;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.tip {
|
|
449
|
+
color: #666;
|
|
450
|
+
margin-top: 15px;
|
|
451
|
+
font-size: 14px;
|
|
452
|
+
}
|
|
453
|
+
</style>
|
|
Binary file
|