1 Commits
v2.0 ... v2.1

Author SHA1 Message Date
2e8bfea262 v2.1: 标签系统增删改、分类编辑功能 2026-05-23 12:01:45 +08:00
3 changed files with 181 additions and 4 deletions

View File

@@ -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
View File

@@ -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()

View File

@@ -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 {