feat: 在设置页新增系统标签管理面板,放开系统标签的 CRUD 限制
- 移除 tags.go 中 UpdateTag 和 DeleteTag 对系统标签的 403 限制 - 在 settings.html 新增「系统标签管理」卡片,支持: - 折叠浏览所有系统标签及其选项 - 编辑标签名称和描述 - 编辑、添加、删除标签选项 - 删除整个标签(带二次确认) - .gitignore 添加 prompt-generator 二进制文件
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
*.exe
|
*.exe
|
||||||
server
|
server
|
||||||
|
prompt-generator
|
||||||
|
|||||||
@@ -49,6 +49,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统标签管理 -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">🏷️ 系统标签管理</div>
|
||||||
|
<div class="card-body" id="system-tags-container">
|
||||||
|
<div class="text-sm text-muted" id="system-tags-loading">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 账号 -->
|
<!-- 账号 -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">🔐 账号</div>
|
<div class="card-header">🔐 账号</div>
|
||||||
@@ -60,6 +68,11 @@
|
|||||||
|
|
||||||
<script src="/static/common.js"></script>
|
<script src="/static/common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
function onLogin() {
|
||||||
|
loadSettings();
|
||||||
|
loadSystemTags();
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const ok = await Auth.check();
|
const ok = await Auth.check();
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -69,6 +82,7 @@
|
|||||||
renderNavbar('settings');
|
renderNavbar('settings');
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
updateThemeBtn();
|
updateThemeBtn();
|
||||||
|
await loadSystemTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
@@ -106,6 +120,111 @@
|
|||||||
document.getElementById('theme-label').textContent = Theme.isDark() ? '🌙 暗色' : '☀️ 亮色';
|
document.getElementById('theme-label').textContent = Theme.isDark() ? '🌙 暗色' : '☀️ 亮色';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 系统标签管理 ===
|
||||||
|
let systemTags = [];
|
||||||
|
|
||||||
|
async function loadSystemTags() {
|
||||||
|
const container = document.getElementById('system-tags-container');
|
||||||
|
try {
|
||||||
|
const tags = await API.get('/api/tags');
|
||||||
|
systemTags = (tags || []).filter(t => t.scope === 'system');
|
||||||
|
renderSystemTags();
|
||||||
|
} catch (e) {
|
||||||
|
container.innerHTML = `<div class="text-sm" style="color:var(--danger-color)">加载失败: ${escapeHtml(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSystemTags() {
|
||||||
|
const container = document.getElementById('system-tags-container');
|
||||||
|
if (!systemTags.length) {
|
||||||
|
container.innerHTML = '<div class="text-sm text-muted">暂无系统标签</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = systemTags.map(tag => `
|
||||||
|
<div class="tag-card" style="border:1px solid var(--border-color);border-radius:8px;margin-bottom:12px;overflow:hidden">
|
||||||
|
<div class="tag-card-header" style="padding:10px 14px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;background:var(--bg-secondary)"
|
||||||
|
onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'block':'none';this.querySelector('.toggle-icon').textContent=this.nextElementSibling.style.display==='none'?'▸':'▾'">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<span class="toggle-icon" style="font-size:12px;color:var(--text-muted)">▾</span>
|
||||||
|
<span class="text-sm font-medium">${escapeHtml(tag.name)}</span>
|
||||||
|
<span class="text-xs text-muted">(${(tag.options||[]).length} 个选项)</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();deleteTag(${tag.id},'${escapeHtml(tag.name)}')" title="删除标签">删除</button>
|
||||||
|
</div>
|
||||||
|
<div class="tag-card-body" style="padding:14px">
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||||||
|
<input class="input" style="flex:1;min-width:120px" value="${escapeHtml(tag.name)}" placeholder="标签名称" id="tag-name-${tag.id}">
|
||||||
|
<input class="input" style="flex:2;min-width:180px" value="${escapeHtml(tag.description||'')}" placeholder="标签描述" id="tag-desc-${tag.id}">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="updateTag(${tag.id})">保存</button>
|
||||||
|
</div>
|
||||||
|
<div id="tag-options-${tag.id}">
|
||||||
|
${(tag.options||[]).map(opt => renderOptionRow(tag.id, opt)).join('')}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm" style="margin-top:8px" onclick="addOption(${tag.id})">+ 添加选项</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOptionRow(tagId, opt) {
|
||||||
|
return `
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:8px;align-items:flex-start" data-opt-id="${opt.id}">
|
||||||
|
<input class="input" style="flex:1;min-width:100px" value="${escapeHtml(opt.label)}" placeholder="选项名称" id="opt-label-${opt.id}">
|
||||||
|
<textarea class="input" style="flex:2;min-width:160px;resize:vertical;min-height:36px" placeholder="约束文本" id="opt-text-${opt.id}">${escapeHtml(opt.constraint_text||'')}</textarea>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="updateOption(${opt.id},${tagId})" title="保存">✓</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteOption(${opt.id},${tagId})" title="删除">✕</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTag(tagId) {
|
||||||
|
const name = document.getElementById(`tag-name-${tagId}`).value.trim();
|
||||||
|
const desc = document.getElementById(`tag-desc-${tagId}`).value.trim();
|
||||||
|
if (!name) { Toast.error('标签名称不能为空'); return; }
|
||||||
|
try {
|
||||||
|
await API.put(`/api/tags/${tagId}`, { name, description: desc });
|
||||||
|
Toast.success('标签已更新');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTag(tagId, tagName) {
|
||||||
|
if (!confirm(`确定要删除标签「${tagName}」及其所有选项吗?`)) return;
|
||||||
|
try {
|
||||||
|
await API.del(`/api/tags/${tagId}`);
|
||||||
|
Toast.success('标签已删除');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOption(optId, tagId) {
|
||||||
|
const label = document.getElementById(`opt-label-${optId}`).value.trim();
|
||||||
|
const text = document.getElementById(`opt-text-${optId}`).value.trim();
|
||||||
|
if (!label) { Toast.error('选项名称不能为空'); return; }
|
||||||
|
try {
|
||||||
|
await API.put(`/api/tag-options/${optId}`, { label, constraint_text: text });
|
||||||
|
Toast.success('选项已更新');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOption(optId, tagId) {
|
||||||
|
if (!confirm('确定要删除此选项吗?')) return;
|
||||||
|
try {
|
||||||
|
await API.del(`/api/tag-options/${optId}`);
|
||||||
|
Toast.success('选项已删除');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addOption(tagId) {
|
||||||
|
try {
|
||||||
|
const res = await API.post(`/api/tags/${tagId}/options`, { label: '新选项', constraint_text: '' });
|
||||||
|
Toast.success('选项已添加');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -70,14 +70,6 @@ func UpdateTag(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ownership
|
|
||||||
var scope string
|
|
||||||
db.DB.QueryRow("SELECT scope FROM tags WHERE id=?", id).Scan(&scope)
|
|
||||||
if scope != "personal" {
|
|
||||||
fail(w, 403, "不能修改系统标签")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@@ -102,13 +94,6 @@ func DeleteTag(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var scope string
|
|
||||||
db.DB.QueryRow("SELECT scope FROM tags WHERE id=?", id).Scan(&scope)
|
|
||||||
if scope != "personal" {
|
|
||||||
fail(w, 403, "不能删除系统标签")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete options first
|
// Delete options first
|
||||||
db.DB.Exec("DELETE FROM tag_options WHERE tag_id=?", id)
|
db.DB.Exec("DELETE FROM tag_options WHERE tag_id=?", id)
|
||||||
db.DB.Exec("DELETE FROM tags WHERE id=?", id)
|
db.DB.Exec("DELETE FROM tags WHERE id=?", id)
|
||||||
|
|||||||
Reference in New Issue
Block a user