Compare commits

..
198 Commits
Author SHA1 Message Date
Hooray 91c7350c34 chore: release v4.12.0 2024-10-18 16:45:04 +08:00
Hooray 924a42e270 chore: 更新依赖 2024-10-18 15:19:53 +08:00
Hooray 5fcfcb7770 fix: 修复vscode无法校验css文件 2024-10-16 10:29:50 +08:00
Hooray 65b2b0fcda chore: 移除演示代码 2024-10-08 15:19:48 +08:00
Hooray c34c2d1052 refactor!: 重构鉴权指令、鉴权组件 2024-10-08 15:19:48 +08:00
Hooray 159832443a perf: 优化导航菜单折叠 2024-10-08 15:19:48 +08:00
Hooray 0ae0f6f352 style: 导航菜单动画优化 2024-10-02 00:12:55 +08:00
Hooray 0f19e26650 refactor: 将 sass 替换为 sass-embedded 2024-09-25 17:02:15 +08:00
Hooray efd8536469 fix: 修复顶部相关布局时,移动端下工具栏不显示 2024-09-23 13:47:13 +08:00
Hooray 3ed947c8da chore: 更新 tsconfig 配置 2024-09-21 14:14:26 +08:00
Hooray ffd040a0ef chore: 更新依赖 2024-09-21 03:55:45 +08:00
Hooray 026dfdbaef fix: 修复顶部相关布局时,工具栏重复渲染 2024-09-20 23:22:05 +08:00
Hooray Hu 4c22d91170 refactor: 使用 vite-plugin-archiver 插件代替框架存档功能 2024-09-19 09:17:28 +08:00
Hooray Hu 6327abb078 chore: 更新依赖 2024-09-15 11:49:39 +08:00
Hooray Hu 484f35d27a refactor: 使用 vite-plugin-vue-app-loading 插件代替框架首屏loading 2024-09-13 02:08:51 +08:00
Hooray Hu a5b3c5660d refactor: 修改全局loading消失时机 2024-09-09 10:19:38 +08:00
Hooray Hu 76c66f9bc9 style: 优化登录相关组件 2024-09-06 01:45:11 +08:00
Hooray Hu b3cfb95f59 refactor: 优化全局loading 2024-09-03 10:25:22 +08:00
Hooray Hu dac7f4b0c1 chore: release v4.11.0 2024-09-01 20:42:20 +08:00
Hooray Hu d83b21715b chore: 更新依赖 2024-09-01 19:11:02 +08:00
Hooray Hu a57c62765b revert: 移除 unocss 自定义 shortcuts 2024-08-28 14:01:47 +08:00
Hooray Hu 2b38152485 refactor: 路由导航守卫移入到单独文件维护 2024-08-27 10:56:00 +08:00
Hooray Hu 57e982ed61 refactor: 布局相关css变量移入到布局组件中维护 2024-08-27 10:35:09 +08:00
Hooray Hu 2fe0fc95ee feat: 引入 postcss-nested 2024-08-26 23:16:32 +08:00
Hooray Hu fab5d0539b chore: 更换域名 2024-08-19 16:02:09 +08:00
Hooray Hu e79bd1dc50 feat: SvgIcon 组件支持传入本地图片,网络图片增加载入和失败状态 2024-08-14 17:49:03 +08:00
Hooray Hu 1e155369b2 fix: 修复带参路由无法记录到标签栏中 2024-08-08 16:15:49 +08:00
Hooray Hu cb230f77bd refactor!: 重构登录页面 2024-08-06 02:18:25 +08:00
Hooray Hu 6f59ddf6b8 feat: 扩充 unocss 自定义 shortcuts 2024-08-05 14:43:03 +08:00
Hooray Hu 7668bd5063 chore: release v4.10.0 2024-08-03 22:16:45 +08:00
Hooray Hu 5e3fa9a567 chore: 更新依赖 2024-08-03 22:01:11 +08:00
Hooray Hu 571190cd78 refactor: 手动登出时不带上重定向地址 2024-07-31 15:39:18 +08:00
Hooray Hu bd5c921bca feat: FileUpload 组件文件列表增加下载功能 2024-07-31 13:53:01 +08:00
Max Sky b236df4e30 微软 Microsoft 拼写错误 (#41) 2024-07-30 17:39:06 +08:00
Hooray Hu 657bec97f6 refactor: 导航菜单尺寸调整 2024-07-21 18:19:46 +08:00
Hooray Hu 5d0141011b chore: 更新依赖 2024-07-18 11:28:31 +08:00
Hooray Hu b8e53fb259 chore: eslint 规则调整 2024-07-11 19:56:58 +08:00
Hooray Hu 66c0e4f8c7 feat: 上传组件增加自定义上传 2024-07-09 18:02:02 +08:00
Hooray Hu 7457fcc23e refactor: vite插件合并到单个文件中维护 2024-07-08 16:40:54 +08:00
Hooray Hu 9a7331f32f refactor!: 移除createInjectionKey函数 2024-07-05 17:42:40 +08:00
Hooray Hu 87819b4b5d chore: 移除无用代码 2024-07-05 15:14:39 +08:00
Hooray Hu d00fc26616 refactor: 环境变量 VITE_APP_DISABLE_DEVTOOL 改成用 vite 插件的方式注入代码中 2024-07-03 23:25:36 +08:00
Hooray Hu 647edd7207 refactor: 环境变量 VITE_APP_DEBUG_TOOL 改成用 vite 插件的方式注入代码中 2024-07-03 23:01:46 +08:00
Hooray Hu 23d36432da feat: 次导航支持隐藏 2024-07-02 00:34:36 +08:00
Hooray Hu 2fdc4f18ca chore: release v4.9.0 2024-06-29 12:51:03 +08:00
Hooray Hu 961df08e2b chore: 更新依赖 2024-06-29 12:50:50 +08:00
Hooray Hu 1d62ac7558 style: HDropdownMenu 组件样式调整 2024-06-28 14:21:26 +08:00
Hooray Hu 456ef83081 feat: 新增哀悼模式和色弱模式 2024-06-28 10:06:56 +08:00
Hooray Hu 90b588028a feat: 增加禁用开发者工具环境变量 2024-06-26 10:45:45 +08:00
Hooray Hu 9e444c83f8 fix: 修复主导航 children 为空时报错 2024-06-25 00:44:07 +08:00
Hooray Hu 57fcd2785c refactor!: 配置项 menu.menuMode 变更为 menu.mode 2024-06-24 17:16:27 +08:00
Hooray Hu 3c8b4881b4 fix: 修复vscode失效配置 2024-06-24 15:55:31 +08:00
Hooray Hu e32d0561c7 chore: 更新readme 2024-06-21 09:58:18 +08:00
Hooray Hu d303776825 chore: 更新依赖 2024-06-15 22:53:45 +08:00
Hooray Hu 3ef0f69bcc chore: 样式微调 2024-06-12 14:10:48 +08:00
Hooray Hu b00477d5fa chore: release v4.8.2 2024-06-07 10:48:33 +08:00
Hooray Hu fe00fa1e6f chore: 更新依赖 2024-06-07 10:32:36 +08:00
Hooray Hu f0679a1d59 chore: 更新readme 2024-06-04 16:12:39 +08:00
Hooray Hu 4c4f811f39 chore: 修改 unocss 写法 2024-05-29 14:29:55 +08:00
Hooray Hu 03e5e99896 perf: 优化标签栏滚动体验 2024-05-28 16:50:02 +08:00
Hooray Hu fda48176ee chore: 更新依赖,修改 unocss 写法 2024-05-22 15:42:16 +08:00
Hooray Hu b639b487db chore: release v4.8.1 2024-05-20 10:04:30 +08:00
Hooray Hu dbd93ffb7f chore: 更新依赖 2024-05-20 10:00:02 +08:00
Hooray Hu daf9b1b2e2 chore: vite base 使用默认设置 2024-05-16 10:50:35 +08:00
Hooray Hu e023671bac fix: 修复后端返回路由数据中 children 为空数组导致菜单无法正常生成 2024-05-14 10:14:30 +08:00
Hooray Hu e6265a7c64 chore: 增加node版本控制文件 2024-05-09 09:51:38 +08:00
Hooray Hu 8c629050ba chore: 更换链接 2024-05-07 09:52:18 +08:00
Hooray Hu c7d2a857f7 chore: release v4.8.0 2024-05-01 12:44:53 +08:00
Hooray Hu b19344a1c1 chore: 更新依赖 2024-05-01 12:35:15 +08:00
Hooray Hu b632b4d3a8 refactor: 调整标签页操作按钮点击区域 2024-04-29 17:51:56 +08:00
Hooray Hu 98d21845ce feat: 登录页增加扫码登录参考 2024-04-26 11:27:16 +08:00
Hooray Hu bbeeac41ce chore: 移除无用代码 2024-04-24 17:26:10 +08:00
Hooray Hu 449495f03d fix: 修复类型定义 2024-04-23 10:27:33 +08:00
Hooray Hu 14583bbaa5 fix: 标签栏解绑错误快捷键 2024-04-15 09:52:33 +08:00
Hooray Hu 045ab02bb2 chore: 主页微调 2024-04-11 00:49:53 +08:00
Hooray Hu b883133dc6 refactor: 明暗图标替换 2024-04-09 17:20:49 +08:00
Hooray Hu 5269dba56b fix: 标签栏右键菜单自定义样式没在指定 class 范围内生效 2024-04-09 16:55:09 +08:00
Hooray Hu 4911affd28 refactor!: 路由 meta.auth 保持元信息默认合并行为,路由访问改为通过 route.matched 进行逐级鉴权 2024-04-09 15:14:00 +08:00
Hooray Hu cb53f4c84e fix: 修复一级路由设置 meta.menu: false 无效 2024-04-05 14:43:44 +08:00
Hooray Hu 3164e33f1d chore: release v4.7.0 2024-03-31 22:54:24 +08:00
Hooray Hu 06eee21032 chore: 更新依赖 2024-03-31 22:53:36 +08:00
Hooray Hu 79184f4eeb fix: 文件路由系统下,用户权限变化后,菜单不更新 2024-03-28 21:50:48 +08:00
Hooray Hu 8416b61f6b refactor: 移除routeStore.routes 2024-03-28 16:38:22 +08:00
Hooray Hu a90a19c62e refactor: menuStore.menus 转为内部使用,外部统一使用 menuStore.allMenus 2024-03-28 13:48:39 +08:00
Hooray Hu ee819445d2 chore: 页面宽度判断移动端设备从992调整为1024 2024-03-28 09:22:42 +08:00
Hooray Hu 679ad4c9ee ci: 修改 github actions 自动同步时间 2024-03-25 09:29:53 +08:00
Hooray Hu 805c13ba7a chore: 更新依赖 2024-03-21 14:47:28 +08:00
Hooray Hu c61675b311 fix: 修复后端返回路由时,因为父级auth为空字符串或数组导致子级权限失效的问题 2024-03-19 22:34:09 +08:00
Hooray Hu a105df66b1 fix: 修复顶部导航数量过多,导致工具栏被遮挡 2024-03-18 00:31:09 +08:00
Hooray Hu 107e820776 fix: 修复顶部导航鼠标滚动回弹 2024-03-18 00:27:07 +08:00
Hooray Hu 04fa39c295 feat: 路由增加 meta.menu 配置,同时 meta.sidebar 进入弃用过渡期 2024-03-17 00:13:14 +08:00
Hooray Hu 1685f8b7be chore: 更新依赖 2024-03-14 11:56:02 +08:00
Hooray Hu 73074101e1 fix: 修复 mock 模板请求无响应数据 2024-03-07 09:22:57 +08:00
Hooray Hu e802354cab refactor: 调整 onsize 事件写法 2024-03-05 09:28:21 +08:00
Hooray Hu 26274fdb8e ci: 修改 workflow 2024-03-04 09:32:17 +08:00
Hooray Hu f26b4cc50e chore: 移除弃用扩展 2024-03-04 09:08:44 +08:00
Hooray Hu fd1d57cebe chore: release v4.6.2 2024-03-01 09:02:50 +08:00
Hooray Hu 4ac539d082 build: 增加最低 node 版本限制 2024-02-29 14:15:21 +08:00
Hooray Hu fa81882f25 ci: 更新 workflows 2024-02-28 10:37:33 +08:00
Hooray Hu 606ce62cfd revert: 用户态改回用 localStorage 存储 2024-02-26 15:17:09 +08:00
Hooray Hu 7479dfe818 chore: release v4.6.1 2024-02-26 13:58:31 +08:00
Hooray Hu a8e964acd4 chore: 更新依赖 2024-02-26 13:58:09 +08:00
Hooray Hu aab429b997 chore: release v4.6.0 2024-02-25 16:38:54 +08:00
Hooray Hu 64fd2eb59c chore: 更新依赖 2024-02-25 16:38:40 +08:00
Hooray Hu c9d25a0e8d refactor: 修改登录相关数据 2024-02-12 23:59:20 +08:00
Hooray Hu fc44d941d0 chore: 更新依赖 2024-02-11 21:06:45 +08:00
Hooray Hu 362562112a chore: release v4.5.0 2024-02-04 14:44:02 +08:00
Hooray Hu 5e5100de11 chore: 更新依赖 2024-02-04 14:43:49 +08:00
Hooray Hu b60236035a fix: 修复标签栏未开启时调用 useTabbar 内函数报错 2024-02-01 11:32:15 +08:00
Hooray Hu a4b33eea73 readme 中链接修改 2024-02-01 01:21:52 +08:00
Hooray Hu 46138b8da9 chore: 样式调整 2024-01-31 17:00:56 +08:00
Hooray Hu b4e0e9740d feat: vite-plugin-vue-devtools 替换 vite-plugin-vue-inspector 2024-01-30 17:09:08 +08:00
Hooray Hu af1324c2cf refactor: 代码微调 2024-01-30 01:17:09 +08:00
Hooray Hu a8b41c7237 feat: 明暗模式设置为跟随系统时,支持动态切换 2024-01-27 10:40:02 +08:00
Hooray Hu 5704453835 fix: 修复顶部模式下页面载入进度条被遮挡 2024-01-26 15:45:26 +08:00
Hooray Hu f9c1c2df42 feat: 导航菜单收起时,无二级的一级导航增加 tooltip 展示 2024-01-26 00:59:10 +08:00
Hooray Hu 9812058805 chore: 更新依赖 2024-01-25 09:42:23 +08:00
Hooray Hu a55f3016ce feat: 增加标签页支持 2024-01-25 09:32:00 +08:00
Hooray Hu 52b25d1eae feat: 增加图片支持,整站图标改用 unocss icon 方式使用,方便在内网环境使用 2024-01-18 00:31:18 +08:00
Hooray Hu 42da9626b7 chore: 移除百度统计相关代码 2024-01-16 09:55:54 +08:00
Hooray Hu 06d6e18dac feat: 增加打包后生成存档文件支持 2024-01-14 00:11:16 +08:00
Hooray Hu 55dcff3b22 refactor: vite-plugin-compression2 替换 vite-plugin-compression 2024-01-14 00:08:53 +08:00
Hooray Hu 8d5c14ea16 chore: 增加运行时信息 2024-01-14 00:06:21 +08:00
Hooray Hu 36c9a73375 feat: 引入 unplugin-turbo-console 优化开发体验 2024-01-12 10:44:53 +08:00
Hooray Hu f6477e03db chore: 更新依赖 2024-01-11 10:51:40 +08:00
Hooray Hu 0dfdb0ff32 fix: 修复导航模式切换到 single 时有几率报错 2024-01-08 11:00:37 +08:00
Hooray Hu 2f1dc07eff refactor: 用 defineModel 重构部分组件 2024-01-07 00:23:36 +08:00
Hooray Hu af49dc8113 chore: 更新依赖 2024-01-06 21:25:15 +08:00
Hooray Hu 67e16872e4 refactor: 优化 HDropdown / HDropdownMenu / HTooltip 组件触发隐藏逻辑 2024-01-06 11:24:54 +08:00
Hooray Hu 1dd1677969 fix: mock 数据修复 2024-01-05 00:49:11 +08:00
Hooray Hu 6c1fca69dc chore: release v4.4.0 2024-01-04 23:38:03 +08:00
Hooray Hu f3993b8563 chore: 移除无用样式 2024-01-04 22:59:51 +08:00
Hooray Hu d39f5c3af6 fix: 修复工具栏显示/隐藏逻辑 2024-01-04 22:58:36 +08:00
Hooray Hu 86df38a142 refactor: 样式微调 2024-01-04 11:21:59 +08:00
Hooray Hu 548eac59f2 refactor!: vite-plugin-fake-server 替换 vite-plugin-mock 2024-01-04 01:15:18 +08:00
Hooray Hu 8dee916774 fix: 修复次导航高度变化时,底部阴影不变化 2024-01-02 02:05:56 +08:00
Hooray Hu 59ffda20e7 refactor: 次导航展开收起按钮位置移动到次导航底部 2024-01-02 01:15:33 +08:00
Hooray Hu d908f3ce26 fix: 修复 FixedActionBar 组件阴影样式未生效 2024-01-02 00:28:43 +08:00
Hooray Hu cc9226f19e chore: 更新依赖 2024-01-01 01:16:45 +08:00
Hooray Hu 30a303195f refactor: 工具栏文件目录结构调整 2023-12-30 15:42:37 +08:00
Hooray Hu 75d815d315 refactor!: 应用配置 toolbar 对象内属性调整
BREAKING CHANGE: `toolbar.enableFullscreen` 变更为 `toolbar.fullscreen`
`toolbar.enablePageReload` 变更为 `toolbar.pageReload`
`toolbar.enableColorScheme` 变更为 `toolbar.colorScheme`
`breadcrumb.enable` 变更为 `toolbar.breadcrumb`
`navSearch.enable` 变更为 `toolbar.navSearch`
2023-12-28 21:56:45 +08:00
Hooray Hu 2b8a198aaf fix: 修复路由 HTML5 模式下点击导航刷新页面的问题 2023-12-26 15:52:10 +08:00
Hooray Hu e015f29e9e chore: 更新依赖 2023-12-24 00:21:14 +08:00
Hooray Hu 795d428ed8 chore: 更新依赖,更新stylelint配置 2023-12-22 16:45:00 +08:00
Hooray Hu 8adcb2654b chore: release v4.3.0 2023-12-21 00:06:12 +08:00
Hooray Hu fbef475459 chore: 更新依赖 2023-12-21 00:05:23 +08:00
Hooray Hu 8311afa712 refactor!: 应用配置 app.enableAppSetting 迁移到环境变量文件中 2023-12-18 00:15:14 +08:00
Hooray Hu dc17038185 refactor: 优化 UI 组件库的配置和使用方式
提取到独立文件中,方便进行替换
2023-12-17 02:54:02 +08:00
Hooray Hu cf63b0c0f6 chore: release v4.2.0 2023-12-16 00:58:50 +08:00
Hooray Hu b9615a49e7 chore: 更新依赖 2023-12-16 00:52:03 +08:00
Hooray Hu 28b8df7c77 feat: 应用配置增加 home.fullPath
框架中所有跳转到主页的操作均改成取该配置设置的路由地址
2023-12-14 21:42:51 +08:00
Hooray Hu ffc402d27f refactor: 优化应用配置复制功能
提取出与框架默认配置不同的差异项进行复制,而非完全复制
2023-12-12 01:03:23 +08:00
Hooray Hu ccc8e51ebd refactor: 导航栏设置激活状态逻辑优化 2023-12-10 03:31:50 +08:00
Hooray Hu 6e1aacab06 refactor: 最子级菜单项用 <a> 渲染 2023-12-08 15:00:09 +08:00
Hooray Hu 55edebae61 chore: 更新 vscode 配置 2023-12-08 09:40:08 +08:00
Hooray Hu 0fffdebb89 chore: 主页样式微调 2023-12-07 14:15:20 +08:00
Hooray Hu aab8f77d17 chore: release v4.1.0 2023-12-07 14:13:56 +08:00
Hooray Hu e5b32aed8b chore: 更新依赖 2023-12-07 14:13:09 +08:00
Hooray Hu 031e6f9c11 chore: 更新依赖 2023-12-04 00:40:16 +08:00
Hooray Hu 671520456f chore: 更新依赖 2023-11-29 14:49:31 +08:00
Hooray Hu 2e98278f74 fix: 修复次导航的显隐逻辑 2023-11-29 14:07:04 +08:00
Hooray Hu b5b5a8e873 更新依赖,eslint 增加 unocss 配置 2023-11-28 16:02:42 +08:00
Hooray Hu b96e9dc4c9 chore: 调整 release actions 2023-11-25 23:35:49 +08:00
Hooray Hu 61eded401e chore: 更新依赖 2023-11-25 23:20:24 +08:00
Hooray Hu 0be837ce7e chore: eslint 规则替换 2023-11-21 17:06:26 +08:00
Hooray Hu e57331905f refactor: 登录页logo图替换为img标签 2023-11-21 15:50:40 +08:00
Hooray Hu 98e85220d1 refactor: 导航title类型补充 2023-11-21 14:27:02 +08:00
Hooray Hu 7ba47879f7 fix: 修复次导航第一个导航无子级路由时不显示 2023-11-21 11:37:43 +08:00
Hooray Hu 45567dddbc refactor: 调整导航顶部模式下展示样式 2023-11-20 15:10:55 +08:00
Hooray Hu 0aa24229dd chore: release v4.0.3 2023-11-20 09:48:06 +08:00
Hooray Hu 3e313f4d66 chore: 更新依赖 2023-11-20 09:47:51 +08:00
Hooray Hu fc5cd48125 chore: 增加 github stars 曲线图 2023-11-19 02:27:49 +08:00
Hooray Hu 8aba058479 fix: 搜索界面弹出时动画失效 2023-11-17 00:28:34 +08:00
Hooray Hu 4df198c4b3 fix: 修复文件系统路由下,导航栏无法展开多级和一级导航无法点击 2023-11-15 00:14:56 +08:00
Hooray Hu 04ea92c714 chore: 系统信息里增加版本号显示 2023-11-14 18:32:47 +08:00
Hooray Hu 7cef77df24 chore: release v4.0.2 2023-11-10 10:48:52 +08:00
Hooray Hu 6abc4b696c chore: 更新依赖 2023-11-10 10:48:37 +08:00
Hooray Hu d9c14872ab chore: 更新依赖 2023-11-05 12:12:38 +08:00
Hooray Hu 8a37439237 refactor: 调整 HDialog / HSlideover 组件 overlay 属性默认为 false 2023-11-03 22:46:51 +08:00
Hooray Hu eac185559a refactor: 移除 Copyright 组件无效样式 2023-11-03 22:41:33 +08:00
Hooray Hu be8041378b chore: release v4.0.1 2023-11-01 10:20:29 +08:00
Hooray Hu 2e4af7c2f5 fix: 修复 plop 无法使用 2023-10-31 20:57:30 +08:00
Hooray Hu 3c3fbd1ece chore: 更新commitlintrc配置 2023-10-31 16:22:38 +08:00
Hooray Hu 197d302d1b fix: 修复导航跳转时如果被中断,高亮位置不正确
closed #29
2023-10-31 16:19:37 +08:00
Hooray Hu ff2dd31096 fix: 修复 HTabList 组件文字不居中的问题 2023-10-31 10:02:26 +08:00
Hooray Hu 0283bfc5dd chore: 更新依赖 2023-10-30 14:52:07 +08:00
Hooray Hu 95748fe4a9 fix: 修复火狐下样式兼容性问题 2023-10-30 14:47:25 +08:00
Hooray Hu 8e1abdb5c2 chore: release v4.0.0 2023-10-29 23:18:29 +08:00
Hooray Hu 00c5a1727b feat: v4.0 2023-10-29 23:17:31 +08:00
Hooray Hu 05b7618536 chore: release v3.2.4 2023-10-25 23:07:08 +08:00
Hooray Hu 9a30f99797 chore: 更新依赖 2023-10-25 23:06:57 +08:00
Hooray Hu 96dae4047b chore: 更新依赖 2023-10-17 09:37:58 +08:00
Hooray Hu 33294c516e fix: 修复搜索结果列表图标展示过大 2023-10-11 17:20:46 +08:00
Hooray Hu 28338e45cb chore: release v3.2.3 2023-10-10 09:10:44 +08:00
Hooray Hu b3ca13fe4e chore: 更新依赖 2023-10-10 09:10:28 +08:00
Hooray Hu 39ade149be fix: 修复 403/404 页面图标尺寸 2023-09-26 15:13:46 +08:00
Hooray Hu 3372a85d5e 修复后端返回路由 component 字段为空导致link导航失效 2023-09-15 00:21:54 +08:00
Hooray Hu 7266776081 build: 开发环境默认关闭本地代理 2023-09-09 09:34:30 +08:00
203 changed files with 16744 additions and 11932 deletions
+7 -8
View File
@@ -1,6 +1,5 @@
// .commitlintrc.js
/** @type {import('cz-git').UserConfig} */
module.exports = {
export default {
rules: {
// @see: https://commitlint.js.org/#/reference-rules
},
@@ -16,7 +15,7 @@ module.exports = {
footerPrefixsSelect: '选择关联issue前缀(可选):',
customFooterPrefixs: '输入自定义issue前缀 :',
footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
confirmCommit: '是否提交或修改commit ?'
confirmCommit: '是否提交或修改commit ?',
},
types: [
{ value: 'feat', name: 'feat: ✨ 新增功能 | A new feature', emoji: ':sparkles:' },
@@ -49,7 +48,7 @@ module.exports = {
issuePrefixs: [
// 如果使用 gitee 作为开发管理
{ value: 'link', name: 'link: 链接 ISSUES 进行中' },
{ value: 'closed', name: 'closed: 标记 ISSUES 已完成' }
{ value: 'closed', name: 'closed: 标记 ISSUES 已完成' },
],
customIssuePrefixsAlign: 'top',
emptyIssuePrefixsAlias: 'skip',
@@ -57,13 +56,13 @@ module.exports = {
allowCustomIssuePrefixs: true,
allowEmptyIssuePrefixs: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
maxHeaderLength: Number.POSITIVE_INFINITY,
maxSubjectLength: Number.POSITIVE_INFINITY,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: '',
defaultIssues: '',
defaultScope: '',
defaultSubject: ''
}
defaultSubject: '',
},
}
Regular → Executable
View File
Regular → Executable
+7 -1
View File
@@ -1,9 +1,15 @@
# 应用配置面板
VITE_APP_SETTING = true
# 页面标题
VITE_APP_TITLE = Fantastic-admin 基础版
# 接口请求地址,会设置到 axios 的 baseURL 参数上
VITE_APP_API_BASEURL = /
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
VITE_APP_DEBUG_TOOL =
# 是否禁用开发者工具,可防止被调试
VITE_APP_DISABLE_DEVTOOL = false
# 是否开启代理
VITE_OPEN_PROXY = true
VITE_OPEN_PROXY = false
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
+6
View File
@@ -1,9 +1,13 @@
# 应用配置面板
VITE_APP_SETTING = false
# 页面标题
VITE_APP_TITLE = 页面标题
# 接口请求地址,会设置到 axios 的 baseURL 参数上
VITE_APP_API_BASEURL = /
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
VITE_APP_DEBUG_TOOL =
# 是否禁用开发者工具,可防止被调试
VITE_APP_DISABLE_DEVTOOL = false
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = false
@@ -11,3 +15,5 @@ VITE_BUILD_MOCK = false
VITE_BUILD_SOURCEMAP = false
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip,brotli
# 是否在打包后生成存档,支持 zip 和 tar
VITE_BUILD_ARCHIVE =
+6
View File
@@ -1,9 +1,13 @@
# 应用配置面板
VITE_APP_SETTING = false
# 页面标题
VITE_APP_TITLE = 页面标题(test)
# 接口请求地址,会设置到 axios 的 baseURL 参数上
VITE_APP_API_BASEURL = /
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
VITE_APP_DEBUG_TOOL =
# 是否禁用开发者工具,可防止被调试
VITE_APP_DISABLE_DEVTOOL = false
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = true
@@ -11,3 +15,5 @@ VITE_BUILD_MOCK = true
VITE_BUILD_SOURCEMAP = true
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS =
# 是否在打包后生成存档,支持 zip 和 tar
VITE_BUILD_ARCHIVE =
-2
View File
@@ -1,2 +0,0 @@
dist
node_modules
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "@antfu",
"rules": {
"eslint-comments/no-unlimited-disable": "off",
"curly": ["error", "all"],
"vue/component-tags-order": ["error", {
"order": ["route", "script", "template", "style"]
}]
}
}
+8 -8
View File
@@ -9,13 +9,13 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 16.x
node-version: 20
- run: npx changelogithub # or changelogithub@0.12 if ensure the stable result
env:
@@ -25,7 +25,7 @@ jobs:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -40,7 +40,7 @@ jobs:
- name: Create Archive
uses: thedoctor0/zip-release@main
with:
type: 'zip'
type: zip
filename: fantastic-admin.${{ steps.last_release.outputs.tag_name }}.zip
exclusions: '/.git/* /.github/*'
@@ -57,7 +57,7 @@ jobs:
needs: release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: example
@@ -73,9 +73,9 @@ jobs:
- name: Create Archive
uses: thedoctor0/zip-release@main
with:
type: 'zip'
type: zip
filename: fantastic-admin.example.${{ steps.last_release.outputs.tag_name }}.zip
exclusions: '/.git/* /.github/*'
exclusions: '/.git/* /.github/* /public/.github/* /public/.nojekyll'
- name: Upload Archive To Release
uses: xresloader/upload-to-github-release@v1
+2 -2
View File
@@ -1,9 +1,9 @@
name: repo-sync
on:
# 每天自动同步
# 每天 00:05 自动同步
schedule:
- cron: '0 0 * * *'
- cron: '5 0 * * *'
# 手动触发部署
workflow_dispatch:
Regular → Executable
+1 -3
View File
@@ -3,8 +3,6 @@ node_modules
dist*
dist-ssr
*.local
*.tsbuildinfo
.eslintcache
.stylelintcache
src/assets/sprites/*.*
!src/assets/sprites/.gitkeep
public/icons
Regular → Executable
+2 -2
View File
@@ -1,4 +1,4 @@
{
"*.{ts,tsx,vue}": "eslint --cache --fix --no-ignore --no-error-on-unmatched-pattern --ext .ts,.tsx,.vue src/",
"*.{css,scss,vue}": "stylelint src/**/*.{css,scss,vue} --cache --fix --allow-empty-input"
"*.{ts,tsx,vue}": "eslint --cache --fix",
"*.{css,scss,vue}": "stylelint --cache --fix"
}
+1
View File
@@ -0,0 +1 @@
20
Regular → Executable
+1
View File
@@ -1,2 +1,3 @@
shamefully-hoist=true
strict-peer-dependencies=false
engine-strict=true
-3
View File
@@ -1,3 +0,0 @@
dist
node_modules
src/assets/sprites
-26
View File
@@ -1,26 +0,0 @@
{
"extends": [
"stylelint-stylistic/config",
"stylelint-config-standard-scss",
"stylelint-config-standard-vue/scss"
],
"plugins": [
"stylelint-scss"
],
"rules": {
"stylistic/max-line-length": null,
"stylistic/block-closing-brace-newline-after": [
"always",
{
"ignoreAtRules": ["if", "else"]
}
],
"at-rule-no-unknown": null,
"no-descending-specificity": null,
"property-no-unknown": null,
"font-family-no-missing-generic-family-keyword": null,
"selector-class-pattern": null,
"scss/double-slash-comment-empty-line-before": null,
"scss/no-global-function-names": null
}
}
Vendored Regular → Executable
+1 -1
View File
@@ -5,6 +5,6 @@
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"antfu.unocss"
]
}
Vendored Regular → Executable
+24 -3
View File
@@ -1,7 +1,28 @@
{
"eslint.useFlatConfig": true,
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"stylelint.validate": ["css", "scss", "vue"]
"stylelint.validate": [
"css",
"postcss",
"scss",
"vue"
],
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020 fantastic-template
Copyright (c) 2020 fantastic-admin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+50 -30
View File
@@ -1,22 +1,15 @@
<p align="center">
<img src="https://fantastic-admin.gitee.io/logo.png" width="200" height="200" />
<img src="https://fantastic-admin.hurui.me/logo.png" width="200" height="200" />
</p>
<h1 align="center">Fantastic-admin</h1>
<p align="center">一款<b>开箱即用</b>的 Vue 中后台管理系统框架</p>
<p align="center">一款<b>开箱即用</b>的 Vue3 中后台管理系统框架</p>
<p align="center">
<a href="https://fantastic-admin.gitee.io" target="_blank">官网</a>
( <a href="https://fantastic-admin.github.io" target="_blank">备用</a> )
<a href="https://fantastic-admin.hurui.me" target="_blank">官网</a>
<span>&nbsp;|&nbsp;</span>
演示
( <a href="https://fantastic-admin.gitee.io/basic-example" target="_blank">基础版</a>
<a href="https://fantastic-admin.gitee.io/pro-example" target="_blank">专业版</a> )
<span>&nbsp;|&nbsp;</span>
<a href="https://fantastic-admin.gitee.io/support.html" target="_blank">技术支持</a>
<span>&nbsp;|&nbsp;</span>
<a href="https://fantastic-admin.gitee.io/buy.html" target="_blank">购买</a>
<a href="https://fantastic-admin.pages.dev" target="_blank">备用地址</a>
<p>
<p align="center">
@@ -26,9 +19,9 @@
## 特点
- 可自由替换 UI 组件库,默认使用 Element Plus
- 丰富的布局与主题,覆盖市面上各种中后台应用场景,兼容PC、平板和移动端
- 提供系统配置文件,轻松实现个性化定制
- 精心设计的动效,让每一处的动画都干净利落
- 根据路由配置自动生成导航栏
- 基于文件系统的路由
- 支持全方位权限验证
@@ -36,29 +29,31 @@
- 轻松实现国际化多语言适配
- 提供接近于浏览器原生标签栏操作体验的标签页功能
## 分支说明
## 下载
> 本仓库为基础版
**直接拉取源码可能会包含未发布的内容,推荐去 [Github Releases](https://github.com/fantastic-admin/basic/releases) 页面下载稳定版本的压缩包**。如果确定需要拉取源码,请参考下列分支说明:
- `main` Vue3 版本框架源码分支,不含示例代码,可直接用于实际开发
- `example` Vue3 版本演示源码分支,同线上演示站,包含大量示例,可用于参考学习
- `vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发(停止维护)
- `vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习(停止维护)
如果你更注重使用时的稳定性,推荐去 [Github Releases](https://github.com/fantastic-admin/basic/releases) 页面下载最新稳定版本的压缩包。
## 预览
> 预览截图为 Vue3 专业版
<table>
<tr>
<td><img src="https://fantastic-admin.gitee.io/preview1.png" /></td>
<td><img src="https://fantastic-admin.gitee.io/preview2.png" /></td>
<td><img src="https://fantastic-admin.gitee.io/preview3.png" /></td>
<td><img src="https://fantastic-admin.hurui.me/preview1.png" /></td>
<td><img src="https://fantastic-admin.hurui.me/preview2.png" /></td>
<td><img src="https://fantastic-admin.hurui.me/preview3.png" /></td>
</tr>
<tr>
<td><img src="https://fantastic-admin.gitee.io/preview4.png" /></td>
<td><img src="https://fantastic-admin.gitee.io/preview5.png" /></td>
<td><img src="https://fantastic-admin.gitee.io/preview6.png" /></td>
<td><img src="https://fantastic-admin.hurui.me/preview4.png" /></td>
<td><img src="https://fantastic-admin.hurui.me/preview5.png" /></td>
<td><img src="https://fantastic-admin.hurui.me/preview6.png" /></td>
</tr>
</table>
@@ -70,17 +65,23 @@
[![star](https://gitee.com/fantastic-admin/basic/badge/star.svg?theme=dark)](https://gitee.com/fantastic-admin/basic)
<details>
<summary>Github Stars 曲线</summary>
[![Stargazers over time](https://starchart.cc/fantastic-admin/basic.svg)](https://starchart.cc/fantastic-admin/basic)
</details>
## 生态
<table>
<tr>
<th colspan="3" align="center">
<a href="https://hooray.gitee.io/fantastic-startkit/" target="_blank">Fantastic-startkit</a>
<a href="https://hooray.github.io/fantastic-startkit/" target="_blank">Fantastic-startkit</a>
</th>
</tr>
<tr>
<th colspan="3" align="center">
一款开箱即用的 Vue 项目启动套件
一款简单好用的 Vue3 项目启动套件
</th>
</tr>
</table>
@@ -88,7 +89,7 @@
<table>
<tr>
<th colspan="3" align="center">
<a href="https://one-step-admin.gitee.io" target="_blank">One-step-admin</a>
<a href="https://one-step-admin.hurui.me" target="_blank">One-step-admin</a>
</th>
</tr>
<tr>
@@ -97,13 +98,32 @@
</th>
</tr>
<tr>
<td><img src="https://one-step-admin.gitee.io/preview1.png" /></td>
<td><img src="https://one-step-admin.gitee.io/preview2.png" /></td>
<td><img src="https://one-step-admin.gitee.io/preview3.png" /></td>
<td><img src="https://one-step-admin.hurui.me/preview1.png" /></td>
<td><img src="https://one-step-admin.hurui.me/preview2.png" /></td>
<td><img src="https://one-step-admin.hurui.me/preview3.png" /></td>
</tr>
<tr>
<td><img src="https://one-step-admin.gitee.io/preview4.png" /></td>
<td><img src="https://one-step-admin.gitee.io/preview5.png" /></td>
<td><img src="https://one-step-admin.gitee.io/preview6.png" /></td>
<td><img src="https://one-step-admin.hurui.me/preview4.png" /></td>
<td><img src="https://one-step-admin.hurui.me/preview5.png" /></td>
<td><img src="https://one-step-admin.hurui.me/preview6.png" /></td>
</tr>
</table>
<table>
<tr>
<th colspan="4" align="center">
<a href="https://fantastic-mobile.hurui.me" target="_blank">Fantastic-mobile</a>
</th>
</tr>
<tr>
<th colspan="4" align="center">
一款自成一派的移动端 H5 框架
</th>
</tr>
<tr>
<td><img src="https://fantastic-mobile.hurui.me/preview1.png" /></td>
<td><img src="https://fantastic-mobile.hurui.me/preview2.png" /></td>
<td><img src="https://fantastic-mobile.hurui.me/preview3.png" /></td>
<td><img src="https://fantastic-mobile.hurui.me/preview4.png" /></td>
</tr>
</table>
+31
View File
@@ -0,0 +1,31 @@
import antfu from '@antfu/eslint-config'
export default antfu(
{
unocss: true,
ignores: [
'public',
'dist*',
],
},
{
rules: {
'eslint-comments/no-unlimited-disable': 'off',
'curly': ['error', 'all'],
'ts/no-unused-expressions': ['error', {
allowShortCircuit: true,
allowTernary: true,
}],
},
},
{
files: [
'src/**/*.vue',
],
rules: {
'vue/block-order': ['error', {
order: ['route', 'script', 'template', 'style'],
}],
},
},
)
Regular → Executable
+1 -11
View File
@@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/loading.css" />
<link rel="stylesheet" href="/browser_upgrade/index.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Expires" content="0">
@@ -14,21 +13,12 @@
</head>
<body>
<div id="app">
<div class="fantastic-admin-home">
<div class="loading">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
<div class="text">载入中</div>
</div>
<div id="browser-upgrade">
<div class="title">为了您的体验,推荐使用以下浏览器</div>
<div class="browsers">
<a href="https://www.microsoft.com/edge" target="_blank" class="browser">
<img class="browser-icon" src="/browser_upgrade/edge.png" />
<div class="browser-name">Mircosoft Edge</div>
<div class="browser-name">Microsoft Edge</div>
</a>
<a href="https://www.google.cn/chrome/" target="_blank" class="browser">
<img class="browser-icon" src="/browser_upgrade/chrome.png" />
+197
View File
@@ -0,0 +1,197 @@
<style>
@keyframes rainbow {
0% { --rainbow-color: #00a98e; }
1.25% { --rainbow-color: #00a996; }
2.5% { --rainbow-color: #00a99f; }
3.75% { --rainbow-color: #00a9a7; }
5% { --rainbow-color: #00a9b0; }
6.25% { --rainbow-color: #00a9b8; }
7.5% { --rainbow-color: #00a9c0; }
8.75% { --rainbow-color: #00a8c7; }
10% { --rainbow-color: #00a8cf; }
11.25% { --rainbow-color: #00a7d5; }
12.5% { --rainbow-color: #00a6dc; }
13.75% { --rainbow-color: #00a6e2; }
15% { --rainbow-color: #00a4e7; }
16.25% { --rainbow-color: #00a3ec; }
17.5% { --rainbow-color: #00a2f1; }
18.75% { --rainbow-color: #00a0f4; }
20% { --rainbow-color: #009ff7; }
21.25% { --rainbow-color: #009dfa; }
22.5% { --rainbow-color: #009bfc; }
23.75% { --rainbow-color: #0098fd; }
25% { --rainbow-color: #0096fd; }
26.25% { --rainbow-color: #0093fd; }
27.5% { --rainbow-color: #2e90fc; }
28.75% { --rainbow-color: #4d8dfa; }
30% { --rainbow-color: #638af8; }
31.25% { --rainbow-color: #7587f5; }
32.5% { --rainbow-color: #8583f1; }
33.75% { --rainbow-color: #9280ed; }
35% { --rainbow-color: #9f7ce9; }
36.25% { --rainbow-color: #aa78e3; }
37.5% { --rainbow-color: #b574dd; }
38.75% { --rainbow-color: #be71d7; }
40% { --rainbow-color: #c76dd1; }
41.25% { --rainbow-color: #cf69c9; }
42.5% { --rainbow-color: #d566c2; }
43.75% { --rainbow-color: #dc63ba; }
45% { --rainbow-color: #e160b3; }
46.25% { --rainbow-color: #e65eab; }
47.5% { --rainbow-color: #e95ca2; }
48.75% { --rainbow-color: #ed5a9a; }
50% { --rainbow-color: #ef5992; }
51.25% { --rainbow-color: #f15989; }
52.5% { --rainbow-color: #f25981; }
53.75% { --rainbow-color: #f25a79; }
55% { --rainbow-color: #f25c71; }
56.25% { --rainbow-color: #f15e69; }
57.5% { --rainbow-color: #ef6061; }
58.75% { --rainbow-color: #ed635a; }
60% { --rainbow-color: #eb6552; }
61.25% { --rainbow-color: #e8694b; }
62.5% { --rainbow-color: #e46c44; }
63.75% { --rainbow-color: #e06f3d; }
65% { --rainbow-color: #db7336; }
66.25% { --rainbow-color: #d77630; }
67.5% { --rainbow-color: #d17a2a; }
68.75% { --rainbow-color: #cc7d24; }
70% { --rainbow-color: #c6811e; }
71.25% { --rainbow-color: #bf8418; }
72.5% { --rainbow-color: #b98713; }
73.75% { --rainbow-color: #b28a0f; }
75% { --rainbow-color: #ab8d0c; }
76.25% { --rainbow-color: #a3900b; }
77.5% { --rainbow-color: #9c920d; }
78.75% { --rainbow-color: #949510; }
80% { --rainbow-color: #8b9715; }
81.25% { --rainbow-color: #83991b; }
82.5% { --rainbow-color: #7a9b21; }
83.75% { --rainbow-color: #719d27; }
85% { --rainbow-color: #679e2e; }
86.25% { --rainbow-color: #5da035; }
87.5% { --rainbow-color: #51a13c; }
88.75% { --rainbow-color: #44a244; }
90% { --rainbow-color: #34a44b; }
91.25% { --rainbow-color: #1ba553; }
92.5% { --rainbow-color: #00a65b; }
93.75% { --rainbow-color: #00a663; }
95% { --rainbow-color: #00a76c; }
96.25% { --rainbow-color: #00a874; }
97.5% { --rainbow-color: #00a87d; }
98.75% { --rainbow-color: #00a985; }
100% { --rainbow-color: #00a98e; }
}
:root {
--rainbow-color: #00a98e;
animation: rainbow 20s linear infinite;
}
.loading-container {
position: fixed;
top: 0;
left: 0;
z-index: 10000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--rainbow-color);
user-select: none;
background-color: snow;
}
.loading-container .loading {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.loading-container .loading .square {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
}
.loading-container .loading .square::before {
width: 10px;
height: 10px;
content: "";
border: 3px solid var(--rainbow-color);
border-radius: 15%;
animation: square-to-dot-animation 2s linear infinite;
}
.loading-container .loading .square:nth-child(1)::before {
animation-delay: calc(150ms * 1);
}
.loading-container .loading .square:nth-child(2)::before {
animation-delay: calc(150ms * 2);
}
.loading-container .loading .square:nth-child(3)::before {
animation-delay: calc(150ms * 3);
}
.loading-container .loading .square:nth-child(4)::before {
animation-delay: calc(150ms * 4);
}
@keyframes square-to-dot-animation {
15%,
25% {
width: 0;
height: 0;
margin: 5px;
border-width: 5px;
border-radius: 100%;
}
40% {
width: 10px;
height: 10px;
margin: initial;
border-width: 3px;
border-radius: 15%;
}
}
.loading-container .name {
position: relative;
margin-top: 20px;
font-size: 24px;
}
.loading-container .tips {
position: relative;
margin-top: 10px;
font-size: 16px;
opacity: 0.5;
}
.loading-container .tips::after {
position: absolute;
padding-left: 5px;
content: "…";
}
</style>
<div class="loading-container">
<div class="loading">
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
<div class="square"></div>
</div>
<div class="name">%VITE_APP_TITLE%</div>
<div class="tips">载入中</div>
</div>
Regular → Executable
+85 -59
View File
@@ -1,85 +1,111 @@
{
"version": "3.2.2",
"type": "module",
"version": "4.12.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"scripts": {
"dev": "vite",
"build:test": "vue-tsc --noEmit && vite build --mode test",
"build": "vue-tsc --noEmit && vite build",
"serve:test": "http-server ./dist-test -o",
"build": "vue-tsc -b && vite build",
"build:test": "vue-tsc -b && vite build --mode test",
"serve": "http-server ./dist -o",
"serve:test": "http-server ./dist-test -o",
"svgo": "svgo -f src/assets/icons",
"new": "plop",
"generate:icons": "esno ./scripts/generate.icons.ts",
"lint": "npm-run-all -s lint:tsc lint:eslint lint:stylelint",
"lint:tsc": "vue-tsc --noEmit",
"lint:eslint": "eslint --cache --fix --no-ignore --no-error-on-unmatched-pattern --ext .ts,.tsx,.vue src/",
"lint:stylelint": "stylelint src/**/*.{css,scss,vue} --cache --fix --allow-empty-input",
"postinstall": "simple-git-hooks && esno scripts/prepare.js",
"lint:tsc": "vue-tsc -b",
"lint:eslint": "eslint . --cache --fix",
"lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix",
"postinstall": "simple-git-hooks",
"preinstall": "npx only-allow pnpm",
"commit": "git cz",
"release": "bumpp"
},
"dependencies": {
"@vueuse/core": "^10.4.0",
"@vueuse/integrations": "^10.4.0",
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"element-plus": "^2.3.10",
"eruda": "^3.0.1",
"hotkeys-js": "^3.12.0",
"@headlessui/vue": "^1.7.23",
"@imengyu/vue3-context-menu": "^1.4.3",
"@vueuse/components": "^11.1.0",
"@vueuse/core": "^11.1.0",
"@vueuse/integrations": "^11.1.0",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"defu": "^6.1.4",
"disable-devtool": "^0.3.8",
"element-plus": "^2.8.5",
"eruda": "^3.4.0",
"floating-vue": "5.2.2",
"hotkeys-js": "^3.13.7",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-vue": "^0.5.9",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.1",
"pinia": "^2.1.6",
"qs": "^6.11.2",
"path-to-regexp": "^8.2.0",
"pinia": "^2.2.4",
"qs": "^6.13.0",
"scule": "^1.3.0",
"vconsole": "^3.15.1",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
"vue": "^3.5.12",
"vue-m-message": "^4.0.2",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@antfu/eslint-config": "^0.41.0",
"@iconify-json/ep": "^1.1.12",
"@iconify/vue": "^4.1.1",
"@types/nprogress": "^0.2.0",
"@types/path-browserify": "^1.0.0",
"@types/qs": "^6.9.7",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.3.3",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"autoprefixer": "^10.4.15",
"bumpp": "^9.2.0",
"cz-git": "^1.7.1",
"eslint": "^8.48.0",
"esno": "^0.17.0",
"fs-extra": "^11.1.1",
"@antfu/eslint-config": "3.8.0",
"@iconify/json": "^2.2.261",
"@iconify/vue": "^4.1.2",
"@stylistic/stylelint-config": "^2.0.0",
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "^1.0.10",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.16",
"@unocss/eslint-plugin": "^0.63.4",
"@vitejs/plugin-legacy": "^5.4.2",
"@vitejs/plugin-vue": "^5.1.4",
"@vitejs/plugin-vue-jsx": "^4.0.1",
"autoprefixer": "^10.4.20",
"boxen": "^8.0.1",
"bumpp": "^9.7.1",
"cz-git": "^1.10.1",
"eslint": "^9.12.0",
"esno": "^4.8.0",
"fs-extra": "^11.2.0",
"http-server": "^14.1.1",
"lint-staged": "^14.0.1",
"npm-run-all": "^4.1.5",
"plop": "^3.1.2",
"postcss-html": "^1.5.0",
"sass": "^1.66.1",
"simple-git-hooks": "^2.9.0",
"stylelint": "^15.10.3",
"stylelint-config-standard-scss": "^10.0.0",
"inquirer": "^12.0.0",
"lint-staged": "^15.2.10",
"npm-run-all2": "^6.2.4",
"picocolors": "^1.1.1",
"plop": "^4.0.1",
"postcss": "^8.4.47",
"postcss-nested": "^6.2.0",
"sass-embedded": "^1.80.2",
"simple-git-hooks": "^2.11.1",
"stylelint": "^16.10.0",
"stylelint-config-recess-order": "^5.1.1",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-scss": "^5.1.0",
"stylelint-stylistic": "^0.4.3",
"svgo": "^3.0.2",
"terser": "^5.19.2",
"typescript": "^5.2.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.9",
"vite-plugin-banner": "^0.7.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.8",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-spritesmith": "^0.1.1",
"stylelint-scss": "^6.8.1",
"svgo": "^3.3.2",
"typescript": "^5.6.3",
"unocss": "^0.63.4",
"unocss-preset-scrollbar": "^0.3.1",
"unplugin-auto-import": "^0.18.3",
"unplugin-turbo-console": "^1.10.4",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.9",
"vite-plugin-app-loading": "^0.3.0",
"vite-plugin-archiver": "^0.1.1",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-compression2": "^1.3.0",
"vite-plugin-fake-server": "^2.1.2",
"vite-plugin-pages": "^0.32.3",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-inspector": "^3.6.0",
"vite-plugin-vue-meta-layouts": "^0.2.2",
"vue-tsc": "^1.8.8"
"vite-plugin-vue-devtools": "^7.5.2",
"vite-plugin-vue-meta-layouts": "^0.4.3",
"vue-tsc": "^2.1.6"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
+2 -2
View File
@@ -12,6 +12,6 @@ defineOptions({
</div>
</template>
<style lang="scss" scoped>
// 样式
<style scoped>
/* 样式 */
</style>
+2 -2
View File
@@ -1,4 +1,4 @@
const fs = require('node:fs')
import fs from 'node:fs'
function getFolder(path) {
const components = []
@@ -13,7 +13,7 @@ function getFolder(path) {
return components
}
module.exports = {
export default {
description: '创建组件',
prompts: [
{
+84
View File
@@ -0,0 +1,84 @@
import Mock from 'mockjs'
const AllList: any[] = []
for (let i = 0; i < 50; i++) {
AllList.push(Mock.mock({
id: '@id',
title: '@ctitle(10, 20)',
}))
}
export default [
{
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/list',
method: 'get',
response: (option: any) => {
const { title, from, limit } = option.query
const list = AllList.filter((item) => {
return title ? item.title.includes(title) : true
})
const pageList = list.filter((item, index) => {
return index >= ~~from && index < (~~from + ~~limit)
})
return {
error: '',
status: 1,
data: {
list: pageList,
total: list.length,
},
}
},
},
{
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/detail',
method: 'get',
response: (option: any) => {
const info = AllList.filter(item => item.id === option.query.id)
return {
error: '',
status: 1,
data: info[0],
}
},
},
{
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/create',
method: 'post',
response: () => {
return {
error: '',
status: 1,
data: {
isSuccess: true,
},
}
},
},
{
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/edit',
method: 'post',
response: () => {
return {
error: '',
status: 1,
data: {
isSuccess: true,
},
}
},
},
{
url: '/mock/{{#if relativePath}}{{ relativePath }}/{{/if}}{{ moduleName }}/delete',
method: 'post',
response: () => {
return {
error: '',
status: 1,
data: {
isSuccess: true,
},
}
},
},
]
+43
View File
@@ -0,0 +1,43 @@
import fs from 'node:fs'
import path from 'node:path'
function getFolder(path) {
const components = []
const files = fs.readdirSync(path)
files.forEach((item) => {
const stat = fs.lstatSync(`${path}/${item}`)
if (stat.isDirectory() === true && item !== 'components') {
components.push(`${path}/${item}`)
components.push(...getFolder(`${path}/${item}`))
}
})
return components
}
export default {
description: '创建标准模块 Mock',
prompts: [
{
type: 'list',
name: 'path',
message: '请选择模块目录',
choices: getFolder('src/views'),
},
],
actions: (data) => {
const pathArr = path.relative('src/views', data.path).split('\\')
const moduleName = pathArr.pop()
const relativePath = pathArr.join('/')
const actions = []
actions.push({
type: 'add',
path: pathArr.length === 0 ? 'src/mock/{{moduleName}}.ts' : `src/mock/${pathArr.join('.')}.{{moduleName}}.ts`,
templateFile: 'plop-templates/mock/mock.hbs',
data: {
relativePath,
moduleName,
},
})
return actions
},
}
Regular → Executable
+2 -2
View File
@@ -17,6 +17,6 @@ defineOptions({
</div>
</template>
<style lang="scss" scoped>
// 样式
<style scoped>
/* 样式 */
</style>
Regular → Executable
+3 -3
View File
@@ -1,5 +1,5 @@
const path = require('node:path')
const fs = require('node:fs')
import fs from 'node:fs'
import path from 'node:path'
function getFolder(path) {
const components = []
@@ -14,7 +14,7 @@ function getFolder(path) {
return components
}
module.exports = {
export default {
description: '创建页面',
prompts: [
{
Regular → Executable
+6 -4
View File
@@ -1,10 +1,12 @@
const use{{ properCase name }}Store = defineStore(
// 唯一ID
'{{ camelCase name }}',
{
state: () => ({}),
getters: {},
actions: {},
() => {
const someThing = ref(0)
return {
someThing,
}
},
)
Regular → Executable
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = {
export default {
description: '创建全局状态',
prompts: [
{
Regular → Executable
+10 -5
View File
@@ -1,8 +1,13 @@
const fs = require('node:fs')
import { promises as fs } from 'node:fs'
module.exports = function (plop) {
export default async function (plop) {
plop.setWelcomeMessage('请选择需要创建的模式:')
fs.readdirSync('./plop-templates').forEach((item) => {
fs.lstatSync(`./plop-templates/${item}`).isDirectory() && plop.setGenerator(item, require(`./plop-templates/${item}/prompt`))
})
const items = await fs.readdir('./plop-templates')
for (const item of items) {
const stat = await fs.lstat(`./plop-templates/${item}`)
if (stat.isDirectory()) {
const prompt = await import(`./plop-templates/${item}/prompt.js`)
plop.setGenerator(item, prompt.default)
}
}
}
Generated Regular → Executable
+10598 -7369
View File
File diff suppressed because it is too large Load Diff
Regular → Executable
+3 -2
View File
@@ -1,5 +1,6 @@
module.exports = {
export default {
plugins: {
autoprefixer: {},
'autoprefixer': {},
'postcss-nested': {},
},
}
View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Regular → Executable
+8 -8
View File
@@ -1,20 +1,20 @@
#browser-upgrade {
position: absolute;
z-index: 10001;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 10001;
display: none;
user-select: none;
width: 100%;
height: 100%;
color: #736477;
user-select: none;
background-color: snow;
}
#browser-upgrade .title {
margin: 40px 0;
text-align: center;
font-size: 24px;
text-align: center;
}
#browser-upgrade .browsers {
@@ -24,8 +24,8 @@
#browser-upgrade .browsers .browser {
display: inline-block;
margin: 0 20px;
cursor: pointer;
text-decoration: none;
cursor: pointer;
}
#browser-upgrade .browsers .browser .browser-icon {
@@ -37,10 +37,10 @@
}
#browser-upgrade .browsers .browser .browser-name {
text-align: center;
margin-top: 10px;
padding-bottom: 2px;
margin-top: 10px;
color: #736477;
text-align: center;
border-bottom: 1px solid transparent;
}
Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

-92
View File
@@ -1,92 +0,0 @@
#app {
height: 100%;
}
.fantastic-admin-home {
position: absolute;
z-index: 10000;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
user-select: none;
color: #736477;
background-color: snow;
}
.fantastic-admin-home .loading {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.fantastic-admin-home .loading .square {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
}
.fantastic-admin-home .loading .square::before {
content: "";
width: 10px;
height: 10px;
border-radius: 15%;
border: 3px solid #8c858f;
animation: square-to-dot-animation 2s linear infinite;
}
.fantastic-admin-home .loading .square:nth-child(1)::before {
animation-delay: calc(150ms * 1);
}
.fantastic-admin-home .loading .square:nth-child(2)::before {
animation-delay: calc(150ms * 2);
}
.fantastic-admin-home .loading .square:nth-child(3)::before {
animation-delay: calc(150ms * 3);
}
.fantastic-admin-home .loading .square:nth-child(4)::before {
animation-delay: calc(150ms * 4);
}
@keyframes square-to-dot-animation {
15%,
25% {
border-radius: 100%;
width: 0;
height: 0;
margin: 5px;
border-width: 5px;
}
40% {
border-radius: 15%;
width: 10px;
height: 10px;
margin: initial;
border-width: 3px;
}
}
.fantastic-admin-home .text {
position: relative;
font-size: 24px;
margin-top: 20px;
}
.fantastic-admin-home .text::after {
content: "…";
position: absolute;
padding-left: 5px;
}
+83
View File
@@ -0,0 +1,83 @@
import { exec } from 'node:child_process'
import path from 'node:path'
import process from 'node:process'
import { lookupCollection, lookupCollections } from '@iconify/json'
import fs from 'fs-extra'
import inquirer from 'inquirer'
async function generateIcons() {
// 拿到全部图标集的原始数据
const raw = await lookupCollections()
let lastChoose = fs.readFileSync(path.resolve(process.cwd(), 'src/iconify/index.json'), 'utf-8')
lastChoose = JSON.parse(lastChoose)
// 取出可使用的图标集数据用于 inquirer 选择,并按名称排序
const collections = Object.entries(raw).map(([id, item]) => ({
...item,
id,
})).sort((a, b) => a.name.localeCompare(b.name))
/**
* 分别会在对应目录下生成以下文件,其中(1)(3)用于离线下载并安装图标,(2)用于图标选择器使用
* (1) src/iconify/index.json 记录用户 inquirer 的交互信息
* (2) src/iconify/data.json 包含多个图标集数据,仅记录图标名
* (3) public/icons/*-raw.json 多个图标集的原始数据,独立存放,用于离线使用
*/
inquirer.prompt([
{
type: 'checkbox',
message: '请选择需要生成的图标集',
name: 'collections',
choices: collections.map(item => ({
name: `${item.name} (${item.total} icons)`,
value: item.id,
})),
default: lastChoose.collections,
},
{
type: 'confirm',
name: 'isOfflineUse',
message: '是否需要离线使用',
default: false,
},
]).then(async (answers) => {
await fs.writeJSON(
path.resolve(process.cwd(), 'src/iconify/index.json'),
{
collections: answers.collections,
isOfflineUse: answers.isOfflineUse,
},
)
const outputDir = path.resolve(process.cwd(), 'public/icons')
await fs.ensureDir(outputDir)
await fs.emptyDir(outputDir)
const collectionsMeta: object[] = []
for (const info of answers.collections) {
const setData = await lookupCollection(info)
collectionsMeta.push({
prefix: setData.prefix,
info: setData.info,
icons: Object.keys(setData.icons),
})
const offlineFilePath = path.join(outputDir, `${info}-raw.json`)
if (answers.isOfflineUse) {
await fs.writeJSON(offlineFilePath, setData)
}
}
await fs.writeJSON(
path.resolve(process.cwd(), 'src/iconify/data.json'),
collectionsMeta,
)
exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix')
})
}
generateIcons()
-16
View File
@@ -1,16 +0,0 @@
import path from 'node:path'
import process from 'node:process'
import fs from 'fs-extra'
import { icons } from '@iconify-json/ep'
async function prepare() {
const outputDir = path.resolve(process.cwd(), 'public/icons')
await fs.ensureDir(outputDir)
await fs.emptyDir(outputDir)
await fs.writeJSON(
path.resolve(outputDir, 'icons.json'),
icons,
)
}
prepare()
-66
View File
@@ -1,66 +0,0 @@
{
// Default options
'functions': true,
'variableNameTransforms': ['dasherize']
}
{{#block "sprites"}}
{{#each sprites}}
${{../spritesheet_info.strings.name}}-sprite-{{strings.name}}: ({{px.x}}, {{px.y}}, {{px.offset_x}}, {{px.offset_y}}, {{px.width}}, {{px.height}}, {{px.total_width}}, {{px.total_height}}, '{{{escaped_image}}}', '{{name}}');
{{/each}}
${{spritesheet_info.strings.name}}-sprites: (
{{#each sprites}}
{{strings.name}}: ${{../spritesheet_info.strings.name}}-sprite-{{strings.name}},
{{/each}}
);
{{/block}}
{{#block "sprite-functions"}}
{{#if options.functions}}
@mixin {{spritesheet_info.strings.name}}-sprite-width($sprite) {
width: nth($sprite, 5);
}
@mixin {{spritesheet_info.strings.name}}-sprite-height($sprite) {
height: nth($sprite, 6);
}
@mixin {{spritesheet_info.strings.name}}-sprite-position($sprite) {
$sprite-offset-x: nth($sprite, 3);
$sprite-offset-y: nth($sprite, 4);
background-position: $sprite-offset-x $sprite-offset-y;
}
@mixin {{spritesheet_info.strings.name}}-sprite-size($sprite) {
background-size: nth($sprite, 7) nth($sprite, 8);
}
@mixin {{spritesheet_info.strings.name}}-sprite-image($sprite) {
$sprite-image: nth($sprite, 9);
background-image: url(#{$sprite-image});
}
@mixin {{spritesheet_info.strings.name}}-sprite($name) {
@include {{spritesheet_info.strings.name}}-sprite-image(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-position(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-size(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-width(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
@include {{spritesheet_info.strings.name}}-sprite-height(map-get(${{spritesheet_info.strings.name}}-sprites, #{$name}));
background-repeat: no-repeat;
}
{{/if}}
{{/block}}
{{#block "spritesheet-functions"}}
{{#if options.functions}}
@mixin all-{{spritesheet_info.strings.name}}-sprites() {
@each $key, $val in ${{spritesheet_info.strings.name}}-sprites {
$sprite-name: nth($val, 10);
.{{spritesheet_info.strings.name}}-#{$sprite-name}-sprites {
@include {{spritesheet_info.strings.name}}-sprite($key);
}
}
}
{{/if}}
{{/block}}
Regular → Executable
+19 -42
View File
@@ -1,36 +1,21 @@
<script setup lang="ts">
import eruda from 'eruda'
import VConsole from 'vconsole'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import hotkeys from 'hotkeys-js'
import eventBus from './utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
import hotkeys from 'hotkeys-js'
import Provider from './ui-provider/index.vue'
import eventBus from './utils/eventBus'
const route = useRoute()
const settingsStore = useSettingsStore()
const { auth } = useAuth()
const buttonConfig = ref({
autoInsertSpace: true,
})
// 侧边栏主导航当前实际宽度
const mainSidebarActualWidth = computed(() => {
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
if (['head', 'single'].includes(settingsStore.settings.menu.menuMode)) {
actualWidth = 0
}
return `${actualWidth}px`
})
// 侧边栏次导航当前实际宽度
const subSidebarActualWidth = computed(() => {
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-width'))
if (settingsStore.settings.menu.subMenuCollapse) {
actualWidth = 64
}
return `${actualWidth}px`
const isAuth = computed(() => {
return route.matched.every((item) => {
return auth(item.meta.auth ?? '')
})
})
// 设置网页 title
watch([
() => settingsStore.settings.app.enableDynamicTitle,
() => settingsStore.title,
@@ -44,34 +29,26 @@ watch([
}
}, {
immediate: true,
deep: true,
})
onMounted(() => {
settingsStore.setMode(document.documentElement.clientWidth)
window.onresize = () => {
window.addEventListener('resize', () => {
settingsStore.setMode(document.documentElement.clientWidth)
}
})
hotkeys('alt+i', () => {
eventBus.emit('global-system-info-toggle')
})
})
import.meta.env.VITE_APP_DEBUG_TOOL === 'eruda' && eruda.init()
import.meta.env.VITE_APP_DEBUG_TOOL === 'vconsole' && new VConsole()
</script>
<template>
<el-config-provider :locale="zhCn" :size="settingsStore.settings.app.elementSize" :button="buttonConfig">
<RouterView
v-slot="{ Component, route }"
:style="{
'--g-main-sidebar-actual-width': mainSidebarActualWidth,
'--g-sub-sidebar-actual-width': subSidebarActualWidth,
}"
>
<component :is="Component" v-if="auth(route.meta.auth ?? '')" />
<not-allowed v-else />
<Provider>
<RouterView v-slot="{ Component }">
<component :is="Component" v-if="isAuth" />
<NotAllowed v-else />
</RouterView>
<system-info />
</el-config-provider>
<SystemInfo />
</Provider>
</template>
Regular → Executable
+16 -15
View File
@@ -1,9 +1,9 @@
import axios from 'axios'
// import qs from 'qs'
import { ElMessage } from 'element-plus'
import useUserStore from '@/store/modules/user'
import axios from 'axios'
// import qs from 'qs'
import Message from 'vue-m-message'
const api = axios.create({
baseURL: (import.meta.env.DEV && import.meta.env.VITE_OPEN_PROXY === 'true') ? '/proxy/' : import.meta.env.VITE_APP_API_BASEURL,
timeout: 1000 * 60,
@@ -12,13 +12,13 @@ const api = axios.create({
api.interceptors.request.use(
(request) => {
// 全局拦截请求发送前提交的参数
const userStore = useUserStore()
/**
* 全局拦截请求发送前提交的参数
* 以下代码为示例,在请求头里带上 token 信息
*/
if (userStore.isLogin && request.headers) {
request.headers.Token = userStore.token
// 设置请求头
if (request.headers) {
if (userStore.isLogin) {
request.headers.Token = userStore.token
}
}
// 是否将 POST 请求参数进行字符串化处理
if (request.method === 'post') {
@@ -40,8 +40,10 @@ api.interceptors.response.use(
*/
if (response.data.status === 1) {
if (response.data.error !== '') {
// 这里做错误提示,如果使用了 element plus 则可以使用 Message 进行提示
// ElMessage.error(options)
// 错误提示
Message.error(response.data.error, {
zIndex: 2000,
})
return Promise.reject(response.data)
}
}
@@ -61,9 +63,8 @@ api.interceptors.response.use(
else if (message.includes('Request failed with status code')) {
message = `接口${message.substr(message.length - 3)}异常`
}
ElMessage({
message,
type: 'error',
Message.error(message, {
zIndex: 2000,
})
return Promise.reject(error)
},
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M768 298.667h170.667V384h-256V128H768v170.667zM341.333 384h-256v-85.333H256V128h85.333v256zM768 725.333V896h-85.333V640h256v85.333H768zM341.333 640v256H256V725.333H85.333V640h256z"/></svg>

Before

Width:  |  Height:  |  Size: 299 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M682.667 128h256v256h-85.334V213.333H682.667V128zm-597.334 0h256v85.333H170.667V384H85.333V128zm768 682.667V640h85.334v256h-256v-85.333h170.666zm-682.666 0h170.666V896h-256V640h85.334v170.667z"/></svg>

Before

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M512.213 958.99 63.595 510.93l84.025-84.023 300.584 302.222V63.546h127.94V729.13l299.652-302.222 84.01 84.024-447.593 448.058z"/></svg>

Before

Width:  |  Height:  |  Size: 244 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M916.21 242.526v215.58H204.8l194.021-194.022-75.453-75.452L0 512l323.368 323.368 75.453-75.452L204.8 565.895H1024V242.526z"/></svg>

Before

Width:  |  Height:  |  Size: 240 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m959.804 511.604-84.009 84.024-299.652-302.222V958.99h-127.94V293.406L147.62 595.63l-84.025-84.024L512.213 63.546l447.591 448.058z"/></svg>

Before

Width:  |  Height:  |  Size: 248 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M165.415 827.077c-11.815 0-19.692-7.877-19.692-19.692V214.646c0-11.815 7.877-19.692 19.692-19.692h159.508c7.877 0 17.723 3.938 23.63 9.846l228.432 287.508c7.877 9.846 7.877 25.6 0 37.415l-230.4 287.508c-5.908 7.877-15.754 11.815-25.6 11.815l-155.57-1.97zm706.954-334.77L641.97 206.77c-9.846-11.815-27.569-15.754-41.354-3.938l-45.292 37.415c-13.785 9.846-15.754 29.539-3.938 41.354L738.462 512 551.385 744.37c-9.847 11.815-7.877 31.507 3.938 41.353l45.292 37.415c13.785 9.847 29.539 7.877 41.354-3.938l230.4-287.508c7.877-15.754 7.877-29.538 0-39.384z"/></svg>

Before

Width:  |  Height:  |  Size: 670 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M487 71.425a75 75 0 0 1 72.575 0l335.7 185.5a75 75 0 0 1 38.75 65.65V690.85a75 75 0 0 1-38.75 65.625l-335.7 185.55a75 75 0 0 1-72.55 0l-335.7-185.5a75 75 0 0 1-38.75-65.675V322.6a75 75 0 0 1 38.75-65.6L487 71.4v.025zM859 322.6 523.275 137.1l-335.7 185.5v368.25l335.7 185.5 335.75-185.5V322.6zm-601.75 37.1A37.5 37.5 0 0 1 308.2 345l215.1 118.875L738.45 345a37.5 37.5 0 0 1 36.25 65.625l-213.9 118.25V764.55a37.5 37.5 0 0 1-75 0v-235.7l-213.9-118.2a37.5 37.5 0 0 1-14.65-51v.05z"/></svg>

Before

Width:  |  Height:  |  Size: 597 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M913.6 135.2H380.2c-26.7 0-44.4 17.8-44.4 44.4V224c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4zm-755.6 0h-44.4c-22.2 0-44.4 22.2-44.4 44.4V224c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm0 311.1h-44.4c-22.2 0-44.4 22.2-44.4 44.4v44.4c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm755.6 0H380.2c-26.7 0-44.4 17.8-44.4 44.4v44.4c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4zM158 757.4h-44.4c-22.2 0-44.4 22.2-44.4 44.4v44.4c0 22.2 22.2 44.4 44.4 44.4H158c22.2 0 44.4-22.2 44.4-44.4v-44.4c.1-22.2-22.1-44.4-44.4-44.4zm755.6 0H380.2c-26.7 0-44.4 17.8-44.4 44.4v44.4c0 26.7 17.8 44.4 44.4 44.4h533.3c26.7 0 44.4-17.8 44.4-44.4v-44.4c.1-26.6-17.7-44.4-44.3-44.4z"/></svg>

Before

Width:  |  Height:  |  Size: 971 B

View File

Before

Width:  |  Height:  |  Size: 998 B

After

Width:  |  Height:  |  Size: 998 B

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Regular → Executable
View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File
-9
View File
@@ -1,9 +0,0 @@
// 暗黑模式
@forward "element-plus/theme-chalk/src/dark/var.scss" with (
$bg-color: (
"page": #0a0a0a,
"": #141414,
"overlay": #1d1e1f,
)
);
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
@@ -1,9 +0,0 @@
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
// $colors: (
// "primary": (
// "base": green
// )
// )
// );
@use "element-plus/theme-chalk/src/index.scss" as *;
@use "./dark.scss";
+149
View File
@@ -0,0 +1,149 @@
/* 页面布局 CSS 变量 */
:root {
/* 头部高度 */
--g-header-height: 60px;
/* 侧边栏宽度 */
--g-main-sidebar-width: 80px;
--g-sub-sidebar-width: 220px;
--g-sub-sidebar-collapse-width: 64px;
/* 侧边栏 Logo 区域高度 */
--g-sidebar-logo-height: 50px;
/* 标签栏高度 */
--g-tabbar-height: 50px;
/* 工具栏高度 */
--g-toolbar-height: 50px;
}
/* 明暗模式 CSS 变量 */
/* stylelint-disable-next-line no-duplicate-selectors */
:root {
--g-box-shadow-color: rgb(0 0 0 / 12%);
&::view-transition-old(root),
&::view-transition-new(root) {
mix-blend-mode: normal;
animation: none;
}
&::view-transition-old(root) {
z-index: 1;
}
&::view-transition-new(root) {
z-index: 9999;
}
&.dark {
--g-box-shadow-color: rgb(0 0 0 / 72%);
&::view-transition-old(root) {
z-index: 9999;
}
&::view-transition-new(root) {
z-index: 1;
}
}
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-thumb {
background-color: rgb(0 0 0 / 40%);
background-clip: padding-box;
border: 3px solid transparent;
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgb(0 0 0 / 50%);
}
::-webkit-scrollbar-track {
background-color: transparent;
}
html,
body {
height: 100%;
}
body {
box-sizing: border-box;
margin: 0;
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
background-color: var(--g-container-bg);
-webkit-tap-highlight-color: transparent;
&.overflow-hidden {
overflow: hidden;
}
}
* {
box-sizing: inherit;
}
#app {
height: 100%;
}
/* 右侧内容区针对fixed元素,有横向铺满的需求,可在fixed元素上设置 [data-fixed-calc-width] */
[data-fixed-calc-width] {
position: fixed;
right: 0;
left: 50%;
width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2));
[data-mode="mobile"] & {
width: 100% !important;
transform: translateX(-50%) !important;
}
}
/* textarea 字体跟随系统 */
textarea {
font-family: inherit;
}
/* Overrides Floating Vue */
.v-popper--theme-dropdown,
.v-popper--theme-tooltip {
--uno: inline-flex;
}
.v-popper--theme-dropdown .v-popper__inner,
.v-popper--theme-tooltip .v-popper__inner {
--uno: bg-white dark-bg-stone-8 text-dark dark-text-white rounded shadow ring-1 ring-gray-200 dark-ring-gray-800 border border-solid border-stone/20 text-xs font-normal;
box-shadow: 0 6px 30px rgb(0 0 0 / 10%);
}
.v-popper--theme-tooltip .v-popper__arrow-inner,
.v-popper--theme-dropdown .v-popper__arrow-inner {
visibility: visible;
--uno: border-white dark-border-stone-8;
}
.v-popper--theme-tooltip .v-popper__arrow-outer,
.v-popper--theme-dropdown .v-popper__arrow-outer {
--uno: border-stone/20;
}
.v-popper--theme-tooltip.v-popper--shown,
.v-popper--theme-tooltip.v-popper--shown * {
transition: none !important;
}
[data-overlayscrollbars-contents] {
overscroll-behavior: contain;
}
-88
View File
@@ -1,88 +0,0 @@
@use "./themes.scss";
@use "./element-plus/index.scss";
// 全局变量
:root {
// 头部宽度(默认自适应宽度,可固定宽度,固定宽度后为居中显示)
--g-header-width: 100%;
// 头部高度
--g-header-height: 70px;
// 侧边栏宽度
--g-main-sidebar-width: 70px;
--g-sub-sidebar-width: 220px;
// 侧边栏Logo高度
--g-sidebar-logo-height: 50px;
// 顶栏高度
--g-topbar-height: 50px;
}
// 全局样式
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-thumb {
background-color: rgb(0 0 0 / 40%);
background-clip: padding-box;
border: 3px solid transparent;
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgb(0 0 0 / 50%);
}
::-webkit-scrollbar-track {
background-color: transparent;
}
html,
body {
height: 100%;
}
body {
margin: 0;
box-sizing: border-box;
font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-tap-highlight-color: transparent;
&.overflow-hidden {
overflow: hidden;
}
}
* {
box-sizing: inherit;
}
// 右侧内容区针对fixed元素,有横向铺满的需求,可在fixed元素上设置 [data-fixed-calc-width]
[data-fixed-calc-width] {
position: fixed;
left: 50%;
right: 0;
width: calc(100% - var(--g-main-sidebar-actual-width) - var(--g-sub-sidebar-actual-width));
transform: translateX(-50%) translateX(calc(var(--g-main-sidebar-actual-width) / 2)) translateX(calc(var(--g-sub-sidebar-actual-width) / 2));
}
[data-mode="mobile"] {
[data-fixed-calc-width] {
width: 100% !important;
transform: translateX(-50%) !important;
}
}
// textarea 字体跟随系统
textarea {
font-family: inherit;
}
// 列表页全局样式
.el-table.list-table {
margin: 20px 0;
.el-button + .el-dropdown {
margin-left: 10px;
}
}
.el-loading-mask {
z-index: 900 !important;
}
+10 -10
View File
@@ -3,48 +3,48 @@
.bar {
position: fixed;
z-index: 1031;
top: 0;
left: 0;
z-index: 2000;
width: 100%;
height: 2px;
background: var(--g-nprogress-color);
background: rgb(var(--ui-primary));
}
.peg {
display: block;
position: absolute;
right: 0;
display: block;
width: 100px;
height: 100%;
box-shadow: 0 0 10px rgb(var(--ui-primary)), 0 0 5px rgb(var(--ui-primary));
opacity: 1;
transform: rotate(3deg) translate(0, -4px);
box-shadow: 0 0 10px var(--g-nprogress-color), 0 0 5px var(--g-nprogress-color);
}
.spinner {
display: block;
position: fixed;
z-index: 1031;
top: 11px;
right: 14px;
z-index: 2000;
display: block;
.spinner-icon {
box-sizing: border-box;
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: rgb(var(--ui-primary));
border-left-color: rgb(var(--ui-primary));
border-radius: 50%;
animation: nprogress-spinner 400ms linear infinite;
border-top-color: var(--g-nprogress-color);
border-left-color: var(--g-nprogress-color);
}
}
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
overflow: hidden;
#nprogress .spinner,
#nprogress .bar {
+3 -3
View File
@@ -7,9 +7,9 @@
} @else {
/* stylelint-disable-next-line value-no-vendor-prefix */
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: $line;
overflow: hidden;
}
}
@@ -28,8 +28,8 @@
}
@if $type == xy {
left: 50%;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
}
@@ -46,8 +46,8 @@
&::before,
&::after {
content: "";
display: block;
clear: both;
content: "";
}
}
View File
-124
View File
@@ -1,124 +0,0 @@
:root {
--g-box-shadow-color: rgb(0 0 0 / 12%);
/* ----- 应用 ----- */
// 应用背景色
--g-app-bg: var(--el-bg-color);
// 主区域背景色
--g-main-bg: var(--el-bg-color-page);
/* ----- 顶部 ----- */
// 背景色
--g-header-bg: #222b45;
// 文字颜色
--g-header-color: #fff;
// 导航文字颜色
--g-header-menu-color: #fff;
// 导航鼠标悬浮文字颜色
--g-header-menu-hover-color: #fff;
// 导航鼠标悬浮背景色
--g-header-menu-hover-bg: #334067;
// 导航选中文字颜色
--g-header-menu-active-color: #fff;
// 导航选中背景色
--g-header-menu-active-bg: #334067;
/* ----- 主导航 ----- */
// 背景色
--g-main-sidebar-bg: #222b45;
// 菜单文字颜色
--g-main-sidebar-menu-color: #eee;
// 菜单鼠标悬浮文字颜色
--g-main-sidebar-menu-hover-color: #fff;
// 菜单鼠标悬浮背景色
--g-main-sidebar-menu-hover-bg: #334067;
// 菜单选中文字颜色
--g-main-sidebar-menu-active-color: #fff;
// 菜单选中背景色
--g-main-sidebar-menu-active-bg: #334067;
/* ----- 次导航 ----- */
// 背景色
--g-sub-sidebar-bg: #fff;
// 菜单背景色
--g-sub-sidebar-menu-bg: #e8e8e8;
// 菜单文字颜色
--g-sub-sidebar-menu-color: var(--el-text-color-primary);
// 菜单鼠标悬浮文字颜色
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
// 菜单鼠标悬浮背景色
--g-sub-sidebar-menu-hover-bg: #e1e1e1;
// 菜单选中文字颜色
--g-sub-sidebar-menu-active-color: #fff;
// 菜单选中背景色
--g-sub-sidebar-menu-active-bg: var(--el-color-primary);
/* ----- 顶栏 ----- */
--g-toolbar-bg: var(--el-bg-color);
/* ----- 进度条 ----- */
--g-nprogress-color: var(--el-color-primary);
/* ----- 暗黑模式 ----- */
&.dark {
--g-box-shadow-color: rgb(0 0 0 / 72%);
/* ----- 应用 ----- */
// 应用背景色
--g-app-bg: var(--el-bg-color);
// 主区域背景色
--g-main-bg: var(--el-bg-color-page);
/* ----- 顶部 ----- */
// 背景色
--g-header-bg: var(--el-bg-color);
// 文字颜色
--g-header-color: #fff;
// 导航文字颜色
--g-header-menu-color: #ccc;
// 导航鼠标悬浮文字颜色
--g-header-menu-hover-color: #fff;
// 导航鼠标悬浮背景色
--g-header-menu-hover-bg: var(--el-color-primary-light-5);
// 导航选中文字颜色
--g-header-menu-active-color: #fff;
// 导航选中背景色
--g-header-menu-active-bg: var(--el-color-primary-light-3);
/* ----- 主导航 ----- */
// 背景色
--g-main-sidebar-bg: var(--el-bg-color);
// 菜单文字颜色
--g-main-sidebar-menu-color: #ccc;
// 菜单鼠标悬浮文字颜色
--g-main-sidebar-menu-hover-color: #fff;
// 菜单鼠标悬浮背景色
--g-main-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
// 菜单选中文字颜色
--g-main-sidebar-menu-active-color: #fff;
// 菜单选中背景色
--g-main-sidebar-menu-active-bg: var(--el-color-primary-light-3);
/* ----- 次导航 ----- */
// 背景色
--g-sub-sidebar-bg: var(--el-bg-color-page);
// 菜单背景色
--g-sub-sidebar-menu-bg: var(--el-bg-color);
// 菜单文字颜色
--g-sub-sidebar-menu-color: var(--el-text-color-secondary);
// 菜单鼠标悬浮文字颜色
--g-sub-sidebar-menu-hover-color: var(--el-text-color-primary);
// 菜单鼠标悬浮背景色
--g-sub-sidebar-menu-hover-bg: var(--el-color-primary-light-5);
// 菜单选中文字颜色
--g-sub-sidebar-menu-active-color: var(--el-text-color-primary);
// 菜单选中背景色
--g-sub-sidebar-menu-active-bg: var(--el-color-primary-light-3);
/* ----- 顶栏 ----- */
--g-toolbar-bg: var(--el-bg-color);
/* ----- 进度条 ----- */
--g-nprogress-color: var(--el-color-primary);
}
}
Regular → Executable
+8 -5
View File
@@ -5,16 +5,19 @@ defineOptions({
const props = defineProps<{
value: string | string[]
all?: boolean
}>()
const auth = useAuth()
function check() {
return useAuth().auth(props.value)
return props.all
? auth.authAll(typeof props.value === 'string' ? [props.value] : props.value)
: auth.auth(props.value)
}
</script>
<template>
<div>
<slot v-if="check()" />
<slot v-else name="no-auth" />
</div>
<slot v-if="check()" />
<slot v-else name="no-auth" />
</template>
-20
View File
@@ -1,20 +0,0 @@
<script setup lang="ts">
defineOptions({
name: 'AuthAll',
})
const props = defineProps<{
value: string[]
}>()
function check() {
return useAuth().authAll(props.value)
}
</script>
<template>
<div>
<slot v-if="check()" />
<slot v-else name="no-auth" />
</div>
</template>
+33 -7
View File
@@ -17,6 +17,7 @@ const props = withDefaults(
files?: UploadUserFile[]
notip?: boolean
ext?: string[]
httpRequest?: UploadProps['httpRequest']
}>(),
{
name: 'file',
@@ -29,7 +30,7 @@ const props = withDefaults(
)
const emits = defineEmits<{
'onSuccess': [
onSuccess: [
res: any,
file: UploadUserFile,
fileList: UploadUserFile[],
@@ -57,10 +58,33 @@ const onExceed: UploadProps['onExceed'] = () => {
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
emits('onSuccess', res, file, fileList)
}
const onPreview: UploadProps['onPreview'] = (e) => {
const getBlob = (url: string) => new Promise((resolve) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
}
}
xhr.send()
})
getBlob(e.url!).then((blob: any) => {
const a = document.createElement('a')
const url = window.URL.createObjectURL(blob)
const event = new MouseEvent('click')
a.target = '_blank'
a.download = e.name
a.href = url
a.dispatchEvent(event)
})
}
</script>
<template>
<el-upload
<ElUpload
:headers="headers"
:action="action"
:data="data"
@@ -68,12 +92,14 @@ const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
:before-upload="beforeUpload"
:on-exceed="onExceed"
:on-success="onSuccess"
:file-list="files as UploadUserFile[]"
:on-preview="onPreview"
:http-request="httpRequest"
:file-list="files"
:limit="max"
drag
>
<div class="slot">
<svg-icon name="ep:upload-filled" class="el-icon--upload" />
<SvgIcon name="i-ep:upload-filled" class="el-icon--upload" />
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
@@ -81,14 +107,14 @@ const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
<template #tip>
<div v-if="!notip" class="el-upload__tip">
<div style="display: inline-block;">
<el-alert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
<ElAlert :title="`上传文件支持 ${ext.join(' / ')} 格式,单个文件大小不超过 ${size}MB,且文件数量不超过 ${max} 个`" type="info" show-icon :closable="false" />
</div>
</div>
</template>
</el-upload>
</ElUpload>
</template>
<style lang="scss" scoped>
<style scoped>
:deep(.el-upload.is-drag) {
display: inline-block;
+5 -13
View File
@@ -27,27 +27,19 @@ function onScroll() {
</script>
<template>
<div class="actionbar" :class="{ shadow: !isBottom }" data-fixed-calc-width>
<div
class="fixed-action-bar bottom-0 z-4 bg-[var(--g-container-bg)] p-5 text-center transition" :class="{ shadow: !isBottom }" data-fixed-calc-width
>
<slot />
</div>
</template>
<style lang="scss" scoped>
.actionbar {
z-index: 4;
bottom: 0;
padding: 20px;
text-align: center;
background-color: var(--g-app-bg);
<style scoped>
.fixed-action-bar {
box-shadow: 0 0 1px 0 var(--g-box-shadow-color);
transition: all 0.3s, var(--el-transition-box-shadow);
&.shadow {
box-shadow: 0 -10px 10px -10px var(--g-box-shadow-color);
}
}
:deep(.el-form-item) {
margin-bottom: 0;
}
</style>
+8 -8
View File
@@ -25,25 +25,25 @@ const realHeight = computed(() => {
</script>
<template>
<el-image :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]" preview-teleported>
<ElImage :src="src" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[src]" preview-teleported>
<template #error>
<div class="image-slot">
<svg-icon name="image-load-fail" />
<SvgIcon name="image-load-fail" />
</div>
</template>
</el-image>
</ElImage>
</template>
<style lang="scss" scoped>
<style scoped>
.el-image {
border-radius: 5px;
background-color: var(--el-fill-color);
border-radius: 5px;
box-shadow: var(--el-box-shadow-light);
transition: background-color 0.3s, var(--el-transition-box-shadow);
:deep(.el-image__inner) {
transition: all 0.3s;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: scale(1.2);
@@ -52,12 +52,12 @@ const realHeight = computed(() => {
:deep(.image-slot) {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #909399;
font-size: 30px;
color: #909399;
}
}
</style>
+39 -34
View File
@@ -12,17 +12,16 @@ const props = withDefaults(
headers?: UploadProps['headers']
data?: UploadProps['data']
name?: UploadProps['name']
url?: string
size?: number
width?: number
height?: number
placeholder?: string
notip?: boolean
ext?: string[]
httpRequest?: UploadProps['httpRequest']
}>(),
{
name: 'file',
url: '',
size: 2,
width: 150,
height: 150,
@@ -33,14 +32,15 @@ const props = withDefaults(
)
const emits = defineEmits<{
'update:url': [
url: string,
]
'onSuccess': [
onSuccess: [
res: any,
]
}>()
const url = defineModel<string>({
default: '',
})
const uploadData = ref({
imageViewerVisible: false,
progress: {
@@ -59,7 +59,7 @@ function previewClose() {
}
// 移除
function remove() {
emits('update:url', '')
url.value = ''
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const fileName = file.name.split('.')
@@ -89,7 +89,7 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
<template>
<div class="upload-container">
<el-upload
<ElUpload
:show-file-list="false"
:headers="headers"
:action="action"
@@ -98,44 +98,45 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
:before-upload="beforeUpload"
:on-progress="onProgress"
:on-success="onSuccess"
:http-request="httpRequest"
drag
class="image-upload"
>
<el-image v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
<ElImage v-if="url === ''" :src="url === '' ? placeholder : url" :style="`width:${width}px;height:${height}px;`" fit="fill">
<template #error>
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
<svg-icon name="ep:plus" />
<SvgIcon name="i-ep:plus" class="icon" />
</div>
</template>
</el-image>
</ElImage>
<div v-else class="image">
<el-image :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
<ElImage :src="url" :style="`width:${width}px;height:${height}px;`" fit="fill" />
<div class="mask">
<div class="actions">
<span title="预览" @click.stop="preview">
<svg-icon name="ep:zoom-in" />
<SvgIcon name="i-ep:zoom-in" class="icon" />
</span>
<span title="移除" @click.stop="remove">
<svg-icon name="ep:delete" />
<SvgIcon name="i-ep:delete" class="icon" />
</span>
</div>
</div>
</div>
<div v-show="url === '' && uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
</div>
</el-upload>
</ElUpload>
<div v-if="!notip" class="el-upload__tip">
<div style="display: inline-block;">
<el-alert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,且图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height}`" type="info" show-icon :closable="false" />
</div>
</div>
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="[url]" teleported @close="previewClose" />
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="[url]" teleported @close="previewClose" />
</div>
</template>
<style lang="scss" scoped>
<style scoped>
.upload-container {
line-height: initial;
}
@@ -146,33 +147,35 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
.image {
position: relative;
border-radius: 6px;
overflow: hidden;
border-radius: 6px;
.mask {
opacity: 0;
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-color: var(--el-overlay-color-lighter);
opacity: 0;
transition: opacity 0.3s;
.actions {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
@include position-center(xy);
width: 100px;
height: 100px;
transform: translateX(-50%) translateY(-50%);
span {
width: 50%;
color: var(--el-color-white);
text-align: center;
cursor: pointer;
color: var(--el-color-white);
transition: color 0.1s, transform 0.1s;
&:hover {
@@ -207,14 +210,14 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
.image-slot {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--el-text-color-placeholder);
background-color: transparent;
i {
.icon {
font-size: 30px;
}
}
@@ -224,19 +227,21 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
top: 0;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
left: 0;
top: 0;
content: "";
background-color: var(--el-overlay-color-lighter);
}
.el-progress {
position: absolute;
top: 50%;
left: 50%;
z-index: 1;
@include position-center(xy);
transform: translateX(-50%) translateY(-50%);
.el-progress__text {
color: var(--el-text-color-placeholder);
+42 -41
View File
@@ -12,7 +12,6 @@ const props = withDefaults(
headers?: UploadProps['headers']
data?: UploadProps['data']
name?: UploadProps['name']
url?: string[]
size?: number
max?: number
width?: number
@@ -20,10 +19,10 @@ const props = withDefaults(
placeholder?: string
notip?: boolean
ext?: string[]
httpRequest?: UploadProps['httpRequest']
}>(),
{
name: 'file',
url: () => [],
size: 2,
max: 3,
width: 150,
@@ -35,14 +34,15 @@ const props = withDefaults(
)
const emits = defineEmits<{
'update:url': [
value: string[],
]
'onSuccess': [
onSuccess: [
res: any,
]
}>()
const url = defineModel<string[]>({
default: [],
})
const uploadData = ref({
dialogImageIndex: 0,
imageViewerVisible: false,
@@ -63,20 +63,16 @@ function previewClose() {
}
// 移除
function remove(index: number) {
const url = props.url
url.splice(index, 1)
emits('update:url', url)
url.value.splice(index, 1)
}
// 移动
function move(index: number, type: 'left' | 'right') {
const url = props.url
if (type === 'left' && index !== 0) {
url[index] = url.splice(index - 1, 1, url[index])[0]
url.value[index] = url.value.splice(index - 1, 1, url.value[index])[0]
}
if (type === 'right' && index !== url.length - 1) {
url[index] = url.splice(index + 1, 1, url[index])[0]
if (type === 'right' && index !== url.value.length - 1) {
url.value[index] = url.value.splice(index + 1, 1, url.value[index])[0]
}
emits('update:url', url)
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
@@ -108,25 +104,25 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
<template>
<div class="upload-container">
<div v-for="(item, index) in (url as string[])" :key="index" class="images">
<el-image v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
<ElImage v-if="index < max" :src="item" :style="`width:${width}px;height:${height}px;`" fit="cover" />
<div class="mask">
<div class="actions">
<span title="预览" @click="preview(index)">
<svg-icon name="ep:zoom-in" />
<SvgIcon name="i-ep:zoom-in" class="icon" />
</span>
<span title="移除" @click="remove(index)">
<svg-icon name="ep:delete" />
<SvgIcon name="i-ep:delete" class="icon" />
</span>
<span v-show="url.length > 1" title="左移" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
<svg-icon name="ep:back" />
<SvgIcon name="i-ep:back" class="icon" />
</span>
<span v-show="url.length > 1" title="右移" :class="{ disabled: index === url.length - 1 }" @click="move(index, 'right')">
<svg-icon name="ep:right" />
<SvgIcon name="i-ep:right" class="icon" />
</span>
</div>
</div>
</div>
<el-upload
<ElUpload
v-show="url.length < max"
:show-file-list="false"
:headers="headers"
@@ -136,27 +132,28 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
:before-upload="beforeUpload"
:on-progress="onProgress"
:on-success="onSuccess"
:http-request="httpRequest"
drag
class="images-upload"
>
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
<svg-icon name="ep:plus" />
<SvgIcon name="i-ep:plus" class="icon" />
</div>
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
<el-image :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
<el-progress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
<ElProgress type="circle" :width="Math.min(width, height) * 0.8" :percentage="uploadData.progress.percent" />
</div>
</el-upload>
</ElUpload>
<div v-if="!notip" class="el-upload__tip">
<div style="display: inline-block;">
<el-alert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
<ElAlert :title="`上传图片支持 ${ext.join(' / ')} 格式,单张图片大小不超过 ${size}MB,建议图片尺寸为 ${width}*${height},且图片数量不超过 ${max} 张`" type="info" show-icon :closable="false" />
</div>
</div>
<el-image-viewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" teleported @close="previewClose" />
<ElImageViewer v-if="uploadData.imageViewerVisible" :url-list="url as string[]" :initial-index="uploadData.dialogImageIndex" teleported @close="previewClose" />
</div>
</template>
<style lang="scss" scoped>
<style scoped>
.upload-container {
line-height: initial;
}
@@ -169,34 +166,36 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
position: relative;
display: inline-block;
margin-right: 10px;
overflow: hidden;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
overflow: hidden;
.mask {
opacity: 0;
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-color: var(--el-overlay-color-lighter);
opacity: 0;
transition: opacity 0.3s;
.actions {
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
@include position-center(xy);
width: 100px;
height: 100px;
transform: translateX(-50%) translateY(-50%);
span {
width: 50%;
color: var(--el-color-white);
text-align: center;
cursor: pointer;
color: var(--el-color-white);
transition: color 0.1s, transform 0.1s;
&.disabled {
@@ -236,14 +235,14 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
.image-slot {
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--el-text-color-placeholder);
background-color: transparent;
i {
.icon {
font-size: 30px;
}
}
@@ -253,19 +252,21 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
top: 0;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
left: 0;
top: 0;
content: "";
background-color: var(--el-overlay-color-lighter);
}
.el-progress {
position: absolute;
top: 50%;
left: 50%;
z-index: 1;
@include position-center(xy);
transform: translateX(-50%) translateY(-50%);
.el-progress__text {
color: var(--el-text-color-placeholder);
+133
View File
@@ -0,0 +1,133 @@
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import useUserStore from '@/store/modules/user'
defineOptions({
name: 'LoginForm',
})
const props = defineProps<{
account?: string
}>()
const emits = defineEmits<{
onLogin: [account: string]
onRegister: [account: string]
onResetPassword: [account: string]
}>()
const userStore = useUserStore()
const title = import.meta.env.VITE_APP_TITLE
const loading = ref(false)
// 登录方式,default 账号密码登录,qrcode 扫码登录
const type = ref('default')
const formRef = useTemplateRef<FormInstance>('formRef')
const form = ref({
account: props.account ?? localStorage.login_account ?? '',
password: '',
remember: !!localStorage.login_account,
})
const rules = ref<FormRules>({
account: [
{ required: true, trigger: 'blur', message: '请输入用户名' },
],
password: [
{ required: true, trigger: 'blur', message: '请输入密码' },
{ min: 6, max: 18, trigger: 'blur', message: '密码长度为6到18位' },
],
})
function handleLogin() {
formRef.value?.validate((valid) => {
if (valid) {
loading.value = true
userStore.login(form.value).then(() => {
if (form.value.remember) {
localStorage.setItem('login_account', form.value.account)
}
else {
localStorage.removeItem('login_account')
}
emits('onLogin', form.value.account)
}).finally(() => {
loading.value = false
})
}
})
}
function testAccount(account: string) {
form.value.account = account
form.value.password = '123456'
handleLogin()
}
</script>
<template>
<ElForm ref="formRef" :model="form" :rules="rules" class="min-h-500px w-full flex-col-stretch-center p-12">
<div class="mb-6">
<HTabList
v-model="type" :options="[
{ label: '账号密码登录', value: 'default' },
{ label: '扫码登录', value: 'qrcode' },
]"
/>
</div>
<template v-if="type === 'default'">
<h3 class="mb-8 text-xl color-[var(--el-text-color-primary)] font-bold">
欢迎使用 {{ title }} ! 👋🏻
</h3>
<div>
<ElFormItem prop="account">
<ElInput v-model="form.account" size="large" placeholder="用户名" type="text" tabindex="1">
<template #prefix>
<SvgIcon name="i-ri:user-3-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="password">
<ElInput v-model="form.password" type="password" size="large" placeholder="密码" tabindex="2" show-password @keyup.enter="handleLogin">
<template #prefix>
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
</div>
<div class="mb-4 flex-center-between">
<ElCheckbox v-model="form.remember">
记住我
</ElCheckbox>
<ElLink type="primary" :underline="false" @click="emits('onResetPassword', form.account)">
忘记密码了?
</ElLink>
</div>
<ElButton :loading="loading" type="primary" size="large" style="width: 100%;" @click.prevent="handleLogin">
登录
</ElButton>
<div class="mt-4 flex-center gap-2 text-sm color-[var(--el-text-color-secondary)]">
还没有帐号?
<ElLink type="primary" :underline="false" @click="emits('onRegister', form.account)">
创建新帐号
</ElLink>
</div>
</template>
<template v-else-if="type === 'qrcode'">
<div class="flex-col-center">
<el-image src="https://s2.loli.net/2024/04/26/GsahtuIZ9XOg5jr.png" class="h-[250px] w-[250px]" />
<div class="mt-2 op-50">
请使用微信扫码登录
</div>
</div>
</template>
<div class="mt-4 text-center -mb-4">
<ElDivider>演示账号一键登录</ElDivider>
<ElButton type="primary" size="small" plain @click="testAccount('admin')">
admin
</ElButton>
<ElButton size="small" plain @click="testAccount('test')">
test
</ElButton>
</div>
</ElForm>
</template>
+17 -39
View File
@@ -1,10 +1,14 @@
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'NotAllowed',
})
const router = useRouter()
const settingsStore = useSettingsStore()
const data = ref({
inter: Number.NaN,
countdown: 5,
@@ -25,51 +29,25 @@ onMounted(() => {
})
function goBack() {
router.push('/')
router.push(settingsStore.settings.home.fullPath)
}
</script>
<template>
<div class="notallowed">
<svg-icon name="403" class="icon" />
<div class="content">
<h1>403</h1>
<div class="desc">
<div class="absolute left-[50%] top-[50%] flex flex-col items-center justify-between lg-flex-row -translate-x-50% -translate-y-50% lg-gap-12">
<SvgIcon name="403" class="text-[300px] lg-text-[400px]" />
<div class="flex flex-col gap-4">
<h1 class="m-0 text-6xl font-sans">
403
</h1>
<div class="desc mx-0 text-xl text-stone-5">
抱歉,你无权访问该页面
</div>
<el-button type="primary" @click="goBack">
{{ data.countdown }} 秒后,返回首页
</el-button>
<div>
<HButton @click="goBack">
{{ data.countdown }} 秒后,返回首页
</HButton>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.notallowed {
display: flex;
align-items: center;
justify-content: space-between;
width: 700px;
@include position-center(xy);
.icon {
width: 400px;
height: 400px;
}
.content {
h1 {
margin: 0;
font-size: 72px;
color: var(--el-text-color-primary);
}
.desc {
margin: 20px 0 30px;
font-size: 20px;
color: var(--el-text-color-secondary);
}
}
}
</style>
+12 -55
View File
@@ -3,73 +3,30 @@ defineOptions({
name: 'PageHeader',
})
withDefaults(
defineProps<{
title: string
content?: string
}>(),
{
content: '',
},
)
defineProps<{
title?: string
content?: string
}>()
const slots = useSlots()
</script>
<template>
<div class="page-header">
<div class="main">
<div class="title">
{{ title }}
<div class="page-header mb-5 flex flex-wrap items-center justify-between gap-5 bg-[var(--g-container-bg)] px-5 py-4 transition-background-color-300">
<div class="main flex-[1_1_70%]">
<div class="text-2xl">
<slot name="title">
{{ title }}
</slot>
</div>
<div class="content">
<div class="mt-2 text-sm text-stone-5 empty-hidden">
<slot name="content">
{{ content }}
</slot>
</div>
</div>
<div v-if="slots.default" class="sub">
<div v-if="slots.default" class="ml-a flex-none">
<slot />
</div>
</div>
</template>
<style lang="scss" scoped>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
padding: 16px 20px;
background-color: var(--g-app-bg);
transition: background-color 0.3s;
.main {
flex: 1 1 70%;
.title {
font-size: 22px;
color: var(--el-text-color-primary);
transition: var(--el-transition-color);
}
.content {
margin-top: 10px;
font-size: 14px;
color: var(--el-text-color-secondary);
transition: var(--el-transition-color);
&:empty {
display: none;
}
}
}
.sub {
flex: none;
margin-left: auto;
}
}
</style>
+13 -55
View File
@@ -18,72 +18,30 @@ const props = withDefaults(
const titleSlot = !!useSlots().title
const collaspeData = ref(props.collaspe)
const isCollaspe = ref(props.collaspe)
function unCollaspe() {
collaspeData.value = false
isCollaspe.value = false
}
</script>
<template>
<div
class="page-main" :class="{
'is-collaspe': collaspeData,
class="page-main relative m-4 flex flex-col bg-[var(--g-container-bg)] transition-background-color-300" :class="{
'of-hidden': isCollaspe,
}" :style="{
height: collaspeData ? height : '',
height: isCollaspe ? height : '',
}"
>
<div v-if="titleSlot || title" class="title-container">
<slot v-if="titleSlot" name="title" />
<template v-else>
<div v-if="titleSlot || title" class="title-container border-b-1 border-b-[var(--g-bg)] border-b-solid px-5 py-4 transition-border-color-300">
<slot name="title">
{{ title }}
</template>
</slot>
</div>
<slot />
<div v-if="collaspeData" class="collaspe" title="展开" @click="unCollaspe">
<svg-icon name="ep:arrow-down" />
<div class="main-container p-5">
<slot />
</div>
<div v-if="isCollaspe" class="collaspe absolute bottom-0 w-full cursor-pointer from-transparent to-[var(--g-container-bg)] bg-gradient-to-b pb-2 pt-10 text-center" @click="unCollaspe">
<SvgIcon name="i-ep:arrow-down" class="text-xl op-30 transition-opacity hover-op-100" />
</div>
</div>
</template>
<style lang="scss" scoped>
.page-main {
position: relative;
margin: 20px;
padding: 20px;
background-color: var(--g-app-bg);
transition: background-color 0.3s;
&.is-collaspe {
overflow: hidden;
.collaspe {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
padding: 40px 0 10px;
text-align: center;
font-size: 24px;
color: var(--el-text-color-primary);
text-shadow: 0 0 1px var(--el-text-color-primary);
background: linear-gradient(to bottom, transparent, var(--g-app-bg));
transition: background 0.3s, var(--el-transition-color);
cursor: pointer;
&:hover {
color: var(--el-text-color-secondary);
}
}
}
.title-container {
width: calc(100% + 40px);
padding: 14px 20px;
margin-left: -20px;
margin-top: -20px;
margin-bottom: 20px;
border-bottom: 1px solid var(--el-border-color-lighter);
transition: var(--el-transition-border);
}
}
</style>
+10 -16
View File
@@ -8,10 +8,6 @@ defineOptions({
const props = withDefaults(
defineProps<{
modelValue: string[] | {
code: string
name: string
}[]
disabled?: boolean
type?: 'pc' | 'pca' | 'pcas'
format?: 'code' | 'name' | 'both'
@@ -23,14 +19,12 @@ const props = withDefaults(
},
)
const emits = defineEmits<{
'update:modelValue': [
value: string[] | {
code: string
name: string
}[],
]
}>()
const value = defineModel<string[] | {
code: string
name: string
}[]>({
default: [],
})
interface pcasItem {
code: string
@@ -88,11 +82,11 @@ const pcasData = computed(() => {
const myValue = computed({
// 将入参数据转成 code 码
get: () => {
return anyToCode(props.modelValue)
return anyToCode(value.value)
},
// 将 code 码转成出参数据
set: (value) => {
emits('update:modelValue', value ? codeToAny(value) : [])
set: (val) => {
value.value = val ? codeToAny(val) : []
},
})
@@ -145,5 +139,5 @@ function codeToAny(codes: string[], dictionarie: any[] = pcasData.value): any {
</script>
<template>
<el-cascader v-model="myValue" :options="pcasData as any[]" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
<ElCascader v-model="myValue" :options="pcasData as any[]" :props="{ value: 'code', label: 'name' }" :disabled="disabled" clearable filterable />
</template>
+1 -1
View File
File diff suppressed because one or more lines are too long
+114
View File
@@ -0,0 +1,114 @@
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
defineOptions({
name: 'RegisterForm',
})
const props = defineProps<{
account?: string
}>()
const emits = defineEmits<{
onLogin: [account: string]
onRegister: [account: string]
}>()
const loading = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const form = ref({
account: props.account ?? '',
captcha: '',
password: '',
checkPassword: '',
})
const rules = ref<FormRules>({
account: [
{ required: true, trigger: 'blur', message: '请输入用户名' },
],
captcha: [
{ required: true, trigger: 'blur', message: () => '请输入验证码' },
],
password: [
{ required: true, trigger: 'blur', message: '请输入密码' },
{ min: 6, max: 18, trigger: 'blur', message: '密码长度为6到18位' },
],
checkPassword: [
{ required: true, trigger: 'blur', message: '请再次输入密码' },
{
validator: (_rule, value, callback) => {
if (value !== form.value.password) {
callback(new Error('两次输入的密码不一致'))
}
else {
callback()
}
},
},
],
})
function handleRegister() {
ElMessage({
message: '注册模块仅提供界面演示,无实际功能,需开发者自行扩展',
type: 'warning',
})
formRef.value?.validate((valid) => {
if (valid) {
// 这里编写业务代码
emits('onRegister', form.value.account)
}
})
}
</script>
<template>
<ElForm ref="formRef" :model="form" :rules="rules" class="min-h-500px w-full flex-col-stretch-center p-12">
<h3 class="mb-8 text-xl color-[var(--el-text-color-primary)] font-bold">
探索从这里开始! 🚀
</h3>
<div>
<ElFormItem prop="account">
<ElInput v-model="form.account" size="large" placeholder="用户名" tabindex="1">
<template #prefix>
<SvgIcon name="i-ri:user-3-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="captcha">
<ElInput v-model="form.captcha" size="large" placeholder="验证码" tabindex="2">
<template #prefix>
<SvgIcon name="i-ic:baseline-verified-user" />
</template>
<template #append>
<ElButton>发送验证码</ElButton>
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="password">
<ElInput v-model="form.password" type="password" size="large" placeholder="密码" tabindex="3" show-password>
<template #prefix>
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="checkPassword">
<ElInput v-model="form.checkPassword" type="password" size="large" placeholder="确认密码" tabindex="4" show-password>
<template #prefix>
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
</div>
<ElButton :loading="loading" type="primary" size="large" style="width: 100%; margin-top: 20px;" @click.prevent="handleRegister">
注册
</ElButton>
<div class="mt-4 flex-center gap-2 text-sm color-[var(--el-text-color-secondary)]">
已经有帐号?
<ElLink type="primary" :underline="false" @click="emits('onLogin', form.account)">
去登录
</ElLink>
</div>
</ElForm>
</template>
@@ -0,0 +1,92 @@
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
defineOptions({
name: 'ResetPasswordForm',
})
const props = defineProps<{
account?: string
}>()
const emits = defineEmits<{
onLogin: [account: string]
onResetPassword: [account: string]
}>()
const loading = ref(false)
const formRef = useTemplateRef<FormInstance>('formRef')
const form = ref({
account: props.account ?? localStorage.login_account ?? '',
captcha: '',
newPassword: '',
})
const rules = ref<FormRules>({
account: [
{ required: true, trigger: 'blur', message: '请输入用户名' },
],
captcha: [
{ required: true, trigger: 'blur', message: '请输入验证码' },
],
newPassword: [
{ required: true, trigger: 'blur', message: '请输入新密码' },
{ min: 6, max: 18, trigger: 'blur', message: '密码长度为6到18位' },
],
})
function handleReset() {
ElMessage({
message: '重置密码仅提供界面演示,无实际功能,需开发者自行扩展',
type: 'info',
})
formRef.value?.validate((valid) => {
if (valid) {
// 这里编写业务代码
emits('onResetPassword', form.value.account)
}
})
}
</script>
<template>
<ElForm ref="formRef" :model="form" :rules="rules" class="min-h-500px w-full flex-col-stretch-center p-12">
<h3 class="mb-8 text-xl color-[var(--el-text-color-primary)] font-bold">
忘记密码了? 🔒
</h3>
<div>
<ElFormItem prop="account">
<ElInput v-model="form.account" size="large" placeholder="用户名" type="text" tabindex="1">
<template #prefix>
<SvgIcon name="i-ri:user-3-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="captcha">
<ElInput v-model="form.captcha" size="large" placeholder="验证码" type="text" tabindex="2">
<template #prefix>
<SvgIcon name="i-ic:baseline-verified-user" />
</template>
<template #append>
<ElButton>发送验证码</ElButton>
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="newPassword">
<ElInput v-model="form.newPassword" type="password" size="large" placeholder="新密码" tabindex="3" show-password>
<template #prefix>
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
</div>
<ElButton :loading="loading" type="primary" size="large" style="width: 100%; margin-top: 20px;" @click.prevent="handleReset">
确认
</ElButton>
<div class="mt-4 flex-center gap-2 text-sm color-[var(--el-text-color-secondary)]">
<ElLink type="primary" :underline="false" @click="emits('onLogin', form.account)">
去登录
</ElLink>
</div>
</ElForm>
</template>
+17 -53
View File
@@ -3,81 +3,45 @@ defineOptions({
name: 'SearchBar',
})
const props = withDefaults(
withDefaults(
defineProps<{
fold?: boolean
showToggle?: boolean
background?: boolean
}>(),
{
fold: true,
showToggle: true,
background: false,
},
)
const emits = defineEmits<{
'update:fold': [
value: boolean,
]
'toggle': [
toggle: [
value: boolean,
]
}>()
const isFold = ref(props.fold)
watch(() => props.fold, (value) => {
isFold.value = value
emits('update:fold', value)
const fold = defineModel<boolean>('fold', {
default: true,
})
function toggle() {
isFold.value = !isFold.value
emits('toggle', isFold.value)
fold.value = !fold.value
emits('toggle', fold.value)
}
</script>
<template>
<div class="search-container" :class="{ 'has-bg': background }">
<slot :fold="isFold" />
<div v-if="showToggle" class="toggle">
<el-button text size="small" @click="toggle">
<template #icon>
<svg-icon :name="isFold ? 'ep:caret-bottom' : 'ep:caret-top' " />
</template>
{{ isFold ? '展开' : '收起' }}
</el-button>
<div
class="relative" :class="{
'py-4': showToggle,
'px-4 bg-[var(--g-bg)] transition': background,
}"
>
<slot :fold="fold" :toggle="toggle" />
<div v-if="showToggle" class="absolute bottom-0 left-0 w-full translate-y-1/2 text-center">
<button class="h-5 inline-flex cursor-pointer select-none items-center border-size-0 rounded bg-[var(--g-bg)] px-2 text-xs font-medium outline-none" @click="toggle">
<SvgIcon :name="fold ? 'i-ep:caret-bottom' : 'i-ep:caret-top' " />
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.search-container {
position: relative;
&.has-bg {
padding: 20px;
background-color: var(--el-fill-color-lighter);
transition: background-color 0.3s;
}
:deep(.el-form) {
margin-bottom: -10px;
.el-select {
width: 100%;
}
.el-date-editor {
width: 100%;
}
}
.toggle {
position: relative;
text-align: center;
margin-bottom: -10px;
}
}
</style>
Regular → Executable
+31 -21
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { UseImage } from '@vueuse/components'
defineOptions({
name: 'SvgIcon',
@@ -13,6 +14,24 @@ const props = defineProps<{
size?: string | number
}>()
const outputType = computed(() => {
const hasPathFeatures = (str: string) => {
return /^\.{1,2}\//.test(str) || str.startsWith('/') || str.includes('/')
}
if (/^https?:\/\//.test(props.name) || hasPathFeatures(props.name) || !props.name) {
return 'img'
}
else if (/i-[^:]+:[^:]+/.test(props.name)) {
return 'unocss'
}
else if (props.name.includes(':')) {
return 'iconify'
}
else {
return 'svg'
}
})
const style = computed(() => {
const transform = []
if (props.flip) {
@@ -41,28 +60,19 @@ const style = computed(() => {
</script>
<template>
<i class="icon" :style="style">
<Icon v-if="name.indexOf('ep:') === 0" :icon="name" />
<svg v-else aria-hidden="true">
<i class="relative h-[1em] w-[1em] flex-inline items-center justify-center fill-current leading-[1em]" :style="style">
<i v-if="outputType === 'unocss'" class="h-[1em] w-[1em]" :class="name" />
<Icon v-else-if="outputType === 'iconify'" :icon="name" />
<svg v-else-if="outputType === 'svg'" class="h-[1em] w-[1em]" aria-hidden="true">
<use :xlink:href="`#icon-${name}`" />
</svg>
<UseImage v-else-if="outputType === 'img'" :src="name" class="h-[1em] w-[1em]">
<template #loading>
<i class="i-line-md:loading-loop h-[1em] w-[1em]" />
</template>
<template #error>
<i class="i-tdesign:image-error h-[1em] w-[1em]" />
</template>
</UseImage>
</i>
</template>
<style lang="scss" scoped>
.icon {
height: 1em;
width: 1em;
line-height: 1em;
display: inline-flex;
justify-content: center;
align-items: center;
position: relative;
fill: currentcolor;
svg {
height: 1em;
width: 1em;
}
}
</style>
+48 -32
View File
@@ -13,36 +13,52 @@ onMounted(() => {
</script>
<template>
<div>
<el-drawer v-model="isShow" title="系统信息" direction="rtl" :size="360">
<el-descriptions direction="vertical" :column="1" border>
<el-descriptions-item label="最后编译时间" align="center">
{{ lastBuildTime }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions title="生产环境依赖" :column="1" size="small" border>
<el-descriptions-item v-for="(val, key) in (pkg.dependencies as object)" :key="key" :label="key">
{{ val }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions title="开发环境依赖" :column="1" size="small" border>
<el-descriptions-item v-for="(val, key) in (pkg.devDependencies as object)" :key="key" :label="key">
{{ val }}
</el-descriptions-item>
</el-descriptions>
</el-drawer>
</div>
<HSlideover v-model="isShow" title="系统信息">
<div v-if="pkg.version" class="px-4">
<h2 class="m-0 text-lg font-bold">
版本号
</h2>
<div class="my-4 text-center text-lg font-sans">
{{ pkg.version }}
</div>
</div>
<div class="px-4">
<h2 class="m-0 text-lg font-bold">
最后编译时间
</h2>
<div class="my-4 text-center text-lg font-sans">
{{ lastBuildTime }}
</div>
</div>
<div class="px-4">
<h2 class="m-0 text-lg font-bold">
生产环境依赖
</h2>
<ul class="list-none pl-0 text-sm">
<li v-for="(val, key) in (pkg.dependencies as object)" :key="key" class="flex items-center justify-between rounded px-2 py-1.5 hover-bg-stone-1 dark-hover-bg-stone-9">
<div class="font-bold">
{{ key }}
</div>
<div class="font-sans">
{{ val }}
</div>
</li>
</ul>
</div>
<div class="px-4">
<h2 class="m-0 text-lg font-bold">
开发环境依赖
</h2>
<ul class="list-none pl-0 text-sm">
<li v-for="(val, key) in (pkg.devDependencies as object)" :key="key" class="flex items-center justify-between rounded px-2 py-1.5 hover-bg-stone-1 dark-hover-bg-stone-9">
<div class="font-bold">
{{ key }}
</div>
<div class="font-sans">
{{ val }}
</div>
</li>
</ul>
</div>
</HSlideover>
</template>
<style lang="scss" scoped>
:deep(.el-drawer__header) {
margin-bottom: initial;
padding-bottom: 20px;
border-bottom: 1px solid var(--el-border-color);
transition: var(--el-transition-border);
}
:deep(.el-descriptions) {
margin-bottom: 20px;
}
</style>
Regular → Executable
+2 -27
View File
@@ -29,35 +29,10 @@ const isUp = computed(() => {
</script>
<template>
<div :class="`trend ${isUp ? 'up' : 'down'}`">
<div class="flex items-center transition" :class="`${isUp ? 'c-green' : 'c-red'}`">
<span v-if="prefix" class="prefix">{{ prefix }}</span>
<span class="text">{{ value }}</span>
<span v-if="suffix" class="suffix">{{ suffix }}</span>
<svg-icon name="ep:caret-top" />
<SvgIcon name="i-ep:caret-top" :rotate="isUp ? 0 : 180" class="ml-1 transition" />
</div>
</template>
<style lang="scss" scoped>
.trend {
display: flex;
align-items: center;
transition: var(--el-transition-color);
&.up {
color: var(--el-color-success);
}
&.down {
color: var(--el-color-danger);
.icon {
transform: rotate(180deg);
}
}
.icon {
margin-left: 5px;
transition: transform 0.3s;
}
}
</style>
-6
View File
@@ -1,6 +0,0 @@
import { addCollection } from '@iconify/vue'
export async function downloadAndInstall() {
const data = Object.freeze(await fetch('./icons/icons.json').then(r => r.json()))
addCollection(data)
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{ "collections": ["ant-design", "ep", "flagpack", "icon-park", "mdi", "ri", "logos", "twemoji", "vscode-icons"], "isOfflineUse": false }
+9
View File
@@ -0,0 +1,9 @@
import { addCollection } from '@iconify/vue'
import data from './data.json'
export async function downloadAndInstall(name: string) {
const data = Object.freeze(await fetch(`./icons/${name}-raw.json`).then(r => r.json()))
addCollection(data)
}
export const icons = data.sort((a, b) => a.info.name.localeCompare(b.info.name))
+325 -376
View File
@@ -1,9 +1,10 @@
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { ElMessage } from 'element-plus'
import eventBus from '@/utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
import settingsDefault from '@/settings.default'
import useMenuStore from '@/store/modules/menu'
import useSettingsStore from '@/store/modules/settings'
import eventBus from '@/utils/eventBus'
import { useClipboard } from '@vueuse/core'
import Message from 'vue-m-message'
defineOptions({
name: 'AppSetting',
@@ -16,7 +17,7 @@ const menuStore = useMenuStore()
const isShow = ref(false)
watch(() => settingsStore.settings.menu.menuMode, (value) => {
watch(() => settingsStore.settings.menu.mode, (value) => {
if (value === 'single') {
menuStore.setActived(0)
}
@@ -35,473 +36,421 @@ const { copy, copied, isSupported } = useClipboard()
watch(copied, (val) => {
if (val) {
ElMessage.success('复制成功,请粘贴到 src/settings.ts 文件中!')
Message.success('复制成功,请粘贴到 src/settings.ts 文件中!', {
zIndex: 2000,
})
}
})
function isObject(value: any) {
return typeof value === 'object' && !Array.isArray(value)
}
// 比较两个对象,并提取出不同的部分
function getObjectDiff(originalObj: Record<string, any>, diffObj: Record<string, any>) {
if (!isObject(originalObj) || !isObject(diffObj)) {
return diffObj
}
const diff: Record<string, any> = {}
for (const key in diffObj) {
const originalValue = originalObj[key]
const diffValue = diffObj[key]
if (JSON.stringify(originalValue) !== JSON.stringify(diffValue)) {
if (isObject(originalValue) && isObject(diffValue)) {
const nestedDiff = getObjectDiff(originalValue, diffValue)
if (Object.keys(nestedDiff).length > 0) {
diff[key] = nestedDiff
}
}
else {
diff[key] = diffValue
}
}
}
return diff
}
function handleCopy() {
copy(JSON.stringify(settingsStore.settings, null, 2))
copy(JSON.stringify(getObjectDiff(settingsDefault, settingsStore.settings), null, 2))
}
</script>
<template>
<div>
<el-drawer v-model="isShow" title="应用配置" direction="rtl" :size="360">
<el-alert title="应用配置可实时预览效果,但只是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,并将配置粘贴到 src/settings.ts 文件中。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
<el-divider>颜色主题</el-divider>
<div class="color-scheme">
<div class="switch" :class="settingsStore.settings.app.colorScheme" @click="settingsStore.settings.app.colorScheme = settingsStore.settings.app.colorScheme === 'dark' ? 'light' : 'dark'">
<svg-icon :name="settingsStore.settings.app.colorScheme === 'light' ? 'ep:sunny' : 'ep:moon'" />
<HSlideover v-model="isShow" title="应用配置">
<div class="rounded-2 bg-rose/20 px-4 py-2 text-sm/6 c-rose">
<p class="my-1">
应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
</p>
<p class="my-1">
注意:在生产环境中应关闭该模块。
</p>
</div>
<div class="divider">
颜色主题风格
</div>
<div class="flex items-center justify-center pb-4">
<HTabList
v-model="settingsStore.settings.app.colorScheme"
:options="[
{ icon: 'i-ri:sun-line', label: '明亮', value: 'light' },
{ icon: 'i-ri:moon-line', label: '暗黑', value: 'dark' },
{ icon: 'i-codicon:color-mode', label: '系统', value: '' },
]"
class="w-60"
/>
</div>
<div v-if="settingsStore.mode === 'pc'" class="divider">
导航栏模式
</div>
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
<HTooltip text="侧边栏模式 (含主导航)" placement="bottom" :delay="500">
<div class="mode mode-side" :class="{ active: settingsStore.settings.menu.mode === 'side' }" @click="settingsStore.settings.menu.mode = 'side'">
<div class="mode-container" />
</div>
</div>
<el-divider v-if="settingsStore.mode === 'pc'">
导航栏模式
</el-divider>
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500">
<div class="mode mode-side" :class="{ active: settingsStore.settings.menu.menuMode === 'side' }" @click="settingsStore.settings.menu.menuMode = 'side'">
<div class="mode-container" />
<svg-icon name="ep:check" />
</div>
</el-tooltip>
<el-tooltip content="顶部模式" placement="top" :show-after="500">
<div class="mode mode-head" :class="{ active: settingsStore.settings.menu.menuMode === 'head' }" @click="settingsStore.settings.menu.menuMode = 'head'">
<div class="mode-container" />
<svg-icon name="ep:check" />
</div>
</el-tooltip>
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
<div class="mode mode-single" :class="{ active: settingsStore.settings.menu.menuMode === 'single' }" @click="settingsStore.settings.menu.menuMode = 'single'">
<div class="mode-container" />
<svg-icon name="ep:check" />
</div>
</el-tooltip>
</div>
<el-divider>导航栏</el-divider>
<div class="setting-item">
<div class="label">
主导航切换跳转
<el-tooltip content="开启该功能后,切换主导航时,页面自动跳转至该主导航下,次导航里第一个导航" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</HTooltip>
<HTooltip text="顶部模式" placement="bottom" :delay="500">
<div class="mode mode-head" :class="{ active: settingsStore.settings.menu.mode === 'head' }" @click="settingsStore.settings.menu.mode = 'head'">
<div class="mode-container" />
</div>
<el-switch v-model="settingsStore.settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
</HTooltip>
<HTooltip text="侧边栏模式 (不含主导航)" placement="bottom" :delay="500">
<div class="mode mode-single" :class="{ active: settingsStore.settings.menu.mode === 'single' }" @click="settingsStore.settings.menu.mode = 'single'">
<div class="mode-container" />
</div>
</HTooltip>
</div>
<div class="divider">
导航栏
</div>
<div class="setting-item">
<div class="label">
主导航切换跳转
<HTooltip text="开启该功能后,切换主导航时,页面自动跳转至该主导航下,次导航里第一个导航">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settingsStore.settings.menu.mode)" />
</div>
<div class="setting-item">
<div class="label">
次导航保持展开一个
<HTooltip text="开启该功能后,次导航只保持单个菜单的展开">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.menu.subMenuUniqueOpened" />
</div>
<div class="setting-item">
<div class="label">
次导航是否折叠
</div>
<HToggle v-model="settingsStore.settings.menu.subMenuCollapse" />
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
显示次导航折叠按钮
</div>
<HToggle v-model="settingsStore.settings.menu.enableSubMenuCollapseButton" />
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<HToggle v-model="settingsStore.settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.settings.menu.mode)" />
</div>
<div class="divider">
顶栏
</div>
<div class="setting-item">
<div class="label">
模式
</div>
<HCheckList
v-model="settingsStore.settings.topbar.mode" :options="[
{ label: '静止', value: 'static' },
{ label: '固定', value: 'fixed' },
{ label: '粘性', value: 'sticky' },
]"
/>
</div>
<div>
<div class="divider">
标签栏
</div>
<div class="setting-item">
<div class="label">
次导航保持展开一个
<el-tooltip content="开启该功能后,次导航只保持单个菜单的展开" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
是否启用
</div>
<el-switch v-model="settingsStore.settings.menu.subMenuUniqueOpened" />
<HToggle v-model="settingsStore.settings.tabbar.enable" />
</div>
<div class="setting-item">
<div class="label">
次导航是否折叠
是否显示图标
</div>
<el-switch v-model="settingsStore.settings.menu.subMenuCollapse" />
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
显示次导航折叠按钮
</div>
<el-switch v-model="settingsStore.settings.menu.enableSubMenuCollapseButton" />
<HToggle v-model="settingsStore.settings.tabbar.enableIcon" :disabled="!settingsStore.settings.tabbar.enable" />
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<el-switch v-model="settingsStore.settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
<HToggle v-model="settingsStore.settings.tabbar.enableHotkeys" :disabled="!settingsStore.settings.tabbar.enable" />
</div>
<el-divider>顶栏</el-divider>
<div class="setting-item">
<div class="label">
模式
</div>
<el-radio-group v-model="settingsStore.settings.topbar.mode" size="small">
<el-radio-button label="static">
静止
</el-radio-button>
<el-radio-button label="fixed">
固定
</el-radio-button>
<el-radio-button label="sticky">
粘性
</el-radio-button>
</el-radio-group>
</div>
<el-divider>工具栏</el-divider>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
全屏
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.toolbar.enableFullscreen" />
</div>
<div class="setting-item">
<div class="label">
页面刷新
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.toolbar.enablePageReload" />
</div>
<div class="setting-item">
<div class="label">
颜色主题
<el-tooltip content="开启后可在明亮/暗黑模式中切换" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.toolbar.enableColorScheme" />
</div>
<el-divider v-if="settingsStore.mode === 'pc'">
</div>
<div class="divider">
工具栏
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
面包屑导航
</el-divider>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
是否启用
</div>
<el-switch v-model="settingsStore.settings.breadcrumb.enable" />
</div>
<el-divider>导航搜索</el-divider>
<div class="setting-item">
<div class="label">
是否启用
<el-tooltip content="对导航进行快捷搜索" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.navSearch.enable" />
<HToggle v-model="settingsStore.settings.toolbar.breadcrumb" />
</div>
<div class="setting-item">
<div class="label">
导航搜索
<HTooltip text="对导航进行快捷搜索">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<el-switch v-model="settingsStore.settings.navSearch.enableHotkeys" :disabled="!settingsStore.settings.navSearch.enable" />
<HToggle v-model="settingsStore.settings.toolbar.navSearch" />
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
全屏
</div>
<el-divider>底部版权</el-divider>
<div class="setting-item">
<div class="label">
是否启用
</div>
<el-switch v-model="settingsStore.settings.copyright.enable" />
<HToggle v-model="settingsStore.settings.toolbar.fullscreen" />
</div>
<div class="setting-item">
<div class="label">
页面刷新
<HTooltip text="使用框架内提供的刷新功能进行页面刷新">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<div class="setting-item">
<div class="label">
日期
</div>
<el-input v-model="settingsStore.settings.copyright.dates" size="small" :disabled="!settingsStore.settings.copyright.enable" />
<HToggle v-model="settingsStore.settings.toolbar.pageReload" />
</div>
<div class="setting-item">
<div class="label">
颜色主题
<HTooltip text="开启后可在明亮/暗黑模式中切换">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<div class="setting-item">
<div class="label">
公司
</div>
<el-input v-model="settingsStore.settings.copyright.company" size="small" :disabled="!settingsStore.settings.copyright.enable" />
<HToggle v-model="settingsStore.settings.toolbar.colorScheme" />
</div>
<div class="divider">
页面
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<div class="setting-item">
<div class="label">
网址
</div>
<el-input v-model="settingsStore.settings.copyright.website" size="small" :disabled="!settingsStore.settings.copyright.enable" />
<HToggle v-model="settingsStore.settings.mainPage.enableHotkeys" />
</div>
<div class="divider">
导航搜索
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<div class="setting-item">
<div class="label">
备案
</div>
<el-input v-model="settingsStore.settings.copyright.beian" size="small" :disabled="!settingsStore.settings.copyright.enable" />
<HToggle v-model="settingsStore.settings.navSearch.enableHotkeys" :disabled="!settingsStore.settings.toolbar.navSearch" />
</div>
<div class="divider">
底部版权
</div>
<div class="setting-item">
<div class="label">
是否启用
</div>
<el-divider>主页</el-divider>
<div class="setting-item">
<div class="label">
是否开启
<el-tooltip content="该功能开启时,登录成功默认进入主页,反之则默认进入导航栏里第一个导航页面" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.home.enable" />
<HToggle v-model="settingsStore.settings.copyright.enable" />
</div>
<div class="setting-item">
<div class="label">
日期
</div>
<div class="setting-item">
<div class="label">
主页名称
</div>
<el-input v-model="settingsStore.settings.home.title" size="small" />
<HInput v-model="settingsStore.settings.copyright.dates" :disabled="!settingsStore.settings.copyright.enable" />
</div>
<div class="setting-item">
<div class="label">
公司
</div>
<el-divider>其它</el-divider>
<div class="setting-item">
<div class="label">
组件尺寸
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-radio-group v-model="settingsStore.settings.app.elementSize" size="small">
<el-radio-button label="large">
较大
</el-radio-button>
<el-radio-button label="default">
默认
</el-radio-button>
<el-radio-button label="small">
稍小
</el-radio-button>
</el-radio-group>
<HInput v-model="settingsStore.settings.copyright.company" :disabled="!settingsStore.settings.copyright.enable" />
</div>
<div class="setting-item">
<div class="label">
网址
</div>
<div class="setting-item">
<div class="label">
是否启用权限
</div>
<el-switch v-model="settingsStore.settings.app.enablePermission" />
<HInput v-model="settingsStore.settings.copyright.website" :disabled="!settingsStore.settings.copyright.enable" />
</div>
<div class="setting-item">
<div class="label">
备案
</div>
<div class="setting-item">
<div class="label">
载入进度条
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.app.enableProgress" />
<HInput v-model="settingsStore.settings.copyright.beian" :disabled="!settingsStore.settings.copyright.enable" />
</div>
<div class="divider">
主页
</div>
<div class="setting-item">
<div class="label">
是否启用
<HTooltip text="该功能开启时,登录成功默认进入主页,反之则默认进入导航栏里第一个导航页面">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<div class="setting-item">
<div class="label">
动态标题
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
<svg-icon name="ep:question-filled" />
</el-tooltip>
</div>
<el-switch v-model="settingsStore.settings.app.enableDynamicTitle" />
<HToggle v-model="settingsStore.settings.home.enable" />
</div>
<div class="setting-item">
<div class="label">
主页名称
<HTooltip text="开启国际化时,该设置无效">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<template v-if="isSupported" #footer>
<el-button type="primary" @click="handleCopy">
<template #icon>
<svg-icon name="ep:document-copy" />
</template>
复制配置
</el-button>
</template>
</el-drawer>
</div>
<HInput v-model="settingsStore.settings.home.title" />
</div>
<div class="divider">
其它
</div>
<div class="setting-item">
<div class="label">
是否启用权限
</div>
<HToggle v-model="settingsStore.settings.app.enablePermission" />
</div>
<div class="setting-item">
<div class="label">
载入进度条
<HTooltip text="该功能开启时,跳转路由会看到页面顶部有进度条">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.app.enableProgress" />
</div>
<div class="setting-item">
<div class="label">
哀悼模式
<HTooltip text="该功能开启时,整站会变为灰色">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.app.enableMournMode" />
</div>
<div class="setting-item">
<div class="label">
色弱模式
</div>
<HToggle v-model="settingsStore.settings.app.enableColorAmblyopiaMode" />
</div>
<div class="setting-item">
<div class="label">
动态标题
<HTooltip text="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.app.enableDynamicTitle" />
</div>
<template v-if="isSupported" #footer>
<HButton block @click="handleCopy">
<SvgIcon name="i-ep:document-copy" />
复制配置
</HButton>
</template>
</HSlideover>
</template>
<style lang="scss" scoped>
:deep(.el-drawer__header) {
margin-bottom: initial;
padding-bottom: 20px;
border-bottom: 1px solid var(--el-border-color);
}
<style scoped>
.divider {
--uno: flex items-center justify-between gap-4 my-4 whitespace-nowrap text-sm font-500;
:deep(.el-drawer__footer) {
padding: 20px;
border-top: 1px solid var(--el-border-color);
transition: var(--el-transition-border);
.el-button {
width: 100%;
}
}
:deep(.el-divider) {
margin: 36px 0 24px;
}
.color-scheme {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 10px;
$width: 50px;
.switch {
width: $width;
height: 30px;
border-radius: 15px;
cursor: pointer;
background-color: var(--el-fill-color-darker);
transition: background-color 0.3s;
&.dark {
.icon {
margin-left: calc($width - 24px - 3px);
}
}
.icon {
margin: 3px;
padding: 5px;
font-size: 24px;
border-radius: 50%;
background-color: var(--el-fill-color-lighter);
transition: margin-left 0.3s, background-color 0.3s;
}
&::before,
&::after {
--uno: content-empty w-full h-1px bg-stone-2 dark-bg-stone-6;
}
}
.menu-mode {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding-bottom: 10px;
--uno: flex items-center justify-center gap-4 pb-4;
.mode {
position: relative;
width: 80px;
height: 55px;
margin: 10px;
border-radius: 5px;
overflow: hidden;
cursor: pointer;
background-color: var(--g-app-bg);
box-shadow: 0 0 5px 1px var(--el-border-color-lighter);
transition: 0.2s;
&:hover {
box-shadow: 0 0 5px 1px var(--el-border-color-darker);
}
--uno: relative w-16 h-12 rounded-2 ring-1 ring-stone-2 dark-ring-stone-7 cursor-pointer transition;
&.active {
box-shadow: 0 0 0 2px var(--el-color-primary);
--uno: ring-ui-primary ring-2;
}
&::before,
&::after,
.mode-container {
pointer-events: none;
position: absolute;
border-radius: 3px;
--uno: absolute pointer-events-none;
}
.mode-container::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: var(--g-sub-sidebar-menu-active-bg);
opacity: 0.2;
&::before {
--uno: content-empty bg-ui-primary;
}
&::after {
--uno: content-empty bg-ui-primary/60;
}
.mode-container {
--uno: bg-ui-primary/20 border-width-1.5 border-dashed border-ui-primary;
&::before {
--uno: content-empty absolute w-full h-full;
}
}
&-side {
&::before {
content: "";
top: 5px;
left: 5px;
bottom: 5px;
width: 10px;
background-color: var(--g-sub-sidebar-menu-active-bg);
--uno: top-2 bottom-2 left-2 w-2 rounded-tl-1 rounded-bl-1;
}
&::after {
content: "";
top: 5px;
left: 20px;
bottom: 5px;
width: 15px;
background-color: var(--g-sub-sidebar-menu-active-bg);
opacity: 0.5;
--uno: top-2 bottom-2 left-4.5 w-3;
}
.mode-container {
inset: 5px 5px 5px 40px;
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
--uno: inset-t-2 inset-r-2 inset-b-2 inset-l-8 rounded-tr-1 rounded-br-1;
}
}
&-head {
&::before {
content: "";
top: 5px;
left: 5px;
right: 5px;
height: 10px;
background-color: var(--g-sub-sidebar-menu-active-bg);
--uno: top-2 left-2 right-2 h-2 rounded-tl-1 rounded-tr-1;
}
&::after {
content: "";
top: 20px;
left: 5px;
bottom: 5px;
width: 15px;
background-color: var(--g-sub-sidebar-menu-active-bg);
opacity: 0.5;
--uno: top-4.5 left-2 bottom-2 w-3 rounded-bl-1;
}
.mode-container {
inset: 20px 5px 5px 25px;
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
--uno: inset-t-4.5 inset-r-2 inset-b-2 inset-l-5.5 rounded-br-1;
}
}
&-single {
&::before {
content: "";
position: absolute;
top: 5px;
left: 5px;
bottom: 5px;
width: 15px;
background-color: var(--g-sub-sidebar-menu-active-bg);
opacity: 0.5;
&::after {
--uno: top-2 left-2 bottom-2 w-3 rounded-tl-1 rounded-bl-1;
}
.mode-container {
inset: 5px 5px 5px 25px;
border: 1px dashed var(--g-sub-sidebar-menu-active-bg);
--uno: inset-t-2 inset-r-2 inset-b-2 inset-l-5.5 rounded-tr-1 rounded-br-1;
}
}
i {
position: absolute;
right: 10px;
bottom: 10px;
display: none;
}
&.active i {
display: block;
color: var(--el-color-primary);
}
}
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin: 5px 0;
padding: 5px 10px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background: var(--el-fill-color);
}
--uno: flex items-center justify-between gap-4 px-4 py-2 rounded-2 transition hover-bg-stone-1 dark-hover-bg-stone-9;
.label {
font-size: 14px;
color: var(--el-text-color-regular);
display: flex;
align-items: center;
--uno: flex items-center flex-shrink-0 gap-2 text-sm;
i {
margin-left: 4px;
font-size: 17px;
color: var(--el-color-warning);
cursor: help;
--uno: text-xl text-orange cursor-help;
}
}
.el-switch {
height: auto;
}
.el-input {
width: 150px;
}
}
</style>
+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
defineOptions({
name: 'BackTop',
})
const transitionClass = {
enterActiveClass: 'ease-out duration-300',
enterFromClass: 'opacity-0 translate-y-4 lg-translate-y-0 lg-scale-95',
enterToClass: 'opacity-100 translate-y-0 lg-scale-100',
leaveActiveClass: 'ease-in duration-200',
leaveFromClass: 'opacity-100 translate-y-0 lg-scale-100',
leaveToClass: 'opacity-0 translate-y-4 lg-translate-y-0 lg-scale-95',
}
onMounted(() => {
window.addEventListener('scroll', handleScroll)
handleScroll()
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
})
const scrollTop = ref<number | null>(null)
function handleScroll() {
scrollTop.value = document.documentElement.scrollTop
}
function handleClick() {
document.documentElement.scrollTo({
top: 0,
behavior: 'smooth',
})
}
</script>
<template>
<Teleport to="body">
<Transition v-bind="transitionClass">
<div v-if="scrollTop && scrollTop >= 200" class="fixed bottom-4 right-4 z-1000 h-12 w-12 flex cursor-pointer items-center justify-center rounded-full bg-white shadow-lg ring-1 ring-stone-3 ring-inset dark-bg-dark hover-bg-stone-1 dark-ring-stone-7 dark-hover-bg-dark/50" @click="handleClick">
<SvgIcon name="i-icon-park-outline:to-top-one" :size="24" />
</div>
</Transition>
</Teleport>
</template>
@@ -0,0 +1,21 @@
<template>
<div class="flex items-center text-sm">
<slot />
</div>
</template>
<style scoped>
:deep(.breadcrumb-item) {
&:first-child {
.separator {
display: none;
}
}
&:last-child {
.text {
opacity: 1;
}
}
}
</style>
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { RouteLocationRaw } from 'vue-router'
const props = withDefaults(
defineProps<{
to?: RouteLocationRaw
replace?: boolean
separator?: string
}>(),
{
separator: '/',
},
)
const router = useRouter()
function onClick() {
if (props.to) {
props.replace ? router.replace(props.to) : router.push(props.to)
}
}
</script>
<template>
<div class="breadcrumb-item flex items-center text-dark dark-text-white">
<span class="separator mx-2">
{{ separator }}
</span>
<span
class="text flex items-center opacity-60"
:class="{
'is-link cursor-pointer transition-opacity hover-opacity-100': !!props.to,
}" @click="onClick"
>
<slot />
</span>
</div>
</template>
+7 -24
View File
@@ -5,14 +5,13 @@ defineOptions({
name: 'Copyright',
})
const route = useRoute()
const settingsStore = useSettingsStore()
</script>
<template>
<footer v-if="route.meta.copyright ?? settingsStore.settings.copyright.enable" class="copyright">
<footer v-if="settingsStore.settings.copyright.enable" class="copyright">
<span>Copyright</span>
<span class="icon">©</span>
<SvgIcon name="i-ri:copyright-line" :size="18" />
<span v-if="settingsStore.settings.copyright.dates">{{ settingsStore.settings.copyright.dates }}</span>
<template v-if="settingsStore.settings.copyright.company">
<a v-if="settingsStore.settings.copyright.website" :href="settingsStore.settings.copyright.website" target="_blank" rel="noopener">{{ settingsStore.settings.copyright.company }}</a>
@@ -22,33 +21,17 @@ const settingsStore = useSettingsStore()
</footer>
</template>
<style lang="scss" scoped>
footer {
display: flex;
align-items: center;
justify-content: center;
margin: 40px 0 20px;
color: var(--el-text-color-secondary);
font-size: 14px;
<style scoped>
.copyright {
--uno: flex items-center justify-center flex-wrap my-4 px-4 text-sm text-stone-5;
span,
a {
padding: 0 5px;
--uno: px-1;
}
a {
text-decoration: none;
color: var(--el-text-color-secondary);
transition: var(--el-transition-color);
&:hover {
color: var(--el-text-color-primary);
}
}
.icon {
padding: 0;
font-size: 18px;
--uno: text-center no-underline text-stone-5 hover-text-dark dark-hover-text-light transition;
}
}
</style>
+86 -123
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import Logo from '../Logo/index.vue'
import Tools from '../Tools/index.vue'
import useSettingsStore from '@/store/modules/settings'
import useMenuStore from '@/store/modules/menu'
import useSettingsStore from '@/store/modules/settings'
import Logo from '../Logo/index.vue'
import ToolbarRightSide from '../Topbar/Toolbar/rightSide.vue'
defineOptions({
name: 'LayoutHeader',
@@ -13,174 +13,137 @@ const menuStore = useMenuStore()
const { switchTo } = useMenu()
const navRef = ref()
const menuRef = useTemplateRef('menuRef')
// 顶部模式鼠标滚动
function handlerMouserScroll(event: WheelEvent) {
navRef.value.scrollBy({
left: (event.deltaY || event.detail) > 0 ? 50 : -50,
})
if (event.deltaY || event.detail !== 0) {
menuRef.value?.scrollBy({
left: (event.deltaY || event.detail) > 0 ? 50 : -50,
})
}
}
</script>
<template>
<transition name="header">
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.menuMode === 'head'">
<Transition name="header">
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.mode === 'head'">
<div class="header-container">
<div class="main">
<Logo />
<Logo class="title" />
<div ref="menuRef" class="menu-container scrollbar-none" @wheel.prevent="handlerMouserScroll">
<!-- 顶部模式 -->
<div ref="navRef" class="nav" @wheel.prevent="handlerMouserScroll">
<div class="menu h-full flex of-hidden transition-all">
<template v-for="(item, index) in menuStore.allMenus" :key="index">
<div v-if="item.children && item.children.length !== 0" class="item-container" :class="{ active: index === menuStore.actived }">
<div class="item" @click="switchTo(index)">
<svg-icon v-if="item.meta?.icon" :name="item.meta.icon" />
<span v-if="item.meta?.title">{{ item.meta.title }}</span>
<div
class="menu-item relative mx-1 py-2 transition-all" :class="{
active: index === menuStore.actived,
}"
>
<div
v-if="item.children && item.children.length !== 0" class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 px-3 text-[var(--g-header-menu-color)] transition-all hover-(bg-[var(--g-header-menu-hover-bg)] text-[var(--g-header-menu-hover-color)])" :class="{
'text-[var(--g-header-menu-active-color)]! bg-[var(--g-header-menu-active-bg)]!': index === menuStore.actived,
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
>
<div class="inline-flex flex-1 items-center justify-center gap-1">
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" class="menu-item-container-icon transition-transform group-hover-scale-120" />
<span class="w-full flex-1 truncate text-sm transition-height transition-opacity transition-width">
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
</span>
</div>
</div>
</div>
</template>
</div>
</div>
<Tools />
<ToolbarRightSide />
</div>
</header>
</transition>
</Transition>
</template>
<style lang="scss" scoped>
<style scoped>
header {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
right: 0;
left: 0;
z-index: 2000;
display: flex;
align-items: center;
padding: 0 20px;
width: 100%;
height: var(--g-header-height);
padding: 0 20px;
margin: 0 auto;
color: var(--g-header-color);
background-color: var(--g-header-bg);
transition: background-color 0.3s, var(--el-transition-color);
box-shadow: -1px 0 0 0 var(--g-border-color), 1px 0 0 0 var(--g-border-color), 0 1px 0 0 var(--g-border-color);
transition: background-color 0.3s;
.header-container {
width: var(--g-header-width);
height: 100%;
margin: 0 auto;
display: flex;
gap: 30px;
align-items: center;
justify-content: space-between;
.main {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
height: 100%;
}
}
@media screen and (max-width: var(--g-header-width)) {
.header-container {
width: 100%;
}
}
:deep(.title) {
position: relative;
width: inherit;
height: inherit;
padding: inherit;
background-color: inherit;
.logo {
width: 50px;
height: 50px;
}
span {
font-size: 24px;
letter-spacing: 1px;
color: var(--g-header-color);
}
}
.nav {
flex: 1;
display: flex;
width: 0;
width: 100%;
height: 100%;
margin: 0 30px;
padding: 0 20px;
overflow-x: auto;
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
margin: 0 auto;
// firefox隐藏滚动条
scrollbar-width: none;
// chrome隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
.item-container {
:deep(a.title) {
position: relative;
display: flex;
width: initial;
flex: 0;
width: inherit;
height: inherit;
padding: inherit;
background-color: inherit;
.item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 0 5px;
width: 80px;
.logo {
width: initial;
height: 40px;
}
span {
font-size: 20px;
color: var(--g-header-color);
letter-spacing: 1px;
}
}
.menu-container {
flex: 1;
height: 100%;
padding: 0 20px;
overflow-x: auto;
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
.menu {
display: inline-flex;
height: 100%;
cursor: pointer;
color: var(--g-header-menu-color);
background-color: var(--g-header-bg);
transition: background-color 0.3s, var(--el-transition-color);
&:hover {
color: var(--g-header-menu-hover-color);
background-color: var(--g-header-menu-hover-bg);
}
:deep(.menu-item) {
.menu-item-container {
color: var(--g-header-menu-color);
.icon {
font-size: 24px;
vertical-align: middle;
}
&:hover {
color: var(--g-header-menu-hover-color);
background-color: var(--g-header-menu-hover-bg);
}
span {
text-align: center;
vertical-align: middle;
word-break: break-all;
.menu-item-container-icon {
font-size: 20px !important;
}
}
@include text-overflow(1, false);
&.active .menu-item-container {
color: var(--g-header-menu-active-color);
background-color: var(--g-header-menu-active-bg);
}
}
}
&.active .item {
color: var(--g-header-menu-active-color);
background-color: var(--g-header-menu-active-bg);
}
}
}
:deep(.tools) {
padding: 0;
.buttons .item .icon {
color: var(--g-header-color);
}
.user-container {
font-size: 16px;
color: var(--g-header-color);
}
}
}
// 头部动画
/* 头部动画 */
.header-enter-active,
.header-leave-active {
transition: transform 0.3s;
+68 -35
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import eventBus from '@/utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
import eventBus from '@/utils/eventBus'
defineOptions({
name: 'HotkeysIntro',
@@ -18,38 +18,71 @@ onMounted(() => {
</script>
<template>
<div>
<el-drawer v-model="isShow" title="快捷键介绍" direction="rtl" :size="360">
<el-descriptions title="全局" :column="1" border>
<el-descriptions-item label="查看系统信息">
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} + I
</el-descriptions-item>
<el-descriptions-item v-if="settingsStore.settings.navSearch.enable && settingsStore.settings.navSearch.enableHotkeys" label="唤起导航搜索">
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} + S
</el-descriptions-item>
</el-descriptions>
<el-descriptions v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.menuMode)" title="主导航" :column="1" border>
<el-descriptions-item label="激活下一个主导航">
{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} + `
</el-descriptions-item>
</el-descriptions>
</el-drawer>
</div>
<HDialog v-model="isShow" title="快捷键介绍">
<div class="px-4">
<div class="grid gap-2 sm-grid-cols-2">
<div>
<h2 class="m-0 text-lg font-bold">
全局
</h2>
<ul class="list-none pl-4 text-sm">
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>I</HKbd>
查看系统信息
</li>
<li v-if="settingsStore.settings.toolbar.navSearch && settingsStore.settings.navSearch.enableHotkeys" class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>S</HKbd>
唤起导航搜索
</li>
</ul>
</div>
<div v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.mode)">
<h2 class="m-0 text-lg font-bold">
主导航
</h2>
<ul class="list-none pl-4 text-sm">
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>`</HKbd>
激活下一个主导航
</li>
</ul>
</div>
<div v-if="settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys">
<h2 class="m-0 text-lg font-bold">
标签栏
</h2>
<ul class="list-none pl-4 text-sm">
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>←</HKbd>
切换到上一个标签页
</li>
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>→</HKbd>
切换到下一个标签页
</li>
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>W</HKbd>
关闭当前标签页
</li>
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>1~9</HKbd>
切换到第 n 个标签页
</li>
<li class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>0</HKbd>
切换到最后一个标签页
</li>
</ul>
</div>
</div>
</div>
</HDialog>
</template>
<style lang="scss" scoped>
:deep(.el-drawer__header) {
margin-bottom: initial;
padding-bottom: 20px;
border-bottom: 1px solid var(--el-border-color);
transition: var(--el-transition-border);
}
:deep(.el-descriptions) {
margin-bottom: 20px;
.el-descriptions__label {
width: 200px;
}
}
</style>
+5 -52
View File
@@ -22,59 +22,12 @@ const settingsStore = useSettingsStore()
const title = ref(import.meta.env.VITE_APP_TITLE)
const logo = ref(imgLogo)
const to = computed(() => {
const rtn: {
name?: string
} = {}
if (settingsStore.settings.home.enable) {
rtn.name = 'home'
}
return rtn
})
const to = computed(() => settingsStore.settings.home.enable ? settingsStore.settings.home.fullPath : '')
</script>
<template>
<router-link :to="to" class="title" :class="{ 'is-link': settingsStore.settings.home.enable }" :title="title">
<img v-if="showLogo" :src="logo" class="logo">
<span v-if="showTitle">{{ title }}</span>
</router-link>
<RouterLink :to="to" class="h-[var(--g-sidebar-logo-height)] w-inherit flex-center gap-2 px-3 text-inherit no-underline" :class="{ 'cursor-pointer': settingsStore.settings.home.enable }" :title="title">
<img v-if="showLogo" :src="logo" class="logo h-[30px] w-[30px] object-contain">
<span v-if="showTitle" class="block truncate font-bold">{{ title }}</span>
</RouterLink>
</template>
<style lang="scss" scoped>
.title {
position: fixed;
z-index: 1000;
top: 0;
width: inherit;
padding: 0 10px;
display: flex;
align-items: center;
justify-content: center;
height: var(--g-sidebar-logo-height);
text-align: center;
overflow: hidden;
text-decoration: none;
&.is-link {
cursor: pointer;
}
.logo {
width: 30px;
height: 30px;
object-fit: contain;
& + span {
margin-left: 10px;
}
}
span {
display: block;
font-weight: bold;
color: #fff;
@include text-overflow;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More