vue3-ts-cms 
技术栈介绍:
- 出自于 :coderwhy 老师
 - 开发工具 :Visual Studio Code
 - 编程语言 :TypeScript 4.x + JavaScript
 - 构建工具 :Vite 3.x / Webpack5.x
 - 前端框架 :Vue 3.x + setup
 - 路由工具 :Vue Router 4.x
 - 状态管理 :Vuex 4.x / Pinia
 - UI 框架 :Element Plus
 - 可视化 :Echart5.x
 - 工具库 :@vueuse/core + dayjs + countup.js 等等
 - CSS 预编译 :Sass / Less
 - HTTP 工具 : Axios
 - Git Hook 工具 :husky
 - 代码规范 :EditorConfig + Prettier + ESLint
 - 提交规范 :Commitizen + Commitlint
 - 自动部署 :Centos + Jenkins + Nginx
 
目录结构 

.vscode:vscode 插件推荐node_modules:包public:公共文件src:源码.eslintrc.cjs:.gitignore:忽略文件.prettierrc.json:格式化配置auto-imports.d.ts:Element 按需引入自动生成components.d.ts:Element 按需引入自动生成env.d.ts:类型声明文件index.html:模板文件package-lock.json:包锁定版本package.json:包需要文件和版本README.md:文档tsconfig.app.json:ts 指定待编译文件和定义编译选项tsconfig.json:ts 文件配置引入(不推荐更改)tsconfig.node.json:ssr 在 node 环境下运行的配置vite.config.ts:给 vite 做配置
src 结构 

assets:静态文件components:组件global:全局工具hooks:功能router:路由service:网络请求store:状态管理utils:工具views:页面App.vue:模板main.ts:入口文件
配置 
- env.d.ts
 
ts
// declare 声明 vue 文件
declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent
  export default component
}- 安装插件
 

- 安装 scss
 
sh
npm install -D sasscss 重置 
sh
npm i normalize.cssts
// main.ts
import 'normalize.css'创建 reset.css 和 common.css
ts
// index.css
@import './reset.css';
@import './common.css';
// reset.css
// https://github.com/willworks
// common.cssrouter 配置 
sh
# 安装 router
npm i vue-routerts
// routet => index.ts
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    // 默认路径
    {
      path: '/',
      redirect: '/main',
    },
    // 错误路径
    {
      path: '/:pathMatch(.*)',
      component: () => import('@/views/NotFound/NotFound.vue'),
    },
  ],
})
export default routerts
createApp(App).use(router).mount('#app')Pinia 配置 
sh
# 安装
npm install piniats
// store => index.ts
import { createPinia } from 'pinia'
const pinia = createPinia()
export default piniats
// src => main.ts
createApp(App).use(router).use(pinia).mount('#app')Pinia 案例: 
- counter.ts
 
ts
import { defineStore } from 'pinia'
export const useCounterSotre = defineStore('counter', {
  state: () => ({
    counter: 100,
  }),
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    },
  },
  actions: {
    changeCounterAction(newCounter: number) {
      this.counter = newCounter
    },
  },
})- Main.vue
 
vue
<script lang="ts" setup>
  import { useCounterSotre } from '@/store/counter'
  const counterStote = useCounterSotre()
  const changeCounter = () => {
    counterStote.changeCounterAction(999)
  }
</script>
<template>
  <div class="main">
    <h2>main:{{ counterStote.counter }}--{{ counterStote.doubleCounter }}</h2>
    <button @click="changeCounter">修改counter</button>
  </div>
</template>
<style lang="scss" scoped></style>Axios 封装 
sh
# 安装
npm i axios- import.meta.env.MODE: {string} 应用运行的模式。
 - import.meta.env.PROD: {boolean} 应用是否运行在生产环境。
 - import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD 相反)。
 - import.meta.env.SSR: {boolean} 应用是否运行在 server 上。
 
