feat: Web UI 实现 - 暗黑主题 + 频道配置 + 连接测试 + 会话监控
This commit is contained in:
86
app/templates/base.html
Normal file
86
app/templates/base.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{# 基础模板 #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}智队中枢{% endblock %}</title>
|
||||
|
||||
{# Tailwind CSS #}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{# 自定义样式 #}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
|
||||
{# HTMX #}
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
|
||||
|
||||
{# 导航栏 #}
|
||||
{% include 'components/navbar.html' %}
|
||||
|
||||
<div class="flex h-[calc(100vh-64px)]">
|
||||
{# 侧边栏 #}
|
||||
{% include 'components/sidebar.html' %}
|
||||
|
||||
{# 主内容区 #}
|
||||
<main class="flex-1 overflow-auto p-6">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{# Toast 提示 #}
|
||||
{% include 'components/toast.html' %}
|
||||
|
||||
{# 主题切换脚本 #}
|
||||
<script>
|
||||
// 初始化主题
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
|
||||
// 主题切换函数
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme');
|
||||
const next = current === 'light' ? 'dark' : 'light';
|
||||
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
}
|
||||
|
||||
// 页面加载后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 监听主题切换按钮
|
||||
document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
33
app/templates/components/navbar.html
Normal file
33
app/templates/components/navbar.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{# 导航栏 #}
|
||||
<nav class="h-16 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between px-6 shadow-sm">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ url_for('web.index') }}" class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
||||
<span class="text-white font-bold">智</span>
|
||||
</div>
|
||||
<span class="text-xl font-semibold dark:text-white">智队中枢</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
{# 主题切换按钮 #}
|
||||
<button id="theme-toggle" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" title="切换主题">
|
||||
{# 太阳图标 (显示在暗黑模式) #}
|
||||
<svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
{# 月亮图标 (显示在明亮模式) #}
|
||||
<svg class="w-5 h-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{# 用户菜单 #}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-primary-100 dark:bg-primary-900 rounded-full flex items-center justify-center">
|
||||
<span class="text-primary-600 dark:text-primary-400 text-sm font-medium">管</span>
|
||||
</div>
|
||||
<span class="text-sm dark:text-gray-200">管理员</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
64
app/templates/components/sidebar.html
Normal file
64
app/templates/components/sidebar.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{# 侧边栏 #}
|
||||
<aside class="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
|
||||
<nav class="flex-1 p-4 space-y-1">
|
||||
{# 仪表盘 #}
|
||||
<a href="{{ url_for('web.index') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg {% if request.endpoint == 'web.index' %}bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400{% else %}text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700{% endif %} transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
<span>仪表盘</span>
|
||||
</a>
|
||||
|
||||
{# 配置管理 #}
|
||||
<div class="pt-2">
|
||||
<div class="px-4 py-2 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">
|
||||
配置管理
|
||||
</div>
|
||||
<a href="{{ url_for('web.channels') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg {% if request.endpoint == 'web.channels' %}bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400{% else %}text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700{% endif %} transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span>频道配置</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# 连接测试 #}
|
||||
<div class="pt-2">
|
||||
<div class="px-4 py-2 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">
|
||||
测试工具
|
||||
</div>
|
||||
<a href="{{ url_for('web.test') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg {% if request.endpoint == 'web.test' %}bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400{% else %}text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700{% endif %} transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
<span>连接测试</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# 会话监控 #}
|
||||
<div class="pt-2">
|
||||
<div class="px-4 py-2 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">
|
||||
监控
|
||||
</div>
|
||||
<a href="{{ url_for('web.sessions') }}"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg {% if request.endpoint == 'web.sessions' %}bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400{% else %}text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700{% endif %} transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<span>会话监控</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{# 版本信息 #}
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
智队中枢 v0.7.0
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
50
app/templates/components/toast.html
Normal file
50
app/templates/components/toast.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{# Toast 提示组件 #}
|
||||
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-2">
|
||||
{# Toast 会通过 JS 动态添加 #}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 显示 Toast
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-primary-500'
|
||||
};
|
||||
|
||||
const icons = {
|
||||
success: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>',
|
||||
error: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>',
|
||||
warning: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>',
|
||||
info: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>'
|
||||
};
|
||||
|
||||
toast.className = `${colors[type]} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 transform transition-all duration-300 translate-x-full`;
|
||||
toast.innerHTML = `
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
${icons[type]}
|
||||
</svg>
|
||||
<span>${message}</span>
|
||||
<button onclick="this.parentElement.remove()" class="ml-2 hover:opacity-75">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// 动画进入
|
||||
setTimeout(() => toast.classList.remove('translate-x-full'), 10);
|
||||
|
||||
// 3秒后自动消失
|
||||
setTimeout(() => {
|
||||
toast.classList.add('translate-x-full');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
222
app/templates/config/channels.html
Normal file
222
app/templates/config/channels.html
Normal file
@@ -0,0 +1,222 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}频道配置 - 智队中枢{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{# 页面标题 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold dark:text-white">频道配置</h1>
|
||||
<button onclick="showModal('add-modal')" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
新增配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# 频道列表 #}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>Gateway URL</th>
|
||||
<th>状态</th>
|
||||
<th>最后连接</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for config in configs %}
|
||||
<tr>
|
||||
<td class="font-medium">{{ config.name }}</td>
|
||||
<td class="font-mono text-sm">{{ config.gateway_url }}</td>
|
||||
<td>
|
||||
{% if config.status == 'online' %}
|
||||
<span class="badge badge-success">在线</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">离线</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-gray-500 dark:text-gray-400 text-sm">
|
||||
{{ config.last_connected.strftime('%Y-%m-%d %H:%M') if config.last_connected else '从未连接' }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="testChannel('{{ config.id }}')"
|
||||
class="p-2 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 rounded-lg transition-colors"
|
||||
title="测试连接">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="editChannel('{{ config.id }}')"
|
||||
class="p-2 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="编辑">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="deleteChannel('{{ config.id }}')"
|
||||
class="p-2 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
|
||||
title="删除">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
暂无配置,点击下方按钮新增
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 新增/编辑配置模态框 #}
|
||||
<div id="channel-modal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg mx-4">
|
||||
<div class="card-header flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold dark:text-white" id="modal-title">新增配置</h3>
|
||||
<button onclick="hideModal('channel-modal')" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="channel-form" class="card-body space-y-4">
|
||||
<input type="hidden" id="channel-id" name="id">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">名称</label>
|
||||
<input type="text" id="channel-name" name="name" class="input" required placeholder="如:默认频道">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">Gateway URL</label>
|
||||
<input type="url" id="channel-url" name="gateway_url" class="input" required placeholder="ws://localhost:18888/ws">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">认证 Token</label>
|
||||
<input type="password" id="channel-token" name="auth_token" class="input" placeholder="可选,用于认证">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">重连间隔 (ms)</label>
|
||||
<input type="number" id="channel-reconnect" name="reconnect_interval" class="input" value="5000" min="1000" step="1000">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">心跳间隔 (ms)</label>
|
||||
<input type="number" id="channel-heartbeat" name="heartbeat_interval" class="input" value="30000" min="1000" step="1000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" id="channel-enabled" name="enabled" class="w-4 h-4 text-primary-600 rounded" checked>
|
||||
<label for="channel-enabled" class="dark:text-gray-300">启用此频道</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button type="button" onclick="hideModal('channel-modal')" class="btn btn-secondary">取消</button>
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 测试频道连接
|
||||
function testChannel(id) {
|
||||
showToast('正在测试连接...', 'info');
|
||||
|
||||
fetch(`/api/web/channels/${id}/test`, { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('连接测试通过!', 'success');
|
||||
} else {
|
||||
showToast(`连接失败: ${data.error}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast(`测试出错: ${err.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑频道
|
||||
function editChannel(id) {
|
||||
fetch(`/api/web/channels/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
document.getElementById('modal-title').textContent = '编辑配置';
|
||||
document.getElementById('channel-id').value = data.id;
|
||||
document.getElementById('channel-name').value = data.name;
|
||||
document.getElementById('channel-url').value = data.gateway_url;
|
||||
document.getElementById('channel-reconnect').value = data.reconnect_interval;
|
||||
document.getElementById('channel-heartbeat').value = data.heartbeat_interval;
|
||||
document.getElementById('channel-enabled').checked = data.enabled;
|
||||
showModal('channel-modal');
|
||||
});
|
||||
}
|
||||
|
||||
// 删除频道
|
||||
function deleteChannel(id) {
|
||||
if (!confirm('确定要删除此配置吗?')) return;
|
||||
|
||||
fetch(`/api/web/channels/${id}`, { method: 'DELETE' })
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('删除成功', 'success');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast('删除失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
document.getElementById('channel-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('channel-id').value;
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const url = id ? `/api/web/channels/${id}` : '/api/web/channels';
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData);
|
||||
data.enabled = !!data.enabled;
|
||||
data.reconnect_interval = parseInt(data.reconnect_interval);
|
||||
data.heartbeat_interval = parseInt(data.heartbeat_interval);
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast(id ? '更新成功' : '创建成功', 'success');
|
||||
hideModal('channel-modal');
|
||||
location.reload();
|
||||
} else {
|
||||
showToast('保存失败', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 显示/隐藏模态框
|
||||
function showModal(id) {
|
||||
document.getElementById(id).classList.remove('hidden');
|
||||
document.getElementById(id).classList.add('flex');
|
||||
}
|
||||
|
||||
function hideModal(id) {
|
||||
document.getElementById(id).classList.add('hidden');
|
||||
document.getElementById(id).classList.remove('flex');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
129
app/templates/index.html
Normal file
129
app/templates/index.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}仪表盘 - 智队中枢{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{# 页面标题 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold dark:text-white">仪表盘</h1>
|
||||
<button onclick="location.reload()" class="btn btn-secondary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# 统计卡片 #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{# 在线 Agent #}
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="stat-value">{{ stats.online_agents }}</p>
|
||||
<p class="stat-label">在线 Agent</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-primary-100 dark:bg-primary-900/30 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 活跃会话 #}
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="stat-value">{{ stats.active_sessions }}</p>
|
||||
<p class="stat-label">活跃会话</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/30 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 今日消息 #}
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="stat-value">{{ stats.today_messages }}</p>
|
||||
<p class="stat-label">今日消息</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 在线 Gateway #}
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="stat-value">{{ stats.online_gateways }}</p>
|
||||
<p class="stat-label">在线 Gateway</p>
|
||||
</div>
|
||||
<div class="w-12 h-12 bg-purple-100 dark:bg-purple-900/30 rounded-lg flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 最近会话 #}
|
||||
<div class="card">
|
||||
<div class="card-header flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold dark:text-white">最近会话</h2>
|
||||
<a href="{{ url_for('web.sessions') }}" class="text-sm text-primary-600 hover:text-primary-700 dark:text-primary-400">
|
||||
查看全部 →
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户</th>
|
||||
<th>Agent</th>
|
||||
<th>消息数</th>
|
||||
<th>状态</th>
|
||||
<th>最后活跃</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for session in recent_sessions %}
|
||||
<tr>
|
||||
<td class="font-medium">{{ session.user.username }}</td>
|
||||
<td>{{ session.primary_agent.name if session.primary_agent else '-' }}</td>
|
||||
<td>{{ session.message_count }}</td>
|
||||
<td>
|
||||
{% if session.status == 'active' %}
|
||||
<span class="badge badge-success">活跃</span>
|
||||
{% elif session.status == 'paused' %}
|
||||
<span class="badge badge-warning">暂停</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">关闭</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-gray-500 dark:text-gray-400 text-sm">{{ session.last_active_at.strftime('%Y-%m-%d %H:%M') if session.last_active_at else '-' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
暂无会话数据
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
278
app/templates/monitor/sessions.html
Normal file
278
app/templates/monitor/sessions.html
Normal file
@@ -0,0 +1,278 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}会话监控 - 智队中枢{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{# 页面标题 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold dark:text-white">会话监控</h1>
|
||||
<button onclick="loadSessions()" class="btn btn-secondary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# 筛选 #}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">Agent</label>
|
||||
<select id="filter-agent" class="select" onchange="loadSessions()">
|
||||
<option value="">全部</option>
|
||||
{% for agent in agents %}
|
||||
<option value="{{ agent.id }}">{{ agent.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[150px]">
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">状态</label>
|
||||
<select id="filter-status" class="select" onchange="loadSessions()">
|
||||
<option value="">全部</option>
|
||||
<option value="active">活跃</option>
|
||||
<option value="paused">暂停</option>
|
||||
<option value="closed">已关闭</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 会话列表 #}
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户</th>
|
||||
<th>Agent</th>
|
||||
<th>消息数</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>最后活跃</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sessions-body">
|
||||
{% for session in sessions %}
|
||||
<tr>
|
||||
<td class="font-mono text-xs">{{ session.id[:8] }}...</td>
|
||||
<td class="font-medium">{{ session.user.username }}</td>
|
||||
<td>{{ session.primary_agent.name if session.primary_agent else '-' }}</td>
|
||||
<td>{{ session.message_count }}</td>
|
||||
<td>
|
||||
{% if session.status == 'active' %}
|
||||
<span class="badge badge-success">活跃</span>
|
||||
{% elif session.status == 'paused' %}
|
||||
<span class="badge badge-warning">暂停</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">关闭</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-gray-500 dark:text-gray-400 text-sm">{{ session.created_at.strftime('%m-%d %H:%M') if session.created_at else '-' }}</td>
|
||||
<td class="text-gray-500 dark:text-gray-400 text-sm">{{ session.last_active_at.strftime('%m-%d %H:%M') if session.last_active_at else '-' }}</td>
|
||||
<td>
|
||||
<button onclick="viewSession('{{ session.id }}')"
|
||||
class="p-2 text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-lg transition-colors"
|
||||
title="查看详情">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
暂无会话数据
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 会话详情模态框 #}
|
||||
<div id="session-modal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="card-header flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold dark:text-white">会话详情</h3>
|
||||
<button onclick="hideModal('session-modal')" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto">
|
||||
{# 会话信息 #}
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">用户</div>
|
||||
<div id="session-user" class="font-medium">-</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Agent</div>
|
||||
<div id="session-agent" class="font-medium">-</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">状态</div>
|
||||
<div id="session-status">-</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">创建时间</div>
|
||||
<div id="session-created" class="text-sm">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 消息记录 #}
|
||||
<div class="p-4">
|
||||
<h4 class="font-medium mb-3 dark:text-white">消息记录</h4>
|
||||
<div id="session-messages" class="space-y-3 max-h-96 overflow-auto">
|
||||
{# 动态加载 #}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<button onclick="exportSession()" class="btn btn-secondary">导出记录</button>
|
||||
<button onclick="closeSession()" class="btn btn-danger">关闭会话</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentSessionId = null;
|
||||
|
||||
function loadSessions() {
|
||||
const agentId = document.getElementById('filter-agent').value;
|
||||
const status = document.getElementById('filter-status').value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (agentId) params.append('agent_id', agentId);
|
||||
if (status) params.append('status', status);
|
||||
|
||||
fetch(`/api/web/sessions?${params}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const tbody = document.getElementById('sessions-body');
|
||||
if (data.sessions.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400">暂无数据</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.sessions.map(s => `
|
||||
<tr>
|
||||
<td class="font-mono text-xs">${s.id.substring(0, 8)}...</td>
|
||||
<td class="font-medium">${s.user?.username || '-'}</td>
|
||||
<td>${s.primary_agent?.name || '-'}</td>
|
||||
<td>${s.message_count}</td>
|
||||
<td>
|
||||
${s.status === 'active' ? '<span class="badge badge-success">活跃</span>' :
|
||||
s.status === 'paused' ? '<span class="badge badge-warning">暂停</span>' :
|
||||
'<span class="badge badge-danger">关闭</span>'}
|
||||
</td>
|
||||
<td class="text-gray-500 dark:text-gray-400 text-sm">${s.created_at ? new Date(s.created_at).toLocaleString() : '-'}</td>
|
||||
<td class="text-gray-500 dark:text-gray-400 text-sm">${s.last_active_at ? new Date(s.last_active_at).toLocaleString() : '-'}</td>
|
||||
<td>
|
||||
<button onclick="viewSession('${s.id}')" class="p-2 text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-lg">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
function viewSession(id) {
|
||||
currentSessionId = id;
|
||||
|
||||
fetch(`/api/web/sessions/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const session = data.session;
|
||||
|
||||
document.getElementById('session-user').textContent = session.user?.username || '-';
|
||||
document.getElementById('session-agent').textContent = session.primary_agent?.name || '-';
|
||||
document.getElementById('session-status').innerHTML = session.status === 'active' ?
|
||||
'<span class="badge badge-success">活跃</span>' :
|
||||
session.status === 'paused' ?
|
||||
'<span class="badge badge-warning">暂停</span>' :
|
||||
'<span class="badge badge-danger">关闭</span>';
|
||||
document.getElementById('session-created').textContent = session.created_at ?
|
||||
new Date(session.created_at).toLocaleString() : '-';
|
||||
|
||||
// 加载消息
|
||||
loadSessionMessages(id);
|
||||
|
||||
showModal('session-modal');
|
||||
});
|
||||
}
|
||||
|
||||
function loadSessionMessages(sessionId) {
|
||||
fetch(`/api/web/sessions/${sessionId}/messages`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById('session-messages');
|
||||
|
||||
if (data.messages.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500 dark:text-gray-400">暂无消息</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = data.messages.map(m => `
|
||||
<div class="flex ${m.sender_type === 'user' ? 'justify-start' : 'justify-end'}">
|
||||
<div class="max-w-[70%] ${m.sender_type === 'user' ? 'bg-gray-100 dark:bg-gray-700' : 'bg-primary-100 dark:bg-primary-900/30'} rounded-lg px-4 py-2">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
${m.sender_type === 'user' ? (m.sender_id || '用户') : (m.sender_id || 'Agent')}
|
||||
</div>
|
||||
<div class="text-sm">${m.content}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">${new Date(m.created_at).toLocaleTimeString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
});
|
||||
}
|
||||
|
||||
function closeSession() {
|
||||
if (!currentSessionId) return;
|
||||
if (!confirm('确定要关闭此会话吗?')) return;
|
||||
|
||||
fetch(`/api/web/sessions/${currentSessionId}/close`, { method: 'PUT' })
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('会话已关闭', 'success');
|
||||
hideModal('session-modal');
|
||||
loadSessions();
|
||||
} else {
|
||||
showToast('关闭失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportSession() {
|
||||
if (!currentSessionId) return;
|
||||
window.open(`/api/web/sessions/${currentSessionId}/export`, '_blank');
|
||||
}
|
||||
|
||||
function showModal(id) {
|
||||
document.getElementById(id).classList.remove('hidden');
|
||||
document.getElementById(id).classList.add('flex');
|
||||
}
|
||||
|
||||
function hideModal(id) {
|
||||
document.getElementById(id).classList.add('hidden');
|
||||
document.getElementById(id).classList.remove('flex');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
159
app/templates/test/index.html
Normal file
159
app/templates/test/index.html
Normal file
@@ -0,0 +1,159 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}连接测试 - 智队中枢{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{# 页面标题 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold dark:text-white">连接测试</h1>
|
||||
</div>
|
||||
|
||||
{# 测试控制面板 #}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">选择频道</label>
|
||||
<select id="test-channel" class="select">
|
||||
<option value="">请选择频道...</option>
|
||||
{% for config in configs %}
|
||||
<option value="{{ config.id }}">{{ config.name }} ({{ config.gateway_url }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="startTest()" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
开始测试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 测试结果 #}
|
||||
<div id="test-result" class="card hidden">
|
||||
<div class="card-header">
|
||||
<h2 class="text-lg font-semibold dark:text-white">测试结果</h2>
|
||||
</div>
|
||||
<div class="card-body space-y-4">
|
||||
{# 结果概览 #}
|
||||
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="result-icon" class="w-10 h-10 rounded-full flex items-center justify-center">
|
||||
{# JS 将填充 #}
|
||||
</div>
|
||||
<div>
|
||||
<div id="result-status" class="font-semibold text-lg">-</div>
|
||||
<div id="result-time" class="text-sm text-gray-500 dark:text-gray-400">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="startTest()" class="btn btn-secondary text-sm">
|
||||
重新测试
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# 详细日志 #}
|
||||
<div>
|
||||
<h3 class="text-sm font-medium mb-2 dark:text-gray-300">测试日志</h3>
|
||||
<div id="test-logs" class="log-area">
|
||||
{# JS 将填充 #}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 测试说明 #}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="font-semibold mb-3 dark:text-white">测试说明</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>测试将依次验证:WebSocket 连接、认证、心跳、消息收发</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>请确保频道配置正确且目标服务正在运行</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>测试日志将实时显示每一步的详细结果</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let eventSource = null;
|
||||
|
||||
function startTest() {
|
||||
const channelId = document.getElementById('test-channel').value;
|
||||
if (!channelId) {
|
||||
showToast('请选择频道', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示结果区域
|
||||
document.getElementById('test-result').classList.remove('hidden');
|
||||
|
||||
// 清空日志
|
||||
document.getElementById('test-logs').innerHTML = '<div class="log-info">[INFO] 开始测试...</div>';
|
||||
|
||||
// 使用 SSE 接收实时日志
|
||||
eventSource = new EventSource(`/api/web/channels/${channelId}/test/stream`);
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
addLog(data.level, data.message);
|
||||
|
||||
if (data.complete) {
|
||||
eventSource.close();
|
||||
showResult(data.success, data.response_time);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function() {
|
||||
addLog('error', '连接中断');
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
|
||||
function addLog(level, message) {
|
||||
const logArea = document.getElementById('test-logs');
|
||||
const time = new Date().toLocaleTimeString();
|
||||
const entry = `<div class="log-entry log-${level}">[${time}] ${message}</div>`;
|
||||
logArea.innerHTML += entry;
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
function showResult(success, responseTime) {
|
||||
const icon = document.getElementById('result-icon');
|
||||
const status = document.getElementById('result-status');
|
||||
const time = document.getElementById('result-time');
|
||||
|
||||
if (success) {
|
||||
icon.innerHTML = '<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
|
||||
icon.className = 'w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center';
|
||||
status.textContent = '测试通过';
|
||||
status.className = 'font-semibold text-lg text-green-600 dark:text-green-400';
|
||||
} else {
|
||||
icon.innerHTML = '<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
|
||||
icon.className = 'w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center';
|
||||
status.textContent = '测试失败';
|
||||
status.className = 'font-semibold text-lg text-red-600 dark:text-red-400';
|
||||
}
|
||||
|
||||
time.textContent = `响应时间: ${responseTime}ms | 测试时间: ${new Date().toLocaleString()}`;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user