- 移除聊天室右下角浮动游戏图标(占卜、百家乐、赛马、老虎机) - 用户名片按钮区:修复已婚/已好友时按钮换行问题,统一单行显示 - 婚礼红包弹窗:重设计为喜庆鲜红背景,领取按钮改为圆形米黄样式 - 新增婚礼红包恢复接口(/wedding/pending-envelopes),刷新后自动恢复领取按钮 - 修复 Alpine :style 字符串覆盖静态 style 导致圆形按钮失效的问题 - 撤职后用户等级改为根据经验值重新计算,不再无条件重置为1 - 管理员修改用户经验值后自动重算等级,有职务用户等级锁定 - 娱乐大厅钓鱼游戏按钮直接调用 startFishing() 简化操作流程 - 新增赛马、占卜、百家乐游戏及相关后端逻辑
378 lines
18 KiB
PHP
378 lines
18 KiB
PHP
{{--
|
|
文件功能:神秘占卜前台弹窗组件
|
|
|
|
聊天室内神秘占卜面板:
|
|
- 点击悬浮 FAB 打开面板
|
|
- 展示今日签文(免费次数 / 付费次数)
|
|
- 卦象摇动动画 + 签文翻转展示
|
|
- 展示近20条历史记录
|
|
--}}
|
|
|
|
{{-- ─── 神秘占卜主面板 ─── --}}
|
|
<div id="fortune-panel" x-data="fortunePanel()" x-show="show" x-cloak>
|
|
<div x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
|
|
style="position:fixed; inset:0; background:rgba(0,0,0,.55); z-index:9942;
|
|
display:flex; align-items:center; justify-content:center;">
|
|
|
|
<div
|
|
style="width:420px; max-width:96vw; border-radius:8px; overflow:hidden;
|
|
box-shadow:0 8px 32px rgba(0,0,0,.3); font-family:'Microsoft YaHei',SimSun,sans-serif; background:#fff;">
|
|
|
|
{{-- ─── 顶部标题 ─── --}}
|
|
<div
|
|
style="background:linear-gradient(135deg,#336699,#5a8fc0); padding:10px 16px; display:flex; align-items:center; gap:10px;">
|
|
<div style="font-size:14px; font-weight:bold; color:#fff; flex:1;">🔮 神秘占卜</div>
|
|
<div style="font-size:11px; color:rgba(255,255,255,.75);">
|
|
今日免费 <span x-text="freeCount"></span> 次,已用 <span x-text="freeUsed"></span> 次
|
|
</div>
|
|
<span onclick="Alpine.$data(document.getElementById('fortune-panel')).show = false"
|
|
style="cursor:pointer; font-size:18px; color:rgba(255,255,255,.8); line-height:1; transition:opacity .15s;"
|
|
onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=.8">×</span>
|
|
</div>
|
|
|
|
{{-- ─── 主体内容 ─── --}}
|
|
<div style="background:#fff; padding:14px 16px;">
|
|
|
|
{{-- Tab 切换 --}}
|
|
<div style="display:flex; gap:6px; margin-bottom:16px;">
|
|
<button @click="activeTab = 'tell'"
|
|
:style="activeTab === 'tell'
|
|
?
|
|
'flex:1; border:none; border-radius:6px; padding:8px 0; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:#336699; color:#fff; box-shadow:0 2px 6px rgba(51,102,153,.25);' :
|
|
'flex:1; border:1px solid #d0e4f5; border-radius:6px; padding:8px 0; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:#f6faff; color:#5a8fc0;'">
|
|
🔮 今日占卜
|
|
</button>
|
|
<button @click="activeTab = 'history'; loadHistory()"
|
|
:style="activeTab === 'history'
|
|
?
|
|
'flex:1; border:none; border-radius:6px; padding:8px 0; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:#336699; color:#fff; box-shadow:0 2px 6px rgba(51,102,153,.25);' :
|
|
'flex:1; border:1px solid #d0e4f5; border-radius:6px; padding:8px 0; font-size:13px; font-weight:bold; cursor:pointer; transition:all .2s; background:#f6faff; color:#5a8fc0;'">
|
|
📜 历史记录
|
|
</button>
|
|
</div>
|
|
|
|
{{-- ── 占卜 Tab ── --}}
|
|
<div x-show="activeTab === 'tell'">
|
|
|
|
{{-- 占卜动画区 --}}
|
|
<div style="text-align:center; padding:10px 0 16px;">
|
|
{{-- 未占卜:摇卦动画 --}}
|
|
<div x-show="!resultGrade">
|
|
<div style="font-size:72px; display:inline-block;"
|
|
:style="shaking ? 'animation:fortune-shake .5s ease-in-out;' :
|
|
'animation:float-orb 3s ease-in-out infinite;'">
|
|
🔮
|
|
</div>
|
|
<div style="color:#888; font-size:12px; margin-top:8px;">
|
|
<span x-show="hasFreeLeft">点击下方按钮,开启今日占卜</span>
|
|
<span x-show="!hasFreeLeft">免费次数已用完,可付费继续占卜</span>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 已占卜:展示签文 --}}
|
|
<div x-show="resultGrade" x-transition:enter="transition ease-out duration-500"
|
|
x-transition:enter-start="opacity-0 scale-75"
|
|
x-transition:enter-end="opacity-100 scale-100">
|
|
|
|
{{-- 签文等级徽章 --}}
|
|
<div style="display:inline-block; padding:4px 16px; border-radius:20px; font-weight:900;
|
|
font-size:14px; letter-spacing:2px; margin-bottom:12px; color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.3);"
|
|
:style="'background:' + resultColor + '; border:1px solid rgba(0,0,0,.1);'"
|
|
x-text="resultLabel"></div>
|
|
|
|
{{-- 签文卡片(白底+左侧彩色装饰条) --}}
|
|
<div style="border-radius:6px; padding:12px 16px; margin:0 4px;
|
|
border:1px solid #d0e4f5; background:#f6faff; position:relative;"
|
|
:style="'border-left:4px solid ' + resultColor + ';'">
|
|
<div style="color:#225588; font-size:13px; line-height:1.8; text-align:center; font-style:italic;"
|
|
x-text="'「' + resultText + '」'"></div>
|
|
</div>
|
|
|
|
{{-- 当日加成 --}}
|
|
<div x-show="resultBuff"
|
|
style="margin:10px 8px 0; padding:8px 14px; border-radius:10px;
|
|
background:#f0f6ff; color:#336699; font-size:12px; text-align:left; border:1px solid #d0e4f5;"
|
|
x-text="resultBuff"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 已有今日签文(只展示最新的,可再次占卜) --}}
|
|
<div x-show="todayLatest && !resultGrade"
|
|
style="border-radius:6px; padding:10px 12px; background:#f6faff;
|
|
border:1px solid #d0e4f5; margin-bottom:10px;">
|
|
<div style="color:#336699; font-size:11px; margin-bottom:6px; font-weight:bold;">今日最新签文</div>
|
|
<div style="display:flex; align-items:center; gap:6px;">
|
|
{{-- 签级标签:改为左彩色竖条 + 圆角徽章文字,不用纯色背景 --}}
|
|
<span
|
|
style="padding:2px 10px; border-radius:20px; font-size:11px; font-weight:bold; border:1.5px solid;"
|
|
:style="todayLatest
|
|
?
|
|
'color:' + todayLatest.grade_color + '; border-color:' + todayLatest.grade_color +
|
|
'; background: transparent;' :
|
|
'display:none'"
|
|
x-text="todayLatest?.grade_label"></span>
|
|
<span style="color:#225588; font-size:12px; flex:1;" x-text="todayLatest?.text"></span>
|
|
</div>
|
|
<div x-show="todayLatest?.buff_desc" style="color:#888; font-size:11px; margin-top:3px;"
|
|
x-text="todayLatest?.buff_desc"></div>
|
|
</div>
|
|
|
|
{{-- 占卜按钮 --}}
|
|
<div x-show="!resultGrade">
|
|
<button @click="doFortune()" :disabled="loading"
|
|
:style="loading
|
|
?
|
|
'display:block; width:100%; border:none; border-radius:12px; padding:12px 0; font-size:14px; font-weight:bold; cursor:not-allowed; transition:all .2s; background:#e0e8f0; color:#99a8b8; box-shadow:none;' :
|
|
hasFreeLeft ?
|
|
'display:block; width:100%; border:none; border-radius:12px; padding:12px 0; font-size:14px; font-weight:bold; cursor:pointer; transition:all .2s; background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff; box-shadow:0 4px 12px rgba(51,102,153,.25);' :
|
|
'display:block; width:100%; border:none; border-radius:12px; padding:13px 0; font-size:15px; font-weight:bold; cursor:pointer; transition:all .2s; background:linear-gradient(135deg,#1e4d8c,#336699); color:#fff; box-shadow:0 4px 14px rgba(30,77,140,.3); letter-spacing:0.5px;'">
|
|
<span
|
|
x-text="loading ? '占卜中…' : (hasFreeLeft ? '🔮 免费占卜' : '🔮 付费占卜(' + extraCost + ' 金币)')"></span>
|
|
</button>
|
|
</div>
|
|
|
|
{{-- 再占一卦按钮 --}}
|
|
<div x-show="resultGrade" style="display:flex; gap:10px;">
|
|
<button @click="resultGrade = ''; resultText = ''; resultBuff = null"
|
|
style="flex:1; border:1px solid #b0d0ee; border-radius:12px; padding:12px 0;
|
|
background:#f0f6ff; color:#336699;
|
|
font-size:14px; font-weight:bold; cursor:pointer; transition:all .15s;"
|
|
onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">
|
|
↩ 返回
|
|
</button>
|
|
<button @click="resultGrade = ''; resultText = ''; resultBuff = null; doFortune()"
|
|
:disabled="loading"
|
|
style="flex:2; border:none; border-radius:12px; padding:12px 0;
|
|
background:linear-gradient(135deg,#336699,#5a8fc0); color:#fff; box-shadow:0 4px 12px rgba(51,102,153,.2);
|
|
font-size:14px; font-weight:bold; cursor:pointer; transition:all .15s;"
|
|
x-text="hasFreeLeft ? '🔮 再占一卦(免费)' : '🔮 再占一卦(' + extraCost + ' 金币)'"></button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ── 历史 Tab ── --}}
|
|
<div x-show="activeTab === 'history'" style="display:none;">
|
|
<div style="max-height:280px; overflow-y:auto; display:flex; flex-direction:column; gap:6px;">
|
|
<template x-for="(log, i) in historyLogs" :key="i">
|
|
<div
|
|
style="border-radius:6px; padding:8px 10px; background:#f6faff;
|
|
border:1px solid #d0e4f5;">
|
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:4px;">
|
|
<span
|
|
style="padding:1px 8px; border-radius:20px; font-size:10px; font-weight:bold; color:#fff;"
|
|
:style="'background:' + log.grade_color + ';'" x-text="log.grade_label"></span>
|
|
<span style="color:#aaa; font-size:10px;" x-text="log.date + ' ' + log.time"></span>
|
|
<span x-show="log.cost > 0" style="color:#b45309; font-size:10px; margin-left:auto;"
|
|
x-text="'花费 ' + log.cost + '金'"></span>
|
|
</div>
|
|
<div style="color:#225588; font-size:12px; line-height:1.6;" x-text="log.text"></div>
|
|
<div x-show="log.buff_desc" style="color:#888; font-size:11px; margin-top:3px;"
|
|
x-text="log.buff_desc"></div>
|
|
</div>
|
|
</template>
|
|
<div x-show="historyLogs.length === 0"
|
|
style="text-align:center; color:#aaa; font-size:12px; padding:20px 0;">
|
|
尚无占卜记录
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ─── 底部关闭 ─── --}}
|
|
<div
|
|
style="background:#fff; border-top:1px solid #d0e4f5; padding:14px 16px; display:flex; justify-content:center;">
|
|
<button @click="show = false"
|
|
style="padding:10px 48px; min-width:140px; background:#f0f6ff; border:1px solid #b0d0ee; border-radius:12px;
|
|
font-size:14px; font-weight:bold; color:#336699; cursor:pointer; transition:all .15s;"
|
|
onmouseover="this.style.background='#ddeeff'" onmouseout="this.style.background='#f0f6ff'">
|
|
关闭
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<style>
|
|
@keyframes float-orb {
|
|
|
|
0%,
|
|
100% {
|
|
transform: translateY(0) scale(1);
|
|
filter: drop-shadow(0 0 12px rgba(168, 85, 247, .6));
|
|
}
|
|
|
|
50% {
|
|
transform: translateY(-8px) scale(1.05);
|
|
filter: drop-shadow(0 0 24px rgba(168, 85, 247, .9));
|
|
}
|
|
}
|
|
|
|
@keyframes fortune-shake {
|
|
|
|
0%,
|
|
100% {
|
|
transform: rotate(0deg) scale(1.1);
|
|
}
|
|
|
|
20% {
|
|
transform: rotate(-15deg) scale(1.15);
|
|
}
|
|
|
|
40% {
|
|
transform: rotate(12deg) scale(1.15);
|
|
}
|
|
|
|
60% {
|
|
transform: rotate(-8deg) scale(1.12);
|
|
}
|
|
|
|
80% {
|
|
transform: rotate(5deg) scale(1.1);
|
|
}
|
|
}
|
|
|
|
@keyframes pulse-fortune {
|
|
|
|
0%,
|
|
100% {
|
|
box-shadow: 0 4px 20px rgba(168, 85, 247, .5);
|
|
}
|
|
|
|
50% {
|
|
box-shadow: 0 4px 30px rgba(168, 85, 247, .9);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
/**
|
|
* 神秘占卜主面板 Alpine 组件
|
|
*/
|
|
function fortunePanel() {
|
|
return {
|
|
show: false,
|
|
activeTab: 'tell',
|
|
loading: false,
|
|
shaking: false,
|
|
|
|
// 游戏配置
|
|
freeCount: 1,
|
|
freeUsed: 0,
|
|
hasFreeLeft: true,
|
|
extraCost: 500,
|
|
|
|
// 今日最新签文(若已占卜过)
|
|
todayLatest: null,
|
|
|
|
// 本次占卜结果
|
|
resultGrade: '',
|
|
resultLabel: '',
|
|
resultColor: '#a855f7',
|
|
resultText: '',
|
|
resultBuff: null,
|
|
|
|
// 历史记录
|
|
historyLogs: [],
|
|
|
|
/**
|
|
* 加载今日占卜状态
|
|
*/
|
|
async loadTodayStatus() {
|
|
try {
|
|
const res = await fetch('/fortune/today');
|
|
const data = await res.json();
|
|
if (!data.enabled) return;
|
|
|
|
this.freeCount = data.free_count || 1;
|
|
this.freeUsed = data.free_used || 0;
|
|
this.hasFreeLeft = data.has_free_left ?? true;
|
|
this.extraCost = data.extra_cost || 500;
|
|
this.todayLatest = data.latest || null;
|
|
} catch {}
|
|
},
|
|
|
|
/**
|
|
* 执行占卜
|
|
*/
|
|
async doFortune() {
|
|
if (this.loading) return;
|
|
this.loading = true;
|
|
this.shaking = true;
|
|
|
|
// 摇卦动画
|
|
await new Promise(r => setTimeout(r, 600));
|
|
this.shaking = false;
|
|
|
|
try {
|
|
const res = await fetch('/fortune/tell', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content,
|
|
},
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
this.resultGrade = data.grade;
|
|
this.resultLabel = data.grade_label;
|
|
this.resultColor = data.grade_color;
|
|
this.resultText = data.text;
|
|
this.resultBuff = data.buff_desc;
|
|
|
|
// 更新今日状态
|
|
if (data.is_free) {
|
|
this.freeUsed++;
|
|
this.hasFreeLeft = this.freeUsed < this.freeCount;
|
|
}
|
|
} else {
|
|
window.chatDialog?.alert(data.message || '占卜失败', '提示', '#ef4444');
|
|
}
|
|
} catch {
|
|
window.chatDialog?.alert('网络异常,请稍后重试。', '错误', '#ef4444');
|
|
}
|
|
|
|
this.loading = false;
|
|
},
|
|
|
|
/**
|
|
* 加载历史记录
|
|
*/
|
|
async loadHistory() {
|
|
if (this.historyLogs.length > 0) return; // 已加载过则不重复请求
|
|
try {
|
|
const res = await fetch('/fortune/history');
|
|
const data = await res.json();
|
|
this.historyLogs = data.history || [];
|
|
} catch {}
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 页面加载时:检查游戏是否开启,若开启则初始化面板数据 */
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
try {
|
|
const res = await fetch('/fortune/today');
|
|
const data = await res.json();
|
|
if (data.enabled) {
|
|
|
|
const panel = document.getElementById('fortune-panel');
|
|
if (panel) {
|
|
const pd = Alpine.$data(panel);
|
|
pd.freeCount = data.free_count || 1;
|
|
pd.freeUsed = data.free_used || 0;
|
|
pd.hasFreeLeft = data.has_free_left ?? true;
|
|
pd.extraCost = data.extra_cost || 500;
|
|
pd.todayLatest = data.latest || null;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('[神秘占卜] 初始化失败', e);
|
|
}
|
|
});
|
|
</script>
|