init
This commit is contained in:
144
src/components/chat/VoiceRecorder.vue
Normal file
144
src/components/chat/VoiceRecorder.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits<{ recorded: [blob: Blob] }>();
|
||||
|
||||
const recording = ref(false);
|
||||
const mediaRecorder = ref<MediaRecorder | null>(null);
|
||||
const chunks = ref<Blob[]>([]);
|
||||
const duration = ref(0);
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder.value = new MediaRecorder(stream);
|
||||
chunks.value = [];
|
||||
|
||||
mediaRecorder.value.ondataavailable = (e) => chunks.value.push(e.data);
|
||||
mediaRecorder.value.onstop = () => {
|
||||
const blob = new Blob(chunks.value, { type: 'audio/webm' });
|
||||
emit('recorded', blob);
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.value.start();
|
||||
recording.value = true;
|
||||
duration.value = 0;
|
||||
timer = setInterval(() => duration.value++, 1000);
|
||||
} catch {
|
||||
// Microphone access denied
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
mediaRecorder.value?.stop();
|
||||
recording.value = false;
|
||||
if (timer) { clearInterval(timer); timer = null; }
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (mediaRecorder.value?.state === 'recording') {
|
||||
mediaRecorder.value.ondataavailable = null;
|
||||
mediaRecorder.value.onstop = null;
|
||||
mediaRecorder.value.stop();
|
||||
}
|
||||
recording.value = false;
|
||||
duration.value = 0;
|
||||
if (timer) { clearInterval(timer); timer = null; }
|
||||
}
|
||||
|
||||
function formatTime(s: number) {
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = s % 60;
|
||||
return `${m}:${sec.toString().padStart(2, '0')}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="voice-recorder">
|
||||
<div v-if="recording" class="voice-recorder__active">
|
||||
<span class="voice-recorder__dot" aria-hidden="true" />
|
||||
<span class="voice-recorder__time meta">{{ formatTime(duration) }}</span>
|
||||
<button class="voice-recorder__cancel" @click="cancel" aria-label="Отменить запись">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="voice-recorder__stop" @click="stop" aria-label="Остановить запись">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button v-else class="voice-recorder__btn" @click="start" aria-label="Записать голосовое сообщение">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="20" height="20">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voice-recorder {
|
||||
&__btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover { color: var(--color-cream); }
|
||||
}
|
||||
|
||||
&__active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-signal);
|
||||
animation: pulse-signal 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__cancel,
|
||||
&__stop {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-muted);
|
||||
&:hover { color: var(--color-cream); }
|
||||
}
|
||||
|
||||
&__stop {
|
||||
background: var(--color-signal);
|
||||
color: white;
|
||||
&:hover { background: #a84e30; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user