ts
// config => index.ts
let BASE_URL = ''
// 生产环境 开发环境
if (import.meta.env.DEV) {
  BASE_URL = 'http://vue-shop-api-t.itheima.net/api/private/v1'
} else {
  BASE_URL = 'http://vue-shop-api-t.itheima.net/api/private/v1'
}
export const TIMEOUT = 10000
export { BASE_URL }ts
// request => index.ts
import axios from 'axios'
import type { AxiosInstance } from 'axios'
import type { SJRequestConfig } from './type'
// 拦截器: 蒙版 loading | token | 修改配置等
class SJRequest {
  instance: AxiosInstance
  // requset 实例 => Axios 实例
  constructor(config: SJRequestConfig) {
    this.instance = axios.create(config)
    // 每个 instance 实例都添加拦截器
    this.instance.interceptors.request.use(
      (config) => {
        // loading/token
        console.log('全局请求成功的拦截')
        return config
      },
      (err) => {
        console.log('全局请求失败的拦截')
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        console.log('全局响应成功的拦截')
        return res.data
      },
      (err) => {
        console.log('全局响应失败的拦截')
        return err
      }
    )
    // 针对特定的 SJRequest 实例添加拦截器
    this.instance.interceptors.request.use(
      config.interceptors?.requestSuccessFn,
      config.interceptors?.requestFailureFn
    )
    this.instance.interceptors.response.use(
      config.interceptors?.responseSuccessFn,
      config.interceptors?.responseFailureFn
    )
  }
  // 封装网络请求
  request(config: SJRequestConfig) {
    return this.instance.request(config)
  }
  get(config: SJRequestConfig) {
    return this.request({ ...config, method: 'GET' })
  }
  post(config: SJRequestConfig) {
    return this.request({ ...config, method: 'POST' })
  }
  delete(config: SJRequestConfig) {
    return this.request({ ...config, method: 'DELETE' })
  }
  patch(config: SJRequestConfig) {
    return this.request({ ...config, method: 'PATCH' })
  }
}
export default SJRequestts
// Request => type.ts
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
// 针对 AxiosRequestConfig 进行扩展
export interface SJInterceptors {
  requestSuccessFn?: (config: AxiosRequestConfig) => any
  requestFailureFn?: (err: any) => any
  responseSuccessFn?: (res: AxiosResponse) => AxiosResponse
  responseFailureFn?: (err: any) => any
}
export interface SJRequestConfig extends AxiosRequestConfig {
  interceptors?: SJInterceptors
}ts
// index.ts
import { BASE_URL, TIMEOUT } from './config'
import SJRequest from './request'
export const sjRequest = new SJRequest({
  baseURL: BASE_URL,
  timeout: TIMEOUT,
  interceptors: {
    requestSuccessFn: (config) => {
      console.log('精细请求成功的拦截')
      return config
    },
    requestFailureFn: (err) => {
      console.log('精细请求失败的拦截')
      return err
    },
    responseSuccessFn: (res) => {
      console.log('精细响应成功的拦截')
      return res
    },
    responseFailureFn: (err) => {
      console.log('精细响应失败的拦截')
      return err
    },
  },
})Element-Plus 集成 
sh
# 安装
npm install element-plus --save
# 按需引入插件安装
npm install -D unplugin-vue-components unplugin-auto-importts
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})tsconfig.app.json
json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": [
    "env.d.ts",
    "src/**/*",
    "src/**/*.vue",
    "auto-imports.d.ts",
    "components.d.ts"
  ],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "composite": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}icon 图标 
sh
# 安装
npm install @element-plus/icons-vuets
// global => register-icons.ts
// 如果您正在使用CDN引入,请删除下面一行。
import type { App } from 'vue'
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
function registerIcons(app: App<Element>) {
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }
}
export default registerIconsts
// main.ts
createApp(App).use(router).use(pinia).use(registerIcons).mount('#app')html
<el-icon><Iphone /></el-icon>postman 





login 
路由守卫 

ts
// 导航守卫(to:哪里来,from:哪里去)
router.beforeEach((to, from) => {
  const token = localCache.getCache(LOGIN_TOKEN)
  // 查看token是否存在:不存在就返回登录页面
  if (to.path === '/main' && !token) {
    return '/login'
  }
})cache 封装 
ts
enum CacheType {
  Local,
  Session,
}
class Cache {
  storage: Storage
  constructor(type: CacheType) {
    this.storage = type === CacheType.Local ? localStorage : sessionStorage
  }
  setCache(key: string, value: any) {
    if (value) {
      this.storage.setItem(key, JSON.stringify(value))
    }
  }
  getCache(key: string) {
    const value = this.storage.getItem(key)
    if (value) {
      return JSON.parse(value)
    }
  }
  removeCache(key: string) {
    this.storage.removeItem(key)
  }
  clear() {
    this.storage.clear()
  }
}
const localCache = new Cache(CacheType.Local)
const sessionCache = new Cache(CacheType.Session)
export { localCache, sessionCache }记住密码(布尔值的记录) 


ts
const isRememberPwd = ref<boolean>(
  localCache.getCache('isRememberPwd') ?? false
)
watch(isRememberPwd, (newValue) => {
  localCache.removeCache('isRememberPwd')
  localCache.setCache('isRememberPwd', newValue)
})账号登录 

ts
// login.vue
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormRules, ElForm } from 'element-plus'
import { useLoginStore } from '@/store/login/login'
import type { IAccount } from '@/types'
import { localCache } from '@/utils/cache'
const CACHE_NAME = 'name'
const CACHE_PASSWORD = 'password'
const account = reactive<IAccount>({
  name: localCache.getCache(CACHE_NAME) ?? '',
  password: localCache.getCache(CACHE_PASSWORD) ?? '',
})
const accountRules: FormRules = {
  name: [
    { required: true, message: '必须输入账号~', trigger: 'blur' },
    {
      pattern: /^[a-z0-9]{6,20}$/,
      message: '必须6~20位数字或字母组成~',
      trigger: 'blur',
    },
  ],
  password: [
    { required: true, message: '必须输入密码~', trigger: 'blur' },
    {
      pattern: /^[a-z0-9]{6,20}$/,
      message: '必须要6位以上的字母或数字',
      trigger: 'blur',
    },
  ],
}
const formRef = ref<InstanceType<typeof ElForm>>()
const loginStore = useLoginStore()
const loginAction = (isRememberPwd: boolean) => {
  formRef.value?.validate((valid) => {
    if (valid) {
      // 1.获取用户账号密码
      const name = account.name
      const password = account.password
      // 2.向服务器发送请求
      loginStore.loginAccountAction({ name, password }).then(() => {
        // 3.判断是否需要记住密码
        if (isRememberPwd) {
          localCache.setCache(CACHE_NAME, name)
          localCache.setCache(CACHE_PASSWORD, password)
        } else {
          localCache.removeCache(CACHE_NAME)
          localCache.removeCache(CACHE_PASSWORD)
        }
      })
    } else {
      ElMessage.error('验证失败')
    }
  })
}
defineExpose({
  loginAction,
})html
<div class="panel-account">
  <el-form
    :model="account"
    label-width="60px"
    size="large"
    :rules="accountRules"
    ref="formRef">
    <el-form-item
      label="账号"
      prop="name">
      <el-input v-model="account.name" />
    </el-form-item>
    <el-form-item
      label="密码"
      prop="password">
      <el-input
        show-password
        v-model="account.password" />
    </el-form-item>
  </el-form>
