Compare commits

..

6 Commits

2 changed files with 189 additions and 24 deletions

View File

@@ -136,5 +136,8 @@ xian-favor/
## 版本历史
- v1.2.1 (2026-04-13): 卡片显示两行,第二行显示内容预览
- v1.2.0 (2026-04-13): 压缩首页卡片高度,更紧凑的列表布局
- v1.1.1 (2026-04-13): 编辑备注改为多行输入框
- v1.1.0 (2026-04-13): 网页端支持查看详情和编辑功能
- v1.0.0 (2026-04-12): 初始版本支持CLI/API/Web三种模式

View File

@@ -188,9 +188,14 @@ INDEX_TEMPLATE = '''
.sidebar a { color: #adb5bd; text-decoration: none; padding: 10px 20px; display: block; }
.sidebar a:hover, .sidebar a.active { background: #495057; color: #fff; }
.content { padding: 20px; }
.card { margin-bottom: 15px; transition: transform 0.2s; }
.card { margin-bottom: 8px; transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.card-body { padding: 10px 15px; }
.tag { margin-right: 5px; }
.item-card { font-size: 14px; }
.item-card h6 { font-size: 14px; margin-bottom: 4px; }
.item-card p { margin-bottom: 4px; }
.item-card .text-muted.small { font-size: 12px; }
.type-text { border-left: 4px solid #17a2b8; }
.type-link { border-left: 4px solid #28a745; }
.type-column { border-left: 4px solid #6f42c1; }
@@ -222,6 +227,8 @@ INDEX_TEMPLATE = '''
<a href="#" data-filter="pending"><i class="bi bi-clock"></i> 待处理</a>
<a href="#" data-filter="in_progress"><i class="bi bi-arrow-repeat"></i> 进行中</a>
<a href="#" data-filter="completed"><i class="bi bi-check-circle"></i> 已完成</a>
<hr class="border-secondary">
<a href="#" onclick="showTagManager(); return false;"><i class="bi bi-tags"></i> 标签管理</a>
</nav>
</div>
@@ -348,7 +355,9 @@ INDEX_TEMPLATE = '''
</div>
<div class="mb-3">
<label class="form-label">标签 (逗号分隔)</label>
<input type="text" id="addTags" class="form-control" placeholder="标签1, 标签2">
<input type="text" id="addTags" class="form-control" placeholder="标签1, 标签2" list="tagList">
<datalist id="tagList"></datalist>
<div id="addTagSuggestions" class="mt-1"></div>
</div>
<div class="mb-3">
<label class="form-label">备注</label>
@@ -445,7 +454,8 @@ INDEX_TEMPLATE = '''
</div>
<div class="mb-3">
<label class="form-label">标签 (逗号分隔)</label>
<input type="text" id="editTags" class="form-control" placeholder="标签1, 标签2">
<input type="text" id="editTags" class="form-control" placeholder="标签1, 标签2" list="tagList">
<div id="editTagSuggestions" class="mt-1"></div>
</div>
<div class="mb-3">
<label class="form-label">备注</label>
@@ -461,6 +471,32 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 标签管理模态框 -->
<div class="modal fade" id="tagManagerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-tags"></i> 标签管理</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<div class="input-group">
<input type="text" id="newTagName" class="form-control" placeholder="新标签名称">
<button class="btn btn-primary" onclick="createTag()"><i class="bi bi-plus"></i> 创建</button>
</div>
</div>
<div id="tagListContainer">
<!-- 动态填充 -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const API_BASE = '/api';
@@ -470,6 +506,11 @@ let currentFilter = { type: '', status: '' };
document.addEventListener('DOMContentLoaded', () => {
loadItems();
loadStats();
loadTags();
// 标签输入自动提示
document.getElementById('addTags').addEventListener('input', showTagSuggestions);
document.getElementById('editTags').addEventListener('input', showTagSuggestionsEdit);
// 类型切换时显示/隐藏字段
document.getElementById('addType').addEventListener('change', (e) => {
@@ -534,34 +575,25 @@ function renderItems(items) {
}
container.innerHTML = items.map(item => `
<div class="card type-${item.type}" style="cursor: pointer;" onclick="showDetail(${item.id})">
<div class="card type-${item.type} item-card" style="cursor: pointer;" onclick="showDetail(${item.id})">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div style="flex: 1;">
<h6 class="card-title">
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 50)}
<div style="flex: 1; min-width: 0;">
<h6 class="card-title text-truncate mb-1">
${getTypeIcon(item.type)} ${item.title || truncate(item.content || item.url, 30)}
</h6>
<p class="card-text text-muted small mb-2">
${item.url ? `<a href="${item.url}" target="_blank" onclick="event.stopPropagation();">${item.url}</a><br>` : ''}
${item.content ? truncate(item.content, 100) : ''}
<p class="card-text text-muted small mb-0 text-truncate" style="font-size:12px;">
${item.url ? truncate(item.url, 50) : item.content ? truncate(item.content, 50) : item.note ? truncate(item.note, 50) : ''}
${item.type === 'todo' ? `${getStatusLabelShort(item.status)} ${getPriorityLabelShort(item.priority)} ${item.due_date ? '📅' + item.due_date : ''}` : ''}
</p>
<div>
${item.tags.map(t => `<span class="badge bg-secondary tag">${t}</span>`).join('')}
${item.type === 'todo' ? `
<span class="badge status-${item.status}">${getStatusLabel(item.status)}</span>
<span class="badge priority-${item.priority}">${getPriorityLabel(item.priority)}</span>
${item.due_date ? `<span class="badge bg-light text-dark">📅 ${item.due_date}</span>` : ''}
` : ''}
</div>
</div>
<div class="btn-group btn-group-sm" onclick="event.stopPropagation();">
<button class="btn btn-outline-primary" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil"></i></button>
${item.type === 'todo' && item.status !== 'completed' ?
`<button class="btn btn-outline-success" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg"></i></button>` : ''}
<button class="btn btn-outline-danger" onclick="deleteItem(${item.id})" title="删除"><i class="bi bi-trash"></i></button>
<div class="d-flex align-items-center gap-1 flex-wrap ms-2" onclick="event.stopPropagation();">
${item.tags.slice(0, 2).map(t => `<span class="badge bg-secondary" style="font-size:10px;">${t}</span>`).join('')}
<button class="btn btn-sm btn-outline-primary py-0 px-1" onclick="openEditModal(${item.id})" title="编辑"><i class="bi bi-pencil" style="font-size:11px;"></i></button>
${item.type === 'todo' && item.status !== 'completed' ? `<button class="btn btn-sm btn-outline-success py-0 px-1" onclick="completeItem(${item.id})" title="完成"><i class="bi bi-check-lg" style="font-size:11px;"></i></button>` : ''}
<button class="btn btn-sm btn-outline-danger py-0 px-1" onclick="deleteItem(${item.id})" title="删除"><i class="bi bi-trash" style="font-size:11px;"></i></button>
</div>
</div>
<div class="text-muted small mt-2">${formatDate(item.created_at)}</div>
</div>
</div>
`).join('');
@@ -768,6 +800,16 @@ function getStatusLabel(status) {
return labels[status] || status;
}
function getStatusLabelShort(status) {
const labels = { pending: '', in_progress: '🔄', completed: '' };
return labels[status] || status;
}
function getPriorityLabelShort(priority) {
const labels = { low: '🟢', medium: '🟡', high: '🟠', urgent: '🔴' };
return labels[priority] || '';
}
function getPriorityLabel(priority) {
const labels = { low: '🟢 低', medium: '🟡 中', high: '🟠 高', urgent: '🔴 紧急' };
return labels[priority] || priority;
@@ -786,6 +828,126 @@ function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 标签管理
let allTags = [];
async function loadTags() {
const res = await fetch(`${API_BASE}/tags`);
const data = await res.json();
if (data.success) {
allTags = data.data.map(t => t.name);
updateTagDatalist();
}
}
function updateTagDatalist() {
const datalist = document.getElementById('tagList');
datalist.innerHTML = allTags.map(t => `<option value="${t}">`).join('');
}
function showTagSuggestions(e) {
const input = e.target.value;
const parts = input.split(',');
const current = parts[parts.length - 1].trim().toLowerCase();
const container = document.getElementById('addTagSuggestions');
if (!current) {
container.innerHTML = '';
return;
}
const suggestions = allTags.filter(t => t.toLowerCase().includes(current) && !parts.slice(0, -1).includes(t));
container.innerHTML = suggestions.slice(0, 5).map(t =>
`<span class="badge bg-light text-dark me-1" style="cursor:pointer;" onclick="addTagToInput('addTags', '${t}')">${t}</span>`
).join('');
}
function showTagSuggestionsEdit(e) {
const input = e.target.value;
const parts = input.split(',');
const current = parts[parts.length - 1].trim().toLowerCase();
const container = document.getElementById('editTagSuggestions');
if (!current) {
container.innerHTML = '';
return;
}
const suggestions = allTags.filter(t => t.toLowerCase().includes(current) && !parts.slice(0, -1).includes(t));
container.innerHTML = suggestions.slice(0, 5).map(t =>
`<span class="badge bg-light text-dark me-1" style="cursor:pointer;" onclick="addTagToInput('editTags', '${t}')">${t}</span>`
).join('');
}
function addTagToInput(inputId, tag) {
const input = document.getElementById(inputId);
const parts = input.value.split(',');
parts[parts.length - 1] = tag;
input.value = parts.join(', ') + ', ';
// 清空提示
if (inputId === 'addTags') {
document.getElementById('addTagSuggestions').innerHTML = '';
} else {
document.getElementById('editTagSuggestions').innerHTML = '';
}
}
async function showTagManager() {
await loadTagManagerList();
new bootstrap.Modal(document.getElementById('tagManagerModal')).show();
}
async function loadTagManagerList() {
const res = await fetch(`${API_BASE}/tags`);
const data = await res.json();
if (!data.success) return;
const container = document.getElementById('tagListContainer');
if (!data.data.length) {
container.innerHTML = '<div class="text-center text-muted py-3">暂无标签</div>';
return;
}
container.innerHTML = data.data.map(tag => `
<div class="d-flex justify-content-between align-items-center p-2 border-bottom">
<div>
<span class="badge bg-secondary">${tag.name}</span>
<span class="text-muted small ms-2">${tag.item_count || 0} 个条目</span>
</div>
<button class="btn btn-sm btn-outline-danger" onclick="deleteTagManager(${tag.id}, '${tag.name}')">
<i class="bi bi-trash"></i>
</button>
</div>
`).join('');
}
async function createTag() {
const name = document.getElementById('newTagName').value.trim();
if (!name) return;
const res = await fetch(`${API_BASE}/tags`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
if (res.ok) {
document.getElementById('newTagName').value = '';
loadTagManagerList();
loadTags();
}
}
async function deleteTagManager(id, name) {
if (!confirm(`确认删除标签 "${name}"?此操作将移除所有条目中的该标签。`)) return;
await fetch(`${API_BASE}/tags/${id}`, { method: 'DELETE' });
loadTagManagerList();
loadTags();
loadItems();
}
function debounce(fn, delay) {
let timer;
return function(...args) {