feat: upgrade to V1.2 - Tags, Click Stats, and Robust WebDAV

- add Tagging system (backend and frontend)
- add Click count statistics and redirection logic
- add config.example.py
- fix WebDAV MKCOL 405 error and response handling
- fix redirection loop during force password change
- audit SQL queries for security
This commit is contained in:
OpenClaw Agent
2026-02-13 07:58:11 +08:00
parent 521cd9ba42
commit c0cdd146b1
11 changed files with 1559 additions and 749 deletions

View File

@@ -13,6 +13,12 @@
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar">
<input type="text" id="searchInput" placeholder="搜索服务或描述..." oninput="handleSearch()">
<span class="search-icon">🔍</span>
</div>
<!-- 分类 Tabs -->
<div class="tabs" id="categoryTabs">
<div class="loading" style="color: #8c8c8c; font-size: 12px; padding: 10px;">加载分类...</div>
@@ -55,6 +61,37 @@
color: #595959;
}
.search-bar {
background: #262626;
padding: 15px 20px;
position: relative;
display: flex;
align-items: center;
}
.search-bar input {
width: 100%;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 10px;
padding: 10px 15px 10px 40px;
color: #fff;
font-size: 14px;
transition: all 0.3s;
}
.search-bar input:focus {
outline: none;
background: rgba(255,255,255,0.15);
border-color: var(--main-red);
}
.search-icon {
position: absolute;
left: 35px;
color: #8c8c8c;
}
.tabs {
display: flex;
background: #262626;
@@ -169,6 +206,22 @@
flex: 1;
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin: 5px 0;
}
.mini-tag {
font-size: 10px;
background: rgba(102, 126, 226, 0.1);
color: #667eea;
padding: 1px 6px;
border-radius: 4px;
border: 1px solid rgba(102, 126, 226, 0.2);
}
.card-footer {
font-size: 11px;
color: #bfbfbf;
@@ -306,13 +359,28 @@
});
}
let currentKeyword = '';
function handleSearch() {
currentKeyword = document.getElementById('searchInput').value.toLowerCase();
renderServices(window.currentTab || 'all');
}
// 渲染服务卡片
function renderServices(category) {
const container = document.getElementById('servicesGrid');
const filteredServices = category === 'all'
let filteredServices = category === 'all'
? allServices
: allServices.filter(s => s.category === category);
// 关键词过滤
if (currentKeyword) {
filteredServices = filteredServices.filter(s =>
s.name.toLowerCase().includes(currentKeyword) ||
(s.description && s.description.toLowerCase().includes(currentKeyword))
);
}
if (filteredServices.length === 0) {
container.innerHTML = '<div class="empty-state">暂无服务</div>';
@@ -334,14 +402,20 @@
}
html += `
<a href="${service.url}" target="_blank" class="service-card" style="animation-delay: ${index * 0.05}s">
<a href="/visit/${service.id}" target="_blank" class="service-card" style="animation-delay: ${index * 0.05}s">
<div class="card-header">
<span class="card-icon">${service.icon || '📡'}</span>
<span class="card-status ${statusClass}">${statusIcon}</span>
</div>
<div class="card-name">${service.name}</div>
<div class="card-desc">${service.description || ''}</div>
<div class="card-footer">${service.category}</div>
<div class="card-tags">
${service.tags ? service.tags.split(',').map(tag => `<span class="mini-tag">${tag.trim()}</span>`).join('') : ''}
</div>
<div class="card-footer">
<span>${service.category}</span>
<span style="float: right;">🔥 ${service.click_count || 0}</span>
</div>
</a>
`;
});