这是一封测试邮件,请忽略
"; + $template = [ + 'code' => $code + ]; + $config = EmailService::getConfig(); + $model = SystemMail::create([ + 'gateway' => Arr::getConfigValue($config,'Host'), + 'from' => Arr::getConfigValue($config,'From'), + 'email' => $email, + 'code' => $code, + ]); + try { + $result = EmailService::sendByTemplate($email, $subject, $content, $template); + if (!empty($result)) { + $model->status = 'failure'; + $model->response = $result; + $model->save(); + return $this->fail('发送失败,请查看日志'); + } else { + $model->status = 'success'; + $model->save(); + return $this->success([], '发送成功'); + } + } catch (\Exception $e) { + $model->status = 'failure'; + $model->response = $e->getMessage(); + $model->save(); + return $this->fail($e->getMessage()); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemDeptController.php b/server/plugin/saiadmin/app/controller/system/SystemDeptController.php new file mode 100644 index 0000000..38c18b0 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemDeptController.php @@ -0,0 +1,134 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\validate\system\SystemDeptValidate; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemDeptLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 部门控制器 + */ +class SystemDeptController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemDeptLogic(); + $this->validate = new SystemDeptValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('部门数据列表', 'core:dept:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $data = $this->logic->tree($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据读取', 'core:dept:read')] + public function read(Request $request) : Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据添加', 'core:dept:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据修改','core:dept:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('部门数据删除','core:dept:destroy')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 可操作部门 + * @param Request $request + * @return Response + */ + public function accessDept(Request $request) : Response + { + $where = ['status' => 1]; + $data = $this->logic->accessDept($where); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemDictDataController.php b/server/plugin/saiadmin/app/controller/system/SystemDictDataController.php new file mode 100644 index 0000000..fa72e6c --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemDictDataController.php @@ -0,0 +1,112 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemDictDataLogic; +use plugin\saiadmin\app\validate\system\SystemDictDataValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 字典数据控制器 + */ +class SystemDictDataController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemDictDataLogic(); + $this->validate = new SystemDictDataValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('数据字典列表', 'core:dict:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['label', ''], + ['value', ''], + ['type_id', ''], + ['status', ''], + ]); + $this->logic->setOrderField('sort'); + $this->logic->setOrderType('desc'); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + DictCache::clear(); + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + DictCache::clear(); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + DictCache::clear(); + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemDictTypeController.php b/server/plugin/saiadmin/app/controller/system/SystemDictTypeController.php new file mode 100644 index 0000000..f8fecf4 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemDictTypeController.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemDictTypeLogic; +use plugin\saiadmin\app\validate\system\SystemDictTypeValidate; +use plugin\saiadmin\service\Permission; +use support\Cache; +use support\Request; +use support\Response; + +/** + * 字典类型控制器 + */ +class SystemDictTypeController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemDictTypeLogic(); + $this->validate = new SystemDictTypeValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('数据字典列表', 'core:dict:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + DictCache::clear(); + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + DictCache::clear(); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('数据字典管理', 'core:dict:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + DictCache::clear(); + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemLogController.php b/server/plugin/saiadmin/app/controller/system/SystemLogController.php new file mode 100644 index 0000000..7878486 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemLogController.php @@ -0,0 +1,100 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemLoginLogLogic; +use plugin\saiadmin\app\logic\system\SystemOperLogLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 日志控制器 + */ +class SystemLogController extends BaseController +{ + + /** + * 登录日志列表 + * @param Request $request + * @return Response + */ + #[Permission('登录日志列表', 'core:logs:login')] + public function getLoginLogPageList(Request $request) : Response + { + $where = $request->more([ + ['login_time', ''], + ['username', ''], + ['status', ''], + ['ip', ''], + ]); + $logic = new SystemLoginLogLogic(); + $query = $logic->search($where); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 删除登录日志 + * @param Request $request + * @return Response + */ + #[Permission('登录日志删除', 'core:logs:deleteLogin')] + public function deleteLoginLog(Request $request) : Response + { + $ids = $request->input('ids', ''); + $logic = new SystemLoginLogLogic(); + if (!empty($ids)) { + $logic->destroy($ids); + return $this->success('删除成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + + /** + * 操作日志列表 + * @param Request $request + * @return Response + */ + #[Permission('操作日志列表', 'core:logs:Oper')] + public function getOperLogPageList(Request $request) : Response + { + $where = $request->more([ + ['create_time', ''], + ['username', ''], + ['service_name', ''], + ['router', ''], + ['ip', ''], + ]); + $logic = new SystemOperLogLogic(); + $logic->init($this->adminInfo); + $query = $logic->search($where); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 删除操作日志 + * @param Request $request + * @return Response + */ + #[Permission('操作日志删除', 'core:logs:deleteOper')] + public function deleteOperLog(Request $request) : Response + { + $ids = $request->input('ids', ''); + $logic = new SystemOperLogLogic(); + if (!empty($ids)) { + $logic->destroy($ids); + return $this->success('删除成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemMailController.php b/server/plugin/saiadmin/app/controller/system/SystemMailController.php new file mode 100644 index 0000000..7dcb52d --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemMailController.php @@ -0,0 +1,72 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\service\Permission; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemMailLogic; +use plugin\saiadmin\app\validate\system\SystemMailValidate; +use support\Request; +use support\Response; + +/** + * 邮件记录控制器 + */ +class SystemMailController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemMailLogic(); + $this->validate = new SystemMailValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('邮件日志列表', 'core:email:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['gateway', ''], + ['from', ''], + ['code', ''], + ['email', ''], + ['status', ''], + ['create_time', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('邮件日志删除', 'core:email:destroy')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemMenuController.php b/server/plugin/saiadmin/app/controller/system/SystemMenuController.php new file mode 100644 index 0000000..5126d2c --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemMenuController.php @@ -0,0 +1,143 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemMenuLogic; +use plugin\saiadmin\app\validate\system\SystemMenuValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 菜单控制器 + */ +class SystemMenuController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemMenuLogic(); + $this->validate = new SystemMenuValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据列表', 'core:menu:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['path', ''], + ['menu', ''], + ['status', ''], + ]); + $data = $this->logic->tree($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据读取', 'core:menu:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据添加', 'core:menu:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + UserMenuCache::clearMenuCache(); + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据修改', 'core:menu:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + UserMenuCache::clearMenuCache(); + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('菜单数据删除', 'core:menu:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + UserMenuCache::clearMenuCache(); + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 可操作菜单 + * @param Request $request + * @return Response + */ + public function accessMenu(Request $request): Response + { + $where = []; + if ($this->adminId > 1) { + $data = $this->logic->auth(); + } else { + $data = $this->logic->tree($where); + } + return $this->success($data); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemPostController.php b/server/plugin/saiadmin/app/controller/system/SystemPostController.php new file mode 100644 index 0000000..08759ff --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemPostController.php @@ -0,0 +1,177 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\system\SystemPostLogic; +use plugin\saiadmin\app\validate\system\SystemPostValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 岗位信息控制器 + */ +class SystemPostController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemPostLogic(); + $this->validate = new SystemPostValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据列表', 'core:post:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据读取', 'core:post:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据添加', 'core:post:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据修改', 'core:post:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据删除', 'core:post:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 导入数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据导入', 'core:post:import')] + public function import(Request $request): Response + { + $file = current($request->file()); + if (!$file || !$file->isValid()) { + return $this->fail('未找到上传文件'); + } + $this->logic->import($file); + return $this->success('导入成功'); + } + + /** + * 导出数据 + * @param Request $request + * @return Response + */ + #[Permission('岗位数据导出', 'core:post:export')] + public function export(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + return $this->logic->export($where); + } + + /** + * 下载导入模板 + * @return Response + */ + public function downloadTemplate(): Response + { + $file_name = "template.xlsx"; + return downloadFile($file_name); + } + + /** + * 可操作岗位 + * @param Request $request + * @return Response + */ + public function accessPost(Request $request): Response + { + $where = ['status' => 1]; + $data = $this->logic->accessPost($where); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemRoleController.php b/server/plugin/saiadmin/app/controller/system/SystemRoleController.php new file mode 100644 index 0000000..47d07e5 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemRoleController.php @@ -0,0 +1,168 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\model\system\SystemUserRole; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\model\system\SystemUser; +use plugin\saiadmin\app\validate\system\SystemRoleValidate; +use plugin\saiadmin\app\logic\system\SystemRoleLogic; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 角色控制器 + */ +class SystemRoleController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemRoleLogic(); + $this->validate = new SystemRoleValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('角色数据列表', 'core:role:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['name', ''], + ['code', ''], + ['status', ''], + ]); + $query = $this->logic->search($where); + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + $query->where('level', '<', $maxLevel); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据读取', 'core:role:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据添加', 'core:role:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据修改', 'core:role:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('角色数据删除', 'core:role:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 根据角色获取菜单 + * @param Request $request + * @return Response + */ + #[Permission('角色数据列表', 'core:role:index')] + public function getMenuByRole(Request $request): Response + { + $id = $request->get('id'); + $data = $this->logic->getMenuByRole($id); + return $this->success($data); + } + + /** + * 菜单权限 + * @param Request $request + * @return Response + */ + #[Permission('角色菜单权限', 'core:role:menu')] + public function menuPermission(Request $request): Response + { + $id = $request->post('id'); + $menu_ids = $request->post('menu_ids'); + $this->logic->saveMenuPermission($id, $menu_ids); + return $this->success('操作成功'); + } + + /** + * 可操作角色 + * @param Request $request + * @return Response + */ + public function accessRole(Request $request): Response + { + $where = ['status' => 1]; + $data = $this->logic->accessRole($where); + return $this->success($data); + } + +} diff --git a/server/plugin/saiadmin/app/controller/system/SystemServerController.php b/server/plugin/saiadmin/app/controller/system/SystemServerController.php new file mode 100644 index 0000000..1304f63 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemServerController.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\service\Permission; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\utils\ServerMonitor; +use support\think\Cache; +use support\Request; +use support\Response; + +/** + * 邮件记录控制器 + */ +class SystemServerController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('服务监控', 'core:server:monitor')] + public function monitor(Request $request): Response + { + $service = new ServerMonitor(); + return $this->success([ + 'memory' => $service->getMemoryInfo(), + 'disk' => $service->getDiskInfo(), + 'phpEnv' => $service->getPhpAndEnvInfo(), + ]); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('缓存信息', 'core:server:cache')] + public function cache(Request $request): Response + { + $menu_cache = config('plugin.saiadmin.saithink.menu_cache', []); + $button_cache = config('plugin.saiadmin.saithink.button_cache', []); + $config_cache = config('plugin.saiadmin.saithink.config_cache', []); + $dict_cache = config('plugin.saiadmin.saithink.dict_cache', []); + $reflection_cache = config('plugin.saiadmin.saithink.reflection_cache', []); + + return $this->success([ + 'menu_cache' => $menu_cache, + 'button_cache' => $button_cache, + 'config_cache' => $config_cache, + 'dict_cache' => $dict_cache, + 'reflection_cache' => $reflection_cache + ]); + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('缓存数据清理', 'core:server:clear')] + public function clear(Request $request) : Response + { + $tag = $request->input('tag', ''); + if (empty($tag)) { + return $this->fail('请选择要删除的缓存'); + } + Cache::tag($tag)->clear(); + Cache::delete($tag); + return $this->success('删除成功'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/system/SystemUserController.php b/server/plugin/saiadmin/app/controller/system/SystemUserController.php new file mode 100644 index 0000000..c169769 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/system/SystemUserController.php @@ -0,0 +1,210 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\system; + +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\logic\system\SystemUserLogic; +use plugin\saiadmin\app\validate\system\SystemUserValidate; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 用户信息控制器 + */ +class SystemUserController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new SystemUserLogic(); + $this->validate = new SystemUserValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('用户数据列表', 'core:user:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['username', ''], + ['phone', ''], + ['email', ''], + ['status', ''], + ['dept_id', ''], + ['create_time', ''], + ]); + $data = $this->logic->indexList($where); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据读取', 'core:user:read')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据保存', 'core:user:save')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据更新', 'core:user:update')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('用户数据删除', 'core:user:destroy')] + public function destroy(Request $request): Response + { + $ids = $request->input('ids', ''); + if (!empty($ids)) { + $this->logic->destroy($ids); + return $this->success('操作成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } + + /** + * 清理用户缓存 + * @param Request $request + * @return Response + */ + #[Permission('清理用户缓存', 'core:user:cache')] + public function clearCache(Request $request): Response + { + $id = $request->post('id', ''); + UserInfoCache::clearUserInfo($id); + UserAuthCache::clearUserAuth($id); + UserMenuCache::clearUserMenu($id); + return $this->success('操作成功'); + } + + /** + * 修改用户密码 + * @param Request $request + * @return Response + */ + #[Permission('修改用户密码', 'core:user:password')] + public function initUserPassword(Request $request): Response + { + $id = $request->post('id', ''); + $password = $request->post('password', ''); + if ($id == 1) { + return $this->fail('超级管理员不允许重置密码'); + } + $data = ['password' => password_hash($password, PASSWORD_DEFAULT)]; + $this->logic->authEdit($id, $data); + UserInfoCache::clearUserInfo($id); + return $this->success('操作成功'); + } + + /** + * 设置用户首页 + * @param Request $request + * @return Response + */ + #[Permission('设置用户首页', 'core:user:home')] + public function setHomePage(Request $request): Response + { + $id = $request->post('id', ''); + $dashboard = $request->post('dashboard', ''); + $data = ['dashboard' => $dashboard]; + $this->logic->authEdit($id, $data); + UserInfoCache::clearUserInfo($id); + return $this->success('操作成功'); + } + + /** + * 更新资料 + * @param Request $request + * @return Response + */ + #[Permission('用户修改资料')] + public function updateInfo(Request $request): Response + { + $data = $request->post(); + unset($data['deptList']); + unset($data['postList']); + unset($data['roleList']); + $result = $this->logic->updateInfo($this->adminId, $data); + if ($result) { + UserInfoCache::clearUserInfo($this->adminId); + return $this->success('操作成功'); + } else { + return $this->fail('操作失败'); + } + } + + /** + * 修改密码 + * @param Request $request + * @return Response + */ + #[Permission('用户修改密码')] + public function modifyPassword(Request $request): Response + { + $oldPassword = $request->input('oldPassword'); + $newPassword = $request->input('newPassword'); + $this->logic->modifyPassword($this->adminId, $oldPassword, $newPassword); + UserInfoCache::clearUserInfo($this->adminId); + return $this->success('修改成功'); + } +} diff --git a/server/plugin/saiadmin/app/controller/tool/CrontabController.php b/server/plugin/saiadmin/app/controller/tool/CrontabController.php new file mode 100644 index 0000000..3b1e032 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/tool/CrontabController.php @@ -0,0 +1,181 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\tool; + +use plugin\saiadmin\app\logic\tool\CrontabLogic; +use plugin\saiadmin\app\logic\tool\CrontabLogLogic; +use plugin\saiadmin\app\validate\tool\CrontabValidate; +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\service\Permission; +use Webman\Channel\Client; +use support\Request; +use support\Response; + +/** + * 定时任务控制器 + */ +class CrontabController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new CrontabLogic(); + $this->validate = new CrontabValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('定时任务列表', 'tool:crontab:index')] + public function index(Request $request) : Response + { + $where = $request->more([ + ['name', ''], + ['type', ''], + ['status', ''], + ['create_time', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 保存数据 + * @param Request $request + * @return Response + */ + #[Permission('定时任务添加', 'tool:crontab:edit')] + public function save(Request $request): Response + { + $data = $request->post(); + $this->validate('save', $data); + $result = $this->logic->add($data); + if ($result) { + return $this->success('添加成功'); + } else { + return $this->fail('添加失败'); + } + } + + /** + * 更新数据 + * @param Request $request + * @return Response + */ + #[Permission('定时任务修改', 'tool:crontab:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('定时任务删除', 'tool:crontab:edit')] + public function destroy(Request $request) : Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 修改状态 + * @param Request $request + * @return Response + */ + #[Permission('定时任务状态修改', 'tool:crontab:edit')] + public function changeStatus(Request $request) : Response + { + $id = $request->input('id', ''); + $status = $request->input('status', 1); + if (empty($id)) { + return $this->fail('参数错误,请检查'); + } + $result = $this->logic->changeStatus($id, $status); + if ($result) { + return $this->success('操作成功'); + } else { + return $this->fail('操作失败'); + } + } + + /** + * 执行定时任务 + * @param Request $request + * @return Response + */ + #[Permission('定时任务执行', 'tool:crontab:run')] + public function run(Request $request) : Response + { + $id = $request->input('id', ''); + $result = $this->logic->run($id); + if ($result) { + return $this->success('执行成功'); + } else { + return $this->fail('执行失败'); + } + } + + /** + * 定时任务日志 + * @param Request $request + * @return Response + */ + #[Permission('定时任务日志', 'tool:crontab:index')] + public function logPageList(Request $request) : Response + { + $where = $request->more([ + ['crontab_id', ''], + ['create_time', []] + ]); + $logic = new CrontabLogLogic(); + $query = $logic->search($where); + $data = $logic->getList($query); + return $this->success($data); + } + + /** + * 定时任务日志删除 + * @param Request $request + * @return Response + */ + #[Permission('定时任务日志删除', 'tool:crontab:edit')] + public function deleteCrontabLog(Request $request) : Response + { + $ids = $request->input('ids', ''); + if (!empty($ids)) { + $logic = new CrontabLogLogic(); + $logic->destroy($ids); + return $this->success('操作成功'); + } else { + return $this->fail('参数错误,请检查'); + } + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/controller/tool/GenerateTablesController.php b/server/plugin/saiadmin/app/controller/tool/GenerateTablesController.php new file mode 100644 index 0000000..8765993 --- /dev/null +++ b/server/plugin/saiadmin/app/controller/tool/GenerateTablesController.php @@ -0,0 +1,178 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\controller\tool; + +use plugin\saiadmin\basic\BaseController; +use plugin\saiadmin\app\logic\tool\GenerateTablesLogic; +use plugin\saiadmin\app\validate\tool\GenerateTablesValidate; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\service\Permission; +use support\Request; +use support\Response; + +/** + * 代码生成控制器 + */ +class GenerateTablesController extends BaseController +{ + /** + * 构造 + */ + public function __construct() + { + $this->logic = new GenerateTablesLogic(); + $this->validate = new GenerateTablesValidate; + parent::__construct(); + } + + /** + * 数据列表 + * @param Request $request + * @return Response + */ + #[Permission('代码生成列表', 'tool:code:index')] + public function index(Request $request): Response + { + $where = $request->more([ + ['table_name', ''], + ]); + $query = $this->logic->search($where); + $data = $this->logic->getList($query); + return $this->success($data); + } + + /** + * 读取数据 + * @param Request $request + * @return Response + */ + #[Permission('代码生成列表', 'tool:code:index')] + public function read(Request $request): Response + { + $id = $request->input('id', ''); + $model = $this->logic->read($id); + if ($model) { + $data = is_array($model) ? $model : $model->toArray(); + return $this->success($data); + } else { + return $this->fail('未查找到信息'); + } + } + + /** + * 修改数据 + * @param Request $request + * @return Response + */ + #[Permission('代码生成修改', 'tool:code:edit')] + public function update(Request $request): Response + { + $data = $request->post(); + $this->validate('update', $data); + $result = $this->logic->edit($data['id'], $data); + if ($result) { + return $this->success('修改成功'); + } else { + return $this->fail('修改失败'); + } + } + + /** + * 删除数据 + * @param Request $request + * @return Response + */ + #[Permission('代码生成删除', 'tool:code:edit')] + public function destroy(Request $request): Response + { + $ids = $request->post('ids', ''); + if (empty($ids)) { + return $this->fail('请选择要删除的数据'); + } + $result = $this->logic->destroy($ids); + if ($result) { + return $this->success('删除成功'); + } else { + return $this->fail('删除失败'); + } + } + + /** + * 装载数据表 + * @param Request $request + * @return Response + */ + #[Permission('代码生成装载', 'tool:code:edit')] + public function loadTable(Request $request): Response + { + $names = $request->input('names', []); + $source = $request->input('source', ''); + $this->logic->loadTable($names, $source); + return $this->success('操作成功'); + } + + /** + * 同步数据表字段信息 + * @param Request $request + * @return Response + */ + #[Permission('代码生成同步表结构', 'tool:code:edit')] + public function sync(Request $request): Response + { + $id = $request->input('id', ''); + $this->logic->sync($id); + return $this->success('操作成功'); + } + + /** + * 代码预览 + */ + #[Permission('代码生成预览', 'tool:code:edit')] + public function preview(Request $request): Response + { + $id = $request->input('id', ''); + $data = $this->logic->preview($id); + return $this->success($data); + } + + /** + * 代码生成 + */ + #[Permission('代码生成文件', 'tool:code:edit')] + public function generate(Request $request): Response + { + $ids = $request->input('ids', ''); + $data = $this->logic->generate($ids); + return response()->download($data['download'], $data['filename']); + } + + /** + * 生成到模块 + */ + #[Permission('代码生成到模块', 'tool:code:edit')] + public function generateFile(Request $request): Response + { + $id = $request->input('id', ''); + $this->logic->generateFile($id); + UserMenuCache::clearMenuCache(); + return $this->success('操作成功'); + } + + /** + * 获取数据表字段信息 + * @param Request $request + * @return Response + */ + #[Permission('代码生成读取表字段', 'tool:code:index')] + public function getTableColumns(Request $request): Response + { + $table_id = $request->input('table_id', ''); + $data = $this->logic->getTableColumns($table_id); + return $this->success($data); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/event/SystemUser.php b/server/plugin/saiadmin/app/event/SystemUser.php new file mode 100644 index 0000000..f85ef3f --- /dev/null +++ b/server/plugin/saiadmin/app/event/SystemUser.php @@ -0,0 +1,162 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\event; + +use plugin\saiadmin\app\cache\ReflectionCache; +use plugin\saiadmin\app\model\system\SystemLoginLog; +use plugin\saiadmin\app\model\system\SystemOperLog; + +class SystemUser +{ + /** + * 登录日志 + * @param $item + */ + public function login($item) + { + $request = request(); + $ip = $request ? $request->getRealIp() : '127.0.0.1'; + $http_user_agent = $request ? $request->header('user-agent') : ''; + $data['username'] = $item['username']; + $data['ip'] = $ip; + $data['ip_location'] = self::getIpLocation($ip); + $data['os'] = self::getOs($http_user_agent); + $data['browser'] = self::getBrowser($http_user_agent); + $data['status'] = $item['status']; + $data['message'] = $item['message']; + $data['login_time'] = date('Y-m-d H:i:s'); + if (isset($item['admin_id'])) { + $data['created_by'] = $item['admin_id']; + $data['updated_by'] = $item['admin_id']; + } + SystemLoginLog::create($data); + } + + /** + * 记录操作日志 + */ + public function operateLog(): bool + { + $request = request(); + if (!$request) { + return false; + } + if ($request->method() === 'GET') { + return false; + } + $info = getCurrentInfo(); + $ip = $request->getRealIp(); + $module = $request->plugin; + $rule = trim($request->uri()); + $data['username'] = $info['username']; + $data['method'] = $request->method(); + $data['router'] = $rule; + $data['service_name'] = self::getServiceName(); + $data['app'] = $module; + $data['ip'] = $ip; + $data['ip_location'] = self::getIpLocation($ip); + $data['request_data'] = $this->filterParams($request->all()); + SystemOperLog::create($data); + return true; + } + + /** + * 获取业务名称 + */ + protected function getServiceName(): string + { + $request = request(); + if (!$request) { + return '未命名业务'; + } + $permissions = ReflectionCache::getPermissionAttributes($request->controller, $request->action); + if (!empty($permissions)) { + return $permissions['title'] ?? '未命名业务'; + } else { + return '未命名业务'; + } + } + + /** + * 过滤字段 + */ + protected function filterParams($params): string + { + $blackList = ['password', 'oldPassword', 'newPassword']; + foreach ($params as $key => $value) { + if (in_array($key, $blackList)) { + $params[$key] = '******'; + } + } + return json_encode($params, JSON_UNESCAPED_UNICODE); + } + + /** + * 获取IP地理位置 + */ + protected function getIpLocation($ip): string + { + $ip2region = new \Ip2Region(); + try { + $region = $ip2region->memorySearch($ip); + } catch (\Exception $e) { + return '未知'; + } + list($country, $province, $city, $network) = explode('|', $region['region']); + if ($network === '内网IP') { + return $network; + } + if ($country == '中国') { + return $province . '-' . $city . ':' . $network; + } else if ($country == '0') { + return '未知'; + } else { + return $country; + } + } + + /** + * 获取浏览器信息 + */ + protected function getBrowser($user_agent): string + { + $br = 'Unknown'; + if (preg_match('/MSIE/i', $user_agent)) { + $br = 'MSIE'; + } elseif (preg_match('/Firefox/i', $user_agent)) { + $br = 'Firefox'; + } elseif (preg_match('/Chrome/i', $user_agent)) { + $br = 'Chrome'; + } elseif (preg_match('/Safari/i', $user_agent)) { + $br = 'Safari'; + } elseif (preg_match('/Opera/i', $user_agent)) { + $br = 'Opera'; + } else { + $br = 'Other'; + } + return $br; + } + + /** + * 获取操作系统信息 + */ + protected function getOs($user_agent): string + { + $os = 'Unknown'; + if (preg_match('/win/i', $user_agent)) { + $os = 'Win'; + } elseif (preg_match('/mac/i', $user_agent)) { + $os = 'Mac'; + } elseif (preg_match('/linux/i', $user_agent)) { + $os = 'Linux'; + } else { + $os = 'Other'; + } + return $os; + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/exception/Handler.php b/server/plugin/saiadmin/app/exception/Handler.php new file mode 100644 index 0000000..59ca1ed --- /dev/null +++ b/server/plugin/saiadmin/app/exception/Handler.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\exception; + +use Throwable; +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\Exception\ExceptionHandler; +use plugin\saiadmin\exception\ApiException; + +/** + * 异常处理类 + */ +class Handler extends ExceptionHandler +{ + public $dontReport = [ + ApiException::class, + ]; + + public function report(Throwable $exception) + { + if ($this->shouldntReport($exception)) { + return; + } + $logs = ''; + if ($request = \request()) { + $user = getCurrentInfo(); + $logs .= $request->method() . ' ' . $request->uri(); + $logs .= PHP_EOL . '[request_param]: ' . json_encode($request->all()); + $logs .= PHP_EOL . '[timestamp]: ' . date('Y-m-d H:i:s'); + $logs .= PHP_EOL . '[client_ip]: ' . $request->getRealIp(); + $logs .= PHP_EOL . '[action_user]: ' . var_export($user, true); + $logs .= PHP_EOL . '[exception_handle]: ' . get_class($exception); + $logs .= PHP_EOL . '[exception_info]: ' . PHP_EOL . $exception; + } + $this->logger->error($logs); + } + + public function render(Request $request, Throwable $exception): Response + { + $debug = config('app.debug', true); + $code = $exception->getCode(); + $json = [ + 'code' => $code ? $code : 500, + 'message' => $code !== 500 ? $exception->getMessage() : 'Server internal error', + 'type' => 'failed' + ]; + if ($debug) { + $json['request_url'] = $request->method() . ' ' . $request->uri(); + $json['timestamp'] = date('Y-m-d H:i:s'); + $json['client_ip'] = $request->getRealIp(); + $json['request_param'] = $request->all(); + $json['exception_handle'] = get_class($exception); + $json['exception_info'] = [ + 'code' => $exception->getCode(), + 'message' => $exception->getMessage(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => explode("\n", $exception->getTraceAsString()) + ]; + } + return new Response(200, ['Content-Type' => 'application/json;charset=utf-8'], json_encode($json)); + } +} diff --git a/server/plugin/saiadmin/app/functions.php b/server/plugin/saiadmin/app/functions.php new file mode 100644 index 0000000..2340248 --- /dev/null +++ b/server/plugin/saiadmin/app/functions.php @@ -0,0 +1,114 @@ + +// +---------------------------------------------------------------------- +use Webman\Route; +use support\Response; +use Tinywan\Jwt\JwtToken; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\app\cache\DictCache; + +if (!function_exists('getCurrentInfo')) { + /** + * 获取当前登录用户 + */ + function getCurrentInfo(): bool|array + { + if (!request()) { + return false; + } + try { + $token = JwtToken::getExtend(); + } catch (\Throwable $e) { + return false; + } + return $token; + } +} + +if (!function_exists('fastRoute')) { + /** + * 快速注册路由[index|save|update|read|destroy|import|export] + * @param string $name + * @param string $controller + * @return void + */ + function fastRoute(string $name, string $controller): void + { + $name = trim($name, '/'); + if (method_exists($controller, 'index')) + Route::get("/$name/index", [$controller, 'index']); + if (method_exists($controller, 'save')) + Route::post("/$name/save", [$controller, 'save']); + if (method_exists($controller, 'update')) + Route::put("/$name/update", [$controller, 'update']); + if (method_exists($controller, 'read')) + Route::get("/$name/read", [$controller, 'read']); + if (method_exists($controller, 'destroy')) + Route::delete("/$name/destroy", [$controller, 'destroy']); + if (method_exists($controller, 'import')) + Route::post("/$name/import", [$controller, 'import']); + if (method_exists($controller, 'export')) + Route::post("/$name/export", [$controller, 'export']); + } +} + +if (!function_exists('downloadFile')) { + /** + * 下载模板 + * @param $file_name + * @return Response + */ + function downloadFile($file_name): Response + { + $base_dir = config('plugin.saiadmin.saithink.template', base_path() . '/public/template'); + if (file_exists($base_dir . DIRECTORY_SEPARATOR . $file_name)) { + return response()->download($base_dir . DIRECTORY_SEPARATOR . $file_name, urlencode($file_name)); + } else { + throw new ApiException('模板不存在'); + } + } +} + +if (!function_exists('formatBytes')) { + /** + * 根据字节计算大小 + * @param $bytes + * @return string + */ + function formatBytes($bytes): string + { + $units = ['B', 'KB', 'MB', 'GB', 'TB']; + for ($i = 0; $bytes > 1024; $i++) { + $bytes /= 1024; + } + return round($bytes, 2) . ' ' . $units[$i]; + } +} + +if (!function_exists('getConfigGroup')) { + /** + * 读取配置组 + * @param $group + * @return array + */ + function getConfigGroup($group): array + { + return ConfigCache::getConfig($group); + } +} + +if (!function_exists('dictDataList')) { + /** + * 根据字典编码获取字典列表 + * @param string $code 字典编码 + * @return array + */ + function dictDataList(string $code): array + { + return DictCache::getDict($code); + } +} diff --git a/server/plugin/saiadmin/app/logic/system/DatabaseLogic.php b/server/plugin/saiadmin/app/logic/system/DatabaseLogic.php new file mode 100644 index 0000000..0570726 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/DatabaseLogic.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use support\think\Db; + +/** + * 数据表维护逻辑层 + */ +class DatabaseLogic extends BaseLogic +{ + /** + * 获取数据源 + * @return array + */ + public function getDbSource(): array + { + $data = config('think-orm.connections'); + $list = []; + foreach ($data as $k => $v) { + $list[] = $k; + } + return $list; + } + + /** + * 数据列表 + * @param $query + * @return mixed + */ + public function getList($query): mixed + { + $request = request(); + $page = $request ? ($request->input('page') ?: 1) : 1; + $limit = $request ? ($request->input('limit') ?: 10) : 10; + + return self::getTableList($query, $page, $limit); + } + + /** + * 获取数据库表数据 + */ + public function getTableList($query, $current_page = 1, $per_page = 10): array + { + if (!empty($query['source'])) { + if (!empty($query['name'])) { + $sql = 'show table status where name=:name '; + $list = Db::connect($query['source'])->query($sql, ['name' => $query['name']]); + } else { + $list = Db::connect($query['source'])->query('show table status'); + } + } else { + if (!empty($query['name'])) { + $sql = 'show table status where name=:name '; + $list = Db::query($sql, ['name' => $query['name']]); + } else { + $list = Db::query('show table status'); + } + } + + $data = []; + foreach ($list as $item) { + $data[] = [ + 'name' => $item['Name'], + 'engine' => $item['Engine'], + 'rows' => $item['Rows'], + 'data_free' => $item['Data_free'], + 'data_length' => $item['Data_length'], + 'index_length' => $item['Index_length'], + 'collation' => $item['Collation'], + 'create_time' => $item['Create_time'], + 'update_time' => $item['Update_time'], + 'comment' => $item['Comment'], + ]; + } + $total = count($data); + $last_page = ceil($total / $per_page); + $startIndex = ($current_page - 1) * $per_page; + $pageData = array_slice($data, $startIndex, $per_page); + return [ + 'data' => $pageData, + 'total' => $total, + 'current_page' => $current_page, + 'per_page' => $per_page, + 'last_page' => $last_page, + ]; + } + + /** + * 获取列信息 + */ + public function getColumnList($table, $source): array + { + $columnList = []; + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + if (!empty($source)) { + $list = Db::connect($source)->query('SHOW FULL COLUMNS FROM `' . $table . '`'); + } else { + $list = Db::query('SHOW FULL COLUMNS FROM `' . $table . '`'); + } + foreach ($list as $column) { + preg_match('/^\w+/', $column['Type'], $matches); + $columnList[] = [ + 'column_key' => $column['Key'], + 'column_name' => $column['Field'], + 'column_type' => $matches[0], + 'column_comment' => trim(preg_replace("/\([^()]*\)/", "", $column['Comment'])), + 'extra' => $column['Extra'], + 'default_value' => $column['Default'], + 'is_nullable' => $column['Null'], + ]; + } + } + return $columnList; + } + + /** + * 优化表 + */ + public function optimizeTable($tables) + { + foreach ($tables as $table) { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + Db::execute('OPTIMIZE TABLE `' . $table . '`'); + } + } + } + + /** + * 清理表碎片 + */ + public function fragmentTable($tables) + { + foreach ($tables as $table) { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + Db::execute('ANALYZE TABLE `' . $table . '`'); + } + } + } + + /** + * 获取回收站数据 + */ + public function recycleData($table) + { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + // 查询表字段 + $sql = 'SHOW COLUMNS FROM `' . $table . '` where Field = "delete_time"'; + $columns = Db::query($sql); + $isDeleteTime = false; + if (count($columns) > 0) { + $isDeleteTime = true; + } + if (!$isDeleteTime) { + throw new ApiException('当前表不支持回收站功能'); + } + // 查询软删除数据 + $request = request(); + $limit = $request ? ($request->input('limit') ?: 10) : 10; + return Db::table($table)->whereNotNull('delete_time') + ->order('delete_time', 'desc') + ->paginate($limit) + ->toArray(); + } else { + return []; + } + } + + /** + * 删除数据 + * @param $table + * @param $ids + * @return bool + */ + public function delete($table, $ids) + { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + $count = Db::table($table)->whereIn('id', $ids)->delete($ids); + return $count > 0; + } else { + return false; + } + } + + /** + * 恢复数据 + * @param $table + * @param $ids + * @return bool + */ + public function recovery($table, $ids) + { + if (preg_match("/^[a-zA-Z0-9_]+$/", $table)) { + $count = Db::table($table) + ->where('id', 'in', $ids) + ->update(['delete_time' => null]); + return $count > 0; + } else { + return false; + } + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemAttachmentLogic.php b/server/plugin/saiadmin/app/logic/system/SystemAttachmentLogic.php new file mode 100644 index 0000000..d7f63ea --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemAttachmentLogic.php @@ -0,0 +1,199 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use Exception; +use plugin\saiadmin\app\model\system\SystemAttachment; +use plugin\saiadmin\app\model\system\SystemCategory; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\service\storage\ChunkUploadService; +use plugin\saiadmin\service\storage\UploadService; +use plugin\saiadmin\utils\Arr; +use plugin\saiadmin\utils\Helper; + +/** + * 附件逻辑层 + */ +class SystemAttachmentLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemAttachment(); + } + + /** + * @param $category_id + * @param $ids + * @return mixed + */ + public function move($category_id, $ids): mixed + { + $category = SystemCategory::where('id', $category_id)->findOrEmpty(); + if ($category->isEmpty()) { + throw new ApiException('目标分类不存在'); + } + return $this->model->whereIn('id', $ids)->update(['category_id' => $category_id]); + } + + /** + * 保存网络图片 + * @param $url + * @param $config + * @return array + * @throws ApiException|Exception + */ + public function saveNetworkImage($url, $config): array + { + $image_data = file_get_contents($url); + if ($image_data === false) { + throw new ApiException('获取文件资源失败'); + } + $image_resource = imagecreatefromstring($image_data); + if (!$image_resource) { + throw new ApiException('创建图片资源失败'); + } + $filename = basename($url); + $file_extension = pathinfo($filename, PATHINFO_EXTENSION); + $full_dir = runtime_path() . '/resource/'; + if (!is_dir($full_dir)) { + mkdir($full_dir, 0777, true); + } + $save_path = $full_dir . $filename; + $mime_type = 'image/'; + switch ($file_extension) { + case 'jpg': + case 'jpeg': + $mime_type = 'image/jpeg'; + $result = imagejpeg($image_resource, $save_path); + break; + case 'png': + $mime_type = 'image/png'; + $result = imagepng($image_resource, $save_path); + break; + case 'gif': + $mime_type = 'image/gif'; + $result = imagegif($image_resource, $save_path); + break; + default: + imagedestroy($image_resource); + throw new ApiException('文件格式错误'); + } + imagedestroy($image_resource); + if (!$result) { + throw new ApiException('文件保存失败'); + } + + $hash = md5_file($save_path); + $size = filesize($save_path); + + $model = $this->model->where('hash', $hash)->find(); + if ($model) { + unlink($save_path); + return $model->toArray(); + } else { + + $logic = new SystemConfigLogic(); + $uploadConfig = $logic->getGroup('upload_config'); + + $root = Arr::getConfigValue($uploadConfig, 'local_root'); + + $folder = date('Ymd'); + $full_dir = base_path() . DIRECTORY_SEPARATOR . $root . $folder . DIRECTORY_SEPARATOR; + if (!is_dir($full_dir)) { + mkdir($full_dir, 0777, true); + } + $object_name = bin2hex(pack('Nn', time(), random_int(1, 65535))) . ".$file_extension"; + $newPath = $full_dir . $object_name; + + copy($save_path, $newPath); + unlink($save_path); + $domain = Arr::getConfigValue($uploadConfig, 'local_domain'); + $uri = Arr::getConfigValue($uploadConfig, 'local_uri'); + $baseUrl = $domain . $uri . $folder . '/'; + + $info['storage_mode'] = 1; + $info['category_id'] = request()->input('category_id', 1); + $info['origin_name'] = $filename; + $info['object_name'] = $object_name; + $info['hash'] = $hash; + $info['mime_type'] = $mime_type; + $info['storage_path'] = $root . $folder . '/' . $object_name; + $info['suffix'] = $file_extension; + $info['size_byte'] = $size; + $info['size_info'] = formatBytes($size); + $info['url'] = $baseUrl . $object_name; + $this->model->save($info); + return $info; + } + } + + /** + * 文件上传 + * @param string $upload + * @param bool $local + * @return array + */ + public function uploadBase(string $upload = 'image', bool $local = false): array + { + $logic = new SystemConfigLogic(); + $uploadConfig = $logic->getGroup('upload_config'); + $type = Arr::getConfigValue($uploadConfig, 'upload_mode'); + if ($local === true) { + $type = 1; + } + $result = UploadService::disk($type, $upload)->uploadFile(); + $data = $result[0]; + $hash = $data['unique_id']; + $hash_check = config('plugin.saiadmin.saithink.file_hash', false); + if ($hash_check) { + $model = $this->model->where('hash', $hash)->findOrEmpty(); + if (!$model->isEmpty()) { + return $model->toArray(); + } + } + $url = str_replace('\\', '/', $data['url']); + $savePath = str_replace('\\', '/', $data['save_path']); + $info['storage_mode'] = $type; + $info['category_id'] = request()->input('category_id', 1); + $info['origin_name'] = $data['origin_name']; + $info['object_name'] = $data['save_name']; + $info['hash'] = $data['unique_id']; + $info['mime_type'] = $data['mime_type']; + $info['storage_path'] = $savePath; + $info['suffix'] = $data['extension']; + $info['size_byte'] = $data['size']; + $info['size_info'] = formatBytes($data['size']); + $info['url'] = $url; + $this->model->save($info); + return $info; + } + + /** + * 切片上传 + * @param $data + * @return array + */ + public function chunkUpload($data): array + { + $chunkService = new ChunkUploadService(); + if ($data['index'] == 0) { + $model = $this->model->where('hash', $data['hash'])->findOrEmpty(); + if (!$model->isEmpty()) { + return $model->toArray(); + } else { + return $chunkService->checkChunk($data); + } + } else { + return $chunkService->uploadChunk($data); + } + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemCategoryLogic.php b/server/plugin/saiadmin/app/logic/system/SystemCategoryLogic.php new file mode 100644 index 0000000..16a7010 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemCategoryLogic.php @@ -0,0 +1,101 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemCategory; +use plugin\saiadmin\utils\Helper; +use plugin\saiadmin\utils\Arr; + +/** + * 附件分类逻辑层 + */ +class SystemCategoryLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemCategory(); + } + + /** + * 添加数据 + */ + public function add($data): bool + { + $data = $this->handleData($data); + return $this->model->save($data); + } + + /** + * 修改数据 + */ + public function edit($id, $data): bool + { + $data = $this->handleData($data); + if ($data['parent_id'] == $id) { + throw new ApiException('上级分类和当前分类不能相同'); + } + if (in_array($id, explode(',', $data['level']))) { + throw new ApiException('不能将上级分类设置为当前分类的子分类'); + } + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + return $model->save($data); + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + $num = $this->model->where('parent_id', 'in', $ids)->count(); + if ($num > 0) { + throw new ApiException('该部门下存在子分类,请先删除子分类'); + } else { + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + if (empty($data['parent_id']) || $data['parent_id'] == 0) { + $data['level'] = '0'; + $data['parent_id'] = 0; + } else { + $parentMenu = SystemCategory::findOrEmpty($data['parent_id']); + $data['level'] = $parentMenu['level'] . $parentMenu['id'] . ','; + } + return $data; + } + + /** + * 数据树形化 + * @param array $where + * @return array + */ + public function tree(array $where = []): array + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { + $query->field('id, id as value, category_name as label, parent_id, category_name, sort'); + } + $query->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemConfigGroupLogic.php b/server/plugin/saiadmin/app/logic/system/SystemConfigGroupLogic.php new file mode 100644 index 0000000..455d266 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemConfigGroupLogic.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\app\model\system\SystemConfigGroup; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemConfig; +use support\think\Db; + +/** + * 参数配置分组逻辑层 + */ +class SystemConfigGroupLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemConfigGroup(); + } + + /** + * 删除配置信息 + */ + public function destroy($ids): bool + { + $id = $ids[0]; + $model = $this->model->where('id', $id)->findOrEmpty(); + if ($model->isEmpty()) { + throw new ApiException('配置数据未找到'); + } + if (in_array(intval($id), [1, 2, 3])) { + throw new ApiException('系统默认分组,无法删除'); + } + Db::startTrans(); + try { + // 删除配置组 + $model->delete(); + // 删除配置组数据 + $typeIds = SystemConfig::where('group_id', $id)->column('id'); + SystemConfig::destroy($typeIds); + ConfigCache::clearConfig($model->code); + Db::commit(); + return true; + } catch (\Exception $e) { + Db::rollback(); + throw new ApiException('删除数据异常,请检查'); + } + } +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemConfigLogic.php b/server/plugin/saiadmin/app/logic/system/SystemConfigLogic.php new file mode 100644 index 0000000..75e96c4 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemConfigLogic.php @@ -0,0 +1,107 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\ConfigCache; +use plugin\saiadmin\app\model\system\SystemConfig; +use plugin\saiadmin\app\model\system\SystemConfigGroup; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Helper; + +/** + * 参数配置逻辑层 + */ +class SystemConfigLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemConfig(); + } + + /** + * 添加数据 + * @param mixed $data + * @return mixed + */ + public function add($data): mixed + { + $result = $this->model->create($data); + $group = SystemConfigGroup::find($data['group_id']); + ConfigCache::clearConfig($group->code); + return $result; + } + + /** + * 编辑数据 + * @param mixed $id + * @param mixed $data + * @return bool + */ + public function edit($id, $data): bool + { + $result = parent::edit($id, $data); + $group = SystemConfigGroup::find($data['group_id']); + ConfigCache::clearConfig($group->code); + return $result; + } + + /** + * 批量更新 + * @param mixed $group_id + * @param mixed $config + * @return bool + */ + public function batchUpdate($group_id, $config): bool + { + $group = SystemConfigGroup::find($group_id); + if (!$group) { + throw new ApiException('配置组未找到'); + } + $saveData = []; + foreach ($config as $key => $value) { + $saveData[] = [ + 'id' => $value['id'], + 'group_id' => $group_id, + 'name' => $value['name'], + 'key' => $value['key'], + 'value' => $value['value'] + ]; + } + // upsert: 根据 id 更新,如果不存在则插入 + $this->model->saveAll($saveData); + ConfigCache::clearConfig($group->code); + return true; + } + + /** + * 获取配置数据 + * @param mixed $code + * @return array + */ + public function getData($code): array + { + $group = SystemConfigGroup::where('code', $code)->findOrEmpty(); + if (empty($group)) { + return []; + } + $config = SystemConfig::where('group_id', $group['id'])->select()->toArray(); + return $config; + } + + /** + * 获取配置组 + */ + public function getGroup($config): array + { + return ConfigCache::getConfig($config); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemDeptLogic.php b/server/plugin/saiadmin/app/logic/system/SystemDeptLogic.php new file mode 100644 index 0000000..933fa3a --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemDeptLogic.php @@ -0,0 +1,127 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemDept; +use plugin\saiadmin\app\model\system\SystemUser; +use plugin\saiadmin\utils\Helper; +use plugin\saiadmin\utils\Arr; + +/** + * 部门逻辑层 + */ +class SystemDeptLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemDept(); + } + + /** + * 添加数据 + */ + public function add($data): mixed + { + $data = $this->handleData($data); + $this->model->save($data); + return $this->model->getKey(); + } + + /** + * 修改数据 + */ + public function edit($id, $data): mixed + { + $oldLevel = $data['level'] . $id . ','; + $data = $this->handleData($data); + if ($data['parent_id'] == $id) { + throw new ApiException('上级部门和当前部门不能相同'); + } + if (in_array($id, explode(',', $data['level']))) { + throw new ApiException('不能将上级部门设置为当前部门的子部门'); + } + $newLevel = $data['level'] . $id . ','; + $deptIds = $this->model->where('level', 'like', $oldLevel . '%')->column('id'); + + return $this->transaction(function () use ($deptIds, $oldLevel, $newLevel, $data, $id) { + $this->model->whereIn('id', $deptIds)->exp('level', "REPLACE(level, '$oldLevel', '$newLevel')")->update([]); + return $this->model->update($data, ['id' => $id]); + }); + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + $num = $this->model->where('parent_id', 'in', $ids)->count(); + if ($num > 0) { + throw new ApiException('该部门下存在子部门,请先删除子部门'); + } else { + $count = SystemUser::where('dept_id', 'in', $ids)->count(); + if ($count > 0) { + throw new ApiException('该部门下存在用户,请先删除或者转移用户'); + } + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + // 处理上级部门 + if (empty($data['parent_id']) || $data['parent_id'] == 0) { + $data['level'] = '0'; + $data['parent_id'] = 0; + } else { + $parentMenu = SystemDept::findOrEmpty($data['parent_id']); + $data['level'] = $parentMenu['level'] . $parentMenu['id'] . ','; + } + return $data; + } + + /** + * 数据树形化 + * @param array $where + * @return array + */ + public function tree(array $where = []): array + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { + $query->field('id, id as value, name as label, parent_id'); + } + $query->order('sort', 'desc'); + $query->with(['leader']); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + + /** + * 可操作部门 + * @param array $where + * @return array + */ + public function accessDept(array $where = []): array + { + $query = $this->search($where); + $query->auth($this->adminInfo['deptList']); + $query->field('id, id as value, name as label, parent_id'); + $query->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemDictDataLogic.php b/server/plugin/saiadmin/app/logic/system/SystemDictDataLogic.php new file mode 100644 index 0000000..a152841 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemDictDataLogic.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemDictData; +use plugin\saiadmin\app\model\system\SystemDictType; +use plugin\saiadmin\app\cache\DictCache; +use plugin\saiadmin\utils\Helper; + +/** + * 字典类型逻辑层 + */ +class SystemDictDataLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemDictData(); + } + + /** + * 添加数据 + * @param $data + * @return mixed + */ + public function add($data): mixed + { + $type = SystemDictType::where('id', $data['type_id'])->findOrEmpty(); + if ($type->isEmpty()) { + throw new ApiException('字典类型不存在'); + } + $data['code'] = $type->code; + $model = $this->model->create($data); + DictCache::clear(); + return $model->getKey(); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemDictTypeLogic.php b/server/plugin/saiadmin/app/logic/system/SystemDictTypeLogic.php new file mode 100644 index 0000000..c3c5065 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemDictTypeLogic.php @@ -0,0 +1,116 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\model\system\SystemDictType; +use plugin\saiadmin\app\model\system\SystemDictData; +use support\think\Db; + +/** + * 字典类型逻辑层 + */ +class SystemDictTypeLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemDictType(); + } + + /** + * 添加数据 + */ + public function add($data): mixed + { + $model = $this->model->where('code', $data['code'])->findOrEmpty(); + if (!$model->isEmpty()) { + throw new ApiException('该字典标识已存在'); + } + return $this->model->save($data); + } + + /** + * 数据更新 + */ + public function edit($id, $data): mixed + { + Db::startTrans(); + try { + // 修改数据字典类型 + $result = $this->model->update($data, ['id' => $id]); + // 更新数据字典数据 + SystemDictData::update(['code' => $data['code']], ['type_id' => $id]); + Db::commit(); + return $result; + } catch (\Exception $e) { + Db::rollback(); + throw new ApiException('修改数据异常,请检查'); + } + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + Db::startTrans(); + try { + // 删除数据字典类型 + $result = $this->model->destroy($ids); + // 删除数据字典数据 + $typeIds = SystemDictData::where('type_id', 'in', $ids)->column('id'); + SystemDictData::destroy($typeIds); + Db::commit(); + return $result; + } catch (\Exception $e) { + Db::rollback(); + throw new ApiException('删除数据异常,请检查'); + } + } + + /** + * 获取全部字典 + * @return array + */ + public function getDictAll(): array + { + $data = $this->model->where('status', 1)->field('id, name, code, remark') + ->with([ + 'dicts' => function ($query) { + $query->where('status', 1)->field('id, type_id, label, value, color, code, sort')->order('sort', 'desc'); + } + ])->select()->toArray(); + return $this->packageDict($data, 'code'); + } + + /** + * 组合数据 + * @param $array + * @param $field + * @return array + */ + private function packageDict($array, $field): array + { + $result = []; + foreach ($array as $item) { + if (isset($item[$field])) { + if (isset($result[$item[$field]])) { + $result[$item[$field]] = [($result[$item[$field]])]; + $result[$item[$field]][] = $item['dicts']; + } else { + $result[$item[$field]] = $item['dicts']; + } + } + } + return $result; + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemLoginLogLogic.php b/server/plugin/saiadmin/app/logic/system/SystemLoginLogLogic.php new file mode 100644 index 0000000..4f7b227 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemLoginLogLogic.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemLoginLog; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; +use support\think\Db; + +/** + * 登录日志逻辑层 + */ +class SystemLoginLogLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemLoginLog(); + } + + /** + * 登录统计图表 + * @return array + */ + public function loginChart(): array + { + $sql = " + SELECT + d.date AS login_date, + COUNT(l.login_time) AS login_count + FROM + (SELECT CURDATE() - INTERVAL (a.N) DAY AS date + FROM (SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 + UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 + UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a + ) d + LEFT JOIN sa_system_login_log l + ON DATE(l.login_time) = d.date + GROUP BY d.date + ORDER BY d.date ASC; + "; + $data = Db::query($sql); + return [ + 'login_count' => array_column($data, 'login_count'), + 'login_date' => array_column($data, 'login_date'), + ]; + } + + /** + * 登录统计图表 + * @return array + */ + public function loginBarChart(): array + { + $sql = " + SELECT + -- 拼接成 YYYY-MM 格式,例如 2023-01 + CONCAT(LPAD(m.month_num, 2, '0'), '月') AS login_month, + COUNT(l.login_time) AS login_count + FROM + -- 生成 1 到 12 的月份数字 + (SELECT 1 AS month_num UNION ALL SELECT 2 UNION ALL SELECT 3 + UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 + UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 + UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12) m + LEFT JOIN sa_system_login_log l + -- 关联条件:年份等于今年 且 月份等于生成的数字 + ON YEAR(l.login_time) = YEAR(CURDATE()) + AND MONTH(l.login_time) = m.month_num + GROUP BY + m.month_num + ORDER BY + m.month_num ASC; + "; + $data = Db::query($sql); + return [ + 'login_count' => array_column($data, 'login_count'), + 'login_month' => array_column($data, 'login_month'), + ]; + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemMailLogic.php b/server/plugin/saiadmin/app/logic/system/SystemMailLogic.php new file mode 100644 index 0000000..8eecc2d --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemMailLogic.php @@ -0,0 +1,26 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemMail; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; + +/** + * 邮件模型逻辑层 + */ +class SystemMailLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemMail(); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemMenuLogic.php b/server/plugin/saiadmin/app/logic/system/SystemMenuLogic.php new file mode 100644 index 0000000..a5e534d --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemMenuLogic.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemMenu; +use plugin\saiadmin\app\model\system\SystemRoleMenu; +use plugin\saiadmin\app\model\system\SystemUserRole; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Arr; +use plugin\saiadmin\utils\Helper; + +/** + * 菜单逻辑层 + */ +class SystemMenuLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemMenu(); + } + + /** + * 数据添加 + */ + public function add($data): mixed + { + $data = $this->handleData($data); + return $this->model->save($data); + } + + /** + * 数据修改 + */ + public function edit($id, $data): mixed + { + $data = $this->handleData($data); + if ($data['parent_id'] == $id) { + throw new ApiException('不能设置父级为自身'); + } + return $this->model->update($data, ['id' => $id]); + } + + /** + * 数据删除 + */ + public function destroy($ids): bool + { + $num = $this->model->where('parent_id', 'in', $ids)->count(); + if ($num > 0) { + throw new ApiException('该菜单下存在子菜单,请先删除子菜单'); + } else { + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + // 处理上级菜单 + if (empty($data['parent_id']) || $data['parent_id'] == 0) { + $data['level'] = '0'; + $data['parent_id'] = 0; + } else { + $parentMenu = $this->model->findOrEmpty($data['parent_id']); + $data['level'] = $parentMenu['level'] . $parentMenu['id'] . ','; + } + return $data; + } + + /** + * 数据树形化 + * @param $where + * @return array + */ + public function tree($where = []): array + { + $query = $this->search($where); + $request = request(); + if ($request && $request->input('tree', 'false') === 'true') { + $query->field('id, id as value, name as label, parent_id, type'); + } + $query->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + + /** + * 权限菜单 + * @return array + */ + public function auth(): array + { + $roleLogic = new SystemRoleLogic(); + $role_ids = Arr::getArrayColumn($this->adminInfo['roleList'], 'id'); + $roles = $roleLogic->getMenuIdsByRoleIds($role_ids); + $ids = $this->filterMenuIds($roles); + $query = $this->model + ->field('id, id as value, name as label, parent_id, type') + ->where('status', 1) + ->where('id', 'in', $ids) + ->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeTree($data); + } + + /** + * 获取全部菜单 + */ + public function getAllMenus(): array + { + $query = $this->search(['status' => 1, 'type' => [1, 2, 4]])->order('sort', 'desc'); + $data = $this->getAll($query); + return Helper::makeArtdMenus($data); + } + + /** + * 获取全部权限 + * @return array + */ + public function getAllAuth(): array + { + return SystemMenu::where('type', 3) + ->where('status', 1) + ->column('slug'); + } + + /** + * 根据角色获取权限 + * @param $roleIds + * @return array + */ + public function getAuthByRole($roleIds): array + { + $menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id'); + + return SystemMenu::distinct(true) + ->where('type', 3) + ->where('status', 1) + ->where('id', 'in', array_unique($menuId)) + ->column('slug'); + } + + /** + * 根据角色获取菜单 + * @param $roleIds + * @return array + */ + public function getMenuByRole($roleIds): array + { + $menuId = SystemRoleMenu::whereIn('role_id', $roleIds)->column('menu_id'); + + $data = SystemMenu::distinct(true) + ->where('status', 1) + ->where('type', 'in', [1, 2, 4]) + ->where('id', 'in', array_unique($menuId)) + ->order('sort', 'desc') + ->select() + ->toArray(); + return Helper::makeArtdMenus($data); + } + + /** + * 过滤通过角色查询出来的菜单id列表,并去重 + * @param array $roleData + * @return array + */ + public function filterMenuIds(array &$roleData): array + { + $ids = []; + foreach ($roleData as $val) { + foreach ($val['menus'] as $menu) { + $ids[] = $menu['id']; + } + } + unset($roleData); + return array_unique($ids); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/logic/system/SystemOperLogLogic.php b/server/plugin/saiadmin/app/logic/system/SystemOperLogLogic.php new file mode 100644 index 0000000..de56274 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemOperLogLogic.php @@ -0,0 +1,38 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemOperLog; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; + +/** + * 操作日志逻辑层 + */ +class SystemOperLogLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemOperLog(); + } + + /** + * 获取自己的操作日志 + * @param mixed $where + * @return array + */ + public function getOwnOperLogList($where): array + { + $query = $this->search($where); + $query->field('id, username, method, router, service_name, ip, ip_location, create_time'); + return $this->getList($query); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemPostLogic.php b/server/plugin/saiadmin/app/logic/system/SystemPostLogic.php new file mode 100644 index 0000000..8ca13a0 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemPostLogic.php @@ -0,0 +1,95 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\model\system\SystemPost; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\service\OpenSpoutWriter; +use OpenSpout\Reader\XLSX\Reader; + +/** + * 岗位管理逻辑层 + */ +class SystemPostLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemPost(); + } + + /** + * 可操作岗位 + * @param array $where + * @return array + */ + public function accessPost(array $where = []): array + { + $query = $this->search($where); + $query->field('id, id as value, name as label, name, code'); + return $this->getAll($query); + } + + /** + * 导入数据 + */ + public function import($file) + { + $path = $this->getImport($file); + $reader = new Reader(); + try { + $reader->open($path); + $data = []; + foreach ($reader->getSheetIterator() as $sheet) { + $isHeader = true; + foreach ($sheet->getRowIterator() as $row) { + if ($isHeader) { + $isHeader = false; + continue; + } + $cells = $row->getCells(); + $data[] = [ + 'name' => $cells[0]->getValue(), + 'code' => $cells[1]->getValue(), + 'sort' => $cells[2]->getValue(), + 'status' => $cells[3]->getValue(), + ]; + } + } + $this->saveAll($data); + } catch (\Exception $e) { + throw new ApiException('导入文件错误,请上传正确的文件格式xlsx'); + } + } + + /** + * 导出数据 + */ + public function export($where = []) + { + $query = $this->search($where)->field('id,name,code,sort,status,create_time'); + $data = $this->getAll($query); + $file_name = '岗位数据.xlsx'; + $header = ['编号', '岗位名称', '岗位标识', '排序', '状态', '创建时间']; + $filter = [ + 'status' => [ + ['value' => 1, 'label' => '正常'], + ['value' => 2, 'label' => '禁用'] + ] + ]; + $writer = new OpenSpoutWriter($file_name); + $writer->setWidth([15, 15, 20, 15, 15, 25]); + $writer->setHeader($header); + $writer->setData($data, null, $filter); + $file_path = $writer->returnFile(); + return response()->download($file_path, urlencode($file_name)); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemRoleLogic.php b/server/plugin/saiadmin/app/logic/system/SystemRoleLogic.php new file mode 100644 index 0000000..c9429a0 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemRoleLogic.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\model\system\SystemRole; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\utils\Helper; +use support\think\Cache; +use support\think\Db; + +/** + * 角色逻辑层 + */ +class SystemRoleLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemRole(); + } + + /** + * 添加数据 + */ + public function add($data): bool + { + $data = $this->handleData($data); + return $this->model->save($data); + } + + /** + * 修改数据 + */ + public function edit($id, $data): bool + { + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + $data = $this->handleData($data); + return $model->save($data); + } + + /** + * 删除数据 + */ + public function destroy($ids): bool + { + // 越权保护 + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + + $num = SystemRole::where('level', '>=', $maxLevel)->whereIn('id', $ids)->count(); + if ($num > 0) { + throw new ApiException('不能操作比当前账户职级高的角色'); + } else { + return $this->model->destroy($ids); + } + } + + /** + * 数据处理 + */ + protected function handleData($data) + { + // 越权保护 + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + if ($data['level'] >= $maxLevel) { + throw new ApiException('不能操作比当前账户职级高的角色'); + } + return $data; + } + + /** + * 可操作角色 + * @param array $where + * @return array + */ + public function accessRole(array $where = []): array + { + $query = $this->search($where); + // 越权保护 + $levelArr = array_column($this->adminInfo['roleList'], 'level'); + $maxLevel = max($levelArr); + $query->where('level', '<', $maxLevel); + $query->order('sort', 'desc'); + return $this->getAll($query); + } + + /** + * 根据角色数组获取菜单 + * @param $ids + * @return array + */ + public function getMenuIdsByRoleIds($ids): array + { + if (empty($ids)) + return []; + return $this->model->where('id', 'in', $ids)->with([ + 'menus' => function ($query) { + $query->where('status', 1)->order('sort', 'desc'); + } + ])->select()->toArray(); + + } + + /** + * 根据角色获取菜单 + * @param $id + * @return array + */ + public function getMenuByRole($id): array + { + $role = $this->model->findOrEmpty($id); + $menus = $role->menus ?: []; + return [ + 'id' => $id, + 'menus' => $menus + ]; + } + + /** + * 保存菜单权限 + * @param $id + * @param $menu_ids + * @return mixed + */ + public function saveMenuPermission($id, $menu_ids): mixed + { + return $this->transaction(function () use ($id, $menu_ids) { + $role = $this->model->findOrEmpty($id); + if ($role) { + $role->menus()->detach(); + $data = array_map(function ($menu_id) use ($id) { + return ['menu_id' => $menu_id, 'role_id' => $id]; + }, $menu_ids); + Db::name('sa_system_role_menu')->limit(100)->insertAll($data); + } + $cache = config('plugin.saiadmin.saithink.button_cache'); + $tag = $cache['role'] . $id; + Cache::tag($tag)->clear(); // 清理权限缓存-角色TAG + UserMenuCache::clearMenuCache(); // 清理菜单缓存 + return true; + }); + } + +} diff --git a/server/plugin/saiadmin/app/logic/system/SystemUserLogic.php b/server/plugin/saiadmin/app/logic/system/SystemUserLogic.php new file mode 100644 index 0000000..7188082 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/system/SystemUserLogic.php @@ -0,0 +1,336 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\system; + +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\UserInfoCache; +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\model\system\SystemDept; +use plugin\saiadmin\app\model\system\SystemRole; +use plugin\saiadmin\app\model\system\SystemUser; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\basic\think\BaseLogic; +use Webman\Event\Event; +use Tinywan\Jwt\JwtToken; + +/** + * 用户信息逻辑层 + */ +class SystemUserLogic extends BaseLogic +{ + + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new SystemUser(); + } + + /** + * 分页数据列表 + * @param mixed $where + * @return array + */ + public function indexList($where): array + { + $query = $this->search($where); + $query->with(['depts']); + $query->auth($this->adminInfo['deptList']); + return $this->getList($query); + } + + /** + * 用户列表数据 + * @param mixed $where + * @return array + */ + public function openUserList($where): array + { + $query = $this->search($where); + $query->field('id, username, realname, avatar, phone, email'); + return $this->getList($query); + } + + /** + * 读取用户信息 + * @param mixed $id + * @return array + */ + public function getUser($id): array + { + $admin = $this->model->findOrEmpty($id); + $data = $admin->hidden(['password'])->toArray(); + $data['roleList'] = $admin->roles->toArray() ?: []; + $data['postList'] = $admin->posts->toArray() ?: []; + $data['deptList'] = $admin->depts ? $admin->depts->toArray() : []; + return $data; + } + + /** + * 读取数据 + * @param $id + * @return array + */ + public function read($id): array + { + $data = $this->getUser($id); + if ($this->adminInfo['id'] > 1) { + // 部门保护 + if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) { + throw new ApiException('没有权限操作该部门数据'); + } + } + return $data; + } + + /** + * 添加数据 + * @param $data + * @return mixed + */ + public function add($data): mixed + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return $this->transaction(function () use ($data) { + $role_ids = $data['role_ids'] ?? []; + $post_ids = $data['post_ids'] ?? []; + if ($this->adminInfo['id'] > 1) { + // 部门保护 + if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) { + throw new ApiException('没有权限操作该部门数据'); + } + // 越权保护 + if (!$this->roleProtect($this->adminInfo['roleList'], $role_ids)) { + throw new ApiException('没有权限操作该角色数据'); + } + } + $user = SystemUser::create($data); + $user->roles()->detach(); + $user->posts()->detach(); + $user->roles()->saveAll($role_ids); + if (!empty($post_ids)) { + $user->posts()->save($post_ids); + } + return $user; + }); + } + + /** + * 修改数据 + * @param $id + * @param $data + * @return mixed + */ + public function edit($id, $data): mixed + { + unset($data['password']); + return $this->transaction(function () use ($data, $id) { + $role_ids = $data['role_ids'] ?? []; + $post_ids = $data['post_ids'] ?? []; + // 仅可修改当前部门和子部门的用户 + $query = $this->model->where('id', $id); + $query->auth($this->adminInfo['deptList']); + $user = $query->findOrEmpty(); + if ($user->isEmpty()) { + throw new ApiException('没有权限操作该数据'); + } + if ($this->adminInfo['id'] > 1) { + // 部门保护 + if (!$this->deptProtect($this->adminInfo['deptList'], $data['dept_id'])) { + throw new ApiException('没有权限操作该部门数据'); + } + // 越权保护 + if (!$this->roleProtect($this->adminInfo['roleList'], $role_ids)) { + throw new ApiException('没有权限操作该角色数据'); + } + } + $result = parent::edit($id, $data); + if ($result) { + $user->roles()->detach(); + $user->posts()->detach(); + $user->roles()->saveAll($role_ids); + if (!empty($post_ids)) { + $user->posts()->save($post_ids); + } + UserInfoCache::clearUserInfo($id); + UserAuthCache::clearUserAuth($id); + UserMenuCache::clearUserMenu($id); + } + return $result; + }); + } + + /** + * 删除数据 + * @param $ids + * @return bool + */ + public function destroy($ids): bool + { + if (is_array($ids)) { + if (count($ids) > 1) { + throw new ApiException('禁止批量删除操作'); + } + $ids = $ids[0]; + } + if ($ids == 1) { + throw new ApiException('超级管理员禁止删除'); + } + $query = $this->model->where('id', $ids); + $query->auth($this->adminInfo['deptList']); + $user = $query->findOrEmpty(); + if ($user->isEmpty()) { + throw new ApiException('没有权限操作该数据'); + } + if ($this->adminInfo['id'] > 1) { + $role_ids = $user->roles->toArray() ?: []; + if (!empty($role_ids)) { + // 越权保护 + if (!$this->roleProtect($this->adminInfo['roleList'], array_column($role_ids, 'id'))) { + throw new ApiException('没有权限操作该角色数据'); + } + } + } + UserInfoCache::clearUserInfo($ids); + UserAuthCache::clearUserAuth($ids); + UserMenuCache::clearUserMenu($ids); + return parent::destroy($ids); + } + + /** + * 用户登录 + * @param string $username + * @param string $password + * @param string $type + * @return array + */ + public function login(string $username, string $password, string $type): array + { + $adminInfo = $this->model->where('username', $username)->findOrEmpty(); + $status = 1; + $message = '登录成功'; + if ($adminInfo->isEmpty()) { + $message = '账号或密码错误,请重新输入!'; + throw new ApiException($message); + } + if ($adminInfo->status === 2) { + $status = 0; + $message = '您已被禁止登录!'; + } + if (!password_verify($password, $adminInfo->password)) { + $status = 0; + $message = '账号或密码错误,请重新输入!'; + } + if ($status === 0) { + // 登录事件 + Event::emit('user.login', compact('username', 'status', 'message')); + throw new ApiException($message); + } + $adminInfo->login_time = date('Y-m-d H:i:s'); + $adminInfo->login_ip = request()->getRealIp(); + $adminInfo->save(); + + $access_exp = config('plugin.saiadmin.saithink.access_exp', 3 * 3600); + $token = JwtToken::generateToken([ + 'access_exp' => $access_exp, + 'id' => $adminInfo->id, + 'username' => $adminInfo->username, + 'type' => $type, + 'plat' => 'saiadmin', + ]); + // 登录事件 + $admin_id = $adminInfo->id; + Event::emit('user.login', compact('username', 'status', 'message', 'admin_id')); + return $token; + } + + /** + * 更新资料 + * @param mixed $id + * @param mixed $data + * @return bool + */ + public function updateInfo($id, $data): bool + { + $this->model->update($data, ['id' => $id], ['realname', 'gender', 'phone', 'email', 'avatar', 'signed']); + return true; + } + + /** + * 密码修改 + * @param $adminId + * @param $oldPassword + * @param $newPassword + * @return bool + */ + public function modifyPassword($adminId, $oldPassword, $newPassword): bool + { + $model = $this->model->findOrEmpty($adminId); + if (password_verify($oldPassword, $model->password)) { + $model->password = password_hash($newPassword, PASSWORD_DEFAULT); + return $model->save(); + } else { + throw new ApiException('原密码错误'); + } + } + + /** + * 修改数据 + */ + public function authEdit($id, $data) + { + if ($this->adminInfo['id'] > 1) { + // 判断用户是否可以操作 + $query = SystemUser::where('id', $id); + $query->auth($this->adminInfo['deptList']); + $user = $query->findOrEmpty(); + if ($user->isEmpty()) { + throw new ApiException('没有权限操作该数据'); + } + } + parent::edit($id, $data); + } + + /** + * 部门保护 + * @param $dept + * @param $dept_id + * @return bool + */ + public function deptProtect($dept, $dept_id): bool + { + // 部门保护 + $deptIds = [$dept['id']]; + $deptLevel = $dept['level'] . $dept['id'] . ','; + $dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id'); + $deptIds = array_merge($deptIds, $dept_ids); + if (!in_array($dept_id, $deptIds)) { + return false; + } + return true; + } + + /** + * 越权保护 + * @param $roleList + * @param $role_ids + * @return bool + */ + public function roleProtect($roleList, $role_ids): bool + { + // 越权保护 + $levelArr = array_column($roleList, 'level'); + $maxLevel = max($levelArr); + $currentLevel = SystemRole::whereIn('id', $role_ids)->max('level'); + if ($currentLevel >= $maxLevel) { + return false; + } + return true; + } + +} diff --git a/server/plugin/saiadmin/app/logic/tool/CrontabLogLogic.php b/server/plugin/saiadmin/app/logic/tool/CrontabLogLogic.php new file mode 100644 index 0000000..cae9b10 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/CrontabLogLogic.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use plugin\saiadmin\app\model\tool\CrontabLog; +use plugin\saiadmin\basic\think\BaseLogic; + +/** + * 定时任务日志逻辑层 + */ +class CrontabLogLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new CrontabLog(); + } + +} diff --git a/server/plugin/saiadmin/app/logic/tool/CrontabLogic.php b/server/plugin/saiadmin/app/logic/tool/CrontabLogic.php new file mode 100644 index 0000000..e269866 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/CrontabLogic.php @@ -0,0 +1,247 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Webman\Channel\Client as ChannelClient; +use plugin\saiadmin\app\model\tool\Crontab; +use plugin\saiadmin\app\model\tool\CrontabLog; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\exception\ApiException; + +/** + * 定时任务逻辑层 + */ +class CrontabLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new Crontab(); + } + + /** + * 添加任务 + */ + public function add($data): bool + { + $second = $data['second']; + $minute = $data['minute']; + $hour = $data['hour']; + $week = $data['week']; + $day = $data['day']; + $month = $data['month']; + + // 规则处理 + $rule = match ($data['task_style']) { + 1 => "0 {$minute} {$hour} * * *", + 2 => "0 {$minute} * * * *", + 3 => "0 {$minute} */{$hour} * * *", + 4 => "0 */{$minute} * * * *", + 5 => "*/{$second} * * * * *", + 6 => "0 {$minute} {$hour} * * {$week}", + 7 => "0 {$minute} {$hour} {$day} * *", + 8 => "0 {$minute} {$hour} {$day} {$month} *", + default => throw new ApiException("任务类型异常"), + }; + + // 定时任务模型新增 + $model = Crontab::create([ + 'name' => $data['name'], + 'type' => $data['type'], + 'task_style' => $data['task_style'], + 'rule' => $rule, + 'target' => $data['target'], + 'parameter' => $data['parameter'], + 'status' => $data['status'], + 'remark' => $data['remark'], + ]); + + $id = $model->getKey(); + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $id]); + + return true; + } + + /** + * 修改任务 + */ + public function edit($id, $data): bool + { + $second = $data['second']; + $minute = $data['minute']; + $hour = $data['hour']; + $week = $data['week']; + $day = $data['day']; + $month = $data['month']; + + // 规则处理 + $rule = match ($data['task_style']) { + 1 => "0 {$minute} {$hour} * * *", + 2 => "0 {$minute} * * * *", + 3 => "0 {$minute} */{$hour} * * *", + 4 => "0 */{$minute} * * * *", + 5 => "*/{$second} * * * * *", + 6 => "0 {$minute} {$hour} * * {$week}", + 7 => "0 {$minute} {$hour} {$day} * *", + 8 => "0 {$minute} {$hour} {$day} {$month} *", + default => throw new ApiException("任务类型异常"), + }; + + // 查询任务数据 + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + + $result = $model->save([ + 'name' => $data['name'], + 'type' => $data['type'], + 'task_style' => $data['task_style'], + 'rule' => $rule, + 'target' => $data['target'], + 'parameter' => $data['parameter'], + 'status' => $data['status'], + 'remark' => $data['remark'], + ]); + if ($result) { + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $id]); + } + + // 修改任务数据 + return $result; + } + + /** + * 删除定时任务 + * @param $ids + * @return bool + * @throws Exception + */ + public function destroy($ids): bool + { + if (is_array($ids)) { + if (count($ids) > 1) { + throw new ApiException('禁止批量删除操作'); + } + $ids = $ids[0]; + } + $result = parent::destroy($ids); + if ($result) { + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $ids]); + } + return $result; + } + + /** + * 修改状态 + * @param $id + * @param $status + * @return bool + */ + public function changeStatus($id, $status): bool + { + $model = $this->model->findOrEmpty($id); + if ($model->isEmpty()) { + throw new ApiException('数据不存在'); + } + $result = $model->save(['status' => $status]); + if ($result) { + // 连接到Channel服务 + ChannelClient::connect(); + ChannelClient::publish('crontab', ['args' => $id]); + } + return $result; + } + + /** + * 执行定时任务 + * @param $id + * @return bool + */ + public function run($id): bool + { + $info = $this->model->where('status', 1)->findOrEmpty($id); + if ($info->isEmpty()) { + return false; + } + $data['crontab_id'] = $info->id; + $data['name'] = $info->name; + $data['target'] = $info->target; + $data['parameter'] = $info->parameter; + switch ($info->type) { + case 1: + // URL任务GET + $httpClient = new Client([ + 'timeout' => 5, + 'verify' => false, + ]); + try { + $httpClient->request('GET', $info->target); + $data['status'] = 1; + CrontabLog::create($data); + return true; + } catch (GuzzleException $e) { + $data['status'] = 2; + $data['exception_info'] = $e->getMessage(); + CrontabLog::create($data); + return false; + } + case 2: + // URL任务POST + $httpClient = new Client([ + 'timeout' => 5, + 'verify' => false, + ]); + try { + $res = $httpClient->request('POST', $info->target, [ + 'form_params' => json_decode($info->parameter ?? '', true) + ]); + $data['status'] = 1; + $data['exception_info'] = $res->getBody(); + CrontabLog::create($data); + return true; + } catch (GuzzleException $e) { + $data['status'] = 2; + $data['exception_info'] = $e->getMessage(); + CrontabLog::create($data); + return false; + } + case 3: + // 类任务 + $class_name = $info->target; + $method_name = 'run'; + $class = new $class_name; + if (method_exists($class, $method_name)) { + $return = $class->$method_name($info->parameter); + $data['status'] = 1; + $data['exception_info'] = $return; + CrontabLog::create($data); + return true; + } else { + $data['status'] = 2; + $data['exception_info'] = '类:' . $class_name . ',方法:run,未找到'; + CrontabLog::create($data); + return false; + + } + default: + return false; + } + } + +} diff --git a/server/plugin/saiadmin/app/logic/tool/GenerateColumnsLogic.php b/server/plugin/saiadmin/app/logic/tool/GenerateColumnsLogic.php new file mode 100644 index 0000000..a22e2bf --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/GenerateColumnsLogic.php @@ -0,0 +1,213 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use plugin\saiadmin\app\model\tool\GenerateColumns; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; + +/** + * 代码生成业务字段逻辑层 + */ +class GenerateColumnsLogic extends BaseLogic +{ + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new GenerateColumns(); + } + + public function saveExtra($data) + { + $default_column = ['create_time', 'update_time', 'created_by', 'updated_by', 'delete_time', 'remark']; + // 组装数据 + foreach ($data as $k => $item) { + + if ($item['column_name'] == 'delete_time') { + continue; + } + + $column = [ + 'table_id' => $item['table_id'], + 'column_name' => $item['column_name'], + 'column_comment' => $item['column_comment'], + 'column_type' => $item['column_type'], + 'default_value' => $item['default_value'], + 'is_pk' => ($item['column_key'] == 'PRI') ? 2 : 1, + 'is_required' => $item['is_nullable'] == 'NO' ? 2 : 1, + 'query_type' => 'eq', + 'view_type' => 'input', + 'sort' => count($data) - $k, + 'options' => $item['options'] ?? null + ]; + + // 设置默认选项 + if (!in_array($item['column_name'], $default_column) && empty($item['column_key'])) { + $column = array_merge( + $column, + [ + 'is_insert' => 2, + 'is_edit' => 2, + 'is_list' => 2, + 'is_query' => 1, + 'is_sort' => 1, + ] + ); + } + $keyList = [ + 'column_comment', + 'column_type', + 'default_value', + 'is_pk', + 'is_required', + 'is_insert', + 'is_edit', + 'is_list', + 'is_query', + 'is_sort', + 'query_type', + 'view_type', + 'dict_type', + 'options', + 'sort', + 'is_cover' + ]; + foreach ($keyList as $key) { + if (isset($item[$key])) + $column[$key] = $item[$key]; + } + GenerateColumns::create($this->fieldDispose($column)); + } + } + + public function update($data, $where) + { + $data['is_insert'] = $data['is_insert'] ? 2 : 1; + $data['is_edit'] = $data['is_edit'] ? 2 : 1; + $data['is_list'] = $data['is_list'] ? 2 : 1; + $data['is_query'] = $data['is_query'] ? 2 : 1; + $data['is_sort'] = $data['is_sort'] ? 2 : 1; + $data['is_required'] = $data['is_required'] ? 2 : 1; + $this->model->update($data, $where); + } + + private function fieldDispose(array $column): array + { + $object = new class { + public function viewTypeDispose(&$column): void + { + switch ($column['column_type']) { + case 'varchar': + $column['view_type'] = 'input'; + break; + // 富文本 + case 'text': + case 'longtext': + $column['is_list'] = 1; + $column['is_query'] = 1; + $column['view_type'] = 'editor'; + $options = [ + 'height' => 400, + ]; + $column['options'] = $options; + break; + // 日期字段 + case 'datetime': + $column['view_type'] = 'date'; + $options = [ + 'mode' => 'datetime' + ]; + $column['options'] = $options; + $column['query_type'] = 'between'; + break; + case 'date': + $column['view_type'] = 'date'; + $options = [ + 'mode' => 'date' + ]; + $column['options'] = $options; + $column['query_type'] = 'between'; + break; + } + } + + public function columnName(&$column): void + { + if (stristr($column['column_name'], 'name')) { + $column['is_query'] = 2; + $column['is_required'] = 2; + $column['query_type'] = 'like'; + } + + if (stristr($column['column_name'], 'title')) { + $column['is_query'] = 2; + $column['is_required'] = 2; + $column['query_type'] = 'like'; + } + + if (stristr($column['column_name'], 'type')) { + $column['is_query'] = 2; + $column['is_required'] = 2; + $column['query_type'] = 'eq'; + } + + if (stristr($column['column_name'], 'image')) { + $column['is_query'] = 1; + $column['view_type'] = 'uploadImage'; + $options = [ + 'multiple' => false, + 'limit' => 1, + ]; + $column['options'] = $options; + } + + if (stristr($column['column_name'], 'file')) { + $column['is_query'] = 1; + $column['view_type'] = 'uploadFile'; + $options = [ + 'multiple' => false, + 'limit' => 1, + ]; + $column['options'] = $options; + } + + if (stristr($column['column_name'], 'attach')) { + $column['is_query'] = 1; + $column['view_type'] = 'uploadFile'; + $options = [ + 'multiple' => false, + 'limit' => 1, + ]; + $column['options'] = $options; + } + + if ($column['column_name'] === 'sort') { + $column['view_type'] = 'inputNumber'; + } + + if ($column['column_name'] === 'status') { + $column['view_type'] = 'radio'; + $column['dict_type'] = 'data_status'; + } + + if (stristr($column['column_name'], 'is_')) { + $column['view_type'] = 'radio'; + $column['dict_type'] = 'yes_or_no'; + } + } + }; + + if (!$column['is_cover']) { + $object->viewTypeDispose($column); + $object->columnName($column); + } + $column['options'] = json_encode($column['options'], JSON_UNESCAPED_UNICODE); + return $column; + } +} diff --git a/server/plugin/saiadmin/app/logic/tool/GenerateTablesLogic.php b/server/plugin/saiadmin/app/logic/tool/GenerateTablesLogic.php new file mode 100644 index 0000000..67a6dd4 --- /dev/null +++ b/server/plugin/saiadmin/app/logic/tool/GenerateTablesLogic.php @@ -0,0 +1,478 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\logic\tool; + +use plugin\saiadmin\app\cache\UserMenuCache; +use plugin\saiadmin\app\logic\system\DatabaseLogic; +use plugin\saiadmin\app\model\system\SystemMenu; +use plugin\saiadmin\app\model\tool\GenerateTables; +use plugin\saiadmin\app\model\tool\GenerateColumns; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\basic\think\BaseLogic; +use plugin\saiadmin\utils\Helper; +use plugin\saiadmin\utils\code\CodeZip; +use plugin\saiadmin\utils\code\CodeEngine; + +/** + * 代码生成业务逻辑层 + */ +class GenerateTablesLogic extends BaseLogic +{ + protected $columnLogic = null; + + protected $dataLogic = null; + + /** + * 构造函数 + */ + public function __construct() + { + $this->model = new GenerateTables(); + $this->columnLogic = new GenerateColumnsLogic(); + $this->dataLogic = new DatabaseLogic(); + } + + /** + * 删除表和字段信息 + * @param $ids + * @return bool + */ + public function destroy($ids): bool + { + return $this->transaction(function () use ($ids) { + parent::destroy($ids); + GenerateColumns::destroy(function ($query) use ($ids) { + $query->where('table_id', 'in', $ids); + }); + return true; + }); + } + + /** + * 装载表信息 + * @param $names + * @param $source + * @return void + */ + public function loadTable($names, $source): void + { + $data = config('think-orm.connections'); + $config = $data[$source]; + if (!$config) { + throw new ApiException('数据库配置读取失败'); + } + + $prefix = $config['prefix'] ?? ''; + foreach ($names as $item) { + $class_name = $item['name']; + if (!empty($prefix)) { + $class_name = Helper::str_replace_once($prefix, '', $class_name); + } + $class_name = Helper::camel($class_name); + $tableInfo = [ + 'table_name' => $item['name'], + 'table_comment' => $item['comment'], + 'class_name' => $class_name, + 'business_name' => Helper::get_business($item['name']), + 'belong_menu_id' => 80, + 'menu_name' => $item['comment'], + 'tpl_category' => 'single', + 'template' => 'app', + 'stub' => 'think', + 'namespace' => '', + 'package_name' => '', + 'source' => $source, + 'generate_menus' => 'index,save,update,read,destroy', + ]; + $model = GenerateTables::create($tableInfo); + $columns = $this->dataLogic->getColumnList($item['name'], $source); + foreach ($columns as &$column) { + $column['table_id'] = $model->id; + $column['is_cover'] = false; + } + $this->columnLogic->saveExtra($columns); + } + } + + /** + * 同步表字段信息 + * @param $id + * @return void + */ + public function sync($id) + { + $model = $this->model->findOrEmpty($id); + // 拉取已有数据表信息 + $queryModel = $this->columnLogic->model->where('table_id', $id); + $columnLogicData = $this->columnLogic->getAll($queryModel); + $columnLogicList = []; + foreach ($columnLogicData as $item) { + $columnLogicList[$item['column_name']] = $item; + } + GenerateColumns::destroy(function ($query) use ($id) { + $query->where('table_id', $id); + }); + $columns = $this->dataLogic->getColumnList($model->table_name, $model->source ?? ''); + foreach ($columns as &$column) { + $column['table_id'] = $model->id; + $column['is_cover'] = false; + if (isset($columnLogicList[$column['column_name']])) { + // 存在历史信息的情况 + $getcolumnLogicItem = $columnLogicList[$column['column_name']]; + if ($getcolumnLogicItem['column_type'] == $column['column_type']) { + $column['is_cover'] = true; + foreach ($getcolumnLogicItem as $key => $item) { + $array = [ + 'column_comment', + 'column_type', + 'default_value', + 'is_pk', + 'is_required', + 'is_insert', + 'is_edit', + 'is_list', + 'is_query', + 'is_sort', + 'query_type', + 'view_type', + 'dict_type', + 'options', + 'sort', + 'is_cover' + ]; + if (in_array($key, $array)) { + $column[$key] = $item; + } + } + } + } + } + $this->columnLogic->saveExtra($columns); + } + + /** + * 代码预览 + * @param $id + * @return array + */ + public function preview($id): array + { + $data = $this->renderData($id); + + $codeEngine = new CodeEngine($data); + $controllerContent = $codeEngine->renderContent('php', 'controller.stub'); + $logicContent = $codeEngine->renderContent('php', 'logic.stub'); + $modelContent = $codeEngine->renderContent('php', 'model.stub'); + $validateContent = $codeEngine->renderContent('php', 'validate.stub'); + $sqlContent = $codeEngine->renderContent('sql', 'sql.stub'); + $indexContent = $codeEngine->renderContent('vue', 'index.stub'); + $editContent = $codeEngine->renderContent('vue', 'edit-dialog.stub'); + $searchContent = $codeEngine->renderContent('vue', 'table-search.stub'); + $apiContent = $codeEngine->renderContent('ts', 'api.stub'); + + // 返回生成内容 + return [ + [ + 'tab_name' => 'controller.php', + 'name' => 'controller', + 'lang' => 'php', + 'code' => $controllerContent + ], + [ + 'tab_name' => 'logic.php', + 'name' => 'logic', + 'lang' => 'php', + 'code' => $logicContent + ], + [ + 'tab_name' => 'model.php', + 'name' => 'model', + 'lang' => 'php', + 'code' => $modelContent + ], + [ + 'tab_name' => 'validate.php', + 'name' => 'validate', + 'lang' => 'php', + 'code' => $validateContent + ], + [ + 'tab_name' => 'sql.sql', + 'name' => 'sql', + 'lang' => 'sql', + 'code' => $sqlContent + ], + [ + 'tab_name' => 'index.vue', + 'name' => 'index', + 'lang' => 'html', + 'code' => $indexContent + ], + [ + 'tab_name' => 'edit-dialog.vue', + 'name' => 'edit-dialog', + 'lang' => 'html', + 'code' => $editContent + ], + [ + 'tab_name' => 'table-search.vue', + 'name' => 'table-search', + 'lang' => 'html', + 'code' => $searchContent + ], + [ + 'tab_name' => 'api.ts', + 'name' => 'api', + 'lang' => 'javascript', + 'code' => $apiContent + ] + ]; + } + + /** + * 生成到模块 + * @param $id + */ + public function genModule($id) + { + $data = $this->renderData($id); + + // 生成文件到模块 + $codeEngine = new CodeEngine($data); + $codeEngine->generateBackend('controller', $codeEngine->renderContent('php', 'controller.stub')); + $codeEngine->generateBackend('logic', $codeEngine->renderContent('php', 'logic.stub')); + $codeEngine->generateBackend('model', $codeEngine->renderContent('php', 'model.stub')); + $codeEngine->generateBackend('validate', $codeEngine->renderContent('php', 'validate.stub')); + $codeEngine->generateFrontend('index', $codeEngine->renderContent('vue', 'index.stub')); + $codeEngine->generateFrontend('edit-dialog', $codeEngine->renderContent('vue', 'edit-dialog.stub')); + $codeEngine->generateFrontend('table-search', $codeEngine->renderContent('vue', 'table-search.stub')); + $codeEngine->generateFrontend('api', $codeEngine->renderContent('ts', 'api.stub')); + } + + /** + * 处理数据 + * @param $id + * @return array + */ + protected function renderData($id): array + { + $table = $this->model->findOrEmpty($id); + if (!in_array($table['template'], ["plugin", "app"])) { + throw new ApiException('应用类型必须为plugin或者app'); + } + if (empty($table['namespace'])) { + throw new ApiException('请先设置应用名称'); + } + + $columns = $this->columnLogic->where('table_id', $id) + ->order('sort', 'desc') + ->select() + ->toArray(); + $pk = 'id'; + foreach ($columns as &$column) { + if ($column['is_pk'] == 2) { + $pk = $column['column_name']; + } + if ($column['column_name'] == 'delete_time') { + unset($column['column_name']); + } + } + + // 处理特殊变量 + if ($table['template'] == 'plugin') { + $namespace_start = "plugin\\" . $table['namespace'] . "\\app\\admin\\"; + $namespace_start_model = "plugin\\" . $table['namespace'] . "\\app\\"; + $namespace_end = "\\" . $table['package_name']; + $url_path = 'app/' . $table['namespace'] . '/admin/' . $table['package_name'] . '/' . $table['class_name']; + $route = 'app/'; + } else { + $namespace_start = "app\\" . $table['namespace'] . "\\"; + $namespace_start_model = "app\\" . $table['namespace'] . "\\"; + $namespace_end = "\\" . $table['package_name']; + $url_path = $table['namespace'] . '/' . $table['package_name'] . '/' . $table['class_name']; + $route = ''; + } + + $config = config('think-orm'); + + $data = $table->toArray(); + $data['pk'] = $pk; + $data['namespace_start'] = $namespace_start; + $data['namespace_start_model'] = $namespace_start_model; + $data['namespace_end'] = $namespace_end; + $data['url_path'] = $url_path; + $data['route'] = $route; + $data['tables'] = [$data]; + $data['columns'] = $columns; + $data['db_source'] = $config['default'] ?? 'mysql'; + + return $data; + } + + /** + * 生成到模块 + */ + public function generateFile($id) + { + $table = $this->model->where('id', $id)->findOrEmpty(); + if ($table->isEmpty()) { + throw new ApiException('请选择要生成的表'); + } + $debug = config('app.debug', true); + if (!$debug) { + throw new ApiException('非调试模式下,不允许生成文件'); + } + $this->updateMenu($table); + $this->genModule($id); + UserMenuCache::clearMenuCache(); + } + + /** + * 代码生成下载 + */ + public function generate($idsArr): array + { + $zip = new CodeZip(); + $tables = $this->model->where('id', 'in', $idsArr)->select()->toArray(); + foreach ($idsArr as $table_id) { + $data = $this->renderData($table_id); + $data['tables'] = $tables; + $codeEngine = new CodeEngine($data); + $codeEngine->generateTemp(); + } + + $filename = 'saiadmin.zip'; + $download = $zip->compress(); + + return compact('filename', 'download'); + } + + /** + * 处理菜单列表 + * @param $tables + */ + public function updateMenu($tables) + { + /*不存在的情况下进行新建操作*/ + $url_path = $tables['namespace'] . ":" . $tables['package_name'] . ':' . $tables['business_name']; + $code = $tables['namespace'] . "/" . $tables['package_name'] . '/' . $tables['business_name']; + $path = $tables['package_name'] . '/' . $tables['business_name']; + $component = $tables['namespace'] . "/" . $tables['package_name'] . '/' . $tables['business_name']; + + /*先获取一下已有的路由中是否包含当前ID的路由的核心信息*/ + $model = new SystemMenu(); + $tableMenu = $model->where('generate_id', $tables['id'])->findOrEmpty(); + $fistMenu = [ + 'parent_id' => $tables['belong_menu_id'], + 'name' => $tables['menu_name'], + 'code' => $code, + 'path' => $path, + 'icon' => 'ri:home-2-line', + 'component' => "/plugin/$component/index", + 'type' => 2, + 'sort' => 100, + 'is_iframe' => 2, + 'is_keep_alive' => 2, + 'is_hidden' => 2, + 'is_fixed_tab' => 2, + 'is_full_page' => 2, + 'generate_id' => $tables['id'] + ]; + if ($tableMenu->isEmpty()) { + $temp = SystemMenu::create($fistMenu); + $fistMenuId = $temp->id; + } else { + $fistMenu['id'] = $tableMenu['id']; + $tableMenu->save($fistMenu); + $fistMenuId = $tableMenu['id']; + } + /*开始进行子权限的判定操作*/ + $childNodes = [ + ['name' => '列表', 'key' => 'index'], + ['name' => '保存', 'key' => 'save'], + ['name' => '更新', 'key' => 'update'], + ['name' => '读取', 'key' => 'read'], + ['name' => '删除', 'key' => 'destroy'], + ]; + + foreach ($childNodes as $node) { + $nodeData = $model->where('parent_id', $fistMenuId)->where('generate_key', $node['key'])->findOrEmpty(); + $childNodeData = [ + 'parent_id' => $fistMenuId, + 'name' => $node['name'], + 'slug' => "$url_path:{$node['key']}", + 'type' => 3, + 'sort' => 100, + 'is_iframe' => 2, + 'is_keep_alive' => 2, + 'is_hidden' => 2, + 'is_fixed_tab' => 2, + 'is_full_page' => 2, + 'generate_key' => $node['key'] + ]; + if (!empty($nodeData)) { + $childNodeData['id'] = $nodeData['id']; + $nodeData->save($childNodeData); + } else { + $menuModel = new SystemMenu(); + $menuModel->save($childNodeData); + } + } + } + + /** + * 获取数据表字段信息 + * @param $table_id + * @return mixed + */ + public function getTableColumns($table_id): mixed + { + $query = $this->columnLogic->where('table_id', $table_id); + return $this->columnLogic->getAll($query); + } + + /** + * 编辑数据 + * @param $id + * @param $data + * @return mixed + */ + public function edit($id, $data): mixed + { + $columns = $data['columns']; + + unset($data['columns']); + + if (!empty($data['belong_menu_id'])) { + $data['belong_menu_id'] = is_array($data['belong_menu_id']) ? array_pop($data['belong_menu_id']) : $data['belong_menu_id']; + } else { + $data['belong_menu_id'] = 0; + } + + $data['generate_menus'] = implode(',', $data['generate_menus']); + + if (empty($data['options'])) { + unset($data['options']); + } + + $data['options'] = json_encode($data['options'], JSON_UNESCAPED_UNICODE); + + // 更新业务表 + $this->update($data, ['id' => $id]); + + // 更新业务字段表 + foreach ($columns as $column) { + if ($column['options']) { + $column['options'] = json_encode($column['options'], JSON_NUMERIC_CHECK); + } + $this->columnLogic->update($column, ['id' => $column['id']]); + } + + return true; + } + +} diff --git a/server/plugin/saiadmin/app/middleware/CheckAuth.php b/server/plugin/saiadmin/app/middleware/CheckAuth.php new file mode 100644 index 0000000..0ccea7b --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/CheckAuth.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; +use plugin\saiadmin\app\cache\UserAuthCache; +use plugin\saiadmin\app\cache\ReflectionCache; +use plugin\saiadmin\exception\SystemException; + +/** + * 权限检查中间件 + */ +class CheckAuth implements MiddlewareInterface +{ + + public function process(Request $request, callable $handler) : Response + { + $controller = $request->controller; + $action = $request->action; + + // 通过反射获取控制器哪些方法不需要登录 + $noNeedLogin = ReflectionCache::getNoNeedLogin($controller); + + // 不登录访问,无需权限验证 + if (in_array($action, $noNeedLogin)) { + return $handler($request); + } + + // 登录信息 + $token = getCurrentInfo(); + if ($token === false) { + throw new SystemException('用户信息读取失败,无法访问或操作'); + } + + // 系统默认超级管理员,无需权限验证 + if ($token['id'] === 1) { + return $handler($request); + } + + // 2. 获取接口权限属性 (使用缓存类) + $permissions = ReflectionCache::getPermissionAttributes($controller, $action); + + if (!empty($permissions) && !empty($permissions['slug'])) { + // 用户权限缓存 + $auth = UserAuthCache::getUserAuth($token['id']); + + if (!$this->checkPermissions($permissions, $auth)) { + throw new SystemException('权限不足,无法访问或操作'); + } + } + + return $handler($request); + } + + /** + * 检查权限 + */ + private function checkPermissions(array $attr, array $userPermissions): bool + { + // 直接对比 slug + return in_array($attr['slug'], $userPermissions); + } + +} diff --git a/server/plugin/saiadmin/app/middleware/CheckLogin.php b/server/plugin/saiadmin/app/middleware/CheckLogin.php new file mode 100644 index 0000000..9e2c6b4 --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/CheckLogin.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; +use Tinywan\Jwt\JwtToken; +use plugin\saiadmin\app\cache\ReflectionCache; +use plugin\saiadmin\exception\ApiException; + +/** + * 登录检查中间件 + */ +class CheckLogin implements MiddlewareInterface +{ + public function process(Request $request, callable $handler): Response + { + // 通过反射获取控制器哪些方法不需要登录 + $noNeedLogin = ReflectionCache::getNoNeedLogin($request->controller); + // 访问的方法需要登录 + if (!in_array($request->action, $noNeedLogin)) { + try { + $token = JwtToken::getExtend(); + } catch (\Throwable $e) { + throw new ApiException('您的登录凭证错误或者已过期,请重新登录', 401); + } + if ($token['plat'] !== 'saiadmin') { + throw new ApiException('登录凭证校验失败'); + } + $request->setHeader('check_login', true); + $request->setHeader('check_admin', $token); + } + return $handler($request); + } +} diff --git a/server/plugin/saiadmin/app/middleware/CrossDomain.php b/server/plugin/saiadmin/app/middleware/CrossDomain.php new file mode 100644 index 0000000..967f131 --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/CrossDomain.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; + +/** + * 跨域中间件 + */ +class CrossDomain implements MiddlewareInterface +{ + public function process(Request $request, callable $handler) : Response + { + // 如果是options请求则返回一个空响应,否则继续向洋葱芯穿越,并得到一个响应 + $response = $request->method() == 'OPTIONS' ? response('') : $handler($request); + + // 给响应添加跨域相关的http头 + $response->withHeaders([ + 'Access-Control-Allow-Credentials' => 'true', + 'Access-Control-Allow-Origin' => $request->header('origin', '*'), + 'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'), + 'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'), + ]); + + return $response; + } +} diff --git a/server/plugin/saiadmin/app/middleware/SystemLog.php b/server/plugin/saiadmin/app/middleware/SystemLog.php new file mode 100644 index 0000000..3bd9ad4 --- /dev/null +++ b/server/plugin/saiadmin/app/middleware/SystemLog.php @@ -0,0 +1,38 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\middleware; + +use Webman\Event\Event; +use Webman\Http\Request; +use Webman\Http\Response; +use Webman\MiddlewareInterface; +use plugin\saiadmin\exception\ApiException; +use plugin\saiadmin\app\cache\ReflectionCache; + +class SystemLog implements MiddlewareInterface +{ + /** + * @param Request $request + * @param callable $handler + * @return Response + */ + public function process(Request $request, callable $handler): Response + { + // 通过反射获取控制器哪些方法不需要登录 + $noNeedLogin = ReflectionCache::getNoNeedLogin($request->controller); + // 访问的方法需要登录 + if (!in_array($request->action, $noNeedLogin)) { + try { + // 记录日志 + Event::emit('user.operateLog', true); + } catch (\Throwable $e) { + throw new ApiException('登录凭获取失败,请检查'); + } + } + return $handler($request); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemAttachment.php b/server/plugin/saiadmin/app/model/system/SystemAttachment.php new file mode 100644 index 0000000..7d31696 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemAttachment.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 附件模型 + * + * sa_system_attachment 附件信息表 + * + * @property $id 主键 + * @property $category_id 文件分类 + * @property $storage_mode 存储模式 + * @property $origin_name 原文件名 + * @property $object_name 新文件名 + * @property $hash 文件hash + * @property $mime_type 资源类型 + * @property $storage_path 存储目录 + * @property $suffix 文件后缀 + * @property $size_byte 字节数 + * @property $size_info 文件大小 + * @property $url url地址 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemAttachment extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_attachment'; + + /** + * 原文件名搜索 + */ + public function searchOriginNameAttr($query, $value) + { + $query->where('origin_name', 'like', '%' . $value . '%'); + } + + /** + * 文件类型搜索 + */ + public function searchMimeTypeAttr($query, $value) + { + $query->where('mime_type', 'like', $value . '/%'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemCategory.php b/server/plugin/saiadmin/app/model/system/SystemCategory.php new file mode 100644 index 0000000..005fcba --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemCategory.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 附件分类模型 + * + * sa_system_category 附件分类表 + * + * @property $id 分类ID + * @property $parent_id 父id + * @property $level 组集关系 + * @property $category_name 分类名称 + * @property $sort 排序 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemCategory extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_category'; + + /** + * 分类名称搜索 + */ + public function searchCategoryNameAttr($query, $value) + { + $query->where('category_name', 'like', '%' . $value . '%'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemConfig.php b/server/plugin/saiadmin/app/model/system/SystemConfig.php new file mode 100644 index 0000000..aab99cd --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemConfig.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 参数配置模型 + * + * sa_system_config 参数配置信息表 + * + * @property $id 编号 + * @property $group_id 组id + * @property $key 配置键名 + * @property $value 配置值 + * @property $name 配置名称 + * @property $input_type 数据输入类型 + * @property $config_select_data 配置选项数据 + * @property $sort 排序 + * @property $remark 备注 + * @property $created_by 创建人 + * @property $updated_by 更新人 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemConfig extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_config'; + + public function getConfigSelectDataAttr($value) + { + return json_decode($value ?? '', true); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemConfigGroup.php b/server/plugin/saiadmin/app/model/system/SystemConfigGroup.php new file mode 100644 index 0000000..56e9964 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemConfigGroup.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 参数配置分组模型 + * + * sa_system_config_group 参数配置分组表 + * + * @property $id 主键 + * @property $name 字典名称 + * @property $code 字典标示 + * @property $remark 备注 + * @property $created_by 创建人 + * @property $updated_by 更新人 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemConfigGroup extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_config_group'; + + /** + * 关联配置列表 + */ + public function configs() + { + return $this->hasMany(SystemConfig::class, 'group_id', 'id'); + } + + /** + * 名称搜索 + */ + public function searchNameAttr($query, $value) + { + return $query->where('name', 'like', '%' . $value . '%'); + } + + /** + * 编码搜索 + */ + public function searchCodeAttr($query, $value) + { + return $query->where('code', 'like', '%' . $value . '%'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemDept.php b/server/plugin/saiadmin/app/model/system/SystemDept.php new file mode 100644 index 0000000..99fc923 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemDept.php @@ -0,0 +1,62 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 部门模型 + * + * sa_system_dept 部门表 + * + * @property $id 编号 + * @property $parent_id 父级ID,0为根节点 + * @property $name 部门名称 + * @property $code 部门编码 + * @property $leader_id 部门负责人ID + * @property $level 祖级列表,格式: 0,1,5, + * @property $sort 排序,数字越小越靠前 + * @property $status 状态: 1启用, 0禁用 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemDept extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_dept'; + + /** + * 权限范围 + */ + public function scopeAuth($query, $value) + { + if (!empty($value)) { + $deptIds = [$value['id']]; + $deptLevel = $value['level'] . $value['id'] . ','; + $ids = static::whereLike('level', $deptLevel . '%')->column('id'); + $deptIds = array_merge($deptIds, $ids); + $query->whereIn('id', $deptIds); + } + } + + /** + * 部门领导 + */ + public function leader() + { + return $this->hasOne(SystemUser::class, 'id', 'leader_id'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemDictData.php b/server/plugin/saiadmin/app/model/system/SystemDictData.php new file mode 100644 index 0000000..27f66ad --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemDictData.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 字典数据模型 + * + * sa_system_dict_data 字典数据表 + * + * @property $id 主键 + * @property $type_id 字典类型ID + * @property $label 字典标签 + * @property $value 字典值 + * @property $color 字典颜色 + * @property $code 字典标示 + * @property $sort 排序 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemDictData extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_dict_data'; + + /** + * 关键字搜索 + */ + public function searchKeywordsAttr($query, $value) + { + $query->where('label|code', 'LIKE', "%$value%"); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemDictType.php b/server/plugin/saiadmin/app/model/system/SystemDictType.php new file mode 100644 index 0000000..b7990ab --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemDictType.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 字典类型模型 + * + * sa_system_dict_type 字典类型表 + * + * @property $id 主键 + * @property $name 字典名称 + * @property $code 字典标示 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemDictType extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_dict_type'; + + /** + * 关联字典数据 + */ + public function dicts() + { + return $this->hasMany(SystemDictData::class, 'type_id', 'id'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemLoginLog.php b/server/plugin/saiadmin/app/model/system/SystemLoginLog.php new file mode 100644 index 0000000..1aaba87 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemLoginLog.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 登录日志模型 + * + * sa_system_login_log 登录日志表 + * + * @property $id 主键 + * @property $username 用户名 + * @property $ip 登录IP地址 + * @property $ip_location IP所属地 + * @property $os 操作系统 + * @property $browser 浏览器 + * @property $status 登录状态 + * @property $message 提示消息 + * @property $login_time 登录时间 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 更新时间 + */ +class SystemLoginLog extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_login_log'; + + /** + * 时间范围搜索 + */ + public function searchLoginTimeAttr($query, $value) + { + $query->whereTime('login_time', 'between', $value); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemMail.php b/server/plugin/saiadmin/app/model/system/SystemMail.php new file mode 100644 index 0000000..367c2c1 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemMail.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 邮件记录模型 + * + * sa_system_mail 邮件记录 + * + * @property $id 编号 + * @property $gateway 网关 + * @property $from 发送人 + * @property $email 接收人 + * @property $code 验证码 + * @property $content 邮箱内容 + * @property $status 发送状态 + * @property $response 返回结果 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemMail extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_mail'; + + /** + * 发送人搜索 + */ + public function searchFromAttr($query, $value) + { + $query->where('from', 'like', '%' . $value . '%'); + } + + /** + * 接收人搜索 + */ + public function searchEmailAttr($query, $value) + { + $query->where('email', 'like', '%' . $value . '%'); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemMenu.php b/server/plugin/saiadmin/app/model/system/SystemMenu.php new file mode 100644 index 0000000..e26261b --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemMenu.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 菜单模型 + * + * sa_system_menu 菜单权限表 + * + * @property $id + * @property $parent_id 父级ID + * @property $name 菜单名称 + * @property $code 组件名称 + * @property $slug 权限标识,如 user:list, user:add + * @property $type 类型: 1目录, 2菜单, 3按钮/API + * @property $path 路由地址或API路径 + * @property $component 前端组件路径,如 layout/User + * @property $method 请求方式 + * @property $icon 图标 + * @property $sort 排序 + * @property $link_url 外部链接 + * @property $is_iframe 是否iframe + * @property $is_keep_alive 是否缓存 + * @property $is_hidden 是否隐藏 + * @property $is_fixed_tab 是否固定标签页 + * @property $is_full_page 是否全屏 + * @property $generate_id 生成id + * @property $generate_key 生成key + * @property $status 状态 + * @property $remark + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemMenu extends BaseModel +{ + // 完整数据库表名称 + protected $table = 'sa_system_menu'; + // 主键 + protected $pk = 'id'; + + /** + * Id搜索 + */ + public function searchIdAttr($query, $value) + { + $query->whereIn('id', $value); + } + + public function searchNameAttr($query, $value) + { + $query->where('name', 'like', '%' . $value . '%'); + } + + public function searchPathAttr($query, $value) + { + $query->where('path', 'like', '%' . $value . '%'); + } + + public function searchMenuAttr($query, $value) + { + if (!empty($value)) { + $query->whereIn('type', [1, 2]); + } + } + + /** + * Type搜索 + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('type', $value); + } else { + $query->where('type', $value); + } + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemOperLog.php b/server/plugin/saiadmin/app/model/system/SystemOperLog.php new file mode 100644 index 0000000..4c95df3 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemOperLog.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 操作日志模型 + * + * sa_system_oper_log 操作日志表 + * + * @property $id 主键 + * @property $username 用户名 + * @property $app 应用名称 + * @property $method 请求方式 + * @property $router 请求路由 + * @property $service_name 业务名称 + * @property $ip 请求IP地址 + * @property $ip_location IP所属地 + * @property $request_data 请求数据 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 更新时间 + */ +class SystemOperLog extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_oper_log'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemPost.php b/server/plugin/saiadmin/app/model/system/SystemPost.php new file mode 100644 index 0000000..5f26cdc --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemPost.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 岗位模型 + * + * sa_system_post 岗位信息表 + * + * @property $id 主键 + * @property $name 岗位名称 + * @property $code 岗位代码 + * @property $sort 排序 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemPost extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_system_post'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemRole.php b/server/plugin/saiadmin/app/model/system/SystemRole.php new file mode 100644 index 0000000..f5ac328 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemRole.php @@ -0,0 +1,78 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 角色模型 + * + * sa_system_role 角色表 + * + * @property $id + * @property $name 角色名称 + * @property $code 角色标识,如: hr_manager + * @property $level 角色级别:用于行政控制,不可操作级别大于自己的角色 + * @property $data_scope 数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义 + * @property $remark 备注 + * @property $sort + * @property $status 状态: 1启用, 0禁用 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemRole extends BaseModel +{ + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 数据表完整名称 + * @var string + */ + protected $table = 'sa_system_role'; + + /** + * 权限范围 + */ + public function scopeAuth($query, $value) + { + $id = $value['id']; + $roles = $value['roles']; + if ($id > 1) { + $ids = []; + foreach ($roles as $item) { + $ids[] = $item['id']; + $temp = static::whereRaw('FIND_IN_SET("' . $item['id'] . '", level) > 0')->column('id'); + $ids = array_merge($ids, $temp); + } + $query->where('id', 'in', array_unique($ids)); + } + } + + /** + * 通过中间表获取菜单 + */ + public function menus() + { + return $this->belongsToMany(SystemMenu::class, SystemRoleMenu::class, 'menu_id', 'role_id'); + } + + /** + * 通过中间表获取部门 + */ + public function depts() + { + return $this->belongsToMany(SystemDept::class, SystemRoleDept::class, 'dept_id', 'role_id'); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemRoleDept.php b/server/plugin/saiadmin/app/model/system/SystemRoleDept.php new file mode 100644 index 0000000..e6590d5 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemRoleDept.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 角色部门关联模型 + * + * sa_system_role_dept 角色-自定义数据权限关联 + * + * @property $id + * @property $role_id + * @property $dept_id + */ +class SystemRoleDept extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_role_dept'; +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemRoleMenu.php b/server/plugin/saiadmin/app/model/system/SystemRoleMenu.php new file mode 100644 index 0000000..10c16bf --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemRoleMenu.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 角色菜单关联模型 + * + * sa_system_role_menu 角色权限关联 + * + * @property $id + * @property $role_id + * @property $menu_id + */ +class SystemRoleMenu extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_role_menu'; +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemUser.php b/server/plugin/saiadmin/app/model/system/SystemUser.php new file mode 100644 index 0000000..44475f9 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemUser.php @@ -0,0 +1,96 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 用户信息模型 + * + * sa_system_user 用户表 + * + * @property $id + * @property $username 登录账号 + * @property $password 加密密码 + * @property $realname 真实姓名 + * @property $gender 性别 + * @property $avatar 头像 + * @property $email 邮箱 + * @property $phone 手机号 + * @property $signed 个性签名 + * @property $dashboard 工作台 + * @property $dept_id 主归属部门 + * @property $is_super 是否超级管理员: 1是 + * @property $status 状态: 1启用, 2禁用 + * @property $remark 备注 + * @property $login_time 最后登录时间 + * @property $login_ip 最后登录IP + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class SystemUser extends BaseModel +{ + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 数据表完整名称 + * @var string + */ + protected $table = 'sa_system_user'; + + public function searchKeywordAttr($query, $value) + { + if ($value) { + $query->where('username|realname|phone', 'like', '%' . $value . '%'); + } + } + + /** + * 权限范围 - 过滤部门用户 + */ + public function scopeAuth($query, $value) + { + if (!empty($value)) { + $deptIds = [$value['id']]; + $deptLevel = $value['level'] . $value['id'] . ','; + $dept_ids = SystemDept::whereLike('level', $deptLevel . '%')->column('id'); + $deptIds = array_merge($deptIds, $dept_ids); + $query->whereIn('dept_id', $deptIds); + } + } + + /** + * 通过中间表关联角色 + */ + public function roles() + { + return $this->belongsToMany(SystemRole::class, SystemUserRole::class, 'role_id', 'user_id'); + } + + /** + * 通过中间表关联岗位 + */ + public function posts() + { + return $this->belongsToMany(SystemPost::class, SystemUserPost::class, 'post_id', 'user_id'); + } + + /** + * 通过中间表关联部门 + */ + public function depts() + { + return $this->belongsTo(SystemDept::class, 'dept_id', 'id'); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemUserPost.php b/server/plugin/saiadmin/app/model/system/SystemUserPost.php new file mode 100644 index 0000000..0866ef3 --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemUserPost.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 用户岗位关联模型 + * + * sa_system_user_post 用户与岗位关联表 + * + * @property $id 主键 + * @property $user_id 用户主键 + * @property $post_id 岗位主键 + */ +class SystemUserPost extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_user_post'; +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/system/SystemUserRole.php b/server/plugin/saiadmin/app/model/system/SystemUserRole.php new file mode 100644 index 0000000..d88febe --- /dev/null +++ b/server/plugin/saiadmin/app/model/system/SystemUserRole.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\system; + +use think\model\Pivot; + +/** + * 用户角色关联模型 + * + * sa_system_user_role 用户角色关联 + * + * @property $id + * @property $user_id + * @property $role_id + */ +class SystemUserRole extends Pivot +{ + protected $pk = 'id'; + + protected $table = 'sa_system_user_role'; + + /** + * 获取角色id + * @param mixed $user_id + * @return array + */ + public static function getRoleIds($user_id): array + { + return static::where('user_id', $user_id)->column('role_id'); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/Crontab.php b/server/plugin/saiadmin/app/model/tool/Crontab.php new file mode 100644 index 0000000..4adba78 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/Crontab.php @@ -0,0 +1,40 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 定时任务模型 + * + * sa_tool_crontab 定时任务信息表 + * + * @property $id 主键 + * @property $name 任务名称 + * @property $type 任务类型 + * @property $target 调用任务字符串 + * @property $parameter 调用任务参数 + * @property $task_style 执行类型 + * @property $rule 任务执行表达式 + * @property $status 状态 + * @property $remark 备注 + * @property $created_by 创建者 + * @property $updated_by 更新者 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class Crontab extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_crontab'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/CrontabLog.php b/server/plugin/saiadmin/app/model/tool/CrontabLog.php new file mode 100644 index 0000000..22b91b3 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/CrontabLog.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 定时任务日志模型 + * + * sa_tool_crontab_log 定时任务执行日志表 + * + * @property $id 主键 + * @property $crontab_id 任务ID + * @property $name 任务名称 + * @property $target 任务调用目标字符串 + * @property $parameter 任务调用参数 + * @property $exception_info 异常信息 + * @property $status 执行状态 + * @property $create_time 创建时间 + * @property $update_time 修改时间 + */ +class CrontabLog extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_crontab_log'; + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/GenerateColumns.php b/server/plugin/saiadmin/app/model/tool/GenerateColumns.php new file mode 100644 index 0000000..e824518 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/GenerateColumns.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 代码生成业务字段模型 + */ +class GenerateColumns extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_generate_columns'; + + public function getOptionsAttr($value) + { + return json_decode($value ?? '', true); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/model/tool/GenerateTables.php b/server/plugin/saiadmin/app/model/tool/GenerateTables.php new file mode 100644 index 0000000..edb03b8 --- /dev/null +++ b/server/plugin/saiadmin/app/model/tool/GenerateTables.php @@ -0,0 +1,29 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\model\tool; + +use plugin\saiadmin\basic\think\BaseModel; + +/** + * 代码生成业务模型 + */ +class GenerateTables extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + protected $table = 'sa_tool_generate_tables'; + + public function getOptionsAttr($value) + { + return json_decode($value ?? '', true); + } + +} \ No newline at end of file diff --git a/server/plugin/saiadmin/app/validate/system/SystemCategoryValidate.php b/server/plugin/saiadmin/app/validate/system/SystemCategoryValidate.php new file mode 100644 index 0000000..6266d46 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemCategoryValidate.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 附件分类验证器 + */ +class SystemCategoryValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'category_name' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'category_name' => '分类名称必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'add' => [ + 'category_name', + ], + 'edit' => [ + 'category_name', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemConfigGroupValidate.php b/server/plugin/saiadmin/app/validate/system/SystemConfigGroupValidate.php new file mode 100644 index 0000000..45ec372 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemConfigGroupValidate.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemConfigGroup; + +/** + * 字典类型验证器 + */ +class SystemConfigGroupValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'code' => 'require|alphaDash|unique:' . SystemConfigGroup::class, + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '组名称必须填写', + 'name.max' => '组名称最多不能超过16个字符', + 'name.chs' => '组名称必须是中文', + 'code.require' => '组标识必须填写', + 'code.alphaDash' => '组标识只能由英文字母组成', + 'code.unique' => '配置组标识不能重复', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'code', + ], + 'update' => [ + 'name', + 'code', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemConfigValidate.php b/server/plugin/saiadmin/app/validate/system/SystemConfigValidate.php new file mode 100644 index 0000000..6c44477 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemConfigValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典类型验证器 + */ +class SystemConfigValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'key' => 'require', + 'group_id' => 'require', + 'input_type' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '配置标题必须填写', + 'key' => '配置标识必须填写', + 'group_id' => '所属组必须填写', + 'input_type' => '输入组件必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'key', + 'group_id', + 'input_type', + ], + 'update' => [ + 'name', + 'key', + 'group_id', + 'input_type', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemCrontabValidate.php b/server/plugin/saiadmin/app/validate/system/SystemCrontabValidate.php new file mode 100644 index 0000000..1c203a5 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemCrontabValidate.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典类型验证器 + */ +class SystemCrontabValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'type' => 'require', + 'rule' => 'require', + 'target' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '任务名称必须填写', + 'type' => '任务类型必须填写', + 'rule' => '任务规则必须填写', + 'target' => '调用目标必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'type', + 'rule', + 'target', + 'status', + ], + 'update' => [ + 'name', + 'type', + 'rule', + 'target', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemDeptValidate.php b/server/plugin/saiadmin/app/validate/system/SystemDeptValidate.php new file mode 100644 index 0000000..88a9940 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemDeptValidate.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 部门验证器 + */ +class SystemDeptValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '部门名称必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'add' => [ + 'name', + 'status', + ], + 'edit' => [ + 'name', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemDictDataValidate.php b/server/plugin/saiadmin/app/validate/system/SystemDictDataValidate.php new file mode 100644 index 0000000..2f068ea --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemDictDataValidate.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典数据验证器 + */ +class SystemDictDataValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'label' => 'require', + 'value' => 'require', + 'status' => 'require', + 'type_id' => 'require', + 'code' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'label' => '字典标签必须填写', + 'value' => '字典键值必须填写', + 'status' => '状态必须填写', + 'type_id' => '字典类型必须填写', + 'code' => '字典标识必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'label', + 'value', + 'status', + 'type_id', + ], + 'update' => [ + 'label', + 'value', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemDictTypeValidate.php b/server/plugin/saiadmin/app/validate/system/SystemDictTypeValidate.php new file mode 100644 index 0000000..24c52b9 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemDictTypeValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemDictType; + +/** + * 字典类型验证器 + */ +class SystemDictTypeValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'code' => 'require|alphaDash|unique:' . SystemDictType::class, + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '字典名称必须填写', + 'name.max' => '字典名称最多不能超过16个字符', + 'code.require' => '字典标识必须填写', + 'code.alphaDash' => '字典标识只能由英文字母组成', + 'code.unique' => '字典标识已存在', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'code', + 'status', + ], + 'update' => [ + 'name', + 'code', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemMailValidate.php b/server/plugin/saiadmin/app/validate/system/SystemMailValidate.php new file mode 100644 index 0000000..5fd729c --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemMailValidate.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 邮件验证器 + */ +class SystemMailValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'gateway' => 'require', + 'from' => 'require', + 'email' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'gateway' => '网关必须填写', + 'from' => '发件人必须填写', + 'email' => '接收人必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'gateway', + 'from', + 'email', + ], + 'update' => [ + 'gateway', + 'from', + 'email', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemMenuValidate.php b/server/plugin/saiadmin/app/validate/system/SystemMenuValidate.php new file mode 100644 index 0000000..ab65bc2 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemMenuValidate.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 菜单验证器 + */ +class SystemMenuValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '菜单名称必须填写', + 'name.max' => '菜单名称最多不能超过16个字符', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'status', + ], + 'update' => [ + 'name', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemNoticeValidate.php b/server/plugin/saiadmin/app/validate/system/SystemNoticeValidate.php new file mode 100644 index 0000000..cb164c8 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemNoticeValidate.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 系统公告验证器 + */ +class SystemNoticeValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'title' => 'require|min:4', + 'content' => 'require', + 'type' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'title.require' => '公告标题必须填写', + 'title.min' => '公告标题必须大于4个字符', + 'content' => '公告内容必须填写', + 'type' => '公告类型必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'title', + 'content', + 'type', + ], + 'update' => [ + 'title', + 'content', + 'type', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemPostValidate.php b/server/plugin/saiadmin/app/validate/system/SystemPostValidate.php new file mode 100644 index 0000000..64e52c0 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemPostValidate.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 用户角色验证器 + */ +class SystemPostValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'code' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '岗位名称必须填写', + 'code' => '岗位标识必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'code', + 'status', + ], + 'update' => [ + 'name', + 'code', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemRoleValidate.php b/server/plugin/saiadmin/app/validate/system/SystemRoleValidate.php new file mode 100644 index 0000000..2bf7149 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemRoleValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemRole; + +/** + * 用户角色验证器 + */ +class SystemRoleValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require|max:16', + 'code' => 'require|alphaDash|unique:' . SystemRole::class, + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name.require' => '角色名称必须填写', + 'name.max' => '角色名称最多不能超过16个字符', + 'code.require' => '角色标识必须填写', + 'code.alphaDash' => '角色标识只能由英文字母组成', + 'code.unique' => '角色标识不能重复', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'add' => [ + 'name', + 'code', + 'status', + ], + 'edit' => [ + 'name', + 'code', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/system/SystemUserValidate.php b/server/plugin/saiadmin/app/validate/system/SystemUserValidate.php new file mode 100644 index 0000000..e681d80 --- /dev/null +++ b/server/plugin/saiadmin/app/validate/system/SystemUserValidate.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\system; + +use plugin\saiadmin\basic\BaseValidate; +use plugin\saiadmin\app\model\system\SystemUser; + +/** + * 用户信息验证器 + */ +class SystemUserValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'username' => 'require|max:16|unique:' . SystemUser::class, + 'password' => 'require|min:6|max:16', + 'role_ids' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'username.require' => '用户名必须填写', + 'username.max' => '用户名最多不能超过16个字符', + 'username.unique' => '用户名不能重复', + 'password.require' => '密码必须填写', + 'password.min' => '密码最少为6位', + 'password.max' => '密码长度不能超过16位', + 'role_ids' => '角色必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'username', + 'password', + 'role_ids', + ], + 'update' => [ + 'username', + 'role_ids', + ], + ]; +} diff --git a/server/plugin/saiadmin/app/validate/tool/CrontabValidate.php b/server/plugin/saiadmin/app/validate/tool/CrontabValidate.php new file mode 100644 index 0000000..4548adb --- /dev/null +++ b/server/plugin/saiadmin/app/validate/tool/CrontabValidate.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\tool; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 字典类型验证器 + */ +class CrontabValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'name' => 'require', + 'type' => 'require', + 'target' => 'require', + 'status' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'name' => '任务名称必须填写', + 'type' => '任务类型必须填写', + 'target' => '调用目标必须填写', + 'status' => '状态必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'name', + 'type', + 'target', + 'status', + ], + 'update' => [ + 'name', + 'type', + 'target', + 'status', + ], + ]; + +} diff --git a/server/plugin/saiadmin/app/validate/tool/GenerateTablesValidate.php b/server/plugin/saiadmin/app/validate/tool/GenerateTablesValidate.php new file mode 100644 index 0000000..5b8943a --- /dev/null +++ b/server/plugin/saiadmin/app/validate/tool/GenerateTablesValidate.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\app\validate\tool; + +use plugin\saiadmin\basic\BaseValidate; + +/** + * 用户角色验证器 + */ +class GenerateTablesValidate extends BaseValidate +{ + /** + * 定义验证规则 + */ + protected $rule = [ + 'table_name' => 'require', + 'table_comment' => 'require', + 'class_name' => 'require|alphaDash', + 'business_name' => 'require|alphaDash', + 'template' => 'require', + 'namespace' => 'require', + 'menu_name' => 'require', + ]; + + /** + * 定义错误信息 + */ + protected $message = [ + 'table_name' => '表名称必须填写', + 'table_comment' => '表描述必须填写', + 'class_name.require' => '实体类必须填写', + 'class_name.alphaDash' => '实体类必须是英文', + 'business_name.require' => '实体别名必须填写', + 'business_name.alphaDash' => '实体别名必须是英文', + 'template' => '模板必须填写', + 'namespace' => '命名空间必须填写', + 'menu_name' => '菜单名称必须填写', + ]; + + /** + * 定义场景 + */ + protected $scene = [ + 'save' => [ + 'table_name', + 'table_comment', + 'class_name', + 'business_name', + 'template', + 'namespace', + 'menu_name', + ], + 'update' => [ + 'table_name', + 'table_comment', + 'class_name', + 'business_name', + 'template', + 'namespace', + 'menu_name', + ] + ]; + +} diff --git a/server/plugin/saiadmin/app/view/install/error.html b/server/plugin/saiadmin/app/view/install/error.html new file mode 100644 index 0000000..b93f1b4 --- /dev/null +++ b/server/plugin/saiadmin/app/view/install/error.html @@ -0,0 +1,253 @@ + + + + + +“平均亩产1209.1公斤,这标志着全国首个两百万亩玉米‘吨粮田’成功创建。”金秋时节,新疆伊犁哈萨克自治州传来喜讯。这一纪录的诞生,离不开中国农业科学院研发的“玉米密植高产精准调控技术”支撑。依托该技术,位于伊犁的200余万亩玉米高产田亩保苗株数从传统的不足5000株提升到7000—8000株,玉米收获穗数大幅提升。
这只是我国科技强农、粮食增产增收的一个缩影。“十四五”以来,我国粮食总产量始终保持在1.3万亿斤以上。2024年粮食总产量更是首次突破1.4万亿斤,比2020年增产740亿斤。
习近平总书记强调,发展现代农业,建设农业强国,必须依靠科技进步,让科技为农业现代化插上腾飞的翅膀。
“十四五”规划提出,完善农业科技创新体系,创新农技推广服务方式,建设智慧农业。5年来,在科技创新的强劲支撑下,14亿人的饭碗端得更牢、农业现代化水平显著提升、产业新动能持续增强,农业强国建设迈上新台阶。
科技铸“芯”,夯实大国粮仓之基
国以农为本,农以种为先,种子被誉为农业的“芯片”。前不久,四川省富顺县水稻百亩超高产攻关片进行实割实测,再生稻亩产达到494.81公斤,加上此前测产中稻亩产807.13公斤,合计亩产突破1300公斤。取得这一成绩的背后,是“甬优4949”等高产突破性品种的选育和“中稻+再生稻”生产模式的推广。
水稻是我国第一大口粮。“十四五”时期全国多地选育出一批水稻突破性品种:安徽农业大学水稻栽培团队推广自育水稻品种,帮助当地农户水稻亩产增至800公斤;湖南杂交水稻研究中心选育出“西子3号”,推动解决部分受重金属污染地区“镉大米”问题;国家耐盐碱水稻技术创新中心培育出“箐两优3261”,填补了我国华南滨海盐碱区暂无强耐盐、多抗、优质杂交稻品种的空白……
习近平总书记指出,中国人的饭碗要牢牢端在自己手中,就必须把种子牢牢攥在自己手里。
作为我国另一大口粮,小麦育种的创新步伐也不断提速。2025年,西北农林科技大学一次性通过国家审定12个新品种,覆盖半冬性、冬性、春性类型,在抗倒伏等方面实现全面突破。这些为不同生态区“量身定制”的品种,在丰富我国小麦品种的同时,也大幅提升了小麦产能潜力。截至目前,西农小麦系列品种累计推广面积已达18亿亩,为保障国家粮食安全提供了坚实的种源支撑。
“十四五”以来,我国深入实施种业振兴行动,育成了一批生产急需的重大品种,选育出优质高产水稻、节水抗病小麦、机收籽粒玉米、高油高产大豆等急需品种,农作物自主选育品种面积占比超过了95%,做到了“中国粮”主要用“中国种”。
“去年全国粮食亩产394.7公斤,比‘十三五’末提高了12.5公斤,单产提升对我国粮食产量增长的贡献超过60%,有些年份会超过80%。”农业农村部党组书记、部长韩俊表示,“十四五”以来,农业农村部深入实施国家粮食安全战略,“以我为主、立足国内、确保产能、适度进口、科技支撑”,坚持产量产能、生产生态、增产增收一起抓,强化藏粮于地、藏粮于技,全方位夯实粮食安全根基。
智慧提“效”,驱动耕作方式变革
气象墒情传感器、智能虫情测报站等设备如同“千里眼”,与空中无人机巡航、地面机器狗巡检形成立体监测网络。这是日前科技日报记者在北京市昌平区的天汇园果园见到的一幕。
“目前,该果园环境和土壤墒情覆盖10余项指标,虫情识别准确率达90%,种植生产信息化率超过95%,同时土壤成分快检技术能在30分钟内完成土壤成分‘体检’,辅助实现果园虫情和灾情等早预警、早干预。”北京市智慧农业创新团队岗位专家吴建伟介绍,该果园管理从“经验驱动”转向“数据驱动”,为果树生长提供了全天候守护。
在四川省成都市新都区稻菜现代农业园区,当地自主研发的农业巡检机器人已代替人工开展巡检工作;在浙江省衢州市龙游县田间地头,一架植保无人机3小时就能完成300亩农田的喷药流程,相当于40多个人整整一天的工作量……
“十四五”以来,类似的农业新场景新模式不断涌现,现代农业设施装备持续普及应用。我国先后支持建设国家智慧农业创新应用项目116个,深入开展国产化智慧农业技术的中试熟化、推广应用,探索形成了一批信息技术与农机农艺相融合的节本增产增效技术模式。
习近平总书记指出,农业科技创新要着力提升创新体系整体效能,农业科技工作要突出应用导向,把论文写在大地上。
5年来,我国农业科技创新体系整体效能显著提升。我国充分利用物联网、大数据、人工智能等现代信息技术发展智慧农业,并研制出一批先进智能适用的农机装备。
“随着智能农机加快推广,全国安装北斗终端的农机约200万台,植保无人机年作业面积超过4.1亿亩。人工智能、农业机器人等新技术与农业生产经营加速融合,精准播种、变量施肥、智慧灌溉、精准饲喂、环境控制等逐渐普及。”农业农村部市场与信息化司司长雷刘功介绍。
这些前沿技术的落地应用,正是农业科技现代化推动农业现代化的生动实践。“十四五”以来,我国坚持用现代设施装备武装农业,用现代科学技术服务农业,推动农业现代化水平不断提高。2024年底,农业科技进步贡献率已经达到了63.2%,农作物耕种收综合机械化率超过75%。
创新延“链”,拓宽食物供给版图
近日,蒙牛集团携多款产品参加第八届中国国际进口博览会,展示其发展新质生产力的最新成果。“我们打造的全球液态奶行业首座‘灯塔工厂’,已成为全球乳业最高人效比的新标杆,是中国乳业抢占全球智能制造新高地的生动写照。”中粮集团副总经理、蒙牛乳业董事长庆立军介绍。这座“灯塔工厂”通过实施30多项第四次工业革命技术,实现了“百人百亿”的极致人效比——100名员工,年产能达百万吨,创造产值百亿元。
今天,科研创新已成为发展现代化海洋牧场的强大引擎。南方海洋实验室研发“珠海琴”等多功能融合的新型组合式结构加强型养殖平台,为海洋养殖带来新变革;珠海市海洋集团形成海工型养殖装备设计、建造、施工和运维等全产业链条,成功研发“格盛一号”养殖平台,订单水体总量相当于新开拓28.25万亩耕地。
习近平总书记指出,要树立大农业观、大食物观,农林牧渔并举,构建多元化食物供给体系。
“十四五”以来,我国突出科技支撑,强化要素保障,努力向森林要食物,向草原要食物,向江河湖海要食物,向设施农业要食物,向植物动物微生物要热量、要蛋白,多元化食物供给体系加快构建。
一组数据表明,农业科技创新正通过看得见的方式,让老百姓的餐桌品类变得愈发丰富——2024年,我国肉蛋奶等畜产品总量达到1.75亿吨,比2020年增加2778万吨,增长18.8%;水产品总产量达到7358万吨,比2020年增长12.3%,水产品总产量持续36年居全球第一。
党的二十届四中全会审议通过的《中共中央关于制定国民经济和社会发展第十五个五年规划的建议》提出,“统筹发展科技农业、绿色农业、质量农业、品牌农业,把农业建成现代化大产业”。科技创新能够催生新产业、新模式、新动能,是发展新质生产力的核心要素。韩俊表示,加快建设农业强国,必须清醒认识到农业科技国际竞争新形势,把农业科技创新放在更加突出的位置,紧盯世界农业科技前沿,加快突破农业关键核心技术,努力抢占农业科技创新制高点,塑造农业农村发展新动能新优势,培育壮大农业新质生产力。
', 5, 100, 1, 2, '', 2, 1, 1, '2024-06-02 22:55:25', '2026-01-10 11:13:25', NULL); +INSERT INTO `sa_article` VALUES (2, 1, '商业航天稳步快跑 “太空旅游”渐行渐近', '新华网', 'https://www.news.cn/tech/20251124/c7cb9d4e405c4c82b78a8f861889cb22/20251124c7cb9d4e405c4c82b78a8f861889cb22_20251124044f95bbab864da2b0c30861aa41279b.png', '业界普遍认为,以可复用火箭为代表的核心技术突破是商业航天提速的关键支撑。据统计,2025年底至2026年初,我国可复用火箭技术将进入密集首飞期,包括蓝箭航天“朱雀三号”、中科宇航“力箭二号”、星际荣耀“双曲线三号”和星河动力“智神星一号”在内的多款可复用火箭将迎来首飞。', '可搭载7名乘客穿越卡门线,体验约4分钟失重体验……记者从11月22日在京开幕的第四届中国空间科学大会上了解到我国太空旅游的最新进展。与会专家学者认为,随着产业链条不断完善、核心技术持续突破,我国商业航天已迈入稳步快跑的发展新阶段,曾经遥不可及的“太空旅游”正加速走进现实。
记者在第四届中国空间科学大会同期举行的“航天新技术、新成果展”上看到,我国首型面向太空旅游的可重复使用飞行器力鸿二号的模型吸引了众多参观者。中科宇航展台工作人员告诉记者,力鸿二号将采用“箭船分离”的方式将乘客送上太空:飞到既定高度之后,载人舱与火箭分离,继续飞越100公里的卡门线,开始约4分钟的失重段,之后返回地面,以伞降的方式着陆,火箭也将垂直着陆回收。“我们的目标是让力鸿二号可重复使用超30次,这样就能把飞行成本降下来,让更多的人体验太空旅游。”
我国商业航天的快速发展让太空旅游渐行渐近。业界普遍认为,以可复用火箭为代表的核心技术突破是商业航天提速的关键支撑。据统计,2025年底至2026年初,我国可复用火箭技术将进入密集首飞期,包括蓝箭航天“朱雀三号”、中科宇航“力箭二号”、星际荣耀“双曲线三号”和星河动力“智神星一号”在内的多款可复用火箭将迎来首飞。
不仅火箭研制加速突破,卫星应用也在不断拓展。此次展会上,微纳星空等卫星企业也带来了最新的研发成果。微纳星空品牌总监刘晓光介绍,即将发射的“全天候卫士”MN200S-2(01B)星是公司自主研制的商业X波段相控阵雷达成像领域的技术标杆型卫星,可广泛应用于应急救灾、海洋维权、国土安全、生态监测、智慧城市建设等场景,并可实现多星高密度堆叠发射,为后续卫星规模化组网编队提供关键技术验证与工程实践依据。“随着国家低轨卫星互联网的能力建设牵引,微纳星空已经开启批量化、低成本的卫星制造。”
业界认为,目前我国已形成覆盖火箭研制、卫星制造、发射服务、地面应用的完整商业航天产业链,产业集群效应逐步显现。在北京,“南箭北星”的产业格局已显露雏形:亦庄新城正在打造全国首个商业航天共性科研生产基地——火箭大街,海淀区作为“北星”的核心承载区,已集聚涵盖商业卫星制造、测运控、运营及数据应用的近200家相关企业。“在此基础上,海淀正全力推进卫星小镇‘两区一平台’的建设:先导区目前已有40余家商业航天企业聚集;紧邻航天城的卫星小镇核心区54万平方米空间预计2026年6月竣备,将重点引入卫星上下游企业;同时,卫星小镇拟建公共服务平台,提供卫星整星及组部件的力学、热真空、抗辐射等多种测试服务。”卫星小镇核心区对接人段叶叶介绍。
“我国发展商业航天的优势是人多、力量大、竞争强,技术和产品能够快速迭代,紧跟国际趋势。”中国科学院微小卫星创新研究院副院长张永合在接受记者专访时表示,但目前我国商业航天企业和人才大多集中在制造领域,“还需要更多能创造任务的人,有非常前沿的想法,有改变当前航天模式的颠覆性路径。”
张永合认为,商业航天关键是要创造需求,“比如太空旅游就是商业航天创造的需求,将人们日常生活中的旅游延伸到太空中去,在产业上就属于增量。”未来,低空经济、空间互联网等也将打开想象空间。“有了坚实的技术底座,新的产业形态就会自然而然生长出来。”
不过,业内专家也指出,我国商业航天发展仍面临体制机制创新不足、部分核心技术有待突破等挑战。从政策层面来看,近年来国家持续加大对商业航天的支持力度,相关扶持政策和行业规范正在逐步完善,旨在优化市场环境、加大核心技术研发支持,为商业航天高质量发展营造良好生态,推动太空旅游等新业态逐步走向成熟。
业内普遍认为,商业航天已成为航天强国建设的重要增长点。从运载火箭重复使用技术突破到卫星应用场景拓展,随着技术持续成熟、产业链不断完善和政策环境优化,未来“上太空”有望从专业探索逐步走向大众体验,中国商业航天也将在全球太空经济格局中占据重要地位。
随着中国式现代化不断向前推进,中国迎来了数字经济发展的新机遇。在数字经济快速发展的背景下,中国式现代化的内涵得以拓展,现代化动力得以重塑,现代化新动能得以培育,现代化新优势得以形成。数字技术创新、实体经济与数字经济融合、产业数字化、数字产业化成为推进中国式现代化的重要驱动力量。
在数字经济推动下,现代化由工业经济时代的现代化向数字经济时代的现代化转变,在这一大背景下需要在理论上研究数字经济赋能中国式现代化的逻辑和机制,需要深入探讨中国式现代化如何紧紧抓住数字经济发展带来的新机遇,以数字化的知识和信息作为关键生产要素,以数字技术为核心驱动力,在数据要素和数字技术的双轮驱动下推动中国式现代化走上新征程。
南京大学数字经济与管理学院任保平教授的专著《数字经济赋能中国式现代化》于2025年在江苏人民出版社出版,全书共17章,35.8万字。该书立足世界范围内数字化浪潮下的经济现代化背景,从理论与实践两个方面研究了数字经济发展对中国式现代化的赋能作用。
在理论层面,该书研究了数字经济发展对中国式现代化的影响、数字经济与中国式现代化的有机衔接,数字经济背景下中国式现代化目标的重塑、数字经济与中国式现代化深度融合的逻辑机制,数字经济背景下中国式现代化的延伸和拓展。在实践层面,从中国式现代化的不同方面具体研究了数字经济的赋能作用,具体包括数字经济赋能中国式新型工业化、新型城镇化、科技现代化、农业农村现代化、产业现代化和科技现代化。
该书的核心观点主要有以下方面。一是,中国式现代化战略在数字化转型背景下发生的一系列拓展。促进工业化与信息化的融合发展,以数字化带动工业化发展,加大数字技术研发力度,大力发展数字产业。以数字化带动农业现代化,补足中国式现代化短板。协同匹配数字经济时代的创新供求,提升产业技术创新能力。促进企业数字化转型,引领数字经济发展。协调产业数字化与数字产业化,推进产业基础现代化。加快新型基础设施建设,提升基础设施支撑能力。构建数字平台体系,打造现代化经济新形态。
二是,以数字经济发展培育中国式现代化新优势。针对数字经济带来的现代化新变化,研究了数字经济对中国式现代化的引擎作用,认为目前中国式现代化正处于数字经济蓬勃发展带来无数新机遇的时代,我们要抓住数字经济发展带来的新机遇,以数字经济推动中国式现代化的新发展。
三是,阐释数字经济赋能中国式现代化的逻辑。在理论上深刻阐释数字经济如何成为中国式现代化的新引擎,数字经济作为新引擎对中国式现代化赋能的驱动机制和路径,论证数字经济发展赋能中国式现代化在目标、路径和战略上的延伸和拓展,为数字经济赋能中国式现代化提供了一个理论框架。
四是,研究数字经济全面赋能中国式现代化的机制。中国式经济现代化涉及多方面内容,包括科技现代化、工业现代化、农业现代化、服务业现代化、产业链现代化、城市现代化、区域现代化、城市现代化、生态现代化、企业现代化、人的现代化和治理现代化,数字经济应该从上述方面赋能中国式现代化。
五是,提出了以数字经济培育中国式现代化新优势的路径。数字经济培育中国式现代化的新优势包括需求端的动力新优势、供给端的效率新优势等。需要从数字化转型的创新能力、基础设施的供给能力、数字化转型的战略支撑能力,数字化转型的保障能力等方面研究数字经济发展培育中国式现代化新优势的实现路径。而且,需要从效率变革机制、动力变革机制和质量变革机制等方面研究数字经济赋能中国式现代化新优势培育的机制,从数字产业化、产业数字化、产学研协同创新、劳动力质量和相关配套制度等方面实现数字经济培育中国式现代化的新优势,全面展示数字经济赋能中国式现代化中的应用场景。
', 2, 100, 1, 2, '', 2, 1, 1, '2024-06-02 22:58:41', '2026-01-10 11:13:01', NULL); +INSERT INTO `sa_article` VALUES (4, 2, '2025腾讯全球数字生态大会在深圳举行', '新华网', 'https://www.news.cn/tech/20250918/a8a0f6e1a6d740188db7752e247518bb/20250918a8a0f6e1a6d740188db7752e247518bb_202509184f78f2904fa2456db9537d878cb89166.jpg', '5月26日晚上18:00,中超第14轮,深圳新鹏城主场迎战上海申花,上半场马莱莱补射斩获赛季第6球,半场战罢,申花暂1-0新鹏城', '9月16日,2025腾讯全球数字生态大会在深圳举行,会上公布多项AI技术和产品最新进展,并宣布全面开放腾讯AI落地能力及优势场景,助力“好用的AI”在千行百业中加速落地。
2026年美国拉斯维加斯消费电子展(CES)6日至9日举行,首次亮相海外展会的中国小机器人“启元Q1”刚一登场就成为焦点,凭借其出色表现“圈粉”海外。
', 3, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:01:17', '2026-01-10 13:42:24', NULL); +INSERT INTO `sa_article` VALUES (6, 3, 'AI助力药物虚拟筛选提速百万倍 开启后AlphaFold时代创新药', '新华网', 'https://www.news.cn/tech/20260109/2e0f65d6733a4e2588a97dfe96593a09/202601092e0f65d6733a4e2588a97dfe96593a09_202601090012b088f5604e22a77ae70f8656f466.jpg', '团队与清华大学闫创业教授团队合作,在去甲肾上腺素转运体(NET)的临床相关靶点上开展了系列生物实验验证。', '1月9日,清华大学智能产业研究院(AIR)联合清华大学生命学院、清华大学化学系在《科学》杂志发表论文《深度对比学习实现基因组级别药物虚拟筛选》。该论文研发了一个AI驱动的超高通量药物虚拟筛选平台DrugCLIP, 筛选速度对比传统方法实现百万倍提升,同时在预测准确率上也取得显著突破。依托该平台,团队打通了从AlphaFold结构预测到药物发现的关键通道,首次完成了覆盖人类基因组规模的药物虚拟筛选,为后AlphaFold时代的创新药物发现带来新可能性。

长期以来,药物研发面临“高风险、高投入、低成功率”的难题,在靶点发现与先导化合物筛选阶段,受限于传统工具的计算能力,绝大多数潜在靶点和化合物仍未被充分探索。如何在广阔的生物与化学空间中精准高效地发现活性化合物,是当前创新药物研发面临的核心挑战。
据了解,为突破虚拟筛选规模瓶颈,DrugCLIP创新性地构建了蛋白口袋与小分子的“向量化结合空间”,将传统基于物理对接的筛选流程转化为高效的向量检索问题。该模型结合对比学习、3D结构预训练与多模态编码技术,能在三维结构层面精准建模蛋白-配体间的相互作用。训练后的高潜力分子将自然聚集于目标蛋白口袋的向量邻域,能够有效支撑快速的大规模虚拟筛选。依托这一机制,DrugCLIP在128核CPU+8张GPU的计算节点上,能实现毫秒级打分与万亿级日吞吐能力,筛选100万个候选分子仅需0.02秒,日处理能力达31万亿次,对比传统方法实现了百万倍提升。

团队与清华大学闫创业教授团队合作,在去甲肾上腺素转运体(NET)的临床相关靶点上开展了系列生物实验验证。团队使用DrugCLIP模型从160万个候选分子中筛选出约100个高评分分子,同位素配体转运实验检测显示,其中15%为有效抑制剂,其中12个分子结合能力优于现有抗抑郁药物安非他酮。相关复合物结构已通过冷冻电镜解析,进一步验证了DrugCLIP筛选结果的生物学可信度。
值得关注的是,DrugCLIP支持对AlphaFold预测的蛋白结构和apo(无配体)状态下的蛋白口袋进行筛选,扩大了其在真实药物发现场景中的适用性。团队和清华大学刘磊教授团队合作,针对E3泛素连接酶TRIP12(thyroid hormone receptor interactor 12)进行了虚拟筛选与实验验证。过往研究发现,TRIP12是多种肿瘤、帕金森综合征的潜在靶点,但是TRIP12缺少已知的小分子配体和复合物结构。团队使用DrugCLIP模型,从160万个候选分子中高通量筛选出约50个高评分分子,SPR实验证实,其中10个分子与TRIP12有结合能力,两个亲和力较高的分子也对TRIP12的泛素连接酶活性有一定的抑制活性。
此外,依托DrugCLIP,团队首次完成了人类基因组规模的虚拟筛选项目,覆盖约1万个蛋白靶点、2万个结合口袋,分析超过5亿个小分子,富集出200万余个高潜力活性分子,构建了目前已知最大规模的蛋白-配体筛选数据库。该数据库已面向全球科研社区开放,为基础研究与早期药物发现提供了强大数据支持。
DrugCLIP平台现已免费开放,用户无需本地部署,通过网页上传蛋白结构即可启动筛选任务。平台集成口袋/分子编码、向量检索、可视化与结果分析等功能,支持多种分子库调用与自定义上传,广泛适用于科研机构与企业用户。
未来,DrugCLIP将与科研产业生态合作伙伴深度合作,在抗癌、传染病、罕见病等方向加速新靶点与First-in-class药物的发现。团队将持续优化引擎性能、拓展支持模态,助力构建一个更智能、高效与普惠的全球药物创新生态。
', 4, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:02:40', '2026-01-10 13:38:51', NULL); +INSERT INTO `sa_article` VALUES (7, 4, '高度重视低空经济为哪般', '新华网', 'https://www.news.cn/tech/20250312/c0453593a495424780c5424c054a1d4d/20250312c0453593a495424780c5424c054a1d4d_2025031215d8945b560d4d169997f7745d0ef56f.jpg', '当前,我国低空经济正处于市场培育初期,关键技术的实用性和商业价值仅得到初步验证,但已彰显出广阔的增长空间', '近年来,低空经济成为全球发达经济体角逐的重要方向。虽然世界范围内低空经济还处于培育初期阶段,但是美国、日本、欧盟等国家和地区已经重点围绕场景开发应用、交通管理能力、运行技术验证、系统标准体系等方面积极出台和完善相关政策,加快发展低空经济。
低空经济是依托低空飞行活动牵引串联的一系列相互关联的产业经济活动,不仅包括上游生产制造飞行器所必需的材料、零部件及分系统的行业企业,还包括中下游低空飞行器组装集成制造和测试试飞、设施配套及低空服务等领域。低空经济产业链条长、产业关联性强、应用场景丰富,具有战略引领性、高增长潜力等显著特征,既可以推动现代农牧业、先进制造业、现代服务业深度融合发展,又能够扩大有效投资、提振消费需求、提升创新能力。世界主要国家高度重视低空经济发展,就是因为看好其发展前景。
当前,我国低空经济正处于市场培育初期,关键技术的实用性和商业价值仅得到初步验证,但已彰显出广阔的增长空间。未来随着技术迭代升级和商业模式逐步成熟,低空经济的高增长潜力将会进一步释放,更容易实现相关产业企业的群体性爆发成长,有望成为拉动经济增长的新引擎。
一方面,低空飞行器的产业规模体量加快增长、产业生态持续完善。目前,我国无人机制造国际竞争力逐步增强,消费级无人机世界领先优势突出。截至2023年底,我国民用无人机研制企业已超过2300家,量产的无人机产品超过1000款。2023年,我国民用无人机产业规模达到1174.3亿元,同比增长32%。同时,新一代信息技术、新材料、新能源加速与航空科学技术融合发展,推动低空飞行器动力装备及系统、传感器、飞控系统等相关技术加速迭代,绿色高效、安全低噪的飞行器设计、制造与验证技术也持续更新升级。
另一方面,体量巨大、类型多样的应用场景持续涌现,牵引低空服务快速释放动能。运营航空器大幅增加,《2023—2024中国民用无人驾驶航空发展报告》显示,截至2024年8月底,我国无人机实名登记数达198.7万架,比2023年底增加72万架;共颁发无人机驾驶员执照22万本,比2023年底增加13.9%。随着影视航拍、航空运动、空中观光游览等低空文旅应用场景快速发展,低空经济能为满足人民群众美好生活需求提供新供给。2023年,横店“航空+影视+旅游”交旅融合案例入选第一批交通运输与旅游融合发展十佳案例;2024年,敦煌“飞天”通用航空项目等航空旅游产品案例入选第二批交通运输与旅游融合发展示范案例。低空旅游市场潜力开始显现。
同时,低空经济在农业植保、现代物流等行业领域的发展应用不断深入。随着无人机应用技术不断成熟和应用场景持续丰富,“农林牧副渔”多场景作业不断拓展,农业无人机服务市场规模呈蓬勃发展态势。2024年,全国植保无人机的保有量达到25.1万架,作业面积更是高达26.7亿亩次,同比增长近25%。从全球看,上世纪80年代以来,美国农业植保无人机作业渗透率超过50%,日本60%的稻田采用无人机进行植保作业。相较而言,我国农业无人机作业渗透率还比较低,有很大发展空间。在低空物流领域,以无人机为载运工具的无人化配送成为优化城市物流的重要方向,这能有效解决传统物流配送模式面临的劳动力成本、运输成本大幅攀升以及物资配送流通效率低下等诸多问题。在“低空+”领域,低空经济赋能社会治理成效突出,促进巡检、应急救援、城市管理、森林防火、医疗救护等公共服务快速发展。实践中,北京延庆、湖北武汉等地已采用电力线路无人机智能巡检,有效降低了巡检成本,提升了巡检效率。
但也要看到,我国低空经济发展还存在一些问题,如统筹发展和安全有短板、产业融合化发展不足、空域管理协同机制尚不健全、基础设施建设相对滞后等。对此,要从突出集群融合、强化科技创新、加强设施建设等方面综合施策,将低空经济的发展潜力充分释放出来。
一是突出集群融合,加快培育壮大低空经济产业集群,以市场需求为牵引、以科技创新为驱动,积极完善产业生态、谋划应用场景,推进低空制造业集群化发展。二是强化科技创新,聚焦低空经济创新链薄弱环节,加大科技创新投入,加快提升低空技术支撑能力。三是加强设施建设,构建低空经济基础设施综合保障体系,坚持绿色发展、节约集约,统筹推进通用机场、电动垂直起降飞行器起降场、固定运营基地、飞行服务站等地面配套基础设施建设,推进低空飞行通信、导航、气象监测等信息基础设施建设,加速低空经济智联网络设施建设。此外,还要统筹发展和安全,加强低空飞行器监控防护,强化低空安全技术攻关,提升空域精细化管理能力。坚持包容审慎的安全风险管控理念,建设监管服务体系,建立灵活调配、动态高效的低空空域管理使用机制,增强管理的协同性与联动性。
', 11, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:04:23', '2026-01-10 13:43:44', NULL); +INSERT INTO `sa_article` VALUES (8, 4, '国家发改委成立低空经济发展司', '新华网', 'https://www.news.cn/tech/20241231/3f5396024a9749ee863292c04c7119dc/202412313f5396024a9749ee863292c04c7119dc_2024123101c42d384b83467f835ffd286af095d4.jpg', '近日,低空经济发展司召开推动低空基础设施建设座谈会和推动低空智能网联系统建设专题座谈会', '记者从国家发展和改革委员会官方网站获悉,低空经济发展司已正式成立。
低空经济发展司的具体职责是拟订并组织实施低空经济发展战略、中长期发展规划,提出有关政策建议,协调有关重大问题等。
近日,低空经济发展司召开推动低空基础设施建设座谈会和推动低空智能网联系统建设专题座谈会。
在推动低空基础设施建设座谈会上,低空经济发展司负责同志同自然资源部、生态环境部等部委和有关中央企业进行座谈,了解相关领域低空经济典型场景应用和相关基础设施建设发展情况,并就推动低空基础设施有序规划建设进行交流。
在推动低空智能网联系统建设专题座谈会上,低空经济发展司负责同志与通信、导航方面有关专家进行座谈,就低空智能网联系统建设进行交流。
', 6, 100, 1, 2, '', 2, 1, 1, '2024-06-02 23:04:23', '2026-01-10 13:42:32', NULL); + +-- ---------------------------- +-- Table structure for sa_article_banner +-- ---------------------------- +DROP TABLE IF EXISTS `sa_article_banner`; +CREATE TABLE `sa_article_banner` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号', + `banner_type` int(11) NULL DEFAULT NULL COMMENT '类型', + `image` varchar(1000) NULL DEFAULT NULL COMMENT '图片地址', + `is_href` tinyint(1) NULL DEFAULT 1 COMMENT '是否链接', + `url` varchar(255) NULL DEFAULT NULL COMMENT '链接地址', + `title` varchar(255) NULL DEFAULT NULL COMMENT '标题', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '描述', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 COMMENT = '文章轮播图' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_article_banner +-- ---------------------------- +INSERT INTO `sa_article_banner` VALUES (1, 1, 'https://picsum.photos/id/490/640/360', 1, '/blog/1', '探索亚洲的烹饪奇迹', 1, 100, '有一系列名为“新加坡传统烹饪”的食谱,探索了新加坡的美食和文化。它包括新加坡华人、马来人、印度人、欧亚人和土生华人(海峡华人)的美食', 1, 1, '2024-06-02 23:06:37', '2026-01-09 21:51:50', NULL); +INSERT INTO `sa_article_banner` VALUES (2, 1, 'https://picsum.photos/id/29/640/360', 1, '/blog/2', '探索雄伟的山峰', 1, 100, '攀登这座风景如画的山峰的最佳方式是乘坐御在所索道,乘坐15 分钟即可将游客带入空中,欣赏周围一览无余的景观', 1, 1, '2024-06-02 23:06:49', '2026-01-09 21:51:54', NULL); +INSERT INTO `sa_article_banner` VALUES (3, 1, 'https://picsum.photos/id/903/640/360', 1, '/blog/3', '揭秘奇迹', 1, 100, '极光是地球磁场与太阳风相互作用的产物,当太阳风中的带电粒子与地球高层大气中的原子、分子碰撞时,会产生发光现象,形成美丽的极光', 1, 1, '2024-06-02 23:06:56', '2026-01-09 21:53:32', NULL); + +-- ---------------------------- +-- Table structure for sa_article_category +-- ---------------------------- +DROP TABLE IF EXISTS `sa_article_category`; +CREATE TABLE `sa_article_category` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父级ID', + `category_name` varchar(255) NOT NULL COMMENT '分类标题', + `describe` varchar(255) NULL DEFAULT NULL COMMENT '分类简介', + `image` varchar(255) NULL DEFAULT NULL COMMENT '分类图片', + `sort` int(10) UNSIGNED NULL DEFAULT 100 COMMENT '排序', + `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 COMMENT = '文章分类表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_article_category +-- ---------------------------- +INSERT INTO `sa_article_category` VALUES (1, 0, '大国科技', '', NULL, 100, 1, 1, 1, '2024-06-02 22:50:51', '2026-01-06 18:03:07', NULL); +INSERT INTO `sa_article_category` VALUES (2, 0, '数字经济', '', NULL, 100, 1, 1, 1, '2024-06-02 22:50:56', '2026-01-09 16:54:05', NULL); +INSERT INTO `sa_article_category` VALUES (3, 0, '科技快讯', '', NULL, 100, 1, 1, 1, '2024-06-02 22:51:01', '2026-01-07 01:03:37', NULL); +INSERT INTO `sa_article_category` VALUES (4, 0, '低空经济', '', NULL, 100, 1, 1, 1, '2024-06-02 22:51:16', '2026-01-06 18:03:14', NULL); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/plugin/saiadmin/db/saiadmin-pure.sql b/server/plugin/saiadmin/db/saiadmin-pure.sql new file mode 100644 index 0000000..20f1ad5 --- /dev/null +++ b/server/plugin/saiadmin/db/saiadmin-pure.sql @@ -0,0 +1,766 @@ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sa_system_attachment +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_attachment`; +CREATE TABLE `sa_system_attachment` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `category_id` int(11) NULL DEFAULT 0 COMMENT '文件分类', + `storage_mode` smallint(6) NULL DEFAULT 1 COMMENT '存储模式 (1 本地 2 阿里云 3 七牛云 4 腾讯云)', + `origin_name` varchar(255) NULL DEFAULT NULL COMMENT '原文件名', + `object_name` varchar(50) NULL DEFAULT NULL COMMENT '新文件名', + `hash` varchar(64) NULL DEFAULT NULL COMMENT '文件hash', + `mime_type` varchar(255) NULL DEFAULT NULL COMMENT '资源类型', + `storage_path` varchar(100) NULL DEFAULT NULL COMMENT '存储目录', + `suffix` varchar(10) NULL DEFAULT NULL COMMENT '文件后缀', + `size_byte` bigint(20) NULL DEFAULT NULL COMMENT '字节数', + `size_info` varchar(50) NULL DEFAULT NULL COMMENT '文件大小', + `url` varchar(255) NULL DEFAULT NULL COMMENT 'url地址', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `hash`(`hash`) USING BTREE, + INDEX `idx_url`(`url`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE, + INDEX `idx_category_id`(`category_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '附件信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_attachment +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_category +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_category`; +CREATE TABLE `sa_system_category` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '分类ID', + `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父id', + `level` varchar(255) NULL DEFAULT NULL COMMENT '组集关系', + `category_name` varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称', + `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `pid`(`parent_id`) USING BTREE, + INDEX `sort`(`sort`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 COMMENT = '附件分类表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_category +-- ---------------------------- +INSERT INTO `sa_system_category` VALUES (1, 0, '0,', '全部分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (2, 1, '0,1,', '图片分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (3, 1, '0,1,', '文件分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (4, 1, '0,1,', '系统图片', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_category` VALUES (5, 1, '0,1,', '其他分类', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_config +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_config`; +CREATE TABLE `sa_system_config` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `group_id` int(11) NULL DEFAULT NULL COMMENT '组id', + `key` varchar(32) NOT NULL COMMENT '配置键名', + `value` text NULL COMMENT '配置值', + `name` varchar(255) NULL DEFAULT NULL COMMENT '配置名称', + `input_type` varchar(32) NULL DEFAULT NULL COMMENT '数据输入类型', + `config_select_data` varchar(500) NULL DEFAULT NULL COMMENT '配置选项数据', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建人', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`, `key`) USING BTREE, + INDEX `group_id`(`group_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 302 COMMENT = '参数配置信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_config +-- ---------------------------- +INSERT INTO `sa_system_config` VALUES (1, 1, 'site_copyright', 'Copyright © 2024 saithink', '版权信息', 'textarea', NULL, 96, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (2, 1, 'site_desc', '基于vue3 + webman 的极速开发框架', '网站描述', 'textarea', NULL, 97, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (3, 1, 'site_keywords', '后台管理系统', '网站关键字', 'input', NULL, 98, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (4, 1, 'site_name', 'SaiAdmin', '网站名称', 'input', NULL, 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (5, 1, 'site_record_number', '9527', '网站备案号', 'input', NULL, 95, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (6, 2, 'upload_allow_file', 'txt,doc,docx,xls,xlsx,ppt,pptx,rar,zip,7z,gz,pdf,wps,md,jpg,png,jpeg,mp4,pem,crt', '文件类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (7, 2, 'upload_allow_image', 'jpg,jpeg,png,gif,svg,bmp', '图片类型', 'input', NULL, 0, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (8, 2, 'upload_mode', '1', '上传模式', 'select', '[{\"label\":\"本地上传\",\"value\":\"1\"},{\"label\":\"阿里云OSS\",\"value\":\"2\"},{\"label\":\"七牛云\",\"value\":\"3\"},{\"label\":\"腾讯云COS\",\"value\":\"4\"},{\"label\":\"亚马逊S3\",\"value\":\"5\"}]', 99, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (10, 2, 'upload_size', '52428800', '上传大小', 'input', NULL, 88, '单位Byte,1MB=1024*1024Byte', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (11, 2, 'local_root', 'public/storage/', '本地存储路径', 'input', NULL, 0, '本地存储文件路径', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (12, 2, 'local_domain', 'http://127.0.0.1:8787', '本地存储域名', 'input', NULL, 0, 'http://127.0.0.1:8787', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (13, 2, 'local_uri', '/storage/', '本地访问路径', 'input', NULL, 0, '访问是通过domain + uri', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (14, 2, 'qiniu_accessKey', '', '七牛key', 'input', NULL, 0, '七牛云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (15, 2, 'qiniu_secretKey', '', '七牛secret', 'input', NULL, 0, '七牛云存储secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (16, 2, 'qiniu_bucket', '', '七牛bucket', 'input', NULL, 0, '七牛云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (17, 2, 'qiniu_dirname', '', '七牛dirname', 'input', NULL, 0, '七牛云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (18, 2, 'qiniu_domain', '', '七牛domain', 'input', NULL, 0, '七牛云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (19, 2, 'cos_secretId', '', '腾讯Id', 'input', NULL, 0, '腾讯云存储secretId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (20, 2, 'cos_secretKey', '', '腾讯key', 'input', NULL, 0, '腾讯云secretKey', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (21, 2, 'cos_bucket', '', '腾讯bucket', 'input', NULL, 0, '腾讯云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (22, 2, 'cos_dirname', '', '腾讯dirname', 'input', NULL, 0, '腾讯云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (23, 2, 'cos_domain', '', '腾讯domain', 'input', NULL, 0, '腾讯云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (24, 2, 'cos_region', '', '腾讯region', 'input', NULL, 0, '腾讯云存储region', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (25, 2, 'oss_accessKeyId', '', '阿里Id', 'input', NULL, 0, '阿里云存储accessKeyId', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (26, 2, 'oss_accessKeySecret', '', '阿里Secret', 'input', NULL, 0, '阿里云存储accessKeySecret', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (27, 2, 'oss_bucket', '', '阿里bucket', 'input', NULL, 0, '阿里云存储bucket', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (28, 2, 'oss_dirname', '', '阿里dirname', 'input', NULL, 0, '阿里云存储dirname', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (29, 2, 'oss_domain', '', '阿里domain', 'input', NULL, 0, '阿里云存储domain', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (30, 2, 'oss_endpoint', '', '阿里endpoint', 'input', NULL, 0, '阿里云存储endpoint', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (31, 3, 'Host', 'smtp.qq.com', 'SMTP服务器', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (32, 3, 'Port', '465', 'SMTP端口', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (33, 3, 'Username', '', 'SMTP用户名', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (34, 3, 'Password', '', 'SMTP密码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (35, 3, 'SMTPSecure', 'ssl', 'SMTP验证方式', 'radio', '[\r\n {\"label\":\"ssl\",\"value\":\"ssl\"},\r\n {\"label\":\"tsl\",\"value\":\"tsl\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (36, 3, 'From', '', '默认发件人', 'input', '', 100, '默认发件的邮箱地址', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (37, 3, 'FromName', '账户注册', '默认发件名称', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (38, 3, 'CharSet', 'UTF-8', '编码', 'input', '', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (39, 3, 'SMTPDebug', '0', '调试模式', 'radio', '[\r\n {\"label\":\"关闭\",\"value\":\"0\"},\r\n {\"label\":\"client\",\"value\":\"1\"},\r\n {\"label\":\"server\",\"value\":\"2\"}\r\n]', 100, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (40, 2, 's3_key', '', 'key', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (41, 2, 's3_secret', '', 'secret', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (42, 2, 's3_bucket', '', 'bucket', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (43, 2, 's3_dirname', '', 'dirname', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (44, 2, 's3_domain', '', 'domain', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (45, 2, 's3_region', '', 'region', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (46, 2, 's3_version', '', 'version', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (47, 2, 's3_use_path_style_endpoint', '', 'path_style_endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (48, 2, 's3_endpoint', '', 'endpoint', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config` VALUES (49, 2, 's3_acl', '', 'acl', 'input', '', 0, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_config_group +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_config_group`; +CREATE TABLE `sa_system_config_group` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建人', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新人', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 COMMENT = '参数配置分组表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_config_group +-- ---------------------------- +INSERT INTO `sa_system_config_group` VALUES (1, '站点配置', 'site_config', '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config_group` VALUES (2, '上传配置', 'upload_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_config_group` VALUES (3, '邮件服务', 'email_config', NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dept`; +CREATE TABLE `sa_system_dept` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID,0为根节点', + `name` varchar(64) NOT NULL COMMENT '部门名称', + `code` varchar(64) NULL DEFAULT NULL COMMENT '部门编码', + `leader_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '部门负责人ID', + `level` varchar(255) NULL DEFAULT '' COMMENT '祖级列表,格式: 0,1,5, (便于查询子孙节点)', + `sort` int(11) NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_path`(`level`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1114 COMMENT = '部门表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dept +-- ---------------------------- +INSERT INTO `sa_system_dept` VALUES (1, 0, '腾讯集团', 'GROUP', 1, '0,', 100, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dict_data +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dict_data`; +CREATE TABLE `sa_system_dict_data` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `type_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '字典类型ID', + `label` varchar(50) NULL DEFAULT NULL COMMENT '字典标签', + `value` varchar(100) NULL DEFAULT NULL COMMENT '字典值', + `color` varchar(50) NULL DEFAULT NULL COMMENT '字典颜色', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `type_id`(`type_id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 50 COMMENT = '字典数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dict_data +-- ---------------------------- +INSERT INTO `sa_system_dict_data` VALUES (2, 2, '本地存储', '1', '#5d87ff', 'upload_mode', 99, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (3, 2, '阿里云OSS', '2', '#f9901f', 'upload_mode', 98, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (4, 2, '七牛云', '3', '#00ced1', 'upload_mode', 97, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (5, 2, '腾讯云COS', '4', '#1d84ff', 'upload_mode', 96, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (6, 2, '亚马逊S3', '5', '#ff80c8', 'upload_mode', 95, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (7, 3, '正常', '1', '#13deb9', 'data_status', 0, 1, '1为正常', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (8, 3, '停用', '2', '#ff4d4f', 'data_status', 0, 1, '2为停用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (9, 4, '统计页面', 'statistics', '#00ced1', 'dashboard', 100, 1, '管理员用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (10, 4, '工作台', 'work', '#ff8c00', 'dashboard', 50, 1, '员工使用', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (11, 5, '男', '1', '#5d87ff', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (12, 5, '女', '2', '#ff4500', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (13, 5, '未知', '3', '#b48df3', 'gender', 0, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (16, 12, '图片', 'image', '#60c041', 'attachment_type', 10, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (17, 12, '文档', 'text', '#1d84ff', 'attachment_type', 9, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (18, 12, '音频', 'audio', '#00ced1', 'attachment_type', 8, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (19, 12, '视频', 'video', '#ff4500', 'attachment_type', 7, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (20, 12, '应用程序', 'application', '#ff8c00', 'attachment_type', 6, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (21, 13, '目录', '1', '#909399', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (22, 13, '菜单', '2', '#1e90ff', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (23, 13, '按钮', '3', '#ff4500', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (24, 13, '外链', '4', '#00ced1', 'menu_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (25, 14, '是', '1', '#60c041', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (26, 14, '否', '2', '#ff4500', 'yes_or_no', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (47, 20, 'URL任务GET', '1', '#5d87ff', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (48, 20, 'URL任务POST', '2', '#00ced1', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_data` VALUES (49, 20, '类任务', '3', '#ff8c00', 'crontab_task_type', 100, 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_dict_type`; +CREATE TABLE `sa_system_dict_type` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '字典名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '字典标示', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_code`(`code`) USING BTREE, + INDEX `idx_name`(`name`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 24 COMMENT = '字典类型表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_dict_type +-- ---------------------------- +INSERT INTO `sa_system_dict_type` VALUES (2, '存储模式', 'upload_mode', 1, '上传文件存储模式', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (3, '数据状态', 'data_status', 1, '通用数据状态', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (4, '后台首页', 'dashboard', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (5, '性别', 'gender', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (12, '附件类型', 'attachment_type', 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (13, '菜单类型', 'menu_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (14, '是否', 'yes_or_no', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_dict_type` VALUES (20, '定时任务类型', 'crontab_task_type', 1, '', 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_login_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_login_log`; +CREATE TABLE `sa_system_login_log` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` varchar(20) NULL DEFAULT NULL COMMENT '用户名', + `ip` varchar(45) NULL DEFAULT NULL COMMENT '登录IP地址', + `ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地', + `os` varchar(50) NULL DEFAULT NULL COMMENT '操作系统', + `browser` varchar(50) NULL DEFAULT NULL COMMENT '浏览器', + `status` smallint(6) NULL DEFAULT 1 COMMENT '登录状态 (1成功 2失败)', + `message` varchar(50) NULL DEFAULT NULL COMMENT '提示消息', + `login_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '登录时间', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `username`(`username`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE, + INDEX `idx_login_time`(`login_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '登录日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_login_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_mail +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_mail`; +CREATE TABLE `sa_system_mail` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '编号', + `gateway` varchar(50) NULL DEFAULT NULL COMMENT '网关', + `from` varchar(50) NULL DEFAULT NULL COMMENT '发送人', + `email` varchar(50) NULL DEFAULT NULL COMMENT '接收人', + `code` varchar(20) NULL DEFAULT NULL COMMENT '验证码', + `content` varchar(500) NULL DEFAULT NULL COMMENT '邮箱内容', + `status` varchar(20) NULL DEFAULT NULL COMMENT '发送状态', + `response` varchar(500) NULL DEFAULT NULL COMMENT '返回结果', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '邮件记录' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_mail +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_menu`; +CREATE TABLE `sa_system_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `parent_id` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '父级ID', + `name` varchar(64) NOT NULL COMMENT '菜单名称', + `code` varchar(64) NULL DEFAULT NULL COMMENT '组件名称', + `slug` varchar(100) NULL DEFAULT NULL COMMENT '权限标识,如 user:list, user:add', + `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型: 1目录, 2菜单, 3按钮/API', + `path` varchar(255) NULL DEFAULT NULL COMMENT '路由地址(前端)或API路径(后端)', + `component` varchar(255) NULL DEFAULT NULL COMMENT '前端组件路径,如 layout/User', + `method` varchar(10) NULL DEFAULT NULL COMMENT '请求方式', + `icon` varchar(64) NULL DEFAULT NULL COMMENT '图标', + `sort` int(11) NULL DEFAULT 100 COMMENT '排序', + `link_url` varchar(255) NULL DEFAULT NULL COMMENT '外部链接', + `is_iframe` tinyint(1) NULL DEFAULT 2 COMMENT '是否iframe', + `is_keep_alive` tinyint(1) NULL DEFAULT 2 COMMENT '是否缓存', + `is_hidden` tinyint(1) NULL DEFAULT 2 COMMENT '是否隐藏', + `is_fixed_tab` tinyint(1) NULL DEFAULT 2 COMMENT '是否固定标签页', + `is_full_page` tinyint(1) NULL DEFAULT 2 COMMENT '是否全屏', + `generate_id` int(11) NULL DEFAULT 0 COMMENT '生成id', + `generate_key` varchar(255) NULL DEFAULT NULL COMMENT '生成key', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态', + `remark` varchar(255) NULL DEFAULT NULL, + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_parent_id`(`parent_id`) USING BTREE, + INDEX `idx_slug`(`slug`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1000 COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_menu +-- ---------------------------- +INSERT INTO `sa_system_menu` VALUES (1, 0, '仪表盘', 'Dashboard', NULL, 1, '/dashboard', NULL, NULL, 'ri:pie-chart-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (2, 1, '工作台', 'Console', NULL, 2, 'console', '/dashboard/console', NULL, 'ri:home-smile-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (3, 0, '系统管理', 'System', NULL, 1, '/system', NULL, NULL, 'ri:user-3-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (4, 3, '用户管理', 'User', NULL, 2, 'user', '/system/user', NULL, 'ri:user-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (5, 3, '部门管理', 'Dept', NULL, 2, 'dept', '/system/dept', NULL, 'ri:node-tree', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (6, 3, '角色管理', 'Role', NULL, 2, 'role', '/system/role', NULL, 'ri:admin-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (7, 3, '岗位管理', 'Post', '', 2, 'post', '/system/post', NULL, 'ri:signpost-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (8, 3, '菜单管理', 'Menu', NULL, 2, 'menu', '/system/menu', NULL, 'ri:menu-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (10, 0, '运维管理', 'Safeguard', NULL, 1, '/safeguard', '', NULL, 'ri:shield-check-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (11, 10, '缓存管理', 'Cache', '', 2, 'cache', '/safeguard/cache', NULL, 'ri:keyboard-box-line', 80, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (12, 10, '数据字典', 'Dict', NULL, 2, 'dict', '/safeguard/dict', NULL, 'ri:database-2-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (13, 10, '附件管理', 'Attachment', '', 2, 'attachment', '/safeguard/attachment', NULL, 'ri:file-cloud-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (14, 10, '数据表维护', 'Database', '', 2, 'database', '/safeguard/database', NULL, 'ri:database-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (15, 10, '登录日志', 'LoginLog', '', 2, 'login-log', '/safeguard/login-log', NULL, 'ri:login-circle-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (16, 10, '操作日志', 'OperLog', '', 2, 'oper-log', '/safeguard/oper-log', NULL, 'ri:shield-keyhole-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (17, 10, '邮件日志', 'EmailLog', '', 2, 'email-log', '/safeguard/email-log', NULL, 'ri:mail-line', 50, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (18, 3, '系统设置', 'Config', NULL, 2, 'config', '/system/config', NULL, 'ri:settings-4-line', 100, NULL, 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (19, 0, '官方文档', 'Document', '', 4, '', '', NULL, 'ri:file-copy-2-fill', 90, 'https://saithink.top', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (20, 4, '数据列表', '', 'core:user:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (21, 1, '个人中心', 'UserCenter', '', 2, 'user-center', '/dashboard/user-center/index', NULL, 'ri:user-2-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (22, 4, '添加', '', 'core:user:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (23, 4, '修改', '', 'core:user:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (24, 4, '读取', '', 'core:user:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (25, 4, '删除', '', 'core:user:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (26, 4, '重置密码', '', 'core:user:password', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (27, 4, '清理缓存', '', 'core:user:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (28, 4, '设置工作台', '', 'core:user:home', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (29, 5, '数据列表', '', 'core:dept:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (30, 5, '添加', '', 'core:dept:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (31, 5, '修改', '', 'core:dept:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (32, 5, '读取', '', 'core:dept:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (33, 5, '删除', '', 'core:dept:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (34, 6, '添加', '', 'core:role:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (35, 6, '数据列表', '', 'core:role:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (36, 6, '修改', '', 'core:role:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (37, 6, '读取', '', 'core:role:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (38, 6, '删除', '', 'core:role:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (39, 6, '菜单权限', '', 'core:role:menu', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (41, 7, '数据列表', '', 'core:post:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (42, 7, '添加', '', 'core:post:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (43, 7, '修改', '', 'core:post:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (44, 7, '读取', '', 'core:post:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (45, 7, '删除', '', 'core:post:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (46, 7, '导入', '', 'core:post:import', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (47, 7, '导出', '', 'core:post:export', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (48, 8, '数据列表', '', 'core:menu:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (49, 8, '读取', '', 'core:menu:read', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (50, 8, '添加', '', 'core:menu:save', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (51, 8, '修改', '', 'core:menu:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (52, 8, '删除', '', 'core:menu:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (53, 18, '数据列表', '', 'core:config:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (54, 18, '管理', '', 'core:config:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (55, 18, '修改', '', 'core:config:update', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (56, 12, '数据列表', '', 'core:dict:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (57, 12, '管理', '', 'core:dict:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (58, 13, '数据列表', '', 'core:attachment:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (59, 13, '管理', '', 'core:attachment:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (60, 14, '数据表列表', '', 'core:database:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (61, 14, '数据表维护', '', 'core:database:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (62, 14, '回收站数据', '', 'core:recycle:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (63, 14, '回收站管理', '', 'core:recycle:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (64, 15, '数据列表', '', 'core:logs:login', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (65, 15, '删除', '', 'core:logs:deleteLogin', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (66, 16, '数据列表', '', 'core:logs:Oper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (67, 16, '删除', '', 'core:logs:deleteOper', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (68, 17, '数据列表', '', 'core:email:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (69, 17, '删除', '', 'core:email:destroy', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (70, 10, '服务监控', 'Server', '', 2, 'server', '/safeguard/server', NULL, 'ri:server-line', 90, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (71, 70, '数据列表', '', 'core:server:monitor', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (72, 11, '数据列表', '', 'core:server:cache', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (73, 11, '缓存清理', '', 'core:server:clear', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (74, 2, '登录数据统计', '', 'core:console:list', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (75, 0, '附加权限', 'Permission', '', 1, 'permission', '', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 1, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (76, 75, '上传图片', '', 'core:system:uploadImage', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (77, 75, '上传文件', '', 'core:system:uploadFile', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (78, 75, '附件列表', '', 'core:system:resource', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (79, 75, '用户列表', '', 'core:system:user', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (80, 0, '工具', 'Tool', '', 1, '/tool', '', NULL, 'ri:tools-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (81, 80, '代码生成', 'Code', '', 2, 'code', '/tool/code', NULL, 'ri:code-s-slash-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (82, 80, '定时任务', 'Crontab', '', 2, 'crontab', '/tool/crontab', NULL, 'ri:time-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (83, 82, '数据列表', '', 'tool:crontab:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (84, 82, '管理', '', 'tool:crontab:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (85, 82, '运行任务', '', 'tool:crontab:run', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (86, 81, '数据列表', '', 'tool:code:index', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (87, 81, '管理', '', 'tool:code:edit', 3, '', '', NULL, '', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); +INSERT INTO `sa_system_menu` VALUES (88, 0, '插件市场', 'Plugin', '', 2, '/plugin', '/plugin/saipackage/install/index', NULL, 'ri:apps-2-ai-line', 100, '', 2, 2, 2, 2, 2, 0, NULL, 1, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_oper_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_oper_log`; +CREATE TABLE `sa_system_oper_log` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `username` varchar(20) NULL DEFAULT NULL COMMENT '用户名', + `app` varchar(50) NULL DEFAULT NULL COMMENT '应用名称', + `method` varchar(20) NULL DEFAULT NULL COMMENT '请求方式', + `router` varchar(500) NULL DEFAULT NULL COMMENT '请求路由', + `service_name` varchar(30) NULL DEFAULT NULL COMMENT '业务名称', + `ip` varchar(45) NULL DEFAULT NULL COMMENT '请求IP地址', + `ip_location` varchar(255) NULL DEFAULT NULL COMMENT 'IP所属地', + `request_data` text NULL COMMENT '请求数据', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `username`(`username`) USING BTREE, + INDEX `idx_create_time`(`create_time`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_oper_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_post +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_post`; +CREATE TABLE `sa_system_post` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(50) NULL DEFAULT NULL COMMENT '岗位名称', + `code` varchar(100) NULL DEFAULT NULL COMMENT '岗位代码', + `sort` smallint(5) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 87 COMMENT = '岗位信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for sa_system_role +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role`; +CREATE TABLE `sa_system_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(64) NOT NULL COMMENT '角色名称', + `code` varchar(64) NOT NULL COMMENT '角色标识(英文唯一),如: hr_manager', + `level` int(11) NULL DEFAULT 1 COMMENT '角色级别(1-100):用于行政控制,不可操作级别>=自己的角色', + `data_scope` tinyint(4) NULL DEFAULT 1 COMMENT '数据范围: 1全部, 2本部门及下属, 3本部门, 4仅本人, 5自定义', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `sort` int(11) NULL DEFAULT 100, + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_slug`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 17 COMMENT = '角色表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role +-- ---------------------------- +INSERT INTO `sa_system_role` VALUES (1, '超级管理员', 'super_admin', 100, 1, '系统维护者,拥有所有权限', 100, 1, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_role_dept +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role_dept`; +CREATE TABLE `sa_system_role_dept` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) UNSIGNED NOT NULL, + `dept_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_dept_id`(`dept_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色-自定义数据权限关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role_dept +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_role_menu`; +CREATE TABLE `sa_system_role_menu` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `role_id` bigint(20) UNSIGNED NOT NULL, + `menu_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_menu_id`(`menu_id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '角色权限关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_role_menu +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_user +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user`; +CREATE TABLE `sa_system_user` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(64) NOT NULL COMMENT '登录账号', + `password` varchar(255) NOT NULL COMMENT '加密密码', + `realname` varchar(64) NULL DEFAULT NULL COMMENT '真实姓名', + `gender` varchar(10) NULL DEFAULT NULL COMMENT '性别', + `avatar` varchar(255) NULL DEFAULT NULL COMMENT '头像', + `email` varchar(128) NULL DEFAULT NULL COMMENT '邮箱', + `phone` varchar(20) NULL DEFAULT NULL COMMENT '手机号', + `signed` varchar(255) NULL DEFAULT NULL COMMENT '个性签名', + `dashboard` varchar(255) NULL DEFAULT 'work' COMMENT '工作台', + `dept_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '主归属部门', + `is_super` tinyint(1) NULL DEFAULT 0 COMMENT '是否超级管理员: 1是(跳过权限检查), 0否', + `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `login_time` timestamp(0) NULL DEFAULT NULL COMMENT '最后登录时间', + `login_ip` varchar(45) NULL DEFAULT NULL COMMENT '最后登录IP', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_username`(`username`) USING BTREE, + INDEX `idx_dept_id`(`dept_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 110 COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user +-- ---------------------------- +INSERT INTO `sa_system_user` VALUES (1, 'admin', '$2y$10$wnixh48uDnaW/6D9EygDd.OHJK0vQY/4nHaTjMKBCVDBP2NiTatqS', '祭道之上', '2', 'https://image.saithink.top/saiadmin/avatar.jpg', 'saiadmin@admin.com', '15888888888', 'SaiAdmin是兼具设计美学与高效开发的后台系统!', 'statistics', 1, 1, 1, NULL, NULL, NULL, 1, 1, '2026-01-01 00:00:00', '2026-01-01 00:00:00', NULL); + +-- ---------------------------- +-- Table structure for sa_system_user_post +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user_post`; +CREATE TABLE `sa_system_user_post` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户主键', + `post_id` bigint(20) UNSIGNED NOT NULL COMMENT '岗位主键', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id`) USING BTREE, + INDEX `idx_post_id`(`post_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '用户与岗位关联表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user_post +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_system_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `sa_system_user_role`; +CREATE TABLE `sa_system_user_role` ( + `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) UNSIGNED NOT NULL, + `role_id` bigint(20) UNSIGNED NOT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_role_id`(`role_id`) USING BTREE, + INDEX `idx_user_id`(`user_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 55 COMMENT = '用户角色关联' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_system_user_role +-- ---------------------------- +INSERT INTO `sa_system_user_role` VALUES (1, 1, 1); + +-- ---------------------------- +-- Table structure for sa_tool_crontab +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_crontab`; +CREATE TABLE `sa_tool_crontab` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(100) NULL DEFAULT NULL COMMENT '任务名称', + `type` smallint(6) NULL DEFAULT 4 COMMENT '任务类型', + `target` varchar(500) NULL DEFAULT NULL COMMENT '调用任务字符串', + `parameter` varchar(1000) NULL DEFAULT NULL COMMENT '调用任务参数', + `task_style` tinyint(1) NULL DEFAULT NULL COMMENT '执行类型', + `rule` varchar(32) NULL DEFAULT NULL COMMENT '任务执行表达式', + `singleton` smallint(6) NULL DEFAULT 1 COMMENT '是否单次执行 (1 是 2 不是)', + `status` smallint(6) NULL DEFAULT 1 COMMENT '状态 (1正常 2停用)', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for sa_tool_crontab_log +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_crontab_log`; +CREATE TABLE `sa_tool_crontab_log` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `crontab_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '任务ID', + `name` varchar(255) NULL DEFAULT NULL COMMENT '任务名称', + `target` varchar(500) NULL DEFAULT NULL COMMENT '任务调用目标字符串', + `parameter` varchar(1000) NULL DEFAULT NULL COMMENT '任务调用参数', + `exception_info` varchar(2000) NULL DEFAULT NULL COMMENT '异常信息', + `status` smallint(6) NULL DEFAULT 1 COMMENT '执行状态 (1成功 2失败)', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '定时任务执行日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_crontab_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_tool_generate_columns +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_generate_columns`; +CREATE TABLE `sa_tool_generate_columns` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `table_id` int(11) UNSIGNED NULL DEFAULT NULL COMMENT '所属表ID', + `column_name` varchar(200) NULL DEFAULT NULL COMMENT '字段名称', + `column_comment` varchar(255) NULL DEFAULT NULL COMMENT '字段注释', + `column_type` varchar(50) NULL DEFAULT NULL COMMENT '字段类型', + `default_value` varchar(50) NULL DEFAULT NULL COMMENT '默认值', + `is_pk` smallint(6) NULL DEFAULT 1 COMMENT '1 非主键 2 主键', + `is_required` smallint(6) NULL DEFAULT 1 COMMENT '1 非必填 2 必填', + `is_insert` smallint(6) NULL DEFAULT 1 COMMENT '1 非插入字段 2 插入字段', + `is_edit` smallint(6) NULL DEFAULT 1 COMMENT '1 非编辑字段 2 编辑字段', + `is_list` smallint(6) NULL DEFAULT 1 COMMENT '1 非列表显示字段 2 列表显示字段', + `is_query` smallint(6) NULL DEFAULT 1 COMMENT '1 非查询字段 2 查询字段', + `is_sort` smallint(6) NULL DEFAULT 1 COMMENT '1 非排序 2 排序', + `query_type` varchar(100) NULL DEFAULT 'eq' COMMENT '查询方式 eq 等于, neq 不等于, gt 大于, lt 小于, like 范围', + `view_type` varchar(100) NULL DEFAULT 'text' COMMENT '页面控件,text, textarea, password, select, checkbox, radio, date, upload, ma-upload(封装的上传控件)', + `dict_type` varchar(200) NULL DEFAULT NULL COMMENT '字典类型', + `allow_roles` varchar(255) NULL DEFAULT NULL COMMENT '允许查看该字段的角色', + `options` varchar(1000) NULL DEFAULT NULL COMMENT '字段其他设置', + `sort` tinyint(3) UNSIGNED NULL DEFAULT 0 COMMENT '排序', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务字段表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_generate_columns +-- ---------------------------- + +-- ---------------------------- +-- Table structure for sa_tool_generate_tables +-- ---------------------------- +DROP TABLE IF EXISTS `sa_tool_generate_tables`; +CREATE TABLE `sa_tool_generate_tables` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `table_name` varchar(200) NULL DEFAULT NULL COMMENT '表名称', + `table_comment` varchar(500) NULL DEFAULT NULL COMMENT '表注释', + `stub` varchar(50) NULL DEFAULT NULL COMMENT 'stub类型', + `template` varchar(50) NULL DEFAULT NULL COMMENT '模板名称', + `namespace` varchar(255) NULL DEFAULT NULL COMMENT '命名空间', + `package_name` varchar(100) NULL DEFAULT NULL COMMENT '控制器包名', + `business_name` varchar(50) NULL DEFAULT NULL COMMENT '业务名称', + `class_name` varchar(50) NULL DEFAULT NULL COMMENT '类名称', + `menu_name` varchar(100) NULL DEFAULT NULL COMMENT '生成菜单名', + `belong_menu_id` int(11) NULL DEFAULT NULL COMMENT '所属菜单', + `tpl_category` varchar(100) NULL DEFAULT NULL COMMENT '生成类型,single 单表CRUD,tree 树表CRUD,parent_sub父子表CRUD', + `generate_type` smallint(6) NULL DEFAULT 1 COMMENT '1 压缩包下载 2 生成到模块', + `generate_path` varchar(100) NULL DEFAULT 'saiadmin-artd' COMMENT '前端根目录', + `generate_model` smallint(6) NULL DEFAULT 1 COMMENT '1 软删除 2 非软删除', + `generate_menus` varchar(255) NULL DEFAULT NULL COMMENT '生成菜单列表', + `build_menu` smallint(6) NULL DEFAULT 1 COMMENT '是否构建菜单', + `component_type` smallint(6) NULL DEFAULT 1 COMMENT '组件显示方式', + `options` varchar(1500) NULL DEFAULT NULL COMMENT '其他业务选项', + `form_width` int(11) NULL DEFAULT 800 COMMENT '表单宽度', + `is_full` tinyint(1) NULL DEFAULT 1 COMMENT '是否全屏', + `remark` varchar(255) NULL DEFAULT NULL COMMENT '备注', + `source` varchar(255) NULL DEFAULT NULL COMMENT '数据源', + `created_by` int(11) NULL DEFAULT NULL COMMENT '创建者', + `updated_by` int(11) NULL DEFAULT NULL COMMENT '更新者', + `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', + `delete_time` datetime(0) NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '代码生成业务表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sa_tool_generate_tables +-- ---------------------------- + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/server/plugin/saiadmin/exception/ApiException.php b/server/plugin/saiadmin/exception/ApiException.php new file mode 100644 index 0000000..a9040e8 --- /dev/null +++ b/server/plugin/saiadmin/exception/ApiException.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\exception; + +use Webman\Http\Request; +use Webman\Http\Response; +use support\exception\BusinessException; + +/** + * 常规操作异常-只返回json数据,不记录异常日志 + */ +class ApiException extends BusinessException +{ + public function render(Request $request): ?Response + { + return json(['code' => $this->getCode() ?: 500, 'message' => $this->getMessage()]); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/exception/SystemException.php b/server/plugin/saiadmin/exception/SystemException.php new file mode 100644 index 0000000..cd9a81f --- /dev/null +++ b/server/plugin/saiadmin/exception/SystemException.php @@ -0,0 +1,20 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\exception; + +use Throwable; + +/** + * 系统接口错误-返回json数据,并且记录异常日志 + */ +class SystemException extends \RuntimeException +{ + public function __construct($message, $code = 400, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/process/Task.php b/server/plugin/saiadmin/process/Task.php new file mode 100644 index 0000000..dc31a59 --- /dev/null +++ b/server/plugin/saiadmin/process/Task.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\process; + +use plugin\saiadmin\app\logic\tool\CrontabLogic; +use Webman\Channel\Client; +use Workerman\Crontab\Crontab; + +class Task +{ + protected $logic; //login对象 + public $crontabIds = []; //定时任务表主键id => Crontab对象id + + public function __construct() + { + $dbName = env('DB_NAME'); + if (!empty($dbName)) { + $this->logic = new CrontabLogic(); + // 连接webman channel服务 + Client::connect(); + // 订阅某个自定义事件并注册回调,收到事件后会自动触发此回调 + Client::on('crontab', function ($data) { + $this->reload($data); + }); + } + } + public function onWorkerStart() + { + $dbName = env('DB_NAME'); + if (!empty($dbName)) { + $this->initStart(); + } + } + + public function initStart() + { + $logic = new CrontabLogic(); + $taskList = $logic->getAll($logic->search(['status' => 1])); + foreach ($taskList as $item) { + $crontab = new Crontab($item['rule'], function () use ($item) { + $this->logic->run($item['id']); + }); + $this->crontabIds[intval($item['id'])] = $crontab->getId(); //存储定时任务表主键id => Crontab对象id + echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $item['id'] . "][" . $item['name'] . "]:启动成功" . PHP_EOL; + } + } + + public function reload($data) + { + $id = intval($data['args'] ?? 0); //定时任务表主键id + if (isset($this->crontabIds[$id])) { + Crontab::remove($this->crontabIds[$id]); + unset($this->crontabIds[$id]); //删除定时任务表主键id => Crontab对象id + echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $id . "]:移除成功" . PHP_EOL; + } + $item = $this->logic->read($id);// 查询定时任务表数据 + if ($item && $item['status'] == 1) { + $crontab = new Crontab($item['rule'], function () use ($item) { + $this->logic->run($item['id']); + }); + $this->crontabIds[$id] = $crontab->getId(); //存储定时任务表主键id => Crontab对象id + echo PHP_EOL . date('Y-m-d H:i:s') . " => 定时任务[" . $item['id'] . "][" . $item['name'] . "]:启动成功" . PHP_EOL; + } + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/process/Test.php b/server/plugin/saiadmin/process/Test.php new file mode 100644 index 0000000..67eb164 --- /dev/null +++ b/server/plugin/saiadmin/process/Test.php @@ -0,0 +1,15 @@ + +// +---------------------------------------------------------------------- +namespace plugin\saiadmin\process; + +class Test +{ + public function run($args): void + { + echo '任务[Test]调用:' . date('Y-m-d H:i:s') . "\n"; + } +} \ No newline at end of file diff --git a/server/plugin/saiadmin/public/assets/bootstrap.min.css b/server/plugin/saiadmin/public/assets/bootstrap.min.css new file mode 100644 index 0000000..1472dec --- /dev/null +++ b/server/plugin/saiadmin/public/assets/bootstrap.min.css @@ -0,0 +1,7 @@ +@charset "UTF-8";/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-color-rgb:33,37,41;--bs-body-bg-rgb:255,255,255;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:.2rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:.3rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/server/plugin/saiadmin/public/assets/jquery.min.js b/server/plugin/saiadmin/public/assets/jquery.min.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/server/plugin/saiadmin/public/assets/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0