基于vue3+ts+scss的后台管理系统(一)

创建项目

vite创建项目

注意:vite需要Node.js版本>=12.0.0
安装方式(不使用模板):

#使用 NPM:  
npm init vite@latest
#yarn 【推荐】
yarn create vite
#pnpm
pnpm create vite

填写项目名->选择vue->选择TS->安装依赖(npm i)->启动项目(npm run dev)

清除项目&工程化项目搭建
  • src
    • api ajax工具管理文件夹
    • assets 静态资源目录
    • components 公共小组件
    • directives 自定义指令文件夹
    • hooks 全局hooks函数 文件夹
    • layout 框架
    • router 路由
    • store pinia仓库
    • utils 工具文件夹
    • views 页面级别组件文件夹
  • types 全局ts约束配置
重置样式&安装sass及配置

重置样式:拷贝一些样式文件到assets/styles下
安装sass: npm i sass -D
在main.ts文件中引入重置样式文件

import { createApp } from 'vue'
import App from './App.vue'

import './assets/styles/index.scss'
const app = createApp(App)

app.mount('#app')

使用重置样式中的变量,要先在vite.config.ts中配置

export default defineConfig({
  plugins: [vue()],
  css: {
    // CSS 预处理器
    preprocessorOptions: {
      scss: {
        additionalData: `@import '@/assets/style/variables.scss';`
      }
    }
  },
})

此时报错Can’t find stylesheet to import.,无法识别@ 继续配置别名

import { defineConfig, resolveBaseUrl } from 'vite'
import vue from '@vitejs/plugin-vue'

// 之所以飘红 是应为 #node不带ts提示 需要安装 @types/node 
import { resolve } from 'path'

//引入插件
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    createSvgIconsPlugin({
      iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
      symbolId: '[name]'
    })
  ],
  css: {
    // CSS 预处理器
    preprocessorOptions: {
      scss: {
        additionalData: `@import '@/assets/style/variables.scss';`
      }
    }
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

此时样式文件中的变量可以使用成功,但是飘红,需要继续安装npm i @types/node
css样式配置:body or html or root 里面配置全局样式变量

body{
	--primary: #007bff;
}

使用全局样式变量:

.app {
  height: 100%;
  #var 可以直接使用 全局样式变量
  background: var(--muted);
}

vue3 Svg的使用

  1. 打开iconfont 下载要使用的图标 (font class使用方式同以前一样)
  2. 使用Symbol方式:
    在main.ts中引入
// Symbol 引入iconfont.js
import './assets/icons/iconfont.js'

在.vue文件中使用

<template>
  <div>
<svg class="icon" aria-hidden="true">
  <use xlink:href="#icon-shuju"></use>
</svg>
  </div>
</template>

<script setup lang="ts">

</script>

<style scoped lang="scss">

.icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>
  1. 针对svg文件使用方法
    安装插件
npm i vite-plugin-svg-icons -D
//or
pnpm i vite-plugin-svg-icons -D


#如果报错Cannot find package 'fast-glob' 
安装插件 yarn add fast-glob -D

在vite.config.ts中引入插件

//引入插件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// 之所以飘红 是应为 #node不带ts提示 需要安装 @types/node 
import { resolve } from 'path'

//引入插件并在plugins中配置
import {createSvgIconsPlugin} from 'vite-plugin-svg-icons'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    createSvgIconsPlugin({
      iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
      symbolId: '[name]'
    })
  ],
  css: {
    // CSS 预处理器
    preprocessorOptions: {
      scss: {
        additionalData: `@import '@/assets/style/variables.scss';`
      }
    }
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  }
})

报错的话就再安装插件

#如果报错Cannot find package 'fast-glob' 
安装插件 yarn add fast-glob -D

在main.ts中注册svg图标

// 注册svg-icons
import 'virtual:svg-icons-register'

全局svg组件封装

创建组件/components/SvgIcon.vue
在SvgIcon.vue中

<template>
    <svg class="icon" aria-hidden="true" :width="props.size + 'em'" :height="props.size + 'em'">
        <use :xlink:href="'#' + props.name" :fill="props.color"></use>
    </svg>
</template>

<script setup lang="ts">
interface Props {
    name: string
    color?: string
    size?: number
}
const props = withDefaults(defineProps<Props>(), {
    color: '',
    size: 1,
})
</script>

<style scoped lang="scss">
.icon {
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
}
</style>

在main.ts中全局注册

const app = createApp(App)

// 引入SvgIcon组件
import SvgIcon from "@/components/SvgIcon.vue";
// 全局注册
app.component('SvgIcon', SvgIcon)

