feat(web): refresh palette, simplify ranking toolbar and drop export

This commit is contained in:
kairee committed 2026-11-01 00:37:20 +08:00
1 parent 6a053bdfe8
commit c2ef998720
16 files changed
+141 -206

No files matched your search

+10 -79
View File
@@ -7,16 +7,12 @@
from __future__ import annotations
import csv
import hashlib
import hmac
import io
import json
import mimetypes
import os
import time
from dataclasses import dataclass
from datetime import date
from http import HTTPStatus
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -27,7 +23,6 @@ from urllib.parse import parse_qs, unquote, urlsplit
from ..analytics import is_loopback
from ..config import ConfigStore
from ..database import Database
from ..timeutil import shanghai_date
from .queries import DashboardQueries
STATIC_ROOT = Path(__file__).resolve().parent / "static"
@@ -105,7 +100,7 @@ class DashboardHandler(BaseHTTPRequestHandler):
self._method_not_allowed()
def _method_not_allowed(self) -> None:
self._send_error(HTTPStatus.METHOD_NOT_ALLOWED, "method_not_allowed", "Web 看板只读")
self._send_error(HTTPStatus.METHOD_NOT_ALLOWED, "method_not_allowed", "不支持该操作")
# ------------------------------------------------------------ dispatch
@@ -140,7 +135,7 @@ class DashboardHandler(BaseHTTPRequestHandler):
self._send_error(
HTTPStatus.METHOD_NOT_ALLOWED,
"method_not_allowed",
"Web 看板只读",
"不支持该操作",
)
return
self._api(path, query, method)
@@ -220,15 +215,15 @@ class DashboardHandler(BaseHTTPRequestHandler):
<style>
:root{color-scheme:light dark}body{margin:0;min-height:100vh;display:grid;
place-items:center;font-family:system-ui,-apple-system,"Segoe UI",sans-serif;
background:#f6f7f9;color:#1c1f23}main{width:min(92vw,360px);padding:28px;
border-radius:16px;background:#fff;box-shadow:0 12px 40px rgba(0,0,0,.08)}
h1{font-size:20px;margin:0 0 8px}p{color:#6b7280;font-size:14px;margin:0 0 18px}
input{width:100%;box-sizing:border-box;padding:12px 14px;border:1px solid #d9dee6;
background:#f7f2ea;color:#2a201a}main{width:min(92vw,360px);padding:28px;
border-radius:16px;background:#fffdf9;box-shadow:0 12px 40px rgba(42,32,26,.12)}
h1{font-size:20px;margin:0 0 8px}p{color:#7b6a58;font-size:14px;margin:0 0 18px}
input{width:100%;box-sizing:border-box;padding:12px 14px;border:1px solid #e6d8c4;
border-radius:10px;font-size:16px}button{width:100%;margin-top:12px;padding:12px;
border:0;border-radius:10px;background:#fb7299;color:#fff;font-size:16px;
font-weight:600;cursor:pointer}@media(prefers-color-scheme:dark){body{background:#111315;
color:#f3f4f6}main{background:#1b1e22}p{color:#9ca3af}input{background:#111315;
border-color:#333a42;color:#f3f4f6}}
border:0;border-radius:10px;background:#a9702f;color:#fff;font-size:16px;
font-weight:600;cursor:pointer}@media(prefers-color-scheme:dark){body{background:#1b1411;
color:#f3e9dc}main{background:#221a15}p{color:#c9b6a0}input{background:#1b1411;
border-color:#3a2c22;color:#f3e9dc}button{background:#e9b97c;color:#2a1a10}}
</style></head><body><main>
<h1>Reaction 看板</h1><p>输入配置中的 web.auth_token 以继续。</p>
<form method="post" action="/login">
@@ -378,63 +373,8 @@ border-color:#333a42;color:#f3f4f6}}
if path == "/api/system":
self._send_json(HTTPStatus.OK, queries.system())
return
if path == "/api/export.csv":
self._export_csv(query)
return
raise ApiError(HTTPStatus.NOT_FOUND, "not_found", "接口不存在")
def _export_csv(self, query: Mapping[str, list[str]]) -> None:
data = self.context.queries.export_videos(
sort=_param(query, "sort", "heat"),
within_days=_optional_int(query, "within_days"),
growth_days=_int_param(query, "growth_days", 7, 1, 365),
folder_id=_optional_int(query, "folder"),
q=_optional_str(query, "q"),
page=1,
page_size=10_000,
)
output = io.StringIO()
writer = csv.writer(output, lineterminator="\n")
writer.writerow([
"排名", "aid", "bvid", "标题", "UP", "发布时间", "时长(秒)",
"播放", "点赞", "投币", "收藏", "转发", "评论", "弹幕",
"互动率", "综合热度", "状态", "收藏夹", "快照日期",
])
for index, item in enumerate(data["items"], start=1):
metrics = item.get("metrics") or {}
writer.writerow([
index,
item["aid"],
item.get("bvid") or "",
item.get("title") or "",
item.get("up_name") or "",
_format_date(item.get("pubdate")),
item.get("duration") or "",
metrics.get("view", ""),
metrics.get("like", ""),
metrics.get("coin", ""),
metrics.get("favorite", ""),
metrics.get("share", ""),
metrics.get("reply", ""),
metrics.get("danmaku", ""),
"" if item.get("interaction_rate") is None else f"{item['interaction_rate']:.6f}",
"" if item.get("heat") is None else item["heat"],
item.get("status") or "",
item.get("folder_title") or "",
item.get("snapshot_date") or "",
])
payload = ("\ufeff" + output.getvalue()).encode("utf-8")
filename = f"reaction-board-{shanghai_date(self.context.clock())}.csv"
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/csv; charset=utf-8")
self.send_header("Content-Disposition", f'attachment; filename="{filename}"')
self.send_header("Content-Length", str(len(payload)))
self.send_header("Cache-Control", "no-store")
self._security_headers()
self.end_headers()
if self.command != "HEAD":
self.wfile.write(payload)
# ------------------------------------------------------------ static
def _static(self, path: str, method: str) -> None:
@@ -588,15 +528,6 @@ def _range_days(query: Mapping[str, list[str]]) -> int | None:
return days
def _format_date(timestamp: int | float | None) -> str:
if not timestamp:
return ""
try:
return shanghai_date(float(timestamp))
except (OverflowError, OSError, ValueError):
return ""
def _preview_marker(db: Database) -> bool:
def run(conn):
row = conn.execute("SELECT v FROM kv WHERE k = 'preview:seeded'").fetchone()