""" 前台导航页视图(无需登录)。 URL 路径(挂在根路径下,便于匿名访问): - GET /site 公开导航聚合首页(按分类展示所有启用的导航) - GET /site/category/ 分类详情:展示指定分类下所有启用的导航 - GET /site/api/navs JSON 接口(供前端动态加载) 注意: - 这些视图**不加** @authorize,避免 login_required 强制登录。 - 使用 url_prefix='/site' 避免与后台 / 路由冲突(后台 / 是登录后的工作台)。 """ from collections import OrderedDict from flask import Blueprint, render_template, request, jsonify from applications.models import Nav, NavCategory from plugins.siteStats.view.stat import get_nav_visit_counts, my_top_navs # 「我常访问」冷启动阈值:至少点过这么多个不同网址才展示个人区 MINE_MIN_ITEMS = 2 bp = Blueprint('public_nav', __name__, url_prefix='/site') @bp.get('/') def index(): """ 公开首页:按 category 分组聚合,渲染模板 """ return _render_index() @bp.get('/category/') def category_detail(name: str): """ 分类详情页:列出指定分类下所有启用的导航 """ items = ( Nav.query .filter(Nav.category == name, Nav.enable == 1) .order_by(Nav.sort.asc(), Nav.id.asc()) .all() ) counts = get_nav_visit_counts() dict_items = [] for it in items: d = it.to_dict() d['visit_count'] = counts.get(it.id, 0) dict_items.append(d) return render_template( 'public/category.html', category=name, items=dict_items, site_name='旺珂 · 导航', ) @bp.get('/api/navs') def api_navs(): """ JSON 接口:返回按分类聚合的导航数据 可用于首页卡片动态加载、第三方嵌入等 """ return jsonify({ 'success': True, 'msg': '请求成功', 'data': _grouped_navs(), }) def render_public_index(): """给外部复用:渲染前台公开导航首页(无需登录)。""" return _render_index() def get_category_meta() -> "dict[str, dict]": """给外部复用:返回启用的分类元信息。""" return _category_meta() def _render_index(): groups = _grouped_navs() total = sum(len(items) for items in groups.values()) mine = my_top_navs(8) return render_template( 'public/index.html', groups=groups, total=total, site_name='旺珂 · 导航', cat_meta=_category_meta(), top_navs=_top_navs(12), my_navs=mine if len(mine) >= MINE_MIN_ITEMS else [], ) 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 排好序 cats = ( NavCategory.query .filter(NavCategory.enable == 1) .order_by(NavCategory.sort.desc(), NavCategory.id.asc()) .all() ) ordered_names = [c.name for c in cats] cat_order = {name: idx for idx, name in enumerate(ordered_names)} items = ( Nav.query .filter(Nav.enable == 1) .order_by(Nav.category.asc(), Nav.sort.asc(), Nav.id.asc()) .all() ) # 一次性取所有 nav 的点击数(带缓存) counts = get_nav_visit_counts() groups = OrderedDict() # 按 cat_order 初始化空分组,保证顺序 for name in ordered_names: groups[name] = [] for item in items: d = item.to_dict() d['visit_count'] = counts.get(item.id, 0) if item.category in groups: groups[item.category].append(d) else: # 对于未在 NavCategory 登记的历史分类,放在最后 groups.setdefault(item.category, []).append(d) # 过滤空分组(启用的分类下可能没有导航) return OrderedDict((k, v) for k, v in groups.items() if v) def _category_meta() -> "dict[str, dict]": """返回分类元信息 {name: {icon, description, ...}},用于抽屉图标等""" cats = NavCategory.query.filter(NavCategory.enable == 1).all() return {c.name: {'icon': c.icon or 'layui-icon-list', 'description': c.description or ''} for c in cats}