</div>main 
侧边栏 

html
<div class="menu">
  <el-menu
    default-active="39"
    text-color="#b7bdc3"
    active-text-color="#fff"
    background-color="#001529">
    <!-- 遍历菜单 -->
    <template
      v-for="item in userMenus"
      :key="item.id">
      <el-sub-menu :index="String(item.id)">
        <template #title>
          <!-- 字符串转成组件: "el-icon-setting" <el-icon><Monitor /></el-icon>-->
          <el-icon>
            <component :is="item.icon.split('el-icon-')[1]" />
          </el-icon>
          <template v-if="item.icon"></template>
          <span>{{ item.name }}</span>
        </template>
        <template
          v-for="subitem in item.children"
          :key="subitem.id">
          <el-menu-item :index="String(subitem.id)"
            >{{ subitem.name }}</el-menu-item
          >
        </template>
      </el-sub-menu>
    </template>
  </el-menu>
</div>动态路由 

sh
# 安装
npm install coderwhy -g
# 查看
coderwhy --version
# 添加页面和路由
coderwhy add3page_setup test -d src/views/main/test封装动态路由 
ts
// utils => map-menus.ts
import type { RouteRecordRaw } from 'vue-router'
function loadLocalRoutes() {
  // 1.获取菜单
  // const userMenusResult = await getUserMenusByRoleId(this.userInfo.role?.id)
  // this.userMenus = userMenusResult.data
  // 2.获取所以路由对象.放到数组中
  const localRoutes: RouteRecordRaw[] = []
  // 2.1 读取router/main所以ts文件
  const files: Record<string, any> = import.meta.glob('@/router/main/**/*.ts', {
    eager: true,
  })
  // 2.2 将所以的数据遍历得到数组,并放入数组
  for (const key in files) {
    const module = files[key]
    localRoutes.push(module.default)
  }
  return localRoutes
}
export function mapMenusToRoutes(userMenus: any[]) {
  const localRoutes = loadLocalRoutes()
  // 3.根据菜单动态匹配路由
  const routes: RouteRecordRaw[] = []
  // 第一层路由
  for (const menu of userMenus) {
    // 第二层路由
    for (const submenu of menu.children) {
      // 遍历localRoutes里面的数据对比后端里面的子路径 {path: '/main/analysis/dashboard', component: ƒ}
      const route = localRoutes.find((item) => item.path === submenu.url)
      // 放入到动态路由目录
      if (route) routes.push(route)
    }
  }
  return routes
}ts
// store => login/login.ts
// 动态添加路由
import { mapMenusToRoutes } from '@/utils/map-menus'
const routes = mapMenusToRoutes(this.userMenus)
routes.forEach((route) => router.addRoute('main', route))封装动态不封装 
ts
import { defineStore } from 'pinia'
import router from '@/router'
import {
  accountLoginRequest,
  getUserInfoById,
  getUserMenusByRoleId,
} from '@/service/login/login'
import type { IAccount } from '@/types'
import type { RouteRecordRaw } from 'vue-router'
import { localCache } from '@/utils/cache'
import { LOGIN_TOKEN, USER_INFO, USER_MENUS } from '@/global/constants'
type ILoginState = {
  token: string
  userMenus: any
  userInfo: any
}
export const useLoginStore = defineStore('login', {
  // 指定state类型
  state: (): ILoginState => ({
    token: '',
    userInfo: {},
    userMenus: [],
  }),
  actions: {
    async loginAccountAction(account: IAccount) {
      // 1.账号登录,获取 token
      const loginResult = await accountLoginRequest(account)
      const id: number = loginResult.data.id
      this.token = loginResult.data.token
      localCache.setCache(LOGIN_TOKEN, this.token)
      // 2.获取用户详细信息
      const userInfoResult = await getUserInfoById(id)
      this.userInfo = userInfoResult.data
      // 3.根据角色请求用户的权限
      const userMenusResult = await getUserMenusByRoleId(this.userInfo.role?.id)
      this.userMenus = userMenusResult.data
      // 2.进行本地缓存
      localCache.setCache(USER_INFO, this.userInfo)
      localCache.setCache(USER_MENUS, this.userMenus)
      // 重要: 动态添加路由
      // 1.获取菜单
      /*
      const userMenusResult = await getUserMenusByRoleId(this.userInfo.role?.id)
      this.userMenus = userMenusResult.data
      */
      // 2.获取所以路由对象.放到数组中
      const localRoutes: RouteRecordRaw[] = []
      // 2.1 读取router/main所以ts文件
      const files: Record<string, any> = import.meta.glob(
        '@/router/main/**/*.ts',
        { eager: true }
      )
      // 2.2 将所以的数据遍历得到数组,并放入数组
      for (const key in files) {
        const module = files[key]
        localRoutes.push(module.default)
      }
      // 3.根据菜单动态匹配路由
      // 第一层路由
      for (const menu of this.userMenus) {
        // 第二层路由
        for (const submenu of menu.children) {
          // 遍历locaRoutes里面的数据对比后端里面的子路径 {path: '/main/analysis/dashboard', component: ƒ}
          const route = localRoutes.find((item) => item.path === submenu.url)
          if (route) router.addRoute('main', route)
        }
      }
      // 5.页面跳转(main)
      router.push('/main')
    },
    loadLocalCacheAction() {
      // 1.用户进行刷新默认加载操作
      const token = localCache.getCache(LOGIN_TOKEN)
      const userInfo = localCache.getCache(USER_INFO)
      const userMenus = localCache.getCache(USER_MENUS)
      // 用户进行刷新: 判断用户是否登录以及是否包含userMenus菜单
      if (token && userInfo && userMenus) {
        this.token = token
        this.userInfo = userInfo
        this.userMenus = userMenus
        // 动态添加路由
        const routes = mapMenusToRoutes(this.userMenus)
        routes.forEach((route) => router.addRoute('main', route))
      }
    },
  },
})动态路由另一种实现方式 
ts
import { createRouter, createWebHashHistory } from 'vue-router'
import { localCache } from '@/utils/cache'
import { LOGIN_TOKEN } from '@/global/constants'
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    {
      path: '/',
      redirect: '/main',
    },
    {
      path: '/login',
      component: () => import('@/views/Login/login.vue'),
    },
    {
      path: '/main',
      name: 'main',
      component: () => import('@/views/Main/Main.vue'),
    },
    {
      path: '/:pathMatch(.*)',
      component: () => import('@/views/NotFound/NotFound.vue'),
    },
  ],
})
const localRoutes = [
  {
    path: '/main/analysis/overview',
    component: () => import('@/views/Main/Analysis/Overview/Overview.vue'),
  },
  {
    path: '/main/analysis/dashboard',
    component: () => import('@/views/Main/Analysis/Dashboard/Dashboard.vue'),
  },
]
// 动态添加路由
router.addRoute('main', localRoutes[0])
router.addRoute('main', localRoutes[1])
// 导航守卫
router.beforeEach((to, from) => {
  const token = localCache.getCache(LOGIN_TOKEN)
  if (to.path === '/main' && !token) {
    return '/login'
  }
})
export default router动态路由刷新 
ts
// store => login/login.ts
export const useLoginStore = defineStore('login', {
  // 指定state类型
  state: (): ILoginState => ({
    token: '',
    userInfo: {},
    userMenus: [],
  }),
  actions: {
    async loginAccountAction(account: IAccount) {
      // 1.账号登录,获取 token
      const loginResult = await accountLoginRequest(account)
      const id: number = loginResult.data.id
      this.token = loginResult.data.token
      localCache.setCache(LOGIN_TOKEN, this.token)
      // 2.获取用户详细信息
      const userInfoResult = await getUserInfoById(id)
      this.userInfo = userInfoResult.data
      // 3.根据角色请求用户的权限
      const userMenusResult = await getUserMenusByRoleId(this.userInfo.role?.id)
      this.userMenus = userMenusResult.data
      // 2.进行本地缓存
      localCache.setCache(USER_INFO, this.userInfo)
      localCache.setCache(USER_MENUS, this.userMenus)
      // 动态添加路由
      const routes = mapMenusToRoutes(this.userMenus)
      routes.forEach((route) => router.addRoute('main', route))
      // 5.页面跳转(main)
      router.push('/main')
    },
    loadLocalCacheAction() {
      // 1.用户进行刷新默认加载操作
      const token = localCache.getCache(LOGIN_TOKEN)
      const userInfo = localCache.getCache(USER_INFO)
      const userMenus = localCache.getCache(USER_MENUS)
      // 用户进行刷新: 判断用户是否登录以及是否包含userMenus菜单
      if (token && userInfo && userMenus) {
        this.token = token
        this.userInfo = userInfo
        this.userMenus = userMenus
        // 动态添加路由
        const routes = mapMenusToRoutes(this.userMenus)
        routes.forEach((route) => router.addRoute('main', route))
      }
    },
  },
})ts
// store => index.ts
import { createPinia } from 'pinia'
import type { App } from 'vue'
import { useLoginStore } from './login/login'
const pinia = createPinia()
function registerStore(app: App<Element>) {
  // 使用pinia
  app.use(pinia)
  // 加载本地数据
  const loginStore = useLoginStore()
  loginStore.loadLocalCacheAction()
}
export default registerStorets
// main.ts
import store from './store'
createApp(App).use(store).use(router).use(registerIcons).mount('#app')进入页面菜单匹配 

