修复聊天室字号偏好和游戏通知显示
This commit is contained in:
@@ -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).'】完成今日签到,连续签到 '
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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,随机计算钓鱼结果,更新经验/金币,广播到聊天室。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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。',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>'
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// 领取成功的公屏格式复用百家乐参与播报风格,保证聊天室感知一致。
|
// 领取成功的公屏格式复用百家乐参与播报风格,保证聊天室感知一致。
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 底部输入工具栏 ─────────────────────────────── */
|
/* ── 底部输入工具栏 ─────────────────────────────── */
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 底部输入工具栏 ─────────────────────────────── */
|
/* ── 底部输入工具栏 ─────────────────────────────── */
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>`;
|
||||||
|
|||||||
@@ -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>`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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?.();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = "🎯 立即答题";
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 测试猜谜活动新文案会兼容落回旧的屏蔽键。
|
* 测试猜谜活动新文案会兼容落回旧的屏蔽键。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user