diff --git a/api/make.js b/api/make.js index aca8d9b..3b5f335 100644 --- a/api/make.js +++ b/api/make.js @@ -30,6 +30,13 @@ export const getCardTemplateContentList = async (page = 1) => { }); }; +export const getCardMusicList = async () => { + return request({ + url: "/api/blessing/card/music/list", + method: "GET", + }); +}; + export const getCardTemplateTitleList = async (page = 1) => { return request({ url: "/api/blessing/card/template-title/list?page=" + page, diff --git a/pages/make/index.vue b/pages/make/index.vue index 29fb05d..19e6832 100644 --- a/pages/make/index.vue +++ b/pages/make/index.vue @@ -26,6 +26,13 @@ + + + 分享或保存即可去除水印 @@ -432,6 +439,51 @@ @logind="handleLogind" :share-token="shareToken" /> + + + + + + 选择背景音乐 + + + + + + + {{ item.name }} + + + + + + + + + + 关闭音乐 + + + @@ -446,6 +498,7 @@ import { getCardTemplateList, getCardTemplateContentList, getCardTemplateTitleList, + getCardMusicList, } from "@/api/make"; import { abilityCheck, getShareReward, msgCheckApi } from "@/api/system"; import { @@ -455,6 +508,8 @@ import { onReachBottom, onShow, onPullDownRefresh, + onUnload, + onHide, } from "@dcloudio/uni-app"; import { useUserStore } from "@/stores/user"; import LoginPopup from "@/components/LoginPopup/LoginPopup.vue"; @@ -484,6 +539,98 @@ const titleState = ref({ scale: 1, }); +const bgms = ref([]); +const currentBgmIndex = ref(0); +const isBgmPlaying = ref(false); +const innerAudioContext = uni.createInnerAudioContext(); +const bgmPopup = ref(null); + +const initBgm = () => { + if (bgms.value.length > 0) { + playBgm(0); + } +}; + +const openBgmList = () => { + bgmPopup.value.open(); +}; + +const closeBgmList = () => { + bgmPopup.value.close(); +}; + +const selectBgm = (index) => { + if (index === currentBgmIndex.value && isBgmPlaying.value) { + // 如果点击当前正在播放的,暂停 + // pauseBgm(); + // 这里用户可能想重播,或者什么都不做。暂时什么都不做 + return; + } + playBgm(index); + closeBgmList(); +}; + +const turnOffBgm = () => { + stopBgm(); + currentBgmIndex.value = -1; // -1 表示关闭 + closeBgmList(); + uni.showToast({ title: "已关闭音乐", icon: "none" }); +}; + +const playBgm = (index) => { + if (index < 0 || index >= bgms.value.length) return; + + // 更新 index + currentBgmIndex.value = index; + + innerAudioContext.stop(); + innerAudioContext.src = bgms.value[index].musicUrl; + innerAudioContext.loop = true; + innerAudioContext.autoplay = true; + innerAudioContext.play(); + + // 重新绑定事件(防止丢失) + innerAudioContext.onPlay(() => { + isBgmPlaying.value = true; + }); + innerAudioContext.onPause(() => { + isBgmPlaying.value = false; + }); + innerAudioContext.onStop(() => { + isBgmPlaying.value = false; + }); + innerAudioContext.onError((res) => { + console.error("BGM Error:", res.errMsg); + isBgmPlaying.value = false; + }); +}; + +const stopBgm = () => { + innerAudioContext.stop(); + isBgmPlaying.value = false; +}; + +onUnload(() => { + innerAudioContext.destroy(); +}); + +onHide(() => { + innerAudioContext.stop(); +}); + +onShow(() => { + // 页面回到前台时,如果之前是播放状态(且不是 Off),尝试恢复播放 + // 但由于 onHide 停止了,isBgmPlaying 变成了 false + // 这里可以根据需求决定是否恢复。 + // 为了简单,我们只在 onLoad 初始化。如果用户想听,需要手动点。 + // 或者我们可以记录一个 shouldPlay 状态。 + // 鉴于用户需求是“点击切换”,我们保持简单。 + // 但要注意:initBgm 在 onLoad 调用,onShow 也会调用 syncUserInfo 等。 + // 我们可以把 initBgm 放在 onLoad。 + syncUserInfo(); + // ... existing onShow logic ... +}); + const titleStyle = computed(() => { return { transform: `translate(${titleState.value.offsetX}rpx, ${titleState.value.offsetY}rpx) scale(${titleState.value.scale})`, @@ -744,6 +891,7 @@ onLoad((options) => { getTemplateList(); getTemplateContentList(); getTemplateTitleList(); + getMusicList(); if (options.shareToken) { shareToken.value = options.shareToken; } @@ -884,6 +1032,12 @@ const getTemplateList = async (isLoadMore = false) => { } }; +const getMusicList = async () => { + const res = await getCardMusicList(); + bgms.value = res || []; + initBgm(); +}; + const getTemplateTitleList = async (isLoadMore = false) => { if (loadingTitles.value || (!hasMoreTitles.value && isLoadMore)) return; @@ -2083,4 +2237,152 @@ function drawRoundRect(ctx, x, y, w, h, r, color) { left: -9999px; top: -9999px; } + +.music-control { + position: absolute; + top: 24rpx; + left: 24rpx; + width: 64rpx; + height: 64rpx; + background: rgba(0, 0, 0, 0.4); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 20; + backdrop-filter: blur(4px); + border: 1px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s; +} + +.music-control.playing { + background: rgba(255, 59, 48, 0.8); + border-color: rgba(255, 59, 48, 0.5); + animation: music-rotate 4s linear infinite; +} + +@keyframes music-rotate { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.bgm-popup { + background: #fff; + border-radius: 24rpx 24rpx 0 0; + padding-bottom: env(safe-area-inset-bottom); + max-height: 60vh; + display: flex; + flex-direction: column; +} + +.bgm-header { + padding: 30rpx; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1rpx solid #eee; +} + +.bgm-title { + font-size: 32rpx; + font-weight: bold; +} + +.bgm-close { + font-size: 32rpx; + color: #999; + padding: 10rpx; +} + +.bgm-scroll { + max-height: 500rpx; +} + +.bgm-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30rpx; + border-bottom: 1rpx solid #f5f5f5; + transition: all 0.2s; +} + +.bgm-item:active { + background: #f9f9f9; +} + +.bgm-item.active { + background: #fff5f5; +} + +.bgm-info { + display: flex; + align-items: center; + gap: 20rpx; +} + +.bgm-name { + font-size: 28rpx; + color: #333; +} + +.bgm-item.active .bgm-name { + color: #ff3b30; + font-weight: 500; +} + +.bgm-playing-icon { + display: flex; + align-items: flex-end; + gap: 4rpx; + height: 24rpx; +} + +.bar { + width: 4rpx; + background: #ff3b30; + animation: equalize 1s infinite; +} + +.bar1 { + animation-delay: 0s; + height: 60%; +} +.bar2 { + animation-delay: 0.2s; + height: 100%; +} +.bar3 { + animation-delay: 0.4s; + height: 80%; +} + +@keyframes equalize { + 0% { + height: 40%; + } + 50% { + height: 100%; + } + 100% { + height: 40%; + } +} + +.bgm-footer { + padding: 30rpx; + display: flex; + align-items: center; + justify-content: center; + border-top: 1rpx solid #eee; +} + +.turn-off-text { + color: #666; + font-size: 28rpx; +}