feat: Web服务区域新增添加Web服务功能

- 添加新增Web服务按钮到筛选栏
- 实现完整的Web服务添加表单(ID、名称、端口、目录、启动命令等)
- 新增的Web服务具有所有现有功能(启停、日志、状态监控)
- 移除之前的文本块功能
This commit is contained in:
2026-06-25 12:29:29 +08:00
parent ffe3dd1fc3
commit 633542a2be
2 changed files with 90 additions and 144 deletions

View File

@@ -446231,3 +446231,5 @@
127.0.0.1 - - [25/Jun/2026 12:11:07] "GET /api/projects HTTP/1.1" 200 - 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] 项目服务管理面板 v3.4.1 初始化完成
[2026-06-25 12:19:01] 启动 项目服务管理面板,端口: 16023 [2026-06-25 12:19:01] 启动 项目服务管理面板,端口: 16023
[2026-06-25 12:29:07] 项目服务管理面板 v3.4.1 初始化完成
[2026-06-25 12:29:07] 启动 项目服务管理面板,端口: 16023

View File

@@ -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('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="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="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> </div>
<!-- 项目列表 --> <!-- 项目列表 -->
@@ -751,42 +751,58 @@
</div> </div>
</div> </div>
<!-- 新增/编辑文本块模态框 --> <!-- 新增Web服务模态框 -->
<div id="textBlockModal" class="modal-overlay hidden"> <div id="addWebModal" class="modal-overlay hidden">
<div class="card rounded-xl modal-content p-6"> <div class="card rounded-xl modal-content p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 id="textBlockModalTitle" class="font-bold text-lg">新增文本块</h3> <h3 class="font-bold text-lg">新增Web服务</h3>
<button onclick="closeTextBlockModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button> <button onclick="closeAddWebModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
</div> </div>
<form id="textBlockForm" onsubmit="saveTextBlock(event)"> <form id="addWebForm" onsubmit="saveWebProject(event)">
<input type="hidden" id="textBlockId">
<div class="mb-3"> <div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">标题 *</label> <label class="block text-gray-400 text-sm mb-1">服务ID *</label>
<input type="text" id="textBlockTitle" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:系统公告、使用说明"> <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>
<div class="mb-3"> <div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">内容 *</label> <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> <input type="text" id="webProjectName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="显示名称">
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="block text-gray-400 text-sm mb-1">颜色主题</label> <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"> <input type="text" id="webProjectPorts" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="多个端口用逗号分隔8080,8081">
<option value="blue">蓝色(信息)</option> </div>
<option value="green">绿色(成功)</option>
<option value="yellow">黄色(警告)</option> <div class="mb-3">
<option value="red">红色(重要)</option> <label class="block text-gray-400 text-sm mb-1">项目目录</label>
<option value="purple">紫色(提示)</option> <input type="text" id="webProjectDir" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="相对于works目录tech-blog">
<option value="gray">灰色(普通)</option> </div>
</select>
<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>
<div class="flex justify-end gap-2"> <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="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> <button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">添加</button>
</div> </div>
</form> </form>
</div> </div>
@@ -1611,12 +1627,10 @@
const activeProjs = projs.filter(p => activeStatus[p.id] !== false); const activeProjs = projs.filter(p => activeStatus[p.id] !== false);
const archivedProjs = projs.filter(p => activeStatus[p.id] === false); const archivedProjs = projs.filter(p => activeStatus[p.id] === false);
if (activeProjs.length > 0 || getTextBlocks().length > 0) { if (activeProjs.length > 0) {
const textBlocks = getTextBlocks();
const secId = 'section-web-active'; 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); }); activeProjs.forEach(p => { html += renderProjectCard(p, true); });
textBlocks.forEach(b => { html += renderTextBlock(b); });
html += '</div></div>'; html += '</div></div>';
} }
@@ -1633,18 +1647,6 @@
html += '</div></div>'; 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; list.innerHTML = html;
// 应用已保存的折叠状态 // 应用已保存的折叠状态
setTimeout(() => { setTimeout(() => {
@@ -1872,124 +1874,66 @@
localStorage.setItem('customServices', JSON.stringify(services)); localStorage.setItem('customServices', JSON.stringify(services));
} }
// ==================== 文本块管理 ==================== // ==================== 新增Web服务管理 ====================
function getTextBlocks() { function showAddWebModal() {
const stored = localStorage.getItem('textBlocks'); document.getElementById('addWebForm').reset();
return stored ? JSON.parse(stored) : []; document.getElementById('addWebModal').classList.remove('hidden');
} }
function saveTextBlocks(blocks) { function closeAddWebModal() {
localStorage.setItem('textBlocks', JSON.stringify(blocks)); document.getElementById('addWebModal').classList.add('hidden');
} }
function showTextBlockModal(blockId = null) { function saveWebProject(event) {
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) {
event.preventDefault(); event.preventDefault();
const id = document.getElementById('textBlockId').value; const id = document.getElementById('webProjectId').value.trim();
const title = document.getElementById('textBlockTitle').value.trim(); const name = document.getElementById('webProjectName').value.trim();
const content = document.getElementById('textBlockContent').value.trim(); const portsStr = document.getElementById('webProjectPorts').value.trim();
const color = document.getElementById('textBlockColor').value; 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; return;
} }
const blocks = getTextBlocks(); const data = {
id: id,
if (id) { name: name,
// 更新现有文本块 type: 'web',
const index = blocks.findIndex(b => b.id === id); ports: ports,
if (index !== -1) { directory: directory,
blocks[index] = { ...blocks[index], title, content, color, updatedAt: new Date().toISOString() }; start_cmd: startCmd || 'python3 app.py',
} stop_cmd: stopCmd,
} else { admin_url: adminUrl,
// 创建新文本块 description: description
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 c = colorMap[block.color] || colorMap.blue; fetch('/api/projects/add', {
const contentLines = block.content.split('\n').filter(l => l.trim()).slice(0, 5).join('<br>'); method: 'POST',
const hasMore = block.content.split('\n').filter(l => l.trim()).length > 5; headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
return ` })
<div class="card rounded-lg p-3 hover:border-gray-500 transition-colors ${c.border} ${c.bg}"> .then(res => res.json())
<div class="flex items-center justify-between mb-2"> .then(result => {
<div class="flex items-center gap-2"> if (result.error) {
<i class="${c.icon} ${c.text}"></i> alert('错误: ' + result.error);
<h3 class="font-semibold text-sm truncate ${c.text}">${block.title}</h3> return;
</div> }
<div class="flex items-center gap-1"> closeAddWebModal();
<button onclick="showTextBlockModal('${block.id}')" class="text-blue-400 hover:text-blue-300" title="编辑"><i class="ri-edit-line text-xs"></i></button> loadProjects();
<button onclick="deleteTextBlock('${block.id}')" class="text-red-400 hover:text-red-300" title="删除"><i class="ri-delete-bin-line text-xs"></i></button> alert('Web服务添加成功');
</div> })
</div> .catch(err => {
<div class="text-gray-300 text-xs leading-relaxed whitespace-pre-line">${contentLines}${hasMore ? '<span class="text-gray-500">...</span>' : ''}</div> alert('添加失败: ' + err.message);
</div> });
`;
} }
// ==================== 邮件通知规则管理 ==================== // ==================== 邮件通知规则管理 ====================