wip: layouts / settings / utils / stores / modules / components / apis / types

main
NoahLan 1 year ago
parent a4b1c68eec
commit 256884d15b

12
.env

@ -1,11 +1,11 @@
# 项目基本地址
VITE_BASE_URL=/
# 项目名称
VITE_APP_NAME=N-Admin
VITE_APP_NAME=N-Admin后台管理系统
# 项目标题
VITE_APP_TITLE=N-Admin
VITE_APP_TITLE=N-Admin后台管理系统
# 项目描述
VITE_APP_DESC=N-Admin-UI
VITE_APP_DESC=N-Admin Background Management System
# API访问地址
VITE_API_URL=/api
@ -18,10 +18,8 @@ VITE_API_URL_PREFIX=
VITE_HTTP_PROXY=false
# 是否开启打包文件大小结果分析
VITE_VISUALIZER=true
# 是否开启打包压缩
VITE_COMPRESS=false
# 压缩算法类型
VITE_COMPRESS_TYPE=gzip
# 是否开启打包压缩 gzip | brotliCompress | deflate | deflateRaw
VITE_COMPRESS='none'
# 是否应用pwa
VITE_PWA=false
# 是否启用Markdown插件

@ -1,6 +0,0 @@
{
"extends": [
"@antfu",
"@unocss"
]
}

@ -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: '::' }),
],
},
},
}),
},
})
}

@ -1,16 +0,0 @@
import path from 'node:path'
import process from 'node:process'
/**
*
*/
export function getRootPath() {
return path.resolve(process.cwd())
}
/**
* src
*/
export function getSrcPath(srcName = 'src') {
return `${getRootPath()}/${srcName}`
}

@ -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,
)

@ -14,8 +14,9 @@
(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
if (setting === 'dark' || (prefersDark && setting !== 'light')) {
document.documentElement.classList.toggle('dark', true)
}
})()
</script>
</head>

