新增每日签到与补签卡功能

This commit is contained in:
2026-04-24 22:47:27 +08:00
parent 34356a26ae
commit be9fc09d9d
46 changed files with 3934 additions and 55 deletions
@@ -80,6 +80,10 @@
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.autoact.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🎲 随机事件' !!}
</a>
<a href="{{ route('admin.sign-in-rules.index') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.sign-in-rules.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '✅ 签到奖励' !!}
</a>
<a href="{{ route('admin.vip.index') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.vip.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '👑 VIP 会员等级' !!}
@@ -20,6 +20,7 @@
'one_time' => ['label' => '一次性道具', 'color' => 'bg-yellow-100 text-yellow-700'],
'ring' => ['label' => '求婚戒指', 'color' => 'bg-rose-100 text-rose-700'],
'auto_fishing' => ['label' => '自动钓鱼卡', 'color' => 'bg-emerald-100 text-emerald-700'],
'sign_repair' => ['label' => '签到补签卡', 'color' => 'bg-teal-100 text-teal-700'],
];
$isSuperAdmin = Auth::id() === 1;
@endphp
@@ -283,6 +284,7 @@
<option value="one_time">one_time 一次性道具</option>
<option value="ring">ring 求婚戒指</option>
<option value="auto_fishing">auto_fishing 自动钓鱼卡</option>
<option value="sign_repair">sign_repair 签到补签卡</option>
</select>
</div>
</div>
@@ -0,0 +1,169 @@
@extends('admin.layouts.app')
@section('title', '签到奖励管理')
@section('content')
<div class="space-y-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 flex justify-between items-center">
<div>
<h2 class="text-lg font-bold text-gray-800"> 签到奖励管理</h2>
<p class="text-xs text-gray-500 mt-1">按连续签到天数配置金币、经验、魅力和专属身份。</p>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-100">
<tr>
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">连续天数</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">金币</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">经验</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">魅力</th>
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">身份</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">有效期</th>
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">状态</th>
<th class="px-4 py-3 text-right text-xs font-bold text-gray-500 uppercase">操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-50">
@forelse ($rules as $rule)
<tr id="rule-row-{{ $rule->id }}" class="{{ $rule->is_enabled ? '' : 'opacity-50' }}">
<td class="px-4 py-3 font-bold text-gray-800"> {{ $rule->streak_days }} </td>
<td class="px-4 py-3 text-center text-amber-600 font-mono">+{{ $rule->gold_reward }}</td>
<td class="px-4 py-3 text-center text-emerald-600 font-mono">+{{ $rule->exp_reward }}</td>
<td class="px-4 py-3 text-center text-pink-600 font-mono">+{{ $rule->charm_reward }}</td>
<td class="px-4 py-3">
@if ($rule->identity_badge_name)
<span class="inline-flex items-center gap-1 rounded-full border px-2 py-1 text-xs font-bold"
style="color: {{ $rule->identity_badge_color ?: '#0f766e' }}; border-color: #99f6e4; background: #f0fdfa;">
<span>{{ $rule->identity_badge_icon ?: '✅' }}</span>
<span>{{ $rule->identity_badge_name }}</span>
</span>
<div class="mt-1 text-[11px] text-gray-400">{{ $rule->identity_badge_code }}</div>
@else
<span class="text-xs text-gray-400">未配置身份</span>
@endif
</td>
<td class="px-4 py-3 text-center text-xs text-gray-600">
{{ $rule->identity_duration_days > 0 ? $rule->identity_duration_days . ' 天' : '永久' }}
</td>
<td class="px-4 py-3 text-center">
<button onclick="toggleSignInRule({{ $rule->id }})" id="toggle-rule-{{ $rule->id }}"
class="px-2 py-1 rounded-full text-xs font-bold transition {{ $rule->is_enabled ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : 'bg-gray-100 text-gray-500 hover:bg-gray-200' }}">
{{ $rule->is_enabled ? '启用' : '停用' }}
</button>
</td>
<td class="px-4 py-3 text-right">
<button type="button" onclick="openEditRule({{ $rule->id }})"
class="px-3 py-1 bg-indigo-50 text-indigo-700 rounded text-xs font-bold hover:bg-indigo-100 transition mr-1">
编辑
</button>
<form action="{{ route('admin.sign-in-rules.destroy', $rule) }}" method="POST"
class="inline" onsubmit="return confirm('确定删除第 {{ $rule->streak_days }} 天签到规则?')">
@csrf
@method('DELETE')
<button type="submit"
class="px-3 py-1 bg-red-50 text-red-600 rounded text-xs font-bold hover:bg-red-100 transition">
删除
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-8 text-center text-sm text-gray-400">暂无签到奖励规则。</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-5 border-b border-gray-100 bg-gray-50">
<h3 class="font-bold text-gray-700 text-sm"> 新增签到档位</h3>
</div>
<form action="{{ route('admin.sign-in-rules.store') }}" method="POST" class="p-5">
@csrf
@include('admin.sign-in-rules.partials.form-fields', ['rule' => null])
<div class="mt-4 flex items-center gap-4">
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm shadow-sm">
💾 添加规则
</button>
<label class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" name="is_enabled" value="1" checked class="rounded">
立即启用
</label>
</div>
</form>
</div>
</div>
<div id="edit-rule-modal" class="hidden fixed inset-0 bg-black/40 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-xl w-full max-w-3xl shadow-2xl">
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-bold text-gray-800">✏️ 编辑签到规则</h3>
<button type="button" onclick="closeEditRule()" class="text-gray-400 hover:text-gray-600 text-xl"></button>
</div>
<form id="edit-rule-form" method="POST" class="p-5">
@csrf
@method('PUT')
@include('admin.sign-in-rules.partials.form-fields', ['rule' => null, 'prefix' => 'edit-'])
<div class="mt-5 flex items-center gap-4">
<button type="submit"
class="px-5 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition text-sm">
💾 保存修改
</button>
<label class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" name="is_enabled" id="edit-is-enabled" value="1" class="rounded">
启用此规则
</label>
<button type="button" onclick="closeEditRule()"
class="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-bold hover:bg-gray-200 transition text-sm">
取消
</button>
</div>
</form>
</div>
</div>
<script>
const signInRules = @json($rules->keyBy('id'));
function openEditRule(id) {
const rule = signInRules[id];
if (!rule) return;
document.getElementById('edit-rule-form').action = `/admin/sign-in-rules/${id}`;
['streak_days', 'gold_reward', 'exp_reward', 'charm_reward', 'identity_badge_code',
'identity_badge_name', 'identity_badge_icon', 'identity_badge_color',
'identity_duration_days', 'sort_order'
].forEach((field) => {
const input = document.getElementById(`edit-${field}`);
if (input) input.value = rule[field] ?? '';
});
document.getElementById('edit-is-enabled').checked = Boolean(rule.is_enabled);
document.getElementById('edit-rule-modal').classList.remove('hidden');
}
function closeEditRule() {
document.getElementById('edit-rule-modal').classList.add('hidden');
}
async function toggleSignInRule(id) {
const response = await fetch(`/admin/sign-in-rules/${id}/toggle`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
});
const data = await response.json();
if (!response.ok || !data.ok) {
alert(data.message || '切换失败');
return;
}
window.location.reload();
}
</script>
@endsection
@@ -0,0 +1,66 @@
@php
$prefix = $prefix ?? '';
@endphp
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">连续天数</label>
<input type="number" id="{{ $prefix }}streak_days" name="streak_days" min="1" required
value="{{ old('streak_days') }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">金币奖励</label>
<input type="number" id="{{ $prefix }}gold_reward" name="gold_reward" min="0" required
value="{{ old('gold_reward', 0) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">经验奖励</label>
<input type="number" id="{{ $prefix }}exp_reward" name="exp_reward" min="0" required
value="{{ old('exp_reward', 0) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">魅力奖励</label>
<input type="number" id="{{ $prefix }}charm_reward" name="charm_reward" min="0" required
value="{{ old('charm_reward', 0) }}"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">身份编码</label>
<input type="text" id="{{ $prefix }}identity_badge_code" name="identity_badge_code"
value="{{ old('identity_badge_code') }}" placeholder="sign_in_7"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">身份名称</label>
<input type="text" id="{{ $prefix }}identity_badge_name" name="identity_badge_name"
value="{{ old('identity_badge_name') }}" placeholder="七日星辉"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">身份图标</label>
<input type="text" id="{{ $prefix }}identity_badge_icon" name="identity_badge_icon"
value="{{ old('identity_badge_icon') }}" placeholder="🔥"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">身份颜色</label>
<input type="text" id="{{ $prefix }}identity_badge_color" name="identity_badge_color"
value="{{ old('identity_badge_color', '#0f766e') }}" placeholder="#0f766e"
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">身份有效天数</label>
<input type="number" id="{{ $prefix }}identity_duration_days" name="identity_duration_days" min="0"
value="{{ old('identity_duration_days', 0) }}" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">排序</label>
<input type="number" id="{{ $prefix }}sort_order" name="sort_order" min="0"
value="{{ old('sort_order', 0) }}" required
class="w-full border border-gray-300 rounded-lg p-2 text-sm focus:border-indigo-400">
</div>
</div>
+5
View File
@@ -95,6 +95,10 @@
refreshAllUrl: "{{ route('command.refresh_all') }}",
chatPreferencesUrl: "{{ route('user.update_chat_preferences') }}",
dailyStatusUpdateUrl: "{{ route('user.update_daily_status') }}",
dailySignInStatusUrl: @json(\Illuminate\Support\Facades\Route::has('daily-sign-in.status') ? route('daily-sign-in.status') : null),
dailySignInCalendarUrl: @json(\Illuminate\Support\Facades\Route::has('daily-sign-in.calendar') ? route('daily-sign-in.calendar') : null),
dailySignInClaimUrl: @json(\Illuminate\Support\Facades\Route::has('daily-sign-in.claim') ? route('daily-sign-in.claim') : null),
dailySignInMakeupUrl: @json(\Illuminate\Support\Facades\Route::has('daily-sign-in.makeup') ? route('daily-sign-in.makeup') : null),
userJjb: {{ (int) $user->jjb }}, // 当前用户金币(求婚前金额预检查用)
myGold: {{ (int) $user->jjb }}, // 赠金币面板显示余额用(赠送成功后前端更新)
chatPreferences: @json($user->chat_preferences ?? []),
@@ -207,6 +211,7 @@
@include('chat.partials.marriage-modals')
{{-- 节日福利弹窗 --}}
@include('chat.partials.holiday-modal')
@include('chat.partials.daily-sign-in-modal')
{{-- ═══════════ 游戏面板(partials/games/ 子目录,各自独立,包含 CSS + HTML + JS ═══════════ --}}
@include('chat.partials.games.baccarat-panel')
@@ -0,0 +1,338 @@
{{--
文件功能:每日签到日历弹窗。
展示当月签到状态、今日签到入口、补签卡数量,并支持快速购买补签卡。
--}}
<style>
.daily-sign-modal-overlay {
position: fixed;
inset: 0;
z-index: 2600;
display: none;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.48);
padding: 8px;
}
.daily-sign-modal {
width: min(520px, 96vw);
max-height: min(610px, 96vh);
overflow: hidden;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 22px 60px rgba(15, 23, 42, 0.28);
border: 1px solid rgba(15, 118, 110, 0.16);
display: flex;
flex-direction: column;
}
.daily-sign-modal-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 10px;
border-bottom: 1px solid #e2e8f0;
background: linear-gradient(90deg, #f0fdfa, #ffffff);
}
.daily-sign-modal-title {
font-size: 14px;
font-weight: 800;
color: #0f766e;
}
.daily-sign-modal-close {
border: none;
background: #e2e8f0;
color: #334155;
border-radius: 7px;
width: 24px;
height: 24px;
cursor: pointer;
font-size: 15px;
line-height: 24px;
}
.daily-sign-modal-body {
padding: 6px 10px 8px;
overflow-y: auto;
}
.daily-sign-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
margin-bottom: 6px;
}
.daily-sign-summary-card {
border: 1px solid #dbeafe;
background: #f8fafc;
border-radius: 8px;
padding: 5px 8px;
min-width: 0;
}
.daily-sign-summary-card strong {
display: block;
color: #0f172a;
font-size: 12px;
}
.daily-sign-summary-card span {
color: #64748b;
font-size: 11px;
}
.daily-sign-actions {
display: flex;
gap: 5px;
margin-bottom: 6px;
}
.daily-sign-action-btn {
flex: 1;
border: none;
border-radius: 8px;
padding: 5px 8px;
font-weight: 800;
cursor: pointer;
color: #ffffff;
background: #0f766e;
font-size: 11px;
}
.daily-sign-action-btn.secondary {
background: #f59e0b;
}
.daily-sign-month-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin: 3px 0 4px;
}
.daily-sign-month-bar button {
border: 1px solid #cbd5e1;
background: #ffffff;
border-radius: 7px;
padding: 3px 7px;
cursor: pointer;
color: #334155;
font-size: 11px;
}
.daily-sign-month-label {
font-size: 12px;
font-weight: 800;
color: #0f172a;
}
.daily-sign-weekdays,
.daily-sign-grid {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 3px;
}
.daily-sign-weekday {
text-align: center;
color: #64748b;
font-size: 10px;
font-weight: 700;
}
.daily-sign-day {
min-height: 32px;
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #ffffff;
padding: 2px;
text-align: center;
font-size: 9px;
color: #334155;
position: relative;
overflow: hidden;
}
.daily-sign-day.blank {
border-color: transparent;
background: transparent;
}
.daily-sign-day.signed {
border-color: #99f6e4;
background: #f0fdfa;
color: #0f766e;
}
.daily-sign-day.missed {
border-color: #fed7aa;
background: #fff7ed;
color: #9a3412;
cursor: pointer;
}
.daily-sign-day.today {
box-shadow: inset 0 0 0 2px #14b8a6;
}
.daily-sign-day.future {
background: #f8fafc;
color: #94a3b8;
}
.daily-sign-day .day-num {
display: block;
font-weight: 800;
font-size: 11px;
}
.daily-sign-day .day-state {
display: block;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.daily-sign-rewards {
margin-top: 6px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
overflow: hidden;
}
.daily-sign-rewards-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 4px 8px;
border-bottom: 1px solid #e2e8f0;
color: #0f172a;
font-size: 11px;
font-weight: 800;
}
.daily-sign-rewards-list {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px;
padding: 5px 8px;
}
.daily-sign-reward-card {
border: 1px solid #dbeafe;
border-radius: 6px;
background: #ffffff;
padding: 4px;
min-width: 0;
}
.daily-sign-reward-card.active {
border-color: #14b8a6;
background: #f0fdfa;
box-shadow: inset 0 0 0 1px #14b8a6;
}
.daily-sign-reward-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 4px;
color: #0f766e;
font-size: 9px;
font-weight: 800;
white-space: nowrap;
}
.daily-sign-reward-name {
margin-top: 1px;
color: #334155;
font-size: 9px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.daily-sign-reward-desc {
margin-top: 1px;
color: #64748b;
font-size: 8px;
line-height: 1.15;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 520px) {
.daily-sign-summary {
grid-template-columns: 1fr 1fr;
}
.daily-sign-actions {
flex-direction: row;
}
.daily-sign-day {
min-height: 30px;
}
.daily-sign-rewards-list {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
</style>
<div id="daily-sign-modal" class="daily-sign-modal-overlay" onclick="if(event.target === this) closeDailySignInModal()">
<div class="daily-sign-modal">
<div class="daily-sign-modal-head">
<div class="daily-sign-modal-title"> 每日签到</div>
<button type="button" class="daily-sign-modal-close" onclick="closeDailySignInModal()">×</button>
</div>
<div class="daily-sign-modal-body">
<div class="daily-sign-summary">
<div class="daily-sign-summary-card">
<strong id="daily-sign-streak">连续 0 </strong>
<span id="daily-sign-preview">加载中...</span>
</div>
<div class="daily-sign-summary-card">
<strong id="daily-sign-card-count">补签卡 0 </strong>
<span id="daily-sign-card-price">可在商店购买</span>
</div>
</div>
<div class="daily-sign-actions">
<button type="button" id="daily-sign-claim-btn" class="daily-sign-action-btn" onclick="claimDailySignInFromModal()">今日签到</button>
<button type="button" id="daily-sign-buy-card-btn" class="daily-sign-action-btn secondary" onclick="buyDailySignRepairCard()">购买补签卡</button>
</div>
<div class="daily-sign-month-bar">
<button type="button" onclick="loadDailySignInCalendar(window.dailySignInState?.prevMonth)">上月</button>
<div id="daily-sign-month-label" class="daily-sign-month-label">本月</div>
<button type="button" onclick="loadDailySignInCalendar(window.dailySignInState?.nextMonth)">下月</button>
</div>
<div class="daily-sign-weekdays">
<div class="daily-sign-weekday"></div>
<div class="daily-sign-weekday"></div>
<div class="daily-sign-weekday"></div>
<div class="daily-sign-weekday"></div>
<div class="daily-sign-weekday"></div>
<div class="daily-sign-weekday"></div>
<div class="daily-sign-weekday"></div>
</div>
<div id="daily-sign-calendar-grid" class="daily-sign-grid" style="margin-top:4px;"></div>
<div class="daily-sign-rewards">
<div class="daily-sign-rewards-head">
<span>连续奖励目标</span>
<span id="daily-sign-reward-progress" style="font-size:11px;color:#64748b;">当前 0 </span>
</div>
<div id="daily-sign-rewards-list" class="daily-sign-rewards-list"></div>
</div>
</div>
</div>
</div>
@@ -186,6 +186,8 @@ $welcomeMessages = [
</div>
<div style="font-size:10px;color:#4338ca;padding:10px 2px 8px;">快捷入口</div>
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;">
<button type="button" onclick="closeFeatureMenu();quickDailySignIn()"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;"> 签到</button>
<button type="button" onclick="runFeatureShortcut('shop')"
style="font-size:11px;padding:6px 8px;background:#fff;color:#334155;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer;">🛍 商店</button>
<button type="button" onclick="runFeatureShortcut('vip')"
@@ -31,6 +31,7 @@
<button class="mobile-drawer-close" onclick="closeMobileDrawer()"></button>
</div>
<div class="mobile-drawer-body">
<div class="mobile-tool-btn" onclick="closeMobileDrawer();quickDailySignIn();"><br>签到</div>
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openShopModal();">🛒<br>商店</div>
<div class="mobile-tool-btn" onclick="closeMobileDrawer();openVipModal();">👑<br>会员</div>
<div class="mobile-tool-btn" onclick="closeMobileDrawer();saveExp();">💾<br>存点</div>
@@ -1204,7 +1204,7 @@ async function generateWechatBindCode() {
{
label: '🎭 道具',
desc: '',
type: 'one_time'
type: 'tools'
},
];
@@ -1212,7 +1212,7 @@ async function generateWechatBindCode() {
list.innerHTML = '';
groups.forEach(g => {
const items = data.items.filter(i => i.type === g.type);
const items = data.items.filter(i => g.type === 'tools' ? ['one_time', 'sign_repair'].includes(i.type) : i.type === g.type);
if (!items.length) return;
// 分组标题(独占一整行)
@@ -1235,6 +1235,9 @@ async function generateWechatBindCode() {
const effName = effItem ? effItem.name : effKey;
groupSuffix =
` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#16a34a;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">✅ 已激活:${effName}</span>`;
} else if (g.type === 'tools' && (data.sign_repair_card_count || 0) > 0) {
groupSuffix =
` <span style="display:inline-block;margin-left:8px;padding:1px 8px;background:#0f766e;color:#fff;border-radius:10px;font-size:10px;font-weight:bold;vertical-align:middle;">🎫 可用 ${data.sign_repair_card_count} 张</span>`;
}
header.innerHTML = `${g.label}${groupSuffix}${g.desc ? ` <span>${g.desc}</span>` : ''}`;
list.appendChild(header);
@@ -1295,10 +1298,15 @@ async function generateWechatBindCode() {
btn.className = 'shop-btn';
btn.innerHTML = `💰 ${Number(item.price).toLocaleString()}`;
btn.onclick = async () => {
let quantity = 1;
if (item.type === 'sign_repair') {
quantity = await window.promptSignRepairQuantity?.(item);
if (quantity === null || quantity === undefined) return;
}
const confirmMsg =
`确认花费 💰 ${Number(item.price).toLocaleString()} 金币购买\n【${item.name}】吗?`;
`确认花费 💰 ${Number(item.price * quantity).toLocaleString()} 金币购买\n【${item.name}】${quantity > 1 ? ' × ' + quantity : ''} 吗?`;
const ok = await window.chatDialog.confirm(confirmMsg, '确认购买');
if (ok) buyItem(item.id, item.name, item.price, 'all', '');
if (ok) buyItem(item.id, item.name, item.price, 'all', '', quantity);
};
}
card.appendChild(btn);
@@ -1341,7 +1349,7 @@ async function generateWechatBindCode() {
};
/** 购买商品(最终执行) */
window.buyItem = function(itemId, name, price, recipient, message) {
window.buyItem = function(itemId, name, price, recipient, message, quantity = 1) {
const roomId = window.chatContext?.roomId ?? 0;
fetch('{{ route('shop.buy') }}', {
method: 'POST',
@@ -1354,6 +1362,7 @@ async function generateWechatBindCode() {
item_id: itemId,
recipient: recipient || 'all',
message: message || '',
quantity: quantity || 1,
room_id: roomId,
}),
})
+601 -18
View File
@@ -423,6 +423,544 @@
renderUserList();
}
/**
* 从服务端响应中提取最新金币余额。
*
* @param {Record<string, any>} data 接口响应数据
* @returns {number|null}
*/
function resolveDailySignInGoldBalance(data) {
const candidates = [
data?.data?.user?.jjb,
data?.data?.user?.gold,
data?.data?.presence?.jjb,
data?.data?.presence?.gold,
data?.data?.my_jjb,
data?.data?.new_jjb,
data?.data?.balance,
data?.my_jjb,
data?.new_jjb,
data?.balance,
];
for (const candidate of candidates) {
const amount = Number(candidate);
if (Number.isFinite(amount)) {
return amount;
}
}
return null;
}
/**
* 从签到响应中提取当前用户最新在线载荷。
*
* @param {Record<string, any>} data 接口响应数据
* @returns {Record<string, any>|null}
*/
function resolveDailySignInPresencePayload(data) {
const candidates = [
data?.data?.presence,
data?.data?.online_user,
data?.data?.onlineUser,
data?.data?.user_payload,
data?.data?.userPayload,
data?.data?.user,
data?.presence,
data?.online_user,
data?.onlineUser,
];
return candidates.find(payload => payload && typeof payload === 'object') || null;
}
/**
* 从签到响应中提取签到身份字段。
*
* @param {Record<string, any>} data 接口响应数据
* @returns {Record<string, any>}
*/
function resolveDailySignInIdentityPayload(data) {
const identity = data?.data?.identity || data?.data?.sign_identity || data?.identity || data?.sign_identity;
if (!identity || typeof identity !== 'object') {
return {};
}
return {
sign_identity_key: identity.key ?? identity.sign_identity_key ?? identity.code ?? '',
sign_identity_label: identity.label ?? identity.name ?? identity.sign_identity_label ?? '',
sign_identity_icon: identity.icon ?? identity.sign_identity_icon ?? '',
sign_identity_color: identity.color ?? identity.sign_identity_color ?? undefined,
sign_identity_bg_color: identity.bg_color ?? identity.background_color ?? identity.sign_identity_bg_color ?? undefined,
sign_identity_border_color: identity.border_color ?? identity.sign_identity_border_color ?? undefined,
};
}
/**
* 将签到成功结果同步到金币余额与在线名单。
*
* @param {Record<string, any>} data 接口响应数据
*/
function applyDailySignInResult(data) {
const balance = resolveDailySignInGoldBalance(data);
const payload = resolveDailySignInPresencePayload(data);
const identityPayload = resolveDailySignInIdentityPayload(data);
const username = window.chatContext?.username;
if (balance !== null && window.chatContext) {
window.chatContext.userJjb = balance;
window.chatContext.myGold = balance;
}
if (username) {
hydrateOnlineUserPayload(username, {
...(payload || {}),
...identityPayload,
username,
});
}
renderUserList();
}
window.dailySignInState = {
month: null,
prevMonth: null,
nextMonth: null,
repairCardItem: null,
repairCardCount: 0,
rewardRules: [],
status: null,
};
/**
* 打开每日签到日历弹窗。
*/
window.openDailySignInModal = async function openDailySignInModal() {
const modal = document.getElementById('daily-sign-modal');
if (!window.chatContext?.dailySignInCalendarUrl || !modal) {
window.chatDialog?.alert('签到入口暂未开放,请稍后再试。', '提示', '#f59e0b');
return;
}
modal.style.display = 'flex';
await Promise.all([
loadDailySignInStatus(),
loadDailySignInCalendar(window.dailySignInState.month),
]);
};
/**
* 关闭每日签到日历弹窗。
*/
window.closeDailySignInModal = function closeDailySignInModal() {
const modal = document.getElementById('daily-sign-modal');
if (modal) {
modal.style.display = 'none';
}
};
/**
* 快速签到入口:打开日历,让用户能看到当月签到和补签状态。
*/
window.quickDailySignIn = async function quickDailySignIn() {
await window.openDailySignInModal();
};
/**
* 拉取今日签到状态。
*/
async function loadDailySignInStatus() {
const statusUrl = window.chatContext?.dailySignInStatusUrl;
if (!statusUrl) {
return;
}
const response = await fetch(statusUrl, {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
},
});
const data = await response.json();
if (!response.ok || data?.status === 'error') {
throw new Error(data?.message || '签到状态加载失败');
}
window.dailySignInState.status = data.data || {};
renderDailySignInStatus();
}
/**
* 拉取并渲染指定月份签到日历。
*
* @param {string|null|undefined} month 月份 YYYY-MM
*/
window.loadDailySignInCalendar = async function loadDailySignInCalendar(month) {
const calendarUrl = window.chatContext?.dailySignInCalendarUrl;
if (!calendarUrl) {
return;
}
const url = new URL(calendarUrl, window.location.origin);
if (month) {
url.searchParams.set('month', month);
}
const response = await fetch(url.toString(), {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
},
});
const data = await response.json();
if (!response.ok || data?.status === 'error') {
throw new Error(data?.message || '签到日历加载失败');
}
const payload = data.data || {};
window.dailySignInState.month = payload.month || month || null;
window.dailySignInState.prevMonth = payload.prev_month || null;
window.dailySignInState.nextMonth = payload.next_month || null;
window.dailySignInState.repairCardItem = payload.sign_repair_card_item || null;
window.dailySignInState.repairCardCount = Number(payload.makeup_card_count || 0);
window.dailySignInState.rewardRules = Array.isArray(payload.reward_rules) ? payload.reward_rules : [];
renderDailySignInCalendar(payload);
renderDailySignInStatus();
renderDailySignInRewardRules();
};
/**
* 渲染签到状态摘要。
*/
function renderDailySignInStatus() {
const status = window.dailySignInState.status || {};
const streakEl = document.getElementById('daily-sign-streak');
const previewEl = document.getElementById('daily-sign-preview');
const cardCountEl = document.getElementById('daily-sign-card-count');
const cardPriceEl = document.getElementById('daily-sign-card-price');
const claimBtn = document.getElementById('daily-sign-claim-btn');
const buyBtn = document.getElementById('daily-sign-buy-card-btn');
const cardItem = window.dailySignInState.repairCardItem;
if (streakEl) {
streakEl.textContent = `连续 ${Number(status.current_streak_days || 0)} 天`;
}
if (previewEl) {
const rule = status.preview_rule || {};
const parts = [];
if (Number(rule.gold_reward || 0) > 0) parts.push(`${rule.gold_reward} 金币`);
if (Number(rule.exp_reward || 0) > 0) parts.push(`${rule.exp_reward} 经验`);
if (Number(rule.charm_reward || 0) > 0) parts.push(`${rule.charm_reward} 魅力`);
previewEl.textContent = status.signed_today ? '今日已签到' : `今日可领:${parts.join(' + ') || '签到奖励'}`;
}
if (cardCountEl) {
cardCountEl.textContent = `补签卡 ${window.dailySignInState.repairCardCount || 0} 张`;
}
if (cardPriceEl) {
cardPriceEl.textContent = cardItem ? `${cardItem.icon || '🗓️'} ${cardItem.name}${Number(cardItem.price || 0).toLocaleString()} 金币` : '补签卡暂未上架';
}
if (claimBtn) {
claimBtn.disabled = !!status.signed_today;
claimBtn.textContent = status.signed_today ? '今日已签到' : '今日签到';
claimBtn.style.opacity = status.signed_today ? '0.55' : '1';
claimBtn.style.cursor = status.signed_today ? 'not-allowed' : 'pointer';
}
if (buyBtn) {
buyBtn.disabled = !cardItem?.id;
buyBtn.style.opacity = cardItem?.id ? '1' : '0.55';
buyBtn.style.cursor = cardItem?.id ? 'pointer' : 'not-allowed';
}
}
/**
* 渲染签到月历。
*
* @param {Record<string, any>} payload 日历响应数据
*/
function renderDailySignInCalendar(payload) {
const grid = document.getElementById('daily-sign-calendar-grid');
const label = document.getElementById('daily-sign-month-label');
if (!grid) {
return;
}
if (label) {
label.textContent = payload.month_label || payload.month || '本月';
}
const days = Array.isArray(payload.days) ? payload.days : [];
grid.innerHTML = '';
const firstWeekday = Number(days[0]?.weekday || 0);
for (let i = 0; i < firstWeekday; i += 1) {
const blank = document.createElement('div');
blank.className = 'daily-sign-day blank';
grid.appendChild(blank);
}
days.forEach(day => {
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'daily-sign-day';
if (day.signed) cell.classList.add('signed');
if (day.can_makeup) cell.classList.add('missed');
if (day.is_today) cell.classList.add('today');
if (day.is_future) cell.classList.add('future');
const stateText = day.signed
? `${day.is_makeup ? '补签' : '已签'} ${day.streak_days || ''}天`
: (day.is_future ? '未到' : (day.is_today ? '今天' : '漏签'));
cell.innerHTML = `<span class="day-num">${day.day}</span><span class="day-state">${escapeHtml(stateText)}</span>`;
cell.title = day.reward_text || stateText;
if (day.can_makeup) {
cell.onclick = () => makeupDailySignIn(day.date);
}
grid.appendChild(cell);
});
}
/**
* 渲染连续签到奖励目标列表。
*/
function renderDailySignInRewardRules() {
const list = document.getElementById('daily-sign-rewards-list');
const progress = document.getElementById('daily-sign-reward-progress');
if (!list) {
return;
}
const currentDays = Number(window.dailySignInState.status?.current_streak_days || 0);
const rules = window.dailySignInState.rewardRules || [];
if (progress) {
progress.textContent = `当前 ${currentDays} 天`;
}
if (!rules.length) {
list.innerHTML = '<div style="font-size:12px;color:#94a3b8;padding:4px;">暂无奖励规则</div>';
return;
}
list.innerHTML = rules.map(rule => {
const streakDays = Number(rule.streak_days || 0);
const parts = [];
if (Number(rule.gold_reward || 0) > 0) parts.push(`${rule.gold_reward}金`);
if (Number(rule.exp_reward || 0) > 0) parts.push(`${rule.exp_reward}经验`);
if (Number(rule.charm_reward || 0) > 0) parts.push(`${rule.charm_reward}魅力`);
const icon = escapeHtml(rule.identity_badge_icon || '✅');
const name = escapeHtml(rule.identity_badge_name || '签到奖励');
const color = escapeHtml(rule.identity_badge_color || '#0f766e');
const activeClass = currentDays >= streakDays ? ' active' : '';
const distanceText = currentDays >= streakDays ? '已达成' : `还差 ${Math.max(streakDays - currentDays, 0)} 天`;
const rewardText = escapeHtml(parts.join(' + ') || '签到记录');
return `
<div class="daily-sign-reward-card${activeClass}" title="${name} · ${rewardText} · ${escapeHtml(distanceText)}">
<div class="daily-sign-reward-title">
<span> ${streakDays} </span>
<span style="color:${color};">${icon}</span>
</div>
<div class="daily-sign-reward-name">${name}</div>
<div class="daily-sign-reward-desc">${rewardText}</div>
</div>
`;
}).join('');
}
/**
* 在日历弹窗中领取今日签到。
*/
window.claimDailySignInFromModal = async function claimDailySignInFromModal() {
const claimUrl = window.chatContext?.dailySignInClaimUrl;
if (!claimUrl) {
window.chatDialog?.alert('签到入口暂未开放,请稍后再试。', '提示', '#f59e0b');
return;
}
try {
const response = await fetch(claimUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
room_id: window.chatContext?.roomId ?? null,
}),
});
const data = await response.json();
if (!response.ok || data?.status === 'error' || data?.ok === false) {
throw new Error(data?.message || '签到失败');
}
applyDailySignInResult(data);
await Promise.all([
loadDailySignInStatus(),
loadDailySignInCalendar(window.dailySignInState.month),
]);
renderDailySignInRewardRules();
window.chatToast?.show({
title: '签到成功',
message: data?.message || '今日签到奖励已到账。',
icon: data?.data?.sign_identity_icon || data?.data?.identity?.icon || '✅',
color: '#16a34a',
duration: 3200,
});
} catch (error) {
window.chatDialog?.alert(error.message || '签到失败,请稍后重试。', '签到失败', '#cc4444');
}
};
/**
* 使用补签卡补签指定日期。
*
* @param {string} targetDate 目标日期 YYYY-MM-DD
*/
async function makeupDailySignIn(targetDate) {
const makeupUrl = window.chatContext?.dailySignInMakeupUrl;
if (!makeupUrl) {
return;
}
const ok = await window.chatDialog?.confirm(`确认使用 1 张补签卡补签 ${targetDate} 吗?`, '确认补签');
if (!ok) {
return;
}
try {
const response = await fetch(makeupUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
target_date: targetDate,
room_id: window.chatContext?.roomId ?? null,
}),
});
const data = await response.json();
if (!response.ok || data?.status === 'error' || data?.ok === false) {
const firstError = data?.errors ? Object.values(data.errors).flat()[0] : null;
throw new Error(firstError || data?.message || '补签失败');
}
applyDailySignInResult(data);
await Promise.all([
loadDailySignInStatus(),
loadDailySignInCalendar(window.dailySignInState.month),
]);
renderDailySignInRewardRules();
window.chatToast?.show({
title: '补签成功',
message: data?.message || '补签已完成。',
icon: '🗓️',
color: '#0f766e',
duration: 3200,
});
} catch (error) {
window.chatDialog?.alert(error.message || '补签失败,请稍后重试。', '补签失败', '#cc4444');
}
}
/**
* 询问补签卡购买数量。
*
* @param {Record<string, any>} item 补签卡商品
* @returns {Promise<number|null>}
*/
window.promptSignRepairQuantity = async function promptSignRepairQuantity(item) {
const unitPrice = Number(item?.price || 0);
const ruleText = item?.description || '补签卡只能补签本月漏掉的未签到日期,不能补签上月或更早日期。';
const promptPromise = window.chatDialog?.prompt(
`请输入要购买的补签卡数量(1-99):\n单价 ${unitPrice.toLocaleString()} 金币\n说明:${ruleText}`,
'1',
'购买补签卡',
'#0f766e'
);
const inputEl = document.getElementById('global-dialog-input');
const previousInputStyle = inputEl?.getAttribute('style') || '';
if (inputEl) {
inputEl.style.minHeight = '40px';
inputEl.style.height = '40px';
inputEl.style.resize = 'none';
inputEl.style.overflow = 'hidden';
}
const rawQuantity = await promptPromise;
if (inputEl) {
inputEl.setAttribute('style', previousInputStyle);
}
if (rawQuantity === null || rawQuantity === undefined) {
return null;
}
const quantity = Number.parseInt(String(rawQuantity).trim(), 10);
if (!Number.isInteger(quantity) || quantity < 1 || quantity > 99) {
window.chatDialog?.alert('购买数量必须是 1 到 99 之间的整数。', '数量不正确', '#cc4444');
return null;
}
return quantity;
};
/**
* 在签到弹窗内快速购买补签卡。
*/
window.buyDailySignRepairCard = async function buyDailySignRepairCard() {
const item = window.dailySignInState.repairCardItem;
if (!item?.id) {
window.chatDialog?.alert('补签卡暂未上架。', '提示', '#f59e0b');
return;
}
const quantity = await window.promptSignRepairQuantity(item);
if (quantity === null) {
return;
}
const totalPrice = Number(item.price || 0) * quantity;
const ok = await window.chatDialog?.confirm(`确认花费 ${totalPrice.toLocaleString()} 金币购买【${item.name}】× ${quantity} 吗?\n说明:补签卡只能补签本月未签到日期。`, '购买补签卡');
if (!ok) {
return;
}
if (typeof window.buyItem === 'function') {
window.buyItem(item.id, item.name, item.price, 'all', '', quantity);
setTimeout(() => {
loadDailySignInCalendar(window.dailySignInState.month);
loadDailySignInStatus();
}, 900);
return;
}
window.openShopModal?.();
};
/**
* 设置新的当日状态。
*
@@ -1512,7 +2050,7 @@
}
/**
* 构建原有徽标(职务 / 管理员 / VIP
* 构建职务 / 管理员徽标
*
* @param {Record<string, any>} user 用户在线载荷
* @param {string} username 用户名
@@ -1531,15 +2069,25 @@
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px;" data-instant-tooltip="最高统帅">🎖️</span>`;
}
if (user.vip_icon) {
const vipColor = user.vip_color || '#f59e0b';
const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));
return '';
}
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
/**
* 构建用户 VIP 徽标。
*
* @param {Record<string, any>} user 用户在线载荷
* @returns {string}
*/
function buildUserVipBadgeHtml(user) {
if (!user.vip_icon) {
return '';
}
return '';
const vipColor = user.vip_color || '#f59e0b';
const safeVipTitle = escapeHtml(String(user.vip_name || 'VIP'));
const safeVipIcon = escapeHtml(String(user.vip_icon || '👑'));
return `<span class="user-badge-icon" style="font-size:12px; margin-left:2px; color:${vipColor};" data-instant-tooltip="${safeVipTitle}">${safeVipIcon}</span>`;
}
/**
@@ -1559,30 +2107,65 @@
const safeTooltip = escapeHtml(`${status.group} · ${status.label}`);
return `
<span style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca;font-size:11px;line-height:18px;vertical-align:middle;"
<span class="user-badge-icon" style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;max-width:78px;overflow:hidden;white-space:nowrap;border-radius:999px;background:#eef2ff;border:1px solid #c7d2fe;color:#4338ca;font-size:11px;line-height:18px;vertical-align:middle;"
data-instant-tooltip="${safeTooltip}">
<span style="font-size:11px;line-height:1;">${safeIcon}</span>
<span style="line-height:1;">${safeLabel}</span>
<span style="font-size:11px;line-height:1;flex-shrink:0;">${safeIcon}</span>
<span style="line-height:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${safeLabel}</span>
</span>
`;
}
/**
* 3 秒节奏在原有徽标与状态徽标之间切换
* 构建签到身份徽标
*
* @param {Record<string, any>} user 用户在线载荷
* @returns {string}
*/
function buildUserSignIdentityBadgeHtml(user) {
const identityKey = String(user.sign_identity_key ?? user.sign_identity ?? '');
const identityLabel = String(user.sign_identity_label ?? user.sign_identity_name ?? '');
const identityIcon = String(user.sign_identity_icon ?? '');
if (!identityKey || !identityLabel || !identityIcon) {
return '';
}
const color = String(user.sign_identity_color || '#0f766e');
const bgColor = String(user.sign_identity_bg_color || '#ccfbf1');
const borderColor = String(user.sign_identity_border_color || '#5eead4');
const safeIcon = escapeHtml(identityIcon);
const safeLabel = escapeHtml(identityLabel);
const safeTooltip = escapeHtml(`签到 · ${identityLabel}`);
return `
<span class="user-badge-icon" style="display:inline-flex;align-items:center;gap:3px;margin-left:4px;padding:0 6px;height:18px;max-width:78px;overflow:hidden;white-space:nowrap;border-radius:999px;background:${bgColor};border:1px solid ${borderColor};color:${color};font-size:11px;line-height:18px;vertical-align:middle;"
data-instant-tooltip="${safeTooltip}">
<span style="font-size:11px;line-height:1;flex-shrink:0;">${safeIcon}</span>
<span style="line-height:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${safeLabel}</span>
</span>
`;
}
/**
* 3 秒节奏在签到身份、状态、职务/管理、VIP 徽标之间轮换。
*
* @param {Record<string, any>} user 用户在线载荷
* @param {string} username 用户名
* @returns {string}
*/
function buildUserBadgeHtml(user, username) {
const statusBadge = buildUserStatusBadgeHtml(user);
const primaryBadge = buildUserPrimaryBadgeHtml(user, username);
const badges = [
buildUserSignIdentityBadgeHtml(user),
buildUserStatusBadgeHtml(user),
buildUserPrimaryBadgeHtml(user, username),
buildUserVipBadgeHtml(user),
].filter(Boolean);
if (statusBadge && primaryBadge) {
return userBadgeRotationTick % 2 === 0 ? statusBadge : primaryBadge;
if (badges.length === 0) {
return '';
}
return statusBadge || primaryBadge;
return badges[userBadgeRotationTick % badges.length];
}
/**
@@ -1603,9 +2186,9 @@
});
}
// 名单中“状态 / 原徽标”双轨展示时,每 3 秒只刷新徽标槽位,不重建头像行。
// 名单中多种徽标轮换展示时,每 3 秒只刷新徽标槽位,不重建头像行。
window.setInterval(() => {
userBadgeRotationTick = (userBadgeRotationTick + 1) % 2;
userBadgeRotationTick = (userBadgeRotationTick + 1) % 4;
refreshRenderedUserBadges();
syncDailyStatusUi();
}, 3000);
@@ -344,7 +344,7 @@
{
label: '🎭 道具',
desc: '',
type: 'one_time'
type: 'tools'
},
];
@@ -352,7 +352,7 @@
itemsEl.innerHTML = '';
groups.forEach(g => {
const items = data.items.filter(i => i.type === g.type);
const items = data.items.filter(i => g.type === 'tools' ? ['one_time', 'sign_repair'].includes(i.type) : i.type === g.type);
if (!items.length) return;
const section = document.createElement('div');
@@ -417,7 +417,7 @@
} else {
btn.className = 'shop-btn';
btn.innerHTML = `💰 ${Number(item.price).toLocaleString()}`;
btn.onclick = () => buyItem(item.id, item.name, item.price);
btn.onclick = () => buyItem(item.id, item.name, item.price, item.type);
}
row.appendChild(btn);
card.appendChild(row);
@@ -457,14 +457,23 @@
}
/** 购买商品 */
window.buyItem = function(itemId, name, price) {
// 使用全局弹窗替代原生 confirm(),通过 .then() 注册回调确保事件正确触发
window.chatDialog.confirm(
`确定花费 ${Number(price).toLocaleString()} 金币购买【${name}】吗?`,
'确认购买',
'#336699'
).then(ok => {
if (!ok) return;
window.buyItem = async function(itemId, name, price, typeOrRecipient = '', message = '', presetQuantity = null) {
const knownTypes = ['instant', 'duration', 'one_time', 'ring', 'auto_fishing', 'sign_repair'];
const type = knownTypes.includes(typeOrRecipient) ? typeOrRecipient : '';
const recipient = type === '' ? (typeOrRecipient || 'all') : 'all';
let quantity = Number(presetQuantity || 1);
if (type === 'sign_repair') {
quantity = await window.promptSignRepairQuantity?.({
name,
price,
description: '补签卡只能补签本月漏掉的未签到日期,不能补签上月或更早日期。',
});
if (quantity === null || quantity === undefined) return;
}
const signRepairNotice = type === 'sign_repair' ? '\n说明:补签卡只能补签本月未签到日期。' : '';
const submitPurchase = () => {
fetch('{{ route('shop.buy') }}', {
method: 'POST',
headers: {
@@ -473,7 +482,10 @@
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
item_id: itemId
item_id: itemId,
recipient,
message: message || '',
quantity: quantity || 1
}),
})
.then(r => r.json())
@@ -494,6 +506,21 @@
}
})
.catch(() => showShopToast('⚠ 网络异常,请重试', false));
};
if (presetQuantity !== null) {
submitPurchase();
return;
}
// 使用全局弹窗替代原生 confirm(),通过 .then() 注册回调确保事件正确触发
window.chatDialog.confirm(
`确定花费 ${Number(price * quantity).toLocaleString()} 金币购买【${name}】${quantity > 1 ? ' × ' + quantity : ''} 吗?${signRepairNotice}`,
'确认购买',
'#336699'
).then(ok => {
if (!ok) return;
submitPurchase();
});
};
@@ -692,6 +692,20 @@
</span>
</div>
<div x-show="userInfo.sign_in?.streak_days || userInfo.sign_in?.identity"
style="margin-top: 6px;">
<span style="display: inline-flex;align-items: center;gap: 6px;padding: 2px 10px;
border-radius: 999px;white-space: nowrap;width: fit-content;max-width: 100%;
font-size: 11px;font-weight: bold;background: #f0fdfa;border: 1px solid #99f6e4;"
:style="'color:' + (userInfo.sign_in?.identity?.color || '#0f766e')">
<span x-text="userInfo.sign_in?.identity?.icon || '✅'"></span>
<span x-text="'连续签到 ' + (userInfo.sign_in?.streak_days || 0) + ' 天'"></span>
<template x-if="userInfo.sign_in?.identity?.label">
<span x-text="'· ' + userInfo.sign_in.identity.label"></span>
</template>
</span>
</div>
<div style="font-size: 11px; color: #999; margin-top: 4px;">
加入: <span x-text="userInfo.created_at"></span>
</div>
+19
View File
@@ -277,6 +277,25 @@
}
}">
<form @submit.prevent="saveProfile">
@php
$currentSignIdentity = Auth::user()->currentSignInIdentity();
$latestSignIn = Auth::user()->dailySignIns()->first();
@endphp
<div class="mb-4 rounded-lg border border-teal-100 bg-teal-50 px-3 py-2 text-sm text-teal-800">
<div class="font-bold">每日签到</div>
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs">
<span>连续签到 {{ (int) ($latestSignIn?->streak_days ?? 0) }} </span>
@if ($currentSignIdentity)
<span class="inline-flex items-center gap-1 rounded-full border border-teal-200 bg-white px-2 py-0.5 font-bold"
style="color: {{ $currentSignIdentity->badge_color ?? '#0f766e' }}">
<span>{{ $currentSignIdentity->badge_icon ?? '✅' }}</span>
<span>{{ $currentSignIdentity->badge_name }}</span>
</span>
@else
<span class="text-teal-600">暂无签到身份</span>
@endif
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-bold text-gray-700 mb-2">性别</label>
<select x-model="profileData.sex"