ts
// utils => map-menus.ts
export let firstMenu: any = null
export function mapMenusToRoutes(userMenus: any[]) {
  const localRoutes = loadLocalRoutes()
  // 3.根据菜单动态匹配路由
  const routes: RouteRecordRaw[] = []
  // 第一层路由
  for (const menu of userMenus) {
    // 第二层路由
    for (const submenu of menu.children) {
      // 遍历localRoutes里面的数据对比后端里面的子路径 {path: '/main/analysis/dashboard', component: ƒ}
      const route = localRoutes.find((item) => item.path === submenu.url)
      // 放入到动态路由目录
      if (route) routes.push(route)
      // 记录第一个被匹配到的菜单
      if (!firstMenu && route) firstMenu = submenu
      console.log(submenu)
    }
  }
  return routes
}ts
// router => index.ts
// 导航守卫
router.beforeEach((to, from) => {
  const token = localCache.getCache(LOGIN_TOKEN)
  if (to.path === '/main' && !token) {
    return '/login'
  }
  if (to.path === '/main') {
    return firstMenu?.url
  }
})ts
// utils => map-menus.ts
/**
 * 根据路径去匹配需要的菜单
 * @param path 需要匹配的路径
 * @param userMenus 所以菜单
 */
export function mapPathToMenu(path: string, userMenus: any[]) {
  for (const menu of userMenus) {
    for (const submenu of menu.children) {
      if (submenu.url === path) {
        return submenu
      }
    }
  }
}ts
// components => MainMenu.vue
const route = useRoute()
const pathMenu = mapPathToMenu(route.path, userMenus)
// 默认选择菜单
const defaultActive = ref(String(pathMenu.id))面包屑 

