- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
324 lines
19 KiB
PHP
324 lines
19 KiB
PHP
@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 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 text-right space-x-2 relative">
|
||
<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>
|
||
</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">×</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
|