chore: 初始提交 - 移除所有硬编码凭据,统一从环境变量读取
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"_comment": "微博工具的前端鉴权配置",
|
||||
"enable_auth_comment": "true:启用鉴权(需要从导航登录);false:完全放开访问微博工具",
|
||||
"enable_auth": true,
|
||||
"max_age_days_comment": "鉴权 cookie 的最大有效天数(1~365)",
|
||||
"max_age_days": 30
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Yang's Toolbox · 微博跟踪</title>
|
||||
<link rel="icon" href="/icons/weibo.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/weibo/weibo.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="subtabs">
|
||||
<button id="tab-fetch" class="subtab">拉取的内容(暂停)</button>
|
||||
<button id="tab-archive" class="subtab">封存的内容</button>
|
||||
<button id="tab-preview" class="subtab active">原版的网页</button>
|
||||
</div>
|
||||
<section class="debug" id="debug-section" style="display:none">
|
||||
<div class="debug-actions">
|
||||
<button id="wb-btn-copy" class="btn">复制调试信息</button>
|
||||
<button id="wb-btn-refresh" class="btn">刷新</button>
|
||||
<button id="wb-btn-clear" class="btn">清空</button>
|
||||
</div>
|
||||
<div id="wb-debug-output" class="debug-output"></div>
|
||||
</section>
|
||||
<section id="panel-fetch" class="panel" style="display:none">
|
||||
<div class="compact">
|
||||
<input id="input-uid" type="text" placeholder="用户UID" value="7716940453" />
|
||||
<button id="btn-fetch" class="btn">拉取最近50条</button>
|
||||
<input id="latest-data-time" type="text" placeholder="最新数据时间(拉取后显示)" readonly />
|
||||
<input id="week-start" type="date" />
|
||||
<input id="week-end" type="date" />
|
||||
<button id="btn-save-week" class="btn">保存至库</button>
|
||||
<button id="btn-week-prev" class="btn">上周</button>
|
||||
<button id="btn-week-current" class="btn">本周</button>
|
||||
<button id="btn-week-next" class="btn">下周</button>
|
||||
<button id="btn-settings" class="btn">设置</button>
|
||||
</div>
|
||||
<div id="status" class="status" style="display:none"></div>
|
||||
<div id="results-meta" class="meta"></div>
|
||||
<div id="fetch-results" class="results"></div>
|
||||
</section>
|
||||
|
||||
<section id="panel-archive" class="panel" style="display:none">
|
||||
<div class="compact">
|
||||
<input id="arch-query" type="text" placeholder="搜索关键词" />
|
||||
<select id="arch-mode">
|
||||
<option value="any">任意匹配</option>
|
||||
<option value="all">同时包含</option>
|
||||
</select>
|
||||
<button id="arch-search" class="btn">搜索</button>
|
||||
<button id="arch-reset" class="btn">复位</button>
|
||||
<div class="preset">
|
||||
<input id="arch-week-start" type="date" />
|
||||
<input id="arch-week-end" type="date" />
|
||||
<button id="arch-week-prev" class="btn">上周</button>
|
||||
<button id="arch-week-current" class="btn">本周</button>
|
||||
<button id="arch-week-next" class="btn">下周</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="archive-results" class="results"></div>
|
||||
</section>
|
||||
|
||||
<section id="panel-preview" class="panel" style="">
|
||||
<div class="compact">
|
||||
<input id="preview-url" type="text" placeholder="原版地址" />
|
||||
<a id="preview-open" class="btn" target="_blank">新窗口打开原版</a>
|
||||
</div>
|
||||
<div id="preview-box" class="results" style="margin-top:8px">
|
||||
<iframe id="preview-frame" style="width:100%; height:80vh; border:none;"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="settings-modal" class="modal hidden">
|
||||
<div class="modal-body">
|
||||
<h3>跟踪设置</h3>
|
||||
<label>用户UID</label>
|
||||
<input id="settings-uid" type="text" value="7716940453" />
|
||||
<label>抽取条数</label>
|
||||
<input id="settings-count" type="number" min="1" max="200" value="50" />
|
||||
<label>关键词(空格分隔表示同时包含)</label>
|
||||
<input id="settings-keywords" type="text" value="上海|上海 无料" />
|
||||
|
||||
<div class="modal-actions">
|
||||
<button id="btn-save-settings">保存</button>
|
||||
<button id="btn-close-settings">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/common/common.js"></script>
|
||||
<script src="/tools/weibo/weibo.js"></script>
|
||||
<script>
|
||||
(async function(){
|
||||
try {
|
||||
const res = await fetch('/api/flags')
|
||||
const data = await res.json()
|
||||
if (data && data.debug && data.debug.weibo) {
|
||||
const sec = document.getElementById('debug-section')
|
||||
if (sec) sec.style.display = ''
|
||||
}
|
||||
} catch {}
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
<%@ WebHandler Language="C#" Class="SaveArchive" %>
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
|
||||
public class SaveArchive : IHttpHandler
|
||||
{
|
||||
public bool IsReusable { get { return true; } }
|
||||
public void ProcessRequest(HttpContext context)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
try
|
||||
{
|
||||
string body;
|
||||
using (var reader = new StreamReader(context.Request.InputStream, Encoding.UTF8)) body = reader.ReadToEnd();
|
||||
var url = ResolveNodeUrl();
|
||||
var req = (HttpWebRequest)WebRequest.Create(url);
|
||||
req.Method = "POST";
|
||||
req.ContentType = "application/json";
|
||||
var data = Encoding.UTF8.GetBytes(body);
|
||||
using (var s = req.GetRequestStream()) s.Write(data, 0, data.Length);
|
||||
using (var resp = (HttpWebResponse)req.GetResponse())
|
||||
using (var rs = resp.GetResponseStream())
|
||||
using (var r = new StreamReader(rs, Encoding.UTF8))
|
||||
{
|
||||
var txt = r.ReadToEnd();
|
||||
context.Response.Write(txt);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
var msg = "{\"ok\":false,\"error\":\"" + Escape(e.Message) + "\"}";
|
||||
context.Response.Write(msg);
|
||||
}
|
||||
}
|
||||
string ResolveNodeUrl()
|
||||
{
|
||||
var ports = new int[] { 8976, 5050 };
|
||||
foreach (var p in ports)
|
||||
{
|
||||
var u = "http://localhost:" + p + "/api/archives/save";
|
||||
if (Probe(u)) return u;
|
||||
}
|
||||
return "http://localhost:5050/api/archives/save";
|
||||
}
|
||||
bool Probe(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = (HttpWebRequest)WebRequest.Create(url.Replace("/api/archives/save", "/api/ping"));
|
||||
req.Method = "GET";
|
||||
req.Timeout = 1500;
|
||||
using (var resp = (HttpWebResponse)req.GetResponse()) return resp.StatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
string Escape(string s)
|
||||
{
|
||||
return s.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
/* 微博地下偶像追踪工具 - 极致骚包设计 */
|
||||
:root {
|
||||
--primary: #ff1493; /* 更鲜艳的深粉色 - 地下偶像主色调 */
|
||||
--primary-light: #ff69b4;
|
||||
--secondary: #8a2be2; /* 更鲜艳的紫色 */
|
||||
--accent: #00ffff; /* 青色 - 霓虹灯效果 */
|
||||
--neon: #ff00ff; /* 霓虹粉色 */
|
||||
--surface: #0a0a0a;
|
||||
--surface-alt: #1a1a1a;
|
||||
--border: #333333;
|
||||
--text: #ffffff;
|
||||
--text-light: #cccccc;
|
||||
--shadow: 0 4px 20px rgba(255, 20, 147, 0.3);
|
||||
--shadow-lg: 0 10px 30px rgba(255, 20, 147, 0.5);
|
||||
--radius: 16px;
|
||||
--radius-sm: 10px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--glow: 0 0 20px rgba(255, 20, 147, 0.5);
|
||||
}
|
||||
|
||||
/* 霓虹灯文字效果 */
|
||||
.neon-text {
|
||||
color: var(--primary);
|
||||
text-shadow: 0 0 10px var(--primary), 0 0 20px var(--primary), 0 0 30px var(--primary);
|
||||
animation: neon-pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes neon-pulse {
|
||||
from { text-shadow: 0 0 10px var(--primary), 0 0 20px var(--primary), 0 0 30px var(--primary); }
|
||||
to { text-shadow: 0 0 20px var(--primary), 0 0 30px var(--primary), 0 0 40px var(--primary); }
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, #1a0033 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, #33001a 0%, transparent 50%),
|
||||
linear-gradient(135deg, #0a0014 0%, #1a0033 50%, #33001a 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 星空背景效果 */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image:
|
||||
radial-gradient(white 1px, transparent 1px),
|
||||
radial-gradient(white 1px, transparent 1px);
|
||||
background-size: 50px 50px, 100px 100px;
|
||||
background-position: 0 0, 25px 25px;
|
||||
opacity: 0.1;
|
||||
z-index: -1;
|
||||
animation: twinkle 3s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
from { opacity: 0.05; }
|
||||
to { opacity: 0.15; }
|
||||
}
|
||||
|
||||
/* 浮动粒子效果 */
|
||||
.floating-particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: float 6s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(100vh) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(-100px) rotate(360deg); opacity: 0; }
|
||||
}
|
||||
|
||||
/* 头部样式 - 地下偶像舞台效果 */
|
||||
#topbar {
|
||||
background:
|
||||
linear-gradient(135deg, var(--primary) 0%, var(--secondary) 50%, var(--neon) 100%);
|
||||
padding: 0;
|
||||
box-shadow: var(--shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#topbar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
animation: stage-light 3s infinite;
|
||||
}
|
||||
|
||||
@keyframes stage-light {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* 标签页样式 - 地下偶像舞台效果 */
|
||||
.subtabs {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
background: rgba(10, 10, 10, 0.8);
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 20, 147, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subtabs::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, rgba(255, 20, 147, 0.1), rgba(138, 43, 226, 0.1));
|
||||
border-radius: var(--radius);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.subtab {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
color: var(--text-light);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.subtab::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.6s;
|
||||
}
|
||||
|
||||
.subtab::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--primary), var(--accent));
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.subtab:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.subtab:hover::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subtab:hover {
|
||||
color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(255, 20, 147, 0.5);
|
||||
box-shadow: 0 5px 15px rgba(255, 20, 147, 0.3);
|
||||
}
|
||||
|
||||
.subtab.active {
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 0 20px rgba(255, 20, 147, 0.5),
|
||||
0 0 40px rgba(255, 20, 147, 0.3);
|
||||
border: 2px solid var(--accent);
|
||||
animation: tab-glow 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes tab-glow {
|
||||
from { box-shadow: 0 0 20px rgba(255, 20, 147, 0.5), 0 0 40px rgba(255, 20, 147, 0.3); }
|
||||
to { box-shadow: 0 0 30px rgba(255, 20, 147, 0.7), 0 0 60px rgba(255, 20, 147, 0.5); }
|
||||
}
|
||||
|
||||
/* 面板样式 - 舞台灯光效果 */
|
||||
.panel {
|
||||
background: rgba(26, 26, 26, 0.9);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
box-shadow:
|
||||
var(--shadow),
|
||||
0 0 0 1px rgba(255, 20, 147, 0.2);
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid rgba(255, 20, 147, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: conic-gradient(from 0deg, transparent, rgba(255, 20, 147, 0.1), transparent);
|
||||
animation: rotate-bg 10s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes rotate-bg {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 紧凑布局 */
|
||||
.compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.compact input[type="text"],
|
||||
.compact input[type="date"],
|
||||
.compact select {
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.compact input[type="text"]:focus,
|
||||
.compact input[type="date"]:focus,
|
||||
.compact select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(255, 45, 149, 0.1);
|
||||
}
|
||||
|
||||
/* 按钮样式 - 地下偶像舞台按钮 */
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 0 20px rgba(255, 20, 147, 0.3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||
transition: left 0.6s;
|
||||
}
|
||||
|
||||
.btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.8) 0%, transparent 70%);
|
||||
transition: all 0.3s;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn:hover::after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px) scale(1.05);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.2),
|
||||
0 0 30px rgba(255, 20, 147, 0.5),
|
||||
0 0 60px rgba(255, 0, 255, 0.3);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0) scale(0.98);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 0 10px rgba(255, 20, 147, 0.3);
|
||||
}
|
||||
|
||||
/* 特殊按钮效果 */
|
||||
.btn-special {
|
||||
background: linear-gradient(135deg, var(--neon), var(--accent));
|
||||
animation: btn-pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes btn-pulse {
|
||||
from { box-shadow: 0 0 20px rgba(255, 0, 255, 0.5), 0 0 40px rgba(0, 255, 255, 0.3); }
|
||||
to { box-shadow: 0 0 30px rgba(255, 0, 255, 0.7), 0 0 60px rgba(0, 255, 255, 0.5); }
|
||||
}
|
||||
|
||||
/* 预设按钮样式 */
|
||||
.preset {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background: var(--surface-alt);
|
||||
color: var(--text);
|
||||
border: 2px solid var(--border);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 结果区域 */
|
||||
.results {
|
||||
margin-top: 1.5rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* 卡片样式 - 偶像经济风格 */
|
||||
.card {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to bottom, var(--primary), var(--secondary));
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* 状态提示 */
|
||||
.status {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ffd666;
|
||||
background: #fffbe6;
|
||||
color: #a87000;
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 调试区域 */
|
||||
.debug {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-alt);
|
||||
}
|
||||
|
||||
.debug-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.debug-output {
|
||||
height: 240px;
|
||||
overflow: auto;
|
||||
background: #fafafa;
|
||||
padding: 1rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.8rem;
|
||||
white-space: pre-wrap;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
/* 模态框样式 - 地下偶像专属设置面板 */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, rgba(255, 20, 147, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 80%, rgba(138, 43, 226, 0.1) 0%, transparent 50%),
|
||||
linear-gradient(135deg, rgba(10, 10, 10, 0.95) 0%, rgba(26, 26, 26, 0.95) 100%);
|
||||
padding: 2rem;
|
||||
border-radius: var(--radius);
|
||||
box-shadow:
|
||||
var(--shadow-lg),
|
||||
0 0 0 1px rgba(255, 20, 147, 0.3),
|
||||
0 0 50px rgba(255, 0, 255, 0.2);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
border: 2px solid rgba(255, 20, 147, 0.5);
|
||||
backdrop-filter: blur(20px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg,
|
||||
rgba(255, 20, 147, 0.3),
|
||||
rgba(138, 43, 226, 0.3),
|
||||
rgba(0, 255, 255, 0.3),
|
||||
rgba(255, 20, 147, 0.3));
|
||||
border-radius: var(--radius);
|
||||
z-index: -1;
|
||||
animation: modal-border 4s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes modal-border {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
.modal-body h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--primary);
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px rgba(255, 20, 147, 0.5);
|
||||
animation: neon-pulse 2s infinite alternate;
|
||||
}
|
||||
|
||||
.modal-body label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.modal-body input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid rgba(255, 20, 147, 0.5);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 1rem;
|
||||
transition: var(--transition);
|
||||
background: rgba(26, 26, 26, 0.8);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-body input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(0, 255, 255, 0.1),
|
||||
0 0 20px rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* 设置页面按钮专用样式 - 极致骚包效果 */
|
||||
.modal-actions button {
|
||||
padding: 1rem 2rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
min-width: 120px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 保存按钮 - 成功确认效果 */
|
||||
#btn-save-settings {
|
||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
color: white;
|
||||
border: 2px solid transparent;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 0 25px rgba(255, 20, 147, 0.4);
|
||||
}
|
||||
|
||||
#btn-save-settings::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.6s;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#btn-save-settings::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: radial-gradient(circle, rgba(0, 255, 0, 0.3) 0%, transparent 70%);
|
||||
transition: all 0.3s;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#btn-save-settings:hover {
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.2),
|
||||
0 0 40px rgba(255, 20, 147, 0.6),
|
||||
0 0 80px rgba(0, 255, 0, 0.3);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
#btn-save-settings:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
#btn-save-settings:hover::after {
|
||||
width: 150%;
|
||||
height: 150%;
|
||||
}
|
||||
|
||||
#btn-save-settings:active {
|
||||
transform: translateY(-1px) scale(0.98);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 0 15px rgba(255, 20, 147, 0.3);
|
||||
}
|
||||
|
||||
/* 保存按钮成功动画 */
|
||||
#btn-save-settings.saving {
|
||||
animation: save-success 1.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes save-success {
|
||||
0% { transform: scale(1); background: linear-gradient(135deg, var(--primary), var(--secondary)); }
|
||||
25% { transform: scale(1.1); background: linear-gradient(135deg, #00ff00, #00cc00); }
|
||||
50% { transform: scale(1.05); background: linear-gradient(135deg, #00ff00, #00cc00); }
|
||||
75% { transform: scale(1.1); background: linear-gradient(135deg, #00ff00, #00cc00); }
|
||||
100% { transform: scale(1); background: linear-gradient(135deg, var(--primary), var(--secondary)); }
|
||||
}
|
||||
|
||||
/* 关闭按钮 - 危险警示效果 */
|
||||
#btn-close-settings {
|
||||
background: linear-gradient(135deg, #ff4444, #cc0000);
|
||||
color: white;
|
||||
border: 2px solid transparent;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 0 25px rgba(255, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
#btn-close-settings::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.6s;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#btn-close-settings::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: radial-gradient(circle, rgba(255, 0, 0, 0.3) 0%, transparent 70%);
|
||||
transition: all 0.3s;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#btn-close-settings:hover {
|
||||
transform: translateY(-3px) scale(1.05);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.2),
|
||||
0 0 40px rgba(255, 0, 0, 0.6),
|
||||
0 0 80px rgba(255, 0, 0, 0.3);
|
||||
border-color: #ff6666;
|
||||
}
|
||||
|
||||
#btn-close-settings:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
#btn-close-settings:hover::after {
|
||||
width: 150%;
|
||||
height: 150%;
|
||||
}
|
||||
|
||||
#btn-close-settings:active {
|
||||
transform: translateY(-1px) scale(0.98);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
0 0 15px rgba(255, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 关闭按钮警告动画 */
|
||||
#btn-close-settings.warning {
|
||||
animation: close-warning 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes close-warning {
|
||||
from {
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 0 25px rgba(255, 0, 0, 0.4);
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3), 0 0 35px rgba(255, 0, 0, 0.6);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* 舞台灯光效果 */
|
||||
.stage-lights {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.light-spot {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(255, 20, 147, 0.1) 0%, transparent 70%);
|
||||
filter: blur(40px);
|
||||
animation: light-spot 8s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
.light-spot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
background: radial-gradient(circle, rgba(255, 0, 255, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.light-spot:nth-child(2) {
|
||||
animation-delay: 2s;
|
||||
background: radial-gradient(circle, rgba(0, 255, 255, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.light-spot:nth-child(3) {
|
||||
animation-delay: 4s;
|
||||
background: radial-gradient(circle, rgba(138, 43, 226, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
@keyframes light-spot {
|
||||
0% { transform: translate(0, 0) scale(1); opacity: 0.3; }
|
||||
33% { transform: translate(50px, 30px) scale(1.2); opacity: 0.5; }
|
||||
66% { transform: translate(-30px, 60px) scale(0.8); opacity: 0.4; }
|
||||
100% { transform: translate(20px, -20px) scale(1.1); opacity: 0.6; }
|
||||
}
|
||||
|
||||
/* 霓虹边框效果 */
|
||||
.neon-border {
|
||||
border: 2px solid var(--primary);
|
||||
box-shadow:
|
||||
0 0 5px var(--primary),
|
||||
0 0 10px var(--primary),
|
||||
0 0 15px var(--primary),
|
||||
inset 0 0 5px var(--primary);
|
||||
animation: neon-border-pulse 3s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes neon-border-pulse {
|
||||
from {
|
||||
border-color: var(--primary);
|
||||
box-shadow:
|
||||
0 0 5px var(--primary),
|
||||
0 0 10px var(--primary),
|
||||
0 0 15px var(--primary);
|
||||
}
|
||||
to {
|
||||
border-color: var(--accent);
|
||||
box-shadow:
|
||||
0 0 10px var(--accent),
|
||||
0 0 20px var(--accent),
|
||||
0 0 30px var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框霓虹效果 */
|
||||
.neon-input {
|
||||
background: rgba(26, 26, 26, 0.8) !important;
|
||||
border: 2px solid var(--primary) !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 0 10px rgba(255, 20, 147, 0.3) !important;
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.neon-input:focus {
|
||||
border-color: var(--accent) !important;
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(0, 255, 255, 0.1),
|
||||
0 0 20px rgba(0, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 卡片霓虹效果增强 */
|
||||
.card {
|
||||
border: 1px solid rgba(255, 20, 147, 0.3);
|
||||
background: rgba(26, 26, 26, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 1px rgba(255, 20, 147, 0.1);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow:
|
||||
0 8px 30px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(0, 255, 255, 0.2),
|
||||
0 0 20px rgba(255, 20, 147, 0.3);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.subtabs {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.compact {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.compact input[type="text"],
|
||||
.compact input[type="date"],
|
||||
.compact select {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.preset {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.light-spot {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
filter: blur(20px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.modal-body {
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtab {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
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)}`)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user