Files
chatroom/resources/views/admin/forbidden-usernames/index.blade.php
T

274 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')
@php require resource_path('views/admin/partials/list-theme.php'); @endphp
<div class="{{ $adminListPageClass }}">
<div class="{{ $adminListHeaderCardClass }}">
<div>
<h2 class="{{ $adminListHeaderTitleClass }}">🚫 禁用用户名管理</h2>
<p class="{{ $adminListHeaderSubtitleClass }}">
以下词语永久禁止注册或改名使用。临时保留的旧昵称(改名后 30 天)不在此列表中显示。
</p>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2" 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); }
}
}">
<div class="{{ $adminListCardClass }} p-5">
{{-- Tab 切换 --}}
<div class="mb-4 -mx-5 flex gap-1 border-b border-gray-200 px-5">
<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="px-1 pb-2 text-sm 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="px-1 pb-2 text-sm transition">📋 批量添加</button>
</div>
<div class="space-y-3">
{{-- 单个模式 --}}
<div x-show="tab==='single'">
<label class="{{ $adminListFilterLabelClass }}">禁用词 <span class="text-red-500">*</span></label>
<input x-model="username" type="text" maxlength="50" placeholder="如:admin、习近平、fuck…"
@keydown.enter="addSingle()"
class="w-full {{ $adminListFilterInputClass }}">
</div>
{{-- 批量模式 --}}
<div x-show="tab==='batch'">
<label class="{{ $adminListFilterLabelClass }}">
批量词语 <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 resize-y font-mono {{ $adminListFilterInputClass }}"></textarea>
<p class="mt-1 text-xs text-gray-400">
自动去重,跳过已存在的词语,超过50字符的词语忽略
</p>
</div>
{{-- 共用原因 --}}
<div>
<label class="{{ $adminListFilterLabelClass }}">禁用原因(选填)</label>
<input x-model="reason" type="text" maxlength="100" placeholder="如:国家领导人姓名 / 攻击性词汇"
class="w-full {{ $adminListFilterInputClass }}">
</div>
{{-- 提交按钮 --}}
<button x-show="tab==='single'" @click="addSingle()" :disabled="saving || !username.trim()"
class="w-full rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-700 disabled:opacity-40">
<span x-text="saving ? '添加中…' : ' 添加到禁用列表'"></span>
</button>
<button x-show="tab==='batch'" @click="addBatch()" :disabled="saving || !words.trim()"
class="w-full rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-700 disabled:opacity-40">
<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="{{ $adminListCardClass }} p-5">
<h3 class="{{ $adminListSectionTitleClass }} 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 {{ $adminListFilterInputClass }}">
<div class="flex gap-2">
<button type="submit"
class="flex-1 rounded-lg bg-gray-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-gray-700">
搜索
</button>
@if ($q)
<a href="{{ route('admin.forbidden-usernames.index') }}"
class="{{ $adminListSecondaryButtonClass }}">
清除
</a>
@endif
</div>
</form>
<p class="mt-3 text-xs text-gray-400">
<strong>{{ $items->total() }}</strong> 条永久禁用词
@if ($q)
(当前筛选"{{ $q }}"
@endif
</p>
</div>
</div>
<div class="{{ $adminListCardClass }}">
<div class="{{ $adminListSectionHeadClass }}">
<div>
<h3 class="{{ $adminListSectionTitleClass }}">禁用词列表</h3>
<p class="{{ $adminListSectionDescClass }}">表格、按钮与分页统一到用户管理页风格,保留高风险词的红色提示。</p>
</div>
</div>
<div class="{{ $adminListTableWrapClass }}">
<table class="{{ $adminListTableClass }}">
<thead>
<tr class="{{ $adminListTableHeadRowClass }}">
<th class="{{ $adminListTableHeadCellClass }}">词语</th>
<th class="{{ $adminListTableHeadCellClass }}">禁用原因</th>
<th class="{{ $adminListTableHeadCellClass }}">添加时间</th>
<th class="{{ $adminListTableHeadCellClass }} text-center">操作</th>
</tr>
</thead>
<tbody class="{{ $adminListTableBodyClass }}">
@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="{{ $adminListTableRowClass }}">
{{-- 词语 --}}
<td class="px-4 py-3">
<span
class="inline-flex items-center rounded-full border border-red-200 bg-red-50 px-2 py-0.5 text-xs font-semibold text-gray-800">
{{ $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="w-48 rounded border border-indigo-300 px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-indigo-400">
<button @click="saveReason()" :disabled="saving"
class="{{ $adminListActionButtonClass }} bg-indigo-600 text-white 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="{{ $adminListActionButtonClass }} border border-red-200 text-red-600 hover:border-red-400 hover:bg-red-50">
🗑 删除
</button>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="{{ $adminListEmptyClass }}">
<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="{{ $adminListPaginationClass }}">
{{ $items->links() }}
</div>
@endif
</div>
</div>
@endsection