书接上回
本篇主要介绍: Layout布局,导航栏,标签页
继续填充
目录
- 按需引入组件
- Layout布局,导航栏,标签页
- css样式
按需引入组件
使用unplugin-vue-components插件完成ant-design-vue组件的按需加载。
前文中已处理过,详情见前文
链接: Vue3 + TS + Antd + Pinia 从零搭建后台系统(一)
此处还需在tsconfig.json同级添加文件 components.d.ts。
tsconfig.json文件配置如下:
{
"compilerOptions": {
"target": "es2019",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"allowJs": true,
"importHelpers": true,
"moduleResolution": "node",
"outDir": "temp",
"resolveJsonModule": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"paths": {
"@/*": ["src/*"]
},
"types": [
"@intlify/unplugin-vue-i18n/types",
"vite/client",
"element-plus/global",
"@types/qrcode",
"vite-plugin-svg-icons/client",
"./components.d.ts"
],
"baseUrl": "./",
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
"include": [
"src/types/**/*.ts",
"src/types/**/*.tsx",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx",
"src/components/BpmnEditor/lib/ReadOnly/ReadOnly.js",
"src/plugin/index.js"
],
"exclude": ["dist", "node_modules"]
}
Layout布局,导航栏,标签页
这里统一写在layout文件夹下面
此处用到的图标为iconfont图标库中的图标,可替换为ant-design-vue中的图标
index.vue文件:
<Layout style="height: 100%; width: 100%">
<!-- 左侧菜单栏 -->
<Layout-sider
:class="collapsed ? 'side-logo' : 'side-title'"
v-model:collapsed="collapsed"
:trigger="null"
collapsible
>
<div style="height: 50px">
<img class="logo-style" src="../../assets/images/logo.png" />
<div v-if="!collapsed" class="title-style">管理平台</div>
</div>
<Menu
v-model:selectedKeys="selectedKeys"
v-model:openKeys="openKeys"
theme="dark"
mode="inline"
>
<Sub-menu v-for="sub in menuList" :key="sub.path">
<template #title>
<span :class="sub.meta.icon" style="margin-right: 8px"></span>
<span>{{ !collapsed ? sub.meta.title : "" }}</span>
</template>
<Menu-item v-for="item in sub.children" :key="item.path">
<span :class="item.meta.icon"></span>
<Router-link :to="`${sub.path}/${item.path}`">{{ item.meta.title }}</Router-link>
</Menu-item>
</Sub-menu>
</Menu>
</Layout-sider>
<!-- 右侧面包屑、标签页、界面 -->
<Layout>
<Layout-header style="height: 75px">
<div class="layHeader">
<div
:class="`trigger icon-new ${!collapsed ? 'icon-new-outdent' : 'icon-new-indent'}`"
@click="() => (collapsed = !collapsed)"
/>
<!-- separator=">" 设置层级间展示的符号 默认‘/’-->
<Breadcrumb>
<Breadcrumb-item v-for="item in breadcrumbList" :key="item.path">
<span>{{ item.meta.title }}</span>
</Breadcrumb-item>
</Breadcrumb>
<Dropdown>
<Button class="icon-new icon-new-user loginOut" type="primary" ghost></Button>
<template #overlay>
<Menu>
<Menu-item>{{ `用户名:${username}` }}</Menu-item>
<Menu-item @click="loginOut">退出系统</Menu-item>
</Menu>
</template></Dropdown
>
</div>
<div style="display: flex">
<span class="icon-new icon-new-doubleleft spanIcon"></span>
<Tabs
v-model:activeKey="activeKey"
hide-add
type="editable-card"
@edit="onEdit"
@tabClick="onTabClick"
>
<Tab-pane v-for="item in tabPanes" :key="item.path" :tab="item.meta.title"> </Tab-pane>
</Tabs>
<Dropdown :trigger="['click']">
<div class="icon-new icon-new-doubleright spanIcon" ></div>
<template #overlay>
<Menu>
<Menu-item v-for="item in tabPanes" :key="item.path">
<Router-link :to="item.path">{{ item.meta.title }}</Router-link>
</Menu-item>
</Menu>
</template>
</Dropdown>
<div
class="icon-new icon-new-close-circle spanIcon"
title="关闭其他"
@click="closeOthers"
></div>
<div class="icon-new icon-new-sync spanIcon" title="刷新" @click="reLoad"></div>
</div>
</Layout-header>
<Layout-content class="content-style">
<Router-view />
</Layout-content>
</Layout>
</Layout>
index.ts文件:
import { defineComponent, reactive, toRefs, watch } from "vue";
import router from "@/router";
import { useUserStore } from "@/pinia/user";
export default defineComponent({
name: "Layout",
setup() {
const datas = reactive({
selectedKeys: ["BBB"],
openKeys: ["/AAA"],
collapsed: false,
menuList: [] as any,
breadcrumbList: [],
activeKey: "",
tabPanes: [] as any,
username: null as any,
});
const methods = reactive({
init() {
datas.username = JSON.parse(localStorage.getItem("user") as any).userInfo?.username;
datas.menuList = router.options.routes.filter((v: any) => !v.meta.hideInMenu);
datas.selectedKeys = [router.currentRoute.value.name];
datas.openKeys = [router.currentRoute.value.matched[0].path];
datas.breadcrumbList = router.currentRoute.value.matched;
},
onEdit(val: any) {
let lastIndex = 0;
if (datas.tabPanes.length > 1) {
datas.tabPanes.forEach((item: any, i: number) => {
if (item.path == val) {
lastIndex = i - 1;
}
});
datas.tabPanes = datas.tabPanes.filter((v: { path: any }) => v.path !== val);
if (datas.tabPanes.length && datas.activeKey == val) {
if (lastIndex >= 0) {
datas.activeKey = datas.tabPanes[lastIndex].path;
datas.selectedKeys = [datas.tabPanes[lastIndex].name];
router.push(datas.tabPanes[lastIndex].path);
} else {
datas.activeKey = datas.tabPanes[0].path;
datas.selectedKeys = [datas.tabPanes[0].name];
}
}
}
},
onTabClick(val: any) {
router.push(val);
},
loginOut() {
// 使用Pinia中定义的退出系统的方法
const userStore = useUserStore();
userStore.logoutConfirm();
},
closeOthers() {
datas.tabPanes = datas.tabPanes.filter((v: { path: any }) => v.path == datas.activeKey);
},
reLoad() {
window.location.reload();
},
});
methods.init();
watch(
() => router.currentRoute.value.matched,
(val) => {
// 路由变化时,更新面包屑及标签页
datas.breadcrumbList = val;
datas.activeKey = router.currentRoute.value.path;
if (datas.tabPanes.findIndex((v: any) => v.path == router.currentRoute.value.path) == -1) {
datas.tabPanes.push(router.currentRoute.value);
}
},
{ immediate: true }
);
return {
...toRefs(datas),
...toRefs(methods),
};
},
});
css样式
style文件夹下,创建index.css 、antd.css、 layout.css
index.css
@import './antd.css';
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
#app {
margin: 0 auto;
}
.main-content {
margin: 10px;
height: 100%;
overflow-y: auto;
}
layout.css 样式
.side-logo {
min-width: 60px !important;
flex: 0 0 60px !important;
}
.side-title {
flex: 0 0 180px !important;
min-width: 180px !important;
}
.logo-style {
float: left;
margin: 4px;
width: 40px;
}
.title-style {
color: white;
font-size: 18px;
line-height: 48px;
float: left;
margin: 4px;
font-weight: 700;
}
.trigger {
width: 32px;
font-size: 18px;
line-height: 40px;
padding: 0px;
cursor: pointer;
transition: color 0.3s;
margin: 0 6px;
}
.trigger:hover {
color: #5487eae0 !important;
}
.content-style {
margin: 8px;
padding: 4px;
background: #fff;
min-height: 280px;
}
.loginOut {
margin: 6px 0px 0;
font-size: 15px;
cursor: pointer;
border-radius: 50% !important;
padding: 0px 0px !important;
width: 30px;
}
.loginOut:hover {
color: #5487eae0 !important;
}
.fade-enter-active .fade-leave-active {
transition: opacity 0.5s;
}
.layHeader {
display: flex;
height: 40px;
border-bottom: 1px solid #05050530;
}
.spanIcon {
width: 32px;
height: 32px;
border: 1px solid #05050530;
padding: 8px 6px;
opacity: 0.5;
line-height: 14px;
}
.msg-style {
background-color: #5487eae0;
}
.collased-menu-dropdown {
transition: background 0.2s ease-in-out;
}
antd.css 调整组件库中的样式
.ant-btn {
font-size: 14px !important;
height: 28px !important;
padding: 0px 10px !important;
border-radius: 4px !important;
margin: 2px 8px 2px 0px;
}
.ant-btn::before {
margin: 4px;
}
.ant-btn:hover {
opacity: 0.8;
}
.ant-form-inline .ant-form-item {
margin: 0 8px 0 0 !important;
}
.ant-menu-dark .ant-menu-item-selected {
border-radius: 4px !important;
}
.ant-layout .ant-layout-header {
padding-inline: 3px !important;
background: #fff;
}
.ant-breadcrumb {
padding: 8px 0 !important;
font-size: 15px !important;
width: calc(100% - 80px) !important;
height: 50px;
}
.ant-tabs {
width: calc(100% - 80px);
}
.ant-tabs-nav {
height: 32px !important;
margin-bottom: 8px !important;
}
.ant-tabs-nav .ant-tabs-nav-wrap {
border-bottom: 1px solid #e6ebf3;
}
.ant-tabs-nav .ant-tabs-tab {
border-top-right-radius: 4px !important;
border-top-left-radius: 4px !important;
padding: 8px 8px !important;
}
.ant-tabs .ant-tabs-tab-remove {
margin: 2px -4px 0px 4px !important;
}
.ant-modal .ant-modal-header {
margin-bottom: 20px;
}
项目效果图:
项目至此基本搭建结束。后续优化或添加功能,不定期更新。ヾ(•ω•`)o