199 lines
8.4 KiB
HTML
199 lines
8.4 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>反馈统计 - GitLink 帮助中心</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
|
|
|
|
.header { background: #1b2440; color: #fff; padding: 1rem 2rem; display: flex; align-items: center; justify-content: space-between; }
|
|
.header h1 { font-size: 1.2rem; font-weight: 600; }
|
|
.header a { color: #8b9fd4; text-decoration: none; font-size: 0.85rem; }
|
|
.header a:hover { color: #fff; }
|
|
|
|
.login-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
|
.login-box { background: #fff; padding: 2rem; border-radius: 12px; width: 360px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); }
|
|
.login-box h2 { margin-bottom: 1rem; font-size: 1.1rem; }
|
|
.login-box input { width: 100%; padding: 0.6rem 0.8rem; border: 1px solid #ddd; border-radius: 6px; font-size: 0.9rem; margin-bottom: 1rem; }
|
|
.login-box button { width: 100%; padding: 0.6rem; background: #466aff; color: #fff; border: none; border-radius: 6px; font-size: 0.9rem; cursor: pointer; }
|
|
.login-box button:hover { background: #3558e0; }
|
|
.login-error { color: #e74c3c; font-size: 0.85rem; margin-bottom: 0.5rem; }
|
|
|
|
.container { max-width: 1100px; margin: 0 auto; padding: 1.5rem; }
|
|
|
|
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem; }
|
|
.stat-card { background: #fff; border-radius: 10px; padding: 1.25rem; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
|
.stat-card .label { font-size: 0.8rem; color: #888; margin-bottom: 0.3rem; }
|
|
.stat-card .value { font-size: 1.8rem; font-weight: 700; color: #1b2440; }
|
|
.stat-card .value.green { color: #27ae60; }
|
|
.stat-card .value.red { color: #e74c3c; }
|
|
.stat-card .value.blue { color: #466aff; }
|
|
|
|
.section { background: #fff; border-radius: 10px; padding: 1.25rem; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 1.5rem; }
|
|
.section h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #eee; }
|
|
|
|
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
|
th { text-align: left; padding: 0.6rem 0.5rem; border-bottom: 2px solid #eee; color: #888; font-weight: 600; font-size: 0.8rem; }
|
|
td { padding: 0.6rem 0.5rem; border-bottom: 1px solid #f0f0f0; }
|
|
tr:hover td { background: #f8f9fb; }
|
|
|
|
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 10px; font-size: 0.75rem; font-weight: 600; }
|
|
.badge-yes { background: #e8f8ef; color: #27ae60; }
|
|
.badge-no { background: #fde8e8; color: #e74c3c; }
|
|
|
|
.bar { height: 8px; border-radius: 4px; background: #eee; overflow: hidden; min-width: 60px; }
|
|
.bar-fill { height: 100%; border-radius: 4px; background: #466aff; }
|
|
|
|
.suggestion-text { background: #f8f9fb; padding: 0.4rem 0.6rem; border-radius: 4px; font-size: 0.82rem; color: #555; margin-top: 0.2rem; }
|
|
.time { color: #aaa; font-size: 0.8rem; }
|
|
|
|
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
|
|
.tab { padding: 0.4rem 1rem; border-radius: 6px; border: 1px solid #ddd; background: #fff; cursor: pointer; font-size: 0.85rem; }
|
|
.tab.active { background: #466aff; color: #fff; border-color: #466aff; }
|
|
|
|
.empty { text-align: center; padding: 2rem; color: #aaa; }
|
|
|
|
@media (max-width: 768px) {
|
|
.stats-row { grid-template-columns: repeat(2, 1fr); }
|
|
.container { padding: 1rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1>GitLink 帮助中心 — 反馈统计</h1>
|
|
<a href="/">← 返回帮助中心</a>
|
|
</div>
|
|
|
|
<div id="loginOverlay" class="login-overlay">
|
|
<div class="login-box">
|
|
<h2>管理员登录</h2>
|
|
<div id="loginError" class="login-error" style="display:none"></div>
|
|
<input type="password" id="tokenInput" placeholder="请输入管理口令" onkeydown="if(event.key==='Enter')doLogin()">
|
|
<button onclick="doLogin()">登 录</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container" id="mainContent" style="display:none">
|
|
<div class="stats-row" id="statsRow"></div>
|
|
|
|
<div class="section">
|
|
<h2>页面反馈分布</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>页面路径</th><th>有帮助</th><th>没帮助</th><th>帮助率</th><th>建议</th></tr>
|
|
</thead>
|
|
<tbody id="byPathBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="tabs">
|
|
<div class="tab active" onclick="switchTab('all',this)">全部</div>
|
|
<div class="tab" onclick="switchTab('suggestion',this)">仅含建议</div>
|
|
</div>
|
|
<h2>最近反馈</h2>
|
|
<table>
|
|
<thead>
|
|
<tr><th>时间</th><th>页面</th><th>评价</th><th>建议内容</th></tr>
|
|
</thead>
|
|
<tbody id="recentBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let TOKEN = '';
|
|
let currentFilter = 'all';
|
|
let recentData = [];
|
|
|
|
function doLogin() {
|
|
TOKEN = document.getElementById('tokenInput').value.trim();
|
|
loadData();
|
|
}
|
|
|
|
async function loadData() {
|
|
try {
|
|
const [statsRes, recentRes] = await Promise.all([
|
|
fetch('/api/feedback/stats?token=' + TOKEN),
|
|
fetch('/api/feedback/recent?token=' + TOKEN + '&limit=100'),
|
|
]);
|
|
if (!statsRes.ok || !recentRes.ok) {
|
|
document.getElementById('loginError').style.display = 'block';
|
|
document.getElementById('loginError').textContent = '口令错误,请重试';
|
|
return;
|
|
}
|
|
document.getElementById('loginOverlay').style.display = 'none';
|
|
document.getElementById('mainContent').style.display = 'block';
|
|
|
|
const stats = await statsRes.json();
|
|
recentData = await recentRes.json();
|
|
|
|
renderStats(stats);
|
|
renderByPath(stats.byPath);
|
|
renderRecent();
|
|
} catch (e) {
|
|
document.getElementById('loginError').style.display = 'block';
|
|
document.getElementById('loginError').textContent = '网络错误: ' + e.message;
|
|
}
|
|
}
|
|
|
|
function renderStats(s) {
|
|
document.getElementById('statsRow').innerHTML = `
|
|
<div class="stat-card"><div class="label">总反馈数</div><div class="value">${s.total}</div></div>
|
|
<div class="stat-card"><div class="label">有帮助</div><div class="value green">${s.yes}</div></div>
|
|
<div class="stat-card"><div class="label">没帮助</div><div class="value red">${s.no}</div></div>
|
|
<div class="stat-card"><div class="label">帮助率</div><div class="value blue">${s.helpfulRate}%</div></div>
|
|
`;
|
|
}
|
|
|
|
function renderByPath(paths) {
|
|
const tbody = document.getElementById('byPathBody');
|
|
if (!paths.length) { tbody.innerHTML = '<tr><td colspan="5" class="empty">暂无数据</td></tr>'; return; }
|
|
tbody.innerHTML = paths.slice(0, 30).map(p => {
|
|
const total = p.yes + p.no;
|
|
const rate = total > 0 ? Math.round((p.yes / total) * 100) : 0;
|
|
return `<tr>
|
|
<td><a href="${p.path}" target="_blank">${p.path}</a></td>
|
|
<td>${p.yes}</td><td>${p.no}</td>
|
|
<td><div class="bar"><div class="bar-fill" style="width:${rate}%"></div></div> ${rate}%</td>
|
|
<td>${p.suggestions.length > 0 ? p.suggestions.length + ' 条' : '-'}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderRecent() {
|
|
const tbody = document.getElementById('recentBody');
|
|
let data = recentData;
|
|
if (currentFilter === 'suggestion') {
|
|
data = data.filter(e => e.suggestion);
|
|
}
|
|
if (!data.length) { tbody.innerHTML = '<tr><td colspan="4" class="empty">暂无数据</td></tr>'; return; }
|
|
tbody.innerHTML = data.map(e => `<tr>
|
|
<td class="time">${new Date(e.timestamp).toLocaleString('zh-CN')}</td>
|
|
<td><a href="${e.docPath}" target="_blank">${e.docPath}</a></td>
|
|
<td><span class="badge ${e.vote === 'yes' ? 'badge-yes' : 'badge-no'}">${e.vote === 'yes' ? '有帮助' : '没帮助'}</span></td>
|
|
<td>${e.suggestion ? '<div class="suggestion-text">' + escHtml(e.suggestion) + '</div>' : '-'}</td>
|
|
</tr>`).join('');
|
|
}
|
|
|
|
function switchTab(filter, el) {
|
|
currentFilter = filter;
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
el.classList.add('active');
|
|
renderRecent();
|
|
}
|
|
|
|
function escHtml(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|