Files
chatroom/resources/views/admin/forbidden-usernames/index.blade.php
lkddi 632a4240c4 功能:禁用词管理支持批量添加
- 新增 ForbiddenUsernameController::batchStore()
  支持换行、逗号、中文逗号、空格多种分隔格式
  自动去重、跳过已存在词语、忽略超长词
  返回成功数/跳过数详细提示
- 新增路由 POST /admin/forbidden-usernames/batch
- View 新增卡片加「单个/批量」两 Tab 切换
  批量 Tab 使用 textarea 多行输入
2026-03-01 14:04:28 +08:00

269 lines
14 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="{
tab: 'single',
username: '',
words: '',
reason: '',
saving: false,
msg: '',
msgOk: true,
async addSingle() {
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); }
},
async addBatch() {
if (!this.words.trim()) return;
this.saving = true;
this.msg = '';
const res = await fetch('{{ route('admin.forbidden-usernames.batch') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ words: this.words, reason: this.reason }),
});
const data = await res.json();
this.saving = false;
this.msgOk = data.status === 'success';
this.msg = data.message;
if (this.msgOk) { this.words = '';
this.reason = '';
setTimeout(() => location.reload(), 1200); }
}
}">
{{-- 新增卡片(单个 + 批量 Tab --}}
<div class="bg-white rounded-xl border border-gray-200 shadow-sm p-5">
{{-- Tab 切换 --}}
<div class="flex border-b border-gray-200 mb-4 -mx-5 px-5 gap-1">
<button @click="tab='single'; msg=''"
:class="tab === 'single' ? 'border-b-2 border-indigo-600 text-indigo-700 font-bold' :
'text-gray-500 hover:text-gray-700'"
class="pb-2 text-sm px-1 transition"> 单个添加</button>
<button @click="tab='batch'; msg=''"
:class="tab === 'batch' ? 'border-b-2 border-indigo-600 text-indigo-700 font-bold' :
'text-gray-500 hover:text-gray-700'"
class="pb-2 text-sm px-1 transition">📋 批量添加</button>
</div>
<div class="space-y-3">
{{-- 单个模式 --}}
<div x-show="tab==='single'">
<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="addSingle()"
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 x-show="tab==='batch'">
<label class="text-xs text-gray-500 mb-1 block">
批量词语 <span class="text-red-500">*</span>
<span class="text-gray-400 font-normal">(每行一个,或用逗号/空格分隔)</span>
</label>
<textarea x-model="words" rows="6" maxlength="5000"
placeholder="admin&#10;root&#10;fuck&#10;操,草,傻&#10;…(每行一个或逗号分隔)"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-400 outline-none resize-y font-mono"></textarea>
<p class="text-xs text-gray-400 mt-1">
自动去重跳过已存在的词语超过50字符的词语忽略
</p>
</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 x-show="tab==='single'" @click="addSingle()" :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>
<button x-show="tab==='batch'" @click="addBatch()" :disabled="saving || !words.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