Files
chatroom/resources/views/admin/users/index.blade.php
lkddi 6fa42b90d5 功能:站长礼包系统(金币/经验双类型)+ 后台用户编辑权限收紧(仅 id=1 超管)
新增功能:
- 礼包系统:superlevel 站长可发 888 数量 10 份礼包,支持金币/经验双类型
- 发包前三按钮选择(金币礼包 / 经验礼包 / 取消),使用 chatBanner 弹窗
- 聊天室系统公告含「立即抢包」按钮,金币红色/经验紫色配色区分
- WebSocket 实时推送红包弹窗卡片至所有在线用户
- Redis LPOP 原子分发 + 数据库 unique 约束防重领,并发安全
- 弹窗打开自动拉取服务端最新状态(剩余数量/已领/过期实时刷新)
- 新增 GET /red-packet/{id}/status 状态查询接口
- 新增 CurrencySource::RED_PACKET_RECV / RED_PACKET_RECV_EXP 枚举
安全加固:
- 后台用户编辑/强杀按钮仅 id=1 超管可见(前端隐藏 + 后端 403 双重拦截)
2026-03-01 22:20:54 +08:00

345 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('admin.layouts.app')
@section('title', '用户检索与管理')
@section('content')
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="userEditor()">
<div class="p-6 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
<form action="{{ route('admin.users.index') }}" method="GET" class="flex gap-2">
<input type="text" name="username" value="{{ request('username') }}" placeholder="搜索用户名..."
class="px-3 py-1.5 border border-gray-300 rounded shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
<button type="submit"
class="bg-indigo-600 text-white px-4 py-1.5 rounded hover:bg-indigo-700 font-bold shadow-sm transition">搜索</button>
<a href="{{ route('admin.users.index') }}"
class="px-4 py-1.5 bg-white border border-gray-300 rounded text-gray-700 hover:bg-gray-50">重置</a>
</form>
</div>
<!-- 用户表格 -->
<div class="overflow-x-auto">
@php
/**
* 生成排序链接:点击同一列切换方向,点击新列默认倒序
*/
$sortLink = function (string $col) use ($sortBy, $sortDir): string {
$newDir = $sortBy === $col && $sortDir === 'desc' ? 'asc' : 'desc';
$qs = http_build_query(
array_merge(request()->except(['sort_by', 'sort_dir', 'page']), [
'sort_by' => $col,
'sort_dir' => $newDir,
]),
);
return url('/admin/users?' . $qs);
};
$arrow = fn(string $col): string => $sortBy === $col ? ($sortDir === 'desc' ? ' ↓' : ' ↑') : '';
@endphp
<table class="w-full text-left border-collapse">
<thead>
<tr
class="bg-gray-50 border-b border-gray-100 text-xs text-gray-500 uppercase font-bold tracking-wider">
<th class="p-4">
<a href="{{ $sortLink('id') }}" class="hover:text-indigo-600 flex items-center gap-1">
ID<span class="text-indigo-500">{{ $arrow('id') }}</span>
</a>
</th>
<th class="p-4">注册名</th>
<th class="p-4">性别</th>
<th class="p-4">
<a href="{{ $sortLink('user_level') }}" class="hover:text-indigo-600 flex items-center gap-1">
等级<span class="text-indigo-500">{{ $arrow('user_level') }}</span>
</a>
</th>
<th class="p-4">职务</th>
<th class="p-4">
<a href="{{ $sortLink('exp_num') }}" class="hover:text-indigo-600 flex items-center gap-1">
经验<span class="text-indigo-500">{{ $arrow('exp_num') }}</span>
</a>
</th>
<th class="p-4">
<a href="{{ $sortLink('jjb') }}" class="hover:text-yellow-600 flex items-center gap-1">
金币<span class="text-yellow-500">{{ $arrow('jjb') }}</span>
</a>
</th>
<th class="p-4">
<a href="{{ $sortLink('meili') }}" class="hover:text-pink-600 flex items-center gap-1">
魅力<span class="text-pink-500">{{ $arrow('meili') }}</span>
</a>
</th>
<th class="p-4">注册时间</th>
<th class="p-4">
<a href="{{ $sortLink('online') }}" class="hover:text-green-600 flex items-center gap-1">
在线<span class="text-green-500">{{ $arrow('online') }}</span>
</a>
</th>
<th class="p-4 text-right">管理操作</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@foreach ($users as $user)
<tr class="hover:bg-gray-50 transition">
<td class="p-4 font-mono text-xs text-gray-500">{{ $user->id }}</td>
<td class="p-4">
<div class="flex items-center space-x-3">
<img src="/images/headface/{{ $user->headface ?? '01.gif' }}"
class="w-8 h-8 rounded border object-cover">
<span class="font-bold text-gray-800">{{ $user->username }}</span>
@if ($user->isVip())
<span title="{{ $user->vipName() }}"
style="color: {{ $user->vipLevel?->color ?? '#f59e0b' }}">{{ $user->vipIcon() }}</span>
@endif
</div>
</td>
<td class="p-4 text-sm">{{ [0 => '保密', 1 => '男', 2 => '女'][$user->sex] ?? '保密' }}</td>
<td class="p-4">
<span
class="px-2 py-0.5 rounded-full text-xs {{ $user->user_level >= 100 ? 'bg-red-100 text-red-700 font-bold' : 'bg-gray-100 text-gray-600' }}">
LV.{{ $user->user_level }}
</span>
</td>
<td class="p-4">
@if ($user->activePosition)
@php $pos = $user->activePosition->position; @endphp
<div class="text-xs text-gray-400">{{ $pos->department->name }}</div>
<div class="font-bold text-sm" style="color: {{ $pos->department->color }}">
{{ $pos->icon }} {{ $pos->name }}
</div>
@else
<span class="text-gray-300 text-xs"></span>
@endif
</td>
<td class="p-4 text-sm font-mono text-gray-600">
{{ number_format($user->exp_num ?? 0) }}
</td>
<td class="p-4 text-sm font-mono text-yellow-700">
{{ number_format($user->jjb ?? 0) }}
</td>
<td class="p-4 text-sm font-mono text-pink-600">
{{ number_format($user->meili ?? 0) }}
</td>
<td class="p-4 text-sm font-mono text-gray-500">{{ $user->created_at->format('Y/m/d H:i') }}
</td>
<td class="p-4">
@php $isOnline = $onlineUsernames->contains($user->username); @endphp
<span
class="inline-flex items-center gap-1.5 text-xs font-bold px-2 py-0.5 rounded-full
{{ $isOnline ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400' }}">
<span
class="w-1.5 h-1.5 rounded-full {{ $isOnline ? 'bg-green-500' : 'bg-gray-300' }}"></span>
{{ $isOnline ? '在线' : '离线' }}
</span>
</td>
<td class="p-4 text-right space-x-2 relative">
@if (auth()->id() === 1)
<button
@click="editingUser = {
id: {{ $user->id }},
username: '{{ addslashes($user->username) }}',
exp_num: {{ $user->exp_num ?? 0 }},
jjb: {{ $user->jjb ?? 0 }},
meili: {{ $user->meili ?? 0 }},
sex: '{{ $user->sex }}',
qianming: '{{ addslashes($user->qianming ?? '') }}',
visit_num: {{ $user->visit_num ?? 0 }},
vip_level_id: '{{ $user->vip_level_id ?? '' }}',
hy_time: '{{ $user->hy_time ? $user->hy_time->format('Y-m-d') : '' }}',
requestUrl: '{{ route('admin.users.update', $user->id) }}'
}; showEditModal = true"
class="text-xs bg-indigo-50 text-indigo-600 font-bold px-3 py-1.5 rounded hover:bg-indigo-600 hover:text-white transition cursor-pointer">
详细 / 修改
</button>
<form action="{{ route('admin.users.destroy', $user->id) }}" method="POST"
class="inline"
onsubmit="return confirm('危险:确定彻底物理清除用户 [{{ $user->username }}] 吗?数据不可恢复!')">
@csrf @method('DELETE')
<button type="submit"
class="text-xs bg-red-50 text-red-600 font-bold px-3 py-1.5 rounded hover:bg-red-600 hover:text-white transition cursor-pointer">
强杀
</button>
</form>
@else
<span class="text-xs text-gray-300 italic">仅超管可操作</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- 分页链接 -->
@if ($users->hasPages())
<div class="p-4 border-t border-gray-100">
{{ $users->links() }}
</div>
@endif
<!-- 弹出的修改框 -->
<div x-show="showEditModal" style="display: none;"
class="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
<div @click.away="showEditModal = false"
class="bg-white rounded-xl shadow-2xl w-full max-w-lg transform transition-all" x-transition>
<div
class="bg-indigo-900 border-b border-indigo-800 px-6 py-4 flex justify-between items-center rounded-t-xl text-white">
<h3 class="font-bold text-lg">编辑用户:<span x-text="editingUser.username" class="text-indigo-300"></span>
</h3>
<button @click="showEditModal = false" class="text-gray-400 hover:text-white">&times;</button>
</div>
<div class="p-6">
{{-- Toast 通知(弹窗内部) --}}
<div x-show="editToast" x-transition style="display:none;"
:class="editToastOk ? 'bg-green-50 border-green-400 text-green-800' :
'bg-red-50 border-red-400 text-red-800'"
class="mb-4 px-4 py-2 border-l-4 rounded text-sm font-bold" x-text="editToastMsg">
</div>
<form @submit.prevent="submitEditUser($el)" method="POST">
@csrf @method('PUT')
<div class="grid grid-cols-2 gap-4">
{{-- 经验 --}}
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">经验值</label>
<input type="number" name="exp_num" x-model="editingUser.exp_num" required
min="0"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
</div>
{{-- 金币 --}}
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">金币 (jjb)</label>
<input type="number" name="jjb" x-model="editingUser.jjb" required min="0"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
</div>
{{-- 魅力 --}}
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">魅力值</label>
<input type="number" name="meili" x-model="editingUser.meili" required min="0"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
</div>
{{-- 性别 --}}
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">性别</label>
<select name="sex" x-model="editingUser.sex"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
<option value="1"></option>
<option value="2"></option>
<option value="0">保密</option>
</select>
</div>
{{-- 访问次数 --}}
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">访问次数</label>
<input type="text" disabled :value="editingUser.visit_num"
class="w-full bg-gray-100 border-gray-200 rounded-md p-2 border text-sm text-gray-500">
</div>
</div>
{{-- 签名 --}}
<div class="mt-4">
<label class="block text-xs font-bold text-gray-600 mb-1">个性签名</label>
<input type="text" name="qianming" x-model="editingUser.qianming" maxlength="255"
placeholder="暂无签名"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
</div>
{{-- VIP 会员设置 --}}
<div class="mt-4 grid grid-cols-2 gap-4">
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">VIP 会员等级</label>
<select name="vip_level_id" x-model="editingUser.vip_level_id"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
<option value="">无(普通用户)</option>
@foreach ($vipLevels as $vl)
<option value="{{ $vl->id }}">{{ $vl->icon }}
{{ $vl->name }}×{{ $vl->exp_multiplier }}经验
×{{ $vl->jjb_multiplier }}金币)</option>
@endforeach
</select>
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">会员到期时间
<span class="font-normal text-gray-400">(留空=永久)</span></label>
<input type="date" name="hy_time" x-model="editingUser.hy_time"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
</div>
</div>
{{-- 密码 --}}
<div class="mt-4">
<label
class="block text-xs font-bold pl-2 text-red-600 border-l-4 border-red-500 bg-red-50 p-2 mb-1">强制重置密码
<span class="font-normal text-gray-500">(留空不修改)</span></label>
<input type="text" name="password" placeholder="输入新密码"
class="w-full border-red-300 rounded-md shadow-sm focus:border-red-500 focus:ring-red-500 p-2 border text-sm placeholder-red-300">
</div>
<div class="flex justify-end space-x-3 pt-4 mt-4 border-t border-gray-100">
<button type="button" @click="showEditModal = false"
class="px-4 py-2 border rounded font-medium text-gray-600 hover:bg-gray-50">取消</button>
<button type="submit" :disabled="editSaving"
class="px-4 py-2 bg-indigo-600 text-white rounded font-bold hover:bg-indigo-700 shadow-sm disabled:opacity-60"
x-text="editSaving ? '保存中...' : '保存修改'"></button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('userEditor', () => ({
showEditModal: false,
editingUser: {},
editToast: false,
editToastOk: true,
editToastMsg: '',
editSaving: false,
async submitEditUser(formEl) {
this.editSaving = true;
this.editToast = false;
const formData = new FormData(formEl);
formData.append('_method', 'PUT'); // 必须带有伪造方法给 Laravel
try {
const res = await fetch(this.editingUser.requestUrl, {
method: 'POST',
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector(
'meta[name="csrf-token"]')
.content,
},
body: formData,
});
const json = await res.json();
this.editToastOk = json.status === 'success';
this.editToastMsg = json.message || (json.status === 'success' ? '保存成功!' :
'保存失败');
this.editToast = true;
if (json.status === 'success') {
setTimeout(() => {
this.showEditModal = false;
}, 1500);
}
} catch (e) {
this.editToastOk = false;
this.editToastMsg = '网络请求异常,请检查连接后重试。';
this.editToast = true;
console.error('Edit User Request Failed:', e); // 输出详细报错方便调试
}
this.editSaving = false;
}
}));
});
</script>
@endsection