ts
// utils => map-menus.ts
/**
 * 面包屑
 * @param path 需要匹配的路径
 * @param userMenus 所有菜单
 */
interface IBreadcrumbs {
  name: string
  path: string
}
export function mapPathToBreadcrumbs(path: string, userMenus: any[]) {
  // 1.定义面包屑
  const breadcrumbs: IBreadcrumbs[] = []
  // 2.遍历数据获取面包屑层级
  for (const menu of userMenus) {
    for (const submenu of menu.children) {
      if (submenu.url === path) {
        breadcrumbs.push({ name: menu.name, path: menu.url })
        breadcrumbs.push({ name: submenu.name, path: submenu.url })
      }
    }
  }
  return breadcrumbs
}ts
// components => MainBreadcrumb.vue
import { useRoute } from 'vue-router'
import { useLoginStore } from '@/store/login/login'
import { mapPathToBreadcrumbs } from '@/utils/map-menus'
import { computed } from 'vue'
const route = useRoute()
const userMenus = useLoginStore().userMenus
const breadcrumbs = computed(() => {
  return mapPathToBreadcrumbs(route.path, userMenus)
})html
<!-- components => MainBreadcrumb.vue -->
<div class="MainBreadcrumb">
  <el-breadcrumb separator-icon="ArrowRight">
    <template
      v-for="item in breadcrumbs"
      :key="item.name">
      <el-breadcrumb-item :to="item.path"> {{ item.name }} </el-breadcrumb-item>
    </template>
  </el-breadcrumb>
</div>ts
// utils => map-menus.ts
// 从定向顶级菜单
function loadLocalRoutes() {
  // 1.获取菜单
  // const userMenusResult = await getUserMenusByRoleId(this.userInfo.role?.id)
  // this.userMenus = userMenusResult.data
  // 2.获取所有路由对象.放到数组中
  const localRoutes: RouteRecordRaw[] = []
  // 2.1 读取router/main所有ts文件
  const files: Record<string, any> = import.meta.glob('@/router/main/**/*.ts', {
    eager: true,
  })
  // 2.2 将所有的数据遍历得到数组,并放入数组
  for (const key in files) {
    const module = files[key]
    localRoutes.push(module.default)
  }
  return localRoutes
}
export let firstMenu: any = null
export function mapMenusToRoutes(userMenus: any[]) {
  const localRoutes = loadLocalRoutes()
  // 3.根据菜单动态匹配路由
  const routes: RouteRecordRaw[] = []
  // 第一层路由
  for (const menu of userMenus) {
    // 第二层路由
    for (const submenu of menu.children) {
      // 遍历localRoutes里面的数据对比后端里面的子路径 {path: '/main/analysis/dashboard', component: ƒ}
      const route = localRoutes.find((item) => item.path === submenu.url)
      // 放入到动态路由目录
      if (route) {
        // 1.给顶层菜单添加重定向
        if (!routes.find((item) => item.path === menu.url)) {
          routes.push({ path: menu.url, redirect: route.path })
        }
        // 2.将二级菜单对应路径
        routes.push(route)
      }
      // 记录第一个被匹配到的菜单
      if (!firstMenu && route) firstMenu = submenu
    }
  }
  return routes
}系统管理 

