From 335ef6ca03d93ea5aeb4fba0bfd46c172f0b0934 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Mon, 29 May 2023 10:55:32 +0800 Subject: [PATCH 001/169] =?UTF-8?q?feat:=E5=88=9D=E5=A7=8B=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=EF=BC=8C=E5=AE=8C=E6=88=90=E9=A1=B9=E7=9B=AE=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 65 ++++ dash-fastapi-backend/config/database.py | 13 + dash-fastapi-backend/config/env.py | 29 ++ dash-fastapi-backend/config/get_db.py | 16 + .../controller/login_controller.py | 66 ++++ .../controller/user_controller.py | 108 ++++++ dash-fastapi-backend/entity/dept_entity.py | 28 ++ dash-fastapi-backend/entity/dict_entity.py | 49 +++ dash-fastapi-backend/entity/log_entity.py | 55 ++++ dash-fastapi-backend/entity/menu_entity.py | 33 ++ dash-fastapi-backend/entity/post_entity.py | 24 ++ dash-fastapi-backend/entity/role_entity.py | 48 +++ dash-fastapi-backend/entity/user_entity.py | 53 +++ dash-fastapi-backend/mapper/crud/dept_crud.py | 16 + .../mapper/crud/login_crud.py | 18 + dash-fastapi-backend/mapper/crud/post_crud.py | 14 + dash-fastapi-backend/mapper/crud/role_crud.py | 15 + dash-fastapi-backend/mapper/crud/user_crud.py | 229 +++++++++++++ .../mapper/schema/login_schema.py | 16 + .../mapper/schema/user_schema.py | 218 ++++++++++++ dash-fastapi-backend/service/login_service.py | 181 ++++++++++ dash-fastapi-backend/service/user_service.py | 103 ++++++ dash-fastapi-backend/utils/log_tool.py | 11 + dash-fastapi-backend/utils/page_tool.py | 33 ++ dash-fastapi-backend/utils/response_tool.py | 65 ++++ .../utils/time_format_tool.py | 26 ++ dash-fastapi-frontend/api/login.py | 11 + dash-fastapi-frontend/api/message.py | 6 + dash-fastapi-frontend/api/user.py | 31 ++ dash-fastapi-frontend/app.py | 217 ++++++++++++ dash-fastapi-frontend/assets/css/global.css | 42 +++ .../assets/imgs/login-background.jpg | Bin 0 -> 521275 bytes ...\350\275\275\345\212\250\347\224\273.webp" | Bin 0 -> 66845 bytes dash-fastapi-frontend/callbacks/__init__.py | 0 dash-fastapi-frontend/callbacks/app_c.py | 47 +++ dash-fastapi-frontend/callbacks/forget_c.py | 311 ++++++++++++++++++ .../callbacks/layout_c/__init__.py | 0 .../callbacks/layout_c/aside_c.py | 17 + .../callbacks/layout_c/fold_side_menu.py | 36 ++ .../callbacks/layout_c/head_c.py | 60 ++++ .../callbacks/layout_c/index_c.py | 131 ++++++++ dash-fastapi-frontend/callbacks/login_c.py | 168 ++++++++++ dash-fastapi-frontend/config/global_config.py | 21 ++ dash-fastapi-frontend/server.py | 85 +++++ dash-fastapi-frontend/store/store.py | 15 + dash-fastapi-frontend/utils/file.py | 0 dash-fastapi-frontend/utils/request.py | 50 +++ dash-fastapi-frontend/utils/tree_tool.py | 118 +++++++ dash-fastapi-frontend/views/__init__.py | 8 + dash-fastapi-frontend/views/forget.py | 133 ++++++++ .../views/layout/__init__.py | 3 + .../views/layout/components/aside.py | 84 +++++ .../views/layout/components/content.py | 42 +++ .../views/layout/components/head.py | 133 ++++++++ dash-fastapi-frontend/views/layout/index.py | 139 ++++++++ dash-fastapi-frontend/views/login.py | 188 +++++++++++ .../views/monitor/__init__.py | 4 + .../views/monitor/logininfor/__init__.py | 3 + .../views/monitor/logininfor/index.py | 8 + .../views/monitor/operlog/__init__.py | 3 + .../views/monitor/operlog/index.py | 8 + dash-fastapi-frontend/views/page_404.py | 38 +++ .../views/system/__init__.py | 10 + .../views/system/config/__init__.py | 3 + .../views/system/config/index.py | 8 + .../views/system/dept/__init__.py | 3 + .../views/system/dept/index.py | 8 + .../views/system/dict/__init__.py | 3 + .../views/system/dict/index.py | 8 + .../views/system/menu/__init__.py | 3 + .../views/system/menu/index.py | 8 + .../views/system/notice/__init__.py | 3 + .../views/system/notice/index.py | 8 + .../views/system/post/__init__.py | 3 + .../views/system/post/index.py | 8 + .../views/system/role/__init__.py | 3 + .../views/system/role/index.py | 8 + .../views/system/user/__init__.py | 3 + .../views/system/user/index.py | 8 + 79 files changed, 3790 insertions(+) create mode 100644 dash-fastapi-backend/app.py create mode 100644 dash-fastapi-backend/config/database.py create mode 100644 dash-fastapi-backend/config/env.py create mode 100644 dash-fastapi-backend/config/get_db.py create mode 100644 dash-fastapi-backend/controller/login_controller.py create mode 100644 dash-fastapi-backend/controller/user_controller.py create mode 100644 dash-fastapi-backend/entity/dept_entity.py create mode 100644 dash-fastapi-backend/entity/dict_entity.py create mode 100644 dash-fastapi-backend/entity/log_entity.py create mode 100644 dash-fastapi-backend/entity/menu_entity.py create mode 100644 dash-fastapi-backend/entity/post_entity.py create mode 100644 dash-fastapi-backend/entity/role_entity.py create mode 100644 dash-fastapi-backend/entity/user_entity.py create mode 100644 dash-fastapi-backend/mapper/crud/dept_crud.py create mode 100644 dash-fastapi-backend/mapper/crud/login_crud.py create mode 100644 dash-fastapi-backend/mapper/crud/post_crud.py create mode 100644 dash-fastapi-backend/mapper/crud/role_crud.py create mode 100644 dash-fastapi-backend/mapper/crud/user_crud.py create mode 100644 dash-fastapi-backend/mapper/schema/login_schema.py create mode 100644 dash-fastapi-backend/mapper/schema/user_schema.py create mode 100644 dash-fastapi-backend/service/login_service.py create mode 100644 dash-fastapi-backend/service/user_service.py create mode 100644 dash-fastapi-backend/utils/log_tool.py create mode 100644 dash-fastapi-backend/utils/page_tool.py create mode 100644 dash-fastapi-backend/utils/response_tool.py create mode 100644 dash-fastapi-backend/utils/time_format_tool.py create mode 100644 dash-fastapi-frontend/api/login.py create mode 100644 dash-fastapi-frontend/api/message.py create mode 100644 dash-fastapi-frontend/api/user.py create mode 100644 dash-fastapi-frontend/app.py create mode 100644 dash-fastapi-frontend/assets/css/global.css create mode 100644 dash-fastapi-frontend/assets/imgs/login-background.jpg create mode 100644 "dash-fastapi-frontend/assets/imgs/\345\212\240\350\275\275\345\212\250\347\224\273.webp" create mode 100644 dash-fastapi-frontend/callbacks/__init__.py create mode 100644 dash-fastapi-frontend/callbacks/app_c.py create mode 100644 dash-fastapi-frontend/callbacks/forget_c.py create mode 100644 dash-fastapi-frontend/callbacks/layout_c/__init__.py create mode 100644 dash-fastapi-frontend/callbacks/layout_c/aside_c.py create mode 100644 dash-fastapi-frontend/callbacks/layout_c/fold_side_menu.py create mode 100644 dash-fastapi-frontend/callbacks/layout_c/head_c.py create mode 100644 dash-fastapi-frontend/callbacks/layout_c/index_c.py create mode 100644 dash-fastapi-frontend/callbacks/login_c.py create mode 100644 dash-fastapi-frontend/config/global_config.py create mode 100644 dash-fastapi-frontend/server.py create mode 100644 dash-fastapi-frontend/store/store.py create mode 100644 dash-fastapi-frontend/utils/file.py create mode 100644 dash-fastapi-frontend/utils/request.py create mode 100644 dash-fastapi-frontend/utils/tree_tool.py create mode 100644 dash-fastapi-frontend/views/__init__.py create mode 100644 dash-fastapi-frontend/views/forget.py create mode 100644 dash-fastapi-frontend/views/layout/__init__.py create mode 100644 dash-fastapi-frontend/views/layout/components/aside.py create mode 100644 dash-fastapi-frontend/views/layout/components/content.py create mode 100644 dash-fastapi-frontend/views/layout/components/head.py create mode 100644 dash-fastapi-frontend/views/layout/index.py create mode 100644 dash-fastapi-frontend/views/login.py create mode 100644 dash-fastapi-frontend/views/monitor/__init__.py create mode 100644 dash-fastapi-frontend/views/monitor/logininfor/__init__.py create mode 100644 dash-fastapi-frontend/views/monitor/logininfor/index.py create mode 100644 dash-fastapi-frontend/views/monitor/operlog/__init__.py create mode 100644 dash-fastapi-frontend/views/monitor/operlog/index.py create mode 100644 dash-fastapi-frontend/views/page_404.py create mode 100644 dash-fastapi-frontend/views/system/__init__.py create mode 100644 dash-fastapi-frontend/views/system/config/__init__.py create mode 100644 dash-fastapi-frontend/views/system/config/index.py create mode 100644 dash-fastapi-frontend/views/system/dept/__init__.py create mode 100644 dash-fastapi-frontend/views/system/dept/index.py create mode 100644 dash-fastapi-frontend/views/system/dict/__init__.py create mode 100644 dash-fastapi-frontend/views/system/dict/index.py create mode 100644 dash-fastapi-frontend/views/system/menu/__init__.py create mode 100644 dash-fastapi-frontend/views/system/menu/index.py create mode 100644 dash-fastapi-frontend/views/system/notice/__init__.py create mode 100644 dash-fastapi-frontend/views/system/notice/index.py create mode 100644 dash-fastapi-frontend/views/system/post/__init__.py create mode 100644 dash-fastapi-frontend/views/system/post/index.py create mode 100644 dash-fastapi-frontend/views/system/role/__init__.py create mode 100644 dash-fastapi-frontend/views/system/role/index.py create mode 100644 dash-fastapi-frontend/views/system/user/__init__.py create mode 100644 dash-fastapi-frontend/views/system/user/index.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py new file mode 100644 index 0000000..d1158fd --- /dev/null +++ b/dash-fastapi-backend/app.py @@ -0,0 +1,65 @@ +from fastapi import FastAPI, Request +import uvicorn +import aioredis +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import HTTPException +from fastapi.middleware.cors import CORSMiddleware +from controller.login_controller import loginController +from controller.user_controller import userController +from config.env import RedisConfig + + +app = FastAPI() + +# 前端页面url +origins = [ + "http://localhost:8088", +] + +# 后台api允许跨域 +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +async def create_redis_pool() -> aioredis.Redis: + redis = await aioredis.from_url( + url=f"redis://{RedisConfig.HOST}", + port=RedisConfig.PORT, + username=RedisConfig.USERNAME, + password=RedisConfig.PASSWORD, + db=RedisConfig.DB, + encoding="utf-8", + decode_responses=True + ) + return redis + + +@app.on_event("startup") +async def startup_event(): + app.state.redis = await create_redis_pool() + + +@app.on_event("shutdown") +async def shutdown_event(): + await app.state.redis.close() + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + content=jsonable_encoder({"message": exc.detail, "code": exc.status_code}), + status_code=exc.status_code + ) + +app.include_router(loginController, prefix="/login", tags=['login']) +app.include_router(userController, prefix="/system", tags=['system']) + + +if __name__ == '__main__': + uvicorn.run(app='app:app', host="127.0.0.1", port=9099, reload=True) diff --git a/dash-fastapi-backend/config/database.py b/dash-fastapi-backend/config/database.py new file mode 100644 index 0000000..707a3e6 --- /dev/null +++ b/dash-fastapi-backend/config/database.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from config.env import DataBaseConfig + +SQLALCHEMY_DATABASE_URL = f"mysql+pymysql://{DataBaseConfig.USERNAME}:{DataBaseConfig.PASSWORD}@" \ + f"{DataBaseConfig.HOST}:{DataBaseConfig.PORT}/{DataBaseConfig.DB}" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, echo=True +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() diff --git a/dash-fastapi-backend/config/env.py b/dash-fastapi-backend/config/env.py new file mode 100644 index 0000000..d602163 --- /dev/null +++ b/dash-fastapi-backend/config/env.py @@ -0,0 +1,29 @@ +class JwtConfig: + """ + Jwt配置 + """ + SECRET_KEY = "b01c66dc2c58dc6a0aabfe2144256be36226de378bf87f72c0c795dda67f4d55" + ALGORITHM = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES = 1440 + + +class DataBaseConfig: + """ + 数据库配置 + """ + HOST = "127.0.0.1" + PORT = 3306 + USERNAME = 'root' + PASSWORD = 'mysqlroot' + DB = 'dash-fastapi' + + +class RedisConfig: + """ + Redis配置 + """ + HOST = "127.0.0.1" + PORT = 6379 + USERNAME = '' + PASSWORD = '' + DB = 2 diff --git a/dash-fastapi-backend/config/get_db.py b/dash-fastapi-backend/config/get_db.py new file mode 100644 index 0000000..391cf7f --- /dev/null +++ b/dash-fastapi-backend/config/get_db.py @@ -0,0 +1,16 @@ +from config.database import * + + +def get_db_pro(): + """ + 每一个请求处理完毕后会关闭当前连接,不同的请求使用不同的连接 + :return: + """ + current_db = SessionLocal() + try: + yield current_db + finally: + current_db.close() + + +get_db = get_db_pro diff --git a/dash-fastapi-backend/controller/login_controller.py b/dash-fastapi-backend/controller/login_controller.py new file mode 100644 index 0000000..a1bab78 --- /dev/null +++ b/dash-fastapi-backend/controller/login_controller.py @@ -0,0 +1,66 @@ +import uuid +from fastapi import APIRouter, Request +from fastapi import Depends, HTTPException, Header +from config.get_db import get_db +from service.login_service import * +from mapper.schema.login_schema import * +from mapper.crud.login_crud import * +from config.env import JwtConfig +from utils.response_tool import * +from utils.log_tool import * +from datetime import datetime, timedelta + + +loginController = APIRouter() + + +@loginController.post("/loginByAccount", response_model=Token) +async def login(request: Request, user: UserLogin, query_db: Session = Depends(get_db)): + try: + result = authenticate_user(query_db, user.user_name, user.password) + if result == '用户不存在': + logger.warning('用户不存在') + return response_400(data="", message="用户不存在") + + elif result == '密码错误': + logger.warning('密码错误') + return response_400(data="", message="密码错误") + + else: + access_token_expires = timedelta(minutes=JwtConfig.ACCESS_TOKEN_EXPIRE_MINUTES) + try: + access_token = create_access_token( + data={"sub": str(result.user_id)}, expires_delta=access_token_expires + ) + session_id = str(uuid.uuid4()) + await request.app.state.redis.set(f'{result.user_id}_access_token', access_token, ex=timedelta(minutes=30)) + await request.app.state.redis.set(f'{result.user_id}_session_id', session_id, ex=timedelta(minutes=30)) + logger.info('登录成功') + return response_200( + data={ + 'token': access_token, + 'session_id': session_id, + }, + message='登录成功' + ) + except Exception as e: + logger.exception(e) + return response_500(data="", message="生成token失败") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse) +async def get_login_user_info(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + logger.info('获取成功') + return response_200(data=current_user, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/user_controller.py b/dash-fastapi-backend/controller/user_controller.py new file mode 100644 index 0000000..db6ff1f --- /dev/null +++ b/dash-fastapi-backend/controller/user_controller.py @@ -0,0 +1,108 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, HTTPException, Header +from config.get_db import get_db +from service.login_service import get_current_user, get_password_hash +from service.user_service import * +from mapper.schema.user_schema import * +from mapper.crud.user_crud import * +from utils.response_tool import * +from utils.log_tool import * + + +userController = APIRouter() + + +@userController.post("/user/get", response_model=UserPageObjectResponse) +async def get_system_user_list(request: Request, user_query: UserPageObject, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + user_query_result = get_user_list_services(query_db, user_query) + logger.info('获取成功') + return response_200(data=user_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.post("/user/add", response_model=CrudUserResponse) +async def add_system_user(request: Request, add_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + add_user.password = get_password_hash(add_user.password) + add_user.create_by = current_user.user.user_name + add_user.update_by = current_user.user.user_name + add_user_result = add_user_services(query_db, add_user) + logger.info(add_user_result.message) + if add_user_result.is_success: + return response_200(data=add_user_result, message=add_user_result.message) + else: + return response_400(data="", message=add_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.post("/user/edit", response_model=CrudUserResponse) +async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_user_result = edit_user_services(query_db, edit_user) + if edit_user_result.is_success: + logger.info(edit_user_result.message) + return response_200(data=edit_user_result, message=edit_user_result.message) + else: + logger.warning(edit_user_result.message) + return response_400(data="", message=edit_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.post("/user/delete", response_model=CrudUserResponse) +async def delete_system_user(request: Request, delete_user: DeleteUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + delete_user_result = delete_user_services(query_db, delete_user) + if delete_user_result.is_success: + logger.info(delete_user_result.message) + return response_200(data=delete_user_result, message=delete_user_result.message) + else: + logger.warning(delete_user_result.message) + return response_400(data="", message=delete_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.get("/user/{user_id}", response_model=UserDetailModel) +async def query_detail_system_user(request: Request, user_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + delete_user_result = detail_user_services(query_db, user_id) + logger.info(f'获取user_id为{user_id}的信息成功') + return response_200(data=delete_user_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/entity/dept_entity.py b/dash-fastapi-backend/entity/dept_entity.py new file mode 100644 index 0000000..283c2fc --- /dev/null +++ b/dash-fastapi-backend/entity/dept_entity.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, Integer, String, DateTime +from config.database import Base, engine +from datetime import datetime + + +class SysDept(Base): + """ + 部门表 + """ + __tablename__ = 'sys_dept' + + dept_id = Column(Integer, primary_key=True, autoincrement=True, comment='部门id') + parent_id = Column(Integer, default=0, comment='父部门id') + ancestors = Column(String(50), nullable=True, default='', comment='祖级列表') + dept_name = Column(String(30), nullable=True, default='', comment='部门名称') + order_num = Column(Integer, default=0, comment='显示顺序') + leader = Column(String(20), nullable=True, default=None, comment='负责人') + phone = Column(String(11), nullable=True, default=None, comment='联系电话') + email = Column(String(50), nullable=True, default=None, comment='邮箱') + status = Column(String(1), nullable=True, default=0, comment='部门状态(0正常 1停用)') + del_flag = Column(String(1), nullable=True, default=0, comment='删除标志(0代表存在 2代表删除)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + + +Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/dash-fastapi-backend/entity/dict_entity.py b/dash-fastapi-backend/entity/dict_entity.py new file mode 100644 index 0000000..4e4d995 --- /dev/null +++ b/dash-fastapi-backend/entity/dict_entity.py @@ -0,0 +1,49 @@ +from sqlalchemy import Column, Integer, String, DateTime, UniqueConstraint +from config.database import Base, engine +from datetime import datetime + + +class SysDictType(Base): + """ + 字典类型表 + """ + __tablename__ = 'sys_dict_type' + + dict_id = Column(Integer, primary_key=True, autoincrement=True, comment='字典主键') + dict_name = Column(String(100), nullable=True, default='', comment='字典名称') + dict_type = Column(String(100), nullable=True, default='', comment='字典类型') + status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1停用)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default='', comment='备注') + + __table_args__ = ( + UniqueConstraint('dict_type', name='uq_sys_dict_type_dict_type'), + ) + + +class SysDictData(Base): + """ + 字典数据表 + """ + __tablename__ = 'sys_dict_data' + + dict_code = Column(Integer, primary_key=True, autoincrement=True, comment='字典编码') + dict_sort = Column(Integer, nullable=True, default=0, comment='字典排序') + dict_label = Column(String(100), nullable=True, default='', comment='字典标签') + dict_value = Column(String(100), nullable=True, default='', comment='字典键值') + dict_type = Column(String(100), nullable=True, default='', comment='字典类型') + css_class = Column(String(100), nullable=True, default='', comment='样式属性(其他样式扩展)') + list_class = Column(String(100), nullable=True, default='', comment='表格回显样式') + is_default = Column(String(1), nullable=True, default='N', comment='是否默认(Y是 N否)') + status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1停用)') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default='', comment='备注') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/entity/log_entity.py b/dash-fastapi-backend/entity/log_entity.py new file mode 100644 index 0000000..090e408 --- /dev/null +++ b/dash-fastapi-backend/entity/log_entity.py @@ -0,0 +1,55 @@ +from sqlalchemy import Column, Integer, String, DateTime, Text, BigInteger, Index +from config.database import Base, engine +from datetime import datetime + + +class SysLogininfor(Base): + """ + 系统访问记录 + """ + __tablename__ = 'sys_logininfor' + + info_id = Column(Integer, primary_key=True, autoincrement=True, comment='访问ID') + user_name = Column(String(50, collation='utf8_general_ci'), nullable=True, default='', comment='用户账号') + ipaddr = Column(String(128, collation='utf8_general_ci'), nullable=True, default='', comment='登录IP地址') + login_location = Column(String(255, collation='utf8_general_ci'), nullable=True, default='', comment='登录地点') + browser = Column(String(50, collation='utf8_general_ci'), nullable=True, default='', comment='浏览器类型') + os = Column(String(50, collation='utf8_general_ci'), nullable=True, default='', comment='操作系统') + status = Column(String(1, collation='utf8_general_ci'), nullable=True, default='0', comment='登录状态(0成功 1失败)') + msg = Column(String(255, collation='utf8_general_ci'), nullable=True, default='', comment='提示消息') + login_time = Column(DateTime, nullable=True, default=datetime.now(), comment='访问时间') + + idx_sys_logininfor_s = Index('idx_sys_logininfor_s', status) + idx_sys_logininfor_lt = Index('idx_sys_logininfor_lt', login_time) + + +class SysOperLog(Base): + """ + 操作日志记录 + """ + __tablename__ = 'sys_oper_log' + + oper_id = Column(BigInteger, primary_key=True, autoincrement=True, comment='日志主键') + title = Column(String(50, collation='utf8_general_ci'), nullable=True, default='', comment='模块标题') + business_type = Column(Integer, default=0, comment='业务类型(0其它 1新增 2修改 3删除)') + method = Column(String(100, collation='utf8_general_ci'), nullable=True, default='', comment='方法名称') + request_method = Column(String(10, collation='utf8_general_ci'), nullable=True, default='', comment='请求方式') + operator_type = Column(Integer, default=0, comment='操作类别(0其它 1后台用户 2手机端用户)') + oper_name = Column(String(50, collation='utf8_general_ci'), nullable=True, default='', comment='操作人员') + dept_name = Column(String(50, collation='utf8_general_ci'), nullable=True, default='', comment='部门名称') + oper_url = Column(String(255, collation='utf8_general_ci'), nullable=True, default='', comment='请求URL') + oper_ip = Column(String(128, collation='utf8_general_ci'), nullable=True, default='', comment='主机地址') + oper_location = Column(String(255, collation='utf8_general_ci'), nullable=True, default='', comment='操作地点') + oper_param = Column(String(2000, collation='utf8_general_ci'), nullable=True, default='', comment='请求参数') + json_result = Column(String(2000, collation='utf8_general_ci'), nullable=True, default='', comment='返回参数') + status = Column(Integer, default=0, comment='操作状态(0正常 1异常)') + error_msg = Column(String(2000, collation='utf8_general_ci'), nullable=True, default='', comment='错误消息') + oper_time = Column(DateTime, nullable=True, default=datetime.now(), comment='操作时间') + cost_time = Column(BigInteger, default=0, comment='消耗时间') + + idx_sys_oper_log_bt = Index('idx_sys_oper_log_bt', business_type) + idx_sys_oper_log_s = Index('idx_sys_oper_log_s', status) + idx_sys_oper_log_ot = Index('idx_sys_oper_log_ot', oper_time) + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/entity/menu_entity.py b/dash-fastapi-backend/entity/menu_entity.py new file mode 100644 index 0000000..74438b9 --- /dev/null +++ b/dash-fastapi-backend/entity/menu_entity.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, Integer, String, DateTime +from config.database import Base, engine +from datetime import datetime + + +class SysMenu(Base): + """ + 菜单权限表 + """ + __tablename__ = 'sys_menu' + + menu_id = Column(Integer, primary_key=True, autoincrement=True, comment='菜单ID') + menu_name = Column(String(50), nullable=False, default='', comment='菜单名称') + parent_id = Column(Integer, default=0, comment='父菜单ID') + order_num = Column(Integer, default=0, comment='显示顺序') + path = Column(String(200), nullable=True, default='', comment='路由地址') + component = Column(String(255), nullable=True, default=None, comment='组件路径') + query = Column(String(255), nullable=True, default=None, comment='路由参数') + is_frame = Column(Integer, default=1, comment='是否为外链(0是 1否)') + is_cache = Column(Integer, default=0, comment='是否缓存(0缓存 1不缓存)') + menu_type = Column(String(1), nullable=True, default='', comment='菜单类型(M目录 C菜单 F按钮)') + visible = Column(String(1), nullable=True, default='0', comment='菜单状态(0显示 1隐藏)') + status = Column(String(1), nullable=True, default='0', comment='菜单状态(0正常 1停用)') + perms = Column(String(100), nullable=True, default=None, comment='权限标识') + icon = Column(String(100), nullable=True, default='#', comment='菜单图标') + create_by = Column(String(64), nullable=True, default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), nullable=True, default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default='', comment='备注') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/entity/post_entity.py b/dash-fastapi-backend/entity/post_entity.py new file mode 100644 index 0000000..78a491a --- /dev/null +++ b/dash-fastapi-backend/entity/post_entity.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String, DateTime +from config.database import Base, engine +from datetime import datetime + + +class SysPost(Base): + """ + 岗位信息表 + """ + __tablename__ = 'sys_post' + + post_id = Column(Integer, primary_key=True, autoincrement=True, comment='岗位ID') + post_code = Column(String(64), nullable=False, comment='岗位编码') + post_name = Column(String(50), nullable=False, comment='岗位名称') + post_sort = Column(Integer, nullable=False, comment='显示顺序') + status = Column(String(1), nullable=False, default='0', comment='状态(0正常 1停用)') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default='', comment='备注') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/entity/role_entity.py b/dash-fastapi-backend/entity/role_entity.py new file mode 100644 index 0000000..68a13da --- /dev/null +++ b/dash-fastapi-backend/entity/role_entity.py @@ -0,0 +1,48 @@ +from sqlalchemy import Column, Integer, String, DateTime +from config.database import Base, engine +from datetime import datetime + + +class SysRole(Base): + """ + 角色信息表 + """ + __tablename__ = 'sys_role' + + role_id = Column(Integer, primary_key=True, autoincrement=True, comment='角色ID') + role_name = Column(String(30, collation='utf8_general_ci'), nullable=False, comment='角色名称') + role_key = Column(String(100, collation='utf8_general_ci'), nullable=False, comment='角色权限字符串') + role_sort = Column(Integer, nullable=False, comment='显示顺序') + data_scope = Column(String(1, collation='utf8_general_ci'), default='1', comment='数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)') + menu_check_strictly = Column(Integer, default=1, comment='菜单树选择项是否关联显示') + dept_check_strictly = Column(Integer, default=1, comment='部门树选择项是否关联显示') + status = Column(String(1, collation='utf8_general_ci'), nullable=False, comment='角色状态(0正常 1停用)') + del_flag = Column(String(1, collation='utf8_general_ci'), default='0', comment='删除标志(0代表存在 2代表删除)') + create_by = Column(String(64, collation='utf8_general_ci'), default='', comment='创建者') + create_time = Column(DateTime, default=datetime.now(), comment='创建时间') + update_by = Column(String(64, collation='utf8_general_ci'), default='', comment='更新者') + update_time = Column(DateTime, default=datetime.now(), comment='更新时间') + remark = Column(String(500, collation='utf8_general_ci'), comment='备注') + + +class SysRoleDept(Base): + """ + 角色和部门关联表 + """ + __tablename__ = 'sys_role_dept' + + role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID') + dept_id = Column(Integer, primary_key=True, nullable=False, comment='部门ID') + + +class SysRoleMenu(Base): + """ + 角色和菜单关联表 + """ + __tablename__ = 'sys_role_menu' + + role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID') + menu_id = Column(Integer, primary_key=True, nullable=False, comment='菜单ID') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/entity/user_entity.py b/dash-fastapi-backend/entity/user_entity.py new file mode 100644 index 0000000..d11ef9d --- /dev/null +++ b/dash-fastapi-backend/entity/user_entity.py @@ -0,0 +1,53 @@ +from sqlalchemy import Column, Integer, String, DateTime +from config.database import Base, engine +from datetime import datetime + + +class SysUser(Base): + """ + 用户信息表 + """ + __tablename__ = 'sys_user' + + user_id = Column(Integer, primary_key=True, autoincrement=True, comment='用户ID') + dept_id = Column(Integer, comment='部门ID') + user_name = Column(String(30, collation='utf8_general_ci'), nullable=False, comment='用户账号') + nick_name = Column(String(30, collation='utf8_general_ci'), nullable=False, comment='用户昵称') + user_type = Column(String(2, collation='utf8_general_ci'), default='00', comment='用户类型(00系统用户)') + email = Column(String(50, collation='utf8_general_ci'), default='', comment='用户邮箱') + phonenumber = Column(String(11, collation='utf8_general_ci'), default='', comment='手机号码') + sex = Column(String(1, collation='utf8_general_ci'), default='0', comment='用户性别(0男 1女 2未知)') + avatar = Column(String(100, collation='utf8_general_ci'), default='', comment='头像地址') + password = Column(String(100, collation='utf8_general_ci'), default='', comment='密码') + status = Column(String(1, collation='utf8_general_ci'), default='0', comment='帐号状态(0正常 1停用)') + del_flag = Column(String(1, collation='utf8_general_ci'), default='0', comment='删除标志(0代表存在 2代表删除)') + login_ip = Column(String(128, collation='utf8_general_ci'), default='', comment='最后登录IP') + login_date = Column(DateTime, comment='最后登录时间') + create_by = Column(String(64, collation='utf8_general_ci'), default='', comment='创建者') + create_time = Column(DateTime, comment='创建时间', default=datetime.now()) + update_by = Column(String(64, collation='utf8_general_ci'), default='', comment='更新者') + update_time = Column(DateTime, comment='更新时间', default=datetime.now()) + remark = Column(String(500, collation='utf8_general_ci'), comment='备注') + + +class SysUserRole(Base): + """ + 用户和角色关联表 + """ + __tablename__ = 'sys_user_role' + + user_id = Column(Integer, primary_key=True, nullable=False, comment='用户ID') + role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID') + + +class SysUserPost(Base): + """ + 用户与岗位关联表 + """ + __tablename__ = 'sys_user_post' + + user_id = Column(Integer, primary_key=True, nullable=False, comment='用户ID') + post_id = Column(Integer, primary_key=True, nullable=False, comment='岗位ID') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/mapper/crud/dept_crud.py new file mode 100644 index 0000000..cb36da0 --- /dev/null +++ b/dash-fastapi-backend/mapper/crud/dept_crud.py @@ -0,0 +1,16 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import Session +from entity.dept_entity import SysDept +from utils.time_format_tool import list_format_datetime +from utils.page_tool import get_page_info + + +def get_dept_by_id(db: Session, dept_id: int): + dept_info = db.query(SysDept) \ + .filter(SysDept.dept_id == dept_id, + SysDept.status == 0, + SysDept.del_flag == 0) \ + .first() + + return dept_info + diff --git a/dash-fastapi-backend/mapper/crud/login_crud.py b/dash-fastapi-backend/mapper/crud/login_crud.py new file mode 100644 index 0000000..d27cf11 --- /dev/null +++ b/dash-fastapi-backend/mapper/crud/login_crud.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import Session +from entity.user_entity import SysUser +from utils.time_format_tool import object_format_datetime + + +def login_by_account(db: Session, user_name: str): + """ + 根据用户名查询用户信息 + :param db: orm对象 + :param user_name: 用户名 + :return: 用户对象 + """ + user = db.query(SysUser).\ + filter_by(user_name=user_name).\ + distinct().\ + first() + + return object_format_datetime(user) diff --git a/dash-fastapi-backend/mapper/crud/post_crud.py b/dash-fastapi-backend/mapper/crud/post_crud.py new file mode 100644 index 0000000..7d89edf --- /dev/null +++ b/dash-fastapi-backend/mapper/crud/post_crud.py @@ -0,0 +1,14 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import Session +from entity.post_entity import SysPost +from utils.time_format_tool import list_format_datetime +from utils.page_tool import get_page_info + + +def get_post_by_id(db: Session, post_id: int): + post_info = db.query(SysPost) \ + .filter(SysPost.post_id == post_id, + SysPost.status == 0) \ + .first() + + return post_info diff --git a/dash-fastapi-backend/mapper/crud/role_crud.py b/dash-fastapi-backend/mapper/crud/role_crud.py new file mode 100644 index 0000000..5bc0291 --- /dev/null +++ b/dash-fastapi-backend/mapper/crud/role_crud.py @@ -0,0 +1,15 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import Session +from entity.role_entity import SysRole +from utils.time_format_tool import list_format_datetime +from utils.page_tool import get_page_info + + +def get_role_by_id(db: Session, role_id: int): + role_info = db.query(SysRole) \ + .filter(SysRole.role_id == role_id, + SysRole.status == 0, + SysRole.del_flag == 0) \ + .first() + + return role_info diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/mapper/crud/user_crud.py new file mode 100644 index 0000000..bfd22c2 --- /dev/null +++ b/dash-fastapi-backend/mapper/crud/user_crud.py @@ -0,0 +1,229 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import Session +from entity.user_entity import SysUser, SysUserRole, SysUserPost +from entity.role_entity import SysRole, SysRoleMenu +from entity.dept_entity import SysDept +from entity.post_entity import SysPost +from entity.menu_entity import SysMenu +from mapper.schema.user_schema import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ + UserPageObjectResponse, CrudUserResponse +from utils.time_format_tool import list_format_datetime +from utils.page_tool import get_page_info + + +def get_user_by_name(db: Session, user_name: str): + """ + 根据用户名获取用户信息 + :param db: orm对象 + :param user_name: 用户名 + :return: 当前用户名的用户信息对象 + """ + query_user_info = db.query(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_name == user_name) \ + .distinct().first() + + return query_user_info + + +def get_user_by_id(db: Session, user_id: int): + """ + 根据user_id获取用户信息 + :param db: orm对象 + :param user_id: 用户id + :return: 当前user_id的用户信息对象 + """ + query_user_basic_info = db.query(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .distinct().all() + query_user_dept_info = db.query(SysDept).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ + .distinct().all() + query_user_role_info = db.query(SysRole).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .distinct().all() + query_user_post_info = db.query(SysPost).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserPost, SysUser.user_id == SysUserPost.user_id) \ + .outerjoin(SysPost, and_(SysUserPost.post_id == SysPost.post_id, SysPost.status == 0)) \ + .distinct().all() + query_user_menu_info = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ + .distinct().all() + results = dict( + user_basic_info=list_format_datetime(query_user_basic_info), + user_dept_info=list_format_datetime(query_user_dept_info), + user_role_info=list_format_datetime(query_user_role_info), + user_post_info=list_format_datetime(query_user_post_info), + user_menu_info=list_format_datetime(query_user_menu_info) + ) + + return CurrentUserInfo(**results) + + +def get_user_list(db: Session, page_object: UserPageObject): + """ + 根据查询参数获取用户列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 用户列表信息对象 + """ + offset = (page_object.page_num - 1) * page_object.page_size + user_list = db.query(SysUser) \ + .filter(SysUser.del_flag == 0, + SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, + SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, + SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, + SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, + SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, + SysUser.sex == page_object.sex if page_object.sex else True + ) \ + .offset(offset) \ + .limit(page_object.page_size) \ + .distinct().all() + count = db.query(SysUser) \ + .filter(SysUser.del_flag == 0, + SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, + SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, + SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, + SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, + SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, + SysUser.sex == page_object.sex if page_object.sex else True + ) \ + .distinct().count() + + page_info = get_page_info(offset, page_object.page_num, page_object.page_size, count) + result = dict( + rows=list_format_datetime(user_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return UserPageObjectResponse(**result) + + +def add_user_crud(db: Session, user: UserModel): + """ + 新增用户数据库操作 + :param db: orm对象 + :param user: 用户对象 + :return: 新增校验结果 + """ + is_user = db.query(SysUser).filter(SysUser.user_name == user.user_name, SysUser.del_flag == 0).all() + if is_user: + result = dict(is_success=False, message='用户名已存在') + else: + db_user = SysUser(**user.dict()) + db.add(db_user) + db.commit() # 提交保存到数据库中 + db.refresh(db_user) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudUserResponse(**result) + + +def edit_user_crud(db: Session, user: UserModel): + """ + 编辑用户数据库操作 + :param db: orm对象 + :param user: 用户对象 + :return: 编辑校验结果 + """ + is_user_id = db.query(SysUser).filter(SysUser.user_id == user.user_id, SysUser.del_flag == 0).all() + is_user_name = db.query(SysUser).filter(SysUser.user_name == user.user_name, SysUser.del_flag == 0).all() + if not is_user_id: + result = dict(is_success=False, message='用户不存在') + elif is_user_name: + result = dict(is_success=False, message='用户名已存在,不允许修改') + else: + # 筛选出属性值为不为None和''的 + filtered_dict = {k: v for k, v in user.dict().items() if v is not None and v != ''} + db.query(SysUser)\ + .filter(SysUser.user_id == user.user_id)\ + .update(filtered_dict) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudUserResponse(**result) + + +def delete_user_crud(db: Session, user: UserModel): + """ + 删除用户数据库操作 + :param db: orm对象 + :param user: 用户对象 + :return: + """ + db.query(SysUser) \ + .filter(SysUser.user_id == user.user_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def add_user_role_crud(db: Session, user_role: UserRoleModel): + """ + 新增用户角色关联信息数据库操作 + :param db: orm对象 + :param user_role: 用户角色关联对象 + :return: + """ + db_user_role = SysUserRole(**user_role.dict()) + db.add(db_user_role) + db.commit() # 提交保存到数据库中 + db.refresh(db_user_role) # 刷新 + + +def delete_user_role_crud(db: Session, user_role: UserRoleModel): + """ + 删除用户角色关联信息数据库操作 + :param db: orm对象 + :param user_role: 用户角色关联对象 + :return: + """ + db.query(SysUserRole) \ + .filter(SysUserRole.user_id == user_role.user_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def add_user_post_crud(db: Session, user_post: UserPostModel): + """ + 新增用户岗位关联信息数据库操作 + :param db: orm对象 + :param user_post: 用户岗位关联对象 + :return: + """ + db_user_post = SysUserPost(**user_post.dict()) + db.add(db_user_post) + db.commit() # 提交保存到数据库中 + db.refresh(db_user_post) # 刷新 + + +def delete_user_post_crud(db: Session, user_post: UserPostModel): + """ + 删除用户岗位关联信息数据库操作 + :param db: orm对象 + :param user_post: 用户岗位关联对象 + :return: + """ + db.query(SysUserPost) \ + .filter(SysUserPost.user_id == user_post.user_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def get_user_dept_info(db: Session, dept_id: int): + dept_basic_info = db.query(SysDept) \ + .filter(SysDept.dept_id == dept_id, + SysDept.status == 0, + SysDept.del_flag == 0) \ + .first() + return dept_basic_info diff --git a/dash-fastapi-backend/mapper/schema/login_schema.py b/dash-fastapi-backend/mapper/schema/login_schema.py new file mode 100644 index 0000000..d7b3056 --- /dev/null +++ b/dash-fastapi-backend/mapper/schema/login_schema.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel +from typing import Optional + + +class UserLogin(BaseModel): + user_name: str + password: str + user_request: Optional[str] = None + + +class Token(BaseModel): + token: str + account: str + phone: str + name: str + diff --git a/dash-fastapi-backend/mapper/schema/user_schema.py b/dash-fastapi-backend/mapper/schema/user_schema.py new file mode 100644 index 0000000..d62d632 --- /dev/null +++ b/dash-fastapi-backend/mapper/schema/user_schema.py @@ -0,0 +1,218 @@ +from pydantic import BaseModel +from typing import Union, Optional +from datetime import datetime + + +class TokenData(BaseModel): + """ + token解析结果 + """ + user_id: Union[int, None] = None + + +class UserModel(BaseModel): + """ + 用户表对应pydantic模型 + """ + user_id: Optional[int] + dept_id: Optional[int] + user_name: Optional[str] + nick_name: Optional[str] + user_type: Optional[str] + email: Optional[str] + phonenumber: Optional[str] + sex: Optional[str] + avatar: Optional[str] + password: Optional[str] + status: Optional[str] + del_flag: Optional[str] + login_ip: Optional[str] + login_date: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class UserRoleModel(BaseModel): + """ + 用户和角色关联表对应pydantic模型 + """ + user_id: Optional[int] + role_id: Optional[int] + + class Config: + orm_mode = True + + +class UserPostModel(BaseModel): + """ + 用户与岗位关联表对应pydantic模型 + """ + user_id: Optional[int] + post_id: Optional[int] + + class Config: + orm_mode = True + + +class DeptModel(BaseModel): + """ + 部门表对应pydantic模型 + """ + dept_id: Optional[int] + parent_id: Optional[int] + ancestors: Optional[str] + dept_name: Optional[str] + order_num: Optional[int] + leader: Optional[str] + phone: Optional[str] + email: Optional[str] + status: Optional[str] + del_flag: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + + class Config: + orm_mode = True + + +class RoleModel(BaseModel): + """ + 角色表对应pydantic模型 + """ + role_id: Optional[int] + role_name: Optional[str] + role_key: Optional[str] + role_sort: Optional[int] + data_scope: Optional[str] + menu_check_strictly: Optional[int] + dept_check_strictly: Optional[int] + status: Optional[str] + del_flag: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class PostModel(BaseModel): + """ + 岗位信息表对应pydantic模型 + """ + post_id: Optional[int] + post_code: Optional[str] + post_name: Optional[str] + post_sort: Optional[int] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class CurrentUserInfo(BaseModel): + """ + 数据库返回当前用户信息 + """ + user_basic_info: list[UserModel] + user_dept_info: list[DeptModel] + user_role_info: list[RoleModel] + user_post_info: list[PostModel] + user_menu_info: list + + +class UserDetailModel(BaseModel): + """ + 获取用户详情信息响应模型 + """ + user: UserModel + dept: DeptModel + role: list[RoleModel] + post: list[PostModel] + + +class CurrentUserInfoServiceResponse(UserDetailModel): + """ + 获取当前用户信息响应模型 + """ + menu: list + + +class UserPageObject(UserModel): + """ + 用户管理分页查询模型 + """ + page_num: int + page_size: int + + +class UserPageObjectResponse(BaseModel): + """ + 用户管理列表分页查询返回模型 + """ + rows: list[UserModel] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class AddUserModel(UserModel): + """ + 新增用户模型 + """ + role_id: Optional[str] + post_id: Optional[str] + + +class DeleteUserModel(BaseModel): + """ + 删除用户模型 + """ + user_ids: str + + +class CrudUserResponse(BaseModel): + """ + 操作用户响应模型 + """ + is_success: bool + message: str + + +class DeptInfo(BaseModel): + """ + 查询部门树 + """ + dept_id: int + dept_name: str + ancestors: str + + +class RoleInfo(BaseModel): + """ + 用户角色信息 + """ + role_info: list + + +class MenuList(BaseModel): + """ + 用户菜单信息 + """ + menu_info: list diff --git a/dash-fastapi-backend/service/login_service.py b/dash-fastapi-backend/service/login_service.py new file mode 100644 index 0000000..f8763b5 --- /dev/null +++ b/dash-fastapi-backend/service/login_service.py @@ -0,0 +1,181 @@ +from mapper.schema.user_schema import * +from mapper.crud.login_crud import * +from mapper.crud.user_crud import * +from jose import JWTError, jwt +from passlib.context import CryptContext +from config.env import JwtConfig +from utils.response_tool import * +from datetime import datetime, timedelta +from fastapi import Request + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +async def get_current_user(request: Request, token: str, result_db: Session): + """ + 根据token获取当前用户信息 + :param request: Request对象 + :param token: 用户token + :param result_db: orm对象 + :return: 当前用户信息对象 + """ + if token[:6] != 'Bearer': + return "用户token不合法" + try: + payload = jwt.decode(token[6:], JwtConfig.SECRET_KEY, algorithms=[JwtConfig.ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + return "用户token不合法" + token_data = TokenData(user_id=int(user_id)) + except JWTError: + return "用户token已失效,请重新登录" + user = get_user_by_id(result_db, user_id=token_data.user_id) + if user is None: + return "用户token不合法" + redis_token = await request.app.state.redis.get(f'{user.user_basic_info[0].user_id}_access_token') + redis_session = await request.app.state.redis.get(f'{user.user_basic_info[0].user_id}_session_id') + if token[6:] == redis_token: + await request.app.state.redis.set(f'{user.user_basic_info[0].user_id}_access_token', redis_token, + ex=timedelta(minutes=30)) + await request.app.state.redis.set(f'{user.user_basic_info[0].user_id}_session_id', redis_session, + ex=timedelta(minutes=30)) + # user_dept_info = deal_user_dept_info(result_db, DeptInfo(dept_id=user.user_dept_info[0].dept_id, + # dept_name=user.user_dept_info[0].dept_name, + # ancestors=user.user_dept_info[0].ancestors)) + # user_role_info = deal_user_role_info(RoleInfo(role_info=user.user_role_info)) + user_menu_info = deal_user_menu_info(0, MenuList(menu_info=user.user_menu_info)) + + return CurrentUserInfoServiceResponse( + user=user.user_basic_info[0], + dept=user.user_dept_info[0], + role=user.user_role_info, + post=user.user_post_info, + menu=user_menu_info + ) + else: + return "用户token已失效,请重新登录" + + +def verify_password(plain_password, hashed_password): + """ + 工具方法:校验当前输入的密码与数据库存储的密码是否一致 + :param plain_password: 当前输入的密码 + :param hashed_password: 数据库存储的密码 + :return: 校验结果 + """ + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(input_password): + """ + 工具方法:对当前输入的密码进行加密 + :param input_password: 输入的密码 + :return: 加密成功的密码 + """ + return pwd_context.hash(input_password) + + +def authenticate_user(query_db: Session, user_name: str, input_password: str): + """ + 根据用户名密码校验用户登录 + :param query_db: orm对象 + :param user_name: 用户名 + :param input_password: 用户密码 + :return: 校验结果 + """ + user = login_by_account(query_db, user_name) + if not user: + return '用户不存在' + if not verify_password(input_password, user.password): + return '密码错误' + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + """ + 根据登录信息创建当前用户token + :param data: 登录信息 + :param expires_delta: token有效期 + :return: token + """ + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, JwtConfig.SECRET_KEY, algorithm=JwtConfig.ALGORITHM) + return encoded_jwt + + +def deal_user_dept_info(db: Session, dept_info: DeptInfo): + tmp_dept_name = dept_info.dept_name + dept_ancestors = dept_info.ancestors.split(',') + tmp_dept_list = [] + for item in dept_ancestors: + dept_obj = get_user_dept_info(db, int(item)) + if dept_obj: + tmp_dept_list.append(dept_obj.dept_name) + tmp_dept_list.append(tmp_dept_name) + user_dept_info = '/'.join(tmp_dept_list) + + return user_dept_info + + +def deal_user_role_info(role_info: RoleInfo): + tmp_user_role_info = [] + for item in role_info.role_info: + tmp_user_role_info.append(item.role_name) + user_role_info = '/'.join(tmp_user_role_info) + + return user_role_info + + +def deal_user_menu_info(pid: int, permission_list: MenuList): + """ + 工具方法:根据菜单信息生成树形嵌套数据 + :param pid: 菜单id + :param permission_list: 菜单列表信息 + :return: 菜单树形嵌套数据 + """ + menu_list = [] + for permission in permission_list.menu_info: + if permission.parent_id == pid: + children = deal_user_menu_info(permission.menu_id, permission_list) + antd_menu_list_data = {} + if children and permission.menu_type == 'M': + antd_menu_list_data['component'] = 'SubMenu' + antd_menu_list_data['props'] = { + 'key': str(permission.menu_id), + 'title': permission.menu_name, + 'icon': permission.icon + } + antd_menu_list_data['children'] = children + elif children and permission.menu_type == 'C': + antd_menu_list_data['component'] = 'Item' + antd_menu_list_data['props'] = { + 'key': str(permission.menu_id), + 'title': permission.menu_name, + 'icon': permission.icon, + 'href': permission.path, + 'modules': permission.component + } + antd_menu_list_data['button'] = children + elif permission.menu_type == 'F': + antd_menu_list_data['component'] = 'Button' + antd_menu_list_data['props'] = { + 'key': str(permission.menu_id), + 'title': permission.menu_name, + 'icon': permission.icon + } + else: + antd_menu_list_data['component'] = 'Item' + antd_menu_list_data['props'] = { + 'key': str(permission.menu_id), + 'title': permission.menu_name, + 'icon': permission.icon, + 'href': permission.path, + } + menu_list.append(antd_menu_list_data) + + return menu_list diff --git a/dash-fastapi-backend/service/user_service.py b/dash-fastapi-backend/service/user_service.py new file mode 100644 index 0000000..b2ffb54 --- /dev/null +++ b/dash-fastapi-backend/service/user_service.py @@ -0,0 +1,103 @@ +from mapper.schema.user_schema import * +from mapper.crud.user_crud import * + + +def get_user_list_services(result_db: Session, page_object: UserPageObject): + """ + 获取用户列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 用户列表信息对象 + """ + user_list_result = get_user_list(result_db, page_object) + + return user_list_result + + +def add_user_services(result_db: Session, page_object: AddUserModel): + """ + 新增用户信息service + :param result_db: orm对象 + :param page_object: 新增用户对象 + :return: 新增用户校验结果 + """ + add_user = UserModel(**page_object.dict()) + add_user_result = add_user_crud(result_db, add_user) + if add_user_result.is_success: + user_id = get_user_by_name(result_db, page_object.user_name).user_id + if page_object.role_id: + role_id_list = page_object.role_id.split(',') + for role in role_id_list: + role_dict = dict(user_id=user_id, role_id=role) + add_user_role_crud(result_db, UserRoleModel(**role_dict)) + if page_object.post_id: + post_id_list = page_object.post_id.split(',') + for post in post_id_list: + post_dict = dict(user_id=user_id, post_id=post) + add_user_post_crud(result_db, UserPostModel(**post_dict)) + + return add_user_result + + +def edit_user_services(result_db: Session, page_object: AddUserModel): + """ + 编辑用户信息service + :param result_db: orm对象 + :param page_object: 编辑用户对象 + :return: 编辑用户校验结果 + """ + edit_user = UserModel(**page_object.dict()) + edit_user_result = edit_user_crud(result_db, edit_user) + if edit_user_result.is_success: + user_id_dict = dict(user_id=page_object.user_id) + delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) + delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) + if page_object.role_id: + role_id_list = page_object.role_id.split(',') + for role in role_id_list: + role_dict = dict(user_id=page_object.user_id, role_id=role) + add_user_role_crud(result_db, UserRoleModel(**role_dict)) + if page_object.post_id: + post_id_list = page_object.post_id.split(',') + for post in post_id_list: + post_dict = dict(user_id=page_object.user_id, post_id=post) + add_user_post_crud(result_db, UserPostModel(**post_dict)) + + return edit_user_result + + +def delete_user_services(result_db: Session, page_object: DeleteUserModel): + """ + 删除用户信息service + :param result_db: orm对象 + :param page_object: 删除用户对象 + :return: 删除用户校验结果 + """ + if page_object.user_ids.split(','): + user_id_list = page_object.user_ids.split(',') + for user_id in user_id_list: + user_id_dict = dict(user_id=user_id) + delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) + delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) + delete_user_crud(result_db, UserModel(**user_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入用户id为空') + return CrudUserResponse(**result) + + +def detail_user_services(result_db: Session, user_id: int): + """ + 获取用户列表信息service + :param result_db: orm对象 + :param user_id: 用户id + :return: 用户id对应的信息 + """ + user = get_user_by_id(result_db, user_id=user_id) + + return UserDetailModel( + user=user.user_basic_info[0], + dept=user.user_dept_info[0], + role=user.user_role_info, + post=user.user_post_info + ) diff --git a/dash-fastapi-backend/utils/log_tool.py b/dash-fastapi-backend/utils/log_tool.py new file mode 100644 index 0000000..e904653 --- /dev/null +++ b/dash-fastapi-backend/utils/log_tool.py @@ -0,0 +1,11 @@ +import os +import time +from loguru import logger + +log_path = os.path.join(os.getcwd(), 'logs') +if not os.path.exists(log_path): + os.mkdir(log_path) + +log_path_error = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_error.log') + +logger.add(log_path_error, rotation="50MB", encoding="utf-8", enqueue=True, compression="zip") diff --git a/dash-fastapi-backend/utils/page_tool.py b/dash-fastapi-backend/utils/page_tool.py new file mode 100644 index 0000000..0c93584 --- /dev/null +++ b/dash-fastapi-backend/utils/page_tool.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel + + +class PageModel(BaseModel): + """ + 分页模型 + """ + page_num: int + page_size: int + total: int + has_next: bool + + +def get_page_info(offset: int, page_num: int, page_size: int, count: int): + """ + 根据分页参数获取分页信息 + :param offset: 起始数据位置 + :param page_num: 当前页码 + :param page_size: 当前页面数据量 + :param count: 数据总数 + :return: 分页信息对象 + """ + has_next = False + res_page_num = 1 + if (offset + page_size) < count: + has_next = True + else: + if page_num > 1: + res_page_num = page_num - 1 + + result = dict(page_num=res_page_num, page_size=page_size, total=count, has_next=has_next) + + return PageModel(**result) diff --git a/dash-fastapi-backend/utils/response_tool.py b/dash-fastapi-backend/utils/response_tool.py new file mode 100644 index 0000000..54fd53f --- /dev/null +++ b/dash-fastapi-backend/utils/response_tool.py @@ -0,0 +1,65 @@ +from fastapi import status +from fastapi.responses import JSONResponse, Response +from fastapi.encoders import jsonable_encoder +from typing import Union +from datetime import datetime + + +def response_200(*, data: Union[list, dict, str], message="获取成功") -> Response: + return JSONResponse( + status_code=status.HTTP_200_OK, + content=jsonable_encoder( + { + 'code': 200, + 'message': message, + 'data': data, + 'success': 'true', + 'time': datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + ) + ) + + +def response_400(*, data: str = None, message: str = "获取失败") -> Response: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=( + { + 'code': 400, + 'message': message, + 'data': data, + 'success': 'false', + 'time': datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + ) + ) + + +def response_401(*, data: str = None, message: str = "获取失败") -> Response: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content=( + { + 'code': 401, + 'message': message, + 'data': data, + 'success': 'false', + 'time': datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + ) + ) + + +def response_500(*, data: str = None, message: str = "接口异常") -> Response: + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=( + { + 'code': 500, + 'message': message, + 'data': data, + 'success': 'false', + 'time': datetime.now().strftime("%Y-%m-%d %H:%M:%S") + } + ) + ) diff --git a/dash-fastapi-backend/utils/time_format_tool.py b/dash-fastapi-backend/utils/time_format_tool.py new file mode 100644 index 0000000..a994b85 --- /dev/null +++ b/dash-fastapi-backend/utils/time_format_tool.py @@ -0,0 +1,26 @@ +import datetime + + +def object_format_datetime(obj): + """ + :param obj: 输入一个对象 + :return:对目标对象所有datetime类型的属性格式化 + """ + for attr in dir(obj): + value = getattr(obj, attr) + if isinstance(value, datetime.datetime): + setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S')) + return obj + + +def list_format_datetime(lst): + """ + :param lst: 输入一个嵌套对象的列表 + :return: 对目标列表中所有对象的datetime类型的属性格式化 + """ + for obj in lst: + for attr in dir(obj): + value = getattr(obj, attr) + if isinstance(value, datetime.datetime): + setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S')) + return lst diff --git a/dash-fastapi-frontend/api/login.py b/dash-fastapi-frontend/api/login.py new file mode 100644 index 0000000..1752f39 --- /dev/null +++ b/dash-fastapi-frontend/api/login.py @@ -0,0 +1,11 @@ +from utils.request import api_request + + +def login_api(page_obj: dict): + + return api_request(method='post', url='/login/loginByAccount', is_headers=False, json=page_obj) + + +def get_current_user_info_api(): + + return api_request(method='post', url='/login/getLoginUserInfo', is_headers=True) diff --git a/dash-fastapi-frontend/api/message.py b/dash-fastapi-frontend/api/message.py new file mode 100644 index 0000000..72b80d9 --- /dev/null +++ b/dash-fastapi-frontend/api/message.py @@ -0,0 +1,6 @@ +from utils.request import api_request + + +def send_message_api(page_obj: dict): + + return api_request(method='post', url='/login/loginByAccount', is_headers=False, json=page_obj) \ No newline at end of file diff --git a/dash-fastapi-frontend/api/user.py b/dash-fastapi-frontend/api/user.py new file mode 100644 index 0000000..181eca7 --- /dev/null +++ b/dash-fastapi-frontend/api/user.py @@ -0,0 +1,31 @@ +from utils.request import api_request + + +def change_password_api(page_obj: dict): + + return api_request(method='post', url='/login/loginByAccount', is_headers=False, json=page_obj) + + +def get_user_list_api(page_obj: dict): + + return api_request(method='post', url='/system/user/get', is_headers=True, json=page_obj) + + +def add_user_api(page_obj: dict): + + return api_request(method='post', url='/system/user/add', is_headers=True, json=page_obj) + + +def edit_user_api(page_obj: dict): + + return api_request(method='post', url='/system/user/edit', is_headers=True, json=page_obj) + + +def delete_user_api(page_obj: dict): + + return api_request(method='post', url='/system/user/delete', is_headers=True, json=page_obj) + + +def get_user_detail_api(user_id: int): + + return api_request(method='get', url=f'/system/user/{user_id}', is_headers=True) diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py new file mode 100644 index 0000000..2f2b906 --- /dev/null +++ b/dash-fastapi-frontend/app.py @@ -0,0 +1,217 @@ +import dash +import time +from dash import html, dcc +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc +from flask import session, request, json + +from server import app, logger, server +from config.global_config import RouterConfig +from store.store import render_store_container + +# 载入子页面 +import views + +from callbacks import app_c +from api.login import get_current_user_info_api +from utils.tree_tool import find_node_values, find_key_by_href + +app.layout = html.Div( + [ + # 注入url监听 + fuc.FefferyLocation(id='url-container'), + # 用于回调pathname信息 + dcc.Location(id='dcc-url', refresh=False), + + # 注入js执行容器 + fuc.FefferyExecuteJs( + id='execute-js-container' + ), + + # 注入页面内容挂载点 + html.Div(id='app-mount'), + + # 辅助处理多输入 -> 存储接口返回token校验信息 + render_store_container(), + + # 重定向容器 + html.Div(id='redirect-container'), + + # 登录消息失效对话框提示 + fac.AntdModal( + html.Div( + [ + fac.AntdIcon(icon='fc-high-priority', style={'font-size': '28px'}), + fac.AntdText('用户信息已过期,请重新登录', style={'margin-left': '5px'}), + ] + ), + id='token-invalid-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + + # 注入全局消息提示容器 + html.Div(id='global-message-container') + ] +) + + +@app.callback( + [Output('app-mount', 'children'), + Output('redirect-container', 'children', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('current-key-container', 'data')], + Input('url-container', 'pathname'), + State('url-container', 'trigger'), + prevent_initial_call=True +) +def router(pathname, trigger): + # 检查当前会话是否已经登录 + token_result = session.get('token') + # 若已登录 + if token_result: + try: + current_user_result = get_current_user_info_api() + if current_user_result['code'] == 200: + current_user = current_user_result['data'] + user_name = current_user['user']['user_name'] + nick_name = current_user['user']['nick_name'] + phone_number = current_user['user']['phonenumber'] + menu_info = current_user['menu'] + session['user_info'] = current_user['user'] + session['dept_info'] = current_user['dept'] + session['role_info'] = current_user['role'] + session['post_info'] = current_user['post'] + session['menu_info'] = menu_info + valid_href_list = find_node_values(menu_info, 'href') + valid_href_list.append('/') + if pathname in valid_href_list: + current_key = find_key_by_href(menu_info, pathname) + if trigger == 'load': + + # 根据pathname控制渲染行为 + if pathname == '/': + current_key = '首页' + if pathname == '/login' or pathname == '/forget': + # 重定向到主页面 + return [ + dash.no_update, + dcc.Location( + pathname='/', + id='router-redirect' + ), + None, + {'timestamp': time.time()} + ] + + # 否则正常渲染主页面 + return [ + views.layout.index.render_content(user_name, nick_name, phone_number, menu_info), + None, + fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), + {'timestamp': time.time()}, + {'current_key': current_key} + ] + + # elif trigger == 'pushstate': + else: + if pathname == '/': + current_key = '首页' + return [ + dash.no_update, + None, + None, + {'timestamp': time.time()}, + {'current_key': current_key} + ] + + # else: + # + # return [ + # dash.no_update, + # dash.no_update, + # dash.no_update, + # {'timestamp': time.time()}, + # dash.no_update + # ] + + else: + # 渲染404状态页 + return [ + views.page_404.render_content(), + None, + None, + {'timestamp': time.time()}, + dash.no_update + ] + + else: + return [ + dash.no_update, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + dash.no_update + ] + + except Exception as e: + print(e) + + return [ + dash.no_update, + None, + fuc.FefferyFancyNotification('接口异常', type='error', autoClose=2000), + {'timestamp': time.time()}, + dash.no_update + ] + else: + # 若未登录 + # 根据pathname控制渲染行为 + # 检验pathname合法性 + if pathname not in RouterConfig.BASIC_VALID_PATHNAME: + # 渲染404状态页 + return [ + views.page_404.render_content(), + None, + None, + {'timestamp': time.time()}, + dash.no_update + ] + + if pathname == '/login': + return [ + views.login.render_content(), + None, + None, + {'timestamp': time.time()}, + dash.no_update + ] + + if pathname == '/forget': + return [ + views.forget.render_forget_content(), + None, + None, + {'timestamp': time.time()}, + dash.no_update + ] + + # 否则重定向到登录页 + return [ + dash.no_update, + dcc.Location( + pathname='/login', + id='router-redirect' + ), + None, + {'timestamp': time.time()}, + dash.no_update + ] + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8088, debug=True) diff --git a/dash-fastapi-frontend/assets/css/global.css b/dash-fastapi-frontend/assets/css/global.css new file mode 100644 index 0000000..1dc1aa3 --- /dev/null +++ b/dash-fastapi-frontend/assets/css/global.css @@ -0,0 +1,42 @@ +._dash-loading { + color: transparent; + position: fixed; + width: calc(544px / 2.5); + height: calc(408px / 2.5); + top: 50vh; + left: 50vw; + transform: translate(-50%, -50%); + background-image: url("/assets/imgs/加载动画.webp"); + background-repeat: no-repeat; + background-size: 100% 100%; +} + +._dash-loading::after { + content: ''; +} + +#login-form-container .ant-card-head-wrapper { + text-align: center; +} + +#login-form-container .ant-card-head-title { + font-size: 26px !important; + font-weight: bold; +} + +/*滚动条自定义样式*/ +*::-webkit-scrollbar-thumb { + background-color: #bfbfbf; + outline: none; + border-radius: 6px; +} + +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +/*火狐浏览器*/ +* { + scrollbar-width: thin; +} \ No newline at end of file diff --git a/dash-fastapi-frontend/assets/imgs/login-background.jpg b/dash-fastapi-frontend/assets/imgs/login-background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a89eb8291d5cb7d9f37ec4f275deab911c9e28e GIT binary patch literal 521275 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¥}>uauIoY!`8n(7niSl_m2d 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

LZs5vs<4*dn)6_orH;Y;`bzi*q=#N#x6 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

8A%Us3k< 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𝔫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

&!CIz1Ns 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⩛TtXwH2N8q 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

Wf@+FD?DGvA4he?g#aN51zy(O$Xy! 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

Acrih5KDW9ugPSk3y-@mXy^-HnY=Jp77llly?k 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

(adheF4D3id^%>F3ZL4&8R0-6CS$2i=& zb>rAL{J?_@YaT_DK)>^dt_KG(rfddm9MHv0A@i17o$Xy&+rzyIu8C6*^UaG*Zx=r) za=AASt9sBR`_$!G8xIoD5VJL!cN4$%Qv4{(in}**zXzdAJJ6T?yZwvaC(;o8No|DU zmNjiQgp_hx9S04-aQA3TUd_i@=;#b6NF8(qS_e*?a*KlbmHFx@S&Y5ZTs)9yeGK<_ zhdo2qu4|SZYvFMcX6$J(&ng(=7HRBC+i$Cp_QuZXORSfszKJGt0}bsC5VU+<1dYhaaUCr-|K z$D9rrP17`m6IXSbN9sqA3&CD|K4m z68sBBr~S`03@(M2-^q;sU;wyip5j`oHvB|HpD=&}Z@xzIxf74^IBza(EMuetF)m0l z{7^nXzWsZAvWUbwL;@^f^dGw|{0Dr+8!(0K{269~|6|o=Akl_o@2lR1KOFym2~TjO zuFL+=%UDauL}Ej-)-${)#d}Vq*R1=O9}jb z-(>{AjvF+TDWy-8%BW8-ELMTk$D?LTupLY5?^8e^X&iG67P|565BTOLPE>IJ;PCce z;Pv1W@L5~z6jQ$U<2S93J>avmbr7pJHi)>KWhhC1AGJ-vwtyx)z%RHcuX6|pg^;G$UbY46| zq-PcS+^N{ClS^d6gMk87a$<9h17bJQ7E^Hl{!AN1to)Ky)kvP^c2|(ZP)i9_OhL&c z@7udqegnw{BK)B*+OI}1`(o1c+b*4nDKUNGlza5UsmF&-8quFU3>Pu-FT}Mtsp5q& zm$RQI!}|JR+B;N6UmIL3j#gG??zZ;>UNehBdmOkZdf%o4NzUiNUKy#&PG%{;9_{1P zW%LX_2V`C-?7X#Wcjo>~SIk40#QYygAZZZ^AA9L5J)%xOr!g8ZjX^{@?#&5c(9LL; zr2Hp9$I8s88#VNx&;V6Ff)|uoy1OLMA(Dcq02}~LqC&fcISFM zD{eqIJNuoL8cHlksLtUOFw#ab>M1pibu9Too$@05!9F1pdf2Za^7`wKUBz0STCF&6 zI-RvhAmCh<@Y|VF$fV3hL~Oc~EB16&jSt0_ljh9T*1PF9_sWRUg;+|aw~=~s(`M?H z30KZ0*`{CUvtKdV?PQO~pFW|L9Rer6)GhNi-&tXFKBIzJez-@Jq8Jb~`{U!IS5+Gj z0S2kmwkcO~?|dqxRx>hoEyB=t;wE`gI{Dhbpo4Za?v%B4qZ z=1=8SUUqmAzj$dW+ZU~Zg4h~4ceEQ+J=cQN7$K_)YRm8EhIN6Fe~OFCS@Iq0;&=^@ z2m8T+8SP*h&m5walnXhO*;DI$HtepFY2|w@bdZmljQ9*upk$cggcztH`ijM+cZB)J zLp8hR2eWm!bVcdqSeC-;HY`P9O9}@ejr>X~WEp5vMH1x#jeGR0129a40qwLyYVkCx1AHISUNba*4W`Xw2V66Tb4}lwc0a%@L|6ZN{=Z3P8lEC*W zp8eNKFC#P4@H!R-e6P4N-U?YiGWV5CP>tl(e0pJ_@3|^~XVzMCuf_q(Xe>P04v0~# z&OrxeH}`V?_`|lrp^*o$F+hyUeB3!4Jv9x8QS8}!b6rzUmw|xPuPi`}di4Rc_NM#Y zXHeyq|3ro*?oolJ4I`lX6r{~W)Ad({boSMumr{a9Uy zp(RJjm6Al*+*tKtSy#UD>% zGL9F{3FHD@O`us@2%!|%4pE>pk*-i703$IzBx!30*Pd&kRG_rMqG66Id+lHn9j!nx z788Zg39DtiG!=sAMI8<_ER9^?#|yvRA3#t*C9dGqXFrSRwM#uI!`ku6?b98P4j?6W zA8m+=GKKTmAXBm+7=IL=ji|pkl7*D-cb)UPWx8NuE%VTI<794kUwJ@tUk%#kF5tDo zw9GmD9)jI0xKZ8abekN?3Hv^1HOkg7eK^+t|>59o&~OH(f?7nX#dTv{P>V2 zadNh6Up2a>ivVT)L}M^>INN3DGQr^Eh6>0^V61Gfjz@+@1FyZ4-eecumJT4}RMiR6 zJ=xX$xHpu5crf#EKR;6Sugq?37H#scW3?YK6b4i7qu*DAx(yHv}Z9%A2?O5eQ5?{owCx| zKFLf8WwW4+0&ZfP!mM7kl1s2WL`qZ?*cB?y64|$gb;9hxDJcb3w^q4{Tk%1hh5=C` z+yVhVV$&s|CUJDmM(&TWi!=0OrAQxwJcaub7d~;f&=3(M<45yP`8*_s=jyN!rg^TB z6p@4DJ(0VXT<5>Xr7Um)OrL)#V~T7QJu|>DatP;X4(a&JdB-8ZYgcO+W(&G-v-GuwR0N6;W0Q3KjiD9lIII2;P;@R`_HCAj$a8Qd zi9JPT8q~d6YM|XsQS&WsS;+G<1i45#Bcy5QuWt+9*O(Ypus?^sYgyct2~lY~0tv3| zl-_$GG(*gcLc+R*?10!MHc;Y-}i2J$kUN^Povmm)Q13 z3{+Z2)s>WLe0Xl7SMU}xH^gjwAJ&Zz?;?s_2=Emft;@vt_XT@a!TW~w&#@kw)2diP z|Hy8>=$7?Y1!aZ}i229EoO+#y)R~ivisL7JuW0Q%k+^VWe39y##t!}b$%f)}+auW!6Ip;@*QDG4~w1et?Stc@ z2yZTpYTI!gilnYL4iOJTlZ%7#yjfd%B;BLgrLL)ptFpVZL5&P4J$48A-Hms${ruX~ zcEmzO@8qYN7b56QzsBF6T$tU0oxH9yPLxhb$taEgN;VE^~@)vqZDRf6>{- zy@b#_qe5rs6*s;H?>VY;E!nSWLW<}8>lI9P^Bb<#^=0+Fe6Y0M1>$$Za**WvSG{q@ z9wRb$L#!Wc$>4Twi#TIf8%LafZWQ_9U5Znzw$X#A%-6|7H$ww;3Y)^W5^J7K>IU1BO>9*qKR;n#`?_tMc6~FltB)dT0_lVI%mTRW)RdPDdR2@2 zODujUx)6_G3N*k;p#|2|s)bP>ym&{Mg95O@^=9N7qd?Kgb)o*>nbix<6#O0&z#k|6 zQ%yLk+e4+|oNYKsCxkpLJFG4%$)aWsm$Da`)QyJp=)TIiG}0=XuGDdukk=Bs4P|2cV_3$oO*VolHTI-SWc#Ar?N-8X5 ziQ7Yn&}rCfn2o48yt9z6YG-qUnZfr8KT|n*)N!Cb8FzzuH-i!)b?A&zJ++aR!lAaO zRqfX27xML39oAr<*LQ6?lSrL`?-#1grT+TpNiMPB4y0*u=bUB zKVwa}BIzCeQ`=eZzT}*oGeG98gFF})F%(+=DbDW>HcR1m)(CrD?QPe`t~U5^N{@wq zoW+h-&AW||I*QgFm6Jn^Cxi{(fuh)!_y1p6-{Q|?|Nnp4=CEOGB-oeHBiy850vnr{+xGzywmeRK-r!-?voI&L^iRj}t(AMo2*y$@VJpK^ z{&zUdPx6%KY}x^1!~Ru<0ArQ`7JNki7#05#vq?-KHT53`P>{ps<>#(+1`E0QWu+?@ zXjWMrP}A7d%&)I+X=`t~aH-?+m7+@k+;ye*X*HRkSeBKG>ItFLV0OZuj)S7+Ih4R+(+QW7^6lF) z4bacD7v|HHindyvYZ9!J$SSFXK|I5HfSV!}mAOE_KHF5ZiSAU?v^mNfisG5|2lq&# z+pKThxQk}@azw54pnTRle-Q#*Gg^&3TWk+XECzO;@?*X3S2{Ax zZKv&wK~1q)9~;-M?T6po+P!4GBvDwlZ#qNKAWu88a&5IYwW$cU`{~9x8`>~^1G3L` zJ#o;rsdCQRjVF8d!jI%n-Sk?q<_^M~1Q85=k zAm`Z54$w3V5&~+Q&{#O~c3*%*-W{*EVmjU-(b!D21Np($7jt=GYi%W1xS){<V>V;Ab)%OE1lZ;}Hs=2j*RzW|fxj=>+`_ZH}nw3fjnq6ZqvQ7g>IMA`3XKE^v zqZbwpFMroGCim{~T9mpFhViX^28*+A%o8gmUV_d+|glflitm*MzNB6cBZXgA8Wbv@!gW- zfK@wAHo0p&sHXKkw_a>$c)8#8t=L>z1#w;HQW9bynG+>9xPIMjF3Dm|Ps5OMFF2g0 zpG_GYOrB4ojdHcEFz@e-Ivzp^T~bZ}rw{}r;p`!@q*QA&SdQ9u>p zpVN&AzeYLh-zewgmjNr&jWx05Wwl_GC)bz%jPm-QQGTJj3d1mp(CICdz^S%JtkDHS zMHm7UYGo8WF1Vz20L8=|Yj`BkH@f#Sh=_Qee#z9b3xG3DrDQ0RZ*l5 zcjZi@t*1tx8keC%aS}Jc85U}HMbY99OI$S61?koW%f)e?*^c(!k>+6&HE33~B+ugl zhwwpvCQnzMCRKn=9x=OTeewO*2Aue<%|5A&cT!4;wPuj>r*x+GhNB$p{bI_Ju#QR` zfdmzMkg((!Y1p-{#0cIRwoh;JltZ!f>ApMtQ7T^Z2%YDO&3Dgg@9~mWhAo3vd7qud zIKnwNu?NmJM<@L-<5L;|7{q}*o8e{GD2|pyHA3Q$_nHo*WMac&Qj_;$p@Q`BslxCz z9O?+)ZEZ7<89vq#jg=(v*gC|h!Tm(g3<-{8xX(!BkBv`(8^Jrf5kke(B%>s~7|YPt z^Q?A(th=GtS5X{ixKN=HS%Md_-88F+SsyHD<>!a(Br@6g=Eb-gFaA*@SLWD4Dqq}r z)L1QlyO%c*tZJyh&Bjuzt&uuD#oGlml`Qy zJu-9y<_6Td^)t`Ll6=k_O&TYSMB5~-4@<94NB%d135uVt#OA*YCj`7TvqDWwFuyXK z5I7wPg$4!xO}QkKBXaYg1f3PirC!Ies+B@+1WQ;+!A7mERuS+^ef^~fVo-NC(gd8F zPgC%&yxxi=j^2L&QSzF&(@NkzYyh6mKg3Y1s+iMN6s6g^`410*39alUD~7_d!=#q_ zF_!h|#3@CywZ_F|G0G*EKhKi1H^ygRkHD5{?Ke=TU6RD=!B3qI!WUc9Ra3MForqG@ zrKxGjt*sLGCdjeIMbrw_eVXgs zZweTsQzKWE4p5EJF?h1^?*l)yJq^={N9Zlem@dx;m5JNx=VT1c#_Z9lO4@H`91lxm z6UCrAui%4=`Sj2?o%jsBXZyqU!*)~!YTI&IglV-&#@DmdsTg2o0*86 zJ`j~;K`M}Ywn|-volGruiq<$n+NV1ra6mv;F58K`Fl_9(^vW>3IEX;*0L;{(bh}YV zm)t?TUy{v1JOVad`~z$0;tWAQ(6(@aY?IpzUu~(aK~vncF&F^(0$6~)0?e-rkKI{( z2yYNc-Ul;?^en=n&rv;c4m+}R;>c}rN=1NSlS!CT%@~<=|nMgq*DVTp~OhGQR63S5c zNn@5HtE!N}D>21V$Z2V9Wvs+hF`Q`BEpELC##Az5(CWsW>fX`&x5$oS=G{uGwguqnigQ6bYmps*}bNje&!wTBsU+v5Tehwt9Q7^pOIac#(3{z8y0jWSN_1&#qGj= zxgx0;)4x0+W5a4Hyp0R$+p0#KJPZKdk;i!Vg((1;Cy&F{8ZG;9)Eb0Jh zop)_(DD5USBGOb%dtDIhoYJCF<(<{IC@(? z?nst2Zhu;Z)UZBC7 zzMr|q3H>wyXx4CoZNaAXY+?G?I7*2^!hj;zFF8SYMC6%5In;biA2i8ROd?AwrzM*o zK0?XP!uwy*&0t$mg)YJ-9J)`UTrVOj6;7d+sg@8hvq~1?t4YNo;3|aph}M z-H?&j2GWawiTU!R^BTP`>`TcIS!I7?(wo=f;D1Z6U?_xvMVWK|aF~CG0zv^p_oA4P ze5wD%pDKh`*PY&ess8gD zK;dWAkuEe$5zc?Bh3%a4V@l#Pl!r@uU+-@`t-MZ?FF4N3VN2cxYm+$#J6EM=ybW{m zDy0E9li`!ab%&hNiDyL$Sy#(lQ&bFxGA3?6-c%~n|0gvO^Fjt>i z2(R&dGjC_d(nITxO}_px<;b%{MR_3XeLsaU)L*j(VHF>KJLub^KA_rn_5q62uB^T_ z!7&~F*h9x$tg8aQJ^VnZ?lHwzmwB;nV&uv50Jl!%6YHl8sXH+K;e?KxFE|%%cFR3t%@)8E- zR9A|hq{kvPS+H1TTQ@0H<=gZ4)R`GuSZ3Cc9^6;i%f>t~QZp#^kNsx`<>aN))ZZ zhE8hU_2^#rlf^|D)G4Msnd1u=cB+mTn*W(V8~kJOcnh?nEo4ekI9=w;IU{G6I||)0 zlIJ4jppE^HiU8vw5~Ly`{wY$f#6v7x0ZfHBaQ43)E?KF;MpjU;<+*@BPX<-|$}7@< zY8?WUT~ve&uLNBh*wr1?M5~m>BB!>V-mK26XL#KOlD?b6u~)AR4nc3+y4y3_IgmO& zac}D4{f>uM#_vqO5IwD)o$H=oc$dBiylxnJ`{&ox#gDA#pWv)#YrH_6@si}Pvda5= z;-wWWqXc0kq9|3PBL^+y4qlB>ThlZ4ar+~S{kWJ+m_~ADpHGYTJFV=h-S$2uPJ{Wo zJsu46t#*C;YxD8S?Y12$hWmzEt8Q0#mah20RsYQx?lC6s4tswEEnn>#Qg;@|O*X(&M`miesw)Oh})+X3>uX3en|NZx6a)u5&qI#wy2x^uQ z>HOlWZ3L7rYb*YEKKkTrHhMAaI2;~vZrljtVftw2^SfF5qn@mOy!`2X=Kgjk4>&UF z%qx?9YYr?7{N;M~VTJp-H*cL0=pS@p+z~ZTo+MV;i8RduH)mTiY>)Hfkk(3`A7Nhx zfg`;$W4n08@Op-8a6AkkYq&RK;wvMU^a%KNf~K^zoIT4uqmMM0bdjPgN#^sEa~l{M z3Oi(Y>2ezIvQ1aqIEor?B$)4ag3Rd$2m@??^BF#T zI3y*E?3TytdO^*vz=tslSZYUX)VD-dJvWK6_VPWI{){+julm`!-rshir;6~48Z+anK{K+4{1igL(HCg_4t?ll+%dSrwrf`7~x-Yc+=3*rA zkmH1@63+u^KFacszZAO`cO=P#j?(A0Yw7SbJql`khTUW{zPl4h5E@1)$?SPS?no29 zA#?MoJqsi0^vx=^FABVPBL=+;8e3P>ANEAZro=hktelF4lL^|(KKH_nZqySOYt5e8 zEWo+PE9O*_Cf*KgrkVGbR&F*pk>i(_#Wa<1IQ8O~nL`Mqq*}G; zke=H+Pn2M_!&}Oia5`CGZ$*YqhV5&K0qw4}D>HIC3NLu_xM1wd=N2?aeJj}x@mDe) zI|Y{7e2VqwZ4W;=U4~bdI?s1l0;0Kl^&EX1fK!eD`p+D8=>NJyCj!O~*5V_a49Uth6wCV)mQuI6K8qoNal2 zLVm|=N-4?HxiFnmY*%!CIOlP3uo_o8FPSvN$WJlk%yHS(F_ri%?hAwR1dET10v1;& zM$T+Ew5gF1wc`L^<=>p3e}0Qt(C;lX0}qQz%T==0-fq9&3*rY=Je0Ave|7YK^{Sr$KL8y458$yZ2mpF({9jJY zO3315-B5C;> z1ODk^%)1YDPdd?)5EtmgOq%UkM|Dz?%n-ZljFOH(Kz+NzK3++7DeT(lT?3}H;t}J7 z#T{x?Jx6}vB9FJe7%9);*=W^9%rM&grcp_R8!lZEcd zlgozQb>?+v6$P2sZk@9VPub=3!Pr6<_;L5-jn0|F37bv^ucg`>`2YU^KWN`7()qW@$aVdjTO2RLrvAZUM0`z)6=JTqLO?_WCR>03Bv}sxP`+f7Y-P@^Wao!e>F4YmX3R#|S<;_y z80V^J+zU;gsmJLNV-G@4jA4gR@%dTzkJNR%+mn_u@IekhJRC_-*`47uS)3X;7ep>( z`-{u-BRZ=XBHMr&xK^MSfLizLv$`JW!oHFd-;e%{G zza6r%+^h@%2+fqNpORVeg=L9gDK(?Kyt=-DUEAE)TGb5b)4jtH*%!KV+f*n_D=+dD z@laYbnZdH6Us%k>t$50YZ zX2Uw9;|7Gy5rde!2gr-dHi$Im%PrkM(1L@BjnIdO+|#nm&YYf<)jhhwvVF4cK5RV_ zYuOlZG3H(JT;!<`cwq>0=mGxT_~g-oVZqVBfN))%($Fz{+Ig_1*Bswy6{xpeW> z14UIgFRX<5=7~6VlbWYA>?qBLh(G&&$^nKNDRs@(o$iaJDz3Gl^<2Jm`i)lQGv#sC zsiU-2fi;$C30C+`G{iuWNB5N|&AZnz&JDVmOt~C~;yisst^bpl_nGE}=bk(AAD^sR z4`7L?^-ENUJR<-KD7e4FZV^(|i`mIQbV;0c?sNb1r5vhWLXtq>Pt=MSz_2RYsWf&$ zKJ_5?>*j#~j>>B}eX@0cS7{Y@6H}d=lD^~dn0F-=b@%C zn|3#fOcNN*+!N;P6>22rrZrBAvcjCk2>f-1aFHN2P{Y5Fx0n-DwCnq1d3te3RYg8) zfFn${>m-ADE>itqOb$uT=EQDGKFGY~`vC-*_T4z(ct#BT9I;xhTCbtsshXs3ae6^t zX6IeH0Xoo(jXwnP0;OxF%Z)_^*rNlMvH#J1AbS4*i;c(rAp-tJFKC>|<#PV%?@mqp z8@*s->x$qTr|>5ckntwrnrL3bNIJ(=;E@y01gyDI&@14MJex-d#&$ow#A8y836>sl6{bKEL zSQ2M660JP=QMb%IKsp5fumCeV&XL-N%1DD;2$MRMi1!M>ah0xQA57d* z4bhKV&ks1r!CSca1R6RI2?AqX?Z!kA9zt|k>N?0|3D14F;!tY(Up@zUr~vXD$v10q zHs?V3=BZp^D>)&@MTYi}m!j=_n54@)a~P&uEO3agxj=-6oM+-ska*wpG&vdME|8!@jK`p;RzW2Y`6Xg zoSax7F)86^YQ{>+Ob5Hf;xbpb=!ASBP+V4AT3TM6U0GSvP*Mkwbur}R#tv@X&(fZw zN_%HR9X!07<%H<#FRKf7M8+X8S+_=n7_V^-qhPWqUne3c6&ZSOCO1jn`+1t<^kNQz zNXSNpzkHt^ZngHU-_(sF{1&K;-$}oKG>MxN+Ax2;pCdOu=dlHkL@tRnRyQZ}a3_SN zd?O;OYuR7_ngc<0nbCBT&*ZF8>UV3OvE4?)+{hU3_{}CkslgJIxCSw0N|NKd1QLiP zLiCxvJ2Rv0Z`1U&iMgIV)j;jq-CeZ%<5hTL=^yeJ=3_1ff;_d8m_HhU=C!kQYb)~_ zS*T2QAh~epV~wagK;_U0`goF#C{N8uZRFlD{J;*CvZ?LkF&Uc>$|f%2POzD?K_}va zd^-<51l2C4rHj+J*quyyjC+2h@~Zr=*7e&+F{+(bkt6;~15h}$P=U8(_WPMYXSiFd zK({JcYB6`X?Zx+;%_Pe67As zLAma!M1A{e5 zV)}4O@a^$(;G*L#UPQs8aylV92>x)cPz4DUgwMV%42OQse)j4^Hs zAnqq^8Zedc0MOB8i6hTtHXtu}TF|(*UI>U=zN+1Bq0-xBnxT2X-D>1&l6w!%WIIGx zO4?e}&$!f3V{f1f#B1HCQ%VZNWmaPu*qnBx3$>(oY6K1~Q^@Ci z<6PWEwtBX(5oNJmc}k=J(|bJ-DTI5u7@t>uac1d7;Uv>tR1Ez{dt4fO+Vc0ms+WsJ zJ3In))c*1q^KU-yu+FenXQaQ)o~lpt;-9b4*0YPw1!EosvB8g?WBagB8_-?KQnA_U z$`X0>uV>O}&_l`l9}lH}!#?4^u;(OYCZ>Zos*J1xAg{P2rBGBPtPqv~2z?AjKe@6w zr%(w)bYd6 zKfU$S9W2#2K<7amzLP`qshJ2D?fh{edVSVg{ACfNa=?wo2(j+n9DEpOv{gS- z*)XYcIE~|`P5E3YYn&;}k7H>bTfkdB*qLwvPpxsSk9YI3TE9VFPuj3O<&(g*4HPxE zPO0?7iWl`u6CxuwtbbozU<<#`u^|YzLAp}_{k5BXY^Hm>WzKNSjF_?m33v?uY{#+$ zts^sGW_I01afIWb^<|`YS)v={t=h!jv*9Sc-XFu?N0M;II?8-X{#+PYxYMw8?jd*A zh?XuA6iFY$-Hcb|6+#=P8)x+sCZx;4%%vzqrNl!COW3I=;Z(@&#Iqx_Z6xSE3f5-J zn}??dD2x+NV>I8!;Fy}r|7o>>7mNtdX>9jDPGi4(%*21AH8XkTW3safvcQkYDNIt( z!32lGlZ16bKqWMsNc4_iP>cvc$jgpO+4Vi?7|La&70*f18^6j22IJZ6y1`a3f(j@K zcWyFuR*&)bu6oQ2Mcz+0(tmz4WK^K{<^}X+8bRevuf@A`(&Il!Cqntq)qk-*YZ;KS zfP=o=Ozikmd%NB8=9?Wdun!!l!KN37@_(P~d7Y|-q9tey zB5vB6rgW+QbT?R&(ykn?;x%mV{rZpa)}$O+w7&vlMICB4)SnuuJhD8x!=5N|=Ql5u zU;%?Nh4!^!5F5JlVfmbS$)Bh;KY*zqAMmlrwJmur+3$eUv2ax#?AAIj1@~5!hRb(sI!=zi{jUvYF(kXj#ahE(dBg_kO&%Q#QzD z$>tz4Mi1B$_X<#c-Bx-MzC)c{qMeK~qPvZJUcx#eWZ+IZsA-qJwD?FFLGb<|{Ol)HHgTsj>C3I1giMJD7B{4rw zL2osTht<{rQt8Na0;aYu-2&Fk_w#7uV1l4IPFK2l@PJz2Sb2BjV3jac^?oC2Tk=O5BN$xnc)L8}1$8>6M&I|E}lq z)DMf-RX`%VP%Q4XRIGiRg3wOcfreJPTqQ_|H+Rsbstaf>g8e&)@7|WMVy& zW_M64pPGJgnmqi(Bh%QRPW^;3pbNdFFeNpz?(q(L+jZOC69|R>a#J(qF;6zQ`C}K-B@k}4$7*|@NYty=r9dY2e^p508L7Buck!}X`2^i6sWf1Z+yhT!pg(rox^{&n#KW9v6?G}8wFoiAm7 zME~=J;(!&6axMp?a#r>;J_Nf>1>h@q2MHh`yICNt&gIfofc-hc4WAk0~fIdnrXJi;F9(0$H6;4icE z_T8Uv{SVAszc82P2whl=wRR|W=J~OdAXi`fe)ZOYx>IRK6pX2;{VYxRpJn%DtDbD7 z0y`aSh*%~9Kx|sOIB&Mf!9mX%w=Ti!X+tK5VhtlP(=3NGm|(6zAk~EaD2%{NSk-kWdN|?3!vJcq`O$lnhX~mGM86fl@qNyS$xaELT&Y zZ#2lGj~d9_xqEN&u0g-gz^Jk1%`{!)D3>vaK;3^eiOPpRejX17Mlf`l<5*gui29(z z9kGe-3LRW*8Zz*-+3F1d8}Sywx>oE8bwpI8VSr7Hs?zJ;d$gz4DFqbw5Kk1Sbe`rm zX0JgbP_uArxW1L4rQNyc(2Lp~>ki&Z->NtHaP`>CX2GYFVYg}q`Et1_-F6rLPwj1G zwmCE5h!{Usi1mk=n0T>JRtJAq!!>FWOID5wi% zNmgmE_#|wNhC?M!PkaKB=oq#B#J(Q3M04Zs)Hi#DDYMpllHyT9ngdBvrEuoN{+8sF zfQ@-)USM4Fpa*ov5|WKcs&cGNsv z<4u=|&g=+abswCiqyQvo(_OoyFgI#VEX5qAPm6eZ?RwJFGRM=3qx8{(YigvBi|wzn z$ZiduzUb$!8PQT}WzOd*)`XLO*)&#OsKw70dMz4s>4bo-e{rBs=g*9vtp`g>)&??j za&2-sDh0(U<|XX4IlxNmU#$RADk@K_Yi(sWmNfG!;UI{`;>rdQq%#Fhrx<~wH@9wi zVfvcVh{*mps1don_!W7dTH|Aa?S_#r&-vd(VPb$ zj)cykSmB-r?9OE(HX3hiykD9idUgW${>4mt^gANEQ?;tUBa%Rr!)4G=rBsC^3k&#> z=-5@kkcjGCFFk)r4OZ5+8BhT?#7ohj(fZG|fu@*0*A}-e4fI^k7C=}zY-1oF6d^Bx z6p2Kopr}`IRdG!k0P-LgF2vX8Hzr@ae5IA$k#mXDef2V=N6;4sVCdeXW4FgAOo$4D zq8n@?vNq0&qBxZO5J7-W#X*U)84uxB3m{NT-ZZ7cq028ne#Z8{+(yzBVM|UUeN?Wxt!B*JOw~C8*vs;ZrF06A4Bms~ z!S%RDLm(@>4p{bPDiBS6Ccr6l44^<^f@SZ5d?b+wEfke82`1&$1y)rx4G?I3V|8|K$A^ME2odm0g?C1e@O$ess1zWBEbIw9u_MvK>@)`22Y2TjSM^;lJ$*Nc;=#fBt2Od zSzJ-X0Dn6;8MIb_I40FKw>7i^7u(xAyDm3(*Gqa3U>j0n?@-f?n;qB0BQ0Zt { + if (nClicks) { + if (oldStyle.flex === '1') { + return [ + {}, + !collapsed, + {'display': 'none'}, + collapsed ? 'antd-menu-fold' : 'antd-menu-unfold', + ]; + } + return [ + {'flex': '1'}, + !collapsed, + {'fontSize': '22px', 'color': 'rgb(255, 255, 255)'}, + collapsed ? 'antd-menu-fold' : 'antd-menu-unfold', + ]; + } + return window.dash_clientside.no_update; + } + ''', + [Output('left-side-menu-container', 'style'), + Output('menu-collapse-sider-custom', 'collapsed'), + Output('logo-text', 'style'), + Output('fold-side-menu-icon', 'icon')], + Input('fold-side-menu', 'nClicks'), + [State('menu-collapse-sider-custom', 'collapsed'), + State('left-side-menu-container', 'style')], + prevent_initial_call=True +) diff --git a/dash-fastapi-frontend/callbacks/layout_c/head_c.py b/dash-fastapi-frontend/callbacks/layout_c/head_c.py new file mode 100644 index 0000000..3c69622 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/layout_c/head_c.py @@ -0,0 +1,60 @@ +import dash +from dash import dcc +from flask import session +from dash.dependencies import Input, Output, State + +from server import app + + +# 页首右侧个人中心选项卡回调 +@app.callback( + [Output('index-personal-info-modal', 'visible'), + Output('logout-modal', 'visible')], + Input('index-header-dropdown', 'nClicks'), + State('index-header-dropdown', 'clickedKey'), + prevent_initial_call=True +) +def index_dropdown_click(nClicks, clickedKey): + if clickedKey == '退出登录': + return [ + False, + True + ] + + elif clickedKey == '个人资料': + return [ + True, + False + ] + + return dash.no_update + + +# 退出登录回调 +@app.callback( + Output('redirect-container', 'children', allow_duplicate=True), + Input('logout-modal', 'okCounts'), + prevent_initial_call=True +) +def logout_confirm(okCounts): + if okCounts: + session.clear() + + return [ + dcc.Location( + pathname='/login', + id='index-redirect' + ), + ] + + return dash.no_update + + +# 全局页面重载回调 +@app.callback( + Output('trigger-reload-output', 'reload'), + Input('index-reload', 'nClicks'), + prevent_initial_call=True +) +def reload_page(nClicks): + return True diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py new file mode 100644 index 0000000..0d1d63e --- /dev/null +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -0,0 +1,131 @@ +import dash +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc +from jsonpath_ng import parse +from flask import session, json +from collections import OrderedDict + +from server import app +import views +from utils.tree_tool import find_title_by_key, find_modules_by_key, find_href_by_key, find_parents + + +@app.callback( + [Output('tabs-container', 'items'), + Output('tabs-container', 'activeKey')], + [Input('index-side-menu', 'currentKey'), + Input('tabs-container', 'latestDeletePane')], + [State('tabs-container', 'items'), + State('tabs-container', 'activeKey')], + prevent_initial_call=True +) +def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, activeKey): + """ + 这个回调函数用于处理标签页子项的新建、切换及删除 + 具体策略: + 1.当左侧某个菜单项被新选中,且右侧标签页子项尚未包含此项时,新建并切换 + 2.当左侧某个菜单项被新选中,且右侧标签页子项已包含此项时,切换 + 3.当右侧标签页子项某项被删除时,销毁对应标签页的同时切换回主标签页 + """ + + trigger_id = dash.ctx.triggered_id + + # 基于jsonpath对各标签页子项中所有已有记录的nClicks参数重置为None + # 以避免每次新的items返回给标签页重新渲染后, + # 先前已更新为非None的按钮的nClicks误触发通知弹出回调 + parser = parse('$..nClicks') + origin_items = parser.update(origin_items, None) + + if trigger_id == 'index-side-menu': + + # 判断当前新选中的菜单栏项对应标签页是否已创建 + if currentKey in [item['key'] for item in origin_items]: + return [ + dash.no_update, + currentKey + ] + + menu_title = find_title_by_key(session.get('menu_info'), currentKey) + menu_modules = find_modules_by_key(session.get('menu_info'), currentKey) + + # 否则追加子项返回 + # 其中若各标签页内元素类似,则推荐配合模式匹配构建交互逻辑 + return [ + [ + *origin_items, + { + 'label': menu_title, + 'key': currentKey, + 'children': eval('views.' + menu_modules + '.render()'), + } + ], + currentKey + ] + + elif trigger_id == 'tabs-container': + + # 若要删除的是当前正激活的标签页 + if latestDeletePane == activeKey: + return [ + [ + item + for item in origin_items + if item['key'] != latestDeletePane + ], + '首页' + ] + + # 否则保持当前激活的标签页子项不变,删去目标子项 + return [ + [ + item + for item in origin_items + if item['key'] != latestDeletePane + ], + dash.no_update + ] + + +# 页首面包屑和hash回调 +@app.callback( + [Output('header-breadcrumb', 'items'), + Output('dcc-url', 'pathname')], + Input('tabs-container', 'activeKey'), + prevent_initial_call=True +) +def get_current_breadcrumbs(active_key): + if active_key: + + if active_key == '首页': + return [ + [ + { + 'title': '首页', + 'icon': 'antd-dashboard', + 'href': '/' + }, + ], + '/' + ] + + else: + result = find_parents(session.get('menu_info'), active_key) + # 去除result的重复项 + parent_info = list(OrderedDict((json.dumps(d, ensure_ascii=False), d) for d in result).values()) + if parent_info: + current_href = find_href_by_key(session.get('menu_info'), active_key) + + return [ + [ + { + 'title': '首页', + 'icon': 'antd-dashboard', + 'href': '/' + }, + ] + parent_info, + current_href + ] + + return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/login_c.py b/dash-fastapi-frontend/callbacks/login_c.py new file mode 100644 index 0000000..40e9357 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/login_c.py @@ -0,0 +1,168 @@ +import dash +from dash import dcc +import feffery_antd_components as fac +import feffery_utils_components as fuc +from dash.dependencies import Input, Output, State +from flask import session, request +import re + +from server import app, logger +from api.login import login_api + + +@app.callback( + [Output('login-username-form-item', 'validateStatus'), + Output('login-password-form-item', 'validateStatus'), + Output('login-captcha-form-item', 'validateStatus'), + Output('login-username-form-item', 'help'), + Output('login-password-form-item', 'help'), + Output('login-captcha-form-item', 'help'), + Output('login-captcha', 'refresh'), + Output('login-submit', 'children'), + Output('login-submit', 'loading'), + Output('redirect-container', 'children', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('login-submit', 'nClicks'), + [State('login-username', 'value'), + State('login-password', 'value'), + State('login-captcha', 'captcha'), + State('input-demo', 'value')], + prevent_initial_call=True +) +def login_auth(nClicks, username, password, captcha, input_captcha): + if nClicks: + # 校验全部输入值是否不为空 + if all([username, password, captcha, input_captcha]): + + if captcha == input_captcha: + try: + user_params = dict(user_name=username, password=password, request=str(request.headers)) + userinfo_result = login_api(user_params) + if userinfo_result['code'] == 200: + token = userinfo_result['data']['token'] + session['token'] = token + + return [ + None, + None, + None, + None, + None, + None, + True, + '登录中', + True, + dcc.Location( + pathname='/', + id='login-redirect' + ), + fuc.FefferyFancyMessage('登录成功', type='success'), + ] + + elif userinfo_result['message'] == '用户不存在': + + return [ + 'error', + None, + None, + '用户不存在', + None, + None, + True, + '登录', + False, + None, + None + ] + + elif userinfo_result['message'] == '密码错误': + + return [ + None, + 'error', + None, + None, + '密码错误', + None, + True, + '登录', + False, + None, + None + ] + + else: + + return [ + None, + None, + None, + None, + None, + None, + True, + '登录', + False, + None, + fuc.FefferyFancyMessage(userinfo_result['message'], type='error'), + ] + except Exception as e: + print(e) + return [ + None, + None, + None, + None, + None, + None, + True, + '登录', + False, + None, + fuc.FefferyFancyMessage('接口异常', type='error'), + ] + + else: + return [ + None, + None, + 'error', + None, + None, + '验证码错误!', + True, + '登录', + False, + None, + None + ] + + return [ + None if username else 'error', + None if password else 'error', + None if input_captcha else 'error', + None if username else '请输入用户名!', + None if password else '请输入密码!', + None if input_captcha else '请输入验证码!', + True, + '登录', + False, + None, + None + ] + + return dash.no_update + + +@app.callback( + Output('container', 'style'), + Input('url-container', 'pathname'), + State('container', 'style') +) +def random_bg(pathname, old_style): + return { + **old_style, + 'backgroundImage': 'url({})'.format(dash.get_asset_url('imgs/login-background.jpg')), + 'backgroundRepeat': 'no-repeat', + 'backgroundSize': 'cover' + } diff --git a/dash-fastapi-frontend/config/global_config.py b/dash-fastapi-frontend/config/global_config.py new file mode 100644 index 0000000..7d337b0 --- /dev/null +++ b/dash-fastapi-frontend/config/global_config.py @@ -0,0 +1,21 @@ +import os + + +class PathConfig: + + # 项目绝对根目录 + ABS_ROOT_PATH = os.path.abspath(os.getcwd()) + + +class RouterConfig: + + # 合法pathname列表 + BASIC_VALID_PATHNAME = [ + '/', '/login', '/forget' + ] + + +class ApiBaseUrlConfig: + + # api基本url + BaseUrl = 'http://127.0.0.1:9099' diff --git a/dash-fastapi-frontend/server.py b/dash-fastapi-frontend/server.py new file mode 100644 index 0000000..8116a3b --- /dev/null +++ b/dash-fastapi-frontend/server.py @@ -0,0 +1,85 @@ +import dash +import os +import time +from loguru import logger +from flask import request, session +from user_agents import parse +from config.global_config import PathConfig + +app = dash.Dash( + __name__, + compress=True, + suppress_callback_exceptions=True, + update_title=None +) + +server = app.server + +app.title = '通用后台管理系统' + +# 配置密钥 +app.server.secret_key = 'Dash-FastAPI' + +log_time = time.strftime("%Y%m%d", time.localtime()) +sys_log_file_path = os.path.join(PathConfig.ABS_ROOT_PATH, 'log', 'sys_log', f'sys_request_log_{log_time}.log') +api_log_file_path = os.path.join(PathConfig.ABS_ROOT_PATH, 'log', 'api_log', f'api_request_log_{log_time}.log') +logger.add(sys_log_file_path, filter=lambda x: '[sys]' in x['message'], + rotation="50MB", encoding="utf-8", enqueue=True, compression="zip") +logger.add(api_log_file_path, filter=lambda x: '[api]' in x['message'], + rotation="50MB", encoding="utf-8", enqueue=True, compression="zip") + + +# 获取用户浏览器信息 +@server.before_request +def get_user_agent_info(): + user_string = str(request.user_agent) + user_agent = parse(user_string) + bw = user_agent.browser.family + bw_version = user_agent.browser.version[0] + if bw == 'IE': + logger.warning("[sys]请求人:{}||请求IP:{}||请求方法:{}||请求Data:{}", + session.get('name'), request.remote_addr, request.method, '用户使用IE内核') + return "

请不要使用IE浏览器或360浏览器兼容模式

" + if bw_version < 71: + logger.warning("[sys]请求人:{}||请求IP:{}||请求方法:{}||请求Data:{}", + session.get('name'), request.remote_addr, request.method, '用户Chrome内核版本太低') + return "

Chrome内核版本号太低,请升级浏览器

" \ + "

点击此处可下载最新版Chrome浏览器

" + + +# 配置系统日志 +# @server.after_request +# def get_callbacks_log(response): +# logger.info("[sys]请求人:{}||请求IP:{}||请求方法:{}||请求Data:{}", +# session.get('name'), request.remote_addr, request.method, request.data.decode("utf-8")) +# +# return response + + +# 这里的app即为Dash实例 +@app.server.route('/upload/', methods=['POST']) +def upload(): + """ + 构建文件上传服务 + :return: + """ + + # 获取上传id参数,用于指向保存路径 + upload_id = request.values.get('uploadId') + + # 获取上传的文件名称 + filename = request.files['file'].filename + + # 基于上传id,若本地不存在则会自动创建目录 + try: + os.mkdir(os.path.join(PathConfig.ABS_ROOT_PATH, 'cache', 'upload', f'{upload_id}')) + except FileExistsError: + pass + + # 流式写出文件到指定目录 + with open(os.path.join(PathConfig.ABS_ROOT_PATH, 'cache', 'upload', f'{upload_id}', filename), 'wb') as f: + # 流式写出大型文件,这里的10代表10MB + for chunk in iter(lambda: request.files['file'].read(1024 * 1024 * 10), b''): + f.write(chunk) + + return {'filename': filename} diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py new file mode 100644 index 0000000..07af70c --- /dev/null +++ b/dash-fastapi-frontend/store/store.py @@ -0,0 +1,15 @@ +from dash import html, dcc + + +def render_store_container(): + return html.Div( + [ + dcc.Store(id='api-check-token'), + # 接口校验返回存储容器 + dcc.Store(id='api-check-result-container'), + # token存储容器 + dcc.Store(id='token-container'), + # 菜单current_key存储容器 + dcc.Store(id='current-key-container') + ] + ) diff --git a/dash-fastapi-frontend/utils/file.py b/dash-fastapi-frontend/utils/file.py new file mode 100644 index 0000000..e69de29 diff --git a/dash-fastapi-frontend/utils/request.py b/dash-fastapi-frontend/utils/request.py new file mode 100644 index 0000000..40f138a --- /dev/null +++ b/dash-fastapi-frontend/utils/request.py @@ -0,0 +1,50 @@ +import requests +from typing import Optional +from flask import session +from config.global_config import ApiBaseUrlConfig +from server import logger +from flask import request + + +def api_request(method: str, url: str, is_headers: bool, params: Optional[dict] = None, data: Optional[dict] = None, + json: Optional[dict] = None, timeout: Optional[int] = None): + api_url = ApiBaseUrlConfig.BaseUrl + url + method = method.lower().strip() + api_headers = None + if is_headers: + api_headers = {'token': 'Bearer' + session.get('token')} + try: + if method == 'get': + response = requests.get(url=api_url, params=params, data=data, json=json, headers=api_headers, + timeout=timeout) + elif method == 'post': + response = requests.post(url=api_url, params=params, data=data, json=json, headers=api_headers, + timeout=timeout) + elif method == 'delete': + response = requests.delete(url=api_url, params=params, data=data, json=json, headers=api_headers, + timeout=timeout) + elif method == 'put': + response = requests.put(url=api_url, params=params, data=data, json=json, headers=api_headers, + timeout=timeout) + else: + raise ValueError(f'Unsupported HTTP method: {method}') + + response_code = response.json()['code'] + response_message = response.json()['message'] + session['code'] = response_code + session['message'] = response_message + if response_code == 200: + logger.info("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求结果:{}", + session.get('user_info').get('user_name') if session.get('user_info') else None, + request.remote_addr, method, url, response_message) + else: + logger.warning("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求结果:{}", + session.get('user_info').get('user_name') if session.get('user_info') else None, + request.remote_addr, method, url, response_message) + + return response.json() + except Exception as e: + logger.error("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求结果:{}", + session.get('user')['user_name'], request.remote_addr, method, url, str(e)) + + raise Exception diff --git a/dash-fastapi-frontend/utils/tree_tool.py b/dash-fastapi-frontend/utils/tree_tool.py new file mode 100644 index 0000000..3dae146 --- /dev/null +++ b/dash-fastapi-frontend/utils/tree_tool.py @@ -0,0 +1,118 @@ +def find_node_values(data, key): + """ + 递归查找所有包含目标键的字典,并返回该键对应的值组成的列表。 + :param data: 待查找的树形list + :param key: 目标键 + :return: 包含目标键的字典中目标键对应的值组成的列表 + """ + result = [] + for item in data: + if isinstance(item, dict): + if key in item: + result.append(item[key]) + # 递归查找子节点 + result.extend(find_node_values(item.values(), key)) + elif isinstance(item, list): + # 递归查找子节点 + result.extend(find_node_values(item, key)) + return result + + +def find_key_by_href(data, href): + """ + 递归查找所有包含目标键的字典,并返回该键对应的值组成的列表。 + :param data: 待查找的树形list + :param href: 目标pathname + :return: 目标值对应的key + """ + for item in data: + if 'children' in item: + result = find_key_by_href(item['children'], href) + if result is not None: + return result + elif 'href' in item['props'] and item['props']['href'] == href: + return item['props']['key'] + return None + + +def find_title_by_key(data, key): + """ + 递归查找所有包含目标键的字典,并返回该键对应的值组成的列表。 + :param data: 待查找的树形list + :param key: 目标key + :return: 目标值对应的title + """ + for item in data: + if 'children' in item: + result = find_title_by_key(item['children'], key) + if result is not None: + return result + elif 'key' in item['props'] and item['props']['key'] == key: + return item['props']['title'] + return None + + +def find_href_by_key(data, key): + """ + 递归查找所有包含目标键的字典,并返回该键对应的值组成的列表。 + :param data: 待查找的树形list + :param key: 目标key + :return: 目标值对应的href + """ + for item in data: + if 'children' in item: + result = find_href_by_key(item['children'], key) + if result is not None: + return result + elif 'key' in item['props'] and item['props']['key'] == key: + return item['props']['href'] + return None + + +def find_modules_by_key(data, key): + """ + 递归查找所有包含目标键的字典,并返回该键对应的值组成的列表。 + :param data: 待查找的树形list + :param key: 目标key + :return: 目标值对应的module + """ + for item in data: + if 'children' in item: + result = find_modules_by_key(item['children'], key) + if result is not None: + return result + elif 'key' in item['props'] and item['props']['key'] == key: + return item['props']['modules'] + return None + + +def find_parents(tree, target_key): + """ + 递归查找所有包含目标键的字典,并返回该键对应的值组成的列表。 + :param tree: 待查找的树形list + :param target_key: 目标target_key + :return: 目标值对应的所有根节点的title + """ + result = [] + + def search_parents(node, key): + if 'children' in node: + for child in node['children']: + temp_result = search_parents(child, key) + if len(temp_result) > 0: + result.append({'title': node['props']['title']}) + result.extend(temp_result) + return result + + if 'key' in node['props'] and node['props']['key'] == key: + result.append({'title': node['props']['title']}) + return result + + return [] + + for node in tree: + result = search_parents(node, target_key) + if len(result) > 0: + break + + return result[::-1] diff --git a/dash-fastapi-frontend/views/__init__.py b/dash-fastapi-frontend/views/__init__.py new file mode 100644 index 0000000..aa469be --- /dev/null +++ b/dash-fastapi-frontend/views/__init__.py @@ -0,0 +1,8 @@ +from . import ( + layout, + system, + monitor, + login, + page_404, + forget +) diff --git a/dash-fastapi-frontend/views/forget.py b/dash-fastapi-frontend/views/forget.py new file mode 100644 index 0000000..4b332db --- /dev/null +++ b/dash-fastapi-frontend/views/forget.py @@ -0,0 +1,133 @@ +from dash import html +import feffery_antd_components as fac +import feffery_utils_components as fuc + +import callbacks.forget_c + + +def render_forget_content(): + return html.Div( + [ + fac.AntdCard( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + placeholder='请输入用户名', + id='forget-username', + size='large', + prefix=fac.AntdIcon( + icon='antd-user' + ), + ), + id='forget-username-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + placeholder='请输入新密码', + id='forget-password', + mode='password', + passwordUseMd5=True, + size='large', + prefix=fac.AntdIcon( + icon='antd-lock' + ), + ), + id='forget-password-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + placeholder='请再次输入新密码', + id='forget-password-again', + mode='password', + passwordUseMd5=True, + size='large', + prefix=fac.AntdIcon( + icon='antd-lock' + ), + ), + id='forget-password-again-form-item' + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + placeholder='请输入短信验证码', + id='forget-input-captcha', + size='large', + prefix=fac.AntdIcon( + icon='antd-check-circle' + ), + style={ + 'width': '270px' + } + ), + id='forget-captcha-form-item' + ), + fac.AntdFormItem( + fac.AntdButton( + '获取验证码', + id='get-message-code', + type='primary', + size='large' + ) + ), + ], + align='end', + size=10 + ), + fac.AntdFormItem( + fac.AntdButton( + '保存', + id='forget-submit', + type='primary', + block=True, + size='large', + ), + style={ + 'marginTop': '20px' + } + ) + ], + layout='vertical', + style={ + 'width': '100%' + } + ), + + # 重定向容器 + html.Div(id='forget-redirect-container'), + fuc.FefferyCountDown(id='message-code-count-down') + ], + id='forget-form-container', + title='重置密码', + hoverable=True, + extraLink={ + 'content': '返回登录', + 'href': '/login', + 'target': '_self', + 'style': { + 'font-size': '16px' + } + }, + headStyle={ + 'font-weight': 'bold', + 'text-align': 'center', + 'font-size': '30px' + }, + style={ + 'position': 'fixed', + 'top': '16%', + 'left': '50%', + 'width': '500px', + 'padding': '0px 30px', + 'transform': 'translateX(-50%)' + } + ), + # 消息提示 + html.Div(id='forget-message-container'), + html.Div(id='forget-sms-container'), + ] + ) + diff --git a/dash-fastapi-frontend/views/layout/__init__.py b/dash-fastapi-frontend/views/layout/__init__.py new file mode 100644 index 0000000..1e652e2 --- /dev/null +++ b/dash-fastapi-frontend/views/layout/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index, +) diff --git a/dash-fastapi-frontend/views/layout/components/aside.py b/dash-fastapi-frontend/views/layout/components/aside.py new file mode 100644 index 0000000..edfcb34 --- /dev/null +++ b/dash-fastapi-frontend/views/layout/components/aside.py @@ -0,0 +1,84 @@ +from dash import html +import feffery_antd_components as fac + +import callbacks.layout_c.aside_c + + +def render_aside_content(menu_info): + + return [ + fac.AntdSider( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdImage( + width=32, + height=32, + src='assets/imgs/logo.png', + preview=False, + ), + flex='1', + style={ + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center' + } + ), + fac.AntdCol( + fac.AntdText( + '后台管理系统', + id='logo-text', + style={ + 'fontSize': '22px', + # 'paddingLeft': '20px', + 'color': 'rgb(255, 255, 255)' + } + ), + flex='5', + style={ + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center', + } + ) + ], + style={ + 'height': '50px', + 'background': '#001529', + 'position': 'sticky', + 'top': 0, + 'zIndex': 999, + 'paddingLeft': '10px' + } + ), + fac.AntdMenu( + id='index-side-menu', + menuItems=[ + { + 'component': 'Item', + 'props': { + 'key': '首页', + 'title': '首页', + 'icon': 'antd-dashboard', + 'href': '/' + } + } + ] + menu_info, + mode='inline', + theme='dark', + defaultSelectedKey='首页', + defaultOpenKeys=['-1_1'], + style={ + 'width': '100%', + 'height': 'calc(100vh - 50px)' + } + ), + ], + id='menu-collapse-sider-custom', + collapsible=True, + collapsedWidth=60, + trigger=None, + width=210 + ), + ] diff --git a/dash-fastapi-frontend/views/layout/components/content.py b/dash-fastapi-frontend/views/layout/components/content.py new file mode 100644 index 0000000..f5caf92 --- /dev/null +++ b/dash-fastapi-frontend/views/layout/components/content.py @@ -0,0 +1,42 @@ +from dash import html +import feffery_antd_components as fac + + +def render_main_content(user_name, nick_name, phone_number): + return [ + # 右侧主体内容区域 + fac.AntdCol( + [ + html.Div( + fac.AntdTabs( + items=[ + { + 'label': '首页', + 'key': '首页', + 'closable': False, + 'children': fac.AntdAlert( + type='info', + showIcon=True, + message='这里是主标签页,通常建议设置为不可关闭并展示一些总览类型的信息' + ) + } + ], + id='tabs-container', + type='editable-card', + # defaultActiveKey='首页', + style={ + 'width': '100%', + 'paddingLeft': '15px' + } + ), + # id='index-main-content-container', + style={ + 'width': '100%', + 'height': '100%', + 'backgroundColor': 'white', + } + ) + ], + flex='auto' + ) + ] diff --git a/dash-fastapi-frontend/views/layout/components/head.py b/dash-fastapi-frontend/views/layout/components/head.py new file mode 100644 index 0000000..5540e12 --- /dev/null +++ b/dash-fastapi-frontend/views/layout/components/head.py @@ -0,0 +1,133 @@ +from dash import html +import feffery_antd_components as fac + +import callbacks.layout_c.head_c + + +def render_head_content(user_name): + return [ + # 页首左侧折叠按钮区域 + fac.AntdCol( + html.Div( + fac.AntdButton( + fac.AntdIcon( + id='fold-side-menu-icon', + icon='antd-menu-fold' + ), + id='fold-side-menu', + type='text', + shape='circle', + size='large', + style={ + 'marginLeft': '5px', + 'background': 'white' + } + ), + style={ + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center' + } + ) + ), + + # 页首面包屑区域 + fac.AntdCol( + fac.AntdBreadcrumb( + items=[ + { + 'title': '首页', + 'icon': 'antd-dashboard', + 'href': '/#' + } + ], + id='header-breadcrumb' + ), + style={ + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center', + 'paddingLeft': '5px' + } + ), + + # 页首右侧用户信息区域 + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdTooltip( + fac.AntdAvatar( + mode='text', + size=36, + text=user_name, + style={ + 'background': 'gold' + } + ), + title='当前用户:' + user_name, + placement='bottom' + ), + + fac.AntdDropdown( + id='index-header-dropdown', + title='个人中心', + arrow=True, + menuItems=[ + { + 'title': '个人资料', + 'key': '个人资料', + 'icon': 'antd-idcard' + }, + { + 'isDivider': True + }, + { + 'title': '退出登录', + 'key': '退出登录', + 'icon': 'antd-logout' + }, + ], + placement='bottomRight', + overlayStyle={ + 'width': '100px' + } + ) + ], + style={ + 'height': '100%', + 'float': 'right', + 'display': 'flex', + 'alignItems': 'center' + } + ), + flex=1 + ), + fac.AntdCol( + # 全局刷新按钮 + html.Div( + fac.AntdTooltip( + fac.AntdButton( + fac.AntdIcon( + id='index-reload-icon', + icon='fc-synchronize' + ), + id='index-reload', + type='text', + shape='circle', + size='large', + style={ + 'backgroundColor': 'rgb(255 255 255 / 0%)', + } + ), + title='刷新', + placement='bottom' + ) + ), + style={ + 'height': '100%', + 'paddingRight': '3px', + 'display': 'flex', + 'alignItems': 'center' + } + ), + ] diff --git a/dash-fastapi-frontend/views/layout/index.py b/dash-fastapi-frontend/views/layout/index.py new file mode 100644 index 0000000..be8c18e --- /dev/null +++ b/dash-fastapi-frontend/views/layout/index.py @@ -0,0 +1,139 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + +from views.layout.components.head import render_head_content +from views.layout.components.content import render_main_content +from views.layout.components.aside import render_aside_content +# import callbacks.index_c +import callbacks.layout_c.fold_side_menu +import callbacks.layout_c.index_c + + +def render_content(user_name, nick_name, phone_number, menu_info): + + return fuc.FefferyTopProgress( + html.Div( + [ + # 全局重载 + fuc.FefferyReload(id='trigger-reload-output'), + + html.Div(id='idle-placeholder-container'), + + # 注入相关modal + html.Div( + [ + # 个人资料面板 + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdText( + user_name, + copyable=True + ), + label='账号' + ), + fac.AntdFormItem( + fac.AntdText( + nick_name, + copyable=True + ), + label='姓名' + ), + fac.AntdFormItem( + fac.AntdText( + phone_number, + copyable=True + ), + label='电话' + ) + ], + labelCol={ + 'span': 4 + } + ) + ], + id='index-personal-info-modal', + title='个人资料', + mask=False + ), + ] + ), + + # 退出登录对话框提示 + fac.AntdModal( + html.Div( + [ + fac.AntdIcon(icon='fc-info', style={'font-size': '28px'}), + fac.AntdText('确定注销并退出系统吗?', style={'margin-left': '5px'}), + ] + ), + id='logout-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + + # 平台主页面 + fac.AntdRow( + [ + # 左侧固定菜单区域 + fac.AntdCol( + fac.AntdAffix( + html.Div( + render_aside_content(menu_info), + id='side-menu', + style={ + 'height': '100vh', + 'overflowY': 'auto', + 'transition': 'width 1s', + 'background': '#001529' + } + ), + ), + # flex='1', + id='left-side-menu-container', + style={ + 'flex': '1' + } + ), + + # 右侧区域 + fac.AntdCol( + [ + fac.AntdRow( + render_head_content(user_name), + style={ + 'height': '50px', + 'boxShadow': 'rgb(240 241 242) 0px 2px 14px', + 'background': 'white', + 'marginBottom': '10px', + 'position': 'sticky', + 'top': 0, + 'zIndex': 999 + } + ), + fac.AntdRow( + render_main_content(user_name, nick_name, phone_number), + wrap=False + ) + ], + # flex='5', + style={ + 'flex': '6', + 'width': '300px' + } + ), + ], + ) + ], + id='index-main-content-container', + ), + listenPropsMode='include', + includeProps=[ + 'tabs-test.children' + ] + ) diff --git a/dash-fastapi-frontend/views/login.py b/dash-fastapi-frontend/views/login.py new file mode 100644 index 0000000..99eb95f --- /dev/null +++ b/dash-fastapi-frontend/views/login.py @@ -0,0 +1,188 @@ +import dash +from dash import html +import feffery_antd_components as fac +import feffery_utils_components as fuc + +import callbacks.login_c + + +def render_content(): + return html.Div( + [ + html.Div( + [ + html.Div( + [ + fac.AntdText('HELLO', style={'color': 'rgba(255,255,255,0.8)'}) + ], + style={ + 'fontSize': '60px', + 'fontWeight': '500' + } + ), + html.Div( + [ + fac.AntdText('WELCOME', style={'color': 'rgba(255,255,255,0.8)'}), + ], + style={ + 'fontSize': '60px', + 'fontWeight': '500' + } + ), + html.Div( + [ + fac.AntdText('欢迎使用通用后台管理系统', style={'color': 'rgba(255,255,255,0.8)'}), + ], + style={ + 'fontSize': '18px', + 'fontWeight': '600', + 'marginTop': '20px' + } + ), + ], + style={ + 'position': 'fixed', + 'top': '20%', + 'left': '26%', + 'width': '430px', + 'padding': '0px 30px', + 'transform': 'translateX(-50%)' + } + ), + fac.AntdCard( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + placeholder='请输入用户名', + id='login-username', + size='large', + prefix=fac.AntdIcon( + icon='antd-user' + ), + ), + id='login-username-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + placeholder='请输入密码', + id='login-password', + mode='password', + passwordUseMd5=True, + size='large', + prefix=fac.AntdIcon( + icon='antd-lock' + ), + ), + id='login-password-form-item' + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + placeholder='请输入验证码', + id='input-demo', + size='large', + prefix=fac.AntdIcon( + icon='antd-check-circle' + ), + style={ + 'width': '210px' + } + ), + id='login-captcha-form-item' + ), + fac.AntdFormItem( + fuc.FefferyCaptcha( + id='login-captcha', + charNum=4, + height=30, + bgColor='white', + style={ + 'marginBottom': 0, + 'paddingBottom': 0 + } + ) + ) + ], + align='end', + size=10 + ), + fac.AntdSpace( + [ + html.Div(id='test'), + fac.AntdButton( + '忘记密码', + id='forget-password-link', + type='link', + href='/forget', + target='_self' + ) + ], + align='center', + size=240 + ), + fac.AntdFormItem( + fac.AntdButton( + '登录', + id='login-submit', + type='primary', + block=True, + size='large', + ), + style={ + 'marginTop': '20px' + } + ) + ], + layout='vertical', + style={ + 'width': '100%' + } + ), + ], + id='login-form-container', + title='登录', + hoverable=True, + style={ + 'position': 'fixed', + 'top': '16%', + 'left': '70%', + 'width': '430px', + 'padding': '0px 30px', + 'transform': 'translateX(-50%)' + } + ), + fac.AntdFooter( + html.Div( + fac.AntdText( + '版权所有©2023 Dash-FastAPI', + style={ + 'margin': '0' + } + ), + style={ + 'display': 'flex', + 'height': '100%', + 'justifyContent': 'center', + 'alignItems': 'center' + } + ), + style={ + 'backgroundColor': 'rgb(255 255 255 / 0%)', + 'height': '40px', + 'position': 'fixed', + 'bottom': 0, + 'left': '50%', + 'width': '500px', + 'padding': '20px 50px', + 'transform': 'translateX(-50%)' + } + ), + ], + id='container', + style={ + 'height': '100vh', + } + ) diff --git a/dash-fastapi-frontend/views/monitor/__init__.py b/dash-fastapi-frontend/views/monitor/__init__.py new file mode 100644 index 0000000..1543d23 --- /dev/null +++ b/dash-fastapi-frontend/views/monitor/__init__.py @@ -0,0 +1,4 @@ +from . import ( + operlog, + logininfor +) diff --git a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/monitor/logininfor/index.py b/dash-fastapi-frontend/views/monitor/logininfor/index.py new file mode 100644 index 0000000..7734a12 --- /dev/null +++ b/dash-fastapi-frontend/views/monitor/logininfor/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是登录日志') diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/monitor/operlog/index.py b/dash-fastapi-frontend/views/monitor/operlog/index.py new file mode 100644 index 0000000..5766ac5 --- /dev/null +++ b/dash-fastapi-frontend/views/monitor/operlog/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是操作日志') diff --git a/dash-fastapi-frontend/views/page_404.py b/dash-fastapi-frontend/views/page_404.py new file mode 100644 index 0000000..654bf70 --- /dev/null +++ b/dash-fastapi-frontend/views/page_404.py @@ -0,0 +1,38 @@ +from dash import html +import feffery_antd_components as fac + + +def render_content(): + + return html.Div( + [ + html.Div( + [ + fac.AntdResult( + status='404', + title='页面不存在', + subTitle='检查您的网址输入是否正确', + style={ + 'paddingBottom': 0, + 'paddingTop': 0 + } + ), + fac.AntdButton( + '回到首页', + type='link', + href='/', + target='_self' + ) + ], + style={ + 'textAlign': 'center' + } + ) + ], + style={ + 'height': '100vh', + 'display': 'flex', + 'alignItems': 'center', + 'justifyContent': 'center' + } + ) diff --git a/dash-fastapi-frontend/views/system/__init__.py b/dash-fastapi-frontend/views/system/__init__.py new file mode 100644 index 0000000..750ffd1 --- /dev/null +++ b/dash-fastapi-frontend/views/system/__init__.py @@ -0,0 +1,10 @@ +from . import ( + user, + role, + menu, + dept, + post, + dict, + config, + notice +) diff --git a/dash-fastapi-frontend/views/system/config/__init__.py b/dash-fastapi-frontend/views/system/config/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/config/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/config/index.py b/dash-fastapi-frontend/views/system/config/index.py new file mode 100644 index 0000000..64badb5 --- /dev/null +++ b/dash-fastapi-frontend/views/system/config/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是参数设置') diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/dept/index.py b/dash-fastapi-frontend/views/system/dept/index.py new file mode 100644 index 0000000..8f449af --- /dev/null +++ b/dash-fastapi-frontend/views/system/dept/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是部门管理') diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/dict/index.py b/dash-fastapi-frontend/views/system/dict/index.py new file mode 100644 index 0000000..46a618d --- /dev/null +++ b/dash-fastapi-frontend/views/system/dict/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是字典管理') diff --git a/dash-fastapi-frontend/views/system/menu/__init__.py b/dash-fastapi-frontend/views/system/menu/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/menu/index.py b/dash-fastapi-frontend/views/system/menu/index.py new file mode 100644 index 0000000..9729ba9 --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是菜单管理') diff --git a/dash-fastapi-frontend/views/system/notice/__init__.py b/dash-fastapi-frontend/views/system/notice/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/notice/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/notice/index.py b/dash-fastapi-frontend/views/system/notice/index.py new file mode 100644 index 0000000..4bcc439 --- /dev/null +++ b/dash-fastapi-frontend/views/system/notice/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是通知公告') diff --git a/dash-fastapi-frontend/views/system/post/__init__.py b/dash-fastapi-frontend/views/system/post/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/post/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/post/index.py b/dash-fastapi-frontend/views/system/post/index.py new file mode 100644 index 0000000..547151f --- /dev/null +++ b/dash-fastapi-frontend/views/system/post/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是岗位管理') diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/role/index.py b/dash-fastapi-frontend/views/system/role/index.py new file mode 100644 index 0000000..7ccec88 --- /dev/null +++ b/dash-fastapi-frontend/views/system/role/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是角色管理') diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py new file mode 100644 index 0000000..251e6c2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -0,0 +1,3 @@ +from . import ( + index +) diff --git a/dash-fastapi-frontend/views/system/user/index.py b/dash-fastapi-frontend/views/system/user/index.py new file mode 100644 index 0000000..3d1c7af --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/index.py @@ -0,0 +1,8 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是用户管理') -- Gitee From d2b3e6c9fbc3db0b2d46afffbdc1878293b99934 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Tue, 30 May 2023 20:09:41 +0800 Subject: [PATCH 002/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E5=A2=9E=E5=88=A0=E6=9F=A5=E6=94=B9=EF=BC=8C=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 9 +- .../controller/dept_controller.py | 28 + .../controller/post_controler.py | 27 + .../controller/role_controller.py | 27 + .../controller/user_controller.py | 2 + dash-fastapi-backend/mapper/crud/dept_crud.py | 27 + dash-fastapi-backend/mapper/crud/post_crud.py | 8 + dash-fastapi-backend/mapper/crud/role_crud.py | 8 + dash-fastapi-backend/mapper/crud/user_crud.py | 101 ++- .../mapper/schema/dept_schema.py | 18 + .../mapper/schema/post_schema.py | 18 + .../mapper/schema/role_schema.py | 18 + .../mapper/schema/user_schema.py | 32 +- dash-fastapi-backend/service/dept_service.py | 41 + dash-fastapi-backend/service/post_service.py | 13 + dash-fastapi-backend/service/role_service.py | 13 + dash-fastapi-backend/service/user_service.py | 4 +- .../utils/time_format_tool.py | 25 + dash-fastapi-frontend/api/dept.py | 6 + dash-fastapi-frontend/api/post.py | 6 + dash-fastapi-frontend/api/role.py | 6 + dash-fastapi-frontend/app.py | 4 +- dash-fastapi-frontend/callbacks/__init__.py | 0 dash-fastapi-frontend/callbacks/app_c.py | 2 +- .../callbacks/layout_c/__init__.py | 0 .../callbacks/layout_c/index_c.py | 28 +- dash-fastapi-frontend/callbacks/login_c.py | 8 - .../callbacks/system_c/user_c.py | 447 ++++++++++ dash-fastapi-frontend/views/layout/index.py | 2 +- dash-fastapi-frontend/views/login.py | 2 + .../views/system/user/index.py | 804 +++++++++++++++++- 31 files changed, 1702 insertions(+), 32 deletions(-) create mode 100644 dash-fastapi-backend/controller/dept_controller.py create mode 100644 dash-fastapi-backend/controller/post_controler.py create mode 100644 dash-fastapi-backend/controller/role_controller.py create mode 100644 dash-fastapi-backend/mapper/schema/dept_schema.py create mode 100644 dash-fastapi-backend/mapper/schema/post_schema.py create mode 100644 dash-fastapi-backend/mapper/schema/role_schema.py create mode 100644 dash-fastapi-backend/service/dept_service.py create mode 100644 dash-fastapi-backend/service/post_service.py create mode 100644 dash-fastapi-backend/service/role_service.py create mode 100644 dash-fastapi-frontend/api/dept.py create mode 100644 dash-fastapi-frontend/api/post.py create mode 100644 dash-fastapi-frontend/api/role.py delete mode 100644 dash-fastapi-frontend/callbacks/__init__.py delete mode 100644 dash-fastapi-frontend/callbacks/layout_c/__init__.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index d1158fd..3617fc4 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -7,6 +7,9 @@ from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware from controller.login_controller import loginController from controller.user_controller import userController +from controller.dept_controller import deptController +from controller.role_controller import roleController +from controller.post_controler import postController from config.env import RedisConfig @@ -57,8 +60,12 @@ async def http_exception_handler(request: Request, exc: HTTPException): status_code=exc.status_code ) + app.include_router(loginController, prefix="/login", tags=['login']) -app.include_router(userController, prefix="/system", tags=['system']) +app.include_router(userController, prefix="/system", tags=['system/user']) +app.include_router(deptController, prefix="/system", tags=['system/dept']) +app.include_router(roleController, prefix="/system", tags=['system/role']) +app.include_router(postController, prefix="/system", tags=['system/post']) if __name__ == '__main__': diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/controller/dept_controller.py new file mode 100644 index 0000000..5adf226 --- /dev/null +++ b/dash-fastapi-backend/controller/dept_controller.py @@ -0,0 +1,28 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, HTTPException, Header +from config.get_db import get_db +from service.login_service import get_current_user, get_password_hash +from service.dept_service import * +from mapper.schema.dept_schema import * +from mapper.crud.dept_crud import * +from utils.response_tool import * +from utils.log_tool import * + + +deptController = APIRouter() + + +@deptController.post("/dept/tree", response_model=DeptTree) +async def get_system_dept_tree(request: Request, dept_query: DeptPageObject, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + dept_query_result = get_dept_tree_services(query_db, dept_query) + logger.info('获取成功') + return response_200(data=dept_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/post_controler.py b/dash-fastapi-backend/controller/post_controler.py new file mode 100644 index 0000000..ce62f92 --- /dev/null +++ b/dash-fastapi-backend/controller/post_controler.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, HTTPException, Header +from config.get_db import get_db +from service.login_service import get_current_user, get_password_hash +from service.post_service import * +from mapper.schema.post_schema import * +from utils.response_tool import * +from utils.log_tool import * + + +postController = APIRouter() + + +@postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel) +async def get_system_post_select(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + role_query_result = get_post_select_option_services(query_db) + logger.info('获取成功') + return response_200(data=role_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/role_controller.py b/dash-fastapi-backend/controller/role_controller.py new file mode 100644 index 0000000..667b727 --- /dev/null +++ b/dash-fastapi-backend/controller/role_controller.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, HTTPException, Header +from config.get_db import get_db +from service.login_service import get_current_user, get_password_hash +from service.role_service import * +from mapper.schema.role_schema import * +from utils.response_tool import * +from utils.log_tool import * + + +roleController = APIRouter() + + +@roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel) +async def get_system_role_select(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + role_query_result = get_role_select_option_services(query_db) + logger.info('获取成功') + return response_200(data=role_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/user_controller.py b/dash-fastapi-backend/controller/user_controller.py index db6ff1f..744f198 100644 --- a/dash-fastapi-backend/controller/user_controller.py +++ b/dash-fastapi-backend/controller/user_controller.py @@ -80,6 +80,8 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok logger.warning(current_user) return response_401(data="", message=current_user) else: + delete_user.update_by = current_user.user.user_name + delete_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") delete_user_result = delete_user_services(query_db, delete_user) if delete_user_result.is_success: logger.info(delete_user_result.message) diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/mapper/crud/dept_crud.py index cb36da0..440997a 100644 --- a/dash-fastapi-backend/mapper/crud/dept_crud.py +++ b/dash-fastapi-backend/mapper/crud/dept_crud.py @@ -1,6 +1,7 @@ from sqlalchemy import and_ from sqlalchemy.orm import Session from entity.dept_entity import SysDept +from mapper.schema.dept_schema import DeptPageObject from utils.time_format_tool import list_format_datetime from utils.page_tool import get_page_info @@ -14,3 +15,29 @@ def get_dept_by_id(db: Session, dept_id: int): return dept_info + +def get_dept_list_for_tree(db: Session, dept_info: DeptPageObject): + if dept_info.dept_name: + dept_query_all = db.query(SysDept) \ + .filter(SysDept.status == 0, + SysDept.del_flag == 0, + SysDept.dept_name.like(f'%{dept_info.dept_name}%') if dept_info.dept_name else True) \ + .all() + + dept = [] + if dept_query_all: + for dept_query in dept_query_all: + ancestor_info = dept_query.ancestors.split(',') + ancestor_info.append(dept_query.dept_id) + for ancestor in ancestor_info: + dept_item = get_dept_by_id(db, int(ancestor)) + if dept_item: + dept.append(dept_item) + # 去重 + dept_result = list(set(dept)) + else: + dept_result = db.query(SysDept) \ + .filter(SysDept.status == 0, SysDept.del_flag == 0) \ + .all() + + return list_format_datetime(dept_result) diff --git a/dash-fastapi-backend/mapper/crud/post_crud.py b/dash-fastapi-backend/mapper/crud/post_crud.py index 7d89edf..f87bd35 100644 --- a/dash-fastapi-backend/mapper/crud/post_crud.py +++ b/dash-fastapi-backend/mapper/crud/post_crud.py @@ -12,3 +12,11 @@ def get_post_by_id(db: Session, post_id: int): .first() return post_info + + +def get_post_select_option_crud(db: Session): + post_info = db.query(SysPost) \ + .filter(SysPost.status == 0) \ + .all() + + return post_info diff --git a/dash-fastapi-backend/mapper/crud/role_crud.py b/dash-fastapi-backend/mapper/crud/role_crud.py index 5bc0291..f17ce9d 100644 --- a/dash-fastapi-backend/mapper/crud/role_crud.py +++ b/dash-fastapi-backend/mapper/crud/role_crud.py @@ -13,3 +13,11 @@ def get_role_by_id(db: Session, role_id: int): .first() return role_info + + +def get_role_select_option_crud(db: Session): + role_info = db.query(SysRole) \ + .filter(SysRole.status == 0, SysRole.del_flag == 0) \ + .all() + + return role_info diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/mapper/crud/user_crud.py index bfd22c2..0a3838d 100644 --- a/dash-fastapi-backend/mapper/crud/user_crud.py +++ b/dash-fastapi-backend/mapper/crud/user_crud.py @@ -6,9 +6,10 @@ from entity.dept_entity import SysDept from entity.post_entity import SysPost from entity.menu_entity import SysMenu from mapper.schema.user_schema import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ - UserPageObjectResponse, CrudUserResponse -from utils.time_format_tool import list_format_datetime + UserPageObjectResponse, CrudUserResponse, UserInfoJoinDept +from utils.time_format_tool import list_format_datetime, format_datetime_dict_list from utils.page_tool import get_page_info +from datetime import datetime, time def get_user_by_name(db: Session, user_name: str): @@ -67,6 +68,48 @@ def get_user_by_id(db: Session, user_id: int): return CurrentUserInfo(**results) +def get_user_detail_by_id(db: Session, user_id: int): + """ + 根据user_id获取用户详细信息 + :param db: orm对象 + :param user_id: 用户id + :return: 当前user_id的用户信息对象 + """ + query_user_basic_info = db.query(SysUser) \ + .filter(SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .distinct().all() + query_user_dept_info = db.query(SysDept).select_from(SysUser) \ + .filter(SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ + .distinct().all() + query_user_role_info = db.query(SysRole).select_from(SysUser) \ + .filter(SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .distinct().all() + query_user_post_info = db.query(SysPost).select_from(SysUser) \ + .filter(SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserPost, SysUser.user_id == SysUserPost.user_id) \ + .outerjoin(SysPost, and_(SysUserPost.post_id == SysPost.post_id, SysPost.status == 0)) \ + .distinct().all() + query_user_menu_info = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ + .distinct().all() + results = dict( + user_basic_info=list_format_datetime(query_user_basic_info), + user_dept_info=list_format_datetime(query_user_dept_info), + user_role_info=list_format_datetime(query_user_role_info), + user_post_info=list_format_datetime(query_user_post_info), + user_menu_info=list_format_datetime(query_user_menu_info) + ) + + return CurrentUserInfo(**results) + + def get_user_list(db: Session, page_object: UserPageObject): """ 根据查询参数获取用户列表信息 @@ -75,32 +118,70 @@ def get_user_list(db: Session, page_object: UserPageObject): :return: 用户列表信息对象 """ offset = (page_object.page_num - 1) * page_object.page_size - user_list = db.query(SysUser) \ + user_list = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, - SysUser.sex == page_object.sex if page_object.sex else True + SysUser.status == page_object.status if page_object.status else True, + SysUser.sex == page_object.sex if page_object.sex else True, + SysUser.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True ) \ + .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ .offset(offset) \ .limit(page_object.page_size) \ .distinct().all() - count = db.query(SysUser) \ + count = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, - SysUser.sex == page_object.sex if page_object.sex else True + SysUser.status == page_object.status if page_object.status else True, + SysUser.sex == page_object.sex if page_object.sex else True, + SysUser.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True ) \ + .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ .distinct().count() + result = [] + if user_list: + for item in user_list: + obj = dict( + user_id=item[0].user_id, + dept_id=item[0].dept_id, + dept_name=item[1].dept_name, + user_name=item[0].user_name, + nick_name=item[0].nick_name, + user_type=item[0].user_type, + email=item[0].email, + phonenumber=item[0].phonenumber, + sex=item[0].sex, + avatar=item[0].avatar, + status=item[0].status, + del_flag=item[0].del_flag, + login_ip=item[0].login_ip, + login_date=item[0].login_date, + create_by=item[0].create_by, + create_time=item[0].create_time, + update_by=item[0].update_by, + update_time=item[0].update_time, + remark=item[0].remark + ) + result.append(obj) + page_info = get_page_info(offset, page_object.page_num, page_object.page_size, count) result = dict( - rows=list_format_datetime(user_list), + rows=format_datetime_dict_list(result), page_num=page_info.page_num, page_size=page_info.page_size, total=page_info.total, @@ -146,8 +227,8 @@ def edit_user_crud(db: Session, user: UserModel): else: # 筛选出属性值为不为None和''的 filtered_dict = {k: v for k, v in user.dict().items() if v is not None and v != ''} - db.query(SysUser)\ - .filter(SysUser.user_id == user.user_id)\ + db.query(SysUser) \ + .filter(SysUser.user_id == user.user_id) \ .update(filtered_dict) db.commit() # 提交保存到数据库中 result = dict(is_success=True, message='更新成功') @@ -164,7 +245,7 @@ def delete_user_crud(db: Session, user: UserModel): """ db.query(SysUser) \ .filter(SysUser.user_id == user.user_id) \ - .delete() + .update({SysUser.del_flag: '2', SysUser.update_by: user.update_by, SysUser.update_time: user.update_time}) db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/mapper/schema/dept_schema.py b/dash-fastapi-backend/mapper/schema/dept_schema.py new file mode 100644 index 0000000..e24d2b3 --- /dev/null +++ b/dash-fastapi-backend/mapper/schema/dept_schema.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from typing import Union, Optional +from mapper.schema.user_schema import DeptModel + + +class DeptPageObject(DeptModel): + """ + 部门管理分页查询模型 + """ + page_num: Optional[int] + page_size: Optional[int] + + +class DeptTree(BaseModel): + """ + 部门树响应模型 + """ + dept_tree: list diff --git a/dash-fastapi-backend/mapper/schema/post_schema.py b/dash-fastapi-backend/mapper/schema/post_schema.py new file mode 100644 index 0000000..8ddde63 --- /dev/null +++ b/dash-fastapi-backend/mapper/schema/post_schema.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from typing import Union, Optional +from mapper.schema.user_schema import PostModel + + +class PostPageObject(PostModel): + """ + 岗位管理分页查询模型 + """ + page_num: Optional[int] + page_size: Optional[int] + + +class PostSelectOptionResponseModel(BaseModel): + """ + 岗位管理不分页查询模型 + """ + post: list[PostModel] diff --git a/dash-fastapi-backend/mapper/schema/role_schema.py b/dash-fastapi-backend/mapper/schema/role_schema.py new file mode 100644 index 0000000..46f3c97 --- /dev/null +++ b/dash-fastapi-backend/mapper/schema/role_schema.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from typing import Union, Optional +from mapper.schema.user_schema import RoleModel + + +class RolePageObject(RoleModel): + """ + 角色管理分页查询模型 + """ + page_num: Optional[int] + page_size: Optional[int] + + +class RoleSelectOptionResponseModel(BaseModel): + """ + 角色管理不分页查询模型 + """ + role: list[RoleModel] diff --git a/dash-fastapi-backend/mapper/schema/user_schema.py b/dash-fastapi-backend/mapper/schema/user_schema.py index d62d632..6fa55bb 100644 --- a/dash-fastapi-backend/mapper/schema/user_schema.py +++ b/dash-fastapi-backend/mapper/schema/user_schema.py @@ -1,6 +1,5 @@ from pydantic import BaseModel from typing import Union, Optional -from datetime import datetime class TokenData(BaseModel): @@ -157,15 +156,42 @@ class UserPageObject(UserModel): """ 用户管理分页查询模型 """ + create_time_start: Optional[str] + create_time_end: Optional[str] page_num: int page_size: int +class UserInfoJoinDept(BaseModel): + """ + 数据库查询用户列表返回模型 + """ + user_id: Optional[int] + dept_id: Optional[int] + dept_name: Optional[str] + user_name: Optional[str] + nick_name: Optional[str] + user_type: Optional[str] + email: Optional[str] + phonenumber: Optional[str] + sex: Optional[str] + avatar: Optional[str] + status: Optional[str] + del_flag: Optional[str] + login_ip: Optional[str] + login_date: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class UserPageObjectResponse(BaseModel): """ 用户管理列表分页查询返回模型 """ - rows: list[UserModel] = [] + rows: list[UserInfoJoinDept] = [] page_num: int page_size: int total: int @@ -185,6 +211,8 @@ class DeleteUserModel(BaseModel): 删除用户模型 """ user_ids: str + update_by: Optional[str] + update_time: Optional[str] class CrudUserResponse(BaseModel): diff --git a/dash-fastapi-backend/service/dept_service.py b/dash-fastapi-backend/service/dept_service.py new file mode 100644 index 0000000..a49c4a9 --- /dev/null +++ b/dash-fastapi-backend/service/dept_service.py @@ -0,0 +1,41 @@ +from mapper.schema.dept_schema import * +from mapper.crud.dept_crud import * + + +def get_dept_tree_services(result_db: Session, page_object: DeptPageObject): + """ + 获取部门树信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 部门树信息对象 + """ + dept_list_result = get_dept_list_for_tree(result_db, page_object) + dept_tree_result = get_dept_tree(0, DeptTree(dept_tree=dept_list_result)) + + return dept_tree_result + + +def get_dept_tree(pid: int, permission_list: DeptTree): + """ + 工具方法:根据部门信息生成树形嵌套数据 + :param pid: 部门id + :param permission_list: 部门列表信息 + :return: 部门树形嵌套数据 + """ + dept_list = [] + for permission in permission_list.dept_tree: + if permission.parent_id == pid: + children = get_dept_tree(permission.dept_id, permission_list) + dept_list_data = {} + if children: + dept_list_data['title'] = permission.dept_name + dept_list_data['key'] = str(permission.dept_id) + dept_list_data['value'] = permission.dept_id + dept_list_data['children'] = children + else: + dept_list_data['title'] = permission.dept_name + dept_list_data['key'] = str(permission.dept_id) + dept_list_data['value'] = permission.dept_id + dept_list.append(dept_list_data) + + return dept_list diff --git a/dash-fastapi-backend/service/post_service.py b/dash-fastapi-backend/service/post_service.py new file mode 100644 index 0000000..b8e5e75 --- /dev/null +++ b/dash-fastapi-backend/service/post_service.py @@ -0,0 +1,13 @@ +from mapper.schema.post_schema import * +from mapper.crud.post_crud import * + + +def get_post_select_option_services(result_db: Session): + """ + 获取岗位列表不分页信息service + :param result_db: orm对象 + :return: 岗位列表不分页信息对象 + """ + post_list_result = get_post_select_option_crud(result_db) + + return post_list_result diff --git a/dash-fastapi-backend/service/role_service.py b/dash-fastapi-backend/service/role_service.py new file mode 100644 index 0000000..a442776 --- /dev/null +++ b/dash-fastapi-backend/service/role_service.py @@ -0,0 +1,13 @@ +from mapper.schema.role_schema import * +from mapper.crud.role_crud import * + + +def get_role_select_option_services(result_db: Session): + """ + 获取角色列表不分页信息service + :param result_db: orm对象 + :return: 角色列表不分页信息对象 + """ + role_list_result = get_role_select_option_crud(result_db) + + return role_list_result diff --git a/dash-fastapi-backend/service/user_service.py b/dash-fastapi-backend/service/user_service.py index b2ffb54..e36f6e7 100644 --- a/dash-fastapi-backend/service/user_service.py +++ b/dash-fastapi-backend/service/user_service.py @@ -76,7 +76,7 @@ def delete_user_services(result_db: Session, page_object: DeleteUserModel): if page_object.user_ids.split(','): user_id_list = page_object.user_ids.split(',') for user_id in user_id_list: - user_id_dict = dict(user_id=user_id) + user_id_dict = dict(user_id=user_id, update_by=page_object.update_by, update_time=page_object.update_time) delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) delete_user_crud(result_db, UserModel(**user_id_dict)) @@ -93,7 +93,7 @@ def detail_user_services(result_db: Session, user_id: int): :param user_id: 用户id :return: 用户id对应的信息 """ - user = get_user_by_id(result_db, user_id=user_id) + user = get_user_detail_by_id(result_db, user_id=user_id) return UserDetailModel( user=user.user_basic_info[0], diff --git a/dash-fastapi-backend/utils/time_format_tool.py b/dash-fastapi-backend/utils/time_format_tool.py index a994b85..ba19489 100644 --- a/dash-fastapi-backend/utils/time_format_tool.py +++ b/dash-fastapi-backend/utils/time_format_tool.py @@ -24,3 +24,28 @@ def list_format_datetime(lst): if isinstance(value, datetime.datetime): setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S')) return lst + + +def format_datetime_dict_list(dicts): + """ + 递归遍历嵌套字典,并将 datetime 值转换为字符串格式 + :param dicts: 输入一个嵌套字典的列表 + :return: 对目标列表中所有字典的datetime类型的属性格式化 + """ + result = [] + + for item in dicts: + new_item = {} + for k, v in item.items(): + if isinstance(v, dict): + # 递归遍历子字典 + new_item[k] = format_datetime_dict_list([v])[0] + elif isinstance(v, datetime.datetime): + # 如果值是 datetime 类型,则格式化为字符串 + new_item[k] = v.strftime('%Y-%m-%d %H:%M:%S') + else: + # 否则保留原始值 + new_item[k] = v + result.append(new_item) + + return result diff --git a/dash-fastapi-frontend/api/dept.py b/dash-fastapi-frontend/api/dept.py new file mode 100644 index 0000000..dc16967 --- /dev/null +++ b/dash-fastapi-frontend/api/dept.py @@ -0,0 +1,6 @@ +from utils.request import api_request + + +def get_dept_tree_api(page_obj: dict): + + return api_request(method='post', url='/system/dept/tree', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/api/post.py b/dash-fastapi-frontend/api/post.py new file mode 100644 index 0000000..c5eef3e --- /dev/null +++ b/dash-fastapi-frontend/api/post.py @@ -0,0 +1,6 @@ +from utils.request import api_request + + +def get_post_select_option_api(): + + return api_request(method='post', url='/system/post/forSelectOption', is_headers=True) diff --git a/dash-fastapi-frontend/api/role.py b/dash-fastapi-frontend/api/role.py new file mode 100644 index 0000000..084edce --- /dev/null +++ b/dash-fastapi-frontend/api/role.py @@ -0,0 +1,6 @@ +from utils.request import api_request + + +def get_role_select_option_api(): + + return api_request(method='post', url='/system/role/forSelectOption', is_headers=True) diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index 2f2b906..abf759c 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -54,7 +54,9 @@ app.layout = html.Div( ), # 注入全局消息提示容器 - html.Div(id='global-message-container') + html.Div(id='global-message-container'), + # 注入全局通知信息容器 + html.Div(id='global-notification-container') ] ) diff --git a/dash-fastapi-frontend/callbacks/__init__.py b/dash-fastapi-frontend/callbacks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dash-fastapi-frontend/callbacks/app_c.py b/dash-fastapi-frontend/callbacks/app_c.py index 5358d3b..a8e8363 100644 --- a/dash-fastapi-frontend/callbacks/app_c.py +++ b/dash-fastapi-frontend/callbacks/app_c.py @@ -10,7 +10,7 @@ from server import app, logger # api拦截器——根据api返回编码确定是否强制退出 @app.callback( [Output('token-invalid-modal', 'visible'), - Output('global-message-container', 'children', allow_duplicate=True)], + Output('global-notification-container', 'children', allow_duplicate=True)], Input('api-check-token', 'data'), prevent_initial_call=True ) diff --git a/dash-fastapi-frontend/callbacks/layout_c/__init__.py b/dash-fastapi-frontend/callbacks/layout_c/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 0d1d63e..15ec34b 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -48,17 +48,39 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act ] menu_title = find_title_by_key(session.get('menu_info'), currentKey) + # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 menu_modules = find_modules_by_key(session.get('menu_info'), currentKey) - # 否则追加子项返回 - # 其中若各标签页内元素类似,则推荐配合模式匹配构建交互逻辑 + if menu_modules: + # 否则追加子项返回 + # 其中若各标签页内元素类似,则推荐配合模式匹配构建交互逻辑 + return [ + [ + *origin_items, + { + 'label': menu_title, + 'key': currentKey, + 'children': eval('views.' + menu_modules + '.render()'), + } + ], + currentKey + ] + return [ [ *origin_items, { 'label': menu_title, 'key': currentKey, - 'children': eval('views.' + menu_modules + '.render()'), + 'children': fac.AntdResult( + status='404', + title='页面不存在', + subTitle='请先配置该路由的页面', + style={ + 'paddingBottom': 0, + 'paddingTop': 0 + } + ), } ], currentKey diff --git a/dash-fastapi-frontend/callbacks/login_c.py b/dash-fastapi-frontend/callbacks/login_c.py index 40e9357..a3ac415 100644 --- a/dash-fastapi-frontend/callbacks/login_c.py +++ b/dash-fastapi-frontend/callbacks/login_c.py @@ -18,7 +18,6 @@ from api.login import login_api Output('login-password-form-item', 'help'), Output('login-captcha-form-item', 'help'), Output('login-captcha', 'refresh'), - Output('login-submit', 'children'), Output('login-submit', 'loading'), Output('redirect-container', 'children', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], @@ -50,7 +49,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): None, None, True, - '登录中', True, dcc.Location( pathname='/', @@ -69,7 +67,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): None, None, True, - '登录', False, None, None @@ -85,7 +82,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): '密码错误', None, True, - '登录', False, None, None @@ -101,7 +97,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): None, None, True, - '登录', False, None, fuc.FefferyFancyMessage(userinfo_result['message'], type='error'), @@ -116,7 +111,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): None, None, True, - '登录', False, None, fuc.FefferyFancyMessage('接口异常', type='error'), @@ -131,7 +125,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): None, '验证码错误!', True, - '登录', False, None, None @@ -145,7 +138,6 @@ def login_auth(nClicks, username, password, captcha, input_captcha): None if password else '请输入密码!', None if input_captcha else '请输入验证码!', True, - '登录', False, None, None diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py new file mode 100644 index 0000000..58a11c3 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -0,0 +1,447 @@ +import dash +import time +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc +from jsonpath_ng import parse +from flask import session, json +from collections import OrderedDict + +from server import app +from api.dept import get_dept_tree_api +from api.user import get_user_list_api, get_user_detail_api, add_user_api, edit_user_api, delete_user_api +from api.role import get_role_select_option_api +from api.post import get_post_select_option_api + + +@app.callback( + [Output('dept-tree', 'treeData'), + Output('api-check-token', 'data', allow_duplicate=True)], + Input('dept-input-search', 'value'), + prevent_initial_call=True +) +def get_search_dept_tree(dept_input): + dept_params = dict(dept_name=dept_input) + tree_info = get_dept_tree_api(dept_params) + if tree_info['code'] == 200: + tree_data = tree_info['data'] + + return [tree_data, {'timestamp': time.time()}] + + return [dash.no_update, {'timestamp': time.time()}] + + +@app.callback( + [Output('user-list-table', 'data', allow_duplicate=True), + Output('user-list-table', 'pagination', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dept-tree', 'selectedKeys'), + Input('user-search', 'nClicks'), + Input('user-list-table', 'pagination'), + Input('operations-store', 'data')], + [State('user-user_name-input', 'value'), + State('user-phone_number-input', 'value'), + State('user-status-select', 'value'), + State('user-create_time-range', 'value')], + prevent_initial_call=True +) +def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, pagination, operations, + user_name, phone_number, status_select, create_time_range): + dept_id = None + create_time_start = None + create_time_end = None + if create_time_range: + create_time_start = create_time_range[0] + create_time_end = create_time_range[1] + if selected_dept_tree: + dept_id = int(selected_dept_tree[0]) + query_params = dict( + dept_id=dept_id, + user_name=user_name, + phonenumber=phone_number, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + dept_id=dept_id, + user_name=user_name, + phonenumber=phone_number, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if selected_dept_tree or search_click or pagination or operations: + table_info = get_user_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(checked=True) + else: + item['status'] = dict(checked=False) + item['key'] = str(item['user_id']) + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + }, + { + 'title': '删除', + 'icon': 'antd-delete' + }, + { + 'title': '重置密码', + 'icon': 'antd-key' + } + ] + + return [table_data, table_pagination, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return dash.no_update + + +@app.callback( + [Output('dept-tree', 'selectedKeys'), + Output('user-user_name-input', 'value'), + Output('user-phone_number-input', 'value'), + Output('user-status-select', 'value'), + Output('user-create_time-range', 'value')], + Input('user-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_user_query_params(reset_click): + if reset_click: + return [None, None, None, None, None] + + return dash.no_update + + +@app.callback( + [Output('user-edit', 'disabled'), + Output('user-delete', 'disabled')], + Input('user-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return dash.no_update + + +@app.callback( + [Output('user-add-modal', 'visible'), + Output('user-add-dept_id', 'treeData'), + Output('user-add-post', 'options'), + Output('user-add-role', 'options'), + Output('api-check-token', 'data', allow_duplicate=True)], + Input('user-add', 'nClicks'), + prevent_initial_call=True +) +def add_user_modal(add_click): + if add_click: + dept_params = dict(dept_name='') + tree_info = get_dept_tree_api(dept_params) + post_option_info = get_post_select_option_api() + role_option_info = get_role_select_option_api() + if tree_info['code'] == 200 and post_option_info['code'] == 200 and role_option_info['code'] == 200: + tree_data = tree_info['data'] + post_option = post_option_info['data'] + role_option = role_option_info['data'] + + return [ + True, + tree_data, + [dict(label=item['post_name'], value=item['post_id']) for item in post_option], + [dict(label=item['role_name'], value=item['role_id']) for item in role_option], + {'timestamp': time.time()} + ] + + return [dash.no_update] * 4 + [{'timestamp': time.time()}] + + return dash.no_update + + +@app.callback( + [Output('user-add-nick_name-form-item', 'validateStatus'), + Output('user-add-user_name-form-item', 'validateStatus'), + Output('user-add-password-form-item', 'validateStatus'), + Output('user-add-nick_name-form-item', 'help'), + Output('user-add-user_name-form-item', 'help'), + Output('user-add-password-form-item', 'help'), + Output('operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('user-add-modal', 'okCounts'), + [State('user-add-nick_name', 'value'), + State('user-add-dept_id', 'value'), + State('user-add-phone_number', 'value'), + State('user-add-email', 'value'), + State('user-add-user_name', 'value'), + State('user-add-password', 'value'), + State('user-add-sex', 'value'), + State('user-add-status', 'value'), + State('user-add-post', 'value'), + State('user-add-role', 'value'), + State('user-add-remark', 'value')], + prevent_initial_call=True +) +def usr_add_confirm(add_confirm, nick_name, dept_id, phone_number, email, user_name, password, sex, status, post, role, + remark): + if add_confirm: + + if all([nick_name, user_name, password]): + params = dict(nick_name=nick_name, dept_id=dept_id, phonenumber=phone_number, + email=email, user_name=user_name, password=password, sex=sex, + status=status, post_id=','.join(map(str, post)), role_id=','.join(map(str, role)), + remark=remark) + add_button_result = add_user_api(params) + + if add_button_result['code'] == 200: + return [ + None, + None, + None, + None, + None, + None, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增失败', type='error') + ] + + return [ + None if nick_name else 'error', + None if user_name else 'error', + None if password else 'error', + None if nick_name else '请输入用户昵称!', + None if user_name else '请输入用户名称!', + None if password else '请输入用户密码!', + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增失败', type='error') + ] + + return dash.no_update + + +@app.callback( + [Output('user-edit-modal', 'visible'), + Output('user-edit-dept_id', 'treeData'), + Output('user-edit-post', 'options'), + Output('user-edit-role', 'options'), + Output('user-edit-nick_name', 'value'), + Output('user-edit-dept_id', 'value'), + Output('user-edit-phone_number', 'value'), + Output('user-edit-email', 'value'), + Output('user-edit-sex', 'value'), + Output('user-edit-status', 'value'), + Output('user-edit-post', 'value'), + Output('user-edit-role', 'value'), + Output('user-edit-remark', 'value'), + Output('edit-id-store', 'data'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('user-edit', 'nClicks'), + Input('user-list-table', 'nClicksDropdownItem')], + [State('user-list-table', 'selectedRowKeys'), + State('user-list-table', 'recentlyClickedDropdownItemTitle'), + State('user-list-table', 'recentlyDropdownItemClickedRow')], + prevent_initial_call=True +) +def user_edit_modal(edit_click, dropdown_click, + selected_row_keys, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + if edit_click or dropdown_click: + trigger_id = dash.ctx.triggered_id + + dept_params = dict(dept_name='') + tree_data = get_dept_tree_api(dept_params)['data'] + post_option = get_post_select_option_api()['data'] + role_option = get_role_select_option_api()['data'] + + if trigger_id == 'user-edit': + user_id = int(selected_row_keys[0]) + else: + if recently_clicked_dropdown_item_title == '修改': + user_id = int(recently_dropdown_item_clicked_row['key']) + else: + return dash.no_update + + edit_button_info = get_user_detail_api(user_id) + if edit_button_info['code'] == 200: + edit_button_result = edit_button_info['data'] + user = edit_button_result['user'] + dept = edit_button_result['dept'] + role = edit_button_result['role'] + post = edit_button_result['post'] + + return [ + True, + tree_data, + [dict(label=item['post_name'], value=item['post_id']) for item in post_option], + [dict(label=item['role_name'], value=item['role_id']) for item in role_option], + user['nick_name'], + dept['dept_id'], + user['phonenumber'], + user['email'], + user['sex'], + user['status'], + [item['post_id'] for item in post], + [item['role_id'] for item in role], + user['remark'], + {'user_id': user_id}, + {'timestamp': time.time()} + ] + + return [dash.no_update] * 14 + [{'timestamp': time.time()}] + + return dash.no_update + + +@app.callback( + [Output('user-edit-nick_name-form-item', 'validateStatus'), + Output('user-edit-nick_name-form-item', 'help'), + Output('operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('user-edit-modal', 'okCounts'), + [State('user-edit-nick_name', 'value'), + State('user-edit-dept_id', 'value'), + State('user-edit-phone_number', 'value'), + State('user-edit-email', 'value'), + State('user-edit-sex', 'value'), + State('user-edit-status', 'value'), + State('user-edit-post', 'value'), + State('user-edit-role', 'value'), + State('user-edit-remark', 'value'), + State('edit-id-store', 'data')], + prevent_initial_call=True +) +def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, status, post, role, remark, user_id): + if edit_confirm: + + if all([nick_name]): + params = dict(user_id=user_id['user_id'], nick_name=nick_name, dept_id=dept_id, phonenumber=phone_number, + email=email, sex=sex, status=status, post_id=','.join(map(str, post)), + role_id=','.join(map(str, role)), remark=remark) + edit_button_result = edit_user_api(params) + + if edit_button_result['code'] == 200: + return [ + None, + None, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑失败', type='error') + ] + + return [ + None if nick_name else 'error', + None if nick_name else '请输入用户昵称!', + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑失败', type='error') + ] + + return dash.no_update + + +@app.callback( + [Output('delete-text', 'children'), + Output('user-delete-confirm-modal', 'visible'), + Output('delete-ids-store', 'data')], + [Input('user-delete', 'nClicks'), + Input('user-list-table', 'nClicksDropdownItem')], + [State('user-list-table', 'selectedRowKeys'), + State('user-list-table', 'recentlyClickedDropdownItemTitle'), + State('user-list-table', 'recentlyDropdownItemClickedRow')], + prevent_initial_call=True +) +def user_delete_modal(delete_click, dropdown_click, + selected_row_keys, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + if delete_click or dropdown_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'user-delete': + user_ids = ','.join(selected_row_keys) + else: + if recently_clicked_dropdown_item_title == '删除': + user_ids = recently_dropdown_item_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除user_id为{user_ids}的用户?', + True, + {'user_ids': user_ids} + ] + + return dash.no_update + + +@app.callback( + [Output('operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('user-delete-confirm-modal', 'okCounts'), + State('delete-ids-store', 'data'), + prevent_initial_call=True +) +def user_delete_confirm(delete_confirm, user_ids_data): + if delete_confirm: + + params = user_ids_data + delete_button_info = delete_user_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return dash.no_update diff --git a/dash-fastapi-frontend/views/layout/index.py b/dash-fastapi-frontend/views/layout/index.py index be8c18e..c2ab0e6 100644 --- a/dash-fastapi-frontend/views/layout/index.py +++ b/dash-fastapi-frontend/views/layout/index.py @@ -134,6 +134,6 @@ def render_content(user_name, nick_name, phone_number, menu_info): ), listenPropsMode='include', includeProps=[ - 'tabs-test.children' + 'tabs-container.items' ] ) diff --git a/dash-fastapi-frontend/views/login.py b/dash-fastapi-frontend/views/login.py index 99eb95f..6149011 100644 --- a/dash-fastapi-frontend/views/login.py +++ b/dash-fastapi-frontend/views/login.py @@ -128,6 +128,8 @@ def render_content(): '登录', id='login-submit', type='primary', + loadingChildren='登录中', + autoSpin=True, block=True, size='large', ), diff --git a/dash-fastapi-frontend/views/system/user/index.py b/dash-fastapi-frontend/views/system/user/index.py index 3d1c7af..dbb96a6 100644 --- a/dash-fastapi-frontend/views/system/user/index.py +++ b/dash-fastapi-frontend/views/system/user/index.py @@ -1,8 +1,806 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc import feffery_antd_components as fac +import callbacks.system_c.user_c +from api.user import get_user_list_api +from api.dept import get_dept_tree_api + def render(): + dept_params = dict(dept_name='') + user_params = dict(page_num=1, page_size=10) + tree_info = get_dept_tree_api(dept_params) + table_info = get_user_list_api(user_params) + tree_data = [] + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if tree_info['code'] == 200: + tree_data = tree_info['data'] + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(checked=True) + else: + item['status'] = dict(checked=False) + item['key'] = str(item['user_id']) + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + }, + { + 'title': '删除', + 'icon': 'antd-delete' + }, + { + 'title': '重置密码', + 'icon': 'antd-key' + } + ] + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdInput( + id='dept-input-search', + placeholder='请输入部门名称', + prefix=fac.AntdIcon( + icon='antd-search' + ), + style={ + 'width': '85%' + } + ), + fac.AntdTree( + id='dept-tree', + treeData=tree_data, + defaultExpandAll=True, + showLine=False, + style={ + 'margin-top': '10px' + } + ) + ], + span=4 + ), + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-user_name-input', + placeholder='请输入用户名称', + autoComplete='off', + style={ + 'width': 240 + } + ), + label='用户名称' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-phone_number-input', + placeholder='请输入手机号码', + autoComplete='off', + style={ + 'width': 240 + } + ), + label='手机号码' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-status-select', + placeholder='用户状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='用户状态' + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='user-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='user-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='user-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='user-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='user-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='user-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-up' + ), + '导入', + ], + id='user-import', + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='user-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='user-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'user_id', + 'title': '用户编号', + 'width': 100, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'user_name', + 'title': '用户名称', + 'width': 120, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'nick_name', + 'title': '用户昵称', + 'width': 120, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dept_name', + 'title': '部门', + 'width': 130, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'phonenumber', + 'title': '手机号码', + 'width': 130, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'width': 110, + 'renderOptions': { + 'renderType': 'switch' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'width': 160, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'dropdown', + 'dropdownProps': { + 'title': '更多' + } + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=20 + ) + ], + gutter=5 + ), + + # 新增用户表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-nick_name', + placeholder='请输入用户昵称', + style={ + 'width': 200 + } + ), + label='用户昵称', + required=True, + id='user-add-nick_name-form-item' + ), + fac.AntdFormItem( + fac.AntdTreeSelect( + id='user-add-dept_id', + placeholder='请选择归属部门', + treeData=[], + style={ + 'width': 200 + } + ), + label='归属部门', + id='user-add-dept_id-form-item', + labelCol={ + 'offset': 1 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-phone_number', + placeholder='请输入手机号码', + style={ + 'width': 200 + } + ), + label='手机号码', + id='user-add-phone_number-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-add-email', + placeholder='请输入邮箱', + style={ + 'width': 200 + } + ), + label='邮箱', + id='user-add-email-form-item', + labelCol={ + 'offset': 5 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-user_name', + placeholder='请输入用户名称', + style={ + 'width': 200 + } + ), + label='用户名称', + required=True, + id='user-add-user_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-add-password', + placeholder='请输入密码', + mode='password', + passwordUseMd5=True, + style={ + 'width': 200 + } + ), + label='用户密码', + required=True, + id='user-add-password-form-item' + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-sex', + placeholder='请选择性别', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + }, + { + 'label': '未知', + 'value': '2' + }, + ], + style={ + 'width': 200 + } + ), + label='用户性别', + id='user-add-sex-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='user-add-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label='用户状态', + id='user-add-status-form-item', + labelCol={ + 'offset': 2 + }, + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-post', + placeholder='请选择岗位', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-add-post-form-item', + labelCol={ + 'offset': 4 + }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-role', + placeholder='请选择角色', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-add-role-form-item', + labelCol={ + 'offset': 8 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-remark', + placeholder='请输入内容', + mode='text-area', + style={ + 'width': 490 + } + ), + label='备注', + id='user-add-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ] + ) + ], + id='user-add-modal', + title='新增用户', + mask=False, + width=650, + renderFooter=True + ), + + # 编辑用户表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-nick_name', + placeholder='请输入用户昵称', + style={ + 'width': 200 + } + ), + label='用户昵称', + required=True, + id='user-edit-nick_name-form-item' + ), + fac.AntdFormItem( + fac.AntdTreeSelect( + id='user-edit-dept_id', + placeholder='请选择归属部门', + treeData=[], + style={ + 'width': 200 + } + ), + label='归属部门', + id='user-edit-dept_id-form-item' + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-phone_number', + placeholder='请输入手机号码', + style={ + 'width': 200 + } + ), + label='手机号码', + id='user-edit-phone_number-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-email', + placeholder='请输入邮箱', + style={ + 'width': 200 + } + ), + label='邮箱', + id='user-edit-email-form-item', + labelCol={ + 'offset': 4 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-sex', + placeholder='请选择性别', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + }, + { + 'label': '未知', + 'value': '2' + }, + ], + style={ + 'width': 200 + } + ), + label='用户性别', + id='user-edit-sex-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='user-edit-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + style={ + 'width': 200 + } + ), + label='用户状态', + id='user-edit-status-form-item', + labelCol={ + 'offset': 1 + }, + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-post', + placeholder='请选择岗位', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-edit-post-form-item', + labelCol={ + 'offset': 4 + }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-role', + placeholder='请选择角色', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-edit-role-form-item', + labelCol={ + 'offset': 7 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-remark', + placeholder='请输入内容', + mode='text-area', + style={ + 'width': 485 + } + ), + label='备注', + id='user-edit-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ] + ) + ], + id='user-edit-modal', + title='编辑用户', + mask=False, + width=650, + renderFooter=True + ), + + # 删除用户二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='delete-text'), + id='user-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + + # 重置密码modal + fac.AntdModal( + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-password-input', + mode='password' + ), + ), + ], + layout='vertical' + ), + id='user-reset-password-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), - return html.Div('我是用户管理') + dcc.Store(id='operations-store'), + dcc.Store(id='edit-id-store'), + dcc.Store(id='delete-ids-store') + ] -- Gitee From 99b079fedb66f064ce6c59d1015e07b5ae8234bc Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 31 May 2023 11:11:21 +0800 Subject: [PATCH 003/169] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=E5=B7=B2=E7=9F=A5?= =?UTF-8?q?bug=20feat:=E6=96=B0=E5=A2=9E=E5=90=8E=E7=AB=AF=E9=80=80?= =?UTF-8?q?=E5=87=BAapi=EF=BC=8C=E5=AE=9E=E7=8E=B0=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=9D=87=E9=80=80=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/login_controller.py | 16 ++++ dash-fastapi-backend/mapper/crud/user_crud.py | 18 ++-- .../mapper/schema/dept_schema.py | 4 +- .../mapper/schema/post_schema.py | 4 +- .../mapper/schema/role_schema.py | 4 +- .../mapper/schema/user_schema.py | 28 +++--- dash-fastapi-backend/service/login_service.py | 13 +++ dash-fastapi-backend/utils/page_tool.py | 20 ++-- dash-fastapi-frontend/api/login.py | 4 + .../callbacks/layout_c/head_c.py | 19 ++-- .../callbacks/system_c/user_c.py | 91 ++++++++++++++----- .../views/system/user/index.py | 45 ++------- 12 files changed, 162 insertions(+), 104 deletions(-) diff --git a/dash-fastapi-backend/controller/login_controller.py b/dash-fastapi-backend/controller/login_controller.py index a1bab78..91d2848 100644 --- a/dash-fastapi-backend/controller/login_controller.py +++ b/dash-fastapi-backend/controller/login_controller.py @@ -64,3 +64,19 @@ async def get_login_user_info(request: Request, token: Optional[str] = Header(.. except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@loginController.post("/logout") +async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + await logout_services(request, current_user) + logger.info('退出成功') + return response_200(data=current_user, message="退出成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/mapper/crud/user_crud.py index 0a3838d..69fb43e 100644 --- a/dash-fastapi-backend/mapper/crud/user_crud.py +++ b/dash-fastapi-backend/mapper/crud/user_crud.py @@ -117,8 +117,7 @@ def get_user_list(db: Session, page_object: UserPageObject): :param page_object: 分页查询参数对象 :return: 用户列表信息对象 """ - offset = (page_object.page_num - 1) * page_object.page_size - user_list = db.query(SysUser, SysDept) \ + count = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, @@ -133,10 +132,10 @@ def get_user_list(db: Session, page_object: UserPageObject): if page_object.create_time_start and page_object.create_time_end else True ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ - .offset(offset) \ - .limit(page_object.page_size) \ - .distinct().all() - count = db.query(SysUser, SysDept) \ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + user_list = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, @@ -151,7 +150,9 @@ def get_user_list(db: Session, page_object: UserPageObject): if page_object.create_time_start and page_object.create_time_end else True ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ - .distinct().count() + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() result = [] if user_list: @@ -159,7 +160,7 @@ def get_user_list(db: Session, page_object: UserPageObject): obj = dict( user_id=item[0].user_id, dept_id=item[0].dept_id, - dept_name=item[1].dept_name, + dept_name=item[1].dept_name if item[1] else '', user_name=item[0].user_name, nick_name=item[0].nick_name, user_type=item[0].user_type, @@ -179,7 +180,6 @@ def get_user_list(db: Session, page_object: UserPageObject): ) result.append(obj) - page_info = get_page_info(offset, page_object.page_num, page_object.page_size, count) result = dict( rows=format_datetime_dict_list(result), page_num=page_info.page_num, diff --git a/dash-fastapi-backend/mapper/schema/dept_schema.py b/dash-fastapi-backend/mapper/schema/dept_schema.py index e24d2b3..03a69c9 100644 --- a/dash-fastapi-backend/mapper/schema/dept_schema.py +++ b/dash-fastapi-backend/mapper/schema/dept_schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Union, Optional +from typing import Union, Optional, List from mapper.schema.user_schema import DeptModel @@ -15,4 +15,4 @@ class DeptTree(BaseModel): """ 部门树响应模型 """ - dept_tree: list + dept_tree: Union[List, None] diff --git a/dash-fastapi-backend/mapper/schema/post_schema.py b/dash-fastapi-backend/mapper/schema/post_schema.py index 8ddde63..4a9064a 100644 --- a/dash-fastapi-backend/mapper/schema/post_schema.py +++ b/dash-fastapi-backend/mapper/schema/post_schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Union, Optional +from typing import Union, Optional, List from mapper.schema.user_schema import PostModel @@ -15,4 +15,4 @@ class PostSelectOptionResponseModel(BaseModel): """ 岗位管理不分页查询模型 """ - post: list[PostModel] + post: List[Union[PostModel, None]] diff --git a/dash-fastapi-backend/mapper/schema/role_schema.py b/dash-fastapi-backend/mapper/schema/role_schema.py index 46f3c97..4a088eb 100644 --- a/dash-fastapi-backend/mapper/schema/role_schema.py +++ b/dash-fastapi-backend/mapper/schema/role_schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Union, Optional +from typing import Union, Optional, List from mapper.schema.user_schema import RoleModel @@ -15,4 +15,4 @@ class RoleSelectOptionResponseModel(BaseModel): """ 角色管理不分页查询模型 """ - role: list[RoleModel] + role: List[Union[RoleModel, None]] diff --git a/dash-fastapi-backend/mapper/schema/user_schema.py b/dash-fastapi-backend/mapper/schema/user_schema.py index 6fa55bb..b0dcfe3 100644 --- a/dash-fastapi-backend/mapper/schema/user_schema.py +++ b/dash-fastapi-backend/mapper/schema/user_schema.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Union, Optional +from typing import Union, Optional, List class TokenData(BaseModel): @@ -128,28 +128,28 @@ class CurrentUserInfo(BaseModel): """ 数据库返回当前用户信息 """ - user_basic_info: list[UserModel] - user_dept_info: list[DeptModel] - user_role_info: list[RoleModel] - user_post_info: list[PostModel] - user_menu_info: list + user_basic_info: List[Union[UserModel, None]] + user_dept_info: List[Union[DeptModel, None]] + user_role_info: List[Union[RoleModel, None]] + user_post_info: List[Union[PostModel, None]] + user_menu_info: Union[List, None] class UserDetailModel(BaseModel): """ 获取用户详情信息响应模型 """ - user: UserModel - dept: DeptModel - role: list[RoleModel] - post: list[PostModel] + user: Union[UserModel, None] + dept: Union[DeptModel, None] + role: List[Union[RoleModel, None]] + post: List[Union[PostModel, None]] class CurrentUserInfoServiceResponse(UserDetailModel): """ 获取当前用户信息响应模型 """ - menu: list + menu: Union[List, None] class UserPageObject(UserModel): @@ -191,7 +191,7 @@ class UserPageObjectResponse(BaseModel): """ 用户管理列表分页查询返回模型 """ - rows: list[UserInfoJoinDept] = [] + rows: List[Union[UserInfoJoinDept, None]] = [] page_num: int page_size: int total: int @@ -236,11 +236,11 @@ class RoleInfo(BaseModel): """ 用户角色信息 """ - role_info: list + role_info: Union[List] class MenuList(BaseModel): """ 用户菜单信息 """ - menu_info: list + menu_info: Union[List] diff --git a/dash-fastapi-backend/service/login_service.py b/dash-fastapi-backend/service/login_service.py index f8763b5..ee67c66 100644 --- a/dash-fastapi-backend/service/login_service.py +++ b/dash-fastapi-backend/service/login_service.py @@ -56,6 +56,19 @@ async def get_current_user(request: Request, token: str, result_db: Session): return "用户token已失效,请重新登录" +async def logout_services(request: Request, current_user: CurrentUserInfoServiceResponse): + """ + 退出登录services + :param request: Request对象 + :param current_user: 用户用户 + :return: 退出登录结果 + """ + await request.app.state.redis.delete(f'{current_user.user.user_id}_access_token') + await request.app.state.redis.delete(f'{current_user.user.user_id}_session_id') + + return True + + def verify_password(plain_password, hashed_password): """ 工具方法:校验当前输入的密码与数据库存储的密码是否一致 diff --git a/dash-fastapi-backend/utils/page_tool.py b/dash-fastapi-backend/utils/page_tool.py index 0c93584..21ef67e 100644 --- a/dash-fastapi-backend/utils/page_tool.py +++ b/dash-fastapi-backend/utils/page_tool.py @@ -5,6 +5,7 @@ class PageModel(BaseModel): """ 分页模型 """ + offset: int page_num: int page_size: int total: int @@ -21,13 +22,20 @@ def get_page_info(offset: int, page_num: int, page_size: int, count: int): :return: 分页信息对象 """ has_next = False - res_page_num = 1 - if (offset + page_size) < count: - has_next = True - else: - if page_num > 1: + if offset >= count: + res_offset_1 = (page_num - 2) * page_size + if res_offset_1 < 0: + res_offset = 0 + res_page_num = 1 + else: + res_offset = res_offset_1 res_page_num = page_num - 1 + else: + res_offset = offset + if (res_offset + page_size) < count: + has_next = True + res_page_num = page_num - result = dict(page_num=res_page_num, page_size=page_size, total=count, has_next=has_next) + result = dict(offset=res_offset, page_num=res_page_num, page_size=page_size, total=count, has_next=has_next) return PageModel(**result) diff --git a/dash-fastapi-frontend/api/login.py b/dash-fastapi-frontend/api/login.py index 1752f39..0dcaf0d 100644 --- a/dash-fastapi-frontend/api/login.py +++ b/dash-fastapi-frontend/api/login.py @@ -9,3 +9,7 @@ def login_api(page_obj: dict): def get_current_user_info_api(): return api_request(method='post', url='/login/getLoginUserInfo', is_headers=True) + + +def logout_api(): + return api_request(method='post', url='/login/logout', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/layout_c/head_c.py b/dash-fastapi-frontend/callbacks/layout_c/head_c.py index 3c69622..f93b772 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/head_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/head_c.py @@ -4,6 +4,7 @@ from flask import session from dash.dependencies import Input, Output, State from server import app +from api.login import logout_api # 页首右侧个人中心选项卡回调 @@ -38,14 +39,16 @@ def index_dropdown_click(nClicks, clickedKey): ) def logout_confirm(okCounts): if okCounts: - session.clear() - - return [ - dcc.Location( - pathname='/login', - id='index-redirect' - ), - ] + result = logout_api() + if result['code'] == 200: + session.clear() + + return [ + dcc.Location( + pathname='/login', + id='index-redirect' + ), + ] return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index 58a11c3..26ad251 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -1,5 +1,6 @@ import dash import time +import uuid from dash import html from dash.dependencies import Input, Output, State import feffery_antd_components as fac @@ -35,6 +36,7 @@ def get_search_dept_tree(dept_input): @app.callback( [Output('user-list-table', 'data', allow_duplicate=True), Output('user-list-table', 'pagination', allow_duplicate=True), + Output('user-list-table', 'key'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('dept-tree', 'selectedKeys'), Input('user-search', 'nClicks'), @@ -110,11 +112,11 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio } ] - return [table_data, table_pagination, {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return dash.no_update + return [dash.no_update] * 4 @app.callback( @@ -130,7 +132,7 @@ def reset_user_query_params(reset_click): if reset_click: return [None, None, None, None, None] - return dash.no_update + return [dash.no_update] * 5 @app.callback( @@ -146,11 +148,11 @@ def change_edit_delete_button_status(table_rows_selected): return [False, False] - return dash.no_update + return [True, True] @app.callback( - [Output('user-add-modal', 'visible'), + [Output('user-add-modal', 'visible', allow_duplicate=True), Output('user-add-dept_id', 'treeData'), Output('user-add-post', 'options'), Output('user-add-role', 'options'), @@ -179,7 +181,7 @@ def add_user_modal(add_click): return [dash.no_update] * 4 + [{'timestamp': time.time()}] - return dash.no_update + return [dash.no_update] * 5 @app.callback( @@ -189,6 +191,7 @@ def add_user_modal(add_click): Output('user-add-nick_name-form-item', 'help'), Output('user-add-user_name-form-item', 'help'), Output('user-add-password-form-item', 'help'), + Output('user-add-modal', 'visible', allow_duplicate=True), Output('operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], @@ -225,6 +228,7 @@ def usr_add_confirm(add_confirm, nick_name, dept_id, phone_number, email, user_n None, None, None, + False, {'type': 'add'}, {'timestamp': time.time()}, fuc.FefferyFancyMessage('新增成功', type='success') @@ -238,6 +242,7 @@ def usr_add_confirm(add_confirm, nick_name, dept_id, phone_number, email, user_n None, None, dash.no_update, + dash.no_update, {'timestamp': time.time()}, fuc.FefferyFancyMessage('新增失败', type='error') ] @@ -250,15 +255,16 @@ def usr_add_confirm(add_confirm, nick_name, dept_id, phone_number, email, user_n None if user_name else '请输入用户名称!', None if password else '请输入用户密码!', dash.no_update, + dash.no_update, {'timestamp': time.time()}, fuc.FefferyFancyMessage('新增失败', type='error') ] - return dash.no_update + return [dash.no_update] * 10 @app.callback( - [Output('user-edit-modal', 'visible'), + [Output('user-edit-modal', 'visible', allow_duplicate=True), Output('user-edit-dept_id', 'treeData'), Output('user-edit-post', 'options'), Output('user-edit-role', 'options'), @@ -296,7 +302,7 @@ def user_edit_modal(edit_click, dropdown_click, if recently_clicked_dropdown_item_title == '修改': user_id = int(recently_dropdown_item_clicked_row['key']) else: - return dash.no_update + return [dash.no_update] * 15 edit_button_info = get_user_detail_api(user_id) if edit_button_info['code'] == 200: @@ -309,16 +315,16 @@ def user_edit_modal(edit_click, dropdown_click, return [ True, tree_data, - [dict(label=item['post_name'], value=item['post_id']) for item in post_option], - [dict(label=item['role_name'], value=item['role_id']) for item in role_option], + [dict(label=item['post_name'], value=item['post_id']) for item in post_option if item] or [], + [dict(label=item['role_name'], value=item['role_id']) for item in role_option if item] or [], user['nick_name'], - dept['dept_id'], + dept['dept_id'] if dept else None, user['phonenumber'], user['email'], user['sex'], user['status'], - [item['post_id'] for item in post], - [item['role_id'] for item in role], + [item['post_id'] for item in post if item] or [], + [item['role_id'] for item in role if item] or [], user['remark'], {'user_id': user_id}, {'timestamp': time.time()} @@ -326,12 +332,13 @@ def user_edit_modal(edit_click, dropdown_click, return [dash.no_update] * 14 + [{'timestamp': time.time()}] - return dash.no_update + return [dash.no_update] * 15 @app.callback( [Output('user-edit-nick_name-form-item', 'validateStatus'), Output('user-edit-nick_name-form-item', 'help'), + Output('user-edit-modal', 'visible', allow_duplicate=True), Output('operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], @@ -352,15 +359,16 @@ def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, if edit_confirm: if all([nick_name]): - params = dict(user_id=user_id['user_id'], nick_name=nick_name, dept_id=dept_id, phonenumber=phone_number, - email=email, sex=sex, status=status, post_id=','.join(map(str, post)), - role_id=','.join(map(str, role)), remark=remark) + params = dict(user_id=user_id['user_id'], nick_name=nick_name, dept_id=dept_id if dept_id else -1, + phonenumber=phone_number, email=email, sex=sex, status=status, + post_id=','.join(map(str, post)), role_id=','.join(map(str, role)), remark=remark) edit_button_result = edit_user_api(params) if edit_button_result['code'] == 200: return [ None, None, + False, {'type': 'edit'}, {'timestamp': time.time()}, fuc.FefferyFancyMessage('编辑成功', type='success') @@ -370,6 +378,7 @@ def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, None, None, dash.no_update, + dash.no_update, {'timestamp': time.time()}, fuc.FefferyFancyMessage('编辑失败', type='error') ] @@ -378,11 +387,45 @@ def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, None if nick_name else 'error', None if nick_name else '请输入用户昵称!', dash.no_update, + dash.no_update, {'timestamp': time.time()}, fuc.FefferyFancyMessage('编辑失败', type='error') ] - return dash.no_update + return [dash.no_update] * 6 + + +@app.callback( + [Output('operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + [Input('user-list-table', 'recentlySwitchDataIndex'), + Input('user-list-table', 'recentlySwitchStatus'), + Input('user-list-table', 'recentlySwitchRow')], + prevent_initial_call=True +) +def table_switch_user_status(recently_switch_data_index, recently_switch_status, recently_switch_row): + if recently_switch_data_index: + if recently_switch_status: + params = dict(user_id=int(recently_switch_row['key']), status='0') + else: + params = dict(user_id=int(recently_switch_row['key']), status='1') + edit_button_result = edit_user_api(params) + if edit_button_result['code'] == 200:\ + + return [ + {'type': 'switch-status'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error') + ] + + return [dash.no_update] * 3 @app.callback( @@ -407,15 +450,15 @@ def user_delete_modal(delete_click, dropdown_click, if recently_clicked_dropdown_item_title == '删除': user_ids = recently_dropdown_item_clicked_row['key'] else: - return dash.no_update + return [dash.no_update] * 3 return [ - f'是否确认删除user_id为{user_ids}的用户?', + f'是否确认删除用户编号为{user_ids}的用户?', True, {'user_ids': user_ids} ] - return dash.no_update + return [dash.no_update] * 3 @app.callback( @@ -444,4 +487,4 @@ def user_delete_confirm(delete_confirm, user_ids_data): fuc.FefferyFancyMessage('删除失败', type='error') ] - return dash.no_update + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/views/system/user/index.py b/dash-fastapi-frontend/views/system/user/index.py index dbb96a6..fc33151 100644 --- a/dash-fastapi-frontend/views/system/user/index.py +++ b/dash-fastapi-frontend/views/system/user/index.py @@ -8,41 +8,10 @@ from api.dept import get_dept_tree_api def render(): dept_params = dict(dept_name='') - user_params = dict(page_num=1, page_size=10) tree_info = get_dept_tree_api(dept_params) - table_info = get_user_list_api(user_params) tree_data = [] - table_data = [] - page_num = 1 - page_size = 10 - total = 0 if tree_info['code'] == 200: tree_data = tree_info['data'] - if table_info['code'] == 200: - table_data = table_info['data']['rows'] - page_num = table_info['data']['page_num'] - page_size = table_info['data']['page_size'] - total = table_info['data']['total'] - for item in table_data: - if item['status'] == '0': - item['status'] = dict(checked=True) - else: - item['status'] = dict(checked=False) - item['key'] = str(item['user_id']) - item['operation'] = [ - { - 'title': '修改', - 'icon': 'antd-edit' - }, - { - 'title': '删除', - 'icon': 'antd-delete' - }, - { - 'title': '重置密码', - 'icon': 'antd-key' - } - ] return [ fac.AntdRow( @@ -259,7 +228,7 @@ def render(): fac.AntdSpin( fac.AntdTable( id='user-list-table', - data=table_data, + data=[], columns=[ { 'dataIndex': 'user_id', @@ -332,12 +301,12 @@ def render(): rowSelectionWidth=50, bordered=True, pagination={ - 'pageSize': page_size, - 'current': page_num, + 'pageSize': 10, + 'current': 1, 'showSizeChanger': True, 'pageSizeOptions': [10, 30, 50, 100], 'showQuickJumper': True, - 'total': total + 'total': 0 }, mode='server-side', style={ @@ -578,7 +547,8 @@ def render(): title='新增用户', mask=False, width=650, - renderFooter=True + renderFooter=True, + okClickClose=False ), # 编辑用户表单modal @@ -767,7 +737,8 @@ def render(): title='编辑用户', mask=False, width=650, - renderFooter=True + renderFooter=True, + okClickClose=False ), # 删除用户二次确认modal -- Gitee From 237c31da88c2124a192fd30215a7658b16117f0e Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Fri, 2 Jun 2023 21:03:07 +0800 Subject: [PATCH 004/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=B2=97?= =?UTF-8?q?=E4=BD=8D=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88=E5=B7=B2?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89=EF=BC=8C=E6=96=B0=E5=A2=9E=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E5=88=97=E8=A1=A8=EF=BC=88=E5=A2=9E=E5=88=A0=E6=94=B9?= =?UTF-8?q?=E5=BE=85=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dept_controller.py | 18 +- .../controller/post_controler.py | 95 +++ dash-fastapi-backend/mapper/crud/dept_crud.py | 49 +- dash-fastapi-backend/mapper/crud/post_crud.py | 97 +++ dash-fastapi-backend/mapper/crud/user_crud.py | 6 +- .../mapper/schema/dept_schema.py | 22 +- .../mapper/schema/post_schema.py | 26 + dash-fastapi-backend/service/dept_service.py | 16 +- dash-fastapi-backend/service/post_service.py | 66 ++ dash-fastapi-backend/service/user_service.py | 2 +- dash-fastapi-frontend/api/post.py | 25 + .../callbacks/system_c/dept_c.py | 83 +++ .../callbacks/system_c/post_c.py | 373 ++++++++++ .../callbacks/system_c/user_c.py | 31 +- dash-fastapi-frontend/store/store.py | 22 +- dash-fastapi-frontend/utils/tree_tool.py | 26 + .../views/system/dept/index.py | 672 +++++++++++++++++- .../views/system/post/index.py | 471 +++++++++++- .../views/system/user/index.py | 45 +- 19 files changed, 2101 insertions(+), 44 deletions(-) create mode 100644 dash-fastapi-frontend/callbacks/system_c/dept_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/post_c.py diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/controller/dept_controller.py index 5adf226..2aee58b 100644 --- a/dash-fastapi-backend/controller/dept_controller.py +++ b/dash-fastapi-backend/controller/dept_controller.py @@ -13,7 +13,7 @@ deptController = APIRouter() @deptController.post("/dept/tree", response_model=DeptTree) -async def get_system_dept_tree(request: Request, dept_query: DeptPageObject, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_dept_tree(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": @@ -26,3 +26,19 @@ async def get_system_dept_tree(request: Request, dept_query: DeptPageObject, tok except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@deptController.post("/dept/get", response_model=DeptResponse) +async def get_system_dept_list(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + dept_query_result = get_dept_list_services(query_db, dept_query) + logger.info('获取成功') + return response_200(data=dept_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/post_controler.py b/dash-fastapi-backend/controller/post_controler.py index ce62f92..9f5d6a0 100644 --- a/dash-fastapi-backend/controller/post_controler.py +++ b/dash-fastapi-backend/controller/post_controler.py @@ -25,3 +25,98 @@ async def get_system_post_select(request: Request, token: Optional[str] = Header except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@postController.post("/post/get", response_model=PostPageObjectResponse) +async def get_system_post_list(request: Request, user_query: PostPageObject, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + post_query_result = get_post_list_services(query_db, user_query) + logger.info('获取成功') + return response_200(data=post_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@postController.post("/post/add", response_model=CrudPostResponse) +async def add_system_post(request: Request, add_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + add_post.create_by = current_user.user.user_name + add_post.update_by = current_user.user.user_name + add_post_result = add_post_services(query_db, add_post) + logger.info(add_post_result.message) + if add_post_result.is_success: + return response_200(data=add_post_result, message=add_post_result.message) + else: + return response_400(data="", message=add_post_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@postController.post("/post/edit", response_model=CrudPostResponse) +async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + edit_post.update_by = current_user.user.user_name + edit_post.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_post_result = edit_post_services(query_db, edit_post) + if edit_post_result.is_success: + logger.info(edit_post_result.message) + return response_200(data=edit_post_result, message=edit_post_result.message) + else: + logger.warning(edit_post_result.message) + return response_400(data="", message=edit_post_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@postController.post("/post/delete", response_model=CrudPostResponse) +async def delete_system_post(request: Request, delete_post: DeletePostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + delete_post_result = delete_post_services(query_db, delete_post) + if delete_post_result.is_success: + logger.info(delete_post_result.message) + return response_200(data=delete_post_result, message=delete_post_result.message) + else: + logger.warning(delete_post_result.message) + return response_400(data="", message=delete_post_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@postController.get("/post/{post_id}", response_model=PostModel) +async def query_detail_system_post(request: Request, post_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + delete_post_result = detail_post_services(query_db, post_id) + logger.info(f'获取post_id为{post_id}的信息成功') + return response_200(data=delete_post_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/mapper/crud/dept_crud.py index 440997a..faf5b25 100644 --- a/dash-fastapi-backend/mapper/crud/dept_crud.py +++ b/dash-fastapi-backend/mapper/crud/dept_crud.py @@ -1,7 +1,7 @@ from sqlalchemy import and_ from sqlalchemy.orm import Session from entity.dept_entity import SysDept -from mapper.schema.dept_schema import DeptPageObject +from mapper.schema.dept_schema import DeptModel, DeptResponse from utils.time_format_tool import list_format_datetime from utils.page_tool import get_page_info @@ -16,13 +16,14 @@ def get_dept_by_id(db: Session, dept_id: int): return dept_info -def get_dept_list_for_tree(db: Session, dept_info: DeptPageObject): +def get_dept_list_for_tree(db: Session, dept_info: DeptModel): if dept_info.dept_name: dept_query_all = db.query(SysDept) \ .filter(SysDept.status == 0, SysDept.del_flag == 0, SysDept.dept_name.like(f'%{dept_info.dept_name}%') if dept_info.dept_name else True) \ - .all() + .order_by(SysDept.order_num) \ + .distinct().all() dept = [] if dept_query_all: @@ -38,6 +39,46 @@ def get_dept_list_for_tree(db: Session, dept_info: DeptPageObject): else: dept_result = db.query(SysDept) \ .filter(SysDept.status == 0, SysDept.del_flag == 0) \ - .all() + .order_by(SysDept.order_num) \ + .distinct().all() return list_format_datetime(dept_result) + + +def get_dept_list(db: Session, page_object: DeptModel): + """ + 根据查询参数获取部门列表信息 + :param db: orm对象 + :param page_object: 不分页查询参数对象 + :return: 部门列表信息对象 + """ + if page_object.dept_name or page_object.status: + dept_query_all = db.query(SysDept) \ + .filter(SysDept.del_flag == 0, + SysDept.status == page_object.status if page_object.status else True, + SysDept.dept_name.like(f'%{page_object.dept_name}%') if page_object.dept_name else True) \ + .order_by(SysDept.order_num)\ + .distinct().all() + + dept = [] + if dept_query_all: + for dept_query in dept_query_all: + ancestor_info = dept_query.ancestors.split(',') + ancestor_info.append(dept_query.dept_id) + for ancestor in ancestor_info: + dept_item = get_dept_by_id(db, int(ancestor)) + if dept_item: + dept.append(dept_item) + # 去重 + dept_result = list(set(dept)) + else: + dept_result = db.query(SysDept) \ + .filter(SysDept.status == 0, SysDept.del_flag == 0) \ + .order_by(SysDept.order_num) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(dept_result), + ) + + return DeptResponse(**result) diff --git a/dash-fastapi-backend/mapper/crud/post_crud.py b/dash-fastapi-backend/mapper/crud/post_crud.py index f87bd35..fc7bd63 100644 --- a/dash-fastapi-backend/mapper/crud/post_crud.py +++ b/dash-fastapi-backend/mapper/crud/post_crud.py @@ -1,6 +1,7 @@ from sqlalchemy import and_ from sqlalchemy.orm import Session from entity.post_entity import SysPost +from mapper.schema.post_schema import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse from utils.time_format_tool import list_format_datetime from utils.page_tool import get_page_info @@ -14,9 +15,105 @@ def get_post_by_id(db: Session, post_id: int): return post_info +def get_post_detail_by_id(db: Session, post_id: int): + post_info = db.query(SysPost) \ + .filter(SysPost.post_id == post_id) \ + .first() + + return post_info + + def get_post_select_option_crud(db: Session): post_info = db.query(SysPost) \ .filter(SysPost.status == 0) \ .all() return post_info + + +def get_post_list(db: Session, page_object: PostPageObject): + """ + 根据查询参数获取岗位列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 岗位列表信息对象 + """ + count = db.query(SysPost) \ + .filter(SysPost.post_code.like(f'%{page_object.post_code}%') if page_object.post_code else True, + SysPost.post_name.like(f'%{page_object.post_name}%') if page_object.post_name else True, + SysPost.status == page_object.status if page_object.status else True + )\ + .order_by(SysPost.post_sort)\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + post_list = db.query(SysPost) \ + .filter(SysPost.post_code.like(f'%{page_object.post_code}%') if page_object.post_code else True, + SysPost.post_name.like(f'%{page_object.post_name}%') if page_object.post_name else True, + SysPost.status == page_object.status if page_object.status else True + ) \ + .order_by(SysPost.post_sort) \ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(post_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return PostPageObjectResponse(**result) + + +def add_post_crud(db: Session, post: PostModel): + """ + 新增岗位数据库操作 + :param db: orm对象 + :param post: 岗位对象 + :return: 新增校验结果 + """ + db_post = SysPost(**post.dict()) + db.add(db_post) + db.commit() # 提交保存到数据库中 + db.refresh(db_post) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudPostResponse(**result) + + +def edit_post_crud(db: Session, post: PostModel): + """ + 编辑岗位数据库操作 + :param db: orm对象 + :param post: 岗位对象 + :return: 编辑校验结果 + """ + is_post_id = db.query(SysPost).filter(SysPost.post_id == post.post_id).all() + if not is_post_id: + result = dict(is_success=False, message='岗位不存在') + else: + # 筛选出属性值为不为None和''的 + filtered_dict = {k: v for k, v in post.dict().items() if v is not None and v != ''} + db.query(SysPost) \ + .filter(SysPost.post_id == post.post_id) \ + .update(filtered_dict) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudPostResponse(**result) + + +def delete_post_crud(db: Session, post: PostModel): + """ + 删除岗位数据库操作 + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + db.query(SysPost) \ + .filter(SysPost.post_id == post.post_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/mapper/crud/user_crud.py index 69fb43e..e8ab54a 100644 --- a/dash-fastapi-backend/mapper/crud/user_crud.py +++ b/dash-fastapi-backend/mapper/crud/user_crud.py @@ -154,7 +154,7 @@ def get_user_list(db: Session, page_object: UserPageObject): .limit(page_object.page_size) \ .distinct().all() - result = [] + result_list = [] if user_list: for item in user_list: obj = dict( @@ -178,10 +178,10 @@ def get_user_list(db: Session, page_object: UserPageObject): update_time=item[0].update_time, remark=item[0].remark ) - result.append(obj) + result_list.append(obj) result = dict( - rows=format_datetime_dict_list(result), + rows=format_datetime_dict_list(result_list), page_num=page_info.page_num, page_size=page_info.page_size, total=page_info.total, diff --git a/dash-fastapi-backend/mapper/schema/dept_schema.py b/dash-fastapi-backend/mapper/schema/dept_schema.py index 03a69c9..0238707 100644 --- a/dash-fastapi-backend/mapper/schema/dept_schema.py +++ b/dash-fastapi-backend/mapper/schema/dept_schema.py @@ -7,8 +7,26 @@ class DeptPageObject(DeptModel): """ 部门管理分页查询模型 """ - page_num: Optional[int] - page_size: Optional[int] + page_num: int + page_size: int + + +class DeptPageObjectResponse(BaseModel): + """ + 用户管理列表分页查询返回模型 + """ + rows: List[Union[DeptModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeptResponse(BaseModel): + """ + 用户管理列表不分页查询返回模型 + """ + rows: List[Union[DeptModel, None]] = [] class DeptTree(BaseModel): diff --git a/dash-fastapi-backend/mapper/schema/post_schema.py b/dash-fastapi-backend/mapper/schema/post_schema.py index 4a9064a..f036242 100644 --- a/dash-fastapi-backend/mapper/schema/post_schema.py +++ b/dash-fastapi-backend/mapper/schema/post_schema.py @@ -11,8 +11,34 @@ class PostPageObject(PostModel): page_size: Optional[int] +class PostPageObjectResponse(BaseModel): + """ + 岗位管理列表分页查询返回模型 + """ + rows: List[Union[PostModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + class PostSelectOptionResponseModel(BaseModel): """ 岗位管理不分页查询模型 """ post: List[Union[PostModel, None]] + + +class CrudPostResponse(BaseModel): + """ + 操作岗位响应模型 + """ + is_success: bool + message: str + + +class DeletePostModel(BaseModel): + """ + 删除岗位模型 + """ + post_ids: str diff --git a/dash-fastapi-backend/service/dept_service.py b/dash-fastapi-backend/service/dept_service.py index a49c4a9..37102ef 100644 --- a/dash-fastapi-backend/service/dept_service.py +++ b/dash-fastapi-backend/service/dept_service.py @@ -2,11 +2,11 @@ from mapper.schema.dept_schema import * from mapper.crud.dept_crud import * -def get_dept_tree_services(result_db: Session, page_object: DeptPageObject): +def get_dept_tree_services(result_db: Session, page_object: DeptModel): """ 获取部门树信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param page_object: 查询参数对象 :return: 部门树信息对象 """ dept_list_result = get_dept_list_for_tree(result_db, page_object) @@ -15,6 +15,18 @@ def get_dept_tree_services(result_db: Session, page_object: DeptPageObject): return dept_tree_result +def get_dept_list_services(result_db: Session, page_object: DeptModel): + """ + 获取部门列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 部门列表信息对象 + """ + dept_list_result = get_dept_list(result_db, page_object) + + return dept_list_result + + def get_dept_tree(pid: int, permission_list: DeptTree): """ 工具方法:根据部门信息生成树形嵌套数据 diff --git a/dash-fastapi-backend/service/post_service.py b/dash-fastapi-backend/service/post_service.py index b8e5e75..e73f20b 100644 --- a/dash-fastapi-backend/service/post_service.py +++ b/dash-fastapi-backend/service/post_service.py @@ -11,3 +11,69 @@ def get_post_select_option_services(result_db: Session): post_list_result = get_post_select_option_crud(result_db) return post_list_result + + +def get_post_list_services(result_db: Session, page_object: PostPageObject): + """ + 获取岗位列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 岗位列表信息对象 + """ + post_list_result = get_post_list(result_db, page_object) + + return post_list_result + + +def add_post_services(result_db: Session, page_object: PostModel): + """ + 新增岗位信息service + :param result_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增岗位校验结果 + """ + add_post_result = add_post_crud(result_db, page_object) + + return add_post_result + + +def edit_post_services(result_db: Session, page_object: PostModel): + """ + 编辑岗位信息service + :param result_db: orm对象 + :param page_object: 编辑岗位对象 + :return: 编辑岗位校验结果 + """ + edit_post_result = edit_post_crud(result_db, page_object) + + return edit_post_result + + +def delete_post_services(result_db: Session, page_object: DeletePostModel): + """ + 删除岗位信息service + :param result_db: orm对象 + :param page_object: 删除岗位对象 + :return: 删除岗位校验结果 + """ + if page_object.post_ids.split(','): + post_id_list = page_object.post_ids.split(',') + for post_id in post_id_list: + post_id_dict = dict(post_id=post_id) + delete_post_crud(result_db, PostModel(**post_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入用户id为空') + return CrudPostResponse(**result) + + +def detail_post_services(result_db: Session, post_id: int): + """ + 获取岗位详细信息service + :param result_db: orm对象 + :param post_id: 岗位id + :return: 岗位id对应的信息 + """ + post = get_post_detail_by_id(result_db, post_id=post_id) + + return post diff --git a/dash-fastapi-backend/service/user_service.py b/dash-fastapi-backend/service/user_service.py index e36f6e7..8a46ead 100644 --- a/dash-fastapi-backend/service/user_service.py +++ b/dash-fastapi-backend/service/user_service.py @@ -88,7 +88,7 @@ def delete_user_services(result_db: Session, page_object: DeleteUserModel): def detail_user_services(result_db: Session, user_id: int): """ - 获取用户列表信息service + 获取用户详细信息service :param result_db: orm对象 :param user_id: 用户id :return: 用户id对应的信息 diff --git a/dash-fastapi-frontend/api/post.py b/dash-fastapi-frontend/api/post.py index c5eef3e..3a94c2a 100644 --- a/dash-fastapi-frontend/api/post.py +++ b/dash-fastapi-frontend/api/post.py @@ -4,3 +4,28 @@ from utils.request import api_request def get_post_select_option_api(): return api_request(method='post', url='/system/post/forSelectOption', is_headers=True) + + +def get_post_list_api(page_obj: dict): + + return api_request(method='post', url='/system/post/get', is_headers=True, json=page_obj) + + +def add_post_api(page_obj: dict): + + return api_request(method='post', url='/system/post/add', is_headers=True, json=page_obj) + + +def edit_post_api(page_obj: dict): + + return api_request(method='post', url='/system/post/edit', is_headers=True, json=page_obj) + + +def delete_post_api(page_obj: dict): + + return api_request(method='post', url='/system/post/delete', is_headers=True, json=page_obj) + + +def get_post_detail_api(post_id: int): + + return api_request(method='get', url=f'/system/post/{post_id}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py new file mode 100644 index 0000000..68e56a2 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -0,0 +1,83 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc +from jsonpath_ng import parse +from flask import session, json +from collections import OrderedDict + +from server import app +from utils.tree_tool import get_dept_tree +from api.dept import get_dept_list_api + + +@app.callback( + [Output('dept-list-table', 'data', allow_duplicate=True), + Output('dept-list-table', 'key'), + Output('dept-list-table', 'defaultExpandedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dept-search', 'nClicks'), + Input('dept-operations-store', 'data')], + [State('dept-dept_name-input', 'value'), + State('dept-status-select', 'value')], + prevent_initial_call=True +) +def get_dept_table_data(search_click, operations, dept_name, status_select): + + query_params = dict( + dept_name=dept_name, + status=status_select + ) + if search_click or operations: + table_info = get_dept_list_api(query_params) + default_expanded_row_keys = [] + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + for item in table_data: + default_expanded_row_keys.append(str(item['dept_id'])) + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dept_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + table_data_new = get_dept_tree(0, table_data) + + return [table_data_new, str(uuid.uuid4()), default_expanded_row_keys, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 4 + + +@app.callback( + [Output('dept-dept_name-input', 'value'), + Output('dept-status-select', 'value'), + Output('dept-operations-store', 'data')], + Input('dept-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_dept_query_params(reset_click): + if reset_click: + return [None, None, {'type': 'reset'}] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py new file mode 100644 index 0000000..5be3460 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -0,0 +1,373 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.post import get_post_list_api, get_post_detail_api, add_post_api, edit_post_api, delete_post_api + + +@app.callback( + [Output('post-list-table', 'data', allow_duplicate=True), + Output('post-list-table', 'pagination', allow_duplicate=True), + Output('post-list-table', 'key'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('post-search', 'nClicks'), + Input('post-list-table', 'pagination'), + Input('post-operations-store', 'data')], + [State('post-post_code-input', 'value'), + State('post-post_name-input', 'value'), + State('post-status-select', 'value')], + prevent_initial_call=True +) +def get_post_table_data(search_click, pagination, operations, post_code, post_name, status_select): + + query_params = dict( + post_code=post_code, + post_name=post_name, + status=status_select, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + post_code=post_code, + post_name=post_name, + status=status_select, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_post_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['post_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 4 + + +@app.callback( + [Output('post-post_code-input', 'value'), + Output('post-post_name-input', 'value'), + Output('post-status-select', 'value'), + Output('post-operations-store', 'data')], + Input('post-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_post_query_params(reset_click): + if reset_click: + return [None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 4 + + +@app.callback( + [Output('post-edit', 'disabled'), + Output('post-delete', 'disabled')], + Input('post-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_post_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return [True, True] + + +@app.callback( + Output('post-add-modal', 'visible', allow_duplicate=True), + Input('post-add', 'nClicks'), + prevent_initial_call=True +) +def add_post_modal(add_click): + if add_click: + + return True + + return dash.no_update + + +@app.callback( + [Output('post-add-post_name-form-item', 'validateStatus'), + Output('post-add-post_code-form-item', 'validateStatus'), + Output('post-add-post_sort-form-item', 'validateStatus'), + Output('post-add-post_name-form-item', 'help'), + Output('post-add-post_code-form-item', 'help'), + Output('post-add-post_sort-form-item', 'help'), + Output('post-add-modal', 'visible', allow_duplicate=True), + Output('post-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('post-add-modal', 'okCounts'), + [State('post-add-post_name', 'value'), + State('post-add-post_code', 'value'), + State('post-add-post_sort', 'value'), + State('post-add-status', 'value'), + State('post-add-remark', 'value')], + prevent_initial_call=True +) +def post_add_confirm(add_confirm, post_name, post_code, post_sort, status, remark): + if add_confirm: + + if all([post_name, post_code, post_sort]): + params = dict(post_name=post_name, post_code=post_code, post_sort=post_sort, status=status, remark=remark) + add_button_result = add_post_api(params) + + if add_button_result['code'] == 200: + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增失败', type='error') + ] + + return [ + None if post_name else 'error', + None if post_code else 'error', + None if post_sort else 'error', + None if post_name else '请输入岗位名称!', + None if post_code else '请输入岗位编码!', + None if post_sort else '请输入岗位顺序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('post-edit-modal', 'visible', allow_duplicate=True), + Output('post-edit-post_name', 'value'), + Output('post-edit-post_code', 'value'), + Output('post-edit-post_sort', 'value'), + Output('post-edit-status', 'value'), + Output('post-edit-remark', 'value'), + Output('post-edit-id-store', 'data'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('post-edit', 'nClicks'), + Input('post-list-table', 'nClicksButton')], + [State('post-list-table', 'selectedRowKeys'), + State('post-list-table', 'clickedContent'), + State('post-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def post_edit_modal(edit_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if edit_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'post-edit': + post_id = int(selected_row_keys[0]) + else: + if clicked_content == '修改': + post_id = int(recently_button_clicked_row['key']) + else: + return dash.no_update + + edit_button_info = get_post_detail_api(post_id) + if edit_button_info['code'] == 200: + edit_button_result = edit_button_info['data'] + + return [ + True, + edit_button_result['post_name'], + edit_button_result['post_code'], + edit_button_result['post_sort'], + edit_button_result['status'], + edit_button_result['remark'], + {'post_id': post_id}, + {'timestamp': time.time()} + ] + + return [dash.no_update] * 7 + [{'timestamp': time.time()}] + + return [dash.no_update] * 8 + + +@app.callback( + [Output('post-edit-post_name-form-item', 'validateStatus'), + Output('post-edit-post_code-form-item', 'validateStatus'), + Output('post-edit-post_sort-form-item', 'validateStatus'), + Output('post-edit-post_name-form-item', 'help'), + Output('post-edit-post_code-form-item', 'help'), + Output('post-edit-post_sort-form-item', 'help'), + Output('post-edit-modal', 'visible', allow_duplicate=True), + Output('post-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('post-edit-modal', 'okCounts'), + [State('post-edit-post_name', 'value'), + State('post-edit-post_code', 'value'), + State('post-edit-post_sort', 'value'), + State('post-edit-status', 'value'), + State('post-edit-remark', 'value'), + State('post-edit-id-store', 'data')], + prevent_initial_call=True +) +def post_edit_confirm(edit_confirm, post_name, post_code, post_sort, status, remark, post_id): + if edit_confirm: + + if all([post_name, post_code, post_sort]): + params = dict(post_id=post_id['post_id'], post_name=post_name, post_code=post_code, + post_sort=post_sort, status=status, remark=remark) + edit_button_result = edit_post_api(params) + + if edit_button_result['code'] == 200: + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑失败', type='error') + ] + + return [ + None if post_name else 'error', + None if post_code else 'error', + None if post_sort else 'error', + None if post_name else '请输入岗位名称!', + None if post_code else '请输入岗位编码!', + None if post_sort else '请输入岗位顺序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('post-delete-text', 'children'), + Output('post-delete-confirm-modal', 'visible'), + Output('post-delete-ids-store', 'data')], + [Input('post-delete', 'nClicks'), + Input('post-list-table', 'nClicksButton')], + [State('post-list-table', 'selectedRowKeys'), + State('post-list-table', 'clickedContent'), + State('post-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def post_delete_modal(delete_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if delete_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'post-delete': + post_ids = ','.join(selected_row_keys) + else: + if clicked_content == '删除': + post_ids = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除岗位编号为{post_ids}的用户?', + True, + {'post_ids': post_ids} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('post-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('post-delete-confirm-modal', 'okCounts'), + State('post-delete-ids-store', 'data'), + prevent_initial_call=True +) +def post_delete_confirm(delete_confirm, post_ids_data): + if delete_confirm: + + params = post_ids_data + delete_button_info = delete_post_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index 26ad251..ba75c82 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -41,7 +41,7 @@ def get_search_dept_tree(dept_input): [Input('dept-tree', 'selectedKeys'), Input('user-search', 'nClicks'), Input('user-list-table', 'pagination'), - Input('operations-store', 'data')], + Input('user-operations-store', 'data')], [State('user-user_name-input', 'value'), State('user-phone_number-input', 'value'), State('user-status-select', 'value'), @@ -124,15 +124,16 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio Output('user-user_name-input', 'value'), Output('user-phone_number-input', 'value'), Output('user-status-select', 'value'), - Output('user-create_time-range', 'value')], + Output('user-create_time-range', 'value'), + Output('user-operations-store', 'data')], Input('user-reset', 'nClicks'), prevent_initial_call=True ) def reset_user_query_params(reset_click): if reset_click: - return [None, None, None, None, None] + return [None, None, None, None, None, {'type': 'reset'}] - return [dash.no_update] * 5 + return [dash.no_update] * 6 @app.callback( @@ -141,7 +142,7 @@ def reset_user_query_params(reset_click): Input('user-list-table', 'selectedRowKeys'), prevent_initial_call=True ) -def change_edit_delete_button_status(table_rows_selected): +def change_user_edit_delete_button_status(table_rows_selected): if table_rows_selected: if len(table_rows_selected) > 1: return [True, False] @@ -192,7 +193,7 @@ def add_user_modal(add_click): Output('user-add-user_name-form-item', 'help'), Output('user-add-password-form-item', 'help'), Output('user-add-modal', 'visible', allow_duplicate=True), - Output('operations-store', 'data', allow_duplicate=True), + Output('user-operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], Input('user-add-modal', 'okCounts'), @@ -277,7 +278,7 @@ def usr_add_confirm(add_confirm, nick_name, dept_id, phone_number, email, user_n Output('user-edit-post', 'value'), Output('user-edit-role', 'value'), Output('user-edit-remark', 'value'), - Output('edit-id-store', 'data'), + Output('user-edit-id-store', 'data'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('user-edit', 'nClicks'), Input('user-list-table', 'nClicksDropdownItem')], @@ -339,7 +340,7 @@ def user_edit_modal(edit_click, dropdown_click, [Output('user-edit-nick_name-form-item', 'validateStatus'), Output('user-edit-nick_name-form-item', 'help'), Output('user-edit-modal', 'visible', allow_duplicate=True), - Output('operations-store', 'data', allow_duplicate=True), + Output('user-operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], Input('user-edit-modal', 'okCounts'), @@ -352,7 +353,7 @@ def user_edit_modal(edit_click, dropdown_click, State('user-edit-post', 'value'), State('user-edit-role', 'value'), State('user-edit-remark', 'value'), - State('edit-id-store', 'data')], + State('user-edit-id-store', 'data')], prevent_initial_call=True ) def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, status, post, role, remark, user_id): @@ -396,7 +397,7 @@ def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, @app.callback( - [Output('operations-store', 'data', allow_duplicate=True), + [Output('user-operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], [Input('user-list-table', 'recentlySwitchDataIndex'), @@ -411,7 +412,7 @@ def table_switch_user_status(recently_switch_data_index, recently_switch_status, else: params = dict(user_id=int(recently_switch_row['key']), status='1') edit_button_result = edit_user_api(params) - if edit_button_result['code'] == 200:\ + if edit_button_result['code'] == 200: return [ {'type': 'switch-status'}, @@ -429,9 +430,9 @@ def table_switch_user_status(recently_switch_data_index, recently_switch_status, @app.callback( - [Output('delete-text', 'children'), + [Output('user-delete-text', 'children'), Output('user-delete-confirm-modal', 'visible'), - Output('delete-ids-store', 'data')], + Output('user-delete-ids-store', 'data')], [Input('user-delete', 'nClicks'), Input('user-list-table', 'nClicksDropdownItem')], [State('user-list-table', 'selectedRowKeys'), @@ -462,11 +463,11 @@ def user_delete_modal(delete_click, dropdown_click, @app.callback( - [Output('operations-store', 'data', allow_duplicate=True), + [Output('user-operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], Input('user-delete-confirm-modal', 'okCounts'), - State('delete-ids-store', 'data'), + State('user-delete-ids-store', 'data'), prevent_initial_call=True ) def user_delete_confirm(delete_confirm, user_ids_data): diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 07af70c..1729595 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -2,14 +2,34 @@ from dash import html, dcc def render_store_container(): + return html.Div( [ + # 接口校验返回存储容器 dcc.Store(id='api-check-token'), # 接口校验返回存储容器 dcc.Store(id='api-check-result-container'), # token存储容器 dcc.Store(id='token-container'), # 菜单current_key存储容器 - dcc.Store(id='current-key-container') + dcc.Store(id='current-key-container'), + # 用户管理模块操作类型存储容器 + dcc.Store(id='user-operations-store'), + # 用户管理模块修改操作行key存储容器 + dcc.Store(id='user-edit-id-store'), + # 用户管理模块删除操作行key存储容器 + dcc.Store(id='user-delete-ids-store'), + # 部门管理模块操作类型存储容器 + dcc.Store(id='dept-operations-store'), + # 部门管理模块修改操作行key存储容器 + dcc.Store(id='dept-edit-id-store'), + # 部门管理模块删除操作行key存储容器 + dcc.Store(id='dept-delete-ids-store'), + # 岗位管理模块操作类型存储容器 + dcc.Store(id='post-operations-store'), + # 岗位管理模块修改操作行key存储容器 + dcc.Store(id='post-edit-id-store'), + # 岗位管理模块删除操作行key存储容器 + dcc.Store(id='post-delete-ids-store'), ] ) diff --git a/dash-fastapi-frontend/utils/tree_tool.py b/dash-fastapi-frontend/utils/tree_tool.py index 3dae146..a8c5a87 100644 --- a/dash-fastapi-frontend/utils/tree_tool.py +++ b/dash-fastapi-frontend/utils/tree_tool.py @@ -116,3 +116,29 @@ def find_parents(tree, target_key): break return result[::-1] + + +def get_dept_tree(pid: int, permission_list: list): + """ + 工具方法:根据部门信息生成树形嵌套数据 + :param pid: 部门id + :param permission_list: 部门列表信息 + :return: 部门树形嵌套数据 + """ + dept_list = [] + for permission in permission_list: + if permission['parent_id'] == pid: + children = get_dept_tree(permission['dept_id'], permission_list) + dept_list_data = {} + if children: + dept_list_data['children'] = children + dept_list_data['key'] = str(permission['dept_id']) + dept_list_data['dept_id'] = permission['dept_id'] + dept_list_data['dept_name'] = permission['dept_name'] + dept_list_data['order_num'] = permission['order_num'] + dept_list_data['status'] = permission['status'] + dept_list_data['create_time'] = permission['create_time'] + dept_list_data['operation'] = permission['operation'] + dept_list.append(dept_list_data) + + return dept_list diff --git a/dash-fastapi-frontend/views/system/dept/index.py b/dash-fastapi-frontend/views/system/dept/index.py index 8f449af..606a577 100644 --- a/dash-fastapi-frontend/views/system/dept/index.py +++ b/dash-fastapi-frontend/views/system/dept/index.py @@ -1,8 +1,674 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc import feffery_antd_components as fac +import callbacks.system_c.dept_c +from api.dept import get_dept_list_api +from utils.tree_tool import get_dept_tree + def render(): + table_data_new = [] + default_expanded_row_keys = [] + table_info = get_dept_list_api({}) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + for item in table_data: + default_expanded_row_keys.append(str(item['dept_id'])) + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dept_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + table_data_new = get_dept_tree(0, table_data) + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-dept_name-input', + placeholder='请输入部门名称', + autoComplete='off', + style={ + 'width': 240 + } + ), + label='部门名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dept-status-select', + placeholder='部门状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='部门状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dept-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dept-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dept-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-swap' + ), + '展开/折叠', + ], + id='dept-edit', + disabled=True, + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='dept-list-table', + data=table_data_new, + columns=[ + { + 'dataIndex': 'dept_id', + 'title': '部门编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + 'hidden': True + }, + { + 'dataIndex': 'dept_name', + 'title': '部门名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'order_num', + 'title': '排序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + bordered=True, + pagination={ + 'hideOnSinglePage': True + }, + defaultExpandedRowKeys=default_expanded_row_keys, + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增部门表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-nick_name', + placeholder='请输入用户昵称', + style={ + 'width': 200 + } + ), + label='用户昵称', + required=True, + id='user-add-nick_name-form-item' + ), + fac.AntdFormItem( + fac.AntdTreeSelect( + id='user-add-dept_id', + placeholder='请选择归属部门', + treeData=[], + style={ + 'width': 200 + } + ), + label='归属部门', + id='user-add-dept_id-form-item', + labelCol={ + 'offset': 1 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-phone_number', + placeholder='请输入手机号码', + style={ + 'width': 200 + } + ), + label='手机号码', + id='user-add-phone_number-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-add-email', + placeholder='请输入邮箱', + style={ + 'width': 200 + } + ), + label='邮箱', + id='user-add-email-form-item', + labelCol={ + 'offset': 5 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-user_name', + placeholder='请输入用户名称', + style={ + 'width': 200 + } + ), + label='用户名称', + required=True, + id='user-add-user_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-add-password', + placeholder='请输入密码', + mode='password', + passwordUseMd5=True, + style={ + 'width': 200 + } + ), + label='用户密码', + required=True, + id='user-add-password-form-item' + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-sex', + placeholder='请选择性别', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + }, + { + 'label': '未知', + 'value': '2' + }, + ], + style={ + 'width': 200 + } + ), + label='用户性别', + id='user-add-sex-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='user-add-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label='用户状态', + id='user-add-status-form-item', + labelCol={ + 'offset': 2 + }, + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-post', + placeholder='请选择岗位', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-add-post-form-item', + labelCol={ + 'offset': 4 + }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-role', + placeholder='请选择角色', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-add-role-form-item', + labelCol={ + 'offset': 8 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-remark', + placeholder='请输入内容', + mode='text-area', + style={ + 'width': 490 + } + ), + label='备注', + id='user-add-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ] + ) + ], + id='user-add-modal', + title='新增用户', + mask=False, + width=650, + renderFooter=True, + okClickClose=False + ), + + # 编辑用户表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-nick_name', + placeholder='请输入用户昵称', + style={ + 'width': 200 + } + ), + label='用户昵称', + required=True, + id='user-edit-nick_name-form-item' + ), + fac.AntdFormItem( + fac.AntdTreeSelect( + id='user-edit-dept_id', + placeholder='请选择归属部门', + treeData=[], + style={ + 'width': 200 + } + ), + label='归属部门', + id='user-edit-dept_id-form-item' + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-phone_number', + placeholder='请输入手机号码', + style={ + 'width': 200 + } + ), + label='手机号码', + id='user-edit-phone_number-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-email', + placeholder='请输入邮箱', + style={ + 'width': 200 + } + ), + label='邮箱', + id='user-edit-email-form-item', + labelCol={ + 'offset': 4 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-sex', + placeholder='请选择性别', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + }, + { + 'label': '未知', + 'value': '2' + }, + ], + style={ + 'width': 200 + } + ), + label='用户性别', + id='user-edit-sex-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='user-edit-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + style={ + 'width': 200 + } + ), + label='用户状态', + id='user-edit-status-form-item', + labelCol={ + 'offset': 1 + }, + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-post', + placeholder='请选择岗位', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-edit-post-form-item', + labelCol={ + 'offset': 4 + }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-role', + placeholder='请选择角色', + options=[], + mode='multiple', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-edit-role-form-item', + labelCol={ + 'offset': 7 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-remark', + placeholder='请输入内容', + mode='text-area', + style={ + 'width': 485 + } + ), + label='备注', + id='user-edit-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ] + ) + ], + id='user-edit-modal', + title='编辑用户', + mask=False, + width=650, + renderFooter=True, + okClickClose=False + ), + + # 删除用户二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='delete-text'), + id='user-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), - return html.Div('我是部门管理') + # 重置密码modal + fac.AntdModal( + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-password-input', + mode='password' + ), + ), + ], + layout='vertical' + ), + id='user-reset-password-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] diff --git a/dash-fastapi-frontend/views/system/post/index.py b/dash-fastapi-frontend/views/system/post/index.py index 547151f..a39f18b 100644 --- a/dash-fastapi-frontend/views/system/post/index.py +++ b/dash-fastapi-frontend/views/system/post/index.py @@ -1,8 +1,473 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc import feffery_antd_components as fac +import callbacks.system_c.post_c +from api.post import get_post_list_api + def render(): - return html.Div('我是岗位管理') + post_params = dict(page_num=1, page_size=10) + table_info = get_post_list_api(post_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['post_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-post_code-input', + placeholder='请输入岗位编码', + autoComplete='off', + style={ + 'width': 210 + } + ), + label='岗位编码' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-post_name-input', + placeholder='请输入岗位名称', + autoComplete='off', + style={ + 'width': 210 + } + ), + label='岗位名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='post-status-select', + placeholder='岗位状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 200 + } + ), + label='岗位状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='post-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='post-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='post-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='post-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='post-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='post-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='post-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'post_id', + 'title': '岗位编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'post_code', + 'title': '岗位编码', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'post_name', + 'title': '岗位名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'post_sort', + 'title': '岗位排序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增岗位表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-add-post_name', + placeholder='请输入岗位名称', + style={ + 'width': 350 + } + ), + label='岗位名称', + required=True, + id='post-add-post_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-add-post_code', + placeholder='请输入岗位编码', + style={ + 'width': 350 + } + ), + label='岗位编码', + required=True, + id='post-add-post_code-form-item', + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='post-add-post_sort', + defaultValue=0, + style={ + 'width': 350 + } + ), + label='岗位顺序', + required=True, + id='post-add-post_sort-form-item', + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='post-add-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='岗位状态', + id='post-add-status-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-add-remark', + placeholder='请输入内容', + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='post-add-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ], + id='post-add-modal', + title='新增岗位', + mask=False, + width=480, + renderFooter=True, + okClickClose=False + ), + + # 编辑岗位表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-edit-post_name', + placeholder='请输入岗位名称', + style={ + 'width': 350 + } + ), + label='岗位名称', + required=True, + id='post-edit-post_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-edit-post_code', + placeholder='请输入岗位编码', + style={ + 'width': 350 + } + ), + label='岗位编码', + required=True, + id='post-edit-post_code-form-item', + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='post-edit-post_sort', + defaultValue=0, + style={ + 'width': 350 + } + ), + label='岗位顺序', + required=True, + id='post-edit-post_sort-form-item', + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='post-edit-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='岗位状态', + id='post-edit-status-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-edit-remark', + placeholder='请输入内容', + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='post-edit-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ], + id='post-edit-modal', + title='编辑岗位', + mask=False, + width=480, + renderFooter=True, + okClickClose=False + ), + + # 删除岗位二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='post-delete-text'), + id='post-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] diff --git a/dash-fastapi-frontend/views/system/user/index.py b/dash-fastapi-frontend/views/system/user/index.py index fc33151..62c3bf5 100644 --- a/dash-fastapi-frontend/views/system/user/index.py +++ b/dash-fastapi-frontend/views/system/user/index.py @@ -8,10 +8,41 @@ from api.dept import get_dept_tree_api def render(): dept_params = dict(dept_name='') + user_params = dict(page_num=1, page_size=10) tree_info = get_dept_tree_api(dept_params) + table_info = get_user_list_api(user_params) tree_data = [] + table_data = [] + page_num = 1 + page_size = 10 + total = 0 if tree_info['code'] == 200: tree_data = tree_info['data'] + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(checked=True) + else: + item['status'] = dict(checked=False) + item['key'] = str(item['user_id']) + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + }, + { + 'title': '删除', + 'icon': 'antd-delete' + }, + { + 'title': '重置密码', + 'icon': 'antd-key' + } + ] return [ fac.AntdRow( @@ -228,7 +259,7 @@ def render(): fac.AntdSpin( fac.AntdTable( id='user-list-table', - data=[], + data=table_data, columns=[ { 'dataIndex': 'user_id', @@ -301,12 +332,12 @@ def render(): rowSelectionWidth=50, bordered=True, pagination={ - 'pageSize': 10, - 'current': 1, + 'pageSize': page_size, + 'current': page_num, 'showSizeChanger': True, 'pageSizeOptions': [10, 30, 50, 100], 'showQuickJumper': True, - 'total': 0 + 'total': total }, mode='server-side', style={ @@ -743,7 +774,7 @@ def render(): # 删除用户二次确认modal fac.AntdModal( - fac.AntdText('是否确认删除?', id='delete-text'), + fac.AntdText('是否确认删除?', id='user-delete-text'), id='user-delete-confirm-modal', visible=False, title='提示', @@ -770,8 +801,4 @@ def render(): renderFooter=True, centered=True ), - - dcc.Store(id='operations-store'), - dcc.Store(id='edit-id-store'), - dcc.Store(id='delete-ids-store') ] -- Gitee From 10c229745b81cec60580cf623186755de1faee3c Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sat, 3 Jun 2023 22:20:34 +0800 Subject: [PATCH 005/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E5=88=97=E8=A1=A8=EF=BC=88=E5=A2=9E=E5=88=A0=E6=9F=A5?= =?UTF-8?q?=E6=94=B9=E5=B7=B2=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dept_controller.py | 97 +++++ dash-fastapi-backend/mapper/crud/dept_crud.py | 93 ++++- .../mapper/schema/dept_schema.py | 17 + dash-fastapi-backend/service/dept_service.py | 119 +++++- dash-fastapi-frontend/api/dept.py | 30 ++ .../callbacks/system_c/dept_c.py | 347 ++++++++++++++-- .../views/system/dept/index.py | 391 ++++++------------ 7 files changed, 789 insertions(+), 305 deletions(-) diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/controller/dept_controller.py index 2aee58b..929d133 100644 --- a/dash-fastapi-backend/controller/dept_controller.py +++ b/dash-fastapi-backend/controller/dept_controller.py @@ -28,6 +28,22 @@ async def get_system_dept_tree(request: Request, dept_query: DeptModel, token: O return response_500(data="", message="接口异常") +@deptController.post("/dept/forEditOption", response_model=DeptTree) +async def get_system_dept_tree_for_edit_option(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) + logger.info('获取成功') + return response_200(data=dept_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + @deptController.post("/dept/get", response_model=DeptResponse) async def get_system_dept_list(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: @@ -42,3 +58,84 @@ async def get_system_dept_list(request: Request, dept_query: DeptModel, token: O except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@deptController.post("/dept/add", response_model=CrudDeptResponse) +async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + add_dept.create_by = current_user.user.user_name + add_dept.update_by = current_user.user.user_name + add_dept_result = add_dept_services(query_db, add_dept) + logger.info(add_dept_result.message) + if add_dept_result.is_success: + return response_200(data=add_dept_result, message=add_dept_result.message) + else: + return response_400(data="", message=add_dept_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@deptController.post("/dept/edit", response_model=CrudDeptResponse) +async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + edit_dept.update_by = current_user.user.user_name + edit_dept.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_dept_result = edit_dept_services(query_db, edit_dept) + if edit_dept_result.is_success: + logger.info(edit_dept_result.message) + return response_200(data=edit_dept_result, message=edit_dept_result.message) + else: + logger.warning(edit_dept_result.message) + return response_400(data="", message=edit_dept_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@deptController.post("/dept/delete", response_model=CrudDeptResponse) +async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + delete_dept.update_by = current_user.user.user_name + delete_dept.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + delete_dept_result = delete_dept_services(query_db, delete_dept) + if delete_dept_result.is_success: + logger.info(delete_dept_result.message) + return response_200(data=delete_dept_result, message=delete_dept_result.message) + else: + logger.warning(delete_dept_result.message) + return response_400(data="", message=delete_dept_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@deptController.get("/dept/{dept_id}", response_model=DeptModel) +async def query_detail_system_post(request: Request, dept_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + detail_dept_result = detail_dept_services(query_db, dept_id) + logger.info(f'获取dept_id为{dept_id}的信息成功') + return response_200(data=detail_dept_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/mapper/crud/dept_crud.py index faf5b25..9d8b9a3 100644 --- a/dash-fastapi-backend/mapper/crud/dept_crud.py +++ b/dash-fastapi-backend/mapper/crud/dept_crud.py @@ -1,7 +1,7 @@ -from sqlalchemy import and_ +from sqlalchemy import and_, or_ from sqlalchemy.orm import Session from entity.dept_entity import SysDept -from mapper.schema.dept_schema import DeptModel, DeptResponse +from mapper.schema.dept_schema import DeptModel, DeptResponse, CrudDeptResponse from utils.time_format_tool import list_format_datetime from utils.page_tool import get_page_info @@ -16,6 +16,41 @@ def get_dept_by_id(db: Session, dept_id: int): return dept_info +def get_dept_detail_by_id(db: Session, dept_id: int): + dept_info = db.query(SysDept) \ + .filter(SysDept.dept_id == dept_id, + SysDept.del_flag == 0) \ + .first() + + return dept_info + + +def get_dept_info_for_edit_option(db: Session, dept_info: DeptModel): + dept_result = db.query(SysDept) \ + .filter(SysDept.dept_id != dept_info.dept_id, SysDept.parent_id != dept_info.dept_id, + SysDept.del_flag == 0, SysDept.status == 0) \ + .all() + + return list_format_datetime(dept_result) + + +def get_children_dept(db: Session, dept_id: int): + dept_result = db.query(SysDept) \ + .filter(SysDept.parent_id == dept_id, + SysDept.del_flag == 0) \ + .all() + + return list_format_datetime(dept_result) + + +def get_dept_all_ancestors(db: Session): + ancestors = db.query(SysDept.ancestors)\ + .filter(SysDept.del_flag == 0)\ + .all() + + return ancestors + + def get_dept_list_for_tree(db: Session, dept_info: DeptModel): if dept_info.dept_name: dept_query_all = db.query(SysDept) \ @@ -73,7 +108,7 @@ def get_dept_list(db: Session, page_object: DeptModel): dept_result = list(set(dept)) else: dept_result = db.query(SysDept) \ - .filter(SysDept.status == 0, SysDept.del_flag == 0) \ + .filter(SysDept.del_flag == 0) \ .order_by(SysDept.order_num) \ .distinct().all() @@ -82,3 +117,55 @@ def get_dept_list(db: Session, page_object: DeptModel): ) return DeptResponse(**result) + + +def add_dept_crud(db: Session, dept: DeptModel): + """ + 新增部门数据库操作 + :param db: orm对象 + :param dept: 部门对象 + :return: 新增校验结果 + """ + db_dept = SysDept(**dept.dict()) + db.add(db_dept) + db.commit() # 提交保存到数据库中 + db.refresh(db_dept) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudDeptResponse(**result) + + +def edit_dept_crud(db: Session, dept: DeptModel): + """ + 编辑部门数据库操作 + :param db: orm对象 + :param dept: 部门对象 + :return: 编辑校验结果 + """ + print(dept.dept_id) + is_dept_id = db.query(SysDept).filter(SysDept.dept_id == dept.dept_id).all() + if not is_dept_id: + result = dict(is_success=False, message='部门不存在') + else: + # 筛选出属性值为不为None和''的 + filtered_dict = {k: v for k, v in dept.dict().items() if v is not None and v != ''} + db.query(SysDept) \ + .filter(SysDept.dept_id == dept.dept_id) \ + .update(filtered_dict) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudDeptResponse(**result) + + +def delete_dept_crud(db: Session, dept: DeptModel): + """ + 删除部门数据库操作 + :param db: orm对象 + :param dept: 部门对象 + :return: + """ + db.query(SysDept) \ + .filter(SysDept.dept_id == dept.dept_id) \ + .update({SysDept.del_flag: '2', SysDept.update_by: dept.update_by, SysDept.update_time: dept.update_time}) + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/mapper/schema/dept_schema.py b/dash-fastapi-backend/mapper/schema/dept_schema.py index 0238707..bb3e622 100644 --- a/dash-fastapi-backend/mapper/schema/dept_schema.py +++ b/dash-fastapi-backend/mapper/schema/dept_schema.py @@ -34,3 +34,20 @@ class DeptTree(BaseModel): 部门树响应模型 """ dept_tree: Union[List, None] + + +class CrudDeptResponse(BaseModel): + """ + 操作部门响应模型 + """ + is_success: bool + message: str + + +class DeleteDeptModel(BaseModel): + """ + 删除部门模型 + """ + dept_ids: str + update_by: Optional[str] + update_time: Optional[str] diff --git a/dash-fastapi-backend/service/dept_service.py b/dash-fastapi-backend/service/dept_service.py index 37102ef..75aa937 100644 --- a/dash-fastapi-backend/service/dept_service.py +++ b/dash-fastapi-backend/service/dept_service.py @@ -15,6 +15,19 @@ def get_dept_tree_services(result_db: Session, page_object: DeptModel): return dept_tree_result +def get_dept_tree_for_edit_option_services(result_db: Session, page_object: DeptModel): + """ + 获取部门编辑部门树信息service + :param result_db: orm对象 + :param page_object: 查询参数对象 + :return: 部门树信息对象 + """ + dept_list_result = get_dept_info_for_edit_option(result_db, page_object) + dept_tree_result = get_dept_tree(0, DeptTree(dept_tree=dept_list_result)) + + return dept_tree_result + + def get_dept_list_services(result_db: Session, page_object: DeptModel): """ 获取部门列表信息service @@ -27,6 +40,83 @@ def get_dept_list_services(result_db: Session, page_object: DeptModel): return dept_list_result +def add_dept_services(result_db: Session, page_object: DeptModel): + """ + 新增部门信息service + :param result_db: orm对象 + :param page_object: 新增部门对象 + :return: 新增部门校验结果 + """ + parent_info = get_dept_by_id(result_db, page_object.parent_id) + if parent_info: + page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}' + else: + page_object.ancestors = '0' + add_dept_result = add_dept_crud(result_db, page_object) + + return add_dept_result + + +def edit_dept_services(result_db: Session, page_object: DeptModel): + """ + 编辑部门信息service + :param result_db: orm对象 + :param page_object: 编辑部门对象 + :return: 编辑部门校验结果 + """ + parent_info = get_dept_by_id(result_db, page_object.parent_id) + if parent_info: + page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}' + else: + page_object.ancestors = '0' + edit_dept_result = edit_dept_crud(result_db, page_object) + update_children_info(result_db, DeptModel(dept_id=page_object.dept_id, + ancestors=page_object.ancestors, + update_by=page_object.update_by, + update_time=page_object.update_time + ) + ) + + return edit_dept_result + + +def delete_dept_services(result_db: Session, page_object: DeleteDeptModel): + """ + 删除部门信息service + :param result_db: orm对象 + :param page_object: 删除部门对象 + :return: 删除部门校验结果 + """ + if page_object.dept_ids.split(','): + dept_id_list = page_object.dept_ids.split(',') + ancestors = get_dept_all_ancestors(result_db) + for dept_id in dept_id_list: + for ancestor in ancestors: + if dept_id in ancestor[0]: + result = dict(is_success=False, message='该部门下有子部门,不允许删除') + + return CrudDeptResponse(**result) + + dept_id_dict = dict(dept_id=dept_id) + delete_dept_crud(result_db, DeptModel(**dept_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入用户id为空') + return CrudDeptResponse(**result) + + +def detail_dept_services(result_db: Session, dept_id: int): + """ + 获取部门详细信息service + :param result_db: orm对象 + :param dept_id: 部门id + :return: 部门id对应的信息 + """ + post = get_dept_detail_by_id(result_db, dept_id=dept_id) + + return post + + def get_dept_tree(pid: int, permission_list: DeptTree): """ 工具方法:根据部门信息生成树形嵌套数据 @@ -42,12 +132,37 @@ def get_dept_tree(pid: int, permission_list: DeptTree): if children: dept_list_data['title'] = permission.dept_name dept_list_data['key'] = str(permission.dept_id) - dept_list_data['value'] = permission.dept_id + dept_list_data['value'] = str(permission.dept_id) dept_list_data['children'] = children else: dept_list_data['title'] = permission.dept_name dept_list_data['key'] = str(permission.dept_id) - dept_list_data['value'] = permission.dept_id + dept_list_data['value'] = str(permission.dept_id) dept_list.append(dept_list_data) return dept_list + + +def update_children_info(result_db, page_object): + """ + 工具方法:递归更新子部门信息 + :param result_db: orm对象 + :param page_object: 编辑部门对象 + :return: + """ + children_info = get_children_dept(result_db, page_object.dept_id) + if children_info: + for child in children_info: + child.ancestors = f'{page_object.ancestors},{page_object.dept_id}' + edit_dept_crud(result_db, + DeptModel(dept_id=child.dept_id, + ancestors=child.ancestors, + update_by=page_object.update_by, + update_time=page_object.update_time + ) + ) + update_children_info(result_db, DeptModel(dept_id=child.dept_id, + ancestors=child.ancestors, + update_by=page_object.update_by, + update_time=page_object.update_time + )) diff --git a/dash-fastapi-frontend/api/dept.py b/dash-fastapi-frontend/api/dept.py index dc16967..b852f1d 100644 --- a/dash-fastapi-frontend/api/dept.py +++ b/dash-fastapi-frontend/api/dept.py @@ -4,3 +4,33 @@ from utils.request import api_request def get_dept_tree_api(page_obj: dict): return api_request(method='post', url='/system/dept/tree', is_headers=True, json=page_obj) + + +def get_dept_tree_for_edit_option_api(page_obj: dict): + + return api_request(method='post', url='/system/dept/forEditOption', is_headers=True, json=page_obj) + + +def get_dept_list_api(page_obj: dict): + + return api_request(method='post', url='/system/dept/get', is_headers=True, json=page_obj) + + +def add_dept_api(page_obj: dict): + + return api_request(method='post', url='/system/dept/add', is_headers=True, json=page_obj) + + +def edit_dept_api(page_obj: dict): + + return api_request(method='post', url='/system/dept/edit', is_headers=True, json=page_obj) + + +def delete_dept_api(page_obj: dict): + + return api_request(method='post', url='/system/dept/delete', is_headers=True, json=page_obj) + + +def get_dept_detail_api(dept_id: int): + + return api_request(method='get', url=f'/system/dept/{dept_id}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index 68e56a2..bb4bf04 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -11,27 +11,31 @@ from collections import OrderedDict from server import app from utils.tree_tool import get_dept_tree -from api.dept import get_dept_list_api +from api.dept import get_dept_tree_api, get_dept_list_api, add_dept_api, edit_dept_api, delete_dept_api, \ + get_dept_detail_api, get_dept_tree_for_edit_option_api @app.callback( [Output('dept-list-table', 'data', allow_duplicate=True), Output('dept-list-table', 'key'), Output('dept-list-table', 'defaultExpandedRowKeys'), - Output('api-check-token', 'data', allow_duplicate=True)], + Output('api-check-token', 'data', allow_duplicate=True), + Output('dept-fold', 'nClicks')], [Input('dept-search', 'nClicks'), - Input('dept-operations-store', 'data')], + Input('dept-operations-store', 'data'), + Input('dept-fold', 'nClicks')], [State('dept-dept_name-input', 'value'), - State('dept-status-select', 'value')], + State('dept-status-select', 'value'), + State('dept-list-table', 'defaultExpandedRowKeys')], prevent_initial_call=True ) -def get_dept_table_data(search_click, operations, dept_name, status_select): +def get_dept_table_data(search_click, operations, fold_click, dept_name, status_select, in_default_expanded_row_keys): query_params = dict( dept_name=dept_name, status=status_select ) - if search_click or operations: + if search_click or operations or fold_click: table_info = get_dept_list_api(query_params) default_expanded_row_keys = [] if table_info['code'] == 200: @@ -43,30 +47,37 @@ def get_dept_table_data(search_click, operations, dept_name, status_select): else: item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dept_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - }, - { - 'content': '新增', - 'type': 'link', - 'icon': 'antd-plus' - }, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - }, - ] + if item['parent_id'] == 0: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] table_data_new = get_dept_tree(0, table_data) - return [table_data_new, str(uuid.uuid4()), default_expanded_row_keys, {'timestamp': time.time()}] + if fold_click: + if in_default_expanded_row_keys: + return [table_data_new, str(uuid.uuid4()), [], {'timestamp': time.time()}, None] + + return [table_data_new, str(uuid.uuid4()), default_expanded_row_keys, {'timestamp': time.time()}, None] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}, None] - return [dash.no_update] * 4 + return [dash.no_update] * 4 + [None] @app.callback( @@ -81,3 +92,287 @@ def reset_dept_query_params(reset_click): return [None, None, {'type': 'reset'}] return [dash.no_update] * 3 + + +@app.callback( + [Output('dept-add-modal', 'visible', allow_duplicate=True), + Output('dept-add-parent_id', 'treeData'), + Output('dept-add-parent_id', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('dept-add', 'nClicks')], + [Input('dept-add', 'nClicks'), + Input('dept-list-table', 'nClicksButton')], + [State('dept-list-table', 'clickedContent'), + State('dept-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_user_modal(add_click, button_click, clicked_content, recently_button_clicked_row): + if add_click or (button_click and clicked_content == '新增'): + dept_params = dict(dept_name='') + tree_info = get_dept_tree_api(dept_params) + if tree_info['code'] == 200: + tree_data = tree_info['data'] + + return [ + True, + tree_data, + int(recently_button_clicked_row['key']) if recently_button_clicked_row else dash.no_update, + {'timestamp': time.time()}, + None + ] + + return [dash.no_update] * 3 + [{'timestamp': time.time()}, None] + + return [dash.no_update] * 4 + [None] + + +@app.callback( + [Output('dept-add-parent_id-form-item', 'validateStatus'), + Output('dept-add-dept_name-form-item', 'validateStatus'), + Output('dept-add-order_num-form-item', 'validateStatus'), + Output('dept-add-parent_id-form-item', 'help'), + Output('dept-add-dept_name-form-item', 'help'), + Output('dept-add-order_num-form-item', 'help'), + Output('dept-add-modal', 'visible', allow_duplicate=True), + Output('dept-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dept-add-modal', 'okCounts'), + [State('dept-add-parent_id', 'value'), + State('dept-add-dept_name', 'value'), + State('dept-add-order_num', 'value'), + State('dept-add-leader', 'value'), + State('dept-add-phone', 'value'), + State('dept-add-email', 'value'), + State('dept-add-status', 'value')], + prevent_initial_call=True +) +def dept_add_confirm(add_confirm, parent_id, dept_name, order_num, leader, phone, email, status): + if add_confirm: + + if all([parent_id, dept_name, order_num]): + params = dict(parent_id=parent_id, dept_name=dept_name, order_num=order_num, + leader=leader, phone=phone, email=email, status=status) + add_button_result = add_dept_api(params) + + if add_button_result['code'] == 200: + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增失败', type='error') + ] + + return [ + None if parent_id else 'error', + None if dept_name else 'error', + None if order_num else 'error', + None if parent_id else '请选择上级部门!', + None if dept_name else '请输入部门名称!', + None if order_num else '请输入显示排序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('dept-edit-modal', 'visible', allow_duplicate=True), + Output('dept-edit-parent_id', 'treeData'), + Output('dept-edit-parent_id', 'value'), + Output('dept-edit-dept_name', 'value'), + Output('dept-edit-order_num', 'value'), + Output('dept-edit-leader', 'value'), + Output('dept-edit-phone', 'value'), + Output('dept-edit-email', 'value'), + Output('dept-edit-status', 'value'), + Output('dept-edit-id-store', 'data'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dept-list-table', 'nClicksButton')], + [State('dept-list-table', 'clickedContent'), + State('dept-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def dept_edit_modal(button_click, clicked_content, recently_button_clicked_row): + if button_click: + + if clicked_content == '修改': + dept_id = int(recently_button_clicked_row['key']) + else: + return dash.no_update + + dept_params = dict(dept_id=dept_id) + tree_info = get_dept_tree_for_edit_option_api(dept_params) + edit_button_info = get_dept_detail_api(dept_id) + if edit_button_info['code'] == 200 and tree_info['code'] == 200: + edit_button_result = edit_button_info['data'] + tree_data = tree_info['data'] + + return [ + True, + tree_data, + edit_button_result['parent_id'], + edit_button_result['dept_name'], + edit_button_result['order_num'], + edit_button_result['leader'], + edit_button_result['phone'], + edit_button_result['email'], + edit_button_result['status'], + {'dept_id': dept_id}, + {'timestamp': time.time()} + ] + + return [dash.no_update] * 10 + [{'timestamp': time.time()}] + + return [dash.no_update] * 11 + + +@app.callback( + [Output('dept-edit-parent_id-form-item', 'validateStatus'), + Output('dept-edit-dept_name-form-item', 'validateStatus'), + Output('dept-edit-order_num-form-item', 'validateStatus'), + Output('dept-edit-parent_id-form-item', 'help'), + Output('dept-edit-dept_name-form-item', 'help'), + Output('dept-edit-order_num-form-item', 'help'), + Output('dept-edit-modal', 'visible', allow_duplicate=True), + Output('dept-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dept-edit-modal', 'okCounts'), + [State('dept-edit-parent_id', 'value'), + State('dept-edit-dept_name', 'value'), + State('dept-edit-order_num', 'value'), + State('dept-edit-leader', 'value'), + State('dept-edit-phone', 'value'), + State('dept-edit-email', 'value'), + State('dept-edit-status', 'value'), + State('dept-edit-id-store', 'data')], + prevent_initial_call=True +) +def dept_edit_confirm(edit_confirm, parent_id, dept_name, order_num, leader, phone, email, status, dept_id): + if edit_confirm: + + if all([parent_id, dept_name, order_num]): + params = dict(dept_id=dept_id['dept_id'], parent_id=parent_id, dept_name=dept_name, + order_num=order_num, leader=leader, phone=phone, email=email, + status=status) + edit_button_result = edit_dept_api(params) + + if edit_button_result['code'] == 200: + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑失败', type='error') + ] + + return [ + None if parent_id else 'error', + None if dept_name else 'error', + None if order_num else 'error', + None if parent_id else '请选择上级部门!', + None if dept_name else '请输入部门名称!', + None if order_num else '请输入显示排序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('dept-delete-text', 'children'), + Output('dept-delete-confirm-modal', 'visible'), + Output('dept-delete-ids-store', 'data')], + [Input('dept-list-table', 'nClicksButton')], + [State('dept-list-table', 'clickedContent'), + State('dept-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def dept_delete_modal(button_click, clicked_content, recently_button_clicked_row): + if button_click: + + if clicked_content == '删除': + dept_ids = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除部门编号为{dept_ids}的部门?', + True, + {'dept_ids': dept_ids} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('dept-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dept-delete-confirm-modal', 'okCounts'), + State('dept-delete-ids-store', 'data'), + prevent_initial_call=True +) +def dept_delete_confirm(delete_confirm, dept_ids_data): + if delete_confirm: + + params = dept_ids_data + delete_button_info = delete_dept_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/views/system/dept/index.py b/dash-fastapi-frontend/views/system/dept/index.py index 606a577..883061e 100644 --- a/dash-fastapi-frontend/views/system/dept/index.py +++ b/dash-fastapi-frontend/views/system/dept/index.py @@ -19,23 +19,26 @@ def render(): else: item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dept_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - }, - { - 'content': '新增', - 'type': 'link', - 'icon': 'antd-plus' - }, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - }, - ] + if item['parent_id'] == 0: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] table_data_new = get_dept_tree(0, table_data) return [ @@ -137,8 +140,7 @@ def render(): ), '展开/折叠', ], - id='dept-edit', - disabled=True, + id='dept-fold', style={ 'color': '#909399', 'background': '#f4f4f5', @@ -234,32 +236,18 @@ def render(): [ fac.AntdSpace( [ - fac.AntdFormItem( - fac.AntdInput( - id='user-add-nick_name', - placeholder='请输入用户昵称', - style={ - 'width': 200 - } - ), - label='用户昵称', - required=True, - id='user-add-nick_name-form-item' - ), fac.AntdFormItem( fac.AntdTreeSelect( - id='user-add-dept_id', - placeholder='请选择归属部门', + id='dept-add-parent_id', + placeholder='请选择上级部门', treeData=[], style={ - 'width': 200 + 'width': 500 } ), - label='归属部门', - id='user-add-dept_id-form-item', - labelCol={ - 'offset': 1 - }, + label='上级部门', + required=True, + id='dept-add-parent_id-form-item', ), ], size="middle" @@ -268,31 +256,26 @@ def render(): [ fac.AntdFormItem( fac.AntdInput( - id='user-add-phone_number', - placeholder='请输入手机号码', + id='dept-add-dept_name', + placeholder='请输入部门名称', style={ 'width': 200 } ), - label='手机号码', - id='user-add-phone_number-form-item', - labelCol={ - 'offset': 1 - }, + label='部门名称', + required=True, + id='dept-add-dept_name-form-item', ), fac.AntdFormItem( - fac.AntdInput( - id='user-add-email', - placeholder='请输入邮箱', + fac.AntdInputNumber( + id='dept-add-order_num', style={ 'width': 200 } ), - label='邮箱', - id='user-add-email-form-item', - labelCol={ - 'offset': 5 - }, + label='显示顺序', + required=True, + id='dept-add-order_num-form-item', ), ], size="middle" @@ -301,29 +284,31 @@ def render(): [ fac.AntdFormItem( fac.AntdInput( - id='user-add-user_name', - placeholder='请输入用户名称', + id='dept-add-leader', + placeholder='请输入负责人', style={ 'width': 200 } ), - label='用户名称', - required=True, - id='user-add-user_name-form-item' + label='负责人', + id='dept-add-leader-form-item', + labelCol={ + 'offset': 2 + }, ), fac.AntdFormItem( fac.AntdInput( - id='user-add-password', - placeholder='请输入密码', - mode='password', - passwordUseMd5=True, + id='dept-add-phone', + placeholder='请输入联系电话', style={ 'width': 200 } ), - label='用户密码', - required=True, - id='user-add-password-form-item' + label='联系电话', + id='dept-add-phone-form-item', + labelCol={ + 'offset': 3 + }, ), ], size="middle" @@ -331,36 +316,22 @@ def render(): fac.AntdSpace( [ fac.AntdFormItem( - fac.AntdSelect( - id='user-add-sex', - placeholder='请选择性别', - options=[ - { - 'label': '男', - 'value': '0' - }, - { - 'label': '女', - 'value': '1' - }, - { - 'label': '未知', - 'value': '2' - }, - ], + fac.AntdInput( + id='dept-add-email', + placeholder='请输入邮箱', style={ 'width': 200 } ), - label='用户性别', - id='user-add-sex-form-item', + label='邮箱', + id='dept-add-email-form-item', labelCol={ - 'offset': 1 + 'offset': 3 }, ), fac.AntdFormItem( fac.AntdRadioGroup( - id='user-add-status', + id='dept-add-status', options=[ { 'label': '正常', @@ -376,112 +347,73 @@ def render(): 'width': 200 } ), - label='用户状态', - id='user-add-status-form-item', - labelCol={ - 'offset': 2 - }, - ) - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdSelect( - id='user-add-post', - placeholder='请选择岗位', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-add-post-form-item', + label='部门状态', + id='dept-add-status-form-item', labelCol={ 'offset': 4 }, - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-add-role', - placeholder='请选择角色', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-add-role-form-item', - labelCol={ - 'offset': 8 - }, - ), + ) ], size="middle" ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-add-remark', - placeholder='请输入内容', - mode='text-area', - style={ - 'width': 490 - } - ), - label='备注', - id='user-add-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] - ) ] ) ], - id='user-add-modal', - title='新增用户', + id='dept-add-modal', + title='新增部门', mask=False, width=650, renderFooter=True, okClickClose=False ), - # 编辑用户表单modal + # 编辑部门表单modal fac.AntdModal( [ fac.AntdForm( [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdTreeSelect( + id='dept-edit-parent_id', + placeholder='请选择上级部门', + treeData=[], + style={ + 'width': 510 + } + ), + label='上级部门', + required=True, + id='dept-edit-parent_id-form-item', + ), + ], + size="middle" + ), fac.AntdSpace( [ fac.AntdFormItem( fac.AntdInput( - id='user-edit-nick_name', - placeholder='请输入用户昵称', + id='dept-edit-dept_name', + placeholder='请输入部门名称', style={ 'width': 200 } ), - label='用户昵称', + label='部门名称', required=True, - id='user-edit-nick_name-form-item' + id='dept-edit-dept_name-form-item', ), fac.AntdFormItem( - fac.AntdTreeSelect( - id='user-edit-dept_id', - placeholder='请选择归属部门', - treeData=[], + fac.AntdInputNumber( + id='dept-edit-order_num', style={ 'width': 200 } ), - label='归属部门', - id='user-edit-dept_id-form-item' + label='显示顺序', + required=True, + id='dept-edit-order_num-form-item', ), ], size="middle" @@ -490,30 +422,30 @@ def render(): [ fac.AntdFormItem( fac.AntdInput( - id='user-edit-phone_number', - placeholder='请输入手机号码', + id='dept-edit-leader', + placeholder='请输入负责人', style={ 'width': 200 } ), - label='手机号码', - id='user-edit-phone_number-form-item', + label='负责人', + id='dept-edit-leader-form-item', labelCol={ - 'offset': 1 + 'offset': 2 }, ), fac.AntdFormItem( fac.AntdInput( - id='user-edit-email', - placeholder='请输入邮箱', + id='dept-edit-phone', + placeholder='请输入联系电话', style={ 'width': 200 } ), - label='邮箱', - id='user-edit-email-form-item', + label='联系电话', + id='dept-edit-phone-form-item', labelCol={ - 'offset': 4 + 'offset': 3 }, ), ], @@ -522,36 +454,22 @@ def render(): fac.AntdSpace( [ fac.AntdFormItem( - fac.AntdSelect( - id='user-edit-sex', - placeholder='请选择性别', - options=[ - { - 'label': '男', - 'value': '0' - }, - { - 'label': '女', - 'value': '1' - }, - { - 'label': '未知', - 'value': '2' - }, - ], + fac.AntdInput( + id='dept-edit-email', + placeholder='请输入邮箱', style={ 'width': 200 } ), - label='用户性别', - id='user-edit-sex-form-item', + label='邮箱', + id='dept-edit-email-form-item', labelCol={ - 'offset': 1 + 'offset': 3 }, ), fac.AntdFormItem( fac.AntdRadioGroup( - id='user-edit-status', + id='dept-edit-status', options=[ { 'label': '正常', @@ -562,110 +480,35 @@ def render(): 'value': '1' }, ], + defaultValue='0', style={ 'width': 200 } ), - label='用户状态', - id='user-edit-status-form-item', + label='部门状态', + id='dept-edit-status-form-item', labelCol={ - 'offset': 1 + 'offset': 3 }, ) ], size="middle" ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdSelect( - id='user-edit-post', - placeholder='请选择岗位', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-edit-post-form-item', - labelCol={ - 'offset': 4 - }, - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-edit-role', - placeholder='请选择角色', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-edit-role-form-item', - labelCol={ - 'offset': 7 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-edit-remark', - placeholder='请输入内容', - mode='text-area', - style={ - 'width': 485 - } - ), - label='备注', - id='user-edit-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] - ) ] ) ], - id='user-edit-modal', - title='编辑用户', + id='dept-edit-modal', + title='编辑部门', mask=False, width=650, renderFooter=True, okClickClose=False ), - # 删除用户二次确认modal + # 删除部门二次确认modal fac.AntdModal( - fac.AntdText('是否确认删除?', id='delete-text'), - id='user-delete-confirm-modal', - visible=False, - title='提示', - renderFooter=True, - centered=True - ), - - # 重置密码modal - fac.AntdModal( - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='reset-password-input', - mode='password' - ), - ), - ], - layout='vertical' - ), - id='user-reset-password-confirm-modal', + fac.AntdText('是否确认删除?', id='dept-delete-text'), + id='dept-delete-confirm-modal', visible=False, title='提示', renderFooter=True, -- Gitee From ded72b74d534293606bbd77dbdf44795eed97a16 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sat, 10 Jun 2023 15:32:17 +0800 Subject: [PATCH 006/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=EF=BC=88=E5=88=A0=E6=9F=A5=E5=B7=B2?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89=20fix:=E4=BF=AE=E5=A4=8D=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=88=97=E8=A1=A8=E6=9F=A5=E8=AF=A2=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=BC=82=E5=B8=B8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../controller/login_controller.py | 14 +- .../controller/menu_controller.py | 139 +++ dash-fastapi-backend/mapper/crud/dept_crud.py | 11 +- .../mapper/crud/login_crud.py | 2 +- dash-fastapi-backend/mapper/crud/menu_crud.py | 109 +++ .../mapper/schema/menu_schema.py | 75 ++ dash-fastapi-backend/service/login_service.py | 4 +- dash-fastapi-backend/service/menu_service.py | 124 +++ dash-fastapi-frontend/api/menu.py | 36 + dash-fastapi-frontend/app.py | 2 +- dash-fastapi-frontend/callbacks/login_c.py | 2 +- .../menu_c/components_c/content_type_c.py | 117 +++ .../callbacks/system_c/menu_c/menu_c.py | 282 ++++++ dash-fastapi-frontend/store/store.py | 13 + dash-fastapi-frontend/utils/request.py | 20 +- dash-fastapi-frontend/utils/tree_tool.py | 27 + dash-fastapi-frontend/views/layout/index.py | 139 --- .../views/monitor/logininfor/__init__.py | 11 +- .../views/monitor/logininfor/index.py | 8 - .../views/monitor/operlog/__init__.py | 11 +- .../views/monitor/operlog/index.py | 8 - .../views/system/config/__init__.py | 11 +- .../views/system/config/index.py | 8 - .../views/system/dept/__init__.py | 534 ++++++++++- .../views/system/dept/index.py | 517 ----------- .../views/system/dict/__init__.py | 11 +- .../views/system/dict/index.py | 8 - .../views/system/menu/__init__.py | 436 ++++++++- .../views/system/menu/components/__init__.py | 5 + .../system/menu/components/button_type.py | 40 + .../system/menu/components/content_type.py | 159 ++++ .../views/system/menu/components/menu_type.py | 288 ++++++ .../views/system/menu/index.py | 8 - .../views/system/notice/__init__.py | 11 +- .../views/system/notice/index.py | 8 - .../views/system/post/__init__.py | 486 ++++++++++- .../views/system/post/index.py | 473 ---------- .../views/system/role/__init__.py | 11 +- .../views/system/role/index.py | 8 - .../views/system/user/__init__.py | 826 +++++++++++++++++- .../views/system/user/index.py | 804 ----------------- 42 files changed, 3767 insertions(+), 2041 deletions(-) create mode 100644 dash-fastapi-backend/controller/menu_controller.py create mode 100644 dash-fastapi-backend/mapper/crud/menu_crud.py create mode 100644 dash-fastapi-backend/mapper/schema/menu_schema.py create mode 100644 dash-fastapi-backend/service/menu_service.py create mode 100644 dash-fastapi-frontend/api/menu.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py delete mode 100644 dash-fastapi-frontend/views/layout/index.py delete mode 100644 dash-fastapi-frontend/views/monitor/logininfor/index.py delete mode 100644 dash-fastapi-frontend/views/monitor/operlog/index.py delete mode 100644 dash-fastapi-frontend/views/system/config/index.py delete mode 100644 dash-fastapi-frontend/views/system/dept/index.py delete mode 100644 dash-fastapi-frontend/views/system/dict/index.py create mode 100644 dash-fastapi-frontend/views/system/menu/components/__init__.py create mode 100644 dash-fastapi-frontend/views/system/menu/components/button_type.py create mode 100644 dash-fastapi-frontend/views/system/menu/components/content_type.py create mode 100644 dash-fastapi-frontend/views/system/menu/components/menu_type.py delete mode 100644 dash-fastapi-frontend/views/system/menu/index.py delete mode 100644 dash-fastapi-frontend/views/system/notice/index.py delete mode 100644 dash-fastapi-frontend/views/system/post/index.py delete mode 100644 dash-fastapi-frontend/views/system/role/index.py delete mode 100644 dash-fastapi-frontend/views/system/user/index.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 3617fc4..543e5ed 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware from controller.login_controller import loginController from controller.user_controller import userController +from controller.menu_controller import menuController from controller.dept_controller import deptController from controller.role_controller import roleController from controller.post_controler import postController @@ -63,6 +64,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): app.include_router(loginController, prefix="/login", tags=['login']) app.include_router(userController, prefix="/system", tags=['system/user']) +app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) diff --git a/dash-fastapi-backend/controller/login_controller.py b/dash-fastapi-backend/controller/login_controller.py index 91d2848..7370d35 100644 --- a/dash-fastapi-backend/controller/login_controller.py +++ b/dash-fastapi-backend/controller/login_controller.py @@ -18,21 +18,17 @@ loginController = APIRouter() async def login(request: Request, user: UserLogin, query_db: Session = Depends(get_db)): try: result = authenticate_user(query_db, user.user_name, user.password) - if result == '用户不存在': - logger.warning('用户不存在') - return response_400(data="", message="用户不存在") - - elif result == '密码错误': - logger.warning('密码错误') - return response_400(data="", message="密码错误") + if result == '用户不存在' or result == '密码错误' or result == '用户已停用': + logger.warning(result) + return response_400(data="", message=result) else: access_token_expires = timedelta(minutes=JwtConfig.ACCESS_TOKEN_EXPIRE_MINUTES) try: + session_id = str(uuid.uuid4()) access_token = create_access_token( - data={"sub": str(result.user_id)}, expires_delta=access_token_expires + data={"user_id": str(result.user_id), "session_id": session_id}, expires_delta=access_token_expires ) - session_id = str(uuid.uuid4()) await request.app.state.redis.set(f'{result.user_id}_access_token', access_token, ex=timedelta(minutes=30)) await request.app.state.redis.set(f'{result.user_id}_session_id', session_id, ex=timedelta(minutes=30)) logger.info('登录成功') diff --git a/dash-fastapi-backend/controller/menu_controller.py b/dash-fastapi-backend/controller/menu_controller.py new file mode 100644 index 0000000..bb72b4f --- /dev/null +++ b/dash-fastapi-backend/controller/menu_controller.py @@ -0,0 +1,139 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, HTTPException, Header +from config.get_db import get_db +from service.login_service import get_current_user, get_password_hash +from service.menu_service import * +from mapper.schema.menu_schema import * +from mapper.crud.menu_crud import * +from utils.response_tool import * +from utils.log_tool import * + + +menuController = APIRouter() + + +@menuController.post("/menu/tree", response_model=MenuTree) +async def get_system_menu_tree(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + menu_query_result = get_menu_tree_services(query_db, menu_query) + logger.info('获取成功') + return response_200(data=menu_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@menuController.post("/menu/forEditOption", response_model=MenuTree) +async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) + logger.info('获取成功') + return response_200(data=menu_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@menuController.post("/menu/get", response_model=MenuResponse) +async def get_system_menu_list(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + menu_query_result = get_menu_list_services(query_db, menu_query) + logger.info('获取成功') + return response_200(data=menu_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@menuController.post("/menu/add", response_model=CrudMenuResponse) +async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + add_menu.create_by = current_user.user.user_name + add_menu.update_by = current_user.user.user_name + add_menu_result = add_menu_services(query_db, add_menu) + logger.info(add_menu_result.message) + if add_menu_result.is_success: + return response_200(data=add_menu_result, message=add_menu_result.message) + else: + return response_400(data="", message=add_menu_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@menuController.post("/menu/edit", response_model=CrudMenuResponse) +async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + edit_menu.update_by = current_user.user.user_name + edit_menu.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_menu_result = edit_menu_services(query_db, edit_menu) + if edit_menu_result.is_success: + logger.info(edit_menu_result.message) + return response_200(data=edit_menu_result, message=edit_menu_result.message) + else: + logger.warning(edit_menu_result.message) + return response_400(data="", message=edit_menu_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@menuController.post("/menu/delete", response_model=CrudMenuResponse) +async def delete_system_menu(request: Request, delete_menu: DeleteMenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + delete_menu_result = delete_menu_services(query_db, delete_menu) + if delete_menu_result.is_success: + logger.info(delete_menu_result.message) + return response_200(data=delete_menu_result, message=delete_menu_result.message) + else: + logger.warning(delete_menu_result.message) + return response_400(data="", message=delete_menu_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@menuController.get("/menu/{menu_id}", response_model=MenuModel) +async def query_detail_system_menu(request: Request, menu_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": + logger.warning(current_user) + return response_401(data="", message=current_user) + else: + detail_menu_result = detail_menu_services(query_db, menu_id) + logger.info(f'获取menu_id为{menu_id}的信息成功') + return response_200(data=detail_menu_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/mapper/crud/dept_crud.py index 9d8b9a3..c676f78 100644 --- a/dash-fastapi-backend/mapper/crud/dept_crud.py +++ b/dash-fastapi-backend/mapper/crud/dept_crud.py @@ -16,6 +16,15 @@ def get_dept_by_id(db: Session, dept_id: int): return dept_info +def get_dept_by_id_for_list(db: Session, dept_id: int): + dept_info = db.query(SysDept) \ + .filter(SysDept.dept_id == dept_id, + SysDept.del_flag == 0) \ + .first() + + return dept_info + + def get_dept_detail_by_id(db: Session, dept_id: int): dept_info = db.query(SysDept) \ .filter(SysDept.dept_id == dept_id, @@ -101,7 +110,7 @@ def get_dept_list(db: Session, page_object: DeptModel): ancestor_info = dept_query.ancestors.split(',') ancestor_info.append(dept_query.dept_id) for ancestor in ancestor_info: - dept_item = get_dept_by_id(db, int(ancestor)) + dept_item = get_dept_by_id_for_list(db, int(ancestor)) if dept_item: dept.append(dept_item) # 去重 diff --git a/dash-fastapi-backend/mapper/crud/login_crud.py b/dash-fastapi-backend/mapper/crud/login_crud.py index d27cf11..f7ce5d8 100644 --- a/dash-fastapi-backend/mapper/crud/login_crud.py +++ b/dash-fastapi-backend/mapper/crud/login_crud.py @@ -11,7 +11,7 @@ def login_by_account(db: Session, user_name: str): :return: 用户对象 """ user = db.query(SysUser).\ - filter_by(user_name=user_name).\ + filter(SysUser.user_name == user_name, SysUser.del_flag == '0').\ distinct().\ first() diff --git a/dash-fastapi-backend/mapper/crud/menu_crud.py b/dash-fastapi-backend/mapper/crud/menu_crud.py new file mode 100644 index 0000000..69bf3a6 --- /dev/null +++ b/dash-fastapi-backend/mapper/crud/menu_crud.py @@ -0,0 +1,109 @@ +from sqlalchemy import and_, or_ +from sqlalchemy.orm import Session +from entity.menu_entity import SysMenu +from mapper.schema.menu_schema import MenuModel, MenuResponse, CrudMenuResponse +from utils.time_format_tool import list_format_datetime +from utils.page_tool import get_page_info + + +def get_menu_detail_by_id(db: Session, menu_id: int): + menu_info = db.query(SysMenu) \ + .filter(SysMenu.menu_id == menu_id) \ + .first() + + return menu_info + + +def get_menu_info_for_edit_option(db: Session, menu_info: MenuModel): + menu_result = db.query(SysMenu) \ + .filter(SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, + SysMenu.status == 0) \ + .all() + + return list_format_datetime(menu_result) + + +def get_menu_list_for_tree(db: Session, menu_info: MenuModel): + menu_query_all = db.query(SysMenu) \ + .filter(SysMenu.status == 0, + SysMenu.menu_name.like(f'%{menu_info.menu_name}%') if menu_info.menu_name else True) \ + .order_by(SysMenu.order_num) \ + .distinct().all() + + return list_format_datetime(menu_query_all) + + +def get_menu_list(db: Session, page_object: MenuModel): + """ + 根据查询参数获取菜单列表信息 + :param db: orm对象 + :param page_object: 不分页查询参数对象 + :return: 菜单列表信息对象 + """ + if page_object.menu_name or page_object.status: + menu_query_all = db.query(SysMenu) \ + .filter(SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like(f'%{page_object.menu_name}%') if page_object.menu_name else True) \ + .order_by(SysMenu.order_num)\ + .distinct().all() + else: + menu_query_all = db.query(SysMenu) \ + .order_by(SysMenu.order_num) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(menu_query_all), + ) + + return MenuResponse(**result) + + +def add_menu_crud(db: Session, menu: MenuModel): + """ + 新增菜单数据库操作 + :param db: orm对象 + :param menu: 菜单对象 + :return: 新增校验结果 + """ + db_menu = SysMenu(**menu.dict()) + db.add(db_menu) + db.commit() # 提交保存到数据库中 + db.refresh(db_menu) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudMenuResponse(**result) + + +def edit_menu_crud(db: Session, menu: MenuModel): + """ + 编辑菜单数据库操作 + :param db: orm对象 + :param menu: 菜单对象 + :return: 编辑校验结果 + """ + is_menu_id = db.query(SysMenu).filter(SysMenu.menu_id == menu.menu_id).all() + if not is_menu_id: + result = dict(is_success=False, message='菜单不存在') + else: + # 筛选出属性值为不为None和''的 + filtered_dict = {k: v for k, v in menu.dict().items() if v is not None and v != ''} + db.query(SysMenu) \ + .filter(SysMenu.menu_id == menu.menu_id) \ + .update(filtered_dict) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudMenuResponse(**result) + + +def delete_menu_crud(db: Session, menu: MenuModel): + """ + 删除菜单数据库操作 + :param db: orm对象 + :param menu: 菜单对象 + :return: + """ + db.query(SysMenu) \ + .filter(SysMenu.menu_id == menu.menu_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/mapper/schema/menu_schema.py b/dash-fastapi-backend/mapper/schema/menu_schema.py new file mode 100644 index 0000000..b9453a5 --- /dev/null +++ b/dash-fastapi-backend/mapper/schema/menu_schema.py @@ -0,0 +1,75 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class MenuModel(BaseModel): + menu_id: Optional[int] + menu_name: Optional[str] + parent_id: Optional[int] + order_num: Optional[int] + path: Optional[str] + component: Optional[str] + query: Optional[str] + is_frame: Optional[int] + is_cache: Optional[int] + menu_type: Optional[str] + visible: Optional[str] + status: Optional[str] + perms: Optional[str] + icon: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class MenuPageObject(MenuModel): + """ + 菜单管理分页查询模型 + """ + page_num: int + page_size: int + + +class MenuPageObjectResponse(BaseModel): + """ + 菜单管理列表分页查询返回模型 + """ + rows: List[Union[MenuModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class MenuResponse(BaseModel): + """ + 菜单管理列表不分页查询返回模型 + """ + rows: List[Union[MenuModel, None]] = [] + + +class MenuTree(BaseModel): + """ + 菜单树响应模型 + """ + menu_tree: Union[List, None] + + +class CrudMenuResponse(BaseModel): + """ + 操作菜单响应模型 + """ + is_success: bool + message: str + + +class DeleteMenuModel(BaseModel): + """ + 删除菜单模型 + """ + menu_ids: str diff --git a/dash-fastapi-backend/service/login_service.py b/dash-fastapi-backend/service/login_service.py index ee67c66..1fc4610 100644 --- a/dash-fastapi-backend/service/login_service.py +++ b/dash-fastapi-backend/service/login_service.py @@ -23,7 +23,7 @@ async def get_current_user(request: Request, token: str, result_db: Session): return "用户token不合法" try: payload = jwt.decode(token[6:], JwtConfig.SECRET_KEY, algorithms=[JwtConfig.ALGORITHM]) - user_id: str = payload.get("sub") + user_id: str = payload.get("user_id") if user_id is None: return "用户token不合法" token_data = TokenData(user_id=int(user_id)) @@ -101,6 +101,8 @@ def authenticate_user(query_db: Session, user_name: str, input_password: str): return '用户不存在' if not verify_password(input_password, user.password): return '密码错误' + if user.status == '1': + return '用户已停用' return user diff --git a/dash-fastapi-backend/service/menu_service.py b/dash-fastapi-backend/service/menu_service.py new file mode 100644 index 0000000..d678414 --- /dev/null +++ b/dash-fastapi-backend/service/menu_service.py @@ -0,0 +1,124 @@ +from mapper.schema.menu_schema import * +from mapper.crud.menu_crud import * + + +def get_menu_tree_services(result_db: Session, page_object: MenuModel): + """ + 获取菜单树信息service + :param result_db: orm对象 + :param page_object: 查询参数对象 + :return: 菜单树信息对象 + """ + menu_tree_option = [] + menu_list_result = get_menu_list_for_tree(result_db, page_object) + menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) + menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) + + return menu_tree_option + + +def get_menu_tree_for_edit_option_services(result_db: Session, page_object: MenuModel): + """ + 获取菜单编辑菜单树信息service + :param result_db: orm对象 + :param page_object: 查询参数对象 + :return: 菜单树信息对象 + """ + menu_tree_option = [] + menu_list_result = get_menu_info_for_edit_option(result_db, page_object) + menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) + menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) + + return menu_tree_option + + +def get_menu_list_services(result_db: Session, page_object: MenuModel): + """ + 获取菜单列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 菜单列表信息对象 + """ + menu_list_result = get_menu_list(result_db, page_object) + + return menu_list_result + + +def add_menu_services(result_db: Session, page_object: MenuModel): + """ + 新增菜单信息service + :param result_db: orm对象 + :param page_object: 新增菜单对象 + :return: 新增菜单校验结果 + """ + add_menu_result = add_menu_crud(result_db, page_object) + + return add_menu_result + + +def edit_menu_services(result_db: Session, page_object: MenuModel): + """ + 编辑菜单信息service + :param result_db: orm对象 + :param page_object: 编辑部门对象 + :return: 编辑菜单校验结果 + """ + edit_menu_result = edit_menu_crud(result_db, page_object) + + return edit_menu_result + + +def delete_menu_services(result_db: Session, page_object: DeleteMenuModel): + """ + 删除菜单信息service + :param result_db: orm对象 + :param page_object: 删除菜单对象 + :return: 删除菜单校验结果 + """ + if page_object.menu_ids.split(','): + menu_id_list = page_object.menu_ids.split(',') + for menu_id in menu_id_list: + menu_id_dict = dict(menu_id=menu_id) + delete_menu_crud(result_db, MenuModel(**menu_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入用户id为空') + return CrudMenuResponse(**result) + + +def detail_menu_services(result_db: Session, menu_id: int): + """ + 获取菜单详细信息service + :param result_db: orm对象 + :param menu_id: 菜单id + :return: 菜单id对应的信息 + """ + menu = get_menu_detail_by_id(result_db, menu_id=menu_id) + + return menu + + +def get_menu_tree(pid: int, permission_list: MenuTree): + """ + 工具方法:根据菜单信息生成树形嵌套数据 + :param pid: 菜单id + :param permission_list: 菜单列表信息 + :return: 菜单树形嵌套数据 + """ + menu_list = [] + for permission in permission_list.menu_tree: + if permission.parent_id == pid: + children = get_menu_tree(permission.menu_id, permission_list) + menu_list_data = {} + if children: + menu_list_data['title'] = permission.menu_name + menu_list_data['key'] = str(permission.menu_id) + menu_list_data['value'] = str(permission.menu_id) + menu_list_data['children'] = children + else: + menu_list_data['title'] = permission.menu_name + menu_list_data['key'] = str(permission.menu_id) + menu_list_data['value'] = str(permission.menu_id) + menu_list.append(menu_list_data) + + return menu_list diff --git a/dash-fastapi-frontend/api/menu.py b/dash-fastapi-frontend/api/menu.py new file mode 100644 index 0000000..7752cae --- /dev/null +++ b/dash-fastapi-frontend/api/menu.py @@ -0,0 +1,36 @@ +from utils.request import api_request + + +def get_menu_tree_api(page_obj: dict): + + return api_request(method='post', url='/system/menu/tree', is_headers=True, json=page_obj) + + +def get_menu_tree_for_edit_option_api(page_obj: dict): + + return api_request(method='post', url='/system/menu/forEditOption', is_headers=True, json=page_obj) + + +def get_menu_list_api(page_obj: dict): + + return api_request(method='post', url='/system/menu/get', is_headers=True, json=page_obj) + + +def add_menu_api(page_obj: dict): + + return api_request(method='post', url='/system/menu/add', is_headers=True, json=page_obj) + + +def edit_menu_api(page_obj: dict): + + return api_request(method='post', url='/system/menu/edit', is_headers=True, json=page_obj) + + +def delete_menu_api(page_obj: dict): + + return api_request(method='post', url='/system/menu/delete', is_headers=True, json=page_obj) + + +def get_menu_detail_api(menu_id: int): + + return api_request(method='get', url=f'/system/menu/{menu_id}', is_headers=True) diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index abf759c..6c5a23a 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -112,7 +112,7 @@ def router(pathname, trigger): # 否则正常渲染主页面 return [ - views.layout.index.render_content(user_name, nick_name, phone_number, menu_info), + views.layout.render_content(user_name, nick_name, phone_number, menu_info), None, fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), {'timestamp': time.time()}, diff --git a/dash-fastapi-frontend/callbacks/login_c.py b/dash-fastapi-frontend/callbacks/login_c.py index a3ac415..6272036 100644 --- a/dash-fastapi-frontend/callbacks/login_c.py +++ b/dash-fastapi-frontend/callbacks/login_c.py @@ -35,7 +35,7 @@ def login_auth(nClicks, username, password, captcha, input_captcha): if captcha == input_captcha: try: - user_params = dict(user_name=username, password=password, request=str(request.headers)) + user_params = dict(user_name=username, password=password, user_request=str(request.headers)) userinfo_result = login_api(user_params) if userinfo_result['code'] == 200: token = userinfo_result['data']['token'] diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py new file mode 100644 index 0000000..a1dfb66 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py @@ -0,0 +1,117 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc +from jsonpath_ng import parse +from flask import session, json +from collections import OrderedDict + +from server import app +from utils.tree_tool import list_to_tree +from views.system.menu.components import * +from api.menu import add_menu_api, edit_menu_api + + +@app.callback( + [Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('content-menu-path-form-item', 'validateStatus'), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True), + Output('content-menu-path-form-item', 'help'), + Output('menu-modal', 'visible', allow_duplicate=True), + Output('menu-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('menu-modal-M-trigger', 'data'), + [State('menu-operations-store-bk', 'data'), + State('menu-parent_id', 'value'), + State('menu-menu_type', 'value'), + State('menu-icon', 'value'), + State('menu-menu_name', 'value'), + State('menu-order_num', 'value'), + State('content-menu-is_frame', 'value'), + State('content-menu-path', 'value'), + State('content-menu-visible', 'value'), + State('content-menu-status', 'value')], + prevent_initial_call=True +) +def menu_confirm(confirm_trigger, operation_type, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, visible, status): + if confirm_trigger: + if all([parent_id, menu_name, order_num, path]): + params = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, + is_frame=is_frame, path=path, visible=visible, status=status) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_menu_api(params) + if operation_type == 'edit': + api_res = edit_menu_api(params) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if parent_id else 'error', + None if menu_name else 'error', + None if order_num else 'error', + None if path else 'error', + None if parent_id else '请选择上级菜单!', + None if menu_name else '请输入菜单名称!', + None if order_num else '请输入显示排序!', + None if path else '请输入路由地址!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 12 diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py new file mode 100644 index 0000000..9a3ea86 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -0,0 +1,282 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc +from jsonpath_ng import parse +from flask import session, json +from collections import OrderedDict + +from server import app +from utils.tree_tool import list_to_tree +from views.system.menu.components import content_type, menu_type, button_type +from api.menu import get_menu_tree_api, get_menu_tree_for_edit_option_api, get_menu_list_api, delete_menu_api, get_menu_detail_api + + +@app.callback( + [Output('menu-list-table', 'data', allow_duplicate=True), + Output('menu-list-table', 'key'), + Output('menu-list-table', 'defaultExpandedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('menu-fold', 'nClicks')], + [Input('menu-search', 'nClicks'), + Input('menu-operations-store', 'data'), + Input('menu-fold', 'nClicks')], + [State('menu-menu_name-input', 'value'), + State('menu-status-select', 'value'), + State('menu-list-table', 'defaultExpandedRowKeys')], + prevent_initial_call=True +) +def get_menu_table_data(search_click, operations, fold_click, menu_name, status_select, in_default_expanded_row_keys): + + query_params = dict( + menu_name=menu_name, + status=status_select + ) + if search_click or operations or fold_click: + table_info = get_menu_list_api(query_params) + default_expanded_row_keys = [] + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + for item in table_data: + default_expanded_row_keys.append(str(item['menu_id'])) + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['menu_id']) + item['icon'] = [ + { + 'type': 'link', + 'icon': item['icon'], + 'disabled': True, + 'style': { + 'color': 'rgba(0, 0, 0, 0.8)' + } + }, + ] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + table_data_new = list_to_tree(table_data) + + if fold_click: + if not in_default_expanded_row_keys: + return [table_data_new, str(uuid.uuid4()), default_expanded_row_keys, {'timestamp': time.time()}, None] + + return [table_data_new, str(uuid.uuid4()), [], {'timestamp': time.time()}, None] + + return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}, None] + + return [dash.no_update] * 4 + [None] + + +@app.callback( + [Output('menu-menu_name-input', 'value'), + Output('menu-status-select', 'value'), + Output('menu-operations-store', 'data')], + Input('menu-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_menu_query_params(reset_click): + if reset_click: + return [None, None, {'type': 'reset'}] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('menu-modal', 'visible', allow_duplicate=True), + Output('menu-modal', 'title'), + Output('menu-parent_id', 'treeData'), + Output('menu-parent_id', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('menu-add', 'nClicks'), + Output('menu-edit-id-store', 'data'), + Output('menu-operations-store-bk', 'data')], + [Input('menu-add', 'nClicks'), + Input('menu-list-table', 'nClicksButton')], + [State('menu-list-table', 'clickedContent'), + State('menu-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_menu_modal(add_click, button_click, clicked_content, recently_button_clicked_row): + if add_click or button_click: + menu_params = dict(menu_name='') + if clicked_content == '修改': + tree_info = get_menu_tree_for_edit_option_api(menu_params) + else: + tree_info = get_menu_tree_api(menu_params) + if tree_info['code'] == 200: + tree_data = tree_info['data'] + + if add_click: + return [ + True, + '新增菜单', + tree_data, + '0', + {'timestamp': time.time()}, + None, + None, + {'type': 'add'} + ] + elif button_click and clicked_content == '新增': + return [ + True, + '新增菜单', + tree_data, + str(recently_button_clicked_row['key']), + {'timestamp': time.time()}, + None, + 'add', + {'type': 'add'} + ] + else: + menu_id = int(recently_button_clicked_row['key']) + menu_info_res = get_menu_detail_api(menu_id=menu_id) + if menu_info_res['code'] == 200: + menu_info = menu_info_res['data'] + return [ + True, + '编辑菜单', + tree_data, + str(menu_info.get('parent_id')), + {'timestamp': time.time()}, + None, + menu_info, + {'type': 'edit'} + ] + + return [dash.no_update] * 4 + [{'timestamp': time.time()}, None, None, None] + + return [dash.no_update] * 5 + [None, None, None] + + +@app.callback( + [Output('content-by-menu-type', 'children'), + Output('content-by-menu-type', 'key'), + Output('menu-modal-menu-type-store', 'data')], + Input('menu-menu_type', 'value'), + prevent_initial_call=True +) +def get_bottom_content(menu_value): + """ + 根据不同菜单类型渲染不同的子区域 + """ + if menu_value == 'M': + return [content_type.render(), str(uuid.uuid4()), {'type': 'M'}] + + elif menu_value == 'C': + return [menu_type.render(), str(uuid.uuid4()), {'type': 'C'}] + + elif menu_value == 'F': + return [button_type.render(), str(uuid.uuid4()), {'type': 'F'}] + + return dash.no_update + + +@app.callback( + [Output('menu-modal-M-trigger', 'data'), + Output('menu-modal-C-trigger', 'data'), + Output('menu-modal-F-trigger', 'data')], + Input('menu-modal', 'okCounts'), + State('menu-modal-menu-type-store', 'data'), +) +def modal_confirm_trigger(confirm, menu_type): + """ + 增加触发器,根据不同菜单类型触发不同的回调,解决组件不存在回调异常的问题 + """ + if confirm: + if menu_type.get('type') == 'M': + return [ + {'timestamp': time.time()}, + dash.no_update, + dash.no_update + ] + if menu_type.get('type') == 'C': + return [ + dash.no_update, + {'timestamp': time.time()}, + dash.no_update + ] + + if menu_type.get('type') == 'F': + return [ + dash.no_update, + dash.no_update, + {'timestamp': time.time()} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('menu-delete-text', 'children'), + Output('menu-delete-confirm-modal', 'visible'), + Output('menu-delete-ids-store', 'data')], + [Input('menu-list-table', 'nClicksButton')], + [State('menu-list-table', 'clickedContent'), + State('menu-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def menu_delete_modal(button_click, clicked_content, recently_button_clicked_row): + if button_click: + + if clicked_content == '删除': + menu_ids = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除菜单编号为{menu_ids}的菜单?', + True, + {'menu_ids': menu_ids} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('menu-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('menu-delete-confirm-modal', 'okCounts'), + State('menu-delete-ids-store', 'data'), + prevent_initial_call=True +) +def menu_delete_confirm(delete_confirm, menu_ids_data): + if delete_confirm: + + params = menu_ids_data + delete_button_info = delete_menu_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 1729595..444a478 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -19,6 +19,19 @@ def render_store_container(): dcc.Store(id='user-edit-id-store'), # 用户管理模块删除操作行key存储容器 dcc.Store(id='user-delete-ids-store'), + # 菜单管理模块操作类型存储容器 + dcc.Store(id='menu-operations-store'), + dcc.Store(id='menu-operations-store-bk'), + # modal菜单类型存储容器 + dcc.Store(id='menu-modal-menu-type-store'), + # 不同菜单类型的触发器 + dcc.Store(id='menu-modal-M-trigger'), + dcc.Store(id='menu-modal-C-trigger'), + dcc.Store(id='menu-modal-F-trigger'), + # 菜单管理模块修改操作行key存储容器 + dcc.Store(id='menu-edit-id-store'), + # 菜单管理模块删除操作行key存储容器 + dcc.Store(id='menu-delete-ids-store'), # 部门管理模块操作类型存储容器 dcc.Store(id='dept-operations-store'), # 部门管理模块修改操作行key存储容器 diff --git a/dash-fastapi-frontend/utils/request.py b/dash-fastapi-frontend/utils/request.py index 40f138a..c2c5f30 100644 --- a/dash-fastapi-frontend/utils/request.py +++ b/dash-fastapi-frontend/utils/request.py @@ -1,9 +1,8 @@ import requests from typing import Optional -from flask import session +from flask import session, request from config.global_config import ApiBaseUrlConfig from server import logger -from flask import request def api_request(method: str, url: str, is_headers: bool, params: Optional[dict] = None, data: Optional[dict] = None, @@ -29,18 +28,23 @@ def api_request(method: str, url: str, is_headers: bool, params: Optional[dict] else: raise ValueError(f'Unsupported HTTP method: {method}') - response_code = response.json()['code'] - response_message = response.json()['message'] + data_list = [params, data, json] + response_code = response.json().get('code') + response_message = response.json().get('message') session['code'] = response_code session['message'] = response_message if response_code == 200: - logger.info("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求结果:{}", + logger.info("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求参数:{}||请求结果:{}", session.get('user_info').get('user_name') if session.get('user_info') else None, - request.remote_addr, method, url, response_message) + request.remote_addr, method, url, + ','.join([str(x) for x in data_list if x]), + response_message) else: - logger.warning("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求结果:{}", + logger.warning("[api]请求人:{}||请求IP:{}||请求方法:{}||请求Api:{}||请求参数:{}||请求结果:{}", session.get('user_info').get('user_name') if session.get('user_info') else None, - request.remote_addr, method, url, response_message) + request.remote_addr, method, url, + ','.join([str(x) for x in data_list if x]), + response_message) return response.json() except Exception as e: diff --git a/dash-fastapi-frontend/utils/tree_tool.py b/dash-fastapi-frontend/utils/tree_tool.py index a8c5a87..92f7188 100644 --- a/dash-fastapi-frontend/utils/tree_tool.py +++ b/dash-fastapi-frontend/utils/tree_tool.py @@ -142,3 +142,30 @@ def get_dept_tree(pid: int, permission_list: list): dept_list.append(dept_list_data) return dept_list + + +def list_to_tree(permission_list: list) -> list: + """ + 工具方法:根据菜单信息生成树形嵌套数据 + :param permission_list: 菜单列表信息 + :return: 菜单树形嵌套数据 + """ + # 转成menu_id为Key的字典 + mapping: dict = dict(zip([i['menu_id'] for i in permission_list], permission_list)) + + # 树容器 + container: list = [] + + for d in permission_list: + # 如果找不到父级项,则是根节点 + parent: dict = mapping.get(d['parent_id']) + if parent is None: + container.append(d) + else: + children: list = parent.get('children') + if not children: + children = [] + children.append(d) + parent.update({'children': children}) + + return container diff --git a/dash-fastapi-frontend/views/layout/index.py b/dash-fastapi-frontend/views/layout/index.py deleted file mode 100644 index c2ab0e6..0000000 --- a/dash-fastapi-frontend/views/layout/index.py +++ /dev/null @@ -1,139 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - -from views.layout.components.head import render_head_content -from views.layout.components.content import render_main_content -from views.layout.components.aside import render_aside_content -# import callbacks.index_c -import callbacks.layout_c.fold_side_menu -import callbacks.layout_c.index_c - - -def render_content(user_name, nick_name, phone_number, menu_info): - - return fuc.FefferyTopProgress( - html.Div( - [ - # 全局重载 - fuc.FefferyReload(id='trigger-reload-output'), - - html.Div(id='idle-placeholder-container'), - - # 注入相关modal - html.Div( - [ - # 个人资料面板 - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdText( - user_name, - copyable=True - ), - label='账号' - ), - fac.AntdFormItem( - fac.AntdText( - nick_name, - copyable=True - ), - label='姓名' - ), - fac.AntdFormItem( - fac.AntdText( - phone_number, - copyable=True - ), - label='电话' - ) - ], - labelCol={ - 'span': 4 - } - ) - ], - id='index-personal-info-modal', - title='个人资料', - mask=False - ), - ] - ), - - # 退出登录对话框提示 - fac.AntdModal( - html.Div( - [ - fac.AntdIcon(icon='fc-info', style={'font-size': '28px'}), - fac.AntdText('确定注销并退出系统吗?', style={'margin-left': '5px'}), - ] - ), - id='logout-modal', - visible=False, - title='提示', - renderFooter=True, - centered=True - ), - - # 平台主页面 - fac.AntdRow( - [ - # 左侧固定菜单区域 - fac.AntdCol( - fac.AntdAffix( - html.Div( - render_aside_content(menu_info), - id='side-menu', - style={ - 'height': '100vh', - 'overflowY': 'auto', - 'transition': 'width 1s', - 'background': '#001529' - } - ), - ), - # flex='1', - id='left-side-menu-container', - style={ - 'flex': '1' - } - ), - - # 右侧区域 - fac.AntdCol( - [ - fac.AntdRow( - render_head_content(user_name), - style={ - 'height': '50px', - 'boxShadow': 'rgb(240 241 242) 0px 2px 14px', - 'background': 'white', - 'marginBottom': '10px', - 'position': 'sticky', - 'top': 0, - 'zIndex': 999 - } - ), - fac.AntdRow( - render_main_content(user_name, nick_name, phone_number), - wrap=False - ) - ], - # flex='5', - style={ - 'flex': '6', - 'width': '300px' - } - ), - ], - ) - ], - id='index-main-content-container', - ), - listenPropsMode='include', - includeProps=[ - 'tabs-container.items' - ] - ) diff --git a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py index 251e6c2..7734a12 100644 --- a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py +++ b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py @@ -1,3 +1,8 @@ -from . import ( - index -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是登录日志') diff --git a/dash-fastapi-frontend/views/monitor/logininfor/index.py b/dash-fastapi-frontend/views/monitor/logininfor/index.py deleted file mode 100644 index 7734a12..0000000 --- a/dash-fastapi-frontend/views/monitor/logininfor/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是登录日志') diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py index 251e6c2..5766ac5 100644 --- a/dash-fastapi-frontend/views/monitor/operlog/__init__.py +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -1,3 +1,8 @@ -from . import ( - index -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是操作日志') diff --git a/dash-fastapi-frontend/views/monitor/operlog/index.py b/dash-fastapi-frontend/views/monitor/operlog/index.py deleted file mode 100644 index 5766ac5..0000000 --- a/dash-fastapi-frontend/views/monitor/operlog/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是操作日志') diff --git a/dash-fastapi-frontend/views/system/config/__init__.py b/dash-fastapi-frontend/views/system/config/__init__.py index 251e6c2..64badb5 100644 --- a/dash-fastapi-frontend/views/system/config/__init__.py +++ b/dash-fastapi-frontend/views/system/config/__init__.py @@ -1,3 +1,8 @@ -from . import ( - index -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是参数设置') diff --git a/dash-fastapi-frontend/views/system/config/index.py b/dash-fastapi-frontend/views/system/config/index.py deleted file mode 100644 index 64badb5..0000000 --- a/dash-fastapi-frontend/views/system/config/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是参数设置') diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index 251e6c2..a88f973 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -1,3 +1,531 @@ -from . import ( - index -) +from dash import dcc +import feffery_antd_components as fac + +import callbacks.system_c.dept_c +from api.dept import get_dept_list_api +from utils.tree_tool import get_dept_tree + + +def render(): + table_data_new = [] + default_expanded_row_keys = [] + table_info = get_dept_list_api({}) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + for item in table_data: + default_expanded_row_keys.append(str(item['dept_id'])) + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dept_id']) + if item['parent_id'] == 0: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + table_data_new = get_dept_tree(0, table_data) + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-dept_name-input', + placeholder='请输入部门名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='部门名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dept-status-select', + placeholder='部门状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='部门状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dept-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dept-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dept-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-swap' + ), + '展开/折叠', + ], + id='dept-fold', + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='dept-list-table', + data=table_data_new, + columns=[ + { + 'dataIndex': 'dept_id', + 'title': '部门编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + 'hidden': True + }, + { + 'dataIndex': 'dept_name', + 'title': '部门名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'order_num', + 'title': '排序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + bordered=True, + pagination={ + 'hideOnSinglePage': True + }, + defaultExpandedRowKeys=default_expanded_row_keys, + style={ + 'width': '100%', + 'padding-right': '10px', + 'padding-bottom': '20px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增部门表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdTreeSelect( + id='dept-add-parent_id', + placeholder='请选择上级部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': 500 + } + ), + label='上级部门', + required=True, + id='dept-add-parent_id-form-item', + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-add-dept_name', + placeholder='请输入部门名称', + allowClear=True, + style={ + 'width': 200 + } + ), + label='部门名称', + required=True, + id='dept-add-dept_name-form-item', + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='dept-add-order_num', + min=0, + style={ + 'width': 200 + } + ), + label='显示顺序', + required=True, + id='dept-add-order_num-form-item', + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-add-leader', + placeholder='请输入负责人', + allowClear=True, + style={ + 'width': 200 + } + ), + label='负责人', + id='dept-add-leader-form-item', + labelCol={ + 'offset': 2 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='dept-add-phone', + placeholder='请输入联系电话', + allowClear=True, + style={ + 'width': 200 + } + ), + label='联系电话', + id='dept-add-phone-form-item', + labelCol={ + 'offset': 3 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-add-email', + placeholder='请输入邮箱', + allowClear=True, + style={ + 'width': 200 + } + ), + label='邮箱', + id='dept-add-email-form-item', + labelCol={ + 'offset': 3 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dept-add-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label='部门状态', + id='dept-add-status-form-item', + labelCol={ + 'offset': 4 + }, + ) + ], + size="middle" + ), + ] + ) + ], + id='dept-add-modal', + title='新增部门', + mask=False, + width=650, + renderFooter=True, + okClickClose=False + ), + + # 编辑部门表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdTreeSelect( + id='dept-edit-parent_id', + placeholder='请选择上级部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': 510 + } + ), + label='上级部门', + required=True, + id='dept-edit-parent_id-form-item', + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-edit-dept_name', + placeholder='请输入部门名称', + allowClear=True, + style={ + 'width': 200 + } + ), + label='部门名称', + required=True, + id='dept-edit-dept_name-form-item', + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='dept-edit-order_num', + min=0, + style={ + 'width': 200 + } + ), + label='显示顺序', + required=True, + id='dept-edit-order_num-form-item', + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-edit-leader', + placeholder='请输入负责人', + allowClear=True, + style={ + 'width': 200 + } + ), + label='负责人', + id='dept-edit-leader-form-item', + labelCol={ + 'offset': 2 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='dept-edit-phone', + placeholder='请输入联系电话', + allowClear=True, + style={ + 'width': 200 + } + ), + label='联系电话', + id='dept-edit-phone-form-item', + labelCol={ + 'offset': 3 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-edit-email', + placeholder='请输入邮箱', + allowClear=True, + style={ + 'width': 200 + } + ), + label='邮箱', + id='dept-edit-email-form-item', + labelCol={ + 'offset': 3 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dept-edit-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label='部门状态', + id='dept-edit-status-form-item', + labelCol={ + 'offset': 4 + }, + ) + ], + size="middle" + ), + ] + ) + ], + id='dept-edit-modal', + title='编辑部门', + mask=False, + width=650, + renderFooter=True, + okClickClose=False + ), + + # 删除部门二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='dept-delete-text'), + id='dept-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] diff --git a/dash-fastapi-frontend/views/system/dept/index.py b/dash-fastapi-frontend/views/system/dept/index.py deleted file mode 100644 index 883061e..0000000 --- a/dash-fastapi-frontend/views/system/dept/index.py +++ /dev/null @@ -1,517 +0,0 @@ -from dash import dcc -import feffery_antd_components as fac - -import callbacks.system_c.dept_c -from api.dept import get_dept_list_api -from utils.tree_tool import get_dept_tree - - -def render(): - table_data_new = [] - default_expanded_row_keys = [] - table_info = get_dept_list_api({}) - if table_info['code'] == 200: - table_data = table_info['data']['rows'] - for item in table_data: - default_expanded_row_keys.append(str(item['dept_id'])) - if item['status'] == '0': - item['status'] = dict(tag='正常', color='blue') - else: - item['status'] = dict(tag='停用', color='volcano') - item['key'] = str(item['dept_id']) - if item['parent_id'] == 0: - item['operation'] = [] - else: - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - }, - { - 'content': '新增', - 'type': 'link', - 'icon': 'antd-plus' - }, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - }, - ] - table_data_new = get_dept_tree(0, table_data) - - return [ - fac.AntdRow( - [ - fac.AntdCol( - [ - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-dept_name-input', - placeholder='请输入部门名称', - autoComplete='off', - style={ - 'width': 240 - } - ), - label='部门名称' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='dept-status-select', - placeholder='部门状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 240 - } - ), - label='部门状态' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='dept-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' - ) - ) - ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='dept-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) - ], - style={ - 'paddingBottom': '10px' - } - ), - ], - layout='inline', - ) - ) - ] - ), - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdSpace( - [ - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-plus' - ), - '新增', - ], - id='dept-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-swap' - ), - '展开/折叠', - ], - id='dept-fold', - style={ - 'color': '#909399', - 'background': '#f4f4f5', - 'border-color': '#d3d4d6' - } - ), - ], - style={ - 'paddingBottom': '10px' - } - ), - ) - ] - ), - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdSpin( - fac.AntdTable( - id='dept-list-table', - data=table_data_new, - columns=[ - { - 'dataIndex': 'dept_id', - 'title': '部门编号', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - 'hidden': True - }, - { - 'dataIndex': 'dept_name', - 'title': '部门名称', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'order_num', - 'title': '排序', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'status', - 'title': '状态', - 'renderOptions': { - 'renderType': 'tags' - }, - }, - { - 'dataIndex': 'create_time', - 'title': '创建时间', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'title': '操作', - 'dataIndex': 'operation', - 'renderOptions': { - 'renderType': 'button' - }, - } - ], - bordered=True, - pagination={ - 'hideOnSinglePage': True - }, - defaultExpandedRowKeys=default_expanded_row_keys, - style={ - 'width': '100%', - 'padding-right': '10px' - } - ), - text='数据加载中' - ), - ) - ] - ), - ], - span=24 - ) - ], - gutter=5 - ), - - # 新增部门表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdTreeSelect( - id='dept-add-parent_id', - placeholder='请选择上级部门', - treeData=[], - style={ - 'width': 500 - } - ), - label='上级部门', - required=True, - id='dept-add-parent_id-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-dept_name', - placeholder='请输入部门名称', - style={ - 'width': 200 - } - ), - label='部门名称', - required=True, - id='dept-add-dept_name-form-item', - ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='dept-add-order_num', - style={ - 'width': 200 - } - ), - label='显示顺序', - required=True, - id='dept-add-order_num-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-leader', - placeholder='请输入负责人', - style={ - 'width': 200 - } - ), - label='负责人', - id='dept-add-leader-form-item', - labelCol={ - 'offset': 2 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-phone', - placeholder='请输入联系电话', - style={ - 'width': 200 - } - ), - label='联系电话', - id='dept-add-phone-form-item', - labelCol={ - 'offset': 3 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-email', - placeholder='请输入邮箱', - style={ - 'width': 200 - } - ), - label='邮箱', - id='dept-add-email-form-item', - labelCol={ - 'offset': 3 - }, - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='dept-add-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 200 - } - ), - label='部门状态', - id='dept-add-status-form-item', - labelCol={ - 'offset': 4 - }, - ) - ], - size="middle" - ), - ] - ) - ], - id='dept-add-modal', - title='新增部门', - mask=False, - width=650, - renderFooter=True, - okClickClose=False - ), - - # 编辑部门表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdTreeSelect( - id='dept-edit-parent_id', - placeholder='请选择上级部门', - treeData=[], - style={ - 'width': 510 - } - ), - label='上级部门', - required=True, - id='dept-edit-parent_id-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-dept_name', - placeholder='请输入部门名称', - style={ - 'width': 200 - } - ), - label='部门名称', - required=True, - id='dept-edit-dept_name-form-item', - ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='dept-edit-order_num', - style={ - 'width': 200 - } - ), - label='显示顺序', - required=True, - id='dept-edit-order_num-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-leader', - placeholder='请输入负责人', - style={ - 'width': 200 - } - ), - label='负责人', - id='dept-edit-leader-form-item', - labelCol={ - 'offset': 2 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-phone', - placeholder='请输入联系电话', - style={ - 'width': 200 - } - ), - label='联系电话', - id='dept-edit-phone-form-item', - labelCol={ - 'offset': 3 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-email', - placeholder='请输入邮箱', - style={ - 'width': 200 - } - ), - label='邮箱', - id='dept-edit-email-form-item', - labelCol={ - 'offset': 3 - }, - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='dept-edit-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 200 - } - ), - label='部门状态', - id='dept-edit-status-form-item', - labelCol={ - 'offset': 3 - }, - ) - ], - size="middle" - ), - ] - ) - ], - id='dept-edit-modal', - title='编辑部门', - mask=False, - width=650, - renderFooter=True, - okClickClose=False - ), - - # 删除部门二次确认modal - fac.AntdModal( - fac.AntdText('是否确认删除?', id='dept-delete-text'), - id='dept-delete-confirm-modal', - visible=False, - title='提示', - renderFooter=True, - centered=True - ), - ] diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index 251e6c2..46a618d 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -1,3 +1,8 @@ -from . import ( - index -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是字典管理') diff --git a/dash-fastapi-frontend/views/system/dict/index.py b/dash-fastapi-frontend/views/system/dict/index.py deleted file mode 100644 index 46a618d..0000000 --- a/dash-fastapi-frontend/views/system/dict/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是字典管理') diff --git a/dash-fastapi-frontend/views/system/menu/__init__.py b/dash-fastapi-frontend/views/system/menu/__init__.py index 251e6c2..ce00838 100644 --- a/dash-fastapi-frontend/views/system/menu/__init__.py +++ b/dash-fastapi-frontend/views/system/menu/__init__.py @@ -1,3 +1,433 @@ -from . import ( - index -) +from dash import html +import feffery_antd_components as fac + +from api.menu import get_menu_list_api +from utils.tree_tool import list_to_tree +import callbacks.system_c.menu_c.menu_c + + +def render(): + table_data_new = [] + table_info = get_menu_list_api({}) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['menu_id']) + item['icon'] = [ + { + 'type': 'link', + 'icon': item['icon'], + 'disabled': True, + 'style': { + 'color': 'rgba(0, 0, 0, 0.8)' + } + }, + ] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + table_data_new = list_to_tree(table_data) + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='menu-menu_name-input', + placeholder='请输入菜单名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='菜单名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='menu-status-select', + placeholder='菜单状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='菜单状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='menu-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='menu-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='menu-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-swap' + ), + '展开/折叠', + ], + id='menu-fold', + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='menu-list-table', + data=table_data_new, + columns=[ + { + 'dataIndex': 'menu_id', + 'title': '菜单编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + 'hidden': True + }, + { + 'dataIndex': 'menu_name', + 'title': '菜单名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'icon', + 'title': '图标', + 'width': 80, + 'renderOptions': { + 'renderType': 'button' + }, + }, + { + 'dataIndex': 'order_num', + 'title': '排序', + 'width': 80, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'perms', + 'title': '权限标识', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'component', + 'title': '组件路径', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'width': 90, + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'width': 150, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + bordered=True, + pagination={ + 'hideOnSinglePage': True + }, + style={ + 'width': '100%', + 'padding-right': '10px', + 'padding-bottom': '20px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增和编辑菜单表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdTreeSelect( + id='menu-parent_id', + placeholder='请选择上级菜单', + treeData=[], + defaultValue='0', + treeNodeFilterProp='title', + style={ + 'width': 495 + } + ), + label='上级菜单', + required=True, + id='menu-parent_id-form-item', + labelCol={ + 'span': 4, + }, + wrapperCol={ + 'span': 20 + } + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdRadioGroup( + id='menu-menu_type', + options=[ + { + 'label': '目录', + 'value': 'M' + }, + { + 'label': '菜单', + 'value': 'C' + }, + { + 'label': '按钮', + 'value': 'F' + }, + ], + defaultValue='M', + style={ + 'width': 495 + } + ), + label='菜单类型', + required=True, + id='menu-menu_type-form-item', + labelCol={ + 'span': 4, + }, + wrapperCol={ + 'span': 20 + } + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdPopover( + fac.AntdInput( + id='menu-icon', + placeholder='点击选择图标', + readOnly=True, + prefix=fac.AntdIcon( + icon='antd-search' + ), + style={ + 'width': 495 + } + ), + title=fac.AntdInput( + id='menu-icon-search', + placeholder='请输入图标名称', + suffix=fac.AntdIcon( + icon='antd-search' + ), + style={ + 'width': 450 + } + ), + trigger='click', + placement='bottom', + ), + label='菜单图标', + id='menu-icon-form-item', + labelCol={ + 'span': 4, + }, + wrapperCol={ + 'span': 20 + } + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='menu-menu_name', + placeholder='请输入菜单名称', + allowClear=True, + style={ + 'width': 200 + } + ), + label='菜单名称', + required=True, + id='menu-menu_name-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='menu-order_num', + min=0, + style={ + 'width': 200 + } + ), + label='显示排序', + required=True, + id='menu-order_num-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ), + html.Div(id='content-by-menu-type'), + ] + ) + ], + id='menu-modal', + mask=False, + width=680, + renderFooter=True, + okClickClose=False + ), + + # 删除菜单二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='menu-delete-text'), + id='menu-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] diff --git a/dash-fastapi-frontend/views/system/menu/components/__init__.py b/dash-fastapi-frontend/views/system/menu/components/__init__.py new file mode 100644 index 0000000..6593c9d --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/components/__init__.py @@ -0,0 +1,5 @@ +from . import ( + content_type, + menu_type, + button_type +) diff --git a/dash-fastapi-frontend/views/system/menu/components/button_type.py b/dash-fastapi-frontend/views/system/menu/components/button_type.py new file mode 100644 index 0000000..824fd71 --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/components/button_type.py @@ -0,0 +1,40 @@ +from dash import html +import feffery_antd_components as fac + + +def render(): + return [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='button-menu-perms', + placeholder='请输入权限字符', + allowClear=True, + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='控制器中定义的权限字符,如:system:user:list' + ), + fac.AntdText('权限字符') + ] + ), + id='button-menu-perms-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ), + ] diff --git a/dash-fastapi-frontend/views/system/menu/components/content_type.py b/dash-fastapi-frontend/views/system/menu/components/content_type.py new file mode 100644 index 0000000..5d4e120 --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/components/content_type.py @@ -0,0 +1,159 @@ +from dash import html +import feffery_antd_components as fac + +import callbacks.system_c.menu_c.components_c.content_type_c + + +def render(): + return [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdRadioGroup( + id='content-menu-is_frame', + options=[ + { + 'label': '是', + 'value': '0' + }, + { + 'label': '否', + 'value': '1' + }, + ], + defaultValue='1', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择是外链则路由地址需要以`http(s)://`开头' + ), + fac.AntdText('是否外链') + ] + ), + id='content-menu-is_frame-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdInput( + id='content-menu-path', + placeholder='请输入路由地址', + allowClear=True, + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头' + ), + fac.AntdText('路由地址') + ] + ), + required=True, + id='content-menu-path-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdRadioGroup( + id='content-menu-visible', + options=[ + { + 'label': '显示', + 'value': '0' + }, + { + 'label': '隐藏', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择隐藏则路由将不会出现在侧边栏,但仍然可以访问' + ), + fac.AntdText('显示状态') + ] + ), + id='content-menu-visible-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='content-menu-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择停用则路由将不会出现在侧边栏,也不能被访问' + ), + fac.AntdText('菜单状态') + ] + ), + id='content-menu-status-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ) + ] diff --git a/dash-fastapi-frontend/views/system/menu/components/menu_type.py b/dash-fastapi-frontend/views/system/menu/components/menu_type.py new file mode 100644 index 0000000..ebb27bb --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/components/menu_type.py @@ -0,0 +1,288 @@ +from dash import html +import feffery_antd_components as fac + + +def render(): + return [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdRadioGroup( + id='menu-menu-is_frame', + options=[ + { + 'label': '是', + 'value': '0' + }, + { + 'label': '否', + 'value': '1' + }, + ], + defaultValue='1', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择是外链则路由地址需要以`http(s)://`开头' + ), + fac.AntdText('是否外链') + ] + ), + id='menu-menu-is_frame-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdInput( + id='menu-menu-path', + placeholder='请输入路由地址', + allowClear=True, + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头' + ), + fac.AntdText('路由地址') + ] + ), + required=True, + id='menu-menu-path-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='menu-menu-component', + placeholder='请输入组件路径', + allowClear=True, + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='访问的组件路径,如:`system.user.index`,默认在`views`目录下' + ), + fac.AntdText('组件路径') + ] + ), + id='menu-menu-component-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdInput( + id='menu-menu-perms', + placeholder='请输入权限字符', + allowClear=True, + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='控制器中定义的权限字符,如:system:user:list' + ), + fac.AntdText('权限字符') + ] + ), + id='menu-menu-perms-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='menu-menu-query', + placeholder='请输入路由参数', + allowClear=True, + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' + ), + fac.AntdText('路由参数') + ] + ), + id='menu-menu-query-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='menu-menu-is_cache', + options=[ + { + 'label': '缓存', + 'value': '0' + }, + { + 'label': '不缓存', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致' + ), + fac.AntdText('是否缓存') + ] + ), + id='menu-menu-is_cache-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdRadioGroup( + id='menu-menu-visible', + options=[ + { + 'label': '显示', + 'value': '0' + }, + { + 'label': '隐藏', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择隐藏则路由将不会出现在侧边栏,但仍然可以访问' + ), + fac.AntdText('显示状态') + ] + ), + id='menu-menu-visible-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='menu-menu-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='选择停用则路由将不会出现在侧边栏,也不能被访问' + ), + fac.AntdText('菜单状态') + ] + ), + id='menu-menu-status-form-item', + labelCol={ + 'span': 8, + }, + wrapperCol={ + 'span': 16 + } + ), + ], + size="middle" + ) + ] diff --git a/dash-fastapi-frontend/views/system/menu/index.py b/dash-fastapi-frontend/views/system/menu/index.py deleted file mode 100644 index 9729ba9..0000000 --- a/dash-fastapi-frontend/views/system/menu/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是菜单管理') diff --git a/dash-fastapi-frontend/views/system/notice/__init__.py b/dash-fastapi-frontend/views/system/notice/__init__.py index 251e6c2..4bcc439 100644 --- a/dash-fastapi-frontend/views/system/notice/__init__.py +++ b/dash-fastapi-frontend/views/system/notice/__init__.py @@ -1,3 +1,8 @@ -from . import ( - index -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是通知公告') diff --git a/dash-fastapi-frontend/views/system/notice/index.py b/dash-fastapi-frontend/views/system/notice/index.py deleted file mode 100644 index 4bcc439..0000000 --- a/dash-fastapi-frontend/views/system/notice/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是通知公告') diff --git a/dash-fastapi-frontend/views/system/post/__init__.py b/dash-fastapi-frontend/views/system/post/__init__.py index 251e6c2..7896d72 100644 --- a/dash-fastapi-frontend/views/system/post/__init__.py +++ b/dash-fastapi-frontend/views/system/post/__init__.py @@ -1,3 +1,483 @@ -from . import ( - index -) +from dash import dcc +import feffery_antd_components as fac + +import callbacks.system_c.post_c +from api.post import get_post_list_api + + +def render(): + + post_params = dict(page_num=1, page_size=10) + table_info = get_post_list_api(post_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['post_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-post_code-input', + placeholder='请输入岗位编码', + autoComplete='off', + allowClear=True, + style={ + 'width': 210 + } + ), + label='岗位编码' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-post_name-input', + placeholder='请输入岗位名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 210 + } + ), + label='岗位名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='post-status-select', + placeholder='岗位状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 200 + } + ), + label='岗位状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='post-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='post-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='post-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='post-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='post-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='post-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='post-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'post_id', + 'title': '岗位编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'post_code', + 'title': '岗位编码', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'post_name', + 'title': '岗位名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'post_sort', + 'title': '岗位排序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增岗位表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-add-post_name', + placeholder='请输入岗位名称', + allowClear=True, + style={ + 'width': 350 + } + ), + label='岗位名称', + required=True, + id='post-add-post_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-add-post_code', + placeholder='请输入岗位编码', + allowClear=True, + style={ + 'width': 350 + } + ), + label='岗位编码', + required=True, + id='post-add-post_code-form-item', + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='post-add-post_sort', + defaultValue=0, + min=0, + style={ + 'width': 350 + } + ), + label='岗位顺序', + required=True, + id='post-add-post_sort-form-item', + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='post-add-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='岗位状态', + id='post-add-status-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-add-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='post-add-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ], + id='post-add-modal', + title='新增岗位', + mask=False, + width=480, + renderFooter=True, + okClickClose=False + ), + + # 编辑岗位表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-edit-post_name', + placeholder='请输入岗位名称', + allowClear=True, + style={ + 'width': 350 + } + ), + label='岗位名称', + required=True, + id='post-edit-post_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-edit-post_code', + placeholder='请输入岗位编码', + allowClear=True, + style={ + 'width': 350 + } + ), + label='岗位编码', + required=True, + id='post-edit-post_code-form-item', + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='post-edit-post_sort', + defaultValue=0, + min=0, + style={ + 'width': 350 + } + ), + label='岗位顺序', + required=True, + id='post-edit-post_sort-form-item', + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='post-edit-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='岗位状态', + id='post-edit-status-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-edit-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='post-edit-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ], + id='post-edit-modal', + title='编辑岗位', + mask=False, + width=480, + renderFooter=True, + okClickClose=False + ), + + # 删除岗位二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='post-delete-text'), + id='post-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] diff --git a/dash-fastapi-frontend/views/system/post/index.py b/dash-fastapi-frontend/views/system/post/index.py deleted file mode 100644 index a39f18b..0000000 --- a/dash-fastapi-frontend/views/system/post/index.py +++ /dev/null @@ -1,473 +0,0 @@ -from dash import dcc -import feffery_antd_components as fac - -import callbacks.system_c.post_c -from api.post import get_post_list_api - - -def render(): - - post_params = dict(page_num=1, page_size=10) - table_info = get_post_list_api(post_params) - table_data = [] - page_num = 1 - page_size = 10 - total = 0 - if table_info['code'] == 200: - table_data = table_info['data']['rows'] - page_num = table_info['data']['page_num'] - page_size = table_info['data']['page_size'] - total = table_info['data']['total'] - for item in table_data: - if item['status'] == '0': - item['status'] = dict(tag='正常', color='blue') - else: - item['status'] = dict(tag='停用', color='volcano') - item['key'] = str(item['post_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - }, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - }, - ] - - return [ - fac.AntdRow( - [ - fac.AntdCol( - [ - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='post-post_code-input', - placeholder='请输入岗位编码', - autoComplete='off', - style={ - 'width': 210 - } - ), - label='岗位编码' - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-post_name-input', - placeholder='请输入岗位名称', - autoComplete='off', - style={ - 'width': 210 - } - ), - label='岗位名称' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='post-status-select', - placeholder='岗位状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 200 - } - ), - label='岗位状态' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='post-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' - ) - ) - ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='post-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) - ], - style={ - 'paddingBottom': '10px' - } - ), - ], - layout='inline', - ) - ) - ] - ), - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdSpace( - [ - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-plus' - ), - '新增', - ], - id='post-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-edit' - ), - '修改', - ], - id='post-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-minus' - ), - '删除', - ], - id='post-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-arrow-down' - ), - '导出', - ], - id='post-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } - ), - ], - style={ - 'paddingBottom': '10px' - } - ), - ) - ] - ), - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdSpin( - fac.AntdTable( - id='post-list-table', - data=table_data, - columns=[ - { - 'dataIndex': 'post_id', - 'title': '岗位编号', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'post_code', - 'title': '岗位编码', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'post_name', - 'title': '岗位名称', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'post_sort', - 'title': '岗位排序', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'status', - 'title': '状态', - 'renderOptions': { - 'renderType': 'tags' - }, - }, - { - 'dataIndex': 'create_time', - 'title': '创建时间', - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'title': '操作', - 'dataIndex': 'operation', - 'renderOptions': { - 'renderType': 'button' - }, - } - ], - rowSelectionType='checkbox', - rowSelectionWidth=50, - bordered=True, - pagination={ - 'pageSize': page_size, - 'current': page_num, - 'showSizeChanger': True, - 'pageSizeOptions': [10, 30, 50, 100], - 'showQuickJumper': True, - 'total': total - }, - mode='server-side', - style={ - 'width': '100%', - 'padding-right': '10px' - } - ), - text='数据加载中' - ), - ) - ] - ), - ], - span=24 - ) - ], - gutter=5 - ), - - # 新增岗位表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='post-add-post_name', - placeholder='请输入岗位名称', - style={ - 'width': 350 - } - ), - label='岗位名称', - required=True, - id='post-add-post_name-form-item' - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-add-post_code', - placeholder='请输入岗位编码', - style={ - 'width': 350 - } - ), - label='岗位编码', - required=True, - id='post-add-post_code-form-item', - ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='post-add-post_sort', - defaultValue=0, - style={ - 'width': 350 - } - ), - label='岗位顺序', - required=True, - id='post-add-post_sort-form-item', - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='post-add-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 350 - } - ), - label='岗位状态', - id='post-add-status-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-add-remark', - placeholder='请输入内容', - mode='text-area', - style={ - 'width': 350 - } - ), - label='备注', - id='post-add-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] - ) - ], - id='post-add-modal', - title='新增岗位', - mask=False, - width=480, - renderFooter=True, - okClickClose=False - ), - - # 编辑岗位表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='post-edit-post_name', - placeholder='请输入岗位名称', - style={ - 'width': 350 - } - ), - label='岗位名称', - required=True, - id='post-edit-post_name-form-item' - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-edit-post_code', - placeholder='请输入岗位编码', - style={ - 'width': 350 - } - ), - label='岗位编码', - required=True, - id='post-edit-post_code-form-item', - ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='post-edit-post_sort', - defaultValue=0, - style={ - 'width': 350 - } - ), - label='岗位顺序', - required=True, - id='post-edit-post_sort-form-item', - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='post-edit-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 350 - } - ), - label='岗位状态', - id='post-edit-status-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-edit-remark', - placeholder='请输入内容', - mode='text-area', - style={ - 'width': 350 - } - ), - label='备注', - id='post-edit-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] - ) - ], - id='post-edit-modal', - title='编辑岗位', - mask=False, - width=480, - renderFooter=True, - okClickClose=False - ), - - # 删除岗位二次确认modal - fac.AntdModal( - fac.AntdText('是否确认删除?', id='post-delete-text'), - id='post-delete-confirm-modal', - visible=False, - title='提示', - renderFooter=True, - centered=True - ), - ] diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index 251e6c2..7ccec88 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -1,3 +1,8 @@ -from . import ( - index -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + + +def render(): + + return html.Div('我是角色管理') diff --git a/dash-fastapi-frontend/views/system/role/index.py b/dash-fastapi-frontend/views/system/role/index.py deleted file mode 100644 index 7ccec88..0000000 --- a/dash-fastapi-frontend/views/system/role/index.py +++ /dev/null @@ -1,8 +0,0 @@ -from dash import html -import feffery_utils_components as fuc -import feffery_antd_components as fac - - -def render(): - - return html.Div('我是角色管理') diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index 251e6c2..3718045 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -1,3 +1,823 @@ -from . import ( - index -) +from dash import dcc +import feffery_antd_components as fac + +import callbacks.system_c.user_c +from api.user import get_user_list_api +from api.dept import get_dept_tree_api + + +def render(): + dept_params = dict(dept_name='') + user_params = dict(page_num=1, page_size=10) + tree_info = get_dept_tree_api(dept_params) + table_info = get_user_list_api(user_params) + tree_data = [] + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if tree_info['code'] == 200: + tree_data = tree_info['data'] + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(checked=True) + else: + item['status'] = dict(checked=False) + item['key'] = str(item['user_id']) + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + }, + { + 'title': '删除', + 'icon': 'antd-delete' + }, + { + 'title': '重置密码', + 'icon': 'antd-key' + } + ] + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdInput( + id='dept-input-search', + placeholder='请输入部门名称', + autoComplete='off', + allowClear=True, + prefix=fac.AntdIcon( + icon='antd-search' + ), + style={ + 'width': '85%' + } + ), + fac.AntdTree( + id='dept-tree', + treeData=tree_data, + defaultExpandAll=True, + showLine=False, + style={ + 'margin-top': '10px' + } + ) + ], + span=4 + ), + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-user_name-input', + placeholder='请输入用户名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='用户名称' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-phone_number-input', + placeholder='请输入手机号码', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='手机号码' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-status-select', + placeholder='用户状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='用户状态' + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='user-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='user-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='user-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) + ) + ], + style={ + 'paddingBottom': '10px' + } + ), + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='user-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='user-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='user-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-up' + ), + '导入', + ], + id='user-import', + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='user-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='user-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'user_id', + 'title': '用户编号', + 'width': 100, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'user_name', + 'title': '用户名称', + 'width': 120, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'nick_name', + 'title': '用户昵称', + 'width': 120, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dept_name', + 'title': '部门', + 'width': 130, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'phonenumber', + 'title': '手机号码', + 'width': 130, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'width': 110, + 'renderOptions': { + 'renderType': 'switch' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'width': 160, + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'dropdown', + 'dropdownProps': { + 'title': '更多' + } + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=20 + ) + ], + gutter=5 + ), + + # 新增用户表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-nick_name', + placeholder='请输入用户昵称', + allowClear=True, + style={ + 'width': 200 + } + ), + label='用户昵称', + required=True, + id='user-add-nick_name-form-item' + ), + fac.AntdFormItem( + fac.AntdTreeSelect( + id='user-add-dept_id', + placeholder='请选择归属部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': 200 + } + ), + label='归属部门', + id='user-add-dept_id-form-item', + labelCol={ + 'offset': 1 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-phone_number', + placeholder='请输入手机号码', + allowClear=True, + style={ + 'width': 200 + } + ), + label='手机号码', + id='user-add-phone_number-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-add-email', + placeholder='请输入邮箱', + allowClear=True, + style={ + 'width': 200 + } + ), + label='邮箱', + id='user-add-email-form-item', + labelCol={ + 'offset': 5 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-user_name', + placeholder='请输入用户名称', + allowClear=True, + style={ + 'width': 200 + } + ), + label='用户名称', + required=True, + id='user-add-user_name-form-item' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-add-password', + placeholder='请输入密码', + mode='password', + passwordUseMd5=True, + style={ + 'width': 200 + } + ), + label='用户密码', + required=True, + id='user-add-password-form-item' + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-sex', + placeholder='请选择性别', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + }, + { + 'label': '未知', + 'value': '2' + }, + ], + style={ + 'width': 200 + } + ), + label='用户性别', + id='user-add-sex-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='user-add-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 200 + } + ), + label='用户状态', + id='user-add-status-form-item', + labelCol={ + 'offset': 2 + }, + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-post', + placeholder='请选择岗位', + options=[], + mode='multiple', + optionFilterProp='label', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-add-post-form-item', + labelCol={ + 'offset': 4 + }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-add-role', + placeholder='请选择角色', + options=[], + mode='multiple', + optionFilterProp='label', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-add-role-form-item', + labelCol={ + 'offset': 8 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-add-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 490 + } + ), + label='备注', + id='user-add-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ] + ) + ], + id='user-add-modal', + title='新增用户', + mask=False, + width=650, + renderFooter=True, + okClickClose=False + ), + + # 编辑用户表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-nick_name', + placeholder='请输入用户昵称', + allowClear=True, + style={ + 'width': 200 + } + ), + label='用户昵称', + required=True, + id='user-edit-nick_name-form-item' + ), + fac.AntdFormItem( + fac.AntdTreeSelect( + id='user-edit-dept_id', + placeholder='请选择归属部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': 200 + } + ), + label='归属部门', + id='user-edit-dept_id-form-item' + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-phone_number', + placeholder='请输入手机号码', + allowClear=True, + style={ + 'width': 200 + } + ), + label='手机号码', + id='user-edit-phone_number-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-email', + placeholder='请输入邮箱', + allowClear=True, + style={ + 'width': 200 + } + ), + label='邮箱', + id='user-edit-email-form-item', + labelCol={ + 'offset': 4 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-sex', + placeholder='请选择性别', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + }, + { + 'label': '未知', + 'value': '2' + }, + ], + style={ + 'width': 200 + } + ), + label='用户性别', + id='user-edit-sex-form-item', + labelCol={ + 'offset': 1 + }, + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='user-edit-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + style={ + 'width': 200 + } + ), + label='用户状态', + id='user-edit-status-form-item', + labelCol={ + 'offset': 1 + }, + ) + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-post', + placeholder='请选择岗位', + options=[], + mode='multiple', + optionFilterProp='label', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-edit-post-form-item', + labelCol={ + 'offset': 4 + }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-edit-role', + placeholder='请选择角色', + options=[], + mode='multiple', + optionFilterProp='label', + style={ + 'width': 200 + } + ), + label='岗位', + id='user-edit-role-form-item', + labelCol={ + 'offset': 7 + }, + ), + ], + size="middle" + ), + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-edit-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 485 + } + ), + label='备注', + id='user-edit-remark-form-item', + labelCol={ + 'offset': 2 + }, + ), + ] + ) + ] + ) + ], + id='user-edit-modal', + title='编辑用户', + mask=False, + width=650, + renderFooter=True, + okClickClose=False + ), + + # 删除用户二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='user-delete-text'), + id='user-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + + # 重置密码modal + fac.AntdModal( + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-password-input', + mode='password' + ), + ), + ], + layout='vertical' + ), + id='user-reset-password-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] diff --git a/dash-fastapi-frontend/views/system/user/index.py b/dash-fastapi-frontend/views/system/user/index.py deleted file mode 100644 index 62c3bf5..0000000 --- a/dash-fastapi-frontend/views/system/user/index.py +++ /dev/null @@ -1,804 +0,0 @@ -from dash import dcc -import feffery_antd_components as fac - -import callbacks.system_c.user_c -from api.user import get_user_list_api -from api.dept import get_dept_tree_api - - -def render(): - dept_params = dict(dept_name='') - user_params = dict(page_num=1, page_size=10) - tree_info = get_dept_tree_api(dept_params) - table_info = get_user_list_api(user_params) - tree_data = [] - table_data = [] - page_num = 1 - page_size = 10 - total = 0 - if tree_info['code'] == 200: - tree_data = tree_info['data'] - if table_info['code'] == 200: - table_data = table_info['data']['rows'] - page_num = table_info['data']['page_num'] - page_size = table_info['data']['page_size'] - total = table_info['data']['total'] - for item in table_data: - if item['status'] == '0': - item['status'] = dict(checked=True) - else: - item['status'] = dict(checked=False) - item['key'] = str(item['user_id']) - item['operation'] = [ - { - 'title': '修改', - 'icon': 'antd-edit' - }, - { - 'title': '删除', - 'icon': 'antd-delete' - }, - { - 'title': '重置密码', - 'icon': 'antd-key' - } - ] - - return [ - fac.AntdRow( - [ - fac.AntdCol( - [ - fac.AntdInput( - id='dept-input-search', - placeholder='请输入部门名称', - prefix=fac.AntdIcon( - icon='antd-search' - ), - style={ - 'width': '85%' - } - ), - fac.AntdTree( - id='dept-tree', - treeData=tree_data, - defaultExpandAll=True, - showLine=False, - style={ - 'margin-top': '10px' - } - ) - ], - span=4 - ), - fac.AntdCol( - [ - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-user_name-input', - placeholder='请输入用户名称', - autoComplete='off', - style={ - 'width': 240 - } - ), - label='用户名称' - ), - fac.AntdFormItem( - fac.AntdInput( - id='user-phone_number-input', - placeholder='请输入手机号码', - autoComplete='off', - style={ - 'width': 240 - } - ), - label='手机号码' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-status-select', - placeholder='用户状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 240 - } - ), - label='用户状态' - ), - ], - style={ - 'paddingBottom': '10px' - } - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdDateRangePicker( - id='user-create_time-range', - style={ - 'width': 240 - } - ), - label='创建时间' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='user-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' - ) - ) - ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='user-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) - ], - style={ - 'paddingBottom': '10px' - } - ), - ], - layout='inline', - ) - ) - ] - ), - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdSpace( - [ - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-plus' - ), - '新增', - ], - id='user-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-edit' - ), - '修改', - ], - id='user-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-minus' - ), - '删除', - ], - id='user-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-arrow-up' - ), - '导入', - ], - id='user-import', - style={ - 'color': '#909399', - 'background': '#f4f4f5', - 'border-color': '#d3d4d6' - } - ), - fac.AntdButton( - [ - fac.AntdIcon( - icon='antd-arrow-down' - ), - '导出', - ], - id='user-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } - ), - ], - style={ - 'paddingBottom': '10px' - } - ), - ) - ] - ), - fac.AntdRow( - [ - fac.AntdCol( - fac.AntdSpin( - fac.AntdTable( - id='user-list-table', - data=table_data, - columns=[ - { - 'dataIndex': 'user_id', - 'title': '用户编号', - 'width': 100, - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'user_name', - 'title': '用户名称', - 'width': 120, - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'nick_name', - 'title': '用户昵称', - 'width': 120, - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'dept_name', - 'title': '部门', - 'width': 130, - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'phonenumber', - 'title': '手机号码', - 'width': 130, - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'dataIndex': 'status', - 'title': '状态', - 'width': 110, - 'renderOptions': { - 'renderType': 'switch' - }, - }, - { - 'dataIndex': 'create_time', - 'title': '创建时间', - 'width': 160, - 'renderOptions': { - 'renderType': 'ellipsis' - }, - }, - { - 'title': '操作', - 'dataIndex': 'operation', - 'renderOptions': { - 'renderType': 'dropdown', - 'dropdownProps': { - 'title': '更多' - } - }, - } - ], - rowSelectionType='checkbox', - rowSelectionWidth=50, - bordered=True, - pagination={ - 'pageSize': page_size, - 'current': page_num, - 'showSizeChanger': True, - 'pageSizeOptions': [10, 30, 50, 100], - 'showQuickJumper': True, - 'total': total - }, - mode='server-side', - style={ - 'width': '100%', - 'padding-right': '10px' - } - ), - text='数据加载中' - ), - ) - ] - ), - ], - span=20 - ) - ], - gutter=5 - ), - - # 新增用户表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-add-nick_name', - placeholder='请输入用户昵称', - style={ - 'width': 200 - } - ), - label='用户昵称', - required=True, - id='user-add-nick_name-form-item' - ), - fac.AntdFormItem( - fac.AntdTreeSelect( - id='user-add-dept_id', - placeholder='请选择归属部门', - treeData=[], - style={ - 'width': 200 - } - ), - label='归属部门', - id='user-add-dept_id-form-item', - labelCol={ - 'offset': 1 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-add-phone_number', - placeholder='请输入手机号码', - style={ - 'width': 200 - } - ), - label='手机号码', - id='user-add-phone_number-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='user-add-email', - placeholder='请输入邮箱', - style={ - 'width': 200 - } - ), - label='邮箱', - id='user-add-email-form-item', - labelCol={ - 'offset': 5 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-add-user_name', - placeholder='请输入用户名称', - style={ - 'width': 200 - } - ), - label='用户名称', - required=True, - id='user-add-user_name-form-item' - ), - fac.AntdFormItem( - fac.AntdInput( - id='user-add-password', - placeholder='请输入密码', - mode='password', - passwordUseMd5=True, - style={ - 'width': 200 - } - ), - label='用户密码', - required=True, - id='user-add-password-form-item' - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdSelect( - id='user-add-sex', - placeholder='请选择性别', - options=[ - { - 'label': '男', - 'value': '0' - }, - { - 'label': '女', - 'value': '1' - }, - { - 'label': '未知', - 'value': '2' - }, - ], - style={ - 'width': 200 - } - ), - label='用户性别', - id='user-add-sex-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='user-add-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 200 - } - ), - label='用户状态', - id='user-add-status-form-item', - labelCol={ - 'offset': 2 - }, - ) - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdSelect( - id='user-add-post', - placeholder='请选择岗位', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-add-post-form-item', - labelCol={ - 'offset': 4 - }, - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-add-role', - placeholder='请选择角色', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-add-role-form-item', - labelCol={ - 'offset': 8 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-add-remark', - placeholder='请输入内容', - mode='text-area', - style={ - 'width': 490 - } - ), - label='备注', - id='user-add-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] - ) - ] - ) - ], - id='user-add-modal', - title='新增用户', - mask=False, - width=650, - renderFooter=True, - okClickClose=False - ), - - # 编辑用户表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-edit-nick_name', - placeholder='请输入用户昵称', - style={ - 'width': 200 - } - ), - label='用户昵称', - required=True, - id='user-edit-nick_name-form-item' - ), - fac.AntdFormItem( - fac.AntdTreeSelect( - id='user-edit-dept_id', - placeholder='请选择归属部门', - treeData=[], - style={ - 'width': 200 - } - ), - label='归属部门', - id='user-edit-dept_id-form-item' - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-edit-phone_number', - placeholder='请输入手机号码', - style={ - 'width': 200 - } - ), - label='手机号码', - id='user-edit-phone_number-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='user-edit-email', - placeholder='请输入邮箱', - style={ - 'width': 200 - } - ), - label='邮箱', - id='user-edit-email-form-item', - labelCol={ - 'offset': 4 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdSelect( - id='user-edit-sex', - placeholder='请选择性别', - options=[ - { - 'label': '男', - 'value': '0' - }, - { - 'label': '女', - 'value': '1' - }, - { - 'label': '未知', - 'value': '2' - }, - ], - style={ - 'width': 200 - } - ), - label='用户性别', - id='user-edit-sex-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='user-edit-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - style={ - 'width': 200 - } - ), - label='用户状态', - id='user-edit-status-form-item', - labelCol={ - 'offset': 1 - }, - ) - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdSelect( - id='user-edit-post', - placeholder='请选择岗位', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-edit-post-form-item', - labelCol={ - 'offset': 4 - }, - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-edit-role', - placeholder='请选择角色', - options=[], - mode='multiple', - style={ - 'width': 200 - } - ), - label='岗位', - id='user-edit-role-form-item', - labelCol={ - 'offset': 7 - }, - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='user-edit-remark', - placeholder='请输入内容', - mode='text-area', - style={ - 'width': 485 - } - ), - label='备注', - id='user-edit-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] - ) - ] - ) - ], - id='user-edit-modal', - title='编辑用户', - mask=False, - width=650, - renderFooter=True, - okClickClose=False - ), - - # 删除用户二次确认modal - fac.AntdModal( - fac.AntdText('是否确认删除?', id='user-delete-text'), - id='user-delete-confirm-modal', - visible=False, - title='提示', - renderFooter=True, - centered=True - ), - - # 重置密码modal - fac.AntdModal( - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='reset-password-input', - mode='password' - ), - ), - ], - layout='vertical' - ), - id='user-reset-password-confirm-modal', - visible=False, - title='提示', - renderFooter=True, - centered=True - ), - ] -- Gitee From 289bc55ac2f6c43826ff4f40e0576546ff2b6aca Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sat, 10 Jun 2023 19:12:57 +0800 Subject: [PATCH 007/169] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88=E5=A2=9E?= =?UTF-8?q?=E5=88=A0=E6=9F=A5=E6=94=B9=E5=B7=B2=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menu_c/components_c/button_type_c.py | 109 ++++ .../menu_c/components_c/content_type_c.py | 40 +- .../menu_c/components_c/menu_type_c.py | 145 ++++++ .../callbacks/system_c/menu_c/menu_c.py | 45 +- dash-fastapi-frontend/config/global_config.py | 468 ++++++++++++++++++ .../views/system/menu/__init__.py | 19 +- .../system/menu/components/button_type.py | 2 + .../system/menu/components/content_type.py | 6 +- .../system/menu/components/icon_category.py | 32 ++ .../views/system/menu/components/menu_type.py | 14 +- 10 files changed, 839 insertions(+), 41 deletions(-) create mode 100644 dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py create mode 100644 dash-fastapi-frontend/views/system/menu/components/icon_category.py diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py new file mode 100644 index 0000000..773a70d --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/button_type_c.py @@ -0,0 +1,109 @@ +import dash +import time +from dash.dependencies import Input, Output, State +import feffery_utils_components as fuc + +from server import app +from api.menu import add_menu_api, edit_menu_api + + +@app.callback( + [Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True), + Output('menu-modal', 'visible', allow_duplicate=True), + Output('menu-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('menu-modal-F-trigger', 'data'), + [State('menu-operations-store-bk', 'data'), + State('menu-edit-id-store', 'data'), + State('menu-parent_id', 'value'), + State('menu-menu_type', 'value'), + State('menu-icon', 'value'), + State('menu-menu_name', 'value'), + State('menu-order_num', 'value'), + State('button-menu-perms', 'value')], + prevent_initial_call=True +) +def menu_confirm_button(confirm_trigger, operation_type, cur_menu_info, parent_id, menu_type, icon, menu_name, order_num, perms): + if confirm_trigger: + if all([parent_id, menu_name, order_num]): + params_add = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, perms=perms) + params_edit = dict(menu_id=cur_menu_info.get('menu_id') if cur_menu_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, + menu_name=menu_name, order_num=order_num, perms=perms) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_menu_api(params_add) + if operation_type == 'edit': + api_res = edit_menu_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if parent_id else 'error', + None if menu_name else 'error', + None if order_num else 'error', + None if parent_id else '请选择上级菜单!', + None if menu_name else '请输入菜单名称!', + None if order_num else '请输入显示排序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + Output('button-menu-perms', 'value'), + Input('menu-edit-id-store', 'data') +) +def set_edit_info(edit_info): + if edit_info: + return edit_info.get('perms') + + return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py index a1dfb66..4b69210 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/content_type_c.py @@ -1,17 +1,9 @@ import dash import time -import uuid -from dash import html from dash.dependencies import Input, Output, State -import feffery_antd_components as fac import feffery_utils_components as fuc -from jsonpath_ng import parse -from flask import session, json -from collections import OrderedDict from server import app -from utils.tree_tool import list_to_tree -from views.system.menu.components import * from api.menu import add_menu_api, edit_menu_api @@ -30,6 +22,7 @@ from api.menu import add_menu_api, edit_menu_api Output('global-message-container', 'children', allow_duplicate=True)], Input('menu-modal-M-trigger', 'data'), [State('menu-operations-store-bk', 'data'), + State('menu-edit-id-store', 'data'), State('menu-parent_id', 'value'), State('menu-menu_type', 'value'), State('menu-icon', 'value'), @@ -41,17 +34,19 @@ from api.menu import add_menu_api, edit_menu_api State('content-menu-status', 'value')], prevent_initial_call=True ) -def menu_confirm(confirm_trigger, operation_type, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, visible, status): +def menu_confirm_content(confirm_trigger, operation_type, cur_menu_info, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, visible, status): if confirm_trigger: if all([parent_id, menu_name, order_num, path]): - params = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, - is_frame=is_frame, path=path, visible=visible, status=status) + params_add = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, + is_frame=is_frame, path=path, visible=visible, status=status) + params_edit = dict(menu_id=cur_menu_info.get('menu_id') if cur_menu_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, + menu_name=menu_name, order_num=order_num, is_frame=is_frame, path=path, visible=visible, status=status) api_res = {} operation_type = operation_type.get('type') if operation_type == 'add': - api_res = add_menu_api(params) + api_res = add_menu_api(params_add) if operation_type == 'edit': - api_res = edit_menu_api(params) + api_res = edit_menu_api(params_edit) if api_res.get('code') == 200: if operation_type == 'add': return [ @@ -115,3 +110,22 @@ def menu_confirm(confirm_trigger, operation_type, parent_id, menu_type, icon, me ] return [dash.no_update] * 12 + + +@app.callback( + [Output('content-menu-is_frame', 'value'), + Output('content-menu-path', 'value'), + Output('content-menu-visible', 'value'), + Output('content-menu-status', 'value')], + Input('menu-edit-id-store', 'data') +) +def set_edit_info(edit_info): + if edit_info: + return [ + edit_info.get('is_frame'), + edit_info.get('path'), + edit_info.get('visible'), + edit_info.get('status') + ] + + return [dash.no_update] * 4 diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py new file mode 100644 index 0000000..5ca9d4a --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/components_c/menu_type_c.py @@ -0,0 +1,145 @@ +import dash +import time +from dash.dependencies import Input, Output, State +import feffery_utils_components as fuc + +from server import app +from api.menu import add_menu_api, edit_menu_api + + +@app.callback( + [Output('menu-parent_id-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu_name-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-order_num-form-item', 'validateStatus', allow_duplicate=True), + Output('menu-menu-path-form-item', 'validateStatus'), + Output('menu-parent_id-form-item', 'help', allow_duplicate=True), + Output('menu-menu_name-form-item', 'help', allow_duplicate=True), + Output('menu-order_num-form-item', 'help', allow_duplicate=True), + Output('menu-menu-path-form-item', 'help', allow_duplicate=True), + Output('menu-modal', 'visible'), + Output('menu-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('menu-modal-C-trigger', 'data'), + [State('menu-operations-store-bk', 'data'), + State('menu-edit-id-store', 'data'), + State('menu-parent_id', 'value'), + State('menu-menu_type', 'value'), + State('menu-icon', 'value'), + State('menu-menu_name', 'value'), + State('menu-order_num', 'value'), + State('menu-menu-is_frame', 'value'), + State('menu-menu-path', 'value'), + State('menu-menu-component', 'value'), + State('menu-menu-perms', 'value'), + State('menu-menu-query', 'value'), + State('menu-menu-is_cache', 'value'), + State('menu-menu-visible', 'value'), + State('menu-menu-status', 'value')], + prevent_initial_call=True +) +def menu_confirm_menu(confirm_trigger, operation_type, cur_menu_info, parent_id, menu_type, icon, menu_name, order_num, is_frame, path, + component, perms, query, is_cache, visible, status): + if confirm_trigger: + if all([parent_id, menu_name, order_num, path]): + params_add = dict(parent_id=parent_id, menu_type=menu_type, icon=icon, menu_name=menu_name, order_num=order_num, is_frame=is_frame, + path=path, component=component, perms=perms, query=query, is_cache=is_cache, visible=visible, status=status) + params_edit = dict(menu_id=cur_menu_info.get('menu_id') if cur_menu_info else None, parent_id=parent_id, menu_type=menu_type, icon=icon, + menu_name=menu_name, order_num=order_num, is_frame=is_frame, path=path, component=component, + perms=perms, query=query, is_cache=is_cache, visible=visible, status=status) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_menu_api(params_add) + if operation_type == 'edit': + api_res = edit_menu_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if parent_id else 'error', + None if menu_name else 'error', + None if order_num else 'error', + None if path else 'error', + None if parent_id else '请选择上级菜单!', + None if menu_name else '请输入菜单名称!', + None if order_num else '请输入显示排序!', + None if path else '请输入路由地址!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 12 + + +@app.callback( + [Output('menu-menu-is_frame', 'value'), + Output('menu-menu-path', 'value'), + Output('menu-menu-component', 'value'), + Output('menu-menu-perms', 'value'), + Output('menu-menu-query', 'value'), + Output('menu-menu-is_cache', 'value'), + Output('menu-menu-visible', 'value'), + Output('menu-menu-status', 'value')], + Input('menu-edit-id-store', 'data') +) +def set_edit_info(edit_info): + if edit_info: + return [ + edit_info.get('is_frame'), + edit_info.get('path'), + edit_info.get('component'), + edit_info.get('perms'), + edit_info.get('query'), + edit_info.get('is_cache'), + edit_info.get('visible'), + edit_info.get('status') + ] + + return [dash.no_update] * 8 diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py index 9a3ea86..a37b466 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -101,11 +101,33 @@ def reset_menu_query_params(reset_click): return [dash.no_update] * 3 +@app.callback( + [Output('menu-icon', 'value'), + Output('menu-icon', 'prefix')], + Input('icon-category', 'value'), + prevent_initial_call=True +) +def get_select_icon(icon): + if icon: + return [ + icon, + fac.AntdIcon(icon=icon) + ] + + return [dash.no_update] * 2 + + + @app.callback( [Output('menu-modal', 'visible', allow_duplicate=True), Output('menu-modal', 'title'), Output('menu-parent_id', 'treeData'), Output('menu-parent_id', 'value'), + Output('menu-menu_type', 'value'), + Output('menu-icon', 'value', allow_duplicate=True), + Output('menu-icon', 'prefix', allow_duplicate=True), + Output('menu-menu_name', 'value'), + Output('menu-order_num', 'value'), Output('api-check-token', 'data', allow_duplicate=True), Output('menu-add', 'nClicks'), Output('menu-edit-id-store', 'data'), @@ -132,6 +154,11 @@ def add_menu_modal(add_click, button_click, clicked_content, recently_button_cli '新增菜单', tree_data, '0', + 'M', + None, + None, + None, + None, {'timestamp': time.time()}, None, None, @@ -143,12 +170,17 @@ def add_menu_modal(add_click, button_click, clicked_content, recently_button_cli '新增菜单', tree_data, str(recently_button_clicked_row['key']), + 'M', + None, + None, + None, + None, {'timestamp': time.time()}, None, - 'add', + None, {'type': 'add'} ] - else: + elif button_click and clicked_content == '修改': menu_id = int(recently_button_clicked_row['key']) menu_info_res = get_menu_detail_api(menu_id=menu_id) if menu_info_res['code'] == 200: @@ -158,15 +190,20 @@ def add_menu_modal(add_click, button_click, clicked_content, recently_button_cli '编辑菜单', tree_data, str(menu_info.get('parent_id')), + menu_info.get('menu_type'), + menu_info.get('icon'), + fac.AntdIcon(icon=menu_info.get('icon')), + menu_info.get('menu_name'), + menu_info.get('order_num'), {'timestamp': time.time()}, None, menu_info, {'type': 'edit'} ] - return [dash.no_update] * 4 + [{'timestamp': time.time()}, None, None, None] + return [dash.no_update] * 9 + [{'timestamp': time.time()}, None, None, None] - return [dash.no_update] * 5 + [None, None, None] + return [dash.no_update] * 10 + [None, None, None] @app.callback( diff --git a/dash-fastapi-frontend/config/global_config.py b/dash-fastapi-frontend/config/global_config.py index 7d337b0..ea4ffd0 100644 --- a/dash-fastapi-frontend/config/global_config.py +++ b/dash-fastapi-frontend/config/global_config.py @@ -19,3 +19,471 @@ class ApiBaseUrlConfig: # api基本url BaseUrl = 'http://127.0.0.1:9099' + + +class IconConfig: + + ICON_LIST = [ + 'antd-carry-out', + 'antd-car', + 'antd-bulb', + 'antd-build', + 'antd-bug', + 'antd-bar-code', + 'antd-branches', + 'antd-aim', + 'antd-issues-close', + 'antd-ellipsis', + 'antd-user', + 'antd-unlock', + 'antd-repair', + 'antd-team', + 'antd-sync', + 'antd-setting', + 'antd-send', + 'antd-schedule', + 'antd-save', + 'antd-rocket', + 'antd-reload', + 'antd-read', + 'antd-qrcode', + 'antd-power-off', + 'antd-number', + 'antd-notification', + 'antd-menu', + 'antd-mail', + 'antd-lock', + 'antd-loading', + 'antd-key', + 'antd-hourglass', + 'antd-global', + 'antd-function', + 'antd-import', + 'antd-export', + 'antd-dashboard', + 'antd-control', + 'antd-console-sql', + 'antd-compass', + 'antd-comment', + 'antd-code', + 'antd-cluster', + 'antd-clear', + 'antd-camera', + 'antd-book', + 'antd-catalog', + 'antd-api', + 'antd-alert', + 'antd-account-book', + 'antd-alipay', + 'antd-alipay-circle', + 'antd-weibo', + 'antd-github', + 'antd-fall', + 'antd-rise', + 'antd-stock', + 'antd-home', + 'antd-fund', + 'antd-area-chart', + 'antd-radar-chart', + 'antd-bar-chart', + 'antd-pie-chart', + 'antd-box-plot', + 'antd-dot-chart', + 'antd-line-chart', + 'antd-field-binary', + 'antd-field-number', + 'antd-field-string', + 'antd-field-time', + 'antd-file-add', + 'antd-file-done', + 'antd-file', + 'antd-file-image', + 'antd-file-markdown', + 'antd-file-pdf', + 'antd-file-protect', + 'antd-file-sync', + 'antd-file-text', + 'antd-file-word', + 'antd-file-zip', + 'antd-filter', + 'antd-fire', + 'antd-woman', + 'antd-arrow-up', + 'antd-arrow-down', + 'antd-arrow-left', + 'antd-arrow-right', + 'antd-flag', + 'antd-user-add', + 'antd-folder-add', + 'antd-man', + 'antd-tag', + 'antd-folder', + 'antd-user-delete', + 'antd-trophy', + 'antd-shopping-cart', + 'antd-folder-open', + 'antd-fork', + 'antd-select', + 'antd-tags', + 'antd-thunderbolt', + 'antd-sound', + 'antd-fund-projection-screen', + 'antd-funnel-plot', + 'antd-gift', + 'antd-robot', + 'antd-pushpin', + 'antd-printer', + 'antd-phone', + 'antd-picture', + 'antd-idcard', + 'antd-partition', + 'antd-monitor', + 'antd-more', + 'antd-apartment', + 'antd-money-collect', + 'antd-experiment', + 'antd-link', + 'antd-mobile', + 'antd-coffee', + 'antd-layout', + 'antd-eye', + 'antd-eye-invisible', + 'antd-exception', + 'antd-dollar', + 'antd-euro', + 'antd-download', + 'antd-environment', + 'antd-deployment-unit', + 'antd-crown', + 'antd-desktop', + 'antd-like', + 'antd-dislike', + 'antd-disconnect', + 'antd-app-store', + 'antd-app-store-add', + 'antd-bell', + 'antd-calculator', + 'antd-calendar', + 'antd-database', + 'antd-history', + 'antd-search', + 'antd-file-search', + 'antd-cloud', + 'antd-cloud-upload', + 'antd-cloud-download', + 'antd-cloud-server', + 'antd-cloud-sync', + 'antd-swap', + 'antd-rollback', + 'antd-login', + 'antd-logout', + 'antd-menu-fold', + 'antd-menu-unfold', + 'antd-full-screen', + 'antd-full-screen-exit', + 'antd-question-circle', + 'antd-plus-circle', + 'antd-minus-circle', + 'antd-info-circle', + 'antd-exclamation-circle', + 'antd-close-circle', + 'antd-check-circle', + 'antd-clock-circle', + 'antd-stop', + 'antd-edit', + 'antd-delete', + 'antd-highlight', + 'antd-redo', + 'antd-undo', + 'antd-zoom-in', + 'antd-zoom-out', + 'antd-sort-ascending', + 'antd-sort-descending', + 'antd-table', + 'antd-question', + 'antd-plus', + 'antd-minus', + 'antd-close', + 'antd-check', + 'antd-sketch', + 'antd-bank', + 'antd-block', + 'antd-insurance', + 'antd-smile', + 'antd-skin', + 'antd-star', + 'antd-right-circle-two-tone', + 'antd-left-circle-two-tone', + 'antd-up-circle-two-tone', + 'antd-down-circle-two-tone', + 'antd-up-square-two-tone', + 'antd-down-square-two-tone', + 'antd-left-square-two-tone', + 'antd-right-square-two-tone', + 'antd-question-circle-two-tone', + 'antd-plus-circle-two-tone', + 'antd-minus-circle-two-tone', + 'antd-plus-square-two-tone', + 'antd-minus-square-two-tone', + 'antd-info-circle-two-tone', + 'antd-exclamation-circle-two-tone', + 'antd-close-circle-two-tone', + 'antd-close-square-two-tone', + 'antd-check-circle-two-tone', + 'antd-check-square-two-tone', + 'antd-edit-two-tone', + 'antd-delete-two-tone', + 'antd-highlight-two-tone', + 'antd-pie-chart-two-tone', + 'antd-box-chart-two-tone', + 'antd-fund-two-tone', + 'antd-sliders-two-tone', + 'antd-api-two-tone', + 'antd-cloud-two-tone', + 'antd-hourglass-two-tone', + 'antd-notification-two-tone', + 'antd-tool-two-tone', + 'antd-down', + 'antd-up', + 'antd-left', + 'antd-right', + 'md-star-half', + 'md-star-border', + 'md-star', + 'md-people', + 'md-plus-one', + 'md-notifications', + 'md-pin-drop', + 'md-layers-clear', + 'md-layers', + 'md-edit-location', + 'md-tune', + 'md-transform', + 'md-timer-off', + 'md-timer', + 'md-file-upload', + 'md-file-download', + 'md-create-new-folder', + 'md-cloud-upload', + 'md-cloud-queue', + 'md-cloud-download', + 'md-cloud-done', + 'md-insert-chart', + 'md-functions', + 'md-format-quote', + 'md-attach-file', + 'md-storage', + 'md-save', + 'md-remove-circle-outline', + 'md-remove-circle', + 'md-remove', + 'md-low-priority', + 'md-link', + 'md-gesture', + 'md-forward', + 'md-flag', + 'md-drafts', + 'md-create', + 'md-content-paste', + 'md-content-cut', + 'md-content-copy', + 'md-clear', + 'md-block', + 'md-backspace', + 'md-add-box', + 'md-add', + 'md-add-circle-outline', + 'md-add-circle', + 'md-location-on', + 'md-mail-outline', + 'md-email', + 'md-not-interested', + 'md-library-books', + 'md-library-add', + 'md-equalizer', + 'md-add-alert', + 'md-visibility-off', + 'md-visibility', + 'md-verified-user', + 'md-update', + 'md-trending-up', + 'md-trending-flat', + 'md-trending-down', + 'md-translate', + 'md-toc', + 'md-timeline', + 'md-thumb-up', + 'md-thumb-down', + 'md-swap-vert', + 'md-swap-horiz', + 'md-supervisor-account', + 'md-subject', + 'md-settings', + 'md-search', + 'md-schedule', + 'md-restore', + 'md-query-builder', + 'md-power-settings-new', + 'md-opacity', + 'md-note-add', + 'md-lock-outline', + 'md-lock-open', + 'md-list', + 'md-lightbulb-outline', + 'md-launch', + 'md-label-outline', + 'md-label', + 'md-input', + 'md-info-outline', + 'md-info', + 'md-hourglass', + 'md-home', + 'md-history', + 'md-highlight-off', + 'md-help-outline', + 'md-help', + 'md-get-app', + 'md-translate', + 'md-fingerprint', + 'md-findIn-page', + 'md-favorite-border', + 'md-favorite', + 'md-extension', + 'md-explore', + 'md-exit-to-app', + 'md-event', + 'md-description', + 'md-delete-forever', + 'md-delete', + 'md-dashboard', + 'md-code', + 'md-build', + 'md-bug-report', + 'md-assignment', + 'md-assessment', + 'md-alarm-on', + 'md-alarm-off', + 'md-alarm-add', + 'md-alarm', + 'md-account-circle', + 'fc-vlc', + 'fc-view-details', + 'fc-upload', + 'fc-tree-structure', + 'fc-timeline', + 'fc-template', + 'fc-survey', + 'fc-signature', + 'fc-share', + 'fc-services', + 'fc-rules', + 'fc-questions', + 'fc-process', + 'fc-plus', + 'fc-overtime', + 'fc-organization', + 'fc-numerical-sorting21', + 'fc-numerical-sorting12', + 'fc-multiple-inputs', + 'fc-mind-map', + 'fc-menu', + 'fc-list', + 'fc-like', + 'fc-like-placeholder', + 'fc-info', + 'fc-import', + 'fc-image-file', + 'fc-idea', + 'fc-home', + 'fc-high-priority', + 'fc-low-priority', + 'fc-genealogy', + 'fc-full-trash', + 'fc-document-search', + 'fc-file', + 'fc-faq', + 'fc-export', + 'fc-empty-trash', + 'fc-download', + 'fc-document', + 'fc-deployment', + 'fc-delete-database', + 'fc-conference-call', + 'fc-database', + 'fc-data-protection', + 'fc-data-encryption', + 'fc-data-configuration', + 'fc-data-backup', + 'fc-checkmark', + 'fc-cancel', + 'fc-briefcase', + 'fc-binoculars', + 'fc-automatic', + 'fc-alphabetical-sorting-za', + 'fc-alphabetical-sorting-az', + 'fc-add-database', + 'fc-accept-database', + 'fc-about', + 'fc-radar-chart', + 'fc-scatter-chart', + 'fc-pie-chart', + 'fc-line-chart', + 'fc-flow-chart', + 'fc-doughnut-chart', + 'fc-bar-chart', + 'fc-area-chart', + 'fc-line-bar-chart', + 'fc-workflow', + 'fc-todo-list', + 'fc-synchronize', + 'fc-repair', + 'fc-statistics', + 'fc-settings', + 'fc-search', + 'fc-serial-tasks', + 'fc-safe', + 'fc-negative-dynamic', + 'fc-positive-dynamic', + 'fc-planner', + 'fc-parallel-tasks', + 'fc-org-unit', + 'fc-opened-folder', + 'fc-ok', + 'fc-inspection', + 'fc-globe', + 'fc-folder', + 'fc-electronics', + 'fc-data-sheet', + 'fc-command-line', + 'fc-calendar', + 'fc-calculator', + 'fc-bullish', + 'fc-bearish', + 'fc-bookmark', + 'fc-approval', + 'fc-advertising', + 'di-linux', + 'di-python', + 'di-chrome', + 'di-database', + 'di-firefox', + 'di-markdown', + 'di-postgresql', + 'di-terminal', + 'di-windows', + 'bi-table', + 'bi-analyse', + 'bi-layer', + 'bi-layer-minus', + 'bi-layer-plus', + 'bs-list-task', + 'bs-list-check', + 'bs-link', + 'bs-link-45-deg', + 'bs-envelope-open', + 'bs-envelope', + 'bs-alarm', + 'gi-mesh-network', + 'im-earth', + 'im-sphere' + ] diff --git a/dash-fastapi-frontend/views/system/menu/__init__.py b/dash-fastapi-frontend/views/system/menu/__init__.py index ce00838..17fb505 100644 --- a/dash-fastapi-frontend/views/system/menu/__init__.py +++ b/dash-fastapi-frontend/views/system/menu/__init__.py @@ -3,6 +3,7 @@ import feffery_antd_components as fac from api.menu import get_menu_list_api from utils.tree_tool import list_to_tree +from views.system.menu.components.icon_category import render_icon import callbacks.system_c.menu_c.menu_c @@ -334,27 +335,15 @@ def render(): fac.AntdPopover( fac.AntdInput( id='menu-icon', - placeholder='点击选择图标', + placeholder='点击此处选择图标', readOnly=True, - prefix=fac.AntdIcon( - icon='antd-search' - ), style={ 'width': 495 } ), - title=fac.AntdInput( - id='menu-icon-search', - placeholder='请输入图标名称', - suffix=fac.AntdIcon( - icon='antd-search' - ), - style={ - 'width': 450 - } - ), + content=render_icon(), trigger='click', - placement='bottom', + placement='bottom' ), label='菜单图标', id='menu-icon-form-item', diff --git a/dash-fastapi-frontend/views/system/menu/components/button_type.py b/dash-fastapi-frontend/views/system/menu/components/button_type.py index 824fd71..e84ece6 100644 --- a/dash-fastapi-frontend/views/system/menu/components/button_type.py +++ b/dash-fastapi-frontend/views/system/menu/components/button_type.py @@ -1,6 +1,8 @@ from dash import html import feffery_antd_components as fac +import callbacks.system_c.menu_c.components_c.button_type_c + def render(): return [ diff --git a/dash-fastapi-frontend/views/system/menu/components/content_type.py b/dash-fastapi-frontend/views/system/menu/components/content_type.py index 5d4e120..5d663af 100644 --- a/dash-fastapi-frontend/views/system/menu/components/content_type.py +++ b/dash-fastapi-frontend/views/system/menu/components/content_type.py @@ -14,14 +14,14 @@ def render(): options=[ { 'label': '是', - 'value': '0' + 'value': 0 }, { 'label': '否', - 'value': '1' + 'value': 1 }, ], - defaultValue='1', + defaultValue=1, style={ 'width': 200 } diff --git a/dash-fastapi-frontend/views/system/menu/components/icon_category.py b/dash-fastapi-frontend/views/system/menu/components/icon_category.py new file mode 100644 index 0000000..1fe1e9b --- /dev/null +++ b/dash-fastapi-frontend/views/system/menu/components/icon_category.py @@ -0,0 +1,32 @@ +from dash import html +import feffery_antd_components as fac +import feffery_utils_components as fuc +from config.global_config import IconConfig + + +def render_icon(): + + return html.Div( + [ + fac.AntdRadioGroup( + id='icon-category', + options=[ + { + 'label': fac.AntdIcon( + icon=icon, + ), + 'value': icon + } + for icon in IconConfig.ICON_LIST + ], + style={ + 'width': 450, + 'paddingLeft': '10px' + } + ), + ], + style={ + 'maxHeight': '135px', + 'overflow': 'auto' + } + ) diff --git a/dash-fastapi-frontend/views/system/menu/components/menu_type.py b/dash-fastapi-frontend/views/system/menu/components/menu_type.py index ebb27bb..6d97592 100644 --- a/dash-fastapi-frontend/views/system/menu/components/menu_type.py +++ b/dash-fastapi-frontend/views/system/menu/components/menu_type.py @@ -1,6 +1,8 @@ from dash import html import feffery_antd_components as fac +import callbacks.system_c.menu_c.components_c.menu_type_c + def render(): return [ @@ -12,14 +14,14 @@ def render(): options=[ { 'label': '是', - 'value': '0' + 'value': 0 }, { 'label': '否', - 'value': '1' + 'value': 1 }, ], - defaultValue='1', + defaultValue=1, style={ 'width': 200 } @@ -172,14 +174,14 @@ def render(): options=[ { 'label': '缓存', - 'value': '0' + 'value': 0 }, { 'label': '不缓存', - 'value': '1' + 'value': 1 }, ], - defaultValue='0', + defaultValue=0, style={ 'width': 200 } -- Gitee From fa3870f09d346f5a8d938bf0410572b691454296 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sat, 10 Jun 2023 19:13:15 +0800 Subject: [PATCH 008/169] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88=E5=A2=9E?= =?UTF-8?q?=E5=88=A0=E6=9F=A5=E6=94=B9=E5=B7=B2=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/layout/__init__.py | 142 +++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/dash-fastapi-frontend/views/layout/__init__.py b/dash-fastapi-frontend/views/layout/__init__.py index 1e652e2..c2ab0e6 100644 --- a/dash-fastapi-frontend/views/layout/__init__.py +++ b/dash-fastapi-frontend/views/layout/__init__.py @@ -1,3 +1,139 @@ -from . import ( - index, -) +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac + +from views.layout.components.head import render_head_content +from views.layout.components.content import render_main_content +from views.layout.components.aside import render_aside_content +# import callbacks.index_c +import callbacks.layout_c.fold_side_menu +import callbacks.layout_c.index_c + + +def render_content(user_name, nick_name, phone_number, menu_info): + + return fuc.FefferyTopProgress( + html.Div( + [ + # 全局重载 + fuc.FefferyReload(id='trigger-reload-output'), + + html.Div(id='idle-placeholder-container'), + + # 注入相关modal + html.Div( + [ + # 个人资料面板 + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdText( + user_name, + copyable=True + ), + label='账号' + ), + fac.AntdFormItem( + fac.AntdText( + nick_name, + copyable=True + ), + label='姓名' + ), + fac.AntdFormItem( + fac.AntdText( + phone_number, + copyable=True + ), + label='电话' + ) + ], + labelCol={ + 'span': 4 + } + ) + ], + id='index-personal-info-modal', + title='个人资料', + mask=False + ), + ] + ), + + # 退出登录对话框提示 + fac.AntdModal( + html.Div( + [ + fac.AntdIcon(icon='fc-info', style={'font-size': '28px'}), + fac.AntdText('确定注销并退出系统吗?', style={'margin-left': '5px'}), + ] + ), + id='logout-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + + # 平台主页面 + fac.AntdRow( + [ + # 左侧固定菜单区域 + fac.AntdCol( + fac.AntdAffix( + html.Div( + render_aside_content(menu_info), + id='side-menu', + style={ + 'height': '100vh', + 'overflowY': 'auto', + 'transition': 'width 1s', + 'background': '#001529' + } + ), + ), + # flex='1', + id='left-side-menu-container', + style={ + 'flex': '1' + } + ), + + # 右侧区域 + fac.AntdCol( + [ + fac.AntdRow( + render_head_content(user_name), + style={ + 'height': '50px', + 'boxShadow': 'rgb(240 241 242) 0px 2px 14px', + 'background': 'white', + 'marginBottom': '10px', + 'position': 'sticky', + 'top': 0, + 'zIndex': 999 + } + ), + fac.AntdRow( + render_main_content(user_name, nick_name, phone_number), + wrap=False + ) + ], + # flex='5', + style={ + 'flex': '6', + 'width': '300px' + } + ), + ], + ) + ], + id='index-main-content-container', + ), + listenPropsMode='include', + includeProps=[ + 'tabs-container.items' + ] + ) -- Gitee From 950d2c0cac9495b52cdb68d154f20fd7ab3ac891 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sun, 11 Jun 2023 21:56:25 +0800 Subject: [PATCH 009/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=BC=82=E5=B8=B8=E7=94=A8=E4=BA=8E=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E6=A3=80=E6=B5=8Btoken=EF=BC=8C=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E5=8E=9F=E6=9C=89token=E6=A3=80=E9=AA=8C=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 7 ++ .../controller/dept_controller.py | 112 +++++++----------- .../controller/login_controller.py | 22 ++-- .../controller/menu_controller.py | 111 ++++++----------- .../controller/post_controler.py | 98 ++++++--------- .../controller/role_controller.py | 15 +-- .../controller/user_controller.py | 88 ++++++-------- dash-fastapi-backend/service/login_service.py | 21 +++- dash-fastapi-backend/utils/response_tool.py | 9 ++ 9 files changed, 190 insertions(+), 293 deletions(-) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 543e5ed..064c55c 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -12,6 +12,7 @@ from controller.dept_controller import deptController from controller.role_controller import roleController from controller.post_controler import postController from config.env import RedisConfig +from utils.response_tool import response_401, AuthException app = FastAPI() @@ -52,6 +53,12 @@ async def startup_event(): @app.on_event("shutdown") async def shutdown_event(): await app.state.redis.close() + + +# 自定义token检验异常 +@app.exception_handler(AuthException) +async def auth_exception_handler(request: Request, exc: AuthException): + return response_401(data=exc.data, message=exc.message) @app.exception_handler(HTTPException) diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/controller/dept_controller.py index 929d133..d3e84e9 100644 --- a/dash-fastapi-backend/controller/dept_controller.py +++ b/dash-fastapi-backend/controller/dept_controller.py @@ -9,52 +9,37 @@ from utils.response_tool import * from utils.log_tool import * -deptController = APIRouter() +deptController = APIRouter(dependencies=[Depends(get_current_user)]) @deptController.post("/dept/tree", response_model=DeptTree) -async def get_system_dept_tree(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - dept_query_result = get_dept_tree_services(query_db, dept_query) - logger.info('获取成功') - return response_200(data=dept_query_result, message="获取成功") + dept_query_result = get_dept_tree_services(query_db, dept_query) + logger.info('获取成功') + return response_200(data=dept_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @deptController.post("/dept/forEditOption", response_model=DeptTree) -async def get_system_dept_tree_for_edit_option(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) - logger.info('获取成功') - return response_200(data=dept_query_result, message="获取成功") + dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) + logger.info('获取成功') + return response_200(data=dept_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @deptController.post("/dept/get", response_model=DeptResponse) -async def get_system_dept_list(request: Request, dept_query: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - dept_query_result = get_dept_list_services(query_db, dept_query) - logger.info('获取成功') - return response_200(data=dept_query_result, message="获取成功") + dept_query_result = get_dept_list_services(query_db, dept_query) + logger.info('获取成功') + return response_200(data=dept_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -64,18 +49,14 @@ async def get_system_dept_list(request: Request, dept_query: DeptModel, token: O async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + add_dept.create_by = current_user.user.user_name + add_dept.update_by = current_user.user.user_name + add_dept_result = add_dept_services(query_db, add_dept) + logger.info(add_dept_result.message) + if add_dept_result.is_success: + return response_200(data=add_dept_result, message=add_dept_result.message) else: - add_dept.create_by = current_user.user.user_name - add_dept.update_by = current_user.user.user_name - add_dept_result = add_dept_services(query_db, add_dept) - logger.info(add_dept_result.message) - if add_dept_result.is_success: - return response_200(data=add_dept_result, message=add_dept_result.message) - else: - return response_400(data="", message=add_dept_result.message) + return response_400(data="", message=add_dept_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -85,19 +66,15 @@ async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + edit_dept.update_by = current_user.user.user_name + edit_dept.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_dept_result = edit_dept_services(query_db, edit_dept) + if edit_dept_result.is_success: + logger.info(edit_dept_result.message) + return response_200(data=edit_dept_result, message=edit_dept_result.message) else: - edit_dept.update_by = current_user.user.user_name - edit_dept.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - edit_dept_result = edit_dept_services(query_db, edit_dept) - if edit_dept_result.is_success: - logger.info(edit_dept_result.message) - return response_200(data=edit_dept_result, message=edit_dept_result.message) - else: - logger.warning(edit_dept_result.message) - return response_400(data="", message=edit_dept_result.message) + logger.warning(edit_dept_result.message) + return response_400(data="", message=edit_dept_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -107,35 +84,26 @@ async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Option async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + delete_dept.update_by = current_user.user.user_name + delete_dept.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + delete_dept_result = delete_dept_services(query_db, delete_dept) + if delete_dept_result.is_success: + logger.info(delete_dept_result.message) + return response_200(data=delete_dept_result, message=delete_dept_result.message) else: - delete_dept.update_by = current_user.user.user_name - delete_dept.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - delete_dept_result = delete_dept_services(query_db, delete_dept) - if delete_dept_result.is_success: - logger.info(delete_dept_result.message) - return response_200(data=delete_dept_result, message=delete_dept_result.message) - else: - logger.warning(delete_dept_result.message) - return response_400(data="", message=delete_dept_result.message) + logger.warning(delete_dept_result.message) + return response_400(data="", message=delete_dept_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @deptController.get("/dept/{dept_id}", response_model=DeptModel) -async def query_detail_system_post(request: Request, dept_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def query_detail_system_post(dept_id: int, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - detail_dept_result = detail_dept_services(query_db, dept_id) - logger.info(f'获取dept_id为{dept_id}的信息成功') - return response_200(data=detail_dept_result, message='获取成功') + detail_dept_result = detail_dept_services(query_db, dept_id) + logger.info(f'获取dept_id为{dept_id}的信息成功') + return response_200(data=detail_dept_result, message='获取成功') except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/login_controller.py b/dash-fastapi-backend/controller/login_controller.py index 7370d35..9f5c4da 100644 --- a/dash-fastapi-backend/controller/login_controller.py +++ b/dash-fastapi-backend/controller/login_controller.py @@ -47,32 +47,24 @@ async def login(request: Request, user: UserLogin, query_db: Session = Depends(g return response_500(data="", message="接口异常") -@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse) +@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse, dependencies=[Depends(get_current_user)]) async def get_login_user_info(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - logger.info('获取成功') - return response_200(data=current_user, message="获取成功") + logger.info('获取成功') + return response_200(data=current_user, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @loginController.post("/logout") -async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db), dependencies=[Depends(get_current_user)]): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - await logout_services(request, current_user) - logger.info('退出成功') - return response_200(data=current_user, message="退出成功") + await logout_services(request, current_user) + logger.info('退出成功') + return response_200(data="", message="退出成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/menu_controller.py b/dash-fastapi-backend/controller/menu_controller.py index bb72b4f..14a29a6 100644 --- a/dash-fastapi-backend/controller/menu_controller.py +++ b/dash-fastapi-backend/controller/menu_controller.py @@ -9,52 +9,37 @@ from utils.response_tool import * from utils.log_tool import * -menuController = APIRouter() +menuController = APIRouter(dependencies=[Depends(get_current_user)]) @menuController.post("/menu/tree", response_model=MenuTree) -async def get_system_menu_tree(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_menu_tree(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - menu_query_result = get_menu_tree_services(query_db, menu_query) - logger.info('获取成功') - return response_200(data=menu_query_result, message="获取成功") + menu_query_result = get_menu_tree_services(query_db, menu_query) + logger.info('获取成功') + return response_200(data=menu_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @menuController.post("/menu/forEditOption", response_model=MenuTree) -async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) - logger.info('获取成功') - return response_200(data=menu_query_result, message="获取成功") + menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) + logger.info('获取成功') + return response_200(data=menu_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @menuController.post("/menu/get", response_model=MenuResponse) -async def get_system_menu_list(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - menu_query_result = get_menu_list_services(query_db, menu_query) - logger.info('获取成功') - return response_200(data=menu_query_result, message="获取成功") + menu_query_result = get_menu_list_services(query_db, menu_query) + logger.info('获取成功') + return response_200(data=menu_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -64,18 +49,14 @@ async def get_system_menu_list(request: Request, menu_query: MenuModel, token: O async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + add_menu.create_by = current_user.user.user_name + add_menu.update_by = current_user.user.user_name + add_menu_result = add_menu_services(query_db, add_menu) + logger.info(add_menu_result.message) + if add_menu_result.is_success: + return response_200(data=add_menu_result, message=add_menu_result.message) else: - add_menu.create_by = current_user.user.user_name - add_menu.update_by = current_user.user.user_name - add_menu_result = add_menu_services(query_db, add_menu) - logger.info(add_menu_result.message) - if add_menu_result.is_success: - return response_200(data=add_menu_result, message=add_menu_result.message) - else: - return response_400(data="", message=add_menu_result.message) + return response_400(data="", message=add_menu_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -85,55 +66,41 @@ async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + edit_menu.update_by = current_user.user.user_name + edit_menu.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_menu_result = edit_menu_services(query_db, edit_menu) + if edit_menu_result.is_success: + logger.info(edit_menu_result.message) + return response_200(data=edit_menu_result, message=edit_menu_result.message) else: - edit_menu.update_by = current_user.user.user_name - edit_menu.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - edit_menu_result = edit_menu_services(query_db, edit_menu) - if edit_menu_result.is_success: - logger.info(edit_menu_result.message) - return response_200(data=edit_menu_result, message=edit_menu_result.message) - else: - logger.warning(edit_menu_result.message) - return response_400(data="", message=edit_menu_result.message) + logger.warning(edit_menu_result.message) + return response_400(data="", message=edit_menu_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @menuController.post("/menu/delete", response_model=CrudMenuResponse) -async def delete_system_menu(request: Request, delete_menu: DeleteMenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + delete_menu_result = delete_menu_services(query_db, delete_menu) + if delete_menu_result.is_success: + logger.info(delete_menu_result.message) + return response_200(data=delete_menu_result, message=delete_menu_result.message) else: - delete_menu_result = delete_menu_services(query_db, delete_menu) - if delete_menu_result.is_success: - logger.info(delete_menu_result.message) - return response_200(data=delete_menu_result, message=delete_menu_result.message) - else: - logger.warning(delete_menu_result.message) - return response_400(data="", message=delete_menu_result.message) + logger.warning(delete_menu_result.message) + return response_400(data="", message=delete_menu_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @menuController.get("/menu/{menu_id}", response_model=MenuModel) -async def query_detail_system_menu(request: Request, menu_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def query_detail_system_menu(menu_id: int, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - detail_menu_result = detail_menu_services(query_db, menu_id) - logger.info(f'获取menu_id为{menu_id}的信息成功') - return response_200(data=detail_menu_result, message='获取成功') + detail_menu_result = detail_menu_services(query_db, menu_id) + logger.info(f'获取menu_id为{menu_id}的信息成功') + return response_200(data=detail_menu_result, message='获取成功') except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/post_controler.py b/dash-fastapi-backend/controller/post_controler.py index 9f5d6a0..138cfd9 100644 --- a/dash-fastapi-backend/controller/post_controler.py +++ b/dash-fastapi-backend/controller/post_controler.py @@ -8,36 +8,26 @@ from utils.response_tool import * from utils.log_tool import * -postController = APIRouter() +postController = APIRouter(dependencies=[Depends(get_current_user)]) @postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel) -async def get_system_post_select(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_post_select(query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - role_query_result = get_post_select_option_services(query_db) - logger.info('获取成功') - return response_200(data=role_query_result, message="获取成功") + role_query_result = get_post_select_option_services(query_db) + logger.info('获取成功') + return response_200(data=role_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @postController.post("/post/get", response_model=PostPageObjectResponse) -async def get_system_post_list(request: Request, user_query: PostPageObject, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_post_list(user_query: PostPageObject, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - post_query_result = get_post_list_services(query_db, user_query) - logger.info('获取成功') - return response_200(data=post_query_result, message="获取成功") + post_query_result = get_post_list_services(query_db, user_query) + logger.info('获取成功') + return response_200(data=post_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -47,18 +37,14 @@ async def get_system_post_list(request: Request, user_query: PostPageObject, tok async def add_system_post(request: Request, add_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + add_post.create_by = current_user.user.user_name + add_post.update_by = current_user.user.user_name + add_post_result = add_post_services(query_db, add_post) + logger.info(add_post_result.message) + if add_post_result.is_success: + return response_200(data=add_post_result, message=add_post_result.message) else: - add_post.create_by = current_user.user.user_name - add_post.update_by = current_user.user.user_name - add_post_result = add_post_services(query_db, add_post) - logger.info(add_post_result.message) - if add_post_result.is_success: - return response_200(data=add_post_result, message=add_post_result.message) - else: - return response_400(data="", message=add_post_result.message) + return response_400(data="", message=add_post_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -68,55 +54,41 @@ async def add_system_post(request: Request, add_post: PostModel, token: Optional async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + edit_post.update_by = current_user.user.user_name + edit_post.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_post_result = edit_post_services(query_db, edit_post) + if edit_post_result.is_success: + logger.info(edit_post_result.message) + return response_200(data=edit_post_result, message=edit_post_result.message) else: - edit_post.update_by = current_user.user.user_name - edit_post.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - edit_post_result = edit_post_services(query_db, edit_post) - if edit_post_result.is_success: - logger.info(edit_post_result.message) - return response_200(data=edit_post_result, message=edit_post_result.message) - else: - logger.warning(edit_post_result.message) - return response_400(data="", message=edit_post_result.message) + logger.warning(edit_post_result.message) + return response_400(data="", message=edit_post_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @postController.post("/post/delete", response_model=CrudPostResponse) -async def delete_system_post(request: Request, delete_post: DeletePostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def delete_system_post(delete_post: DeletePostModel, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + delete_post_result = delete_post_services(query_db, delete_post) + if delete_post_result.is_success: + logger.info(delete_post_result.message) + return response_200(data=delete_post_result, message=delete_post_result.message) else: - delete_post_result = delete_post_services(query_db, delete_post) - if delete_post_result.is_success: - logger.info(delete_post_result.message) - return response_200(data=delete_post_result, message=delete_post_result.message) - else: - logger.warning(delete_post_result.message) - return response_400(data="", message=delete_post_result.message) + logger.warning(delete_post_result.message) + return response_400(data="", message=delete_post_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @postController.get("/post/{post_id}", response_model=PostModel) -async def query_detail_system_post(request: Request, post_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def query_detail_system_post(post_id: int, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - delete_post_result = detail_post_services(query_db, post_id) - logger.info(f'获取post_id为{post_id}的信息成功') - return response_200(data=delete_post_result, message='获取成功') + detail_post_result = detail_post_services(query_db, post_id) + logger.info(f'获取post_id为{post_id}的信息成功') + return response_200(data=detail_post_result, message='获取成功') except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/role_controller.py b/dash-fastapi-backend/controller/role_controller.py index 667b727..20e5cab 100644 --- a/dash-fastapi-backend/controller/role_controller.py +++ b/dash-fastapi-backend/controller/role_controller.py @@ -8,20 +8,15 @@ from utils.response_tool import * from utils.log_tool import * -roleController = APIRouter() +roleController = APIRouter(dependencies=[Depends(get_current_user)]) @roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel) -async def get_system_role_select(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_role_select(query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - role_query_result = get_role_select_option_services(query_db) - logger.info('获取成功') - return response_200(data=role_query_result, message="获取成功") + role_query_result = get_role_select_option_services(query_db) + logger.info('获取成功') + return response_200(data=role_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/controller/user_controller.py b/dash-fastapi-backend/controller/user_controller.py index 744f198..0acecbb 100644 --- a/dash-fastapi-backend/controller/user_controller.py +++ b/dash-fastapi-backend/controller/user_controller.py @@ -9,20 +9,15 @@ from utils.response_tool import * from utils.log_tool import * -userController = APIRouter() +userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse) -async def get_system_user_list(request: Request, user_query: UserPageObject, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def get_system_user_list(user_query: UserPageObject, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - user_query_result = get_user_list_services(query_db, user_query) - logger.info('获取成功') - return response_200(data=user_query_result, message="获取成功") + user_query_result = get_user_list_services(query_db, user_query) + logger.info('获取成功') + return response_200(data=user_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -32,19 +27,15 @@ async def get_system_user_list(request: Request, user_query: UserPageObject, tok async def add_system_user(request: Request, add_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + add_user.password = get_password_hash(add_user.password) + add_user.create_by = current_user.user.user_name + add_user.update_by = current_user.user.user_name + add_user_result = add_user_services(query_db, add_user) + logger.info(add_user_result.message) + if add_user_result.is_success: + return response_200(data=add_user_result, message=add_user_result.message) else: - add_user.password = get_password_hash(add_user.password) - add_user.create_by = current_user.user.user_name - add_user.update_by = current_user.user.user_name - add_user_result = add_user_services(query_db, add_user) - logger.info(add_user_result.message) - if add_user_result.is_success: - return response_200(data=add_user_result, message=add_user_result.message) - else: - return response_400(data="", message=add_user_result.message) + return response_400(data="", message=add_user_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -54,19 +45,15 @@ async def add_system_user(request: Request, add_user: AddUserModel, token: Optio async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_user_result = edit_user_services(query_db, edit_user) + if edit_user_result.is_success: + logger.info(edit_user_result.message) + return response_200(data=edit_user_result, message=edit_user_result.message) else: - edit_user.update_by = current_user.user.user_name - edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - edit_user_result = edit_user_services(query_db, edit_user) - if edit_user_result.is_success: - logger.info(edit_user_result.message) - return response_200(data=edit_user_result, message=edit_user_result.message) - else: - logger.warning(edit_user_result.message) - return response_400(data="", message=edit_user_result.message) + logger.warning(edit_user_result.message) + return response_400(data="", message=edit_user_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -76,35 +63,26 @@ async def edit_system_user(request: Request, edit_user: AddUserModel, token: Opt async def delete_system_user(request: Request, delete_user: DeleteUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) + delete_user.update_by = current_user.user.user_name + delete_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + delete_user_result = delete_user_services(query_db, delete_user) + if delete_user_result.is_success: + logger.info(delete_user_result.message) + return response_200(data=delete_user_result, message=delete_user_result.message) else: - delete_user.update_by = current_user.user.user_name - delete_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - delete_user_result = delete_user_services(query_db, delete_user) - if delete_user_result.is_success: - logger.info(delete_user_result.message) - return response_200(data=delete_user_result, message=delete_user_result.message) - else: - logger.warning(delete_user_result.message) - return response_400(data="", message=delete_user_result.message) + logger.warning(delete_user_result.message) + return response_400(data="", message=delete_user_result.message) except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @userController.get("/user/{user_id}", response_model=UserDetailModel) -async def query_detail_system_user(request: Request, user_id: int, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): +async def query_detail_system_user(user_id: int, query_db: Session = Depends(get_db)): try: - current_user = await get_current_user(request, token, query_db) - if current_user == "用户token已失效,请重新登录" or current_user == "用户token不合法": - logger.warning(current_user) - return response_401(data="", message=current_user) - else: - delete_user_result = detail_user_services(query_db, user_id) - logger.info(f'获取user_id为{user_id}的信息成功') - return response_200(data=delete_user_result, message='获取成功') + delete_user_result = detail_user_services(query_db, user_id) + logger.info(f'获取user_id为{user_id}的信息成功') + return response_200(data=delete_user_result, message='获取成功') except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/service/login_service.py b/dash-fastapi-backend/service/login_service.py index 1fc4610..de2fc91 100644 --- a/dash-fastapi-backend/service/login_service.py +++ b/dash-fastapi-backend/service/login_service.py @@ -5,33 +5,41 @@ from jose import JWTError, jwt from passlib.context import CryptContext from config.env import JwtConfig from utils.response_tool import * +from utils.log_tool import * from datetime import datetime, timedelta from fastapi import Request +from fastapi import Depends, Header +from config.get_db import get_db pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -async def get_current_user(request: Request, token: str, result_db: Session): +async def get_current_user(request: Request = Request, token: str = Header(...), result_db: Session = Depends(get_db)): """ 根据token获取当前用户信息 :param request: Request对象 :param token: 用户token :param result_db: orm对象 :return: 当前用户信息对象 + :raise: 令牌异常AuthException """ if token[:6] != 'Bearer': - return "用户token不合法" + logger.warning("用户token不合法") + raise AuthException(data="", message="用户token不合法") try: payload = jwt.decode(token[6:], JwtConfig.SECRET_KEY, algorithms=[JwtConfig.ALGORITHM]) user_id: str = payload.get("user_id") if user_id is None: - return "用户token不合法" + logger.warning("用户token不合法") + raise AuthException(data="", message="用户token不合法") token_data = TokenData(user_id=int(user_id)) except JWTError: - return "用户token已失效,请重新登录" + logger.warning("用户token已失效,请重新登录") + raise AuthException(data="", message="用户token已失效,请重新登录") user = get_user_by_id(result_db, user_id=token_data.user_id) if user is None: - return "用户token不合法" + logger.warning("用户token不合法") + raise AuthException(data="", message="用户token不合法") redis_token = await request.app.state.redis.get(f'{user.user_basic_info[0].user_id}_access_token') redis_session = await request.app.state.redis.get(f'{user.user_basic_info[0].user_id}_session_id') if token[6:] == redis_token: @@ -53,7 +61,8 @@ async def get_current_user(request: Request, token: str, result_db: Session): menu=user_menu_info ) else: - return "用户token已失效,请重新登录" + logger.warning("用户token已失效,请重新登录") + raise AuthException(data="", message="用户token已失效,请重新登录") async def logout_services(request: Request, current_user: CurrentUserInfoServiceResponse): diff --git a/dash-fastapi-backend/utils/response_tool.py b/dash-fastapi-backend/utils/response_tool.py index 54fd53f..6140611 100644 --- a/dash-fastapi-backend/utils/response_tool.py +++ b/dash-fastapi-backend/utils/response_tool.py @@ -63,3 +63,12 @@ def response_500(*, data: str = None, message: str = "接口异常") -> Response } ) ) + + +class AuthException(Exception): + """ + 自定义令牌异常AuthException + """ + def __init__(self, data: str = None, message: str = None): + self.data = data + self.message = message -- Gitee From a95168ebe09e363bbbd51c1cfa3debf5c977a422 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Mon, 12 Jun 2023 17:54:44 +0800 Subject: [PATCH 010/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=88=A0=E6=9F=A5=E6=94=B9=E5=B7=B2=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/menu_controller.py | 2 +- .../controller/post_controler.py | 4 +- .../controller/role_controller.py | 75 +++ dash-fastapi-backend/mapper/crud/role_crud.py | 170 +++++- dash-fastapi-backend/mapper/crud/user_crud.py | 4 +- .../mapper/schema/menu_schema.py | 10 + .../mapper/schema/role_schema.py | 57 ++ .../mapper/schema/user_schema.py | 4 +- dash-fastapi-backend/service/menu_service.py | 9 +- dash-fastapi-backend/service/role_service.py | 84 +++ dash-fastapi-frontend/api/role.py | 25 + .../callbacks/system_c/menu_c/menu_c.py | 2 +- dash-fastapi-frontend/store/store.py | 9 + .../views/system/role/__init__.py | 491 +++++++++++++++++- 14 files changed, 929 insertions(+), 17 deletions(-) diff --git a/dash-fastapi-backend/controller/menu_controller.py b/dash-fastapi-backend/controller/menu_controller.py index 14a29a6..7abb191 100644 --- a/dash-fastapi-backend/controller/menu_controller.py +++ b/dash-fastapi-backend/controller/menu_controller.py @@ -13,7 +13,7 @@ menuController = APIRouter(dependencies=[Depends(get_current_user)]) @menuController.post("/menu/tree", response_model=MenuTree) -async def get_system_menu_tree(menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_services(query_db, menu_query) logger.info('获取成功') diff --git a/dash-fastapi-backend/controller/post_controler.py b/dash-fastapi-backend/controller/post_controler.py index 138cfd9..59a0411 100644 --- a/dash-fastapi-backend/controller/post_controler.py +++ b/dash-fastapi-backend/controller/post_controler.py @@ -23,9 +23,9 @@ async def get_system_post_select(query_db: Session = Depends(get_db)): @postController.post("/post/get", response_model=PostPageObjectResponse) -async def get_system_post_list(user_query: PostPageObject, query_db: Session = Depends(get_db)): +async def get_system_post_list(post_query: PostPageObject, query_db: Session = Depends(get_db)): try: - post_query_result = get_post_list_services(query_db, user_query) + post_query_result = get_post_list_services(query_db, post_query) logger.info('获取成功') return response_200(data=post_query_result, message="获取成功") except Exception as e: diff --git a/dash-fastapi-backend/controller/role_controller.py b/dash-fastapi-backend/controller/role_controller.py index 20e5cab..e1b4622 100644 --- a/dash-fastapi-backend/controller/role_controller.py +++ b/dash-fastapi-backend/controller/role_controller.py @@ -20,3 +20,78 @@ async def get_system_role_select(query_db: Session = Depends(get_db)): except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@roleController.post("/role/get", response_model=RolePageObjectResponse) +async def get_system_role_list(role_query: RolePageObject, query_db: Session = Depends(get_db)): + try: + role_query_result = get_role_list_services(query_db, role_query) + logger.info('获取成功') + return response_200(data=role_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@roleController.post("/role/add", response_model=CrudRoleResponse) +async def add_system_role(request: Request, add_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_role.create_by = current_user.user.user_name + add_role.update_by = current_user.user.user_name + add_role_result = add_role_services(query_db, add_role) + logger.info(add_role_result.message) + if add_role_result.is_success: + return response_200(data=add_role_result, message=add_role_result.message) + else: + return response_400(data="", message=add_role_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@roleController.post("/role/edit", response_model=CrudRoleResponse) +async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_role.update_by = current_user.user.user_name + edit_role.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_role_result = edit_role_services(query_db, edit_role) + if edit_role_result.is_success: + logger.info(edit_role_result.message) + return response_200(data=edit_role_result, message=edit_role_result.message) + else: + logger.warning(edit_role_result.message) + return response_400(data="", message=edit_role_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@roleController.post("/role/delete", response_model=CrudRoleResponse) +async def delete_system_role(request: Request, delete_role: DeleteRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + delete_role.update_by = current_user.user.user_name + delete_role.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + delete_role_result = delete_role_services(query_db, delete_role) + if delete_role_result.is_success: + logger.info(delete_role_result.message) + return response_200(data=delete_role_result, message=delete_role_result.message) + else: + logger.warning(delete_role_result.message) + return response_400(data="", message=delete_role_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@roleController.get("/role/{role_id}", response_model=RoleDetailModel) +async def query_detail_system_role(role_id: int, query_db: Session = Depends(get_db)): + try: + delete_role_result = detail_role_services(query_db, role_id) + logger.info(f'获取role_id为{role_id}的信息成功') + return response_200(data=delete_role_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/mapper/crud/role_crud.py b/dash-fastapi-backend/mapper/crud/role_crud.py index f17ce9d..fc9e32b 100644 --- a/dash-fastapi-backend/mapper/crud/role_crud.py +++ b/dash-fastapi-backend/mapper/crud/role_crud.py @@ -1,8 +1,25 @@ -from sqlalchemy import and_ +from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from entity.role_entity import SysRole -from utils.time_format_tool import list_format_datetime +from entity.role_entity import SysRole, SysRoleMenu +from entity.menu_entity import SysMenu +from mapper.schema.role_schema import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel +from utils.time_format_tool import list_format_datetime, object_format_datetime from utils.page_tool import get_page_info +from datetime import datetime, time + + +def get_role_by_name(db: Session, role_name: str): + """ + 根据角色名获取角色信息 + :param db: orm对象 + :param role_name: 角色名 + :return: 当前角色名的角色信息对象 + """ + query_role_info = db.query(SysRole) \ + .filter(SysRole.status == 0, SysRole.del_flag == 0, SysRole.role_name == role_name) \ + .order_by(desc(SysRole.create_time)).distinct().first() + + return query_role_info def get_role_by_id(db: Session, role_id: int): @@ -15,9 +32,156 @@ def get_role_by_id(db: Session, role_id: int): return role_info +def get_role_detail_by_id(db: Session, role_id: int): + """ + 根据role_id获取角色详细信息 + :param db: orm对象 + :param role_id: 角色id + :return: 当前role_id的角色信息对象 + """ + query_role_basic_info = db.query(SysRole) \ + .filter(SysRole.del_flag == 0, SysRole.role_id == role_id) \ + .distinct().first() + query_role_menu_info = db.query(SysMenu).select_from(SysRole) \ + .filter(SysRole.del_flag == 0, SysRole.role_id == role_id) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ + .distinct().all() + results = dict( + role=object_format_datetime(query_role_basic_info), + menu=list_format_datetime(query_role_menu_info), + ) + + return RoleDetailModel(**results) + + def get_role_select_option_crud(db: Session): role_info = db.query(SysRole) \ .filter(SysRole.status == 0, SysRole.del_flag == 0) \ .all() return role_info + + +def get_role_list(db: Session, page_object: RolePageObject): + """ + 根据查询参数获取角色列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 角色列表信息对象 + """ + count = db.query(SysRole) \ + .filter(SysRole.del_flag == 0, + SysRole.role_name.like(f'%{page_object.role_name}%') if page_object.role_name else True, + SysRole.role_key.like(f'%{page_object.role_key}%') if page_object.role_key else True, + SysRole.status == page_object.status if page_object.status else True, + SysRole.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True + )\ + .order_by(SysRole.role_sort)\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + role_list = db.query(SysRole) \ + .filter(SysRole.del_flag == 0, + SysRole.role_name.like(f'%{page_object.role_name}%') if page_object.role_name else True, + SysRole.role_key.like(f'%{page_object.role_key}%') if page_object.role_key else True, + SysRole.status == page_object.status if page_object.status else True, + SysRole.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True + ) \ + .order_by(SysRole.role_sort) \ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(role_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return RolePageObjectResponse(**result) + + +def add_role_crud(db: Session, role: RoleModel): + """ + 新增角色数据库操作 + :param db: orm对象 + :param role: 角色对象 + :return: 新增校验结果 + """ + db_role = SysRole(**role.dict()) + db.add(db_role) + db.commit() # 提交保存到数据库中 + db.refresh(db_role) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudRoleResponse(**result) + + +def edit_role_crud(db: Session, role: RoleModel): + """ + 编辑角色数据库操作 + :param db: orm对象 + :param role: 角色对象 + :return: 编辑校验结果 + """ + is_role_id = db.query(SysRole).filter(SysRole.role_id == role.role_id).all() + if not is_role_id: + result = dict(is_success=False, message='角色不存在') + else: + # 筛选出属性值为不为None和''的 + filtered_dict = {k: v for k, v in role.dict().items() if v is not None and v != ''} + db.query(SysRole) \ + .filter(SysRole.role_id == role.role_id) \ + .update(filtered_dict) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudRoleResponse(**result) + + +def delete_role_crud(db: Session, role: RoleModel): + """ + 删除角色数据库操作 + :param db: orm对象 + :param user: 角色对象 + :return: + """ + db.query(SysRole) \ + .filter(SysRole.role_id == role.role_id) \ + .update({SysRole.del_flag: '2', SysRole.update_by: role.update_by, SysRole.update_time: role.update_time}) + db.commit() # 提交保存到数据库中 + + +def add_role_menu_crud(db: Session, role_menu: RoleMenuModel): + """ + 新增角色菜单关联信息数据库操作 + :param db: orm对象 + :param role_menu: 用户角色菜单关联对象 + :return: + """ + db_role_menu = SysRoleMenu(**role_menu.dict()) + db.add(db_role_menu) + db.commit() # 提交保存到数据库中 + db.refresh(db_role_menu) # 刷新 + + +def delete_role_menu_crud(db: Session, role_menu: RoleMenuModel): + """ + 删除角色菜单关联信息数据库操作 + :param db: orm对象 + :param role_menu: 角色菜单关联对象 + :return: + """ + db.query(SysRoleMenu) \ + .filter(SysRoleMenu.role_id == role_menu.role_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/mapper/crud/user_crud.py index e8ab54a..3cef103 100644 --- a/dash-fastapi-backend/mapper/crud/user_crud.py +++ b/dash-fastapi-backend/mapper/crud/user_crud.py @@ -1,4 +1,4 @@ -from sqlalchemy import and_ +from sqlalchemy import and_, desc from sqlalchemy.orm import Session from entity.user_entity import SysUser, SysUserRole, SysUserPost from entity.role_entity import SysRole, SysRoleMenu @@ -21,7 +21,7 @@ def get_user_by_name(db: Session, user_name: str): """ query_user_info = db.query(SysUser) \ .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_name == user_name) \ - .distinct().first() + .order_by(desc(SysUser.create_time)).distinct().first() return query_user_info diff --git a/dash-fastapi-backend/mapper/schema/menu_schema.py b/dash-fastapi-backend/mapper/schema/menu_schema.py index b9453a5..c0aa5c2 100644 --- a/dash-fastapi-backend/mapper/schema/menu_schema.py +++ b/dash-fastapi-backend/mapper/schema/menu_schema.py @@ -3,6 +3,9 @@ from typing import Union, Optional, List class MenuModel(BaseModel): + """ + 菜单表对应pydantic模型 + """ menu_id: Optional[int] menu_name: Optional[str] parent_id: Optional[int] @@ -25,6 +28,13 @@ class MenuModel(BaseModel): class Config: orm_mode = True + + +class MenuTreeModel(MenuModel): + """ + 菜单树查询模型 + """ + type: Optional[str] class MenuPageObject(MenuModel): diff --git a/dash-fastapi-backend/mapper/schema/role_schema.py b/dash-fastapi-backend/mapper/schema/role_schema.py index 4a088eb..9782382 100644 --- a/dash-fastapi-backend/mapper/schema/role_schema.py +++ b/dash-fastapi-backend/mapper/schema/role_schema.py @@ -1,14 +1,39 @@ from pydantic import BaseModel from typing import Union, Optional, List from mapper.schema.user_schema import RoleModel +from mapper.schema.menu_schema import MenuModel + + +class RoleMenuModel(BaseModel): + """ + 角色和菜单关联表对应pydantic模型 + """ + role_id: Optional[int] + menu_id: Optional[int] + + class Config: + orm_mode = True class RolePageObject(RoleModel): """ 角色管理分页查询模型 """ + create_time_start: Optional[str] + create_time_end: Optional[str] page_num: Optional[int] page_size: Optional[int] + + +class RolePageObjectResponse(BaseModel): + """ + 角色管理列表分页查询返回模型 + """ + rows: List[Union[RoleModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool class RoleSelectOptionResponseModel(BaseModel): @@ -16,3 +41,35 @@ class RoleSelectOptionResponseModel(BaseModel): 角色管理不分页查询模型 """ role: List[Union[RoleModel, None]] + + +class CrudRoleResponse(BaseModel): + """ + 操作角色响应模型 + """ + is_success: bool + message: str + + +class AddRoleModel(RoleModel): + """ + 新增角色模型 + """ + menu_id: Optional[str] + + +class DeleteRoleModel(BaseModel): + """ + 删除角色模型 + """ + role_ids: str + update_by: Optional[str] + update_time: Optional[str] + + +class RoleDetailModel(BaseModel): + """ + 获取角色详情信息响应模型 + """ + role: Union[RoleModel, None] + menu: List[Union[MenuModel, None]] diff --git a/dash-fastapi-backend/mapper/schema/user_schema.py b/dash-fastapi-backend/mapper/schema/user_schema.py index b0dcfe3..8351db3 100644 --- a/dash-fastapi-backend/mapper/schema/user_schema.py +++ b/dash-fastapi-backend/mapper/schema/user_schema.py @@ -236,11 +236,11 @@ class RoleInfo(BaseModel): """ 用户角色信息 """ - role_info: Union[List] + role_info: Union[List, None] class MenuList(BaseModel): """ 用户菜单信息 """ - menu_info: Union[List] + menu_info: Union[List, None] diff --git a/dash-fastapi-backend/service/menu_service.py b/dash-fastapi-backend/service/menu_service.py index d678414..002e0ac 100644 --- a/dash-fastapi-backend/service/menu_service.py +++ b/dash-fastapi-backend/service/menu_service.py @@ -2,7 +2,7 @@ from mapper.schema.menu_schema import * from mapper.crud.menu_crud import * -def get_menu_tree_services(result_db: Session, page_object: MenuModel): +def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): """ 获取菜单树信息service :param result_db: orm对象 @@ -10,9 +10,12 @@ def get_menu_tree_services(result_db: Session, page_object: MenuModel): :return: 菜单树信息对象 """ menu_tree_option = [] - menu_list_result = get_menu_list_for_tree(result_db, page_object) + menu_list_result = get_menu_list_for_tree(result_db, MenuModel(**page_object.dict())) menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) - menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) + if page_object.type != 'role': + menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) + else: + menu_tree_option = [menu_tree_result, menu_list_result] return menu_tree_option diff --git a/dash-fastapi-backend/service/role_service.py b/dash-fastapi-backend/service/role_service.py index a442776..9b142ea 100644 --- a/dash-fastapi-backend/service/role_service.py +++ b/dash-fastapi-backend/service/role_service.py @@ -11,3 +11,87 @@ def get_role_select_option_services(result_db: Session): role_list_result = get_role_select_option_crud(result_db) return role_list_result + + +def get_role_list_services(result_db: Session, page_object: RolePageObject): + """ + 获取角色列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 角色列表信息对象 + """ + role_list_result = get_role_list(result_db, page_object) + + return role_list_result + + +def add_role_services(result_db: Session, page_object: AddRoleModel): + """ + 新增角色信息service + :param result_db: orm对象 + :param page_object: 新增角色对象 + :return: 新增角色校验结果 + """ + add_role = RoleModel(**page_object.dict()) + add_role_result = add_role_crud(result_db, add_role) + if add_role_result.is_success: + role_id = get_role_by_name(result_db, page_object.role_name).role_id + if page_object.menu_id: + menu_id_list = page_object.menu_id.split(',') + for menu in menu_id_list: + menu_dict = dict(role_id=role_id, menu_id=menu) + add_role_menu_crud(result_db, RoleMenuModel(**menu_dict)) + + return add_role_result + + +def edit_role_services(result_db: Session, page_object: AddRoleModel): + """ + 编辑角色信息service + :param result_db: orm对象 + :param page_object: 编辑角色对象 + :return: 编辑角色校验结果 + """ + edit_role = RoleModel(**page_object.dict()) + edit_role_result = edit_role_crud(result_db, edit_role) + if edit_role_result.is_success: + role_id_dict = dict(role_id=page_object.role_id) + delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) + if page_object.menu_id: + menu_id_list = page_object.menu_id.split(',') + for menu in menu_id_list: + menu_dict = dict(role_id=page_object.role_id, menu_id=menu) + add_role_menu_crud(result_db, RoleMenuModel(**menu_dict)) + + return edit_role_result + + +def delete_role_services(result_db: Session, page_object: DeleteRoleModel): + """ + 删除角色信息service + :param result_db: orm对象 + :param page_object: 删除角色对象 + :return: 删除角色校验结果 + """ + if page_object.role_ids.split(','): + role_id_list = page_object.role_ids.split(',') + for role_id in role_id_list: + role_id_dict = dict(role_id=role_id, update_by=page_object.update_by, update_time=page_object.update_time) + delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) + delete_role_crud(result_db, RoleModel(**role_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入角色id为空') + return CrudRoleResponse(**result) + + +def detail_role_services(result_db: Session, role_id: int): + """ + 获取角色详细信息service + :param result_db: orm对象 + :param role_id: 角色id + :return: 角色id对应的信息 + """ + role = get_role_detail_by_id(result_db, role_id=role_id) + + return role diff --git a/dash-fastapi-frontend/api/role.py b/dash-fastapi-frontend/api/role.py index 084edce..8c10178 100644 --- a/dash-fastapi-frontend/api/role.py +++ b/dash-fastapi-frontend/api/role.py @@ -4,3 +4,28 @@ from utils.request import api_request def get_role_select_option_api(): return api_request(method='post', url='/system/role/forSelectOption', is_headers=True) + + +def get_role_list_api(page_obj: dict): + + return api_request(method='post', url='/system/role/get', is_headers=True, json=page_obj) + + +def add_role_api(page_obj: dict): + + return api_request(method='post', url='/system/role/add', is_headers=True, json=page_obj) + + +def edit_role_api(page_obj: dict): + + return api_request(method='post', url='/system/role/edit', is_headers=True, json=page_obj) + + +def delete_role_api(page_obj: dict): + + return api_request(method='post', url='/system/role/delete', is_headers=True, json=page_obj) + + +def get_role_detail_api(role_id: int): + + return api_request(method='get', url=f'/system/role/{role_id}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py index a37b466..478adb8 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -138,7 +138,7 @@ def get_select_icon(icon): State('menu-list-table', 'recentlyButtonClickedRow')], prevent_initial_call=True ) -def add_menu_modal(add_click, button_click, clicked_content, recently_button_clicked_row): +def add_edit_menu_modal(add_click, button_click, clicked_content, recently_button_clicked_row): if add_click or button_click: menu_params = dict(menu_name='') if clicked_content == '修改': diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 444a478..12a297a 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -19,6 +19,15 @@ def render_store_container(): dcc.Store(id='user-edit-id-store'), # 用户管理模块删除操作行key存储容器 dcc.Store(id='user-delete-ids-store'), + # 角色管理模块操作类型存储容器 + dcc.Store(id='role-operations-store'), + dcc.Store(id='role-operations-store-bk'), + # 角色管理模块修改操作行key存储容器 + dcc.Store(id='role-edit-id-store'), + # 角色管理模块删除操作行key存储容器 + dcc.Store(id='role-delete-ids-store'), + # 角色管理模块菜单权限存储容器 + dcc.Store(id='role-menu-store'), # 菜单管理模块操作类型存储容器 dcc.Store(id='menu-operations-store'), dcc.Store(id='menu-operations-store-bk'), diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index 7ccec88..6e7d6b4 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -1,8 +1,493 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.system_c.role_c +from api.role import get_role_list_api + def render(): - return html.Div('我是角色管理') + role_params = dict(page_num=1, page_size=10) + table_info = get_role_list_api(role_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(checked=True) + else: + item['status'] = dict(checked=False) + item['key'] = str(item['role_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + + return [ + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='role-role_name-input', + placeholder='请输入角色名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 220 + } + ), + label='角色名称', + style={ 'paddingBottom': '10px' }, + ), + fac.AntdFormItem( + fac.AntdInput( + id='role-role_key-input', + placeholder='请输入权限字符', + autoComplete='off', + allowClear=True, + style={ + 'width': 220 + } + ), + label='权限字符', + style={ 'paddingBottom': '10px' }, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='role-status-select', + placeholder='角色状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 220 + } + ), + label='状态', + style={ 'paddingBottom': '10px' }, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='role-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间', + style={ 'paddingBottom': '10px' }, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='role-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={ 'paddingBottom': '10px' }, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='role-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={ 'paddingBottom': '10px' }, + ) + ], + layout='inline', + ) + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='role-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='role-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='role-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='role-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='role-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'role_id', + 'title': '角色编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'role_name', + 'title': '角色名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'role_key', + 'title': '权限字符', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'role_sort', + 'title': '显示顺序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'switch' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增和编辑角色表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='role-role_name', + placeholder='请输入角色名称', + allowClear=True, + style={ + 'width': 350 + } + ), + label='角色名称', + required=True, + id='role-role_name-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + fac.AntdInput( + id='role-role_Key', + placeholder='请输入权限字符', + allowClear=True, + style={ + 'width': 350 + } + ), + label=html.Div( + [ + fac.AntdTooltip( + fac.AntdIcon( + icon='antd-question-circle' + ), + title='控制器中定义的权限字符,如:common' + ), + fac.AntdText('权限字符') + ] + ), + required=True, + id='role-role_Key-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + fac.AntdInputNumber( + id='role-role_sort', + defaultValue=0, + min=0, + style={ + 'width': 350 + } + ), + label='角色顺序', + required=True, + id='role-role_sort-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='role-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + style={ + 'width': 350 + } + ), + label='状态', + id='role-status-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdCheckbox( + id='role-menu-perms-radio-fold-unfold', + label='展开/折叠' + ), + span=7, + ), + fac.AntdCol( + fac.AntdCheckbox( + id='role-menu-perms-radio-all-none', + label='全选/全不选' + ), + span=8, + ), + fac.AntdCol( + fac.AntdCheckbox( + id='role-menu-perms-radio-parent-children', + label='父子联动', + checked=True + ), + span=6, + ), + ], + style={ + 'paddingTop': '6px' + } + ), + fac.AntdRow( + fac.AntdCol( + html.Div( + [ + fac.AntdTree( + id='role-menu-perms', + treeData=[], + multiple=True, + checkable=True, + showLine=False, + selectable=False + ) + ], + style={ + 'border': 'solid 1px rgba(0, 0, 0, 0.2)', + 'border-radius': '5px', + 'width': 350 + } + ) + ), + style={ + 'paddingTop': '6px' + } + ), + ], + label='菜单权限', + id='role-menu-perms-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + fac.AntdFormItem( + fac.AntdInput( + id='role-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='role-remark-form-item', + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ), + ] + ) + ], + id='role-modal', + mask=False, + width=600, + renderFooter=True, + okClickClose=False + ), + + # 删除角色二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='role-delete-text'), + id='role-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From 292dfdb545ed23b71eb202a0c6c6bdf4f5e522c0 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Mon, 12 Jun 2023 17:55:18 +0800 Subject: [PATCH 011/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=88=A0=E6=9F=A5=E6=94=B9=E5=B7=B2=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/system_c/role_c.py | 411 ++++++++++++++++++ 1 file changed, 411 insertions(+) create mode 100644 dash-fastapi-frontend/callbacks/system_c/role_c.py diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py new file mode 100644 index 0000000..93d4ca8 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -0,0 +1,411 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.role import get_role_list_api, get_role_detail_api, add_role_api, edit_role_api, delete_role_api +from api.menu import get_menu_tree_api + + +@app.callback( + [Output('role-list-table', 'data', allow_duplicate=True), + Output('role-list-table', 'pagination', allow_duplicate=True), + Output('role-list-table', 'key'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('role-search', 'nClicks'), + Input('role-list-table', 'pagination'), + Input('role-operations-store', 'data')], + [State('role-role_name-input', 'value'), + State('role-role_key-input', 'value'), + State('role-status-select', 'value'), + State('role-create_time-range', 'value')], + prevent_initial_call=True +) +def get_role_table_data(search_click, pagination, operations, role_name, role_key, status_select, create_time_range): + + create_time_start = None + create_time_end = None + if create_time_range: + create_time_start = create_time_range[0] + create_time_end = create_time_range[1] + query_params = dict( + role_name=role_name, + role_key=role_key, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + role_name=role_name, + role_key=role_key, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_role_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(checked=True) + else: + item['status'] = dict(checked=False) + item['key'] = str(item['role_id']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + }, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + }, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 4 + + +@app.callback( + [Output('role-role_name-input', 'value'), + Output('role-role_key-input', 'value'), + Output('role-status-select', 'value'), + Output('role-create_time-range', 'value'), + Output('role-operations-store', 'data')], + Input('role-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_role_query_params(reset_click): + if reset_click: + return [None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('role-edit', 'disabled'), + Output('role-delete', 'disabled')], + Input('role-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_post_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return [True, True] + + +@app.callback( + Output('role-menu-perms', 'expandedKeys', allow_duplicate=True), + Input('role-menu-perms-radio-fold-unfold', 'checked'), + State('role-menu-store', 'data'), + prevent_initial_call=True +) +def fold_unfold_role_menu(fold_unfold, menu_info): + if menu_info: + default_expanded_keys = [] + for item in menu_info: + if item.get('parent_id') == 0: + default_expanded_keys.append(str(item.get('menu_id'))) + + if fold_unfold: + return default_expanded_keys + else: + return [] + + return dash.no_update + + +@app.callback( + Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), + Input('role-menu-perms-radio-all-none', 'checked'), + State('role-menu-store', 'data'), + prevent_initial_call=True +) +def all_none_role_menu_mode(all_none, menu_info): + if menu_info: + default_expanded_keys = [] + for item in menu_info: + if item.get('parent_id') == 0: + default_expanded_keys.append(str(item.get('menu_id'))) + + if all_none: + return [str(item.get('menu_id')) for item in menu_info] + else: + return [] + + return dash.no_update + + +@app.callback( + Output('role-menu-perms', 'checkStrictly'), + Input('role-menu-perms-radio-parent-children', 'checked'), + prevent_initial_call=True +) +def change_role_menu_mode(parent_children): + if parent_children: + return False + else: + return True + + +@app.callback( + [Output('role-modal', 'visible', allow_duplicate=True), + Output('role-modal', 'title'), + Output('role-role_name', 'value'), + Output('role-role_Key', 'value'), + Output('role-role_sort', 'value'), + Output('role-status', 'value'), + Output('role-menu-perms', 'treeData'), + Output('role-menu-perms', 'expandedKeys', allow_duplicate=True), + Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), + Output('role-menu-store', 'data'), + Output('role-remark', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('role-add', 'nClicks'), + Output('role-edit', 'nClicks'), + Output('role-edit-id-store', 'data'), + Output('role-operations-store-bk', 'data')], + [Input('role-add', 'nClicks'), + Input('role-edit', 'nClicks'), + Input('role-list-table', 'nClicksButton')], + [State('role-list-table', 'selectedRowKeys'), + State('role-list-table', 'clickedContent'), + State('role-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + if add_click or edit_click or button_click: + menu_params = dict(menu_name='', type='role') + tree_info = get_menu_tree_api(menu_params) + if tree_info.get('code') == 200: + tree_data = tree_info['data'] + if add_click: + return [ + True, + '新增角色', + None, + None, + None, + '0', + tree_data[0], + [], + None, + tree_data[1], + None, + {'timestamp': time.time()}, + None, + None, + None, + {'type': 'add'} + ] + elif edit_click or (button_click and clicked_content == '修改'): + if edit_click: + role_id = int(','.join(selected_row_keys)) + else: + role_id = int(recently_button_clicked_row['key']) + role_info_res = get_role_detail_api(role_id=role_id) + if role_info_res['code'] == 200: + role_info = role_info_res['data'] + checked_menu = [str(item.get('menu_id')) for item in role_info.get('menu') if item] or [] + return [ + True, + '编辑角色', + role_info.get('role').get('role_name'), + role_info.get('role').get('role_key'), + role_info.get('role').get('role_sort'), + role_info.get('role').get('status'), + tree_data[0], + [], + checked_menu, + tree_data[1], + role_info.get('role').get('remark'), + {'timestamp': time.time()}, + None, + None, + role_info.get('role') if role_info else None, + {'type': 'edit'} + ] + + return [dash.no_update] * 11 + [{'timestamp': time.time()}, None, None, None, None] + + return [dash.no_update] * 12 + [None, None, None, None] + + +@app.callback( + [Output('role-role_name-form-item', 'validateStatus'), + Output('role-role_Key-form-item', 'validateStatus'), + Output('role-role_sort-form-item', 'validateStatus'), + Output('role-role_name-form-item', 'help'), + Output('role-role_Key-form-item', 'help'), + Output('role-role_sort-form-item', 'help'), + Output('role-modal', 'visible'), + Output('role-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('role-modal', 'okCounts'), + [State('role-operations-store-bk', 'data'), + State('role-edit-id-store', 'data'), + State('role-role_name', 'value'), + State('role-role_Key', 'value'), + State('role-role_sort', 'value'), + State('role-status', 'value'), + State('role-menu-perms', 'checkedKeys'), + State('role-remark', 'value')], + prevent_initial_call=True +) +def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role_key, role_sort, status, menu_perms, remark): + if confirm_trigger: + if all([role_name, role_key, role_sort]): + params_add = dict(role_name=role_name, role_key=role_key, role_sort=role_sort, menu_id=','.join(menu_perms) if menu_perms else None, status=status, remark=remark) + params_edit = dict(role_id=cur_role_info.get('role_id') if cur_role_info else None, role_name=role_name, role_key=role_key, role_sort=role_sort, + menu_id=','.join(menu_perms) if menu_perms else '', status=status, remark=remark) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_role_api(params_add) + if operation_type == 'edit': + api_res = edit_role_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if role_name else 'error', + None if role_key else 'error', + None if role_sort else 'error', + None if role_name else '请输入角色名称!', + None if role_key else '请输入权限字符!', + None if role_sort else '请输入角色排序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('role-delete-text', 'children'), + Output('role-delete-confirm-modal', 'visible'), + Output('role-delete-ids-store', 'data')], + [Input('role-delete', 'nClicks'), + Input('role-list-table', 'nClicksButton')], + [State('role-list-table', 'selectedRowKeys'), + State('role-list-table', 'clickedContent'), + State('role-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def role_delete_modal(delete_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if delete_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'role-delete': + role_ids = ','.join(selected_row_keys) + else: + if clicked_content == '删除': + role_ids = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除角色编号为{role_ids}的用户?', + True, + {'role_ids': role_ids} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('role-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('role-delete-confirm-modal', 'okCounts'), + State('role-delete-ids-store', 'data'), + prevent_initial_call=True +) +def role_delete_confirm(delete_confirm, role_ids_data): + if delete_confirm: + + params = role_ids_data + delete_button_info = delete_role_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 -- Gitee From 9a6185eb25c2c326c6896da1f642acf9cc29315c Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Mon, 12 Jun 2023 22:02:23 +0800 Subject: [PATCH 012/169] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E5=92=8C=E8=A7=92=E8=89=B2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=BC=96=E8=BE=91=E5=BC=82=E5=B8=B8=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=85=B3=E8=81=94=E8=A1=A8=E5=86=85=E5=AE=B9=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/schema/role_schema.py | 1 + .../mapper/schema/user_schema.py | 1 + dash-fastapi-backend/service/role_service.py | 2 +- dash-fastapi-backend/service/user_service.py | 2 +- .../callbacks/system_c/role_c.py | 33 +++++++++++++++++++ .../callbacks/system_c/user_c.py | 4 +-- .../views/system/role/__init__.py | 1 + 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/dash-fastapi-backend/mapper/schema/role_schema.py b/dash-fastapi-backend/mapper/schema/role_schema.py index 9782382..003e0e4 100644 --- a/dash-fastapi-backend/mapper/schema/role_schema.py +++ b/dash-fastapi-backend/mapper/schema/role_schema.py @@ -56,6 +56,7 @@ class AddRoleModel(RoleModel): 新增角色模型 """ menu_id: Optional[str] + type: Optional[str] class DeleteRoleModel(BaseModel): diff --git a/dash-fastapi-backend/mapper/schema/user_schema.py b/dash-fastapi-backend/mapper/schema/user_schema.py index 8351db3..7c9c700 100644 --- a/dash-fastapi-backend/mapper/schema/user_schema.py +++ b/dash-fastapi-backend/mapper/schema/user_schema.py @@ -204,6 +204,7 @@ class AddUserModel(UserModel): """ role_id: Optional[str] post_id: Optional[str] + type: Optional[str] class DeleteUserModel(BaseModel): diff --git a/dash-fastapi-backend/service/role_service.py b/dash-fastapi-backend/service/role_service.py index 9b142ea..259a59e 100644 --- a/dash-fastapi-backend/service/role_service.py +++ b/dash-fastapi-backend/service/role_service.py @@ -54,7 +54,7 @@ def edit_role_services(result_db: Session, page_object: AddRoleModel): """ edit_role = RoleModel(**page_object.dict()) edit_role_result = edit_role_crud(result_db, edit_role) - if edit_role_result.is_success: + if edit_role_result.is_success and page_object.type != 'status': role_id_dict = dict(role_id=page_object.role_id) delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) if page_object.menu_id: diff --git a/dash-fastapi-backend/service/user_service.py b/dash-fastapi-backend/service/user_service.py index 8a46ead..f31542b 100644 --- a/dash-fastapi-backend/service/user_service.py +++ b/dash-fastapi-backend/service/user_service.py @@ -48,7 +48,7 @@ def edit_user_services(result_db: Session, page_object: AddUserModel): """ edit_user = UserModel(**page_object.dict()) edit_user_result = edit_user_crud(result_db, edit_user) - if edit_user_result.is_success: + if edit_user_result.is_success and page_object.type != 'status': user_id_dict = dict(user_id=page_object.user_id) delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 93d4ca8..db0e9f6 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -349,6 +349,39 @@ def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role return [dash.no_update] * 10 +@app.callback( + [Output('role-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + [Input('role-list-table', 'recentlySwitchDataIndex'), + Input('role-list-table', 'recentlySwitchStatus'), + Input('role-list-table', 'recentlySwitchRow')], + prevent_initial_call=True +) +def table_switch_role_status(recently_switch_data_index, recently_switch_status, recently_switch_row): + if recently_switch_data_index: + if recently_switch_status: + params = dict(role_id=int(recently_switch_row['key']), status='0', type='status') + else: + params = dict(role_id=int(recently_switch_row['key']), status='1', type='status') + edit_button_result = edit_role_api(params) + if edit_button_result['code'] == 200: + + return [ + {'type': 'switch-status'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error') + ] + + return [dash.no_update] * 3 + + @app.callback( [Output('role-delete-text', 'children'), Output('role-delete-confirm-modal', 'visible'), diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index ba75c82..ee733b0 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -408,9 +408,9 @@ def usr_edit_confirm(edit_confirm, nick_name, dept_id, phone_number, email, sex, def table_switch_user_status(recently_switch_data_index, recently_switch_status, recently_switch_row): if recently_switch_data_index: if recently_switch_status: - params = dict(user_id=int(recently_switch_row['key']), status='0') + params = dict(user_id=int(recently_switch_row['key']), status='0', type='status') else: - params = dict(user_id=int(recently_switch_row['key']), status='1') + params = dict(user_id=int(recently_switch_row['key']), status='1', type='status') edit_button_result = edit_user_api(params) if edit_button_result['code'] == 200: diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index 6e7d6b4..81d7c82 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -345,6 +345,7 @@ def render(): fac.AntdFormItem( fac.AntdInputNumber( id='role-role_sort', + placeholder='请输入角色顺序', defaultValue=0, min=0, style={ -- Gitee From 429186c19a2d38e5b1e88fdb3bb1f2f70fe829d6 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Tue, 13 Jun 2023 11:15:56 +0800 Subject: [PATCH 013/169] =?UTF-8?q?fix:=E6=9B=B4=E6=96=B0=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=BF=AE=E6=94=B9=E4=B8=BApatch=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BB=A5=E5=AE=9E=E7=8E=B0=E9=83=A8=E5=88=86=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/controller/dept_controller.py | 2 +- dash-fastapi-backend/controller/menu_controller.py | 2 +- dash-fastapi-backend/controller/post_controler.py | 2 +- dash-fastapi-backend/controller/role_controller.py | 2 +- dash-fastapi-backend/controller/user_controller.py | 2 +- dash-fastapi-backend/mapper/crud/dept_crud.py | 13 +++++-------- dash-fastapi-backend/mapper/crud/menu_crud.py | 12 +++++------- dash-fastapi-backend/mapper/crud/post_crud.py | 12 +++++------- dash-fastapi-backend/mapper/crud/role_crud.py | 12 +++++------- dash-fastapi-backend/mapper/crud/user_crud.py | 14 ++++++-------- dash-fastapi-backend/service/dept_service.py | 13 +++++++------ dash-fastapi-backend/service/menu_service.py | 3 ++- dash-fastapi-backend/service/post_service.py | 3 ++- dash-fastapi-backend/service/role_service.py | 6 +++++- dash-fastapi-backend/service/user_service.py | 7 ++++++- dash-fastapi-frontend/api/dept.py | 2 +- dash-fastapi-frontend/api/menu.py | 2 +- dash-fastapi-frontend/api/post.py | 2 +- dash-fastapi-frontend/api/role.py | 2 +- dash-fastapi-frontend/api/user.py | 2 +- dash-fastapi-frontend/utils/request.py | 3 +++ 21 files changed, 61 insertions(+), 57 deletions(-) diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/controller/dept_controller.py index d3e84e9..6835341 100644 --- a/dash-fastapi-backend/controller/dept_controller.py +++ b/dash-fastapi-backend/controller/dept_controller.py @@ -62,7 +62,7 @@ async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional return response_500(data="", message="接口异常") -@deptController.post("/dept/edit", response_model=CrudDeptResponse) +@deptController.patch("/dept/edit", response_model=CrudDeptResponse) async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/controller/menu_controller.py b/dash-fastapi-backend/controller/menu_controller.py index 7abb191..be0ca97 100644 --- a/dash-fastapi-backend/controller/menu_controller.py +++ b/dash-fastapi-backend/controller/menu_controller.py @@ -62,7 +62,7 @@ async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional return response_500(data="", message="接口异常") -@menuController.post("/menu/edit", response_model=CrudMenuResponse) +@menuController.patch("/menu/edit", response_model=CrudMenuResponse) async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/controller/post_controler.py b/dash-fastapi-backend/controller/post_controler.py index 59a0411..4103a88 100644 --- a/dash-fastapi-backend/controller/post_controler.py +++ b/dash-fastapi-backend/controller/post_controler.py @@ -50,7 +50,7 @@ async def add_system_post(request: Request, add_post: PostModel, token: Optional return response_500(data="", message="接口异常") -@postController.post("/post/edit", response_model=CrudPostResponse) +@postController.patch("/post/edit", response_model=CrudPostResponse) async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/controller/role_controller.py b/dash-fastapi-backend/controller/role_controller.py index e1b4622..3a3c295 100644 --- a/dash-fastapi-backend/controller/role_controller.py +++ b/dash-fastapi-backend/controller/role_controller.py @@ -50,7 +50,7 @@ async def add_system_role(request: Request, add_role: AddRoleModel, token: Optio return response_500(data="", message="接口异常") -@roleController.post("/role/edit", response_model=CrudRoleResponse) +@roleController.patch("/role/edit", response_model=CrudRoleResponse) async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/controller/user_controller.py b/dash-fastapi-backend/controller/user_controller.py index 0acecbb..1507719 100644 --- a/dash-fastapi-backend/controller/user_controller.py +++ b/dash-fastapi-backend/controller/user_controller.py @@ -41,7 +41,7 @@ async def add_system_user(request: Request, add_user: AddUserModel, token: Optio return response_500(data="", message="接口异常") -@userController.post("/user/edit", response_model=CrudUserResponse) +@userController.patch("/user/edit", response_model=CrudUserResponse) async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/mapper/crud/dept_crud.py index c676f78..1d5f8ac 100644 --- a/dash-fastapi-backend/mapper/crud/dept_crud.py +++ b/dash-fastapi-backend/mapper/crud/dept_crud.py @@ -144,23 +144,20 @@ def add_dept_crud(db: Session, dept: DeptModel): return CrudDeptResponse(**result) -def edit_dept_crud(db: Session, dept: DeptModel): +def edit_dept_crud(db: Session, dept: dict): """ 编辑部门数据库操作 :param db: orm对象 - :param dept: 部门对象 + :param dept: 需要更新的部门字典 :return: 编辑校验结果 """ - print(dept.dept_id) - is_dept_id = db.query(SysDept).filter(SysDept.dept_id == dept.dept_id).all() + is_dept_id = db.query(SysDept).filter(SysDept.dept_id == dept.get('dept_id')).all() if not is_dept_id: result = dict(is_success=False, message='部门不存在') else: - # 筛选出属性值为不为None和''的 - filtered_dict = {k: v for k, v in dept.dict().items() if v is not None and v != ''} db.query(SysDept) \ - .filter(SysDept.dept_id == dept.dept_id) \ - .update(filtered_dict) + .filter(SysDept.dept_id == dept.get('dept_id')) \ + .update(dept) db.commit() # 提交保存到数据库中 result = dict(is_success=True, message='更新成功') diff --git a/dash-fastapi-backend/mapper/crud/menu_crud.py b/dash-fastapi-backend/mapper/crud/menu_crud.py index 69bf3a6..62010c3 100644 --- a/dash-fastapi-backend/mapper/crud/menu_crud.py +++ b/dash-fastapi-backend/mapper/crud/menu_crud.py @@ -74,22 +74,20 @@ def add_menu_crud(db: Session, menu: MenuModel): return CrudMenuResponse(**result) -def edit_menu_crud(db: Session, menu: MenuModel): +def edit_menu_crud(db: Session, menu: dict): """ 编辑菜单数据库操作 :param db: orm对象 - :param menu: 菜单对象 + :param menu: 需要更新的菜单字典 :return: 编辑校验结果 """ - is_menu_id = db.query(SysMenu).filter(SysMenu.menu_id == menu.menu_id).all() + is_menu_id = db.query(SysMenu).filter(SysMenu.menu_id == menu.get('menu_id')).all() if not is_menu_id: result = dict(is_success=False, message='菜单不存在') else: - # 筛选出属性值为不为None和''的 - filtered_dict = {k: v for k, v in menu.dict().items() if v is not None and v != ''} db.query(SysMenu) \ - .filter(SysMenu.menu_id == menu.menu_id) \ - .update(filtered_dict) + .filter(SysMenu.menu_id == menu.get('menu_id')) \ + .update(menu) db.commit() # 提交保存到数据库中 result = dict(is_success=True, message='更新成功') diff --git a/dash-fastapi-backend/mapper/crud/post_crud.py b/dash-fastapi-backend/mapper/crud/post_crud.py index fc7bd63..bd9925b 100644 --- a/dash-fastapi-backend/mapper/crud/post_crud.py +++ b/dash-fastapi-backend/mapper/crud/post_crud.py @@ -84,22 +84,20 @@ def add_post_crud(db: Session, post: PostModel): return CrudPostResponse(**result) -def edit_post_crud(db: Session, post: PostModel): +def edit_post_crud(db: Session, post: dict): """ 编辑岗位数据库操作 :param db: orm对象 - :param post: 岗位对象 + :param post: 需要更新的岗位字典 :return: 编辑校验结果 """ - is_post_id = db.query(SysPost).filter(SysPost.post_id == post.post_id).all() + is_post_id = db.query(SysPost).filter(SysPost.post_id == post.get('post_id')).all() if not is_post_id: result = dict(is_success=False, message='岗位不存在') else: - # 筛选出属性值为不为None和''的 - filtered_dict = {k: v for k, v in post.dict().items() if v is not None and v != ''} db.query(SysPost) \ - .filter(SysPost.post_id == post.post_id) \ - .update(filtered_dict) + .filter(SysPost.post_id == post.get('post_id')) \ + .update(post) db.commit() # 提交保存到数据库中 result = dict(is_success=True, message='更新成功') diff --git a/dash-fastapi-backend/mapper/crud/role_crud.py b/dash-fastapi-backend/mapper/crud/role_crud.py index fc9e32b..318baf5 100644 --- a/dash-fastapi-backend/mapper/crud/role_crud.py +++ b/dash-fastapi-backend/mapper/crud/role_crud.py @@ -126,22 +126,20 @@ def add_role_crud(db: Session, role: RoleModel): return CrudRoleResponse(**result) -def edit_role_crud(db: Session, role: RoleModel): +def edit_role_crud(db: Session, role: dict): """ 编辑角色数据库操作 :param db: orm对象 - :param role: 角色对象 + :param role: 需要更新的角色字典 :return: 编辑校验结果 """ - is_role_id = db.query(SysRole).filter(SysRole.role_id == role.role_id).all() + is_role_id = db.query(SysRole).filter(SysRole.role_id == role.get('role_id')).all() if not is_role_id: result = dict(is_success=False, message='角色不存在') else: - # 筛选出属性值为不为None和''的 - filtered_dict = {k: v for k, v in role.dict().items() if v is not None and v != ''} db.query(SysRole) \ - .filter(SysRole.role_id == role.role_id) \ - .update(filtered_dict) + .filter(SysRole.role_id == role.get('role_id')) \ + .update(role) db.commit() # 提交保存到数据库中 result = dict(is_success=True, message='更新成功') diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/mapper/crud/user_crud.py index 3cef103..2367ed1 100644 --- a/dash-fastapi-backend/mapper/crud/user_crud.py +++ b/dash-fastapi-backend/mapper/crud/user_crud.py @@ -211,25 +211,23 @@ def add_user_crud(db: Session, user: UserModel): return CrudUserResponse(**result) -def edit_user_crud(db: Session, user: UserModel): +def edit_user_crud(db: Session, user: dict): """ 编辑用户数据库操作 :param db: orm对象 - :param user: 用户对象 + :param user: 需要更新的用户字典 :return: 编辑校验结果 """ - is_user_id = db.query(SysUser).filter(SysUser.user_id == user.user_id, SysUser.del_flag == 0).all() - is_user_name = db.query(SysUser).filter(SysUser.user_name == user.user_name, SysUser.del_flag == 0).all() + is_user_id = db.query(SysUser).filter(SysUser.user_id == user.get('user_id'), SysUser.del_flag == 0).all() + is_user_name = db.query(SysUser).filter(SysUser.user_name == user.get('user_name'), SysUser.del_flag == 0).all() if not is_user_id: result = dict(is_success=False, message='用户不存在') elif is_user_name: result = dict(is_success=False, message='用户名已存在,不允许修改') else: - # 筛选出属性值为不为None和''的 - filtered_dict = {k: v for k, v in user.dict().items() if v is not None and v != ''} db.query(SysUser) \ - .filter(SysUser.user_id == user.user_id) \ - .update(filtered_dict) + .filter(SysUser.user_id == user.get('user_id')) \ + .update(user) db.commit() # 提交保存到数据库中 result = dict(is_success=True, message='更新成功') diff --git a/dash-fastapi-backend/service/dept_service.py b/dash-fastapi-backend/service/dept_service.py index 75aa937..4344548 100644 --- a/dash-fastapi-backend/service/dept_service.py +++ b/dash-fastapi-backend/service/dept_service.py @@ -69,7 +69,8 @@ def edit_dept_services(result_db: Session, page_object: DeptModel): page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}' else: page_object.ancestors = '0' - edit_dept_result = edit_dept_crud(result_db, page_object) + edit_dept = page_object.dict(exclude_unset=True) + edit_dept_result = edit_dept_crud(result_db, edit_dept) update_children_info(result_db, DeptModel(dept_id=page_object.dept_id, ancestors=page_object.ancestors, update_by=page_object.update_by, @@ -155,11 +156,11 @@ def update_children_info(result_db, page_object): for child in children_info: child.ancestors = f'{page_object.ancestors},{page_object.dept_id}' edit_dept_crud(result_db, - DeptModel(dept_id=child.dept_id, - ancestors=child.ancestors, - update_by=page_object.update_by, - update_time=page_object.update_time - ) + dict(dept_id=child.dept_id, + ancestors=child.ancestors, + update_by=page_object.update_by, + update_time=page_object.update_time + ) ) update_children_info(result_db, DeptModel(dept_id=child.dept_id, ancestors=child.ancestors, diff --git a/dash-fastapi-backend/service/menu_service.py b/dash-fastapi-backend/service/menu_service.py index 002e0ac..a8ef2eb 100644 --- a/dash-fastapi-backend/service/menu_service.py +++ b/dash-fastapi-backend/service/menu_service.py @@ -66,7 +66,8 @@ def edit_menu_services(result_db: Session, page_object: MenuModel): :param page_object: 编辑部门对象 :return: 编辑菜单校验结果 """ - edit_menu_result = edit_menu_crud(result_db, page_object) + edit_menu = page_object.dict(exclude_unset=True) + edit_menu_result = edit_menu_crud(result_db, edit_menu) return edit_menu_result diff --git a/dash-fastapi-backend/service/post_service.py b/dash-fastapi-backend/service/post_service.py index e73f20b..2fe3766 100644 --- a/dash-fastapi-backend/service/post_service.py +++ b/dash-fastapi-backend/service/post_service.py @@ -44,7 +44,8 @@ def edit_post_services(result_db: Session, page_object: PostModel): :param page_object: 编辑岗位对象 :return: 编辑岗位校验结果 """ - edit_post_result = edit_post_crud(result_db, page_object) + edit_post = page_object.dict(exclude_unset=True) + edit_post_result = edit_post_crud(result_db, edit_post) return edit_post_result diff --git a/dash-fastapi-backend/service/role_service.py b/dash-fastapi-backend/service/role_service.py index 259a59e..230515a 100644 --- a/dash-fastapi-backend/service/role_service.py +++ b/dash-fastapi-backend/service/role_service.py @@ -52,7 +52,11 @@ def edit_role_services(result_db: Session, page_object: AddRoleModel): :param page_object: 编辑角色对象 :return: 编辑角色校验结果 """ - edit_role = RoleModel(**page_object.dict()) + edit_role = page_object.dict(exclude_unset=True) + if page_object.type != 'status': + del edit_role['menu_id'] + if page_object.type == 'status': + del edit_role['type'] edit_role_result = edit_role_crud(result_db, edit_role) if edit_role_result.is_success and page_object.type != 'status': role_id_dict = dict(role_id=page_object.role_id) diff --git a/dash-fastapi-backend/service/user_service.py b/dash-fastapi-backend/service/user_service.py index f31542b..8f7b0f1 100644 --- a/dash-fastapi-backend/service/user_service.py +++ b/dash-fastapi-backend/service/user_service.py @@ -46,7 +46,12 @@ def edit_user_services(result_db: Session, page_object: AddUserModel): :param page_object: 编辑用户对象 :return: 编辑用户校验结果 """ - edit_user = UserModel(**page_object.dict()) + edit_user = page_object.dict(exclude_unset=True) + if page_object.type != 'status': + del edit_user['role_id'] + del edit_user['post_id'] + if page_object.type == 'status': + del edit_user['type'] edit_user_result = edit_user_crud(result_db, edit_user) if edit_user_result.is_success and page_object.type != 'status': user_id_dict = dict(user_id=page_object.user_id) diff --git a/dash-fastapi-frontend/api/dept.py b/dash-fastapi-frontend/api/dept.py index b852f1d..00a3bcd 100644 --- a/dash-fastapi-frontend/api/dept.py +++ b/dash-fastapi-frontend/api/dept.py @@ -23,7 +23,7 @@ def add_dept_api(page_obj: dict): def edit_dept_api(page_obj: dict): - return api_request(method='post', url='/system/dept/edit', is_headers=True, json=page_obj) + return api_request(method='patch', url='/system/dept/edit', is_headers=True, json=page_obj) def delete_dept_api(page_obj: dict): diff --git a/dash-fastapi-frontend/api/menu.py b/dash-fastapi-frontend/api/menu.py index 7752cae..3581e57 100644 --- a/dash-fastapi-frontend/api/menu.py +++ b/dash-fastapi-frontend/api/menu.py @@ -23,7 +23,7 @@ def add_menu_api(page_obj: dict): def edit_menu_api(page_obj: dict): - return api_request(method='post', url='/system/menu/edit', is_headers=True, json=page_obj) + return api_request(method='patch', url='/system/menu/edit', is_headers=True, json=page_obj) def delete_menu_api(page_obj: dict): diff --git a/dash-fastapi-frontend/api/post.py b/dash-fastapi-frontend/api/post.py index 3a94c2a..42277c7 100644 --- a/dash-fastapi-frontend/api/post.py +++ b/dash-fastapi-frontend/api/post.py @@ -18,7 +18,7 @@ def add_post_api(page_obj: dict): def edit_post_api(page_obj: dict): - return api_request(method='post', url='/system/post/edit', is_headers=True, json=page_obj) + return api_request(method='patch', url='/system/post/edit', is_headers=True, json=page_obj) def delete_post_api(page_obj: dict): diff --git a/dash-fastapi-frontend/api/role.py b/dash-fastapi-frontend/api/role.py index 8c10178..9cafd61 100644 --- a/dash-fastapi-frontend/api/role.py +++ b/dash-fastapi-frontend/api/role.py @@ -18,7 +18,7 @@ def add_role_api(page_obj: dict): def edit_role_api(page_obj: dict): - return api_request(method='post', url='/system/role/edit', is_headers=True, json=page_obj) + return api_request(method='patch', url='/system/role/edit', is_headers=True, json=page_obj) def delete_role_api(page_obj: dict): diff --git a/dash-fastapi-frontend/api/user.py b/dash-fastapi-frontend/api/user.py index 181eca7..2edf88d 100644 --- a/dash-fastapi-frontend/api/user.py +++ b/dash-fastapi-frontend/api/user.py @@ -18,7 +18,7 @@ def add_user_api(page_obj: dict): def edit_user_api(page_obj: dict): - return api_request(method='post', url='/system/user/edit', is_headers=True, json=page_obj) + return api_request(method='patch', url='/system/user/edit', is_headers=True, json=page_obj) def delete_user_api(page_obj: dict): diff --git a/dash-fastapi-frontend/utils/request.py b/dash-fastapi-frontend/utils/request.py index c2c5f30..cfc6a66 100644 --- a/dash-fastapi-frontend/utils/request.py +++ b/dash-fastapi-frontend/utils/request.py @@ -25,6 +25,9 @@ def api_request(method: str, url: str, is_headers: bool, params: Optional[dict] elif method == 'put': response = requests.put(url=api_url, params=params, data=data, json=json, headers=api_headers, timeout=timeout) + elif method == 'patch': + response = requests.patch(url=api_url, params=params, data=data, json=json, headers=api_headers, + timeout=timeout) else: raise ValueError(f'Unsupported HTTP method: {method}') -- Gitee From 1c2f8127d4d7e8f39ced7e72ed2e0b712a71c37f Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 14 Jun 2023 14:40:17 +0800 Subject: [PATCH 014/169] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=8A=A0=E8=BD=BD=E5=BC=82=E5=B8=B8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/controller/dept_controller.py | 2 +- dash-fastapi-backend/controller/login_controller.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/controller/dept_controller.py index 6835341..82a88ff 100644 --- a/dash-fastapi-backend/controller/dept_controller.py +++ b/dash-fastapi-backend/controller/dept_controller.py @@ -99,7 +99,7 @@ async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, tok @deptController.get("/dept/{dept_id}", response_model=DeptModel) -async def query_detail_system_post(dept_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_dept(dept_id: int, query_db: Session = Depends(get_db)): try: detail_dept_result = detail_dept_services(query_db, dept_id) logger.info(f'获取dept_id为{dept_id}的信息成功') diff --git a/dash-fastapi-backend/controller/login_controller.py b/dash-fastapi-backend/controller/login_controller.py index 9f5c4da..7a3733e 100644 --- a/dash-fastapi-backend/controller/login_controller.py +++ b/dash-fastapi-backend/controller/login_controller.py @@ -58,8 +58,8 @@ async def get_login_user_info(request: Request, token: Optional[str] = Header(.. return response_500(data="", message="接口异常") -@loginController.post("/logout") -async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db), dependencies=[Depends(get_current_user)]): +@loginController.post("/logout", dependencies=[Depends(get_current_user)]) +async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) await logout_services(request, current_user) -- Gitee From 97e73e4927ddf184655b1dcdbe4efc8d12e593c6 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 14 Jun 2023 14:43:02 +0800 Subject: [PATCH 015/169] =?UTF-8?q?refactor:=E6=95=B4=E5=90=88=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E7=AE=A1=E7=90=86=E3=80=81=E5=B2=97=E4=BD=8D=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=A8=A1=E5=9D=97=E6=96=B0=E5=A2=9E=E5=92=8C=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=BC=B9=E7=AA=97=EF=BC=8C=E9=87=8D=E6=9E=84=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/system_c/dept_c.py | 300 ++++++-------- .../callbacks/system_c/menu_c/menu_c.py | 2 +- .../callbacks/system_c/post_c.py | 262 +++++------- .../callbacks/system_c/role_c.py | 4 +- dash-fastapi-frontend/store/store.py | 2 + .../views/system/dept/__init__.py | 378 ++++++------------ .../views/system/post/__init__.py | 136 +------ .../views/system/role/__init__.py | 2 +- 8 files changed, 390 insertions(+), 696 deletions(-) diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index bb4bf04..a9497f7 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -95,205 +95,161 @@ def reset_dept_query_params(reset_click): @app.callback( - [Output('dept-add-modal', 'visible', allow_duplicate=True), - Output('dept-add-parent_id', 'treeData'), - Output('dept-add-parent_id', 'value'), + [Output('dept-modal', 'visible', allow_duplicate=True), + Output('dept-modal', 'title'), + Output('dept-parent_id', 'treeData'), + Output('dept-parent_id', 'value'), + Output('dept-dept_name', 'value'), + Output('dept-order_num', 'value'), + Output('dept-leader', 'value'), + Output('dept-phone', 'value'), + Output('dept-email', 'value'), + Output('dept-status', 'value'), Output('api-check-token', 'data', allow_duplicate=True), - Output('dept-add', 'nClicks')], + Output('dept-add', 'nClicks'), + Output('dept-edit-id-store', 'data'), + Output('dept-operations-store-bk', 'data')], [Input('dept-add', 'nClicks'), Input('dept-list-table', 'nClicksButton')], [State('dept-list-table', 'clickedContent'), State('dept-list-table', 'recentlyButtonClickedRow')], prevent_initial_call=True ) -def add_user_modal(add_click, button_click, clicked_content, recently_button_clicked_row): - if add_click or (button_click and clicked_content == '新增'): +def add_edit_dept_modal(add_click, button_click, clicked_content, recently_button_clicked_row): + if add_click or (button_click and clicked_content != '删除'): dept_params = dict(dept_name='') - tree_info = get_dept_tree_api(dept_params) + if clicked_content == '修改': + tree_info = get_dept_tree_for_edit_option_api(dept_params) + else: + tree_info = get_dept_tree_api(dept_params) if tree_info['code'] == 200: tree_data = tree_info['data'] - return [ - True, - tree_data, - int(recently_button_clicked_row['key']) if recently_button_clicked_row else dash.no_update, - {'timestamp': time.time()}, - None - ] - - return [dash.no_update] * 3 + [{'timestamp': time.time()}, None] - - return [dash.no_update] * 4 + [None] - - -@app.callback( - [Output('dept-add-parent_id-form-item', 'validateStatus'), - Output('dept-add-dept_name-form-item', 'validateStatus'), - Output('dept-add-order_num-form-item', 'validateStatus'), - Output('dept-add-parent_id-form-item', 'help'), - Output('dept-add-dept_name-form-item', 'help'), - Output('dept-add-order_num-form-item', 'help'), - Output('dept-add-modal', 'visible', allow_duplicate=True), - Output('dept-operations-store', 'data', allow_duplicate=True), - Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('dept-add-modal', 'okCounts'), - [State('dept-add-parent_id', 'value'), - State('dept-add-dept_name', 'value'), - State('dept-add-order_num', 'value'), - State('dept-add-leader', 'value'), - State('dept-add-phone', 'value'), - State('dept-add-email', 'value'), - State('dept-add-status', 'value')], - prevent_initial_call=True -) -def dept_add_confirm(add_confirm, parent_id, dept_name, order_num, leader, phone, email, status): - if add_confirm: - - if all([parent_id, dept_name, order_num]): - params = dict(parent_id=parent_id, dept_name=dept_name, order_num=order_num, - leader=leader, phone=phone, email=email, status=status) - add_button_result = add_dept_api(params) - - if add_button_result['code'] == 200: + if add_click: return [ + True, + '新增部门', + tree_data, None, None, None, None, None, None, - False, - {'type': 'add'}, + '0', {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') + None, + None, + {'type': 'add'} ] + elif button_click and clicked_content == '新增': + return [ + True, + '新增部门', + tree_data, + str(recently_button_clicked_row['key']), + None, + None, + None, + None, + None, + '0', + {'timestamp': time.time()}, + None, + None, + {'type': 'add'} + ] + elif button_click and clicked_content == '修改': + dept_id = int(recently_button_clicked_row['key']) + dept_info_res = get_dept_detail_api(dept_id=dept_id) + if dept_info_res['code'] == 200: + dept_info = dept_info_res['data'] + return [ + True, + '编辑部门', + tree_data, + str(dept_info.get('parent_id')), + dept_info.get('dept_name'), + dept_info.get('order_num'), + dept_info.get('leader'), + dept_info.get('phone'), + dept_info.get('email'), + dept_info.get('status'), + {'timestamp': time.time()}, + None, + dept_info, + {'type': 'edit'} + ] - return [ - None, - None, - None, - None, - None, - None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增失败', type='error') - ] - - return [ - None if parent_id else 'error', - None if dept_name else 'error', - None if order_num else 'error', - None if parent_id else '请选择上级部门!', - None if dept_name else '请输入部门名称!', - None if order_num else '请输入显示排序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增失败', type='error') - ] - - return [dash.no_update] * 10 - - -@app.callback( - [Output('dept-edit-modal', 'visible', allow_duplicate=True), - Output('dept-edit-parent_id', 'treeData'), - Output('dept-edit-parent_id', 'value'), - Output('dept-edit-dept_name', 'value'), - Output('dept-edit-order_num', 'value'), - Output('dept-edit-leader', 'value'), - Output('dept-edit-phone', 'value'), - Output('dept-edit-email', 'value'), - Output('dept-edit-status', 'value'), - Output('dept-edit-id-store', 'data'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('dept-list-table', 'nClicksButton')], - [State('dept-list-table', 'clickedContent'), - State('dept-list-table', 'recentlyButtonClickedRow')], - prevent_initial_call=True -) -def dept_edit_modal(button_click, clicked_content, recently_button_clicked_row): - if button_click: - - if clicked_content == '修改': - dept_id = int(recently_button_clicked_row['key']) - else: - return dash.no_update - - dept_params = dict(dept_id=dept_id) - tree_info = get_dept_tree_for_edit_option_api(dept_params) - edit_button_info = get_dept_detail_api(dept_id) - if edit_button_info['code'] == 200 and tree_info['code'] == 200: - edit_button_result = edit_button_info['data'] - tree_data = tree_info['data'] - - return [ - True, - tree_data, - edit_button_result['parent_id'], - edit_button_result['dept_name'], - edit_button_result['order_num'], - edit_button_result['leader'], - edit_button_result['phone'], - edit_button_result['email'], - edit_button_result['status'], - {'dept_id': dept_id}, - {'timestamp': time.time()} - ] - - return [dash.no_update] * 10 + [{'timestamp': time.time()}] + return [dash.no_update] * 10 + [{'timestamp': time.time()}, None, None, None] - return [dash.no_update] * 11 + return [dash.no_update] * 11 + [None, None, None] @app.callback( - [Output('dept-edit-parent_id-form-item', 'validateStatus'), - Output('dept-edit-dept_name-form-item', 'validateStatus'), - Output('dept-edit-order_num-form-item', 'validateStatus'), - Output('dept-edit-parent_id-form-item', 'help'), - Output('dept-edit-dept_name-form-item', 'help'), - Output('dept-edit-order_num-form-item', 'help'), - Output('dept-edit-modal', 'visible', allow_duplicate=True), + [Output('dept-parent_id-form-item', 'validateStatus'), + Output('dept-dept_name-form-item', 'validateStatus'), + Output('dept-order_num-form-item', 'validateStatus'), + Output('dept-parent_id-form-item', 'help'), + Output('dept-dept_name-form-item', 'help'), + Output('dept-order_num-form-item', 'help'), + Output('dept-modal', 'visible'), Output('dept-operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], - Input('dept-edit-modal', 'okCounts'), - [State('dept-edit-parent_id', 'value'), - State('dept-edit-dept_name', 'value'), - State('dept-edit-order_num', 'value'), - State('dept-edit-leader', 'value'), - State('dept-edit-phone', 'value'), - State('dept-edit-email', 'value'), - State('dept-edit-status', 'value'), - State('dept-edit-id-store', 'data')], + Input('dept-modal', 'okCounts'), + [State('dept-operations-store-bk', 'data'), + State('dept-edit-id-store', 'data'), + State('dept-parent_id', 'value'), + State('dept-dept_name', 'value'), + State('dept-order_num', 'value'), + State('dept-leader', 'value'), + State('dept-phone', 'value'), + State('dept-email', 'value'), + State('dept-status', 'value')], prevent_initial_call=True ) -def dept_edit_confirm(edit_confirm, parent_id, dept_name, order_num, leader, phone, email, status, dept_id): - if edit_confirm: - +def dept_confirm(confirm_trigger, operation_type, cur_dept_info, parent_id, dept_name, order_num, leader, phone, email, status): + if confirm_trigger: if all([parent_id, dept_name, order_num]): - params = dict(dept_id=dept_id['dept_id'], parent_id=parent_id, dept_name=dept_name, - order_num=order_num, leader=leader, phone=phone, email=email, - status=status) - edit_button_result = edit_dept_api(params) - - if edit_button_result['code'] == 200: - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - + params_add = dict(parent_id=parent_id, dept_name=dept_name, order_num=order_num, leader=leader, phone=phone, + email=email, status=status) + params_edit = dict(dept_id=cur_dept_info.get('dept_id') if cur_dept_info else None, parent_id=parent_id, dept_name=dept_name, + order_num=order_num, leader=leader, phone=phone, email=email, status=status) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_dept_api(params_add) + if operation_type == 'edit': + api_res = edit_dept_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + return [ None, None, @@ -304,9 +260,9 @@ def dept_edit_confirm(edit_confirm, parent_id, dept_name, order_num, leader, pho dash.no_update, dash.no_update, {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑失败', type='error') + fuc.FefferyFancyMessage('处理失败', type='error') ] - + return [ None if parent_id else 'error', None if dept_name else 'error', @@ -317,8 +273,8 @@ def dept_edit_confirm(edit_confirm, parent_id, dept_name, order_num, leader, pho dash.no_update, dash.no_update, {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑失败', type='error') - ] + fuc.FefferyFancyMessage('处理失败', type='error') + ] return [dash.no_update] * 10 diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py index 478adb8..d68af1b 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -139,7 +139,7 @@ def get_select_icon(icon): prevent_initial_call=True ) def add_edit_menu_modal(add_click, button_click, clicked_content, recently_button_clicked_row): - if add_click or button_click: + if add_click or (button_click and clicked_content != '删除'): menu_params = dict(menu_name='') if clicked_content == '修改': tree_info = get_menu_tree_for_edit_option_api(menu_params) diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index 5be3460..b071aeb 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -110,178 +110,132 @@ def change_post_edit_delete_button_status(table_rows_selected): @app.callback( - Output('post-add-modal', 'visible', allow_duplicate=True), - Input('post-add', 'nClicks'), - prevent_initial_call=True -) -def add_post_modal(add_click): - if add_click: - - return True - - return dash.no_update - - -@app.callback( - [Output('post-add-post_name-form-item', 'validateStatus'), - Output('post-add-post_code-form-item', 'validateStatus'), - Output('post-add-post_sort-form-item', 'validateStatus'), - Output('post-add-post_name-form-item', 'help'), - Output('post-add-post_code-form-item', 'help'), - Output('post-add-post_sort-form-item', 'help'), - Output('post-add-modal', 'visible', allow_duplicate=True), - Output('post-operations-store', 'data', allow_duplicate=True), + [Output('post-modal', 'visible', allow_duplicate=True), + Output('post-modal', 'title'), + Output('post-post_name', 'value'), + Output('post-post_code', 'value'), + Output('post-post_sort', 'value'), + Output('post-status', 'value'), + Output('post-remark', 'value'), Output('api-check-token', 'data', allow_duplicate=True), - Output('global-message-container', 'children', allow_duplicate=True)], - Input('post-add-modal', 'okCounts'), - [State('post-add-post_name', 'value'), - State('post-add-post_code', 'value'), - State('post-add-post_sort', 'value'), - State('post-add-status', 'value'), - State('post-add-remark', 'value')], + Output('post-add', 'nClicks'), + Output('post-edit', 'nClicks'), + Output('post-edit-id-store', 'data'), + Output('post-operations-store-bk', 'data')], + [Input('post-add', 'nClicks'), + Input('post-edit', 'nClicks'), + Input('post-list-table', 'nClicksButton')], + [State('post-list-table', 'selectedRowKeys'), + State('post-list-table', 'clickedContent'), + State('post-list-table', 'recentlyButtonClickedRow')], prevent_initial_call=True ) -def post_add_confirm(add_confirm, post_name, post_code, post_sort, status, remark): - if add_confirm: - - if all([post_name, post_code, post_sort]): - params = dict(post_name=post_name, post_code=post_code, post_sort=post_sort, status=status, remark=remark) - add_button_result = add_post_api(params) - - if add_button_result['code'] == 200: - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'add'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增成功', type='success') - ] - +def add_edit_post_modal(add_click, edit_click, button_click, selected_row_keys, clicked_content, recently_button_clicked_row): + if add_click or edit_click or button_click: + if add_click: return [ + True, + '新增岗位', None, None, + 0, + '0', None, + {'timestamp': time.time()}, None, None, None, - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增失败', type='error') + {'type': 'add'} ] - - return [ - None if post_name else 'error', - None if post_code else 'error', - None if post_sort else 'error', - None if post_name else '请输入岗位名称!', - None if post_code else '请输入岗位编码!', - None if post_sort else '请输入岗位顺序!', - dash.no_update, - dash.no_update, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('新增失败', type='error') - ] - - return [dash.no_update] * 10 - - -@app.callback( - [Output('post-edit-modal', 'visible', allow_duplicate=True), - Output('post-edit-post_name', 'value'), - Output('post-edit-post_code', 'value'), - Output('post-edit-post_sort', 'value'), - Output('post-edit-status', 'value'), - Output('post-edit-remark', 'value'), - Output('post-edit-id-store', 'data'), - Output('api-check-token', 'data', allow_duplicate=True)], - [Input('post-edit', 'nClicks'), - Input('post-list-table', 'nClicksButton')], - [State('post-list-table', 'selectedRowKeys'), - State('post-list-table', 'clickedContent'), - State('post-list-table', 'recentlyButtonClickedRow')], - prevent_initial_call=True -) -def post_edit_modal(edit_click, button_click, - selected_row_keys, clicked_content, recently_button_clicked_row): - if edit_click or button_click: - trigger_id = dash.ctx.triggered_id - - if trigger_id == 'post-edit': - post_id = int(selected_row_keys[0]) - else: - if clicked_content == '修改': - post_id = int(recently_button_clicked_row['key']) + elif edit_click or (button_click and clicked_content == '修改'): + if edit_click: + post_id = int(','.join(selected_row_keys)) else: - return dash.no_update - - edit_button_info = get_post_detail_api(post_id) - if edit_button_info['code'] == 200: - edit_button_result = edit_button_info['data'] - - return [ - True, - edit_button_result['post_name'], - edit_button_result['post_code'], - edit_button_result['post_sort'], - edit_button_result['status'], - edit_button_result['remark'], - {'post_id': post_id}, - {'timestamp': time.time()} - ] - - return [dash.no_update] * 7 + [{'timestamp': time.time()}] + post_id = int(recently_button_clicked_row['key']) + post_info_res = get_post_detail_api(post_id=post_id) + if post_info_res['code'] == 200: + post_info = post_info_res['data'] + return [ + True, + '编辑岗位', + post_info.get('post_name'), + post_info.get('post_code'), + post_info.get('post_sort'), + post_info.get('status'), + post_info.get('remark'), + {'timestamp': time.time()}, + None, + None, + post_info if post_info else None, + {'type': 'edit'} + ] + + return [dash.no_update] * 7 + [{'timestamp': time.time()}, None, None, None, None] - return [dash.no_update] * 8 + return [dash.no_update] * 8 + [None, None, None, None] @app.callback( - [Output('post-edit-post_name-form-item', 'validateStatus'), - Output('post-edit-post_code-form-item', 'validateStatus'), - Output('post-edit-post_sort-form-item', 'validateStatus'), - Output('post-edit-post_name-form-item', 'help'), - Output('post-edit-post_code-form-item', 'help'), - Output('post-edit-post_sort-form-item', 'help'), - Output('post-edit-modal', 'visible', allow_duplicate=True), + [Output('post-post_name-form-item', 'validateStatus'), + Output('post-post_code-form-item', 'validateStatus'), + Output('post-post_sort-form-item', 'validateStatus'), + Output('post-post_name-form-item', 'help'), + Output('post-post_code-form-item', 'help'), + Output('post-post_sort-form-item', 'help'), + Output('post-modal', 'visible'), Output('post-operations-store', 'data', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], - Input('post-edit-modal', 'okCounts'), - [State('post-edit-post_name', 'value'), - State('post-edit-post_code', 'value'), - State('post-edit-post_sort', 'value'), - State('post-edit-status', 'value'), - State('post-edit-remark', 'value'), - State('post-edit-id-store', 'data')], + Input('post-modal', 'okCounts'), + [State('post-operations-store-bk', 'data'), + State('post-edit-id-store', 'data'), + State('post-post_name', 'value'), + State('post-post_code', 'value'), + State('post-post_sort', 'value'), + State('post-status', 'value'), + State('post-remark', 'value')], prevent_initial_call=True ) -def post_edit_confirm(edit_confirm, post_name, post_code, post_sort, status, remark, post_id): - if edit_confirm: - +def post_confirm(confirm_trigger, operation_type, cur_post_info, post_name, post_code, post_sort, status, remark): + if confirm_trigger: if all([post_name, post_code, post_sort]): - params = dict(post_id=post_id['post_id'], post_name=post_name, post_code=post_code, - post_sort=post_sort, status=status, remark=remark) - edit_button_result = edit_post_api(params) - - if edit_button_result['code'] == 200: - return [ - None, - None, - None, - None, - None, - None, - False, - {'type': 'edit'}, - {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑成功', type='success') - ] - + params_add = dict(post_name=post_name, post_code=post_code, post_sort=post_sort, status=status, remark=remark) + params_edit = dict(post_id=cur_post_info.get('post_id') if cur_post_info else None, post_name=post_name, + post_code=post_code, post_sort=post_sort, status=status, remark=remark) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_post_api(params_add) + if operation_type == 'edit': + api_res = edit_post_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + return [ None, None, @@ -292,9 +246,9 @@ def post_edit_confirm(edit_confirm, post_name, post_code, post_sort, status, rem dash.no_update, dash.no_update, {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑失败', type='error') + fuc.FefferyFancyMessage('处理失败', type='error') ] - + return [ None if post_name else 'error', None if post_code else 'error', @@ -305,8 +259,8 @@ def post_edit_confirm(edit_confirm, post_name, post_code, post_sort, status, rem dash.no_update, dash.no_update, {'timestamp': time.time()}, - fuc.FefferyFancyMessage('编辑失败', type='error') - ] + fuc.FefferyFancyMessage('处理失败', type='error') + ] return [dash.no_update] * 10 diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index db0e9f6..f11b72f 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -179,7 +179,7 @@ def change_role_menu_mode(parent_children): [Output('role-modal', 'visible', allow_duplicate=True), Output('role-modal', 'title'), Output('role-role_name', 'value'), - Output('role-role_Key', 'value'), + Output('role-role_key', 'value'), Output('role-role_sort', 'value'), Output('role-status', 'value'), Output('role-menu-perms', 'treeData'), @@ -273,7 +273,7 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, [State('role-operations-store-bk', 'data'), State('role-edit-id-store', 'data'), State('role-role_name', 'value'), - State('role-role_Key', 'value'), + State('role-role_key', 'value'), State('role-role_sort', 'value'), State('role-status', 'value'), State('role-menu-perms', 'checkedKeys'), diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 12a297a..d04f47e 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -43,12 +43,14 @@ def render_store_container(): dcc.Store(id='menu-delete-ids-store'), # 部门管理模块操作类型存储容器 dcc.Store(id='dept-operations-store'), + dcc.Store(id='dept-operations-store-bk'), # 部门管理模块修改操作行key存储容器 dcc.Store(id='dept-edit-id-store'), # 部门管理模块删除操作行key存储容器 dcc.Store(id='dept-delete-ids-store'), # 岗位管理模块操作类型存储容器 dcc.Store(id='post-operations-store'), + dcc.Store(id='post-operations-store-bk'), # 岗位管理模块修改操作行key存储容器 dcc.Store(id='post-edit-id-store'), # 岗位管理模块删除操作行key存储容器 diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index a88f973..07160fe 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -231,288 +231,166 @@ def render(): gutter=5 ), - # 新增部门表单modal + # 新增和编辑部门表单modal fac.AntdModal( [ fac.AntdForm( [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdTreeSelect( - id='dept-add-parent_id', - placeholder='请选择上级部门', - treeData=[], - treeNodeFilterProp='title', - style={ - 'width': 500 - } - ), - label='上级部门', - required=True, - id='dept-add-parent_id-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-dept_name', - placeholder='请输入部门名称', - allowClear=True, - style={ - 'width': 200 - } - ), - label='部门名称', - required=True, - id='dept-add-dept_name-form-item', - ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='dept-add-order_num', - min=0, - style={ - 'width': 200 - } - ), - label='显示顺序', - required=True, - id='dept-add-order_num-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( + fac.AntdRow( [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-leader', - placeholder='请输入负责人', - allowClear=True, - style={ - 'width': 200 - } - ), - label='负责人', - id='dept-add-leader-form-item', - labelCol={ - 'offset': 2 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-phone', - placeholder='请输入联系电话', - allowClear=True, - style={ - 'width': 200 + fac.AntdCol( + fac.AntdFormItem( + fac.AntdTreeSelect( + id='dept-parent_id', + placeholder='请选择上级部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': '100%' + } + ), + label='上级部门', + required=True, + id='dept-parent_id-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 } ), - label='联系电话', - id='dept-add-phone-form-item', - labelCol={ - 'offset': 3 - }, + span=24 ), - ], - size="middle" + ] ), - fac.AntdSpace( + fac.AntdRow( [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-add-email', - placeholder='请输入邮箱', - allowClear=True, - style={ - 'width': 200 - } + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dept-dept_name', + placeholder='请输入部门名称', + allowClear=True, + style={ + 'width': '100%' + } + ), + label='部门名称', + required=True, + id='dept-dept_name-form-item' ), - label='邮箱', - id='dept-add-email-form-item', - labelCol={ - 'offset': 3 - }, + span=12 ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='dept-add-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 200 - } + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInputNumber( + id='dept-order_num', + min=0, + style={ + 'width': '100%' + } + ), + label='显示顺序', + required=True, + id='dept-order_num-form-item' ), - label='部门状态', - id='dept-add-status-form-item', - labelCol={ - 'offset': 4 - }, + span=12 ) ], - size="middle" + gutter=5 ), - ] - ) - ], - id='dept-add-modal', - title='新增部门', - mask=False, - width=650, - renderFooter=True, - okClickClose=False - ), - - # 编辑部门表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdTreeSelect( - id='dept-edit-parent_id', - placeholder='请选择上级部门', - treeData=[], - treeNodeFilterProp='title', - style={ - 'width': 510 - } - ), - label='上级部门', - required=True, - id='dept-edit-parent_id-form-item', - ), - ], - size="middle" - ), - fac.AntdSpace( + fac.AntdRow( [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-dept_name', - placeholder='请输入部门名称', - allowClear=True, - style={ - 'width': 200 - } + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dept-leader', + placeholder='请输入负责人', + allowClear=True, + style={ + 'width': '100%' + } + ), + label='负责人', + id='dept-leader-form-item' ), - label='部门名称', - required=True, - id='dept-edit-dept_name-form-item', + span=12 ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='dept-edit-order_num', - min=0, - style={ - 'width': 200 - } + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dept-phone', + placeholder='请输入联系电话', + allowClear=True, + style={ + 'width': '100%' + } + ), + label='联系电话', + id='dept-phone-form-item' ), - label='显示顺序', - required=True, - id='dept-edit-order_num-form-item', + span=12 ), ], - size="middle" + gutter=5 ), - fac.AntdSpace( + fac.AntdRow( [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-leader', - placeholder='请输入负责人', - allowClear=True, - style={ - 'width': 200 - } - ), - label='负责人', - id='dept-edit-leader-form-item', - labelCol={ - 'offset': 2 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-phone', - placeholder='请输入联系电话', - allowClear=True, - style={ - 'width': 200 - } + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dept-email', + placeholder='请输入邮箱', + allowClear=True, + style={ + 'width': '100%' + } + ), + label='邮箱', + id='dept-email-form-item' ), - label='联系电话', - id='dept-edit-phone-form-item', - labelCol={ - 'offset': 3 - }, + span=12 ), - ], - size="middle" - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-edit-email', - placeholder='请输入邮箱', - allowClear=True, - style={ - 'width': 200 - } + fac.AntdCol( + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dept-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': '100%' + } + ), + label='部门状态', + id='dept-status-form-item' ), - label='邮箱', - id='dept-edit-email-form-item', - labelCol={ - 'offset': 3 - }, + span=12 ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='dept-edit-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 200 - } - ), - label='部门状态', - id='dept-edit-status-form-item', - labelCol={ - 'offset': 4 - }, - ) ], - size="middle" + gutter=5 ), - ] + ], + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + }, + style={ + 'marginRight': '15px' + } ) ], - id='dept-edit-modal', - title='编辑部门', + id='dept-modal', mask=False, width=650, renderFooter=True, diff --git a/dash-fastapi-frontend/views/system/post/__init__.py b/dash-fastapi-frontend/views/system/post/__init__.py index 7896d72..69dd206 100644 --- a/dash-fastapi-frontend/views/system/post/__init__.py +++ b/dash-fastapi-frontend/views/system/post/__init__.py @@ -281,14 +281,14 @@ def render(): gutter=5 ), - # 新增岗位表单modal + # 新增和编辑岗位表单modal fac.AntdModal( [ fac.AntdForm( [ fac.AntdFormItem( fac.AntdInput( - id='post-add-post_name', + id='post-post_name', placeholder='请输入岗位名称', allowClear=True, style={ @@ -297,11 +297,11 @@ def render(): ), label='岗位名称', required=True, - id='post-add-post_name-form-item' + id='post-post_name-form-item' ), fac.AntdFormItem( fac.AntdInput( - id='post-add-post_code', + id='post-post_code', placeholder='请输入岗位编码', allowClear=True, style={ @@ -310,11 +310,11 @@ def render(): ), label='岗位编码', required=True, - id='post-add-post_code-form-item', + id='post-post_code-form-item' ), fac.AntdFormItem( fac.AntdInputNumber( - id='post-add-post_sort', + id='post-post_sort', defaultValue=0, min=0, style={ @@ -323,11 +323,11 @@ def render(): ), label='岗位顺序', required=True, - id='post-add-post_sort-form-item', + id='post-post_sort-form-item' ), fac.AntdFormItem( fac.AntdRadioGroup( - id='post-add-status', + id='post-status', options=[ { 'label': '正常', @@ -344,14 +344,11 @@ def render(): } ), label='岗位状态', - id='post-add-status-form-item', - labelCol={ - 'offset': 1 - }, + id='post-status-form-item' ), fac.AntdFormItem( fac.AntdInput( - id='post-add-remark', + id='post-remark', placeholder='请输入内容', allowClear=True, mode='text-area', @@ -360,113 +357,20 @@ def render(): } ), label='备注', - id='post-add-remark-form-item', - labelCol={ - 'offset': 2 - }, + id='post-remark-form-item' ), - ] - ) - ], - id='post-add-modal', - title='新增岗位', - mask=False, - width=480, - renderFooter=True, - okClickClose=False - ), - - # 编辑岗位表单modal - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='post-edit-post_name', - placeholder='请输入岗位名称', - allowClear=True, - style={ - 'width': 350 - } - ), - label='岗位名称', - required=True, - id='post-edit-post_name-form-item' - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-edit-post_code', - placeholder='请输入岗位编码', - allowClear=True, - style={ - 'width': 350 - } - ), - label='岗位编码', - required=True, - id='post-edit-post_code-form-item', - ), - fac.AntdFormItem( - fac.AntdInputNumber( - id='post-edit-post_sort', - defaultValue=0, - min=0, - style={ - 'width': 350 - } - ), - label='岗位顺序', - required=True, - id='post-edit-post_sort-form-item', - ), - fac.AntdFormItem( - fac.AntdRadioGroup( - id='post-edit-status', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - }, - ], - defaultValue='0', - style={ - 'width': 350 - } - ), - label='岗位状态', - id='post-edit-status-form-item', - labelCol={ - 'offset': 1 - }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-edit-remark', - placeholder='请输入内容', - allowClear=True, - mode='text-area', - style={ - 'width': 350 - } - ), - label='备注', - id='post-edit-remark-form-item', - labelCol={ - 'offset': 2 - }, - ), - ] + ], + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } ) ], - id='post-edit-modal', - title='编辑岗位', + id='post-modal', mask=False, - width=480, + width=580, renderFooter=True, okClickClose=False ), diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index 81d7c82..ec7d1f9 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -315,7 +315,7 @@ def render(): ), fac.AntdFormItem( fac.AntdInput( - id='role-role_Key', + id='role-role_key', placeholder='请输入权限字符', allowClear=True, style={ -- Gitee From fa3b440a92c1f5d8bea469254dbb1c41be55730b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=85=89=E9=93=AD?= Date: Tue, 20 Jun 2023 16:20:05 +0800 Subject: [PATCH 016/169] =?UTF-8?q?refactor:=E9=A1=B9=E7=9B=AE=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 14 +++++++------- .../controller/dept_controller.py | 14 +++++++------- .../controller/login_controller.py | 16 +++++++--------- .../controller/menu_controller.py | 14 +++++++------- .../controller/post_controler.py | 12 ++++++------ .../controller/role_controller.py | 12 ++++++------ .../controller/user_controller.py | 14 +++++++------- .../entity/do}/dept_entity.py | 0 .../entity/do}/dict_entity.py | 0 .../entity/do}/log_entity.py | 0 .../entity/do}/menu_entity.py | 0 .../entity/do}/post_entity.py | 0 .../entity/do}/role_entity.py | 0 .../entity/do}/user_entity.py | 0 .../entity/vo}/dept_schema.py | 2 +- .../entity/vo}/login_schema.py | 0 .../entity/vo}/menu_schema.py | 0 .../entity/vo}/post_schema.py | 2 +- .../entity/vo}/role_schema.py | 4 ++-- .../entity/vo}/user_schema.py | 0 .../crud => module_admin/mapper}/dept_crud.py | 8 +++----- .../crud => module_admin/mapper}/login_crud.py | 4 ++-- .../crud => module_admin/mapper}/menu_crud.py | 8 +++----- .../crud => module_admin/mapper}/post_crud.py | 9 ++++----- .../crud => module_admin/mapper}/role_crud.py | 10 +++++----- .../crud => module_admin/mapper}/user_crud.py | 18 +++++++++--------- .../{ => module_admin}/service/dept_service.py | 4 ++-- .../service/login_service.py | 10 +++++----- .../{ => module_admin}/service/menu_service.py | 4 ++-- .../{ => module_admin}/service/post_service.py | 4 ++-- .../{ => module_admin}/service/role_service.py | 4 ++-- .../{ => module_admin}/service/user_service.py | 4 ++-- .../{ => module_admin}/utils/log_tool.py | 0 .../{ => module_admin}/utils/page_tool.py | 0 .../{ => module_admin}/utils/response_tool.py | 0 .../utils/time_format_tool.py | 0 36 files changed, 92 insertions(+), 99 deletions(-) rename dash-fastapi-backend/{ => module_admin}/controller/dept_controller.py (93%) rename dash-fastapi-backend/{ => module_admin}/controller/login_controller.py (88%) rename dash-fastapi-backend/{ => module_admin}/controller/menu_controller.py (93%) rename dash-fastapi-backend/{ => module_admin}/controller/post_controler.py (93%) rename dash-fastapi-backend/{ => module_admin}/controller/role_controller.py (93%) rename dash-fastapi-backend/{ => module_admin}/controller/user_controller.py (91%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/dept_entity.py (100%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/dict_entity.py (100%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/log_entity.py (100%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/menu_entity.py (100%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/post_entity.py (100%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/role_entity.py (100%) rename dash-fastapi-backend/{entity => module_admin/entity/do}/user_entity.py (100%) rename dash-fastapi-backend/{mapper/schema => module_admin/entity/vo}/dept_schema.py (94%) rename dash-fastapi-backend/{mapper/schema => module_admin/entity/vo}/login_schema.py (100%) rename dash-fastapi-backend/{mapper/schema => module_admin/entity/vo}/menu_schema.py (100%) rename dash-fastapi-backend/{mapper/schema => module_admin/entity/vo}/post_schema.py (93%) rename dash-fastapi-backend/{mapper/schema => module_admin/entity/vo}/role_schema.py (92%) rename dash-fastapi-backend/{mapper/schema => module_admin/entity/vo}/user_schema.py (100%) rename dash-fastapi-backend/{mapper/crud => module_admin/mapper}/dept_crud.py (95%) rename dash-fastapi-backend/{mapper/crud => module_admin/mapper}/login_crud.py (76%) rename dash-fastapi-backend/{mapper/crud => module_admin/mapper}/menu_crud.py (92%) rename dash-fastapi-backend/{mapper/crud => module_admin/mapper}/post_crud.py (92%) rename dash-fastapi-backend/{mapper/crud => module_admin/mapper}/role_crud.py (93%) rename dash-fastapi-backend/{mapper/crud => module_admin/mapper}/user_crud.py (95%) rename dash-fastapi-backend/{ => module_admin}/service/dept_service.py (98%) rename dash-fastapi-backend/{ => module_admin}/service/login_service.py (97%) rename dash-fastapi-backend/{ => module_admin}/service/menu_service.py (97%) rename dash-fastapi-backend/{ => module_admin}/service/post_service.py (96%) rename dash-fastapi-backend/{ => module_admin}/service/role_service.py (97%) rename dash-fastapi-backend/{ => module_admin}/service/user_service.py (97%) rename dash-fastapi-backend/{ => module_admin}/utils/log_tool.py (100%) rename dash-fastapi-backend/{ => module_admin}/utils/page_tool.py (100%) rename dash-fastapi-backend/{ => module_admin}/utils/response_tool.py (100%) rename dash-fastapi-backend/{ => module_admin}/utils/time_format_tool.py (100%) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 064c55c..2592bb3 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -5,14 +5,14 @@ from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware -from controller.login_controller import loginController -from controller.user_controller import userController -from controller.menu_controller import menuController -from controller.dept_controller import deptController -from controller.role_controller import roleController -from controller.post_controler import postController +from module_admin.controller.login_controller import loginController +from module_admin.controller.user_controller import userController +from module_admin.controller.menu_controller import menuController +from module_admin.controller.dept_controller import deptController +from module_admin.controller.role_controller import roleController +from module_admin.controller.post_controler import postController from config.env import RedisConfig -from utils.response_tool import response_401, AuthException +from module_admin.utils.response_tool import response_401, AuthException app = FastAPI() diff --git a/dash-fastapi-backend/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py similarity index 93% rename from dash-fastapi-backend/controller/dept_controller.py rename to dash-fastapi-backend/module_admin/controller/dept_controller.py index 82a88ff..69c87a5 100644 --- a/dash-fastapi-backend/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -1,12 +1,12 @@ from fastapi import APIRouter, Request -from fastapi import Depends, HTTPException, Header +from fastapi import Depends, Header from config.get_db import get_db -from service.login_service import get_current_user, get_password_hash -from service.dept_service import * -from mapper.schema.dept_schema import * -from mapper.crud.dept_crud import * -from utils.response_tool import * -from utils.log_tool import * +from module_admin.service.login_service import get_current_user +from module_admin.service.dept_service import * +from module_admin.entity.vo.dept_schema import * +from module_admin.mapper.dept_crud import * +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * deptController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py similarity index 88% rename from dash-fastapi-backend/controller/login_controller.py rename to dash-fastapi-backend/module_admin/controller/login_controller.py index 7a3733e..c934add 100644 --- a/dash-fastapi-backend/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -1,14 +1,12 @@ import uuid -from fastapi import APIRouter, Request -from fastapi import Depends, HTTPException, Header -from config.get_db import get_db -from service.login_service import * -from mapper.schema.login_schema import * -from mapper.crud.login_crud import * +from fastapi import APIRouter +from module_admin.service.login_service import * +from module_admin.entity.vo.login_schema import * +from module_admin.mapper.login_crud import * from config.env import JwtConfig -from utils.response_tool import * -from utils.log_tool import * -from datetime import datetime, timedelta +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * +from datetime import timedelta loginController = APIRouter() diff --git a/dash-fastapi-backend/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py similarity index 93% rename from dash-fastapi-backend/controller/menu_controller.py rename to dash-fastapi-backend/module_admin/controller/menu_controller.py index be0ca97..70e273a 100644 --- a/dash-fastapi-backend/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -1,12 +1,12 @@ from fastapi import APIRouter, Request -from fastapi import Depends, HTTPException, Header +from fastapi import Depends, Header from config.get_db import get_db -from service.login_service import get_current_user, get_password_hash -from service.menu_service import * -from mapper.schema.menu_schema import * -from mapper.crud.menu_crud import * -from utils.response_tool import * -from utils.log_tool import * +from module_admin.service.login_service import get_current_user +from module_admin.service.menu_service import * +from module_admin.entity.vo.menu_schema import * +from module_admin.mapper.menu_crud import * +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * menuController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py similarity index 93% rename from dash-fastapi-backend/controller/post_controler.py rename to dash-fastapi-backend/module_admin/controller/post_controler.py index 4103a88..c6d9614 100644 --- a/dash-fastapi-backend/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -1,11 +1,11 @@ from fastapi import APIRouter, Request -from fastapi import Depends, HTTPException, Header +from fastapi import Depends, Header from config.get_db import get_db -from service.login_service import get_current_user, get_password_hash -from service.post_service import * -from mapper.schema.post_schema import * -from utils.response_tool import * -from utils.log_tool import * +from module_admin.service.login_service import get_current_user +from module_admin.service.post_service import * +from module_admin.entity.vo.post_schema import * +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * postController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py similarity index 93% rename from dash-fastapi-backend/controller/role_controller.py rename to dash-fastapi-backend/module_admin/controller/role_controller.py index 3a3c295..7d53d83 100644 --- a/dash-fastapi-backend/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -1,11 +1,11 @@ from fastapi import APIRouter, Request -from fastapi import Depends, HTTPException, Header +from fastapi import Depends, Header from config.get_db import get_db -from service.login_service import get_current_user, get_password_hash -from service.role_service import * -from mapper.schema.role_schema import * -from utils.response_tool import * -from utils.log_tool import * +from module_admin.service.login_service import get_current_user +from module_admin.service.role_service import * +from module_admin.entity.vo.role_schema import * +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * roleController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py similarity index 91% rename from dash-fastapi-backend/controller/user_controller.py rename to dash-fastapi-backend/module_admin/controller/user_controller.py index 1507719..dbaf254 100644 --- a/dash-fastapi-backend/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -1,12 +1,12 @@ from fastapi import APIRouter, Request -from fastapi import Depends, HTTPException, Header +from fastapi import Depends, Header from config.get_db import get_db -from service.login_service import get_current_user, get_password_hash -from service.user_service import * -from mapper.schema.user_schema import * -from mapper.crud.user_crud import * -from utils.response_tool import * -from utils.log_tool import * +from module_admin.service.login_service import get_current_user, get_password_hash +from module_admin.service.user_service import * +from module_admin.entity.vo.user_schema import * +from module_admin.mapper.user_crud import * +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * userController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/entity/dept_entity.py b/dash-fastapi-backend/module_admin/entity/do/dept_entity.py similarity index 100% rename from dash-fastapi-backend/entity/dept_entity.py rename to dash-fastapi-backend/module_admin/entity/do/dept_entity.py diff --git a/dash-fastapi-backend/entity/dict_entity.py b/dash-fastapi-backend/module_admin/entity/do/dict_entity.py similarity index 100% rename from dash-fastapi-backend/entity/dict_entity.py rename to dash-fastapi-backend/module_admin/entity/do/dict_entity.py diff --git a/dash-fastapi-backend/entity/log_entity.py b/dash-fastapi-backend/module_admin/entity/do/log_entity.py similarity index 100% rename from dash-fastapi-backend/entity/log_entity.py rename to dash-fastapi-backend/module_admin/entity/do/log_entity.py diff --git a/dash-fastapi-backend/entity/menu_entity.py b/dash-fastapi-backend/module_admin/entity/do/menu_entity.py similarity index 100% rename from dash-fastapi-backend/entity/menu_entity.py rename to dash-fastapi-backend/module_admin/entity/do/menu_entity.py diff --git a/dash-fastapi-backend/entity/post_entity.py b/dash-fastapi-backend/module_admin/entity/do/post_entity.py similarity index 100% rename from dash-fastapi-backend/entity/post_entity.py rename to dash-fastapi-backend/module_admin/entity/do/post_entity.py diff --git a/dash-fastapi-backend/entity/role_entity.py b/dash-fastapi-backend/module_admin/entity/do/role_entity.py similarity index 100% rename from dash-fastapi-backend/entity/role_entity.py rename to dash-fastapi-backend/module_admin/entity/do/role_entity.py diff --git a/dash-fastapi-backend/entity/user_entity.py b/dash-fastapi-backend/module_admin/entity/do/user_entity.py similarity index 100% rename from dash-fastapi-backend/entity/user_entity.py rename to dash-fastapi-backend/module_admin/entity/do/user_entity.py diff --git a/dash-fastapi-backend/mapper/schema/dept_schema.py b/dash-fastapi-backend/module_admin/entity/vo/dept_schema.py similarity index 94% rename from dash-fastapi-backend/mapper/schema/dept_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/dept_schema.py index bb3e622..2a72eb0 100644 --- a/dash-fastapi-backend/mapper/schema/dept_schema.py +++ b/dash-fastapi-backend/module_admin/entity/vo/dept_schema.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import Union, Optional, List -from mapper.schema.user_schema import DeptModel +from module_admin.entity.vo.user_schema import DeptModel class DeptPageObject(DeptModel): diff --git a/dash-fastapi-backend/mapper/schema/login_schema.py b/dash-fastapi-backend/module_admin/entity/vo/login_schema.py similarity index 100% rename from dash-fastapi-backend/mapper/schema/login_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/login_schema.py diff --git a/dash-fastapi-backend/mapper/schema/menu_schema.py b/dash-fastapi-backend/module_admin/entity/vo/menu_schema.py similarity index 100% rename from dash-fastapi-backend/mapper/schema/menu_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/menu_schema.py diff --git a/dash-fastapi-backend/mapper/schema/post_schema.py b/dash-fastapi-backend/module_admin/entity/vo/post_schema.py similarity index 93% rename from dash-fastapi-backend/mapper/schema/post_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/post_schema.py index f036242..c2edd89 100644 --- a/dash-fastapi-backend/mapper/schema/post_schema.py +++ b/dash-fastapi-backend/module_admin/entity/vo/post_schema.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import Union, Optional, List -from mapper.schema.user_schema import PostModel +from module_admin.entity.vo.user_schema import PostModel class PostPageObject(PostModel): diff --git a/dash-fastapi-backend/mapper/schema/role_schema.py b/dash-fastapi-backend/module_admin/entity/vo/role_schema.py similarity index 92% rename from dash-fastapi-backend/mapper/schema/role_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/role_schema.py index 003e0e4..68a7d9e 100644 --- a/dash-fastapi-backend/mapper/schema/role_schema.py +++ b/dash-fastapi-backend/module_admin/entity/vo/role_schema.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from typing import Union, Optional, List -from mapper.schema.user_schema import RoleModel -from mapper.schema.menu_schema import MenuModel +from module_admin.entity.vo.user_schema import RoleModel +from module_admin.entity.vo.menu_schema import MenuModel class RoleMenuModel(BaseModel): diff --git a/dash-fastapi-backend/mapper/schema/user_schema.py b/dash-fastapi-backend/module_admin/entity/vo/user_schema.py similarity index 100% rename from dash-fastapi-backend/mapper/schema/user_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/user_schema.py diff --git a/dash-fastapi-backend/mapper/crud/dept_crud.py b/dash-fastapi-backend/module_admin/mapper/dept_crud.py similarity index 95% rename from dash-fastapi-backend/mapper/crud/dept_crud.py rename to dash-fastapi-backend/module_admin/mapper/dept_crud.py index 1d5f8ac..e2ce754 100644 --- a/dash-fastapi-backend/mapper/crud/dept_crud.py +++ b/dash-fastapi-backend/module_admin/mapper/dept_crud.py @@ -1,9 +1,7 @@ -from sqlalchemy import and_, or_ from sqlalchemy.orm import Session -from entity.dept_entity import SysDept -from mapper.schema.dept_schema import DeptModel, DeptResponse, CrudDeptResponse -from utils.time_format_tool import list_format_datetime -from utils.page_tool import get_page_info +from module_admin.entity.do.dept_entity import SysDept +from module_admin.entity.vo.dept_schema import DeptModel, DeptResponse, CrudDeptResponse +from module_admin.utils.time_format_tool import list_format_datetime def get_dept_by_id(db: Session, dept_id: int): diff --git a/dash-fastapi-backend/mapper/crud/login_crud.py b/dash-fastapi-backend/module_admin/mapper/login_crud.py similarity index 76% rename from dash-fastapi-backend/mapper/crud/login_crud.py rename to dash-fastapi-backend/module_admin/mapper/login_crud.py index f7ce5d8..0e39974 100644 --- a/dash-fastapi-backend/mapper/crud/login_crud.py +++ b/dash-fastapi-backend/module_admin/mapper/login_crud.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from entity.user_entity import SysUser -from utils.time_format_tool import object_format_datetime +from module_admin.entity.do.user_entity import SysUser +from module_admin.utils.time_format_tool import object_format_datetime def login_by_account(db: Session, user_name: str): diff --git a/dash-fastapi-backend/mapper/crud/menu_crud.py b/dash-fastapi-backend/module_admin/mapper/menu_crud.py similarity index 92% rename from dash-fastapi-backend/mapper/crud/menu_crud.py rename to dash-fastapi-backend/module_admin/mapper/menu_crud.py index 62010c3..50749e8 100644 --- a/dash-fastapi-backend/mapper/crud/menu_crud.py +++ b/dash-fastapi-backend/module_admin/mapper/menu_crud.py @@ -1,9 +1,7 @@ -from sqlalchemy import and_, or_ from sqlalchemy.orm import Session -from entity.menu_entity import SysMenu -from mapper.schema.menu_schema import MenuModel, MenuResponse, CrudMenuResponse -from utils.time_format_tool import list_format_datetime -from utils.page_tool import get_page_info +from module_admin.entity.do.menu_entity import SysMenu +from module_admin.entity.vo.menu_schema import MenuModel, MenuResponse, CrudMenuResponse +from module_admin.utils.time_format_tool import list_format_datetime def get_menu_detail_by_id(db: Session, menu_id: int): diff --git a/dash-fastapi-backend/mapper/crud/post_crud.py b/dash-fastapi-backend/module_admin/mapper/post_crud.py similarity index 92% rename from dash-fastapi-backend/mapper/crud/post_crud.py rename to dash-fastapi-backend/module_admin/mapper/post_crud.py index bd9925b..8b04c4f 100644 --- a/dash-fastapi-backend/mapper/crud/post_crud.py +++ b/dash-fastapi-backend/module_admin/mapper/post_crud.py @@ -1,9 +1,8 @@ -from sqlalchemy import and_ from sqlalchemy.orm import Session -from entity.post_entity import SysPost -from mapper.schema.post_schema import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse -from utils.time_format_tool import list_format_datetime -from utils.page_tool import get_page_info +from module_admin.entity.do.post_entity import SysPost +from module_admin.entity.vo.post_schema import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse +from module_admin.utils.time_format_tool import list_format_datetime +from module_admin.utils.page_tool import get_page_info def get_post_by_id(db: Session, post_id: int): diff --git a/dash-fastapi-backend/mapper/crud/role_crud.py b/dash-fastapi-backend/module_admin/mapper/role_crud.py similarity index 93% rename from dash-fastapi-backend/mapper/crud/role_crud.py rename to dash-fastapi-backend/module_admin/mapper/role_crud.py index 318baf5..6923dd5 100644 --- a/dash-fastapi-backend/mapper/crud/role_crud.py +++ b/dash-fastapi-backend/module_admin/mapper/role_crud.py @@ -1,10 +1,10 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from entity.role_entity import SysRole, SysRoleMenu -from entity.menu_entity import SysMenu -from mapper.schema.role_schema import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel -from utils.time_format_tool import list_format_datetime, object_format_datetime -from utils.page_tool import get_page_info +from module_admin.entity.do.role_entity import SysRole, SysRoleMenu +from module_admin.entity.do.menu_entity import SysMenu +from module_admin.entity.vo.role_schema import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel +from module_admin.utils.time_format_tool import list_format_datetime, object_format_datetime +from module_admin.utils.page_tool import get_page_info from datetime import datetime, time diff --git a/dash-fastapi-backend/mapper/crud/user_crud.py b/dash-fastapi-backend/module_admin/mapper/user_crud.py similarity index 95% rename from dash-fastapi-backend/mapper/crud/user_crud.py rename to dash-fastapi-backend/module_admin/mapper/user_crud.py index 2367ed1..81da538 100644 --- a/dash-fastapi-backend/mapper/crud/user_crud.py +++ b/dash-fastapi-backend/module_admin/mapper/user_crud.py @@ -1,14 +1,14 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from entity.user_entity import SysUser, SysUserRole, SysUserPost -from entity.role_entity import SysRole, SysRoleMenu -from entity.dept_entity import SysDept -from entity.post_entity import SysPost -from entity.menu_entity import SysMenu -from mapper.schema.user_schema import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ - UserPageObjectResponse, CrudUserResponse, UserInfoJoinDept -from utils.time_format_tool import list_format_datetime, format_datetime_dict_list -from utils.page_tool import get_page_info +from module_admin.entity.do.user_entity import SysUser, SysUserRole, SysUserPost +from module_admin.entity.do.role_entity import SysRole, SysRoleMenu +from module_admin.entity.do.dept_entity import SysDept +from module_admin.entity.do.post_entity import SysPost +from module_admin.entity.do.menu_entity import SysMenu +from module_admin.entity.vo.user_schema import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ + UserPageObjectResponse, CrudUserResponse +from module_admin.utils.time_format_tool import list_format_datetime, format_datetime_dict_list +from module_admin.utils.page_tool import get_page_info from datetime import datetime, time diff --git a/dash-fastapi-backend/service/dept_service.py b/dash-fastapi-backend/module_admin/service/dept_service.py similarity index 98% rename from dash-fastapi-backend/service/dept_service.py rename to dash-fastapi-backend/module_admin/service/dept_service.py index 4344548..c86ad40 100644 --- a/dash-fastapi-backend/service/dept_service.py +++ b/dash-fastapi-backend/module_admin/service/dept_service.py @@ -1,5 +1,5 @@ -from mapper.schema.dept_schema import * -from mapper.crud.dept_crud import * +from module_admin.entity.vo.dept_schema import * +from module_admin.mapper.dept_crud import * def get_dept_tree_services(result_db: Session, page_object: DeptModel): diff --git a/dash-fastapi-backend/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py similarity index 97% rename from dash-fastapi-backend/service/login_service.py rename to dash-fastapi-backend/module_admin/service/login_service.py index de2fc91..29deaef 100644 --- a/dash-fastapi-backend/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -1,11 +1,11 @@ -from mapper.schema.user_schema import * -from mapper.crud.login_crud import * -from mapper.crud.user_crud import * +from module_admin.entity.vo.user_schema import * +from module_admin.mapper.login_crud import * +from module_admin.mapper.user_crud import * from jose import JWTError, jwt from passlib.context import CryptContext from config.env import JwtConfig -from utils.response_tool import * -from utils.log_tool import * +from module_admin.utils.response_tool import * +from module_admin.utils.log_tool import * from datetime import datetime, timedelta from fastapi import Request from fastapi import Depends, Header diff --git a/dash-fastapi-backend/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py similarity index 97% rename from dash-fastapi-backend/service/menu_service.py rename to dash-fastapi-backend/module_admin/service/menu_service.py index a8ef2eb..f3465ec 100644 --- a/dash-fastapi-backend/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -1,5 +1,5 @@ -from mapper.schema.menu_schema import * -from mapper.crud.menu_crud import * +from module_admin.entity.vo.menu_schema import * +from module_admin.mapper.menu_crud import * def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): diff --git a/dash-fastapi-backend/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py similarity index 96% rename from dash-fastapi-backend/service/post_service.py rename to dash-fastapi-backend/module_admin/service/post_service.py index 2fe3766..6151ed8 100644 --- a/dash-fastapi-backend/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -1,5 +1,5 @@ -from mapper.schema.post_schema import * -from mapper.crud.post_crud import * +from module_admin.entity.vo.post_schema import * +from module_admin.mapper.post_crud import * def get_post_select_option_services(result_db: Session): diff --git a/dash-fastapi-backend/service/role_service.py b/dash-fastapi-backend/module_admin/service/role_service.py similarity index 97% rename from dash-fastapi-backend/service/role_service.py rename to dash-fastapi-backend/module_admin/service/role_service.py index 230515a..ff0b1e9 100644 --- a/dash-fastapi-backend/service/role_service.py +++ b/dash-fastapi-backend/module_admin/service/role_service.py @@ -1,5 +1,5 @@ -from mapper.schema.role_schema import * -from mapper.crud.role_crud import * +from module_admin.entity.vo.role_schema import * +from module_admin.mapper.role_crud import * def get_role_select_option_services(result_db: Session): diff --git a/dash-fastapi-backend/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py similarity index 97% rename from dash-fastapi-backend/service/user_service.py rename to dash-fastapi-backend/module_admin/service/user_service.py index 8f7b0f1..909f5b3 100644 --- a/dash-fastapi-backend/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -1,5 +1,5 @@ -from mapper.schema.user_schema import * -from mapper.crud.user_crud import * +from module_admin.entity.vo.user_schema import * +from module_admin.mapper.user_crud import * def get_user_list_services(result_db: Session, page_object: UserPageObject): diff --git a/dash-fastapi-backend/utils/log_tool.py b/dash-fastapi-backend/module_admin/utils/log_tool.py similarity index 100% rename from dash-fastapi-backend/utils/log_tool.py rename to dash-fastapi-backend/module_admin/utils/log_tool.py diff --git a/dash-fastapi-backend/utils/page_tool.py b/dash-fastapi-backend/module_admin/utils/page_tool.py similarity index 100% rename from dash-fastapi-backend/utils/page_tool.py rename to dash-fastapi-backend/module_admin/utils/page_tool.py diff --git a/dash-fastapi-backend/utils/response_tool.py b/dash-fastapi-backend/module_admin/utils/response_tool.py similarity index 100% rename from dash-fastapi-backend/utils/response_tool.py rename to dash-fastapi-backend/module_admin/utils/response_tool.py diff --git a/dash-fastapi-backend/utils/time_format_tool.py b/dash-fastapi-backend/module_admin/utils/time_format_tool.py similarity index 100% rename from dash-fastapi-backend/utils/time_format_tool.py rename to dash-fastapi-backend/module_admin/utils/time_format_tool.py -- Gitee From 418b0ee0ae5e76d9555a91ec371e90d1cd8ff729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=85=89=E9=93=AD?= Date: Tue, 20 Jun 2023 16:54:04 +0800 Subject: [PATCH 017/169] =?UTF-8?q?refactor:=E6=96=87=E4=BB=B6=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 +- .../module_admin/{mapper/dept_crud.py => dao/dept_dao.py} | 0 .../module_admin/{mapper/login_crud.py => dao/login_dao.py} | 0 .../module_admin/{mapper/menu_crud.py => dao/menu_dao.py} | 0 .../module_admin/{mapper/post_crud.py => dao/post_dao.py} | 0 .../module_admin/{mapper/role_crud.py => dao/role_dao.py} | 0 .../module_admin/{mapper/user_crud.py => dao/user_dao.py} | 0 .../module_admin/entity/do/{dept_entity.py => dept_do.py} | 0 .../module_admin/entity/do/{dict_entity.py => dict_do.py} | 0 .../module_admin/entity/do/{log_entity.py => log_do.py} | 0 .../module_admin/entity/do/{menu_entity.py => menu_do.py} | 0 .../module_admin/entity/do/{post_entity.py => post_do.py} | 0 .../module_admin/entity/do/{role_entity.py => role_do.py} | 0 .../module_admin/entity/do/{user_entity.py => user_do.py} | 0 .../module_admin/entity/vo/{dept_schema.py => dept_vo.py} | 0 .../module_admin/entity/vo/{login_schema.py => login_vo.py} | 0 .../module_admin/entity/vo/{menu_schema.py => menu_vo.py} | 0 .../module_admin/entity/vo/{post_schema.py => post_vo.py} | 0 .../module_admin/entity/vo/{role_schema.py => role_vo.py} | 0 .../module_admin/entity/vo/{user_schema.py => user_vo.py} | 0 .../module_admin/utils/{log_tool.py => log_util.py} | 0 .../module_admin/utils/{page_tool.py => page_util.py} | 0 .../module_admin/utils/{response_tool.py => response_util.py} | 0 .../utils/{time_format_tool.py => time_format_util.py} | 0 24 files changed, 1 insertion(+), 1 deletion(-) rename dash-fastapi-backend/module_admin/{mapper/dept_crud.py => dao/dept_dao.py} (100%) rename dash-fastapi-backend/module_admin/{mapper/login_crud.py => dao/login_dao.py} (100%) rename dash-fastapi-backend/module_admin/{mapper/menu_crud.py => dao/menu_dao.py} (100%) rename dash-fastapi-backend/module_admin/{mapper/post_crud.py => dao/post_dao.py} (100%) rename dash-fastapi-backend/module_admin/{mapper/role_crud.py => dao/role_dao.py} (100%) rename dash-fastapi-backend/module_admin/{mapper/user_crud.py => dao/user_dao.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{dept_entity.py => dept_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{dict_entity.py => dict_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{log_entity.py => log_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{menu_entity.py => menu_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{post_entity.py => post_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{role_entity.py => role_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/do/{user_entity.py => user_do.py} (100%) rename dash-fastapi-backend/module_admin/entity/vo/{dept_schema.py => dept_vo.py} (100%) rename dash-fastapi-backend/module_admin/entity/vo/{login_schema.py => login_vo.py} (100%) rename dash-fastapi-backend/module_admin/entity/vo/{menu_schema.py => menu_vo.py} (100%) rename dash-fastapi-backend/module_admin/entity/vo/{post_schema.py => post_vo.py} (100%) rename dash-fastapi-backend/module_admin/entity/vo/{role_schema.py => role_vo.py} (100%) rename dash-fastapi-backend/module_admin/entity/vo/{user_schema.py => user_vo.py} (100%) rename dash-fastapi-backend/module_admin/utils/{log_tool.py => log_util.py} (100%) rename dash-fastapi-backend/module_admin/utils/{page_tool.py => page_util.py} (100%) rename dash-fastapi-backend/module_admin/utils/{response_tool.py => response_util.py} (100%) rename dash-fastapi-backend/module_admin/utils/{time_format_tool.py => time_format_util.py} (100%) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 2592bb3..9ceb84f 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -12,7 +12,7 @@ from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from config.env import RedisConfig -from module_admin.utils.response_tool import response_401, AuthException +from module_admin.utils.response_util import response_401, AuthException app = FastAPI() diff --git a/dash-fastapi-backend/module_admin/mapper/dept_crud.py b/dash-fastapi-backend/module_admin/dao/dept_dao.py similarity index 100% rename from dash-fastapi-backend/module_admin/mapper/dept_crud.py rename to dash-fastapi-backend/module_admin/dao/dept_dao.py diff --git a/dash-fastapi-backend/module_admin/mapper/login_crud.py b/dash-fastapi-backend/module_admin/dao/login_dao.py similarity index 100% rename from dash-fastapi-backend/module_admin/mapper/login_crud.py rename to dash-fastapi-backend/module_admin/dao/login_dao.py diff --git a/dash-fastapi-backend/module_admin/mapper/menu_crud.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py similarity index 100% rename from dash-fastapi-backend/module_admin/mapper/menu_crud.py rename to dash-fastapi-backend/module_admin/dao/menu_dao.py diff --git a/dash-fastapi-backend/module_admin/mapper/post_crud.py b/dash-fastapi-backend/module_admin/dao/post_dao.py similarity index 100% rename from dash-fastapi-backend/module_admin/mapper/post_crud.py rename to dash-fastapi-backend/module_admin/dao/post_dao.py diff --git a/dash-fastapi-backend/module_admin/mapper/role_crud.py b/dash-fastapi-backend/module_admin/dao/role_dao.py similarity index 100% rename from dash-fastapi-backend/module_admin/mapper/role_crud.py rename to dash-fastapi-backend/module_admin/dao/role_dao.py diff --git a/dash-fastapi-backend/module_admin/mapper/user_crud.py b/dash-fastapi-backend/module_admin/dao/user_dao.py similarity index 100% rename from dash-fastapi-backend/module_admin/mapper/user_crud.py rename to dash-fastapi-backend/module_admin/dao/user_dao.py diff --git a/dash-fastapi-backend/module_admin/entity/do/dept_entity.py b/dash-fastapi-backend/module_admin/entity/do/dept_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/dept_entity.py rename to dash-fastapi-backend/module_admin/entity/do/dept_do.py diff --git a/dash-fastapi-backend/module_admin/entity/do/dict_entity.py b/dash-fastapi-backend/module_admin/entity/do/dict_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/dict_entity.py rename to dash-fastapi-backend/module_admin/entity/do/dict_do.py diff --git a/dash-fastapi-backend/module_admin/entity/do/log_entity.py b/dash-fastapi-backend/module_admin/entity/do/log_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/log_entity.py rename to dash-fastapi-backend/module_admin/entity/do/log_do.py diff --git a/dash-fastapi-backend/module_admin/entity/do/menu_entity.py b/dash-fastapi-backend/module_admin/entity/do/menu_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/menu_entity.py rename to dash-fastapi-backend/module_admin/entity/do/menu_do.py diff --git a/dash-fastapi-backend/module_admin/entity/do/post_entity.py b/dash-fastapi-backend/module_admin/entity/do/post_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/post_entity.py rename to dash-fastapi-backend/module_admin/entity/do/post_do.py diff --git a/dash-fastapi-backend/module_admin/entity/do/role_entity.py b/dash-fastapi-backend/module_admin/entity/do/role_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/role_entity.py rename to dash-fastapi-backend/module_admin/entity/do/role_do.py diff --git a/dash-fastapi-backend/module_admin/entity/do/user_entity.py b/dash-fastapi-backend/module_admin/entity/do/user_do.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/do/user_entity.py rename to dash-fastapi-backend/module_admin/entity/do/user_do.py diff --git a/dash-fastapi-backend/module_admin/entity/vo/dept_schema.py b/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/vo/dept_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/dept_vo.py diff --git a/dash-fastapi-backend/module_admin/entity/vo/login_schema.py b/dash-fastapi-backend/module_admin/entity/vo/login_vo.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/vo/login_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/login_vo.py diff --git a/dash-fastapi-backend/module_admin/entity/vo/menu_schema.py b/dash-fastapi-backend/module_admin/entity/vo/menu_vo.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/vo/menu_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/menu_vo.py diff --git a/dash-fastapi-backend/module_admin/entity/vo/post_schema.py b/dash-fastapi-backend/module_admin/entity/vo/post_vo.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/vo/post_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/post_vo.py diff --git a/dash-fastapi-backend/module_admin/entity/vo/role_schema.py b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/vo/role_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/role_vo.py diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_schema.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py similarity index 100% rename from dash-fastapi-backend/module_admin/entity/vo/user_schema.py rename to dash-fastapi-backend/module_admin/entity/vo/user_vo.py diff --git a/dash-fastapi-backend/module_admin/utils/log_tool.py b/dash-fastapi-backend/module_admin/utils/log_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/log_tool.py rename to dash-fastapi-backend/module_admin/utils/log_util.py diff --git a/dash-fastapi-backend/module_admin/utils/page_tool.py b/dash-fastapi-backend/module_admin/utils/page_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/page_tool.py rename to dash-fastapi-backend/module_admin/utils/page_util.py diff --git a/dash-fastapi-backend/module_admin/utils/response_tool.py b/dash-fastapi-backend/module_admin/utils/response_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/response_tool.py rename to dash-fastapi-backend/module_admin/utils/response_util.py diff --git a/dash-fastapi-backend/module_admin/utils/time_format_tool.py b/dash-fastapi-backend/module_admin/utils/time_format_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/time_format_tool.py rename to dash-fastapi-backend/module_admin/utils/time_format_util.py -- Gitee From feac5ab6f36ebcc50d7111322281ba9451368fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=85=89=E9=93=AD?= Date: Wed, 21 Jun 2023 14:53:03 +0800 Subject: [PATCH 018/169] =?UTF-8?q?refactor:=E4=BF=AE=E6=94=B9=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dept_controller.py | 8 ++--- .../controller/login_controller.py | 8 ++--- .../controller/menu_controller.py | 8 ++--- .../module_admin/controller/post_controler.py | 6 ++-- .../controller/role_controller.py | 6 ++-- .../controller/user_controller.py | 8 ++--- .../module_admin/dao/dept_dao.py | 12 ++++---- .../module_admin/dao/login_dao.py | 4 +-- .../module_admin/dao/menu_dao.py | 12 ++++---- .../module_admin/dao/post_dao.py | 16 +++++----- .../module_admin/dao/role_dao.py | 22 +++++++------- .../module_admin/dao/user_dao.py | 30 +++++++++---------- .../module_admin/entity/vo/dept_vo.py | 2 +- .../module_admin/entity/vo/post_vo.py | 2 +- .../module_admin/entity/vo/role_vo.py | 4 +-- .../module_admin/service/dept_service.py | 12 ++++---- .../module_admin/service/login_service.py | 10 +++---- .../module_admin/service/menu_service.py | 10 +++---- .../module_admin/service/post_service.py | 12 ++++---- .../module_admin/service/role_service.py | 20 ++++++------- .../module_admin/service/user_service.py | 26 ++++++++-------- 21 files changed, 119 insertions(+), 119 deletions(-) diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 69c87a5..186d2bb 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -3,10 +3,10 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.dept_service import * -from module_admin.entity.vo.dept_schema import * -from module_admin.mapper.dept_crud import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.dept_vo import * +from module_admin.dao.dept_dao import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * deptController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index c934add..7d10809 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -1,11 +1,11 @@ import uuid from fastapi import APIRouter from module_admin.service.login_service import * -from module_admin.entity.vo.login_schema import * -from module_admin.mapper.login_crud import * +from module_admin.entity.vo.login_vo import * +from module_admin.dao.login_dao import * from config.env import JwtConfig -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * from datetime import timedelta diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 70e273a..4dc107c 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -3,10 +3,10 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.menu_service import * -from module_admin.entity.vo.menu_schema import * -from module_admin.mapper.menu_crud import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.menu_vo import * +from module_admin.dao.menu_dao import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * menuController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index c6d9614..f2fac53 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -3,9 +3,9 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.post_service import * -from module_admin.entity.vo.post_schema import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.post_vo import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * postController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 7d53d83..fbcac03 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -3,9 +3,9 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.role_service import * -from module_admin.entity.vo.role_schema import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.role_vo import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * roleController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index dbaf254..2bfb939 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -3,10 +3,10 @@ from fastapi import Depends, Header from config.get_db import get_db from module_admin.service.login_service import get_current_user, get_password_hash from module_admin.service.user_service import * -from module_admin.entity.vo.user_schema import * -from module_admin.mapper.user_crud import * -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.entity.vo.user_vo import * +from module_admin.dao.user_dao import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * userController = APIRouter(dependencies=[Depends(get_current_user)]) diff --git a/dash-fastapi-backend/module_admin/dao/dept_dao.py b/dash-fastapi-backend/module_admin/dao/dept_dao.py index e2ce754..f4815d3 100644 --- a/dash-fastapi-backend/module_admin/dao/dept_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dept_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.dept_entity import SysDept -from module_admin.entity.vo.dept_schema import DeptModel, DeptResponse, CrudDeptResponse -from module_admin.utils.time_format_tool import list_format_datetime +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.vo.dept_vo import DeptModel, DeptResponse, CrudDeptResponse +from module_admin.utils.time_format_util import list_format_datetime def get_dept_by_id(db: Session, dept_id: int): @@ -126,7 +126,7 @@ def get_dept_list(db: Session, page_object: DeptModel): return DeptResponse(**result) -def add_dept_crud(db: Session, dept: DeptModel): +def add_dept_dao(db: Session, dept: DeptModel): """ 新增部门数据库操作 :param db: orm对象 @@ -142,7 +142,7 @@ def add_dept_crud(db: Session, dept: DeptModel): return CrudDeptResponse(**result) -def edit_dept_crud(db: Session, dept: dict): +def edit_dept_dao(db: Session, dept: dict): """ 编辑部门数据库操作 :param db: orm对象 @@ -162,7 +162,7 @@ def edit_dept_crud(db: Session, dept: dict): return CrudDeptResponse(**result) -def delete_dept_crud(db: Session, dept: DeptModel): +def delete_dept_dao(db: Session, dept: DeptModel): """ 删除部门数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/login_dao.py b/dash-fastapi-backend/module_admin/dao/login_dao.py index 0e39974..e723c2e 100644 --- a/dash-fastapi-backend/module_admin/dao/login_dao.py +++ b/dash-fastapi-backend/module_admin/dao/login_dao.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.user_entity import SysUser -from module_admin.utils.time_format_tool import object_format_datetime +from module_admin.entity.do.user_do import SysUser +from module_admin.utils.time_format_util import object_format_datetime def login_by_account(db: Session, user_name: str): diff --git a/dash-fastapi-backend/module_admin/dao/menu_dao.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py index 50749e8..421fbd5 100644 --- a/dash-fastapi-backend/module_admin/dao/menu_dao.py +++ b/dash-fastapi-backend/module_admin/dao/menu_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.menu_entity import SysMenu -from module_admin.entity.vo.menu_schema import MenuModel, MenuResponse, CrudMenuResponse -from module_admin.utils.time_format_tool import list_format_datetime +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.menu_vo import MenuModel, MenuResponse, CrudMenuResponse +from module_admin.utils.time_format_util import list_format_datetime def get_menu_detail_by_id(db: Session, menu_id: int): @@ -56,7 +56,7 @@ def get_menu_list(db: Session, page_object: MenuModel): return MenuResponse(**result) -def add_menu_crud(db: Session, menu: MenuModel): +def add_menu_dao(db: Session, menu: MenuModel): """ 新增菜单数据库操作 :param db: orm对象 @@ -72,7 +72,7 @@ def add_menu_crud(db: Session, menu: MenuModel): return CrudMenuResponse(**result) -def edit_menu_crud(db: Session, menu: dict): +def edit_menu_dao(db: Session, menu: dict): """ 编辑菜单数据库操作 :param db: orm对象 @@ -92,7 +92,7 @@ def edit_menu_crud(db: Session, menu: dict): return CrudMenuResponse(**result) -def delete_menu_crud(db: Session, menu: MenuModel): +def delete_menu_dao(db: Session, menu: MenuModel): """ 删除菜单数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/post_dao.py b/dash-fastapi-backend/module_admin/dao/post_dao.py index 8b04c4f..4b3a841 100644 --- a/dash-fastapi-backend/module_admin/dao/post_dao.py +++ b/dash-fastapi-backend/module_admin/dao/post_dao.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session -from module_admin.entity.do.post_entity import SysPost -from module_admin.entity.vo.post_schema import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse -from module_admin.utils.time_format_tool import list_format_datetime -from module_admin.utils.page_tool import get_page_info +from module_admin.entity.do.post_do import SysPost +from module_admin.entity.vo.post_vo import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse +from module_admin.utils.time_format_util import list_format_datetime +from module_admin.utils.page_util import get_page_info def get_post_by_id(db: Session, post_id: int): @@ -22,7 +22,7 @@ def get_post_detail_by_id(db: Session, post_id: int): return post_info -def get_post_select_option_crud(db: Session): +def get_post_select_option_dao(db: Session): post_info = db.query(SysPost) \ .filter(SysPost.status == 0) \ .all() @@ -67,7 +67,7 @@ def get_post_list(db: Session, page_object: PostPageObject): return PostPageObjectResponse(**result) -def add_post_crud(db: Session, post: PostModel): +def add_post_dao(db: Session, post: PostModel): """ 新增岗位数据库操作 :param db: orm对象 @@ -83,7 +83,7 @@ def add_post_crud(db: Session, post: PostModel): return CrudPostResponse(**result) -def edit_post_crud(db: Session, post: dict): +def edit_post_dao(db: Session, post: dict): """ 编辑岗位数据库操作 :param db: orm对象 @@ -103,7 +103,7 @@ def edit_post_crud(db: Session, post: dict): return CrudPostResponse(**result) -def delete_post_crud(db: Session, post: PostModel): +def delete_post_dao(db: Session, post: PostModel): """ 删除岗位数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 6923dd5..45d5edb 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -1,10 +1,10 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from module_admin.entity.do.role_entity import SysRole, SysRoleMenu -from module_admin.entity.do.menu_entity import SysMenu -from module_admin.entity.vo.role_schema import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel -from module_admin.utils.time_format_tool import list_format_datetime, object_format_datetime -from module_admin.utils.page_tool import get_page_info +from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel +from module_admin.utils.time_format_util import list_format_datetime, object_format_datetime +from module_admin.utils.page_util import get_page_info from datetime import datetime, time @@ -55,7 +55,7 @@ def get_role_detail_by_id(db: Session, role_id: int): return RoleDetailModel(**results) -def get_role_select_option_crud(db: Session): +def get_role_select_option_dao(db: Session): role_info = db.query(SysRole) \ .filter(SysRole.status == 0, SysRole.del_flag == 0) \ .all() @@ -110,7 +110,7 @@ def get_role_list(db: Session, page_object: RolePageObject): return RolePageObjectResponse(**result) -def add_role_crud(db: Session, role: RoleModel): +def add_role_dao(db: Session, role: RoleModel): """ 新增角色数据库操作 :param db: orm对象 @@ -126,7 +126,7 @@ def add_role_crud(db: Session, role: RoleModel): return CrudRoleResponse(**result) -def edit_role_crud(db: Session, role: dict): +def edit_role_dao(db: Session, role: dict): """ 编辑角色数据库操作 :param db: orm对象 @@ -146,7 +146,7 @@ def edit_role_crud(db: Session, role: dict): return CrudRoleResponse(**result) -def delete_role_crud(db: Session, role: RoleModel): +def delete_role_dao(db: Session, role: RoleModel): """ 删除角色数据库操作 :param db: orm对象 @@ -159,7 +159,7 @@ def delete_role_crud(db: Session, role: RoleModel): db.commit() # 提交保存到数据库中 -def add_role_menu_crud(db: Session, role_menu: RoleMenuModel): +def add_role_menu_dao(db: Session, role_menu: RoleMenuModel): """ 新增角色菜单关联信息数据库操作 :param db: orm对象 @@ -172,7 +172,7 @@ def add_role_menu_crud(db: Session, role_menu: RoleMenuModel): db.refresh(db_role_menu) # 刷新 -def delete_role_menu_crud(db: Session, role_menu: RoleMenuModel): +def delete_role_menu_dao(db: Session, role_menu: RoleMenuModel): """ 删除角色菜单关联信息数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index 81da538..efc9682 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -1,14 +1,14 @@ from sqlalchemy import and_, desc from sqlalchemy.orm import Session -from module_admin.entity.do.user_entity import SysUser, SysUserRole, SysUserPost -from module_admin.entity.do.role_entity import SysRole, SysRoleMenu -from module_admin.entity.do.dept_entity import SysDept -from module_admin.entity.do.post_entity import SysPost -from module_admin.entity.do.menu_entity import SysMenu -from module_admin.entity.vo.user_schema import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ +from module_admin.entity.do.user_do import SysUser, SysUserRole, SysUserPost +from module_admin.entity.do.role_do import SysRole, SysRoleMenu +from module_admin.entity.do.dept_do import SysDept +from module_admin.entity.do.post_do import SysPost +from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.vo.user_vo import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ UserPageObjectResponse, CrudUserResponse -from module_admin.utils.time_format_tool import list_format_datetime, format_datetime_dict_list -from module_admin.utils.page_tool import get_page_info +from module_admin.utils.time_format_util import list_format_datetime, format_datetime_dict_list +from module_admin.utils.page_util import get_page_info from datetime import datetime, time @@ -191,7 +191,7 @@ def get_user_list(db: Session, page_object: UserPageObject): return UserPageObjectResponse(**result) -def add_user_crud(db: Session, user: UserModel): +def add_user_dao(db: Session, user: UserModel): """ 新增用户数据库操作 :param db: orm对象 @@ -211,7 +211,7 @@ def add_user_crud(db: Session, user: UserModel): return CrudUserResponse(**result) -def edit_user_crud(db: Session, user: dict): +def edit_user_dao(db: Session, user: dict): """ 编辑用户数据库操作 :param db: orm对象 @@ -234,7 +234,7 @@ def edit_user_crud(db: Session, user: dict): return CrudUserResponse(**result) -def delete_user_crud(db: Session, user: UserModel): +def delete_user_dao(db: Session, user: UserModel): """ 删除用户数据库操作 :param db: orm对象 @@ -247,7 +247,7 @@ def delete_user_crud(db: Session, user: UserModel): db.commit() # 提交保存到数据库中 -def add_user_role_crud(db: Session, user_role: UserRoleModel): +def add_user_role_dao(db: Session, user_role: UserRoleModel): """ 新增用户角色关联信息数据库操作 :param db: orm对象 @@ -260,7 +260,7 @@ def add_user_role_crud(db: Session, user_role: UserRoleModel): db.refresh(db_user_role) # 刷新 -def delete_user_role_crud(db: Session, user_role: UserRoleModel): +def delete_user_role_dao(db: Session, user_role: UserRoleModel): """ 删除用户角色关联信息数据库操作 :param db: orm对象 @@ -273,7 +273,7 @@ def delete_user_role_crud(db: Session, user_role: UserRoleModel): db.commit() # 提交保存到数据库中 -def add_user_post_crud(db: Session, user_post: UserPostModel): +def add_user_post_dao(db: Session, user_post: UserPostModel): """ 新增用户岗位关联信息数据库操作 :param db: orm对象 @@ -286,7 +286,7 @@ def add_user_post_crud(db: Session, user_post: UserPostModel): db.refresh(db_user_post) # 刷新 -def delete_user_post_crud(db: Session, user_post: UserPostModel): +def delete_user_post_dao(db: Session, user_post: UserPostModel): """ 删除用户岗位关联信息数据库操作 :param db: orm对象 diff --git a/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py b/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py index 2a72eb0..a2cf228 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/dept_vo.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import Union, Optional, List -from module_admin.entity.vo.user_schema import DeptModel +from module_admin.entity.vo.user_vo import DeptModel class DeptPageObject(DeptModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/post_vo.py b/dash-fastapi-backend/module_admin/entity/vo/post_vo.py index c2edd89..5c39e88 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/post_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/post_vo.py @@ -1,6 +1,6 @@ from pydantic import BaseModel from typing import Union, Optional, List -from module_admin.entity.vo.user_schema import PostModel +from module_admin.entity.vo.user_vo import PostModel class PostPageObject(PostModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/role_vo.py b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py index 68a7d9e..c31322d 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/role_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/role_vo.py @@ -1,7 +1,7 @@ from pydantic import BaseModel from typing import Union, Optional, List -from module_admin.entity.vo.user_schema import RoleModel -from module_admin.entity.vo.menu_schema import MenuModel +from module_admin.entity.vo.user_vo import RoleModel +from module_admin.entity.vo.menu_vo import MenuModel class RoleMenuModel(BaseModel): diff --git a/dash-fastapi-backend/module_admin/service/dept_service.py b/dash-fastapi-backend/module_admin/service/dept_service.py index c86ad40..682706e 100644 --- a/dash-fastapi-backend/module_admin/service/dept_service.py +++ b/dash-fastapi-backend/module_admin/service/dept_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.dept_schema import * -from module_admin.mapper.dept_crud import * +from module_admin.entity.vo.dept_vo import * +from module_admin.dao.dept_dao import * def get_dept_tree_services(result_db: Session, page_object: DeptModel): @@ -52,7 +52,7 @@ def add_dept_services(result_db: Session, page_object: DeptModel): page_object.ancestors = f'{parent_info.ancestors},{page_object.parent_id}' else: page_object.ancestors = '0' - add_dept_result = add_dept_crud(result_db, page_object) + add_dept_result = add_dept_dao(result_db, page_object) return add_dept_result @@ -70,7 +70,7 @@ def edit_dept_services(result_db: Session, page_object: DeptModel): else: page_object.ancestors = '0' edit_dept = page_object.dict(exclude_unset=True) - edit_dept_result = edit_dept_crud(result_db, edit_dept) + edit_dept_result = edit_dept_dao(result_db, edit_dept) update_children_info(result_db, DeptModel(dept_id=page_object.dept_id, ancestors=page_object.ancestors, update_by=page_object.update_by, @@ -99,7 +99,7 @@ def delete_dept_services(result_db: Session, page_object: DeleteDeptModel): return CrudDeptResponse(**result) dept_id_dict = dict(dept_id=dept_id) - delete_dept_crud(result_db, DeptModel(**dept_id_dict)) + delete_dept_dao(result_db, DeptModel(**dept_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') @@ -155,7 +155,7 @@ def update_children_info(result_db, page_object): if children_info: for child in children_info: child.ancestors = f'{page_object.ancestors},{page_object.dept_id}' - edit_dept_crud(result_db, + edit_dept_dao(result_db, dict(dept_id=child.dept_id, ancestors=child.ancestors, update_by=page_object.update_by, diff --git a/dash-fastapi-backend/module_admin/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py index 29deaef..d098017 100644 --- a/dash-fastapi-backend/module_admin/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -1,11 +1,11 @@ -from module_admin.entity.vo.user_schema import * -from module_admin.mapper.login_crud import * -from module_admin.mapper.user_crud import * +from module_admin.entity.vo.user_vo import * +from module_admin.dao.login_dao import * +from module_admin.dao.user_dao import * from jose import JWTError, jwt from passlib.context import CryptContext from config.env import JwtConfig -from module_admin.utils.response_tool import * -from module_admin.utils.log_tool import * +from module_admin.utils.response_util import * +from module_admin.utils.log_util import * from datetime import datetime, timedelta from fastapi import Request from fastapi import Depends, Header diff --git a/dash-fastapi-backend/module_admin/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py index f3465ec..2c102b5 100644 --- a/dash-fastapi-backend/module_admin/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.menu_schema import * -from module_admin.mapper.menu_crud import * +from module_admin.entity.vo.menu_vo import * +from module_admin.dao.menu_dao import * def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): @@ -54,7 +54,7 @@ def add_menu_services(result_db: Session, page_object: MenuModel): :param page_object: 新增菜单对象 :return: 新增菜单校验结果 """ - add_menu_result = add_menu_crud(result_db, page_object) + add_menu_result = add_menu_dao(result_db, page_object) return add_menu_result @@ -67,7 +67,7 @@ def edit_menu_services(result_db: Session, page_object: MenuModel): :return: 编辑菜单校验结果 """ edit_menu = page_object.dict(exclude_unset=True) - edit_menu_result = edit_menu_crud(result_db, edit_menu) + edit_menu_result = edit_menu_dao(result_db, edit_menu) return edit_menu_result @@ -83,7 +83,7 @@ def delete_menu_services(result_db: Session, page_object: DeleteMenuModel): menu_id_list = page_object.menu_ids.split(',') for menu_id in menu_id_list: menu_id_dict = dict(menu_id=menu_id) - delete_menu_crud(result_db, MenuModel(**menu_id_dict)) + delete_menu_dao(result_db, MenuModel(**menu_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') diff --git a/dash-fastapi-backend/module_admin/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py index 6151ed8..f048a05 100644 --- a/dash-fastapi-backend/module_admin/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.post_schema import * -from module_admin.mapper.post_crud import * +from module_admin.entity.vo.post_vo import * +from module_admin.dao.post_dao import * def get_post_select_option_services(result_db: Session): @@ -8,7 +8,7 @@ def get_post_select_option_services(result_db: Session): :param result_db: orm对象 :return: 岗位列表不分页信息对象 """ - post_list_result = get_post_select_option_crud(result_db) + post_list_result = get_post_select_option_dao(result_db) return post_list_result @@ -32,7 +32,7 @@ def add_post_services(result_db: Session, page_object: PostModel): :param page_object: 新增岗位对象 :return: 新增岗位校验结果 """ - add_post_result = add_post_crud(result_db, page_object) + add_post_result = add_post_dao(result_db, page_object) return add_post_result @@ -45,7 +45,7 @@ def edit_post_services(result_db: Session, page_object: PostModel): :return: 编辑岗位校验结果 """ edit_post = page_object.dict(exclude_unset=True) - edit_post_result = edit_post_crud(result_db, edit_post) + edit_post_result = edit_post_dao(result_db, edit_post) return edit_post_result @@ -61,7 +61,7 @@ def delete_post_services(result_db: Session, page_object: DeletePostModel): post_id_list = page_object.post_ids.split(',') for post_id in post_id_list: post_id_dict = dict(post_id=post_id) - delete_post_crud(result_db, PostModel(**post_id_dict)) + delete_post_dao(result_db, PostModel(**post_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') diff --git a/dash-fastapi-backend/module_admin/service/role_service.py b/dash-fastapi-backend/module_admin/service/role_service.py index ff0b1e9..ed30123 100644 --- a/dash-fastapi-backend/module_admin/service/role_service.py +++ b/dash-fastapi-backend/module_admin/service/role_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.role_schema import * -from module_admin.mapper.role_crud import * +from module_admin.entity.vo.role_vo import * +from module_admin.dao.role_dao import * def get_role_select_option_services(result_db: Session): @@ -8,7 +8,7 @@ def get_role_select_option_services(result_db: Session): :param result_db: orm对象 :return: 角色列表不分页信息对象 """ - role_list_result = get_role_select_option_crud(result_db) + role_list_result = get_role_select_option_dao(result_db) return role_list_result @@ -33,14 +33,14 @@ def add_role_services(result_db: Session, page_object: AddRoleModel): :return: 新增角色校验结果 """ add_role = RoleModel(**page_object.dict()) - add_role_result = add_role_crud(result_db, add_role) + add_role_result = add_role_dao(result_db, add_role) if add_role_result.is_success: role_id = get_role_by_name(result_db, page_object.role_name).role_id if page_object.menu_id: menu_id_list = page_object.menu_id.split(',') for menu in menu_id_list: menu_dict = dict(role_id=role_id, menu_id=menu) - add_role_menu_crud(result_db, RoleMenuModel(**menu_dict)) + add_role_menu_dao(result_db, RoleMenuModel(**menu_dict)) return add_role_result @@ -57,15 +57,15 @@ def edit_role_services(result_db: Session, page_object: AddRoleModel): del edit_role['menu_id'] if page_object.type == 'status': del edit_role['type'] - edit_role_result = edit_role_crud(result_db, edit_role) + edit_role_result = edit_role_dao(result_db, edit_role) if edit_role_result.is_success and page_object.type != 'status': role_id_dict = dict(role_id=page_object.role_id) - delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) + delete_role_menu_dao(result_db, RoleMenuModel(**role_id_dict)) if page_object.menu_id: menu_id_list = page_object.menu_id.split(',') for menu in menu_id_list: menu_dict = dict(role_id=page_object.role_id, menu_id=menu) - add_role_menu_crud(result_db, RoleMenuModel(**menu_dict)) + add_role_menu_dao(result_db, RoleMenuModel(**menu_dict)) return edit_role_result @@ -81,8 +81,8 @@ def delete_role_services(result_db: Session, page_object: DeleteRoleModel): role_id_list = page_object.role_ids.split(',') for role_id in role_id_list: role_id_dict = dict(role_id=role_id, update_by=page_object.update_by, update_time=page_object.update_time) - delete_role_menu_crud(result_db, RoleMenuModel(**role_id_dict)) - delete_role_crud(result_db, RoleModel(**role_id_dict)) + delete_role_menu_dao(result_db, RoleMenuModel(**role_id_dict)) + delete_role_dao(result_db, RoleModel(**role_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入角色id为空') diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 909f5b3..230de75 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -1,5 +1,5 @@ -from module_admin.entity.vo.user_schema import * -from module_admin.mapper.user_crud import * +from module_admin.entity.vo.user_vo import * +from module_admin.dao.user_dao import * def get_user_list_services(result_db: Session, page_object: UserPageObject): @@ -22,19 +22,19 @@ def add_user_services(result_db: Session, page_object: AddUserModel): :return: 新增用户校验结果 """ add_user = UserModel(**page_object.dict()) - add_user_result = add_user_crud(result_db, add_user) + add_user_result = add_user_dao(result_db, add_user) if add_user_result.is_success: user_id = get_user_by_name(result_db, page_object.user_name).user_id if page_object.role_id: role_id_list = page_object.role_id.split(',') for role in role_id_list: role_dict = dict(user_id=user_id, role_id=role) - add_user_role_crud(result_db, UserRoleModel(**role_dict)) + add_user_role_dao(result_db, UserRoleModel(**role_dict)) if page_object.post_id: post_id_list = page_object.post_id.split(',') for post in post_id_list: post_dict = dict(user_id=user_id, post_id=post) - add_user_post_crud(result_db, UserPostModel(**post_dict)) + add_user_post_dao(result_db, UserPostModel(**post_dict)) return add_user_result @@ -52,21 +52,21 @@ def edit_user_services(result_db: Session, page_object: AddUserModel): del edit_user['post_id'] if page_object.type == 'status': del edit_user['type'] - edit_user_result = edit_user_crud(result_db, edit_user) + edit_user_result = edit_user_dao(result_db, edit_user) if edit_user_result.is_success and page_object.type != 'status': user_id_dict = dict(user_id=page_object.user_id) - delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) - delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) + delete_user_role_dao(result_db, UserRoleModel(**user_id_dict)) + delete_user_post_dao(result_db, UserPostModel(**user_id_dict)) if page_object.role_id: role_id_list = page_object.role_id.split(',') for role in role_id_list: role_dict = dict(user_id=page_object.user_id, role_id=role) - add_user_role_crud(result_db, UserRoleModel(**role_dict)) + add_user_role_dao(result_db, UserRoleModel(**role_dict)) if page_object.post_id: post_id_list = page_object.post_id.split(',') for post in post_id_list: post_dict = dict(user_id=page_object.user_id, post_id=post) - add_user_post_crud(result_db, UserPostModel(**post_dict)) + add_user_post_dao(result_db, UserPostModel(**post_dict)) return edit_user_result @@ -82,9 +82,9 @@ def delete_user_services(result_db: Session, page_object: DeleteUserModel): user_id_list = page_object.user_ids.split(',') for user_id in user_id_list: user_id_dict = dict(user_id=user_id, update_by=page_object.update_by, update_time=page_object.update_time) - delete_user_role_crud(result_db, UserRoleModel(**user_id_dict)) - delete_user_post_crud(result_db, UserPostModel(**user_id_dict)) - delete_user_crud(result_db, UserModel(**user_id_dict)) + delete_user_role_dao(result_db, UserRoleModel(**user_id_dict)) + delete_user_post_dao(result_db, UserPostModel(**user_id_dict)) + delete_user_dao(result_db, UserModel(**user_id_dict)) result = dict(is_success=True, message='删除成功') else: result = dict(is_success=False, message='传入用户id为空') -- Gitee From 16b0bafcefc8915a838d38e31ad4df57ddcb8a52 Mon Sep 17 00:00:00 2001 From: xlf Date: Thu, 6 Jul 2023 16:29:06 +0800 Subject: [PATCH 019/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E7=BA=A7=E5=88=AB=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=20fix:=E4=BF=AE=E5=A4=8D=E8=8F=9C=E5=8D=95=E9=9A=90=E8=97=8F?= =?UTF-8?q?=E6=97=A0=E6=95=88=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/service/login_service.py | 4 +- dash-fastapi-frontend/app.py | 41 ++- .../callbacks/layout_c/index_c.py | 20 +- .../callbacks/system_c/dept_c.py | 11 +- .../callbacks/system_c/menu_c/menu_c.py | 11 +- .../callbacks/system_c/post_c.py | 11 +- .../callbacks/system_c/role_c.py | 56 +++- .../callbacks/system_c/user_c.py | 11 +- dash-fastapi-frontend/store/store.py | 4 + dash-fastapi-frontend/utils/tree_tool.py | 50 +++ .../views/system/config/__init__.py | 2 +- .../views/system/dept/__init__.py | 155 +++++---- .../views/system/dict/__init__.py | 2 +- .../views/system/menu/__init__.py | 11 +- .../views/system/notice/__init__.py | 2 +- .../views/system/post/__init__.py | 256 +++++++------- .../views/system/role/__init__.py | 274 ++++++++------- .../views/system/user/__init__.py | 313 ++++++++++-------- 18 files changed, 721 insertions(+), 513 deletions(-) diff --git a/dash-fastapi-backend/module_admin/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py index d098017..49be7ca 100644 --- a/dash-fastapi-backend/module_admin/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -51,14 +51,14 @@ async def get_current_user(request: Request = Request, token: str = Header(...), # dept_name=user.user_dept_info[0].dept_name, # ancestors=user.user_dept_info[0].ancestors)) # user_role_info = deal_user_role_info(RoleInfo(role_info=user.user_role_info)) - user_menu_info = deal_user_menu_info(0, MenuList(menu_info=user.user_menu_info)) + # user_menu_info = deal_user_menu_info(0, MenuList(menu_info=user.user_menu_info)) return CurrentUserInfoServiceResponse( user=user.user_basic_info[0], dept=user.user_dept_info[0], role=user.user_role_info, post=user.user_post_info, - menu=user_menu_info + menu=user.user_menu_info ) else: logger.warning("用户token已失效,请重新登录") diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index 6c5a23a..c827f04 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -15,7 +15,7 @@ import views from callbacks import app_c from api.login import get_current_user_info_api -from utils.tree_tool import find_node_values, find_key_by_href +from utils.tree_tool import find_node_values, find_key_by_href, deal_user_menu_info app.layout = html.Div( [ @@ -66,7 +66,9 @@ app.layout = html.Div( Output('redirect-container', 'children', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True), Output('api-check-token', 'data', allow_duplicate=True), - Output('current-key-container', 'data')], + Output('current-key-container', 'data'), + Output('menu-info-store-container', 'data'), + Output('menu-list-store-container', 'data')], Input('url-container', 'pathname'), State('url-container', 'trigger'), prevent_initial_call=True @@ -83,12 +85,14 @@ def router(pathname, trigger): user_name = current_user['user']['user_name'] nick_name = current_user['user']['nick_name'] phone_number = current_user['user']['phonenumber'] - menu_info = current_user['menu'] + menu_list = current_user['menu'] + user_menu_list = [item for item in menu_list if item.get('visible') == '0'] + menu_info = deal_user_menu_info(0, menu_list) + user_menu_info = deal_user_menu_info(0, user_menu_list) session['user_info'] = current_user['user'] session['dept_info'] = current_user['dept'] session['role_info'] = current_user['role'] session['post_info'] = current_user['post'] - session['menu_info'] = menu_info valid_href_list = find_node_values(menu_info, 'href') valid_href_list.append('/') if pathname in valid_href_list: @@ -107,16 +111,21 @@ def router(pathname, trigger): id='router-redirect' ), None, - {'timestamp': time.time()} + {'timestamp': time.time()}, + {'current_key': current_key}, + {'menu_info': menu_info}, + {'menu_list': menu_list} ] # 否则正常渲染主页面 return [ - views.layout.render_content(user_name, nick_name, phone_number, menu_info), + views.layout.render_content(user_name, nick_name, phone_number, user_menu_info), None, fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), {'timestamp': time.time()}, - {'current_key': current_key} + {'current_key': current_key}, + {'menu_info': menu_info}, + {'menu_list': menu_list} ] # elif trigger == 'pushstate': @@ -128,7 +137,9 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, - {'current_key': current_key} + {'current_key': current_key}, + {'menu_info': menu_info}, + {'menu_list': menu_list} ] # else: @@ -148,6 +159,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -157,6 +170,8 @@ def router(pathname, trigger): dash.no_update, dash.no_update, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -168,6 +183,8 @@ def router(pathname, trigger): None, fuc.FefferyFancyNotification('接口异常', type='error', autoClose=2000), {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] else: @@ -181,6 +198,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -190,6 +209,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -199,6 +220,8 @@ def router(pathname, trigger): None, None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] @@ -211,6 +234,8 @@ def router(pathname, trigger): ), None, {'timestamp': time.time()}, + dash.no_update, + dash.no_update, dash.no_update ] diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 15ec34b..91bb76a 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -18,10 +18,12 @@ from utils.tree_tool import find_title_by_key, find_modules_by_key, find_href_by [Input('index-side-menu', 'currentKey'), Input('tabs-container', 'latestDeletePane')], [State('tabs-container', 'items'), - State('tabs-container', 'activeKey')], + State('tabs-container', 'activeKey'), + State('menu-info-store-container', 'data'), + State('menu-list-store-container', 'data')], prevent_initial_call=True ) -def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, activeKey): +def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, activeKey, menu_info, menu_list): """ 这个回调函数用于处理标签页子项的新建、切换及删除 具体策略: @@ -47,9 +49,10 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act currentKey ] - menu_title = find_title_by_key(session.get('menu_info'), currentKey) + menu_title = find_title_by_key(menu_info.get('menu_info'), currentKey) + button_perms = [item.get('perms') for item in menu_list.get('menu_list') if str(item.get('parent_id')) == currentKey] # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 - menu_modules = find_modules_by_key(session.get('menu_info'), currentKey) + menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) if menu_modules: # 否则追加子项返回 @@ -60,7 +63,7 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act { 'label': menu_title, 'key': currentKey, - 'children': eval('views.' + menu_modules + '.render()'), + 'children': eval('views.' + menu_modules + '.render(button_perms)'), } ], currentKey @@ -115,9 +118,10 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act [Output('header-breadcrumb', 'items'), Output('dcc-url', 'pathname')], Input('tabs-container', 'activeKey'), + State('menu-info-store-container', 'data'), prevent_initial_call=True ) -def get_current_breadcrumbs(active_key): +def get_current_breadcrumbs(active_key, menu_info): if active_key: if active_key == '首页': @@ -133,11 +137,11 @@ def get_current_breadcrumbs(active_key): ] else: - result = find_parents(session.get('menu_info'), active_key) + result = find_parents(menu_info.get('menu_info'), active_key) # 去除result的重复项 parent_info = list(OrderedDict((json.dumps(d, ensure_ascii=False), d) for d in result).values()) if parent_info: - current_href = find_href_by_key(session.get('menu_info'), active_key) + current_href = find_href_by_key(menu_info.get('menu_info'), active_key) return [ [ diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index a9497f7..028218e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -26,10 +26,11 @@ from api.dept import get_dept_tree_api, get_dept_list_api, add_dept_api, edit_de Input('dept-fold', 'nClicks')], [State('dept-dept_name-input', 'value'), State('dept-status-select', 'value'), - State('dept-list-table', 'defaultExpandedRowKeys')], + State('dept-list-table', 'defaultExpandedRowKeys'), + State('dept-button-perms-container', 'data')], prevent_initial_call=True ) -def get_dept_table_data(search_click, operations, fold_click, dept_name, status_select, in_default_expanded_row_keys): +def get_dept_table_data(search_click, operations, fold_click, dept_name, status_select, in_default_expanded_row_keys, button_perms): query_params = dict( dept_name=dept_name, @@ -55,17 +56,17 @@ def get_dept_table_data(search_click, operations, fold_click, dept_name, status_ 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:dept:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:dept:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:dept:remove' in button_perms else {}, ] table_data_new = get_dept_tree(0, table_data) diff --git a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py index d68af1b..b5dc97e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/menu_c/menu_c.py @@ -26,10 +26,11 @@ from api.menu import get_menu_tree_api, get_menu_tree_for_edit_option_api, get_m Input('menu-fold', 'nClicks')], [State('menu-menu_name-input', 'value'), State('menu-status-select', 'value'), - State('menu-list-table', 'defaultExpandedRowKeys')], + State('menu-list-table', 'defaultExpandedRowKeys'), + State('menu-button-perms-container', 'data')], prevent_initial_call=True ) -def get_menu_table_data(search_click, operations, fold_click, menu_name, status_select, in_default_expanded_row_keys): +def get_menu_table_data(search_click, operations, fold_click, menu_name, status_select, in_default_expanded_row_keys, button_perms): query_params = dict( menu_name=menu_name, @@ -62,17 +63,17 @@ def get_menu_table_data(search_click, operations, fold_click, menu_name, status_ 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:menu:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:menu:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:menu:remove' in button_perms else {}, ] table_data_new = list_to_tree(table_data) diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index b071aeb..33c21c3 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -20,10 +20,11 @@ from api.post import get_post_list_api, get_post_detail_api, add_post_api, edit_ Input('post-operations-store', 'data')], [State('post-post_code-input', 'value'), State('post-post_name-input', 'value'), - State('post-status-select', 'value')], + State('post-status-select', 'value'), + State('post-button-perms-container', 'data')], prevent_initial_call=True ) -def get_post_table_data(search_click, pagination, operations, post_code, post_name, status_select): +def get_post_table_data(search_click, pagination, operations, post_code, post_name, status_select, button_perms): query_params = dict( post_code=post_code, @@ -63,12 +64,12 @@ def get_post_table_data(search_click, pagination, operations, post_code, post_na 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:post:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:post:remove' in button_perms else {}, ] return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] @@ -290,7 +291,7 @@ def post_delete_modal(delete_click, button_click, return dash.no_update return [ - f'是否确认删除岗位编号为{post_ids}的用户?', + f'是否确认删除岗位编号为{post_ids}的岗位?', True, {'post_ids': post_ids} ] diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index f11b72f..69982c6 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -22,10 +22,11 @@ from api.menu import get_menu_tree_api [State('role-role_name-input', 'value'), State('role-role_key-input', 'value'), State('role-status-select', 'value'), - State('role-create_time-range', 'value')], + State('role-create_time-range', 'value'), + State('role-button-perms-container', 'data')], prevent_initial_call=True ) -def get_role_table_data(search_click, pagination, operations, role_name, role_key, status_select, create_time_range): +def get_role_table_data(search_click, pagination, operations, role_name, role_key, status_select, create_time_range, button_perms): create_time_start = None create_time_end = None @@ -74,12 +75,12 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:role:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:role:remove' in button_perms else {}, ] return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] @@ -143,8 +144,8 @@ def fold_unfold_role_menu(fold_unfold, menu_info): @app.callback( - Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), - Input('role-menu-perms-radio-all-none', 'checked'), + Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), + Input('role-menu-perms-radio-all-none', 'checked'), State('role-menu-store', 'data'), prevent_initial_call=True ) @@ -164,15 +165,27 @@ def all_none_role_menu_mode(all_none, menu_info): @app.callback( - Output('role-menu-perms', 'checkStrictly'), + [Output('role-menu-perms', 'checkStrictly'), + Output('role-menu-perms', 'checkedKeys', allow_duplicate=True)], Input('role-menu-perms-radio-parent-children', 'checked'), + State('current-role-menu-store', 'data'), prevent_initial_call=True ) -def change_role_menu_mode(parent_children): +def change_role_menu_mode(parent_children, current_role_menu): if parent_children: - return False + checked_menu = [] + for item in current_role_menu: + has_children = False + for other_item in current_role_menu: + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) + return [False, checked_menu] else: - return True + checked_menu = [str(item.get('menu_id')) for item in current_role_menu if item] or [] + return [True, checked_menu] @app.callback( @@ -186,6 +199,7 @@ def change_role_menu_mode(parent_children): Output('role-menu-perms', 'expandedKeys', allow_duplicate=True), Output('role-menu-perms', 'checkedKeys', allow_duplicate=True), Output('role-menu-store', 'data'), + Output('current-role-menu-store', 'data'), Output('role-remark', 'value'), Output('api-check-token', 'data', allow_duplicate=True), Output('role-add', 'nClicks'), @@ -219,6 +233,7 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, None, tree_data[1], None, + None, {'timestamp': time.time()}, None, None, @@ -233,7 +248,15 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, role_info_res = get_role_detail_api(role_id=role_id) if role_info_res['code'] == 200: role_info = role_info_res['data'] - checked_menu = [str(item.get('menu_id')) for item in role_info.get('menu') if item] or [] + checked_menu = [] + for item in role_info.get('menu'): + has_children = False + for other_item in role_info.get('menu'): + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) return [ True, '编辑角色', @@ -245,6 +268,7 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, [], checked_menu, tree_data[1], + role_info.get('menu'), role_info.get('role').get('remark'), {'timestamp': time.time()}, None, @@ -253,9 +277,9 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, {'type': 'edit'} ] - return [dash.no_update] * 11 + [{'timestamp': time.time()}, None, None, None, None] + return [dash.no_update] * 12 + [{'timestamp': time.time()}, None, None, None, None] - return [dash.no_update] * 12 + [None, None, None, None] + return [dash.no_update] * 13 + [None, None, None, None] @app.callback( @@ -277,12 +301,14 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, State('role-role_sort', 'value'), State('role-status', 'value'), State('role-menu-perms', 'checkedKeys'), + State('role-menu-perms', 'halfCheckedKeys'), State('role-remark', 'value')], prevent_initial_call=True ) -def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role_key, role_sort, status, menu_perms, remark): +def role_confirm(confirm_trigger, operation_type, cur_role_info, role_name, role_key, role_sort, status, menu_checked_keys, menu_half_checked_eys, remark): if confirm_trigger: if all([role_name, role_key, role_sort]): + menu_perms = menu_half_checked_eys + menu_checked_keys params_add = dict(role_name=role_name, role_key=role_key, role_sort=role_sort, menu_id=','.join(menu_perms) if menu_perms else None, status=status, remark=remark) params_edit = dict(role_id=cur_role_info.get('role_id') if cur_role_info else None, role_name=role_name, role_key=role_key, role_sort=role_sort, menu_id=','.join(menu_perms) if menu_perms else '', status=status, remark=remark) @@ -407,7 +433,7 @@ def role_delete_modal(delete_click, button_click, return dash.no_update return [ - f'是否确认删除角色编号为{role_ids}的用户?', + f'是否确认删除角色编号为{role_ids}的角色?', True, {'role_ids': role_ids} ] diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index ee733b0..99abb4f 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -45,11 +45,12 @@ def get_search_dept_tree(dept_input): [State('user-user_name-input', 'value'), State('user-phone_number-input', 'value'), State('user-status-select', 'value'), - State('user-create_time-range', 'value')], + State('user-create_time-range', 'value'), + State('user-button-perms-container', 'data')], prevent_initial_call=True ) def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, pagination, operations, - user_name, phone_number, status_select, create_time_range): + user_name, phone_number, status_select, create_time_range, button_perms): dept_id = None create_time_start = None create_time_end = None @@ -101,15 +102,15 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio { 'title': '修改', 'icon': 'antd-edit' - }, + } if 'system:user:edit' in button_perms else None, { 'title': '删除', 'icon': 'antd-delete' - }, + } if 'system:user:remove' in button_perms else None, { 'title': '重置密码', 'icon': 'antd-key' - } + } if 'system:user:resetPwd' in button_perms else None ] return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index d04f47e..6cbc206 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -11,6 +11,9 @@ def render_store_container(): dcc.Store(id='api-check-result-container'), # token存储容器 dcc.Store(id='token-container'), + # 菜单信息存储容器 + dcc.Store(id='menu-info-store-container'), + dcc.Store(id='menu-list-store-container'), # 菜单current_key存储容器 dcc.Store(id='current-key-container'), # 用户管理模块操作类型存储容器 @@ -28,6 +31,7 @@ def render_store_container(): dcc.Store(id='role-delete-ids-store'), # 角色管理模块菜单权限存储容器 dcc.Store(id='role-menu-store'), + dcc.Store(id='current-role-menu-store'), # 菜单管理模块操作类型存储容器 dcc.Store(id='menu-operations-store'), dcc.Store(id='menu-operations-store-bk'), diff --git a/dash-fastapi-frontend/utils/tree_tool.py b/dash-fastapi-frontend/utils/tree_tool.py index 92f7188..c7f5792 100644 --- a/dash-fastapi-frontend/utils/tree_tool.py +++ b/dash-fastapi-frontend/utils/tree_tool.py @@ -118,6 +118,56 @@ def find_parents(tree, target_key): return result[::-1] +def deal_user_menu_info(pid: int, permission_list: list): + """ + 工具方法:根据菜单信息生成树形嵌套数据 + :param pid: 菜单id + :param permission_list: 菜单列表信息 + :return: 菜单树形嵌套数据 + """ + menu_list = [] + for permission in permission_list: + if permission['parent_id'] == pid: + children = deal_user_menu_info(permission['menu_id'], permission_list) + antd_menu_list_data = {} + if children and permission['menu_type'] == 'M': + antd_menu_list_data['component'] = 'SubMenu' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'] + } + antd_menu_list_data['children'] = children + elif children and permission['menu_type'] == 'C': + antd_menu_list_data['component'] = 'Item' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'], + 'href': permission['path'], + 'modules': permission['component'] + } + antd_menu_list_data['button'] = children + elif permission['menu_type'] == 'F': + antd_menu_list_data['component'] = 'Button' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'] + } + else: + antd_menu_list_data['component'] = 'Item' + antd_menu_list_data['props'] = { + 'key': str(permission['menu_id']), + 'title': permission['menu_name'], + 'icon': permission['icon'], + 'href': permission['path'], + } + menu_list.append(antd_menu_list_data) + + return menu_list + + def get_dept_tree(pid: int, permission_list: list): """ 工具方法:根据部门信息生成树形嵌套数据 diff --git a/dash-fastapi-frontend/views/system/config/__init__.py b/dash-fastapi-frontend/views/system/config/__init__.py index 64badb5..9cdfad2 100644 --- a/dash-fastapi-frontend/views/system/config/__init__.py +++ b/dash-fastapi-frontend/views/system/config/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是参数设置') diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index 07160fe..3a22623 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -1,4 +1,4 @@ -from dash import dcc +from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.dept_c @@ -6,7 +6,7 @@ from api.dept import get_dept_list_api from utils.tree_tool import get_dept_tree -def render(): +def render(button_perms): table_data_new = [] default_expanded_row_keys = [] table_info = get_dept_list_api({}) @@ -27,21 +27,22 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:dept:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:dept:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:dept:remove' in button_perms else {}, ] table_data_new = get_dept_tree(0, table_data) return [ + dcc.Store(id='dept-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -49,69 +50,74 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdSpace( + fac.AntdForm( [ - fac.AntdFormItem( - fac.AntdInput( - id='dept-dept_name-input', - placeholder='请输入部门名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 240 - } - ), - label='部门名称' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='dept-status-select', - placeholder='部门状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 240 - } - ), - label='部门状态' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='dept-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dept-dept_name-input', + placeholder='请输入部门名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='部门名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dept-status-select', + placeholder='部门状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='部门状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dept-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dept-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) ) - ) + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='dept-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) ], - style={ - 'paddingBottom': '10px' - } - ), + layout='inline', + ) ], - layout='inline', - ) + hidden='system:dept:query' not in button_perms + ), ) ] ), @@ -120,19 +126,24 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dept-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='dept-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:dept:add' not in button_perms ), fac.AntdButton( [ diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index 46a618d..e0aa134 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是字典管理') diff --git a/dash-fastapi-frontend/views/system/menu/__init__.py b/dash-fastapi-frontend/views/system/menu/__init__.py index 17fb505..4837da6 100644 --- a/dash-fastapi-frontend/views/system/menu/__init__.py +++ b/dash-fastapi-frontend/views/system/menu/__init__.py @@ -1,4 +1,4 @@ -from dash import html +from dash import dcc, html import feffery_antd_components as fac from api.menu import get_menu_list_api @@ -7,7 +7,7 @@ from views.system.menu.components.icon_category import render_icon import callbacks.system_c.menu_c.menu_c -def render(): +def render(button_perms): table_data_new = [] table_info = get_menu_list_api({}) if table_info['code'] == 200: @@ -33,21 +33,22 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:menu:edit' in button_perms else {}, { 'content': '新增', 'type': 'link', 'icon': 'antd-plus' - }, + } if 'system:menu:add' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:menu:remove' in button_perms else {}, ] table_data_new = list_to_tree(table_data) return [ + dcc.Store(id='menu-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( diff --git a/dash-fastapi-frontend/views/system/notice/__init__.py b/dash-fastapi-frontend/views/system/notice/__init__.py index 4bcc439..00785db 100644 --- a/dash-fastapi-frontend/views/system/notice/__init__.py +++ b/dash-fastapi-frontend/views/system/notice/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是通知公告') diff --git a/dash-fastapi-frontend/views/system/post/__init__.py b/dash-fastapi-frontend/views/system/post/__init__.py index 69dd206..5e010fa 100644 --- a/dash-fastapi-frontend/views/system/post/__init__.py +++ b/dash-fastapi-frontend/views/system/post/__init__.py @@ -1,11 +1,11 @@ -from dash import dcc +from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.post_c from api.post import get_post_list_api -def render(): +def render(button_perms): post_params = dict(page_num=1, page_size=10) table_info = get_post_list_api(post_params) @@ -29,15 +29,16 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:post:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:post:remove' in button_perms else {}, ] return [ + dcc.Store(id='post-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -45,81 +46,86 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdSpace( + fac.AntdForm( [ - fac.AntdFormItem( - fac.AntdInput( - id='post-post_code-input', - placeholder='请输入岗位编码', - autoComplete='off', - allowClear=True, - style={ - 'width': 210 - } - ), - label='岗位编码' - ), - fac.AntdFormItem( - fac.AntdInput( - id='post-post_name-input', - placeholder='请输入岗位名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 210 - } - ), - label='岗位名称' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='post-status-select', - placeholder='岗位状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 200 - } - ), - label='岗位状态' - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='post-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='post-post_code-input', + placeholder='请输入岗位编码', + autoComplete='off', + allowClear=True, + style={ + 'width': 210 + } + ), + label='岗位编码' + ), + fac.AntdFormItem( + fac.AntdInput( + id='post-post_name-input', + placeholder='请输入岗位名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 210 + } + ), + label='岗位名称' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='post-status-select', + placeholder='岗位状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 200 + } + ), + label='岗位状态' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='post-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='post-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) ) - ) + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='post-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) ], - style={ - 'paddingBottom': '10px' - } - ), + layout='inline', + ) ], - layout='inline', - ) + hidden='system:post:query' not in button_perms + ), ) ] ), @@ -128,63 +134,83 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='post-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='post-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:post:add' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-edit' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='post-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } ), - '修改', ], - id='post-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } + hidden='system:post:edit' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-minus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='post-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } ), - '删除', ], - id='post-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } + hidden='system:post:remove' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-down' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='post-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } ), - '导出', ], - id='post-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } + hidden='system:post:export' not in button_perms ), ], style={ diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index ec7d1f9..e17ac94 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -5,7 +5,7 @@ import callbacks.system_c.role_c from api.role import get_role_list_api -def render(): +def render(button_perms): role_params = dict(page_num=1, page_size=10) table_info = get_role_list_api(role_params) @@ -29,15 +29,16 @@ def render(): 'content': '修改', 'type': 'link', 'icon': 'antd-edit' - }, + } if 'system:role:edit' in button_perms else {}, { 'content': '删除', 'type': 'link', 'icon': 'antd-delete' - }, + } if 'system:role:remove' in button_perms else {}, ] return [ + dcc.Store(id='role-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -45,89 +46,94 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdFormItem( - fac.AntdInput( - id='role-role_name-input', - placeholder='请输入角色名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 220 - } - ), - label='角色名称', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdInput( - id='role-role_key-input', - placeholder='请输入权限字符', - autoComplete='off', - allowClear=True, - style={ - 'width': 220 - } - ), - label='权限字符', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdSelect( - id='role-status-select', - placeholder='角色状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 220 - } - ), - label='状态', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdDateRangePicker( - id='role-create_time-range', - style={ - 'width': 240 - } - ), - label='创建时间', - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='role-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' - ) - ), - style={ 'paddingBottom': '10px' }, - ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='role-reset', - icon=fac.AntdIcon( - icon='antd-sync' + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='role-role_name-input', + placeholder='请输入角色名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 220 + } + ), + label='角色名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='role-role_key-input', + placeholder='请输入权限字符', + autoComplete='off', + allowClear=True, + style={ + 'width': 220 + } + ), + label='权限字符', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='role-status-select', + placeholder='角色状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 220 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='role-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='role-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='role-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, ) - ), - style={ 'paddingBottom': '10px' }, + ], + layout='inline', ) ], - layout='inline', - ) + hidden='system:role:query' not in button_perms + ), ) ] ), @@ -136,63 +142,83 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='role-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='role-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:role:add' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-edit' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='role-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } ), - '修改', ], - id='role-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } + hidden='system:role:edit' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-minus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='role-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } ), - '删除', ], - id='role-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } + hidden='system:role:remove' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-down' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='role-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } ), - '导出', ], - id='role-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } + hidden='system:role:export' not in button_perms ), ], style={ diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index 3718045..e067f9f 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -1,4 +1,4 @@ -from dash import dcc +from dash import dcc, html import feffery_antd_components as fac import callbacks.system_c.user_c @@ -6,7 +6,7 @@ from api.user import get_user_list_api from api.dept import get_dept_tree_api -def render(): +def render(button_perms): dept_params = dict(dept_name='') user_params = dict(page_num=1, page_size=10) tree_info = get_dept_tree_api(dept_params) @@ -33,18 +33,19 @@ def render(): { 'title': '修改', 'icon': 'antd-edit' - }, + } if 'system:user:edit' in button_perms else None, { 'title': '删除', 'icon': 'antd-delete' - }, + } if 'system:user:remove' in button_perms else None, { 'title': '重置密码', 'icon': 'antd-key' - } + } if 'system:user:resetPwd' in button_perms else None ] return [ + dcc.Store(id='user-button-perms-container', data=button_perms), fac.AntdRow( [ fac.AntdCol( @@ -78,97 +79,102 @@ def render(): fac.AntdRow( [ fac.AntdCol( - fac.AntdForm( + html.Div( [ - fac.AntdSpace( + fac.AntdForm( [ - fac.AntdFormItem( - fac.AntdInput( - id='user-user_name-input', - placeholder='请输入用户名称', - autoComplete='off', - allowClear=True, - style={ - 'width': 240 - } - ), - label='用户名称' - ), - fac.AntdFormItem( - fac.AntdInput( - id='user-phone_number-input', - placeholder='请输入手机号码', - autoComplete='off', - allowClear=True, - style={ - 'width': 240 - } - ), - label='手机号码' - ), - fac.AntdFormItem( - fac.AntdSelect( - id='user-status-select', - placeholder='用户状态', - options=[ - { - 'label': '正常', - 'value': '0' - }, - { - 'label': '停用', - 'value': '1' - } - ], - style={ - 'width': 240 - } - ), - label='用户状态' - ), - ], - style={ - 'paddingBottom': '10px' - } - ), - fac.AntdSpace( - [ - fac.AntdFormItem( - fac.AntdDateRangePicker( - id='user-create_time-range', - style={ - 'width': 240 - } - ), - label='创建时间' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdInput( + id='user-user_name-input', + placeholder='请输入用户名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='用户名称' + ), + fac.AntdFormItem( + fac.AntdInput( + id='user-phone_number-input', + placeholder='请输入手机号码', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='手机号码' + ), + fac.AntdFormItem( + fac.AntdSelect( + id='user-status-select', + placeholder='用户状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='用户状态' + ), + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '搜索', - id='user-search', - type='primary', - icon=fac.AntdIcon( - icon='antd-search' + fac.AntdSpace( + [ + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='user-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间' + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='user-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ) + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='user-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ) ) - ) + ], + style={ + 'paddingBottom': '10px' + } ), - fac.AntdFormItem( - fac.AntdButton( - '重置', - id='user-reset', - icon=fac.AntdIcon( - icon='antd-sync' - ) - ) - ) ], - style={ - 'paddingBottom': '10px' - } - ), + layout='inline', + ) ], - layout='inline', - ) + hidden='system:user:query' not in button_perms + ), ) ] ), @@ -177,77 +183,102 @@ def render(): fac.AntdCol( fac.AntdSpace( [ - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-plus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='user-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } ), - '新增', ], - id='user-add', - style={ - 'color': '#1890ff', - 'background': '#e8f4ff', - 'border-color': '#a3d3ff' - } + hidden='system:user:add' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-edit' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='user-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } ), - '修改', ], - id='user-edit', - disabled=True, - style={ - 'color': '#71e2a3', - 'background': '#e7faf0', - 'border-color': '#d0f5e0' - } + hidden='system:user:edit' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-minus' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='user-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } ), - '删除', ], - id='user-delete', - disabled=True, - style={ - 'color': '#ff9292', - 'background': '#ffeded', - 'border-color': '#ffdbdb' - } + hidden='system:user:remove' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-up' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-up' + ), + '导入', + ], + id='user-import', + style={ + 'color': '#909399', + 'background': '#f4f4f5', + 'border-color': '#d3d4d6' + } ), - '导入', ], - id='user-import', - style={ - 'color': '#909399', - 'background': '#f4f4f5', - 'border-color': '#d3d4d6' - } + hidden='system:user:export' not in button_perms ), - fac.AntdButton( + html.Div( [ - fac.AntdIcon( - icon='antd-arrow-down' + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='user-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } ), - '导出', ], - id='user-export', - style={ - 'color': '#ffba00', - 'background': '#fff8e6', - 'border-color': '#ffe399' - } + hidden='system:user:import' not in button_perms ), ], style={ -- Gitee From d26fd55a05f3039589b6f8a159b4a23ace9bed0e Mon Sep 17 00:00:00 2001 From: xlf Date: Tue, 11 Jul 2023 15:35:54 +0800 Subject: [PATCH 020/169] =?UTF-8?q?feat:=E5=90=8E=E7=AB=AF=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=A0=A1=E9=AA=8C=E6=8E=A5=E5=8F=A3=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AF=B9=E6=89=80=E6=9C=89=E6=8E=A5=E5=8F=A3=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/aspect/interface_auth.py | 19 +++++++++++++++++++ .../controller/dept_controller.py | 15 ++++++++------- .../controller/login_controller.py | 5 +++-- .../controller/menu_controller.py | 15 ++++++++------- .../module_admin/controller/post_controler.py | 13 +++++++------ .../controller/role_controller.py | 13 +++++++------ .../controller/user_controller.py | 11 ++++++----- dash-fastapi-frontend/callbacks/app_c.py | 2 +- .../views/monitor/operlog/__init__.py | 2 +- 9 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/aspect/interface_auth.py diff --git a/dash-fastapi-backend/module_admin/aspect/interface_auth.py b/dash-fastapi-backend/module_admin/aspect/interface_auth.py new file mode 100644 index 0000000..ca25cc5 --- /dev/null +++ b/dash-fastapi-backend/module_admin/aspect/interface_auth.py @@ -0,0 +1,19 @@ +from fastapi import Depends +from module_admin.entity.vo.user_vo import * +from module_admin.service.login_service import get_current_user +from module_admin.utils.response_util import AuthException + + +class CheckUserInterfaceAuth: + """ + 校验当前用户是否具有相应的接口权限 + """ + def __init__(self, perm_str: str = 'common'): + self.perm_str = perm_str + + def __call__(self, current_user: CurrentUserInfoServiceResponse = Depends(get_current_user)): + user_auth_list = [item.perms for item in current_user.menu] + user_auth_list.append('common') + if self.perm_str in user_auth_list: + return True + raise AuthException(data="", message="该用户无此接口权限") diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 186d2bb..217d78d 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -7,12 +7,13 @@ from module_admin.entity.vo.dept_vo import * from module_admin.dao.dept_dao import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth deptController = APIRouter(dependencies=[Depends(get_current_user)]) -@deptController.post("/dept/tree", response_model=DeptTree) +@deptController.post("/dept/tree", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_services(query_db, dept_query) @@ -23,7 +24,7 @@ async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depend return response_500(data="", message="接口异常") -@deptController.post("/dept/forEditOption", response_model=DeptTree) +@deptController.post("/dept/forEditOption", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) @@ -34,7 +35,7 @@ async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: return response_500(data="", message="接口异常") -@deptController.post("/dept/get", response_model=DeptResponse) +@deptController.post("/dept/get", response_model=DeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))]) async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_list_services(query_db, dept_query) @@ -45,7 +46,7 @@ async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depend return response_500(data="", message="接口异常") -@deptController.post("/dept/add", response_model=CrudDeptResponse) +@deptController.post("/dept/add", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:add'))]) async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -62,7 +63,7 @@ async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional return response_500(data="", message="接口异常") -@deptController.patch("/dept/edit", response_model=CrudDeptResponse) +@deptController.patch("/dept/edit", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -80,7 +81,7 @@ async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Option return response_500(data="", message="接口异常") -@deptController.post("/dept/delete", response_model=CrudDeptResponse) +@deptController.post("/dept/delete", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:delete'))]) async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -98,7 +99,7 @@ async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, tok return response_500(data="", message="接口异常") -@deptController.get("/dept/{dept_id}", response_model=DeptModel) +@deptController.get("/dept/{dept_id}", response_model=DeptModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) async def query_detail_system_dept(dept_id: int, query_db: Session = Depends(get_db)): try: detail_dept_result = detail_dept_services(query_db, dept_id) diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index 7d10809..378b19b 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -6,6 +6,7 @@ from module_admin.dao.login_dao import * from config.env import JwtConfig from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from datetime import timedelta @@ -45,7 +46,7 @@ async def login(request: Request, user: UserLogin, query_db: Session = Depends(g return response_500(data="", message="接口异常") -@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse, dependencies=[Depends(get_current_user)]) +@loginController.post("/getLoginUserInfo", response_model=CurrentUserInfoServiceResponse, dependencies=[Depends(get_current_user), Depends(CheckUserInterfaceAuth('common'))]) async def get_login_user_info(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -56,7 +57,7 @@ async def get_login_user_info(request: Request, token: Optional[str] = Header(.. return response_500(data="", message="接口异常") -@loginController.post("/logout", dependencies=[Depends(get_current_user)]) +@loginController.post("/logout", dependencies=[Depends(get_current_user), Depends(CheckUserInterfaceAuth('common'))]) async def logout(request: Request, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 4dc107c..b364cbc 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -7,12 +7,13 @@ from module_admin.entity.vo.menu_vo import * from module_admin.dao.menu_dao import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth menuController = APIRouter(dependencies=[Depends(get_current_user)]) -@menuController.post("/menu/tree", response_model=MenuTree) +@menuController.post("/menu/tree", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_services(query_db, menu_query) @@ -23,7 +24,7 @@ async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = De return response_500(data="", message="接口异常") -@menuController.post("/menu/forEditOption", response_model=MenuTree) +@menuController.post("/menu/forEditOption", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) @@ -34,7 +35,7 @@ async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: return response_500(data="", message="接口异常") -@menuController.post("/menu/get", response_model=MenuResponse) +@menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_list_services(query_db, menu_query) @@ -45,7 +46,7 @@ async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depend return response_500(data="", message="接口异常") -@menuController.post("/menu/add", response_model=CrudMenuResponse) +@menuController.post("/menu/add", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:add'))]) async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -62,7 +63,7 @@ async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional return response_500(data="", message="接口异常") -@menuController.patch("/menu/edit", response_model=CrudMenuResponse) +@menuController.patch("/menu/edit", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -80,7 +81,7 @@ async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Option return response_500(data="", message="接口异常") -@menuController.post("/menu/delete", response_model=CrudMenuResponse) +@menuController.post("/menu/delete", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:delete'))]) async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): try: delete_menu_result = delete_menu_services(query_db, delete_menu) @@ -95,7 +96,7 @@ async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = D return response_500(data="", message="接口异常") -@menuController.get("/menu/{menu_id}", response_model=MenuModel) +@menuController.get("/menu/{menu_id}", response_model=MenuModel, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) async def query_detail_system_menu(menu_id: int, query_db: Session = Depends(get_db)): try: detail_menu_result = detail_menu_services(query_db, menu_id) diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index f2fac53..edd0ec9 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -6,12 +6,13 @@ from module_admin.service.post_service import * from module_admin.entity.vo.post_vo import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth postController = APIRouter(dependencies=[Depends(get_current_user)]) -@postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel) +@postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_post_select(query_db: Session = Depends(get_db)): try: role_query_result = get_post_select_option_services(query_db) @@ -22,7 +23,7 @@ async def get_system_post_select(query_db: Session = Depends(get_db)): return response_500(data="", message="接口异常") -@postController.post("/post/get", response_model=PostPageObjectResponse) +@postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) async def get_system_post_list(post_query: PostPageObject, query_db: Session = Depends(get_db)): try: post_query_result = get_post_list_services(query_db, post_query) @@ -33,7 +34,7 @@ async def get_system_post_list(post_query: PostPageObject, query_db: Session = D return response_500(data="", message="接口异常") -@postController.post("/post/add", response_model=CrudPostResponse) +@postController.post("/post/add", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:add'))]) async def add_system_post(request: Request, add_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -50,7 +51,7 @@ async def add_system_post(request: Request, add_post: PostModel, token: Optional return response_500(data="", message="接口异常") -@postController.patch("/post/edit", response_model=CrudPostResponse) +@postController.patch("/post/edit", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -68,7 +69,7 @@ async def edit_system_post(request: Request, edit_post: PostModel, token: Option return response_500(data="", message="接口异常") -@postController.post("/post/delete", response_model=CrudPostResponse) +@postController.post("/post/delete", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:delete'))]) async def delete_system_post(delete_post: DeletePostModel, query_db: Session = Depends(get_db)): try: delete_post_result = delete_post_services(query_db, delete_post) @@ -83,7 +84,7 @@ async def delete_system_post(delete_post: DeletePostModel, query_db: Session = D return response_500(data="", message="接口异常") -@postController.get("/post/{post_id}", response_model=PostModel) +@postController.get("/post/{post_id}", response_model=PostModel, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) async def query_detail_system_post(post_id: int, query_db: Session = Depends(get_db)): try: detail_post_result = detail_post_services(query_db, post_id) diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index fbcac03..3b8ef89 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -6,12 +6,13 @@ from module_admin.service.role_service import * from module_admin.entity.vo.role_vo import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth roleController = APIRouter(dependencies=[Depends(get_current_user)]) -@roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel) +@roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def get_system_role_select(query_db: Session = Depends(get_db)): try: role_query_result = get_role_select_option_services(query_db) @@ -22,7 +23,7 @@ async def get_system_role_select(query_db: Session = Depends(get_db)): return response_500(data="", message="接口异常") -@roleController.post("/role/get", response_model=RolePageObjectResponse) +@roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) async def get_system_role_list(role_query: RolePageObject, query_db: Session = Depends(get_db)): try: role_query_result = get_role_list_services(query_db, role_query) @@ -33,7 +34,7 @@ async def get_system_role_list(role_query: RolePageObject, query_db: Session = D return response_500(data="", message="接口异常") -@roleController.post("/role/add", response_model=CrudRoleResponse) +@roleController.post("/role/add", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:add'))]) async def add_system_role(request: Request, add_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -50,7 +51,7 @@ async def add_system_role(request: Request, add_role: AddRoleModel, token: Optio return response_500(data="", message="接口异常") -@roleController.patch("/role/edit", response_model=CrudRoleResponse) +@roleController.patch("/role/edit", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -68,7 +69,7 @@ async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Opt return response_500(data="", message="接口异常") -@roleController.post("/role/delete", response_model=CrudRoleResponse) +@roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:delete'))]) async def delete_system_role(request: Request, delete_role: DeleteRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -86,7 +87,7 @@ async def delete_system_role(request: Request, delete_role: DeleteRoleModel, tok return response_500(data="", message="接口异常") -@roleController.get("/role/{role_id}", response_model=RoleDetailModel) +@roleController.get("/role/{role_id}", response_model=RoleDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) async def query_detail_system_role(role_id: int, query_db: Session = Depends(get_db)): try: delete_role_result = detail_role_services(query_db, role_id) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index 2bfb939..68457f1 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -7,12 +7,13 @@ from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * from module_admin.utils.response_util import * from module_admin.utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth userController = APIRouter(dependencies=[Depends(get_current_user)]) -@userController.post("/user/get", response_model=UserPageObjectResponse) +@userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) async def get_system_user_list(user_query: UserPageObject, query_db: Session = Depends(get_db)): try: user_query_result = get_user_list_services(query_db, user_query) @@ -23,7 +24,7 @@ async def get_system_user_list(user_query: UserPageObject, query_db: Session = D return response_500(data="", message="接口异常") -@userController.post("/user/add", response_model=CrudUserResponse) +@userController.post("/user/add", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:add'))]) async def add_system_user(request: Request, add_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -41,7 +42,7 @@ async def add_system_user(request: Request, add_user: AddUserModel, token: Optio return response_500(data="", message="接口异常") -@userController.patch("/user/edit", response_model=CrudUserResponse) +@userController.patch("/user/edit", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -59,7 +60,7 @@ async def edit_system_user(request: Request, edit_user: AddUserModel, token: Opt return response_500(data="", message="接口异常") -@userController.post("/user/delete", response_model=CrudUserResponse) +@userController.post("/user/delete", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:delete'))]) async def delete_system_user(request: Request, delete_user: DeleteUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -77,7 +78,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok return response_500(data="", message="接口异常") -@userController.get("/user/{user_id}", response_model=UserDetailModel) +@userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def query_detail_system_user(user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) diff --git a/dash-fastapi-frontend/callbacks/app_c.py b/dash-fastapi-frontend/callbacks/app_c.py index a8e8363..83b0041 100644 --- a/dash-fastapi-frontend/callbacks/app_c.py +++ b/dash-fastapi-frontend/callbacks/app_c.py @@ -16,7 +16,7 @@ from server import app, logger ) def check_api_response(data): - if session.get('code') == 401: + if session.get('code') == 401 and 'token' in session.get('message'): return [True, fuc.FefferyFancyNotification(session.get('message'), type='error', autoClose=2000)] elif session.get('code') == 200: diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py index 5766ac5..b9b2243 100644 --- a/dash-fastapi-frontend/views/monitor/operlog/__init__.py +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是操作日志') -- Gitee From 297dae88b22f59d1ab407d4621e33117136b2b95 Mon Sep 17 00:00:00 2001 From: xlf Date: Tue, 11 Jul 2023 17:06:27 +0800 Subject: [PATCH 021/169] =?UTF-8?q?refactor:=E8=B0=83=E6=95=B4utils?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 +- dash-fastapi-backend/module_admin/aspect/interface_auth.py | 2 +- .../module_admin/controller/dept_controller.py | 4 ++-- .../module_admin/controller/login_controller.py | 4 ++-- .../module_admin/controller/menu_controller.py | 4 ++-- .../module_admin/controller/post_controler.py | 4 ++-- .../module_admin/controller/role_controller.py | 4 ++-- .../module_admin/controller/user_controller.py | 4 ++-- dash-fastapi-backend/module_admin/dao/dept_dao.py | 2 +- dash-fastapi-backend/module_admin/dao/login_dao.py | 2 +- dash-fastapi-backend/module_admin/dao/menu_dao.py | 2 +- dash-fastapi-backend/module_admin/dao/post_dao.py | 4 ++-- dash-fastapi-backend/module_admin/dao/role_dao.py | 4 ++-- dash-fastapi-backend/module_admin/dao/user_dao.py | 4 ++-- dash-fastapi-backend/module_admin/service/login_service.py | 4 ++-- dash-fastapi-backend/{module_admin => }/utils/log_util.py | 0 dash-fastapi-backend/{module_admin => }/utils/page_util.py | 0 .../{module_admin => }/utils/response_util.py | 0 .../{module_admin => }/utils/time_format_util.py | 0 dash-fastapi-frontend/views/monitor/logininfor/__init__.py | 2 +- 20 files changed, 26 insertions(+), 26 deletions(-) rename dash-fastapi-backend/{module_admin => }/utils/log_util.py (100%) rename dash-fastapi-backend/{module_admin => }/utils/page_util.py (100%) rename dash-fastapi-backend/{module_admin => }/utils/response_util.py (100%) rename dash-fastapi-backend/{module_admin => }/utils/time_format_util.py (100%) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 9ceb84f..92958f1 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -12,7 +12,7 @@ from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from config.env import RedisConfig -from module_admin.utils.response_util import response_401, AuthException +from utils.response_util import response_401, AuthException app = FastAPI() diff --git a/dash-fastapi-backend/module_admin/aspect/interface_auth.py b/dash-fastapi-backend/module_admin/aspect/interface_auth.py index ca25cc5..0100ee5 100644 --- a/dash-fastapi-backend/module_admin/aspect/interface_auth.py +++ b/dash-fastapi-backend/module_admin/aspect/interface_auth.py @@ -1,7 +1,7 @@ from fastapi import Depends from module_admin.entity.vo.user_vo import * from module_admin.service.login_service import get_current_user -from module_admin.utils.response_util import AuthException +from utils.response_util import AuthException class CheckUserInterfaceAuth: diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 217d78d..40d90e2 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -5,8 +5,8 @@ from module_admin.service.login_service import get_current_user from module_admin.service.dept_service import * from module_admin.entity.vo.dept_vo import * from module_admin.dao.dept_dao import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index 378b19b..d3a50dd 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -4,8 +4,8 @@ from module_admin.service.login_service import * from module_admin.entity.vo.login_vo import * from module_admin.dao.login_dao import * from config.env import JwtConfig -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from datetime import timedelta diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index b364cbc..40a6df2 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -5,8 +5,8 @@ from module_admin.service.login_service import get_current_user from module_admin.service.menu_service import * from module_admin.entity.vo.menu_vo import * from module_admin.dao.menu_dao import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index edd0ec9..5939291 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -4,8 +4,8 @@ from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.post_service import * from module_admin.entity.vo.post_vo import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 3b8ef89..1127a3e 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -4,8 +4,8 @@ from config.get_db import get_db from module_admin.service.login_service import get_current_user from module_admin.service.role_service import * from module_admin.entity.vo.role_vo import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index 68457f1..fabea12 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -5,8 +5,8 @@ from module_admin.service.login_service import get_current_user, get_password_ha from module_admin.service.user_service import * from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth diff --git a/dash-fastapi-backend/module_admin/dao/dept_dao.py b/dash-fastapi-backend/module_admin/dao/dept_dao.py index f4815d3..28c6f65 100644 --- a/dash-fastapi-backend/module_admin/dao/dept_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dept_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session from module_admin.entity.do.dept_do import SysDept from module_admin.entity.vo.dept_vo import DeptModel, DeptResponse, CrudDeptResponse -from module_admin.utils.time_format_util import list_format_datetime +from utils.time_format_util import list_format_datetime def get_dept_by_id(db: Session, dept_id: int): diff --git a/dash-fastapi-backend/module_admin/dao/login_dao.py b/dash-fastapi-backend/module_admin/dao/login_dao.py index e723c2e..710233a 100644 --- a/dash-fastapi-backend/module_admin/dao/login_dao.py +++ b/dash-fastapi-backend/module_admin/dao/login_dao.py @@ -1,6 +1,6 @@ from sqlalchemy.orm import Session from module_admin.entity.do.user_do import SysUser -from module_admin.utils.time_format_util import object_format_datetime +from utils.time_format_util import object_format_datetime def login_by_account(db: Session, user_name: str): diff --git a/dash-fastapi-backend/module_admin/dao/menu_dao.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py index 421fbd5..63dbefa 100644 --- a/dash-fastapi-backend/module_admin/dao/menu_dao.py +++ b/dash-fastapi-backend/module_admin/dao/menu_dao.py @@ -1,7 +1,7 @@ from sqlalchemy.orm import Session from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.menu_vo import MenuModel, MenuResponse, CrudMenuResponse -from module_admin.utils.time_format_util import list_format_datetime +from utils.time_format_util import list_format_datetime def get_menu_detail_by_id(db: Session, menu_id: int): diff --git a/dash-fastapi-backend/module_admin/dao/post_dao.py b/dash-fastapi-backend/module_admin/dao/post_dao.py index 4b3a841..a9a70b1 100644 --- a/dash-fastapi-backend/module_admin/dao/post_dao.py +++ b/dash-fastapi-backend/module_admin/dao/post_dao.py @@ -1,8 +1,8 @@ from sqlalchemy.orm import Session from module_admin.entity.do.post_do import SysPost from module_admin.entity.vo.post_vo import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse -from module_admin.utils.time_format_util import list_format_datetime -from module_admin.utils.page_util import get_page_info +from utils.time_format_util import list_format_datetime +from utils.page_util import get_page_info def get_post_by_id(db: Session, post_id: int): diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 45d5edb..6f4f38b 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -3,8 +3,8 @@ from sqlalchemy.orm import Session from module_admin.entity.do.role_do import SysRole, SysRoleMenu from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel -from module_admin.utils.time_format_util import list_format_datetime, object_format_datetime -from module_admin.utils.page_util import get_page_info +from utils.time_format_util import list_format_datetime, object_format_datetime +from utils.page_util import get_page_info from datetime import datetime, time diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index efc9682..158da3b 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -7,8 +7,8 @@ from module_admin.entity.do.post_do import SysPost from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.user_vo import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ UserPageObjectResponse, CrudUserResponse -from module_admin.utils.time_format_util import list_format_datetime, format_datetime_dict_list -from module_admin.utils.page_util import get_page_info +from utils.time_format_util import list_format_datetime, format_datetime_dict_list +from utils.page_util import get_page_info from datetime import datetime, time diff --git a/dash-fastapi-backend/module_admin/service/login_service.py b/dash-fastapi-backend/module_admin/service/login_service.py index 49be7ca..7166dd5 100644 --- a/dash-fastapi-backend/module_admin/service/login_service.py +++ b/dash-fastapi-backend/module_admin/service/login_service.py @@ -4,8 +4,8 @@ from module_admin.dao.user_dao import * from jose import JWTError, jwt from passlib.context import CryptContext from config.env import JwtConfig -from module_admin.utils.response_util import * -from module_admin.utils.log_util import * +from utils.response_util import * +from utils.log_util import * from datetime import datetime, timedelta from fastapi import Request from fastapi import Depends, Header diff --git a/dash-fastapi-backend/module_admin/utils/log_util.py b/dash-fastapi-backend/utils/log_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/log_util.py rename to dash-fastapi-backend/utils/log_util.py diff --git a/dash-fastapi-backend/module_admin/utils/page_util.py b/dash-fastapi-backend/utils/page_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/page_util.py rename to dash-fastapi-backend/utils/page_util.py diff --git a/dash-fastapi-backend/module_admin/utils/response_util.py b/dash-fastapi-backend/utils/response_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/response_util.py rename to dash-fastapi-backend/utils/response_util.py diff --git a/dash-fastapi-backend/module_admin/utils/time_format_util.py b/dash-fastapi-backend/utils/time_format_util.py similarity index 100% rename from dash-fastapi-backend/module_admin/utils/time_format_util.py rename to dash-fastapi-backend/utils/time_format_util.py diff --git a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py index 7734a12..c3e2dbb 100644 --- a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py +++ b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py @@ -3,6 +3,6 @@ import feffery_utils_components as fuc import feffery_antd_components as fac -def render(): +def render(button_perms): return html.Div('我是登录日志') -- Gitee From 07975af2ab072e450eae98fcaa97f28a64cc7690 Mon Sep 17 00:00:00 2001 From: xlf Date: Wed, 12 Jul 2023 16:47:10 +0800 Subject: [PATCH 022/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=A3=85=E9=A5=B0=E5=99=A8=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E6=8E=A5=E5=8F=A3=E6=97=A5=E5=BF=97=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=EF=BC=9B=E6=96=B0=E5=A2=9E=E6=97=A5=E5=BF=97=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=88=E5=90=8E=E7=AB=AF=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../module_admin/annotation/log_annotation.py | 128 ++++++++++++++ .../module_admin/aspect/interface_auth.py | 2 +- .../controller/dept_controller.py | 15 +- .../module_admin/controller/log_controller.py | 80 +++++++++ .../controller/login_controller.py | 2 + .../controller/menu_controller.py | 17 +- .../module_admin/controller/post_controler.py | 15 +- .../controller/role_controller.py | 13 +- .../controller/user_controller.py | 11 +- .../module_admin/dao/log_dao.py | 161 ++++++++++++++++++ .../module_admin/entity/vo/log_vo.py | 110 ++++++++++++ .../module_admin/entity/vo/login_vo.py | 1 - .../module_admin/service/log_service.py | 98 +++++++++++ dash-fastapi-backend/utils/response_util.py | 6 +- dash-fastapi-frontend/callbacks/login_c.py | 2 +- dash-fastapi-frontend/utils/request.py | 6 +- 17 files changed, 638 insertions(+), 31 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/annotation/log_annotation.py create mode 100644 dash-fastapi-backend/module_admin/controller/log_controller.py create mode 100644 dash-fastapi-backend/module_admin/dao/log_dao.py create mode 100644 dash-fastapi-backend/module_admin/entity/vo/log_vo.py create mode 100644 dash-fastapi-backend/module_admin/service/log_service.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 92958f1..613d269 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -11,6 +11,7 @@ from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController +from module_admin.controller.log_controller import logController from config.env import RedisConfig from utils.response_util import response_401, AuthException @@ -75,6 +76,7 @@ app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) +app.include_router(logController, prefix="/system", tags=['system/log']) if __name__ == '__main__': diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py new file mode 100644 index 0000000..c67cd57 --- /dev/null +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -0,0 +1,128 @@ +from functools import wraps +from fastapi import Request +import inspect +import os +import json +import time +from datetime import datetime +import requests +from user_agents import parse +from typing import Optional +from module_admin.service.login_service import get_current_user +from module_admin.service.log_service import add_operation_log_services, add_login_log_services +from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel + + +def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'operation'): + """ + 日志装饰器 + :param log_type: 日志类型(login表示登录日志,为空表示为操作日志) + :param title: 当前日志装饰器装饰的模块标题 + :param business_type: 业务类型(0其它 1新增 2修改 3删除 4授权 5导出 6导入 7强退 8生成代码 9清空数据) + :return: + """ + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + start_time = time.time() + # 获取被装饰函数的文件路径 + file_path = inspect.getfile(func) + # 获取项目根路径 + project_root = os.getcwd() + # 处理文件路径,去除项目根路径部分 + relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.') + # 获取当前被装饰函数所在路径 + func_path = f'{relative_path}{func.__name__}' + # 获取上下文信息 + request: Request = kwargs.get('request') + token = request.headers.get('token') + query_db = kwargs.get('query_db') + request_method = request.method + operator_type = 0 + user_agent = request.headers.get('User-Agent') + if "Windows" in user_agent or "Macintosh" in user_agent or "Linux" in user_agent: + operator_type = 1 + if "Mobile" in user_agent or "Android" in user_agent or "iPhone" in user_agent: + operator_type = 2 + oper_url = request.url.path + oper_ip = request.headers.get('remote_addr') + oper_location = '内网IP' + try: + if oper_ip != '127.0.0.1' and oper_ip != 'localhost': + ip_result = requests.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}') + if ip_result.status_code == 200: + prov = ip_result.json().get('data').get('prov') + city = ip_result.json().get('data').get('city') + if prov or city: + oper_location = f'{prov}-{city}' + else: + oper_location = '未知' + else: + oper_location = '未知' + except Exception as e: + oper_location = '未知' + print(e) + finally: + payload = await request.body() + oper_param = json.dumps(json.loads(str(payload, 'utf-8')), ensure_ascii=False) + + # 调用原始函数 + oper_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + result = await func(*args, **kwargs) + cost_time = float(time.time() - start_time) * 100 + result_dict = json.loads(str(result.body, 'utf-8')) + json_result = json.dumps(dict(code=result_dict.get('code'), message=result_dict.get('message')), ensure_ascii=False) + status = 1 + error_msg = '' + if result_dict.get('code') == 200: + status = 0 + else: + error_msg = result_dict.get('message') + if log_type == 'login': + print(request.headers) + # user_agent_info = parse(user_agent) + # browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' + # system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' + # user = kwargs.get('user') + # user_name = user.user_name + # login_log = dict( + # user_name=user_name, + # ipaddr=oper_ip, + # login_location=oper_location, + # browser=browser, + # os=system_os, + # status=str(status), + # msg=result_dict.get('message'), + # login_time=oper_time + # ) + # + # add_login_log_services(query_db, LogininforModel(**login_log)) + else: + current_user = await get_current_user(request, token, query_db) + oper_name = current_user.user.user_name + dept_name = current_user.dept.dept_name + operation_log = dict( + title=title, + business_type=business_type, + method=func_path, + request_method=request_method, + operator_type=operator_type, + oper_name=oper_name, + dept_name=dept_name, + oper_url=oper_url, + oper_ip=oper_ip, + oper_location=oper_location, + oper_param=oper_param, + json_result=json_result, + status=status, + error_msg=error_msg, + oper_time=oper_time, + cost_time=cost_time + ) + add_operation_log_services(query_db, OperLogModel(**operation_log)) + + return result + + return wrapper + + return decorator diff --git a/dash-fastapi-backend/module_admin/aspect/interface_auth.py b/dash-fastapi-backend/module_admin/aspect/interface_auth.py index 0100ee5..ebaaf14 100644 --- a/dash-fastapi-backend/module_admin/aspect/interface_auth.py +++ b/dash-fastapi-backend/module_admin/aspect/interface_auth.py @@ -1,5 +1,5 @@ from fastapi import Depends -from module_admin.entity.vo.user_vo import * +from module_admin.entity.vo.user_vo import CurrentUserInfoServiceResponse from module_admin.service.login_service import get_current_user from utils.response_util import AuthException diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 40d90e2..2a11c24 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -8,13 +8,14 @@ from module_admin.dao.dept_dao import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator deptController = APIRouter(dependencies=[Depends(get_current_user)]) @deptController.post("/dept/tree", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depends(get_db)): +async def get_system_dept_tree(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_services(query_db, dept_query) logger.info('获取成功') @@ -25,7 +26,7 @@ async def get_system_dept_tree(dept_query: DeptModel, query_db: Session = Depend @deptController.post("/dept/forEditOption", response_model=DeptTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: Session = Depends(get_db)): +async def get_system_dept_tree_for_edit_option(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_tree_for_edit_option_services(query_db, dept_query) logger.info('获取成功') @@ -36,7 +37,8 @@ async def get_system_dept_tree_for_edit_option(dept_query: DeptModel, query_db: @deptController.post("/dept/get", response_model=DeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))]) -async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depends(get_db)): +@log_decorator(title='部门管理', business_type=0) +async def get_system_dept_list(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_list_services(query_db, dept_query) logger.info('获取成功') @@ -47,6 +49,7 @@ async def get_system_dept_list(dept_query: DeptModel, query_db: Session = Depend @deptController.post("/dept/add", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:add'))]) +@log_decorator(title='部门管理', business_type=1) async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -64,6 +67,7 @@ async def add_system_dept(request: Request, add_dept: DeptModel, token: Optional @deptController.patch("/dept/edit", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) +@log_decorator(title='部门管理', business_type=2) async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -81,7 +85,8 @@ async def edit_system_dept(request: Request, edit_dept: DeptModel, token: Option return response_500(data="", message="接口异常") -@deptController.post("/dept/delete", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:delete'))]) +@deptController.post("/dept/delete", response_model=CrudDeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:remove'))]) +@log_decorator(title='部门管理', business_type=3) async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -100,7 +105,7 @@ async def delete_system_dept(request: Request, delete_dept: DeleteDeptModel, tok @deptController.get("/dept/{dept_id}", response_model=DeptModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:edit'))]) -async def query_detail_system_dept(dept_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_dept(request: Request, dept_id: int, query_db: Session = Depends(get_db)): try: detail_dept_result = detail_dept_services(query_db, dept_id) logger.info(f'获取dept_id为{dept_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py new file mode 100644 index 0000000..602e7e4 --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -0,0 +1,80 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, Header +from config.get_db import get_db +from module_admin.service.login_service import get_current_user +from module_admin.service.log_service import * +from module_admin.entity.vo.log_vo import * +from utils.response_util import * +from utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator + + +logController = APIRouter(prefix='/log', dependencies=[Depends(get_current_user)]) + + +@logController.post("/operation/get", response_model=OperLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))]) +@log_decorator(title='操作日志管理', business_type=0) +async def get_system_operation_log_list(request: Request, operation_log_query: OperLogPageObject, query_db: Session = Depends(get_db)): + try: + operation_log_query_result = get_operation_log_list_services(query_db, operation_log_query) + logger.info('获取成功') + return response_200(data=operation_log_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.post("/operation/delete", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:remove'))]) +@log_decorator(title='操作日志管理', business_type=3) +async def delete_system_operation_log(request: Request, delete_operation_log: DeleteOperLogModel, query_db: Session = Depends(get_db)): + try: + delete_operation_log_result = delete_operation_log_services(query_db, delete_operation_log) + if delete_operation_log_result.is_success: + logger.info(delete_operation_log_result.message) + return response_200(data=delete_operation_log_result, message=delete_operation_log_result.message) + else: + logger.warning(delete_operation_log_result.message) + return response_400(data="", message=delete_operation_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.get("/operation/{oper_id}", response_model=OperLogModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:query'))]) +async def query_detail_system_operation_log(request: Request, oper_id: int, query_db: Session = Depends(get_db)): + try: + detail_operation_log_result = detail_operation_log_services(query_db, oper_id) + logger.info(f'获取oper_id为{oper_id}的信息成功') + return response_200(data=detail_operation_log_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.post("/login/get", response_model=LoginLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))]) +@log_decorator(title='登录日志管理', business_type=0) +async def get_system_login_log_list(request: Request, login_log_query: LoginLogPageObject, query_db: Session = Depends(get_db)): + try: + login_log_query_result = get_login_log_list_services(query_db, login_log_query) + logger.info('获取成功') + return response_200(data=login_log_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@logController.post("/login/delete", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) +@log_decorator(title='登录日志管理', business_type=3) +async def delete_system_login_log(request: Request, delete_login_log: DeleteLoginLogModel, query_db: Session = Depends(get_db)): + try: + delete_login_log_result = delete_login_log_services(query_db, delete_login_log) + if delete_login_log_result.is_success: + logger.info(delete_login_log_result.message) + return response_200(data=delete_login_log_result, message=delete_login_log_result.message) + else: + logger.warning(delete_login_log_result.message) + return response_400(data="", message=delete_login_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index d3a50dd..6876ba4 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -7,6 +7,7 @@ from config.env import JwtConfig from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator from datetime import timedelta @@ -14,6 +15,7 @@ loginController = APIRouter() @loginController.post("/loginByAccount", response_model=Token) +@log_decorator(title='用户登录', business_type=0, log_type='login') async def login(request: Request, user: UserLogin, query_db: Session = Depends(get_db)): try: result = authenticate_user(query_db, user.user_name, user.password) diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 40a6df2..06579d5 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -8,13 +8,14 @@ from module_admin.dao.menu_dao import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator menuController = APIRouter(dependencies=[Depends(get_current_user)]) @menuController.post("/menu/tree", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_services(query_db, menu_query) logger.info('获取成功') @@ -25,7 +26,7 @@ async def get_system_menu_tree(menu_query: MenuTreeModel, query_db: Session = De @menuController.post("/menu/forEditOption", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) logger.info('获取成功') @@ -36,7 +37,8 @@ async def get_system_menu_tree_for_edit_option(menu_query: MenuModel, query_db: @menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) -async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depends(get_db)): +@log_decorator(title='菜单管理', business_type=0) +async def get_system_menu_list(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_list_services(query_db, menu_query) logger.info('获取成功') @@ -47,6 +49,7 @@ async def get_system_menu_list(menu_query: MenuModel, query_db: Session = Depend @menuController.post("/menu/add", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:add'))]) +@log_decorator(title='菜单管理', business_type=1) async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -64,6 +67,7 @@ async def add_system_menu(request: Request, add_menu: MenuModel, token: Optional @menuController.patch("/menu/edit", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) +@log_decorator(title='菜单管理', business_type=2) async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -81,8 +85,9 @@ async def edit_system_menu(request: Request, edit_menu: MenuModel, token: Option return response_500(data="", message="接口异常") -@menuController.post("/menu/delete", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:delete'))]) -async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): +@menuController.post("/menu/delete", response_model=CrudMenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:remove'))]) +@log_decorator(title='菜单管理', business_type=3) +async def delete_system_menu(request: Request, delete_menu: DeleteMenuModel, query_db: Session = Depends(get_db)): try: delete_menu_result = delete_menu_services(query_db, delete_menu) if delete_menu_result.is_success: @@ -97,7 +102,7 @@ async def delete_system_menu(delete_menu: DeleteMenuModel, query_db: Session = D @menuController.get("/menu/{menu_id}", response_model=MenuModel, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:edit'))]) -async def query_detail_system_menu(menu_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_menu(request: Request, menu_id: int, query_db: Session = Depends(get_db)): try: detail_menu_result = detail_menu_services(query_db, menu_id) logger.info(f'获取menu_id为{menu_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index 5939291..a11d4e9 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -7,13 +7,14 @@ from module_admin.entity.vo.post_vo import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator postController = APIRouter(dependencies=[Depends(get_current_user)]) @postController.post("/post/forSelectOption", response_model=PostSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_post_select(query_db: Session = Depends(get_db)): +async def get_system_post_select(request: Request, query_db: Session = Depends(get_db)): try: role_query_result = get_post_select_option_services(query_db) logger.info('获取成功') @@ -24,7 +25,8 @@ async def get_system_post_select(query_db: Session = Depends(get_db)): @postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) -async def get_system_post_list(post_query: PostPageObject, query_db: Session = Depends(get_db)): +@log_decorator(title='岗位管理', business_type=0) +async def get_system_post_list(request: Request, post_query: PostPageObject, query_db: Session = Depends(get_db)): try: post_query_result = get_post_list_services(query_db, post_query) logger.info('获取成功') @@ -35,6 +37,7 @@ async def get_system_post_list(post_query: PostPageObject, query_db: Session = D @postController.post("/post/add", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:add'))]) +@log_decorator(title='岗位管理', business_type=1) async def add_system_post(request: Request, add_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -52,6 +55,7 @@ async def add_system_post(request: Request, add_post: PostModel, token: Optional @postController.patch("/post/edit", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) +@log_decorator(title='岗位管理', business_type=2) async def edit_system_post(request: Request, edit_post: PostModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -69,8 +73,9 @@ async def edit_system_post(request: Request, edit_post: PostModel, token: Option return response_500(data="", message="接口异常") -@postController.post("/post/delete", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:delete'))]) -async def delete_system_post(delete_post: DeletePostModel, query_db: Session = Depends(get_db)): +@postController.post("/post/delete", response_model=CrudPostResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:remove'))]) +@log_decorator(title='岗位管理', business_type=3) +async def delete_system_post(request: Request, delete_post: DeletePostModel, query_db: Session = Depends(get_db)): try: delete_post_result = delete_post_services(query_db, delete_post) if delete_post_result.is_success: @@ -85,7 +90,7 @@ async def delete_system_post(delete_post: DeletePostModel, query_db: Session = D @postController.get("/post/{post_id}", response_model=PostModel, dependencies=[Depends(CheckUserInterfaceAuth('system:post:edit'))]) -async def query_detail_system_post(post_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_post(request: Request, post_id: int, query_db: Session = Depends(get_db)): try: detail_post_result = detail_post_services(query_db, post_id) logger.info(f'获取post_id为{post_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 1127a3e..c107855 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -7,13 +7,14 @@ from module_admin.entity.vo.role_vo import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator roleController = APIRouter(dependencies=[Depends(get_current_user)]) @roleController.post("/role/forSelectOption", response_model=RoleSelectOptionResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_role_select(query_db: Session = Depends(get_db)): +async def get_system_role_select(request: Request, query_db: Session = Depends(get_db)): try: role_query_result = get_role_select_option_services(query_db) logger.info('获取成功') @@ -24,7 +25,8 @@ async def get_system_role_select(query_db: Session = Depends(get_db)): @roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) -async def get_system_role_list(role_query: RolePageObject, query_db: Session = Depends(get_db)): +@log_decorator(title='角色管理', business_type=0) +async def get_system_role_list(request: Request, role_query: RolePageObject, query_db: Session = Depends(get_db)): try: role_query_result = get_role_list_services(query_db, role_query) logger.info('获取成功') @@ -35,6 +37,7 @@ async def get_system_role_list(role_query: RolePageObject, query_db: Session = D @roleController.post("/role/add", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:add'))]) +@log_decorator(title='角色管理', business_type=1) async def add_system_role(request: Request, add_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -52,6 +55,7 @@ async def add_system_role(request: Request, add_role: AddRoleModel, token: Optio @roleController.patch("/role/edit", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) +@log_decorator(title='角色管理', business_type=2) async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -69,7 +73,8 @@ async def edit_system_role(request: Request, edit_role: AddRoleModel, token: Opt return response_500(data="", message="接口异常") -@roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:delete'))]) +@roleController.post("/role/delete", response_model=CrudRoleResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:remove'))]) +@log_decorator(title='角色管理', business_type=3) async def delete_system_role(request: Request, delete_role: DeleteRoleModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -88,7 +93,7 @@ async def delete_system_role(request: Request, delete_role: DeleteRoleModel, tok @roleController.get("/role/{role_id}", response_model=RoleDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:role:edit'))]) -async def query_detail_system_role(role_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_role(request: Request, role_id: int, query_db: Session = Depends(get_db)): try: delete_role_result = detail_role_services(query_db, role_id) logger.info(f'获取role_id为{role_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index fabea12..e58b7a3 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -8,13 +8,15 @@ from module_admin.dao.user_dao import * from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -async def get_system_user_list(user_query: UserPageObject, query_db: Session = Depends(get_db)): +@log_decorator(title='用户管理', business_type=0) +async def get_system_user_list(request: Request, user_query: UserPageObject, query_db: Session = Depends(get_db)): try: user_query_result = get_user_list_services(query_db, user_query) logger.info('获取成功') @@ -25,6 +27,7 @@ async def get_system_user_list(user_query: UserPageObject, query_db: Session = D @userController.post("/user/add", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:add'))]) +@log_decorator(title='用户管理', business_type=1) async def add_system_user(request: Request, add_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -43,6 +46,7 @@ async def add_system_user(request: Request, add_user: AddUserModel, token: Optio @userController.patch("/user/edit", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@log_decorator(title='用户管理', business_type=2) async def edit_system_user(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -60,7 +64,8 @@ async def edit_system_user(request: Request, edit_user: AddUserModel, token: Opt return response_500(data="", message="接口异常") -@userController.post("/user/delete", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:delete'))]) +@userController.post("/user/delete", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:remove'))]) +@log_decorator(title='用户管理', business_type=3) async def delete_system_user(request: Request, delete_user: DeleteUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) @@ -79,7 +84,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok @userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) -async def query_detail_system_user(user_id: int, query_db: Session = Depends(get_db)): +async def query_detail_system_user(request: Request, user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) logger.info(f'获取user_id为{user_id}的信息成功') diff --git a/dash-fastapi-backend/module_admin/dao/log_dao.py b/dash-fastapi-backend/module_admin/dao/log_dao.py new file mode 100644 index 0000000..3c4a1d2 --- /dev/null +++ b/dash-fastapi-backend/module_admin/dao/log_dao.py @@ -0,0 +1,161 @@ +from sqlalchemy.orm import Session +from module_admin.entity.do.log_do import SysOperLog, SysLogininfor +from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel, OperLogPageObject, OperLogPageObjectResponse, \ + LoginLogPageObject, LoginLogPageObjectResponse, CrudLogResponse +from utils.time_format_util import list_format_datetime +from utils.page_util import get_page_info +from datetime import datetime, time + + +def get_operation_log_detail_by_id(db: Session, oper_id: int): + operation_log_info = db.query(SysOperLog) \ + .filter(SysOperLog.oper_id == oper_id) \ + .first() + + return operation_log_info + + +def get_operation_log_list(db: Session, page_object: OperLogPageObject): + """ + 根据查询参数获取操作日志列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 操作日志列表信息对象 + """ + count = db.query(SysOperLog) \ + .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, + SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, + SysOperLog.business_type == page_object.business_type if page_object.business_type else True, + SysOperLog.status == page_object.status if page_object.status else True, + SysOperLog.oper_time.between( + datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.oper_time_start and page_object.oper_time_end else True + )\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + operation_log_list = db.query(SysOperLog) \ + .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, + SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, + SysOperLog.business_type == page_object.business_type if page_object.business_type else True, + SysOperLog.status == page_object.status if page_object.status else True, + SysOperLog.oper_time.between( + datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.oper_time_start and page_object.oper_time_end else True + )\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(operation_log_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return OperLogPageObjectResponse(**result) + + +def add_operation_log_dao(db: Session, operation_log: OperLogModel): + """ + 新增操作日志数据库操作 + :param db: orm对象 + :param operation_log: 操作日志对象 + :return: 新增校验结果 + """ + db_operation_log = SysOperLog(**operation_log.dict()) + db.add(db_operation_log) + db.commit() # 提交保存到数据库中 + db.refresh(db_operation_log) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudLogResponse(**result) + + +def delete_operation_log_dao(db: Session, operation_log: OperLogModel): + """ + 删除操作日志数据库操作 + :param db: orm对象 + :param operation_log: 操作日志对象 + :return: + """ + db.query(SysOperLog) \ + .filter(SysOperLog.oper_id == operation_log.oper_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def get_login_log_list(db: Session, page_object: LoginLogPageObject): + """ + 根据查询参数获取登录日志列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 登录日志列表信息对象 + """ + count = db.query(SysLogininfor) \ + .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, + SysLogininfor.status == page_object.status if page_object.status else True, + SysLogininfor.login_time.between( + datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.login_time_start and page_object.login_time_end else True + )\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + login_log_list = db.query(SysLogininfor) \ + .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, + SysLogininfor.status == page_object.status if page_object.status else True, + SysLogininfor.login_time.between( + datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.login_time_start and page_object.login_time_end else True + )\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(login_log_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return LoginLogPageObjectResponse(**result) + + +def add_login_log_dao(db: Session, login_log: LogininforModel): + """ + 新增登录日志数据库操作 + :param db: orm对象 + :param login_log: 登录日志对象 + :return: 新增校验结果 + """ + db_login_log = SysLogininfor(**login_log.dict()) + db.add(db_login_log) + db.commit() # 提交保存到数据库中 + db.refresh(db_login_log) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudLogResponse(**result) + + +def delete_login_log_dao(db: Session, login_log: LogininforModel): + """ + 删除登录日志数据库操作 + :param db: orm对象 + :param login_log: 登录日志对象 + :return: + """ + db.query(SysLogininfor) \ + .filter(SysLogininfor.info_id == login_log.info_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py new file mode 100644 index 0000000..2f87702 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -0,0 +1,110 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class OperLogModel(BaseModel): + """ + 操作日志表对应pydantic模型 + """ + oper_id: Optional[int] + title: Optional[str] + business_type: Optional[int] + method: Optional[str] + request_method: Optional[str] + operator_type: Optional[int] + oper_name: Optional[str] + dept_name: Optional[str] + oper_url: Optional[str] + oper_ip: Optional[str] + oper_location: Optional[str] + oper_param: Optional[str] + json_result: Optional[str] + status: Optional[int] + error_msg: Optional[str] + oper_time: Optional[str] + cost_time: Optional[int] + + class Config: + orm_mode = True + + +class LogininforModel(BaseModel): + """ + 登录日志表对应pydantic模型 + """ + info_id: Optional[int] + user_name: Optional[str] + ipaddr: Optional[str] + login_location: Optional[str] + browser: Optional[str] + os: Optional[str] + status: Optional[str] + msg: Optional[str] + login_time: Optional[str] + + class Config: + orm_mode = True + + +class OperLogPageObject(OperLogModel): + """ + 操作日志管理分页查询模型 + """ + oper_time_start: Optional[str] + oper_time_end: Optional[str] + page_num: Optional[int] + page_size: Optional[int] + + +class OperLogPageObjectResponse(BaseModel): + """ + 操作日志列表分页查询返回模型 + """ + rows: List[Union[OperLogModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteOperLogModel(BaseModel): + """ + 删除操作日志模型 + """ + oper_ids: str + + +class LoginLogPageObject(LogininforModel): + """ + 登录日志管理分页查询模型 + """ + login_time_start: Optional[str] + login_time_end: Optional[str] + page_num: Optional[int] + page_size: Optional[int] + + +class LoginLogPageObjectResponse(BaseModel): + """ + 登录日志列表分页查询返回模型 + """ + rows: List[Union[LogininforModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteLoginLogModel(BaseModel): + """ + 删除登录日志模型 + """ + info_ids: str + + +class CrudLogResponse(BaseModel): + """ + 操作各类日志响应模型 + """ + is_success: bool + message: str diff --git a/dash-fastapi-backend/module_admin/entity/vo/login_vo.py b/dash-fastapi-backend/module_admin/entity/vo/login_vo.py index d7b3056..fa08a4a 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/login_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/login_vo.py @@ -5,7 +5,6 @@ from typing import Optional class UserLogin(BaseModel): user_name: str password: str - user_request: Optional[str] = None class Token(BaseModel): diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py new file mode 100644 index 0000000..23ac492 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -0,0 +1,98 @@ +from module_admin.entity.vo.log_vo import * +from module_admin.dao.log_dao import * + + +def get_operation_log_list_services(result_db: Session, page_object: OperLogPageObject): + """ + 获取操作日志列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 操作日志列表信息对象 + """ + operation_log_list_result = get_operation_log_list(result_db, page_object) + + return operation_log_list_result + + +def add_operation_log_services(result_db: Session, page_object: OperLogModel): + """ + 新增操作日志service + :param result_db: orm对象 + :param page_object: 新增操作日志对象 + :return: 新增操作日志校验结果 + """ + add_operation_log_result = add_operation_log_dao(result_db, page_object) + + return add_operation_log_result + + +def delete_operation_log_services(result_db: Session, page_object: DeleteOperLogModel): + """ + 删除操作日志信息service + :param result_db: orm对象 + :param page_object: 删除操作日志对象 + :return: 删除操作日志校验结果 + """ + if page_object.oper_ids.split(','): + oper_id_list = page_object.oper_ids.split(',') + for oper_id in oper_id_list: + oper_id_dict = dict(oper_id=oper_id) + delete_operation_log_dao(result_db, OperLogModel(**oper_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入操作日志id为空') + return CrudLogResponse(**result) + + +def detail_operation_log_services(result_db: Session, oper_id: int): + """ + 获取操作日志详细信息service + :param result_db: orm对象 + :param oper_id: 操作日志id + :return: 操作日志id对应的信息 + """ + operation_log = get_operation_log_detail_by_id(result_db, oper_id=oper_id) + + return operation_log + + +def get_login_log_list_services(result_db: Session, page_object: LoginLogPageObject): + """ + 获取登录日志列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 登录日志列表信息对象 + """ + operation_log_list_result = get_login_log_list(result_db, page_object) + + return operation_log_list_result + + +def add_login_log_services(result_db: Session, page_object: LogininforModel): + """ + 新增登录日志service + :param result_db: orm对象 + :param page_object: 新增登录日志对象 + :return: 新增登录日志校验结果 + """ + add_login_log_result = add_login_log_dao(result_db, page_object) + + return add_login_log_result + + +def delete_login_log_services(result_db: Session, page_object: DeleteLoginLogModel): + """ + 删除操作日志信息service + :param result_db: orm对象 + :param page_object: 删除操作日志对象 + :return: 删除操作日志校验结果 + """ + if page_object.info_ids.split(','): + info_id_list = page_object.info_ids.split(',') + for info_id in info_id_list: + info_id_dict = dict(info_id=info_id) + delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入登录日志id为空') + return CrudLogResponse(**result) diff --git a/dash-fastapi-backend/utils/response_util.py b/dash-fastapi-backend/utils/response_util.py index 6140611..b5f7607 100644 --- a/dash-fastapi-backend/utils/response_util.py +++ b/dash-fastapi-backend/utils/response_util.py @@ -23,7 +23,7 @@ def response_200(*, data: Union[list, dict, str], message="获取成功") -> Res def response_400(*, data: str = None, message: str = "获取失败") -> Response: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, - content=( + content=jsonable_encoder( { 'code': 400, 'message': message, @@ -38,7 +38,7 @@ def response_400(*, data: str = None, message: str = "获取失败") -> Response def response_401(*, data: str = None, message: str = "获取失败") -> Response: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, - content=( + content=jsonable_encoder( { 'code': 401, 'message': message, @@ -53,7 +53,7 @@ def response_401(*, data: str = None, message: str = "获取失败") -> Response def response_500(*, data: str = None, message: str = "接口异常") -> Response: return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content=( + content=jsonable_encoder( { 'code': 500, 'message': message, diff --git a/dash-fastapi-frontend/callbacks/login_c.py b/dash-fastapi-frontend/callbacks/login_c.py index 6272036..7a35d07 100644 --- a/dash-fastapi-frontend/callbacks/login_c.py +++ b/dash-fastapi-frontend/callbacks/login_c.py @@ -35,7 +35,7 @@ def login_auth(nClicks, username, password, captcha, input_captcha): if captcha == input_captcha: try: - user_params = dict(user_name=username, password=password, user_request=str(request.headers)) + user_params = dict(user_name=username, password=password) userinfo_result = login_api(user_params) if userinfo_result['code'] == 200: token = userinfo_result['data']['token'] diff --git a/dash-fastapi-frontend/utils/request.py b/dash-fastapi-frontend/utils/request.py index cfc6a66..9654d91 100644 --- a/dash-fastapi-frontend/utils/request.py +++ b/dash-fastapi-frontend/utils/request.py @@ -9,9 +9,11 @@ def api_request(method: str, url: str, is_headers: bool, params: Optional[dict] json: Optional[dict] = None, timeout: Optional[int] = None): api_url = ApiBaseUrlConfig.BaseUrl + url method = method.lower().strip() - api_headers = None + user_agent = request.headers.get('User-Agent') if is_headers: - api_headers = {'token': 'Bearer' + session.get('token')} + api_headers = {'token': 'Bearer' + session.get('token'), 'remote_addr': request.remote_addr, 'User-Agent': user_agent} + else: + api_headers = {'remote_addr': request.remote_addr, 'User-Agent': user_agent} try: if method == 'get': response = requests.get(url=api_url, params=params, data=data, json=json, headers=api_headers, -- Gitee From 27708d1900992965693c55dca03f4cf2c53c714e Mon Sep 17 00:00:00 2001 From: xlf Date: Wed, 12 Jul 2023 20:55:43 +0800 Subject: [PATCH 023/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/annotation/log_annotation.py | 38 +- .../controller/dept_controller.py | 1 - .../module_admin/controller/log_controller.py | 18 +- .../controller/menu_controller.py | 1 - .../module_admin/controller/post_controler.py | 1 - .../controller/role_controller.py | 1 - .../controller/user_controller.py | 1 - .../module_admin/dao/log_dao.py | 26 +- .../module_admin/entity/vo/log_vo.py | 7 + .../module_admin/service/log_service.py | 34 +- dash-fastapi-frontend/api/log.py | 31 + .../callbacks/monitor_c/operlog_c.py | 268 ++++++++ .../callbacks/system_c/post_c.py | 7 +- .../callbacks/system_c/role_c.py | 7 +- .../callbacks/system_c/user_c.py | 7 +- dash-fastapi-frontend/store/store.py | 4 + .../views/monitor/operlog/__init__.py | 595 +++++++++++++++++- 17 files changed, 1000 insertions(+), 47 deletions(-) create mode 100644 dash-fastapi-frontend/api/log.py create mode 100644 dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py index c67cd57..ddeac6c 100644 --- a/dash-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -32,7 +32,7 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope # 处理文件路径,去除项目根路径部分 relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.') # 获取当前被装饰函数所在路径 - func_path = f'{relative_path}{func.__name__}' + func_path = f'{relative_path}{func.__name__}()' # 获取上下文信息 request: Request = kwargs.get('request') token = request.headers.get('token') @@ -79,24 +79,24 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope else: error_msg = result_dict.get('message') if log_type == 'login': - print(request.headers) - # user_agent_info = parse(user_agent) - # browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' - # system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' - # user = kwargs.get('user') - # user_name = user.user_name - # login_log = dict( - # user_name=user_name, - # ipaddr=oper_ip, - # login_location=oper_location, - # browser=browser, - # os=system_os, - # status=str(status), - # msg=result_dict.get('message'), - # login_time=oper_time - # ) - # - # add_login_log_services(query_db, LogininforModel(**login_log)) + # print(request.headers) + user_agent_info = parse(user_agent) + browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' + system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' + user = kwargs.get('user') + user_name = user.user_name + login_log = dict( + user_name=user_name, + ipaddr=oper_ip, + login_location=oper_location, + browser=browser, + os=system_os, + status=str(status), + msg=result_dict.get('message'), + login_time=oper_time + ) + + add_login_log_services(query_db, LogininforModel(**login_log)) else: current_user = await get_current_user(request, token, query_db) oper_name = current_user.user.user_name diff --git a/dash-fastapi-backend/module_admin/controller/dept_controller.py b/dash-fastapi-backend/module_admin/controller/dept_controller.py index 2a11c24..786d9e0 100644 --- a/dash-fastapi-backend/module_admin/controller/dept_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dept_controller.py @@ -37,7 +37,6 @@ async def get_system_dept_tree_for_edit_option(request: Request, dept_query: Dep @deptController.post("/dept/get", response_model=DeptResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dept:list'))]) -@log_decorator(title='部门管理', business_type=0) async def get_system_dept_list(request: Request, dept_query: DeptModel, query_db: Session = Depends(get_db)): try: dept_query_result = get_dept_list_services(query_db, dept_query) diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index 602e7e4..b714327 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -14,7 +14,6 @@ logController = APIRouter(prefix='/log', dependencies=[Depends(get_current_user) @logController.post("/operation/get", response_model=OperLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))]) -@log_decorator(title='操作日志管理', business_type=0) async def get_system_operation_log_list(request: Request, operation_log_query: OperLogPageObject, query_db: Session = Depends(get_db)): try: operation_log_query_result = get_operation_log_list_services(query_db, operation_log_query) @@ -41,6 +40,22 @@ async def delete_system_operation_log(request: Request, delete_operation_log: De return response_500(data="", message="接口异常") +@logController.post("/operation/clear", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:remove'))]) +@log_decorator(title='操作日志管理', business_type=9) +async def clear_system_operation_log(request: Request, clear_operation_log: ClearOperLogModel, query_db: Session = Depends(get_db)): + try: + clear_operation_log_result = clear_operation_log_services(query_db, clear_operation_log) + if clear_operation_log_result.is_success: + logger.info(clear_operation_log_result.message) + return response_200(data=clear_operation_log_result, message=clear_operation_log_result.message) + else: + logger.warning(clear_operation_log_result.message) + return response_400(data="", message=clear_operation_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + @logController.get("/operation/{oper_id}", response_model=OperLogModel, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:query'))]) async def query_detail_system_operation_log(request: Request, oper_id: int, query_db: Session = Depends(get_db)): try: @@ -53,7 +68,6 @@ async def query_detail_system_operation_log(request: Request, oper_id: int, quer @logController.post("/login/get", response_model=LoginLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))]) -@log_decorator(title='登录日志管理', business_type=0) async def get_system_login_log_list(request: Request, login_log_query: LoginLogPageObject, query_db: Session = Depends(get_db)): try: login_log_query_result = get_login_log_list_services(query_db, login_log_query) diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 06579d5..8703183 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -37,7 +37,6 @@ async def get_system_menu_tree_for_edit_option(request: Request, menu_query: Men @menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) -@log_decorator(title='菜单管理', business_type=0) async def get_system_menu_list(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): try: menu_query_result = get_menu_list_services(query_db, menu_query) diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index a11d4e9..740f9e8 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -25,7 +25,6 @@ async def get_system_post_select(request: Request, query_db: Session = Depends(g @postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) -@log_decorator(title='岗位管理', business_type=0) async def get_system_post_list(request: Request, post_query: PostPageObject, query_db: Session = Depends(get_db)): try: post_query_result = get_post_list_services(query_db, post_query) diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index c107855..562fff2 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -25,7 +25,6 @@ async def get_system_role_select(request: Request, query_db: Session = Depends(g @roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) -@log_decorator(title='角色管理', business_type=0) async def get_system_role_list(request: Request, role_query: RolePageObject, query_db: Session = Depends(get_db)): try: role_query_result = get_role_list_services(query_db, role_query) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index e58b7a3..bfaafef 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -15,7 +15,6 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -@log_decorator(title='用户管理', business_type=0) async def get_system_user_list(request: Request, user_query: UserPageObject, query_db: Session = Depends(get_db)): try: user_query_result = get_user_list_services(query_db, user_query) diff --git a/dash-fastapi-backend/module_admin/dao/log_dao.py b/dash-fastapi-backend/module_admin/dao/log_dao.py index 3c4a1d2..8447a15 100644 --- a/dash-fastapi-backend/module_admin/dao/log_dao.py +++ b/dash-fastapi-backend/module_admin/dao/log_dao.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import Session from module_admin.entity.do.log_do import SysOperLog, SysLogininfor from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel, OperLogPageObject, OperLogPageObjectResponse, \ LoginLogPageObject, LoginLogPageObjectResponse, CrudLogResponse -from utils.time_format_util import list_format_datetime +from utils.time_format_util import object_format_datetime, list_format_datetime from utils.page_util import get_page_info from datetime import datetime, time @@ -12,7 +12,7 @@ def get_operation_log_detail_by_id(db: Session, oper_id: int): .filter(SysOperLog.oper_id == oper_id) \ .first() - return operation_log_info + return object_format_datetime(operation_log_info) def get_operation_log_list(db: Session, page_object: OperLogPageObject): @@ -89,6 +89,17 @@ def delete_operation_log_dao(db: Session, operation_log: OperLogModel): db.commit() # 提交保存到数据库中 +def clear_operation_log_dao(db: Session): + """ + 清除操作日志数据库操作 + :param db: orm对象 + :return: + """ + db.query(SysOperLog) \ + .delete() + db.commit() # 提交保存到数据库中 + + def get_login_log_list(db: Session, page_object: LoginLogPageObject): """ 根据查询参数获取登录日志列表信息 @@ -159,3 +170,14 @@ def delete_login_log_dao(db: Session, login_log: LogininforModel): .filter(SysLogininfor.info_id == login_log.info_id) \ .delete() db.commit() # 提交保存到数据库中 + + +def clear_login_log_dao(db: Session): + """ + 清除登录日志数据库操作 + :param db: orm对象 + :return: + """ + db.query(SysLogininfor) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py index 2f87702..fb778b6 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -74,6 +74,13 @@ class DeleteOperLogModel(BaseModel): oper_ids: str +class ClearOperLogModel(BaseModel): + """ + 清除操作日志模型 + """ + oper_type: str + + class LoginLogPageObject(LogininforModel): """ 登录日志管理分页查询模型 diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py index 23ac492..06301cc 100644 --- a/dash-fastapi-backend/module_admin/service/log_service.py +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -44,6 +44,22 @@ def delete_operation_log_services(result_db: Session, page_object: DeleteOperLog return CrudLogResponse(**result) +def clear_operation_log_services(result_db: Session, page_object: ClearOperLogModel): + """ + 清除操作日志信息service + :param result_db: orm对象 + :param page_object: 清除操作日志对象 + :return: 清除操作日志校验结果 + """ + if page_object.oper_type == 'clear': + clear_operation_log_dao(result_db) + result = dict(is_success=True, message='清除成功') + else: + result = dict(is_success=False, message='清除标识不合法') + + return CrudLogResponse(**result) + + def detail_operation_log_services(result_db: Session, oper_id: int): """ 获取操作日志详细信息service @@ -87,12 +103,16 @@ def delete_login_log_services(result_db: Session, page_object: DeleteLoginLogMod :param page_object: 删除操作日志对象 :return: 删除操作日志校验结果 """ - if page_object.info_ids.split(','): - info_id_list = page_object.info_ids.split(',') - for info_id in info_id_list: - info_id_dict = dict(info_id=info_id) - delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) - result = dict(is_success=True, message='删除成功') + if page_object.oper_type == 'clear': + clear_operation_log_dao(result_db) + result = dict(is_success=True, message='清除成功') else: - result = dict(is_success=False, message='传入登录日志id为空') + if page_object.info_ids.split(','): + info_id_list = page_object.info_ids.split(',') + for info_id in info_id_list: + info_id_dict = dict(info_id=info_id) + delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入登录日志id为空') return CrudLogResponse(**result) diff --git a/dash-fastapi-frontend/api/log.py b/dash-fastapi-frontend/api/log.py new file mode 100644 index 0000000..714af6d --- /dev/null +++ b/dash-fastapi-frontend/api/log.py @@ -0,0 +1,31 @@ +from utils.request import api_request + + +def get_operation_log_list_api(page_obj: dict): + + return api_request(method='post', url='/system/log/operation/get', is_headers=True, json=page_obj) + + +def delete_operation_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/operation/delete', is_headers=True, json=page_obj) + + +def clear_operation_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/operation/clear', is_headers=True, json=page_obj) + + +def get_operation_log_detail_api(oper_id: int): + + return api_request(method='get', url=f'/system/log/operation/{oper_id}', is_headers=True) + + +def get_login_log_list_api(page_obj: dict): + + return api_request(method='post', url='/system/log/login/get', is_headers=True, json=page_obj) + + +def delete_login_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/login/delete', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py new file mode 100644 index 0000000..09dfc19 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -0,0 +1,268 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.log import get_operation_log_list_api, get_operation_log_detail_api, delete_operation_log_api, clear_operation_log_api + + +@app.callback( + [Output('operation_log-list-table', 'data', allow_duplicate=True), + Output('operation_log-list-table', 'pagination', allow_duplicate=True), + Output('operation_log-list-table', 'key'), + Output('operation_log-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('operation_log-search', 'nClicks'), + Input('operation_log-list-table', 'pagination'), + Input('operation_log-operations-store', 'data')], + [State('operation_log-title-input', 'value'), + State('operation_log-oper_name-input', 'value'), + State('operation_log-business_type-select', 'value'), + State('operation_log-status-select', 'value'), + State('operation_log-oper_time-range', 'value'), + State('operation_log-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_operation_log_table_data(search_click, pagination, operations, title, oper_name, business_type, status_select, oper_time_range, button_perms): + + oper_time_start = None + oper_time_end = None + if oper_time_range: + oper_time_start = oper_time_range[0] + oper_time_end = oper_time_range[1] + query_params = dict( + title=title, + oper_name=oper_name, + business_type=business_type, + status=status_select, + oper_time_start=oper_time_start, + oper_time_end=oper_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + title=title, + oper_name=oper_name, + business_type=business_type, + status=status_select, + oper_time_start=oper_time_start, + oper_time_end=oper_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_operation_log_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == 0: + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + if item['business_type'] == 0: + item['business_type'] = dict(tag='其他', color='purple') + elif item['business_type'] == 1: + item['business_type'] = dict(tag='新增', color='green') + elif item['business_type'] == 2: + item['business_type'] = dict(tag='修改', color='orange') + elif item['business_type'] == 3: + item['business_type'] = dict(tag='删除', color='red') + elif item['business_type'] == 4: + item['business_type'] = dict(tag='授权', color='lime') + elif item['business_type'] == 5: + item['business_type'] = dict(tag='导出', color='geekblue') + elif item['business_type'] == 6: + item['business_type'] = dict(tag='导入', color='blue') + elif item['business_type'] == 7: + item['business_type'] = dict(tag='强退', color='magenta') + elif item['business_type'] == 8: + item['business_type'] = dict(tag='生成代码', color='cyan') + elif item['business_type'] == 9: + item['business_type'] = dict(tag='清空数据', color='volcano') + item['key'] = str(item['oper_id']) + item['cost_time'] = f"{item['cost_time']}毫秒" + item['operation'] = [ + { + 'content': '详情', + 'type': 'link', + 'icon': 'antd-eye' + } if 'monitor:operlog:query' in button_perms else {}, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('operation_log-title-input', 'value'), + Output('operation_log-oper_name-input', 'value'), + Output('operation_log-business_type-select', 'value'), + Output('operation_log-status-select', 'value'), + Output('operation_log-oper_time-range', 'value'), + Output('operation_log-operations-store', 'data')], + Input('operation_log-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_operation_log_query_params(reset_click): + if reset_click: + return [None, None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 6 + + +@app.callback( + [Output('operation_log-modal', 'visible', allow_duplicate=True), + Output('operation_log-modal', 'title'), + Output('operation_log-title-text', 'children'), + Output('operation_log-oper_url-text', 'children'), + Output('operation_log-login_info-text', 'children'), + Output('operation_log-request_method-text', 'children'), + Output('operation_log-method-text', 'children'), + Output('operation_log-oper_param-text', 'children'), + Output('operation_log-json_result-text', 'children'), + Output('operation_log-status-text', 'children'), + Output('operation_log-cost_time-text', 'children'), + Output('operation_log-oper_time-text', 'children'), + Output('api-check-token', 'data', allow_duplicate=True)], + Input('operation_log-list-table', 'nClicksButton'), + [State('operation_log-list-table', 'clickedContent'), + State('operation_log-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_edit_operation_log_modal(button_click, clicked_content, recently_button_clicked_row): + if button_click: + oper_id = int(recently_button_clicked_row['key']) + operation_log_info_res = get_operation_log_detail_api(oper_id=oper_id) + if operation_log_info_res['code'] == 200: + operation_log_info = operation_log_info_res['data'] + oper_name = operation_log_info.get('oper_name') if operation_log_info.get('oper_name') else '' + oper_ip = operation_log_info.get('oper_ip') if operation_log_info.get('oper_ip') else '' + oper_location = operation_log_info.get('oper_location') if operation_log_info.get('oper_location') else '' + login_info = f'{oper_name} / {oper_ip} / {oper_location}' + return [ + True, + '操作日志详情', + operation_log_info.get('title'), + operation_log_info.get('oper_url'), + login_info, + operation_log_info.get('request_method'), + operation_log_info.get('method'), + operation_log_info.get('oper_param'), + operation_log_info.get('json_result'), + '正常' if operation_log_info.get('status') == 0 else '失败', + f"{operation_log_info.get('cost_time')}毫秒", + operation_log_info.get('oper_time'), + {'timestamp': time.time()}, + ] + + return [dash.no_update] * 12 + [{'timestamp': time.time()}] + + return [dash.no_update] * 13 + + +@app.callback( + Output('operation_log-delete', 'disabled'), + Input('operation_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_operation_log_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return False + + return False + + return True + + +@app.callback( + [Output('operation_log-delete-text', 'children'), + Output('operation_log-delete-confirm-modal', 'visible'), + Output('operation_log-delete-ids-store', 'data')], + [Input('operation_log-delete', 'nClicks'), + Input('operation_log-clear', 'nClicks')], + State('operation_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def post_delete_modal(delete_click, clear_click, selected_row_keys): + if delete_click or clear_click: + trigger_id = dash.ctx.triggered_id + if trigger_id == 'operation_log-delete': + oper_ids = ','.join(selected_row_keys) + + return [ + f'是否确认删除日志编号为{oper_ids}的操作日志?', + True, + {'oper_type': 'delete', 'oper_ids': oper_ids} + ] + + elif trigger_id == 'operation_log-clear': + return [ + f'是否确认清除所有的操作日志?', + True, + {'oper_type': 'clear', 'oper_ids': ''} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('operation_log-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('operation_log-delete-confirm-modal', 'okCounts'), + State('operation_log-delete-ids-store', 'data'), + prevent_initial_call=True +) +def operation_log_delete_confirm(delete_confirm, oper_ids_data): + if delete_confirm: + + oper_type = oper_ids_data.get('oper_type') + if oper_type == 'clear': + params = dict(oper_type=oper_ids_data.get('oper_type')) + clear_button_info = clear_operation_log_api(params) + if clear_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除失败', type='error') + ] + else: + params = dict(oper_ids=oper_ids_data.get('oper_ids')) + delete_button_info = delete_operation_log_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index 33c21c3..3a6fdee 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -14,6 +14,7 @@ from api.post import get_post_list_api, get_post_detail_api, add_post_api, edit_ [Output('post-list-table', 'data', allow_duplicate=True), Output('post-list-table', 'pagination', allow_duplicate=True), Output('post-list-table', 'key'), + Output('post-list-table', 'selectedRowKeys'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('post-search', 'nClicks'), Input('post-list-table', 'pagination'), @@ -72,11 +73,11 @@ def get_post_table_data(search_click, pagination, operations, post_code, post_na } if 'system:post:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return [dash.no_update] * 4 + return [dash.no_update] * 5 @app.callback( diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 69982c6..5c69cf5 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -15,6 +15,7 @@ from api.menu import get_menu_tree_api [Output('role-list-table', 'data', allow_duplicate=True), Output('role-list-table', 'pagination', allow_duplicate=True), Output('role-list-table', 'key'), + Output('role-list-table', 'selectedRowKeys'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('role-search', 'nClicks'), Input('role-list-table', 'pagination'), @@ -83,11 +84,11 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke } if 'system:role:remove' in button_perms else {}, ] - return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return [dash.no_update] * 4 + return [dash.no_update] * 5 @app.callback( diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c.py index 99abb4f..ef43a6e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c.py @@ -37,6 +37,7 @@ def get_search_dept_tree(dept_input): [Output('user-list-table', 'data', allow_duplicate=True), Output('user-list-table', 'pagination', allow_duplicate=True), Output('user-list-table', 'key'), + Output('user-list-table', 'selectedRowKeys'), Output('api-check-token', 'data', allow_duplicate=True)], [Input('dept-tree', 'selectedKeys'), Input('user-search', 'nClicks'), @@ -113,11 +114,11 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio } if 'system:user:resetPwd' in button_perms else None ] - return [table_data, table_pagination, str(uuid.uuid4()), {'timestamp': time.time()}] + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] - return [dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] - return [dash.no_update] * 4 + return [dash.no_update] * 5 @app.callback( diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 6cbc206..791b673 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -59,5 +59,9 @@ def render_store_container(): dcc.Store(id='post-edit-id-store'), # 岗位管理模块删除操作行key存储容器 dcc.Store(id='post-delete-ids-store'), + # 操作日志管理模块操作类型存储容器 + dcc.Store(id='operation_log-operations-store'), + # 操作日志管理模块删除操作行key存储容器 + dcc.Store(id='operation_log-delete-ids-store'), ] ) diff --git a/dash-fastapi-frontend/views/monitor/operlog/__init__.py b/dash-fastapi-frontend/views/monitor/operlog/__init__.py index b9b2243..382e81c 100644 --- a/dash-fastapi-frontend/views/monitor/operlog/__init__.py +++ b/dash-fastapi-frontend/views/monitor/operlog/__init__.py @@ -1,8 +1,597 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.monitor_c.operlog_c +from api.log import get_operation_log_list_api + def render(button_perms): - return html.Div('我是操作日志') + operation_log_params = dict(page_num=1, page_size=10) + table_info = get_operation_log_list_api(operation_log_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == 0: + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + if item['business_type'] == 0: + item['business_type'] = dict(tag='其他', color='purple') + elif item['business_type'] == 1: + item['business_type'] = dict(tag='新增', color='green') + elif item['business_type'] == 2: + item['business_type'] = dict(tag='修改', color='orange') + elif item['business_type'] == 3: + item['business_type'] = dict(tag='删除', color='red') + elif item['business_type'] == 4: + item['business_type'] = dict(tag='授权', color='lime') + elif item['business_type'] == 5: + item['business_type'] = dict(tag='导出', color='geekblue') + elif item['business_type'] == 6: + item['business_type'] = dict(tag='导入', color='blue') + elif item['business_type'] == 7: + item['business_type'] = dict(tag='强退', color='magenta') + elif item['business_type'] == 8: + item['business_type'] = dict(tag='生成代码', color='cyan') + elif item['business_type'] == 9: + item['business_type'] = dict(tag='清空数据', color='volcano') + item['key'] = str(item['oper_id']) + item['cost_time'] = f"{item['cost_time']}毫秒" + item['operation'] = [ + { + 'content': '详情', + 'type': 'link', + 'icon': 'antd-eye' + } if 'monitor:operlog:query' in button_perms else {}, + ] + + return [ + dcc.Store(id='operation_log-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='operation_log-title-input', + placeholder='请输入系统模块', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='系统模块', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='operation_log-oper_name-input', + placeholder='请输入操作人员', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='操作人员', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='operation_log-business_type-select', + placeholder='操作类型', + options=[ + { + 'label': '新增', + 'value': 1 + }, + { + 'label': '修改', + 'value': 2 + }, + { + 'label': '删除', + 'value': 3 + }, + { + 'label': '授权', + 'value': 4 + }, + { + 'label': '导出', + 'value': 5 + }, + { + 'label': '导入', + 'value': 6 + }, + { + 'label': '强退', + 'value': 7 + }, + { + 'label': '生成代码', + 'value': 8 + }, + { + 'label': '清空数据', + 'value': 9 + }, + { + 'label': '其他', + 'value': 0 + }, + ], + style={ + 'width': 240 + } + ), + label='类型', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='operation_log-status-select', + placeholder='操作状态', + options=[ + { + 'label': '成功', + 'value': 0 + }, + { + 'label': '失败', + 'value': 1 + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='operation_log-oper_time-range', + style={ + 'width': 240 + } + ), + label='操作时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='operation_log-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='operation_log-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='monitor:operlog:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-delete' + ), + '删除', + ], + id='operation_log-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:operlog:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-clear' + ), + '清空', + ], + id='operation_log-clear', + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:operlog:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='operation_log-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='monitor:operlog:export' not in button_perms + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='operation_log-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'oper_id', + 'title': '日志编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'title', + 'title': '系统模块', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'business_type', + 'title': '操作类型', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'oper_name', + 'title': '操作人员', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'oper_ip', + 'title': '操作地址', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'oper_location', + 'title': '操作地点', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '操作状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'oper_time', + 'title': '操作日期', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'cost_time', + 'title': '消耗时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 操作日志明细modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-title-text'), + label='操作模块', + required=True, + id='operation_log-title-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-oper_url-text'), + label='请求地址', + required=True, + id='operation_log-oper_url-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + ], + gutter=5 + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-login_info-text'), + label='登录信息', + required=True, + id='operation_log-login_info-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-request_method-text'), + label='请求方式', + required=True, + id='operation_log-request_method-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=12 + ), + ], + gutter=5 + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-method-text'), + label='操作方法', + required=True, + id='operation_log-method-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + span=24 + ), + ], + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-oper_param-text'), + label='请求参数', + required=True, + id='operation_log-oper_param-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + span=24 + ), + ], + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-json_result-text'), + label='返回参数', + required=True, + id='operation_log-json_result-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + span=24 + ), + ], + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-status-text'), + label='操作状态', + required=True, + id='operation_log-status-form-item', + labelCol={ + 'span': 12 + }, + wrapperCol={ + 'span': 12 + } + ), + span=8 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-cost_time-text'), + label='消耗时间', + required=True, + id='operation_log-cost_time-form-item', + labelCol={ + 'span': 12 + }, + wrapperCol={ + 'span': 12 + } + ), + span=6 + ), + fac.AntdCol( + fac.AntdFormItem( + fac.AntdText(id='operation_log-oper_time-text'), + label='操作时间', + required=True, + id='operation_log-oper_time-form-item', + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + } + ), + span=10 + ), + ], + gutter=5 + ), + ], + labelCol={ + 'span': 8 + }, + wrapperCol={ + 'span': 16 + }, + style={ + 'marginRight': '15px' + } + ) + ], + id='operation_log-modal', + mask=False, + width=850, + renderFooter=False, + ), + + # 删除操作日志二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='operation_log-delete-text'), + id='operation_log-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From bc33bb5316832b54fb542f7e29c0cbd0aa538a3d Mon Sep 17 00:00:00 2001 From: xlf Date: Thu, 13 Jul 2023 09:40:15 +0800 Subject: [PATCH 024/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/controller/log_controller.py | 16 + .../module_admin/entity/vo/log_vo.py | 7 + .../module_admin/service/log_service.py | 30 +- dash-fastapi-frontend/api/log.py | 5 + .../callbacks/monitor_c/logininfor_c.py | 187 ++++++++++ .../callbacks/monitor_c/operlog_c.py | 2 +- dash-fastapi-frontend/store/store.py | 4 + .../views/monitor/logininfor/__init__.py | 327 +++++++++++++++++- 8 files changed, 565 insertions(+), 13 deletions(-) create mode 100644 dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index b714327..5d0caba 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -92,3 +92,19 @@ async def delete_system_login_log(request: Request, delete_login_log: DeleteLogi except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@logController.post("/login/clear", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) +@log_decorator(title='操作日志管理', business_type=9) +async def clear_system_login_log(request: Request, clear_login_log: ClearLoginLogModel, query_db: Session = Depends(get_db)): + try: + clear_login_log_result = clear_login_log_services(query_db, clear_login_log) + if clear_login_log_result.is_success: + logger.info(clear_login_log_result.message) + return response_200(data=clear_login_log_result, message=clear_login_log_result.message) + else: + logger.warning(clear_login_log_result.message) + return response_400(data="", message=clear_login_log_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py index fb778b6..aa38949 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/log_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/log_vo.py @@ -109,6 +109,13 @@ class DeleteLoginLogModel(BaseModel): info_ids: str +class ClearLoginLogModel(BaseModel): + """ + 清除登录日志模型 + """ + oper_type: str + + class CrudLogResponse(BaseModel): """ 操作各类日志响应模型 diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py index 06301cc..6aac3d1 100644 --- a/dash-fastapi-backend/module_admin/service/log_service.py +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -103,16 +103,28 @@ def delete_login_log_services(result_db: Session, page_object: DeleteLoginLogMod :param page_object: 删除操作日志对象 :return: 删除操作日志校验结果 """ + if page_object.info_ids.split(','): + info_id_list = page_object.info_ids.split(',') + for info_id in info_id_list: + info_id_dict = dict(info_id=info_id) + delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入登录日志id为空') + return CrudLogResponse(**result) + + +def clear_login_log_services(result_db: Session, page_object: ClearLoginLogModel): + """ + 清除操作日志信息service + :param result_db: orm对象 + :param page_object: 清除操作日志对象 + :return: 清除操作日志校验结果 + """ if page_object.oper_type == 'clear': - clear_operation_log_dao(result_db) + clear_login_log_dao(result_db) result = dict(is_success=True, message='清除成功') else: - if page_object.info_ids.split(','): - info_id_list = page_object.info_ids.split(',') - for info_id in info_id_list: - info_id_dict = dict(info_id=info_id) - delete_login_log_dao(result_db, LogininforModel(**info_id_dict)) - result = dict(is_success=True, message='删除成功') - else: - result = dict(is_success=False, message='传入登录日志id为空') + result = dict(is_success=False, message='清除标识不合法') + return CrudLogResponse(**result) diff --git a/dash-fastapi-frontend/api/log.py b/dash-fastapi-frontend/api/log.py index 714af6d..4387c7e 100644 --- a/dash-fastapi-frontend/api/log.py +++ b/dash-fastapi-frontend/api/log.py @@ -29,3 +29,8 @@ def get_login_log_list_api(page_obj: dict): def delete_login_log_api(page_obj: dict): return api_request(method='post', url='/system/log/login/delete', is_headers=True, json=page_obj) + + +def clear_login_log_api(page_obj: dict): + + return api_request(method='post', url='/system/log/login/clear', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py new file mode 100644 index 0000000..143eb2c --- /dev/null +++ b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py @@ -0,0 +1,187 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.log import get_login_log_list_api, delete_login_log_api, clear_login_log_api + + +@app.callback( + [Output('login_log-list-table', 'data', allow_duplicate=True), + Output('login_log-list-table', 'pagination', allow_duplicate=True), + Output('login_log-list-table', 'key'), + Output('login_log-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('login_log-search', 'nClicks'), + Input('login_log-list-table', 'pagination'), + Input('login_log-operations-store', 'data')], + [State('login_log-ipaddr-input', 'value'), + State('login_log-user_name-input', 'value'), + State('login_log-status-select', 'value'), + State('login_log-login_time-range', 'value'), + State('login_log-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_login_log_table_data(search_click, pagination, operations, ipaddr, user_name, status_select, login_time_range, button_perms): + + login_time_start = None + login_time_end = None + if login_time_range: + login_time_start = login_time_range[0] + login_time_end = login_time_range[1] + query_params = dict( + ipaddr=ipaddr, + user_name=user_name, + status=status_select, + login_time_start=login_time_start, + login_time_end=login_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + ipaddr=ipaddr, + user_name=user_name, + status=status_select, + login_time_start=login_time_start, + login_time_end=login_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_login_log_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + item['key'] = str(item['info_id']) + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('login_log-ipaddr-input', 'value'), + Output('login_log-user_name-input', 'value'), + Output('login_log-status-select', 'value'), + Output('login_log-login_time-range', 'value'), + Output('login_log-operations-store', 'data')], + Input('login_log-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_login_log_query_params(reset_click): + if reset_click: + return [None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('login_log-delete', 'disabled'), + Output('login_log-unlock', 'disabled')], + Input('login_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_login_log_delete_unlock_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [False, True] + + return [False, False] + + return [True, True] + + +@app.callback( + [Output('login_log-delete-text', 'children'), + Output('login_log-delete-confirm-modal', 'visible'), + Output('login_log-delete-ids-store', 'data')], + [Input('login_log-delete', 'nClicks'), + Input('login_log-clear', 'nClicks')], + State('login_log-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def login_log_delete_modal(delete_click, clear_click, selected_row_keys): + if delete_click or clear_click: + trigger_id = dash.ctx.triggered_id + if trigger_id == 'login_log-delete': + info_ids = ','.join(selected_row_keys) + + return [ + f'是否确认删除访问编号为{info_ids}的操作日志?', + True, + {'oper_type': 'delete', 'info_ids': info_ids} + ] + + elif trigger_id == 'login_log-clear': + return [ + f'是否确认清除所有的登录日志?', + True, + {'oper_type': 'clear', 'info_ids': ''} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('login_log-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('login_log-delete-confirm-modal', 'okCounts'), + State('login_log-delete-ids-store', 'data'), + prevent_initial_call=True +) +def login_log_delete_confirm(delete_confirm, info_ids_data): + if delete_confirm: + + oper_type = info_ids_data.get('oper_type') + if oper_type == 'clear': + params = dict(oper_type=info_ids_data.get('oper_type')) + clear_button_info = clear_login_log_api(params) + if clear_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('清除失败', type='error') + ] + else: + params = dict(info_ids=info_ids_data.get('info_ids')) + delete_button_info = delete_login_log_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py index 09dfc19..eb26438 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -200,7 +200,7 @@ def change_operation_log_delete_button_status(table_rows_selected): State('operation_log-list-table', 'selectedRowKeys'), prevent_initial_call=True ) -def post_delete_modal(delete_click, clear_click, selected_row_keys): +def operation_log_delete_modal(delete_click, clear_click, selected_row_keys): if delete_click or clear_click: trigger_id = dash.ctx.triggered_id if trigger_id == 'operation_log-delete': diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 791b673..21f88d5 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -63,5 +63,9 @@ def render_store_container(): dcc.Store(id='operation_log-operations-store'), # 操作日志管理模块删除操作行key存储容器 dcc.Store(id='operation_log-delete-ids-store'), + # 登录日志管理模块操作类型存储容器 + dcc.Store(id='login_log-operations-store'), + # 操作日志管理模块删除操作行key存储容器 + dcc.Store(id='login_log-delete-ids-store'), ] ) diff --git a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py index c3e2dbb..3ba6c94 100644 --- a/dash-fastapi-frontend/views/monitor/logininfor/__init__.py +++ b/dash-fastapi-frontend/views/monitor/logininfor/__init__.py @@ -1,8 +1,329 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.monitor_c.logininfor_c +from api.log import get_login_log_list_api + def render(button_perms): - return html.Div('我是登录日志') + login_log_params = dict(page_num=1, page_size=10) + table_info = get_login_log_list_api(login_log_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='成功', color='blue') + else: + item['status'] = dict(tag='失败', color='volcano') + item['key'] = str(item['info_id']) + + return [ + dcc.Store(id='login_log-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='login_log-ipaddr-input', + placeholder='请输入登录地址', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='登录地址', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='login_log-user_name-input', + placeholder='请输入用户名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='用户名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='login_log-status-select', + placeholder='登录状态', + options=[ + { + 'label': '成功', + 'value': 0 + }, + { + 'label': '失败', + 'value': 1 + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='login_log-login_time-range', + style={ + 'width': 240 + } + ), + label='登录时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='login_log-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='login_log-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='monitor:logininfor:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-delete' + ), + '删除', + ], + id='login_log-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:logininfor:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-clear' + ), + '清空', + ], + id='login_log-clear', + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='monitor:logininfor:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-unlock' + ), + '解锁', + ], + id='login_log-unlock', + disabled=True, + style={ + 'color': '#74bcff', + 'background': '#e8f4ff', + 'border-color': '#d1e9ff' + } + ), + ], + hidden='monitor:logininfor:unlock' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='login_log-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='monitor:logininfor:export' not in button_perms + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='login_log-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'info_id', + 'title': '访问编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'user_name', + 'title': '用户名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'ipaddr', + 'title': '登录地址', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'login_location', + 'title': '登录地点', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'browser', + 'title': '浏览器', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'os', + 'title': '操作系统', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '登录状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'msg', + 'title': '操作信息', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'login_time', + 'title': '登录日期', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 删除操作日志二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='login_log-delete-text'), + id='login_log-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From e1dbd6a97dae42b97095d2cb7d890ad03e530d93 Mon Sep 17 00:00:00 2001 From: xlf Date: Thu, 13 Jul 2023 17:39:14 +0800 Subject: [PATCH 025/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E7=B1=BB=E5=9E=8B=E7=AE=A1=E7=90=86=EF=BC=9B=20fix:?= =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../controller/dict_controller.py | 163 +++++++ .../module_admin/controller/log_controller.py | 2 +- .../module_admin/dao/dict_dao.py | 201 ++++++++ .../module_admin/entity/vo/dict_vo.py | 105 ++++ .../module_admin/service/dept_service.py | 2 +- .../module_admin/service/dict_service.py | 136 ++++++ .../module_admin/service/menu_service.py | 2 +- .../module_admin/service/post_service.py | 2 +- dash-fastapi-frontend/api/dict.py | 51 ++ .../callbacks/system_c/dict_c.py | 333 +++++++++++++ .../callbacks/system_c/role_c.py | 34 +- dash-fastapi-frontend/store/store.py | 7 + .../views/system/dict/__init__.py | 458 +++++++++++++++++- 14 files changed, 1475 insertions(+), 23 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/controller/dict_controller.py create mode 100644 dash-fastapi-backend/module_admin/dao/dict_dao.py create mode 100644 dash-fastapi-backend/module_admin/entity/vo/dict_vo.py create mode 100644 dash-fastapi-backend/module_admin/service/dict_service.py create mode 100644 dash-fastapi-frontend/api/dict.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/dict_c.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 613d269..246d710 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -11,6 +11,7 @@ from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController +from module_admin.controller.dict_controller import dictController from module_admin.controller.log_controller import logController from config.env import RedisConfig from utils.response_util import response_401, AuthException @@ -76,6 +77,7 @@ app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) +app.include_router(dictController, prefix="/system", tags=['system/dict']) app.include_router(logController, prefix="/system", tags=['system/log']) diff --git a/dash-fastapi-backend/module_admin/controller/dict_controller.py b/dash-fastapi-backend/module_admin/controller/dict_controller.py new file mode 100644 index 0000000..28ab00e --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/dict_controller.py @@ -0,0 +1,163 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, Header +from config.get_db import get_db +from module_admin.service.login_service import get_current_user +from module_admin.service.dict_service import * +from module_admin.entity.vo.dict_vo import * +from utils.response_util import * +from utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator + + +dictController = APIRouter(dependencies=[Depends(get_current_user)]) + + +@dictController.post("/dictType/get", response_model=DictTypePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) +async def get_system_dict_type_list(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): + try: + dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + logger.info('获取成功') + return response_200(data=dict_type_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictType/add", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) +@log_decorator(title='字典管理', business_type=1) +async def add_system_dict_type(request: Request, add_dict_type: DictTypeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_dict_type.create_by = current_user.user.user_name + add_dict_type.update_by = current_user.user.user_name + add_dict_type_result = add_dict_type_services(query_db, add_dict_type) + logger.info(add_dict_type_result.message) + if add_dict_type_result.is_success: + return response_200(data=add_dict_type_result, message=add_dict_type_result.message) + else: + return response_400(data="", message=add_dict_type_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.patch("/dictType/edit", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +@log_decorator(title='字典管理', business_type=2) +async def edit_system_dict_type(request: Request, edit_dict_type: DictTypeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_dict_type.update_by = current_user.user.user_name + edit_dict_type.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_dict_type_result = edit_dict_type_services(query_db, edit_dict_type) + if edit_dict_type_result.is_success: + logger.info(edit_dict_type_result.message) + return response_200(data=edit_dict_type_result, message=edit_dict_type_result.message) + else: + logger.warning(edit_dict_type_result.message) + return response_400(data="", message=edit_dict_type_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictType/delete", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@log_decorator(title='字典管理', business_type=3) +async def delete_system_dict_type(request: Request, delete_dict_type: DeleteDictTypeModel, query_db: Session = Depends(get_db)): + try: + delete_dict_type_result = delete_dict_type_services(query_db, delete_dict_type) + if delete_dict_type_result.is_success: + logger.info(delete_dict_type_result.message) + return response_200(data=delete_dict_type_result, message=delete_dict_type_result.message) + else: + logger.warning(delete_dict_type_result.message) + return response_400(data="", message=delete_dict_type_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.get("/dictType/{dict_id}", response_model=DictTypeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +async def query_detail_system_dict_type(request: Request, dict_id: int, query_db: Session = Depends(get_db)): + try: + detail_dict_type_result = detail_dict_type_services(query_db, dict_id) + logger.info(f'获取dict_id为{dict_id}的信息成功') + return response_200(data=detail_dict_type_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictData/get", response_model=DictDataPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) +async def get_system_dict_data_list(request: Request, dict_data_query: DictDataPageObject, query_db: Session = Depends(get_db)): + try: + dict_data_query_result = get_dict_data_list(query_db, dict_data_query) + logger.info('获取成功') + return response_200(data=dict_data_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictData/add", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) +@log_decorator(title='字典管理', business_type=1) +async def add_system_dict_data(request: Request, add_dict_data: DictDataModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_dict_data.create_by = current_user.user.user_name + add_dict_data.update_by = current_user.user.user_name + add_dict_data_result = add_dict_data_services(query_db, add_dict_data) + logger.info(add_dict_data_result.message) + if add_dict_data_result.is_success: + return response_200(data=add_dict_data_result, message=add_dict_data_result.message) + else: + return response_400(data="", message=add_dict_data_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.patch("/dictData/edit", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +@log_decorator(title='字典管理', business_type=2) +async def edit_system_dict_data(request: Request, edit_dict_data: DictDataModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_dict_data.update_by = current_user.user.user_name + edit_dict_data.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_dict_data_result = edit_dict_data_services(query_db, edit_dict_data) + if edit_dict_data_result.is_success: + logger.info(edit_dict_data_result.message) + return response_200(data=edit_dict_data_result, message=edit_dict_data_result.message) + else: + logger.warning(edit_dict_data_result.message) + return response_400(data="", message=edit_dict_data_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.post("/dictData/delete", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:remove'))]) +@log_decorator(title='字典管理', business_type=3) +async def delete_system_dict_data(request: Request, delete_dict_data: DeleteDictDataModel, query_db: Session = Depends(get_db)): + try: + delete_dict_data_result = delete_dict_data_services(query_db, delete_dict_data) + if delete_dict_data_result.is_success: + logger.info(delete_dict_data_result.message) + return response_200(data=delete_dict_data_result, message=delete_dict_data_result.message) + else: + logger.warning(delete_dict_data_result.message) + return response_400(data="", message=delete_dict_data_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@dictController.get("/dictData/{dict_code}", response_model=DictDataModel, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:edit'))]) +async def query_detail_system_dict_data(request: Request, dict_code: int, query_db: Session = Depends(get_db)): + try: + detail_dict_data_result = detail_dict_data_services(query_db, dict_code) + logger.info(f'获取dict_code为{dict_code}的信息成功') + return response_200(data=detail_dict_data_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index 5d0caba..c5ef771 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -95,7 +95,7 @@ async def delete_system_login_log(request: Request, delete_login_log: DeleteLogi @logController.post("/login/clear", response_model=CrudLogResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:remove'))]) -@log_decorator(title='操作日志管理', business_type=9) +@log_decorator(title='登录日志管理', business_type=9) async def clear_system_login_log(request: Request, clear_login_log: ClearLoginLogModel, query_db: Session = Depends(get_db)): try: clear_login_log_result = clear_login_log_services(query_db, clear_login_log) diff --git a/dash-fastapi-backend/module_admin/dao/dict_dao.py b/dash-fastapi-backend/module_admin/dao/dict_dao.py new file mode 100644 index 0000000..6146099 --- /dev/null +++ b/dash-fastapi-backend/module_admin/dao/dict_dao.py @@ -0,0 +1,201 @@ +from sqlalchemy.orm import Session +from module_admin.entity.do.dict_do import SysDictType, SysDictData +from module_admin.entity.vo.dict_vo import DictTypeModel, DictTypePageObject, DictTypePageObjectResponse, \ + DictDataModel, DictDataPageObject, DictDataPageObjectResponse, CrudDictResponse +from utils.time_format_util import list_format_datetime +from utils.page_util import get_page_info +from datetime import datetime, time + + +def get_dict_type_detail_by_id(db: Session, dict_id: int): + dict_type_info = db.query(SysDictType) \ + .filter(SysDictType.dict_id == dict_id) \ + .first() + + return dict_type_info + + +def get_dict_type_list(db: Session, page_object: DictTypePageObject): + """ + 根据查询参数获取字典类型列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典类型列表信息对象 + """ + count = db.query(SysDictType) \ + .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, + SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, + SysDictType.status == page_object.status if page_object.status else True, + SysDictType.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True + )\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + dict_type_list = db.query(SysDictType) \ + .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, + SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, + SysDictType.status == page_object.status if page_object.status else True, + SysDictType.create_time.between( + datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if page_object.create_time_start and page_object.create_time_end else True + )\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(dict_type_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return DictTypePageObjectResponse(**result) + + +def add_dict_type_dao(db: Session, dict_type: DictTypeModel): + """ + 新增字典类型数据库操作 + :param db: orm对象 + :param dict_type: 字典类型对象 + :return: 新增校验结果 + """ + db_dict_type = SysDictType(**dict_type.dict()) + db.add(db_dict_type) + db.commit() # 提交保存到数据库中 + db.refresh(db_dict_type) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudDictResponse(**result) + + +def edit_dict_type_dao(db: Session, dict_type: dict): + """ + 编辑字典类型数据库操作 + :param db: orm对象 + :param dict_type: 需要更新的字典类型字典 + :return: 编辑校验结果 + """ + is_dict_type_id = db.query(SysDictType).filter(SysDictType.dict_id == dict_type.get('dict_id')).all() + if not is_dict_type_id: + result = dict(is_success=False, message='字典类型不存在') + else: + db.query(SysDictType) \ + .filter(SysDictType.dict_id == dict_type.get('dict_id')) \ + .update(dict_type) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudDictResponse(**result) + + +def delete_dict_type_dao(db: Session, dict_type: DictTypeModel): + """ + 删除字典类型数据库操作 + :param db: orm对象 + :param dict_type: 字典类型对象 + :return: + """ + db.query(SysDictType) \ + .filter(SysDictType.dict_id == dict_type.dict_id) \ + .delete() + db.commit() # 提交保存到数据库中 + + +def get_dict_data_detail_by_id(db: Session, dict_code: int): + dict_data_info = db.query(SysDictData) \ + .filter(SysDictData.dict_code == dict_code) \ + .first() + + return dict_data_info + + +def get_dict_data_list(db: Session, page_object: DictDataPageObject): + """ + 根据查询参数获取字典数据列表信息 + :param db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典数据列表信息对象 + """ + count = db.query(SysDictData) \ + .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, + SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, + SysDictData.status == page_object.status if page_object.status else True + )\ + .order_by(SysDictData.dict_sort)\ + .distinct().count() + offset_com = (page_object.page_num - 1) * page_object.page_size + page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) + dict_data_list = db.query(SysDictData) \ + .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, + SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, + SysDictData.status == page_object.status if page_object.status else True + )\ + .order_by(SysDictData.dict_sort)\ + .offset(page_info.offset) \ + .limit(page_object.page_size) \ + .distinct().all() + + result = dict( + rows=list_format_datetime(dict_data_list), + page_num=page_info.page_num, + page_size=page_info.page_size, + total=page_info.total, + has_next=page_info.has_next + ) + + return DictDataPageObjectResponse(**result) + + +def add_dict_data_dao(db: Session, dict_data: DictDataModel): + """ + 新增字典数据数据库操作 + :param db: orm对象 + :param dict_data: 字典数据对象 + :return: 新增校验结果 + """ + db_data_type = SysDictData(**dict_data.dict()) + db.add(db_data_type) + db.commit() # 提交保存到数据库中 + db.refresh(db_data_type) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudDictResponse(**result) + + +def edit_dict_data_dao(db: Session, dict_data: dict): + """ + 编辑字典数据数据库操作 + :param db: orm对象 + :param dict_data: 需要更新的字典数据字典 + :return: 编辑校验结果 + """ + is_dict_data_id = db.query(SysDictData).filter(SysDictData.dict_code == dict_data.get('dict_code')).all() + if not is_dict_data_id: + result = dict(is_success=False, message='字典数据不存在') + else: + db.query(SysDictData) \ + .filter(SysDictData.dict_code == dict_data.get('dict_code')) \ + .update(dict_data) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudDictResponse(**result) + + +def delete_dict_data_dao(db: Session, dict_data: DictDataModel): + """ + 删除字典数据数据库操作 + :param db: orm对象 + :param dict_data: 字典数据对象 + :return: + """ + db.query(SysDictData) \ + .filter(SysDictData.dict_code == dict_data.dict_code) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/vo/dict_vo.py b/dash-fastapi-backend/module_admin/entity/vo/dict_vo.py new file mode 100644 index 0000000..e7b9fe6 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/vo/dict_vo.py @@ -0,0 +1,105 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class DictTypeModel(BaseModel): + """ + 字典类型表对应pydantic模型 + """ + dict_id: Optional[int] + dict_name: Optional[str] + dict_type: Optional[str] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class DictDataModel(BaseModel): + """ + 字典数据表对应pydantic模型 + """ + dict_code: Optional[int] + dict_sort: Optional[int] + dict_label: Optional[str] + dict_value: Optional[str] + dict_type: Optional[str] + css_class: Optional[str] + list_class: Optional[str] + is_default: Optional[str] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class DictTypePageObject(DictTypeModel): + """ + 字典类型管理分页查询模型 + """ + create_time_start: Optional[str] + create_time_end: Optional[str] + page_num: Optional[int] + page_size: Optional[int] + + +class DictTypePageObjectResponse(BaseModel): + """ + 字典类型管理列表分页查询返回模型 + """ + rows: List[Union[DictTypeModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteDictTypeModel(BaseModel): + """ + 删除字典类型模型 + """ + dict_ids: str + + +class DictDataPageObject(DictDataModel): + """ + 字典数据管理分页查询模型 + """ + page_num: Optional[int] + page_size: Optional[int] + + +class DictDataPageObjectResponse(BaseModel): + """ + 字典数据管理列表分页查询返回模型 + """ + rows: List[Union[DictDataModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class DeleteDictDataModel(BaseModel): + """ + 删除字典数据模型 + """ + dict_codes: str + + +class CrudDictResponse(BaseModel): + """ + 操作字典响应模型 + """ + is_success: bool + message: str diff --git a/dash-fastapi-backend/module_admin/service/dept_service.py b/dash-fastapi-backend/module_admin/service/dept_service.py index 682706e..383bfb3 100644 --- a/dash-fastapi-backend/module_admin/service/dept_service.py +++ b/dash-fastapi-backend/module_admin/service/dept_service.py @@ -102,7 +102,7 @@ def delete_dept_services(result_db: Session, page_object: DeleteDeptModel): delete_dept_dao(result_db, DeptModel(**dept_id_dict)) result = dict(is_success=True, message='删除成功') else: - result = dict(is_success=False, message='传入用户id为空') + result = dict(is_success=False, message='传入部门id为空') return CrudDeptResponse(**result) diff --git a/dash-fastapi-backend/module_admin/service/dict_service.py b/dash-fastapi-backend/module_admin/service/dict_service.py new file mode 100644 index 0000000..70a16c1 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/dict_service.py @@ -0,0 +1,136 @@ +from module_admin.entity.vo.dict_vo import * +from module_admin.dao.dict_dao import * + + +def get_dict_type_list_services(result_db: Session, page_object: DictTypePageObject): + """ + 获取字典类型列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典类型列表信息对象 + """ + dict_type_list_result = get_dict_type_list(result_db, page_object) + + return dict_type_list_result + + +def add_dict_type_services(result_db: Session, page_object: DictTypeModel): + """ + 新增字典类型信息service + :param result_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增字典类型校验结果 + """ + add_dict_type_result = add_dict_type_dao(result_db, page_object) + + return add_dict_type_result + + +def edit_dict_type_services(result_db: Session, page_object: DictTypeModel): + """ + 编辑字典类型信息service + :param result_db: orm对象 + :param page_object: 编辑字典类型对象 + :return: 编辑字典类型校验结果 + """ + edit_dict_type = page_object.dict(exclude_unset=True) + edit_dict_type_result = edit_dict_type_dao(result_db, edit_dict_type) + + return edit_dict_type_result + + +def delete_dict_type_services(result_db: Session, page_object: DeleteDictTypeModel): + """ + 删除字典类型信息service + :param result_db: orm对象 + :param page_object: 删除字典类型对象 + :return: 删除字典类型校验结果 + """ + if page_object.dict_ids.split(','): + dict_id_list = page_object.dict_ids.split(',') + for dict_id in dict_id_list: + dict_id_dict = dict(dict_id=dict_id) + delete_dict_type_dao(result_db, DictTypeModel(**dict_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入字典类型id为空') + return CrudDictResponse(**result) + + +def detail_dict_type_services(result_db: Session, dict_id: int): + """ + 获取字典类型详细信息service + :param result_db: orm对象 + :param dict_id: 字典类型id + :return: 字典类型id对应的信息 + """ + dict_type = get_dict_type_detail_by_id(result_db, dict_id=dict_id) + + return dict_type + + +def get_dict_data_list_services(result_db: Session, page_object: DictDataPageObject): + """ + 获取字典数据列表信息service + :param result_db: orm对象 + :param page_object: 分页查询参数对象 + :return: 字典数据列表信息对象 + """ + dict_data_list_result = get_dict_data_list(result_db, page_object) + + return dict_data_list_result + + +def add_dict_data_services(result_db: Session, page_object: DictDataModel): + """ + 新增字典数据信息service + :param result_db: orm对象 + :param page_object: 新增岗位对象 + :return: 新增字典数据校验结果 + """ + add_dict_data_result = add_dict_data_dao(result_db, page_object) + + return add_dict_data_result + + +def edit_dict_data_services(result_db: Session, page_object: DictDataModel): + """ + 编辑字典数据信息service + :param result_db: orm对象 + :param page_object: 编辑字典数据对象 + :return: 编辑字典数据校验结果 + """ + edit_data_type = page_object.dict(exclude_unset=True) + edit_dict_data_result = edit_dict_data_dao(result_db, edit_data_type) + + return edit_dict_data_result + + +def delete_dict_data_services(result_db: Session, page_object: DeleteDictDataModel): + """ + 删除字典数据信息service + :param result_db: orm对象 + :param page_object: 删除字典数据对象 + :return: 删除字典数据校验结果 + """ + if page_object.dict_codes.split(','): + dict_code_list = page_object.dict_codes.split(',') + for dict_code in dict_code_list: + dict_code_dict = dict(dict_code=dict_code) + delete_dict_data_dao(result_db, DictDataModel(**dict_code_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入字典数据id为空') + return CrudDictResponse(**result) + + +def detail_dict_data_services(result_db: Session, dict_code: int): + """ + 获取字典数据详细信息service + :param result_db: orm对象 + :param dict_code: 字典数据id + :return: 字典数据id对应的信息 + """ + dict_data = get_dict_data_detail_by_id(result_db, dict_code=dict_code) + + return dict_data diff --git a/dash-fastapi-backend/module_admin/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py index 2c102b5..adebaa8 100644 --- a/dash-fastapi-backend/module_admin/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -86,7 +86,7 @@ def delete_menu_services(result_db: Session, page_object: DeleteMenuModel): delete_menu_dao(result_db, MenuModel(**menu_id_dict)) result = dict(is_success=True, message='删除成功') else: - result = dict(is_success=False, message='传入用户id为空') + result = dict(is_success=False, message='传入菜单id为空') return CrudMenuResponse(**result) diff --git a/dash-fastapi-backend/module_admin/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py index f048a05..507f477 100644 --- a/dash-fastapi-backend/module_admin/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -64,7 +64,7 @@ def delete_post_services(result_db: Session, page_object: DeletePostModel): delete_post_dao(result_db, PostModel(**post_id_dict)) result = dict(is_success=True, message='删除成功') else: - result = dict(is_success=False, message='传入用户id为空') + result = dict(is_success=False, message='传入岗位id为空') return CrudPostResponse(**result) diff --git a/dash-fastapi-frontend/api/dict.py b/dash-fastapi-frontend/api/dict.py new file mode 100644 index 0000000..c412531 --- /dev/null +++ b/dash-fastapi-frontend/api/dict.py @@ -0,0 +1,51 @@ +from utils.request import api_request + + +def get_dict_type_list_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/get', is_headers=True, json=page_obj) + + +def add_dict_type_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/add', is_headers=True, json=page_obj) + + +def edit_dict_type_api(page_obj: dict): + + return api_request(method='patch', url='/system/dictType/edit', is_headers=True, json=page_obj) + + +def delete_dict_type_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/delete', is_headers=True, json=page_obj) + + +def get_dict_type_detail_api(dict_id: int): + + return api_request(method='get', url=f'/system/dictType/{dict_id}', is_headers=True) + + +def get_dict_data_list_api(page_obj: dict): + + return api_request(method='post', url='/system/dictData/get', is_headers=True, json=page_obj) + + +def add_dict_data_api(page_obj: dict): + + return api_request(method='post', url='/system/dictData/add', is_headers=True, json=page_obj) + + +def edit_dict_data_api(page_obj: dict): + + return api_request(method='patch', url='/system/dictData/edit', is_headers=True, json=page_obj) + + +def delete_dict_data_api(page_obj: dict): + + return api_request(method='post', url='/system/dictData/delete', is_headers=True, json=page_obj) + + +def get_dict_data_detail_api(dict_id: int): + + return api_request(method='get', url=f'/system/dictData/{dict_id}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c.py new file mode 100644 index 0000000..c7c154d --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c.py @@ -0,0 +1,333 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.dict import get_dict_type_list_api, get_dict_type_detail_api, add_dict_type_api, edit_dict_type_api, delete_dict_type_api + + +@app.callback( + [Output('dict_type-list-table', 'data', allow_duplicate=True), + Output('dict_type-list-table', 'pagination', allow_duplicate=True), + Output('dict_type-list-table', 'key'), + Output('dict_type-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dict_type-search', 'nClicks'), + Input('dict_type-list-table', 'pagination'), + Input('dict_type-operations-store', 'data')], + [State('dict_type-dict_name-input', 'value'), + State('dict_type-dict_type-input', 'value'), + State('dict_type-status-select', 'value'), + State('dict_type-create_time-range', 'value'), + State('dict_type-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_dict_type_table_data(search_click, pagination, operations, dict_name, dict_type, status_select, create_time_range, button_perms): + create_time_start = None + create_time_end = None + if create_time_range: + create_time_start = create_time_range[0] + create_time_end = create_time_range[1] + + query_params = dict( + dict_name=dict_name, + dict_type=dict_type, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + dict_name=dict_name, + dict_type=dict_type, + status=status_select, + create_time_start=create_time_start, + create_time_end=create_time_end, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_dict_type_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dict_id']) + item['dict_type'] = [ + { + 'content': item['dict_type'], + 'type': 'link', + } + ] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dict:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:dict:remove' in button_perms else {}, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('dict_type-dict_name-input', 'value'), + Output('dict_type-dict_type-input', 'value'), + Output('dict_type-status-select', 'value'), + Output('dict_type-create_time-range', 'value'), + Output('dict_type-operations-store', 'data')], + Input('dict_type-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_dict_type_query_params(reset_click): + if reset_click: + return [None, None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('dict_type-edit', 'disabled'), + Output('dict_type-delete', 'disabled')], + Input('dict_type-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_dict_type_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return [True, True] + + +@app.callback( + [Output('dict_type-modal', 'visible', allow_duplicate=True), + Output('dict_type-modal', 'title'), + Output('dict_type-dict_name', 'value'), + Output('dict_type-dict_type', 'value'), + Output('dict_type-status', 'value'), + Output('dict_type-remark', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('dict_type-add', 'nClicks'), + Output('dict_type-edit', 'nClicks'), + Output('dict_type-edit-id-store', 'data'), + Output('dict_type-operations-store-bk', 'data')], + [Input('dict_type-add', 'nClicks'), + Input('dict_type-edit', 'nClicks'), + Input('dict_type-list-table', 'nClicksButton')], + [State('dict_type-list-table', 'selectedRowKeys'), + State('dict_type-list-table', 'clickedContent'), + State('dict_type-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def add_edit_dict_type_modal(add_click, edit_click, button_click, selected_row_keys, clicked_content, + recently_button_clicked_row): + if add_click or edit_click or button_click: + if add_click: + return [ + True, + '新增字典类型', + None, + None, + '0', + None, + {'timestamp': time.time()}, + None, + None, + None, + {'type': 'add'} + ] + elif edit_click or (button_click and clicked_content == '修改'): + if edit_click: + dict_id = int(','.join(selected_row_keys)) + else: + dict_id = int(recently_button_clicked_row['key']) + dict_type_info_res = get_dict_type_detail_api(dict_id=dict_id) + if dict_type_info_res['code'] == 200: + dict_type_info = dict_type_info_res['data'] + return [ + True, + '编辑字典类型', + dict_type_info.get('dict_name'), + dict_type_info.get('dict_type'), + dict_type_info.get('status'), + dict_type_info.get('remark'), + {'timestamp': time.time()}, + None, + None, + dict_type_info if dict_type_info else None, + {'type': 'edit'} + ] + + return [dash.no_update] * 6 + [{'timestamp': time.time()}, None, None, None, None] + + return [dash.no_update] * 7 + [None, None, None, None] + + +@app.callback( + [Output('dict_type-dict_name-form-item', 'validateStatus'), + Output('dict_type-dict_type-form-item', 'validateStatus'), + Output('dict_type-dict_name-form-item', 'help'), + Output('dict_type-dict_type-form-item', 'help'), + Output('dict_type-modal', 'visible'), + Output('dict_type-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_type-modal', 'okCounts'), + [State('dict_type-operations-store-bk', 'data'), + State('dict_type-edit-id-store', 'data'), + State('dict_type-dict_name', 'value'), + State('dict_type-dict_type', 'value'), + State('dict_type-status', 'value'), + State('dict_type-remark', 'value')], + prevent_initial_call=True +) +def dict_type_confirm(confirm_trigger, operation_type, cur_post_info, dict_name, dict_type, status, remark): + if confirm_trigger: + if all([dict_name, dict_type]): + params_add = dict(dict_name=dict_name, dict_type=dict_type, status=status, remark=remark) + params_edit = dict(dict_id=cur_post_info.get('dict_id') if cur_post_info else None, dict_name=dict_name, + dict_type=dict_type, status=status, remark=remark) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_dict_type_api(params_add) + if operation_type == 'edit': + api_res = edit_dict_type_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if dict_name else 'error', + None if dict_type else 'error', + None if dict_name else '请输入字典名称!', + None if dict_type else '请输入字典类型!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 8 + + +@app.callback( + [Output('dict_type-delete-text', 'children'), + Output('dict_type-delete-confirm-modal', 'visible'), + Output('dict_type-delete-ids-store', 'data')], + [Input('dict_type-delete', 'nClicks'), + Input('dict_type-list-table', 'nClicksButton')], + [State('dict_type-list-table', 'selectedRowKeys'), + State('dict_type-list-table', 'clickedContent'), + State('dict_type-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def dict_type_delete_modal(delete_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if delete_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'dict_type-delete': + dict_ids = ','.join(selected_row_keys) + else: + if clicked_content == '删除': + dict_ids = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除字典编号为{dict_ids}的岗位?', + True, + {'dict_ids': dict_ids} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('dict_type-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_type-delete-confirm-modal', 'okCounts'), + State('dict_type-delete-ids-store', 'data'), + prevent_initial_call=True +) +def dict_type_delete_confirm(delete_confirm, dict_ids_data): + if delete_confirm: + + params = dict_ids_data + delete_button_info = delete_dict_type_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 5c69cf5..87ad59e 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -175,14 +175,15 @@ def all_none_role_menu_mode(all_none, menu_info): def change_role_menu_mode(parent_children, current_role_menu): if parent_children: checked_menu = [] - for item in current_role_menu: - has_children = False - for other_item in current_role_menu: - if other_item['parent_id'] == item['menu_id']: - has_children = True - break - if not has_children: - checked_menu.append(str(item.get('menu_id'))) + if current_role_menu[0]: + for item in current_role_menu: + has_children = False + for other_item in current_role_menu: + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) return [False, checked_menu] else: checked_menu = [str(item.get('menu_id')) for item in current_role_menu if item] or [] @@ -250,14 +251,15 @@ def add_edit_role_modal(add_click, edit_click, button_click, selected_row_keys, if role_info_res['code'] == 200: role_info = role_info_res['data'] checked_menu = [] - for item in role_info.get('menu'): - has_children = False - for other_item in role_info.get('menu'): - if other_item['parent_id'] == item['menu_id']: - has_children = True - break - if not has_children: - checked_menu.append(str(item.get('menu_id'))) + if role_info.get('menu')[0]: + for item in role_info.get('menu'): + has_children = False + for other_item in role_info.get('menu'): + if other_item['parent_id'] == item['menu_id']: + has_children = True + break + if not has_children: + checked_menu.append(str(item.get('menu_id'))) return [ True, '编辑角色', diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index 21f88d5..f9ad59f 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -59,6 +59,13 @@ def render_store_container(): dcc.Store(id='post-edit-id-store'), # 岗位管理模块删除操作行key存储容器 dcc.Store(id='post-delete-ids-store'), + # 字典管理模块操作类型存储容器 + dcc.Store(id='dict_type-operations-store'), + dcc.Store(id='dict_type-operations-store-bk'), + # 字典管理模块修改操作行key存储容器 + dcc.Store(id='dict_type-edit-id-store'), + # 字典管理模块删除操作行key存储容器 + dcc.Store(id='dict_type-delete-ids-store'), # 操作日志管理模块操作类型存储容器 dcc.Store(id='operation_log-operations-store'), # 操作日志管理模块删除操作行key存储容器 diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index e0aa134..fd264a0 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -1,8 +1,460 @@ -from dash import html -import feffery_utils_components as fuc +from dash import dcc, html import feffery_antd_components as fac +import callbacks.system_c.dict_c +from api.dict import get_dict_type_list_api + def render(button_perms): - return html.Div('我是字典管理') + dict_type_params = dict(page_num=1, page_size=10) + table_info = get_dict_type_list_api(dict_type_params) + table_data = [] + page_num = 1 + page_size = 10 + total = 0 + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + page_num = table_info['data']['page_num'] + page_size = table_info['data']['page_size'] + total = table_info['data']['total'] + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dict_id']) + item['dict_type'] = [ + { + 'content': item['dict_type'], + 'type': 'link', + } + ] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dict:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:dict:remove' in button_perms else {}, + ] + + return [ + dcc.Store(id='dict_type-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_name-input', + placeholder='请输入字典名称', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='字典名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_type-input', + placeholder='请输入字典类型', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='字典类型', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dict_type-status-select', + placeholder='字典状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdDateRangePicker( + id='dict_type-create_time-range', + style={ + 'width': 240 + } + ), + label='创建时间', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dict_type-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dict_type-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='system:dict:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dict_type-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + ], + hidden='system:dict:add' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='dict_type-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + ], + hidden='system:dict:edit' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='dict_type-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='system:dict:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='dict_type-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='system:dict:export' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-sync' + ), + '刷新缓存', + ], + id='dict_type-refresh', + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='dict_type-list-table', + data=table_data, + columns=[ + { + 'dataIndex': 'dict_id', + 'title': '字典编号', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_name', + 'title': '字典名称', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_type', + 'title': '字典类型', + 'renderOptions': { + 'renderType': 'button' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'remark', + 'title': '备注', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': page_size, + 'current': page_num, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': total + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增和编辑字典类型表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_name', + placeholder='请输入字典名称', + allowClear=True, + style={ + 'width': 350 + } + ), + label='字典名称', + required=True, + id='dict_type-dict_name-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-dict_type', + placeholder='请输入字典类型', + allowClear=True, + style={ + 'width': 350 + } + ), + label='字典类型', + required=True, + id='dict_type-dict_type-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dict_type-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='状态', + id='dict_type-status-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_type-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='dict_type-remark-form-item' + ), + span=24 + ), + ] + ), + ], + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ) + ], + id='dict_type-modal', + mask=False, + width=580, + renderFooter=True, + okClickClose=False + ), + + # 删除字典类型二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='dict_type-delete-text'), + id='dict_type-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True, + centered=True + ), + ] -- Gitee From cc86bcff897e383ffa941a537bccb52920008ef5 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Sat, 15 Jul 2023 00:08:18 +0800 Subject: [PATCH 026/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dict_controller.py | 11 + .../module_admin/dao/dict_dao.py | 6 + .../module_admin/service/dict_service.py | 5 +- dash-fastapi-frontend/api/dict.py | 9 +- .../callbacks/system_c/{ => dict_c}/dict_c.py | 55 +- .../callbacks/system_c/dict_c/dict_data_c.py | 342 ++++++++++++ dash-fastapi-frontend/store/store.py | 4 + .../views/system/dict/__init__.py | 24 +- .../views/system/dict/dict_data.py | 504 ++++++++++++++++++ 9 files changed, 943 insertions(+), 17 deletions(-) rename dash-fastapi-frontend/callbacks/system_c/{ => dict_c}/dict_c.py (85%) create mode 100644 dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py create mode 100644 dash-fastapi-frontend/views/system/dict/dict_data.py diff --git a/dash-fastapi-backend/module_admin/controller/dict_controller.py b/dash-fastapi-backend/module_admin/controller/dict_controller.py index 28ab00e..4634e06 100644 --- a/dash-fastapi-backend/module_admin/controller/dict_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dict_controller.py @@ -24,6 +24,17 @@ async def get_system_dict_type_list(request: Request, dict_type_query: DictTypeP return response_500(data="", message="接口异常") +@dictController.post("/dictType/all", dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) +async def get_system_all_dict_type(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): + try: + dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + logger.info('获取成功') + return response_200(data=dict_type_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + @dictController.post("/dictType/add", response_model=CrudDictResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:add'))]) @log_decorator(title='字典管理', business_type=1) async def add_system_dict_type(request: Request, add_dict_type: DictTypeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): diff --git a/dash-fastapi-backend/module_admin/dao/dict_dao.py b/dash-fastapi-backend/module_admin/dao/dict_dao.py index 6146099..c15f36d 100644 --- a/dash-fastapi-backend/module_admin/dao/dict_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dict_dao.py @@ -15,6 +15,12 @@ def get_dict_type_detail_by_id(db: Session, dict_id: int): return dict_type_info +def get_all_dict_type(db: Session): + dict_type_info = db.query(SysDictType).all() + + return list_format_datetime(dict_type_info) + + def get_dict_type_list(db: Session, page_object: DictTypePageObject): """ 根据查询参数获取字典类型列表信息 diff --git a/dash-fastapi-backend/module_admin/service/dict_service.py b/dash-fastapi-backend/module_admin/service/dict_service.py index 70a16c1..9069456 100644 --- a/dash-fastapi-backend/module_admin/service/dict_service.py +++ b/dash-fastapi-backend/module_admin/service/dict_service.py @@ -9,7 +9,10 @@ def get_dict_type_list_services(result_db: Session, page_object: DictTypePageObj :param page_object: 分页查询参数对象 :return: 字典类型列表信息对象 """ - dict_type_list_result = get_dict_type_list(result_db, page_object) + if page_object.page_num and page_object.page_size: + dict_type_list_result = get_dict_type_list(result_db, page_object) + else: + dict_type_list_result = get_all_dict_type(result_db) return dict_type_list_result diff --git a/dash-fastapi-frontend/api/dict.py b/dash-fastapi-frontend/api/dict.py index c412531..96d440c 100644 --- a/dash-fastapi-frontend/api/dict.py +++ b/dash-fastapi-frontend/api/dict.py @@ -6,6 +6,11 @@ def get_dict_type_list_api(page_obj: dict): return api_request(method='post', url='/system/dictType/get', is_headers=True, json=page_obj) +def get_all_dict_type_api(page_obj: dict): + + return api_request(method='post', url='/system/dictType/all', is_headers=True, json=page_obj) + + def add_dict_type_api(page_obj: dict): return api_request(method='post', url='/system/dictType/add', is_headers=True, json=page_obj) @@ -46,6 +51,6 @@ def delete_dict_data_api(page_obj: dict): return api_request(method='post', url='/system/dictData/delete', is_headers=True, json=page_obj) -def get_dict_data_detail_api(dict_id: int): +def get_dict_data_detail_api(dict_code: int): - return api_request(method='get', url=f'/system/dictData/{dict_id}', is_headers=True) + return api_request(method='get', url=f'/system/dictData/{dict_code}', is_headers=True) diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py similarity index 85% rename from dash-fastapi-frontend/callbacks/system_c/dict_c.py rename to dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py index c7c154d..4ab9899 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py @@ -7,7 +7,7 @@ import feffery_antd_components as fac import feffery_utils_components as fuc from server import app -from api.dict import get_dict_type_list_api, get_dict_type_detail_api, add_dict_type_api, edit_dict_type_api, delete_dict_type_api +from api.dict import get_dict_type_list_api, get_all_dict_type_api, get_dict_type_detail_api, add_dict_type_api, edit_dict_type_api, delete_dict_type_api @app.callback( @@ -70,12 +70,10 @@ def get_dict_type_table_data(search_click, pagination, operations, dict_name, di else: item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dict_id']) - item['dict_type'] = [ - { - 'content': item['dict_type'], - 'type': 'link', - } - ] + item['dict_type'] = { + 'content': item['dict_type'], + 'type': 'link', + } item['operation'] = [ { 'content': '修改', @@ -331,3 +329,46 @@ def dict_type_delete_confirm(delete_confirm, dict_ids_data): ] return [dash.no_update] * 3 + + +@app.callback( + [Output('dict_type_to_dict_data-modal', 'visible'), + Output('dict_type_to_dict_data-modal', 'title'), + Output('dict_data-dict_type-select', 'options'), + Output('dict_data-dict_type-select', 'value', allow_duplicate=True), + Output('dict_data-search', 'nClicks'), + Output('api-check-token', 'data', allow_duplicate=True)], + Input('dict_type-list-table', 'nClicksButton'), + [State('dict_type-list-table', 'clickedContent'), + State('dict_type-list-table', 'recentlyButtonClickedRow'), + State('dict_data-search', 'nClicks')], + prevent_initial_call=True +) +def dict_type_to_dict_data_modal(button_click, clicked_content, recently_button_clicked_row, dict_data_search_nclick): + + if button_click and clicked_content == recently_button_clicked_row.get('dict_type').get('content'): + all_dict_type_info = get_all_dict_type_api({}) + if all_dict_type_info.get('code') == 200: + all_dict_type = all_dict_type_info.get('data') + dict_data_options = [dict(label=item.get('dict_name'), value=item.get('dict_type')) for item in all_dict_type] + + return [ + True, + '字典数据', + dict_data_options, + recently_button_clicked_row.get('dict_type').get('content'), + dict_data_search_nclick + 1 if dict_data_search_nclick else 1, + {'timestamp': time.time()}, + ] + + return [ + True, + '字典数据', + [], + recently_button_clicked_row.get('dict_type').get('content'), + dict_data_search_nclick + 1 if dict_data_search_nclick else 1, + {'timestamp': time.time()}, + ] + + return [dash.no_update] * 6 + diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py new file mode 100644 index 0000000..6ea7dd8 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py @@ -0,0 +1,342 @@ +import dash +import time +import uuid +from dash import html +from dash.dependencies import Input, Output, State +import feffery_antd_components as fac +import feffery_utils_components as fuc + +from server import app +from api.dict import get_dict_data_list_api, get_dict_data_detail_api, add_dict_data_api, edit_dict_data_api, delete_dict_data_api + + +@app.callback( + [Output('dict_data-list-table', 'data', allow_duplicate=True), + Output('dict_data-list-table', 'pagination', allow_duplicate=True), + Output('dict_data-list-table', 'key'), + Output('dict_data-list-table', 'selectedRowKeys'), + Output('api-check-token', 'data', allow_duplicate=True)], + [Input('dict_data-search', 'nClicks'), + Input('dict_data-list-table', 'pagination'), + Input('dict_data-operations-store', 'data')], + [State('dict_data-dict_type-select', 'value'), + State('dict_data-dict_label-input', 'value'), + State('dict_data-status-select', 'value'), + State('dict_data-button-perms-container', 'data')], + prevent_initial_call=True +) +def get_dict_data_table_data(search_click, pagination, operations, dict_type, dict_label, status_select, button_perms): + + query_params = dict( + dict_type=dict_type, + dict_label=dict_label, + status=status_select, + page_num=1, + page_size=10 + ) + if pagination: + query_params = dict( + dict_type=dict_type, + dict_label=dict_label, + status=status_select, + page_num=pagination['current'], + page_size=pagination['pageSize'] + ) + if search_click or pagination or operations: + table_info = get_dict_data_list_api(query_params) + if table_info['code'] == 200: + table_data = table_info['data']['rows'] + table_pagination = dict( + pageSize=table_info['data']['page_size'], + current=table_info['data']['page_num'], + showSizeChanger=True, + pageSizeOptions=[10, 30, 50, 100], + showQuickJumper=True, + total=table_info['data']['total'] + ) + for item in table_data: + if item['status'] == '0': + item['status'] = dict(tag='正常', color='blue') + else: + item['status'] = dict(tag='停用', color='volcano') + item['key'] = str(item['dict_code']) + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dict:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:dict:remove' in button_perms else {}, + ] + + return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] + + return [dash.no_update, dash.no_update, dash.no_update, dash.no_update, {'timestamp': time.time()}] + + return [dash.no_update] * 5 + + +@app.callback( + [Output('dict_data-dict_type-select', 'value', allow_duplicate=True), + Output('dict_data-dict_label-input', 'value'), + Output('dict_data-status-select', 'value'), + Output('dict_data-operations-store', 'data')], + Input('dict_data-reset', 'nClicks'), + prevent_initial_call=True +) +def reset_dict_data_query_params(reset_click): + if reset_click: + return [None, None, None, {'type': 'reset'}] + + return [dash.no_update] * 4 + + +@app.callback( + [Output('dict_data-edit', 'disabled'), + Output('dict_data-delete', 'disabled')], + Input('dict_data-list-table', 'selectedRowKeys'), + prevent_initial_call=True +) +def change_dict_data_edit_delete_button_status(table_rows_selected): + if table_rows_selected: + if len(table_rows_selected) > 1: + return [True, False] + + return [False, False] + + return [True, True] + + +@app.callback( + [Output('dict_data-modal', 'visible', allow_duplicate=True), + Output('dict_data-modal', 'title'), + Output('dict_data-dict_type', 'value'), + Output('dict_data-dict_label', 'value'), + Output('dict_data-dict_value', 'value'), + Output('dict_data-css_class', 'value'), + Output('dict_data-dict_sort', 'value'), + Output('dict_data-list_class', 'value'), + Output('dict_data-status', 'value'), + Output('dict_data-remark', 'value'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('dict_data-add', 'nClicks'), + Output('dict_data-edit', 'nClicks'), + Output('dict_data-edit-id-store', 'data'), + Output('dict_data-operations-store-bk', 'data')], + [Input('dict_data-add', 'nClicks'), + Input('dict_data-edit', 'nClicks'), + Input('dict_data-list-table', 'nClicksButton')], + [State('dict_data-list-table', 'selectedRowKeys'), + State('dict_data-list-table', 'clickedContent'), + State('dict_data-list-table', 'recentlyButtonClickedRow'), + State('dict_data-dict_type-select', 'value')], + prevent_initial_call=True +) +def add_edit_dict_data_modal(add_click, edit_click, button_click, selected_row_keys, clicked_content, + recently_button_clicked_row, dict_type_select): + if add_click or edit_click or button_click: + if add_click: + return [ + True, + '新增字典数据', + dict_type_select, + None, + None, + None, + 0, + 'default', + '0', + None, + {'timestamp': time.time()}, + None, + None, + None, + {'type': 'add'} + ] + elif edit_click or (button_click and clicked_content == '修改'): + if edit_click: + dict_code = int(','.join(selected_row_keys)) + else: + dict_code = int(recently_button_clicked_row['key']) + dict_data_info_res = get_dict_data_detail_api(dict_code=dict_code) + if dict_data_info_res['code'] == 200: + dict_data_info = dict_data_info_res['data'] + return [ + True, + '编辑字典数据', + dict_data_info.get('dict_type'), + dict_data_info.get('dict_label'), + dict_data_info.get('dict_value'), + dict_data_info.get('css_class'), + dict_data_info.get('dict_sort'), + dict_data_info.get('list_class'), + dict_data_info.get('status'), + dict_data_info.get('remark'), + {'timestamp': time.time()}, + None, + None, + dict_data_info if dict_data_info else None, + {'type': 'edit'} + ] + + return [dash.no_update] * 10 + [{'timestamp': time.time()}, None, None, None, None] + + return [dash.no_update] * 11 + [None, None, None, None] + + +@app.callback( + [Output('dict_data-dict_label-form-item', 'validateStatus'), + Output('dict_data-dict_value-form-item', 'validateStatus'), + Output('dict_data-dict_sort-form-item', 'validateStatus'), + Output('dict_data-dict_label-form-item', 'help'), + Output('dict_data-dict_value-form-item', 'help'), + Output('dict_data-dict_sort-form-item', 'help'), + Output('dict_data-modal', 'visible'), + Output('dict_data-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_data-modal', 'okCounts'), + [State('dict_data-operations-store-bk', 'data'), + State('dict_data-edit-id-store', 'data'), + State('dict_data-dict_type', 'value'), + State('dict_data-dict_label', 'value'), + State('dict_data-dict_value', 'value'), + State('dict_data-css_class', 'value'), + State('dict_data-dict_sort', 'value'), + State('dict_data-list_class', 'value'), + State('dict_data-status', 'value'), + State('dict_data-remark', 'value')], + prevent_initial_call=True +) +def dict_data_confirm(confirm_trigger, operation_type, cur_post_info, dict_type, dict_label, dict_value, css_class, dict_sort, list_class, status, remark): + if confirm_trigger: + if all([dict_label, dict_value, dict_sort]): + params_add = dict(dict_type=dict_type, dict_label=dict_label, dict_value=dict_value, css_class=css_class, dict_sort=dict_sort, list_class=list_class, status=status, remark=remark) + params_edit = dict(dict_code=cur_post_info.get('dict_code') if cur_post_info else None, dict_type=dict_type, dict_label=dict_label, dict_value=dict_value, css_class=css_class, dict_sort=dict_sort, list_class=list_class, status=status, remark=remark) + api_res = {} + operation_type = operation_type.get('type') + if operation_type == 'add': + api_res = add_dict_data_api(params_add) + if operation_type == 'edit': + api_res = edit_dict_data_api(params_edit) + if api_res.get('code') == 200: + if operation_type == 'add': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'add'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('新增成功', type='success') + ] + if operation_type == 'edit': + return [ + None, + None, + None, + None, + None, + None, + False, + {'type': 'edit'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('编辑成功', type='success') + ] + + return [ + None, + None, + None, + None, + None, + None, + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [ + None if dict_label else 'error', + None if dict_value else 'error', + None if dict_sort else 'error', + None if dict_label else '请输入数据标签!', + None if dict_value else '请输入数据键值!', + None if dict_sort else '请输入显示排序!', + dash.no_update, + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('处理失败', type='error') + ] + + return [dash.no_update] * 10 + + +@app.callback( + [Output('dict_data-delete-text', 'children'), + Output('dict_data-delete-confirm-modal', 'visible'), + Output('dict_data-delete-ids-store', 'data')], + [Input('dict_data-delete', 'nClicks'), + Input('dict_data-list-table', 'nClicksButton')], + [State('dict_data-list-table', 'selectedRowKeys'), + State('dict_data-list-table', 'clickedContent'), + State('dict_data-list-table', 'recentlyButtonClickedRow')], + prevent_initial_call=True +) +def dict_data_delete_modal(delete_click, button_click, + selected_row_keys, clicked_content, recently_button_clicked_row): + if delete_click or button_click: + trigger_id = dash.ctx.triggered_id + + if trigger_id == 'dict_data-delete': + dict_codes = ','.join(selected_row_keys) + else: + if clicked_content == '删除': + dict_codes = recently_button_clicked_row['key'] + else: + return dash.no_update + + return [ + f'是否确认删除字典编码为{dict_codes}的数据?', + True, + {'dict_codes': dict_codes} + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('dict_data-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('dict_data-delete-confirm-modal', 'okCounts'), + State('dict_data-delete-ids-store', 'data'), + prevent_initial_call=True +) +def dict_data_delete_confirm(delete_confirm, dict_codes_data): + if delete_confirm: + + params = dict_codes_data + delete_button_info = delete_dict_data_api(params) + if delete_button_info['code'] == 200: + return [ + {'type': 'delete'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('删除失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/store/store.py b/dash-fastapi-frontend/store/store.py index f9ad59f..096faad 100644 --- a/dash-fastapi-frontend/store/store.py +++ b/dash-fastapi-frontend/store/store.py @@ -62,10 +62,14 @@ def render_store_container(): # 字典管理模块操作类型存储容器 dcc.Store(id='dict_type-operations-store'), dcc.Store(id='dict_type-operations-store-bk'), + dcc.Store(id='dict_data-operations-store'), + dcc.Store(id='dict_data-operations-store-bk'), # 字典管理模块修改操作行key存储容器 dcc.Store(id='dict_type-edit-id-store'), + dcc.Store(id='dict_data-edit-id-store'), # 字典管理模块删除操作行key存储容器 dcc.Store(id='dict_type-delete-ids-store'), + dcc.Store(id='dict_data-delete-ids-store'), # 操作日志管理模块操作类型存储容器 dcc.Store(id='operation_log-operations-store'), # 操作日志管理模块删除操作行key存储容器 diff --git a/dash-fastapi-frontend/views/system/dict/__init__.py b/dash-fastapi-frontend/views/system/dict/__init__.py index fd264a0..4a8f4e7 100644 --- a/dash-fastapi-frontend/views/system/dict/__init__.py +++ b/dash-fastapi-frontend/views/system/dict/__init__.py @@ -1,7 +1,8 @@ from dash import dcc, html import feffery_antd_components as fac -import callbacks.system_c.dict_c +import callbacks.system_c.dict_c.dict_c +from . import dict_data from api.dict import get_dict_type_list_api @@ -24,12 +25,10 @@ def render(button_perms): else: item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dict_id']) - item['dict_type'] = [ - { - 'content': item['dict_type'], - 'type': 'link', - } - ] + item['dict_type'] = { + 'content': item['dict_type'], + 'type': 'link', + } item['operation'] = [ { 'content': '修改', @@ -457,4 +456,15 @@ def render(button_perms): renderFooter=True, centered=True ), + + # 字典数据modal + fac.AntdModal( + dict_data.render(button_perms), + id='dict_type_to_dict_data-modal', + mask=False, + maskClosable=False, + width=1200, + renderFooter=True, + okClickClose=False + ) ] diff --git a/dash-fastapi-frontend/views/system/dict/dict_data.py b/dash-fastapi-frontend/views/system/dict/dict_data.py new file mode 100644 index 0000000..f959c54 --- /dev/null +++ b/dash-fastapi-frontend/views/system/dict/dict_data.py @@ -0,0 +1,504 @@ +from dash import dcc, html +import feffery_antd_components as fac + +import callbacks.system_c.dict_c.dict_data_c + + +def render(button_perms): + + return [ + dcc.Store(id='dict_data-button-perms-container', data=button_perms), + fac.AntdRow( + [ + fac.AntdCol( + [ + fac.AntdRow( + [ + fac.AntdCol( + html.Div( + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdSelect( + id='dict_data-dict_type-select', + placeholder='字典名称', + options=[], + allowClear=False, + style={ + 'width': 240 + } + ), + label='字典名称', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_label-input', + placeholder='请输入字典标签', + autoComplete='off', + allowClear=True, + style={ + 'width': 240 + } + ), + label='字典标签', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdSelect( + id='dict_data-status-select', + placeholder='数据状态', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + } + ], + style={ + 'width': 240 + } + ), + label='状态', + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '搜索', + id='dict_data-search', + type='primary', + icon=fac.AntdIcon( + icon='antd-search' + ) + ), + style={'paddingBottom': '10px'}, + ), + fac.AntdFormItem( + fac.AntdButton( + '重置', + id='dict_data-reset', + icon=fac.AntdIcon( + icon='antd-sync' + ) + ), + style={'paddingBottom': '10px'}, + ) + ], + layout='inline', + ) + ], + hidden='system:dict:query' not in button_perms + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpace( + [ + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-plus' + ), + '新增', + ], + id='dict_data-add', + style={ + 'color': '#1890ff', + 'background': '#e8f4ff', + 'border-color': '#a3d3ff' + } + ), + ], + hidden='system:dict:add' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-edit' + ), + '修改', + ], + id='dict_data-edit', + disabled=True, + style={ + 'color': '#71e2a3', + 'background': '#e7faf0', + 'border-color': '#d0f5e0' + } + ), + ], + hidden='system:dict:edit' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-minus' + ), + '删除', + ], + id='dict_data-delete', + disabled=True, + style={ + 'color': '#ff9292', + 'background': '#ffeded', + 'border-color': '#ffdbdb' + } + ), + ], + hidden='system:dict:remove' not in button_perms + ), + html.Div( + [ + fac.AntdButton( + [ + fac.AntdIcon( + icon='antd-arrow-down' + ), + '导出', + ], + id='dict_data-export', + style={ + 'color': '#ffba00', + 'background': '#fff8e6', + 'border-color': '#ffe399' + } + ), + ], + hidden='system:dict:export' not in button_perms + ), + ], + style={ + 'paddingBottom': '10px' + } + ), + ) + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdSpin( + fac.AntdTable( + id='dict_data-list-table', + data=[], + columns=[ + { + 'dataIndex': 'dict_code', + 'title': '字典编码', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_label', + 'title': '字典标签', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_value', + 'title': '字典键值', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'dict_sort', + 'title': '字典排序', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'status', + 'title': '状态', + 'renderOptions': { + 'renderType': 'tags' + }, + }, + { + 'dataIndex': 'remark', + 'title': '备注', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'dataIndex': 'create_time', + 'title': '创建时间', + 'renderOptions': { + 'renderType': 'ellipsis' + }, + }, + { + 'title': '操作', + 'dataIndex': 'operation', + 'renderOptions': { + 'renderType': 'button' + }, + } + ], + rowSelectionType='checkbox', + rowSelectionWidth=50, + bordered=True, + pagination={ + 'pageSize': 10, + 'current': 1, + 'showSizeChanger': True, + 'pageSizeOptions': [10, 30, 50, 100], + 'showQuickJumper': True, + 'total': 0 + }, + mode='server-side', + style={ + 'width': '100%', + 'padding-right': '10px' + } + ), + text='数据加载中' + ), + ) + ] + ), + ], + span=24 + ) + ], + gutter=5 + ), + + # 新增和编辑字典数据表单modal + fac.AntdModal( + [ + fac.AntdForm( + [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_type', + placeholder='请输入字典类型', + disabled=True, + style={ + 'width': 350 + } + ), + label='字典类型', + id='dict_data-dict_type-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_label', + placeholder='请输入数据标签', + allowClear=True, + style={ + 'width': 350 + } + ), + label='数据标签', + required=True, + id='dict_data-dict_label-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-dict_value', + placeholder='请输入数据键值', + allowClear=True, + style={ + 'width': 350 + } + ), + label='数据键值', + required=True, + id='dict_data-dict_value-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-css_class', + placeholder='请输入样式属性', + allowClear=True, + style={ + 'width': 350 + } + ), + label='样式属性', + id='dict_data-css_class-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInputNumber( + id='dict_data-dict_sort', + defaultValue=0, + min=0, + style={ + 'width': 350 + } + ), + label='显示排序', + required=True, + id='dict_data-dict_sort-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdSelect( + id='dict_data-list_class', + placeholder='回显样式', + options=[ + { + 'label': '默认', + 'value': 'default' + }, + { + 'label': '主要', + 'value': 'primary' + }, + { + 'label': '成功', + 'value': 'success' + }, + { + 'label': '信息', + 'value': 'info' + }, + { + 'label': '警告', + 'value': 'warning' + }, + { + 'label': '危险', + 'value': 'danger' + } + ], + style={ + 'width': 350 + } + ), + label='回显样式', + id='dict_data-list_class-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdRadioGroup( + id='dict_data-status', + options=[ + { + 'label': '正常', + 'value': '0' + }, + { + 'label': '停用', + 'value': '1' + }, + ], + defaultValue='0', + style={ + 'width': 350 + } + ), + label='状态', + id='dict_data-status-form-item' + ), + span=24 + ), + ] + ), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdFormItem( + fac.AntdInput( + id='dict_data-remark', + placeholder='请输入内容', + allowClear=True, + mode='text-area', + style={ + 'width': 350 + } + ), + label='备注', + id='dict_data-remark-form-item' + ), + span=24 + ), + ] + ), + ], + labelCol={ + 'span': 6 + }, + wrapperCol={ + 'span': 18 + } + ) + ], + id='dict_data-modal', + mask=False, + maskClosable=False, + width=580, + renderFooter=True, + okClickClose=False + ), + + # 删除字典数据二次确认modal + fac.AntdModal( + fac.AntdText('是否确认删除?', id='dict_data-delete-text'), + id='dict_data-delete-confirm-modal', + visible=False, + title='提示', + renderFooter=True + ), + ] -- Gitee From 6faffea65f26103a90d739509aa3fa18e52cf5cc Mon Sep 17 00:00:00 2001 From: xlf Date: Fri, 21 Jul 2023 10:44:36 +0800 Subject: [PATCH 027/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=B5=84=E6=96=99=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=88=E5=BC=95=E5=85=A5cropper.js=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E8=A3=81=E5=89=AA=EF=BC=89=EF=BC=8C=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=96=B0=E5=A2=9E=E9=80=9A=E7=94=A8=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8F=8A=E4=B8=8B=E8=BD=BD=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 3 + .../caches/avatar/ry_avatar.jpeg | Bin 0 -> 216574 bytes dash-fastapi-backend/config/env.py | 11 ++ .../module_admin/annotation/log_annotation.py | 1 - .../controller/common_controller.py | 40 ++++ .../controller/login_controller.py | 2 +- .../controller/user_controller.py | 77 ++++++++ .../module_admin/entity/vo/user_vo.py | 7 + .../module_admin/service/common_service.py | 14 ++ .../module_admin/service/user_service.py | 25 ++- dash-fastapi-frontend/api/user.py | 15 ++ dash-fastapi-frontend/app.py | 11 +- .../assets/css/cropper.min.css | 9 + .../assets/js/cropper.min.js | 10 + .../callbacks/layout_c/head_c.py | 9 +- .../callbacks/layout_c/index_c.py | 15 +- .../callbacks/monitor_c/logininfor_c.py | 3 +- .../callbacks/monitor_c/operlog_c.py | 3 +- .../callbacks/system_c/dict_c/dict_c.py | 3 +- .../callbacks/system_c/dict_c/dict_data_c.py | 10 +- .../callbacks/system_c/post_c.py | 3 +- .../callbacks/system_c/role_c.py | 3 +- .../system_c/user_c/profile_c/avatar_c.py | 154 +++++++++++++++ .../system_c/user_c/profile_c/reset_pwd_c.py | 91 +++++++++ .../system_c/user_c/profile_c/user_info_c.py | 79 ++++++++ .../callbacks/system_c/{ => user_c}/user_c.py | 3 +- dash-fastapi-frontend/config/global_config.py | 3 + .../views/layout/__init__.py | 59 ++---- .../views/layout/components/content.py | 5 +- .../views/layout/components/head.py | 18 +- .../views/system/user/__init__.py | 2 +- .../views/system/user/profile/__init__.py | 182 ++++++++++++++++++ .../views/system/user/profile/reset_pwd.py | 66 +++++++ .../views/system/user/profile/user_avatar.py | 177 +++++++++++++++++ .../views/system/user/profile/user_info.py | 84 ++++++++ 35 files changed, 1110 insertions(+), 87 deletions(-) create mode 100644 dash-fastapi-backend/caches/avatar/ry_avatar.jpeg create mode 100644 dash-fastapi-backend/module_admin/controller/common_controller.py create mode 100644 dash-fastapi-backend/module_admin/service/common_service.py create mode 100644 dash-fastapi-frontend/assets/css/cropper.min.css create mode 100644 dash-fastapi-frontend/assets/js/cropper.min.js create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py create mode 100644 dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py rename dash-fastapi-frontend/callbacks/system_c/{ => user_c}/user_c.py (99%) create mode 100644 dash-fastapi-frontend/views/system/user/profile/__init__.py create mode 100644 dash-fastapi-frontend/views/system/user/profile/reset_pwd.py create mode 100644 dash-fastapi-frontend/views/system/user/profile/user_avatar.py create mode 100644 dash-fastapi-frontend/views/system/user/profile/user_info.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 246d710..6fd8a88 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -13,6 +13,7 @@ from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from module_admin.controller.dict_controller import dictController from module_admin.controller.log_controller import logController +from module_admin.controller.common_controller import commonController from config.env import RedisConfig from utils.response_util import response_401, AuthException @@ -22,6 +23,7 @@ app = FastAPI() # 前端页面url origins = [ "http://localhost:8088", + "http://127.0.0.1:8088", ] # 后台api允许跨域 @@ -79,6 +81,7 @@ app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) app.include_router(dictController, prefix="/system", tags=['system/dict']) app.include_router(logController, prefix="/system", tags=['system/log']) +app.include_router(commonController, prefix="/common", tags=['common']) if __name__ == '__main__': diff --git a/dash-fastapi-backend/caches/avatar/ry_avatar.jpeg b/dash-fastapi-backend/caches/avatar/ry_avatar.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7a683af3b9fceb35b19a74c94ce5da212b2b89e9 GIT binary patch literal 216574 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

XMAFL2 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 literal 0 HcmV?d00001 diff --git a/dash-fastapi-backend/config/env.py b/dash-fastapi-backend/config/env.py index d602163..fbfb944 100644 --- a/dash-fastapi-backend/config/env.py +++ b/dash-fastapi-backend/config/env.py @@ -1,3 +1,6 @@ +import os + + class JwtConfig: """ Jwt配置 @@ -27,3 +30,11 @@ class RedisConfig: USERNAME = '' PASSWORD = '' DB = 2 + + +class CachePathConfig: + """ + 缓存目录配置 + """ + PATH = os.path.join(os.path.abspath(os.getcwd()), 'caches') + PATHSTR = 'caches' diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py index ddeac6c..5ef2342 100644 --- a/dash-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -79,7 +79,6 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope else: error_msg = result_dict.get('message') if log_type == 'login': - # print(request.headers) user_agent_info = parse(user_agent) browser = f'{user_agent_info.browser.family} {user_agent_info.browser.version[0]}' system_os = f'{user_agent_info.os.family} {user_agent_info.os.version[0]}' diff --git a/dash-fastapi-backend/module_admin/controller/common_controller.py b/dash-fastapi-backend/module_admin/controller/common_controller.py new file mode 100644 index 0000000..5be2b13 --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/common_controller.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, UploadFile, File, Form +from fastapi.responses import StreamingResponse +from config.env import CachePathConfig +from module_admin.service.login_service import get_current_user +from module_admin.service.common_service import * +from utils.response_util import * +from utils.log_util import * +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth + + +commonController = APIRouter() + + +@commonController.post("/upload", dependencies=[Depends(get_current_user), Depends(CheckUserInterfaceAuth('common'))]) +async def common_upload(request: Request, uploadId: str = Form(), file: UploadFile = File(...)): + try: + try: + os.makedirs(os.path.join(CachePathConfig.PATH, uploadId)) + except FileExistsError: + pass + upload_service(CachePathConfig.PATH, uploadId, file) + logger.info('上传成功') + return response_200(data={'filename': file.filename}, message="上传成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@commonController.get(f"/{CachePathConfig.PATHSTR}") +def common_download(request: Request, taskId: str, filename: str): + try: + def generate_file(): + with open(os.path.join(CachePathConfig.PATH, taskId, filename), 'rb') as response_file: + yield from response_file + logger.info('获取成功') + return StreamingResponse(generate_file()) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/login_controller.py b/dash-fastapi-backend/module_admin/controller/login_controller.py index 6876ba4..0a91b56 100644 --- a/dash-fastapi-backend/module_admin/controller/login_controller.py +++ b/dash-fastapi-backend/module_admin/controller/login_controller.py @@ -15,7 +15,7 @@ loginController = APIRouter() @loginController.post("/loginByAccount", response_model=Token) -@log_decorator(title='用户登录', business_type=0, log_type='login') +# @log_decorator(title='用户登录', business_type=0, log_type='login') async def login(request: Request, user: UserLogin, query_db: Session = Depends(get_db)): try: result = authenticate_user(query_db, user.user_name, user.password) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index bfaafef..f58be54 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, Request from fastapi import Depends, Header +import base64 from config.get_db import get_db +from config.env import CachePathConfig from module_admin.service.login_service import get_current_user, get_password_hash from module_admin.service.user_service import * from module_admin.entity.vo.user_vo import * @@ -91,3 +93,78 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") + + +@userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + avatar = edit_user.avatar + # 去除 base64 字符串中的头部信息(data:image/jpeg;base64, 等等) + base64_string = avatar.split(',', 1)[1] + # 解码 base64 字符串 + file_data = base64.b64decode(base64_string) + dir_path = os.path.join(CachePathConfig.PATH, 'avatar') + try: + os.makedirs(dir_path) + except FileExistsError: + pass + filepath = os.path.join(dir_path, f'{current_user.user.user_name}_avatar.jpeg') + with open(filepath, 'wb') as f: + f.write(file_data) + edit_user.user_id = current_user.user.user_id + edit_user.avatar = f'{request.base_url}common/{CachePathConfig.PATHSTR}?taskId=avatar&filename={current_user.user.user_name}_avatar.jpeg' + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_user_result = edit_user_services(query_db, edit_user) + if edit_user_result.is_success: + logger.info(edit_user_result.message) + return response_200(data=edit_user_result, message=edit_user_result.message) + else: + logger.warning(edit_user_result.message) + return response_400(data="", message=edit_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.patch("/user/profile/changeInfo", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +@log_decorator(title='个人信息', business_type=2) +async def change_system_user_profile_info(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_user.user_id = current_user.user.user_id + edit_user.update_by = current_user.user.user_name + edit_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_user_result = edit_user_services(query_db, edit_user) + if edit_user_result.is_success: + logger.info(edit_user_result.message) + return response_200(data=edit_user_result, message=edit_user_result.message) + else: + logger.warning(edit_user_result.message) + return response_400(data="", message=edit_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@userController.patch("/user/profile/resetPwd", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +@log_decorator(title='个人信息', business_type=2) +async def reset_system_user_password(request: Request, reset_user: ResetUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + if not reset_user.user_id: + reset_user.user_id = current_user.user.user_id + reset_user.password = get_password_hash(reset_user.password) + reset_user.update_by = current_user.user.user_name + reset_user.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + reset_user_result = reset_user_services(query_db, reset_user) + if reset_user_result.is_success: + logger.info(reset_user_result.message) + return response_200(data=reset_user_result, message=reset_user_result.message) + else: + logger.warning(reset_user_result.message) + return response_400(data="", message=reset_user_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py index 7c9c700..94cfe66 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -207,6 +207,13 @@ class AddUserModel(UserModel): type: Optional[str] +class ResetUserModel(UserModel): + """ + 重置用户密码模型 + """ + old_password: Optional[str] + + class DeleteUserModel(BaseModel): """ 删除用户模型 diff --git a/dash-fastapi-backend/module_admin/service/common_service.py b/dash-fastapi-backend/module_admin/service/common_service.py new file mode 100644 index 0000000..a0bb413 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/common_service.py @@ -0,0 +1,14 @@ +import os +from datetime import datetime +from fastapi import UploadFile +from config.env import CachePathConfig + + +def upload_service(path: str, upload_id: str, file: UploadFile): + + filepath = os.path.join(path, upload_id, f'{file.filename}') + with open(filepath, 'wb') as f: + # 流式写出大型文件,这里的10代表10MB + for chunk in iter(lambda: file.file.read(1024 * 1024 * 10), b''): + f.write(chunk) + diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 230de75..4c68e2d 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -1,5 +1,6 @@ from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * +from module_admin.service.login_service import verify_password def get_user_list_services(result_db: Session, page_object: UserPageObject): @@ -47,13 +48,13 @@ def edit_user_services(result_db: Session, page_object: AddUserModel): :return: 编辑用户校验结果 """ edit_user = page_object.dict(exclude_unset=True) - if page_object.type != 'status': + if page_object.type != 'status' and page_object.type != 'avatar': del edit_user['role_id'] del edit_user['post_id'] - if page_object.type == 'status': + if page_object.type == 'status' or page_object.type == 'avatar': del edit_user['type'] edit_user_result = edit_user_dao(result_db, edit_user) - if edit_user_result.is_success and page_object.type != 'status': + if edit_user_result.is_success and page_object.type != 'status' and page_object.type != 'avatar': user_id_dict = dict(user_id=page_object.user_id) delete_user_role_dao(result_db, UserRoleModel(**user_id_dict)) delete_user_post_dao(result_db, UserPostModel(**user_id_dict)) @@ -106,3 +107,21 @@ def detail_user_services(result_db: Session, user_id: int): role=user.user_role_info, post=user.user_post_info ) + + +def reset_user_services(result_db: Session, page_object: ResetUserModel): + """ + 重置用户密码service + :param result_db: orm对象 + :param page_object: 重置用户对象 + :return: 重置用户校验结果 + """ + user = get_user_detail_by_id(result_db, user_id=page_object.user_id).user_basic_info[0] + if not verify_password(page_object.old_password, user.password): + result = CrudUserResponse(**dict(is_success=False, message='旧密码不正确')) + else: + reset_user = page_object.dict(exclude_unset=True) + del reset_user['old_password'] + result = edit_user_dao(result_db, reset_user) + + return result diff --git a/dash-fastapi-frontend/api/user.py b/dash-fastapi-frontend/api/user.py index 2edf88d..4ca0f5d 100644 --- a/dash-fastapi-frontend/api/user.py +++ b/dash-fastapi-frontend/api/user.py @@ -29,3 +29,18 @@ def delete_user_api(page_obj: dict): def get_user_detail_api(user_id: int): return api_request(method='get', url=f'/system/user/{user_id}', is_headers=True) + + +def change_user_avatar_api(page_obj: dict): + + return api_request(method='patch', url='/system/user/profile/changeAvatar', is_headers=True, json=page_obj) + + +def change_user_info_api(page_obj: dict): + + return api_request(method='patch', url='/system/user/profile/changeInfo', is_headers=True, json=page_obj) + + +def reset_user_password_api(page_obj: dict): + + return api_request(method='patch', url='/system/user/profile/resetPwd', is_headers=True, json=page_obj) diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index c827f04..ff353f4 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -82,9 +82,6 @@ def router(pathname, trigger): current_user_result = get_current_user_info_api() if current_user_result['code'] == 200: current_user = current_user_result['data'] - user_name = current_user['user']['user_name'] - nick_name = current_user['user']['nick_name'] - phone_number = current_user['user']['phonenumber'] menu_list = current_user['menu'] user_menu_list = [item for item in menu_list if item.get('visible') == '0'] menu_info = deal_user_menu_info(0, menu_list) @@ -94,7 +91,7 @@ def router(pathname, trigger): session['role_info'] = current_user['role'] session['post_info'] = current_user['post'] valid_href_list = find_node_values(menu_info, 'href') - valid_href_list.append('/') + valid_href_list = valid_href_list + RouterConfig.STATIC_VALID_PATHNAME if pathname in valid_href_list: current_key = find_key_by_href(menu_info, pathname) if trigger == 'load': @@ -102,6 +99,8 @@ def router(pathname, trigger): # 根据pathname控制渲染行为 if pathname == '/': current_key = '首页' + if pathname == '/user/profile': + current_key = '个人资料' if pathname == '/login' or pathname == '/forget': # 重定向到主页面 return [ @@ -119,7 +118,7 @@ def router(pathname, trigger): # 否则正常渲染主页面 return [ - views.layout.render_content(user_name, nick_name, phone_number, user_menu_info), + views.layout.render_content(user_menu_info), None, fuc.FefferyFancyNotification('进入主页面', type='success', autoClose=2000), {'timestamp': time.time()}, @@ -132,6 +131,8 @@ def router(pathname, trigger): else: if pathname == '/': current_key = '首页' + if pathname == '/user/profile': + current_key = '个人资料' return [ dash.no_update, None, diff --git a/dash-fastapi-frontend/assets/css/cropper.min.css b/dash-fastapi-frontend/assets/css/cropper.min.css new file mode 100644 index 0000000..e97743a --- /dev/null +++ b/dash-fastapi-frontend/assets/css/cropper.min.css @@ -0,0 +1,9 @@ +/*! + * Cropper.js v1.5.13 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2022-11-20T05:30:43.444Z + */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-canvas,.cropper-crop-box,.cropper-drag-box,.cropper-modal,.cropper-wrap-box{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-canvas,.cropper-wrap-box{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:rgba(51,153,255,.75);overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:33.33333%;left:0;top:33.33333%;width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:33.33333%;top:0;width:33.33333%}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:after,.cropper-center:before{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width:768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width:992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width:1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC")}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed} \ No newline at end of file diff --git a/dash-fastapi-frontend/assets/js/cropper.min.js b/dash-fastapi-frontend/assets/js/cropper.min.js new file mode 100644 index 0000000..03aed4c --- /dev/null +++ b/dash-fastapi-frontend/assets/js/cropper.min.js @@ -0,0 +1,10 @@ +/*! + * Cropper.js v1.5.13 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2022-11-20T05:30:46.114Z + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Cropper=e()}(this,function(){"use strict";function C(e,t){var i,a=Object.keys(e);return Object.getOwnPropertySymbols&&(i=Object.getOwnPropertySymbols(e),t&&(i=i.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),a.push.apply(a,i)),a}function S(a){for(var t=1;tt.length)&&(e=t.length);for(var i=0,a=new Array(e);it.width?3===i?o=t.height*e:h=t.width/e:3===i?h=t.width/e:o=t.height*e,{aspectRatio:e,naturalWidth:n,naturalHeight:a,width:o,height:h});this.canvasData=e,this.limited=1===i||2===i,this.limitCanvas(!0,!0),e.width=Math.min(Math.max(e.width,e.minWidth),e.maxWidth),e.height=Math.min(Math.max(e.height,e.minHeight),e.maxHeight),e.left=(t.width-e.width)/2,e.top=(t.height-e.height)/2,e.oldLeft=e.left,e.oldTop=e.top,this.initialCanvasData=g({},e)},limitCanvas:function(t,e){var i=this.options,a=this.containerData,n=this.canvasData,o=this.cropBoxData,h=i.viewMode,r=n.aspectRatio,s=this.cropped&&o;t&&(t=Number(i.minCanvasWidth)||0,i=Number(i.minCanvasHeight)||0,1=a.width&&(n.minLeft=Math.min(0,r),n.maxLeft=Math.max(0,r)),n.height>=a.height)&&(n.minTop=Math.min(0,t),n.maxTop=Math.max(0,t))):(n.minLeft=-n.width,n.minTop=-n.height,n.maxLeft=a.width,n.maxTop=a.height))},renderCanvas:function(t,e){var i,a,n,o,h=this.canvasData,r=this.imageData;e&&(e={width:r.naturalWidth*Math.abs(r.scaleX||1),height:r.naturalHeight*Math.abs(r.scaleY||1),degree:r.rotate||0},r=e.width,o=e.height,e=e.degree,i=90==(e=Math.abs(e)%180)?{width:o,height:r}:(a=e%90*Math.PI/180,i=Math.sin(a),n=r*(a=Math.cos(a))+o*i,r=r*i+o*a,90h.maxWidth||h.widthh.maxHeight||h.heighte.width?a.height=a.width/i:a.width=a.height*i),this.cropBoxData=a,this.limitCropBox(!0,!0),a.width=Math.min(Math.max(a.width,a.minWidth),a.maxWidth),a.height=Math.min(Math.max(a.height,a.minHeight),a.maxHeight),a.width=Math.max(a.minWidth,a.width*t),a.height=Math.max(a.minHeight,a.height*t),a.left=e.left+(e.width-a.width)/2,a.top=e.top+(e.height-a.height)/2,a.oldLeft=a.left,a.oldTop=a.top,this.initialCropBoxData=g({},a)},limitCropBox:function(t,e){var i,a,n=this.options,o=this.containerData,h=this.canvasData,r=this.cropBoxData,s=this.limited,c=n.aspectRatio;t&&(t=Number(n.minCropBoxWidth)||0,n=Number(n.minCropBoxHeight)||0,i=s?Math.min(o.width,h.width,h.width+h.left,o.width-h.left):o.width,a=s?Math.min(o.height,h.height,h.height+h.top,o.height-h.top):o.height,t=Math.min(t,o.width),n=Math.min(n,o.height),c&&(t&&n?ti.maxWidth||i.widthi.maxHeight||i.height=e.width&&i.height>=e.height?U:P),f(this.cropBox,g({width:i.width,height:i.height},x({translateX:i.left,translateY:i.top}))),this.cropped&&this.limited&&this.limitCanvas(!0,!0),this.disabled||this.output()},output:function(){this.preview(),y(this.element,_,this.getData())}},i={initPreview:function(){var t=this.element,i=this.crossOrigin,e=this.options.preview,a=i?this.crossOriginUrl:this.url,n=t.alt||"The image to preview",o=document.createElement("img");i&&(o.crossOrigin=i),o.src=a,o.alt=n,this.viewBox.appendChild(o),this.viewBoxImage=o,e&&("string"==typeof(o=e)?o=t.ownerDocument.querySelectorAll(e):e.querySelector&&(o=[e]),z(this.previews=o,function(t){var e=document.createElement("img");w(t,m,{width:t.offsetWidth,height:t.offsetHeight,html:t.innerHTML}),i&&(e.crossOrigin=i),e.src=a,e.alt=n,e.style.cssText='display:block;width:100%;height:auto;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important;image-orientation:0deg!important;"',t.innerHTML="",t.appendChild(e)}))},resetPreview:function(){z(this.previews,function(e){var i=Dt(e,m),i=(f(e,{width:i.width,height:i.height}),e.innerHTML=i.html,e),e=m;if(o(i[e]))try{delete i[e]}catch(t){i[e]=void 0}else if(i.dataset)try{delete i.dataset[e]}catch(t){i.dataset[e]=void 0}else i.removeAttribute("data-".concat(Ct(e)))})},preview:function(){var h=this.imageData,t=this.canvasData,e=this.cropBoxData,r=e.width,s=e.height,c=h.width,d=h.height,l=e.left-t.left-h.left,p=e.top-t.top-h.top;this.cropped&&!this.disabled&&(f(this.viewBoxImage,g({width:c,height:d},x(g({translateX:-l,translateY:-p},h)))),z(this.previews,function(t){var e=Dt(t,m),i=e.width,e=e.height,a=i,n=e,o=1;r&&(n=s*(o=i/r)),s&&eMath.abs(a-1)?i:a)&&(t.restore&&(o=this.getCanvasData(),h=this.getCropBoxData()),this.render(),t.restore)&&(this.setCanvasData(z(o,function(t,e){o[e]=t*n})),this.setCropBoxData(z(h,function(t,e){h[e]=t*n}))))},dblclick:function(){var t,e;this.disabled||this.options.dragMode===J||this.setDragMode((t=this.dragBox,e=$,(t.classList?t.classList.contains(e):-1y&&(D.x=y-f);break;case k:p+D.xx&&(D.y=x-v)}}var i,a,o,n=this.options,h=this.canvasData,r=this.containerData,s=this.cropBoxData,c=this.pointers,d=this.action,l=n.aspectRatio,p=s.left,m=s.top,u=s.width,g=s.height,f=p+u,v=m+g,w=0,b=0,y=r.width,x=r.height,M=!0,C=(!l&&t.shiftKey&&(l=u&&g?u/g:1),this.limited&&(w=s.minLeft,b=s.minTop,y=w+Math.min(r.width,h.width,h.left+h.width),x=b+Math.min(r.height,h.height,h.top+h.height)),c[Object.keys(c)[0]]),D={x:C.endX-C.startX,y:C.endY-C.startY};switch(d){case P:p+=D.x,m+=D.y;break;case B:0<=D.x&&(y<=f||l&&(m<=b||x<=v))?M=!1:(e(B),(u+=D.x)<0&&(d=k,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case T:D.y<=0&&(m<=b||l&&(p<=w||y<=f))?M=!1:(e(T),g-=D.y,m+=D.y,g<0&&(d=O,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case k:D.x<=0&&(p<=w||l&&(m<=b||x<=v))?M=!1:(e(k),u-=D.x,p+=D.x,u<0&&(d=B,p-=u=-u),l&&(m+=(s.height-(g=u/l))/2));break;case O:0<=D.y&&(x<=v||l&&(p<=w||y<=f))?M=!1:(e(O),(g+=D.y)<0&&(d=T,m-=g=-g),l&&(p+=(s.width-(u=g*l))/2));break;case E:if(l){if(D.y<=0&&(m<=b||y<=f)){M=!1;break}e(T),g-=D.y,m+=D.y,u=g*l}else e(T),e(B),!(0<=D.x)||fMath.abs(o)&&(o=i)})}),o),t),M=!1;break;case I:D.x&&D.y?(i=Et(this.cropper),p=C.startX-i.left,m=C.startY-i.top,u=s.minWidth,g=s.minHeight,0 or element.");this.element=t,this.options=g({},mt,u(e)&&e),this.cropped=!1,this.disabled=!1,this.pointers={},this.ready=!1,this.reloading=!1,this.replaced=!1,this.sized=!1,this.sizing=!1,this.init()}var t,e,i;return t=n,i=[{key:"noConflict",value:function(){return window.Cropper=jt,n}},{key:"setDefaults",value:function(t){g(mt,u(t)&&t)}}],(e=[{key:"init",value:function(){var t,e=this.element,i=e.tagName.toLowerCase();if(!e[c]){if(e[c]=this,"img"===i){if(this.isImg=!0,t=e.getAttribute("src")||"",!(this.originalUrl=t))return;t=e.src}else"canvas"===i&&window.HTMLCanvasElement&&(t=e.toDataURL());this.load(t)}}},{key:"load",value:function(t){var e,i,a,n,o,h,r=this;t&&(this.url=t,this.imageData={},e=this.element,(i=this.options).rotatable||i.scalable||(i.checkOrientation=!1),i.checkOrientation&&window.ArrayBuffer?dt.test(t)?lt.test(t)?this.read((h=(h=t).replace(Yt,""),a=atob(h),h=new ArrayBuffer(a.length),z(n=new Uint8Array(h),function(t,e){n[e]=a.charCodeAt(e)}),h)):this.clone():(o=new XMLHttpRequest,h=this.clone.bind(this),this.reloading=!0,(this.xhr=o).onabort=h,o.onerror=h,o.ontimeout=h,o.onprogress=function(){o.getResponseHeader("content-type")!==st&&o.abort()},o.onload=function(){r.read(o.response)},o.onloadend=function(){r.reloading=!1,r.xhr=null},i.checkCrossOrigin&&Nt(t)&&e.crossOrigin&&(t=Lt(t)),o.open("GET",t,!0),o.responseType="arraybuffer",o.withCredentials="use-credentials"===e.crossOrigin,o.send()):this.clone())}},{key:"read",value:function(t){var e=this.options,i=this.imageData,a=Xt(t),n=0,o=1,h=1;1

',o=(n=n.querySelector(".".concat(c,"-container"))).querySelector(".".concat(c,"-canvas")),h=n.querySelector(".".concat(c,"-drag-box")),s=(r=n.querySelector(".".concat(c,"-crop-box"))).querySelector(".".concat(c,"-face")),this.container=a,this.cropper=n,this.canvas=o,this.dragBox=h,this.cropBox=r,this.viewBox=n.querySelector(".".concat(c,"-view-box")),this.face=s,o.appendChild(i),v(t,L),a.insertBefore(n,t.nextSibling),X(i,K),this.initPreview(),this.bind(),e.initialAspectRatio=Math.max(0,e.initialAspectRatio)||NaN,e.aspectRatio=Math.max(0,e.aspectRatio)||NaN,e.viewMode=Math.max(0,Math.min(3,Math.round(e.viewMode)))||0,v(r,L),e.guides||v(r.getElementsByClassName("".concat(c,"-dashed")),L),e.center||v(r.getElementsByClassName("".concat(c,"-center")),L),e.background&&v(n,"".concat(c,"-bg")),e.highlight||v(s,Z),e.cropBoxMovable&&(v(s,G),w(s,d,P)),e.cropBoxResizable||(v(r.getElementsByClassName("".concat(c,"-line")),L),v(r.getElementsByClassName("".concat(c,"-point")),L)),this.render(),this.ready=!0,this.setDragMode(e.dragMode),e.autoCrop&&this.crop(),this.setData(e.data),l(e.ready)&&b(t,"ready",e.ready,{once:!0}),y(t,"ready"))}},{key:"unbuild",value:function(){var t;this.ready&&(this.ready=!1,this.unbind(),this.resetPreview(),(t=this.cropper.parentNode)&&t.removeChild(this.cropper),X(this.element,L))}},{key:"uncreate",value:function(){this.ready?(this.unbuild(),this.ready=!1,this.cropped=!1):this.sizing?(this.sizingImage.onload=null,this.sizing=!1,this.sized=!1):this.reloading?(this.xhr.onabort=null,this.xhr.abort()):this.image&&this.stop()}}])&&A(t.prototype,e),i&&A(t,i),Object.defineProperty(t,"prototype",{writable:!1}),n}();return g(Pt.prototype,t,i,e,Rt,St,At),Pt}); \ No newline at end of file diff --git a/dash-fastapi-frontend/callbacks/layout_c/head_c.py b/dash-fastapi-frontend/callbacks/layout_c/head_c.py index f93b772..d686f91 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/head_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/head_c.py @@ -1,6 +1,7 @@ import dash from dash import dcc from flask import session +import time from dash.dependencies import Input, Output, State from server import app @@ -9,7 +10,7 @@ from api.login import logout_api # 页首右侧个人中心选项卡回调 @app.callback( - [Output('index-personal-info-modal', 'visible'), + [Output('dcc-url', 'pathname', allow_duplicate=True), Output('logout-modal', 'visible')], Input('index-header-dropdown', 'nClicks'), State('index-header-dropdown', 'clickedKey'), @@ -18,17 +19,17 @@ from api.login import logout_api def index_dropdown_click(nClicks, clickedKey): if clickedKey == '退出登录': return [ - False, + dash.no_update, True ] elif clickedKey == '个人资料': return [ - True, + '/user/profile', False ] - return dash.no_update + return [dash.no_update] * 2 # 退出登录回调 diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 91bb76a..801f310 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -49,10 +49,15 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act currentKey ] - menu_title = find_title_by_key(menu_info.get('menu_info'), currentKey) - button_perms = [item.get('perms') for item in menu_list.get('menu_list') if str(item.get('parent_id')) == currentKey] - # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 - menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) + if currentKey == '个人资料': + menu_title = '个人资料' + button_perms = [] + menu_modules = 'system.user.profile' + else: + menu_title = find_title_by_key(menu_info.get('menu_info'), currentKey) + button_perms = [item.get('perms') for item in menu_list.get('menu_list') if str(item.get('parent_id')) == currentKey] + # 判断当前选中的菜单栏项是否存在module,如果有,则动态导入module,否则返回404页面 + menu_modules = find_modules_by_key(menu_info.get('menu_info'), currentKey) if menu_modules: # 否则追加子项返回 @@ -116,7 +121,7 @@ def handle_tab_switch_and_create(currentKey, latestDeletePane, origin_items, act # 页首面包屑和hash回调 @app.callback( [Output('header-breadcrumb', 'items'), - Output('dcc-url', 'pathname')], + Output('dcc-url', 'pathname', allow_duplicate=True)], Input('tabs-container', 'activeKey'), State('menu-info-store-container', 'data'), prevent_initial_call=True diff --git a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py index 143eb2c..233b2b6 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/logininfor_c.py @@ -42,7 +42,8 @@ def get_login_log_table_data(search_click, pagination, operations, ipaddr, user_ page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'login_log-list-table': query_params = dict( ipaddr=ipaddr, user_name=user_name, diff --git a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py index eb26438..3032fe8 100644 --- a/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py +++ b/dash-fastapi-frontend/callbacks/monitor_c/operlog_c.py @@ -44,7 +44,8 @@ def get_operation_log_table_data(search_click, pagination, operations, title, op page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'operation_log-list-table': query_params = dict( title=title, oper_name=oper_name, diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py index 4ab9899..5a09b33 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_c.py @@ -42,7 +42,8 @@ def get_dict_type_table_data(search_click, pagination, operations, dict_name, di page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'dict_type-list-table': query_params = dict( dict_name=dict_name, dict_type=dict_type, diff --git a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py index 6ea7dd8..a1d06c4 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dict_c/dict_data_c.py @@ -34,7 +34,8 @@ def get_dict_data_table_data(search_click, pagination, operations, dict_type, di page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'dict_data-list-table': query_params = dict( dict_type=dict_type, dict_label=dict_label, @@ -81,8 +82,7 @@ def get_dict_data_table_data(search_click, pagination, operations, dict_type, di @app.callback( - [Output('dict_data-dict_type-select', 'value', allow_duplicate=True), - Output('dict_data-dict_label-input', 'value'), + [Output('dict_data-dict_label-input', 'value'), Output('dict_data-status-select', 'value'), Output('dict_data-operations-store', 'data')], Input('dict_data-reset', 'nClicks'), @@ -90,9 +90,9 @@ def get_dict_data_table_data(search_click, pagination, operations, dict_type, di ) def reset_dict_data_query_params(reset_click): if reset_click: - return [None, None, None, {'type': 'reset'}] + return [None, None, {'type': 'reset'}] - return [dash.no_update] * 4 + return [dash.no_update] * 3 @app.callback( diff --git a/dash-fastapi-frontend/callbacks/system_c/post_c.py b/dash-fastapi-frontend/callbacks/system_c/post_c.py index 3a6fdee..73ad53c 100644 --- a/dash-fastapi-frontend/callbacks/system_c/post_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/post_c.py @@ -34,7 +34,8 @@ def get_post_table_data(search_click, pagination, operations, post_code, post_na page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'post-list-table': query_params = dict( post_code=post_code, post_name=post_name, diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index 87ad59e..fde15bd 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -43,7 +43,8 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'role-list-table': query_params = dict( role_name=role_name, role_key=role_key, diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py new file mode 100644 index 0000000..e74c31c --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/avatar_c.py @@ -0,0 +1,154 @@ +import dash +import feffery_utils_components as fuc +import time +import uuid +from dash.dependencies import Input, Output, State +from server import app + +from api.user import change_user_avatar_api + + +@app.callback( + [Output('avatar-cropper-modal', 'visible', allow_duplicate=True), + Output('avatar-src-data', 'data', allow_duplicate=True)], + Input('avatar-edit-click', 'n_clicks'), + State('user-avatar-image-info', 'src'), + prevent_initial_call=True +) +def avatar_cropper_modal_visible(n_clicks, user_avatar_image_info): + if n_clicks: + return [True, user_avatar_image_info] + + return dash.no_update, dash.no_update + + +@app.callback( + Output('avatar-src-data', 'data', allow_duplicate=True), + Input('avatar-upload-choose', 'listUploadTaskRecord'), + prevent_initial_call=True +) +def upload_user_avatar(list_upload_task_record): + if list_upload_task_record: + + return list_upload_task_record[-1].get('url') + + return dash.no_update + + +@app.callback( + Output('avatar-cropper', 'jsString'), + Input('avatar-src-data', 'data'), + prevent_initial_call=True +) +def edit_user_avatar(src_data): + + return """ + // 创建新图像元素 + var newImage = document.createElement('img'); + newImage.id = 'user-avatar-image'; + newImage.src = '% s'; + newImage.onload = function() { + // 删除旧图像元素 + var oldImage = document.getElementById('user-avatar-image'); + oldImage.parentNode.removeChild(oldImage); + // 销毁旧的 Cropper.js 实例 + var oldCropper = oldImage.cropper; + if (oldCropper) { + oldCropper.destroy(); + } + // 将新图像添加到页面中 + var container = document.getElementById('avatar-cropper-container'); + container.appendChild(newImage); + // var image = document.getElementById('user-avatar-image'); + var previewImage = document.getElementById('user-avatar-image-preview'); + // 创建新的 Cropper 实例 + console.log(cropper) + var cropper = new Cropper(newImage, { + viewMode: 1, + dragMode: 'none', + initialAspectRatio: 1, + aspectRatio: 1, + preview: previewImage, + background: true, + autoCropArea: 0.6, + zoomOnWheel: true, + crop: function(event) { + // 当裁剪框的位置或尺寸发生改变时触发的回调函数 + console.log(event.detail.x); + console.log(event.detail.y); + console.log(event.detail.width); + console.log(event.detail.height); + console.log(event.detail.rotate); + console.log(event.detail.scaleX); + console.log(event.detail.scaleY); + // 当需要获取裁剪后的数据时 + var croppedDataUrl = cropper.getCroppedCanvas().toDataURL("image/jpeg", 1); + sessionStorage.setItem('cropper-avatar-base64', JSON.stringify({avatarBase64: croppedDataUrl})) + console.log(croppedDataUrl) + } + }); + // 获取旋转按钮的引用 + var rotateLeftButton = document.getElementById('rotate-left'); + var rotateRightButton = document.getElementById('rotate-right'); + + // 添加点击事件监听器 + rotateLeftButton.addEventListener('click', function() { + // 向左旋转图像90度 + cropper.rotate(-90); + }); + rotateRightButton.addEventListener('click', function() { + // 向右旋转图像90度 + cropper.rotate(90); + }); + // 获取缩小按钮和放大按钮的引用 + var zoomOutButton = document.getElementById('zoom-out'); + var zoomInButton = document.getElementById('zoom-in'); + + // 添加点击事件监听器 + zoomOutButton.addEventListener('click', function() { + // 放大图像 + cropper.zoom(0.1); + }); + + zoomInButton.addEventListener('click', function() { + // 缩小图像 + cropper.zoom(-0.1); + }); + } + """ % src_data + + +@app.callback( + [Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True), + Output('avatar-cropper-modal', 'visible', allow_duplicate=True), + Output('user-avatar-image-info', 'key'), + Output('avatar-info', 'key')], + Input('change-avatar-submit', 'nClicks'), + State('cropper-avatar-base64', 'data'), + prevent_initial_call=True +) +def change_user_avatar_callback(submit_click, avatar_data): + + if submit_click: + params = dict(type='avatar', avatar=avatar_data['avatarBase64']) + change_avatar_result = change_user_avatar_api(params) + if change_avatar_result.get('code') == 200: + + return [ + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success'), + False, + str(uuid.uuid4()), + str(uuid.uuid4()) + ] + + return [ + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + dash.no_update, + dash.no_update, + dash.no_update + ] + + return [dash.no_update] * 5 diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py new file mode 100644 index 0000000..642b52d --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py @@ -0,0 +1,91 @@ +import dash +import feffery_utils_components as fuc +import time +from dash.dependencies import Input, Output, State +from server import app + +from api.user import reset_user_password_api + + +@app.callback( + [Output('reset-old-password-form-item', 'validateStatus'), + Output('reset-new-password-form-item', 'validateStatus'), + Output('reset-confirm-password-form-item', 'validateStatus'), + Output('reset-new-password-form-item', 'help'), + Output('reset-old-password-form-item', 'help'), + Output('reset-confirm-password-form-item', 'help'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('reset-password-submit', 'nClicks'), + [State('reset-old-password', 'value'), + State('reset-new-password', 'value'), + State('reset-confirm-password', 'value')], + prevent_initial_call=True +) +def reset_submit_user_info(reset_click, old_password, new_password, confirm_password): + if reset_click: + if all([old_password, new_password, confirm_password]): + + if new_password == confirm_password: + + params = dict(type='avatar', old_password=old_password, password=new_password) + reset_password_result = reset_user_password_api(params) + if reset_password_result.get('code') == 200: + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success'), + ] + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [ + None, + None if new_password else 'error', + None if confirm_password else 'error', + None, + None if new_password else '前后两次密码不一致!', + None if confirm_password else '前后两次密码不一致!', + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [ + None if old_password else 'error', + None if new_password else 'error', + None if confirm_password else 'error', + None if old_password else '请输入旧密码!', + None if new_password else '请输入新密码!', + None if confirm_password else '请输入确认密码!', + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [dash.no_update] * 8 + + +@app.callback( + Output('tabs-container', 'latestDeletePane', allow_duplicate=True), + Input('reset-password-close', 'nClicks'), + prevent_initial_call=True +) +def close_personal_info_modal(close_click): + if close_click: + + return '个人资料' + return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py new file mode 100644 index 0000000..6cdef31 --- /dev/null +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/user_info_c.py @@ -0,0 +1,79 @@ +import dash +import feffery_utils_components as fuc +import time +from dash.dependencies import Input, Output, State +from server import app + +from api.user import change_user_info_api + + +@app.callback( + [Output('reset-user-nick_name-form-item', 'validateStatus'), + Output('reset-user-phonenumber-form-item', 'validateStatus'), + Output('reset-user-email-form-item', 'validateStatus'), + Output('reset-user-nick_name-form-item', 'help'), + Output('reset-user-phonenumber-form-item', 'help'), + Output('reset-user-email-form-item', 'help'), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('reset-submit', 'nClicks'), + [State('reset-user-nick_name', 'value'), + State('reset-user-phonenumber', 'value'), + State('reset-user-email', 'value'), + State('reset-user-sex', 'value')], + prevent_initial_call=True +) +def reset_submit_user_info(reset_click, nick_name, phonenumber, email, sex): + if reset_click: + if all([nick_name, phonenumber, email]): + + params = dict(type='avatar', nick_name=nick_name, phonenumber=phonenumber, email=email, sex=sex) + change_user_info_result = change_user_info_api(params) + if change_user_info_result.get('code') == 200: + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改成功', type='success'), + ] + + return [ + None, + None, + None, + None, + None, + None, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [ + None if nick_name else 'error', + None if phonenumber else 'error', + None if email else 'error', + None if nick_name else '请输入用户昵称!', + None if phonenumber else '请输入手机号码!', + None if email else '请输入邮箱!', + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('修改失败', type='error'), + ] + + return [dash.no_update] * 8 + + +@app.callback( + Output('tabs-container', 'latestDeletePane', allow_duplicate=True), + Input('reset-close', 'nClicks'), + prevent_initial_call=True +) +def close_personal_info_modal(close_click): + if close_click: + + return '个人资料' + return dash.no_update diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py similarity index 99% rename from dash-fastapi-frontend/callbacks/system_c/user_c.py rename to dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py index ef43a6e..e161a7a 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py @@ -70,7 +70,8 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio page_num=1, page_size=10 ) - if pagination: + triggered_id = dash.ctx.triggered_id + if triggered_id == 'user-list-table': query_params = dict( dept_id=dept_id, user_name=user_name, diff --git a/dash-fastapi-frontend/config/global_config.py b/dash-fastapi-frontend/config/global_config.py index ea4ffd0..b1e5c3c 100644 --- a/dash-fastapi-frontend/config/global_config.py +++ b/dash-fastapi-frontend/config/global_config.py @@ -14,6 +14,9 @@ class RouterConfig: '/', '/login', '/forget' ] + # 静态路由列表 + STATIC_VALID_PATHNAME = ['/', '/user/profile'] + class ApiBaseUrlConfig: diff --git a/dash-fastapi-frontend/views/layout/__init__.py b/dash-fastapi-frontend/views/layout/__init__.py index c2ab0e6..fbb23ce 100644 --- a/dash-fastapi-frontend/views/layout/__init__.py +++ b/dash-fastapi-frontend/views/layout/__init__.py @@ -5,12 +5,11 @@ import feffery_antd_components as fac from views.layout.components.head import render_head_content from views.layout.components.content import render_main_content from views.layout.components.aside import render_aside_content -# import callbacks.index_c import callbacks.layout_c.fold_side_menu import callbacks.layout_c.index_c -def render_content(user_name, nick_name, phone_number, menu_info): +def render_content(menu_info): return fuc.FefferyTopProgress( html.Div( @@ -21,46 +20,18 @@ def render_content(user_name, nick_name, phone_number, menu_info): html.Div(id='idle-placeholder-container'), # 注入相关modal - html.Div( - [ - # 个人资料面板 - fac.AntdModal( - [ - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdText( - user_name, - copyable=True - ), - label='账号' - ), - fac.AntdFormItem( - fac.AntdText( - nick_name, - copyable=True - ), - label='姓名' - ), - fac.AntdFormItem( - fac.AntdText( - phone_number, - copyable=True - ), - label='电话' - ) - ], - labelCol={ - 'span': 4 - } - ) - ], - id='index-personal-info-modal', - title='个人资料', - mask=False - ), - ] - ), + # html.Div( + # [ + # # 个人资料面板 + # fac.AntdModal( + # render_user_profile(), + # id='index-personal-info-modal', + # title='个人资料', + # width=1000, + # mask=False + # ) + # ] + # ), # 退出登录对话框提示 fac.AntdModal( @@ -105,7 +76,7 @@ def render_content(user_name, nick_name, phone_number, menu_info): fac.AntdCol( [ fac.AntdRow( - render_head_content(user_name), + render_head_content(), style={ 'height': '50px', 'boxShadow': 'rgb(240 241 242) 0px 2px 14px', @@ -117,7 +88,7 @@ def render_content(user_name, nick_name, phone_number, menu_info): } ), fac.AntdRow( - render_main_content(user_name, nick_name, phone_number), + render_main_content(), wrap=False ) ], diff --git a/dash-fastapi-frontend/views/layout/components/content.py b/dash-fastapi-frontend/views/layout/components/content.py index f5caf92..7d17211 100644 --- a/dash-fastapi-frontend/views/layout/components/content.py +++ b/dash-fastapi-frontend/views/layout/components/content.py @@ -2,7 +2,7 @@ from dash import html import feffery_antd_components as fac -def render_main_content(user_name, nick_name, phone_number): +def render_main_content(): return [ # 右侧主体内容区域 fac.AntdCol( @@ -26,7 +26,8 @@ def render_main_content(user_name, nick_name, phone_number): # defaultActiveKey='首页', style={ 'width': '100%', - 'paddingLeft': '15px' + 'paddingLeft': '15px', + 'paddingRight': '15px' } ), # id='index-main-content-container', diff --git a/dash-fastapi-frontend/views/layout/components/head.py b/dash-fastapi-frontend/views/layout/components/head.py index 5540e12..5651e87 100644 --- a/dash-fastapi-frontend/views/layout/components/head.py +++ b/dash-fastapi-frontend/views/layout/components/head.py @@ -1,10 +1,10 @@ -from dash import html +from dash import html, dcc import feffery_antd_components as fac - +from flask import session import callbacks.layout_c.head_c -def render_head_content(user_name): +def render_head_content(): return [ # 页首左侧折叠按钮区域 fac.AntdCol( @@ -57,14 +57,12 @@ def render_head_content(user_name): [ fac.AntdTooltip( fac.AntdAvatar( - mode='text', - size=36, - text=user_name, - style={ - 'background': 'gold' - } + id='avatar-info', + mode='image', + src=session.get('user_info').get('avatar'), + size=36 ), - title='当前用户:' + user_name, + title='当前用户:' + session.get('user_info').get('user_name'), placement='bottom' ), diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index e067f9f..b87ae59 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -1,7 +1,7 @@ from dash import dcc, html import feffery_antd_components as fac -import callbacks.system_c.user_c +from . import profile from api.user import get_user_list_api from api.dept import get_dept_tree_api diff --git a/dash-fastapi-frontend/views/system/user/profile/__init__.py b/dash-fastapi-frontend/views/system/user/profile/__init__.py new file mode 100644 index 0000000..d7c0de9 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/__init__.py @@ -0,0 +1,182 @@ +from dash import html +import feffery_utils_components as fuc +import feffery_antd_components as fac +from flask import session +from . import user_avatar, user_info, reset_pwd + + +def render(button_perms): + + return [ + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdCard( + [ + html.Div( + [ + html.Div( + user_avatar.render(), + style={ + 'textAlign': 'center', + 'marginBottom': '10px' + } + ), + html.Ul( + [ + html.Li( + [ + fac.AntdIcon(icon='antd-user'), + fac.AntdText('用户名称'), + html.Div( + session.get('user_info').get('user_name'), + id='profile_c-username', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-mobile'), + fac.AntdText('手机号码'), + html.Div( + session.get('user_info').get('phonenumber'), + id='profile_c-phonenumber', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-mail'), + fac.AntdText('用户邮箱'), + html.Div( + session.get('user_info').get('email'), + id='profile_c-email', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-cluster'), + fac.AntdText('所属部门'), + html.Div( + session.get('dept_info').get('dept_name') + "/" + ','.join( + [item.get('post_name') for item in + session.get('post_info')]), + id='profile_c-dept', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-team'), + fac.AntdText('所属角色'), + html.Div( + ','.join([item.get('role_name') for item in + session.get('role_info')]), + id='profile_c-role', + className='pull-right' + ) + ], + className='list-group-item' + ), + html.Li( + [ + fac.AntdIcon(icon='antd-schedule'), + fac.AntdText('创建日期'), + html.Div( + session.get('user_info').get('create_time'), + id='profile_c-create_time', + className='pull-right' + ) + ], + className='list-group-item' + ), + ], + className='list-group list-group-striped' + ), + fuc.FefferyStyle( + rawStyle= + ''' + .list-group-striped > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; + } + + .list-group { + padding-left: 0px; + list-style: none; + } + + .list-group-item { + border-bottom: 1px solid #e7eaec; + border-top: 1px solid #e7eaec; + margin-bottom: -1px; + padding: 11px 0px; + font-size: 13px; + } + + .pull-right { + float: right !important; + } + ''' + ) + ], + style={ + 'width': '100%' + } + ), + ], + 'size="small"', + title='个人信息', + size='small', + style={ + 'boxShadow': 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px' + } + ), + span=10 + ), + fac.AntdCol( + fac.AntdCard( + [ + fac.AntdTabs( + items=[ + { + 'key': '基本资料', + 'label': '基本资料', + 'children': user_info.render() + }, + { + 'key': '修改密码', + 'label': '修改密码', + 'children': reset_pwd.render() + } + ], + style={ + 'width': '100%' + } + ) + ], + 'size="small"', + title='基本资料', + size='small', + style={ + 'boxShadow': 'rgba(99, 99, 99, 0.2) 0px 2px 8px 0px' + } + ), + span=14 + ), + ], + gutter=10 + ), + ] diff --git a/dash-fastapi-frontend/views/system/user/profile/reset_pwd.py b/dash-fastapi-frontend/views/system/user/profile/reset_pwd.py new file mode 100644 index 0000000..b7b15b2 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/reset_pwd.py @@ -0,0 +1,66 @@ +import feffery_antd_components as fac + +import callbacks.system_c.user_c.profile_c.reset_pwd_c + + +def render(): + return fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-old-password', + mode='password' + ), + id='reset-old-password-form-item', + label='旧密码', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-new-password', + mode='password' + ), + id='reset-new-password-form-item', + label='新密码', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-confirm-password', + mode='password' + ), + id='reset-confirm-password-form-item', + label='确认密码', + required=True + ), + fac.AntdFormItem( + fac.AntdSpace( + [ + fac.AntdButton( + '保存', + id='reset-password-submit', + type='primary' + ), + fac.AntdButton( + '关闭', + id='reset-password-close', + type='primary', + danger=True + ), + ], + ), + wrapperCol={ + 'offset': 4 + } + ) + ], + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + }, + style={ + 'margin': '0 auto' # 以快捷实现居中布局效果 + } + ) diff --git a/dash-fastapi-frontend/views/system/user/profile/user_avatar.py b/dash-fastapi-frontend/views/system/user/profile/user_avatar.py new file mode 100644 index 0000000..f8b2341 --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/user_avatar.py @@ -0,0 +1,177 @@ +from dash import html, dcc +import feffery_utils_components as fuc +import feffery_antd_components as fac +from flask import session + +from config.global_config import ApiBaseUrlConfig +import callbacks.system_c.user_c.profile_c.avatar_c + + +def render(): + return [ + dcc.Store(id='init-cropper'), + dcc.Store(id='avatar-src-data'), + # 监听裁剪的图片数据 + fuc.FefferySessionStorage( + id='cropper-avatar-base64' + ), + html.Div( + [ + fac.AntdImage( + id='user-avatar-image-info', + src=session.get('user_info').get('avatar'), + preview=False, + height='120px', + width='120px', + style={ + 'borderRadius': '50%' + } + ) + ], + id='avatar-edit-click', + className='user-info-head' + ), + fuc.FefferyStyle( + rawStyle=''' + .user-info-head { + position: relative; + display: inline-block; + height: 120px; + } + + .user-info-head:hover:after { + content: '+'; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + color: #eee; + background: rgba(0, 0, 0, 0.5); + font-size: 24px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + cursor: pointer; + line-height: 110px; + border-radius: 50%; + } + ''' + ), + fuc.FefferyExecuteJs(id='avatar-cropper'), + fac.AntdModal( + [ + fac.AntdRow( + [ + fac.AntdCol( + [ + html.Div( + [ + html.Img( + id='user-avatar-image', + height='120px', + width='120px' + ), + ], + id='avatar-cropper-container', + style={ + 'height': '350px', + 'width': '100%' + } + ), + ], + span=12 + ), + fac.AntdCol( + [ + html.Div( + id='user-avatar-image-preview', + className='avatar-upload-preview' + ), + fuc.FefferyStyle( + rawStyle=""" + .avatar-upload-preview { + margin: 18% auto; + width: 220px; + height: 220px; + border-radius: 50%; + box-shadow: 0 0 4px #ccc; + overflow: hidden; + } + """ + ) + ], + span=12 + ) + ] + ), + html.Br(), + fac.AntdRow( + [ + fac.AntdCol( + fac.AntdUpload( + id='avatar-upload-choose', + apiUrl=f'{ApiBaseUrlConfig.BaseUrl}/common/upload', + downloadUrl=f'{ApiBaseUrlConfig.BaseUrl}/common/caches', + headers={'token': 'Bearer' + session.get('token')}, + fileMaxSize=1, + showUploadList=False, + fileTypes=['jpeg', 'jpg', 'png'], + buttonContent='选择' + ), + span=4 + ), + fac.AntdCol( + fac.AntdButton( + id='zoom-out', + icon=fac.AntdIcon( + icon='antd-plus' + ) + ), + span=2 + ), + fac.AntdCol( + fac.AntdButton( + id='zoom-in', + icon=fac.AntdIcon( + icon='antd-minus' + ) + ), + span=2 + ), + fac.AntdCol( + fac.AntdButton( + icon=fac.AntdIcon( + id='rotate-left', + icon='antd-undo' + ) + ), + span=2 + ), + fac.AntdCol( + fac.AntdButton( + icon=fac.AntdIcon( + id='rotate-right', + icon='antd-redo' + ) + ), + span=7 + ), + fac.AntdCol( + fac.AntdButton( + '提交', + id='change-avatar-submit', + type='primary' + ), + span=7 + ), + ], + gutter=10 + ) + ], + id='avatar-cropper-modal', + title='修改头像', + width=850, + mask=False + ) + ] diff --git a/dash-fastapi-frontend/views/system/user/profile/user_info.py b/dash-fastapi-frontend/views/system/user/profile/user_info.py new file mode 100644 index 0000000..75ca5ab --- /dev/null +++ b/dash-fastapi-frontend/views/system/user/profile/user_info.py @@ -0,0 +1,84 @@ +import feffery_antd_components as fac + +import callbacks.system_c.user_c.profile_c.user_info_c + + +def render(): + return fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-user-nick_name', + placeholder='请输入用户昵称' + ), + id='reset-user-nick_name-form-item', + label='用户昵称', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-user-phonenumber', + placeholder='请输入手机号码' + ), + id='reset-user-phonenumber-form-item', + label='手机号码', + required=True + ), + fac.AntdFormItem( + fac.AntdInput( + id='reset-user-email', + placeholder='请输入邮箱' + ), + id='reset-user-email-form-item', + label='邮箱', + required=True + ), + fac.AntdFormItem( + fac.AntdRadioGroup( + id='reset-user-sex', + options=[ + { + 'label': '男', + 'value': '0' + }, + { + 'label': '女', + 'value': '1' + } + ], + defaultValue='1' + ), + id='reset-user-sex-form-item', + label='性别' + ), + fac.AntdFormItem( + fac.AntdSpace( + [ + fac.AntdButton( + '保存', + id='reset-submit', + type='primary' + ), + fac.AntdButton( + '关闭', + id='reset-close', + type='primary', + danger=True + ), + ], + ), + wrapperCol={ + 'offset': 4 + } + ) + ], + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + }, + style={ + 'margin': '0 auto' # 以快捷实现居中布局效果 + } + ) -- Gitee From 26e0d2448d34994afa00cd232949fcbb4e0a009e Mon Sep 17 00:00:00 2001 From: xlf Date: Fri, 21 Jul 2023 14:16:44 +0800 Subject: [PATCH 028/169] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E8=B5=84=E6=96=99tab=E5=88=87=E6=8D=A2=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E4=B8=8D=E5=90=8C=E6=AD=A5=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/layout_c/index_c.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dash-fastapi-frontend/callbacks/layout_c/index_c.py b/dash-fastapi-frontend/callbacks/layout_c/index_c.py index 801f310..a70b5b8 100644 --- a/dash-fastapi-frontend/callbacks/layout_c/index_c.py +++ b/dash-fastapi-frontend/callbacks/layout_c/index_c.py @@ -141,6 +141,21 @@ def get_current_breadcrumbs(active_key, menu_info): '/' ] + elif active_key == '个人资料': + return [ + [ + { + 'title': '首页', + 'icon': 'antd-dashboard', + 'href': '/' + }, + { + 'title': '个人资料', + } + ], + '/user/profile' + ] + else: result = find_parents(menu_info.get('menu_info'), active_key) # 去除result的重复项 -- Gitee From b0280e84b7cb9478fc67428020780a05d6c4efb0 Mon Sep 17 00:00:00 2001 From: xlf Date: Fri, 21 Jul 2023 15:09:13 +0800 Subject: [PATCH 029/169] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E5=9B=9E=E8=B0=83=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E7=9A=84bug=E5=8F=8A=E5=85=B6=E4=BB=96bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callbacks/system_c/user_c/profile_c/reset_pwd_c.py | 2 +- dash-fastapi-frontend/views/system/user/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py index 642b52d..a5457d8 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/profile_c/reset_pwd_c.py @@ -11,8 +11,8 @@ from api.user import reset_user_password_api [Output('reset-old-password-form-item', 'validateStatus'), Output('reset-new-password-form-item', 'validateStatus'), Output('reset-confirm-password-form-item', 'validateStatus'), - Output('reset-new-password-form-item', 'help'), Output('reset-old-password-form-item', 'help'), + Output('reset-new-password-form-item', 'help'), Output('reset-confirm-password-form-item', 'help'), Output('api-check-token', 'data', allow_duplicate=True), Output('global-message-container', 'children', allow_duplicate=True)], diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index b87ae59..0911d78 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -5,6 +5,8 @@ from . import profile from api.user import get_user_list_api from api.dept import get_dept_tree_api +import callbacks.system_c.user_c.user_c + def render(button_perms): dept_params = dict(dept_name='') -- Gitee From dfe198344492c219fcc2902b0b7d8aa70042ff60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E5=85=89=E9=93=AD?= Date: Tue, 25 Jul 2023 14:34:01 +0800 Subject: [PATCH 030/169] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E6=9F=A5=E8=AF=A2=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../controller/user_controller.py | 25 +++- .../module_admin/dao/user_dao.py | 140 ++++++++++++------ .../module_admin/entity/vo/user_vo.py | 4 +- .../module_admin/service/user_service.py | 18 ++- dash-fastapi-backend/utils/page_util.py | 45 ++++++ 6 files changed, 183 insertions(+), 51 deletions(-) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 6fd8a88..e705c7a 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -6,6 +6,7 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware from module_admin.controller.login_controller import loginController +from module_admin.controller.captcha_controller import captchaController from module_admin.controller.user_controller import userController from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController @@ -74,6 +75,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): app.include_router(loginController, prefix="/login", tags=['login']) +app.include_router(captchaController, prefix="/captcha", tags=['captcha']) app.include_router(userController, prefix="/system", tags=['system/user']) app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index f58be54..dd6cb63 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -7,6 +7,7 @@ from module_admin.service.login_service import get_current_user, get_password_ha from module_admin.service.user_service import * from module_admin.entity.vo.user_vo import * from module_admin.dao.user_dao import * +from utils.page_util import get_page_obj from utils.response_util import * from utils.log_util import * from module_admin.aspect.interface_auth import CheckUserInterfaceAuth @@ -17,11 +18,16 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) -async def get_system_user_list(request: Request, user_query: UserPageObject, query_db: Session = Depends(get_db)): +async def get_system_user_list(request: Request, user_page_query: UserPageObject, query_db: Session = Depends(get_db)): try: + # 拆分user_query = 分页类 + UserModel + user_query = UserModel(**user_page_query.dict()) + # 获取全量数据 user_query_result = get_user_list_services(query_db, user_query) + # 分页操作 + user_page_query_result = get_page_obj(user_query_result, user_page_query.page_num, user_page_query.page_size) logger.info('获取成功') - return response_200(data=user_query_result, message="获取成功") + return response_200(data=user_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -84,7 +90,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok return response_500(data="", message="接口异常") -@userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@userController.post("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def query_detail_system_user(request: Request, user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) @@ -95,6 +101,19 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses return response_500(data="", message="接口异常") +# @userController.post("/user/export", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:expot'))]) +# @log_decorator(title='用户管理', business_type=5) +# async def export_detail_system_user(request: Request, export_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + # try: + # delete_user_result = detail_user_services(query_db, user_id) + # logger.info(f'获取user_id为{user_id}的信息成功') + # return response_200(data=delete_user_result, message='获取成功') + # except Exception as e: + # logger.exception(e) + # return response_500(data="", message="接口异常") + + + @userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index 158da3b..f34330b 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -6,10 +6,11 @@ from module_admin.entity.do.dept_do import SysDept from module_admin.entity.do.post_do import SysPost from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.user_vo import UserModel, UserRoleModel, UserPostModel, CurrentUserInfo, UserPageObject, \ - UserPageObjectResponse, CrudUserResponse + UserPageObjectResponse, CrudUserResponse, UserInfoJoinDept from utils.time_format_util import list_format_datetime, format_datetime_dict_list from utils.page_util import get_page_info from datetime import datetime, time +from typing import Union, List def get_user_by_name(db: Session, user_name: str): @@ -110,51 +111,112 @@ def get_user_detail_by_id(db: Session, user_id: int): return CurrentUserInfo(**results) -def get_user_list(db: Session, page_object: UserPageObject): +# def get_user_list(db: Session, page_object: UserPageObject): +# """ +# 根据查询参数获取用户列表信息 +# :param db: orm对象 +# :param page_object: 分页查询参数对象 +# :return: 用户列表信息对象 +# """ +# count = db.query(SysUser, SysDept) \ +# .filter(SysUser.del_flag == 0, +# SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, +# SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, +# SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, +# SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, +# SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, +# SysUser.status == page_object.status if page_object.status else True, +# SysUser.sex == page_object.sex if page_object.sex else True, +# SysUser.create_time.between( +# datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), +# datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) +# if page_object.create_time_start and page_object.create_time_end else True +# ) \ +# .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ +# .distinct().count() +# offset_com = (page_object.page_num - 1) * page_object.page_size +# page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) +# user_list = db.query(SysUser, SysDept) \ +# .filter(SysUser.del_flag == 0, +# SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, +# SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, +# SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, +# SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, +# SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, +# SysUser.status == page_object.status if page_object.status else True, +# SysUser.sex == page_object.sex if page_object.sex else True, +# SysUser.create_time.between( +# datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), +# datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) +# if page_object.create_time_start and page_object.create_time_end else True +# ) \ +# .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ +# .offset(page_info.offset) \ +# .limit(page_object.page_size) \ +# .distinct().all() +# +# result_list = [] +# if user_list: +# for item in user_list: +# obj = dict( +# user_id=item[0].user_id, +# dept_id=item[0].dept_id, +# dept_name=item[1].dept_name if item[1] else '', +# user_name=item[0].user_name, +# nick_name=item[0].nick_name, +# user_type=item[0].user_type, +# email=item[0].email, +# phonenumber=item[0].phonenumber, +# sex=item[0].sex, +# avatar=item[0].avatar, +# status=item[0].status, +# del_flag=item[0].del_flag, +# login_ip=item[0].login_ip, +# login_date=item[0].login_date, +# create_by=item[0].create_by, +# create_time=item[0].create_time, +# update_by=item[0].update_by, +# update_time=item[0].update_time, +# remark=item[0].remark +# ) +# result_list.append(obj) +# +# result = dict( +# rows=format_datetime_dict_list(result_list), +# page_num=page_info.page_num, +# page_size=page_info.page_size, +# total=page_info.total, +# has_next=page_info.has_next +# ) +# +# return UserPageObjectResponse(**result) + + +def get_user_list(db: Session, user_object: UserModel): """ 根据查询参数获取用户列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param user_object: 分页查询参数对象 :return: 用户列表信息对象 """ - count = db.query(SysUser, SysDept) \ - .filter(SysUser.del_flag == 0, - SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, - SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, - SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, - SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, - SysUser.status == page_object.status if page_object.status else True, - SysUser.sex == page_object.sex if page_object.sex else True, - SysUser.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True - ) \ - .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) user_list = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, - SysUser.dept_id == page_object.dept_id if page_object.dept_id else True, - SysUser.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysUser.nick_name.like(f'%{page_object.nick_name}%') if page_object.nick_name else True, - SysUser.email.like(f'%{page_object.email}%') if page_object.email else True, - SysUser.phonenumber.like(f'%{page_object.phonenumber}%') if page_object.phonenumber else True, - SysUser.status == page_object.status if page_object.status else True, - SysUser.sex == page_object.sex if page_object.sex else True, + SysUser.dept_id == user_object.dept_id if user_object.dept_id else True, + SysUser.user_name.like(f'%{user_object.user_name}%') if user_object.user_name else True, + SysUser.nick_name.like(f'%{user_object.nick_name}%') if user_object.nick_name else True, + SysUser.email.like(f'%{user_object.email}%') if user_object.email else True, + SysUser.phonenumber.like(f'%{user_object.phonenumber}%') if user_object.phonenumber else True, + SysUser.status == user_object.status if user_object.status else True, + SysUser.sex == user_object.sex if user_object.sex else True, SysUser.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True + datetime.combine(datetime.strptime(user_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(user_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if user_object.create_time_start and user_object.create_time_end else True ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result_list = [] + result_list: List[Union[UserInfoJoinDept, None]] = [] if user_list: for item in user_list: obj = dict( @@ -180,15 +242,7 @@ def get_user_list(db: Session, page_object: UserPageObject): ) result_list.append(obj) - result = dict( - rows=format_datetime_dict_list(result_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return UserPageObjectResponse(**result) + return result_list def add_user_dao(db: Session, user: UserModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py index 94cfe66..c492d8a 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -32,6 +32,8 @@ class UserModel(BaseModel): update_by: Optional[str] update_time: Optional[str] remark: Optional[str] + create_time_start: Optional[str] + create_time_end: Optional[str] class Config: orm_mode = True @@ -156,8 +158,6 @@ class UserPageObject(UserModel): """ 用户管理分页查询模型 """ - create_time_start: Optional[str] - create_time_end: Optional[str] page_num: int page_size: int diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index 4c68e2d..b070411 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -3,14 +3,26 @@ from module_admin.dao.user_dao import * from module_admin.service.login_service import verify_password -def get_user_list_services(result_db: Session, page_object: UserPageObject): +# def get_user_list_services(result_db: Session, page_object: UserPageObject): +# """ +# 获取用户列表信息service +# :param result_db: orm对象 +# :param page_object: 分页查询参数对象 +# :return: 用户列表信息对象 +# """ +# user_list_result = get_user_list(result_db, page_object) +# +# return user_list_result + + +def get_user_list_services(result_db: Session, user_object: UserModel): """ 获取用户列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param user_object: 分页查询参数对象 :return: 用户列表信息对象 """ - user_list_result = get_user_list(result_db, page_object) + user_list_result = get_user_list(result_db, user_object) return user_list_result diff --git a/dash-fastapi-backend/utils/page_util.py b/dash-fastapi-backend/utils/page_util.py index 21ef67e..6b63ff8 100644 --- a/dash-fastapi-backend/utils/page_util.py +++ b/dash-fastapi-backend/utils/page_util.py @@ -1,5 +1,10 @@ +import math +from typing import List + from pydantic import BaseModel +from utils.time_format_util import format_datetime_dict_list + class PageModel(BaseModel): """ @@ -12,6 +17,17 @@ class PageModel(BaseModel): has_next: bool +class PageObjectResponse(BaseModel): + """ + 用户管理列表分页查询返回模型 + """ + rows: List = [] + page_num: int + page_size: int + total: int + has_next: bool + + def get_page_info(offset: int, page_num: int, page_size: int, count: int): """ 根据分页参数获取分页信息 @@ -39,3 +55,32 @@ def get_page_info(offset: int, page_num: int, page_size: int, count: int): result = dict(offset=res_offset, page_num=res_page_num, page_size=page_size, total=count, has_next=has_next) return PageModel(**result) + + +def get_page_obj(data_list: List, page_num: int, page_size: int): + """ + 输入数据列表data_list和分页信息,返回分页数据列表结果 + :param data_list: 原始数据列表 + :param page_num: 当前页码 + :param page_size: 当前页面数据量 + :return: 分页数据对象 + """ + # 计算起始索引和结束索引 + start = (page_num - 1) * page_size + end = page_num * page_size + + # 根据计算得到的起始索引和结束索引对数据列表进行切片 + paginated_data = data_list[start:end] + has_next = True if math.ceil(len(data_list) / page_size) > page_num else False; + + result = dict( + rows=format_datetime_dict_list(paginated_data), + page_num=page_num, + page_size=page_size, + total=len(data_list), + has_next=has_next + ) + + return PageObjectResponse(**result) + + -- Gitee From 176dbe713181e4b80dd847cc02b4a683136991a9 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Tue, 8 Aug 2023 16:56:18 +0800 Subject: [PATCH 031/169] =?UTF-8?q?refactor:=E9=87=8D=E6=9E=84=E6=89=80?= =?UTF-8?q?=E6=9C=89=E6=A8=A1=E5=9D=97=E7=9A=84=E5=90=8E=E7=AB=AF=E5=88=86?= =?UTF-8?q?=E9=A1=B5=20feat:=E6=96=B0=E5=A2=9E=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=87=8D=E7=BD=AE=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20fix:=E4=BF=AE=E5=A4=8D=E8=A7=92=E8=89=B2=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=B6=8A=E6=9D=83=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 4 +- .../controller/dict_controller.py | 21 +++-- .../module_admin/controller/log_controller.py | 19 ++-- .../controller/menu_controller.py | 15 +-- .../module_admin/controller/post_controler.py | 10 +- .../controller/role_controller.py | 9 +- .../controller/user_controller.py | 7 +- .../module_admin/dao/dict_dao.py | 71 +++----------- .../module_admin/dao/log_dao.py | 84 ++++------------- .../module_admin/dao/menu_dao.py | 94 ++++++++++++++----- .../module_admin/dao/post_dao.py | 32 ++----- .../module_admin/dao/role_dao.py | 45 ++------- .../module_admin/dao/user_dao.py | 50 +++++----- .../module_admin/entity/vo/user_vo.py | 24 +---- .../module_admin/service/dict_service.py | 26 +++-- .../module_admin/service/log_service.py | 12 +-- .../module_admin/service/menu_service.py | 16 ++-- .../module_admin/service/post_service.py | 6 +- .../module_admin/service/role_service.py | 6 +- .../module_admin/service/user_service.py | 16 ++-- dash-fastapi-backend/utils/page_util.py | 6 +- dash-fastapi-frontend/app.py | 16 ++-- .../callbacks/system_c/dept_c.py | 68 ++++++++++---- .../callbacks/system_c/role_c.py | 27 +++--- .../callbacks/system_c/user_c/user_c.py | 89 +++++++++++++++--- .../views/system/dept/__init__.py | 57 +++++++---- .../views/system/role/__init__.py | 27 +++--- .../views/system/user/__init__.py | 57 ++++++----- 28 files changed, 488 insertions(+), 426 deletions(-) diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index e705c7a..3cde6db 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -6,7 +6,7 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware from module_admin.controller.login_controller import loginController -from module_admin.controller.captcha_controller import captchaController +# from module_admin.controller.captcha_controller import captchaController from module_admin.controller.user_controller import userController from module_admin.controller.menu_controller import menuController from module_admin.controller.dept_controller import deptController @@ -75,7 +75,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): app.include_router(loginController, prefix="/login", tags=['login']) -app.include_router(captchaController, prefix="/captcha", tags=['captcha']) +# app.include_router(captchaController, prefix="/captcha", tags=['captcha']) app.include_router(userController, prefix="/system", tags=['system/user']) app.include_router(menuController, prefix="/system", tags=['system/menu']) app.include_router(deptController, prefix="/system", tags=['system/dept']) diff --git a/dash-fastapi-backend/module_admin/controller/dict_controller.py b/dash-fastapi-backend/module_admin/controller/dict_controller.py index 4634e06..4c92e70 100644 --- a/dash-fastapi-backend/module_admin/controller/dict_controller.py +++ b/dash-fastapi-backend/module_admin/controller/dict_controller.py @@ -6,6 +6,7 @@ from module_admin.service.dict_service import * from module_admin.entity.vo.dict_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -14,11 +15,14 @@ dictController = APIRouter(dependencies=[Depends(get_current_user)]) @dictController.post("/dictType/get", response_model=DictTypePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) -async def get_system_dict_type_list(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): +async def get_system_dict_type_list(request: Request, dict_type_page_query: DictTypePageObject, query_db: Session = Depends(get_db)): try: - dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + # 获取全量数据 + dict_type_query_result = get_dict_type_list_services(query_db, dict_type_page_query) + # 分页操作 + dict_type_page_query_result = get_page_obj(dict_type_query_result, dict_type_page_query.page_num, dict_type_page_query.page_size) logger.info('获取成功') - return response_200(data=dict_type_query_result, message="获取成功") + return response_200(data=dict_type_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -27,7 +31,7 @@ async def get_system_dict_type_list(request: Request, dict_type_query: DictTypeP @dictController.post("/dictType/all", dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) async def get_system_all_dict_type(request: Request, dict_type_query: DictTypePageObject, query_db: Session = Depends(get_db)): try: - dict_type_query_result = get_dict_type_list_services(query_db, dict_type_query) + dict_type_query_result = get_all_dict_type_services(query_db) logger.info('获取成功') return response_200(data=dict_type_query_result, message="获取成功") except Exception as e: @@ -100,11 +104,14 @@ async def query_detail_system_dict_type(request: Request, dict_id: int, query_db @dictController.post("/dictData/get", response_model=DictDataPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:dict:list'))]) -async def get_system_dict_data_list(request: Request, dict_data_query: DictDataPageObject, query_db: Session = Depends(get_db)): +async def get_system_dict_data_list(request: Request, dict_data_page_query: DictDataPageObject, query_db: Session = Depends(get_db)): try: - dict_data_query_result = get_dict_data_list(query_db, dict_data_query) + # 获取全量数据 + dict_data_query_result = get_dict_data_list_services(query_db, dict_data_page_query) + # 分页操作 + dict_data_page_query_result = get_page_obj(dict_data_query_result, dict_data_page_query.page_num, dict_data_page_query.page_size) logger.info('获取成功') - return response_200(data=dict_data_query_result, message="获取成功") + return response_200(data=dict_data_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/log_controller.py b/dash-fastapi-backend/module_admin/controller/log_controller.py index c5ef771..4813fdc 100644 --- a/dash-fastapi-backend/module_admin/controller/log_controller.py +++ b/dash-fastapi-backend/module_admin/controller/log_controller.py @@ -6,6 +6,7 @@ from module_admin.service.log_service import * from module_admin.entity.vo.log_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -14,11 +15,14 @@ logController = APIRouter(prefix='/log', dependencies=[Depends(get_current_user) @logController.post("/operation/get", response_model=OperLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:operlog:list'))]) -async def get_system_operation_log_list(request: Request, operation_log_query: OperLogPageObject, query_db: Session = Depends(get_db)): +async def get_system_operation_log_list(request: Request, operation_log_page_query: OperLogPageObject, query_db: Session = Depends(get_db)): try: - operation_log_query_result = get_operation_log_list_services(query_db, operation_log_query) + # 获取全量数据 + operation_log_query_result = get_operation_log_list_services(query_db, operation_log_page_query) + # 分页操作 + operation_log_page_query_result = get_page_obj(operation_log_query_result, operation_log_page_query.page_num, operation_log_page_query.page_size) logger.info('获取成功') - return response_200(data=operation_log_query_result, message="获取成功") + return response_200(data=operation_log_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") @@ -68,11 +72,14 @@ async def query_detail_system_operation_log(request: Request, oper_id: int, quer @logController.post("/login/get", response_model=LoginLogPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('monitor:logininfor:list'))]) -async def get_system_login_log_list(request: Request, login_log_query: LoginLogPageObject, query_db: Session = Depends(get_db)): +async def get_system_login_log_list(request: Request, login_log_page_query: LoginLogPageObject, query_db: Session = Depends(get_db)): try: - login_log_query_result = get_login_log_list_services(query_db, login_log_query) + # 获取全量数据 + login_log_query_result = get_login_log_list_services(query_db, login_log_page_query) + # 分页操作 + login_log_page_query_result = get_page_obj(login_log_query_result, login_log_page_query.page_num, login_log_page_query.page_size) logger.info('获取成功') - return response_200(data=login_log_query_result, message="获取成功") + return response_200(data=login_log_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/menu_controller.py b/dash-fastapi-backend/module_admin/controller/menu_controller.py index 8703183..66adb6c 100644 --- a/dash-fastapi-backend/module_admin/controller/menu_controller.py +++ b/dash-fastapi-backend/module_admin/controller/menu_controller.py @@ -15,9 +15,10 @@ menuController = APIRouter(dependencies=[Depends(get_current_user)]) @menuController.post("/menu/tree", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: - menu_query_result = get_menu_tree_services(query_db, menu_query) + current_user = await get_current_user(request, token, query_db) + menu_query_result = get_menu_tree_services(query_db, menu_query, current_user) logger.info('获取成功') return response_200(data=menu_query_result, message="获取成功") except Exception as e: @@ -26,9 +27,10 @@ async def get_system_menu_tree(request: Request, menu_query: MenuTreeModel, quer @menuController.post("/menu/forEditOption", response_model=MenuTree, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) -async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_tree_for_edit_option(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: - menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query) + current_user = await get_current_user(request, token, query_db) + menu_query_result = get_menu_tree_for_edit_option_services(query_db, menu_query, current_user) logger.info('获取成功') return response_200(data=menu_query_result, message="获取成功") except Exception as e: @@ -37,9 +39,10 @@ async def get_system_menu_tree_for_edit_option(request: Request, menu_query: Men @menuController.post("/menu/get", response_model=MenuResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:menu:list'))]) -async def get_system_menu_list(request: Request, menu_query: MenuModel, query_db: Session = Depends(get_db)): +async def get_system_menu_list(request: Request, menu_query: MenuModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: - menu_query_result = get_menu_list_services(query_db, menu_query) + current_user = await get_current_user(request, token, query_db) + menu_query_result = get_menu_list_services(query_db, menu_query, current_user) logger.info('获取成功') return response_200(data=menu_query_result, message="获取成功") except Exception as e: diff --git a/dash-fastapi-backend/module_admin/controller/post_controler.py b/dash-fastapi-backend/module_admin/controller/post_controler.py index 740f9e8..2ed47d7 100644 --- a/dash-fastapi-backend/module_admin/controller/post_controler.py +++ b/dash-fastapi-backend/module_admin/controller/post_controler.py @@ -6,6 +6,7 @@ from module_admin.service.post_service import * from module_admin.entity.vo.post_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -25,11 +26,14 @@ async def get_system_post_select(request: Request, query_db: Session = Depends(g @postController.post("/post/get", response_model=PostPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:post:list'))]) -async def get_system_post_list(request: Request, post_query: PostPageObject, query_db: Session = Depends(get_db)): +async def get_system_post_list(request: Request, post_page_query: PostPageObject, query_db: Session = Depends(get_db)): try: - post_query_result = get_post_list_services(query_db, post_query) + # 获取全量数据 + post_query_result = get_post_list_services(query_db, post_page_query) + # 分页操作 + post_page_query_result = get_page_obj(post_query_result, post_page_query.page_num, post_page_query.page_size) logger.info('获取成功') - return response_200(data=post_query_result, message="获取成功") + return response_200(data=post_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/role_controller.py b/dash-fastapi-backend/module_admin/controller/role_controller.py index 562fff2..3af026b 100644 --- a/dash-fastapi-backend/module_admin/controller/role_controller.py +++ b/dash-fastapi-backend/module_admin/controller/role_controller.py @@ -6,6 +6,7 @@ from module_admin.service.role_service import * from module_admin.entity.vo.role_vo import * from utils.response_util import * from utils.log_util import * +from utils.page_util import get_page_obj from module_admin.aspect.interface_auth import CheckUserInterfaceAuth from module_admin.annotation.log_annotation import log_decorator @@ -25,11 +26,13 @@ async def get_system_role_select(request: Request, query_db: Session = Depends(g @roleController.post("/role/get", response_model=RolePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:role:list'))]) -async def get_system_role_list(request: Request, role_query: RolePageObject, query_db: Session = Depends(get_db)): +async def get_system_role_list(request: Request, role_page_query: RolePageObject, query_db: Session = Depends(get_db)): try: - role_query_result = get_role_list_services(query_db, role_query) + role_query_result = get_role_list_services(query_db, role_page_query) + # 分页操作 + role_page_query_result = get_page_obj(role_query_result, role_page_query.page_num, role_page_query.page_size) logger.info('获取成功') - return response_200(data=role_query_result, message="获取成功") + return response_200(data=role_page_query_result, message="获取成功") except Exception as e: logger.exception(e) return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index dd6cb63..70820fb 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -20,10 +20,8 @@ userController = APIRouter(dependencies=[Depends(get_current_user)]) @userController.post("/user/get", response_model=UserPageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:user:list'))]) async def get_system_user_list(request: Request, user_page_query: UserPageObject, query_db: Session = Depends(get_db)): try: - # 拆分user_query = 分页类 + UserModel - user_query = UserModel(**user_page_query.dict()) # 获取全量数据 - user_query_result = get_user_list_services(query_db, user_query) + user_query_result = get_user_list_services(query_db, user_page_query) # 分页操作 user_page_query_result = get_page_obj(user_query_result, user_page_query.page_num, user_page_query.page_size) logger.info('获取成功') @@ -90,7 +88,7 @@ async def delete_system_user(request: Request, delete_user: DeleteUserModel, tok return response_500(data="", message="接口异常") -@userController.post("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) +@userController.get("/user/{user_id}", response_model=UserDetailModel, dependencies=[Depends(CheckUserInterfaceAuth('system:user:edit'))]) async def query_detail_system_user(request: Request, user_id: int, query_db: Session = Depends(get_db)): try: delete_user_result = detail_user_services(query_db, user_id) @@ -113,7 +111,6 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses # return response_500(data="", message="接口异常") - @userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: diff --git a/dash-fastapi-backend/module_admin/dao/dict_dao.py b/dash-fastapi-backend/module_admin/dao/dict_dao.py index c15f36d..114afb7 100644 --- a/dash-fastapi-backend/module_admin/dao/dict_dao.py +++ b/dash-fastapi-backend/module_admin/dao/dict_dao.py @@ -21,47 +21,25 @@ def get_all_dict_type(db: Session): return list_format_datetime(dict_type_info) -def get_dict_type_list(db: Session, page_object: DictTypePageObject): +def get_dict_type_list(db: Session, query_object: DictTypePageObject): """ 根据查询参数获取字典类型列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典类型列表信息对象 """ - count = db.query(SysDictType) \ - .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, - SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, - SysDictType.status == page_object.status if page_object.status else True, - SysDictType.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True - )\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) dict_type_list = db.query(SysDictType) \ - .filter(SysDictType.dict_name.like(f'%{page_object.dict_name}%') if page_object.dict_name else True, - SysDictType.dict_type.like(f'%{page_object.dict_type}%') if page_object.dict_type else True, - SysDictType.status == page_object.status if page_object.status else True, + .filter(SysDictType.dict_name.like(f'%{query_object.dict_name}%') if query_object.dict_name else True, + SysDictType.dict_type.like(f'%{query_object.dict_type}%') if query_object.dict_type else True, + SysDictType.status == query_object.status if query_object.status else True, SysDictType.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True )\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(dict_type_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return DictTypePageObjectResponse(**result) + return list_format_datetime(dict_type_list) def add_dict_type_dao(db: Session, dict_type: DictTypeModel): @@ -121,41 +99,22 @@ def get_dict_data_detail_by_id(db: Session, dict_code: int): return dict_data_info -def get_dict_data_list(db: Session, page_object: DictDataPageObject): +def get_dict_data_list(db: Session, query_object: DictDataPageObject): """ 根据查询参数获取字典数据列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典数据列表信息对象 """ - count = db.query(SysDictData) \ - .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, - SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, - SysDictData.status == page_object.status if page_object.status else True - )\ - .order_by(SysDictData.dict_sort)\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) dict_data_list = db.query(SysDictData) \ - .filter(SysDictData.dict_type == page_object.dict_type if page_object.dict_type else True, - SysDictData.dict_label.like(f'%{page_object.dict_label}%') if page_object.dict_label else True, - SysDictData.status == page_object.status if page_object.status else True + .filter(SysDictData.dict_type == query_object.dict_type if query_object.dict_type else True, + SysDictData.dict_label.like(f'%{query_object.dict_label}%') if query_object.dict_label else True, + SysDictData.status == query_object.status if query_object.status else True )\ .order_by(SysDictData.dict_sort)\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(dict_data_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return DictDataPageObjectResponse(**result) + return list_format_datetime(dict_data_list) def add_dict_data_dao(db: Session, dict_data: DictDataModel): diff --git a/dash-fastapi-backend/module_admin/dao/log_dao.py b/dash-fastapi-backend/module_admin/dao/log_dao.py index 8447a15..780dc6c 100644 --- a/dash-fastapi-backend/module_admin/dao/log_dao.py +++ b/dash-fastapi-backend/module_admin/dao/log_dao.py @@ -3,7 +3,6 @@ from module_admin.entity.do.log_do import SysOperLog, SysLogininfor from module_admin.entity.vo.log_vo import OperLogModel, LogininforModel, OperLogPageObject, OperLogPageObjectResponse, \ LoginLogPageObject, LoginLogPageObjectResponse, CrudLogResponse from utils.time_format_util import object_format_datetime, list_format_datetime -from utils.page_util import get_page_info from datetime import datetime, time @@ -15,49 +14,26 @@ def get_operation_log_detail_by_id(db: Session, oper_id: int): return object_format_datetime(operation_log_info) -def get_operation_log_list(db: Session, page_object: OperLogPageObject): +def get_operation_log_list(db: Session, query_object: OperLogPageObject): """ 根据查询参数获取操作日志列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 操作日志列表信息对象 """ - count = db.query(SysOperLog) \ - .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, - SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, - SysOperLog.business_type == page_object.business_type if page_object.business_type else True, - SysOperLog.status == page_object.status if page_object.status else True, - SysOperLog.oper_time.between( - datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.oper_time_start and page_object.oper_time_end else True - )\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) operation_log_list = db.query(SysOperLog) \ - .filter(SysOperLog.title.like(f'%{page_object.title}%') if page_object.title else True, - SysOperLog.oper_name.like(f'%{page_object.oper_name}%') if page_object.oper_name else True, - SysOperLog.business_type == page_object.business_type if page_object.business_type else True, - SysOperLog.status == page_object.status if page_object.status else True, + .filter(SysOperLog.title.like(f'%{query_object.title}%') if query_object.title else True, + SysOperLog.oper_name.like(f'%{query_object.oper_name}%') if query_object.oper_name else True, + SysOperLog.business_type == query_object.business_type if query_object.business_type else True, + SysOperLog.status == query_object.status if query_object.status else True, SysOperLog.oper_time.between( - datetime.combine(datetime.strptime(page_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.oper_time_start and page_object.oper_time_end else True + datetime.combine(datetime.strptime(query_object.oper_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.oper_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.oper_time_start and query_object.oper_time_end else True )\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(operation_log_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return OperLogPageObjectResponse(**result) + return list_format_datetime(operation_log_list) def add_operation_log_dao(db: Session, operation_log: OperLogModel): @@ -100,47 +76,25 @@ def clear_operation_log_dao(db: Session): db.commit() # 提交保存到数据库中 -def get_login_log_list(db: Session, page_object: LoginLogPageObject): +def get_login_log_list(db: Session, query_object: LoginLogPageObject): """ 根据查询参数获取登录日志列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 登录日志列表信息对象 """ - count = db.query(SysLogininfor) \ - .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, - SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysLogininfor.status == page_object.status if page_object.status else True, - SysLogininfor.login_time.between( - datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.login_time_start and page_object.login_time_end else True - )\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) login_log_list = db.query(SysLogininfor) \ - .filter(SysLogininfor.ipaddr.like(f'%{page_object.ipaddr}%') if page_object.ipaddr else True, - SysLogininfor.user_name.like(f'%{page_object.user_name}%') if page_object.user_name else True, - SysLogininfor.status == page_object.status if page_object.status else True, + .filter(SysLogininfor.ipaddr.like(f'%{query_object.ipaddr}%') if query_object.ipaddr else True, + SysLogininfor.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True, + SysLogininfor.status == query_object.status if query_object.status else True, SysLogininfor.login_time.between( - datetime.combine(datetime.strptime(page_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.login_time_start and page_object.login_time_end else True + datetime.combine(datetime.strptime(query_object.login_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.login_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.login_time_start and query_object.login_time_end else True )\ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(login_log_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return LoginLogPageObjectResponse(**result) + return list_format_datetime(login_log_list) def add_login_log_dao(db: Session, login_log: LogininforModel): diff --git a/dash-fastapi-backend/module_admin/dao/menu_dao.py b/dash-fastapi-backend/module_admin/dao/menu_dao.py index 63dbefa..7bc9bf4 100644 --- a/dash-fastapi-backend/module_admin/dao/menu_dao.py +++ b/dash-fastapi-backend/module_admin/dao/menu_dao.py @@ -1,5 +1,8 @@ +from sqlalchemy import and_ from sqlalchemy.orm import Session from module_admin.entity.do.menu_do import SysMenu +from module_admin.entity.do.user_do import SysUser, SysUserRole +from module_admin.entity.do.role_do import SysRole, SysRoleMenu from module_admin.entity.vo.menu_vo import MenuModel, MenuResponse, CrudMenuResponse from utils.time_format_util import list_format_datetime @@ -12,42 +15,89 @@ def get_menu_detail_by_id(db: Session, menu_id: int): return menu_info -def get_menu_info_for_edit_option(db: Session, menu_info: MenuModel): - menu_result = db.query(SysMenu) \ - .filter(SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, - SysMenu.status == 0) \ - .all() +def get_menu_info_for_edit_option(db: Session, menu_info: MenuModel, user_id: int, role: list): + menu_result = [] + for item in role: + if item.role_id == 1: + menu_result = db.query(SysMenu) \ + .filter(SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, + SysMenu.status == 0) \ + .all() + else: + menu_result = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.menu_id != menu_info.menu_id, SysMenu.parent_id != menu_info.menu_id, + SysMenu.status == 0)) \ + .order_by(SysMenu.order_num) \ + .distinct().all() return list_format_datetime(menu_result) -def get_menu_list_for_tree(db: Session, menu_info: MenuModel): - menu_query_all = db.query(SysMenu) \ - .filter(SysMenu.status == 0, - SysMenu.menu_name.like(f'%{menu_info.menu_name}%') if menu_info.menu_name else True) \ - .order_by(SysMenu.order_num) \ - .distinct().all() +def get_menu_list_for_tree(db: Session, menu_info: MenuModel, user_id: int, role: list): + menu_query_all = [] + for item in role: + if item.role_id == 1: + menu_query_all = db.query(SysMenu) \ + .filter(SysMenu.status == 0, + SysMenu.menu_name.like(f'%{menu_info.menu_name}%') if menu_info.menu_name else True) \ + .order_by(SysMenu.order_num) \ + .distinct().all() + break + else: + menu_query_all = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.status == 0, + SysMenu.menu_name.like( + f'%{menu_info.menu_name}%') if menu_info.menu_name else True)) \ + .order_by(SysMenu.order_num) \ + .distinct().all() return list_format_datetime(menu_query_all) -def get_menu_list(db: Session, page_object: MenuModel): +def get_menu_list(db: Session, page_object: MenuModel, user_id: int, role: list): """ 根据查询参数获取菜单列表信息 :param db: orm对象 :param page_object: 不分页查询参数对象 + :param user_id: 用户id + :param role: 用户角色列表 :return: 菜单列表信息对象 """ - if page_object.menu_name or page_object.status: - menu_query_all = db.query(SysMenu) \ - .filter(SysMenu.status == page_object.status if page_object.status else True, - SysMenu.menu_name.like(f'%{page_object.menu_name}%') if page_object.menu_name else True) \ - .order_by(SysMenu.order_num)\ - .distinct().all() - else: - menu_query_all = db.query(SysMenu) \ - .order_by(SysMenu.order_num) \ - .distinct().all() + menu_query_all = [] + for item in role: + if item.role_id == 1: + menu_query_all = db.query(SysMenu) \ + .filter(SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like( + f'%{page_object.menu_name}%') if page_object.menu_name else True) \ + .order_by(SysMenu.order_num) \ + .distinct().all() + break + else: + menu_query_all = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, + and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, + SysMenu.status == page_object.status if page_object.status else True, + SysMenu.menu_name.like( + f'%{page_object.menu_name}%') if page_object.menu_name else True)) \ + .order_by(SysMenu.order_num) \ + .distinct().all() result = dict( rows=list_format_datetime(menu_query_all), diff --git a/dash-fastapi-backend/module_admin/dao/post_dao.py b/dash-fastapi-backend/module_admin/dao/post_dao.py index a9a70b1..4c222a0 100644 --- a/dash-fastapi-backend/module_admin/dao/post_dao.py +++ b/dash-fastapi-backend/module_admin/dao/post_dao.py @@ -2,7 +2,6 @@ from sqlalchemy.orm import Session from module_admin.entity.do.post_do import SysPost from module_admin.entity.vo.post_vo import PostModel, PostPageObject, PostPageObjectResponse, CrudPostResponse from utils.time_format_util import list_format_datetime -from utils.page_util import get_page_info def get_post_by_id(db: Session, post_id: int): @@ -30,41 +29,22 @@ def get_post_select_option_dao(db: Session): return post_info -def get_post_list(db: Session, page_object: PostPageObject): +def get_post_list(db: Session, query_object: PostPageObject): """ 根据查询参数获取岗位列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 岗位列表信息对象 """ - count = db.query(SysPost) \ - .filter(SysPost.post_code.like(f'%{page_object.post_code}%') if page_object.post_code else True, - SysPost.post_name.like(f'%{page_object.post_name}%') if page_object.post_name else True, - SysPost.status == page_object.status if page_object.status else True - )\ - .order_by(SysPost.post_sort)\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) post_list = db.query(SysPost) \ - .filter(SysPost.post_code.like(f'%{page_object.post_code}%') if page_object.post_code else True, - SysPost.post_name.like(f'%{page_object.post_name}%') if page_object.post_name else True, - SysPost.status == page_object.status if page_object.status else True + .filter(SysPost.post_code.like(f'%{query_object.post_code}%') if query_object.post_code else True, + SysPost.post_name.like(f'%{query_object.post_name}%') if query_object.post_name else True, + SysPost.status == query_object.status if query_object.status else True ) \ .order_by(SysPost.post_sort) \ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(post_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return PostPageObjectResponse(**result) + return list_format_datetime(post_list) def add_post_dao(db: Session, post: PostModel): diff --git a/dash-fastapi-backend/module_admin/dao/role_dao.py b/dash-fastapi-backend/module_admin/dao/role_dao.py index 6f4f38b..2f75ea0 100644 --- a/dash-fastapi-backend/module_admin/dao/role_dao.py +++ b/dash-fastapi-backend/module_admin/dao/role_dao.py @@ -4,7 +4,6 @@ from module_admin.entity.do.role_do import SysRole, SysRoleMenu from module_admin.entity.do.menu_do import SysMenu from module_admin.entity.vo.role_vo import RoleModel, RoleMenuModel, RolePageObject, RolePageObjectResponse, CrudRoleResponse, RoleDetailModel from utils.time_format_util import list_format_datetime, object_format_datetime -from utils.page_util import get_page_info from datetime import datetime, time @@ -57,57 +56,33 @@ def get_role_detail_by_id(db: Session, role_id: int): def get_role_select_option_dao(db: Session): role_info = db.query(SysRole) \ - .filter(SysRole.status == 0, SysRole.del_flag == 0) \ + .filter(SysRole.role_id != 1, SysRole.status == 0, SysRole.del_flag == 0) \ .all() return role_info -def get_role_list(db: Session, page_object: RolePageObject): +def get_role_list(db: Session, query_object: RolePageObject): """ 根据查询参数获取角色列表信息 :param db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 角色列表信息对象 """ - count = db.query(SysRole) \ - .filter(SysRole.del_flag == 0, - SysRole.role_name.like(f'%{page_object.role_name}%') if page_object.role_name else True, - SysRole.role_key.like(f'%{page_object.role_key}%') if page_object.role_key else True, - SysRole.status == page_object.status if page_object.status else True, - SysRole.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True - )\ - .order_by(SysRole.role_sort)\ - .distinct().count() - offset_com = (page_object.page_num - 1) * page_object.page_size - page_info = get_page_info(offset_com, page_object.page_num, page_object.page_size, count) role_list = db.query(SysRole) \ .filter(SysRole.del_flag == 0, - SysRole.role_name.like(f'%{page_object.role_name}%') if page_object.role_name else True, - SysRole.role_key.like(f'%{page_object.role_key}%') if page_object.role_key else True, - SysRole.status == page_object.status if page_object.status else True, + SysRole.role_name.like(f'%{query_object.role_name}%') if query_object.role_name else True, + SysRole.role_key.like(f'%{query_object.role_key}%') if query_object.role_key else True, + SysRole.status == query_object.status if query_object.status else True, SysRole.create_time.between( - datetime.combine(datetime.strptime(page_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(page_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if page_object.create_time_start and page_object.create_time_end else True + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True ) \ .order_by(SysRole.role_sort) \ - .offset(page_info.offset) \ - .limit(page_object.page_size) \ .distinct().all() - result = dict( - rows=list_format_datetime(role_list), - page_num=page_info.page_num, - page_size=page_info.page_size, - total=page_info.total, - has_next=page_info.has_next - ) - - return RolePageObjectResponse(**result) + return list_format_datetime(role_list) def add_role_dao(db: Session, role: RoleModel): diff --git a/dash-fastapi-backend/module_admin/dao/user_dao.py b/dash-fastapi-backend/module_admin/dao/user_dao.py index f34330b..3174806 100644 --- a/dash-fastapi-backend/module_admin/dao/user_dao.py +++ b/dash-fastapi-backend/module_admin/dao/user_dao.py @@ -51,13 +51,21 @@ def get_user_by_id(db: Session, user_id: int): .outerjoin(SysUserPost, SysUser.user_id == SysUserPost.user_id) \ .outerjoin(SysPost, and_(SysUserPost.post_id == SysPost.post_id, SysPost.status == 0)) \ .distinct().all() - query_user_menu_info = db.query(SysMenu).select_from(SysUser) \ - .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ - .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ - .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ - .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ - .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ - .distinct().all() + query_user_menu_info = [] + for item in query_user_role_info: + if item.role_id == 1: + query_user_menu_info = db.query(SysMenu) \ + .filter(SysMenu.status == 0) \ + .distinct().all() + break + else: + query_user_menu_info = db.query(SysMenu).select_from(SysUser) \ + .filter(SysUser.status == 0, SysUser.del_flag == 0, SysUser.user_id == user_id) \ + .outerjoin(SysUserRole, SysUser.user_id == SysUserRole.user_id) \ + .outerjoin(SysRole, and_(SysUserRole.role_id == SysRole.role_id, SysRole.status == 0, SysRole.del_flag == 0)) \ + .outerjoin(SysRoleMenu, SysRole.role_id == SysRoleMenu.role_id) \ + .outerjoin(SysMenu, and_(SysRoleMenu.menu_id == SysMenu.menu_id, SysMenu.status == 0)) \ + .distinct().all() results = dict( user_basic_info=list_format_datetime(query_user_basic_info), user_dept_info=list_format_datetime(query_user_dept_info), @@ -192,31 +200,31 @@ def get_user_detail_by_id(db: Session, user_id: int): # return UserPageObjectResponse(**result) -def get_user_list(db: Session, user_object: UserModel): +def get_user_list(db: Session, query_object: UserPageObject): """ 根据查询参数获取用户列表信息 :param db: orm对象 - :param user_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 用户列表信息对象 """ user_list = db.query(SysUser, SysDept) \ .filter(SysUser.del_flag == 0, - SysUser.dept_id == user_object.dept_id if user_object.dept_id else True, - SysUser.user_name.like(f'%{user_object.user_name}%') if user_object.user_name else True, - SysUser.nick_name.like(f'%{user_object.nick_name}%') if user_object.nick_name else True, - SysUser.email.like(f'%{user_object.email}%') if user_object.email else True, - SysUser.phonenumber.like(f'%{user_object.phonenumber}%') if user_object.phonenumber else True, - SysUser.status == user_object.status if user_object.status else True, - SysUser.sex == user_object.sex if user_object.sex else True, + SysUser.dept_id == query_object.dept_id if query_object.dept_id else True, + SysUser.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True, + SysUser.nick_name.like(f'%{query_object.nick_name}%') if query_object.nick_name else True, + SysUser.email.like(f'%{query_object.email}%') if query_object.email else True, + SysUser.phonenumber.like(f'%{query_object.phonenumber}%') if query_object.phonenumber else True, + SysUser.status == query_object.status if query_object.status else True, + SysUser.sex == query_object.sex if query_object.sex else True, SysUser.create_time.between( - datetime.combine(datetime.strptime(user_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), - datetime.combine(datetime.strptime(user_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) - if user_object.create_time_start and user_object.create_time_end else True + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True ) \ .outerjoin(SysDept, and_(SysUser.dept_id == SysDept.dept_id, SysDept.status == 0, SysDept.del_flag == 0)) \ .distinct().all() - result_list: List[Union[UserInfoJoinDept, None]] = [] + result_list: List[Union[dict, None]] = [] if user_list: for item in user_list: obj = dict( @@ -242,7 +250,7 @@ def get_user_list(db: Session, user_object: UserModel): ) result_list.append(obj) - return result_list + return format_datetime_dict_list(result_list) def add_user_dao(db: Session, user: UserModel): diff --git a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py index c492d8a..01cf656 100644 --- a/dash-fastapi-backend/module_admin/entity/vo/user_vo.py +++ b/dash-fastapi-backend/module_admin/entity/vo/user_vo.py @@ -32,8 +32,6 @@ class UserModel(BaseModel): update_by: Optional[str] update_time: Optional[str] remark: Optional[str] - create_time_start: Optional[str] - create_time_end: Optional[str] class Config: orm_mode = True @@ -158,33 +156,17 @@ class UserPageObject(UserModel): """ 用户管理分页查询模型 """ + create_time_start: Optional[str] + create_time_end: Optional[str] page_num: int page_size: int -class UserInfoJoinDept(BaseModel): +class UserInfoJoinDept(UserModel): """ 数据库查询用户列表返回模型 """ - user_id: Optional[int] - dept_id: Optional[int] dept_name: Optional[str] - user_name: Optional[str] - nick_name: Optional[str] - user_type: Optional[str] - email: Optional[str] - phonenumber: Optional[str] - sex: Optional[str] - avatar: Optional[str] - status: Optional[str] - del_flag: Optional[str] - login_ip: Optional[str] - login_date: Optional[str] - create_by: Optional[str] - create_time: Optional[str] - update_by: Optional[str] - update_time: Optional[str] - remark: Optional[str] class UserPageObjectResponse(BaseModel): diff --git a/dash-fastapi-backend/module_admin/service/dict_service.py b/dash-fastapi-backend/module_admin/service/dict_service.py index 9069456..bf4d943 100644 --- a/dash-fastapi-backend/module_admin/service/dict_service.py +++ b/dash-fastapi-backend/module_admin/service/dict_service.py @@ -2,17 +2,25 @@ from module_admin.entity.vo.dict_vo import * from module_admin.dao.dict_dao import * -def get_dict_type_list_services(result_db: Session, page_object: DictTypePageObject): +def get_dict_type_list_services(result_db: Session, query_object: DictTypePageObject): """ 获取字典类型列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典类型列表信息对象 """ - if page_object.page_num and page_object.page_size: - dict_type_list_result = get_dict_type_list(result_db, page_object) - else: - dict_type_list_result = get_all_dict_type(result_db) + dict_type_list_result = get_dict_type_list(result_db, query_object) + + return dict_type_list_result + + +def get_all_dict_type_services(result_db: Session): + """ + 获取字所有典类型列表信息service + :param result_db: orm对象 + :return: 字典类型列表信息对象 + """ + dict_type_list_result = get_all_dict_type(result_db) return dict_type_list_result @@ -72,14 +80,14 @@ def detail_dict_type_services(result_db: Session, dict_id: int): return dict_type -def get_dict_data_list_services(result_db: Session, page_object: DictDataPageObject): +def get_dict_data_list_services(result_db: Session, query_object: DictDataPageObject): """ 获取字典数据列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 字典数据列表信息对象 """ - dict_data_list_result = get_dict_data_list(result_db, page_object) + dict_data_list_result = get_dict_data_list(result_db, query_object) return dict_data_list_result diff --git a/dash-fastapi-backend/module_admin/service/log_service.py b/dash-fastapi-backend/module_admin/service/log_service.py index 6aac3d1..2944728 100644 --- a/dash-fastapi-backend/module_admin/service/log_service.py +++ b/dash-fastapi-backend/module_admin/service/log_service.py @@ -2,14 +2,14 @@ from module_admin.entity.vo.log_vo import * from module_admin.dao.log_dao import * -def get_operation_log_list_services(result_db: Session, page_object: OperLogPageObject): +def get_operation_log_list_services(result_db: Session, query_object: OperLogPageObject): """ 获取操作日志列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 操作日志列表信息对象 """ - operation_log_list_result = get_operation_log_list(result_db, page_object) + operation_log_list_result = get_operation_log_list(result_db, query_object) return operation_log_list_result @@ -72,14 +72,14 @@ def detail_operation_log_services(result_db: Session, oper_id: int): return operation_log -def get_login_log_list_services(result_db: Session, page_object: LoginLogPageObject): +def get_login_log_list_services(result_db: Session, query_object: LoginLogPageObject): """ 获取登录日志列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 登录日志列表信息对象 """ - operation_log_list_result = get_login_log_list(result_db, page_object) + operation_log_list_result = get_login_log_list(result_db, query_object) return operation_log_list_result diff --git a/dash-fastapi-backend/module_admin/service/menu_service.py b/dash-fastapi-backend/module_admin/service/menu_service.py index adebaa8..083e2e8 100644 --- a/dash-fastapi-backend/module_admin/service/menu_service.py +++ b/dash-fastapi-backend/module_admin/service/menu_service.py @@ -1,16 +1,18 @@ from module_admin.entity.vo.menu_vo import * from module_admin.dao.menu_dao import * +from module_admin.entity.vo.user_vo import CurrentUserInfoServiceResponse -def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): +def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel, current_user: Optional[CurrentUserInfoServiceResponse] = None): """ 获取菜单树信息service :param result_db: orm对象 :param page_object: 查询参数对象 + :param current_user: 当前用户对象 :return: 菜单树信息对象 """ menu_tree_option = [] - menu_list_result = get_menu_list_for_tree(result_db, MenuModel(**page_object.dict())) + menu_list_result = get_menu_list_for_tree(result_db, MenuModel(**page_object.dict()), current_user.user.user_id, current_user.role) menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) if page_object.type != 'role': menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) @@ -20,29 +22,31 @@ def get_menu_tree_services(result_db: Session, page_object: MenuTreeModel): return menu_tree_option -def get_menu_tree_for_edit_option_services(result_db: Session, page_object: MenuModel): +def get_menu_tree_for_edit_option_services(result_db: Session, page_object: MenuModel, current_user: Optional[CurrentUserInfoServiceResponse] = None): """ 获取菜单编辑菜单树信息service :param result_db: orm对象 :param page_object: 查询参数对象 + :param current_user: 当前用户 :return: 菜单树信息对象 """ menu_tree_option = [] - menu_list_result = get_menu_info_for_edit_option(result_db, page_object) + menu_list_result = get_menu_info_for_edit_option(result_db, page_object, current_user.user.user_id, current_user.role) menu_tree_result = get_menu_tree(0, MenuTree(menu_tree=menu_list_result)) menu_tree_option.append(dict(title='主类目', value='0', key='0', children=menu_tree_result)) return menu_tree_option -def get_menu_list_services(result_db: Session, page_object: MenuModel): +def get_menu_list_services(result_db: Session, page_object: MenuModel, current_user: Optional[CurrentUserInfoServiceResponse] = None): """ 获取菜单列表信息service :param result_db: orm对象 :param page_object: 分页查询参数对象 + :param current_user: 当前用户对象 :return: 菜单列表信息对象 """ - menu_list_result = get_menu_list(result_db, page_object) + menu_list_result = get_menu_list(result_db, page_object, current_user.user.user_id, current_user.role) return menu_list_result diff --git a/dash-fastapi-backend/module_admin/service/post_service.py b/dash-fastapi-backend/module_admin/service/post_service.py index 507f477..5ecd66e 100644 --- a/dash-fastapi-backend/module_admin/service/post_service.py +++ b/dash-fastapi-backend/module_admin/service/post_service.py @@ -13,14 +13,14 @@ def get_post_select_option_services(result_db: Session): return post_list_result -def get_post_list_services(result_db: Session, page_object: PostPageObject): +def get_post_list_services(result_db: Session, query_object: PostPageObject): """ 获取岗位列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 岗位列表信息对象 """ - post_list_result = get_post_list(result_db, page_object) + post_list_result = get_post_list(result_db, query_object) return post_list_result diff --git a/dash-fastapi-backend/module_admin/service/role_service.py b/dash-fastapi-backend/module_admin/service/role_service.py index ed30123..50f4ec3 100644 --- a/dash-fastapi-backend/module_admin/service/role_service.py +++ b/dash-fastapi-backend/module_admin/service/role_service.py @@ -13,14 +13,14 @@ def get_role_select_option_services(result_db: Session): return role_list_result -def get_role_list_services(result_db: Session, page_object: RolePageObject): +def get_role_list_services(result_db: Session, query_object: RolePageObject): """ 获取角色列表信息service :param result_db: orm对象 - :param page_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 角色列表信息对象 """ - role_list_result = get_role_list(result_db, page_object) + role_list_result = get_role_list(result_db, query_object) return role_list_result diff --git a/dash-fastapi-backend/module_admin/service/user_service.py b/dash-fastapi-backend/module_admin/service/user_service.py index b070411..5d8bb90 100644 --- a/dash-fastapi-backend/module_admin/service/user_service.py +++ b/dash-fastapi-backend/module_admin/service/user_service.py @@ -15,14 +15,14 @@ from module_admin.service.login_service import verify_password # return user_list_result -def get_user_list_services(result_db: Session, user_object: UserModel): +def get_user_list_services(result_db: Session, query_object: UserPageObject): """ 获取用户列表信息service :param result_db: orm对象 - :param user_object: 分页查询参数对象 + :param query_object: 查询参数对象 :return: 用户列表信息对象 """ - user_list_result = get_user_list(result_db, user_object) + user_list_result = get_user_list(result_db, query_object) return user_list_result @@ -129,11 +129,15 @@ def reset_user_services(result_db: Session, page_object: ResetUserModel): :return: 重置用户校验结果 """ user = get_user_detail_by_id(result_db, user_id=page_object.user_id).user_basic_info[0] - if not verify_password(page_object.old_password, user.password): - result = CrudUserResponse(**dict(is_success=False, message='旧密码不正确')) + if page_object.old_password: + if not verify_password(page_object.old_password, user.password): + result = CrudUserResponse(**dict(is_success=False, message='旧密码不正确')) + else: + reset_user = page_object.dict(exclude_unset=True) + del reset_user['old_password'] + result = edit_user_dao(result_db, reset_user) else: reset_user = page_object.dict(exclude_unset=True) - del reset_user['old_password'] result = edit_user_dao(result_db, reset_user) return result diff --git a/dash-fastapi-backend/utils/page_util.py b/dash-fastapi-backend/utils/page_util.py index 6b63ff8..dd78021 100644 --- a/dash-fastapi-backend/utils/page_util.py +++ b/dash-fastapi-backend/utils/page_util.py @@ -3,8 +3,6 @@ from typing import List from pydantic import BaseModel -from utils.time_format_util import format_datetime_dict_list - class PageModel(BaseModel): """ @@ -71,10 +69,10 @@ def get_page_obj(data_list: List, page_num: int, page_size: int): # 根据计算得到的起始索引和结束索引对数据列表进行切片 paginated_data = data_list[start:end] - has_next = True if math.ceil(len(data_list) / page_size) > page_num else False; + has_next = True if math.ceil(len(data_list) / page_size) > page_num else False result = dict( - rows=format_datetime_dict_list(paginated_data), + rows=paginated_data, page_num=page_num, page_size=page_size, total=len(data_list), diff --git a/dash-fastapi-frontend/app.py b/dash-fastapi-frontend/app.py index ff353f4..e0454ee 100644 --- a/dash-fastapi-frontend/app.py +++ b/dash-fastapi-frontend/app.py @@ -90,17 +90,17 @@ def router(pathname, trigger): session['dept_info'] = current_user['dept'] session['role_info'] = current_user['role'] session['post_info'] = current_user['post'] - valid_href_list = find_node_values(menu_info, 'href') - valid_href_list = valid_href_list + RouterConfig.STATIC_VALID_PATHNAME + dynamic_valid_pathname_list = find_node_values(menu_info, 'href') + valid_href_list = dynamic_valid_pathname_list + RouterConfig.STATIC_VALID_PATHNAME if pathname in valid_href_list: current_key = find_key_by_href(menu_info, pathname) + if pathname == '/': + current_key = '首页' + if pathname == '/user/profile': + current_key = '个人资料' if trigger == 'load': # 根据pathname控制渲染行为 - if pathname == '/': - current_key = '首页' - if pathname == '/user/profile': - current_key = '个人资料' if pathname == '/login' or pathname == '/forget': # 重定向到主页面 return [ @@ -129,10 +129,6 @@ def router(pathname, trigger): # elif trigger == 'pushstate': else: - if pathname == '/': - current_key = '首页' - if pathname == '/user/profile': - current_key = '个人资料' return [ dash.no_update, None, diff --git a/dash-fastapi-frontend/callbacks/system_c/dept_c.py b/dash-fastapi-frontend/callbacks/system_c/dept_c.py index 028218e..3c06cba 100644 --- a/dash-fastapi-frontend/callbacks/system_c/dept_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/dept_c.py @@ -49,7 +49,18 @@ def get_dept_table_data(search_click, operations, fold_click, dept_name, status_ item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dept_id']) if item['parent_id'] == 0: - item['operation'] = [] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dept:edit' in button_perms else {}, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + } if 'system:dept:add' in button_perms else {}, + ] else: item['operation'] = [ { @@ -98,6 +109,7 @@ def reset_dept_query_params(reset_click): @app.callback( [Output('dept-modal', 'visible', allow_duplicate=True), Output('dept-modal', 'title'), + Output('dept-parent_id-div', 'hidden'), Output('dept-parent_id', 'treeData'), Output('dept-parent_id', 'value'), Output('dept-dept_name', 'value'), @@ -130,6 +142,7 @@ def add_edit_dept_modal(add_click, button_click, clicked_content, recently_butto return [ True, '新增部门', + False, tree_data, None, None, @@ -147,6 +160,7 @@ def add_edit_dept_modal(add_click, button_click, clicked_content, recently_butto return [ True, '新增部门', + False, tree_data, str(recently_button_clicked_row['key']), None, @@ -165,22 +179,42 @@ def add_edit_dept_modal(add_click, button_click, clicked_content, recently_butto dept_info_res = get_dept_detail_api(dept_id=dept_id) if dept_info_res['code'] == 200: dept_info = dept_info_res['data'] - return [ - True, - '编辑部门', - tree_data, - str(dept_info.get('parent_id')), - dept_info.get('dept_name'), - dept_info.get('order_num'), - dept_info.get('leader'), - dept_info.get('phone'), - dept_info.get('email'), - dept_info.get('status'), - {'timestamp': time.time()}, - None, - dept_info, - {'type': 'edit'} - ] + if dept_info.get('parent_id') == 0: + return [ + True, + '编辑部门', + True, + tree_data, + str(dept_info.get('parent_id')), + dept_info.get('dept_name'), + dept_info.get('order_num'), + dept_info.get('leader'), + dept_info.get('phone'), + dept_info.get('email'), + dept_info.get('status'), + {'timestamp': time.time()}, + None, + dept_info, + {'type': 'edit'} + ] + else: + return [ + True, + '编辑部门', + False, + tree_data, + str(dept_info.get('parent_id')), + dept_info.get('dept_name'), + dept_info.get('order_num'), + dept_info.get('leader'), + dept_info.get('phone'), + dept_info.get('email'), + dept_info.get('status'), + {'timestamp': time.time()}, + None, + dept_info, + {'type': 'edit'} + ] return [dash.no_update] * 10 + [{'timestamp': time.time()}, None, None, None] diff --git a/dash-fastapi-frontend/callbacks/system_c/role_c.py b/dash-fastapi-frontend/callbacks/system_c/role_c.py index fde15bd..5e2a33a 100644 --- a/dash-fastapi-frontend/callbacks/system_c/role_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/role_c.py @@ -72,18 +72,21 @@ def get_role_table_data(search_click, pagination, operations, role_name, role_ke else: item['status'] = dict(checked=False) item['key'] = str(item['role_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - } if 'system:role:edit' in button_perms else {}, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - } if 'system:role:remove' in button_perms else {}, - ] + if item['role_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:role:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:role:remove' in button_perms else {}, + ] return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] diff --git a/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py index e161a7a..18ac37d 100644 --- a/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py +++ b/dash-fastapi-frontend/callbacks/system_c/user_c/user_c.py @@ -11,7 +11,7 @@ from collections import OrderedDict from server import app from api.dept import get_dept_tree_api -from api.user import get_user_list_api, get_user_detail_api, add_user_api, edit_user_api, delete_user_api +from api.user import get_user_list_api, get_user_detail_api, add_user_api, edit_user_api, delete_user_api, reset_user_password_api from api.role import get_role_select_option_api from api.post import get_post_select_option_api @@ -100,20 +100,23 @@ def get_user_table_data_by_dept_tree(selected_dept_tree, search_click, paginatio else: item['status'] = dict(checked=False) item['key'] = str(item['user_id']) - item['operation'] = [ - { - 'title': '修改', - 'icon': 'antd-edit' - } if 'system:user:edit' in button_perms else None, - { - 'title': '删除', - 'icon': 'antd-delete' - } if 'system:user:remove' in button_perms else None, - { - 'title': '重置密码', - 'icon': 'antd-key' - } if 'system:user:resetPwd' in button_perms else None - ] + if item['user_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + } if 'system:user:edit' in button_perms else None, + { + 'title': '删除', + 'icon': 'antd-delete' + } if 'system:user:remove' in button_perms else None, + { + 'title': '重置密码', + 'icon': 'antd-key' + } if 'system:user:resetPwd' in button_perms else None + ] return [table_data, table_pagination, str(uuid.uuid4()), None, {'timestamp': time.time()}] @@ -492,3 +495,59 @@ def user_delete_confirm(delete_confirm, user_ids_data): ] return [dash.no_update] * 3 + + +@app.callback( + [Output('user-reset-password-confirm-modal', 'visible'), + Output('reset-password-row-key-store', 'data'), + Output('reset-password-input', 'value')], + Input('user-list-table', 'nClicksDropdownItem'), + [State('user-list-table', 'recentlyClickedDropdownItemTitle'), + State('user-list-table', 'recentlyDropdownItemClickedRow')], + prevent_initial_call=True +) +def user_reset_password_modal(dropdown_click, recently_clicked_dropdown_item_title, recently_dropdown_item_clicked_row): + if dropdown_click: + if recently_clicked_dropdown_item_title == '重置密码': + user_id = recently_dropdown_item_clicked_row['key'] + else: + return [dash.no_update] * 3 + + return [ + True, + {'user_id': user_id}, + None + ] + + return [dash.no_update] * 3 + + +@app.callback( + [Output('user-operations-store', 'data', allow_duplicate=True), + Output('api-check-token', 'data', allow_duplicate=True), + Output('global-message-container', 'children', allow_duplicate=True)], + Input('user-reset-password-confirm-modal', 'okCounts'), + [State('reset-password-row-key-store', 'data'), + State('reset-password-input', 'value')], + prevent_initial_call=True +) +def user_reset_password_confirm(reset_confirm, user_id_data, reset_password): + if reset_confirm: + + user_id_data['password'] = reset_password + params = user_id_data + reset_button_info = reset_user_password_api(params) + if reset_button_info['code'] == 200: + return [ + {'type': 'reset-password'}, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('重置成功', type='success') + ] + + return [ + dash.no_update, + {'timestamp': time.time()}, + fuc.FefferyFancyMessage('重置失败', type='error') + ] + + return [dash.no_update] * 3 diff --git a/dash-fastapi-frontend/views/system/dept/__init__.py b/dash-fastapi-frontend/views/system/dept/__init__.py index 3a22623..fe6a886 100644 --- a/dash-fastapi-frontend/views/system/dept/__init__.py +++ b/dash-fastapi-frontend/views/system/dept/__init__.py @@ -20,7 +20,18 @@ def render(button_perms): item['status'] = dict(tag='停用', color='volcano') item['key'] = str(item['dept_id']) if item['parent_id'] == 0: - item['operation'] = [] + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:dept:edit' in button_perms else {}, + { + 'content': '新增', + 'type': 'link', + 'icon': 'antd-plus' + } if 'system:dept:add' in button_perms else {}, + ] else: item['operation'] = [ { @@ -250,25 +261,31 @@ def render(button_perms): fac.AntdRow( [ fac.AntdCol( - fac.AntdFormItem( - fac.AntdTreeSelect( - id='dept-parent_id', - placeholder='请选择上级部门', - treeData=[], - treeNodeFilterProp='title', - style={ - 'width': '100%' - } - ), - label='上级部门', - required=True, - id='dept-parent_id-form-item', - labelCol={ - 'span': 4 - }, - wrapperCol={ - 'span': 20 - } + html.Div( + [ + fac.AntdFormItem( + fac.AntdTreeSelect( + id='dept-parent_id', + placeholder='请选择上级部门', + treeData=[], + treeNodeFilterProp='title', + style={ + 'width': '100%' + } + ), + label='上级部门', + required=True, + id='dept-parent_id-form-item', + labelCol={ + 'span': 4 + }, + wrapperCol={ + 'span': 20 + } + ), + ], + id='dept-parent_id-div', + hidden=False ), span=24 ), diff --git a/dash-fastapi-frontend/views/system/role/__init__.py b/dash-fastapi-frontend/views/system/role/__init__.py index e17ac94..5a8a6ee 100644 --- a/dash-fastapi-frontend/views/system/role/__init__.py +++ b/dash-fastapi-frontend/views/system/role/__init__.py @@ -24,18 +24,21 @@ def render(button_perms): else: item['status'] = dict(checked=False) item['key'] = str(item['role_id']) - item['operation'] = [ - { - 'content': '修改', - 'type': 'link', - 'icon': 'antd-edit' - } if 'system:role:edit' in button_perms else {}, - { - 'content': '删除', - 'type': 'link', - 'icon': 'antd-delete' - } if 'system:role:remove' in button_perms else {}, - ] + if item['role_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'content': '修改', + 'type': 'link', + 'icon': 'antd-edit' + } if 'system:role:edit' in button_perms else {}, + { + 'content': '删除', + 'type': 'link', + 'icon': 'antd-delete' + } if 'system:role:remove' in button_perms else {}, + ] return [ dcc.Store(id='role-button-perms-container', data=button_perms), diff --git a/dash-fastapi-frontend/views/system/user/__init__.py b/dash-fastapi-frontend/views/system/user/__init__.py index 0911d78..59d7803 100644 --- a/dash-fastapi-frontend/views/system/user/__init__.py +++ b/dash-fastapi-frontend/views/system/user/__init__.py @@ -31,20 +31,23 @@ def render(button_perms): else: item['status'] = dict(checked=False) item['key'] = str(item['user_id']) - item['operation'] = [ - { - 'title': '修改', - 'icon': 'antd-edit' - } if 'system:user:edit' in button_perms else None, - { - 'title': '删除', - 'icon': 'antd-delete' - } if 'system:user:remove' in button_perms else None, - { - 'title': '重置密码', - 'icon': 'antd-key' - } if 'system:user:resetPwd' in button_perms else None - ] + if item['user_id'] == 1: + item['operation'] = [] + else: + item['operation'] = [ + { + 'title': '修改', + 'icon': 'antd-edit' + } if 'system:user:edit' in button_perms else None, + { + 'title': '删除', + 'icon': 'antd-delete' + } if 'system:user:remove' in button_perms else None, + { + 'title': '重置密码', + 'icon': 'antd-key' + } if 'system:user:resetPwd' in button_perms else None + ] return [ dcc.Store(id='user-button-perms-container', data=button_perms), @@ -836,20 +839,24 @@ def render(button_perms): # 重置密码modal fac.AntdModal( - fac.AntdForm( - [ - fac.AntdFormItem( - fac.AntdInput( - id='reset-password-input', - mode='password' + [ + fac.AntdForm( + [ + fac.AntdFormItem( + fac.AntdInput( + id='reset-password-input', + mode='password' + ), + label='请输入新密码' ), - ), - ], - layout='vertical' - ), + ], + layout='vertical' + ), + dcc.Store(id='reset-password-row-key-store') + ], id='user-reset-password-confirm-modal', visible=False, - title='提示', + title='重置密码', renderFooter=True, centered=True ), -- Gitee From 0093740cb5a14f0b5a5d912c887a168476860083 Mon Sep 17 00:00:00 2001 From: xlf <3055204202@qq.com> Date: Wed, 9 Aug 2023 16:18:41 +0800 Subject: [PATCH 032/169] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=85=AC=E5=91=8A=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=88=E5=BC=95=E5=85=A5wangeditor=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dash-fastapi-backend/app.py | 2 + .../module_admin/annotation/log_annotation.py | 2 + .../controller/notice_controller.py | 92 + .../controller/user_controller.py | 1 + .../module_admin/dao/notice_dao.py | 83 + .../module_admin/entity/do/notice_do.py | 24 + .../module_admin/entity/vo/notice_vo.py | 57 + .../module_admin/service/notice_service.py | 69 + dash-fastapi-frontend/api/notice.py | 26 + .../assets/wangeditor/css/style.css | 27 + .../assets/wangeditor/index.js | 24129 ++++++++++++++++ .../callbacks/system_c/notice_c.py | 417 + dash-fastapi-frontend/store/store.py | 7 + .../views/system/notice/__init__.py | 472 +- 14 files changed, 25405 insertions(+), 3 deletions(-) create mode 100644 dash-fastapi-backend/module_admin/controller/notice_controller.py create mode 100644 dash-fastapi-backend/module_admin/dao/notice_dao.py create mode 100644 dash-fastapi-backend/module_admin/entity/do/notice_do.py create mode 100644 dash-fastapi-backend/module_admin/entity/vo/notice_vo.py create mode 100644 dash-fastapi-backend/module_admin/service/notice_service.py create mode 100644 dash-fastapi-frontend/api/notice.py create mode 100644 dash-fastapi-frontend/assets/wangeditor/css/style.css create mode 100644 dash-fastapi-frontend/assets/wangeditor/index.js create mode 100644 dash-fastapi-frontend/callbacks/system_c/notice_c.py diff --git a/dash-fastapi-backend/app.py b/dash-fastapi-backend/app.py index 3cde6db..ee57bcb 100644 --- a/dash-fastapi-backend/app.py +++ b/dash-fastapi-backend/app.py @@ -13,6 +13,7 @@ from module_admin.controller.dept_controller import deptController from module_admin.controller.role_controller import roleController from module_admin.controller.post_controler import postController from module_admin.controller.dict_controller import dictController +from module_admin.controller.notice_controller import noticeController from module_admin.controller.log_controller import logController from module_admin.controller.common_controller import commonController from config.env import RedisConfig @@ -82,6 +83,7 @@ app.include_router(deptController, prefix="/system", tags=['system/dept']) app.include_router(roleController, prefix="/system", tags=['system/role']) app.include_router(postController, prefix="/system", tags=['system/post']) app.include_router(dictController, prefix="/system", tags=['system/dict']) +app.include_router(noticeController, prefix="/system", tags=['system/notice']) app.include_router(logController, prefix="/system", tags=['system/log']) app.include_router(commonController, prefix="/common", tags=['common']) diff --git a/dash-fastapi-backend/module_admin/annotation/log_annotation.py b/dash-fastapi-backend/module_admin/annotation/log_annotation.py index 5ef2342..4bafe6f 100644 --- a/dash-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/dash-fastapi-backend/module_admin/annotation/log_annotation.py @@ -65,6 +65,8 @@ def log_decorator(title: str, business_type: int, log_type: Optional[str] = 'ope finally: payload = await request.body() oper_param = json.dumps(json.loads(str(payload, 'utf-8')), ensure_ascii=False) + if len(oper_param) > 2000: + oper_param = '请求参数过长' # 调用原始函数 oper_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/dash-fastapi-backend/module_admin/controller/notice_controller.py b/dash-fastapi-backend/module_admin/controller/notice_controller.py new file mode 100644 index 0000000..a985268 --- /dev/null +++ b/dash-fastapi-backend/module_admin/controller/notice_controller.py @@ -0,0 +1,92 @@ +from fastapi import APIRouter, Request +from fastapi import Depends, Header +from config.get_db import get_db +from module_admin.service.login_service import get_current_user +from module_admin.service.notice_service import * +from module_admin.entity.vo.notice_vo import * +from utils.response_util import * +from utils.log_util import * +from utils.page_util import get_page_obj +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.annotation.log_annotation import log_decorator + + +noticeController = APIRouter(dependencies=[Depends(get_current_user)]) + + +@noticeController.post("/notice/get", response_model=NoticePageObjectResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:list'))]) +async def get_system_notice_list(request: Request, notice_page_query: NoticePageObject, query_db: Session = Depends(get_db)): + try: + # 获取全量数据 + notice_query_result = get_notice_list_services(query_db, notice_page_query) + # 分页操作 + notice_page_query_result = get_page_obj(notice_query_result, notice_page_query.page_num, notice_page_query.page_size) + logger.info('获取成功') + return response_200(data=notice_page_query_result, message="获取成功") + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.post("/notice/add", response_model=CrudNoticeResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:add'))]) +@log_decorator(title='通知公告管理', business_type=1) +async def add_system_notice(request: Request, add_notice: NoticeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + add_notice.create_by = current_user.user.user_name + add_notice.update_by = current_user.user.user_name + add_notice_result = add_notice_services(query_db, add_notice) + logger.info(add_notice_result.message) + if add_notice_result.is_success: + return response_200(data=add_notice_result, message=add_notice_result.message) + else: + return response_400(data="", message=add_notice_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.patch("/notice/edit", response_model=CrudNoticeResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:edit'))]) +@log_decorator(title='通知公告管理', business_type=2) +async def edit_system_notice(request: Request, edit_notice: NoticeModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): + try: + current_user = await get_current_user(request, token, query_db) + edit_notice.update_by = current_user.user.user_name + edit_notice.update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + edit_notice_result = edit_notice_services(query_db, edit_notice) + if edit_notice_result.is_success: + logger.info(edit_notice_result.message) + return response_200(data=edit_notice_result, message=edit_notice_result.message) + else: + logger.warning(edit_notice_result.message) + return response_400(data="", message=edit_notice_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.post("/notice/delete", response_model=CrudNoticeResponse, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:remove'))]) +@log_decorator(title='通知公告管理', business_type=3) +async def delete_system_notice(request: Request, delete_notice: DeleteNoticeModel, query_db: Session = Depends(get_db)): + try: + delete_notice_result = delete_notice_services(query_db, delete_notice) + if delete_notice_result.is_success: + logger.info(delete_notice_result.message) + return response_200(data=delete_notice_result, message=delete_notice_result.message) + else: + logger.warning(delete_notice_result.message) + return response_400(data="", message=delete_notice_result.message) + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") + + +@noticeController.get("/notice/{notice_id}", response_model=NoticeModel, dependencies=[Depends(CheckUserInterfaceAuth('system:notice:edit'))]) +async def query_detail_system_post(request: Request, notice_id: int, query_db: Session = Depends(get_db)): + try: + detail_notice_result = detail_notice_services(query_db, notice_id) + logger.info(f'获取notice_id为{notice_id}的信息成功') + return response_200(data=detail_notice_result, message='获取成功') + except Exception as e: + logger.exception(e) + return response_500(data="", message="接口异常") diff --git a/dash-fastapi-backend/module_admin/controller/user_controller.py b/dash-fastapi-backend/module_admin/controller/user_controller.py index 70820fb..f51a8b6 100644 --- a/dash-fastapi-backend/module_admin/controller/user_controller.py +++ b/dash-fastapi-backend/module_admin/controller/user_controller.py @@ -112,6 +112,7 @@ async def query_detail_system_user(request: Request, user_id: int, query_db: Ses @userController.patch("/user/profile/changeAvatar", response_model=CrudUserResponse, dependencies=[Depends(CheckUserInterfaceAuth('common'))]) +@log_decorator(title='个人信息', business_type=2) async def change_system_user_profile_avatar(request: Request, edit_user: AddUserModel, token: Optional[str] = Header(...), query_db: Session = Depends(get_db)): try: current_user = await get_current_user(request, token, query_db) diff --git a/dash-fastapi-backend/module_admin/dao/notice_dao.py b/dash-fastapi-backend/module_admin/dao/notice_dao.py new file mode 100644 index 0000000..3b2bf56 --- /dev/null +++ b/dash-fastapi-backend/module_admin/dao/notice_dao.py @@ -0,0 +1,83 @@ +from sqlalchemy.orm import Session +from module_admin.entity.do.notice_do import SysNotice +from module_admin.entity.vo.notice_vo import NoticeModel, NoticePageObject, NoticePageObjectResponse, CrudNoticeResponse +from utils.time_format_util import list_format_datetime, object_format_datetime +from datetime import datetime, time + + +def get_notice_detail_by_id(db: Session, notice_id: int): + notice_info = db.query(SysNotice) \ + .filter(SysNotice.notice_id == notice_id) \ + .first() + + return object_format_datetime(notice_info) + + +def get_notice_list(db: Session, query_object: NoticePageObject): + """ + 根据查询参数获取通知公告列表信息 + :param db: orm对象 + :param query_object: 查询参数对象 + :return: 通知公告列表信息对象 + """ + notice_list = db.query(SysNotice) \ + .filter(SysNotice.notice_title.like(f'%{query_object.notice_title}%') if query_object.notice_title else True, + SysNotice.update_by.like(f'%{query_object.update_by}%') if query_object.update_by else True, + SysNotice.notice_type == query_object.notice_type if query_object.notice_type else True, + SysNotice.create_time.between( + datetime.combine(datetime.strptime(query_object.create_time_start, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.create_time_end, '%Y-%m-%d'), time(23, 59, 59))) + if query_object.create_time_start and query_object.create_time_end else True + ) \ + .distinct().all() + + return list_format_datetime(notice_list) + + +def add_notice_dao(db: Session, notice: NoticeModel): + """ + 新增通知公告数据库操作 + :param db: orm对象 + :param notice: 通知公告对象 + :return: 新增校验结果 + """ + db_notice = SysNotice(**notice.dict()) + db.add(db_notice) + db.commit() # 提交保存到数据库中 + db.refresh(db_notice) # 刷新 + result = dict(is_success=True, message='新增成功') + + return CrudNoticeResponse(**result) + + +def edit_notice_dao(db: Session, notice: dict): + """ + 编辑通知公告数据库操作 + :param db: orm对象 + :param notice: 需要更新的通知公告字典 + :return: 编辑校验结果 + """ + is_notice_id = db.query(SysNotice).filter(SysNotice.notice_id == notice.get('notice_id')).all() + if not is_notice_id: + result = dict(is_success=False, message='通知公告不存在') + else: + db.query(SysNotice) \ + .filter(SysNotice.notice_id == notice.get('notice_id')) \ + .update(notice) + db.commit() # 提交保存到数据库中 + result = dict(is_success=True, message='更新成功') + + return CrudNoticeResponse(**result) + + +def delete_notice_dao(db: Session, notice: NoticeModel): + """ + 删除通知公告数据库操作 + :param db: orm对象 + :param notice: 通知公告对象 + :return: + """ + db.query(SysNotice) \ + .filter(SysNotice.notice_id == notice.notice_id) \ + .delete() + db.commit() # 提交保存到数据库中 diff --git a/dash-fastapi-backend/module_admin/entity/do/notice_do.py b/dash-fastapi-backend/module_admin/entity/do/notice_do.py new file mode 100644 index 0000000..94e1f22 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/do/notice_do.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, String, DateTime, LargeBinary +from config.database import Base, engine +from datetime import datetime + + +class SysNotice(Base): + """ + 通知公告表 + """ + __tablename__ = 'sys_notice' + + notice_id = Column(Integer, primary_key=True, autoincrement=True, comment='公告ID') + notice_title = Column(String(50, collation='utf8_general_ci'), nullable=False, comment='公告标题') + notice_type = Column(String(1, collation='utf8_general_ci'), nullable=False, comment='公告类型(1通知 2公告)') + notice_content = Column(LargeBinary, comment='公告内容') + status = Column(String(1, collation='utf8_general_ci'), default='0', comment='公告状态(0正常 1关闭)') + create_by = Column(String(64, collation='utf8_general_ci'), default='', comment='创建者') + create_time = Column(DateTime, comment='创建时间', default=datetime.now()) + update_by = Column(String(64, collation='utf8_general_ci'), default='', comment='更新者') + update_time = Column(DateTime, comment='更新时间', default=datetime.now()) + remark = Column(String(255, collation='utf8_general_ci'), comment='备注') + + +Base.metadata.create_all(bind=engine) diff --git a/dash-fastapi-backend/module_admin/entity/vo/notice_vo.py b/dash-fastapi-backend/module_admin/entity/vo/notice_vo.py new file mode 100644 index 0000000..57d6753 --- /dev/null +++ b/dash-fastapi-backend/module_admin/entity/vo/notice_vo.py @@ -0,0 +1,57 @@ +from pydantic import BaseModel +from typing import Union, Optional, List + + +class NoticeModel(BaseModel): + """ + 通知公告表对应pydantic模型 + """ + notice_id: Optional[int] + notice_title: Optional[str] + notice_type: Optional[str] + notice_content: Optional[bytes] + status: Optional[str] + create_by: Optional[str] + create_time: Optional[str] + update_by: Optional[str] + update_time: Optional[str] + remark: Optional[str] + + class Config: + orm_mode = True + + +class NoticePageObject(NoticeModel): + """ + 通知公告管理分页查询模型 + """ + create_time_start: Optional[str] + create_time_end: Optional[str] + page_num: int + page_size: int + + +class NoticePageObjectResponse(BaseModel): + """ + 通知公告管理列表分页查询返回模型 + """ + rows: List[Union[NoticeModel, None]] = [] + page_num: int + page_size: int + total: int + has_next: bool + + +class CrudNoticeResponse(BaseModel): + """ + 操作通知公告响应模型 + """ + is_success: bool + message: str + + +class DeleteNoticeModel(BaseModel): + """ + 删除通知公告模型 + """ + notice_ids: str diff --git a/dash-fastapi-backend/module_admin/service/notice_service.py b/dash-fastapi-backend/module_admin/service/notice_service.py new file mode 100644 index 0000000..ee03af6 --- /dev/null +++ b/dash-fastapi-backend/module_admin/service/notice_service.py @@ -0,0 +1,69 @@ +from module_admin.entity.vo.notice_vo import * +from module_admin.dao.notice_dao import * + + +def get_notice_list_services(result_db: Session, query_object: NoticePageObject): + """ + 获取通知公告列表信息service + :param result_db: orm对象 + :param query_object: 查询参数对象 + :return: 通知公告列表信息对象 + """ + notice_list_result = get_notice_list(result_db, query_object) + + return notice_list_result + + +def add_notice_services(result_db: Session, page_object: NoticeModel): + """ + 新增通知公告信息service + :param result_db: orm对象 + :param page_object: 新增通知公告对象 + :return: 新增通知公告校验结果 + """ + add_notice_result = add_notice_dao(result_db, page_object) + + return add_notice_result + + +def edit_notice_services(result_db: Session, page_object: NoticeModel): + """ + 编辑通知公告信息service + :param result_db: orm对象 + :param page_object: 编辑通知公告对象 + :return: 编辑通知公告校验结果 + """ + edit_notice = page_object.dict(exclude_unset=True) + edit_notice_result = edit_notice_dao(result_db, edit_notice) + + return edit_notice_result + + +def delete_notice_services(result_db: Session, page_object: DeleteNoticeModel): + """ + 删除通知公告信息service + :param result_db: orm对象 + :param page_object: 删除通知公告对象 + :return: 删除通知公告校验结果 + """ + if page_object.notice_ids.split(','): + notice_id_list = page_object.notice_ids.split(',') + for notice_id in notice_id_list: + notice_id_dict = dict(notice_id=notice_id) + delete_notice_dao(result_db, NoticeModel(**notice_id_dict)) + result = dict(is_success=True, message='删除成功') + else: + result = dict(is_success=False, message='传入岗位id为空') + return CrudNoticeResponse(**result) + + +def detail_notice_services(result_db: Session, notice_id: int): + """ + 获取通知公告详细信息service + :param result_db: orm对象 + :param notice_id: 通知公告id + :return: 通知公告id对应的信息 + """ + notice = get_notice_detail_by_id(result_db, notice_id=notice_id) + + return notice diff --git a/dash-fastapi-frontend/api/notice.py b/dash-fastapi-frontend/api/notice.py new file mode 100644 index 0000000..af972ed --- /dev/null +++ b/dash-fastapi-frontend/api/notice.py @@ -0,0 +1,26 @@ +from utils.request import api_request + + +def get_notice_list_api(page_obj: dict): + + return api_request(method='post', url='/system/notice/get', is_headers=True, json=page_obj) + + +def add_notice_api(page_obj: dict): + + return api_request(method='post', url='/system/notice/add', is_headers=True, json=page_obj) + + +def edit_notice_api(page_obj: dict): + + return api_request(method='patch', url='/system/notice/edit', is_headers=True, json=page_obj) + + +def delete_notice_api(page_obj: dict): + + return api_request(method='post', url='/system/notice/delete', is_headers=True, json=page_obj) + + +def get_notice_detail_api(notice_id: int): + + return api_request(method='get', url=f'/system/notice/{notice_id}', is_headers=True) diff --git a/dash-fastapi-frontend/assets/wangeditor/css/style.css b/dash-fastapi-frontend/assets/wangeditor/css/style.css new file mode 100644 index 0000000..6d13c91 --- /dev/null +++ b/dash-fastapi-frontend/assets/wangeditor/css/style.css @@ -0,0 +1,27 @@ +:root, +:host { + --w-e-textarea-bg-color: #fff; + --w-e-textarea-color: #333; + --w-e-textarea-border-color: #ccc; + --w-e-textarea-slight-border-color: #e8e8e8; + --w-e-textarea-slight-color: #d4d4d4; + --w-e-textarea-slight-bg-color: #f5f2f0; + --w-e-textarea-selected-border-color: #B4D5FF; + --w-e-textarea-handler-bg-color: #4290f7; + --w-e-toolbar-color: #595959; + --w-e-toolbar-bg-color: #fff; + --w-e-toolbar-active-color: #333; + --w-e-toolbar-active-bg-color: #f1f1f1; + --w-e-toolbar-disabled-color: #999; + --w-e-toolbar-border-color: #e8e8e8; + --w-e-modal-button-bg-color: #fafafa; + --w-e-modal-button-border-color: #d9d9d9; +} + +.w-e-text-container *,.w-e-toolbar *{box-sizing:border-box;margin:0;outline:none;padding:0}.w-e-text-container blockquote,.w-e-text-container li,.w-e-text-container p,.w-e-text-container td,.w-e-text-container th,.w-e-toolbar *{line-height:1.5}.w-e-text-container{background-color:var(--w-e-textarea-bg-color);color:var(--w-e-textarea-color);height:100%;position:relative}.w-e-text-container .w-e-scroll{-webkit-overflow-scrolling:touch;height:100%}.w-e-text-container [data-slate-editor]{word-wrap:break-word;border-top:1px solid transparent;min-height:100%;outline:0;padding:0 10px;white-space:pre-wrap}.w-e-text-container [data-slate-editor] p{margin:15px 0}.w-e-text-container [data-slate-editor] h1,.w-e-text-container [data-slate-editor] h2,.w-e-text-container [data-slate-editor] h3,.w-e-text-container [data-slate-editor] h4,.w-e-text-container [data-slate-editor] h5{margin:20px 0}.w-e-text-container [data-slate-editor] img{cursor:default;display:inline!important;max-width:100%;min-height:20px;min-width:20px}.w-e-text-container [data-slate-editor] span{text-indent:0}.w-e-text-container [data-slate-editor] [data-selected=true]{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-placeholder{font-style:italic;left:10px;top:17px;width:90%}.w-e-max-length-info,.w-e-text-placeholder{color:var(--w-e-textarea-slight-color);pointer-events:none;position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none}.w-e-max-length-info{bottom:.5em;right:1em}.w-e-bar{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-color);font-size:14px;padding:0 5px}.w-e-bar svg{fill:var(--w-e-toolbar-color);height:14px;width:14px}.w-e-bar-show{display:flex}.w-e-bar-hidden{display:none}.w-e-hover-bar{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 5px #0000001f;position:absolute}.w-e-toolbar{flex-wrap:wrap;position:relative}.w-e-bar-divider{background-color:var(--w-e-toolbar-border-color);display:inline-flex;height:40px;margin:0 5px;width:1px}.w-e-bar-item{display:flex;height:40px;padding:4px;position:relative;text-align:center}.w-e-bar-item,.w-e-bar-item button{align-items:center;justify-content:center}.w-e-bar-item button{background:transparent;border:none;color:var(--w-e-toolbar-color);cursor:pointer;display:inline-flex;height:32px;overflow:hidden;padding:0 8px;white-space:nowrap}.w-e-bar-item button:hover{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item button .title{margin-left:5px}.w-e-bar-item .active{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item .disabled{color:var(--w-e-toolbar-disabled-color);cursor:not-allowed}.w-e-bar-item .disabled svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-menu-tooltip-v5:before{background-color:var(--w-e-toolbar-active-color);border-radius:5px;color:var(--w-e-toolbar-bg-color);content:attr(data-tooltip);font-size:.75em;opacity:0;padding:5px 10px;position:absolute;text-align:center;top:40px;transition:opacity .6s;visibility:hidden;white-space:pre;z-index:1}.w-e-menu-tooltip-v5:after{border:5px solid transparent;border-bottom:5px solid var(--w-e-toolbar-active-color);content:"";opacity:0;position:absolute;top:30px;transition:opacity .6s;visibility:hidden}.w-e-menu-tooltip-v5:hover:after,.w-e-menu-tooltip-v5:hover:before{opacity:1;visibility:visible}.w-e-menu-tooltip-v5.tooltip-right:before{left:100%;top:10px}.w-e-menu-tooltip-v5.tooltip-right:after{border-bottom-color:transparent;border-left-color:transparent;border-right-color:var(--w-e-toolbar-active-color);border-top-color:transparent;left:100%;margin-left:-10px;top:16px}.w-e-bar-item-group .w-e-bar-item-menus-container{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;display:none;left:0;margin-top:40px;position:absolute;top:0;z-index:1}.w-e-bar-item-group:hover .w-e-bar-item-menus-container{display:block}.w-e-select-list{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;left:0;margin-top:40px;max-height:350px;min-width:100px;overflow-y:auto;position:absolute;top:0;z-index:1}.w-e-select-list ul{line-height:1;list-style:none}.w-e-select-list ul .selected{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li{cursor:pointer;padding:7px 0 7px 25px;position:relative;text-align:left;white-space:nowrap}.w-e-select-list ul li:hover{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li svg{left:0;margin-left:5px;margin-top:-7px;position:absolute;top:50%}.w-e-bar-bottom .w-e-select-list{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-drop-panel{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;margin-top:40px;min-width:200px;padding:10px;position:absolute;top:0;z-index:1}.w-e-bar-bottom .w-e-drop-panel{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-modal{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;color:var(--w-e-toolbar-color);font-size:14px;min-height:40px;min-width:100px;padding:20px 15px 0;position:absolute;text-align:left;z-index:1}.w-e-modal .btn-close{cursor:pointer;line-height:1;padding:5px;position:absolute;right:8px;top:7px}.w-e-modal .btn-close svg{fill:var(--w-e-toolbar-color);height:10px;width:10px}.w-e-modal .babel-container{display:block;margin-bottom:15px}.w-e-modal .babel-container span{display:block;margin-bottom:10px}.w-e-modal .button-container{margin-bottom:15px}.w-e-modal button{background-color:var(--w-e-modal-button-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);cursor:pointer;font-weight:400;height:32px;padding:4.5px 15px;text-align:center;touch-action:manipulation;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.w-e-modal input[type=number],.w-e-modal input[type=text],.w-e-modal textarea{font-feature-settings:"tnum";background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color);font-variant:tabular-nums;padding:4.5px 11px;transition:all .3s;width:100%}.w-e-modal textarea{min-height:60px}body .w-e-modal,body .w-e-modal *{box-sizing:border-box}.w-e-progress-bar{background-color:var(--w-e-textarea-handler-bg-color);height:1px;position:absolute;transition:width .3s;width:0}.w-e-full-screen-container{bottom:0!important;display:flex!important;flex-direction:column!important;height:100%!important;left:0!important;margin:0!important;padding:0!important;position:fixed;right:0!important;top:0!important;width:100%!important}.w-e-full-screen-container [data-w-e-textarea=true]{flex:1!important} +.w-e-text-container [data-slate-editor] code{background-color:var(--w-e-textarea-slight-bg-color);border-radius:3px;font-family:monospace;padding:3px}.w-e-panel-content-color{list-style:none;text-align:left;width:230px}.w-e-panel-content-color li{border:1px solid var(--w-e-toolbar-bg-color);border-radius:3px 3px;cursor:pointer;display:inline-block;padding:2px}.w-e-panel-content-color li:hover{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color li .color-block{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px 3px;height:17px;width:17px}.w-e-panel-content-color .active{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color .clear{line-height:1.5;margin-bottom:5px;width:100%}.w-e-panel-content-color .clear svg{height:16px;margin-bottom:-4px;width:16px}.w-e-text-container [data-slate-editor] blockquote{background-color:var(--w-e-textarea-slight-bg-color);border-left:8px solid var(--w-e-textarea-selected-border-color);display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px}.w-e-panel-content-emotion{font-size:20px;list-style:none;text-align:left;width:300px}.w-e-panel-content-emotion li{border-radius:3px 3px;cursor:pointer;display:inline-block;padding:0 5px}.w-e-panel-content-emotion li:hover{background-color:var(--w-e-textarea-slight-bg-color)}.w-e-textarea-divider{border-radius:3px;margin:20px auto;padding:20px}.w-e-textarea-divider hr{background-color:var(--w-e-textarea-border-color);border:0;display:block;height:1px}.w-e-text-container [data-slate-editor] pre>code{background-color:var(--w-e-textarea-slight-bg-color);border:1px solid var(--w-e-textarea-slight-border-color);border-radius:4px 4px;display:block;font-size:14px;padding:10px;text-indent:0}.w-e-text-container [data-slate-editor] .w-e-image-container{display:inline-block;margin:0 3px}.w-e-text-container [data-slate-editor] .w-e-image-container:hover{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-container [data-slate-editor] .w-e-selected-image-container{overflow:hidden;position:relative}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .w-e-image-dragger{background-color:var(--w-e-textarea-handler-bg-color);height:7px;position:absolute;width:7px}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-top{cursor:nwse-resize;left:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-top{cursor:nesw-resize;right:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-bottom{bottom:0;cursor:nesw-resize;left:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-bottom{bottom:0;cursor:nwse-resize;right:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container:hover{box-shadow:none}.w-e-text-container [contenteditable=false] .w-e-image-container:hover{box-shadow:none} + +.w-e-text-container [data-slate-editor] .table-container{border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin-top:10px;overflow-x:auto;padding:10px;width:100%}.w-e-text-container [data-slate-editor] table{border-collapse:collapse}.w-e-text-container [data-slate-editor] table td,.w-e-text-container [data-slate-editor] table th{border:1px solid var(--w-e-textarea-border-color);line-height:1.5;min-width:30px;padding:3px 5px;text-align:left}.w-e-text-container [data-slate-editor] table th{background-color:var(--w-e-textarea-slight-bg-color);font-weight:700;text-align:center}.w-e-panel-content-table{background-color:var(--w-e-toolbar-bg-color)}.w-e-panel-content-table table{border-collapse:collapse}.w-e-panel-content-table td{border:1px solid var(--w-e-toolbar-border-color);cursor:pointer;height:15px;padding:3px 5px;width:20px}.w-e-panel-content-table td.active{background-color:var(--w-e-toolbar-active-bg-color)} +.w-e-textarea-video-container{background-image:linear-gradient(45deg,#eee 25%,transparent 0,transparent 75%,#eee 0,#eee),linear-gradient(45deg,#eee 25%,#fff 0,#fff 75%,#eee 0,#eee);background-position:0 0,10px 10px;background-size:20px 20px;border:1px dashed var(--w-e-textarea-border-color);border-radius:5px;margin:10px auto 0;padding:10px 0;text-align:center} + +.w-e-text-container [data-slate-editor] pre>code{word-wrap:normal;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;-webkit-hyphens:none;hyphens:none;line-height:1.5;margin:.5em 0;overflow:auto;padding:1em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 1px #fff;white-space:pre;word-break:normal;word-spacing:normal}.w-e-text-container [data-slate-editor] pre>code .token.cdata,.w-e-text-container [data-slate-editor] pre>code .token.comment,.w-e-text-container [data-slate-editor] pre>code .token.doctype,.w-e-text-container [data-slate-editor] pre>code .token.prolog{color:#708090}.w-e-text-container [data-slate-editor] pre>code .token.punctuation{color:#999}.w-e-text-container [data-slate-editor] pre>code .token.namespace{opacity:.7}.w-e-text-container [data-slate-editor] pre>code .token.boolean,.w-e-text-container [data-slate-editor] pre>code .token.constant,.w-e-text-container [data-slate-editor] pre>code .token.deleted,.w-e-text-container [data-slate-editor] pre>code .token.number,.w-e-text-container [data-slate-editor] pre>code .token.property,.w-e-text-container [data-slate-editor] pre>code .token.symbol,.w-e-text-container [data-slate-editor] pre>code .token.tag{color:#905}.w-e-text-container [data-slate-editor] pre>code .token.attr-name,.w-e-text-container [data-slate-editor] pre>code .token.builtin,.w-e-text-container [data-slate-editor] pre>code .token.char,.w-e-text-container [data-slate-editor] pre>code .token.inserted,.w-e-text-container [data-slate-editor] pre>code .token.selector,.w-e-text-container [data-slate-editor] pre>code .token.string{color:#690}.w-e-text-container [data-slate-editor] pre>code .language-css .token.string,.w-e-text-container [data-slate-editor] pre>code .style .token.string,.w-e-text-container [data-slate-editor] pre>code .token.entity,.w-e-text-container [data-slate-editor] pre>code .token.operator,.w-e-text-container [data-slate-editor] pre>code .token.url{color:#9a6e3a}.w-e-text-container [data-slate-editor] pre>code .token.atrule,.w-e-text-container [data-slate-editor] pre>code .token.attr-value,.w-e-text-container [data-slate-editor] pre>code .token.keyword{color:#07a}.w-e-text-container [data-slate-editor] pre>code .token.class-name,.w-e-text-container [data-slate-editor] pre>code .token.function{color:#dd4a68}.w-e-text-container [data-slate-editor] pre>code .token.important,.w-e-text-container [data-slate-editor] pre>code .token.regex,.w-e-text-container [data-slate-editor] pre>code .token.variable{color:#e90}.w-e-text-container [data-slate-editor] pre>code .token.bold,.w-e-text-container [data-slate-editor] pre>code .token.important{font-weight:700}.w-e-text-container [data-slate-editor] pre>code .token.italic{font-style:italic}.w-e-text-container [data-slate-editor] pre>code .token.entity{cursor:help} \ No newline at end of file diff --git a/dash-fastapi-frontend/assets/wangeditor/index.js b/dash-fastapi-frontend/assets/wangeditor/index.js new file mode 100644 index 0000000..a2233e4 --- /dev/null +++ b/dash-fastapi-frontend/assets/wangeditor/index.js @@ -0,0 +1,24129 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.wangEditor = {})); +})(this, (function (exports) { 'use strict'; + + /** + * @description browser polyfill + * @author wangfupeng + */ + var _a; + // @ts-nocheck + // 必须是浏览器环境 + if (typeof global === 'undefined') { + // 检查 IE 浏览器 + if ('ActiveXObject' in window) { + var info = '抱歉,wangEditor V5+ 版本开始,不在支持 IE 浏览器'; + info += '\n Sorry, wangEditor V5+ versions do not support IE browser.'; + console.error(info); + } + globalThisPolyfill(); + AggregateErrorPolyfill(); + } + else if (global && ((_a = global.navigator) === null || _a === void 0 ? void 0 : _a.userAgent.match('QQBrowser'))) { + // 兼容 QQ 浏览器 AggregateError 报错 + globalThisPolyfill(); + AggregateErrorPolyfill(); + } + function globalThisPolyfill() { + // 部分浏览器不支持 globalThis + if (typeof globalThis === 'undefined') { + // @ts-ignore + window.globalThis = window; + } + } + function AggregateErrorPolyfill() { + if (typeof AggregateError === 'undefined') { + window.AggregateError = function (errors, msg) { + var err = new Error(msg); + err.errors = errors; + return err; + }; + } + } + + /** + * @description node polyfill + * @author wangfupeng + */ + // @ts-nocheck + // 必须是 node 环境 + if (typeof global === 'object') { + // 用于 nodejs ,避免报错 + var globalProperty = Object.getOwnPropertyDescriptor(global, 'window'); + // global.window 为空则直接写入 + // 部分框架下已经定义了global.window且是不可写属性 + if (!global.window || globalProperty.set) { + global.window = global; + global.requestAnimationFrame = function () { }; + global.navigator = { + userAgent: '', + }; + global.location = { + hostname: '0.0.0.0', + port: 0, + protocol: 'http:', + }; + global.btoa = function () { }; + global.crypto = { + getRandomValues: function (buffer) { + return nodeCrypto.randomFillSync(buffer); + }, + }; + } + if (global.document != null) { + // SSR 环境下可能会报错 (issue 4409) + if (global.document.getElementsByTagName == null) { + global.document.getElementsByTagName = function () { return []; }; + } + } + } + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function getDefaultExportFromCjs (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + function createCommonjsModule$1(fn) { + var module = { exports: {} }; + return fn(module, module.exports), module.exports; + } + + /*! + * is-plain-object + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ + + function isObject$4(o) { + return Object.prototype.toString.call(o) === '[object Object]'; + } + + function isPlainObject$2(o) { + var ctor,prot; + + if (isObject$4(o) === false) return false; + + // If has modified constructor + ctor = o.constructor; + if (ctor === undefined) return true; + + // If has modified prototype + prot = ctor.prototype; + if (isObject$4(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (prot.hasOwnProperty('isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; + } + + var isPlainObject_2 = isPlainObject$2; + + var isPlainObject_1 = /*#__PURE__*/Object.defineProperty({ + isPlainObject: isPlainObject_2 + }, '__esModule', {value: true}); + + var _ref; + + // Should be no imports here! + // Some things that should be evaluated before all else... + // We only want to know if non-polyfilled symbols are available + var hasSymbol = typeof Symbol !== "undefined" && typeof + /*#__PURE__*/ + Symbol("x") === "symbol"; + var hasMap = typeof Map !== "undefined"; + var hasSet = typeof Set !== "undefined"; + var hasProxies = typeof Proxy !== "undefined" && typeof Proxy.revocable !== "undefined" && typeof Reflect !== "undefined"; + /** + * The sentinel value returned by producers to replace the draft with undefined. + */ + + var NOTHING = hasSymbol ? + /*#__PURE__*/ + Symbol.for("immer-nothing") : (_ref = {}, _ref["immer-nothing"] = true, _ref); + /** + * To let Immer treat your class instances as plain immutable objects + * (albeit with a custom prototype), you must define either an instance property + * or a static property on each of your custom classes. + * + * Otherwise, your class instance will never be drafted, which means it won't be + * safe to mutate in a produce callback. + */ + + var DRAFTABLE = hasSymbol ? + /*#__PURE__*/ + Symbol.for("immer-draftable") : "__$immer_draftable"; + var DRAFT_STATE = hasSymbol ? + /*#__PURE__*/ + Symbol.for("immer-state") : "__$immer_state"; // Even a polyfilled Symbol might provide Symbol.iterator + + var iteratorSymbol$1 = typeof Symbol != "undefined" && Symbol.iterator || "@@iterator"; + + var errors = { + 0: "Illegal state", + 1: "Immer drafts cannot have computed properties", + 2: "This object has been frozen and should not be mutated", + 3: function _(data) { + return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data; + }, + 4: "An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.", + 5: "Immer forbids circular references", + 6: "The first or second argument to `produce` must be a function", + 7: "The third argument to `produce` must be a function or undefined", + 8: "First argument to `createDraft` must be a plain object, an array, or an immerable object", + 9: "First argument to `finishDraft` must be a draft returned by `createDraft`", + 10: "The given draft is already finalized", + 11: "Object.defineProperty() cannot be used on an Immer draft", + 12: "Object.setPrototypeOf() cannot be used on an Immer draft", + 13: "Immer only supports deleting array indices", + 14: "Immer only supports setting array indices and the 'length' property", + 15: function _(path) { + return "Cannot apply patch, path doesn't resolve: " + path; + }, + 16: 'Sets cannot have "replace" patches.', + 17: function _(op) { + return "Unsupported patch operation: " + op; + }, + 18: function _(plugin) { + return "The plugin for '" + plugin + "' has not been loaded into Immer. To enable the plugin, import and call `enable" + plugin + "()` when initializing your application."; + }, + 20: "Cannot use proxies if Proxy, Proxy.revocable or Reflect are not available", + 21: function _(thing) { + return "produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '" + thing + "'"; + }, + 22: function _(thing) { + return "'current' expects a draft, got: " + thing; + }, + 23: function _(thing) { + return "'original' expects a draft, got: " + thing; + }, + 24: "Patching reserved attributes like __proto__, prototype and constructor is not allowed" + }; + function die(error) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + { + var e = errors[error]; + var msg = !e ? "unknown error nr: " + error : typeof e === "function" ? e.apply(null, args) : e; + throw new Error("[Immer] " + msg); + } + } + + /** Returns true if the given value is an Immer draft */ + + + + function isDraft(value) { + return !!value && !!value[DRAFT_STATE]; + } + /** Returns true if the given value can be drafted by Immer */ + + + + function isDraftable(value) { + if (!value) return false; + return isPlainObject$1(value) || Array.isArray(value) || !!value[DRAFTABLE] || !!value.constructor[DRAFTABLE] || isMap(value) || isSet(value); + } + var objectCtorString = + /*#__PURE__*/ + Object.prototype.constructor.toString(); + + + function isPlainObject$1(value) { + if (!value || typeof value !== "object") return false; + var proto = Object.getPrototypeOf(value); + + if (proto === null) { + return true; + } + + var Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor; + if (Ctor === Object) return true; + return typeof Ctor == "function" && Function.toString.call(Ctor) === objectCtorString; + } + function original(value) { + if (!isDraft(value)) die(23, value); + return value[DRAFT_STATE].base_; + } + + + var ownKeys$a = typeof Reflect !== "undefined" && Reflect.ownKeys ? Reflect.ownKeys : typeof Object.getOwnPropertySymbols !== "undefined" ? function (obj) { + return Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj)); + } : + /* istanbul ignore next */ + Object.getOwnPropertyNames; + var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function getOwnPropertyDescriptors(target) { + // Polyfill needed for Hermes and IE, see https://github.com/facebook/hermes/issues/274 + var res = {}; + ownKeys$a(target).forEach(function (key) { + res[key] = Object.getOwnPropertyDescriptor(target, key); + }); + return res; + }; + function each$1(obj, iter, enumerableOnly) { + if (enumerableOnly === void 0) { + enumerableOnly = false; + } + + if (getArchtype(obj) === 0 + /* Object */ + ) { + (enumerableOnly ? Object.keys : ownKeys$a)(obj).forEach(function (key) { + if (!enumerableOnly || typeof key !== "symbol") iter(key, obj[key], obj); + }); + } else { + obj.forEach(function (entry, index) { + return iter(index, entry, obj); + }); + } + } + + + function getArchtype(thing) { + /* istanbul ignore next */ + var state = thing[DRAFT_STATE]; + return state ? state.type_ > 3 ? state.type_ - 4 // cause Object and Array map back from 4 and 5 + : state.type_ // others are the same + : Array.isArray(thing) ? 1 + /* Array */ + : isMap(thing) ? 2 + /* Map */ + : isSet(thing) ? 3 + /* Set */ + : 0 + /* Object */ + ; + } + + + function has(thing, prop) { + return getArchtype(thing) === 2 + /* Map */ + ? thing.has(prop) : Object.prototype.hasOwnProperty.call(thing, prop); + } + + + function get(thing, prop) { + // @ts-ignore + return getArchtype(thing) === 2 + /* Map */ + ? thing.get(prop) : thing[prop]; + } + + + function set(thing, propOrOldValue, value) { + var t = getArchtype(thing); + if (t === 2 + /* Map */ + ) thing.set(propOrOldValue, value);else if (t === 3 + /* Set */ + ) { + thing.delete(propOrOldValue); + thing.add(value); + } else thing[propOrOldValue] = value; + } + + + function is$1(x, y) { + // From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js + if (x === y) { + return x !== 0 || 1 / x === 1 / y; + } else { + return x !== x && y !== y; + } + } + + + function isMap(target) { + return hasMap && target instanceof Map; + } + + + function isSet(target) { + return hasSet && target instanceof Set; + } + + + function latest(state) { + return state.copy_ || state.base_; + } + + + function shallowCopy(base) { + if (Array.isArray(base)) return Array.prototype.slice.call(base); + var descriptors = getOwnPropertyDescriptors(base); + delete descriptors[DRAFT_STATE]; + var keys = ownKeys$a(descriptors); + + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + var desc = descriptors[key]; + + if (desc.writable === false) { + desc.writable = true; + desc.configurable = true; + } // like object.assign, we will read any _own_, get/set accessors. This helps in dealing + // with libraries that trap values, like mobx or vue + // unlike object.assign, non-enumerables will be copied as well + + + if (desc.get || desc.set) descriptors[key] = { + configurable: true, + writable: true, + enumerable: desc.enumerable, + value: base[key] + }; + } + + return Object.create(Object.getPrototypeOf(base), descriptors); + } + function freeze(obj, deep) { + if (deep === void 0) { + deep = false; + } + + if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj; + + if (getArchtype(obj) > 1 + /* Map or Set */ + ) { + obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections; + } + + Object.freeze(obj); + if (deep) each$1(obj, function (key, value) { + return freeze(value, true); + }, true); + return obj; + } + + function dontMutateFrozenCollections() { + die(2); + } + + function isFrozen(obj) { + if (obj == null || typeof obj !== "object") return true; // See #600, IE dies on non-objects in Object.isFrozen + + return Object.isFrozen(obj); + } + + /** Plugin utilities */ + + var plugins = {}; + function getPlugin(pluginKey) { + var plugin = plugins[pluginKey]; + + if (!plugin) { + die(18, pluginKey); + } // @ts-ignore + + + return plugin; + } + function loadPlugin(pluginKey, implementation) { + if (!plugins[pluginKey]) plugins[pluginKey] = implementation; + } + + var currentScope; + function getCurrentScope() { + if ( !currentScope) die(0); + return currentScope; + } + + function createScope(parent_, immer_) { + return { + drafts_: [], + parent_: parent_, + immer_: immer_, + // Whenever the modified draft contains a draft from another scope, we + // need to prevent auto-freezing so the unowned draft can be finalized. + canAutoFreeze_: true, + unfinalizedDrafts_: 0 + }; + } + + function usePatchesInScope(scope, patchListener) { + if (patchListener) { + getPlugin("Patches"); // assert we have the plugin + + scope.patches_ = []; + scope.inversePatches_ = []; + scope.patchListener_ = patchListener; + } + } + function revokeScope(scope) { + leaveScope(scope); + scope.drafts_.forEach(revokeDraft); // @ts-ignore + + scope.drafts_ = null; + } + function leaveScope(scope) { + if (scope === currentScope) { + currentScope = scope.parent_; + } + } + function enterScope(immer) { + return currentScope = createScope(currentScope, immer); + } + + function revokeDraft(draft) { + var state = draft[DRAFT_STATE]; + if (state.type_ === 0 + /* ProxyObject */ + || state.type_ === 1 + /* ProxyArray */ + ) state.revoke_();else state.revoked_ = true; + } + + function processResult(result, scope) { + scope.unfinalizedDrafts_ = scope.drafts_.length; + var baseDraft = scope.drafts_[0]; + var isReplaced = result !== undefined && result !== baseDraft; + if (!scope.immer_.useProxies_) getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced); + + if (isReplaced) { + if (baseDraft[DRAFT_STATE].modified_) { + revokeScope(scope); + die(4); + } + + if (isDraftable(result)) { + // Finalize the result in case it contains (or is) a subset of the draft. + result = finalize(scope, result); + if (!scope.parent_) maybeFreeze(scope, result); + } + + if (scope.patches_) { + getPlugin("Patches").generateReplacementPatches_(baseDraft[DRAFT_STATE], result, scope.patches_, scope.inversePatches_); + } + } else { + // Finalize the base draft. + result = finalize(scope, baseDraft, []); + } + + revokeScope(scope); + + if (scope.patches_) { + scope.patchListener_(scope.patches_, scope.inversePatches_); + } + + return result !== NOTHING ? result : undefined; + } + + function finalize(rootScope, value, path) { + // Don't recurse in tho recursive data structures + if (isFrozen(value)) return value; + var state = value[DRAFT_STATE]; // A plain object, might need freezing, might contain drafts + + if (!state) { + each$1(value, function (key, childValue) { + return finalizeProperty(rootScope, state, value, key, childValue, path); + }, true // See #590, don't recurse into non-enumerable of non drafted objects + ); + return value; + } // Never finalize drafts owned by another scope. + + + if (state.scope_ !== rootScope) return value; // Unmodified draft, return the (frozen) original + + if (!state.modified_) { + maybeFreeze(rootScope, state.base_, true); + return state.base_; + } // Not finalized yet, let's do that now + + + if (!state.finalized_) { + state.finalized_ = true; + state.scope_.unfinalizedDrafts_--; + var result = // For ES5, create a good copy from the draft first, with added keys and without deleted keys. + state.type_ === 4 + /* ES5Object */ + || state.type_ === 5 + /* ES5Array */ + ? state.copy_ = shallowCopy(state.draft_) : state.copy_; // Finalize all children of the copy + // For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628 + // Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line + // back to each(result, ....) + + each$1(state.type_ === 3 + /* Set */ + ? new Set(result) : result, function (key, childValue) { + return finalizeProperty(rootScope, state, result, key, childValue, path); + }); // everything inside is frozen, we can freeze here + + maybeFreeze(rootScope, result, false); // first time finalizing, let's create those patches + + if (path && rootScope.patches_) { + getPlugin("Patches").generatePatches_(state, path, rootScope.patches_, rootScope.inversePatches_); + } + } + + return state.copy_; + } + + function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath) { + if ( childValue === targetObject) die(5); + + if (isDraft(childValue)) { + var path = rootPath && parentState && parentState.type_ !== 3 + /* Set */ + && // Set objects are atomic since they have no keys. + !has(parentState.assigned_, prop) // Skip deep patches for assigned keys. + ? rootPath.concat(prop) : undefined; // Drafts owned by `scope` are finalized here. + + var res = finalize(rootScope, childValue, path); + set(targetObject, prop, res); // Drafts from another scope must prevented to be frozen + // if we got a draft back from finalize, we're in a nested produce and shouldn't freeze + + if (isDraft(res)) { + rootScope.canAutoFreeze_ = false; + } else return; + } // Search new objects for unfinalized drafts. Frozen objects should never contain drafts. + + + if (isDraftable(childValue) && !isFrozen(childValue)) { + if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) { + // optimization: if an object is not a draft, and we don't have to + // deepfreeze everything, and we are sure that no drafts are left in the remaining object + // cause we saw and finalized all drafts already; we can stop visiting the rest of the tree. + // This benefits especially adding large data tree's without further processing. + // See add-data.js perf test + return; + } + + finalize(rootScope, childValue); // immer deep freezes plain objects, so if there is no parent state, we freeze as well + + if (!parentState || !parentState.scope_.parent_) maybeFreeze(rootScope, childValue); + } + } + + function maybeFreeze(scope, value, deep) { + if (deep === void 0) { + deep = false; + } + + if (scope.immer_.autoFreeze_ && scope.canAutoFreeze_) { + freeze(value, deep); + } + } + + /** + * Returns a new draft of the `base` object. + * + * The second argument is the parent draft-state (used internally). + */ + + function createProxyProxy(base, parent) { + var isArray = Array.isArray(base); + var state = { + type_: isArray ? 1 + /* ProxyArray */ + : 0 + /* ProxyObject */ + , + // Track which produce call this is associated with. + scope_: parent ? parent.scope_ : getCurrentScope(), + // True for both shallow and deep changes. + modified_: false, + // Used during finalization. + finalized_: false, + // Track which properties have been assigned (true) or deleted (false). + assigned_: {}, + // The parent draft state. + parent_: parent, + // The base state. + base_: base, + // The base proxy. + draft_: null, + // The base copy with any updated values. + copy_: null, + // Called by the `produce` function. + revoke_: null, + isManual_: false + }; // the traps must target something, a bit like the 'real' base. + // but also, we need to be able to determine from the target what the relevant state is + // (to avoid creating traps per instance to capture the state in closure, + // and to avoid creating weird hidden properties as well) + // So the trick is to use 'state' as the actual 'target'! (and make sure we intercept everything) + // Note that in the case of an array, we put the state in an array to have better Reflect defaults ootb + + var target = state; + var traps = objectTraps; + + if (isArray) { + target = [state]; + traps = arrayTraps; + } + + var _Proxy$revocable = Proxy.revocable(target, traps), + revoke = _Proxy$revocable.revoke, + proxy = _Proxy$revocable.proxy; + + state.draft_ = proxy; + state.revoke_ = revoke; + return proxy; + } + /** + * Object drafts + */ + + var objectTraps = { + get: function get(state, prop) { + if (prop === DRAFT_STATE) return state; + var source = latest(state); + + if (!has(source, prop)) { + // non-existing or non-own property... + return readPropFromProto(state, source, prop); + } + + var value = source[prop]; + + if (state.finalized_ || !isDraftable(value)) { + return value; + } // Check for existing draft in modified state. + // Assigned values are never drafted. This catches any drafts we created, too. + + + if (value === peek(state.base_, prop)) { + prepareCopy(state); + return state.copy_[prop] = createProxy(state.scope_.immer_, value, state); + } + + return value; + }, + has: function has(state, prop) { + return prop in latest(state); + }, + ownKeys: function ownKeys(state) { + return Reflect.ownKeys(latest(state)); + }, + set: function set(state, prop + /* strictly not, but helps TS */ + , value) { + var desc = getDescriptorFromProto(latest(state), prop); + + if (desc === null || desc === void 0 ? void 0 : desc.set) { + // special case: if this write is captured by a setter, we have + // to trigger it with the correct context + desc.set.call(state.draft_, value); + return true; + } + + if (!state.modified_) { + // the last check is because we need to be able to distinguish setting a non-existing to undefined (which is a change) + // from setting an existing property with value undefined to undefined (which is not a change) + var current = peek(latest(state), prop); // special case, if we assigning the original value to a draft, we can ignore the assignment + + var currentState = current === null || current === void 0 ? void 0 : current[DRAFT_STATE]; + + if (currentState && currentState.base_ === value) { + state.copy_[prop] = value; + state.assigned_[prop] = false; + return true; + } + + if (is$1(value, current) && (value !== undefined || has(state.base_, prop))) return true; + prepareCopy(state); + markChanged(state); + } + + if (state.copy_[prop] === value && // special case: NaN + typeof value !== "number" && ( // special case: handle new props with value 'undefined' + value !== undefined || prop in state.copy_)) return true; // @ts-ignore + + state.copy_[prop] = value; + state.assigned_[prop] = true; + return true; + }, + deleteProperty: function deleteProperty(state, prop) { + // The `undefined` check is a fast path for pre-existing keys. + if (peek(state.base_, prop) !== undefined || prop in state.base_) { + state.assigned_[prop] = false; + prepareCopy(state); + markChanged(state); + } else { + // if an originally not assigned property was deleted + delete state.assigned_[prop]; + } // @ts-ignore + + + if (state.copy_) delete state.copy_[prop]; + return true; + }, + // Note: We never coerce `desc.value` into an Immer draft, because we can't make + // the same guarantee in ES5 mode. + getOwnPropertyDescriptor: function getOwnPropertyDescriptor(state, prop) { + var owner = latest(state); + var desc = Reflect.getOwnPropertyDescriptor(owner, prop); + if (!desc) return desc; + return { + writable: true, + configurable: state.type_ !== 1 + /* ProxyArray */ + || prop !== "length", + enumerable: desc.enumerable, + value: owner[prop] + }; + }, + defineProperty: function defineProperty() { + die(11); + }, + getPrototypeOf: function getPrototypeOf(state) { + return Object.getPrototypeOf(state.base_); + }, + setPrototypeOf: function setPrototypeOf() { + die(12); + } + }; + /** + * Array drafts + */ + + var arrayTraps = {}; + each$1(objectTraps, function (key, fn) { + // @ts-ignore + arrayTraps[key] = function () { + arguments[0] = arguments[0][0]; + return fn.apply(this, arguments); + }; + }); + + arrayTraps.deleteProperty = function (state, prop) { + if ( isNaN(parseInt(prop))) die(13); + return objectTraps.deleteProperty.call(this, state[0], prop); + }; + + arrayTraps.set = function (state, prop, value) { + if ( prop !== "length" && isNaN(parseInt(prop))) die(14); + return objectTraps.set.call(this, state[0], prop, value, state[0]); + }; // Access a property without creating an Immer draft. + + + function peek(draft, prop) { + var state = draft[DRAFT_STATE]; + var source = state ? latest(state) : draft; + return source[prop]; + } + + function readPropFromProto(state, source, prop) { + var _desc$get; + + var desc = getDescriptorFromProto(source, prop); + return desc ? "value" in desc ? desc.value : // This is a very special case, if the prop is a getter defined by the + // prototype, we should invoke it with the draft as context! + (_desc$get = desc.get) === null || _desc$get === void 0 ? void 0 : _desc$get.call(state.draft_) : undefined; + } + + function getDescriptorFromProto(source, prop) { + // 'in' checks proto! + if (!(prop in source)) return undefined; + var proto = Object.getPrototypeOf(source); + + while (proto) { + var desc = Object.getOwnPropertyDescriptor(proto, prop); + if (desc) return desc; + proto = Object.getPrototypeOf(proto); + } + + return undefined; + } + + function markChanged(state) { + if (!state.modified_) { + state.modified_ = true; + + if (state.parent_) { + markChanged(state.parent_); + } + } + } + function prepareCopy(state) { + if (!state.copy_) { + state.copy_ = shallowCopy(state.base_); + } + } + + var Immer = + /*#__PURE__*/ + function () { + function Immer(config) { + var _this = this; + + this.useProxies_ = hasProxies; + this.autoFreeze_ = true; + /** + * The `produce` function takes a value and a "recipe function" (whose + * return value often depends on the base state). The recipe function is + * free to mutate its first argument however it wants. All mutations are + * only ever applied to a __copy__ of the base state. + * + * Pass only a function to create a "curried producer" which relieves you + * from passing the recipe function every time. + * + * Only plain objects and arrays are made mutable. All other objects are + * considered uncopyable. + * + * Note: This function is __bound__ to its `Immer` instance. + * + * @param {any} base - the initial state + * @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified + * @param {Function} patchListener - optional function that will be called with all the patches produced here + * @returns {any} a new state, or the initial state if nothing was modified + */ + + this.produce = function (base, recipe, patchListener) { + // curried invocation + if (typeof base === "function" && typeof recipe !== "function") { + var defaultBase = recipe; + recipe = base; + var self = _this; + return function curriedProduce(base) { + var _this2 = this; + + if (base === void 0) { + base = defaultBase; + } + + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + return self.produce(base, function (draft) { + var _recipe; + + return (_recipe = recipe).call.apply(_recipe, [_this2, draft].concat(args)); + }); // prettier-ignore + }; + } + + if (typeof recipe !== "function") die(6); + if (patchListener !== undefined && typeof patchListener !== "function") die(7); + var result; // Only plain objects, arrays, and "immerable classes" are drafted. + + if (isDraftable(base)) { + var scope = enterScope(_this); + var proxy = createProxy(_this, base, undefined); + var hasError = true; + + try { + result = recipe(proxy); + hasError = false; + } finally { + // finally instead of catch + rethrow better preserves original stack + if (hasError) revokeScope(scope);else leaveScope(scope); + } + + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then(function (result) { + usePatchesInScope(scope, patchListener); + return processResult(result, scope); + }, function (error) { + revokeScope(scope); + throw error; + }); + } + + usePatchesInScope(scope, patchListener); + return processResult(result, scope); + } else if (!base || typeof base !== "object") { + result = recipe(base); + if (result === NOTHING) return undefined; + if (result === undefined) result = base; + if (_this.autoFreeze_) freeze(result, true); + return result; + } else die(21, base); + }; + + this.produceWithPatches = function (arg1, arg2, arg3) { + if (typeof arg1 === "function") { + return function (state) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + + return _this.produceWithPatches(state, function (draft) { + return arg1.apply(void 0, [draft].concat(args)); + }); + }; + } + + var patches, inversePatches; + + var nextState = _this.produce(arg1, arg2, function (p, ip) { + patches = p; + inversePatches = ip; + }); + + return [nextState, patches, inversePatches]; + }; + + if (typeof (config === null || config === void 0 ? void 0 : config.useProxies) === "boolean") this.setUseProxies(config.useProxies); + if (typeof (config === null || config === void 0 ? void 0 : config.autoFreeze) === "boolean") this.setAutoFreeze(config.autoFreeze); + } + + var _proto = Immer.prototype; + + _proto.createDraft = function createDraft(base) { + if (!isDraftable(base)) die(8); + if (isDraft(base)) base = current(base); + var scope = enterScope(this); + var proxy = createProxy(this, base, undefined); + proxy[DRAFT_STATE].isManual_ = true; + leaveScope(scope); + return proxy; + }; + + _proto.finishDraft = function finishDraft(draft, patchListener) { + var state = draft && draft[DRAFT_STATE]; + + { + if (!state || !state.isManual_) die(9); + if (state.finalized_) die(10); + } + + var scope = state.scope_; + usePatchesInScope(scope, patchListener); + return processResult(undefined, scope); + } + /** + * Pass true to automatically freeze all copies created by Immer. + * + * By default, auto-freezing is enabled. + */ + ; + + _proto.setAutoFreeze = function setAutoFreeze(value) { + this.autoFreeze_ = value; + } + /** + * Pass true to use the ES2015 `Proxy` class when creating drafts, which is + * always faster than using ES5 proxies. + * + * By default, feature detection is used, so calling this is rarely necessary. + */ + ; + + _proto.setUseProxies = function setUseProxies(value) { + if (value && !hasProxies) { + die(20); + } + + this.useProxies_ = value; + }; + + _proto.applyPatches = function applyPatches(base, patches) { + // If a patch replaces the entire state, take that replacement as base + // before applying patches + var i; + + for (i = patches.length - 1; i >= 0; i--) { + var patch = patches[i]; + + if (patch.path.length === 0 && patch.op === "replace") { + base = patch.value; + break; + } + } // If there was a patch that replaced the entire state, start from the + // patch after that. + + + if (i > -1) { + patches = patches.slice(i + 1); + } + + var applyPatchesImpl = getPlugin("Patches").applyPatches_; + + if (isDraft(base)) { + // N.B: never hits if some patch a replacement, patches are never drafts + return applyPatchesImpl(base, patches); + } // Otherwise, produce a copy of the base state. + + + return this.produce(base, function (draft) { + return applyPatchesImpl(draft, patches); + }); + }; + + return Immer; + }(); + function createProxy(immer, value, parent) { + // precondition: createProxy should be guarded by isDraftable, so we know we can safely draft + var draft = isMap(value) ? getPlugin("MapSet").proxyMap_(value, parent) : isSet(value) ? getPlugin("MapSet").proxySet_(value, parent) : immer.useProxies_ ? createProxyProxy(value, parent) : getPlugin("ES5").createES5Proxy_(value, parent); + var scope = parent ? parent.scope_ : getCurrentScope(); + scope.drafts_.push(draft); + return draft; + } + + function current(value) { + if (!isDraft(value)) die(22, value); + return currentImpl(value); + } + + function currentImpl(value) { + if (!isDraftable(value)) return value; + var state = value[DRAFT_STATE]; + var copy; + var archType = getArchtype(value); + + if (state) { + if (!state.modified_ && (state.type_ < 4 || !getPlugin("ES5").hasChanges_(state))) return state.base_; // Optimization: avoid generating new drafts during copying + + state.finalized_ = true; + copy = copyHelper(value, archType); + state.finalized_ = false; + } else { + copy = copyHelper(value, archType); + } + + each$1(copy, function (key, childValue) { + if (state && get(state.base_, key) === childValue) return; // no need to copy or search in something that didn't change + + set(copy, key, currentImpl(childValue)); + }); // In the future, we might consider freezing here, based on the current settings + + return archType === 3 + /* Set */ + ? new Set(copy) : copy; + } + + function copyHelper(value, archType) { + // creates a shallow copy, even if it is a map or set + switch (archType) { + case 2 + /* Map */ + : + return new Map(value); + + case 3 + /* Set */ + : + // Set will be cloned as array temporarily, so that we can replace individual items + return Array.from(value); + } + + return shallowCopy(value); + } + + function enableES5() { + function willFinalizeES5_(scope, result, isReplaced) { + if (!isReplaced) { + if (scope.patches_) { + markChangesRecursively(scope.drafts_[0]); + } // This is faster when we don't care about which attributes changed. + + + markChangesSweep(scope.drafts_); + } // When a child draft is returned, look for changes. + else if (isDraft(result) && result[DRAFT_STATE].scope_ === scope) { + markChangesSweep(scope.drafts_); + } + } + + function createES5Draft(isArray, base) { + if (isArray) { + var draft = new Array(base.length); + + for (var i = 0; i < base.length; i++) { + Object.defineProperty(draft, "" + i, proxyProperty(i, true)); + } + + return draft; + } else { + var _descriptors = getOwnPropertyDescriptors(base); + + delete _descriptors[DRAFT_STATE]; + var keys = ownKeys$a(_descriptors); + + for (var _i = 0; _i < keys.length; _i++) { + var key = keys[_i]; + _descriptors[key] = proxyProperty(key, isArray || !!_descriptors[key].enumerable); + } + + return Object.create(Object.getPrototypeOf(base), _descriptors); + } + } + + function createES5Proxy_(base, parent) { + var isArray = Array.isArray(base); + var draft = createES5Draft(isArray, base); + var state = { + type_: isArray ? 5 + /* ES5Array */ + : 4 + /* ES5Object */ + , + scope_: parent ? parent.scope_ : getCurrentScope(), + modified_: false, + finalized_: false, + assigned_: {}, + parent_: parent, + // base is the object we are drafting + base_: base, + // draft is the draft object itself, that traps all reads and reads from either the base (if unmodified) or copy (if modified) + draft_: draft, + copy_: null, + revoked_: false, + isManual_: false + }; + Object.defineProperty(draft, DRAFT_STATE, { + value: state, + // enumerable: false <- the default + writable: true + }); + return draft; + } // property descriptors are recycled to make sure we don't create a get and set closure per property, + // but share them all instead + + + var descriptors = {}; + + function proxyProperty(prop, enumerable) { + var desc = descriptors[prop]; + + if (desc) { + desc.enumerable = enumerable; + } else { + descriptors[prop] = desc = { + configurable: true, + enumerable: enumerable, + get: function get() { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); // @ts-ignore + + return objectTraps.get(state, prop); + }, + set: function set(value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); // @ts-ignore + + objectTraps.set(state, prop, value); + } + }; + } + + return desc; + } // This looks expensive, but only proxies are visited, and only objects without known changes are scanned. + + + function markChangesSweep(drafts) { + // The natural order of drafts in the `scope` array is based on when they + // were accessed. By processing drafts in reverse natural order, we have a + // better chance of processing leaf nodes first. When a leaf node is known to + // have changed, we can avoid any traversal of its ancestor nodes. + for (var i = drafts.length - 1; i >= 0; i--) { + var state = drafts[i][DRAFT_STATE]; + + if (!state.modified_) { + switch (state.type_) { + case 5 + /* ES5Array */ + : + if (hasArrayChanges(state)) markChanged(state); + break; + + case 4 + /* ES5Object */ + : + if (hasObjectChanges(state)) markChanged(state); + break; + } + } + } + } + + function markChangesRecursively(object) { + if (!object || typeof object !== "object") return; + var state = object[DRAFT_STATE]; + if (!state) return; + var base_ = state.base_, + draft_ = state.draft_, + assigned_ = state.assigned_, + type_ = state.type_; + + if (type_ === 4 + /* ES5Object */ + ) { + // Look for added keys. + // probably there is a faster way to detect changes, as sweep + recurse seems to do some + // unnecessary work. + // also: probably we can store the information we detect here, to speed up tree finalization! + each$1(draft_, function (key) { + if (key === DRAFT_STATE) return; // The `undefined` check is a fast path for pre-existing keys. + + if (base_[key] === undefined && !has(base_, key)) { + assigned_[key] = true; + markChanged(state); + } else if (!assigned_[key]) { + // Only untouched properties trigger recursion. + markChangesRecursively(draft_[key]); + } + }); // Look for removed keys. + + each$1(base_, function (key) { + // The `undefined` check is a fast path for pre-existing keys. + if (draft_[key] === undefined && !has(draft_, key)) { + assigned_[key] = false; + markChanged(state); + } + }); + } else if (type_ === 5 + /* ES5Array */ + ) { + if (hasArrayChanges(state)) { + markChanged(state); + assigned_.length = true; + } + + if (draft_.length < base_.length) { + for (var i = draft_.length; i < base_.length; i++) { + assigned_[i] = false; + } + } else { + for (var _i2 = base_.length; _i2 < draft_.length; _i2++) { + assigned_[_i2] = true; + } + } // Minimum count is enough, the other parts has been processed. + + + var min = Math.min(draft_.length, base_.length); + + for (var _i3 = 0; _i3 < min; _i3++) { + // Only untouched indices trigger recursion. + if (assigned_[_i3] === undefined) markChangesRecursively(draft_[_i3]); + } + } + } + + function hasObjectChanges(state) { + var base_ = state.base_, + draft_ = state.draft_; // Search for added keys and changed keys. Start at the back, because + // non-numeric keys are ordered by time of definition on the object. + + var keys = ownKeys$a(draft_); + + for (var i = keys.length - 1; i >= 0; i--) { + var key = keys[i]; + if (key === DRAFT_STATE) continue; + var baseValue = base_[key]; // The `undefined` check is a fast path for pre-existing keys. + + if (baseValue === undefined && !has(base_, key)) { + return true; + } // Once a base key is deleted, future changes go undetected, because its + // descriptor is erased. This branch detects any missed changes. + else { + var value = draft_[key]; + + var _state = value && value[DRAFT_STATE]; + + if (_state ? _state.base_ !== baseValue : !is$1(value, baseValue)) { + return true; + } + } + } // At this point, no keys were added or changed. + // Compare key count to determine if keys were deleted. + + + var baseIsDraft = !!base_[DRAFT_STATE]; + return keys.length !== ownKeys$a(base_).length + (baseIsDraft ? 0 : 1); // + 1 to correct for DRAFT_STATE + } + + function hasArrayChanges(state) { + var draft_ = state.draft_; + if (draft_.length !== state.base_.length) return true; // See #116 + // If we first shorten the length, our array interceptors will be removed. + // If after that new items are added, result in the same original length, + // those last items will have no intercepting property. + // So if there is no own descriptor on the last position, we know that items were removed and added + // N.B.: splice, unshift, etc only shift values around, but not prop descriptors, so we only have to check + // the last one + + var descriptor = Object.getOwnPropertyDescriptor(draft_, draft_.length - 1); // descriptor can be null, but only for newly created sparse arrays, eg. new Array(10) + + if (descriptor && !descriptor.get) return true; // For all other cases, we don't have to compare, as they would have been picked up by the index setters + + return false; + } + + function hasChanges_(state) { + return state.type_ === 4 + /* ES5Object */ + ? hasObjectChanges(state) : hasArrayChanges(state); + } + + function assertUnrevoked(state + /*ES5State | MapState | SetState*/ + ) { + if (state.revoked_) die(3, JSON.stringify(latest(state))); + } + + loadPlugin("ES5", { + createES5Proxy_: createES5Proxy_, + willFinalizeES5_: willFinalizeES5_, + hasChanges_: hasChanges_ + }); + } + + function enablePatches() { + var REPLACE = "replace"; + var ADD = "add"; + var REMOVE = "remove"; + + function generatePatches_(state, basePath, patches, inversePatches) { + switch (state.type_) { + case 0 + /* ProxyObject */ + : + case 4 + /* ES5Object */ + : + case 2 + /* Map */ + : + return generatePatchesFromAssigned(state, basePath, patches, inversePatches); + + case 5 + /* ES5Array */ + : + case 1 + /* ProxyArray */ + : + return generateArrayPatches(state, basePath, patches, inversePatches); + + case 3 + /* Set */ + : + return generateSetPatches(state, basePath, patches, inversePatches); + } + } + + function generateArrayPatches(state, basePath, patches, inversePatches) { + var base_ = state.base_, + assigned_ = state.assigned_; + var copy_ = state.copy_; // Reduce complexity by ensuring `base` is never longer. + + if (copy_.length < base_.length) { + var _ref = [copy_, base_]; + base_ = _ref[0]; + copy_ = _ref[1]; + var _ref2 = [inversePatches, patches]; + patches = _ref2[0]; + inversePatches = _ref2[1]; + } // Process replaced indices. + + + for (var i = 0; i < base_.length; i++) { + if (assigned_[i] && copy_[i] !== base_[i]) { + var path = basePath.concat([i]); + patches.push({ + op: REPLACE, + path: path, + // Need to maybe clone it, as it can in fact be the original value + // due to the base/copy inversion at the start of this function + value: clonePatchValueIfNeeded(copy_[i]) + }); + inversePatches.push({ + op: REPLACE, + path: path, + value: clonePatchValueIfNeeded(base_[i]) + }); + } + } // Process added indices. + + + for (var _i = base_.length; _i < copy_.length; _i++) { + var _path = basePath.concat([_i]); + + patches.push({ + op: ADD, + path: _path, + // Need to maybe clone it, as it can in fact be the original value + // due to the base/copy inversion at the start of this function + value: clonePatchValueIfNeeded(copy_[_i]) + }); + } + + if (base_.length < copy_.length) { + inversePatches.push({ + op: REPLACE, + path: basePath.concat(["length"]), + value: base_.length + }); + } + } // This is used for both Map objects and normal objects. + + + function generatePatchesFromAssigned(state, basePath, patches, inversePatches) { + var base_ = state.base_, + copy_ = state.copy_; + each$1(state.assigned_, function (key, assignedValue) { + var origValue = get(base_, key); + var value = get(copy_, key); + var op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD; + if (origValue === value && op === REPLACE) return; + var path = basePath.concat(key); + patches.push(op === REMOVE ? { + op: op, + path: path + } : { + op: op, + path: path, + value: value + }); + inversePatches.push(op === ADD ? { + op: REMOVE, + path: path + } : op === REMOVE ? { + op: ADD, + path: path, + value: clonePatchValueIfNeeded(origValue) + } : { + op: REPLACE, + path: path, + value: clonePatchValueIfNeeded(origValue) + }); + }); + } + + function generateSetPatches(state, basePath, patches, inversePatches) { + var base_ = state.base_, + copy_ = state.copy_; + var i = 0; + base_.forEach(function (value) { + if (!copy_.has(value)) { + var path = basePath.concat([i]); + patches.push({ + op: REMOVE, + path: path, + value: value + }); + inversePatches.unshift({ + op: ADD, + path: path, + value: value + }); + } + + i++; + }); + i = 0; + copy_.forEach(function (value) { + if (!base_.has(value)) { + var path = basePath.concat([i]); + patches.push({ + op: ADD, + path: path, + value: value + }); + inversePatches.unshift({ + op: REMOVE, + path: path, + value: value + }); + } + + i++; + }); + } + + function generateReplacementPatches_(rootState, replacement, patches, inversePatches) { + patches.push({ + op: REPLACE, + path: [], + value: replacement === NOTHING ? undefined : replacement + }); + inversePatches.push({ + op: REPLACE, + path: [], + value: rootState.base_ + }); + } + + function applyPatches_(draft, patches) { + patches.forEach(function (patch) { + var path = patch.path, + op = patch.op; + var base = draft; + + for (var i = 0; i < path.length - 1; i++) { + var parentType = getArchtype(base); + var p = "" + path[i]; // See #738, avoid prototype pollution + + if ((parentType === 0 + /* Object */ + || parentType === 1 + /* Array */ + ) && (p === "__proto__" || p === "constructor")) die(24); + if (typeof base === "function" && p === "prototype") die(24); + base = get(base, p); + if (typeof base !== "object") die(15, path.join("/")); + } + + var type = getArchtype(base); + var value = deepClonePatchValue(patch.value); // used to clone patch to ensure original patch is not modified, see #411 + + var key = path[path.length - 1]; + + switch (op) { + case REPLACE: + switch (type) { + case 2 + /* Map */ + : + return base.set(key, value); + + /* istanbul ignore next */ + + case 3 + /* Set */ + : + die(16); + + default: + // if value is an object, then it's assigned by reference + // in the following add or remove ops, the value field inside the patch will also be modifyed + // so we use value from the cloned patch + // @ts-ignore + return base[key] = value; + } + + case ADD: + switch (type) { + case 1 + /* Array */ + : + return key === "-" ? base.push(value) : base.splice(key, 0, value); + + case 2 + /* Map */ + : + return base.set(key, value); + + case 3 + /* Set */ + : + return base.add(value); + + default: + return base[key] = value; + } + + case REMOVE: + switch (type) { + case 1 + /* Array */ + : + return base.splice(key, 1); + + case 2 + /* Map */ + : + return base.delete(key); + + case 3 + /* Set */ + : + return base.delete(patch.value); + + default: + return delete base[key]; + } + + default: + die(17, op); + } + }); + return draft; + } + + function deepClonePatchValue(obj) { + if (!isDraftable(obj)) return obj; + if (Array.isArray(obj)) return obj.map(deepClonePatchValue); + if (isMap(obj)) return new Map(Array.from(obj.entries()).map(function (_ref3) { + var k = _ref3[0], + v = _ref3[1]; + return [k, deepClonePatchValue(v)]; + })); + if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue)); + var cloned = Object.create(Object.getPrototypeOf(obj)); + + for (var key in obj) { + cloned[key] = deepClonePatchValue(obj[key]); + } + + if (has(obj, DRAFTABLE)) cloned[DRAFTABLE] = obj[DRAFTABLE]; + return cloned; + } + + function clonePatchValueIfNeeded(obj) { + if (isDraft(obj)) { + return deepClonePatchValue(obj); + } else return obj; + } + + loadPlugin("Patches", { + applyPatches_: applyPatches_, + generatePatches_: generatePatches_, + generateReplacementPatches_: generateReplacementPatches_ + }); + } + + // types only! + function enableMapSet() { + /* istanbul ignore next */ + var _extendStatics = function extendStatics(d, b) { + _extendStatics = Object.setPrototypeOf || { + __proto__: [] + } instanceof Array && function (d, b) { + d.__proto__ = b; + } || function (d, b) { + for (var p in b) { + if (b.hasOwnProperty(p)) d[p] = b[p]; + } + }; + + return _extendStatics(d, b); + }; // Ugly hack to resolve #502 and inherit built in Map / Set + + + function __extends(d, b) { + _extendStatics(d, b); + + function __() { + this.constructor = d; + } + + d.prototype = ( // @ts-ignore + __.prototype = b.prototype, new __()); + } + + var DraftMap = function (_super) { + __extends(DraftMap, _super); // Create class manually, cause #502 + + + function DraftMap(target, parent) { + this[DRAFT_STATE] = { + type_: 2 + /* Map */ + , + parent_: parent, + scope_: parent ? parent.scope_ : getCurrentScope(), + modified_: false, + finalized_: false, + copy_: undefined, + assigned_: undefined, + base_: target, + draft_: this, + isManual_: false, + revoked_: false + }; + return this; + } + + var p = DraftMap.prototype; + Object.defineProperty(p, "size", { + get: function get() { + return latest(this[DRAFT_STATE]).size; + } // enumerable: false, + // configurable: true + + }); + + p.has = function (key) { + return latest(this[DRAFT_STATE]).has(key); + }; + + p.set = function (key, value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (!latest(state).has(key) || latest(state).get(key) !== value) { + prepareMapCopy(state); + markChanged(state); + state.assigned_.set(key, true); + state.copy_.set(key, value); + state.assigned_.set(key, true); + } + + return this; + }; + + p.delete = function (key) { + if (!this.has(key)) { + return false; + } + + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareMapCopy(state); + markChanged(state); + state.assigned_.set(key, false); + state.copy_.delete(key); + return true; + }; + + p.clear = function () { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (latest(state).size) { + prepareMapCopy(state); + markChanged(state); + state.assigned_ = new Map(); + each$1(state.base_, function (key) { + state.assigned_.set(key, false); + }); + state.copy_.clear(); + } + }; + + p.forEach = function (cb, thisArg) { + var _this = this; + + var state = this[DRAFT_STATE]; + latest(state).forEach(function (_value, key, _map) { + cb.call(thisArg, _this.get(key), key, _this); + }); + }; + + p.get = function (key) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + var value = latest(state).get(key); + + if (state.finalized_ || !isDraftable(value)) { + return value; + } + + if (value !== state.base_.get(key)) { + return value; // either already drafted or reassigned + } // despite what it looks, this creates a draft only once, see above condition + + + var draft = createProxy(state.scope_.immer_, value, state); + prepareMapCopy(state); + state.copy_.set(key, draft); + return draft; + }; + + p.keys = function () { + return latest(this[DRAFT_STATE]).keys(); + }; + + p.values = function () { + var _this2 = this, + _ref; + + var iterator = this.keys(); + return _ref = {}, _ref[iteratorSymbol$1] = function () { + return _this2.values(); + }, _ref.next = function next() { + var r = iterator.next(); + /* istanbul ignore next */ + + if (r.done) return r; + + var value = _this2.get(r.value); + + return { + done: false, + value: value + }; + }, _ref; + }; + + p.entries = function () { + var _this3 = this, + _ref2; + + var iterator = this.keys(); + return _ref2 = {}, _ref2[iteratorSymbol$1] = function () { + return _this3.entries(); + }, _ref2.next = function next() { + var r = iterator.next(); + /* istanbul ignore next */ + + if (r.done) return r; + + var value = _this3.get(r.value); + + return { + done: false, + value: [r.value, value] + }; + }, _ref2; + }; + + p[iteratorSymbol$1] = function () { + return this.entries(); + }; + + return DraftMap; + }(Map); + + function proxyMap_(target, parent) { + // @ts-ignore + return new DraftMap(target, parent); + } + + function prepareMapCopy(state) { + if (!state.copy_) { + state.assigned_ = new Map(); + state.copy_ = new Map(state.base_); + } + } + + var DraftSet = function (_super) { + __extends(DraftSet, _super); // Create class manually, cause #502 + + + function DraftSet(target, parent) { + this[DRAFT_STATE] = { + type_: 3 + /* Set */ + , + parent_: parent, + scope_: parent ? parent.scope_ : getCurrentScope(), + modified_: false, + finalized_: false, + copy_: undefined, + base_: target, + draft_: this, + drafts_: new Map(), + revoked_: false, + isManual_: false + }; + return this; + } + + var p = DraftSet.prototype; + Object.defineProperty(p, "size", { + get: function get() { + return latest(this[DRAFT_STATE]).size; + } // enumerable: true, + + }); + + p.has = function (value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); // bit of trickery here, to be able to recognize both the value, and the draft of its value + + if (!state.copy_) { + return state.base_.has(value); + } + + if (state.copy_.has(value)) return true; + if (state.drafts_.has(value) && state.copy_.has(state.drafts_.get(value))) return true; + return false; + }; + + p.add = function (value) { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (!this.has(value)) { + prepareSetCopy(state); + markChanged(state); + state.copy_.add(value); + } + + return this; + }; + + p.delete = function (value) { + if (!this.has(value)) { + return false; + } + + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareSetCopy(state); + markChanged(state); + return state.copy_.delete(value) || (state.drafts_.has(value) ? state.copy_.delete(state.drafts_.get(value)) : + /* istanbul ignore next */ + false); + }; + + p.clear = function () { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + + if (latest(state).size) { + prepareSetCopy(state); + markChanged(state); + state.copy_.clear(); + } + }; + + p.values = function () { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareSetCopy(state); + return state.copy_.values(); + }; + + p.entries = function entries() { + var state = this[DRAFT_STATE]; + assertUnrevoked(state); + prepareSetCopy(state); + return state.copy_.entries(); + }; + + p.keys = function () { + return this.values(); + }; + + p[iteratorSymbol$1] = function () { + return this.values(); + }; + + p.forEach = function forEach(cb, thisArg) { + var iterator = this.values(); + var result = iterator.next(); + + while (!result.done) { + cb.call(thisArg, result.value, result.value, this); + result = iterator.next(); + } + }; + + return DraftSet; + }(Set); + + function proxySet_(target, parent) { + // @ts-ignore + return new DraftSet(target, parent); + } + + function prepareSetCopy(state) { + if (!state.copy_) { + // create drafts for all entries to preserve insertion order + state.copy_ = new Set(); + state.base_.forEach(function (value) { + if (isDraftable(value)) { + var draft = createProxy(state.scope_.immer_, value, state); + state.drafts_.set(value, draft); + state.copy_.add(draft); + } else { + state.copy_.add(value); + } + }); + } + } + + function assertUnrevoked(state + /*ES5State | MapState | SetState*/ + ) { + if (state.revoked_) die(3, JSON.stringify(latest(state))); + } + + loadPlugin("MapSet", { + proxyMap_: proxyMap_, + proxySet_: proxySet_ + }); + } + + function enableAllPlugins() { + enableES5(); + enableMapSet(); + enablePatches(); + } + + var immer$1 = + /*#__PURE__*/ + new Immer(); + /** + * The `produce` function takes a value and a "recipe function" (whose + * return value often depends on the base state). The recipe function is + * free to mutate its first argument however it wants. All mutations are + * only ever applied to a __copy__ of the base state. + * + * Pass only a function to create a "curried producer" which relieves you + * from passing the recipe function every time. + * + * Only plain objects and arrays are made mutable. All other objects are + * considered uncopyable. + * + * Note: This function is __bound__ to its `Immer` instance. + * + * @param {any} base - the initial state + * @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified + * @param {Function} patchListener - optional function that will be called with all the patches produced here + * @returns {any} a new state, or the initial state if nothing was modified + */ + + var produce = immer$1.produce; + /** + * Like `produce`, but `produceWithPatches` always returns a tuple + * [nextState, patches, inversePatches] (instead of just the next state) + */ + + var produceWithPatches = + /*#__PURE__*/ + immer$1.produceWithPatches.bind(immer$1); + /** + * Pass true to automatically freeze all copies created by Immer. + * + * Always freeze by default, even in production mode + */ + + var setAutoFreeze = + /*#__PURE__*/ + immer$1.setAutoFreeze.bind(immer$1); + /** + * Pass true to use the ES2015 `Proxy` class when creating drafts, which is + * always faster than using ES5 proxies. + * + * By default, feature detection is used, so calling this is rarely necessary. + */ + + var setUseProxies = + /*#__PURE__*/ + immer$1.setUseProxies.bind(immer$1); + /** + * Apply an array of Immer patches to the first argument. + * + * This function is a producer, which means copy-on-write is in effect. + */ + + var applyPatches = + /*#__PURE__*/ + immer$1.applyPatches.bind(immer$1); + /** + * Create an Immer draft from the given base state, which may be a draft itself. + * The draft can be modified until you finalize it with the `finishDraft` function. + */ + + var createDraft = + /*#__PURE__*/ + immer$1.createDraft.bind(immer$1); + /** + * Finalize an Immer draft from a `createDraft` call, returning the base state + * (if no changes were made) or a modified copy. The draft must *not* be + * mutated afterwards. + * + * Pass a function as the 2nd argument to generate Immer patches based on the + * changes that were made. + */ + + var finishDraft = + /*#__PURE__*/ + immer$1.finishDraft.bind(immer$1); + /** + * This function is actually a no-op, but can be used to cast an immutable type + * to an draft type and make TypeScript happy + * + * @param value + */ + + function castDraft(value) { + return value; + } + /** + * This function is actually a no-op, but can be used to cast a mutable type + * to an immutable type and make TypeScript happy + * @param value + */ + + function castImmutable(value) { + return value; + } + + var Immer_1 = Immer; + var applyPatches_1 = applyPatches; + var castDraft_1 = castDraft; + var castImmutable_1 = castImmutable; + var createDraft_1 = createDraft; + var current_1 = current; + var _default$2 = produce; + var enableAllPlugins_1 = enableAllPlugins; + var enableES5_1 = enableES5; + var enableMapSet_1 = enableMapSet; + var enablePatches_1 = enablePatches; + var finishDraft_1 = finishDraft; + var freeze_1 = freeze; + var immerable = DRAFTABLE; + var isDraft_1 = isDraft; + var isDraftable_1 = isDraftable; + var nothing = NOTHING; + var original_1 = original; + var produce_1 = produce; + var produceWithPatches_1 = produceWithPatches; + var setAutoFreeze_1 = setAutoFreeze; + var setUseProxies_1 = setUseProxies; + + + var immer_cjs_development = /*#__PURE__*/Object.defineProperty({ + Immer: Immer_1, + applyPatches: applyPatches_1, + castDraft: castDraft_1, + castImmutable: castImmutable_1, + createDraft: createDraft_1, + current: current_1, + default: _default$2, + enableAllPlugins: enableAllPlugins_1, + enableES5: enableES5_1, + enableMapSet: enableMapSet_1, + enablePatches: enablePatches_1, + finishDraft: finishDraft_1, + freeze: freeze_1, + immerable: immerable, + isDraft: isDraft_1, + isDraftable: isDraftable_1, + nothing: nothing, + original: original_1, + produce: produce_1, + produceWithPatches: produceWithPatches_1, + setAutoFreeze: setAutoFreeze_1, + setUseProxies: setUseProxies_1 + }, '__esModule', {value: true}); + + var require$$1$1 = immer_cjs_development; + + var dist$8 = createCommonjsModule$1(function (module) { + + { + module.exports = require$$1$1; + } + }); + + var isPlainObject = isPlainObject_1; + + var immer = dist$8; + + function unwrapExports (x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var arrayLikeToArray = createCommonjsModule(function (module) { + function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) { + arr2[i] = arr[i]; + } + + return arr2; + } + + module.exports = _arrayLikeToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(arrayLikeToArray); + + var arrayWithoutHoles = createCommonjsModule(function (module) { + function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) return arrayLikeToArray(arr); + } + + module.exports = _arrayWithoutHoles; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(arrayWithoutHoles); + + var iterableToArray = createCommonjsModule(function (module) { + function _iterableToArray(iter) { + if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); + } + + module.exports = _iterableToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(iterableToArray); + + var unsupportedIterableToArray = createCommonjsModule(function (module) { + function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen); + } + + module.exports = _unsupportedIterableToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(unsupportedIterableToArray); + + var nonIterableSpread = createCommonjsModule(function (module) { + function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + module.exports = _nonIterableSpread; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(nonIterableSpread); + + var toConsumableArray = createCommonjsModule(function (module) { + function _toConsumableArray(arr) { + return arrayWithoutHoles(arr) || iterableToArray(arr) || unsupportedIterableToArray(arr) || nonIterableSpread(); + } + + module.exports = _toConsumableArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _toConsumableArray = unwrapExports(toConsumableArray); + + var arrayWithHoles = createCommonjsModule(function (module) { + function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; + } + + module.exports = _arrayWithHoles; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(arrayWithHoles); + + var iterableToArrayLimit = createCommonjsModule(function (module) { + function _iterableToArrayLimit(arr, i) { + var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; + + if (_i == null) return; + var _arr = []; + var _n = true; + var _d = false; + + var _s, _e; + + try { + for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + module.exports = _iterableToArrayLimit; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(iterableToArrayLimit); + + var nonIterableRest = createCommonjsModule(function (module) { + function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); + } + + module.exports = _nonIterableRest; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(nonIterableRest); + + var slicedToArray = createCommonjsModule(function (module) { + function _slicedToArray(arr, i) { + return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest(); + } + + module.exports = _slicedToArray; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _slicedToArray = unwrapExports(slicedToArray); + + var defineProperty = createCommonjsModule(function (module) { + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + module.exports = _defineProperty; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _defineProperty = unwrapExports(defineProperty); + + var DIRTY_PATHS = new WeakMap(); + var FLUSHING = new WeakMap(); + var NORMALIZING = new WeakMap(); + var PATH_REFS = new WeakMap(); + var POINT_REFS = new WeakMap(); + var RANGE_REFS = new WeakMap(); + + function ownKeys$9(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$9(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$9(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$9(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$7(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$7(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$7(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$7(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$7(o, minLen); } + + function _arrayLikeToArray$7(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + /** + * Create a new Slate `Editor` object. + */ + + var createEditor$1 = function createEditor() { + var editor = { + children: [], + operations: [], + selection: null, + marks: null, + isInline: function isInline() { + return false; + }, + isVoid: function isVoid() { + return false; + }, + onChange: function onChange() {}, + apply: function apply(op) { + var _iterator = _createForOfIteratorHelper$7(Editor.pathRefs(editor)), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var ref = _step.value; + PathRef.transform(ref, op); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + var _iterator2 = _createForOfIteratorHelper$7(Editor.pointRefs(editor)), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _ref = _step2.value; + PointRef.transform(_ref, op); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + var _iterator3 = _createForOfIteratorHelper$7(Editor.rangeRefs(editor)), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _ref2 = _step3.value; + RangeRef.transform(_ref2, op); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + + var set = new Set(); + var dirtyPaths = []; + + var add = function add(path) { + if (path) { + var key = path.join(','); + + if (!set.has(key)) { + set.add(key); + dirtyPaths.push(path); + } + } + }; + + var oldDirtyPaths = DIRTY_PATHS.get(editor) || []; + var newDirtyPaths = getDirtyPaths(op); + + var _iterator4 = _createForOfIteratorHelper$7(oldDirtyPaths), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var path = _step4.value; + var newPath = Path.transform(path, op); + add(newPath); + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + var _iterator5 = _createForOfIteratorHelper$7(newDirtyPaths), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _path = _step5.value; + add(_path); + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + + DIRTY_PATHS.set(editor, dirtyPaths); + Transforms.transform(editor, op); + editor.operations.push(op); + Editor.normalize(editor); // Clear any formats applied to the cursor if the selection changes. + + if (op.type === 'set_selection') { + editor.marks = null; + } + + if (!FLUSHING.get(editor)) { + FLUSHING.set(editor, true); + Promise.resolve().then(function () { + FLUSHING.set(editor, false); + editor.onChange(); + editor.operations = []; + }); + } + }, + addMark: function addMark(key, value) { + var selection = editor.selection; + + if (selection) { + if (Range.isExpanded(selection)) { + Transforms.setNodes(editor, _defineProperty({}, key, value), { + match: Text.isText, + split: true + }); + } else { + var marks = _objectSpread$9(_objectSpread$9({}, Editor.marks(editor) || {}), {}, _defineProperty({}, key, value)); + + editor.marks = marks; + + if (!FLUSHING.get(editor)) { + editor.onChange(); + } + } + } + }, + deleteBackward: function deleteBackward(unit) { + var selection = editor.selection; + + if (selection && Range.isCollapsed(selection)) { + Transforms["delete"](editor, { + unit: unit, + reverse: true + }); + } + }, + deleteForward: function deleteForward(unit) { + var selection = editor.selection; + + if (selection && Range.isCollapsed(selection)) { + Transforms["delete"](editor, { + unit: unit + }); + } + }, + deleteFragment: function deleteFragment(direction) { + var selection = editor.selection; + + if (selection && Range.isExpanded(selection)) { + Transforms["delete"](editor, { + reverse: direction === 'backward' + }); + } + }, + getFragment: function getFragment() { + var selection = editor.selection; + + if (selection) { + return Node$1.fragment(editor, selection); + } + + return []; + }, + insertBreak: function insertBreak() { + Transforms.splitNodes(editor, { + always: true + }); + }, + insertFragment: function insertFragment(fragment) { + Transforms.insertFragment(editor, fragment); + }, + insertNode: function insertNode(node) { + Transforms.insertNodes(editor, node); + }, + insertText: function insertText(text) { + var selection = editor.selection, + marks = editor.marks; + + if (selection) { + if (marks) { + var node = _objectSpread$9({ + text: text + }, marks); + + Transforms.insertNodes(editor, node); + } else { + Transforms.insertText(editor, text); + } + + editor.marks = null; + } + }, + normalizeNode: function normalizeNode(entry) { + var _entry = _slicedToArray(entry, 2), + node = _entry[0], + path = _entry[1]; // There are no core normalizations for text nodes. + + + if (Text.isText(node)) { + return; + } // Ensure that block and inline nodes have at least one text child. + + + if (Element$1.isElement(node) && node.children.length === 0) { + var child = { + text: '' + }; + Transforms.insertNodes(editor, child, { + at: path.concat(0), + voids: true + }); + return; + } // Determine whether the node should have block or inline children. + + + var shouldHaveInlines = Editor.isEditor(node) ? false : Element$1.isElement(node) && (editor.isInline(node) || node.children.length === 0 || Text.isText(node.children[0]) || editor.isInline(node.children[0])); // Since we'll be applying operations while iterating, keep track of an + // index that accounts for any added/removed nodes. + + var n = 0; + + for (var i = 0; i < node.children.length; i++, n++) { + var currentNode = Node$1.get(editor, path); + if (Text.isText(currentNode)) continue; + var _child = node.children[i]; + var prev = currentNode.children[n - 1]; + var isLast = i === node.children.length - 1; + var isInlineOrText = Text.isText(_child) || Element$1.isElement(_child) && editor.isInline(_child); // Only allow block nodes in the top-level children and parent blocks + // that only contain block nodes. Similarly, only allow inline nodes in + // other inline nodes, or parent blocks that only contain inlines and + // text. + + if (isInlineOrText !== shouldHaveInlines) { + Transforms.removeNodes(editor, { + at: path.concat(n), + voids: true + }); + n--; + } else if (Element$1.isElement(_child)) { + // Ensure that inline nodes are surrounded by text nodes. + if (editor.isInline(_child)) { + if (prev == null || !Text.isText(prev)) { + var newChild = { + text: '' + }; + Transforms.insertNodes(editor, newChild, { + at: path.concat(n), + voids: true + }); + n++; + } else if (isLast) { + var _newChild = { + text: '' + }; + Transforms.insertNodes(editor, _newChild, { + at: path.concat(n + 1), + voids: true + }); + n++; + } + } + } else { + // Merge adjacent text nodes that are empty or match. + if (prev != null && Text.isText(prev)) { + if (Text.equals(_child, prev, { + loose: true + })) { + Transforms.mergeNodes(editor, { + at: path.concat(n), + voids: true + }); + n--; + } else if (prev.text === '') { + Transforms.removeNodes(editor, { + at: path.concat(n - 1), + voids: true + }); + n--; + } else if (_child.text === '') { + Transforms.removeNodes(editor, { + at: path.concat(n), + voids: true + }); + n--; + } + } + } + } + }, + removeMark: function removeMark(key) { + var selection = editor.selection; + + if (selection) { + if (Range.isExpanded(selection)) { + Transforms.unsetNodes(editor, key, { + match: Text.isText, + split: true + }); + } else { + var marks = _objectSpread$9({}, Editor.marks(editor) || {}); + + delete marks[key]; + editor.marks = marks; + + if (!FLUSHING.get(editor)) { + editor.onChange(); + } + } + } + } + }; + return editor; + }; + /** + * Get the "dirty" paths generated from an operation. + */ + + var getDirtyPaths = function getDirtyPaths(op) { + switch (op.type) { + case 'insert_text': + case 'remove_text': + case 'set_node': + { + var path = op.path; + return Path.levels(path); + } + + case 'insert_node': + { + var node = op.node, + _path2 = op.path; + var levels = Path.levels(_path2); + var descendants = Text.isText(node) ? [] : Array.from(Node$1.nodes(node), function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + p = _ref4[1]; + + return _path2.concat(p); + }); + return [].concat(_toConsumableArray(levels), _toConsumableArray(descendants)); + } + + case 'merge_node': + { + var _path3 = op.path; + var ancestors = Path.ancestors(_path3); + var previousPath = Path.previous(_path3); + return [].concat(_toConsumableArray(ancestors), [previousPath]); + } + + case 'move_node': + { + var _path4 = op.path, + newPath = op.newPath; + + if (Path.equals(_path4, newPath)) { + return []; + } + + var oldAncestors = []; + var newAncestors = []; + + var _iterator6 = _createForOfIteratorHelper$7(Path.ancestors(_path4)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var ancestor = _step6.value; + var p = Path.transform(ancestor, op); + oldAncestors.push(p); + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + + var _iterator7 = _createForOfIteratorHelper$7(Path.ancestors(newPath)), + _step7; + + try { + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var _ancestor = _step7.value; + + var _p = Path.transform(_ancestor, op); + + newAncestors.push(_p); + } + } catch (err) { + _iterator7.e(err); + } finally { + _iterator7.f(); + } + + var newParent = newAncestors[newAncestors.length - 1]; + var newIndex = newPath[newPath.length - 1]; + var resultPath = newParent.concat(newIndex); + return [].concat(oldAncestors, newAncestors, [resultPath]); + } + + case 'remove_node': + { + var _path5 = op.path; + + var _ancestors = Path.ancestors(_path5); + + return _toConsumableArray(_ancestors); + } + + case 'split_node': + { + var _path6 = op.path; + + var _levels = Path.levels(_path6); + + var nextPath = Path.next(_path6); + return [].concat(_toConsumableArray(_levels), [nextPath]); + } + + default: + { + return []; + } + } + }; + + var objectWithoutPropertiesLoose = createCommonjsModule(function (module) { + function _objectWithoutPropertiesLoose(source, excluded) { + if (source == null) return {}; + var target = {}; + var sourceKeys = Object.keys(source); + var key, i; + + for (i = 0; i < sourceKeys.length; i++) { + key = sourceKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + target[key] = source[key]; + } + + return target; + } + + module.exports = _objectWithoutPropertiesLoose; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + unwrapExports(objectWithoutPropertiesLoose); + + var objectWithoutProperties = createCommonjsModule(function (module) { + function _objectWithoutProperties(source, excluded) { + if (source == null) return {}; + var target = objectWithoutPropertiesLoose(source, excluded); + var key, i; + + if (Object.getOwnPropertySymbols) { + var sourceSymbolKeys = Object.getOwnPropertySymbols(source); + + for (i = 0; i < sourceSymbolKeys.length; i++) { + key = sourceSymbolKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; + target[key] = source[key]; + } + } + + return target; + } + + module.exports = _objectWithoutProperties; + module.exports["default"] = module.exports, module.exports.__esModule = true; + }); + + var _objectWithoutProperties = unwrapExports(objectWithoutProperties); + + function _createForOfIteratorHelper$6(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$6(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$6(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$6(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$6(o, minLen); } + + function _arrayLikeToArray$6(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + + // Character (grapheme cluster) boundaries are determined according to + // the default grapheme cluster boundary specification, extended grapheme clusters variant[1]. + // + // References: + // + // [1] https://www.unicode.org/reports/tr29/#Default_Grapheme_Cluster_Table + // [2] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakProperty.txt + // [3] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakTest.html + // [4] https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakTest.txt + + /** + * Get the distance to the end of the first character in a string of text. + */ + var getCharacterDistance = function getCharacterDistance(str) { + var isRTL = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var isLTR = !isRTL; + var codepoints = isRTL ? codepointsIteratorRTL(str) : str; + var left = CodepointType.None; + var right = CodepointType.None; + var distance = 0; // Evaluation of these conditions are deferred. + + var gb11 = null; // Is GB11 applicable? + + var gb12Or13 = null; // Is GB12 or GB13 applicable? + + var _iterator = _createForOfIteratorHelper$6(codepoints), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _char = _step.value; + + var code = _char.codePointAt(0); + + if (!code) break; + var type = getCodepointType(_char, code); + + var _ref = isLTR ? [right, type] : [type, left]; + + var _ref2 = _slicedToArray(_ref, 2); + + left = _ref2[0]; + right = _ref2[1]; + + if (intersects(left, CodepointType.ZWJ) && intersects(right, CodepointType.ExtPict)) { + if (isLTR) { + gb11 = endsWithEmojiZWJ(str.substring(0, distance)); + } else { + gb11 = endsWithEmojiZWJ(str.substring(0, str.length - distance)); + } + + if (!gb11) break; + } + + if (intersects(left, CodepointType.RI) && intersects(right, CodepointType.RI)) { + if (gb12Or13 !== null) { + gb12Or13 = !gb12Or13; + } else { + if (isLTR) { + gb12Or13 = true; + } else { + gb12Or13 = endsWithOddNumberOfRIs(str.substring(0, str.length - distance)); + } + } + + if (!gb12Or13) break; + } + + if (left !== CodepointType.None && right !== CodepointType.None && isBoundaryPair(left, right)) { + break; + } + + distance += _char.length; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + return distance || 1; + }; + var SPACE = /\s/; + var PUNCTUATION = /[\u0021-\u0023\u0025-\u002A\u002C-\u002F\u003A\u003B\u003F\u0040\u005B-\u005D\u005F\u007B\u007D\u00A1\u00A7\u00AB\u00B6\u00B7\u00BB\u00BF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E3B\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]/; + var CHAMELEON = /['\u2018\u2019]/; + /** + * Get the distance to the end of the first word in a string of text. + */ + + var getWordDistance = function getWordDistance(text) { + var isRTL = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var dist = 0; + var started = false; + + while (text.length > 0) { + var charDist = getCharacterDistance(text, isRTL); + + var _splitByCharacterDist = splitByCharacterDistance(text, charDist, isRTL), + _splitByCharacterDist2 = _slicedToArray(_splitByCharacterDist, 2), + _char2 = _splitByCharacterDist2[0], + remaining = _splitByCharacterDist2[1]; + + if (isWordCharacter(_char2, remaining, isRTL)) { + started = true; + dist += charDist; + } else if (!started) { + dist += charDist; + } else { + break; + } + + text = remaining; + } + + return dist; + }; + /** + * Split a string in two parts at a given distance starting from the end when + * `isRTL` is set to `true`. + */ + + var splitByCharacterDistance = function splitByCharacterDistance(str, dist, isRTL) { + if (isRTL) { + var at = str.length - dist; + return [str.slice(at, str.length), str.slice(0, at)]; + } + + return [str.slice(0, dist), str.slice(dist)]; + }; + /** + * Check if a character is a word character. The `remaining` argument is used + * because sometimes you must read subsequent characters to truly determine it. + */ + + var isWordCharacter = function isWordCharacter(_char3, remaining) { + var isRTL = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + if (SPACE.test(_char3)) { + return false; + } // Chameleons count as word characters as long as they're in a word, so + // recurse to see if the next one is a word character or not. + + + if (CHAMELEON.test(_char3)) { + var charDist = getCharacterDistance(remaining, isRTL); + + var _splitByCharacterDist3 = splitByCharacterDistance(remaining, charDist, isRTL), + _splitByCharacterDist4 = _slicedToArray(_splitByCharacterDist3, 2), + nextChar = _splitByCharacterDist4[0], + nextRemaining = _splitByCharacterDist4[1]; + + if (isWordCharacter(nextChar, nextRemaining, isRTL)) { + return true; + } + } + + if (PUNCTUATION.test(_char3)) { + return false; + } + + return true; + }; + /** + * Iterate on codepoints from right to left. + */ + + + var codepointsIteratorRTL = function* codepointsIteratorRTL(str) { + var end = str.length - 1; + + for (var i = 0; i < str.length; i++) { + var char1 = str.charAt(end - i); + + if (isLowSurrogate(char1.charCodeAt(0))) { + var char2 = str.charAt(end - i - 1); + + if (isHighSurrogate(char2.charCodeAt(0))) { + yield char2 + char1; + i++; + continue; + } + } + + yield char1; + } + }; + /** + * Is `charCode` a high surrogate. + * + * https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + */ + + var isHighSurrogate = function isHighSurrogate(charCode) { + return charCode >= 0xd800 && charCode <= 0xdbff; + }; + /** + * Is `charCode` a low surrogate. + * + * https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates + */ + + + var isLowSurrogate = function isLowSurrogate(charCode) { + return charCode >= 0xdc00 && charCode <= 0xdfff; + }; + + var CodepointType; + + (function (CodepointType) { + CodepointType[CodepointType["None"] = 0] = "None"; + CodepointType[CodepointType["Extend"] = 1] = "Extend"; + CodepointType[CodepointType["ZWJ"] = 2] = "ZWJ"; + CodepointType[CodepointType["RI"] = 4] = "RI"; + CodepointType[CodepointType["Prepend"] = 8] = "Prepend"; + CodepointType[CodepointType["SpacingMark"] = 16] = "SpacingMark"; + CodepointType[CodepointType["L"] = 32] = "L"; + CodepointType[CodepointType["V"] = 64] = "V"; + CodepointType[CodepointType["T"] = 128] = "T"; + CodepointType[CodepointType["LV"] = 256] = "LV"; + CodepointType[CodepointType["LVT"] = 512] = "LVT"; + CodepointType[CodepointType["ExtPict"] = 1024] = "ExtPict"; + CodepointType[CodepointType["Any"] = 2048] = "Any"; + })(CodepointType || (CodepointType = {})); + + var reExtend = /^(?:[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D3E\u0D41-\u0D44\u0D4D\u0D57\u0D62\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1AC0\u1B00-\u1B03\u1B34-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200C\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E\uFF9F]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD803[\uDD24-\uDD27\uDEAB\uDEAC\uDF46-\uDF50]|\uD804[\uDC01\uDC38-\uDC46\uDC7F-\uDC81\uDCB3-\uDCB6\uDCB9\uDCBA\uDD00-\uDD02\uDD27-\uDD2B\uDD2D-\uDD34\uDD73\uDD80\uDD81\uDDB6-\uDDBE\uDDC9-\uDDCC\uDDCF\uDE2F-\uDE31\uDE34\uDE36\uDE37\uDE3E\uDEDF\uDEE3-\uDEEA\uDF00\uDF01\uDF3B\uDF3C\uDF3E\uDF40\uDF57\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC38-\uDC3F\uDC42-\uDC44\uDC46\uDC5E\uDCB0\uDCB3-\uDCB8\uDCBA\uDCBD\uDCBF\uDCC0\uDCC2\uDCC3\uDDAF\uDDB2-\uDDB5\uDDBC\uDDBD\uDDBF\uDDC0\uDDDC\uDDDD\uDE33-\uDE3A\uDE3D\uDE3F\uDE40\uDEAB\uDEAD\uDEB0-\uDEB5\uDEB7\uDF1D-\uDF1F\uDF22-\uDF25\uDF27-\uDF2B]|\uD806[\uDC2F-\uDC37\uDC39\uDC3A\uDD30\uDD3B\uDD3C\uDD3E\uDD43\uDDD4-\uDDD7\uDDDA\uDDDB\uDDE0\uDE01-\uDE0A\uDE33-\uDE38\uDE3B-\uDE3E\uDE47\uDE51-\uDE56\uDE59-\uDE5B\uDE8A-\uDE96\uDE98\uDE99]|\uD807[\uDC30-\uDC36\uDC38-\uDC3D\uDC3F\uDC92-\uDCA7\uDCAA-\uDCB0\uDCB2\uDCB3\uDCB5\uDCB6\uDD31-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD45\uDD47\uDD90\uDD91\uDD95\uDD97\uDEF3\uDEF4]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF4F\uDF8F-\uDF92\uDFE4]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65\uDD67-\uDD69\uDD6E-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDD30-\uDD36\uDEEC-\uDEEF]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uD83C[\uDFFB-\uDFFF]|\uDB40[\uDC20-\uDC7F\uDD00-\uDDEF])$/; + var rePrepend = /^(?:[\u0600-\u0605\u06DD\u070F\u0890\u0891\u08E2\u0D4E]|\uD804[\uDCBD\uDCCD\uDDC2\uDDC3]|\uD806[\uDD3F\uDD41\uDE3A\uDE84-\uDE89]|\uD807\uDD46)$/; + var reSpacingMark = /^(?:[\u0903\u093B\u093E-\u0940\u0949-\u094C\u094E\u094F\u0982\u0983\u09BF\u09C0\u09C7\u09C8\u09CB\u09CC\u0A03\u0A3E-\u0A40\u0A83\u0ABE-\u0AC0\u0AC9\u0ACB\u0ACC\u0B02\u0B03\u0B40\u0B47\u0B48\u0B4B\u0B4C\u0BBF\u0BC1\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0C01-\u0C03\u0C41-\u0C44\u0C82\u0C83\u0CBE\u0CC0\u0CC1\u0CC3\u0CC4\u0CC7\u0CC8\u0CCA\u0CCB\u0D02\u0D03\u0D3F\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D82\u0D83\u0DD0\u0DD1\u0DD8-\u0DDE\u0DF2\u0DF3\u0E33\u0EB3\u0F3E\u0F3F\u0F7F\u1031\u103B\u103C\u1056\u1057\u1084\u1715\u1734\u17B6\u17BE-\u17C5\u17C7\u17C8\u1923-\u1926\u1929-\u192B\u1930\u1931\u1933-\u1938\u1A19\u1A1A\u1A55\u1A57\u1A6D-\u1A72\u1B04\u1B3B\u1B3D-\u1B41\u1B43\u1B44\u1B82\u1BA1\u1BA6\u1BA7\u1BAA\u1BE7\u1BEA-\u1BEC\u1BEE\u1BF2\u1BF3\u1C24-\u1C2B\u1C34\u1C35\u1CE1\u1CF7\uA823\uA824\uA827\uA880\uA881\uA8B4-\uA8C3\uA952\uA953\uA983\uA9B4\uA9B5\uA9BA\uA9BB\uA9BE-\uA9C0\uAA2F\uAA30\uAA33\uAA34\uAA4D\uAAEB\uAAEE\uAAEF\uAAF5\uABE3\uABE4\uABE6\uABE7\uABE9\uABEA\uABEC]|\uD804[\uDC00\uDC02\uDC82\uDCB0-\uDCB2\uDCB7\uDCB8\uDD2C\uDD45\uDD46\uDD82\uDDB3-\uDDB5\uDDBF\uDDC0\uDDCE\uDE2C-\uDE2E\uDE32\uDE33\uDE35\uDEE0-\uDEE2\uDF02\uDF03\uDF3F\uDF41-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF62\uDF63]|\uD805[\uDC35-\uDC37\uDC40\uDC41\uDC45\uDCB1\uDCB2\uDCB9\uDCBB\uDCBC\uDCBE\uDCC1\uDDB0\uDDB1\uDDB8-\uDDBB\uDDBE\uDE30-\uDE32\uDE3B\uDE3C\uDE3E\uDEAC\uDEAE\uDEAF\uDEB6\uDF26]|\uD806[\uDC2C-\uDC2E\uDC38\uDD31-\uDD35\uDD37\uDD38\uDD3D\uDD40\uDD42\uDDD1-\uDDD3\uDDDC-\uDDDF\uDDE4\uDE39\uDE57\uDE58\uDE97]|\uD807[\uDC2F\uDC3E\uDCA9\uDCB1\uDCB4\uDD8A-\uDD8E\uDD93\uDD94\uDD96\uDEF5\uDEF6]|\uD81B[\uDF51-\uDF87\uDFF0\uDFF1]|\uD834[\uDD66\uDD6D])$/; + var reL = /^[\u1100-\u115F\uA960-\uA97C]$/; + var reV = /^[\u1160-\u11A7\uD7B0-\uD7C6]$/; + var reT = /^[\u11A8-\u11FF\uD7CB-\uD7FB]$/; + var reLV = /^[\uAC00\uAC1C\uAC38\uAC54\uAC70\uAC8C\uACA8\uACC4\uACE0\uACFC\uAD18\uAD34\uAD50\uAD6C\uAD88\uADA4\uADC0\uADDC\uADF8\uAE14\uAE30\uAE4C\uAE68\uAE84\uAEA0\uAEBC\uAED8\uAEF4\uAF10\uAF2C\uAF48\uAF64\uAF80\uAF9C\uAFB8\uAFD4\uAFF0\uB00C\uB028\uB044\uB060\uB07C\uB098\uB0B4\uB0D0\uB0EC\uB108\uB124\uB140\uB15C\uB178\uB194\uB1B0\uB1CC\uB1E8\uB204\uB220\uB23C\uB258\uB274\uB290\uB2AC\uB2C8\uB2E4\uB300\uB31C\uB338\uB354\uB370\uB38C\uB3A8\uB3C4\uB3E0\uB3FC\uB418\uB434\uB450\uB46C\uB488\uB4A4\uB4C0\uB4DC\uB4F8\uB514\uB530\uB54C\uB568\uB584\uB5A0\uB5BC\uB5D8\uB5F4\uB610\uB62C\uB648\uB664\uB680\uB69C\uB6B8\uB6D4\uB6F0\uB70C\uB728\uB744\uB760\uB77C\uB798\uB7B4\uB7D0\uB7EC\uB808\uB824\uB840\uB85C\uB878\uB894\uB8B0\uB8CC\uB8E8\uB904\uB920\uB93C\uB958\uB974\uB990\uB9AC\uB9C8\uB9E4\uBA00\uBA1C\uBA38\uBA54\uBA70\uBA8C\uBAA8\uBAC4\uBAE0\uBAFC\uBB18\uBB34\uBB50\uBB6C\uBB88\uBBA4\uBBC0\uBBDC\uBBF8\uBC14\uBC30\uBC4C\uBC68\uBC84\uBCA0\uBCBC\uBCD8\uBCF4\uBD10\uBD2C\uBD48\uBD64\uBD80\uBD9C\uBDB8\uBDD4\uBDF0\uBE0C\uBE28\uBE44\uBE60\uBE7C\uBE98\uBEB4\uBED0\uBEEC\uBF08\uBF24\uBF40\uBF5C\uBF78\uBF94\uBFB0\uBFCC\uBFE8\uC004\uC020\uC03C\uC058\uC074\uC090\uC0AC\uC0C8\uC0E4\uC100\uC11C\uC138\uC154\uC170\uC18C\uC1A8\uC1C4\uC1E0\uC1FC\uC218\uC234\uC250\uC26C\uC288\uC2A4\uC2C0\uC2DC\uC2F8\uC314\uC330\uC34C\uC368\uC384\uC3A0\uC3BC\uC3D8\uC3F4\uC410\uC42C\uC448\uC464\uC480\uC49C\uC4B8\uC4D4\uC4F0\uC50C\uC528\uC544\uC560\uC57C\uC598\uC5B4\uC5D0\uC5EC\uC608\uC624\uC640\uC65C\uC678\uC694\uC6B0\uC6CC\uC6E8\uC704\uC720\uC73C\uC758\uC774\uC790\uC7AC\uC7C8\uC7E4\uC800\uC81C\uC838\uC854\uC870\uC88C\uC8A8\uC8C4\uC8E0\uC8FC\uC918\uC934\uC950\uC96C\uC988\uC9A4\uC9C0\uC9DC\uC9F8\uCA14\uCA30\uCA4C\uCA68\uCA84\uCAA0\uCABC\uCAD8\uCAF4\uCB10\uCB2C\uCB48\uCB64\uCB80\uCB9C\uCBB8\uCBD4\uCBF0\uCC0C\uCC28\uCC44\uCC60\uCC7C\uCC98\uCCB4\uCCD0\uCCEC\uCD08\uCD24\uCD40\uCD5C\uCD78\uCD94\uCDB0\uCDCC\uCDE8\uCE04\uCE20\uCE3C\uCE58\uCE74\uCE90\uCEAC\uCEC8\uCEE4\uCF00\uCF1C\uCF38\uCF54\uCF70\uCF8C\uCFA8\uCFC4\uCFE0\uCFFC\uD018\uD034\uD050\uD06C\uD088\uD0A4\uD0C0\uD0DC\uD0F8\uD114\uD130\uD14C\uD168\uD184\uD1A0\uD1BC\uD1D8\uD1F4\uD210\uD22C\uD248\uD264\uD280\uD29C\uD2B8\uD2D4\uD2F0\uD30C\uD328\uD344\uD360\uD37C\uD398\uD3B4\uD3D0\uD3EC\uD408\uD424\uD440\uD45C\uD478\uD494\uD4B0\uD4CC\uD4E8\uD504\uD520\uD53C\uD558\uD574\uD590\uD5AC\uD5C8\uD5E4\uD600\uD61C\uD638\uD654\uD670\uD68C\uD6A8\uD6C4\uD6E0\uD6FC\uD718\uD734\uD750\uD76C\uD788]$/; + var reLVT = /^[\uAC01-\uAC1B\uAC1D-\uAC37\uAC39-\uAC53\uAC55-\uAC6F\uAC71-\uAC8B\uAC8D-\uACA7\uACA9-\uACC3\uACC5-\uACDF\uACE1-\uACFB\uACFD-\uAD17\uAD19-\uAD33\uAD35-\uAD4F\uAD51-\uAD6B\uAD6D-\uAD87\uAD89-\uADA3\uADA5-\uADBF\uADC1-\uADDB\uADDD-\uADF7\uADF9-\uAE13\uAE15-\uAE2F\uAE31-\uAE4B\uAE4D-\uAE67\uAE69-\uAE83\uAE85-\uAE9F\uAEA1-\uAEBB\uAEBD-\uAED7\uAED9-\uAEF3\uAEF5-\uAF0F\uAF11-\uAF2B\uAF2D-\uAF47\uAF49-\uAF63\uAF65-\uAF7F\uAF81-\uAF9B\uAF9D-\uAFB7\uAFB9-\uAFD3\uAFD5-\uAFEF\uAFF1-\uB00B\uB00D-\uB027\uB029-\uB043\uB045-\uB05F\uB061-\uB07B\uB07D-\uB097\uB099-\uB0B3\uB0B5-\uB0CF\uB0D1-\uB0EB\uB0ED-\uB107\uB109-\uB123\uB125-\uB13F\uB141-\uB15B\uB15D-\uB177\uB179-\uB193\uB195-\uB1AF\uB1B1-\uB1CB\uB1CD-\uB1E7\uB1E9-\uB203\uB205-\uB21F\uB221-\uB23B\uB23D-\uB257\uB259-\uB273\uB275-\uB28F\uB291-\uB2AB\uB2AD-\uB2C7\uB2C9-\uB2E3\uB2E5-\uB2FF\uB301-\uB31B\uB31D-\uB337\uB339-\uB353\uB355-\uB36F\uB371-\uB38B\uB38D-\uB3A7\uB3A9-\uB3C3\uB3C5-\uB3DF\uB3E1-\uB3FB\uB3FD-\uB417\uB419-\uB433\uB435-\uB44F\uB451-\uB46B\uB46D-\uB487\uB489-\uB4A3\uB4A5-\uB4BF\uB4C1-\uB4DB\uB4DD-\uB4F7\uB4F9-\uB513\uB515-\uB52F\uB531-\uB54B\uB54D-\uB567\uB569-\uB583\uB585-\uB59F\uB5A1-\uB5BB\uB5BD-\uB5D7\uB5D9-\uB5F3\uB5F5-\uB60F\uB611-\uB62B\uB62D-\uB647\uB649-\uB663\uB665-\uB67F\uB681-\uB69B\uB69D-\uB6B7\uB6B9-\uB6D3\uB6D5-\uB6EF\uB6F1-\uB70B\uB70D-\uB727\uB729-\uB743\uB745-\uB75F\uB761-\uB77B\uB77D-\uB797\uB799-\uB7B3\uB7B5-\uB7CF\uB7D1-\uB7EB\uB7ED-\uB807\uB809-\uB823\uB825-\uB83F\uB841-\uB85B\uB85D-\uB877\uB879-\uB893\uB895-\uB8AF\uB8B1-\uB8CB\uB8CD-\uB8E7\uB8E9-\uB903\uB905-\uB91F\uB921-\uB93B\uB93D-\uB957\uB959-\uB973\uB975-\uB98F\uB991-\uB9AB\uB9AD-\uB9C7\uB9C9-\uB9E3\uB9E5-\uB9FF\uBA01-\uBA1B\uBA1D-\uBA37\uBA39-\uBA53\uBA55-\uBA6F\uBA71-\uBA8B\uBA8D-\uBAA7\uBAA9-\uBAC3\uBAC5-\uBADF\uBAE1-\uBAFB\uBAFD-\uBB17\uBB19-\uBB33\uBB35-\uBB4F\uBB51-\uBB6B\uBB6D-\uBB87\uBB89-\uBBA3\uBBA5-\uBBBF\uBBC1-\uBBDB\uBBDD-\uBBF7\uBBF9-\uBC13\uBC15-\uBC2F\uBC31-\uBC4B\uBC4D-\uBC67\uBC69-\uBC83\uBC85-\uBC9F\uBCA1-\uBCBB\uBCBD-\uBCD7\uBCD9-\uBCF3\uBCF5-\uBD0F\uBD11-\uBD2B\uBD2D-\uBD47\uBD49-\uBD63\uBD65-\uBD7F\uBD81-\uBD9B\uBD9D-\uBDB7\uBDB9-\uBDD3\uBDD5-\uBDEF\uBDF1-\uBE0B\uBE0D-\uBE27\uBE29-\uBE43\uBE45-\uBE5F\uBE61-\uBE7B\uBE7D-\uBE97\uBE99-\uBEB3\uBEB5-\uBECF\uBED1-\uBEEB\uBEED-\uBF07\uBF09-\uBF23\uBF25-\uBF3F\uBF41-\uBF5B\uBF5D-\uBF77\uBF79-\uBF93\uBF95-\uBFAF\uBFB1-\uBFCB\uBFCD-\uBFE7\uBFE9-\uC003\uC005-\uC01F\uC021-\uC03B\uC03D-\uC057\uC059-\uC073\uC075-\uC08F\uC091-\uC0AB\uC0AD-\uC0C7\uC0C9-\uC0E3\uC0E5-\uC0FF\uC101-\uC11B\uC11D-\uC137\uC139-\uC153\uC155-\uC16F\uC171-\uC18B\uC18D-\uC1A7\uC1A9-\uC1C3\uC1C5-\uC1DF\uC1E1-\uC1FB\uC1FD-\uC217\uC219-\uC233\uC235-\uC24F\uC251-\uC26B\uC26D-\uC287\uC289-\uC2A3\uC2A5-\uC2BF\uC2C1-\uC2DB\uC2DD-\uC2F7\uC2F9-\uC313\uC315-\uC32F\uC331-\uC34B\uC34D-\uC367\uC369-\uC383\uC385-\uC39F\uC3A1-\uC3BB\uC3BD-\uC3D7\uC3D9-\uC3F3\uC3F5-\uC40F\uC411-\uC42B\uC42D-\uC447\uC449-\uC463\uC465-\uC47F\uC481-\uC49B\uC49D-\uC4B7\uC4B9-\uC4D3\uC4D5-\uC4EF\uC4F1-\uC50B\uC50D-\uC527\uC529-\uC543\uC545-\uC55F\uC561-\uC57B\uC57D-\uC597\uC599-\uC5B3\uC5B5-\uC5CF\uC5D1-\uC5EB\uC5ED-\uC607\uC609-\uC623\uC625-\uC63F\uC641-\uC65B\uC65D-\uC677\uC679-\uC693\uC695-\uC6AF\uC6B1-\uC6CB\uC6CD-\uC6E7\uC6E9-\uC703\uC705-\uC71F\uC721-\uC73B\uC73D-\uC757\uC759-\uC773\uC775-\uC78F\uC791-\uC7AB\uC7AD-\uC7C7\uC7C9-\uC7E3\uC7E5-\uC7FF\uC801-\uC81B\uC81D-\uC837\uC839-\uC853\uC855-\uC86F\uC871-\uC88B\uC88D-\uC8A7\uC8A9-\uC8C3\uC8C5-\uC8DF\uC8E1-\uC8FB\uC8FD-\uC917\uC919-\uC933\uC935-\uC94F\uC951-\uC96B\uC96D-\uC987\uC989-\uC9A3\uC9A5-\uC9BF\uC9C1-\uC9DB\uC9DD-\uC9F7\uC9F9-\uCA13\uCA15-\uCA2F\uCA31-\uCA4B\uCA4D-\uCA67\uCA69-\uCA83\uCA85-\uCA9F\uCAA1-\uCABB\uCABD-\uCAD7\uCAD9-\uCAF3\uCAF5-\uCB0F\uCB11-\uCB2B\uCB2D-\uCB47\uCB49-\uCB63\uCB65-\uCB7F\uCB81-\uCB9B\uCB9D-\uCBB7\uCBB9-\uCBD3\uCBD5-\uCBEF\uCBF1-\uCC0B\uCC0D-\uCC27\uCC29-\uCC43\uCC45-\uCC5F\uCC61-\uCC7B\uCC7D-\uCC97\uCC99-\uCCB3\uCCB5-\uCCCF\uCCD1-\uCCEB\uCCED-\uCD07\uCD09-\uCD23\uCD25-\uCD3F\uCD41-\uCD5B\uCD5D-\uCD77\uCD79-\uCD93\uCD95-\uCDAF\uCDB1-\uCDCB\uCDCD-\uCDE7\uCDE9-\uCE03\uCE05-\uCE1F\uCE21-\uCE3B\uCE3D-\uCE57\uCE59-\uCE73\uCE75-\uCE8F\uCE91-\uCEAB\uCEAD-\uCEC7\uCEC9-\uCEE3\uCEE5-\uCEFF\uCF01-\uCF1B\uCF1D-\uCF37\uCF39-\uCF53\uCF55-\uCF6F\uCF71-\uCF8B\uCF8D-\uCFA7\uCFA9-\uCFC3\uCFC5-\uCFDF\uCFE1-\uCFFB\uCFFD-\uD017\uD019-\uD033\uD035-\uD04F\uD051-\uD06B\uD06D-\uD087\uD089-\uD0A3\uD0A5-\uD0BF\uD0C1-\uD0DB\uD0DD-\uD0F7\uD0F9-\uD113\uD115-\uD12F\uD131-\uD14B\uD14D-\uD167\uD169-\uD183\uD185-\uD19F\uD1A1-\uD1BB\uD1BD-\uD1D7\uD1D9-\uD1F3\uD1F5-\uD20F\uD211-\uD22B\uD22D-\uD247\uD249-\uD263\uD265-\uD27F\uD281-\uD29B\uD29D-\uD2B7\uD2B9-\uD2D3\uD2D5-\uD2EF\uD2F1-\uD30B\uD30D-\uD327\uD329-\uD343\uD345-\uD35F\uD361-\uD37B\uD37D-\uD397\uD399-\uD3B3\uD3B5-\uD3CF\uD3D1-\uD3EB\uD3ED-\uD407\uD409-\uD423\uD425-\uD43F\uD441-\uD45B\uD45D-\uD477\uD479-\uD493\uD495-\uD4AF\uD4B1-\uD4CB\uD4CD-\uD4E7\uD4E9-\uD503\uD505-\uD51F\uD521-\uD53B\uD53D-\uD557\uD559-\uD573\uD575-\uD58F\uD591-\uD5AB\uD5AD-\uD5C7\uD5C9-\uD5E3\uD5E5-\uD5FF\uD601-\uD61B\uD61D-\uD637\uD639-\uD653\uD655-\uD66F\uD671-\uD68B\uD68D-\uD6A7\uD6A9-\uD6C3\uD6C5-\uD6DF\uD6E1-\uD6FB\uD6FD-\uD717\uD719-\uD733\uD735-\uD74F\uD751-\uD76B\uD76D-\uD787\uD789-\uD7A3]$/; + var reExtPict = /^(?:[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u2388\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2605\u2607-\u2612\u2614-\u2685\u2690-\u2705\u2708-\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763-\u2767\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC00-\uDCFF\uDD0D-\uDD0F\uDD2F\uDD6C-\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDAD-\uDDE5\uDE01-\uDE0F\uDE1A\uDE2F\uDE32-\uDE3A\uDE3C-\uDE3F\uDE49-\uDFFA]|\uD83D[\uDC00-\uDD3D\uDD46-\uDE4F\uDE80-\uDEFF\uDF74-\uDF7F\uDFD5-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE-\uDCFF\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDEFF]|\uD83F[\uDC00-\uDFFD])$/; + + var getCodepointType = function getCodepointType(_char4, code) { + var type = CodepointType.Any; + + if (_char4.search(reExtend) !== -1) { + type |= CodepointType.Extend; + } + + if (code === 0x200d) { + type |= CodepointType.ZWJ; + } + + if (code >= 0x1f1e6 && code <= 0x1f1ff) { + type |= CodepointType.RI; + } + + if (_char4.search(rePrepend) !== -1) { + type |= CodepointType.Prepend; + } + + if (_char4.search(reSpacingMark) !== -1) { + type |= CodepointType.SpacingMark; + } + + if (_char4.search(reL) !== -1) { + type |= CodepointType.L; + } + + if (_char4.search(reV) !== -1) { + type |= CodepointType.V; + } + + if (_char4.search(reT) !== -1) { + type |= CodepointType.T; + } + + if (_char4.search(reLV) !== -1) { + type |= CodepointType.LV; + } + + if (_char4.search(reLVT) !== -1) { + type |= CodepointType.LVT; + } + + if (_char4.search(reExtPict) !== -1) { + type |= CodepointType.ExtPict; + } + + return type; + }; + + function intersects(x, y) { + return (x & y) !== 0; + } + + var NonBoundaryPairs = [// GB6 + [CodepointType.L, CodepointType.L | CodepointType.V | CodepointType.LV | CodepointType.LVT], // GB7 + [CodepointType.LV | CodepointType.V, CodepointType.V | CodepointType.T], // GB8 + [CodepointType.LVT | CodepointType.T, CodepointType.T], // GB9 + [CodepointType.Any, CodepointType.Extend | CodepointType.ZWJ], // GB9a + [CodepointType.Any, CodepointType.SpacingMark], // GB9b + [CodepointType.Prepend, CodepointType.Any], // GB11 + [CodepointType.ZWJ, CodepointType.ExtPict], // GB12 and GB13 + [CodepointType.RI, CodepointType.RI]]; + + function isBoundaryPair(left, right) { + return NonBoundaryPairs.findIndex(function (r) { + return intersects(left, r[0]) && intersects(right, r[1]); + }) === -1; + } + + var endingEmojiZWJ = /(?:[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u2388\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2605\u2607-\u2612\u2614-\u2685\u2690-\u2705\u2708-\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763-\u2767\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC00-\uDCFF\uDD0D-\uDD0F\uDD2F\uDD6C-\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDAD-\uDDE5\uDE01-\uDE0F\uDE1A\uDE2F\uDE32-\uDE3A\uDE3C-\uDE3F\uDE49-\uDFFA]|\uD83D[\uDC00-\uDD3D\uDD46-\uDE4F\uDE80-\uDEFF\uDF74-\uDF7F\uDFD5-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE-\uDCFF\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDEFF]|\uD83F[\uDC00-\uDFFD])(?:[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08D3-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2\u09E3\u09FE\u0A01\u0A02\u0A3C\u0A41\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7\u0AC8\u0ACD\u0AE2\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B55-\u0B57\u0B62\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C81\u0CBC\u0CBF\u0CC2\u0CC6\u0CCC\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D00\u0D01\u0D3B\u0D3C\u0D3E\u0D41-\u0D44\u0D4D\u0D57\u0D62\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039\u103A\u103D\u103E\u1058\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u1885\u1886\u18A9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193B\u1A17\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1AC0\u1B00-\u1B03\u1B34-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80\u1B81\u1BA2-\u1BA5\u1BA8\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8\u1CF9\u1DC0-\u1DF9\u1DFB-\u1DFF\u200C\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69E\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA825\uA826\uA82C\uA8C4\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9BD\uA9E5\uAA29-\uAA2E\uAA31\uAA32\uAA35\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEC\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E\uFF9F]|\uD800[\uDDFD\uDEE0\uDF76-\uDF7A]|\uD802[\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F\uDEE5\uDEE6]|\uD803[\uDD24-\uDD27\uDEAB\uDEAC\uDF46-\uDF50]|\uD804[\uDC01\uDC38-\uDC46\uDC7F-\uDC81\uDCB3-\uDCB6\uDCB9\uDCBA\uDD00-\uDD02\uDD27-\uDD2B\uDD2D-\uDD34\uDD73\uDD80\uDD81\uDDB6-\uDDBE\uDDC9-\uDDCC\uDDCF\uDE2F-\uDE31\uDE34\uDE36\uDE37\uDE3E\uDEDF\uDEE3-\uDEEA\uDF00\uDF01\uDF3B\uDF3C\uDF3E\uDF40\uDF57\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC38-\uDC3F\uDC42-\uDC44\uDC46\uDC5E\uDCB0\uDCB3-\uDCB8\uDCBA\uDCBD\uDCBF\uDCC0\uDCC2\uDCC3\uDDAF\uDDB2-\uDDB5\uDDBC\uDDBD\uDDBF\uDDC0\uDDDC\uDDDD\uDE33-\uDE3A\uDE3D\uDE3F\uDE40\uDEAB\uDEAD\uDEB0-\uDEB5\uDEB7\uDF1D-\uDF1F\uDF22-\uDF25\uDF27-\uDF2B]|\uD806[\uDC2F-\uDC37\uDC39\uDC3A\uDD30\uDD3B\uDD3C\uDD3E\uDD43\uDDD4-\uDDD7\uDDDA\uDDDB\uDDE0\uDE01-\uDE0A\uDE33-\uDE38\uDE3B-\uDE3E\uDE47\uDE51-\uDE56\uDE59-\uDE5B\uDE8A-\uDE96\uDE98\uDE99]|\uD807[\uDC30-\uDC36\uDC38-\uDC3D\uDC3F\uDC92-\uDCA7\uDCAA-\uDCB0\uDCB2\uDCB3\uDCB5\uDCB6\uDD31-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD45\uDD47\uDD90\uDD91\uDD95\uDD97\uDEF3\uDEF4]|\uD81A[\uDEF0-\uDEF4\uDF30-\uDF36]|\uD81B[\uDF4F\uDF8F-\uDF92\uDFE4]|\uD82F[\uDC9D\uDC9E]|\uD834[\uDD65\uDD67-\uDD69\uDD6E-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A\uDD30-\uDD36\uDEEC-\uDEEF]|\uD83A[\uDCD0-\uDCD6\uDD44-\uDD4A]|\uD83C[\uDFFB-\uDFFF]|\uDB40[\uDC20-\uDC7F\uDD00-\uDDEF])*\u200D$/; + + var endsWithEmojiZWJ = function endsWithEmojiZWJ(str) { + return str.search(endingEmojiZWJ) !== -1; + }; + + var endingRIs = /(?:\uD83C[\uDDE6-\uDDFF])+$/g; + + var endsWithOddNumberOfRIs = function endsWithOddNumberOfRIs(str) { + var match = str.match(endingRIs); + + if (match === null) { + return false; + } else { + // A RI is represented by a surrogate pair. + var numRIs = match[0].length / 2; + return numRIs % 2 === 1; + } + }; + + /** + * Shared the function with isElementType utility + */ + + var isElement = function isElement(value) { + return isPlainObject.isPlainObject(value) && Node$1.isNodeList(value.children) && !Editor.isEditor(value); + }; + + var Element$1 = { + /** + * Check if a value implements the 'Ancestor' interface. + */ + isAncestor: function isAncestor(value) { + return isPlainObject.isPlainObject(value) && Node$1.isNodeList(value.children); + }, + + /** + * Check if a value implements the `Element` interface. + */ + isElement: isElement, + + /** + * Check if a value is an array of `Element` objects. + */ + isElementList: function isElementList(value) { + return Array.isArray(value) && value.every(function (val) { + return Element$1.isElement(val); + }); + }, + + /** + * Check if a set of props is a partial of Element. + */ + isElementProps: function isElementProps(props) { + return props.children !== undefined; + }, + + /** + * Check if a value implements the `Element` interface and has elementKey with selected value. + * Default it check to `type` key value + */ + isElementType: function isElementType(value, elementVal) { + var elementKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'type'; + return isElement(value) && value[elementKey] === elementVal; + }, + + /** + * Check if an element matches set of properties. + * + * Note: this checks custom properties, and it does not ensure that any + * children are equivalent. + */ + matches: function matches(element, props) { + for (var key in props) { + if (key === 'children') { + continue; + } + + if (element[key] !== props[key]) { + return false; + } + } + + return true; + } + }; + + var _excluded$4 = ["text"], + _excluded2$3 = ["text"]; + + function ownKeys$8(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$8(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$8(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$8(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$5(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$5(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$5(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$5(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$5(o, minLen); } + + function _arrayLikeToArray$5(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var IS_EDITOR_CACHE = new WeakMap(); + var Editor = { + /** + * Get the ancestor above a location in the document. + */ + above: function above(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$voids = options.voids, + voids = _options$voids === void 0 ? false : _options$voids, + _options$mode = options.mode, + mode = _options$mode === void 0 ? 'lowest' : _options$mode, + _options$at = options.at, + at = _options$at === void 0 ? editor.selection : _options$at, + match = options.match; + + if (!at) { + return; + } + + var path = Editor.path(editor, at); + var reverse = mode === 'lowest'; + + var _iterator = _createForOfIteratorHelper$5(Editor.levels(editor, { + at: path, + voids: voids, + match: match, + reverse: reverse + })), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _step$value = _slicedToArray(_step.value, 2), + n = _step$value[0], + p = _step$value[1]; + + if (!Text.isText(n) && !Path.equals(path, p)) { + return [n, p]; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + }, + + /** + * Add a custom property to the leaf text nodes in the current selection. + * + * If the selection is currently collapsed, the marks will be added to the + * `editor.marks` property instead, and applied when text is inserted next. + */ + addMark: function addMark(editor, key, value) { + editor.addMark(key, value); + }, + + /** + * Get the point after a location. + */ + after: function after(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var anchor = Editor.point(editor, at, { + edge: 'end' + }); + var focus = Editor.end(editor, []); + var range = { + anchor: anchor, + focus: focus + }; + var _options$distance = options.distance, + distance = _options$distance === void 0 ? 1 : _options$distance; + var d = 0; + var target; + + var _iterator2 = _createForOfIteratorHelper$5(Editor.positions(editor, _objectSpread$8(_objectSpread$8({}, options), {}, { + at: range + }))), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var p = _step2.value; + + if (d > distance) { + break; + } + + if (d !== 0) { + target = p; + } + + d++; + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + return target; + }, + + /** + * Get the point before a location. + */ + before: function before(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var anchor = Editor.start(editor, []); + var focus = Editor.point(editor, at, { + edge: 'start' + }); + var range = { + anchor: anchor, + focus: focus + }; + var _options$distance2 = options.distance, + distance = _options$distance2 === void 0 ? 1 : _options$distance2; + var d = 0; + var target; + + var _iterator3 = _createForOfIteratorHelper$5(Editor.positions(editor, _objectSpread$8(_objectSpread$8({}, options), {}, { + at: range, + reverse: true + }))), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var p = _step3.value; + + if (d > distance) { + break; + } + + if (d !== 0) { + target = p; + } + + d++; + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + + return target; + }, + + /** + * Delete content in the editor backward from the current selection. + */ + deleteBackward: function deleteBackward(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$unit = options.unit, + unit = _options$unit === void 0 ? 'character' : _options$unit; + editor.deleteBackward(unit); + }, + + /** + * Delete content in the editor forward from the current selection. + */ + deleteForward: function deleteForward(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$unit2 = options.unit, + unit = _options$unit2 === void 0 ? 'character' : _options$unit2; + editor.deleteForward(unit); + }, + + /** + * Delete the content in the current selection. + */ + deleteFragment: function deleteFragment(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$direction = options.direction, + direction = _options$direction === void 0 ? 'forward' : _options$direction; + editor.deleteFragment(direction); + }, + + /** + * Get the start and end points of a location. + */ + edges: function edges(editor, at) { + return [Editor.start(editor, at), Editor.end(editor, at)]; + }, + + /** + * Get the end point of a location. + */ + end: function end(editor, at) { + return Editor.point(editor, at, { + edge: 'end' + }); + }, + + /** + * Get the first node at a location. + */ + first: function first(editor, at) { + var path = Editor.path(editor, at, { + edge: 'start' + }); + return Editor.node(editor, path); + }, + + /** + * Get the fragment at a location. + */ + fragment: function fragment(editor, at) { + var range = Editor.range(editor, at); + var fragment = Node$1.fragment(editor, range); + return fragment; + }, + + /** + * Check if a node has block children. + */ + hasBlocks: function hasBlocks(editor, element) { + return element.children.some(function (n) { + return Editor.isBlock(editor, n); + }); + }, + + /** + * Check if a node has inline and text children. + */ + hasInlines: function hasInlines(editor, element) { + return element.children.some(function (n) { + return Text.isText(n) || Editor.isInline(editor, n); + }); + }, + + /** + * Check if a node has text children. + */ + hasTexts: function hasTexts(editor, element) { + return element.children.every(function (n) { + return Text.isText(n); + }); + }, + + /** + * Insert a block break at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertBreak: function insertBreak(editor) { + editor.insertBreak(); + }, + + /** + * Insert a fragment at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertFragment: function insertFragment(editor, fragment) { + editor.insertFragment(fragment); + }, + + /** + * Insert a node at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertNode: function insertNode(editor, node) { + editor.insertNode(node); + }, + + /** + * Insert text at the current selection. + * + * If the selection is currently expanded, it will be deleted first. + */ + insertText: function insertText(editor, text) { + editor.insertText(text); + }, + + /** + * Check if a value is a block `Element` object. + */ + isBlock: function isBlock(editor, value) { + return Element$1.isElement(value) && !editor.isInline(value); + }, + + /** + * Check if a value is an `Editor` object. + */ + isEditor: function isEditor(value) { + if (!isPlainObject.isPlainObject(value)) return false; + var cachedIsEditor = IS_EDITOR_CACHE.get(value); + + if (cachedIsEditor !== undefined) { + return cachedIsEditor; + } + + var isEditor = typeof value.addMark === 'function' && typeof value.apply === 'function' && typeof value.deleteBackward === 'function' && typeof value.deleteForward === 'function' && typeof value.deleteFragment === 'function' && typeof value.insertBreak === 'function' && typeof value.insertFragment === 'function' && typeof value.insertNode === 'function' && typeof value.insertText === 'function' && typeof value.isInline === 'function' && typeof value.isVoid === 'function' && typeof value.normalizeNode === 'function' && typeof value.onChange === 'function' && typeof value.removeMark === 'function' && (value.marks === null || isPlainObject.isPlainObject(value.marks)) && (value.selection === null || Range.isRange(value.selection)) && Node$1.isNodeList(value.children) && Operation.isOperationList(value.operations); + IS_EDITOR_CACHE.set(value, isEditor); + return isEditor; + }, + + /** + * Check if a point is the end point of a location. + */ + isEnd: function isEnd(editor, point, at) { + var end = Editor.end(editor, at); + return Point.equals(point, end); + }, + + /** + * Check if a point is an edge of a location. + */ + isEdge: function isEdge(editor, point, at) { + return Editor.isStart(editor, point, at) || Editor.isEnd(editor, point, at); + }, + + /** + * Check if an element is empty, accounting for void nodes. + */ + isEmpty: function isEmpty(editor, element) { + var children = element.children; + + var _children = _slicedToArray(children, 1), + first = _children[0]; + + return children.length === 0 || children.length === 1 && Text.isText(first) && first.text === '' && !editor.isVoid(element); + }, + + /** + * Check if a value is an inline `Element` object. + */ + isInline: function isInline(editor, value) { + return Element$1.isElement(value) && editor.isInline(value); + }, + + /** + * Check if the editor is currently normalizing after each operation. + */ + isNormalizing: function isNormalizing(editor) { + var isNormalizing = NORMALIZING.get(editor); + return isNormalizing === undefined ? true : isNormalizing; + }, + + /** + * Check if a point is the start point of a location. + */ + isStart: function isStart(editor, point, at) { + // PERF: If the offset isn't `0` we know it's not the start. + if (point.offset !== 0) { + return false; + } + + var start = Editor.start(editor, at); + return Point.equals(point, start); + }, + + /** + * Check if a value is a void `Element` object. + */ + isVoid: function isVoid(editor, value) { + return Element$1.isElement(value) && editor.isVoid(value); + }, + + /** + * Get the last node at a location. + */ + last: function last(editor, at) { + var path = Editor.path(editor, at, { + edge: 'end' + }); + return Editor.node(editor, path); + }, + + /** + * Get the leaf text node at a location. + */ + leaf: function leaf(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var path = Editor.path(editor, at, options); + var node = Node$1.leaf(editor, path); + return [node, path]; + }, + + /** + * Iterate through all of the levels at a location. + */ + levels: function* levels(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$at2 = options.at, + at = _options$at2 === void 0 ? editor.selection : _options$at2, + _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse, + _options$voids2 = options.voids, + voids = _options$voids2 === void 0 ? false : _options$voids2; + var match = options.match; + + if (match == null) { + match = function match() { + return true; + }; + } + + if (!at) { + return; + } + + var levels = []; + var path = Editor.path(editor, at); + + var _iterator4 = _createForOfIteratorHelper$5(Node$1.levels(editor, path)), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var _step4$value = _slicedToArray(_step4.value, 2), + n = _step4$value[0], + p = _step4$value[1]; + + if (!match(n, p)) { + continue; + } + + levels.push([n, p]); + + if (!voids && Editor.isVoid(editor, n)) { + break; + } + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + if (reverse) { + levels.reverse(); + } + + yield* levels; + }, + + /** + * Get the marks that would be added to text at the current selection. + */ + marks: function marks(editor) { + var marks = editor.marks, + selection = editor.selection; + + if (!selection) { + return null; + } + + if (marks) { + return marks; + } + + if (Range.isExpanded(selection)) { + var _Editor$nodes = Editor.nodes(editor, { + match: Text.isText + }), + _Editor$nodes2 = _slicedToArray(_Editor$nodes, 1), + match = _Editor$nodes2[0]; + + if (match) { + var _match = _slicedToArray(match, 1), + _node = _match[0]; + + _node.text; + var _rest = _objectWithoutProperties(_node, _excluded$4); + + return _rest; + } else { + return {}; + } + } + + var anchor = selection.anchor; + var path = anchor.path; + + var _Editor$leaf = Editor.leaf(editor, path), + _Editor$leaf2 = _slicedToArray(_Editor$leaf, 1), + node = _Editor$leaf2[0]; + + if (anchor.offset === 0) { + var prev = Editor.previous(editor, { + at: path, + match: Text.isText + }); + var block = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + } + }); + + if (prev && block) { + var _prev = _slicedToArray(prev, 2), + prevNode = _prev[0], + prevPath = _prev[1]; + + var _block = _slicedToArray(block, 2), + blockPath = _block[1]; + + if (Path.isAncestor(blockPath, prevPath)) { + node = prevNode; + } + } + } + + var _node2 = node; + _node2.text; + var rest = _objectWithoutProperties(_node2, _excluded2$3); + + return rest; + }, + + /** + * Get the matching node in the branch of the document after a location. + */ + next: function next(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$mode2 = options.mode, + mode = _options$mode2 === void 0 ? 'lowest' : _options$mode2, + _options$voids3 = options.voids, + voids = _options$voids3 === void 0 ? false : _options$voids3; + var match = options.match, + _options$at3 = options.at, + at = _options$at3 === void 0 ? editor.selection : _options$at3; + + if (!at) { + return; + } + + var pointAfterLocation = Editor.after(editor, at, { + voids: voids + }); + if (!pointAfterLocation) return; + + var _Editor$last = Editor.last(editor, []), + _Editor$last2 = _slicedToArray(_Editor$last, 2), + to = _Editor$last2[1]; + + var span = [pointAfterLocation.path, to]; + + if (Path.isPath(at) && at.length === 0) { + throw new Error("Cannot get the next node from the root node!"); + } + + if (match == null) { + if (Path.isPath(at)) { + var _Editor$parent = Editor.parent(editor, at), + _Editor$parent2 = _slicedToArray(_Editor$parent, 1), + parent = _Editor$parent2[0]; + + match = function match(n) { + return parent.children.includes(n); + }; + } else { + match = function match() { + return true; + }; + } + } + + var _Editor$nodes3 = Editor.nodes(editor, { + at: span, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes4 = _slicedToArray(_Editor$nodes3, 1), + next = _Editor$nodes4[0]; + + return next; + }, + + /** + * Get the node at a location. + */ + node: function node(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var path = Editor.path(editor, at, options); + var node = Node$1.get(editor, path); + return [node, path]; + }, + + /** + * Iterate through all of the nodes in the Editor. + */ + nodes: function* nodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$at4 = options.at, + at = _options$at4 === void 0 ? editor.selection : _options$at4, + _options$mode3 = options.mode, + mode = _options$mode3 === void 0 ? 'all' : _options$mode3, + _options$universal = options.universal, + universal = _options$universal === void 0 ? false : _options$universal, + _options$reverse2 = options.reverse, + reverse = _options$reverse2 === void 0 ? false : _options$reverse2, + _options$voids4 = options.voids, + voids = _options$voids4 === void 0 ? false : _options$voids4; + var match = options.match; + + if (!match) { + match = function match() { + return true; + }; + } + + if (!at) { + return; + } + + var from; + var to; + + if (Span.isSpan(at)) { + from = at[0]; + to = at[1]; + } else { + var first = Editor.path(editor, at, { + edge: 'start' + }); + var last = Editor.path(editor, at, { + edge: 'end' + }); + from = reverse ? last : first; + to = reverse ? first : last; + } + + var nodeEntries = Node$1.nodes(editor, { + reverse: reverse, + from: from, + to: to, + pass: function pass(_ref) { + var _ref2 = _slicedToArray(_ref, 1), + n = _ref2[0]; + + return voids ? false : Editor.isVoid(editor, n); + } + }); + var matches = []; + var hit; + + var _iterator5 = _createForOfIteratorHelper$5(nodeEntries), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _step5$value = _slicedToArray(_step5.value, 2), + node = _step5$value[0], + path = _step5$value[1]; + + var isLower = hit && Path.compare(path, hit[1]) === 0; // In highest mode any node lower than the last hit is not a match. + + if (mode === 'highest' && isLower) { + continue; + } + + if (!match(node, path)) { + // If we've arrived at a leaf text node that is not lower than the last + // hit, then we've found a branch that doesn't include a match, which + // means the match is not universal. + if (universal && !isLower && Text.isText(node)) { + return; + } else { + continue; + } + } // If there's a match and it's lower than the last, update the hit. + + + if (mode === 'lowest' && isLower) { + hit = [node, path]; + continue; + } // In lowest mode we emit the last hit, once it's guaranteed lowest. + + + var emit = mode === 'lowest' ? hit : [node, path]; + + if (emit) { + if (universal) { + matches.push(emit); + } else { + yield emit; + } + } + + hit = [node, path]; + } // Since lowest is always emitting one behind, catch up at the end. + + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + + if (mode === 'lowest' && hit) { + if (universal) { + matches.push(hit); + } else { + yield hit; + } + } // Universal defers to ensure that the match occurs in every branch, so we + // yield all of the matches after iterating. + + + if (universal) { + yield* matches; + } + }, + + /** + * Normalize any dirty objects in the editor. + */ + normalize: function normalize(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$force = options.force, + force = _options$force === void 0 ? false : _options$force; + + var getDirtyPaths = function getDirtyPaths(editor) { + return DIRTY_PATHS.get(editor) || []; + }; + + if (!Editor.isNormalizing(editor)) { + return; + } + + if (force) { + var allPaths = Array.from(Node$1.nodes(editor), function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + p = _ref4[1]; + + return p; + }); + DIRTY_PATHS.set(editor, allPaths); + } + + if (getDirtyPaths(editor).length === 0) { + return; + } + + Editor.withoutNormalizing(editor, function () { + /* + Fix dirty elements with no children. + editor.normalizeNode() does fix this, but some normalization fixes also require it to work. + Running an initial pass avoids the catch-22 race condition. + */ + var _iterator6 = _createForOfIteratorHelper$5(getDirtyPaths(editor)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var _dirtyPath = _step6.value; + + if (Node$1.has(editor, _dirtyPath)) { + var _entry = Editor.node(editor, _dirtyPath); + + var _entry2 = _slicedToArray(_entry, 2), + node = _entry2[0], + _ = _entry2[1]; + /* + The default normalizer inserts an empty text node in this scenario, but it can be customised. + So there is some risk here. + As long as the normalizer only inserts child nodes for this case it is safe to do in any order; + by definition adding children to an empty node can't cause other paths to change. + */ + + + if (Element$1.isElement(node) && node.children.length === 0) { + editor.normalizeNode(_entry); + } + } + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + + var max = getDirtyPaths(editor).length * 42; // HACK: better way? + + var m = 0; + + while (getDirtyPaths(editor).length !== 0) { + if (m > max) { + throw new Error("\n Could not completely normalize the editor after ".concat(max, " iterations! This is usually due to incorrect normalization logic that leaves a node in an invalid state.\n ")); + } + + var dirtyPath = getDirtyPaths(editor).pop(); // If the node doesn't exist in the tree, it does not need to be normalized. + + if (Node$1.has(editor, dirtyPath)) { + var entry = Editor.node(editor, dirtyPath); + editor.normalizeNode(entry); + } + + m++; + } + }); + }, + + /** + * Get the parent node of a location. + */ + parent: function parent(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var path = Editor.path(editor, at, options); + var parentPath = Path.parent(path); + var entry = Editor.node(editor, parentPath); + return entry; + }, + + /** + * Get the path of a location. + */ + path: function path(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var depth = options.depth, + edge = options.edge; + + if (Path.isPath(at)) { + if (edge === 'start') { + var _Node$first = Node$1.first(editor, at), + _Node$first2 = _slicedToArray(_Node$first, 2), + firstPath = _Node$first2[1]; + + at = firstPath; + } else if (edge === 'end') { + var _Node$last = Node$1.last(editor, at), + _Node$last2 = _slicedToArray(_Node$last, 2), + lastPath = _Node$last2[1]; + + at = lastPath; + } + } + + if (Range.isRange(at)) { + if (edge === 'start') { + at = Range.start(at); + } else if (edge === 'end') { + at = Range.end(at); + } else { + at = Path.common(at.anchor.path, at.focus.path); + } + } + + if (Point.isPoint(at)) { + at = at.path; + } + + if (depth != null) { + at = at.slice(0, depth); + } + + return at; + }, + hasPath: function hasPath(editor, path) { + return Node$1.has(editor, path); + }, + + /** + * Create a mutable ref for a `Path` object, which will stay in sync as new + * operations are applied to the editor. + */ + pathRef: function pathRef(editor, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'forward' : _options$affinity; + var ref = { + current: path, + affinity: affinity, + unref: function unref() { + var current = ref.current; + var pathRefs = Editor.pathRefs(editor); + pathRefs["delete"](ref); + ref.current = null; + return current; + } + }; + var refs = Editor.pathRefs(editor); + refs.add(ref); + return ref; + }, + + /** + * Get the set of currently tracked path refs of the editor. + */ + pathRefs: function pathRefs(editor) { + var refs = PATH_REFS.get(editor); + + if (!refs) { + refs = new Set(); + PATH_REFS.set(editor, refs); + } + + return refs; + }, + + /** + * Get the start or end point of a location. + */ + point: function point(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$edge = options.edge, + edge = _options$edge === void 0 ? 'start' : _options$edge; + + if (Path.isPath(at)) { + var path; + + if (edge === 'end') { + var _Node$last3 = Node$1.last(editor, at), + _Node$last4 = _slicedToArray(_Node$last3, 2), + lastPath = _Node$last4[1]; + + path = lastPath; + } else { + var _Node$first3 = Node$1.first(editor, at), + _Node$first4 = _slicedToArray(_Node$first3, 2), + firstPath = _Node$first4[1]; + + path = firstPath; + } + + var node = Node$1.get(editor, path); + + if (!Text.isText(node)) { + throw new Error("Cannot get the ".concat(edge, " point in the node at path [").concat(at, "] because it has no ").concat(edge, " text node.")); + } + + return { + path: path, + offset: edge === 'end' ? node.text.length : 0 + }; + } + + if (Range.isRange(at)) { + var _Range$edges = Range.edges(at), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + start = _Range$edges2[0], + end = _Range$edges2[1]; + + return edge === 'start' ? start : end; + } + + return at; + }, + + /** + * Create a mutable ref for a `Point` object, which will stay in sync as new + * operations are applied to the editor. + */ + pointRef: function pointRef(editor, point) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$affinity2 = options.affinity, + affinity = _options$affinity2 === void 0 ? 'forward' : _options$affinity2; + var ref = { + current: point, + affinity: affinity, + unref: function unref() { + var current = ref.current; + var pointRefs = Editor.pointRefs(editor); + pointRefs["delete"](ref); + ref.current = null; + return current; + } + }; + var refs = Editor.pointRefs(editor); + refs.add(ref); + return ref; + }, + + /** + * Get the set of currently tracked point refs of the editor. + */ + pointRefs: function pointRefs(editor) { + var refs = POINT_REFS.get(editor); + + if (!refs) { + refs = new Set(); + POINT_REFS.set(editor, refs); + } + + return refs; + }, + + /** + * Return all the positions in `at` range where a `Point` can be placed. + * + * By default, moves forward by individual offsets at a time, but + * the `unit` option can be used to to move by character, word, line, or block. + * + * The `reverse` option can be used to change iteration direction. + * + * Note: By default void nodes are treated as a single point and iteration + * will not happen inside their content unless you pass in true for the + * `voids` option, then iteration will occur. + */ + positions: function* positions(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$at5 = options.at, + at = _options$at5 === void 0 ? editor.selection : _options$at5, + _options$unit3 = options.unit, + unit = _options$unit3 === void 0 ? 'offset' : _options$unit3, + _options$reverse3 = options.reverse, + reverse = _options$reverse3 === void 0 ? false : _options$reverse3, + _options$voids5 = options.voids, + voids = _options$voids5 === void 0 ? false : _options$voids5; + + if (!at) { + return; + } + /** + * Algorithm notes: + * + * Each step `distance` is dynamic depending on the underlying text + * and the `unit` specified. Each step, e.g., a line or word, may + * span multiple text nodes, so we iterate through the text both on + * two levels in step-sync: + * + * `leafText` stores the text on a text leaf level, and is advanced + * through using the counters `leafTextOffset` and `leafTextRemaining`. + * + * `blockText` stores the text on a block level, and is shortened + * by `distance` every time it is advanced. + * + * We only maintain a window of one blockText and one leafText because + * a block node always appears before all of its leaf nodes. + */ + + + var range = Editor.range(editor, at); + + var _Range$edges3 = Range.edges(range), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + start = _Range$edges4[0], + end = _Range$edges4[1]; + + var first = reverse ? end : start; + var isNewBlock = false; + var blockText = ''; + var distance = 0; // Distance for leafText to catch up to blockText. + + var leafTextRemaining = 0; + var leafTextOffset = 0; // Iterate through all nodes in range, grabbing entire textual content + // of block nodes in blockText, and text nodes in leafText. + // Exploits the fact that nodes are sequenced in such a way that we first + // encounter the block node, then all of its text nodes, so when iterating + // through the blockText and leafText we just need to remember a window of + // one block node and leaf node, respectively. + + var _iterator7 = _createForOfIteratorHelper$5(Editor.nodes(editor, { + at: at, + reverse: reverse, + voids: voids + })), + _step7; + + try { + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var _step7$value = _slicedToArray(_step7.value, 2), + node = _step7$value[0], + path = _step7$value[1]; + + /* + * ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks + */ + if (Element$1.isElement(node)) { + // Void nodes are a special case, so by default we will always + // yield their first point. If the `voids` option is set to true, + // then we will iterate over their content. + if (!voids && editor.isVoid(node)) { + yield Editor.start(editor, path); + continue; + } // Inline element nodes are ignored as they don't themselves + // contribute to `blockText` or `leafText` - their parent and + // children do. + + + if (editor.isInline(node)) continue; // Block element node - set `blockText` to its text content. + + if (Editor.hasInlines(editor, node)) { + // We always exhaust block nodes before encountering a new one: + // console.assert(blockText === '', + // `blockText='${blockText}' - `+ + // `not exhausted before new block node`, path) + // Ensure range considered is capped to `range`, in the + // start/end edge cases where block extends beyond range. + // Equivalent to this, but presumably more performant: + // blockRange = Editor.range(editor, ...Editor.edges(editor, path)) + // blockRange = Range.intersection(range, blockRange) // intersect + // blockText = Editor.string(editor, blockRange, { voids }) + var e = Path.isAncestor(path, end.path) ? end : Editor.end(editor, path); + var s = Path.isAncestor(path, start.path) ? start : Editor.start(editor, path); + blockText = Editor.string(editor, { + anchor: s, + focus: e + }, { + voids: voids + }); + isNewBlock = true; + } + } + /* + * TEXT LEAF NODE - Iterate through text content, yielding + * positions every `distance` offset according to `unit`. + */ + + + if (Text.isText(node)) { + var isFirst = Path.equals(path, first.path); // Proof that we always exhaust text nodes before encountering a new one: + // console.assert(leafTextRemaining <= 0, + // `leafTextRemaining=${leafTextRemaining} - `+ + // `not exhausted before new leaf text node`, path) + // Reset `leafText` counters for new text node. + + if (isFirst) { + leafTextRemaining = reverse ? first.offset : node.text.length - first.offset; + leafTextOffset = first.offset; // Works for reverse too. + } else { + leafTextRemaining = node.text.length; + leafTextOffset = reverse ? leafTextRemaining : 0; + } // Yield position at the start of node (potentially). + + + if (isFirst || isNewBlock || unit === 'offset') { + yield { + path: path, + offset: leafTextOffset + }; + isNewBlock = false; + } // Yield positions every (dynamically calculated) `distance` offset. + + + while (true) { + // If `leafText` has caught up with `blockText` (distance=0), + // and if blockText is exhausted, break to get another block node, + // otherwise advance blockText forward by the new `distance`. + if (distance === 0) { + if (blockText === '') break; + distance = calcDistance(blockText, unit, reverse); // Split the string at the previously found distance and use the + // remaining string for the next iteration. + + blockText = splitByCharacterDistance(blockText, distance, reverse)[1]; + } // Advance `leafText` by the current `distance`. + + + leafTextOffset = reverse ? leafTextOffset - distance : leafTextOffset + distance; + leafTextRemaining = leafTextRemaining - distance; // If `leafText` is exhausted, break to get a new leaf node + // and set distance to the overflow amount, so we'll (maybe) + // catch up to blockText in the next leaf text node. + + if (leafTextRemaining < 0) { + distance = -leafTextRemaining; + break; + } // Successfully walked `distance` offsets through `leafText` + // to catch up with `blockText`, so we can reset `distance` + // and yield this position in this node. + + + distance = 0; + yield { + path: path, + offset: leafTextOffset + }; + } + } + } // Proof that upon completion, we've exahusted both leaf and block text: + // console.assert(leafTextRemaining <= 0, "leafText wasn't exhausted") + // console.assert(blockText === '', "blockText wasn't exhausted") + // Helper: + // Return the distance in offsets for a step of size `unit` on given string. + + } catch (err) { + _iterator7.e(err); + } finally { + _iterator7.f(); + } + + function calcDistance(text, unit, reverse) { + if (unit === 'character') { + return getCharacterDistance(text, reverse); + } else if (unit === 'word') { + return getWordDistance(text, reverse); + } else if (unit === 'line' || unit === 'block') { + return text.length; + } + + return 1; + } + }, + + /** + * Get the matching node in the branch of the document before a location. + */ + previous: function previous(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$mode4 = options.mode, + mode = _options$mode4 === void 0 ? 'lowest' : _options$mode4, + _options$voids6 = options.voids, + voids = _options$voids6 === void 0 ? false : _options$voids6; + var match = options.match, + _options$at6 = options.at, + at = _options$at6 === void 0 ? editor.selection : _options$at6; + + if (!at) { + return; + } + + var pointBeforeLocation = Editor.before(editor, at, { + voids: voids + }); + + if (!pointBeforeLocation) { + return; + } + + var _Editor$first = Editor.first(editor, []), + _Editor$first2 = _slicedToArray(_Editor$first, 2), + to = _Editor$first2[1]; // The search location is from the start of the document to the path of + // the point before the location passed in + + + var span = [pointBeforeLocation.path, to]; + + if (Path.isPath(at) && at.length === 0) { + throw new Error("Cannot get the previous node from the root node!"); + } + + if (match == null) { + if (Path.isPath(at)) { + var _Editor$parent3 = Editor.parent(editor, at), + _Editor$parent4 = _slicedToArray(_Editor$parent3, 1), + parent = _Editor$parent4[0]; + + match = function match(n) { + return parent.children.includes(n); + }; + } else { + match = function match() { + return true; + }; + } + } + + var _Editor$nodes5 = Editor.nodes(editor, { + reverse: true, + at: span, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes6 = _slicedToArray(_Editor$nodes5, 1), + previous = _Editor$nodes6[0]; + + return previous; + }, + + /** + * Get a range of a location. + */ + range: function range(editor, at, to) { + if (Range.isRange(at) && !to) { + return at; + } + + var start = Editor.start(editor, at); + var end = Editor.end(editor, to || at); + return { + anchor: start, + focus: end + }; + }, + + /** + * Create a mutable ref for a `Range` object, which will stay in sync as new + * operations are applied to the editor. + */ + rangeRef: function rangeRef(editor, range) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$affinity3 = options.affinity, + affinity = _options$affinity3 === void 0 ? 'forward' : _options$affinity3; + var ref = { + current: range, + affinity: affinity, + unref: function unref() { + var current = ref.current; + var rangeRefs = Editor.rangeRefs(editor); + rangeRefs["delete"](ref); + ref.current = null; + return current; + } + }; + var refs = Editor.rangeRefs(editor); + refs.add(ref); + return ref; + }, + + /** + * Get the set of currently tracked range refs of the editor. + */ + rangeRefs: function rangeRefs(editor) { + var refs = RANGE_REFS.get(editor); + + if (!refs) { + refs = new Set(); + RANGE_REFS.set(editor, refs); + } + + return refs; + }, + + /** + * Remove a custom property from all of the leaf text nodes in the current + * selection. + * + * If the selection is currently collapsed, the removal will be stored on + * `editor.marks` and applied to the text inserted next. + */ + removeMark: function removeMark(editor, key) { + editor.removeMark(key); + }, + + /** + * Manually set if the editor should currently be normalizing. + * + * Note: Using this incorrectly can leave the editor in an invalid state. + * + */ + setNormalizing: function setNormalizing(editor, isNormalizing) { + NORMALIZING.set(editor, isNormalizing); + }, + + /** + * Get the start point of a location. + */ + start: function start(editor, at) { + return Editor.point(editor, at, { + edge: 'start' + }); + }, + + /** + * Get the text string content of a location. + * + * Note: by default the text of void nodes is considered to be an empty + * string, regardless of content, unless you pass in true for the voids option + */ + string: function string(editor, at) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$voids7 = options.voids, + voids = _options$voids7 === void 0 ? false : _options$voids7; + var range = Editor.range(editor, at); + + var _Range$edges5 = Range.edges(range), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + start = _Range$edges6[0], + end = _Range$edges6[1]; + + var text = ''; + + var _iterator8 = _createForOfIteratorHelper$5(Editor.nodes(editor, { + at: range, + match: Text.isText, + voids: voids + })), + _step8; + + try { + for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { + var _step8$value = _slicedToArray(_step8.value, 2), + node = _step8$value[0], + path = _step8$value[1]; + + var t = node.text; + + if (Path.equals(path, end.path)) { + t = t.slice(0, end.offset); + } + + if (Path.equals(path, start.path)) { + t = t.slice(start.offset); + } + + text += t; + } + } catch (err) { + _iterator8.e(err); + } finally { + _iterator8.f(); + } + + return text; + }, + + /** + * Convert a range into a non-hanging one. + */ + unhangRange: function unhangRange(editor, range) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$voids8 = options.voids, + voids = _options$voids8 === void 0 ? false : _options$voids8; + + var _Range$edges7 = Range.edges(range), + _Range$edges8 = _slicedToArray(_Range$edges7, 2), + start = _Range$edges8[0], + end = _Range$edges8[1]; // PERF: exit early if we can guarantee that the range isn't hanging. + + + if (start.offset !== 0 || end.offset !== 0 || Range.isCollapsed(range)) { + return range; + } + + var endBlock = Editor.above(editor, { + at: end, + match: function match(n) { + return Editor.isBlock(editor, n); + } + }); + var blockPath = endBlock ? endBlock[1] : []; + var first = Editor.start(editor, []); + var before = { + anchor: first, + focus: end + }; + var skip = true; + + var _iterator9 = _createForOfIteratorHelper$5(Editor.nodes(editor, { + at: before, + match: Text.isText, + reverse: true, + voids: voids + })), + _step9; + + try { + for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) { + var _step9$value = _slicedToArray(_step9.value, 2), + node = _step9$value[0], + path = _step9$value[1]; + + if (skip) { + skip = false; + continue; + } + + if (node.text !== '' || Path.isBefore(path, blockPath)) { + end = { + path: path, + offset: node.text.length + }; + break; + } + } + } catch (err) { + _iterator9.e(err); + } finally { + _iterator9.f(); + } + + return { + anchor: start, + focus: end + }; + }, + + /** + * Match a void node in the current branch of the editor. + */ + "void": function _void(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return Editor.above(editor, _objectSpread$8(_objectSpread$8({}, options), {}, { + match: function match(n) { + return Editor.isVoid(editor, n); + } + })); + }, + + /** + * Call a function, deferring normalization until after it completes. + */ + withoutNormalizing: function withoutNormalizing(editor, fn) { + var value = Editor.isNormalizing(editor); + Editor.setNormalizing(editor, false); + + try { + fn(); + } finally { + Editor.setNormalizing(editor, value); + } + + Editor.normalize(editor); + } + }; + + var Location = { + /** + * Check if a value implements the `Location` interface. + */ + isLocation: function isLocation(value) { + return Path.isPath(value) || Point.isPoint(value) || Range.isRange(value); + } + }; + var Span = { + /** + * Check if a value implements the `Span` interface. + */ + isSpan: function isSpan(value) { + return Array.isArray(value) && value.length === 2 && value.every(Path.isPath); + } + }; + + var _excluded$3 = ["children"], + _excluded2$2 = ["text"]; + + function _createForOfIteratorHelper$4(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$4(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$4(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$4(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$4(o, minLen); } + + function _arrayLikeToArray$4(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var IS_NODE_LIST_CACHE = new WeakMap(); + var Node$1 = { + /** + * Get the node at a specific path, asserting that it's an ancestor node. + */ + ancestor: function ancestor(root, path) { + var node = Node$1.get(root, path); + + if (Text.isText(node)) { + throw new Error("Cannot get the ancestor node at path [".concat(path, "] because it refers to a text node instead: ").concat(node)); + } + + return node; + }, + + /** + * Return a generator of all the ancestor nodes above a specific path. + * + * By default the order is bottom-up, from lowest to highest ancestor in + * the tree, but you can pass the `reverse: true` option to go top-down. + */ + ancestors: function* ancestors(root, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + var _iterator = _createForOfIteratorHelper$4(Path.ancestors(path, options)), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var p = _step.value; + var n = Node$1.ancestor(root, p); + var entry = [n, p]; + yield entry; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + }, + + /** + * Get the child of a node at a specific index. + */ + child: function child(root, index) { + if (Text.isText(root)) { + throw new Error("Cannot get the child of a text node: ".concat(JSON.stringify(root))); + } + + var c = root.children[index]; + + if (c == null) { + throw new Error("Cannot get child at index `".concat(index, "` in node: ").concat(JSON.stringify(root))); + } + + return c; + }, + + /** + * Iterate over the children of a node at a specific path. + */ + children: function* children(root, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var ancestor = Node$1.ancestor(root, path); + var children = ancestor.children; + var index = reverse ? children.length - 1 : 0; + + while (reverse ? index >= 0 : index < children.length) { + var child = Node$1.child(ancestor, index); + var childPath = path.concat(index); + yield [child, childPath]; + index = reverse ? index - 1 : index + 1; + } + }, + + /** + * Get an entry for the common ancesetor node of two paths. + */ + common: function common(root, path, another) { + var p = Path.common(path, another); + var n = Node$1.get(root, p); + return [n, p]; + }, + + /** + * Get the node at a specific path, asserting that it's a descendant node. + */ + descendant: function descendant(root, path) { + var node = Node$1.get(root, path); + + if (Editor.isEditor(node)) { + throw new Error("Cannot get the descendant node at path [".concat(path, "] because it refers to the root editor node instead: ").concat(node)); + } + + return node; + }, + + /** + * Return a generator of all the descendant node entries inside a root node. + */ + descendants: function* descendants(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var _iterator2 = _createForOfIteratorHelper$4(Node$1.nodes(root, options)), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _step2$value = _slicedToArray(_step2.value, 2), + node = _step2$value[0], + path = _step2$value[1]; + + if (path.length !== 0) { + // NOTE: we have to coerce here because checking the path's length does + // guarantee that `node` is not a `Editor`, but TypeScript doesn't know. + yield [node, path]; + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + }, + + /** + * Return a generator of all the element nodes inside a root node. Each iteration + * will return an `ElementEntry` tuple consisting of `[Element, Path]`. If the + * root node is an element it will be included in the iteration as well. + */ + elements: function* elements(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var _iterator3 = _createForOfIteratorHelper$4(Node$1.nodes(root, options)), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _step3$value = _slicedToArray(_step3.value, 2), + node = _step3$value[0], + path = _step3$value[1]; + + if (Element$1.isElement(node)) { + yield [node, path]; + } + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + }, + + /** + * Extract props from a Node. + */ + extractProps: function extractProps(node) { + if (Element$1.isAncestor(node)) { + node.children; + var properties = _objectWithoutProperties(node, _excluded$3); + + return properties; + } else { + node.text; + var _properties = _objectWithoutProperties(node, _excluded2$2); + + return _properties; + } + }, + + /** + * Get the first node entry in a root node from a path. + */ + first: function first(root, path) { + var p = path.slice(); + var n = Node$1.get(root, p); + + while (n) { + if (Text.isText(n) || n.children.length === 0) { + break; + } else { + n = n.children[0]; + p.push(0); + } + } + + return [n, p]; + }, + + /** + * Get the sliced fragment represented by a range inside a root node. + */ + fragment: function fragment(root, range) { + if (Text.isText(root)) { + throw new Error("Cannot get a fragment starting from a root text node: ".concat(JSON.stringify(root))); + } + + var newRoot = immer.produce({ + children: root.children + }, function (r) { + var _Range$edges = Range.edges(range), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + start = _Range$edges2[0], + end = _Range$edges2[1]; + + var nodeEntries = Node$1.nodes(r, { + reverse: true, + pass: function pass(_ref) { + var _ref2 = _slicedToArray(_ref, 2), + path = _ref2[1]; + + return !Range.includes(range, path); + } + }); + + var _iterator4 = _createForOfIteratorHelper$4(nodeEntries), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var _step4$value = _slicedToArray(_step4.value, 2), + path = _step4$value[1]; + + if (!Range.includes(range, path)) { + var parent = Node$1.parent(r, path); + var index = path[path.length - 1]; + parent.children.splice(index, 1); + } + + if (Path.equals(path, end.path)) { + var leaf = Node$1.leaf(r, path); + leaf.text = leaf.text.slice(0, end.offset); + } + + if (Path.equals(path, start.path)) { + var _leaf = Node$1.leaf(r, path); + + _leaf.text = _leaf.text.slice(start.offset); + } + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + if (Editor.isEditor(r)) { + r.selection = null; + } + }); + return newRoot.children; + }, + + /** + * Get the descendant node referred to by a specific path. If the path is an + * empty array, it refers to the root node itself. + */ + get: function get(root, path) { + var node = root; + + for (var i = 0; i < path.length; i++) { + var p = path[i]; + + if (Text.isText(node) || !node.children[p]) { + throw new Error("Cannot find a descendant at path [".concat(path, "] in node: ").concat(JSON.stringify(root))); + } + + node = node.children[p]; + } + + return node; + }, + + /** + * Check if a descendant node exists at a specific path. + */ + has: function has(root, path) { + var node = root; + + for (var i = 0; i < path.length; i++) { + var p = path[i]; + + if (Text.isText(node) || !node.children[p]) { + return false; + } + + node = node.children[p]; + } + + return true; + }, + + /** + * Check if a value implements the `Node` interface. + */ + isNode: function isNode(value) { + return Text.isText(value) || Element$1.isElement(value) || Editor.isEditor(value); + }, + + /** + * Check if a value is a list of `Node` objects. + */ + isNodeList: function isNodeList(value) { + if (!Array.isArray(value)) { + return false; + } + + var cachedResult = IS_NODE_LIST_CACHE.get(value); + + if (cachedResult !== undefined) { + return cachedResult; + } + + var isNodeList = value.every(function (val) { + return Node$1.isNode(val); + }); + IS_NODE_LIST_CACHE.set(value, isNodeList); + return isNodeList; + }, + + /** + * Get the last node entry in a root node from a path. + */ + last: function last(root, path) { + var p = path.slice(); + var n = Node$1.get(root, p); + + while (n) { + if (Text.isText(n) || n.children.length === 0) { + break; + } else { + var i = n.children.length - 1; + n = n.children[i]; + p.push(i); + } + } + + return [n, p]; + }, + + /** + * Get the node at a specific path, ensuring it's a leaf text node. + */ + leaf: function leaf(root, path) { + var node = Node$1.get(root, path); + + if (!Text.isText(node)) { + throw new Error("Cannot get the leaf node at path [".concat(path, "] because it refers to a non-leaf node: ").concat(node)); + } + + return node; + }, + + /** + * Return a generator of the in a branch of the tree, from a specific path. + * + * By default the order is top-down, from lowest to highest node in the tree, + * but you can pass the `reverse: true` option to go bottom-up. + */ + levels: function* levels(root, path) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + var _iterator5 = _createForOfIteratorHelper$4(Path.levels(path, options)), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var p = _step5.value; + var n = Node$1.get(root, p); + yield [n, p]; + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + }, + + /** + * Check if a node matches a set of props. + */ + matches: function matches(node, props) { + return Element$1.isElement(node) && Element$1.isElementProps(props) && Element$1.matches(node, props) || Text.isText(node) && Text.isTextProps(props) && Text.matches(node, props); + }, + + /** + * Return a generator of all the node entries of a root node. Each entry is + * returned as a `[Node, Path]` tuple, with the path referring to the node's + * position inside the root node. + */ + nodes: function* nodes(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var pass = options.pass, + _options$reverse2 = options.reverse, + reverse = _options$reverse2 === void 0 ? false : _options$reverse2; + var _options$from = options.from, + from = _options$from === void 0 ? [] : _options$from, + to = options.to; + var visited = new Set(); + var p = []; + var n = root; + + while (true) { + if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) { + break; + } + + if (!visited.has(n)) { + yield [n, p]; + } // If we're allowed to go downward and we haven't descended yet, do. + + + if (!visited.has(n) && !Text.isText(n) && n.children.length !== 0 && (pass == null || pass([n, p]) === false)) { + visited.add(n); + var nextIndex = reverse ? n.children.length - 1 : 0; + + if (Path.isAncestor(p, from)) { + nextIndex = from[p.length]; + } + + p = p.concat(nextIndex); + n = Node$1.get(root, p); + continue; + } // If we're at the root and we can't go down, we're done. + + + if (p.length === 0) { + break; + } // If we're going forward... + + + if (!reverse) { + var newPath = Path.next(p); + + if (Node$1.has(root, newPath)) { + p = newPath; + n = Node$1.get(root, p); + continue; + } + } // If we're going backward... + + + if (reverse && p[p.length - 1] !== 0) { + var _newPath = Path.previous(p); + + p = _newPath; + n = Node$1.get(root, p); + continue; + } // Otherwise we're going upward... + + + p = Path.parent(p); + n = Node$1.get(root, p); + visited.add(n); + } + }, + + /** + * Get the parent of a node at a specific path. + */ + parent: function parent(root, path) { + var parentPath = Path.parent(path); + var p = Node$1.get(root, parentPath); + + if (Text.isText(p)) { + throw new Error("Cannot get the parent of path [".concat(path, "] because it does not exist in the root.")); + } + + return p; + }, + + /** + * Get the concatenated text string of a node's content. + * + * Note that this will not include spaces or line breaks between block nodes. + * It is not a user-facing string, but a string for performing offset-related + * computations for a node. + */ + string: function string(node) { + if (Text.isText(node)) { + return node.text; + } else { + return node.children.map(Node$1.string).join(''); + } + }, + + /** + * Return a generator of all leaf text nodes in a root node. + */ + texts: function* texts(root) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var _iterator6 = _createForOfIteratorHelper$4(Node$1.nodes(root, options)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var _step6$value = _slicedToArray(_step6.value, 2), + node = _step6$value[0], + path = _step6$value[1]; + + if (Text.isText(node)) { + yield [node, path]; + } + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + } + }; + + function ownKeys$7(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$7(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$7(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$7(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Operation = { + /** + * Check of a value is a `NodeOperation` object. + */ + isNodeOperation: function isNodeOperation(value) { + return Operation.isOperation(value) && value.type.endsWith('_node'); + }, + + /** + * Check of a value is an `Operation` object. + */ + isOperation: function isOperation(value) { + if (!isPlainObject.isPlainObject(value)) { + return false; + } + + switch (value.type) { + case 'insert_node': + return Path.isPath(value.path) && Node$1.isNode(value.node); + + case 'insert_text': + return typeof value.offset === 'number' && typeof value.text === 'string' && Path.isPath(value.path); + + case 'merge_node': + return typeof value.position === 'number' && Path.isPath(value.path) && isPlainObject.isPlainObject(value.properties); + + case 'move_node': + return Path.isPath(value.path) && Path.isPath(value.newPath); + + case 'remove_node': + return Path.isPath(value.path) && Node$1.isNode(value.node); + + case 'remove_text': + return typeof value.offset === 'number' && typeof value.text === 'string' && Path.isPath(value.path); + + case 'set_node': + return Path.isPath(value.path) && isPlainObject.isPlainObject(value.properties) && isPlainObject.isPlainObject(value.newProperties); + + case 'set_selection': + return value.properties === null && Range.isRange(value.newProperties) || value.newProperties === null && Range.isRange(value.properties) || isPlainObject.isPlainObject(value.properties) && isPlainObject.isPlainObject(value.newProperties); + + case 'split_node': + return Path.isPath(value.path) && typeof value.position === 'number' && isPlainObject.isPlainObject(value.properties); + + default: + return false; + } + }, + + /** + * Check if a value is a list of `Operation` objects. + */ + isOperationList: function isOperationList(value) { + return Array.isArray(value) && value.every(function (val) { + return Operation.isOperation(val); + }); + }, + + /** + * Check of a value is a `SelectionOperation` object. + */ + isSelectionOperation: function isSelectionOperation(value) { + return Operation.isOperation(value) && value.type.endsWith('_selection'); + }, + + /** + * Check of a value is a `TextOperation` object. + */ + isTextOperation: function isTextOperation(value) { + return Operation.isOperation(value) && value.type.endsWith('_text'); + }, + + /** + * Invert an operation, returning a new operation that will exactly undo the + * original when applied. + */ + inverse: function inverse(op) { + switch (op.type) { + case 'insert_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'remove_node' + }); + } + + case 'insert_text': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'remove_text' + }); + } + + case 'merge_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'split_node', + path: Path.previous(op.path) + }); + } + + case 'move_node': + { + var newPath = op.newPath, + path = op.path; // PERF: in this case the move operation is a no-op anyways. + + if (Path.equals(newPath, path)) { + return op; + } // If the move happens completely within a single parent the path and + // newPath are stable with respect to each other. + + + if (Path.isSibling(path, newPath)) { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + path: newPath, + newPath: path + }); + } // If the move does not happen within a single parent it is possible + // for the move to impact the true path to the location where the node + // was removed from and where it was inserted. We have to adjust for this + // and find the original path. We can accomplish this (only in non-sibling) + // moves by looking at the impact of the move operation on the node + // after the original move path. + + + var inversePath = Path.transform(path, op); + var inverseNewPath = Path.transform(Path.next(path), op); + return _objectSpread$7(_objectSpread$7({}, op), {}, { + path: inversePath, + newPath: inverseNewPath + }); + } + + case 'remove_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'insert_node' + }); + } + + case 'remove_text': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'insert_text' + }); + } + + case 'set_node': + { + var properties = op.properties, + newProperties = op.newProperties; + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: newProperties, + newProperties: properties + }); + } + + case 'set_selection': + { + var _properties = op.properties, + _newProperties = op.newProperties; + + if (_properties == null) { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: _newProperties, + newProperties: null + }); + } else if (_newProperties == null) { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: null, + newProperties: _properties + }); + } else { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + properties: _newProperties, + newProperties: _properties + }); + } + } + + case 'split_node': + { + return _objectSpread$7(_objectSpread$7({}, op), {}, { + type: 'merge_node', + path: Path.next(op.path) + }); + } + } + } + }; + + var Path = { + /** + * Get a list of ancestor paths for a given path. + * + * The paths are sorted from deepest to shallowest ancestor. However, if the + * `reverse: true` option is passed, they are reversed. + */ + ancestors: function ancestors(path) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var paths = Path.levels(path, options); + + if (reverse) { + paths = paths.slice(1); + } else { + paths = paths.slice(0, -1); + } + + return paths; + }, + + /** + * Get the common ancestor path of two paths. + */ + common: function common(path, another) { + var common = []; + + for (var i = 0; i < path.length && i < another.length; i++) { + var av = path[i]; + var bv = another[i]; + + if (av !== bv) { + break; + } + + common.push(av); + } + + return common; + }, + + /** + * Compare a path to another, returning an integer indicating whether the path + * was before, at, or after the other. + * + * Note: Two paths of unequal length can still receive a `0` result if one is + * directly above or below the other. If you want exact matching, use + * [[Path.equals]] instead. + */ + compare: function compare(path, another) { + var min = Math.min(path.length, another.length); + + for (var i = 0; i < min; i++) { + if (path[i] < another[i]) return -1; + if (path[i] > another[i]) return 1; + } + + return 0; + }, + + /** + * Check if a path ends after one of the indexes in another. + */ + endsAfter: function endsAfter(path, another) { + var i = path.length - 1; + var as = path.slice(0, i); + var bs = another.slice(0, i); + var av = path[i]; + var bv = another[i]; + return Path.equals(as, bs) && av > bv; + }, + + /** + * Check if a path ends at one of the indexes in another. + */ + endsAt: function endsAt(path, another) { + var i = path.length; + var as = path.slice(0, i); + var bs = another.slice(0, i); + return Path.equals(as, bs); + }, + + /** + * Check if a path ends before one of the indexes in another. + */ + endsBefore: function endsBefore(path, another) { + var i = path.length - 1; + var as = path.slice(0, i); + var bs = another.slice(0, i); + var av = path[i]; + var bv = another[i]; + return Path.equals(as, bs) && av < bv; + }, + + /** + * Check if a path is exactly equal to another. + */ + equals: function equals(path, another) { + return path.length === another.length && path.every(function (n, i) { + return n === another[i]; + }); + }, + + /** + * Check if the path of previous sibling node exists + */ + hasPrevious: function hasPrevious(path) { + return path[path.length - 1] > 0; + }, + + /** + * Check if a path is after another. + */ + isAfter: function isAfter(path, another) { + return Path.compare(path, another) === 1; + }, + + /** + * Check if a path is an ancestor of another. + */ + isAncestor: function isAncestor(path, another) { + return path.length < another.length && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is before another. + */ + isBefore: function isBefore(path, another) { + return Path.compare(path, another) === -1; + }, + + /** + * Check if a path is a child of another. + */ + isChild: function isChild(path, another) { + return path.length === another.length + 1 && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is equal to or an ancestor of another. + */ + isCommon: function isCommon(path, another) { + return path.length <= another.length && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is a descendant of another. + */ + isDescendant: function isDescendant(path, another) { + return path.length > another.length && Path.compare(path, another) === 0; + }, + + /** + * Check if a path is the parent of another. + */ + isParent: function isParent(path, another) { + return path.length + 1 === another.length && Path.compare(path, another) === 0; + }, + + /** + * Check is a value implements the `Path` interface. + */ + isPath: function isPath(value) { + return Array.isArray(value) && (value.length === 0 || typeof value[0] === 'number'); + }, + + /** + * Check if a path is a sibling of another. + */ + isSibling: function isSibling(path, another) { + if (path.length !== another.length) { + return false; + } + + var as = path.slice(0, -1); + var bs = another.slice(0, -1); + var al = path[path.length - 1]; + var bl = another[another.length - 1]; + return al !== bl && Path.equals(as, bs); + }, + + /** + * Get a list of paths at every level down to a path. Note: this is the same + * as `Path.ancestors`, but including the path itself. + * + * The paths are sorted from shallowest to deepest. However, if the `reverse: + * true` option is passed, they are reversed. + */ + levels: function levels(path) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$reverse2 = options.reverse, + reverse = _options$reverse2 === void 0 ? false : _options$reverse2; + var list = []; + + for (var i = 0; i <= path.length; i++) { + list.push(path.slice(0, i)); + } + + if (reverse) { + list.reverse(); + } + + return list; + }, + + /** + * Given a path, get the path to the next sibling node. + */ + next: function next(path) { + if (path.length === 0) { + throw new Error("Cannot get the next path of a root path [".concat(path, "], because it has no next index.")); + } + + var last = path[path.length - 1]; + return path.slice(0, -1).concat(last + 1); + }, + + /** + * Given a path, return a new path referring to the parent node above it. + */ + parent: function parent(path) { + if (path.length === 0) { + throw new Error("Cannot get the parent path of the root path [".concat(path, "].")); + } + + return path.slice(0, -1); + }, + + /** + * Given a path, get the path to the previous sibling node. + */ + previous: function previous(path) { + if (path.length === 0) { + throw new Error("Cannot get the previous path of a root path [".concat(path, "], because it has no previous index.")); + } + + var last = path[path.length - 1]; + + if (last <= 0) { + throw new Error("Cannot get the previous path of a first child path [".concat(path, "] because it would result in a negative index.")); + } + + return path.slice(0, -1).concat(last - 1); + }, + + /** + * Get a path relative to an ancestor. + */ + relative: function relative(path, ancestor) { + if (!Path.isAncestor(ancestor, path) && !Path.equals(path, ancestor)) { + throw new Error("Cannot get the relative path of [".concat(path, "] inside ancestor [").concat(ancestor, "], because it is not above or equal to the path.")); + } + + return path.slice(ancestor.length); + }, + + /** + * Transform a path by an operation. + */ + transform: function transform(path, operation) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return immer.produce(path, function (p) { + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'forward' : _options$affinity; // PERF: Exit early if the operation is guaranteed not to have an effect. + + if (!path || (path === null || path === void 0 ? void 0 : path.length) === 0) { + return; + } + + if (p === null) { + return null; + } + + switch (operation.type) { + case 'insert_node': + { + var op = operation.path; + + if (Path.equals(op, p) || Path.endsBefore(op, p) || Path.isAncestor(op, p)) { + p[op.length - 1] += 1; + } + + break; + } + + case 'remove_node': + { + var _op = operation.path; + + if (Path.equals(_op, p) || Path.isAncestor(_op, p)) { + return null; + } else if (Path.endsBefore(_op, p)) { + p[_op.length - 1] -= 1; + } + + break; + } + + case 'merge_node': + { + var _op2 = operation.path, + position = operation.position; + + if (Path.equals(_op2, p) || Path.endsBefore(_op2, p)) { + p[_op2.length - 1] -= 1; + } else if (Path.isAncestor(_op2, p)) { + p[_op2.length - 1] -= 1; + p[_op2.length] += position; + } + + break; + } + + case 'split_node': + { + var _op3 = operation.path, + _position = operation.position; + + if (Path.equals(_op3, p)) { + if (affinity === 'forward') { + p[p.length - 1] += 1; + } else if (affinity === 'backward') ; else { + return null; + } + } else if (Path.endsBefore(_op3, p)) { + p[_op3.length - 1] += 1; + } else if (Path.isAncestor(_op3, p) && path[_op3.length] >= _position) { + p[_op3.length - 1] += 1; + p[_op3.length] -= _position; + } + + break; + } + + case 'move_node': + { + var _op4 = operation.path, + onp = operation.newPath; // If the old and new path are the same, it's a no-op. + + if (Path.equals(_op4, onp)) { + return; + } + + if (Path.isAncestor(_op4, p) || Path.equals(_op4, p)) { + var copy = onp.slice(); + + if (Path.endsBefore(_op4, onp) && _op4.length < onp.length) { + copy[_op4.length - 1] -= 1; + } + + return copy.concat(p.slice(_op4.length)); + } else if (Path.isSibling(_op4, onp) && (Path.isAncestor(onp, p) || Path.equals(onp, p))) { + if (Path.endsBefore(_op4, p)) { + p[_op4.length - 1] -= 1; + } else { + p[_op4.length - 1] += 1; + } + } else if (Path.endsBefore(onp, p) || Path.equals(onp, p) || Path.isAncestor(onp, p)) { + if (Path.endsBefore(_op4, p)) { + p[_op4.length - 1] -= 1; + } + + p[onp.length - 1] += 1; + } else if (Path.endsBefore(_op4, p)) { + if (Path.equals(onp, p)) { + p[onp.length - 1] += 1; + } + + p[_op4.length - 1] -= 1; + } + + break; + } + } + }); + } + }; + + var PathRef = { + /** + * Transform the path ref's current value by an operation. + */ + transform: function transform(ref, op) { + var current = ref.current, + affinity = ref.affinity; + + if (current == null) { + return; + } + + var path = Path.transform(current, op, { + affinity: affinity + }); + ref.current = path; + + if (path == null) { + ref.unref(); + } + } + }; + + function ownKeys$6(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$6(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$6(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$6(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Point = { + /** + * Compare a point to another, returning an integer indicating whether the + * point was before, at, or after the other. + */ + compare: function compare(point, another) { + var result = Path.compare(point.path, another.path); + + if (result === 0) { + if (point.offset < another.offset) return -1; + if (point.offset > another.offset) return 1; + return 0; + } + + return result; + }, + + /** + * Check if a point is after another. + */ + isAfter: function isAfter(point, another) { + return Point.compare(point, another) === 1; + }, + + /** + * Check if a point is before another. + */ + isBefore: function isBefore(point, another) { + return Point.compare(point, another) === -1; + }, + + /** + * Check if a point is exactly equal to another. + */ + equals: function equals(point, another) { + // PERF: ensure the offsets are equal first since they are cheaper to check. + return point.offset === another.offset && Path.equals(point.path, another.path); + }, + + /** + * Check if a value implements the `Point` interface. + */ + isPoint: function isPoint(value) { + return isPlainObject.isPlainObject(value) && typeof value.offset === 'number' && Path.isPath(value.path); + }, + + /** + * Transform a point by an operation. + */ + transform: function transform(point, op) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return immer.produce(point, function (p) { + if (p === null) { + return null; + } + + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'forward' : _options$affinity; + var path = p.path, + offset = p.offset; + + switch (op.type) { + case 'insert_node': + case 'move_node': + { + p.path = Path.transform(path, op, options); + break; + } + + case 'insert_text': + { + if (Path.equals(op.path, path) && op.offset <= offset) { + p.offset += op.text.length; + } + + break; + } + + case 'merge_node': + { + if (Path.equals(op.path, path)) { + p.offset += op.position; + } + + p.path = Path.transform(path, op, options); + break; + } + + case 'remove_text': + { + if (Path.equals(op.path, path) && op.offset <= offset) { + p.offset -= Math.min(offset - op.offset, op.text.length); + } + + break; + } + + case 'remove_node': + { + if (Path.equals(op.path, path) || Path.isAncestor(op.path, path)) { + return null; + } + + p.path = Path.transform(path, op, options); + break; + } + + case 'split_node': + { + if (Path.equals(op.path, path)) { + if (op.position === offset && affinity == null) { + return null; + } else if (op.position < offset || op.position === offset && affinity === 'forward') { + p.offset -= op.position; + p.path = Path.transform(path, op, _objectSpread$6(_objectSpread$6({}, options), {}, { + affinity: 'forward' + })); + } + } else { + p.path = Path.transform(path, op, options); + } + + break; + } + } + }); + } + }; + + var PointRef = { + /** + * Transform the point ref's current value by an operation. + */ + transform: function transform(ref, op) { + var current = ref.current, + affinity = ref.affinity; + + if (current == null) { + return; + } + + var point = Point.transform(current, op, { + affinity: affinity + }); + ref.current = point; + + if (point == null) { + ref.unref(); + } + } + }; + + var _excluded$2 = ["anchor", "focus"]; + + function ownKeys$5(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$5(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$5(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$5(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Range = { + /** + * Get the start and end points of a range, in the order in which they appear + * in the document. + */ + edges: function edges(range) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var anchor = range.anchor, + focus = range.focus; + return Range.isBackward(range) === reverse ? [anchor, focus] : [focus, anchor]; + }, + + /** + * Get the end point of a range. + */ + end: function end(range) { + var _Range$edges = Range.edges(range), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + end = _Range$edges2[1]; + + return end; + }, + + /** + * Check if a range is exactly equal to another. + */ + equals: function equals(range, another) { + return Point.equals(range.anchor, another.anchor) && Point.equals(range.focus, another.focus); + }, + + /** + * Check if a range includes a path, a point or part of another range. + */ + includes: function includes(range, target) { + if (Range.isRange(target)) { + if (Range.includes(range, target.anchor) || Range.includes(range, target.focus)) { + return true; + } + + var _Range$edges3 = Range.edges(range), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + rs = _Range$edges4[0], + re = _Range$edges4[1]; + + var _Range$edges5 = Range.edges(target), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + ts = _Range$edges6[0], + te = _Range$edges6[1]; + + return Point.isBefore(rs, ts) && Point.isAfter(re, te); + } + + var _Range$edges7 = Range.edges(range), + _Range$edges8 = _slicedToArray(_Range$edges7, 2), + start = _Range$edges8[0], + end = _Range$edges8[1]; + + var isAfterStart = false; + var isBeforeEnd = false; + + if (Point.isPoint(target)) { + isAfterStart = Point.compare(target, start) >= 0; + isBeforeEnd = Point.compare(target, end) <= 0; + } else { + isAfterStart = Path.compare(target, start.path) >= 0; + isBeforeEnd = Path.compare(target, end.path) <= 0; + } + + return isAfterStart && isBeforeEnd; + }, + + /** + * Get the intersection of a range with another. + */ + intersection: function intersection(range, another) { + range.anchor; + range.focus; + var rest = _objectWithoutProperties(range, _excluded$2); + + var _Range$edges9 = Range.edges(range), + _Range$edges10 = _slicedToArray(_Range$edges9, 2), + s1 = _Range$edges10[0], + e1 = _Range$edges10[1]; + + var _Range$edges11 = Range.edges(another), + _Range$edges12 = _slicedToArray(_Range$edges11, 2), + s2 = _Range$edges12[0], + e2 = _Range$edges12[1]; + + var start = Point.isBefore(s1, s2) ? s2 : s1; + var end = Point.isBefore(e1, e2) ? e1 : e2; + + if (Point.isBefore(end, start)) { + return null; + } else { + return _objectSpread$5({ + anchor: start, + focus: end + }, rest); + } + }, + + /** + * Check if a range is backward, meaning that its anchor point appears in the + * document _after_ its focus point. + */ + isBackward: function isBackward(range) { + var anchor = range.anchor, + focus = range.focus; + return Point.isAfter(anchor, focus); + }, + + /** + * Check if a range is collapsed, meaning that both its anchor and focus + * points refer to the exact same position in the document. + */ + isCollapsed: function isCollapsed(range) { + var anchor = range.anchor, + focus = range.focus; + return Point.equals(anchor, focus); + }, + + /** + * Check if a range is expanded. + * + * This is the opposite of [[Range.isCollapsed]] and is provided for legibility. + */ + isExpanded: function isExpanded(range) { + return !Range.isCollapsed(range); + }, + + /** + * Check if a range is forward. + * + * This is the opposite of [[Range.isBackward]] and is provided for legibility. + */ + isForward: function isForward(range) { + return !Range.isBackward(range); + }, + + /** + * Check if a value implements the [[Range]] interface. + */ + isRange: function isRange(value) { + return isPlainObject.isPlainObject(value) && Point.isPoint(value.anchor) && Point.isPoint(value.focus); + }, + + /** + * Iterate through all of the point entries in a range. + */ + points: function* points(range) { + yield [range.anchor, 'anchor']; + yield [range.focus, 'focus']; + }, + + /** + * Get the start point of a range. + */ + start: function start(range) { + var _Range$edges13 = Range.edges(range), + _Range$edges14 = _slicedToArray(_Range$edges13, 1), + start = _Range$edges14[0]; + + return start; + }, + + /** + * Transform a range by an operation. + */ + transform: function transform(range, op) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return immer.produce(range, function (r) { + if (r === null) { + return null; + } + + var _options$affinity = options.affinity, + affinity = _options$affinity === void 0 ? 'inward' : _options$affinity; + var affinityAnchor; + var affinityFocus; + + if (affinity === 'inward') { + // If the range is collapsed, make sure to use the same affinity to + // avoid the two points passing each other and expanding in the opposite + // direction + var isCollapsed = Range.isCollapsed(r); + + if (Range.isForward(r)) { + affinityAnchor = 'forward'; + affinityFocus = isCollapsed ? affinityAnchor : 'backward'; + } else { + affinityAnchor = 'backward'; + affinityFocus = isCollapsed ? affinityAnchor : 'forward'; + } + } else if (affinity === 'outward') { + if (Range.isForward(r)) { + affinityAnchor = 'backward'; + affinityFocus = 'forward'; + } else { + affinityAnchor = 'forward'; + affinityFocus = 'backward'; + } + } else { + affinityAnchor = affinity; + affinityFocus = affinity; + } + + var anchor = Point.transform(r.anchor, op, { + affinity: affinityAnchor + }); + var focus = Point.transform(r.focus, op, { + affinity: affinityFocus + }); + + if (!anchor || !focus) { + return null; + } + + r.anchor = anchor; + r.focus = focus; + }); + } + }; + + var RangeRef = { + /** + * Transform the range ref's current value by an operation. + */ + transform: function transform(ref, op) { + var current = ref.current, + affinity = ref.affinity; + + if (current == null) { + return; + } + + var path = Range.transform(current, op, { + affinity: affinity + }); + ref.current = path; + + if (path == null) { + ref.unref(); + } + } + }; + + /* + Custom deep equal comparison for Slate nodes. + + We don't need general purpose deep equality; + Slate only supports plain values, Arrays, and nested objects. + Complex values nested inside Arrays are not supported. + + Slate objects are designed to be serialised, so + missing keys are deliberately normalised to undefined. + */ + + var isDeepEqual = function isDeepEqual(node, another) { + for (var key in node) { + var a = node[key]; + var b = another[key]; + + if (isPlainObject.isPlainObject(a) && isPlainObject.isPlainObject(b)) { + if (!isDeepEqual(a, b)) return false; + } else if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + + for (var i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + } else if (a !== b) { + return false; + } + } + /* + Deep object equality is only necessary in one direction; in the reverse direction + we are only looking for keys that are missing. + As above, undefined keys are normalised to missing. + */ + + + for (var _key in another) { + if (node[_key] === undefined && another[_key] !== undefined) { + return false; + } + } + + return true; + }; + + var _excluded$1 = ["text"], + _excluded2$1 = ["anchor", "focus"]; + + function _createForOfIteratorHelper$3(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$3(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$3(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$3(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$3(o, minLen); } + + function _arrayLikeToArray$3(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + + function ownKeys$4(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$4(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$4(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$4(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Text = { + /** + * Check if two text nodes are equal. + * + * When loose is set, the text is not compared. This is + * used to check whether sibling text nodes can be merged. + */ + equals: function equals(text, another) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var _options$loose = options.loose, + loose = _options$loose === void 0 ? false : _options$loose; + + function omitText(obj) { + obj.text; + var rest = _objectWithoutProperties(obj, _excluded$1); + + return rest; + } + + return isDeepEqual(loose ? omitText(text) : text, loose ? omitText(another) : another); + }, + + /** + * Check if a value implements the `Text` interface. + */ + isText: function isText(value) { + return isPlainObject.isPlainObject(value) && typeof value.text === 'string'; + }, + + /** + * Check if a value is a list of `Text` objects. + */ + isTextList: function isTextList(value) { + return Array.isArray(value) && value.every(function (val) { + return Text.isText(val); + }); + }, + + /** + * Check if some props are a partial of Text. + */ + isTextProps: function isTextProps(props) { + return props.text !== undefined; + }, + + /** + * Check if an text matches set of properties. + * + * Note: this is for matching custom properties, and it does not ensure that + * the `text` property are two nodes equal. + */ + matches: function matches(text, props) { + for (var key in props) { + if (key === 'text') { + continue; + } + + if (!text.hasOwnProperty(key) || text[key] !== props[key]) { + return false; + } + } + + return true; + }, + + /** + * Get the leaves for a text node given decorations. + */ + decorations: function decorations(node, _decorations) { + var leaves = [_objectSpread$4({}, node)]; + + var _iterator = _createForOfIteratorHelper$3(_decorations), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var dec = _step.value; + + var anchor = dec.anchor, + focus = dec.focus, + rest = _objectWithoutProperties(dec, _excluded2$1); + + var _Range$edges = Range.edges(dec), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + start = _Range$edges2[0], + end = _Range$edges2[1]; + + var next = []; + var o = 0; + + var _iterator2 = _createForOfIteratorHelper$3(leaves), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var leaf = _step2.value; + var length = leaf.text.length; + var offset = o; + o += length; // If the range encompases the entire leaf, add the range. + + if (start.offset <= offset && end.offset >= o) { + Object.assign(leaf, rest); + next.push(leaf); + continue; + } // If the range expanded and match the leaf, or starts after, or ends before it, continue. + + + if (start.offset !== end.offset && (start.offset === o || end.offset === offset) || start.offset > o || end.offset < offset || end.offset === offset && offset !== 0) { + next.push(leaf); + continue; + } // Otherwise we need to split the leaf, at the start, end, or both, + // and add the range to the middle intersecting section. Do the end + // split first since we don't need to update the offset that way. + + + var middle = leaf; + var before = void 0; + var after = void 0; + + if (end.offset < o) { + var off = end.offset - offset; + after = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(off) + }); + middle = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(0, off) + }); + } + + if (start.offset > offset) { + var _off = start.offset - offset; + + before = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(0, _off) + }); + middle = _objectSpread$4(_objectSpread$4({}, middle), {}, { + text: middle.text.slice(_off) + }); + } + + Object.assign(middle, rest); + + if (before) { + next.push(before); + } + + next.push(middle); + + if (after) { + next.push(after); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + leaves = next; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + return leaves; + } + }; + + function ownKeys$3(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$3(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$3(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$3(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$2(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$2(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$2(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$2(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$2(o, minLen); } + + function _arrayLikeToArray$2(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + + var applyToDraft = function applyToDraft(editor, selection, op) { + switch (op.type) { + case 'insert_node': + { + var path = op.path, + node = op.node; + var parent = Node$1.parent(editor, path); + var index = path[path.length - 1]; + + if (index > parent.children.length) { + throw new Error("Cannot apply an \"insert_node\" operation at path [".concat(path, "] because the destination is past the end of the node.")); + } + + parent.children.splice(index, 0, node); + + if (selection) { + var _iterator = _createForOfIteratorHelper$2(Range.points(selection)), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _step$value = _slicedToArray(_step.value, 2), + point = _step$value[0], + key = _step$value[1]; + + selection[key] = Point.transform(point, op); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + } + + break; + } + + case 'insert_text': + { + var _path = op.path, + offset = op.offset, + text = op.text; + if (text.length === 0) break; + + var _node = Node$1.leaf(editor, _path); + + var before = _node.text.slice(0, offset); + + var after = _node.text.slice(offset); + + _node.text = before + text + after; + + if (selection) { + var _iterator2 = _createForOfIteratorHelper$2(Range.points(selection)), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _step2$value = _slicedToArray(_step2.value, 2), + _point = _step2$value[0], + _key = _step2$value[1]; + + selection[_key] = Point.transform(_point, op); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + } + + break; + } + + case 'merge_node': + { + var _path2 = op.path; + + var _node2 = Node$1.get(editor, _path2); + + var prevPath = Path.previous(_path2); + var prev = Node$1.get(editor, prevPath); + + var _parent = Node$1.parent(editor, _path2); + + var _index = _path2[_path2.length - 1]; + + if (Text.isText(_node2) && Text.isText(prev)) { + prev.text += _node2.text; + } else if (!Text.isText(_node2) && !Text.isText(prev)) { + var _prev$children; + + (_prev$children = prev.children).push.apply(_prev$children, _toConsumableArray(_node2.children)); + } else { + throw new Error("Cannot apply a \"merge_node\" operation at path [".concat(_path2, "] to nodes of different interfaces: ").concat(_node2, " ").concat(prev)); + } + + _parent.children.splice(_index, 1); + + if (selection) { + var _iterator3 = _createForOfIteratorHelper$2(Range.points(selection)), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _step3$value = _slicedToArray(_step3.value, 2), + _point2 = _step3$value[0], + _key2 = _step3$value[1]; + + selection[_key2] = Point.transform(_point2, op); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + } + + break; + } + + case 'move_node': + { + var _path3 = op.path, + newPath = op.newPath; + + if (Path.isAncestor(_path3, newPath)) { + throw new Error("Cannot move a path [".concat(_path3, "] to new path [").concat(newPath, "] because the destination is inside itself.")); + } + + var _node3 = Node$1.get(editor, _path3); + + var _parent2 = Node$1.parent(editor, _path3); + + var _index2 = _path3[_path3.length - 1]; // This is tricky, but since the `path` and `newPath` both refer to + // the same snapshot in time, there's a mismatch. After either + // removing the original position, the second step's path can be out + // of date. So instead of using the `op.newPath` directly, we + // transform `op.path` to ascertain what the `newPath` would be after + // the operation was applied. + + _parent2.children.splice(_index2, 1); + + var truePath = Path.transform(_path3, op); + var newParent = Node$1.get(editor, Path.parent(truePath)); + var newIndex = truePath[truePath.length - 1]; + newParent.children.splice(newIndex, 0, _node3); + + if (selection) { + var _iterator4 = _createForOfIteratorHelper$2(Range.points(selection)), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var _step4$value = _slicedToArray(_step4.value, 2), + _point3 = _step4$value[0], + _key3 = _step4$value[1]; + + selection[_key3] = Point.transform(_point3, op); + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + } + + break; + } + + case 'remove_node': + { + var _path4 = op.path; + var _index3 = _path4[_path4.length - 1]; + + var _parent3 = Node$1.parent(editor, _path4); + + _parent3.children.splice(_index3, 1); // Transform all of the points in the value, but if the point was in the + // node that was removed we need to update the range or remove it. + + + if (selection) { + var _iterator5 = _createForOfIteratorHelper$2(Range.points(selection)), + _step5; + + try { + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + var _step5$value = _slicedToArray(_step5.value, 2), + _point4 = _step5$value[0], + _key4 = _step5$value[1]; + + var result = Point.transform(_point4, op); + + if (selection != null && result != null) { + selection[_key4] = result; + } else { + var _prev = void 0; + + var next = void 0; + + var _iterator6 = _createForOfIteratorHelper$2(Node$1.texts(editor)), + _step6; + + try { + for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { + var _step6$value = _slicedToArray(_step6.value, 2), + n = _step6$value[0], + p = _step6$value[1]; + + if (Path.compare(p, _path4) === -1) { + _prev = [n, p]; + } else { + next = [n, p]; + break; + } + } + } catch (err) { + _iterator6.e(err); + } finally { + _iterator6.f(); + } + + var preferNext = false; + + if (_prev && next) { + if (Path.equals(next[1], _path4)) { + preferNext = !Path.hasPrevious(next[1]); + } else { + preferNext = Path.common(_prev[1], _path4).length < Path.common(next[1], _path4).length; + } + } + + if (_prev && !preferNext) { + _point4.path = _prev[1]; + _point4.offset = _prev[0].text.length; + } else if (next) { + _point4.path = next[1]; + _point4.offset = 0; + } else { + selection = null; + } + } + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + } + + break; + } + + case 'remove_text': + { + var _path5 = op.path, + _offset = op.offset, + _text = op.text; + if (_text.length === 0) break; + + var _node4 = Node$1.leaf(editor, _path5); + + var _before = _node4.text.slice(0, _offset); + + var _after = _node4.text.slice(_offset + _text.length); + + _node4.text = _before + _after; + + if (selection) { + var _iterator7 = _createForOfIteratorHelper$2(Range.points(selection)), + _step7; + + try { + for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) { + var _step7$value = _slicedToArray(_step7.value, 2), + _point5 = _step7$value[0], + _key5 = _step7$value[1]; + + selection[_key5] = Point.transform(_point5, op); + } + } catch (err) { + _iterator7.e(err); + } finally { + _iterator7.f(); + } + } + + break; + } + + case 'set_node': + { + var _path6 = op.path, + properties = op.properties, + newProperties = op.newProperties; + + if (_path6.length === 0) { + throw new Error("Cannot set properties on the root node!"); + } + + var _node5 = Node$1.get(editor, _path6); + + for (var _key6 in newProperties) { + if (_key6 === 'children' || _key6 === 'text') { + throw new Error("Cannot set the \"".concat(_key6, "\" property of nodes!")); + } + + var value = newProperties[_key6]; + + if (value == null) { + delete _node5[_key6]; + } else { + _node5[_key6] = value; + } + } // properties that were previously defined, but are now missing, must be deleted + + + for (var _key7 in properties) { + if (!newProperties.hasOwnProperty(_key7)) { + delete _node5[_key7]; + } + } + + break; + } + + case 'set_selection': + { + var _newProperties = op.newProperties; + + if (_newProperties == null) { + selection = _newProperties; + } else { + if (selection == null) { + if (!Range.isRange(_newProperties)) { + throw new Error("Cannot apply an incomplete \"set_selection\" operation properties ".concat(JSON.stringify(_newProperties), " when there is no current selection.")); + } + + selection = _objectSpread$3({}, _newProperties); + } + + for (var _key8 in _newProperties) { + var _value = _newProperties[_key8]; + + if (_value == null) { + if (_key8 === 'anchor' || _key8 === 'focus') { + throw new Error("Cannot remove the \"".concat(_key8, "\" selection property")); + } + + delete selection[_key8]; + } else { + selection[_key8] = _value; + } + } + } + + break; + } + + case 'split_node': + { + var _path7 = op.path, + position = op.position, + _properties = op.properties; + + if (_path7.length === 0) { + throw new Error("Cannot apply a \"split_node\" operation at path [".concat(_path7, "] because the root node cannot be split.")); + } + + var _node6 = Node$1.get(editor, _path7); + + var _parent4 = Node$1.parent(editor, _path7); + + var _index4 = _path7[_path7.length - 1]; + var newNode; + + if (Text.isText(_node6)) { + var _before2 = _node6.text.slice(0, position); + + var _after2 = _node6.text.slice(position); + + _node6.text = _before2; + newNode = _objectSpread$3(_objectSpread$3({}, _properties), {}, { + text: _after2 + }); + } else { + var _before3 = _node6.children.slice(0, position); + + var _after3 = _node6.children.slice(position); + + _node6.children = _before3; + newNode = _objectSpread$3(_objectSpread$3({}, _properties), {}, { + children: _after3 + }); + } + + _parent4.children.splice(_index4 + 1, 0, newNode); + + if (selection) { + var _iterator8 = _createForOfIteratorHelper$2(Range.points(selection)), + _step8; + + try { + for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) { + var _step8$value = _slicedToArray(_step8.value, 2), + _point6 = _step8$value[0], + _key9 = _step8$value[1]; + + selection[_key9] = Point.transform(_point6, op); + } + } catch (err) { + _iterator8.e(err); + } finally { + _iterator8.f(); + } + } + + break; + } + } + + return selection; + }; + + var GeneralTransforms = { + /** + * Transform the editor by an operation. + */ + transform: function transform(editor, op) { + editor.children = immer.createDraft(editor.children); + var selection = editor.selection && immer.createDraft(editor.selection); + + try { + selection = applyToDraft(editor, selection, op); + } finally { + editor.children = immer.finishDraft(editor.children); + + if (selection) { + editor.selection = immer.isDraft(selection) ? immer.finishDraft(selection) : selection; + } else { + editor.selection = null; + } + } + } + }; + + var _excluded = ["text"], + _excluded2 = ["children"]; + + function ownKeys$2(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$2(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$2(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + + function _createForOfIteratorHelper$1(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$1(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray$1(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$1(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray$1(o, minLen); } + + function _arrayLikeToArray$1(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var NodeTransforms = { + /** + * Insert nodes at a specific location in the Editor. + */ + insertNodes: function insertNodes(editor, nodes) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$hanging = options.hanging, + hanging = _options$hanging === void 0 ? false : _options$hanging, + _options$voids = options.voids, + voids = _options$voids === void 0 ? false : _options$voids, + _options$mode = options.mode, + mode = _options$mode === void 0 ? 'lowest' : _options$mode; + var at = options.at, + match = options.match, + select = options.select; + + if (Node$1.isNode(nodes)) { + nodes = [nodes]; + } + + if (nodes.length === 0) { + return; + } + + var _nodes = nodes, + _nodes2 = _slicedToArray(_nodes, 1), + node = _nodes2[0]; // By default, use the selection as the target location. But if there is + // no selection, insert at the end of the document since that is such a + // common use case when inserting from a non-selected state. + + + if (!at) { + if (editor.selection) { + at = editor.selection; + } else if (editor.children.length > 0) { + at = Editor.end(editor, []); + } else { + at = [0]; + } + + select = true; + } + + if (select == null) { + select = false; + } + + if (Range.isRange(at)) { + if (!hanging) { + at = Editor.unhangRange(editor, at); + } + + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var _Range$edges = Range.edges(at), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + end = _Range$edges2[1]; + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at + }); + at = pointRef.unref(); + } + } + + if (Point.isPoint(at)) { + if (match == null) { + if (Text.isText(node)) { + match = function match(n) { + return Text.isText(n); + }; + } else if (editor.isInline(node)) { + match = function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }; + } else { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + } + + var _Editor$nodes = Editor.nodes(editor, { + at: at.path, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes2 = _slicedToArray(_Editor$nodes, 1), + entry = _Editor$nodes2[0]; + + if (entry) { + var _entry = _slicedToArray(entry, 2), + _matchPath = _entry[1]; + + var pathRef = Editor.pathRef(editor, _matchPath); + var isAtEnd = Editor.isEnd(editor, at, _matchPath); + Transforms.splitNodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var path = pathRef.unref(); + at = isAtEnd ? Path.next(path) : path; + } else { + return; + } + } + + var parentPath = Path.parent(at); + var index = at[at.length - 1]; + + if (!voids && Editor["void"](editor, { + at: parentPath + })) { + return; + } + + var _iterator = _createForOfIteratorHelper$1(nodes), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _node = _step.value; + + var _path = parentPath.concat(index); + + index++; + editor.apply({ + type: 'insert_node', + path: _path, + node: _node + }); + at = Path.next(at); + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + at = Path.previous(at); + + if (select) { + var point = Editor.end(editor, at); + + if (point) { + Transforms.select(editor, point); + } + } + }); + }, + + /** + * Lift nodes at a specific location upwards in the document tree, splitting + * their parent in two if necessary. + */ + liftNodes: function liftNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$at = options.at, + at = _options$at === void 0 ? editor.selection : _options$at, + _options$mode2 = options.mode, + mode = _options$mode2 === void 0 ? 'lowest' : _options$mode2, + _options$voids2 = options.voids, + voids = _options$voids2 === void 0 ? false : _options$voids2; + var match = options.match; + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (!at) { + return; + } + + var matches = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(matches, function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + p = _ref2[1]; + + return Editor.pathRef(editor, p); + }); + + for (var _i = 0, _pathRefs = pathRefs; _i < _pathRefs.length; _i++) { + var pathRef = _pathRefs[_i]; + var path = pathRef.unref(); + + if (path.length < 2) { + throw new Error("Cannot lift node at a path [".concat(path, "] because it has a depth of less than `2`.")); + } + + var parentNodeEntry = Editor.node(editor, Path.parent(path)); + + var _parentNodeEntry = _slicedToArray(parentNodeEntry, 2), + parent = _parentNodeEntry[0], + parentPath = _parentNodeEntry[1]; + + var index = path[path.length - 1]; + var length = parent.children.length; + + if (length === 1) { + var toPath = Path.next(parentPath); + Transforms.moveNodes(editor, { + at: path, + to: toPath, + voids: voids + }); + Transforms.removeNodes(editor, { + at: parentPath, + voids: voids + }); + } else if (index === 0) { + Transforms.moveNodes(editor, { + at: path, + to: parentPath, + voids: voids + }); + } else if (index === length - 1) { + var _toPath = Path.next(parentPath); + + Transforms.moveNodes(editor, { + at: path, + to: _toPath, + voids: voids + }); + } else { + var splitPath = Path.next(path); + + var _toPath2 = Path.next(parentPath); + + Transforms.splitNodes(editor, { + at: splitPath, + voids: voids + }); + Transforms.moveNodes(editor, { + at: path, + to: _toPath2, + voids: voids + }); + } + } + }); + }, + + /** + * Merge a node at a location with the previous node of the same depth, + * removing any empty containing nodes after the merge if necessary. + */ + mergeNodes: function mergeNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var match = options.match, + _options$at2 = options.at, + at = _options$at2 === void 0 ? editor.selection : _options$at2; + var _options$hanging2 = options.hanging, + hanging = _options$hanging2 === void 0 ? false : _options$hanging2, + _options$voids3 = options.voids, + voids = _options$voids3 === void 0 ? false : _options$voids3, + _options$mode3 = options.mode, + mode = _options$mode3 === void 0 ? 'lowest' : _options$mode3; + + if (!at) { + return; + } + + if (match == null) { + if (Path.isPath(at)) { + var _Editor$parent = Editor.parent(editor, at), + _Editor$parent2 = _slicedToArray(_Editor$parent, 1), + parent = _Editor$parent2[0]; + + match = function match(n) { + return parent.children.includes(n); + }; + } else { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + } + + if (!hanging && Range.isRange(at)) { + at = Editor.unhangRange(editor, at); + } + + if (Range.isRange(at)) { + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var _Range$edges3 = Range.edges(at), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + end = _Range$edges4[1]; + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at + }); + at = pointRef.unref(); + + if (options.at == null) { + Transforms.select(editor, at); + } + } + } + + var _Editor$nodes3 = Editor.nodes(editor, { + at: at, + match: match, + voids: voids, + mode: mode + }), + _Editor$nodes4 = _slicedToArray(_Editor$nodes3, 1), + current = _Editor$nodes4[0]; + + var prev = Editor.previous(editor, { + at: at, + match: match, + voids: voids, + mode: mode + }); + + if (!current || !prev) { + return; + } + + var _current = _slicedToArray(current, 2), + node = _current[0], + path = _current[1]; + + var _prev = _slicedToArray(prev, 2), + prevNode = _prev[0], + prevPath = _prev[1]; + + if (path.length === 0 || prevPath.length === 0) { + return; + } + + var newPath = Path.next(prevPath); + var commonPath = Path.common(path, prevPath); + var isPreviousSibling = Path.isSibling(path, prevPath); + var levels = Array.from(Editor.levels(editor, { + at: path + }), function (_ref3) { + var _ref4 = _slicedToArray(_ref3, 1), + n = _ref4[0]; + + return n; + }).slice(commonPath.length).slice(0, -1); // Determine if the merge will leave an ancestor of the path empty as a + // result, in which case we'll want to remove it after merging. + + var emptyAncestor = Editor.above(editor, { + at: path, + mode: 'highest', + match: function match(n) { + return levels.includes(n) && hasSingleChildNest(editor, n); + } + }); + var emptyRef = emptyAncestor && Editor.pathRef(editor, emptyAncestor[1]); + var properties; + var position; // Ensure that the nodes are equivalent, and figure out what the position + // and extra properties of the merge will be. + + if (Text.isText(node) && Text.isText(prevNode)) { + node.text; + var rest = _objectWithoutProperties(node, _excluded); + + position = prevNode.text.length; + properties = rest; + } else if (Element$1.isElement(node) && Element$1.isElement(prevNode)) { + node.children; + var _rest = _objectWithoutProperties(node, _excluded2); + + position = prevNode.children.length; + properties = _rest; + } else { + throw new Error("Cannot merge the node at path [".concat(path, "] with the previous sibling because it is not the same kind: ").concat(JSON.stringify(node), " ").concat(JSON.stringify(prevNode))); + } // If the node isn't already the next sibling of the previous node, move + // it so that it is before merging. + + + if (!isPreviousSibling) { + Transforms.moveNodes(editor, { + at: path, + to: newPath, + voids: voids + }); + } // If there was going to be an empty ancestor of the node that was merged, + // we remove it from the tree. + + + if (emptyRef) { + Transforms.removeNodes(editor, { + at: emptyRef.current, + voids: voids + }); + } // If the target node that we're merging with is empty, remove it instead + // of merging the two. This is a common rich text editor behavior to + // prevent losing formatting when deleting entire nodes when you have a + // hanging selection. + // if prevNode is first child in parent,don't remove it. + + + if (Element$1.isElement(prevNode) && Editor.isEmpty(editor, prevNode) || Text.isText(prevNode) && prevNode.text === '' && prevPath[prevPath.length - 1] !== 0) { + Transforms.removeNodes(editor, { + at: prevPath, + voids: voids + }); + } else { + editor.apply({ + type: 'merge_node', + path: newPath, + position: position, + properties: properties + }); + } + + if (emptyRef) { + emptyRef.unref(); + } + }); + }, + + /** + * Move the nodes at a location to a new location. + */ + moveNodes: function moveNodes(editor, options) { + Editor.withoutNormalizing(editor, function () { + var to = options.to, + _options$at3 = options.at, + at = _options$at3 === void 0 ? editor.selection : _options$at3, + _options$mode4 = options.mode, + mode = _options$mode4 === void 0 ? 'lowest' : _options$mode4, + _options$voids4 = options.voids, + voids = _options$voids4 === void 0 ? false : _options$voids4; + var match = options.match; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + var toRef = Editor.pathRef(editor, to); + var targets = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(targets, function (_ref5) { + var _ref6 = _slicedToArray(_ref5, 2), + p = _ref6[1]; + + return Editor.pathRef(editor, p); + }); + + for (var _i2 = 0, _pathRefs2 = pathRefs; _i2 < _pathRefs2.length; _i2++) { + var pathRef = _pathRefs2[_i2]; + var path = pathRef.unref(); + var newPath = toRef.current; + + if (path.length !== 0) { + editor.apply({ + type: 'move_node', + path: path, + newPath: newPath + }); + } + + if (toRef.current && Path.isSibling(newPath, path) && Path.isAfter(newPath, path)) { + // When performing a sibling move to a later index, the path at the destination is shifted + // to before the insertion point instead of after. To ensure our group of nodes are inserted + // in the correct order we increment toRef to account for that + toRef.current = Path.next(toRef.current); + } + } + + toRef.unref(); + }); + }, + + /** + * Remove the nodes at a specific location in the document. + */ + removeNodes: function removeNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$hanging3 = options.hanging, + hanging = _options$hanging3 === void 0 ? false : _options$hanging3, + _options$voids5 = options.voids, + voids = _options$voids5 === void 0 ? false : _options$voids5, + _options$mode5 = options.mode, + mode = _options$mode5 === void 0 ? 'lowest' : _options$mode5; + var _options$at4 = options.at, + at = _options$at4 === void 0 ? editor.selection : _options$at4, + match = options.match; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (!hanging && Range.isRange(at)) { + at = Editor.unhangRange(editor, at); + } + + var depths = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(depths, function (_ref7) { + var _ref8 = _slicedToArray(_ref7, 2), + p = _ref8[1]; + + return Editor.pathRef(editor, p); + }); + + for (var _i3 = 0, _pathRefs3 = pathRefs; _i3 < _pathRefs3.length; _i3++) { + var pathRef = _pathRefs3[_i3]; + var path = pathRef.unref(); + + if (path) { + var _Editor$node = Editor.node(editor, path), + _Editor$node2 = _slicedToArray(_Editor$node, 1), + node = _Editor$node2[0]; + + editor.apply({ + type: 'remove_node', + path: path, + node: node + }); + } + } + }); + }, + + /** + * Set new properties on the nodes at a location. + */ + setNodes: function setNodes(editor, props) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var match = options.match, + _options$at5 = options.at, + at = _options$at5 === void 0 ? editor.selection : _options$at5; + var _options$hanging4 = options.hanging, + hanging = _options$hanging4 === void 0 ? false : _options$hanging4, + _options$mode6 = options.mode, + mode = _options$mode6 === void 0 ? 'lowest' : _options$mode6, + _options$split = options.split, + split = _options$split === void 0 ? false : _options$split, + _options$voids6 = options.voids, + voids = _options$voids6 === void 0 ? false : _options$voids6; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (!hanging && Range.isRange(at)) { + at = Editor.unhangRange(editor, at); + } + + if (split && Range.isRange(at)) { + if (Range.isCollapsed(at) && Editor.leaf(editor, at.anchor)[0].text.length > 0) { + // If the range is collapsed in a non-empty node and 'split' is true, there's nothing to + // set that won't get normalized away + return; + } + + var rangeRef = Editor.rangeRef(editor, at, { + affinity: 'inward' + }); + + var _Range$edges5 = Range.edges(at), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + start = _Range$edges6[0], + end = _Range$edges6[1]; + + var splitMode = mode === 'lowest' ? 'lowest' : 'highest'; + var endAtEndOfNode = Editor.isEnd(editor, end, end.path); + Transforms.splitNodes(editor, { + at: end, + match: match, + mode: splitMode, + voids: voids, + always: !endAtEndOfNode + }); + var startAtStartOfNode = Editor.isStart(editor, start, start.path); + Transforms.splitNodes(editor, { + at: start, + match: match, + mode: splitMode, + voids: voids, + always: !startAtStartOfNode + }); + at = rangeRef.unref(); + + if (options.at == null) { + Transforms.select(editor, at); + } + } + + var _iterator2 = _createForOfIteratorHelper$1(Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + })), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _step2$value = _slicedToArray(_step2.value, 2), + node = _step2$value[0], + path = _step2$value[1]; + + var properties = {}; + var newProperties = {}; // You can't set properties on the editor node. + + if (path.length === 0) { + continue; + } + + var hasChanges = false; + + for (var k in props) { + if (k === 'children' || k === 'text') { + continue; + } + + if (props[k] !== node[k]) { + hasChanges = true; // Omit new properties from the old properties list + + if (node.hasOwnProperty(k)) properties[k] = node[k]; // Omit properties that have been removed from the new properties list + + if (props[k] != null) newProperties[k] = props[k]; + } + } + + if (hasChanges) { + editor.apply({ + type: 'set_node', + path: path, + properties: properties, + newProperties: newProperties + }); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + }); + }, + + /** + * Split the nodes at a specific location. + */ + splitNodes: function splitNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$mode7 = options.mode, + mode = _options$mode7 === void 0 ? 'lowest' : _options$mode7, + _options$voids7 = options.voids, + voids = _options$voids7 === void 0 ? false : _options$voids7; + var match = options.match, + _options$at6 = options.at, + at = _options$at6 === void 0 ? editor.selection : _options$at6, + _options$height = options.height, + height = _options$height === void 0 ? 0 : _options$height, + _options$always = options.always, + always = _options$always === void 0 ? false : _options$always; + + if (match == null) { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + + if (Range.isRange(at)) { + at = deleteRange(editor, at); + } // If the target is a path, the default height-skipping and position + // counters need to account for us potentially splitting at a non-leaf. + + + if (Path.isPath(at)) { + var path = at; + var point = Editor.point(editor, path); + + var _Editor$parent3 = Editor.parent(editor, path), + _Editor$parent4 = _slicedToArray(_Editor$parent3, 1), + parent = _Editor$parent4[0]; + + match = function match(n) { + return n === parent; + }; + + height = point.path.length - path.length + 1; + at = point; + always = true; + } + + if (!at) { + return; + } + + var beforeRef = Editor.pointRef(editor, at, { + affinity: 'backward' + }); + + var _Editor$nodes5 = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }), + _Editor$nodes6 = _slicedToArray(_Editor$nodes5, 1), + highest = _Editor$nodes6[0]; + + if (!highest) { + return; + } + + var voidMatch = Editor["void"](editor, { + at: at, + mode: 'highest' + }); + var nudge = 0; + + if (!voids && voidMatch) { + var _voidMatch = _slicedToArray(voidMatch, 2), + voidNode = _voidMatch[0], + voidPath = _voidMatch[1]; + + if (Element$1.isElement(voidNode) && editor.isInline(voidNode)) { + var after = Editor.after(editor, voidPath); + + if (!after) { + var text = { + text: '' + }; + var afterPath = Path.next(voidPath); + Transforms.insertNodes(editor, text, { + at: afterPath, + voids: voids + }); + after = Editor.point(editor, afterPath); + } + + at = after; + always = true; + } + + var siblingHeight = at.path.length - voidPath.length; + height = siblingHeight + 1; + always = true; + } + + var afterRef = Editor.pointRef(editor, at); + var depth = at.path.length - height; + + var _highest = _slicedToArray(highest, 2), + highestPath = _highest[1]; + + var lowestPath = at.path.slice(0, depth); + var position = height === 0 ? at.offset : at.path[depth] + nudge; + + var _iterator3 = _createForOfIteratorHelper$1(Editor.levels(editor, { + at: lowestPath, + reverse: true, + voids: voids + })), + _step3; + + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _step3$value = _slicedToArray(_step3.value, 2), + node = _step3$value[0], + _path2 = _step3$value[1]; + + var split = false; + + if (_path2.length < highestPath.length || _path2.length === 0 || !voids && Editor.isVoid(editor, node)) { + break; + } + + var _point2 = beforeRef.current; + var isEnd = Editor.isEnd(editor, _point2, _path2); + + if (always || !beforeRef || !Editor.isEdge(editor, _point2, _path2)) { + split = true; + var properties = Node$1.extractProps(node); + editor.apply({ + type: 'split_node', + path: _path2, + position: position, + properties: properties + }); + } + + position = _path2[_path2.length - 1] + (split || isEnd ? 1 : 0); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); + } + + if (options.at == null) { + var _point = afterRef.current || Editor.end(editor, []); + + Transforms.select(editor, _point); + } + + beforeRef.unref(); + afterRef.unref(); + }); + }, + + /** + * Unset properties on the nodes at a location. + */ + unsetNodes: function unsetNodes(editor, props) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + if (!Array.isArray(props)) { + props = [props]; + } + + var obj = {}; + + var _iterator4 = _createForOfIteratorHelper$1(props), + _step4; + + try { + for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { + var key = _step4.value; + obj[key] = null; + } + } catch (err) { + _iterator4.e(err); + } finally { + _iterator4.f(); + } + + Transforms.setNodes(editor, obj, options); + }, + + /** + * Unwrap the nodes at a location from a parent node, splitting the parent if + * necessary to ensure that only the content in the range is unwrapped. + */ + unwrapNodes: function unwrapNodes(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$mode8 = options.mode, + mode = _options$mode8 === void 0 ? 'lowest' : _options$mode8, + _options$split2 = options.split, + split = _options$split2 === void 0 ? false : _options$split2, + _options$voids8 = options.voids, + voids = _options$voids8 === void 0 ? false : _options$voids8; + var _options$at7 = options.at, + at = _options$at7 === void 0 ? editor.selection : _options$at7, + match = options.match; + + if (!at) { + return; + } + + if (match == null) { + match = Path.isPath(at) ? matchPath(editor, at) : function (n) { + return Editor.isBlock(editor, n); + }; + } + + if (Path.isPath(at)) { + at = Editor.range(editor, at); + } + + var rangeRef = Range.isRange(at) ? Editor.rangeRef(editor, at) : null; + var matches = Editor.nodes(editor, { + at: at, + match: match, + mode: mode, + voids: voids + }); + var pathRefs = Array.from(matches, function (_ref9) { + var _ref10 = _slicedToArray(_ref9, 2), + p = _ref10[1]; + + return Editor.pathRef(editor, p); + } // unwrapNode will call liftNode which does not support splitting the node when nested. + // If we do not reverse the order and call it from top to the bottom, it will remove all blocks + // that wrap target node. So we reverse the order. + ).reverse(); + + var _iterator5 = _createForOfIteratorHelper$1(pathRefs), + _step5; + + try { + var _loop = function _loop() { + var pathRef = _step5.value; + var path = pathRef.unref(); + + var _Editor$node3 = Editor.node(editor, path), + _Editor$node4 = _slicedToArray(_Editor$node3, 1), + node = _Editor$node4[0]; + + var range = Editor.range(editor, path); + + if (split && rangeRef) { + range = Range.intersection(rangeRef.current, range); + } + + Transforms.liftNodes(editor, { + at: range, + match: function match(n) { + return Element$1.isAncestor(node) && node.children.includes(n); + }, + voids: voids + }); + }; + + for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { + _loop(); + } + } catch (err) { + _iterator5.e(err); + } finally { + _iterator5.f(); + } + + if (rangeRef) { + rangeRef.unref(); + } + }); + }, + + /** + * Wrap the nodes at a location in a new container node, splitting the edges + * of the range first to ensure that only the content in the range is wrapped. + */ + wrapNodes: function wrapNodes(editor, element) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$mode9 = options.mode, + mode = _options$mode9 === void 0 ? 'lowest' : _options$mode9, + _options$split3 = options.split, + split = _options$split3 === void 0 ? false : _options$split3, + _options$voids9 = options.voids, + voids = _options$voids9 === void 0 ? false : _options$voids9; + var match = options.match, + _options$at8 = options.at, + at = _options$at8 === void 0 ? editor.selection : _options$at8; + + if (!at) { + return; + } + + if (match == null) { + if (Path.isPath(at)) { + match = matchPath(editor, at); + } else if (editor.isInline(element)) { + match = function match(n) { + return Editor.isInline(editor, n) || Text.isText(n); + }; + } else { + match = function match(n) { + return Editor.isBlock(editor, n); + }; + } + } + + if (split && Range.isRange(at)) { + var _Range$edges7 = Range.edges(at), + _Range$edges8 = _slicedToArray(_Range$edges7, 2), + start = _Range$edges8[0], + end = _Range$edges8[1]; + + var rangeRef = Editor.rangeRef(editor, at, { + affinity: 'inward' + }); + Transforms.splitNodes(editor, { + at: end, + match: match, + voids: voids + }); + Transforms.splitNodes(editor, { + at: start, + match: match, + voids: voids + }); + at = rangeRef.unref(); + + if (options.at == null) { + Transforms.select(editor, at); + } + } + + var roots = Array.from(Editor.nodes(editor, { + at: at, + match: editor.isInline(element) ? function (n) { + return Editor.isBlock(editor, n); + } : function (n) { + return Editor.isEditor(n); + }, + mode: 'lowest', + voids: voids + })); + + for (var _i4 = 0, _roots = roots; _i4 < _roots.length; _i4++) { + var _roots$_i = _slicedToArray(_roots[_i4], 2), + rootPath = _roots$_i[1]; + + var a = Range.isRange(at) ? Range.intersection(at, Editor.range(editor, rootPath)) : at; + + if (!a) { + continue; + } + + var matches = Array.from(Editor.nodes(editor, { + at: a, + match: match, + mode: mode, + voids: voids + })); + + if (matches.length > 0) { + var _ret = function () { + var _matches = _slicedToArray(matches, 1), + first = _matches[0]; + + var last = matches[matches.length - 1]; + + var _first = _slicedToArray(first, 2), + firstPath = _first[1]; + + var _last = _slicedToArray(last, 2), + lastPath = _last[1]; + + if (firstPath.length === 0 && lastPath.length === 0) { + // if there's no matching parent - usually means the node is an editor - don't do anything + return "continue"; + } + + var commonPath = Path.equals(firstPath, lastPath) ? Path.parent(firstPath) : Path.common(firstPath, lastPath); + var range = Editor.range(editor, firstPath, lastPath); + var commonNodeEntry = Editor.node(editor, commonPath); + + var _commonNodeEntry = _slicedToArray(commonNodeEntry, 1), + commonNode = _commonNodeEntry[0]; + + var depth = commonPath.length + 1; + var wrapperPath = Path.next(lastPath.slice(0, depth)); + + var wrapper = _objectSpread$2(_objectSpread$2({}, element), {}, { + children: [] + }); + + Transforms.insertNodes(editor, wrapper, { + at: wrapperPath, + voids: voids + }); + Transforms.moveNodes(editor, { + at: range, + match: function match(n) { + return Element$1.isAncestor(commonNode) && commonNode.children.includes(n); + }, + to: wrapperPath.concat(0), + voids: voids + }); + }(); + + if (_ret === "continue") continue; + } + } + }); + } + }; + + var hasSingleChildNest = function hasSingleChildNest(editor, node) { + if (Element$1.isElement(node)) { + var element = node; + + if (Editor.isVoid(editor, node)) { + return true; + } else if (element.children.length === 1) { + return hasSingleChildNest(editor, element.children[0]); + } else { + return false; + } + } else if (Editor.isEditor(node)) { + return false; + } else { + return true; + } + }; + /** + * Convert a range into a point by deleting it's content. + */ + + + var deleteRange = function deleteRange(editor, range) { + if (Range.isCollapsed(range)) { + return range.anchor; + } else { + var _Range$edges9 = Range.edges(range), + _Range$edges10 = _slicedToArray(_Range$edges9, 2), + end = _Range$edges10[1]; + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: range + }); + return pointRef.unref(); + } + }; + + var matchPath = function matchPath(editor, path) { + var _Editor$node5 = Editor.node(editor, path), + _Editor$node6 = _slicedToArray(_Editor$node5, 1), + node = _Editor$node6[0]; + + return function (n) { + return n === node; + }; + }; + + function ownKeys$1(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread$1(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys$1(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys$1(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var SelectionTransforms = { + /** + * Collapse the selection. + */ + collapse: function collapse(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var _options$edge = options.edge, + edge = _options$edge === void 0 ? 'anchor' : _options$edge; + var selection = editor.selection; + + if (!selection) { + return; + } else if (edge === 'anchor') { + Transforms.select(editor, selection.anchor); + } else if (edge === 'focus') { + Transforms.select(editor, selection.focus); + } else if (edge === 'start') { + var _Range$edges = Range.edges(selection), + _Range$edges2 = _slicedToArray(_Range$edges, 1), + start = _Range$edges2[0]; + + Transforms.select(editor, start); + } else if (edge === 'end') { + var _Range$edges3 = Range.edges(selection), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + end = _Range$edges4[1]; + + Transforms.select(editor, end); + } + }, + + /** + * Unset the selection. + */ + deselect: function deselect(editor) { + var selection = editor.selection; + + if (selection) { + editor.apply({ + type: 'set_selection', + properties: selection, + newProperties: null + }); + } + }, + + /** + * Move the selection's point forward or backward. + */ + move: function move(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var selection = editor.selection; + var _options$distance = options.distance, + distance = _options$distance === void 0 ? 1 : _options$distance, + _options$unit = options.unit, + unit = _options$unit === void 0 ? 'character' : _options$unit, + _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse; + var _options$edge2 = options.edge, + edge = _options$edge2 === void 0 ? null : _options$edge2; + + if (!selection) { + return; + } + + if (edge === 'start') { + edge = Range.isBackward(selection) ? 'focus' : 'anchor'; + } + + if (edge === 'end') { + edge = Range.isBackward(selection) ? 'anchor' : 'focus'; + } + + var anchor = selection.anchor, + focus = selection.focus; + var opts = { + distance: distance, + unit: unit + }; + var props = {}; + + if (edge == null || edge === 'anchor') { + var point = reverse ? Editor.before(editor, anchor, opts) : Editor.after(editor, anchor, opts); + + if (point) { + props.anchor = point; + } + } + + if (edge == null || edge === 'focus') { + var _point = reverse ? Editor.before(editor, focus, opts) : Editor.after(editor, focus, opts); + + if (_point) { + props.focus = _point; + } + } + + Transforms.setSelection(editor, props); + }, + + /** + * Set the selection to a new value. + */ + select: function select(editor, target) { + var selection = editor.selection; + target = Editor.range(editor, target); + + if (selection) { + Transforms.setSelection(editor, target); + return; + } + + if (!Range.isRange(target)) { + throw new Error("When setting the selection and the current selection is `null` you must provide at least an `anchor` and `focus`, but you passed: ".concat(JSON.stringify(target))); + } + + editor.apply({ + type: 'set_selection', + properties: selection, + newProperties: target + }); + }, + + /** + * Set new properties on one of the selection's points. + */ + setPoint: function setPoint(editor, props) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var selection = editor.selection; + var _options$edge3 = options.edge, + edge = _options$edge3 === void 0 ? 'both' : _options$edge3; + + if (!selection) { + return; + } + + if (edge === 'start') { + edge = Range.isBackward(selection) ? 'focus' : 'anchor'; + } + + if (edge === 'end') { + edge = Range.isBackward(selection) ? 'anchor' : 'focus'; + } + + var anchor = selection.anchor, + focus = selection.focus; + var point = edge === 'anchor' ? anchor : focus; + Transforms.setSelection(editor, _defineProperty({}, edge === 'anchor' ? 'anchor' : 'focus', _objectSpread$1(_objectSpread$1({}, point), props))); + }, + + /** + * Set new properties on the selection. + */ + setSelection: function setSelection(editor, props) { + var selection = editor.selection; + var oldProps = {}; + var newProps = {}; + + if (!selection) { + return; + } + + for (var k in props) { + if (k === 'anchor' && props.anchor != null && !Point.equals(props.anchor, selection.anchor) || k === 'focus' && props.focus != null && !Point.equals(props.focus, selection.focus) || k !== 'anchor' && k !== 'focus' && props[k] !== selection[k]) { + oldProps[k] = selection[k]; + newProps[k] = props[k]; + } + } + + if (Object.keys(oldProps).length > 0) { + editor.apply({ + type: 'set_selection', + properties: oldProps, + newProperties: newProps + }); + } + } + }; + + function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } + + function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + + function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + var TextTransforms = { + /** + * Delete content in the editor. + */ + "delete": function _delete(editor) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$reverse = options.reverse, + reverse = _options$reverse === void 0 ? false : _options$reverse, + _options$unit = options.unit, + unit = _options$unit === void 0 ? 'character' : _options$unit, + _options$distance = options.distance, + distance = _options$distance === void 0 ? 1 : _options$distance, + _options$voids = options.voids, + voids = _options$voids === void 0 ? false : _options$voids; + var _options$at = options.at, + at = _options$at === void 0 ? editor.selection : _options$at, + _options$hanging = options.hanging, + hanging = _options$hanging === void 0 ? false : _options$hanging; + + if (!at) { + return; + } + + if (Range.isRange(at) && Range.isCollapsed(at)) { + at = at.anchor; + } + + if (Point.isPoint(at)) { + var furthestVoid = Editor["void"](editor, { + at: at, + mode: 'highest' + }); + + if (!voids && furthestVoid) { + var _furthestVoid = _slicedToArray(furthestVoid, 2), + voidPath = _furthestVoid[1]; + + at = voidPath; + } else { + var opts = { + unit: unit, + distance: distance + }; + var target = reverse ? Editor.before(editor, at, opts) || Editor.start(editor, []) : Editor.after(editor, at, opts) || Editor.end(editor, []); + at = { + anchor: at, + focus: target + }; + hanging = true; + } + } + + if (Path.isPath(at)) { + Transforms.removeNodes(editor, { + at: at, + voids: voids + }); + return; + } + + if (Range.isCollapsed(at)) { + return; + } + + if (!hanging) { + var _Range$edges = Range.edges(at), + _Range$edges2 = _slicedToArray(_Range$edges, 2), + _end = _Range$edges2[1]; + + var endOfDoc = Editor.end(editor, []); + + if (!Point.equals(_end, endOfDoc)) { + at = Editor.unhangRange(editor, at, { + voids: voids + }); + } + } + + var _Range$edges3 = Range.edges(at), + _Range$edges4 = _slicedToArray(_Range$edges3, 2), + start = _Range$edges4[0], + end = _Range$edges4[1]; + + var startBlock = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + }, + at: start, + voids: voids + }); + var endBlock = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + }, + at: end, + voids: voids + }); + var isAcrossBlocks = startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1]); + var isSingleText = Path.equals(start.path, end.path); + var startVoid = voids ? null : Editor["void"](editor, { + at: start, + mode: 'highest' + }); + var endVoid = voids ? null : Editor["void"](editor, { + at: end, + mode: 'highest' + }); // If the start or end points are inside an inline void, nudge them out. + + if (startVoid) { + var before = Editor.before(editor, start); + + if (before && startBlock && Path.isAncestor(startBlock[1], before.path)) { + start = before; + } + } + + if (endVoid) { + var after = Editor.after(editor, end); + + if (after && endBlock && Path.isAncestor(endBlock[1], after.path)) { + end = after; + } + } // Get the highest nodes that are completely inside the range, as well as + // the start and end nodes. + + + var matches = []; + var lastPath; + + var _iterator = _createForOfIteratorHelper(Editor.nodes(editor, { + at: at, + voids: voids + })), + _step; + + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var entry = _step.value; + + var _entry = _slicedToArray(entry, 2), + _node2 = _entry[0], + _path3 = _entry[1]; + + if (lastPath && Path.compare(_path3, lastPath) === 0) { + continue; + } + + if (!voids && Editor.isVoid(editor, _node2) || !Path.isCommon(_path3, start.path) && !Path.isCommon(_path3, end.path)) { + matches.push(entry); + lastPath = _path3; + } + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + + var pathRefs = Array.from(matches, function (_ref) { + var _ref2 = _slicedToArray(_ref, 2), + p = _ref2[1]; + + return Editor.pathRef(editor, p); + }); + var startRef = Editor.pointRef(editor, start); + var endRef = Editor.pointRef(editor, end); + + if (!isSingleText && !startVoid) { + var _point = startRef.current; + + var _Editor$leaf = Editor.leaf(editor, _point), + _Editor$leaf2 = _slicedToArray(_Editor$leaf, 1), + node = _Editor$leaf2[0]; + + var path = _point.path; + var _start = start, + offset = _start.offset; + var text = node.text.slice(offset); + if (text.length > 0) editor.apply({ + type: 'remove_text', + path: path, + offset: offset, + text: text + }); + } + + for (var _i = 0, _pathRefs = pathRefs; _i < _pathRefs.length; _i++) { + var pathRef = _pathRefs[_i]; + + var _path = pathRef.unref(); + + Transforms.removeNodes(editor, { + at: _path, + voids: voids + }); + } + + if (!endVoid) { + var _point2 = endRef.current; + + var _Editor$leaf3 = Editor.leaf(editor, _point2), + _Editor$leaf4 = _slicedToArray(_Editor$leaf3, 1), + _node = _Editor$leaf4[0]; + + var _path2 = _point2.path; + + var _offset = isSingleText ? start.offset : 0; + + var _text = _node.text.slice(_offset, end.offset); + + if (_text.length > 0) editor.apply({ + type: 'remove_text', + path: _path2, + offset: _offset, + text: _text + }); + } + + if (!isSingleText && isAcrossBlocks && endRef.current && startRef.current) { + Transforms.mergeNodes(editor, { + at: endRef.current, + hanging: true, + voids: voids + }); + } + + var point = reverse ? startRef.unref() || endRef.unref() : endRef.unref() || startRef.unref(); + + if (options.at == null && point) { + Transforms.select(editor, point); + } + }); + }, + + /** + * Insert a fragment at a specific location in the editor. + */ + insertFragment: function insertFragment(editor, fragment) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$hanging2 = options.hanging, + hanging = _options$hanging2 === void 0 ? false : _options$hanging2, + _options$voids2 = options.voids, + voids = _options$voids2 === void 0 ? false : _options$voids2; + var _options$at2 = options.at, + at = _options$at2 === void 0 ? editor.selection : _options$at2; + + if (!fragment.length) { + return; + } + + if (!at) { + return; + } else if (Range.isRange(at)) { + if (!hanging) { + at = Editor.unhangRange(editor, at); + } + + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var _Range$edges5 = Range.edges(at), + _Range$edges6 = _slicedToArray(_Range$edges5, 2), + end = _Range$edges6[1]; + + if (!voids && Editor["void"](editor, { + at: end + })) { + return; + } + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at + }); + at = pointRef.unref(); + } + } else if (Path.isPath(at)) { + at = Editor.start(editor, at); + } + + if (!voids && Editor["void"](editor, { + at: at + })) { + return; + } // If the insert point is at the edge of an inline node, move it outside + // instead since it will need to be split otherwise. + + + var inlineElementMatch = Editor.above(editor, { + at: at, + match: function match(n) { + return Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }); + + if (inlineElementMatch) { + var _inlineElementMatch = _slicedToArray(inlineElementMatch, 2), + _inlinePath = _inlineElementMatch[1]; + + if (Editor.isEnd(editor, at, _inlinePath)) { + var after = Editor.after(editor, _inlinePath); + at = after; + } else if (Editor.isStart(editor, at, _inlinePath)) { + var before = Editor.before(editor, _inlinePath); + at = before; + } + } + + var blockMatch = Editor.above(editor, { + match: function match(n) { + return Editor.isBlock(editor, n); + }, + at: at, + voids: voids + }); + + var _blockMatch = _slicedToArray(blockMatch, 2), + blockPath = _blockMatch[1]; + + var isBlockStart = Editor.isStart(editor, at, blockPath); + var isBlockEnd = Editor.isEnd(editor, at, blockPath); + var isBlockEmpty = isBlockStart && isBlockEnd; + var mergeStart = !isBlockStart || isBlockStart && isBlockEnd; + var mergeEnd = !isBlockEnd; + + var _Node$first = Node$1.first({ + children: fragment + }, []), + _Node$first2 = _slicedToArray(_Node$first, 2), + firstPath = _Node$first2[1]; + + var _Node$last = Node$1.last({ + children: fragment + }, []), + _Node$last2 = _slicedToArray(_Node$last, 2), + lastPath = _Node$last2[1]; + + var matches = []; + + var matcher = function matcher(_ref3) { + var _ref4 = _slicedToArray(_ref3, 2), + n = _ref4[0], + p = _ref4[1]; + + var isRoot = p.length === 0; + + if (isRoot) { + return false; + } + + if (isBlockEmpty) { + return true; + } + + if (mergeStart && Path.isAncestor(p, firstPath) && Element$1.isElement(n) && !editor.isVoid(n) && !editor.isInline(n)) { + return false; + } + + if (mergeEnd && Path.isAncestor(p, lastPath) && Element$1.isElement(n) && !editor.isVoid(n) && !editor.isInline(n)) { + return false; + } + + return true; + }; + + var _iterator2 = _createForOfIteratorHelper(Node$1.nodes({ + children: fragment + }, { + pass: matcher + })), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var entry = _step2.value; + + if (matcher(entry)) { + matches.push(entry); + } + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + var starts = []; + var middles = []; + var ends = []; + var starting = true; + var hasBlocks = false; + + for (var _i2 = 0, _matches = matches; _i2 < _matches.length; _i2++) { + var _matches$_i = _slicedToArray(_matches[_i2], 1), + node = _matches$_i[0]; + + if (Element$1.isElement(node) && !editor.isInline(node)) { + starting = false; + hasBlocks = true; + middles.push(node); + } else if (starting) { + starts.push(node); + } else { + ends.push(node); + } + } + + var _Editor$nodes = Editor.nodes(editor, { + at: at, + match: function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }), + _Editor$nodes2 = _slicedToArray(_Editor$nodes, 1), + inlineMatch = _Editor$nodes2[0]; + + var _inlineMatch = _slicedToArray(inlineMatch, 2), + inlinePath = _inlineMatch[1]; + + var isInlineStart = Editor.isStart(editor, at, inlinePath); + var isInlineEnd = Editor.isEnd(editor, at, inlinePath); + var middleRef = Editor.pathRef(editor, isBlockEnd ? Path.next(blockPath) : blockPath); + var endRef = Editor.pathRef(editor, isInlineEnd ? Path.next(inlinePath) : inlinePath); + var blockPathRef = Editor.pathRef(editor, blockPath); + Transforms.splitNodes(editor, { + at: at, + match: function match(n) { + return hasBlocks ? Editor.isBlock(editor, n) : Text.isText(n) || Editor.isInline(editor, n); + }, + mode: hasBlocks ? 'lowest' : 'highest', + voids: voids + }); + var startRef = Editor.pathRef(editor, !isInlineStart || isInlineStart && isInlineEnd ? Path.next(inlinePath) : inlinePath); + Transforms.insertNodes(editor, starts, { + at: startRef.current, + match: function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }); + + if (isBlockEmpty && middles.length) { + Transforms["delete"](editor, { + at: blockPathRef.unref(), + voids: voids + }); + } + + Transforms.insertNodes(editor, middles, { + at: middleRef.current, + match: function match(n) { + return Editor.isBlock(editor, n); + }, + mode: 'lowest', + voids: voids + }); + Transforms.insertNodes(editor, ends, { + at: endRef.current, + match: function match(n) { + return Text.isText(n) || Editor.isInline(editor, n); + }, + mode: 'highest', + voids: voids + }); + + if (!options.at) { + var path; + + if (ends.length > 0) { + path = Path.previous(endRef.current); + } else if (middles.length > 0) { + path = Path.previous(middleRef.current); + } else { + path = Path.previous(startRef.current); + } + + var _end2 = Editor.end(editor, path); + + Transforms.select(editor, _end2); + } + + startRef.unref(); + middleRef.unref(); + endRef.unref(); + }); + }, + + /** + * Insert a string of text in the Editor. + */ + insertText: function insertText(editor, text) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + Editor.withoutNormalizing(editor, function () { + var _options$voids3 = options.voids, + voids = _options$voids3 === void 0 ? false : _options$voids3; + var _options$at3 = options.at, + at = _options$at3 === void 0 ? editor.selection : _options$at3; + + if (!at) { + return; + } + + if (Path.isPath(at)) { + at = Editor.range(editor, at); + } + + if (Range.isRange(at)) { + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + var end = Range.end(at); + + if (!voids && Editor["void"](editor, { + at: end + })) { + return; + } + + var pointRef = Editor.pointRef(editor, end); + Transforms["delete"](editor, { + at: at, + voids: voids + }); + at = pointRef.unref(); + Transforms.setSelection(editor, { + anchor: at, + focus: at + }); + } + } + + if (!voids && Editor["void"](editor, { + at: at + })) { + return; + } + + var _at = at, + path = _at.path, + offset = _at.offset; + if (text.length > 0) editor.apply({ + type: 'insert_text', + path: path, + offset: offset, + text: text + }); + }); + } + }; + + function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } + + function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } + var Transforms = _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, GeneralTransforms), NodeTransforms), SelectionTransforms), TextTransforms); + + var Editor_1 = Editor; + var Element_1 = Element$1; + var Location_1 = Location; + var Node_1 = Node$1; + var Operation_1 = Operation; + var Path_1 = Path; + var PathRef_1 = PathRef; + var Point_1 = Point; + var PointRef_1 = PointRef; + var Range_1 = Range; + var RangeRef_1 = RangeRef; + var Span_1 = Span; + var Text_1 = Text; + var Transforms_1 = Transforms; + var createEditor_1 = createEditor$1; + + + var dist$7 = /*#__PURE__*/Object.defineProperty({ + Editor: Editor_1, + Element: Element_1, + Location: Location_1, + Node: Node_1, + Operation: Operation_1, + Path: Path_1, + PathRef: PathRef_1, + Point: Point_1, + PointRef: PointRef_1, + Range: Range_1, + RangeRef: RangeRef_1, + Span: Span_1, + Text: Text_1, + Transforms: Transforms_1, + createEditor: createEditor_1 + }, '__esModule', {value: true}); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as references for various `Number` constants. */ + var MAX_SAFE_INTEGER$1 = 9007199254740991; + + /** `Object#toString` result references. */ + var argsTag$1 = '[object Arguments]', + funcTag$1 = '[object Function]', + genTag$1 = '[object GeneratorFunction]', + mapTag = '[object Map]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + setTag = '[object Set]', + stringTag = '[object String]', + weakMapTag = '[object WeakMap]'; + + var dataViewTag = '[object DataView]'; + + /** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ + var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + + /** Used to detect host constructors (Safari). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect unsigned integer values. */ + var reIsUint$1 = /^(?:0|[1-9]\d*)$/; + + /** Used to compose unicode character classes. */ + var rsAstralRange$1 = '\\ud800-\\udfff', + rsComboMarksRange$1 = '\\u0300-\\u036f\\ufe20-\\ufe23', + rsComboSymbolsRange$1 = '\\u20d0-\\u20f0', + rsVarRange$1 = '\\ufe0e\\ufe0f'; + + /** Used to compose unicode capture groups. */ + var rsAstral$1 = '[' + rsAstralRange$1 + ']', + rsCombo$1 = '[' + rsComboMarksRange$1 + rsComboSymbolsRange$1 + ']', + rsFitz$1 = '\\ud83c[\\udffb-\\udfff]', + rsModifier$1 = '(?:' + rsCombo$1 + '|' + rsFitz$1 + ')', + rsNonAstral$1 = '[^' + rsAstralRange$1 + ']', + rsRegional$1 = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair$1 = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsZWJ$1 = '\\u200d'; + + /** Used to compose unicode regexes. */ + var reOptMod$1 = rsModifier$1 + '?', + rsOptVar$1 = '[' + rsVarRange$1 + ']?', + rsOptJoin$1 = '(?:' + rsZWJ$1 + '(?:' + [rsNonAstral$1, rsRegional$1, rsSurrPair$1].join('|') + ')' + rsOptVar$1 + reOptMod$1 + ')*', + rsSeq$1 = rsOptVar$1 + reOptMod$1 + rsOptJoin$1, + rsSymbol$1 = '(?:' + [rsNonAstral$1 + rsCombo$1 + '?', rsCombo$1, rsRegional$1, rsSurrPair$1, rsAstral$1].join('|') + ')'; + + /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ + var reUnicode$1 = RegExp(rsFitz$1 + '(?=' + rsFitz$1 + ')|' + rsSymbol$1 + rsSeq$1, 'g'); + + /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ + var reHasUnicode$1 = RegExp('[' + rsZWJ$1 + rsAstralRange$1 + rsComboMarksRange$1 + rsComboSymbolsRange$1 + rsVarRange$1 + ']'); + + /** Detect free variable `global` from Node.js. */ + var freeGlobal$3 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf$3 = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root$3 = freeGlobal$3 || freeSelf$3 || Function('return this')(); + + /** + * A specialized version of `_.map` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function arrayMap(array, iteratee) { + var index = -1, + length = array ? array.length : 0, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; + } + + /** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function asciiToArray$1(string) { + return string.split(''); + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes$1(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * The base implementation of `_.values` and `_.valuesIn` which creates an + * array of `object` property values corresponding to the property names + * of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the array of property values. + */ + function baseValues(object, props) { + return arrayMap(props, function(key) { + return object[key]; + }); + } + + /** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function getValue(object, key) { + return object == null ? undefined : object[key]; + } + + /** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ + function hasUnicode$1(string) { + return reHasUnicode$1.test(string); + } + + /** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ + function isHostObject(value) { + // Many host objects are `Object` objects that can coerce to strings + // despite having improperly defined `toString` methods. + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; + } + + /** + * Converts `iterator` to an array. + * + * @private + * @param {Object} iterator The iterator to convert. + * @returns {Array} Returns the converted array. + */ + function iteratorToArray(iterator) { + var data, + result = []; + + while (!(data = iterator.next()).done) { + result.push(data.value); + } + return result; + } + + /** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ + function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg$1(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ + function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; + } + + /** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function stringToArray$1(string) { + return hasUnicode$1(string) + ? unicodeToArray$1(string) + : asciiToArray$1(string); + } + + /** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function unicodeToArray$1(string) { + return string.match(reUnicode$1) || []; + } + + /** Used for built-in method references. */ + var funcProto = Function.prototype, + objectProto$4 = Object.prototype; + + /** Used to detect overreaching core-js shims. */ + var coreJsData = root$3['__core-js_shared__']; + + /** Used to detect methods masquerading as native. */ + var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; + }()); + + /** Used to resolve the decompiled source of functions. */ + var funcToString = funcProto.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty$2 = objectProto$4.hasOwnProperty; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$4 = objectProto$4.toString; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty$2).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Built-in value references. */ + var Symbol$2 = root$3.Symbol, + iteratorSymbol = Symbol$2 ? Symbol$2.iterator : undefined, + propertyIsEnumerable$1 = objectProto$4.propertyIsEnumerable; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeKeys$1 = overArg$1(Object.keys, Object); + + /* Built-in method references that are verified to be native. */ + var DataView = getNative(root$3, 'DataView'), + Map$1 = getNative(root$3, 'Map'), + Promise$1 = getNative(root$3, 'Promise'), + Set$1 = getNative(root$3, 'Set'), + WeakMap$1 = getNative(root$3, 'WeakMap'); + + /** Used to detect maps, sets, and weakmaps. */ + var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map$1), + promiseCtorString = toSource(Promise$1), + setCtorString = toSource(Set$1), + weakMapCtorString = toSource(WeakMap$1); + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys$1(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray$1(value) || isArguments$1(value)) + ? baseTimes$1(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty$2.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex$1(key, length)))) { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `getTag`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + return objectToString$4.call(value); + } + + /** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ + function baseIsNative(value) { + if (!isObject$3(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction$1(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys$1(object) { + if (!isPrototype$1(object)) { + return nativeKeys$1(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty$2.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; + } + + /** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + var getTag = baseGetTag; + + // Fallback for data views, maps, sets, and weak maps in IE 11, + // for data views in Edge < 14, and promises in Node.js. + if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map$1 && getTag(new Map$1) != mapTag) || + (Promise$1 && getTag(Promise$1.resolve()) != promiseTag) || + (Set$1 && getTag(new Set$1) != setTag) || + (WeakMap$1 && getTag(new WeakMap$1) != weakMapTag)) { + getTag = function(value) { + var result = objectToString$4.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex$1(value, length) { + length = length == null ? MAX_SAFE_INTEGER$1 : length; + return !!length && + (typeof value == 'number' || reIsUint$1.test(value)) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ + function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); + } + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype$1(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto$4; + + return value === proto; + } + + /** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to process. + * @returns {string} Returns the source code. + */ + function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; + } + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments$1(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject$1(value) && hasOwnProperty$2.call(value, 'callee') && + (!propertyIsEnumerable$1.call(value, 'callee') || objectToString$4.call(value) == argsTag$1); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray$1 = Array.isArray; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike$1(value) { + return value != null && isLength$1(value.length) && !isFunction$1(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject$1(value) { + return isObjectLike$4(value) && isArrayLike$1(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction$1(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject$3(value) ? objectToString$4.call(value) : ''; + return tag == funcTag$1 || tag == genTag$1; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength$1(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER$1; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject$3(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$4(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a string, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ + function isString(value) { + return typeof value == 'string' || + (!isArray$1(value) && isObjectLike$4(value) && objectToString$4.call(value) == stringTag); + } + + /** + * Converts `value` to an array. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Array} Returns the converted array. + * @example + * + * _.toArray({ 'a': 1, 'b': 2 }); + * // => [1, 2] + * + * _.toArray('abc'); + * // => ['a', 'b', 'c'] + * + * _.toArray(1); + * // => [] + * + * _.toArray(null); + * // => [] + */ + function toArray(value) { + if (!value) { + return []; + } + if (isArrayLike$1(value)) { + return isString(value) ? stringToArray$1(value) : copyArray(value); + } + if (iteratorSymbol && value[iteratorSymbol]) { + return iteratorToArray(value[iteratorSymbol]()); + } + var tag = getTag(value), + func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); + + return func(value); + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys$1(object) { + return isArrayLike$1(object) ? arrayLikeKeys$1(object) : baseKeys$1(object); + } + + /** + * Creates an array of the own enumerable string keyed property values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.values(new Foo); + * // => [1, 2] (iteration order is not guaranteed) + * + * _.values('hi'); + * // => ['h', 'i'] + */ + function values(object) { + return object ? baseValues(object, keys$1(object)) : []; + } + + var lodash_toarray = toArray; + + /** + * SSR Window 3.0.0 + * Better handling for window object in SSR environment + * https://github.com/nolimits4web/ssr-window + * + * Copyright 2020, Vladimir Kharlampidi + * + * Licensed under MIT + * + * Released on: November 9, 2020 + */ + + var ssrWindow_umd = createCommonjsModule$1(function (module, exports) { + (function (global, factory) { + factory(exports) ; + }(commonjsGlobal, (function (exports) { + /* eslint-disable no-param-reassign */ + function isObject(obj) { + return (obj !== null && + typeof obj === 'object' && + 'constructor' in obj && + obj.constructor === Object); + } + function extend(target, src) { + if (target === void 0) { target = {}; } + if (src === void 0) { src = {}; } + Object.keys(src).forEach(function (key) { + if (typeof target[key] === 'undefined') + target[key] = src[key]; + else if (isObject(src[key]) && + isObject(target[key]) && + Object.keys(src[key]).length > 0) { + extend(target[key], src[key]); + } + }); + } + + var ssrDocument = { + body: {}, + addEventListener: function () { }, + removeEventListener: function () { }, + activeElement: { + blur: function () { }, + nodeName: '', + }, + querySelector: function () { + return null; + }, + querySelectorAll: function () { + return []; + }, + getElementById: function () { + return null; + }, + createEvent: function () { + return { + initEvent: function () { }, + }; + }, + createElement: function () { + return { + children: [], + childNodes: [], + style: {}, + setAttribute: function () { }, + getElementsByTagName: function () { + return []; + }, + }; + }, + createElementNS: function () { + return {}; + }, + importNode: function () { + return null; + }, + location: { + hash: '', + host: '', + hostname: '', + href: '', + origin: '', + pathname: '', + protocol: '', + search: '', + }, + }; + function getDocument() { + var doc = typeof document !== 'undefined' ? document : {}; + extend(doc, ssrDocument); + return doc; + } + + var ssrWindow = { + document: ssrDocument, + navigator: { + userAgent: '', + }, + location: { + hash: '', + host: '', + hostname: '', + href: '', + origin: '', + pathname: '', + protocol: '', + search: '', + }, + history: { + replaceState: function () { }, + pushState: function () { }, + go: function () { }, + back: function () { }, + }, + CustomEvent: function CustomEvent() { + return this; + }, + addEventListener: function () { }, + removeEventListener: function () { }, + getComputedStyle: function () { + return { + getPropertyValue: function () { + return ''; + }, + }; + }, + Image: function () { }, + Date: function () { }, + screen: {}, + setTimeout: function () { }, + clearTimeout: function () { }, + matchMedia: function () { + return {}; + }, + requestAnimationFrame: function (callback) { + if (typeof setTimeout === 'undefined') { + callback(); + return null; + } + return setTimeout(callback, 0); + }, + cancelAnimationFrame: function (id) { + if (typeof setTimeout === 'undefined') { + return; + } + clearTimeout(id); + }, + }; + function getWindow() { + var win = typeof window !== 'undefined' ? window : {}; + extend(win, ssrWindow); + return win; + } + + exports.extend = extend; + exports.getDocument = getDocument; + exports.getWindow = getWindow; + exports.ssrDocument = ssrDocument; + exports.ssrWindow = ssrWindow; + + Object.defineProperty(exports, '__esModule', { value: true }); + + }))); + + }); + + /** + * Dom7 3.0.0 + * Minimalistic JavaScript library for DOM manipulation, with a jQuery-compatible API + * https://framework7.io/docs/dom7.html + * + * Copyright 2020, Vladimir Kharlampidi + * + * Licensed under MIT + * + * Released on: November 9, 2020 + */ + + + + + + function _inheritsLoose(subClass, superClass) { + subClass.prototype = Object.create(superClass.prototype); + subClass.prototype.constructor = subClass; + subClass.__proto__ = superClass; + } + + function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); + } + + function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); + } + + function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); + return true; + } catch (e) { + return false; + } + } + + function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct; + } else { + _construct = function _construct(Parent, args, Class) { + var a = [null]; + a.push.apply(a, args); + var Constructor = Function.bind.apply(Parent, a); + var instance = new Constructor(); + if (Class) _setPrototypeOf(instance, Class.prototype); + return instance; + }; + } + + return _construct.apply(null, arguments); + } + + function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; + } + + function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? new Map() : undefined; + + _wrapNativeSuper = function _wrapNativeSuper(Class) { + if (Class === null || !_isNativeFunction(Class)) return Class; + + if (typeof Class !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + + if (typeof _cache !== "undefined") { + if (_cache.has(Class)) return _cache.get(Class); + + _cache.set(Class, Wrapper); + } + + function Wrapper() { + return _construct(Class, arguments, _getPrototypeOf(this).constructor); + } + + Wrapper.prototype = Object.create(Class.prototype, { + constructor: { + value: Wrapper, + enumerable: false, + writable: true, + configurable: true + } + }); + return _setPrototypeOf(Wrapper, Class); + }; + + return _wrapNativeSuper(Class); + } + + function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; + } + + /* eslint-disable no-proto */ + function makeReactive(obj) { + var proto = obj.__proto__; + Object.defineProperty(obj, '__proto__', { + get: function get() { + return proto; + }, + set: function set(value) { + proto.__proto__ = value; + } + }); + } + + var Dom7 = /*#__PURE__*/function (_Array) { + _inheritsLoose(Dom7, _Array); + + function Dom7(items) { + var _this; + + _this = _Array.call.apply(_Array, [this].concat(items)) || this; + makeReactive(_assertThisInitialized(_this)); + return _this; + } + + return Dom7; + }( /*#__PURE__*/_wrapNativeSuper(Array)); + + function arrayFlat(arr) { + if (arr === void 0) { + arr = []; + } + + var res = []; + arr.forEach(function (el) { + if (Array.isArray(el)) { + res.push.apply(res, arrayFlat(el)); + } else { + res.push(el); + } + }); + return res; + } + function arrayFilter(arr, callback) { + return Array.prototype.filter.call(arr, callback); + } + function arrayUnique(arr) { + var uniqueArray = []; + + for (var i = 0; i < arr.length; i += 1) { + if (uniqueArray.indexOf(arr[i]) === -1) uniqueArray.push(arr[i]); + } + + return uniqueArray; + } + function toCamelCase(string) { + return string.toLowerCase().replace(/-(.)/g, function (match, group) { + return group.toUpperCase(); + }); + } + + function qsa(selector, context) { + if (typeof selector !== 'string') { + return [selector]; + } + + var a = []; + var res = context.querySelectorAll(selector); + + for (var i = 0; i < res.length; i += 1) { + a.push(res[i]); + } + + return a; + } + + function $(selector, context) { + var window = ssrWindow_umd.getWindow(); + var document = ssrWindow_umd.getDocument(); + var arr = []; + + if (!context && selector instanceof Dom7) { + return selector; + } + + if (!selector) { + return new Dom7(arr); + } + + if (typeof selector === 'string') { + var html = selector.trim(); + + if (html.indexOf('<') >= 0 && html.indexOf('>') >= 0) { + var toCreate = 'div'; + if (html.indexOf(' 0; + }).length > 0; + } + + function attr(attrs, value) { + if (arguments.length === 1 && typeof attrs === 'string') { + // Get attr + if (this[0]) return this[0].getAttribute(attrs); + return undefined; + } // Set attrs + + + for (var i = 0; i < this.length; i += 1) { + if (arguments.length === 2) { + // String + this[i].setAttribute(attrs, value); + } else { + // Object + for (var attrName in attrs) { + this[i][attrName] = attrs[attrName]; + this[i].setAttribute(attrName, attrs[attrName]); + } + } + } + + return this; + } + + function removeAttr(attr) { + for (var i = 0; i < this.length; i += 1) { + this[i].removeAttribute(attr); + } + + return this; + } + + function prop(props, value) { + if (arguments.length === 1 && typeof props === 'string') { + // Get prop + if (this[0]) return this[0][props]; + } else { + // Set props + for (var i = 0; i < this.length; i += 1) { + if (arguments.length === 2) { + // String + this[i][props] = value; + } else { + // Object + for (var propName in props) { + this[i][propName] = props[propName]; + } + } + } + + return this; + } + + return this; + } + + function data(key, value) { + var el; + + if (typeof value === 'undefined') { + el = this[0]; + if (!el) return undefined; // Get value + + if (el.dom7ElementDataStorage && key in el.dom7ElementDataStorage) { + return el.dom7ElementDataStorage[key]; + } + + var dataKey = el.getAttribute("data-" + key); + + if (dataKey) { + return dataKey; + } + + return undefined; + } // Set value + + + for (var i = 0; i < this.length; i += 1) { + el = this[i]; + if (!el.dom7ElementDataStorage) el.dom7ElementDataStorage = {}; + el.dom7ElementDataStorage[key] = value; + } + + return this; + } + + function removeData(key) { + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (el.dom7ElementDataStorage && el.dom7ElementDataStorage[key]) { + el.dom7ElementDataStorage[key] = null; + delete el.dom7ElementDataStorage[key]; + } + } + } + + function dataset() { + var el = this[0]; + if (!el) return undefined; + var dataset = {}; // eslint-disable-line + + if (el.dataset) { + for (var dataKey in el.dataset) { + dataset[dataKey] = el.dataset[dataKey]; + } + } else { + for (var i = 0; i < el.attributes.length; i += 1) { + var _attr = el.attributes[i]; + + if (_attr.name.indexOf('data-') >= 0) { + dataset[toCamelCase(_attr.name.split('data-')[1])] = _attr.value; + } + } + } + + for (var key in dataset) { + if (dataset[key] === 'false') dataset[key] = false;else if (dataset[key] === 'true') dataset[key] = true;else if (parseFloat(dataset[key]) === dataset[key] * 1) dataset[key] *= 1; + } + + return dataset; + } + + function val(value) { + if (typeof value === 'undefined') { + // get value + var el = this[0]; + if (!el) return undefined; + + if (el.multiple && el.nodeName.toLowerCase() === 'select') { + var values = []; + + for (var i = 0; i < el.selectedOptions.length; i += 1) { + values.push(el.selectedOptions[i].value); + } + + return values; + } + + return el.value; + } // set value + + + for (var _i = 0; _i < this.length; _i += 1) { + var _el = this[_i]; + + if (Array.isArray(value) && _el.multiple && _el.nodeName.toLowerCase() === 'select') { + for (var j = 0; j < _el.options.length; j += 1) { + _el.options[j].selected = value.indexOf(_el.options[j].value) >= 0; + } + } else { + _el.value = value; + } + } + + return this; + } + + function value(value) { + return this.val(value); + } + + function transform(transform) { + for (var i = 0; i < this.length; i += 1) { + this[i].style.transform = transform; + } + + return this; + } + + function transition(duration) { + for (var i = 0; i < this.length; i += 1) { + this[i].style.transitionDuration = typeof duration !== 'string' ? duration + "ms" : duration; + } + + return this; + } + + function on() { + for (var _len5 = arguments.length, args = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { + args[_key5] = arguments[_key5]; + } + + var eventType = args[0], + targetSelector = args[1], + listener = args[2], + capture = args[3]; + + if (typeof args[1] === 'function') { + eventType = args[0]; + listener = args[1]; + capture = args[2]; + targetSelector = undefined; + } + + if (!capture) capture = false; + + function handleLiveEvent(e) { + var target = e.target; + if (!target) return; + var eventData = e.target.dom7EventData || []; + + if (eventData.indexOf(e) < 0) { + eventData.unshift(e); + } + + if ($(target).is(targetSelector)) listener.apply(target, eventData);else { + var _parents = $(target).parents(); // eslint-disable-line + + + for (var k = 0; k < _parents.length; k += 1) { + if ($(_parents[k]).is(targetSelector)) listener.apply(_parents[k], eventData); + } + } + } + + function handleEvent(e) { + var eventData = e && e.target ? e.target.dom7EventData || [] : []; + + if (eventData.indexOf(e) < 0) { + eventData.unshift(e); + } + + listener.apply(this, eventData); + } + + var events = eventType.split(' '); + var j; + + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (!targetSelector) { + for (j = 0; j < events.length; j += 1) { + var event = events[j]; + if (!el.dom7Listeners) el.dom7Listeners = {}; + if (!el.dom7Listeners[event]) el.dom7Listeners[event] = []; + el.dom7Listeners[event].push({ + listener: listener, + proxyListener: handleEvent + }); + el.addEventListener(event, handleEvent, capture); + } + } else { + // Live events + for (j = 0; j < events.length; j += 1) { + var _event = events[j]; + if (!el.dom7LiveListeners) el.dom7LiveListeners = {}; + if (!el.dom7LiveListeners[_event]) el.dom7LiveListeners[_event] = []; + + el.dom7LiveListeners[_event].push({ + listener: listener, + proxyListener: handleLiveEvent + }); + + el.addEventListener(_event, handleLiveEvent, capture); + } + } + } + + return this; + } + + function off() { + for (var _len6 = arguments.length, args = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) { + args[_key6] = arguments[_key6]; + } + + var eventType = args[0], + targetSelector = args[1], + listener = args[2], + capture = args[3]; + + if (typeof args[1] === 'function') { + eventType = args[0]; + listener = args[1]; + capture = args[2]; + targetSelector = undefined; + } + + if (!capture) capture = false; + var events = eventType.split(' '); + + for (var i = 0; i < events.length; i += 1) { + var event = events[i]; + + for (var j = 0; j < this.length; j += 1) { + var el = this[j]; + var handlers = void 0; + + if (!targetSelector && el.dom7Listeners) { + handlers = el.dom7Listeners[event]; + } else if (targetSelector && el.dom7LiveListeners) { + handlers = el.dom7LiveListeners[event]; + } + + if (handlers && handlers.length) { + for (var k = handlers.length - 1; k >= 0; k -= 1) { + var handler = handlers[k]; + + if (listener && handler.listener === listener) { + el.removeEventListener(event, handler.proxyListener, capture); + handlers.splice(k, 1); + } else if (listener && handler.listener && handler.listener.dom7proxy && handler.listener.dom7proxy === listener) { + el.removeEventListener(event, handler.proxyListener, capture); + handlers.splice(k, 1); + } else if (!listener) { + el.removeEventListener(event, handler.proxyListener, capture); + handlers.splice(k, 1); + } + } + } + } + } + + return this; + } + + function once() { + var dom = this; + + for (var _len7 = arguments.length, args = new Array(_len7), _key7 = 0; _key7 < _len7; _key7++) { + args[_key7] = arguments[_key7]; + } + + var eventName = args[0], + targetSelector = args[1], + listener = args[2], + capture = args[3]; + + if (typeof args[1] === 'function') { + eventName = args[0]; + listener = args[1]; + capture = args[2]; + targetSelector = undefined; + } + + function onceHandler() { + for (var _len8 = arguments.length, eventArgs = new Array(_len8), _key8 = 0; _key8 < _len8; _key8++) { + eventArgs[_key8] = arguments[_key8]; + } + + listener.apply(this, eventArgs); + dom.off(eventName, targetSelector, onceHandler, capture); + + if (onceHandler.dom7proxy) { + delete onceHandler.dom7proxy; + } + } + + onceHandler.dom7proxy = listener; + return dom.on(eventName, targetSelector, onceHandler, capture); + } + + function trigger() { + var window = ssrWindow_umd.getWindow(); + + for (var _len9 = arguments.length, args = new Array(_len9), _key9 = 0; _key9 < _len9; _key9++) { + args[_key9] = arguments[_key9]; + } + + var events = args[0].split(' '); + var eventData = args[1]; + + for (var i = 0; i < events.length; i += 1) { + var event = events[i]; + + for (var j = 0; j < this.length; j += 1) { + var el = this[j]; + + if (window.CustomEvent) { + var evt = new window.CustomEvent(event, { + detail: eventData, + bubbles: true, + cancelable: true + }); + el.dom7EventData = args.filter(function (data, dataIndex) { + return dataIndex > 0; + }); + el.dispatchEvent(evt); + el.dom7EventData = []; + delete el.dom7EventData; + } + } + } + + return this; + } + + function transitionEnd(callback) { + var dom = this; + + function fireCallBack(e) { + if (e.target !== this) return; + callback.call(this, e); + dom.off('transitionend', fireCallBack); + } + + if (callback) { + dom.on('transitionend', fireCallBack); + } + + return this; + } + + function animationEnd(callback) { + var dom = this; + + function fireCallBack(e) { + if (e.target !== this) return; + callback.call(this, e); + dom.off('animationend', fireCallBack); + } + + if (callback) { + dom.on('animationend', fireCallBack); + } + + return this; + } + + function width() { + var window = ssrWindow_umd.getWindow(); + + if (this[0] === window) { + return window.innerWidth; + } + + if (this.length > 0) { + return parseFloat(this.css('width')); + } + + return null; + } + + function outerWidth(includeMargins) { + if (this.length > 0) { + if (includeMargins) { + var _styles = this.styles(); + + return this[0].offsetWidth + parseFloat(_styles.getPropertyValue('margin-right')) + parseFloat(_styles.getPropertyValue('margin-left')); + } + + return this[0].offsetWidth; + } + + return null; + } + + function height() { + var window = ssrWindow_umd.getWindow(); + + if (this[0] === window) { + return window.innerHeight; + } + + if (this.length > 0) { + return parseFloat(this.css('height')); + } + + return null; + } + + function outerHeight(includeMargins) { + if (this.length > 0) { + if (includeMargins) { + var _styles2 = this.styles(); + + return this[0].offsetHeight + parseFloat(_styles2.getPropertyValue('margin-top')) + parseFloat(_styles2.getPropertyValue('margin-bottom')); + } + + return this[0].offsetHeight; + } + + return null; + } + + function offset() { + if (this.length > 0) { + var window = ssrWindow_umd.getWindow(); + var document = ssrWindow_umd.getDocument(); + var el = this[0]; + var box = el.getBoundingClientRect(); + var body = document.body; + var clientTop = el.clientTop || body.clientTop || 0; + var clientLeft = el.clientLeft || body.clientLeft || 0; + var scrollTop = el === window ? window.scrollY : el.scrollTop; + var scrollLeft = el === window ? window.scrollX : el.scrollLeft; + return { + top: box.top + scrollTop - clientTop, + left: box.left + scrollLeft - clientLeft + }; + } + + return null; + } + + function hide() { + for (var i = 0; i < this.length; i += 1) { + this[i].style.display = 'none'; + } + + return this; + } + + function show() { + var window = ssrWindow_umd.getWindow(); + + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (el.style.display === 'none') { + el.style.display = ''; + } + + if (window.getComputedStyle(el, null).getPropertyValue('display') === 'none') { + // Still not visible + el.style.display = 'block'; + } + } + + return this; + } + + function styles() { + var window = ssrWindow_umd.getWindow(); + if (this[0]) return window.getComputedStyle(this[0], null); + return {}; + } + + function css(props, value) { + var window = ssrWindow_umd.getWindow(); + var i; + + if (arguments.length === 1) { + if (typeof props === 'string') { + // .css('width') + if (this[0]) return window.getComputedStyle(this[0], null).getPropertyValue(props); + } else { + // .css({ width: '100px' }) + for (i = 0; i < this.length; i += 1) { + for (var _prop in props) { + this[i].style[_prop] = props[_prop]; + } + } + + return this; + } + } + + if (arguments.length === 2 && typeof props === 'string') { + // .css('width', '100px') + for (i = 0; i < this.length; i += 1) { + this[i].style[props] = value; + } + + return this; + } + + return this; + } + + function each(callback) { + if (!callback) return this; + this.forEach(function (el, index) { + callback.apply(el, [el, index]); + }); + return this; + } + + function filter(callback) { + var result = arrayFilter(this, callback); + return $(result); + } + + function html(html) { + if (typeof html === 'undefined') { + return this[0] ? this[0].innerHTML : null; + } + + for (var i = 0; i < this.length; i += 1) { + this[i].innerHTML = html; + } + + return this; + } + + function text(text) { + if (typeof text === 'undefined') { + return this[0] ? this[0].textContent.trim() : null; + } + + for (var i = 0; i < this.length; i += 1) { + this[i].textContent = text; + } + + return this; + } + + function is(selector) { + var window = ssrWindow_umd.getWindow(); + var document = ssrWindow_umd.getDocument(); + var el = this[0]; + var compareWith; + var i; + if (!el || typeof selector === 'undefined') return false; + + if (typeof selector === 'string') { + if (el.matches) return el.matches(selector); + if (el.webkitMatchesSelector) return el.webkitMatchesSelector(selector); + if (el.msMatchesSelector) return el.msMatchesSelector(selector); + compareWith = $(selector); + + for (i = 0; i < compareWith.length; i += 1) { + if (compareWith[i] === el) return true; + } + + return false; + } + + if (selector === document) { + return el === document; + } + + if (selector === window) { + return el === window; + } + + if (selector.nodeType || selector instanceof Dom7) { + compareWith = selector.nodeType ? [selector] : selector; + + for (i = 0; i < compareWith.length; i += 1) { + if (compareWith[i] === el) return true; + } + + return false; + } + + return false; + } + + function index$1() { + var child = this[0]; + var i; + + if (child) { + i = 0; // eslint-disable-next-line + + while ((child = child.previousSibling) !== null) { + if (child.nodeType === 1) i += 1; + } + + return i; + } + + return undefined; + } + + function eq(index) { + if (typeof index === 'undefined') return this; + var length = this.length; + + if (index > length - 1) { + return $([]); + } + + if (index < 0) { + var returnIndex = length + index; + if (returnIndex < 0) return $([]); + return $([this[returnIndex]]); + } + + return $([this[index]]); + } + + function append() { + var newChild; + var document = ssrWindow_umd.getDocument(); + + for (var k = 0; k < arguments.length; k += 1) { + newChild = k < 0 || arguments.length <= k ? undefined : arguments[k]; + + for (var i = 0; i < this.length; i += 1) { + if (typeof newChild === 'string') { + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = newChild; + + while (tempDiv.firstChild) { + this[i].appendChild(tempDiv.firstChild); + } + } else if (newChild instanceof Dom7) { + for (var j = 0; j < newChild.length; j += 1) { + this[i].appendChild(newChild[j]); + } + } else { + this[i].appendChild(newChild); + } + } + } + + return this; + } + + function appendTo(parent) { + $(parent).append(this); + return this; + } + + function prepend(newChild) { + var document = ssrWindow_umd.getDocument(); + var i; + var j; + + for (i = 0; i < this.length; i += 1) { + if (typeof newChild === 'string') { + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = newChild; + + for (j = tempDiv.childNodes.length - 1; j >= 0; j -= 1) { + this[i].insertBefore(tempDiv.childNodes[j], this[i].childNodes[0]); + } + } else if (newChild instanceof Dom7) { + for (j = 0; j < newChild.length; j += 1) { + this[i].insertBefore(newChild[j], this[i].childNodes[0]); + } + } else { + this[i].insertBefore(newChild, this[i].childNodes[0]); + } + } + + return this; + } + + function prependTo(parent) { + $(parent).prepend(this); + return this; + } + + function insertBefore(selector) { + var before = $(selector); + + for (var i = 0; i < this.length; i += 1) { + if (before.length === 1) { + before[0].parentNode.insertBefore(this[i], before[0]); + } else if (before.length > 1) { + for (var j = 0; j < before.length; j += 1) { + before[j].parentNode.insertBefore(this[i].cloneNode(true), before[j]); + } + } + } + } + + function insertAfter(selector) { + var after = $(selector); + + for (var i = 0; i < this.length; i += 1) { + if (after.length === 1) { + after[0].parentNode.insertBefore(this[i], after[0].nextSibling); + } else if (after.length > 1) { + for (var j = 0; j < after.length; j += 1) { + after[j].parentNode.insertBefore(this[i].cloneNode(true), after[j].nextSibling); + } + } + } + } + + function next(selector) { + if (this.length > 0) { + if (selector) { + if (this[0].nextElementSibling && $(this[0].nextElementSibling).is(selector)) { + return $([this[0].nextElementSibling]); + } + + return $([]); + } + + if (this[0].nextElementSibling) return $([this[0].nextElementSibling]); + return $([]); + } + + return $([]); + } + + function nextAll(selector) { + var nextEls = []; + var el = this[0]; + if (!el) return $([]); + + while (el.nextElementSibling) { + var _next = el.nextElementSibling; // eslint-disable-line + + if (selector) { + if ($(_next).is(selector)) nextEls.push(_next); + } else nextEls.push(_next); + + el = _next; + } + + return $(nextEls); + } + + function prev(selector) { + if (this.length > 0) { + var el = this[0]; + + if (selector) { + if (el.previousElementSibling && $(el.previousElementSibling).is(selector)) { + return $([el.previousElementSibling]); + } + + return $([]); + } + + if (el.previousElementSibling) return $([el.previousElementSibling]); + return $([]); + } + + return $([]); + } + + function prevAll(selector) { + var prevEls = []; + var el = this[0]; + if (!el) return $([]); + + while (el.previousElementSibling) { + var _prev = el.previousElementSibling; // eslint-disable-line + + if (selector) { + if ($(_prev).is(selector)) prevEls.push(_prev); + } else prevEls.push(_prev); + + el = _prev; + } + + return $(prevEls); + } + + function siblings(selector) { + return this.nextAll(selector).add(this.prevAll(selector)); + } + + function parent(selector) { + var parents = []; // eslint-disable-line + + for (var i = 0; i < this.length; i += 1) { + if (this[i].parentNode !== null) { + if (selector) { + if ($(this[i].parentNode).is(selector)) parents.push(this[i].parentNode); + } else { + parents.push(this[i].parentNode); + } + } + } + + return $(parents); + } + + function parents(selector) { + var parents = []; // eslint-disable-line + + for (var i = 0; i < this.length; i += 1) { + var _parent = this[i].parentNode; // eslint-disable-line + + while (_parent) { + if (selector) { + if ($(_parent).is(selector)) parents.push(_parent); + } else { + parents.push(_parent); + } + + _parent = _parent.parentNode; + } + } + + return $(parents); + } + + function closest(selector) { + var closest = this; // eslint-disable-line + + if (typeof selector === 'undefined') { + return $([]); + } + + if (!closest.is(selector)) { + closest = closest.parents(selector).eq(0); + } + + return closest; + } + + function find(selector) { + var foundElements = []; + + for (var i = 0; i < this.length; i += 1) { + var found = this[i].querySelectorAll(selector); + + for (var j = 0; j < found.length; j += 1) { + foundElements.push(found[j]); + } + } + + return $(foundElements); + } + + function children(selector) { + var children = []; // eslint-disable-line + + for (var i = 0; i < this.length; i += 1) { + var childNodes = this[i].children; + + for (var j = 0; j < childNodes.length; j += 1) { + if (!selector || $(childNodes[j]).is(selector)) { + children.push(childNodes[j]); + } + } + } + + return $(children); + } + + function remove() { + for (var i = 0; i < this.length; i += 1) { + if (this[i].parentNode) this[i].parentNode.removeChild(this[i]); + } + + return this; + } + + function detach() { + return this.remove(); + } + + function add() { + var dom = this; + var i; + var j; + + for (var _len10 = arguments.length, els = new Array(_len10), _key10 = 0; _key10 < _len10; _key10++) { + els[_key10] = arguments[_key10]; + } + + for (i = 0; i < els.length; i += 1) { + var toAdd = $(els[i]); + + for (j = 0; j < toAdd.length; j += 1) { + dom.push(toAdd[j]); + } + } + + return dom; + } + + function empty() { + for (var i = 0; i < this.length; i += 1) { + var el = this[i]; + + if (el.nodeType === 1) { + for (var j = 0; j < el.childNodes.length; j += 1) { + if (el.childNodes[j].parentNode) { + el.childNodes[j].parentNode.removeChild(el.childNodes[j]); + } + } + + el.textContent = ''; + } + } + + return this; + } + + function scrollTo() { + var window = ssrWindow_umd.getWindow(); + + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var left = args[0], + top = args[1], + duration = args[2], + easing = args[3], + callback = args[4]; + + if (args.length === 4 && typeof easing === 'function') { + callback = easing; + left = args[0]; + top = args[1]; + duration = args[2]; + callback = args[3]; + easing = args[4]; + } + + if (typeof easing === 'undefined') easing = 'swing'; + return this.each(function animate() { + var el = this; + var currentTop; + var currentLeft; + var maxTop; + var maxLeft; + var newTop; + var newLeft; + var scrollTop; // eslint-disable-line + + var scrollLeft; // eslint-disable-line + + var animateTop = top > 0 || top === 0; + var animateLeft = left > 0 || left === 0; + + if (typeof easing === 'undefined') { + easing = 'swing'; + } + + if (animateTop) { + currentTop = el.scrollTop; + + if (!duration) { + el.scrollTop = top; + } + } + + if (animateLeft) { + currentLeft = el.scrollLeft; + + if (!duration) { + el.scrollLeft = left; + } + } + + if (!duration) return; + + if (animateTop) { + maxTop = el.scrollHeight - el.offsetHeight; + newTop = Math.max(Math.min(top, maxTop), 0); + } + + if (animateLeft) { + maxLeft = el.scrollWidth - el.offsetWidth; + newLeft = Math.max(Math.min(left, maxLeft), 0); + } + + var startTime = null; + if (animateTop && newTop === currentTop) animateTop = false; + if (animateLeft && newLeft === currentLeft) animateLeft = false; + + function render(time) { + if (time === void 0) { + time = new Date().getTime(); + } + + if (startTime === null) { + startTime = time; + } + + var progress = Math.max(Math.min((time - startTime) / duration, 1), 0); + var easeProgress = easing === 'linear' ? progress : 0.5 - Math.cos(progress * Math.PI) / 2; + var done; + if (animateTop) scrollTop = currentTop + easeProgress * (newTop - currentTop); + if (animateLeft) scrollLeft = currentLeft + easeProgress * (newLeft - currentLeft); + + if (animateTop && newTop > currentTop && scrollTop >= newTop) { + el.scrollTop = newTop; + done = true; + } + + if (animateTop && newTop < currentTop && scrollTop <= newTop) { + el.scrollTop = newTop; + done = true; + } + + if (animateLeft && newLeft > currentLeft && scrollLeft >= newLeft) { + el.scrollLeft = newLeft; + done = true; + } + + if (animateLeft && newLeft < currentLeft && scrollLeft <= newLeft) { + el.scrollLeft = newLeft; + done = true; + } + + if (done) { + if (callback) callback(); + return; + } + + if (animateTop) el.scrollTop = scrollTop; + if (animateLeft) el.scrollLeft = scrollLeft; + window.requestAnimationFrame(render); + } + + window.requestAnimationFrame(render); + }); + } // scrollTop(top, duration, easing, callback) { + + + function scrollTop() { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + var top = args[0], + duration = args[1], + easing = args[2], + callback = args[3]; + + if (args.length === 3 && typeof easing === 'function') { + top = args[0]; + duration = args[1]; + callback = args[2]; + easing = args[3]; + } + + var dom = this; + + if (typeof top === 'undefined') { + if (dom.length > 0) return dom[0].scrollTop; + return null; + } + + return dom.scrollTo(undefined, top, duration, easing, callback); + } + + function scrollLeft() { + for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + var left = args[0], + duration = args[1], + easing = args[2], + callback = args[3]; + + if (args.length === 3 && typeof easing === 'function') { + left = args[0]; + duration = args[1]; + callback = args[2]; + easing = args[3]; + } + + var dom = this; + + if (typeof left === 'undefined') { + if (dom.length > 0) return dom[0].scrollLeft; + return null; + } + + return dom.scrollTo(left, undefined, duration, easing, callback); + } + + function animate(initialProps, initialParams) { + var window = ssrWindow_umd.getWindow(); + var els = this; + var a = { + props: Object.assign({}, initialProps), + params: Object.assign({ + duration: 300, + easing: 'swing' // or 'linear' + + /* Callbacks + begin(elements) + complete(elements) + progress(elements, complete, remaining, start, tweenValue) + */ + + }, initialParams), + elements: els, + animating: false, + que: [], + easingProgress: function easingProgress(easing, progress) { + if (easing === 'swing') { + return 0.5 - Math.cos(progress * Math.PI) / 2; + } + + if (typeof easing === 'function') { + return easing(progress); + } + + return progress; + }, + stop: function stop() { + if (a.frameId) { + window.cancelAnimationFrame(a.frameId); + } + + a.animating = false; + a.elements.each(function (el) { + var element = el; + delete element.dom7AnimateInstance; + }); + a.que = []; + }, + done: function done(complete) { + a.animating = false; + a.elements.each(function (el) { + var element = el; + delete element.dom7AnimateInstance; + }); + if (complete) complete(els); + + if (a.que.length > 0) { + var que = a.que.shift(); + a.animate(que[0], que[1]); + } + }, + animate: function animate(props, params) { + if (a.animating) { + a.que.push([props, params]); + return a; + } + + var elements = []; // Define & Cache Initials & Units + + a.elements.each(function (el, index) { + var initialFullValue; + var initialValue; + var unit; + var finalValue; + var finalFullValue; + if (!el.dom7AnimateInstance) a.elements[index].dom7AnimateInstance = a; + elements[index] = { + container: el + }; + Object.keys(props).forEach(function (prop) { + initialFullValue = window.getComputedStyle(el, null).getPropertyValue(prop).replace(',', '.'); + initialValue = parseFloat(initialFullValue); + unit = initialFullValue.replace(initialValue, ''); + finalValue = parseFloat(props[prop]); + finalFullValue = props[prop] + unit; + elements[index][prop] = { + initialFullValue: initialFullValue, + initialValue: initialValue, + unit: unit, + finalValue: finalValue, + finalFullValue: finalFullValue, + currentValue: initialValue + }; + }); + }); + var startTime = null; + var time; + var elementsDone = 0; + var propsDone = 0; + var done; + var began = false; + a.animating = true; + + function render() { + time = new Date().getTime(); + var progress; + var easeProgress; // let el; + + if (!began) { + began = true; + if (params.begin) params.begin(els); + } + + if (startTime === null) { + startTime = time; + } + + if (params.progress) { + // eslint-disable-next-line + params.progress(els, Math.max(Math.min((time - startTime) / params.duration, 1), 0), startTime + params.duration - time < 0 ? 0 : startTime + params.duration - time, startTime); + } + + elements.forEach(function (element) { + var el = element; + if (done || el.done) return; + Object.keys(props).forEach(function (prop) { + if (done || el.done) return; + progress = Math.max(Math.min((time - startTime) / params.duration, 1), 0); + easeProgress = a.easingProgress(params.easing, progress); + var _el$prop = el[prop], + initialValue = _el$prop.initialValue, + finalValue = _el$prop.finalValue, + unit = _el$prop.unit; + el[prop].currentValue = initialValue + easeProgress * (finalValue - initialValue); + var currentValue = el[prop].currentValue; + + if (finalValue > initialValue && currentValue >= finalValue || finalValue < initialValue && currentValue <= finalValue) { + el.container.style[prop] = finalValue + unit; + propsDone += 1; + + if (propsDone === Object.keys(props).length) { + el.done = true; + elementsDone += 1; + } + + if (elementsDone === elements.length) { + done = true; + } + } + + if (done) { + a.done(params.complete); + return; + } + + el.container.style[prop] = currentValue + unit; + }); + }); + if (done) return; // Then call + + a.frameId = window.requestAnimationFrame(render); + } + + a.frameId = window.requestAnimationFrame(render); + return a; + } + }; + + if (a.elements.length === 0) { + return els; + } + + var animateInstance; + + for (var i = 0; i < a.elements.length; i += 1) { + if (a.elements[i].dom7AnimateInstance) { + animateInstance = a.elements[i].dom7AnimateInstance; + } else a.elements[i].dom7AnimateInstance = a; + } + + if (!animateInstance) { + animateInstance = a; + } + + if (initialProps === 'stop') { + animateInstance.stop(); + } else { + animateInstance.animate(a.props, a.params); + } + + return els; + } + + function stop() { + var els = this; + + for (var i = 0; i < els.length; i += 1) { + if (els[i].dom7AnimateInstance) { + els[i].dom7AnimateInstance.stop(); + } + } + } + + var noTrigger = 'resize scroll'.split(' '); + + function shortcut(name) { + function eventHandler() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + if (typeof args[0] === 'undefined') { + for (var i = 0; i < this.length; i += 1) { + if (noTrigger.indexOf(name) < 0) { + if (name in this[i]) this[i][name]();else { + $(this[i]).trigger(name); + } + } + } + + return this; + } + + return this.on.apply(this, [name].concat(args)); + } + + return eventHandler; + } + + var click = shortcut('click'); + var blur = shortcut('blur'); + var focus = shortcut('focus'); + var focusin = shortcut('focusin'); + var focusout = shortcut('focusout'); + var keyup = shortcut('keyup'); + var keydown = shortcut('keydown'); + var keypress = shortcut('keypress'); + var submit = shortcut('submit'); + var change = shortcut('change'); + var mousedown = shortcut('mousedown'); + var mousemove = shortcut('mousemove'); + var mouseup = shortcut('mouseup'); + var mouseenter = shortcut('mouseenter'); + var mouseleave = shortcut('mouseleave'); + var mouseout = shortcut('mouseout'); + var mouseover = shortcut('mouseover'); + var touchstart = shortcut('touchstart'); + var touchend = shortcut('touchend'); + var touchmove = shortcut('touchmove'); + var resize = shortcut('resize'); + var scroll = shortcut('scroll'); + + var $_1 = $; + var add_1 = add; + var addClass_1 = addClass; + var animate_1 = animate; + var animationEnd_1 = animationEnd; + var append_1 = append; + var appendTo_1 = appendTo; + var attr_1 = attr; + var blur_1 = blur; + var change_1 = change; + var children_1 = children; + var click_1 = click; + var closest_1 = closest; + var css_1 = css; + var data_1 = data; + var dataset_1 = dataset; + var _default$1 = $; + var detach_1 = detach; + var each_1 = each; + var empty_1 = empty; + var eq_1 = eq; + var filter_1 = filter; + var find_1 = find; + var focus_1 = focus; + var focusin_1 = focusin; + var focusout_1 = focusout; + var hasClass_1 = hasClass; + var height_1 = height; + var hide_1 = hide; + var html_1 = html; + var index_1 = index$1; + var insertAfter_1 = insertAfter; + var insertBefore_1 = insertBefore; + var is_1 = is; + var keydown_1 = keydown; + var keypress_1 = keypress; + var keyup_1 = keyup; + var mousedown_1 = mousedown; + var mouseenter_1 = mouseenter; + var mouseleave_1 = mouseleave; + var mousemove_1 = mousemove; + var mouseout_1 = mouseout; + var mouseover_1 = mouseover; + var mouseup_1 = mouseup; + var next_1 = next; + var nextAll_1 = nextAll; + var off_1 = off; + var offset_1 = offset; + var on_1 = on; + var once_1 = once; + var outerHeight_1 = outerHeight; + var outerWidth_1 = outerWidth; + var parent_1 = parent; + var parents_1 = parents; + var prepend_1 = prepend; + var prependTo_1 = prependTo; + var prev_1 = prev; + var prevAll_1 = prevAll; + var prop_1 = prop; + var remove_1 = remove; + var removeAttr_1 = removeAttr; + var removeClass_1 = removeClass; + var removeData_1 = removeData; + var resize_1 = resize; + var scroll_1 = scroll; + var scrollLeft_1 = scrollLeft; + var scrollTo_1 = scrollTo; + var scrollTop_1 = scrollTop; + var show_1 = show; + var siblings_1 = siblings; + var stop_1 = stop; + var styles_1 = styles; + var submit_1 = submit; + var text_1 = text; + var toggleClass_1 = toggleClass; + var touchend_1 = touchend; + var touchmove_1 = touchmove; + var touchstart_1 = touchstart; + var transform_1 = transform; + var transition_1 = transition; + var transitionEnd_1 = transitionEnd; + var trigger_1 = trigger; + var val_1 = val; + var value_1 = value; + var width_1 = width; + + var dom7_cjs = /*#__PURE__*/Object.defineProperty({ + $: $_1, + add: add_1, + addClass: addClass_1, + animate: animate_1, + animationEnd: animationEnd_1, + append: append_1, + appendTo: appendTo_1, + attr: attr_1, + blur: blur_1, + change: change_1, + children: children_1, + click: click_1, + closest: closest_1, + css: css_1, + data: data_1, + dataset: dataset_1, + default: _default$1, + detach: detach_1, + each: each_1, + empty: empty_1, + eq: eq_1, + filter: filter_1, + find: find_1, + focus: focus_1, + focusin: focusin_1, + focusout: focusout_1, + hasClass: hasClass_1, + height: height_1, + hide: hide_1, + html: html_1, + index: index_1, + insertAfter: insertAfter_1, + insertBefore: insertBefore_1, + is: is_1, + keydown: keydown_1, + keypress: keypress_1, + keyup: keyup_1, + mousedown: mousedown_1, + mouseenter: mouseenter_1, + mouseleave: mouseleave_1, + mousemove: mousemove_1, + mouseout: mouseout_1, + mouseover: mouseover_1, + mouseup: mouseup_1, + next: next_1, + nextAll: nextAll_1, + off: off_1, + offset: offset_1, + on: on_1, + once: once_1, + outerHeight: outerHeight_1, + outerWidth: outerWidth_1, + parent: parent_1, + parents: parents_1, + prepend: prepend_1, + prependTo: prependTo_1, + prev: prev_1, + prevAll: prevAll_1, + prop: prop_1, + remove: remove_1, + removeAttr: removeAttr_1, + removeClass: removeClass_1, + removeData: removeData_1, + resize: resize_1, + scroll: scroll_1, + scrollLeft: scrollLeft_1, + scrollTo: scrollTo_1, + scrollTop: scrollTop_1, + show: show_1, + siblings: siblings_1, + stop: stop_1, + styles: styles_1, + submit: submit_1, + text: text_1, + toggleClass: toggleClass_1, + touchend: touchend_1, + touchmove: touchmove_1, + touchstart: touchstart_1, + transform: transform_1, + transition: transition_1, + transitionEnd: transitionEnd_1, + trigger: trigger_1, + val: val_1, + value: value_1, + width: width_1 + }, '__esModule', {value: true}); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + /** Used as references for various `Number` constants. */ + var MAX_SAFE_INTEGER = 9007199254740991; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]'; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^(?:0|[1-9]\d*)$/; + + /** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** Used for built-in method references. */ + var objectProto$3 = Object.prototype; + + /** Used to check objects for own properties. */ + var hasOwnProperty$1 = objectProto$3.hasOwnProperty; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$3 = objectProto$3.toString; + + /** Built-in value references. */ + var propertyIsEnumerable = objectProto$3.propertyIsEnumerable; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeKeys = overArg(Object.keys, Object); + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty$1.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; + } + + /** + * The base implementation of `_.forEach` without support for iteratee shorthands. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + */ + var baseEach = createBaseEach(baseForOwn); + + /** + * The base implementation of `baseForOwn` which iterates over `object` + * properties returned by `keysFunc` and invokes `iteratee` for each property. + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseFor = createBaseFor(); + + /** + * The base implementation of `_.forOwn` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwn(object, iteratee) { + return object && baseFor(object, iteratee, keys); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty$1.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * Creates a `baseEach` or `baseEachRight` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseEach(eachFunc, fromRight) { + return function(collection, iteratee) { + if (collection == null) { + return collection; + } + if (!isArrayLike(collection)) { + return eachFunc(collection, iteratee); + } + var length = collection.length, + index = fromRight ? length : -1, + iterable = Object(collection); + + while ((fromRight ? index-- : ++index < length)) { + if (iteratee(iterable[index], index, iterable) === false) { + break; + } + } + return collection; + }; + } + + /** + * Creates a base function for methods like `_.forIn` and `_.forOwn`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var index = -1, + iterable = Object(object), + props = keysFunc(object), + length = props.length; + + while (length--) { + var key = props[fromRight ? length : ++index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto$3; + + return value === proto; + } + + /** + * Iterates over elements of `collection` and invokes `iteratee` for each element. + * The iteratee is invoked with three arguments: (value, index|key, collection). + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * **Note:** As with other "Collections" methods, objects with a "length" + * property are iterated like arrays. To avoid this behavior use `_.forIn` + * or `_.forOwn` for object iteration. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @alias each + * @category Collection + * @param {Array|Object} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Array|Object} Returns `collection`. + * @see _.forEachRight + * @example + * + * _([1, 2]).forEach(function(value) { + * console.log(value); + * }); + * // => Logs `1` then `2`. + * + * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { + * console.log(key); + * }); + * // => Logs 'a' then 'b' (iteration order is not guaranteed). + */ + function forEach(collection, iteratee) { + var func = isArray(collection) ? arrayEach : baseEach; + return func(collection, typeof iteratee == 'function' ? iteratee : identity); + } + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject(value) && hasOwnProperty$1.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString$3.call(value) == argsTag); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray = Array.isArray; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject(value) { + return isObjectLike$3(value) && isArrayLike(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject$2(value) ? objectToString$3.call(value) : ''; + return tag == funcTag || tag == genTag; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject$2(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$3(value) { + return !!value && typeof value == 'object'; + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); + } + + /** + * This method returns the first argument it receives. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Util + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'a': 1 }; + * + * console.log(_.identity(object) === object); + * // => true + */ + function identity(value) { + return value; + } + + var lodash_foreach = forEach; + + let urlAlphabet$1 = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + var urlAlphabet_1 = { urlAlphabet: urlAlphabet$1 }; + + let { urlAlphabet } = urlAlphabet_1; + { + if ( + typeof navigator !== 'undefined' && + navigator.product === 'ReactNative' && + typeof crypto === 'undefined' + ) { + throw new Error( + 'React Native does not have a built-in secure random generator. ' + + 'If you don’t need unpredictable IDs use `nanoid/non-secure`. ' + + 'For secure IDs, import `react-native-get-random-values` ' + + 'before Nano ID.' + ) + } + if (typeof msCrypto !== 'undefined' && typeof crypto === 'undefined') { + throw new Error( + 'Import file with `if (!window.crypto) window.crypto = window.msCrypto`' + + ' before importing Nano ID to fix IE 11 support' + ) + } + if (typeof crypto === 'undefined') { + throw new Error( + 'Your browser does not have secure random generator. ' + + 'If you don’t need unpredictable IDs, you can use nanoid/non-secure.' + ) + } + } + let random = bytes => crypto.getRandomValues(new Uint8Array(bytes)); + let customRandom = (alphabet, size, getRandom) => { + let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1; + let step = -~((1.6 * mask * size) / alphabet.length); + return () => { + let id = ''; + while (true) { + let bytes = getRandom(step); + let j = step; + while (j--) { + id += alphabet[bytes[j] & mask] || ''; + if (id.length === size) return id + } + } + } + }; + let customAlphabet = (alphabet, size) => customRandom(alphabet, size, random); + let nanoid$2 = (size = 21) => { + let id = ''; + let bytes = crypto.getRandomValues(new Uint8Array(size)); + while (size--) { + let byte = bytes[size] & 63; + if (byte < 36) { + id += byte.toString(36); + } else if (byte < 62) { + id += (byte - 26).toString(36).toUpperCase(); + } else if (byte < 63) { + id += '_'; + } else { + id += '-'; + } + } + return id + }; + var index_browser = { nanoid: nanoid$2, customAlphabet, customRandom, urlAlphabet, random }; + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT$1 = 'Expected a function'; + + /** Used as references for various `Number` constants. */ + var NAN$1 = 0 / 0; + + /** `Object#toString` result references. */ + var symbolTag$2 = '[object Symbol]'; + + /** Used to match leading and trailing whitespace. */ + var reTrim$1 = /^\s+|\s+$/g; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex$1 = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary$1 = /^0b[01]+$/i; + + /** Used to detect octal string values. */ + var reIsOctal$1 = /^0o[0-7]+$/i; + + /** Built-in method references without a dependency on `root`. */ + var freeParseInt$1 = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal$2 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf$2 = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root$2 = freeGlobal$2 || freeSelf$2 || Function('return this')(); + + /** Used for built-in method references. */ + var objectProto$2 = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$2 = objectProto$2.toString; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeMax$1 = Math.max, + nativeMin$1 = Math.min; + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now$1 = function() { + return root$2.Date.now(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce$2(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + wait = toNumber$1(wait) || 0; + if (isObject$1(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax$1(toNumber$1(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin$1(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now$1(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now$1()); + } + + function debounced() { + var time = now$1(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed `func` invocations and a `flush` method to + * immediately invoke them. Provide `options` to indicate whether `func` + * should be invoked on the leading and/or trailing edge of the `wait` + * timeout. The `func` is invoked with the last arguments provided to the + * throttled function. Subsequent calls to the throttled function return the + * result of the last `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the throttled function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=true] + * Specify invoking on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // Avoid excessively updating the position while scrolling. + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. + * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); + * jQuery(element).on('click', throttled); + * + * // Cancel the trailing throttled invocation. + * jQuery(window).on('popstate', throttled.cancel); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + if (isObject$1(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce$2(func, wait, { + 'leading': leading, + 'maxWait': wait, + 'trailing': trailing + }); + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject$1(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$2(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol$2(value) { + return typeof value == 'symbol' || + (isObjectLike$2(value) && objectToString$2.call(value) == symbolTag$2); + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber$1(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol$2(value)) { + return NAN$1; + } + if (isObject$1(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject$1(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim$1, ''); + var isBinary = reIsBinary$1.test(value); + return (isBinary || reIsOctal$1.test(value)) + ? freeParseInt$1(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex$1.test(value) ? NAN$1 : +value); + } + + var lodash_throttle = throttle; + + var snabbdom_cjs = createCommonjsModule$1(function (module, exports) { + + Object.defineProperty(exports, '__esModule', { value: true }); + + function createElement(tagName, options) { + return document.createElement(tagName, options); + } + function createElementNS(namespaceURI, qualifiedName, options) { + return document.createElementNS(namespaceURI, qualifiedName, options); + } + function createTextNode(text) { + return document.createTextNode(text); + } + function createComment(text) { + return document.createComment(text); + } + function insertBefore(parentNode, newNode, referenceNode) { + parentNode.insertBefore(newNode, referenceNode); + } + function removeChild(node, child) { + node.removeChild(child); + } + function appendChild(node, child) { + node.appendChild(child); + } + function parentNode(node) { + return node.parentNode; + } + function nextSibling(node) { + return node.nextSibling; + } + function tagName(elm) { + return elm.tagName; + } + function setTextContent(node, text) { + node.textContent = text; + } + function getTextContent(node) { + return node.textContent; + } + function isElement(node) { + return node.nodeType === 1; + } + function isText(node) { + return node.nodeType === 3; + } + function isComment(node) { + return node.nodeType === 8; + } + const htmlDomApi = { + createElement, + createElementNS, + createTextNode, + createComment, + insertBefore, + removeChild, + appendChild, + parentNode, + nextSibling, + tagName, + setTextContent, + getTextContent, + isElement, + isText, + isComment, + }; + + function vnode(sel, data, children, text, elm) { + const key = data === undefined ? undefined : data.key; + return { sel, data, children, text, elm, key }; + } + + const array = Array.isArray; + function primitive(s) { + return typeof s === "string" || + typeof s === "number" || + s instanceof String || + s instanceof Number; + } + + function isUndef(s) { + return s === undefined; + } + function isDef(s) { + return s !== undefined; + } + const emptyNode = vnode("", {}, [], undefined, undefined); + function sameVnode(vnode1, vnode2) { + var _a, _b; + const isSameKey = vnode1.key === vnode2.key; + const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is); + const isSameSel = vnode1.sel === vnode2.sel; + return isSameSel && isSameKey && isSameIs; + } + function isVnode(vnode) { + return vnode.sel !== undefined; + } + function createKeyToOldIdx(children, beginIdx, endIdx) { + var _a; + const map = {}; + for (let i = beginIdx; i <= endIdx; ++i) { + const key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key; + if (key !== undefined) { + map[key] = i; + } + } + return map; + } + const hooks = [ + "create", + "update", + "remove", + "destroy", + "pre", + "post", + ]; + function init$1(modules, domApi) { + const cbs = { + create: [], + update: [], + remove: [], + destroy: [], + pre: [], + post: [], + }; + const api = domApi !== undefined ? domApi : htmlDomApi; + for (const hook of hooks) { + for (const module of modules) { + const currentHook = module[hook]; + if (currentHook !== undefined) { + cbs[hook].push(currentHook); + } + } + } + function emptyNodeAt(elm) { + const id = elm.id ? "#" + elm.id : ""; + // elm.className doesn't return a string when elm is an SVG element inside a shadowRoot. + // https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring + const classes = elm.getAttribute("class"); + const c = classes ? "." + classes.split(" ").join(".") : ""; + return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); + } + function createRmCb(childElm, listeners) { + return function rmCb() { + if (--listeners === 0) { + const parent = api.parentNode(childElm); + api.removeChild(parent, childElm); + } + }; + } + function createElm(vnode, insertedVnodeQueue) { + var _a, _b; + let i; + let data = vnode.data; + if (data !== undefined) { + const init = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init; + if (isDef(init)) { + init(vnode); + data = vnode.data; + } + } + const children = vnode.children; + const sel = vnode.sel; + if (sel === "!") { + if (isUndef(vnode.text)) { + vnode.text = ""; + } + vnode.elm = api.createComment(vnode.text); + } + else if (sel !== undefined) { + // Parse selector + const hashIdx = sel.indexOf("#"); + const dotIdx = sel.indexOf(".", hashIdx); + const hash = hashIdx > 0 ? hashIdx : sel.length; + const dot = dotIdx > 0 ? dotIdx : sel.length; + const tag = hashIdx !== -1 || dotIdx !== -1 + ? sel.slice(0, Math.min(hash, dot)) + : sel; + const elm = (vnode.elm = + isDef(data) && isDef((i = data.ns)) + ? api.createElementNS(i, tag, data) + : api.createElement(tag, data)); + if (hash < dot) + elm.setAttribute("id", sel.slice(hash + 1, dot)); + if (dotIdx > 0) + elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); + for (i = 0; i < cbs.create.length; ++i) + cbs.create[i](emptyNode, vnode); + if (array(children)) { + for (i = 0; i < children.length; ++i) { + const ch = children[i]; + if (ch != null) { + api.appendChild(elm, createElm(ch, insertedVnodeQueue)); + } + } + } + else if (primitive(vnode.text)) { + api.appendChild(elm, api.createTextNode(vnode.text)); + } + const hook = vnode.data.hook; + if (isDef(hook)) { + (_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode); + if (hook.insert) { + insertedVnodeQueue.push(vnode); + } + } + } + else { + vnode.elm = api.createTextNode(vnode.text); + } + return vnode.elm; + } + function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + const ch = vnodes[startIdx]; + if (ch != null) { + api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); + } + } + } + function invokeDestroyHook(vnode) { + var _a, _b; + const data = vnode.data; + if (data !== undefined) { + (_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); + for (let i = 0; i < cbs.destroy.length; ++i) + cbs.destroy[i](vnode); + if (vnode.children !== undefined) { + for (let j = 0; j < vnode.children.length; ++j) { + const child = vnode.children[j]; + if (child != null && typeof child !== "string") { + invokeDestroyHook(child); + } + } + } + } + } + function removeVnodes(parentElm, vnodes, startIdx, endIdx) { + var _a, _b; + for (; startIdx <= endIdx; ++startIdx) { + let listeners; + let rm; + const ch = vnodes[startIdx]; + if (ch != null) { + if (isDef(ch.sel)) { + invokeDestroyHook(ch); + listeners = cbs.remove.length + 1; + rm = createRmCb(ch.elm, listeners); + for (let i = 0; i < cbs.remove.length; ++i) + cbs.remove[i](ch, rm); + const removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove; + if (isDef(removeHook)) { + removeHook(ch, rm); + } + else { + rm(); + } + } + else { + // Text node + api.removeChild(parentElm, ch.elm); + } + } + } + } + function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { + let oldStartIdx = 0; + let newStartIdx = 0; + let oldEndIdx = oldCh.length - 1; + let oldStartVnode = oldCh[0]; + let oldEndVnode = oldCh[oldEndIdx]; + let newEndIdx = newCh.length - 1; + let newStartVnode = newCh[0]; + let newEndVnode = newCh[newEndIdx]; + let oldKeyToIdx; + let idxInOld; + let elmToMove; + let before; + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (oldStartVnode == null) { + oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left + } + else if (oldEndVnode == null) { + oldEndVnode = oldCh[--oldEndIdx]; + } + else if (newStartVnode == null) { + newStartVnode = newCh[++newStartIdx]; + } + else if (newEndVnode == null) { + newEndVnode = newCh[--newEndIdx]; + } + else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } + else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } + else if (sameVnode(oldStartVnode, newEndVnode)) { + // Vnode moved right + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } + else if (sameVnode(oldEndVnode, newStartVnode)) { + // Vnode moved left + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } + else { + if (oldKeyToIdx === undefined) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + idxInOld = oldKeyToIdx[newStartVnode.key]; + if (isUndef(idxInOld)) { + // New element + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); + } + else { + elmToMove = oldCh[idxInOld]; + if (elmToMove.sel !== newStartVnode.sel) { + api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); + } + else { + patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); + oldCh[idxInOld] = undefined; + api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { + if (oldStartIdx > oldEndIdx) { + before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; + addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } + else { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + } + function patchVnode(oldVnode, vnode, insertedVnodeQueue) { + var _a, _b, _c, _d, _e; + const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook; + (_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); + const elm = (vnode.elm = oldVnode.elm); + const oldCh = oldVnode.children; + const ch = vnode.children; + if (oldVnode === vnode) + return; + if (vnode.data !== undefined) { + for (let i = 0; i < cbs.update.length; ++i) + cbs.update[i](oldVnode, vnode); + (_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode); + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) + updateChildren(elm, oldCh, ch, insertedVnodeQueue); + } + else if (isDef(ch)) { + if (isDef(oldVnode.text)) + api.setTextContent(elm, ""); + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } + else if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } + else if (isDef(oldVnode.text)) { + api.setTextContent(elm, ""); + } + } + else if (oldVnode.text !== vnode.text) { + if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } + api.setTextContent(elm, vnode.text); + } + (_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); + } + return function patch(oldVnode, vnode) { + let i, elm, parent; + const insertedVnodeQueue = []; + for (i = 0; i < cbs.pre.length; ++i) + cbs.pre[i](); + if (!isVnode(oldVnode)) { + oldVnode = emptyNodeAt(oldVnode); + } + if (sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode, insertedVnodeQueue); + } + else { + elm = oldVnode.elm; + parent = api.parentNode(elm); + createElm(vnode, insertedVnodeQueue); + if (parent !== null) { + api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); + removeVnodes(parent, [oldVnode], 0, 0); + } + } + for (i = 0; i < insertedVnodeQueue.length; ++i) { + insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); + } + for (i = 0; i < cbs.post.length; ++i) + cbs.post[i](); + return vnode; + }; + } + + function addNS(data, children, sel) { + data.ns = "http://www.w3.org/2000/svg"; + if (sel !== "foreignObject" && children !== undefined) { + for (let i = 0; i < children.length; ++i) { + const childData = children[i].data; + if (childData !== undefined) { + addNS(childData, children[i].children, children[i].sel); + } + } + } + } + function h(sel, b, c) { + let data = {}; + let children; + let text; + let i; + if (c !== undefined) { + if (b !== null) { + data = b; + } + if (array(c)) { + children = c; + } + else if (primitive(c)) { + text = c.toString(); + } + else if (c && c.sel) { + children = [c]; + } + } + else if (b !== undefined && b !== null) { + if (array(b)) { + children = b; + } + else if (primitive(b)) { + text = b.toString(); + } + else if (b && b.sel) { + children = [b]; + } + else { + data = b; + } + } + if (children !== undefined) { + for (i = 0; i < children.length; ++i) { + if (primitive(children[i])) + children[i] = vnode(undefined, undefined, undefined, children[i], undefined); + } + } + if (sel[0] === "s" && + sel[1] === "v" && + sel[2] === "g" && + (sel.length === 3 || sel[3] === "." || sel[3] === "#")) { + addNS(data, children, sel); + } + return vnode(sel, data, children, text, undefined); + } + + function copyToThunk(vnode, thunk) { + vnode.data.fn = thunk.data.fn; + vnode.data.args = thunk.data.args; + thunk.data = vnode.data; + thunk.children = vnode.children; + thunk.text = vnode.text; + thunk.elm = vnode.elm; + } + function init(thunk) { + const cur = thunk.data; + const vnode = cur.fn(...cur.args); + copyToThunk(vnode, thunk); + } + function prepatch(oldVnode, thunk) { + let i; + const old = oldVnode.data; + const cur = thunk.data; + const oldArgs = old.args; + const args = cur.args; + if (old.fn !== cur.fn || oldArgs.length !== args.length) { + copyToThunk(cur.fn(...args), thunk); + return; + } + for (i = 0; i < args.length; ++i) { + if (oldArgs[i] !== args[i]) { + copyToThunk(cur.fn(...args), thunk); + return; + } + } + copyToThunk(oldVnode, thunk); + } + const thunk = function thunk(sel, key, fn, args) { + if (args === undefined) { + args = fn; + fn = key; + key = undefined; + } + return h(sel, { + key: key, + hook: { init, prepatch }, + fn: fn, + args: args, + }); + }; + + function pre(vnode, newVnode) { + const attachData = vnode.data.attachData; + // Copy created placeholder and real element from old vnode + newVnode.data.attachData.placeholder = attachData.placeholder; + newVnode.data.attachData.real = attachData.real; + // Mount real element in vnode so the patch process operates on it + vnode.elm = vnode.data.attachData.real; + } + function post(_, vnode) { + // Mount dummy placeholder in vnode so potential reorders use it + vnode.elm = vnode.data.attachData.placeholder; + } + function destroy(vnode) { + // Remove placeholder + if (vnode.elm !== undefined) { + vnode.elm.parentNode.removeChild(vnode.elm); + } + // Remove real element from where it was inserted + vnode.elm = vnode.data.attachData.real; + } + function create(_, vnode) { + const real = vnode.elm; + const attachData = vnode.data.attachData; + const placeholder = document.createElement("span"); + // Replace actual element with dummy placeholder + // Snabbdom will then insert placeholder instead + vnode.elm = placeholder; + attachData.target.appendChild(real); + attachData.real = real; + attachData.placeholder = placeholder; + } + function attachTo(target, vnode) { + if (vnode.data === undefined) + vnode.data = {}; + if (vnode.data.hook === undefined) + vnode.data.hook = {}; + const data = vnode.data; + const hook = vnode.data.hook; + data.attachData = { target: target, placeholder: undefined, real: undefined }; + hook.create = create; + hook.prepatch = pre; + hook.postpatch = post; + hook.destroy = destroy; + return vnode; + } + + function toVNode(node, domApi) { + const api = domApi !== undefined ? domApi : htmlDomApi; + let text; + if (api.isElement(node)) { + const id = node.id ? "#" + node.id : ""; + const cn = node.getAttribute("class"); + const c = cn ? "." + cn.split(" ").join(".") : ""; + const sel = api.tagName(node).toLowerCase() + id + c; + const attrs = {}; + const children = []; + let name; + let i, n; + const elmAttrs = node.attributes; + const elmChildren = node.childNodes; + for (i = 0, n = elmAttrs.length; i < n; i++) { + name = elmAttrs[i].nodeName; + if (name !== "id" && name !== "class") { + attrs[name] = elmAttrs[i].nodeValue; + } + } + for (i = 0, n = elmChildren.length; i < n; i++) { + children.push(toVNode(elmChildren[i], domApi)); + } + return vnode(sel, { attrs }, children, undefined, node); + } + else if (api.isText(node)) { + text = api.getTextContent(node); + return vnode(undefined, undefined, undefined, text, node); + } + else if (api.isComment(node)) { + text = api.getTextContent(node); + return vnode("!", {}, [], text, node); + } + else { + return vnode("", {}, [], undefined, node); + } + } + + const xlinkNS = "http://www.w3.org/1999/xlink"; + const xmlNS = "http://www.w3.org/XML/1998/namespace"; + const colonChar = 58; + const xChar = 120; + function updateAttrs(oldVnode, vnode) { + let key; + const elm = vnode.elm; + let oldAttrs = oldVnode.data.attrs; + let attrs = vnode.data.attrs; + if (!oldAttrs && !attrs) + return; + if (oldAttrs === attrs) + return; + oldAttrs = oldAttrs || {}; + attrs = attrs || {}; + // update modified attributes, add new attributes + for (key in attrs) { + const cur = attrs[key]; + const old = oldAttrs[key]; + if (old !== cur) { + if (cur === true) { + elm.setAttribute(key, ""); + } + else if (cur === false) { + elm.removeAttribute(key); + } + else { + if (key.charCodeAt(0) !== xChar) { + elm.setAttribute(key, cur); + } + else if (key.charCodeAt(3) === colonChar) { + // Assume xml namespace + elm.setAttributeNS(xmlNS, key, cur); + } + else if (key.charCodeAt(5) === colonChar) { + // Assume xlink namespace + elm.setAttributeNS(xlinkNS, key, cur); + } + else { + elm.setAttribute(key, cur); + } + } + } + } + // remove removed attributes + // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) + // the other option is to remove all attributes with value == undefined + for (key in oldAttrs) { + if (!(key in attrs)) { + elm.removeAttribute(key); + } + } + } + const attributesModule = { + create: updateAttrs, + update: updateAttrs, + }; + + function updateClass(oldVnode, vnode) { + let cur; + let name; + const elm = vnode.elm; + let oldClass = oldVnode.data.class; + let klass = vnode.data.class; + if (!oldClass && !klass) + return; + if (oldClass === klass) + return; + oldClass = oldClass || {}; + klass = klass || {}; + for (name in oldClass) { + if (oldClass[name] && !Object.prototype.hasOwnProperty.call(klass, name)) { + // was `true` and now not provided + elm.classList.remove(name); + } + } + for (name in klass) { + cur = klass[name]; + if (cur !== oldClass[name]) { + elm.classList[cur ? "add" : "remove"](name); + } + } + } + const classModule = { create: updateClass, update: updateClass }; + + const CAPS_REGEX = /[A-Z]/g; + function updateDataset(oldVnode, vnode) { + const elm = vnode.elm; + let oldDataset = oldVnode.data.dataset; + let dataset = vnode.data.dataset; + let key; + if (!oldDataset && !dataset) + return; + if (oldDataset === dataset) + return; + oldDataset = oldDataset || {}; + dataset = dataset || {}; + const d = elm.dataset; + for (key in oldDataset) { + if (!dataset[key]) { + if (d) { + if (key in d) { + delete d[key]; + } + } + else { + elm.removeAttribute("data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase()); + } + } + } + for (key in dataset) { + if (oldDataset[key] !== dataset[key]) { + if (d) { + d[key] = dataset[key]; + } + else { + elm.setAttribute("data-" + key.replace(CAPS_REGEX, "-$&").toLowerCase(), dataset[key]); + } + } + } + } + const datasetModule = { + create: updateDataset, + update: updateDataset, + }; + + function invokeHandler(handler, vnode, event) { + if (typeof handler === "function") { + // call function handler + handler.call(vnode, event, vnode); + } + else if (typeof handler === "object") { + // call multiple handlers + for (let i = 0; i < handler.length; i++) { + invokeHandler(handler[i], vnode, event); + } + } + } + function handleEvent(event, vnode) { + const name = event.type; + const on = vnode.data.on; + // call event handler(s) if exists + if (on && on[name]) { + invokeHandler(on[name], vnode, event); + } + } + function createListener() { + return function handler(event) { + handleEvent(event, handler.vnode); + }; + } + function updateEventListeners(oldVnode, vnode) { + const oldOn = oldVnode.data.on; + const oldListener = oldVnode.listener; + const oldElm = oldVnode.elm; + const on = vnode && vnode.data.on; + const elm = (vnode && vnode.elm); + let name; + // optimization for reused immutable handlers + if (oldOn === on) { + return; + } + // remove existing listeners which no longer used + if (oldOn && oldListener) { + // if element changed or deleted we remove all existing listeners unconditionally + if (!on) { + for (name in oldOn) { + // remove listener if element was changed or existing listeners removed + oldElm.removeEventListener(name, oldListener, false); + } + } + else { + for (name in oldOn) { + // remove listener if existing listener removed + if (!on[name]) { + oldElm.removeEventListener(name, oldListener, false); + } + } + } + } + // add new listeners which has not already attached + if (on) { + // reuse existing listener or create new + const listener = (vnode.listener = + oldVnode.listener || createListener()); + // update vnode for listener + listener.vnode = vnode; + // if element changed or added we add all needed listeners unconditionally + if (!oldOn) { + for (name in on) { + // add listener if element was changed or new listeners added + elm.addEventListener(name, listener, false); + } + } + else { + for (name in on) { + // add listener if new listener added + if (!oldOn[name]) { + elm.addEventListener(name, listener, false); + } + } + } + } + } + const eventListenersModule = { + create: updateEventListeners, + update: updateEventListeners, + destroy: updateEventListeners, + }; + + function updateProps(oldVnode, vnode) { + let key; + let cur; + let old; + const elm = vnode.elm; + let oldProps = oldVnode.data.props; + let props = vnode.data.props; + if (!oldProps && !props) + return; + if (oldProps === props) + return; + oldProps = oldProps || {}; + props = props || {}; + for (key in props) { + cur = props[key]; + old = oldProps[key]; + if (old !== cur && (key !== "value" || elm[key] !== cur)) { + elm[key] = cur; + } + } + } + const propsModule = { create: updateProps, update: updateProps }; + + // Bindig `requestAnimationFrame` like this fixes a bug in IE/Edge. See #360 and #409. + const raf = (typeof window !== "undefined" && + window.requestAnimationFrame.bind(window)) || + setTimeout; + const nextFrame = function (fn) { + raf(function () { + raf(fn); + }); + }; + let reflowForced = false; + function setNextFrame(obj, prop, val) { + nextFrame(function () { + obj[prop] = val; + }); + } + function updateStyle(oldVnode, vnode) { + let cur; + let name; + const elm = vnode.elm; + let oldStyle = oldVnode.data.style; + let style = vnode.data.style; + if (!oldStyle && !style) + return; + if (oldStyle === style) + return; + oldStyle = oldStyle || {}; + style = style || {}; + const oldHasDel = "delayed" in oldStyle; + for (name in oldStyle) { + if (!style[name]) { + if (name[0] === "-" && name[1] === "-") { + elm.style.removeProperty(name); + } + else { + elm.style[name] = ""; + } + } + } + for (name in style) { + cur = style[name]; + if (name === "delayed" && style.delayed) { + for (const name2 in style.delayed) { + cur = style.delayed[name2]; + if (!oldHasDel || cur !== oldStyle.delayed[name2]) { + setNextFrame(elm.style, name2, cur); + } + } + } + else if (name !== "remove" && cur !== oldStyle[name]) { + if (name[0] === "-" && name[1] === "-") { + elm.style.setProperty(name, cur); + } + else { + elm.style[name] = cur; + } + } + } + } + function applyDestroyStyle(vnode) { + let style; + let name; + const elm = vnode.elm; + const s = vnode.data.style; + if (!s || !(style = s.destroy)) + return; + for (name in style) { + elm.style[name] = style[name]; + } + } + function applyRemoveStyle(vnode, rm) { + const s = vnode.data.style; + if (!s || !s.remove) { + rm(); + return; + } + if (!reflowForced) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + vnode.elm.offsetLeft; + reflowForced = true; + } + let name; + const elm = vnode.elm; + let i = 0; + const style = s.remove; + let amount = 0; + const applied = []; + for (name in style) { + applied.push(name); + elm.style[name] = style[name]; + } + const compStyle = getComputedStyle(elm); + const props = compStyle["transition-property"].split(", "); + for (; i < props.length; ++i) { + if (applied.indexOf(props[i]) !== -1) + amount++; + } + elm.addEventListener("transitionend", function (ev) { + if (ev.target === elm) + --amount; + if (amount === 0) + rm(); + }); + } + function forceReflow() { + reflowForced = false; + } + const styleModule = { + pre: forceReflow, + create: updateStyle, + update: updateStyle, + destroy: applyDestroyStyle, + remove: applyRemoveStyle, + }; + + /* eslint-disable @typescript-eslint/no-namespace, import/export */ + function flattenAndFilter(children, flattened) { + for (const child of children) { + // filter out falsey children, except 0 since zero can be a valid value e.g inside a chart + if (child !== undefined && + child !== null && + child !== false && + child !== "") { + if (Array.isArray(child)) { + flattenAndFilter(child, flattened); + } + else if (typeof child === "string" || + typeof child === "number" || + typeof child === "boolean") { + flattened.push(vnode(undefined, undefined, undefined, String(child), undefined)); + } + else { + flattened.push(child); + } + } + } + return flattened; + } + /** + * jsx/tsx compatible factory function + * see: https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions + */ + function jsx(tag, data, ...children) { + const flatChildren = flattenAndFilter(children, []); + if (typeof tag === "function") { + // tag is a function component + return tag(data, flatChildren); + } + else { + if (flatChildren.length === 1 && + !flatChildren[0].sel && + flatChildren[0].text) { + // only child is a simple text node, pass as text for a simpler vtree + return h(tag, data, flatChildren[0].text); + } + else { + return h(tag, data, flatChildren); + } + } + } + (function (jsx) { + })(jsx || (jsx = {})); + + exports.array = array; + exports.attachTo = attachTo; + exports.attributesModule = attributesModule; + exports.classModule = classModule; + exports.datasetModule = datasetModule; + exports.eventListenersModule = eventListenersModule; + exports.h = h; + exports.htmlDomApi = htmlDomApi; + exports.init = init$1; + exports.jsx = jsx; + exports.primitive = primitive; + exports.propsModule = propsModule; + exports.styleModule = styleModule; + exports.thunk = thunk; + exports.toVNode = toVNode; + exports.vnode = vnode; + }); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as references for various `Number` constants. */ + var INFINITY = 1 / 0; + + /** `Object#toString` result references. */ + var symbolTag$1 = '[object Symbol]'; + + /** Used to match words composed of alphanumeric characters. */ + var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; + + /** Used to match Latin Unicode letters (excluding mathematical operators). */ + var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; + + /** Used to compose unicode character classes. */ + var rsAstralRange = '\\ud800-\\udfff', + rsComboMarksRange = '\\u0300-\\u036f\\ufe20-\\ufe23', + rsComboSymbolsRange = '\\u20d0-\\u20f0', + rsDingbatRange = '\\u2700-\\u27bf', + rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', + rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', + rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', + rsPunctuationRange = '\\u2000-\\u206f', + rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', + rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', + rsVarRange = '\\ufe0e\\ufe0f', + rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; + + /** Used to compose unicode capture groups. */ + var rsApos = "['\u2019]", + rsAstral = '[' + rsAstralRange + ']', + rsBreak = '[' + rsBreakRange + ']', + rsCombo = '[' + rsComboMarksRange + rsComboSymbolsRange + ']', + rsDigits = '\\d+', + rsDingbat = '[' + rsDingbatRange + ']', + rsLower = '[' + rsLowerRange + ']', + rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', + rsFitz = '\\ud83c[\\udffb-\\udfff]', + rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', + rsNonAstral = '[^' + rsAstralRange + ']', + rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsUpper = '[' + rsUpperRange + ']', + rsZWJ = '\\u200d'; + + /** Used to compose unicode regexes. */ + var rsLowerMisc = '(?:' + rsLower + '|' + rsMisc + ')', + rsUpperMisc = '(?:' + rsUpper + '|' + rsMisc + ')', + rsOptLowerContr = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?', + rsOptUpperContr = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?', + reOptMod = rsModifier + '?', + rsOptVar = '[' + rsVarRange + ']?', + rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', + rsSeq = rsOptVar + reOptMod + rsOptJoin, + rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, + rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; + + /** Used to match apostrophes. */ + var reApos = RegExp(rsApos, 'g'); + + /** + * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and + * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). + */ + var reComboMark = RegExp(rsCombo, 'g'); + + /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ + var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); + + /** Used to match complex or compound words. */ + var reUnicodeWord = RegExp([ + rsUpper + '?' + rsLower + '+' + rsOptLowerContr + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', + rsUpperMisc + '+' + rsOptUpperContr + '(?=' + [rsBreak, rsUpper + rsLowerMisc, '$'].join('|') + ')', + rsUpper + '?' + rsLowerMisc + '+' + rsOptLowerContr, + rsUpper + '+' + rsOptUpperContr, + rsDigits, + rsEmoji + ].join('|'), 'g'); + + /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ + var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboMarksRange + rsComboSymbolsRange + rsVarRange + ']'); + + /** Used to detect strings that need a more robust regexp to match words. */ + var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; + + /** Used to map Latin Unicode letters to basic Latin letters. */ + var deburredLetters = { + // Latin-1 Supplement block. + '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', + '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', + '\xc7': 'C', '\xe7': 'c', + '\xd0': 'D', '\xf0': 'd', + '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', + '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', + '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', + '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', + '\xd1': 'N', '\xf1': 'n', + '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', + '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', + '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', + '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', + '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', + '\xc6': 'Ae', '\xe6': 'ae', + '\xde': 'Th', '\xfe': 'th', + '\xdf': 'ss', + // Latin Extended-A block. + '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', + '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', + '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', + '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', + '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', + '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', + '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', + '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', + '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', + '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', + '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', + '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', + '\u0134': 'J', '\u0135': 'j', + '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', + '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', + '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', + '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', + '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', + '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', + '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', + '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', + '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', + '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', + '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', + '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', + '\u0163': 't', '\u0165': 't', '\u0167': 't', + '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', + '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', + '\u0174': 'W', '\u0175': 'w', + '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', + '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', + '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', + '\u0132': 'IJ', '\u0133': 'ij', + '\u0152': 'Oe', '\u0153': 'oe', + '\u0149': "'n", '\u017f': 'ss' + }; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal$1 = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf$1 = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root$1 = freeGlobal$1 || freeSelf$1 || Function('return this')(); + + /** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function asciiToArray(string) { + return string.split(''); + } + + /** + * Splits an ASCII `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function asciiWords(string) { + return string.match(reAsciiWord) || []; + } + + /** + * The base implementation of `_.propertyOf` without support for deep paths. + * + * @private + * @param {Object} object The object to query. + * @returns {Function} Returns the new accessor function. + */ + function basePropertyOf(object) { + return function(key) { + return object == null ? undefined : object[key]; + }; + } + + /** + * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A + * letters to basic Latin letters. + * + * @private + * @param {string} letter The matched letter to deburr. + * @returns {string} Returns the deburred letter. + */ + var deburrLetter = basePropertyOf(deburredLetters); + + /** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ + function hasUnicode(string) { + return reHasUnicode.test(string); + } + + /** + * Checks if `string` contains a word composed of Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a word is found, else `false`. + */ + function hasUnicodeWord(string) { + return reHasUnicodeWord.test(string); + } + + /** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function stringToArray(string) { + return hasUnicode(string) + ? unicodeToArray(string) + : asciiToArray(string); + } + + /** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ + function unicodeToArray(string) { + return string.match(reUnicode) || []; + } + + /** + * Splits a Unicode `string` into an array of its words. + * + * @private + * @param {string} The string to inspect. + * @returns {Array} Returns the words of `string`. + */ + function unicodeWords(string) { + return string.match(reUnicodeWord) || []; + } + + /** Used for built-in method references. */ + var objectProto$1 = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString$1 = objectProto$1.toString; + + /** Built-in value references. */ + var Symbol$1 = root$1.Symbol; + + /** Used to convert symbols to primitives and strings. */ + var symbolProto = Symbol$1 ? Symbol$1.prototype : undefined, + symbolToString = symbolProto ? symbolProto.toString : undefined; + + /** + * The base implementation of `_.slice` without an iteratee call guard. + * + * @private + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = end > length ? length : end; + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; + } + + /** + * The base implementation of `_.toString` which doesn't convert nullish + * values to empty strings. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ + function baseToString(value) { + // Exit early for strings to avoid a performance hit in some environments. + if (typeof value == 'string') { + return value; + } + if (isSymbol$1(value)) { + return symbolToString ? symbolToString.call(value) : ''; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; + } + + /** + * Casts `array` to a slice if it's needed. + * + * @private + * @param {Array} array The array to inspect. + * @param {number} start The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the cast slice. + */ + function castSlice(array, start, end) { + var length = array.length; + end = end === undefined ? length : end; + return (!start && end >= length) ? array : baseSlice(array, start, end); + } + + /** + * Creates a function like `_.lowerFirst`. + * + * @private + * @param {string} methodName The name of the `String` case method to use. + * @returns {Function} Returns the new case function. + */ + function createCaseFirst(methodName) { + return function(string) { + string = toString(string); + + var strSymbols = hasUnicode(string) + ? stringToArray(string) + : undefined; + + var chr = strSymbols + ? strSymbols[0] + : string.charAt(0); + + var trailing = strSymbols + ? castSlice(strSymbols, 1).join('') + : string.slice(1); + + return chr[methodName]() + trailing; + }; + } + + /** + * Creates a function like `_.camelCase`. + * + * @private + * @param {Function} callback The function to combine each word. + * @returns {Function} Returns the new compounder function. + */ + function createCompounder(callback) { + return function(string) { + return arrayReduce(words(deburr(string).replace(reApos, '')), callback, ''); + }; + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike$1(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol$1(value) { + return typeof value == 'symbol' || + (isObjectLike$1(value) && objectToString$1.call(value) == symbolTag$1); + } + + /** + * Converts `value` to a string. An empty string is returned for `null` + * and `undefined` values. The sign of `-0` is preserved. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {string} Returns the string. + * @example + * + * _.toString(null); + * // => '' + * + * _.toString(-0); + * // => '-0' + * + * _.toString([1, 2, 3]); + * // => '1,2,3' + */ + function toString(value) { + return value == null ? '' : baseToString(value); + } + + /** + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the camel cased string. + * @example + * + * _.camelCase('Foo Bar'); + * // => 'fooBar' + * + * _.camelCase('--foo-bar--'); + * // => 'fooBar' + * + * _.camelCase('__FOO_BAR__'); + * // => 'fooBar' + */ + var camelCase = createCompounder(function(result, word, index) { + word = word.toLowerCase(); + return result + (index ? capitalize(word) : word); + }); + + /** + * Converts the first character of `string` to upper case and the remaining + * to lower case. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to capitalize. + * @returns {string} Returns the capitalized string. + * @example + * + * _.capitalize('FRED'); + * // => 'Fred' + */ + function capitalize(string) { + return upperFirst(toString(string).toLowerCase()); + } + + /** + * Deburrs `string` by converting + * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) + * letters to basic Latin letters and removing + * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to deburr. + * @returns {string} Returns the deburred string. + * @example + * + * _.deburr('déjà vu'); + * // => 'deja vu' + */ + function deburr(string) { + string = toString(string); + return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); + } + + /** + * Converts the first character of `string` to upper case. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.upperFirst('fred'); + * // => 'Fred' + * + * _.upperFirst('FRED'); + * // => 'FRED' + */ + var upperFirst = createCaseFirst('toUpperCase'); + + /** + * Splits `string` into an array of its words. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to inspect. + * @param {RegExp|string} [pattern] The pattern to match words. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {Array} Returns the words of `string`. + * @example + * + * _.words('fred, barney, & pebbles'); + * // => ['fred', 'barney', 'pebbles'] + * + * _.words('fred, barney, & pebbles', /[^, ]+/g); + * // => ['fred', 'barney', '&', 'pebbles'] + */ + function words(string, pattern, guard) { + string = toString(string); + pattern = guard ? undefined : pattern; + + if (pattern === undefined) { + return hasUnicodeWord(string) ? unicodeWords(string) : asciiWords(string); + } + return string.match(pattern) || []; + } + + var lodash_camelcase = camelCase; + + /** + * Constants. + */ + + var IS_MAC = typeof window != 'undefined' && /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); + + var MODIFIERS = { + alt: 'altKey', + control: 'ctrlKey', + meta: 'metaKey', + shift: 'shiftKey' + }; + + var ALIASES = { + add: '+', + break: 'pause', + cmd: 'meta', + command: 'meta', + ctl: 'control', + ctrl: 'control', + del: 'delete', + down: 'arrowdown', + esc: 'escape', + ins: 'insert', + left: 'arrowleft', + mod: IS_MAC ? 'meta' : 'control', + opt: 'alt', + option: 'alt', + return: 'enter', + right: 'arrowright', + space: ' ', + spacebar: ' ', + up: 'arrowup', + win: 'meta', + windows: 'meta' + }; + + var CODES = { + backspace: 8, + tab: 9, + enter: 13, + shift: 16, + control: 17, + alt: 18, + pause: 19, + capslock: 20, + escape: 27, + ' ': 32, + pageup: 33, + pagedown: 34, + end: 35, + home: 36, + arrowleft: 37, + arrowup: 38, + arrowright: 39, + arrowdown: 40, + insert: 45, + delete: 46, + meta: 91, + numlock: 144, + scrolllock: 145, + ';': 186, + '=': 187, + ',': 188, + '-': 189, + '.': 190, + '/': 191, + '`': 192, + '[': 219, + '\\': 220, + ']': 221, + '\'': 222 + }; + + for (var f = 1; f < 20; f++) { + CODES['f' + f] = 111 + f; + } + + /** + * Is hotkey? + */ + + function isHotkey(hotkey, options, event) { + if (options && !('byKey' in options)) { + event = options; + options = null; + } + + if (!Array.isArray(hotkey)) { + hotkey = [hotkey]; + } + + var array = hotkey.map(function (string) { + return parseHotkey(string, options); + }); + var check = function check(e) { + return array.some(function (object) { + return compareHotkey(object, e); + }); + }; + var ret = event == null ? check : check(event); + return ret; + } + + function isCodeHotkey(hotkey, event) { + return isHotkey(hotkey, event); + } + + function isKeyHotkey(hotkey, event) { + return isHotkey(hotkey, { byKey: true }, event); + } + + /** + * Parse. + */ + + function parseHotkey(hotkey, options) { + var byKey = options && options.byKey; + var ret = {}; + + // Special case to handle the `+` key since we use it as a separator. + hotkey = hotkey.replace('++', '+add'); + var values = hotkey.split('+'); + var length = values.length; + + // Ensure that all the modifiers are set to false unless the hotkey has them. + + for (var k in MODIFIERS) { + ret[MODIFIERS[k]] = false; + } + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = values[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var value = _step.value; + + var optional = value.endsWith('?') && value.length > 1; + + if (optional) { + value = value.slice(0, -1); + } + + var name = toKeyName(value); + var modifier = MODIFIERS[name]; + + if (value.length > 1 && !modifier && !ALIASES[value] && !CODES[name]) { + throw new TypeError('Unknown modifier: "' + value + '"'); + } + + if (length === 1 || !modifier) { + if (byKey) { + ret.key = name; + } else { + ret.which = toKeyCode(value); + } + } + + if (modifier) { + ret[modifier] = optional ? null : true; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return ret; + } + + /** + * Compare. + */ + + function compareHotkey(object, event) { + for (var key in object) { + var expected = object[key]; + var actual = void 0; + + if (expected == null) { + continue; + } + + if (key === 'key' && event.key != null) { + actual = event.key.toLowerCase(); + } else if (key === 'which') { + actual = expected === 91 && event.which === 93 ? 91 : event.which; + } else { + actual = event[key]; + } + + if (actual == null && expected === false) { + continue; + } + + if (actual !== expected) { + return false; + } + } + + return true; + } + + /** + * Utils. + */ + + function toKeyCode(name) { + name = toKeyName(name); + var code = CODES[name] || name.toUpperCase().charCodeAt(0); + return code; + } + + function toKeyName(name) { + name = name.toLowerCase(); + name = ALIASES[name] || name; + return name; + } + + /** + * Export. + */ + + var _default = isHotkey; + var isHotkey_1 = isHotkey; + var isCodeHotkey_1 = isCodeHotkey; + var isKeyHotkey_1 = isKeyHotkey; + var parseHotkey_1 = parseHotkey; + var compareHotkey_1 = compareHotkey; + var toKeyCode_1 = toKeyCode; + var toKeyName_1 = toKeyName; + + var lib$4 = /*#__PURE__*/Object.defineProperty({ + default: _default, + isHotkey: isHotkey_1, + isCodeHotkey: isCodeHotkey_1, + isKeyHotkey: isKeyHotkey_1, + parseHotkey: parseHotkey_1, + compareHotkey: compareHotkey_1, + toKeyCode: toKeyCode_1, + toKeyName: toKeyName_1 + }, '__esModule', {value: true}); + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + + /** Used as references for various `Number` constants. */ + var NAN = 0 / 0; + + /** `Object#toString` result references. */ + var symbolTag = '[object Symbol]'; + + /** Used to match leading and trailing whitespace. */ + var reTrim = /^\s+|\s+$/g; + + /** Used to detect bad signed hexadecimal string values. */ + var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; + + /** Used to detect binary string values. */ + var reIsBinary = /^0b[01]+$/i; + + /** Used to detect octal string values. */ + var reIsOctal = /^0o[0-7]+$/i; + + /** Built-in method references without a dependency on `root`. */ + var freeParseInt = parseInt; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Used for built-in method references. */ + var objectProto = Object.prototype; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString = objectProto.toString; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeMax = Math.max, + nativeMin = Math.min; + + /** + * Gets the timestamp of the number of milliseconds that have elapsed since + * the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Date + * @returns {number} Returns the timestamp. + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => Logs the number of milliseconds it took for the deferred invocation. + */ + var now = function() { + return root.Date.now(); + }; + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed `func` invocations and a `flush` method to immediately invoke them. + * Provide `options` to indicate whether `func` should be invoked on the + * leading and/or trailing edge of the `wait` timeout. The `func` is invoked + * with the last arguments provided to the debounced function. Subsequent + * calls to the debounced function return the result of the last `func` + * invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is + * invoked on the trailing edge of the timeout only if the debounced function + * is invoked more than once during the `wait` timeout. + * + * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred + * until to the next tick, similar to `setTimeout` with a timeout of `0`. + * + * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options={}] The options object. + * @param {boolean} [options.leading=false] + * Specify invoking on the leading edge of the timeout. + * @param {number} [options.maxWait] + * The maximum time `func` is allowed to be delayed before it's invoked. + * @param {boolean} [options.trailing=true] + * Specify invoking on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // Avoid costly calculations while the window size is in flux. + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // Invoke `sendMail` when clicked, debouncing subsequent calls. + * jQuery(element).on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // Ensure `batchLog` is invoked once after 1 second of debounced calls. + * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); + * var source = new EventSource('/stream'); + * jQuery(source).on('message', debounced); + * + * // Cancel the trailing debounced invocation. + * jQuery(window).on('popstate', debounced.cancel); + */ + function debounce$1(func, wait, options) { + var lastArgs, + lastThis, + maxWait, + result, + timerId, + lastCallTime, + lastInvokeTime = 0, + leading = false, + maxing = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = toNumber(wait) || 0; + if (isObject(options)) { + leading = !!options.leading; + maxing = 'maxWait' in options; + maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + var args = lastArgs, + thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + // Reset any `maxWait` timer. + lastInvokeTime = time; + // Start the timer for the trailing edge. + timerId = setTimeout(timerExpired, wait); + // Invoke the leading edge. + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime, + result = wait - timeSinceLastCall; + + return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + var timeSinceLastCall = time - lastCallTime, + timeSinceLastInvoke = time - lastInvokeTime; + + // Either this is the first call, activity has stopped and we're at the + // trailing edge, the system time has gone backwards and we're treating + // it as the trailing edge, or we've hit the `maxWait` limit. + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || + (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + var time = now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + // Only invoke if we have `lastArgs` which means `func` has been + // debounced at least once. + if (trailing && lastArgs) { + return invokeFunc(time); + } + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) { + clearTimeout(timerId); + } + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(now()); + } + + function debounced() { + var time = now(), + isInvoking = shouldInvoke(time); + + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) { + return leadingEdge(lastCallTime); + } + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) { + timerId = setTimeout(timerExpired, wait); + } + return result; + } + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return !!value && typeof value == 'object'; + } + + /** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ + function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && objectToString.call(value) == symbolTag); + } + + /** + * Converts `value` to a number. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to process. + * @returns {number} Returns the number. + * @example + * + * _.toNumber(3.2); + * // => 3.2 + * + * _.toNumber(Number.MIN_VALUE); + * // => 5e-324 + * + * _.toNumber(Infinity); + * // => Infinity + * + * _.toNumber('3.2'); + * // => 3.2 + */ + function toNumber(value) { + if (typeof value == 'number') { + return value; + } + if (isSymbol(value)) { + return NAN; + } + if (isObject(value)) { + var other = typeof value.valueOf == 'function' ? value.valueOf() : value; + value = isObject(other) ? (other + '') : other; + } + if (typeof value != 'string') { + return value === 0 ? value : +value; + } + value = value.replace(reTrim, ''); + var isBinary = reIsBinary.test(value); + return (isBinary || reIsOctal.test(value)) + ? freeParseInt(value.slice(2), isBinary ? 2 : 8) + : (reIsBadHex.test(value) ? NAN : +value); + } + + var lodash_debounce = debounce$1; + + /** + * lodash (Custom Build) + * Build: `lodash modularize exports="npm" -o ./` + * Copyright jQuery Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ + + var lodash_clonedeep = createCommonjsModule$1(function (module, exports) { + /** Used as the size to enable large array optimizations. */ + var LARGE_ARRAY_SIZE = 200; + + /** Used to stand-in for `undefined` hash values. */ + var HASH_UNDEFINED = '__lodash_hash_undefined__'; + + /** Used as references for various `Number` constants. */ + var MAX_SAFE_INTEGER = 9007199254740991; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + promiseTag = '[object Promise]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + symbolTag = '[object Symbol]', + weakMapTag = '[object WeakMap]'; + + var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + + /** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ + var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + + /** Used to match `RegExp` flags from their coerced string values. */ + var reFlags = /\w*$/; + + /** Used to detect host constructors (Safari). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^(?:0|[1-9]\d*)$/; + + /** Used to identify `toStringTag` values supported by `_.clone`. */ + var cloneableTags = {}; + cloneableTags[argsTag] = cloneableTags[arrayTag] = + cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = + cloneableTags[boolTag] = cloneableTags[dateTag] = + cloneableTags[float32Tag] = cloneableTags[float64Tag] = + cloneableTags[int8Tag] = cloneableTags[int16Tag] = + cloneableTags[int32Tag] = cloneableTags[mapTag] = + cloneableTags[numberTag] = cloneableTags[objectTag] = + cloneableTags[regexpTag] = cloneableTags[setTag] = + cloneableTags[stringTag] = cloneableTags[symbolTag] = + cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = + cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; + cloneableTags[errorTag] = cloneableTags[funcTag] = + cloneableTags[weakMapTag] = false; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal && commonjsGlobal.Object === Object && commonjsGlobal; + + /** Detect free variable `self`. */ + var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + + /** Used as a reference to the global object. */ + var root = freeGlobal || freeSelf || Function('return this')(); + + /** Detect free variable `exports`. */ + var freeExports = exports && !exports.nodeType && exports; + + /** Detect free variable `module`. */ + var freeModule = freeExports && 'object' == 'object' && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports`. */ + var moduleExports = freeModule && freeModule.exports === freeExports; + + /** + * Adds the key-value `pair` to `map`. + * + * @private + * @param {Object} map The map to modify. + * @param {Array} pair The key-value pair to add. + * @returns {Object} Returns `map`. + */ + function addMapEntry(map, pair) { + // Don't return `map.set` because it's not chainable in IE 11. + map.set(pair[0], pair[1]); + return map; + } + + /** + * Adds `value` to `set`. + * + * @private + * @param {Object} set The set to modify. + * @param {*} value The value to add. + * @returns {Object} Returns `set`. + */ + function addSetEntry(set, value) { + // Don't return `set.add` because it's not chainable in IE 11. + set.add(value); + return set; + } + + /** + * A specialized version of `_.forEach` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array ? array.length : 0; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ + function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; + } + + /** + * A specialized version of `_.reduce` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initAccum] Specify using the first element of `array` as + * the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initAccum) { + var index = -1, + length = array ? array.length : 0; + + if (initAccum && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ + function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; + } + + /** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ + function getValue(object, key) { + return object == null ? undefined : object[key]; + } + + /** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ + function isHostObject(value) { + // Many host objects are `Object` objects that can coerce to strings + // despite having improperly defined `toString` methods. + var result = false; + if (value != null && typeof value.toString != 'function') { + try { + result = !!(value + ''); + } catch (e) {} + } + return result; + } + + /** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ + function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; + } + + /** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ + function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; + } + + /** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ + function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; + } + + /** Used for built-in method references. */ + var arrayProto = Array.prototype, + funcProto = Function.prototype, + objectProto = Object.prototype; + + /** Used to detect overreaching core-js shims. */ + var coreJsData = root['__core-js_shared__']; + + /** Used to detect methods masquerading as native. */ + var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; + }()); + + /** Used to resolve the decompiled source of functions. */ + var funcToString = funcProto.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty = objectProto.hasOwnProperty; + + /** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ + var objectToString = objectProto.toString; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Built-in value references. */ + var Buffer = moduleExports ? root.Buffer : undefined, + Symbol = root.Symbol, + Uint8Array = root.Uint8Array, + getPrototype = overArg(Object.getPrototypeOf, Object), + objectCreate = Object.create, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + splice = arrayProto.splice; + + /* Built-in method references for those with the same name as other `lodash` methods. */ + var nativeGetSymbols = Object.getOwnPropertySymbols, + nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, + nativeKeys = overArg(Object.keys, Object); + + /* Built-in method references that are verified to be native. */ + var DataView = getNative(root, 'DataView'), + Map = getNative(root, 'Map'), + Promise = getNative(root, 'Promise'), + Set = getNative(root, 'Set'), + WeakMap = getNative(root, 'WeakMap'), + nativeCreate = getNative(Object, 'create'); + + /** Used to detect maps, sets, and weakmaps. */ + var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map), + promiseCtorString = toSource(Promise), + setCtorString = toSource(Set), + weakMapCtorString = toSource(WeakMap); + + /** Used to convert symbols to primitives and strings. */ + var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto ? symbolProto.valueOf : undefined; + + /** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Hash(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ + function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + } + + /** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function hashDelete(key) { + return this.has(key) && delete this.__data__[key]; + } + + /** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty.call(data, key) ? data[key] : undefined; + } + + /** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function hashHas(key) { + var data = this.__data__; + return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key); + } + + /** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ + function hashSet(key, value) { + var data = this.__data__; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; + return this; + } + + // Add methods to `Hash`. + Hash.prototype.clear = hashClear; + Hash.prototype['delete'] = hashDelete; + Hash.prototype.get = hashGet; + Hash.prototype.has = hashHas; + Hash.prototype.set = hashSet; + + /** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function ListCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ + function listCacheClear() { + this.__data__ = []; + } + + /** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + return true; + } + + /** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; + } + + /** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; + } + + /** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ + function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; + } + + // Add methods to `ListCache`. + ListCache.prototype.clear = listCacheClear; + ListCache.prototype['delete'] = listCacheDelete; + ListCache.prototype.get = listCacheGet; + ListCache.prototype.has = listCacheHas; + ListCache.prototype.set = listCacheSet; + + /** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function MapCache(entries) { + var index = -1, + length = entries ? entries.length : 0; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } + } + + /** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ + function mapCacheClear() { + this.__data__ = { + 'hash': new Hash, + 'map': new (Map || ListCache), + 'string': new Hash + }; + } + + /** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function mapCacheDelete(key) { + return getMapData(this, key)['delete'](key); + } + + /** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function mapCacheGet(key) { + return getMapData(this, key).get(key); + } + + /** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function mapCacheHas(key) { + return getMapData(this, key).has(key); + } + + /** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ + function mapCacheSet(key, value) { + getMapData(this, key).set(key, value); + return this; + } + + // Add methods to `MapCache`. + MapCache.prototype.clear = mapCacheClear; + MapCache.prototype['delete'] = mapCacheDelete; + MapCache.prototype.get = mapCacheGet; + MapCache.prototype.has = mapCacheHas; + MapCache.prototype.set = mapCacheSet; + + /** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ + function Stack(entries) { + this.__data__ = new ListCache(entries); + } + + /** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ + function stackClear() { + this.__data__ = new ListCache; + } + + /** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ + function stackDelete(key) { + return this.__data__['delete'](key); + } + + /** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ + function stackGet(key) { + return this.__data__.get(key); + } + + /** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function stackHas(key) { + return this.__data__.has(key); + } + + /** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ + function stackSet(key, value) { + var cache = this.__data__; + if (cache instanceof ListCache) { + var pairs = cache.__data__; + if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + return this; + } + cache = this.__data__ = new MapCache(pairs); + } + cache.set(key, value); + return this; + } + + // Add methods to `Stack`. + Stack.prototype.clear = stackClear; + Stack.prototype['delete'] = stackDelete; + Stack.prototype.get = stackGet; + Stack.prototype.has = stackHas; + Stack.prototype.set = stackSet; + + /** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ + function arrayLikeKeys(value, inherited) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + // Safari 9 makes `arguments.length` enumerable in strict mode. + var result = (isArray(value) || isArguments(value)) + ? baseTimes(value.length, String) + : []; + + var length = result.length, + skipIndexes = !!length; + + for (var key in value) { + if ((inherited || hasOwnProperty.call(value, key)) && + !(skipIndexes && (key == 'length' || isIndex(key, length)))) { + result.push(key); + } + } + return result; + } + + /** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ + function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + object[key] = value; + } + } + + /** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; + } + + /** + * The base implementation of `_.assign` without support for multiple sources + * or `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssign(object, source) { + return object && copyObject(source, keys(source), object); + } + + /** + * The base implementation of `_.clone` and `_.cloneDeep` which tracks + * traversed objects. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @param {boolean} [isFull] Specify a clone including symbols. + * @param {Function} [customizer] The function to customize cloning. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The parent object of `value`. + * @param {Object} [stack] Tracks traversed objects and their clone counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, isDeep, isFull, customizer, key, object, stack) { + var result; + if (customizer) { + result = object ? customizer(value, key, object, stack) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return copyArray(value, result); + } + } else { + var tag = getTag(value), + isFunc = tag == funcTag || tag == genTag; + + if (isBuffer(value)) { + return cloneBuffer(value, isDeep); + } + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + if (isHostObject(value)) { + return object ? value : {}; + } + result = initCloneObject(isFunc ? {} : value); + if (!isDeep) { + return copySymbols(value, baseAssign(result, value)); + } + } else { + if (!cloneableTags[tag]) { + return object ? value : {}; + } + result = initCloneByTag(value, tag, baseClone, isDeep); + } + } + // Check for circular references and return its corresponding clone. + stack || (stack = new Stack); + var stacked = stack.get(value); + if (stacked) { + return stacked; + } + stack.set(value, result); + + if (!isArr) { + var props = isFull ? getAllKeys(value) : keys(value); + } + arrayEach(props || value, function(subValue, key) { + if (props) { + key = subValue; + subValue = value[key]; + } + // Recursively populate clone (susceptible to call stack limits). + assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack)); + }); + return result; + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ + function baseCreate(proto) { + return isObject(proto) ? objectCreate(proto) : {}; + } + + /** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ + function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); + } + + /** + * The base implementation of `getTag`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + function baseGetTag(value) { + return objectToString.call(value); + } + + /** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ + function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); + } + + /** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; + } + + /** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ + function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var result = new buffer.constructor(buffer.length); + buffer.copy(result); + return result; + } + + /** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ + function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; + } + + /** + * Creates a clone of `dataView`. + * + * @private + * @param {Object} dataView The data view to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned data view. + */ + function cloneDataView(dataView, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; + return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); + } + + /** + * Creates a clone of `map`. + * + * @private + * @param {Object} map The map to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned map. + */ + function cloneMap(map, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map); + return arrayReduce(array, addMapEntry, new map.constructor); + } + + /** + * Creates a clone of `regexp`. + * + * @private + * @param {Object} regexp The regexp to clone. + * @returns {Object} Returns the cloned regexp. + */ + function cloneRegExp(regexp) { + var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); + result.lastIndex = regexp.lastIndex; + return result; + } + + /** + * Creates a clone of `set`. + * + * @private + * @param {Object} set The set to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned set. + */ + function cloneSet(set, isDeep, cloneFunc) { + var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set); + return arrayReduce(array, addSetEntry, new set.constructor); + } + + /** + * Creates a clone of the `symbol` object. + * + * @private + * @param {Object} symbol The symbol object to clone. + * @returns {Object} Returns the cloned symbol object. + */ + function cloneSymbol(symbol) { + return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; + } + + /** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ + function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ + function copyObject(source, props, object, customizer) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + assignValue(object, key, newValue === undefined ? source[key] : newValue); + } + return object; + } + + /** + * Copies own symbol properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy symbols from. + * @param {Object} [object={}] The object to copy symbols to. + * @returns {Object} Returns `object`. + */ + function copySymbols(source, object) { + return copyObject(source, getSymbols(source), object); + } + + /** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ + function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); + } + + /** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ + function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; + } + + /** + * Creates an array of the own enumerable symbol properties of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ + var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray; + + /** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ + var getTag = baseGetTag; + + // Fallback for data views, maps, sets, and weak maps in IE 11, + // for data views in Edge < 14, and promises in Node.js. + if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || + (Map && getTag(new Map) != mapTag) || + (Promise && getTag(Promise.resolve()) != promiseTag) || + (Set && getTag(new Set) != setTag) || + (WeakMap && getTag(new WeakMap) != weakMapTag)) { + getTag = function(value) { + var result = objectToString.call(value), + Ctor = result == objectTag ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : undefined; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag; + case mapCtorString: return mapTag; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag; + case weakMapCtorString: return weakMapTag; + } + } + return result; + }; + } + + /** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ + function initCloneArray(array) { + var length = array.length, + result = array.constructor(length); + + // Add properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; + } + + /** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; + } + + /** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {Function} cloneFunc The function to clone values. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneByTag(object, tag, cloneFunc, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return cloneArrayBuffer(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case dataViewTag: + return cloneDataView(object, isDeep); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + return cloneTypedArray(object, isDeep); + + case mapTag: + return cloneMap(object, isDeep, cloneFunc); + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + return cloneRegExp(object); + + case setTag: + return cloneSet(object, isDeep, cloneFunc); + + case symbolTag: + return cloneSymbol(object); + } + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + length = length == null ? MAX_SAFE_INTEGER : length; + return !!length && + (typeof value == 'number' || reIsUint.test(value)) && + (value > -1 && value % 1 == 0 && value < length); + } + + /** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ + function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); + } + + /** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ + function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); + } + + /** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ + function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; + + return value === proto; + } + + /** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to process. + * @returns {string} Returns the source code. + */ + function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; + } + + /** + * This method is like `_.clone` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @since 1.0.0 + * @category Lang + * @param {*} value The value to recursively clone. + * @returns {*} Returns the deep cloned value. + * @see _.clone + * @example + * + * var objects = [{ 'a': 1 }, { 'b': 2 }]; + * + * var deep = _.cloneDeep(objects); + * console.log(deep[0] === objects[0]); + * // => false + */ + function cloneDeep(value) { + return baseClone(value, true, true); + } + + /** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ + function eq(value, other) { + return value === other || (value !== value && other !== other); + } + + /** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + // Safari 8.1 makes `arguments.callee` enumerable in strict mode. + return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && + (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ + var isArray = Array.isArray; + + /** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ + function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); + } + + /** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ + function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); + } + + /** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ + var isBuffer = nativeIsBuffer || stubFalse; + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 8-9 which returns 'object' for typed array and other constructors. + var tag = isObject(value) ? objectToString.call(value) : ''; + return tag == funcTag || tag == genTag; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ + function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ + function isObject(value) { + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ + function isObjectLike(value) { + return !!value && typeof value == 'object'; + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); + } + + /** + * This method returns a new empty array. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {Array} Returns the new empty array. + * @example + * + * var arrays = _.times(2, _.stubArray); + * + * console.log(arrays); + * // => [[], []] + * + * console.log(arrays[0] === arrays[1]); + * // => false + */ + function stubArray() { + return []; + } + + /** + * This method returns `false`. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {boolean} Returns `false`. + * @example + * + * _.times(2, _.stubFalse); + * // => [false, false] + */ + function stubFalse() { + return false; + } + + module.exports = cloneDeep; + }); + + var hasProperty = function has(object, key) { + return Object.prototype.hasOwnProperty.call(object, key); + }; + + var _apply; + + function _classPrivateFieldLooseBase$8(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; } + + var id$8 = 0; + + function _classPrivateFieldLooseKey$8(name) { return "__private_" + id$8++ + "_" + name; } + + + + function insertReplacement(source, rx, replacement) { + const newParts = []; + source.forEach(chunk => { + // When the source contains multiple placeholders for interpolation, + // we should ignore chunks that are not strings, because those + // can be JSX objects and will be otherwise incorrectly turned into strings. + // Without this condition we’d get this: [object Object] hello [object Object] my