feat: Web服务区域新增文本块功能
- 添加新增文本块按钮到筛选栏 - 实现文本块增删改查功能 - 支持多种颜色主题(蓝/绿/黄/红/紫/灰) - 文本块显示在Web服务网格中 - 数据存储在localStorage
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user