@@ -132,6 +132,23 @@ def create_tag():
return jsonify ( { ' success ' : False , ' error ' : str ( e ) } ) , 500
@app.route ( ' /api/tags/<int:tag_id> ' , methods = [ ' PUT ' ] )
def update_tag ( tag_id ) :
""" 更新标签 """
data = request . get_json ( )
name = data . get ( ' name ' , ' ' ) . strip ( )
if not name :
return jsonify ( { ' success ' : False , ' error ' : ' 标签名不能为空 ' } ) , 400
try :
if db . update_tag ( tag_id , name ) :
return jsonify ( { ' success ' : True , ' data ' : { ' id ' : tag_id , ' name ' : name } } )
return jsonify ( { ' success ' : False , ' error ' : ' 标签不存在或名称已存在 ' } ) , 404
except Exception as e :
return jsonify ( { ' success ' : False , ' error ' : str ( e ) } ) , 500
@app.route ( ' /api/tags/<int:tag_id> ' , methods = [ ' DELETE ' ] )
def delete_tag ( tag_id ) :
""" 删除标签 """
@@ -179,6 +196,7 @@ INDEX_TEMPLATE = '''
<meta charset= " UTF-8 " >
<meta name= " viewport " content= " width=device-width, initial-scale=1.0 " >
<title>Xian Favor - 收藏系统</title>
<link rel= " icon " href= " data:image/svg+xml,<svg xmlns= ' http://www.w3.org/2000/svg ' viewBox= ' 0 0 100 100 ' ><text y= ' .9em ' font-size= ' 90 ' >⭐</text></svg> " >
<link href= " https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css " rel= " stylesheet " >
<link href= " https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css " rel= " stylesheet " >
<style>
@@ -187,9 +205,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: 15 px; transition: transform 0.2s; }
.card { margin-bottom: 8 px; 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; }
@@ -221,6 +244,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>
@@ -238,6 +263,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>
@@ -281,13 +309,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>
@@ -347,11 +378,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>
@@ -363,15 +396,160 @@ INDEX_TEMPLATE = '''
</div>
</div>
<!-- 详情模态框 -->
<div class= " modal fade " id= " detailModal " tabindex= " -1 " >
<div class= " modal-dialog modal-lg " >
<div class= " modal-content " >
<div class= " modal-header " >
<h5 class= " modal-title " ><span id= " detailTypeIcon " ></span> <span id= " detailTitle " ></span></h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<div id= " detailContent " >
<!-- 动态填充 -->
</div>
</div>
<div class= " modal-footer " >
<button type= " button " class= " btn btn-outline-primary " onclick= " openEditModalFromDetail() " >
<i class= " bi bi-pencil " ></i> 编辑
</button>
<button type= " button " class= " btn btn-secondary " data-bs-dismiss= " modal " >关闭</button>
</div>
</div>
</div>
</div>
<!-- 编辑模态框 -->
<div class= " modal fade " id= " editModal " tabindex= " -1 " >
<div class= " modal-dialog modal-lg " >
<div class= " modal-content " >
<div class= " modal-header " >
<h5 class= " modal-title " >编辑条目</h5>
<button type= " button " class= " btn-close " data-bs-dismiss= " modal " ></button>
</div>
<div class= " modal-body " >
<form id= " editForm " >
<input type= " hidden " id= " editId " >
<div class= " mb-3 " >
<label class= " form-label " >类型</label>
<select id= " editType " class= " form-select " >
<option value= " text " >📝 文本</option>
<option value= " link " >🔗 链接</option>
<option value= " column " >📰 专栏</option>
<option value= " todo " >✅ 待办</option>
</select>
</div>
<div class= " mb-3 " >
<label class= " form-label " >标题</label>
<input type= " text " id= " editTitle " class= " form-control " >
</div>
<div class= " mb-3 " id= " editContentGroup " >
<label class= " form-label " >内容</label>
<textarea id= " editContent " class= " form-control " rows= " 5 " ></textarea>
</div>
<div class= " mb-3 " id= " editUrlGroup " style= " display:none; " >
<label class= " form-label " >URL</label>
<input type= " url " id= " editUrl " class= " form-control " >
</div>
<div class= " mb-3 " id= " editSourceGroup " style= " display:none; " >
<label class= " form-label " >来源</label>
<input type= " text " id= " editSource " class= " form-control " >
</div>
<div class= " mb-3 " id= " editTodoFields " style= " display:none; " >
<div class= " row " >
<div class= " col " >
<label class= " form-label " >状态</label>
<select id= " editStatus " class= " form-select " >
<option value= " pending " >⏳ 待处理</option>
<option value= " in_progress " >🔄 进行中</option>
<option value= " completed " >✅ 已完成</option>
</select>
</div>
<div class= " col " >
<label class= " form-label " >优先级</label>
<select id= " editPriority " class= " form-select " >
<option value= " low " >🟢 低</option>
<option value= " medium " >🟡 中</option>
<option value= " high " >🟠 高</option>
<option value= " urgent " >🔴 紧急</option>
</select>
</div>
</div>
<div class= " mt-3 " >
<label class= " form-label " >截止日期</label>
<input type= " date " id= " editDueDate " class= " form-control " >
</div>
</div>
<div class= " mb-3 " >
<label class= " form-label " >标签 (逗号分隔)</label>
<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= " 5 " ></textarea>
</div>
</form>
</div>
<div class= " modal-footer " >
<button type= " button " class= " btn btn-secondary " data-bs-dismiss= " modal " >取消</button>
<button type= " button " class= " btn btn-primary " onclick= " saveEdit() " >保存</button>
</div>
</div>
</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();
loadStat s();
loadTag s();
// 标签输入自动提示
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) => {
@@ -382,6 +560,11 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById( ' todoFields ' ).style.display = type === ' todo ' ? ' block ' : ' none ' ;
});
// 编辑时类型切换
document.getElementById( ' editType ' ).addEventListener( ' change ' , (e) => {
updateEditFieldsByType(e.target.value);
});
// 搜索
document.getElementById( ' searchInput ' ).addEventListener( ' input ' , debounce(loadItems, 300));
@@ -412,9 +595,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)}`;
@@ -424,6 +611,7 @@ async function loadItems() {
if (data.success) {
renderItems(data.data);
renderPagination(data.data.length, page);
}
}
@@ -436,38 +624,77 @@ function renderItems(items) {
}
container.innerHTML = items.map(item => `
<div class= " card type-$ {item.type} " >
<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>
<h6 class= " card-title " >
$ { getTypeIcon(item.type)} $ { item.title || truncate(item.content || item.url, 5 0)}
<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, 3 0)}
</h6>
<p class= " card-text text-muted small mb-2 " >
$ { item.url ? `<a href= " $ {item.url} " target= " _blank " >$ {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 " >
$ { 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= " de leteItem(${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= " comp leteItem(${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`);
@@ -480,6 +707,12 @@ async function loadStats() {
}
}
// 刷新数据(统计+列表)
async function refreshData() {
await loadStats();
loadItems(currentPage);
}
// 添加条目
async function addItem() {
const type = document.getElementById( ' addType ' ).value;
@@ -505,24 +738,150 @@ 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
let currentDetailId = null;
// 显示详情
async function showDetail(id) {
currentDetailId = id;
const res = await fetch(`$ {API_BASE} /items/$ {id} `);
const data = await res.json();
if (!data.success) return;
const item = data.data;
document.getElementById( ' detailTypeIcon ' ).textContent = getTypeIcon(item.type);
document.getElementById( ' detailTitle ' ).textContent = item.title || ' (无标题) ' ;
let html = `<div class= " mb-3 " ><strong>类型:</strong> $ { getTypeLabel(item.type)}</div>`;
if (item.url) {
html += `<div class= " mb-3 " ><strong>URL:</strong> <a href= " $ {item.url} " target= " _blank " >$ {item.url} </a></div>`;
}
if (item.content) {
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.content)}</div></div>`;
}
if (item.source) {
html += `<div class= " mb-3 " ><strong>来源:</strong> $ { escapeHtml(item.source)}</div>`;
}
if (item.type === ' todo ' ) {
html += `<div class= " mb-3 " ><strong>状态:</strong> $ { getStatusLabel(item.status)}</div>`;
html += `<div class= " mb-3 " ><strong>优先级:</strong> $ { getPriorityLabel(item.priority)}</div>`;
if (item.due_date) {
html += `<div class= " mb-3 " ><strong>截止日期:</strong> $ {item.due_date} </div>`;
}
}
if (item.tags.length) {
html += `<div class= " mb-3 " ><strong>标签:</strong> $ { item.tags.map(t => `<span class= " badge bg-secondary " >$ { escapeHtml(t)}</span>`).join( ' ' )}</div>`;
}
if (item.note) {
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>`;
document.getElementById( ' detailContent ' ).innerHTML = html;
new bootstrap.Modal(document.getElementById( ' detailModal ' )).show();
}
// 从详情页打开编辑
function openEditModalFromDetail() {
bootstrap.Modal.getInstance(document.getElementById( ' detailModal ' )).hide();
setTimeout(() => openEditModal(currentDetailId), 300);
}
// 打开编辑模态框
async function openEditModal(id) {
currentDetailId = id;
const res = await fetch(`$ {API_BASE} /items/$ {id} `);
const data = await res.json();
if (!data.success) return;
const item = data.data;
const type = item.type;
document.getElementById( ' editId ' ).value = id;
document.getElementById( ' editType ' ).value = type;
document.getElementById( ' editTitle ' ).value = item.title || ' ' ;
// 根据类型显示/隐藏字段
updateEditFieldsByType(type);
document.getElementById( ' editContent ' ).value = item.content || ' ' ;
document.getElementById( ' editUrl ' ).value = item.url || ' ' ;
document.getElementById( ' editSource ' ).value = item.source || ' ' ;
document.getElementById( ' editTags ' ).value = item.tags.join( ' , ' );
document.getElementById( ' editNote ' ).value = item.note || ' ' ;
if (type === ' todo ' ) {
document.getElementById( ' editStatus ' ).value = item.status;
document.getElementById( ' editPriority ' ).value = item.priority;
document.getElementById( ' editDueDate ' ).value = item.due_date || ' ' ;
}
new bootstrap.Modal(document.getElementById( ' editModal ' )).show();
}
// 根据类型更新编辑表单字段显示
function updateEditFieldsByType(type) {
document.getElementById( ' editContentGroup ' ).style.display = type === ' text ' ? ' block ' : ' none ' ;
document.getElementById( ' editUrlGroup ' ).style.display = [ ' link ' , ' column ' ].includes(type) ? ' block ' : ' none ' ;
document.getElementById( ' editSourceGroup ' ).style.display = type === ' column ' ? ' block ' : ' none ' ;
document.getElementById( ' editTodoFields ' ).style.display = type === ' todo ' ? ' block ' : ' none ' ;
}
// 保存编辑
async function saveEdit() {
const id = document.getElementById( ' editId ' ).value;
const type = document.getElementById( ' editType ' ).value; // 从下拉框获取新类型
const data = {
type: type, // 包含类型变更
title: document.getElementById( ' editTitle ' ).value,
content: type === ' text ' ? document.getElementById( ' editContent ' ).value : null,
url: [ ' link ' , ' column ' ].includes(type) ? document.getElementById( ' editUrl ' ).value : null,
source: type === ' column ' ? document.getElementById( ' editSource ' ).value : null,
status: type === ' todo ' ? document.getElementById( ' editStatus ' ).value : null,
priority: type === ' todo ' ? document.getElementById( ' editPriority ' ).value : null,
due_date: type === ' todo ' ? document.getElementById( ' editDueDate ' ).value : null,
note: document.getElementById( ' editNote ' ).value,
tags: document.getElementById( ' editTags ' ).value.split( ' , ' ).map(t => t.trim()).filter(t => t)
};
const res = await fetch(`$ {API_BASE} /items/$ {id} `, {
method: ' PUT ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify(data)
});
if (res.ok) {
bootstrap.Modal.getInstance(document.getElementById( ' editModal ' )).hide();
refreshData();
}
}
// 工具函数
@@ -531,13 +890,28 @@ function getTypeIcon(type) {
return icons[type] || ' 📄 ' ;
}
function getTypeLabel(type) {
const labels = { text: ' 📝 文本 ' , link: ' 🔗 链接 ' , column: ' 📰 专栏 ' , todo: ' ✅ 待办 ' };
return labels[type] || type;
}
function getStatusLabel(status) {
const labels = { pending: ' 待处理 ' , in_progress: ' 进行中 ' , completed: ' 已完成 ' };
const labels = { pending: ' ⏳ 待处理' , in_progress: ' 🔄 进行中' , completed: ' ✅ 已完成' };
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: ' 紧急 ' };
const labels = { low: ' 🟢 低' , medium: ' 🟡 中' , high: ' 🟠 高' , urgent: ' 🔴 紧急' };
return labels[priority] || priority;
}
@@ -549,6 +923,215 @@ function formatDate(dateStr) {
return new Date(dateStr).toLocaleString( ' zh-CN ' );
}
function escapeHtml(str) {
if (!str) return ' ' ;
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 " id= " tag-row-$ {tag.id} " >
<div id= " tag-display-$ {tag.id} " >
<span class= " badge bg-secondary " >$ {tag.name} </span>
<span class= " text-muted small ms-2 " >$ { tag.item_count || 0} 个条目</span>
</div>
<div id= " tag-edit-$ {tag.id} " style= " display:none; " >
<input type= " text " class= " form-control form-control-sm " id= " edit-tag-name-$ {tag.id} " value= " $ {tag.name} " style= " width:150px; " >
</div>
<div class= " btn-group btn-group-sm " >
<button class= " btn btn-outline-primary " id= " tag-edit-btn-$ {tag.id} " onclick= " showEditTag($ {tag.id} ) " title= " 编辑 " >
<i class= " bi bi-pencil " ></i>
</button>
<button class= " btn btn-outline-success " id= " tag-save-btn-$ {tag.id} " style= " display:none; " onclick= " saveEditTag($ {tag.id} ) " title= " 保存 " >
<i class= " bi bi-check " ></i>
</button>
<button class= " btn btn-outline-secondary " id= " tag-cancel-btn-$ {tag.id} " style= " display:none; " onclick= " cancelEditTag($ {tag.id} , ' $ {tag.name} ' ) " title= " 取消 " >
<i class= " bi bi-x " ></i>
</button>
<button class= " btn btn-outline-danger " onclick= " deleteTagManager($ {tag.id} , ' $ {tag.name} ' ) " title= " 删除 " >
<i class= " bi bi-trash " ></i>
</button>
</div>
</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 showEditTag(id) {
document.getElementById(`tag-display-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-edit-$ {id} `).style.display = ' block ' ;
document.getElementById(`tag-edit-btn-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-save-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`tag-cancel-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`edit-tag-name-$ {id} `).focus();
}
function cancelEditTag(id, oldName) {
document.getElementById(`edit-tag-name-$ {id} `).value = oldName;
document.getElementById(`tag-display-$ {id} `).style.display = ' block ' ;
document.getElementById(`tag-edit-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-edit-btn-$ {id} `).style.display = ' inline-block ' ;
document.getElementById(`tag-save-btn-$ {id} `).style.display = ' none ' ;
document.getElementById(`tag-cancel-btn-$ {id} `).style.display = ' none ' ;
}
async function saveEditTag(id) {
const newName = document.getElementById(`edit-tag-name-$ {id} `).value.trim();
if (!newName) return;
const res = await fetch(`$ {API_BASE} /tags/$ {id} `, {
method: ' PUT ' ,
headers: { ' Content-Type ' : ' application/json ' },
body: JSON.stringify( { name: newName })
});
if (res.ok) {
loadTagManagerList();
loadTags();
loadItems();
} else {
const data = await res.json();
alert(data.error || ' 更新失败 ' );
}
}
// 导出数据
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) {