feat(practice): 添加长按操作功能
This commit is contained in:
parent
d3453f1a34
commit
3a00bd64db
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onUnmounted } from "vue";
|
import { ref, computed, onUnmounted, reactive } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import {
|
import {
|
||||||
DOUBAO_APP_ID,
|
DOUBAO_APP_ID,
|
||||||
|
|
@ -90,6 +90,87 @@ const completedWords = ref(0);
|
||||||
let audioInstance = null;
|
let audioInstance = null;
|
||||||
let audioCache = new Map(); // 缓存已生成的音频
|
let audioCache = new Map(); // 缓存已生成的音频
|
||||||
|
|
||||||
|
// 长按状态
|
||||||
|
const longPressState = reactive({
|
||||||
|
pause: false, // 暂停按钮是否正在长按
|
||||||
|
replay: false, // 重播按钮是否正在长按
|
||||||
|
});
|
||||||
|
|
||||||
|
// 长按定时器
|
||||||
|
let longPressTimer = null;
|
||||||
|
|
||||||
|
// 长按开始
|
||||||
|
const handleLongPressStart = (buttonType) => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (longPressTimer) {
|
||||||
|
clearTimeout(longPressTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
longPressState[buttonType] = true;
|
||||||
|
|
||||||
|
// 设置长按触发时间(800ms)
|
||||||
|
longPressTimer = setTimeout(() => {
|
||||||
|
if (buttonType === "pause" && dictationState.value === "playing") {
|
||||||
|
// 长按暂停按钮 = 停止听写
|
||||||
|
stopDictation();
|
||||||
|
} else if (buttonType === "replay") {
|
||||||
|
// 长按重播按钮 = 显示当前单词
|
||||||
|
showCurrentWord();
|
||||||
|
}
|
||||||
|
longPressState[buttonType] = false;
|
||||||
|
}, 800);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 长按结束
|
||||||
|
const handleLongPressEnd = (buttonType) => {
|
||||||
|
if (longPressTimer) {
|
||||||
|
clearTimeout(longPressTimer);
|
||||||
|
longPressTimer = null;
|
||||||
|
}
|
||||||
|
longPressState[buttonType] = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示当前单词(用于听写时查看答案)
|
||||||
|
const showCurrentWord = () => {
|
||||||
|
if (!currentWord.value) return;
|
||||||
|
|
||||||
|
// 如果正在播放,先暂停
|
||||||
|
const wasPlaying = dictationState.value === "playing";
|
||||||
|
if (wasPlaying) {
|
||||||
|
pauseDictation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时显示元素
|
||||||
|
const wordDisplay = document.createElement("div");
|
||||||
|
wordDisplay.className = "word-tooltip";
|
||||||
|
wordDisplay.textContent = currentWord.value;
|
||||||
|
wordDisplay.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
background: linear-gradient(135deg, var(--accent-5), #d97706);
|
||||||
|
color: white;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 40px rgba(245, 158, 11, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(wordDisplay);
|
||||||
|
|
||||||
|
// 3秒后自动消失
|
||||||
|
setTimeout(() => {
|
||||||
|
wordDisplay.style.animation = "fadeOut 0.3s ease";
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(wordDisplay);
|
||||||
|
}, 300);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
// 进度计算
|
// 进度计算
|
||||||
const progress = computed(() => {
|
const progress = computed(() => {
|
||||||
if (wordList.value.length === 0) return 0;
|
if (wordList.value.length === 0) return 0;
|
||||||
|
|
@ -366,6 +447,11 @@ onUnmounted(() => {
|
||||||
// 清理缓存的音频 URL
|
// 清理缓存的音频 URL
|
||||||
audioCache.forEach((url) => URL.revokeObjectURL(url));
|
audioCache.forEach((url) => URL.revokeObjectURL(url));
|
||||||
audioCache.clear();
|
audioCache.clear();
|
||||||
|
// 清理长按定时器
|
||||||
|
if (longPressTimer) {
|
||||||
|
clearTimeout(longPressTimer);
|
||||||
|
longPressTimer = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -506,7 +592,16 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<!-- 播放中 -->
|
<!-- 播放中 -->
|
||||||
<template v-else-if="dictationState === 'playing'">
|
<template v-else-if="dictationState === 'playing'">
|
||||||
<button class="control-btn pause-btn" @click="pauseDictation">
|
<button
|
||||||
|
class="control-btn pause-btn"
|
||||||
|
:class="{ pressing: longPressState.pause }"
|
||||||
|
@click="pauseDictation"
|
||||||
|
@mousedown="handleLongPressStart('pause')"
|
||||||
|
@mouseup="handleLongPressEnd('pause')"
|
||||||
|
@mouseleave="handleLongPressEnd('pause')"
|
||||||
|
@touchstart.prevent="handleLongPressStart('pause')"
|
||||||
|
@touchend="handleLongPressEnd('pause')"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|
@ -521,7 +616,16 @@ onUnmounted(() => {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="control-btn replay-btn" @click="replayCurrentWord">
|
<button
|
||||||
|
class="control-btn replay-btn"
|
||||||
|
:class="{ pressing: longPressState.replay }"
|
||||||
|
@click="replayCurrentWord"
|
||||||
|
@mousedown="handleLongPressStart('replay')"
|
||||||
|
@mouseup="handleLongPressEnd('replay')"
|
||||||
|
@mouseleave="handleLongPressEnd('replay')"
|
||||||
|
@touchstart.prevent="handleLongPressStart('replay')"
|
||||||
|
@touchend="handleLongPressEnd('replay')"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|
@ -570,7 +674,16 @@ onUnmounted(() => {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="control-btn replay-btn" @click="replayCurrentWord">
|
<button
|
||||||
|
class="control-btn replay-btn"
|
||||||
|
:class="{ pressing: longPressState.replay }"
|
||||||
|
@click="replayCurrentWord"
|
||||||
|
@mousedown="handleLongPressStart('replay')"
|
||||||
|
@mouseup="handleLongPressEnd('replay')"
|
||||||
|
@mouseleave="handleLongPressEnd('replay')"
|
||||||
|
@touchstart.prevent="handleLongPressStart('replay')"
|
||||||
|
@touchend="handleLongPressEnd('replay')"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|
@ -1319,6 +1432,46 @@ onUnmounted(() => {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 长按状态 */
|
||||||
|
.control-btn.pressing {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-btn.pressing {
|
||||||
|
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
||||||
|
box-shadow: 0 2px 10px rgba(239, 68, 68, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay-btn.pressing {
|
||||||
|
background: linear-gradient(135deg, var(--accent-5), #d97706);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%) scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.content-grid {
|
.content-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue