Files
chatroom/resources/views/admin/forbidden-usernames/index.blade.php
lkddi fc495ccceb 功能:禁用用户名管理(永久禁词列表)
数据库:
- 新增迁移 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
  新增表单、关键词搜索、分页、行内编辑原因、删除
- 侧边栏加「🚫 禁用用户名」入口(仅站长可见)
2026-03-01 14:00:38 +08:00

211 lines
11 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.
{{--
文件功能:后台禁用用户名管理页面(仅站长 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