状态 

html
<el-table-column
  prop="enable"
  label="状态"
  width="100"
  align="center">
  <!-- 作用域插槽 -->
  <template #default="scope">
    <el-button
      size="small"
      :type="scope.row.enable ? 'success' : 'danger'"
      plain>
      {{ scope.row.enable ? '启用' : '禁用' }}
    </el-button>
  </template>
</el-table-column>时间格式化 

ts
// utils => formatTime.ts
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
export function formatUTC(
  utcString: string,
  format: string = 'YYYY/MM/DD HH:mm:ss'
) {
  const resultTime = dayjs.utc(utcString).utcOffset(8).format(format)
  return resultTime
}html
<el-table-column
  prop="createAt"
  label="创建时间"
  align="center">
  <template #default="scope"> {{ formatUTC(scope.row.createAt) }} </template>
</el-table-column>分页器 

封装网络请求 
ts
// service => main => system => system.ts
import { sjRequest } from '@/service'
export function postUserListData(queryInfo: any) {
  return sjRequest.post({
    url: '/users/list',
    data: queryInfo,
  })
}ts
import { defineStore } from 'pinia'
import { postUserListData } from '@/service/main/system/stytem'
import type { ISystem } from './type'
export const useSystemStore = defineStore('system', {
  // 为了完整类型推理,推荐使用箭头函数
  state: (): ISystem => {
    return {
      userList: [],
      userTotalCount: 0,
    }
  },
  actions: {
    async postUserListAction(queryInfo: any) {
      // 调用网络请求
      const userListResult = await postUserListData(queryInfo)
      const { list, totalCount } = userListResult.data
      this.userList = list
      this.userTotalCount = totalCount
    },
  },
})html
<!-- 页面 -->
<div class="Pagination">
  <el-pagination
    v-model:current-page="currentPage"
    v-model:page-size="pageSize"
    :page-sizes="[10, 20, 30, 40]"
    layout="total, sizes, prev, pager, next, jumper"
    :total="userTotalCount"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange" />
</div>ts
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSystemStore } from '@/store/main/system/system'
// 获取store
const systemStore = useSystemStore()
// 页数
const pageSize = ref(10)
// 页码
const currentPage = ref(1)
// 调用网络请求
const fetchUserListData = () => {
  const size = pageSize.value
  const offset = (currentPage.value - 1) * size
  const info = { size, offset }
  // 发送请求
  systemStore.postUserListAction(info)
}
fetchUserListData()
// 从store中解构列表和总列表数 响应式store数据
const { userList, userTotalCount } = storeToRefs(systemStore)
const handleSizeChange = () => {
  fetchUserListData()
}
const handleCurrentChange = () => {
  fetchUserListData()
}抽取重构 
页面 

html
<!-- 新建页面 -->
<div class="model">
  <el-dialog
    v-model="dialogVisible"
    :title="
        isNewRef ? modelConfig.header.newTitle : modelConfig.header.editTitle
      "
    width="30%"
    center>
    <div class="form">
      <el-form
        :model="formData"
        label-width="80px"
        size="large">
        <template
          v-for="item in modelConfig.formItems"
          :key="item.prop">
          <el-form-item v-bind="item">
            <template v-if="item.type === 'input'">
              <el-input
                v-model="formData[item.prop]"
                :placeholder="item.placeholder" />
            </template>
            <template v-if="item.type === 'select'">
              <el-select
                v-model="formData[item.prop]"
                :placeholder="item.placeholder"
                style="width: 100%">
                <template
                  v-for="option in item.options"
                  :key="option.value">
                  <el-option
                    :label="option.label"
                    :value="option.value" />
                </template>
              </el-select>
            </template>
          </el-form-item>
        </template>
      </el-form>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="dialogVisible = false"> 取消 </el-button>
        <el-button
          @click="newConfirmClick"
          type="primary">
          确定
        </el-button>
      </div>
    </template>
  </el-dialog>
