feat: Web服务区域新增文本块功能

- 添加新增文本块按钮到筛选栏
- 实现文本块增删改查功能
- 支持多种颜色主题(蓝/绿/黄/红/紫/灰)
- 文本块显示在Web服务网格中
- 数据存储在localStorage
This commit is contained in:
2026-06-25 12:19:26 +08:00
parent 701889d22f
commit ffe3dd1fc3
13 changed files with 180 additions and 2 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -446229,3 +446229,5 @@
127.0.0.1 - - [25/Jun/2026 12:10:58] "GET /api/alerts HTTP/1.1" 200 -
127.0.0.1 - - [25/Jun/2026 12:10:58] "GET /api/system/stats 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] 启动 项目服务管理面板,端口: 16023

View File

@@ -177,6 +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>
</div>
<!-- 项目列表 -->
@@ -750,6 +751,47 @@
</div>
</div>
<!-- 新增/编辑文本块模态框 -->
<div id="textBlockModal" 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>
</div>
<form id="textBlockForm" onsubmit="saveTextBlock(event)">
<input type="hidden" id="textBlockId">
<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="例如:系统公告、使用说明">
</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>
</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>
</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>
</div>
</form>
</div>
</div>
<!-- 邮件通知规则模态框 -->
<div id="emailRuleModal" class="modal-overlay hidden">
<div class="card rounded-xl modal-content p-6">
@@ -1569,10 +1611,12 @@
const activeProjs = projs.filter(p => activeStatus[p.id] !== false);
const archivedProjs = projs.filter(p => activeStatus[p.id] === false);
if (activeProjs.length > 0) {
if (activeProjs.length > 0 || getTextBlocks().length > 0) {
const textBlocks = getTextBlocks();
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})</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}${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}">`;
activeProjs.forEach(p => { html += renderProjectCard(p, true); });
textBlocks.forEach(b => { html += renderTextBlock(b); });
html += '</div></div>';
}
@@ -1589,6 +1633,18 @@
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(() => {
@@ -1816,6 +1872,126 @@
localStorage.setItem('customServices', JSON.stringify(services));
}
// ==================== 文本块管理 ====================
function getTextBlocks() {
const stored = localStorage.getItem('textBlocks');
return stored ? JSON.parse(stored) : [];
}
function saveTextBlocks(blocks) {
localStorage.setItem('textBlocks', JSON.stringify(blocks));
}
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) {
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;
if (!title || !content) {
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 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>
`;
}
// ==================== 邮件通知规则管理 ====================
function getEmailRules() {