441 lines
20 KiB
JavaScript
441 lines
20 KiB
JavaScript
const state = { uid: '7716940453', mode: 'any', query: '', terms: [], count: 50 }
|
|
const wbOut = document.getElementById('wb-debug-output')
|
|
const wbBtnCopy = document.getElementById('wb-btn-copy')
|
|
const wbBtnRefresh = document.getElementById('wb-btn-refresh')
|
|
const wbBtnClear = document.getElementById('wb-btn-clear')
|
|
|
|
const wbRender = lines => {
|
|
if (!wbOut) return
|
|
wbOut.textContent = lines.join('\n')
|
|
wbOut.scrollTop = wbOut.scrollHeight
|
|
}
|
|
|
|
const wbLoad = async () => {
|
|
try {
|
|
const res = await fetch('/api/debug/logs?limit=5000&channel=weibo')
|
|
const data = await res.json()
|
|
if (data.ok) wbRender(data.lines)
|
|
} catch {}
|
|
}
|
|
|
|
const ts = () => new Date().toISOString()
|
|
const logLocal = msg => {
|
|
if (!wbOut) return
|
|
const line = `${ts()} ${msg}`
|
|
const current = wbOut.textContent || ''
|
|
wbOut.textContent = current ? current + "\n" + line : line
|
|
wbOut.scrollTop = wbOut.scrollHeight
|
|
}
|
|
|
|
const renderPosts = rows => {
|
|
const box = el('fetch-results')
|
|
box.innerHTML = ''
|
|
for (const r of rows) {
|
|
const card = document.createElement('div')
|
|
card.className = 'card'
|
|
const meta = document.createElement('div')
|
|
meta.className = 'meta'
|
|
meta.textContent = `${r.screen_name || ''} · ${r.created_at || ''}`
|
|
const text = document.createElement('div')
|
|
text.innerHTML = highlight(r.text || '', state.terms)
|
|
const link = document.createElement('a')
|
|
link.href = (r.retweeted_url || r.url || '#')
|
|
link.target = '_blank'
|
|
link.textContent = '打开原文'
|
|
if (r.retweeted_text) {
|
|
const rt = document.createElement('div')
|
|
rt.className = 'meta'
|
|
rt.innerHTML = `↪ 原博 ${r.retweeted_screen_name || ''}: ` + highlight(r.retweeted_text || '', state.terms)
|
|
card.appendChild(rt)
|
|
}
|
|
const stat = document.createElement('div')
|
|
stat.className = 'meta'
|
|
stat.textContent = `转发:${r.reposts_count} 评论:${r.comments_count} 赞:${r.attitudes_count}`
|
|
card.appendChild(meta)
|
|
card.appendChild(text)
|
|
card.appendChild(link)
|
|
card.appendChild(stat)
|
|
box.appendChild(card)
|
|
}
|
|
}
|
|
|
|
const setResultsMeta = text => {
|
|
const m = document.getElementById('results-meta')
|
|
if (m) m.textContent = String(text || '')
|
|
}
|
|
|
|
const fetchLatest = async () => {
|
|
const uid = el('input-uid').value.trim()
|
|
logLocal(`frontend.event {"source":"weibo","action":"fetch.click","uid":"${uid}"}`)
|
|
try { await fetch('/api/debug/event', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source: 'weibo', action: 'fetch.click', uid }) }) } catch {}
|
|
wbLoad()
|
|
const statusBox = document.getElementById('status')
|
|
if (statusBox) { statusBox.style.display = ''; statusBox.textContent = `正在拉取最近${state.count}条...` }
|
|
const res = await fetch(`/api/weibo/${uid}/fetch?count=${encodeURIComponent(state.count || 50)}`)
|
|
const data = await res.json()
|
|
if (!data.ok) {
|
|
logLocal(`weibo.fetch.error ${JSON.stringify({ error: data.error })}`)
|
|
if (statusBox) statusBox.textContent = (data.hint ? data.hint + '。' : '') + (data.error || '拉取失败')
|
|
alert(`拉取失败:${String(data.error || 'unknown')}`)
|
|
} else {
|
|
logLocal(`weibo.fetch.done ${JSON.stringify({ uid, fetched: data.fetched })}`)
|
|
if (statusBox) { statusBox.style.display = 'none'; statusBox.textContent = '' }
|
|
alert(`本次拉取:${data.fetched || 0} 条`)
|
|
const latestBox = document.getElementById('latest-data-time'); if (latestBox) latestBox.value = String(data.latest_time || '')
|
|
const sourceMap = { devtools: 'DevTools', settings: '设置Cookie', anonymous: '匿名Cookie', '': '未知' }
|
|
const srcLabel = sourceMap[String(data.source || '')] || '未知'
|
|
const rows = Array.isArray(data.rows) ? data.rows : []
|
|
renderPosts(rows)
|
|
setResultsMeta(`数据来源:${srcLabel} · 仅显示本次拉取结果 · 当前 ${rows.length} 条`)
|
|
}
|
|
wbLoad()
|
|
return data
|
|
}
|
|
|
|
const renderAll = async (q = '', mode = 'any') => {
|
|
const uid = el('input-uid').value.trim()
|
|
state.mode = mode
|
|
state.query = q
|
|
state.terms = q ? q.split(/\s+/).filter(Boolean) : []
|
|
logLocal(`frontend.event ${JSON.stringify({ source:'weibo', action:'kw.search', uid, mode, terms: state.terms })}`)
|
|
const res = await fetch(`/api/weibo/search?uid=${encodeURIComponent(uid)}&q=${encodeURIComponent(q)}&mode=${encodeURIComponent(mode)}&limit=50`)
|
|
const data = await res.json()
|
|
if (data.ok) { renderPosts(data.rows); logLocal(`frontend.event ${JSON.stringify({ source:'weibo', action:'kw.search.done', count: (data.rows||[]).length })}`); return data.rows || [] }
|
|
return []
|
|
}
|
|
|
|
const matches = (text, terms, mode) => {
|
|
if (!terms || terms.length === 0) return true
|
|
const checks = terms.map(t => new RegExp(t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(text))
|
|
return mode === 'all' ? checks.every(Boolean) : checks.some(Boolean)
|
|
}
|
|
const loadArchiveToPanel = async () => {
|
|
const uid = el('input-uid').value.trim()
|
|
const start = el('arch-week-start').value
|
|
const end = el('arch-week-end').value
|
|
const res = await fetch(`/api/archives/get?uid=${encodeURIComponent(uid)}&start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`)
|
|
const data = await res.json()
|
|
if (data.ok) el('archive-results').innerHTML = data.html || ''
|
|
}
|
|
const searchArchive = async () => {
|
|
const q = el('arch-query').value.trim()
|
|
const mode = el('arch-mode').value
|
|
const terms = q ? q.split(/\s+/).filter(Boolean) : []
|
|
const cards = Array.from(document.querySelectorAll('#archive-results .card'))
|
|
if (cards.length === 0) return
|
|
cards.forEach(card => {
|
|
const text = card.textContent || ''
|
|
const show = matches(text, terms, mode)
|
|
card.style.display = show ? '' : 'none'
|
|
if (show) card.innerHTML = highlight(card.innerHTML, terms)
|
|
})
|
|
}
|
|
|
|
const openSettings = () => { el('settings-modal').classList.remove('hidden') }
|
|
const closeSettings = () => {
|
|
const closeBtn = el('btn-close-settings')
|
|
if (closeBtn) {
|
|
closeBtn.classList.add('warning')
|
|
setTimeout(() => {
|
|
closeBtn.classList.remove('warning')
|
|
el('settings-modal').classList.add('hidden')
|
|
}, 300)
|
|
} else {
|
|
el('settings-modal').classList.add('hidden')
|
|
}
|
|
}
|
|
|
|
const saveSettings = async () => {
|
|
const saveBtn = el('btn-save-settings')
|
|
|
|
// 添加保存动画效果
|
|
if (saveBtn) {
|
|
saveBtn.classList.add('saving')
|
|
saveBtn.disabled = true
|
|
saveBtn.textContent = '保存中...'
|
|
}
|
|
|
|
const uid = el('settings-uid').value.trim()
|
|
const countVal = parseInt((document.getElementById('settings-count').value || '50'), 10)
|
|
const kwRaw = el('settings-keywords').value.trim()
|
|
const keywords = kwRaw.split('|').map(s => s.trim()).filter(Boolean)
|
|
const body = { users: [{ uid, screen_name: '' }], keywords, headers: {}, count: countVal }
|
|
|
|
logLocal(`frontend.event ${JSON.stringify({ source: 'weibo', action: 'settings.save', uid, keywordsCount: keywords.length })}`)
|
|
|
|
try {
|
|
await fetch('/api/debug/event', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ source: 'weibo', action: 'settings.save', uid, keywordsCount: keywords.length })
|
|
})
|
|
} catch {}
|
|
|
|
await fetch('/api/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
})
|
|
|
|
// 恢复按钮状态
|
|
if (saveBtn) {
|
|
setTimeout(() => {
|
|
saveBtn.classList.remove('saving')
|
|
saveBtn.disabled = false
|
|
saveBtn.textContent = '保存'
|
|
|
|
// 添加成功确认效果
|
|
saveBtn.style.background = 'linear-gradient(135deg, #00ff00, #00cc00)'
|
|
setTimeout(() => {
|
|
saveBtn.style.background = 'linear-gradient(135deg, var(--primary), var(--secondary))'
|
|
closeSettings()
|
|
}, 500)
|
|
}, 1000)
|
|
} else {
|
|
closeSettings()
|
|
}
|
|
|
|
wbLoad()
|
|
state.count = countVal
|
|
const b = document.getElementById('btn-fetch')
|
|
if (b) b.textContent = `拉取最近${state.count}条`
|
|
}
|
|
|
|
const loadSettings = async () => {
|
|
try {
|
|
const res = await fetch('/api/settings')
|
|
const data = await res.json()
|
|
const cfg = (data && data.file) || {}
|
|
const uid = (cfg.users && cfg.users[0] && cfg.users[0].uid) ? String(cfg.users[0].uid) : (state.uid || '')
|
|
const count = parseInt(String(cfg.count || state.count), 10)
|
|
const kw = Array.isArray(cfg.keywords) ? cfg.keywords.join('|') : ''
|
|
const iu = document.getElementById('input-uid'); if (iu) iu.value = uid
|
|
const su = document.getElementById('settings-uid'); if (su) su.value = uid
|
|
const sc = document.getElementById('settings-count'); if (sc) sc.value = String(count)
|
|
const sk = document.getElementById('settings-keywords'); if (sk) sk.value = kw
|
|
|
|
state.uid = uid
|
|
state.count = count
|
|
const b = document.getElementById('btn-fetch'); if (b) b.textContent = `拉取最近${state.count}条`
|
|
renderKeywordChips(Array.isArray(cfg.keywords) ? cfg.keywords : [])
|
|
} catch {}
|
|
}
|
|
|
|
const truncateLabel = (s, n = 8) => {
|
|
const str = String(s || '')
|
|
return str.length > n ? (str.slice(0, n) + '...') : str
|
|
}
|
|
|
|
const renderKeywordChips = (keywords) => {
|
|
try {
|
|
const panelArchive = document.getElementById('panel-archive')
|
|
let archHolder = document.getElementById('arch-chips')
|
|
if (!archHolder && panelArchive) { archHolder = document.createElement('div'); archHolder.id = 'arch-chips'; archHolder.className = 'chips'; panelArchive.insertBefore(archHolder, panelArchive.firstChild) }
|
|
if (archHolder) archHolder.innerHTML = ''
|
|
const groups = (keywords || []).map(k => String(k || '').trim()).filter(Boolean)
|
|
for (const g of groups) {
|
|
const btn = document.createElement('button'); btn.className = 'chip'; btn.dataset.q = g; btn.textContent = truncateLabel(g, 8)
|
|
btn.style.margin = '0 6px 6px 0'
|
|
btn.addEventListener('click', () => { const iq = document.getElementById('arch-query'); if (iq) iq.value = g; searchArchive() })
|
|
if (archHolder) archHolder.appendChild(btn)
|
|
}
|
|
// ensure no stray chips remain in fetch panel
|
|
Array.from(document.querySelectorAll('#panel-fetch .chip')).forEach(ch => ch.remove())
|
|
} catch {}
|
|
}
|
|
|
|
// 创建粒子效果
|
|
function createParticles() {
|
|
const container = document.getElementById('particles-container')
|
|
if (!container) return
|
|
|
|
for (let i = 0; i < 30; i++) {
|
|
const particle = document.createElement('div')
|
|
particle.className = 'particle'
|
|
particle.style.left = Math.random() * 100 + '%'
|
|
particle.style.animationDelay = Math.random() * 6 + 's'
|
|
particle.style.animationDuration = (Math.random() * 3 + 4) + 's'
|
|
container.appendChild(particle)
|
|
}
|
|
}
|
|
|
|
// 添加霓虹灯效果到输入框
|
|
function addNeonEffects() {
|
|
const inputs = document.querySelectorAll('input[type="text"], input[type="date"], select')
|
|
inputs.forEach(input => {
|
|
input.classList.add('neon-input')
|
|
|
|
// 添加焦点效果
|
|
input.addEventListener('focus', function() {
|
|
this.style.boxShadow = '0 0 20px rgba(0, 255, 255, 0.5), 0 0 40px rgba(0, 255, 255, 0.3)'
|
|
})
|
|
|
|
input.addEventListener('blur', function() {
|
|
this.style.boxShadow = '0 0 10px rgba(255, 20, 147, 0.3)'
|
|
})
|
|
})
|
|
}
|
|
|
|
// 添加按钮特殊效果
|
|
function enhanceButtons() {
|
|
const specialButtons = ['btn-fetch', 'btn-save-week', 'arch-search']
|
|
specialButtons.forEach(id => {
|
|
const btn = document.getElementById(id)
|
|
if (btn) {
|
|
btn.classList.add('btn-special')
|
|
}
|
|
})
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
try {
|
|
// 初始化视觉效果
|
|
createParticles()
|
|
addNeonEffects()
|
|
enhanceButtons()
|
|
|
|
logLocal('frontend.event {"source":"weibo","action":"dom.ready"}')
|
|
const bf = el('btn-fetch'); if (bf) bf.addEventListener('click', async () => { await fetchLatest() })
|
|
const bs = el('btn-search'); if (bs) bs.addEventListener('click', searchPosts)
|
|
const bset = el('btn-settings'); if (bset) bset.addEventListener('click', openSettings)
|
|
const bclose = el('btn-close-settings'); if (bclose) bclose.addEventListener('click', closeSettings)
|
|
const bsave = el('btn-save-settings'); if (bsave) bsave.addEventListener('click', saveSettings)
|
|
|
|
const dStart = document.getElementById('week-start')
|
|
const dEnd = document.getElementById('week-end')
|
|
const btnSaveWeek = document.getElementById('btn-save-week')
|
|
const btnWeekPrev = document.getElementById('btn-week-prev')
|
|
const btnWeekCurrent = document.getElementById('btn-week-current')
|
|
const btnWeekNext = document.getElementById('btn-week-next')
|
|
|
|
const fmt = d => {
|
|
const y = d.getFullYear()
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${day}`
|
|
}
|
|
const weekRangeFromDate = anchor => {
|
|
const d = new Date(anchor)
|
|
const dow = d.getDay()
|
|
const shift = dow === 0 ? -6 : 1 - dow
|
|
const start = new Date(d.getFullYear(), d.getMonth(), d.getDate() + shift)
|
|
const end = new Date(start.getFullYear(), start.getMonth(), start.getDate() + 6)
|
|
return { start, end }
|
|
}
|
|
const setWeek = anchor => {
|
|
const r = weekRangeFromDate(anchor)
|
|
dStart.value = fmt(r.start)
|
|
dEnd.value = fmt(r.end)
|
|
}
|
|
const saveArchive = async () => {
|
|
const uid = el('input-uid').value.trim()
|
|
const content = el('fetch-results').innerHTML
|
|
const header = `<div class="meta">封存范围:${dStart.value} 至 ${dEnd.value} · UID ${uid}</div>`
|
|
const html = `<article class="archive">${header}${content}</article>`
|
|
await fetch('/api/archives/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uid, start: dStart.value, end: dEnd.value, html }) })
|
|
logLocal(`frontend.event ${JSON.stringify({ source: 'weibo', action: 'archive.save', uid, start: dStart.value, end: dEnd.value, html_len: html.length })}`)
|
|
}
|
|
if (btnSaveWeek) btnSaveWeek.addEventListener('click', saveArchive)
|
|
if (btnWeekPrev) btnWeekPrev.addEventListener('click', () => { const s = new Date(dStart.value); s.setDate(s.getDate() - 7); setWeek(s) })
|
|
if (btnWeekCurrent) btnWeekCurrent.addEventListener('click', () => { setWeek(new Date()) })
|
|
if (btnWeekNext) btnWeekNext.addEventListener('click', () => { const s = new Date(dStart.value); s.setDate(s.getDate() + 7); setWeek(s) })
|
|
setWeek(new Date())
|
|
// Archive panel controls
|
|
const archStart = document.getElementById('arch-week-start')
|
|
const archEnd = document.getElementById('arch-week-end')
|
|
const archPrev = document.getElementById('arch-week-prev')
|
|
const archCurr = document.getElementById('arch-week-current')
|
|
const archNext = document.getElementById('arch-week-next')
|
|
const setArchWeek = anchor => { const r = weekRangeFromDate(anchor); archStart.value = fmt(r.start); archEnd.value = fmt(r.end) }
|
|
if (archPrev) archPrev.addEventListener('click', async () => { const s = new Date(archStart.value); s.setDate(s.getDate() - 7); setArchWeek(s); await loadArchiveToPanel() })
|
|
if (archCurr) archCurr.addEventListener('click', async () => { setArchWeek(new Date()); await loadArchiveToPanel() })
|
|
if (archNext) archNext.addEventListener('click', async () => { const s = new Date(archStart.value); s.setDate(s.getDate() + 7); setArchWeek(s); await loadArchiveToPanel() })
|
|
const archSearchBtn = document.getElementById('arch-search')
|
|
if (archSearchBtn) archSearchBtn.addEventListener('click', searchArchive)
|
|
const archReset = document.getElementById('arch-reset')
|
|
if (archReset) archReset.addEventListener('click', async () => { el('arch-query').value = ''; const m=document.getElementById('arch-mode'); if (m) m.value='any'; await loadArchiveToPanel() })
|
|
document.querySelectorAll('#panel-archive .chip').forEach(b => { b.addEventListener('click', () => { el('arch-query').value = b.dataset.q || ''; searchArchive() }) })
|
|
// Subtabs
|
|
const tabFetch = document.getElementById('tab-fetch')
|
|
const tabArchive = document.getElementById('tab-archive')
|
|
const tabPreview = document.getElementById('tab-preview')
|
|
const panelFetch = document.getElementById('panel-fetch')
|
|
const panelArchive = document.getElementById('panel-archive')
|
|
const panelPreview = document.getElementById('panel-preview')
|
|
const previewUrlInput = document.getElementById('preview-url')
|
|
const previewOpen = document.getElementById('preview-open')
|
|
const previewFrame = document.getElementById('preview-frame')
|
|
|
|
const computePreviewUrl = () => {
|
|
const uid = el('input-uid').value.trim()
|
|
return `https://m.weibo.cn/u/${uid}`
|
|
}
|
|
const loadPreview = async () => {
|
|
const url = (previewUrlInput && previewUrlInput.value.trim()) || computePreviewUrl()
|
|
if (previewUrlInput) previewUrlInput.value = url
|
|
if (previewOpen) previewOpen.href = url
|
|
if (previewFrame) previewFrame.src = url
|
|
logLocal(`frontend.event ${JSON.stringify({ source:'weibo', action:'preview.iframe', url })}`)
|
|
}
|
|
const activate = async (name) => {
|
|
if (name === 'fetch') {
|
|
panelFetch.style.display = ''
|
|
panelArchive.style.display = 'none'
|
|
panelPreview.style.display = 'none'
|
|
tabFetch.classList.add('active'); tabArchive.classList.remove('active'); if (tabPreview) tabPreview.classList.remove('active')
|
|
} else if (name === 'archive') {
|
|
panelFetch.style.display = 'none'
|
|
panelArchive.style.display = ''
|
|
panelPreview.style.display = 'none'
|
|
tabArchive.classList.add('active'); tabFetch.classList.remove('active'); if (tabPreview) tabPreview.classList.remove('active')
|
|
} else if (name === 'preview') {
|
|
panelFetch.style.display = 'none'
|
|
panelArchive.style.display = 'none'
|
|
panelPreview.style.display = ''
|
|
if (tabPreview) tabPreview.classList.add('active'); tabFetch.classList.remove('active'); tabArchive.classList.remove('active')
|
|
await loadSettings()
|
|
if (previewUrlInput) previewUrlInput.value = computePreviewUrl()
|
|
if (previewOpen) previewOpen.href = computePreviewUrl()
|
|
await loadPreview()
|
|
}
|
|
}
|
|
if (tabFetch) tabFetch.addEventListener('click', async () => { await activate('fetch'); clearFetchCache(); await checkDevtools() })
|
|
if (tabArchive) tabArchive.addEventListener('click', () => activate('archive'))
|
|
if (tabPreview) tabPreview.addEventListener('click', () => activate('preview'))
|
|
if (previewUrlInput) previewUrlInput.addEventListener('change', loadPreview)
|
|
if (previewUrlInput) previewUrlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') loadPreview() })
|
|
if (wbBtnCopy) wbBtnCopy.addEventListener('click', async () => { try { await navigator.clipboard.writeText(wbOut.textContent || '') } catch {} })
|
|
if (wbBtnRefresh) wbBtnRefresh.addEventListener('click', wbLoad)
|
|
if (wbBtnClear) wbBtnClear.addEventListener('click', async () => { try { await fetch('/api/debug/clear?channel=weibo', { method: 'POST' }) } catch {}; wbLoad() })
|
|
setInterval(wbLoad, 5000)
|
|
wbLoad()
|
|
await loadSettings()
|
|
setWeek(new Date())
|
|
setArchWeek(new Date())
|
|
const clearFetchCache = () => {
|
|
const box = el('fetch-results'); if (box) box.innerHTML = ''
|
|
alert('缓存已清理,当前无数据状态')
|
|
logLocal('frontend.event {"source":"weibo","action":"cache.cleared"}')
|
|
}
|
|
const checkDevtools = async () => {
|
|
try {
|
|
const ds = await fetch('/api/weibo/devtools/status')
|
|
const dj = await ds.json()
|
|
if (dj && dj.ok && dj.connected) alert('已对接服务器 Chrome DevTools')
|
|
else alert('未对接服务器 Chrome DevTools,请在服务器Chrome登录微博并开启9222端口')
|
|
logLocal(`frontend.event ${JSON.stringify({ source:'weibo', action:'devtools.status', ok: !!(dj && dj.ok), connected: !!(dj && dj.connected) })}`)
|
|
} catch (e) {
|
|
alert('未能获取 DevTools 状态')
|
|
logLocal(`frontend.event ${JSON.stringify({ source:'weibo', action:'devtools.error', error: String(e.message||e) })}`)
|
|
}
|
|
}
|
|
const isFetchActive = tabFetch && tabFetch.classList.contains('active')
|
|
if (isFetchActive) {
|
|
clearFetchCache()
|
|
await checkDevtools()
|
|
}
|
|
} catch (e) {
|
|
logLocal(`frontend.error ${String(e && e.message ? e.message : e)}`)
|
|
}
|
|
})
|