app.mount('#app')

使用

<svg-icon name="user" :size="3" color="pink"></svg-icon>

针对一下代码使用@飘红的问题

// 引入SvgIcon组件
import SvgIcon from "@/components/SvgIcon.vue";

在vite-env.d.ts中配置即可

declare module "*.vue" {
    import type { DefineComponent } from "vue"
    const vueComponent: DefineComponent<{}, {}, any>
    export default vueComponent
}

安装 & 使用element plus & 中文化

官网:Element Plus
安装:

# NPM
$ npm install element-plus --save

# Yarn
$ yarn add element-plus

完整引入在main.ts中

import { createApp } from 'vue'
import App from './App.vue'

import './assets/style/index.scss'

// Symbol 引入iconfont.js
import './assets/icons/iconfont.js'

// 注册svg-icons
import 'virtual:svg-icons-register'

const app = createApp(App)

// 引入SvgIcon组件
import SvgIcon from "@/components/SvgIcon.vue";
// 全局注册
app.component('SvgIcon', SvgIcon)
//引入ElementPlus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
app.mount('#app')

由于emlementPlus默认是英文,故还需配置成为中文
在main.ts中全局配置

// 全局注册
app.component('SvgIcon', SvgIcon)
//引入ElementPlus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//ElementPlus配置为中文
import zhCn from 'element-plus/es/locale/lang/zh-cn'
app.use(ElementPlus,{
    locale: zhCn,
    size: 'small',
  })
app.mount('#app')

此时即可正常使用

<template>
  <div>
<el-button type="success">Success</el-button>
<el-popconfirm title="Are you sure to delete this?">
    <template #reference>
      <el-button>Delete</el-button>
    </template>
  </el-popconfirm>
  </div>
</template>

配置路由vue-router

1. 建立页面级别组件

在views文件夹下建立页面

2. 配置路由地址和页面级别组件的一一对应关系

安装路由npm install vue-router
在router/index.ts中

import { createRouter,createWebHashHistory} from "vue-router"

//路由的配置数组,路径与页面的对映
const routes = []

//路由实例
const router = createRouter({
    routes,
    history:createWebHashHistory(),
})

//暴露
export default  router

在main.ts中使用路由

const app = createApp(App)
// 引入SvgIcon组件
import SvgIcon from "@/components/SvgIcon.vue";
// 全局注册
app.component('SvgIcon', SvgIcon)
//引入ElementPlus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//ElementPlus配置为中文
import zhCn from 'element-plus/es/locale/lang/zh-cn'

app.use(ElementPlus,{
    locale: zhCn,
    size: 'small',
  })

  //导入路由
  import router from './router';
  app.use(router)
  
app.mount('#app')

3. 给出口测试

在App.vue文件中配置一级路由出口

<!-- 一级路由出口 -->
 <router-view></router-view>

在layout.vue文件中配置二级路由出口

<!-- 二级路由出口 -->
        <router-view></router-view>

4.配置路由&路由自定义约束

在types文件下新建文件router.d.ts

//路由扩展types/router.d.ts

import type { RouteRecordRaw } from "vue-router";

interface metaItem {
    title?: string
    path?: string
}

interface RouteItem {
    hidden?: Boolean
    obj?: metaItem // 测试使用
    children?: RoutesType
}

type myRouteType = RouteRecordRaw & RouteItem

export type RoutesType = Array<myRouteType>

在router/index.ts中

import { createRouter,createWebHashHistory} from "vue-router";
// import type { RouteRecordRaw } from "vue-router";

import Layout from '@/views/layout/index.vue'

//路由的配置数组,路径与页面的对映
import { RoutesType } from "../../types/router";

const routes:RoutesType = [
    //登录
    {
        path:'/login',
        component:() => import('@/views/login/index.vue')
    },
    //数据中心
    {
        path:'/data',
        component: Layout,
        children: [
            {
                path: '',
                component: () => import('@/views/data/index.vue')
            }
        ]
    },
    //用户管理
    {
        path:'/account',
        component: Layout,
        redirect:'/account/list',
        children: [
            {
                path: '/account/list',
                component: () => import('@/views/account/account-list.vue')
            },
            {
                path: '/account/add',
                component: () => import('@/views/account/account-add.vue')
            },
            {
                path: '/account/personal',
                component: () => import('@/views/account/personal.vue')
            }
        ]
    },
    //文章管理
    {
        path:'/article',
        component: Layout,
        children: [
            {
                path: '',
                component: () => import('@/views/article/index.vue')
            }
        ]
    },
    //excel管理
    {
        path:'/excel',
        component: Layout,
        redirect:'/excel/import',
        children: [
            {
                path: '/excel/import',
                component: () => import('@/views/excel/excel-import.vue')
            },
            {
                path: '/excel/export',
                component: () => import('@/views/excel/excel-export.vue')
            }
        ]
    },
    //配置404
    {
        path: '/:pathMatch(.*)*',
        component: () => import('@/views/404/index.vue')
    }
]

