mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 06:43:51 +02:00
replace navmenu
This commit is contained in:
218
package-lock.json
generated
218
package-lock.json
generated
@@ -41,7 +41,6 @@
|
|||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "^26.4.0",
|
"electron-builder": "^26.4.0",
|
||||||
"element-plus": "^2.13.1",
|
|
||||||
"embla-carousel-vue": "^8.6.0",
|
"embla-carousel-vue": "^8.6.0",
|
||||||
"esbuild-jest": "^0.5.0",
|
"esbuild-jest": "^0.5.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
@@ -767,16 +766,6 @@
|
|||||||
"node": ">=0.1.95"
|
"node": ">=0.1.95"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ctrl/tinycolor": {
|
|
||||||
"version": "3.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
|
||||||
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@develar/schema-utils": {
|
"node_modules/@develar/schema-utils": {
|
||||||
"version": "2.6.5",
|
"version": "2.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
|
||||||
@@ -1319,16 +1308,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@element-plus/icons-vue": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
|
||||||
@@ -4044,18 +4023,6 @@
|
|||||||
"url": "https://opencollective.com/pkgr"
|
"url": "https://opencollective.com/pkgr"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@popperjs/core": {
|
|
||||||
"name": "@sxzz/popperjs-es",
|
|
||||||
"version": "2.11.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
|
|
||||||
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/popperjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.53",
|
"version": "1.0.0-beta.53",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
|
||||||
@@ -5287,24 +5254,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/lodash": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/lodash-es": {
|
|
||||||
"version": "4.17.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
|
||||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/lodash": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
@@ -6511,13 +6460,6 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/async-validator": {
|
|
||||||
"version": "4.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
|
||||||
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -8545,132 +8487,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/element-plus": {
|
|
||||||
"version": "2.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.1.tgz",
|
|
||||||
"integrity": "sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ctrl/tinycolor": "^3.4.1",
|
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
|
||||||
"@floating-ui/dom": "^1.0.1",
|
|
||||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
|
||||||
"@types/lodash": "^4.17.20",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@vueuse/core": "^10.11.0",
|
|
||||||
"async-validator": "^4.2.5",
|
|
||||||
"dayjs": "^1.11.19",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"lodash-unified": "^1.0.3",
|
|
||||||
"memoize-one": "^6.0.0",
|
|
||||||
"normalize-wheel-es": "^1.2.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/element-plus/node_modules/@types/web-bluetooth": {
|
|
||||||
"version": "0.0.20",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
|
||||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/element-plus/node_modules/@vueuse/core": {
|
|
||||||
"version": "10.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
|
||||||
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/web-bluetooth": "^0.0.20",
|
|
||||||
"@vueuse/metadata": "10.11.1",
|
|
||||||
"@vueuse/shared": "10.11.1",
|
|
||||||
"vue-demi": ">=0.14.8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": {
|
|
||||||
"version": "0.14.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
|
||||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
|
||||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@vue/composition-api": "^1.0.0-rc.1",
|
|
||||||
"vue": "^3.0.0-0 || ^2.6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@vue/composition-api": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/element-plus/node_modules/@vueuse/metadata": {
|
|
||||||
"version": "10.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
|
||||||
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/element-plus/node_modules/@vueuse/shared": {
|
|
||||||
"version": "10.11.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
|
||||||
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"vue-demi": ">=0.14.8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": {
|
|
||||||
"version": "0.14.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
|
||||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
|
||||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/antfu"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@vue/composition-api": "^1.0.0-rc.1",
|
|
||||||
"vue": "^3.0.0-0 || ^2.6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@vue/composition-api": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/embla-carousel": {
|
"node_modules/embla-carousel": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
@@ -13465,26 +13281,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
|
||||||
"version": "4.17.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/lodash-unified": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/lodash-es": "*",
|
|
||||||
"lodash": "*",
|
|
||||||
"lodash-es": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -13648,13 +13444,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/memoize-one": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
@@ -14233,13 +14022,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-wheel-es": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/noty": {
|
"node_modules/noty": {
|
||||||
"version": "3.2.0-beta-deprecated",
|
"version": "3.2.0-beta-deprecated",
|
||||||
"resolved": "https://registry.npmjs.org/noty/-/noty-3.2.0-beta-deprecated.tgz",
|
"resolved": "https://registry.npmjs.org/noty/-/noty-3.2.0-beta-deprecated.tgz",
|
||||||
|
|||||||
@@ -62,7 +62,6 @@
|
|||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "^26.4.0",
|
"electron-builder": "^26.4.0",
|
||||||
"element-plus": "^2.13.1",
|
|
||||||
"embla-carousel-vue": "^8.6.0",
|
"embla-carousel-vue": "^8.6.0",
|
||||||
"esbuild-jest": "^0.5.0",
|
"esbuild-jest": "^0.5.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
@import 'element-plus/dist/index.css';
|
|
||||||
@import 'element-plus/theme-chalk/dark/css-vars.css';
|
|
||||||
|
|
||||||
@import 'animate.css/animate.min.css';
|
@import 'animate.css/animate.min.css';
|
||||||
@import 'noty/lib/noty.css';
|
@import 'noty/lib/noty.css';
|
||||||
@import 'remixicon/fonts/remixicon.css';
|
@import 'remixicon/fonts/remixicon.css';
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
|
|
||||||
import ElementPlus from 'element-plus';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
i18n,
|
i18n,
|
||||||
initComponents,
|
initComponents,
|
||||||
@@ -20,7 +18,7 @@ await initPiniaPlugins();
|
|||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(pinia).use(i18n).use(ElementPlus);
|
app.use(pinia).use(i18n);
|
||||||
initComponents(app);
|
initComponents(app);
|
||||||
initRouter(app);
|
initRouter(app);
|
||||||
await initSentry(app);
|
await initSentry(app);
|
||||||
|
|||||||
@@ -1,241 +1,210 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="x-menu-container nav-menu-container" :class="{ 'is-collapsed': isCollapsed }">
|
<Sidebar side="left" variant="sidebar" collapsible="icon">
|
||||||
<template v-if="navLayoutReady">
|
<SidebarContent class="pt-4">
|
||||||
<div class="nav-menu-body mt-5">
|
<div v-if="navLayoutReady" class="px-2">
|
||||||
<div v-if="pendingVRCXUpdate || pendingVRCXInstall" class="pending-update">
|
<SidebarMenu>
|
||||||
<Button
|
<SidebarMenuItem v-if="pendingVRCXUpdate || pendingVRCXInstall">
|
||||||
variant="ghost"
|
<SidebarMenuButton
|
||||||
size="icon"
|
:tooltip="t('nav_menu.update_available')"
|
||||||
class="hover:bg-transparent"
|
variant="default"
|
||||||
style="font-size: 19px; height: 36px; margin: 10px"
|
@click="showVRCXUpdateDialog">
|
||||||
@click="showVRCXUpdateDialog">
|
<span class="relative inline-flex size-6 items-center justify-center">
|
||||||
<span class="relative inline-flex items-center justify-center">
|
<i class="ri-arrow-down-circle-line text-muted-foreground text-[20px]"></i>
|
||||||
<i class="ri-arrow-down-circle-line text-muted-foreground text-[20px]"></i>
|
<span class="absolute top-0.5 -right-1 h-1.5 w-1.5 rounded-full bg-red-500"></span>
|
||||||
<span class="absolute top-0.5 -right-1 h-1.5 w-1.5 rounded-full bg-red-500"></span>
|
</span>
|
||||||
</span>
|
<span v-show="!isCollapsed" class="text-[13px] text-muted-foreground">{{
|
||||||
<span v-if="!isCollapsed" class="text-[13px] text-muted-foreground">{{
|
t('nav_menu.update_available')
|
||||||
t('nav_menu.update_available')
|
}}</span>
|
||||||
}}</span>
|
</SidebarMenuButton>
|
||||||
</Button>
|
</SidebarMenuItem>
|
||||||
</div>
|
</SidebarMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-menu ref="navMenuRef" class="nav-menu" :collapse="isCollapsed" :collapse-transition="false">
|
<SidebarGroup>
|
||||||
<template v-for="item in menuItems" :key="item.index">
|
<SidebarGroupContent>
|
||||||
<el-menu-item
|
<SidebarMenu v-if="navLayoutReady">
|
||||||
v-if="!item.children?.length"
|
<template v-for="item in menuItems" :key="item.index">
|
||||||
:index="item.index"
|
<SidebarMenuItem v-if="!item.children?.length">
|
||||||
:class="{ notify: isNavItemNotified(item) }"
|
<SidebarMenuButton
|
||||||
@click="handleMenuItemClick(item)">
|
:is-active="activeMenuIndex === item.index"
|
||||||
<i :class="item.icon"></i>
|
:tooltip="item.titleIsCustom ? item.title : t(item.title || '')"
|
||||||
<template #title>
|
:class="isNavItemNotified(item) ? 'notify' : undefined"
|
||||||
<span>{{ item.titleIsCustom ? item.title : t(item.title || '') }}</span>
|
@click="handleMenuItemClick(item)">
|
||||||
</template>
|
<i
|
||||||
</el-menu-item>
|
:class="item.icon"
|
||||||
<el-sub-menu v-else :index="item.index">
|
class="inline-flex size-6 items-center justify-center text-[19px]" />
|
||||||
<template #title>
|
|
||||||
<div :class="{ notify: isNavItemNotified(item) }">
|
|
||||||
<i :class="item.icon"></i>
|
|
||||||
<span v-show="!isCollapsed">{{
|
<span v-show="!isCollapsed">{{
|
||||||
item.titleIsCustom ? item.title : t(item.title || '')
|
item.titleIsCustom ? item.title : t(item.title || '')
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</SidebarMenuButton>
|
||||||
</template>
|
</SidebarMenuItem>
|
||||||
<el-menu-item
|
|
||||||
v-for="entry in item.children"
|
|
||||||
:key="entry.index"
|
|
||||||
:index="entry.index"
|
|
||||||
class="pl-9!"
|
|
||||||
:class="{ notify: isEntryNotified(entry) }"
|
|
||||||
@click="handleSubmenuClick(entry, item.index)">
|
|
||||||
<i v-show="entry.icon" :class="entry.icon"></i>
|
|
||||||
<template #title>
|
|
||||||
<span>{{ t(entry.label) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-sub-menu>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-menu-container-bottom mb-4">
|
<SidebarMenuItem v-else>
|
||||||
<el-popover
|
<Collapsible
|
||||||
v-model:visible="supportMenuVisible"
|
class="group/collapsible"
|
||||||
placement="right"
|
:default-open="activeMenuIndex && item.children?.some((e) => e.index === activeMenuIndex)">
|
||||||
trigger="click"
|
<template #default="{ open }">
|
||||||
popper-style="padding:4px;border-radius:8px;"
|
<CollapsibleTrigger as-child>
|
||||||
:offset="-10"
|
<SidebarMenuButton
|
||||||
:show-arrow="false"
|
:is-active="item.children?.some((e) => e.index === activeMenuIndex)"
|
||||||
:width="200"
|
:tooltip="item.titleIsCustom ? item.title : t(item.title || '')"
|
||||||
:hide-after="0">
|
:class="isNavItemNotified(item) ? 'notify' : undefined">
|
||||||
<div class="nav-menu-support nav-menu-settings">
|
<i
|
||||||
<div class="nav-menu-support__section">
|
:class="item.icon"
|
||||||
<button type="button" class="nav-menu-settings__item" @click="showChangeLogDialog">
|
class="inline-flex size-6 items-center justify-center text-[19px]" />
|
||||||
<span>{{ t('nav_menu.whats_new') }}</span>
|
<span v-show="!isCollapsed">{{
|
||||||
</button>
|
item.titleIsCustom ? item.title : t(item.title || '')
|
||||||
</div>
|
}}</span>
|
||||||
<Separator />
|
<ChevronRight
|
||||||
<div class="nav-menu-support__section">
|
v-show="!isCollapsed"
|
||||||
<span class="nav-menu-support__title">{{ t('nav_menu.resources') }}</span>
|
class="ml-auto transition-transform"
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('wiki')">
|
:class="open ? 'rotate-90' : ''" />
|
||||||
<span>{{ t('nav_menu.wiki') }}</span>
|
</SidebarMenuButton>
|
||||||
</button>
|
</CollapsibleTrigger>
|
||||||
</div>
|
<CollapsibleContent>
|
||||||
<Separator />
|
<SidebarMenuSub>
|
||||||
<div class="nav-menu-support__section">
|
<SidebarMenuSubItem
|
||||||
<span class="nav-menu-support__title">{{ t('nav_menu.get_help') }}</span>
|
v-for="entry in item.children"
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('github')">
|
:key="entry.index">
|
||||||
<span>{{ t('nav_menu.github') }}</span>
|
<SidebarMenuSubButton
|
||||||
</button>
|
:is-active="activeMenuIndex === entry.index"
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('discord')">
|
@click="handleSubmenuClick(entry, item.index)">
|
||||||
<span>{{ t('nav_menu.discord') }}</span>
|
<i
|
||||||
</button>
|
v-if="entry.icon"
|
||||||
</div>
|
:class="entry.icon"
|
||||||
</div>
|
class="inline-flex size-5 items-center justify-center text-[16px]" />
|
||||||
<template #reference>
|
<span>{{ t(entry.label) }}</span>
|
||||||
<div>
|
</SidebarMenuSubButton>
|
||||||
<TooltipWrapper
|
</SidebarMenuSubItem>
|
||||||
:delay-duration="150"
|
</SidebarMenuSub>
|
||||||
:content="t('nav_tooltip.help_support')"
|
</CollapsibleContent>
|
||||||
side="right"
|
|
||||||
:disabled="!isCollapsed">
|
|
||||||
<div class="bottom-button">
|
|
||||||
<i class="ri-question-line"></i>
|
|
||||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
|
||||||
t('nav_tooltip.help_support')
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</TooltipWrapper>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
|
|
||||||
<el-popover
|
|
||||||
v-model:visible="settingsMenuVisible"
|
|
||||||
placement="right"
|
|
||||||
trigger="click"
|
|
||||||
popper-style="padding:4px;border-radius:8px;"
|
|
||||||
:offset="6"
|
|
||||||
:show-arrow="false"
|
|
||||||
:width="200"
|
|
||||||
:hide-after="0">
|
|
||||||
<div class="nav-menu-settings">
|
|
||||||
<div class="nav-menu-settings__header">
|
|
||||||
<img class="nav-menu-settings__logo" :src="vrcxLogo" alt="VRCX" @click="openGithub" />
|
|
||||||
<div class="nav-menu-settings__meta">
|
|
||||||
<span class="nav-menu-settings__title" @click="openGithub"
|
|
||||||
>VRCX
|
|
||||||
<i class="ri-heart-3-fill nav-menu-settings__heart"></i>
|
|
||||||
</span>
|
|
||||||
<span class="nav-menu-settings__version">{{ version }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator />
|
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSettingsClick">
|
|
||||||
<span>{{ t('nav_tooltip.settings') }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleOpenCustomNavDialog">
|
|
||||||
<span>{{ t('nav_menu.custom_nav.header') }}</span>
|
|
||||||
</button>
|
|
||||||
<el-popover
|
|
||||||
v-model:visible="themeMenuVisible"
|
|
||||||
placement="right-start"
|
|
||||||
trigger="hover"
|
|
||||||
popper-style="padding:4px;border-radius:8px;"
|
|
||||||
:offset="8"
|
|
||||||
:width="200"
|
|
||||||
:hide-after="0">
|
|
||||||
<div class="nav-menu-theme">
|
|
||||||
<button
|
|
||||||
v-for="theme in themes"
|
|
||||||
:key="theme"
|
|
||||||
type="button"
|
|
||||||
class="nav-menu-theme__item"
|
|
||||||
:class="{ 'is-active': themeMode === theme }"
|
|
||||||
@click="handleThemeSelect(theme)">
|
|
||||||
<span class="nav-menu-theme__label">{{ themeDisplayName(theme) }}</span>
|
|
||||||
<span v-if="themeMode === theme" class="nav-menu-theme__check">✓</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<el-popover
|
|
||||||
v-model:visible="themeColorMenuVisible"
|
|
||||||
placement="right-start"
|
|
||||||
trigger="hover"
|
|
||||||
popper-style="padding:4px;border-radius:8px;"
|
|
||||||
:offset="8"
|
|
||||||
:width="200"
|
|
||||||
:show-arrow="false"
|
|
||||||
:hide-after="0"
|
|
||||||
:teleported="false">
|
|
||||||
<div class="nav-menu-theme nav-menu-theme--colors">
|
|
||||||
<button
|
|
||||||
v-for="color in colorFamilies"
|
|
||||||
:key="color.name"
|
|
||||||
type="button"
|
|
||||||
class="nav-menu-theme__item"
|
|
||||||
:class="{ 'is-active': currentPrimary === color.base }"
|
|
||||||
:disabled="isApplyingPrimaryColor"
|
|
||||||
@click="handleThemeColorSelect(color)">
|
|
||||||
<span class="nav-menu-theme__label nav-menu-theme__label--swatch">
|
|
||||||
<span
|
|
||||||
class="nav-menu-theme__swatch"
|
|
||||||
:style="{ backgroundColor: color.base }"></span>
|
|
||||||
<span class="nav-menu-theme__label-text">{{ color.name }}</span>
|
|
||||||
</span>
|
|
||||||
<span v-if="currentPrimary === color.base" class="nav-menu-theme__check">
|
|
||||||
✓
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<template #reference>
|
|
||||||
<button type="button" class="nav-menu-theme__item" @click.prevent>
|
|
||||||
<span class="nav-menu-theme__label">{{
|
|
||||||
t('view.settings.appearance.theme_color.header')
|
|
||||||
}}</span>
|
|
||||||
<span class="nav-menu-settings__arrow">›</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</Collapsible>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</template>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</SidebarContent>
|
||||||
|
|
||||||
|
<SidebarFooter class="p-2">
|
||||||
|
<SidebarMenu>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton :tooltip="t('nav_tooltip.help_support')">
|
||||||
|
<i class="ri-question-line inline-flex size-6 items-center justify-center text-[19px]" />
|
||||||
|
<span v-show="!isCollapsed">{{ t('nav_tooltip.help_support') }}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" align="start" class="w-56">
|
||||||
|
<DropdownMenuItem @click="showChangeLogDialog">
|
||||||
|
<span>{{ t('nav_menu.whats_new') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>{{ t('nav_menu.resources') }}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem @click="handleSupportLink('wiki')">
|
||||||
|
<span>{{ t('nav_menu.wiki') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>{{ t('nav_menu.get_help') }}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem @click="handleSupportLink('github')">
|
||||||
|
<span>{{ t('nav_menu.github') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="handleSupportLink('discord')">
|
||||||
|
<span>{{ t('nav_menu.discord') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<SidebarMenuButton :tooltip="t('nav_tooltip.manage')">
|
||||||
|
<i class="ri-settings-3-line inline-flex size-6 items-center justify-center text-[19px]" />
|
||||||
|
<span v-show="!isCollapsed">{{ t('nav_tooltip.manage') }}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="right" align="start" class="w-64">
|
||||||
|
<div class="flex items-center gap-2 px-2 py-1.5">
|
||||||
|
<img class="h-6 w-6 cursor-pointer" :src="vrcxLogo" alt="VRCX" @click="openGithub" />
|
||||||
|
<div class="flex min-w-0 flex-col">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-left text-sm font-medium truncate"
|
||||||
|
@click="openGithub">
|
||||||
|
VRCX
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-muted-foreground">{{ version }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #reference>
|
<DropdownMenuSeparator />
|
||||||
<button type="button" class="nav-menu-settings__item" @click.prevent>
|
<DropdownMenuItem @click="handleSettingsClick">
|
||||||
|
<span>{{ t('nav_tooltip.settings') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="handleOpenCustomNavDialog">
|
||||||
|
<span>{{ t('nav_menu.custom_nav.header') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
<span>{{ t('view.settings.appearance.appearance.theme_mode') }}</span>
|
<span>{{ t('view.settings.appearance.appearance.theme_mode') }}</span>
|
||||||
<span class="nav-menu-settings__arrow">›</span>
|
</DropdownMenuSubTrigger>
|
||||||
</button>
|
<DropdownMenuSubContent side="right" align="start" class="w-72">
|
||||||
</template>
|
<DropdownMenuItem
|
||||||
</el-popover>
|
v-for="theme in themes"
|
||||||
<button
|
:key="theme"
|
||||||
type="button"
|
:class="themeMode === theme ? 'font-medium' : undefined"
|
||||||
class="nav-menu-settings__item nav-menu-settings__item--danger"
|
@click="handleThemeSelect(theme)">
|
||||||
@click="handleLogoutClick">
|
<span class="flex-1">{{ themeDisplayName(theme) }}</span>
|
||||||
<span>{{ t('dialog.user.actions.logout') }}</span>
|
<span v-if="themeMode === theme" class="text-muted-foreground">✓</span>
|
||||||
</button>
|
</DropdownMenuItem>
|
||||||
</div>
|
<DropdownMenuSeparator />
|
||||||
<template #reference>
|
|
||||||
<div class="bottom-button">
|
<DropdownMenuSub>
|
||||||
<i class="ri-settings-3-line"></i>
|
<DropdownMenuSubTrigger>
|
||||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
<span>{{ t('view.settings.appearance.theme_color.header') }}</span>
|
||||||
t('nav_tooltip.manage')
|
</DropdownMenuSubTrigger>
|
||||||
}}</span>
|
<DropdownMenuSubContent side="right" align="start" class="w-72 max-h-80 overflow-auto">
|
||||||
</div>
|
<DropdownMenuItem
|
||||||
</template>
|
v-for="color in colorFamilies"
|
||||||
</el-popover>
|
:key="color.name"
|
||||||
<TooltipWrapper
|
:disabled="isApplyingPrimaryColor"
|
||||||
:delay-duration="150"
|
@click="handleThemeColorSelect(color)">
|
||||||
:content="t('nav_tooltip.expand_menu')"
|
<span class="flex items-center gap-2 min-w-0 flex-1">
|
||||||
:disabled="!isCollapsed"
|
<span class="h-3 w-3 shrink-0 rounded-sm" :style="{ backgroundColor: color.base }" />
|
||||||
side="right">
|
<span class="truncate">{{ color.name }}</span>
|
||||||
<div class="bottom-button" @click="toggleNavCollapse">
|
</span>
|
||||||
<i class="ri-side-bar-line"></i>
|
<span v-if="currentPrimary === color.base" class="text-muted-foreground">✓</span>
|
||||||
<span v-show="!isCollapsed" class="bottom-button__label">{{
|
</DropdownMenuItem>
|
||||||
t('nav_tooltip.collapse_menu')
|
</DropdownMenuSubContent>
|
||||||
}}</span>
|
</DropdownMenuSub>
|
||||||
</div>
|
</DropdownMenuSubContent>
|
||||||
</TooltipWrapper>
|
</DropdownMenuSub>
|
||||||
</div>
|
|
||||||
</template>
|
<DropdownMenuSeparator />
|
||||||
</div>
|
<DropdownMenuItem variant="destructive" @click="handleLogoutClick">
|
||||||
|
<span>{{ t('dialog.user.actions.logout') }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton
|
||||||
|
:tooltip="isCollapsed ? t('nav_tooltip.expand_menu') : t('nav_tooltip.collapse_menu')"
|
||||||
|
@click="toggleNavCollapse">
|
||||||
|
<i class="ri-side-bar-line inline-flex size-6 items-center justify-center text-[19px]" />
|
||||||
|
<span v-show="!isCollapsed">{{ t('nav_tooltip.collapse_menu') }}</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarFooter>
|
||||||
|
|
||||||
|
<SidebarRail />
|
||||||
|
</Sidebar>
|
||||||
|
|
||||||
<CustomNavDialog
|
<CustomNavDialog
|
||||||
v-model:visible="customNavDialogVisible"
|
v-model:visible="customNavDialogVisible"
|
||||||
:layout="navLayout"
|
:layout="navLayout"
|
||||||
@@ -244,15 +213,41 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
|
||||||
import { ElMenu, ElMenuItem, ElPopover, ElSubMenu } from 'element-plus';
|
import dayjs from 'dayjs';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { dayjs } from 'element-plus';
|
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { ChevronRight } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarFooter,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuButton,
|
||||||
|
SidebarMenuItem,
|
||||||
|
SidebarMenuSub,
|
||||||
|
SidebarMenuSubButton,
|
||||||
|
SidebarMenuSubItem,
|
||||||
|
SidebarRail
|
||||||
|
} from '@/components/ui/sidebar';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
useAuthStore,
|
useAuthStore,
|
||||||
@@ -314,11 +309,6 @@
|
|||||||
const { logout } = useAuthStore();
|
const { logout } = useAuthStore();
|
||||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore);
|
const { themeMode, isNavCollapsed: isCollapsed } = storeToRefs(appearanceSettingsStore);
|
||||||
const settingsMenuVisible = ref(false);
|
|
||||||
const themeMenuVisible = ref(false);
|
|
||||||
const themeColorMenuVisible = ref(false);
|
|
||||||
const supportMenuVisible = ref(false);
|
|
||||||
const navMenuRef = ref(null);
|
|
||||||
const navLayout = ref([]);
|
const navLayout = ref([]);
|
||||||
const navLayoutReady = ref(false);
|
const navLayoutReady = ref(false);
|
||||||
|
|
||||||
@@ -407,16 +397,6 @@
|
|||||||
selectPaletteColor
|
selectPaletteColor
|
||||||
} = useThemePrimaryColor();
|
} = useThemePrimaryColor();
|
||||||
|
|
||||||
watch(
|
|
||||||
() => activeMenuIndex.value,
|
|
||||||
(value) => {
|
|
||||||
if (value) {
|
|
||||||
navMenuRef.value?.updateActiveIndex(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => locale.value,
|
() => locale.value,
|
||||||
() => {
|
() => {
|
||||||
@@ -504,28 +484,19 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingsClick = () => {
|
const handleSettingsClick = () => {
|
||||||
themeMenuVisible.value = false;
|
|
||||||
supportMenuVisible.value = false;
|
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
router.push({ name: 'settings' });
|
router.push({ name: 'settings' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
logout();
|
logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleThemeSelect = (theme) => {
|
const handleThemeSelect = (theme) => {
|
||||||
themeMenuVisible.value = false;
|
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
appearanceSettingsStore.setThemeMode(theme);
|
appearanceSettingsStore.setThemeMode(theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleThemeColorSelect = async (colorFamily) => {
|
const handleThemeColorSelect = async (colorFamily) => {
|
||||||
await selectPaletteColor(colorFamily);
|
await selectPaletteColor(colorFamily);
|
||||||
themeColorMenuVisible.value = false;
|
|
||||||
themeMenuVisible.value = false;
|
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openGithub = () => {
|
const openGithub = () => {
|
||||||
@@ -548,9 +519,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenCustomNavDialog = () => {
|
const handleOpenCustomNavDialog = () => {
|
||||||
themeMenuVisible.value = false;
|
|
||||||
supportMenuVisible.value = false;
|
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
customNavDialogVisible.value = true;
|
customNavDialogVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -602,7 +570,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSupportLink = (id) => {
|
const handleSupportLink = (id) => {
|
||||||
supportMenuVisible.value = false;
|
|
||||||
const target = links[id];
|
const target = links[id];
|
||||||
if (target) {
|
if (target) {
|
||||||
openExternalLink(target);
|
openExternalLink(target);
|
||||||
@@ -639,39 +606,23 @@
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeNavFlyouts = () => {
|
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
supportMenuVisible.value = false;
|
|
||||||
themeMenuVisible.value = false;
|
|
||||||
themeColorMenuVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerNavAction = (entry, navIndex = entry?.index) => {
|
const triggerNavAction = (entry, navIndex = entry?.index) => {
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.action === 'direct-access') {
|
if (entry.action === 'direct-access') {
|
||||||
closeNavFlyouts();
|
|
||||||
directAccessPaste();
|
directAccessPaste();
|
||||||
if (navIndex) {
|
|
||||||
navMenuRef.value?.updateActiveIndex(navIndex);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.routeName) {
|
if (entry.routeName) {
|
||||||
handleRouteChange(entry.routeName, navIndex);
|
handleRouteChange(entry.routeName, navIndex);
|
||||||
closeNavFlyouts();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.path) {
|
if (entry.path) {
|
||||||
router.push(entry.path);
|
router.push(entry.path);
|
||||||
if (navIndex) {
|
|
||||||
navMenuRef.value?.updateActiveIndex(navIndex);
|
|
||||||
}
|
|
||||||
closeNavFlyouts();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -680,26 +631,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push({ name: routeName });
|
router.push({ name: routeName });
|
||||||
if (navIndex) {
|
|
||||||
navMenuRef.value?.updateActiveIndex(navIndex);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(settingsMenuVisible, (visible) => {
|
|
||||||
if (visible) {
|
|
||||||
supportMenuVisible.value = false;
|
|
||||||
} else {
|
|
||||||
themeMenuVisible.value = false;
|
|
||||||
themeColorMenuVisible.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(supportMenuVisible, (visible) => {
|
|
||||||
if (visible) {
|
|
||||||
settingsMenuVisible.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getFirstNavRoute(layout) {
|
function getFirstNavRoute(layout) {
|
||||||
for (const entry of layout) {
|
for (const entry of layout) {
|
||||||
if (entry.type === 'item') {
|
if (entry.type === 'item') {
|
||||||
@@ -759,261 +692,6 @@
|
|||||||
min-width: 64px;
|
min-width: 64px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: flex-start;
|
|
||||||
background-color: var(--el-bg-color-page);
|
|
||||||
box-shadow: none;
|
|
||||||
backdrop-filter: blur(14px) saturate(130%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden auto;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu {
|
|
||||||
background: transparent;
|
|
||||||
border: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu :deep(.el-menu-item),
|
|
||||||
.nav-menu :deep(.el-sub-menu__title) {
|
|
||||||
height: 46px;
|
|
||||||
line-height: 46px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
column-gap: 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 0 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu :deep(.el-menu-item i[class*='ri-']),
|
|
||||||
.nav-menu :deep(.el-sub-menu__title i[class*='ri-']) {
|
|
||||||
font-size: 19px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: 1;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu :deep(.el-sub-menu__title > div) {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu :deep(.el-sub-menu__icon-arrow) {
|
|
||||||
right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-button {
|
|
||||||
font-size: 19px;
|
|
||||||
width: 100%;
|
|
||||||
height: 46px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 0 20px;
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: middle;
|
|
||||||
cursor: pointer;
|
|
||||||
box-sizing: border-box;
|
|
||||||
& > span {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-button i {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-button__label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-button:hover {
|
|
||||||
background-color: var(--el-menu-hover-bg-color);
|
|
||||||
transition:
|
|
||||||
border-color var(--el-transition-duration),
|
|
||||||
background-color var(--el-transition-duration),
|
|
||||||
color var(--el-transition-duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container-bottom {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-menu-item),
|
|
||||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title) {
|
|
||||||
column-gap: 0;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container.is-collapsed {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container.is-collapsed .nav-menu :deep(.el-sub-menu__title > div) {
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container.is-collapsed .bottom-button {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item .el-menu-tooltip__trigger) {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-button.is-text:not(.is-disabled):hover) {
|
|
||||||
background-color: var(--el-menu-hover-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-settings {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
.nav-menu-settings__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 8px 10px;
|
|
||||||
.nav-menu-settings__logo {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
object-fit: contain;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-settings__meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.nav-menu-settings__title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-settings__heart {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--el-color-success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-settings__version {
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.nav-menu-settings__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding: 8px 12px;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--el-text-color-regular);
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background-color var(--el-transition-duration);
|
|
||||||
cursor: pointer;
|
|
||||||
.nav-menu-settings__arrow {
|
|
||||||
margin-left: auto;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.nav-menu-settings__item:hover {
|
|
||||||
background-color: var(--el-menu-hover-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-settings__item--danger {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-settings__item--danger:hover {
|
|
||||||
background-color: color-mix(in oklch, var(--el-color-danger) 18%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-support {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.nav-menu-support__section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-support__title {
|
|
||||||
padding: 0 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-theme {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
.nav-menu-theme__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: background-color var(--el-transition-duration);
|
|
||||||
cursor: pointer;
|
|
||||||
.nav-menu-theme__check {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.nav-menu-theme__item:hover,
|
|
||||||
.nav-menu-theme__item.is-active {
|
|
||||||
background-color: var(--el-menu-hover-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-theme__label--swatch {
|
.nav-menu-theme__label--swatch {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -22,6 +22,14 @@
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: undefined
|
default: undefined
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
widthIcon: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
class: {
|
class: {
|
||||||
type: [String, Array, Object],
|
type: [String, Array, Object],
|
||||||
default: undefined
|
default: undefined
|
||||||
@@ -33,10 +41,18 @@
|
|||||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||||
const openMobile = ref(false);
|
const openMobile = ref(false);
|
||||||
|
|
||||||
const open = useVModel(props, 'open', emits, {
|
let open;
|
||||||
defaultValue: props.defaultOpen ?? false,
|
if (props.open === undefined) {
|
||||||
passive: props.open === undefined
|
open = useVModel(props, 'open', emits, {
|
||||||
});
|
defaultValue: props.defaultOpen ?? false,
|
||||||
|
passive: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
open = useVModel(props, 'open', emits, {
|
||||||
|
defaultValue: props.defaultOpen ?? false,
|
||||||
|
passive: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setOpen(value) {
|
function setOpen(value) {
|
||||||
open.value = value; // emits('update:open', value)
|
open.value = value; // emits('update:open', value)
|
||||||
@@ -65,6 +81,23 @@
|
|||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
// This makes it easier to style the sidebar with Tailwind classes.
|
||||||
const state = computed(() => (open.value ? 'expanded' : 'collapsed'));
|
const state = computed(() => (open.value ? 'expanded' : 'collapsed'));
|
||||||
|
|
||||||
|
const normalizeCssSize = (value) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return `${value}px`;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed || undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssSidebarWidth = computed(() => normalizeCssSize(props.width) ?? SIDEBAR_WIDTH);
|
||||||
|
const cssSidebarWidthIcon = computed(() => normalizeCssSize(props.widthIcon) ?? SIDEBAR_WIDTH_ICON);
|
||||||
|
|
||||||
provideSidebarContext({
|
provideSidebarContext({
|
||||||
state,
|
state,
|
||||||
open,
|
open,
|
||||||
@@ -81,8 +114,8 @@
|
|||||||
<div
|
<div
|
||||||
data-slot="sidebar-wrapper"
|
data-slot="sidebar-wrapper"
|
||||||
:style="{
|
:style="{
|
||||||
'--sidebar-width': SIDEBAR_WIDTH,
|
'--sidebar-width': cssSidebarWidth,
|
||||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON
|
'--sidebar-width-icon': cssSidebarWidthIcon
|
||||||
}"
|
}"
|
||||||
:class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
|
:class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
|
||||||
v-bind="$attrs">
|
v-bind="$attrs">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
import { useAppearanceSettingsStore } from '../stores';
|
import { useAppearanceSettingsStore } from '../stores';
|
||||||
@@ -6,16 +6,11 @@ import { useAppearanceSettingsStore } from '../stores';
|
|||||||
import configRepository from '../service/config';
|
import configRepository from '../service/config';
|
||||||
|
|
||||||
export function useAuthenticatedLayoutResizable() {
|
export function useAuthenticatedLayoutResizable() {
|
||||||
const navCollapsedPx = 64;
|
|
||||||
const navDefaultPx = 240;
|
|
||||||
const navMinPx = 180;
|
|
||||||
const navMaxPx = 300;
|
|
||||||
const asideMaxPx = 500;
|
const asideMaxPx = 500;
|
||||||
|
|
||||||
const appearanceStore = useAppearanceSettingsStore();
|
const appearanceStore = useAppearanceSettingsStore();
|
||||||
const { setAsideWidth, setNavWidth } = appearanceStore;
|
const { setAsideWidth } = appearanceStore;
|
||||||
const { asideWidth, isNavCollapsed, isSideBarTabShow, navWidth } =
|
const { asideWidth, isSideBarTabShow, isNavCollapsed } = storeToRefs(appearanceStore);
|
||||||
storeToRefs(appearanceStore);
|
|
||||||
|
|
||||||
const fallbackWidth =
|
const fallbackWidth =
|
||||||
typeof window !== 'undefined' && window.innerWidth
|
typeof window !== 'undefined' && window.innerWidth
|
||||||
@@ -23,9 +18,7 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
: 1200;
|
: 1200;
|
||||||
|
|
||||||
const panelGroupRef = ref(null);
|
const panelGroupRef = ref(null);
|
||||||
const navPanelRef = ref(null);
|
|
||||||
const asidePanelRef = ref(null);
|
const asidePanelRef = ref(null);
|
||||||
const navExpandedSize = ref(null);
|
|
||||||
const groupWidth = ref(fallbackWidth);
|
const groupWidth = ref(fallbackWidth);
|
||||||
const draggingCount = ref(0);
|
const draggingCount = ref(0);
|
||||||
let resizeObserver = null;
|
let resizeObserver = null;
|
||||||
@@ -70,27 +63,7 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
|
const percentToPx = (percent, groupWidth) => (percent / 100) * groupWidth;
|
||||||
|
|
||||||
const isAsideCollapsed = (layout) =>
|
const isAsideCollapsed = (layout) =>
|
||||||
Array.isArray(layout) &&
|
Array.isArray(layout) && layout.length >= 2 && layout[layout.length - 1] <= 1;
|
||||||
layout.length >= 3 &&
|
|
||||||
layout[layout.length - 1] <= 1;
|
|
||||||
|
|
||||||
const navCollapsedSize = computed(() =>
|
|
||||||
pxToPercent(navCollapsedPx, groupWidth.value)
|
|
||||||
);
|
|
||||||
const navExpandedPx = computed(() => navWidth.value || navDefaultPx);
|
|
||||||
|
|
||||||
const navDefaultSize = computed(() =>
|
|
||||||
isNavCollapsed.value
|
|
||||||
? navCollapsedSize.value
|
|
||||||
: pxToPercent(navExpandedPx.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
const navMinSize = computed(() =>
|
|
||||||
isNavCollapsed.value ? navCollapsedSize.value : pxToPercent(navMinPx)
|
|
||||||
);
|
|
||||||
const navMaxSize = computed(() =>
|
|
||||||
isNavCollapsed.value ? navCollapsedSize.value : pxToPercent(navMaxPx)
|
|
||||||
);
|
|
||||||
|
|
||||||
const asideDefaultSize = computed(() =>
|
const asideDefaultSize = computed(() =>
|
||||||
pxToPercent(asideWidth.value, undefined, 0)
|
pxToPercent(asideWidth.value, undefined, 0)
|
||||||
@@ -98,14 +71,11 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
const asideMaxSize = computed(() => pxToPercent(asideMaxPx, undefined, 0));
|
const asideMaxSize = computed(() => pxToPercent(asideMaxPx, undefined, 0));
|
||||||
|
|
||||||
const mainDefaultSize = computed(
|
const mainDefaultSize = computed(
|
||||||
() =>
|
() => 100 - (isSideBarTabShow.value ? asideDefaultSize.value : 0)
|
||||||
100 -
|
|
||||||
navDefaultSize.value -
|
|
||||||
(isSideBarTabShow.value ? asideDefaultSize.value : 0)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleLayout = (sizes) => {
|
const handleLayout = (sizes) => {
|
||||||
if (!Array.isArray(sizes) || sizes.length < 2) {
|
if (!Array.isArray(sizes) || sizes.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,22 +83,16 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSideBarTabShow.value || sizes.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rawWidth = getGroupWidthRaw();
|
const rawWidth = getGroupWidthRaw();
|
||||||
if (!Number.isFinite(rawWidth) || rawWidth <= 0) {
|
if (!Number.isFinite(rawWidth) || rawWidth <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const width = rawWidth;
|
const width = rawWidth;
|
||||||
|
|
||||||
const navSize = sizes[0];
|
|
||||||
if (!isNavCollapsed.value && Number.isFinite(navSize) && navSize > 0) {
|
|
||||||
navExpandedSize.value = navSize;
|
|
||||||
setNavWidth(Math.round(percentToPx(navSize, width)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSideBarTabShow.value || sizes.length < 3) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const asideSize = sizes[sizes.length - 1];
|
const asideSize = sizes[sizes.length - 1];
|
||||||
if (!Number.isFinite(asideSize)) {
|
if (!Number.isFinite(asideSize)) {
|
||||||
return;
|
return;
|
||||||
@@ -142,9 +106,6 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
setAsideWidth(Math.round(percentToPx(asideSize, width)));
|
setAsideWidth(Math.round(percentToPx(asideSize, width)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const resizeNavPanel = (targetSize) =>
|
|
||||||
navPanelRef.value?.resize?.(targetSize);
|
|
||||||
|
|
||||||
const resizeAsidePanel = (targetSize) =>
|
const resizeAsidePanel = (targetSize) =>
|
||||||
asidePanelRef.value?.resize?.(targetSize);
|
asidePanelRef.value?.resize?.(targetSize);
|
||||||
|
|
||||||
@@ -152,14 +113,6 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
const width = getGroupWidth();
|
const width = getGroupWidth();
|
||||||
groupWidth.value = width;
|
groupWidth.value = width;
|
||||||
|
|
||||||
if (isNavCollapsed.value) {
|
|
||||||
resizeNavPanel(navCollapsedSize.value);
|
|
||||||
} else {
|
|
||||||
const targetSize = pxToPercent(navExpandedPx.value, width);
|
|
||||||
navExpandedSize.value = targetSize;
|
|
||||||
resizeNavPanel(targetSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSideBarTabShow.value) {
|
if (!isSideBarTabShow.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -194,27 +147,9 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(isNavCollapsed, async (collapsed) => {
|
|
||||||
await nextTick();
|
|
||||||
if (collapsed) {
|
|
||||||
resizeNavPanel(navCollapsedSize.value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const targetSize =
|
|
||||||
navExpandedSize.value ?? pxToPercent(navExpandedPx.value);
|
|
||||||
resizeNavPanel(targetSize);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateGroupWidth();
|
updateGroupWidth();
|
||||||
let panelSize = null;
|
|
||||||
panelSize = navPanelRef.value?.getSize?.() ?? null;
|
|
||||||
|
|
||||||
navExpandedSize.value = panelSize ?? navDefaultSize.value;
|
|
||||||
if (isNavCollapsed.value) {
|
|
||||||
resizeNavPanel(navCollapsedSize.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = panelGroupRef.value?.$el ?? panelGroupRef.value;
|
const element = panelGroupRef.value?.$el ?? panelGroupRef.value;
|
||||||
if (element && typeof ResizeObserver !== 'undefined') {
|
if (element && typeof ResizeObserver !== 'undefined') {
|
||||||
@@ -232,11 +167,7 @@ export function useAuthenticatedLayoutResizable() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
panelGroupRef,
|
panelGroupRef,
|
||||||
navPanelRef,
|
|
||||||
asidePanelRef,
|
asidePanelRef,
|
||||||
navDefaultSize,
|
|
||||||
navMinSize,
|
|
||||||
navMaxSize,
|
|
||||||
asideDefaultSize,
|
asideDefaultSize,
|
||||||
asideMaxSize,
|
asideMaxSize,
|
||||||
mainDefaultSize,
|
mainDefaultSize,
|
||||||
|
|||||||
@@ -1,46 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<template v-if="watchState.isLoggedIn">
|
<template v-if="watchState.isLoggedIn">
|
||||||
<ResizablePanelGroup
|
<SidebarProvider
|
||||||
ref="panelGroupRef"
|
:open="sidebarOpen"
|
||||||
direction="horizontal"
|
:width="navWidth"
|
||||||
class="group/main-layout flex-1 h-full min-w-0"
|
width-icon="64"
|
||||||
@layout="handleLayout">
|
class="relative flex-1 h-full min-w-0"
|
||||||
<template #default="{ layout }">
|
@update:open="handleSidebarOpenChange">
|
||||||
<ResizablePanel ref="navPanelRef" :min-size="navMinSize" :max-size="navMaxSize" :order="1">
|
<NavMenu />
|
||||||
<NavMenu></NavMenu>
|
|
||||||
</ResizablePanel>
|
|
||||||
<ResizableHandle
|
|
||||||
:disabled="isNavCollapsed"
|
|
||||||
class="opacity-0"
|
|
||||||
@dragging="setIsDragging"></ResizableHandle>
|
|
||||||
<ResizablePanel :default-size="mainDefaultSize" :order="2">
|
|
||||||
<RouterView v-slot="{ Component }">
|
|
||||||
<KeepAlive exclude="Charts">
|
|
||||||
<component :is="Component" />
|
|
||||||
</KeepAlive>
|
|
||||||
</RouterView>
|
|
||||||
</ResizablePanel>
|
|
||||||
|
|
||||||
<template v-if="isSideBarTabShow">
|
<div
|
||||||
<ResizableHandle
|
v-show="sidebarOpen"
|
||||||
with-handle
|
class="absolute top-0 bottom-0 z-30 w-1 cursor-col-resize select-none hover:bg-border/60"
|
||||||
:class="[
|
:style="{ left: 'var(--sidebar-width)' }"
|
||||||
isAsideCollapsed(layout) ? 'opacity-100' : 'opacity-0',
|
@pointerdown.prevent="startNavResize" />
|
||||||
'z-20 [&>div]:-translate-x-1/2'
|
|
||||||
]"
|
<SidebarInset class="min-w-0">
|
||||||
@dragging="setIsDragging"></ResizableHandle>
|
<ResizablePanelGroup
|
||||||
<ResizablePanel
|
ref="panelGroupRef"
|
||||||
ref="asidePanelRef"
|
direction="horizontal"
|
||||||
:default-size="asideDefaultSize"
|
class="group/main-layout flex-1 h-full min-w-0"
|
||||||
:max-size="asideMaxSize"
|
@layout="handleLayout">
|
||||||
:collapsed-size="0"
|
<template #default="{ layout }">
|
||||||
collapsible
|
<ResizablePanel :default-size="mainDefaultSize" :order="1">
|
||||||
:order="3">
|
<RouterView v-slot="{ Component }">
|
||||||
<Sidebar></Sidebar>
|
<KeepAlive exclude="Charts">
|
||||||
</ResizablePanel>
|
<component :is="Component" />
|
||||||
</template>
|
</KeepAlive>
|
||||||
</template>
|
</RouterView>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanel>
|
||||||
|
|
||||||
|
<template v-if="isSideBarTabShow">
|
||||||
|
<ResizableHandle
|
||||||
|
with-handle
|
||||||
|
:class="[
|
||||||
|
isAsideCollapsed(layout) ? 'opacity-100' : 'opacity-0',
|
||||||
|
'z-20 [&>div]:-translate-x-1/2'
|
||||||
|
]"
|
||||||
|
@dragging="setIsDragging"></ResizableHandle>
|
||||||
|
<ResizablePanel
|
||||||
|
ref="asidePanelRef"
|
||||||
|
:default-size="asideDefaultSize"
|
||||||
|
:max-size="asideMaxSize"
|
||||||
|
:collapsed-size="0"
|
||||||
|
collapsible
|
||||||
|
:order="2">
|
||||||
|
<Sidebar></Sidebar>
|
||||||
|
</ResizablePanel>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
|
||||||
<!-- ## Dialogs ## -->
|
<!-- ## Dialogs ## -->
|
||||||
<UserDialog></UserDialog>
|
<UserDialog></UserDialog>
|
||||||
@@ -82,10 +92,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, onUnmounted, watch } from 'vue';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { watch } from 'vue';
|
|
||||||
|
|
||||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable';
|
||||||
|
import { SidebarInset, SidebarProvider } from '../../components/ui/sidebar';
|
||||||
|
import { useAppearanceSettingsStore } from '../../stores';
|
||||||
import { useAuthenticatedLayoutResizable } from '../../composables/useAuthenticatedLayoutResizable';
|
import { useAuthenticatedLayoutResizable } from '../../composables/useAuthenticatedLayoutResizable';
|
||||||
import { watchState } from '../../service/watchState';
|
import { watchState } from '../../service/watchState';
|
||||||
|
|
||||||
@@ -112,19 +125,64 @@
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||||
|
const { navWidth, isNavCollapsed } = storeToRefs(appearanceSettingsStore);
|
||||||
|
|
||||||
|
const sidebarOpen = computed(() => !isNavCollapsed.value);
|
||||||
|
|
||||||
|
const handleSidebarOpenChange = (open) => {
|
||||||
|
appearanceSettingsStore.setNavCollapsed(!open);
|
||||||
|
};
|
||||||
|
|
||||||
|
let isResizingNav = false;
|
||||||
|
let cleanupNavResize = null;
|
||||||
|
|
||||||
|
const startNavResize = (event) => {
|
||||||
|
if (!sidebarOpen.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isResizingNav = true;
|
||||||
|
const prevUserSelect = document.body.style.userSelect;
|
||||||
|
const prevCursor = document.body.style.cursor;
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
document.body.style.cursor = 'col-resize';
|
||||||
|
|
||||||
|
const handleMove = (e) => {
|
||||||
|
if (!isResizingNav) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appearanceSettingsStore.setNavWidth(e.clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUp = () => {
|
||||||
|
isResizingNav = false;
|
||||||
|
document.body.style.userSelect = prevUserSelect;
|
||||||
|
document.body.style.cursor = prevCursor;
|
||||||
|
window.removeEventListener('pointermove', handleMove);
|
||||||
|
window.removeEventListener('pointerup', handleUp);
|
||||||
|
cleanupNavResize = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handleMove);
|
||||||
|
window.addEventListener('pointerup', handleUp);
|
||||||
|
cleanupNavResize = handleUp;
|
||||||
|
appearanceSettingsStore.setNavWidth(event.clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupNavResize?.();
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
panelGroupRef,
|
panelGroupRef,
|
||||||
navPanelRef,
|
|
||||||
asidePanelRef,
|
asidePanelRef,
|
||||||
navMinSize,
|
|
||||||
navMaxSize,
|
|
||||||
asideDefaultSize,
|
asideDefaultSize,
|
||||||
asideMaxSize,
|
asideMaxSize,
|
||||||
mainDefaultSize,
|
mainDefaultSize,
|
||||||
handleLayout,
|
handleLayout,
|
||||||
setIsDragging,
|
setIsDragging,
|
||||||
isAsideCollapsed,
|
isAsideCollapsed,
|
||||||
isNavCollapsed,
|
|
||||||
isSideBarTabShow
|
isSideBarTabShow
|
||||||
} = useAuthenticatedLayoutResizable();
|
} = useAuthenticatedLayoutResizable();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"lib": ["esnext", "dom", "dom.iterable"],
|
"lib": ["esnext", "dom", "dom.iterable"],
|
||||||
"types": ["vite/client", "element-plus/global"],
|
"types": ["vite/client"],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
Reference in New Issue
Block a user