From c2ef998720182f905d2aecebf9496dd9784e6f77 Mon Sep 17 00:00:00 2001 From: kaireewu Date: Sun, 1 Nov 2026 00:37:20 +0800 Subject: [PATCH] feat(web): refresh palette, simplify ranking toolbar and drop export --- reaction_board/web/queries.py | 21 +--- reaction_board/web/server.py | 89 ++----------- reaction_board/web/static/api.js | 4 - reaction_board/web/static/app.js | 2 + reaction_board/web/static/charts.js | 41 +++--- reaction_board/web/static/favicon.svg | 2 +- reaction_board/web/static/index.html | 6 +- reaction_board/web/static/style.css | 119 +++++++++++------- reaction_board/web/static/views/candidates.js | 8 +- reaction_board/web/static/views/fans.js | 4 +- reaction_board/web/static/views/rankings.js | 31 ++--- reaction_board/web/static/views/system.js | 2 +- reaction_board/web/static/views/up.js | 6 +- reaction_board/web/static/views/video.js | 4 +- tests/test_web_queries.py | 4 - tests/test_web_server.py | 4 +- 16 files changed, 141 insertions(+), 206 deletions(-) diff --git a/reaction_board/web/queries.py b/reaction_board/web/queries.py index 4bdd5f6..382bc87 100644 --- a/reaction_board/web/queries.py +++ b/reaction_board/web/queries.py @@ -523,14 +523,13 @@ class DashboardQueries: q: str | None = None, page: int = 1, page_size: int = 20, - export: bool = False, ) -> dict[str, Any]: if sort not in VIDEO_SORTS: raise ValueError(f"不支持的排序: {sort}") if growth_days < 1 or growth_days > 365: raise ValueError("growth_days 必须在 1~365 之间") page = max(1, int(page)) - page_size = max(1, min(int(page_size), 200 if not export else 10_000)) + page_size = max(1, min(int(page_size), 200)) def run(conn: sqlite3.Connection) -> dict[str, Any]: latest = self._latest_date(conn) or self._today() @@ -607,9 +606,8 @@ class DashboardQueries: growth_available = any( item.get("growth", {}).get("available") for item in items ) - if not export: - start = (page - 1) * page_size - items = items[start:start + page_size] + start = (page - 1) * page_size + items = items[start:start + page_size] available = True reason = None if sort in {"growth", "growth_rate"} and not growth_available: @@ -732,12 +730,11 @@ class DashboardQueries: min_videos: int = 1, page: int = 1, page_size: int = 20, - export: bool = False, ) -> dict[str, Any]: if sort not in UP_SORTS: raise ValueError(f"不支持的 UP 排序: {sort}") page = max(1, int(page)) - page_size = max(1, min(int(page_size), 200 if not export else 10_000)) + page_size = max(1, min(int(page_size), 200)) min_videos = max(1, int(min_videos)) def run(conn: sqlite3.Connection) -> dict[str, Any]: @@ -813,9 +810,8 @@ class DashboardQueries: key = sort_map[sort] items.sort(key=lambda item: (key(item), item["up_mid"]), reverse=True) total = len(items) - if not export: - start = (page - 1) * page_size - items = items[start:start + page_size] + start = (page - 1) * page_size + items = items[start:start + page_size] return { "sort": sort, "within_days": within_days, @@ -1115,8 +1111,3 @@ class DashboardQueries: "telegram_enabled": bool(cfg.telegram.enabled), } return self.db.read(run) - - def export_videos(self, **filters) -> dict[str, Any]: - filters.pop("page", None) - filters.pop("page_size", None) - return self.list_videos(**filters, export=True) diff --git a/reaction_board/web/server.py b/reaction_board/web/server.py index ad07fd3..b200f86 100644 --- a/reaction_board/web/server.py +++ b/reaction_board/web/server.py @@ -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):

Reaction 看板

输入配置中的 web.auth_token 以继续。