//路由实例
const router = createRouter({
    routes,
    history:createWebHashHistory(),
})

//暴露
export default  router

主界面建框架

创建组件,实现大体框架布局

基于vue3+ts+scss的后台管理系统(一)_第1张图片

左边菜单栏

在leftMenu.vue中
引入menu组件

<template>
    <div class="left-menu">
 <el-menu
        default-active="/data"
        class="el-menu-vertical-demo"
        background-color="var(--el-color-danger-light-3)"
        text-color="var(--el-bg-color)"
        active-text-color="var(--el-color-danger-light-8)"
        unique-opened
        router
      >
      <el-menu-item index="/data">
          <el-icon>
              <svg-icon name="icon-shuju"/>
          </el-icon>
          <span>数据中心</span>
        </el-menu-item>
        <el-sub-menu index="1">
          <template #title>
            <el-icon><svg-icon name="icon-yonghu"/></el-icon>
            <span>用户管理</span>
          </template>
            <el-menu-item index="/account/list">用户列表</el-menu-item>
            <el-menu-item index="/account/add">添加用户</el-menu-item>
            <el-menu-item index="/account/personal">个人中心</el-menu-item>
        </el-sub-menu>
        <el-menu-item index="/article">
          <el-icon><svg-icon name="icon-bianjiwenzhang_huaban"/></el-icon>
          <span>文章管理</span>
        </el-menu-item>
        <el-sub-menu index="2">
          <template #title>
            <el-icon><svg-icon name="icon-file-excel"/></el-icon>
            <span>excel管理</span>
          </template>
            <el-menu-item index="/excel/import">excel导入</el-menu-item>
            <el-menu-item index="/excel/export">excel导出</el-menu-item>
        </el-sub-menu>     
      </el-menu>
    </div>
</template>

<script setup lang="ts">

</script>

<style scoped>
.left-menu{
    background-color:#f89898;
}
</style>

解决菜单栏右侧边框问题

.el-menu {
border-right: none !important;
}

默认选中菜单第一个default-active="/data"

点击菜单栏显示对应组件内容

添加router,配置index为对应路径
拿到路由信息对象

<script setup lang="ts">
import { useRoute } from 'vue-router';

const $route = useRoute()
console.log($route.path);
</script>

再修改默认激活路径为:default-active="$route.path"

主要显示内容设置动画效果transition

vue3中内置组件transition官网地址:transition
在mainContent.vue中

<template>
    <div class="p15">
		<!-- 二级路由出口 -->
        <router-view v-slot="{ Component }">
            <transition name="fade" mode="out-in">
            <component :is="Component"></component>
            </transition>
        </router-view>
    </div>
</template>
导航菜单栏折叠与收起

设置折叠收起按钮图标,安装:npm install @element-plus/icons-vue
引入:

<script setup lang="ts">
import { Expand } from "@element-plus/icons-vue"
</script>

使用:

<el-icon size="20" class="cursor-pointer"><Expand /></el-icon>

点击图标菜单栏折叠或收起,要修改collapse的值为true或false,将其存到pinia中
安装pinia:npm install pinia
在main.ts中

  //引入pinia并创建
  import { createPinia } from 'pinia'
  app.use(createPinia())

在store/index.ts中定义

//定义仓库
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useStore = defineStore('app',() =>  {
    //定义collapse动态数据
    let collapse = ref<boolean>(false)

    return {
        collapse,
    }
  })

在leftMenu.vue中获取响应式collapse并修改:collapse="collapse"

import { useStore } from '@/store';
import { computed } from 'vue';

//获取store的实例
const $appStore = useStore()
//获取响应式collapse
let collapse = computed(() => $appStore.collapse)
console.log(collapse);

在store/index.ts中

 //修改collapse
    const changeCollapse = () =>{
        collapse.value = !collapse.value
    }

    return {
        collapse,
        changeCollapse
    }

在mainTo.vue中实现点击图标切换及左侧菜单栏改变

<template>
    <div class="right-top df aic plr15">
        <el-icon @click="changeCollapse" size="20" class="cursor-pointer">
        <Expand v-show="collapse" />
        <Fold v-show="!collapse"/>
        </el-icon>
