Files
prompt-generator/frontend/static/common.js
T
wonder f456cdb565 feat: add frontend — builder, dashboard, and settings pages
- common.css: 全局样式,支持亮色/暗色主题,完整组件库
- common.js: API 封装、认证模块、Toast 提示、主题切换、草稿管理
- index.html: Prompt 构建器完整 UI
  - 左侧面板:项目/会话选择、标签系统(搜索/折叠/单选)、片段选择(分组/搜索)、CLAUDE.md 输入、自定义内容
  - 右侧面板:实时 Prompt 预览、智能建议列表、复制到剪贴板
  - 草稿自动保存(localStorage,防抖 1s)
  - 新增个人标签模态框
  - 保存构建会话功能
- dashboard.html: 复盘看板
  - 左侧项目树(项目→会话层级)
  - 右侧 Prompt 列表(文本视图,XML 标签折叠)
  - 导出功能(Markdown/JSON/纯文本)
  - 关键词搜索
- settings.html: 设置页
  - LLM 配置(API URL/Key/模型)
  - 主题切换
  - 退出登录

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 15:05:46 +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.html' },
{ id: 'settings', label: '设置', href: '/settings.html' },
];
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);
}