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:
bwstudio
2026-09-06 18:18:06 +08:00
parent 1ea69c2c34
commit 0ffc103e46
53 changed files with 4705 additions and 89 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"plugin_name": "关于本站",
"plugin_version": "1.0.0",
"plugin_description": "关于本站后台编辑页与保存接口。表 site_about 单条记录。"
}
+9
View File
@@ -0,0 +1,9 @@
"""关于本站插件入口"""
from flask import Flask
from .view.about import bp
def event_init(app: Flask):
app.register_blueprint(bp)
print(" * aboutManager: registered /system/about/* blueprint")
@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>关于本站</title>
{% include 'system/common/header.html' %}
</head>
<body class="pear-container">
<div class="layui-card">
<div class="layui-card-header">
<span class="layui-icon layui-icon-about"></span> 关于本站
<span style="float:right;font-size:12px;color:#999;margin-top:5px;">
共 1 条记录(站点全局唯一,修改即时生效)
</span>
</div>
<div class="layui-card-body">
<form class="layui-form" lay-filter="about-form" style="padding-right: 20px;">
{{ csrf_input()|safe }}
<div class="layui-form-item">
<label class="layui-form-label">站点名</label>
<div class="layui-input-block">
<input type="text" name="siteName" value="{{ about.site_name or '' }}"
placeholder="公开页显示的标题" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">自我介绍</label>
<div class="layui-input-block">
<textarea name="contentMd" placeholder="支持 Markdown 语法(# 标题、- 列表、** 加粗**、` 代码、> 引用……)"
class="layui-textarea" rows="10"
style="font-family: Consolas, 'Microsoft YaHei', monospace;">{{ about.content_md or '' }}</textarea>
</div>
<div class="layui-form-mid layui-word-aux">
前台 /site/about 会用 Markdown 渲染,提交空内容则显示默认占位文字
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">ICP 备案</label>
<div class="layui-input-block">
<input type="text" name="icp" value="{{ about.icp or '' }}"
placeholder="如 京ICP备12345678号" autocomplete="off" class="layui-input">
</div>
</div>
<fieldset class="layui-elem-field" style="margin: 20px 0; padding: 0 20px;">
<legend>联系方式</legend>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">邮箱</label>
<div class="layui-input-inline">
<input type="email" name="contactEmail" value="{{ about.contact_email or '' }}"
placeholder="hi@example.com" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">QQ</label>
<div class="layui-input-inline">
<input type="text" name="contactQq" value="{{ about.contact_qq or '' }}"
placeholder="12345678" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">微信</label>
<div class="layui-input-inline">
<input type="text" name="contactWechat" value="{{ about.contact_wechat or '' }}"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">Telegram</label>
<div class="layui-input-inline">
<input type="text" name="contactTelegram" value="{{ about.contact_telegram or '' }}"
placeholder="@your_id 或 https://t.me/xxx" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">GitHub</label>
<div class="layui-input-block">
<input type="url" name="contactGithub" value="{{ about.contact_github or '' }}"
placeholder="https://github.com/yourname" autocomplete="off" class="layui-input">
</div>
</div>
</fieldset>
<div class="layui-form-item">
<label class="layui-form-label">打赏/捐赠</label>
<div class="layui-input-block">
<input type="text" name="donateUrl" value="{{ about.donate_url or '' }}"
placeholder="可填二维码图片地址 或 PayPal/支付宝收款链接" autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">
前台 /site/about 会在底部显示
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn" lay-submit lay-filter="about-submit">
<i class="layui-icon layui-icon-ok"></i> 保存
</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
<a href="/site/about" target="_blank" class="layui-btn layui-btn-warm" style="margin-left: 16px;">
<i class="layui-icon layui-icon-link"></i> 查看前台效果
</a>
</div>
</div>
</form>
</div>
</div>
</body>
{% include 'system/common/footer.html' %}
<script>
layui.use(['form', 'layer'], function(){
var form = layui.form;
var layer = layui.layer;
var $ = layui.$;
// 隐藏字段随 Layui form 一起序列化
form.on('submit(about-submit)', function(data){
// data.field 已经包含 csrf_token(隐藏域)+ 所有可见字段
$.ajax({
url: '/system/about/save',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data.field),
success: function(res){
if (res.success) {
layer.msg(res.msg, {icon: 1});
} else {
layer.msg(res.msg, {icon: 2});
}
},
error: function(){
layer.msg('保存失败,请稍后再试', {icon: 2});
}
});
return false;
});
});
</script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
"""
「关于本站」后台管理:编辑单条记录 (id=1)。
插件版。
"""
import os
from flask import Blueprint, render_template, request
from flask_wtf.csrf import validate_csrf
from wtforms.validators import ValidationError
from applications.common.utils.http import fail_api, success_api
from applications.common.utils.rights import authorize
from applications.common.utils.validate import str_escape
from applications.extensions import db
from applications.models import About
dir_path = os.path.dirname(os.path.abspath(__file__))
bp = Blueprint(
'about', __name__,
url_prefix='/system/about',
template_folder=os.path.join(dir_path, '..', 'templates'),
)
def _get_about() -> About:
a = About.query.get(1)
if a is None:
a = About(id=1, site_name='旺珂 · 导航')
db.session.add(a)
db.session.commit()
return a
@bp.get('/')
@authorize("site:about:main")
def main():
about = _get_about()
return render_template('system/about/main.html', about=about)
@bp.post('/save')
@authorize("site:about:main", log=True)
def save():
req_json = request.get_json(force=True, silent=True) or {}
try:
validate_csrf(req_json.get("csrf_token"))
except ValidationError:
return fail_api(msg='非法请求')
a = _get_about()
fields = {
'site_name': (req_json.get('siteName') or '').strip()[:120],
'content_md': (req_json.get('contentMd') or '').strip(),
'icp': (req_json.get('icp') or '').strip()[:120],
'contact_email': (req_json.get('contactEmail') or '').strip()[:120],
'contact_qq': (req_json.get('contactQq') or '').strip()[:32],
'contact_wechat': (req_json.get('contactWechat') or '').strip()[:120],
'contact_telegram': (req_json.get('contactTelegram') or '').strip()[:120],
'contact_github': (req_json.get('contactGithub') or '').strip()[:255],
'donate_url': (req_json.get('donateUrl') or '').strip()[:255],
}
for k, v in fields.items():
setattr(a, k, v)
db.session.commit()
return success_api(msg='保存成功')