| <!DOCTYPE html> |
| <html lang="zh"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>data fetch</title> |
|
|
| <style> |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial; |
| background: linear-gradient(135deg, #eef2f7, #f8fafc); |
| margin: 0; |
| padding: 30px; |
| } |
| |
| |
| |
| .title { |
| text-align: center; |
| margin-bottom: 10px; |
| } |
| |
| |
| |
| .header { |
| position: relative; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| margin-bottom: 18px; |
| min-height: 44px; |
| } |
| |
| .btn-group { |
| display: flex; |
| gap: 12px; |
| } |
| |
| .info-group { |
| position: absolute; |
| right: 0; |
| display: flex; |
| gap: 10px; |
| align-items: center; |
| } |
| |
| button { |
| padding: 10px 18px; |
| border-radius: 10px; |
| border: none; |
| cursor: pointer; |
| font-size: 14px; |
| transition: 0.2s; |
| } |
| |
| .primary { |
| background: #1677ff; |
| color: white; |
| } |
| |
| .secondary { |
| background: #e5e7eb; |
| } |
| |
| .auto-on { |
| background: #16a34a; |
| color: white; |
| box-shadow: 0 0 0 3px rgba(22,163,74,0.15); |
| } |
| |
| button:disabled { |
| opacity: 0.6; |
| cursor: not-allowed; |
| } |
| |
| #lastTime { |
| font-size: 13px; |
| background: #fff; |
| padding: 6px 10px; |
| border-radius: 999px; |
| box-shadow: 0 2px 6px rgba(0,0,0,0.06); |
| } |
| |
| #status { |
| font-size: 13px; |
| color: #666; |
| } |
| |
| |
| |
| .grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
| gap: 18px; |
| } |
| |
| .card { |
| background: white; |
| border-radius: 16px; |
| padding: 16px; |
| box-shadow: 0 6px 18px rgba(0,0,0,0.08); |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| |
| .time { |
| font-size: 12px; |
| color: #888; |
| } |
| |
| .text { |
| font-size: 14px; |
| line-height: 1.6; |
| word-break: break-all; |
| } |
| |
| .badges { |
| display: flex; |
| gap: 6px; |
| flex-wrap: wrap; |
| } |
| |
| .badge { |
| font-size: 11px; |
| padding: 2px 8px; |
| border-radius: 999px; |
| font-weight: 600; |
| } |
| |
| .badge.new { |
| transition: opacity 0.4s; |
| } |
| |
| .new { |
| background: #dcfce7; |
| color: #166534; |
| } |
| |
| .high { |
| background: #fee2e2; |
| color: #991b1b; |
| } |
| |
| .copy-btn { |
| align-self: flex-end; |
| background: #f3f4f6; |
| font-size: 12px; |
| padding: 6px 10px; |
| } |
| |
| .copy-btn:hover { |
| background: #e5e7eb; |
| } |
| |
| |
| |
| .toast { |
| position: fixed; |
| bottom: 30px; |
| left: 50%; |
| transform: translateX(-50%); |
| background: #111; |
| color: white; |
| padding: 10px 18px; |
| border-radius: 999px; |
| font-size: 13px; |
| opacity: 0; |
| transition: 0.25s; |
| pointer-events: none; |
| } |
| |
| .toast.show { |
| opacity: 1; |
| } |
| </style> |
| </head> |
|
|
| <body> |
|
|
| <h2 class="title">📊 Data Fetch</h2> |
|
|
| <div class="header"> |
| <div class="btn-group"> |
| <button id="loadBtn" class="primary" onclick="manualLoad()">拉取最新列表</button> |
| <button id="autoBtn" class="auto-on" onclick="toggleAuto()">自动拉取中</button> |
| </div> |
|
|
| <div class="info-group"> |
| <div id="lastTime">最近拉取:--</div> |
| <div id="status">自动模式</div> |
| </div> |
| </div> |
|
|
| <div id="grid" class="grid"></div> |
|
|
| <div id="toast" class="toast">复制成功</div> |
|
|
| <script> |
| let cooldown = false; |
| let autoMode = true; |
| let autoTimer = null; |
| let seenSet = new Set(); |
| |
| |
| |
| function extractPrice(text) { |
| const m = text.match(/今天(\d+)块/); |
| return m ? parseInt(m[1]) : 0; |
| } |
| |
| function showToast(msg="复制成功") { |
| const t = document.getElementById("toast"); |
| t.innerText = msg; |
| t.classList.add("show"); |
| setTimeout(()=>t.classList.remove("show"), 1200); |
| } |
| |
| |
| |
| function setCooldown(btn) { |
| cooldown = true; |
| let t = 5; |
| btn.disabled = true; |
| btn.innerText = `冷却 ${t}s`; |
| |
| const timer = setInterval(() => { |
| t--; |
| btn.innerText = `冷却 ${t}s`; |
| if (t <= 0) { |
| clearInterval(timer); |
| btn.disabled = false; |
| btn.innerText = "拉取最新列表"; |
| cooldown = false; |
| } |
| }, 1000); |
| } |
| |
| |
| |
| async function loadData() { |
| const status = document.getElementById("status"); |
| const last = document.getElementById("lastTime"); |
| |
| status.innerText = autoMode ? "自动拉取中…" : "加载中…"; |
| |
| try { |
| const res = await fetch("/api/data"); |
| const data = await res.json(); |
| |
| |
| render(data.posts); |
| |
| last.innerText = "最近拉取:" + new Date().toLocaleTimeString(); |
| status.innerText = autoMode ? "自动模式" : "完成"; |
| } catch (e) { |
| status.innerText = "加载失败"; |
| } |
| } |
| |
| function clearNewBadges() { |
| document.querySelectorAll(".badge.new").forEach(b => b.remove()); |
| } |
| |
| |
| |
| function render(posts) { |
| const grid = document.getElementById("grid"); |
| |
| |
| posts.slice().reverse().forEach(p => { |
| const key = p.created_at + p.text; |
| if (seenSet.has(key)) return; |
| seenSet.add(key); |
| |
| const price = extractPrice(p.text); |
| |
| const badges = []; |
| badges.push(`<span class="badge new">NEW</span>`); |
| if (price > 850) badges.push(`<span class="badge high">高价</span>`); |
| |
| const card = document.createElement("div"); |
| card.className = "card"; |
| card.dataset.insertTime = Date.now(); |
| |
| card.innerHTML = ` |
| <div class="time">${p.created_at}</div> |
| <div class="badges">${badges.join("")}</div> |
| <div class="text">${p.text}</div> |
| <button class="copy-btn" onclick='copyText(${JSON.stringify(p.text)})'>复制</button> |
| `; |
| |
| grid.prepend(card); |
| }); |
| } |
| |
| |
| |
| function copyText(t) { |
| navigator.clipboard.writeText(t); |
| showToast(); |
| } |
| |
| |
| |
| function manualLoad() { |
| if (cooldown) return; |
| const btn = document.getElementById("loadBtn"); |
| setCooldown(btn); |
| loadData(); |
| } |
| |
| |
| |
| function toggleAuto() { |
| autoMode = !autoMode; |
| const btn = document.getElementById("autoBtn"); |
| |
| if (autoMode) { |
| btn.innerText = "自动拉取中"; |
| btn.classList.add("auto-on"); |
| loadData(); |
| autoTimer = setInterval(loadData, 5000); |
| } else { |
| btn.innerText = "开启自动拉取"; |
| btn.classList.remove("auto-on"); |
| clearInterval(autoTimer); |
| } |
| } |
| |
| |
| |
| loadData(); |
| autoTimer = setInterval(loadData, 5000); |
| |
| const NEW_KEEP_SECONDS = 15; |
| setInterval(() => { |
| const now = Date.now(); |
| |
| document.querySelectorAll(".card").forEach(card => { |
| const t = parseInt(card.dataset.insertTime || 0); |
| if (!t) return; |
| |
| if (now - t > NEW_KEEP_SECONDS * 1000) { |
| const badge = card.querySelector(".badge.new"); |
| if (badge) { |
| badge.style.opacity = 0; |
| setTimeout(()=>badge.remove(), 400); |
| } |
| } |
| }); |
| |
| }, 1000); |
| </script> |
|
|
| </body> |
| </html> |
|
|