Files
plugins/example/demo_utils_client.html

856 lines
35 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DemoUtilsPlugin 测试客户端</title>
<style>
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1, h2, h3 {
color: #005e8a;
}
h1 {
text-align: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.section {
margin-bottom: 30px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 5px;
border-left: 4px solid #005e8a;
}
.panel {
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
margin-bottom: 20px;
background-color: white;
}
.panel-header {
display: flex;
justify-content: space-between;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 15px;
font-weight: bold;
}
button {
background-color: #005e8a;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #004a6e;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, select, textarea {
width: 100%;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.input-group {
margin-bottom: 15px;
}
#operationForm {
max-width: 600px;
}
#resultPanel, #eventPanel {
max-height: 400px;
overflow-y: auto;
font-family: monospace;
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.loading {
text-align: center;
margin: 20px 0;
font-style: italic;
color: #666;
}
.success {
color: #28a745;
font-weight: bold;
}
.error {
color: #dc3545;
font-weight: bold;
}
.tab-container {
margin-bottom: 20px;
}
.tab-header {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
.tab-button {
padding: 8px 15px;
background-color: #f0f0f0;
border: none;
margin-right: 5px;
border-radius: 4px 4px 0 0;
cursor: pointer;
}
.tab-button.active {
background-color: #005e8a;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
}
.event-log {
padding: 5px;
margin: 5px 0;
border-bottom: 1px solid #eee;
}
.event-timestamp {
color: #666;
font-size: 0.8em;
}
.parameter-row {
display: flex;
margin-bottom: 10px;
}
.parameter-row .key {
flex: 1;
padding-right: 10px;
}
.parameter-row .value {
flex: 2;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group input {
width: auto;
margin-right: 10px;
}
#actionsList {
margin-bottom: 20px;
max-height: 200px;
overflow-y: auto;
}
.action-item {
display: flex;
justify-content: space-between;
padding: 8px;
background-color: #f0f0f0;
margin-bottom: 5px;
border-radius: 4px;
}
.action-item:hover {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<h1>DemoUtilsPlugin 测试客户端</h1>
<div class="section">
<h2>插件状态</h2>
<div class="panel">
<div class="panel-header">
<span>插件信息</span>
<button id="refreshStatus">刷新状态</button>
</div>
<div id="pluginStatus">正在加载插件信息...</div>
</div>
</div>
<div class="section">
<h2>操作调用</h2>
<div class="tab-container">
<div class="tab-header">
<button class="tab-button active" data-tab="operations">可用操作</button>
<button class="tab-button" data-tab="execute">执行操作</button>
<button class="tab-button" data-tab="events">事件监听</button>
</div>
<!-- 可用操作列表 -->
<div id="operations" class="tab-content active">
<p>以下是 DemoUtilsPlugin 支持的所有操作:</p>
<div id="actionsList">
正在加载操作列表...
</div>
</div>
<!-- 执行操作表单 -->
<div id="execute" class="tab-content">
<div class="grid">
<div class="panel">
<div class="panel-header">执行操作</div>
<form id="operationForm">
<div class="input-group">
<label for="operation">选择操作:</label>
<select id="operation" name="operation" required>
<option value="">-- 请选择操作 --</option>
</select>
</div>
<div id="parameterFields">
<!-- 参数字段将在这里动态生成 -->
</div>
<button type="submit">执行</button>
</form>
</div>
<div class="panel">
<div class="panel-header">执行结果</div>
<div id="resultPanel">
尚未执行任何操作。
</div>
</div>
</div>
</div>
<!-- 事件监听 -->
<div id="events" class="tab-content">
<div class="grid">
<div class="panel">
<div class="panel-header">事件订阅</div>
<div class="input-group">
<label>订阅事件类型:</label>
<div class="checkbox-group">
<input type="checkbox" id="eventInitialized" value="initialized" checked>
<label for="eventInitialized">初始化</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="eventStarted" value="started" checked>
<label for="eventStarted">启动</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="eventStopped" value="stopped" checked>
<label for="eventStopped">停止</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="eventCustom" value="custom" checked>
<label for="eventCustom">自定义</label>
</div>
</div>
<button id="subscribeEvents">订阅事件</button>
<button id="unsubscribeEvents">取消订阅</button>
<button id="triggerCustomEvent">触发自定义事件</button>
</div>
<div class="panel">
<div class="panel-header">
<span>事件日志</span>
<button id="clearEvents">清空</button>
</div>
<div id="eventPanel">
尚未接收到任何事件。
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section">
<h2>快速操作</h2>
<button id="testCalculate">计算测试 (10*5)</button>
<button id="testFormat">格式化测试 (大写转换)</button>
<button id="testStore">数据存储测试</button>
<button id="testRetrieve">数据检索测试</button>
<button id="testCounter">计数器操作测试</button>
<button id="testSort">排序测试</button>
</div>
</div>
<script>
// 定义全局变量
const PLUGIN_NAME = 'DemoUtilsPlugin';
let operations = [];
let isSubscribed = false;
let eventSource = null;
// 当文档加载完成时执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化标签页切换
initTabs();
// 加载插件状态
loadPluginStatus();
// 加载操作列表
loadOperations();
// 绑定表单提交事件
document.getElementById('operationForm').addEventListener('submit', executeOperation);
// 绑定操作选择变化事件
document.getElementById('operation').addEventListener('change', updateParameterFields);
// 绑定事件相关按钮
document.getElementById('subscribeEvents').addEventListener('click', subscribeEvents);
document.getElementById('unsubscribeEvents').addEventListener('click', unsubscribeEvents);
document.getElementById('clearEvents').addEventListener('click', clearEventLog);
document.getElementById('triggerCustomEvent').addEventListener('click', triggerCustomEvent);
// 绑定刷新状态按钮
document.getElementById('refreshStatus').addEventListener('click', loadPluginStatus);
// 绑定快速操作按钮
document.getElementById('testCalculate').addEventListener('click', () => quickTestOperation('calculate'));
document.getElementById('testFormat').addEventListener('click', () => quickTestOperation('format'));
document.getElementById('testStore').addEventListener('click', () => quickTestOperation('store'));
document.getElementById('testRetrieve').addEventListener('click', () => quickTestOperation('retrieve'));
document.getElementById('testCounter').addEventListener('click', () => quickTestOperation('counter'));
document.getElementById('testSort').addEventListener('click', () => quickTestOperation('sort'));
});
// 初始化标签页
function initTabs() {
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', function() {
// 移除所有活动类
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 添加活动类
this.classList.add('active');
const tabId = this.getAttribute('data-tab');
document.getElementById(tabId).classList.add('active');
});
});
}
// 加载插件状态
async function loadPluginStatus() {
try {
const statusElem = document.getElementById('pluginStatus');
statusElem.innerHTML = '正在加载插件信息...';
const response = await fetch('/api/plugins');
const plugins = await response.json();
const demoPlugin = plugins.find(p => p.name === PLUGIN_NAME);
if (demoPlugin) {
let html = `
<div>
<p><strong>名称:</strong> ${demoPlugin.name}</p>
<p><strong>版本:</strong> ${demoPlugin.version}</p>
<p><strong>描述:</strong> ${demoPlugin.description}</p>
<p><strong>作者:</strong> ${demoPlugin.author}</p>
<p><strong>类型:</strong> ${demoPlugin.type}</p>
<p><strong>状态:</strong> <span class="${demoPlugin.enabled ? 'success' : 'error'}">${demoPlugin.enabled ? '已启用' : '已禁用'}</span></p>
</div>
`;
if (demoPlugin.config && Object.keys(demoPlugin.config).length > 0) {
html += '<p><strong>配置:</strong></p><ul>';
for (const [key, value] of Object.entries(demoPlugin.config)) {
html += `<li><strong>${key}:</strong> ${value}</li>`;
}
html += '</ul>';
}
statusElem.innerHTML = html;
} else {
statusElem.innerHTML = '<p class="error">未找到 DemoUtilsPlugin 插件。请确保它已正确加载。</p>';
}
} catch (error) {
document.getElementById('pluginStatus').innerHTML = `<p class="error">加载插件状态失败: ${error.message}</p>`;
}
}
// 加载操作列表
async function loadOperations() {
try {
const actionsListElem = document.getElementById('actionsList');
const operationSelectElem = document.getElementById('operation');
actionsListElem.innerHTML = '正在加载操作列表...';
const response = await fetch(`/api/operations/${PLUGIN_NAME}`);
const data = await response.json();
if (data && data.operations) {
operations = data.operations;
let html = '';
operationSelectElem.innerHTML = '<option value="">-- 请选择操作 --</option>';
operations.forEach(op => {
// 更新可用操作列表
html += `
<div class="action-item">
<span><strong>${op.name}</strong>: ${op.description || '无描述'}</span>
<button onclick="selectOperation('${op.name}')">使用此操作</button>
</div>
`;
// 更新操作选择下拉框
operationSelectElem.innerHTML += `<option value="${op.name}">${op.name} - ${op.description || '无描述'}</option>`;
});
actionsListElem.innerHTML = html || '<p>没有可用的操作。</p>';
} else {
actionsListElem.innerHTML = '<p class="error">未找到任何操作。</p>';
operationSelectElem.innerHTML = '<option value="">无可用操作</option>';
}
} catch (error) {
document.getElementById('actionsList').innerHTML = `<p class="error">加载操作列表失败: ${error.message}</p>`;
}
}
// 选择操作
function selectOperation(operationName) {
// 切换到执行标签页
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.querySelector('[data-tab="execute"]').classList.add('active');
document.getElementById('execute').classList.add('active');
// 设置选定的操作
document.getElementById('operation').value = operationName;
// 更新参数字段
updateParameterFields();
}
// 更新参数字段
function updateParameterFields() {
const operationName = document.getElementById('operation').value;
const parameterFieldsElem = document.getElementById('parameterFields');
if (!operationName) {
parameterFieldsElem.innerHTML = '<p>请先选择一个操作。</p>';
return;
}
const operation = operations.find(op => op.name === operationName);
if (operation && operation.params && operation.params.length > 0) {
let html = '';
operation.params.forEach(param => {
html += `
<div class="input-group">
<label for="param-${param.name}">${param.name}${param.required ? ' *' : ''}:</label>
<div class="parameter-description">${param.description || ''}</div>
`;
if (param.type === 'boolean' || param.type === 'bool') {
html += `
<div class="checkbox-group">
<input type="checkbox" id="param-${param.name}" name="${param.name}" ${param.default ? 'checked' : ''}>
</div>
`;
} else if (param.type === 'integer' || param.type === 'int' || param.type === 'int64') {
html += `
<input type="number" id="param-${param.name}" name="${param.name}" value="${param.default || ''}" ${param.required ? 'required' : ''}>
`;
} else if (param.type === 'float' || param.type === 'float64') {
html += `
<input type="number" id="param-${param.name}" name="${param.name}" step="0.01" value="${param.default || ''}" ${param.required ? 'required' : ''}>
`;
} else {
html += `
<input type="text" id="param-${param.name}" name="${param.name}" value="${param.default || ''}" ${param.required ? 'required' : ''}>
`;
}
html += `</div>`;
});
parameterFieldsElem.innerHTML = html;
} else {
parameterFieldsElem.innerHTML = '<p>此操作不需要参数。</p>';
}
}
// 执行操作
async function executeOperation(event) {
event.preventDefault();
const operationName = document.getElementById('operation').value;
if (!operationName) {
alert('请选择一个操作');
return;
}
const operation = operations.find(op => op.name === operationName);
if (!operation) {
alert('未找到所选操作');
return;
}
// 收集参数
const parameters = {};
if (operation.params && operation.params.length > 0) {
operation.params.forEach(param => {
const element = document.getElementById(`param-${param.name}`);
if (element) {
if (param.type === 'boolean' || param.type === 'bool') {
parameters[param.name] = element.checked;
} else if (param.type === 'integer' || param.type === 'int' || param.type === 'int64') {
parameters[param.name] = parseInt(element.value || '0');
} else if (param.type === 'float' || param.type === 'float64') {
parameters[param.name] = parseFloat(element.value || '0');
} else {
parameters[param.name] = element.value;
}
}
});
}
try {
const resultElem = document.getElementById('resultPanel');
resultElem.innerHTML = '正在执行操作...';
const response = await fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plugin: PLUGIN_NAME,
operation: operationName,
parameters: parameters
})
});
const data = await response.json();
if (data.success) {
resultElem.innerHTML = `
<div class="success">操作成功!</div>
<h3>结果:</h3>
<pre>${formatJSON(data.result)}</pre>
`;
} else {
resultElem.innerHTML = `
<div class="error">操作失败: ${data.error}</div>
<h3>请求参数:</h3>
<pre>${formatJSON(parameters)}</pre>
`;
}
} catch (error) {
document.getElementById('resultPanel').innerHTML = `
<div class="error">执行操作时发生错误: ${error.message}</div>
`;
}
}
// 订阅事件
function subscribeEvents() {
if (isSubscribed) {
alert('已经订阅了事件');
return;
}
const eventTypes = [];
if (document.getElementById('eventInitialized').checked) eventTypes.push('initialized');
if (document.getElementById('eventStarted').checked) eventTypes.push('started');
if (document.getElementById('eventStopped').checked) eventTypes.push('stopped');
if (document.getElementById('eventCustom').checked) eventTypes.push('custom');
if (eventTypes.length === 0) {
alert('请至少选择一种事件类型');
return;
}
// 使用EventSource订阅服务器发送的事件
const eventPanel = document.getElementById('eventPanel');
eventPanel.innerHTML = '<div class="success">正在连接事件流...</div>';
// 创建EventSource连接
eventSource = new EventSource(`/events/${PLUGIN_NAME}`);
// 连接建立时
eventSource.addEventListener('connected', function(e) {
const data = JSON.parse(e.data);
eventPanel.innerHTML = `<div class="success">已成功连接到插件 ${data.pluginName} 的事件流</div>`;
});
// 处理各种事件类型
for (const eventType of ['initialized', 'started', 'stopped', 'custom', 'error', 'loaded']) {
eventSource.addEventListener(eventType, function(e) {
const event = JSON.parse(e.data);
// 根据选中的事件类型过滤
if ((eventType === 'initialized' && !document.getElementById('eventInitialized').checked) ||
(eventType === 'started' && !document.getElementById('eventStarted').checked) ||
(eventType === 'stopped' && !document.getElementById('eventStopped').checked) ||
(eventType === 'custom' && !document.getElementById('eventCustom').checked)) {
return;
}
eventPanel.innerHTML += `
<div class="event-log">
<span class="event-timestamp">[${getCurrentTime()}]</span>
<strong>事件类型: ${eventType}</strong><br>
数据: ${formatJSON(event)}
</div>
`;
// 滚动到底部
eventPanel.scrollTop = eventPanel.scrollHeight;
});
}
// 处理通用消息
eventSource.onmessage = function(e) {
console.log('收到未处理的事件: ', e.data);
};
// 处理连接打开
eventSource.onopen = function() {
console.log('连接已打开');
isSubscribed = true;
document.getElementById('subscribeEvents').disabled = true;
document.getElementById('unsubscribeEvents').disabled = false;
};
// 处理错误
eventSource.onerror = function(e) {
console.error('SSE错误:', e);
eventPanel.innerHTML += `
<div class="event-log error">
<span class="event-timestamp">[${getCurrentTime()}]</span>
<strong>连接错误</strong><br>
尝试重新连接...
</div>
`;
};
}
// 取消订阅事件
function unsubscribeEvents() {
if (!isSubscribed || !eventSource) {
alert('尚未订阅事件');
return;
}
// 关闭EventSource连接
eventSource.close();
eventSource = null;
isSubscribed = false;
document.getElementById('subscribeEvents').disabled = false;
document.getElementById('unsubscribeEvents').disabled = true;
const eventPanel = document.getElementById('eventPanel');
eventPanel.innerHTML += '<div class="event-log"><span class="event-timestamp">[' + getCurrentTime() + ']</span> 已取消订阅事件</div>';
}
// 清空事件日志
function clearEventLog() {
const eventPanel = document.getElementById('eventPanel');
eventPanel.innerHTML = isSubscribed
? '<div class="success">已成功订阅事件,等待事件发生...</div>'
: '尚未接收到任何事件。';
}
// 触发自定义事件
async function triggerCustomEvent() {
try {
const eventPanel = document.getElementById('eventPanel');
// 调用服务器触发自定义事件
const response = await fetch('/api/trigger-event', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plugin: PLUGIN_NAME,
type: 'custom',
data: {
action: 'user_triggered',
time: new Date().toISOString(),
message: '用户触发的自定义事件'
}
})
});
const data = await response.json();
if (data.success) {
eventPanel.innerHTML += `
<div class="event-log">
<span class="event-timestamp">[${getCurrentTime()}]</span>
<span>已触发自定义事件,如果已订阅事件,应该会收到通知</span>
</div>
`;
} else {
eventPanel.innerHTML += `
<div class="event-log error">
<span class="event-timestamp">[${getCurrentTime()}]</span>
<span>触发自定义事件失败: ${data.message || '未知错误'}</span>
</div>
`;
}
} catch (error) {
const eventPanel = document.getElementById('eventPanel');
eventPanel.innerHTML += `
<div class="event-log error">
<span class="event-timestamp">[${getCurrentTime()}]</span>
<span>触发自定义事件时发生错误: ${error.message}</span>
</div>
`;
}
}
// 快速测试操作
async function quickTestOperation(operationType) {
let operationName = operationType;
let parameters = {};
// 根据操作类型设置默认参数
switch (operationType) {
case 'calculate':
parameters = { expression: '10*5' };
break;
case 'format':
parameters = { text: 'hello world', type: 'upper' };
break;
case 'store':
parameters = { key: 'test_key', value: '测试数据 ' + new Date().toLocaleString() };
break;
case 'retrieve':
parameters = { key: 'test_key' };
break;
case 'counter':
parameters = { action: 'increment' };
break;
case 'sort':
parameters = { items: '5,3,8,1,9,2', numeric: true };
break;
}
try {
const resultElem = document.getElementById('resultPanel');
resultElem.innerHTML = `正在执行快速测试: ${operationName}...`;
// 切换到执行标签页
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.querySelector('[data-tab="execute"]').classList.add('active');
document.getElementById('execute').classList.add('active');
const response = await fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
plugin: PLUGIN_NAME,
operation: operationName,
parameters: parameters
})
});
const data = await response.json();
if (data.success) {
resultElem.innerHTML = `
<div class="success">快速测试成功!</div>
<h3>操作: ${operationName}</h3>
<h3>参数:</h3>
<pre>${formatJSON(parameters)}</pre>
<h3>结果:</h3>
<pre>${formatJSON(data.result)}</pre>
`;
} else {
resultElem.innerHTML = `
<div class="error">快速测试失败: ${data.error}</div>
<h3>操作: ${operationName}</h3>
<h3>参数:</h3>
<pre>${formatJSON(parameters)}</pre>
`;
}
} catch (error) {
document.getElementById('resultPanel').innerHTML = `
<div class="error">执行快速测试时发生错误: ${error.message}</div>
`;
}
}
// 辅助函数格式化JSON
function formatJSON(data) {
if (!data) return 'null';
try {
return JSON.stringify(data, null, 2);
} catch (e) {
return String(data);
}
}
// 辅助函数:获取当前时间
function getCurrentTime() {
const now = new Date();
return now.toLocaleTimeString() + '.' + now.getMilliseconds().toString().padStart(3, '0');
}
</script>
</body>
</html>