|
|
|
|
@@ -79,6 +79,9 @@
|
|
|
|
|
<button class="btn btn-outline-accent btn-sm" onclick="showCategoryModal()">
|
|
|
|
|
<i class="bi bi-folder me-1"></i>分类管理
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-outline-accent btn-sm" onclick="showTagModal()">
|
|
|
|
|
<i class="bi bi-tags me-1"></i>标签管理
|
|
|
|
|
</button>
|
|
|
|
|
<button class="btn btn-accent" onclick="showCreateModal()">
|
|
|
|
|
<i class="bi bi-plus-lg me-1"></i>新建主题
|
|
|
|
|
</button>
|
|
|
|
|
@@ -244,6 +247,27 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 标签管理模态框 -->
|
|
|
|
|
<div class="modal fade" id="tagModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
<h5 class="modal-title"><i class="bi bi-tags me-2"></i>标签管理</h5>
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
<div class="d-flex gap-2 mb-3">
|
|
|
|
|
<input type="text" class="form-control form-control-sm" id="newTagName" placeholder="新标签名称">
|
|
|
|
|
<button class="btn btn-sm btn-accent" onclick="createTag()">添加</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="tagList">
|
|
|
|
|
<!-- 动态填充 -->
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- LLM配置模态框 -->
|
|
|
|
|
<div class="modal fade" id="configModal" tabindex="-1">
|
|
|
|
|
<div class="modal-dialog">
|
|
|
|
|
@@ -291,6 +315,7 @@
|
|
|
|
|
const createModalInstance = new bootstrap.Modal(document.getElementById('createModal'));
|
|
|
|
|
const configModalInstance = new bootstrap.Modal(document.getElementById('configModal'));
|
|
|
|
|
const categoryModalInstance = new bootstrap.Modal(document.getElementById('categoryModal'));
|
|
|
|
|
const tagModalInstance = new bootstrap.Modal(document.getElementById('tagModal'));
|
|
|
|
|
|
|
|
|
|
// 筛选状态
|
|
|
|
|
let activeCategoryId = '';
|
|
|
|
|
@@ -480,14 +505,55 @@
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
container.innerHTML = cats.map(c => `
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center p-2 mb-1" style="background:var(--bg-dark);border-radius:6px;">
|
|
|
|
|
<span><i class="bi bi-folder2 text-warning me-2"></i>${c.name} <span class="text-muted small">(${c.topic_count || 0})</span></span>
|
|
|
|
|
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteCategory('${c.id}','${c.name}')"><i class="bi bi-trash3"></i></button>
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center p-2 mb-1" style="background:var(--bg-dark);border-radius:6px;" id="cat-row-${c.id}">
|
|
|
|
|
<span id="cat-display-${c.id}"><i class="bi bi-folder2 text-warning me-2"></i>${c.name} <span class="text-muted small">(${c.topic_count || 0})</span></span>
|
|
|
|
|
<span id="cat-edit-${c.id}" style="display:none;" class="d-flex align-items-center gap-1 flex-grow-1 ms-2">
|
|
|
|
|
<input type="text" class="form-control form-control-sm" id="cat-input-${c.id}" value="${c.name}" style="max-width:160px;">
|
|
|
|
|
<button class="btn btn-sm btn-accent py-0 px-2" onclick="saveCategory('${c.id}')"><i class="bi bi-check-lg"></i></button>
|
|
|
|
|
<button class="btn btn-sm btn-secondary py-0 px-2" onclick="cancelCategoryEdit('${c.id}','${c.name}')"><i class="bi bi-x-lg"></i></button>
|
|
|
|
|
</span>
|
|
|
|
|
<span id="cat-actions-${c.id}" class="d-flex gap-1">
|
|
|
|
|
<button class="btn btn-sm btn-link text-accent p-0" onclick="editCategory('${c.id}')" title="编辑"><i class="bi bi-pencil"></i></button>
|
|
|
|
|
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteCategory('${c.id}','${c.name}')" title="删除"><i class="bi bi-trash3"></i></button>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editCategory(id) {
|
|
|
|
|
document.getElementById('cat-display-' + id).style.display = 'none';
|
|
|
|
|
document.getElementById('cat-edit-' + id).style.display = 'flex';
|
|
|
|
|
document.getElementById('cat-actions-' + id).style.display = 'none';
|
|
|
|
|
const input = document.getElementById('cat-input-' + id);
|
|
|
|
|
input.focus();
|
|
|
|
|
input.select();
|
|
|
|
|
// 回车保存
|
|
|
|
|
input.onkeydown = function(e) { if (e.key === 'Enter') saveCategory(id); if (e.key === 'Escape') cancelCategoryEdit(id, input.value); };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelCategoryEdit(id, originalName) {
|
|
|
|
|
document.getElementById('cat-display-' + id).style.display = '';
|
|
|
|
|
document.getElementById('cat-edit-' + id).style.display = 'none';
|
|
|
|
|
document.getElementById('cat-actions-' + id).style.display = 'flex';
|
|
|
|
|
document.getElementById('cat-input-' + id).value = originalName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveCategory(id) {
|
|
|
|
|
const name = document.getElementById('cat-input-' + id).value.trim();
|
|
|
|
|
if (!name) return alert('分类名不能为空');
|
|
|
|
|
const res = await fetch(`/api/categories/${id}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
body: JSON.stringify({name})
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
await loadCategoryList();
|
|
|
|
|
} else {
|
|
|
|
|
const d = await res.json(); alert(d.error || '修改失败');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createCategory() {
|
|
|
|
|
const name = document.getElementById('newCatName').value.trim();
|
|
|
|
|
if (!name) return alert('请输入分类名称');
|
|
|
|
|
@@ -510,6 +576,92 @@
|
|
|
|
|
await loadCategoryList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== 标签管理 =====
|
|
|
|
|
async function showTagModal() {
|
|
|
|
|
tagModalInstance.show();
|
|
|
|
|
await loadTagList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadTagList() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/tags');
|
|
|
|
|
const tags = await res.json();
|
|
|
|
|
const container = document.getElementById('tagList');
|
|
|
|
|
if (tags.length === 0) {
|
|
|
|
|
container.innerHTML = '<div class="text-muted small text-center py-2">暂无标签</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
container.innerHTML = tags.map(t => `
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center p-2 mb-1" style="background:var(--bg-dark);border-radius:6px;" id="tag-row-${t.id}">
|
|
|
|
|
<span id="tag-display-${t.id}"><i class="bi bi-tag text-success me-2"></i>${t.name} <span class="text-muted small">(${t.topic_count || 0})</span></span>
|
|
|
|
|
<span id="tag-edit-${t.id}" style="display:none;" class="d-flex align-items-center gap-1 flex-grow-1 ms-2">
|
|
|
|
|
<input type="text" class="form-control form-control-sm" id="tag-input-${t.id}" value="${t.name}" style="max-width:160px;">
|
|
|
|
|
<button class="btn btn-sm btn-accent py-0 px-2" onclick="saveTag('${t.id}')"><i class="bi bi-check-lg"></i></button>
|
|
|
|
|
<button class="btn btn-sm btn-secondary py-0 px-2" onclick="cancelTagEdit('${t.id}','${t.name}')"><i class="bi bi-x-lg"></i></button>
|
|
|
|
|
</span>
|
|
|
|
|
<span id="tag-actions-${t.id}" class="d-flex gap-1">
|
|
|
|
|
<button class="btn btn-sm btn-link text-accent p-0" onclick="editTag('${t.id}')" title="编辑"><i class="bi bi-pencil"></i></button>
|
|
|
|
|
<button class="btn btn-sm btn-link text-danger p-0" onclick="deleteTag('${t.id}','${t.name}')" title="删除"><i class="bi bi-trash3"></i></button>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function editTag(id) {
|
|
|
|
|
document.getElementById('tag-display-' + id).style.display = 'none';
|
|
|
|
|
document.getElementById('tag-edit-' + id).style.display = 'flex';
|
|
|
|
|
document.getElementById('tag-actions-' + id).style.display = 'none';
|
|
|
|
|
const input = document.getElementById('tag-input-' + id);
|
|
|
|
|
input.focus();
|
|
|
|
|
input.select();
|
|
|
|
|
input.onkeydown = function(e) { if (e.key === 'Enter') saveTag(id); if (e.key === 'Escape') cancelTagEdit(id, input.value); };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelTagEdit(id, originalName) {
|
|
|
|
|
document.getElementById('tag-display-' + id).style.display = '';
|
|
|
|
|
document.getElementById('tag-edit-' + id).style.display = 'none';
|
|
|
|
|
document.getElementById('tag-actions-' + id).style.display = 'flex';
|
|
|
|
|
document.getElementById('tag-input-' + id).value = originalName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveTag(id) {
|
|
|
|
|
const name = document.getElementById('tag-input-' + id).value.trim();
|
|
|
|
|
if (!name) return alert('标签名不能为空');
|
|
|
|
|
const res = await fetch(`/api/tags/${id}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
body: JSON.stringify({name})
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
await loadTagList();
|
|
|
|
|
} else {
|
|
|
|
|
const d = await res.json(); alert(d.error || '修改失败');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createTag() {
|
|
|
|
|
const name = document.getElementById('newTagName').value.trim();
|
|
|
|
|
if (!name) return alert('请输入标签名称');
|
|
|
|
|
const res = await fetch('/api/tags', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {'Content-Type': 'application/json'},
|
|
|
|
|
body: JSON.stringify({name})
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
document.getElementById('newTagName').value = '';
|
|
|
|
|
await loadTagList();
|
|
|
|
|
} else {
|
|
|
|
|
const d = await res.json(); alert(d.error || '创建失败');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteTag(id, name) {
|
|
|
|
|
if (!confirm(`确定删除标签「${name}」?`)) return;
|
|
|
|
|
await fetch(`/api/tags/${id}`, {method: 'DELETE'});
|
|
|
|
|
await loadTagList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== LLM配置 =====
|
|
|
|
|
async function showConfigModal() {
|
|
|
|
|
try {
|
|
|
|
|
|