feat(public): 首页增加「热门网址」分类(访问量 TOP12)

跨分类聚合:按 NavClick 总点击数倒序,取前 12 个有效 nav。

实现要点:
- applications/view/public/nav.py: 新增 _top_navs(limit=12),
  复用 get_nav_visit_counts() 的 60s 进程内缓存;同点击数按 id asc 稳定排序;
  0 点击返回空列表(前端整段不渲染)
- _render_index() 注入 top_navs 到模板
- templates/public/index.html: 在 groups 循环前渲染 .section-hot,标题「🔥 热门网址」,
  meta 显示「近期访问量最大的 N 个」;卡片复用 .card,新增 .card-top 类
- templates/public/base.html:
  - .card-top:左侧 3px 品牌色边框 + 浅色背景渐变 + hover 增强阴影
  - .section-hot h2 图标用 brand-orange #ff6b35
  - .cat-nav-inner a.cat-nav-hot 顶部导航热门入口用品牌色
  - 抽屉:首页 top_navs 非空时插入「🔥 热门」区段,链接到 #cat-top

边界:
- 没有任何点击时整段隐藏(包括抽屉和顶部 nav),不出现「0 个」空标题
- 同一 nav 不会重复(按 nav.id 维度聚合)
- Nav.enable=0 的不会进 TOP12

验证:
- 15 个 nav 各灌入 20..6 次点击,前 12 个按降序显示访问数 20..9
- 清空所有 NavClick 后首页 section-hot / #cat-top / .cat-drawer-hot 三处全不渲染
- 测试数据已清理
This commit is contained in:
bwstudio
2026-09-06 18:52:05 +08:00
parent 2fe59e15ef
commit a47ad991d0
3 changed files with 114 additions and 2 deletions
+29
View File
@@ -85,9 +85,38 @@ def _render_index():
total=total,
site_name='旺珂 · 导航',
cat_meta=_category_meta(),
top_navs=_top_navs(12),
)
def _top_navs(limit: int = 12) -> "list[dict]":
"""跨分类聚合:返回点击数 TOP N 的有效 nav 列表(按点击数降序)。
- 复用 get_nav_visit_counts() 的 60s 进程内缓存
- 0 点击时返回空列表(调用方负责隐藏整个分类块)
- 同一个 nav 不可能重复出现(按 nav.id 维度)
"""
counts = get_nav_visit_counts()
if not counts:
return []
# 取点击数最高的 N 个 nav_id(点击数相同的按 id asc 稳定排序)
top_ids = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:limit]
nav_ids = [nid for nid, _ in top_ids]
rows = (
Nav.query
.filter(Nav.id.in_(nav_ids), Nav.enable == 1)
.all()
)
by_id = {n.id: n.to_dict() for n in rows}
out = []
for nid in nav_ids:
if nid in by_id:
d = by_id[nid]
d['visit_count'] = counts.get(nid, 0)
out.append(d)
return out
def _grouped_navs() -> "OrderedDict[str, list]":
"""按 category 分组,组内按 sort asc 排序;分组顺序优先按 NavCategory.sort 排列"""
# 先取启用的分类,按 sort 排好序
+42
View File
@@ -396,6 +396,32 @@
border-color: var(--brand);
}
.card:active { transform: translateY(0); }
/* ===== 热门网址卡片:与普通卡片区分 ===== */
.section-hot { margin-top: 8px; }
.section-hot .section-header h2 i { color: #ff6b35; margin-right: 2px; }
.card-top {
border-color: rgba(22, 186, 170, 0.35);
background: linear-gradient(135deg, var(--card-bg) 0%, rgba(22, 186, 170, 0.05) 100%);
position: relative;
}
.card-top::before {
content: '';
position: absolute; top: 0; left: 0;
width: 3px; height: 100%;
background: var(--brand);
border-radius: 10px 0 0 10px;
}
.card-top:hover {
border-color: var(--brand);
box-shadow: 0 6px 20px rgba(22, 186, 170, 0.18);
}
.cat-nav-inner a.cat-nav-hot {
color: #ff6b35;
font-weight: 600;
}
.card .icon-wrap {
width: 42px; height: 42px;
background: rgba(22, 186, 170, 0.1);
@@ -770,6 +796,22 @@
<button type="button" class="close" id="cat-drawer-close" aria-label="关闭">×</button>
</div>
{# 热门网址入口(仅首页,且首页必须传 top_navs 才渲染)#}
{% if top_navs is defined and top_navs %}
<div class="cat-drawer-section">
<div class="cat-drawer-title"><i class="layui-icon layui-icon-fire"></i> 热门</div>
<ul class="cat-drawer-list">
<li>
<a href="#cat-top" class="cat-drawer-hot">
<i class="layui-icon cat-icon layui-icon-fire"></i>
<span>热门网址</span>
<span class="arrow"></span>
</a>
</li>
</ul>
</div>
{% endif %}
{# 分类区 #}
{% if cat_meta is defined %}
<div class="cat-drawer-section">
+43 -2
View File
@@ -2,9 +2,10 @@
{# 分类目录条(sticky 顶部导航) #}
{% block cat_nav %}
{% if groups %}
{% if groups or top_navs %}
<nav class="cat-nav" aria-label="分类导航">
<div class="cat-nav-inner">
{% if top_navs %}<a href="#cat-top" class="cat-nav-hot">🔥 热门网址</a>{% endif %}
{% for category in groups.keys() %}
<a href="#cat-{{ loop.index }}">{{ category }}</a>
{% endfor %}
@@ -14,12 +15,52 @@
{% endblock %}
{% block content %}
{% if not groups %}
{% if not groups and not top_navs %}
<div class="empty">
<i class="layui-icon layui-icon-face-surprised" style="font-size:48px;color:#ccc;"></i>
<p>暂无导航数据,请先在后台【系统管理 → 导航管理】添加。</p>
</div>
{% else %}
{# ===== 热门网址:跨分类聚合的 TOP N ===== #}
{% if top_navs %}
<section class="section section-hot" id="cat-top">
<div class="section-header">
<h2><i class="layui-icon layui-icon-fire"></i> 热门网址</h2>
<div class="meta">近期访问量最大的 {{ top_navs|length }} 个</div>
</div>
<div class="grid">
{% for item in top_navs %}
<a class="card card-top"
href="{{ item.url }}"
data-nav-id="{{ item.id }}"
{% if item.is_external %}target="_blank" rel="noopener noreferrer"{% endif %}>
<div class="icon-wrap">
<i class="layui-icon {{ item.icon }}"></i>
</div>
<div class="card-body">
<div class="title-row">
<div class="title" title="{{ item.title }}">{{ item.title }}</div>
{% if item.is_external %}
<span class="ext-tag" title="外部链接"><i class="layui-icon layui-icon-link"></i></span>
{% endif %}
</div>
<div class="desc">{{ item.description or '暂无描述' }}</div>
<div class="url" title="{{ item.url }}">{{ item.url }}</div>
<div class="visit-count" data-nav-visit="{{ item.visit_count or 0 }}">
{%- set vc = item.visit_count or 0 -%}
{%- if vc >= 3 -%}访问 {{ '{:,}'.format(vc) }}
{%- else -%}新
{%- endif -%}
</div>
</div>
<div class="card-arrow"><i class="layui-icon layui-icon-right"></i></div>
</a>
{% endfor %}
</div>
</section>
{% endif %}
{% for category, items in groups.items() %}
{% set cat_id = 'cat-' ~ loop.index %}
<section class="section" id="{{ cat_id }}">