代码推送
This commit is contained in:
273
web/dist/admin/index.html
vendored
273
web/dist/admin/index.html
vendored
@@ -216,19 +216,38 @@ tbody tr:last-child td{border-bottom:none}
|
||||
<!-- Login Overlay -->
|
||||
<div class="login-overlay" id="loginOverlay">
|
||||
<div class="login-card">
|
||||
<h1>🔐 管理后台</h1>
|
||||
<p class="subtitle">Gotest Admin Dashboard</p>
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="loginUsername" placeholder="请输入用户名" value="admin">
|
||||
<h1 id="loginTitle">🔐 管理后台</h1>
|
||||
<p class="subtitle" id="loginSubtitle">Gotest Admin Dashboard</p>
|
||||
<!-- 登录表单 -->
|
||||
<div id="loginForm">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="loginUsername" placeholder="请输入用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="loginPassword" placeholder="请输入密码">
|
||||
</div>
|
||||
<button class="login-btn" id="loginBtn" onclick="doLogin()">登 录</button>
|
||||
<div class="login-error" id="loginError"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="loginPassword" placeholder="请输入密码" value="admin123">
|
||||
<!-- 初始化表单(无管理员时显示) -->
|
||||
<div id="initForm" style="display:none">
|
||||
<div class="form-group">
|
||||
<label>管理员用户名</label>
|
||||
<input type="text" id="initUsername" placeholder="请设置管理员用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>管理员密码</label>
|
||||
<input type="password" id="initPassword" placeholder="请设置密码(至少6位)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认密码</label>
|
||||
<input type="password" id="initPasswordConfirm" placeholder="请再次输入密码">
|
||||
</div>
|
||||
<button class="login-btn" id="initBtn" onclick="doInitAdmin()">初始化管理员</button>
|
||||
<div class="login-error" id="initError"></div>
|
||||
</div>
|
||||
<button class="login-btn" id="loginBtn" onclick="doLogin()">登 录</button>
|
||||
<div class="login-error" id="loginError"></div>
|
||||
<div class="login-hint">默认账号: admin / admin123</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -248,6 +267,12 @@ tbody tr:last-child td{border-bottom:none}
|
||||
<span class="icon">📦</span> 内容管理
|
||||
</a>
|
||||
<div class="sidebar-section">系统</div>
|
||||
<a class="sidebar-link" onclick="navigate('accesslog')" data-page="accesslog">
|
||||
<span class="icon">📊</span> 访问日志
|
||||
</a>
|
||||
<a class="sidebar-link" onclick="navigate('webhook')" data-page="webhook">
|
||||
<span class="icon">🔔</span> Webhook配置
|
||||
</a>
|
||||
<a class="sidebar-link" onclick="navigate('settings')" data-page="settings">
|
||||
<span class="icon">⚙️</span> 系统设置
|
||||
</a>
|
||||
@@ -343,6 +368,45 @@ function toast(msg, type = 'info') {
|
||||
}
|
||||
|
||||
// ===== 登录/登出 =====
|
||||
async function checkAdminExists() {
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/admin/check');
|
||||
const data = await resp.json();
|
||||
return data.data && data.data.initialized;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function doInitAdmin() {
|
||||
const btn = document.getElementById('initBtn');
|
||||
const username = document.getElementById('initUsername').value.trim();
|
||||
const password = document.getElementById('initPassword').value;
|
||||
const confirm = document.getElementById('initPasswordConfirm').value;
|
||||
const errEl = document.getElementById('initError');
|
||||
if (!username || !password) { errEl.textContent = '请输入用户名和密码'; return; }
|
||||
if (password.length < 6) { errEl.textContent = '密码至少6位'; return; }
|
||||
if (password !== confirm) { errEl.textContent = '两次密码不一致'; return; }
|
||||
btn.disabled = true; btn.textContent = '初始化中...'; errEl.textContent = '';
|
||||
try {
|
||||
const resp = await fetch(API_BASE + '/admin/init', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.code !== 0) { errEl.textContent = data.message || '初始化失败'; return; }
|
||||
toast('初始化成功,请登录', 'success');
|
||||
document.getElementById('initForm').style.display = 'none';
|
||||
document.getElementById('loginForm').style.display = 'block';
|
||||
document.getElementById('loginSubtitle').textContent = '请输入刚创建的管理员账号';
|
||||
} catch (e) {
|
||||
errEl.textContent = '网络错误,请重试';
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '初始化管理员';
|
||||
}
|
||||
}
|
||||
|
||||
async function doLogin() {
|
||||
const btn = document.getElementById('loginBtn');
|
||||
const username = document.getElementById('loginUsername').value.trim();
|
||||
@@ -391,12 +455,14 @@ function navigate(page, params = {}) {
|
||||
document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active'));
|
||||
const link = document.querySelector(`[data-page="${page}"]`);
|
||||
if (link) link.classList.add('active');
|
||||
const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航' };
|
||||
const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航', accesslog: '访问日志', webhook: 'Webhook配置' };
|
||||
document.getElementById('pageTitle').textContent = titles[page] || page;
|
||||
if (page === 'users') renderUsers();
|
||||
else if (page === 'content') renderContent(params.table);
|
||||
else if (page === 'settings') renderSettings();
|
||||
else if (page === 'sites') renderSites();
|
||||
else if (page === 'accesslog') renderAccessLogs();
|
||||
else if (page === 'webhook') renderWebhook();
|
||||
if (window.innerWidth <= 768) document.getElementById('sidebar').classList.remove('open');
|
||||
}
|
||||
|
||||
@@ -1050,6 +1116,175 @@ function renderSites() {
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
// ===== 访问日志 =====
|
||||
let accessLogPage = 1, accessLogPageSize = 20;
|
||||
async function renderAccessLogs(page) {
|
||||
if (page) accessLogPage = page;
|
||||
const el = document.getElementById('content');
|
||||
el.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
try {
|
||||
const [statsData, logsData] = await Promise.all([
|
||||
api('/admin/access-logs/stats'),
|
||||
api(`/admin/access-logs?page=${accessLogPage}&page_size=${accessLogPageSize}`)
|
||||
]);
|
||||
const stats = statsData.data;
|
||||
const logs = logsData.data;
|
||||
|
||||
let hourlyChart = '';
|
||||
if (stats.hourly && stats.hourly.length > 0) {
|
||||
const maxCount = Math.max(...stats.hourly.map(h => h.count));
|
||||
hourlyChart = stats.hourly.map(h => {
|
||||
const pct = maxCount > 0 ? (h.count / maxCount * 100) : 0;
|
||||
const hour = h.hour ? h.hour.split(' ')[1] || h.hour : '';
|
||||
return `<div style="display:flex;align-items:end;gap:4px;flex-direction:column;align-items:center;flex:1">
|
||||
<div style="font-size:11px;color:var(--text-muted)">${h.count}</div>
|
||||
<div style="width:100%;max-width:32px;background:var(--primary);border-radius:4px 4px 0 0;height:${Math.max(4, pct)}%;min-height:4px;transition:height .3s"></div>
|
||||
<div style="font-size:10px;color:var(--text-muted)">${hour}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="stats-grid" style="margin-bottom:24px">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-blue">📈</div>
|
||||
<div class="stat-value">${stats.total_visits || 0}</div>
|
||||
<div class="stat-label">总访问量</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-green">📅</div>
|
||||
<div class="stat-value">${stats.today_visits || 0}</div>
|
||||
<div class="stat-label">今日访问</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon bg-purple">🌐</div>
|
||||
<div class="stat-value">${stats.unique_ips || 0}</div>
|
||||
<div class="stat-label">独立IP</div>
|
||||
</div>
|
||||
</div>
|
||||
${hourlyChart ? `<div class="table-card" style="margin-bottom:24px">
|
||||
<div class="section-header" style="padding:16px"><h4>📊 最近24小时访问趋势</h4></div>
|
||||
<div style="padding:16px;display:flex;align-items:end;gap:6px;height:180px">${hourlyChart}</div>
|
||||
</div>` : ''}
|
||||
<div class="table-card">
|
||||
<div class="table-toolbar">
|
||||
<h4 style="font-size:14px;font-weight:600">📋 访问记录</h4>
|
||||
<button class="btn btn-danger btn-sm" onclick="clearAccessLogs()">清空日志</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>时间</th><th>IP</th><th>归属地</th><th>路径</th><th>方法</th><th>状态</th><th>耗时</th></tr></thead>
|
||||
<tbody>
|
||||
${logs.list.length === 0 ? '<tr class="empty-row"><td colspan="7">暂无访问记录</td></tr>' : logs.list.map(l => `
|
||||
<tr>
|
||||
<td>${formatDate(l.created_at)}</td>
|
||||
<td><code style="background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:12px">${l.ip}</code></td>
|
||||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${l.location||''}">${l.location || '-'}</td>
|
||||
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${l.path}">${l.path}</td>
|
||||
<td><span class="badge-status ${l.method==='GET'?'badge-active':'badge-inactive'}">${l.method}</span></td>
|
||||
<td><span class="badge-status ${l.status<400?'badge-active':'badge-inactive'}">${l.status}</span></td>
|
||||
<td>${l.cost}ms</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${renderPagination(logs, 'renderAccessLogs')}
|
||||
</div>`;
|
||||
} catch (e) { el.innerHTML = '<div class="loading">加载失败</div>'; }
|
||||
}
|
||||
|
||||
async function clearAccessLogs() {
|
||||
if (!confirm('确定清空所有访问日志?')) return;
|
||||
try {
|
||||
await api('/admin/access-logs', { method: 'DELETE' });
|
||||
toast('清空成功', 'success');
|
||||
renderAccessLogs(1);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ===== Webhook 配置 =====
|
||||
async function renderWebhook() {
|
||||
const el = document.getElementById('content');
|
||||
el.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
try {
|
||||
const data = await api('/admin/config/wechat_webhook_url');
|
||||
const url = data.data ? data.data.value : '';
|
||||
el.innerHTML = `
|
||||
<div class="table-card">
|
||||
<div class="section-header" style="padding:16px"><h4>🔔 企业微信 Webhook 配置</h4></div>
|
||||
<div style="padding:24px;max-width:700px">
|
||||
<div class="form-group">
|
||||
<label>Webhook URL</label>
|
||||
<input type="text" id="webhookUrl" value="${url || ''}" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" style="width:100%">
|
||||
<div style="font-size:12px;color:var(--text-muted);margin-top:6px">
|
||||
企业微信群机器人的 Webhook 地址,用于接收客户咨询通知和系统告警
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:20px">
|
||||
<button class="btn btn-primary" onclick="saveWebhook()">保存配置</button>
|
||||
<button class="btn btn-outline" onclick="testWebhook()">测试发送</button>
|
||||
</div>
|
||||
<div id="webhookTestResult" style="margin-top:12px;display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card" style="margin-top:16px">
|
||||
<div class="section-header" style="padding:16px"><h4>📖 使用说明</h4></div>
|
||||
<div style="padding:0 24px 24px">
|
||||
<ol style="line-height:2;color:var(--text-muted);font-size:13px">
|
||||
<li>在企业微信群中添加一个<strong>群机器人</strong></li>
|
||||
<li>点击机器人头像,复制 <strong>Webhook 地址</strong></li>
|
||||
<li>将地址粘贴到上方输入框并保存</li>
|
||||
<li>点击「测试发送」验证配置是否正确</li>
|
||||
<li>配置成功后,客户咨询通知将自动推送到该群</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>`;
|
||||
} catch (e) { el.innerHTML = '<div class="loading">加载失败</div>'; }
|
||||
}
|
||||
|
||||
async function saveWebhook() {
|
||||
const url = document.getElementById('webhookUrl').value.trim();
|
||||
try {
|
||||
await api('/admin/config/wechat_webhook_url', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: url, remark: '企业微信群机器人Webhook地址' })
|
||||
});
|
||||
toast('保存成功', 'success');
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
async function testWebhook() {
|
||||
const resultEl = document.getElementById('webhookTestResult');
|
||||
resultEl.style.display = 'block';
|
||||
resultEl.innerHTML = '<div style="color:var(--text-muted)">发送中...</div>';
|
||||
try {
|
||||
await api('/admin/config/wechat_webhook_url', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ value: document.getElementById('webhookUrl').value.trim(), remark: '企业微信群机器人Webhook地址' })
|
||||
});
|
||||
// 通过客户咨询接口发送测试消息
|
||||
const resp = await fetch(API_BASE + '/customer/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: '测试用户',
|
||||
phone: '13800000000',
|
||||
company: '测试公司',
|
||||
email: 'test@example.com',
|
||||
needs: '这是一条测试消息,用于验证 Webhook 配置是否正确'
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.code === 0) {
|
||||
resultEl.innerHTML = '<div style="color:var(--success);font-weight:600">✅ 测试消息已发送,请检查企业微信群</div>';
|
||||
} else {
|
||||
resultEl.innerHTML = `<div style="color:var(--danger)">❌ 发送失败: ${data.message}</div>`;
|
||||
}
|
||||
} catch (e) {
|
||||
resultEl.innerHTML = '<div style="color:var(--danger)">❌ 网络错误</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 通用工具 =====
|
||||
function formatDate(d) {
|
||||
if (!d) return '-';
|
||||
@@ -1126,6 +1361,20 @@ document.getElementById('modalOverlay').addEventListener('click', (e) => {
|
||||
document.getElementById('loginPassword').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') doLogin();
|
||||
});
|
||||
document.getElementById('initPasswordConfirm')?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') doInitAdmin();
|
||||
});
|
||||
|
||||
// 检查管理员是否存在,决定显示登录还是初始化
|
||||
(async function() {
|
||||
const hasAdmin = await checkAdminExists();
|
||||
if (!hasAdmin) {
|
||||
document.getElementById('loginForm').style.display = 'none';
|
||||
document.getElementById('initForm').style.display = 'block';
|
||||
document.getElementById('loginTitle').textContent = '🚀 初始化系统';
|
||||
document.getElementById('loginSubtitle').textContent = '首次使用,请创建管理员账号';
|
||||
}
|
||||
})();
|
||||
|
||||
// 检查登录状态
|
||||
if (token) {
|
||||
|
||||
Reference in New Issue
Block a user