Add VIP presence themes and custom greetings

This commit is contained in:
2026-04-11 15:44:30 +08:00
parent 9fb7710079
commit 4eba9dfc12
21 changed files with 1126 additions and 49 deletions
+126
View File
@@ -9,3 +9,129 @@
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
}
.vip-presence-banner {
position: fixed;
inset: 24px 24px auto 24px;
z-index: 100000;
display: flex;
justify-content: center;
pointer-events: none;
animation: vip-presence-enter .55s ease-out both;
}
.vip-presence-banner.is-leaving {
animation: vip-presence-leave .65s ease-in both;
}
.vip-presence-banner__glow {
position: absolute;
inset: 14px auto auto 50%;
width: min(72vw, 720px);
height: 88px;
border-radius: 9999px;
filter: blur(34px);
transform: translateX(-50%);
opacity: .95;
}
.vip-presence-banner__card {
position: relative;
width: min(92vw, 760px);
border: 1px solid rgba(255, 255, 255, .35);
border-radius: 28px;
padding: 20px 24px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(15, 23, 42, .35);
}
.vip-presence-banner__card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(120deg, transparent 0%, rgba(255,255,255,.16) 38%, transparent 72%);
transform: translateX(-120%);
animation: vip-presence-shine 2.6s ease-in-out infinite;
}
.vip-presence-banner__meta {
position: relative;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.vip-presence-banner__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 16px;
background: rgba(15, 23, 42, .22);
backdrop-filter: blur(10px);
font-size: 24px;
}
.vip-presence-banner__level {
font-size: 12px;
font-weight: 800;
letter-spacing: .12em;
text-transform: uppercase;
color: rgba(255, 255, 255, .92);
}
.vip-presence-banner__type {
font-size: 11px;
font-weight: 700;
padding: 6px 10px;
border-radius: 9999px;
color: #0f172a;
background: rgba(255, 255, 255, .72);
}
.vip-presence-banner__text {
position: relative;
margin-top: 14px;
font-size: clamp(16px, 2vw, 24px);
font-weight: 800;
line-height: 1.5;
text-wrap: balance;
text-shadow: 0 2px 18px rgba(15, 23, 42, .22);
}
@keyframes vip-presence-enter {
from {
opacity: 0;
transform: translateY(-22px) scale(.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes vip-presence-leave {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-16px) scale(.98);
}
}
@keyframes vip-presence-shine {
0% {
transform: translateX(-120%);
}
55%,
100% {
transform: translateX(140%);
}
}
+73
View File
@@ -27,6 +27,11 @@
duration_days: 30,
join_templates: '',
leave_templates: '',
join_effect: 'none',
leave_effect: 'none',
join_banner_style: 'aurora',
leave_banner_style: 'farewell',
allow_custom_messages: true,
},
openCreate() {
@@ -42,6 +47,11 @@
duration_days: 30,
join_templates: '',
leave_templates: '',
join_effect: 'none',
leave_effect: 'none',
join_banner_style: 'aurora',
leave_banner_style: 'farewell',
allow_custom_messages: true,
};
this.showForm = true;
},
@@ -59,6 +69,11 @@
duration_days: level.duration_days,
join_templates: level.join_templates_text,
leave_templates: level.leave_templates_text,
join_effect: level.join_effect,
leave_effect: level.leave_effect,
join_banner_style: level.join_banner_style,
leave_banner_style: level.leave_banner_style,
allow_custom_messages: level.allow_custom_messages,
};
this.showForm = true;
}
@@ -112,6 +127,18 @@
<span class="text-gray-500">当前会员</span>
<span class="font-bold text-indigo-600">{{ $level->users()->count() }} </span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">进场特效</span>
<span class="font-bold text-sky-600">{{ $effectOptions[$level->joinEffectKey()] ?? '无特效' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">离场特效</span>
<span class="font-bold text-violet-600">{{ $effectOptions[$level->leaveEffectKey()] ?? '无特效' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">允许自定义</span>
<span class="font-bold {{ $level->allow_custom_messages ? 'text-emerald-600' : 'text-gray-400' }}">{{ $level->allow_custom_messages ? '允许' : '关闭' }}</span>
</div>
</div>
</div>
@@ -127,6 +154,11 @@
sort_order: {{ $level->sort_order }},
price: {{ $level->price }},
duration_days: {{ $level->duration_days }},
join_effect: '{{ $level->joinEffectKey() }}',
leave_effect: '{{ $level->leaveEffectKey() }}',
join_banner_style: '{{ $level->joinBannerStyleKey() }}',
leave_banner_style: '{{ $level->leaveBannerStyleKey() }}',
allow_custom_messages: {{ $level->allow_custom_messages ? 'true' : 'false' }},
join_templates_text: `{{ str_replace('`', '', implode("\n", $level->join_templates_array)) }}`,
leave_templates_text: `{{ str_replace('`', '', implode("\n", $level->leave_templates_array)) }}`,
requestUrl: '{{ route('admin.vip.update', $level->id) }}'
@@ -229,6 +261,47 @@
class="w-full border rounded-md p-2 text-sm"></textarea>
</div>
<div class="mt-4 grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">入场特效</label>
<select name="join_effect" x-model="form.join_effect" class="w-full border rounded-md p-2 text-sm">
@foreach ($effectOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">离场特效</label>
<select name="leave_effect" x-model="form.leave_effect" class="w-full border rounded-md p-2 text-sm">
@foreach ($effectOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">入场横幅风格</label>
<select name="join_banner_style" x-model="form.join_banner_style" class="w-full border rounded-md p-2 text-sm">
@foreach ($bannerStyleOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">离场横幅风格</label>
<select name="leave_banner_style" x-model="form.leave_banner_style" class="w-full border rounded-md p-2 text-sm">
@foreach ($bannerStyleOptions as $value => $label)
<option value="{{ $value }}">{{ $label }}</option>
@endforeach
</select>
</div>
</div>
<label class="mt-4 flex items-center gap-3 rounded-xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-900">
<input type="checkbox" name="allow_custom_messages" value="1" x-model="form.allow_custom_messages"
class="rounded border-amber-300 text-amber-600 focus:ring-amber-400">
允许该会员等级用户在会员中心自定义欢迎语和离开语
</label>
<div class="flex justify-end space-x-3 pt-4 mt-4 border-t">
<button type="button" @click="showForm = false"
class="px-4 py-2 border rounded font-medium text-gray-600 hover:bg-gray-50">取消</button>
+16 -3
View File
@@ -232,16 +232,18 @@
});
</script>
@endif
@if (!empty($newbieEffect) || !empty($weekEffect))
@if (!empty($newbieEffect) || !empty($weekEffect) || !empty($initialPresenceTheme['presence_effect']))
<script>
/**
* 延迟1秒待页面完成初始化后,自动播放进房附带的特效
* 优先级:如果有新人礼包特效,优先播放新人大礼包;如果没有,再播放周卡特效
* 延迟1秒待页面完成初始化后,自动播放进房附带的特效
* 优先级:新人礼包特效 -> 会员专属进场特效 -> 周卡特效
*/
setTimeout(() => {
if (typeof EffectManager !== 'undefined') {
@if (!empty($newbieEffect))
EffectManager.play('{{ $newbieEffect }}');
@elseif (!empty($initialPresenceTheme['presence_effect']))
EffectManager.play('{{ $initialPresenceTheme['presence_effect'] }}');
@elseif (!empty($weekEffect))
EffectManager.play('{{ $weekEffect }}');
@endif
@@ -249,6 +251,17 @@
}, 1000);
</script>
@endif
@if (!empty($initialPresenceTheme))
<script>
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (typeof window.showVipPresenceBanner === 'function') {
window.showVipPresenceBanner(@json($initialPresenceTheme));
}
}, 700);
});
</script>
@endif
{{-- 页面初始加载时,若存在挂起的求婚 / 离婚请求,则弹窗 --}}
@if (!empty($pendingProposal) || !empty($pendingDivorce))
@@ -68,6 +68,89 @@
let autoScroll = true;
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
/**
* 转义会员横幅文本,避免横幅层被注入 HTML。
*/
function escapePresenceText(text) {
return escapeHtml(String(text ?? '')).replace(/\n/g, '<br>');
}
/**
* 根据不同的会员横幅风格返回渐变与光影配置。
*/
function getVipPresenceStyleConfig(style, color) {
const fallback = color || '#f59e0b';
const map = {
aurora: {
gradient: `linear-gradient(135deg, ${fallback}, #fde68a, #fff7ed)`,
glow: `${fallback}66`,
accent: '#fff7ed',
},
storm: {
gradient: `linear-gradient(135deg, #1e3a8a, ${fallback}, #dbeafe)`,
glow: '#60a5fa88',
accent: '#dbeafe',
},
royal: {
gradient: `linear-gradient(135deg, #111827, ${fallback}, #fbbf24)`,
glow: '#fbbf2488',
accent: '#fef3c7',
},
cosmic: {
gradient: `linear-gradient(135deg, #312e81, ${fallback}, #ec4899)`,
glow: '#c084fc99',
accent: '#f5d0fe',
},
farewell: {
gradient: `linear-gradient(135deg, #334155, ${fallback}, #94a3b8)`,
glow: '#cbd5e188',
accent: '#f8fafc',
},
};
return map[style] || map.aurora;
}
/**
* 显示会员进退场豪华横幅。
*/
function showVipPresenceBanner(payload) {
if (!payload || !payload.presence_text) {
return;
}
const existing = document.getElementById('vip-presence-banner');
if (existing) {
existing.remove();
}
const styleConfig = getVipPresenceStyleConfig(payload.presence_banner_style, payload.presence_color);
const banner = document.createElement('div');
banner.id = 'vip-presence-banner';
banner.className = 'vip-presence-banner';
banner.innerHTML = `
<div class="vip-presence-banner__glow" style="background:${styleConfig.glow};"></div>
<div class="vip-presence-banner__card" style="background:${styleConfig.gradient}; border-color:${payload.presence_color || '#fff'};">
<div class="vip-presence-banner__meta">
<span class="vip-presence-banner__icon">${escapeHtml(payload.presence_icon || '👑')}</span>
<span class="vip-presence-banner__level">${escapeHtml(payload.presence_level_name || '尊贵会员')}</span>
<span class="vip-presence-banner__type">${payload.presence_type === 'leave' ? '离场提示' : '闪耀登场'}</span>
</div>
<div class="vip-presence-banner__text" style="color:${styleConfig.accent};">${escapePresenceText(payload.presence_text)}</div>
</div>
`;
document.body.appendChild(banner);
setTimeout(() => {
banner.classList.add('is-leaving');
setTimeout(() => banner.remove(), 700);
}, 4200);
}
window.showVipPresenceBanner = showVipPresenceBanner;
// ── Tab 切换 ──────────────────────────────────────
let _roomsRefreshTimer = null;
@@ -539,6 +622,31 @@
html = `${iconImg} ${parsedContent}`;
}
// 会员专属进退场播报:更醒目的卡片化样式,同时由外层额外触发豪华横幅。
else if (msg.action === 'vip_presence') {
div.style.cssText =
'background:linear-gradient(135deg, rgba(15,23,42,.96), rgba(30,41,59,.9)); border:1px solid rgba(255,255,255,.14); border-radius:12px; padding:10px 12px; margin:6px 0; box-shadow:0 10px 26px rgba(15,23,42,.22);';
const icon = escapeHtml(msg.presence_icon || '👑');
const levelName = escapeHtml(msg.presence_level_name || '尊贵会员');
const typeLabel = msg.presence_type === 'leave' ? '华丽离场' : '荣耀入场';
const accent = msg.presence_color || '#f59e0b';
const safeText = escapePresenceText(msg.presence_text || '');
html = `
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:42px;height:42px;border-radius:14px;background:radial-gradient(circle at top, ${accent}, #111827);display:flex;align-items:center;justify-content:center;font-size:22px;box-shadow:0 0 22px ${accent}55;">${icon}</div>
<div style="min-width:0;flex:1;">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<span style="font-size:12px;font-weight:800;letter-spacing:.08em;color:${accent};text-transform:uppercase;">${typeLabel}</span>
<span style="font-size:12px;color:#e2e8f0;">${levelName}</span>
<span style="font-size:11px;color:#94a3b8;">(${timeStr})</span>
</div>
<div style="margin-top:4px;font-size:14px;line-height:1.6;color:#f8fafc;">${safeText}</div>
</div>
</div>
`;
timeStrOverride = true;
}
// 贾妖语 —— 蓝色左边框渐变样式,比 系统公告 低调
else if (msg.action === '欢迎') {
div.style.cssText =
@@ -759,6 +867,11 @@
return;
}
appendMessage(msg);
if (msg.action === 'vip_presence') {
showVipPresenceBanner(msg);
}
// 若消息携带 toast_notification 字段且当前用户是接收者,弹右下角小卡片
if (msg.toast_notification && msg.to_user === window.chatContext.username) {
const t = msg.toast_notification;
+112
View File
@@ -139,6 +139,118 @@
</div>
</section>
<section class="bg-white rounded-[2rem] border border-slate-200 shadow-sm overflow-hidden">
<div class="bg-[radial-gradient(circle_at_top_left,_rgba(251,191,36,0.24),_transparent_42%),linear-gradient(135deg,#0f172a,#1e293b,#334155)] px-6 py-6 text-white">
<div class="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div>
<p class="text-xs font-bold uppercase tracking-[0.28em] text-amber-300/90">会员进退场主题</p>
<h3 class="mt-2 text-2xl font-black">欢迎语、离开语与专属入场仪式</h3>
<p class="mt-2 max-w-2xl text-sm text-slate-200">
这里可以查看当前会员档位的专属特效和横幅风格;若当前档位允许自定义,你还可以设置自己的欢迎语和离开语。
</p>
</div>
@if ($user->isVip())
<div class="rounded-2xl border border-white/10 bg-white/10 px-4 py-3 text-sm text-slate-100">
当前档位:<span class="font-extrabold" style="color: {{ $user->vipLevel?->color ?: '#fef3c7' }}">{{ $user->vipName() }}</span>
</div>
@endif
</div>
</div>
<div class="grid grid-cols-1 gap-6 p-6 lg:grid-cols-[1.2fr,0.8fr]">
<div class="space-y-5">
<div class="rounded-3xl border border-slate-200 bg-slate-50 p-5">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-900 text-2xl text-white shadow-lg">
{{ $user->vipLevel?->icon ?: '✨' }}
</div>
<div>
<p class="text-sm font-bold text-slate-500">当前主题预览</p>
<h4 class="text-xl font-black text-slate-900">{{ $user->vipLevel?->name ?? '普通用户' }}</h4>
</div>
</div>
<div class="mt-5 grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="rounded-2xl border border-slate-200 bg-white p-4">
<p class="text-xs font-bold uppercase tracking-[0.18em] text-slate-400">入场特效</p>
<p class="mt-2 text-lg font-extrabold text-slate-900">{{ $effectOptions[$user->vipLevel?->joinEffectKey() ?? 'none'] ?? '无特效' }}</p>
<p class="mt-2 text-sm text-slate-500">横幅风格:{{ $bannerStyleOptions[$user->vipLevel?->joinBannerStyleKey() ?? 'aurora'] ?? '鎏光星幕' }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-4">
<p class="text-xs font-bold uppercase tracking-[0.18em] text-slate-400">离场特效</p>
<p class="mt-2 text-lg font-extrabold text-slate-900">{{ $effectOptions[$user->vipLevel?->leaveEffectKey() ?? 'none'] ?? '无特效' }}</p>
<p class="mt-2 text-sm text-slate-500">横幅风格:{{ $bannerStyleOptions[$user->vipLevel?->leaveBannerStyleKey() ?? 'farewell'] ?? '告别暮光' }}</p>
</div>
</div>
</div>
<div class="rounded-3xl border border-amber-100 bg-amber-50/70 p-5">
<p class="text-sm font-bold text-amber-700">等级默认语句</p>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs font-bold uppercase tracking-[0.18em] text-amber-500">默认欢迎语</p>
<p class="mt-3 text-sm leading-7 text-slate-700">
{{ $user->vipLevel?->join_templates_array[0] ?? '当前档位尚未配置默认欢迎语。' }}
</p>
</div>
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs font-bold uppercase tracking-[0.18em] text-amber-500">默认离开语</p>
<p class="mt-3 text-sm leading-7 text-slate-700">
{{ $user->vipLevel?->leave_templates_array[0] ?? '当前档位尚未配置默认离开语。' }}
</p>
</div>
</div>
</div>
</div>
<div class="rounded-3xl border border-slate-200 bg-white p-5 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-sm font-bold text-slate-500">我的个性化设置</p>
<h4 class="mt-1 text-xl font-black text-slate-900">自定义欢迎语与离开语</h4>
</div>
@if ($user->canCustomizeVipPresence())
<span class="inline-flex rounded-full bg-emerald-100 px-3 py-1 text-xs font-bold text-emerald-700">已开启</span>
@else
<span class="inline-flex rounded-full bg-gray-100 px-3 py-1 text-xs font-bold text-gray-500">未开放</span>
@endif
</div>
@if ($user->canCustomizeVipPresence())
<form action="{{ route('vip.center.presence.update') }}" method="POST" class="mt-5 space-y-4">
@csrf
@method('PUT')
<div>
<label class="mb-2 block text-sm font-bold text-slate-700">我的欢迎语</label>
<textarea name="custom_join_message" rows="4" maxlength="255"
class="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-amber-400 focus:bg-white"
placeholder="例:{username} 乘着星舰闪耀登场,今晚全场高光属于 TA!">{{ old('custom_join_message', $user->custom_join_message) }}</textarea>
<p class="mt-2 text-xs text-slate-500">支持使用 <span class="font-mono text-slate-700">{username}</span> 占位符自动替换成你的昵称。</p>
</div>
<div>
<label class="mb-2 block text-sm font-bold text-slate-700">我的离开语</label>
<textarea name="custom_leave_message" rows="4" maxlength="255"
class="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-amber-400 focus:bg-white"
placeholder="例:{username} 留下一道流光背影,优雅谢幕,我们下次再见。">{{ old('custom_leave_message', $user->custom_leave_message) }}</textarea>
</div>
<button type="submit"
class="w-full rounded-2xl bg-slate-900 px-5 py-3 text-sm font-bold text-white transition hover:bg-slate-800">
保存我的专属语句
</button>
</form>
@elseif ($user->isVip())
<div class="mt-5 rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-4 py-5 text-sm leading-7 text-slate-500">
当前会员档位暂未开放个人自定义功能,不过你仍会自动使用本等级配置的专属欢迎语、离开语和华丽特效。
</div>
@else
<div class="mt-5 rounded-2xl border border-dashed border-amber-200 bg-amber-50 px-4 py-5 text-sm leading-7 text-amber-800">
开通会员后,这里会解锁对应等级的专属进退场主题;若等级允许,还能设置你自己的欢迎语和离开语。
</div>
@endif
</div>
</div>
</section>
<section class="bg-white rounded-3xl border border-gray-100 shadow-sm p-6">
<div class="flex items-center justify-between gap-4 mb-5">
<div>