Vue3后台项目模板
# 后台管理系统模版
技术栈:vue3.2 + ts + pina + vite
# 初始化项目
npm create vite
- project name: 输入 naive-admin-pro
- 选择框架 - 选择 vue
- 选择语言 - 选择 typerscript
创建完成
安装依赖
cd naive-admin-pro
npm i
npm run dev
2
3
# 配置代码规范工具
安装插件
npm install eslint @mistjs/eslint-config-vue -D
在根目录下创建 .eslintrc 页面
{
// 使用规则库
"extends": "@mistjs/eslint-config-vue",
// 自定义自己的规则
"rules": {}
}
2
3
4
5
6
在 vscode 中下载 eslint 插件
下载完成后,在使用如下的命令:
macos: command + shift
windows: ctrl + shift + p
输入:setting.json 找到用户配置点击进入
选择 Open User Settings [JSON]
"editor.codeActionsOnSave": {
"source.fixAll": false,
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true, // 允许 eslint 在保存的时候自动格式化
"source.organizeImports": false
}
2
3
4
5
6
# Husky 配置
git hooks 钩子
安装
npm i husky -D
使用
在 package.json 的 script 中配置如下:
{
"scripts": {
"prepare": "husky install"
}
}
2
3
4
5
配置完成后,我们 npm i 初始化一下我们的工程
如初始化之后没有出现 .husky 文件,是因为当前项目没有初始化 git ,请先 git init 在项目根目录初始化下 git 在 重新 npm i
初始化完成后会自动生成一个 .husky 的目录,接下来我们来配置一下再提交信息之前先去检查我们的代码。如果检查通过允许提交,如果检查不通过,我们不允许提交代码。
通过如下命令:
npx husky add .husky/pre-commit "npm run lint"
windows10 用户请用以下命令:
npx husky add .husky/pre-commit "npm-run-lint"
命令正常之后会在 .husky 下生成 pre-commit 文件
还需要配置一下 lint 的命令在项目中:
{
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src"
}
}
2
3
4
5
测试一下,我们在项目中写一个 debugger ,来测试一些命令:
npm run lint
然后再去除掉 debugger 之后在:
npm run lint
观察结果
像一些净搞我们想要默认修复他,那么我们可以直接在命令的后面加一个 -- fix 就可以修复一些警告:
{
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx,.vue src --fix"
}
}
2
3
4
5
添加之后,eslint 会帮助我们按照我们自定义的规范自动修复。
存在的问题
我们每次没必要吧所有的文件都检查一遍,我们只需要检查我们提交改动的代码即可,所有这种情况下我们需要配合 lint-staged 使用。他的作用相当于一个文件过滤器,每次提交时只检查本地提交的暂存区的文件,但是他不能校验我们的代码。所以我们可以配合着 husky lint-staged 和我们的 eslint 一起使用。
我们先安装一下 lint-staged:
npm install lint-staged -D
然后我们在 package.json 中配置如下:
{
"lint-staged": {
"*.{js,tsx,vue,ts,jsx}": "eslint --fix"
}
}
2
3
4
5
调整 husky -> pre-commit 的执行命令:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# npm-run-lint 改为
npm run lint-staged
2
3
4
5
测试提交代码:
git add .
git commit -m '测试'
2
# 项目配置
- 配置依赖库
- 配置自动导入插件
# 项目配置上
# 依赖库配置 and 自动导入插件
首先我们需要安装一下我们需要用到的 UI 库和工具库:naive-ui、vueuse。
然后我们还需要安装一些按需加载的插件:unplugin-auto-import、unplugin-vue-components。
路由库:vue-router
状态管理库:pinia
多语言:vue-i18n
开启 vue 的响应式语法糖
安装:
开发环境
npm install naive-ui unplugin-auto-import unplugin-vue-components -D
生产环境
npm install @vueuse/core vue-router pinia vue-i18n
- 根目录下创建 types 文件夹
- 将 src 下 vite-env.d-ts 移动到 types 文件夹下
- 并改名为 env.d.ts
配置下 vite.config.ts 中插件:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
// 响应式语法糖
reactivityTransform: true
}),
AutoImport({
// 配置需要自动导入的库
imports: [
'vue',
'vue/macros',
'vue-router',
'vue-i18n',
'@vueuse/core',
'pinia',
{
'naive-ui': [
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar'
]
}
],
// 生成到的地址
dts: 'types/auto-imports.d.ts',
dirs: [
// pinia状态管理目录
'src/stores',
// 自动以组合式 api 目录
'src/composables'
]
}),
Components({
// 导入 naiveui 的配置项目
resolvers: [NaiveUiResolver()],
// 生成类型的地址
dts: 'types/components.d.ts',
dirs: ['src/components']
})
]
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
完成配置启动项目进行测试。
项目里去掉 HelloWorld 的引入测试是否正常。
tsconfig.json 添加
"include": ['"types/**/*.d.ts"']
重新启动项目
组件飘黄波浪号处理
npm install @vue/runtime-core -D
# 项目配置中
# 配置 unocss
安装
这里我们需要安装 unocss 和重置默认样式的工具 @unocss/reset
npm install unocss @unocss/reset -D
使用
在 vite.config.ts 中导入插件
import Unocss from 'unocss/vite';
{
plugins: [Unocss()];
}
2
3
4
5
根目录下创建 unocss.config.ts
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup
} from 'unocss';
export default defineConfig({
presets: [
presetUno(), // 默认 wind 预设
presetAttributify(), // class拆分属性预设
presetTypography(), // 排版预设
presetIcons({
// 图标库预设
scale: 1.2,
warn: true
}),
presetWebFonts({
// 网络字体预设
fonts: {
sans: 'DM Sans',
serif: 'DM Serif Display',
mono: 'DM Mono'
}
})
],
transformers: [
transformerVariantGroup(), // windi CSS的变体组功能
transformerDirectives() // @apply @screen theme()转换器
]
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
main.ts 中添加
import '@unocss/reset/tailwind.css';
import 'uno.css';
2
潜在的样式冲突
关于 tailwind 的 preflight 样式
在 main.ts 中添加
const meta = document.createElement('meta');
meta.name = 'naive-ui-style';
document.head.appendChild(meta);
2
3
删除 style.css 不需要默认样式
# 项目配置下
配置路径前缀
创建环境变量文件
# 配置路径前缀
我们经常会在一些项目框架中会看到类似下面的引用路径:
import App from '~/App.vue';
那么他是怎么通过 “~” 来配置路径的呢。
vite 为我们提供了 alias 的系统别名的属性,我们可以通过他来配置。
在 vite.config.ts 中
// 安装 node 的包
npm install @types/node -D
2
import { fileURLToPath } from 'url';
const baseSrc = fileURLToPath(new URL('./src', import.meta.url));
export default {
// ...省略其他
resolve: {
alias: {
'~': baseSrc,
'~@': baseSrc,
'@': baseSrc
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
配置完成后,我们在项目中尝试引用,发现并没有路径提示,那么我们还需要再 tsconfig.json 中做配置如下:
{
"compilerOptions": {
"baseUrl":".",
"paths": {
"~@/*":["src/*"],
"~/*":["src/*"]
}
}
}
2
3
4
5
6
7
8
9
然后我们再在项目中测试发现已经生效了(如果未生效建议重新编辑器)
# 环境变量
默认情况下,vite 会使用 dotenv 来读取一下的文件,作为我们的环境变量。
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[model] # 只在指定模式下加载
.env.[model].local # 只在指定模式下加载,但会被 git 忽略
[model] 自定义
2
3
4
5
6
环境变量文档参考地址,这里我们不再赘述直接上手使用。
我们在根目录下,创建 .env .env.production .env.development 文件,分别用于默认配置,生产环境配置,开发环境配置。
默认情况下,为了防止意外地将一些环境变量泄露到客户端,只有以 VITE* 为前缀的变量才会暴露给 vite 处理的代码。所以我们在应用的时候保证我们的前缀是以 VITE* 开头即可。
例如:
VITE_APP_BASE = '/';
我们在项目中测试一下读取这个变量
在 main.js 中:
console.log(import.meta.env.VITE_APP_BASE);
其中 import.meta.env 仅仅是 vite 中的特性,不能在其他地方使用
# typescript 提示配置
默认情况下,环境变量是没有提示的,所以我们可以通过配置类型使得我们在使用环境变量的时候有一定的提示信息,方便我们后续的开发。
在 env.d.ts 中加入如下的配置
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_APP_BASE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们在尝试输入,就会自带提示了,我们需要哪些变量我们就配置哪些即可。
# 路由配置
目标
创建路由文件夹并完成基本的路由配置
文件夹创建
在项目根目录中的 src 目录下创建 routes 用于存放我们的路由文件
然后再 routes 的文件夹下先创建一个 index.ts 的路由配置文件。
初始化路由
src -> routes -> index.ts
import { createRouter, createWebHistory } from 'vue-router';
import staticRoutes from './static-routes';
const router = createRouter({
routes: [],
history: createWebHistory(import.meta.env.VITE_APP_BASE ?? '/')
});
export default router;
2
3
4
5
6
7
8
9
src -> routes -> index.ts
import type { RouteRecordRaw } from 'vue-router'
const staticRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('~/pages/Index.vue'),
},
{
path: '/workspace',
name: 'Workspace',
component: () => import('~/pages/workspace/Index.vue'),
},
]
export default staticRoutes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pages -> Index.vue
pages ->workspace -> Index.vue
App.vue
<router-view />
main.ts
import router from './routes';
# pinia 配置
目标
完成 pinia 的初始化配置并实现一个 useCounter 状态管理
为什么采用 pinia
pinia 也是由 vue 官方出品的一个新一代的状态管理器,用于替代 vuex 的产物。
在 pinia 中没有为了 devtools 的集成而延伸的 mutation 的历史包袱,同时呢还支持了组合式 api 的使用方式,我们也会完全通过组合式 api 的使用方式去开发我们的项目。
挂载实例
首先我们需要再 main.ts 中挂载 pinia 的实例
import { createPinia } from 'pinia';
const pinia = createPinia();
app.use(pinia);
2
3
4
5
src -> stores -> counter.ts
export const useCounter = defineStore('counter', () => {
const counter = ref(0);
const increment = () => counter.value++;
return {
counter,
increment
};
});
2
3
4
5
6
7
8
9
使用
<script setup lang="ts">
const counterStore = useCounter();
const { counter } = storeToRefs(counterStore);
</script>
<template>
<div @click="counterStore.increment">index + {{ counter }}</div>
</template>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
# naive-ui 全局配置
目标
- 配置 naive-ui 的全局配置
- 测试各个弹层的全局配置是否可以正常使用
# 全局配置
在我们开发前端中后台管理框架的时候,全局化配置是必不可少的,其中我们可以通过 naive-ui 的全局配置可以做很多事情,比如我们的多主体模式,暗黑模式,多语言等等。
# 在 App.vue 中
我们在 app.vue 中删除之前测试的代码直接导入如下代码:
<script setup lang="ts"></script>
<template>
<n-config-provider>
<app-provider>
<router-view />
</app-provider>
</n-config-provider>
</template>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
配置全局组件
src -> components ->app-provider -> index.vue
<template>
<n-message-provider>
<n-dialog-provider>
<n-notification-provider>
<n-loading-bar-provider>
<slot />
</n-loading-bar-provider>
</n-notification-provider>
</n-dialog-provider>
</n-message-provider>
</template>
2
3
4
5
6
7
8
9
10
11
# 布局全局配置
目标
- 创建全局默认的布局配置文件
- 配置开发环境与生产环境的布局配置
创建全局默认配置
为了方便我们修改我们的一些通用配置项,我们需要创建一个用来管理全局配置的文件夹,来管理我们所有的全局配置项。
创建文件夹
我们在 src 目录下创建一个 config 文件夹用于存放当前项目中所有的全局配置项。
config -> layout-theme.ts
export interface layoutTheme {
title?: string;
layout: 'mix' | 'side' | 'top';
headerHeight: number;
}
export const layoutThemeConfig: layoutTheme = {
title: 'Naive admin Pro',
layout: 'mix',
headerHeight: 48
};
2
3
4
5
6
7
8
9
10
11
创建布局全局配置项
然后我们在项目中创建一个用于控制布局的全局配置文件 layout-theme.ts
src -> composables -> layout-theme.ts
import { layoutThemeConfig } from '~/config/layout-theme';
// useStorage持久化保存
export const useLayoutTheme = createGlobalState(() =>
useStorage('layout-theme', layoutThemeConfig)
);
2
3
4
5
6
src -> stores -> app.ts
import { layoutThemeConfig } from '~/config/layout-theme';
export const useAppStore = defineStore('app', () => {
const defaultTheme = import.meta.env.DEV
? layoutThemeConfig
: useLayoutTheme();
const layout = reactive(unref(defaultTheme));
return {
layout
};
});
2
3
4
5
6
7
8
9
10
11
12
# 基础布局
目标
- 完成基础布局配置
- 调整路由配置使我们的基础布局生效
创建布局文件夹
首先我们先在 src 的目录下创建一个基础 layouts 的布局文件夹。
然后我们在创建一个 base-layout 的布局文件夹,在其下创建一个 index.vue 的文件备用。
layouts 根目录暴露导出文件
我们在 layouts 的根目录创建一个 index.ts 暴露我们需要再外面使用的布局文件,方便我们后续集成使用。
import Layout from './base-layout/index.vue';
export { Layout };
2
3
集成到路由中
我们在 base-layout 中的 index.vue 中集成我们的 router-view 如下:
<script setup></script>
<template>
<router-view />
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
src -> routes -> static-routes.ts
import type { RouteRecordRaw } from 'vue-router'
import { Layout } from '~/layouts/index'
const staticRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'index',
component: Layout,
redirect: '/home',
children: [
{
path: '/home',
name: 'Home',
component: () => import('~/pages/index.vue'),
},
{
path: '/workspace',
name: 'Workspace',
component: () => import('~/pages/workspace/index.vue'),
},
],
},
]
export default staticRoutes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 混合布局开发
我们这里所说的混合布局模式,就是在顶部通栏加上侧边栏,就叫做混合布局模式。
- Header 贯穿整个页面
- 侧边栏在顶部的下面并且高度撑开
- 其余部分为内容部分
# 混合布局开发上
base-layout -> index.vue
<script setup lang="ts">
import MixLayout from '../mix-layout/index.vue';
const appStore = useAppStore();
const { layout } = storeToRefs(appStore);
</script>
<template>
<MixLayout v-if="layout.layout === 'mix'">
<router-view />
</MixLayout>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
mix-layout -> index.vue
<script setup lang="ts">
const props = withDefaults(
defineProps<{
headerHeight?: number;
}>(),
{
headerHeight: 48
}
);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const contentHeightVar = computed(
() => `calc(100vh - ${props.headerHeight}px)`
);
</script>
<template>
<n-layout class="h-screen">
<n-layout-header class="pro-admin-mix-layout-header">
颐和园路
</n-layout-header>
<n-layout has-sider class="pro-admin-mix-layout-content">
<n-layout-sider content-style="padding: 24px;">
海淀桥
</n-layout-sider>
<n-layout-content content-style="padding: 24px;">
<slot />
</n-layout-content>
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
.pro-admin-mix-layout-content {
height: v-bind(contentHeightVar);
}
.n-layout-header,
.n-layout-footer {
background: rgba(128, 128, 128, 0.2);
/* padding: 24px; */
}
.n-layout-sider {
background: rgba(128, 128, 128, 0.3);
}
.n-layout-content {
background: rgba(128, 128, 128, 0.4);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 混合布局开发中
目标
开发 header 通栏部分
布局开发
接下来我们删除掉我们复制过来的样式,接下来我们来开发我们的通栏部分。
内容垂直居中
mix-layout -> index.vue
<script setup lang="ts">
import Logo from '~/layouts/common/logo.vue';
import Title from '~/layouts/common/title.vue';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
}>(),
{
headerHeight: 48
}
);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const contentHeightVar = computed(
() => `calc(100vh - ${props.headerHeight}px)`
);
</script>
<template>
<n-layout class="h-screen">
<n-layout-header
inverted
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" />
<Title :title="title" />
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<n-layout has-sider class="pro-admin-mix-layout-content">
<n-layout-sider content-style="padding: 24px;">
海淀桥
</n-layout-sider>
<n-layout-content content-style="padding: 24px;">
<slot />
</n-layout-content>
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
.pro-admin-mix-layout-content {
height: v-bind(contentHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
layouts -> common -> title.vue
<script setup lang="ts">
import type { CSSProperties } from 'vue'
const props = withDefaults(defineProps<{
title?: string
size: number
}>(), {
size: 24,
})
const titleStyle = computed<CSSProperties>(() => ({
fontSize: `${props.size}px`,
}))
</script>
<template>
<span v-if="title" class="ml-2" :style="titleStyle">{{ title }}</span>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
layouts -> common -> logo.vue
<script setup lang="ts">
withDefaults(
defineProps<{
src?: string;
size?: number;
}>(),
{
size: 24
}
);
</script>
<template>
<n-image :src="src" :width="size" :height="size" :preview-disabled="true" />
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
config -> layout-theme.ts
import logo from '~/assets/vue.svg';
export interface layoutTheme {
title?: string;
layout: 'mix' | 'side' | 'top';
headerHeight: number;
logo?: string;
}
export const layoutThemeConfig: layoutTheme = {
title: 'Naive admin Pro',
layout: 'mix',
headerHeight: 48,
logo
};
2
3
4
5
6
7
8
9
10
11
12
13
14
layouts -> base-layout -> index.vue
<script setup lang="ts">
import MixLayout from '../mix-layout/index.vue';
const appStore = useAppStore();
const { layout } = storeToRefs(appStore);
</script>
<template>
<MixLayout
v-if="layout.layout === 'mix'"
:logo="layout.logo"
:title="layout.title"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MixLayout>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 混合布局开发下
src -> assets -> styles -> index.css
@import "vars.css"
2
3
src -> assets -> styles -> vars.css
html {
--pro-admin-layout-content-bg: #f0f2f5;
}
2
3
main.ts 中引入
import '~/assets/styles/index.css';
layouts -> common -> layout-content.vue
<script setup lang="ts">
const attrs = useAttrs();
</script>
<template>
<n-layout-content
v-bind="attrs"
style="--n-color:var(--pro-admin-layout-content-bg)"
>
// 修改部分
<slot />
</n-layout-content>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
layouts -> common -> index.ts
import Title from './title.vue';
import Logo from './logo.vue';
import LayoutContent from './layout-content.vue';
export { Title, Logo, LayoutContent };
2
3
4
5
mix-layout -> index.vue
<script setup lang="ts">
import { LayoutContent, Logo, Title } from '~/layouts/common'; // 修改部分
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
}>(),
{
headerHeight: 48
}
);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const contentHeightVar = computed(
() => `calc(100vh - ${props.headerHeight}px)`
);
</script>
<template>
<n-layout class="h-screen">
<n-layout-header
inverted
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" />
<Title :title="title" />
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<n-layout has-sider class="pro-admin-mix-layout-content">
<n-layout-sider content-style="padding: 24px;">
海淀桥
</n-layout-sider>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
// 修改部分
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
.pro-admin-mix-layout-content {
height: v-bind(contentHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
layouts -> common -> layout-sider.vue
<script setup lang="ts">
const attrs = useAttrs();
</script>
<template>
<n-layout-sider v-bind="attrs" collapse-mode="width">
<slot />
</n-layout-sider>
</template>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
layouts -> common -> index.ts
import Title from './title.vue';
import Logo from './logo.vue';
import LayoutContent from './layout-content.vue';
import LayoutSider from './layout-sider.vue';
export { Title, Logo, LayoutContent, LayoutSider };
2
3
4
5
6
config -> layout-theme.ts
import logo from '~/assets/vue.svg';
export interface layoutTheme {
title?: string;
layout: 'mix' | 'side' | 'top';
headerHeight: number;
logo?: string;
siderWidth: number;
siderCollapsedWidth: number;
showSiderTrigger: boolean | 'bar' | 'arrow-circle';
}
export const layoutThemeConfig: layoutTheme = {
title: 'Naive admin Pro',
layout: 'mix',
headerHeight: 48,
logo,
siderWidth: 240,
siderCollapsedWidth: 48,
showSiderTrigger: 'bar'
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mix-layout -> index.vue
<script setup lang="ts">
import { LayoutContent, LayoutSider, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
siderWidth?: number;
siderCollapsedWidth?: number;
showSiderTrigger?: boolean | 'bar' | 'arrow-circle';
}>(),
{
headerHeight: 48
}
);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const contentHeightVar = computed(
() => `calc(100vh - ${props.headerHeight}px)`
);
</script>
<template>
<n-layout class="h-screen">
<n-layout-header
inverted
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" />
<Title :title="title" />
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<n-layout has-sider class="pro-admin-mix-layout-content">
<LayoutSider
:collapsed-width="siderCollapsedWidth"
:width="siderWidth"
:show-trigger="showSiderTrigger"
content-style="padding: 24px;"
>
海淀桥
</LayoutSider>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
.pro-admin-mix-layout-content {
height: v-bind(contentHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 侧边通栏布局
目标
完成侧边通栏布局模式
layouts -> side-layout -> index.vue
<script setup lang="ts">
import { LayoutContent, LayoutSider, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
siderWidth?: number;
siderCollapsedWidth?: number;
showSiderTrigger?: boolean | 'bar' | 'arrow-circle';
inverted?: boolean;
}>(),
{
headerHeight: 48,
inverted: false
}
);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const contentHeightVar = computed(
() => `calc(100vh - ${props.headerHeight}px)`
);
</script>
<template>
<n-layout has-sider class="h-screen">
<LayoutSider
:inverted="inverted"
:collapsed-width="siderCollapsedWidth"
:width="siderWidth"
:show-trigger="showSiderTrigger"
content-style="padding: 24px;"
>
<div class="flex items-center">
<Logo :src="logo" />
<Title :title="title" size="20" />
</div>
</LayoutSider>
<n-layout
class="pro-admin-mix-layout-content"
style="--n-color:var(--pro-admin-layout-content-bg)"
>
<n-layout-header
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
颐和园路
</n-layout-header>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
.pro-admin-mix-layout-content {
height: v-bind(contentHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 布局功能完善
目标
- 侧边栏收缩功能配置
- 完善混合布局
侧边栏收缩功能配置
我们先来看一下我们实现的收缩的功能,在默认情况下,我们没有配置受控模式的侧边栏,所以整个收缩的控制是由于 layout-sider 自己控制的看,接下来我们转换成受控模式的方式,我们在 side-layout 中先做属性的定义:
layouts ->side-layout -> index.vue
<script setup lang="ts">
import { update } from 'lodash';
import { LayoutContent, LayoutSider, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
siderWidth?: number;
siderCollapsedWidth?: number;
showSiderTrigger?: boolean | 'bar' | 'arrow-circle';
inverted?: boolean;
collapsed?: boolean;
}>(),
{
headerHeight: 48,
inverted: false,
collapsed: false
}
);
defineEmits(['update:collapsed']);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
</script>
<template>
<n-layout has-sider class="h-screen">
<LayoutSider
:inverted="inverted"
:collapsed="collapsed"
:collapsed-width="siderCollapsedWidth"
:width="siderWidth"
:show-trigger="showSiderTrigger"
@update:collapsed="$event => $emit('update:collapsed', $event)"
>
<div class="flex items-center justify-center mt-24px">
<Logo :src="logo" size="30" />
<Title v-if="!collapsed" :title="title" size="22" />
</div>
</LayoutSider>
<n-layout
class="pro-admin-mix-layout-content"
style="--n-color:var(--pro-admin-layout-content-bg)"
>
<n-layout-header
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<slot name="headerLeft">
<div />
</slot>
<slot name="headerRight">
<div />
</slot>
</n-layout-header>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
layouts ->mix-layout -> index.vue
<script setup lang="ts">
import { LayoutContent, LayoutSider, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
siderWidth?: number;
siderCollapsedWidth?: number;
showSiderTrigger?: boolean | 'bar' | 'arrow-circle';
collapsed?: boolean;
}>(),
{
headerHeight: 48,
collapsed: false
}
);
defineEmits(['update:collapsed']);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const contentHeightVar = computed(
() => `calc(100vh - ${props.headerHeight}px)`
);
</script>
<template>
<n-layout class="h-screen">
<n-layout-header
inverted
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" />
<Title :title="title" />
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<n-layout has-sider class="pro-admin-mix-layout-content">
<LayoutSider
:collapsed-width="siderCollapsedWidth"
:width="siderWidth"
:show-trigger="showSiderTrigger"
>
海淀桥
</LayoutSider>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</n-layout>
</n-layout>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
.pro-admin-mix-layout-content {
height: v-bind(contentHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
config -> layout-theme.ts
import logo from '~/assets/vue.svg';
export interface layoutTheme {
title?: string;
layout: 'mix' | 'side' | 'top';
headerHeight: number;
logo?: string;
siderWidth: number;
siderCollapsedWidth: number;
showSiderTrigger: boolean | 'bar' | 'arrow-circle';
collapsed: boolean;
}
export const layoutThemeConfig: layoutTheme = {
title: 'Naive admin Pro',
layout: 'mix',
headerHeight: 48,
logo,
siderWidth: 240,
siderCollapsedWidth: 48,
showSiderTrigger: 'bar',
collapsed: false
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
layouts -> base-layout -> index.vue
<script setup lang="ts">
import MixLayout from '../mix-layout/index.vue';
import SideLayout from '../side-layout/index.vue';
const appStore = useAppStore();
const { layout } = storeToRefs(appStore);
</script>
<template>
<MixLayout
v-if="layout.layout === 'mix'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MixLayout>
<SideLayout
v-if="layout.layout === 'side'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</SideLayout>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 顶部通栏布局
目标
完成顶部通栏布局模式的开发
# 布局文件初始化
在 layouts 目录下创建一个 top-layout 布局文件夹然后创建一个 index.vue 文件。
由于我们顶部通栏的布局模式和我们的混合布局的模式差不多,所以我们直接 copy 我们混合布局的样式进行改造。
layouts -> base-layout -> index.vue
<script setup lang="ts">
import MixLayout from '../mix-layout/index.vue';
import SideLayout from '../side-layout/index.vue';
import TopLayout from '../top-layout/index.vue';
const appStore = useAppStore();
const { layout } = storeToRefs(appStore);
</script>
<template>
<MixLayout
v-if="layout.layout === 'mix'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MixLayout>
<SideLayout
v-if="layout.layout === 'side'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</SideLayout>
<TopLayout
v-if="layout.layout === 'top'"
:logo="layout.logo"
:title="layout.title"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</TopLayout>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
layouts -> common -> layout.vue
<script setup lang="ts"></script>
<template>
<n-layout style="--n-color:var(--pro-admin-layout-content-bg)">
<slot />
</n-layout>
</template>
<style scoped></style>
2
3
4
5
6
7
8
9
layouts ->top-layout -> index.vue
<script setup lang="ts">
import { LayoutBase, LayoutContent, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
inverted?: boolean;
}>(),
{
headerHeight: 48,
inverted: false
}
);
defineEmits(['update:collapsed']);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
</script>
<template>
<LayoutBase class="h-screen">
<n-layout-header
:inverted="inverted"
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" />
<Title :title="title" />
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</LayoutBase>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
config -> layout-theme.ts
import logo from '~/assets/vue.svg';
export interface layoutTheme {
title?: string;
layout: 'mix' | 'side' | 'top';
headerHeight: number;
logo?: string;
siderWidth: number;
siderCollapsedWidth: number;
showSiderTrigger: boolean | 'bar' | 'arrow-circle';
collapsed: boolean;
}
export const layoutThemeConfig: layoutTheme = {
title: 'Naive admin Pro',
layout: 'top',
headerHeight: 48,
logo,
siderWidth: 240,
siderCollapsedWidth: 48,
showSiderTrigger: 'bar',
collapsed: false
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 移动端模式开发
目标
- 开发试图断电查询可组合式 api
- 基本的移动端布局
# 常用的视图断点枚举值
我们在开发中和视图大小相关的代名词像是: xi 、lg、md 等等。
我们这里定义的视图大小范围值也是一样的。
# 创建组合式 api 文件
在 composables 文件夹下创建一个 query-breakpoints 的文件。
然后首先定义我们的断点枚举值:
query-breakpoints.ts
export const breakpointsEnum = {
xl: 1600,
lg: 1199,
md: 991,
sm: 767,
xs: 575
};
export const useQueryBreakpoints = () => {
const breakpoints = reactive(useBreakpoints(breakpointsEnum));
// 手机端
const isMobile = breakpoints.smaller('sm');
// pad端
const isPad = breakpoints.between('sm', 'md');
// pc端
const isDesktop = breakpoints.greater('md');
return {
breakpoints,
isMobile,
isPad,
isDesktop
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
layouts ->mobile-layout -> index.vue
<script setup lang="ts">
import { LayoutBase, LayoutContent, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
inverted?: boolean;
visible?: boolean;
}>(),
{
headerHeight: 48,
inverted: false,
visible: false
}
);
defineEmits(['update:collapsed']);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
</script>
<template>
<LayoutBase class="h-screen">
<n-layout-header
:inverted="inverted"
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" />
<!-- <Title :title="title" /> -->
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</LayoutBase>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
base-layout -> index.vue
<script setup lang="ts">
import MixLayout from '../mix-layout/index.vue';
import SideLayout from '../side-layout/index.vue';
import TopLayout from '../top-layout/index.vue';
import MobileLayout from '../mobile-layout/index.vue';
const appStore = useAppStore();
const { layout } = storeToRefs(appStore);
const { isMobile } = useQueryBreakpoints();
</script>
<template>
<MobileLayout v-if="isMobile" :logo="layout.logo" :title="layout.title">
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MobileLayout>
<template v-else>
<MixLayout
v-if="layout.layout === 'mix'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MixLayout>
<SideLayout
v-if="layout.layout === 'side'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</SideLayout>
<TopLayout
v-if="layout.layout === 'top'"
:logo="layout.logo"
:title="layout.title"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</TopLayout>
</template>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
安装 图标依赖
npm i -D @vicons/antd
修改 mobile-layout -> index.vue
<script setup lang="ts">
import { MenuUnfoldOutlined } from '@vicons/antd';
import { LayoutBase, LayoutContent, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
inverted?: boolean;
visible?: boolean;
}>(),
{
headerHeight: 48,
inverted: false,
visible: false
}
);
const emits = defineEmits(['update:visible']);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const onShow = () => emits('update:visible', true);
</script>
<template>
<LayoutBase class="h-screen">
<n-layout-header
:inverted="inverted"
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" size="26" />
<n-icon size="24" class="ml-12px" @click="onShow">
<MenuUnfoldOutlined />
</n-icon>
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</LayoutBase>
<n-drawer
:show="visible"
:width="240"
placement="left"
@update:show="val => $emit('update:visible', val)"
>
<n-drawer-content title="斯通纳">
《斯通纳》是美国作家约翰·威廉姆斯在 1965 年出版的小说。
</n-drawer-content>
</n-drawer>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
修改 stores -> app.ts
import { layoutThemeConfig } from '~/config/layout-theme';
export const useAppStore = defineStore('app', () => {
const defaultTheme = import.meta.env.DEV
? layoutThemeConfig
: useLayoutTheme();
const layout = reactive(unref(defaultTheme));
const visible = ref(false);
const toggleVisible = (val: boolean) => {
visible.value = val;
};
const toggleCollapsed = (val: boolean) => {
layout.collapsed = val;
};
return {
layout,
visible,
toggleVisible,
toggleCollapsed
};
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
修改 base -layout -> index.vue
<script setup lang="ts">
import MixLayout from '../mix-layout/index.vue';
import SideLayout from '../side-layout/index.vue';
import TopLayout from '../top-layout/index.vue';
import MobileLayout from '../mobile-layout/index.vue';
const appStore = useAppStore();
const { layout, visible } = storeToRefs(appStore);
const { isMobile } = useQueryBreakpoints();
</script>
<template>
<MobileLayout
v-if="isMobile"
v-model:visible="visible"
:logo="layout.logo"
:title="layout.title"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MobileLayout>
<template v-else>
<MixLayout
v-if="layout.layout === 'mix'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</MixLayout>
<SideLayout
v-if="layout.layout === 'side'"
v-model:collapsed="layout.collapsed"
:logo="layout.logo"
:title="layout.title"
:show-sider-trigger="layout.showSiderTrigger"
:sider-width="layout.siderWidth"
:sider-collapsed-width="layout.siderCollapsedWidth"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</SideLayout>
<TopLayout
v-if="layout.layout === 'top'"
:logo="layout.logo"
:title="layout.title"
>
<template #headerRight>
<div>测试右侧插槽</div>
</template>
<router-view />
</TopLayout>
</template>
</template>
<style lang="scss" scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 移动端布局完善
目标
完成移动端布局反转色支持
完成移动端到 pad 端的菜单转换
# 改进反转色模式
我们在移动模式下,我们的侧边栏和我们的 header 部分都需要反转色的功能,并且是单独控制的,所以删除原先的属性,并增加两个新的属性分别是 headerInverted,drawerInverted
# 实现 drawer 反转色功能
我们需要实现 drawer 的反转色功能的话,我们可以借助 layout 布局的方式显示。
- 取消边框的 padding
- 使用 n-layout 布局
- 显示 logo 和标题,可以动态的隐藏和显示(参考 side-layout 布局效果)
mobile-layout -> index.vue
<script setup lang="ts">
import { MenuUnfoldOutlined } from '@vicons/antd';
import { LayoutBase, LayoutContent, Logo, Title } from '~/layouts/common';
const props = withDefaults(
defineProps<{
headerHeight?: number;
logo?: string;
title?: string;
headerInverted?: boolean;
drawerInverted?: boolean;
visible?: boolean;
logoVIsible?: boolean;
}>(),
{
headerHeight: 48,
headerInverted: false,
drawerInverted: false,
visible: false,
logoVIsible: true
}
);
const emits = defineEmits(['update:visible']);
const headerHeightVar = computed(() => `${props.headerHeight}px`);
const onShow = () => emits('update:visible', true);
</script>
<template>
<LayoutBase class="h-screen">
<n-layout-header
:inverted="headerInverted"
class="pro-admin-mix-layout-header flex items-center justify-between px-4"
>
<div class="flex items-center">
<Logo :src="logo" size="26" />
<n-icon size="24" class="ml-12px" @click="onShow">
<MenuUnfoldOutlined />
</n-icon>
</div>
<slot name="headerRight">
<div>右侧</div>
</slot>
</n-layout-header>
<LayoutContent content-style="padding: 24px;">
<slot />
</LayoutContent>
</LayoutBase>
<n-drawer
:show="visible"
:width="240"
placement="left"
@update:show="val => $emit('update:visible', val)"
>
<n-drawer-content body-content-style="padding: 0">
<n-layout class="h-100%">
<n-layout-header class="h-100%" inverted>
<div
v-if="logoVIsible"
class="flex items-center justify-center py-12px"
>
<Logo :src="logo" size="26" />
<Title :title="title" size="22" />
</div>
</n-layout-header>
</n-layout>
</n-drawer-content>
</n-drawer>
</template>
<style scoped>
.pro-admin-mix-layout-header {
height: v-bind(headerHeightVar);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# 布局切换抽屉开发
目标
完成布局切换抽屉样式的开发
背景
我们开发了这么多套主题,那么是不是应该有个地方可以去灵活的切换我们的主题呢?接下来我们就一起来开发一下这一部分。
开发
创建文件
在 layouts 目录下创建一个 setting-drawer 的文件夹,并同时创建一个 index.vue 文件
<script setup lang="ts"></script>
<template>
<div />
</template>
<style scoped></style>
2
3
4
5
6
7
思路
- 页面中要有一个浮动的按钮
- 实现一个抽屉以及实现点击按钮打开抽屉功能
- 实现关闭抽屉的浮动按钮
- 切换布局以及其他个性化配置
setting-drawer -> index.vue
<script setup lang="ts">
import { CloseOutlined, SettingOutlined } from '@vicons/antd';
const props = withDefaults(
defineProps<{
floatTop?: number | string;
drawerWidth?: number | string;
}>(),
{
floatTop: 240,
drawerWidth: 300
}
);
const show = ref(false);
const handleClick = (val: boolean) => {
show.value = val;
};
const cssVar = computed(() => {
return {
'--pro-admin--float-top': `${props.floatTop}px`,
'--pro-admin--drawer-width': `${props.drawerWidth}px`
};
});
</script>
<template>
<Teleport to="body">
<div
:style="cssVar"
class="fixed top-[var(--pro-admin--float-top)] right-0"
>
<n-button
class="b-rd-tr-0! b-rd-br-0!"
size="large"
type="primary"
@click="handleClick(true)"
>
<template #icon>
<n-icon size="24">
<SettingOutlined />
</n-icon>
</template>
</n-button>
</div>
</Teleport>
<n-drawer v-model:show="show" :width="drawerWidth">
<n-drawer-content>
这里是内容区域
</n-drawer-content>
<div
:style="cssVar"
class="absolute top-[var(--pro-admin--float-top)] right-[var(--pro-admin--drawer-width)]"
>
<n-button
class="b-rd-tr-0! b-rd-br-0!"
size="large"
type="primary"
@click="handleClick(false)"
>
<template #icon>
<n-icon size="24">
<CloseOutlined />
</n-icon>
</template>
</n-button>
</div>
</n-drawer>
</template>
<style scoped></style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 布局缩略图开发
目标
实现布局的缩略图选择组件
用途
完成了抽屉布局的开发后,接下来为了能让用户直接看到我们大概的布局样式,那么我们通过缩略图的方式来实现一下我们的切换布局的效果。
开发
我们在 setting-drawer 目录下创建一个 checkbox-layout.vue 的文件用于开发我们的选择布局的组件。
为了更直观的能看到效果我们在 pages/index.vue 中进行开发,开发完成后我们再迁移我们的 setting-drawer 中去,如下: