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_*)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
前台公开蓝图:导航聚合页 / 分类详情页
|
||||
前台公开蓝图:导航聚合页 / 分类详情页 / 关于本站
|
||||
|
||||
URL 前缀:/site(避免与后台 / 路由冲突)
|
||||
鉴权要求:无(任何用户都可访问,含未登录匿名用户)
|
||||
@@ -7,7 +7,15 @@ URL 前缀:/site(避免与后台 / 路由冲突)
|
||||
注意:不要给这些视图加 @authorize 装饰器,否则会强制登录_required。
|
||||
"""
|
||||
from applications.view.public.nav import bp as nav_bp
|
||||
from applications.view.public.about import bp as about_bp
|
||||
from applications.view.public.friend import bp as friend_bp
|
||||
|
||||
|
||||
# 把 nav 蓝图作为 public 包的对外入口
|
||||
bp = nav_bp
|
||||
bp = nav_bp
|
||||
|
||||
|
||||
def register_public_bp(app):
|
||||
"""注册 public 子蓝图"""
|
||||
app.register_blueprint(about_bp)
|
||||
app.register_blueprint(friend_bp)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
前台公开"关于本站"页面。
|
||||
|
||||
不需要登录,匿名可访问。Markdown 在模板里渲染。
|
||||
"""
|
||||
import re
|
||||
|
||||
import markdown as _markdown
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
from applications.models import About
|
||||
|
||||
bp = Blueprint('public_about', __name__, url_prefix='/site')
|
||||
|
||||
|
||||
def _get_about() -> About:
|
||||
"""复用 model 的种子逻辑,避免空表时炸"""
|
||||
a = About.query.get(1)
|
||||
if a is None:
|
||||
from applications.extensions import db
|
||||
a = About(id=1, site_name='旺珂 · 导航')
|
||||
db.session.add(a)
|
||||
db.session.commit()
|
||||
return a
|
||||
|
||||
|
||||
@bp.get('/about')
|
||||
def about():
|
||||
a = _get_about()
|
||||
html = _render_md(a.content_md)
|
||||
return render_template('public/about.html', about=a, content_html=html)
|
||||
|
||||
|
||||
_URL_RE = re.compile(r'(https?://[^\s)<>"\\]+|[\w.+-]+@[\w-]+\.[\w.-]+)')
|
||||
|
||||
|
||||
def _render_md(text: str) -> str:
|
||||
"""Markdown → HTML;自动把裸链接/邮箱变可点;同时禁掉 script/iframe。"""
|
||||
if not text or not text.strip():
|
||||
return ''
|
||||
html = _markdown.markdown(
|
||||
text,
|
||||
extensions=['fenced_code', 'tables', 'nl2br', 'sane_lists'],
|
||||
output_format='html5',
|
||||
)
|
||||
# 防御:清掉 <script>/<iframe>
|
||||
html = re.sub(r'<\s*script[^>]*>.*?<\s*/\s*script\s*>', '', html, flags=re.I | re.S)
|
||||
html = re.sub(r'<\s*iframe[^>]*>.*?<\s*/\s*iframe\s*>', '', html, flags=re.I | re.S)
|
||||
return html
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
前台友情链接页视图(无需登录)。
|
||||
|
||||
URL:
|
||||
- GET /site/friend 按分组聚合展示所有启用友情链接
|
||||
|
||||
注:不是 /site/friends(避免与系统内部 hyperlink 对齐)
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
from applications.models import Friend
|
||||
|
||||
bp = Blueprint('public_friend', __name__, url_prefix='/site')
|
||||
|
||||
|
||||
@bp.get('/friend')
|
||||
def friend_index():
|
||||
"""
|
||||
公开友情链接页:按 category 分组聚合展示
|
||||
"""
|
||||
items = (
|
||||
Friend.query
|
||||
.filter(Friend.enable == 1)
|
||||
.order_by(Friend.category.asc(), Friend.sort.asc(), Friend.id.asc())
|
||||
.all()
|
||||
)
|
||||
groups = OrderedDict()
|
||||
for item in items:
|
||||
if not item.url:
|
||||
continue
|
||||
groups.setdefault(item.category or '推荐友链', []).append(item.to_dict())
|
||||
|
||||
total = sum(len(g) for g in groups.values())
|
||||
return render_template(
|
||||
'public/friend.html',
|
||||
groups=groups,
|
||||
total=total,
|
||||
site_name='旺珂 · 导航',
|
||||
)
|
||||
@@ -14,7 +14,8 @@ from collections import OrderedDict
|
||||
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
|
||||
from applications.models import Nav
|
||||
from applications.models import Nav, NavCategory
|
||||
from plugins.siteStats.view.stat import get_nav_visit_counts
|
||||
|
||||
bp = Blueprint('public_nav', __name__, url_prefix='/site')
|
||||
|
||||
@@ -24,14 +25,7 @@ def index():
|
||||
"""
|
||||
公开首页:按 category 分组聚合,渲染模板
|
||||
"""
|
||||
groups = _grouped_navs()
|
||||
total = sum(len(items) for items in groups.values())
|
||||
return render_template(
|
||||
'public/index.html',
|
||||
groups=groups,
|
||||
total=total,
|
||||
site_name='BWStudio · 导航',
|
||||
)
|
||||
return _render_index()
|
||||
|
||||
|
||||
@bp.get('/category/<string:name>')
|
||||
@@ -41,15 +35,21 @@ def category_detail(name: str):
|
||||
"""
|
||||
items = (
|
||||
Nav.query
|
||||
.filter(Nav.category == name, Nav.status == 1)
|
||||
.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=items,
|
||||
site_name='BWStudio · 导航',
|
||||
items=dict_items,
|
||||
site_name='旺珂 · 导航',
|
||||
)
|
||||
|
||||
|
||||
@@ -66,15 +66,65 @@ def api_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 排序"""
|
||||
"""按 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.status == 1)
|
||||
.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:
|
||||
groups.setdefault(item.category, []).append(item.to_dict())
|
||||
return groups
|
||||
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}
|
||||
Reference in New Issue
Block a user