</div>ts
// 新建逻辑
import { reactive } from 'vue'
import { ref } from 'vue'
import { useSystemStore } from '@/store/main/system/system'
interface IProps {
  modelConfig: {
    pageName: string
    header: {
      newTitle: string
      editTitle: string
    }
    formItems: any[]
  }
}
const props = defineProps<IProps>()
const dialogVisible = ref(false)
const isNewRef = ref(true)
const editData = ref()
const initialData: any = {}
for (const item of props.modelConfig.formItems) {
  initialData[item.prop] = ''
}
const formData = reactive<any>(initialData)
const setModelVisible = (isNew: boolean = true, itemData?: any) => {
  dialogVisible.value = true
  isNewRef.value = isNew
  if (!isNew && itemData) {
    // 编辑
    for (const key in formData) {
      formData[key] = itemData[key]
    }
    editData.value = itemData
  } else {
    // 新建
    for (const key in formData) {
      formData[key] = ''
    }
    editData.value = null
  }
}
const systemStore = useSystemStore()
const newConfirmClick = () => {
  dialogVisible.value = false
  if (!isNewRef.value && editData.value) {
    // 编辑
    systemStore.editPageDataAction(
      props.modelConfig.pageName,
      editData.value.id,
      formData
    )
  } else {
    // 新建
    systemStore.newPageListAction(props.modelConfig.pageName, formData)
    console.log(formData)
  }
}
defineExpose({ setModelVisible })ts
// 新建配置
const modelConfig = {
  pageName: 'department',
  header: {
    newTitle: '新建部门',
    editTitle: '编辑部门',
  },
  formItems: [
    {
      type: 'input',
      label: '部门名称',
      prop: 'name',
      placeholder: '请输入部门名称',
    },
    {
      type: 'select',
      label: '上级部门',
      prop: 'parentId',
      placeholder: '请输入上级部门',
      options: [],
    },
    {
      type: 'input',
      label: '部门领导',
      prop: 'leader',
      placeholder: '请输入部门领导',
    },
  ],
}
export default modelConfig父级页面 
html
<!-- 父级调用 -->
<div class="department">
  <page-search
    :search-config="searchConfig"
    @query-click="handleQueryClick"
    @reset-click="handleResetClick">
  </page-search>
  <page-content
    ref="contentRef"
    :content-config="contentConfig"
    @new-click="handleNewClick"
    @edit-click="handleEditClick">
  </page-content>
  <page-model
    :model-config="modelConfigRef"
    ref="modelRef"></page-model>
</div>ts
import { ref, computed } from 'vue'
import PageContent from '@/components/page-content/page-content.vue'
import PageModel from '@/components/page-model/page-model.vue'
import modelConfig from './config/model.config'
import { userMainStore } from '@/store/main/main'
const modelConfigRef = computed(() => {
  const mainStore = userMainStore()
  const department = mainStore.entireDepartments.map((item) => {
    return { label: item.name, value: item.id }
  })
  modelConfig.formItems.forEach((item: any) => {
    if (item.prop === 'parentId') {
      item.options?.push(...department)
    }
  })
  return modelConfig
})
const modelRef = ref<InstanceType<typeof PageModel>>()
const handleNewClick = () => {
  modelRef.value?.setModelVisible()
}
const handleEditClick = (itemData: any) => {
  modelRef.value?.setModelVisible(false, itemData)
}
ts
// 下拉框选择数据获取
const modelConfigRef = computed(() => {
  const mainStore = userMainStore()
  const department = mainStore.entireDepartments.map((item) => {
    return { label: item.name, value: item.id }
  })
  modelConfig.formItems.forEach((item: any) => {
    if (item.prop === 'parentId') {
      item.options?.push(...department)
    }
  })
  return modelConfig
})ts
// 功能
const newConfirmClick = () => {
  dialogVisible.value = false
  if (!isNewRef.value && editData.value) {
    // 编辑
    systemStore.editPageDataAction(
      props.modelConfig.pageName,
      editData.value.id,
      formData
    )
  } else {
    // 新建
    systemStore.newPageListAction(props.modelConfig.pageName, formData)
    console.log(formData)
  }
}store 封装 
ts
// type.ts
export type IUser = {
  id: number
  name: string
  realname: string
  cellphone: number
  enable: number
  departmentId: number
  roleId: number
  createAt: string
  updateAt: string
}
export interface ISystem {
  userList: IUser[]
  userTotalCount: number
  pageList: any[]
  pageTotalCount: number
}ts
// 状态管理
import { defineStore } from 'pinia'
import {
  postPageListData,
  deletePageById,
  editPageListData,
  newPageData,
} from '@/service/main/system/stytem'
import type { ISystem } from './type'
export const useSystemStore = defineStore('system', {
  // 为了完整类型推理,推荐使用箭头函数
  state: (): ISystem => {
    return {
      pageList: [],
      pageTotalCount: 0,
    }
  },
  actions: {
    async postPageListAction(pageName: string, queryInfo: any) {
      const pageListResult = await postPageListData(pageName, queryInfo)
      const { totalCount, list } = pageListResult.data
      this.pageList = list
      this.pageTotalCount = totalCount
    },
    async deletePageListByIdAction(pageName: string, id: number) {
      const deleteResult = await deletePageById(pageName, id)
      this.postPageListAction(pageName, { offset: 0, size: 10 })
    },
    async editPageDataAction(pageName: string, id: number, userInfo: any) {
      const editResult = await editPageListData(pageName, id, userInfo)
      this.postPageListAction(pageName, { offset: 0, size: 10 })
    },
    async newPageListAction(pageName: string, userInfo: any) {
      const newListResult = await newPageData(pageName, userInfo)
      this.postPageListAction(pageName, { offset: 0, size: 10 })
    },
  },
})网络请求封装 
ts
// 网络请求
import { sjRequest } from '@/service'
export function postPageListData(pageName: string, queryInfo: any) {
  return sjRequest.post({
    url: `/${pageName}/list`,
    data: queryInfo,
  })
}
export function deletePageById(pageName: string, id: number) {
  return sjRequest.delete({
    url: `/${pageName}/${id}`,
  })
}
export function newPageData(pageName: string, userInfo: any) {
  return sjRequest.post({
    url: `/${pageName}`,
    data: userInfo,
  })
}
export function editPageListData(pageName: string, id: number, userInfo: any) {
  return sjRequest.patch({
    url: `/${pageName}/${id}`,
    data: userInfo,
  })
}表格页面 

