Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8780b9000 | |||
| 196bfaf213 | |||
| 2675edb695 | |||
| 1f9d0a7ea2 | |||
| 4642a278a5 | |||
| 598a86b32d | |||
| ae27641b6d | |||
| 7ab6647c32 | |||
| 4d51a680aa | |||
| e2db77e93f | |||
| 1c2c8bbebe | |||
| f77fd667d9 | |||
| 4c8dd35d53 | |||
| a4f7606794 | |||
| f6674a2ad3 | |||
| 6441b3817c |
12
README.md
12
README.md
@@ -136,5 +136,17 @@ xian-favor/
|
||||
|
||||
## 版本历史
|
||||
|
||||
- v1.5.0 (2026-04-13): 首页添加分页功能,每页20条记录
|
||||
- v1.4.0 (2026-04-13): 首页添加导出按钮,一键导出所有收藏为JSON文件
|
||||
- v1.3.2 (2026-04-13): 编辑和添加模态框改为大尺寸(modal-lg)
|
||||
- v1.3.1 (2026-04-13): 备注改名为详情,支持换行显示,扩大输入框
|
||||
- v1.3.0 (2026-04-13): 标签管理功能和输入自动提示
|
||||
- 侧边栏新增"标签管理"入口
|
||||
- 标签管理页面:显示所有标签、使用条目数、创建/删除标签
|
||||
- 添加/编辑时标签输入支持自动提示(输入时显示匹配的已有标签)
|
||||
- 点击提示标签快速添加
|
||||
- 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三种模式
|
||||
@@ -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>
|
||||
|
||||
@@ -239,6 +246,9 @@ INDEX_TEMPLATE = '''
|
||||
<option value="todo">待办</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-outline-success me-2" onclick="exportData()" title="导出JSON">
|
||||
<i class="bi bi-download"></i> 导出
|
||||
</button>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
|
||||
<i class="bi bi-plus-lg"></i> 添加
|
||||
</button>
|
||||
@@ -282,13 +292,16 @@ INDEX_TEMPLATE = '''
|
||||
|
||||
<!-- 列表 -->
|
||||
<div id="itemList"></div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div id="pagination" class="d-flex justify-content-center mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加模态框 -->
|
||||
<div class="modal fade" id="addModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">添加条目</h5>
|
||||
@@ -348,11 +361,13 @@ 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>
|
||||
<input type="text" id="addNote" class="form-control">
|
||||
<label class="form-label">详情/备注</label>
|
||||
<textarea id="addNote" class="form-control" rows="5"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -389,7 +404,7 @@ INDEX_TEMPLATE = '''
|
||||
|
||||
<!-- 编辑模态框 -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">编辑条目</h5>
|
||||
@@ -445,11 +460,12 @@ 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>
|
||||
<textarea id="editNote" class="form-control" rows="3"></textarea>
|
||||
<label class="form-label">详情/备注</label>
|
||||
<textarea id="editNote" class="form-control" rows="5"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -461,15 +477,57 @@ 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="row mb-3">
|
||||
<div class="col">
|
||||
<div class="input-group">
|
||||
<input type="text" id="tagSearch" class="form-control" placeholder="搜索标签...">
|
||||
<button class="btn btn-outline-secondary" onclick="loadTagManagerList()"><i class="bi bi-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<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>
|
||||
<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';
|
||||
let currentFilter = { type: '', status: '' };
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadStats(); // 先加载统计,确保总数可用
|
||||
loadItems();
|
||||
loadStats();
|
||||
loadTags();
|
||||
|
||||
// 标签输入自动提示
|
||||
document.getElementById('addTags').addEventListener('input', showTagSuggestions);
|
||||
document.getElementById('editTags').addEventListener('input', showTagSuggestionsEdit);
|
||||
|
||||
// 标签搜索实时过滤
|
||||
document.getElementById('tagSearch')?.addEventListener('input', debounce(loadTagManagerList, 300));
|
||||
|
||||
// 类型切换时显示/隐藏字段
|
||||
document.getElementById('addType').addEventListener('change', (e) => {
|
||||
@@ -510,9 +568,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
// 加载列表
|
||||
async function loadItems() {
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
async function loadItems(page = 1) {
|
||||
currentPage = page;
|
||||
const keyword = document.getElementById('searchInput').value;
|
||||
let url = `${API_BASE}/items?limit=100`;
|
||||
let url = `${API_BASE}/items?limit=${pageSize}&offset=${(page-1)*pageSize}`;
|
||||
if (currentFilter.type) url += `&type=${currentFilter.type}`;
|
||||
if (currentFilter.status) url += `&status=${currentFilter.status}`;
|
||||
if (keyword) url += `&keyword=${encodeURIComponent(keyword)}`;
|
||||
@@ -522,6 +584,7 @@ async function loadItems() {
|
||||
|
||||
if (data.success) {
|
||||
renderItems(data.data);
|
||||
renderPagination(data.data.length, page);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,39 +597,77 @@ 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('');
|
||||
}
|
||||
|
||||
// 渲染分页
|
||||
function renderPagination(itemCount, page) {
|
||||
const container = document.getElementById('pagination');
|
||||
const total = parseInt(document.getElementById('statTotal').textContent);
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<nav><ul class="pagination">';
|
||||
|
||||
// 上一页
|
||||
html += `<li class="page-item ${page === 1 ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadItems(${page-1}); return false;">«</a>
|
||||
</li>`;
|
||||
|
||||
// 页码(最多显示5个)
|
||||
const startPage = Math.max(1, page - 2);
|
||||
const endPage = Math.min(totalPages, page + 2);
|
||||
|
||||
if (startPage > 1) {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" onclick="loadItems(1); return false;">1</a></li>`;
|
||||
if (startPage > 2) html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
}
|
||||
|
||||
for (let p = startPage; p <= endPage; p++) {
|
||||
html += `<li class="page-item ${p === page ? 'active' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadItems(${p}); return false;">${p}</a>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
|
||||
html += `<li class="page-item"><a class="page-link" href="#" onclick="loadItems(${totalPages}); return false;">${totalPages}</a></li>`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
html += `<li class="page-item ${page === totalPages ? 'disabled' : ''}">
|
||||
<a class="page-link" href="#" onclick="loadItems(${page+1}); return false;">»</a>
|
||||
</li>`;
|
||||
|
||||
html += '</ul></nav>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 加载统计
|
||||
async function loadStats() {
|
||||
const res = await fetch(`${API_BASE}/stats`);
|
||||
@@ -579,6 +680,12 @@ async function loadStats() {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据(统计+列表)
|
||||
async function refreshData() {
|
||||
await loadStats();
|
||||
loadItems(currentPage);
|
||||
}
|
||||
|
||||
// 添加条目
|
||||
async function addItem() {
|
||||
const type = document.getElementById('addType').value;
|
||||
@@ -604,24 +711,21 @@ async function addItem() {
|
||||
if (res.ok) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
|
||||
document.getElementById('addForm').reset();
|
||||
loadItems();
|
||||
loadStats();
|
||||
refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
// 完成待办
|
||||
async function completeItem(id) {
|
||||
await fetch(`${API_BASE}/items/${id}/done`, { method: 'POST' });
|
||||
loadItems();
|
||||
loadStats();
|
||||
refreshData();
|
||||
}
|
||||
|
||||
// 删除条目
|
||||
async function deleteItem(id) {
|
||||
if (!confirm('确认删除?')) return;
|
||||
await fetch(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
||||
loadItems();
|
||||
loadStats();
|
||||
refreshData();
|
||||
}
|
||||
|
||||
// 当前查看的条目ID
|
||||
@@ -666,7 +770,7 @@ async function showDetail(id) {
|
||||
}
|
||||
|
||||
if (item.note) {
|
||||
html += `<div class="mb-3"><strong>备注:</strong> ${escapeHtml(item.note)}</div>`;
|
||||
html += `<div class="mb-3"><strong>详情/备注:</strong><br><div class="border rounded p-3 bg-light" style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(item.note)}</div></div>`;
|
||||
}
|
||||
|
||||
html += `<div class="text-muted small"><strong>创建时间:</strong> ${formatDate(item.created_at)}<br><strong>更新时间:</strong> ${formatDate(item.updated_at)}</div>`;
|
||||
@@ -747,8 +851,7 @@ async function saveEdit() {
|
||||
|
||||
if (res.ok) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
loadItems();
|
||||
loadStats();
|
||||
refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,6 +871,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 +899,157 @@ function escapeHtml(str) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// 标签管理
|
||||
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');
|
||||
|
||||
// 搜索过滤
|
||||
const searchKeyword = document.getElementById('tagSearch').value.trim().toLowerCase();
|
||||
let tags = data.data;
|
||||
if (searchKeyword) {
|
||||
tags = tags.filter(t => t.name.toLowerCase().includes(searchKeyword));
|
||||
}
|
||||
|
||||
if (!tags.length) {
|
||||
container.innerHTML = '<div class="text-center text-muted py-3">暂无标签</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = tags.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();
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
async function exportData() {
|
||||
const res = await fetch(`${API_BASE}/items?limit=1000`);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success) {
|
||||
alert('导出失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 格式化JSON
|
||||
const jsonStr = JSON.stringify(data.data, null, 2);
|
||||
|
||||
// 创建下载
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `xian_favor_export_${new Date().toISOString().slice(0,10)}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function debounce(fn, delay) {
|
||||
let timer;
|
||||
return function(...args) {
|
||||
|
||||
Reference in New Issue
Block a user