feat(web): refresh palette, simplify ranking toolbar and drop export
This commit is contained in:
1 parent
6a053bdfe8
commit
c2ef998720
16 files changed
+141
-206
No files matched your search
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user