1. 礼物特效
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<canvas ref="canvasRef" class="gift-effect-canvas" aria-hidden="true" />
|
||||
<Teleport to="body">
|
||||
<canvas ref="canvasRef" class="gift-effect-canvas" aria-hidden="true" />
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -252,12 +254,22 @@ function resizeCanvas() {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
dpr = Math.min(window.devicePixelRatio || 1, DPR_CAP)
|
||||
// iOS:优先用 visualViewport,避免地址栏伸缩导致尺寸错乱
|
||||
const vv = typeof window !== 'undefined' ? window.visualViewport : null
|
||||
const cssW = Math.max(1, Math.floor((vv && vv.width) || window.innerWidth || canvas.clientWidth || 1))
|
||||
const cssH = Math.max(1, Math.floor((vv && vv.height) || window.innerHeight || canvas.clientHeight || 1))
|
||||
// 用布局视口铺满全屏。iOS visualViewport 常窄于整屏且有 offset,
|
||||
// 再叠 wrapper overflow 裁切,会出现「只盖住左侧一块」的灰幕。
|
||||
const cssW = Math.max(
|
||||
1,
|
||||
Math.floor(window.innerWidth || 0),
|
||||
Math.floor(document.documentElement?.clientWidth || 0),
|
||||
)
|
||||
const cssH = Math.max(
|
||||
1,
|
||||
Math.floor(window.innerHeight || 0),
|
||||
Math.floor(document.documentElement?.clientHeight || 0),
|
||||
)
|
||||
W = cssW
|
||||
H = cssH
|
||||
canvas.style.left = '0px'
|
||||
canvas.style.top = '0px'
|
||||
canvas.style.width = `${cssW}px`
|
||||
canvas.style.height = `${cssH}px`
|
||||
canvas.style.writingMode = 'horizontal-tb'
|
||||
@@ -819,7 +831,7 @@ function playOne(item) {
|
||||
const payload = normalizePlayItem(item)
|
||||
if (!payload) return 0
|
||||
const { giftType, name, count } = payload
|
||||
if (!W || !H) resizeCanvas()
|
||||
resizeCanvas()
|
||||
clearVisuals()
|
||||
const now = performance.now()
|
||||
const style = GIFT_STYLE[giftType]
|
||||
@@ -1340,7 +1352,6 @@ onMounted(() => {
|
||||
}
|
||||
window.addEventListener('resize', onResize)
|
||||
window.visualViewport?.addEventListener('resize', onResize)
|
||||
window.visualViewport?.addEventListener('scroll', onResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -1353,7 +1364,6 @@ onUnmounted(() => {
|
||||
if (rafId) cancelAnimationFrame(rafId)
|
||||
window.removeEventListener('resize', onResize)
|
||||
window.visualViewport?.removeEventListener('resize', onResize)
|
||||
window.visualViewport?.removeEventListener('scroll', onResize)
|
||||
texts = []
|
||||
particles = []
|
||||
rings = []
|
||||
@@ -1379,9 +1389,11 @@ defineExpose({ play, playSequence, clear: clearAll, stop: clearAll })
|
||||
<style scoped>
|
||||
.gift-effect-canvas {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
width: 100vw;
|
||||
width: 100dvw;
|
||||
height: 100%;
|
||||
height: 100dvh;
|
||||
z-index: 46;
|
||||
@@ -1391,9 +1403,9 @@ defineExpose({ play, playSequence, clear: clearAll, stop: clearAll })
|
||||
text-orientation: mixed;
|
||||
direction: ltr;
|
||||
white-space: nowrap;
|
||||
/* iOS:独立合成层,避免和左侧点赞 canvas / transform 滚动打架 */
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
/* 独立合成层;挂到 body 后不再受请柬 wrapper overflow 裁切 */
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -372,6 +372,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 送礼未填姓名:弹出姓名模态框 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showGiftNameModal"
|
||||
class="fixed inset-0 z-[80] bg-black/40 flex items-center justify-center px-5"
|
||||
@click.self="closeGiftNameModal"
|
||||
>
|
||||
<div class="gift-name-modal w-full max-w-[360px] bg-white rounded-2xl shadow-2xl px-5 pt-5 pb-4">
|
||||
<div class="text-center mb-4">
|
||||
<div class="text-[15px] text-[#2c2c2c] tracking-[0.12em]">留下您的姓名</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1.5 leading-relaxed">送礼需要署名,方便新人知道心意来自谁</div>
|
||||
</div>
|
||||
<input
|
||||
ref="giftNameInputRef"
|
||||
v-model="giftNameDraft"
|
||||
type="text"
|
||||
maxlength="20"
|
||||
placeholder="您的姓名"
|
||||
class="w-full bg-[#Fdfbf7] border-[0.5px] border-[#a88c6b]/35 rounded-xl px-4 py-3 text-[14px] focus:outline-none focus:border-[#a88c6b]"
|
||||
@keyup.enter="confirmGiftName"
|
||||
/>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 py-3 rounded-full text-[12px] tracking-[0.2em] border border-[#a88c6b]/30 text-[#8A8680]"
|
||||
@click="closeGiftNameModal"
|
||||
>取消</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 py-3 rounded-full text-[12px] tracking-[0.2em] bg-[#a88c6b] text-white"
|
||||
:disabled="giftBusy"
|
||||
@click="confirmGiftName"
|
||||
>确认送礼</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 主滚动区域(注意:不要加 scroll-smooth,它会劫持 JS 滚动赋值导致卡顿) -->
|
||||
<!-- iOS 自由自动滚时对该节点做 translate3d,避免每帧写 scrollTop 与系统合成器打架 -->
|
||||
<div ref="scrollContainerRef"
|
||||
@@ -886,6 +924,10 @@ const giftTotal = ref(0)
|
||||
const giftBusy = ref(false)
|
||||
const showGiftPanel = ref(false)
|
||||
const giftForm = reactive({ name: '' })
|
||||
const showGiftNameModal = ref(false)
|
||||
const giftNameDraft = ref('')
|
||||
const giftNameInputRef = ref(null)
|
||||
const pendingGiftType = ref('')
|
||||
const giftQuota = reactive({
|
||||
my_likes: 0,
|
||||
spent: 0,
|
||||
@@ -1776,6 +1818,62 @@ const closeGiftPanel = () => {
|
||||
showGiftPanel.value = false
|
||||
}
|
||||
|
||||
const closeGiftNameModal = () => {
|
||||
showGiftNameModal.value = false
|
||||
pendingGiftType.value = ''
|
||||
giftNameDraft.value = ''
|
||||
}
|
||||
|
||||
const openGiftNameModal = (giftType) => {
|
||||
pendingGiftType.value = giftType
|
||||
giftNameDraft.value = giftForm.name.trim() || getGuestName() || ''
|
||||
showGiftNameModal.value = true
|
||||
nextTick(() => {
|
||||
giftNameInputRef.value?.focus?.()
|
||||
})
|
||||
}
|
||||
|
||||
const sendGiftRequest = async (giftType, name) => {
|
||||
giftBusy.value = true
|
||||
try {
|
||||
const res = await submitGift({
|
||||
giftType,
|
||||
name,
|
||||
clientId: getClientId(),
|
||||
})
|
||||
setGuestName(name)
|
||||
giftForm.name = name
|
||||
syncGuestNameIntoForms()
|
||||
applyGiftCounts(res.data?.counts, res.data?.total)
|
||||
applyGiftQuota(res.data?.quota)
|
||||
if (showGiftFx.value) {
|
||||
giftEffectRef.value?.play({ giftType, name, count: 1 })
|
||||
}
|
||||
toast.success('心意已送达')
|
||||
scheduleGiftPanelClose()
|
||||
} catch (e) {
|
||||
toast.error(e.message || '送礼失败')
|
||||
try {
|
||||
const res = await getGiftStatus(getClientId())
|
||||
applyGiftQuota(res.data?.quota)
|
||||
} catch { /* ignore */ }
|
||||
} finally {
|
||||
giftBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmGiftName = async () => {
|
||||
const name = giftNameDraft.value.trim()
|
||||
if (!name) return toast.error('请填写姓名')
|
||||
const giftType = pendingGiftType.value
|
||||
showGiftNameModal.value = false
|
||||
pendingGiftType.value = ''
|
||||
giftNameDraft.value = ''
|
||||
if (!giftType) return
|
||||
clearGiftPanelCloseTimer()
|
||||
await sendGiftRequest(giftType, name)
|
||||
}
|
||||
|
||||
const handleSendGift = async (giftType) => {
|
||||
// 点了任意礼物就延后关闭;忙碌中也续上 800ms,方便连送
|
||||
clearGiftPanelCloseTimer()
|
||||
@@ -1789,34 +1887,12 @@ const handleSendGift = async (giftType) => {
|
||||
scheduleGiftPanelClose()
|
||||
return toast.error(`点赞不足,再点赞 ${need} 次可兑换`)
|
||||
}
|
||||
const name = giftForm.name.trim()
|
||||
if (!name) return toast.error('请填写姓名')
|
||||
giftBusy.value = true
|
||||
try {
|
||||
const res = await submitGift({
|
||||
giftType,
|
||||
name,
|
||||
clientId: getClientId(),
|
||||
})
|
||||
setGuestName(name)
|
||||
syncGuestNameIntoForms()
|
||||
applyGiftCounts(res.data?.counts, res.data?.total)
|
||||
applyGiftQuota(res.data?.quota)
|
||||
if (showGiftFx.value) {
|
||||
giftEffectRef.value?.play({ giftType, name, count: 1 })
|
||||
}
|
||||
toast.success('心意已送达')
|
||||
// 800ms 内再点其他礼物则继续送;否则自动收起面板
|
||||
scheduleGiftPanelClose()
|
||||
} catch (e) {
|
||||
toast.error(e.message || '送礼失败')
|
||||
try {
|
||||
const res = await getGiftStatus(getClientId())
|
||||
applyGiftQuota(res.data?.quota)
|
||||
} catch { /* ignore */ }
|
||||
} finally {
|
||||
giftBusy.value = false
|
||||
const name = giftForm.name.trim() || getGuestName()
|
||||
if (!name) {
|
||||
openGiftNameModal(giftType)
|
||||
return
|
||||
}
|
||||
await sendGiftRequest(giftType, name)
|
||||
}
|
||||
|
||||
const loadLikeStatus = async () => {
|
||||
|
||||
Reference in New Issue
Block a user