!72 更新插件功能

Merge pull request !72 from 我叫以赏/master
This commit is contained in:
不胜舟
2023-02-10 15:59:52 +00:00
committed by Gitee
29 changed files with 1698 additions and 137 deletions
+3
View File
@@ -27,3 +27,6 @@ SECRET_KEY='pear-admin-flask'
MAIL_SERVER = 'smtp.qq.com'
MAIL_USERNAME = '123@qq.com'
MAIL_PASSWORD = 'XXXXX' # 生成的授权码
# 插件配置
PLUGIN_ENABLE_FOLDERS = ["helloworld"]
+15
View File
@@ -5,6 +5,18 @@ from urllib.parse import quote_plus as urlquote
from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
# 强制读入 flaskenv 中的环境变量
with open(".flaskenv", "r", encoding='utf-8') as f:
for line in f.read().split("\n"):
pos = line.find("#")
if pos != -1:
line = line[:pos]
line = line.strip()
if line == "":
continue
_ = line.split("=")
key, value = _[0], '='.join(_[1:])
os.environ[key.strip()] = value.strip()
class BaseConfig:
@@ -75,6 +87,9 @@ class BaseConfig:
'max_instances': 3
}
# 插件配置
PLUGIN_ENABLE_FOLDERS = os.getenv('PLUGIN_ENABLE_FOLDERS')
# 配置多个数据库连接的连接串写法示例
# HOSTNAME: 指数据库的IP地址、USERNAME:指数据库登录的用户名、PASSWORD:指数据库登录密码、PORT:指数据库开放的端口、DATABASE:指需要连接的数据库名称
# MSSQL: f"mssql+pymssql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=cp936"
+9
View File
@@ -0,0 +1,9 @@
from applications.dev import user
from applications.dev import role
from applications.dev import power
from applications.dev import department
from applications.dev import console
from flask import Flask
# 获取app应用实例,会被初始化插件时重新赋值
app = None # type: Flask
+89
View File
@@ -0,0 +1,89 @@
"""
输出控制台日志
"""
import sys
import time
import ctypes
NONE = "\033[m"
RED = "\033[0;32;31m"
LIGHT_RED = "\033[1;31m"
GREEN = "\033[0;32;32m"
LIGHT_GREEN = "\033[1;32m"
BLUE = "\033[0;32;34m"
LIGHT_BLUE = "\033[1;34m"
DARY_GRAY = "\033[1;30m"
CYAN = "\033[0;36m"
LIGHT_CYAN = "\033[1;36m"
PURPLE = "\033[0;35m"
LIGHT_PURPLE = "\033[1;35m"
BROWN = "\033[0;33m"
YELLOW = "\033[1;33m"
LIGHT_GRAY = "\033[0;37m"
WHITE = "\033[1;37m"
# 开启 Windows 下对于 ESC控制符 的支持
if sys.platform == "win32":
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
def _print(level, msg):
time_ = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
level_name = {10: "Plain",
11: "Log",
12: "Info",
13: "Debug",
14: "Success",
15: "Warning",
16: "Error"}
color = {10: NONE,
11: LIGHT_CYAN,
12: LIGHT_BLUE,
13: PURPLE,
14: GREEN,
15: YELLOW,
16: RED}
print(f'{color.get(level, NONE)}[{time_}]({level_name.get(level, "Plain")}):', msg, f"{NONE}")
def plain(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(10, msg)
def log(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(11, msg)
def info(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(12, msg)
def debug(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(13, msg)
def success(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(14, msg)
def warn(*args):
warning(*args)
def warning(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(15, msg)
def error(*args, sep=' '):
msg = sep.join(str(_) for _ in args)
_print(16, msg)
+108
View File
@@ -0,0 +1,108 @@
"""
集成了对 Pear Admin Flask 的部门的操作,并给了相对应的示例。
"""
from applications.common import curd
from applications.extensions import db
from applications.models import Dept, User
from applications.schemas import DeptOutSchema
def get_all():
"""
获取全部权限,会返回一个列表,每个列表是一个字典。
字典构成如下::
{
"address":"这是总公司", # 地址
"deptId":1, # 公司ID
"deptName":"总公司", # 公司名
"email":"1", # 公司 email
"leader":"", # 公司领导人
"parentId":0, # 父ID
"phone":"", # 联系电话
"sort":1, # 排序
"status":"1" # 状态 1-开启 0-关闭
}
:return: 列表
"""
dept = Dept.query.order_by(Dept.sort).all()
return curd.model_to_dicts(schema=DeptOutSchema, data=dept)
def add(parentId, deptName, sort, leader, phone, email, status, address):
"""
添加一个公司
:param parentId: 父公司ID0未总公司
:param deptName: 公司名称
:param sort: 排序
:param leader: 负责人
:param phone: 手机
:param email: 邮箱
:param status: 状态 1-打开 0-关闭
:param address: 地址
:return: 是否成功
"""
dept = Dept(
parent_id=parentId,
dept_name=deptName,
sort=sort,
leader=leader,
phone=phone,
email=email,
status=status,
address=address
)
r = db.session.add(dept)
db.session.commit()
if r:
return True
else:
return False
def update(deptId, data):
"""
更新公司信息
可更新内容如下::
"dept_name"
"sort"
"leader"
"phone"
"email"
"status"
"address"
:param deptId: 公司ID
:param data: 要更新公司字典
:return: 是否成功
"""
d = Dept.query.filter_by(id=deptId).update(data)
if not d:
return False
db.session.commit()
return True
def delete(deptId):
"""
删除公司
:param deptId: 公司ID
:return: 是否成功
"""
d = Dept.query.filter_by(id=deptId).delete()
if not d:
return False
res = User.query.filter_by(dept_id=deptId).update({"dept_id": None})
db.session.commit()
if res:
return True
else:
return False
+83
View File
@@ -0,0 +1,83 @@
"""
集成了对 Pear Admin Flask 二次开发的的邮件操作,并给了相对应的示例。
"""
from flask import current_app
from flask_mail import Message
from applications.common.curd import model_to_dicts
from applications.common.helper import ModelFilter
from applications.extensions import db, flask_mail
from applications.models import Mail
from applications.schemas import MailOutSchema
def get_all(receiver=None, subject=None, content=None):
"""
获取邮件
返回的列表中的字典构造如下::
{
"content": "", # html内容
"create_at": "2022-12-25T10:51:17", # 时间
"id": 17, # 邮件ID
"realname": "超级管理", # 创建者
"receiver": "", # 接收者
"subject": "" # 主题
}
:param receiver: 发送者
:param subject: 邮件标题
:param content: 邮件内容
:return: 列表
"""
# 查询参数构造
mf = ModelFilter()
if receiver:
mf.contains(field_name="receiver", value=receiver)
if subject:
mf.contains(field_name="subject", value=subject)
if content:
mf.exact(field_name="content", value=content)
# orm查询
# 使用分页获取data需要.items
mail = Mail.query.filter(mf.get_filter(Mail)).layui_paginate()
return model_to_dicts(schema=MailOutSchema, data=mail.items)
def add(receiver, subject, content, user_id):
"""
发送一封邮件,若发送成功立刻提交数据库。
:param receiver: 接收者 多个用英文逗号隔开
:param subject: 邮件主题
:param content: 邮件 html
:param user_id: 发送用户ID(谁发送的?) 可以用 from flask_login import current_user ; current_user.id 来表示当前登录用户
:return: 成功与否
"""
try:
msg = Message(subject=subject, recipients=receiver.split(";"), html=content)
flask_mail.send(msg)
except BaseException as e:
current_app.log_exception(e)
return False
mail = Mail(receiver=receiver, subject=subject, content=content, user_id=user_id)
db.session.add(mail)
db.session.commit()
return True
def delete(id):
"""
删除邮件记录,立刻写入数据库。
:param id: 邮件ID
:return: 成功与否
"""
res = Mail.query.filter_by(id=id).delete()
if not res:
return False
db.session.commit()
return True
+138
View File
@@ -0,0 +1,138 @@
"""
集成了对 Pear Admin Flask 的权限操作,并给了相对应的示例。
注意:此权限相当于添加后台菜单。
"""
from applications.common import curd
from applications.extensions import db
from applications.models import Power
from applications.extensions import ma
from marshmallow import fields
class PowerOutSchema2(ma.Schema): # 序列化类
powerId = fields.Str(attribute="id")
powerName = fields.Str(attribute="name")
powerType = fields.Str(attribute="type")
powerUrl = fields.Str(attribute="url")
powerCode = fields.Str(attribute="code")
openType = fields.Str(attribute="open_type")
parentId = fields.Str(attribute="parent_id")
icon = fields.Str()
sort = fields.Integer()
create_time = fields.DateTime()
update_time = fields.DateTime()
enable = fields.Integer()
def get_all():
"""
获取所有权限,会返回一个含有菜单的列表。每个列表都是一个字典。
字典构成如下::
{
"powerType": "0", # 权限类型 0目录 1菜单 2按钮
"powerUrl": None, # 路径
"powerCode": "", # 权限标识
"update_time": None, # 更新时间
"sort": 1, # 排序
"openType": None, # 打开方式 _iframe框架 _blank新页面
"icon": "layui-icon layui-icon-set-fill", # 图标
"powerName": "系统管理", # 名称
"create_time": None, # 创建时间
"parentId": "0", # 父id
"powerId": "1", # 自己的id
"enable": 1 # 是否启用
}
:return: 菜单列表。
"""
power = Power.query.all()
res = curd.model_to_dicts(schema=PowerOutSchema2, data=power)
res.append({"powerId": 0, "powerName": "顶级权限", "parentId": -1})
return res
def add(parentId, powerName, powerType, icon, sort: int, enable: bool, powerCode="", powerUrl="", openType=""):
"""
新建一个菜单权限。
参考代码::
dev.power.add("0", "测试", "1", "layui-icon-time", 0, True, "testfor", "https://baidu.com", "_iframe")
:param parentId: 父ID0为顶级菜单ID
:param powerName: 菜单名称
:param powerType: 权限类型(状态) 0目录 1菜单 2按钮
:param icon: 图标,详细查看layui的图标
:param sort: 排序
:param enable: 是否启用
:param powerCode: 权限标识
:param powerUrl: 权限URL,菜单打开的网址,或者是路径。可选,菜单和按钮类型必填。
:param openType: 打开方式,_iframe框架 _blank新页面。可选,菜单和按钮类型必填。
:return: 返回新权限ID
"""
power = Power(
icon=icon,
open_type=openType,
parent_id=parentId,
code=powerCode,
name=powerName,
type=powerType,
url=powerUrl,
sort=sort,
enable=1
)
r = db.session.add(power)
db.session.commit()
return power.id
def update(powerId, data):
"""
更新权限。
data可选::
"icon"
"open_type"
"parent_id"
"code"
"name"
"type"
"url"
"sort"
:param powerId: 要更新的权限ID
:return: 是否成功
"""
res = Power.query.filter_by(id=powerId).update(data)
db.session.commit()
if res:
return True
else:
return False
def delete(powerId):
"""
删除权限。
:param powerId: 要更新的权限ID
:return: 是否成功
"""
power = Power.query.filter_by(id=powerId).first()
power.role = []
r = Power.query.filter_by(id=powerId).delete()
db.session.commit()
if r:
return True
else:
return False
+157
View File
@@ -0,0 +1,157 @@
"""
集成了对 Pear Admin Flask 的角色操作,并给了相对应的示例。
"""
from applications.extensions import db
from applications.models import Role, Power
from applications.schemas import PowerOutSchema2
def filter_by(**kwargs):
"""
检索角色字段信息,可用于获取角色ID等。系统中默认管理员角色id为1,普通用户为2。
内部采用的是使用 Role.query.filter_by(**kwargs) 进行数据库查询。
注意:此函数返回的结果为构造的 SQL的查询字符串 ,以 role_filter 命名,但是并不是用户数据。
返回的字段如下::
id name code enable remark details sort create_time update_time power
具体参考 applications/models/admin_role.py 中的模型定义。
参考调用如下::
roleinfo = dev.role.filter_by(code='admin').first() # 第一个符合要求的角色信息
print(role.id, role.name) # 输出角色名称与角色标识
# 找出所有角色id
for role in dev.role.filter_by().all():
print(role.id, role.name)
:param kwargs: 查询参数
:return: 角色SQL的查询字符串
"""
return Role.query.filter_by(**kwargs)
def add(roleName, roleCode, enable, sort, details):
"""
添加一个角色。此函数直接写入数据库。此函数不会检测角色是否已经存在(官方在API接口中也没有检测)。
:param roleName: 角色名称 (如管理员)
:param roleCode: 角色标识 (如admin)
:param enable: 是否启用 True or False
:param sort: 排序
:param details: 描述
:return: None
"""
role = Role(
details=details,
enable=enable,
code=roleCode,
name=roleName,
sort=sort
)
db.session.add(role)
db.session.commit()
def get_power(role_filter, detail=False, p=0):
"""
获取角色的权限。
如果是非详细数据,会返回一个含有权限id的列表。
如果是详细数据返回此函数,将会返回一个列表,列表中会包含字典,字典的键如下::
{
"checkArr": "1", # 是否有权限 1 为有 0为无
"create_time": null, # 权限创建
"enable": 1, # 权限是否启用
"icon": "layui-icon layui-icon-set-fill", # 权限图标
"openType": null, # 开启状态
"parentId": "0", # 父权限ID
"powerId": "1", # 权限ID
"powerName": "系统管理", # 权限名称
"powerType": "0", # 权限类型
"powerUrl": null, # 权限URL
"sort": 1, # 权限排序
"update_time": null # 权限更新时间
}
:param role_filter: dev.role.filter_by() 返回结果。
:param detail: 返回详细数据
:param p: 如果有多个结果被找到,p可以确定使用第几个结果。内部使用 role_filter.all()[p]
:return: 用户拥有的权限列表。
"""
role = role_filter.all()[p]
check_powers = role.power
check_powers_list = []
for cp in check_powers:
check_powers_list.append(cp.id)
if not detail:
return check_powers_list
powers = Power.query.all()
power_schema = PowerOutSchema2(many=True) # 用已继承ma.ModelSchema类的自定制类生成序列化类
output = power_schema.dump(powers) # 生成可序列化对象
for i in output:
if int(i.get("powerId")) in check_powers_list:
i["checkArr"] = "1"
else:
i["checkArr"] = "0"
return output
def set_power(role_filter, powerIds, p=0):
"""
保存角色权限。此函数会直接写入数据库。
:param role_filter: dev.role.filter_by() 返回结果。
:param powerIds: 必须是一个包含权限ID的列表。如 [1, 2, 3]
:param p: 如果有多个结果被找到,p可以确定使用第几个结果。内部使用 role_filter.all()[p]
:return: None
"""
role = role_filter.all()[p]
powers = Power.query.filter(Power.id.in_(powerIds)).all()
role.power = powers
db.session.commit()
def update(role_filter, data):
"""
更新角色数据。此功能将直接写入数据库。
可更新的字段如下::
id name code enable remark details sort create_time update_time power
具体参考 applications/models/admin_role.py 中的模型定义。
参考调用如下::
role_filter = dev.role.filter_by(id=0) # 获取指定角色ID的角色,注意不要使用会引起歧义的查询条件,否则会匹配到多个角色。
dev.role.update(role_filter, {enable: 0}) # 禁用
:param role_filter: dev.role.filter_by() 返回结果。
:param data: 要更新的数据,必须是字典。
:return: None
"""
role_filter.update(data)
db.session.commit()
def delete(role_filter):
"""
删除角色。此功能将直接写入数据库。
:param role_filter: dev.role.filter_by() 返回结果。
:return: 是否成功。
"""
role = role_filter.first()
# 删除该角色的权限和用户
role.power = []
role.user = []
r = role_filter.delete()
db.session.commit()
return r
+160
View File
@@ -0,0 +1,160 @@
"""
集成了对 Pear Admin Flask 的用户操作,并给了相对应的示例。
调用示例::
from applications import dev
dev.user.login_required # 用户是否登录
dev.user.current_user # 当前登录用户
dev.user.authorize("XXX", log=True) # 用户是否有此权限
"""
from applications.extensions import db
from applications.models import Role
from applications.models import User
def filter_by(**kwargs):
"""
用于在用户数据中查询用户信息,可以通过用户名、用户id等进行检索。(建议使用id检索)
内部采用的是使用 User.query.filter_by(**kwargs) 进行数据库查询
注意:此函数返回的结果为构造的 SQL的查询字符串 ,以 user_filter 命名,但是并不是用户数据。
返回的字段如下::
id username password_hash create_at update_at
enable realname remark avatar dept_id
具体参考 applications/models/admin_user.py 中的模型定义。
参考调用如下::
userinfo = dev.user.filter_by(username='zsq').first() # 查询符合要求的第一个用户
print(userinfo.realname) # 获取用户真实名字
:param kwargs: 查询参数
:return: 用户数据SQL的查询字符串
"""
return User.query.filter_by(**kwargs)
def update(user_filter, data):
"""
更新用户数据,修改将直接保存到数据库中。
注意:更新用户角色(role)请使用 dev.user.update_role() 函数。
可更新的字段如下::
id username password_hash create_at update_at
enable realname remark avatar dept_id
具体参考 applications/models/admin_user.py 中的模型定义。
参考调用如下::
user_filter = dev.user.filter_by(id=0) # 获取指定用户ID的用户,注意不要使用会引起歧义的查询条件,否则会匹配到多个用户。
dev.user.update(user_filter, {username: 'zsq1314'}) # 更新其用户名
:param user_filter: dev.user.filter_by() 的结果。
:param data: 要更新的数据,必须是字典。
:return: None
"""
user_filter.update(data)
db.session.commit()
def update_role(user_filter, roleIds):
"""
更新用户角色,修改将直接保存到数据库中。
参考调用如下::
user_filter = dev.user.filter_by(username='zsq') # 获取符合要求的第一个用户
roleIds = []
roleIds.append(dev.role.filter_by(code='admin').first().id) # 管理员角色ID
roleIds.append(dev.role.filter_by(code='common').first().id) # 普通用户角色ID
dev.user.update_role(user_filter, roleIds)
:param user_filter: dev.user.filter_by() 的结果。
:param roleIds: 要更新的角色ID,作为列表传入。
:return: None
"""
user_filter.first().role = Role.query.filter(Role.id.in_(roleIds)).all()
db.session.commit()
def get_role(user_filter):
"""
获取用户的所有角色ID,将会返回一个整数列表。
:param user_filter: dev.user.filter_by() 的结果。
:return: 列表 (roleIds)
"""
checked_roles = []
for r in user_filter.first().role:
checked_roles.append(r.id)
return checked_roles
def set_password(user_filter, password):
"""
设置用户密码,此函数不会验证用户原始密码哈希值,直接写入新密码哈希值。
参考调用如下::
user_filter = dev.user.filter_by(username='zsq') # 获取符合要求的用户
dev.user.set_password(user_filter, 'zsq1314') # 设置密码
:param user_filter: dev.user.filter_by() 的结果。
:param password: 新密码。
:return: None
"""
user = user_filter.first()
user.set_password(password)
db.session.add(user)
db.session.commit()
def add(username, realname, password, roleIds):
"""
添加一个新用户。函数会判断用户名是否已存在,存在返回 False ,成功返回用户数据(userinfo)。此函数会直接写入数据库。
注意:此函数创建出来的用户默认是禁用的,可以使用 enable 启用。
:param username: 新用户名
:param realname: 真实名字
:param password: 密码字符串
:param roleIds: 角色id列表,如 [1, 2],角色id具体查看 dev.role.get_all() 函数。
:return: 是否成功
"""
if bool(User.query.filter_by(username=username).count()):
return False
user = User(username=username, realname=realname)
user.set_password(password)
db.session.add(user)
roles = Role.query.filter(Role.id.in_(roleIds)).all()
for r in roles:
user.role.append(r)
db.session.commit()
return user
def delete(user_filter):
"""
删除一个用户。此函数立刻写入数据库。
:param user_filter: dev.user.filter_by() 的结果。
:return: 是否成功
"""
user = user_filter.first()
user.role = []
res = user_filter.delete()
db.session.commit()
return res
+2 -1
View File
@@ -3,7 +3,7 @@ from applications.view.index import register_index_views
from applications.view.passport import register_passport_views
from applications.view.rights import register_rights_view
from applications.view.department import register_dept_views
from applications.view.plugin import register_plugin_views
def init_view(app):
register_admin_views(app)
@@ -11,3 +11,4 @@ def init_view(app):
register_rights_view(app)
register_passport_views(app)
register_dept_views(app)
register_plugin_views(app)
+16 -6
View File
@@ -1,9 +1,10 @@
import os
import platform
import re
from datetime import datetime
import sys
import time
import psutil
import platform
from datetime import datetime
from flask import Blueprint, render_template, jsonify
from applications.common.utils.rights import authorize
@@ -22,8 +23,8 @@ def main():
python_version = platform.python_version()
# 逻辑cpu数量
cpu_count = psutil.cpu_count()
# cup使用率
cpus_percent = psutil.cpu_percent(interval=0.1)
# cpu使用率
cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率
# 内存
memory_information = psutil.virtual_memory()
# 内存使用率
@@ -79,10 +80,19 @@ def main():
@admin_monitor_bp.get('/polling')
@authorize("admin:monitor:main")
def ajax_polling():
# 获取cup使用率
cpus_percent = psutil.cpu_percent(interval=0.1)
# 获取cpu使用率
cpus_percent = psutil.cpu_percent(interval=0.1, percpu=False) # percpu 获取主使用率
# 获取内存使用率
memory_information = psutil.virtual_memory()
memory_usage = memory_information.percent
time_now = time.strftime('%H:%M:%S ', time.localtime(time.time()))
return jsonify(cups_percent=cpus_percent, memory_used=memory_usage, time_now=time_now)
# 关闭程序
@admin_monitor_bp.get('/kill')
@authorize("admin:monitor:main")
def kill():
for proc in psutil.process_iter():
if proc.pid == os.getpid():
proc.kill()
sys.exit(1)
+168
View File
@@ -0,0 +1,168 @@
import shutil
from flask import Flask
from flask import Blueprint, render_template, request, jsonify, escape
from applications.common.utils.http import table_api, fail_api, success_api
import os
import json
import traceback
import importlib
import applications.dev
from applications.common.utils.rights import authorize
plugin_bp = Blueprint('plugin', __name__, url_prefix='/plugin')
PLUGIN_ENABLE_FOLDERS = []
def register_plugin_views(app: Flask):
global PLUGIN_ENABLE_FOLDERS
applications.dev.app = app # 对app重新赋值 便于插件简单调用
app.register_blueprint(plugin_bp)
# 载入插件过程
# plugin_folder 配置的是插件的文件夹名
PLUGIN_ENABLE_FOLDERS = json.loads(app.config['PLUGIN_ENABLE_FOLDERS'])
for plugin_folder in PLUGIN_ENABLE_FOLDERS:
plugin_info = {}
try:
with open("plugins/" + plugin_folder + "/__init__.json", "r", encoding='utf-8') as f:
plugin_info = json.loads(f.read())
# 初始化完成事件
try:
getattr(importlib.import_module('plugins.' + plugin_folder), "event_init")(app)
except AttributeError: # 没有插件启用事件就不调用
pass
except BaseException as error:
return fail_api(msg="Crash a error! Info: " + str(error))
print(f" * Plugin: Loaded plugin: {plugin_info['plugin_name']} .")
except BaseException as e:
info = f" * Plugin: Crash a error when loading {plugin_info['plugin_name'] if len(plugin_info) != 0 else 'plugin'} :" + "\n"
info += 'str(Exception):\t' + str(Exception) + "\n"
info += 'str(e):\t\t' + str(e) + "\n"
info += 'repr(e):\t' + repr(e) + "\n"
info += 'traceback.format_exc():\n%s' + traceback.format_exc()
print(info)
@plugin_bp.get('/')
@authorize("admin:plugin:main", log=True)
def main():
"""此处渲染管理模板"""
return render_template('admin/plugin/main.html')
@plugin_bp.get('/data')
@authorize("admin:plugin:main", log=True)
def data():
"""请求插件数据"""
plugin_name = escape(request.args.get("plugin_name"))
all_plugins = []
count = 0
for filename in os.listdir("plugins"):
try:
with open("plugins/" + filename + "/__init__.json", "r", encoding='utf-8') as f:
info = json.loads(f.read())
if plugin_name is None:
if info['plugin_name'].find(plugin_name) == -1:
continue
all_plugins.append(
{
"plugin_name": info["plugin_name"],
"plugin_version": info["plugin_version"],
"plugin_description": info["plugin_description"],
"developer_name": info["developer_name"],
"developer_website": info["developer_website"],
"developer_email": info["developer_email"],
"developer_phone": info["developer_phone"],
"plugin_folder_name": filename,
"enable": "1" if filename in PLUGIN_ENABLE_FOLDERS else "0"
}
)
count += 1
except BaseException as error:
print(filename, error)
continue
return table_api(data=all_plugins, count=count)
@plugin_bp.put('/enable')
@authorize("admin:plugin:enable", log=True)
def enable():
"""启用插件"""
plugin_folder_name = request.json.get('plugin_folder_name')
if plugin_folder_name:
try:
if plugin_folder_name not in PLUGIN_ENABLE_FOLDERS:
PLUGIN_ENABLE_FOLDERS.append(plugin_folder_name)
with open(".flaskenv", "r", encoding='utf-8') as f:
flaskenv = f.read() # type: str
pos1 = flaskenv.find("PLUGIN_ENABLE_FOLDERS")
pos2 = flaskenv.find("\n", pos1)
with open(".flaskenv", "w", encoding='utf-8') as f:
if pos2 == -1:
f.write(flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS))
else:
f.write(
flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS) + flaskenv[
pos2:])
# 启用插件事件
try:
getattr(importlib.import_module('plugins.' + plugin_folder_name), "event_enable")()
except AttributeError: # 没有插件启用事件就不调用
pass
except BaseException as error:
return fail_api(msg="Crash a error! Info: " + str(error))
except BaseException as error:
return fail_api(msg="Crash a error! Info: " + str(error))
return success_api(msg="启用成功,要使修改生效需要重启程序。")
return fail_api(msg="数据错误")
@plugin_bp.put('/disable')
@authorize("admin:plugin:enable", log=True)
def disable():
"""禁用插件"""
plugin_folder_name = request.json.get('plugin_folder_name')
if plugin_folder_name:
try:
if plugin_folder_name in PLUGIN_ENABLE_FOLDERS:
PLUGIN_ENABLE_FOLDERS.remove(plugin_folder_name)
with open(".flaskenv", "r", encoding='utf-8') as f:
flaskenv = f.read() # type: str
pos1 = flaskenv.find("PLUGIN_ENABLE_FOLDERS")
pos2 = flaskenv.find("\n", pos1)
with open(".flaskenv", "w", encoding='utf-8') as f:
if pos2 == -1:
f.write(flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS))
else:
f.write(
flaskenv[:pos1] + "PLUGIN_ENABLE_FOLDERS = " + json.dumps(PLUGIN_ENABLE_FOLDERS) + flaskenv[
pos2:])
# 禁用插件事件
try:
getattr(importlib.import_module('plugins.' + plugin_folder_name), "event_disable")()
except AttributeError: # 没有插件禁用事件就不调用
pass
except BaseException as error:
return fail_api(msg="Crash a error! Info: " + str(error))
except BaseException as error:
return fail_api(msg="Crash a error! Info: " + str(error))
return success_api(msg="禁用成功,要使修改生效需要重启程序。")
return fail_api(msg="数据错误")
# 删除
@plugin_bp.delete('/remove/<string:plugin_folder_name>')
@authorize("admin:mail:remove", log=True)
def delete(plugin_folder_name):
if plugin_folder_name in PLUGIN_ENABLE_FOLDERS:
return fail_api(msg="您必须先禁用插件!")
try:
shutil.rmtree(os.path.abspath("plugins/" + plugin_folder_name))
return success_api(msg="删除成功")
except BaseException as error:
return fail_api(msg="删除失败!原因:" + str(error))
+8
View File
@@ -211,6 +211,10 @@ INSERT INTO `admin_power` VALUES (56, '任务删除', '2', 'admin:task:remove',
INSERT INTO `admin_power` VALUES (57, '邮件管理', '1', 'admin:mail:main', '/admin/mail', '_iframe', '1', 'layui-icon layui-icon layui-icon-release', 7, '2022-10-11 11:21:05', '2022-10-11 11:21:22', 1);
INSERT INTO `admin_power` VALUES (58, '邮件发送', '2', 'admin:mail:add', '', '', '57', 'layui-icon layui-icon-ok-circle', 1, '2022-10-11 11:22:26', '2022-10-11 11:22:26', 1);
INSERT INTO `admin_power` VALUES (59, '邮件删除', '2', 'admin:mail:remove', '', '', '57', 'layui-icon layui-icon layui-icon-close', 2, '2022-10-11 11:23:06', '2022-10-11 11:23:18', 1);
INSERT INTO `admin_power` VALUES (60, '拓展插件', '0', '', '', '', '0', 'layui-icon layui-icon layui-icon-senior', 2, '2022-12-18 12:28:19', '2022-12-18 12:30:25', 1);
INSERT INTO `admin_power` VALUES (61, '插件管理', '1', 'admin:plugin:main', '/plugin', '_iframe', '60', 'layui-icon layui-icon layui-icon layui-icon ', 1, '2022-12-18 12:30:13', '2022-12-18 13:57:20', 1);
INSERT INTO `admin_power` VALUES (62, '启禁插件', '2', 'admin:plugin:enable', '', '', '61', 'layui-icon ', 1, '2022-12-18 13:25:37', '2022-12-18 13:25:37', 1);
INSERT INTO `admin_power` VALUES (63, '删除插件', '2', 'admin:plugin:remove', '', '', '61', 'layui-icon layui-icon ', 2, '2022-12-18 13:26:30', '2022-12-18 13:27:17', 1);
-- ----------------------------
-- Table structure for admin_role
@@ -299,6 +303,10 @@ INSERT INTO `admin_role_power` VALUES (366, 56, 1);
INSERT INTO `admin_role_power` VALUES (367, 57, 1);
INSERT INTO `admin_role_power` VALUES (368, 58, 1);
INSERT INTO `admin_role_power` VALUES (369, 59, 1);
INSERT INTO `admin_role_power` VALUES (370, 60, 1);
INSERT INTO `admin_role_power` VALUES (371, 61, 1);
INSERT INTO `admin_role_power` VALUES (372, 62, 1);
INSERT INTO `admin_role_power` VALUES (373, 63, 1);
-- ----------------------------
-- Table structure for admin_user
+9
View File
@@ -0,0 +1,9 @@
{
"plugin_name": "Hello World",
"plugin_version": "1.0.0.1",
"plugin_description": "一个测试的插件。",
"developer_name": "Yishang",
"developer_website": "https://lovepikachu.top",
"developer_email": "422880152@qq.com",
"developer_phone": "-"
}
+24
View File
@@ -0,0 +1,24 @@
"""
初始化插件
"""
import os
from flask import Flask
from .main import helloworld_blueprint
# 获取插件所在的目录(结尾没有分割符号)
dir_path = os.path.dirname(__file__).replace("\\", "/")
folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称
def event_enable():
"""当此插件被启用时会调用此处"""
print(f"启用插件,dir_path: {dir_path} ; folder_name: {folder_name}")
def event_disable():
"""当此插件被禁用时会调用此处"""
print(f"禁用插件,dir_path: {dir_path} ; folder_name: {folder_name}")
def event_init(app: Flask):
"""初始化完成时会调用这里"""
app.register_blueprint(helloworld_blueprint)
+12
View File
@@ -0,0 +1,12 @@
from applications.dev import *
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")
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pear-admin-flask</title>
<meta name="description" content="pear-admin-flask">
{% include 'admin/common/header.html' %}
<link rel="stylesheet" href="{{ url_for('static', filename='index/layui/css/layui.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index/css/main.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index/css/load.css') }}"/>
<style>
.layui-nav .layui-this:after {
display: none;
}
.layui-nav .layui-nav-bar {
display: none;
}
</style>
</head>
<body>
<!-- header -->
<div class="ew-header">
<img class="layui-logo" src="{{ url_for('static', filename='index/images/un28.svg') }}"
style="height:50px;padding: 10px;"/>
<div class="ew-nav-group">
<div class="nav-toggle"><i class="layui-icon layui-icon-more-vertical"></i></div>
<ul class="layui-nav" lay-filter="ew-header-nav">
<li class="layui-nav-item">
<a lay-href="/">首页</a>
</li>
<li class="layui-nav-item">
<a href="/admin" target="_blank">后台</a>
</li>
</ul>
</div>
</div>
<!-- banner -->
<div class="ew-banner" style="box-shadow: 0 0 8px rgba(0,0,0,.101562);">
<div class="layui-container">
<div class="layui-row layui-col-space25" style="margin-top: 10px;">
<h1>你好世界!这是一个测试页面!</h1>
</div>
</div>
<br/>
<br/>
</div>
</div>
<!-- js部分 -->
<script src="{{ url_for('static', filename='admin/component/layui/layui.js') }}"></script>
<script src="{{ url_for('static', filename='admin/component/pear/pear.js') }}"></script>
<script>
layui.use(['layer', 'form', 'jquery'], function () {
var $ = layui.jquery
var layer = layui.layer
var form = layui.form
var jquery = layui.jquery
})
</script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
{
"plugin_name": "真实IP插件",
"plugin_version": "1.0.0.1",
"plugin_description": "在上游更改访客IP地址,并采用自定义日志输出。",
"developer_name": "Yishang",
"developer_website": "https://lovepikachu.top",
"developer_email": "422880152@qq.com",
"developer_phone": "-"
}
+66
View File
@@ -0,0 +1,66 @@
"""
初始化插件
"""
import os
import logging
from flask import Flask, request
from applications.dev import console
# 获取插件所在的目录(结尾没有分割符号)
dir_path = os.path.dirname(__file__).replace("\\", "/")
folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称
def event_enable():
"""当此插件被启用时会调用此处"""
print(f"启用插件,dir_path: {dir_path} ; folder_name: {folder_name}")
def event_disable():
"""当此插件被禁用时会调用此处"""
print(f"禁用插件,dir_path: {dir_path} ; folder_name: {folder_name}")
def event_init(app: Flask):
"""初始化完成时会调用这里"""
# 移除原有的输出日志
app.logger = None
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
# 更改IP地址,只有在最新版的flask中才能生效
@app.before_request
def before_request():
request.remote_addr = get_user_ip(request)
# 使用自定义的日志输出
@app.after_request
def after_request(rep):
if rep.status_code == 200:
console.success(f"{request.remote_addr} -- {request.full_path} 200")
elif rep.status_code == 404:
console.error(f"{request.remote_addr} -- {request.full_path} 404")
elif rep.status_code == 500:
console.warning(f"{request.remote_addr} -- {request.full_path} 500")
else:
console.info(f"{request.remote_addr} -- {request.full_path} {rep.status_code}")
return rep
def get_user_ip(request):
"""获取用户真实IP"""
if 'HTTP_X_FORWARDED_FOR' in request.headers:
arr = request.headers['HTTP_X_FORWARDED_FOR'].strip().split(",")
i = 0
while i < len(arr):
if arr[i].find("unknown") != -1:
del arr[i]
else:
i += 1
if len(arr) != 0:
return arr[0].strip()
elif 'HTTP_CLIENT_IP' in request.headers:
return request.headers['HTTP_CLIENT_IP']
elif 'REMOTE_ADDR' in request.headers:
return request.headers['REMOTE_ADDR']
elif 'X-Forwarded-For' in request.headers:
return request.headers['X-Forwarded-For']
return request.remote_addr
+9
View File
@@ -0,0 +1,9 @@
{
"plugin_name": "页面替换插件",
"plugin_version": "1.0.0.1",
"plugin_description": "在不更改原有框架视图函数的情况下更改渲染输出页面。",
"developer_name": "Yishang",
"developer_website": "https://lovepikachu.top",
"developer_email": "422880152@qq.com",
"developer_phone": "-"
}
+37
View File
@@ -0,0 +1,37 @@
"""
初始化插件
"""
import os
import logging
from flask import Flask, render_template_string
from applications.dev import console
# 获取插件所在的目录(结尾没有分割符号)
dir_path = os.path.dirname(__file__).replace("\\", "/")
folder_name = dir_path[dir_path.rfind("/") + 1:] # 插件文件夹名称
def event_enable():
"""当此插件被启用时会调用此处"""
print(f"启用插件,dir_path: {dir_path} ; folder_name: {folder_name}")
def event_disable():
"""当此插件被禁用时会调用此处"""
print(f"禁用插件,dir_path: {dir_path} ; folder_name: {folder_name}")
def event_init(app: Flask):
"""初始化完成时会调用这里"""
# 使用下面的代码 查看所有注册的视图函数。对于 Flask app.route 函数的实现,请参考 https://www.jianshu.com/p/dff3bc2f4836
# print(app.view_functions)
# 定义新视图函数
def new_index():
# 规避 render_template 的做法
with open(dir_path + "/templates/replacePage_index.html", "r", encoding='utf-8') as f:
return render_template_string(f.read())
# Index.index 是主页的视图函数对应的名称,原视图函数位于 applications/view/index/index.py
del app.view_functions['Index.index'] # 释放原视图函数
app.view_functions['Index.index'] = new_index # 替换原视图函数
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pear-admin-flask</title>
<meta name="description" content="pear-admin-flask">
{% include 'admin/common/header.html' %}
<link rel="stylesheet" href="{{ url_for('static', filename='index/layui/css/layui.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index/css/main.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index/css/load.css') }}"/>
<style>
.layui-nav .layui-this:after {
display: none;
}
.layui-nav .layui-nav-bar {
display: none;
}
</style>
</head>
<body>
<!-- header -->
<div class="ew-header">
<img class="layui-logo" src="{{ url_for('static', filename='index/images/un28.svg') }}"
style="height:50px;padding: 10px;"/>
<div class="ew-nav-group">
<div class="nav-toggle"><i class="layui-icon layui-icon-more-vertical"></i></div>
<ul class="layui-nav" lay-filter="ew-header-nav">
<li class="layui-nav-item">
<a lay-href="/">首页</a>
</li>
<li class="layui-nav-item">
<a href="/admin" target="_blank">后台</a>
</li>
</ul>
</div>
</div>
<!-- banner -->
<div class="ew-banner" style="box-shadow: 0 0 8px rgba(0,0,0,.101562);">
<div class="layui-container">
<div class="layui-row layui-col-space25" style="margin-top: 10px;">
<h1>主页被我替换了哈哈哈!</h1>
</div>
</div>
<br/>
<br/>
</div>
</div>
<!-- js部分 -->
<script src="{{ url_for('static', filename='admin/component/layui/layui.js') }}"></script>
<script src="{{ url_for('static', filename='admin/component/pear/pear.js') }}"></script>
<script>
layui.use(['layer', 'form', 'jquery'], function () {
var $ = layui.jquery
var layer = layui.layer
var form = layui.form
var jquery = layui.jquery
})
</script>
</body>
</html>
+51 -36
View File
@@ -1,93 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title> Pear Admin Flask </title>
{% include 'admin/common/header.html' %}
<title> Pear Admin Layui </title>
<link rel="stylesheet" href="{{ url_for('static', filename='admin/component/pear/css/pear.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='admin/admin/css/loader.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='admin/admin/css/admin.css') }}"/>
<!-- 主 题 更 换 -->
<style id="pearadmin-bg-color"></style>
</head>
<!-- 结 构 代 码 -->
<body class="layui-layout-body pear-admin">
<!-- 布 局 框 架 -->
<div class="layui-layout layui-layout-admin">
<!-- 顶 部 样 式 -->
<div class="layui-header">
<!-- 菜 单 顶 部 -->
<div class="layui-logo">
<!-- 图 标 -->
<img class="logo">
<!-- 标 题 -->
<span class="title"></span>
</div>
<!-- 顶 部 左 侧 功 能 -->
<ul class="layui-nav layui-layout-left">
<li class="collaspe layui-nav-item"><a href="#" class="layui-icon layui-icon-shrink-right"></a></li>
<li class="refresh layui-nav-item"><a href="#" class="layui-icon layui-icon-refresh-1" title="刷新页面"
loading=600></a></li>
<li class="collapse layui-nav-item"><a href="#" class="layui-icon layui-icon-shrink-right"></a></li>
<li class="refresh layui-nav-item"><a href="#" class="layui-icon layui-icon-refresh-1" loading = 600></a></li>
</ul>
<!-- 顶 部 右 侧 菜 单 -->
<!-- 多 系 统 菜 单 -->
<div id="control" class="layui-layout-control"></div>
<!-- 顶 部 右 侧 菜 单 -->
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item layui-hide-xs">
<a href="#" class="fullScreen layui-icon layui-icon-screen-full"></a>
</li>
<li class="layui-nav-item layui-hide-xs">
<a href="http://www.pearadmin.com" class="layui-icon layui-icon-website"></a>
</li>
<li class="layui-nav-item layui-hide-xs"><a href="#" class="menuSearch layui-icon layui-icon-search"></a></li>
<li class="layui-nav-item layui-hide-xs"><a href="#" class="fullScreen layui-icon layui-icon-screen-full"></a></li>
<li class="layui-nav-item layui-hide-xs"><a href="http://www.pearadmin.com" class="layui-icon layui-icon-website"></a></li>
<li class="layui-nav-item layui-hide-xs message"></li>
<li class="layui-nav-item user">
<!-- 头 像 -->
<a href="javascript:;">
<img src="{{ user.avatar }}" id="avatar" class="layui-nav-img">
<span>{{ user.realname }}</span>
</a>
<a class="layui-icon layui-icon-username" href="javascript:;"></a>
<!-- 功 能 菜 单 -->
<dl class="layui-nav-child">
<dd><a user-menu-url="/admin/user/center" user-menu-id="5555" user-menu-title="基本资料">基本资料</a></dd>
<dd><a href="javascript:void(0);" class="logout">注销登录</a></dd>
</dl>
</li>
<!-- 主 题 配 置 -->
<li class="layui-nav-item setting"><a href="#" class="layui-icon layui-icon-more-vertical"></a></li>
</ul>
</div>
<!-- 侧 边 区 域 -->
<div class="layui-side layui-bg-black">
<!-- 顶 部 图 标 -->
<!-- 菜 单 顶 部 -->
<div class="layui-logo">
<img class="logo"/>
<!-- 图 标 -->
<img class="logo">
<!-- 标 题 -->
<span class="title"></span>
</div>
<!-- 侧 边 菜 单 -->
<!-- 菜 单 内 容 -->
<div class="layui-side-scroll">
<div id="sideMenu"></div>
</div>
</div>
<!-- 视 图 页 面 -->
<div class="layui-body">
<!-- 内 容 页 面 -->
<div id="content"></div>
</div>
<!-- 页脚 -->
<div class="layui-footer layui-text">
<span class="left">
<span>
官网:<a href="http://www.pearadmin.com/team.html" target="_blank">www.pearadmin.com</a>&nbsp;&nbsp;&nbsp;
仓库:<a href="https://gitee.com/pear-admin/Pear-Admin-Layui" target="_blank">gitee.com/pear-admin</a>
</span>
</span>
<span class="center"></span>
<span class="right"></span>
</div>
<!-- 遮 盖 层 -->
<div class="pear-cover"></div>
<!-- 移 动 端 便 捷 操 作 -->
<div class="pear-collasped-pe collaspe"><a href="#" class="layui-icon layui-icon-shrink-right"></a></div>
<!-- 加 载 动 画 -->
<div class="loader-main">
<!-- 动 画 对 象 -->
<div class="loader"></div>
</div>
</div>
<!-- 移 动 端 便 捷 操 作 -->
<div class="pear-collapsed-pe collapse">
<a href="#" class="layui-icon layui-icon-shrink-right"></a>
</div>
<!-- 依 赖 脚 本 -->
{% include 'admin/common/footer.html' %}
<!-- 框 架 初 始 化 -->
<script>
layui.use(['admin', 'jquery', 'layer', 'popup'], function () {
let admin = layui.admin
let $ = layui.jquery
let layer = layui.layer
let popup = layui.popup
layui.use(['admin','jquery','popup','drawer'], function() {
var $ = layui.jquery;
var admin = layui.admin;
var popup = layui.popup;
// 框架初始化时会读取 根目录下 pear.configs.yml 文件作为初始化配置
// 你可以通过 admin.setConfigPath 方法修改配置文件位置
// 你可以通过 admin.setConfigType 方法修改配置文件类型
admin.setConfigType('json')
admin.setConfigPath("{{ url_for('rights.configs') }}")
admin.render()
admin.render();
// 注销实现
// 登出逻辑
admin.logout(function () {
let loading = layer.load()
$.ajax({
@@ -106,8 +121,8 @@
})
})
// 初始化消息事件
admin.message()
// 自定义消息点击回调
admin.message(function(id, title, context, form) {});
})
</script>
</body>
+1 -1
View File
@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>登录</title>
{% include 'admin/common/header.html' %}
<!-- 样 式 文 件 -->
<link rel="stylesheet" href="{{ url_for('static', filename='admin/component/pear/css/pear.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='admin/admin/css/other/login.css') }}"/>
+1 -1
View File
@@ -118,7 +118,7 @@
skin: 'line',
height: 'full-148',
toolbar: '#mail-toolbar', /*工具栏*/
text: { none: '暂无人员信息' },
text: { none: '暂无邮件信息' },
defaultToolbar: [{ layEvent: 'refresh', icon: 'layui-icon-refresh' }, 'filter', 'print', 'exports'] /*默认工具栏*/
})
+7
View File
@@ -145,6 +145,13 @@
<td>python版本</td>
<td>{{ python_version }}</td>
</tr>
<tr>
<td>程序操作</td>
<td>
<a href="javascript:kill();"
class="pear-btn pear-btn-xs pear-btn-primary">关闭程序</a>
</td>
</tr>
</tbody>
</table>
</div>
+292
View File
@@ -0,0 +1,292 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>插件管理</title>
{% include 'admin/common/header.html' %}
<link rel="stylesheet" href="{{ url_for('static', filename='/admin/admin/css/other/user.css') }}"/>
</head>
<body class="pear-container">
{# 查询表单 #}
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form" action="" lay-filter="plugin-query-form">
<div class="layui-form-item">
<label class="layui-form-label">插件名称</label>
<div class="layui-input-inline">
<input type="text" name="plugin_name" placeholder="" class="layui-input">
</div>
<button class="pear-btn pear-btn-md pear-btn-primary" lay-submit lay-filter="plugin-query">
<i class="layui-icon layui-icon-search"></i>
查询
</button>
<button type="reset" class="pear-btn pear-btn-md">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</form>
</div>
</div>
{# 用户表格 #}
<div class="user-plugin user-collasped">
<div class="layui-card">
<div class="layui-card-body">
<table id="plugin-table" lay-filter="plugin-table"></table>
</div>
</div>
</div>
</body>
{# 表格操作 #}
<script type="text/html" id="plugin-toolbar">
{% if authorize("admin:plugin:enable") %}
{# <button class="pear-btn pear-btn-primary pear-btn-md" lay-event="add">#}
{# <i class="pear-icon pear-icon-add"></i>#}
{# 新增#}
{# </button>#}
{% endif %}
{% if authorize("admin:plugin:remove") %}
{# <button class="pear-btn pear-btn-md" lay-event="batchRemove">#}
{# <i class="pear-icon pear-icon-ashbin"></i>#}
{# 删除#}
{# </button>#}
{% endif %}
</script>
{# 用户修改操作 #}
<script type="text/html" id="plugin-bar">
{% if authorize("admin:plugin:remove") %}
<button class="pear-btn pear-btn-danger pear-btn-sm" lay-event="remove"><i
class="pear-icon pear-icon-ashbin"></i>
</button>
{% endif %}
</script>
{# 启动与禁用 #}
<script type="text/html" id="plugin-enable">
<input type="checkbox" name="enable" value="{{ "{{ d.plugin_folder_name }}" }}" lay-skin="switch"
lay-text="启用|禁用"
lay-filter="plugin-enable"
{{ "{{# if(d.enable==1){ }} checked {{# } }}" }} />
</script>
{# 用户注册时间 #}
<script type="text/html" id="plugin-createTime">
{{ ' {{layui.util.toDateString(d.create_at, "yyyy-MM-dd HH:mm:ss")}' |safe }}}
</script>
{% include 'admin/common/footer.html' %}
<script>
// 重启程序
function restart() {
layui.use(['layer'], function () {
var $ = layui.jquery;
let popup = layui.popup;
layer.confirm('需要重启程序吗?这样可以使插件更改生效。(如果没有守护进程需要手动启动。)', {
icon: 3,
title: '提示'
}, function (index) {
layer.close(index)
let loading = layer.load()
$.ajax({
url: '/admin/monitor/kill',
dataType: 'json',
type: 'get',
error: function (result) {
setInterval(function () {
tip = false
$.ajax({
url: '/admin/monitor',
type: 'get',
success: function (result) {
layer.close(loading)
if (tip == false) {
popup.success("重启程序成功!", function () {
window.location.reload()
})
}
}
})
}, 1000);
}
})
})
}
)
}
layui.use(['table', 'form', 'jquery', 'popup', 'common'], function () {
let table = layui.table
let form = layui.form
let $ = layui.jquery
let dtree = layui.dtree
let popup = layui.popup
let common = layui.common
let MODULE_PATH = '/plugin/'
// 表格数据
let cols = [
[
{% if authorize("admin:plugin:remove") %}
{type: 'checkbox'},
{% endif %}
{field: 'plugin_name', minWidth: 100, title: '插件名称'},
{field: 'plugin_version', title: '插件版本'},
{field: 'plugin_description', title: '插件描述'},
{field: 'developer_name', title: '开发者名称'},
{field: 'developer_website', title: '开发者网站'},
{field: 'developer_email', title: '开发者邮箱'},
{field: 'developer_phone', title: '开发者电话'},
{field: 'plugin_folder_name', hide: true},
{field: 'enable', title: '状态', templet: '#plugin-enable', width: 100, align: 'center'},
{title: '操作', templet: '#plugin-bar', width: 120, align: 'center'}
]
]
// 渲染表格数据
table.render({
elem: '#plugin-table',
url: MODULE_PATH + 'data',
page: true,
cols: cols,
skin: 'line',
height: 'full-148',
toolbar: '#plugin-toolbar', /*工具栏*/
text: {none: '暂无人员信息'},
defaultToolbar: [{layEvent: 'refresh', icon: 'layui-icon-refresh'}, 'filter', 'print', 'exports'] /*默认工具栏*/
})
// 禁用启用
form.on('switch(plugin-enable)', function (obj) {
let operate
if (obj.elem.checked) {
operate = 'enable'
} else {
operate = 'disable'
}
let loading = layer.load()
$.ajax({
url: MODULE_PATH + operate,
data: JSON.stringify({plugin_folder_name: this.value}),
dataType: 'json',
contentType: 'application/json',
type: 'put',
success: function (result) {
layer.close(loading)
if (result.success) {
popup.success(result.msg)
restart()
} else {
popup.failure(result.msg)
}
}
})
})
table.on('tool(plugin-table)', function (obj) {
if (obj.event === 'remove') {
window.remove(obj)
}
})
table.on('toolbar(plugin-table)', function (obj) {
if (obj.event === 'add') {
window.add()
} else if (obj.event === 'refresh') {
window.refresh()
} else if (obj.event === 'batchRemove') {
window.batchRemove(obj)
}
})
form.on('submit(plugin-query)', function (data) {
window.refresh(data.field)
return false
})
window.add = function () {
layer.open({
type: 2,
title: '新增',
shade: 0.1,
area: ['550px', '550px'],
content: MODULE_PATH + 'add'
})
}
window.remove = function (obj) {
layer.confirm('确定要删除', {icon: 3, title: '提示'}, function (index) {
layer.close(index)
let loading = layer.load()
$.ajax({
url: MODULE_PATH + 'remove/' + obj.data['plugin_folder_name'],
dataType: 'json',
type: 'delete',
success: function (result) {
layer.close(loading)
if (result.success) {
popup.success(result.msg, function () {
obj.del()
})
} else {
popup.failure(result.msg)
}
}
})
})
}
window.batchRemove = function (obj) {
let data = table.checkStatus(obj.config.id).data
if (data.length === 0) {
layer.msg('未选中数据', {
icon: 3,
time: 1000
})
return false
}
var ids = []
var hasCheck = table.checkStatus('plugin-table')
var hasCheckData = hasCheck.data
if (hasCheckData.length > 0) {
$.each(hasCheckData, function (index, element) {
ids.push(element.id)
})
}
{#console.log(ids);#}
layer.confirm('确定要删除选中数据', {
icon: 3,
title: '提示'
}, function (index) {
layer.close(index)
let loading = layer.load()
$.ajax({
url: MODULE_PATH + 'batchRemove',
data: {ids: ids},
dataType: 'json',
type: 'delete',
success: function (result) {
layer.close(loading)
if (result.success) {
popup.success(result.msg, function () {
table.reload('plugin-table')
})
} else {
popup.failure(result.msg)
}
}
})
})
}
window.refresh = function (param) {
table.reload('plugin-table', {where: param})
}
})
</script>
</html>
+1 -3
View File
@@ -4,9 +4,7 @@
<meta charset="UTF-8">
<title>pear-admin-flask</title>
<meta name="description" content="pear-admin-flask">
<meta name="keyword" content="pear-admin-flask">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{% include 'admin/common/header.html' %}
<link rel="stylesheet" href="{{ url_for('static', filename='index/layui/css/layui.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index/css/main.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='index/css/load.css') }}"/>