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:
@@ -0,0 +1,176 @@
|
||||
"""
|
||||
后台导航管理视图(通过插件方式开发)
|
||||
|
||||
URL 前缀变更:
|
||||
- 原蓝图 url_prefix='/nav'(注册到 system_bp → /system/nav/...)
|
||||
- 改为 url_prefix='/system/nav' 直接挂到 app,这样去掉了对 framework 内 system_bp 的依赖
|
||||
- 与用户原始菜单路径 /system/nav 完全兼容,权限码 system:nav:* 保持不变
|
||||
"""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, render_template, request
|
||||
from flask_login import current_user
|
||||
|
||||
from applications.common import curd
|
||||
from applications.common.utils.http import table_api, 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.extensions.init_limit import limiter
|
||||
from applications.models import Nav
|
||||
from applications.schemas import NavManageSchema
|
||||
|
||||
# 插件级 Blueprint:直接挂到 app,前缀 /system/nav,独立模板目录
|
||||
dir_path = os.path.dirname(os.path.abspath(__file__))
|
||||
bp = Blueprint(
|
||||
'nav', __name__,
|
||||
url_prefix='/system/nav',
|
||||
template_folder=os.path.join(dir_path, '..', 'templates'),
|
||||
)
|
||||
|
||||
|
||||
@bp.get('/')
|
||||
@authorize("system:nav:main")
|
||||
def main():
|
||||
return render_template('system/nav/main.html')
|
||||
|
||||
|
||||
@bp.get('/data')
|
||||
@limiter.limit("60 per minute")
|
||||
@authorize("system:nav:main")
|
||||
def data():
|
||||
keyword = str_escape(request.args.get('keyword', type=str))
|
||||
category = str_escape(request.args.get('category', type=str))
|
||||
|
||||
query = Nav.query.filter()
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Nav.title.contains(keyword),
|
||||
Nav.url.contains(keyword),
|
||||
Nav.description.contains(keyword),
|
||||
)
|
||||
)
|
||||
if category:
|
||||
query = query.filter(Nav.category == category)
|
||||
|
||||
items = query.order_by(Nav.category.asc(), Nav.sort.asc(), Nav.id.asc()).layui_paginate()
|
||||
return table_api(
|
||||
msg="请求成功",
|
||||
data=curd.model_to_dicts(schema=NavManageSchema, data=items.items),
|
||||
count=items.total,
|
||||
)
|
||||
|
||||
|
||||
@bp.get('/add')
|
||||
@authorize("system:nav:add", log=True)
|
||||
def add():
|
||||
return render_template('system/nav/add.html')
|
||||
|
||||
|
||||
@bp.post('/save')
|
||||
@authorize("system:nav:add", log=True)
|
||||
def save():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
category = str_escape(req_json.get('category'))
|
||||
title = str_escape(req_json.get('title'))
|
||||
url = str_escape(req_json.get('url'))
|
||||
icon = str_escape(req_json.get('icon')) or 'layui-icon-link'
|
||||
|
||||
if not all([category, title, url]):
|
||||
return fail_api(msg="分类/标题/链接不得为空")
|
||||
|
||||
try:
|
||||
nav = Nav(
|
||||
category=category,
|
||||
title=title,
|
||||
url=url,
|
||||
description=str_escape(req_json.get('description')),
|
||||
icon=icon,
|
||||
sort=int(req_json.get('sort') or 0),
|
||||
enable=int(req_json.get('enable') or 1),
|
||||
is_external=int(req_json.get('isExternal') or 1),
|
||||
create_by=current_user.username if current_user.is_authenticated else 'admin',
|
||||
)
|
||||
db.session.add(nav)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return fail_api(msg=f"新增失败:{e}")
|
||||
return success_api(msg="新增成功")
|
||||
|
||||
|
||||
@bp.get('/edit')
|
||||
@authorize("system:nav:edit", log=True)
|
||||
def edit():
|
||||
nav_id = request.args.get('navId', type=int)
|
||||
nav = curd.get_one_by_id(Nav, nav_id)
|
||||
if not nav:
|
||||
return fail_api(msg="导航不存在")
|
||||
return render_template('system/nav/edit.html', nav=nav)
|
||||
|
||||
|
||||
@bp.put('/update')
|
||||
@authorize("system:nav:edit", log=True)
|
||||
def update():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
nav_id = req_json.get('navId')
|
||||
nav = curd.get_one_by_id(Nav, nav_id)
|
||||
if not nav:
|
||||
return fail_api(msg="导航不存在")
|
||||
|
||||
category = str_escape(req_json.get('category'))
|
||||
title = str_escape(req_json.get('title'))
|
||||
url = str_escape(req_json.get('url'))
|
||||
if not all([category, title, url]):
|
||||
return fail_api(msg="分类/标题/链接不得为空")
|
||||
|
||||
try:
|
||||
nav.category = category
|
||||
nav.title = title
|
||||
nav.url = url
|
||||
nav.description = str_escape(req_json.get('description'))
|
||||
nav.icon = str_escape(req_json.get('icon')) or 'layui-icon-link'
|
||||
nav.sort = int(req_json.get('sort') or 0)
|
||||
nav.enable = int(req_json.get('enable') or 1)
|
||||
nav.is_external = int(req_json.get('isExternal') or 1)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return fail_api(msg=f"更新失败:{e}")
|
||||
return success_api(msg="更新成功")
|
||||
|
||||
|
||||
@bp.delete('/remove/<int:nav_id>')
|
||||
@authorize("system:nav:remove", log=True)
|
||||
def remove(nav_id):
|
||||
res = curd.delete_one_by_id(Nav, nav_id)
|
||||
if not res:
|
||||
return fail_api(msg="删除失败")
|
||||
return success_api(msg="删除成功")
|
||||
|
||||
|
||||
@bp.put('/enable')
|
||||
@authorize("system:nav:edit", log=True)
|
||||
def enable():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
nav_id = req_json.get('navId')
|
||||
if not nav_id:
|
||||
return fail_api(msg="数据错误")
|
||||
res = curd.enable_status(Nav, nav_id)
|
||||
if not res:
|
||||
return fail_api(msg="操作失败")
|
||||
return success_api(msg="已启用")
|
||||
|
||||
|
||||
@bp.put('/disable')
|
||||
@authorize("system:nav:edit", log=True)
|
||||
def dis_enable():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
nav_id = req_json.get('navId')
|
||||
if not nav_id:
|
||||
return fail_api(msg="数据错误")
|
||||
res = curd.disable_status(Nav, nav_id)
|
||||
if not res:
|
||||
return fail_api(msg="操作失败")
|
||||
return success_api(msg="已禁用")
|
||||
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
导航分类管理后台视图 — navManager 插件内的子模块。
|
||||
|
||||
URL:/system/nav-category
|
||||
权限:system:nav-category:*
|
||||
"""
|
||||
import os
|
||||
|
||||
from flask import Blueprint, render_template, request
|
||||
from flask_login import current_user
|
||||
|
||||
from applications.common import curd
|
||||
from applications.common.utils.http import table_api, 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.extensions.init_limit import limiter
|
||||
from applications.models import NavCategory
|
||||
from applications.schemas import NavCategorySchema
|
||||
|
||||
dir_path = os.path.dirname(os.path.abspath(__file__))
|
||||
bp = Blueprint(
|
||||
'nav_category', __name__,
|
||||
url_prefix='/system/nav-category',
|
||||
template_folder=os.path.join(dir_path, '..', 'templates'),
|
||||
)
|
||||
|
||||
|
||||
@bp.get('/')
|
||||
@authorize("system:nav-category:main")
|
||||
def main():
|
||||
return render_template('system/nav/category_main.html')
|
||||
|
||||
|
||||
@bp.get('/data')
|
||||
@limiter.limit("60 per minute")
|
||||
@authorize("system:nav-category:main")
|
||||
def data():
|
||||
keyword = str_escape(request.args.get('keyword', type=str))
|
||||
query = NavCategory.query.filter()
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
NavCategory.name.contains(keyword),
|
||||
NavCategory.description.contains(keyword),
|
||||
)
|
||||
)
|
||||
items = query.order_by(NavCategory.sort.desc(), NavCategory.id.asc()).layui_paginate()
|
||||
return table_api(
|
||||
msg='请求成功',
|
||||
data=curd.model_to_dicts(schema=NavCategorySchema, data=items.items),
|
||||
count=items.total,
|
||||
)
|
||||
|
||||
|
||||
@bp.get('/add')
|
||||
@authorize("system:nav-category:add", log=True)
|
||||
def add():
|
||||
return render_template('system/nav/category_add.html')
|
||||
|
||||
|
||||
@bp.post('/save')
|
||||
@authorize("system:nav-category:add", log=True)
|
||||
def save():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
name = str_escape(req_json.get('name'))
|
||||
if not name:
|
||||
return fail_api(msg='分类名不能为空')
|
||||
if NavCategory.query.filter_by(name=name).first():
|
||||
return fail_api(msg=f'分类"{name}"已存在')
|
||||
|
||||
try:
|
||||
cat = NavCategory(
|
||||
name=name,
|
||||
icon=str_escape(req_json.get('icon')) or 'layui-icon-list',
|
||||
description=str_escape(req_json.get('description')),
|
||||
sort=int(req_json.get('sort') or 0),
|
||||
enable=int(req_json.get('enable') or 1),
|
||||
)
|
||||
db.session.add(cat)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return fail_api(msg=f'新增失败:{e}')
|
||||
return success_api(msg='新增成功')
|
||||
|
||||
|
||||
@bp.get('/edit')
|
||||
@authorize("system:nav-category:edit", log=True)
|
||||
def edit():
|
||||
cat_id = request.args.get('catId', type=int)
|
||||
cat = curd.get_one_by_id(NavCategory, cat_id)
|
||||
if not cat:
|
||||
return fail_api(msg='分类不存在')
|
||||
return render_template('system/nav/category_edit.html', cat=cat)
|
||||
|
||||
|
||||
@bp.put('/update')
|
||||
@authorize("system:nav-category:edit", log=True)
|
||||
def update():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
cat_id = req_json.get('catId')
|
||||
cat = curd.get_one_by_id(NavCategory, cat_id)
|
||||
if not cat:
|
||||
return fail_api(msg='分类不存在')
|
||||
name = str_escape(req_json.get('name'))
|
||||
if not name:
|
||||
return fail_api(msg='分类名不能为空')
|
||||
# name 是否被其他行占用
|
||||
other = NavCategory.query.filter(NavCategory.name == name, NavCategory.id != cat_id).first()
|
||||
if other:
|
||||
return fail_api(msg=f'分类"{name}"已被占用')
|
||||
|
||||
try:
|
||||
cat.name = name
|
||||
cat.icon = str_escape(req_json.get('icon')) or 'layui-icon-list'
|
||||
cat.description = str_escape(req_json.get('description'))
|
||||
cat.sort = int(req_json.get('sort') or 0)
|
||||
cat.enable = int(req_json.get('enable') or 1)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return fail_api(msg=f'更新失败:{e}')
|
||||
return success_api(msg='更新成功')
|
||||
|
||||
|
||||
@bp.delete('/remove/<int:cat_id>')
|
||||
@authorize("system:nav-category:remove", log=True)
|
||||
def remove(cat_id):
|
||||
"""删除分类时不会动 Nav 记录,但 Nav.category 字段会孤立成 '未分组' 显示"""
|
||||
cat = curd.get_one_by_id(NavCategory, cat_id)
|
||||
if cat is None:
|
||||
return fail_api(msg='分类不存在')
|
||||
res = curd.delete_one_by_id(NavCategory, cat_id)
|
||||
if not res:
|
||||
return fail_api(msg='删除失败')
|
||||
return success_api(msg='删除成功(分类下导航已自动归入「未分组」)')
|
||||
|
||||
|
||||
@bp.put('/enable')
|
||||
@authorize("system:nav-category:edit", log=True)
|
||||
def enable():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
cat_id = req_json.get('catId')
|
||||
if not cat_id:
|
||||
return fail_api(msg='数据错误')
|
||||
res = curd.enable_status(NavCategory, cat_id)
|
||||
if not res:
|
||||
return fail_api(msg='操作失败')
|
||||
return success_api(msg='已启用')
|
||||
|
||||
|
||||
@bp.put('/disable')
|
||||
@authorize("system:nav-category:edit", log=True)
|
||||
def dis_enable():
|
||||
req_json = request.get_json(force=True, silent=True) or {}
|
||||
cat_id = req_json.get('catId')
|
||||
if not cat_id:
|
||||
return fail_api(msg='数据错误')
|
||||
res = curd.disable_status(NavCategory, cat_id)
|
||||
if not res:
|
||||
return fail_api(msg='操作失败')
|
||||
return success_api(msg='已禁用')
|
||||
Reference in New Issue
Block a user