diff --git a/resources/js/admin/ai-providers.js b/resources/js/admin/ai-providers.js new file mode 100644 index 0000000..aafa952 --- /dev/null +++ b/resources/js/admin/ai-providers.js @@ -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} + */ +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} + */ +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} + */ +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} + */ +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} + */ +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); + } + }); +} diff --git a/resources/js/app.js b/resources/js/app.js index 4f53538..37f4fe9 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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(); diff --git a/resources/views/admin/ai-providers/index.blade.php b/resources/views/admin/ai-providers/index.blade.php index 69bbd5f..6ee2360 100644 --- a/resources/views/admin/ai-providers/index.blade.php +++ b/resources/views/admin/ai-providers/index.blade.php @@ -76,7 +76,7 @@

🤖 AI 聊天机器人配置

大厅状态: - @@ -192,7 +192,7 @@ ★ 默认 @else - @@ -200,13 +200,13 @@
-
+ data-admin-confirm="确定要删除 {{ $provider->name }} 吗?"> @csrf @method('DELETE')
- - @endsection