@@ -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() diff --git a/reaction_board/web/static/api.js b/reaction_board/web/static/api.js index b4cc82b..316652a 100644 --- a/reaction_board/web/static/api.js +++ b/reaction_board/web/static/api.js @@ -44,10 +44,6 @@ export async function api(path, params) { return payload.data; } -export function apiUrl(path, params) { - return buildUrl(path, params).toString(); -} - export async function health() { const response = await fetch("/api/health", { credentials: "same-origin" }); if (!response.ok) throw new ApiError("无法连接看板服务", response.status); diff --git a/reaction_board/web/static/app.js b/reaction_board/web/static/app.js index 1a336a7..9cf0050 100644 --- a/reaction_board/web/static/app.js +++ b/reaction_board/web/static/app.js @@ -90,11 +90,13 @@ window.addEventListener("error", (event) => { async function boot() { renderNav(); const banner = document.querySelector("#preview-banner"); + const hint = document.querySelector("#topbar-hint"); try { const payload = await health(); const data = payload?.data || payload; if (data?.preview) { banner.hidden = false; + if (hint) hint.hidden = false; } } catch { toast("无法连接看板服务", "error"); diff --git a/reaction_board/web/static/charts.js b/reaction_board/web/static/charts.js index 6430c55..63bd33b 100644 --- a/reaction_board/web/static/charts.js +++ b/reaction_board/web/static/charts.js @@ -6,15 +6,21 @@ let resizeObserver = null; function colors() { const styles = getComputedStyle(document.documentElement); return { - text: styles.getPropertyValue("--text").trim() || "#1c1f23", - muted: styles.getPropertyValue("--muted").trim() || "#6b7280", - grid: styles.getPropertyValue("--grid").trim() || "#e8ebf0", - accent: "#fb7299", - blue: "#00aeec", - surface: styles.getPropertyValue("--surface").trim() || "#ffffff", + text: styles.getPropertyValue("--text").trim() || "#2a201a", + muted: styles.getPropertyValue("--muted").trim() || "#7b6a58", + grid: styles.getPropertyValue("--grid").trim() || "#eadfce", + accent: styles.getPropertyValue("--accent").trim() || "#a9702f", + blue: styles.getPropertyValue("--blue").trim() || "#5f7f96", + success: styles.getPropertyValue("--success").trim() || "#5f7a4e", + danger: styles.getPropertyValue("--danger").trim() || "#b4553f", + surface: styles.getPropertyValue("--surface").trim() || "#fffdf9", }; } +export function themeColor(name) { + return colors()[name] || colors().accent; +} + function ensureEcharts() { if (!window.echarts) { throw new Error("ECharts 未加载,请检查 /static/vendor/echarts.min.js"); @@ -54,16 +60,10 @@ export function disposeAll() { resizeObserver = null; } -const palette = { - accent: "#fb7299", - blue: "#00aeec", - purple: "#9b7bff", - green: "#22c55e", -}; - -export function lineChart(element, { dates, values, name = "数值", color = palette.accent, scale = false }) { +export function lineChart(element, { dates, values, name = "数值", color, scale = false }) { if (!element) return null; const theme = colors(); + const lineColor = color || theme.accent; const option = { animationDuration: 300, backgroundColor: "transparent", @@ -114,14 +114,14 @@ export function lineChart(element, { dates, values, name = "数值", color = pal symbol: "circle", symbolSize: 5, showSymbol: values.length <= 45, - lineStyle: { width: 2.2, color }, - itemStyle: { color }, + lineStyle: { width: 2.2, color: lineColor }, + itemStyle: { color: lineColor }, areaStyle: { color: { type: "linear", x: 0, y: 0, x2: 0, y2: 1, colorStops: [ - { offset: 0, color: `${color}55` }, - { offset: 1, color: `${color}00` }, + { offset: 0, color: `${lineColor}55` }, + { offset: 1, color: `${lineColor}00` }, ], }, }, @@ -130,9 +130,10 @@ export function lineChart(element, { dates, values, name = "数值", color = pal return mount(element, option); } -export function barChart(element, { labels, values, name = "数值", color = palette.blue }) { +export function barChart(element, { labels, values, name = "数值", color }) { if (!element) return null; const theme = colors(); + const barColor = color || theme.blue; const option = { animationDuration: 300, backgroundColor: "transparent", @@ -176,7 +177,7 @@ export function barChart(element, { labels, values, name = "数值", color = pal type: "bar", data: values, barMaxWidth: 26, - itemStyle: { color, borderRadius: [4, 4, 0, 0] }, + itemStyle: { color: barColor, borderRadius: [4, 4, 0, 0] }, }], }; return mount(element, option); diff --git a/reaction_board/web/static/favicon.svg b/reaction_board/web/static/favicon.svg index 873cd6a..96d79f5 100644 --- a/reaction_board/web/static/favicon.svg +++ b/reaction_board/web/static/favicon.svg @@ -1,6 +1,6 @@ - + + + 咻咻满 Reaction 看板 @@ -18,9 +20,9 @@ 最新动态 - 只读 + - +
diff --git a/reaction_board/web/static/style.css b/reaction_board/web/static/style.css index 42e0d77..4933fbd 100644 --- a/reaction_board/web/static/style.css +++ b/reaction_board/web/static/style.css @@ -1,43 +1,57 @@ :root { color-scheme: light dark; - --bg: #f5f6f8; - --surface: #ffffff; - --surface-2: #eef1f5; - --surface-3: #e5e9ef; - --text: #1b1e23; - --muted: #6b7280; - --border: #e3e7ee; - --grid: #e8ebf0; - --accent: #fb7299; - --accent-strong: #e75c84; - --accent-soft: #ffe8ef; - --blue: #00aeec; - --blue-soft: #e3f7fe; - --warning: #d97706; - --warning-soft: #fff4df; - --danger: #dc2626; - --success: #16a34a; + --bg: #f7f2ea; + --bg-deep: #efe7db; + --surface: #fffdf9; + --surface-2: #f2ebe0; + --surface-3: #e9ddca; + --text: #2a201a; + --muted: #7b6a58; + --faint: #a3927f; + --border: #e6d8c4; + --grid: #eadfce; + --accent: #a9702f; + --accent-strong: #8a5417; + --accent-soft: #f3e2c5; + --accent-contrast: #ffffff; + --blue: #5f7f96; + --blue-soft: #e3edf3; + --pink: #c25a52; + --warning: #b9793a; + --warning-soft: #f7e6c8; + --danger: #b4553f; + --success: #5f7a4e; --radius: 16px; --radius-small: 10px; - --shadow: 0 8px 30px rgba(20, 28, 45, 0.06); + --shadow: 0 10px 30px rgba(42, 32, 26, 0.08); --nav-width: 220px; --nav-height: 64px; } @media (prefers-color-scheme: dark) { :root { - --bg: #0e1014; - --surface: #171a20; - --surface-2: #20252d; - --surface-3: #2a313b; - --text: #edf0f4; - --muted: #9aa3af; - --border: #2a3038; - --grid: #2a3038; - --accent-soft: #3a2029; - --blue-soft: #102f3a; - --warning-soft: #3a2a12; - --shadow: 0 12px 34px rgba(0, 0, 0, 0.28); + --bg: #1b1411; + --bg-deep: #140f0c; + --surface: #221a15; + --surface-2: #2a201a; + --surface-3: #342820; + --text: #f3e9dc; + --muted: #c9b6a0; + --faint: #9c8976; + --border: #3a2c22; + --grid: #3a2c22; + --accent: #e9b97c; + --accent-strong: #f6d9ae; + --accent-soft: #e9b97c29; + --accent-contrast: #2a1a10; + --blue: #a8bccc; + --blue-soft: #a8bccc1f; + --pink: #e3958c; + --warning: #e9b97c; + --warning-soft: #e9b97c1f; + --danger: #e3958c; + --success: #a7b98f; + --shadow: 0 14px 38px rgba(0, 0, 0, 0.35); } } @@ -92,11 +106,11 @@ h1, h2, h3, p { margin: 0; } height: 34px; flex: 0 0 auto; border-radius: 11px; - background: linear-gradient(135deg, var(--accent), #ff9fbc); - color: #fff; + background: linear-gradient(135deg, var(--accent-strong), var(--accent)); + color: var(--accent-contrast); font-weight: 800; font-size: 17px; - box-shadow: 0 6px 16px rgba(251, 114, 153, 0.3); + box-shadow: 0 6px 16px color-mix(in srgb, var(--accent) 35%, transparent); } .brand-text { display: flex; flex-direction: column; line-height: 1.15; min-width: 0; } .brand-text strong { font-size: 15px; letter-spacing: 0.2px; } @@ -118,11 +132,11 @@ h1, h2, h3, p { margin: 0; } text-align: center; font-size: 12px; font-weight: 600; - color: #5c3b00; - background: #ffd479; + color: var(--accent-contrast); + background: var(--accent); } @media (prefers-color-scheme: dark) { - .preview-banner { color: #ffe7b0; background: #6b4a00; } + .preview-banner { color: var(--accent-contrast); background: var(--accent); } } .main-shell { @@ -161,7 +175,7 @@ h1, h2, h3, p { margin: 0; } border: 0; border-radius: 10px; background: var(--accent); - color: #fff; + color: var(--accent-contrast); font-size: 13px; font-weight: 650; cursor: pointer; @@ -306,9 +320,9 @@ h1, h2, h3, p { margin: 0; } font-size: 12px; font-weight: 700; } -.rank-badge.rank-1 { background: #ffd166; color: #5b3a00; } -.rank-badge.rank-2 { background: #dbe3ec; color: #39414d; } -.rank-badge.rank-3 { background: #f2c7a5; color: #5c3218; } +.rank-badge.rank-1 { background: var(--accent); color: var(--accent-contrast); } +.rank-badge.rank-2 { background: var(--accent-soft); color: var(--accent-strong); } +.rank-badge.rank-3 { background: var(--surface-3); color: var(--muted); } .video-thumb, .hero-cover, .heat-thumb, .candidate-thumb { position: relative; overflow: hidden; @@ -469,7 +483,7 @@ h1, h2, h3, p { margin: 0; } font-weight: 600; cursor: pointer; } -.segmented button.active { background: var(--accent); color: #fff; } +.segmented button.active { background: var(--accent); color: var(--accent-contrast); } .chip-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; } .chip-row.scroll-x { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; } @@ -489,7 +503,17 @@ h1, h2, h3, p { margin: 0; } .chip-label { color: var(--muted); font-size: 11px; padding-right: 2px; } .range-row { margin-top: 8px; } -.filter-bar { display: flex; gap: 7px; } +.rank-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} +.rank-search { display: flex; flex: 1; min-width: 0; gap: 6px; } +.rank-search .search-input { flex: 1; min-width: 0; } +@media (max-width: 619px) { + .rank-search { flex-basis: 100%; } +} .search-input, .select { min-height: 38px; padding: 8px 11px; @@ -613,8 +637,8 @@ h1, h2, h3, p { margin: 0; } .hero-number { padding: 18px; border-radius: var(--radius); - background: linear-gradient(135deg, #2b2140, #4a2c4f 55%, #a33d5f); - color: #fff; + background: linear-gradient(135deg, var(--accent-strong), var(--accent) 65%, var(--bg-deep)); + color: var(--accent-contrast); } .hero-number span { display: block; font-size: 12px; opacity: 0.72; } .hero-number strong { display: block; margin: 4px 0 2px; font-size: 34px; letter-spacing: -1px; } @@ -680,8 +704,9 @@ h1, h2, h3, p { margin: 0; } max-width: min(88vw, 420px); padding: 9px 14px; border-radius: 999px; - background: rgba(20, 24, 32, 0.92); - color: #fff; + background: var(--bg-deep); + color: var(--text); + border: 1px solid var(--border); font-size: 12.5px; opacity: 0; transform: translate(-50%, 10px); @@ -689,7 +714,7 @@ h1, h2, h3, p { margin: 0; } transition: opacity 0.18s, transform 0.18s; } .toast.show { opacity: 1; transform: translate(-50%, 0); } -.toast.error { background: rgba(160, 30, 45, 0.95); } +.toast.error { background: var(--danger); color: #fff7f2; border-color: transparent; } @media (min-width: 620px) { .live-strip { grid-template-columns: repeat(4, minmax(0, 1fr)); } diff --git a/reaction_board/web/static/views/candidates.js b/reaction_board/web/static/views/candidates.js index 6cfd1c1..20ed8ef 100644 --- a/reaction_board/web/static/views/candidates.js +++ b/reaction_board/web/static/views/candidates.js @@ -49,10 +49,10 @@ export async function renderCandidates(root) { -
- Web 看板只读 - ${data.telegram_enabled ? "请在 Telegram 消息中处理候选。" : "Telegram 未启用,请使用 CLI 或数据库处理候选。"} -
+ ${data.items.length ? `
+ 候选处理 + ${data.telegram_enabled ? "请在 Telegram 消息中收藏、忽略或拉黑。" : "Telegram 未启用,请使用 CLI 或数据库处理候选。"} +
` : ""}
${sectionHeader("待确认")} diff --git a/reaction_board/web/static/views/fans.js b/reaction_board/web/static/views/fans.js index 26d6217..9e54273 100644 --- a/reaction_board/web/static/views/fans.js +++ b/reaction_board/web/static/views/fans.js @@ -1,6 +1,6 @@ import { api } from "../api.js"; import { centerActiveChips, emptyState, sectionHeader } from "../components.js"; -import { lineChart } from "../charts.js"; +import { lineChart, themeColor } from "../charts.js"; import { escapeAttr, escapeHtml, formatCount, formatDate, formatDelta, } from "../format.js"; @@ -82,7 +82,7 @@ export async function renderFans(root, { query }) { dates: fans.dates || [], values: fans.series || [], name: "粉丝", - color: "#9b7bff", + color: themeColor("accent"), scale: true, }); } diff --git a/reaction_board/web/static/views/rankings.js b/reaction_board/web/static/views/rankings.js index 1588dea..bbd01e5 100644 --- a/reaction_board/web/static/views/rankings.js +++ b/reaction_board/web/static/views/rankings.js @@ -1,4 +1,4 @@ -import { api, apiUrl } from "../api.js"; +import { api } from "../api.js"; import { centerActiveChips, chip, emptyState, pager, sectionHeader, upCard, videoCard, } from "../components.js"; @@ -8,7 +8,7 @@ import { replaceQuery } from "../router.js"; const VIDEO_SORTS = [ ["heat", "综合热度"], ["view", "播放"], ["like", "点赞"], ["coin", "投币"], ["favorite", "收藏"], ["share", "转发"], ["reply", "评论"], ["danmaku", "弹幕"], - ["interaction", "互动率"], ["growth", "增长"], ["growth_rate", "增速"], + ["interaction", "互动率"], ["growth", "播放增量"], ["growth_rate", "播放增速"], ]; const UP_SORTS = [ ["avg_views", "平均播放"], ["avg_interaction", "平均互动"], @@ -20,18 +20,20 @@ const METRIC_PRIMARY = new Set(["view", "like", "coin", "favorite", "share", "re function controls({ scope, sort, within, growthDays, q, folder, minVideos, folders }) { const sorts = scope === "up" ? UP_SORTS : VIDEO_SORTS; return ` -
- - +
+
+ + +
+ + + +
${sorts.map(([value, label]) => chip(label, sort === value, "sort", value)).join("")}
-
- - -
${[["all", "全部发布时间"], ["7", "近 7 天"], ["30", "近 30 天"], ["90", "近 90 天"]] @@ -106,12 +108,6 @@ export async function renderRankings(root, { query }) { } const startRank = (data.page - 1) * data.page_size + 1; - const exportParams = { - ...params, folder: folder || null, scope, - min_videos: scope === "up" ? minVideos : null, - }; - delete exportParams.page; - delete exportParams.page_size; root.innerHTML = `
@@ -119,9 +115,6 @@ export async function renderRankings(root, { query }) {

榜单

共 ${formatCount(data.total)} ${scope === "up" ? "个 UP" : "条视频"}

- ${scope === "video" - ? `导出 CSV` - : ""} ${controls({ scope, sort, within, growthDays, q, folder, minVideos, folders })} ${data.available === false ? `
diff --git a/reaction_board/web/static/views/system.js b/reaction_board/web/static/views/system.js index 2473849..481fcfd 100644 --- a/reaction_board/web/static/views/system.js +++ b/reaction_board/web/static/views/system.js @@ -104,7 +104,7 @@ export async function renderSystem(root) {
${sectionHeader("UP 黑名单")} -

只影响未来搜索发现;已入库视频继续追踪。Web 只读,移除请使用 CLI 或数据库。

+

只影响未来搜索发现;已入库视频继续追踪。移除请使用 CLI 或数据库。

${(system.blacklist || []).length ? `
diff --git a/reaction_board/web/static/views/up.js b/reaction_board/web/static/views/up.js index c765d0f..8d0c438 100644 --- a/reaction_board/web/static/views/up.js +++ b/reaction_board/web/static/views/up.js @@ -2,7 +2,7 @@ import { api } from "../api.js"; import { badge, emptyState, sectionHeader, videoCard, } from "../components.js"; -import { barChart, lineChart } from "../charts.js"; +import { barChart, lineChart, themeColor } from "../charts.js"; import { escapeAttr, escapeHtml, formatCount, formatDate, formatPercent, } from "../format.js"; @@ -89,7 +89,7 @@ export async function renderUp(root, { params, query }) { dates: up.series.dates, values: up.series.views, name: "总播放", - color: "#fb7299", + color: themeColor("accent"), scale: true, }); } @@ -99,7 +99,7 @@ export async function renderUp(root, { params, query }) { labels: topForBar.map((item) => (item.title || `#${item.aid}`).slice(0, 12)), values: topForBar.map((item) => item.metrics?.view || 0), name: "播放", - color: "#00aeec", + color: themeColor("blue"), }); } root.querySelector("#up-sort")?.addEventListener("change", (event) => { diff --git a/reaction_board/web/static/views/video.js b/reaction_board/web/static/views/video.js index 1c06457..e444b7d 100644 --- a/reaction_board/web/static/views/video.js +++ b/reaction_board/web/static/views/video.js @@ -3,7 +3,7 @@ import { badge, centerActiveChips, cover, emptyState, metricChips, metricGrid, sectionHeader, videoCard, } from "../components.js"; -import { lineChart, emptyChart } from "../charts.js"; +import { lineChart, themeColor } from "../charts.js"; import { escapeAttr, escapeHtml, formatCount, formatDate, formatDelta, formatDuration, formatPercent, formatScore, metricLabel, statusLabel, @@ -119,7 +119,7 @@ export async function renderVideo(root, { params, query }) { dates: trend.dates || [], values: trend.series?.[metric] || [], name: metricLabel(metric), - color: metric === "view" ? "#fb7299" : "#00aeec", + color: metric === "view" ? themeColor("accent") : themeColor("blue"), scale: true, }); } diff --git a/tests/test_web_queries.py b/tests/test_web_queries.py index 584503d..2410d37 100644 --- a/tests/test_web_queries.py +++ b/tests/test_web_queries.py @@ -239,10 +239,6 @@ def test_fans_candidates_system_and_export(): assert system["next_runs"]["snapshot"] > 0 assert system["next_runs"]["folder_sync"] > FIXED_NOW assert system["next_runs"]["discover"] > FIXED_NOW - - exported = q.export_videos(sort="view") - assert exported["total"] == 3 - assert all("metrics" in item for item in exported["items"]) finally: db.close() diff --git a/tests/test_web_server.py b/tests/test_web_server.py index d9d805a..b4e1b83 100644 --- a/tests/test_web_server.py +++ b/tests/test_web_server.py @@ -119,9 +119,7 @@ def test_readonly_routes_and_static(): assert status == 404 status, _, payload = running.request("/api/export.csv?sort=view") - assert status == 200 - assert payload.startswith(b"\xef\xbb\xbf") - assert "咻咻满".encode("utf-8") in payload + assert status == 404 status, headers, _ = running.request("/api/overview") assert headers["X-Content-Type-Options"] == "nosniff"
mid昵称原因在库视频