This commit is contained in:
2026-08-26 15:10:25 +08:00
parent 06debfd1fc
commit fc9613197b
202 changed files with 28919 additions and 5551 deletions
@@ -0,0 +1,311 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style scoped>
html, body, #app { margin: 0; width: 100%; height: 100%; overflow: hidden; }
.birthday-page { position: relative; width: 100%; height: 100vh; overflow: hidden; color: #7f1d1d; background: #e76f45; }
.birthday-background { position: absolute; inset: 0; background-position: center; background-size: cover; z-index: 0; }
.birthday-mask { position: absolute; inset: 0; z-index: 1; background: linear-gradient(180deg, rgba(255,255,255,.02), rgba(92,20,8,.16)); }
.brand { position: absolute; top: calc(12px + env(safe-area-inset-top)); left: 14px; z-index: 8; padding: 7px 12px;
border: 1px solid rgba(255,255,255,.68); border-radius: 16px; color: #fff; background: rgba(128,29,16,.2);
font-size: 13px; font-weight: 600; backdrop-filter: blur(6px); }
.member-tag { position: absolute; top: calc(12px + env(safe-area-inset-top)); right: 14px; z-index: 8; padding: 7px 12px;
border-radius: 16px; color: #8a321c; background: rgba(255,244,213,.9); font-size: 12px; font-weight: 600; }
.animation-zone { position: relative; z-index: 3; height: 52%; padding-top: calc(42px + env(safe-area-inset-top)); }
.birthday-font { position: absolute; top: 8%; left: 4%; width: 92%; height: 47%; }
.birthday-cake { position: absolute; left: 12%; bottom: -7%; width: 76%; height: 64%; }
.confetti { position: absolute; top: -12px; width: 7px; height: 12px; border-radius: 3px; opacity: 0;
animation: confetti-fall linear infinite; }
.confetti:nth-child(3n) { background: #ffe16b; }
.confetti:nth-child(3n+1) { background: #fff4dd; }
.confetti:nth-child(3n+2) { background: #e9473f; }
@keyframes confetti-fall { 0% { transform: translate3d(0,-20px,0) rotate(0); opacity: 0; }
12% { opacity: 1; } 100% { transform: translate3d(35px,62vh,0) rotate(540deg); opacity: 0; } }
.greeting-zone { position: relative; z-index: 4; height: 48%; display: flex; flex-direction: column; align-items: center;
justify-content: flex-start; padding: 0 24px calc(20px + env(safe-area-inset-bottom)); box-sizing: border-box; }
.greeting-card { width: 100%; box-sizing: border-box; padding: 20px 20px 16px; border: 1px solid rgba(164,60,26,.3);
border-radius: 18px; background: rgba(255,249,228,.92); box-shadow: 0 12px 30px rgba(109,29,10,.22);
text-align: center; animation: card-enter .7s ease-out both; }
@keyframes card-enter { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } }
.greeting-title { margin-bottom: 10px; font-size: 18px; font-weight: 700; text-align: left; }
.greeting-content { font-size: 15px; line-height: 1.8; letter-spacing: 1px; }
.receive-button { width: 190px; margin-top: 18px; padding: 13px 20px; border: 0; border-radius: 25px; color: #fff;
background: linear-gradient(135deg, #d9362b, #9d1717); box-shadow: 0 8px 18px rgba(131,21,17,.34);
font-size: 16px; font-weight: 700; letter-spacing: 2px; }
.music-button { position: absolute; right: 14px; bottom: calc(14px + env(safe-area-inset-bottom)); z-index: 9;
width: 42px; height: 42px; border: 1px solid rgba(255,255,255,.7); border-radius: 50%; color: #fff;
background: rgba(121,27,15,.42); font-size: 20px; }
.music-button.playing { animation: music-rotate 3s linear infinite; }
@keyframes music-rotate { to { transform: rotate(360deg); } }
.music-consent-modal { position: fixed; inset: 0; z-index: 10040; display: flex; align-items: center; justify-content: center;
padding: calc(24px + env(safe-area-inset-top)) 24px calc(24px + env(safe-area-inset-bottom)); box-sizing: border-box;
background: rgba(61,13,8,.76); backdrop-filter: blur(7px); }
.music-consent-card { position: relative; width: 100%; max-width: 390px; padding: 70px 24px 26px; box-sizing: border-box;
border: 1px solid rgba(211,148,57,.72); border-radius: 28px; background: #fff8e8;
box-shadow: 0 22px 60px rgba(56,8,4,.48), inset 0 0 0 5px rgba(255,255,255,.44); text-align: center;
animation: music-dialog-enter .48s cubic-bezier(.2,.85,.32,1.16) both; }
@keyframes music-dialog-enter { from { opacity: 0; transform: translateY(24px) scale(.92); }
to { opacity: 1; transform: translateY(0) scale(1); } }
.music-consent-emblem { position: absolute; top: -45px; left: 50%; display: flex; align-items: center; justify-content: center;
width: 90px; height: 90px; border: 6px solid #fff8e8; border-radius: 50%; color: #fff;
background: #d9362b; box-shadow: 0 10px 24px rgba(144,29,20,.34); transform: translateX(-50%); }
.music-consent-emblem .fa { font-size: 36px; }
.music-consent-title { margin: 0; color: #a51f1a; font-size: 31px; line-height: 1.25; font-weight: 800;
letter-spacing: 2px; }
.music-consent-subtitle { margin: 14px 0 0; color: #8b4b2c; font-size: 16px; line-height: 1.7; }
.music-consent-divider { width: 70%; height: 1px; margin: 20px auto 18px; background: #e7b35b; opacity: .72; }
.music-consent-tip { margin-bottom: 18px; color: #9a5d3a; font-size: 13px; line-height: 1.6; }
.music-consent-button { display: flex; align-items: center; justify-content: center; width: 100%; height: 50px;
border-radius: 25px; font-size: 17px; font-weight: 700; letter-spacing: 1px; outline: none; }
.music-consent-button .fa { margin-right: 9px; font-size: 18px; }
.music-consent-button.primary { border: 0; color: #fff; background: #c92921;
box-shadow: 0 9px 20px rgba(174,35,27,.3); }
.music-consent-button.secondary { margin-top: 13px; border: 1px solid #d99a45; color: #9b542b; background: #fffaf0; }
.music-consent-button:active { transform: scale(.98); }
.music-consent-button:focus-visible, .music-button:focus-visible { outline: 3px solid rgba(255,225,107,.9); outline-offset: 3px; }
.coupon-modal { position: fixed; inset: 0; z-index: 10020; display: flex; align-items: center; justify-content: center;
padding: calc(22px + env(safe-area-inset-top)) 20px calc(22px + env(safe-area-inset-bottom));
box-sizing: border-box; background: rgba(43,10,7,.72); backdrop-filter: blur(5px); }
.coupon-panel { position: relative; width: 100%; max-width: 430px; max-height: 88vh; padding: 12px; box-sizing: border-box;
border-radius: 20px; background: #fff7e6; box-shadow: 0 18px 55px rgba(40,5,0,.45); overflow: auto; }
.coupon-close { position: absolute; top: 18px; right: 18px; z-index: 2; width: 34px; height: 34px; border: 0;
border-radius: 50%; color: #fff; background: rgba(87,17,13,.72); font-size: 24px; line-height: 34px; }
.coupon-image { display: block; width: 100%; border-radius: 14px; }
.coupon-empty { min-height: 390px; padding: 70px 28px 30px; box-sizing: border-box; border: 2px dashed #d59569;
border-radius: 14px; text-align: center; background: linear-gradient(155deg, #fff7df, #ffd9bd); }
.coupon-empty-icon { font-size: 74px; }
.coupon-empty-title { margin-top: 18px; color: #a21e19; font-size: 28px; font-weight: 800; }
.coupon-empty-text { margin-top: 24px; color: #7b3a2a; font-size: 15px; line-height: 1.8; }
.coupon-tip { padding: 10px 4px 2px; color: #7b3a2a; font-size: 12px; line-height: 1.6; text-align: center; }
.loading-cover { position: fixed; inset: 0; z-index: 10030; display: flex; align-items: center; justify-content: center;
color: #fff; background: rgba(76,20,10,.42); font-size: 14px; }
@media (max-width: 375px) {
.animation-zone { height: 48%; }
.greeting-zone { height: 52%; padding-left: 18px; padding-right: 18px; }
.greeting-card { padding: 16px; }
.greeting-content { font-size: 14px; line-height: 1.65; }
.receive-button { margin-top: 12px; }
.music-consent-card { padding: 64px 20px 22px; }
.music-consent-title { font-size: 27px; }
}
@media (prefers-reduced-motion: reduce) {
.music-consent-card, .greeting-card, .confetti, .music-button.playing { animation: none; }
}
</style>
<div id="app">
<div class="birthday-page">
<div class="birthday-background" :style="{backgroundImage:'url(' + config.backgroundUrl + ')'}"></div>
<div class="birthday-mask"></div>
<div class="brand">{{ appName }}</div>
<div class="member-tag">会员专享</div>
<div class="animation-zone">
<div ref="fontAnimation" class="birthday-font"></div>
<div ref="cakeAnimation" class="birthday-cake"></div>
<i v-for="item in confettiItems" :key="item.id" class="confetti" :style="item.style"></i>
</div>
<div class="greeting-zone">
<div class="greeting-card">
<div class="greeting-title">亲爱的 {{ userName }} 老师:</div>
<div class="greeting-content">
步履所至,皆有回响,心之所向,岁月成诗。<br>
在这个特别的日子里,校工会祝您:<br>
生日快乐,幸福安康!
</div>
</div>
<button class="receive-button" @click="openCoupon">领取生日福利</button>
</div>
<button type="button" class="music-button" :class="{playing:musicPlaying}"
:aria-label="musicPlaying ? '暂停音乐' : '播放音乐'" @click="toggleMusic">
<i class="fa fa-music" aria-hidden="true"></i>
</button>
<div v-if="showMusicPrompt" class="music-consent-modal">
<section class="music-consent-card" role="dialog" aria-modal="true" aria-labelledby="birthdayMusicTitle">
<div class="music-consent-emblem" aria-hidden="true">
<i class="fa fa-music"></i>
</div>
<h2 id="birthdayMusicTitle" class="music-consent-title">祝您生日快乐</h2>
<p class="music-consent-subtitle">愿美好与祝福伴随您的每一天</p>
<div class="music-consent-divider"></div>
<div class="music-consent-tip">开启音乐,沉浸感受生日祝福</div>
<button type="button" class="music-consent-button primary" @click="enableBirthdayMusic">
<i class="fa fa-music" aria-hidden="true"></i>
开启音乐
</button>
<button type="button" class="music-consent-button secondary" @click="declineBirthdayMusic">暂不开启</button>
</section>
</div>
<div v-if="showCoupon" class="coupon-modal" @click.self="closeCoupon">
<div class="coupon-panel">
<button class="coupon-close" @click="closeCoupon">×</button>
<img v-if="config.couponImageUrl" class="coupon-image" :src="config.couponImageUrl" alt="生日福利贺卡">
<div v-else class="coupon-empty">
<div class="coupon-empty-icon">🎂</div>
<div class="coupon-empty-title">生日快乐</div>
<div class="coupon-empty-text">校工会为您送上诚挚祝福。<br>福利贺卡尚未配置,请联系工作人员。</div>
</div>
<div class="coupon-tip">请按福利贺卡上的说明领取和使用生日福利</div>
</div>
</div>
<div v-if="pageLoading" class="loading-cover">生日祝福加载中...</div>
</div>
</div>
<script src="${base!}/assets/platform/plugins/lottie/lottie.min.js"></script>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
appName: "${AppName!}",
userName: "${userName!}",
pageLoading: false,
showMusicPrompt: false,
musicPromptResolved: false,
showCoupon: false,
musicPlaying: false,
popupHistoryToken: "",
fontAnimation: null,
cakeAnimation: null,
audio: null,
config: {
backgroundUrl: "${base!}/assets/mobile/img/birthday/custom-bg.png",
couponImageUrl: "",
musicUrl: "${base!}/assets/mobile/music/birthday/birthdaySong.mp3",
fontAnimationUrl: "${base!}/assets/mobile/lottie/birthday/happyBirthday.json",
cakeAnimationUrl: "${base!}/assets/mobile/lottie/birthday/lihua.json"
},
confettiItems: Array.from({length:28}, (value, index) => ({
id:index,
style:{
left:(index * 37 % 100) + "%",
animationDelay:((index * 13 % 30) / 10) + "s",
animationDuration:(3 + (index % 5) * .45) + "s"
}
}))
}
},
methods: {
resolveFileUrl(value) {
if (!value) return ""
if (value.indexOf("http://") === 0 || value.indexOf("https://") === 0 || value.indexOf("/") === 0) return value
return "/platform/sys/file/download?id=" + encodeURIComponent(value)
},
loadConfig() {
this.$set(this, "pageLoading", true)
this.$axios.post("/platform/staffManage/birthday/manage/getConfig").then((res) => {
if (res.code !== 0 || !res.data) return
if (res.data.picUrl) this.$set(this.config, "backgroundUrl", this.resolveFileUrl(res.data.picUrl))
const linkFileId = "${fileId!}"
const couponFile = linkFileId || res.data.birthdayUrl
this.$set(this.config, "couponImageUrl", this.resolveFileUrl(couponFile))
}).finally(() => {
this.$set(this, "pageLoading", false)
})
},
loadAnimations() {
if (window.lottie && this.$refs.fontAnimation) {
this.$set(this, "fontAnimation", window.lottie.loadAnimation({
container:this.$refs.fontAnimation,
renderer:"svg",
loop:true,
autoplay:true,
path:this.config.fontAnimationUrl
}))
}
if (window.lottie && this.$refs.cakeAnimation) {
this.$set(this, "cakeAnimation", window.lottie.loadAnimation({
container:this.$refs.cakeAnimation,
renderer:"svg",
loop:true,
autoplay:true,
path:this.config.cakeAnimationUrl
}))
}
},
openCoupon() {
if (this.showCoupon) return
this.$set(this, "showCoupon", true)
window.h5PopupHistory.open(this.popupHistoryToken, "birthdayCoupon")
},
closeCoupon() {
const historyHandled = window.h5PopupHistory.close(this.popupHistoryToken, "birthdayCoupon")
if (!historyHandled) this.closeCouponState()
},
closeCouponState() {
this.$set(this, "showCoupon", false)
},
openMusicPrompt() {
if (this.showMusicPrompt || this.musicPromptResolved) return
this.$set(this, "showMusicPrompt", true)
window.h5PopupHistory.open(this.popupHistoryToken, "birthdayMusicPrompt")
},
closeMusicPrompt() {
if (!this.showMusicPrompt) return
// 先同步关闭界面,再回退弹框历史,避免部分 WebView 延迟 popstate 导致弹窗无法关闭。
this.closeMusicPromptState()
window.h5PopupHistory.close(this.popupHistoryToken, "birthdayMusicPrompt")
},
closeMusicPromptState() {
this.$set(this, "showMusicPrompt", false)
this.$set(this, "musicPromptResolved", true)
},
enableBirthdayMusic() {
if (!this.showMusicPrompt) return
this.closeMusicPrompt()
this.playMusic()
},
declineBirthdayMusic() {
if (!this.showMusicPrompt) return
this.closeMusicPrompt()
},
playMusic() {
if (!this.audio) this.$set(this, "audio", new Audio(this.config.musicUrl))
this.audio.loop = true
this.audio.volume = .3
this.audio.play().then(() => {
this.$set(this, "musicPlaying", true)
}).catch(() => {
this.$set(this, "musicPlaying", false)
})
},
toggleMusic() {
if (this.musicPlaying && this.audio) {
this.audio.pause()
this.$set(this, "musicPlaying", false)
return
}
this.playMusic()
}
},
mounted() {
this.$set(this, "popupHistoryToken", window.h5PopupHistory.register((popupKey) => {
if (popupKey === "birthdayCoupon") this.closeCouponState()
if (popupKey === "birthdayMusicPrompt") this.closeMusicPromptState()
}))
this.loadConfig()
this.loadAnimations()
this.$nextTick(() => {
this.openMusicPrompt()
})
},
beforeDestroy() {
window.h5PopupHistory.unregister(this.popupHistoryToken)
if (this.fontAnimation) this.fontAnimation.destroy()
if (this.cakeAnimation) this.cakeAnimation.destroy()
if (this.audio) {
this.audio.pause()
this.audio.src = ""
}
}
})
</script>
<!--#
}
#-->
@@ -1,6 +1,29 @@
const MOBILE_MEMBER_INFO = {
template: /*language=HTML*/ `
<div class="detail-container">
<div v-if="profileMode" class="mine-profile-details">
<section class="mine-detail-card">
<h2>基本信息</h2>
<div class="mine-detail-row"><span>姓名</span><strong>{{ viewData.username || '-' }}</strong></div>
<div class="mine-detail-row"><span>性别</span><strong>{{ viewData.sex || '-' }}</strong></div>
<div class="mine-detail-row"><span>民族</span><strong>{{ viewData.nation || '-' }}</strong></div>
<div class="mine-detail-row"><span>出生日期</span><strong>{{ formatBirthday(viewData.birthday) }}</strong></div>
<div class="mine-detail-row"><span>政治面貌</span><strong>{{ viewData.political || '-' }}</strong></div>
</section>
<section class="mine-detail-card">
<h2>组织信息</h2>
<div class="mine-detail-row"><span>工作单位</span><strong>{{ viewData.unitName || '-' }}</strong></div>
<div class="mine-detail-row"><span>所属工会</span><strong>{{ viewData.unionName || '-' }}</strong></div>
<div class="mine-detail-row"><span>人员类型</span><strong>{{ viewData.personType || '-' }}</strong></div>
</section>
<section class="mine-detail-card">
<h2>联系方式</h2>
<div class="mine-detail-row"><span>联系电话</span><strong>{{ maskMobile(viewData.mobile) }}</strong></div>
<div class="mine-detail-row"><span>电子邮箱</span><strong>{{ viewData.email || '-' }}</strong></div>
</section>
</div>
<div v-else class="detail-container">
<van-cell-group title="基本信息">
<van-cell title="工号">{{ viewData.loginname }}</van-cell>
<van-cell title="姓名">{{ viewData.username }}</van-cell>
@@ -61,7 +84,8 @@
</div>
`,
props: {
userId: { type: String, required: true }
userId: { type: String, required: true },
profileMode: { type: Boolean, default: false }
},
data() {
return {
@@ -77,6 +101,18 @@
}
},
methods: {
formatBirthday(birthday) {
if (!birthday) {
return "-"
}
return this.$moment(birthday).format("YYYY-MM-DD")
},
maskMobile(mobile) {
if (!mobile) {
return "-"
}
return mobile.replace(/^(\d{3})\d+(\d{4})$/, "$1****$2")
},
findUserInfoByUserId() {
if (!this.userId) {
return