Files
toworker/smsreceiver-worker/pages/public/app.js
2026-03-23 12:58:51 +08:00

320 lines
10 KiB
JavaScript

const API_BASE = window.API_BASE || 'https://sms-api.ouai.nyc.mn'
const app = document.getElementById('app')
const state = {
view: 'login',
messages: [],
logs: [],
page: 1,
total: 0,
logsPage: 1,
logsTotal: 0,
currentMessage: null,
fromNumbers: [],
selectedFrom: '',
search: '',
stats: { total: 0, today: 0, failed: 0 },
startTs: 0,
endTs: 0,
}
function monthRange(offset = 0) {
const now = new Date()
const y = now.getFullYear()
const m = now.getMonth() + offset
const start = new Date(y, m, 1, 0, 0, 0)
const end = new Date(y, m + 1, 0, 23, 59, 59)
return [start.getTime(), end.getTime()]
}
function initDefaultRange() {
const [s, e] = monthRange(0)
state.startTs = s
state.endTs = e
}
async function api(path, options = {}) {
const res = await fetch(`${API_BASE}${path}`, {
credentials: 'include',
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
...options,
})
return res.json()
}
function setView(view) {
state.view = view
render()
}
function nav(active) {
return `
<div class="header pixel-card">
<div class="title">📱 短信转发接收端</div>
<div class="nav">
<a class="btn ${active==='list'?'active':''}" data-view="list">短信列表</a>
<a class="btn ${active==='logs'?'active':''}" data-view="logs">接收日志</a>
<a class="btn ${active==='stats'?'active':''}" data-view="stats">统计信息</a>
<a class="btn danger" id="logoutBtn">退出</a>
</div>
</div>
`
}
function loginView() {
return `
<div class="login-wrap">
<div class="login-card pixel-card">
<h1>📱 短信转发接收端</h1>
<div class="form-group">
<label>用户名</label>
<input id="username" placeholder="请输入用户名" />
</div>
<div class="form-group">
<label>密码</label>
<input id="password" type="password" placeholder="请输入密码" />
</div>
<button class="btn primary" id="loginBtn">登录</button>
</div>
</div>
`
}
function statsCards() {
const s = state.stats
return `
<div class="stats">
<div class="stat-card pixel-card"><div class="label">总短信</div><div class="value">${s.total}</div></div>
<div class="stat-card pixel-card"><div class="label">今日</div><div class="value">${s.today}</div></div>
<div class="stat-card pixel-card"><div class="label">异常</div><div class="value">${s.failed}</div></div>
</div>
`
}
function fromFilter() {
const tags = state.fromNumbers.map(n => `
<span class="tag ${state.selectedFrom===n?'active':''}" data-from="${n}">${n}</span>
`).join('')
return `
<div class="filter pixel-card">
<div class="label">发送方筛选</div>
<div class="tags">
<span class="tag ${state.selectedFrom===''?'active':''}" data-from="">全部</span>
${tags}
</div>
</div>
`
}
function timeFilter() {
return `
<div class="toolbar pixel-card">
<span class="label">时间筛选</span>
<button class="btn" id="thisMonthBtn">本月</button>
<button class="btn" id="lastMonthBtn">上月</button>
<button class="btn" id="allBtn">全部</button>
</div>
`
}
function toolbar() {
return `
<div class="toolbar pixel-card">
<input id="search" placeholder="搜索内容/号码" value="${state.search}" />
<button class="btn primary" id="searchBtn">搜索</button>
<button class="btn" id="refreshBtn">刷新</button>
</div>
`
}
function listView() {
const rows = state.messages.map(m => `
<tr>
<td>${m.id}</td>
<td>${m.from_number}</td>
<td class="ellipsis">${m.content}</td>
<td>${new Date(m.timestamp).toLocaleString()}</td>
<td><a class="btn small" data-id="${m.id}" data-action="detail">详情</a></td>
</tr>
`).join('')
return `
${nav('list')}
${statsCards()}
${fromFilter()}
${timeFilter()}
${toolbar()}
<div class="table-wrap pixel-card">
<table>
<thead><tr><th>ID</th><th>号码</th><th>内容</th><th>时间</th><th></th></tr></thead>
<tbody>${rows || ''}</tbody>
</table>
</div>
<div class="pager">
<button class="btn" id="prevPage">上一页</button>
<span>第 ${state.page} 页</span>
<button class="btn" id="nextPage">下一页</button>
</div>
`
}
function detailView() {
const m = state.currentMessage
if (!m) return ''
return `
${nav('list')}
<div class="detail pixel-card">
<h3>📱 短信详情</h3>
<div class="detail-row"><div class="k">ID</div><div class="v">${m.id}</div></div>
<div class="detail-row"><div class="k">发送方号码</div><div class="v">${m.from_number}</div></div>
<div class="detail-row"><div class="k">短信内容</div><div class="v">${m.content}</div></div>
<div class="detail-row"><div class="k">原始时间戳</div><div class="v">${m.timestamp}</div></div>
<div class="detail-row"><div class="k">入库时间</div><div class="v">${new Date(m.created_at || m.timestamp).toLocaleString()}</div></div>
<div class="detail-row"><div class="k">签名验证</div><div class="v">${m.sign_verified ? '已验证' : '未验证'}</div></div>
<div class="detail-row"><div class="k">设备信息</div><div class="v">${m.device_info || '-'}</div></div>
<div class="detail-row"><div class="k">SIM 卡信息</div><div class="v">${m.sim_info || '-'}</div></div>
<div class="detail-row"><div class="k">IP 地址</div><div class="v">${m.ip_address || '-'}</div></div>
<button class="btn" id="backBtn">返回列表</button>
</div>
`
}
function logsView() {
const rows = state.logs.map(l => `
<tr>
<td>${l.id}</td>
<td>${l.from_number}</td>
<td><span class="badge ${l.status==='success'?'ok':'err'}">${l.status}</span></td>
<td>${l.error_message || ''}</td>
<td>${new Date(l.timestamp).toLocaleString()}</td>
</tr>
`).join('')
return `
${nav('logs')}
<div class="table-wrap pixel-card">
<table>
<thead><tr><th>ID</th><th>号码</th><th>状态</th><th>错误</th><th>时间</th></tr></thead>
<tbody>${rows || ''}</tbody>
</table>
</div>
<div class="pager">
<button class="btn" id="prevLogs">上一页</button>
<span>第 ${state.logsPage} 页</span>
<button class="btn" id="nextLogs">下一页</button>
</div>
`
}
function statsView() {
return `
${nav('stats')}
${statsCards()}
<div class="detail pixel-card">
<h3>统计概览</h3>
<div class="detail-row"><div class="k">总短信</div><div class="v">${state.stats.total}</div></div>
<div class="detail-row"><div class="k">今日</div><div class="v">${state.stats.today}</div></div>
<div class="detail-row"><div class="k">异常</div><div class="v">${state.stats.failed}</div></div>
</div>
`
}
async function loadStats() {
const data = await api('/api/stats')
if (data.success) state.stats = data.data
}
async function loadMessages() {
const data = await api(`/api/messages?page=${state.page}&limit=20&from=${encodeURIComponent(state.selectedFrom)}&q=${encodeURIComponent(state.search)}&start_ts=${state.startTs}&end_ts=${state.endTs}`)
if (!data.success) return
state.messages = data.data
state.total = data.total
state.fromNumbers = data.from_numbers || []
}
async function loadLogs() {
const data = await api(`/api/logs?page=${state.logsPage}&limit=20`)
if (!data.success) return
state.logs = data.data
state.logsTotal = data.total
}
async function render() {
if (state.view === 'login') {
app.innerHTML = loginView()
document.getElementById('loginBtn').onclick = async () => {
const username = document.getElementById('username').value
const password = document.getElementById('password').value
const res = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) })
if (res.success) {
initDefaultRange()
state.view = 'list'
await loadStats()
await loadMessages()
render()
} else alert(res.error || '登录失败')
}
return
}
if (state.view === 'list') {
await loadStats()
await loadMessages()
app.innerHTML = listView()
bindCommon()
document.querySelectorAll('.tag').forEach(tag => {
tag.onclick = () => { state.selectedFrom = tag.dataset.from || ''; state.page = 1; render(); }
})
document.getElementById('searchBtn').onclick = () => { state.search = document.getElementById('search').value; state.page = 1; render(); }
document.getElementById('refreshBtn').onclick = () => render()
document.getElementById('prevPage').onclick = () => { if (state.page>1) { state.page--; render(); } }
document.getElementById('nextPage').onclick = () => { state.page++; render(); }
document.getElementById('thisMonthBtn').onclick = () => { const [s,e] = monthRange(0); state.startTs=s; state.endTs=e; state.page=1; render(); }
document.getElementById('lastMonthBtn').onclick = () => { const [s,e] = monthRange(-1); state.startTs=s; state.endTs=e; state.page=1; render(); }
document.getElementById('allBtn').onclick = () => { state.startTs=0; state.endTs=0; state.page=1; render(); }
document.querySelectorAll('[data-action="detail"]').forEach(btn => {
btn.onclick = async () => {
const id = btn.dataset.id
const data = await api(`/api/messages/${id}`)
if (data.success) { state.currentMessage = data.data; state.view = 'detail'; render(); }
}
})
return
}
if (state.view === 'detail') {
app.innerHTML = detailView()
bindCommon()
document.getElementById('backBtn').onclick = () => { state.view = 'list'; render(); }
return
}
if (state.view === 'logs') {
await loadLogs()
app.innerHTML = logsView()
bindCommon()
document.getElementById('prevLogs').onclick = () => { if (state.logsPage>1) { state.logsPage--; render(); } }
document.getElementById('nextLogs').onclick = () => { state.logsPage++; render(); }
return
}
if (state.view === 'stats') {
await loadStats()
app.innerHTML = statsView()
bindCommon()
return
}
}
function bindCommon() {
document.querySelectorAll('[data-view]').forEach(a => {
a.onclick = () => setView(a.dataset.view)
})
const logout = document.getElementById('logoutBtn')
if (logout) logout.onclick = async () => { await api('/api/auth/logout', { method: 'POST' }); state.view='login'; render(); }
}
initDefaultRange()
render()