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_*)
525 lines
19 KiB
Markdown
525 lines
19 KiB
Markdown
# Pear Admin Flask 插件开发操作手册
|
||
|
||
> 适用版本:Pear Admin Flask master 分支(fork 自 lab.lovepikachu.top 文档站点的最新文档)
|
||
>
|
||
> 文档维护:根据 plugins/<name>/ 下的实际插件实现同步更新
|
||
|
||
Pear Admin Flask 的插件机制是其核心竞争力——**不修改框架代码即可扩展新页面、新菜单、新数据埋点**。
|
||
|
||
本文以仓库中已落地的 `navManager` / `friendManager` / `aboutManager` / `siteStats` 四个插件为依据,整理出从设计到上线的完整操作步骤。
|
||
|
||
---
|
||
|
||
## 1. 插件是什么
|
||
|
||
| 概念 | framework(框架) | plugin(插件) |
|
||
|---|---|---|
|
||
| 代码位置 | `applications/`、`templates/system/` 之外的共用代码 | `plugins/<pluginName>/` 下,与 framework 解耦 |
|
||
| 启用方式 | 自动加载 | 在 `applications/config.py` 的 `PLUGIN_ENABLE_FOLDERS` 列表中显式声明 |
|
||
| 修改影响 | 改 framework 会影响所有升级 | 改 plugin 不影响 framework 与其他 plugin |
|
||
| 典型用途 | 用户/角色/权限/部门等所有项目共用的基础模块 | 站点特有业务(导航、友链、关于、统计等) |
|
||
|
||
---
|
||
|
||
## 2. 目录结构最小模型
|
||
|
||
一个最简插件的目录是这样:
|
||
|
||
```
|
||
plugins/
|
||
└── helloworld/ # 插件文件夹名(与 PLUGIN_ENABLE_FOLDERS 中写的保持一致)
|
||
├── __init__.json # 【可选】插件元信息(名称/版本/描述),删掉也能加载
|
||
├── __init__.py # 必须。定义 event_init / event_finish 等事件函数
|
||
└── view/ # 建议。存放 view.py(Blueprint 定义和路由)
|
||
└── main.py # 自定义视图
|
||
|
||
# 复杂插件(仿 navManager/friendManager/aboutManager/siteStats):
|
||
plugins/
|
||
└── navManager/
|
||
├── __init__.json
|
||
├── __init__.py # 入口
|
||
├── view/
|
||
│ └── nav.py # Blueprint 'nav',url_prefix='/system/nav'
|
||
└── templates/
|
||
└── system/
|
||
└── nav/
|
||
├── main.html
|
||
├── add.html
|
||
└── edit.html
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 启用插件
|
||
|
||
打开 `applications/config.py`,找到 `BaseConfig` 类尾部的配置项:
|
||
|
||
```python
|
||
# ---- 插件 ----
|
||
# 站点业务模块(导航 / 友链 / 关于 / 访问统计)均通过 plugins 方式开发,启用即可挂载路由
|
||
PLUGIN_ENABLE_FOLDERS = ["navManager", "friendManager", "aboutManager", "siteStats"]
|
||
```
|
||
|
||
> 顺序就是加载顺序,越靠前越早被 import;按需新增或删除。
|
||
|
||
修改完成后,**重启 Flask** 即可生效,启动日志会列出:
|
||
|
||
```
|
||
* Plugin: Loaded plugin: 导航管理 .
|
||
* Plugin: Loaded plugin: 友情链接 .
|
||
* Plugin: Loaded plugin: 关于本站 .
|
||
* Plugin: Loaded plugin: 访问统计 .
|
||
* navManager: registered /system/nav/* blueprint
|
||
* friendManager: registered /system/friend/* blueprint
|
||
* aboutManager: registered /system/about/* blueprint
|
||
* siteStats: registered /system/stats/* blueprint
|
||
* siteStats: registered /site/* before_request hook
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 插件生命周期(4 个事件)
|
||
|
||
插件入口 `__init__.py` 中可以暴露 4 个事件函数(任意函数缺省将自动跳过):
|
||
|
||
| 函数 | 时机 | 典型用途 |
|
||
|---|---|---|
|
||
| `event_begin(app)` | 框架功能注册之前(数据库连上之前) | 极少数兼容场景;多数插件不需要 |
|
||
| `event_init(app)` | 所有 framework 蓝图注册之后 | `app.register_blueprint(bp)` |
|
||
| `event_finish(app)` | 数据库/CLI/脚本初始化完毕之后 | 注册 `@app.before_request` 钩子、`@app.cli.command` |
|
||
| `event_context(app)` | 第一个请求到来之前(自动 `app.app_context()`) | 预热缓存、检查/修正数据库初始数据 |
|
||
|
||
调用顺序:`event_begin` → ... framework 注册 ... → `event_init` → ... 启动余下逻辑 ... → `event_finish` → (自动)`event_context`。
|
||
|
||
**注意**:如果同时定义 `event_init` 和 `event_finish`,前者注册蓝图、后者挂 before_request / CLI,**不能**用一次事件全做——否则会与 framework 自身的广播节奏冲突。
|
||
|
||
---
|
||
|
||
## 5. 最小可运行示例:Hello World(来自仓库 plugins/helloworld/)
|
||
|
||
### 5.1 `plugins/helloworld/__init__.py`
|
||
|
||
```python
|
||
import os
|
||
from flask import Flask
|
||
from .main import helloworld_blueprint
|
||
|
||
from applications.models import Dept
|
||
from applications.common import curd
|
||
from applications.schemas import DeptSchema
|
||
|
||
dir_path = os.path.dirname(__file__).replace("\\", "/")
|
||
|
||
|
||
def event_begin(app: Flask):
|
||
print("所有功能初始化之前加载")
|
||
|
||
|
||
def event_init(app: Flask):
|
||
"""初始化完成时会调用这里"""
|
||
print("初始插件初始化视图")
|
||
app.register_blueprint(helloworld_blueprint)
|
||
|
||
|
||
def event_finish(app: Flask):
|
||
print("所有初始化完毕")
|
||
|
||
|
||
def event_context(app: Flask):
|
||
"""Flask 初始化完成,等待第一个请求之前,等同于 with app.app_context():"""
|
||
dept = Dept.query.order_by(Dept.sort).all()
|
||
print(curd.model_to_dicts(schema=DeptSchema, data=dept))
|
||
```
|
||
|
||
### 5.2 `plugins/helloworld/main.py`
|
||
|
||
```python
|
||
from flask import render_template, Blueprint
|
||
|
||
helloworld_blueprint = Blueprint('hello_world', __name__,
|
||
template_folder='templates',
|
||
static_folder='static',
|
||
url_prefix='/hello_world')
|
||
|
||
|
||
@helloworld_blueprint.route('/')
|
||
def index():
|
||
return render_template('helloworld_index.html')
|
||
```
|
||
|
||
### 5.3 启用
|
||
|
||
```python
|
||
PLUGIN_ENABLE_FOLDERS = ["helloworld"]
|
||
```
|
||
|
||
启动后访问 `http://127.0.0.1:5000/hello_world/`。
|
||
|
||
---
|
||
|
||
## 6. 含数据库的完整插件(实操示例:navManager)
|
||
|
||
这一节以仓库实际存在的 `navManager` 为模板,演示创建"导航表 + 后台 CRUD + 模板 + 权限码绑定 + 菜单种子"完整业务插件的全流程。
|
||
|
||
### 6.1 设计
|
||
|
||
- 数据模型:单表 `site_nav`(继承自 `applications.models.admin_nav.Nav`)
|
||
- 路由:`/system/nav/`、`/system/nav/data`、`/system/nav/add`、`/system/nav/save`、`/system/nav/edit`、`/system/nav/update`、`/system/nav/remove/<int:nav_id>`、`/system/nav/enable`、`/system/nav/disable`
|
||
- 权限码:`system:nav:main / system:nav:add / system:nav:edit / system:nav:remove`
|
||
|
||
### 6.2 创建插件骨架
|
||
|
||
```bash
|
||
mkdir -p plugins/navManager/{view,templates/system/nav}
|
||
```
|
||
|
||
### 6.3 写 `plugins/navManager/__init__.json`
|
||
|
||
```json
|
||
{
|
||
"plugin_name": "导航管理",
|
||
"plugin_version": "1.0.0",
|
||
"plugin_description": "后台导航管理模块。对 nav 表的增删改查、分页查询、关键字 + 分类过滤、启用/禁用切换。"
|
||
}
|
||
```
|
||
|
||
### 6.4 写 `plugins/navManager/__init__.py`
|
||
|
||
```python
|
||
from flask import Flask
|
||
from .view.nav import bp
|
||
|
||
|
||
def event_init(app: Flask):
|
||
"""初始化完成时注册蓝图到 app。"""
|
||
app.register_blueprint(bp)
|
||
print(" * navManager: registered /system/nav/* blueprint")
|
||
```
|
||
|
||
### 6.5 写 `plugins/navManager/view/nav.py`
|
||
|
||
最关键的一步——要把 Blueprint 的 `url_prefix` 拼成 `/system/nav` 并指定独立 `template_folder`:
|
||
|
||
```python
|
||
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
|
||
|
||
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,
|
||
)
|
||
# ... 其余 CRUD 路由基本按 user.py 风格展开
|
||
```
|
||
|
||
**⚠️ 与 framework 视图的关键区别**:
|
||
- `Blueprint` 的 `url_prefix` 直接是 `/system/nav`,**不再**注册到 framework 的 `system_bp`,避免对 framework 文件的依赖
|
||
- `template_folder` 显式指向 plugin 自带的 `templates/`,与根 `templates/` 解耦
|
||
|
||
### 6.6 数据库 Schema
|
||
|
||
模型**仍然需要放在 framework 的 `applications/models/admin_nav.py`**,因为 Flask 在 `init_databases(app)` 时只 import 这一目录,而且 Alembic 也只看这里做迁移:
|
||
|
||
```python
|
||
# applications/models/admin_nav.py
|
||
class Nav(db.Model):
|
||
__tablename__ = 'site_nav'
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
...
|
||
```
|
||
|
||
> 这点是 Pear Admin Flask 的设计局限:模型层不能放到 plugin。Alembic migration 也需要落到 `migrations/versions/` 下。
|
||
|
||
### 6.7 模板文件
|
||
|
||
把模板放到 `plugins/navManager/templates/system/nav/main.html`:
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>导航管理</title>
|
||
{% include 'system/common/header.html' %}
|
||
</head>
|
||
<body class="pear-container">
|
||
<!-- 查询表单 + 表格 -->
|
||
</body>
|
||
</body>
|
||
{% include 'system/common/footer.html' %}
|
||
{% raw %}
|
||
<script type="text/html" id="col-status">...</script>
|
||
{% endraw %}
|
||
<script>
|
||
layui.use(['table', 'form', 'layer'], function () {
|
||
var table = layui.table;
|
||
table.render({
|
||
elem: '#nav-table',
|
||
url: '/system/nav/data',
|
||
cols: [[...]],
|
||
page: true, limit: 20,
|
||
skin: 'line',
|
||
text: {none: '暂无导航数据'},
|
||
});
|
||
});
|
||
</script>
|
||
```
|
||
|
||
**注意 footer include 必须放在所有业务 `<script>` 之前**(这是 Pear Admin 2.x 同步加载的特性,错误顺序会 `layui is not defined`)。
|
||
|
||
### 6.8 菜单种子(Power 表)
|
||
|
||
`site_nav` 表的菜单项需要 seed 到 RBAC 的 `Power` 表:
|
||
|
||
```python
|
||
Power(
|
||
id=60, name='导航管理', type='1',
|
||
code='system:nav:main',
|
||
url='/system/nav/', open_type='_iframe',
|
||
parent_id=64, icon='layui-icon layui-icon-link',
|
||
enable=1, sort=1,
|
||
),
|
||
Power(
|
||
name='导航新增', type='2', code='system:nav:add', ...
|
||
),
|
||
# 编辑 / 删除 同理
|
||
```
|
||
|
||
子权限的 `parent_id` 必须指向主菜单的 id;Python seed 时可先 commit 主菜单拿到 id。
|
||
|
||
### 6.9 启用
|
||
|
||
```python
|
||
PLUGIN_ENABLE_FOLDERS = ["navManager"]
|
||
```
|
||
|
||
启动 Flask 后访问 `http://127.0.0.1:5000/system/nav/`(仍需要登录),可见后台菜单「站点管理 / 导航管理」。
|
||
|
||
---
|
||
|
||
## 7. 含 before_request 钩子的插件(实操示例:siteStats)
|
||
|
||
类似访问统计这种需要监听全局请求的插件,用 `event_finish` 钩子:
|
||
|
||
```python
|
||
# plugins/siteStats/__init__.py
|
||
from flask import Flask
|
||
from .view.stat import bp, record_visit
|
||
|
||
|
||
def event_init(app: Flask):
|
||
app.register_blueprint(bp)
|
||
|
||
|
||
def event_finish(app: Flask):
|
||
@app.before_request
|
||
def _stat_record():
|
||
record_visit()
|
||
```
|
||
|
||
```python
|
||
# plugins/siteStats/view/stat.py
|
||
def record_visit():
|
||
"""埋点 /site/* GET 请求。"""
|
||
if not request.path.startswith('/site'):
|
||
return
|
||
if request.method != 'GET':
|
||
return
|
||
# ... 累计 PV/UV 到 PageStat
|
||
```
|
||
|
||
**优势**:未来增加新的"统计规则"或"埋点路径"只需要改 plugin 文件,不动 framework 的 `app.py`。
|
||
|
||
---
|
||
|
||
## 8. 含自定义 CLI 子命令的插件(实操示例:giftManager)
|
||
|
||
若插件需要自定义 `flask <your-cmd>` 命令:
|
||
|
||
```python
|
||
# plugins/giftManager/cli/__init__.py
|
||
from flask.cli import AppGroup
|
||
|
||
gift_cli = AppGroup('gift')
|
||
|
||
|
||
@gift_cli.command('init')
|
||
def init_db():
|
||
"""初始化兑换码管理插件的菜单和种子数据"""
|
||
# seed Power / seed Gift data
|
||
pass
|
||
```
|
||
|
||
```python
|
||
# plugins/giftManager/__init__.py
|
||
from flask import Flask
|
||
from .cli import gift_cli
|
||
from .view.gift import bp
|
||
|
||
|
||
def event_init(app: Flask):
|
||
app.register_blueprint(bp)
|
||
|
||
|
||
def event_finish(app: Flask):
|
||
app.cli.add_command(gift_cli)
|
||
```
|
||
|
||
执行 `flask gift init` 即可。
|
||
|
||
---
|
||
|
||
## 9. 插件开发检查清单
|
||
|
||
下面是一份自检表,开发完成后逐项确认:
|
||
|
||
- [ ] `plugins/<name>/__init__.json` 写好
|
||
- [ ] `plugins/<name>/__init__.py` 定义 `event_init`(注册蓝图)或/和 `event_finish`(before_request / CLI)
|
||
- [ ] 视图文件 `view/<name>.py` 中 Blueprint 的 `name` 全项目唯一;`url_prefix` 在 `/system/<xxx>` 或自定其它前缀
|
||
- [ ] 蓝图 `template_folder` 显式指向 plugin 内 templates,避免与 framework 模板同名冲突
|
||
- [ ] 模型文件保留在 `applications/models/admin_<name>.py`(Pear Admin 限制)
|
||
- [ ] Schema 保留在 `applications/schemas/admin_<name>.py`
|
||
- [ ] 权限码 seed 已写入 Power 表(type=1 主菜单 + type=2 子权限)
|
||
- [ ] 管理员 role 的 power 列表中已挂载本插件相关权限码
|
||
- [ ] `applications/config.py` 中 `PLUGIN_ENABLE_FOLDERS` 加上插件文件夹名
|
||
- [ ] framework 中**没有**与该插件同名的 Blueprint 注册(避免 endpoint 冲突)
|
||
- [ ] 模板放在 `{% include 'system/common/footer.html' %}` **之前** + `<script>layui.use(...)` **之后** 仍然运行正常
|
||
- [ ] 用 Playwright/Selenium 验证一次页面渲染(控制台无 `layui is not defined`)
|
||
- [ ] alembic migration 已生成(如果模型新增列):`flask db migrate -m "..."` + `flask db upgrade`
|
||
|
||
---
|
||
|
||
## 10. 常见问题(FAQ)
|
||
|
||
### Q1:出现 `ValueError: The name 'xxx' is already registered for this blueprint`
|
||
答:framework 里有个同名 Blueprint 已注册 + 当前 plugin 又注册了同名 Blueprint。检查:
|
||
1. `applications/view/system/__init__.py` 的 `register_system_bps` 是否仍包含迁移前的 bp 注册
|
||
2. `extensions/__init__.py:33-34` 早期版本同时调用了 `event_init` / `event_finish`,后又由 `init_bps` / `init_script` 重复触发——必须删除其中一处(建议只在 `view/__init__.py` 触发 `event_init`,只在 `script/__init__.py` 触发 `event_finish`)。
|
||
|
||
### Q2:`event_finish` 调用了两次,before_request 钩子被注册两次
|
||
答:同 Q1,问题来源 + 修复同。
|
||
|
||
### Q3:菜单在前台后台不显示 / 点菜单 403
|
||
答:检查 `applications/models/admin_power.py` 的 Power 表里菜单的 `code`、`parent_id` 是否正确;管理员 role 的 power 关联表里是否绑定了相关 code。
|
||
|
||
### Q4:模板渲染找不到文件 `TemplateNotFound`
|
||
答:检查 Blueprint 实例化时是否显式设置 `template_folder=os.path.join(dir_path, '..', 'templates')`,不要省略。
|
||
|
||
### Q5:插件导入顺序错误(a 插件 import 时触发 b 插件未加载)
|
||
答:把"a"放 `PLUGIN_ENABLE_FOLDERS` 更前面。Pear Admin 的 loader 通过 `importlib.import_module` 顺序装载。
|
||
|
||
### Q6:插件里如何获取当前请求
|
||
答:直接 `request` 对象或 `flask.session`;访问数据库用 `with app.app_context():` 或在 `event_context` 函数体内。
|
||
|
||
### Q7:插件改动后必须重启 Flask 吗?
|
||
答:是的。所有 `__init__.py`、`event_*` 函数、Blueprint 注册只发生在进程启动时。模板可用 `TEMPLATES_AUTO_RELOAD=True` 动态生效。
|
||
|
||
---
|
||
|
||
## 11. 插件模式 vs framework 内置
|
||
|
||
| 维度 | plugin 内置 | framework 内置 |
|
||
|---|---|---|
|
||
| 升级 pear-admin-framework 后是否受影响 | 无 | 有 |
|
||
| 删除即下线的便利性 | 注释 PLUGIN_ENABLE_FOLDERS 即可 | 改 framework 才能下 |
|
||
| 团队协作时移植到新项目 | 直接复制 plugins/ 整目录 | diff 框架代码 |
|
||
| 适合场景 | 大多数业务模块 | 不变的用户/角色/权限/部门等基础模块 |
|
||
|
||
**结论**:除非改动涉及 framework 自身的核心能力(如重写 `@authorize` 实现),否则一律走 plugin。这正是 Pear Admin Flask 推荐的最佳实践。
|
||
|
||
---
|
||
|
||
## 12. 当前仓库已实现的插件
|
||
|
||
| 插件 | 文件夹 | 用途 | 状态 |
|
||
|---|---|---|---|
|
||
| 导航管理 | `plugins/navManager/` | 后台 CRUD nav 表;前台 `/site/` 入口 + 类目聚合走 framework 的 `public_nav` | ✅ |
|
||
| 友情链接 | `plugins/friendManager/` | 后台 CRUD friend 表;前台 `/site/friend` | ✅ |
|
||
| 关于本站 | `plugins/aboutManager/` | 后台编辑表单 + 保存;前台 `/site/about` 渲染 | ✅ |
|
||
| 访问统计 | `plugins/siteStats/` | 后台 PV/UV 主页 + 3 个 JSON 接口 + 全站 `/site/*` 埋点 | ✅ |
|
||
| 兑换码示例 | `plugins/giftManager/` | 官方示例,需要 `flask gift init` 初始化 | ✅(未启用) |
|
||
| Hello World | `plugins/helloworld/` | 官方示例 | ✅(未启用) |
|
||
| Replace Page | `plugins/replacePage/` | framework 页面替换示例 | ✅(未启用) |
|
||
| Real IP | `plugins/realip/` | Flask 上下文修改示例 | ✅(未启用) |
|
||
|
||
启用列表见 `applications/config.py` 的 `PLUGIN_ENABLE_FOLDERS`。
|
||
|
||
---
|
||
|
||
## 13. 首次启用 nav / friend / about / stat 时的手动操作
|
||
|
||
这些插件依赖数据库模型和权限码已经在 framework 中存在。**首次**部署到新环境:
|
||
|
||
1. `flask db migrate -m "add admin_nav / admin_friend / admin_about / admin_stat tables"`(如已存在表跳过)
|
||
2. `flask db upgrade`
|
||
3. 为 site_nav / site_about / site_friend 注册菜单权限(Power)记录:
|
||
```python
|
||
# 用 Flask shell 或在 admin.py 中 seed
|
||
Power(name='导航管理', type='1', code='system:nav:main', url='/system/nav/', open_type='_iframe', parent_id=1, ...)
|
||
# ... 类似地注册 site:friend:main / site:about:main / site:stats:main 与子权限
|
||
```
|
||
4. 把上述 `code` 加到 admin role 的 `power` 列表
|
||
5. 在 `PLUGIN_ENABLE_FOLDERS` 写好四项
|
||
6. 重启 Flask,用 admin / 123456 登录后台,可见新增菜单
|
||
|
||
---
|
||
|
||
## 14. 进阶:把现有 framework 业务迁出到 plugin 的步骤
|
||
|
||
如果有一天希望把用户管理(`applications/view/system/user.py`)也从 framework 迁移到 plugin,按以下顺序:
|
||
|
||
1. 复制 `applications/view/system/user.py` 内容到 `plugins/userManager/view/user.py`
|
||
2. 修改 Blueprint:`url_prefix='/system/user'`、加 `template_folder=...`
|
||
3. 复制 `templates/system/user/` 到 `plugins/userManager/templates/system/user/`
|
||
4. 从 `applications/view/system/__init__.py` 删除 `user_bp` 的 import 和 `system_bp.register_blueprint(user_bp)` 一行
|
||
5. 删除 `applications/view/system/user.py` 与 `applications/templates/system/user/`(如果都已迁出)
|
||
6. `PLUGIN_ENABLE_FOLDERS = [..., "userManager"]`
|
||
7. 重启验证
|
||
|
||
> ⚠️ 注意保留模型和 Schema 不动。
|
||
|
||
---
|
||
|
||
文档结束。
|