wip: layouts / settings / utils / stores / modules / components / apis / types
parent
a4b1c68eec
commit
256884d15b
@ -1,15 +1,63 @@
|
||||
{
|
||||
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"cSpell.words": [
|
||||
"antfu",
|
||||
"demi",
|
||||
"iconify",
|
||||
"intlify",
|
||||
"persistedstate",
|
||||
"pinia",
|
||||
"pnpm",
|
||||
"Sider",
|
||||
"unocss",
|
||||
"unplugin",
|
||||
"unref",
|
||||
"Vite",
|
||||
"vitejs",
|
||||
"Vitesse",
|
||||
"vitest",
|
||||
"vmodel",
|
||||
"vueuse"
|
||||
],
|
||||
"i18n-ally.sourceLanguage": "zh-CN",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": "locales",
|
||||
"i18n-ally.sortKeys": true,
|
||||
// Enable the ESlint flat config support
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off" },
|
||||
{ "rule": "*-indent", "severity": "off" },
|
||||
{ "rule": "*-spacing", "severity": "off" },
|
||||
{ "rule": "*-spaces", "severity": "off" },
|
||||
{ "rule": "*-order", "severity": "off" },
|
||||
{ "rule": "*-dangle", "severity": "off" },
|
||||
{ "rule": "*-newline", "severity": "off" },
|
||||
{ "rule": "*quotes", "severity": "off" },
|
||||
{ "rule": "*semi", "severity": "off" }
|
||||
],
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
],
|
||||
"files.associations": {
|
||||
"*.css": "postcss"
|
||||
},
|
||||
"editor.formatOnSave": false
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
export default () => {
|
||||
// https://github.com/antfu/unplugin-auto-import
|
||||
return AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'vue-i18n',
|
||||
'vue/macros',
|
||||
'@vueuse/head',
|
||||
'@vueuse/core',
|
||||
{
|
||||
'naive-ui': [
|
||||
'useDialog',
|
||||
'useMessage',
|
||||
'useNotification',
|
||||
'useLoadingBar',
|
||||
],
|
||||
},
|
||||
],
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
dirs: [
|
||||
'src/composables',
|
||||
'src/stores',
|
||||
'src/types',
|
||||
'src/api/**',
|
||||
],
|
||||
vueTemplate: true,
|
||||
resolvers: [
|
||||
NaiveUiResolver(),
|
||||
],
|
||||
})
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import ViteCompression from 'vite-plugin-compression'
|
||||
|
||||
export default (viteEnv: ImportMetaEnv) => {
|
||||
const { VITE_COMPRESS_TYPE = 'gzip' } = viteEnv
|
||||
return ViteCompression({ algorithm: VITE_COMPRESS_TYPE })
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
import type { PluginOption } from 'vite'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
import Pages from 'vite-plugin-pages'
|
||||
import WebfontDownload from 'vite-plugin-webfont-dl'
|
||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||
import Unocss from 'unocss/vite'
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
|
||||
// must
|
||||
import { getRootPath } from 'build/utils'
|
||||
import vueMacros from './vuemacros'
|
||||
import autoImport from './autoimport'
|
||||
import unplugins from './unplugins'
|
||||
|
||||
// select
|
||||
import visualizer from './visualizer'
|
||||
import compress from './compress'
|
||||
import pwa from './pwa'
|
||||
import markdown from './markdown'
|
||||
|
||||
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
||||
const plugins = [
|
||||
vueMacros(),
|
||||
autoImport(),
|
||||
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
|
||||
Layouts(),
|
||||
// https://github.com/hannoeru/vite-plugin-pages
|
||||
Pages({
|
||||
extensions: ['vue', 'md'],
|
||||
}),
|
||||
// https://github.com/feat-agency/vite-plugin-webfont-dl
|
||||
WebfontDownload(),
|
||||
// https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||
VueDevTools(),
|
||||
// https://github.com/antfu/unocss
|
||||
// see uno.config.ts for config
|
||||
Unocss(),
|
||||
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
|
||||
VueI18n({
|
||||
runtimeOnly: true,
|
||||
compositionOnly: true,
|
||||
fullInstall: true,
|
||||
include: [`${getRootPath()}/locales/**`],
|
||||
}),
|
||||
...unplugins(viteEnv),
|
||||
]
|
||||
if (viteEnv.VITE_VISUALIZER)
|
||||
plugins.push(visualizer as PluginOption)
|
||||
|
||||
if (viteEnv.VITE_COMPRESS)
|
||||
plugins.push(compress(viteEnv))
|
||||
|
||||
if (viteEnv.VITE_PWA)
|
||||
plugins.push(pwa())
|
||||
|
||||
if (viteEnv.VITE_MARKDOWN)
|
||||
plugins.push(markdown(viteEnv))
|
||||
|
||||
return plugins
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import Markdown from 'vite-plugin-vue-markdown'
|
||||
import LinkAttributes from 'markdown-it-link-attributes'
|
||||
import Shiki from 'markdown-it-shiki'
|
||||
|
||||
export default (viteEnv: ImportMetaEnv) => {
|
||||
// https://github.com/antfu/vite-plugin-vue-markdown
|
||||
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
|
||||
return Markdown({
|
||||
wrapperClasses: 'prose prose-sm m-auto text-left',
|
||||
headEnabled: true,
|
||||
markdownItSetup(md) {
|
||||
// https://prismjs.com/
|
||||
md.use(Shiki, {
|
||||
theme: {
|
||||
light: 'vitesse-light',
|
||||
dark: 'vitesse-dark',
|
||||
},
|
||||
})
|
||||
md.use(LinkAttributes, {
|
||||
matcher: (link: string) => /^https?:\/\//.test(link),
|
||||
attrs: {
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export default function setupVitePwa() {
|
||||
// https://github.com/antfu/vite-plugin-pwa
|
||||
return VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favicon.svg', 'safari-pinned-tab.svg'],
|
||||
manifest: {
|
||||
name: 'N-Admin',
|
||||
short_name: 'N-Admin',
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: '/pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: '/pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
export default (viteEnv: ImportMetaEnv) => {
|
||||
return [
|
||||
// https://github.com/antfu/unplugin-vue-components
|
||||
Components({
|
||||
// allow auto load markdown components under `./src/components/`
|
||||
extensions: ['vue', 'md'],
|
||||
// allow auto import and register components used in markdown
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
dts: 'src/components.d.ts',
|
||||
resolvers: [
|
||||
NaiveUiResolver(),
|
||||
],
|
||||
}),
|
||||
]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
||||
export default visualizer({
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
open: true,
|
||||
})
|
@ -1,23 +0,0 @@
|
||||
import { transformShortVmodel } from '@vue-macros/short-vmodel'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
|
||||
// @ts-expect-error failed to resolve types
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
|
||||
export default () => {
|
||||
return VueMacros({
|
||||
plugins: {
|
||||
vue: Vue({
|
||||
include: [/\.vue$/, /\.md$/],
|
||||
reactivityTransform: true,
|
||||
template: {
|
||||
compilerOptions: {
|
||||
nodeTransforms: [
|
||||
transformShortVmodel({ prefix: '::' }),
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import unocss from '@unocss/eslint-plugin'
|
||||
|
||||
export default antfu(
|
||||
{
|
||||
languageOptions: {},
|
||||
rules: {
|
||||
'curly': 'off',
|
||||
'ts/prefer-literal-enum-member': ['off', {
|
||||
allowBitwiseExpressions: true,
|
||||
}],
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
unocss.configs.flat,
|
||||
)
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 johncampionjr
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
declare module 'layouts-generated' {
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export function setupLayouts(routes: RouteRecordRaw[]): RouteRecordRaw[]
|
||||
}
|
||||
|
||||
declare module 'virtual:generated-layouts' {
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export function setupLayouts(routes: RouteRecordRaw[]): RouteRecordRaw[]
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "vite-plugin-vue-layouts",
|
||||
"version": "0.8.0",
|
||||
"description": "Router based layout plugin for Vite and Vue",
|
||||
"author": "johncampionjr <npm@relate.dev>",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/johncampionjr/vite-plugin-vue-layouts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/johncampionjr/vite-plugin-vue-layouts"
|
||||
},
|
||||
"bugs": "https://github.com/johncampionjr/vite-plugin-vue-layouts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./*": "./*"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"client.d.ts",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "npm run build -- --watch",
|
||||
"build": "tsup src/index.ts --dts --format cjs,esm && cp dist/index.d.ts dist/index.d.mts",
|
||||
"prepublishOnly": "npm run build",
|
||||
"release": "npx bumpp --commit --tag --push && npm publish",
|
||||
"example:dev": "npm -C examples/spa run dev",
|
||||
"example:build": "npm -C examples/spa run build",
|
||||
"example:serve": "npm -C examples/spa run serve",
|
||||
"example:build-ssg": "npm -C examples/ssg run build",
|
||||
"example:serve-ssg": "npm -C examples/ssg run serve"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^2.5.0 || ^3.0.0-0 || ^4.0.0",
|
||||
"vue": "^2.6.12 || ^3.2.4",
|
||||
"vue-router": "^3.5.1 || ^4.0.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^4.3.3",
|
||||
"fast-glob": "^3.2.11",
|
||||
"local-pkg": "^0.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.7.0",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^16.11.26",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.32.0",
|
||||
"rollup": "^2.68.0",
|
||||
"tsup": "^4.14.0",
|
||||
"typescript": "^4.6.2",
|
||||
"vite": "^2.8.6",
|
||||
"vue": "^3.2.31",
|
||||
"vue-router": "^4.0.13"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 8.0 KiB |
@ -1,25 +0,0 @@
|
||||
declare namespace API {
|
||||
// 分页模型
|
||||
interface Pagination {
|
||||
size?: number
|
||||
current?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
// 时间模型
|
||||
interface TimeInfo {
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// 基础ID
|
||||
interface BaseID {
|
||||
id?: string
|
||||
}
|
||||
|
||||
// 验证码请求
|
||||
interface CaptchaReq {
|
||||
captchaId: string
|
||||
captcha: string
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
declare namespace API {
|
||||
// 分页模型
|
||||
interface Pagination {
|
||||
size: number
|
||||
current: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// 时间模型
|
||||
interface TimeInfo {
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// 基础ID
|
||||
interface BaseID {
|
||||
id: string
|
||||
}
|
||||
|
||||
// 验证码请求
|
||||
interface CaptchaReq {
|
||||
captchaId: string
|
||||
captcha: string
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
declare namespace API {
|
||||
/** 用户信息 */
|
||||
type UserInfo = BaseID & TimeInfo & {
|
||||
status: string
|
||||
username: string
|
||||
phoneNumber: string
|
||||
email: string
|
||||
nickname: string
|
||||
roles: string[]
|
||||
perms: string[] // 功能权限代码
|
||||
loginRecord: UserLoginRecordInfo
|
||||
metadata: Recordable<string>
|
||||
}
|
||||
|
||||
/** 用户登录记录信息 */
|
||||
type UserLoginRecordInfo = BaseID & TimeInfo & {
|
||||
userId: string
|
||||
lastLoginAt: string
|
||||
lastLoginIpv4: string
|
||||
lastLoginDevice: string
|
||||
lastLoginUa: string
|
||||
loginCount: number
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import type { Fn } from '@vueuse/core'
|
||||
|
||||
const r180 = Math.PI
|
||||
const r90 = Math.PI / 2
|
||||
const r15 = Math.PI / 12
|
||||
const color = '#88888825'
|
||||
|
||||
const el = $ref<HTMLCanvasElement>()
|
||||
|
||||
const { random } = Math
|
||||
const size = reactive(useWindowSize())
|
||||
|
||||
let start = $ref<Fn>(() => { })
|
||||
const init = $ref(4)
|
||||
const len = $ref(6)
|
||||
// let stopped = $ref(false)
|
||||
|
||||
function initCanvas(canvas: HTMLCanvasElement, width = 400, height = 400, _dpi?: number) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
// @ts-expect-error vendor
|
||||
const bsr = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1
|
||||
const dpi = _dpi || dpr / bsr
|
||||
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
canvas.width = dpi * width
|
||||
canvas.height = dpi * height
|
||||
ctx.scale(dpi, dpi)
|
||||
|
||||
return { ctx, dpi }
|
||||
}
|
||||
|
||||
function polar2cart(x = 0, y = 0, r = 0, theta = 0) {
|
||||
const dx = r * Math.cos(theta)
|
||||
const dy = r * Math.sin(theta)
|
||||
return [x + dx, y + dy]
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const canvas = el!
|
||||
const { ctx } = initCanvas(canvas, size.width, size.height)
|
||||
if (!ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
const { width, height } = canvas
|
||||
|
||||
let steps: Fn[] = []
|
||||
let prevSteps: Fn[] = []
|
||||
|
||||
let iterations = 0
|
||||
|
||||
const step = (x: number, y: number, rad: number) => {
|
||||
const length = random() * len
|
||||
|
||||
const [nx, ny] = polar2cart(x, y, length, rad)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
ctx.lineTo(nx, ny)
|
||||
ctx.stroke()
|
||||
|
||||
const rad1 = rad + random() * r15
|
||||
const rad2 = rad - random() * r15
|
||||
|
||||
if (nx < -100 || nx > size.width + 100 || ny < -100 || ny > size.height + 100)
|
||||
return
|
||||
|
||||
if (iterations <= init || random() > 0.5) {
|
||||
steps.push(() => step(nx, ny, rad1))
|
||||
}
|
||||
if (iterations <= init || random() > 0.5) {
|
||||
steps.push(() => step(nx, ny, rad2))
|
||||
}
|
||||
}
|
||||
|
||||
let lastTime = performance.now()
|
||||
const interval = 1000 / 40
|
||||
|
||||
let controls: ReturnType<typeof useRafFn>
|
||||
|
||||
const frame = () => {
|
||||
if (performance.now() - lastTime < interval) {
|
||||
return
|
||||
}
|
||||
|
||||
iterations += 1
|
||||
prevSteps = steps
|
||||
steps = []
|
||||
lastTime = performance.now()
|
||||
|
||||
if (!prevSteps.length) {
|
||||
controls.pause()
|
||||
// stopped = true
|
||||
}
|
||||
|
||||
prevSteps.forEach(i => i())
|
||||
}
|
||||
|
||||
controls = useRafFn(frame, { immediate: false })
|
||||
|
||||
start = () => {
|
||||
controls.pause()
|
||||
iterations = 0
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = color
|
||||
prevSteps = []
|
||||
steps = [
|
||||
() => step(random() * size.width, 0, r90),
|
||||
() => step(random() * size.width, size.height, -r90),
|
||||
() => step(0, random() * size.height, 0),
|
||||
() => step(size.width, random() * size.height, r180),
|
||||
]
|
||||
if (size.width < 500) {
|
||||
steps = steps.slice(0, 2)
|
||||
}
|
||||
controls.resume()
|
||||
// stopped = false
|
||||
}
|
||||
|
||||
start()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pointer-events-none fixed bottom-0 left-0 right-0 top-0">
|
||||
<canvas ref="el" width="400" height="400" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
login?: boolean
|
||||
to?: string
|
||||
}>(), {
|
||||
login: true,
|
||||
})
|
||||
|
||||
const title = computed(() => import.meta.env.VITE_APP_TITLE)
|
||||
const desc = computed(() => import.meta.env.VITE_APP_DESC)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col" h-screen w-screen bg="zinc-50 dark:zinc-900">
|
||||
<AuthBg />
|
||||
<!-- 头部 -->
|
||||
<div
|
||||
bg="white dark:zinc-800" flex="~ col"
|
||||
relative z-1 m-auto box-content min-h-md min-w-sm rounded-lg p-10 shadow-lg
|
||||
>
|
||||
<RouterLink
|
||||
:to="props.to || ''" bg="gray-300 dark:zinc-700"
|
||||
class="bg-register absolute right-0 top-0 h-16 w-16 cursor-pointer rounded-rt text-right"
|
||||
>
|
||||
<div class="relative right-1.5 top-3 z-1 text-black dark:text-gray-400">
|
||||
<span>{{ props.login ? $t('auth.register.title') : $t('auth.login.title') }}</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<div flex items-center gap-3>
|
||||
<i class="i-nl-logo" h-18 w-18 select-none />
|
||||
<div>
|
||||
<div whitespace-nowrap font-extrabold tracking-widest text="4xl gray-700 dark:gray-100">
|
||||
{{ title }}
|
||||
</div>
|
||||
<p text="sm gray-400 tracking-widest">
|
||||
{{ desc }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<NDivider dashed />
|
||||
|
||||
<slot />
|
||||
|
||||
<NDivider dashed />
|
||||
<!-- 版权信息 -->
|
||||
<div class="mx-auto text-sm font-medium tracking-widest text-gray-400">
|
||||
CopyRight © 2022-present NoahLan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.bg-register {
|
||||
clip-path: polygon(100% 0, 0 0, 100% 100%);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>222</div>
|
||||
</template>
|
@ -0,0 +1,237 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { HandlerSettingEnum, ThemeEnum } from '~/constants'
|
||||
import { _omit } from '~/utils'
|
||||
|
||||
export function useAppConfig() {
|
||||
const configStore = useAppConfigStore()
|
||||
const appConfigOptions = storeToRefs(configStore)
|
||||
const {
|
||||
isMixSidebar,
|
||||
isSidebar,
|
||||
openSettingDrawer,
|
||||
sidebar,
|
||||
menu,
|
||||
} = appConfigOptions
|
||||
|
||||
function setAppConfig(configs: DeepPartial<DefineAppConfigOptions>) {
|
||||
configStore.setAppConfig(configs)
|
||||
}
|
||||
|
||||
function toggleOpenSettingDrawer() {
|
||||
configStore.openSettingDrawer = !unref(openSettingDrawer)
|
||||
}
|
||||
|
||||
function toggleCollapse() {
|
||||
configStore.setSidebar({ collapsed: !unref(sidebar).collapsed })
|
||||
}
|
||||
|
||||
function toggleMenuFixed() {
|
||||
configStore.setMenu({ mixSideFixed: !unref(menu).mixSideFixed })
|
||||
}
|
||||
|
||||
function baseHandler(event: HandlerSettingEnum, value: any) {
|
||||
setAppConfig(handlerResults(event, value, configStore.$state))
|
||||
}
|
||||
|
||||
async function copyConfigs() {
|
||||
try {
|
||||
const { copy, isSupported } = useClipboard()
|
||||
if (!isSupported)
|
||||
return console.error('Your browser does not support Clipboard API')
|
||||
const source = reactive(_omit(appConfigOptions, ['openSettingDrawer']))
|
||||
await copy(JSON.stringify(source, null, 2))
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function clearAndRedo() {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
location.reload()
|
||||
}
|
||||
|
||||
function resetAllConfig() {
|
||||
configStore.$reset()
|
||||
}
|
||||
|
||||
const getCollapsedShowTitle = computed<boolean>(() => {
|
||||
if (unref(isMixSidebar) || unref(isSidebar))
|
||||
return !unref(sidebar).collapsed
|
||||
|
||||
return unref(menu).collapsedShowTitle && unref(sidebar).collapsed
|
||||
})
|
||||
|
||||
return {
|
||||
...appConfigOptions,
|
||||
setAppConfig,
|
||||
toggleOpenSettingDrawer,
|
||||
toggleCollapse,
|
||||
toggleMenuFixed,
|
||||
getCollapsedShowTitle,
|
||||
baseHandler,
|
||||
copyConfigs,
|
||||
clearAndRedo,
|
||||
resetAllConfig,
|
||||
}
|
||||
}
|
||||
|
||||
function handlerResults(
|
||||
event: HandlerSettingEnum,
|
||||
value: any,
|
||||
configOptions: DefineAppConfigOptions,
|
||||
): DeepPartial<DefineAppConfigOptions> {
|
||||
const { themeColor, theme, sidebar, header } = configOptions
|
||||
switch (event) {
|
||||
case HandlerSettingEnum.CHANGE_LAYOUT:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { mode, type, split } = value
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const splitOpt = split === undefined ? { split } : {}
|
||||
return {
|
||||
navBarMode: type,
|
||||
menu: {
|
||||
...splitOpt,
|
||||
mode,
|
||||
},
|
||||
sidebar: { collapsed: false },
|
||||
}
|
||||
|
||||
case HandlerSettingEnum.CHANGE_THEME_COLOR:
|
||||
if (unref(themeColor) === value)
|
||||
return {}
|
||||
|
||||
// changeTheme(value);
|
||||
return { themeColor: value }
|
||||
|
||||
case HandlerSettingEnum.CHANGE_THEME:
|
||||
if (unref(theme) === value)
|
||||
return {}
|
||||
|
||||
return { theme: value ? ThemeEnum.DARK : ThemeEnum.LIGHT }
|
||||
|
||||
case HandlerSettingEnum.MENU_HAS_DRAG:
|
||||
return { menu: { canDrag: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_ACCORDION:
|
||||
return { menu: { accordion: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_TRIGGER:
|
||||
return { sidebar: { trigger: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_TOP_ALIGN:
|
||||
return { menu: { topMenuAlign: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_COLLAPSED:
|
||||
return { sidebar: { collapsed: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_WIDTH:
|
||||
return { sidebar: { width: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_SHOW_SIDEBAR:
|
||||
return { sidebar: { show: value, visible: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE:
|
||||
return { menu: { collapsedShowTitle: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_THEME:
|
||||
// updateSidebarBgColor(value);
|
||||
if (unref(sidebar).bgColor === value)
|
||||
return {}
|
||||
return { sidebar: { bgColor: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_SPLIT:
|
||||
return { menu: { split: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE:
|
||||
return { closeMixSidebarOnChange: value }
|
||||
|
||||
case HandlerSettingEnum.MENU_FIXED:
|
||||
return { sidebar: { fixed: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_TRIGGER_MIX_SIDEBAR:
|
||||
return { menu: { mixSideTrigger: value } }
|
||||
|
||||
case HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR:
|
||||
return { menu: { mixSideFixed: value } }
|
||||
|
||||
// ============transition==================
|
||||
case HandlerSettingEnum.OPEN_PAGE_LOADING:
|
||||
return { transition: { openPageLoading: value } }
|
||||
|
||||
case HandlerSettingEnum.ROUTER_TRANSITION:
|
||||
return { transition: { basicTransition: value } }
|
||||
|
||||
case HandlerSettingEnum.OPEN_ROUTE_TRANSITION:
|
||||
return { transition: { enable: value } }
|
||||
|
||||
case HandlerSettingEnum.OPEN_PROGRESS:
|
||||
return { transition: { openNProgress: value } }
|
||||
// ============root==================
|
||||
|
||||
case HandlerSettingEnum.LOCK_TIME:
|
||||
return { lockTime: value }
|
||||
|
||||
case HandlerSettingEnum.FULL_CONTENT:
|
||||
return {
|
||||
content: { fullScreen: value },
|
||||
// sidebar: { visible: !value, show: !value },
|
||||
header: { visible: !value, show: !value },
|
||||
tabTar: { visible: !value, show: !value },
|
||||
menu: { show: !value },
|
||||
}
|
||||
|
||||
case HandlerSettingEnum.CONTENT_MODE:
|
||||
return { content: { mode: value } }
|
||||
|
||||
case HandlerSettingEnum.SHOW_BREADCRUMB:
|
||||
return { header: { showBreadCrumb: value } }
|
||||
|
||||
case HandlerSettingEnum.SHOW_BREADCRUMB_ICON:
|
||||
return { header: { showBreadCrumbIcon: value } }
|
||||
|
||||
case HandlerSettingEnum.GRAY_MODE:
|
||||
return { grayMode: value }
|
||||
|
||||
case HandlerSettingEnum.SHOW_FOOTER:
|
||||
return { footer: { show: value, visible: value } }
|
||||
|
||||
case HandlerSettingEnum.COLOR_WEAK:
|
||||
return { colorWeak: value }
|
||||
|
||||
case HandlerSettingEnum.SHOW_LOGO:
|
||||
return { logo: { show: value, visible: value } }
|
||||
|
||||
// ============tabs==================
|
||||
case HandlerSettingEnum.TABS_SHOW_QUICK:
|
||||
return { tabTar: { showQuick: value } }
|
||||
|
||||
case HandlerSettingEnum.TABS_SHOW:
|
||||
return { tabTar: { show: value, visible: value } }
|
||||
|
||||
case HandlerSettingEnum.TABS_SHOW_REDO:
|
||||
return { tabTar: { showRedo: value } }
|
||||
|
||||
case HandlerSettingEnum.TABS_SHOW_FOLD:
|
||||
return { tabTar: { showFold: value } }
|
||||
|
||||
// ============header==================
|
||||
case HandlerSettingEnum.HEADER_THEME:
|
||||
// updateHeaderBgColor(value);
|
||||
if (unref(header).bgColor === value)
|
||||
return {}
|
||||
return { header: { bgColor: value } }
|
||||
|
||||
case HandlerSettingEnum.HEADER_SEARCH:
|
||||
return { header: { showSearch: value } }
|
||||
|
||||
case HandlerSettingEnum.HEADER_FIXED:
|
||||
return { header: { fixed: value } }
|
||||
|
||||
case HandlerSettingEnum.HEADER_SHOW:
|
||||
return { header: { show: value, visible: value } }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { i18n } from '~/modules/i18n'
|
||||
|
||||
type I18nTranslationRestParameters = [string, any]
|
||||
|
||||
function getKey(namespace: string | undefined, key: string) {
|
||||
if (!namespace) {
|
||||
return key
|
||||
}
|
||||
if (key.startsWith(namespace)) {
|
||||
return key
|
||||
}
|
||||
return `${namespace}.${key}`
|
||||
}
|
||||
|
||||
export function useI18n(namespace?: string) {
|
||||
const normalFn = {
|
||||
t: (key: string) => {
|
||||
return getKey(namespace, key)
|
||||
},
|
||||
}
|
||||
if (!i18n) {
|
||||
return normalFn
|
||||
}
|
||||
const { t, ...other } = i18n.global
|
||||
const tFn = (key: string, ...arg: any[]) => {
|
||||
if (!key) {
|
||||
return ''
|
||||
}
|
||||
if (!key.includes('.') && !namespace) {
|
||||
return key
|
||||
}
|
||||
return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
|
||||
}
|
||||
return {
|
||||
...other,
|
||||
t: tFn,
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
export function useLayout() {
|
||||
const { getFixed: getHeaderFixed } = useHeaderSetting()
|
||||
const headerRef = ref<HTMLElement | null>(null)
|
||||
const { height: headerHeight, width: headerWidth } = useElementSize(headerRef)
|
||||
|
||||
const contentRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const tabRef = ref<HTMLElement | null>(null)
|
||||
const { height: tabHeight, width: tabWidth } = useElementSize(tabRef)
|
||||
|
||||
const footerRef = ref<HTMLElement | null>(null)
|
||||
const { height: footerHeight, width: footerWidth } = useElementSize(footerRef)
|
||||
|
||||
const omitContentHeight = computed(() => {
|
||||
return unref(headerHeight) + unref(tabHeight)
|
||||
})
|
||||
|
||||
const contentFixedHeight = computed(() => unref(getHeaderFixed) ? `calc(100vh - ${unref(omitContentHeight)}px)` : 'auto')
|
||||
|
||||
const contentStyle = computed(() => {
|
||||
return {
|
||||
height: unref(contentFixedHeight),
|
||||
minHeight: unref(contentFixedHeight),
|
||||
}
|
||||
})
|
||||
const mainStyle = computed(() => {
|
||||
return {
|
||||
minHeight: `calc(100vh - ${
|
||||
unref(omitContentHeight) + unref(footerHeight)
|
||||
}px)`,
|
||||
}
|
||||
})
|
||||
return {
|
||||
headerRef,
|
||||
contentRef,
|
||||
tabRef,
|
||||
footerRef,
|
||||
headerHeight,
|
||||
headerWidth,
|
||||
tabHeight,
|
||||
tabWidth,
|
||||
footerHeight,
|
||||
footerWidth,
|
||||
omitContentHeight,
|
||||
contentStyle,
|
||||
mainStyle,
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import type { RouteLocationRaw, Router } from 'vue-router'
|
||||
import { PageEnum, REDIRECT_NAME } from '~/constants'
|
||||
|
||||
export type PathAsPageEnum<T> = T extends { path: string } ? T & { path: PageEnum } : T
|
||||
export type RouteLocationRawEx = PathAsPageEnum<RouteLocationRaw>
|
||||
|
||||
function handleError(e: Error) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
export function useGo(_router?: Router) {
|
||||
const { push, replace } = _router || useRouter()
|
||||
function go(opt: RouteLocationRawEx = PageEnum.BASE_HOME, isReplace = false) {
|
||||
if (!opt) {
|
||||
return
|
||||
}
|
||||
isReplace ? replace(opt).catch(handleError) : push(opt).catch(handleError)
|
||||
}
|
||||
return go
|
||||
}
|
||||
|
||||
export function useRedo(_router?: Router) {
|
||||
const { push, currentRoute } = _router || useRouter()
|
||||
const { query, params = {}, name, fullPath } = unref(currentRoute)
|
||||
function redo(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
if (name === REDIRECT_NAME) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
if (name && Object.keys(params).length > 0) {
|
||||
params._redirect_type = 'name'
|
||||
params.path = String(name)
|
||||
}
|
||||
else {
|
||||
params._redirect_type = 'path'
|
||||
params.path = fullPath
|
||||
}
|
||||
push({ name: REDIRECT_NAME, params, query }).then(() => resolve(true))
|
||||
})
|
||||
}
|
||||
return redo
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export function useMenu() {
|
||||
const menuStore = useMenuStore()
|
||||
const userStore = useUserStore()
|
||||
const { resolve } = useRouter()
|
||||
|
||||
/** 判断给定的route中是否具有给定perms列表的权限 */
|
||||
function hasPermission(route: RouteRecordRaw, perms: any[] = []) {
|
||||
if (!route.meta?.perm) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 递归寻找子节点perm
|
||||
if (route.meta?.perm === true && route.children?.length) {
|
||||
return filterAsyncRoutes(route.children, perms).length
|
||||
}
|
||||
|
||||
// 否则直接通过perm进行判断
|
||||
return perms.includes(
|
||||
Array.isArray(route.meta?.perm)
|
||||
? route.meta.perm[0]?.perm
|
||||
: route.meta?.perm,
|
||||
)
|
||||
}
|
||||
|
||||
// 过滤掉所有perm不匹配的路由
|
||||
function filterAsyncRoutes(routes: RouteRecordRaw[], perms: any[]) {
|
||||
return routes.reduce((rs: RouteRecordRaw[], route) => {
|
||||
if (hasPermission(route, perms)) {
|
||||
rs.push({
|
||||
...route,
|
||||
children: route.children ? filterAsyncRoutes(route.children, perms) : [],
|
||||
})
|
||||
}
|
||||
return rs
|
||||
}, [])
|
||||
}
|
||||
|
||||
async function generateRoutes() {
|
||||
const perms = userStore.userInfo?.perms ?? ['role', 'role/post']
|
||||
menuStore.menuList = filterAsyncRoutes(getRoutes(), perms)
|
||||
}
|
||||
|
||||
function getMenuList(routes: RouteRecordRaw[]) {
|
||||
return routes.reduce((rs: RouteRecordRaw[], route) => {
|
||||
if (!route.meta?.hidden && !route.meta?.hideInMenu) {
|
||||
rs.push({
|
||||
...route,
|
||||
path: resolve(<string>route.redirect || route).path,
|
||||
children: route.children ? getMenuList(route.children) : [],
|
||||
})
|
||||
}
|
||||
return rs
|
||||
}, []).sort((a, b) => {
|
||||
return (a.meta?.order || 999) - (b.meta?.order || 999)
|
||||
})
|
||||
}
|
||||
|
||||
const menuList = computed(() => getMenuList(menuStore.menuList))
|
||||
|
||||
return {
|
||||
menuList,
|
||||
generateRoutes,
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import routes from '~pages'
|
||||
|
||||
/**
|
||||
* 过滤所有hidden的路由
|
||||
*/
|
||||
function filterHiddenRoutes(routes: RouteRecordRaw[]) {
|
||||
return routes.filter((i) => {
|
||||
if (i.children) {
|
||||
i.children = filterHiddenRoutes(i.children)
|
||||
}
|
||||
return !i.meta?.hidden
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取真实存在的路由
|
||||
*/
|
||||
export function getRoutes() {
|
||||
return filterHiddenRoutes(routes)
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import { MenuModeEnum } from '~/constants'
|
||||
|
||||
export function useHeaderSetting() {
|
||||
const { getFullContent } = useFullContent()
|
||||
const appConfig = useAppConfig()
|
||||
const {
|
||||
getMenuMode,
|
||||
getSplit,
|
||||
getShowHeaderTrigger,
|
||||
getIsSidebarType,
|
||||
getIsMixSidebar,
|
||||
getIsTopMenu,
|
||||
} = useMenuSetting()
|
||||
|
||||
const { getShowBreadCrumb, getShowLogo } = useRootSetting()
|
||||
|
||||
const getShowDoc = computed(() => appConfig.header.value.showDoc)
|
||||
const getHeaderTheme = computed(() => appConfig.header.value.theme)
|
||||
const getShowHeader = computed(() => appConfig.header.value.show)
|
||||
const getFixed = computed(() => appConfig.header.value.fixed)
|
||||
const getHeaderBgColor = computed(() => appConfig.header.value.bgColor)
|
||||
const getShowSearch = computed(() => appConfig.header.value.showSearch)
|
||||
const getUseLockPage = computed(() => false)
|
||||
const getShowFullScreen = computed(() => appConfig.header.value.showFullScreen)
|
||||
const getShowLocalePicker = computed(() => appConfig.header.value.showLocalePicker)
|
||||
const getShowNotice = computed(() => appConfig.header.value.showNotice)
|
||||
const getShowBread = computed(() => {
|
||||
return (
|
||||
unref(getMenuMode) !== MenuModeEnum.HORIZONTAL
|
||||
&& unref(getShowBreadCrumb)
|
||||
&& !unref(getSplit)
|
||||
)
|
||||
})
|
||||
const getShowHeaderLogo = computed(() => {
|
||||
return (
|
||||
unref(getShowLogo) && !unref(getIsSidebarType) && !unref(getIsMixSidebar)
|
||||
)
|
||||
})
|
||||
const getShowContent = computed(() => {
|
||||
return unref(getShowBread) || unref(getShowHeaderTrigger)
|
||||
})
|
||||
|
||||
const getShowFullHeaderRef = computed(() => {
|
||||
return !unref(getFullContent) && unref(getShowHeader)
|
||||
})
|
||||
|
||||
const getUnFixedAndFull = computed(
|
||||
() => !unref(getFixed) && !unref(getShowFullHeaderRef),
|
||||
)
|
||||
|
||||
const getShowMixHeaderRef = computed(() => !unref(getIsSidebarType) && unref(getShowHeader))
|
||||
|
||||
const getShowInsetHeaderRef = computed(() => {
|
||||
const need = !unref(getFullContent) && unref(getShowHeader)
|
||||
return (
|
||||
(need && !unref(getShowMixHeaderRef))
|
||||
|| (need && unref(getIsTopMenu))
|
||||
|| (need && unref(getIsMixSidebar))
|
||||
)
|
||||
})
|
||||
|
||||
// Set header configuration
|
||||
function setHeaderSetting(headerSetting: Partial<HeaderSetting>) {
|
||||
appConfig.setAppConfig({ header: headerSetting })
|
||||
}
|
||||
return {
|
||||
setHeaderSetting,
|
||||
getShowDoc,
|
||||
getShowSearch,
|
||||
getHeaderTheme,
|
||||
getUseLockPage,
|
||||
getShowFullScreen,
|
||||
getShowNotice,
|
||||
getShowBread,
|
||||
getShowContent,
|
||||
getShowHeaderLogo,
|
||||
getShowHeader,
|
||||
getFixed,
|
||||
getShowMixHeaderRef,
|
||||
getShowFullHeaderRef,
|
||||
getShowInsetHeaderRef,
|
||||
getUnFixedAndFull,
|
||||
getHeaderBgColor,
|
||||
getShowLocalePicker,
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
import {
|
||||
SIDE_BAR_MINI_WIDTH,
|
||||
SIDE_BAR_SHOW_TIT_MINI_WIDTH,
|
||||
TriggerEnum,
|
||||
} from '~/constants'
|
||||
|
||||
const mixSideHasChildren = ref(false)
|
||||
|
||||
export function useMenuSetting() {
|
||||
const { getFullContent: fullContent } = useFullContent()
|
||||
const configStore = useAppConfigStore()
|
||||
const { getShowLogo } = useRootSetting()
|
||||
|
||||
const getCollapsed = computed(() => configStore.sidebar.collapsed)
|
||||
const getMenuType = computed(() => configStore.navBarMode)
|
||||
const getMenuMode = computed(() => configStore.menu.mode)
|
||||
const getMenuFixed = computed(() => configStore.sidebar.fixed)
|
||||
const getShowMenu = computed(() => configStore.menu.show)
|
||||
const getMenuHidden = computed(() => !configStore.sidebar.visible)
|
||||
const getMenuWidth = computed(() => configStore.sidebar.width)
|
||||
const getTrigger = computed(() => configStore.sidebar.trigger)
|
||||
const getMenuTheme = computed(() => configStore.sidebar.theme)
|
||||
const getSplit = computed(() => configStore.menu.split)
|
||||
const getMenuBgColor = computed(() => configStore.sidebar.bgColor)
|
||||
const getMixSideTrigger = computed(() => configStore.menu.mixSideTrigger)
|
||||
const getShowSidebar = computed(() => {
|
||||
return (
|
||||
unref(getSplit)
|
||||
|| (unref(getShowMenu)
|
||||
&& !unref(configStore.isHorizontal)
|
||||
&& !unref(fullContent))
|
||||
)
|
||||
})
|
||||
|
||||
const getCanDrag = computed(() => configStore.menu.canDrag)
|
||||
const getAccordion = computed(() => configStore.menu.accordion)
|
||||
const getMixSideFixed = computed(() => configStore.menu.mixSideFixed)
|
||||
const getTopMenuAlign = computed(() => configStore.menu.topMenuAlign)
|
||||
const getCloseMixSidebarOnChange = computed(() => configStore.closeMixSidebarOnChange)
|
||||
const getIsSidebarType = computed(() => configStore.isSidebar)
|
||||
const getIsTopMenu = computed(() => configStore.isTopMenu)
|
||||
const getMenuShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType))
|
||||
const getCollapsedShowTitle = computed(() => configStore.isCollapsedShowTitle)
|
||||
const getShowTopMenu = computed(() => unref(configStore.isHorizontal) || unref(getSplit))
|
||||
const getShowHeaderTrigger = computed(() => {
|
||||
if (
|
||||
configStore.isTopMenu
|
||||
|| !unref(getShowMenu)
|
||||
|| unref(getMenuHidden)
|
||||
)
|
||||
return false
|
||||
|
||||
return unref(getTrigger) === TriggerEnum.HEADER
|
||||
})
|
||||
|
||||
const getShowCenterTrigger = computed(() => unref(getTrigger) === TriggerEnum.CENTER)
|
||||
const getShowFooterTrigger = computed(() => unref(getTrigger) === TriggerEnum.FOOTER)
|
||||
const getIsHorizontal = computed(() => configStore.isHorizontal)
|
||||
const getIsMixSidebar = computed(() => configStore.isMixSidebar)
|
||||
const getIsMixMode = computed(() => configStore.isMixMode)
|
||||
|
||||
const getMiniWidthNumber = computed(() => {
|
||||
const { collapsedShowTitle } = configStore.menu
|
||||
return collapsedShowTitle
|
||||
? SIDE_BAR_SHOW_TIT_MINI_WIDTH
|
||||
: SIDE_BAR_MINI_WIDTH
|
||||
})
|
||||
|
||||
const getRealWidth = computed(() => {
|
||||
if (unref(getIsMixSidebar)) {
|
||||
return unref(getCollapsed) && !unref(getMixSideFixed)
|
||||
? unref(getMiniWidthNumber)
|
||||
: unref(getMenuWidth)
|
||||
}
|
||||
return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth)
|
||||
})
|
||||
|
||||
const getCalcContentWidth = computed(() => {
|
||||
const width
|
||||
= unref(getIsTopMenu)
|
||||
|| !unref(getShowMenu)
|
||||
|| (unref(getSplit) && unref(getMenuHidden))
|
||||
? 0
|
||||
: unref(getIsMixSidebar)
|
||||
? (unref(getCollapsed)
|
||||
? SIDE_BAR_MINI_WIDTH
|
||||
: SIDE_BAR_SHOW_TIT_MINI_WIDTH)
|
||||
+ (unref(getMixSideFixed) && unref(mixSideHasChildren)
|
||||
? unref(getRealWidth)
|
||||
: 0)
|
||||
: unref(getRealWidth)
|
||||
|
||||
return `calc(100% - ${unref(width)}px)`
|
||||
})
|
||||
|
||||
function setMenuSetting(menuSetting: Partial<MenuSetting>): void {
|
||||
configStore.setMenu(menuSetting)
|
||||
}
|
||||
|
||||
function setSidebarSetting(
|
||||
sidebarSetting: Partial<SidebarConfigOptions>,
|
||||
): void {
|
||||
configStore.setSidebar(sidebarSetting)
|
||||
}
|
||||
|
||||
function setSiderWidth(width: number) {
|
||||
setSidebarSetting({ width })
|
||||
}
|
||||
|
||||
function toggleCollapsed() {
|
||||
setSidebarSetting({
|
||||
collapsed: !unref(getCollapsed),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
setSiderWidth,
|
||||
setMenuSetting,
|
||||
toggleCollapsed,
|
||||
getMenuFixed,
|
||||
getRealWidth,
|
||||
getMenuType,
|
||||
getMenuMode,
|
||||
getShowMenu,
|
||||
getCollapsed,
|
||||
getMiniWidthNumber,
|
||||
getCalcContentWidth,
|
||||
getMenuWidth,
|
||||
getTrigger,
|
||||
getSplit,
|
||||
getMenuTheme,
|
||||
getCanDrag,
|
||||
getCollapsedShowTitle,
|
||||
getIsHorizontal,
|
||||
getIsSidebarType,
|
||||
getAccordion,
|
||||
getShowTopMenu,
|
||||
getShowHeaderTrigger,
|
||||
getShowCenterTrigger,
|
||||
getShowFooterTrigger,
|
||||
getTopMenuAlign,
|
||||
getMenuHidden,
|
||||
getIsTopMenu,
|
||||
getMenuBgColor,
|
||||
getShowSidebar,
|
||||
getIsMixMode,
|
||||
getIsMixSidebar,
|
||||
getCloseMixSidebarOnChange,
|
||||
getMixSideTrigger,
|
||||
getMixSideFixed,
|
||||
mixSideHasChildren,
|
||||
getMenuShowLogo,
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
export function useMultipleTabSetting() {
|
||||
const configStore = useAppConfig()
|
||||
const getShowMultipleTab = computed(() => configStore.tabTar.value.show)
|
||||
const getShowQuick = computed(() => configStore.tabTar.value.showQuick)
|
||||
const getShowRedo = computed(() => configStore.tabTar.value.showRedo)
|
||||
const getShowFold = computed(() => configStore.tabTar.value.showFold)
|
||||
|
||||
function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) {
|
||||
configStore.setAppConfig({ tabTar: multiTabsSetting })
|
||||
}
|
||||
|
||||
return {
|
||||
setMultipleTabSetting,
|
||||
getShowMultipleTab,
|
||||
getShowQuick,
|
||||
getShowRedo,
|
||||
getShowFold,
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import { ContentLayoutEnum } from '~/constants'
|
||||
|
||||
export function useRootSetting() {
|
||||
const appStore = useAppConfigStore()
|
||||
const getPageLoading = computed(
|
||||
() => appStore.transition.openPageLoading,
|
||||
)
|
||||
|
||||
const getOpenKeepAlive = computed(() => appStore.openKeepAlive)
|
||||
|
||||
const getSettingButtonPosition = computed(
|
||||
() => appStore.settingButtonPosition,
|
||||
)
|
||||
|
||||
const getCanEmbedIFramePage = computed(() => appStore.canEmbedIFramePage)
|
||||
const getShowLogo = computed(() => appStore.logo.show)
|
||||
const getContentMode = computed(() => appStore.content.mode)
|
||||
const getUseOpenBackTop = computed(() => appStore.useOpenBackTop)
|
||||
const getShowSettingButton = computed(() => appStore.header.showSetting)
|
||||
const getShowFooter = computed(() => appStore.footer.show)
|
||||
const getShowBreadCrumb = computed(() => appStore.header.showBreadCrumb)
|
||||
const getThemeColor = computed(() => appStore.themeColor)
|
||||
const getShowBreadCrumbIcon = computed(
|
||||
() => appStore.header.showBreadCrumbIcon,
|
||||
)
|
||||
|
||||
const getFullContent = computed(() => appStore.content.fullScreen)
|
||||
const getColorWeak = computed(() => appStore.colorWeak)
|
||||
const getGrayMode = computed(() => appStore.grayMode)
|
||||
const getLockTime = computed(() => appStore.lockTime)
|
||||
const getShowDarkModeToggle = computed(() => appStore.showThemeModeToggle)
|
||||
const getLayoutContentMode = computed(() =>
|
||||
appStore.content.mode === ContentLayoutEnum.FULL
|
||||
? ContentLayoutEnum.FULL
|
||||
: ContentLayoutEnum.FIXED,
|
||||
)
|
||||
// TODO 待实现
|
||||
// const getDarkMode = computed(() => configStore.getDarkMode)
|
||||
|
||||
// TODO 待实现
|
||||
// function setRootSetting(setting: Partial<RootSetting>) {
|
||||
// configStore.setProjectConfig(setting)
|
||||
// }
|
||||
// TODO 待实现
|
||||
// function setDarkMode(mode: ThemeEnum) {
|
||||
// configStore.setDarkMode(mode)
|
||||
// }
|
||||
return {
|
||||
// setRootSetting,
|
||||
getSettingButtonPosition,
|
||||
getFullContent,
|
||||
getColorWeak,
|
||||
getGrayMode,
|
||||
getLayoutContentMode,
|
||||
getPageLoading,
|
||||
getOpenKeepAlive,
|
||||
getCanEmbedIFramePage,
|
||||
getShowLogo,
|
||||
getShowBreadCrumb,
|
||||
getShowBreadCrumbIcon,
|
||||
getUseOpenBackTop,
|
||||
getShowSettingButton,
|
||||
getShowFooter,
|
||||
getContentMode,
|
||||
getLockTime,
|
||||
getThemeColor,
|
||||
// getDarkMode,
|
||||
// setDarkMode,
|
||||
getShowDarkModeToggle,
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
export function useSiteSetting() {
|
||||
const configStore = useSiteConfigStore()
|
||||
const siteGeneral = storeToRefs(configStore)
|
||||
|
||||
function initSiteGeneralConfig(configs: DeepPartial<DefineSiteOptions>) {
|
||||
configStore.setSiteConfig(configs)
|
||||
}
|
||||
return {
|
||||
...siteGeneral,
|
||||
initSiteGeneralConfig,
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
export function useTransitionSetting() {
|
||||
const configStore = useAppConfig()
|
||||
|
||||
const getEnableTransition = computed(() => configStore.transition.value.enable)
|
||||
const getOpenNProgress = computed(() => configStore.transition.value.openNProgress)
|
||||
const getOpenPageLoading = computed((): boolean => {
|
||||
return !!configStore.transition.value.openPageLoading
|
||||
})
|
||||
const getBasicTransition = computed(
|
||||
() => configStore.transition.value.basicTransition,
|
||||
)
|
||||
|
||||
function setTransitionSetting(transitionSetting: Partial<TransitionSetting>) {
|
||||
configStore.setAppConfig({ transition: transitionSetting })
|
||||
}
|
||||
return {
|
||||
setTransitionSetting,
|
||||
|
||||
getEnableTransition,
|
||||
getOpenNProgress,
|
||||
getOpenPageLoading,
|
||||
getBasicTransition,
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
export function useUser() {
|
||||
const userStore = useUserStore()
|
||||
const { userInfo, authInfo } = storeToRefs(userStore)
|
||||
|
||||
const displayName = computed(() => userInfo.value?.nickname || userInfo.value?.username || 'Name')
|
||||
const isLogin = computed(() => (
|
||||
authInfo.value !== undefined
|
||||
&& authInfo.value.token !== undefined
|
||||
&& authInfo.value.token.access_token !== undefined
|
||||
&& authInfo.value.token.access_token !== ''
|
||||
))
|
||||
|
||||
return {
|
||||
displayName,
|
||||
isLogin,
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
export function useFullContent() {
|
||||
const configStore = useAppConfigStore()
|
||||
const router = useRouter()
|
||||
const { currentRoute } = router
|
||||
|
||||
// Whether to display the content in full screen without displaying the menu
|
||||
const getFullContent = computed(() => {
|
||||
// Query parameters, the full screen is displayed when the address bar has a full parameter
|
||||
const route = unref(currentRoute)
|
||||
const query = route.query
|
||||
if (query && Reflect.has(query, '__full__'))
|
||||
return true
|
||||
|
||||
// Return to the configuration in the configuration file
|
||||
return configStore.content.fullScreen
|
||||
})
|
||||
|
||||
return { getFullContent }
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
export enum ContentLayoutEnum {
|
||||
// auto width
|
||||
FULL = 'full',
|
||||
// fixed width
|
||||
FIXED = 'fixed',
|
||||
}
|
||||
|
||||
// menu theme enum
|
||||
export enum ThemeEnum {
|
||||
DARK = 'dark',
|
||||
LIGHT = 'light',
|
||||
}
|
||||
|
||||
// 导航栏模式
|
||||
export enum NavBarModeEnum {
|
||||
// left menu
|
||||
SIDEBAR = 'sidebar',
|
||||
// mix-sidebar
|
||||
MIX_SIDEBAR = 'mix-sidebar',
|
||||
// mixin menu
|
||||
MIX = 'mix',
|
||||
// top menu
|
||||
TOP_MENU = 'top-menu',
|
||||
}
|
||||
|
||||
// Session过期处理方式
|
||||
export enum SessionTimeoutProcessingEnum {
|
||||
ROUTE_JUMP, // 路由跳转
|
||||
PAGE_COVERAGE, // 页面覆盖
|
||||
}
|
||||
|
||||
// 设置按钮位置
|
||||
export enum SettingButtonPositionEnum {
|
||||
AUTO = 'auto',
|
||||
HEADER = 'header',
|
||||
FIXED = 'fixed',
|
||||
}
|
||||
|
||||
// 路由切换动画
|
||||
export enum RouterTransitionEnum {
|
||||
ZOOM_FADE = 'zoom-fade',
|
||||
ZOOM_OUT = 'zoom-out',
|
||||
FADE_SIDE = 'fade-slide',
|
||||
FADE = 'fade',
|
||||
FADE_BOTTOM = 'fade-bottom',
|
||||
FADE_SCALE = 'fade-scale',
|
||||
}
|
||||
|
||||
// 错误类型
|
||||
export enum ErrorTypeEnum {
|
||||
VUE = 'vue',
|
||||
SCRIPT = 'script',
|
||||
RESOURCE = 'resource',
|
||||
AJAX = 'ajax',
|
||||
PROMISE = 'promise',
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
export const APP_PRESET_COLOR_LIST: string[] = [
|
||||
'#0960bd',
|
||||
'#0084f4',
|
||||
'#009688',
|
||||
'#536dfe',
|
||||
'#ff5c93',
|
||||
'#ee4f12',
|
||||
'#0096c7',
|
||||
'#9c27b0',
|
||||
'#ff9800',
|
||||
]
|
||||
|
||||
// header preset color
|
||||
export const HEADER_PRESET_BG_COLOR_LIST: string[] = [
|
||||
'#ffffff',
|
||||
'#151515',
|
||||
'#009688',
|
||||
'#5172DC',
|
||||
'#018ffb',
|
||||
'#409eff',
|
||||
'#e74c3c',
|
||||
'#24292e',
|
||||
'#394664',
|
||||
'#001529',
|
||||
'#383f45',
|
||||
]
|
||||
|
||||
// sider preset color
|
||||
export const SIDE_BAR_BG_COLOR_LIST: string[] = [
|
||||
'#001529',
|
||||
'#212121',
|
||||
'#273352',
|
||||
'#ffffff',
|
||||
'#191b24',
|
||||
'#191a23',
|
||||
'#304156',
|
||||
'#001628',
|
||||
'#28333E',
|
||||
'#344058',
|
||||
'#383f45',
|
||||
]
|
||||
|
||||
// 设置事件Enum
|
||||
export enum HandlerSettingEnum {
|
||||
CHANGE_LAYOUT,
|
||||
CHANGE_THEME_COLOR,
|
||||
CHANGE_THEME,
|
||||
// menu
|
||||
MENU_HAS_DRAG,
|
||||
MENU_ACCORDION,
|
||||
MENU_TRIGGER,
|
||||
MENU_TOP_ALIGN,
|
||||
MENU_COLLAPSED,
|
||||
MENU_COLLAPSED_SHOW_TITLE,
|
||||
MENU_WIDTH,
|
||||
MENU_SHOW_SIDEBAR,
|
||||
MENU_THEME,
|
||||
MENU_SPLIT,
|
||||
MENU_FIXED,
|
||||
MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE,
|
||||
MENU_TRIGGER_MIX_SIDEBAR,
|
||||
MENU_FIXED_MIX_SIDEBAR,
|
||||
|
||||
// header
|
||||
HEADER_SHOW,
|
||||
HEADER_THEME,
|
||||
HEADER_FIXED,
|
||||
|
||||
HEADER_SEARCH,
|
||||
|
||||
TABS_SHOW_QUICK,
|
||||
TABS_SHOW_REDO,
|
||||
TABS_SHOW,
|
||||
TABS_SHOW_FOLD,
|
||||
|
||||
LOCK_TIME,
|
||||
FULL_CONTENT,
|
||||
CONTENT_MODE,
|
||||
SHOW_BREADCRUMB,
|
||||
SHOW_BREADCRUMB_ICON,
|
||||
GRAY_MODE,
|
||||
COLOR_WEAK,
|
||||
SHOW_LOGO,
|
||||
SHOW_FOOTER,
|
||||
|
||||
ROUTER_TRANSITION,
|
||||
OPEN_PROGRESS,
|
||||
OPEN_PAGE_LOADING,
|
||||
OPEN_ROUTE_TRANSITION,
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export * from './menu'
|
||||
export * from './position'
|
||||
export * from './app'
|
||||
export * from './design'
|
||||
export * from './sidebar'
|
||||
export * from './router'
|
||||
export * from './multiple-tab'
|
@ -0,0 +1,41 @@
|
||||
// 折叠触发器位置
|
||||
export enum TriggerEnum {
|
||||
// 不显示
|
||||
NONE = 'NONE',
|
||||
// 菜单底部
|
||||
FOOTER = 'FOOTER',
|
||||
// 菜单中间
|
||||
CENTER = 'CENTER',
|
||||
// 头部
|
||||
HEADER = 'HEADER',
|
||||
}
|
||||
|
||||
// 菜单模式
|
||||
export type Mode = 'vertical' | 'vertical-right' | 'horizontal' | 'inline'
|
||||
|
||||
// 菜单模式
|
||||
export enum MenuModeEnum {
|
||||
VERTICAL = 'vertical',
|
||||
HORIZONTAL = 'horizontal',
|
||||
VERTICAL_RIGHT = 'vertical-right',
|
||||
INLINE = 'inline',
|
||||
}
|
||||
|
||||
// 分割菜单类型
|
||||
export enum MenuSplitTyeEnum {
|
||||
NONE,
|
||||
TOP,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
export enum TopMenuAlignEnum {
|
||||
CENTER = 'center',
|
||||
START = 'start',
|
||||
END = 'end',
|
||||
}
|
||||
|
||||
// 混合菜单触发器模式
|
||||
export enum MixSidebarTriggerEnum {
|
||||
HOVER = 'hover',
|
||||
CLICK = 'click',
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
export enum TabContentEnum {
|
||||
TAB_TYPE,
|
||||
EXTRA_TYPE,
|
||||
}
|
||||
|
||||
export enum TabActionEnum {
|
||||
// 刷新页面
|
||||
REFRESH_PAGE,
|
||||
// 关闭当前页
|
||||
CLOSE_CURRENT,
|
||||
// 关闭左边
|
||||
CLOSE_LEFT,
|
||||
// 关闭右边
|
||||
CLOSE_RIGHT,
|
||||
// 关闭其它页
|
||||
CLOSE_OTHER,
|
||||
// 关闭全部
|
||||
CLOSE_ALL,
|
||||
SCALE,
|
||||
CLOSE,
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/** 对齐枚举 */
|
||||
export enum AlignEnum {
|
||||
CENTER = 'center',
|
||||
START = 'start',
|
||||
END = 'end',
|
||||
}
|
||||
|
||||
export type Placement =
|
||||
| 'top-start'
|
||||
| 'top'
|
||||
| 'top-end'
|
||||
| 'right-start'
|
||||
| 'right'
|
||||
| 'right-end'
|
||||
| 'bottom-start'
|
||||
| 'bottom'
|
||||
| 'bottom-end'
|
||||
| 'left-start'
|
||||
| 'left'
|
||||
| 'left-end'
|
||||
|
||||
/** 位置枚举 */
|
||||
export enum PlacementEnum {
|
||||
TOP_START = 'top-start',
|
||||
TOP = 'top',
|
||||
TOP_END = 'top-end',
|
||||
RIGHT_START = 'right-start',
|
||||
RIGHT = 'right',
|
||||
RIGHT_END = 'right-end',
|
||||
BOTTOM_START = 'bottom-start',
|
||||
BOTTOM = 'bottom',
|
||||
BOTTOM_END = 'bottom-end',
|
||||
LEFT_START = 'left-start',
|
||||
LEFT = 'left',
|
||||
LEFT_END = 'left-end',
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
export const REDIRECT_NAME = 'Redirect'
|
||||
export const PAGE_NOT_FOUND_NAME = 'PageNotFound'
|
||||
export const BASIC_LOGIN_PATH = '/login'
|
||||
export const BASIC_HOME_PATH = '/'
|
||||
export const BASIC_ERROR_PATH = '/exception'
|
||||
export const BASIC_LOCK_PATH = '/lock'
|
||||
|
||||
export enum PageEnum {
|
||||
// basic login path
|
||||
BASE_LOGIN = '/login',
|
||||
// basic register path
|
||||
BASE_REGISTER = '/register',
|
||||
// basic home path
|
||||
BASE_HOME = '/',
|
||||
// error page path
|
||||
ERROR_PAGE = '/exception',
|
||||
// error log page path
|
||||
ERROR_LOG_PAGE = '/error-log/list',
|
||||
BASE_LOCK = '/lock',
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export const SIDE_BAR_MINI_WIDTH = 48
|
||||
export const SIDE_BAR_SHOW_TIT_MINI_WIDTH = 80
|
@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import Layout from './nlayout/index.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-layout
|
||||
class="h-full"
|
||||
:native-scrollbar="false"
|
||||
>
|
||||
<n-layout-header>
|
||||
<div i="nl-logo" />
|
||||
</n-layout-header>
|
||||
<Layout>
|
||||
<template #main>
|
||||
<RouterView />
|
||||
</n-layout>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
@ -0,0 +1,110 @@
|
||||
<!-- eslint-disable unused-imports/no-unused-vars -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NIcon } from 'naive-ui'
|
||||
import type { RouteLocationMatched } from 'vue-router'
|
||||
import { REDIRECT_NAME } from '~/constants'
|
||||
import { filterTree, isString } from '~/utils'
|
||||
|
||||
const { header } = useAppConfig()
|
||||
const { currentRoute } = useRouter()
|
||||
const { t } = useI18n()
|
||||
const go = useGo()
|
||||
const routes = $ref<RouteLocationMatched[]>([])
|
||||
|
||||
// const { menuList } = useMenu()
|
||||
|
||||
watchEffect(async () => {
|
||||
if (currentRoute.value.name === REDIRECT_NAME) {
|
||||
console.log('233')
|
||||
}
|
||||
// const menus = unref(menuList)
|
||||
|
||||
// const { matched: routeMatched } = unref(currentRoute)
|
||||
|
||||
// const cur = routeMatched?.[routeMatched.length - 1]
|
||||
// let path = currentRoute.value.path
|
||||
// if(cur && cur?.meta?.currentActiveMenu) {
|
||||
// path = cur.meta.currentActiveMenu as string
|
||||
// }
|
||||
// const parent =
|
||||
})
|
||||
|
||||
function getMatched(menus: Menu[], parent: string[]) {
|
||||
const matched: Menu[] = []
|
||||
|
||||
menus.forEach((item) => {
|
||||
if (parent.includes(item.path)) {
|
||||
matched.push(item)
|
||||
}
|
||||
|
||||
if (item.children?.length) {
|
||||
matched.push(...getMatched(item.children, parent))
|
||||
}
|
||||
})
|
||||
return matched
|
||||
}
|
||||
|
||||
function filterItem(list: RouteLocationMatched[]) {
|
||||
return filterTree(list, (item: RouteLocationMatched) => {
|
||||
const { meta, name } = item
|
||||
if (!meta) {
|
||||
return !!name
|
||||
}
|
||||
const { title, hideBreadcrumb, hideMenu } = meta
|
||||
if (!title || hideBreadcrumb || hideMenu) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}).filter(item => !item.meta?.hideBreadcrumb)
|
||||
}
|
||||
|
||||
function renderDropdownLabel(route: any) {
|
||||
return t(route.title)
|
||||
}
|
||||
|
||||
function renderDropdownIcon(option: any) {
|
||||
return option.icon ? renderIcon(option.icon)() : null
|
||||
}
|
||||
|
||||
function renderIcon(icon: string) {
|
||||
return () => h(NIcon, null, { default: () => h('div', { [icon]: '', class: icon }) })
|
||||
}
|
||||
|
||||
function handleClick(path: string, route: Recordable<any>) {
|
||||
const { children, meta, redirect } = route
|
||||
if (children?.length && !redirect) {
|
||||
return
|
||||
}
|
||||
// carryParam
|
||||
if (redirect && isString(redirect)) {
|
||||
go(redirect)
|
||||
}
|
||||
else {
|
||||
path = /^\//.test(path) ? path : `/${path}`
|
||||
go(path)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace align="center" justify="space-between" class="pl-8px pr-8px">
|
||||
<NBreadcrumb v-if="header.showBreadCrumb">
|
||||
<NBreadcrumbItem v-for="(route, index) in routes" :key="index">
|
||||
<NDropdown
|
||||
key-field="path" size="small" :options="route.children" :render-label="renderDropdownLabel"
|
||||
:render-icon="renderDropdownIcon" @select="handleClick"
|
||||
>
|
||||
<NSpace align="center" :size="0">
|
||||
<div
|
||||
v-if="route.meta.icon && header.showBreadCrumbIcon" class="v-middle"
|
||||
:class="`i-${route.meta.icon}`"
|
||||
/>
|
||||
<span class="ml-1.2 mr-1.2">{{ $t(route.meta.title ?? '') }}</span>
|
||||
<div v-if="route.children" class="i-gridicons:dropdown" />
|
||||
</NSpace>
|
||||
</NDropdown>
|
||||
</NBreadcrumbItem>
|
||||
</NBreadcrumb>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import SettingButtonAffix from '../setting/components/setting-button-affix.vue'
|
||||
import { SettingButtonPositionEnum } from '~/constants'
|
||||
|
||||
const {
|
||||
getShowSettingButton,
|
||||
getSettingButtonPosition,
|
||||
getFullContent,
|
||||
} = useRootSetting()
|
||||
|
||||
const { getShowHeader } = useHeaderSetting()
|
||||
const getIsFixedSettingDrawer = computed(() => {
|
||||
if (!unref(getShowSettingButton)) {
|
||||
return false
|
||||
}
|
||||
const settingButtonPosition = unref(getSettingButtonPosition)
|
||||
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
|
||||
return !unref(getShowHeader) || unref(getFullContent)
|
||||
}
|
||||
return settingButtonPosition === SettingButtonPositionEnum.FIXED
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<SettingButtonAffix v-if="getIsFixedSettingDrawer" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
const props = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const { copyright, links } = useSiteSetting()
|
||||
|
||||
const { bem, cssVarBlock } = createNamespace('footer')
|
||||
const style = computed(() => (
|
||||
props.height
|
||||
? cssVarBlock({ height: props.height })
|
||||
: {}) as CSSProperties)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer :class="bem()" :style="style">
|
||||
<div class="flex items-center justify-center">
|
||||
<template v-for="(item, index) in links" :key="index">
|
||||
<NButton text tag="a" :href="item.url" target="_blank" class="mx-1">
|
||||
<span class="flex items-center lh-32px">
|
||||
<div :class="`i-${item.icon}`" size="18" />
|
||||
<NText depth="3">{{ item.label }}</NText>
|
||||
</span>
|
||||
</NButton>
|
||||
</template>
|
||||
</div>
|
||||
<NText depth="3">
|
||||
Copyright © {{ copyright }}
|
||||
</NText>
|
||||
</footer>
|
||||
</template>
|
@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import Logo from './logo/index.vue'
|
||||
import HeaderTrigger from './trigger/header-trigger.vue'
|
||||
import BreadCrumb from './breadcrumb/index.vue'
|
||||
import SettingButton from './setting/components/setting-button.vue'
|
||||
import { SettingButtonPositionEnum } from '~/constants'
|
||||
|
||||
const {
|
||||
isTopMenu,
|
||||
isMix,
|
||||
menu,
|
||||
} = useAppConfig()
|
||||
|
||||
const {
|
||||
getShowHeader,
|
||||
getShowHeaderLogo,
|
||||
getShowFullHeaderRef,
|
||||
} = useHeaderSetting()
|
||||
|
||||
const {
|
||||
getShowHeaderTrigger,
|
||||
getMenuWidth,
|
||||
} = useMenuSetting()
|
||||
|
||||
const { getShowSettingButton, getSettingButtonPosition } = useRootSetting()
|
||||
|
||||
const getShowSetting = computed(() => {
|
||||
if (!unref(getShowSettingButton)) {
|
||||
return false
|
||||
}
|
||||
const settingButtonPosition = unref(getSettingButtonPosition)
|
||||
if (settingButtonPosition === SettingButtonPositionEnum.AUTO) {
|
||||
return unref(getShowHeader)
|
||||
}
|
||||
return settingButtonPosition === SettingButtonPositionEnum.HEADER
|
||||
})
|
||||
|
||||
// TODO multiple tab
|
||||
// 根据布局模式设置Logo宽度
|
||||
const logoWidth = computed(() => (unref(isTopMenu) ? 150 : getMenuWidth))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NSpace
|
||||
v-if="getShowFullHeaderRef" class="h-48px shadow" :class="[{ 'mb-8px': !false }]"
|
||||
:style="{ '--un-shadow-color': 'var(--n-border-color)' }" justify="space-between" align="center"
|
||||
>
|
||||
<slot name="logo">
|
||||
<NSpace align="center" class="items-center" :size="0">
|
||||
<Logo
|
||||
v-if="getShowHeaderLogo" :style="{
|
||||
width: `${logoWidth}px`,
|
||||
maxWidth: `${logoWidth}px`,
|
||||
}"
|
||||
/>
|
||||
<HeaderTrigger v-if="getShowHeaderTrigger" class="mx-2" />
|
||||
<slot name="breadcrumb">
|
||||
<BreadCrumb v-if="!(isTopMenu || (isMix && menu.split))" />
|
||||
</slot>
|
||||
</NSpace>
|
||||
</slot>
|
||||
<slot name="menu" />
|
||||
<div class="pl-8px pr-8px">
|
||||
<slot name="buttons">
|
||||
<NSpace class="p-1" :size="16" align="center">
|
||||
<!-- Search -->
|
||||
<!-- Notify -->
|
||||
<!-- FullScreen -->
|
||||
<!-- LocalePicker -->
|
||||
<!-- UserDropDown -->
|
||||
<SettingButton v-if="getShowSetting" />
|
||||
</NSpace>
|
||||
</slot>
|
||||
</div>
|
||||
</NSpace>
|
||||
<!-- Multiple tab -->
|
||||
<template v-if="false">
|
||||
<slot name="tags">
|
||||
<!-- LayoutTabs -->
|
||||
</slot>
|
||||
</template>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { getCollapsed } = useMenuSetting()
|
||||
const { logo } = useSiteSetting()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="logo">
|
||||
<img :src="logo" alt="" :class="{ 'mr-2': !getCollapsed }">
|
||||
<h2 v-show="!getCollapsed && props.showTitle" class="title">
|
||||
{{ title }}
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
width: auto;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutFeature from './feature/index.vue'
|
||||
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
const { bem } = createNamespace('main')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutFeature />
|
||||
<main :class="bem()">
|
||||
<slot />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
display: block;
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,174 @@
|
||||
<script setup lang="ts" name="LayoutMenu">
|
||||
import type { MenuInst } from 'naive-ui'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
|
||||
import Logo from '../logo/index.vue'
|
||||
import FooterTrigger from '../trigger/footer-trigger.vue'
|
||||
import { createNamespace, listenerRouteChange, mapTree } from '~/utils'
|
||||
import { REDIRECT_NAME } from '~/constants'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
mode?: 'vertical' | 'horizontal'
|
||||
split?: boolean
|
||||
}>(), {
|
||||
mode: 'vertical',
|
||||
split: false,
|
||||
})
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE
|
||||
|
||||
const {
|
||||
menu,
|
||||
isMixSidebar,
|
||||
getCollapsedShowTitle,
|
||||
sidebar,
|
||||
isSidebar,
|
||||
} = useAppConfig()
|
||||
const { getTopMenuAlign, getShowFooterTrigger } = useMenuSetting()
|
||||
|
||||
const showSidebarLogo = computed(() => {
|
||||
return unref(isSidebar) || unref(isMixSidebar)
|
||||
})
|
||||
|
||||
const { bem } = createNamespace('layout-menu')
|
||||
const { t } = useI18n()
|
||||
const { currentRoute } = useRouter()
|
||||
|
||||
const menuRef = $ref<MenuInst | null>(null)
|
||||
let menuListRef = $ref<any[]>([])
|
||||
let activeKey = $ref<any>()
|
||||
|
||||
const getMenuCollapsed = computed(() => {
|
||||
if (unref(isMixSidebar)) {
|
||||
return true
|
||||
}
|
||||
return unref(sidebar).collapsed
|
||||
})
|
||||
|
||||
// 定位菜单选择 与 当前路由匹配
|
||||
function showOption() {
|
||||
nextTick(() => {
|
||||
if (!menuRef) {
|
||||
return
|
||||
}
|
||||
menuRef.showOption()
|
||||
})
|
||||
}
|
||||
|
||||
const { menuList, generateRoutes } = useMenu()
|
||||
// TODO 静态路由 待实现
|
||||
onMounted(async () => {
|
||||
await generateRoutes()
|
||||
menuListRef = mapTree<any>(unref(menuList), { conversion: menu => routeToOption(menu) })
|
||||
showOption()
|
||||
})
|
||||
|
||||
listenerRouteChange((route: RouteLocationNormalizedLoaded) => {
|
||||
if (route.name === REDIRECT_NAME) {
|
||||
return
|
||||
}
|
||||
const currentActiveMenu = route.meta?.currentActiveMenu as string
|
||||
handleMenuChange(route)
|
||||
if (currentActiveMenu) {
|
||||
activeKey = currentActiveMenu
|
||||
}
|
||||
showOption()
|
||||
})
|
||||
|
||||
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
|
||||
const menu = route || unref(currentRoute)
|
||||
activeKey = menu.name
|
||||
}
|
||||
|
||||
// 路由格式转换
|
||||
function routeToOption(item: RouteRecordRaw) {
|
||||
const { name, children, meta: metaRef, component } = item
|
||||
const meta = unref(metaRef)
|
||||
const title = meta?.title ? t(meta.title) : ''
|
||||
|
||||
const icon = `i-${meta?.icon}`
|
||||
|
||||
return {
|
||||
label: () => {
|
||||
if (!component) {
|
||||
return title
|
||||
}
|
||||
if (children && children.length > 0) {
|
||||
return title
|
||||
}
|
||||
|
||||
return h(
|
||||
RouterLink,
|
||||
{
|
||||
to: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
{ default: () => title },
|
||||
)
|
||||
},
|
||||
key: name,
|
||||
icon: () => {
|
||||
if (!meta?.icon)
|
||||
return true
|
||||
return h(NIcon, null, { default: () => h('div', { [icon]: '', class: icon }) })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// function clickMenu(key: any) {
|
||||
// if (unref(isTopMenu) && menu.value.split && !props.split) {
|
||||
// // 通过emit将子路由传递出去
|
||||
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="bem()">
|
||||
<Logo v-if="showSidebarLogo" :class="bem('logo')" :title="title" :show-title="getCollapsedShowTitle" />
|
||||
<NScrollbar :class="bem('scrollbar')">
|
||||
<NMenu
|
||||
ref="menuRef" v-model:value="activeKey" class="w-full" :class="bem('menu')" :style="{
|
||||
justifyContent: getTopMenuAlign === 'center' ? 'center' : `flex-${getTopMenuAlign}`,
|
||||
}" :options="menuListRef" :collapsed="getMenuCollapsed" :collapsed-width="48"
|
||||
:collapsed-icon-size="22" :indent="18" :root-indent="18" :mode="props.mode" :accordion="menu.accordion"
|
||||
/>
|
||||
</NScrollbar>
|
||||
<FooterTrigger v-if="getShowFooterTrigger" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
&__logo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__scrollbar {
|
||||
flex: 1;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
&__menu:not(.n-menu--collapsed) {
|
||||
.n-menu-item-content {
|
||||
&::before {
|
||||
left: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
&.n-menu-item-content--selected,
|
||||
&:hover {
|
||||
&::before {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import SwitchItem from './switch-item.vue'
|
||||
import { HandlerSettingEnum } from '~/constants'
|
||||
|
||||
const {
|
||||
getShowFooter,
|
||||
getShowBreadCrumb,
|
||||
getShowBreadCrumbIcon,
|
||||
getShowLogo,
|
||||
getFullContent,
|
||||
getColorWeak,
|
||||
getGrayMode,
|
||||
} = useRootSetting()
|
||||
|
||||
const {
|
||||
getIsHorizontal,
|
||||
getShowMenu,
|
||||
getIsMixSidebar,
|
||||
} = useMenuSetting()
|
||||
|
||||
const {
|
||||
getShowMultipleTab,
|
||||
getShowFold,
|
||||
getShowQuick,
|
||||
getShowRedo,
|
||||
} = useMultipleTabSetting()
|
||||
|
||||
const { getShowHeader } = useHeaderSetting()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.breadcrumb')" :def="getShowBreadCrumb"
|
||||
:event="HandlerSettingEnum.SHOW_BREADCRUMB" :disabled="!getShowHeader"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.breadcrumbIcon')" :def="getShowBreadCrumbIcon"
|
||||
:event="HandlerSettingEnum.SHOW_BREADCRUMB_ICON" :disabled="!getShowBreadCrumb"
|
||||
/>
|
||||
<SwitchItem :title="$t('layout.setting.tabs')" :def="getShowMultipleTab" :event="HandlerSettingEnum.TABS_SHOW" />
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.tabsRedoBtn')" :def="getShowRedo" :event="HandlerSettingEnum.TABS_SHOW_REDO"
|
||||
:disabled="!getShowMultipleTab"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.tabsQuickBtn')" :def="getShowQuick"
|
||||
:event="HandlerSettingEnum.TABS_SHOW_QUICK" :disabled="!getShowMultipleTab"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.tabsFoldBtn')" :def="getShowFold" :event="HandlerSettingEnum.TABS_SHOW_FOLD"
|
||||
:disabled="!getShowMultipleTab"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.sidebar')" :def="getShowMenu" :event="HandlerSettingEnum.MENU_SHOW_SIDEBAR"
|
||||
:disabled="getIsHorizontal"
|
||||
/>
|
||||
<SwitchItem :title="$t('layout.setting.header')" :def="getShowHeader" :event="HandlerSettingEnum.HEADER_SHOW" />
|
||||
<SwitchItem title="Logo" :def="getShowLogo" :event="HandlerSettingEnum.SHOW_LOGO" :disabled="getIsMixSidebar" />
|
||||
<SwitchItem :title="$t('layout.setting.footer')" :def="getShowFooter" :event="HandlerSettingEnum.SHOW_FOOTER" />
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.fullContent')" :def="getFullContent"
|
||||
:event="HandlerSettingEnum.FULL_CONTENT"
|
||||
/>
|
||||
<SwitchItem :title="$t('layout.setting.grayMode')" :def="getGrayMode" :event="HandlerSettingEnum.GRAY_MODE" />
|
||||
<SwitchItem :title="$t('layout.setting.colorWeak')" :def="getColorWeak" :event="HandlerSettingEnum.COLOR_WEAK" />
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace justify="center">
|
||||
<NSwitch :value="isDark" @update:value="toggleDark">
|
||||
<template #checked>
|
||||
{{ $t('layout.setting.darkMode') }}
|
||||
</template>
|
||||
<template #unchecked>
|
||||
{{ $t('layout.setting.lightMode') }}
|
||||
</template>
|
||||
<template #checked-icon>
|
||||
<div class="i-emojione:crescent-moon hover:cursor-pointer" />
|
||||
</template>
|
||||
<template #unchecked-icon>
|
||||
<div class="i-emojione:sun-with-face hover:cursor-pointer" />
|
||||
</template>
|
||||
</NSwitch>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,126 @@
|
||||
<script setup lang="ts" name="Features">
|
||||
import {
|
||||
contentModeOptions,
|
||||
getMenuTriggerOptions,
|
||||
mixSidebarTriggerOptions,
|
||||
topMenuAlignOptions,
|
||||
} from '../constant'
|
||||
import SwitchItem from './switch-item.vue'
|
||||
import SelectItem from './select-item.vue'
|
||||
import InputNumberItem from './input-number-item.vue'
|
||||
import { HandlerSettingEnum, NavBarModeEnum, TriggerEnum } from '~/constants'
|
||||
|
||||
const { getContentMode, getLockTime } = useRootSetting()
|
||||
|
||||
const {
|
||||
getIsHorizontal,
|
||||
getShowMenu,
|
||||
getMenuType,
|
||||
getTrigger,
|
||||
getCollapsedShowTitle,
|
||||
getMenuFixed,
|
||||
getCollapsed,
|
||||
getCanDrag,
|
||||
getTopMenuAlign,
|
||||
getAccordion,
|
||||
getMenuWidth,
|
||||
getIsTopMenu,
|
||||
getSplit,
|
||||
getIsMixSidebar,
|
||||
getCloseMixSidebarOnChange,
|
||||
getMixSideTrigger,
|
||||
getMixSideFixed,
|
||||
setMenuSetting,
|
||||
} = useMenuSetting()
|
||||
|
||||
const {
|
||||
getShowHeader,
|
||||
getFixed: getHeaderFixed,
|
||||
getShowSearch,
|
||||
} = useHeaderSetting()
|
||||
|
||||
const getShowMenuRef = computed(() => (unref(getShowMenu) && !unref(getIsHorizontal)))
|
||||
|
||||
const triggerOptions = getMenuTriggerOptions(unref(getSplit))
|
||||
const some = triggerOptions.some(item => item.value === unref(getTrigger))
|
||||
if (!some) {
|
||||
setMenuSetting({ trigger: TriggerEnum.FOOTER })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.splitMenu')" :def="getSplit" :event="HandlerSettingEnum.MENU_SPLIT"
|
||||
:disabled="!getShowMenuRef || getMenuType !== NavBarModeEnum.MIX"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.mixSidebarFixed')" :def="getMixSideFixed"
|
||||
:event="HandlerSettingEnum.MENU_FIXED_MIX_SIDEBAR" :disabled="!getIsMixSidebar"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.closeMixSidebarOnChange')" :def="getCloseMixSidebarOnChange"
|
||||
:event="HandlerSettingEnum.MENU_CLOSE_MIX_SIDEBAR_ON_CHANGE" :disabled="!getIsMixSidebar"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.menuCollapse')" :def="getCollapsed"
|
||||
:event="HandlerSettingEnum.MENU_COLLAPSED" :disabled="!getShowMenuRef"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.menuDrag')" :def="getCanDrag" :event="HandlerSettingEnum.MENU_HAS_DRAG"
|
||||
:disabled="!getShowMenuRef"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.menuSearch')" :def="getShowSearch" :event="HandlerSettingEnum.HEADER_SEARCH"
|
||||
:disabled="!getShowHeader"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.menuAccordion')" :def="getAccordion"
|
||||
:event="HandlerSettingEnum.MENU_ACCORDION" :disabled="!getShowMenuRef"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.collapseMenuDisplayName')" :def="getCollapsedShowTitle"
|
||||
:event="HandlerSettingEnum.MENU_COLLAPSED_SHOW_TITLE"
|
||||
:disabled="!getShowMenuRef || !getCollapsed || getIsMixSidebar"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.fixedHeader')" :def="getHeaderFixed" :event="HandlerSettingEnum.HEADER_FIXED"
|
||||
:disabled="!getShowHeader"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.fixedSideBar')" :def="getMenuFixed" :event="HandlerSettingEnum.MENU_FIXED"
|
||||
:disabled="!getShowMenuRef || getIsMixSidebar"
|
||||
/>
|
||||
<SelectItem
|
||||
:title="$t('layout.setting.mixSidebarTrigger')" :options="mixSidebarTriggerOptions"
|
||||
:def="getMixSideTrigger" :event="HandlerSettingEnum.MENU_TRIGGER_MIX_SIDEBAR" :disabled="!getIsMixSidebar"
|
||||
/>
|
||||
<SelectItem
|
||||
:title="$t('layout.setting.topMenuLayout')" :options="topMenuAlignOptions" :def="getTopMenuAlign"
|
||||
:event="HandlerSettingEnum.MENU_TOP_ALIGN" :disabled="!getShowHeader
|
||||
|| getSplit
|
||||
|| (!getIsTopMenu && !getSplit)
|
||||
|| getIsMixSidebar
|
||||
"
|
||||
/>
|
||||
<SelectItem
|
||||
:title="$t('layout.setting.menuCollapseButton')" :options="triggerOptions" :def="getTrigger"
|
||||
:event="HandlerSettingEnum.MENU_TRIGGER" :disabled="!getShowMenuRef || getIsMixSidebar"
|
||||
/>
|
||||
<SelectItem
|
||||
:title="$t('layout.setting.contentMode')" :options="contentModeOptions" :def="getContentMode"
|
||||
:event="HandlerSettingEnum.CONTENT_MODE"
|
||||
/>
|
||||
<InputNumberItem
|
||||
:title="$t('layout.setting.autoScreenLock')" :min="0" :def="getLockTime"
|
||||
:event="HandlerSettingEnum.LOCK_TIME" :suffix="getLockTime === 0
|
||||
? $t('layout.setting.notAutoScreenLock')
|
||||
: $t('layout.setting.minute')
|
||||
"
|
||||
/>
|
||||
<InputNumberItem
|
||||
:title="$t('layout.setting.expandedMenuWidth')" :min="100" :max="600" :step="10"
|
||||
:def="getMenuWidth" suffix="px" :event="HandlerSettingEnum.MENU_WIDTH" :disabled="!getShowMenuRef"
|
||||
/>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
copyConfigs,
|
||||
resetAllConfig,
|
||||
clearAndRedo,
|
||||
} = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NButton type="info" block @click="copyConfigs">
|
||||
<template #icon>
|
||||
<div class="i-ant-design:snippets-twotone" />
|
||||
</template>
|
||||
{{ $t('layout.setting.copyBtn') }}
|
||||
</NButton>
|
||||
<NButton type="warning" block @click="resetAllConfig">
|
||||
<template #icon>
|
||||
<div class="i-ant-design:reload-outlined" />
|
||||
</template>
|
||||
{{ $t('layout.setting.resetBtn') }}
|
||||
</NButton>
|
||||
<NButton type="error" block @click="clearAndRedo">
|
||||
<template #icon>
|
||||
<div class="i-ant-design:redo-outlined" />
|
||||
</template>
|
||||
{{ $t('layout.setting.clearBtn') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts" name="InputNumberItem">
|
||||
import type { HandlerSettingEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
def: {
|
||||
type: [Number] as PropType<number>,
|
||||
},
|
||||
event: {
|
||||
type: Number as PropType<HandlerSettingEnum>,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
suffix: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const { baseHandler } = useAppConfig()
|
||||
|
||||
function onChange(value: any) {
|
||||
baseHandler(props.event, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-number-item">
|
||||
<NSpace justify="space-between" align="center">
|
||||
<span>{{ title }}</span>
|
||||
<NInputNumber
|
||||
class="w-130px!" size="small" v-bind="$attrs" :value="def" :disabled="disabled"
|
||||
@update:value="onChange"
|
||||
>
|
||||
<template #suffix>
|
||||
{{ suffix }}
|
||||
</template>
|
||||
</NInputNumber>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,126 @@
|
||||
<script setup lang="ts" name="NavigationBarPicker">
|
||||
import type { MenuModeEnum } from '~/constants'
|
||||
import { NavBarModeEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
typeList: {
|
||||
type: Array as PropType<{
|
||||
title: string
|
||||
type: NavBarModeEnum
|
||||
mode: MenuModeEnum
|
||||
}[]>,
|
||||
default: () => [],
|
||||
},
|
||||
def: {
|
||||
type: String as PropType<NavBarModeEnum>,
|
||||
default: NavBarModeEnum.SIDEBAR,
|
||||
},
|
||||
})
|
||||
|
||||
const emits = defineEmits(['handler'])
|
||||
|
||||
function handlerPicker(data: any) {
|
||||
emits('handler', data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting-navigation-bar-picker">
|
||||
<NSpace justify="space-between">
|
||||
<template v-for="(item, index) in props.typeList" :key="index">
|
||||
<NTooltip placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<div
|
||||
class="bar-item relative box-border h-48px w-56px cursor-pointer overflow-hidden rounded bg-gray-300 shadow-inner"
|
||||
:class="[`bar-item__${item.type}`, { active: item.type === def }]" @click="handlerPicker(item)"
|
||||
>
|
||||
<div class="mix-sidebar" />
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ $t(item.title) }}</span>
|
||||
</NTooltip>
|
||||
</template>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-navigation-bar-picker {
|
||||
.bar-item {
|
||||
box-shadow: 0px 1px 5px rgba(156, 163, 173, 1);
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border: 2px solid rgba(6, 96, 189, 1);
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background-color: rgba(12, 33, 53, 1);
|
||||
}
|
||||
|
||||
&:after {
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: rgba(249, 250, 251, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.bar-item__sidebar {
|
||||
&:before {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bar-item__mix {
|
||||
&:before {
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background-color: rgba(249, 250, 251, 1);
|
||||
}
|
||||
|
||||
&:after {
|
||||
z-index: 1;
|
||||
background-color: rgba(12, 33, 53, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.bar-item__top-menu {
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:after {
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 25%;
|
||||
background-color: rgba(12, 33, 53, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.bar-item__mix-sidebar {
|
||||
&:before {
|
||||
width: 25%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mix-sidebar {
|
||||
width: 18%;
|
||||
height: 100%;
|
||||
background-color: rgba(249, 250, 251, 1);
|
||||
margin-left: 25%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<script setup lang="ts" name="SelectItem">
|
||||
import type { HandlerSettingEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
def: {
|
||||
type: [String, Number] as PropType<string | number>,
|
||||
},
|
||||
event: {
|
||||
type: Number as PropType<HandlerSettingEnum>,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
options: { type: Array<any>, default: () => [] },
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { baseHandler } = useAppConfig()
|
||||
|
||||
function onChange(value: any) {
|
||||
baseHandler(props.event, value)
|
||||
}
|
||||
/*
|
||||
* options 数据传入时,多语言会失效,这里再渲染一遍
|
||||
* */
|
||||
function renderLabel(option: { label: string; value: string | number }) {
|
||||
return h('span', {}, t(option.label))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="select-item">
|
||||
<NSpace justify="space-between" align="center">
|
||||
<span>{{ title }}</span>
|
||||
<NSelect
|
||||
class="w-130px!" size="small" :value="def" :options="options" :disabled="disabled"
|
||||
:render-label="renderLabel" @update:value="onChange"
|
||||
/>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import SettingDrawer from './setting-drawer.vue'
|
||||
|
||||
const visible = $ref(false)
|
||||
const { contentRef } = useLayout()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAffix
|
||||
:listen-to="contentRef" :trigger-top="240" class="flex-center right-0 z-999 cursor-pointer border-rd-l bg-[#0960bd] p-10px text-white"
|
||||
@click="visible = true"
|
||||
>
|
||||
<div class="i-ion:settings-outline hover:cursor-pointer" />
|
||||
</NAffix>
|
||||
<SettingDrawer $visible="visible" />
|
||||
</template>
|
@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import SettingDrawer from './setting-drawer.vue'
|
||||
|
||||
const visible = $ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="i-ion:settings-outline hover:cursor-pointer" @click="visible = true" />
|
||||
<SettingDrawer $visible="visible" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { navigationBarTypeList } from '../constant'
|
||||
import DarkModeToggle from './dark-mode-toggle.vue'
|
||||
import NavigationBarPicker from './navigation-bar-picker.vue'
|
||||
import ThemeColorPicker from './theme-color-picker.vue'
|
||||
import Features from './features.vue'
|
||||
import Content from './content.vue'
|
||||
import Transitions from './transitions.vue'
|
||||
import FooterButtons from './footer-buttons.vue'
|
||||
import {
|
||||
APP_PRESET_COLOR_LIST,
|
||||
HEADER_PRESET_BG_COLOR_LIST,
|
||||
HandlerSettingEnum,
|
||||
SIDE_BAR_BG_COLOR_LIST,
|
||||
} from '~/constants'
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
const { visible } = defineModels<{
|
||||
visible: boolean
|
||||
}>()
|
||||
|
||||
const { getShowDarkModeToggle, getThemeColor } = useRootSetting()
|
||||
const { baseHandler } = useAppConfig()
|
||||
const { getIsHorizontal, getMenuType, getMenuBgColor } = useMenuSetting()
|
||||
const { getHeaderBgColor } = useHeaderSetting()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer $show="visible" :width="330">
|
||||
<NDrawerContent closable>
|
||||
<template #header>
|
||||
{{ $t('layout.setting.drawerTitle') }}
|
||||
</template>
|
||||
<NDivider title-placement="left" class="mt-0!">
|
||||
{{ $t('layout.setting.theme') }}
|
||||
</NDivider>
|
||||
<template v-if="getShowDarkModeToggle">
|
||||
<DarkModeToggle />
|
||||
</template>
|
||||
<NH6 prefix="bar" :theme-overrides="{ headerMargin6: '12px 0 8px 0', headerFontSize6: '12px' }">
|
||||
{{
|
||||
$t('layout.setting.sysTheme') }}
|
||||
</NH6>
|
||||
<ThemeColorPicker
|
||||
:def="getThemeColor" :event="HandlerSettingEnum.CHANGE_THEME_COLOR"
|
||||
:color-list="APP_PRESET_COLOR_LIST"
|
||||
/>
|
||||
<NH6 prefix="bar" :theme-overrides="{ headerMargin6: '12px 0 8px 0', headerFontSize6: '12px' }">
|
||||
{{ $t('layout.setting.headerTheme') }}
|
||||
</NH6>
|
||||
<ThemeColorPicker
|
||||
:def="getHeaderBgColor" :event="HandlerSettingEnum.HEADER_THEME"
|
||||
:color-list="HEADER_PRESET_BG_COLOR_LIST"
|
||||
/>
|
||||
<NH6 prefix="bar" :theme-overrides="{ headerMargin6: '12px 0 8px 0', headerFontSize6: '12px' }">
|
||||
{{
|
||||
$t('layout.setting.sidebarTheme') }}
|
||||
</NH6>
|
||||
<ThemeColorPicker
|
||||
:def="getMenuBgColor" :event="HandlerSettingEnum.MENU_THEME"
|
||||
:color-list="SIDE_BAR_BG_COLOR_LIST"
|
||||
/>
|
||||
|
||||
<NDivider title-placement="left">
|
||||
{{ $t('layout.setting.navMode') }}
|
||||
</NDivider>
|
||||
<NavigationBarPicker
|
||||
:def="getMenuType" :event="HandlerSettingEnum.CHANGE_LAYOUT"
|
||||
:type-list="navigationBarTypeList" @handler="(item) => {
|
||||
baseHandler(HandlerSettingEnum.CHANGE_LAYOUT, {
|
||||
mode: item.mode,
|
||||
type: item.type,
|
||||
split: getIsHorizontal ? false : undefined,
|
||||
})
|
||||
}"
|
||||
/>
|
||||
<NDivider title-placement="left">
|
||||
{{ $t('layout.setting.interfaceFunction') }}
|
||||
</NDivider>
|
||||
<Features />
|
||||
<NDivider title-placement="left">
|
||||
{{ $t('layout.setting.interfaceDisplay') }}
|
||||
</NDivider>
|
||||
<Content />
|
||||
<NDivider title-placement="left">
|
||||
{{ $t('layout.setting.animation') }}
|
||||
</NDivider>
|
||||
<Transitions />
|
||||
<FooterButtons />
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
@ -0,0 +1,39 @@
|
||||
<script setup lang="ts" name="SwitchItem">
|
||||
import type { HandlerSettingEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
def: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
},
|
||||
event: {
|
||||
type: Number as PropType<HandlerSettingEnum>,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
})
|
||||
|
||||
const { baseHandler } = useAppConfig()
|
||||
|
||||
function onChange(value: any) {
|
||||
baseHandler(props.event, value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="switch-item">
|
||||
<NSpace justify="space-between" align="center">
|
||||
<span>{{ title }}</span>
|
||||
<NSwitch :value="def" :disabled="disabled" @update:value="onChange">
|
||||
<template #checked-icon>
|
||||
<div class="i-ant-design:check-outlined" color="#18A058" />
|
||||
</template>
|
||||
<template #unchecked-icon>
|
||||
<div class="i-ant-design:close-outlined" color="#BEBEBE" />
|
||||
</template>
|
||||
</NSwitch>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,52 @@
|
||||
<script setup lang="ts" name="ThemeColorPicker">
|
||||
import type { HandlerSettingEnum } from '~/constants'
|
||||
|
||||
const props = defineProps({
|
||||
colorList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
event: {
|
||||
type: Number as PropType<HandlerSettingEnum>,
|
||||
required: true,
|
||||
},
|
||||
def: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const { baseHandler } = useAppConfig()
|
||||
|
||||
function handlerClick(color: any) {
|
||||
baseHandler(props.event, color)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="theme-color-picker">
|
||||
<NSpace justify="space-between" :size="0" :wrap="false">
|
||||
<template v-for="color in colorList" :key="color">
|
||||
<span
|
||||
class="color-item box-border inline-block h-20px w-20px cursor-pointer border border-gray-300 rounded-sm"
|
||||
:class="{ active: def === color }"
|
||||
:style="{ background: color }" @click="handlerClick(color)"
|
||||
>
|
||||
<NSpace v-if="def === color" justify="center">
|
||||
<div class="i-ant-design:check-outlined text-white hover:text-#d1d5db" />
|
||||
</NSpace>
|
||||
</span>
|
||||
</template>
|
||||
</NSpace>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.color-item {
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-color: rgba(6, 96, 189, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { routerTransitionOptions } from '../constant'
|
||||
import SwitchItem from './switch-item.vue'
|
||||
import SelectItem from './select-item.vue'
|
||||
import { HandlerSettingEnum } from '~/constants'
|
||||
|
||||
const {
|
||||
getOpenPageLoading,
|
||||
getBasicTransition,
|
||||
getEnableTransition,
|
||||
getOpenNProgress,
|
||||
} = useTransitionSetting()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.progress')" :def="getOpenNProgress"
|
||||
:event="HandlerSettingEnum.OPEN_PROGRESS"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.switchLoading')" :def="getOpenPageLoading"
|
||||
:event="HandlerSettingEnum.OPEN_PAGE_LOADING"
|
||||
/>
|
||||
<SwitchItem
|
||||
:title="$t('layout.setting.switchAnimation')" :def="getEnableTransition"
|
||||
:event="HandlerSettingEnum.OPEN_ROUTE_TRANSITION"
|
||||
/>
|
||||
<SelectItem
|
||||
:title="$t('layout.setting.animationType')" :options="routerTransitionOptions" :def="getBasicTransition"
|
||||
:event="HandlerSettingEnum.ROUTER_TRANSITION" :disabled="getEnableTransition"
|
||||
/>
|
||||
</NSpace>
|
||||
</template>
|
@ -0,0 +1,109 @@
|
||||
import {
|
||||
ContentLayoutEnum,
|
||||
MenuModeEnum,
|
||||
MixSidebarTriggerEnum,
|
||||
NavBarModeEnum,
|
||||
RouterTransitionEnum,
|
||||
TopMenuAlignEnum,
|
||||
TriggerEnum,
|
||||
} from '~/constants'
|
||||
|
||||
export const navigationBarTypeList = [
|
||||
{
|
||||
title: 'layout.setting.menuTypeSidebar',
|
||||
mode: MenuModeEnum.INLINE,
|
||||
type: NavBarModeEnum.SIDEBAR,
|
||||
},
|
||||
{
|
||||
title: 'layout.setting.menuTypeMix',
|
||||
mode: MenuModeEnum.INLINE,
|
||||
type: NavBarModeEnum.MIX,
|
||||
},
|
||||
|
||||
{
|
||||
title: 'layout.setting.menuTypeTopMenu',
|
||||
mode: MenuModeEnum.HORIZONTAL,
|
||||
type: NavBarModeEnum.TOP_MENU,
|
||||
},
|
||||
{
|
||||
title: 'layout.setting.menuTypeMixSidebar',
|
||||
mode: MenuModeEnum.INLINE,
|
||||
type: NavBarModeEnum.MIX_SIDEBAR,
|
||||
},
|
||||
]
|
||||
|
||||
export const contentModeOptions = [
|
||||
{
|
||||
value: ContentLayoutEnum.FULL,
|
||||
label: 'layout.setting.contentModeFull',
|
||||
},
|
||||
{
|
||||
value: ContentLayoutEnum.FIXED,
|
||||
label: 'layout.setting.contentModeFixed',
|
||||
},
|
||||
]
|
||||
|
||||
export const topMenuAlignOptions = [
|
||||
{
|
||||
value: TopMenuAlignEnum.CENTER,
|
||||
label: 'layout.setting.topMenuAlignRight',
|
||||
},
|
||||
{
|
||||
value: TopMenuAlignEnum.START,
|
||||
label: 'layout.setting.topMenuAlignLeft',
|
||||
},
|
||||
{
|
||||
value: TopMenuAlignEnum.END,
|
||||
label: 'layout.setting.topMenuAlignCenter',
|
||||
},
|
||||
]
|
||||
|
||||
export function getMenuTriggerOptions(hideTop: boolean) {
|
||||
return [
|
||||
{
|
||||
value: TriggerEnum.NONE,
|
||||
label: 'layout.setting.menuTriggerNone',
|
||||
},
|
||||
{
|
||||
value: TriggerEnum.CENTER,
|
||||
label: 'layout.setting.menuTriggerCenter',
|
||||
},
|
||||
{
|
||||
value: TriggerEnum.FOOTER,
|
||||
label: 'layout.setting.menuTriggerBottom',
|
||||
},
|
||||
...(hideTop
|
||||
? []
|
||||
: [
|
||||
{
|
||||
value: TriggerEnum.HEADER,
|
||||
label: 'layout.setting.menuTriggerTop',
|
||||
},
|
||||
]),
|
||||
]
|
||||
}
|
||||
|
||||
export const routerTransitionOptions = [
|
||||
RouterTransitionEnum.ZOOM_FADE,
|
||||
RouterTransitionEnum.FADE,
|
||||
RouterTransitionEnum.ZOOM_OUT,
|
||||
RouterTransitionEnum.FADE_SIDE,
|
||||
RouterTransitionEnum.FADE_BOTTOM,
|
||||
RouterTransitionEnum.FADE_SCALE,
|
||||
].map((item) => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
}
|
||||
})
|
||||
|
||||
export const mixSidebarTriggerOptions = [
|
||||
{
|
||||
value: MixSidebarTriggerEnum.HOVER,
|
||||
label: 'layout.setting.triggerHover',
|
||||
},
|
||||
{
|
||||
value: MixSidebarTriggerEnum.CLICK,
|
||||
label: 'layout.setting.triggerClick',
|
||||
},
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
const { toggleCollapsed, getCollapsed } = useMenuSetting()
|
||||
const { bem } = createNamespace('footer-trigger')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="bem()" class="h-6 flex items-center justify-center hover:cursor-pointer" @click="toggleCollapsed">
|
||||
<svg
|
||||
focusable="false" :class="getCollapsed ? 'rotate-180' : ''" data-icon="double-left" width="1em" height="1em"
|
||||
fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"
|
||||
>
|
||||
<path
|
||||
d="M272.9 512l265.4-339.1c4.1-5.2.4-12.9-6.3-12.9h-77.3c-4.9 0-9.6 2.3-12.6 6.1L186.8 492.3a31.99 31.99 0 000 39.5l255.3 326.1c3 3.9 7.7 6.1 12.6 6.1H532c6.7 0 10.4-7.7 6.3-12.9L272.9 512zm304 0l265.4-339.1c4.1-5.2.4-12.9-6.3-12.9h-77.3c-4.9 0-9.6 2.3-12.6 6.1L490.8 492.3a31.99 31.99 0 000 39.5l255.3 326.1c3 3.9 7.7 6.1 12.6 6.1H836c6.7 0 10.4-7.7 6.3-12.9L576.9 512z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
const { toggleCollapsed, getCollapsed } = useMenuSetting()
|
||||
const { bem } = createNamespace('header-trigger')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="bem()" class="hover:cursor-pointer" @click="toggleCollapsed">
|
||||
<div v-if="getCollapsed" class="i-ant-design-menu-unfold-outlined" />
|
||||
<div v-else class="i-ant-design-menu-fold-outlined" />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { createNamespace } from '~/utils'
|
||||
|
||||
const { toggleCollapsed, getCollapsed } = useMenuSetting()
|
||||
const { bem } = createNamespace('side-trigger')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="bem()" @click.stop="toggleCollapsed">
|
||||
<i v-if="getCollapsed" i-ph-caret-double-right-bold />
|
||||
<i v-else i-ph-caret-double-left-bold />
|
||||
</div>
|
||||
</template>
|
@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import LeftMenu from './left-menu.vue'
|
||||
import { NavBarModeEnum } from '~/constants'
|
||||
|
||||
// TODO lockScreen
|
||||
|
||||
const { getMenuType } = useMenuSetting()
|
||||
const layout = computed<ReturnType<typeof defineComponent>>(() => {
|
||||
// TODO appInject mobile
|
||||
switch (getMenuType.value) {
|
||||
case NavBarModeEnum.SIDEBAR:
|
||||
return LeftMenu
|
||||
case NavBarModeEnum.MIX:
|
||||
case NavBarModeEnum.TOP_MENU:
|
||||
case NavBarModeEnum.MIX_SIDEBAR:
|
||||
default:
|
||||
return LeftMenu
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Component :is="layout">
|
||||
<template v-for="item in Object.keys($slots)" #[item]="data" :key="item">
|
||||
<slot :name="item" v-bind="data || {}" />
|
||||
</template>
|
||||
</Component>
|
||||
</template>
|
@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutMenu from './components/menu/index.vue'
|
||||
import LayoutHeader from './components/header.vue'
|
||||
import LayoutFooter from './components/footer.vue'
|
||||
import LayoutMain from './components/main.vue'
|
||||
import SiderDragBar from './components/trigger/sider-dragbar.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'LeftMenuLayout',
|
||||
})
|
||||
const {
|
||||
getShowMenu,
|
||||
getCollapsed,
|
||||
getShowCenterTrigger,
|
||||
setSiderWidth,
|
||||
} = useMenuSetting()
|
||||
|
||||
const {
|
||||
headerRef,
|
||||
footerRef,
|
||||
contentRef,
|
||||
contentStyle,
|
||||
mainStyle,
|
||||
} = useLayout()
|
||||
|
||||
const {
|
||||
toggleCollapse,
|
||||
sidebar,
|
||||
footer,
|
||||
} = useAppConfig()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLayout has-sider class="h-full">
|
||||
<NLayoutSider
|
||||
v-if="getShowMenu" :show-trigger="getShowCenterTrigger" bordered
|
||||
:collapsed-width="sidebar.collapsedWidth" :width="sidebar.width" collapse-mode="width" :collapsed="getCollapsed"
|
||||
@update:collapsed="toggleCollapse"
|
||||
>
|
||||
<slot name="sider">
|
||||
<div class="static h-full">
|
||||
<SiderDragBar :mix="sidebar.mixSidebarWidth" :width="sidebar.width" :fn="setSiderWidth" />
|
||||
<LayoutMenu />
|
||||
</div>
|
||||
</slot>
|
||||
</NLayoutSider>
|
||||
<NLayout>
|
||||
<NLayoutHeader ref="headerRef">
|
||||
<slot name="header">
|
||||
<LayoutHeader />
|
||||
</slot>
|
||||
</NLayoutHeader>
|
||||
<NLayout :content-style="contentStyle">
|
||||
<NLayoutContent ref="contentRef" :content-style="mainStyle">
|
||||
<LayoutMain>
|
||||
<slot name="main" />
|
||||
</LayoutMain>
|
||||
</NLayoutContent>
|
||||
<NLayoutFooter v-if="footer.show" ref="footerRef">
|
||||
<slot name="footer">
|
||||
<LayoutFooter />
|
||||
</slot>
|
||||
</NLayoutFooter>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</template>
|
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<RouterView>
|
||||
<template #default="{ Component, route }">
|
||||
<Transition name="fade-slide" mode="out-in" appear>
|
||||
<Component :is="Component" :key="route.fullPath" />
|
||||
</Transition>
|
||||
</template>
|
||||
</RouterView>
|
||||
</template>
|
@ -1,25 +1,41 @@
|
||||
import { ViteSSG } from 'vite-ssg'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
|
||||
// import Previewer from 'virtual:vue-component-preview'
|
||||
import App from './App.vue'
|
||||
import type { UserModule } from './types'
|
||||
import generatedRoutes from '~pages'
|
||||
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
import { setupPinia } from './modules/pinia'
|
||||
import { setupI18n } from './modules/i18n'
|
||||
import { initRouter, setupRouterGuards } from './modules/router/router'
|
||||
import { setupPWA } from './modules/pwa'
|
||||
|
||||
let meta = document.createElement('meta')
|
||||
meta.name = 'naive-ui-style'
|
||||
document.head.appendChild(meta)
|
||||
|
||||
meta = document.createElement('meta')
|
||||
meta.name = 'vueuc-style'
|
||||
document.head.appendChild(meta)
|
||||
|
||||
; (async () => {
|
||||
const app = createApp(App)
|
||||
setupPinia(app)
|
||||
|
||||
// initApplication
|
||||
|
||||
// setup I18n
|
||||
await setupI18n(app)
|
||||
|
||||
// Router
|
||||
const router = initRouter(import.meta.env.VITE_BASE_URL)
|
||||
app.use(router)
|
||||
// Router guards
|
||||
await setupRouterGuards()
|
||||
await router.isReady()
|
||||
|
||||
// pwa
|
||||
await setupPWA()
|
||||
|
||||
const routes = setupLayouts(generatedRoutes)
|
||||
|
||||
// https://github.com/antfu/vite-ssg
|
||||
export const createApp = ViteSSG(
|
||||
App,
|
||||
{ routes, base: import.meta.env.BASE_URL },
|
||||
(ctx) => {
|
||||
// install all modules under `modules/`
|
||||
Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
|
||||
.forEach(i => i.install?.(ctx))
|
||||
// ctx.app.use(Previewer)
|
||||
},
|
||||
)
|
||||
// mount
|
||||
app.mount('#app')
|
||||
// TODO MOCK
|
||||
})()
|
||||
|
@ -1,14 +0,0 @@
|
||||
import NProgress from 'nprogress'
|
||||
import { type UserModule } from '~/types'
|
||||
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (isClient) {
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path !== from.path)
|
||||
NProgress.start()
|
||||
})
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
}
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { type UserModule } from '~/types'
|
||||
import { createPersistedState } from 'pinia-plugin-persistedstate'
|
||||
import type { App } from 'vue'
|
||||
|
||||
// Setup Pinia
|
||||
// https://pinia.vuejs.org/
|
||||
export const install: UserModule = ({ isClient, initialState, app }) => {
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
// Refer to
|
||||
// https://github.com/antfu/vite-ssg/blob/main/README.md#state-serialization
|
||||
// for other serialization strategies.
|
||||
if (isClient)
|
||||
pinia.state.value = (initialState.pinia) || {}
|
||||
|
||||
else
|
||||
initialState.pinia = pinia.state.value
|
||||
// 持久化插件(localStorage)
|
||||
pinia.use(createPersistedState({
|
||||
storage: localStorage,
|
||||
key: id => `${import.meta.env}__${id}`,
|
||||
}))
|
||||
|
||||
export function setupPinia(app: App<Element>) {
|
||||
app.use(pinia)
|
||||
}
|
||||
|
@ -1,14 +1,4 @@
|
||||
import { type UserModule } from '~/types'
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (!isClient)
|
||||
return
|
||||
|
||||
router.isReady()
|
||||
.then(async () => {
|
||||
export async function setupPWA() {
|
||||
const { registerSW } = await import('virtual:pwa-register')
|
||||
registerSW({ immediate: true })
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
@ -0,0 +1,27 @@
|
||||
import type { Router } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { createNProgressGuard } from './nprogress'
|
||||
import { createTabsGuard } from './tabs'
|
||||
import { setRouteChange } from '~/utils'
|
||||
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let router: Router
|
||||
|
||||
export function initRouter(path: string): Router {
|
||||
router = createRouter({
|
||||
history: createWebHistory(path),
|
||||
routes: setupLayouts(getRoutes()),
|
||||
strict: false,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
return router
|
||||
}
|
||||
|
||||
export function setupRouterGuards() {
|
||||
createNProgressGuard(router)
|
||||
createTabsGuard(router, setRouteChange)
|
||||
// 暂时的
|
||||
useMenuStore()
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import type { Router } from 'vue-router'
|
||||
|
||||
export function createTabsGuard(router: Router, func: AnyFunction<any>) {
|
||||
router.beforeEach(async (to) => {
|
||||
// TODO whitePathList
|
||||
func(to)
|
||||
})
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: page-layout
|
||||
title: menu.login
|
||||
hideInMenu: true
|
||||
</route>
|
@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { runAsync: auth, loading: authLoading } = useRequest(login, { manual: true })
|
||||
|
||||
const rules: FormRules = {
|
||||
subject: [
|
||||
{ required: true, message: t('auth.login.account.subject.required') },
|
||||
],
|
||||
credentials: [
|
||||
{ required: true, message: t('auth.login.account.credentials.required') },
|
||||
{ len: 6, message: t('auth.login.account.credentials.len') },
|
||||
],
|
||||
}
|
||||
const formElRef = $shallowRef<FormInst>()
|
||||
const formModelRef = $ref<API.LoginReq>({
|
||||
subject: '',
|
||||
credentials: '',
|
||||
platform: 'pc',
|
||||
})
|
||||
function submit() {
|
||||
formElRef?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
return
|
||||
}
|
||||
const res = await auth(formModelRef)
|
||||
console.log('🚀 ~ formElRef?.validate ~ res.token:', res.token)
|
||||
}).catch((_) => { })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<NForm
|
||||
ref="formElRef" size="large" class="mt-4" :model="formModelRef" :rules="rules" label-placement="left"
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<NFormItem path="subject">
|
||||
<NInput $value="formModelRef.subject" clearable :placeholder="$t('auth.login.account.subject.placeholder')">
|
||||
<template #prefix>
|
||||
<i i-carbon-user />
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
<NFormItem path="credentials">
|
||||
<NInput
|
||||
$value="formModelRef.credentials" clearable :placeholder="$t('auth.login.account.credentials.placeholder')"
|
||||
show-password-on="click" type="password"
|
||||
>
|
||||
<template #prefix>
|
||||
<i i-carbon-locked />
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
<NButton strong block type="primary" attr-type="submit" :loading="authLoading">
|
||||
{{ $t('auth.login.submit') }}
|
||||
</NButton>
|
||||
<!-- <div class="mt-2">
|
||||
<NSpace justify="end">
|
||||
<RouterLink to="/auth/forget">忘记账号</RouterLink>
|
||||
<RouterLink to="/auth/recover">找回密码</RouterLink>
|
||||
</NSpace>
|
||||
</div> -->
|
||||
</NForm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
hidden: true
|
||||
</route>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue