Files
chatroom/resources/views/admin/users/index.blade.php

327 lines
19 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')
@php
// 管理员级别 = 最高等级 + 1后台编辑最高可设到管理员级别
$adminLevel = (int) \App\Models\Sysparam::getValue('maxlevel', '15') + 1;
@endphp
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="{ showEditModal: false, editingUser: {}, adminLevel: {{ $adminLevel }}, editToast: false, editToastOk: true, editToastMsg: '', editSaving: false }">
<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">ID</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">
<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 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" x-data>
<button
@click="editingUser = {
id: {{ $user->id }},
username: '{{ addslashes($user->username) }}',
user_level: {{ $user->user_level }},
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">&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">
等级
<span class="text-gray-400 font-normal">
(最高 <span x-text="adminLevel"></span> )
</span>
</label>
<input type="number" name="user_level" x-model="editingUser.user_level" required
min="0" :max="adminLevel"
:readonly="{{ Auth::id() }} !== 1 && editingUser.id !== {{ Auth::id() }}"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm"
:class="{
'bg-gray-100 cursor-not-allowed': {{ Auth::id() }} !== 1 && editingUser
.id !==
{{ Auth::id() }}
}"
:title="{{ Auth::id() }} !== 1 && editingUser.id !== {{ Auth::id() }} ?
'仅系统创始人可修改他人等级' : ''">
</div>
{{-- 经验 --}}
<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>
/**
* 用户编辑弹窗 AJAX 提交
* 避免传统表单导致 302 重定向,改为 fetch 就地显示结果
*/
async function submitEditUser(form) {
// 由 x-data 上层访问 Alpine 状态
const el = document.querySelector('[x-data]');
const data = Alpine.$data(el);
data.editSaving = true;
data.editToast = false;
const formEl = form.querySelector('form') ?? form;
const formData = new FormData(formEl);
try {
const res = await fetch(data.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();
data.editToastOk = json.status === 'success';
data.editToastMsg = json.message || (json.status === 'success' ? '保存成功!' : '保存失败');
data.editToast = true;
// 成功后 1.5 秒关闭弹窗
if (json.status === 'success') {
setTimeout(() => {
data.showEditModal = false;
}, 1500);
}
} catch (e) {
data.editToastOk = false;
data.editToastMsg = '网络异常,请检查连接后重试。';
data.editToast = true;
}
data.editSaving = false;
}
</script>
@endsection