feat: 项目服务列表新增搜索/状态筛选/端口排序
- 新增关键字搜索框:按服务名称/描述/id/端口实时过滤 - 新增状态筛选按钮:全部状态/在线中(running|partial)/有后台(admin_url) - 新增端口排序按钮:点击三态切换 无排序→升序→降序 - 筛选栏分组 + 分隔线,样式与现有主题一致
This commit is contained in:
+81
-3
@@ -28,6 +28,12 @@
|
|||||||
.nav-btn i { font-size: 20px; color: #94a3b8; }
|
.nav-btn i { font-size: 20px; color: #94a3b8; }
|
||||||
.nav-btn:hover i { color: #f1f5f9; }
|
.nav-btn:hover i { color: #f1f5f9; }
|
||||||
.filter-bar { position: sticky; top: 0; z-index: 50; display: flex; gap: 8px; padding: 12px 0; background: #0f172a; margin-bottom: 16px; flex-wrap: wrap; }
|
.filter-bar { position: sticky; top: 0; z-index: 50; display: flex; gap: 8px; padding: 12px 0; background: #0f172a; margin-bottom: 16px; flex-wrap: wrap; }
|
||||||
|
.filter-search { padding: 8px 12px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #f1f5f9; font-size: 14px; outline: none; min-width: 200px; transition: border-color 0.2s; }
|
||||||
|
.filter-search:focus { border-color: #3b82f6; }
|
||||||
|
.filter-search::placeholder { color: #64748b; }
|
||||||
|
.filter-divider { width: 1px; height: 24px; background: #334155; margin: 0 4px; }
|
||||||
|
.filter-bar-btn[data-status].active { background: #8b5cf6; border-color: #8b5cf6; }
|
||||||
|
.filter-bar-btn.sort-active { background: #0ea5e9; border-color: #0ea5e9; color: #fff; }
|
||||||
.filter-bar-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #94a3b8; font-size: 14px; cursor: pointer; transition: all 0.2s; }
|
.filter-bar-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #94a3b8; font-size: 14px; cursor: pointer; transition: all 0.2s; }
|
||||||
.filter-bar-btn:hover { background: #334155; color: #f1f5f9; }
|
.filter-bar-btn:hover { background: #334155; color: #f1f5f9; }
|
||||||
.filter-bar-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
|
.filter-bar-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
|
||||||
@@ -171,11 +177,18 @@
|
|||||||
|
|
||||||
<!-- 筛选器(顶部固定) -->
|
<!-- 筛选器(顶部固定) -->
|
||||||
<div class="filter-bar">
|
<div class="filter-bar">
|
||||||
|
<input type="text" id="projectSearch" class="filter-search" placeholder="🔍 搜索服务名称/描述/端口..." oninput="setSearch(this.value)" />
|
||||||
<button onclick="filterType('all')" class="filter-bar-btn active" data-type="all"><i class="ri-apps-line"></i> 全部</button>
|
<button onclick="filterType('all')" class="filter-bar-btn active" data-type="all"><i class="ri-apps-line"></i> 全部</button>
|
||||||
<button onclick="filterType('web')" class="filter-bar-btn" data-type="web"><i class="ri-server-line"></i> Web服务</button>
|
<button onclick="filterType('web')" class="filter-bar-btn" data-type="web"><i class="ri-server-line"></i> Web服务</button>
|
||||||
<button onclick="filterType('custom')" class="filter-bar-btn" data-type="custom"><i class="ri-global-line"></i> 自定义</button>
|
<button onclick="filterType('custom')" class="filter-bar-btn" data-type="custom"><i class="ri-global-line"></i> 自定义</button>
|
||||||
<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>
|
||||||
|
<span class="filter-divider"></span>
|
||||||
|
<button onclick="setStatusFilter('')" class="filter-bar-btn" data-status=""><i class="ri-globe-line"></i> 全部状态</button>
|
||||||
|
<button onclick="setStatusFilter('online')" class="filter-bar-btn" data-status="online"><i class="ri-wifi-line"></i> 在线中</button>
|
||||||
|
<button onclick="setStatusFilter('admin')" class="filter-bar-btn" data-status="admin"><i class="ri-settings-3-line"></i> 有后台</button>
|
||||||
|
<span class="filter-divider"></span>
|
||||||
|
<button onclick="togglePortSort()" id="portSortBtn" class="filter-bar-btn" title="按端口号排序"><i class="ri-sort-asc"></i><span>端口排序</span></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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1001,6 +1014,9 @@
|
|||||||
let cronTasks = [];
|
let cronTasks = [];
|
||||||
let currentTab = 'projects';
|
let currentTab = 'projects';
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
|
let searchKeyword = '';
|
||||||
|
let statusFilter = ''; // '' = all, 'online' = running/partial, 'admin' = has admin_url
|
||||||
|
let portSort = ''; // '' = none, 'asc', 'desc'
|
||||||
let externalIp = localStorage.getItem('externalIp') || '192.168.2.17';
|
let externalIp = localStorage.getItem('externalIp') || '192.168.2.17';
|
||||||
let enableLogs = localStorage.getItem('enableLogs') === 'true';
|
let enableLogs = localStorage.getItem('enableLogs') === 'true';
|
||||||
|
|
||||||
@@ -1526,11 +1542,40 @@
|
|||||||
function filterType(type) {
|
function filterType(type) {
|
||||||
currentFilter = type;
|
currentFilter = type;
|
||||||
document.querySelectorAll('.filter-bar-btn').forEach(btn => {
|
document.querySelectorAll('.filter-bar-btn').forEach(btn => {
|
||||||
btn.classList.toggle('active', btn.dataset.type === type);
|
if (btn.dataset.type !== undefined) {
|
||||||
|
btn.classList.toggle('active', btn.dataset.type === type);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
renderProjects();
|
renderProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSearch(val) {
|
||||||
|
searchKeyword = (val || '').trim().toLowerCase();
|
||||||
|
renderProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatusFilter(status) {
|
||||||
|
statusFilter = status;
|
||||||
|
document.querySelectorAll('.filter-bar-btn[data-status]').forEach(btn => {
|
||||||
|
btn.classList.toggle('active', btn.dataset.status === status);
|
||||||
|
});
|
||||||
|
renderProjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePortSort() {
|
||||||
|
if (!portSort) portSort = 'asc';
|
||||||
|
else if (portSort === 'asc') portSort = 'desc';
|
||||||
|
else portSort = '';
|
||||||
|
const btn = document.getElementById('portSortBtn');
|
||||||
|
btn.classList.toggle('sort-active', !!portSort);
|
||||||
|
const icon = btn.querySelector('i');
|
||||||
|
const label = btn.querySelector('span');
|
||||||
|
if (portSort === 'asc') { icon.className = 'ri-sort-asc'; label.textContent = '端口↑'; }
|
||||||
|
else if (portSort === 'desc') { icon.className = 'ri-sort-desc'; label.textContent = '端口↓'; }
|
||||||
|
else { icon.className = 'ri-sort-asc'; label.textContent = '端口排序'; }
|
||||||
|
renderProjects();
|
||||||
|
}
|
||||||
|
|
||||||
function renderProjects() {
|
function renderProjects() {
|
||||||
const list = document.getElementById('projectsList');
|
const list = document.getElementById('projectsList');
|
||||||
|
|
||||||
@@ -1538,10 +1583,43 @@
|
|||||||
const customServices = getCustomServices();
|
const customServices = getCustomServices();
|
||||||
const allProjects = [...projects, ...customServices];
|
const allProjects = [...projects, ...customServices];
|
||||||
|
|
||||||
const filtered = currentFilter === 'all' ? allProjects : allProjects.filter(p => p.type === currentFilter || (currentFilter === 'web' && p.isCustom));
|
let filtered = currentFilter === 'all' ? allProjects : allProjects.filter(p => p.type === currentFilter || (currentFilter === 'web' && p.isCustom));
|
||||||
|
|
||||||
|
// 关键字搜索:名称/描述/id/端口
|
||||||
|
if (searchKeyword) {
|
||||||
|
filtered = filtered.filter(p => {
|
||||||
|
const haystack = [
|
||||||
|
p.name || '',
|
||||||
|
p.description || '',
|
||||||
|
p.id || '',
|
||||||
|
...(p.ports || []),
|
||||||
|
p.url || ''
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
return haystack.includes(searchKeyword);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选:在线中(running/partial)/ 有后台(admin_url)
|
||||||
|
if (statusFilter === 'online') {
|
||||||
|
filtered = filtered.filter(p => {
|
||||||
|
const st = p.status?.status;
|
||||||
|
return st === 'running' || st === 'partial';
|
||||||
|
});
|
||||||
|
} else if (statusFilter === 'admin') {
|
||||||
|
filtered = filtered.filter(p => !!p.admin_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 端口排序:升/降序
|
||||||
|
if (portSort) {
|
||||||
|
filtered = filtered.slice().sort((a, b) => {
|
||||||
|
const pa = a.ports && a.ports.length ? a.ports[0] : 0;
|
||||||
|
const pb = b.ports && b.ports.length ? b.ports[0] : 0;
|
||||||
|
return portSort === 'asc' ? pa - pb : pb - pa;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
list.innerHTML = '<div class="text-center py-12 text-gray-400"><i class="ri-folder-open-line text-4xl"></i><p class="mt-2">暂无项目</p></div>';
|
list.innerHTML = '<div class="text-center py-12 text-gray-400"><i class="ri-folder-open-line text-4xl"></i><p class="mt-2">暂无匹配项目</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user