limeng hace 1 año
padre
commit
400d9eafcd
Se han modificado 100 ficheros con 11822 adiciones y 0 borrados
  1. 19 0
      .gitignore
  2. 0 0
      11.txt
  3. 44 0
      README.md
  4. 423 0
      sql/ssp-server.sql
  5. 11 0
      ssp-admin-vue3/.editorconfig
  6. 4 0
      ssp-admin-vue3/.env
  7. 7 0
      ssp-admin-vue3/.env.development
  8. 7 0
      ssp-admin-vue3/.env.production
  9. 5 0
      ssp-admin-vue3/.eslintignore
  10. 79 0
      ssp-admin-vue3/.eslintrc.js
  11. 26 0
      ssp-admin-vue3/.gitignore
  12. 24 0
      ssp-admin-vue3/README.md
  13. 40 0
      ssp-admin-vue3/index.html
  14. 4843 0
      ssp-admin-vue3/package-lock.json
  15. 40 0
      ssp-admin-vue3/package.json
  16. BIN
      ssp-admin-vue3/public/favicon-sj.ico
  17. BIN
      ssp-admin-vue3/public/favicon.ico
  18. 11 0
      ssp-admin-vue3/src/App.vue
  19. BIN
      ssp-admin-vue3/src/assets/err-icon.png
  20. BIN
      ssp-admin-vue3/src/assets/icon/dyc.png
  21. BIN
      ssp-admin-vue3/src/assets/icon/gly.png
  22. BIN
      ssp-admin-vue3/src/assets/icon/icon-user.png
  23. BIN
      ssp-admin-vue3/src/assets/icon/qiandao.png
  24. BIN
      ssp-admin-vue3/src/assets/icon/reg-d.png
  25. BIN
      ssp-admin-vue3/src/assets/icon/sys-gzh.png
  26. 1 0
      ssp-admin-vue3/src/assets/login-bg-low.svg
  27. 1 0
      ssp-admin-vue3/src/assets/login-icon.svg
  28. BIN
      ssp-admin-vue3/src/assets/logo-sj.png
  29. BIN
      ssp-admin-vue3/src/assets/logo.png
  30. 77 0
      ssp-admin-vue3/src/components/com-right-box.vue
  31. 35 0
      ssp-admin-vue3/src/components/svg-icon/index.vue
  32. 15 0
      ssp-admin-vue3/src/init/error-handler.js
  33. 102 0
      ssp-admin-vue3/src/init/init-admin.js
  34. 16 0
      ssp-admin-vue3/src/init/init-el-icons.js
  35. 16 0
      ssp-admin-vue3/src/init/init-sa-form.js
  36. 142 0
      ssp-admin-vue3/src/layout/index.vue
  37. 38 0
      ssp-admin-vue3/src/layout/main/layout-classic.vue
  38. 75 0
      ssp-admin-vue3/src/layout/main/layout-column.vue
  39. 131 0
      ssp-admin-vue3/src/layout/main/nav-column/nav-cmb-left.vue
  40. 176 0
      ssp-admin-vue3/src/layout/main/nav-column/nav-cmb-right.vue
  41. 157 0
      ssp-admin-vue3/src/layout/nav/com-right-menu.vue
  42. 69 0
      ssp-admin-vue3/src/layout/nav/com-setting.vue
  43. 80 0
      ssp-admin-vue3/src/layout/nav/com-setting/com-setting-layout-mode.vue
  44. 46 0
      ssp-admin-vue3/src/layout/nav/com-setting/com-setting-more.vue
  45. 45 0
      ssp-admin-vue3/src/layout/nav/com-setting/com-setting-switch.vue
  46. 46 0
      ssp-admin-vue3/src/layout/nav/com-setting/com-setting-tab-style.vue
  47. 73 0
      ssp-admin-vue3/src/layout/nav/com-setting/com-setting-theme.vue
  48. 47 0
      ssp-admin-vue3/src/layout/nav/nav-logo.vue
  49. 136 0
      ssp-admin-vue3/src/layout/nav/nav-menu-bar.vue
  50. 220 0
      ssp-admin-vue3/src/layout/nav/nav-tab-bar.vue
  51. 52 0
      ssp-admin-vue3/src/layout/nav/nav-tool-bar.vue
  52. 83 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-breadcrumb.vue
  53. 56 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-datetime.vue
  54. 31 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-fold.vue
  55. 53 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-note.vue
  56. 16 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-refresh.vue
  57. 62 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-screen.vue
  58. 88 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-search.vue
  59. 17 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-setting.vue
  60. 48 0
      ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-user.vue
  61. 72 0
      ssp-admin-vue3/src/layout/nav/nav-view-vessel.vue
  62. 253 0
      ssp-admin-vue3/src/layout/theme.css
  63. 79 0
      ssp-admin-vue3/src/layout/transition.scss
  64. 59 0
      ssp-admin-vue3/src/layout/view/403.vue
  65. 59 0
      ssp-admin-vue3/src/layout/view/404.vue
  66. 27 0
      ssp-admin-vue3/src/layout/view/iframe-view.vue
  67. 41 0
      ssp-admin-vue3/src/layout/view/os-loading.vue
  68. 116 0
      ssp-admin-vue3/src/layout/view/os-model.vue
  69. 22 0
      ssp-admin-vue3/src/layout/view/whole404.vue
  70. 53 0
      ssp-admin-vue3/src/main.js
  71. 9 0
      ssp-admin-vue3/src/mitt/index.js
  72. 34 0
      ssp-admin-vue3/src/router/async-import.js
  73. 20 0
      ssp-admin-vue3/src/router/home.js
  74. 29 0
      ssp-admin-vue3/src/router/index.js
  75. 134 0
      ssp-admin-vue3/src/router/menu-list.js
  76. 51 0
      ssp-admin-vue3/src/router/router-guards.js
  77. 260 0
      ssp-admin-vue3/src/router/router-util.js
  78. 48 0
      ssp-admin-vue3/src/router/routes-dynamic.js
  79. 55 0
      ssp-admin-vue3/src/router/routes-static.js
  80. 108 0
      ssp-admin-vue3/src/sa-frame/com/in/in-enum.vue
  81. 112 0
      ssp-admin-vue3/src/sa-frame/com/in/in-input.vue
  82. 23 0
      ssp-admin-vue3/src/sa-frame/com/in/in-item.vue
  83. 23 0
      ssp-admin-vue3/src/sa-frame/com/in/in-item2.vue
  84. 134 0
      ssp-admin-vue3/src/sa-frame/com/in/in-list.vue
  85. 52 0
      ssp-admin-vue3/src/sa-frame/com/in/in-money-f.vue
  86. 90 0
      ssp-admin-vue3/src/sa-frame/com/in/in-rich-text.vue
  87. 46 0
      ssp-admin-vue3/src/sa-frame/com/more/com-fast-btn.vue
  88. 44 0
      ssp-admin-vue3/src/sa-frame/com/more/com-page.vue
  89. 73 0
      ssp-admin-vue3/src/sa-frame/com/show/show-enum.vue
  90. 88 0
      ssp-admin-vue3/src/sa-frame/com/show/show-info.vue
  91. 80 0
      ssp-admin-vue3/src/sa-frame/com/show/show-list.vue
  92. 117 0
      ssp-admin-vue3/src/sa-frame/com/td/td-enum.vue
  93. 108 0
      ssp-admin-vue3/src/sa-frame/com/td/td-info.vue
  94. 75 0
      ssp-admin-vue3/src/sa-frame/com/td/td-list.vue
  95. BIN
      ssp-admin-vue3/src/sa-frame/img/kulian.png
  96. BIN
      ssp-admin-vue3/src/sa-frame/img/up-icon.png
  97. 1392 0
      ssp-admin-vue3/src/sa-frame/kj/layer/layer.js
  98. 2 0
      ssp-admin-vue3/src/sa-frame/kj/layer/mobile/layer.js
  99. 1 0
      ssp-admin-vue3/src/sa-frame/kj/layer/mobile/need/layer.css
  100. 0 0
      ssp-admin-vue3/src/sa-frame/kj/layer/theme/default/icon-ext.png

+ 19 - 0
.gitignore

@@ -0,0 +1,19 @@
+target/
+.project
+.classpath
+.settings
+
+*.rar
+*.zip
+
+/.idea/
+
+node_modules/
+bin/
+.settings/
+unpackage/
+/.apt_generated/
+/.apt_generated_tests/
+
+*.imi
+*.iml

+ 0 - 0
11.txt


+ 44 - 0
README.md