@ -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,245 @@
# vite-plugin-vue-layouts
[![npm version](https://img.shields.io/npm/v/vite-plugin-vue-layouts)](https://www.npmjs.com/package/vite-plugin-vue-layouts)
> Router based layout for Vue 3 applications using [Vite](https://github.com/vitejs/vite)
## Overview
This works best along with the [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages).
Layouts are stored in the `/src/layouts` folder by default and are standard Vue components with a `<router-view></router-view>` in the template.
Pages without a layout specified use `default.vue` for their layout.
You can use route blocks to allow each page to determine its layout. The block below in a page will look for `/src/layouts/users.vue` for its layout.
See the [Vitesse starter template](https://github.com/antfu/vitesse) for a working example.
```html
<route lang="yaml">
meta:
layout: users
</route>
```
## Getting Started
Install Layouts:
```bash
$ npm install -D vite-plugin-vue-layouts
```
Add to your `vite.config.js`:
```js
import Vue from '@vitejs/plugin-vue'
import Pages from 'vite-plugin-pages'
import Layouts from 'vite-plugin-vue-layouts'
export default {
plugins: [Vue(), Pages(), Layouts()],
}
```
In main.ts, you need to add a few lines to import the generated code and setup the layouts.
```js
import { createRouter } from 'vue-router'
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
const routes = setupLayouts(generatedRoutes)
const router = createRouter({
// ...
routes,
})
```
## Client Types
If you want type definition of `virtual:generated-layouts`, add `vite-plugin-vue-layouts/client` to `compilerOptions.types` of your `tsconfig`:
```json
{
"compilerOptions": {
"types": ["vite-plugin-vue-layouts/client"]
}
}
```
## Configuration
```ts
interface UserOptions {
layoutsDirs?: string | string[]
exclude: string[]
defaultLayout?: string
}
```
### Using configuration
To use custom configuration, pass your options to Layouts when instantiating the plugin:
```js
// vite.config.js
import Layouts from 'vite-plugin-vue-layouts'
export default {
plugins: [
Layouts({
layoutsDirs: 'src/mylayouts',
defaultLayout: 'myDefault'
}),
],
}
```
### layoutsDirs
Relative path to the layouts directory. Supports globs.
All .vue files in this folder are imported async into the generated code.
Can also be an array of layout dirs
Any files named `__*__.vue` will be excluded, and you can specify any additional exclusions with the `exclude` option
**Default:** `'src/layouts'`
## How it works
`setupLayouts` transforms the original `router` by
1. Replacing every page with its specified layout
2. Appending the original page in the `children` property.
Simply put, layouts are [nested routes](https://next.router.vuejs.org/guide/essentials/nested-routes.html#nested-routes) with the same path.
Before:
```
router: [ page1, page2, page3 ]
```
After `setupLayouts()`:
```
router: [
layoutA: page1,
layoutB: page2,
layoutA: page3,
]
```
That means you have the full flexibility of the [vue-router API](https://next.router.vuejs.org/api/) at your disposal.
## Common patterns
### Transitions
Layouts and Transitions work as expected and explained in the [vue-router docs](https://next.router.vuejs.org/guide/advanced/transitions.html) only as long as `Component` changes on each route. So if you want a transition between pages with the same layout *and* a different layout, you have to mutate `:key` on `<component>` (for a detailed example, see the vue docs about [transitions between elements](https://v3.vuejs.org/guide/transitions-enterleave.html#transitioning-between-elements)).
`App.vue`
```html
<template>
<router-view v-slot="{ Component, route }">
<transition name="slide">
<component :is="Component" :key="route" />
</transition>
</router-view>
</template>
```
Now Vue will always trigger a transition if you change the route.
### Data from layout to page
If you want to send data *down* from the layout to the page, use props
```
<router-view foo="bar" />
```
### Set static data at the page
If you want to set state in your page and do something with it in your layout, add additional properties to a route's `meta` property. Doing so only works if you know the state at build-time.
You can use the `<route>` block if you work with [vite-plugin-pages](https://github.com/hannoeru/vite-plugin-pages).
In `page.vue`:
```html
<template><div>Content</div></template>
<route lang="yaml">
meta:
layout: default
bgColor: yellow
</route>
```
Now you can read `bgColor` in `layout.vue`:
```html
<script setup>
import { useRouter } from 'vue-router'
</script>
<template>
<div :style="`background: ${useRouter().currentRoute.value.meta.bgColor};`">
<router-view />
</div>
</template>
```
### Data dynamically from page to layout
If you need to set `bgColor` dynamically at run-time, you can use [custom events](https://v3.vuejs.org/guide/component-custom-events.html#custom-events).
Emit the event in `page.vue`:
```html
<script setup>
import { defineEmit } from 'vue'
const emit = defineEmit(['setColor'])
if (2 + 2 === 4)
emit('setColor', 'green')
else
emit('setColor', 'red')
</script>
```
Listen for `setColor` custom-event in `layout.vue`:
```html
<script setup>
import { ref } from 'vue'
const bgColor = ref('yellow')
const setBg = (color) => {
bgColor.value = color
}
</script>
<template>
<main :style="`background: ${bgColor};`">
<router-view @set-color="setBg" />
</main>
</template>
```
## ClientSideLayout
The clientSideLayout uses a simpler [virtual file](https://vitejs.dev/guide/api-plugin.html#importing-a-virtual-file) + [glob import](https://vitejs.dev/guide/features.html#glob-import) scheme, This means that its hmr is faster and more accurate, but also more limited
### Usage
```js
// vite.config.js
import { ClientSideLayout } from 'vite-plugin-vue-layouts'
export default {
plugins: [
ClientSideLayout({
layoutsDir: 'src/mylayouts', // default to 'src/layout'
defaultLayout: 'myDefault', // default to 'default', no need '.vue'
importMode: 'sync' // The default will automatically detect -> ssg is syncother is async
}),
],
}
```

@ -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"
}
}

@ -12,3 +12,7 @@ intro:
aka: Also known as
whats-your-name: What's your name?
not-found: Not found
menu:
title:
notfound: Title
home: Home

@ -5,10 +5,171 @@ button:
home: 首页
toggle_dark: 切换深色模式
toggle_langs: 切换语言
intro:
desc: 固执己见的 Vite 项目模板
dynamic-route: 动态路由演示
hi: 你好,{name}
aka: 也叫
whats-your-name: 输入你的名字
not-found: 未找到页面
menu:
home: 首页
login: 登录
about: 关于
readme: README
dialog:
errTitle: 错误提示
layout:
footer:
onlineDocument: 在线文档
setting:
# content mode
contentModeFull: 流式
contentModeFixed: 定宽
# topMenu align
topMenuAlignLeft: 居左
topMenuAlignRight: 居中
topMenuAlignCenter: 居右
# menu trigger
menuTriggerNone: 不显示
menuTriggerBottom: 底部
menuTriggerTop: 顶部
menuTriggerCenter: 侧边
# menu type
menuTypeSidebar: 左侧菜单模式
menuTypeMixSidebar: 左侧菜单混合模式
menuTypeMix: 顶部菜单混合模式
menuTypeTopMenu: 顶部菜单模式
on:
off:
minute: 分钟
operatingTitle: 操作成功
operatingContent: 复制成功,请到 src/settings/projectSetting.ts 中修改配置!
resetSuccess: 重置成功!
copyBtn: 拷贝
resetBtn: 重置配置
clearBtn: 清空缓存并返回登录页
drawerTitle: 项目配置
theme: 主题
darkMode: 暗黑
lightMode: 明亮
navMode: 导航栏模式
interfaceFunction: 界面功能
interfaceDisplay: 界面显示
animation: 动画
splitMenu: 分割菜单
closeMixSidebarOnChange: 切换页面关闭菜单
sysTheme: 系统主题
headerTheme: 顶栏主题
sidebarTheme: 菜单主题
menuDrag: 侧边菜单拖拽
menuSearch: 菜单搜索
menuAccordion: 侧边菜单手风琴模式
menuCollapse: 折叠菜单
collapseMenuDisplayName: 折叠菜单显示名称
topMenuLayout: 顶部菜单布局
menuCollapseButton: 菜单折叠按钮
contentMode: 内容区域宽度
expandedMenuWidth: 菜单展开宽度
breadcrumb: 面包屑
breadcrumbIcon: 面包屑图标
tabs: 标签页
tabDetail: 标签详情页
tabsQuickBtn: 标签页快捷按钮
tabsRedoBtn: 标签页刷新按钮
tabsFoldBtn: 标签页折叠按钮
sidebar: 左侧菜单
header: 顶栏
footer: 页脚
fullContent: 全屏内容
grayMode: 灰色模式
colorWeak: 色弱模式
progress: 顶部进度条
switchLoading: 切换Loading
switchAnimation: 切换动画
animationType: 动画类型
autoScreenLock: 自动锁屏
notAutoScreenLock: 不锁屏
fixedHeader: 固定Header
fixedSideBar: 固定Sidebar
mixSidebarTrigger: 混合菜单触发方式
triggerHover: 悬停
triggerClick: 点击
mixSidebarFixed: 固定展开菜单
api:
errMsg400: 权限不足!
errMsg401: 用户未授权!
errMsg403: 用户得到授权,但是访问是被禁止的!
errMsg404: 网络请求错误,未找到该资源!
errMsg405: 网络请求错误,请求方法未允许!
errMsg408: 网络请求超时!
errMsg500: 服务器错误,请联系管理员!
errMsg501: 服务端未实现此接口,请联系管理员!
errMsg502: 网络错误!
errMsg503: 服务不可用,服务器暂时过载或维护!
errMsg504: 网络超时!
errMsg505: http版本不支持该请求\U+0021
auth:
login:
title: 登录
submit: 登 录
agreementTip: 登录视为您已同意第三方账号绑定协议、服务条款、隐私政策
accountLogin: 账号登录
smsLogin: 短信登录
socialLogin: 其它登录方式
account:
subject:
placeholder: 输入用户名/手机号码/邮箱
required: 请填写用户名/手机号码/邮箱
credentials:
placeholder: 输入登录密码
required: 请填写登录密码
len: 密码长度不足6位
sms:
subject:
placeholder: 输入手机号码/邮箱
required: 请填写手机号码/邮箱
credentials:
placeholder: 输入验证码
required: 请填写验证码
len: 验证码长度不足4位
register:
title: 注册
submit: 注 册
accountRegister: 账号注册
smsRegister: 短信注册
sms:
phoneNumber:
tip: 输入手机号
code:
tip: 输入验证码
account:
username:
placeholder: 输入用户名
required: 请输入用户名
length: 5-25字符, 可以包含字母
phoneNumber:
placeholder: 输入手机号码
required: 请输入手机号码
credentials:
placeholder: 输入登录密码
placeholder2: 再次输入登录密码
notMatched: 两次密码输入不一致
required: 请输入登录密码
required2: 请再次输入登录密码
invalid: 密码设置不符合要求
code:
placeholder: 输入验证码
required: 请输入验证码
rule:
r1: 6-20个字符
r2: 密码不能与登录名相似
r3: 只能包含字母、数字以及标点符号(除空格)
r4: 字母、数字和标点符号至少包含2种

@ -10,7 +10,6 @@
},
"scripts": {
"build": "vite build",
"build:ssg": "vite-ssg build",
"dev": "vite --port 3333",
"lint": "eslint .",
"preview": "vite preview",
@ -26,63 +25,70 @@
"dependencies": {
"@unhead/vue": "^1.7.4",
"@unocss/reset": "^0.55.7",
"@vueuse/core": "^10.4.1",
"@vueuse/core": "^10.5.0",
"@vueuse/head": "^2.0.0",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"vue": "^3.3.4",
"pinia": "^2.1.7",
"vue": "^3.3.6",
"vue-demi": "^0.14.6",
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.4"
"vue-i18n": "^9.5.0",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@antfu/eslint-config": "^0.43.0",
"@antfu/eslint-config": "1.0.0-beta.28",
"@iconify-json/ant-design": "^1.1.10",
"@iconify-json/carbon": "^1.1.21",
"@intlify/unplugin-vue-i18n": "^1.2.0",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/nprogress": "^0.2.0",
"@types/qs": "^6.9.7",
"@unocss/eslint-config": "^0.55.7",
"@vitejs/plugin-vue": "^4.3.4",
"@vue-macros/reactivity-transform": "^0.3.9",
"@vue-macros/short-vmodel": "^1.2.8",
"@iconify-json/emojione": "^1.1.7",
"@iconify-json/gridicons": "^1.1.11",
"@iconify-json/ion": "^1.1.12",
"@intlify/unplugin-vue-i18n": "^1.4.0",
"@types/markdown-it-link-attributes": "^3.0.3",
"@types/nprogress": "^0.2.2",
"@types/qs": "^6.9.9",
"@unocss/eslint-config": "^0.57.0",
"@vitejs/plugin-vue": "^4.4.0",
"@vue-macros/reactivity-transform": "^0.3.23",
"@vue-macros/short-vmodel": "^1.3.0",
"@vue-macros/volar": "^0.14.3",
"@vue/test-utils": "^2.4.1",
"critters": "^0.0.20",
"cross-env": "^7.0.3",
"cypress": "^13.2.0",
"cypress": "^13.3.2",
"cypress-vite": "^1.4.2",
"eslint": "^8.49.0",
"eslint": "^8.52.0",
"eslint-plugin-cypress": "^2.15.1",
"https-localhost": "^4.7.1",
"less": "^4.2.0",
"lint-staged": "^14.0.1",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-shiki": "^0.9.0",
"naive-ui": "^2.34.4",
"pnpm": "^8.7.6",
"shiki": "^0.14.4",
"naive-ui": "^2.35.0",
"path-to-regexp": "^6.2.1",
"pinia-plugin-persistedstate": "^3.2.0",
"pnpm": "^8.9.2",
"shiki": "^0.14.5",
"simple-git-hooks": "^2.9.0",
"taze": "^0.11.2",
"sortablejs": "^1.15.0",
"taze": "^0.11.4",
"typescript": "^5.2.2",
"unocss": "^0.55.7",
"unplugin-auto-import": "^0.16.6",
"unplugin-vue-components": "^0.25.2",
"unplugin-vue-macros": "^2.5.1",
"unplugin-vue-macros": "^2.6.1",
"unplugin-vue-markdown": "^0.24.3",
"vite": "^4.4.9",
"vite": "^4.5.0",
"vite-bundle-visualizer": "^0.10.0",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-inspect": "^0.7.40",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pwa": "^0.16.5",
"vite-plugin-vue-component-preview": "^1.1.6",
"vite-plugin-vue-devtools": "^1.0.0-rc.4",
"vite-plugin-vue-layouts": "^0.8.0",
"vite-plugin-vue-devtools": "1.0.0-rc.5",
"vite-plugin-vue-layouts": "file:lib/vite-plugin-vue-layouts",
"vite-plugin-webfont-dl": "^3.7.6",
"vite-ssg": "^0.23.2",
"vite-ssg-sitemap": "^0.5.1",
"vitest": "^0.34.4",
"vue-request": "^2.0.3",
"vue-tsc": "^1.8.11"
"vitest": "^0.34.6",
"vue-request": "^2.0.4",
"vue-tsc": "^1.8.20"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

@ -5,23 +5,23 @@ import { darkTheme, dateZhCN, zhCN } from 'naive-ui'
// https://github.com/vueuse/head
// you can use this to manipulate the document head in any components,
// they will be rendered correctly in the html results with vite-ssg
useHead({
title: `${import.meta.env.VITE_TITLE}`,
meta: [
{ name: 'description', content: `${import.meta.env.VITE_DESCRIPTION}` },
{
name: 'theme-color',
content: () => isDark.value ? '#00aba9' : '#ffffff',
},
],
link: [
{
rel: 'icon',
type: 'image/svg+xml',
href: () => preferredDark.value ? '/favicon-dark.svg' : '/favicon.svg',
},
],
})
// useHead({
// title: `${import.meta.env.VITE_TITLE}`,
// meta: [
// { name: 'description', content: `${import.meta.env.VITE_DESCRIPTION}` },
// {
// name: 'theme-color',
// content: () => isDark.value ? '#00aba9' : '#ffffff',
// },
// ],
// link: [
// {
// rel: 'icon',
// type: 'image/svg+xml',
// href: () => preferredDark.value ? '/favicon-dark.svg' : '/favicon.svg',
// },
// ],
// })
const theme = computed(() => {
return isDark.value ? darkTheme : null
@ -30,30 +30,36 @@ const theme = computed(() => {
// const i18n = useI18n()
// createLocale({ name: i18n.locale }, zhCN)
const themeOverrides: GlobalThemeOverrides = {
common: {
primaryColor: '#0052D9',
primaryColorHover: '#366ef4',
primaryColorPressed: '#003cab',
primaryColorSuppl: '#003ccc',
},
}
const themeOverridesRef = computed((): GlobalThemeOverrides => {
return {
// common: {
// primaryColor: '#0052D9',
// primaryColorHover: '#366ef4',
// primaryColorPressed: '#003cab',
// primaryColorSuppl: '#003ccc',
// },
}
})
</script>
<template>
<n-config-provider class="h-full w-full" :theme="theme" :theme-overrides="themeOverrides" :locale="zhCN"
:date-locale="dateZhCN">
<n-loading-bar-provider>
<n-message-provider>
<n-notification-provider>
<n-dialog-provider>
<n-watermark content="TODO 水印" cross fullscreen :font-size="12" :line-height="16" :width="384" :height="384"
:x-offset="12" :y-offset="60" :rotate="-15" />
<NConfigProvider
class="h-full w-full" :theme="theme" :theme-overrides="themeOverridesRef" :locale="zhCN"
:date-locale="dateZhCN"
>
<NLoadingBarProvider>
<NMessageProvider>
<NNotificationProvider>
<NDialogProvider>
<NWatermark
content="TODO 水印" cross fullscreen :font-size="12" :line-height="16" :width="384" :height="384"
:x-offset="12" :y-offset="60" :rotate="-15"
/>
<RouterView />
</n-dialog-provider>
</n-notification-provider>
</n-message-provider>
</n-loading-bar-provider>
<n-global-style />
</n-config-provider>
</NDialogProvider>
</NNotificationProvider>
</NMessageProvider>
</NLoadingBarProvider>
<NGlobalStyle />
</NConfigProvider>
</template>

25
src/api/api.d.ts vendored

@ -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
}
}

104
src/api/auth.d.ts vendored

@ -1,61 +1,61 @@
declare namespace API {
// OAuth 登录请求(网页登录)
interface OauthLoginReq {
// 服务提供商名称
provider: string
}
// OAuth 登录请求(网页登录)
interface OauthLoginReq {
// 服务提供商名称
provider: string
}
// OAuth 登录返回登录页(网页登录)
interface OauthLoginResp {
// 提供商登录地址
AuthUrl: string
}
// OAuth 登录返回登录页(网页登录)
interface OauthLoginResp {
// 提供商登录地址
AuthUrl: string
}
// OAuth Code 登录请求
interface OauthLoginByCodeReq {
code: string
provider: string
}
// OAuth Code 登录请求
interface OauthLoginByCodeReq {
code: string
provider: string
}
// OAuth 手机号登录请求
interface OauthLoginByPhoneCodeReq {
authCode?: string
code: string
provider: string
}
// OAuth 手机号登录请求
interface OauthLoginByPhoneCodeReq {
authCode?: string
code: string
provider: string
}
// 登录请求
type LoginReq = Partial<CaptchaReq> & {
// 登录主体(用户名/邮箱/手机号码)| 作用于验证码时,不支持用户名
subject: string
// 登录凭证
credentials: string
// 当前登录平台
platform: string
}
// 登录请求
type LoginReq = Partial<CaptchaReq> & {
// 登录主体(用户名/邮箱/手机号码)| 作用于验证码时,不支持用户名
subject: string
// 登录凭证
credentials: string
// 当前登录平台
platform: string
}
// 登录Token信息
interface LoginTokenInfo {
uid: string
token_type: string
access_token: string
expires_at: Number
scope: string
}
// 登录Token信息
interface LoginTokenInfo {
uid: string
token_type: string
access_token: string
expires_at: number
scope: string
}
// 登录返回
interface LoginResp {
twoFactorType: string
token: LoginTokenInfo
}
// 登录返回
interface LoginResp {
twoFactorType: string
token: LoginTokenInfo
}
// 注册请求
type RegisterReq = Partial<CaptchaReq> & Partial<{
username: string
email: string
phoneNumber: string
code: string
}> & {
credentials: string
}
// 注册请求
type RegisterReq = Partial<CaptchaReq> & Partial<{
username: string
email: string
phoneNumber: string
code: string
}> & {
credentials: string
}
}

@ -2,54 +2,54 @@ import { defHttp } from '~/utils/http'
// 密码登录
export function login(data: API.LoginReq) {
defHttp.post<API.LoginResp>({
url: '/api/auth/login',
return defHttp.post<API.LoginResp>({
url: '/auth/login',
method: 'POST',
data
data,
}, { withToken: false })
}
// 验证码登录手机x邮箱
export function loginByCode(data: API.LoginReq) {
defHttp.post<API.LoginResp>({
url: '/api/auth/login/byCode',
return defHttp.post<API.LoginResp>({
url: '/auth/login/byCode',
method: 'POST',
data
data,
}, { withToken: false })
}
// Oauth 第三方登录获取登录地址通常针对PC网页登录APP端自行获取code
export function oauthLogin(params: API.OauthLoginReq) {
defHttp.post<API.OauthLoginResp>({
url: '/api/auth/oauth/login',
return defHttp.post<API.OauthLoginResp>({
url: '/auth/oauth/login',
method: 'GET',
params
params,
}, { withToken: false })
}
// Oauth 第三方登录客户端自行获取code进行登录
export function oauthLoginByCode(data: API.OauthLoginByCodeReq) {
defHttp.post<API.LoginResp>({
url: '/api/auth/oauth/login/byCode',
return defHttp.post<API.LoginResp>({
url: '/auth/oauth/login/byCode',
method: 'POST',
data
data,
}, { withToken: false })
}
// Oauth 第三方登录客户端获取换取手机号的code进行登录
export function oauthLoginByPhone(data: API.OauthLoginByPhoneCodeReq) {
defHttp.post<API.LoginResp>({
url: '/api/auth/oauth/login/byPhone',
return defHttp.post<API.LoginResp>({
url: '/auth/oauth/login/byPhone',
method: 'POST',
data
data,
}, { withToken: false })
}
// 用户注册
export function register(data: API.RegisterReq) {
defHttp.post<API.BaseID>({
url: '/api/auth/register',
return defHttp.post<API.BaseID>({
url: '/auth/register',
method: 'POST',
data
data,
}, { withToken: false })
}

25
src/api/base.d.ts vendored

@ -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
}
}

24
src/api/user.d.ts vendored

@ -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
}
}

@ -13,6 +13,7 @@ declare global {
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const EffectScope: typeof import('vue')['EffectScope']
const RouterLink: typeof import('vue-router/auto')['RouterLink']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
@ -36,15 +37,19 @@ declare global {
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineLoader: typeof import('vue-router/auto')['defineLoader']
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const envD: typeof import('./types/env.d')['default']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getRoutes: typeof import('./composables/router/routes')['getRoutes']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDark: typeof import('./composables/dark')['isDark']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isProxy: typeof import('vue')['isProxy']
@ -53,7 +58,6 @@ declare global {
const isRef: typeof import('vue')['isRef']
const login: typeof import('./api/auth/auth')['login']
const loginByCode: typeof import('./api/auth/auth')['loginByCode']
const login: typeof import('./api/auth/auth')['login']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
@ -82,6 +86,7 @@ declare global {
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const preferredDark: typeof import('./composables/dark')['preferredDark']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
@ -125,6 +130,8 @@ declare global {
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useAppConfig: typeof import('./composables/config/app-config')['useAppConfig']
const useAppConfigStore: typeof import('./stores/app-config')['useAppConfigStore']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
@ -187,11 +194,14 @@ declare global {
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullContent: typeof import('./composables/web/full-content')['useFullContent']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGo: typeof import('./composables/page')['useGo']
const useHead: typeof import('@vueuse/head')['useHead']
const useI18n: typeof import('vue-i18n')['useI18n']
const useHeaderSetting: typeof import('./composables/setting/header-setting')['useHeaderSetting']
const useI18n: typeof import('./composables/i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
@ -200,6 +210,7 @@ declare global {
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLayout: typeof import('./composables/layout')['useLayout']
const useLink: typeof import('vue-router')['useLink']
const useLoadMore: typeof import('./composables/request')['useLoadMore']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
@ -210,11 +221,17 @@ declare global {
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMenu: typeof import('./composables/router/menu')['useMenu']
const useMenuSetting: typeof import('./composables/setting/menu-setting')['useMenuSetting']
const useMenuStore: typeof import('./stores/menu')['useMenuStore']
const useMessage: typeof import('naive-ui')['useMessage']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMultipleTab: typeof import('./stores/multiple-tab')['useMultipleTab']
const useMultipleTabSetting: typeof import('./composables/setting/multiple-tab-setting')['useMultipleTabSetting']
const useMultipleTabStore: typeof import('./stores/multiple-tab')['useMultipleTabStore']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
@ -239,10 +256,12 @@ declare global {
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRedo: typeof import('./composables/page')['useRedo']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useRequest: typeof import('./composables/request')['useRequest']
const useRequestProvider: typeof import('./composables/request')['useRequestProvider']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRootSetting: typeof import('./composables/setting/root-setting')['useRootSetting']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
@ -253,6 +272,8 @@ declare global {
const useSeoMeta: typeof import('@vueuse/head')['useSeoMeta']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSiteConfigStore: typeof import('./stores/site')['useSiteConfigStore']
const useSiteSetting: typeof import('./composables/setting/site-settings')['useSiteSetting']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
@ -263,6 +284,7 @@ declare global {
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTabs: typeof import('./composables/config/tags')['useTabs']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
@ -280,7 +302,9 @@ declare global {
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useTransitionSetting: typeof import('./composables/setting/transition-setting')['useTransitionSetting']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUser: typeof import('./composables/user')['useUser']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useUserStore: typeof import('./stores/user')['useUserStore']
const useVModel: typeof import('@vueuse/core')['useVModel']
@ -315,7 +339,7 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
}
// for vue template auto import
import { UnwrapRef } from 'vue'
@ -357,9 +381,11 @@ declare module 'vue' {
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRoutes: UnwrapRef<typeof import('./composables/router/routes')['getRoutes']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
@ -368,7 +394,6 @@ declare module 'vue' {
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly login: UnwrapRef<typeof import('./api/auth/auth')['login']>
readonly loginByCode: UnwrapRef<typeof import('./api/auth/auth')['loginByCode']>
readonly login: UnwrapRef<typeof import('./api/auth/auth')['login']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@ -397,6 +422,7 @@ declare module 'vue' {
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@ -440,6 +466,8 @@ declare module 'vue' {
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']>
readonly useAppConfigStore: UnwrapRef<typeof import('./stores/app-config')['useAppConfigStore']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@ -502,11 +530,14 @@ declare module 'vue' {
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullContent: UnwrapRef<typeof import('./composables/web/full-content')['useFullContent']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGo: UnwrapRef<typeof import('./composables/page')['useGo']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useHeaderSetting: UnwrapRef<typeof import('./composables/setting/header-setting')['useHeaderSetting']>
readonly useI18n: UnwrapRef<typeof import('./composables/i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@ -515,6 +546,7 @@ declare module 'vue' {
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLayout: UnwrapRef<typeof import('./composables/layout')['useLayout']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']>
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
@ -525,11 +557,16 @@ declare module 'vue' {
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']>
readonly useMenuSetting: UnwrapRef<typeof import('./composables/setting/menu-setting')['useMenuSetting']>
readonly useMenuStore: UnwrapRef<typeof import('./stores/menu')['useMenuStore']>
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMultipleTabSetting: UnwrapRef<typeof import('./composables/setting/multiple-tab-setting')['useMultipleTabSetting']>
readonly useMultipleTabStore: UnwrapRef<typeof import('./stores/multiple-tab')['useMultipleTabStore']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
@ -554,10 +591,12 @@ declare module 'vue' {
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useRequest: UnwrapRef<typeof import('./composables/request')['useRequest']>
readonly useRequestProvider: UnwrapRef<typeof import('./composables/request')['useRequestProvider']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
@ -568,6 +607,8 @@ declare module 'vue' {
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']>
readonly useSiteSetting: UnwrapRef<typeof import('./composables/setting/site-settings')['useSiteSetting']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
@ -578,6 +619,7 @@ declare module 'vue' {
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTabs: UnwrapRef<typeof import('./composables/config/tags')['useTabs']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
@ -595,7 +637,9 @@ declare module 'vue' {
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useTransitionSetting: UnwrapRef<typeof import('./composables/setting/transition-setting')['useTransitionSetting']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUser: UnwrapRef<typeof import('./composables/user')['useUser']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useUserStore: UnwrapRef<typeof import('./stores/user')['useUserStore']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
@ -666,9 +710,11 @@ declare module '@vue/runtime-core' {
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getRoutes: UnwrapRef<typeof import('./composables/router/routes')['getRoutes']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
@ -677,7 +723,6 @@ declare module '@vue/runtime-core' {
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly login: UnwrapRef<typeof import('./api/auth/auth')['login']>
readonly loginByCode: UnwrapRef<typeof import('./api/auth/auth')['loginByCode']>
readonly login: UnwrapRef<typeof import('./api/auth/auth')['login']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@ -706,6 +751,7 @@ declare module '@vue/runtime-core' {
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
@ -749,6 +795,8 @@ declare module '@vue/runtime-core' {
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useAppConfig: UnwrapRef<typeof import('./composables/config/app-config')['useAppConfig']>
readonly useAppConfigStore: UnwrapRef<typeof import('./stores/app-config')['useAppConfigStore']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
@ -811,11 +859,14 @@ declare module '@vue/runtime-core' {
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullContent: UnwrapRef<typeof import('./composables/web/full-content')['useFullContent']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGo: UnwrapRef<typeof import('./composables/page')['useGo']>
readonly useHead: UnwrapRef<typeof import('@vueuse/head')['useHead']>
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
readonly useHeaderSetting: UnwrapRef<typeof import('./composables/setting/header-setting')['useHeaderSetting']>
readonly useI18n: UnwrapRef<typeof import('./composables/i18n')['useI18n']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
@ -824,6 +875,7 @@ declare module '@vue/runtime-core' {
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLayout: UnwrapRef<typeof import('./composables/layout')['useLayout']>
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
readonly useLoadMore: UnwrapRef<typeof import('./composables/request')['useLoadMore']>
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
@ -834,11 +886,16 @@ declare module '@vue/runtime-core' {
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMenu: UnwrapRef<typeof import('./composables/router/menu')['useMenu']>
readonly useMenuSetting: UnwrapRef<typeof import('./composables/setting/menu-setting')['useMenuSetting']>
readonly useMenuStore: UnwrapRef<typeof import('./stores/menu')['useMenuStore']>
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMultipleTabSetting: UnwrapRef<typeof import('./composables/setting/multiple-tab-setting')['useMultipleTabSetting']>
readonly useMultipleTabStore: UnwrapRef<typeof import('./stores/multiple-tab')['useMultipleTabStore']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
@ -863,10 +920,12 @@ declare module '@vue/runtime-core' {
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRedo: UnwrapRef<typeof import('./composables/page')['useRedo']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useRequest: UnwrapRef<typeof import('./composables/request')['useRequest']>
readonly useRequestProvider: UnwrapRef<typeof import('./composables/request')['useRequestProvider']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRootSetting: UnwrapRef<typeof import('./composables/setting/root-setting')['useRootSetting']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
@ -877,6 +936,8 @@ declare module '@vue/runtime-core' {
readonly useSeoMeta: UnwrapRef<typeof import('@vueuse/head')['useSeoMeta']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSiteConfigStore: UnwrapRef<typeof import('./stores/site')['useSiteConfigStore']>
readonly useSiteSetting: UnwrapRef<typeof import('./composables/setting/site-settings')['useSiteSetting']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
@ -887,6 +948,7 @@ declare module '@vue/runtime-core' {
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTabs: UnwrapRef<typeof import('./composables/config/tags')['useTabs']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
@ -904,7 +966,9 @@ declare module '@vue/runtime-core' {
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useTransitionSetting: UnwrapRef<typeof import('./composables/setting/transition-setting')['useTransitionSetting']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUser: UnwrapRef<typeof import('./composables/user')['useUser']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useUserStore: UnwrapRef<typeof import('./stores/user')['useUserStore']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>

@ -7,14 +7,45 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AuthBg: typeof import('./components/auth/bg.vue')['default']
AuthLayout: typeof import('./components/auth/layout.vue')['default']
FormBg: typeof import('./components/form/bg.vue')['default']
NAffix: typeof import('naive-ui')['NAffix']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
NH6: typeof import('naive-ui')['NH6']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NWatermark: typeof import('naive-ui')['NWatermark']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

@ -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,86 @@
import type { RouteLocationNormalized, Router } from 'vue-router'
import { TabActionEnum } from '~/constants'
export function useTabs(_router?: Router) {
const appStore = useAppConfig()
function canIUseTabs(): boolean {
const { show } = unref(appStore.tabTar)
if (!show) {
throw new Error(
'The multi-tab page is currently not open, please open it in the settings',
)
}
return !!show
}
const tabStore = useMultipleTabStore()
const router = _router || useRouter()
const { currentRoute } = router
function getCurrentTab() {
const route = unref(currentRoute)
return tabStore.tabList.find(item => item.fullPath === route.fullPath)!
}
async function updateTabTitle(title: string, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs
if (!canIUse()) {
return
}
const targetTab = tab || getCurrentTab()
await tabStore.setTabTitle(title, targetTab)
}
async function updateTabPath(path: string, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs
if (!canIUse()) {
return
}
const targetTab = tab || getCurrentTab()
await tabStore.updateTabPath(path, targetTab)
}
async function handleTabAction(action: TabActionEnum, tab?: RouteLocationNormalized) {
const canIUse = canIUseTabs
if (!canIUse()) {
return
}
const currentTab = tab || getCurrentTab()
switch (action) {
case TabActionEnum.REFRESH_PAGE:
await tabStore.refreshPage(router)
await useRedo(router)()
break
case TabActionEnum.CLOSE_ALL:
await tabStore.closeAllTabs(router)
break
case TabActionEnum.CLOSE_LEFT:
await tabStore.closeLeftTabs(currentTab, router)
break
case TabActionEnum.CLOSE_RIGHT:
await tabStore.closeRightTabs(currentTab, router)
break
case TabActionEnum.CLOSE_OTHER:
await tabStore.closeOtherTabs(currentTab, router)
break
case TabActionEnum.CLOSE_CURRENT:
case TabActionEnum.CLOSE:
await tabStore.closeTab(currentTab, router)
break
}
}
return {
refreshPage: () => handleTabAction(TabActionEnum.REFRESH_PAGE),
closeAll: () => handleTabAction(TabActionEnum.CLOSE_ALL),
closeLeft: (tab?: RouteLocationNormalized) => handleTabAction(TabActionEnum.CLOSE_LEFT, tab),
closeRight: (tab?: RouteLocationNormalized) => handleTabAction(TabActionEnum.CLOSE_RIGHT, tab),
closeOther: (tab?: RouteLocationNormalized) => handleTabAction(TabActionEnum.CLOSE_RIGHT, tab),
closeCurrent: () => handleTabAction(TabActionEnum.CLOSE_CURRENT),
close: (tab?: RouteLocationNormalized) => handleTabAction(TabActionEnum.CLOSE, tab),
setTitle: updateTabTitle,
updatePath: updateTabPath,
}
}

@ -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>
<RouterView />
</n-layout>
<Layout>
<template #main>
<RouterView />
</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,66 @@
<script setup lang="ts">
//
// fn sider
// width sider
// mix sider
const props = defineProps({
fn: {
type: Function,
},
width: {
type: Number,
},
mix: {
type: Number,
},
})
//
let canMove = $ref(false)
//
let pX = $ref(0)
//
const { x } = useMouse()
//
const { pressed } = useMousePressed()
// //
// function mouseDown(e: MouseEvent) {
// canMove = true
// pX = e.clientX
// }
//
watch(() => x.value, () => {
if (!canMove) {
return
}
const t = x.value - pX
if (t === 0) {
return
}
const n = props.width ?? 0 + t
if (n < (props.mix ?? 0)) {
return
}
if (props.fn) {
props.fn(n)
}
pX = x.value
})
//
watch(
() => pressed.value,
() => {
if (!pressed.value) {
canMove = false
}
},
)
// TODO
</script>
<template>
<div class="dragbar absolute right-0 h-full w-1 hover:cursor-col-resize" />
</template>
<style lang="less" scoped></style>

@ -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,12 +1,12 @@
import type { App } from 'vue'
import type { Locale } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import { type UserModule } from '~/types'
// Import i18n resources
// https://vitejs.dev/guide/features.html#glob-import
//
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
const i18n = createI18n({
export const i18n = createI18n({
legacy: false,
locale: '',
messages: {},
@ -44,7 +44,7 @@ export async function loadLanguageAsync(lang: string): Promise<Locale> {
return setI18nLanguage(lang)
}
export const install: UserModule = ({ app }) => {
export async function setupI18n(app: App) {
app.use(i18n)
loadLanguageAsync('en')
await loadLanguageAsync('zh-CN')
}

@ -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) || {}
const pinia = createPinia()
// 持久化插件(localStorage)
pinia.use(createPersistedState({
storage: localStorage,
key: id => `${import.meta.env}__${id}`,
}))
else
initialState.pinia = pinia.state.value
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 () => {
const { registerSW } = await import('virtual:pwa-register')
registerSW({ immediate: true })
})
.catch(() => {})
export async function setupPWA() {
const { registerSW } = await import('virtual:pwa-register')
registerSW({ immediate: true })
}

@ -0,0 +1,27 @@
import NProgress from 'nprogress'
import type { Router } from 'vue-router'
const LOADED_PAGE_POOL = new Map<string, boolean>()
export function createNProgressGuard(router: Router) {
const openNProgress = useAppConfigStore().transition.openNProgress
router.beforeEach((to) => {
// The page has already been loaded, it will be faster to open it again, you dont need to do loading and other processing
to.meta.loaded = !!LOADED_PAGE_POOL.get(to.path)
// Display a progress bar at the top when switching pages
// Only works when the page is loaded for the first time
if (openNProgress && !to.meta.loaded) {
NProgress.start()
}
return true
})
router.afterEach((to) => {
// Indicates that the page has been loaded
// When opening again, you can turn off some progress display interactions
LOADED_PAGE_POOL.set(to.path, true)
// Close the page loading progress bar
if (openNProgress && !to.meta.loaded) {
NProgress.done()
}
})
}

@ -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)
})
}

@ -1,3 +1,8 @@
<route lang="yaml">
meta:
title: menu.readme
</route>
## File-based Routing
Routes will be auto-generated for Vue files in this dir with the same file structure.

@ -1,7 +1,15 @@
---
title: About
title: menu.home
layout: page-layout
path: /abb
---
<route lang="yaml">
meta:
title: menu.about
order: 2
</route>
<div class="text-center">
<!-- You can use Vue components inside markdown -->
<div i-carbon-dicom-overlay class="text-4xl -mb-6 m-auto" />

@ -2,17 +2,20 @@
defineOptions({
name: 'IndexPage',
})
const api = import.meta.env.VITE_API_URL
// useHead({
// title: '',
// })
</script>
<template>
<div>
aaa {{ api }}
Index
</div>
</template>
<route lang="yaml">
meta:
layout: default
title: menu.home
order: 1
icon: carbon-home
</route>

@ -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…
Cancel
Save