@@ -38,7 +38,7 @@
< DanmakuLayer
ref = "danmakuLayerRef"
: enabled= "!!data.danmakuEnabled"
: enabled= "!!data.danmakuEnabled && showDanmaku "
:show-time = "!!data.danmakuShowTime"
:active = "effectsActive"
/ >
@@ -54,13 +54,32 @@
< span class = "scroll-progress-xi calligraphy" > 囍 < / span >
< / div >
<!-- 右上角 : 自动滚动 -- >
< div class = "fixed top-6 right-5 z-50 flex flex-col items-end gap-4 " >
<!-- 右上角 : 自动滚动 / 弹幕 / 礼物特效 -->
< div class = "fixed top-6 right-5 z-50 flex flex-col items-end gap-3 " >
< div @click ="toggleAutoScroll" class = "control-btn cursor-pointer transition-all duration-300"
: class = "isAutoScroll ? 'bg-[#a88c6b]/15 !border-[#a88c6b]/60 shadow-sm' : 'bg-white/60 shadow-sm border-[#a88c6b]/30'" >
: class = "isAutoScroll ? 'bg-[#a88c6b]/15 !border-[#a88c6b]/60 shadow-sm' : 'bg-white/60 shadow-sm border-[#a88c6b]/30'"
title = "自动滚动" >
< img v-if = "isAutoScroll" src="https://api.iconify.design/lucide:pause.svg?color=%23a88c6b" class="w-4 h-4" / >
< img v-else src = "https://api.iconify.design/lucide:play.svg?color=%23a88c6b" class = "w-4 h-4 ml-0.5" / >
< / div >
< button
type = "button"
class = "control-btn cursor-pointer transition-all duration-300"
: class = "showDanmaku ? 'bg-[#a88c6b]/15 !border-[#a88c6b]/60 shadow-sm' : 'bg-white/60 shadow-sm border-[#a88c6b]/30 opacity-55'"
title = "弹幕展示"
@click ="toggleShowDanmaku"
>
< i class = "fa-solid fa-comment-dots text-[13px]" > < / i >
< / button >
< button
type = "button"
class = "control-btn cursor-pointer transition-all duration-300"
: class = "showGiftFx ? 'bg-[#a88c6b]/15 !border-[#a88c6b]/60 shadow-sm' : 'bg-white/60 shadow-sm border-[#a88c6b]/30 opacity-55'"
title = "礼物特效"
@click ="toggleShowGiftFx"
>
< i class = "fa-solid fa-gift text-[13px]" > < / i >
< / button >
< / div >
<!-- 底部互动区 -- >
@@ -89,6 +108,19 @@
< / button >
< / div >
< / div >
<!-- 行1 .5 : 礼物 -- >
< div class = "action-dock-row" >
< button
type = "button"
class = "action-pill gift-fab"
: class = "{ busy: giftBusy }"
title = "送礼物"
@click ="openGiftPanel"
>
< i class = "fa-solid fa-gift gift-icon" > < / i >
< span class = "like-count" > { { giftTotal || '礼物' } } < / span >
< / button >
< / div >
<!-- 行2 : 导航 + 回执 -- >
< div class = "action-dock-row" >
< button
@@ -103,7 +135,7 @@
type = "button"
class = "action-pill action-rsvp"
title = "出席回执"
@click ="showTrackList = false; isDrawerOpen = true "
@click ="showTrackList = false; openRsvpDrawer() "
>
< img src = "https://api.iconify.design/lucide:clipboard-pen.svg?color=%23a88c6b" class = "action-pill-icon" alt = "" / >
< / button >
@@ -253,6 +285,45 @@
< / div >
< LikeBurst ref = "likeBurstRef" / >
< GiftEffect ref = "giftEffectRef" @ play -start = " onGiftPlayStart " / >
< GiftFeed ref = "giftFeedRef" / >
<!-- 礼物面板 -- >
< div v-if = "showGiftPanel" class="fixed inset-0 z-[60] bg-black/25 transition-opacity duration-200" @click="closeGiftPanel" > < / div >
< div
class = "gift-panel fixed bottom-0 left-0 right-0 mx-auto w-full max-w-[420px] bg-[#fdfbf7] rounded-t-2xl shadow-xl transition-transform duration-300 z-[70] px-4 pt-4 pb-5 flex flex-col"
: class = "{ 'pointer-events-none': !showGiftPanel }"
: style = "{ transform: showGiftPanel ? 'translateY(0)' : 'translateY(110%)' }"
>
< div class = "gift-panel-handle" aria -hidden = " true " > < / div >
< div class = "flex items-center justify-between mb-2.5 px-0.5" >
< span class = "text-[13px] text-[#a88c6b] tracking-[0.25em]" > 送一份心意 < / span >
< button type = "button" class = "p-1.5 text-[#a88c6b]/70 text-sm leading-none" @click ="closeGiftPanel" > ✕ < / button >
< / div >
< div class = "gift-quota mb-2.5" >
< span v-if = "giftQuota.remain > 0" > 还可赠送 < b > { { giftQuota . remain } } < / b > 次 < / span >
< span v-else > 赠送次数已用完 , 再点赞 < b > { { giftQuota . likes _to _next } } < / b > 次可多送 1 次 < / span >
< span class = "gift-quota-sub" > 已送 { { giftQuota . gift _used } } / { { giftQuota . gift _allow } } · 点赞 { { giftQuota . my _likes } } ( 每 { { giftQuota . likes _per _gift } } 赞 + 1 次 ) < / span >
< / div >
< input
v-model = "giftForm.name"
placeholder = "您的姓名"
class = "w-full bg-white border-[0.5px] border-[#a88c6b]/25 rounded-lg px-3 py-2 text-[12px] mb-2.5 focus:outline-none focus:border-[#a88c6b]"
/ >
< div class = "gift-grid" >
< button
v-for = "g in giftTypes"
:key = "g.key"
type = "button"
class = "gift-item"
: disabled = "giftBusy || giftQuota.remain <= 0"
@click ="handleSendGift(g.key)"
>
< span class = "gift-item-label calligraphy-strong" > { { g . label } } < / span >
< span class = "gift-item-count" > { { giftCounts [ g . key ] || 0 } } < / span >
< / button >
< / div >
< / div >
<!-- 主滚动区域 ( 注意 : 不要加 scroll - smooth , 它会劫持 JS 滚动赋值导致卡顿 ) -- >
<!-- iOS 自由自动滚时对该节点做 translate3d , 避免每帧写 scrollTop 与系统合成器打架 -- >
@@ -427,7 +498,7 @@
< span class = "ending-address" > { { data . address } } < / span >
< / div >
< div v-reveal = "{ variant: 'up', delay: 300 }" class="ending-actions w-full max-w-[280px]" >
< button class = "rsvp-cta" @click ="isDrawerOpen = true " >
< button class = "rsvp-cta" @click ="openRsvpDrawer " >
< span class = "rsvp-cta-deco" > ❀ < / span >
< span class = "rsvp-cta-text" > 填写出席回执 < / span >
< span class = "rsvp-cta-line" > < / span >
@@ -622,11 +693,13 @@
< script setup >
import { ref , reactive , computed , nextTick , onMounted , onUnmounted , watch } from 'vue'
import { getAlbumImages , getConfig , submitRsvp , submitDanmaku , generateAiBlessing , getLikeStatus , submitLike } from '@/api/wedding'
import { getAlbumImages , getConfig , submitRsvp , submitDanmaku , generateAiBlessing , getLikeStatus , submitLike , getGiftStatus , submitGift } from '@/api/wedding'
import { useAudio } from '@/composables/useAudio'
import { toast } from '@/composables/useToast'
import { pickMusicUrl , rememberMusicPick } from '@/utils/musicPick'
import { getClientId } from '@/utils/clientId'
import { getGuestName , setGuestName } from '@/utils/guestName'
import { GIFT _TYPES , emptyGiftCounts , aggregateGiftRecent } from '@/utils/giftTypes'
import { sendVisitBeacon } from '@/utils/visitBeacon'
import { optimizeImageUrl , isIOS } from '@/composables/useAndroidOptimize'
import { DANMAKU _COLORS , DANMAKU _GRADIENT _COLORS , DEFAULT _DANMAKU _COLOR , BLESSING _STYLES } from '@/utils/danmakuColors'
@@ -634,6 +707,8 @@ import { parseLrc, formatAudioTime, findLrcIndex } from '@/utils/parseLrc'
import CanvasEffects from '@/components/CanvasEffects.vue'
import DanmakuLayer from '@/components/DanmakuLayer.vue'
import LikeBurst from '@/components/LikeBurst.vue'
import GiftEffect from '@/components/GiftEffect.vue'
import GiftFeed from '@/components/GiftFeed.vue'
import PhotoGallery from '@/components/PhotoGallery.vue'
const DEFAULTS = {
@@ -742,14 +817,64 @@ const aiBlessingLoading = ref(false)
const aiBlessingTarget = ref ( '' )
const blessingStyles = BLESSING _STYLES
const likeBurstRef = ref ( null )
const giftEffectRef = ref ( null )
const giftFeedRef = ref ( null )
const likeCount = ref ( 0 )
const likePulse = ref ( false )
const likeBusy = ref ( false )
let likePulseTimer = null
const giftTypes = GIFT _TYPES
const giftCounts = reactive ( emptyGiftCounts ( ) )
const giftTotal = ref ( 0 )
const giftBusy = ref ( false )
const showGiftPanel = ref ( false )
const giftForm = reactive ( { name : '' } )
const giftQuota = reactive ( {
my _likes : 0 ,
gift _used : 0 ,
gift _allow : 1 ,
gift _remain : 1 ,
likes _to _next : 50 ,
likes _per _gift : 50 ,
} )
const pendingGiftReplay = ref ( [ ] )
const applyGiftQuota = ( quota ) => {
if ( ! quota || typeof quota !== 'object' ) return
giftQuota . my _likes = Number ( quota . my _likes ) || 0
giftQuota . gift _used = Number ( quota . gift _used ) || 0
giftQuota . gift _allow = Number ( quota . gift _allow ) || 1
giftQuota . gift _remain = Math . max ( 0 , Number ( quota . gift _remain ) || 0 )
giftQuota . likes _to _next = Number ( quota . likes _to _next ) || 50
giftQuota . likes _per _gift = Number ( quota . likes _per _gift ) || 50
}
const DANMAKU _PREF _KEY = 'wedding_show_danmaku_v1'
const GIFT _FX _PREF _KEY = 'wedding_show_gift_fx_v1'
const readPref = ( key , fallback = true ) => {
try {
const v = localStorage . getItem ( key )
if ( v === null ) return fallback
return v !== '0' && v !== 'false'
} catch {
return fallback
}
}
const showDanmaku = ref ( readPref ( DANMAKU _PREF _KEY , true ) )
const showGiftFx = ref ( readPref ( GIFT _FX _PREF _KEY , true ) )
const toggleShowDanmaku = ( ) => {
showDanmaku . value = ! showDanmaku . value
try { localStorage . setItem ( DANMAKU _PREF _KEY , showDanmaku . value ? '1' : '0' ) } catch { /* ignore */ }
}
const toggleShowGiftFx = ( ) => {
showGiftFx . value = ! showGiftFx . value
try { localStorage . setItem ( GIFT _FX _PREF _KEY , showGiftFx . value ? '1' : '0' ) } catch { /* ignore */ }
}
const previewVisible = ref ( false ) , previewIndex = ref ( 0 ) , albumLoaded = ref ( false )
const albumPreviewImages = ref ( [ ] )
const rsvpForm = reactive ( { name : '' , guest _count : '1' , wishes : '' } )
const blessingForm = reactive ( { name : '' , content : '' , color : DEFAULT _DANMAKU _COLOR } )
const rsvpForm = reactive ( { name : getGuestName ( ) , guest _count : '1' , wishes : '' } )
const blessingForm = reactive ( { name : getGuestName ( ) , content : '' , color : DEFAULT _DANMAKU _COLOR } )
const danmakuColorOptions = DANMAKU _COLORS
const danmakuGradientOptions = DANMAKU _GRADIENT _COLORS
const rsvpOptions = [ { value : '1' , label : '1 人出席' } , { value : '2' , label : '2 人出席' } , { value : '3' , label : '3 人及以上' } , { value : '0' , label : '遗憾缺席' } ]
@@ -762,8 +887,22 @@ const clearBlessingAutoClose = () => {
blessingCloseLeft . value = 0
}
const syncGuestNameIntoForms = ( ) => {
const n = getGuestName ( )
if ( ! n ) return
if ( ! rsvpForm . name . trim ( ) ) rsvpForm . name = n
if ( ! blessingForm . name . trim ( ) ) blessingForm . name = n
if ( ! giftForm . name . trim ( ) ) giftForm . name = n
}
const openRsvpDrawer = ( ) => {
syncGuestNameIntoForms ( )
isDrawerOpen . value = true
}
const openBlessingDrawer = ( ) => {
clearBlessingAutoClose ( )
syncGuestNameIntoForms ( )
blessingSubmitted . value = false
showBlessingDrawer . value = true
}
@@ -1018,7 +1157,7 @@ const openImagePreview = async (src) => {
}
const effectsActive = computed ( ( ) =>
! isDrawerOpen . value && ! showBlessingDrawer . value && ! previewVisible . value && ! showMapOptions . value && ! showOpenInBrowser . value && ! showIntro . value
! isDrawerOpen . value && ! showBlessingDrawer . value && ! showGiftPanel . value && ! previewVisible . value && ! showMapOptions . value && ! showOpenInBrowser . value && ! showIntro . value
)
const closeDrawer = ( ) => { isDrawerOpen . value = false }
@@ -1085,7 +1224,7 @@ const clearBottomReturn = () => {
}
const scheduleBottomReturn = ( reset = false ) => {
if ( ! isAtBottom ( ) || ! isAutoScroll . value || previewVisible . value || showMapOptions . value || showOpenInBrowser . value || showBlessingDrawer . value ) return
if ( ! isAtBottom ( ) || ! isAutoScroll . value || previewVisible . value || showMapOptions . value || showOpenInBrowser . value || showBlessingDrawer . value || showGiftPanel . value ) return
if ( bottomReturnTimer && ! reset ) return
clearBottomReturn ( )
bottomReturnTimer = setTimeout ( ( ) => {
@@ -1135,6 +1274,7 @@ const canAutoAdvance = () =>
&& ! isSnapping . value
&& ! isDrawerOpen . value
&& ! showBlessingDrawer . value
&& ! showGiftPanel . value
&& ! previewVisible . value
&& ! showMapOptions . value
&& ! showOpenInBrowser . value
@@ -1243,6 +1383,15 @@ const completeIntro = () => {
pendingLikeBurst = 0
nextTick ( ( ) => likeBurstRef . value ? . burst ( n ) )
}
if ( pendingGiftReplay . value . length ) {
const list = pendingGiftReplay . value . slice ( )
pendingGiftReplay . value = [ ]
if ( showGiftFx . value ) {
nextTick ( ( ) => giftEffectRef . value ? . playSequence ( list , { max : 12 } ) )
} else {
nextTick ( ( ) => replayGiftFeedOnly ( list ) )
}
}
scheduleAutoScrollStart ( )
}
@@ -1334,6 +1483,8 @@ const handleSubmitRsvp = async () => {
try {
const res = await submitRsvp ( rsvpForm )
if ( res . code === 200 ) {
setGuestName ( rsvpForm . name )
syncGuestNameIntoForms ( )
if ( res . danmaku ) danmakuLayerRef . value ? . pushItem ? . ( res . danmaku )
isSubmitted . value = true
}
@@ -1351,6 +1502,8 @@ const handleSubmitBlessing = async () => {
color : blessingForm . color || DEFAULT _DANMAKU _COLOR ,
} )
if ( res . code === 200 ) {
setGuestName ( blessingForm . name )
syncGuestNameIntoForms ( )
if ( res . data ) danmakuLayerRef . value ? . pushItem ? . ( res . data )
blessingSubmitted . value = true
scheduleBlessingAutoClose ( )
@@ -1363,6 +1516,87 @@ const handleSubmitBlessing = async () => {
let pendingLikeBurst = 0
const applyGiftCounts = ( counts , total ) => {
const next = emptyGiftCounts ( )
if ( counts && typeof counts === 'object' ) {
for ( const k of Object . keys ( next ) ) {
next [ k ] = Number ( counts [ k ] ) || 0
}
}
Object . assign ( giftCounts , next )
giftTotal . value = Number ( total ) || Object . values ( next ) . reduce ( ( a , b ) => a + b , 0 )
}
const openGiftPanel = async ( ) => {
syncGuestNameIntoForms ( )
giftForm . name = getGuestName ( ) || giftForm . name
showGiftPanel . value = true
try {
const res = await getGiftStatus ( getClientId ( ) )
applyGiftCounts ( res . data ? . counts , res . data ? . total )
applyGiftQuota ( res . data ? . quota )
} catch { /* ignore */ }
}
const onGiftPlayStart = ( { giftType , name , count } ) => {
giftFeedRef . value ? . push ( name , giftType , count )
}
/** 关闭特效时仍按序刷左下角记录 */
const replayGiftFeedOnly = ( list ) => {
const items = Array . isArray ( list ) ? list : [ ]
if ( ! items . length ) return
let i = 0
const tick = ( ) => {
if ( i >= items . length ) return
const it = items [ i ]
i += 1
giftFeedRef . value ? . push ( it . name , it . giftType , it . count )
if ( i < items . length ) setTimeout ( tick , 900 )
}
tick ( )
}
const closeGiftPanel = ( ) => {
showGiftPanel . value = false
}
const handleSendGift = async ( giftType ) => {
if ( giftBusy . value ) return
if ( giftQuota . remain <= 0 ) {
return toast . error ( ` 赠送次数已用完,再点赞 ${ giftQuota . likes _to _next } 次可多送 1 次 ` )
}
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 )
closeGiftPanel ( )
if ( showGiftFx . value ) {
giftEffectRef . value ? . play ( { giftType , name , count : 1 } )
} else {
giftFeedRef . value ? . push ( name , giftType , 1 )
}
toast . success ( '心意已送达' )
} catch ( e ) {
toast . error ( e . message || '送礼失败' )
try {
const res = await getGiftStatus ( getClientId ( ) )
applyGiftQuota ( res . data ? . quota )
} catch { /* ignore */ }
} finally {
giftBusy . value = false
}
}
const loadLikeStatus = async ( ) => {
try {
const res = await getLikeStatus ( getClientId ( ) )
@@ -1375,6 +1609,20 @@ const loadLikeStatus = async () => {
} catch { /* ignore */ }
}
const loadGiftStatus = async ( ) => {
try {
const res = await getGiftStatus ( getClientId ( ) )
applyGiftCounts ( res . data ? . counts , res . data ? . total )
applyGiftQuota ( res . data ? . quota )
const recent = Array . isArray ( res . data ? . recent ) ? res . data . recent : [ ]
const aggregated = aggregateGiftRecent ( recent )
if ( ! aggregated . length ) return
if ( showIntro . value ) pendingGiftReplay . value = aggregated
else if ( showGiftFx . value ) nextTick ( ( ) => giftEffectRef . value ? . playSequence ( aggregated , { max : 12 } ) )
else nextTick ( ( ) => replayGiftFeedOnly ( aggregated ) )
} catch { /* ignore */ }
}
const handleLike = async ( ) => {
if ( likeBusy . value ) return
likeBusy . value = true
@@ -1385,6 +1633,12 @@ const handleLike = async () => {
clearTimeout ( likePulseTimer )
likePulseTimer = setTimeout ( ( ) => { likePulse . value = false } , 600 )
likeBurstRef . value ? . burst ( 1 )
// 点赞后刷新个人礼物额度(抽屉打开时立刻可见)
giftQuota . my _likes += 1
giftQuota . gift _allow = 1 + Math . floor ( giftQuota . my _likes / giftQuota . likes _per _gift )
giftQuota . gift _remain = Math . max ( 0 , giftQuota . gift _allow - giftQuota . gift _used )
const mod = giftQuota . my _likes % giftQuota . likes _per _gift
giftQuota . likes _to _next = mod === 0 ? giftQuota . likes _per _gift : giftQuota . likes _per _gift - mod
} catch ( e ) {
toast . error ( e . message || '点赞失败' )
} finally {
@@ -1439,6 +1693,7 @@ onMounted(async () => {
// 改为用户首次点开预览时再加载( loadAlbumPreviewImages 内部有缓存保护)
albumPreviewImages . value = [ ]
loadLikeStatus ( )
loadGiftStatus ( )
sendVisitBeacon ( )
if ( data . value . introEnabled ) showIntro . value = true
@@ -1505,9 +1760,9 @@ watch(
)
watch ( freeScrollSignature , restartAutoScroll )
watch (
( ) => [ isDrawerOpen . value , showBlessingDrawer . value , previewVisible . value , showMapOptions . value , showOpenInBrowser . value ] ,
( ) => [ isDrawerOpen . value , showBlessingDrawer . value , showGiftPanel . value , previewVisible . value , showMapOptions . value , showOpenInBrowser . value ] ,
( ) => {
if ( isDrawerOpen . value || showBlessingDrawer . value || previewVisible . value || showMapOptions . value || showOpenInBrowser . value ) {
if ( isDrawerOpen . value || showBlessingDrawer . value || showGiftPanel . value || previewVisible . value || showMapOptions . value || showOpenInBrowser . value ) {
cancelAnimationFrame ( animId )
clearBottomReturn ( )
}
@@ -1579,7 +1834,7 @@ watch(
right : 0 ;
bottom : 0 ;
width : min ( 56 vw , 240 px ) ;
height : calc ( 20 0 px + env ( safe - area - inset - bottom , 0 px ) ) ;
height : calc ( 26 0 px + env ( safe - area - inset - bottom , 0 px ) ) ;
pointer - events : auto ;
z - index : 1 ;
background : transparent ;
@@ -1637,6 +1892,83 @@ watch(
line - height : 1 ; text - align : center ; max - width : 100 % ;
}
. like - fab . liked . like - count { color : # f472b6 ; font - weight : 600 ; }
. gift - fab {
flex - direction : column ;
gap : 3 px ;
padding : 8 px 12 px ;
min - width : 52 px ;
}
. gift - fab . busy { opacity : 0.7 ; pointer - events : none ; }
. gift - icon { font - size : 16 px ; color : # a88c6b ; line - height : 1 ; }
. gift - panel {
max - height : min ( 52 dvh , 380 px ) ;
overflow - y : auto ;
padding - bottom : calc ( 12 px + env ( safe - area - inset - bottom , 0 px ) ) ;
}
. gift - panel - handle {
width : 36 px ;
height : 4 px ;
border - radius : 999 px ;
background : rgba ( 168 , 140 , 107 , 0.28 ) ;
margin : 0 auto 10 px ;
}
. gift - quota {
display : flex ;
flex - direction : column ;
gap : 2 px ;
padding : 8 px 10 px ;
border - radius : 10 px ;
background : rgba ( 168 , 140 , 107 , 0.08 ) ;
color : # 7 a6550 ;
font - size : 11 px ;
line - height : 1.45 ;
letter - spacing : 0.04 em ;
}
. gift - quota b { color : # a88c6b ; font - weight : 600 ; }
. gift - quota - sub {
font - size : 10 px ;
color : # 8 A8680 ;
letter - spacing : 0.02 em ;
}
. gift - grid {
display : grid ;
grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ;
gap : 6 px ;
}
. gift - item {
display : flex ;
flex - direction : column ;
align - items : center ;
justify - content : center ;
gap : 2 px ;
min - height : 52 px ;
padding : 6 px 4 px ;
border - radius : 10 px ;
border : 0.5 px solid rgba ( 168 , 140 , 107 , 0.22 ) ;
background : # fff ;
color : # 5 c4a35 ;
cursor : pointer ;
transition : transform 0.15 s ease , border - color 0.2 s ease , background 0.2 s ease ;
}
. gift - item : active { transform : scale ( 0.96 ) ; }
. gift - item : disabled { opacity : 0.6 ; pointer - events : none ; }
. gift - item - label {
font - size : 14 px ;
line - height : 1.15 ;
color : # a88c6b ;
letter - spacing : 0.04 em ;
text - align : center ;
}
. gift - item - count {
font - size : 10 px ;
color : # 8 A8680 ;
letter - spacing : 0.04 em ;
}
@ media ( max - width : 360 px ) {
. gift - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; }
. gift - item { min - height : 48 px ; }
. gift - item - label { font - size : 15 px ; }
}
. action - comment - row {
position : relative ;
z - index : 2 ;