feat(web): inline toolbar with label sort, compact time, icon search
This commit is contained in:
1 parent
9be7d1cab2
commit
d00fcd5c38
5 files changed
+62
-36
No files matched your search
+2
-2
@@ -542,8 +542,8 @@ P3 实现说明:
|
|||||||
| 页面 | 内容 |
|
| 页面 | 内容 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 首页 | 最新动态优先:今日增量、最新入库、最近发布、增长动态、热度速览、容量与采集状态。 |
|
| 首页 | 最新动态优先:今日增量、最新入库、最近发布、增长动态、热度速览、容量与采集状态。 |
|
||||||
| 视频(`/videos`) | 内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量、播放增速;排序与时间筛选为下拉框,时间支持全部/7 天/30 天/90 天/1 年/年份。 |
|
| 视频(`/videos`) | 内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量、播放增速;工具条为「排序」下拉 + 时间下拉 + 右侧放大镜搜索框(一行展示),时间支持全部/7 天/30 天/90 天/1 年/年份。 |
|
||||||
| UP主(`/ups`) | 独立入口,不再与视频混排;排序下拉含内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布。 |
|
| UP主(`/ups`) | 独立入口,不再与视频混排;排序下拉含内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;不提供按视频数筛选。 |
|
||||||
| 数据说明(`/about`) | 数据来源与更新、发布时间/入库时间/快照日期、指标公式、时长与成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取。 |
|
| 数据说明(`/about`) | 数据来源与更新、发布时间/入库时间/快照日期、指标公式、时长与成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取。 |
|
||||||
| 视频详情 | 元数据、分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表、同 UP 对比。 |
|
| 视频详情 | 元数据、分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表、同 UP 对比。 |
|
||||||
| UP 页 | 该 UP 的全部视频、聚合趋势、单视频对比、总时长与播放·小时、入库时与当前粉丝数。 |
|
| UP 页 | 该 UP 的全部视频、聚合趋势、单视频对比、总时长与播放·小时、入库时与当前粉丝数。 |
|
||||||
|
|||||||
@@ -171,9 +171,9 @@ uv run python app.py serve --preview --config data/preview/config.json
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 首页默认“最新动态”:今日增量、最新入库、最近发布、增长动态、热度速览、收藏夹容量与采集状态;
|
- 首页默认“最新动态”:今日增量、最新入库、最近发布、增长动态、热度速览、收藏夹容量与采集状态;
|
||||||
- 「视频」与「UP主」是两个独立入口;搜索框固定在约 230px,排序与时间筛选都是下拉框,可放在同一行;
|
- 「视频」与「UP主」是两个独立入口;工具条顺序为「排序」下拉 → 时间下拉(无需 label)→ 右侧自适应搜索框(放大镜按钮在输入框内),三者在 PC 与移动端都保持一行;
|
||||||
- 视频排序:内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量/播放增速;UP 排序:内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;
|
- 视频排序:内容效率(默认)、综合热度、发布时间、入库时间、播放/投币/收藏/评论、互动率、播放增量/播放增速;UP 排序:内容效率、互动效率、内容量、总播放、视频数、平均播放、总互动、最近发布;
|
||||||
- 时间筛选:全部 / 近 7 天 / 近 30 天 / 近 90 天 / 近 1 年 / 已入库的年份;不再提供按收藏夹过滤;
|
- 时间筛选:全部 / 近 7 天 / 近 30 天 / 近 90 天 / 近 1 年 / 已入库的年份;不再提供按收藏夹过滤,UP 页也不再按视频数筛选;
|
||||||
- 内容效率 = 播放 / 内容小时;单条视频用 60 分钟先验暴露量向全库均值收缩,只统计时长 ≥ 600 秒、发布满 2 天、播放 ≥ 2000 的视频(综合热度榜同样要求播放 ≥ 2000);UP 内容效率用经验贝叶斯收缩,避免极小样本冲榜;
|
- 内容效率 = 播放 / 内容小时;单条视频用 60 分钟先验暴露量向全库均值收缩,只统计时长 ≥ 600 秒、发布满 2 天、播放 ≥ 2000 的视频(综合热度榜同样要求播放 ≥ 2000);UP 内容效率用经验贝叶斯收缩,避免极小样本冲榜;
|
||||||
- 「数据说明」(`/about`)说明数据来源、发布时间/入库时间/快照日期、指标公式、筛选口径与已知局限,数值由 `/api/about` 动态读取;
|
- 「数据说明」(`/about`)说明数据来源、发布时间/入库时间/快照日期、指标公式、筛选口径与已知局限,数值由 `/api/about` 动态读取;
|
||||||
- 视频详情提供分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表和同 UP 对比;UP 页提供聚合趋势、单视频对比与入库时/当前粉丝数;
|
- 视频详情提供分 P 数、七项指标、内容/互动效率、入库时粉丝数、单指标趋势、快照表和同 UP 对比;UP 页提供聚合趋势、单视频对比与入库时/当前粉丝数;
|
||||||
|
|||||||
+2
-1
@@ -702,7 +702,8 @@ UP 内容效率 = EB 收缩后的 Σview / Σ内容小时
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 首页 | 最新动态优先:今日增量、最新入库、最近发布、增长动态、热度速览、收藏夹容量、采集状态 |
|
| 首页 | 最新动态优先:今日增量、最新入库、最近发布、增长动态、热度速览、收藏夹容量、采集状态 |
|
||||||
| **视频** | 排序下拉:内容效率(默认)/综合热度/发布时间/入库时间/播放/投币/收藏/评论/互动率/播放增量/播放增速;时间下拉:全部/7 天/30 天/90 天/1 年/年份;搜索框约 230px;不再按收藏夹过滤 |
|
| **视频** | 排序下拉:内容效率(默认)/综合热度/发布时间/入库时间/播放/投币/收藏/评论/互动率/播放增量/播放增速;时间下拉:全部/7 天/30 天/90 天/1 年/年份;搜索框约 230px;不再按收藏夹过滤 |
|
||||||
| **UP主** | 独立入口;排序下拉:内容效率/互动效率/内容量/总播放/视频数/平均播放/总互动/最近发布;时间下拉同上 |
|
| **UP主** | 独立入口;排序下拉:内容效率/互动效率/内容量/总播放/视频数/平均播放/总互动/最近发布;时间下拉同上;不按视频数筛选 |
|
||||||
|
| 工具条 | 顺序为「排序」(带 label)→ 时间(无 label)→ 右侧自适应搜索框(放大镜按钮内嵌),gap 10px,PC 与移动均一行 |
|
||||||
| 数据说明 | 数据来源与更新节奏、三个时间概念、指标公式、时长/成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取当前配置 |
|
| 数据说明 | 数据来源与更新节奏、三个时间概念、指标公式、时长/成熟窗口筛选、粉丝快照覆盖与已知局限;数值由 `/api/about` 动态读取当前配置 |
|
||||||
| 视频详情 | 元数据 + 分 P 数 + 七项指标 + 内容/互动效率 + 入库时粉丝数 + 单指标趋势 + 同 UP 对比 + 快照表 |
|
| 视频详情 | 元数据 + 分 P 数 + 七项指标 + 内容/互动效率 + 入库时粉丝数 + 单指标趋势 + 同 UP 对比 + 快照表 |
|
||||||
| UP 页 | 该 UP 全部视频、聚合趋势、单视频对比、总时长/播放·小时、入库时与当前粉丝数 |
|
| UP 页 | 该 UP 全部视频、聚合趋势、单视频对比、总时长/播放·小时、入库时与当前粉丝数 |
|
||||||
|
|||||||
@@ -516,13 +516,15 @@ h1, h2, h3, p { margin: 0; }
|
|||||||
|
|
||||||
.rank-toolbar {
|
.rank-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
.rank-search { display: flex; flex: 0 0 auto; width: 230px; gap: 6px; }
|
.rank-toolbar::-webkit-scrollbar { display: none; }
|
||||||
.rank-search .search-input { flex: 1 1 auto; min-width: 0; }
|
.rank-toolbar .select-label {
|
||||||
.select-label {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -530,11 +532,38 @@ h1, h2, h3, p { margin: 0; }
|
|||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.rank-toolbar .select { max-width: 220px; width: auto; }
|
.rank-toolbar .select { max-width: 150px; min-width: 0; width: auto; }
|
||||||
@media (max-width: 619px) {
|
.rank-toolbar #rank-time { max-width: 128px; }
|
||||||
.rank-search { width: 230px; }
|
.rank-search {
|
||||||
.rank-toolbar .select { max-width: 100%; }
|
display: flex;
|
||||||
|
flex: 1 1 120px;
|
||||||
|
min-width: 96px;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
.search-box { position: relative; display: flex; align-items: center; width: 100%; }
|
||||||
|
.search-box .search-input { width: 100%; padding-right: 34px; }
|
||||||
|
.search-box .search-input::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
.search-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.search-button svg { width: 18px; height: 18px; }
|
||||||
|
.search-button:hover { color: var(--accent); }
|
||||||
.search-input, .select {
|
.search-input, .select {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
padding: 8px 11px;
|
padding: 8px 11px;
|
||||||
|
|||||||
@@ -67,27 +67,27 @@ function timeOptions(within, year, years) {
|
|||||||
return base + yearOptions;
|
return base + yearOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function controls({ scope, sort, within, year, growthDays, q, minVideos, years }) {
|
function controls({ scope, sort, within, year, growthDays, q, years }) {
|
||||||
return `
|
return `
|
||||||
<div class="rank-toolbar">
|
<div class="rank-toolbar">
|
||||||
<form class="rank-search" id="rank-filter">
|
|
||||||
<input class="search-input" type="search" name="q" value="${escapeAttr(q || "")}"
|
|
||||||
placeholder="${scope === "up" ? "搜索 UP 名" : "搜索标题或 UP"}" autocomplete="off">
|
|
||||||
<button class="button" type="submit">搜索</button>
|
|
||||||
</form>
|
|
||||||
<label class="select-label">排序
|
<label class="select-label">排序
|
||||||
<select class="select" id="rank-sort">${sortOptions(scope, sort)}</select>
|
<select class="select" id="rank-sort">${sortOptions(scope, sort)}</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="select-label">时间
|
<select class="select" id="rank-time" aria-label="时间范围" title="时间范围">
|
||||||
<select class="select" id="rank-time">${timeOptions(within, year, years)}</select>
|
${timeOptions(within, year, years)}
|
||||||
</label>
|
</select>
|
||||||
${scope === "up" ? `<label class="select-label">视频数
|
<form class="rank-search" id="rank-filter">
|
||||||
<select class="select" id="min-videos">
|
<span class="search-box">
|
||||||
<option value="1" ${String(minVideos) === "1" ? "selected" : ""}>全部 UP</option>
|
<input class="search-input" type="search" name="q" value="${escapeAttr(q || "")}"
|
||||||
<option value="2" ${String(minVideos) === "2" ? "selected" : ""}>≥ 2 条</option>
|
placeholder="${scope === "up" ? "搜索 UP 名" : "搜索标题或 UP"}" autocomplete="off">
|
||||||
<option value="5" ${String(minVideos) === "5" ? "selected" : ""}>≥ 5 条</option>
|
<button class="search-button" type="submit" aria-label="搜索" title="搜索">
|
||||||
</select>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
</label>` : ""}
|
stroke-linecap="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.2-3.2"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
${scope === "video" && (sort === "growth" || sort === "growth_rate") ? `
|
${scope === "video" && (sort === "growth" || sort === "growth_rate") ? `
|
||||||
<div class="chip-row growth-days">
|
<div class="chip-row growth-days">
|
||||||
@@ -184,9 +184,6 @@ function bindControls(root) {
|
|||||||
root.querySelectorAll("[data-growth-days]").forEach((button) => button.addEventListener("click", () => {
|
root.querySelectorAll("[data-growth-days]").forEach((button) => button.addEventListener("click", () => {
|
||||||
replaceQuery({ growth_days: button.dataset.growthDays, page: null });
|
replaceQuery({ growth_days: button.dataset.growthDays, page: null });
|
||||||
}));
|
}));
|
||||||
root.querySelector("#min-videos")?.addEventListener("change", (event) => {
|
|
||||||
replaceQuery({ min_videos: event.target.value, page: null });
|
|
||||||
});
|
|
||||||
root.querySelector("#rank-filter")?.addEventListener("submit", (event) => {
|
root.querySelector("#rank-filter")?.addEventListener("submit", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const value = new FormData(event.target).get("q") || "";
|
const value = new FormData(event.target).get("q") || "";
|
||||||
@@ -205,7 +202,6 @@ async function renderScope(root, { query }, scope) {
|
|||||||
const year = query.get("year") || "";
|
const year = query.get("year") || "";
|
||||||
const growthDays = query.get("growth_days") || "7";
|
const growthDays = query.get("growth_days") || "7";
|
||||||
const q = query.get("q") || "";
|
const q = query.get("q") || "";
|
||||||
const minVideos = query.get("min_videos") || "1";
|
|
||||||
const page = Math.max(1, Number(query.get("page") || 1));
|
const page = Math.max(1, Number(query.get("page") || 1));
|
||||||
|
|
||||||
const overview = await getOverview();
|
const overview = await getOverview();
|
||||||
@@ -222,7 +218,7 @@ async function renderScope(root, { query }, scope) {
|
|||||||
};
|
};
|
||||||
let data;
|
let data;
|
||||||
if (scope === "up") {
|
if (scope === "up") {
|
||||||
data = await api("/api/ups", { ...params, min_videos: minVideos });
|
data = await api("/api/ups", params);
|
||||||
} else {
|
} else {
|
||||||
data = await api("/api/videos", params);
|
data = await api("/api/videos", params);
|
||||||
}
|
}
|
||||||
@@ -235,7 +231,7 @@ async function renderScope(root, { query }, scope) {
|
|||||||
<header class="page-head">
|
<header class="page-head">
|
||||||
<h1>${title} <span class="page-sub">共 ${formatCount(data.total)} ${unit}</span></h1>
|
<h1>${title} <span class="page-sub">共 ${formatCount(data.total)} ${unit}</span></h1>
|
||||||
</header>
|
</header>
|
||||||
${controls({ scope, sort, within, year, growthDays, q, minVideos, years })}
|
${controls({ scope, sort, within, year, growthDays, q, years })}
|
||||||
${durationNote(data)}
|
${durationNote(data)}
|
||||||
${data.available === false ? `<div class="notice-row warning">
|
${data.available === false ? `<div class="notice-row warning">
|
||||||
<strong>${sort.startsWith("growth") ? "增长数据尚不可用" : "榜单筛选后为空"}</strong>
|
<strong>${sort.startsWith("growth") ? "增长数据尚不可用" : "榜单筛选后为空"}</strong>
|
||||||
|
|||||||
Reference in new issue
Block a user