2025-12-02 20:25:02 +08:00

696 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- src/pages/view/routine/sr/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: #666;
margin-bottom: 30px;
}
</style>