feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
+107
View File
@@ -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>
+908
View File
@@ -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;
}
}
+440
View File
@@ -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)}`)
}
})