优化屏蔽,可以保存状态

This commit is contained in:
2026-04-14 22:48:29 +08:00
parent 7255d50966
commit 1a39ddd725
8 changed files with 279 additions and 5 deletions
+26 -1
View File
@@ -20,6 +20,7 @@ namespace App\Http\Controllers;
use App\Events\UserKicked;
use App\Events\UserMuted;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\UpdateChatPreferencesRequest;
use App\Http\Requests\UpdateProfileRequest;
use App\Models\Room;
use App\Models\Sysparam;
@@ -109,7 +110,6 @@ class UserController extends Controller
$data['vip']['Name'] = $targetUser->vipName();
$data['vip']['Icon'] = $targetUser->vipIcon();
// 拥有封禁IPlevel_banip)或踢人以上权限的管理,可以查看IP和归属地
$levelBanIp = (int) Sysparam::getValue('level_banip', '15');
if ($operator && $operator->user_level >= $levelBanIp) {
@@ -203,6 +203,31 @@ class UserController extends Controller
return response()->json(['status' => 'success', 'message' => '资料更新成功。']);
}
/**
* 保存聊天室屏蔽与禁音偏好。
*/
public function updateChatPreferences(UpdateChatPreferencesRequest $request): JsonResponse
{
$user = Auth::user();
$data = $request->validated();
$preferences = [
// 去重并重建索引,保持存储结构稳定,便于后续继续扩展其它屏蔽项。
'blocked_system_senders' => array_values(array_unique($data['blocked_system_senders'] ?? [])),
'sound_muted' => (bool) $data['sound_muted'],
];
$user->update([
'chat_preferences' => $preferences,
]);
return response()->json([
'status' => 'success',
'message' => '聊天室偏好已保存。',
'data' => $preferences,
]);
}
/**
* 修改密码 (对应 chpasswd.asp)
*/
@@ -0,0 +1,58 @@
<?php
/**
* 文件功能:聊天室偏好设置验证器
* 负责校验用户提交的屏蔽播报与禁音配置。
*/
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
/**
* 聊天室偏好设置验证器
* 仅允许提交白名单内的屏蔽项与布尔型禁音状态。
*/
class UpdateChatPreferencesRequest extends FormRequest
{
/**
* 允许已登录用户保存自己的聊天室偏好。
*/
public function authorize(): bool
{
return $this->user() !== null;
}
/**
* 获取聊天室偏好的验证规则。
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'blocked_system_senders' => ['nullable', 'array'],
'blocked_system_senders.*' => [
'string',
Rule::in(['钓鱼播报', '星海小博士', '百家乐', '跑马']),
],
'sound_muted' => ['required', 'boolean'],
];
}
/**
* 获取聊天室偏好的中文错误提示。
*
* @return array<string, string>
*/
public function messages(): array
{
return [
'blocked_system_senders.array' => '屏蔽设置格式无效。',
'blocked_system_senders.*.in' => '存在不支持的屏蔽项目。',
'sound_muted.required' => '请传入禁音状态。',
'sound_muted.boolean' => '禁音状态格式无效。',
];
}
}
+2
View File
@@ -49,6 +49,7 @@ class User extends Authenticatable
'custom_leave_message',
'custom_join_effect',
'custom_leave_effect',
'chat_preferences',
'user_level',
'inviter_id',
'room_id',
@@ -101,6 +102,7 @@ class User extends Authenticatable
'sj' => 'datetime',
'q3_time' => 'datetime',
'has_received_new_gift' => 'boolean',
'chat_preferences' => 'array',
];
}
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* 为用户表增加聊天室偏好配置字段。
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->json('chat_preferences')->nullable()->after('custom_leave_effect')->comment('聊天室屏蔽与禁音偏好配置');
});
}
/**
* 回滚用户表中的聊天室偏好配置字段。
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('chat_preferences');
});
}
};
+2
View File
@@ -94,8 +94,10 @@
revokeUrl: "{{ route('chat.appoint.revoke') }}",
rewardUrl: "{{ route('command.reward') }}",
rewardQuotaUrl: "{{ route('command.reward_quota') }}",
chatPreferencesUrl: "{{ route('user.update_chat_preferences') }}",
userJjb: {{ (int) $user->jjb }}, // 当前用户金币(求婚前金额预检查用)
myGold: {{ (int) $user->jjb }}, // 赠金币面板显示余额用(赠送成功后前端更新)
chatPreferences: @json($user->chat_preferences ?? []),
// ─── 婚姻系统 ──────────────────────────────
minWeddingCost: {{ (int) \App\Models\WeddingTier::where('is_active', true)->orderBy('amount')->value('amount') ?? 0 }},
+115 -4
View File
@@ -38,6 +38,7 @@
const onlineCount = document.getElementById('online-count');
const onlineCountBottom = document.getElementById('online-count-bottom');
const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = 'chat_blocked_system_senders';
const CHAT_SOUND_MUTED_STORAGE_KEY = 'chat_sound_muted';
const BLOCKABLE_SYSTEM_SENDERS = ['钓鱼播报', '星海小博士', '百家乐', '跑马'];
// ── 消息区:手机端双触发打开用户名片(PC 端靠 ondblclick 内联属性)──
@@ -70,7 +71,26 @@
let onlineUsers = {};
let autoScroll = true;
let _maxMsgId = 0; // 记录当前收到的最大消息 ID
let blockedSystemSenders = new Set(loadBlockedSystemSenders());
const initialChatPreferences = normalizeChatPreferences(window.chatContext?.chatPreferences || {});
let blockedSystemSenders = new Set(initialChatPreferences.blocked_system_senders);
/**
* 规整聊天室偏好对象,过滤非法配置并补齐默认值。
*
* @param {Record<string, any>} raw 原始偏好对象
* @returns {Object}
*/
function normalizeChatPreferences(raw) {
const blocked = Array.isArray(raw?.blocked_system_senders)
? raw.blocked_system_senders.filter(sender => BLOCKABLE_SYSTEM_SENDERS.includes(sender))
: [];
return {
// 默认所有用户都处于“不屏蔽”的开放状态,只有显式勾选的项目才会进入该列表。
blocked_system_senders: Array.from(new Set(blocked)),
sound_muted: Boolean(raw?.sound_muted),
};
}
/**
* localStorage 读取已屏蔽的系统播报发送者列表。
@@ -101,6 +121,77 @@
);
}
/**
* 判断当前禁音开关是否处于打开状态。
*
* @returns {boolean}
*/
function isSoundMuted() {
const muteCheckbox = document.getElementById('sound_muted');
if (muteCheckbox) {
return Boolean(muteCheckbox.checked);
}
return localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1';
}
/**
* 获取当前聊天室偏好快照。
*
* @returns {Object}
*/
function buildChatPreferencesPayload() {
return {
blocked_system_senders: Array.from(blockedSystemSenders),
sound_muted: isSoundMuted(),
};
}
/**
* 将聊天室偏好写入本地缓存,供刷新前快速恢复与迁移兜底。
*/
function persistChatPreferencesToLocal() {
persistBlockedSystemSenders();
localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, isSoundMuted() ? '1' : '0');
}
/**
* 将当前聊天室偏好保存到当前登录账号。
*/
async function saveChatPreferences() {
const payload = buildChatPreferencesPayload();
persistChatPreferencesToLocal();
if (!window.chatContext?.chatPreferencesUrl) {
return;
}
try {
const response = await fetch(window.chatContext.chatPreferencesUrl, {
method: 'PUT',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content ?? '',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error('save chat preferences failed');
}
const data = await response.json();
if (data?.status === 'success') {
window.chatContext.chatPreferences = normalizeChatPreferences(data.data || payload);
}
} catch (error) {
console.error('聊天室偏好保存失败:', error);
}
}
/**
* 同步屏蔽菜单中的复选框状态。
*/
@@ -237,6 +328,7 @@
persistBlockedSystemSenders();
syncBlockedSystemSenderCheckboxes();
void saveChatPreferences();
}
syncBlockedSystemSenderCheckboxes();
@@ -1589,11 +1681,28 @@
if (saved) {
applyFontSize(saved);
}
// 恢复禁音复选框状态
const muted = localStorage.getItem('chat_sound_muted') === '1';
const storedBlockedSystemSenders = loadBlockedSystemSenders();
const mutedFromLocal = localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === '1';
const hasServerPreferences = initialChatPreferences.blocked_system_senders.length > 0 || initialChatPreferences.sound_muted;
const shouldMigrateLocalPreferences = !hasServerPreferences
&& (storedBlockedSystemSenders.length > 0 || mutedFromLocal);
if (shouldMigrateLocalPreferences) {
blockedSystemSenders = new Set(storedBlockedSystemSenders);
}
// 恢复禁音复选框状态;默认一律为未禁音。
const muted = shouldMigrateLocalPreferences ? mutedFromLocal : initialChatPreferences.sound_muted;
const muteChk = document.getElementById('sound_muted');
if (muteChk) muteChk.checked = muted;
syncBlockedSystemSenderCheckboxes();
if (shouldMigrateLocalPreferences) {
void saveChatPreferences();
} else {
persistChatPreferencesToLocal();
}
});
// ── 特效禁音开关 ─────────────────────────────────────────────────
@@ -1604,10 +1713,12 @@
* @param {boolean} muted true = 禁音,false = 开启声音
*/
function toggleSoundMute(muted) {
localStorage.setItem('chat_sound_muted', muted ? '1' : '0');
localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, muted ? '1' : '0');
if (muted && typeof EffectSounds !== 'undefined') {
EffectSounds.stop(); // 立即停止当前音效
}
void saveChatPreferences();
}
window.toggleSoundMute = toggleSoundMute;
+1
View File
@@ -85,6 +85,7 @@ Route::middleware(['chat.auth'])->group(function () {
// ---- 第七阶段:用户资料与特权管理 ----
Route::get('/user/{username}', [UserController::class, 'show'])->name('user.show');
Route::put('/user/profile', [UserController::class, 'updateProfile'])->name('user.update_profile');
Route::put('/user/chat-preferences', [UserController::class, 'updateChatPreferences'])->name('user.update_chat_preferences');
Route::post('/user/generate-wechat-code', [UserController::class, 'generateWechatCode'])->name('user.generate_wechat_code');
Route::post('/user/unbind-wechat', [UserController::class, 'unbindWechat'])->name('user.unbind_wechat');
Route::post('/user/send-email-code', [\App\Http\Controllers\Api\VerificationController::class, 'sendEmailCode'])->name('user.send_email_code');
+47
View File
@@ -1,5 +1,10 @@
<?php
/**
* 文件功能:用户控制器功能测试
* 覆盖个人资料、密码与聊天室偏好设置等关键接口。
*/
namespace Tests\Feature;
use App\Models\Room;
@@ -15,6 +20,9 @@ class UserControllerTest extends TestCase
{
use RefreshDatabase;
/**
* 初始化用户控制器测试所需的系统参数。
*/
protected function setUp(): void
{
parent::setUp();
@@ -27,6 +35,9 @@ class UserControllerTest extends TestCase
Sysparam::updateOrCreate(['alias' => 'smtp_enabled'], ['body' => '1']); // Allow email changing in tests
}
/**
* 测试可以查看用户资料卡接口。
*/
public function test_can_view_user_profile()
{
$user = User::factory()->create([
@@ -43,6 +54,9 @@ class UserControllerTest extends TestCase
->assertJsonPath('data.user_level', 10);
}
/**
* 测试不改邮箱时可以正常更新个人资料。
*/
public function test_can_update_profile_without_email_change()
{
$user = User::factory()->create([
@@ -67,6 +81,9 @@ class UserControllerTest extends TestCase
$this->assertEquals('new sign', $user->sign);
}
/**
* 测试改邮箱但未提交验证码时会被拒绝。
*/
public function test_cannot_update_email_without_verification_code()
{
$user = User::factory()->create([
@@ -87,6 +104,9 @@ class UserControllerTest extends TestCase
->assertJsonPath('message', '新邮箱需要验证码,请先获取并填写验证码。');
}
/**
* 测试提供有效验证码后可以成功修改邮箱。
*/
public function test_can_update_email_with_valid_code()
{
$user = User::factory()->create([
@@ -111,6 +131,33 @@ class UserControllerTest extends TestCase
$this->assertEquals('new@example.com', $user->email);
}
/**
* 测试可以保存聊天室屏蔽与禁音偏好。
*/
public function test_can_update_chat_preferences(): void
{
$user = User::factory()->create([
'chat_preferences' => null,
]);
$response = $this->actingAs($user)->putJson('/user/chat-preferences', [
'blocked_system_senders' => ['钓鱼播报', '跑马'],
'sound_muted' => true,
]);
$response->assertOk()
->assertJsonPath('status', 'success')
->assertJsonPath('data.blocked_system_senders.0', '钓鱼播报')
->assertJsonPath('data.blocked_system_senders.1', '跑马')
->assertJsonPath('data.sound_muted', true);
$user->refresh();
$this->assertEquals([
'blocked_system_senders' => ['钓鱼播报', '跑马'],
'sound_muted' => true,
], $user->chat_preferences);
}
public function test_can_change_password()
{
$user = User::factory()->create([