Compare commits

...
87 Commits
Author SHA1 Message Date
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
111 changed files with 5919 additions and 4196 deletions
+1 -2
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
},
+4
View File
@@ -1,3 +1,5 @@
# 应用配置面板
VITE_APP_SETTING = true
# 页面标题
VITE_APP_TITLE = Fantastic-admin 基础版
# 接口请求地址,会设置到 axios 的 baseURL 参数上
@@ -7,3 +9,5 @@ VITE_APP_DEBUG_TOOL =
# 是否开启代理
VITE_OPEN_PROXY = false
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
+4
View File
@@ -1,3 +1,5 @@
# 应用配置面板
VITE_APP_SETTING = false
# 页面标题
VITE_APP_TITLE = 页面标题
# 接口请求地址,会设置到 axios 的 baseURL 参数上
@@ -11,3 +13,5 @@ VITE_BUILD_MOCK = false
VITE_BUILD_SOURCEMAP = false
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip,brotli
# 是否在打包后生成存档,支持 zip 和 tar
VITE_BUILD_ARCHIVE =
+4
View File
@@ -1,3 +1,5 @@
# 应用配置面板
VITE_APP_SETTING = false
# 页面标题
VITE_APP_TITLE = 页面标题(test)
# 接口请求地址,会设置到 axios 的 baseURL 参数上
@@ -11,3 +13,5 @@ VITE_BUILD_MOCK = true
VITE_BUILD_SOURCEMAP = true
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS =
# 是否在打包后生成存档,支持 zip 和 tar
VITE_BUILD_ARCHIVE =
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
with:
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,4 +1,4 @@
{
"*.{ts,tsx,vue}": "eslint . --cache --fix",
"*.{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"
}
-4
View File
@@ -1,4 +0,0 @@
dist
node_modules
src/assets/sprites
public/tinymce
-45
View File
@@ -1,45 +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,
"function-no-unknown": [
true,
{
"ignoreFunctions": [
"v-bind",
"map-get",
"lighten",
"darken"
]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": [
"/^view-transition/"
]
}
],
"scss/double-slash-comment-empty-line-before": null,
"scss/no-global-function-names": null
}
}
+1
View File
@@ -5,6 +5,7 @@
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"antfu.unocss"
]
}
+3 -3
View File
@@ -3,9 +3,9 @@
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true,
"source.organizeImports": false
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit",
"source.organizeImports": "never"
},
"stylelint.validate": [
"css",
+27 -22
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://fantastic-admin.gitee.io/logo.png" width="200" height="200" />
<img src="https://fantastic-admin.github.io/logo.png" width="200" height="200" />
</p>
<h1 align="center">Fantastic-admin</h1>
@@ -7,16 +7,16 @@
<p align="center">一款<b>开箱即用</b>的 Vue3 中后台管理系统框架</p>
<p align="center">
<a href="https://fantastic-admin.gitee.io" target="_blank">官网</a>
<sup><a href="https://fantastic-admin.github.io" target="_blank">备用地址</a></sup>
<a href="https://fantastic-admin.github.io" target="_blank">官网</a>
<sup><a href="https://fantastic-admin.gitee.io" target="_blank">备用地址</a></sup>
<span>&nbsp;|&nbsp;</span>
<a href="https://fantastic-admin.gitee.io/support.html" target="_blank">技术支持</a>
<a href="https://fantastic-admin.github.io/support.html" target="_blank">技术支持</a>
<span>&nbsp;|&nbsp;</span>
演示
<sup><a href="https://fantastic-admin.gitee.io/basic-example" target="_blank">基础版</a> <a href="https://fantastic-admin.gitee.io/pro-example" target="_blank">专业版</a></sup>
<sup><a href="https://fantastic-admin.github.io/basic-example" target="_blank">基础版</a> <a href="https://fantastic-admin.github.io/pro-example" target="_blank">专业版</a></sup>
<span>&nbsp;|&nbsp;</span>
购买
<sup><a href="https://fantastic-admin.gitee.io/buy.html" target="_blank">专业版</a></sup>
<sup><a href="https://fantastic-admin.github.io/buy.html" target="_blank">专业版</a></sup>
<p>
<p align="center">
@@ -47,21 +47,20 @@
- `vue2` Vue2 版本框架源码分支,不含示例代码,可直接用于实际开发(停止维护)
- `vue2-example` Vue2 版本演示源码分支,包含大量示例,可用于参考学习(停止维护)
## 预览
> 预览截图为 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.github.io/preview1.png" /></td>
<td><img src="https://fantastic-admin.github.io/preview2.png" /></td>
<td><img src="https://fantastic-admin.github.io/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.github.io/preview4.png" /></td>
<td><img src="https://fantastic-admin.github.io/preview5.png" /></td>
<td><img src="https://fantastic-admin.github.io/preview6.png" /></td>
</tr>
</table>
@@ -73,17 +72,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">
一款开箱即用的 Vue3 项目启动套件
一款简单好用的 Vue3 项目启动套件
</th>
</tr>
</table>
@@ -91,7 +96,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.github.io" target="_blank">One-step-admin</a>
</th>
</tr>
<tr>
@@ -100,13 +105,13 @@
</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.github.io/preview1.png" /></td>
<td><img src="https://one-step-admin.github.io/preview2.png" /></td>
<td><img src="https://one-step-admin.github.io/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.github.io/preview4.png" /></td>
<td><img src="https://one-step-admin.github.io/preview5.png" /></td>
<td><img src="https://one-step-admin.github.io/preview6.png" /></td>
</tr>
</table>
+2 -1
View File
@@ -2,6 +2,7 @@ import antfu from '@antfu/eslint-config'
export default antfu(
{
unocss: true,
ignores: [
'public',
'dist*',
@@ -19,7 +20,7 @@ export default antfu(
'src/**/*.vue',
],
rules: {
'vue/component-tags-order': ['error', {
'vue/block-order': ['error', {
order: ['route', 'script', 'template', 'style'],
}],
},
+59 -51
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "4.0.0",
"version": "4.6.1",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
@@ -13,83 +13,91 @@
"lint": "npm-run-all -s lint:tsc lint:eslint lint:stylelint",
"lint:tsc": "vue-tsc",
"lint:eslint": "eslint . --cache --fix",
"lint:stylelint": "stylelint src/**/*.{css,scss,vue} --cache --fix --allow-empty-input",
"lint:stylelint": "stylelint \"src/**/*.{css,scss,vue}\" --cache --fix",
"postinstall": "simple-git-hooks",
"preinstall": "npx only-allow pnpm",
"commit": "git cz",
"release": "bumpp"
},
"dependencies": {
"@headlessui/vue": "^1.7.16",
"@vueuse/core": "^10.5.0",
"@vueuse/integrations": "^10.5.0",
"axios": "^1.5.1",
"@headlessui/vue": "^1.7.19",
"@imengyu/vue3-context-menu": "^1.3.8",
"@vueuse/core": "^10.8.0",
"@vueuse/integrations": "^10.8.0",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"defu": "^6.1.3",
"element-plus": "^2.4.1",
"defu": "^6.1.4",
"element-plus": "^2.5.6",
"eruda": "^3.0.1",
"floating-vue": "2.0.0-beta.24",
"hotkeys-js": "^3.12.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-vue": "^0.5.6",
"overlayscrollbars": "^2.5.0",
"overlayscrollbars-vue": "^0.5.7",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.1",
"pinia": "^2.1.7",
"qs": "^6.11.2",
"scule": "^1.0.0",
"scule": "^1.3.0",
"vconsole": "^3.15.1",
"vue": "^3.3.7",
"vue": "^3.4.20",
"vue-m-message": "^4.0.2",
"vue-router": "^4.2.5"
"vue-router": "^4.3.0"
},
"devDependencies": {
"@antfu/eslint-config": "1.0.0-beta.29",
"@iconify/json": "^2.2.133",
"@antfu/eslint-config": "2.6.4",
"@iconify/json": "^2.2.186",
"@iconify/vue": "^4.1.1",
"@types/lodash-es": "^4.17.10",
"@types/nprogress": "^0.2.2",
"@types/path-browserify": "^1.0.1",
"@types/qs": "^6.9.9",
"@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue": "^4.4.0",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"autoprefixer": "^10.4.16",
"bumpp": "^9.2.0",
"cz-git": "^1.7.1",
"eslint": "^8.52.0",
"esno": "^0.17.0",
"fs-extra": "^11.1.1",
"@stylistic/stylelint-config": "^1.0.1",
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "^1.0.10",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.2",
"@types/qs": "^6.9.11",
"@unocss/eslint-plugin": "^0.58.5",
"@vitejs/plugin-legacy": "^5.3.1",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"archiver": "^6.0.1",
"autoprefixer": "^10.4.17",
"boxen": "^7.1.1",
"bumpp": "^9.3.0",
"cz-git": "^1.8.0",
"eslint": "^8.57.0",
"esno": "^4.0.0",
"fs-extra": "^11.2.0",
"http-server": "^14.1.1",
"inquirer": "^9.2.11",
"lint-staged": "^15.0.2",
"inquirer": "^9.2.15",
"lint-staged": "^15.2.2",
"npm-run-all": "^4.1.5",
"plop": "^4.0.0",
"postcss-html": "^1.5.0",
"sass": "^1.69.5",
"picocolors": "^1.0.0",
"plop": "^4.0.1",
"sass": "^1.71.1",
"simple-git-hooks": "^2.9.0",
"stylelint": "^15.11.0",
"stylelint-config-standard-scss": "^11.0.0",
"stylelint": "^16.2.1",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-standard-scss": "^13.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-scss": "^5.2.1",
"stylelint-stylistic": "^0.4.3",
"svgo": "^3.0.2",
"terser": "^5.22.0",
"typescript": "^5.2.2",
"unocss": "^0.57.1",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"stylelint-scss": "^6.1.0",
"svgo": "^3.2.0",
"terser": "^5.28.1",
"typescript": "^5.3.3",
"unocss": "^0.58.5",
"unplugin-auto-import": "^0.17.5",
"unplugin-turbo-console": "^1.4.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.4",
"vite-plugin-banner": "^0.7.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.8",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-compression2": "^0.12.0",
"vite-plugin-fake-server": "^2.1.1",
"vite-plugin-pages": "^0.32.0",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-inspector": "^4.0.0",
"vite-plugin-vue-meta-layouts": "^0.3.1",
"vue-tsc": "^1.8.21"
"vite-plugin-vue-devtools": "^7.0.15",
"vite-plugin-vue-meta-layouts": "^0.4.1",
"vue-tsc": "^1.8.27"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
+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: [
{
+3 -3
View File
@@ -1,5 +1,5 @@
const path = require('node:path')
const fs = require('node:fs')
import path from 'node:path'
import fs from 'node:fs'
function getFolder(path) {
const components = []
@@ -14,7 +14,7 @@ function getFolder(path) {
return components
}
module.exports = {
export default {
description: '创建标准模块 Mock',
prompts: [
{
+3 -3
View File
@@ -1,5 +1,5 @@
const path = require('node:path')
const fs = require('node:fs')
import path from 'node:path'
import fs from 'node:fs'
function getFolder(path) {
const components = []
@@ -14,7 +14,7 @@ function getFolder(path) {
return components
}
module.exports = {
export default {
description: '创建页面',
prompts: [
{
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = {
export default {
description: '创建全局状态',
prompts: [
{
+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)
}
}
}
+3728 -3073
View File
File diff suppressed because it is too large Load Diff
+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;
}
+15 -15
View File
@@ -4,43 +4,43 @@
.fantastic-admin-home {
position: absolute;
z-index: 10000;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 10000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-direction: column;
user-select: none;
width: 100%;
height: 100%;
color: #736477;
user-select: none;
background-color: snow;
}
.fantastic-admin-home .loading {
height: 40px;
width: 40px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
flex-wrap: wrap;
width: 40px;
height: 40px;
}
.fantastic-admin-home .loading .square {
display: flex;
align-items: center;
justify-content: center;
height: 20px;
width: 20px;
height: 20px;
}
.fantastic-admin-home .loading .square::before {
content: "";
width: 10px;
height: 10px;
border-radius: 15%;
content: "";
border: 3px solid #8c858f;
border-radius: 15%;
animation: square-to-dot-animation 2s linear infinite;
}
@@ -63,30 +63,30 @@
@keyframes square-to-dot-animation {
15%,
25% {
border-radius: 100%;
width: 0;
height: 0;
margin: 5px;
border-width: 5px;
border-radius: 100%;
}
40% {
border-radius: 15%;
width: 10px;
height: 10px;
margin: initial;
border-width: 3px;
border-radius: 15%;
}
}
.fantastic-admin-home .text {
position: relative;
font-size: 24px;
margin-top: 20px;
font-size: 24px;
}
.fantastic-admin-home .text::after {
content: "…";
position: absolute;
padding-left: 5px;
content: "…";
}
Executable → Regular
+3
View File
@@ -1,5 +1,6 @@
import path from 'node:path'
import process from 'node:process'
import { exec } from 'node:child_process'
import fs from 'fs-extra'
import inquirer from 'inquirer'
import { lookupCollection, lookupCollections } from '@iconify/json'
@@ -74,6 +75,8 @@ async function generateIcons() {
path.resolve(process.cwd(), 'src/iconify/data.json'),
collectionsMeta,
)
exec('eslint src/iconify/data.json src/iconify/index.json --cache --fix')
})
}
+3 -14
View File
@@ -2,13 +2,11 @@
import eruda from 'eruda'
import VConsole from 'vconsole'
import hotkeys from 'hotkeys-js'
import elementPlusLocaleZhCN from 'element-plus/es/locale/lang/zh-cn.mjs'
import eventBus from './utils/eventBus'
import Provider from './ui-provider/index.vue'
import useSettingsStore from '@/store/modules/settings'
import useMenuStore from '@/store/modules/menu'
const settingsStore = useSettingsStore()
const menuStore = useMenuStore()
const { auth } = useAuth()
// 侧边栏主导航当前实际宽度
@@ -26,15 +24,6 @@ const subSidebarActualWidth = computed(() => {
if (settingsStore.settings.menu.subMenuCollapse && settingsStore.mode !== 'mobile') {
actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-collapse-width'))
}
if (
menuStore.sidebarMenus.length === 1
&& (
!menuStore.sidebarMenus[0].children
|| menuStore.sidebarMenus[0]?.children.every(item => item.meta?.sidebar === false)
)
) {
actualWidth = 0
}
return `${actualWidth}px`
})
@@ -70,7 +59,7 @@ import.meta.env.VITE_APP_DEBUG_TOOL === 'vconsole' && new VConsole()
</script>
<template>
<ElConfigProvider :locale="elementPlusLocaleZhCN" :button="{ autoInsertSpace: true }">
<Provider>
<RouterView
v-slot="{ Component, route }"
:style="{
@@ -82,5 +71,5 @@ import.meta.env.VITE_APP_DEBUG_TOOL === 'vconsole' && new VConsole()
<NotAllowed v-else />
</RouterView>
<SystemInfo />
</ElConfigProvider>
</Provider>
</template>
+7 -5
View File
@@ -1,13 +1,15 @@
// 页面布局 CSS 变量
:root {
// 头部高度
--g-header-height: 80px;
--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;
}
@@ -17,8 +19,8 @@
:root {
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
animation: none;
}
&::view-transition-old(root) {
@@ -70,10 +72,10 @@ body {
}
body {
margin: 0;
background-color: var(--g-container-bg);
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 {
@@ -88,8 +90,8 @@ body {
// 右侧内容区针对fixed元素,有横向铺满的需求,可在fixed元素上设置 [data-fixed-calc-width]
[data-fixed-calc-width] {
position: fixed;
left: 50%;
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));
}
+8 -8
View File
@@ -3,48 +3,48 @@
.bar {
position: fixed;
z-index: 1031;
top: 0;
left: 0;
z-index: 2000;
width: 100%;
height: 2px;
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 rgb(var(--ui-primary)), 0 0 5px rgb(var(--ui-primary));
}
.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-radius: 50%;
border-top-color: rgb(var(--ui-primary));
border-left-color: rgb(var(--ui-primary));
border-radius: 50%;
animation: nprogress-spinner 400ms linear infinite;
}
}
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
overflow: hidden;
#nprogress .spinner,
#nprogress .bar {
+2 -2
View File
@@ -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: "";
}
}
+1 -1
View File
@@ -73,7 +73,7 @@ const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
drag
>
<div class="slot">
<SvgIcon 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>
+11 -3
View File
@@ -28,10 +28,18 @@ function onScroll() {
<template>
<div
class="z-4 bottom-0 p-5 text-center bg-[var(--g-container-bg)] transition" :class="[
isBottom ? 'shadow-[0_0_1px_0_var(--g-box-shadow-color)]' : 'shadow-[0_-10px_10px_-10px_var(--g-box-shadow-color)]',
]" data-fixed-calc-width
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>
.fixed-action-bar {
box-shadow: 0 0 1px 0 var(--g-box-shadow-color);
&.shadow {
box-shadow: 0 -10px 10px -10px var(--g-box-shadow-color);
}
}
</style>
+4 -4
View File
@@ -36,14 +36,14 @@ const realHeight = computed(() => {
<style lang="scss" 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>
+17 -18
View File
@@ -12,7 +12,6 @@ const props = withDefaults(
headers?: UploadProps['headers']
data?: UploadProps['data']
name?: UploadProps['name']
url?: string
size?: number
width?: number
height?: number
@@ -22,7 +21,6 @@ const props = withDefaults(
}>(),
{
name: 'file',
url: '',
size: 2,
width: 150,
height: 150,
@@ -33,14 +31,15 @@ const props = withDefaults(
)
const emits = defineEmits<{
'update:url': [
url: string,
]
'onSuccess': [
res: any,
]
}>()
const url = defineModel<string>({
default: '',
})
const uploadData = ref({
imageViewerVisible: false,
progress: {
@@ -59,7 +58,7 @@ function previewClose() {
}
// 移除
function remove() {
emits('update:url', '')
url.value = ''
}
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
const fileName = file.name.split('.')
@@ -104,7 +103,7 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
<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;`">
<SvgIcon name="ep:plus" class="icon" />
<SvgIcon name="i-ep:plus" class="icon" />
</div>
</template>
</ElImage>
@@ -113,10 +112,10 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
<div class="mask">
<div class="actions">
<span title="预览" @click.stop="preview">
<SvgIcon name="ep:zoom-in" class="icon" />
<SvgIcon name="i-ep:zoom-in" class="icon" />
</span>
<span title="移除" @click.stop="remove">
<SvgIcon name="ep:delete" class="icon" />
<SvgIcon name="i-ep:delete" class="icon" />
</span>
</div>
</div>
@@ -146,33 +145,33 @@ 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;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
@include position-center(xy);
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,8 +206,8 @@ 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);
@@ -224,12 +223,12 @@ 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);
}
+22 -27
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
@@ -23,7 +22,6 @@ const props = withDefaults(
}>(),
{
name: 'file',
url: () => [],
size: 2,
max: 3,
width: 150,
@@ -35,14 +33,15 @@ const props = withDefaults(
)
const emits = defineEmits<{
'update:url': [
url: string[],
]
'onSuccess': [
res: any,
]
}>()
const url = defineModel<string[]>({
default: [],
})
const uploadData = ref({
dialogImageIndex: 0,
imageViewerVisible: false,
@@ -63,20 +62,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) => {
@@ -112,16 +107,16 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
<div class="mask">
<div class="actions">
<span title="预览" @click="preview(index)">
<SvgIcon name="ep:zoom-in" class="icon" />
<SvgIcon name="i-ep:zoom-in" class="icon" />
</span>
<span title="移除" @click="remove(index)">
<SvgIcon name="ep:delete" class="icon" />
<SvgIcon name="i-ep:delete" class="icon" />
</span>
<span v-show="url.length > 1" title="左移" :class="{ disabled: index === 0 }" @click="move(index, 'left')">
<SvgIcon name="ep:back" class="icon" />
<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')">
<SvgIcon name="ep:right" class="icon" />
<SvgIcon name="i-ep:right" class="icon" />
</span>
</div>
</div>
@@ -140,7 +135,7 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
class="images-upload"
>
<div class="image-slot" :style="`width:${width}px;height:${height}px;`">
<SvgIcon name="ep:plus" class="icon" />
<SvgIcon name="i-ep:plus" class="icon" />
</div>
<div v-show="uploadData.progress.percent" class="progress" :style="`width:${width}px;height:${height}px;`">
<ElImage :src="uploadData.progress.preview" :style="`width:${width}px;height:${height}px;`" fit="fill" />
@@ -169,34 +164,34 @@ 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;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
@include position-center(xy);
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,8 +231,8 @@ 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);
@@ -253,12 +248,12 @@ 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);
}
+7 -3
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,18 +29,18 @@ onMounted(() => {
})
function goBack() {
router.push('/')
router.push(settingsStore.settings.home.fullPath)
}
</script>
<template>
<div class="flex items-center justify-between flex-col lg:flex-row lg:gap-12 absolute left-[50%] top-[50%] -translate-x-50% -translate-y-50%">
<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-stone-5 text-xl">
<div class="desc mx-0 text-xl text-stone-5">
抱歉,你无权访问该页面
</div>
<div>
+2 -2
View File
@@ -12,7 +12,7 @@ const slots = useSlots()
</script>
<template>
<div class="page-header flex items-center justify-between flex-wrap gap-5 mb-5 px-5 py-4 bg-[var(--g-container-bg)] transition-background-color-300">
<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">
@@ -25,7 +25,7 @@ const slots = useSlots()
</slot>
</div>
</div>
<div v-if="slots.default" class="flex-none ml-a">
<div v-if="slots.default" class="ml-a flex-none">
<slot />
</div>
</div>
+4 -4
View File
@@ -26,13 +26,13 @@ function unCollaspe() {
<template>
<div
class="page-main flex flex-col relative m-4 bg-[var(--g-container-bg)] transition-background-color-300" :class="{
class="page-main relative m-4 flex flex-col bg-[var(--g-container-bg)] transition-background-color-300" :class="{
'of-hidden': isCollaspe,
}" :style="{
height: isCollaspe ? height : '',
}"
>
<div v-if="titleSlot || title" class="title-container px-5 py-4 border-b-1 border-b-solid border-b-[var(--g-bg)] transition-border-color-300">
<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 }}
</slot>
@@ -40,8 +40,8 @@ function unCollaspe() {
<div class="main-container p-5">
<slot />
</div>
<div v-if="isCollaspe" class="collaspe absolute bottom-0 w-full pt-10 pb-2 text-center cursor-pointer bg-gradient-to-b from-transparent to-[var(--g-container-bg)]" @click="unCollaspe">
<SvgIcon name="ep:arrow-down" class="text-xl op-30 hover:op-100 transition-opacity" />
<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>
+9 -15
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) : []
},
})
+9 -17
View File
@@ -3,38 +3,30 @@ 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': [
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>
@@ -45,10 +37,10 @@ function toggle() {
'px-4 bg-[var(--g-bg)] transition': background,
}"
>
<slot :fold="isFold" />
<div v-if="showToggle" class="absolute left-0 bottom-0 w-full text-center translate-y-1/2">
<button class="outline-none cursor-pointer inline-flex items-center px-2 h-5 border-size-0 font-medium text-xs rounded select-none bg-[var(--g-bg)]" @click="toggle">
<SvgIcon :name="isFold ? 'ep:caret-bottom' : 'ep:caret-top' " />
<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>
+13 -25
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { isOfflineUse } from '@/iconify/index.json'
defineOptions({
name: 'SvgIcon',
@@ -8,7 +7,6 @@ defineOptions({
const props = defineProps<{
name: string
async?: boolean
flip?: 'horizontal' | 'vertical' | 'both'
rotate?: number
color?: string
@@ -16,28 +14,18 @@ const props = defineProps<{
}>()
const outputType = computed(() => {
if (props.name.indexOf('i-') === 0) {
return (props.async || isOfflineUse) ? 'svg' : 'css'
if (/^https?:\/\//.test(props.name)) {
return 'img'
}
else if (/i-[^:]+:[^:]+/.test(props.name)) {
return 'unocss'
}
else if (props.name.includes(':')) {
return 'iconify'
}
else {
return 'svg'
}
else {
return 'custom'
}
})
const outputName = computed(() => {
if (props.name.indexOf('i-') === 0) {
let conversionName = props.name
if (props.async || isOfflineUse) {
conversionName = conversionName.replace('i-', '')
}
return conversionName
}
else {
return props.name
}
})
const style = computed(() => {
@@ -68,11 +56,11 @@ const style = computed(() => {
</script>
<template>
<i class="h-[1em] w-[1em] leading-[1em] flex-inline justify-center items-center relative fill-current" :style="style">
<i v-if="outputType === 'css'" :class="outputName" />
<Icon v-else-if="outputType === 'svg'" :icon="outputName" />
<svg v-else h-1em w-1em aria-hidden="true">
<use :xlink:href="`#icon-${outputName}`" />
<i class="relative h-[1em] w-[1em] flex-inline items-center justify-center fill-current leading-[1em]" :class="{ [name]: outputType === 'unocss' }" :style="style">
<Icon v-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>
<img v-else-if="outputType === 'img'" :src="name" class="h-[1em] w-[1em]">
</i>
</template>
+14 -6
View File
@@ -15,19 +15,27 @@ onMounted(() => {
<template>
<HSlideover v-model="isShow" title="系统信息">
<div class="px-4">
<h2 class="text-lg font-bold m-0">
<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-lg text-center font-sans">
<div class="my-4 text-center text-lg font-sans">
{{ lastBuildTime }}
</div>
</div>
<div class="px-4">
<h2 class="text-lg font-bold m-0">
<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="py-1.5 px-2 rounded flex items-center justify-between hover:bg-stone-1 dark:hover:bg-stone-9">
<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>
@@ -38,11 +46,11 @@ onMounted(() => {
</ul>
</div>
<div class="px-4">
<h2 class="text-lg font-bold m-0">
<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="py-1.5 px-2 rounded flex items-center justify-between hover:bg-stone-1 dark:hover:bg-stone-9">
<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>
+1 -1
View File
@@ -33,6 +33,6 @@ const isUp = computed(() => {
<span v-if="prefix" class="prefix">{{ prefix }}</span>
<span class="text">{{ value }}</span>
<span v-if="suffix" class="suffix">{{ suffix }}</span>
<SvgIcon name="ep:caret-top" :rotate="isUp ? 0 : 180" class="ml-1 transition" />
<SvgIcon name="i-ep:caret-top" :rotate="isUp ? 0 : 180" class="ml-1 transition" />
</div>
</template>
Executable → Regular
+1 -1
View File
File diff suppressed because one or more lines are too long
+92 -45
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import Message from 'vue-m-message'
import settingsDefault from '@/settings.default'
import eventBus from '@/utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
import useMenuStore from '@/store/modules/menu'
@@ -16,15 +17,6 @@ const menuStore = useMenuStore()
const isShow = ref(false)
const isDark = computed({
get() {
return settingsStore.settings.app.colorScheme === 'dark'
},
set(value) {
settingsStore.settings.app.colorScheme = value ? 'dark' : 'light'
},
})
watch(() => settingsStore.settings.menu.menuMode, (value) => {
if (value === 'single') {
menuStore.setActived(0)
@@ -50,14 +42,41 @@ watch(copied, (val) => {
}
})
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>
<HSlideover v-model="isShow" title="应用配置">
<div class="px-4 py-2 rounded-2 text-sm/6 c-rose bg-rose/20 ">
<div class="rounded-2 bg-rose/20 px-4 py-2 text-sm/6 c-rose">
<p class="my-1">
应用配置可实时预览效果,但只是临时生效,要想真正应用于项目,可以点击下方的「复制配置」按钮,并将配置粘贴到 src/settings.ts 文件中。
</p>
@@ -68,8 +87,16 @@ function handleCopy() {
<div class="divider">
颜色主题风格
</div>
<div class="flex justify-center items-center pb-4">
<HToggle v-model="isDark" on-icon="ri:sun-line" off-icon="ri:moon-line" />
<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-ri:computer-line', label: '系统', value: '' },
]"
class="w-60"
/>
</div>
<div v-if="settingsStore.mode === 'pc'" class="divider">
导航栏模式
@@ -98,7 +125,7 @@ function handleCopy() {
<div class="label">
主导航切换跳转
<HTooltip text="开启该功能后,切换主导航时,页面自动跳转至该主导航下,次导航里第一个导航">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
@@ -107,7 +134,7 @@ function handleCopy() {
<div class="label">
次导航保持展开一个
<HTooltip text="开启该功能后,次导航只保持单个菜单的展开">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.menu.subMenuUniqueOpened" />
@@ -145,41 +172,70 @@ function handleCopy() {
]"
/>
</div>
<div>
<div class="divider">
标签栏
</div>
<div class="setting-item">
<div class="label">
是否启用
</div>
<HToggle v-model="settingsStore.settings.tabbar.enable" />
</div>
<div class="setting-item">
<div class="label">
是否显示图标
</div>
<HToggle v-model="settingsStore.settings.tabbar.enableIcon" :disabled="!settingsStore.settings.tabbar.enable" />
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<HToggle v-model="settingsStore.settings.tabbar.enableHotkeys" :disabled="!settingsStore.settings.tabbar.enable" />
</div>
</div>
<div class="divider">
工具栏
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
面包屑导航
</div>
<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>
<HToggle v-model="settingsStore.settings.toolbar.navSearch" />
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
全屏
</div>
<HToggle v-model="settingsStore.settings.toolbar.enableFullscreen" />
<HToggle v-model="settingsStore.settings.toolbar.fullscreen" />
</div>
<div class="setting-item">
<div class="label">
页面刷新
<HTooltip text="使用框架内提供的刷新功能进行页面刷新">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.toolbar.enablePageReload" />
<HToggle v-model="settingsStore.settings.toolbar.pageReload" />
</div>
<div class="setting-item">
<div class="label">
颜色主题
<HTooltip text="开启后可在明亮/暗黑模式中切换">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.toolbar.enableColorScheme" />
</div>
<div v-if="settingsStore.mode === 'pc'" class="divider">
面包屑导航
</div>
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
是否启用
</div>
<HToggle v-model="settingsStore.settings.breadcrumb.enable" />
<HToggle v-model="settingsStore.settings.toolbar.colorScheme" />
</div>
<div class="divider">
页面
@@ -193,20 +249,11 @@ function handleCopy() {
<div class="divider">
导航搜索
</div>
<div class="setting-item">
<div class="label">
是否启用
<HTooltip text="对导航进行快捷搜索">
<SvgIcon name="ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.navSearch.enable" />
</div>
<div class="setting-item">
<div class="label">
是否启用快捷键
</div>
<HToggle v-model="settingsStore.settings.navSearch.enableHotkeys" :disabled="!settingsStore.settings.navSearch.enable" />
<HToggle v-model="settingsStore.settings.navSearch.enableHotkeys" :disabled="!settingsStore.settings.toolbar.navSearch" />
</div>
<div class="divider">
底部版权
@@ -248,7 +295,7 @@ function handleCopy() {
<div class="label">
是否启用
<HTooltip text="该功能开启时,登录成功默认进入主页,反之则默认进入导航栏里第一个导航页面">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.home.enable" />
@@ -257,7 +304,7 @@ function handleCopy() {
<div class="label">
主页名称
<HTooltip text="开启国际化时,该设置无效">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HInput v-model="settingsStore.settings.home.title" />
@@ -275,7 +322,7 @@ function handleCopy() {
<div class="label">
载入进度条
<HTooltip text="该功能开启时,跳转路由会看到页面顶部有进度条">
<SvgIcon name="ri:question-line" />
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.app.enableProgress" />
@@ -284,14 +331,14 @@ function handleCopy() {
<div class="label">
动态标题
<HTooltip text="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置">
<SvgIcon name="ri:question-line" />
<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="ep:document-copy" />
<SvgIcon name="i-ep:document-copy" />
复制配置
</HButton>
</template>
@@ -300,7 +347,7 @@ function handleCopy() {
<style lang="scss" scoped>
.divider {
--at-apply: flex items-center justify-between gap-4 my-4 text-nowrap text-sm font-500;
--at-apply: flex items-center justify-between gap-4 my-4 whitespace-nowrap text-sm font-500;
&::before,
&::after {
+2 -2
View File
@@ -37,8 +37,8 @@ function handleClick() {
<template>
<Teleport to="body">
<Transition v-bind="transitionClass">
<div v-if="scrollTop && scrollTop >= 200" class="fixed z-1000 right-4 bottom-4 w-12 h-12 flex justify-center items-center rounded-full cursor-pointer shadow-lg ring-1 ring-inset ring-stone-3 dark:ring-stone-7 bg-white dark:bg-dark hover:bg-stone-1 dark:hover:bg-dark/50" @click="handleClick">
<SvgIcon name="icon-park-outline:to-top-one" :size="24" />
<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>
+1 -5
View File
@@ -11,7 +11,7 @@ const settingsStore = useSettingsStore()
<template>
<footer v-if="settingsStore.settings.copyright.enable" class="copyright">
<span>Copyright</span>
<SvgIcon name="ri:copyright-line" :size="18" />
<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>
@@ -30,10 +30,6 @@ const settingsStore = useSettingsStore()
--at-apply: px-1;
}
.icon {
--at-apply: mx-1;
}
a {
--at-apply: text-center no-underline text-stone-5 hover:text-dark dark:hover:text-light transition;
}
+21 -43
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import Logo from '../Logo/index.vue'
import Tools from '../Tools/index.vue'
import ToolbarRightSide from '../Topbar/Toolbar/rightSide.vue'
import useMenuStore from '@/store/modules/menu'
import useSettingsStore from '@/store/modules/settings'
@@ -39,14 +39,14 @@ function handlerMouserScroll(event: WheelEvent) {
}"
>
<div
v-if="item.children && item.children.length !== 0" class="menu-item-container w-full h-full flex justify-between items-center gap-1 px-5 py-4 transition-all cursor-pointer group text-[var(--g-header-menu-color)] hover:(text-[var(--g-header-menu-hover-color)] bg-[var(--g-header-menu-hover-bg)])" :class="{
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="item.meta?.title" @click="switchTo(index)"
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
>
<div class="inline-flex flex-col justify-center items-center flex-1">
<div class="inline-flex flex-1 items-center justify-center gap-1">
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" :size="20" class="menu-item-container-icon transition-transform group-hover:scale-120" async />
<span class="flex-1 text-sm w-full text-center truncate transition-width transition-height transition-opacity">
{{ item.meta?.title }}
<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>
@@ -55,7 +55,7 @@ function handlerMouserScroll(event: WheelEvent) {
</div>
</div>
</div>
<Tools />
<ToolbarRightSide />
</div>
</header>
</Transition>
@@ -64,32 +64,32 @@ function handlerMouserScroll(event: WheelEvent) {
<style lang="scss" scoped>
header {
position: fixed;
z-index: 2000;
top: 0;
left: 0;
right: 0;
left: 0;
z-index: 2000;
display: flex;
align-items: center;
margin: 0 auto;
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);
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: 100%;
height: 100%;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 100%;
margin: 0 auto;
.main {
flex: 1;
display: flex;
flex: 1;
flex-wrap: wrap;
align-items: center;
height: 100%;
@@ -104,22 +104,22 @@ header {
background-color: inherit;
.logo {
width: 50px;
height: 50px;
width: initial;
height: 40px;
}
span {
font-size: 24px;
letter-spacing: 1px;
font-size: 20px;
color: var(--g-header-color);
letter-spacing: 1px;
}
}
.menu-container {
flex: 1;
margin: 0 30px;
padding: 0 20px;
height: 100%;
padding: 0 20px;
margin: 0 30px;
overflow-x: auto;
mask-image: linear-gradient(to right, transparent, #000 20px, #000 calc(100% - 20px), transparent);
@@ -135,24 +135,15 @@ header {
.menu {
display: inline-flex;
height: 100%;
background-color: var(--g-header-bg);
:deep(.menu-item) {
.menu-item-container {
width: 80px;
padding: 0 5px;
color: var(--g-header-menu-color);
background-color: var(--g-header-menu-bg);
&:hover {
color: var(--g-header-menu-hover-color);
background-color: var(--g-header-menu-hover-bg);
}
.menu-item-container-icon {
transform: scale(1);
font-size: 24px !important;
}
}
&.active .menu-item-container {
@@ -161,19 +152,6 @@ header {
}
}
}
:deep(.tools) {
padding: 0;
.buttons .item .icon {
color: var(--g-header-color);
}
.user-container {
font-size: 16px;
color: var(--g-header-color);
}
}
}
// 头部动画
+36 -4
View File
@@ -20,9 +20,9 @@ onMounted(() => {
<template>
<HDialog v-model="isShow" title="快捷键介绍">
<div class="px-4">
<div class="grid md:grid-cols-2 gap-2">
<div class="grid gap-2 md:grid-cols-2">
<div>
<h2 class="text-lg font-bold m-0">
<h2 class="m-0 text-lg font-bold">
全局
</h2>
<ul class="list-none pl-4 text-sm">
@@ -31,7 +31,7 @@ onMounted(() => {
<HKbd>I</HKbd>
查看系统信息
</li>
<li v-if="settingsStore.settings.navSearch.enable && settingsStore.settings.navSearch.enableHotkeys" class="py-1">
<li v-if="settingsStore.settings.toolbar.navSearch && settingsStore.settings.navSearch.enableHotkeys" class="py-1">
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
<HKbd>S</HKbd>
唤起导航搜索
@@ -39,7 +39,7 @@ onMounted(() => {
</ul>
</div>
<div v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.menuMode)">
<h2 class="text-lg font-bold m-0">
<h2 class="m-0 text-lg font-bold">
主导航
</h2>
<ul class="list-none pl-4 text-sm">
@@ -50,6 +50,38 @@ onMounted(() => {
</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>
+4 -8
View File
@@ -22,16 +22,12 @@ const settingsStore = useSettingsStore()
const title = ref(import.meta.env.VITE_APP_TITLE)
const logo = ref(imgLogo)
const to = computed(() => {
return {
...(settingsStore.settings.home.enable && { name: 'home' }),
}
})
const to = computed(() => settingsStore.settings.home.enable ? settingsStore.settings.home.fullPath : '')
</script>
<template>
<RouterLink :to="to" class="flex-center gap-2 w-inherit h-[var(--g-sidebar-logo-height)] px-3 text-inherit no-underline" :class="{ 'cursor-pointer': settingsStore.settings.home.enable }" :title="title">
<img v-if="showLogo" :src="logo" class="logo w-[30px] h-[30px] object-contain">
<span v-if="showTitle" class="block font-bold truncate">{{ title }}</span>
<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>
+13 -16
View File
@@ -26,14 +26,14 @@ const { switchTo } = useMenu()
}"
>
<div
v-if="item.children && item.children.length !== 0" class="menu-item-container w-full h-full flex justify-between items-center gap-1 px-2! py-4 transition-all cursor-pointer group text-[var(--g-main-sidebar-menu-color)] hover:(text-[var(--g-main-sidebar-menu-hover-color)] bg-[var(--g-main-sidebar-menu-hover-bg)])" :class="{
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 py-4 text-[var(--g-main-sidebar-menu-color)] transition-all hover:(bg-[var(--g-main-sidebar-menu-hover-bg)] text-[var(--g-main-sidebar-menu-hover-color)]) px-2!" :class="{
'text-[var(--g-main-sidebar-menu-active-color)]! bg-[var(--g-main-sidebar-menu-active-bg)]!': index === menuStore.actived,
}" :title="item.meta?.title" @click="switchTo(index)"
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
>
<div class="inline-flex flex-col justify-center items-center flex-1 gap-[2px] w-full">
<div class="w-full inline-flex flex-1 flex-col items-center justify-center gap-[2px]">
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" :size="20" class="menu-item-container-icon transition-transform group-hover:scale-120" async />
<span class="flex-1 text-sm w-full text-center truncate transition-width transition-height transition-opacity">
{{ item.meta?.title }}
<span class="w-full flex-1 truncate text-center text-sm transition-height transition-opacity transition-width">
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
</span>
</div>
</div>
@@ -46,10 +46,10 @@ const { switchTo } = useMenu()
<style lang="scss" scoped>
.main-sidebar-container {
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: var(--g-main-sidebar-width);
color: var(--g-main-sidebar-menu-color);
background-color: var(--g-main-sidebar-bg);
@@ -77,22 +77,19 @@ const { switchTo } = useMenu()
:deep(.menu-item) {
.menu-item-container {
height: 60px;
padding: 0 5px;
padding-block: 8px;
color: var(--g-main-sidebar-menu-color);
background-color: var(--g-main-sidebar-menu-bg);
&:hover {
color: var(--g-main-sidebar-menu-hover-color);
background-color: var(--g-main-sidebar-menu-hover-bg);
}
.menu-item-container-icon {
transform: scale(1);
font-size: 24px !important;
}
}
&:hover .menu-item-container {
color: var(--g-main-sidebar-menu-hover-color);
background-color: var(--g-main-sidebar-menu-hover-bg);
}
&.active .menu-item-container {
color: var(--g-main-sidebar-menu-active-color) !important;
background-color: var(--g-main-sidebar-menu-active-bg) !important;
+9 -14
View File
@@ -19,8 +19,6 @@ const props = withDefaults(
},
)
const router = useRouter()
const activeIndex = ref<MenuInjection['activeIndex']>(props.value)
const items = ref<MenuInjection['items']>({})
const subMenus = ref<MenuInjection['subMenus']>({})
@@ -79,25 +77,22 @@ const closeMenu: MenuInjection['closeMenu'] = (index) => {
}
function setSubMenusActive(index: string) {
Object.keys(subMenus.value).forEach((index) => {
subMenus.value[index].active = false
for (const key in subMenus.value) {
subMenus.value[key].active = false
}
subMenus.value[index]?.indexPath.forEach((idx) => {
subMenus.value[idx].active = true
})
items.value[index]?.indexPath.forEach((index) => {
subMenus.value[index].active = true
items.value[index]?.indexPath.forEach((idx) => {
subMenus.value[idx].active = true
})
}
const handleMenuItemClick: MenuInjection['handleMenuItemClick'] = (index, meta) => {
const handleMenuItemClick: MenuInjection['handleMenuItemClick'] = (index) => {
if (props.mode === 'horizontal' || props.collapse) {
openedMenus.value = []
}
activeIndex.value = index
setSubMenusActive(index)
if (meta?.link) {
window.open(meta.link)
return
}
router.push(index)
}
const handleSubMenuClick: MenuInjection['handleSubMenuClick'] = (index, indexPath) => {
if (openedMenus.value.includes(index)) {
@@ -176,7 +171,7 @@ provide(rootMenuInjectionKey, reactive({
>
<template v-for="(item, index) in menu" :key="index">
<SubMenu v-if="item.children?.length" :menu="item" :unique-key="[item.path ?? JSON.stringify(item)]" />
<Item v-else :item="item" :unique-key="[item.path ?? JSON.stringify(item)]" />
<Item v-else :item="item" :unique-key="[item.path ?? JSON.stringify(item)]" @click="handleMenuItemClick(item.path ?? JSON.stringify(item))" />
</template>
</div>
</template>
+46 -33
View File
@@ -17,8 +17,8 @@ const itemRef = ref<HTMLElement>()
const isActived = computed(() => {
return props.subMenu
? rootMenu.subMenus[props.uniqueKey[props.uniqueKey.length - 1]]?.active
: rootMenu.activeIndex === props.uniqueKey[props.uniqueKey.length - 1]
? rootMenu.subMenus[props.uniqueKey.at(-1)!].active
: rootMenu.activeIndex === props.uniqueKey.at(-1)!
})
const isItemActive = computed(() => {
@@ -43,38 +43,51 @@ defineExpose({
active: isItemActive,
}"
>
<div
class="menu-item-container w-full h-full flex justify-between items-center gap-1 px-5 py-4 transition-all cursor-pointer group text-[var(--g-sub-sidebar-menu-color)] hover:(text-[var(--g-sub-sidebar-menu-hover-color)] bg-[var(--g-sub-sidebar-menu-hover-bg)])" :class="{
'text-[var(--g-sub-sidebar-menu-active-color)]! bg-[var(--g-sub-sidebar-menu-active-bg)]!': isItemActive,
'px-2!': rootMenu.isMenuPopup && level === 0,
}" :title="item.meta?.title"
>
<div
class="inline-flex justify-center items-center gap-[12px] flex-1" :class="{
'flex-col gap-none': rootMenu.isMenuPopup && level === 0,
'flex-col gap-[2px]!': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
'w-full': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName && rootMenu.props.mode === 'vertical',
'w-[50px]': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName && rootMenu.props.mode === 'horizontal',
}" :style="indentStyle"
>
<SvgIcon v-if="props.item.meta?.icon" :name="props.item.meta.icon" :size="20" class="menu-item-container-icon transition-transform group-hover:scale-120" async />
<span
v-if="!(rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName)" class="flex-1 text-sm w-0 truncate transition-width transition-height transition-opacity"
:class="{
'opacity-0 w-0 h-0': rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName,
'w-full text-center': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
<router-link v-slot="{ href, navigate }" custom :to="uniqueKey.at(-1) ?? ''">
<HTooltip :enable="rootMenu.isMenuPopup && level === 0 && !subMenu" :text="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" placement="right" class="h-full w-full">
<component
:is="subMenu ? 'div' : 'a'" v-bind="{
...(!subMenu && {
href: item.meta?.link ? item.meta.link : href,
target: item.meta?.link ? '_blank' : '_self',
class: 'no-underline',
}),
}" class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 px-5 py-4 text-[var(--g-sub-sidebar-menu-color)] transition-all hover:(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])" :class="{
'text-[var(--g-sub-sidebar-menu-active-color)]! bg-[var(--g-sub-sidebar-menu-active-bg)]!': isItemActive,
'px-3!': rootMenu.isMenuPopup && level === 0,
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" v-on="{
...(!subMenu && {
click: navigate,
}),
}"
>
{{ item.meta?.title }}
</span>
</div>
<i
v-if="subMenu && !(rootMenu.isMenuPopup && level === 0)" class="relative w-[10px] ml-1 before:(content-empty absolute w-[6px] h-[1.5px] bg-current -translate-y-[1px] transition-transform-200) after:(content-empty absolute w-[6px] h-[1.5px] bg-current -translate-y-[1px] transition-transform-200)" :class="[
expand ? 'before:(-rotate-45 -translate-x-[2px]) after:(rotate-45 translate-x-[2px])' : 'before:(rotate-45 -translate-x-[2px]) after:(-rotate-45 translate-x-[2px])',
rootMenu.isMenuPopup && level === 0 && 'opacity-0',
rootMenu.isMenuPopup && level !== 0 && '-rotate-90 -top-[1.5px]',
]"
/>
</div>
<div
class="inline-flex flex-1 items-center justify-center gap-[12px]" :class="{
'flex-col': rootMenu.isMenuPopup && level === 0 && rootMenu.props.mode === 'vertical',
'gap-1!': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
'w-full': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName && rootMenu.props.mode === 'vertical',
}" :style="indentStyle"
>
<SvgIcon v-if="props.item.meta?.icon" :name="props.item.meta.icon" :size="20" class="menu-item-container-icon transition-transform group-hover:scale-120" async />
<span
v-if="!(rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName)" class="w-0 flex-1 truncate text-sm transition-height transition-opacity transition-width"
:class="{
'opacity-0 w-0 h-0': rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName,
'w-full text-center': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName,
}"
>
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
</span>
</div>
<i
v-if="subMenu && !(rootMenu.isMenuPopup && level === 0)" class="relative ml-1 w-[10px] after:(absolute h-[1.5px] w-[6px] bg-current transition-transform-200 content-empty -translate-y-[1px]) before:(absolute h-[1.5px] w-[6px] bg-current transition-transform-200 content-empty -translate-y-[1px])" :class="[
expand ? 'before:(-rotate-45 -translate-x-[2px]) after:(rotate-45 translate-x-[2px])' : 'before:(rotate-45 -translate-x-[2px]) after:(-rotate-45 translate-x-[2px])',
rootMenu.isMenuPopup && level === 0 && 'opacity-0',
rootMenu.isMenuPopup && level !== 0 && '-rotate-90 -top-[1.5px]',
]"
/>
</component>
</HTooltip>
</router-link>
</div>
</template>
+4 -4
View File
@@ -23,7 +23,7 @@ const subMenuRef = shallowRef<OverlayScrollbarsComponentRef>()
const rootMenu = inject(rootMenuInjectionKey)!
const opened = computed(() => {
return rootMenu.openedMenus.includes(props.uniqueKey[props.uniqueKey.length - 1])
return rootMenu.openedMenus.includes(props.uniqueKey.at(-1)!)
})
const transitionEvent = computed(() => {
@@ -114,7 +114,7 @@ function handleClick() {
rootMenu.handleSubMenuClick(index, props.uniqueKey)
}
else {
rootMenu.handleMenuItemClick(index, props.menu.meta)
rootMenu.handleMenuItemClick(index)
}
}
@@ -188,8 +188,8 @@ function handleMouseleave() {
'mx-2': rootMenu.isMenuPopup && (rootMenu.props.mode === 'vertical' || level !== 0),
}"
>
<template v-for="item in menu.children" :key="item.path">
<SubMenu v-if="item.meta?.sidebar !== false" :unique-key="[...uniqueKey, item.path]" :menu="item" :level="level + 1" />
<template v-for="item in menu.children" :key="item.path ?? JSON.stringify(item)">
<SubMenu v-if="item.meta?.sidebar !== false" :unique-key="[...uniqueKey, item.path ?? JSON.stringify(item)]" :menu="item" :level="level + 1" />
</template>
</OverlayScrollbarsComponent>
</Transition>
+1 -1
View File
@@ -27,7 +27,7 @@ export interface MenuInjection {
isMenuPopup: boolean
openMenu: (index: string, indexPath: string[]) => void
closeMenu: (index: string | string[]) => void
handleMenuItemClick: (index: string, meta?: Menu.recordRaw['meta']) => void
handleMenuItemClick: (index: string) => void
handleSubMenuClick: (index: string, indexPath: string[]) => void
}
+50 -40
View File
@@ -48,14 +48,15 @@ interface listTypes {
icon?: string
title?: string | (() => string)
link?: string
breadcrumb: any[]
breadcrumb: {
title?: string | (() => string)
}[]
}
const isShow = ref(false)
const searchInput = ref('')
const sourceList = ref<listTypes[]>([])
const actived = ref(-1)
const isScrollbarsInit = ref(false)
const searchInputRef = ref()
const searchResultRef = ref<OverlayScrollbarsComponentRef>()
@@ -83,7 +84,19 @@ const resultList = computed(() => {
if (item.path.includes(searchInput.value)) {
flag = true
}
if (item.breadcrumb.some((b: { title: any }) => b.title.includes(searchInput.value))) {
if (item.breadcrumb.some((b) => {
if (typeof b.title === 'function') {
if (b.title().includes(searchInput.value)) {
return true
}
}
else {
if (b.title?.includes(searchInput.value)) {
return true
}
}
return false
})) {
flag = true
}
return flag
@@ -119,14 +132,14 @@ onMounted(() => {
isShow.value = !isShow.value
})
hotkeys('alt+s', (e) => {
if (settingsStore.settings.navSearch.enable && settingsStore.settings.navSearch.enableHotkeys) {
if (settingsStore.settings.toolbar.navSearch && settingsStore.settings.navSearch.enableHotkeys) {
e.preventDefault()
initSourceList()
isShow.value = true
}
})
hotkeys('esc', (e) => {
if (settingsStore.settings.navSearch.enable && settingsStore.settings.navSearch.enableHotkeys) {
if (settingsStore.settings.toolbar.navSearch && settingsStore.settings.navSearch.enableHotkeys) {
e.preventDefault()
isShow.value = false
}
@@ -226,7 +239,6 @@ function keyEnter() {
searchResultItemRef.value.find(item => Number.parseInt(item.dataset.index!) === actived.value)?.click()
}
}
// FIXME 列表数据变更后,快捷键滚动失效
function handleScroll() {
if (searchResultRef.value) {
const contentDom = searchResultRef.value.osInstance()!.elements().content
@@ -262,61 +274,59 @@ function pageJump(path: listTypes['path'], link: listTypes['link']) {
</script>
<template>
<TransitionRoot as="template" :show="isShow" @after-leave="isScrollbarsInit = false">
<Dialog :initial-focus="searchInputRef" class="fixed inset-0 flex z-2000" @close="isShow && eventBus.emit('global-search-toggle')">
<TransitionRoot as="template" :show="isShow">
<Dialog :initial-focus="searchInputRef" class="fixed inset-0 z-2000 flex" @close="isShow && eventBus.emit('global-search-toggle')">
<TransitionChild as="template" v-bind="overlayTransitionClass">
<div class="fixed inset-0 transition-opacity bg-stone-200/75 dark:bg-stone-8/75 backdrop-blur-sm" />
<div class="fixed inset-0 bg-stone-200/75 backdrop-blur-sm transition-opacity dark:bg-stone-8/75" />
</TransitionChild>
<div class="fixed inset-0">
<div class="flex h-full items-end sm:items-center justify-center text-center p-4 sm:p-0">
<div class="h-full flex items-end justify-center p-4 text-center sm:items-center sm:p-0">
<TransitionChild as="template" v-bind="transitionClass">
<DialogPanel class="relative text-left w-full sm:max-w-2xl h-full max-h-4/5 flex flex-col">
<div class="flex flex-col bg-white dark:bg-stone-8 rounded-xl shadow-xl overflow-y-auto">
<DialogPanel class="relative h-full max-h-4/5 w-full flex flex-col text-left sm:max-w-2xl">
<div class="flex flex-col overflow-y-auto rounded-xl bg-white shadow-xl dark:bg-stone-8">
<div class="flex items-center px-4 py-3" border-b="~ solid stone-2 dark:stone-7">
<SvgIcon name="ep:search" :size="18" class="text-stone-5" />
<input ref="searchInputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" class="w-full focus:outline-none border-0 rounded-md placeholder-stone-4 dark:placeholder-stone-5 text-base px-3 bg-transparent text-dark dark:text-white" @keydown.esc="eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
<SvgIcon name="i-ep:search" :size="18" class="text-stone-5" />
<input ref="searchInputRef" v-model="searchInput" placeholder="搜索页面,支持标题、URL模糊查询" class="w-full border-0 rounded-md bg-transparent px-3 text-base text-dark dark:text-white focus:outline-none placeholder-stone-4 dark:placeholder-stone-5" @keydown.esc="eventBus.emit('global-search-toggle')" @keydown.up.prevent="keyUp" @keydown.down.prevent="keyDown" @keydown.enter.prevent="keyEnter">
</div>
<DialogDescription class="relative m-0 of-y-hidden">
<OverlayScrollbarsComponent ref="searchResultRef" :options="{ scrollbars: { autoHide: 'leave', autoHideDelay: 300 } }" class="h-full" @os-initialized="isScrollbarsInit = true">
<template v-if="isScrollbarsInit">
<template v-if="resultList.length > 0">
<a v-for="(item, index) in resultList" ref="searchResultItemRef" :key="item.path" class="flex items-center cursor-pointer" :class="{ 'bg-stone-2/40 dark:bg-stone-7/40': index === actived }" :data-index="index" @click="pageJump(item.path, item.link)" @mouseover="actived = index">
<SvgIcon v-if="item.icon" :name="item.icon" :size="20" class="basis-16 transition" :class="{ 'scale-120 text-ui-primary': index === actived }" />
<div class="flex-1 flex flex-col gap-1 px-4 py-3 truncate" border-l="~ solid stone-2 dark:stone-7">
<div class="text-base font-bold truncate">{{ item.title ?? '[ 无标题 ]' }}</div>
<Breadcrumb v-if="item.breadcrumb.length" class="truncate">
<BreadcrumbItem v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex" class="text-xs">
{{ bc.title ?? '[ 无标题 ]' }}
</BreadcrumbItem>
</Breadcrumb>
</div>
</a>
</template>
<template v-else>
<div flex="center col" py-6 text-stone-5>
<SvgIcon name="tabler:mood-empty" :size="40" />
<p text-base m-2>
没有找到你想要的
</p>
<OverlayScrollbarsComponent ref="searchResultRef" :options="{ scrollbars: { autoHide: 'leave', autoHideDelay: 300 } }" defer class="h-full">
<template v-if="resultList.length > 0">
<a v-for="(item, index) in resultList" ref="searchResultItemRef" :key="item.path" class="flex cursor-pointer items-center" :class="{ 'bg-stone-2/40 dark:bg-stone-7/40': index === actived }" :data-index="index" @click="pageJump(item.path, item.link)" @mouseover="actived = index">
<SvgIcon v-if="item.icon" :name="item.icon" :size="20" class="basis-16 transition" :class="{ 'scale-120 text-ui-primary': index === actived }" />
<div class="flex flex-1 flex-col gap-1 truncate px-4 py-3" border-l="~ solid stone-2 dark:stone-7">
<div class="truncate text-base font-bold">{{ (typeof item.title === 'function' ? item.title() : item.title) ?? '[ 无标题 ]' }}</div>
<Breadcrumb v-if="item.breadcrumb.length" class="truncate">
<BreadcrumbItem v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex" class="text-xs">
{{ (typeof bc.title === 'function' ? bc.title() : bc.title) ?? '[ 无标题 ]' }}
</BreadcrumbItem>
</Breadcrumb>
</div>
</template>
</a>
</template>
<template v-else>
<div flex="center col" py-6 text-stone-5>
<SvgIcon name="i-tabler:mood-empty" :size="40" />
<p m-2 text-base>
没有找到你想要的
</p>
</div>
</template>
</OverlayScrollbarsComponent>
</DialogDescription>
<div v-if="settingsStore.mode === 'pc'" class="px-4 py-3 flex justify-between" border-t="~ solid stone-2 dark:stone-7">
<div v-if="settingsStore.mode === 'pc'" class="flex justify-between px-4 py-3" border-t="~ solid stone-2 dark:stone-7">
<div class="flex gap-8">
<div class="inline-flex items-center gap-1 text-xs">
<HKbd>
<SvgIcon name="ion:md-return-left" :size="14" />
<SvgIcon name="i-ion:md-return-left" :size="14" />
</HKbd>
<span>访问</span>
</div>
<div class="inline-flex items-center gap-1 text-xs">
<HKbd>
<SvgIcon name="ant-design:caret-up-filled" :size="14" />
<SvgIcon name="i-ant-design:caret-up-filled" :size="14" />
</HKbd>
<HKbd>
<SvgIcon name="ant-design:caret-down-filled" :size="14" />
<SvgIcon name="i-ant-design:caret-down-filled" :size="14" />
</HKbd>
<span>切换</span>
</div>
+53 -28
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useElementSize } from '@vueuse/core'
import Logo from '../Logo/index.vue'
import Menu from '../Menu/index.vue'
import useSettingsStore from '@/store/modules/settings'
@@ -13,27 +14,35 @@ const route = useRoute()
const settingsStore = useSettingsStore()
const menuStore = useMenuStore()
const sidebarScrollTop = ref(0)
function onSidebarScroll(e: Event) {
sidebarScrollTop.value = (e.target as HTMLElement).scrollTop
const subSidebarRef = ref()
const showShadowTop = ref(false)
const showShadowBottom = ref(false)
function onSidebarScroll() {
const scrollTop = subSidebarRef.value.scrollTop
showShadowTop.value = scrollTop > 0
const clientHeight = subSidebarRef.value.clientHeight
const scrollHeight = subSidebarRef.value.scrollHeight
showShadowBottom.value = Math.ceil(scrollTop + clientHeight) < scrollHeight
}
const enableSidebar = computed(() => {
return settingsStore.mode === 'mobile' || (
['side', 'head', 'single'].includes(settingsStore.settings.menu.menuMode)
&& menuStore.sidebarMenus.length !== 0
&& !(
!menuStore.sidebarMenus[0].children
|| menuStore.sidebarMenus[0]?.children.every(item => item.meta?.sidebar === false)
)
)
const menuRef = ref()
onMounted(() => {
onSidebarScroll()
const { height } = useElementSize(menuRef)
watch(() => height.value, () => {
if (height.value > 0) {
onSidebarScroll()
}
}, {
immediate: true,
})
})
</script>
<template>
<div
v-if="enableSidebar" class="sub-sidebar-container" :class="{
class="sub-sidebar-container" :class="{
'is-collapse': settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse,
}"
>
@@ -43,29 +52,37 @@ const enableSidebar = computed(() => {
}"
/>
<div
class="sub-sidebar flex-1 transition-shadow-300" :class="{
shadow: sidebarScrollTop,
ref="subSidebarRef" class="sub-sidebar flex-1 transition-shadow-300" :class="{
'shadow-top': showShadowTop,
'shadow-bottom': showShadowBottom,
}" @scroll="onSidebarScroll"
>
<TransitionGroup name="sub-sidebar">
<template v-for="(mainItem, mainIndex) in menuStore.allMenus" :key="mainIndex">
<div v-show="mainIndex === menuStore.actived">
<Menu :menu="mainItem.children" :value="route.meta.activeMenu || route.path" :default-openeds="menuStore.defaultOpenedPaths" :accordion="settingsStore.settings.menu.subMenuUniqueOpened" :collapse="settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse" class="menu" />
</div>
</template>
</TransitionGroup>
<div ref="menuRef">
<TransitionGroup name="sub-sidebar">
<template v-for="(mainItem, mainIndex) in menuStore.allMenus" :key="mainIndex">
<div v-show="mainIndex === menuStore.actived">
<Menu :menu="mainItem.children" :value="route.meta.activeMenu || route.path" :default-openeds="menuStore.defaultOpenedPaths" :accordion="settingsStore.settings.menu.subMenuUniqueOpened" :collapse="settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse" class="menu" />
</div>
</template>
</TransitionGroup>
</div>
</div>
<div v-if="settingsStore.mode === 'pc'" class="relative flex items-center px-4 py-3" :class="[settingsStore.settings.menu.subMenuCollapse ? 'justify-center' : 'justify-end']">
<span v-show="settingsStore.settings.menu.enableSubMenuCollapseButton" class="flex-center cursor-pointer rounded bg-stone-1 p-2 transition dark:bg-stone-9 hover:bg-stone-2 dark:hover:bg-stone-8" :class="{ '-rotate-z-180': settingsStore.settings.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()">
<SvgIcon name="toolbar-collapse" />
</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.sub-sidebar-container {
display: flex;
flex-direction: column;
position: absolute;
left: 0;
top: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
width: var(--g-sub-sidebar-width);
background-color: var(--g-sub-sidebar-bg);
transition: background-color 0.3s, left 0.3s, width 0.3s;
@@ -109,8 +126,16 @@ const enableSidebar = computed(() => {
display: none;
}
&.shadow {
box-shadow: inset 0 10px 10px -10px var(--g-box-shadow-color);
&.shadow-top {
box-shadow: inset 0 10px 10px -10px var(--g-box-shadow-color), inset 0 0 0 transparent;
}
&.shadow-bottom {
box-shadow: inset 0 0 0 transparent, inset 0 -10px 10px -10px var(--g-box-shadow-color);
}
&.shadow-top.shadow-bottom {
box-shadow: inset 0 10px 10px -10px var(--g-box-shadow-color), inset 0 -10px 10px -10px var(--g-box-shadow-color);
}
}
-103
View File
@@ -1,103 +0,0 @@
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
import eventBus from '@/utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
import useUserStore from '@/store/modules/user'
defineOptions({
name: 'Tools',
})
const router = useRouter()
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const mainPage = useMainPage()
const { isFullscreen, toggle } = useFullscreen()
function toggleColorScheme(event: MouseEvent) {
const { startViewTransition } = useViewTransition(() => {
settingsStore.setColorScheme(settingsStore.settings.app.colorScheme === 'dark' ? 'light' : 'dark')
})
startViewTransition()?.ready.then(() => {
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
document.documentElement.animate(
{
clipPath: settingsStore.settings.app.colorScheme !== 'dark' ? clipPath : clipPath.reverse(),
},
{
duration: 300,
easing: 'ease-out',
pseudoElement: settingsStore.settings.app.colorScheme !== 'dark' ? '::view-transition-new(root)' : '::view-transition-old(root)',
},
)
})
}
const avatarError = ref(false)
watch(() => userStore.avatar, () => {
if (avatarError.value) {
avatarError.value = false
}
})
</script>
<template>
<div class="tools flex items-center gap-4 px-4 whitespace-nowrap">
<span v-if="settingsStore.settings.navSearch.enable && settingsStore.mode === 'pc'" class="group inline-flex items-center gap-1 px-2 py-1.5 rounded-2 text-dark dark:text-white bg-stone-1 dark:bg-stone-9 ring-inset ring-stone-3 dark:ring-stone-7 hover:ring-1 cursor-pointer transition" @click="eventBus.emit('global-search-toggle')">
<SvgIcon name="ri:search-line" />
<span class="text-sm text-stone-5 group-hover:text-dark dark:group-hover:text-white transition">搜索</span>
<HKbd v-if="settingsStore.settings.navSearch.enableHotkeys" class="ml-2">{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} S</HKbd>
</span>
<div class="flex items-center empty:hidden">
<span v-if="settingsStore.settings.navSearch.enable && settingsStore.mode === 'mobile'" class="item" @click="eventBus.emit('global-search-toggle')">
<SvgIcon name="ri:search-line" />
</span>
<span v-if="settingsStore.mode === 'pc' && settingsStore.settings.toolbar.enableFullscreen" class="item" @click="toggle">
<SvgIcon :name="isFullscreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-line'" />
</span>
<span v-if="settingsStore.settings.toolbar.enablePageReload" class="item" @click="mainPage.reload()">
<SvgIcon name="iconoir:refresh-double" />
</span>
<span v-if="settingsStore.settings.toolbar.enableColorScheme" class="item" @click="toggleColorScheme">
<SvgIcon :name="settingsStore.settings.app.colorScheme === 'light' ? 'ri:sun-line' : 'ri:moon-line'" />
</span>
</div>
<HDropdownMenu
:items="[
[
{ label: settingsStore.settings.home.title, handle: () => router.push({ name: 'home' }), hide: !settingsStore.settings.home.enable },
],
[
{ label: '快捷键介绍', handle: () => eventBus.emit('global-hotkeys-intro-toggle'), hide: settingsStore.mode !== 'pc' },
],
[
{ label: '退出登录', handle: () => userStore.logout() },
],
]"
>
<div flex-center gap-1 cursor-pointer>
<img v-if="userStore.avatar && !avatarError" :src="userStore.avatar" :onerror="() => (avatarError = true)" class="w-[24px] h-[24px] rounded-full">
<SvgIcon v-else name="carbon:user-avatar-filled-alt" :size="24" class="text-gray-400" />
{{ userStore.account }}
<SvgIcon name="ep:caret-bottom" />
</div>
</HDropdownMenu>
</div>
</template>
<style lang="scss" scoped>
.item {
--at-apply: flex px-2 py-1 cursor-pointer;
}
</style>
@@ -0,0 +1,465 @@
<script setup lang="ts">
import ContextMenu from '@imengyu/vue3-context-menu'
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import hotkeys from 'hotkeys-js'
import Message from 'vue-m-message'
import { useMagicKeys } from '@vueuse/core'
import useSettingsStore from '@/store/modules/settings'
import useTabbarStore from '@/store/modules/tabbar'
import type { Tabbar } from '#/global'
defineOptions({
name: 'Tabbar',
})
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const tabbarStore = useTabbarStore()
const tabbar = useTabbar()
const mainPage = useMainPage()
const keys = useMagicKeys({ reactive: true })
const activedTabId = computed(() => tabbar.getId())
const tabsRef = ref()
const tabContainerRef = ref()
const tabRef = shallowRef<HTMLElement[]>([])
onBeforeUpdate(() => {
tabRef.value = []
})
watch(() => route, (val) => {
if (settingsStore.settings.tabbar.enable) {
tabbarStore.add(val).then(() => {
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
if (index !== -1) {
scrollTo(tabRef.value[index].offsetLeft)
tabbarScrollTip()
}
})
}
}, {
immediate: true,
deep: true,
})
function tabbarScrollTip() {
if (tabContainerRef.value.$el.clientWidth > tabsRef.value.clientWidth && localStorage.getItem('tabbarScrollTip') === undefined) {
localStorage.setItem('tabbarScrollTip', '')
Message.info('标签栏数量超过展示区域范围,可以将鼠标移到标签栏上,通过鼠标滚轮滑动浏览', {
title: '温馨提示',
duration: 5000,
closable: true,
zIndex: 2000,
})
}
}
function handlerMouserScroll(event: WheelEvent) {
if (event.deltaY || event.detail !== 0) {
tabsRef.value.scrollBy({
left: (event.deltaY || event.detail) > 0 ? 50 : -50,
})
}
}
function scrollTo(offsetLeft: number) {
tabsRef.value.scrollTo({
left: offsetLeft - 50,
behavior: 'smooth',
})
}
function onTabbarContextmenu(event: MouseEvent, routeItem: Tabbar.recordRaw) {
event.preventDefault()
ContextMenu.showContextMenu({
x: event.x,
y: event.y,
zIndex: 1000,
iconFontClass: '',
customClass: 'contextmenu-custom',
items: [
{
label: '重新加载',
icon: 'i-ri:refresh-line',
disabled: routeItem.tabId !== activedTabId.value,
onClick: () => mainPage.reload(),
},
{
label: '关闭标签页',
icon: 'i-ri:close-line',
disabled: tabbarStore.list.length <= 1,
divided: true,
onClick: () => {
tabbar.closeById(routeItem.tabId)
},
},
{
label: '关闭其他标签页',
disabled: !tabbar.checkCloseOtherSide(routeItem.tabId),
onClick: () => {
tabbar.closeOtherSide(routeItem.tabId)
},
},
{
label: '关闭左侧标签页',
disabled: !tabbar.checkCloseLeftSide(routeItem.tabId),
onClick: () => {
tabbar.closeLeftSide(routeItem.tabId)
},
},
{
label: '关闭右侧标签页',
disabled: !tabbar.checkCloseRightSide(routeItem.tabId),
onClick: () => {
tabbar.closeRightSide(routeItem.tabId)
},
},
],
})
}
onMounted(() => {
hotkeys('alt+left,alt+right,alt+w,alt+1,alt+2,alt+3,alt+4,alt+5,alt+6,alt+7,alt+8,alt+9,alt+0', (e, handle) => {
if (settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys) {
e.preventDefault()
switch (handle.key) {
// 切换到当前标签页紧邻的上一个标签页
case 'alt+left':
if (tabbarStore.list[0].tabId !== activedTabId.value) {
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
router.push(tabbarStore.list[index - 1].fullPath)
}
break
// 切换到当前标签页紧邻的下一个标签页
case 'alt+right':
if (tabbarStore.list.at(-1)?.tabId !== activedTabId.value) {
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
router.push(tabbarStore.list[index + 1].fullPath)
}
break
// 关闭当前标签页
case 'alt+w':
tabbar.closeById(activedTabId.value)
break
// 切换到第 n 个标签页
case 'alt+1':
case 'alt+2':
case 'alt+3':
case 'alt+4':
case 'alt+5':
case 'alt+6':
case 'alt+7':
case 'alt+8':
case 'alt+9':
{
const number = Number(handle.key.split('+')[1])
tabbarStore.list[number - 1]?.fullPath && router.push(tabbarStore.list[number - 1].fullPath)
break
}
// 切换到最后一个标签页
case 'alt+0':
router.push(tabbarStore.list[tabbarStore.list.length - 1].fullPath)
break
}
}
})
})
onUnmounted(() => {
hotkeys.unbind('alt+q,alt+e,alt+w,alt+1,alt+2,alt+3,alt+4,alt+5,alt+6,alt+7,alt+8,alt+9,alt+0')
})
</script>
<template>
<div class="tabbar-container">
<div ref="tabsRef" class="tabs" @wheel.prevent="handlerMouserScroll">
<TransitionGroup ref="tabContainerRef" name="tabbar" tag="div" class="tab-container">
<div
v-for="(element, index) in tabbarStore.list" :key="element.tabId"
ref="tabRef" :data-index="index" class="tab" :class="{
actived: element.tabId === activedTabId,
}" :title="typeof element?.title === 'function' ? element.title() : element.title" @click="router.push(element.fullPath)" @contextmenu="onTabbarContextmenu($event, element)"
>
<div class="tab-dividers" />
<div class="tab-background" />
<div class="tab-content">
<div :key="element.tabId" class="title">
<SvgIcon v-if="settingsStore.settings.tabbar.enableIcon && element.icon" :name="element.icon" class="icon" />
{{ typeof element?.title === 'function' ? element.title() : element.title }}
</div>
<div v-if="tabbarStore.list.length > 1" class="action-icon">
<SvgIcon name="i-ri:close-fill" @click.stop="tabbar.closeById(element.tabId)" />
</div>
<div v-show="keys.alt && index < 9" class="hotkey-number">
{{ index + 1 }}
</div>
</div>
</div>
</TransitionGroup>
</div>
</div>
</template>
<style lang="scss">
.mx-menu-ghost-host {
z-index: 1000;
.mx-context-menu {
--at-apply: fixed ring-1 ring-stone-2 dark:ring-stone-7 shadow-2xl;
background-color: var(--g-container-bg);
.mx-context-menu-items .mx-context-menu-item {
--at-apply: transition-background-color;
&:not(.disabled):hover {
--at-apply: cursor-pointer bg-stone-1 dark:bg-stone-9;
}
span {
color: initial;
}
.icon {
width: 1em;
margin-right: 10px;
color: initial;
}
&.disabled span,
&.disabled .icon {
opacity: 0.25;
}
}
.mx-context-menu-item-sperator {
background-color: var(--g-container-bg);
&::after {
--at-apply: bg-stone-2 dark:bg-stone-7;
}
}
}
}
</style>
<style lang="scss" scoped>
.tabbar-container {
position: relative;
height: var(--g-tabbar-height);
background-color: var(--g-bg);
transition: background-color 0.3s;
.tabs {
position: absolute;
right: 0;
left: 0;
overflow-y: hidden;
white-space: nowrap;
// firefox隐藏滚动条
scrollbar-width: none;
// chrome隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
.tab-container {
display: inline-block;
.tab {
position: relative;
display: inline-block;
width: 150px;
height: var(--g-tabbar-height);
font-size: 14px;
line-height: calc(var(--g-tabbar-height) - 2px);
vertical-align: bottom;
pointer-events: none;
cursor: pointer;
&:not(.actived):hover {
z-index: 3;
&::before,
&::after {
content: none;
}
& + .tab .tab-dividers::before {
opacity: 0;
}
.tab-content {
.title,
.action-icon {
color: var(--g-tabbar-tab-hover-color);
}
}
.tab-background {
background-color: var(--g-tabbar-tab-hover-bg);
}
}
* {
user-select: none;
}
&.actived {
z-index: 5;
&::before,
&::after {
content: none;
}
& + .tab .tab-dividers::before {
opacity: 0;
}
.tab-content {
.title,
.action-icon {
color: var(--g-tabbar-tab-active-color);
}
}
.tab-background {
background-color: var(--g-container-bg);
}
}
.tab-dividers {
position: absolute;
top: 50%;
right: -1px;
left: -1px;
z-index: 0;
height: 14px;
transform: translateY(-50%);
&::before {
position: absolute;
top: 0;
bottom: 0;
left: 1px;
display: block;
width: 1px;
content: "";
background-color: var(--g-tabbar-dividers-bg);
opacity: 1;
transition: opacity 0.2s ease, background-color 0.3s;
}
}
&:first-child .tab-dividers::before {
opacity: 0;
}
.tab-background {
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
transition: opacity 0.3s, background-color 0.3s;
}
.tab-content {
display: flex;
width: 100%;
height: 100%;
pointer-events: all;
.title {
display: flex;
flex: 1;
gap: 5px;
align-items: center;
height: 100%;
padding: 0 10px;
margin-right: 10px;
overflow: hidden;
color: var(--g-tabbar-tab-color);
white-space: nowrap;
mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
transition: margin-right 0.3s;
&:has(+ .action-icon) {
margin-right: 28px;
}
.icon {
flex-shrink: 0;
}
}
.action-icon {
position: absolute;
top: 50%;
right: 0.5em;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 1.5em;
height: 1.5em;
font-size: 12px;
color: var(--g-tabbar-tab-color);
border-radius: 50%;
transform: translateY(-50%);
&:hover {
--at-apply: ring-1 ring-stone-3 dark:ring-stone-7;
background-color: var(--g-bg);
}
}
.hotkey-number {
--at-apply: ring-1 ring-stone-3 dark:ring-stone-7;
position: absolute;
top: 50%;
right: 0.5em;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 1.5em;
height: 1.5em;
font-size: 12px;
color: var(--g-tabbar-tab-color);
background-color: var(--g-bg);
border-radius: 50%;
transform: translateY(-50%);
}
}
}
}
}
}
// 标签栏动画
.tabs {
.tabbar-move,
.tabbar-enter-active,
.tabbar-leave-active {
transition: all 0.3s;
}
.tabbar-enter-from,
.tabbar-leave-to {
opacity: 0;
transform: translateY(30px);
}
.tabbar-leave-active {
position: absolute !important;
}
}
</style>
@@ -0,0 +1,58 @@
<script setup lang="ts">
import { compile } from 'path-to-regexp'
import Breadcrumb from '../../../Breadcrumb/index.vue'
import BreadcrumbItem from '../../../Breadcrumb/item.vue'
import useSettingsStore from '@/store/modules/settings'
const route = useRoute()
const settingsStore = useSettingsStore()
const breadcrumbList = computed(() => {
const breadcrumbList = []
if (settingsStore.settings.home.enable) {
breadcrumbList.push({
path: settingsStore.settings.home.fullPath,
title: settingsStore.settings.home.title,
})
}
if (route.meta.breadcrumbNeste) {
route.meta.breadcrumbNeste.forEach((item) => {
if (item.hide === false) {
breadcrumbList.push({
path: item.path,
title: item.title,
})
}
})
}
return breadcrumbList
})
function pathCompile(path: string) {
const toPath = compile(path)
return toPath(route.params)
}
</script>
<template>
<Breadcrumb v-if="settingsStore.mode === 'pc' && settingsStore.settings.app.routeBaseOn !== 'filesystem'" class="breadcrumb whitespace-nowrap px-2">
<TransitionGroup name="breadcrumb">
<BreadcrumbItem v-for="(item, index) in breadcrumbList" :key="`${index}_${item.path}_${item.title}`" :to="index < breadcrumbList.length - 1 && item.path !== '' ? pathCompile(item.path) : ''">
{{ item.title }}
</BreadcrumbItem>
</TransitionGroup>
</Breadcrumb>
</template>
<style lang="scss" scoped>
// 面包屑动画
.breadcrumb-enter-active {
transition: transform 0.3s, opacity 0.3s;
}
.breadcrumb-enter-from {
opacity: 0;
transform: translateX(30px) skewX(-50deg);
}
</style>
@@ -0,0 +1,60 @@
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'ColorScheme',
})
const settingsStore = useSettingsStore()
function toggleColorScheme(event: MouseEvent) {
const { startViewTransition } = useViewTransition(() => {
settingsStore.currentColorScheme && settingsStore.setColorScheme(settingsStore.currentColorScheme === 'dark' ? 'light' : 'dark')
})
startViewTransition()?.ready.then(() => {
const x = event.clientX
const y = event.clientY
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
]
document.documentElement.animate(
{
clipPath: settingsStore.settings.app.colorScheme !== 'dark' ? clipPath : clipPath.reverse(),
},
{
duration: 300,
easing: 'ease-out',
pseudoElement: settingsStore.settings.app.colorScheme !== 'dark' ? '::view-transition-new(root)' : '::view-transition-old(root)',
},
)
})
}
</script>
<template>
<HDropdown class="flex-center cursor-pointer px-2 py-1">
<SvgIcon
:name="{
'': 'i-ri:computer-line',
'light': 'i-ri:sun-line',
'dark': 'i-ri:moon-line',
}[settingsStore.settings.app.colorScheme]" @click="toggleColorScheme"
/>
<template #dropdown>
<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-ri:computer-line', label: '', value: '' },
]"
class="m-3"
/>
</template>
</HDropdown>
</template>
@@ -0,0 +1,18 @@
<script setup lang="ts">
import { useFullscreen } from '@vueuse/core'
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'Fullscreen',
})
const settingsStore = useSettingsStore()
const { isFullscreen, toggle } = useFullscreen()
</script>
<template>
<span v-if="settingsStore.mode === 'pc'" class="flex-center cursor-pointer px-2 py-1" @click="toggle">
<SvgIcon :name="isFullscreen ? 'i-ri:fullscreen-exit-line' : 'i-ri:fullscreen-line'" />
</span>
</template>
@@ -0,0 +1,21 @@
<script setup lang="ts">
import eventBus from '@/utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'ToolbarRightSide',
})
const settingsStore = useSettingsStore()
</script>
<template>
<span class="flex-center cursor-pointer px-2 py-1" @click="eventBus.emit('global-search-toggle')">
<SvgIcon v-if="settingsStore.mode === 'mobile'" name="i-ri:search-line" />
<span v-else class="group inline-flex cursor-pointer items-center gap-1 whitespace-nowrap rounded-2 bg-stone-1 px-2 py-1.5 text-dark ring-stone-3 ring-inset transition dark:bg-stone-9 dark:text-white hover:ring-1 dark:ring-stone-7">
<SvgIcon name="i-ri:search-line" />
<span class="text-sm text-stone-5 transition group-hover:text-dark dark:group-hover:text-white">搜索</span>
<HKbd v-if="settingsStore.settings.navSearch.enableHotkeys" class="ml-2">{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }} S</HKbd>
</span>
</span>
</template>
@@ -0,0 +1,13 @@
<script setup lang="ts">
defineOptions({
name: 'PageReload',
})
const mainPage = useMainPage()
</script>
<template>
<span class="flex-center cursor-pointer px-2 py-1" @click="mainPage.reload()">
<SvgIcon name="i-iconoir:refresh-double" />
</span>
</template>
@@ -1,107 +1,30 @@
<script setup lang="ts">
import { compile } from 'path-to-regexp'
import Breadcrumb from '../../Breadcrumb/index.vue'
import BreadcrumbItem from '../../Breadcrumb/item.vue'
import Tools from '../../Tools/index.vue'
import LeftSide from './leftSide.vue'
import RightSide from './rightSide.vue'
import useSettingsStore from '@/store/modules/settings'
import useMenuStore from '@/store/modules/menu'
defineOptions({
name: 'Toolbar',
})
const route = useRoute()
const settingsStore = useSettingsStore()
const menuStore = useMenuStore()
const enableSubMenuCollapseButton = computed(() => {
return settingsStore.mode === 'mobile' || (
['side', 'head', 'single'].includes(settingsStore.settings.menu.menuMode)
&& settingsStore.settings.menu.enableSubMenuCollapseButton
&& !(
!menuStore.sidebarMenus[0].children
|| menuStore.sidebarMenus[0]?.children.every(item => item.meta?.sidebar === false)
)
)
})
const breadcrumbList = computed(() => {
const breadcrumbList = []
if (settingsStore.settings.home.enable) {
breadcrumbList.push({
path: '/',
title: settingsStore.settings.home.title,
})
}
if (route.meta.breadcrumbNeste) {
route.meta.breadcrumbNeste.forEach((item) => {
if (item.hide === false) {
breadcrumbList.push({
path: item.path,
title: item.title,
})
}
})
}
return breadcrumbList
})
function pathCompile(path: string) {
const toPath = compile(path)
return toPath(route.params)
}
</script>
<template>
<div class="toolbar-container">
<div class="left-box">
<div v-if="enableSubMenuCollapseButton" class="flex-center px-2 py-1 cursor-pointer transition-transform" :class="{ '-rotate-z-180': settingsStore.settings.menu.subMenuCollapse }" @click="settingsStore.toggleSidebarCollapse()">
<SvgIcon name="toolbar-collapse" class="icon" />
</div>
<Breadcrumb v-if="settingsStore.mode === 'pc' && settingsStore.settings.breadcrumb.enable && settingsStore.settings.app.routeBaseOn !== 'filesystem'" class="breadcrumb">
<TransitionGroup name="breadcrumb">
<BreadcrumbItem v-for="(item, index) in breadcrumbList" :key="`${index}_${item.path}_${item.title}`" :to="index < breadcrumbList.length - 1 && item.path !== '' ? pathCompile(item.path) : ''">
{{ item.title }}
</BreadcrumbItem>
</TransitionGroup>
</Breadcrumb>
<div class="toolbar-container flex items-center justify-between">
<div class="h-full flex items-center of-hidden pl-2 pr-16" style="mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);">
<LeftSide />
</div>
<div v-show="['side', 'single'].includes(settingsStore.settings.menu.menuMode)" class="h-full flex items-center px-2">
<RightSide />
</div>
<Tools />
</div>
</template>
<style lang="scss" scoped>
.toolbar-container {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--g-toolbar-height);
background-color: var(--g-container-bg);
transition: background-color 0.3s;
.left-box {
display: flex;
align-items: center;
padding-left: 10px;
padding-right: 50px;
overflow: hidden;
mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);
.breadcrumb {
padding-left: 10px;
white-space: nowrap;
}
}
}
// 面包屑动画
.breadcrumb-enter-active {
transition: transform 0.3s, opacity 0.3s;
}
.breadcrumb-enter-from {
opacity: 0;
transform: translateX(30px) skewX(-50deg);
}
</style>
@@ -0,0 +1,19 @@
<script setup lang="ts">
import Breadcrumb from './Breadcrumb/index.vue'
import useSettingsStore from '@/store/modules/settings'
defineOptions({
name: 'ToolbarLeftSide',
})
const settingsStore = useSettingsStore()
</script>
<template>
<div class="flex items-center">
<div v-if="settingsStore.mode === 'mobile'" class="flex-center cursor-pointer px-2 py-1 -rotate-z-180" @click="settingsStore.toggleSidebarCollapse()">
<SvgIcon name="toolbar-collapse" />
</div>
<Breadcrumb v-if="settingsStore.settings.toolbar.breadcrumb" />
</div>
</template>
@@ -0,0 +1,54 @@
<script setup lang="ts">
import NavSearch from './NavSearch/index.vue'
import Fullscreen from './Fullscreen/index.vue'
import PageReload from './PageReload/index.vue'
import ColorScheme from './ColorScheme/index.vue'
import eventBus from '@/utils/eventBus'
import useSettingsStore from '@/store/modules/settings'
import useUserStore from '@/store/modules/user'
defineOptions({
name: 'Tools',
})
const router = useRouter()
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const avatarError = ref(false)
watch(() => userStore.avatar, () => {
if (avatarError.value) {
avatarError.value = false
}
})
</script>
<template>
<div class="flex items-center">
<NavSearch v-if="settingsStore.settings.toolbar.navSearch" />
<Fullscreen v-if="settingsStore.settings.toolbar.fullscreen" />
<PageReload v-if="settingsStore.settings.toolbar.pageReload" />
<ColorScheme v-if="settingsStore.settings.toolbar.colorScheme" />
<HDropdownMenu
:items="[
[
{ label: settingsStore.settings.home.title, handle: () => router.push({ path: settingsStore.settings.home.fullPath }), hide: !settingsStore.settings.home.enable },
],
[
{ label: '快捷键介绍', handle: () => eventBus.emit('global-hotkeys-intro-toggle'), hide: settingsStore.mode !== 'pc' },
],
[
{ label: '退出登录', handle: () => userStore.logout() },
],
]" class="flex-center cursor-pointer px-2"
>
<div class="flex-center gap-1">
<img v-if="userStore.avatar && !avatarError" :src="userStore.avatar" :onerror="() => (avatarError = true)" class="h-[24px] w-[24px] rounded-full">
<SvgIcon v-else name="i-carbon:user-avatar-filled-alt" :size="24" class="text-gray-400" />
{{ userStore.account }}
<SvgIcon name="i-ep:caret-bottom" />
</div>
</HDropdownMenu>
</div>
</template>
+10 -5
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import Tabbar from './Tabbar/index.vue'
import Toolbar from './Toolbar/index.vue'
import useSettingsStore from '@/store/modules/settings'
@@ -10,8 +11,8 @@ const settingsStore = useSettingsStore()
const enableToolbar = computed(() => {
return !(
settingsStore.settings.menu.menuMode === 'head' && !settingsStore.settings.menu.enableSubMenuCollapseButton && (
!settingsStore.settings.breadcrumb.enable || settingsStore.settings.app.routeBaseOn === 'filesystem'
settingsStore.settings.menu.menuMode === 'head' && (
!settingsStore.settings.toolbar.breadcrumb || settingsStore.settings.app.routeBaseOn === 'filesystem'
)
)
})
@@ -19,7 +20,9 @@ const enableToolbar = computed(() => {
const scrollTop = ref(0)
const scrollOnHide = ref(false)
const topbarHeight = computed(() => {
return enableToolbar.value ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-toolbar-height')) : 0
const tabbarHeight = settingsStore.settings.tabbar.enable ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-tabbar-height')) : 0
const toolbarHeight = enableToolbar.value ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-toolbar-height')) : 0
return tabbarHeight + toolbarHeight
})
onMounted(() => {
window.addEventListener('scroll', onScroll)
@@ -38,12 +41,14 @@ watch(scrollTop, (val, oldVal) => {
<template>
<div
class="topbar-container" :class="{
'has-tabbar': settingsStore.settings.tabbar.enable,
'has-toolbar': enableToolbar,
[`topbar-${settingsStore.settings.topbar.mode}`]: true,
'shadow': scrollTop,
'hide': scrollOnHide,
}" data-fixed-calc-width
>
<Tabbar v-if="settingsStore.settings.tabbar.enable" />
<Toolbar v-if="enableToolbar" />
</div>
</template>
@@ -51,8 +56,8 @@ watch(scrollTop, (val, oldVal) => {
<style lang="scss" scoped>
.topbar-container {
position: absolute;
z-index: 999;
top: 0;
z-index: 999;
display: flex;
flex-direction: column;
box-shadow: 0 1px 0 0 var(--g-border-color);
@@ -68,7 +73,7 @@ watch(scrollTop, (val, oldVal) => {
}
&.topbar-sticky.hide {
top: calc(var(--g-toolbar-height) * -1) !important;
top: calc((var(--g-tabbar-height) + var(--g-toolbar-height)) * -1) !important;
}
}
</style>
+10 -8
View File
@@ -10,7 +10,9 @@ const route = useRoute()
const { copy, copied } = useClipboard()
watch(copied, (val) => {
val && Message.success('复制成功')
val && Message.success('复制成功', {
zIndex: 2000,
})
})
function open() {
@@ -19,15 +21,15 @@ function open() {
</script>
<template>
<div class="flex flex-col absolute w-full h-full">
<div class="absolute h-full w-full flex flex-col">
<Transition name="slide-right" mode="out-in" appear>
<PageMain :key="route.meta.link" class="flex flex-col flex-1 justify-center">
<PageMain :key="route.meta.link" class="flex flex-1 flex-col justify-center">
<div class="flex flex-col items-center">
<SvgIcon name="icon-park-twotone:planet" :size="120" class="text-ui-primary/80" />
<SvgIcon name="i-icon-park-twotone:planet" :size="120" class="text-ui-primary/80" />
<div class="my-2 text-xl text-dark dark:text-white">
是否访问此链接
</div>
<div class="my-2 max-w-[300px] text-[14px] text-center text-stone-5 cursor-pointer" @click="route.meta.link && copy(route.meta.link)">
<div class="my-2 max-w-[300px] cursor-pointer text-center text-[14px] text-stone-5" @click="route.meta.link && copy(route.meta.link)">
<HTooltip text="复制链接">
<div class="line-clamp-3">
{{ route.meta.link }}
@@ -35,7 +37,7 @@ function open() {
</HTooltip>
</div>
<HButton class="my-4" @click="open">
<SvgIcon name="ri:external-link-fill" />
<SvgIcon name="i-ri:external-link-fill" />
立即访问
</HButton>
</div>
@@ -54,12 +56,12 @@ function open() {
}
.slide-right-enter-from {
opacity: 0;
margin-left: -20px;
opacity: 0;
}
.slide-right-leave-to {
opacity: 0;
margin-left: 20px;
opacity: 0;
}
</style>
+31 -20
View File
@@ -51,7 +51,7 @@ watch(() => routeInfo.path, () => {
onMounted(() => {
hotkeys('f5', (e) => {
if (settingsStore.settings.toolbar.enablePageReload) {
if (settingsStore.settings.toolbar.pageReload) {
e.preventDefault()
mainPage.reload()
}
@@ -67,6 +67,8 @@ onUnmounted(() => {
hotkeys.unbind('f5')
hotkeys.unbind('alt+`')
})
const enableAppSetting = import.meta.env.VITE_APP_SETTING === 'true'
</script>
<template>
@@ -97,9 +99,9 @@ onUnmounted(() => {
</div>
<Search />
<HotkeysIntro />
<template v-if="settingsStore.settings.app.enableAppSetting">
<template v-if="enableAppSetting">
<div class="app-setting" @click="eventBus.emit('global-app-setting-toggle')">
<SvgIcon name="uiw:setting-o" class="icon" />
<SvgIcon name="i-uiw:setting-o" class="icon" />
</div>
<AppSetting />
</template>
@@ -150,32 +152,37 @@ onUnmounted(() => {
.sidebar-container {
position: fixed;
z-index: 1010;
top: 0;
bottom: 0;
z-index: 1010;
display: flex;
width: calc(var(--g-main-sidebar-actual-width) + var(--g-sub-sidebar-actual-width));
box-shadow: -1px 0 0 0 var(--g-border-color), 1px 0 0 0 var(--g-border-color);
transition: width 0.3s, transform 0.3s, box-shadow 0.3s, top 0.3s;
&:has(> .main-sidebar-container.main-sidebar-enter-active),
&:has(> .main-sidebar-container.main-sidebar-leave-active) {
overflow: hidden;
}
}
.sidebar-mask {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100%;
visibility: hidden;
background-image: radial-gradient(transparent 1px, rgb(0 0 0 / 30%) 1px);
background-size: 4px 4px;
backdrop-filter: saturate(50%) blur(4px);
transition: all 0.2s;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
&.show {
opacity: 1;
visibility: visible;
opacity: 1;
}
}
@@ -193,16 +200,24 @@ onUnmounted(() => {
transition: margin-left 0.3s, background-color 0.3s, box-shadow 0.3s;
.main {
height: 100%;
flex: auto;
position: relative;
flex: auto;
height: 100%;
overflow: hidden;
transition: 0.3s;
}
.topbar-container.has-tabbar + .main {
margin: var(--g-tabbar-height) 0 0;
}
.topbar-container.has-toolbar + .main {
margin: var(--g-toolbar-height) 0 0;
}
.topbar-container.has-tabbar.has-toolbar + .main {
margin: calc(var(--g-tabbar-height) + var(--g-toolbar-height)) 0 0;
}
}
}
@@ -220,10 +235,6 @@ header:not(.header-leave-active) + .wrapper {
.main-container {
.topbar-container {
top: var(--g-header-height);
:deep(.tools) {
display: none;
}
}
}
}
@@ -232,18 +243,18 @@ header:not(.header-leave-active) + .wrapper {
--at-apply: text-white dark:text-dark bg-ui-primary;
position: fixed;
z-index: 10;
right: 0;
top: calc(50% + 250px);
right: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
font-size: 24px;
cursor: pointer;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
.icon {
animation: rotate 5s linear infinite;
@@ -270,12 +281,12 @@ header:not(.header-leave-active) + .wrapper {
}
.slide-right-enter-from {
opacity: 0;
margin-left: -20px;
opacity: 0;
}
.slide-right-leave-to {
opacity: 0;
margin-left: 20px;
opacity: 0;
}
</style>
+6 -6
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
const props = withDefaults(
withDefaults(
defineProps<{
modelValue: string | number | undefined
options: {
label?: string | number
icon?: string
@@ -16,18 +15,19 @@ const props = withDefaults(
)
const emits = defineEmits<{
'update:modelValue': [string | number]
'change': [string | number | undefined]
}>()
watch(() => props.modelValue, (val) => {
const value = defineModel<string | number>()
watch(value, (val) => {
emits('change', val)
})
</script>
<template>
<div class="inline-flex items-center justify-center rounded-md bg-stone-3 dark:bg-stone-7 of-hidden select-none">
<button v-for="option in options" :key="option.value" :disabled="disabled || option.disabled" class="flex items-center px-2 py-1.5 border-size-0 cursor-pointer bg-inherit disabled:cursor-not-allowed disabled:opacity-50 text-sm truncate hover:not-disabled:(text-ui-text bg-ui-primary)" :class="{ 'text-ui-text bg-ui-primary': modelValue === option.value }" @click="emits('update:modelValue', option.value)">
<div class="inline-flex select-none items-center justify-center of-hidden rounded-md bg-stone-3 dark:bg-stone-7">
<button v-for="option in options" :key="option.value" :disabled="disabled || option.disabled" class="flex cursor-pointer items-center truncate border-size-0 bg-inherit px-2 py-1.5 text-sm disabled:cursor-not-allowed disabled:opacity-50 hover:not-disabled:(bg-ui-primary text-ui-text)" :class="{ 'text-ui-text bg-ui-primary': value === option.value }" @click="value = option.value">
<SvgIcon v-if="option.icon" :name="option.icon" />
<template v-else>
{{ option.label }}
+12 -20
View File
@@ -1,27 +1,28 @@
<script setup lang="ts">
import { Dialog, DialogDescription, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
const props = withDefaults(
withDefaults(
defineProps<{
modelValue: boolean
appear?: boolean
title?: string
preventClose?: boolean
overlay?: boolean
}>(),
{
modelValue: false,
appear: false,
preventClose: false,
overlay: true,
overlay: false,
},
)
const emits = defineEmits<{
'update:modelValue': [boolean]
'close': []
}>()
const isOpen = defineModel<boolean>({
default: false,
})
const slots = useSlots()
const overlayTransitionClass = ref({
@@ -44,15 +45,6 @@ const transitionClass = computed(() => {
}
})
const isOpen = computed({
get() {
return props.modelValue
},
set(value) {
emits('update:modelValue', value)
},
})
function close() {
isOpen.value = false
emits('close')
@@ -61,21 +53,21 @@ function close() {
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog class="fixed inset-0 flex z-2000" @close="!preventClose && close()">
<Dialog class="fixed inset-0 z-2000 flex" @close="!preventClose && close()">
<TransitionChild as="template" :appear="appear" v-bind="overlayTransitionClass">
<div class="fixed inset-0 transition-opacity bg-stone-2/75 dark:bg-stone-8/75" :class="{ 'backdrop-blur-sm': overlay }" />
<div class="fixed inset-0 bg-stone-2/75 transition-opacity dark:bg-stone-8/75" :class="{ 'backdrop-blur-sm': overlay }" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div class="flex min-h-full items-end sm:items-center justify-center text-center p-4 sm:p-0">
<div class="min-h-full flex items-end justify-center p-4 text-center sm:items-center sm:p-0">
<TransitionChild as="template" :appear="appear" v-bind="transitionClass">
<DialogPanel class="relative text-left overflow-hidden sm:my-8 w-full flex flex-col sm:max-w-lg bg-white dark:bg-stone-8 rounded-xl shadow-xl">
<DialogPanel class="relative w-full flex flex-col overflow-hidden rounded-xl bg-white text-left shadow-xl sm:my-8 sm:max-w-lg dark:bg-stone-8">
<div flex="~ items-center justify-between" px-4 py-3 border-b="~ solid stone/15" text-6>
<DialogTitle m-0 text-lg text-dark dark:text-white>
{{ title }}
</DialogTitle>
<SvgIcon name="carbon:close" cursor-pointer @click="close" />
<SvgIcon name="i-carbon:close" cursor-pointer @click="close" />
</div>
<DialogDescription m-0 p-4 overflow-y-auto>
<DialogDescription m-0 overflow-y-auto p-4>
<slot />
</DialogDescription>
<div v-if="!!slots.footer" flex="~ items-center justify-end" px-4 py-3 border-t="~ solid stone/15">
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<VDropdown :triggers="['hover']" :popper-triggers="['hover']" :delay="200" v-bind="$attrs">
<VDropdown :show-triggers="['hover']" :hide-triggers="['hover']" :auto-hide="false" :popper-triggers="['hover']" :delay="200" v-bind="$attrs">
<slot />
<template #popper>
<slot name="dropdown" />
+2 -2
View File
@@ -16,11 +16,11 @@ const myItems = computed(() => {
</script>
<template>
<VMenu :delay="200" v-bind="$attrs">
<VMenu :show-triggers="['hover']" :auto-hide="false" :popper-triggers="['hover', 'click']" :delay="200" v-bind="$attrs">
<slot />
<template #popper>
<div v-for="(item, index) in myItems" :key="index" class="p-1" border-b="~ solid stone-2 dark:stone-7 last:size-0">
<button v-for="(v, i) in item" :key="i" :disabled="v.disabled" class="flex items-center gap-2 w-full px-2 py-1.5 border-size-0 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 text-sm rounded-md text-dark dark:text-white bg-inherit hover:not-disabled:bg-stone-1 dark:hover:not-disabled:bg-stone-9" @click="v.handle">
<button v-for="(v, i) in item" :key="i" :disabled="v.disabled" class="w-full flex cursor-pointer items-center gap-2 border-size-0 rounded-md bg-inherit px-2 py-1.5 text-sm text-dark disabled:cursor-not-allowed dark:text-white disabled:opacity-50 hover:not-disabled:bg-stone-1 dark:hover:not-disabled:bg-stone-9" @click="v.handle">
{{ v.label }}
</button>
</div>
+3 -10
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends string | number">
withDefaults(
defineProps<{
modelValue: string
placeholder?: string
disabled?: boolean
}>(),
@@ -10,16 +9,10 @@ withDefaults(
},
)
const emits = defineEmits<{
'update:modelValue': [string]
}>()
const value = defineModel<T>()
const inputRef = ref()
function handleInput(event: Event) {
emits('update:modelValue', (event.target as HTMLInputElement).value)
}
defineExpose({
ref: inputRef,
})
@@ -27,6 +20,6 @@ defineExpose({
<template>
<div class="relative w-full lg:w-48">
<input ref="inputRef" type="text" :value="modelValue" :placeholder="placeholder" :disabled="disabled" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none border-0 rounded-md placeholder-stone-4 dark:placeholder-stone-5 text-sm px-2.5 py-1.5 shadow-sm bg-white dark:bg-dark ring-1 ring-inset ring-stone-2 dark:ring-stone-8 focus:ring-2 focus:ring-ui-primary" @input="handleInput">
<input v-model="value" type="text" :placeholder="placeholder" :disabled="disabled" class="relative block w-full border-0 rounded-md bg-white px-2.5 py-1.5 text-sm shadow-sm ring-1 ring-stone-2 ring-inset disabled:cursor-not-allowed dark:bg-dark disabled:opacity-50 focus:outline-none focus:ring-2 dark:ring-stone-8 focus:ring-ui-primary placeholder-stone-4 dark:placeholder-stone-5">
</div>
</template>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<kbd class="inline-flex items-center justify-center text-dark dark:text-white px-1 h-6 min-w-[24px] text-[12px] rounded font-medium font-sans bg-stone-1 dark:bg-dark-9 ring-1 ring-inset ring-stone-3 dark:ring-stone-7 mr-[4px] last:mr-0">
<kbd class="mr-[4px] h-6 min-w-[24px] inline-flex items-center justify-center rounded bg-stone-1 px-1 text-[12px] text-dark font-medium font-sans ring-1 ring-stone-3 ring-inset last:mr-0 dark:bg-dark-9 dark:text-white dark:ring-stone-7">
<slot />
</kbd>
</template>
+9 -12
View File
@@ -1,7 +1,6 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
modelValue: string | number | undefined
options: {
label: string | number
value: string | number
@@ -14,35 +13,33 @@ const props = withDefaults(
},
)
const emits = defineEmits<{
'update:modelValue': [string | number]
}>()
const value = defineModel<string | number>()
const selected = computed({
get() {
return props.options.find(option => option.value === props.modelValue) ?? props.options[0]
return props.options.find(option => option.value === value.value) ?? props.options[0]
},
set(val) {
emits('update:modelValue', val.value)
value.value = val.value
},
})
</script>
<template>
<VMenu :triggers="['click']" :popper-triggers="['click']" :delay="0" :disabled="disabled" v-bind="$attrs">
<div class="inline-flex w-full">
<button class="relative block w-full lg:w-48 disabled:cursor-not-allowed focus:outline-none border-0 rounded-md text-left cursor-default text-sm gap-x-2 px-2.5 py-1.5 shadow-sm bg-white dark:bg-dark ring-1 ring-inset ring-stone-2 dark:ring-stone-8 focus:ring-2 focus:ring-ui-primary pe-9 flex items-center" :disabled="disabled">
<div class="w-full inline-flex">
<button class="relative block w-full flex cursor-default items-center gap-x-2 border-0 rounded-md bg-white px-2.5 py-1.5 pe-9 text-left text-sm shadow-sm ring-1 ring-stone-2 ring-inset lg:w-48 disabled:cursor-not-allowed dark:bg-dark focus:outline-none focus:ring-2 dark:ring-stone-8 focus:ring-ui-primary" :disabled="disabled">
<span class="block truncate">
{{ selected.label }}
</span>
<span class="absolute inset-y-0 end-0 flex items-center pointer-events-none pe-2.5">
<SvgIcon name="carbon:chevron-down" class="flex-shrink-0 text-stone-5 h-5 w-5" />
<span class="pointer-events-none absolute end-0 inset-y-0 flex items-center pe-2.5">
<SvgIcon name="i-carbon:chevron-down" class="h-5 w-5 flex-shrink-0 text-stone-5" />
</span>
</button>
</div>
<template #popper>
<div class="w-full lg:w-48 focus:outline-none overflow-y-auto scroll-py-1 p-1 max-h-60">
<button v-for="option in options" :key="option.value" :disabled="option.disabled" class="w-full px-2 py-1.5 border-size-0 cursor-pointer disabled:cursor-not-allowed text-sm text-left rounded-md truncate bg-inherit hover:not-disabled:bg-stone-1 dark:hover:not-disabled:bg-stone-9" :class="{ 'font-bold': modelValue === option.value }" @click="selected = option">
<div class="max-h-60 w-full scroll-py-1 overflow-y-auto p-1 lg:w-48 focus:outline-none">
<button v-for="option in options" :key="option.value" :disabled="option.disabled" class="w-full cursor-pointer truncate border-size-0 rounded-md bg-inherit px-2 py-1.5 text-left text-sm disabled:cursor-not-allowed hover:not-disabled:bg-stone-1 dark:hover:not-disabled:bg-stone-9" :class="{ 'font-bold': modelValue === option.value }" @click="selected = option">
{{ option.label }}
</button>
</div>
+10 -18
View File
@@ -4,7 +4,6 @@ import { OverlayScrollbarsComponent } from 'overlayscrollbars-vue'
const props = withDefaults(
defineProps<{
modelValue: boolean
appear?: boolean
side?: 'left' | 'right'
title?: string
@@ -12,19 +11,21 @@ const props = withDefaults(
overlay?: boolean
}>(),
{
modelValue: false,
appear: false,
side: 'right',
preventClose: false,
overlay: true,
overlay: false,
},
)
const emits = defineEmits<{
'update:modelValue': [boolean]
'close': []
}>()
const isOpen = defineModel<boolean>({
default: false,
})
const slots = useSlots()
const overlayTransitionClass = ref({
@@ -47,15 +48,6 @@ const transitionClass = computed(() => {
}
})
const isOpen = computed({
get() {
return props.modelValue
},
set(value) {
emits('update:modelValue', value)
},
})
function close() {
isOpen.value = false
emits('close')
@@ -64,19 +56,19 @@ function close() {
<template>
<TransitionRoot as="template" :appear="appear" :show="isOpen">
<Dialog class="fixed inset-0 flex z-2000" :class="{ 'justify-end': side === 'right' }" @close="!preventClose && close()">
<Dialog class="fixed inset-0 z-2000 flex" :class="{ 'justify-end': side === 'right' }" @close="!preventClose && close()">
<TransitionChild as="template" :appear="appear" v-bind="overlayTransitionClass">
<div class="fixed inset-0 transition-opacity bg-stone-2/75 dark:bg-stone-8/75" :class="{ 'backdrop-blur-sm': overlay }" />
<div class="fixed inset-0 bg-stone-2/75 transition-opacity dark:bg-stone-8/75" :class="{ 'backdrop-blur-sm': overlay }" />
</TransitionChild>
<TransitionChild as="template" :appear="appear" v-bind="transitionClass">
<DialogPanel relative flex-1 flex flex-col w-full focus:outline-none w-screen max-w-md bg-white dark:bg-stone-8>
<DialogPanel relative max-w-md w-full w-screen flex flex-1 flex-col bg-white dark:bg-stone-8 focus:outline-none>
<div flex="~ items-center justify-between" p-4 border-b="~ solid stone/15" text-6>
<DialogTitle m-0 text-lg text-dark dark:text-white>
{{ title }}
</DialogTitle>
<SvgIcon name="carbon:close" cursor-pointer @click="close" />
<SvgIcon name="i-carbon:close" cursor-pointer @click="close" />
</div>
<DialogDescription flex-1 m-0 of-y-hidden>
<DialogDescription m-0 flex-1 of-y-hidden>
<OverlayScrollbarsComponent :options="{ scrollbars: { autoHide: 'leave', autoHideDelay: 300 } }" defer class="h-full p-4">
<slot />
</OverlayScrollbarsComponent>
+12 -10
View File
@@ -2,46 +2,48 @@
import { Tab, TabGroup, TabList } from '@headlessui/vue'
const props = defineProps<{
modelValue: T
options: {
icon?: string
label: any
value: T
}[]
}>()
const emits = defineEmits<{
'update:modelValue': [T]
'change': [T]
}>()
const value = defineModel<T>()
const selectedIndex = computed({
get() {
return props.options.findIndex(option => option.value === props.modelValue)
return props.options.findIndex(option => option.value === value.value)
},
set(val) {
emits('update:modelValue', props.options[val].value)
value.value = props.options[val].value
},
})
watch(() => props.modelValue, (val) => {
emits('change', val)
watch(value, (val) => {
val && emits('change', val)
})
function handleChange(index: number) {
emits('update:modelValue', props.options[index].value)
value.value = props.options[index].value
}
</script>
<template>
<TabGroup :selected-index="selectedIndex" @change="handleChange">
<TabList class="inline-flex items-center justify-center p-1 rounded-md bg-stone-1 dark:bg-stone-9 ring-1 ring-stone-2 dark:ring-stone-8 select-none">
<TabList class="inline-flex select-none items-center justify-center rounded-md bg-stone-1 p-1 ring-1 ring-stone-2 dark:bg-stone-9 dark:ring-stone-8">
<Tab v-for="(option, index) in options" :key="index" v-slot="{ selected }" as="template">
<button
class="w-full px-2 py-1.5 border-size-0 text-sm rounded-md truncate text-dark dark:text-white bg-inherit focus:outline-none ring-inset ring-stone-2 dark:ring-stone-8 focus:ring-2" :class="{
class="w-full inline-flex items-center justify-center gap-1 break-keep border-size-0 rounded-md bg-inherit px-2 py-1.5 text-sm text-dark ring-stone-2 ring-inset dark:text-white focus:outline-none focus:ring-2 dark:ring-stone-8" :class="{
'cursor-default bg-white dark:bg-dark-9': selected,
'cursor-pointer opacity-50 transition hover:(opacity-100)': !selected,
'cursor-pointer opacity-50 hover:(opacity-100)': !selected,
}"
>
<SvgIcon v-if="option.icon" :name="option.icon" class="flex-shrink-0" />
{{ option.label }}
</button>
</Tab>
+4 -16
View File
@@ -1,9 +1,8 @@
<script setup lang="ts">
import { Switch } from '@headlessui/vue'
const props = withDefaults(
withDefaults(
defineProps<{
modelValue?: boolean
disabled?: boolean
onIcon?: string
offIcon?: string
@@ -13,23 +12,12 @@ const props = withDefaults(
},
)
const emits = defineEmits<{
'update:modelValue': [boolean]
}>()
const enabled = computed({
get() {
return props.modelValue
},
set(value) {
emits('update:modelValue', value)
},
})
const enabled = defineModel<boolean>()
</script>
<template>
<Switch v-model="enabled" :disabled="disabled" class="relative inline-flex vertical-middle h-5 w-10 p-0 flex-shrink-0 border-2 border-transparent cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none rounded-full focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900" :class="[enabled ? 'bg-ui-primary' : 'bg-stone-3 dark:bg-stone-7']">
<span class="pointer-events-none relative inline-block h-4 w-4 rounded-full bg-white dark:bg-dark shadow transform ring-0 transition ease-in-out duration-200" :class="[enabled ? 'translate-x-5' : 'translate-x-0']">
<Switch v-model="enabled" :disabled="disabled" class="relative h-5 w-10 inline-flex flex-shrink-0 cursor-pointer border-2 border-transparent rounded-full p-0 vertical-middle disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900" :class="[enabled ? 'bg-ui-primary' : 'bg-stone-3 dark:bg-stone-7']">
<span class="pointer-events-none relative inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out dark:bg-dark" :class="[enabled ? 'translate-x-5' : 'translate-x-0']">
<span class="absolute inset-0 h-full w-full flex items-center justify-center">
<SvgIcon v-if="(enabled && onIcon) || (!enabled && offIcon)" :name="(enabled ? onIcon : offIcon) as string" class="h-3 w-3 text-stone-7 dark:text-stone-3" />
</span>
+6 -1
View File
@@ -2,15 +2,17 @@
withDefaults(
defineProps<{
text: string
enable?: boolean
}>(),
{
text: '',
enable: true,
},
)
</script>
<template>
<VTooltip v-bind="$attrs">
<VTooltip v-if="enable" :popper-triggers="['hover']" v-bind="$attrs">
<slot />
<template #popper>
<slot name="text">
@@ -18,4 +20,7 @@ withDefaults(
</slot>
</template>
</VTooltip>
<div v-else>
<slot />
</div>
</template>
+2 -7
View File
@@ -1,11 +1,5 @@
import '@/utils/baidu'
import '@/utils/system.copyright'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import FloatingVue from 'floating-vue'
import 'floating-vue/dist/style.css'
@@ -17,6 +11,7 @@ import 'overlayscrollbars/overlayscrollbars.css'
import App from './App.vue'
import pinia from './store'
import router from './router'
import ui from './ui-provider'
// 自定义指令
import directive from '@/utils/directive'
@@ -34,13 +29,13 @@ import 'virtual:uno.css'
import '@/assets/styles/globals.scss'
const app = createApp(App)
app.use(ElementPlus)
app.use(FloatingVue, {
distance: 12,
})
app.use(Message)
app.use(pinia)
app.use(router)
app.use(ui)
directive(app)
if (icons.isOfflineUse) {
for (const info of icons.collections) {
+4 -2
View File
@@ -1,4 +1,6 @@
export default [
import { defineFakeRoute } from 'vite-plugin-fake-server/client'
export default defineFakeRoute([
{
url: '/mock/app/route/list',
method: 'get',
@@ -149,4 +151,4 @@ export default [
}
},
},
]
])
+20 -16
View File
@@ -1,26 +1,28 @@
export default [
import { defineFakeRoute } from 'vite-plugin-fake-server/client'
import Mock from 'mockjs'
export default defineFakeRoute([
{
url: '/mock/user/login',
method: 'post',
response: ({ body }: any) => {
response: ({ body }) => {
return {
error: '',
status: 1,
data: {
data: Mock.mock({
account: body.account,
token: `${body.account}_@string`,
failure_time: Math.ceil(new Date().getTime() / 1000) + 24 * 60 * 60,
avatar: '',
},
avatar: 'https://fantastic-admin.github.io/logo.png',
}),
}
},
},
{
url: '/mock/user/permission',
method: 'get',
response: ({ headers }: any) => {
response: ({ headers }) => {
let permissions: string[] = []
if (headers.token.indexOf('admin') === 0) {
if (headers.token?.indexOf('admin') === 0) {
permissions = [
'permission.browse',
'permission.create',
@@ -28,7 +30,7 @@ export default [
'permission.remove',
]
}
else if (headers.token.indexOf('test') === 0) {
else if (headers.token?.indexOf('test') === 0) {
permissions = [
'permission.browse',
]
@@ -45,12 +47,14 @@ export default [
{
url: '/mock/user/password/edit',
method: 'post',
response: {
error: '',
status: 1,
data: {
isSuccess: true,
},
response: () => {
return {
error: '',
status: 1,
data: {
isSuccess: true,
},
}
},
},
]
])
-12
View File
@@ -1,12 +0,0 @@
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const mocks: any[] = []
const mockContext = import.meta.glob('./mock/*.ts', { eager: true })
Object.keys(mockContext).forEach((v) => {
mocks.push(...(mockContext[v] as any).default)
})
export function setupProdMockServer() {
createProdMockServer(mocks)
}
+18 -3
View File
@@ -34,12 +34,12 @@ router.beforeEach(async (to, from, next) => {
// 如果已登录状态下,进入登录页会强制跳转到主页
if (to.name === 'login') {
next({
name: 'home',
path: settingsStore.settings.home.fullPath,
replace: true,
})
}
// 如果未开启主页,但进入的是主页,则会进入侧边栏导航第一个模块
else if (!settingsStore.settings.home.enable && to.name === 'home') {
else if (!settingsStore.settings.home.enable && to.fullPath === settingsStore.settings.home.fullPath) {
if (menuStore.sidebarMenus.length > 0) {
next({
path: menuStore.sidebarMenusFirstDeepestPath,
@@ -105,7 +105,7 @@ router.beforeEach(async (to, from, next) => {
next({
name: 'login',
query: {
redirect: to.fullPath !== '/' ? to.fullPath : undefined,
redirect: to.fullPath !== settingsStore.settings.home.fullPath ? to.fullPath : undefined,
},
})
}
@@ -156,6 +156,21 @@ router.afterEach((to, from) => {
}
break
}
// 通过 meta.noCache 判断针对哪些页面不需要进行缓存
if (from.meta.noCache) {
switch (typeof from.meta.noCache) {
case 'string':
if (from.meta.noCache === to.name) {
keepAliveStore.remove(componentName)
}
break
case 'object':
if (from.meta.noCache.includes(to.name as string)) {
keepAliveStore.remove(componentName)
}
break
}
}
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
if (to.name === 'reload') {
keepAliveStore.remove(componentName)
@@ -11,7 +11,7 @@ const routes: RouteRecordRaw = {
name: 'multilevelMenuExample',
meta: {
title: '多级导航',
icon: 'heroicons-solid:menu-alt-3',
icon: 'i-heroicons-solid:menu-alt-3',
},
children: [
{
+2 -3
View File
@@ -37,11 +37,10 @@ const systemRoutes: RouteRecordRaw[] = [
children: [
{
path: '',
name: 'home',
component: () => import('@/views/index.vue'),
meta: {
title: () => useSettingsStore().settings.home.title,
icon: 'ant-design:home-twotone',
icon: 'i-ant-design:home-twotone',
breadcrumb: false,
},
},
@@ -63,7 +62,7 @@ const asyncRoutes: Route.recordMainRaw[] = [
{
meta: {
title: '演示',
icon: 'uim:box',
icon: 'i-uim:box',
},
children: [
MultilevelMenuExample,
+11 -8
View File
@@ -9,11 +9,11 @@ const globalSettingsDefault: RecursiveRequired<Settings.all> = {
enableProgress: true,
enableDynamicTitle: false,
routeBaseOn: 'frontend',
enableAppSetting: false,
},
home: {
enable: true,
title: '主页',
fullPath: '/',
},
layout: {
enableMobileAdaptation: false,
@@ -30,19 +30,22 @@ const globalSettingsDefault: RecursiveRequired<Settings.all> = {
topbar: {
mode: 'static',
},
toolbar: {
enableFullscreen: false,
enablePageReload: false,
enableColorScheme: false,
tabbar: {
enable: false,
enableIcon: false,
enableHotkeys: false,
},
breadcrumb: {
enable: true,
toolbar: {
breadcrumb: true,
navSearch: true,
fullscreen: false,
pageReload: false,
colorScheme: false,
},
mainPage: {
enableHotkeys: true,
},
navSearch: {
enable: true,
enableHotkeys: true,
},
copyright: {
+4 -2
View File
@@ -57,14 +57,16 @@ const useMenuStore = defineStore(
// 次导航数据
const sidebarMenus = computed<Menu.recordMainRaw['children']>(() => {
return allMenus.value.length > 0
? allMenus.value[actived.value].children
? allMenus.value.length > 1
? allMenus.value[actived.value].children
: allMenus.value[0].children
: []
})
// 次导航第一层最深路径
const sidebarMenusFirstDeepestPath = computed(() => {
return sidebarMenus.value.length > 0
? getDeepestPath(sidebarMenus.value[0])
: '/'
: settingsStore.settings.home.fullPath
})
function getDeepestPath(menu: Menu.recordRaw, rootPath = '') {
let retnPath = ''
+31 -12
View File
@@ -8,10 +8,28 @@ const useSettingsStore = defineStore(
'settings',
() => {
const settings = ref(settingsDefault)
watch(() => settings.value.app.colorScheme, (colorScheme) => {
if (colorScheme === '') {
colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const prefersColorScheme = window.matchMedia('(prefers-color-scheme: dark)')
const currentColorScheme = ref<Exclude<Settings.app['colorScheme'], ''>>()
watch(() => settings.value.app.colorScheme, (val) => {
if (val === '') {
prefersColorScheme.addEventListener('change', updateTheme)
}
else {
prefersColorScheme.removeEventListener('change', updateTheme)
}
}, {
immediate: true,
})
watch(() => settings.value.app.colorScheme, updateTheme, {
immediate: true,
})
function updateTheme() {
let colorScheme = settings.value.app.colorScheme
if (colorScheme === '') {
colorScheme = prefersColorScheme.matches ? 'dark' : 'light'
}
currentColorScheme.value = colorScheme
switch (colorScheme) {
case 'light':
document.documentElement.classList.remove('dark')
@@ -20,9 +38,8 @@ const useSettingsStore = defineStore(
document.documentElement.classList.add('dark')
break
}
}, {
immediate: true,
})
}
watch(() => settings.value.menu.menuMode, (val) => {
document.body.setAttribute('data-menu-mode', val)
}, {
@@ -70,16 +87,17 @@ const useSettingsStore = defineStore(
}
}
// 次导航是否收起(用于记录 pc 模式下最后的状态)
const subMenuCollapseLastStatus = ref(settingsDefault.menu.subMenuCollapse)
// 切换侧边栏导航展开/收起
function toggleSidebarCollapse() {
settings.value.menu.subMenuCollapse = !settings.value.menu.subMenuCollapse
if (mode.value === 'pc') {
subMenuCollapseLastStatus.value = !subMenuCollapseLastStatus.value
}
}
// 次导航是否收起(用于记录 pc 模式下最后的状态)
const subMenuCollapseLastStatus = ref(settingsDefault.menu.subMenuCollapse)
watch(() => settings.value.menu.subMenuCollapse, (val) => {
if (mode.value === 'pc') {
subMenuCollapseLastStatus.value = val
}
})
watch(mode, (val) => {
switch (val) {
case 'pc':
@@ -106,6 +124,7 @@ const useSettingsStore = defineStore(
return {
settings,
currentColorScheme,
os,
title,
setTitle,
+169
View File
@@ -0,0 +1,169 @@
import type { RouteLocationNormalized } from 'vue-router'
import useKeepAliveStore from './keepAlive'
import type { Tabbar } from '#/global'
const useTabbarStore = defineStore(
// 唯一ID
'tabbar',
() => {
const keepAliveStore = useKeepAliveStore()
const list = ref<Tabbar.recordRaw[]>([])
const leaveIndex = ref(-1)
// 添加标签页
async function add(route: RouteLocationNormalized) {
const names: string[] = []
route.matched.forEach((v, i) => {
if (i > 0) {
v.components?.default.name && names.push(v.components.default.name)
}
})
const meta = route.matched.at(-1)?.meta
const tabId = route.fullPath
if (route.name !== 'reload') {
// 记录查找到的标签页
const findTab = list.value.find((item) => {
if (item.routeName) {
return item.routeName === route.name
}
else {
return item.tabId === tabId
}
})
// 新增标签页
if (!findTab) {
const listItem = {
tabId,
fullPath: route.fullPath,
routeName: route.name,
title: typeof meta?.title === 'function' ? meta.title() : meta?.title,
icon: meta?.icon ?? meta?.breadcrumbNeste?.findLast(item => item.icon)?.icon,
name: names,
}
if (leaveIndex.value >= 0) {
list.value.splice(leaveIndex.value + 1, 0, listItem)
leaveIndex.value = -1
}
else {
list.value.push(listItem)
}
}
}
}
// 删除指定标签页
function remove(tabId: Tabbar.recordRaw['tabId']) {
const keepName: string[] = []
const removeName: string[] = []
list.value.forEach((v) => {
if (v.tabId === tabId) {
removeName.push(...v.name)
}
else {
keepName.push(...v.name)
}
})
const name: string[] = []
removeName.forEach((v) => {
if (!keepName.includes(v)) {
name.push(v)
}
})
// 如果是手动点击关闭 tab 标签页,则删除页面缓存
keepAliveStore.remove(name)
list.value = list.value.filter((item) => {
return item.tabId !== tabId
})
}
// 删除两侧标签页
function removeOtherSide(tabId: Tabbar.recordRaw['tabId']) {
const keepName: string[] = []
const removeName: string[] = []
list.value.forEach((v) => {
if (v.tabId !== tabId) {
removeName.push(...v.name)
}
else {
keepName.push(...v.name)
}
})
const name: string[] = []
removeName.forEach((v) => {
if (!keepName.includes(v)) {
name.push(v)
}
})
keepAliveStore.remove(name)
list.value = list.value.filter((item) => {
return item.tabId === tabId
})
}
// 删除左侧标签页
function removeLeftSide(tabId: Tabbar.recordRaw['tabId']) {
// 查找指定路由对应在标签页列表里的下标
const index = list.value.findIndex(item => item.tabId === tabId)
const keepName: string[] = []
const removeName: string[] = []
list.value.forEach((v, i) => {
if (i < index) {
removeName.push(...v.name)
}
else {
keepName.push(...v.name)
}
})
const name: string[] = []
removeName.forEach((v) => {
if (!keepName.includes(v)) {
name.push(v)
}
})
keepAliveStore.remove(name)
list.value = list.value.filter((item, i) => {
return i >= index
})
}
// 删除右侧标签页
function removeRightSide(tabId: Tabbar.recordRaw['tabId']) {
// 查找指定路由对应在标签页列表里的下标
const index = list.value.findIndex(item => item.tabId === tabId)
const keepName: string[] = []
const removeName: string[] = []
list.value.forEach((v, i) => {
if (i > index) {
removeName.push(...v.name)
}
else {
keepName.push(...v.name)
}
})
const name: string[] = []
removeName.forEach((v) => {
if (!keepName.includes(v)) {
name.push(v)
}
})
keepAliveStore.remove(name)
list.value = list.value.filter((item, i) => {
return i <= index
})
}
// 清空所有标签页,登出的时候需要清空
function clean() {
list.value = []
}
return {
list,
leaveIndex,
add,
remove,
removeOtherSide,
removeLeftSide,
removeRightSide,
clean,
}
},
)
export default useTabbarStore
+14 -20
View File
@@ -1,3 +1,4 @@
import useSettingsStore from './settings'
import useRouteStore from './route'
import useMenuStore from './menu'
import router from '@/router'
@@ -7,22 +8,19 @@ const useUserStore = defineStore(
// 唯一ID
'user',
() => {
const settingsStore = useSettingsStore()
const routeStore = useRouteStore()
const menuStore = useMenuStore()
const account = ref(localStorage.account ?? '')
const token = ref(localStorage.token ?? '')
const failure_time = ref(localStorage.failure_time ?? '')
const avatar = ref(localStorage.avatar ?? '')
const account = ref(sessionStorage.account ?? '')
const token = ref(sessionStorage.token ?? '')
const avatar = ref(sessionStorage.avatar ?? '')
const permissions = ref<string[]>([])
const isLogin = computed(() => {
let retn = false
if (token.value) {
if (new Date().getTime() < Number.parseInt(failure_time.value) * 1000) {
retn = true
}
return true
}
return retn
return false
})
// 登录
@@ -31,24 +29,20 @@ const useUserStore = defineStore(
password: string
}) {
const res = await apiUser.login(data)
localStorage.setItem('account', res.data.account)
localStorage.setItem('token', res.data.token)
localStorage.setItem('failure_time', res.data.failure_time)
localStorage.setItem('avatar', res.data.avatar)
sessionStorage.setItem('account', res.data.account)
sessionStorage.setItem('token', res.data.token)
sessionStorage.setItem('avatar', res.data.avatar)
account.value = res.data.account
token.value = res.data.token
failure_time.value = res.data.failure_time
avatar.value = res.data.avatar
}
// 登出
async function logout(redirect = router.currentRoute.value.fullPath) {
localStorage.removeItem('account')
localStorage.removeItem('token')
localStorage.removeItem('failure_time')
localStorage.removeItem('avatar')
sessionStorage.removeItem('account')
sessionStorage.removeItem('token')
sessionStorage.removeItem('avatar')
account.value = ''
token.value = ''
failure_time.value = ''
avatar.value = ''
permissions.value = []
routeStore.removeRoutes()
@@ -56,7 +50,7 @@ const useUserStore = defineStore(
router.push({
name: 'login',
query: {
...(router.currentRoute.value.path !== '/' && router.currentRoute.value.name !== 'login' && { redirect }),
...(router.currentRoute.value.path !== settingsStore.settings.home.fullPath && router.currentRoute.value.name !== 'login' && { redirect }),
},
})
}
+3 -1
View File
@@ -74,6 +74,7 @@ declare global {
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTabbar: typeof import('../utils/composables/useTabbar')['default']
const useViewTransition: typeof import('../utils/composables/useViewTransition')['default']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
@@ -83,5 +84,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+50 -23
View File
@@ -38,11 +38,6 @@ declare namespace Settings {
* @可选值 `'filesystem'` 文件系统
*/
routeBaseOn?: 'frontend' | 'backend' | 'filesystem'
/**
* 是否开启应用配置,强烈建议在生产环境中关闭
* @默认值 `false`
*/
enableAppSetting?: boolean
}
interface home {
/**
@@ -55,6 +50,11 @@ declare namespace Settings {
* @默认值 `'主页'`
*/
title?: string
/**
* 主页完整路径
* @默认值 `'/'`
*/
fullPath?: string
}
interface layout {
/**
@@ -112,29 +112,49 @@ declare namespace Settings {
*/
mode?: 'static' | 'fixed' | 'sticky'
}
interface toolbar {
interface tabbar {
/**
* 是否开启全屏
* 是否开启标签栏
* @默认值 `false`
*/
enableFullscreen?: boolean
enable?: boolean
/**
* 是否开启页面刷新
* 是否开启标签栏图标显示
* @默认值 `false`
*/
enablePageReload?: boolean
enableIcon?: boolean
/**
* 是否开启颜色主题
* 是否开启标签栏快捷键
* @默认值 `false`
*/
enableColorScheme?: boolean
enableHotkeys?: boolean
}
interface breadcrumb {
interface toolbar {
/**
* 是否开启面包屑导航
* @默认值 `true`
*/
enable?: boolean
breadcrumb?: boolean
/**
* 是否开启导航搜索
* @默认值 `true`
*/
navSearch?: boolean
/**
* 是否开启全屏
* @默认值 `false`
*/
fullscreen?: boolean
/**
* 是否开启页面刷新
* @默认值 `false`
*/
pageReload?: boolean
/**
* 是否开启颜色主题
* @默认值 `false`
*/
colorScheme?: boolean
}
interface mainPage {
/**
@@ -144,11 +164,6 @@ declare namespace Settings {
enableHotkeys?: boolean
}
interface navSearch {
/**
* 是否开启导航搜索
* @默认值 `true`
*/
enable?: boolean
/**
* 是否开启导航搜索快捷键
* @默认值 `true`
@@ -193,10 +208,10 @@ declare namespace Settings {
menu?: menu
/** 顶栏设置 */
topbar?: topbar
/** 标签栏设置 */
tabbar?: tabbar
/** 工具栏设置 */
toolbar?: toolbar
/** 面包屑导航设置 */
breadcrumb?: breadcrumb
/** 页面设置 */
mainPage?: mainPage
/** 导航搜索设置 */
@@ -216,6 +231,7 @@ declare module 'vue-router' {
breadcrumb?: boolean
activeMenu?: string
cache?: boolean | string | string[]
noCache?: string | string[]
link?: string
breadcrumbNeste?: Route.breadcrumb[]
}
@@ -243,7 +259,7 @@ declare namespace Menu {
interface recordRaw {
path?: string
meta?: {
title?: string
title?: string | (() => string)
icon?: string
defaultOpened?: boolean
auth?: string | string[]
@@ -255,10 +271,21 @@ declare namespace Menu {
/** 主导航 */
interface recordMainRaw {
meta?: {
title?: string
title?: string | (() => string)
icon?: string
auth?: string | string[]
}
children: recordRaw[]
}
}
declare namespace Tabbar {
interface recordRaw {
tabId: string
fullPath: string
routeName?: RouteRecordRaw.name
title?: string | (() => string)
icon?: string
name: string[]
}
}
+1
View File
@@ -5,6 +5,7 @@ declare interface Window {
declare const __SYSTEM_INFO__: {
pkg: {
version: Recordable<string>
dependencies: Recordable<string>
devDependencies: Recordable<string>
}
+10
View File
@@ -0,0 +1,10 @@
import type { App } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
function install(app: App) {
app.use(ElementPlus)
}
export default { install }
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import zhCN from 'element-plus/es/locale/lang/zh-cn.mjs'
</script>
<template>
<ElConfigProvider :locale="zhCN" :button="{ autoInsertSpace: true }">
<slot />
</ElConfigProvider>
</template>
-16
View File
@@ -1,16 +0,0 @@
if (!import.meta.env.DEV) {
const _hmt = []
_hmt.push(['_requirePlugin', 'UrlChangeTracker', {
shouldTrackUrlChange(newPath: any, oldPath: any) {
return newPath && oldPath
},
}]);
(function () {
const hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?7bb58fdfd1fe8ab146d4b4f23686b768'
const s = document.getElementsByTagName('script')[0]
s.parentNode?.insertBefore(hm, s)
})()
}
export {}
+168
View File
@@ -0,0 +1,168 @@
import type { RouteLocationRaw } from 'vue-router'
import Message from 'vue-m-message'
import useTabbarStore from '@/store/modules/tabbar'
export default function useTabbar() {
const route = useRoute()
const router = useRouter()
const tabbarStore = useTabbarStore()
function getId() {
return route.fullPath
}
function open(to: RouteLocationRaw) {
const index = tabbarStore.list.findIndex(item => item.tabId === getId())
tabbarStore.$patch({
leaveIndex: index,
})
router.push(to)
}
function go(delta: number) {
const tabId = getId()
router.go(delta)
tabbarStore.remove(tabId)
}
function close(to: RouteLocationRaw) {
const tabId = getId()
router.push(to).then(() => {
tabbarStore.remove(tabId)
})
}
function closeById(tabId = getId()) {
const activedTabId = getId()
if (tabbarStore.list.some(item => item.tabId === tabId)) {
if (tabbarStore.list.length > 1) {
// 如果关闭的标签正好是当前路由
if (tabId === activedTabId) {
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
if (index < tabbarStore.list.length - 1) {
close(tabbarStore.list[index + 1].fullPath)
}
else {
close(tabbarStore.list[index - 1].fullPath)
}
}
else {
tabbarStore.remove(tabId)
}
}
else {
Message.error('当前只有一个标签页,已阻止关闭', {
zIndex: 2000,
})
}
}
else {
Message.error('关闭的页面不存在', {
zIndex: 2000,
})
}
}
/**
* 关闭两侧标签页
*/
function closeOtherSide(tabId = getId()) {
const activedTabId = getId()
// 如果操作的是非当前路由标签页,则先跳转到指定路由标签页
if (tabId !== activedTabId) {
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
router.push(tabbarStore.list[index].fullPath)
}
tabbarStore.removeOtherSide(tabId)
}
/**
* 关闭左侧标签页
*/
function closeLeftSide(tabId = getId()) {
const activedTabId = getId()
// 如果操作的是非当前路由标签页,需要判断当前标签页是否在指定标签页左侧,如果是则先跳转到指定路由标签页
if (tabId !== activedTabId) {
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
const activedIndex = tabbarStore.list.findIndex(item => item.tabId === activedTabId)
if (activedIndex < index) {
router.push(tabbarStore.list[index].fullPath)
}
}
tabbarStore.removeLeftSide(tabId)
}
/**
* 关闭右侧标签页
*/
function closeRightSide(tabId = getId()) {
const activedTabId = getId()
// 如果操作的是非当前路由标签页,需要判断当前标签页是否在指定标签页右侧,如果是则先跳转到指定路由标签页
if (tabId !== activedTabId) {
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
const activedIndex = tabbarStore.list.findIndex(item => item.tabId === activedTabId)
if (activedIndex > index) {
router.push(tabbarStore.list[index].fullPath)
}
}
tabbarStore.removeRightSide(tabId)
}
/**
* 校验指定标签两侧是否有可关闭的标签
*/
function checkCloseOtherSide(tabId = getId()) {
return tabbarStore.list.some((item) => {
return item.tabId !== tabId
})
}
/**
* 校验指定标签左侧是否有可关闭的标签
*/
function checkCloseLeftSide(tabId = getId()) {
let flag = true
if (tabId === tabbarStore.list[0]?.tabId) {
flag = false
}
else {
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
flag = tabbarStore.list.some((item, i) => {
return i < index && item.tabId !== tabId
})
}
return flag
}
/**
* 校验指定标签右侧是否有可关闭的标签
*/
function checkCloseRightSide(tabId = getId()) {
let flag = true
if (tabId === tabbarStore.list.at(-1)?.tabId) {
flag = false
}
else {
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
flag = tabbarStore.list.some((item, i) => {
return i >= index && item.tabId !== tabId
})
}
return flag
}
return {
getId,
open,
go,
close,
closeById,
closeOtherSide,
closeLeftSide,
closeRightSide,
checkCloseOtherSide,
checkCloseLeftSide,
checkCloseRightSide,
}
}
+7 -3
View File
@@ -7,8 +7,12 @@ meta:
</route>
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
const router = useRouter()
const settingsStore = useSettingsStore()
const data = ref({
inter: Number.NaN,
countdown: 5,
@@ -29,18 +33,18 @@ onMounted(() => {
})
function goBack() {
router.push('/')
router.push(settingsStore.settings.home.fullPath)
}
</script>
<template>
<div class="flex items-center justify-between flex-col lg:flex-row lg:gap-12 absolute left-[50%] top-[50%] -translate-x-50% -translate-y-50%">
<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="404" class="text-[300px] lg:text-[400px]" />
<div class="flex flex-col gap-4">
<h1 class="m-0 text-6xl font-sans">
404
</h1>
<div class="desc mx-0 text-stone-5 text-xl">
<div class="desc mx-0 text-xl text-stone-5">
抱歉,你访问的页面不存在
</div>
<div>
+9 -42
View File
@@ -71,7 +71,7 @@ function open(url: string) {
</div>
</template>
<HButton outline @click="open('https://fantastic-admin.gitee.io')">
<SvgIcon name="ri:file-text-line" />
<SvgIcon name="i-ri:file-text-line" />
开发文档
</HButton>
<HDropdownMenu
@@ -83,13 +83,16 @@ function open(url: string) {
]"
>
<HButton class="ml-2">
<SvgIcon name="ri:code-s-slash-line" />
<SvgIcon name="i-ri:code-s-slash-line" />
代码仓库
<SvgIcon name="ep:arrow-down" />
<SvgIcon name="i-ep:arrow-down" />
</HButton>
</HDropdownMenu>
</PageHeader>
<div class="w-full flex flex-col xl:flex-row gap-[20px] px-[20px]">
<PageMain class="rounded-lg text-white" style="background: linear-gradient(50deg, rgb(14 76 253), rgb(106 142 255));">
全新版本 V4.0 正式发布,支持替换 UI 组件库,点击查看<span class="cursor-pointer px-1 text-yellow font-bold hover:op-70" @click="open('https://fantastic-admin.gitee.io/guide/v4.html')">V4.0 更新介绍</span>。
</PageMain>
<div class="w-full flex flex-col gap-4 px-4 xl:flex-row">
<PageMain class="ecology">
<template #title>
<div class="title-info">
@@ -138,7 +141,7 @@ function open(url: string) {
</template>
<ElCarousel trigger="click" indicator-position="none" :interval="5000" height="250px">
<ElCarouselItem v-for="(item, index) in fantasticAdminInfo.data" :key="item">
<ElImage :src="item" fit="cover" style="cursor: pointer; width: 100%; height: 250px; margin: auto;" @click="fantasticAdminInfo.imageVisible = true; fantasticAdminInfo.index = index" />
<ElImage :src="item" fit="cover" style="width: 100%; height: 250px; margin: auto; cursor: pointer;" @click="fantasticAdminInfo.imageVisible = true; fantasticAdminInfo.index = index" />
</ElCarouselItem>
</ElCarousel>
<ElImageViewer v-if="fantasticAdminInfo.imageVisible" :url-list="fantasticAdminInfo.data" :initial-index="fantasticAdminInfo.index" @close="fantasticAdminInfo.imageVisible = false" />
@@ -165,34 +168,12 @@ function open(url: string) {
</template>
<ElCarousel trigger="click" indicator-position="none" :interval="5000" height="250px">
<ElCarouselItem v-for="(item, index) in oneStepAdminInfo.data" :key="item">
<ElImage :src="item" fit="cover" style="cursor: pointer; width: 100%; height: 250px; margin: auto;" @click="oneStepAdminInfo.imageVisible = true; oneStepAdminInfo.index = index" />
<ElImage :src="item" fit="cover" style="width: 100%; height: 250px; margin: auto; cursor: pointer;" @click="oneStepAdminInfo.imageVisible = true; oneStepAdminInfo.index = index" />
</ElCarouselItem>
</ElCarousel>
<ElImageViewer v-if="oneStepAdminInfo.imageVisible" :url-list="oneStepAdminInfo.data" :initial-index="oneStepAdminInfo.index" @close="oneStepAdminInfo.imageVisible = false" />
</PageMain>
</div>
<PageMain title="应用场景">
<div class="question">
<ol class="answer">
<li><span>没有前端开发人员的小型公司。</span>有很多小型公司没有前端开发人员,而这些公司在开发中后台系统的时候,会要求后端开发人员来进行开发工作。所以借助 Vue 的易学习易上手特性,再加上本框架的加持,可以让后端开发人员能在短时间内转型成为全栈开发。</li>
<li><span>前端开发人员不足的中小型公司。</span>根据招聘网站统计,几乎所有公司都缺前端,其中有很大一部分中小型公司标配只有1-2名前端开发人员,而这些公司在开发中后台系统的时候,如果能有一套现成的中后台框架系统,不仅能提高项目开发效率,同时还大大减轻前端开发人员工作压力。</li>
<li><span>项目型公司。</span>特点为项目多,周期短,甲方对页面布局和主题风格有绝对话语权,而通过专业版提供的布局和主题风格,可应对绝大部分甲方需求,并且可自定义扩展主题风格的样式,实现高度定制化。</li>
<li><span>产品型公司。</span>产品型公司最担心的就是产品开发中代码不可控的因素,本框架除了提供完善的开发文档和代码注释外,专业版用户还可加入技术群,确保开发人员尽可能理解整套框架源码的方方面面,为产品保驾护航。</li>
<li><span>个人开发者。</span>手里有一套可高度定制化的中后台框架,什么项目都不用担心啦~</li>
</ol>
</div>
</PageMain>
<PageMain title="优势">
<div class="question">
<ol class="answer">
<li><span>作者拥有10年+的前后端开发经验。</span>部分框架的作者由于缺少后端开发经验,可能会在设计框架的时候,很少或者没有考虑后端的实现逻辑,导致框架在实际使用中,业务场景无法落地,开发人员得通过修改源码自行实现业务。</li>
<li><span>经历过数十个真实项目的打磨。</span>没用在真实业务场景中使用过的框架都是纸飞机,哪怕它提供的演示功能特别华丽。而本框架在作者就职的公司,已经稳定应用在电商、直播、OA、CRM、ERP等多个不同领域的中后台系统中。</li>
<li><span>丰富的组件库。</span>除了可以接入市面上任意 UI 组件库外,框架还扩充了一些业务组件,以及第三方插件。借助以往的项目经验,提供最佳实践方案,方便开发人员直接使用。</li>
<li><span>持续更新的业务应用静态页面。</span>通过项目积累,沉淀出数十个业务应用的静态页面,做到开发人员拿来即可使用,极大提升开发效率的同时,还省去了产品和设计人员的工作。</li>
<li><span>长期维护。</span>无论是免费的基础版,还是付费的专业版,均提供长期维护。区别在于基础版侧重于稳定性维护,主要在修复 bug ,不定期增加新特性;专业版侧重于新特性开发,在确保稳定的基础上,会长期深挖中后台系统框架,持续产出可落地的特性或开发规范。</li>
</ol>
</div>
</PageMain>
</div>
</template>
@@ -224,18 +205,4 @@ function open(url: string) {
}
}
}
.question {
.answer {
--at-apply: m-0 pl-6 text-stone-5;
li {
--at-apply: mb-2 lh-6 last:mb-0;
}
span {
--at-apply: text-stone-7 dark:text-stone-3 font-bold;
}
}
}
</style>
+29 -29
View File
@@ -9,6 +9,7 @@ meta:
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import Copyright from '@/layouts/components/Copyright/index.vue'
import useSettingsStore from '@/store/modules/settings'
import useUserStore from '@/store/modules/user'
defineOptions({
@@ -18,15 +19,17 @@ defineOptions({
const route = useRoute()
const router = useRouter()
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const banner = new URL('../assets/images/login-banner.png', import.meta.url).href
const logo = new URL('../assets/images/logo.png', import.meta.url).href
const title = import.meta.env.VITE_APP_TITLE
// 表单类型,login 登录,register 注册,reset 重置密码
const formType = ref('login')
const loading = ref(false)
const redirect = ref(route.query.redirect?.toString() ?? '/')
const redirect = ref(route.query.redirect?.toString() ?? settingsStore.settings.home.fullPath)
// 登录
const loginFormRef = ref<FormInstance>()
@@ -152,7 +155,7 @@ function testAccount(account: string) {
<div class="bg-banner" />
<div id="login-box">
<div class="login-banner">
<div class="logo" />
<img :src="logo" class="logo">
<img :src="banner" class="banner">
</div>
<ElForm v-show="formType === 'login'" ref="loginFormRef" :model="loginForm" :rules="loginRules" class="login-form">
@@ -165,14 +168,14 @@ function testAccount(account: string) {
<ElFormItem prop="account">
<ElInput v-model="loginForm.account" placeholder="用户名" type="text" tabindex="1">
<template #prefix>
<SvgIcon name="ri:user-3-fill" />
<SvgIcon name="i-ri:user-3-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="password">
<ElInput v-model="loginForm.password" type="password" placeholder="密码" tabindex="2" autocomplete="new-password" show-password @keyup.enter="handleLogin">
<template #prefix>
<SvgIcon name="ri:lock-2-fill" />
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
@@ -214,14 +217,14 @@ function testAccount(account: string) {
<ElFormItem prop="account">
<ElInput v-model="registerForm.account" placeholder="用户名" tabindex="1">
<template #prefix>
<SvgIcon name="ri:user-3-fill" />
<SvgIcon name="i-ri:user-3-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="captcha">
<ElInput v-model="registerForm.captcha" placeholder="验证码" tabindex="2">
<template #prefix>
<SvgIcon name="ic:baseline-verified-user" />
<SvgIcon name="i-ic:baseline-verified-user" />
</template>
<template #append>
<ElButton>发送验证码</ElButton>
@@ -231,14 +234,14 @@ function testAccount(account: string) {
<ElFormItem prop="password">
<ElInput v-model="registerForm.password" type="password" placeholder="密码" tabindex="3" show-password>
<template #prefix>
<SvgIcon name="ri:lock-2-fill" />
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="checkPassword">
<ElInput v-model="registerForm.checkPassword" type="password" placeholder="确认密码" tabindex="4" show-password>
<template #prefix>
<SvgIcon name="ri:lock-2-fill" />
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
@@ -263,14 +266,14 @@ function testAccount(account: string) {
<ElFormItem prop="account">
<ElInput v-model="resetForm.account" placeholder="用户名" type="text" tabindex="1">
<template #prefix>
<SvgIcon name="ri:user-3-fill" />
<SvgIcon name="i-ri:user-3-fill" />
</template>
</ElInput>
</ElFormItem>
<ElFormItem prop="captcha">
<ElInput v-model="resetForm.captcha" placeholder="验证码" type="text" tabindex="2">
<template #prefix>
<SvgIcon name="ic:baseline-verified-user" />
<SvgIcon name="i-ic:baseline-verified-user" />
</template>
<template #append>
<ElButton>发送验证码</ElButton>
@@ -280,7 +283,7 @@ function testAccount(account: string) {
<ElFormItem prop="newPassword">
<ElInput v-model="resetForm.newPassword" type="password" placeholder="新密码" tabindex="3" show-password>
<template #prefix>
<SvgIcon name="ri:lock-2-fill" />
<SvgIcon name="i-ri:lock-2-fill" />
</template>
</ElInput>
</ElFormItem>
@@ -303,15 +306,15 @@ function testAccount(account: string) {
[data-mode="mobile"] {
#login-box {
position: relative;
width: 100%;
height: 100%;
top: inherit;
left: inherit;
transform: translateX(0) translateY(0);
flex-direction: column;
justify-content: start;
width: 100%;
height: 100%;
border-radius: 0;
box-shadow: none;
transform: translateX(0) translateY(0);
.login-banner {
width: 100%;
@@ -319,12 +322,12 @@ function testAccount(account: string) {
.banner {
position: relative;
top: inherit;
right: inherit;
display: inherit;
width: 100%;
max-width: 375px;
margin: 0 auto;
display: inherit;
top: inherit;
transform: translateY(0);
}
}
@@ -354,22 +357,22 @@ function testAccount(account: string) {
}
#login-box {
display: flex;
justify-content: space-between;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
display: flex;
justify-content: space-between;
overflow: hidden;
background-color: var(--g-container-bg);
border-radius: 10px;
overflow: hidden;
box-shadow: var(--el-box-shadow);
transform: translateX(-50%) translateY(-50%);
.login-banner {
position: relative;
width: 450px;
background-color: var(--g-bg);
overflow: hidden;
background-color: var(--g-bg);
.banner {
width: 100%;
@@ -381,11 +384,8 @@ function testAccount(account: string) {
position: absolute;
top: 20px;
left: 20px;
width: 30px;
height: 30px;
border-radius: 4px;
background: url("../assets/images/logo.png") no-repeat;
background-size: contain;
box-shadow: var(--el-box-shadow-light);
}
}
@@ -394,8 +394,8 @@ function testAccount(account: string) {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 500px;
width: 500px;
min-height: 500px;
padding: 50px;
overflow: hidden;
@@ -403,10 +403,10 @@ function testAccount(account: string) {
position: relative;
.title {
font-size: 1.3em;
color: var(--el-text-color-primary);
margin: 0 auto 30px;
font-size: 1.3em;
font-weight: bold;
color: var(--el-text-color-primary);
}
}
}
@@ -415,9 +415,9 @@ function testAccount(account: string) {
margin-bottom: 24px;
:deep(.el-input) {
width: 100%;
height: 48px;
line-height: inherit;
width: 100%;
input {
height: 48px;
@@ -467,8 +467,8 @@ function testAccount(account: string) {
.copyright {
position: absolute;
bottom: 0;
width: 100%;
padding: 20px;
margin: 0;
width: 100%;
}
</style>
+51
View File
@@ -0,0 +1,51 @@
export default {
extends: [
'stylelint-config-standard-scss',
'stylelint-config-standard-vue/scss',
'stylelint-config-recess-order',
'@stylistic/stylelint-config',
],
plugins: [
'stylelint-scss',
],
rules: {
'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,
'function-no-unknown': [
true,
{
ignoreFunctions: [
'v-bind',
'map-get',
'lighten',
'darken',
],
},
],
'selector-pseudo-element-no-unknown': [
true,
{
ignorePseudoElements: [
'/^view-transition/',
],
},
],
'scss/double-slash-comment-empty-line-before': null,
'scss/no-global-function-names': null,
'@stylistic/max-line-length': null,
'@stylistic/block-closing-brace-newline-after': [
'always',
{
ignoreAtRules: ['if', 'else'],
},
],
},
allowEmptyInput: true,
ignoreFiles: [
'node_modules/**/*',
'dist*/**/*',
],
}

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