|
|
|
|
@@ -227,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>
|
|
|
|
|
|
|
|
|
|
@@ -353,11 +355,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>
|
|
|
|
|
@@ -450,11 +454,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>
|
|
|
|
|
@@ -466,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';
|
|
|
|
|
@@ -475,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) => {
|
|
|
|
|
@@ -662,7 +698,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>`;
|
|
|
|
|
@@ -792,6 +828,126 @@ 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');
|
|
|
|
|
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) {
|
|
|
|
|
|