右侧顶部
    </div>
</template>

<script setup lang="ts">
import { Expand,Fold } from "@element-plus/icons-vue"
import { useStore } from "@/store"
import { computed } from 'vue';

//获取store对象
const $appStore = useStore()
const { changeCollapse } = $appStore
// console.log(changeCollapse);
const collapse = computed(() => $appStore.collapse)

</script>

<style scoped lang="scss">
.right-top{
    height:40px;
    background-color:#fcd3d3;
}
</style>

若@符号无法识别,飘红找不到模块则在tsconfig,json中配置

"noEmit": true,
    "baseUrl": "./",
    "paths": {
      "@/*": [
        "src/*"
      ],
      "@": [
        "src"
      ],
      "~/*": [
        "types/*"
      ]
    }
侧边栏hooks适配

在store/index.ts中

//设置collapse的值
    const setCollapse = (val : boolean) =>{
        collapse.value=val
    }
    return {
        collapse,
        changeCollapse,
        setCollapse
    }

在hooks文件夹下新建useResize.ts文件

import { useStore } from "@/store"
import { onMounted, onUnmounted } from "vue"

//在函数中监听屏幕的变化
export const useResize = () => {
    const minW = 700//屏幕固定值

    const $appStore = useStore()
    const { setCollapse } = $appStore

    const $_resize = () => {
        //获取屏幕的宽
        const w = document.body.clientWidth

        if (w > minW) {
            setCollapse(false)
        } else {
            setCollapse(true)
        }
    }

    onMounted(() => {
        $_resize()//初始化时要走一次故要调用一次
        window.addEventListener('resize', $_resize)
    })

    onUnmounted(() => {
        window.removeEventListener('resize', $_resize)
    })
}

在layout/index.ts中调用函数

//引入屏幕变化后 侧边栏状态变化的hooks函数
import {useResize} from "@/hooks/useResize"
useResize()
右边面包屑

静态–>使路由动态起来->配置路由下的meta,拿到title和path,凑成一个数组循环面包屑
router/index.ts

const routes: RoutesType = [
    //登录
    {
        path: '/login',
        component: () => import('@/views/login/index.vue')
    },
    //数据中心
    {
        path: '/data',
        component: Layout,
        meta: { title: '数据中心', path: '/data' },
        children: [
            {
                path: '',
                component: () => import('@/views/data/index.vue')
            }
        ]
    },
    //用户管理
    {
        path: '/account',
        component: Layout,
        meta: { title: '用户管理', path: '/account' },
        redirect: '/account/list',
        children: [
            {
                path: '/account/list',
                meta: { title: '用户列表', path: '/account/list' },
                component: () => import('@/views/account/account-list.vue')
            },
            {
                path: '/account/add',
                meta: { title: '用户添加', path: '/account/add' },
                component: () => import('@/views/account/account-add.vue')
            },
            {
                path: '/account/personal',
                meta: { title: '个人中心', path: '/account/personal' },
                component: () => import('@/views/account/personal.vue')
            }
        ]
    },
    //文章管理
    {
        path: '/article',
        component: Layout,
        meta: { title: '文章管理', path: '/article' },
        children: [
            {
                path: '',
                component: () => import('@/views/article/index.vue')
            }
        ]
    },
    //excel管理
    {
        path: '/excel',
        component: Layout,
        meta: { title: 'excel管理', path: '/excel' },
        redirect: '/excel/import',
        children: [
            {
                path: '/excel/import',
                meta: { title: 'excel导入', path: '/excel/import' },
                component: () => import('@/views/excel/excel-import.vue')
            },
            {
                path: '/excel/export',
                meta: { title: 'excel导出', path: '/excel/export' },
                component: () => import('@/views/excel/excel-export.vue')
            }
        ]
    },
    //配置404
    {
        path: '/:pathMatch(.*)*',
        component: () => import('@/views/404/index.vue')
    }
]

在mainTop.vue中,通过获取路由信息获取meta对象,判断matched是否为非空,非空则添加到循环数组中,点击菜单之后,面包屑也应该变化,故每点击一次,数据重新获取一次,将重新获取的数据添加到循环数组中,获取重新获取到的数据可以用:监听,计算属性,watchEffect

计算属性获取面包屑
import { useRoute } from 'vue-router'
import { metaItem } from '~/router'
//获取store对象
const $appStore = useStore()
const { changeCollapse } = $appStore
// console.log(changeCollapse);
const collapse = computed(() => $appStore.collapse)

// 通过获取路由信息获取meta对象,判断matched是否为非空,非空则添加到循环数组中
//点击菜单之后,面包屑也应该变化,故每点击一次,数据重新获取一次,将重新获取的数据添加到循环数组中
//获取重新获取到的数据可以用:监听,计算属性,watchEffect
// 获取meta
const $route = useRoute()
console.log(111,$route.matched);
//计算属性设置面包屑
const breadList = computed(() => {
    let arr:Array<metaItem> =[]
    //循环当前路由下的meta
    $route.matched.forEach(item => {
        //判断当前meta是否有title或path
        if(item.meta?.title){
            arr.push(item.meta)
        }
    })
    return arr
})
侦听器设置面包屑
import { computed,watch,ref} from 'vue';
//侦听器设置面包屑-监听某一个属性(),回调,立即执行
let breadList2 = ref<Array<metaItem>>([])
watch(
    $route,
    ()=>{
        breadList2.value=[]
        $route.matched.forEach(item => {
        //判断当前meta是否有title或path
        if(item.meta?.title){
            breadList2.value.push(item.meta)
        }
    })
    },
    {
        immediate:true //立即执行
    }
)
watchEffect设置面包屑
import { computed,watch,ref,watchEffect} from 'vue';
//watcheffect设置面包屑  副作用监听 会初始化执行里面代码 里面有响应式数据变化都会执行代码
let breacList3 = ref<Array<metaItem>>([])
const stop = watchEffect((deCleanr) => {
    breacList3.value = []
    $route.matched.forEach(item => {
        if (item.meta?.title) {
            breacList3.value.push(item.meta)
        }
    })
    deCleanr(() => {
        // 清除副作用
    })
})
// 调用stop函数后,watchEffect将停止监听
stop()
右侧下拉菜单的实现,点击退出登录

基于vue3+ts+scss的后台管理系统(一)_第2张图片

<el-dropdown @command="handleCommand">
        <span class="el-dropdown-link">
          欢迎您,xxx
          <el-icon class="el-icon--right">
            <arrow-down />
          </el-icon>
        </span>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item command="/login">退出登录</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
import { useRoute, useRouter } from "vue-router";
//下拉菜单的事件
const $router = useRouter();
const handleCommand = (command: string) => {
  $router.push(command);
};

登录页面

静态
基于vue3+ts+scss的后台管理系统(一)_第3张图片
登录验证实现

<template>
  <div class="login df aic jcc">
    <div class="mr-1">
      <h2 class="font-weight">我是文字我是文字</h2>
      <h1 class="font-weight mt-2 border-bottom-c pb-1">我是文字</h1>
      <h2 class="font-weight mt-2">我是文字我是文字</h2>
    </div>
    <div class="right rounded p15">
      <h2 class="font-weight mb-3">欢迎登录</h2>
      <el-form :model="loginFormData" :rules="rules" status-icon ref="loginFormRef">
        <el-form-item prop="account">
          <el-input v-model="loginFormData.account">
            <template #prefix>
              <el-icon class="el-input__icon">
                <svg-icon name="icon-yonghu1"></svg-icon>
              </el-icon>
            </template>
          </el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input v-model="loginFormData.password" type="password">
            <template #prefix>
              <el-icon class="el-input__icon">
                <svg-icon name="icon-tianchongxing-1"></svg-icon>
              </el-icon> </template
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" class="login-btn" @click="handleLogin(loginFormRef)">登录</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref,reactive } from "vue";
import type { FormInstance, FormRules } from 'element-plus';
import { useRouter } from "vue-router"
//登录数据约束
interface loginForm {
  account: string;
  password: string;
}
//登录数据
const loginFormData = ref<loginForm>({
  account: "",
  password: "",
});
const loginFormRef = ref<FormInstance>()
const $router = useRouter()
//验证规则
const rules = reactive<FormRules>({
  account: [
    { required: true, message: "请输入账号", trigger: "blur" },
    { min: 3, max: 6, message: "账号需为3-6位", trigger: "blur" },
  ],
  password: [
    { required: true, message: "请输入密码", trigger: "blur" },
    { min: 6, max: 12, message: "密码需为6-12位", trigger: "blur" },
  ],
});


//登录函数
const handleLogin = (formEl: FormInstance | undefined) => {
    if(!formEl) return
    formEl.validate((valid)=>{
        $router.push('/data')
    })
}
</script>

<style scoped lang="scss">
.login {
  height: 100%;
  background-color: #e5e5e5;
  .right {
    width: 240px;
    height: 260px;
    background-color: white;
    .login-btn {
      width: 100%;
    }
  }
}
</style>

你可能感兴趣的:(ts,vue,scss,vue.js,javascript)