迁移AI厂商后台事件

This commit is contained in:
2026-04-25 13:35:20 +08:00
parent 7900145ba9
commit cedc787586
3 changed files with 206 additions and 119 deletions
+198
View File
@@ -0,0 +1,198 @@
// AI 厂商后台事件代理,集中管理全局开关、厂商状态、默认厂商和连通性测试。
let adminAiProvidersControlsBound = false;
/**
* 读取 CSRF 令牌,供后台 AJAX 请求使用。
*
* @returns {string}
*/
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") ?? "";
}
/**
* 使用后台接口切换状态类请求。
*
* @param {string} url
* @returns {Promise<object>}
*/
async function postJson(url) {
const response = await fetch(url, {
method: "POST",
headers: {
"X-CSRF-TOKEN": getCsrfToken(),
Accept: "application/json",
},
});
return response.json();
}
/**
* 同步聊天机器人总开关按钮与状态文案。
*
* @param {HTMLButtonElement} button
* @param {boolean} enabled
* @returns {void}
*/
function renderChatBotState(button, enabled) {
const knob = button.firstElementChild;
const statusText = document.getElementById("chatbot-status-text");
button.classList.toggle("bg-emerald-500", enabled);
button.classList.toggle("bg-gray-300", !enabled);
knob?.classList.toggle("translate-x-6", enabled);
knob?.classList.toggle("translate-x-1", !enabled);
if (statusText) {
statusText.textContent = enabled ? "已开启" : "已关闭";
statusText.classList.toggle("text-emerald-600", enabled);
statusText.classList.toggle("text-gray-400", !enabled);
}
}
/**
* 切换全局聊天机器人开关。
*
* @param {HTMLButtonElement} button
* @returns {Promise<void>}
*/
async function toggleChatBot(button) {
try {
const toggleUrl = button.getAttribute("data-ai-chatbot-toggle-url");
if (!toggleUrl) {
return;
}
const data = await postJson(toggleUrl);
if (data.status === "success") {
renderChatBotState(button, Boolean(data.enabled));
window.alert(data.message);
}
} catch (error) {
window.alert(`操作失败:${error.message}`);
}
}
/**
* 切换厂商启用状态,成功后刷新列表。
*
* @param {HTMLButtonElement} button
* @returns {Promise<void>}
*/
async function toggleProvider(button) {
try {
const toggleUrl = button.getAttribute("data-ai-provider-toggle-url");
if (!toggleUrl) {
return;
}
const data = await postJson(toggleUrl);
if (data.status === "success") {
window.location.reload();
}
} catch (error) {
window.alert(`操作失败:${error.message}`);
}
}
/**
* 将当前厂商设为默认,成功后刷新列表。
*
* @param {HTMLButtonElement} button
* @returns {Promise<void>}
*/
async function setDefaultProvider(button) {
try {
const defaultUrl = button.getAttribute("data-ai-provider-default-url");
if (!defaultUrl) {
return;
}
const data = await postJson(defaultUrl);
if (data.status === "success") {
window.location.reload();
}
} catch (error) {
window.alert(`操作失败:${error.message}`);
}
}
/**
* 测试 AI 厂商接口连通性,并恢复按钮状态。
*
* @param {HTMLButtonElement} button
* @returns {Promise<void>}
*/
async function testProviderConnection(button) {
const testUrl = button.getAttribute("data-ai-provider-test-url");
const providerName = button.getAttribute("data-ai-provider-name") ?? "AI 厂商";
if (!testUrl) {
return;
}
const originalText = button.textContent;
button.textContent = "测试中…";
button.disabled = true;
try {
const data = await postJson(testUrl);
if (data.ok) {
window.alert(`✅ 「${providerName}」 连通成功!\n⤵ 响应耗时:${data.ms}ms(包含冷启动)\n🤖 模型回复:${data.message}`);
} else {
window.alert(`❌ 「${providerName}」 连通失败!\n耗时:${data.ms}ms\n错误:${data.message}`);
}
} catch (error) {
window.alert(`请求异常:${error.message}`);
} finally {
button.textContent = originalText;
button.disabled = false;
}
}
/**
* 绑定 AI 厂商后台页操作。
*
* @returns {void}
*/
export function bindAdminAiProvidersControls() {
if (adminAiProvidersControlsBound || typeof document === "undefined") {
return;
}
adminAiProvidersControlsBound = true;
document.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) {
return;
}
const chatBotToggle = event.target.closest("[data-ai-chatbot-toggle-url]");
if (chatBotToggle instanceof HTMLButtonElement) {
event.preventDefault();
void toggleChatBot(chatBotToggle);
return;
}
const providerToggle = event.target.closest("[data-ai-provider-toggle-url]");
if (providerToggle instanceof HTMLButtonElement) {
event.preventDefault();
void toggleProvider(providerToggle);
return;
}
const defaultButton = event.target.closest("[data-ai-provider-default-url]");
if (defaultButton instanceof HTMLButtonElement) {
event.preventDefault();
void setDefaultProvider(defaultButton);
return;
}
const testButton = event.target.closest("[data-ai-provider-test-url]");
if (testButton instanceof HTMLButtonElement) {
event.preventDefault();
void testProviderConnection(testButton);
}
});
}
+2
View File
@@ -1,4 +1,5 @@
import './bootstrap';
import { bindAdminAiProvidersControls } from './admin/ai-providers.js';
import { bindAdminAutoactControls } from './admin/autoact.js';
import { bindAdminFishingEventsControls } from './admin/fishing-events.js';
import { bindAdminFormConfirmations } from './admin/form-confirmations.js';
@@ -10,6 +11,7 @@ import { bindAdminRoomControls } from './admin/rooms.js';
import { bindAdminSignInRulesControls } from './admin/sign-in-rules.js';
// 后台共用入口只注册轻量事件代理,具体页面通过 data-* 属性决定是否响应。
bindAdminAiProvidersControls();
bindAdminAutoactControls();
bindAdminFishingEventsControls();
bindAdminFormConfirmations();
@@ -76,7 +76,7 @@
<h2 class="text-lg font-bold text-gray-800">🤖 AI 聊天机器人配置</h2>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500">大厅状态:</span>
<button id="chatbot-toggle-btn" onclick="toggleChatBot()"
<button id="chatbot-toggle-btn" data-ai-chatbot-toggle-url="{{ route('admin.ai-providers.toggle-chatbot') }}"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $chatbotEnabled ? 'bg-emerald-500' : 'bg-gray-300' }}">
<span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $chatbotEnabled ? 'translate-x-6' : 'translate-x-1' }}"></span>
@@ -182,7 +182,7 @@
{{ $provider->sort_order }}
</td>
<td class="px-6 py-4 text-center">
<button onclick="toggleProvider({{ $provider->id }}, this)"
<button data-ai-provider-toggle-url="{{ route('admin.ai-providers.toggle', $provider->id) }}"
class="px-3 py-1 rounded-full text-xs font-bold {{ $provider->is_enabled ? 'bg-emerald-100 text-emerald-700' : 'bg-gray-100 text-gray-500' }}">
{{ $provider->is_enabled ? '已启用' : '已禁用' }}
</button>
@@ -192,7 +192,7 @@
<span class="px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">
默认</span>
@else
<button onclick="setDefault({{ $provider->id }})"
<button data-ai-provider-default-url="{{ route('admin.ai-providers.default', $provider->id) }}"
class="px-3 py-1 rounded-full text-xs text-gray-400 hover:text-amber-600 hover:bg-amber-50 transition">
设为默认
</button>
@@ -200,13 +200,13 @@
</td>
<td class="px-6 py-4 text-center">
<div class="flex items-center justify-center gap-2">
<button
onclick="testConnection({{ $provider->id }}, '{{ addslashes($provider->name) }}')"
<button data-ai-provider-test-url="{{ route('admin.ai-providers.test', $provider->id) }}"
data-ai-provider-name="{{ $provider->name }}"
class="text-teal-600 hover:text-teal-800 text-xs font-bold"> 测试</button>
<button x-on:click="openEdit({{ $provider->toJson() }})"
class="text-indigo-600 hover:text-indigo-800 text-xs font-bold">编辑</button>
<form action="{{ route('admin.ai-providers.destroy', $provider->id) }}" method="POST"
onsubmit="return confirm('确定要删除 {{ $provider->name }} 吗?')">
data-admin-confirm="确定要删除 {{ $provider->name }} 吗?">
@csrf
@method('DELETE')
<button type="submit"
@@ -328,117 +328,4 @@
</div>
</div>
</div>
<script>
/**
* 切换全局聊天机器人开关
*/
async function toggleChatBot() {
try {
const res = await fetch('{{ route('admin.ai-providers.toggle-chatbot') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.status === 'success') {
// 更新按钮样式
const btn = document.getElementById('chatbot-toggle-btn');
const text = document.getElementById('chatbot-status-text');
if (data.enabled) {
btn.className = btn.className.replace('bg-gray-300', 'bg-emerald-500');
btn.firstElementChild.className = btn.firstElementChild.className.replace('translate-x-1',
'translate-x-6');
text.textContent = '已开启';
text.className = text.className.replace('text-gray-400', 'text-emerald-600');
} else {
btn.className = btn.className.replace('bg-emerald-500', 'bg-gray-300');
btn.firstElementChild.className = btn.firstElementChild.className.replace('translate-x-6',
'translate-x-1');
text.textContent = '已关闭';
text.className = text.className.replace('text-emerald-600', 'text-gray-400');
}
alert(data.message);
}
} catch (e) {
alert('操作失败:' + e.message);
}
}
/**
* 切换厂商启用/禁用状态
*/
async function toggleProvider(id, btn) {
try {
const res = await fetch('/admin/ai-providers/' + id + '/toggle', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.status === 'success') {
location.reload();
}
} catch (e) {
alert('操作失败:' + e.message);
}
}
/**
* 测试 AI 接口连通性
*/
async function testConnection(id, name) {
const btn = event.target;
const origText = btn.textContent;
btn.textContent = '测试中…';
btn.disabled = true;
try {
const res = await fetch('/admin/ai-providers/' + id + '/test', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.ok) {
alert(`✅ 「${name}」 连通成功!\n⤵ 响应耗时:${data.ms}ms(包含冷启动)\n🤖 模型回复:${data.message}`);
} else {
alert(`❌ 「${name}」 连通失败!\n耗时:${data.ms}ms\n错误:${data.message}`);
}
} catch (e) {
alert('请求异常:' + e.message);
} finally {
btn.textContent = origText;
btn.disabled = false;
}
}
/**
* 设为默认 AI 厂商
*/
async function setDefault(id) {
try {
const res = await fetch('/admin/ai-providers/' + id + '/default', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
});
const data = await res.json();
if (data.status === 'success') {
location.reload();
}
} catch (e) {
alert('操作失败:' + e.message);
}
}
</script>
@endsection