@@ -0,0 +1,44 @@
+# Sa-Sso-Pro v1.6.0
+ 
+### 一、项目介绍
+ 
+Sa-Sso-Pro(单点登录Pro商业版),是基于 Sa-Token SSO 模块搭建的独立单点登录系统。
+
+此为付费授权项目,请不要随意传播源码。为了方便您的项目对接,请务必在开发前完整阅读开发文档:[http://sa-pro.dev33.cn/ssp-doc/index.html](http://sa-pro.dev33.cn/ssp-doc/index.html)
+
+###  二、项目优势
+
+1. 基于 SSO 三种模式搭建,可解决:同域、跨域、共享Redis、跨Redis、前后端一体、前后端分离、纯js、vue2、vue3 等架构下的单点登录问题。
+2. 提供各种登录示例:超链接直接登录、访问页面触发登录、调用Ajax触发登录、超链接直接注销、调用Ajax单点注销 等各种场景下的交互姿势。
+3. 提供可视化后台 UI 管理界面,更方便的维护数据(使用最新主流技术栈:vue3 + vite + element-plus + pinia 等)。
+4. 提供用户同步方案:从认证中心同步到第三方系统,或者从第三方系统同步到认证中心。
+5. 提供详细文档:项目搭建、启动、测试、开发、打包、部署等关键步骤的文档说明。
+6. Server 端集成了文件上传、验证码授权、Redis 控制台、API 日志统计、登录日志统计等常见功能,项目基本部署即可用,同时也可方便的二开。
+
+
+
+### 三、购买项目授权ID 
+如果您是通过正规途径得到的当前源码,我们的团队人员会在发送源码时附赠一个ID授权码,请妥善保管此授权码,这将是您购买源码授权的重要凭证
+
+为了证明项目代码的授权合法性,请您在正式部署项目前,将授权码填写到您的项目源码中,
+文件所在位置为:`\ssp-server\src\main\resources\static\sa-res\login.js`,打开此文件,最后三行代码,将授权码填写到`sspId`变量中
+
+要求:在项目部署后,通过浏览器访问登录页面时,F12控制台可以明显的看到授权码id打印 
+
+任何不填写、填错、伪造授权码id、冒用他人授权码的行为都将视为无效授权,为了不引起不必要的法律责任,请您严格按照授权要求操作 
+
+您可以在 [http://sa-pro.dev33.cn/index.html](http://sa-pro.dev33.cn/index.html) 查看更完整的授权要求
+
+
+### 四、版本信息
+
+当前源码版本:v1.6.0,更新于:2023-1-13
+
+
+
+
+
+
+
+
+<div style="height: 400px"></div>

+ 423 - 0
sql/ssp-server.sql

@@ -0,0 +1,423 @@
+
+
+
+
+
+
+
+
+
+
+
+
+-- ======================================== Sa-Sso-Pro 系统表 ====================================  
+
+-- 系统角色表 
+drop table if exists sp_role; 
+CREATE TABLE `sp_role` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色id,--主键、自增',
+  `name` varchar(20) NOT NULL COMMENT '角色名称, 唯一约束',
+  `info` varchar(200) DEFAULT NULL COMMENT '角色详细描述',
+  `is_lock` int(11) NOT NULL DEFAULT '1' COMMENT '是否锁定(1=是,2=否), 锁定之后不可随意删除, 防止用户误操作',
+  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE KEY `name` (`name`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='系统角色表';
+
+INSERT INTO `sp_role`(`id`, `name`, `info`, `is_lock`) VALUES (1, 'Root 超管', '拥有系统最高权限', 1);
+INSERT INTO `sp_role`(`id`, `name`, `info`, `is_lock`) VALUES (2, '管理员', '系统维护管理员', 2);
+INSERT INTO `sp_role`(`id`, `name`, `info`, `is_lock`) VALUES (11, '普通账号', '普通账号', 2);
+INSERT INTO `sp_role`(`id`, `name`, `info`, `is_lock`) VALUES (12, '测试角色', '测试角色', 2);
+
+
+
+-- 菜单表 
+drop table if exists sp_menu;
+CREATE TABLE `sp_menu` (
+  `aid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增id [no]',
+  `id` varchar(50) NOT NULL COMMENT '菜单id',
+  `title` varchar(50) NOT NULL COMMENT '菜单名称',
+  `icon` varchar(200) COMMENT '菜单图标',
+  `info` varchar(500) COMMENT '菜单介绍',
+  `type` varchar(20) default 'com' COMMENT '菜单类型(dir=目录, com=组件, btn=按钮, link=链接)',
+  `path` varchar(500) COMMENT '菜单路由',
+	`component_path` varchar(500) COMMENT '组件路径',
+  `url` varchar(1024) COMMENT '菜单url (如果指定了此值,则通过 iframe 打开页面视图)',
+  `is_blank` varchar(1024) COMMENT '是否属于外部链接 (如果为true, 则点击菜单时从新窗口打开url) [j switch=true]',
+  `show` varchar(500) COMMENT '是否显示 (yes=永远显示,no=永远不显示,auth=根据权限决定是否显示) [j a-type=3]',
+  `auth` varchar(500) COMMENT '是否鉴权 [j a-type=3]',
+  `parent_id` varchar(50) COMMENT '父菜单id',
+	`sort` bigint(20) COMMENT '排序索引 [num]', 
+  `create_time` datetime COMMENT '创建时间',
+  `update_time` datetime COMMENT '更新时间',
+  PRIMARY KEY (`aid`) USING BTREE,
+  UNIQUE KEY `id` (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='菜单表';
+
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1001, 'bas', '身份相关', '', '', 'dir', '', '', '', '0', 'no', '1', '-1', 1001, '2022-11-09 16:15:27', '2022-11-12 13:20:49');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1002, 'in-system', '允许进入后台管理', '', '', 'btn', '', '', '', '0', 'no', '1', 'bas', 1003, '2022-11-09 16:18:13', '2022-11-30 05:29:24');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1003, 'root', 'Root 权限(最高权限)', '', '当前系统的最高权限标识,请谨慎授权', 'btn', '', '', '', '0', 'no', '1', 'bas', 1002, '2022-11-09 16:16:50', '2022-11-30 03:32:28');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1004, 'auth', '权限控制', 'el-icon-Unlock', '控制 Admin 管理员对后台的访问规则', 'dir', '', '', '', '0', 'auth', '1', '-1', 1004, '2022-11-09 16:18:51', '2022-11-30 03:55:23');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1005, 'role-list', '角色管理', 'el-icon-Unlock', '', 'com', '', '@/sp-views/sp-role/role-list.vue', NULL, '0', 'auth', '1', 'auth', 1005, '2022-11-09 16:24:52', '2022-12-11 22:01:33');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1006, 'menu-list', '菜单管理', 'el-icon-CollectionTag', '', 'com', '', '/@/sp-views/sp-role/menu-list.vue', NULL, '0', 'auth', '1', 'auth', 1006, '2022-11-09 16:25:41', '2022-12-11 22:01:59');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1007, 'admin-list', '管理员列表', 'el-icon-Key', '管理所有可以登录后台的 Admin 账号', 'com', '', '@/sp-views/sp-admin/admin-list.vue', NULL, '0', 'auth', '1', 'auth', 1007, '2022-11-09 17:01:02', '2022-12-11 22:02:09');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1008, 'console', '监控中心', 'el-icon-View', '提供 Redis、SQL、API访问日志等在线监控能力', 'dir', '', '', '', '0', 'auth', '1', '-1', 1008, '2022-11-09 18:11:13', '2022-11-30 03:54:16');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1009, 'redis-console', 'Redis 监控台', 'el-icon-Search', '', 'com', '', '/@/sp-views/sp-console/redis-console.vue', NULL, '0', 'auth', '1', 'console', 1009, '2022-11-09 18:12:32', '2022-11-30 05:12:32');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1010, 'apilog-list', 'API 请求日志', 'el-icon-MostlyCloudy', '', 'com', '', '@/sp-views/sp-apilog/apilog-list.vue', NULL, '0', 'auth', '1', 'console', 1010, '2022-11-09 18:13:35', '2022-11-30 05:12:37');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1015, 'admin-add', '管理员添加', 'el-icon-Key', '按钮权限:决定管理员列表页是否显示 [ 管理员添加 ] 按钮', 'btn', '', '@/sp-views/sp-admin/admin-add.vue', '', '0', 'no', '1', 'auth', 1011, '2022-11-12 18:02:34', '2022-12-11 22:02:15');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1016, 'sp-admin-login', '管理员登录日志', 'el-icon-Mouse', '', 'com', '', '@/sp-views/sp-admin-login/sp-admin-login-list.vue', NULL, '0', 'auth', '1', 'auth', 1012, '2022-11-12 18:03:37', '2022-12-11 22:02:23');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1017, 'sql-console', 'SQL 监控台', 'el-icon-View', '', 'link', '', NULL, '${SERVER_URL}/druid/index.html', '0', 'auth', '1', 'console', 1013, '2022-11-12 18:04:46', '2022-11-30 03:53:46');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1018, 'form-generator', '在线表单构建', 'el-icon-View', '', 'link', '', NULL, 'https://mrhj.gitee.io/form-generator', '0', 'no', '1', 'console', 1014, '2022-11-12 18:05:25', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1019, 'sys-client', '应用管理', 'el-icon-Eleme', '管理所有可 SSO 授权的 url 地址', 'dir', '', '', '', '0', 'auth', '1', '-1', 1015, '2022-11-13 11:48:27', '2022-11-30 03:32:56');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1020, 'sys-client-list', '应用列表', '', '', 'com', '', '@/views/sys-client/sys-client-list.vue', NULL, '0', 'auth', '1', 'sys-client', 1016, '2022-11-13 11:48:52', '2022-11-13 11:49:04');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1021, 'sys-client-add', '应用添加', '', '', 'btn', '', '', '', '0', 'no', '1', 'sys-client', 1017, '2022-11-13 11:50:59', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1022, 'sys-user', '用户管理', 'el-icon-User', '管理 SSO 统一认证的 User 用户', 'dir', '', '', '', '0', 'auth', '1', '-1', 1018, '2022-11-13 11:51:39', '2022-11-30 03:33:42');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1023, 'sys-user-list', '用户列表', '', '', 'com', '', '@/views/sys-user/sys-user-list.vue', NULL, '0', 'auth', '1', 'sys-user', 1019, '2022-11-13 11:51:59', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1024, 'sys-login-log', '登录日志', '', '', 'com', '', '@/views/sys-login-log/sys-login-log-list.vue', NULL, '0', 'auth', '1', 'sys-user', 1021, '2022-11-13 11:52:51', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1025, 'console-plate', '数据走势', '', '无此权限的用户无法进入首页大屏', 'com', '', '@/views/sys-user-sta/console-plate.vue', NULL, '0', 'auth', '1', 'sys-user', 1034, '2022-11-13 11:53:15', '2022-11-30 06:18:50');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1026, 'sys-user-online', '在线用户', 'el-icon-CollectionTag', '查看所有正在登录的 User 用户,提供踢人下线操作', 'dir', '', '', '', '0', 'auth', '1', '-1', 1022, '2022-11-13 11:54:19', '2022-11-30 03:38:00');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1027, 'sys-user-online-list', '在线用户', '', '', 'com', '', '@/views/sys-user-online/sys-user-online-list.vue', NULL, '0', 'auth', '1', 'sys-user-online', 1023, '2022-11-13 11:54:52', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1028, 'sp-config', '系统配置', 'el-icon-Setting', '维护系统全局参数配置、User 用户同步 等', 'dir', '', '', '', '0', 'auth', '1', '-1', 1024, '2022-11-13 11:55:14', '2022-11-30 03:53:34');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1029, 'config-view-info', '系统信息', 'el-icon-Plus', '', 'com', '', '@/sp-views/sp-config/config-view-info.vue', NULL, '0', 'auth', '1', 'sp-config', 1025, '2022-11-13 11:56:02', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1030, 'config-view-server', '全局参数', 'el-icon-Plus', '', 'com', '', '@/sp-views/sp-config/config-view-server.vue', NULL, '0', 'auth', '1', 'sp-config', 1026, '2022-11-13 11:57:34', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1031, 'sp-config-list', '表格视图', 'el-icon-Postcard', '', 'com', '', '@/sp-views/sp-config/sp-config-list.vue', NULL, '0', 'auth', '1', 'sp-config', 1035, '2022-11-13 11:58:00', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1032, 'test', '组件测试', 'el-icon-Scissor', '提供 UI 表单增删改查的封装写法展示', 'dir', '', '@/sp-views/test/data-info.vue', '', '0', 'yes', '1', '-1', 1028, '2022-11-13 11:58:52', '2022-11-30 03:38:39');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1033, 'data-list', '简单列表', 'el-icon-DocumentRemove', '', 'com', '', '@/sp-views/test/data-list.vue', NULL, '0', 'yes', '1', 'test', 1029, '2022-11-13 12:04:10', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1034, 'data-list2', '复杂列表', 'el-icon-DocumentRemove', '', 'com', '', '@/sp-views/test/list-more/data-list2.vue', NULL, '0', 'yes', '1', 'test', 1030, '2022-11-13 12:04:49', NULL);
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1035, 'data-add', '表单提交', 'el-icon-Edit', '', 'com', '', '@/sp-views/test/data-add.vue', NULL, '0', 'yes', '1', 'test', 1031, '2022-11-13 12:05:24', '2022-11-13 12:24:11');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1036, 'data-info', '信息展示', 'el-icon-Finished', '', 'com', '', '@/sp-views/test/data-info.vue', NULL, '0', 'yes', '1', 'test', 1032, '2022-11-13 12:06:00', '2022-11-13 12:26:18');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1050, 'sys-user-list-gc', '用户回收站', '', '', 'com', '', '@/views/sys-user/sys-user-list-gc.vue', NULL, '0', 'auth', '1', 'sys-user', 1020, '2022-11-18 02:48:47', '2022-11-30 03:52:59');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1051, 'config-view-sync-user', '用户同步', 'el-icon-Plus', '', 'com', '', '@/sp-views/sp-config/config-view-sync-user.vue', NULL, '0', 'auth', '1', 'sp-config', 1027, '2022-11-18 10:19:48', '2022-11-30 03:53:21');
+INSERT INTO `sp_menu`(`aid`, `id`, `title`, `icon`, `info`, `type`, `path`, `component_path`, `url`, `is_blank`, `show`, `auth`, `parent_id`, `sort`, `create_time`, `update_time`) VALUES (1052, 'sys-client-visit', '应用访问关系', '', '', 'com', '', '@/views/sys-client-visit/sys-client-visit-list.vue', NULL, '0', 'auth', '1', 'sys-client', 1036, '2022-12-03 11:58:17', NULL);
+
+
+
+
+-- 角色权限对应表  
+drop table if exists sp_role_permission; 
+CREATE TABLE `sp_role_permission` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id号',
+  `role_id` bigint(20) DEFAULT NULL COMMENT '角色ID ',
+  `permission_code` varchar(50) DEFAULT NULL COMMENT '菜单项ID',
+  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色权限中间表';
+
+
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'bas', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'root', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'in-system', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'auth', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'role-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'menu-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'admin-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'admin-add', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sp-admin-login', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'console', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'redis-console', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'apilog-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sql-console', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'form-generator', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-client', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-client-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-client-add', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-client-visit', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-user', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-user-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-user-list-gc', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-login-log', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'console-plate', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-user-online', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sys-user-online-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sp-config', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'config-view-info', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'config-view-server', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'config-view-sync-user', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (1, 'sp-config-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'in-system', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-client', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-client-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-client-add', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-client-visit', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-user', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-user-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-user-list-gc', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-login-log', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'console-plate', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-user-online', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sys-user-online-list', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sp-config', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'config-view-info', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'config-view-server', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'config-view-sync-user', now());
+INSERT INTO `sp_role_permission`(`role_id`, `permission_code`, `create_time`) VALUES (2, 'sp-config-list', now());
+
+
+-- 系统管理员表 
+drop table if exists sp_admin; 
+CREATE TABLE `sp_admin` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id,--主键、自增',
+  `name` varchar(100) NOT NULL COMMENT 'admin名称',
+  `avatar` varchar(500) DEFAULT NULL COMMENT '头像地址',
+  `password` varchar(100) DEFAULT NULL COMMENT '密码',
+  `pw` varchar(50) DEFAULT NULL COMMENT '明文密码',
+  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
+  `role_id` int(11) DEFAULT '11' COMMENT '所属角色id',
+  `status` int(11) DEFAULT '1' COMMENT '账号状态(1=正常, 2=禁用)',
+  `create_by_aid` bigint(20) DEFAULT '-1' COMMENT '创建自哪个管理员',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `login_time` datetime DEFAULT NULL COMMENT '上次登陆时间',
+  `login_ip` varchar(50) DEFAULT NULL COMMENT '上次登陆IP',
+  `login_count` int(11) DEFAULT '0' COMMENT '登陆次数',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE KEY `name` (`name`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='系统管理员表';
+
+INSERT INTO `sp_admin`(`id`, `name`, `avatar`, `password`, `pw`, `role_id`, create_time) 
+VALUES (10001, 'sa', 'http://file.dev33.cn/ssp/avatar1.png', 'E4EF2A290589A23EFE1565BB698437F5', '123456', 1, now()); 
+INSERT INTO `sp_admin`(`id`, `name`, `avatar`, `password`, `pw`, `role_id`, create_time) 
+VALUES (10002, 'admin', 'http://file.dev33.cn/ssp/avatar2.png', '1DE197572C0B23B82BB2F54202E8E00B', 'admin', 2, now()); 
+INSERT INTO `sp_admin`(`id`, `name`, `avatar`, `password`, `pw`, `role_id`, create_time) 
+VALUES (10003, 'uper', 'http://file.dev33.cn/ssp/avatar3.png', '276AE2077ADA0ACEDA51B9D3432E4764', '123123', 11, now()); 
+INSERT INTO `sp_admin`(`id`, `name`, `avatar`, `password`, `pw`, `role_id`, create_time) 
+VALUES (10004, 'sky', 'http://file.dev33.cn/ssp/avatar4.png', 'D8E4064CBDDC70E9B54ED85155160F6F', '123123', 11, now()); 
+
+
+-- 管理员登录日志表 
+drop table if exists sp_admin_login;
+CREATE TABLE `sp_admin_login` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id号',
+  `acc_id` bigint(20) NOT NULL COMMENT '管理员账号id',
+  `acc_token` varchar(300) DEFAULT NULL COMMENT '本次登录Token',
+  `login_ip` varchar(50) DEFAULT NULL COMMENT '登陆IP',
+  `login_address` varchar(127) DEFAULT NULL COMMENT '登录地点',
+  `login_device` varchar(127) DEFAULT NULL COMMENT '客户端设备标识',
+  `login_system` varchar(127) DEFAULT NULL COMMENT '客户端系统标识',
+  `create_time` datetime NOT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT 
+COMMENT='管理员登录日志表';
+
+
+-- 配置信息表   
+drop table if exists sp_config;
+CREATE TABLE `sp_config` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id号',
+  `group_name` varchar(100) NOT NULL COMMENT '配置分组',
+  `name` varchar(100) NOT NULL COMMENT '配置名称',
+  `value` text COMMENT '配置值',
+  `remarks` varchar(255) DEFAULT NULL COMMENT '备注',
+  `create_time` datetime COMMENT '创建时间',
+  `update_time` datetime COMMENT '更新时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  UNIQUE KEY (`name`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='配置信息表';
+
+INSERT INTO `sp_config`() VALUES (0, 'server', 'isAllowRegister', 'true', '是否开放注册', '2022-11-08 08:13:15', NULL);
+INSERT INTO `sp_config`() VALUES (0, 'server', 'userDefaultAvatar', 'http://file.dev33.cn/ssp/user-avatar/1.jpg,http://file.dev33.cn/ssp/user-avatar/2.png,http://file.dev33.cn/ssp/user-avatar/3.jpg,http://file.dev33.cn/ssp/user-avatar/4.jpg,http://file.dev33.cn/ssp/user-avatar/5.jpg,http://file.dev33.cn/ssp/user-avatar/6.jpg,http://file.dev33.cn/ssp/user-avatar/7.jpeg', '用户默认头像', '2022-11-08 08:14:21', '2022-11-08 04:18:59');
+INSERT INTO `sp_config`() VALUES (0, 'server', 'reserveInfo', '预留信息', '预留信息', '2022-11-08 08:14:58', '2022-11-08 04:19:09');
+INSERT INTO `sp_config`() VALUES (0, 'info', 'appName', 'Sa-Sso-Pro 后台', '', '2022-11-08 08:39:40', NULL);
+INSERT INTO `sp_config`() VALUES (0, 'info', 'appLogo', 'http://file.dev33.cn/ssp/ssp-logo-480.png', '', '2022-11-08 08:39:40', '2022-12-02 08:43:00');
+INSERT INTO `sp_config`() VALUES (0, 'info', 'appVersion', 'v1.6.0', '', '2022-11-08 08:39:40', '2022-12-02 08:30:47');
+INSERT INTO `sp_config`() VALUES (0, 'info', 'appIntro', 'Sa-Sso-Pro 后台管理', '', '2022-11-08 08:39:40', '2022-12-02 03:51:52');
+INSERT INTO `sp_config`() VALUES (0, 'info', 'isDynamicInfo', 'false', '', '2022-12-02 08:52:04', NULL);
+INSERT INTO `sp_config`() VALUES (0, 'info', 'appUpdateTime', '2023-1-13', '', '2022-12-02 08:33:16', NULL);
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'isListen', 'true', '', '2022-11-20 23:25:51', '2022-11-24 03:46:34');
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'isBrd', 'false', '', '2022-11-20 23:25:58', '2022-11-24 03:46:28');
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'brdUrlSync', '', '', '2022-11-20 23:27:02', '2022-11-24 03:46:17');
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'brdUrl', 'http://localhost:9012', '', '2022-11-20 23:27:04', NULL);
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'isListenDecrypt', 'true', '', '2022-11-22 20:56:46', '2022-11-24 03:45:32');
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'isBrdEncrypt', 'true', '', '2022-11-22 20:56:53', '2022-11-24 03:45:11');
+INSERT INTO `sp_config`() VALUES (0, 'sync-user', 'listenIpList', '127.0.0.1,192.198.1.102', '', '2022-11-22 21:13:52', '2022-12-17 03:46:17');
+
+
+
+
+-- 系统api请求记录表 
+-- 如果此段脚本执行报错,请将 datetime(3) 改为 datetime 再次执行
+drop table if exists sp_apilog; 
+CREATE TABLE `sp_apilog` (
+  `id` bigint(50) NOT NULL AUTO_INCREMENT COMMENT '请求id',
+  `req_ip` varchar(100) DEFAULT NULL COMMENT '客户端ip',
+  `req_api` varchar(512) DEFAULT NULL COMMENT '请求api',
+  `req_parame` text COMMENT '请求参数',
+  `req_type` varchar(50) DEFAULT NULL COMMENT '请求类型(GET、POST...)',
+  `req_token` varchar(50) DEFAULT NULL COMMENT '请求token',
+  `req_header` text DEFAULT NULL COMMENT '请求header',
+  `res_code` varchar(50) DEFAULT NULL COMMENT '返回-状态码',
+  `res_msg` text COMMENT '返回-信息描述',
+  `res_string` text COMMENT '返回-整个信息字符串形式',
+  `user_id` bigint(20) DEFAULT NULL COMMENT 'user_id',
+  `admin_id` bigint(20) DEFAULT NULL COMMENT 'admin_id',
+  `start_time` datetime(3) DEFAULT NULL COMMENT '请求开始时间',
+  `end_time` datetime(3) DEFAULT NULL COMMENT '请求结束时间',
+  `cost_time` bigint(20) DEFAULT NULL COMMENT '花费时间,单位ms',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='api请求记录表';
+
+
+
+
+
+
+-- ======================================== 登录相关表 ====================================  
+
+-- 用户表 
+drop table if exists sys_user;
+CREATE TABLE `sys_user` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id号 [no]',
+  `username` varchar(30) DEFAULT NULL COMMENT '用户昵称 [t j=like]',
+  `password` varchar(50) DEFAULT NULL COMMENT '账号密码',
+  `pw` varchar(50) DEFAULT NULL COMMENT '明文密码',
+  `avatar` varchar(512) DEFAULT NULL COMMENT '用户头像 [img]',
+  `intro` varchar(1024) DEFAULT NULL COMMENT '个人介绍(签名) [textarea]',
+  `age` int(11) DEFAULT 0 COMMENT '用户年龄 [num]',
+  `sex` int(11) DEFAULT '3' COMMENT '用户性别 (1=男,2=女,3=未知) [j]',
+  `phone` varchar(20) DEFAULT NULL COMMENT '手机号 [num]',
+  `email` varchar(50) DEFAULT NULL COMMENT '用户邮箱',
+  `status` int(11) DEFAULT '1' COMMENT '账号状态(1=正常,2=禁用) [j]',
+  `create_time` datetime NOT NULL COMMENT '创建时间 [date-create]',
+  `login_time` datetime DEFAULT NULL COMMENT '上次登陆时间 [date]',
+  `login_ip` varchar(50) DEFAULT NULL COMMENT '上次登陆IP',
+  `login_count` int(11) DEFAULT '0' COMMENT '登陆次数 [num]',
+  `is_del` int(11) not null DEFAULT 1 COMMENT '是否删除(1=否,2=是) [num]',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='用户表'; 
+
+INSERT INTO `sys_user`(`id`, `username`, `password`, `pw`, `avatar`, `age`, `sex`, `phone`, `create_time`, `is_del`) 
+VALUES (1000001, 'zhang', 'AEDBE3D6D827FE5624FEF050CA9BD429', '123456', 'http://file.dev33.cn/ssp/user-avatar/1.jpg', 10, 1, '15688889999', now(), 1);
+
+INSERT INTO `sys_user`(`id`, `username`, `password`, `pw`, `avatar`, `age`, `sex`, `phone`, `create_time`, `is_del`) 
+VALUES (1000002, 'wangwu', 'F493A5C66A04B4BC7E31718D24B95049', '123456', 'http://file.dev33.cn/ssp/user-avatar/2.png', 10, 1, '13644445555', now(), 1);
+
+INSERT INTO `sys_user`(`id`, `username`, `password`, `pw`, `avatar`, `age`, `sex`, `phone`, `create_time`, `is_del`) 
+VALUES (1000003, 'zhaoliu', '0B0261D9F8A649A8B566D2C2633D2224', '123456', 'http://file.dev33.cn/ssp/user-avatar/3.jpg', 10, 1, '13644446666', now(), 2);
+
+
+ 
+-- 单点登录日志表 
+drop table if exists sys_login_log;
+CREATE TABLE `sys_login_log` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id号 [no]',
+  `user_id` bigint(20) NOT NULL COMMENT '用户id [num click=sys_user.id]', 
+	`acc_token` varchar(300) DEFAULT NULL COMMENT '本次登录Token', 
+  `login_ip` varchar(50) DEFAULT NULL COMMENT '登陆IP', 
+  `login_address` varchar(127) DEFAULT NULL COMMENT '登录地点',
+  `login_device` varchar(127) DEFAULT NULL COMMENT '客户端设备标识',
+  `login_system` varchar(127) DEFAULT NULL COMMENT '客户端系统标识',
+	`client_id` bigint(20) DEFAULT NULL COMMENT '所属应用id', 
+	`client_domain` varchar(300) DEFAULT NULL COMMENT 'Client端域名', 
+  `create_time` datetime NOT NULL COMMENT '创建时间 [date-create]',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT 
+COMMENT='用户登录日志表 [fk-s js=(user_id=sys_user.id), show=username.账号昵称.avatar.头像]'; 
+
+
+-- 应用表 
+drop table if exists sys_client;
+CREATE TABLE `sys_client` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id号 [no]',
+	`name` varchar(50) NOT NULL COMMENT '应用名称', 
+	`logo` varchar(512) COMMENT '应用图标 [img]', 
+	`intro` varchar(512) COMMENT '应用介绍 [textarea]', 
+	`allow_url` varchar(5120) COMMENT '允许授权的url (多个用逗号隔开)', 
+  `is_public` int(11) DEFAULT '1' COMMENT '是否公开(1=公开应用, 2=私有应用) [j]',
+  `status` int(11) DEFAULT '1' COMMENT '应用状态(1=启用, 2=禁用) [j switch=true]',
+  `create_time` datetime NOT NULL COMMENT '创建时间 [date-create]',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1001 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='应用表'; 
+
+
+INSERT INTO `sys_client`(`id`, `name`, `logo`, `intro`, `allow_url`, `status`, `create_time`) 
+VALUES (1001, '凉云商城', 'http://file.dev33.cn/ssp/client1.png', '凉云商城,在线百货,生鲜超市', 'http://sa-sso-client.dev33.cn/sso/login', 1, '2022-01-26 22:42:11');
+INSERT INTO `sys_client`(`id`, `name`, `logo`, `intro`, `allow_url`, `status`, `create_time`) 
+VALUES (1002, '凉云商城(前后台分离版)', 'http://file.dev33.cn/ssp/client2.png', '凉云商城,在线百货', 'http://sa-sso-client-h5.dev33.cn/sso-login.html,http://sa-sso-client-vue2.dev33.cn/*,http://sa-sso-client-vue3.dev33.cn/*', 1, '2022-01-28 20:45:05');
+INSERT INTO `sys_client`(`id`, `name`, `logo`, `intro`, `allow_url`, `status`, `create_time`) 
+VALUES (1003, '凉云短视频', 'http://file.dev33.cn/ssp/client3.png', '凉云短视频,分享美好生活', 'http://sa-sso-client1.com:9001/*,http://sa-sso-client2.com:9001/*,http://sa-sso-client3.com:9001/*', 1, '2022-01-26 22:44:19');
+INSERT INTO `sys_client`(`id`, `name`, `logo`, `intro`, `allow_url`, `status`, `create_time`) 
+VALUES (1004, '测试应用', 'http://file.dev33.cn/ssp/client4.png', '测试应用,本地测试', 'http://127.0.0.1*,http://localhost*', 1, '2022-02-02 06:00:39');
+INSERT INTO `sys_client`(`id`, `name`, `logo`, `intro`, `allow_url`, `is_public`, `status`, `create_time`) 
+VALUES (1005, '测试应用2', 'http://file.dev33.cn/ssp/client5.png', '测试应用,本地测试2', 'http://192.168.1.*', 2, 1, '2022-02-02 06:00:39');
+
+
+
+-- 应用访问关系表,决定一个用户是否可以访问一个应用 
+drop table if exists sys_client_visit; 
+CREATE TABLE `sys_client_visit` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录id',
+  `client_id` bigint(20) DEFAULT NULL COMMENT '应用ID',
+  `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
+  `visit` int(12) DEFAULT 1 COMMENT '所属关系 (1=允许访问,2=禁止访问, 3=跟随应用) [j]',
+  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT 
+COMMENT='应用访问关系表 [fk-s js=(client_id=sys_client.id), show=name.应用名称.logo.应用Logo][fk-s js=(user_id=sys_user.id), show=username.用户昵称.avatar.用户头像]';
+
+insert into sys_client_visit() values (0, 1001, 1000001, 1, now()); 
+insert into sys_client_visit() values (0, 1005, 1000001, 1, now());
+insert into sys_client_visit() values (0, 1001, 1000002, 2, now());
+insert into sys_client_visit() values (0, 1002, 1000002, 2, now()); 
+insert into sys_client_visit() values (0, 1003, 1000002, 3, now()); 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 11 - 0
ssp-admin-vue3/.editorconfig

@@ -0,0 +1,11 @@
+# https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = false
+

+ 4 - 0
ssp-admin-vue3/.env

@@ -0,0 +1,4 @@
+# 环境变量(所有环境)
+
+# 全局标题   -- 此配置仅在 index.html 加载界面有效,更多配置请在 /src/store/setting.js 中更改 
+VITE_ADMIN_TITLE='Sooka-SSO'

+ 7 - 0
ssp-admin-vue3/.env.development

@@ -0,0 +1,7 @@
+# 环境变量(开发环境)
+
+# 项目上下文path
+VITE_PUBLIC_PATH=/
+
+# 后台接口地址 
+VITE_SERVER_URL=http://192.168.10.12:3000

+ 7 - 0
ssp-admin-vue3/.env.production

@@ -0,0 +1,7 @@
+# 环境变量(生产环境)
+
+# 项目上下文path
+VITE_PUBLIC_PATH=/
+
+# 后台接口地址 
+VITE_SERVER_URL=http://192.168.10.12:3000

+ 5 - 0
ssp-admin-vue3/.eslintignore

@@ -0,0 +1,5 @@
+build/*.js
+src/assets
+src/sa-frame
+public
+dist

+ 79 - 0
ssp-admin-vue3/.eslintrc.js

@@ -0,0 +1,79 @@
+// eslint 代码校正(自虐)配置
+module.exports = {
+    // 是否为跟配置文件
+    root: true,
+    // 对环境定义的一组全局变量的预设
+    env: {
+        browser: true,
+        es2021: true,
+        node: true,
+    },
+    // 代码中所有用到的全局变量
+    globals: {
+        // '$': true,
+        // 'console': false
+    },
+    // 解释器
+    parser: 'vue-eslint-parser',
+    // 继承的规则
+    extends: ['plugin:vue/recommended', 'plugin:vue/vue3-essential'],
+    plugins: ['vue'],
+    /**
+	 *	自定义的规则,
+	 *	规则值:
+	 *		off / 0:代表关闭,
+	 *		warn / 1:代表警告,
+	 *		error / 2:代表错误
+	 *	参考:
+	 * 		eslint 规则:http://eslint.cn/docs/rules/
+	 *		vue 配置 eslint :https://eslint.vuejs.org/rules/
+ 	 */
+    rules: {
+        // 是否必须末尾分号,never=不、always=是
+        // semi: [1, 'always'],
+        // 规定代码缩进是几个空格
+        "indent": [1, 4],
+        "vue/html-indent": [1, 4],
+
+        // 关闭组件名风格限制 
+        'vue/component-options-name-casing': 'off',
+        'vue/component-definition-name-casing': 'off',
+        //
+        "vue/require-valid-default-prop": 'off',
+        //
+        'vue/custom-event-name-casing': 'off',
+        'vue/attributes-order': 'off',
+        'vue/one-component-per-file': 'off',
+        'vue/html-closing-bracket-newline': 'off',
+        'vue/max-attributes-per-line': 'off',
+        'vue/multiline-html-element-content-newline': 'off',
+        'vue/singleline-html-element-content-newline': 'off',
+        'vue/attribute-hyphenation': 'off',
+        'vue/html-self-closing': 'off',
+        'vue/no-multiple-template-root': 'off',
+        'vue/require-default-prop': 'off',
+        'vue/no-v-model-argument': 'off',
+        'vue/no-arrow-functions-in-watch': 'off',
+        'vue/no-template-key': 'off',
+        'vue/no-v-html': 'off',
+        'vue/comment-directive': 'off',
+        'vue/no-parsing-error': 'off',
+        'vue/no-deprecated-v-on-native-modifier': 'off',
+        'vue/multi-word-component-names': 'off',
+        'no-useless-escape': 'off',
+        'no-sparse-arrays': 'off',
+        'no-prototype-builtins': 'off',
+        'no-constant-condition': 'off',
+        'no-use-before-define': 'off',
+        'no-restricted-globals': 'off',
+        'no-restricted-syntax': 'off',
+        'generator-star-spacing': 'off',
+        'no-unreachable': 'off',
+        'no-multiple-template-root': 'off',
+        'no-unused-vars': 'off',
+        'no-v-model-argument': 'off',
+        'no-case-declarations': 'off',
+        'no-console': 'off',
+        'no-undef': 'off',
+    },
+};

+ 26 - 0
ssp-admin-vue3/.gitignore

@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.imi

+ 24 - 0
ssp-admin-vue3/README.md

@@ -0,0 +1,24 @@
+# ssp-admin-vue3
+ssp-sso-pro 的后台管理UI界面 (vue3 版本)
+
+
+## 技术栈
+vue3、js、element-plus、vite、pinal、vue-router、axios、echarts、jquery、layer、xlsx、wangeditor、vuedraggable、mitt
+
+
+
+## 运行
+先安装依赖
+``` bat
+npm install --registry=https://registry.npm.taobao.org
+```
+
+运行
+``` bat
+npm run dev
+```
+
+打包
+``` bat
+npm run build
+```

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 40 - 0
ssp-admin-vue3/index.html


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4843 - 0
ssp-admin-vue3/package-lock.json


+ 40 - 0
ssp-admin-vue3/package.json

@@ -0,0 +1,40 @@
+{
+    "name": "vite-project",
+    "private": true,
+    "version": "0.0.0",
+    "scripts": {
+        "dev": "vite --host --port 3000 --open",
+        "build": "vite build",
+        "preview": "vite preview",
+        "eslint-fix": "eslint src/**/*.*  --fix"
+    },
+    "dependencies": {
+        "@element-plus/icons-vue": "^1.1.4",
+        "axios": "^0.26.1",
+        "default-passive-events": "^2.0.0",
+        "echarts": "^5.3.2",
+        "element-plus": "^2.2.20",
+        "jquery": "^3.6.0",
+        "mitt": "^3.0.0",
+        "nprogress": "^0.2.0",
+        "pinia": "^2.0.12",
+        "vue": "^3.2.25",
+        "vue-router": "^4.0.14",
+        "vuedraggable": "^4.1.0",
+        "wangeditor": "^4.7.13",
+        "xlsx": "^0.18.4"
+    },
+    "devDependencies": {
+        "@vitejs/plugin-legacy": "^1.7.1",
+        "@vitejs/plugin-vue": "^2.2.0",
+        "eslint": "^8.12.0",
+        "eslint-plugin-vue": "^8.5.0",
+        "sass": "^1.49.9",
+        "sass-loader": "^12.6.0",
+        "unplugin-auto-import": "^0.6.9",
+        "vite": "^2.9.5",
+        "vite-plugin-html": "^2.1.1",
+        "vite-plugin-vue-setup-extend": "^0.4.0",
+        "vue-eslint-parser": "^8.3.0"
+    }
+}

BIN
ssp-admin-vue3/public/favicon-sj.ico


BIN
ssp-admin-vue3/public/favicon.ico


+ 11 - 0
ssp-admin-vue3/src/App.vue

@@ -0,0 +1,11 @@
+<template>
+    <router-view />
+</template>
+
+<script setup>
+
+</script>
+
+<style>
+
+</style>

BIN
ssp-admin-vue3/src/assets/err-icon.png


BIN
ssp-admin-vue3/src/assets/icon/dyc.png


BIN
ssp-admin-vue3/src/assets/icon/gly.png


BIN
ssp-admin-vue3/src/assets/icon/icon-user.png


BIN
ssp-admin-vue3/src/assets/icon/qiandao.png


BIN
ssp-admin-vue3/src/assets/icon/reg-d.png


BIN
ssp-admin-vue3/src/assets/icon/sys-gzh.png


+ 1 - 0
ssp-admin-vue3/src/assets/login-bg-low.svg

@@ -0,0 +1 @@
+<svg t="1648545854869" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18257" width="800" height="800"><path d="M1024 1024H0v-71c27.8 2.70000001 55.50000001 5.30000001 83.30000001 8 75.10000001 11.90000001 179.6 24.90000001 259.39999998 7 79.8-17.90000001 129.10000001-51.2 222.50000001-63 77.10000001-9.8 156.10000001 27.30000001 204.8 43 66.6 21.50000001 159.70000001 32.2 254 23v53z" p-id="18258" fill="#1672d2"></path></svg>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
ssp-admin-vue3/src/assets/login-icon.svg


BIN
ssp-admin-vue3/src/assets/logo-sj.png


BIN
ssp-admin-vue3/src/assets/logo.png


+ 77 - 0
ssp-admin-vue3/src/components/com-right-box.vue

@@ -0,0 +1,77 @@
+<!-- 鼠标右键弹出的盒子 -->
+<template>
+    <div class="rt-div" :style="state.boxStyle" v-show="state.boxShow" tabindex="-1" @blur="close2()">
+        <div class="rt-div-2">
+            <slot name="default" :row="state.nativeObj" :close="close" :close2="close2" v-if="state.nativeObj"></slot>
+        </div>
+    </div>
+</template>
+
+<script setup name="com-right-box">
+import {nextTick, reactive} from "vue";
+import {useAppStore} from "../store/app";
+const appStore = useAppStore();
+
+// --------------- 所有状态 ---------------
+const state = reactive({
+    boxShow: false, // 右键菜单是否正在显示
+    boxStyle: {		// 右键菜单的 style 样式
+        left: '0px',		// 坐标x
+        top: '0px',			// 坐标y
+        maxHeight: '0px'	// 右键菜单的最高高度 (控制是否展开)
+    },
+    nativeObj: null     // 操作的对象  
+})
+
+// --------------- 所有方法 ---------------
+// 展开右键菜单
+const show = function(event, nativeObj) {
+    // console.log('-------- 打开右键菜单')
+    const e = event || window.event;
+    state.boxStyle.left = (e.clientX + 1) + 'px';	// 设置给坐标x
+    state.boxStyle.top = e.clientY + 'px';		// 设置给坐标y
+    state.nativeObj = nativeObj;
+    state.boxShow = true;	// 显示右键菜单
+    nextTick(function() {
+        const foxHeight = document.querySelector('.rt-div-2').offsetHeight;	// 应该展开多高
+        state.boxStyle.maxHeight = foxHeight + 'px';	// 展开
+        document.querySelector('.rt-div').focus();		// 获得焦点,以被捕获失去焦点事件
+    });
+};
+
+// 关闭右键菜单 - 立即关闭
+const close = function() {
+    state.boxStyle.maxHeight = '0px';
+    state.boxShow = false;
+};
+
+// 关闭右键菜单 - 带动画折叠关闭 (失去焦点和点击取消时调用, 为什么不全部调用这个? 因为其它时候调用这个都太卡了)
+const close2 = function() {
+    state.boxStyle.maxHeight = '0px';
+    // this.rightShow = false;
+};
+
+// 所有开放属性、方法
+defineExpose({show});
+
+</script>
+
+<style scoped lang="scss">
+	
+	/* 右键菜单 样式 */
+	.rt-div {
+		position: fixed;
+		z-index: 2147483647;
+		transition: max-height 0.2s;
+		outline:none;
+		max-height: 0px;
+		overflow: hidden;
+		box-shadow: 1px 1px 2px #000;
+	}
+	.rt-div-2{font-size: 0.8em; padding: 0.5em 0; border: 1px #aaa solid; border-radius: 1px; background-color: #FFF;}
+	.rt-div-2 {
+        :deep(li) {line-height: 2.2em; padding-left: 0.8em; padding-right: 1.4em; cursor: pointer; white-space: nowrap; list-style-type: none;}
+        :deep(li):hover {background-color: #ddd;color: #2D8CF0;}
+        :deep(li) i{margin-left: 3px; margin-right: 5px; transform: translate(0, 1px)}
+    }
+</style>

+ 35 - 0
ssp-admin-vue3/src/components/svg-icon/index.vue

@@ -0,0 +1,35 @@
+<script>
+// 渲染函数:https://v3.cn.vuejs.org/guide/render-function.html
+import { h, resolveComponent } from 'vue';
+
+export default {
+    name: 'svg-icon',
+    props: {
+        // svg 图标组件名字
+        name: {
+            type: String,
+        },
+        // svg 大小
+        size: {
+            type: Number,
+            default: () => 14,
+        },
+        // svg 颜色
+        color: {
+            type: String,
+        },
+    },
+    setup(props) {
+        // 定义变量
+        const linesString = ['https', 'http', '/src', '/assets']; // import.meta.env.VITE_PUBLIC_PATH
+        const onLineStyle = `font-size: ${props.size}px;color: ${props.color}`;
+        const localsStyle = `width: ${props.size}px;height: ${props.size}px`;
+        const eleSetStyle = { class: 'el-icon', style: onLineStyle };
+
+        // 逻辑判断
+        if (props.name?.startsWith('el-icon-')) return () => h('i', eleSetStyle, [props.name === 'el-icon-' ? '' : h(resolveComponent(props.name))]);
+        else if (linesString.find((str) => props.name?.startsWith(str))) return () => h('img', { src: props.name, style: localsStyle });
+        else return () => h('i', { class: props.name, style: onLineStyle });
+    },
+};
+</script>

+ 15 - 0
ssp-admin-vue3/src/init/error-handler.js

@@ -0,0 +1,15 @@
+// 全局异常处理 
+import sa from "../sa-frame/sa";
+
+export const initErrorHandler = function (app) {
+    // 全局错误处理 
+    app.config.errorHandler = function(err, vm, info) {
+        if(err.type === 'sa-error') {
+            return sa.error(err.msg);
+        }
+        console.error(err);
+    }
+}
+
+
+

+ 102 - 0
ssp-admin-vue3/src/init/init-admin.js

@@ -0,0 +1,102 @@
+import sa from '/@/sa-frame/sa';
+import {useUserStore} from "../store/user";
+import router from "../router";
+import {addRoutesByMenuList} from "../router/routes-dynamic";
+import {defineMenu} from "../router/router-util";
+import {useSettingStore} from "../store/setting";
+import {useAppStore} from "../store/app";
+import {menuList} from "../router/menu-list";
+
+
+// Admin 模板初始化函数 
+export default function(jumpPath) {
+    
+    const appStore = useAppStore();
+    let settingStore = useSettingStore();
+    
+    // 获取当前会话信息 
+    sa.ajax("/AccAdmin/getLoginInfo", function (res) {
+
+        // 校验当前登录账号,是否有权限进入后台 
+        if(res.data.perList === undefined || res.data.perList.indexOf('in-system') === -1) {
+            sa.$sys.setCurrUser(res.data.admin);
+            return sa.alert('当前账号暂无进入后台权限', function (){
+                router.push('/login');
+            });
+        }
+
+        // 把账号相关信息,保存到本地 
+        sa.$sys.setCurrUser(res.data.admin);
+        useUserStore().setUserInfo({
+            name: res.data.admin.name,
+            avatar: res.data.admin.avatar
+        });
+
+        // 配置信息
+        sa.$sys.setAppCfg(res.data.appCfg);
+
+        // 保存权限数据
+        sa.setAuth(res.data.perList);
+
+        // 初始化右上角的菜单选项 
+        appStore.setAppInfo(res.data.appCfg);
+        
+        // 初始化菜单, isServerMenu=菜单加载模式, true=从后端请求获取菜单, false=前端定义菜单 (menu-list.js 文件里定义)
+        const beMenuList = settingStore.isServerMenu ? defineMenu(res.data.menuList) : menuList;
+        addRoutesByMenuList(beMenuList);
+        appStore.setMenuList(beMenuList, res.data.perList);
+        
+        if(jumpPath) {
+            router.push(jumpPath);
+        }
+        
+        // 初始化右上角的菜单选项 
+        initDropList();
+
+        // 初始化模板,一定要调用 
+        appStore.init();
+    }, {msg: '加载初始数据...', msgType: 'layer'})
+        
+}
+
+// 初始化右上角的菜单选项 
+const initDropList = function () {
+    const layer = sa.layer;
+    useUserStore().setDropList([
+        {
+            name: '我的资料',
+            click: function (){
+                sa.$page.openAdminInfo();
+            }
+        },
+        {
+            name: '修改资料',
+            click: function () {
+                sa.showModel("修改我的资料", () => import('../sp-views/sp-admin/admin-add'), {
+                    id: sa.$sys.getCurrUser().id,
+                    curr: true
+                })
+            }
+        },
+        {
+            name: '修改密码',
+            click: function () {
+                sa.showModel('修改密码', import('@/sp-views/sp-admin/update-password'))
+                // sa.showIframe('修改密码', 'sa-view-sp/sp-admin/update-password.html', '550px', '350px');
+            }
+        },
+        {
+            name: '退出登录',
+            click: function (){
+                layer.confirm('退出登录?', function() {
+                    sa.ajax('/AccAdmin/doExit', function(res) {
+                        layer.alert('注销成功', function() {
+                            layer.closeAll()
+                            router.push(`/login?redirect=${encodeURIComponent(router.currentRoute.value.fullPath)}`)
+                        })
+                    })
+                });
+            }
+        }
+    ])
+}

+ 16 - 0
ssp-admin-vue3/src/init/init-el-icons.js

@@ -0,0 +1,16 @@
+import * as ElIcons from '@element-plus/icons-vue'
+import SvgIcon from "../components/svg-icon/index.vue";
+
+
+/*
+ * 安装所有 icon
+ *  样例1: <el-icon><el-icon-Apple /></el-icon>
+ *  样例2: <svg-icon name="el-icon-Top" />
+ *  样例3: <el-button type="primary" icon="el-icon-Edit" />
+ */
+export const initElIcons = function (app) {
+    for (const name in ElIcons){
+        app.component(`el-icon-${name}`, ElIcons[name]);
+    }
+    app.component('svg-icon', SvgIcon);
+}

+ 16 - 0
ssp-admin-vue3/src/init/init-sa-form.js

@@ -0,0 +1,16 @@
+// 所有全局组件 
+const modules = import.meta.globEager('./../sa-frame/com/*/*.vue')
+
+// 安装全局表单组件 
+export const initSaForm = function (app) {
+    for (const path in modules) {
+        app.component(getComponentName(path), modules[path].default);
+    }
+}
+
+// 获取组件名
+const getComponentName = function (path) {
+    const start = path.lastIndexOf('/');
+    const end = path.lastIndexOf('.');
+    return path.substring(start + 1, end);
+}

+ 142 - 0
ssp-admin-vue3/src/layout/index.vue

@@ -0,0 +1,142 @@
+<!-- layout 首页 -->
+<template>
+    <div
+        v-if="appStore.isInit"
+        class="layout-index"
+        :class="['theme-def', 'theme-' + themeStore.theme, (appStore.isOpen ? '' : 'app-fold')]">
+
+        <transition name="opacity">
+            <!-- 经典布局 -->
+            <layout-classic v-if=" themeStore.layoutMode === 'classic' "></layout-classic>
+            <!-- 分栏布局 -->
+            <layout-column v-else-if=" themeStore.layoutMode === 'column' "></layout-column>
+        </transition>
+
+        <!-- 全局设置 -->
+        <com-setting></com-setting>
+
+        <!-- 全局 Dialog 弹窗 -->
+        <os-model></os-model>
+
+        <!-- 全局 loading 加载图标 -->
+        <os-loading></os-loading>
+        
+    </div>
+</template>
+
+<script setup name="layout-index">
+import LayoutClassic from './main/layout-classic'
+import LayoutColumn from './main/layout-column'
+import ComSetting from './nav/com-setting';      // 全局设置组件
+import {useAppStore} from "../store/app";
+import {useThemeStore} from "../store/theme";
+import OsModel from "./view/os-model";
+import OsLoading from "./view/os-loading";
+import {onMounted} from "vue";
+import {getRouteByPath} from "../router/router-util";
+import router from "../router";
+
+//
+const appStore = useAppStore();
+const themeStore = useThemeStore();
+
+onMounted(() => {
+    // 根据路由高亮菜单
+    const route = router.currentRoute.value;
+    let routeObj = getRouteByPath(route.path);
+    if (routeObj?.meta?.menu) {
+        useAppStore().showMenuById(routeObj.meta.menu.id);
+    }
+})
+
+</script>
+
+<style scoped lang="scss">
+
+/* 变量 */
+.layout-index{
+    --nav-left-width: 200px;
+    --nav-left-width-fold: 64px;
+    --nav-right-1-height: 50px;
+    --nav-right-2-height: 35px;
+}
+
+/* 布局样式 */
+.layout-index :deep(div) {
+
+    .nav-left, .nav-right {
+        position: fixed;
+        top: 0;
+        height: 100%;
+    }
+
+    /* 左边 */
+    .nav-left {
+        width: var(--nav-left-width);
+        left: 0px;
+        z-index: 200;
+        overflow: hidden;
+    }
+
+    .nav-left-top {
+        width: 100%;
+        box-sizing: border-box;
+        height: 60px;
+        line-height: 60px; /* z-index: 100; */
+        overflow: hidden;
+    }
+
+    .nav-left-bottom {
+        width: 100%;
+        box-sizing: border-box;
+        height: calc(100% - 60px);
+        overflow: hidden;
+    }
+
+    /* 右边 */
+    .nav-right {
+        width: calc(100% - var(--nav-left-width));
+        right: 0px;
+        z-index: 100;
+    }
+
+    .nav-right-1 {
+        height: var(--nav-right-1-height);
+        line-height: var(--nav-right-1-height);
+        z-index: 201;
+        position: relative;
+        /*border-bottom: 1px #F1F1F1 solid;*/
+        box-sizing: border-box;
+        overflow: hidden;
+        box-shadow: 0 1px 4px rgba(0,21,41,.08);
+    }
+
+    .nav-right-2 {
+        height: var(--nav-right-2-height);
+        //line-height: var(--nav-right-2-height);
+        z-index: 200;
+        position: relative;
+        /*box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);*/
+        /*border-bottom: 1px solid #d8dce5;*/
+        box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+    }
+
+    .nav-right-3 {
+        height: calc(100vh - var(--nav-right-1-height) - var(--nav-right-2-height));
+        z-index: 199;
+        position: relative;
+        overflow: hidden;
+    }
+
+    /* 所有带动画的元素 */
+    .nav-left, .nav-left-top, .nav-right {transition: all 0.3s;}
+    .nav-left-bottom{transition: all 0.4s;}
+
+}
+
+/* 折叠时的样式 */
+.app-fold {
+    --nav-left-width: 64px;
+}
+
+</style>

+ 38 - 0
ssp-admin-vue3/src/layout/main/layout-classic.vue

@@ -0,0 +1,38 @@
+<!-- 经典布局 -->
+<template>
+    <div class="layout-classic">
+        <!-- 左 -->
+        <div class="nav-left">
+            <!-- logo部分 -->
+            <nav-logo class="nav-left-top"></nav-logo>
+            <!-- 左下:菜单 -->
+            <transition name="left-jump-right">
+                <nav-menu-bar class="nav-left-bottom" v-if="appStore.isOpen"></nav-menu-bar>
+                <nav-menu-bar class="nav-left-bottom" v-else></nav-menu-bar>
+            </transition>
+        </div>
+        <!-- 右 -->
+        <div class="nav-right">
+            <!-- 工具栏 -->
+            <nav-tool-bar class="nav-right-1"></nav-tool-bar>
+            <!-- Tab栏 -->
+            <nav-tab-bar class="nav-right-2"></nav-tab-bar>
+            <!-- 视图容器 -->
+            <nav-view-vessel class="nav-right-3"></nav-view-vessel>
+        </div>
+    </div>
+</template>
+
+<script setup name="layout-classic">
+import NavLogo from './../nav/nav-logo';				// logo
+import NavMenuBar from './../nav/nav-menu-bar';		// 菜单栏
+import NavToolBar from './../nav/nav-tool-bar';		// 工具栏
+import NavTabBar from './../nav/nav-tab-bar';		// tab栏
+import NavViewVessel from './../nav/nav-view-vessel';
+import {useAppStore} from "../../store/app";	// 视图容器
+const appStore = useAppStore();
+</script>
+
+<style scoped>
+
+</style>

+ 75 - 0
ssp-admin-vue3/src/layout/main/layout-column.vue

@@ -0,0 +1,75 @@
+<!-- 分栏布局 -->
+<template>
+    <div class="layout-column" :class="appStore.isOpen ? '' : 'layout-column-fold'">
+        <!-- 左 -->
+        <div class="nav-left">
+            <!-- 分栏 - 顶级导航栏 -->
+            <nav-cmb-left class="nav-cmb-left"></nav-cmb-left>
+            <!-- 分栏 - 次级导航栏 -->
+            <transition name="left-jump-right">
+                <nav-cmb-right class="nav-cmb-right" v-if="appStore.isOpen"></nav-cmb-right>
+            </transition>
+        </div>
+        <!-- 右 -->
+        <div class="nav-right">
+            <!-- 工具栏 -->
+            <nav-tool-bar class="nav-right-1"></nav-tool-bar>
+            <!-- Tab栏 -->
+            <nav-tab-bar class="nav-right-2"></nav-tab-bar>
+            <!-- 视图容器 -->
+            <nav-view-vessel class="nav-right-3"></nav-view-vessel>
+        </div>
+    </div>
+</template>
+
+<script setup name="layout-column">
+
+import NavCmbLeft from './nav-column/nav-cmb-left';		// 顶级菜单栏
+import NavCmbRight from './nav-column/nav-cmb-right';		// 次级菜单栏
+import NavToolBar from './../nav/nav-tool-bar';		// 工具栏
+import NavTabBar from './../nav/nav-tab-bar';		// tab栏
+import NavViewVessel from './../nav/nav-view-vessel';	// 视图容器
+import {useAppStore} from "../../store/app";
+const appStore = useAppStore();
+
+</script>
+
+<style scoped lang="scss">
+  .layout-column{
+    --nav-left-width: 264px;
+  }
+  .nav-left{
+    box-sizing: border-box;
+    border-right: 1px #DDD solid;
+    background-color: #FFF !important;
+  }
+
+  .nav-cmb-left{
+    position: absolute;
+    z-index: 60;
+    left: 0;
+    width: var(--nav-left-width-fold);
+    height: 100%;
+    box-sizing: border-box;
+    background-color: var(--menu-bg-color);
+  }
+  .nav-cmb-right{
+    position: absolute;
+    z-index: 50;
+    left: var(--nav-left-width-fold);
+    width: calc(var(--nav-left-width) - var(--nav-left-width-fold));
+    height: 100%;
+  }
+
+  // 折叠时
+  .layout-column.layout-column-fold{
+    .nav-left{
+      width: 64px;
+      overflow: hidden;
+    }
+    .nav-right{
+      width: calc(100% - 64px);
+    }
+  }
+
+</style>

+ 131 - 0
ssp-admin-vue3/src/layout/main/nav-column/nav-cmb-left.vue

@@ -0,0 +1,131 @@
+<!-- 分栏布局下,顶级菜单栏 -->
+<template>
+    <div>
+        <!-- Logo 部分 -->
+        <div class="nav-left-top" :title="settingStore.title" @click="appStore.showHome()">
+            <img :src="settingStore.logo" class="admin-logo" alt="logo">
+        </div>
+        <!-- 菜单部分 -->
+        <el-scrollbar class="top-menu-box">
+            <template v-for="menu in appStore.menuList" :key="menu.id">
+                <div
+                    class="top-menu-item"
+                    :class="{'top-menu-item-active': layoutColumnStore.activeTopMenu && menu.id === layoutColumnStore.activeTopMenu.id}"
+                    v-if="menuIsShow(menu)"
+                    @click="selectTopMenu(menu)">
+                    <svg-icon class="menu-i" :name="menu.icon" :title="menu.title"></svg-icon>
+                    <p>{{ menu.title }}</p>
+                </div>
+            </template>
+        </el-scrollbar>
+    </div>
+</template>
+
+<script setup name="nav-cmb-left">
+import { useAppStore } from "../../../store/app";
+import {useLayoutColumnStore} from "../../../store/layoutColumn";
+import {useSettingStore} from "../../../store/setting";
+import {watch, nextTick} from "vue";
+import {homeTab} from "../../../router/home";
+import {menuIsShow} from '../../../router/router-util';
+const layoutColumnStore = useLayoutColumnStore();
+const appStore = useAppStore();
+const settingStore = useSettingStore();
+
+// 点击菜单时
+const selectTopMenu = function(menu) {
+    if(layoutColumnStore.activeTopMenu === menu) {
+        return;
+    }
+    layoutColumnStore.activeTopMenu = menu;
+    // 动画效果 
+    appStore.navCmbRightShow = false;
+    nextTick(function (){
+        appStore.navCmbRightShow = true;
+    })
+};
+
+// 刷新 top-menu 选中信息
+const f5SelectTopMenu = function() {
+    // 如果是首屏
+    if(appStore.nativeTab.id === homeTab.id) {
+        return;
+    }
+
+    // 否则从搜索列表中搜寻
+    for (let item of appStore.searchList) {
+        if(item.id === appStore.nativeTab.id) {
+            const topMenu = item.level[0];
+            return selectTopMenu(topMenu);
+        }
+    }
+
+    // 如果遍历全局没有找到
+    // return;
+}
+
+// 监听 tab 切换,刷新一下
+watch(() => appStore.nativeTab, () => {
+    f5SelectTopMenu();
+})
+
+// 组件加载时触发一下
+nextTick(function (){
+    f5SelectTopMenu();
+})
+
+
+</script>
+
+<style scoped lang="scss">
+// logo部分
+.nav-left-top{
+  cursor: pointer;
+  border-right: 0 !important;
+}
+.admin-logo {
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  vertical-align: middle;
+  margin-left: 18px;
+}
+
+// 菜单部分
+.top-menu-box{
+  //background-color: red;
+}
+.top-menu-item{
+  height: 50px;
+  text-align: center;
+  line-height: 20px;
+  white-space: nowrap;
+  transition: all 0.2s;
+  .menu-i{
+    margin-top: 9px;
+  }
+  p{
+    font-size: 12px;
+    margin-top: -4px;
+  }
+}
+
+.top-menu-item:hover{
+  background-color: var(--menu-hover-bg-color);
+  cursor: pointer;
+}
+.top-menu-item.top-menu-item-active{
+  background-color: var(--menu-active-bg-color);
+  color: var(--menu-active-color);
+}
+
+
+
+
+
+
+
+
+
+
+</style>

+ 176 - 0
ssp-admin-vue3/src/layout/main/nav-column/nav-cmb-right.vue

@@ -0,0 +1,176 @@
+<!-- 分栏布局下,次级菜单栏 -->
+<template>
+    <div>
+        <el-scrollbar class="nav-sub-menu-box">
+            <!-- logo部分 -->
+            <div class="nav-left-top" :title="settingStore.title" @click="appStore.showHome()">
+                <span class="admin-title">{{ settingStore.title }} 后台</span>
+            </div>
+            <!-- 分割线 -->
+            <div>
+                <el-divider class="nav-smb-divider">
+                    <span v-if="layoutColumnStore.activeTopMenu">{{ layoutColumnStore.activeTopMenu.title }}</span>
+                    <span v-else>选择菜单</span>
+                </el-divider>
+            </div>
+            <!-- 次级菜单部分 -->
+            <transition name="left-jump-right">
+                <div v-if="appStore.navCmbRightShow" class="nav-cmb-right-menu-box">
+                    <el-menu
+                        :unique-opened="true"
+                        :default-active="appStore.activeMenuId"
+                        @select="selectMenu"
+                        v-if="layoutColumnStore.activeTopMenu"
+                    >
+                        <template v-for="(menu) in layoutColumnStore.activeTopMenu.children">
+                            <!-- 1 如果是子菜单 -->
+                            <el-menu-item v-if="menuIsShow(menu) && !menuIsDir(menu)" :index="menu.id" :key="menu.id">
+                                <svg-icon class="menu-i" :name="menu.icon" :title="menu.title"></svg-icon>
+                                <span class="menu-name">{{ menu.title }}</span>
+                            </el-menu-item>
+                            <!-- 1 如果是父菜单 -->
+                            <el-sub-menu v-if="menuIsShow(menu) && menuIsDir(menu)" :index="menu.id" :key="menu.id">
+                                <template #title>
+                                    <svg-icon class="menu-i" :name="menu.icon" :title="menu.title"></svg-icon>
+                                    <span class="menu-name">{{ menu.title }}</span>
+                                </template>
+                                <!-- 遍历其子项 -->
+                                <template v-for="(menu2) in menu.children">
+                                    <!-- 2 如果是子菜单 -->
+                                    <el-menu-item v-if="menuIsShow(menu2) && !menuIsDir(menu2)" :index="menu2.id" :key="menu2.id">
+                                        <svg-icon class="menu-i" :name="menu2.icon" :title="menu2.title"></svg-icon>
+                                        <span class="menu-name">{{ menu2.title }}</span>
+                                    </el-menu-item>
+                                    <!-- 2 如果是父菜单 -->
+                                    <el-sub-menu v-if="menuIsShow(menu2) && menuIsDir(menu2)" :index="menu2.id" :key="menu2.id">
+                                        <template #title>
+                                            <svg-icon class="menu-i" :name="menu2.icon" :title="menu2.title"></svg-icon>
+                                            <span class="menu-name">{{ menu2.title }}</span>
+                                        </template>
+                                        <!-- 遍历其子项 -->
+                                        <template v-for="(menu3) in menu2.children">
+                                            <!-- 3 如果是子菜单 -->
+                                            <el-menu-item v-if="menuIsShow(menu3) && !menuIsDir(menu3)" :index="menu3.id" :key="menu3.id">
+                                                <svg-icon class="menu-i" :name="menu3.icon" :title="menu3.title"></svg-icon>
+                                                <span class="menu-name">{{ menu3.title }}</span>
+                                            </el-menu-item>
+                                            <!-- 3 如果是父菜单 -->
+                                            <el-sub-menu v-if="menuIsShow(menu3) && menuIsDir(menu3)" :index="menu3.id" :key="menu3.id">
+                                                <template #title>
+                                                    <svg-icon class="menu-i" :name="menu3.icon" :title="menu3.title"></svg-icon>
+                                                    <span class="menu-name">{{ menu3.title }}</span>
+                                                </template>
+                                                <!-- 4 -->
+                                                <template v-for="(menu4) in menu3.children">
+                                                    <el-menu-item v-if="menuIsShow(menu4)" :index="menu4.id" :key="menu4.id">
+                                                        <svg-icon class="menu-i" :name="menu4.icon" :title="menu4.title"></svg-icon>
+                                                        <span class="menu-name">{{ menu4.title }}</span>
+                                                    </el-menu-item>
+                                                </template>
+                                            </el-sub-menu>
+                                        </template>
+                                    </el-sub-menu>
+                                </template>
+                            </el-sub-menu>
+                        </template>
+                    </el-menu>
+                </div>
+            </transition>
+        </el-scrollbar>
+    </div>
+</template>
+
+<script setup name="nav-cmb-right">
+import SvgIcon from "../../../components/svg-icon";
+import { useAppStore } from "../../../store/app";
+import {useSettingStore} from "../../../store/setting";
+import {useLayoutColumnStore} from "../../../store/layoutColumn";
+const settingStore = useSettingStore();
+const appStore = useAppStore();
+const layoutColumnStore = useLayoutColumnStore();
+import {menuIsShow, menuIsDir} from '../../../router/router-util';
+
+
+// 点击子菜单时触发的回调
+// 参数:index=点击菜单index标识(不是下标,是菜单id),
+// indexArray=所有已经打开的菜单id数组,形如:['1', '1-1', '1-1-1']
+const selectMenu = function(index, indexArray) {
+    appStore.showMenuById(index);
+};
+
+</script>
+
+<style scoped lang="scss">
+// 样式变量 (把深色布局改为浅色布局)
+.nav-sub-menu-box{
+    background-color: #FFF;
+    color: #333;
+
+    --menu-bg-color-2: #fafafa;	/* 二级菜单 - 背景色 */
+    --menu-hover-bg-color: var(--t-light-color);			/* 菜单悬浮 - 背景色 */
+    --menu-active-bg-color: var(--t-light-color);		/* 菜单选中 - 背景色 */
+    --menu-active-color: var(--menu-active-bg-color-copy2);				/* 菜单选中 - 文字色 */
+}
+.nav-sub-menu-box{
+    //border-right: 111px #DDD solid;
+}
+.nav-cmb-right-menu-box{width: 100%; transition: all 0.5s !important;}
+
+// logo 部分
+.nav-left-top{
+    border-right: 0 !important;
+    text-align: center;
+    cursor: pointer;
+    .admin-title{
+        font-size: 1.1em;
+        vertical-align: middle;
+    }
+}
+
+// 分割线
+.nav-smb-divider{
+    margin-top: 10px;
+    :deep(.el-divider__text){
+        color: #888;
+        font-size: 13px;
+    }
+}
+
+// 菜单
+.nav-sub-menu-box .menu-i{display: inline-block; vertical-align: middle; width: 24px; font-size: 16px !important; position: relative; top: -1px;}
+.nav-sub-menu-box .menu-i{margin-right: 0px;}
+.nav-sub-menu-box .menu-name{margin-left: 5px;}
+
+/* 动画速度加快 */
+.nav-sub-menu-box :deep(*){transition: all 0.3s;}
+
+.nav-sub-menu-box :deep(*) {
+
+    /* 避免菜单文字居中显示带来的不和谐现象 */
+    .el-menu-item{display: block;}
+
+    /* 隐藏右边框 */
+    .el-menu{border: 0px;}
+
+    /* 一级菜单,高度45px */
+    .el-menu-item,
+    .el-sub-menu__title{height: 45px !important; line-height: 45px !important;}
+
+    /* 二级以下菜单,高度40px */
+    .el-sub-menu .el-menu-item,
+    .el-sub-menu .el-sub-menu .el-sub-menu__title{height: 40px !important; line-height: 40px !important;}
+
+    /* 二级菜单 左边距 */
+    .el-sub-menu .el-menu-item,
+    .el-sub-menu .el-sub-menu .el-sub-menu__title{padding-left: 2.5em !important;}
+
+    /* 三级菜单 左边距 */
+    .el-sub-menu .el-sub-menu .el-menu-item,
+    .el-sub-menu .el-sub-menu .el-sub-menu .el-sub-menu__title{padding-left: 3.6em !important;}
+
+    /* 四级菜单 左边距 */
+    .el-sub-menu .el-sub-menu .el-sub-menu .el-menu-item{padding-left: 4.7em !important;}
+
+}
+
+</style>

+ 157 - 0
ssp-admin-vue3/src/layout/nav/com-right-menu.vue

@@ -0,0 +1,157 @@
+<template>
+    <!-- 鼠标右键弹出的盒子 -->
+    <!-- 【向下展开动画,坐标平移动画】二者只可得其一 -->
+    <div class="right-box" :style="state.rightStyle" v-show="state.rightShow" tabindex="-1" @blur="right_closeMenu2()">
+        <div class="right-box-2">
+            <div @click="right_closeMenu(); right_f5()"><el-icon><el-icon-RefreshRight /></el-icon>刷新</div>
+            <div @click="right_closeMenu(); right_close()"><el-icon><el-icon-Close /></el-icon>关闭</div>
+            <div @click="right_closeMenu(); right_close_other()"><el-icon><el-icon-Close /></el-icon>关闭其它</div>
+            <div @click="right_closeMenu(); right_close_all()"><el-icon><el-icon-Close /></el-icon>关闭所有</div>
+            <div @click="right_closeMenu(); right_window_open()" v-if="state.rightTab && state.rightTab.url"><el-icon><el-icon-Plus /></el-icon>新窗口打开</div>
+            <div @click="right_closeMenu2();"><el-icon><el-icon-Minus /></el-icon>取消</div>
+        </div>
+    </div>
+</template>
+
+<script setup name="com-right-menu">
+
+import {ref, nextTick, reactive} from "vue";
+import {useAppStore} from "../../store/app";
+import {ElMessage} from "element-plus";
+import mitt from "@/mitt";
+const appStore = useAppStore();
+
+// --------------- 所有状态
+const state = reactive({
+    rightShow: false, // 右键菜单是否正在显示
+    rightTab: null, // 右键菜单正在操作的 tab
+    rightStyle: {		// 右键菜单的 style 样式
+        left: '0px',		// 坐标x
+        top: '0px',			// 坐标y
+        maxHeight: '0px'	// 右键菜单的最高高度 (控制是否展开)
+    },
+})
+
+
+// --------------- 所有方法
+// 展开右键菜单
+const right_showMenu = function(tab, event) {
+    state.rightTab = tab;	// 绑定操作tab
+    const e = event || window.event;
+    state.rightStyle.left = (e.clientX + 1) + 'px';	// 设置给坐标x
+    state.rightStyle.top = e.clientY + 'px';		// 设置给坐标y
+    state.rightShow = true;	// 显示右键菜单
+    nextTick(function() {
+        const foxHeight = document.querySelector('.right-box-2').offsetHeight;	// 应该展开多高
+        state.rightStyle.maxHeight = foxHeight + 'px';	// 展开
+        document.querySelector('.right-box').focus();		// 获得焦点,以被捕获失去焦点事件
+    });
+};
+
+// 关闭右键菜单 - 立即关闭
+const right_closeMenu = function() {
+    state.rightStyle.maxHeight = '0px';
+    state.rightShow = false;
+};
+
+// 关闭右键菜单 - 带动画折叠关闭 (失去焦点和点击取消时调用, 为什么不全部调用这个? 因为其它时候调用这个都太卡了)
+const right_closeMenu2 = function() {
+    state.rightStyle.maxHeight = '0px';
+    // this.rightShow = false;
+};
+
+// 右键 - 刷新
+const right_f5 = function() {
+    appStore.f5Tab(state.rightTab);
+};
+
+// 右键 - 新窗口打开
+const right_window_open = function() {
+    appStore.newWinTab(state.rightTab);
+};
+
+// 右键 - 关闭
+const right_close = function() {
+    if(state.rightTab === appStore.homeTab){
+        return ElMessage({
+            dangerouslyUseHTMLString: true,
+            message: '<b>这个不能关闭哦</b>',
+            type: 'warning',
+            showClose: true,
+        });
+    }
+    appStore.closeTab(state.rightTab);
+};
+
+// 右键 - 关闭其它
+const right_close_other = function() {
+    // 先滑到最左边
+    mitt.emit('setScroll', 0);
+    // 递归删除
+    let i = 0;
+    const deleteFn = function() {
+        // 如果已经遍历全部
+        if(i >= appStore.tabList.length) {
+            return;
+        }
+        // 如果在白名单,i++继续遍历, 如果不是,递归删除
+        const tab = appStore.tabList[i];
+        if(tab === appStore.homeTab || tab === state.rightTab){
+            i++;
+            deleteFn();
+        } else {
+            appStore.closeTab(tab, function() {
+                deleteFn();
+            });
+        }
+    }.bind(this);
+    deleteFn();
+};
+
+// 右键 - 关闭所有
+const right_close_all = function() {
+    // 先滑到最左边
+    mitt.emit('setScroll', 0);
+    // 递归删除
+    let i = 0;
+    const deleteFn = function() {
+    // 如果已经遍历全部
+        if(i >= appStore.tabList.length) {
+            return;
+        }
+        // 如果在白名单,i++继续遍历, 如果不是,递归删除
+        const tab = appStore.tabList[i];
+        if(tab === appStore.homeTab){
+            i++;
+            deleteFn();
+        } else {
+            appStore.closeTab(tab, function() {
+                deleteFn();
+            });
+        }
+    };
+    deleteFn();
+};
+
+// 所有开放属性、方法
+defineExpose({right_showMenu});
+
+</script>
+
+<style scoped>
+	
+	/* 右键菜单 样式 */
+	.right-box {
+		position: fixed;
+		z-index: 2147483647;
+		transition: max-height 0.2s;
+		outline:none;
+		max-height: 0px;
+		overflow: hidden;
+		box-shadow: 1px 1px 2px #000;
+	}
+	.right-box-2{font-size: 0.8em; padding: 0.5em 0; border: 1px #aaa solid; border-radius: 1px; background-color: #FFF;}
+	.right-box-2>div {line-height: 2.2em; padding-left: 0.8em; padding-right: 1.4em; cursor: pointer; white-space: nowrap;}
+	.right-box-2>div:hover {background-color: #ddd;color: #2D8CF0;}
+	.right-box-2>div i{ margin-right: 5px; transform: translate(0, 1px)}
+</style>

+ 69 - 0
ssp-admin-vue3/src/layout/nav/com-setting.vue

@@ -0,0 +1,69 @@
+<!-- 全局设置组件 -->
+<template>
+    <div class="setting-drawer-box">
+        <el-drawer
+            v-model="appStore.isShowSetting"
+            title="全局设置"
+            size="300px"
+        >
+            <div style="height: 100%">
+                <el-scrollbar>
+                    <div style="padding: 0px 20px;">
+                        <ComSettingLayoutMode></ComSettingLayoutMode>
+                        <ComSettingTheme></ComSettingTheme>
+                        <ComSettingTabStyle></ComSettingTabStyle>
+                        <ComSettingSwitch></ComSettingSwitch>
+                        <ComSettingMore></ComSettingMore>
+
+                        <el-divider></el-divider>
+                        <div class="btn-box">
+                            <el-button icon="el-icon-Refresh" style="width: 100%;" @click="reset">一键重置</el-button>
+                        </div>
+                    </div>
+                </el-scrollbar>
+            </div>
+        </el-drawer>
+    </div>
+</template>
+
+<script setup name="com-setting">
+import ComSettingTheme from './com-setting/com-setting-theme';
+import ComSettingSwitch from './com-setting/com-setting-switch';
+import ComSettingTabStyle from './com-setting/com-setting-tab-style';
+import ComSettingLayoutMode from './com-setting/com-setting-layout-mode';
+import ComSettingMore from './com-setting/com-setting-more';
+import {useAppStore} from "../../store/app";
+import {watch} from "vue";
+import {useThemeStore} from "../../store/theme";
+import {ElMessage} from "element-plus";
+const appStore = useAppStore();
+const themeStore = useThemeStore();
+
+// 监听 themeStore,用户改动时,缓存下来
+watch(themeStore, () => {
+    localStorage.setItem('user-layout-theme', JSON.stringify(themeStore.$state));
+})
+
+// 恢复默认
+const reset = function () {
+    themeStore.resetTheme();
+    ElMessage({message: '重置成功'})
+}
+
+</script>
+
+<style scoped lang="scss">
+.setting-drawer-box{
+  :deep(.el-drawer__header){
+    margin-bottom: 25px !important;
+  }
+  :deep(.el-drawer__body){
+    padding: 0 !important;
+    overflow: hidden;
+  }
+}
+.btn-box{
+  .el-button{border-radius: 2px !important;}
+  padding-bottom: 50px;
+}
+</style>

+ 80 - 0
ssp-admin-vue3/src/layout/nav/com-setting/com-setting-layout-mode.vue

@@ -0,0 +1,80 @@
+<!-- 全局设置:layout 布局模式 -->
+<template>
+    <div class="setting-item-box">
+        <el-divider content-position="left">布局方式</el-divider>
+        <div style="padding-top: 10px; padding-bottom: 20px">
+
+            <!-- ------- 经典布局 ------- -->
+            <div
+                class="layout-mode-item" :class="themeStore.layoutMode === 'classic' ? 'layout-mode-item-active' : ''"
+                @click="themeStore.layoutMode = 'classic' ">
+                <div class="mode-name">经典</div>
+                <el-container style="height: 50px;">
+                    <el-aside width="15px" style=" background-color: #333;"></el-aside>
+                    <el-container>
+                        <el-header height="8px" style="background-color: #fafafa;"></el-header>
+                        <el-main style="background-color: #eee;"></el-main>
+                    </el-container>
+                </el-container>
+            </div>
+
+            <!-- ------- 分栏布局 ------- -->
+            <div
+                class="layout-mode-item" :class="themeStore.layoutMode === 'column' ? 'layout-mode-item-active' : ''"
+                @click="themeStore.layoutMode = 'column' ">
+                <div class="mode-name">分栏</div>
+                <el-container style="height: 50px;">
+                    <el-aside width="8px" style=" background-color: #333;"></el-aside>
+                    <el-aside width="15px" style=" background-color: #FFF;"></el-aside>
+                    <el-container>
+                        <el-header height="8px" style="background-color: #fafafa;"></el-header>
+                        <el-main style="background-color: #eee;"></el-main>
+                    </el-container>
+                </el-container>
+            </div>
+
+        </div>
+    </div>
+</template>
+
+<script setup name="com-setting-layout-mode">
+import {useThemeStore} from "../../../store/theme";
+const themeStore = useThemeStore();
+
+</script>
+
+<style scoped lang="scss">
+
+.setting-item-box{
+  .layout-mode-item{
+    width: 70px;
+    height: 50px;
+    display: inline-block;
+    margin-right: 10px;
+    border: 1px #e5e5e5 solid;
+    cursor: pointer;
+    transition: all 0.2s;
+    position: relative;
+    border-radius: 1px;
+    .mode-name{
+      position: absolute;
+      line-height: 55px;
+      text-align: center;
+      width: 100%;
+      font-size: 12px;
+      color: #bbb;
+      transition: all 0.2s;
+      text-indent: 1em;
+    }
+  }
+  .layout-mode-item-active{
+    border: 1px #2d8cf0 solid;
+    box-shadow:  0 0 5px #a6c5e7 inset;
+    .mode-name{
+      color: #2d8cf0;
+    }
+  }
+
+}
+
+</style>

+ 46 - 0
ssp-admin-vue3/src/layout/nav/com-setting/com-setting-more.vue

@@ -0,0 +1,46 @@
+<!-- 全局设置:其它配置  -->
+<template>
+    <div class="setting-item-box">
+        <el-divider content-position="left">其它配置</el-divider>
+        <div style="padding-top: 5px; padding-bottom: 20px;">
+            <el-form label-position="left">
+                <el-form-item label="灰色模式 ">
+                    <el-switch v-model="themeStore.greyMode" @change="themeStore.greyModeChange(themeStore.greyMode)" />
+                </el-form-item>
+                <el-form-item label="色弱模式 ">
+                    <el-switch v-model="themeStore.weakMode" @change="themeStore.weakModeChange(themeStore.weakMode)" />
+                </el-form-item>
+                <el-form-item label="刷新按钮 ">
+                    <el-switch v-model="themeStore.isShowRefresh" />
+                </el-form-item>
+                <el-form-item label="当前时间 ">
+                    <el-switch v-model="themeStore.isShowDateTime" />
+                </el-form-item>
+                <el-form-item label="面包屑栏 ">
+                    <el-switch v-model="themeStore.isShowBreadcrumb" />
+                </el-form-item>
+            </el-form>
+        </div>
+    </div>
+</template>
+
+<script setup name="com-setting-more">
+import {useThemeStore} from "../../../store/theme";
+const themeStore = useThemeStore();
+
+</script>
+
+<style scoped lang="scss">
+
+.setting-item-box{
+  .el-button-group>.el-button{
+    padding: 12px 12px;
+  }
+  .el-button-group>.el-button:first-child{border-radius: 1px 0 0 1px !important;}
+  .el-button-group>.el-button:last-child{border-radius: 0 1px 1px 0 !important;}
+  // 使其左右靠边
+  :deep(.el-form-item__label){flex: 1;}
+  :deep(.el-form-item__content){flex: 0 0 auto;}
+}
+
+</style>

+ 45 - 0
ssp-admin-vue3/src/layout/nav/com-setting/com-setting-switch.vue

@@ -0,0 +1,45 @@
+<!-- 全局设置:设置 切页动画  -->
+<template>
+    <div class="setting-item-box">
+        <el-divider content-position="left">切页动画</el-divider>
+        <div style="padding-top: 5px; padding-bottom: 20px;">
+            <el-button-group size="small">
+                <el-button
+                    v-for="mode in state.modeList" :key="mode.name"
+                    :type="mode.name === themeStore.switchMode ? 'primary' : ''"
+                    @click="themeStore.switchMode = mode.name"
+                >{{ mode.title }}</el-button>
+            </el-button-group>
+        </div>
+    </div>
+</template>
+
+<script setup name="com-setting-switch">
+import {useThemeStore} from "../../../store/theme";
+import {reactive} from "vue";
+
+const themeStore = useThemeStore();
+
+// 所有切换样式 
+const state = reactive({
+    modeList: [
+        {name: 'opacity', title: '渐变'},
+        {name: 'right-jump-right', title: '左渐入'},
+        {name: 'left-jump-left', title: '右渐入'},
+    ]
+})
+
+
+</script>
+
+<style scoped lang="scss">
+
+.setting-item-box{
+  .el-button-group>.el-button{
+    padding: 12px 14px;
+  }
+  .el-button-group>.el-button:first-child{border-radius: 1px 0 0 1px !important;}
+  .el-button-group>.el-button:last-child{border-radius: 0 1px 1px 0 !important;}
+}
+
+</style>

+ 46 - 0
ssp-admin-vue3/src/layout/nav/com-setting/com-setting-tab-style.vue

@@ -0,0 +1,46 @@
+<!-- 全局设置:tab 卡片样式  -->
+<template>
+    <div class="setting-item-box">
+        <el-divider content-position="left">卡片风格</el-divider>
+        <div style="padding-top: 5px; padding-bottom: 20px;">
+            <el-button-group size="small">
+                <el-button
+                    v-for="mode in state.modeList" :key="mode.name"
+                    :type="mode.name === themeStore.tabStyle ? 'primary' : ''"
+                    @click="themeStore.tabStyle = mode.name"
+                >{{ mode.title }}</el-button>
+            </el-button-group>
+        </div>
+    </div>
+</template>
+
+<script setup name="com-setting-tab-style">
+import {useThemeStore} from "../../../store/theme";
+import {reactive} from "vue";
+
+const themeStore = useThemeStore();
+
+// 所有切换样式 
+const state = reactive({
+    modeList: [
+        {name: 'block', title: '方块'},
+        {name: 'mellow', title: '圆润'},
+        {name: 'mask', title: '遮罩'},
+        {name: 'mask-light', title: '浅色调遮罩'},
+    ]
+})
+
+
+</script>
+
+<style scoped lang="scss">
+
+.setting-item-box{
+  .el-button-group>.el-button{
+    padding: 12px 12px;
+  }
+  .el-button-group>.el-button:first-child{border-radius: 1px 0 0 1px !important;}
+  .el-button-group>.el-button:last-child{border-radius: 0 1px 1px 0 !important;}
+}
+
+</style>

+ 73 - 0
ssp-admin-vue3/src/layout/nav/com-setting/com-setting-theme.vue

@@ -0,0 +1,73 @@
+<!-- 全局设置:设置 theme 主题 -->
+<template>
+    <el-divider content-position="left">主题色调</el-divider>
+    <div style="padding-top: 5px; padding-bottom: 20px">
+        <el-tooltip
+            v-for="item in state.themeList"
+            :key="item.tcs"
+            :content="item.name"
+            :hide-after="0">
+            <div
+                class="theme-legend"
+                :class="['theme-legend-' + item.tcs, (item.tcs === themeStore.theme ? 'theme-legend-active' : '')]"
+                :style="{backgroundColor: item.legend}"
+                @click="themeStore.theme = item.tcs">
+                <el-icon><el-icon-Check /></el-icon>
+            </div>
+        </el-tooltip>
+    </div>
+</template>
+
+<script setup name="com-setting-theme">
+import {useThemeStore} from "../../../store/theme";
+import {reactive} from "vue";
+
+const themeStore = useThemeStore();
+
+/* 所有主题
+  {
+    tcs: 'blue',  // 主题类
+    legend: '#2d8cf0'   // 色块颜色
+  },
+ */
+const state = reactive({
+    themeList: [
+        {tcs: 'blue', legend: '#2d8cf0', name: '蓝色'},
+        {tcs: 'green', legend: '#009688', name: '绿色'},
+        {tcs: 'red', legend: '#dd4949', name: '红色'},
+        {tcs: 'purple', legend: '#A906B3', name: '紫色'},
+        {tcs: 'tit', legend: '#805322', name: '钛合金'},
+        {tcs: 'ash', legend: '#4e5465', name: '沉淀灰'},
+        {tcs: 'dark-green', legend: '#73d13d', name: '简约草绿'},
+        {tcs: 'white', legend: '#fff', name: '简约白'},
+    ]
+})
+
+
+</script>
+
+<style scoped lang="scss">
+
+/* 主题色 */
+.theme-legend{
+  width: 18px;
+  height: 18px;
+  display: inline-block;
+  border-radius: 1px;
+  cursor: pointer;
+  margin-right: 6px;
+  margin-bottom: 6px;
+  line-height: 23px;
+  text-align: center;
+  color: transparent;
+  border: 1px transparent solid;
+}
+.theme-legend-active{
+  color: #FFF;
+  border: 1px #009688 solid !important;
+}
+// 白色主题调整一下色块显示,不然看不见
+.theme-legend-white{border: 1px #ccc solid;}
+.theme-legend-white.theme-legend-active{color: #009688}
+
+</style>

+ 47 - 0
ssp-admin-vue3/src/layout/nav/nav-logo.vue

@@ -0,0 +1,47 @@
+<!-- 左上:logo部分 -->
+<template>
+    <div class="com-logo-box" :title="settingStore.title" @click="appStore.showHome()">
+        <transition name="left-jump-right">
+            <div class="tr-box" v-if="appStore.isOpen">
+                <img :src="settingStore.logo" class="admin-logo" alt="logo">
+                <span class="admin-title">{{ settingStore.title }} 后台</span>
+            </div>
+            <div class="tr-box" v-else>
+                <img :src="settingStore.logo" class="admin-logo" alt="logo" style="margin-left: 18px;">
+            </div>
+        </transition>
+    </div>
+</template>
+
+<script setup name="nav-logo">
+import {useAppStore} from "../../store/app";
+import {useSettingStore} from "../../store/setting";
+
+const appStore = useAppStore();
+const settingStore = useSettingStore();
+
+</script>
+
+<style scoped>
+.com-logo-box {
+    cursor: pointer;
+    overflow: hidden;
+}
+.tr-box{
+    display: inline-block;
+    white-space: nowrap;
+    transition: all 0.6s
+}
+
+.admin-logo {
+    width: 30px;
+    height: 30px;
+    border-radius: 50%;
+    vertical-align: middle;
+    margin-left: 22px;
+    transition: all 0.2s;
+}
+
+.admin-title{padding-left: 10px; font-size: 1.1em; /*font-weight: 700;*/ vertical-align: middle;}
+
+</style>

+ 136 - 0
ssp-admin-vue3/src/layout/nav/nav-menu-bar.vue

@@ -0,0 +1,136 @@
+<template>
+    <!-- 左下:菜单栏 -->
+    <div>
+        <el-scrollbar class="menu-box">
+            <div class="menu-box-pan">
+
+                <!--
+                  菜单:
+                    unique-opened = 是否只有菜单打开
+                    default-active = 正在高亮的菜单id
+                    collapse = 是否折叠
+                    参考文档:https://element-plus.gitee.io/zh-CN/component/menu.html
+                -->
+                <el-menu
+                    :unique-opened="true"
+                    :default-active="appStore.activeMenuId"
+                    :collapse="!appStore.isOpen"
+                    @select="selectMenu"
+                >
+                    <template v-for="(menu) in appStore.menuList">
+                        <!-- 1 如果是子菜单 -->
+                        <el-menu-item v-if="menuIsShow(menu) && !menuIsDir(menu)" :index="menu.id" :key="menu.id">
+                            <svg-icon class="menu-i" :name="menu.icon" :title="menu.title"></svg-icon>
+                            <span class="menu-name">{{ menu.title }}</span>
+                        </el-menu-item>
+                        <!-- 1 如果是父菜单 -->
+                        <el-sub-menu v-if="menuIsShow(menu) && menuIsDir(menu)" :index="menu.id" :key="menu.id">
+                            <template #title>
+                                <svg-icon class="menu-i" :name="menu.icon" :title="menu.title"></svg-icon>
+                                <span class="menu-name">{{ menu.title }}</span>
+                            </template>
+                            <!-- 遍历其子项 -->
+                            <template v-for="(menu2) in menu.children">
+                                <!-- 2 如果是子菜单 -->
+                                <el-menu-item v-if="menuIsShow(menu2) && !menuIsDir(menu2)" :index="menu2.id" :key="menu2.id">
+                                    <svg-icon class="menu-i" :name="menu2.icon" :title="menu2.title"></svg-icon>
+                                    <span class="menu-name">{{ menu2.title }}</span>
+                                </el-menu-item>
+                                <!-- 2 如果是父菜单 -->
+                                <el-sub-menu v-if="menuIsShow(menu2) && menuIsDir(menu2)" :index="menu2.id" :key="menu2.id">
+                                    <template #title>
+                                        <svg-icon class="menu-i" :name="menu2.icon" :title="menu2.title"></svg-icon>
+                                        <span class="menu-name">{{ menu2.title }}</span>
+                                    </template>
+                                    <!-- 遍历其子项 -->
+                                    <template v-for="(menu3) in menu2.children">
+                                        <!-- 3 如果是子菜单 -->
+                                        <el-menu-item v-if="menuIsShow(menu3) && !menuIsDir(menu3)" :index="menu3.id" :key="menu3.id">
+                                            <svg-icon class="menu-i" :name="menu3.icon" :title="menu3.title"></svg-icon>
+                                            <span class="menu-name">{{ menu3.title }}</span>
+                                        </el-menu-item>
+                                        <!-- 3 如果是父菜单 -->
+                                        <el-sub-menu v-if="menuIsShow(menu3) && menuIsDir(menu3)" :index="menu3.id" :key="menu3.id">
+                                            <template #title>
+                                                <svg-icon class="menu-i" :name="menu3.icon" :title="menu3.title"></svg-icon>
+                                                <span class="menu-name">{{ menu3.title }}</span>
+                                            </template>
+                                            <!-- 4 -->
+                                            <template v-for="(menu4) in menu3.children">
+                                                <el-menu-item v-if="menuIsShow(menu4)" :index="menu4.id" :key="menu4.id">
+                                                    <svg-icon class="menu-i" :name="menu4.icon" :title="menu4.title"></svg-icon>
+                                                    <span class="menu-name">{{ menu4.title }}</span>
+                                                </el-menu-item>
+                                            </template>
+                                        </el-sub-menu>
+                                    </template>
+                                </el-sub-menu>
+                            </template>
+                        </el-sub-menu>
+                    </template>
+                </el-menu>
+            </div>
+        </el-scrollbar>
+    </div>
+</template>
+
+<script setup name="nav-menu-bar">
+import SvgIcon from "../../components/svg-icon";
+import { useAppStore } from "../../store/app";
+import {menuIsShow, menuIsDir} from '../../router/router-util';
+
+const appStore = useAppStore();
+
+// 点击子菜单时触发的回调
+// 参数:index=点击菜单index标识(不是下标,是菜单id),
+// indexArray=所有已经打开的菜单id数组,形如:['1', '1-1', '1-1-1']
+const selectMenu = function(index, indexArray) {
+    appStore.showMenuById(index);
+};
+
+</script>
+
+<style scoped lang="scss">
+
+  .menu-box{
+    .menu-box-pan{
+      width: 100%; overflow-x: hidden; padding-bottom: 200px
+    }
+
+    .menu-i{display: inline-block; vertical-align: middle; width: 24px; font-size: 16px !important; position: relative; top: -1px;}
+    .menu-i{margin-right: 0px;}
+    .menu-name{margin-left: 5px;}
+  }
+
+  .menu-box :deep(*) {
+    // 动画速度加快
+    *{transition: all 0.3s;}
+
+    /* 避免菜单文字居中显示带来的不和谐现象 */
+    .el-menu-item{display: block;}
+
+    /* 隐藏右边框 */
+    .el-menu{border: 0px;}
+
+    /* 一级菜单,高度45px */
+    .el-menu-item,
+    .el-sub-menu__title{height: 45px !important; line-height: 45px !important;}
+
+    /* 二级以下菜单,高度40px */
+    .el-sub-menu .el-menu-item,
+    .el-sub-menu .el-sub-menu .el-sub-menu__title{height: 40px !important; line-height: 40px !important;}
+
+    /* 二级菜单 左边距 */
+    .el-sub-menu .el-menu-item,
+    .el-sub-menu .el-sub-menu .el-sub-menu__title{padding-left: 2.5em !important;}
+
+    /* 三级菜单 左边距 */
+    .el-sub-menu .el-sub-menu .el-menu-item,
+    .el-sub-menu .el-sub-menu .el-sub-menu .el-sub-menu__title{padding-left: 3.6em !important;}
+
+    /* 四级菜单 左边距 */
+    .el-sub-menu .el-sub-menu .el-sub-menu .el-menu-item{padding-left: 4.7em !important;}
+
+  }
+
+</style>

+ 220 - 0
ssp-admin-vue3/src/layout/nav/nav-tab-bar.vue

@@ -0,0 +1,220 @@
+<!-- 右边,第二行:tab栏 -->
+<template>
+    <div>
+        <el-scrollbar
+            ref="tab-title-box"
+            @wheel.native.prevent="handleScroll"
+            class="tab-title-box" :class=" 'tab-style-' + themeStore.tabStyle "
+        >
+            <draggable
+                v-model="appStore.tabList"
+                chosen-class="chosen-tab"
+                animation="500"
+                item-key="id"
+                :scroll="true"
+            >
+                <template #item="{element: tab}">
+                    <div
+                        :id=" 'tab-' + tab.id "
+                        class="tab-title"
+                        :class=" (tab === appStore.nativeTab ? 'tab-native' : '') "
+                        @click="appStore.showTab(tab)"
+                        @contextmenu.prevent="$refs['com-right-menu'].right_showMenu(tab, $event)"
+                        draggable="true"
+                        @dragstart="appStore.isDrag = true; appStore.dragTab = tab"
+                        @dragend="appStore.isDrag = false;"
+                    >
+                        <div class="tab-title-2">
+                            <!-- <i class="el-icon-caret-right"></i> -->
+                            <span>{{ tab.title }}</span>
+                            <svg-icon class="tab-title-close" name="el-icon-Close" v-if="!tab.hideClose" @click.stop="appStore.closeTab(tab)"></svg-icon>
+                        </div>
+                    </div>
+                </template>
+            </draggable>
+        </el-scrollbar>
+
+        <!-- 右键菜单 -->
+        <com-right-menu ref="com-right-menu"></com-right-menu>
+
+    </div>
+</template>
+
+<script setup name="nav-tab-bar">
+import Draggable from 'vuedraggable'
+import ComRightMenu from './com-right-menu'
+import {useAppStore} from "../../store/app";
+import {getCurrentInstance, ref} from "vue";
+import {watch} from "vue";
+import {useRoute} from "vue-router";
+import {useThemeStore} from "../../store/theme";
+import mitt from "@/mitt";
+const appStore = useAppStore();
+const themeStore = useThemeStore();
+const route = useRoute();
+
+// 获取滚动条操作对象
+const {proxy} = getCurrentInstance()
+const getWrap = function () {
+    return proxy.$refs['tab-title-box'].$refs.wrap$;
+}
+
+// 处理鼠标滚动
+const handleScroll =function (e) {
+    getWrap().scrollLeft += - e.wheelDelta / 5;
+}
+
+// 设置滚动条 带动画效果,value=要滚动到的位置
+const setScroll = function (value){
+
+    const start = getWrap().scrollLeft; // 初始值
+    const end = value;   // 结束值
+    let ci = 20;    // 分多少次完成
+    const bu = (end - start) / ci;  // 每次跳跃多少
+
+    // 递归函数
+    const fn = function (){
+        getWrap().scrollLeft += bu;
+        ci--;
+        if(ci >= 0) {
+            setTimeout(fn, 10);
+        }
+    }
+    fn();
+
+}
+
+// 滚动条自动归位
+const scrollToAuto = function() {
+    //
+    try{
+        var boxWidth = document.querySelector('.nav-right-2').clientWidth;	// 视角宽度
+        var nativeTab = document.querySelector('.tab-native');  // 当前选中元素
+
+        // 元素与box左边框的距离
+        const leftBor = nativeTab.offsetLeft - getWrap().scrollLeft;
+
+        // 元素与box右边的距离
+        // const rightBor = boxWidth + leftBor;
+
+        // 元素滚到中间需要的距离
+        // const centerX = boxWidth / 2 - leftBor;
+
+        // 让滚动条滚过去
+        // getWrap().scrollLeft -= centerX;
+        // setScroll(getWrap().scrollLeft - centerX);
+
+        // 如果越过了左边,需要往右滚动一些
+        if(leftBor - 100 < 0) {
+            setScroll(getWrap().scrollLeft + leftBor - 100);
+        }
+
+        // 如果越过了右边,需要向左滚动一些
+        if(leftBor + nativeTab.clientWidth + 50 > boxWidth) {
+            setScroll(getWrap().scrollLeft + leftBor + 100 + nativeTab.clientWidth - boxWidth);
+        }
+
+    }catch(e){
+        // throw e;
+    }
+};
+
+// 监听:每次切换 path 时,刷新一下 tab 栏
+watch(() => route.path, () => {
+    if(route.meta && route.meta.menu) {
+        // 如果是 homeTab
+        const menu = route.meta.menu;
+        if(menu.id === appStore.homeTab.id) {
+            return appStore.showHome();
+        }
+        // 先从 tab 栏查询打开
+        const tab = appStore.showTabById(menu.id);
+        if(tab) {
+            return;
+        }
+        // tab栏里找不到时,再尝试从菜单栏查询 
+        appStore.showMenuById(menu.id);
+    }
+})
+
+
+// 订阅事件:设置滚动条值
+mitt.off('setScroll');
+mitt.on('setScroll', data => {
+    setScroll(data);
+});
+
+// 订阅事件:滚动条自动归位
+mitt.off('scrollToAuto');
+mitt.on('scrollToAuto', data => {
+    scrollToAuto();
+});
+
+
+</script>
+
+<style scoped lang="scss">
+
+.tab-title-box{width: 100%; height: 100%; position: absolute; white-space: nowrap; background-color: #FFF; transition: all 0.2s; }
+.tab-title-box :deep(.el-scrollbar__wrap){overflow-y: hidden;}
+
+.tab-title:first-child{margin-left: 10px;}
+.tab-title:last-child{margin-right: 40px;}
+
+.tab-title{font-size: 12px; cursor: pointer; display: inline-block; overflow: hidden; text-decoration: none; color: #414060;}
+.tab-title-2{padding: 0px 5px; /* background-color: #FFF; */ }
+.tab-title-2{transition: padding 0.1s, margin 0.1s;}
+
+.tab-title span{display: inline-block; margin-left: 11px; margin-right: 11px;}
+.tab-title:hover span,.tab-native span{/* font-weight: bold; */}
+
+/* 关闭图标 */
+.tab-title .tab-title-close{display: inline-block; font-size: 12px !important; vertical-align: -20%;}
+.tab-title .tab-title-close{border-radius: 50%; padding: 1px; color: #ccc; margin-left: -5px;}
+.tab-title .tab-title-close:hover{background-color: red; color: #FFF;}
+
+/* 卡片样式 */
+.tab-title{transition: width 0.2s, background 0s, border 0.2s, border-radius 0.2s;}
+.tab-title:hover,.tab-native{transition: width 0.2s, background 0.2s, border 0.2s;}
+.tab-title{border-radius: 1px; border: 1px #e5e5e5 solid; height: 24px; line-height: 26px; margin: 6px 2px 0px; background-color: #fff;}
+
+/* ------- 卡片风格 ------- */
+// 方块风格(取默认)
+// 圆润风格
+.tab-style-mellow{
+    .tab-title{
+        border-radius: 20px;
+    }
+    .tab-title-2{
+        padding: 0px 10px;
+    }
+}
+
+// 遮罩风格
+.tab-style-mask,.tab-style-mask-light{
+    .tab-title{
+        mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
+        mask-size: 100% 100%;
+        -webkit-mask: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANoAAAAkBAMAAAAdqzmBAAAAMFBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlTPQ5AAAAD3RSTlMAr3DvEM8wgCBA379gj5//tJBPAAAAnUlEQVRIx2NgAAM27fj/tAO/xBsYkIHyf9qCT8iWMf6nNQhAsk2f5rYheY7Dnua2/U+A28ZEe8v+F9Ax2v7/F4DbxkUH2wzgtvHTwbYPo7aN2jZq26hto7aN2jZq25Cy7Qvctnw62PYNbls9HWz7S8/G6//PsI6H4396gAUQy1je08W2jxDbpv6nD4gB2uWp+J9eYPsEhv/0BPS1DQBvoBLVZ3BppgAAAABJRU5ErkJggg==);
+        -webkit-mask-size: 100% 100%;
+        border-width: 0 !important;
+        height: 30px;
+        line-height: 32px;
+        //transition: all 0.2s;
+    }
+    .tab-title-2{
+        padding: 0px 13px;
+    }
+}
+// 浅色调遮罩
+.tab-style-mask-light{
+    .tab-title:hover{
+        background-color: var(--t-light-color);
+    }
+    .tab-native{
+        background-color: var(--t-light-color) !important;
+        color: var(--menu-active-bg-color) !important;
+    }
+}
+
+</style>

+ 52 - 0
ssp-admin-vue3/src/layout/nav/nav-tool-bar.vue

@@ -0,0 +1,52 @@
+<!-- 右边第一行,工具栏 -->
+<template>
+    <div>
+        <div class="tools-left">
+            <NavToolFold></NavToolFold>
+            <NavToolRefresh v-if="themeStore.isShowRefresh"></NavToolRefresh>
+            <NavToolDatetime v-if="themeStore.isShowDateTime"></NavToolDatetime>
+            <NavToolBreadcrumb v-if="themeStore.isShowBreadcrumb"></NavToolBreadcrumb>
+        </div>
+        <div class="tools-right">
+            <NavToolUser></NavToolUser>
+            <NavToolNote></NavToolNote>
+            <NavToolSearch></NavToolSearch>
+            <NavToolScreen></NavToolScreen>
+            <NavToolSetting></NavToolSetting>
+        </div>
+    </div>
+</template>
+
+<script setup name="nav-tool-bar">
+import NavToolFold from './nav-tools/nav-tool-fold';
+import NavToolSearch from './nav-tools/nav-tool-search';
+import NavToolRefresh from './nav-tools/nav-tool-refresh';
+import NavToolDatetime from './nav-tools/nav-tool-datetime';
+import NavToolBreadcrumb from './nav-tools/nav-tool-breadcrumb';
+
+import NavToolUser from './nav-tools/nav-tool-user';
+import NavToolNote from './nav-tools/nav-tool-note';
+import NavToolScreen from './nav-tools/nav-tool-screen';
+import NavToolSetting from './nav-tools/nav-tool-setting';
+
+import {useThemeStore} from "../../store/theme";
+const themeStore = useThemeStore();
+
+</script>
+
+<style scoped lang="scss">
+
+.tools-left{border: 0px #000 solid; float: left;}
+.tools-right{float: right;}
+
+:deep(.tool-fox) {padding: 0 1em; display: inline-block; cursor: pointer;}
+:deep(.tool-fox i, .tool-fox svg) {transition: all 0.2s; font-size: 18px; vertical-align: middle;}
+:deep(.tool-fox:hover) {
+    background-color: var(--tool-hover-bg-color);
+}
+
+/*800之下*/
+@media(max-width: 800px) {
+    .tools-right{display: none;}
+}
+</style>

+ 83 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-breadcrumb.vue

@@ -0,0 +1,83 @@
+<!-- 工具栏:面包屑导航栏 -->
+<template>
+    <span class="tool-fox nav-breadcrumb">
+        <el-breadcrumb separator="/">
+            <transition-group name="bre">
+                <el-breadcrumb-item class="bre-item" v-for="(item, index) in state.levelArray" :key="item.id">
+                    <span class="bre-text" v-if="index === state.levelArray.length - 1">{{ item.title }}</span>
+                    <b v-else>{{ item.title }}</b>
+                </el-breadcrumb-item>
+            </transition-group>
+        </el-breadcrumb>
+    </span>
+</template>
+
+<script setup name="nav-tool-breadcrumb">
+import {nextTick, reactive, watch} from "vue";
+import {useAppStore} from "../../../store/app";
+import {homeTab} from "../../../router/home";
+const appStore = useAppStore();
+
+const state = reactive({
+    levelArray: [homeTab] // 面包屑信息
+})
+
+// 刷新面包屑信息
+function f5BreadcrumbInfo() {
+    // 如果是首屏
+    if(appStore.nativeTab.id === homeTab.id) {
+        return state.levelArray = [homeTab];
+    }
+
+    // 否则从搜索列表中搜寻
+    for (let item of appStore.searchList) {
+        if(item.id === appStore.nativeTab.id) {
+            return state.levelArray = [homeTab, ...item.level];
+        }
+    }
+
+    // 如果遍历全局没有找到
+    return state.levelArray = [homeTab, appStore.nativeTab];
+}
+
+// 监听 tab 切换,刷新一下
+watch(() => appStore.nativeTab, () => {
+    f5BreadcrumbInfo();
+})
+
+// 组件加载时触发一下
+nextTick(function (){
+    f5BreadcrumbInfo();
+})
+
+
+</script>
+
+<style scoped lang="scss">
+.nav-breadcrumb{
+  height: 100%;
+  vertical-align: middle;
+  margin-left: -8px;
+  cursor: text !important;
+  display: inline-block;
+}
+.nav-breadcrumb:hover{
+  background-color: rgba(0,0,0,0) !important;
+}
+.bre-item{cursor: text; display: inline-block; }
+.bre-text{color: #888}
+
+
+.bre-enter-active,
+.bre-leave-active {
+  will-change: transform;
+  transition: all 0.5s ease;
+}
+.bre-enter-from,
+.bre-leave-to {
+  opacity: 0;
+  transform: translateX(50px);
+  position: absolute;
+}
+
+</style>

+ 56 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-datetime.vue

@@ -0,0 +1,56 @@
+<!-- 工具栏:日期时间 -->
+<template>
+    <span title="当前时间" class="tool-fox curr-datetime-box">
+        <span style="font-size: 0.90em;">{{ state.nowTime }}</span>
+    </span>
+</template>
+
+<script setup name="nav-tool-datetime">
+import {reactive} from "vue";
+
+const state = reactive({
+    nowTime: '加载中...',    // 当前时间字段
+    currInterval: null,    // 定时器
+})
+
+
+// 刷新时间
+const initInterval = function() {
+    if(state.currInterval) {
+        clearInterval(state.currInterval);
+    }
+    // 一直更新时间
+    state.currInterval = setInterval(function() {
+        const da = new Date();
+        const Y = da.getFullYear(); //年
+        const M = da.getMonth() + 1; //月
+        const D = da.getDate(); //日
+        let h = da.getHours(); //小时
+        let sx = "凌晨";
+        if (h >= 6) {
+            sx = "上午"
+        }
+        if (h >= 12) {
+            sx = "下午";
+            if (h >= 18) {
+                sx = "晚上";
+            }
+            h -= 12;
+        }
+        const m = da.getMinutes(); //分
+        const s = da.getSeconds(); //秒
+        const z = ['日', '一', '二', '三', '四', '五', '六'][da.getDay()] ; //周几
+        // z = z == 0 ? '日' : z;
+        state.nowTime = Y + "-" + M + "-" + D + " " + sx + " " + h + ":" + m + ":" + s + " 周" + z;
+    }, 1000);
+}
+initInterval();
+
+
+</script>
+
+<style scoped lang="scss">
+    .curr-datetime-box{
+        vertical-align: middle;
+    }
+</style>

+ 31 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-fold.vue

@@ -0,0 +1,31 @@
+<!-- 工具栏:折叠/展开菜单栏按钮 -->
+<template>
+    <span title="折叠 / 展开" class="tool-fox" @click="changeOpen()">
+        <el-icon
+            style="transition: all 0.2s;"
+            :style="appStore.isOpen ? 'transform: rotate(0deg)' : 'transform: rotate(-180deg)'">
+            <el-icon-Fold />
+        </el-icon>
+    </span>
+</template>
+
+<script setup name="nav-tool-fold">
+import {useAppStore} from "../../../store/app";
+
+const appStore = useAppStore();
+
+
+// 切换菜单栏的打开和关闭
+const changeOpen = function () {
+    if(appStore.isOpen) {
+        appStore.endOpen();
+    } else {
+        appStore.startOpen();
+    }
+}
+
+</script>
+
+<style scoped>
+
+</style>

+ 53 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-note.vue

@@ -0,0 +1,53 @@
+<!-- 工具栏:便签按钮 -->
+<template>
+    <span title="便签" class="tool-fox" @click="openNote()">
+        <el-icon class="note-icon"><el-icon-Edit /></el-icon>
+    </span>
+</template>
+
+<script setup name="nav-tool-note">
+// 打开便签
+const openNote = function() {
+    var w = (document.body.clientWidth * 0.4) + 'px';
+    var h = (document.body.clientHeight * 0.6) + 'px';
+    var default_content = '一个简单的小便签, 关闭浏览器后再次打开仍然可以加载到上一次的记录, 你可以用它来记录一些临时资料';
+    var value = localStorage.getItem('sa_admin_note') || default_content;
+    var index = layer.prompt({
+        title: '一个小便签',
+        value: value,
+        formType: 2,
+        area: [w, h],
+        btn: ['保存'],
+        maxlength: 99999999,
+        skin: 'layer-note-class'
+    }, function(pass, index){
+        layer.close(index)
+    });
+    var se = '#layui-layer' + index + ' .layui-layer-input';
+    var d = document.querySelector(se);
+    d.oninput = function() {
+        localStorage.setItem('sa_admin_note', this.value);
+    }
+};
+
+</script>
+
+<style scoped>
+  .note-icon{
+
+  }
+</style>
+
+<style>
+
+/* 去除掉便签的大边框 */
+.layer-note-class .layui-layer-input {
+  outline: 0;
+  box-shadow: none !important;
+  padding: 0.8em !important;
+  font-family: 'Times New Roman', Times, serif;
+  border: 0px #ddd solid;
+  border-bottom: 1px #ddd solid;
+}
+
+</style>

+ 16 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-refresh.vue

@@ -0,0 +1,16 @@
+<!-- 工具栏:刷新当前视图按钮 -->
+<template>
+    <span title="刷新" class="tool-fox" @click="appStore.f5NativeTab()">
+        <el-icon><el-icon-RefreshRight /></el-icon>
+    </span>
+</template>
+
+<script setup name="nav-tool-refresh">
+import {useAppStore} from "../../../store/app";
+let appStore = useAppStore();
+
+</script>
+
+<style scoped>
+
+</style>

+ 62 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-screen.vue

@@ -0,0 +1,62 @@
+<!-- 工具栏:全屏按钮 -->
+<template>
+    <span title="全屏" class="tool-fox" v-if="state.isFullScreen === false" @click="fullScreen()">
+        <el-icon><el-icon-FullScreen /></el-icon>
+    </span>
+    <span title="退出全屏" class="tool-fox" v-if="state.isFullScreen === true" @click="outFullScreen()">
+        <el-icon><el-icon-FullScreen /></el-icon>
+    </span>
+</template>
+
+<script setup name="nav-tool-screen">
+import {reactive} from "vue";
+
+const state = reactive({
+    isFullScreen: false // 标记是否全屏的变量
+})
+
+// 进入全屏
+const fullScreen = function() {
+    state.isFullScreen = true;
+    if(document.documentElement.RequestFullScreen){
+        document.documentElement.RequestFullScreen();
+    }
+    //兼容火狐
+    if(document.documentElement.mozRequestFullScreen){
+        document.documentElement.mozRequestFullScreen();
+    }
+    //兼容谷歌等可以webkitRequestFullScreen也可以webkitRequestFullscreen
+    if(document.documentElement.webkitRequestFullScreen){
+        document.documentElement.webkitRequestFullScreen();
+    }
+    //兼容IE,只能写msRequestFullscreen
+    if(document.documentElement.msRequestFullscreen){
+        document.documentElement.msRequestFullscreen();
+    }
+};
+
+// 退出全屏
+const outFullScreen = function() {
+    state.isFullScreen = false;
+    if(document.exitFullScreen){
+        document.exitFullscreen()
+    }
+    //兼容火狐
+    if(document.mozCancelFullScreen){
+        document.mozCancelFullScreen()
+    }
+    //兼容谷歌等
+    if(document.webkitExitFullscreen){
+        document.webkitExitFullscreen()
+    }
+    //兼容IE
+    if(document.msExitFullscreen){
+        document.msExitFullscreen()
+    }
+};
+
+</script>
+
+<style scoped>
+
+</style>

+ 88 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-search.vue

@@ -0,0 +1,88 @@
+<!-- 工具栏:搜索框 -->
+<template>
+    <span title="搜索菜单" class="tool-fox" @click="closeSearch()" v-if="!state.isShowSearchBtn">
+        <el-icon><el-icon-Search /></el-icon>
+    </span>
+    <span title="搜索菜单" class="tool-fox" @click="startSearch()" v-else>
+        <el-icon><el-icon-Search /></el-icon>
+    </span>
+    <span title="搜索-input" class="tool-fox search-fox" :class=" state.isSearch ? 'search-fox-show' : '' ">
+        <el-select
+            v-model="state.searchText" size="small" filterable placeholder="请输入菜单关键字" ref="search"
+            @change="findMenuBySearch" @blur="closeSearch" @keyup.esc.native="closeSearch">
+            <el-option v-for="item in appStore.searchList" :key="item.id" :label="item.text" :value="item.id"></el-option>
+        </el-select>
+    </span>
+</template>
+
+<script setup name="nav-tool-search">
+import {getCurrentInstance, reactive} from "vue";
+import {useAppStore} from "../../../store/app";
+let { proxy } = getCurrentInstance();
+const appStore = useAppStore();
+
+// 所有状态
+const state = reactive({
+    isSearch: false,	// 当前是否处于搜索模式
+    isShowSearchBtn: true,	// 是否显示打开搜索图标
+    searchText: ''		// 搜索框已经输入的字符
+})
+
+// 开启搜索
+const startSearch = function() {
+    state.searchText = '';
+    state.isSearch = true;
+    setTimeout(function() {
+        state.isShowSearchBtn = false;
+        proxy.$refs['search'].focus();	//.$refs['nav-tool-bar'].
+    }, 200);
+};
+// 关闭搜索
+const closeSearch = function() {
+    state.searchText = '';
+    state.isSearch = false;
+    setTimeout(function() {
+        try{
+            state.isShowSearchBtn = true;
+            // 使其失去焦点
+            proxy.$refs['search'].blur();
+            // let dom = document.querySelector('body>div>.el-popper');
+            // if(dom && dom.parentElement.parentElement === document.body) {
+            //   document.querySelector('body>div>.el-popper').parentElement.style.display = 'none';
+            // }
+        }catch(e){throw e}
+    }, 200);
+};
+
+// 查找菜单
+const findMenuBySearch = function(id) {
+    appStore.showMenuById(id);
+    closeSearch();
+};
+
+
+
+
+
+
+</script>
+
+<style scoped lang="scss">
+  /* 搜素框 */
+  .search-fox{display: inline-block; vertical-align: middle; overflow: hidden; max-width: 0px;
+    padding: 0em 0em !important; /*margin-left: -5px; */transition: all 0.2s !important; transition-timing-function: linear !important;}
+  .search-fox-show{display: inline-block; max-width: 240px; margin-left: 0px; padding: 0 1em;}
+  .search-fox:hover{background-color: rgba(0,0,0,0) !important;}
+
+  //.search-fox
+
+  .search-fox {
+    :deep(.el-input__wrapper){border-radius: 0px !important; border-width: 0px !important; border-bottom: 1px #409EFF solid !important; background-color: rgba(0,0,0,0);}
+    :deep(.el-input__suffix){display: none;}
+
+    /*去掉阴影边框*/
+    :deep(.el-input__wrapper),
+    :deep(.el-select .is-focus .el-input__wrapper),
+    :deep(.el-input__wrapper:focus){box-shadow: none !important; }
+  }
+</style>

+ 17 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-setting.vue

@@ -0,0 +1,17 @@
+<!-- 工具栏:全局设置 -->
+<template>
+    <span title="设置" class="tool-fox" @click="appStore.isShowSetting = ! appStore.isShowSetting">
+        <el-icon><el-icon-Setting /></el-icon>
+    </span>
+</template>
+
+<script setup name="nav-tool-setting">
+import {useAppStore} from "../../../store/app";
+
+const appStore = useAppStore();
+
+</script>
+
+<style scoped>
+
+</style>

+ 48 - 0
ssp-admin-vue3/src/layout/nav/nav-tools/nav-tool-user.vue

@@ -0,0 +1,48 @@
+<!-- 工具栏:User 信息按钮 -->
+<template>
+    <span title="点击登录" class="tool-fox" @click="$router.push('/login')" v-if="user.name == null">
+        <span style="font-size: 0.8em; font-weight: bold; position: relative; top: -2px;">未登录</span>
+    </span>
+    <span title="我的信息" class="tool-fox user-info" style="padding: 0;" v-else>
+        <!-- 昵称头像 -->
+        <el-dropdown @command="handleCommand" trigger="click" size="medium" style="line-height: inherit;">
+            <span class="el-dropdown-link" style="height: 100%; padding: 0 1em; display: inline-block;">
+                <img class="user-avatar" :src="user.avatar" alt="user-avatar">
+                <span class="user-name">{{ user.name }}</span>
+                <el-icon class="el-icon--right" style="position: relative; top: 2px;">
+                    <el-icon-ArrowDown />
+                </el-icon>
+            </span>
+            <template #dropdown>
+                <el-dropdown-menu>
+                    <el-dropdown-item v-for="drop in dropList" :command="drop.name" :key="drop.name">{{ drop.name }}</el-dropdown-item>
+                </el-dropdown-menu>
+            </template>
+        </el-dropdown>
+    </span>
+</template>
+
+<script setup name="nav-tool-user">
+import {useUserStore} from "../../../store/user";
+
+const userStore = useUserStore();
+const { user, dropList } = userStore;
+
+// 处理userinfo的下拉点击
+const handleCommand = function(command) {
+    dropList.forEach(function(drop) {
+        if(drop.name === command) {
+            drop.click();
+        }
+    })
+};
+
+</script>
+
+<style scoped>
+
+.user-info{position: relative; top: -1px;}
+.user-avatar{width: 25px; height: 25px; margin-right: 5px; border-radius: 50%; vertical-align: middle;}
+.user-name{font-size: 0.9em; color: var(--tool-color); position: relative; top: 1px;}
+
+</style>

+ 72 - 0
ssp-admin-vue3/src/layout/nav/nav-view-vessel.vue

@@ -0,0 +1,72 @@
+<!-- 右边第三行:视图容器 -->
+<template>
+    <div>
+        <div class="view-vessel">
+            <router-view v-slot="{ Component }">
+                <transition :name="themeStore.switchMode">
+                    <keep-alive :include="appStore.viewNameList">
+                        <component :is="Component" :key="$route.name" class="a-view" ref="curr-view" v-if="state.isShow" />
+                    </keep-alive>
+                </transition>
+            </router-view>
+        </div>
+    </div>
+</template>
+
+<script setup name="nav-view-vessel">
+import {useAppStore} from "../../store/app";
+import {useThemeStore} from "../../store/theme";
+import {getCurrentInstance, nextTick, onMounted, reactive, ref} from "vue";
+import {useRoute} from "vue-router";
+import {arrayDelete} from "../../router/router-util";
+import mitt from "@/mitt";
+import {useComStore} from "../../store/com";
+const appStore = useAppStore();
+const themeStore = useThemeStore();
+const comStore = useComStore();
+const route = useRoute();
+const { proxy } = getCurrentInstance();
+
+// 刷新视图
+const state = reactive({
+    isShow: true
+})
+
+const f5View = function (){
+    state.isShow = false;
+    arrayDelete(appStore.viewNameList, route.name);
+    nextTick(function (){
+        state.isShow = true;
+        appStore.viewNameList.push(route.name);
+    })
+}
+
+defineExpose({
+    f5View
+})
+
+onMounted(() => {
+    comStore.currViewFunction = function (){
+        return proxy.$refs['curr-view'];
+    }
+})
+
+// 订阅事件:刷新当前视图
+mitt.off('f5CurrView');
+mitt.on('f5CurrView', data => {
+    f5View();
+});
+
+</script>
+
+<style scoped>
+
+.view-vessel{width: 100%; height: 100%; position: relative; overflow: hidden; border: 0px #000 solid;}
+.a-view{width: 100%; height: 100%; /*background-color: #FFF; */overflow: auto;}
+
+/*.a-view>iframe{width: 100%; height: 100%; border: 0px #000 solid;}*/
+/*.a-view>.vue-com-view{width: 100%; height: 100%; overflow: auto; background-color: #EEE;}*/
+
+/* .iframe-no-scroll{width: calc(100% + 22px); } */
+
+</style>

+ 253 - 0
ssp-admin-vue3/src/layout/theme.css

@@ -0,0 +1,253 @@
+/* 主题定义文件 */
+
+/* -------- 全局样式 ------- */
+body{overflow: hidden;}
+
+/* 灰色模式 */
+.gray-mode {
+    filter: grayscale(100%);
+}
+
+/* 色弱模式 */
+.weak-mode {
+    filter: invert(80%);
+}
+
+/* iframe 视图 */
+.iframe-view{
+    width: 100%;
+    height: calc(100vh - 84px);
+    border: 0px;
+}
+
+
+/* -------- 主题 ------- */
+
+/* 样式调整为继承父级 */
+.nav-left .el-sub-menu__title i,
+.nav-left .el-menu-item i,
+.nav-right-1 .el-dropdown,
+.tab-title:hover .el-icon-caret-right,
+.tab-title.tab-native .el-icon-caret-right {
+	color: inherit;
+}
+
+.nav-left .el-menu,
+.nav-left .el-sub-menu,
+.nav-left .el-sub-menu__title,
+.nav-left .el-sub-menu .el-sub-menu .el-sub-menu__title,
+.nav-left .el-menu-item {
+	color: inherit;
+	background-color: inherit;
+}
+
+.theme-def .menu-name,.theme-def .tab-title-2>span{transition: none !important;}
+
+
+/* 声明变量 */
+.layout-index{
+	--menu-bg-color: #222;		/* 菜单 - 背景色 */
+	--menu-color: #FFF;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #000;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #4E5465;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #2D8CF0;		/* 菜单选中 - 背景色 */
+	--menu-active-color: #FFF;				/* 菜单选中 - 文字色 */
+	--tool-bg-color: #FFF;			/* 工具栏 - 背景色 */
+	--tool-color: #333;				/* 工具栏 - 文字色 */
+	--tool-hover-bg-color: #EEE;			/* 工具栏悬浮 - 背景色 */
+
+	--t-light-color: #e7f0fa;				/* 对应的浅色(在遮罩卡片风格里用到) */
+
+	--menu-active-bg-color-copy2: var(--menu-active-bg-color);  /* copy 一下 菜单选中-背景色 的变量值,以解决分栏布局中变量相互复制的问题  */
+
+	/* --tab-hover-bg-color: var(--menu-active-bg-color); */		/* Tab栏悬浮和选中 - 文字色 */
+	/* --tab-hover-color: var(--menu-active-color); */			/* Tab栏悬浮和选中 - 文字色 */
+	
+	--nav-left-top-border-color: 1px #222 solid;	/* 左上 - 右边框颜色 */
+	--nav-left-bottom-border-color: 1px #222 solid;	/* 左下 - 右边框颜色 */
+}
+
+/* ========================== 主题 - 0 默认样式 蓝色 ==========================  */
+.theme-def {}
+
+/* 左上 - 右边框颜色 */
+.theme-def .nav-left-top{
+	border-right: var(--nav-left-top-border-color);
+}
+/* 左下 - 右边框颜色 */
+.theme-def .nav-left-bottom{
+	border-right: var(--nav-left-bottom-border-color);
+}
+
+/* 左边栏背景色,前景色 */
+.theme-def .nav-left {
+	background-color: var(--menu-bg-color);
+	color: var(--menu-color);
+}
+
+/* 二级菜单背景色 */
+.theme-def .el-sub-menu .el-menu-item,
+.theme-def .nav-left .el-sub-menu .el-sub-menu .el-sub-menu__title{
+	background: var(--menu-bg-color-2);
+}
+
+/* 所有菜单悬浮样式*/
+.theme-def .nav-left .el-sub-menu__title:hover,
+.theme-def .nav-left .el-sub-menu .el-sub-menu .el-sub-menu__title:hover,
+.theme-def .nav-left .el-menu-item:hover{
+	background-color: var(--menu-hover-bg-color);
+}
+/* 所有菜单选中时 */
+.theme-def .nav-left .el-menu-item.is-active {
+	/* background-color: var(--menu-active-bg-color); */
+	background: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+}
+
+/* 工具栏背景色颜色、前景色 */
+.theme-def .nav-right-1 {
+	background-color: var(--tool-bg-color);
+	color: var(--tool-color);
+}
+
+/* 工具栏悬浮颜色 */
+.theme-def .tool-fox:hover {
+	background-color: var(--tool-hover-bg-color);
+}
+
+/* tab卡片栏 - 悬浮颜色 */
+.theme-def .tab-title:hover{
+	color: var(--menu-active-bg-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+/* tab卡片栏 - 选中颜色 */
+.theme-def .tab-native.tab-title {
+	background-color: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+
+/* 以下的主题 logo栏变小 */
+/*.theme-3 .nav-left-top,*/
+/*.theme-4 .nav-left-top,*/
+/*.theme-10 .nav-left-top{height: 50px; line-height: 50px; text-indent: 0.3em;}*/
+
+/*.theme-3 .nav-left-top .admin-logo,*/
+/*.theme-4 .nav-left-top .admin-logo,*/
+/*.theme-10 .nav-left-top .admin-logo{width: 28px; height: 28px; position: relative; top: -2px;}*/
+
+/*.theme-3 .nav-left-bottom,*/
+/*.theme-4 .nav-left-bottom,*/
+/*.theme-10 .nav-left-bottom{height: calc(100% - 85px + 36px);}*/
+
+
+
+
+/* ------------------------ 主题 theme-blue 蓝色 (什么也不覆盖 即:全部取默认样式) ------------------------  */
+.theme-blue {}
+
+/* ------------------------ 主题 theme-green 绿色 ------------------------  */
+.theme-green {
+	--menu-active-bg-color: #009688;	/* 菜单选中 - 背景色 */
+	--t-light-color: #ddf6f8;		/* 对应浅色 */
+}
+
+/* ------------------------ 主题 theme-red 红色 ------------------------  */
+.theme-red {
+	--menu-active-bg-color: #dd4949;	/* 菜单选中 - 背景色 */
+	--t-light-color: #feedeb;		/* 对应浅色 */
+}
+
+/* ------------------------ 主题 theme-purple 紫色 ------------------------  */
+.theme-purple {
+	--menu-active-bg-color: #A906B3;	/* 菜单选中 - 背景色 */
+	--t-light-color: #f2d9f3;		/* 对应浅色 */
+}
+
+/* ------------------------ 主题 theme-tit 钛合金 ------------------------  */
+.theme-tit {
+	--menu-active-bg-color: #805322;	/* 菜单选中 - 背景色 */
+	--t-light-color: #ebe5dd;		/* 对应浅色 */
+}
+
+/* ------------------------ 主题 theme-ash 简约灰 ------------------------  */
+.theme-ash {
+	--menu-active-bg-color: #4e5465;	/* 菜单选中 - 背景色 */
+	--t-light-color: #e5e5e5;		/* 对应浅色 */
+}
+
+/* ------------------------ 主题 theme-dark-green 简约草绿 ------------------------  */
+.theme-dark-green {
+	--menu-bg-color: #FFF;		/* 菜单 - 背景色 */
+	--menu-color: #333;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #fff;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #ECF5FF;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #73D13D;		/* 菜单选中 - 背景色 */
+
+	--t-light-color: #eaf6eb;		/* 对应浅色 */
+
+	--nav-left-top-border-color: 1px #ddd solid;	/* 左上 - 右边框颜色 */
+	--nav-left-bottom-border-color: 1px #ddd solid;	/* 左下 - 右边框颜色 */
+}
+
+/* tab卡片栏 - 悬浮颜色 */
+.theme-dark-green .tab-title:hover{
+	color: var(--menu-active-bg-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+/* tab卡片栏 - 选中颜色 */
+.theme-dark-green .tab-native.tab-title {
+	background-color: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-bg-color) solid;
+}
+.theme-dark-green .layout-column .nav-cmb-left{
+	border-right: 1px #e5e5e5 solid;
+}
+
+
+/* ------------------------ 主题 theme-white 简约白 ------------------------  */
+.theme-white {
+	--menu-bg-color: #FFF;		/* 菜单 - 背景色 */
+	--menu-color: #333;			/* 菜单 - 文字色 */
+	--menu-bg-color-2: #f5f5f5;	/* 二级菜单 - 背景色 */
+	--menu-hover-bg-color: #E7F0FA;			/* 菜单悬浮 - 背景色 */
+	--menu-active-bg-color: #E7F0FA;		/* 菜单选中 - 背景色 */
+	--menu-active-color: #409EFF;				/* 菜单选中 - 文字色 */
+
+	--nav-left-top-border-color: 1px #ddd solid;	/* 左上 - 右边框颜色 */
+	--nav-left-bottom-border-color: 1px #ddd solid;	/* 左下 - 右边框颜色 */
+}
+/* tab卡片栏 - 悬浮颜色 */
+.theme-white .tab-title:hover{
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-color) solid;
+}
+/* tab卡片栏 - 选中颜色 */
+.theme-white .tab-native.tab-title {
+	background-color: var(--menu-active-bg-color);
+	color: var(--menu-active-color);
+	border: 1px var(--menu-active-color) solid;
+}
+
+/* 浅色调遮罩调整 */
+.theme-white .tab-style-mask-light .tab-native.tab-title{
+	color: var(--menu-active-color) !important;
+}
+/* 分栏布局的菜单选中前景色调整 */
+.theme-white .layout-column .nav-sub-menu-box{
+	--menu-active-color: #409EFF;
+}
+.theme-white .layout-column .nav-cmb-left{
+	border-right: 1px #e5e5e5 solid;
+}
+
+
+
+
+
+
+
+
+
+

+ 79 - 0
ssp-admin-vue3/src/layout/transition.scss

@@ -0,0 +1,79 @@
+// 全局 css 动画
+
+/* ------------------ 切页动画 ------------------ */
+.left-jump-left-leave-active,
+.left-jump-left-enter-active,
+.right-jump-right-leave-active,
+.right-jump-right-enter-active,
+.left-jump-right-enter-active,
+.left-jump-right-leave-active,
+.right-jump-left-enter-active,
+.right-jump-left-leave-active {
+    transition: all .5s ease;
+    position: absolute;
+}
+
+/* --- right-jump-left  左出左进 --- */
+.left-jump-left-enter-from {opacity: 0; transform: translateX(30px);}
+.left-jump-left-leave-to {opacity: 0; transform: translateX(-30px);}
+
+/* --- right-jump-right  右出右进 --- */
+.right-jump-right-enter-from {opacity: 0; transform: translateX(-30px);}
+.right-jump-right-leave-to {opacity: 0; transform: translateX(30px);}
+
+/* --- left-jump-right 左出右进 --- */
+.left-jump-right-enter-from, .left-jump-right-leave-to {opacity: 0;transform: translateX(-30px);}
+
+/* --- right-jump-left 右出左进 --- */
+.right-jump-left-enter-from, .right-jump-left-leave-to {opacity: 0; transform: translateX(30px);}
+
+/* --- opacity 渐变 --- */
+.opacity-enter-active,
+.opacity-leave-active {
+    transition: all 0.7s ease;
+    position: absolute;
+}
+.opacity-enter-from,
+.opacity-leave-to {
+    opacity: 0;
+}
+
+
+/* ------------------ 其它动画 ------------------ */
+
+/* 从右向左 渐入 */
+@keyframes right-to-left {
+    0% {transform: translateX(60px); opacity: 0;}
+    100% {transform: translateX(0); opacity: 1;}
+}
+
+/* 从左向右 渐入 */
+@keyframes left-to-right {
+    0% {transform: translateX(-60px); opacity: 0;}
+    100% {transform: translateX(0); opacity: 1;}
+}
+
+/* 从下往上渐入 */
+@keyframes bottom-to-top {
+    0% {transform: translateY(60px); opacity: 0;}
+    100% {transform: translateY(0); opacity: 1;}
+}
+
+/* 从下往上渐入 */
+@keyframes top-to-bottom {
+    0% {transform: translateY(-60px); opacity: 0;}
+    100% {transform: translateY(0); opacity: 1;}
+}
+
+/* 透明,渐入 */
+@keyframes fade-in {
+    0% {opacity: 0;}
+    100% {opacity: 1;}
+}
+
+/* 透明,渐出 */
+@keyframes fade-out {
+    0% {opacity: 1;}
+    100% {opacity: 0;}
+}
+

+ 59 - 0
ssp-admin-vue3/src/layout/view/403.vue

@@ -0,0 +1,59 @@
+<!-- 错误页 403 -->
+<template>
+    <div class="con-box" style="background-color: #fafeff">
+        <div class="con-box2">
+            <el-row>
+                <el-col :span="6"></el-col>
+                <el-col :span="6" class="nr-img-box">
+                    <img class="nr-img ele-an-0" src="../../assets/err-icon.png" alt="404">
+                </el-col>
+                <el-col :span="1"></el-col>
+                <el-col :span="6" class="nr-fox">
+                    <h2 class="nr-title ele-an-1">403 Forbidden !</h2>
+                    <p class="nr-sub-title ele-an-2">你没有权限打开这个页面...</p>
+                    <p class="nr-info ele-an-3">请检查您输入的URL是否正确,或单击下面按钮返回主页。</p>
+                    <p class="nr-btn-box ele-an-4">
+                        <router-link to="/">
+                            <el-button type="primary" size="large" round>回到首页</el-button>
+                        </router-link>
+                    </p>
+                </el-col>
+                <el-col :span="5"></el-col>
+            </el-row>
+            <div style="height: 15vh;"></div>
+        </div>
+    </div>
+</template>
+
+<script setup name="404">
+
+</script>
+
+<style lang="scss" scoped>
+.con-box{
+    width: 100%; height: 100vh; display: flex; align-items: center; text-align: center;
+}
+.con-box2{
+    flex: 1;
+}
+
+.nr-img-box{text-align: left;}
+.nr-img{width: 100%;}
+.nr-fox{text-align: left;}
+.nr-title{ padding-top: 8vh; font-size: 36px; color: #1582f0;}
+.nr-sub-title{margin-top: 22px; font-size: 20px; color: #333; font-weight: bold;}
+.nr-info{margin-top: 15px; font-size: 14px; color: #666;}
+.nr-btn-box{margin-top: 40px;}
+
+// 动画
+@for $i from 0 through 4 {
+    .ele-an-#{$i} {
+        opacity: 0;
+        animation-name: bottom-to-top;
+        animation-duration: 0.5s;
+        animation-fill-mode: forwards;
+        animation-delay: calc(($i + 2) / 10) + s;
+    }
+}
+
+</style>

+ 59 - 0
ssp-admin-vue3/src/layout/view/404.vue

@@ -0,0 +1,59 @@
+<!-- 错误页 404 -->
+<template>
+    <div class="con-box" style="background-color: #fafeff">
+        <div class="con-box2">
+            <el-row>
+                <el-col :span="6"></el-col>
+                <el-col :span="6" class="nr-img-box">
+                    <img class="nr-img ele-an-0" src="../../assets/err-icon.png" alt="404">
+                </el-col>
+                <el-col :span="1"></el-col>
+                <el-col :span="6" class="nr-fox">
+                    <h2 class="nr-title ele-an-1">404 Not Found !</h2>
+                    <p class="nr-sub-title ele-an-2">没有找到你想要的页面...</p>
+                    <p class="nr-info ele-an-3">请检查您输入的URL是否正确,或单击下面按钮返回主页。</p>
+                    <p class="nr-btn-box ele-an-4">
+                        <router-link to="/">
+                            <el-button type="primary" size="large" round>回到首页</el-button>
+                        </router-link>
+                    </p>
+                </el-col>
+                <el-col :span="5"></el-col>
+            </el-row>
+            <div style="height: 15vh;"></div>
+        </div>
+    </div>
+</template>
+
+<script setup name="404">
+
+</script>
+
+<style lang="scss" scoped>
+.con-box{
+    width: 100%; height: 100vh; display: flex; align-items: center; text-align: center;
+}
+.con-box2{
+    flex: 1;
+}
+
+.nr-img-box{text-align: left;}
+.nr-img{width: 100%;}
+.nr-fox{text-align: left;}
+.nr-title{ padding-top: 8vh; font-size: 36px; color: #1582f0;}
+.nr-sub-title{margin-top: 22px; font-size: 20px; color: #333; font-weight: bold;}
+.nr-info{margin-top: 15px; font-size: 14px; color: #666;}
+.nr-btn-box{margin-top: 40px;}
+
+// 动画
+@for $i from 0 through 4 {
+    .ele-an-#{$i} {
+        opacity: 0;
+        animation-name: bottom-to-top;
+        animation-duration: 0.5s;
+        animation-fill-mode: forwards;
+        animation-delay: calc(($i + 2) / 10) + s;
+    }
+}
+
+</style>

+ 27 - 0
ssp-admin-vue3/src/layout/view/iframe-view.vue

@@ -0,0 +1,27 @@
+<!-- iframe页 -->
+<template>
+    <div class="iframe-view-box">
+        <iframe class="iframe-view" :src="state.url" v-if="state.url && state.url !== 'undefined'" />
+        <div v-else style="padding: 0.8em">Iframe 视图请提供 url 参数</div>
+    </div>
+</template>
+
+<script setup name="iframe-view">
+import {useRoute} from "vue-router";
+import {reactive} from "vue";
+const route = useRoute();
+
+// 解析 url 参数
+const state = reactive({
+    url: route.meta.iframeUrl || decodeURIComponent(route.query.url)
+})
+
+</script>
+
+<style scoped>
+.iframe-view-box,.iframe-view{
+    width: 100%;
+    height: calc(100vh - 84px);
+}
+.iframe-view{border: 0px;}
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 41 - 0
ssp-admin-vue3/src/layout/view/os-loading.vue


+ 116 - 0
ssp-admin-vue3/src/layout/view/os-model.vue

@@ -0,0 +1,116 @@
+<!--
+  全局 dialog
+ -->
+<template>
+    <!-- 全局 dialog -->
+    <div class="os-dialog-fox">
+        <el-dialog
+            v-model="state.show"
+            :title="state.title"
+            top="auto"
+            width="auto"
+            :append-to-body="false"
+            :close-on-click-modal="false"
+            :destroy-on-close="true"
+            class="os-dialog"
+        >
+            <div>
+                <!-- ------- 内容部分 ------- -->
+                <div class="s-body">
+                    <component :is="state.view" ref="os-model-view" :param="state.param" />
+                </div>
+                <!-- ------- 底部按钮 ------- -->
+                <div v-if="state.param.btn" class="s-foot">
+                    <el-button :type="getBtnType()" @click="ok">{{ state.param.btnText }}</el-button>
+                    <el-button @click="closeModel()">取消</el-button>
+                </div>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup name="os-model">
+import {getCurrentInstance, reactive, shallowRef} from "vue";
+import {useComStore} from "../../store/com";
+const { proxy } = getCurrentInstance();
+const comStore = useComStore();
+import mitt from "@/mitt";
+
+// 信息 
+const state = reactive({
+    show: false, // 是否显示
+    title: '提示', // 标题
+    view: null, // view视图
+    param: { // view视图的参数
+        btn: true,
+        btnText: '确定'
+    },
+});
+
+// 显示全局dialog
+const showModel = function(title, view, param) {
+    // 处理
+    param = param || {};
+    param.btn = (param.btn === undefined ? true : param.btn);
+    param.btnText = (param.btnText === undefined ? '确定' : param.btnText);
+    param.isModel = true;
+    if(typeof view === 'function') {
+        view = view();
+    }
+    view.then(com => {
+        state.title = title || '信息';
+        state.view = shallowRef(com.default);
+        state.param = param;
+        state.show = true;
+    })
+}
+
+// 关闭全局dialog
+const closeModel = function() {
+    state.show = false;
+}
+
+// 点击 [确定] 按钮执行的事件
+const ok = function() {
+    if (proxy.$refs['os-model-view'].ok) {
+        proxy.$refs['os-model-view'].ok();
+    } else {
+        console.warn('该组件未声明默认 ok 函数');
+        closeModel();
+    }
+}
+
+// 订阅事件:打开弹窗 
+mitt.off('showModel');
+mitt.on('showModel', ({title, view, param}) => {
+    // console.log('----所有参数,', title, view, param);
+    showModel(title, view, param);
+});
+
+// 订阅事件:关闭弹窗 
+mitt.off('closeModel');
+mitt.on('closeModel', data => {
+    closeModel();
+});
+
+
+// 获取按钮类型
+const getBtnType = function() {
+    if (isEnd(state.title, '详情')) {
+        return 'success';
+    }
+    return 'primary';
+}
+
+// 是否以指定字符串结尾
+const isEnd = function(str, target) {
+    let start = str.length - target.length;
+    let arr = str.substr(start, target.length);
+    return arr === target;
+}
+
+</script>
+
+<style scoped>
+
+</style>

+ 22 - 0
ssp-admin-vue3/src/layout/view/whole404.vue

@@ -0,0 +1,22 @@
+<!-- whole 404 -->
+<template>
+    <div class="con-box" style="background-color: #fafeff">
+        <div class="con-box2">
+            加载中.... 
+        </div>
+    </div>
+</template>
+
+<script setup name="whole404">
+
+</script>
+
+<style lang="scss" scoped>
+.con-box{
+    width: 100%; height: 100vh; display: flex; align-items: center; text-align: center;
+}
+.con-box2{
+    flex: 1;
+}
+
+</style>

+ 53 - 0
ssp-admin-vue3/src/main.js

@@ -0,0 +1,53 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+
+// 去除警告: Added non-passive event listener to a scroll-blocking ‘wheel‘
+import 'default-passive-events'
+
+// createApp
+const app = createApp(App);
+
+// 安装 pinia
+import store from "./store";
+app.use(store);
+
+// 安装 vue-router
+import './router/router-guards'
+import router from './router';
+app.use(router);
+
+// 安装 Element-Plus
+import 'element-plus/dist/index.css';
+import ElementPlus from 'element-plus';
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+app.use(ElementPlus, {
+    locale: zhCn,
+    // size: 'small'
+});
+
+// 全局图标 
+import { initElIcons } from "./init/init-el-icons";
+initElIcons(app);
+
+// 全局表单组件 
+import { initSaForm } from "./init/init-sa-form";
+initSaForm(app);
+
+// 全局样式
+import './layout/theme.css';
+import './layout/transition.scss'
+import './sa-frame/kj/layer/theme/default/layer.css'
+import './sa-frame/sa.scss';
+
+// 全局 sa 对象
+import sa from './sa-frame/sa.js';
+import './sa-frame/kj/upload-util.js';
+app.config.globalProperties.sa = sa;
+globalThis.sa = sa;
+
+// 全局异常处理 
+import {initErrorHandler} from "./init/error-handler";
+initErrorHandler(app);
+
+// 绑定dom
+app.mount('#app');

+ 9 - 0
ssp-admin-vue3/src/mitt/index.js

@@ -0,0 +1,9 @@
+import mitt from 'mitt'
+
+/*
+ * mitt 事件订阅发布,目前已实现:
+ *      f5CurrView  刷新当前视图
+ *      setScroll  设置 tab-bar 滚动条
+ *      scrollToAuto  让 tab-bar 滚动条自动归到一个合适的位置
+ */
+export default mitt();

+ 34 - 0
ssp-admin-vue3/src/router/async-import.js

@@ -0,0 +1,34 @@
+// ---------------- 异步组件导入 ---------------- 
+
+const modules = import.meta.globEager('./../*views/**/*.vue');
+// console.log(modules, modules)
+
+// 根据 path 获取 component 
+export const getComponentByPath = path => {
+    const comPath = path.replace('/@/', './../')
+    const module = modules[comPath];
+    if(module) {
+        return module.default;
+    } else {
+        console.warn('未能找到异步组件: ' + path);
+        return undefined;
+    }
+};
+
+// 将 componentPath 转化为 component 
+export const pathToComponent = function (_menuList) {
+    for (let menu of _menuList) {
+        // 如果是一个父菜单,则递归解析
+        if(menu.children) {
+            pathToComponent(menu.children);
+            continue;
+        }
+
+        if(menu.componentPath) {
+            menu.component = getComponentByPath(menu.componentPath);
+        }
+
+    }
+    return _menuList;
+}
+

+ 20 - 0
ssp-admin-vue3/src/router/home.js

@@ -0,0 +1,20 @@
+// ----- 定义首屏 tab 相关信息
+
+// Home 首屏tab
+export const homeTab = {
+    id: 'home',	// 唯一标识
+    title: '首页',    // 标题
+    path: '/home',	// 页面地址
+    component: () => import('@/sp-views/home/index.vue'),
+    hideClose: true	// 隐藏关闭键
+}
+
+// Home 首屏视图
+export const homeRoute = {
+    name: homeTab.id,
+    path: homeTab.path,
+    component: homeTab.component,
+    meta: {
+        menu: homeTab
+    }
+}

+ 29 - 0
ssp-admin-vue3/src/router/index.js

@@ -0,0 +1,29 @@
+import { createRouter, createWebHashHistory } from 'vue-router';
+
+// 所有路由
+import {staticRoutes} from "./routes-static";
+import {dynamicRoutes} from "./routes-dynamic";
+import {menuList} from "./menu-list";
+
+/**
+ * 创建 vue-router 实例
+ */
+const router = createRouter({
+    history: createWebHashHistory(),
+    routes: [...staticRoutes, ...dynamicRoutes],
+});
+
+// 导出
+export default router;
+
+
+// ---------------- 对外开放对象 ----------------
+// 从页面中直接导入 menuList 在热刷新组件时会报错,不影响运行但影响开发体验,报错原因未知,猜测和vite热刷新流程相关 
+// 改成下面的 useXxx 的形式则可以规避此问题
+
+/**
+ * 获取全局定义的菜单对象 
+ */
+export const useMenuList = function () {
+    return menuList;
+}

+ 134 - 0
ssp-admin-vue3/src/router/menu-list.js

@@ -0,0 +1,134 @@
+/**
+ * ---------------- 定义所有菜单项 ----------------
+ */
+import {defineMenu} from "./router-util";
+
+/**
+ * 菜单所有属性:
+ * {
+ * 	id: 'role',		        // 菜单id, 必须唯一 [必填]
+ * 	title: '用户中心',		// 菜单名称, 同时也是tab选项卡上显示的名称 [必填]
+ * 	icon: 'el-icon-user',	// 菜单图标, 参考地址:  https://element-plus.gitee.io/zh-CN/component/icon.html
+ * 	info: '管理所有用户',	    // 菜单详细介绍, 在菜单预览和分配权限时会有显示
+ * 	path: '/role-list',	    // 菜单path,如果不写,则根据id生成
+ * 	show: 'yes',			// 菜单是否显示, 取值:yes=永远显示,no=永远不显示,auth=根据权限决定是否显示,默认auth
+ * 	auth: true,             // 是否需要鉴权,默认true,此值为true时,在导航守卫里需要鉴权才可通过,为 false 时将无条件通过
+ * 	component: 'vue组件',    // 菜单对应的 vue 组件,
+ * 	parentId: 1,			// 所属父菜单id, 如果指定了一个值, 框架在初始化时会将此菜单转移到指定菜单上
+ * 	url: '',		        // 菜单url,如果指定了此值,则通过 iframe 打开页面视图
+ * 	isBlank: false,		    // 是否属于外部链接, 如果为true, 则点击菜单时从新窗口打开(只在url有值时此属性才会生效)
+ * 	click: function(){}		// 如果指定了此值,则点击菜单时执行一个函数
+ * 	children: [			    // 指定这个菜单所有的子菜单, 子菜单可以继续指定子菜单, 至多支持四级菜单
+ * 		// ....
+ * 	],
+ * }
+ */
+
+/**
+ * 定义动态路由表:需要展现在菜单栏上的路由
+ */
+export const menuList = defineMenu([
+    {
+        id: 'bas',
+        show: 'no',
+        title: '身份相关',
+        children: [
+            { id: 'root', title: 'Root 权限(最高权限)', show: 'no', info: '当前系统的最高权限标识,请谨慎授权'},
+            { id: 'in-system', title: '允许进入后台管理', show: 'no'},
+        ]
+    },
+    {
+        id: 'auth',
+        title: '权限控制',
+        icon: 'el-icon-Unlock',
+        info: '控制 Admin 管理员对后台的访问规则',
+        children: [
+            {id: 'role-list', title: '角色管理', icon: 'el-icon-Unlock', component: () => import('@/sp-views/sp-role/role-list.vue')},
+            {id: 'menu-list', title: '菜单管理', icon: 'el-icon-CollectionTag', component: () => import('@/sp-views/sp-role/menu-list.vue')},
+            {id: 'admin-list', title: '管理员列表', icon: 'el-icon-Key', component: () => import('@/sp-views/sp-admin/admin-list.vue'), info: '控制 Admin 管理员对后台的访问规则'},
+            {id: 'admin-add', title: '管理员添加', component: () => import('@/sp-views/sp-admin/admin-add.vue'), show: 'no', info: '按钮权限:决定管理员列表页是否显示 [ 管理员添加 ] 按钮'},
+            {id: 'sp-admin-login', title: '管理员登录日志', icon: 'el-icon-Mouse', component: () => import('@/sp-views/sp-admin-login/sp-admin-login-list')},
+        ]
+    },
+
+    {
+        id: 'console',
+        title: '监控中心',
+        icon: 'el-icon-View',
+        info: '提供 Redis、SQL、API访问日志等在线监控能力', 
+        children: [
+            {id: 'redis-console', title: 'Redis 监控台', icon: 'el-icon-Search', component: () => import('/@/sp-views/sp-console/redis-console.vue')},
+            {id: 'apilog-list', title: 'API 请求日志', icon: 'el-icon-MostlyCloudy', component: () => import('@/sp-views/sp-apilog/apilog-list.vue')},
+            {id: 'sql-console', title: 'SQL 监控台', icon: 'el-icon-View', url: '${SERVER_URL}/druid/index.html'},
+            {id: 'form-generator', title: '在线表单构建', show: 'no', icon: 'el-icon-View', url: 'https://mrhj.gitee.io/form-generator'},
+        ]
+    },
+
+    // sso 相关 
+    {
+        id: 'sys-client',
+        title: '应用管理',
+        icon: 'el-icon-Eleme',
+        info: '管理所有可 SSO 授权的 url 地址',
+        children: [
+            {id: 'sys-client-list', title: '应用列表', component: () => import('@/views/sys-client/sys-client-list.vue')},
+            {id: 'sys-client-add', title: '应用添加', show: 'no'},
+            {id: 'sys-client-visit', title: '应用访问关系', component: () => import('@/views/sys-client-visit/sys-client-visit-list.vue')},
+        ]
+    },
+    {
+        id: 'sys-user',
+        title: '用户管理',
+        icon: 'el-icon-User',
+        info: '管理 SSO 统一认证的 User 用户',
+        children: [
+            {id: 'sys-user-list', title: '用户列表', component: () => import('@/views/sys-user/sys-user-list.vue')},
+            {id: 'sys-user-list-gc', title: '用户回收站', component: () => import('@/views/sys-user/sys-user-list-gc.vue')},
+            {id: 'sys-login-log', title: '登录日志', component: () => import('@/views/sys-login-log/sys-login-log-list.vue')},
+            {id: 'console-plate', title: '数据走势', component: () => import('@/views/sys-user-sta/console-plate.vue'), info: '无此权限的用户无法进入首页大屏'},
+        ]
+    },
+    {
+        id: 'sys-user-online',
+        title: '在线用户',
+        icon: 'el-icon-CollectionTag',
+        info: '查看所有正在登录的 User 用户,提供踢人下线操作',
+        children: [
+            {id: 'sys-user-online-list', title: '在线用户', component: () => import('@/views/sys-user-online/sys-user-online-list.vue')},
+        ]
+    },
+    
+    {
+        id: 'sp-config',
+        title: '系统配置',
+        icon: 'el-icon-Setting',
+        info: '维护系统全局参数配置、User 用户同步 等',
+        children: [
+            {id: 'config-view-info', title: '系统信息', icon: 'el-icon-Plus', component: () => import('@/sp-views/sp-config/config-view-info.vue')},
+            {id: 'config-view-server', title: '全局参数', icon: 'el-icon-Plus', component: () => import('@/sp-views/sp-config/config-view-server.vue')},
+            {id: 'config-view-sync-user', title: '用户同步', icon: 'el-icon-Plus', component: () => import('@/sp-views/sp-config/config-view-sync-user.vue')},
+            {id: 'sp-config-list', title: '表格视图', icon: 'el-icon-Postcard', component: () => import('@/sp-views/sp-config/sp-config-list.vue')},
+        ]
+    },
+    
+    // 组件测试 
+    {
+        id: 'test',
+        title: '组件测试',
+        icon: 'el-icon-Scissor',
+        show: 'yes',
+        info: '提供 UI 表单增删改查的封装写法展示',
+        children: [
+            {id: 'data-list', title: '简单列表', icon: 'el-icon-DocumentRemove', show: 'yes', component: () => import('@/sp-views/test/data-list.vue')},
+            {id: 'data-list2', title: '复杂列表', icon: 'el-icon-DocumentRemove', show: 'yes', component: () => import('@/sp-views/test/list-more/data-list2.vue')},
+            {id: 'data-add', title: '表单提交', icon: 'el-icon-Edit', show: 'yes', component: () => import('@/sp-views/test/data-add.vue')},
+            {id: 'data-info', title: '信息展示', icon: 'el-icon-Finished', show: 'yes', component: () => import('@/sp-views/test/data-info.vue')},
+        ]
+    },
+
+]);
+
+// console.log(menuList)
+
+
+

+ 51 - 0
ssp-admin-vue3/src/router/router-guards.js

@@ -0,0 +1,51 @@
+/**
+ * ---------------- 定义导航守卫 ----------------
+ */
+import router from "./index";
+import NProgress from "nprogress";
+import 'nprogress/nprogress.css';
+import {setTitle} from "./router-util";
+import {useAppStore} from "../store/app";
+import {isStaticRoute} from "./routes-static";
+import initAdmin from "../init/init-admin";
+
+// 进度条配置:隐藏右上角进度环
+NProgress.configure({ showSpinner: false });
+
+// 路由初始化完毕钩子
+router.isReady().then(() => {
+    // 首次访问的不是登录界面,就去初始化 
+    if(router.currentRoute.value.path !== '/login') {
+        initAdmin();
+    }
+});
+
+// 路由加载前
+router.beforeEach(async (to, from, next) => {
+    // 进度条和loading 
+    NProgress.start();
+    useAppStore().showOsLoading();
+    
+    // 设置标题 
+    setTitle(to);
+    
+    // 鉴权 
+    const menu = to.meta.menu;
+    if(isStaticRoute(to) || menu === undefined || menu.show === 'yes' || menu.auth === false || sa.isAuth(menu.id)) {
+        next();
+    } else {
+        next('/403');
+    }
+})
+
+// 路由加载后
+router.afterEach(() => {
+    // 关闭进度条
+    NProgress.done();
+    // 关闭全局loading 
+    useAppStore().hideOsLoading2();
+    // setTimeout(function (){
+    //     useAppStore().isOsLoading = false;
+    // }, 10)
+})
+

+ 260 - 0
ssp-admin-vue3/src/router/router-util.js

@@ -0,0 +1,260 @@
+// --------------- 定义所有在操作 Router 时可能用到的工具函数
+
+// 从一个 MenuList 里查找指定 id 的 menu,支持多级递归查询
+import {useSettingStore} from "../store/setting";
+import {useAppStore} from "../store/app";
+const serverUrl = import.meta.env.VITE_SERVER_URL;
+
+export const findMenuById = function(menuList, id) {
+    for (var i = 0; i < menuList.length; i++) {
+        var menu = menuList[i];
+        if(menu.id + '' === id + '') {
+            return menu;
+        }
+        // 如果是二级或多级
+        if(menu.children) {
+            var menu2 = findMenuById(menu.children, id);
+            if(menu2) {
+                return menu2;
+            }
+        }
+    }
+    return null;
+};
+
+// 将一维平面数组转换为 Tree 菜单 (根据其指定的 parentId 添加到其父菜单的 children)
+export const arrayToTree = function(menuList) {
+    for (var i = 0; i < menuList.length; i++) {
+        var menu = menuList[i];
+        // 如果这个 Menu 指定了 parentId 属性,则将其转移到其指定的父 Menu 的 children 属性上
+        if(menu.parentId) {
+            var parentMenu = findMenuById(menuList, menu.parentId);
+            if(parentMenu) {
+                menu.parentMenu = parentMenu;
+                parentMenu.children = parentMenu.children || [];
+                parentMenu.children.push(menu);
+                menuList.splice(i, 1);	// 从一维中删除
+                i--;
+            }
+        }
+    }
+    return menuList;
+};
+
+// 返回菜单集合的 一维数组 形式 (将树形菜单转化为一维数组并返回) 方便遍历
+export const treeToArray = function(menuList) {
+    var arr = [];
+    function _dg(_menuList) {
+        _menuList = _menuList || [];
+        for (var i = 0; i < _menuList.length; i++) {
+            var menu = _menuList[i];
+            arr.push(menu);
+            // 如果有子菜单
+            if(menu.children) {
+                _dg(menu.children);
+            }
+        }
+    }
+    _dg(menuList);
+    return arr;
+};
+
+// 获取菜单的所有id
+export const getAllId = function(menuList) {
+    var arr = [];
+    treeToArray(menuList).forEach(function(item) {
+        arr.push(item.id);
+    });
+    return arr;
+};
+
+// 根据 url 获取文件后缀
+export const getUrlExt = function(url) {
+    if(!url) {
+        return "";
+    }
+    if(url.indexOf('?') > -1) {
+        url = url.split('?')[0];
+    }
+    if(url.indexOf('#') > -1) {
+        url = url.split('#')[0];
+    }
+    var index= url.lastIndexOf(".");
+    if(index === -1) {
+        return "";
+    }
+    return url.substr(index + 1);
+};
+
+// 从数组中删除指定元素
+export const arrayDelete = function(arr, item){
+    if(item instanceof Array) {
+        for (let i = 0; i < item.length; i++) {
+            let ite = item[i];
+            let index = arr.indexOf(ite);
+            if (index > -1) {
+                arr.splice(index, 1);
+            }
+        }
+    } else {
+        var index = arr.indexOf(item);
+        if (index > -1) {
+            arr.splice(index, 1);
+        }
+    }
+}
+
+// 返回一个 iframe 视图 (此方法构建的视图会在打包后不显示,原因未知)
+export const getIframeView = function (name, url) {
+    return {
+        name: name,
+        template: `<div><iframe class="iframe-view" src="${url}" /></div>`
+    }
+}
+
+// 返回随机数
+export const randomNum = function(min, max) {
+    min = min || 1;
+    max = max || 1000000000;
+    return parseInt(Math.random() * (max - min + 1) + min, 10);
+};
+
+// 定义菜单的方法,此方法不会转换数据结构,只会为 menu 补上一些默认值
+export const defineMenu = function (_menuList) {
+    for (let menu of _menuList) {
+        // 如果是一个父菜单,则递归解析
+        if(menu.children) {
+            menu.children = defineMenu(menu.children);
+            continue;
+        }
+
+        // 缺少必要属性时,不再加工数据
+        if (menu.title === undefined) {
+            continue;
+        }
+
+        // 创建一些必要的属性
+        menu.id = menu.id + '';
+        menu.name = menu.name || menu.id;
+        menu.path = menu.path || '/' + menu.id;
+        menu.show = menu.show || 'auth';
+        // menu.jumpPath = menu.path;
+        menu.meta = menu.meta || {};
+        if(menu.url) {
+            if(menu.url.indexOf('${SERVER_URL}') === 0) {
+                menu.url = menu.url.replace('${SERVER_URL}', serverUrl);
+            }
+        }
+
+        // 如果指定了 menu.componentPath 值,则强制其以 /@/ 开头
+        if(menu.componentPath) {
+            if(menu.componentPath.indexOf("/@") === 0) {
+                
+            } else if (menu.componentPath.indexOf("@") === 0) {
+                menu.componentPath = "/" + menu.componentPath;
+            } else {
+                menu.componentPath = "/@" + menu.componentPath;
+            }
+            // menu.component = () => import(/* @vite-ignore */menu.componentPath);
+        }
+
+        // 如果是 iframe 视图,则创建对应的 component
+        if(menu.url && menu.isBlank !== true) {
+            // menu.component = getIframeView(menu.name, menu.url);
+            menu.meta.iframeUrl = menu.url;
+            menu.component = () => import('../layout/view/iframe-view');
+        }
+
+    }
+    return _menuList;
+}
+
+// path 与 Route 的对应关系表
+// 由于在初始界面时,vue-router 获取到的 route 对象读取不到 meta 元信息,尚未不知道是bug还是框架故意而为之,所以这里自建映射关系表,以便后续操作 
+const pathRouteMap = {};
+
+// 写入 path 与 Route 对象的映射  
+export const setRouteByPath = function(path, route) {
+    pathRouteMap[path] = route;
+}
+
+// 根据 path 寻找对应的 Route 对象 
+export const getRouteByPath = function(path) {
+    return pathRouteMap[path];
+}
+
+// 将提供 Menu 菜单对象,翻译为 vue-router 需要的 Route 路由对象,并提供回调函数操作 
+export const menuToRouter = function(_menuList, callback) {
+    for (let menu of _menuList) {
+        // 如果是一个父菜单,则递归解析
+        if(menu.children) {
+            menuToRouter(menu.children, callback);
+            continue;
+        }
+
+        // 如果不是一个路由,而仅仅只是一个菜单
+        if (menu.title === undefined || menu.component === undefined) {    //  || menu.path === undefined ||
+            continue;
+        }
+
+        // 创建此 Menu 对应的 Route
+        const route = {
+            name: menu.name,
+            path: menu.path,
+            component: menu.component,
+            meta: {
+                ... (menu.meta || {}),
+                menu: menu
+            },
+        }
+
+        // 写入 path 与 Route 对象的映射  
+        setRouteByPath(route.path, route);
+
+        // 调用回调函数操作 
+        if(callback) {
+            callback(route);
+        }
+    }
+    return _menuList;
+}
+
+
+// 设置网页标题
+export const setTitle = function (route) {
+    if(route.meta.menu) {
+        document.title = route.meta.menu.title + ' - ' + useSettingStore().title;
+    }
+}
+
+// 判断一个菜单是否应该显示
+export const menuIsShow = function (menu) {
+    if(menu.show === 'yes') {
+        return true;
+    }
+    if(menu.show === 'no') {
+        return false;
+    }
+    return useAppStore().showList.indexOf(menu.id) > -1;
+}
+
+// 判断一个菜单是否为目录
+export const menuIsDir = function (menu) {
+    // 如果强制指定了是一个目录,则直接返回 true
+    if(menu.type === 'dir') {
+        return true;
+    }
+    
+    // 如果存在子菜单
+    if (menu.children) {
+        // 如果存在 component、url、type=btn,则依然认定还是子菜单
+        if(menu.component || menu.url || menu.type === 'btn') {
+            return false;
+        }
+        return true;
+    } else {
+        // 不存在 children 属性,必为子菜单 
+        return false;
+    }
+}
+

+ 48 - 0
ssp-admin-vue3/src/router/routes-dynamic.js

@@ -0,0 +1,48 @@
+/**
+ * 动态路由表:一般情况下你不需要改动此文件,因为所有动态路由都是由 menu-list 转化生成的
+ */
+import {homeRoute} from "./home";
+import Layout from '../layout/index.vue';
+import {pathToComponent} from "./async-import";
+import {menuToRouter} from "./router-util";
+import router from "./index";
+
+// Layout 主视图路由 
+export const layoutRoute = {
+    name: 'layout',
+    path: '/',
+    redirect: homeRoute.path,
+    component: Layout,
+    children: [homeRoute]
+};
+
+// 动态路由表
+export const dynamicRoutes = [layoutRoute];
+
+
+/**
+ * 根据菜单信息,把组件动态添加到路由表,用于从后端加载菜单信息的场景
+ */
+export const addRoutesByMenuList = function (menuList) {
+    // 补上缺少的菜单属性
+    pathToComponent(menuList);
+
+    // 将菜单指向的 route 添加到 vue-router 路由表中 
+    menuToRouter(menuList, route => {
+        // console.log('--- 追加 route: ', route)
+        router.addRoute('layout', route);
+    });
+
+    // 安装 404 路由
+    const whole404 = {
+        name: 'whole404',
+        path: "/:pathMatch(.*)*",
+        // redirect: '/404',   // 使用 redirect 方式,会导致浏览器path也变成404,不方便调试,所以改为 component 显示方式 
+        component: () => import('/@/layout/view/404'),
+    }
+    router.addRoute(whole404);
+
+    // 刷新一下页面,新添加的路由才会及时显示出来 
+    const route = router.currentRoute.value;
+    router.replace(route.fullPath);
+}

+ 55 - 0
ssp-admin-vue3/src/router/routes-static.js

@@ -0,0 +1,55 @@
+/**
+ * 定义静态路由表:
+ * 1、在任何权限下都可以访问,例如:login、401、404
+ * 2、这些路由不会出现在菜单树上
+ */
+import Layout from '/@/layout/index.vue';
+import {homeRoute} from "./home";
+
+/**
+ * 静态路由表
+ */
+export const staticRoutes = [
+    // 登录页
+    {
+        name: 'login',
+        path: '/login',
+        component: () => import('/@/sp-views/login/index.vue'),
+    },
+    // 404
+    {
+        name: '403',
+        path: "/403",
+        component: () => import('/@/layout/view/403'),
+    },
+    // 404
+    {
+        name: '404',
+        path: "/404",
+        component: () => import('/@/layout/view/404'),
+    },
+    // iframe 视图
+    {
+        redirect: homeRoute.path,
+        component: Layout,
+        children: [
+            {
+                name: 'iframe',
+                path: '/iframe',
+                component: () => import('/@/layout/view/iframe-view'),
+            }
+        ],
+    },
+    {
+        name: 'whole404',
+        path: "/:pathMatch(.*)*",
+        component: () => import('/@/layout/view/whole404'),
+        // redirect: '/404',   // 使用 redirect 方式,会导致浏览器path也变成404,不方便调试,所以改为 component 显示方式 
+        // component: () => import('/@/layout/view/404'),
+    }
+];
+
+// 判断一个路由是否为静态路由 
+export const isStaticRoute = function (to) {
+    return ['login', '403', '404', 'iframe', homeRoute.name].indexOf(to.name) > -1;
+}

+ 108 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-enum.vue

@@ -0,0 +1,108 @@
+<!-- in-enum 枚举 -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules ">
+        <el-radio-group v-if="jtype === '1'" :model-value="modelValue" @change="onChange">
+            <el-radio label="" v-if="def">{{def}}</el-radio>
+            <el-radio v-for="j in jvList" :key="j.key" :label="j.key">{{j.value}}</el-radio>
+        </el-radio-group>
+        <el-radio-group v-if="jtype === '2'" :model-value="modelValue" @change="onChange" class="s-radio-text">
+            <el-radio label="" v-if="def">{{def}}</el-radio>
+            <el-radio v-for="j in jvList" :key="j.key" :label="j.key">{{j.value}}</el-radio>
+        </el-radio-group>
+        <el-radio-group v-if="jtype === '3'" :model-value="modelValue" @change="onChange" size="default">
+            <el-radio-button label="" v-if="def">{{def}}</el-radio-button>
+            <el-radio-button v-for="j in jvList" :key="j.key" :label="j.key">{{j.value}}</el-radio-button>
+        </el-radio-group>
+        <el-select v-if="jtype === '4'" :model-value="modelValue" @change="onChange" style="width: 100%;">
+            <el-option label="" v-if="def" :value="def"></el-option>
+            <el-option v-for="j in jvList" :key="j.key" :label="j.value" :value="j.key"></el-option>
+        </el-select>
+        <!-- 备注信息 -->
+        <p class="c-remark" v-if="remark" style="width: 100%;">{{remark}}</p>
+    </el-form-item>
+</template>
+
+<script setup name="in-enum">
+import {getCurrentInstance, onMounted, ref, watch} from "vue";
+const { proxy } = getCurrentInstance();
+
+// 形参 
+defineProps({
+    // 类型
+    type: {},
+    // 标题 
+    name: {},
+    // 绑定值 
+    modelValue: {},
+    // 提示文字 
+    placeholder: {},
+    // 备注文字
+    remark: {}, 
+    // 是否禁用 
+    disabled: {},
+    // type=enum时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+    jv: {default: ''},
+    // type=enum时,具体的展示类型 -- 1=单选框,2=单选文字,3=单选按钮,4=单选下拉框
+    jtype: {default: '1'},
+    // type=enum时,增加的默认项文字 
+    def: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+});
+
+// 解析的枚举列表 
+const jvList = ref([]);
+
+// 解析枚举 
+const parseJv = function(jv) {
+    jv = jv || proxy.jv;
+    jvList.value = [];
+    for(let key in jv) {
+        let value = jv[key];
+        let color = '';
+        // 
+        if(value.indexOf('[') !== -1 && value.endsWith(']')) {
+            let index = value.indexOf('[');
+            color = value.substring(index + 1, value.length - 1);
+            value = value.substring(0, index);
+            // console.log(color + ' --- ' + value);
+        }
+        // 
+        if(isNaN(key) === false) {
+            key = parseInt(key);
+        }
+        // 
+        jvList.value.push({
+            key: key,
+            value: value,
+            color: color
+        })
+    }
+};
+
+// input值发生变化时触发
+const onChange = function($event) {
+    proxy.$emit('update:modelValue', $event);
+};
+
+// 初始化
+onMounted(function (){
+    parseJv();
+});
+//
+// 二次变动时重新解析 
+watch(() => proxy.jv, (oldVal, newVal) => {
+    if(JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
+        parseJv();
+    }
+})
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 112 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-input.vue

@@ -0,0 +1,112 @@
+<!-- in 单行输入 -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules ">
+        <!-- 普通输入框 -->
+        <el-input v-if="type === 'text'" 
+                  type="text" :model-value="modelValue" @input="onInput" :placeholder="placeholder" :disabled="disabled" />
+        <!-- 数字输入框 -->
+        <el-input v-else-if="type === 'num'" 
+                  type="number" :model-value="modelValue" @input="onInput" :placeholder="placeholder" :disabled="disabled" />
+        <!-- 密码输入框 -->
+        <el-input v-else-if="type === 'password'" 
+                  type="password" :model-value="modelValue" @input="onInput" :placeholder="placeholder" :disabled="disabled" />
+        <!-- 多行输入框 -->
+        <el-input v-else-if="type === 'textarea'" 
+                  type="textarea" :model-value="modelValue" @input="onInput"
+                  :autosize="{minRows: 3, maxRows: 10}" :placeholder="placeholder" :disabled="disabled"></el-input>
+        <!-- 日期 -->
+        <el-date-picker v-else-if="type === 'date'" 
+                        type="date" v-model="spareValue" @change="onInput"
+                        value-format="YYYY-MM-DD" :placeholder="placeholder" :disabled="disabled"></el-date-picker>
+        <!-- 日期时间 -->
+        <el-date-picker v-else-if="type === 'datetime'" 
+                        type="datetime" v-model="spareValue" @change="onInput"
+                        value-format="YYYY-MM-DD HH:mm:ss" :placeholder="placeholder" :disabled="disabled"></el-date-picker>
+        <!-- 时间 -->
+        <el-time-picker v-else-if="type === 'time'"
+                        type="datetime" v-model="spareValue" @change="onInput"
+                        value-format="HH:mm:ss" :placeholder="placeholder" :disabled="disabled"></el-time-picker>
+        <!-- 滑块参数 -->
+        <el-slider v-else-if="type === 'slider'"
+                   :model-value="modelValue" @input="onInput" :disabled="disabled"></el-slider>
+        <!-- 颜色输入 -->
+        <div v-else-if="type === 'color'"
+             style="line-height: 0;">
+            <el-color-picker :model-value="modelValue" @change="onInput" :disabled="disabled"></el-color-picker>
+        </div>
+        <!-- 评分组件 -->
+        <div v-else-if="type === 'rate'"
+             style="line-height: 0;">
+            <el-rate :model-value="modelValue" @change="onInput" show-text :disabled="disabled"></el-rate>
+        </div>
+        <!-- img 图片 -->
+        <template v-else-if="type === 'img'">
+            <img :src="modelValue" class="nar-img" @click="sa.showImage(modelValue, '400px', '400px')" v-if="!sa.isNull(modelValue)" alt="img">
+            <el-link type="primary" @click="sa.uploadImage(src => {$emit('update:modelValue', src); sa.ok2('上传成功');})">上传</el-link>
+        </template>
+        <!-- audio 音频 -->
+        <template v-else-if="type === 'audio'">
+            <el-link class="file-link" type="info" :href="modelValue" target="_blank" v-if="!sa.isNull(modelValue)">{{modelValue}}</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadAudio(src => {$emit('update:modelValue', src); sa.ok2('上传成功');})">上传</el-link>
+        </template>
+        <!-- video 视频 -->
+        <template v-else-if="type === 'video'">
+            <el-link class="file-link" type="info" :href="modelValue" target="_blank" v-if="!sa.isNull(modelValue)">{{modelValue}}</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadVideo(src => {$emit('update:modelValue', src); sa.ok2('上传成功');})">上传</el-link>
+        </template>
+        <!-- file 文件 -->
+        <template v-else-if="type === 'file'">
+            <el-link class="file-link" type="info" :href="modelValue" target="_blank" v-if="!sa.isNull(modelValue)">{{modelValue}}</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadFile(src => {$emit('update:modelValue', src); sa.ok2('上传成功');})">上传</el-link>
+        </template>
+
+        <!-- 备注信息 -->
+        <span class="c-remark" v-if="remark">{{remark}}</span>
+    </el-form-item>
+</template>
+
+<script setup name="in-input">
+import {getCurrentInstance} from "vue";
+const { proxy } = getCurrentInstance();
+
+const props = defineProps({
+    // 类型:text、num、password、date、datetime 等等
+    type: {
+      default: 'text'  
+    },
+    // 标题 
+    name: {},
+    // 绑定值 
+    modelValue: {},
+    // 提示文字 
+    placeholder: {},
+    // 备注文字
+    remark: {}, 
+    // 是否禁用 
+    disabled: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+})
+
+const emit = defineEmits(["update:modelValue"])
+
+// 子组件定义自己的 spareValue 
+const spareValue = computed({
+    get: () => props.modelValue,
+    set: (value) => emit("update:modelValue", value),
+})
+
+// input值发生变化时触发
+const onInput = function($event) {
+    // console.log('change', $event)
+    proxy.$emit('update:modelValue', $event);
+};
+
+</script>
+
+<style scoped>
+
+</style>

+ 23 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-item.vue

@@ -0,0 +1,23 @@
+<!-- in-item 自定义slot -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules ">
+        <slot></slot>
+    </el-form-item>
+</template>
+
+<script setup name="in-item">
+
+defineProps({
+    name: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+})
+
+</script>
+
+<style scoped>
+
+</style>

+ 23 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-item2.vue

@@ -0,0 +1,23 @@
+<!-- in-item 自定义slot (点击label时不会触发事件) -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules " @click.stop="$event.preventDefault()">
+        <slot></slot>
+    </el-form-item>
+</template>
+
+<script setup name="in-item">
+
+defineProps({
+    name: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+})
+
+</script>
+
+<style scoped>
+
+</style>

+ 134 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-list.vue

@@ -0,0 +1,134 @@
+<!-- in-list 列表输入 -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules ">
+        <!-- img-list 图片列表 -->
+        <div v-if="type === 'img-list'" class="image-box">
+            <div class="image-box-2" v-for="item in valueArray">
+                <img :src="item.value" @click="sa.showImage(item.value, '500px', '400px')" alt="img"/>
+                <p>
+                    <el-link @click="valueArrayDelete(item)" style="color: #999;">
+                        <el-icon><el-icon-Close style="margin-right: 3px;"/></el-icon>
+                        <span>移除</span>
+                    </el-link>
+                </p>
+            </div>
+            <!-- 上传 -->
+            <div class="image-box-2 up-img-box" @click="sa.uploadImageList(src => valueArrayPush({value: src}))">
+                <img src="../../img/up-icon.png" alt="img">
+            </div>
+        </div>
+        <!-- audio-list、video-list、file-list、img-video-list -->
+        <div v-else-if="['audio-list', 'video-list', 'file-list', 'img-video-list'].indexOf(type) !== -1">
+            <div v-for="item in valueArray">
+                <el-link type="info" :href="item.value" target="_blank">{{item.value}}</el-link>
+                <el-link type="danger" class="del-rr" @click="valueArrayDelete(item)">
+                    <el-icon><el-icon-Close style="margin-right: 3px;"/></el-icon>
+                    <small style="vertical-align: top;">删除</small>
+                </el-link>
+            </div>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadAudioList(src => valueArrayPush({value: src}))" v-if="type === 'audio-list'">上传</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadVideoList(src => valueArrayPush({value: src}))" v-if="type === 'video-list'">上传</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadFileList(src => valueArrayPush({value: src}))" v-if="type === 'file-list'">上传</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadImageList(src => valueArrayPush({value: src}))" v-if="type === 'img-video-list'">上传图片</el-link>
+            <el-link class="up-btn-link" type="primary" @click="sa.uploadVideoList(src => valueArrayPush({value: src}))" v-if="type === 'img-video-list'" style="margin-left: 7px;">上传视频</el-link>
+        </div>
+
+        <!-- text-list 文本列表 -->
+        <div v-if="type === 'text-list'" style="width: 100%;">
+            <div v-for="item in valueArray" style="width: 100%;">
+                <el-input v-model="item.value" @input="valueArrayChange" class="in-text-list-input"></el-input>
+                <el-link type="danger" class="del-rr" @click="valueArrayDelete(item)">
+                    <el-icon><el-icon-Close style="margin-right: 3px;"/></el-icon>
+                    <small style="vertical-align: top;">删除</small>
+                </el-link>
+            </div>
+            <el-link type="primary" @click="valueArrayPush({value: ''})">[ + 添加 ]</el-link>
+        </div>
+        
+        <!-- 备注信息 -->
+        <span class="c-remark" v-if="remark">{{remark}}</span>
+    </el-form-item>
+</template>
+
+<script setup name="in-list">
+import sa from '../../sa'
+import {getCurrentInstance, onMounted, ref, watch} from "vue";
+const { proxy } = getCurrentInstance();
+
+// 形参 
+defineProps({
+    // 类型
+    type: {},
+    // 标题 
+    name: {},
+    // 绑定值 
+    modelValue: {},
+    // 提示文字 
+    placeholder: {},
+    // 备注文字
+    remark: {}, 
+    // 是否禁用 
+    disabled: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+});
+
+// 解析的元素List 
+const valueArray = ref([]);
+
+// 解析 value 为 valueArray
+const valueToArray = function(value) {
+    value = value || proxy.modelValue;
+    let arr = sa.isNull(value) ? [] : value.split(',');
+    let arr2 = [];
+    for (let i = 0; i < arr.length; i++) {
+        if(arr[i] !== '' && arr[i].trim() !== '') {
+            arr2.push({value: arr[i]});
+        }
+    }
+    valueArray.value = arr2;
+};
+    
+// 给 valueArray 数组增加值
+const valueArrayPush = function(item) {
+    valueArray.value.push(item);
+    valueArrayChange();
+};
+
+// valueArray 数组删除值 
+const valueArrayDelete = function(item) {
+    sa.arrayDelete(valueArray.value, item);
+    valueArrayChange();
+};
+
+// valueArray 更改值时触发 
+const valueArrayChange = function() {
+    proxy.$emit('update:modelValue', sa.getArrayField(valueArray.value, 'value').join(','));
+};
+
+// 初始化
+onMounted(valueToArray);
+
+// 监听变化 
+watch(() => proxy.modelValue, () => {
+    if(proxy.type !== 'text-list') {
+        valueToArray(proxy.modelValue);
+    }
+})
+
+// 对外开放方法,方便二次修改 
+defineExpose({
+    setValue: function (value) {
+        valueToArray(value)
+    }
+})
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 52 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-money-f.vue

@@ -0,0 +1,52 @@
+<!-- in-money-f 钱,单位:分 (内部输入框显示的是元,外部 v-model 得到的是分) -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules ">
+        <el-input type="number" 
+                  v-model="realValue" @input="onInput" :placeholder="placeholder" :disabled="disabled" />
+        <span class="c-remark" v-if="remark">{{remark}}</span>
+    </el-form-item>
+</template>
+
+<script setup name="in-money-f">
+import {getCurrentInstance, onMounted} from "vue";
+const { proxy } = getCurrentInstance();
+
+defineProps({
+    // 标题 
+    name: {},
+    // 绑定值 
+    modelValue: {},
+    // 提示文字 
+    placeholder: {},
+    // 备注文字
+    remark: {}, 
+    // 是否禁用 
+    disabled: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+})
+
+// 真实的值 -- 将外界的分 转化为本组件的元  
+const realValue = ref(''); 
+const initRealValue = () => {
+    const value = proxy.modelValue / 100;
+    if(isNaN(value) === false) {
+        realValue.value = value;
+    }
+}
+onMounted(initRealValue)
+watch(() => proxy.modelValue, initRealValue);
+
+// input值发生变化时触发
+const onInput = function($event) {
+    proxy.$emit('update:modelValue', $event * 100);
+};
+
+</script>
+
+<style scoped>
+
+</style>

+ 90 - 0
ssp-admin-vue3/src/sa-frame/com/in/in-rich-text.vue

@@ -0,0 +1,90 @@
+<!-- in-rich-text 富文本 -->
+<template>
+    <el-form-item class="c-item" :label="name" :rules="required ? {required: true} : rules ">
+        <div class="editor-box">
+            <div :id="'editor-' + editorId"></div>
+        </div>
+        <!-- 备注信息 -->
+        <span class="c-remark" v-if="remark">{{remark}}</span>
+    </el-form-item>
+</template>
+
+<script setup name="in-rich-text">
+import sa from '../../sa'
+import {getCurrentInstance, onMounted, ref, watch} from "vue";
+const { proxy } = getCurrentInstance();
+import E from 'wangeditor';
+
+// 形参 
+defineProps({
+    // 类型
+    type: {},
+    // 标题 
+    name: {},
+    // 绑定值 
+    modelValue: {},
+    // 提示文字 
+    placeholder: {},
+    // 备注文字
+    remark: {}, 
+    // 是否禁用 
+    disabled: {},
+    // 表单校验 
+    rules: {},
+    required: {
+        type: Boolean
+    }
+});
+
+// 编辑器id 
+const editorId = ref(sa.randomString(32));
+const editorObj = ref({});
+
+// 创建编辑器
+// 创建富文本编辑器
+const createEditor = function(content) {
+    // var E = window.wangEditor;
+    content = content || proxy.modelValue;
+    const editor = new E('#editor-' + editorId.value);
+
+    editor.config.menus = [
+        'head', 'fontSize', 'fontName', 'italic', 'underline', 'strikeThrough', 'foreColor', 'backColor', 'link', 'list',
+        'justify', 'quote', 'emoticon', 'image', 'table', 'code', 'undo', 'redo' // 重复
+    ]
+    editor.config.debug = true; // debug模式
+    // editor.config.uploadFileName = 'file'; // 图片流name
+    editor.config.withCredentials = true; // 跨域携带cookie
+    editor.config.uploadImgMaxSize = 100 * 1024 * 1024;	// 图片大小最大100M
+    // editor.config.uploadImgShowBase64 = true   	// 使用 base64 保存图片
+    // 监听内容变动
+    editor.config.onchange = function(newHtml) {
+        proxy.$emit('update:modelValue', newHtml);
+    };
+    // 重写上传图片的函数到OSS
+    editor.config.customUploadImg = function(files, insert) {
+        const file = files[0]; // 文件对象
+        sa.startUploadImage(file, function(src) {
+            insert(src);
+            sa.msg('上传成功');
+        });
+    }
+    editor.create(); // 创建
+    editor.txt.html(content);	// 为编辑器赋值
+    editorObj.value = editor;
+}
+
+// 初始化
+onMounted(createEditor);
+
+// 对外开放方法,方便二次修改 
+defineExpose({
+    setValue: function (value) {
+        editorObj.value.txt.html(value);
+    }
+})
+
+</script>
+
+<style scoped>
+
+</style>

+ 46 - 0
ssp-admin-vue3/src/sa-frame/com/more/com-fast-btn.vue

@@ -0,0 +1,46 @@
+<!-- 快捷增删改查按钮 -->
+<template>
+    <div class="fast-btn">
+        <el-button type="primary" icon="el-icon-Plus" @click="$emit('add')" v-if="showBtns.indexOf('add') !== -1">新增</el-button>
+<!--        <el-button type="success" icon="el-icon-View" @click="$parent.getBySelect()" v-if="showBtns.indexOf('get') !== -1">查看</el-button>-->
+        <el-button type="primary" icon="el-icon-Download" @click="sa.exportExcel()" v-if="showBtns.indexOf('export') !== -1">导出</el-button>
+        <el-button type="danger" plain icon="el-icon-Delete" @click="$emit('deleteByIds')" v-if="showBtns.indexOf('delete') !== -1">删除</el-button>
+        <el-button type="default" plain icon="el-icon-Refresh"  @click="appStore.f5NativeTab()" v-if="showBtns.indexOf('reset') !== -1">重置</el-button>
+        <slot></slot>
+    </div>
+</template>
+
+<script setup name="com-fast-btn">
+import {getCurrentInstance, onMounted, ref} from "vue";
+import {useAppStore} from "../../../store/app";
+const {proxy} = getCurrentInstance();
+const appStore = useAppStore();
+
+defineProps({
+    // 快捷按钮显示列表,形如:add,get,delete,export,reset 
+    show: {},
+})
+
+// 快捷按钮显示按钮列表 
+const showBtns = ref([]);
+
+// 解析
+onMounted(function (){
+    let arr = proxy.show.split(',');
+    for (var i = 0; i < arr.length; i++) {
+        arr[i] = arr[i].trim();
+    }
+    showBtns.value = arr;
+})
+
+const ddd = () => {
+    console.log('dddd')
+    console.log(proxy.$parent)
+    proxy.$parent.deleteByIds();
+}
+
+</script>
+
+<style scoped>
+
+</style>

+ 44 - 0
ssp-admin-vue3/src/sa-frame/com/more/com-page.vue

@@ -0,0 +1,44 @@
+<!-- 分页组件封装 -->
+<template>
+    <div class="page-box">
+        <el-pagination 
+            background
+            layout="total, prev, pager, next, sizes, jumper"
+            v-model:current-page="curr"
+            v-model:page-size="size"
+            :total="total"
+            :page-sizes="sizes || [1, 10, 20, 30, 40, 50, 100]"
+            @current-change="changePage()"
+            @size-change="changePage()">
+        </el-pagination>
+    </div>
+</template>
+
+<script setup name="com-page">
+import {getCurrentInstance} from "vue";
+const { proxy } = getCurrentInstance();
+
+
+defineProps({
+    // 当前页 
+    curr: {}, 
+    // 页大小 
+    size: {}, 
+    // 数据总数
+    total: {},
+    // 页大小合集
+    sizes: {}
+})
+
+// 刷新分页 
+const changePage = function() {
+    proxy.$emit('update:curr', proxy.curr);
+    proxy.$emit('update:size', proxy.size);
+    proxy.$emit('change');
+};
+
+</script>
+
+<style scoped>
+
+</style>

+ 73 - 0
ssp-admin-vue3/src/sa-frame/com/show/show-enum.vue

@@ -0,0 +1,73 @@
+<!-- show-enum 枚举 -->
+<template>
+    <el-form-item :label="name" class="c-item">
+        <span v-for="j in jvList" :key="j.key">
+			<b :style="{color: j.color || '#303236'}" v-if="value === j.key">{{j.value}}</b>
+		</span>
+    </el-form-item>
+</template>
+
+<script setup name="show-enum">
+import {getCurrentInstance, onMounted, ref, watch} from "vue";
+const { proxy } = getCurrentInstance();
+
+// 形参 
+defineProps({
+    // 标题 
+    name: {},
+    // 绑定值 
+    value: {},
+    // type=enum时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+    jv: {default: ''},
+    // type=enum时,增加的默认项文字 
+    def: {},
+});
+
+// 解析的枚举列表 
+const jvList = ref([]);
+
+// 解析枚举 
+const parseJv = function(jv) {
+    jv = jv || proxy.jv;
+    jvList.value = [];
+    for(let key in jv) {
+        let value = jv[key];
+        let color = '';
+        // 
+        if(value.indexOf('[') !== -1 && value.endsWith(']')) {
+            let index = value.indexOf('[');
+            color = value.substring(index + 1, value.length - 1);
+            value = value.substring(0, index);
+            // console.log(color + ' --- ' + value);
+        }
+        // 
+        if(isNaN(key) === false) {
+            key = parseInt(key);
+        }
+        // 
+        jvList.value.push({
+            key: key,
+            value: value,
+            color: color
+        })
+    }
+};
+
+// 初始化
+onMounted(function (){
+    parseJv();
+});
+//
+// 二次变动时重新解析 
+watch(() => proxy.jv, (oldVal, newVal) => {
+    if(JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
+        parseJv();
+    }
+})
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 88 - 0
ssp-admin-vue3/src/sa-frame/com/show/show-info.vue

@@ -0,0 +1,88 @@
+<!-- show-info 信息展示框 -->
+<template>
+    <el-form-item :label="name" class="c-item">
+        <!-- slot 插槽模式 -->
+        <template v-if="$slots.default">
+            <slot></slot>
+        </template>
+        
+        <!-- 如果 value 为空 -->
+        <span v-else-if="!value">无</span>
+        
+        <!-- text 普通信息展示 -->
+        <span v-else-if="type === 'text'">{{ value }}</span>
+
+        <!-- num 数字 -->
+        <span v-else-if="type === 'num'" class="tc-num">{{ value }}</span>
+
+        <!-- date 日期 -->
+        <span v-else-if="type === 'date'" class="tc-date">{{ sa.forDate(value) }}</span>
+
+        <!-- datetime 日期时间 -->
+        <span v-else-if="type === 'datetime'" class="tc-date">{{ sa.forDate(value, 2) }}</span>
+
+        <!-- color 颜色 -->
+        <div v-else-if="type === 'color'">
+            <span class="show-color-item" :style="{backgroundColor: value}"></span>
+            <span style="color: #666">{{ value }}</span>
+        </div>
+        
+        <!-- rate 评分组件 -->
+        <div v-else-if="type === 'rate'"
+             style="line-height: 0;">
+            <el-rate v-model="value" show-text disabled></el-rate>
+        </div>
+        
+        <!-- money 金额,单位:元 -->
+        <b v-else-if="type === 'money'" class="c-price">¥{{value}}</b>
+        
+        <!-- money-f 金额,单位:分 -->
+        <b v-else-if="type === 'money-f'" class="c-price">¥{{value / 100}}</b>
+        
+        <!-- img 图片 -->
+        <img v-else-if="type === 'img'" 
+             :src="value" class="nar-img" @click="sa.showImage(value, '400px', '400px')" alt="img" />
+        
+        <!-- audio、video、file -->
+        <el-link v-else-if="['audio', 'video', 'file'].indexOf(type) !== -1" 
+                 type="info" :href="value" target="_blank" v-if="!sa.isNull(value)">{{value}}</el-link>
+
+        <!-- rich-text 富文本 -->
+        <div v-else-if="type === 'rich-text'" class="editor-box content-box-info">
+            <div v-html="value"></div>
+        </div>
+        
+        <!-- link 连接 -->
+        <el-link v-else-if="type === 'link'" 
+                 type="primary" :href="value" target="_blank">{{value}}</el-link>
+        
+        
+    </el-form-item>
+</template>
+
+<script setup name="show-info">
+import {getCurrentInstance} from "vue";
+const { proxy } = getCurrentInstance();
+
+defineProps({
+    // 类型
+    type: {
+        default: 'text'
+    },
+    // 标题 
+    name: {},
+    // 绑定值 
+    value: {},
+})
+
+// input值发生变化时触发
+const onInput = function($event) {
+    // console.log('change', $event)
+    proxy.$emit('update:modelValue', $event);
+};
+
+</script>
+
+<style scoped>
+
+</style>

+ 80 - 0
ssp-admin-vue3/src/sa-frame/com/show/show-list.vue

@@ -0,0 +1,80 @@
+<!-- show-list 展示列表 -->
+<template>
+    <el-form-item :label="name" class="c-item">
+        <!-- 如果 value 为空 -->
+        <span v-if="!value">无</span>
+        
+        <!-- img-list 图片列表 -->
+        <div v-else-if="type === 'img-list'" class="image-box image-box-info">
+            <div class="image-box-2" v-if="valueArray.length > 0" v-for="item in valueArray">
+                <img :src="item.value" @click="sa.showImage(item.value, '500px', '400px')" alt="img"/>
+            </div>
+        </div>
+        <!-- audio-list、video-list、file-list、img-video-list -->
+        <div v-else-if="['audio-list', 'video-list', 'file-list', 'img-video-list'].indexOf(type) !== -1">
+            <div v-for="item in valueArray">
+                <el-link type="info" :href="item.value" target="_blank">{{item.value}}</el-link>
+            </div>
+        </div>
+        <!-- text-list 文本列表 -->
+        <div v-else-if="type === 'text-list'">
+            <div v-for="item in valueArray" class="text-list-item">{{ item.value }}</div>
+        </div>
+        
+    </el-form-item>
+</template>
+
+<script setup name="show-list">
+import sa from '../../sa'
+import {getCurrentInstance, onMounted, ref} from "vue";
+const { proxy } = getCurrentInstance();
+
+// 形参 
+defineProps({
+    // 类型
+    type: {},
+    // 标题 
+    name: {},
+    // 绑定值 
+    value: {},
+});
+
+// 解析的元素List 
+const valueArray = ref([]);
+
+// 解析 value 为 valueArray
+const valueToArray = function(value) {
+    value = value || proxy.value;
+    let arr = sa.isNull(value) ? [] : value.split(',');
+    let arr2 = [];
+    for (let i = 0; i < arr.length; i++) {
+        if(arr[i] !== '' && arr[i].trim() !== '') {
+            arr2.push({value: arr[i]});
+        }
+    }
+    valueArray.value = arr2;
+};
+
+// 初始化
+onMounted(function (){
+    valueToArray(proxy.value);
+});
+
+// 监听变化 
+watch(() => proxy.value, () => {
+    valueToArray(proxy.value);
+})
+
+// 对外开放方法,方便二次修改 
+defineExpose({
+    setValue: function (value) {
+        valueToArray(value)
+    }
+})
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 117 - 0
ssp-admin-vue3/src/sa-frame/com/td/td-enum.vue

@@ -0,0 +1,117 @@
+<!-- td-enum 枚举 -->
+<template>
+    <el-table-column :label="name" :width="width" :min-width="minWidth">
+        <template #default="s">
+            <!-- enum 普通枚举 -->
+            <template v-if="type === 'enum'">
+                <template v-for="j in jvList" :key="j.key">
+                    <b :style="{color: j.color || '#606266'}" v-if="s.row[prop] === j.key">{{j.value}}</b>
+                </template>
+            </template>
+            <!-- switch 开关 -->
+            <template v-if="type === 'switch'">
+                <el-switch
+                    v-model="s.row[prop]" 
+                    v-if='jvList.length >= 2'
+                    :active-value="jvList[0].key" 
+                    :inactive-value="jvList[1].key"
+                    :active-color="jvList[0].color || '#409EFF'" 
+                    :inactive-color="jvList[1].color || '#ccc'"
+                    @change="onChange(s)"
+                    :before-change="beforeChange.bind(this, s)"
+                >
+                </el-switch>
+                <template v-for="j in jvList" :key="j.key">
+                    <span :style="{color: '#999'}" style="margin-left: 5px;" v-if="s.row[prop] === j.key">{{j.value}}</span>
+                </template>
+            </template>
+        </template>
+    </el-table-column>
+</template>
+
+<script setup name="td-enum">
+import {getCurrentInstance, onMounted, ref, watch} from "vue";
+const { proxy } = getCurrentInstance();
+
+defineProps({
+    // 类型 
+    type: {
+        default: 'enum'
+    },
+    // label提示文字
+    name: {},
+    label: {},
+    // 绑定的属性  
+    prop: {},
+    // 宽度 
+    width: {},
+    // 最小宽度
+    minWidth: {},
+    // type=menu时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+    jv: {default: ''},
+    // 空值时显示的文字
+    not: {default: '无'}
+})
+
+// 解析的枚举列表 
+const jvList = ref([]);
+
+// 解析枚举 
+const parseJv = function(jv) {
+    jv = jv || proxy.jv;
+    jvList.value = [];
+    for(let key in jv) {
+        let value = jv[key];
+        let color = '';
+        // 
+        if(value.indexOf('[') !== -1 && value.endsWith(']')) {
+            let index = value.indexOf('[');
+            color = value.substring(index + 1, value.length - 1);
+            value = value.substring(0, index);
+            // console.log(color + ' --- ' + value);
+        }
+        // 
+        if(isNaN(key) === false) {
+            key = parseInt(key);
+        }
+        // 
+        jvList.value.push({
+            key: key,
+            value: value,
+            color: color
+        })
+    }
+};
+
+// 切换时
+const onChange = function (s) {
+    // console.log('change...', JSON.stringify(s.row));
+    proxy.$emit('change', s);
+}
+
+// 切换前 
+const beforeChange = function (s) {
+    return new Promise((resolve, reject)=>{
+        s.resolve = resolve;
+        proxy.$emit('before-change', s);
+    })
+}
+
+// 初始化
+onMounted(function (){
+    parseJv();
+});
+//
+// 二次变动时重新解析 
+watch(() => proxy.jv, (oldVal, newVal) => {
+    if(JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
+        parseJv();
+    }
+})
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 108 - 0
ssp-admin-vue3/src/sa-frame/com/td/td-info.vue

@@ -0,0 +1,108 @@
+<!-- td-info 表格单元格 -->
+<template>
+    <!-- 自定义slot -->
+    <el-table-column v-if="$slots.default" :label="name" :width="width" :min-width="minWidth">
+        <template #default="s">
+            <slot name="default" :row="s.row" :index="s.index"></slot>
+        </template>
+    </el-table-column>
+    <!-- selection框 -->
+    <el-table-column v-else-if="type === 'selection'" type="selection" :width="width || '45px'" :min-width="minWidth"></el-table-column>
+    <!-- index -->
+    <el-table-column v-else-if="type === 'index'" type="index" :label="name" :width="width || '80px'" :min-width="minWidth"></el-table-column>
+    <!-- 其它 -->
+    <el-table-column 
+            v-else 
+            :label="name" :width="width" :min-width="minWidth" 
+            :show-overflow-tooltip="['textarea', 'rich-text'].indexOf(type)  !== -1"
+            >
+        <template #default="s">
+            <!-- 无内容 -->
+            <span v-if="!s.row[prop] && type !== 'user-avatar'">{{ not }}</span>
+            <!-- text 文本 -->
+            <span v-else-if="type === 'text'">{{ s.row[prop] }}</span>
+            <!-- num 数字 -->
+            <span v-else-if="type === 'num'" class="tc-num">{{ s.row[prop] }}</span>
+            <!-- icon 数字 -->
+            <svg-icon v-else-if="type === 'icon'" :name="s.row[prop]" style="font-size: 1.2em; vertical-align: sub;"></svg-icon>
+            <!-- money 钱 (单位 元) -->
+            <b v-else-if="type === 'money'" class="c-price">¥{{ s.row[prop] }}</b>
+            <!-- money-f 钱 (单位 分) -->
+            <b v-else-if="type === 'money-f'" class="c-price">¥{{ s.row[prop] / 100 }}</b>
+            <!-- textarea 文本域 -->
+            <span v-else-if="type === 'textarea'">{{sa.maxLength(s.row[prop], 200)}}</span>
+            <!-- rich-text 富文本 -->
+            <span v-else-if="type === 'rich-text'">{{sa.maxLength(sa.text(s.row[prop]), 200)}}</span>
+            <!-- link 链接 -->
+            <el-link v-else-if="type === 'link'" type="primary" :href="s.row[prop]" target="_blank">{{ s.row[prop] }}</el-link>
+            <!-- link-btn 链接按钮 -->
+            <el-link v-else-if="type === 'link-btn'" type="primary" @click="$emit('click', s)">{{s.row[prop]}}</el-link>
+            
+            <!-- date 日期 -->
+            <span v-else-if="type === 'date'" class-name="tc-date">
+                {{ sa.forDate(s.row[prop]) }}
+            </span>
+            <!-- datetime 日期时间 -->
+            <span v-else-if="type === 'datetime'" class-name="tc-date">
+                {{ sa.forDate(s.row[prop], 2) }}
+            </span>
+            <!-- rate 评分 -->
+            <p v-else-if="type === 'rate'">
+                <el-rate v-model="s.row[prop]" disabled style="line-height: 0; vertical-align: middle;"></el-rate>
+                <span style="vertical-align: middle; color: #666;">{{s.row[prop]}}</span>
+            </p>
+
+            <!-- img 图片 -->
+            <img v-else-if="type === 'img'" :src="s.row[prop]" class="td-img" 
+                 @click="sa.showImage(s.row[prop], '400px', '400px')" alt="img" />
+            <!-- audio、video、file -->
+            <template v-else-if="['audio', 'video', 'file'].indexOf(type) !== -1">
+                <el-link type="info" :href="s.row[prop]" target="_blank">预览</el-link>
+            </template>
+            
+            <!-- 头像 -->
+            <template v-else-if="type === 'user-avatar'">
+                <p v-if="sa.isNull(s.row[prop.split(',')[0]]) && sa.isNull(s.row[prop.split(',')[1]])">暂无</p>
+                <p v-else>
+                    <img :src="s.row[prop.split(',')[1]]" class="td-img"
+                         style="vertical-align: middle; margin-right: 8px;"
+                         @click="sa.showImage(s.row[prop.split(',')[1]], '400px', '400px')" alt="img" />
+                    <b>{{s.row[prop.split(',')[0]]}}</b>
+                </p>
+            </template>
+            
+        </template>
+    </el-table-column>
+    
+    
+</template>
+
+<script setup name="td-info">
+
+defineProps({
+    // 类型 
+    type: {
+        default: 'text'
+    },
+    // label提示文字
+    name: {},
+    label: {},
+    // 绑定的属性  
+    prop: {},
+    // 宽度 
+    width: {},
+    // 最小宽度
+    minWidth: {},
+    // type=menu时,值列表    -- 形如:{1: '正常[green]', 2: '禁用[red]'}  
+    jv: {default: ''},
+    // 空值时显示的文字
+    not: {default: '无'}
+})
+
+defineEmits(['click'])
+
+</script>
+
+<style scoped>
+
+</style>

+ 75 - 0
ssp-admin-vue3/src/sa-frame/com/td/td-list.vue

@@ -0,0 +1,75 @@
+<!-- td-list 在单元格里展示列表数据 -->
+<template>
+    <el-table-column :label="name" :width="width" :min-width="minWidth">
+        <template #default="s">
+            <!-- 如果没有数据 -->
+            <div v-if="!s.row[prop]"> {{ not }} </div>
+            
+            <!-- img-list 图片列表 -->
+            <template v-else-if="type === 'img-list'">
+                <div @click="sa.showImageList(valueToArray(s.row[prop]))" style="cursor: pointer;">
+                    <img :src="valueToArray(s.row[prop])[0]" class="td-img"  alt="img" style=" vertical-align: middle;"/>
+                    <span style="color: #999; padding-left: 0.5em; vertical-align: middle;">点击预览</span>
+                </div>
+            </template>
+            
+            <!-- audio-list、video-list、file-list、img-video-list -->
+            <template v-else-if="['audio-list', 'video-list', 'file-list', 'img-video-list'].indexOf(type) !== -1">
+                <span style="color: #666;">共 {{valueToArray(s.row[prop]).length}} 个</span>
+            </template>
+            
+            <!-- text-list 文本列表 -->
+            <template v-else-if="type === 'text-list'">
+                <p v-for="(item, index) in valueToArray(s.row[prop])" :key="index" class="s-text-list-p">
+                    {{item}}
+                </p>
+            </template>
+
+        </template>
+    </el-table-column>
+</template>
+
+<script setup name="td-list">
+import sa from '../../sa'
+import {getCurrentInstance, onMounted, ref} from "vue";
+const { proxy } = getCurrentInstance();
+
+// 形参 
+defineProps({
+    // 类型 
+    type: {
+        default: 'img-list'
+    },
+    // label提示文字
+    name: {},
+    label: {},
+    // 绑定的属性  
+    prop: {},
+    // 宽度 
+    width: {},
+    // 最小宽度
+    minWidth: {},
+    // 空值时显示的文字
+    not: {default: '无'}
+});
+
+// 解析的元素List 
+// const valueArray = ref([]);
+
+// 解析 value 为 valueArray
+const valueToArray = function(value) {
+    let arr = sa.isNull(value) ? [] : value.split(',');
+    let arr2 = [];
+    for (let i = 0; i < arr.length; i++) {
+        if(arr[i] !== '' && arr[i].trim() !== '') {
+            arr2.push(arr[i]);
+        }
+    }
+    return arr2;
+};
+
+</script>
+
+<style scoped>
+
+</style>

BIN
ssp-admin-vue3/src/sa-frame/img/kulian.png


BIN
ssp-admin-vue3/src/sa-frame/img/up-icon.png


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1392 - 0
ssp-admin-vue3/src/sa-frame/kj/layer/layer.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2 - 0
ssp-admin-vue3/src/sa-frame/kj/layer/mobile/layer.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
ssp-admin-vue3/src/sa-frame/kj/layer/mobile/need/layer.css


+ 0 - 0
ssp-admin-vue3/src/sa-frame/kj/layer/theme/default/icon-ext.png


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio