Files
prompt-generator/frontend/static/common.js
T
wonder b7008afd29 fix: SPA routing and prompts table handling
- Add clean URL routing: /dashboard -> dashboard.html, /settings -> settings.html
- Add EnsurePromptsTable for dev environments where prompts table may not exist
- Fix nav links to use clean URLs instead of .html paths

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 15:08:12 +08:00

197 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Prompt Generator — 全局 JS 模块
const API = {
async request(method, path, body) {
const opts = {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
};
if (body !== undefined) {
opts.body = JSON.stringify(body);
}
const resp = await fetch(path, opts);
const data = await resp.json();
if (resp.status === 401) {
Auth.showLoginModal();
throw new Error('未登录');
}
if (data.code !== 0) {
throw new Error(data.message || '请求失败');
}
return data.data;
},
get(path) { return this.request('GET', path); },
post(path, body) { return this.request('POST', path, body); },
put(path, body) { return this.request('PUT', path, body); },
del(path) { return this.request('DELETE', path); },
};
// 认证模块
const Auth = {
modal: null,
showLoginModal() {
if (this.modal) return;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
<div class="modal">
<div class="modal-title">🔐 输入访问密码</div>
<input type="password" class="input mb-3" id="login-password" placeholder="请输入密码" autofocus>
<div id="login-error" class="text-sm mb-3" style="color:var(--danger-color);display:none"></div>
<button class="btn btn-primary w-full" id="login-btn">登录</button>
</div>
`;
document.body.appendChild(overlay);
this.modal = overlay;
const input = overlay.querySelector('#login-password');
const btn = overlay.querySelector('#login-btn');
const err = overlay.querySelector('#login-error');
const doLogin = async () => {
try {
btn.disabled = true;
btn.textContent = '登录中...';
await API.post('/api/auth/login', { password: input.value });
overlay.remove();
this.modal = null;
Toast.success('登录成功');
// Reload page data
if (typeof onLogin === 'function') onLogin();
} catch (e) {
err.textContent = e.message;
err.style.display = 'block';
btn.disabled = false;
btn.textContent = '登录';
}
};
btn.onclick = doLogin;
input.onkeydown = (e) => { if (e.key === 'Enter') doLogin(); };
},
async check() {
try {
await API.get('/api/auth/check');
return true;
} catch {
return false;
}
},
async logout() {
try {
await API.post('/api/auth/logout');
} catch {}
location.reload();
}
};
// Toast 提示
const Toast = {
show(message, type = 'success', duration = 3000) {
const el = document.createElement('div');
el.className = `toast toast-${type}`;
el.textContent = message;
document.body.appendChild(el);
setTimeout(() => el.remove(), duration);
},
success(msg) { this.show(msg, 'success'); },
error(msg) { this.show(msg, 'error', 5000); },
};
// 主题切换
const Theme = {
init() {
const saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-theme', 'dark');
}
},
toggle() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
},
isDark() {
return document.documentElement.getAttribute('data-theme') === 'dark';
}
};
// 草稿管理(localStorage)
const Draft = {
save(key, data) {
localStorage.setItem(`draft_${key}`, JSON.stringify(data));
},
load(key) {
const raw = localStorage.getItem(`draft_${key}`);
if (!raw) return null;
try { return JSON.parse(raw); } catch { return null; }
},
clear(key) {
localStorage.removeItem(`draft_${key}`);
}
};
// 防抖
function debounce(fn, delay = 1000) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 工具函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function truncateText(text, maxLen = 150) {
if (!text) return '';
// 跳过 XML 标签内容,提取用户实际输入
const cleaned = text.replace(/<[a-zA-Z_-][^>]*>[\s\S]*?<\/[a-zA-Z_-][^>]*>/g, '').trim();
if (cleaned.length <= maxLen) return cleaned;
return cleaned.substring(0, maxLen) + '...';
}
function formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
// 初始化
Theme.init();
// 导航栏渲染
function renderNavbar(activePage) {
const pages = [
{ id: 'builder', label: '构建器', href: '/' },
{ id: 'dashboard', label: '复盘看板', href: '/dashboard' },
{ id: 'settings', label: '设置', href: '/settings' },
];
const nav = document.createElement('nav');
nav.className = 'navbar';
nav.innerHTML = `
<div class="nav-brand">🔥 Prompt Generator</div>
<div class="nav-tabs">
${pages.map(p => `<a class="nav-tab ${p.id === activePage ? 'active' : ''}" href="${p.href}">${p.label}</a>`).join('')}
</div>
<div class="nav-actions">
<button class="btn btn-sm" onclick="Theme.toggle()" title="切换主题">${Theme.isDark() ? '☀️' : '🌙'}</button>
<button class="btn btn-sm" onclick="Auth.logout()">退出</button>
</div>
`;
document.body.insertBefore(nav, document.body.firstChild);
}