696 lines
16 KiB
Vue
Raw Normal View History

2025-12-02 20:25:48 +08:00
<!-- src/pages/base/birthday/card.vue -->
<!-- 生日贺卡展示页面家长端 - 信纸样式 -->
<template>
<view class="birthday-card-page" :class="bgColorClass">
<!-- 加载遮罩层 -->
<view v-if="isLoading" class="loading-overlay">
<view class="loading-content">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
<!-- 贺卡内容 -->
<view v-else-if="cardData" class="card-container">
<!-- 信纸主体 -->
<view class="letter-paper">
<!-- 信纸顶部装饰 -->
<view class="paper-header">
<view class="header-line"></view>
<view class="header-title">
<text class="title-icon">🎂</text>
<text class="title-text">生日祝福</text>
<text class="title-icon">🎂</text>
</view>
<view class="header-date">{{ formatBirthdayDate }}</view>
</view>
<!-- 称呼 -->
<view class="letter-greeting" v-if="greetingFromContent">
<view class="greeting-line">
<text class="greeting-text">{{ greetingFromContent }}</text>
</view>
</view>
<!-- 信件正文带虚线 -->
<view class="letter-content">
<view
v-for="(line, index) in contentLines"
:key="index"
class="content-line"
>
<text class="line-text">{{ line || '\u00A0' }}</text>
<view class="line-border"></view>
</view>
</view>
<!-- 签名区域 -->
<view class="letter-signature" v-if="cardData.signerName || cardData.signLabel">
<view class="signature-wrapper">
<!-- 签名文字 -->
<view class="signature-info">
<text class="signer-name">
<text v-if="cardData.signLabel" class="signer-label">{{ cardData.signLabel }}</text>
<text v-if="cardData.signerName">{{ cardData.signerName }}</text>
</text>
</view>
<!-- 日期 -->
<view class="signature-date">
<text>{{ currentDate }}</text>
</view>
</view>
</view>
<!-- 信纸底部装饰 -->
<view class="paper-footer">
<view class="footer-decoration">
<text class="footer-icon">🎉</text>
<text class="footer-icon">🎁</text>
<text class="footer-icon">🎈</text>
</view>
</view>
</view>
<!-- 右上角音乐图标 -->
<view
class="music-icon-wrapper"
:class="{ 'playing': isMusicPlaying }"
@click="toggleMusic"
>
<text class="music-icon-emoji">🎵</text>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-state">
<text class="empty-icon">🎂</text>
<text class="empty-text">贺卡不存在或已过期</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed, onUnmounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { srGetCardDetailApi, srMarkAsViewedApi } from "@/api/base/srApi";
import { imagUrl } from "@/utils";
const xxtsId = ref<string>("");
const cardData = ref<any>(null);
const isLoading = ref(false);
const isMusicPlaying = ref(false);
const audioContext = ref<any>(null);
// 格式化生日日期
const formatBirthdayDate = computed(() => {
if (!cardData.value?.birthdayDate) return '';
const date = cardData.value.birthdayDate;
if (date.includes('月')) return date;
try {
const d = new Date(date);
return `${d.getMonth() + 1}${d.getDate()}`;
} catch {
return date;
}
});
// 当前日期
const currentDate = computed(() => {
const now = new Date();
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}`;
});
// 寿星类型文本
const personTypeText = computed(() => {
const type = cardData.value?.srPersonType;
if (type === 'JS') return '老师';
if (type === 'XS') return '同学';
return '';
});
// 背景颜色类名(根据后台配置)
const bgColorClass = computed(() => {
const colorType = cardData.value?.hkMb?.animationType || 'blue';
return `bg-${colorType}`;
});
// 去掉HTML标签保留文本内容
const stripHtmlTags = (html: string): string => {
if (!html) return '';
// 使用正则表达式去掉所有HTML标签
return html.replace(/<[^>]+>/g, '').trim();
};
// 从内容中提取第一行作为称呼
const greetingFromContent = computed(() => {
if (!cardData.value?.zfContent) return '';
const content = cardData.value.zfContent;
// 按换行符或</p>标签分割
const lines = content.split(/<\/p>|<\/div>|\n/).filter((line: string) => line.trim());
if (lines.length > 0) {
// 去掉HTML标签获取第一行文本
let firstLine = lines[0];
// 去掉开头的<p>、<div>等标签
firstLine = firstLine.replace(/^<[^>]+>/, '');
// 去掉所有HTML标签
firstLine = stripHtmlTags(firstLine).trim();
return firstLine || '';
}
return '';
});
// 将祝福语内容按行分割从第二行开始去掉HTML标签确保每行都有虚线
const contentLines = computed(() => {
if (!cardData.value?.zfContent) return [''];
const content = cardData.value.zfContent;
// 按换行符或</p>标签分割
let lines = content.split(/<\/p>|<\/div>|\n/).filter((line: string) => line.trim());
// 去掉第一行(已用作称呼)
if (lines.length > 0) {
lines = lines.slice(1);
}
// 去掉每行的HTML标签
lines = lines.map((line: string) => {
// 去掉开头的<p>、<div>等标签
line = line.replace(/^<[^>]+>/, '');
// 去掉所有HTML标签保留文本
return stripHtmlTags(line).trim();
}).filter((line: string) => line); // 过滤空行
// 确保至少有5行让信纸看起来更完整
const minLines = 5;
while (lines.length < minLines) {
lines.push('');
}
return lines;
});
// 加载贺卡详情
const loadCardDetail = async () => {
if (!xxtsId.value) return;
isLoading.value = true;
try {
const res = await srGetCardDetailApi({ id: xxtsId.value });
if (res?.result) {
cardData.value = res.result;
const returnedXxtsId = res.result.xxtsId || xxtsId.value;
if (returnedXxtsId) {
await markAsViewed(returnedXxtsId);
}
// 初始化并播放背景音乐(优先使用后台配置,否则使用默认音乐)
initAudio();
}
} catch (e) {
console.error("加载贺卡详情失败:", e);
uni.showToast({ title: "加载失败", icon: "none" });
} finally {
isLoading.value = false;
}
};
// 标记已查阅
const markAsViewed = async (id: string) => {
if (!id) return;
try {
await srMarkAsViewedApi({ xxtsId: id });
} catch (e) {
console.error("标记已查阅失败:", e);
}
};
// 初始化音频
const initAudio = () => {
// 优先使用后台配置的音乐,如果没有则使用本地默认音乐
const musicUrl = cardData.value?.hkMb?.musicUrl
? imagUrl(cardData.value.hkMb.musicUrl)
: '/static/base/music/srkl.mp3';
try {
// #ifdef H5
audioContext.value = new Audio(musicUrl);
audioContext.value.loop = true;
audioContext.value.play().catch((e: any) => {
console.error("自动播放失败,可能需要用户交互:", e);
});
// #endif
// #ifdef MP-WEIXIN
audioContext.value = uni.createInnerAudioContext();
audioContext.value.src = musicUrl;
audioContext.value.loop = true;
audioContext.value.play();
// #endif
// 自动播放
isMusicPlaying.value = true;
} catch (e) {
console.error("初始化音频失败:", e);
}
};
// 切换音乐播放
const toggleMusic = () => {
if (!audioContext.value) return;
try {
if (isMusicPlaying.value) {
audioContext.value.pause();
} else {
audioContext.value.play();
}
isMusicPlaying.value = !isMusicPlaying.value;
} catch (e) {
console.error("音乐播放控制失败:", e);
}
};
// 页面卸载时清理
onUnmounted(() => {
if (audioContext.value) {
try {
audioContext.value.pause();
// #ifdef MP-WEIXIN
audioContext.value.destroy?.();
// #endif
} catch (e) {
console.error("清理音频失败:", e);
}
}
});
onLoad(async (options) => {
xxtsId.value = options?.id || '';
if (xxtsId.value) {
await loadCardDetail();
uni.setNavigationBarTitle({ title: "生日祝福" });
} else {
uni.showToast({ title: "缺少贺卡ID", icon: "none" });
}
});
</script>
<style scoped lang="scss">
.birthday-card-page {
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
// 默认浅蓝色背景
&.bg-blue {
background: linear-gradient(180deg, #e6f3ff 0%, #cce5ff 50%, #b3d9ff 100%);
.letter-paper {
background: linear-gradient(180deg, #f5faff 0%, #eef6ff 100%);
border-color: rgba(59, 130, 246, 0.2);
}
.header-line {
background: linear-gradient(90deg, transparent 0%, #3b82f6 50%, transparent 100%);
}
.title-text {
color: #1e40af;
}
.paper-footer {
border-top-color: #93c5fd;
}
.loading-overlay {
background-color: rgba(230, 243, 255, 0.95);
}
.loading-spinner {
border-color: #93c5fd;
border-top-color: #3b82f6;
}
}
// 浅黄色背景
&.bg-yellow {
background: linear-gradient(180deg, #fdf6e3 0%, #f5e6c8 50%, #efe0b9 100%);
.letter-paper {
background: linear-gradient(180deg, #fffef5 0%, #fff9e6 100%);
border-color: rgba(201, 162, 39, 0.2);
}
.header-line {
background: linear-gradient(90deg, transparent 0%, #c9a227 50%, transparent 100%);
}
.title-text {
color: #8b4513;
}
.paper-footer {
border-top-color: #e8d5a3;
}
.loading-overlay {
background-color: rgba(253, 246, 227, 0.95);
}
.loading-spinner {
border-color: #e8d5a3;
border-top-color: #c9a227;
}
}
// 浅紫色背景
&.bg-purple {
background: linear-gradient(180deg, #f3e8ff 0%, #e9d5ff 50%, #ddd6fe 100%);
.letter-paper {
background: linear-gradient(180deg, #faf5ff 0%, #f5f0ff 100%);
border-color: rgba(139, 92, 246, 0.2);
}
.header-line {
background: linear-gradient(90deg, transparent 0%, #8b5cf6 50%, transparent 100%);
}
.title-text {
color: #6b21a8;
}
.paper-footer {
border-top-color: #c4b5fd;
}
.loading-overlay {
background-color: rgba(243, 232, 255, 0.95);
}
.loading-spinner {
border-color: #c4b5fd;
border-top-color: #8b5cf6;
}
}
}
/* 加载遮罩 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 16px;
color: #666;
}
/* 贺卡容器 */
.card-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
padding-top: calc(20px + env(safe-area-inset-top));
padding-bottom: calc(20px + env(safe-area-inset-bottom));
}
/* 信纸主体 */
.letter-paper {
flex: 1;
border-radius: 8px;
padding: 30px 25px;
box-shadow:
0 4px 20px rgba(0, 0, 0, 0.1),
0 1px 3px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
border: 1px solid;
position: relative;
display: flex;
flex-direction: column;
}
/* 信纸左边红色竖线(模拟信纸) */
.letter-paper::before {
content: '';
position: absolute;
left: 20px;
top: 80px;
bottom: 100px;
width: 2px;
background: linear-gradient(180deg, #e74c3c 0%, #c0392b 100%);
opacity: 0.3;
}
/* 信纸顶部装饰 */
.paper-header {
text-align: center;
margin-bottom: 25px;
padding-bottom: 15px;
}
.header-line {
height: 2px;
margin-bottom: 15px;
}
.header-title {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 8px;
}
.title-icon {
font-size: 24px;
}
.title-text {
font-size: 26px;
font-weight: bold;
font-family: "KaiTi", "STKaiti", "楷体", serif;
}
.header-date {
font-size: 14px;
color: #a08060;
}
/* 称呼 */
.letter-greeting {
margin-bottom: 20px;
padding-left: 15px;
}
.greeting-line {
padding-bottom: 8px;
border-bottom: 1px dashed #d4c4a8;
}
.greeting-text {
font-size: 18px;
color: #5a4a3a;
font-family: "KaiTi", "STKaiti", "楷体", serif;
}
/* 信件正文 */
.letter-content {
flex: 1;
padding-left: 15px;
margin-bottom: 30px;
}
.content-line {
position: relative;
min-height: 40px;
display: flex;
align-items: flex-end;
padding-bottom: 8px;
}
.line-text {
font-size: 16px;
line-height: 1.8;
color: #4a3a2a;
font-family: "KaiTi", "STKaiti", "楷体", serif;
text-indent: 2em;
display: block;
width: 100%;
}
.line-border {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
border-bottom: 1px dashed #d4c4a8;
}
/* 签名区域 */
.letter-signature {
margin-top: auto;
padding-top: 20px;
}
.signature-wrapper {
display: flex;
flex-direction: column;
align-items: flex-end;
padding-right: 20px;
}
.signature-image {
width: 100px;
height: 45px;
margin-bottom: 5px;
}
.signature-info {
text-align: right;
margin-bottom: 8px;
}
.signer-label {
font-size: 14px;
color: #8b7355;
display: inline;
margin-right: 8px;
}
.signer-name {
font-size: 18px;
color: #5a4a3a;
font-weight: 500;
font-family: "KaiTi", "STKaiti", "楷体", serif;
display: inline;
}
.signature-date {
font-size: 14px;
color: #a08060;
border-bottom: 1px dashed #d4c4a8;
padding-bottom: 5px;
}
/* 信纸底部装饰 */
.paper-footer {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid;
}
.footer-decoration {
display: flex;
justify-content: center;
gap: 20px;
padding-top: 15px;
}
.footer-icon {
font-size: 24px;
animation: float-icon 3s ease-in-out infinite;
}
.footer-icon:nth-child(2) {
animation-delay: 0.5s;
}
.footer-icon:nth-child(3) {
animation-delay: 1s;
}
@keyframes float-icon {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
/* 右上角音乐图标 */
.music-icon-wrapper {
position: fixed;
top: calc(20px + env(safe-area-inset-top));
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
z-index: 100;
cursor: pointer;
transition: all 0.3s ease;
}
.music-icon-wrapper:active {
transform: scale(0.95);
}
.music-icon-wrapper.playing .music-icon-emoji {
animation: music-rotate 2s linear infinite;
}
.music-icon-emoji {
font-size: 28px;
display: block;
transition: transform 0.3s ease;
}
@keyframes music-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 空状态 */
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-text {
font-size: 16px;
color: #8b7355;
margin-bottom: 30px;
}
</style>