功能:禁用词管理支持批量添加
- 新增 ForbiddenUsernameController::batchStore() 支持换行、逗号、中文逗号、空格多种分隔格式 自动去重、跳过已存在词语、忽略超长词 返回成功数/跳过数详细提示 - 新增路由 POST /admin/forbidden-usernames/batch - View 新增卡片加「单个/批量」两 Tab 切换 批量 Tab 使用 textarea 多行输入
This commit is contained in:
@@ -80,6 +80,72 @@ class ForbiddenUsernameController extends Controller
|
||||
return response()->json(['status' => 'success', 'message' => "「{$username}」已加入永久禁用列表。"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加永久禁用词。
|
||||
*
|
||||
* 接受多行文本或逗号分隔的词语列表,自动去重并过滤已存在者。
|
||||
* 返回成功添加数量和跳过数量。
|
||||
*
|
||||
* @param Request $request 请求体:words(换行/逗号分隔),reason(选填,共用)
|
||||
*/
|
||||
public function batchStore(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'words' => ['required', 'string'],
|
||||
'reason' => ['nullable', 'string', 'max:100'],
|
||||
], [
|
||||
'words.required' => '请输入至少一个词语。',
|
||||
]);
|
||||
|
||||
// 支持换行、逗号、中文逗号、空格分隔
|
||||
$rawWords = preg_split('/[\r\n,,\s]+/', $validated['words']);
|
||||
$reason = trim($validated['reason'] ?? '');
|
||||
|
||||
// 过滤空串、截断过长、去重
|
||||
$words = collect($rawWords)
|
||||
->map(fn ($w) => trim($w))
|
||||
->filter(fn ($w) => $w !== '' && mb_strlen($w) <= 50)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($words->isEmpty()) {
|
||||
return response()->json(['status' => 'error', 'message' => '没有有效的词语,请检查输入。'], 422);
|
||||
}
|
||||
|
||||
// 查询已存在的(批量一次查询)
|
||||
$existing = UsernameBlacklist::permanent()
|
||||
->whereIn('username', $words->all())
|
||||
->pluck('username')
|
||||
->flip(); // 转为键名方便 has() 查询
|
||||
|
||||
$now = Carbon::now();
|
||||
$added = 0;
|
||||
$rows = [];
|
||||
|
||||
foreach ($words as $word) {
|
||||
if ($existing->has($word)) {
|
||||
continue; // 跳过已存在
|
||||
}
|
||||
$rows[] = [
|
||||
'username' => $word,
|
||||
'type' => 'permanent',
|
||||
'reserved_until' => null,
|
||||
'reason' => $reason ?: null,
|
||||
'created_at' => $now,
|
||||
];
|
||||
$added++;
|
||||
}
|
||||
|
||||
if (! empty($rows)) {
|
||||
UsernameBlacklist::insert($rows);
|
||||
}
|
||||
|
||||
$skipped = $words->count() - $added;
|
||||
$msg = "成功添加 {$added} 个词语".($skipped > 0 ? ",跳过 {$skipped} 个(已存在)" : '').'。';
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => $msg, 'added' => $added]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定禁用词的原因备注。
|
||||
*
|
||||
|
||||
@@ -25,12 +25,14 @@
|
||||
|
||||
{{-- 新增表单 + 搜索栏 --}}
|
||||
<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 addWord() {
|
||||
async addSingle() {
|
||||
if (!this.username.trim()) return;
|
||||
this.saving = true;
|
||||
this.msg = '';
|
||||
@@ -50,28 +52,84 @@
|
||||
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">
|
||||
<h3 class="font-bold text-gray-700 text-sm mb-3">➕ 新增禁用词</h3>
|
||||
{{-- 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>
|
||||
<label class="text-xs text-gray-500 mb-1 block">禁用词(用户名)<span class="text-red-500">*</span></label>
|
||||
{{-- 单个模式 --}}
|
||||
<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="addWord()"
|
||||
@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 root fuck 操,草,傻 …(每行一个或逗号分隔)"
|
||||
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>
|
||||
<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()"
|
||||
|
||||
{{-- 提交按钮 --}}
|
||||
<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>
|
||||
|
||||
@@ -265,6 +265,7 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
|
||||
// 禁用用户名管理(永久禁止注册/改名的词语:领导人名称、攻击性词汇等)
|
||||
Route::get('/forbidden-usernames', [\App\Http\Controllers\Admin\ForbiddenUsernameController::class, 'index'])->name('forbidden-usernames.index');
|
||||
Route::post('/forbidden-usernames', [\App\Http\Controllers\Admin\ForbiddenUsernameController::class, 'store'])->name('forbidden-usernames.store');
|
||||
Route::post('/forbidden-usernames/batch', [\App\Http\Controllers\Admin\ForbiddenUsernameController::class, 'batchStore'])->name('forbidden-usernames.batch');
|
||||
Route::put('/forbidden-usernames/{id}', [\App\Http\Controllers\Admin\ForbiddenUsernameController::class, 'update'])->name('forbidden-usernames.update');
|
||||
Route::delete('/forbidden-usernames/{id}', [\App\Http\Controllers\Admin\ForbiddenUsernameController::class, 'destroy'])->name('forbidden-usernames.destroy');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user