ts
// 配置
const contentConfig = {
  pageName: 'department',
  header: {
    title: '部门列表',
    btnTitle: '新建部门',
  },
  propsList: [
    {
      type: 'selection',
      label: '选择',
      width: '55px',
    },
    {
      type: 'normal',
      label: 'ID',
      prop: 'id',
      width: '70px',
    },
    {
      type: 'normal',
      label: '部门名称',
      prop: 'name',
      width: '150px',
    },
    {
      type: 'normal',
      label: '部门领导',
      prop: 'leader',
      width: '150px',
    },
    {
      type: 'normal',
      label: '上级部门',
      prop: 'parentId',
      width: '150px',
    },
    {
      type: 'timer',
      label: '创建时间',
      prop: 'createAt',
    },
    {
      type: 'timer',
      label: '更新时间',
      prop: 'updateAt',
    },
    {
      type: 'btnClick',
      label: '操作',
      width: '170px',
    },
  ],
}
export default contentConfigts
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useSystemStore } from '@/store/main/system/system'
import { formatUTC } from '@/utils/formatTime'
interface IProps {
  contentConfig: {
    pageName: string
    header?: {
      title?: string
      btnTitle?: string
    }
    propsList: any[]
  }
}
const props = defineProps<IProps>()
const emit = defineEmits(['newClick', 'editClick'])
const systemStore = useSystemStore()
const pageSize = ref(10)
const currentPage = ref(1)
// 获取表格
const fetchPageListData = (formatData: any = {}) => {
  const size = pageSize.value
  const offset = (currentPage.value - 1) * size
  const info = { size, offset }
  const queryInfo = { ...info, ...formatData }
  systemStore.postPageListAction(props.contentConfig.pageName, queryInfo)
}
fetchPageListData()
const { pageList, pageTotalCount } = storeToRefs(systemStore)
const handleSizeChange = () => {
  fetchPageListData()
}
const handleCurrentChange = () => {
  fetchPageListData()
}
// 新建
const newBtnClick = () => {
  emit('newClick')
}
// 删除
const deleteBtnClick = (id: number) => {
  systemStore.deletePageListByIdAction(props.contentConfig.pageName, id)
}
// 修改
const editBtnClick = (itemData: any) => {
  emit('editClick', itemData)
}
defineExpose({ fetchPageListData })
html
<div class="Pagination">
  <el-pagination
    v-model:current-page="currentPage"
    v-model:page-size="pageSize"
    :page-sizes="[10, 20, 30, 40]"
    layout="total, sizes, prev, pager, next, jumper"
    :total="pageTotalCount"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange" />
</div>hooks 
ts
// 抽取前
const modelRef = ref<InstanceType<typeof PageModel>>()
const handleNewClick = () => {
  modelRef.value?.setModelVisible()
}
const handleEditClick = (itemData: any) => {
  modelRef.value?.setModelVisible(false, itemData)
}ts
// hooks
import { ref } from 'vue'
import type PageModel from '@/components/page-model/page-model.vue'
function usePageModel() {
  const modelRef = ref<InstanceType<typeof PageModel>>()
  const handleNewClick = () => {
    modelRef.value?.setModelVisible()
  }
  const handleEditClick = (itemData: any) => {
    modelRef.value?.setModelVisible(false, itemData)
  }
  return {
    modelRef,
    handleNewClick,
    handleEditClick,
  }
}
export default usePageModelts
// 抽取后
const { modelRef, handleEditClick, handleNewClick } = usePageModel()角色权限回显 

ts
// role.vue
const { modelRef, handleEditClick, handleNewClick } = usePageModel(editCallback)
const treeRef = ref<InstanceType<typeof ElTree>>()
function editCallback(itemData: any) {
  nextTick(() => {
    const menuIds = mapMenuListToIds(itemData.menuList)
    treeRef.value?.setCheckedKeys(menuIds)
  })
}ts
// utils => map-menus.ts
/**
 * 菜单映射到id的列表
 * @param menuList
 */
export function mapMenuListToIds(menuList: any[]) {
  const ids: number[] = []
  function recuseGetId(menus: any[]) {
    for (const item of menus) {
      if (item.children) {
        recuseGetId(item.children)
      } else {
        ids.push(item.id)
      }
    }
  }
  recuseGetId(menuList)
  return ids
}ts
// hooks
import { ref } from 'vue'
import type PageModel from '@/components/page-model/page-model.vue'
type EditFnType = (data: any) => void
function usePageModel(editCallback?: EditFnType) {
  const modelRef = ref<InstanceType<typeof PageModel>>()
  const handleNewClick = () => {
    modelRef.value?.setModelVisible()
  }
  const handleEditClick = (itemData: any) => {
    modelRef.value?.setModelVisible(false, itemData)
    if (editCallback) editCallback(itemData)
  }
  return {
    modelRef,
    handleNewClick,
    handleEditClick,
  }
}
export default usePageModel