Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad0f174996 | ||
|
|
45874b17ee | ||
|
|
d7d7e554ee | ||
|
|
8835721924 | ||
|
|
4748a286db | ||
|
|
7501ae1dc2 | ||
|
|
9fc2eb0ae3 | ||
|
|
12d4dfe410 | ||
|
|
d54eb4dc3e | ||
|
|
246ce3b3b7 | ||
|
|
36250fad43 | ||
|
|
0d25b479e5 | ||
|
|
7e6aecc99b | ||
|
|
82de7b0273 | ||
|
|
8760f21b03 | ||
|
|
1e34ef339a | ||
|
|
5333a68416 | ||
|
|
decde3a69a | ||
|
|
9dcc05deda | ||
|
|
c0d963628b | ||
|
|
6978002393 | ||
|
|
3453b01938 | ||
|
|
820ab9f5c6 | ||
|
|
9490ccb7c0 | ||
|
|
056c4b7023 | ||
|
|
d4d8d8b294 | ||
|
|
8bc5d6e0c2 | ||
|
|
3fa8ae0a8a | ||
|
|
316ebc2441 | ||
|
|
550f99e468 | ||
|
|
b9c449ad57 | ||
|
|
db5b4e89ed | ||
|
|
733286b090 | ||
|
|
7505e4552b | ||
|
|
e918727984 | ||
|
|
00053da117 | ||
|
|
27f5d78c35 | ||
|
|
97e0160aaf | ||
|
|
04794e583f | ||
|
|
0640255519 | ||
|
|
460984f209 | ||
|
|
635dcc1dac | ||
|
|
b26237e24b | ||
|
|
a35d443fb6 | ||
|
|
ab4bb1ca40 | ||
|
|
2b7304e615 | ||
|
|
64748e513d | ||
|
|
5b07fed518 | ||
|
|
eb85e30b79 | ||
|
|
b8e0173a9c | ||
|
|
0ca75c81d4 | ||
|
|
aa86bbe372 | ||
|
|
86f07817b1 | ||
|
|
3f74011c98 | ||
|
|
ce8adda79d | ||
|
|
5470837707 | ||
|
|
c26f15ae69 | ||
|
|
4d6d998ae2 | ||
|
|
d628432621 | ||
|
|
2bcaa8e071 | ||
|
|
ebf76055a4 | ||
|
|
b2b08769b0 | ||
|
|
aae15fd07a | ||
|
|
945f746d97 | ||
|
|
dce7908041 | ||
|
|
0ba2b3aa56 | ||
|
|
17792ccacf | ||
|
|
e3dd5458ad | ||
|
|
e9bd8a17a0 | ||
|
|
b8870d4c33 | ||
|
|
8fba71c464 | ||
|
|
0cb74c5eb7 | ||
|
|
6da6768f81 | ||
|
|
05dff28523 | ||
|
|
d7c7237cd4 |
@@ -1,14 +1,3 @@
|
||||
# flask配置
|
||||
FLASK_APP="entrance:create_app()"
|
||||
FLASK_ENV=development
|
||||
FLASK_DEBUG=1
|
||||
FLASK_RUN_HOST = 127.0.0.1
|
||||
FLASK_RUN_PORT = 5000
|
||||
|
||||
# MySql配置信息
|
||||
MYSQL_HOST=127.0.0.1
|
||||
# MYSQL_HOST=dbserver
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=next
|
||||
MYSQL_USERNAME=root
|
||||
MYSQL_PASSWORD=root
|
||||
FLASK_RUN_HOST=0.0.0.0
|
||||
FLASK_APP='applications:create_app'
|
||||
|
||||
+7
-1
@@ -115,4 +115,10 @@ dmypy.json
|
||||
.pyre/
|
||||
|
||||
#ide
|
||||
.idea/
|
||||
.idea/
|
||||
|
||||
# 数据库文件
|
||||
*.db
|
||||
#上传到本地的图片文件
|
||||
*upload/
|
||||
migrations/
|
||||
@@ -1,6 +1,7 @@
|
||||
<div align="center">
|
||||
<br/>
|
||||
<br/>
|
||||
<img src="https://gitee.com/pear-admin/Pear-Admin-Layui/raw/master/admin/images/logo.png" width="90px" style="margin-top:30px;"/>
|
||||
<h1 align="center">
|
||||
Pear Admin Flask
|
||||
</h1>
|
||||
@@ -28,29 +29,108 @@
|
||||
<img width="92%" style="border-radius:10px;margin-top:20px;margin-bottom:20px;box-shadow: 2px 0 6px gray;" src="https://images.gitee.com/uploads/images/2020/1019/104805_042b888c_4835367.png" />
|
||||
</div>
|
||||
|
||||
#### 项目简介
|
||||
>Pear Admin Flask 新版本开发中
|
||||
## 项目简介
|
||||
Pear Admin Flask 基于 Flask 生态的后台管理系统,该项目旨在为 python 开发者提供一个快速开发前后端半分离的后台管理系统的模板
|
||||
|
||||
#### 安装
|
||||
在使用本项目之前最好掌握以下知识点
|
||||
|
||||
```shell
|
||||
pip install -r require/common.txt
|
||||
+ [html、css、JavaScript](https://developer.mozilla.org/zh-CN/docs/Learn/HTML)
|
||||
+ [jQuery](https://www.w3school.com.cn/jquery/index.asp)
|
||||
+ [layui](https://www.layui.com/)
|
||||
+ [flask](https://dormousehole.readthedocs.io/en/latest/)
|
||||
+ [flask-login](https://flask-login.readthedocs.io/en/latest/)
|
||||
+ [flask-sqlalchemy](http://www.pythondoc.com/flask-sqlalchemy/quickstart.html)
|
||||
|
||||
|
||||
## 预览
|
||||
Pear Admin Flask 有以下几个版本:
|
||||
|
||||
[master分支版本 ](https://gitee.com/pear-admin/pear-admin-flask/tree/master/)
|
||||
|
||||
>flask 2.x + flask-sqlalchemy + 权限验证 + Flask-APScheduler 定时任务 + marshmallow 序列化与数据验证
|
||||
|
||||
[Mini 分支版本 ](https://gitee.com/pear-admin/pear-admin-flask/tree/mini/)
|
||||
|
||||
>flask 2.x + flask-sqlalchemy + 基于角色的权限管理
|
||||
|
||||
| | |
|
||||
|---------------------|---------------------|
|
||||
|  |  |
|
||||
| |  |
|
||||
|  |  |
|
||||
|
||||
## 内置功能
|
||||
|
||||
- [x] 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
|
||||
- [x] 权限管理:配置系统菜单,操作权限,按钮权限标识等。
|
||||
- [x] 角色管理:角色菜单权限分配。
|
||||
- [x] 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
|
||||
- [x] 登录日志:系统登录日志记录查询包含登录异常。
|
||||
- [x] 文件上传: 图片上传示例
|
||||
|
||||
|
||||
## 文档
|
||||
正在编写
|
||||
|
||||
|
||||
## 安装使用
|
||||
|
||||
+ 下载源码
|
||||
```bash
|
||||
git clone https://gitee.com/pear-admin/pear-admin-flask
|
||||
|
||||
# 切换分支
|
||||
git checkout mini
|
||||
```
|
||||
|
||||
#### 目录结构
|
||||
+ 安装依赖
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
python -m venv venv
|
||||
|
||||
pear-admin-flask
|
||||
# 然后使虚拟环境生效(windows)
|
||||
venv\Scripts\activate
|
||||
# source venv/bin/activate # (Linux激活虚拟环境)
|
||||
|
||||
common -> 公共
|
||||
# 安装依赖
|
||||
pip install -r requirement.txt
|
||||
```
|
||||
|
||||
entrance -> 核心
|
||||
+ 数据迁移
|
||||
|
||||
logs -> 日志
|
||||
默认的使用 `sqlite3` 作为测试环境的数据库进行演示,不需要按照mysql即可查看演示。如果需要二次开发,建议改成 `mysql` 。
|
||||
|
||||
modules -> 蓝图,视图,模型,服务
|
||||
如果需要在开发环境使用 mysql 作为数据库,请查看 `applications/configs/config.py` 文件里面的相关配置文件, 注释掉 sqlite 的配置即可
|
||||
|
||||
require -> 安装需要的文件
|
||||
如果需要修改数据的配置信息,请在 `.flaskenv` 里面调整即可
|
||||
|
||||
.flaskenv -> 配置文件
|
||||
```bash
|
||||
flask db init
|
||||
flask db migrate -m '数据初始化'
|
||||
flask db upgrade
|
||||
|
||||
app.py -> 启动模块(使用flask run命令的话可以删除)
|
||||
flask init-db
|
||||
```
|
||||
|
||||
+ 项目启动
|
||||
|
||||
```bash
|
||||
flask run
|
||||
```
|
||||
|
||||
## Git 贡献提交规范
|
||||
|
||||
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
@@ -1,6 +0,0 @@
|
||||
from entrance import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
|
||||
from flask import Flask, Blueprint
|
||||
|
||||
import config
|
||||
from applications.view import init_view
|
||||
from common import register_api
|
||||
from common.flask_uploads import configure_uploads
|
||||
from common.utils.upload import photos
|
||||
from extensions import init_plugs
|
||||
|
||||
api_bp: Blueprint = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
|
||||
from .rights import register_rights_api
|
||||
from .system import register_sys_api
|
||||
from .users import register_users_api
|
||||
|
||||
register_rights_api(api_bp)
|
||||
register_users_api(api_bp)
|
||||
register_sys_api(api_bp)
|
||||
|
||||
|
||||
def init_api(app: Flask) -> None:
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
app = Flask('pear-admin-flask')
|
||||
|
||||
# 引入数据库配置
|
||||
app.config.from_object(config)
|
||||
|
||||
# 注册各种插件
|
||||
init_plugs(app)
|
||||
|
||||
# 注册路由
|
||||
init_view(app)
|
||||
|
||||
# 注册接口(restful api)
|
||||
init_api(app)
|
||||
|
||||
# 文件上传
|
||||
configure_uploads(app, photos)
|
||||
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
logo()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def logo():
|
||||
print('''
|
||||
_____ _ _ ______ _ _
|
||||
| __ \ /\ | | (_) | ____| | | |
|
||||
| |__) |__ __ _ _ __ / \ __| |_ __ ___ _ _ __ | |__ | | __ _ ___| | __
|
||||
| ___/ _ \/ _` | '__| / /\ \ / _` | '_ ` _ \| | '_ \ | __| | |/ _` / __| |/ /
|
||||
| | | __/ (_| | | / ____ \ (_| | | | | | | | | | | | | | | (_| \__ \ <
|
||||
|_| \___|\__,_|_| /_/ \_\__,_|_| |_| |_|_|_| |_| |_| |_|\__,_|___/_|\_\\
|
||||
|
||||
''')
|
||||
@@ -0,0 +1,42 @@
|
||||
from common import register_api
|
||||
from .department import DepartmentsApi, DeptEnableAPI
|
||||
from .rights import (
|
||||
RightsApi, PowerApi, right_power_enable_resource,
|
||||
admin_configs_resource, admin_menu_resource
|
||||
)
|
||||
from .roles import RoleRoleApi, RolePowerApi, role_deletes, role_enable_resource
|
||||
|
||||
|
||||
def register_rights_api(api_bp):
|
||||
register_api(DepartmentsApi, 'rights_dept_api', '/dept/department/', pk='_id', app=api_bp)
|
||||
api_bp.add_url_rule('/dept/department/<int:_id>/status',
|
||||
view_func=DeptEnableAPI.as_view('dept_status_api'),
|
||||
methods=['PUT', ])
|
||||
api_bp.add_url_rule('/rights/rights',
|
||||
view_func=RightsApi.as_view('dept_rights_api'),
|
||||
methods=['GET', 'DELETE'])
|
||||
|
||||
register_api(PowerApi, 'rights_power_api', '/rights/power/', pk='_id', app=api_bp)
|
||||
api_bp.add_url_rule('/rights/configs',
|
||||
endpoint='rights_config_api',
|
||||
view_func=admin_configs_resource,
|
||||
methods=['GET'])
|
||||
api_bp.add_url_rule('/rights/menu',
|
||||
endpoint='rights_menu_api',
|
||||
view_func=admin_menu_resource,
|
||||
methods=['GET'])
|
||||
api_bp.add_url_rule('/rights/power/<int:_id>/<action>',
|
||||
endpoint='rights_power_action_api',
|
||||
view_func=right_power_enable_resource,
|
||||
methods=['PUT'])
|
||||
api_bp.add_url_rule('/roles/role',
|
||||
endpoint='rights_roles_role_api',
|
||||
view_func=role_deletes,
|
||||
methods=['DELETE'])
|
||||
|
||||
register_api(RoleRoleApi, 'rights_role_api', '/roles/role/', pk='_id', app=api_bp)
|
||||
register_api(RolePowerApi, 'rights_role_power_api', '/roles/role_power/', pk='_id', app=api_bp)
|
||||
api_bp.add_url_rule('/roles/role/<int:_id>/status',
|
||||
endpoint='rights_roles_enable_api',
|
||||
view_func=role_enable_resource,
|
||||
methods=['PUT'])
|
||||
@@ -0,0 +1,112 @@
|
||||
import typing as t
|
||||
|
||||
from flask.views import MethodView
|
||||
from flask_pydantic import validate
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from common.utils.http import success_api, fail_api, table_api
|
||||
from extensions import db
|
||||
from models import DepartmentModel, UserModel
|
||||
|
||||
|
||||
class DeptSchema(BaseModel):
|
||||
address: t.Optional[str]
|
||||
dept_name: t.Optional[str] = Field(alias='deptName')
|
||||
email: t.Optional[str]
|
||||
leader: t.Optional[str]
|
||||
parent_id: t.Optional[str] = Field(alias='parentId')
|
||||
phone: t.Optional[str]
|
||||
sort: t.Optional[int]
|
||||
status: t.Optional[int]
|
||||
|
||||
|
||||
class DepartmentsApi(MethodView):
|
||||
|
||||
def get(self, _id):
|
||||
if _id:
|
||||
dept = DepartmentModel.query.filter_by(id=_id).first()
|
||||
dept_data = {
|
||||
'id': dept.id,
|
||||
'dept_name': dept.dept_name,
|
||||
'leader': dept.leader,
|
||||
'email': dept.email,
|
||||
'phone': dept.phone,
|
||||
'status': dept.status,
|
||||
'sort': dept.sort,
|
||||
'address': dept.address,
|
||||
}
|
||||
return dict(success=True, message='ok', dept=dept_data)
|
||||
|
||||
dept_data = DepartmentModel.query.order_by(DepartmentModel.sort).all()
|
||||
return table_api(
|
||||
result={
|
||||
'items': [
|
||||
{
|
||||
'deptId': item.id,
|
||||
'parentId': item.parent_id,
|
||||
'deptName': item.dept_name,
|
||||
'sort': item.sort,
|
||||
'leader': item.leader,
|
||||
'phone': item.phone,
|
||||
'email': item.email,
|
||||
'status': item.status,
|
||||
'comment': item.comment,
|
||||
'address': item.address,
|
||||
'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
} for item in dept_data
|
||||
],
|
||||
'total': len(dept_data)}
|
||||
, code=0)
|
||||
|
||||
@validate()
|
||||
def post(self, body: DeptSchema):
|
||||
dept = DepartmentModel(
|
||||
parent_id=body.parent_id,
|
||||
dept_name=body.dept_name,
|
||||
sort=body.sort,
|
||||
leader=body.leader,
|
||||
phone=body.phone,
|
||||
email=body.email,
|
||||
status=body.status,
|
||||
address=body.address
|
||||
)
|
||||
db.session.add(dept)
|
||||
db.session.commit()
|
||||
|
||||
return success_api(message="成功")
|
||||
|
||||
@validate()
|
||||
def put(self, _id, body: DeptSchema):
|
||||
data = {
|
||||
"dept_name": body.dept_name,
|
||||
"sort": body.sort,
|
||||
"leader": body.leader,
|
||||
"phone": body.phone,
|
||||
"email": body.email,
|
||||
"status": body.status,
|
||||
"address": body.address
|
||||
}
|
||||
body = DepartmentModel.query.filter_by(id=_id).update(data)
|
||||
if not body:
|
||||
return {'success': False, 'message': "更新失败", 'code': 404}
|
||||
db.session.commit()
|
||||
return {'success': True, 'message': '更新成功', 'code': 200}
|
||||
|
||||
def delete(self, _id):
|
||||
ret = DepartmentModel.query.filter_by(id=_id).delete()
|
||||
UserModel.query.filter_by(dept_id=_id).update({"dept_id": None})
|
||||
db.session.commit()
|
||||
if ret:
|
||||
return success_api(message="删除成功")
|
||||
return fail_api(message="删除失败")
|
||||
|
||||
|
||||
class DeptEnableAPI(MethodView):
|
||||
def put(self, _id):
|
||||
d = DepartmentModel.query.get(_id)
|
||||
if d:
|
||||
d.status = not d.status
|
||||
db.session.commit()
|
||||
message = '修改成功'
|
||||
return success_api(message=message)
|
||||
return fail_api(message="出错啦")
|
||||
@@ -0,0 +1,278 @@
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
from typing import Optional
|
||||
|
||||
from flask import request, jsonify, current_app
|
||||
from flask.views import MethodView
|
||||
from flask_login import current_user
|
||||
from flask_pydantic import validate
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from common.utils.http import success_api, fail_api
|
||||
from extensions import db
|
||||
from models import RightModel, RoleModel
|
||||
|
||||
|
||||
def get_render_config():
|
||||
# 网站配置
|
||||
config = {
|
||||
'logo': {
|
||||
# 网站名称
|
||||
"title": current_app.config.get("SYSTEM_NAME"),
|
||||
# 网站图标
|
||||
"image": "/static/admin/admin/images/logo.png"
|
||||
# 菜单配置
|
||||
},
|
||||
'menu': {
|
||||
# 菜单数据来源
|
||||
"data": "/api/v1/rights/menu",
|
||||
"collaspe": True,
|
||||
# 是否同时只打开一个菜单目录
|
||||
"accordion": True,
|
||||
"method": "GET",
|
||||
# 是否开启多系统菜单模式
|
||||
"control": False,
|
||||
# 默认选中的菜单项
|
||||
"select": "0",
|
||||
# 是否开启异步菜单,false 时 data 属性设置为菜单数据,false 时为 json 文件或后端接口
|
||||
"async": True
|
||||
},
|
||||
'tab': {
|
||||
# 是否开启多选项卡
|
||||
"muiltTab": True,
|
||||
# 切换选项卡时,是否刷新页面状态
|
||||
"keepState": True,
|
||||
# 是否开启 Tab 记忆
|
||||
"session": True,
|
||||
# 最大可打开的选项卡数量
|
||||
"tabMax": 30,
|
||||
"index": {
|
||||
# 标识 ID , 建议与菜单项中的 ID 一致
|
||||
"id": "10",
|
||||
# 页面地址
|
||||
"href": "/admin/welcome",
|
||||
# 标题
|
||||
"title": "首页"
|
||||
}
|
||||
},
|
||||
'theme': {
|
||||
# 默认主题色,对应 colors 配置中的 ID 标识
|
||||
"defaultColor": "2",
|
||||
# 默认的菜单主题 dark-theme 黑 / light-theme 白
|
||||
"defaultMenu": "dark-theme",
|
||||
# 是否允许用户切换主题,false 时关闭自定义主题面板
|
||||
"allowCustom": True
|
||||
},
|
||||
'colors': [
|
||||
{
|
||||
"id": "1",
|
||||
"color": "#2d8cf0"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"color": "#5FB878"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"color": "#1E9FFF"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"color": "#FFB800"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"color": "darkgray"
|
||||
},
|
||||
],
|
||||
'links': current_app.config.get("SYSTEM_PANEL_LINKS"),
|
||||
'other': {
|
||||
# 主页动画时长
|
||||
"keepLoad": 1200,
|
||||
# 布局顶部主题
|
||||
"autoHead": False
|
||||
},
|
||||
'header': False
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
# 生成菜单树
|
||||
def make_menu_tree():
|
||||
role = current_user.role
|
||||
powers = []
|
||||
for i in role:
|
||||
# 如果角色没有被启用就直接跳过
|
||||
if i.enable == 0:
|
||||
continue
|
||||
# 变量角色用户的权限
|
||||
for p in i.power:
|
||||
# 如果权限关闭了就直接跳过
|
||||
if p.enable == 0:
|
||||
continue
|
||||
# 一二级菜单
|
||||
if p.type == 0 or p.type == 1:
|
||||
powers.append(p)
|
||||
|
||||
power_dict = [
|
||||
{
|
||||
'id': item.id,
|
||||
'title': item.name,
|
||||
'type': item.type,
|
||||
'code': item.code,
|
||||
'href': item.url,
|
||||
'openType': item.open_type,
|
||||
'parent_id': item.parent_id,
|
||||
'icon': item.icon,
|
||||
'sort': item.sort,
|
||||
'enable': item.enable,
|
||||
} for item in powers
|
||||
]
|
||||
power_dict.sort(key=lambda x: x['id'], reverse=True)
|
||||
|
||||
menu_dict = OrderedDict()
|
||||
for _dict in power_dict:
|
||||
if _dict['id'] in menu_dict:
|
||||
# 当前节点添加子节点
|
||||
_dict['children'] = copy.deepcopy(menu_dict[_dict['id']])
|
||||
_dict['children'].sort(key=lambda item: item['sort'])
|
||||
# 删除子节点
|
||||
del menu_dict[_dict['id']]
|
||||
|
||||
if _dict['parent_id'] not in menu_dict:
|
||||
menu_dict[_dict['parent_id']] = [_dict]
|
||||
else:
|
||||
menu_dict[_dict['parent_id']].append(_dict)
|
||||
|
||||
return sorted(menu_dict.get(0), key=lambda item: item['sort'])
|
||||
|
||||
|
||||
class PowerSchema(BaseModel):
|
||||
icon: str
|
||||
open_type: Optional[str] = Field(alias='openType')
|
||||
parent_id: Optional[str] = Field(alias='parentId')
|
||||
power_code: Optional[str] = Field(alias='powerCode')
|
||||
power_name: Optional[str] = Field(alias='powerName')
|
||||
power_type: Optional[str] = Field(alias='powerType')
|
||||
power_url: Optional[str] = Field(alias='powerUrl')
|
||||
sort: Optional[int] = Field(alias='sort')
|
||||
|
||||
|
||||
class RightsApi(MethodView):
|
||||
def get(self):
|
||||
"""获取选择父节点"""
|
||||
|
||||
power = RightModel.query.all()
|
||||
# power_data = marshal(power, RightModel.fields())
|
||||
power_data = [
|
||||
{
|
||||
'powerId': item.id,
|
||||
'powerName': item.name,
|
||||
'powerType': item.type,
|
||||
'powerUrl': item.url,
|
||||
'openType': item.open_type,
|
||||
'parentId': item.parent_id,
|
||||
'icon': item.icon,
|
||||
'sort': item.sort,
|
||||
'enable': item.enable,
|
||||
} for item in power
|
||||
]
|
||||
power_data.append({"powerId": 0, "powerName": "顶级权限", "parentId": -1})
|
||||
res = {
|
||||
"status": {"code": 200, "message": "默认"},
|
||||
"data": power_data
|
||||
}
|
||||
return res
|
||||
|
||||
def delete(self):
|
||||
ids = request.form.getlist('ids[]')
|
||||
for id in ids:
|
||||
role = RoleModel.query.filter_by(id=id).first()
|
||||
# 删除该角色的权限和用户
|
||||
role.power = []
|
||||
role.user = []
|
||||
|
||||
r = RoleModel.query.filter_by(id=id).delete()
|
||||
db.session.commit()
|
||||
return success_api(message="批量删除成功")
|
||||
|
||||
|
||||
class PowerApi(MethodView):
|
||||
@validate()
|
||||
def post(self, body: PowerSchema):
|
||||
power = RightModel(
|
||||
icon=body.icon,
|
||||
open_type=body.open_type,
|
||||
parent_id=body.parent_id,
|
||||
code=body.power_code,
|
||||
name=body.power_name,
|
||||
type=body.power_type,
|
||||
url=body.power_url,
|
||||
sort=body.sort,
|
||||
enable=1
|
||||
)
|
||||
try:
|
||||
db.session.add(power)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return fail_api(message='数据提交失败')
|
||||
|
||||
return success_api(message="成功")
|
||||
|
||||
def delete(self, _id):
|
||||
# 删除权限(目前没有判断父节点自动删除子节点)
|
||||
power = RightModel.query.filter_by(id=_id).first()
|
||||
role_id_list = []
|
||||
roles = power.role
|
||||
for role in roles:
|
||||
role_id_list.append(role.id)
|
||||
roles = RoleModel.query.filter(RoleModel.id.in_(role_id_list)).all()
|
||||
for p in roles:
|
||||
power.role.remove(p)
|
||||
r = RightModel.query.filter_by(id=_id).delete()
|
||||
db.session.commit()
|
||||
|
||||
if r:
|
||||
return success_api(message="删除成功")
|
||||
else:
|
||||
return fail_api(message="删除失败")
|
||||
|
||||
@validate()
|
||||
def put(self, _id, body: PowerSchema):
|
||||
data = {
|
||||
"icon": body.icon,
|
||||
"open_type": body.open_type,
|
||||
"parent_id": body.parent_id,
|
||||
"code": body.power_code,
|
||||
"name": body.power_name,
|
||||
"type": body.power_type,
|
||||
"url": body.power_url,
|
||||
"sort": body.sort
|
||||
}
|
||||
power = RightModel.query.filter_by(id=_id).update(data)
|
||||
db.session.commit()
|
||||
|
||||
if not power:
|
||||
return fail_api(message="更新权限失败")
|
||||
return success_api(message="更新权限成功")
|
||||
|
||||
|
||||
def right_power_enable_resource(_id, action):
|
||||
power = RightModel.query.get(_id)
|
||||
if power:
|
||||
power.enable = not power.enable
|
||||
db.session.commit()
|
||||
message = "修改成功"
|
||||
return success_api(message=message)
|
||||
else:
|
||||
return fail_api(message="出错啦")
|
||||
|
||||
|
||||
def admin_configs_resource():
|
||||
return get_render_config()
|
||||
|
||||
|
||||
def admin_menu_resource():
|
||||
menu_tree = make_menu_tree()
|
||||
return jsonify(menu_tree)
|
||||
@@ -0,0 +1,162 @@
|
||||
from flask import request
|
||||
from flask.views import MethodView
|
||||
|
||||
from common.utils.http import table_api, success_api, fail_api
|
||||
from extensions import db
|
||||
from models import RightModel, RoleModel
|
||||
|
||||
|
||||
def role_deletes():
|
||||
ids = request.form.getlist('ids[]')
|
||||
for id in ids:
|
||||
role = RoleModel.query.filter_by(id=id).first()
|
||||
# 删除该角色的权限和用户
|
||||
role.power = []
|
||||
role.user = []
|
||||
|
||||
r = RoleModel.query.filter_by(id=id).delete()
|
||||
db.session.commit()
|
||||
return success_api(message="批量删除成功")
|
||||
|
||||
|
||||
class RoleRoleApi(MethodView):
|
||||
|
||||
def get(self, _id):
|
||||
if not _id:
|
||||
page = request.args.get('page', default=1, type=int)
|
||||
limit = request.args.get('limit', default=10, type=int)
|
||||
role_name = request.args.get('roleName', default="", type=str)
|
||||
role_code = request.args.get('roleCode', default="", type=str)
|
||||
|
||||
filters = []
|
||||
if role_name:
|
||||
filters.append(RoleModel.name.like('%' + role_name + '%'))
|
||||
if role_code:
|
||||
filters.append(RoleModel.code.like('%' + role_code + '%'))
|
||||
|
||||
paginate = RoleModel.query.filter(*filters).paginate(page=page, per_page=limit, error_out=False)
|
||||
|
||||
return table_api(
|
||||
result={
|
||||
'items': [{'id': item.id,
|
||||
'roleName': item.name,
|
||||
'roleCode': item.code,
|
||||
'enable': item.enable,
|
||||
'comment': item.comment,
|
||||
'details': item.details,
|
||||
'sort': item.sort,
|
||||
} for item in paginate.items],
|
||||
'total': paginate.total}
|
||||
, code=0)
|
||||
|
||||
def post(self):
|
||||
# TODO 添加校验
|
||||
details = request.json.get('details', '')
|
||||
enable = request.json.get('enable', 0)
|
||||
role_code = request.json.get('roleCode', '')
|
||||
role_name = request.json.get('roleName', '')
|
||||
sort = request.json.get('sort', 0)
|
||||
|
||||
role = RoleModel(
|
||||
details=details,
|
||||
enable=int(enable),
|
||||
code=role_code,
|
||||
name=role_name,
|
||||
sort=int(sort)
|
||||
)
|
||||
db.session.add(role)
|
||||
db.session.commit()
|
||||
return success_api(message="成功")
|
||||
|
||||
# 更新角色
|
||||
def put(self, _id):
|
||||
# TODO 添加校验
|
||||
role_code = request.json.get('roleCode', 0)
|
||||
role_name = request.json.get('roleName', '')
|
||||
sort = request.json.get('sort', 0) # int
|
||||
enable = request.json.get('enable', 0) # int
|
||||
details = request.json.get('details', '')
|
||||
|
||||
data = {
|
||||
"code": role_code,
|
||||
"name": role_name,
|
||||
"sort": sort,
|
||||
"enable": enable,
|
||||
"details": details
|
||||
}
|
||||
|
||||
role = RoleModel.query.filter_by(id=_id).update(data)
|
||||
db.session.commit()
|
||||
if not role:
|
||||
return fail_api(message="更新角色失败")
|
||||
return success_api(message="更新角色成功")
|
||||
|
||||
|
||||
def role_enable_resource(_id):
|
||||
"""启用用户"""
|
||||
ret = RoleModel.query.get(_id)
|
||||
ret.enable = not ret.enable
|
||||
db.session.commit()
|
||||
|
||||
message = "修改成功"
|
||||
if not ret:
|
||||
return fail_api(message="出错啦")
|
||||
return success_api(message=message)
|
||||
|
||||
|
||||
class RolePowerApi(MethodView):
|
||||
|
||||
def get(self, _id):
|
||||
# 获取角色权限
|
||||
role = RoleModel.query.filter_by(id=_id).first()
|
||||
# 获取权限列表的 id
|
||||
check_powers_list = [rp.id for rp in role.power]
|
||||
powers = RightModel.query.all() # 获取所有的权限
|
||||
powers = [
|
||||
{
|
||||
'powerId': item.id,
|
||||
'powerName': item.name,
|
||||
'powerType': item.type,
|
||||
'powerUrl': item.url,
|
||||
'openType': item.open_type,
|
||||
'parentId': item.parent_id,
|
||||
'icon': item.icon,
|
||||
'sort': item.sort,
|
||||
'enable': item.enable,
|
||||
} for item in powers]
|
||||
for i in powers:
|
||||
if int(i.get("powerId")) in check_powers_list:
|
||||
i["checkArr"] = "1"
|
||||
else:
|
||||
i["checkArr"] = "0"
|
||||
return {
|
||||
"data": powers,
|
||||
"status": {"code": 200, "message": "默认"}
|
||||
}
|
||||
|
||||
# 保存角色权限
|
||||
def put(self, _id):
|
||||
power_ids = request.json.get('powerIds', '')
|
||||
|
||||
power_list = power_ids.split(',')
|
||||
|
||||
""" 更新角色权限 """
|
||||
role = RoleModel.query.filter_by(id=_id).first()
|
||||
powers = RightModel.query.filter(RightModel.id.in_(power_list)).all()
|
||||
role.power = powers
|
||||
|
||||
db.session.commit()
|
||||
return success_api(message="授权成功")
|
||||
|
||||
# 角色删除
|
||||
def delete(self, _id):
|
||||
role = RoleModel.query.filter_by(id=_id).first()
|
||||
# 删除该角色的权限和用户
|
||||
role.power = []
|
||||
role.user = []
|
||||
|
||||
r = RoleModel.query.filter_by(id=_id).delete()
|
||||
db.session.commit()
|
||||
if not r:
|
||||
return fail_api(message="角色删除失败")
|
||||
return success_api(message="角色删除成功")
|
||||
@@ -0,0 +1,8 @@
|
||||
from common import register_api
|
||||
from .file import FilePhotoAPI
|
||||
from .passport import LoginAPI
|
||||
|
||||
|
||||
def register_sys_api(api_bp):
|
||||
register_api(LoginAPI, 'login_api', '/passport/login', pk='_id', app=api_bp)
|
||||
register_api(FilePhotoAPI, 'photo_api', '/file/photo', pk='photo_id', app=api_bp)
|
||||
@@ -0,0 +1,78 @@
|
||||
from flask import request, jsonify
|
||||
from flask.views import MethodView
|
||||
from sqlalchemy import desc
|
||||
|
||||
from common.utils.http import fail_api, success_api, table_api
|
||||
from common.utils.upload import upload_one, delete_photo_by_id
|
||||
from models import PhotoModel
|
||||
|
||||
|
||||
class FilePhotoAPI(MethodView):
|
||||
|
||||
def get(self, photo_id):
|
||||
if photo_id is None:
|
||||
page = request.args.get('page', type=int, default=1)
|
||||
limit = request.args.get('limit', type=int, default=10)
|
||||
photo_paginate = PhotoModel.query.order_by(
|
||||
desc(PhotoModel.create_at)).paginate(
|
||||
page=page, per_page=limit, error_out=False)
|
||||
data = [
|
||||
{
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'href': item.href,
|
||||
'mime': item.mime,
|
||||
'size': item.size,
|
||||
'ext': item.ext if hasattr(item, 'ext') else "",
|
||||
'create_at': str(item.create_at),
|
||||
} for item in photo_paginate.items
|
||||
]
|
||||
return table_api(
|
||||
result={
|
||||
'items': data,
|
||||
'total': photo_paginate.total,
|
||||
},
|
||||
code=0)
|
||||
else:
|
||||
# 显示一张图片
|
||||
item = PhotoModel.query.get(photo_id)
|
||||
return table_api(
|
||||
result={
|
||||
'items': {
|
||||
'id': item.id,
|
||||
'name': item.name,
|
||||
'href': item.href,
|
||||
'mime': item.mime,
|
||||
'size': item.size,
|
||||
'ext': item.ext if hasattr(item, 'ext') else "",
|
||||
'create_at': str(item.create_at),
|
||||
},
|
||||
'total': 1,
|
||||
},
|
||||
code=0
|
||||
)
|
||||
|
||||
def post(self):
|
||||
if 'file' in request.files:
|
||||
photo = request.files['file']
|
||||
mime = request.files['file'].content_type
|
||||
file_url = upload_one(photo=photo, mime=mime)
|
||||
|
||||
res = {
|
||||
"message": "上传成功",
|
||||
"code": 0,
|
||||
"success": True,
|
||||
"data": {"src": file_url},
|
||||
}
|
||||
return jsonify(res)
|
||||
return fail_api()
|
||||
|
||||
def delete(self, photo_id):
|
||||
res = delete_photo_by_id(photo_id)
|
||||
if res:
|
||||
return success_api(message="删除成功")
|
||||
else:
|
||||
return fail_api(message="删除失败")
|
||||
|
||||
def put(self, photo_id):
|
||||
pass
|
||||
@@ -0,0 +1,52 @@
|
||||
from flask import render_template, make_response
|
||||
from flask import session, redirect, url_for
|
||||
from flask.views import MethodView
|
||||
from flask_login import current_user, login_user
|
||||
from flask_pydantic import validate
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common.gen_captcha import add_auth_session
|
||||
from common.utils.http import fail_api, success_api
|
||||
from common.utils.rights import record_logging
|
||||
from models import UserModel
|
||||
|
||||
|
||||
class LoginSchema(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
captcha: str
|
||||
|
||||
|
||||
class LoginAPI(MethodView):
|
||||
|
||||
def get(self):
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('admin.index'))
|
||||
return make_response(render_template('index/login.html'))
|
||||
|
||||
@validate()
|
||||
def post(self, body: LoginSchema):
|
||||
s_code = session.get("code", None)
|
||||
session["code"] = None
|
||||
|
||||
if body.captcha != s_code:
|
||||
return fail_api(message="验证码错误")
|
||||
user = UserModel.query.filter_by(username=body.username).first()
|
||||
|
||||
if user is None:
|
||||
return fail_api(message="不存在的用户")
|
||||
|
||||
if user.enable == 0:
|
||||
return fail_api(message="用户被暂停使用")
|
||||
|
||||
if user.validate_password(body.password):
|
||||
# 登录
|
||||
login_user(user)
|
||||
# 记录登录日志
|
||||
record_logging()
|
||||
|
||||
# 存入权限
|
||||
add_auth_session()
|
||||
return success_api(message="登录成功")
|
||||
record_logging()
|
||||
return fail_api(message="用户名或密码错误")
|
||||
@@ -0,0 +1,14 @@
|
||||
from common import register_api
|
||||
from .user import UserApi, user_role_resource, user_info
|
||||
|
||||
|
||||
def register_users_api(api_bp):
|
||||
api_bp.add_url_rule('/users/user/<int:_id>/<action>',
|
||||
view_func=user_info,
|
||||
methods=['PUT'])
|
||||
|
||||
register_api(UserApi, 'users_api', '/users/user/', pk='_id', app=api_bp)
|
||||
|
||||
api_bp.add_url_rule('/users/user/<int:_id>/role',
|
||||
view_func=user_role_resource,
|
||||
methods=['PUT'])
|
||||
@@ -0,0 +1,243 @@
|
||||
import typing as t
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask.views import MethodView
|
||||
from flask_login import current_user
|
||||
from flask_pydantic import validate
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import desc
|
||||
|
||||
from common.utils.http import fail_api, success_api, table_api
|
||||
from extensions import db
|
||||
from models import LogModel
|
||||
from models import UserModel, RoleModel, DepartmentModel
|
||||
|
||||
|
||||
def get_current_user_logs():
|
||||
""" 获取当前用户日志 """
|
||||
log = LogModel.query.filter_by(url='/passport/login').filter_by(uid=current_user.id).order_by(
|
||||
desc(LogModel.create_at)).limit(10)
|
||||
return log
|
||||
|
||||
|
||||
def is_user_exists(username):
|
||||
""" 判断用户是否存在 """
|
||||
res = UserModel.query.filter_by(username=username).count()
|
||||
return bool(res)
|
||||
|
||||
|
||||
def delete_by_id(_id):
|
||||
""" 删除用户 """
|
||||
user = UserModel.query.filter_by(id=_id).first()
|
||||
roles_id = []
|
||||
for role in user.role:
|
||||
roles_id.append(role.id)
|
||||
roles = RoleModel.query.filter(RoleModel.id.in_(roles_id)).all()
|
||||
for r in roles:
|
||||
user.role.remove(r)
|
||||
res = UserModel.query.filter_by(id=_id).delete()
|
||||
db.session.commit()
|
||||
return res
|
||||
|
||||
|
||||
def batch_remove(ids):
|
||||
""" 批量删除 """
|
||||
for _id in ids:
|
||||
delete_by_id(_id)
|
||||
|
||||
|
||||
def update_user_role(_id, roles_list):
|
||||
user = UserModel.query.filter_by(id=_id).first()
|
||||
roles_id = []
|
||||
for role in user.role:
|
||||
roles_id.append(role.id)
|
||||
roles = RoleModel.query.filter(RoleModel.id.in_(roles_id)).all()
|
||||
for r in roles:
|
||||
user.role.remove(r)
|
||||
roles = RoleModel.query.filter(RoleModel.id.in_(roles_list)).all()
|
||||
for r in roles:
|
||||
user.role.append(r)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def users_delete():
|
||||
"""批量删除"""
|
||||
ids = request.form.getlist('ids[]')
|
||||
batch_remove(ids)
|
||||
return success_api(message="批量删除成功")
|
||||
|
||||
|
||||
class QuerySchema(BaseModel):
|
||||
page: int = 1
|
||||
limit: int = 10
|
||||
real_name: t.Optional[str] = Field(alias='realName')
|
||||
username: t.Optional[str]
|
||||
dept_id: t.Optional[str] = Field(alias='deptId', default=0)
|
||||
phone: t.Optional[str]
|
||||
sort: t.Optional[int]
|
||||
status: t.Optional[int]
|
||||
|
||||
|
||||
class PersonSchema(BaseModel):
|
||||
role_ids: str = Field(alias='roleIds')
|
||||
username: str
|
||||
real_name: str = Field(alias='realName')
|
||||
password: str
|
||||
|
||||
|
||||
class UserApi(MethodView):
|
||||
"""修改用户数据"""
|
||||
|
||||
@validate()
|
||||
def get(self, _id, query: QuerySchema):
|
||||
|
||||
filters = []
|
||||
|
||||
if query.real_name:
|
||||
filters.append(UserModel.realname.like('%' + query.real_name + '%'))
|
||||
if query.username:
|
||||
filters.append(UserModel.username.like('%' + query.username + '%'))
|
||||
if query.dept_id:
|
||||
filters.append(UserModel.dept_id == query.dept_id)
|
||||
|
||||
paginate = UserModel.query.filter(
|
||||
*filters).paginate(
|
||||
page=query.page, per_page=query.limit, error_out=False)
|
||||
|
||||
dept_name = lambda _id: DepartmentModel.query.filter_by(id=_id).first().dept_name if _id else ""
|
||||
user_data = [{
|
||||
'id': item.id,
|
||||
'username': item.username,
|
||||
'realname': item.realname,
|
||||
'enable': item.enable,
|
||||
'create_at': str(item.create_at),
|
||||
'update_at': str(item.update_at),
|
||||
'dept': dept_name(item.dept_id),
|
||||
} for item in paginate.items]
|
||||
return table_api(
|
||||
result={
|
||||
'items': user_data,
|
||||
'total': paginate.total,
|
||||
}
|
||||
, code=0
|
||||
)
|
||||
|
||||
@validate()
|
||||
def post(self, body: PersonSchema):
|
||||
"""新建单个用户"""
|
||||
|
||||
role_ids = body.role_ids.split(',')
|
||||
|
||||
if is_user_exists(body.username):
|
||||
return fail_api(message="用户已经存在")
|
||||
|
||||
user = UserModel()
|
||||
user.username = body.username
|
||||
user.realname = body.real_name
|
||||
user.set_password(body.password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
""" 增加用户角色 """
|
||||
user = UserModel.query.filter_by(id=user.id).first()
|
||||
roles = RoleModel.query.filter(RoleModel.id.in_(role_ids)).all()
|
||||
for r in roles:
|
||||
user.role.append(r)
|
||||
db.session.commit()
|
||||
|
||||
return success_api(message="增加成功", code=0)
|
||||
|
||||
def delete(self, _id):
|
||||
# 删除用户
|
||||
res = delete_by_id(_id)
|
||||
if not res:
|
||||
return fail_api(message="删除失败")
|
||||
return success_api(message="删除成功")
|
||||
|
||||
|
||||
class PersonModel2(BaseModel):
|
||||
role_ids: str = Field(alias='roleIds')
|
||||
user_id: str = Field(alias='userId')
|
||||
username: str
|
||||
real_name: str = Field(alias='realName')
|
||||
dept_id: str = Field(alias='deptId')
|
||||
|
||||
|
||||
@validate()
|
||||
def user_role_resource(_id, body: PersonModel2):
|
||||
role_ids = body.role_ids.split(',')
|
||||
|
||||
# 更新用户数据
|
||||
UserModel.query.filter_by(id=_id).update({'username': body.username,
|
||||
'realname': body.real_name,
|
||||
'dept_id': body.dept_id})
|
||||
db.session.commit()
|
||||
|
||||
update_user_role(_id, role_ids)
|
||||
|
||||
return success_api(message="更新成功")
|
||||
|
||||
|
||||
def user_info(_id, action):
|
||||
if action == 'info':
|
||||
real_name = request.json.get('realName', '')
|
||||
username = request.json.get('username', '')
|
||||
remark = request.json.get('remark', '')
|
||||
details = request.json.get('details', '')
|
||||
|
||||
ret = UserModel.query.get(_id)
|
||||
ret.username = username
|
||||
ret.realname = real_name
|
||||
ret.remark = details
|
||||
db.session.commit()
|
||||
if not ret:
|
||||
return fail_api(message="出错啦")
|
||||
return success_api(message="更新成功")
|
||||
elif action == 'status':
|
||||
user_id = int(request.json.get('userId', 0)) # int
|
||||
operate = int(request.json.get('operate', 0)) # int
|
||||
if operate not in [0, 1]:
|
||||
return {'status': 'error', 'message': '请求有误'}
|
||||
|
||||
if operate == 1:
|
||||
user = UserModel.query.get(_id)
|
||||
user.enable = operate
|
||||
message = success_api(message="启动成功")
|
||||
else:
|
||||
user = UserModel.query.filter_by(id=user_id).update({"enable": operate})
|
||||
message = success_api(message="禁用成功")
|
||||
if user:
|
||||
db.session.commit()
|
||||
else:
|
||||
return fail_api(message="出错啦")
|
||||
return message
|
||||
elif action == 'avatar':
|
||||
url = request.json.get("avatar").get("src")
|
||||
ret = UserModel.query.get(_id)
|
||||
ret.avatar = url
|
||||
db.session.commit()
|
||||
if not ret:
|
||||
return fail_api(message="出错啦")
|
||||
return success_api(message="修改成功")
|
||||
elif action == 'password':
|
||||
oldPassword = request.json.get('oldPassword', '')
|
||||
newPassword = request.json.get('newPassword', '')
|
||||
confirmPassword = request.json.get('confirmPassword', '')
|
||||
if not all([oldPassword, newPassword, confirmPassword]):
|
||||
return {'status': 'error', 'message': '密码不能为空'}
|
||||
|
||||
if newPassword != confirmPassword:
|
||||
return fail_api(message='确认密码不一致')
|
||||
|
||||
""" 修改当前用户密码 """
|
||||
user = UserModel.query.get(_id)
|
||||
is_right = user.validate_password(oldPassword)
|
||||
if not is_right:
|
||||
return jsonify(success=False, message="旧密码错误")
|
||||
user.set_password(newPassword)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(success=True, message="更改成功")
|
||||
else:
|
||||
return jsonify(success=False, message="操作有误")
|
||||
@@ -0,0 +1,20 @@
|
||||
from flask import Flask
|
||||
|
||||
from .index import index_bp
|
||||
from .logs_view import logs_bp
|
||||
from .roles import role_bp
|
||||
from .system_view import system_bp
|
||||
from .document_view import document_bp
|
||||
|
||||
from . import department
|
||||
from . import file
|
||||
from . import rights
|
||||
from . import users
|
||||
|
||||
|
||||
def init_view(app: Flask):
|
||||
app.register_blueprint(index_bp)
|
||||
app.register_blueprint(logs_bp)
|
||||
app.register_blueprint(role_bp)
|
||||
app.register_blueprint(system_bp)
|
||||
app.register_blueprint(document_bp)
|
||||
@@ -0,0 +1,21 @@
|
||||
from flask import render_template, request
|
||||
|
||||
from models import DepartmentModel
|
||||
from common.utils.rights import permission_required, view_logging_required
|
||||
from applications.view import index_bp
|
||||
|
||||
|
||||
@index_bp.get('/dept/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:dept:add")
|
||||
def add():
|
||||
return render_template('view/system/department_add.html')
|
||||
|
||||
|
||||
@index_bp.get('/dept/edit')
|
||||
@view_logging_required
|
||||
@permission_required("admin:dept:edit")
|
||||
def edit():
|
||||
dept_id = request.args.get("deptId", type=int)
|
||||
dept = DepartmentModel.query.get(dept_id)
|
||||
return render_template('view/system/department_edit.html', dept=dept)
|
||||
@@ -0,0 +1,18 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
document_bp = Blueprint('document', __name__)
|
||||
|
||||
|
||||
@document_bp.route('/component/code/index.html')
|
||||
def component_code():
|
||||
return render_template('component/code/index.html')
|
||||
|
||||
|
||||
@document_bp.route('/view/<document>/<file>')
|
||||
def document_view(document, file):
|
||||
return render_template(f'view/{document}/{file}')
|
||||
|
||||
|
||||
@document_bp.get('/login.html')
|
||||
def login_html():
|
||||
return render_template('login.html')
|
||||
@@ -0,0 +1,18 @@
|
||||
from flask import render_template
|
||||
|
||||
from applications.view import index_bp
|
||||
from common.utils.rights import view_logging_required, permission_required
|
||||
|
||||
|
||||
@index_bp.get('/file')
|
||||
@view_logging_required
|
||||
@permission_required("admin:file:main")
|
||||
def file_index():
|
||||
return render_template('view/system/photo.html')
|
||||
|
||||
|
||||
@index_bp.get('/file/photo/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:file:main")
|
||||
def file_photo_add():
|
||||
return render_template('view/system/photo_add.html')
|
||||
@@ -0,0 +1,78 @@
|
||||
from flask import Blueprint, redirect, url_for
|
||||
from flask import send_file
|
||||
from flask import session, render_template
|
||||
from flask_login import current_user, logout_user, login_required
|
||||
|
||||
from common.gen_captcha import get_captcha_image
|
||||
|
||||
index_bp = Blueprint('index', __name__)
|
||||
|
||||
|
||||
@index_bp.route('/')
|
||||
@index_bp.route('/admin')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@index_bp.route('/config/pear.config.json')
|
||||
def pear_config():
|
||||
return send_file('static/config/pear.config.json')
|
||||
|
||||
|
||||
@index_bp.route('/admin/data/menu.json')
|
||||
def menu():
|
||||
return send_file('static/admin/data/menu.json')
|
||||
|
||||
|
||||
@index_bp.route('/admin/data/message.json')
|
||||
def message():
|
||||
return send_file('static/admin/data/message.json')
|
||||
|
||||
|
||||
@index_bp.route('/view/console/console1.html')
|
||||
def console1():
|
||||
# 控制后台
|
||||
return render_template('view/console/console1.html')
|
||||
|
||||
|
||||
@index_bp.route('/view/console/console2.html')
|
||||
def console2():
|
||||
# 数据分析
|
||||
return render_template('view/console/console2.html')
|
||||
|
||||
|
||||
@index_bp.route('/view/system/theme.html')
|
||||
def theme():
|
||||
# 酸爽翻倍
|
||||
return render_template('view/system/theme.html')
|
||||
|
||||
|
||||
@index_bp.route('/view/document/core.html')
|
||||
def core():
|
||||
# 酸爽翻倍
|
||||
return render_template('view/document/core.html')
|
||||
|
||||
|
||||
@index_bp.get('/passport/getCaptcha')
|
||||
def get_captcha():
|
||||
resp, code = get_captcha_image()
|
||||
session["code"] = code
|
||||
return resp
|
||||
|
||||
|
||||
# 退出登录
|
||||
@index_bp.post('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
session.pop('permissions')
|
||||
print({"message": "注销成功", "success": True})
|
||||
return {"message": "注销成功", "success": True}
|
||||
|
||||
|
||||
@index_bp.get('/login')
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('admin.index'))
|
||||
return render_template('login.html')
|
||||
@@ -0,0 +1,63 @@
|
||||
from flask import Blueprint, request, render_template
|
||||
from sqlalchemy import desc
|
||||
|
||||
from common.utils.http import table_api
|
||||
from common.utils.rights import permission_required
|
||||
from models import LogModel
|
||||
|
||||
logs_bp = Blueprint('logs', __name__, url_prefix='/logs')
|
||||
|
||||
|
||||
@logs_bp.get('/login_log')
|
||||
@permission_required("admin:log:main")
|
||||
def login_log():
|
||||
page = request.args.get('page', type=int)
|
||||
limit = request.args.get('limit', type=int)
|
||||
log_paginate = LogModel.query.filter_by(
|
||||
url='/api/v1/passport/login').order_by(
|
||||
desc(LogModel.create_at)).paginate(
|
||||
page=page, per_page=limit, error_out=False)
|
||||
data = [
|
||||
{
|
||||
'id': item.id,
|
||||
'method': item.method,
|
||||
'uid': item.uid,
|
||||
'url': item.url,
|
||||
'desc': item.desc,
|
||||
'ip': item.ip,
|
||||
'success': item.success,
|
||||
'user_agent': item.user_agent,
|
||||
'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
} for item in log_paginate.items
|
||||
]
|
||||
|
||||
return table_api(result={'items': data,
|
||||
'total': log_paginate.total, },
|
||||
code=0)
|
||||
|
||||
|
||||
@logs_bp.get('/access_log')
|
||||
@permission_required("admin:log:main")
|
||||
def operate_log():
|
||||
page = request.args.get('page', type=int)
|
||||
limit = request.args.get('limit', type=int)
|
||||
log_paginate = LogModel.query.filter(
|
||||
LogModel.url != '/api/v1/passport/login').order_by(
|
||||
desc(LogModel.create_at)).paginate(
|
||||
page=page, per_page=limit, error_out=False)
|
||||
data = [
|
||||
{
|
||||
'id': item.id,
|
||||
'method': item.method,
|
||||
'uid': item.uid,
|
||||
'url': item.url,
|
||||
'desc': item.desc,
|
||||
'ip': item.ip,
|
||||
'success': item.success,
|
||||
'user_agent': item.user_agent,
|
||||
'create_at': item.create_at.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
} for item in log_paginate.items
|
||||
]
|
||||
return table_api(result={'items': data,
|
||||
'total': log_paginate.total, },
|
||||
code=0)
|
||||
@@ -0,0 +1,25 @@
|
||||
from flask import render_template
|
||||
|
||||
from common.utils.rights import permission_required, view_logging_required
|
||||
from models import RightModel
|
||||
from applications.view import index_bp
|
||||
|
||||
|
||||
@index_bp.get('/rights/power/<int:power_id>')
|
||||
@view_logging_required
|
||||
@permission_required("admin:power:edit")
|
||||
def rights_edit(power_id):
|
||||
power = RightModel.query.filter_by(id=power_id).first()
|
||||
icon = str(power.icon).split()
|
||||
if len(icon) == 2:
|
||||
icon = icon[1]
|
||||
else:
|
||||
icon = None
|
||||
return render_template('view/system/power_edit.html', power=power, icon=icon)
|
||||
|
||||
|
||||
@index_bp.get('/rights/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:power:main")
|
||||
def rights_add():
|
||||
return render_template('view/system/power_add.html')
|
||||
@@ -0,0 +1,31 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
from common.utils.rights import permission_required, view_logging_required
|
||||
|
||||
from models import RoleModel
|
||||
|
||||
role_bp = Blueprint('role', __name__, url_prefix='/admin/role')
|
||||
|
||||
|
||||
# 角色授权操作
|
||||
@role_bp.get('/power/<int:role_id>')
|
||||
@view_logging_required
|
||||
@permission_required("admin:role:power")
|
||||
def power(role_id):
|
||||
return render_template('view/system/roles_power.html', role_id=role_id)
|
||||
|
||||
|
||||
# 角色编辑
|
||||
@role_bp.get('/edit/<int:role_id>')
|
||||
@view_logging_required
|
||||
@permission_required("admin:role:edit")
|
||||
def role_editor(role_id):
|
||||
role = RoleModel.query.filter_by(id=role_id).first()
|
||||
return render_template('view/system/roles_edit.html', role=role)
|
||||
|
||||
|
||||
@role_bp.get('/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:role:edit")
|
||||
def role_add():
|
||||
return render_template('view/system/roles_add.html')
|
||||
@@ -0,0 +1,48 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
system_bp = Blueprint('system', __name__)
|
||||
|
||||
|
||||
@system_bp.route('/view/system/user.html')
|
||||
def system_user():
|
||||
return render_template('view/system/user.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/role.html')
|
||||
def system_role():
|
||||
return render_template('view/system/role.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/power.html')
|
||||
def system_power():
|
||||
return render_template('view/system/power.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/department.html')
|
||||
def system_department():
|
||||
return render_template('view/system/department.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/log.html')
|
||||
def system_log():
|
||||
return render_template('view/system/log.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/dict.html')
|
||||
def system_dict():
|
||||
return render_template('view/system/dict.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/operate/add.html')
|
||||
def system_operate_add():
|
||||
return render_template('view/system/operate/add.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/operate/edit.html')
|
||||
def system_operate_edit():
|
||||
return render_template('view/system/operate/edit.html')
|
||||
|
||||
|
||||
@system_bp.route('/view/system/operate/profile.html')
|
||||
def system_operate_profile():
|
||||
return render_template('view/system/operate/profile.html')
|
||||
@@ -0,0 +1,26 @@
|
||||
from flask import render_template
|
||||
|
||||
from common.utils.rights import permission_required, view_logging_required
|
||||
from models import RoleModel, UserModel
|
||||
from . import index_bp
|
||||
|
||||
|
||||
@index_bp.get('/users/add')
|
||||
@view_logging_required
|
||||
@permission_required("admin:user:add")
|
||||
def users_add_view():
|
||||
roles = RoleModel.query.all()
|
||||
return render_template('view/system/user_add.html', roles=roles)
|
||||
|
||||
|
||||
@index_bp.get('/users/<user_id>')
|
||||
@view_logging_required
|
||||
@permission_required("admin:user:edit")
|
||||
def users_user_id_view(user_id):
|
||||
# 获取编辑用户信息
|
||||
user = UserModel.query.filter_by(id=user_id).first()
|
||||
roles = RoleModel.query.all()
|
||||
checked_roles = []
|
||||
for r in user.role:
|
||||
checked_roles.append(r.id)
|
||||
return render_template('view/system/user_edit.html', user=user, roles=roles, checked_roles=checked_roles)
|
||||
@@ -0,0 +1,5 @@
|
||||
def register_api(view, endpoint, url, pk='id', pk_type='int', app=None):
|
||||
view_func = view.as_view(endpoint)
|
||||
app.add_url_rule(url, defaults={pk: None}, view_func=view_func, methods=['GET', ])
|
||||
app.add_url_rule(url, view_func=view_func, methods=['POST', ])
|
||||
app.add_url_rule(f'{url}<{pk_type}:{pk}>', view_func=view_func, methods=['GET', 'PUT', 'DELETE'])
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
from flask import current_app
|
||||
from cachelib.file import FileSystemCache
|
||||
from cachelib.memcached import MemcachedCache
|
||||
from cachelib.redis import RedisCache
|
||||
from cachelib.simple import SimpleCache
|
||||
from cachelib.uwsgi import UWSGICache
|
||||
|
||||
cacher: dict = {
|
||||
'filesystem': FileSystemCache,
|
||||
'memcached': MemcachedCache,
|
||||
'redis': RedisCache,
|
||||
'simple': SimpleCache,
|
||||
'uwsgi': UWSGICache
|
||||
}
|
||||
cache = RedisCache()
|
||||
|
||||
|
||||
def get_cache():
|
||||
return cache.get(current_app.config.get("CACHE_TYPE") or "filesystem")()
|
||||
@@ -1,18 +0,0 @@
|
||||
from celery import Celery
|
||||
|
||||
from entrance.config import celery as config
|
||||
from entrance.config.celery import broker_url
|
||||
|
||||
celery = Celery(__name__, broker=broker_url)
|
||||
|
||||
|
||||
def register_celery(app):
|
||||
celery.config_from_object(config)
|
||||
|
||||
class ContextTask(celery.Task):
|
||||
def __call__(self, *args, **kwargs):
|
||||
with app.app_context():
|
||||
return self.run(*args, **kwargs)
|
||||
|
||||
celery.Task = ContextTask
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
limiter = Limiter(
|
||||
key_func=get_remote_address,
|
||||
default_limits=["200 per day", "50 per hour"]
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
from logging.config import dictConfig
|
||||
|
||||
|
||||
def config_logger():
|
||||
dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"simple": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
"info_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "INFO",
|
||||
"formatter": "simple",
|
||||
"filename": "logs/info.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 50,
|
||||
"encoding": "utf8",
|
||||
},
|
||||
"error_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "ERROR",
|
||||
"formatter": "simple",
|
||||
"filename": "logs/errors.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 20,
|
||||
"encoding": "utf8",
|
||||
},
|
||||
"debug_file_handler": {
|
||||
"class": "logging.handlers.RotatingFileHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "simple",
|
||||
"filename": "logs/debug.log",
|
||||
"maxBytes": 10485760,
|
||||
"backupCount": 50,
|
||||
"encoding": "utf8",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"my_module": {"level": "ERROR", "handlers": ["console"], "propagate": "no"}
|
||||
},
|
||||
"root": {
|
||||
"level": "DEBUG",
|
||||
"handlers": ["error_file_handler", "debug_file_handler"],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
from flask_mail import Mail
|
||||
|
||||
mail = Mail()
|
||||
@@ -1,7 +0,0 @@
|
||||
from flask_migrate import Migrate
|
||||
|
||||
from common.extend.orm import db
|
||||
from entrance.models import model_lists
|
||||
|
||||
migrate = Migrate(db=db, directory='entrance/migrations')
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import datetime
|
||||
from typing import Tuple
|
||||
|
||||
from flask import request
|
||||
from flask_sqlalchemy import SQLAlchemy, BaseQuery
|
||||
|
||||
|
||||
class Query(BaseQuery):
|
||||
|
||||
def all_json(self) -> list:
|
||||
return [dict(i) for i in self.all()]
|
||||
|
||||
def soft_delete(self):
|
||||
return self.update({"delete_at": datetime.datetime.now()})
|
||||
|
||||
def logic_all(self):
|
||||
return self.filter_by(delete_at=None).all()
|
||||
|
||||
def layui_paginate(self):
|
||||
return self.paginate(page=request.args.get('page', type=int),
|
||||
per_page=request.args.get('limit', type=int),
|
||||
error_out=False)
|
||||
|
||||
def layui_paginate_json(self) -> Tuple[list, int]:
|
||||
p = self.paginate(page=request.args.get('page', type=int),
|
||||
per_page=request.args.get('limit', type=int),
|
||||
error_out=False)
|
||||
|
||||
return [dict(i) for i in p.items], p.total
|
||||
|
||||
|
||||
db = SQLAlchemy(query_class=Query)
|
||||
@@ -0,0 +1,504 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
为了解决 flask_uploads 与 Werkzeug 2.0.1 冲突而直接复制源码进行适配
|
||||
原始地址: https://github.com/maxcountryman/flask-uploads/blob/master/flask_uploads.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
string_types = str,
|
||||
else:
|
||||
string_types = basestring,
|
||||
|
||||
import os.path
|
||||
import posixpath
|
||||
|
||||
from flask import current_app, send_from_directory, abort, url_for
|
||||
from itertools import chain
|
||||
from werkzeug.datastructures import FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
# Extension presets
|
||||
|
||||
#: This just contains plain text files (.txt).
|
||||
TEXT = ('txt',)
|
||||
|
||||
#: This contains various office document formats (.rtf, .odf, .ods, .gnumeric,
|
||||
#: .abw, .doc, .docx, .xls, and .xlsx). Note that the macro-enabled versions
|
||||
#: of Microsoft Office 2007 files are not included.
|
||||
DOCUMENTS = tuple('rtf odf ods gnumeric abw doc docx xls xlsx'.split())
|
||||
|
||||
#: This contains basic image types that are viewable from most browsers (.jpg,
|
||||
#: .jpe, .jpeg, .png, .gif, .svg, and .bmp).
|
||||
IMAGES = tuple('jpg jpe jpeg png gif svg bmp'.split())
|
||||
|
||||
#: This contains audio file types (.wav, .mp3, .aac, .ogg, .oga, and .flac).
|
||||
AUDIO = tuple('wav mp3 aac ogg oga flac'.split())
|
||||
|
||||
#: This is for structured data files (.csv, .ini, .json, .plist, .xml, .yaml,
|
||||
#: and .yml).
|
||||
DATA = tuple('csv ini json plist xml yaml yml'.split())
|
||||
|
||||
#: This contains various types of scripts (.js, .php, .pl, .py .rb, and .sh).
|
||||
#: If your Web server has PHP installed and set to auto-run, you might want to
|
||||
#: add ``php`` to the DENY setting.
|
||||
SCRIPTS = tuple('js php pl py rb sh'.split())
|
||||
|
||||
#: This contains archive and compression formats (.gz, .bz2, .zip, .tar,
|
||||
#: .tgz, .txz, and .7z).
|
||||
ARCHIVES = tuple('gz bz2 zip tar tgz txz 7z'.split())
|
||||
|
||||
#: This contains shared libraries and executable files (.so, .exe and .dll).
|
||||
#: Most of the time, you will not want to allow this - it's better suited for
|
||||
#: use with `AllExcept`.
|
||||
EXECUTABLES = tuple('so exe dll'.split())
|
||||
|
||||
#: The default allowed extensions - `TEXT`, `DOCUMENTS`, `DATA`, and `IMAGES`.
|
||||
DEFAULTS = TEXT + DOCUMENTS + IMAGES + DATA
|
||||
|
||||
|
||||
class UploadNotAllowed(Exception):
|
||||
"""
|
||||
This exception is raised if the upload was not allowed. You should catch
|
||||
it in your view code and display an appropriate message to the user.
|
||||
"""
|
||||
|
||||
|
||||
def tuple_from(*iters):
|
||||
return tuple(itertools.chain(*iters))
|
||||
|
||||
|
||||
def extension(filename):
|
||||
ext = os.path.splitext(filename)[1]
|
||||
if ext.startswith('.'):
|
||||
# os.path.splitext retains . separator
|
||||
ext = ext[1:]
|
||||
return ext
|
||||
|
||||
|
||||
def lowercase_ext(filename):
|
||||
"""
|
||||
This is a helper used by UploadSet.save to provide lowercase extensions for
|
||||
all processed files, to compare with configured extensions in the same
|
||||
case.
|
||||
|
||||
.. versionchanged:: 0.1.4
|
||||
Filenames without extensions are no longer lowercased, only the
|
||||
extension is returned in lowercase, if an extension exists.
|
||||
|
||||
:param filename: The filename to ensure has a lowercase extension.
|
||||
"""
|
||||
if '.' in filename:
|
||||
main, ext = os.path.splitext(filename)
|
||||
return main + ext.lower()
|
||||
# For consistency with os.path.splitext,
|
||||
# do not treat a filename without an extension as an extension.
|
||||
# That is, do not return filename.lower().
|
||||
return filename
|
||||
|
||||
|
||||
def addslash(url):
|
||||
if url.endswith('/'):
|
||||
return url
|
||||
return url + '/'
|
||||
|
||||
|
||||
def patch_request_class(app, size=64 * 1024 * 1024):
|
||||
"""
|
||||
By default, Flask will accept uploads to an arbitrary size. While Werkzeug
|
||||
switches uploads from memory to a temporary file when they hit 500 KiB,
|
||||
it's still possible for someone to overload your disk space with a
|
||||
gigantic file.
|
||||
|
||||
This patches the app's request class's
|
||||
`~werkzeug.BaseRequest.max_content_length` attribute so that any upload
|
||||
larger than the given size is rejected with an HTTP error.
|
||||
|
||||
.. note::
|
||||
|
||||
In Flask 0.6, you can do this by setting the `MAX_CONTENT_LENGTH`
|
||||
setting, without patching the request class. To emulate this behavior,
|
||||
you can pass `None` as the size (you must pass it explicitly). That is
|
||||
the best way to call this function, as it won't break the Flask 0.6
|
||||
functionality if it exists.
|
||||
|
||||
.. versionchanged:: 0.1.1
|
||||
|
||||
:param app: The app to patch the request class of.
|
||||
:param size: The maximum size to accept, in bytes. The default is 64 MiB.
|
||||
If it is `None`, the app's `MAX_CONTENT_LENGTH` configuration
|
||||
setting will be used to patch.
|
||||
"""
|
||||
if size is None:
|
||||
if isinstance(app.request_class.__dict__['max_content_length'],
|
||||
property):
|
||||
return
|
||||
size = app.config.get('MAX_CONTENT_LENGTH')
|
||||
reqclass = app.request_class
|
||||
patched = type(reqclass.__name__, (reqclass,),
|
||||
{'max_content_length': size})
|
||||
app.request_class = patched
|
||||
|
||||
|
||||
def config_for_set(uset, app, defaults=None):
|
||||
"""
|
||||
This is a helper function for `configure_uploads` that extracts the
|
||||
configuration for a single set.
|
||||
|
||||
:param uset: The upload set.
|
||||
:param app: The app to load the configuration from.
|
||||
:param defaults: A dict with keys `url` and `dest` from the
|
||||
`UPLOADS_DEFAULT_DEST` and `DEFAULT_UPLOADS_URL`
|
||||
settings.
|
||||
"""
|
||||
config = app.config
|
||||
prefix = 'UPLOADED_%s_' % uset.name.upper()
|
||||
using_defaults = False
|
||||
if defaults is None:
|
||||
defaults = dict(dest=None, url=None)
|
||||
|
||||
allow_extns = tuple(config.get(prefix + 'ALLOW', ()))
|
||||
deny_extns = tuple(config.get(prefix + 'DENY', ()))
|
||||
destination = config.get(prefix + 'DEST')
|
||||
base_url = config.get(prefix + 'URL')
|
||||
|
||||
if destination is None:
|
||||
# the upload set's destination wasn't given
|
||||
if uset.default_dest:
|
||||
# use the "default_dest" callable
|
||||
destination = uset.default_dest(app)
|
||||
if destination is None: # still
|
||||
# use the default dest from the config
|
||||
if defaults['dest'] is not None:
|
||||
using_defaults = True
|
||||
destination = os.path.join(defaults['dest'], uset.name)
|
||||
else:
|
||||
raise RuntimeError("no destination for set %s" % uset.name)
|
||||
|
||||
if base_url is None and using_defaults and defaults['url']:
|
||||
base_url = addslash(defaults['url']) + uset.name + '/'
|
||||
|
||||
return UploadConfiguration(destination, base_url, allow_extns, deny_extns)
|
||||
|
||||
|
||||
def configure_uploads(app, upload_sets):
|
||||
"""
|
||||
Call this after the app has been configured. It will go through all the
|
||||
upload sets, get their configuration, and store the configuration on the
|
||||
app. It will also register the uploads module if it hasn't been set. This
|
||||
can be called multiple times with different upload sets.
|
||||
|
||||
.. versionchanged:: 0.1.3
|
||||
The uploads module/blueprint will only be registered if it is needed
|
||||
to serve the upload sets.
|
||||
|
||||
:param app: The `~flask.Flask` instance to get the configuration from.
|
||||
:param upload_sets: The `UploadSet` instances to configure.
|
||||
"""
|
||||
if isinstance(upload_sets, UploadSet):
|
||||
upload_sets = (upload_sets,)
|
||||
|
||||
if not hasattr(app, 'upload_set_config'):
|
||||
app.upload_set_config = {}
|
||||
set_config = app.upload_set_config
|
||||
defaults = dict(dest=app.config.get('UPLOADS_DEFAULT_DEST'),
|
||||
url=app.config.get('UPLOADS_DEFAULT_URL'))
|
||||
|
||||
for uset in upload_sets:
|
||||
config = config_for_set(uset, app, defaults)
|
||||
set_config[uset.name] = config
|
||||
|
||||
should_serve = any(s.base_url is None for s in set_config.values())
|
||||
if '_uploads' not in app.blueprints and should_serve:
|
||||
app.register_blueprint(uploads_mod)
|
||||
|
||||
|
||||
class All(object):
|
||||
"""
|
||||
This type can be used to allow all extensions. There is a predefined
|
||||
instance named `ALL`.
|
||||
"""
|
||||
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
|
||||
#: This "contains" all items. You can use it to allow all extensions to be
|
||||
#: uploaded.
|
||||
ALL = All()
|
||||
|
||||
|
||||
class AllExcept(object):
|
||||
"""
|
||||
This can be used to allow all file types except certain ones. For example,
|
||||
to ban .exe and .iso files, pass::
|
||||
|
||||
AllExcept(('exe', 'iso'))
|
||||
|
||||
to the `UploadSet` constructor as `extensions`. You can use any container,
|
||||
for example::
|
||||
|
||||
AllExcept(SCRIPTS + EXECUTABLES)
|
||||
"""
|
||||
|
||||
def __init__(self, items):
|
||||
self.items = items
|
||||
|
||||
def __contains__(self, item):
|
||||
return item not in self.items
|
||||
|
||||
|
||||
class UploadConfiguration(object):
|
||||
"""
|
||||
This holds the configuration for a single `UploadSet`. The constructor's
|
||||
arguments are also the attributes.
|
||||
|
||||
:param destination: The directory to save files to.
|
||||
:param base_url: The URL (ending with a /) that files can be downloaded
|
||||
from. If this is `None`, Flask-Uploads will serve the
|
||||
files itself.
|
||||
:param allow: A list of extensions to allow, even if they're not in the
|
||||
`UploadSet` extensions list.
|
||||
:param deny: A list of extensions to deny, even if they are in the
|
||||
`UploadSet` extensions list.
|
||||
"""
|
||||
|
||||
def __init__(self, destination, base_url=None, allow=(), deny=()):
|
||||
self.destination = destination
|
||||
self.base_url = base_url
|
||||
self.allow = allow
|
||||
self.deny = deny
|
||||
|
||||
@property
|
||||
def tuple(self):
|
||||
return (self.destination, self.base_url, self.allow, self.deny)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tuple == other.tuple
|
||||
|
||||
|
||||
class UploadSet(object):
|
||||
"""
|
||||
This represents a single set of uploaded files. Each upload set is
|
||||
independent of the others. This can be reused across multiple application
|
||||
instances, as all configuration is stored on the application object itself
|
||||
and found with `flask.current_app`.
|
||||
|
||||
:param name: The name of this upload set. It defaults to ``files``, but
|
||||
you can pick any alphanumeric name you want. (For simplicity,
|
||||
it's best to use a plural noun.)
|
||||
:param extensions: The extensions to allow uploading in this set. The
|
||||
easiest way to do this is to add together the extension
|
||||
presets (for example, ``TEXT + DOCUMENTS + IMAGES``).
|
||||
It can be overridden by the configuration with the
|
||||
`UPLOADED_X_ALLOW` and `UPLOADED_X_DENY` configuration
|
||||
parameters. The default is `DEFAULTS`.
|
||||
:param default_dest: If given, this should be a callable. If you call it
|
||||
with the app, it should return the default upload
|
||||
destination path for that app.
|
||||
"""
|
||||
|
||||
def __init__(self, name='files', extensions=DEFAULTS, default_dest=None):
|
||||
if not name.isalnum():
|
||||
raise ValueError("Name must be alphanumeric (no underscores)")
|
||||
self.name = name
|
||||
self.extensions = extensions
|
||||
self._config = None
|
||||
self.default_dest = default_dest
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""
|
||||
This gets the current configuration. By default, it looks up the
|
||||
current application and gets the configuration from there. But if you
|
||||
don't want to go to the full effort of setting an application, or it's
|
||||
otherwise outside of a request context, set the `_config` attribute to
|
||||
an `UploadConfiguration` instance, then set it back to `None` when
|
||||
you're done.
|
||||
"""
|
||||
if self._config is not None:
|
||||
return self._config
|
||||
try:
|
||||
return current_app.upload_set_config[self.name]
|
||||
except AttributeError:
|
||||
raise RuntimeError("cannot access configuration outside request")
|
||||
|
||||
def url(self, filename):
|
||||
"""
|
||||
This function gets the URL a file uploaded to this set would be
|
||||
accessed at. It doesn't check whether said file exists.
|
||||
|
||||
:param filename: The filename to return the URL for.
|
||||
"""
|
||||
base = self.config.base_url
|
||||
if base is None:
|
||||
return url_for('_uploads.uploaded_file', setname=self.name,
|
||||
filename=filename, _external=True)
|
||||
else:
|
||||
return base + filename
|
||||
|
||||
def path(self, filename, folder=None):
|
||||
"""
|
||||
This returns the absolute path of a file uploaded to this set. It
|
||||
doesn't actually check whether said file exists.
|
||||
|
||||
:param filename: The filename to return the path for.
|
||||
:param folder: The subfolder within the upload set previously used
|
||||
to save to.
|
||||
"""
|
||||
if folder is not None:
|
||||
target_folder = os.path.join(self.config.destination, folder)
|
||||
else:
|
||||
target_folder = self.config.destination
|
||||
return os.path.join(target_folder, filename)
|
||||
|
||||
def file_allowed(self, storage, basename):
|
||||
"""
|
||||
This tells whether a file is allowed. It should return `True` if the
|
||||
given `werkzeug.FileStorage` object can be saved with the given
|
||||
basename, and `False` if it can't. The default implementation just
|
||||
checks the extension, so you can override this if you want.
|
||||
|
||||
:param storage: The `werkzeug.FileStorage` to check.
|
||||
:param basename: The basename it will be saved under.
|
||||
"""
|
||||
return self.extension_allowed(extension(basename))
|
||||
|
||||
def extension_allowed(self, ext):
|
||||
"""
|
||||
This determines whether a specific extension is allowed. It is called
|
||||
by `file_allowed`, so if you override that but still want to check
|
||||
extensions, call back into this.
|
||||
|
||||
:param ext: The extension to check, without the dot.
|
||||
"""
|
||||
return ((ext in self.config.allow) or
|
||||
(ext in self.extensions and ext not in self.config.deny))
|
||||
|
||||
def get_basename(self, filename):
|
||||
return lowercase_ext(secure_filename(filename))
|
||||
|
||||
def save(self, storage, folder=None, name=None):
|
||||
"""
|
||||
This saves a `werkzeug.FileStorage` into this upload set. If the
|
||||
upload is not allowed, an `UploadNotAllowed` error will be raised.
|
||||
Otherwise, the file will be saved and its name (including the folder)
|
||||
will be returned.
|
||||
|
||||
:param storage: The uploaded file to save.
|
||||
:param folder: The subfolder within the upload set to save to.
|
||||
:param name: The name to save the file as. If it ends with a dot, the
|
||||
file's extension will be appended to the end. (If you
|
||||
are using `name`, you can include the folder in the
|
||||
`name` instead of explicitly using `folder`, i.e.
|
||||
``uset.save(file, name="someguy/photo_123.")``
|
||||
"""
|
||||
if not isinstance(storage, FileStorage):
|
||||
raise TypeError("storage must be a werkzeug.FileStorage")
|
||||
|
||||
if folder is None and name is not None and "/" in name:
|
||||
folder, name = os.path.split(name)
|
||||
|
||||
basename = self.get_basename(storage.filename)
|
||||
if name:
|
||||
if name.endswith('.'):
|
||||
basename = name + extension(basename)
|
||||
else:
|
||||
basename = name
|
||||
|
||||
if not self.file_allowed(storage, basename):
|
||||
raise UploadNotAllowed()
|
||||
|
||||
if folder:
|
||||
target_folder = os.path.join(self.config.destination, folder)
|
||||
else:
|
||||
target_folder = self.config.destination
|
||||
if not os.path.exists(target_folder):
|
||||
os.makedirs(target_folder)
|
||||
if os.path.exists(os.path.join(target_folder, basename)):
|
||||
basename = self.resolve_conflict(target_folder, basename)
|
||||
|
||||
target = os.path.join(target_folder, basename)
|
||||
storage.save(target)
|
||||
if folder:
|
||||
return posixpath.join(folder, basename)
|
||||
else:
|
||||
return basename
|
||||
|
||||
def resolve_conflict(self, target_folder, basename):
|
||||
"""
|
||||
If a file with the selected name already exists in the target folder,
|
||||
this method is called to resolve the conflict. It should return a new
|
||||
basename for the file.
|
||||
|
||||
The default implementation splits the name and extension and adds a
|
||||
suffix to the name consisting of an underscore and a number, and tries
|
||||
that until it finds one that doesn't exist.
|
||||
|
||||
:param target_folder: The absolute path to the target.
|
||||
:param basename: The file's original basename.
|
||||
"""
|
||||
name, ext = os.path.splitext(basename)
|
||||
count = 0
|
||||
while True:
|
||||
count = count + 1
|
||||
newname = '%s_%d%s' % (name, count, ext)
|
||||
if not os.path.exists(os.path.join(target_folder, newname)):
|
||||
return newname
|
||||
|
||||
|
||||
uploads_mod = Blueprint('_uploads', __name__, url_prefix='/_uploads')
|
||||
|
||||
|
||||
@uploads_mod.route('/<setname>/<path:filename>')
|
||||
def uploaded_file(setname, filename):
|
||||
config = current_app.upload_set_config.get(setname)
|
||||
if config is None:
|
||||
abort(404)
|
||||
return send_from_directory(config.destination, filename)
|
||||
|
||||
|
||||
class TestingFileStorage(FileStorage):
|
||||
"""
|
||||
This is a helper for testing upload behavior in your application. You
|
||||
can manually create it, and its save method is overloaded to set `saved`
|
||||
to the name of the file it was saved to. All of these parameters are
|
||||
optional, so only bother setting the ones relevant to your application.
|
||||
|
||||
:param stream: A stream. The default is an empty stream.
|
||||
:param filename: The filename uploaded from the client. The default is the
|
||||
stream's name.
|
||||
:param name: The name of the form field it was loaded from. The default is
|
||||
`None`.
|
||||
:param content_type: The content type it was uploaded as. The default is
|
||||
``application/octet-stream``.
|
||||
:param content_length: How long it is. The default is -1.
|
||||
:param headers: Multipart headers as a `werkzeug.Headers`. The default is
|
||||
`None`.
|
||||
"""
|
||||
|
||||
def __init__(self, stream=None, filename=None, name=None,
|
||||
content_type='application/octet-stream', content_length=-1,
|
||||
headers=None):
|
||||
FileStorage.__init__(self, stream, filename, name=name,
|
||||
content_type=content_type, content_length=content_length,
|
||||
headers=None)
|
||||
self.saved = None
|
||||
|
||||
def save(self, dst, buffer_size=16384):
|
||||
"""
|
||||
This marks the file as saved by setting the `saved` attribute to the
|
||||
name of the file it was saved to.
|
||||
|
||||
:param dst: The file to save to.
|
||||
:param buffer_size: Ignored.
|
||||
"""
|
||||
if isinstance(dst, string_types):
|
||||
self.saved = dst
|
||||
else:
|
||||
self.saved = dst.name
|
||||
@@ -0,0 +1,43 @@
|
||||
from captcha.image import ImageCaptcha
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
from random import choices
|
||||
|
||||
from flask import session, make_response
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
def gen_captcha(content='0123456789'):
|
||||
""" 生成验证码 """
|
||||
image = ImageCaptcha()
|
||||
# 获取字符串
|
||||
captcha_text = "".join(choices(content, k=4))
|
||||
# 生成图像
|
||||
captcha_image = Image.open(image.generate(captcha_text))
|
||||
return captcha_text, captcha_image
|
||||
|
||||
|
||||
# 生成验证码
|
||||
def get_captcha_image():
|
||||
code, image = gen_captcha()
|
||||
out = BytesIO()
|
||||
session["code"] = code
|
||||
image.save(out, 'png')
|
||||
out.seek(0)
|
||||
resp = make_response(out.read())
|
||||
resp.content_type = 'image/png'
|
||||
return resp, code
|
||||
|
||||
|
||||
# 授权路由存入session
|
||||
def add_auth_session():
|
||||
role = current_user.role
|
||||
user_power = []
|
||||
for i in role:
|
||||
if i.enable == 0:
|
||||
continue
|
||||
for p in i.power:
|
||||
if p.enable == 0:
|
||||
continue
|
||||
user_power.append(p.code)
|
||||
session['permissions'] = user_power
|
||||
@@ -1,89 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from entrance.extensions import db
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import backref
|
||||
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class BaseModel(db.Model):
|
||||
__abstract__ = True
|
||||
|
||||
# insert and update
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
|
||||
# delete
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class CommonModel(BaseModel):
|
||||
__abstract__ = True
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='用户ID')
|
||||
create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_time = db.Column(db.DateTime, onupdate=datetime.datetime.now, comment='更新时间')
|
||||
|
||||
|
||||
class CurdModel(BaseModel):
|
||||
__abstract__ = True
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='用户ID')
|
||||
create_time = db.Column(db.DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_time = db.Column(db.DateTime, onupdate=datetime.datetime.now, comment='更新时间')
|
||||
delete_at = db.Column(db.DateTime, comment='删除时间')
|
||||
|
||||
|
||||
Column = db.Column
|
||||
Table = db.Table
|
||||
relationship = relationship
|
||||
ForeignKey = ForeignKey
|
||||
backref = backref
|
||||
|
||||
TypeDecorator = db.TypeDecorator
|
||||
INT = db.INT
|
||||
CHAR = db.CHAR
|
||||
VARCHAR = db.VARCHAR
|
||||
NCHAR = db.NCHAR
|
||||
NVARCHAR = db.NVARCHAR
|
||||
TEXT = db.TEXT
|
||||
Text = db.Text
|
||||
FLOAT = db.FLOAT
|
||||
NUMERIC = db.NUMERIC
|
||||
REAL = db.REAL
|
||||
DECIMAL = db.DECIMAL
|
||||
TIMESTAMP = db.TIMESTAMP
|
||||
DATETIME = db.DATETIME
|
||||
CLOB = db.CLOB
|
||||
BLOB = db.BLOB
|
||||
BINARY = db.BINARY
|
||||
VARBINARY = db.VARBINARY
|
||||
BOOLEAN = db.BOOLEAN
|
||||
BIGINT = db.BIGINT
|
||||
SMALLINT = db.SMALLINT
|
||||
INTEGER = db.INTEGER
|
||||
DATE = db.DATE
|
||||
TIME = db.TIME
|
||||
TupleType = db.TupleType
|
||||
String = db.String
|
||||
Integer = db.Integer
|
||||
SmallInteger = db.SmallInteger
|
||||
BigInteger = db.BigInteger
|
||||
Numeric = db.Numeric
|
||||
Float = db.Float
|
||||
DateTime = db.DateTime
|
||||
Date = db.Date
|
||||
Time = db.Time
|
||||
LargeBinary = db.LargeBinary
|
||||
Boolean = db.Boolean
|
||||
Unicode = db.Unicode
|
||||
UnicodeText = db.UnicodeText
|
||||
PickleType = db.PickleType
|
||||
Interval = db.Interval
|
||||
Enum = db.Enum
|
||||
ARRAY = db.ARRAY
|
||||
JSON = db.JSON
|
||||
|
||||
SQLAlchemyAutoSchema = SQLAlchemyAutoSchema
|
||||
@@ -1,121 +0,0 @@
|
||||
import random, math
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
|
||||
|
||||
class vieCode:
|
||||
__fontSize = 20 # 字体大小
|
||||
__width = 120 # 画布宽度
|
||||
__heigth = 45 # 画布高度
|
||||
__length = 4 # 验证码长度
|
||||
__draw = None # 画布
|
||||
__img = None # 图片资源
|
||||
__code = None # 验证码字符
|
||||
__str = None # 自定义验证码字符集
|
||||
__inCurve = True # 是否画干扰线
|
||||
__inNoise = True # 是否画干扰点
|
||||
__type = 2 # 验证码类型 1、纯字母 2、数字字母混合
|
||||
__fontPatn = 'applications/common/utils/fonts/2.ttf' # 字体
|
||||
|
||||
def GetCodeImage(self, size=80, length=4):
|
||||
'''获取验证码图片
|
||||
@param int size 验证码大小
|
||||
@param int length 验证码长度
|
||||
'''
|
||||
# 准备基础数据
|
||||
self.__length = length
|
||||
self.__fontSize = size
|
||||
self.__width = self.__fontSize * self.__length
|
||||
self.__heigth = int(self.__fontSize * 1.5)
|
||||
|
||||
# 生成验证码图片
|
||||
self.__createCode()
|
||||
self.__createImage()
|
||||
self.__createNoise()
|
||||
self.__printString()
|
||||
self.__cerateFilter()
|
||||
|
||||
return self.__img, self.__code
|
||||
|
||||
def __cerateFilter(self):
|
||||
'''模糊处理'''
|
||||
self.__img = self.__img.filter(ImageFilter.BLUR)
|
||||
filter = ImageFilter.ModeFilter(8)
|
||||
self.__img = self.__img.filter(filter)
|
||||
|
||||
def __createCode(self):
|
||||
'''创建验证码字符'''
|
||||
# 是否自定义字符集合
|
||||
if not self.__str:
|
||||
# 源文本
|
||||
number = "3456789"
|
||||
srcLetter = "qwertyuipasdfghjkzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
|
||||
srcUpper = srcLetter.upper()
|
||||
if self.__type == 1:
|
||||
self.__str = number
|
||||
else:
|
||||
self.__str = srcLetter + srcUpper + number
|
||||
|
||||
# 构造验证码
|
||||
self.__code = random.sample(self.__str, self.__length)
|
||||
|
||||
def __createImage(self):
|
||||
'''创建画布'''
|
||||
bgColor = (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255))
|
||||
self.__img = Image.new('RGB', (self.__width, self.__heigth), bgColor)
|
||||
self.__draw = ImageDraw.Draw(self.__img)
|
||||
|
||||
def __createNoise(self):
|
||||
'''画干扰点'''
|
||||
if not self.__inNoise:
|
||||
return
|
||||
font = ImageFont.truetype(self.__fontPatn, int(self.__fontSize / 1.5))
|
||||
for i in range(5):
|
||||
# 杂点颜色
|
||||
noiseColor = (random.randint(150, 200), random.randint(150, 200), random.randint(150, 200))
|
||||
putStr = random.sample(self.__str, 2)
|
||||
for j in range(2):
|
||||
# 绘杂点
|
||||
size = (random.randint(-10, self.__width), random.randint(-10, self.__heigth))
|
||||
self.__draw.text(size, putStr[j], font=font, fill=noiseColor)
|
||||
pass
|
||||
|
||||
def __createCurve(self):
|
||||
'''画干扰线'''
|
||||
if not self.__inCurve:
|
||||
return
|
||||
x = y = 0
|
||||
|
||||
# 计算曲线系数
|
||||
a = random.uniform(1, self.__heigth / 2)
|
||||
b = random.uniform(-self.__width / 4, self.__heigth / 4)
|
||||
f = random.uniform(-self.__heigth / 4, self.__heigth / 4)
|
||||
t = random.uniform(self.__heigth, self.__width * 2)
|
||||
xend = random.randint(self.__width / 2, self.__width * 2)
|
||||
w = (2 * math.pi) / t
|
||||
|
||||
# 画曲线
|
||||
color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150))
|
||||
for x in range(xend):
|
||||
if w != 0:
|
||||
for k in range(int(self.__heigth / 10)):
|
||||
y = a * math.sin(w * x + f) + b + self.__heigth / 2
|
||||
i = int(self.__fontSize / 5)
|
||||
while i > 0:
|
||||
px = x + i
|
||||
py = y + i + k
|
||||
self.__draw.point((px, py), color)
|
||||
i -= i
|
||||
|
||||
def __printString(self):
|
||||
'''打印验证码字符串'''
|
||||
font = ImageFont.truetype(self.__fontPatn, self.__fontSize)
|
||||
x = 0
|
||||
# 打印字符到画板
|
||||
for i in range(self.__length):
|
||||
# 设置字体随机颜色
|
||||
color = (random.randint(30, 150), random.randint(30, 150), random.randint(30, 150))
|
||||
# 计算座标
|
||||
x = random.uniform(self.__fontSize * i * 0.95, self.__fontSize * i * 1.1)
|
||||
y = self.__fontSize * random.uniform(0.3, 0.5)
|
||||
# 打印字符
|
||||
self.__draw.text((x, y), self.__code[i], font=font, fill=color)
|
||||
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
import typing as t
|
||||
|
||||
|
||||
def success_api(message: str = "成功", code: int = 200) -> t.Dict:
|
||||
""" 成功响应 默认值”成功“ """
|
||||
return {'success': True, 'message': message, 'code': code}
|
||||
|
||||
|
||||
def fail_api(message: str = "失败", code: int = 404) -> t.Dict:
|
||||
""" 失败响应 默认值“失败” """
|
||||
return {'success': False, 'message': message, 'code': code}
|
||||
|
||||
|
||||
def table_api(success: bool = True,
|
||||
message: str = "",
|
||||
result: t.Union[dict, list] = None,
|
||||
code: int = 0) -> t.Dict:
|
||||
"""
|
||||
动态表格渲染响应
|
||||
此方法返回数据给前端
|
||||
{
|
||||
'success': True,
|
||||
'code': 10002,
|
||||
'message': '提示消息',
|
||||
'result':{'items':[],'total': 100}
|
||||
}
|
||||
|
||||
注:lay_ui 表格数据需要指定 code=0
|
||||
"""
|
||||
return {
|
||||
'success': success,
|
||||
'message': message,
|
||||
'code': code,
|
||||
'result': result,
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
from flask_mail import Message
|
||||
|
||||
# send_mail(subject='title', recipients=['123@qq.com'], content='body')
|
||||
from common.extend.mail import mail
|
||||
|
||||
|
||||
def send_mail(subject, recipients, content):
|
||||
try:
|
||||
message = Message(subject=subject, recipients=recipients, body=content)
|
||||
mail.send(message)
|
||||
except Exception as e:
|
||||
print('邮箱发送出错了')
|
||||
raise
|
||||
@@ -0,0 +1,61 @@
|
||||
import typing as t
|
||||
from functools import wraps
|
||||
|
||||
from flask import abort, request, jsonify, session
|
||||
from flask_login import login_required
|
||||
from flask_login import current_user
|
||||
|
||||
from extensions import db
|
||||
from models import LogModel
|
||||
|
||||
|
||||
def record_logging(success: bool = True) -> None:
|
||||
"""
|
||||
记录用户日志数据
|
||||
"""
|
||||
info = {
|
||||
'method': request.method,
|
||||
'url': request.path,
|
||||
'ip': request.remote_addr,
|
||||
'user_agent': request.headers.get('User-Agent'),
|
||||
'desc': str(dict(request.values)),
|
||||
'uid': current_user.id,
|
||||
'success': success
|
||||
}
|
||||
log = LogModel()
|
||||
for key, value in info.items():
|
||||
setattr(log, key, value)
|
||||
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def view_logging_required(func: t.Callable) -> t.Callable:
|
||||
"""
|
||||
日志装饰器,用于记录请求
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> t.Callable:
|
||||
record_logging()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def permission_required(permission: str) -> t.Callable:
|
||||
"""
|
||||
权限装饰器,用于过滤需要的权限
|
||||
"""
|
||||
|
||||
def decorator(func: t.Callable):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> t.Callable:
|
||||
if permission not in session.get('permissions'):
|
||||
record_logging(success=False)
|
||||
abort(403)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from common.flask_uploads import UploadSet, IMAGES
|
||||
from extensions import db
|
||||
from models import PhotoModel
|
||||
|
||||
photos = UploadSet('photos', IMAGES)
|
||||
|
||||
|
||||
def upload_one(photo, mime):
|
||||
filename = photos.save(photo)
|
||||
file_url = photos.url(filename)
|
||||
|
||||
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
|
||||
size = os.path.getsize(upload_url + '/' + filename)
|
||||
photo = PhotoModel(name=filename, href=file_url, mime=mime, size=size)
|
||||
db.session.add(photo)
|
||||
db.session.commit()
|
||||
return file_url
|
||||
|
||||
|
||||
def delete_photo_by_id(_id):
|
||||
photo_name = PhotoModel.query.filter_by(id=_id).first().name
|
||||
photo = PhotoModel.query.filter_by(id=_id).delete()
|
||||
db.session.commit()
|
||||
upload_url = current_app.config.get("UPLOADED_PHOTOS_DEST")
|
||||
os.remove(upload_url + '/' + photo_name)
|
||||
return photo
|
||||
@@ -1,14 +0,0 @@
|
||||
from file_system_uploader import FilesystemUploader
|
||||
from qiniu_uploader import QiniuUploader
|
||||
from upyun_uploader import UpyunUploader
|
||||
from flask import current_app
|
||||
|
||||
uploader: dict = {
|
||||
"filesystem": FilesystemUploader,
|
||||
"qiniu": QiniuUploader,
|
||||
'upyun': UpyunUploader
|
||||
}
|
||||
|
||||
|
||||
def get_uploader():
|
||||
return uploader.get(current_app.config.get("UPLOAD_PROVIDER") or "filesystem")()
|
||||
@@ -1,19 +0,0 @@
|
||||
class BaseUploader(object):
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def store(self, fileobj, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def upload(self, file_obj, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def download(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def sync(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import os
|
||||
import posixpath
|
||||
from pathlib import PurePath
|
||||
from shutil import copyfileobj, rmtree
|
||||
|
||||
from PIL.features import codecs
|
||||
from flask import current_app, send_file
|
||||
from werkzeug.utils import safe_join, secure_filename
|
||||
|
||||
from common.utils.uploads.base_uploader import BaseUploader
|
||||
|
||||
|
||||
def hexencode(s):
|
||||
if isinstance(s, (str,)):
|
||||
s = s.encode("utf-8")
|
||||
encoded = codecs.encode(s, "hex")
|
||||
try:
|
||||
encoded = encoded.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
return encoded
|
||||
|
||||
|
||||
class FilesystemUploader(BaseUploader):
|
||||
def __init__(self, base_path=None):
|
||||
super(BaseUploader, self).__init__()
|
||||
self.base_path = base_path or current_app.config.get("UPLOAD_FOLDER")
|
||||
|
||||
def store(self, fileobj, filename):
|
||||
location = os.path.join(self.base_path, filename)
|
||||
directory = os.path.dirname(location)
|
||||
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
with open(location, "wb") as dst:
|
||||
copyfileobj(fileobj, dst, 16384)
|
||||
|
||||
return filename
|
||||
|
||||
def upload(self, file_obj, filename):
|
||||
if len(filename) == 0:
|
||||
raise Exception("Empty filenames cannot be used")
|
||||
|
||||
filename = secure_filename(filename)
|
||||
md5hash = hexencode(os.urandom(16))
|
||||
file_path = posixpath.join(md5hash, filename)
|
||||
|
||||
return self.store(file_obj, file_path)
|
||||
|
||||
def download(self, filename):
|
||||
return send_file(safe_join(self.base_path, filename), as_attachment=True)
|
||||
|
||||
def delete(self, filename):
|
||||
if os.path.exists(os.path.join(self.base_path, filename)):
|
||||
file_path = PurePath(filename).parts[0]
|
||||
rmtree(os.path.join(self.base_path, file_path))
|
||||
return True
|
||||
return False
|
||||
|
||||
def sync(self):
|
||||
pass
|
||||
@@ -1,38 +0,0 @@
|
||||
from qiniu import Auth, put_data
|
||||
from base_uploader import BaseUploader
|
||||
|
||||
access_key = '自己账号的access key'
|
||||
|
||||
secret_key = '自己账号的secret key'
|
||||
|
||||
bucket_name = '空间名'
|
||||
|
||||
|
||||
class QiniuUploader(BaseUploader):
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def store(self, fileobj, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def upload(self, file_obj, filename):
|
||||
|
||||
token = Auth(access_key=access_key, secret_key=secret_key).upload_token(bucket=bucket_name)
|
||||
ret, res = put_data(token, None, data=file_obj)
|
||||
ret.get('key')
|
||||
print(ret)
|
||||
print(res)
|
||||
|
||||
if res.status_code != 200:
|
||||
raise Exception("upload failed")
|
||||
return ret, res
|
||||
|
||||
def download(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def sync(self):
|
||||
raise NotImplementedError
|
||||
@@ -1,22 +0,0 @@
|
||||
from base_uploader import BaseUploader
|
||||
import upyun
|
||||
|
||||
|
||||
class UpyunUploader(BaseUploader):
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def store(self, fileobj, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def upload(self, file_obj, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def download(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def delete(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def sync(self):
|
||||
raise NotImplementedError
|
||||
@@ -1,67 +0,0 @@
|
||||
from flask import Blueprint as FlaskBlueprint
|
||||
from flask.views import MethodViewType, MethodView
|
||||
|
||||
|
||||
class Blueprint(FlaskBlueprint):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._endpoints = []
|
||||
|
||||
def add_url_rule(
|
||||
self,
|
||||
rule,
|
||||
endpoint=None,
|
||||
view_func=None,
|
||||
provide_automatic_options=None,
|
||||
*,
|
||||
parameters=None,
|
||||
tags=None,
|
||||
**options,
|
||||
):
|
||||
|
||||
if view_func is None:
|
||||
raise TypeError("view_func must be provided")
|
||||
|
||||
if endpoint is None:
|
||||
endpoint = view_func.__name__
|
||||
|
||||
# Ensure endpoint name is unique
|
||||
# - to avoid a name clash when registering a MethodView
|
||||
# - to use it as a key internally in endpoint -> doc mapping
|
||||
if endpoint in self._endpoints:
|
||||
endpoint = f"{endpoint}_{len(self._endpoints)}"
|
||||
self._endpoints.append(endpoint)
|
||||
|
||||
if isinstance(view_func, MethodViewType):
|
||||
func = view_func.as_view(endpoint)
|
||||
else:
|
||||
func = view_func
|
||||
|
||||
# Add URL rule in Flask and store endpoint documentation
|
||||
super().add_url_rule(rule, endpoint, func, **options)
|
||||
|
||||
def route(self, rule, *, parameters=None, tags=None, **options):
|
||||
"""Decorator to register view function in application and documentation
|
||||
|
||||
Calls :meth:`add_url_rule <Blueprint.add_url_rule>`.
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
endpoint = options.pop("endpoint", None)
|
||||
self.add_url_rule(
|
||||
rule, endpoint, func, parameters=parameters, tags=tags, **options
|
||||
)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def register_child_bp(self, bp_lists):
|
||||
for bp in bp_lists:
|
||||
self.register_blueprint(bp)
|
||||
|
||||
|
||||
class View(MethodView):
|
||||
pass
|
||||
@@ -0,0 +1,50 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
# 主题面板的链接列表配置
|
||||
SYSTEM_NAME = "Pear Admin"
|
||||
SYSTEM_PANEL_LINKS = [
|
||||
{
|
||||
"icon": "layui-icon layui-icon-auz",
|
||||
"title": "官方网站",
|
||||
"href": "http://www.pearadmin.com"
|
||||
},
|
||||
{
|
||||
"icon": "layui-icon layui-icon-auz",
|
||||
"title": "开发文档",
|
||||
"href": "http://www.pearadmin.com"
|
||||
},
|
||||
{
|
||||
"icon": "layui-icon layui-icon-auz",
|
||||
"title": "开源地址",
|
||||
"href": "https://gitee.com/Jmysy/Pear-Admin-Layui"
|
||||
}
|
||||
]
|
||||
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'dev key')
|
||||
|
||||
# mysql 配置
|
||||
MYSQL_USERNAME = "windows"
|
||||
MYSQL_PASSWORD = "123456"
|
||||
MYSQL_HOST = "127.0.0.1"
|
||||
MYSQL_PORT = 3306
|
||||
MYSQL_DATABASE = "PearAdminFlask"
|
||||
|
||||
# redis 配置
|
||||
REDIS_HOST = "127.0.0.1"
|
||||
REDIS_PORT = 6379
|
||||
|
||||
""" Sqlalchemy 配置 """
|
||||
SQLALCHEMY_DATABASE_URI = r'sqlite:///pear_admin.db'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
SQLALCHEMY_ECHO = False
|
||||
SQLALCHEMY_POOL_RECYCLE = 8
|
||||
# SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}"
|
||||
|
||||
LOG_LEVEL = logging.ERROR
|
||||
|
||||
# 图片文件存放位置
|
||||
UPLOADED_PHOTOS_DEST = os.path.join(os.path.dirname(os.path.abspath(__name__)), 'static', 'upload')
|
||||
UPLOADED_FILES_ALLOW = ['gif', 'jpg']
|
||||
# JSON配置
|
||||
JSON_AS_ASCII = False
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -1,27 +0,0 @@
|
||||
from flask import Flask
|
||||
import os
|
||||
from common.extend.logger import config_logger
|
||||
from entrance.bps import sys, register_parent_bp
|
||||
from entrance.extensions import init_extensions
|
||||
|
||||
|
||||
def create_app():
|
||||
config_logger()
|
||||
app = Flask(__name__)
|
||||
app.config.from_pyfile('config/config.py')
|
||||
register_parent_bp(app)
|
||||
init_extensions(app)
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
print(
|
||||
"""
|
||||
|
||||
___ _ _ _ ___ _ _
|
||||
| _ \___ __ _ _ _ /_\ __| |_ __ (_)_ _ | __| |__ _ __| |__
|
||||
| _/ -_) _` | '_| / _ \/ _` | ' \| | ' \ | _|| / _` (_-< / /
|
||||
|_| \___\__,_|_| /_/ \_\__,_|_|_|_|_|_||_| |_| |_\__,_/__/_\_\\
|
||||
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
return app
|
||||
@@ -1,19 +0,0 @@
|
||||
from flask import Flask
|
||||
from typing import List
|
||||
from common.view import Blueprint
|
||||
from modules.sys.view.user import user
|
||||
|
||||
sys = Blueprint('sys', __name__, url_prefix='/sys')
|
||||
sys_lists: List[Blueprint] = [
|
||||
user
|
||||
]
|
||||
sys.register_child_bp(sys_lists)
|
||||
|
||||
parent_lists: List[Blueprint] = [
|
||||
sys
|
||||
]
|
||||
|
||||
|
||||
def register_parent_bp(app: Flask):
|
||||
for bp in parent_lists:
|
||||
app.register_blueprint(bp)
|
||||
@@ -1,8 +0,0 @@
|
||||
broker_url = 'redis://127.0.0.1:6379/1'
|
||||
result_backend = 'rpc://'
|
||||
|
||||
task_serializer = 'json'
|
||||
result_serializer = 'json'
|
||||
accept_content = ['json']
|
||||
timezone = 'Asia/Shanghai'
|
||||
enable_utc = True
|
||||
@@ -1,15 +0,0 @@
|
||||
import os
|
||||
|
||||
# mysql 配置
|
||||
MYSQL_USERNAME: str = os.getenv('MYSQL_USERNAME') or "root"
|
||||
MYSQL_PASSWORD: str = os.getenv('MYSQL_PASSWORD') or "123456"
|
||||
MYSQL_HOST: str = os.getenv('MYSQL_HOST') or "127.0.0.1"
|
||||
MYSQL_PORT: int = int(os.getenv('MYSQL_PORT') or 3306)
|
||||
MYSQL_DATABASE: str = os.getenv('MYSQL_DATABASE') or "PearAdminFlask"
|
||||
|
||||
# mysql 数据库的配置信息
|
||||
SQLALCHEMY_DATABASE_URI: str = f"mysql+pymysql://{MYSQL_USERNAME}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}"
|
||||
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
SQLALCHEMY_ECHO = False
|
||||
SQLALCHEMY_POOL_RECYCLE = 8
|
||||
@@ -1,13 +0,0 @@
|
||||
from flask import Flask
|
||||
|
||||
from common.extend.celeryer import register_celery
|
||||
from common.extend.limit import limiter
|
||||
from common.extend.orm import db
|
||||
from common.extend.migrate import migrate
|
||||
|
||||
|
||||
def init_extensions(app: Flask):
|
||||
db.init_app(app)
|
||||
migrate.init_app(app)
|
||||
register_celery(app)
|
||||
limiter.init_app(app)
|
||||
@@ -1 +0,0 @@
|
||||
Single-database configuration for Flask.
|
||||
@@ -1,50 +0,0 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -1,91 +0,0 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = current_app.extensions['migrate'].db.get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -1,24 +0,0 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -1,32 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 74133ba99af7
|
||||
Revises:
|
||||
Create Date: 2022-01-22 18:50:29.097473
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '74133ba99af7'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('admin_user',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='用户ID'),
|
||||
sa.Column('username', sa.String(length=20), nullable=True, comment='用户名'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('admin_user')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,168 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 8293d5eb9040
|
||||
Revises: d51040631f21
|
||||
Create Date: 2022-01-23 15:56:22.001914
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8293d5eb9040'
|
||||
down_revision = 'd51040631f21'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('sys_admin_log',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('method', sa.String(length=10), nullable=True),
|
||||
sa.Column('uid', sa.Integer(), nullable=True),
|
||||
sa.Column('url', sa.String(length=255), nullable=True),
|
||||
sa.Column('desc', sa.Text(), nullable=True),
|
||||
sa.Column('ip', sa.String(length=255), nullable=True),
|
||||
sa.Column('success', sa.Integer(), nullable=True),
|
||||
sa.Column('user_agent', sa.Text(), nullable=True),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_dept',
|
||||
sa.Column('id', sa.Integer(), nullable=False, comment='部门ID'),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True, comment='父级编号'),
|
||||
sa.Column('dept_name', sa.String(length=50), nullable=True, comment='部门名称'),
|
||||
sa.Column('sort', sa.Integer(), nullable=True, comment='排序'),
|
||||
sa.Column('leader', sa.String(length=50), nullable=True, comment='负责人'),
|
||||
sa.Column('phone', sa.String(length=20), nullable=True, comment='联系方式'),
|
||||
sa.Column('email', sa.String(length=50), nullable=True, comment='邮箱'),
|
||||
sa.Column('status', sa.Integer(), nullable=True, comment='状态(1开启,0关闭)'),
|
||||
sa.Column('remark', sa.Text(), nullable=True, comment='备注'),
|
||||
sa.Column('address', sa.String(length=255), nullable=True, comment='详细地址'),
|
||||
sa.Column('create_at', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.Column('update_at', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_dict_data',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('data_label', sa.String(length=255), nullable=True, comment='字典类型名称'),
|
||||
sa.Column('data_value', sa.String(length=255), nullable=True, comment='字典类型标识'),
|
||||
sa.Column('type_code', sa.String(length=255), nullable=True, comment='字典类型描述'),
|
||||
sa.Column('is_default', sa.Integer(), nullable=True, comment='是否默认'),
|
||||
sa.Column('enable', sa.Integer(), nullable=True, comment='是否开启'),
|
||||
sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.Column('update_time', sa.DateTime(), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_dict_type',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('type_name', sa.String(length=255), nullable=True, comment='字典类型名称'),
|
||||
sa.Column('type_code', sa.String(length=255), nullable=True, comment='字典类型标识'),
|
||||
sa.Column('description', sa.String(length=255), nullable=True, comment='字典类型描述'),
|
||||
sa.Column('enable', sa.Integer(), nullable=True, comment='是否开启'),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.Column('update_time', sa.DateTime(), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_photo',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=False),
|
||||
sa.Column('href', sa.String(length=255), nullable=True),
|
||||
sa.Column('mime', sa.CHAR(length=50), nullable=False),
|
||||
sa.Column('size', sa.CHAR(length=30), nullable=False),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_power',
|
||||
sa.Column('id', sa.Integer(), nullable=False, comment='权限编号'),
|
||||
sa.Column('name', sa.String(length=255), nullable=True, comment='权限名称'),
|
||||
sa.Column('type', sa.String(length=1), nullable=True, comment='权限类型'),
|
||||
sa.Column('code', sa.String(length=30), nullable=True, comment='权限标识'),
|
||||
sa.Column('url', sa.String(length=255), nullable=True, comment='权限路径'),
|
||||
sa.Column('open_type', sa.String(length=10), nullable=True, comment='打开方式'),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True, comment='父类编号'),
|
||||
sa.Column('icon', sa.String(length=128), nullable=True, comment='图标'),
|
||||
sa.Column('sort', sa.Integer(), nullable=True, comment='排序'),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.Column('update_time', sa.DateTime(), nullable=True, comment='更新时间'),
|
||||
sa.Column('enable', sa.Integer(), nullable=True, comment='是否开启'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_role',
|
||||
sa.Column('id', sa.Integer(), nullable=False, comment='角色ID'),
|
||||
sa.Column('name', sa.String(length=255), nullable=True, comment='角色名称'),
|
||||
sa.Column('code', sa.String(length=255), nullable=True, comment='角色标识'),
|
||||
sa.Column('enable', sa.Integer(), nullable=True, comment='是否启用'),
|
||||
sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'),
|
||||
sa.Column('details', sa.String(length=255), nullable=True, comment='详情'),
|
||||
sa.Column('sort', sa.Integer(), nullable=True, comment='排序'),
|
||||
sa.Column('create_time', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.Column('update_time', sa.DateTime(), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_user',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='用户ID'),
|
||||
sa.Column('username', sa.String(length=20), nullable=True, comment='用户名'),
|
||||
sa.Column('realname', sa.String(length=20), nullable=True, comment='真实名字'),
|
||||
sa.Column('avatar', sa.String(length=255), nullable=True, comment='头像'),
|
||||
sa.Column('remark', sa.String(length=255), nullable=True, comment='备注'),
|
||||
sa.Column('password_hash', sa.String(length=128), nullable=True, comment='哈希密码'),
|
||||
sa.Column('enable', sa.Integer(), nullable=True, comment='启用'),
|
||||
sa.Column('dept_id', sa.Integer(), nullable=True, comment='部门id'),
|
||||
sa.Column('create_at', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.Column('update_at', sa.DateTime(), nullable=True, comment='创建时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_role_power',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='标识'),
|
||||
sa.Column('power_id', sa.Integer(), nullable=True, comment='用户编号'),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True, comment='角色编号'),
|
||||
sa.ForeignKeyConstraint(['power_id'], ['sys_power.id'], ),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['sys_role.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sys_user_role',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='标识'),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True, comment='用户编号'),
|
||||
sa.Column('role_id', sa.Integer(), nullable=True, comment='角色编号'),
|
||||
sa.ForeignKeyConstraint(['role_id'], ['sys_role.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['sys_user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.drop_table('admin_test')
|
||||
op.drop_table('admin_user')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('admin_user',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False, comment='用户ID'),
|
||||
sa.Column('username', mysql.VARCHAR(collation='utf8mb4_general_ci', length=20), nullable=True, comment='用户名'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='MyISAM'
|
||||
)
|
||||
op.create_table('admin_test',
|
||||
sa.Column('id', mysql.INTEGER(display_width=11), autoincrement=True, nullable=False, comment='ID'),
|
||||
sa.Column('name', mysql.VARCHAR(collation='utf8mb4_general_ci', length=20), nullable=True, comment='名'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
mysql_collate='utf8mb4_general_ci',
|
||||
mysql_default_charset='utf8mb4',
|
||||
mysql_engine='MyISAM'
|
||||
)
|
||||
op.drop_table('sys_user_role')
|
||||
op.drop_table('sys_role_power')
|
||||
op.drop_table('sys_user')
|
||||
op.drop_table('sys_role')
|
||||
op.drop_table('sys_power')
|
||||
op.drop_table('sys_photo')
|
||||
op.drop_table('sys_dict_type')
|
||||
op.drop_table('sys_dict_data')
|
||||
op.drop_table('sys_dept')
|
||||
op.drop_table('sys_admin_log')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,32 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: d51040631f21
|
||||
Revises: 74133ba99af7
|
||||
Create Date: 2022-01-22 23:30:04.372043
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd51040631f21'
|
||||
down_revision = '74133ba99af7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('admin_test',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False, comment='ID'),
|
||||
sa.Column('name', sa.String(length=20), nullable=True, comment='名'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('admin_test')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
from modules.sys.models.user_role import user_role
|
||||
from modules.sys.models.role_power import role_power
|
||||
from modules.sys.models.dict import DictType, DictData
|
||||
from modules.sys.models.admin_log import AdminLog
|
||||
from modules.sys.models.photo import Photo
|
||||
from modules.sys.models.power import Power
|
||||
from modules.sys.models.role import Role
|
||||
from modules.sys.models.dept import Dept
|
||||
|
||||
from modules.sys.models.user import User
|
||||
|
||||
|
||||
model_lists: list = [
|
||||
role_power,
|
||||
user_role,
|
||||
Dept,
|
||||
DictType, DictData,
|
||||
AdminLog,
|
||||
Photo,
|
||||
Power,
|
||||
Role,
|
||||
User,
|
||||
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from flask import Flask
|
||||
|
||||
from .init_databases import register_script
|
||||
from .init_sqlalchemy import db, init_databases
|
||||
from .init_login import init_login_manager
|
||||
from .init_template_directives import init_template_directives
|
||||
from .init_error_views import init_error_views
|
||||
|
||||
|
||||
def init_plugs(app: Flask) -> None:
|
||||
init_login_manager(app)
|
||||
init_databases(app)
|
||||
init_template_directives(app)
|
||||
init_error_views(app)
|
||||
|
||||
# 生成测试数据的命令,生成环境可以注释
|
||||
register_script(app)
|
||||
@@ -0,0 +1,82 @@
|
||||
from flask import Flask
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
date_str = re.compile('\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$')
|
||||
|
||||
|
||||
def add_data(data_list, obj):
|
||||
from extensions import db
|
||||
|
||||
for _data in data_list:
|
||||
dept = obj()
|
||||
for key, value in _data.items():
|
||||
|
||||
if isinstance(value, str) and date_str.match(value):
|
||||
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
|
||||
setattr(dept, key, value)
|
||||
db.session.add(dept)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def register_script(app: Flask):
|
||||
@app.cli.command()
|
||||
def init_db():
|
||||
import json
|
||||
import os
|
||||
|
||||
path = os.path.dirname(os.path.abspath(__name__))
|
||||
path = os.path.join(path, 'static', 'data')
|
||||
cp_dept_data_list = json.loads(open(os.path.join(path, 'cp_dept.json'), encoding='utf-8').read())
|
||||
cp_user_data_list = json.loads(open(os.path.join(path, 'cp_user.json'), encoding='utf-8').read())
|
||||
file_photo_data_list = json.loads(open(os.path.join(path, 'file_photo.json'), encoding='utf-8').read())
|
||||
rt_power_data_list = json.loads(open(os.path.join(path, 'rt_power.json'), encoding='utf-8').read())
|
||||
rt_role_data_list = json.loads(open(os.path.join(path, 'rt_role.json'), encoding='utf-8').read())
|
||||
rt_role_power_data_list = json.loads(open(os.path.join(path, 'rt_role_power.json'), encoding='utf-8').read())
|
||||
rt_user_role_data_list = json.loads(open(os.path.join(path, 'rt_user_role.json'), encoding='utf-8').read())
|
||||
|
||||
"""数据库初始化"""
|
||||
|
||||
# 创建化部门数据
|
||||
from models import DepartmentModel
|
||||
add_data(cp_dept_data_list, DepartmentModel)
|
||||
|
||||
# 图片数据
|
||||
from models import PhotoModel
|
||||
|
||||
add_data(file_photo_data_list, PhotoModel)
|
||||
|
||||
# 初始化权限表数据
|
||||
from models import RightModel
|
||||
|
||||
add_data(rt_power_data_list, RightModel)
|
||||
# 初始化角色表
|
||||
from models import RoleModel
|
||||
|
||||
# 角色权限关系表
|
||||
from extensions import db
|
||||
for data in rt_role_power_data_list:
|
||||
db.session.execute('insert into rt_role_power VALUES (%s, %s, %s);' % tuple(data))
|
||||
db.session.commit()
|
||||
|
||||
add_data(rt_role_data_list, RoleModel)
|
||||
|
||||
# 管理员用户
|
||||
from models import UserModel
|
||||
|
||||
add_data(cp_user_data_list, UserModel)
|
||||
|
||||
# 用户角色表
|
||||
from extensions import db
|
||||
|
||||
for data in rt_user_role_data_list:
|
||||
db.session.execute('insert into rt_user_role VALUES (%s, %s, %s);' % tuple(data))
|
||||
db.session.commit()
|
||||
|
||||
@app.cli.command()
|
||||
def turn():
|
||||
"""清空数据库"""
|
||||
from extensions import db
|
||||
db.drop_all()
|
||||
db.create_all()
|
||||
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
root_path = os.path.abspath(os.path.dirname(__file__)).split('applications')[0]
|
||||
dot_env_path = os.path.join(root_path, '.env')
|
||||
flask_env_path = os.path.join(root_path, '.flaskenv')
|
||||
|
||||
if os.path.exists(dot_env_path):
|
||||
load_dotenv(dot_env_path)
|
||||
|
||||
if os.path.exists(flask_env_path):
|
||||
load_dotenv(flask_env_path)
|
||||
@@ -0,0 +1,15 @@
|
||||
from flask import render_template
|
||||
|
||||
|
||||
def init_error_views(app):
|
||||
@app.errorhandler(403)
|
||||
def page_not_found(e):
|
||||
return render_template('errors/403.html'), 403
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('errors/404.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_server_error(e):
|
||||
return render_template('errors/500.html'), 500
|
||||
@@ -0,0 +1,15 @@
|
||||
from flask_login import LoginManager
|
||||
|
||||
|
||||
def init_login_manager(app):
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
|
||||
login_manager.login_view = 'index.login'
|
||||
login_manager.login_message = u'请登录以访问此页面'
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from models import UserModel
|
||||
user = UserModel.query.get(int(user_id))
|
||||
return user
|
||||
@@ -0,0 +1,11 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
|
||||
|
||||
def init_databases(app: Flask):
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
@@ -0,0 +1,7 @@
|
||||
from flask import session
|
||||
|
||||
|
||||
def init_template_directives(app):
|
||||
@app.template_global()
|
||||
def authorize(power):
|
||||
return bool(power in session.get('permissions'))
|
||||
@@ -0,0 +1,4 @@
|
||||
from .file import PhotoModel
|
||||
from .rights import RightModel, RoleModel
|
||||
from .system import LogModel
|
||||
from .users import UserModel, DepartmentModel
|
||||
@@ -0,0 +1,15 @@
|
||||
from datetime import datetime
|
||||
|
||||
from extensions import db
|
||||
|
||||
|
||||
class PhotoModel(db.Model):
|
||||
__tablename__ = 'file_photo'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(255), nullable=False)
|
||||
href = db.Column(db.String(255))
|
||||
mime = db.Column(db.CHAR(50), nullable=False)
|
||||
size = db.Column(db.CHAR(30), nullable=False)
|
||||
|
||||
create_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
||||
update_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
|
||||
@@ -0,0 +1,62 @@
|
||||
from extensions import db
|
||||
|
||||
# 创建中间表
|
||||
user_role = db.Table(
|
||||
"rt_user_role", # 中间表名称
|
||||
db.Column("id", db.Integer, primary_key=True, autoincrement=True, comment='标识'), # 主键
|
||||
db.Column("user_id", db.Integer, db.ForeignKey("cp_user.id"), comment='用户编号'), # 属性 外键
|
||||
db.Column("role_id", db.Integer, db.ForeignKey("rt_role.id"), comment='角色编号'), # 属性 外键
|
||||
)
|
||||
|
||||
# 创建中间表
|
||||
role_power = db.Table(
|
||||
"rt_role_power", # 中间表名称
|
||||
db.Column("id", db.Integer, primary_key=True, autoincrement=True, comment='标识'), # 主键
|
||||
db.Column("power_id", db.Integer, db.ForeignKey("rt_power.id"), comment='用户编号'), # 属性 外键
|
||||
db.Column("role_id", db.Integer, db.ForeignKey("rt_role.id"), comment='角色编号'), # 属性 外键
|
||||
)
|
||||
|
||||
|
||||
class RightModel(db.Model):
|
||||
__tablename__ = 'rt_power'
|
||||
id = db.Column(db.Integer, primary_key=True, comment='权限编号')
|
||||
name = db.Column(db.String(255), comment='权限名称')
|
||||
type = db.Column(db.SMALLINT, comment='权限类型')
|
||||
code = db.Column(db.String(30), comment='权限标识')
|
||||
url = db.Column(db.String(255), comment='权限路径')
|
||||
open_type = db.Column(db.String(10), comment='打开方式')
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey("rt_power.id"), comment='父类编号')
|
||||
icon = db.Column(db.String(128), comment='图标')
|
||||
sort = db.Column(db.Integer, comment='排序')
|
||||
enable = db.Column(db.Boolean, comment='是否开启')
|
||||
|
||||
parent = db.relationship("RightModel", remote_side=[id]) # 自关联
|
||||
|
||||
"""
|
||||
id = db.Column(db.Integer, primary_key=True, comment='权限编号')
|
||||
powerName = db.Column(db.String(255), comment='权限名称')
|
||||
powerType = db.Column(db.SMALLINT, comment='权限类型')
|
||||
powerCode = db.Column(db.String(30), comment='权限标识')
|
||||
powerUrl = db.Column(db.String(255), comment='权限路径')
|
||||
openType = db.Column(db.String(10), comment='打开方式')
|
||||
parentId = db.Column(db.Integer, db.ForeignKey("rt_power.id"), comment='父类编号')
|
||||
icon = db.Column(db.String(128), comment='图标')
|
||||
sort = db.Column(db.Integer, comment='排序')
|
||||
enable = db.Column(db.Boolean, comment='是否开启')
|
||||
checkArr = db.Column(db.String(128), comment='')
|
||||
|
||||
parent = db.relationship("RightModel", remote_side=[id]) # 自关联
|
||||
"""
|
||||
|
||||
|
||||
class RoleModel(db.Model):
|
||||
__tablename__ = 'rt_role'
|
||||
id = db.Column(db.Integer, primary_key=True, comment='角色ID')
|
||||
name = db.Column(db.String(255), comment='角色名称')
|
||||
code = db.Column(db.String(255), comment='角色标识')
|
||||
enable = db.Column(db.Boolean, comment='是否启用')
|
||||
comment = db.Column(db.String(255), comment='备注')
|
||||
details = db.Column(db.String(255), comment='详情')
|
||||
sort = db.Column(db.Integer, comment='排序')
|
||||
|
||||
power = db.relationship('RightModel', secondary="rt_role_power", backref=db.backref('role'))
|
||||
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
|
||||
from extensions import db
|
||||
|
||||
|
||||
class LogModel(db.Model):
|
||||
__tablename__ = 'lg_logging'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
method = db.Column(db.String(10))
|
||||
uid = db.Column(db.Integer, default=None)
|
||||
url = db.Column(db.String(255))
|
||||
desc = db.Column(db.Text)
|
||||
ip = db.Column(db.String(255))
|
||||
success = db.Column(db.Boolean, default=True)
|
||||
user_agent = db.Column(db.Text)
|
||||
|
||||
create_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
||||
update_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
|
||||
@@ -0,0 +1,48 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from extensions import db
|
||||
|
||||
|
||||
class UserModel(db.Model, UserMixin):
|
||||
__tablename__ = 'cp_user'
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True, comment='用户ID')
|
||||
username = db.Column(db.String(20), comment='用户名')
|
||||
realname = db.Column(db.String(20), comment='真实名字')
|
||||
mobile = db.Column(db.String(11), comment='电话号码')
|
||||
avatar = db.Column(db.String(255), comment='头像', default="/static/admin/admin/images/avatar.jpg")
|
||||
comment = db.Column(db.String(255), comment='备注')
|
||||
password_hash = db.Column(db.String(128), comment='哈希密码')
|
||||
enable = db.Column(db.Integer, default=0, comment='启用')
|
||||
dept_id = db.Column(db.Integer, comment='部门id')
|
||||
|
||||
role = db.relationship('RoleModel', secondary="rt_user_role", backref=db.backref('user'), lazy='dynamic')
|
||||
|
||||
def set_password(self, password):
|
||||
"""设置密码,对密码进行加密存储"""
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def validate_password(self, password):
|
||||
"""校验密码方法"""
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
create_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
||||
update_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
|
||||
|
||||
|
||||
class DepartmentModel(db.Model):
|
||||
__tablename__ = 'cp_dept'
|
||||
id = db.Column(db.Integer, primary_key=True, comment="部门ID")
|
||||
parent_id = db.Column(db.Integer, comment="父级编号")
|
||||
dept_name = db.Column(db.String(50), comment="部门名称")
|
||||
leader = db.Column(db.String(50), comment="负责人")
|
||||
phone = db.Column(db.String(20), comment="联系方式")
|
||||
email = db.Column(db.String(50), comment="邮箱")
|
||||
status = db.Column(db.Boolean, comment='状态(1开启,0关闭)')
|
||||
comment = db.Column(db.Text, comment="备注")
|
||||
address = db.Column(db.String(255), comment="详细地址")
|
||||
sort = db.Column(db.Integer, comment="排序")
|
||||
|
||||
create_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间')
|
||||
@@ -1,23 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from common.model import BaseModel, Column, Integer, String, Text, DateTime, SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class AdminLog(BaseModel):
|
||||
__tablename__ = 'sys_admin_log'
|
||||
id = Column(Integer, primary_key=True)
|
||||
method = Column(String(10))
|
||||
uid = Column(Integer)
|
||||
url = Column(String(255))
|
||||
desc = Column(Text)
|
||||
ip = Column(String(255))
|
||||
success = Column(Integer)
|
||||
user_agent = Column(Text)
|
||||
create_time = Column(DateTime, default=datetime.datetime.now)
|
||||
|
||||
|
||||
class AdminLogSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = AdminLog
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,26 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from common.model import BaseModel, Column, Integer, String, Text, DateTime, SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class Dept(BaseModel):
|
||||
__tablename__ = 'sys_dept'
|
||||
id = Column(Integer, primary_key=True, comment="部门ID")
|
||||
parent_id = Column(Integer, comment="父级编号")
|
||||
dept_name = Column(String(50), comment="部门名称")
|
||||
sort = Column(Integer, comment="排序")
|
||||
leader = Column(String(50), comment="负责人")
|
||||
phone = Column(String(20), comment="联系方式")
|
||||
email = Column(String(50), comment="邮箱")
|
||||
status = Column(Integer, comment='状态(1开启,0关闭)')
|
||||
remark = Column(Text, comment="备注")
|
||||
address = Column(String(255), comment="详细地址")
|
||||
create_at = Column(DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='创建时间')
|
||||
|
||||
|
||||
class DeptSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Dept
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,41 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from common.model import BaseModel, Column, Integer, String, DateTime, SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class DictType(BaseModel):
|
||||
__tablename__ = 'sys_dict_type'
|
||||
id = Column(Integer, primary_key=True)
|
||||
type_name = Column(String(255), comment='字典类型名称')
|
||||
type_code = Column(String(255), comment='字典类型标识')
|
||||
description = Column(String(255), comment='字典类型描述')
|
||||
enable = Column(Integer, comment='是否开启')
|
||||
create_time = Column(DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
|
||||
|
||||
|
||||
class DictTypeSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = DictType
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
|
||||
|
||||
class DictData(BaseModel):
|
||||
__tablename__ = 'sys_dict_data'
|
||||
id = Column(Integer, primary_key=True)
|
||||
data_label = Column(String(255), comment='字典类型名称')
|
||||
data_value = Column(String(255), comment='字典类型标识')
|
||||
type_code = Column(String(255), comment='字典类型描述')
|
||||
is_default = Column(Integer, comment='是否默认')
|
||||
enable = Column(Integer, comment='是否开启')
|
||||
remark = Column(String(255), comment='备注')
|
||||
create_time = Column(DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
|
||||
|
||||
|
||||
class DictDataSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = DictData
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,20 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from common.model import BaseModel, Integer, Column, String, CHAR, DateTime, SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class File(BaseModel):
|
||||
__tablename__ = 'sys_file'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
href = Column(String(255))
|
||||
mime = Column(CHAR(50), nullable=False)
|
||||
size = Column(CHAR(30), nullable=False)
|
||||
create_time = Column(DateTime, default=datetime.datetime.now)
|
||||
|
||||
|
||||
class AdminLogSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = False
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,20 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from common.model import BaseModel, Integer, Column, String, CHAR, DateTime, SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class Photo(BaseModel):
|
||||
__tablename__ = 'sys_photo'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
href = Column(String(255))
|
||||
mime = Column(CHAR(50), nullable=False)
|
||||
size = Column(CHAR(30), nullable=False)
|
||||
create_time = Column(DateTime, default=datetime.datetime.now)
|
||||
|
||||
|
||||
class AdminLogSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Photo
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,26 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from common.model import BaseModel, Column, Integer, String, DateTime, SQLAlchemyAutoSchema
|
||||
|
||||
|
||||
class Power(BaseModel):
|
||||
__tablename__ = 'sys_power'
|
||||
id = Column(Integer, primary_key=True, comment='权限编号')
|
||||
name = Column(String(255), comment='权限名称')
|
||||
type = Column(String(1), comment='权限类型')
|
||||
code = Column(String(30), comment='权限标识')
|
||||
url = Column(String(255), comment='权限路径')
|
||||
open_type = Column(String(10), comment='打开方式')
|
||||
parent_id = Column(Integer, comment='父类编号')
|
||||
icon = Column(String(128), comment='图标')
|
||||
sort = Column(Integer, comment='排序')
|
||||
create_time = Column(DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
|
||||
enable = Column(Integer, comment='是否开启')
|
||||
|
||||
|
||||
class PowerSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Power
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,23 +0,0 @@
|
||||
import datetime
|
||||
from common.model import BaseModel, Column, String, Integer, DateTime, relationship, SQLAlchemyAutoSchema, backref
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
__tablename__ = 'sys_role'
|
||||
id = Column(Integer, primary_key=True, comment='角色ID')
|
||||
name = Column(String(255), comment='角色名称')
|
||||
code = Column(String(255), comment='角色标识')
|
||||
enable = Column(Integer, comment='是否启用')
|
||||
remark = Column(String(255), comment='备注')
|
||||
details = Column(String(255), comment='详情')
|
||||
sort = Column(Integer, comment='排序')
|
||||
create_time = Column(DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_time = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='更新时间')
|
||||
power = relationship('Power', secondary="sys_role_power", backref=backref('role'))
|
||||
|
||||
|
||||
class RoleSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Role
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
@@ -1,8 +0,0 @@
|
||||
from common.model import Column, Table, Integer, ForeignKey
|
||||
|
||||
role_power = Table(
|
||||
"sys_role_power",
|
||||
Column("id", Integer, primary_key=True, autoincrement=True, comment='标识'),
|
||||
Column("power_id", Integer, ForeignKey("sys_power.id"), comment='用户编号'),
|
||||
Column("role_id", Integer, ForeignKey("sys_role.id"), comment='角色编号'),
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
import datetime
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
from common.model import BaseModel, Column, Integer, String, DateTime, relationship, SQLAlchemyAutoSchema, backref
|
||||
|
||||
|
||||
class User(BaseModel, UserMixin):
|
||||
__tablename__ = 'sys_user'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, comment='用户ID')
|
||||
username = Column(String(20), comment='用户名')
|
||||
realname = Column(String(20), comment='真实名字')
|
||||
avatar = Column(String(255), comment='头像', default="/static/admin/admin/images/avatar.jpg")
|
||||
remark = Column(String(255), comment='备注')
|
||||
password_hash = Column(String(128), comment='哈希密码')
|
||||
enable = Column(Integer, default=0, comment='启用')
|
||||
dept_id = Column(Integer, comment='部门id')
|
||||
create_at = Column(DateTime, default=datetime.datetime.now, comment='创建时间')
|
||||
update_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment='创建时间')
|
||||
role = relationship('Role', secondary="sys_user_role", backref=backref('user'), lazy='dynamic')
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def validate_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
|
||||
# 如果要全换全部字段,就不要声明fields或exclude字段即可
|
||||
class UserRoleSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = User
|
||||
include_fk = True
|
||||
load_instance = True
|
||||
exclude = ['password_hash']
|
||||
@@ -1,8 +0,0 @@
|
||||
from common.model import Table, Column, Integer, ForeignKey
|
||||
|
||||
user_role = Table(
|
||||
"sys_user_role",
|
||||
Column("id", Integer, primary_key=True, autoincrement=True, comment='标识'),
|
||||
Column("user_id", Integer, ForeignKey("sys_user.id"), comment='用户编号'),
|
||||
Column("role_id", Integer, ForeignKey("sys_role.id"), comment='角色编号'),
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
from common.view import Blueprint, View
|
||||
|
||||
|
||||
user = Blueprint('sys', __name__, url_prefix='/user')
|
||||
|
||||
|
||||
@user.route('/')
|
||||
class UserView(View):
|
||||
def get(self):
|
||||
pass
|
||||
# data, total = db.session.query(User.id, User.username).layui_paginate_json()
|
||||
# print(data)
|
||||
# return jsonify(data)
|
||||
@@ -1,2 +0,0 @@
|
||||
-r min.txt
|
||||
-r extra.txt
|
||||
@@ -1,4 +0,0 @@
|
||||
Flask-Mail==0.9.1
|
||||
celery==5.2.3
|
||||
qiniu==7.5.0
|
||||
upyun==2.5.5
|
||||
@@ -1,11 +0,0 @@
|
||||
-i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
Flask==2.0.2
|
||||
flask-restx==0.5.1
|
||||
pymysql==1.0.2
|
||||
Flask-Migrate==3.1.0
|
||||
flask-marshmallow==0.14.0
|
||||
marshmallow-sqlalchemy==0.27.0
|
||||
Flask-Login==0.5.0
|
||||
Pillow==9.0.0
|
||||
flask-limiter-2.1
|
||||
cachelib-0.6.0
|
||||
@@ -0,0 +1,28 @@
|
||||
alembic==1.6.5
|
||||
aniso8601==9.0.1
|
||||
captcha==0.3
|
||||
click==8.0.1
|
||||
colorama==0.4.4
|
||||
Flask==2.0.1
|
||||
Flask-Login==0.5.0
|
||||
Flask-Migrate==3.0.1
|
||||
Flask-Pydantic==0.9.0
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
greenlet==1.1.0
|
||||
gunicorn==20.1.0
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.1
|
||||
Mako==1.1.4
|
||||
MarkupSafe==2.0.1
|
||||
Pillow==8.2.0
|
||||
psutil==5.8.0
|
||||
pydantic==1.9.1
|
||||
PyMySQL==1.0.2
|
||||
python-dateutil==2.8.1
|
||||
python-dotenv==0.18.0
|
||||
python-editor==1.0.4
|
||||
pytz==2021.1
|
||||
six==1.16.0
|
||||
SQLAlchemy==1.4.18
|
||||
typing_extensions==4.2.0
|
||||
Werkzeug==2.0.1
|
||||
@@ -0,0 +1,560 @@
|
||||
html,
|
||||
body,
|
||||
.layui-layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pear-admin .layui-header,
|
||||
.pear-admin .layui-body,
|
||||
.pear-admin .layui-logo,
|
||||
.pear-admin .layui-side,
|
||||
.pear-admin .layui-header .layui-layout-left {
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-side {
|
||||
top: 60px!important;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-side .layui-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-header .layui-logo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-side .layui-side-scroll {
|
||||
height: 100%!important;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-side .layui-side-scroll {
|
||||
height: 100%!important;
|
||||
}
|
||||
|
||||
.pear-admin .layui-header.dark-theme .layui-layout-control .layui-this *{
|
||||
background-color: rgba(0,0,0,.1)!important;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-header {
|
||||
z-index: 99999;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-header .layui-layout-left {
|
||||
left: 230px;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-header .layui-logo .title {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-header .layui-layout-control {
|
||||
display: inline-block;
|
||||
left: 370px;
|
||||
}
|
||||
|
||||
.pear-admin.banner-layout .layui-header.dark-theme {
|
||||
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
|
||||
}
|
||||
|
||||
.pear-admin .layui-header .layui-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-admin .layui-logo .title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-layout-right .layui-nav-child {
|
||||
border: 1px solid whitesmoke;
|
||||
border-radius: 4px;
|
||||
width: auto;
|
||||
left: auto;
|
||||
right: -23px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-header {
|
||||
left: 230px;
|
||||
width: calc(100% - 230px);
|
||||
background-color: white;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
|
||||
.pear-admin .layui-layout-control {
|
||||
left: 140px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.pear-admin .layui-layout-control .layui-nav {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-logo {
|
||||
width: 230px;
|
||||
height: 59px;
|
||||
line-height: 59px;
|
||||
position: relative;
|
||||
background-color: #28333E;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.pear-admin .layui-logo img {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-logo .title {
|
||||
font-size: 21px;
|
||||
font-weight: 550;
|
||||
color: #5FB878;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-logo .logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-admin .layui-side {
|
||||
top: 0px;
|
||||
width: 230px;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.pear-admin .layui-side-scroll::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-side-scroll {
|
||||
height: calc(100% - 60px) !important;
|
||||
background-color: #28333E;
|
||||
width: 247px;
|
||||
|
||||
}
|
||||
|
||||
.pear-admin .layui-header .layui-nav .layui-nav-item>a {
|
||||
color: black;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-body {
|
||||
left: 230px;
|
||||
bottom: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-layout-left {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-footer {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
left: 230px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #F2F2F2;
|
||||
box-shadow: none;
|
||||
-webkit-transition: left .3s;
|
||||
transition: left .3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pear-admin .layui-footer.close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** 收缩布局 */
|
||||
.pear-mini .layui-side .layui-logo .title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-mini .layui-side .layui-logo .logo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.pear-mini .layui-side {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-header {
|
||||
left: 60px;
|
||||
width: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.pear-mini .layui-body {
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-side .layui-logo {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-footer {
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-nav-tree .layui-nav-item span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-mini .bottom-nav li {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.pear-mini .layui-side-scroll {
|
||||
height: calc(100% - 60px);
|
||||
}
|
||||
|
||||
.pear-admin .layui-header .layui-nav .layui-nav-bar {
|
||||
top: 0px !important;
|
||||
height: 2px !important;
|
||||
background-color: #5FB878;
|
||||
}
|
||||
|
||||
.pear-admin .layui-header .layui-nav .layui-this:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-admin .layui-header .layui-nav-more {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-collapsed-pe {
|
||||
display: none;
|
||||
width: 50px;
|
||||
position: absolute;
|
||||
z-index: 400000;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
background-color: #5FB878 !important;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
border-radius: 50px;
|
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
|
||||
}
|
||||
|
||||
.pear-collapsed-pe a {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.layui-hide-sm {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.layui-hide-sm {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增兼容 */
|
||||
@media screen and (max-width:768px) {
|
||||
.collapse {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pear-collapsed-pe {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.layui-layout-control {
|
||||
left: 45px !important;
|
||||
}
|
||||
|
||||
.layui-layout-left {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.pear-mini .bottom-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pear-mini .layui-side-scroll {
|
||||
height: calc(100% - 62px);
|
||||
}
|
||||
|
||||
.pear-mini .layui-side {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-header {
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pear-mini .layui-body {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-footer {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.pear-mini .layui-logo {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-body {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.pear-admin .layui-header {
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pear-admin .pear-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #1E1E1E;
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
margin-top: -60px;
|
||||
}
|
||||
|
||||
.pear-mini .pear-cover {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes am-horizontal-roll_show {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateX(2000px);
|
||||
transform: translateX(2000px)
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes am-horizontal-roll_show {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateX(800px);
|
||||
-ms-transform: translateX(800px);
|
||||
transform: translateX(800px)
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateX(0);
|
||||
-ms-transform: translateX(0);
|
||||
transform: translateX(0)
|
||||
}
|
||||
}
|
||||
|
||||
.layer-anim-right {
|
||||
-webkit-animation: am-horizontal-roll_show .6s ease-out;
|
||||
animation: am-horizontal-roll_show .6s ease-out;
|
||||
|
||||
}
|
||||
|
||||
/** 侧边主题 (亮) */
|
||||
.light-theme.layui-side {
|
||||
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, .05) !important;
|
||||
}
|
||||
|
||||
.light-theme.layui-side .layui-logo {
|
||||
background-color: white !important;
|
||||
color: black !important;
|
||||
border-bottom: 1px whitesmoke solid;
|
||||
}
|
||||
|
||||
.light-theme.layui-side .layui-side-scroll {
|
||||
background-color: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.dark-theme.layui-header {
|
||||
border-bottom: none;
|
||||
background-color: #28333E;
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
.dark-theme.layui-header li>a{
|
||||
color: whitesmoke!important;
|
||||
}
|
||||
|
||||
.dark-theme.layui-header .layui-logo {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/** 顶部主题 (白) */
|
||||
.light-theme.layui-header .layui-logo {
|
||||
background-color: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/** 主题面板 */
|
||||
.pearone-color .set-text {
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
}
|
||||
|
||||
.pearone-color .color-title {
|
||||
padding: 15px 0 0px 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.pearone-color .color-content {
|
||||
padding: 15px 10px 0 20px;
|
||||
}
|
||||
|
||||
.pearone-color .color-content ul {
|
||||
list-style: none;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.pearone-color .color-content ul li {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 70px;
|
||||
height: 50px;
|
||||
margin: 0 20px 20px 0;
|
||||
padding: 2px 2px 2px 2px;
|
||||
background-color: #f2f2f2;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pearone-color .color-content li.layui-this:after,
|
||||
.pearone-color .color-content li:hover:after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 4px;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
border: #5FB878 2px solid;
|
||||
opacity: 1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pearone-color .color-content li:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 1px;
|
||||
height: 0;
|
||||
border: 2px solid #F2F2F2;
|
||||
transition: all .3s;
|
||||
-webkit-transition: all .3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.select-color {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.select-color .select-color-title {
|
||||
padding: 15px 0 0px 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.select-color .select-color-content {
|
||||
padding: 20px 0 0px 0px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.select-color .select-color-content .select-color-item {
|
||||
background-color: gray;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 3px;
|
||||
float: left;
|
||||
margin-left: 20px;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .15);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.message .layui-tab-title li:not(:last-child) {
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* 搜索面板 */
|
||||
.menu-search-content .layui-input-prefix {
|
||||
position: absolute;
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.menu-search-content .layui-input {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.menu-search-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menu-search-input-wrapper {
|
||||
width: 100%;
|
||||
padding: 15px 15px;
|
||||
}
|
||||
|
||||
.menu-search-no-data {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 122px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-search-list {
|
||||
width: 100%;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
.menu-search-list li {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
height: 50px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0px 10px;
|
||||
color: currentColor;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px #d4d9e1;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.menu-search-list li:hover {
|
||||
background-color: #5FB878;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-search-list li.this {
|
||||
background-color: #5FB878;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 搜索面板结束 */
|
||||
@@ -0,0 +1,105 @@
|
||||
.loader-main{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: whitesmoke;
|
||||
z-index: 9999999;
|
||||
}
|
||||
.loader {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 30px auto 40px;
|
||||
margin-top: 20%;
|
||||
position: relative;
|
||||
z-index: 999999;
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
.loader:before {
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
opacity: 0.1;
|
||||
position: absolute;
|
||||
top: 59px;
|
||||
left: 0;
|
||||
animation: shadow .5s linear infinite;
|
||||
}
|
||||
.loader:after {
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 3px;
|
||||
background-color: #5FB878;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
animation: loading .5s linear infinite;
|
||||
}
|
||||
@-webkit-keyframes loading {
|
||||
17% {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(9px) rotate(22.5deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
|
||||
border-bottom-right-radius: 40px;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateY(9px) rotate(67.5deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0) rotate(90deg);
|
||||
}
|
||||
}
|
||||
@keyframes loading {
|
||||
17% {
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translateY(9px) rotate(22.5deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(18px) scale(1, 0.9) rotate(45deg);
|
||||
border-bottom-right-radius: 40px;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateY(9px) rotate(67.5deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0) rotate(90deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes shadow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2, 1);
|
||||
}
|
||||
}
|
||||
@keyframes shadow {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
.top-panel {
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.top-panel>.layui-card-body {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.top-panel-number {
|
||||
line-height: 60px;
|
||||
font-size: 29px;
|
||||
border-right: 1px solid #eceff9;
|
||||
}
|
||||
|
||||
.top-panel-tips {
|
||||
padding-left: 8px;
|
||||
padding-top: 16px;
|
||||
line-height: 30px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pear-container {
|
||||
background-color: whitesmoke;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
background-color: whitesmoke;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card .header .avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 20px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
|
||||
.card .header {
|
||||
color: dimgray;
|
||||
}
|
||||
|
||||
.card .body {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.card .body {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.card .footer {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-top: 20px;
|
||||
font-size: 13px;
|
||||
color: gray;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.custom-tab .layui-tab-title {
|
||||
border-bottom-width: 0px;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
|
||||
.custom-tab .layui-tab-title li {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.list .list-item {
|
||||
height: 31.8px;
|
||||
line-height: 31.8px;
|
||||
color: gray;
|
||||
padding: 5px;
|
||||
padding-left: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 5.2px;
|
||||
}
|
||||
|
||||
.list .list-item:hover {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
.list .list-item .title {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list .list-item .footer {
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.top-panel-tips i {
|
||||
font-size: 33px;
|
||||
}
|
||||
|
||||
.layuiadmin-card-status {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.layuiadmin-card-status dd {
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #EEE;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.layuiadmin-card-status dd div.layui-status-img,
|
||||
.layuiadmin-card-team .layui-team-img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.layuiadmin-card-status dd div.layui-status-img a {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.layuiadmin-card-status dd div span {
|
||||
color: #BBB;
|
||||
}
|
||||
|
||||
.layuiadmin-card-status dd div a {
|
||||
color: #01AAED;
|
||||
}
|
||||
|
||||
.top-panel-tips svg {
|
||||
margin-top: -12px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
.pear-container {
|
||||
background-color: whitesmoke;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.pear-card {
|
||||
width: 100%;
|
||||
height: 66px;
|
||||
background-color: #F8F8F8;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.pear-card:hover,
|
||||
.pear-card2:hover {
|
||||
box-shadow: 2px 0 8px 0 lightgray !important;
|
||||
}
|
||||
|
||||
.pear-card2 {
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
background-color: #F8F8F8;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.pear-card2 i {
|
||||
font-size: 30px;
|
||||
height: 90px;
|
||||
line-height: 90px;
|
||||
}
|
||||
|
||||
.pear-card i {
|
||||
font-size: 30px;
|
||||
height: 66px;
|
||||
line-height: 66px;
|
||||
}
|
||||
|
||||
.layui-col-md3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pear-card-title {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.person img {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 4px;
|
||||
margin-top: 8px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.pear-card2 .count {
|
||||
color: #51A351;
|
||||
font-size: 30px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.pear-card2 .title {
|
||||
color: gray;
|
||||
font-size: 14px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.pear-card-status {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.pear-card-status li {
|
||||
position: relative;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #EEE;
|
||||
}
|
||||
|
||||
.pear-card-status li h3 {
|
||||
padding-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pear-card-status li p {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.pear-card-status li>span {
|
||||
color: #999;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.pear-reply {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 12px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.person .title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin-left: 18px;
|
||||
margin-top: 16px;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.person .desc {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-left: 115px;
|
||||
margin-top: -30px;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.organizationTree {
|
||||
width: 100% !important;
|
||||
height: -webkit-calc(100vh - 130px);
|
||||
height: -moz-calc(100vh - 130px);
|
||||
height: calc(100vh - 130px);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 0.38rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
-webkit-tap-highlight-color: transparent
|
||||
}
|
||||
|
||||
.clearfix:after {
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 0.28rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background-color: white !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content>img {
|
||||
height: 300px;
|
||||
max-width: 370px;
|
||||
margin-right: 180px;
|
||||
}
|
||||
|
||||
.content>* {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content-r {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.content-r>h1 {
|
||||
font-size: 72px;
|
||||
color: #434e59;
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content-r>p {
|
||||
font-size: 20px;
|
||||
color: rgba(0, 0, 0, .45);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user