实现音频组件的音频播放
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
</view>
|
||||
|
||||
<view class="discovery-panel" :class="{ 'is-collapsed': isDiscoveryPanelCollapsed }">
|
||||
<Discovery @content-scroll="handleDiscoveryContentScroll" />
|
||||
<Discovery :active="tabIndex === 0" @content-scroll="handleDiscoveryContentScroll" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
<view class="audio-visual">
|
||||
<image v-if="normalizedItem.coverUrl" class="audio-cover" :src="normalizedItem.coverUrl" mode="aspectFill" />
|
||||
<view v-else class="audio-cover audio-cover-fallback" />
|
||||
<view class="audio-play">
|
||||
<view class="audio-play-triangle" />
|
||||
<view class="audio-play" @tap.stop="handlePlaybackToggle">
|
||||
<view v-if="isPlaying" class="flex items-center gap-[6px]">
|
||||
<view class="h-[20px] w-[5px] rounded-[2px] bg-white" />
|
||||
<view class="h-[20px] w-[5px] rounded-[2px] bg-white" />
|
||||
</view>
|
||||
<view v-else class="audio-play-triangle" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -13,39 +17,32 @@
|
||||
<text class="audio-subtitle block w-full truncate">{{ normalizedItem.subTitle }}</text>
|
||||
|
||||
<view class="audio-progress-row w-full min-w-0">
|
||||
<view class="audio-progress-track" />
|
||||
<text class="audio-time block w-[90px] truncate">{{ normalizedItem.duration }}</text>
|
||||
<view class="audio-progress-track">
|
||||
<view class="audio-progress-fill" :style="{ width: `${progressPercent}%` }" />
|
||||
</view>
|
||||
<text class="audio-time block w-[90px] truncate">{{ playbackTimeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { computed, onUnmounted, ref, watch } from "vue";
|
||||
import { onPageHide } from "@dcloudio/uni-app";
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["didSelectItem"]);
|
||||
|
||||
const formatDuration = (durationMs) => {
|
||||
if (durationMs === null || durationMs === undefined || durationMs === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const value = Number(durationMs);
|
||||
if (!Number.isFinite(value) || value < 0) return "";
|
||||
|
||||
const totalSeconds = Math.floor(value / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const normalizedItem = computed(() => {
|
||||
const source = props.item && typeof props.item === "object" ? props.item : {};
|
||||
const resource = source.resource || {};
|
||||
@@ -61,10 +58,169 @@ const normalizedItem = computed(() => {
|
||||
url: payload.url || "",
|
||||
mimeType: payload.mimeType || "",
|
||||
durationMs: payload.durationMs,
|
||||
duration: formatDuration(payload.durationMs),
|
||||
};
|
||||
});
|
||||
|
||||
const isPlaying = ref(false);
|
||||
const currentTimeSeconds = ref(0);
|
||||
const audioDurationSeconds = ref(0);
|
||||
let audioContext = null;
|
||||
|
||||
const formatSeconds = (secondsValue) => {
|
||||
const value = Number(secondsValue);
|
||||
if (!Number.isFinite(value) || value < 0) return "";
|
||||
|
||||
const totalSeconds = Math.floor(value);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const metadataDurationSeconds = computed(() => {
|
||||
const durationMs = Number(normalizedItem.value.durationMs);
|
||||
return Number.isFinite(durationMs) && durationMs > 0 ? durationMs / 1000 : 0;
|
||||
});
|
||||
|
||||
const effectiveDurationSeconds = computed(
|
||||
() => audioDurationSeconds.value || metadataDurationSeconds.value,
|
||||
);
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (!effectiveDurationSeconds.value) return 0;
|
||||
|
||||
const progress = (currentTimeSeconds.value / effectiveDurationSeconds.value) * 100;
|
||||
return Math.min(Math.max(progress, 0), 100);
|
||||
});
|
||||
|
||||
const playbackTimeText = computed(() => {
|
||||
if (!effectiveDurationSeconds.value && !currentTimeSeconds.value) return "";
|
||||
|
||||
const currentTimeText = formatSeconds(currentTimeSeconds.value);
|
||||
const durationText = formatSeconds(effectiveDurationSeconds.value);
|
||||
return durationText ? `${currentTimeText} / ${durationText}` : currentTimeText;
|
||||
});
|
||||
|
||||
const resetPlaybackState = () => {
|
||||
isPlaying.value = false;
|
||||
currentTimeSeconds.value = 0;
|
||||
audioDurationSeconds.value = 0;
|
||||
};
|
||||
|
||||
const destroyAudioContext = () => {
|
||||
if (!audioContext) return;
|
||||
|
||||
const context = audioContext;
|
||||
audioContext = null;
|
||||
context.pause();
|
||||
context.destroy();
|
||||
};
|
||||
|
||||
const stopAndResetPlayback = () => {
|
||||
destroyAudioContext();
|
||||
resetPlaybackState();
|
||||
};
|
||||
|
||||
const createAudioContext = () => {
|
||||
const url = normalizedItem.value.url;
|
||||
console.log("Creating audio context for URL:", url);
|
||||
if (!url) return null;
|
||||
|
||||
const context = uni.createInnerAudioContext();
|
||||
audioContext = context;
|
||||
context.autoplay = false;
|
||||
context.src = url;
|
||||
|
||||
context.onPlay(() => {
|
||||
if (audioContext === context) isPlaying.value = true;
|
||||
});
|
||||
|
||||
context.onPause(() => {
|
||||
if (audioContext === context) isPlaying.value = false;
|
||||
});
|
||||
|
||||
context.onCanplay(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
const duration = Number(context.duration);
|
||||
if (Number.isFinite(duration) && duration > 0) {
|
||||
audioDurationSeconds.value = duration;
|
||||
}
|
||||
});
|
||||
|
||||
context.onTimeUpdate(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
currentTimeSeconds.value = Number(context.currentTime) || 0;
|
||||
const duration = Number(context.duration);
|
||||
if (Number.isFinite(duration) && duration > 0) {
|
||||
audioDurationSeconds.value = duration;
|
||||
}
|
||||
});
|
||||
|
||||
context.onEnded(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
isPlaying.value = false;
|
||||
currentTimeSeconds.value = 0;
|
||||
});
|
||||
|
||||
context.onStop(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
isPlaying.value = false;
|
||||
currentTimeSeconds.value = 0;
|
||||
});
|
||||
|
||||
context.onError(() => {
|
||||
if (audioContext !== context) return;
|
||||
|
||||
stopAndResetPlayback();
|
||||
uni.showToast({
|
||||
title: "音频播放失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
const handlePlaybackToggle = () => {
|
||||
if (!normalizedItem.value.url) return;
|
||||
|
||||
const context = audioContext || createAudioContext();
|
||||
if (!context) return;
|
||||
|
||||
if (isPlaying.value) {
|
||||
isPlaying.value = false;
|
||||
context.pause();
|
||||
} else {
|
||||
isPlaying.value = true;
|
||||
context.play();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
(active) => {
|
||||
if (!active) stopAndResetPlayback();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => normalizedItem.value.url,
|
||||
() => {
|
||||
stopAndResetPlayback();
|
||||
},
|
||||
);
|
||||
|
||||
onPageHide(() => {
|
||||
stopAndResetPlayback();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopAndResetPlayback();
|
||||
});
|
||||
|
||||
const handleSelect = () => {
|
||||
emit("didSelectItem", normalizedItem.value.raw);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<DiscoveryPhotoToolCard v-if="discoveryPhotoToolData" :item="discoveryPhotoToolData"
|
||||
@didSelectItem="handleToolCardClick" @didSelectOption="handleToolOptionClick" />
|
||||
<DiscoveryAudioToolCard v-if="discoveryAudioToolData" :item="discoveryAudioToolData"
|
||||
@didSelectItem="handleToolCardClick" />
|
||||
:active="props.active" @didSelectItem="handleToolCardClick" />
|
||||
<DiscoveryGuideMapToolCard v-if="discoveryGuideMapToolData" :item="discoveryGuideMapToolData" />
|
||||
<DiscoveryGoodsCard v-if="discoveryGoodsData.length > 0" :list="discoveryGoodsData" />
|
||||
|
||||
@@ -61,6 +61,13 @@ import { homeTabsData, getNearbyTags, homeTabContentData, homeQuickQuestionData,
|
||||
import { useAppStore, useLocationStore } from "@/store";
|
||||
import { JumpType } from "../../model/ChatModel";
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const appStore = useAppStore();
|
||||
const locationStore = useLocationStore();
|
||||
/// 从个渠道获取如二维码
|
||||
|
||||
Reference in New Issue
Block a user