修复聊天室字号偏好和游戏通知显示

This commit is contained in:
pllx
2026-04-29 18:27:32 +08:00
parent 6748fbc44e
commit 50b050c4bc
18 changed files with 363 additions and 92 deletions
@@ -185,9 +185,10 @@ class DailySignInController extends Controller
.',当前连续签到 '.$streakDays.' 天,获得 '.$rewardText.$identityText.'。'; .',当前连续签到 '.$streakDays.' 天,获得 '.$rewardText.$identityText.'。';
} }
// 聊天消息内的快捷按钮使用相对字号,避免覆盖用户选择的消息字号。
$quickButton = '<button type="button" onclick="window.quickDailySignIn && window.quickDailySignIn()" ' $quickButton = '<button type="button" onclick="window.quickDailySignIn && window.quickDailySignIn()" '
.'style="display:inline-block;margin-left:6px;padding:1px 8px;border:none;border-radius:999px;' .'style="display:inline-block;margin-left:6px;padding:1px 8px;border:none;border-radius:999px;'
.'background:#ccfbf1;color:#0f766e;font-size:10px;font-weight:bold;cursor:pointer;vertical-align:middle;">' .'background:#ccfbf1;color:#0f766e;font-size:0.78em;font-weight:bold;cursor:pointer;vertical-align:middle;">'
.'✅ 快速签到</button>'; .'✅ 快速签到</button>';
return '【'.e($user->username).'】完成今日签到,连续签到 ' return '【'.e($user->username).'】完成今日签到,连续签到 '
+2 -1
View File
@@ -104,9 +104,10 @@ class EarnController extends Controller
// 6. 广播全服系统消息 // 6. 广播全服系统消息
if ($roomId > 0) { if ($roomId > 0) {
// 公屏消息内的入口标签使用相对字号,跟随用户在聊天室选择的字号。
$promoTag = ' <span onclick="window.dispatchEvent(new CustomEvent(\'open-earn-panel\'))" ' $promoTag = ' <span onclick="window.dispatchEvent(new CustomEvent(\'open-earn-panel\'))" '
.'style="display:inline-block;margin-left:6px;padding:1px 7px;background:#e9e4f5;' .'style="display:inline-block;margin-left:6px;padding:1px 7px;background:#e9e4f5;'
.'color:#6d4fa8;border-radius:10px;font-size:10px;cursor:pointer;font-weight:bold;vertical-align:middle;' .'color:#6d4fa8;border-radius:10px;font-size:0.78em;cursor:pointer;font-weight:bold;vertical-align:middle;'
.'border:1px solid #d0c4ec;" title="点击赚金币">💰 看视频赚金币</span>'; .'border:1px solid #d0c4ec;" title="点击赚金币">💰 看视频赚金币</span>';
$sysMsg = [ $sysMsg = [
+80 -17
View File
@@ -19,6 +19,7 @@ namespace App\Http\Controllers;
use App\Enums\CurrencySource; use App\Enums\CurrencySource;
use App\Models\GameConfig; use App\Models\GameConfig;
use App\Models\Sysparam; use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService; use App\Services\ChatStateService;
use App\Services\FishingService; use App\Services\FishingService;
use App\Services\GameRoomScopeService; use App\Services\GameRoomScopeService;
@@ -36,6 +37,9 @@ use Illuminate\Support\Str;
*/ */
class FishingController extends Controller class FishingController extends Controller
{ {
/**
* 注入钓鱼流程需要的状态、会员、金币、商店和房间范围服务。
*/
public function __construct( public function __construct(
private readonly ChatStateService $chatState, private readonly ChatStateService $chatState,
private readonly VipService $vipService, private readonly VipService $vipService,
@@ -84,6 +88,14 @@ class FishingController extends Controller
], 429); ], 429);
} }
$tokenKey = "fishing:token:{$user->id}";
if (Redis::exists($tokenKey)) {
$activeSessionResponse = $this->restoreActiveFishingSessionResponse($user, $tokenKey);
if ($activeSessionResponse) {
return $activeSessionResponse;
}
}
// 2. 检查金币是否足够 // 2. 检查金币是否足够
$cost = (int) (GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5')); $cost = (int) (GameConfig::param('fishing', 'fishing_cost') ?? Sysparam::getValue('fishing_cost', '5'));
if (($user->jjb ?? 0) < $cost) { if (($user->jjb ?? 0) < $cost) {
@@ -93,34 +105,54 @@ class FishingController extends Controller
], 422); ], 422);
} }
// 3. 扣除金币 // 3. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲)
$this->currencyService->change(
$user, 'gold', -$cost,
CurrencySource::FISHING_COST,
"钓鱼抛竿消耗 {$cost} 金币",
$id,
);
$user->refresh();
// 4. 生成一次性 token,存入 Redis(TTL = 等待时间 + 收竿窗口 + 缓冲)
$waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8')); $waitMin = (int) (GameConfig::param('fishing', 'fishing_wait_min') ?? Sysparam::getValue('fishing_wait_min', '8'));
$waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15')); $waitMax = (int) (GameConfig::param('fishing', 'fishing_wait_max') ?? Sysparam::getValue('fishing_wait_max', '15'));
$waitTime = rand($waitMin, $waitMax); $waitTime = rand($waitMin, $waitMax);
$token = Str::random(32); $token = Str::random(32);
$tokenKey = "fishing:token:{$user->id}"; $tokenTtl = $waitTime + 13;
// token 有效期 = 等待时间 + 8秒点击窗口 + 5秒缓冲 $tokenPayload = json_encode([
// 同时把 cast 时间戳和 wait_time 一起存入,供 reel 做服务端时间校验
Redis::setex($tokenKey, $waitTime + 13, json_encode([
'token' => $token, 'token' => $token,
'cast_at' => time(), 'cast_at' => time(),
'wait_time' => $waitTime, 'wait_time' => $waitTime,
])); ]);
// 5. 生成随机浮漂坐标(百分比,避开边缘) // 原子占用本次抛竿 token,避免多标签页自动钓鱼互相覆盖令牌。
$reserved = Redis::command('set', [$tokenKey, $tokenPayload, 'EX', $tokenTtl, 'NX']);
if (! $reserved) {
$activeSessionResponse = $this->restoreActiveFishingSessionResponse($user, $tokenKey);
if ($activeSessionResponse) {
return $activeSessionResponse;
}
return response()->json([
'status' => 'error',
'message' => '钓鱼状态同步中,请稍后重试。',
'retry_after' => 3,
], 409);
}
try {
// token 占用成功后才扣金币,确保重复抛竿不会多扣费用。
$this->currencyService->change(
$user, 'gold', -$cost,
CurrencySource::FISHING_COST,
"钓鱼抛竿消耗 {$cost} 金币",
$id,
);
$user->refresh();
} catch (\Throwable $exception) {
// 金币扣除失败时释放 token,避免用户被短时间卡在未收竿状态。
Redis::del($tokenKey);
throw $exception;
}
// 4. 生成随机浮漂坐标(百分比,避开边缘)
$bobberX = rand(15, 85); // 左右 15%~85% $bobberX = rand(15, 85); // 左右 15%~85%
$bobberY = rand(20, 65); // 上下 20%~65% $bobberY = rand(20, 65); // 上下 20%~65%
// 6. 检查是否持有有效自动钓鱼卡 // 5. 检查是否持有有效自动钓鱼卡
$autoFishingMinutes = $this->shopService->getActiveAutoFishingMinutesLeft($user); $autoFishingMinutes = $this->shopService->getActiveAutoFishingMinutesLeft($user);
return response()->json([ return response()->json([
@@ -137,6 +169,37 @@ class FishingController extends Controller
]); ]);
} }
/**
* 恢复已有钓鱼会话,避免刷新页面后丢失前端内存里的收竿令牌。
*/
private function restoreActiveFishingSessionResponse(User $user, string $tokenKey): ?JsonResponse
{
$stored = json_decode((string) Redis::get($tokenKey), true);
if (! is_array($stored) || empty($stored['token'])) {
Redis::del($tokenKey);
return null;
}
$elapsed = time() - (int) ($stored['cast_at'] ?? 0);
$waitTime = max(0, (int) ($stored['wait_time'] ?? 0) - $elapsed);
$autoFishingMinutes = $this->shopService->getActiveAutoFishingMinutesLeft($user);
return response()->json([
'status' => 'success',
'message' => '已恢复正在进行的钓鱼,请等待本次收竿。',
'wait_time' => $waitTime,
'bobber_x' => rand(15, 85),
'bobber_y' => rand(20, 65),
'token' => (string) $stored['token'],
'auto_fishing' => $autoFishingMinutes > 0,
'auto_fishing_minutes_left' => $autoFishingMinutes,
'cost' => 0,
'jjb' => $user->jjb,
'restored' => true,
]);
}
/** /**
* 收竿 验证浮漂 token,随机计算钓鱼结果,更新经验/金币,广播到聊天室。 * 收竿 验证浮漂 token,随机计算钓鱼结果,更新经验/金币,广播到聊天室。
* *
+11 -1
View File
@@ -303,12 +303,13 @@ class UserController extends Controller
} }
/** /**
* 保存聊天室屏蔽禁音偏好。 * 保存聊天室屏蔽禁音与字号偏好。
*/ */
public function updateChatPreferences(UpdateChatPreferencesRequest $request): JsonResponse public function updateChatPreferences(UpdateChatPreferencesRequest $request): JsonResponse
{ {
$user = Auth::user(); $user = Auth::user();
$data = $request->validated(); $data = $request->validated();
$existingPreferences = is_array($user->chat_preferences) ? $user->chat_preferences : [];
$blockedSystemSenders = collect($data['blocked_system_senders'] ?? []) $blockedSystemSenders = collect($data['blocked_system_senders'] ?? [])
->map(function (string $sender): string { ->map(function (string $sender): string {
// 猜谜活动前端文案允许升级,但持久化键仍复用旧值,避免历史偏好失效。 // 猜谜活动前端文案允许升级,但持久化键仍复用旧值,避免历史偏好失效。
@@ -324,6 +325,15 @@ class UserController extends Controller
'sound_muted' => (bool) $data['sound_muted'], 'sound_muted' => (bool) $data['sound_muted'],
]; ];
// 字号偏好和屏蔽/禁音共用账号配置,旧请求未携带字号时保留原值。
$fontSize = array_key_exists('font_size', $data) && $data['font_size'] !== null
? (int) $data['font_size']
: ($existingPreferences['font_size'] ?? null);
if ($fontSize !== null) {
$preferences['font_size'] = (int) $fontSize;
}
$user->update([ $user->update([
'chat_preferences' => $preferences, 'chat_preferences' => $preferences,
]); ]);
@@ -2,7 +2,7 @@
/** /**
* 文件功能:聊天室偏好设置验证器 * 文件功能:聊天室偏好设置验证器
* 负责校验用户提交的屏蔽播报禁音配置。 * 负责校验用户提交的屏蔽播报禁音与聊天室字号配置。
*/ */
namespace App\Http\Requests; namespace App\Http\Requests;
@@ -12,7 +12,7 @@ use Illuminate\Validation\Rule;
/** /**
* 聊天室偏好设置验证器 * 聊天室偏好设置验证器
* 仅允许提交白名单内的屏蔽项布尔型禁音状态。 * 仅允许提交白名单内的屏蔽项布尔型禁音状态与合法字号
*/ */
class UpdateChatPreferencesRequest extends FormRequest class UpdateChatPreferencesRequest extends FormRequest
{ {
@@ -38,6 +38,7 @@ class UpdateChatPreferencesRequest extends FormRequest
Rule::in(['钓鱼播报', '猜成语', '猜谜活动', '星海小博士', '百家乐', '跑马', '神秘箱子', '五子棋', '老虎机', '双色球彩票']), Rule::in(['钓鱼播报', '猜成语', '猜谜活动', '星海小博士', '百家乐', '跑马', '神秘箱子', '五子棋', '老虎机', '双色球彩票']),
], ],
'sound_muted' => ['required', 'boolean'], 'sound_muted' => ['required', 'boolean'],
'font_size' => ['nullable', 'integer', 'min:10', 'max:30'],
]; ];
} }
@@ -53,6 +54,9 @@ class UpdateChatPreferencesRequest extends FormRequest
'blocked_system_senders.*.in' => '存在不支持的屏蔽项目。', 'blocked_system_senders.*.in' => '存在不支持的屏蔽项目。',
'sound_muted.required' => '请传入禁音状态。', 'sound_muted.required' => '请传入禁音状态。',
'sound_muted.boolean' => '禁音状态格式无效。', 'sound_muted.boolean' => '禁音状态格式无效。',
'font_size.integer' => '聊天室字号格式无效。',
'font_size.min' => '聊天室字号不能小于 10。',
'font_size.max' => '聊天室字号不能大于 30。',
]; ];
} }
} }
+3 -2
View File
@@ -400,7 +400,8 @@ class BaccaratLossCoverService
} }
if ($compensableCount > 0) { if ($compensableCount > 0) {
$button = '<button onclick="claimBaccaratLossCover('.$event->id.')" style="margin-left:8px;padding:3px 12px;background:#16a34a;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;font-weight:bold;">领取补偿</button>'; // 聊天消息内的按钮使用相对字号,跟随用户在底部工具栏选择的聊天字号。
$button = '<button onclick="claimBaccaratLossCover('.$event->id.')" style="margin-left:8px;padding:3px 12px;background:#16a34a;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:0.82em;font-weight:bold;">领取补偿</button>';
$content = "📣 【{$event->title}】活动已结束并完成结算!本次共有 <b>{$compensableCount}</b> 位玩家可领取补偿,截止时间:{$event->claim_deadline_at?->format('m-d H:i')}{$button}"; $content = "📣 【{$event->title}】活动已结束并完成结算!本次共有 <b>{$compensableCount}</b> 位玩家可领取补偿,截止时间:{$event->claim_deadline_at?->format('m-d H:i')}{$button}";
} else { } else {
$content = "📣 【{$event->title}】活动已结束!本次活动没有产生可领取补偿的记录。"; $content = "📣 【{$event->title}】活动已结束!本次活动没有产生可领取补偿的记录。";
@@ -446,7 +447,7 @@ class BaccaratLossCoverService
$formattedAmount = number_format($amount); $formattedAmount = number_format($amount);
$button = $event->status === 'claimable' $button = $event->status === 'claimable'
? ' <button onclick="claimBaccaratLossCover('.$event->id.')" style="margin-left:8px;padding:3px 12px;background:#16a34a;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:12px;font-weight:bold;">领取补偿</button>' ? ' <button onclick="claimBaccaratLossCover('.$event->id.')" style="margin-left:8px;padding:3px 12px;background:#16a34a;color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:0.82em;font-weight:bold;">领取补偿</button>'
: ''; : '';
// 领取成功的公屏格式复用百家乐参与播报风格,保证聊天室感知一致。 // 领取成功的公屏格式复用百家乐参与播报风格,保证聊天室感知一致。
+2 -1
View File
@@ -94,10 +94,11 @@ class FishingService
$promoTag = ''; $promoTag = '';
if (! $isAi) { if (! $isAi) {
$autoFishingMinutesLeft = $this->shopService->getActiveAutoFishingMinutesLeft($user); $autoFishingMinutesLeft = $this->shopService->getActiveAutoFishingMinutesLeft($user);
// 公屏消息内的促销标签使用相对字号,避免覆盖用户在聊天室选择的字号。
$promoTag = $autoFishingMinutesLeft > 0 $promoTag = $autoFishingMinutesLeft > 0
? ' <span onclick="window.openShopModal&&window.openShopModal()" ' ? ' <span onclick="window.openShopModal&&window.openShopModal()" '
.'style="display:inline-block;margin-left:6px;padding:1px 7px;background:#e9e4f5;' .'style="display:inline-block;margin-left:6px;padding:1px 7px;background:#e9e4f5;'
.'color:#6d4fa8;border-radius:10px;font-size:10px;cursor:pointer;font-weight:bold;vertical-align:middle;' .'color:#6d4fa8;border-radius:10px;font-size:0.78em;cursor:pointer;font-weight:bold;vertical-align:middle;'
.'border:1px solid #d0c4ec;" title="点击购买自动钓鱼卡">🎣 自动钓鱼卡</span>' .'border:1px solid #d0c4ec;" title="点击购买自动钓鱼卡">🎣 自动钓鱼卡</span>'
: ''; : '';
} }
+2 -2
View File
@@ -216,7 +216,7 @@ a:hover {
} }
.msg-line .msg-time { .msg-line .msg-time {
font-size: 9px; font-size: 0.72em;
color: #999; color: #999;
} }
@@ -245,7 +245,7 @@ a:hover {
.msg-line.sys-msg { .msg-line.sys-msg {
color: #cc0000; color: #cc0000;
text-align: center; text-align: center;
font-size: 9pt; font-size: 0.9em;
} }
/* ── 底部输入工具栏 ─────────────────────────────── */ /* ── 底部输入工具栏 ─────────────────────────────── */
+2 -2
View File
@@ -240,7 +240,7 @@ a:hover {
} }
.msg-line .msg-time { .msg-line .msg-time {
font-size: 9px; font-size: 0.72em;
color: #999; color: #999;
} }
@@ -269,7 +269,7 @@ a:hover {
.msg-line.sys-msg { .msg-line.sys-msg {
color: #cc0000; color: #cc0000;
text-align: center; text-align: center;
font-size: 9pt; font-size: 0.9em;
} }
/* ── 底部输入工具栏 ─────────────────────────────── */ /* ── 底部输入工具栏 ─────────────────────────────── */
+2 -1
View File
@@ -216,7 +216,7 @@ import { initChatImageLightboxEvents, closeChatImageLightbox, openChatImageLight
import { bindRoomStatusControls, normalizeRoomStatus, renderRoomStatusRow, renderRoomsOnlineStatus, renderRoomsOnlineStatusToContainer, resolveRoomUrl } from "./chat-room/rooms.js"; import { bindRoomStatusControls, normalizeRoomStatus, renderRoomStatusRow, renderRoomsOnlineStatus, renderRoomsOnlineStatusToContainer, resolveRoomUrl } from "./chat-room/rooms.js";
import { bindChatRightPanelControls } from "./chat-room/right-panel.js"; import { bindChatRightPanelControls } from "./chat-room/right-panel.js";
import { bindChatImageUploadControl } from "./chat-room/image-upload.js"; import { bindChatImageUploadControl } from "./chat-room/image-upload.js";
import { applyFontSize, bindChatFontSizeControl, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js"; import { applyFontSize, bindChatFontSizeControl, CHAT_DEFAULT_FONT_SIZE, CHAT_FONT_SIZE_STORAGE_KEY, restoreChatFontSize } from "./chat-room/font-size.js";
import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js"; import { bindAppointmentAnnouncementControls, showAppointmentBanner } from "./chat-room/appointment-announcement.js";
import { bindChatBanner } from "./chat-room/banner.js"; import { bindChatBanner } from "./chat-room/banner.js";
import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js"; import { bindChatBotControls, clearChatBotContext, sendToChatBot } from "./chat-room/chat-bot.js";
@@ -461,6 +461,7 @@ if (typeof window !== "undefined") {
sendToChatBot, sendToChatBot,
applyFontSize, applyFontSize,
bindChatFontSizeControl, bindChatFontSizeControl,
CHAT_DEFAULT_FONT_SIZE,
CHAT_FONT_SIZE_STORAGE_KEY, CHAT_FONT_SIZE_STORAGE_KEY,
restoreChatFontSize, restoreChatFontSize,
bindChatImageUploadControl, bindChatImageUploadControl,
+3 -2
View File
@@ -8,6 +8,7 @@ import { enqueueChatMessage } from "./message-renderer.js";
// ── 事件注册标记 ── // ── 事件注册标记 ──
let chatEventsBound = false; let chatEventsBound = false;
let chatWebSocketInitRetryTimer = null; let chatWebSocketInitRetryTimer = null;
const GOMOKU_INVITE_BUTTON_FONT_SIZE = "0.82em";
// ── 辅助函数 ── // ── 辅助函数 ──
function csrf() { function csrf() {
@@ -235,13 +236,13 @@ function setupGomokuInviteListener() {
const acceptBtn = isSelf const acceptBtn = isSelf
? `<button type="button" data-gomoku-open-panel class="gomoku-invite-open" ? `<button type="button" data-gomoku-open-panel class="gomoku-invite-open"
style="margin-left:10px; padding:3px 12px; border:1.5px solid #2d6096; style="margin-left:10px; padding:3px 12px; border:1.5px solid #2d6096;
border-radius:12px; background:#f0f6ff; color:#2d6096; font-size:12px; border-radius:12px; background:#f0f6ff; color:#2d6096; font-size:${GOMOKU_INVITE_BUTTON_FONT_SIZE};
cursor:pointer; font-family:inherit; transition:all .15s;"> cursor:pointer; font-family:inherit; transition:all .15s;">
⤴️ 打开面板 ⤴️ 打开面板
</button>` </button>`
: `<button type="button" data-gomoku-accept-id="${gomokuGameId}" id="gomoku-accept-${gomokuGameId}" class="gomoku-invite-accept" : `<button type="button" data-gomoku-accept-id="${gomokuGameId}" id="gomoku-accept-${gomokuGameId}" class="gomoku-invite-accept"
style="margin-left:10px; padding:3px 12px; border:1.5px solid #336699; style="margin-left:10px; padding:3px 12px; border:1.5px solid #336699;
border-radius:12px; background:#336699; color:#fff; font-size:12px; border-radius:12px; background:#336699; color:#fff; font-size:${GOMOKU_INVITE_BUTTON_FONT_SIZE};
cursor:pointer; font-family:inherit; transition:opacity .15s;"> cursor:pointer; font-family:inherit; transition:opacity .15s;">
⚔️ 接受挑战 ⚔️ 接受挑战
</button>`; </button>`;
+12 -3
View File
@@ -10,6 +10,8 @@ let autoFishing = false;
let autoFishCooldownTimer = null; let autoFishCooldownTimer = null;
let autoFishCooldownCountdown = null; let autoFishCooldownCountdown = null;
let fishingCastPending = false; let fishingCastPending = false;
const FISHING_MESSAGE_META_FONT_SIZE = "0.78em";
const FISHING_MESSAGE_BODY_FONT_SIZE = "1em";
/** /**
* 读取 CSRF Token。 * 读取 CSRF Token。
@@ -432,6 +434,13 @@ export async function startFishing() {
return; return;
} }
if (autoFishing && response.status === 409) {
// 多标签页或重复自动抛竿时,后端会保留先到的 token,当前页等待后再接管。
appendFishingMessage(`<span style="color:#d97706;">【钓鱼】${escapeHtml(data.message || "已有钓鱼正在进行,稍后自动重试。")}</span><span class="msg-time">(${timeText()})</span>`);
startAutoFishingCooldown(Math.max(1, Number(data.retry_after) || 5));
return;
}
window.chatDialog?.alert?.(data.message || "钓鱼失败", "操作失败", "#cc4444"); window.chatDialog?.alert?.(data.message || "钓鱼失败", "操作失败", "#cc4444");
setFishingButton("🎣 钓鱼", false); setFishingButton("🎣 钓鱼", false);
return; return;
@@ -460,7 +469,7 @@ export async function startFishing() {
if (data.auto_fishing) { if (data.auto_fishing) {
showAutoFishStopButton(0); showAutoFishStopButton(0);
updateAutoFishStopButtonCountdown(0, "自动收竿中 · 可拖动"); updateAutoFishStopButtonCountdown(0, "自动收竿中 · 可拖动");
appendFishingMessage(`<span style="color:#7c3aed;font-weight:bold;">🎣 自动钓鱼卡生效!自动收竿中... <span style="font-size:10px;opacity:0.7">(剩余${Number(data.auto_fishing_minutes_left) || 0}分钟)</span></span>`); appendFishingMessage(`<span style="color:#7c3aed;font-weight:bold;">🎣 自动钓鱼卡生效!自动收竿中... <span style="font-size:${FISHING_MESSAGE_META_FONT_SIZE};opacity:0.7">(剩余${Number(data.auto_fishing_minutes_left) || 0}分钟)</span></span>`);
fishingReelTimeout = window.setTimeout(() => { fishingReelTimeout = window.setTimeout(() => {
removeBobber(); removeBobber();
void reelFish(); void reelFish();
@@ -468,7 +477,7 @@ export async function startFishing() {
return; return;
} }
appendFishingMessage('<span style="color:#d97706;font-weight:bold;font-size:14px;">🐟 鱼上钩了!快点击屏幕上的浮漂!</span>'); appendFishingMessage(`<span style="color:#d97706;font-weight:bold;font-size:${FISHING_MESSAGE_BODY_FONT_SIZE};">🐟 鱼上钩了!快点击屏幕上的浮漂!</span>`);
setFishingButton("🎣 点击浮漂!", true); setFishingButton("🎣 点击浮漂!", true);
bobber.addEventListener("click", () => { bobber.addEventListener("click", () => {
removeBobber(); removeBobber();
@@ -530,7 +539,7 @@ export async function reelFish() {
const color = Number(result.exp || 0) >= 0 ? "#16a34a" : "#dc2626"; const color = Number(result.exp || 0) >= 0 ? "#16a34a" : "#dc2626";
appendFishingMessage( appendFishingMessage(
`<span style="color:${color};font-weight:bold;">${escapeHtml(result.emoji || "🎣")}【钓鱼结果】</span>${escapeHtml(result.message || "")}` + `<span style="color:${color};font-weight:bold;">${escapeHtml(result.emoji || "🎣")}【钓鱼结果】</span>${escapeHtml(result.message || "")}` +
` <span style="color:#666;font-size:11px;">(经验:${Number(data.exp_num) || 0} 金币:${Number(data.jjb) || 0}</span>` + ` <span style="color:#666;font-size:${FISHING_MESSAGE_META_FONT_SIZE};">(经验:${Number(data.exp_num) || 0} 金币:${Number(data.jjb) || 0}</span>` +
`<span class="msg-time">(${timeText()})</span>`, `<span class="msg-time">(${timeText()})</span>`,
); );
+71 -8
View File
@@ -1,17 +1,69 @@
// 聊天室字号偏好控制,保留旧的 localStorage key 以兼容已有用户设置。 // 聊天室字号偏好控制,保留旧的 localStorage key 以兼容已有用户设置。
export const CHAT_FONT_SIZE_STORAGE_KEY = "chat_font_size"; export const CHAT_FONT_SIZE_STORAGE_KEY = "chat_font_size";
export const CHAT_DEFAULT_FONT_SIZE = 13;
export const CHAT_FONT_SIZE_MIN = 10;
export const CHAT_FONT_SIZE_MAX = 30;
let fontSizeEventsBound = false; let fontSizeEventsBound = false;
/** /**
* 应用字号到聊天消息窗口,并保存到 localStorage * 规整聊天室字号,过滤非法或越界的旧缓存值
*
* @param {unknown} size 字号大小
* @returns {number|null}
*/
export function normalizeChatFontSize(size) {
const px = Number.parseInt(String(size ?? ""), 10);
if (Number.isNaN(px) || px < CHAT_FONT_SIZE_MIN || px > CHAT_FONT_SIZE_MAX) {
return null;
}
return px;
}
/**
* 同步底部输入框上方工具按钮字号。
*
* @param {number} px 用户选择的聊天字号
* @returns {void}
*/
function applyInputToolbarFontSize(px) {
const toolbarRow = document.querySelector("#chat-form > .input-row");
if (!(toolbarRow instanceof HTMLElement)) {
return;
}
const fontSize = `${px}px`;
toolbarRow.style.fontSize = fontSize;
toolbarRow.style.fontFamily = "inherit";
toolbarRow.style.lineHeight = "1.2";
toolbarRow.querySelectorAll([
":scope > label",
":scope > label select",
":scope > label input",
":scope > button",
":scope > div > button",
"#feature-menu button",
"#admin-menu button",
].join(",")).forEach((control) => {
control.style.fontFamily = "inherit";
control.style.fontSize = "inherit";
control.style.lineHeight = "1.2";
control.style.fontWeight = "400";
});
}
/**
* 应用字号到聊天消息窗口和输入栏工具按钮,并保存到 localStorage。
* *
* @param {string|number} size 字号大小 * @param {string|number} size 字号大小
* @param {{syncContext?:boolean}} options 同步选项
* @returns {boolean} * @returns {boolean}
*/ */
export function applyFontSize(size) { export function applyFontSize(size, options = {}) {
const px = Number.parseInt(size, 10); const px = normalizeChatFontSize(size);
if (Number.isNaN(px) || px < 10 || px > 30) { if (px === null) {
return false; return false;
} }
@@ -23,8 +75,15 @@ export function applyFontSize(size) {
if (privateContainer) { if (privateContainer) {
privateContainer.style.fontSize = `${px}px`; privateContainer.style.fontSize = `${px}px`;
} }
applyInputToolbarFontSize(px);
localStorage.setItem(CHAT_FONT_SIZE_STORAGE_KEY, String(px)); localStorage.setItem(CHAT_FONT_SIZE_STORAGE_KEY, String(px));
if (options.syncContext !== false && window.chatContext && typeof window.chatContext === "object") {
window.chatContext.chatPreferences = {
...(window.chatContext.chatPreferences || {}),
font_size: px,
};
}
const selector = document.getElementById("font_size_select"); const selector = document.getElementById("font_size_select");
if (selector) { if (selector) {
@@ -35,14 +94,16 @@ export function applyFontSize(size) {
} }
/** /**
* 从 localStorage 恢复已保存的聊天室字号。 * 从账号偏好或 localStorage 恢复已保存的聊天室字号。
* *
* @returns {boolean} * @returns {boolean}
*/ */
export function restoreChatFontSize() { export function restoreChatFontSize() {
const saved = localStorage.getItem(CHAT_FONT_SIZE_STORAGE_KEY); const serverFontSize = normalizeChatFontSize(window.chatContext?.chatPreferences?.font_size);
const localFontSize = normalizeChatFontSize(localStorage.getItem(CHAT_FONT_SIZE_STORAGE_KEY));
const saved = serverFontSize ?? localFontSize ?? CHAT_DEFAULT_FONT_SIZE;
return saved ? applyFontSize(saved) : false; return applyFontSize(saved, { syncContext: serverFontSize !== null });
} }
/** /**
@@ -61,6 +122,8 @@ export function bindChatFontSizeControl() {
return; return;
} }
applyFontSize(event.target.value); if (applyFontSize(event.target.value)) {
void window.saveChatPreferences?.();
}
}); });
} }
+40 -32
View File
@@ -23,6 +23,14 @@ import {
// ── 游戏标签判断 ── // ── 游戏标签判断 ──
const GAME_LABEL_PREFIXES = ["五子棋", "双色球", "钓鱼", "老虎机", "百家乐", "赛马"]; const GAME_LABEL_PREFIXES = ["五子棋", "双色球", "钓鱼", "老虎机", "百家乐", "赛马"];
const CHAT_NOTICE_CHIP_FONT_SIZE = "0.82em";
const CHAT_NOTICE_META_FONT_SIZE = "0.72em";
const CHAT_NOTICE_BUTTON_FONT_SIZE = "0.82em";
const CHAT_NOTICE_BODY_FONT_SIZE = "1em";
const CHAT_NOTICE_ICON_FONT_SIZE = "1.08em";
const CHAT_NOTICE_LARGE_ICON_FONT_SIZE = "1.35em";
const CHAT_NOTICE_DECOR_ICON_FONT_SIZE = "4.25em";
function isGameLabel(name) { function isGameLabel(name) {
if (GAME_LABEL_PREFIXES.some((p) => name.startsWith(p))) return true; if (GAME_LABEL_PREFIXES.some((p) => name.startsWith(p))) return true;
if (name.includes(" ")) return true; if (name.includes(" ")) return true;
@@ -60,7 +68,7 @@ function parseBracketUsers(content, color = "#000099") {
* 构建统一的猜谜活动标题与题型标签。 * 构建统一的猜谜活动标题与题型标签。
*/ */
function buildGameLabelChipHtml(label, accentColor) { function buildGameLabelChipHtml(label, accentColor) {
return `<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor};color:#fff;font-size:11px;font-weight:700;line-height:1;border:1px solid ${accentColor};">${escapeHtml(label)}</span>`; return `<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor};color:#fff;font-size:${CHAT_NOTICE_CHIP_FONT_SIZE};font-weight:700;line-height:1;border:1px solid ${accentColor};">${escapeHtml(label)}</span>`;
} }
/** /**
@@ -115,19 +123,19 @@ function buildRedPacketAnnouncementHtml(msg, timeStr) {
.trim(); .trim();
const summary = escapeHtml(textOnlyContent); const summary = escapeHtml(textOnlyContent);
const actionButtonHtml = `<button type="button"${buttonOnclick ? ` onclick="${escapeHtml(buttonOnclick)}"` : ""} style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor};color:#fff;font-size:11px;font-weight:700;line-height:1;border:1px solid ${accentColor};cursor:pointer;box-shadow:none;vertical-align:middle;">${escapeHtml(buttonLabel)}</button>`; const actionButtonHtml = `<button type="button"${buttonOnclick ? ` onclick="${escapeHtml(buttonOnclick)}"` : ""} style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor};color:#fff;font-size:${CHAT_NOTICE_BUTTON_FONT_SIZE};font-weight:700;line-height:1;border:1px solid ${accentColor};cursor:pointer;box-shadow:none;vertical-align:middle;">${escapeHtml(buttonLabel)}</button>`;
return ` return `
<div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:${colorPalette.softBackground};border:1px solid ${colorPalette.softBorder};box-shadow:0 4px 12px rgba(15,23,42,.045);overflow:hidden;"> <div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:${colorPalette.softBackground};border:1px solid ${colorPalette.softBorder};box-shadow:0 4px 12px rgba(15,23,42,.045);overflow:hidden;">
<div style="width:23px;height:23px;border-radius:7px;background:${accentColor};display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;box-shadow:0 2px 6px ${colorPalette.softBorder};flex-shrink:0;">${icon}</div> <div style="width:23px;height:23px;border-radius:7px;background:${accentColor};display:flex;align-items:center;justify-content:center;color:#fff;font-size:${CHAT_NOTICE_ICON_FONT_SIZE};box-shadow:0 2px 6px ${colorPalette.softBorder};flex-shrink:0;">${icon}</div>
<div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:${colorPalette.text};"> <div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:${colorPalette.text};">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex-shrink:0;"> <div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex-shrink:0;">
${buildGameLabelChipHtml("礼包红包", accentColor)} ${buildGameLabelChipHtml("礼包红包", accentColor)}
<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${colorPalette.chipBackground};color:${colorPalette.chipText};font-size:11px;font-weight:700;line-height:1;border:1px solid ${colorPalette.chipBorder};">${escapeHtml(typeLabel)}</span> <span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${colorPalette.chipBackground};color:${colorPalette.chipText};font-size:${CHAT_NOTICE_CHIP_FONT_SIZE};font-weight:700;line-height:1;border:1px solid ${colorPalette.chipBorder};">${escapeHtml(typeLabel)}</span>
</div> </div>
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:12px;line-height:1.25;font-weight:700;min-width:200px;flex:1;"> <div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:${CHAT_NOTICE_BODY_FONT_SIZE};line-height:1.25;font-weight:700;min-width:200px;flex:1;">
<span>${summary}</span> <span>${summary}</span>
<span class="msg-time" style="font-size:10px;color:#94a3b8;">(${timeStr})</span> <span class="msg-time" style="font-size:${CHAT_NOTICE_META_FONT_SIZE};color:#94a3b8;">(${timeStr})</span>
${actionButtonHtml} ${actionButtonHtml}
</div> </div>
</div> </div>
@@ -144,7 +152,7 @@ function buildQuizBadgeHtml(msg, accentColor = "#7c3aed") {
return ` return `
<span style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;"> <span style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;">
${buildGameLabelChipHtml(activityLabel, accentColor)} ${buildGameLabelChipHtml(activityLabel, accentColor)}
<span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor}1A;color:${accentColor};font-size:11px;font-weight:700;line-height:1;border:1px solid ${accentColor}33;">${escapeHtml(typeLabel)}</span> <span style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:${accentColor}1A;color:${accentColor};font-size:${CHAT_NOTICE_CHIP_FONT_SIZE};font-weight:700;line-height:1;border:1px solid ${accentColor}33;">${escapeHtml(typeLabel)}</span>
</span> </span>
`; `;
} }
@@ -373,7 +381,7 @@ function normalizeSystemGameCardActions(content, meta) {
const onclickAttr = onclickMatch ? ` onclick="${escapeHtml(onclickMatch[2])}"` : ""; const onclickAttr = onclickMatch ? ` onclick="${escapeHtml(onclickMatch[2])}"` : "";
const safeLabel = String(label || "").trim(); const safeLabel = String(label || "").trim();
return `<button type="button"${onclickAttr} style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:#fff;color:${meta.accent};font-size:11px;font-weight:700;line-height:1;border:1px solid ${meta.accent};cursor:pointer;box-shadow:none;vertical-align:middle;">${safeLabel}</button>`; return `<button type="button"${onclickAttr} style="display:inline-flex;align-items:center;padding:2px 9px;border-radius:999px;background:#fff;color:${meta.accent};font-size:${CHAT_NOTICE_BUTTON_FONT_SIZE};font-weight:700;line-height:1;border:1px solid ${meta.accent};cursor:pointer;box-shadow:none;vertical-align:middle;">${safeLabel}</button>`;
}); });
} }
@@ -391,12 +399,12 @@ function buildSystemGameNotificationHtml(msg, timeStr) {
return ` return `
<div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:${meta.background};border:1px solid ${meta.border};box-shadow:0 4px 12px rgba(15,23,42,.045);overflow:hidden;"> <div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:${meta.background};border:1px solid ${meta.border};box-shadow:0 4px 12px rgba(15,23,42,.045);overflow:hidden;">
<div style="width:23px;height:23px;border-radius:7px;background:${meta.accent};display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;box-shadow:0 2px 6px ${meta.border};flex-shrink:0;">${meta.icon}</div> <div style="width:23px;height:23px;border-radius:7px;background:${meta.accent};display:flex;align-items:center;justify-content:center;color:#fff;font-size:${CHAT_NOTICE_ICON_FONT_SIZE};box-shadow:0 2px 6px ${meta.border};flex-shrink:0;">${meta.icon}</div>
<div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:${meta.text};"> <div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:${meta.text};">
${buildGameLabelChipHtml(meta.label, meta.accent)} ${buildGameLabelChipHtml(meta.label, meta.accent)}
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:12px;line-height:1.25;font-weight:700;min-width:200px;flex:1;"> <div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:${CHAT_NOTICE_BODY_FONT_SIZE};line-height:1.25;font-weight:700;min-width:200px;flex:1;">
<span>${parseBracketUsers(summary, meta.text)}</span> <span>${parseBracketUsers(summary, meta.text)}</span>
<span class="msg-time" style="font-size:10px;color:#94a3b8;font-weight:600;">(${timeStr})</span> <span class="msg-time" style="font-size:${CHAT_NOTICE_META_FONT_SIZE};color:#94a3b8;font-weight:600;">(${timeStr})</span>
</div> </div>
</div> </div>
</div> </div>
@@ -420,15 +428,15 @@ function buildQuizStartHtml(msg, timeStr) {
return ` return `
<div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:linear-gradient(135deg,#f5f3ff,#faf5ff);border:1px solid rgba(124,58,237,.16);box-shadow:0 4px 12px rgba(124,58,237,.07);overflow:hidden;"> <div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:linear-gradient(135deg,#f5f3ff,#faf5ff);border:1px solid rgba(124,58,237,.16);box-shadow:0 4px 12px rgba(124,58,237,.07);overflow:hidden;">
<div style="width:23px;height:23px;border-radius:7px;background:linear-gradient(135deg,#7c3aed,#a78bfa);display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;box-shadow:0 2px 6px rgba(124,58,237,.16);flex-shrink:0;">🧩</div> <div style="width:23px;height:23px;border-radius:7px;background:linear-gradient(135deg,#7c3aed,#a78bfa);display:flex;align-items:center;justify-content:center;color:#fff;font-size:${CHAT_NOTICE_ICON_FONT_SIZE};box-shadow:0 2px 6px rgba(124,58,237,.16);flex-shrink:0;">🧩</div>
<div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:#312e81;"> <div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:#312e81;">
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex-shrink:0;">${buildQuizBadgeHtml(msg)}</div> <div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex-shrink:0;">${buildQuizBadgeHtml(msg)}</div>
<div data-quiz-inline-text style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:12px;line-height:1.25;font-weight:700;min-width:200px;flex:1;"> <div data-quiz-inline-text style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:${CHAT_NOTICE_BODY_FONT_SIZE};line-height:1.25;font-weight:700;min-width:200px;flex:1;">
<span>${safeHint}</span> <span>${safeHint}</span>
<span class="msg-time" style="font-size:10px;color:#94a3b8;">(${timeStr})</span> <span class="msg-time" style="font-size:${CHAT_NOTICE_META_FONT_SIZE};color:#94a3b8;">(${timeStr})</span>
<span data-quiz-inline-action-anchor></span> <span data-quiz-inline-action-anchor></span>
</div> </div>
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;color:#6d28d9;font-size:10px;flex-shrink:0;margin-left:auto;"> <div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;color:#6d28d9;font-size:${CHAT_NOTICE_META_FONT_SIZE};flex-shrink:0;margin-left:auto;">
<span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(124,58,237,.10);white-space:nowrap;">💰 ${quizMeta.rewardGold} 金币</span> <span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(124,58,237,.10);white-space:nowrap;">💰 ${quizMeta.rewardGold} 金币</span>
<span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(124,58,237,.10);white-space:nowrap;">⭐ ${quizMeta.rewardExp} 经验</span> <span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px rgba(124,58,237,.10);white-space:nowrap;">⭐ ${quizMeta.rewardExp} 经验</span>
</div> </div>
@@ -478,15 +486,15 @@ function buildQuizResultHtml(msg, timeStr) {
return ` return `
<div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:${accentBackground};border:1px solid ${accentBorder};box-shadow:0 4px 12px rgba(15,23,42,.045);overflow:hidden;"> <div style="display:flex;align-items:center;gap:7px;padding:5px 9px;border-radius:11px;background:${accentBackground};border:1px solid ${accentBorder};box-shadow:0 4px 12px rgba(15,23,42,.045);overflow:hidden;">
<div style="width:23px;height:23px;border-radius:7px;background:${iconBackground};display:flex;align-items:center;justify-content:center;color:#fff;font-size:13px;box-shadow:0 2px 6px ${accentBorder};flex-shrink:0;">${icon}</div> <div style="width:23px;height:23px;border-radius:7px;background:${iconBackground};display:flex;align-items:center;justify-content:center;color:#fff;font-size:${CHAT_NOTICE_ICON_FONT_SIZE};box-shadow:0 2px 6px ${accentBorder};flex-shrink:0;">${icon}</div>
<div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:${textColor};"> <div style="min-width:0;flex:1;display:flex;align-items:center;gap:7px;flex-wrap:wrap;color:${textColor};">
<div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex-shrink:0;">${buildQuizBadgeHtml(msg, badgeColor)}</div> <div style="display:flex;align-items:center;gap:7px;flex-wrap:wrap;flex-shrink:0;">${buildQuizBadgeHtml(msg, badgeColor)}</div>
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:12px;line-height:1.25;font-weight:700;min-width:200px;flex:1;"> <div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:${CHAT_NOTICE_BODY_FONT_SIZE};line-height:1.25;font-weight:700;min-width:200px;flex:1;">
<span>${summaryHtml}</span> <span>${summaryHtml}</span>
<span class="msg-time" style="font-size:10px;color:#94a3b8;">(${timeStr})</span> <span class="msg-time" style="font-size:${CHAT_NOTICE_META_FONT_SIZE};color:#94a3b8;">(${timeStr})</span>
</div> </div>
${isAnsweredResult ? ` ${isAnsweredResult ? `
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;color:${textColor};font-size:10px;flex-shrink:0;margin-left:auto;"> <div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;color:${textColor};font-size:${CHAT_NOTICE_META_FONT_SIZE};flex-shrink:0;margin-left:auto;">
<span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px ${isAnsweredResult ? "rgba(124,58,237,.10)" : "rgba(217,119,6,.12)"};white-space:nowrap;">💰 ${quizMeta.rewardGold} 金币</span> <span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px ${isAnsweredResult ? "rgba(124,58,237,.10)" : "rgba(217,119,6,.12)"};white-space:nowrap;">💰 ${quizMeta.rewardGold} 金币</span>
<span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px ${isAnsweredResult ? "rgba(124,58,237,.10)" : "rgba(217,119,6,.12)"};white-space:nowrap;">⭐ ${quizMeta.rewardExp} 经验</span> <span style="padding:1px 6px;border-radius:999px;background:#ffffff;box-shadow:inset 0 0 0 1px ${isAnsweredResult ? "rgba(124,58,237,.10)" : "rgba(217,119,6,.12)"};white-space:nowrap;">⭐ ${quizMeta.rewardExp} 经验</span>
</div> </div>
@@ -546,7 +554,7 @@ export function buildChatMessageContent(msg, fontColor, textColorClass) {
return ` return `
<span style="display:inline-flex; align-items:center; gap:6px; vertical-align:middle;"> <span style="display:inline-flex; align-items:center; gap:6px; vertical-align:middle;">
<span style="display:inline-flex; align-items:center; padding:4px 8px; border:1px dashed #94a3b8; border-radius:999px; background:#f8fafc; color:#64748b; font-size:12px;">🖼️ 图片已过期</span> <span style="display:inline-flex; align-items:center; padding:4px 8px; border:1px dashed #94a3b8; border-radius:999px; background:#f8fafc; color:#64748b; font-size:${CHAT_NOTICE_BUTTON_FONT_SIZE};">🖼️ 图片已过期</span>
${captionHtml} ${captionHtml}
</span> </span>
`; `;
@@ -682,16 +690,16 @@ export function appendMessage(msg, renderBatch = null) {
html = ` html = `
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;border-radius:14px;background:linear-gradient(135deg, ${accent}, #fbbf24);display:flex;align-items:center;justify-content:center;font-size:24px;box-shadow: 0 4px 12px ${accent}44; flex-shrink: 0;">${icon}</div> <div style="width:48px;height:48px;border-radius:14px;background:linear-gradient(135deg, ${accent}, #fbbf24);display:flex;align-items:center;justify-content:center;font-size:${CHAT_NOTICE_LARGE_ICON_FONT_SIZE};box-shadow: 0 4px 12px ${accent}44; flex-shrink: 0;">${icon}</div>
<div style="min-width:0;flex:1;"> <div style="min-width:0;flex:1;">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"> <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<span style="font-size:13px;font-weight:900;letter-spacing:.05em;color:${accent}; text-shadow: 0.5px 0.5px 0px rgba(0,0,0,0.05);">${typeLabel}</span> <span style="font-size:${CHAT_NOTICE_CHIP_FONT_SIZE};font-weight:900;letter-spacing:.05em;color:${accent}; text-shadow: 0.5px 0.5px 0px rgba(0,0,0,0.05);">${typeLabel}</span>
<span style="font-size:13px;color:#475569;font-weight:bold;">${levelName}</span> <span style="font-size:${CHAT_NOTICE_CHIP_FONT_SIZE};color:#475569;font-weight:bold;">${levelName}</span>
<span style="font-size:11px;color:#94a3b8;">(${timeStr})</span> <span style="font-size:${CHAT_NOTICE_META_FONT_SIZE};color:#94a3b8;">(${timeStr})</span>
</div> </div>
<div style="margin-top:4px;font-size:15px;line-height:1.6;color:#1e293b;font-weight:500;">${safeText}</div> <div style="margin-top:4px;font-size:${CHAT_NOTICE_BODY_FONT_SIZE};line-height:1.6;color:#1e293b;font-weight:500;">${safeText}</div>
</div> </div>
<div style="position:absolute; right:-10px; bottom:-10px; font-size:60px; opacity:0.05; transform:rotate(-15deg); pointer-events:none;">${icon}</div> <div style="position:absolute; right:-10px; bottom:-10px; font-size:${CHAT_NOTICE_DECOR_ICON_FONT_SIZE}; opacity:0.05; transform:rotate(-15deg); pointer-events:none;">${icon}</div>
</div> </div>
`; `;
timeStrOverride = true; timeStrOverride = true;
@@ -716,7 +724,7 @@ export function appendMessage(msg, renderBatch = null) {
} }
} }
const parsedBody = parseBracketUsers(bodyPart, "#1d4ed8"); const parsedBody = parseBracketUsers(bodyPart, "#1d4ed8");
html = `<div style="color: #1e40af;">💬 ${clickablePrefix}${parsedBody} <span style="color: #93c5fd; font-size: 11px; font-weight: normal;">(${timeStr})</span></div>`; html = `<div style="color: #1e40af;">💬 ${clickablePrefix}${parsedBody} <span style="color: #93c5fd; font-size: ${CHAT_NOTICE_META_FONT_SIZE}; font-weight: normal;">(${timeStr})</span></div>`;
timeStrOverride = true; timeStrOverride = true;
} else if (SYSTEM_USERS.includes(msg.from_user)) { } else if (SYSTEM_USERS.includes(msg.from_user)) {
if (msg.from_user === "系统公告") { if (msg.from_user === "系统公告") {
@@ -727,7 +735,7 @@ export function appendMessage(msg, renderBatch = null) {
div.style.cssText = div.style.cssText =
"background: linear-gradient(135deg, #fef2f2, #fff1f2); border: 2px solid #ef4444; border-radius: 8px; padding: 10px 14px; margin: 6px 0; box-shadow: 0 3px 8px rgba(239,68,68,0.16);"; "background: linear-gradient(135deg, #fef2f2, #fff1f2); border: 2px solid #ef4444; border-radius: 8px; padding: 10px 14px; margin: 6px 0; box-shadow: 0 3px 8px rgba(239,68,68,0.16);";
const parsedContent = parseBracketUsers(msg.content, "#dc2626"); const parsedContent = parseBracketUsers(msg.content, "#dc2626");
html = `<div style="font-size: 18px; line-height: 1.75; font-weight: 800; color: #dc2626;">${parsedContent} <span style="color: #999; font-size: 14px; font-weight: 500;">(${timeStr})</span></div>`; html = `<div style="font-size: ${CHAT_NOTICE_BODY_FONT_SIZE}; line-height: 1.75; font-weight: 800; color: #dc2626;">${parsedContent} <span style="color: #999; font-size: ${CHAT_NOTICE_META_FONT_SIZE}; font-weight: 500;">(${timeStr})</span></div>`;
timeStrOverride = true; timeStrOverride = true;
} }
} else if (msg.from_user === "系统传音") { } else if (msg.from_user === "系统传音") {
@@ -748,13 +756,13 @@ export function appendMessage(msg, renderBatch = null) {
"background:linear-gradient(135deg,#fff7ed,#fffbeb);border:1px solid rgba(245,158,11,.28);border-left:4px solid #f59e0b;border-radius:12px;padding:8px 12px;margin:4px 0;box-shadow:0 10px 24px rgba(245,158,11,.14);"; "background:linear-gradient(135deg,#fff7ed,#fffbeb);border:1px solid rgba(245,158,11,.28);border-left:4px solid #f59e0b;border-radius:12px;padding:8px 12px;margin:4px 0;box-shadow:0 10px 24px rgba(245,158,11,.14);";
html = ` html = `
<div style="display:flex;align-items:flex-start;gap:10px;"> <div style="display:flex;align-items:flex-start;gap:10px;">
<div style="width:38px;height:38px;border-radius:12px;background:linear-gradient(135deg,#f59e0b,#f97316);display:flex;align-items:center;justify-content:center;color:#fff;font-size:20px;box-shadow:0 8px 18px rgba(249,115,22,.22);flex-shrink:0;">📣</div> <div style="width:38px;height:38px;border-radius:12px;background:linear-gradient(135deg,#f59e0b,#f97316);display:flex;align-items:center;justify-content:center;color:#fff;font-size:${CHAT_NOTICE_LARGE_ICON_FONT_SIZE};box-shadow:0 8px 18px rgba(249,115,22,.22);flex-shrink:0;">📣</div>
<div style="min-width:0;flex:1;"> <div style="min-width:0;flex:1;">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;"> <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
${buildQuizBadgeHtml(msg, "#d97706")} ${buildQuizBadgeHtml(msg, "#d97706")}
<span class="msg-time">(${timeStr})</span> <span class="msg-time">(${timeStr})</span>
</div> </div>
<div style="margin-top:7px;color:#9a3412;font-size:15px;font-weight:800;line-height:1.75;">${parseBracketUsers(content, "#b45309")}</div> <div style="margin-top:7px;color:#9a3412;font-size:${CHAT_NOTICE_BODY_FONT_SIZE};font-weight:800;line-height:1.75;">${parseBracketUsers(content, "#b45309")}</div>
</div> </div>
</div> </div>
`; `;
@@ -792,7 +800,7 @@ export function appendMessage(msg, renderBatch = null) {
} else if (msg.is_secret) { } else if (msg.is_secret) {
if (msg.from_user === "系统") { if (msg.from_user === "系统") {
div.style.cssText = div.style.cssText =
"background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px;padding:3px 8px;margin:2px 0;font-size:12px;"; "background:#f0fdf4;border-left:3px solid #16a34a;border-radius:4px;padding:3px 8px;margin:2px 0;";
html = `<span style="color:#16a34a;font-weight:bold;">📢 系统:</span><span style="color:#15803d;">${msg.content}</span>`; html = `<span style="color:#16a34a;font-weight:bold;">📢 系统:</span><span style="color:#15803d;">${msg.content}</span>`;
} else { } else {
const fromHtml = clickableUser(msg.from_user, "#cc00cc", nameClass); const fromHtml = clickableUser(msg.from_user, "#cc00cc", nameClass);
@@ -1059,7 +1067,7 @@ export function flushQueuedChatMessages() {
const notice = document.createElement("div"); const notice = document.createElement("div");
notice.className = "msg-line msg-burst-notice"; notice.className = "msg-line msg-burst-notice";
notice.style.cssText = notice.style.cssText =
"text-align:center;padding:6px 0;margin:4px 0;font-size:12px;color:#94a3b8;border-top:1px dashed #d1d5db;border-bottom:1px dashed #d1d5db;"; `text-align:center;padding:6px 0;margin:4px 0;font-size:${CHAT_NOTICE_META_FONT_SIZE};color:#94a3b8;border-top:1px dashed #d1d5db;border-bottom:1px dashed #d1d5db;`;
notice.textContent = `⏫ 省略了 ${dropped} 条系统通知`; notice.textContent = `⏫ 省略了 ${dropped} 条系统通知`;
container.appendChild(notice); container.appendChild(notice);
} }
+16 -5
View File
@@ -1,5 +1,7 @@
// 聊天室偏好与每日状态工具,承接从 Blade 内联脚本迁移出的纯数据规整逻辑。 // 聊天室偏好与每日状态工具,承接从 Blade 内联脚本迁移出的纯数据规整逻辑。
import { CHAT_FONT_SIZE_STORAGE_KEY, normalizeChatFontSize } from "./font-size.js";
export const BLOCKABLE_SYSTEM_SENDERS = ["钓鱼播报", "猜成语", "星海小博士", "百家乐", "跑马", "神秘箱子", "五子棋", "老虎机", "双色球彩票"]; export const BLOCKABLE_SYSTEM_SENDERS = ["钓鱼播报", "猜成语", "星海小博士", "百家乐", "跑马", "神秘箱子", "五子棋", "老虎机", "双色球彩票"];
export const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = "chat_blocked_system_senders"; export const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = "chat_blocked_system_senders";
export const CHAT_SOUND_MUTED_STORAGE_KEY = "chat_sound_muted"; export const CHAT_SOUND_MUTED_STORAGE_KEY = "chat_sound_muted";
@@ -12,7 +14,7 @@ let blockMenuEventsBound = false;
* *
* @param {Record<string, unknown>|null|undefined} raw * @param {Record<string, unknown>|null|undefined} raw
* @param {string[]} blockableSystemSenders * @param {string[]} blockableSystemSenders
* @returns {{blocked_system_senders:string[],sound_muted:boolean}} * @returns {{blocked_system_senders:string[],sound_muted:boolean,font_size:number|null}}
*/ */
export function normalizeChatPreferences(raw, blockableSystemSenders = BLOCKABLE_SYSTEM_SENDERS) { export function normalizeChatPreferences(raw, blockableSystemSenders = BLOCKABLE_SYSTEM_SENDERS) {
// 服务端或旧本地缓存可能包含已下架发送者,规整时只保留当前白名单。 // 服务端或旧本地缓存可能包含已下架发送者,规整时只保留当前白名单。
@@ -23,6 +25,7 @@ export function normalizeChatPreferences(raw, blockableSystemSenders = BLOCKABLE
return { return {
blocked_system_senders: Array.from(new Set(blocked)), blocked_system_senders: Array.from(new Set(blocked)),
sound_muted: Boolean(raw?.sound_muted), sound_muted: Boolean(raw?.sound_muted),
font_size: normalizeChatFontSize(raw?.font_size),
}; };
} }
@@ -454,17 +457,19 @@ export function bindBlockMenuControls() {
/** /**
* 当前登录账号没有服务端偏好时,判断是否需要迁移旧本地偏好。 * 当前登录账号没有服务端偏好时,判断是否需要迁移旧本地偏好。
* *
* @param {{blocked_system_senders?:string[],sound_muted?:boolean}} serverPreferences * @param {{blocked_system_senders?:string[],sound_muted?:boolean,font_size?:number|null}} serverPreferences
* @param {string[]} localBlockedSenders * @param {string[]} localBlockedSenders
* @param {boolean} localMuted * @param {boolean} localMuted
* @returns {boolean} * @returns {boolean}
*/ */
export function shouldMigrateLocalChatPreferences(serverPreferences, localBlockedSenders, localMuted) { export function shouldMigrateLocalChatPreferences(serverPreferences, localBlockedSenders, localMuted) {
// 只有服务端尚无偏好时才迁移旧本地设置,避免覆盖已同步的账号配置。 // 只有服务端尚无偏好时才迁移旧本地设置,避免覆盖已同步的账号配置。
const localFontSize = normalizeChatFontSize(localStorage.getItem(CHAT_FONT_SIZE_STORAGE_KEY));
const hasServerPreferences = (serverPreferences?.blocked_system_senders || []).length > 0 const hasServerPreferences = (serverPreferences?.blocked_system_senders || []).length > 0
|| Boolean(serverPreferences?.sound_muted); || Boolean(serverPreferences?.sound_muted)
|| normalizeChatFontSize(serverPreferences?.font_size) !== null;
return !hasServerPreferences && (localBlockedSenders.length > 0 || localMuted); return !hasServerPreferences && (localBlockedSenders.length > 0 || localMuted || localFontSize !== null);
} }
/** /**
@@ -580,13 +585,19 @@ export function resolveBlockedSystemSenderKey(msg) {
/** /**
* 构建当前聊天室偏好快照。 * 构建当前聊天室偏好快照。
* *
* @returns {{blocked_system_senders:string[],sound_muted:boolean}} * @returns {{blocked_system_senders:string[],sound_muted:boolean,font_size:number|null}}
*/ */
export function buildChatPreferencesPayload() { export function buildChatPreferencesPayload() {
const state = window.chatState; const state = window.chatState;
const selector = document.getElementById("font_size_select");
const fontSize = normalizeChatFontSize(selector?.value)
?? normalizeChatFontSize(localStorage.getItem(CHAT_FONT_SIZE_STORAGE_KEY))
?? normalizeChatFontSize(window.chatContext?.chatPreferences?.font_size);
return { return {
blocked_system_senders: state ? Array.from(state.blockedSystemSenders) : [], blocked_system_senders: state ? Array.from(state.blockedSystemSenders) : [],
sound_muted: isSoundMuted(), sound_muted: isSoundMuted(),
font_size: fontSize,
}; };
} }
+6 -4
View File
@@ -9,6 +9,8 @@ let currentRoundId = 0;
let currentRoomId = 0; let currentRoomId = 0;
let currentQuizType = "idiom"; let currentQuizType = "idiom";
const QUIZ_TYPES = ["idiom", "brain_teaser"]; const QUIZ_TYPES = ["idiom", "brain_teaser"];
const QUIZ_INLINE_BUTTON_FONT_SIZE = "0.82em";
const QUIZ_INLINE_META_FONT_SIZE = "0.78em";
/** /**
* 兼容新旧字段,提取前端统一使用的猜谜活动回合信息。 * 兼容新旧字段,提取前端统一使用的猜谜活动回合信息。
@@ -141,7 +143,7 @@ function buildIdiomAnswerButton(roundId, hint, rewardGold, rewardExp, typeLabel,
btn.textContent = "🎯 立即答题"; btn.textContent = "🎯 立即答题";
btn.style.cssText = btn.style.cssText =
"display:inline-flex;align-items:center;gap:4px;padding:2px 9px;background:linear-gradient(135deg,#7c3aed,#a78bfa);" + "display:inline-flex;align-items:center;gap:4px;padding:2px 9px;background:linear-gradient(135deg,#7c3aed,#a78bfa);" +
"color:#fff;border:1px solid #7c3aed;border-radius:999px;font-size:11px;cursor:pointer;" + `color:#fff;border:1px solid #7c3aed;border-radius:999px;font-size:${QUIZ_INLINE_BUTTON_FONT_SIZE};cursor:pointer;` +
"font-weight:700;line-height:1;vertical-align:middle;box-shadow:0 2px 6px rgba(124,58,237,.14);"; "font-weight:700;line-height:1;vertical-align:middle;box-shadow:0 2px 6px rgba(124,58,237,.14);";
return btn; return btn;
@@ -195,7 +197,7 @@ function syncQuizWinnerLabel(button, winnerUsername = "") {
const winnerLabel = existingLabel || document.createElement("span"); const winnerLabel = existingLabel || document.createElement("span");
winnerLabel.dataset.quizWinnerLabel = String(button.dataset.quizAnswerBtn || button.dataset.idiomAnswerBtn || "0"); winnerLabel.dataset.quizWinnerLabel = String(button.dataset.quizAnswerBtn || button.dataset.idiomAnswerBtn || "0");
winnerLabel.textContent = `答对:${winnerUsername}`; winnerLabel.textContent = `答对:${winnerUsername}`;
winnerLabel.style.cssText = "margin-left:6px;font-size:11px;line-height:1.2;color:#64748b;font-weight:700;white-space:nowrap;"; winnerLabel.style.cssText = `margin-left:6px;font-size:${QUIZ_INLINE_META_FONT_SIZE};line-height:1.2;color:#64748b;font-weight:700;white-space:nowrap;`;
if (!existingLabel) { if (!existingLabel) {
button.insertAdjacentElement("afterend", winnerLabel); button.insertAdjacentElement("afterend", winnerLabel);
@@ -216,7 +218,7 @@ export function disableIdiomAnswerButtons(roundId = 0, endedText = "本回合已
button.style.boxShadow = "none"; button.style.boxShadow = "none";
button.style.opacity = ".92"; button.style.opacity = ".92";
button.style.padding = "2px 9px"; button.style.padding = "2px 9px";
button.style.fontSize = "11px"; button.style.fontSize = QUIZ_INLINE_BUTTON_FONT_SIZE;
button.style.lineHeight = "1"; button.style.lineHeight = "1";
button.title = endedText; button.title = endedText;
button.textContent = "已结束"; button.textContent = "已结束";
@@ -242,7 +244,7 @@ function syncQuizAnswerButtons(activeRoundIds) {
button.style.boxShadow = "0 2px 6px rgba(124,58,237,.14)"; button.style.boxShadow = "0 2px 6px rgba(124,58,237,.14)";
button.style.opacity = "1"; button.style.opacity = "1";
button.style.padding = "2px 9px"; button.style.padding = "2px 9px";
button.style.fontSize = "11px"; button.style.fontSize = QUIZ_INLINE_BUTTON_FONT_SIZE;
button.style.lineHeight = "1"; button.style.lineHeight = "1";
button.title = ""; button.title = "";
button.textContent = "🎯 立即答题"; button.textContent = "🎯 立即答题";
+56 -6
View File
@@ -8,10 +8,16 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Redis; use Illuminate\Support\Facades\Redis;
use Tests\TestCase; use Tests\TestCase;
/**
* 类功能:覆盖钓鱼小游戏抛竿、收竿和令牌校验流程。
*/
class FishingControllerTest extends TestCase class FishingControllerTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
/**
* 初始化钓鱼配置并清理 Redis 状态。
*/
protected function setUp(): void protected function setUp(): void
{ {
parent::setUp(); parent::setUp();
@@ -32,7 +38,10 @@ class FishingControllerTest extends TestCase
); );
} }
public function test_can_cast_rod() /**
* 方法功能:验证用户金币足够时可以正常抛竿并扣除费用。
*/
public function test_can_cast_rod(): void
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = User::factory()->create(['jjb' => 10]); $user = User::factory()->create(['jjb' => 10]);
@@ -48,7 +57,10 @@ class FishingControllerTest extends TestCase
$this->assertEquals(5, $user->fresh()->jjb); $this->assertEquals(5, $user->fresh()->jjb);
} }
public function test_cannot_cast_when_on_cooldown() /**
* 方法功能:验证冷却期间不能重复抛竿。
*/
public function test_cannot_cast_when_on_cooldown(): void
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = User::factory()->create(['jjb' => 10]); $user = User::factory()->create(['jjb' => 10]);
@@ -62,7 +74,10 @@ class FishingControllerTest extends TestCase
]); ]);
} }
public function test_cannot_cast_without_enough_gold() /**
* 方法功能:验证金币不足时不能抛竿。
*/
public function test_cannot_cast_without_enough_gold(): void
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = User::factory()->create(['jjb' => 2]); $user = User::factory()->create(['jjb' => 2]);
@@ -75,13 +90,15 @@ class FishingControllerTest extends TestCase
]); ]);
} }
public function test_can_reel_after_waiting() /**
* 方法功能:验证等待完成后携带正确令牌可以成功收竿。
*/
public function test_can_reel_after_waiting(): void
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = User::factory()->create(['jjb' => 10]); $user = User::factory()->create(['jjb' => 10]);
$token = 'test-token'; $token = 'test-token';
$waitTime = 0; // Set to 0 so we can test immediately
Redis::set("fishing:token:{$user->id}", json_encode([ Redis::set("fishing:token:{$user->id}", json_encode([
'token' => $token, 'token' => $token,
@@ -102,7 +119,10 @@ class FishingControllerTest extends TestCase
$this->assertTrue((bool) Redis::exists("fishing:cd:{$user->id}")); $this->assertTrue((bool) Redis::exists("fishing:cd:{$user->id}"));
} }
public function test_cannot_reel_with_invalid_token() /**
* 方法功能:验证错误令牌不能收竿。
*/
public function test_cannot_reel_with_invalid_token(): void
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = User::factory()->create(['jjb' => 10]); $user = User::factory()->create(['jjb' => 10]);
@@ -122,4 +142,34 @@ class FishingControllerTest extends TestCase
'status' => 'error', 'status' => 'error',
]); ]);
} }
/**
* 方法功能:验证已有未收竿令牌时再次抛竿会恢复原会话且不会重复扣金币。
*/
public function test_cast_restores_active_token_without_charging_again(): void
{
/** @var \App\Models\User $user */
$user = User::factory()->create(['jjb' => 10]);
$tokenKey = "fishing:token:{$user->id}";
$payload = json_encode([
'token' => 'active-token',
'cast_at' => time(),
'wait_time' => 10,
]);
Redis::setex($tokenKey, 20, $payload);
$response = $this->actingAs($user)->postJson(route('fishing.cast', ['id' => 1]));
$response->assertStatus(200);
$response->assertJson([
'status' => 'success',
'token' => 'active-token',
'cost' => 0,
'restored' => true,
]);
$this->assertSame(10, $user->fresh()->jjb);
$this->assertSame($payload, Redis::get($tokenKey));
}
} }
+47 -2
View File
@@ -417,7 +417,7 @@ class UserControllerTest extends TestCase
} }
/** /**
* 测试可以保存聊天室屏蔽禁音偏好。 * 测试可以保存聊天室屏蔽禁音与字号偏好。
*/ */
public function test_can_update_chat_preferences(): void public function test_can_update_chat_preferences(): void
{ {
@@ -428,6 +428,7 @@ class UserControllerTest extends TestCase
$response = $this->actingAs($user)->putJson('/user/chat-preferences', [ $response = $this->actingAs($user)->putJson('/user/chat-preferences', [
'blocked_system_senders' => ['钓鱼播报', '神秘箱子', '跑马'], 'blocked_system_senders' => ['钓鱼播报', '神秘箱子', '跑马'],
'sound_muted' => true, 'sound_muted' => true,
'font_size' => 22,
]); ]);
$response->assertOk() $response->assertOk()
@@ -435,15 +436,59 @@ class UserControllerTest extends TestCase
->assertJsonPath('data.blocked_system_senders.0', '钓鱼播报') ->assertJsonPath('data.blocked_system_senders.0', '钓鱼播报')
->assertJsonPath('data.blocked_system_senders.1', '神秘箱子') ->assertJsonPath('data.blocked_system_senders.1', '神秘箱子')
->assertJsonPath('data.blocked_system_senders.2', '跑马') ->assertJsonPath('data.blocked_system_senders.2', '跑马')
->assertJsonPath('data.sound_muted', true); ->assertJsonPath('data.sound_muted', true)
->assertJsonPath('data.font_size', 22);
$user->refresh(); $user->refresh();
$this->assertEquals([ $this->assertEquals([
'blocked_system_senders' => ['钓鱼播报', '神秘箱子', '跑马'], 'blocked_system_senders' => ['钓鱼播报', '神秘箱子', '跑马'],
'sound_muted' => true, 'sound_muted' => true,
'font_size' => 22,
], $user->chat_preferences); ], $user->chat_preferences);
} }
/**
* 测试保存屏蔽偏好时未提交字号不会清空已有字号。
*/
public function test_chat_preferences_keep_existing_font_size_when_missing_from_payload(): void
{
$user = User::factory()->create([
'chat_preferences' => [
'blocked_system_senders' => [],
'sound_muted' => false,
'font_size' => 18,
],
]);
$response = $this->actingAs($user)->putJson('/user/chat-preferences', [
'blocked_system_senders' => ['钓鱼播报'],
'sound_muted' => false,
]);
$response->assertOk()
->assertJsonPath('data.font_size', 18);
$user->refresh();
$this->assertSame(18, $user->chat_preferences['font_size']);
}
/**
* 测试非法聊天室字号会返回校验错误。
*/
public function test_invalid_chat_font_size_returns_validation_error(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->putJson('/user/chat-preferences', [
'blocked_system_senders' => [],
'sound_muted' => false,
'font_size' => 31,
]);
$response->assertStatus(422)
->assertJsonValidationErrors('font_size');
}
/** /**
* 测试猜谜活动新文案会兼容落回旧的屏蔽键。 * 测试猜谜活动新文案会兼容落回旧的屏蔽键。
*/ */