项目搭建
创建项目
perl
//====第一步创建项目========================
1.npm init vue
2.输入项目名称
3.选择ts语法,router,pinia
4.npm i
5.npm run dev
//====第二步进入项目删除无用的代码,保证页面是空白的
保留 src/assets/base.css
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
//将这个引入到main.js中,这个放在最上面,初始化这个模块
//安装必要的依赖
npm install --save-dev @arco-design/web-vue
npm i axios
npm i imockjs
//安装less 因为要预留arco的主题色,所以使用less作为css的预处理会更加方便
npm install less -D
npm install less-loader -D
main.ts
javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import "@/assets/base.css"//引入初始化样式(放在最前面)
import App from './App.vue'
import router from './router'
import ArcoVue from '@arco-design/web-vue'; //引入arco_design
import '@arco-design/web-vue/dist/arco.css';//引入arco_design 样式
import ArcoVueIcon from '@arco-design/web-vue/es/icon'; //引入arco_design 图标
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ArcoVue); //注册arco_design
app.use(ArcoVueIcon);//注册arco_design图标
app.mount('#app')
路由构建
src\router\index.ts
javascript
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
name:"home",
path:"/",
redirect:"/admin"
},
{
name:"web",
path:"/web",
component:()=>import("@/views/web/index.vue")
},
{
name:"login",
path:"/login",
component:()=>import("@/views/login/index.vue")
},
{
name:"admin",
path:"/admin",
component:()=>import("@/views/admin/index.vue")
}
],
})
export default router
创建目录
bash
└─views
├─admin
├─login
└─web
项目设计
admin样式初调
src\views\admin\index.vue
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<icon-sun-fill />
<icon-moon-fill />
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px solid var(--color-neutral-2);
.g_logo {
width: 100%;
height: 90px;
border-bottom: 1px solid var(--color-neutral-2);
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: 1px solid var(--color-neutral-2);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: 1px solid var(--color-neutral-2);
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
}
}
}</style>
使用arco desgin的less变量
- 在vite.config.ts中配置vite帮忙预处理,可以配置处理lessyu
javascript
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),],
// 配置less预处理
css:{
preprocessorOptions:{
less:{
additionalData:'@import "@arco-design/web-vue/es/style/theme/global.less";',
javascriptEnabled:true,
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
- 使用,首先可以使用变量直接代替原来的元素配置,--color-neutral-2是arco-desigin的变量语法
javascript
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: 1px solid @color-border-1;
.g_logo {
width: 100%;
height: 90px;
border-bottom: 1px solid var(--color-neutral-2);
}
}
- 也可以提取出来将border-right: 1px solid @color-border-1; 作为一个完整的变量,这样就直接使用一个简介明了的变量名称就可
javascript
<style lang='less'>
@g_border:1px solid @color-border-1;
.g_admin {
display: flex;
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: 1px solid var(--color-neutral-2);
}
}
- 但是我们将这个提取到变量中,这样就可以全局使用这个变量,方便代码在全局中使用
src\assets\var.less【封装这个变量在这个文件中】
javascript
@import "@arco-design/web-vue/es/style/theme/global.less";
@g_border:1px solid @color-border-1;
src\views\admin\index.vue【在实际的代码中使用只用写这个变量名即可】
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<icon-sun-fill />
<icon-moon-fill />
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
}
}
}</style>
vite.config.ts【记得修改配置文件将预处理的文件路径进行修改】
javascript
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),],
// 配置less预处理
css:{
preprocessorOptions:{
less:{
modifyVars:{
// 'primary-6':"red" //修改arco-disgin的主题色,默认是蓝色
},
additionalData:'@import "@/assets/var.less";',
javascriptEnabled:true,
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
主题切换
src\views\admin\index.vue【已可以实现出题切换】
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import { ref } from 'vue';
const theme = ref('')
// setTheme 切换主题色函数,根据arco-disgin的特性,将arco-theme设置成dark就是默认是黑夜模式
function setTheme(val: string){
if (val === 'dark'){
document.body.setAttribute('arco-theme','dark')
}else{
document.body.removeAttribute('arco-theme')
}
theme.value = val
}
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<icon-sun-fill v-if="theme === 'dark'" @click="setTheme('')"/>
<icon-moon-fill v-if="theme === ''" @click="setTheme('dark')"/>
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
}
}
}</style>
主题切换代码抽离并暴露出去
src\components\common\g_theme.ts【将主题切换函数封装入ts中并暴露出去供其他调用】
javascript
import { ref } from 'vue';
export const theme = ref('')
// setTheme 切换主题色函数,根据arco-disgin的特性,将arco-theme设置成dark就是默认是黑夜模式
export function setTheme(val: string){
if (val === 'dark'){
document.body.setAttribute('arco-theme','dark')
}else{
document.body.removeAttribute('arco-theme')
}
theme.value = val
localStorage.setItem("g_theme",val) //保存到缓存中
}
// loadTheme 获取主题色函数
export function loadTheme(){
const val = localStorage.getItem("g_theme") //从缓存中获取主题
if (val){
if (val === 'dark'){
theme.value = val
setTheme(val) //切换主题
}
}
}
loadTheme() //执行函数
src\components\common\g_theme.vue【主题切换样式】
javascript
<script setup lang='ts'>
import { theme,setTheme,loadTheme } from './g_theme';
</script>
<template>
<!-- title在span标签中才会在页面中展示,所有在外层套一个span标签 -->
<span v-if="theme === 'dark'" title="白天模式"> <icon-sun-fill @click="setTheme('')"/></span>
<span v-if="theme === ''" title="黑夜模式"><icon-moon-fill @click="setTheme('dark')"/></span>
</template>
<style scoped>
</style>
src\views\admin\index.vue【使用g_theme组件】
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import { ref } from 'vue';
import G_theme from '../../components/common/g_theme.vue';
</script>
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<G_theme></G_theme>
<icon-fullscreen />
<icon-fullsreen-exit />
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: @color-fill-1; //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
width: 240px;
height: 100vh;
overflow-y: auto;
overflow-x: hidden;
border-right: @g_border;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
}
.g_main {
width: calc(100% - 240px);
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
}
}
}</style>
全屏切换
src\components\common\g_screen.vue
javascript
<script setup lang='ts'>
import { ref } from "vue"
const isFullSreen = ref(false)
//全屏
function fullScreen() {
document.documentElement?.requestFullscreen()
isFullSreen.value = true
}
//退出全屏
function exitFullScreen() {
document?.exitFullscreen()
isFullSreen.value = false
}
</script>
<template>
<!-- title在span标签中才会在页面中展示,所有在外层套一个span标签 -->
<span v-if="!isFullSreen" title="全屏"> <icon-fullscreen @click="fullScreen" /></span>
<span v-else title="退出全屏"> <icon-fullscreen-exit @click="exitFullScreen" /></span>
</template>
<style scoped lang='scss'></style>
然后调用
src\views\admin\index.vue
javascript
<template>
<div class="g_admin">
<div class="g_aside">
<div class="g_logo"></div>
<div class="g_menu"></div>
</div>
<div class="g_main">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<G_theme></G_theme>
<G_screen></G_screen>
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
菜单配置
【点击菜单进行路由跳转、刷新在当前路由时自动展开路由层级、菜单栏侧边按钮展开收缩实现】
src\components\admin\g_menu.vue
(记得创建响应的视图目录结构,可以参考router/index.ts)
javascript
<script setup lang='ts'>
import { Component } from 'vue';
import { IconHome, IconUser, IconSettings } from "@arco-design/web-vue/es/icon"
import G_iconComponent from '../common/g_iconComponent.vue';
import {collapsed} from "../../components/admin/g_menu"
import { ref } from 'vue';
import router from '@/router';
import { useRoute } from 'vue-router';
const route = useRoute()
interface MenuType {
title: string
name: string
icon?: string | Component
children?: MenuType[]
}
const menuList: MenuType[] = [
{ title: "首页", name: "home", icon: IconHome },
{
title: "个人中心", name: "userCenter", icon: IconUser, children: [
{ title: "用户信息", name: "userinfo" },
]
},
{
title: "用户管理", name: "userManage", icon: IconUser, children: [
{ title: "用户列表", name: "userlist" },
]
},
{
title: "系统设置", name: "systemCenter", icon: IconSettings, children: [
{ title: "系统信息", name: "systeminfo" },
]
},
]
// menuItemClick 菜单点击跳转函数
function menuItemClick(key:string) {
router.push({
name:key
})
}
const openkeys = ref<string[]>([])
// 路由初始化函数,判断路由的层级是否是三级,将中间的路由名称赋值给openkeys可以实现层级展开,只适用于三级的路层级,三级以上的路由层级需要遍历处理给openkeys,
function initRoute(){
if (route.matched.length ===3){
openkeys.value = [route.matched[1].name as string]
}
}
initRoute()
console.log(route);
</script>
<template>
<div class="g_menu" :class="{collapsed:collapsed}">
<div class="g_menu_inner scrollbar">
<a-menu
@menu-item-click="menuItemClick"
v-model:collapsed="collapsed"
v-model:open-keys="openkeys"
:default-selected-keys="[route.name]"
show-collapse-button >
<!-- 注意这里的key必须使用name字段,因为在处理route时是通过name字段来进行判断的 -->
<template v-for="menu in menuList">
<a-menu-item :key="menu.name" v-if="!menu.children">
<template #icon>
<G_iconComponent :is="menu.icon">
{{ menu.icon }}</G_iconComponent>
</template>
{{ menu.title }}
</a-menu-item>
<a-sub-menu :key="menu.name" v-else :title="menu.title">
<template #icon>
<G_iconComponent :is="menu.icon"></G_iconComponent>
</template>
<a-menu-item :key="sub.name" v-for="sub in menu.children">
{{ sub.title }}
<template #icon>
<G_iconComponent :is="sub.icon"></G_iconComponent>
</template>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</div>
</div>
</template>
<style lang='less'>
.g_menu {
height: calc(100vh - 90px);
position: relative;
&.collapsed{
.arco-menu-collapse-button {
left: 48px !important;
}
}
&:hover {
.arco-menu-collapse-button {
opacity: 1 !important;
}
}
.g_menu_inner {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.arco-menu {
position: inherit;
.arco-menu-collapse-button {
top: 50%;
transform: translate(-50%, -50%);
left: 240px;
transition: all .3s;
opacity: 0;
}
}
}
}
</style>
src\components\admin\g_menu.ts
javascript
import { ref } from 'vue';
// 使用ts的方式暴露出去,方便后续别的地方调佣
export const collapsed = ref(false)
src\components\common\g_iconComponent.vue
javascript
<script setup lang='ts'>
import type { Component } from 'vue';
interface Props {
is?: Component | string
}
const props = defineProps<Props>()
</script>
<!-- 处理图标显示,如果是 Component类型,直接使用component渲染,如果是字符串,使用i标签渲染-->
<template>
<component v-if="typeof props.is === 'object'" :is="props.is"></component>
<i :class="props.is" v-else></i>
</template>
src\router\index.ts
javascript
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
name: "home",
path: "/",
redirect: "/admin"
},
{
name: "web",
path: "/web",
component: () => import("@/views/web/index.vue")
},
{
name: "login",
path: "/login",
component: () => import("@/views/login/index.vue")
},
{
name: "admin",
path: "/admin",
component: () => import("@/views/admin/index.vue"),
meta:{title:"首页"},
children: [
{ name: "home", path: "", component: () => import("@/views/admin/home/index.vue") },
{
name: "userCenter", path: "user_Center", children: [{name: "userinfo",path: "user_info",component: () => import("@/views/admin/user_center/index.vue"),meta:{title:"用户信息"} }],
meta:{title:"用户中心"}
},
{
name: "userManage", path: "user_Manage", children: [
{ name: "userlist", path: "user_list", component: () => import("@/views/admin/user_manage/index.vue"),meta:{title:"用户列表"} }],
meta:{title:"用户管理"}
},
{
name: "systemCenter", path: "system_Center", children: [
{ name: "systeminfo", path: "system_info", component: () => import("@/views/admin/system_manage/index.vue"),meta:{title:"系统管理"} }],
meta:{title:"系统信息"}
},
]
}
],
})
export default router
src\views\admin\index.vue
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import G_theme from '../../components/common/g_theme.vue';
import G_screen from '../../components/common/g_screen.vue';
import G_menu from '../../components/admin/g_menu.vue'
import {collapsed} from "../../components/admin/g_menu"
</script>
<template>
<div class="g_admin" >
<!-- 给gasid 添加动态的collapsed类,在触发收缩时添加,方便给aside设置宽度 -->
<div class="g_aside" :class="{collapsed:collapsed}">
<div class="g_logo"></div>
<G_menu></G_menu>
</div>
<div class="g_main" :class="{collapsed:collapsed}">
<div class="g_head">
<div class="g_breadcrumbs"></div>
<div class="g_actions">
<icon-home />
<G_theme></G_theme>
<G_screen></G_screen>
<div class="g_user_info_action">
</div>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
// 不能使用overflow。否则,收缩展开按钮会被覆盖
width: 240px;
height: 100vh;
border-right: @g_border;
transition: width .3s;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
//g_main下添加了collapsed类,如果存在就将这边框设置为48px即为收起状态
&.collapsed{
width: 48px;
// &~只有下兄弟标签适用,将g_main的宽度进行调整
&~.g_main{
width: calc(100% - 48px);
}
}
}
.g_main {
width: calc(100% - 240px);
transition: width .3s;
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
}
}
}</style>
面包屑配置
在admin/index.vue中将g_breadcrumbs换成该组件
src\components\admin\g_breadcrumbs.vue
javascript
<script setup lang='ts'>
import {type RouteMeta,useRoute} from "vue-router"
const route =useRoute()
export interface MetaType extends RouteMeta{
title:string
}
</script>
<!-- 利用router的meta的title显示中文作为面包屑,但是的严格按照路由的配置来,这里也可以添加跳转点击 -->
<template>
<a-breadcrumb>
<template v-for="r in route.matched">
<a-breadcrumb-item v-if="r.name !=='home'">{{ (r.meta as MetaType)?.title }}</a-breadcrumb-item>
</template>
</a-breadcrumb>
</template>
<style scoped lang='scss'>
</style>
iconfont图标引入
在iconfont-阿里巴巴矢量图标库 中选入好图标,添加到项目中,下载下来保存为iconfont.css到assets下,然后在main.ts中import "@/assets/iconfont.css" 即可使用
用户下拉部分
src\components\common\g_user_dropdown.vue
javascript
<script setup lang='ts'>
import router from '@/router';
function handleSelect(val:string){
if (val === 'logout'){
// 注销登录
return
}
// 其他选项跳转到相应的页面中去
router.push({
name:val
})
}
interface OptionType {
name:string
title:string
}
// name必须与路由对上才能正确跳转
const options :OptionType[] = [
{title:"用户中心",name:"userinfo"},
{title:"用户管理",name:"userlist"},
{title:"系统信息",name:"systeminfo"},
{title:"用户注销",name:"logout"},
]
</script>
<template>
<a-dropdown @select="handleSelect" :popup-max-height="false">
<div class="g_user_dropdown_com">
<a-avatar :size="35" image-url="data:image/jpeg;base64,HE+CeI9ZpFpZsTIzT7GiSKpi6Qj5xJJotxeVhTaG0xibRF2fbD7Q+3lZQ8QPtj7D7Z//2Q=="></a-avatar>
<span class="user_name">王五</span>
<icon-down></icon-down>
</div>
<template #content>
<a-doption v-for="item in options" :value="item.name">{{ item.title }}</a-doption>
</template>
</a-dropdown>
</template>
<style lang='less'>
.g_user_dropdown_com{
cursor: pointer;
.user_name{
margin: 0 5px;
}
svg{
margin-right: 0 !important;
}
}
</style>
src\views\admin\index.vue【引入组件并调节下拉部分的样式】
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import G_theme from '../../components/common/g_theme.vue';
import G_screen from '../../components/common/g_screen.vue';
import G_menu from '../../components/admin/g_menu.vue'
import G_breadcrumbs from "../../components/admin/g_breadcrumbs.vue"
import G_user_deopdown from "../../components/common/g_user_dropdown.vue"
import {collapsed} from "../../components/admin/g_menu"
import router from '@/router';
function goHome(){
router.push({
name:"web"
})
}
</script>
<template>
<div class="g_admin" >
<!-- 给gasid 添加动态的collapsed类,在触发收缩时添加,方便给aside设置宽度 -->
<div class="g_aside" :class="{collapsed:collapsed}">
<div class="g_logo"></div>
<G_menu></G_menu>
</div>
<div class="g_main" :class="{collapsed:collapsed}">
<div class="g_head">
<G_breadcrumbs></G_breadcrumbs>
<div class="g_actions">
<span title="去首页" @click="goHome"> <icon-home></icon-home></span>
<G_theme></G_theme>
<G_screen></G_screen>
<G_user_deopdown></G_user_deopdown>
</div>
</div>
<div class="g_tabs">
</div>
<div class="g_container">
<RouterView></RouterView>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
// 不能使用overflow。否则,收缩展开按钮会被覆盖
width: 240px;
height: 100vh;
border-right: @g_border;
transition: width .3s;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
//g_main下添加了collapsed类,如果存在就将这边框设置为48px即为收起状态
&.collapsed{
width: 48px;
// &~只有下兄弟标签适用,将g_main的宽度进行调整
&~.g_main{
width: calc(100% - 48px);
}
}
}
.g_main {
width: calc(100% - 240px);
transition: width .3s;
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
.g_actions{
display: flex;
align-items: center;
}
svg{
font-size: 18px;
cursor: pointer;
margin-right: 10px;
}
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
}
}
}</style>
src\components\admin\g_menu.vue【调整菜单展开的逻辑,不使用默认的,而是监听路由变化】
javascript
<script setup lang='ts'>
import { Component } from 'vue';
import { IconHome, IconUser, IconSettings } from "@arco-design/web-vue/es/icon"
import G_iconComponent from '../common/g_iconComponent.vue';
import {collapsed} from "../../components/admin/g_menu"
import { ref,watch } from 'vue';
import router from '@/router';
import { useRoute } from 'vue-router';
const route = useRoute()
interface MenuType {
title: string
name: string
icon?: string | Component
children?: MenuType[]
}
const menuList: MenuType[] = [
{ title: "首页", name: "home", icon: IconHome },
{
title: "个人中心", name: "userCenter", icon: "iconfont icon-user-manage", children: [
{ title: "用户信息", name: "userinfo",icon:"iconfot icon-account-pin-circle-line" },
]
},
{
title: "用户管理", name: "userManage", icon: "iconfont icon-user-manage", children: [
{ title: "用户列表", name: "userlist", icon: "iconfont icon-yonghuliebiao" },
]
},
{
title: "系统设置", name: "systemCenter", icon: "iconfont icon-xitongguanli", children: [
{ title: "系统信息", name: "systeminfo", icon: "iconfont icon-xitongxinxi" },
]
},
]
// menuItemClick 菜单点击跳转函数
function menuItemClick(key:string) {
router.push({
name:key
})
}
const openkeys = ref<string[]>([])
const selectedKeys = ref<string[]>([])
// 路由初始化函数,判断路由的层级是否是三级,将中间的路由名称赋值给openkeys可以实现层级展开,只适用于三级的路层级,三级以上的路由层级需要遍历处理给openkeys,
function initRoute(){
if (route.matched.length ===3){
openkeys.value = [route.matched[1].name as string]
}
// 将路由的name添加到elected-keys中,默认展开路由
selectedKeys.value = [route.name as string]
}
//监听路由变化,如果路由变化就执行一遍initRoute
watch(()=>route.name,()=>{
initRoute()
},{immediate:true})
</script>
<template>
<div class="g_menu" :class="{collapsed:collapsed}">
<div class="g_menu_inner scrollbar">
<a-menu
@menu-item-click="menuItemClick"
v-model:collapsed="collapsed"
v-model:open-keys="openkeys"
v-model:selected-keys ="selectedKeys"
show-collapse-button >
<!-- 注意这里的key必须使用name字段,因为在处理route时是通过name字段来进行判断的 -->
<template v-for="menu in menuList">
<a-menu-item :key="menu.name" v-if="!menu.children">
<template #icon>
<G_iconComponent :is="menu.icon">
{{ menu.icon }}</G_iconComponent>
</template>
{{ menu.title }}
</a-menu-item>
<a-sub-menu :key="menu.name" v-else :title="menu.title">
<template #icon>
<G_iconComponent :is="menu.icon"></G_iconComponent>
</template>
<a-menu-item :key="sub.name" v-for="sub in menu.children">
{{ sub.title }}
<template #icon>
<G_iconComponent :is="sub.icon"></G_iconComponent>
</template>
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</div>
</div>
</template>
<style lang='less'>
.g_menu {
height: calc(100vh - 90px);
position: relative;
&.collapsed{
.arco-menu-collapse-button {
left: 48px !important;
}
}
&:hover {
.arco-menu-collapse-button {
opacity: 1 !important;
}
}
.g_menu_inner {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
.arco-menu {
position: inherit;
.arco-menu-collapse-button {
top: 50%;
transform: translate(-50%, -50%);
left: 240px;
transition: all .3s;
opacity: 0;
}
}
}
}
</style>
tabs组件
env.d.ts
javascript
/// <reference types="vite/client" />
// 给 RouteMeta 声明一个title类型,这样在声明对象时不会报警告,也不会飘红
import "vue-router"
declare module "ue-router"{
interface RouteMeta{
title:string
}
}
安装 swiper npm i swiper
src\components\admin\g_tabs.vue
javascript
<script setup lang='ts'>
import { ref } from 'vue';
import { IconClose } from '@arco-design/web-vue/es/icon';
import router from '@/router';
import { useRoute } from 'vue-router';
import { watch } from 'vue';
import { Swiper, SwiperSlide } from 'swiper/vue';
import { onMounted } from 'vue';
const route = useRoute()
interface TabType {
name: string
title: string
}
const tabs = ref<TabType[]>([
{ title: "首页", name: "home" },
])
function check(itme: TabType) {
router.push({
name: itme.name
})
saveTabs()
}
function removeItem(itme: TabType) {
if (itme.name === 'home') {
return
}
const index = tabs.value.findIndex((value) => itme.name === value.name)
if (index != -1) {
//判断我删除的这个元素,是不是我当前所在的
if (itme.name === route.name) {
router.push({
name: tabs.value[index - 1].name
})
}
tabs.value.splice(index, 1)
}
saveTabs()
}
function removeAllItem() {
tabs.value = [
{ title: "首页", name: "home" },
]
router.push({ name: "home" })
saveTabs()
}
// 持久化tabs
function saveTabs() {
localStorage.setItem("g_tabs", JSON.stringify(tabs.value))
}
// 初始化tabs
function loadTabs() {
const g_tabs = localStorage.getItem("g_tabs")
if (g_tabs) {
try {
tabs.value = JSON.parse(g_tabs)
} catch (e) {
console.log(e);
}
}
}
loadTabs()
watch(() => route.name, () => {
//判断当前路由的名称,在不在 tabs 里面,如果不在就加入进去
const index = tabs.value.findIndex((value) => route.name === value.name)
if (index === -1) {
tabs.value.push({
name: route.name as string,
title: route.meta.title as string,
})
}
}, { immediate: true })
const slidesCount = ref(100)
onMounted(() => {
// 先算总宽度
// 算实际宽度(scrollWidth)有没有超出总宽度
// 如果超出了总宽度,就遍历所有的 swiper-slide
// 从前往后加,如果超过了总宽度,那个时候的个数,就是实际显示的个数
// 显示总宽度
const swiperDom = document.querySelector(".g_tabs_swiper") as HTMLDivElement
const swiperWidth = swiperDom.clientWidth
// 显示实际的总宽度
const wrapperDom = document.querySelector(".g_tabs_swiper .swiper-wrapper") as HTMLDivElement
const wrapperWidth = wrapperDom.scrollWidth
if (swiperWidth > wrapperWidth) {
return
}
// 如果实际的总宽度大雨了显示的总宽度
// 遍历 swiper-slide然后从后往前加
const slideList = document.querySelectorAll(".g_tabs_swiper .swiper-slide")
let allWith = 0
let index = 1
for (const sliderListElement of slideList) {
// 加上margin的宽度 20
allWith += (sliderListElement.clientWidth + 20)
index++
if (allWith >= swiperWidth) {
break
}
}
slidesCount.value = index
// 用户刚进来这个页面时,选中高亮元素
const activateSlide = document.querySelector(".g_tabs_swiper .swiper-slide.activate") as HTMLDivElement
if (activateSlide.offsetLeft > swiperWidth){
const offsetLeft = swiperWidth - activateSlide.offsetLeft
setTimeout(()=>{
wrapperDom.style.transform = `translate3d(${offsetLeft}px,0px,0px)`
})
}
})
</script>
<template>
<div class="g_tabs">
<swiper :slides-per-view="slidesCount" class="g_tabs_swiper">
<swiper-slide v-for="item in tabs" :class="{ activate: route.name === item.name }">
<div class="item" @click="check(item)" @mousedown.middle.stop="removeItem(item)"
:class="{ activate: route.name === item.name }">{{ item.title }}
<span class="close" title="删除" @click.stop="removeItem(item)" v-if="item.name != 'home'">
<IconClose></IconClose>
</span>
</div>
</swiper-slide>
</swiper>
<div class="item" @click="removeAllItem">删除全部</div>
</div>
</template>
<style lang='less'>
.g_tabs {
display: flex;
align-items: center;
padding: 0 10px;
justify-content: space-between;
.swiper {
width: calc(100% - 100px);
display: flex;
overflow-y: hidden;
overflow-x: hidden;
.swiper-wrapper {
display: flex;
align-items: center;
}
.swiper-slide {
width: fit-content !important;
flex-shrink: 0;
}
}
.item {
padding: 1px 8px;
background-color: var(--color-bg-1);
border: @g_border;
margin-right: 10px;
cursor: pointer;
border-radius: 5px;
flex-shrink: 0;
&:hover {
background-color: var(--color-fill-1);
}
&.activate {
background-color: @primary-6;
color: white;
}
}
}</style>
然后在admin/idnex.vue中引入该组件
logo组件
src\components\admin\g_logo.vue
javascript
<script setup lang='ts'>
import { collapsed } from './g_menu';
</script>
<template>
<div class="g_logo" :class="{collapsed:collapsed}">
<img src="../../assets/logo.svg" alt="logo" class="logo">
<div class="slogan">
<div>通用后台</div>
<div>Genernaladmin</div>
</div>
</div>
</template>
<style lang='less'>
.g_logo{
display: flex;
justify-content: center;
align-items: center;
&.collapsed{
.slogan{
transform: scale(0);
transform-origin: left;
opacity: 0;
}
.logo{
transform: translateX(50px);
width: 40px;
height: 40px;
}
}
.logo{
width: 50px;
height: 50px;
}
.slogan{
margin-left: 10px;
div:nth-child(1){
font-size: 22px;
font-weight: 600;
}
div:nth-child(2){
font-size: 12px;
margin-top: 1px;
}
}
}
</style>
路由切换动画+container样式优化
src\views\admin\index.vue
javascript
<script setup lang='ts'>
import { RouterView } from 'vue-router';
import G_theme from '../../components/common/g_theme.vue';
import G_screen from '../../components/common/g_screen.vue';
import G_menu from '../../components/admin/g_menu.vue'
import G_breadcrumbs from "../../components/admin/g_breadcrumbs.vue"
import G_user_deopdown from "../../components/common/g_user_dropdown.vue"
import {collapsed} from "../../components/admin/g_menu"
import router from '@/router';
import G_tabs from '@/components/admin/g_tabs.vue';
import G_logo from '@/components/admin/g_logo.vue';
function goHome(){
router.push({
name:"web"
})
}
</script>
<template>
<div class="g_admin" >
<!-- 给gasid 添加动态的collapsed类,在触发收缩时添加,方便给aside设置宽度 -->
<div class="g_aside" :class="{collapsed:collapsed}">
<G_logo></G_logo>
<G_menu></G_menu>
</div>
<div class="g_main" :class="{collapsed:collapsed}">
<div class="g_head">
<G_breadcrumbs></G_breadcrumbs>
<div class="g_actions">
<span title="去首页" @click="goHome"> <icon-home></icon-home></span>
<G_theme></G_theme>
<G_screen></G_screen>
<G_user_deopdown></G_user_deopdown>
</div>
</div>
<G_tabs></G_tabs>
<!-- 路由切换动画配置 -->
<div class="g_container scrollbar">
<router-view v-slot="{Component}" class="g_base_view">
<transition name="fade" mode="out-in">
<component :is="Component"></component>
</transition>
</router-view>
</div>
</div>
</div>
</template>
<style lang='less'>
.g_admin {
display: flex;
background-color: var(--color-bg-1); //设置背景主题色
color: @color-text-1; //设置文本颜色
.g_aside {
// 不能使用overflow。否则,收缩展开按钮会被覆盖
width: 240px;
height: 100vh;
border-right: @g_border;
transition: width .3s;
.g_logo {
width: 100%;
height: 90px;
border-bottom: @g_border;
}
//g_main下添加了collapsed类,如果存在就将这边框设置为48px即为收起状态
&.collapsed{
width: 48px;
// &~只有下兄弟标签适用,将g_main的宽度进行调整
&~.g_main{
width: calc(100% - 48px);
}
}
}
.g_main {
width: calc(100% - 240px);
transition: width .3s;
.g_head {
width: 100%;
height: 60px;
border-bottom: @g_border;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
.g_actions{
display: flex;
align-items: center;
}
svg{
font-size: 18px;
cursor: pointer;
margin-right: 10px;
}
}
.g_tabs {
width: 100%;
height: 30px;
border-bottom: @g_border;
}
.g_container {
width: 100%;
height: calc(100vh - 90px);
overflow-y: auto;
overflow-x: hidden;
background-color: @color-fill-2; //背景色,颜色稍微和外面的背景色有少许的不一样
padding: 20px 0px 20px 20px;
.g_base_view{
background-color: var(--color-bg-1);
border-radius: 5px;
}
}
}
}
// 组件刚开始离开
.fade-leave-active {
}
// 组件离开结束
.fade-leave-to {
transform: translateX(30px);
opacity: 0;
}
// 组件刚开始进入
.fade-enter-active {
transform: translateX(-30px);
opacity: 0;
}
// 组件进入完成
.fade-enter-to {
transform: translateX(0);
opacity: 1;
}
// 正在进入和离开
.fade-leave-active, .fade-enter-active {
transition: all .3s ease-out;
}
</style>
路由进度条使用
1.安装
javascript
npm install --save nprogress
npm i -D @types/nprogress
2.在路由组件中使用
javascript
import NProgress from "nprogress";
router.beforeEach((to, from, next) => {
NProgress.start();//开启进度条
next()
})
//当路由进入后:关闭进度条
router.afterEach(() => {
// 在即将进入新的页面组件前,关闭掉进度条
NProgress.done()//完成进度条
})
在main.js中引入
javascript
import "nprogress/nprogress.css";
环境变量
1.首先修改package.json中的dev启动配置,可以通过不同的配置读取不同的.env文件中的内容
javascript
"scripts": {
"dev": "vite", //会去读取.env文件中带有前缀VITE的变量
"dev1":"vite --mode dev1", //会去读取.env.dev1文件中带有前缀VITE的变量
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
2.配置.env类似中的文件如.env.dev1
javascript
VITE_SERVERNAME='测试环境1'
VITE_SERVER_URL=http://127.0.0.1:8000
3.使用函数方式编辑配置文件vite.config.ts
javascript
import { EnvMeta } from './env.d'; //添加loadEnv的类型
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
import { loadEnv } from 'vite' //加载env函数
import { log } from 'node:console';
const envDir = './' //.env文件的地址
// 使用函数方式配置
export default defineConfig((config)=>{
const env:EnvMeta =loadEnv(config.mode,envDir) as EnvMeta
log(env.VITE_SERVER_URL) //声明EnvMeta类型后可以直接点出来VITE_SERVER_URL。但是不声明的话也可以,只不过不会自动弹出来。可以在env.d.ts中声明
return {
plugins: [
vue(),
vueDevTools(),],
// 配置less预处理
css:{
preprocessorOptions:{
less:{
modifyVars:{
// 'primary-6':"red" //修改arco-disgin的主题色,默认是蓝色
},
additionalData:'@import "@/assets/var.less";',
javascriptEnabled:true,
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
// 前端地址端口以及ip配置
server:{
host:"0.0.0.0",
port:8080
},
//配置env地址
envDir:envDir
}
})
env.d.ts
javascript
/// <reference types="vite/client" />
// 给 RouteMeta 声明一个title类型,这样在声明对象时不会报警告,也不会飘红
import "vue-router"
declare module "ue-router"{
interface RouteMeta{
title:string
}
}
// 声明类型,继承自Record
export interface EnvMeta extends Record<string,string>{
VITE_SERVER_URL:string
VITE_SERVER_NAME:string
}
跨域配置
javascript
// 前端地址端口以及ip配置
server:{
host:"0.0.0.0",
port:8080,
// 跨域配置,如果后端的路由是以/api开始的,识别到后转成同源的路径进行请求
proxy:{
"/api":{
target:env.VITE_SERVER_URL,
rewrite:(path:string)=>path.replace("/api","") //这个是重写路径,如果后端的接口不满足以api开头的,那么就进行替换为空,也能完成解决跨域
}
}
},
axios封装
src\api\index.ts
javascript
import axios from "axios";
import {Message} from "@arco-design/web-vue";
// import {userStorei} from "@/stores/user_store";
import type {Ref} from "vue";
// 基础的响应类型,一般和后端项目的响应类型一致
export interface baseResponse<T> {
code: number
msg: string
data: T
}
// 列表类型的响应类型
export interface listResponse<T> {
list: T[]
count: number
}
// URL的Query Parameters参数
export interface paramsType {
key?: string
limit?: number
page?: number
sort?: string
[key: string]: any
}
export const useAxios = axios.create({
timeout: 6000,
baseURL: "", // 在使用前端代理的情况下,这里必须留空,不然会跨域
})
// axios请求拦截
useAxios.interceptors.request.use((config) => {
// const userStore = userStorei() //先去这个全局的store中拿到这个用户的token
// config.headers.set("token", userStore.userInfo.token) //将token配置到请求头中
return config
})
// axios响应拦截
useAxios.interceptors.response.use((res) => {
// 响应拦截时先做一个判断,如果状态是200才是响应成功,将数据返回出去
if (res.status === 200) {
return res.data
}
return res
}, (res) => {
// 如果有错误,将错误展示出来
Message.error(res.message)
})
// 默认删除的接口,接受一个url,一个id_list列表,返回基础的响应数据(因为后端的删除接口的请求参数都是是id_list,所以可以放在前端作为一个默认的接口方便调用)
export function defaultDeleteApi(url: string, id_list: number[]): Promise<baseResponse<string>> {
return useAxios.delete(url, {data: {id_list}})
}
// 默认post的接口,接受一个url,一个data,返回基础的响应数据
export function defaultPostApi(url: string, data: any): Promise<baseResponse<string>> {
return useAxios.post(url, data)
}
export function defaultPutApi(url: string, data: any): Promise<baseResponse<string>> {
return useAxios.put(url, data)
}
export interface optionsType {
label: string
value: number | string
}
export type optionsFunc = (params?: paramsType) => Promise<baseResponse<optionsType[]>>
export function getOptions(ref: Ref<optionsType[]>, func: optionsFunc, params?: paramsType){
func(params).then((res)=>{
ref.value = res.data
})
}