Files
pear-admin-flask/applications/view/public/nav.py
T
bwstudio 0ffc103e46 feat(plugins): 4 个业务插件代码 + 前台公开页面改造
plugins/navManager:导航 CRUD + 导航分类 CRUD(独立 blueprint)
plugins/friendManager:友情链接 CRUD
plugins/aboutManager:关于本站 CRUD
plugins/siteStats:访问统计主页 + JSON 接口 + /site/* GET 埋点 + nav 卡片点击埋点

applications/view/public/nav.py:导航聚合页 / 分类详情 / JSON 接口,支持 visit_count 注入
applications/view/public/about.py:关于本站公开页
applications/view/public/friend.py:友情链接公开页

templates/public/base.html:响应式首页(搜索框 / 分类抽屉 / 5 主题切换 / footer 隐私小字)
templates/public/index.html / category.html:卡片右下角访问次数
templates/public/about.html / friend.html:关于 + 友链前台页

docs/plugins-development.md:插件开发完整指南(生命周期 + 4 个示例 + FAQ + framework 迁移)

scripts/:探针与端到端验证脚本(probe_*/verify_*/test_*)
2026-09-06 18:18:06 +08:00

130 lines
4.0 KiB
Python

"""
前台导航页视图(无需登录)。
URL 路径(挂在根路径下,便于匿名访问):
- GET /site 公开导航聚合首页(按分类展示所有启用的导航)
- GET /site/category/<name> 分类详情:展示指定分类下所有启用的导航
- 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
bp = Blueprint('public_nav', __name__, url_prefix='/site')
@bp.get('/')
def index():
"""
公开首页:按 category 分组聚合,渲染模板
"""
return _render_index()
@bp.get('/category/<string:name>')
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())
return render_template(
'public/index.html',
groups=groups,
total=total,
site_name='旺珂 · 导航',
cat_meta=_category_meta(),
)
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}