数据库:
- 新增迁移 username_blacklist 表加 type/reason 列
type: temp(改名30天保留)| permanent(管理员永久禁用)
reason: 禁用原因备注(最长100字符)
核心逻辑:
- UsernameBlacklist::isBlocked() 同时拦截两种类型
也包含 isReserved() 兼容旧调用
增加 scopePermanent()/scopeTemp() 查询作用域
- AuthController 注册时加 isBlocked() 拦截
禁词/保留期内均不可注册
- ShopService::useRenameCard() 已有 isReserved() 调用
因已改用 isBlocked() 别名,无需修改
后台:
- ForbiddenUsernameController:index/store/update/destroy
- 路由:/admin/forbidden-usernames(chat.site_owner 中间件)
- 视图:admin/forbidden-usernames/index.blade.php
新增表单、关键词搜索、分页、行内编辑原因、删除
- 侧边栏加「🚫 禁用用户名」入口(仅站长可见)
211 lines
11 KiB
PHP
211 lines
11 KiB
PHP
{{--
|
||
文件功能:后台禁用用户名管理页面(仅站长 id=1 可访问)
|
||
|
||
管理 username_blacklist.type=permanent 的永久禁止词列表。
|
||
用户在注册或使用改名卡时,系统自动查询此表进行拦截。
|
||
|
||
@extends admin.layouts.app
|
||
--}}
|
||
@extends('admin.layouts.app')
|
||
|
||
@section('title', '禁用用户名管理')
|
||
|
||
@section('content')
|
||
|
||
{{-- 标题 --}}
|
||
<div class="flex items-start justify-between mb-6">
|
||
<div>
|
||
<h2 class="text-xl font-bold text-gray-800">🚫 禁用用户名管理</h2>
|
||
<p class="text-sm text-gray-500 mt-1">
|
||
以下词语永久禁止注册或改名使用(领导人名称、攻击性词汇、违禁词等)。
|
||
临时保留的旧昵称(改名后30天)不在此列表中显示。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 新增表单 + 搜索栏 --}}
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6" x-data="{
|
||
username: '',
|
||
reason: '',
|
||
saving: false,
|
||
msg: '',
|
||
msgOk: true,
|
||
async addWord() {
|
||
if (!this.username.trim()) return;
|
||
this.saving = true;
|
||
this.msg = '';
|
||
const res = await fetch('{{ route('admin.forbidden-usernames.store') }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
},
|
||
body: JSON.stringify({ username: this.username, reason: this.reason }),
|
||
});
|
||
const data = await res.json();
|
||
this.saving = false;
|
||
this.msgOk = data.status === 'success';
|
||
this.msg = data.message;
|
||
if (this.msgOk) { this.username = '';
|
||
this.reason = '';
|
||
setTimeout(() => location.reload(), 800); }
|
||
}
|
||
}">
|
||
|
||
{{-- 新增卡片 --}}
|
||
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
||
<h3 class="font-bold text-gray-700 text-sm mb-3">➕ 新增禁用词</h3>
|
||
<div class="space-y-3">
|
||
<div>
|
||
<label class="text-xs text-gray-500 mb-1 block">禁用词(用户名)<span class="text-red-500">*</span></label>
|
||
<input x-model="username" type="text" maxlength="50" placeholder="如:admin、习近平、fuck…"
|
||
@keydown.enter="addWord()"
|
||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 outline-none">
|
||
</div>
|
||
<div>
|
||
<label class="text-xs text-gray-500 mb-1 block">禁用原因(备注,选填)</label>
|
||
<input x-model="reason" type="text" maxlength="100" placeholder="如:国家领导人姓名 / 攻击性词汇"
|
||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 outline-none">
|
||
</div>
|
||
<button @click="addWord()" :disabled="saving || !username.trim()"
|
||
class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-lg px-4 py-2 text-sm font-bold transition">
|
||
<span x-text="saving ? '添加中…' : '➕ 添加到禁用列表'"></span>
|
||
</button>
|
||
<p x-show="msg" x-text="msg" :class="msgOk ? 'text-green-600' : 'text-red-500'"
|
||
class="text-xs font-bold"></p>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 搜索卡片 --}}
|
||
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
|
||
<h3 class="font-bold text-gray-700 text-sm mb-3">🔍 搜索禁用词</h3>
|
||
<form method="GET" action="{{ route('admin.forbidden-usernames.index') }}" class="space-y-3">
|
||
<input type="text" name="q" value="{{ $q }}" placeholder="输入关键词搜索…"
|
||
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 outline-none">
|
||
<div class="flex gap-2">
|
||
<button type="submit"
|
||
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white rounded-lg px-4 py-2 text-sm font-bold transition">
|
||
搜索
|
||
</button>
|
||
@if ($q)
|
||
<a href="{{ route('admin.forbidden-usernames.index') }}"
|
||
class="px-4 py-2 text-sm text-gray-500 border border-gray-300 rounded-lg hover:bg-gray-50 transition">
|
||
✕ 清除
|
||
</a>
|
||
@endif
|
||
</div>
|
||
</form>
|
||
<p class="text-xs text-gray-400 mt-3">
|
||
共 <strong>{{ $items->total() }}</strong> 条永久禁用词
|
||
@if ($q)
|
||
(当前筛选"{{ $q }}")
|
||
@endif
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 禁用词列表 --}}
|
||
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||
<table class="w-full text-sm">
|
||
<thead class="bg-gray-50 border-b border-gray-200">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">词语</th>
|
||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">禁用原因</th>
|
||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">添加时间</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
@forelse ($items as $item)
|
||
<tr x-data="{
|
||
editing: false,
|
||
reason: @js($item->reason ?? ''),
|
||
saving: false,
|
||
async saveReason() {
|
||
this.saving = true;
|
||
await fetch('/admin/forbidden-usernames/{{ $item->id }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'X-HTTP-Method-Override': 'PUT',
|
||
},
|
||
body: JSON.stringify({ reason: this.reason, _method: 'PUT' }),
|
||
});
|
||
this.saving = false;
|
||
this.editing = false;
|
||
},
|
||
async destroy() {
|
||
if (!confirm('确定要从禁用列表中移除「{{ $item->username }}」吗?')) return;
|
||
await fetch('/admin/forbidden-usernames/{{ $item->id }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'X-HTTP-Method-Override': 'DELETE',
|
||
},
|
||
body: JSON.stringify({ _method: 'DELETE' }),
|
||
});
|
||
location.reload();
|
||
}
|
||
}" class="hover:bg-gray-50 transition">
|
||
{{-- 词语 --}}
|
||
<td class="px-4 py-3">
|
||
<span
|
||
class="font-mono font-bold text-gray-800 bg-red-50 border border-red-200 px-2 py-0.5 rounded text-xs">
|
||
{{ $item->username }}
|
||
</span>
|
||
</td>
|
||
{{-- 原因(点击编辑) --}}
|
||
<td class="px-4 py-3 text-gray-600">
|
||
<span x-show="!editing" @click="editing = true" class="cursor-pointer hover:text-indigo-600"
|
||
x-text="reason || '(点击添加原因)'"></span>
|
||
<div x-show="editing" class="flex items-center gap-2">
|
||
<input x-model="reason" type="text" maxlength="100" @keydown.enter="saveReason()"
|
||
@keydown.escape="editing = false"
|
||
class="border border-indigo-300 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-indigo-400 outline-none w-48">
|
||
<button @click="saveReason()" :disabled="saving"
|
||
class="text-xs bg-indigo-600 text-white rounded px-2 py-1 hover:bg-indigo-700 disabled:opacity-50">
|
||
<span x-text="saving ? '…' : '保存'"></span>
|
||
</button>
|
||
<button @click="editing = false"
|
||
class="text-xs text-gray-400 hover:text-gray-600">取消</button>
|
||
</div>
|
||
</td>
|
||
{{-- 时间 --}}
|
||
<td class="px-4 py-3 text-gray-400 text-xs whitespace-nowrap">
|
||
{{ $item->created_at?->format('Y-m-d H:i') ?? '-' }}
|
||
</td>
|
||
{{-- 操作 --}}
|
||
<td class="px-4 py-3 text-center">
|
||
<button @click="destroy()"
|
||
class="text-xs text-red-600 hover:text-red-800 border border-red-200 hover:border-red-400 rounded px-3 py-1 transition">
|
||
🗑 删除
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="4" class="py-16 text-center text-gray-400">
|
||
<p class="text-3xl mb-2">🚫</p>
|
||
<p class="font-bold">暂无禁用词</p>
|
||
<p class="text-xs mt-1">使用左侧表单添加第一条禁用词</p>
|
||
</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{{-- 分页 --}}
|
||
@if ($items->hasPages())
|
||
<div class="mt-6">
|
||
{{ $items->links() }}
|
||
</div>
|
||
@endif
|
||
|
||
@endsection
|