Files

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)}`)
}
})