v2.1: 标签系统增删改、分类编辑功能
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
- 主题管理:创建、编辑、删除文章主题
|
||||
- 分类系统:多级分类,每个主题可归属多个分类
|
||||
- 标签系统:灵活标签,每个主题支持多个标签,支持新建标签
|
||||
- 标签系统:灵活标签,每个主题支持多个标签,支持增删改
|
||||
- 素材收集:支持文本素材和图片素材(含Ctrl+V粘贴截图)
|
||||
- Prompt编辑:每个主题可自定义生成提示词,支持历史记录
|
||||
- 大模型生成:调用 LLM 根据素材自动生成文章
|
||||
@@ -82,6 +82,7 @@ article-writer/
|
||||
### 标签
|
||||
- `GET /api/tags` - 标签列表(含主题计数)
|
||||
- `POST /api/tags` - 创建标签
|
||||
- `PUT /api/tags/<id>` - 更新标签
|
||||
- `DELETE /api/tags/<id>` - 删除标签
|
||||
|
||||
### 主题
|
||||
|
||||
24
app.py
24
app.py
@@ -390,6 +390,30 @@ def create_tag():
|
||||
return jsonify(dict(tag))
|
||||
|
||||
|
||||
@app.route('/api/tags/<tag_id>', methods=['PUT'])
|
||||
def update_tag(tag_id):
|
||||
data = request.get_json(force=True)
|
||||
conn = get_db()
|
||||
tag = conn.execute('SELECT * FROM tags WHERE id=?', (tag_id,)).fetchone()
|
||||
if not tag:
|
||||
conn.close()
|
||||
return jsonify({'error': '标签不存在'}), 404
|
||||
name = data.get('name', '').strip()
|
||||
if not name:
|
||||
conn.close()
|
||||
return jsonify({'error': '标签名不能为空'}), 400
|
||||
# 检查重名(排除自身)
|
||||
existing = conn.execute('SELECT id FROM tags WHERE name=? AND id!=?', (name, tag_id)).fetchone()
|
||||
if existing:
|
||||
conn.close()
|
||||
return jsonify({'error': '标签名已存在'}), 400
|
||||
conn.execute('UPDATE tags SET name=? WHERE id=?', (name, tag_id))
|
||||
conn.commit()
|
||||
tag = conn.execute('SELECT * FROM tags WHERE id=?', (tag_id,)).fetchone()
|
||||
conn.close()
|
||||
return jsonify(dict(tag))
|
||||
|
||||
|
||||
@app.route('/api/tags/<tag_id>', methods=['DELETE'])
|
||||
def delete_tag(tag_id):
|
||||
conn = get_db()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user