Files
wonder 8c5d540565 fix: 修复所有页面 Tab 导航下方冗余空白
- 将 body 布局从普通流改为 flex 纵向布局,navbar 和内容区自然排列
- navbar 移除 position:sticky,改为 flex-shrink:0 作为固定头部
- 新增 .main-content 工具类(flex:1; overflow:hidden),替代 calc(100vh-56px)+margin-top 方案
- builder/dashboard 页面使用 main-content 类占满剩余空间
- settings 页面 margin-top 从 80px 调整为 24px
2026-06-26 17:44:01 +08:00

232 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt Generator — 设置</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/static/common.css">
</head>
<body>
<div style="max-width:640px;margin:24px auto 0;padding:0 24px">
<h1 style="font-size:24px;font-weight:700;margin-bottom:24px">⚙️ 设置</h1>
<!-- LLM 配置 -->
<div class="card mb-4">
<div class="card-header">🤖 LLM 配置</div>
<div class="card-body flex flex-col gap-4">
<div>
<label class="text-sm font-medium mb-2" style="display:block">API Base URL</label>
<input class="input" id="llm-url" placeholder="https://api.deepseek.com/v1">
<div class="text-xs text-muted mt-1">兼容 OpenAI 接口的 API 地址</div>
</div>
<div>
<label class="text-sm font-medium mb-2" style="display:block">API Key</label>
<input class="input" id="llm-key" type="password" placeholder="sk-xxx">
<div class="text-xs text-muted mt-1">留空则不更新。API Key 仅存储在服务端</div>
</div>
<div>
<label class="text-sm font-medium mb-2" style="display:block">模型名称</label>
<input class="input" id="llm-model" placeholder="deepseek-chat">
</div>
<button class="btn btn-primary" onclick="saveSettings()" id="save-btn">保存配置</button>
</div>
</div>
<!-- 主题设置 -->
<div class="card mb-4">
<div class="card-header">🎨 主题设置</div>
<div class="card-body">
<div class="flex items-center justify-between">
<div>
<div class="text-sm font-medium">暗色模式</div>
<div class="text-xs text-muted">切换亮色/暗色主题</div>
</div>
<button class="btn" onclick="Theme.toggle(); updateThemeBtn()">
<span id="theme-label">${Theme.isDark() ? '🌙 暗色' : '☀️ 亮色'}</span>
</button>
</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-header">🔐 账号</div>
<div class="card-body">
<button class="btn btn-danger" onclick="Auth.logout()">退出登录</button>
</div>
</div>
</div>
<script src="/static/common.js"></script>
<script>
function onLogin() {
loadSettings();
loadSystemTags();
}
async function init() {
const ok = await Auth.check();
if (!ok) {
Auth.showLoginModal();
return;
}
renderNavbar('settings');
await loadSettings();
updateThemeBtn();
await loadSystemTags();
}
async function loadSettings() {
try {
const settings = await API.get('/api/settings');
document.getElementById('llm-url').value = settings.llm_api_base_url || '';
document.getElementById('llm-model').value = settings.llm_model_name || '';
} catch (e) {
Toast.error('加载设置失败: ' + e.message);
}
}
async function saveSettings() {
const btn = document.getElementById('save-btn');
btn.disabled = true;
btn.textContent = '保存中...';
try {
await API.put('/api/settings', {
llm_api_base_url: document.getElementById('llm-url').value,
llm_api_key: document.getElementById('llm-key').value,
llm_model_name: document.getElementById('llm-model').value
});
document.getElementById('llm-key').value = '';
Toast.success('配置已保存');
} catch (e) {
Toast.error(e.message);
} finally {
btn.disabled = false;
btn.textContent = '保存配置';
}
}
function updateThemeBtn() {
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();
</script>
</body>
</html>