feat: card music

This commit is contained in:
zzc
2026-02-12 02:50:03 +08:00
parent bf9930d4e4
commit a787280e6f
2 changed files with 309 additions and 0 deletions

View File

@@ -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) => { export const getCardTemplateTitleList = async (page = 1) => {
return request({ return request({
url: "/api/blessing/card/template-title/list?page=" + page, url: "/api/blessing/card/template-title/list?page=" + page,

View File

@@ -26,6 +26,13 @@
<!-- 预览卡片 --> <!-- 预览卡片 -->
<view class="card-preview"> <view class="card-preview">
<view
class="music-control"
@tap.stop="openBgmList"
:class="{ playing: isBgmPlaying }"
>
<uni-icons type="headphones" size="20" color="#fff"></uni-icons>
</view>
<view class="premium-tag"> <view class="premium-tag">
<uni-icons type="info" size="12" color="#fff"></uni-icons> <uni-icons type="info" size="12" color="#fff"></uni-icons>
<text>分享或保存即可去除水印</text> <text>分享或保存即可去除水印</text>
@@ -432,6 +439,51 @@
@logind="handleLogind" @logind="handleLogind"
:share-token="shareToken" :share-token="shareToken"
/> />
<!-- Music List Popup -->
<uni-popup ref="bgmPopup" type="bottom">
<view class="bgm-popup">
<view class="bgm-header">
<text class="bgm-title">选择背景音乐</text>
<view class="bgm-close" @tap="closeBgmList"></view>
</view>
<scroll-view scroll-y class="bgm-scroll">
<view
v-for="(item, index) in bgms"
:key="index"
class="bgm-item"
:class="{ active: currentBgmIndex === index && isBgmPlaying }"
@tap="selectBgm(index)"
>
<view class="bgm-info">
<uni-icons
:type="
currentBgmIndex === index && isBgmPlaying
? 'sound-filled'
: 'sound'
"
size="18"
:color="
currentBgmIndex === index && isBgmPlaying ? '#ff3b30' : '#333'
"
></uni-icons>
<text class="bgm-name">{{ item.name }}</text>
</view>
<view
v-if="currentBgmIndex === index && isBgmPlaying"
class="bgm-playing-icon"
>
<view class="bar bar1"></view>
<view class="bar bar2"></view>
<view class="bar bar3"></view>
</view>
</view>
</scroll-view>
<view class="bgm-footer" @tap="turnOffBgm">
<text class="turn-off-text">关闭音乐</text>
</view>
</view>
</uni-popup>
</view> </view>
</template> </template>
@@ -446,6 +498,7 @@ import {
getCardTemplateList, getCardTemplateList,
getCardTemplateContentList, getCardTemplateContentList,
getCardTemplateTitleList, getCardTemplateTitleList,
getCardMusicList,
} from "@/api/make"; } from "@/api/make";
import { abilityCheck, getShareReward, msgCheckApi } from "@/api/system"; import { abilityCheck, getShareReward, msgCheckApi } from "@/api/system";
import { import {
@@ -455,6 +508,8 @@ import {
onReachBottom, onReachBottom,
onShow, onShow,
onPullDownRefresh, onPullDownRefresh,
onUnload,
onHide,
} from "@dcloudio/uni-app"; } from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue"; import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
@@ -484,6 +539,98 @@ const titleState = ref({
scale: 1, 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(() => { const titleStyle = computed(() => {
return { return {
transform: `translate(${titleState.value.offsetX}rpx, ${titleState.value.offsetY}rpx) scale(${titleState.value.scale})`, transform: `translate(${titleState.value.offsetX}rpx, ${titleState.value.offsetY}rpx) scale(${titleState.value.scale})`,
@@ -744,6 +891,7 @@ onLoad((options) => {
getTemplateList(); getTemplateList();
getTemplateContentList(); getTemplateContentList();
getTemplateTitleList(); getTemplateTitleList();
getMusicList();
if (options.shareToken) { if (options.shareToken) {
shareToken.value = 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) => { const getTemplateTitleList = async (isLoadMore = false) => {
if (loadingTitles.value || (!hasMoreTitles.value && isLoadMore)) return; if (loadingTitles.value || (!hasMoreTitles.value && isLoadMore)) return;
@@ -2083,4 +2237,152 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
left: -9999px; left: -9999px;
top: -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;
}
</style> </style>