feat: Web服务区域新增添加Web服务功能
- 添加新增Web服务按钮到筛选栏 - 实现完整的Web服务添加表单(ID、名称、端口、目录、启动命令等) - 新增的Web服务具有所有现有功能(启停、日志、状态监控) - 移除之前的文本块功能
This commit is contained in:
@@ -446231,3 +446231,5 @@
|
||||
127.0.0.1 - - [25/Jun/2026 12:11:07] "GET /api/projects HTTP/1.1" 200 -
|
||||
[2026-06-25 12:19:01] 项目服务管理面板 v3.4.1 初始化完成
|
||||
[2026-06-25 12:19:01] 启动 项目服务管理面板,端口: 16023
|
||||
[2026-06-25 12:29:07] 项目服务管理面板 v3.4.1 初始化完成
|
||||
[2026-06-25 12:29:07] 启动 项目服务管理面板,端口: 16023
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
<button onclick="filterType('cli')" class="filter-bar-btn" data-type="cli"><i class="ri-terminal-box-line"></i> CLI工具</button>
|
||||
<button onclick="filterType('extension')" class="filter-bar-btn" data-type="extension"><i class="ri-chrome-line"></i> 插件</button>
|
||||
<button onclick="showAddServiceModal()" class="filter-bar-btn add-btn"><i class="ri-add-circle-line"></i> 新增服务</button>
|
||||
<button onclick="showTextBlockModal()" class="filter-bar-btn add-btn"><i class="ri-file-text-line"></i> 新增文本块</button>
|
||||
<button onclick="showAddWebModal()" class="filter-bar-btn add-btn"><i class="ri-server-line"></i> 新增Web服务</button>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
@@ -751,42 +751,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑文本块模态框 -->
|
||||
<div id="textBlockModal" class="modal-overlay hidden">
|
||||
<!-- 新增Web服务模态框 -->
|
||||
<div id="addWebModal" class="modal-overlay hidden">
|
||||
<div class="card rounded-xl modal-content p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 id="textBlockModalTitle" class="font-bold text-lg">新增文本块</h3>
|
||||
<button onclick="closeTextBlockModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||
<h3 class="font-bold text-lg">新增Web服务</h3>
|
||||
<button onclick="closeAddWebModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
|
||||
<form id="textBlockForm" onsubmit="saveTextBlock(event)">
|
||||
<input type="hidden" id="textBlockId">
|
||||
|
||||
<form id="addWebForm" onsubmit="saveWebProject(event)">
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">标题 *</label>
|
||||
<input type="text" id="textBlockTitle" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:系统公告、使用说明">
|
||||
<label class="block text-gray-400 text-sm mb-1">服务ID *</label>
|
||||
<input type="text" id="webProjectId" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="唯一标识,如:my-service">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">内容 *</label>
|
||||
<textarea id="textBlockContent" required rows="6" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="输入文本内容,支持多行"></textarea>
|
||||
<label class="block text-gray-400 text-sm mb-1">服务名称 *</label>
|
||||
<input type="text" id="webProjectName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="显示名称">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">颜色主题</label>
|
||||
<select id="textBlockColor" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
<option value="blue">蓝色(信息)</option>
|
||||
<option value="green">绿色(成功)</option>
|
||||
<option value="yellow">黄色(警告)</option>
|
||||
<option value="red">红色(重要)</option>
|
||||
<option value="purple">紫色(提示)</option>
|
||||
<option value="gray">灰色(普通)</option>
|
||||
</select>
|
||||
<label class="block text-gray-400 text-sm mb-1">端口 *</label>
|
||||
<input type="text" id="webProjectPorts" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="多个端口用逗号分隔,如:8080,8081">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">项目目录</label>
|
||||
<input type="text" id="webProjectDir" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="相对于works目录,如:tech-blog">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">启动命令</label>
|
||||
<input type="text" id="webProjectStartCmd" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="如:python3 app.py">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">停止命令</label>
|
||||
<input type="text" id="webProjectStopCmd" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="可选">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">后台URL</label>
|
||||
<input type="text" id="webProjectAdminUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="如:http://localhost:8080/admin">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">描述</label>
|
||||
<input type="text" id="webProjectDesc" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="简要描述">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeTextBlockModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
<button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">保存</button>
|
||||
<button type="button" onclick="closeAddWebModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
<button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">添加</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1611,12 +1627,10 @@
|
||||
const activeProjs = projs.filter(p => activeStatus[p.id] !== false);
|
||||
const archivedProjs = projs.filter(p => activeStatus[p.id] === false);
|
||||
|
||||
if (activeProjs.length > 0 || getTextBlocks().length > 0) {
|
||||
const textBlocks = getTextBlocks();
|
||||
if (activeProjs.length > 0) {
|
||||
const secId = 'section-web-active';
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length}${textBlocks.length > 0 ? '+' + textBlocks.length + '文本块' : ''})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
|
||||
activeProjs.forEach(p => { html += renderProjectCard(p, true); });
|
||||
textBlocks.forEach(b => { html += renderTextBlock(b); });
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
@@ -1633,18 +1647,6 @@
|
||||
html += '</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有Web服务项目但有文本块,单独渲染文本块区域
|
||||
if (!grouped['web'] && (currentFilter === 'all' || currentFilter === 'web')) {
|
||||
const textBlocks = getTextBlocks();
|
||||
if (textBlocks.length > 0) {
|
||||
const secId = 'section-web-active';
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="ri-server-line text-blue-400"></i>Web服务<span class="text-sm text-gray-500">(${textBlocks.length}文本块)</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
|
||||
textBlocks.forEach(b => { html += renderTextBlock(b); });
|
||||
html += '</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
list.innerHTML = html;
|
||||
// 应用已保存的折叠状态
|
||||
setTimeout(() => {
|
||||
@@ -1872,124 +1874,66 @@
|
||||
localStorage.setItem('customServices', JSON.stringify(services));
|
||||
}
|
||||
|
||||
// ==================== 文本块管理 ====================
|
||||
// ==================== 新增Web服务管理 ====================
|
||||
|
||||
function getTextBlocks() {
|
||||
const stored = localStorage.getItem('textBlocks');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
function showAddWebModal() {
|
||||
document.getElementById('addWebForm').reset();
|
||||
document.getElementById('addWebModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function saveTextBlocks(blocks) {
|
||||
localStorage.setItem('textBlocks', JSON.stringify(blocks));
|
||||
function closeAddWebModal() {
|
||||
document.getElementById('addWebModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function showTextBlockModal(blockId = null) {
|
||||
const modal = document.getElementById('textBlockModal');
|
||||
const title = document.getElementById('textBlockModalTitle');
|
||||
const form = document.getElementById('textBlockForm');
|
||||
|
||||
if (blockId) {
|
||||
// 编辑模式
|
||||
const blocks = getTextBlocks();
|
||||
const block = blocks.find(b => b.id === blockId);
|
||||
if (!block) return;
|
||||
|
||||
title.textContent = '编辑文本块';
|
||||
document.getElementById('textBlockId').value = block.id;
|
||||
document.getElementById('textBlockTitle').value = block.title;
|
||||
document.getElementById('textBlockContent').value = block.content;
|
||||
document.getElementById('textBlockColor').value = block.color || 'blue';
|
||||
} else {
|
||||
// 新增模式
|
||||
title.textContent = '新增文本块';
|
||||
form.reset();
|
||||
document.getElementById('textBlockId').value = '';
|
||||
}
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeTextBlockModal() {
|
||||
document.getElementById('textBlockModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function saveTextBlock(event) {
|
||||
function saveWebProject(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const id = document.getElementById('textBlockId').value;
|
||||
const title = document.getElementById('textBlockTitle').value.trim();
|
||||
const content = document.getElementById('textBlockContent').value.trim();
|
||||
const color = document.getElementById('textBlockColor').value;
|
||||
const id = document.getElementById('webProjectId').value.trim();
|
||||
const name = document.getElementById('webProjectName').value.trim();
|
||||
const portsStr = document.getElementById('webProjectPorts').value.trim();
|
||||
const directory = document.getElementById('webProjectDir').value.trim();
|
||||
const startCmd = document.getElementById('webProjectStartCmd').value.trim();
|
||||
const stopCmd = document.getElementById('webProjectStopCmd').value.trim();
|
||||
const adminUrl = document.getElementById('webProjectAdminUrl').value.trim();
|
||||
const description = document.getElementById('webProjectDesc').value.trim();
|
||||
|
||||
if (!title || !content) {
|
||||
alert('请填写标题和内容');
|
||||
// 解析端口
|
||||
const ports = portsStr.split(',').map(p => parseInt(p.trim())).filter(p => !isNaN(p));
|
||||
if (ports.length === 0) {
|
||||
alert('请输入有效的端口号');
|
||||
return;
|
||||
}
|
||||
|
||||
const blocks = getTextBlocks();
|
||||
|
||||
if (id) {
|
||||
// 更新现有文本块
|
||||
const index = blocks.findIndex(b => b.id === id);
|
||||
if (index !== -1) {
|
||||
blocks[index] = { ...blocks[index], title, content, color, updatedAt: new Date().toISOString() };
|
||||
}
|
||||
} else {
|
||||
// 创建新文本块
|
||||
const newBlock = {
|
||||
id: 'tb_' + Date.now(),
|
||||
title,
|
||||
content,
|
||||
color,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
blocks.push(newBlock);
|
||||
}
|
||||
|
||||
saveTextBlocks(blocks);
|
||||
closeTextBlockModal();
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function deleteTextBlock(blockId) {
|
||||
if (!confirm('确定删除此文本块?')) return;
|
||||
|
||||
const blocks = getTextBlocks();
|
||||
const filtered = blocks.filter(b => b.id !== blockId);
|
||||
saveTextBlocks(filtered);
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function renderTextBlock(block) {
|
||||
const colorMap = {
|
||||
blue: { border: 'border-blue-500/30', bg: 'bg-blue-500/10', text: 'text-blue-400', icon: 'ri-information-line' },
|
||||
green: { border: 'border-green-500/30', bg: 'bg-green-500/10', text: 'text-green-400', icon: 'ri-check-line' },
|
||||
yellow: { border: 'border-yellow-500/30', bg: 'bg-yellow-500/10', text: 'text-yellow-400', icon: 'ri-alert-line' },
|
||||
red: { border: 'border-red-500/30', bg: 'bg-red-500/10', text: 'text-red-400', icon: 'ri-error-warning-line' },
|
||||
purple: { border: 'border-purple-500/30', bg: 'bg-purple-500/10', text: 'text-purple-400', icon: 'ri-lightbulb-line' },
|
||||
gray: { border: 'border-gray-500/30', bg: 'bg-gray-500/10', text: 'text-gray-400', icon: 'ri-file-text-line' }
|
||||
const data = {
|
||||
id: id,
|
||||
name: name,
|
||||
type: 'web',
|
||||
ports: ports,
|
||||
directory: directory,
|
||||
start_cmd: startCmd || 'python3 app.py',
|
||||
stop_cmd: stopCmd,
|
||||
admin_url: adminUrl,
|
||||
description: description
|
||||
};
|
||||
|
||||
const c = colorMap[block.color] || colorMap.blue;
|
||||
const contentLines = block.content.split('\n').filter(l => l.trim()).slice(0, 5).join('<br>');
|
||||
const hasMore = block.content.split('\n').filter(l => l.trim()).length > 5;
|
||||
|
||||
return `
|
||||
<div class="card rounded-lg p-3 hover:border-gray-500 transition-colors ${c.border} ${c.bg}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="${c.icon} ${c.text}"></i>
|
||||
<h3 class="font-semibold text-sm truncate ${c.text}">${block.title}</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<button onclick="showTextBlockModal('${block.id}')" class="text-blue-400 hover:text-blue-300" title="编辑"><i class="ri-edit-line text-xs"></i></button>
|
||||
<button onclick="deleteTextBlock('${block.id}')" class="text-red-400 hover:text-red-300" title="删除"><i class="ri-delete-bin-line text-xs"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-gray-300 text-xs leading-relaxed whitespace-pre-line">${contentLines}${hasMore ? '<span class="text-gray-500">...</span>' : ''}</div>
|
||||
</div>
|
||||
`;
|
||||
fetch('/api/projects/add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(result => {
|
||||
if (result.error) {
|
||||
alert('错误: ' + result.error);
|
||||
return;
|
||||
}
|
||||
closeAddWebModal();
|
||||
loadProjects();
|
||||
alert('Web服务添加成功!');
|
||||
})
|
||||
.catch(err => {
|
||||
alert('添加失败: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 邮件通知规则管理 ====================
|
||||
|
||||
Reference in New Issue
Block a user