Vue 3 脚手架搭建

news2024/11/22 9:43:16

Vue 3 脚手架搭建

    • 一、环境准备
      • 1.1 node.js 安装
      • 1.2 包管理工具安装:可选
    • 二、创建项目
      • 2.1 使用 npm
      • 2.2 使用 yarn
    • 三、配置项目
      • 3.1 安装初始依赖
      • 3.2 增加依赖
      • 3.2 配置自动导包
      • 3.3 配置 eslint 规则
      • 3.4 .vue 文件导入报错解决
    • 四、路由配置
      • 4.1 路由类型定义
      • 4.2 index.ts 路由主入口
      • 4.3 system.ts 自定义路由
    • 五、常用类型定义
      • 5.1 response.d.ts
      • 5.2 user.d.ts 自定义返回类型
    • 六、二次封装 axios
      • 6.1 httpClient.ts
      • 6.2 httpClient 实例
    • 七、本地响应式存储
      • 7.1 定义
      • 7.2 调用
    • 八、全局样式
      • 8.1 样式重置
        • 8.1.1 index.scss
        • 8.1.2 reset.scss
        • 8.1.3 样式引入
      • 8.2 样式常量
        • 8.2.1 variables.scss
        • 8.2.2 常量使用
    • 九、layout
      • 9.1 面包屑
      • 9.2 导航栏
      • 9.3 侧边栏
      • 9.4 页脚
      • 9.5 主体结构
    • 十、错误页面
      • 10.1 404页面
      • 10.2 401页面

技术栈

  • vite
  • vue3
  • typescript
  • vue-router4
  • pinia
  • axios
  • eslint
  • prettier

项目结构:

  • src : 项目代码

    • api :封装的网络请求

    • assets:静态文件

    • components:组件

    • layout:主体结构

    • model: 类型定义,严格遵守 ts 准则

    • router: 路由管理

    • stores: 全局变量管理

    • utils: 工具包

      -httpClient.ts : axios 二次封装

    • views:页面

  • types

    -route.d.ts : 定义路由类型

-.eslintrc.cjs :定义 eslint 规则

-.eslintrc-auto-import.json :unplugin-auto-import 插件自动生成的 eslint 类型规范

-.prettierrc.json :prettier 规则

-auto-imports.d.ts : unplugin-auto-import 插件自动生成,定义自动导入的方法

-components.d.ts :unplugin-vue-components 自动生成,定义自动导入的组件

-env.d.ts : 全局环境变量控制

-package.json : 包管理、依赖管理

-tsconfig.app.json :app 相关配置

-tsconfig.json :项目配置入口

-tsconfig.node.json : node 相关配置

-vite.config.ts : 项目内配置

一、环境准备

1.1 node.js 安装

node.js 官方下载地址

1.2 包管理工具安装:可选

  • npm:

    node.js 中自带,速度较慢

  • yarn:

# cmd 全局安装 yarn
npm install -g yarn 

二、创建项目

选择要创建项目的目录,打开cmd 执行以下命令

2.1 使用 npm

npm init vite@latest 
# npm 6.x
npm init vite@latest 项目名称 --template vue
# npm 7+, 需要额外的双横线:
npm init vite@latest 项目名称 -- --template vue

2.2 使用 yarn

# 方式一
yarn create vite
# 方式二
yarn create vite 项目名称 --template vue
# 方式三:该方式可直接配置需要的组件,可减少依赖版本冲突
npm init vue
# npm init vue 提示如下:Project name:<your-project-name>Add TypeScript?No / YesAdd JSX Support?No / YesAdd Vue Router for Single Page Application development?No / YesAdd Pinia for state management?No / YesAdd Vitest for Unit testing?No / YesAdd Cypress for both Unit and End-to-End testing?No / YesAdd ESLint for code quality?No / YesAdd Prettier for code formatting?No / Yes

Scaffolding project in ./<your-project-name>...
Done.

三、配置项目

3.1 安装初始依赖

# npm
npm install
# yarn
yarn init

3.2 增加依赖

yarn: yarn add 依赖名称

npm: npm install 依赖名称

# 异步请求包
yarn add axios
# scss 解析器,也可使用 less 等
yarn add sass -d
# 自动导包插件
yarn add unplugin-auto-import -d
yarn add unplugin-icons -d
yarn add unplugin-vue-components -d

package.json

{
"name": "crowd-funding-fronted",
"version": "0.0.0",
"private": true,
"scripts": {
 "dev": "vite",
 "build": "run-p type-check build-only",
 "preview": "vite preview",
 "build-only": "vite build",
 "type-check": "vue-tsc --noEmit",
 "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
 "format": "prettier --write src/"
},
"dependencies": {
 "axios": "^1.4.0",
 "element-plus": "^2.3.4",
 "pinia": "^2.0.35",
 "vue": "^3.2.47",
 "vue-router": "^4.1.6"
},
"devDependencies": {
 "@rushstack/eslint-patch": "^1.2.0",
 "@tsconfig/node18": "^2.0.0",
 "@types/jsdom": "^21.1.1",
 "@types/node": "^18.16.3",
 "@vitejs/plugin-vue": "^4.2.1",
 "@vue/eslint-config-prettier": "^7.1.0",
 "@vue/eslint-config-typescript": "^11.0.3",
 "@vue/tsconfig": "^0.3.2",
 "eslint": "^8.39.0",
 "eslint-plugin-vue": "^9.11.0",
 "jsdom": "^22.0.0",
 "npm-run-all": "^4.1.5",
 "prettier": "^2.8.8",
 "sass": "^1.62.1",
 "typescript": "~5.0.4",
 "unplugin-auto-import": "^0.15.3",
 "unplugin-icons": "^0.16.1",
 "unplugin-vue-components": "^0.24.1",
 "vite": "^4.3.4",
 "vue-tsc": "^1.6.4"
}
}

3.2 配置自动导包

// vite.config.ts
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import Icons from 'unplugin-icons/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import path from 'path'

export default defineConfig({
  base: '/',
  server: {
    open: true,
    // 修改端口号
    port: 8080, 
    proxy: {
        // 后端微服务时可在此处配置代理,解决测试环境跨域问题
    },
  },
  plugins: [
    vue(),
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      resolvers: [ElementPlusResolver()],
      eslintrc: {
        enabled: true, // Default `false`
      },
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      // 默认组件加载
      dirs: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.d.ts'],
      // 全局导入组件
      globs: [ 
        'src/layout/**/*.vue',
        'src/layout/**/*.ts',
        'src/layout/**/*.d.ts',
      ],
      extensions: ['vue', 'ts'],
    }),
    Icons({
      /* options */
    }),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

配置后会在根目录下自动生成以下文件:

  • components.d.ts

    组件库自动导入、自定义组件自动导入

  • auto-imports.d.ts

    vue 相关方法自动注入

  • .prettierrc

    prettier 规则

  • .eslintrc.cjs

    eslint 规则

  • .eslintrc-auto-import.json

    auto-imports 自动插件生成的 eslint 规则,需要在 eslint 中引入

3.3 配置 eslint 规则

// .eslintrc.cjs
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  'extends': [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier/skip-formatting',
    // 引入 auto-imports 自动生成的规则
    './.eslintrc-auto-import.json'  
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
    'prettier/prettier': [
      'error',
      {
        arrowParens: 'always',
        bracketSameLine: true,
        bracketSpacing: true,
        embeddedLanguageFormatting: 'auto',
        htmlWhitespaceSensitivity: 'ignore',
        insertPragma: false,
        jsxSingleQuote: true,
        printWidth: 80,
        proseWrap: 'preserve',
        quoteProps: 'as-needed',
        requirePragma: false,
        semi: false,
        singleAttributePerLine: false,
        singleQuote: true,
        tabWidth: 2,
        trailingComma: 'es5',
        useTabs: false,
        vueIndentScriptAndStyle: false,
      },
    ],
      // 设置文件命名可以为单个单词 
      'vue/multi-word-component-names': 0
  },
}

3.4 .vue 文件导入报错解决

// env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_APP_BASE_API: string
  readonly VITE_APP_DEV_USER: string
  readonly VITE_APP_DEV_PWD: string
  // more env variables...
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

declare module '*.vue' // .vue 文件引入报错解决

四、路由配置

该 layout 仅供参考,可按需求自行替换

src/router:

  • index.ts

    路由主要加载文件,可引入自定义路由文件

  • system.ts

    根据用户需求自定义路由文件,定义完成后在 index 中引入

4.1 路由类型定义

// types/route.d.ts
// This can be directly added to any of your `.ts` files like `router.ts`
// It can also be added to a `.d.ts` file, in which case you will need to add an export
// to ensure it is treated as a module
export {}

import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    // 标题
    title: string
    // 图标
    icon?: string
    // 访问权限
    perms?: string[]
    // 是否在菜单中,默认在,设置为true则不在
    hidden?: boolean
    // 是否忽略子菜单全都被过滤掉导致的不显示,设置为true时忽略
    alwaysShow?: boolean
    // 是否在角色管理中不显示
    roleManageIgnore?: boolean
  }
}

4.2 index.ts 路由主入口

// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import system from '@/router/system' // 自定义路由文件

// 静态路由
export const commonRoutes: RouteRecordRaw[] = [
  {
    path: '/layout',
    name: 'Layout',
    component: () => import('@/layout/Home.vue'),
    meta: {
      title: '首页',
      hidden: true,
    },
    children: [
      {
        path: '/dashboard',
        name: 'dashboard',
        component: () => import('@/views/Dashboard.vue'),
        meta: {
          title: '首页',
          hidden: true,
        },
      },
    ],
  },
  {
    path: '/',
    redirect: '/login',
    meta: {
      title: '登录重定向',
      hidden: true,
    },
  },
  {
    path: '/login',
    name: 'login',
    component: () => import('@/components/login/Login.vue'),
    meta: {
      title: '登录',
      hidden: true,
    },
  },
  {
    path: '/404',
    name: '404',
    component: () => import('@/components/error/404.vue'),
    meta: {
      title: '404',
      hidden: true,
    },
  },
  {
    path: '/401',
    name: '401',
    component: () => import('@/components/error/401.vue'),
    meta: {
      title: '401',
      hidden: true,
    },
  },
]

// 将静态路由 和 自定义路由进行整合
export const allRoutes: RouteRecordRaw[] = [...commonRoutes, system]

const router = createRouter({
  history: createWebHistory(),
  routes: allRoutes,
})

export default router

4.3 system.ts 自定义路由

// src/router/system.ts
import type { RouteRecordRaw } from 'vue-router'
import Home from '@/layout/Home.vue'

// 该路由文件的最顶层路由
export const systemRoute: RouteRecordRaw = {
  path: '/system',
  name: 'system',
  meta: {
    title: '系统管理',
  },
  component: Home,
}

export const userRoute: RouteRecordRaw = {
  path: '/user',
  name: 'user',
  meta: {
    title: '用户管理',
  },
  component: () => import('@/views/system/user/user.vue'),
}
export const roleRoute: RouteRecordRaw = {
  path: '/role',
  name: 'role',
  meta: {
    title: '角色管理',
  },
  component: () => import('@/views/system/role/role.vue'),
}
export const logRoute: RouteRecordRaw = {
  path: '/log',
  name: 'log',
  meta: {
    title: '日志管理',
  },
  component: () => import('@/views/system/log/log.vue'),
}

export default <RouteRecordRaw>{
  ...systemRoute,
  children: [userRoute, roleRoute, logRoute],
}

五、常用类型定义

5.1 response.d.ts

// src/model/response.d.ts
// 父接口, 不返回对象
export interface BaseResponse {
  success: boolean
  code: number
  message?: string
}

// 用于返回一个对象
export interface ItemResponse<T> extends BaseResponse {
  item?: T
}

// 用于返回一个List
export interface ListResponse<T> extends BaseResponse {
  items: T[]
}

// 用于返回含分页的 list
export interface PageResponse<T> extends BaseResponse {
  pageNum: number
  pageSize: number
  total: number
  totalPages?: number
  items?: T[]
}

5.2 user.d.ts 自定义返回类型

// 用户信息,用于存放在全局变量
export interface UserInfo {
  username: string
  name: string
  phone?: string
  email?: string
}
// 登录的用户
export interface LoginUser {
  username: string
  password: string
}

// 注册用户
export interface RegisterUser {
  username: string
  password: string
  name: string
  sex: string
  email: string
  phone: string
  checkPassword: string
}

六、二次封装 axios

6.1 httpClient.ts

// src/utils/httpClient.ts
import axios, { type AxiosRequestConfig } from 'axios'
import type { BaseResponse } from '@/model/response'

const httpClient = axios.create({
  // 设置默认请求地址
  baseURL: import.meta.env.VITE_APP_BASE_API,
  // 设置超时时间
  timeout: import.meta.env.DEV ? 10000 : 10000,
  // 跨域时允许携带凭证
  withCredentials: true,
})

// 添加请求拦截器
httpClient.interceptors.request.use(
  (config) => {
    // 在发送请求之前做些什么
    return config
  },
  (error) => {
    // 对请求错误做些什么
    return Promise.reject(error)
  }
)

// 添加响应拦截器
httpClient.interceptors.response.use(
  (response) => {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response
  },
  (error) => {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error)
  }
)

export function post(
  url: string,
  data?: any,
  config?: AxiosRequestConfig<any>
) {
  return httpClient.post<never, BaseResponse>(url, data, config)
}

export function put(url: string, data?: any, config?: AxiosRequestConfig<any>) {
  return httpClient.put<never, BaseResponse>(url, data, config)
}

export function get(url: string, config?: AxiosRequestConfig<any>) {
  return httpClient.get<never, BaseResponse>(url, config)
}

export function del(url: string, config?: AxiosRequestConfig<any>) {
  return httpClient.delete<never, BaseResponse>(url, config)
}

export default httpClient

6.2 httpClient 实例

// src/api/user.ts
import httpClient from '@/utils/httpClient'
// 以下类型在 src/model 中定义
import type { BaseResponse, ItemResponse } from '@/model/response'
import type { LoginUser, RegisterUser, UserInfo } from '@/model/user'

/**
 * 登录
 * @param user 用户
 * @returns {BaseResponse} 登录结果
 */
export const login: (user: LoginUser) => Promise<ItemResponse<UserInfo>> = (
  user
) => {
  return httpClient.post('/api/gateway/login', null, {
    params: {
      username: user.username,
      password: user.password,
    },
  })
}

/**
 * @param user
 */
export const logout: () => Promise<BaseResponse> = () => {
  return httpClient.get('/api/gateway/logout')
}

/**
 * 注册
 * @param user 用户
 * @returns {Promise<BaseResponse>} 注册结果
 */
export const register: (
  user: RegisterUser
) => Promise<ItemResponse<RegisterUser>> = (user) => {
  return httpClient.post('/api/gateway/user-service/user/registerUser', user)
}

七、本地响应式存储

7.1 定义

  • user.ts
// src/stores/user.ts
// 用户相关
import type { LoginUser, UserInfo } from '@/model/user'
import { login } from '@/api/user' // api 中封装的登录请求

export const userStore = defineStore('user', () => {
   // 用户信息
  const userInfo = ref<UserInfo | null>(null)
  // 登录后获取用户信息
  async function loginUser(user: LoginUser) {
    try {
      const res = await login(user)
      const { success, code, item } = res
      if (success && code == 200) {
        userInfo.value = item!
      }
      return Promise.resolve(res)
    } catch (error: any) {
      return Promise.reject(error)
    }
  }
  // 对外暴漏的属性及方法
  return { userInfo, loginUser }
})
  • permission.ts
// src/stores/permission.ts
// 权限相关
import { allRoutes } from '@/router'
import type { RouteRecordRaw } from 'vue-router'

export const permissionStore = defineStore('permission', () => {
  const perms = ref<string[]>([])

  // 这个用于后面 layout 中根据用户权限生成菜单
  const authorizedRoutes = computed(() => {
    // 根据权限过滤路由
    const filterRoutes = filterAsyncRoutes(perms.value, allRoutes)
    return filterRoutes
  })

  function setPermission(newPermission: string[]) {
    perms.value = newPermission
  }

  function $reset() {
    perms.value = []
  }

  return { perms, $reset, setPermission, authorizedRoutes }
})

export function filterAsyncRoutes(
  perms: string[],
  routes: RouteRecordRaw[]
): RouteRecordRaw[] {
  const result = <RouteRecordRaw[]>[]
  routes.forEach((e) => {
    const route = <RouteRecordRaw>{ ...e }
    if (hasPermission(perms, route)) {
      if (route.children) {
        route.children = filterAsyncRoutes(perms, route.children)
      }
      result.push(route)
    }
  })
  return result
}

/**
 * Use meta.perms to determine if the current user has permission
 * @param perms
 * @param route
 */
function hasPermission(perms: string[], route: RouteRecordRaw) {
  if (route.meta && route.meta.perms) {
    // 如果该路由设置了权限,检查用户权限
    return perms.some((perm) => route.meta?.perms?.includes(perm))
  } else {
    // 如果该路由没有设置权限,则默认所有人可见
    return true
  }
}

7.2 调用

  • 页面中使用
<!-- 该页面属于 layout 中 -->
<!-- src/layout/Header.vue -->
<template>
  <div class="header">
    <el-header>
      <Breadcrumb />
        <!-- 使用本地存储中的属性 -->
      <div>{{ userStore().userInfo?.name || '测试' }}</div>
    </el-header>
  </div>
</template>

<script setup lang="ts">
// 引入 userStore
import { userStore } from '@/stores/user'
</script>
  • ts 中使用
<!-- 该页面属于 layout 中 -->
<!-- src/layout/menu/menu.ts -->
import { isExternal } from '@/utils/validate'
import { allRoutes } from '@/router'
import type { RouteRecordRaw } from 'vue-router'
import { permissionStore } from '@/stores/permission'

export const useMenu = () => {
  const menus = computed(() => {
    // 使用本地存储获取用户权限 
    const routes: RouteRecordRaw[] = permissionStore().authorizedRoutes
    // 根据权限匹配路由
    console.log(routes)
    console.log(generateMenuFromRoute(routes))
    return generateMenuFromRoute(routes)
  })
  return { menus }
}

八、全局样式

8.1 样式重置

8.1.1 index.scss

// src/assets/style/index.scss
/* 全局样式重置 */
* {
  /* 初始化 */
  padding: 0;
  margin: 0;
}

html.body {
  width: 100%;
  height: 100vh;
}
#app {
  width: 100%;
  height: 100%;
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

8.1.2 reset.scss

// src/assets/style/reset.scss
/**
 * src/assets/style/reset.scss
 * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
 * http://cssreset.com
 */
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  font-weight: normal;
  vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
  display: flex;
}
ol,
ul,
li {
  list-style: none;
}
blockquote,
q {
  quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
  content: '';
  content: none;
}
table {
  border-collapse: collapse;
  border-spacing: 0;
}

/* custom */
a {
  color: #7e8c8d;
  text-decoration: none;
  -webkit-backface-visibility: hidden;
}
::-webkit-scrollbar {
  width: 5px;
  height: 5px;
}
::-webkit-scrollbar-track-piece {
  background-color: rgba(0, 0, 0, 0.2);
  -webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:vertical {
  height: 5px;
  background-color: rgba(125, 125, 125, 0.7);
  -webkit-border-radius: 6px;
}
::-webkit-scrollbar-thumb:horizontal {
  width: 5px;
  background-color: rgba(125, 125, 125, 0.7);
  -webkit-border-radius: 6px;
}
html,
body {
  font-family: 'Arial', 'Microsoft YaHei', '黑体', '宋体', '微软雅黑', sans-serif;
}
body {
  line-height: 1;
  -webkit-text-size-adjust: none;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
html {
  //overflow-y: scroll;
}

/*清除浮动*/
.clearfix:before,
.clearfix:after {
  content: ' ';
  display: inline-block;
  height: 0;
  clear: both;
  visibility: hidden;
}
.clearfix {
  *zoom: 1;
}

/*隐藏*/
.dn {
  display: none;
}

8.1.3 样式引入

  • main.ts
// 引入 index.scss
import '@/assets/style/index.scss' 

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.mount('#app')
  • App.vue
<script setup lang="ts">
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

<template>
  <el-config-provider :locale="zhCn" id="app">
    <router-view />
  </el-config-provider>
</template>

<style scoped lang="scss">
/* 引入 reset.scss */
@import '@/assets/style/reset'; 
</style>

8.2 样式常量

8.2.1 variables.scss

// src/styles/variables.scss
// base
$testWith: 200px;

8.2.2 常量使用

// src/styles/test.scss
// 导入 variables.scss
@import "variables.scss";
.test{
  width: $testWith; // 使用中定义的样式常量
}

九、layout

9.1 面包屑

<!-- src/layout/Breadcrumb.vue -->
<template>
  <div class="breadcrumb">
    <el-breadcrumb class="app-breadcrumb" separator=">">
      <transition-group name="breadcrumb">
        <el-breadcrumb-item
          v-for="(item, index) in route!.matched"
          :key="index">
          <span
            v-if="
              item.redirect === 'noRedirect' ||
              index == route!.matched.length - 1
            "
            class="no-redirect">
            {{ item.meta.title }}
          </span>
          <!-- 放开后可点击 -->
          <!-- <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> -->
          <span v-else>{{ item.meta.title }}</span>
        </el-breadcrumb-item>
      </transition-group>
    </el-breadcrumb>
  </div>
</template>

<script lang="ts" setup>
import { useRoute } from 'vue-router'
const route = useRoute()
</script>

<style scoped lang="scss">
@import '@/styles/layout';
</style>

9.2 导航栏

<!-- src/layout/Header.vue -->
<template>
  <div class="header">
    <el-header>
       <!-- 面包屑 自动导入 -->
      <Breadcrumb />
      <div>{{ userStore().userInfo?.name || '测试' }}</div>
    </el-header>
  </div>
</template>

<script setup lang="ts">
import { userStore } from '@/stores/user'
</script>

<style lang="scss" scoped>
@import '@/styles/layout';
</style>

9.3 侧边栏

  • menu.ts
// src/layout/menu/menu.ts
// 将菜单中的逻辑封装,简化页面代码
import { isExternal } from '@/utils/validate'
import { allRoutes } from '@/router'
import type { RouteRecordRaw } from 'vue-router'
import { permissionStore } from '@/stores/permission'

export interface Menu {
   // 标题
   title: string
   // 图标
   icon?: string
   // 路径
   path: string
   // 子目录
   children?: Menu[]
   // 隐藏
   hidden?: boolean
   // 外链
   external?: boolean
}

// 出口方法,获取到 Menu 对象
export const useMenu = () => {
   const menus = computed(() => {
       // TODO 获取用户权限
       const routes: RouteRecordRaw[] = permissionStore().authorizedRoutes
       // 根据权限匹配路由
       console.log(routes)
       console.log(generateMenuFromRoute(routes))
       return generateMenuFromRoute(routes)
   })
   return { menus }
}

// 根据路由控制菜单显示
const generateMenuFromRoute = (
  routes: RouteRecordRaw[],
  parent?: Menu
): Menu[] => {
  const result: Menu[] = []
  routes.forEach((route) => {
    // 如果路由隐藏直接返回
    if (route.meta?.hidden) {
      return
    }
    // 将当前路由转换为 menu
    const menu: Menu = routeToMenu(route, parent?.path)
    if (route.children && route.children.length > 0) {
      menu.children = generateMenuFromRoute(route.children, menu)
    }
    result.push(menu)
  })

  return result
}

// 路由转化为 Menu 对象
const routeToMenu = (route: RouteRecordRaw, basePath?: string): Menu => {
  let path: string
  if (route.path.startsWith('/')) {
    // / 开头为 / 是绝对路径
    path = route.path
  } else {
    if (route.path == '') {
      // 空路由则返回父路由
      path = basePath ?? ''
    } else {
      // 没有 / 则为相对路径
      path = basePath + '/' + route.path
    }
  }
  return <Menu>{
    title: route.meta?.title ?? '',
    icon: route.meta?.icon,
    path: path,
    external: isExternal(route.path),
    hidden: route.meta?.hidden,
  }
}
  • SideMenuItem.vue
<!-- src/layout/menu/SideMenuItem.vue -->
 <!-- 递归生成菜单 -->
 <template>
   <div>
     <el-sub-menu v-if="item.children" :index="item.path">
       <template #title>
         {{ item.title }}
       </template>
       <side-menu-item
         v-for="menu in item.children"
         :key="menu.path"
         :item="menu" />
     </el-sub-menu>
     <el-menu-item v-else :index="item.path">
       <i :class="item.icon"></i>
       {{ item.title }}
     </el-menu-item>
   </div>
 </template>
 
 <script setup lang="ts">
 import type { Menu } from '@/layout/menu/menu'
 // 父传子属性
 const props = defineProps<{
   item: Menu
 }>()
 </script>
  • SideBar.vue
<!-- src/layout/menu/SideBar.vue -->
<!-- 侧边栏结构 -->
<template>
  <div class="sideBar">
    <div class="title">后台管理系统</div>
    <el-scrollbar>
      <el-menu text-color="#fff" background-color="#304156" router>
        <side-menu-item v-for="menu in menus" :key="menu.path" :item="menu" />
      </el-menu>
    </el-scrollbar>
  </div>
</template>

<script setup lang="ts">
import { useMenu } from '@/layout/menu/menu'
const { menus } = useMenu()
</script>

<style scoped lang="scss">
@import '@/styles/layout';
</style>

9.4 页脚

<!-- src/layout/Footer.vue -->
<template>
  <div class="footer">
    <el-card>Frontend 2022 dily</el-card>
  </div>
</template>

<script lang="ts" setup></script>

<style scoped lang="scss"></style>

9.5 主体结构

由 面包屑、侧边栏、导航栏、页脚搭建组合成

<!-- src/layout/Home.vue -->
<template>
  <div class="common-layout">
    <el-container>
      <el-aside>
        <side-bar />
      </el-aside>
      <el-container>
        <Header />
        <el-main>
          <div class="cont">
            <router-view v-slot="{ Component, route }">
              <transition name="fade-transform" mode="out-in">
                <keep-alive :include="cachedViews">
                  <component :is="Component" :key="route.path" />
                </keep-alive>
              </transition>
            </router-view>
          </div>
        </el-main>
        <el-footer>
          <Footer />
        </el-footer>
      </el-container>
    </el-container>
  </div>
</template>
<script setup lang="ts">
const cachedViews = ref<string[]>([])
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
@import '@/styles/layout';
</style>

十、错误页面

错误页面 仅供参考,可自行替换

10.1 404页面

<!-- src/component/error/404.vue -->
<template>
  <div class="wscn-http404-container">
    <div class="wscn-http404">
      <div class="pic-404">
        <img
          class="pic-404__parent"
          src="@/assets/404_images/404.png"
          alt="404" />
        <img
          class="pic-404__child left"
          src="@/assets/404_images/404_cloud.png"
          alt="404" />
        <img
          class="pic-404__child mid"
          src="@/assets/404_images/404_cloud.png"
          alt="404" />
        <img
          class="pic-404__child right"
          src="@/assets/404_images/404_cloud.png"
          alt="404" />
      </div>
      <div class="bullshit">
        <div class="bullshit__oops">OOPS!</div>
        <div class="bullshit__info">
          All rights reserved
          <a
            style="color: #20a0ff"
            href="https://wallstreetcn.com"
            target="_blank">
            wallstreetcn
          </a>
        </div>
        <div class="bullshit__headline">{{ message }}</div>
        <div class="bullshit__info">
          Please check that the URL you entered is correct, or click the button
          below to return to the homepage.
        </div>
        <a href="" class="bullshit__return-home">Back to home</a>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Page_404',
  computed: {
    message() {
      return 'The webmaster said that you can not enter this page...'
    },
  },
}
</script>

<style lang="scss" scoped>
.wscn-http404-container {
  transform: translate(-50%, -50%);
  position: absolute;
  top: 40%;
  left: 50%;
}
.wscn-http404 {
  position: relative;
  width: 1200px;
  padding: 0 50px;
  overflow: hidden;
  .pic-404 {
    position: relative;
    float: left;
    width: 600px;
    overflow: hidden;
    &__parent {
      width: 100%;
    }
    &__child {
      position: absolute;
      &.left {
        width: 80px;
        top: 17px;
        left: 220px;
        opacity: 0;
        animation-name: cloudLeft;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      &.mid {
        width: 46px;
        top: 10px;
        left: 420px;
        opacity: 0;
        animation-name: cloudMid;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1.2s;
      }
      &.right {
        width: 62px;
        top: 100px;
        left: 500px;
        opacity: 0;
        animation-name: cloudRight;
        animation-duration: 2s;
        animation-timing-function: linear;
        animation-fill-mode: forwards;
        animation-delay: 1s;
      }
      @keyframes cloudLeft {
        0% {
          top: 17px;
          left: 220px;
          opacity: 0;
        }
        20% {
          top: 33px;
          left: 188px;
          opacity: 1;
        }
        80% {
          top: 81px;
          left: 92px;
          opacity: 1;
        }
        100% {
          top: 97px;
          left: 60px;
          opacity: 0;
        }
      }
      @keyframes cloudMid {
        0% {
          top: 10px;
          left: 420px;
          opacity: 0;
        }
        20% {
          top: 40px;
          left: 360px;
          opacity: 1;
        }
        70% {
          top: 130px;
          left: 180px;
          opacity: 1;
        }
        100% {
          top: 160px;
          left: 120px;
          opacity: 0;
        }
      }
      @keyframes cloudRight {
        0% {
          top: 100px;
          left: 500px;
          opacity: 0;
        }
        20% {
          top: 120px;
          left: 460px;
          opacity: 1;
        }
        80% {
          top: 180px;
          left: 340px;
          opacity: 1;
        }
        100% {
          top: 200px;
          left: 300px;
          opacity: 0;
        }
      }
    }
  }
  .bullshit {
    position: relative;
    float: left;
    width: 300px;
    padding: 30px 0;
    overflow: hidden;
    &__oops {
      font-size: 32px;
      font-weight: bold;
      line-height: 40px;
      color: #1482f0;
      opacity: 0;
      margin-bottom: 20px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-fill-mode: forwards;
    }
    &__headline {
      font-size: 20px;
      line-height: 24px;
      color: #222;
      font-weight: bold;
      opacity: 0;
      margin-bottom: 10px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.1s;
      animation-fill-mode: forwards;
    }
    &__info {
      font-size: 13px;
      line-height: 21px;
      color: grey;
      opacity: 0;
      margin-bottom: 30px;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.2s;
      animation-fill-mode: forwards;
    }
    &__return-home {
      display: block;
      float: left;
      width: 110px;
      height: 36px;
      background: #1482f0;
      border-radius: 100px;
      text-align: center;
      color: #ffffff;
      opacity: 0;
      font-size: 14px;
      line-height: 36px;
      cursor: pointer;
      animation-name: slideUp;
      animation-duration: 0.5s;
      animation-delay: 0.3s;
      animation-fill-mode: forwards;
    }
    @keyframes slideUp {
      0% {
        transform: translateY(60px);
        opacity: 0;
      }
      100% {
        transform: translateY(0);
        opacity: 1;
      }
    }
  }
}
</style>

  • src/assets/404_images
    404.png
    在这里插入图片描述

404_cloud.png
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3UgNjSzB-1684392651798)(img/404_cloud.png)]

10.2 401页面

<!-- src/component/error/401.vue -->
<template>
  <div class="errPage-container">
    <el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back">
      返回
    </el-button>
    <el-row>
      <el-col :span="12">
        <h1 class="text-jumbo text-ginormous">Oops!</h1>
        gif来源
        <a href="https://zh.airbnb.com/" target="_blank">airbnb</a>
        页面
        <h2>你没有权限去该页面</h2>
        <h6>如有不满请联系你领导</h6>
        <ul class="list-unstyled">
          <li>或者你可以去:</li>
          <li class="link-type">
            <router-link to="/dashboard">回首页</router-link>
          </li>
          <li class="link-type">
            <a href="https://www.taobao.com/">随便看看</a>
          </li>
          <li>
            <a href="#" @click.prevent="dialogVisible = true">点我看图</a>
          </li>
        </ul>
      </el-col>
      <el-col :span="12">
        <img
          :src="errGif"
          width="313"
          height="428"
          alt="Girl has dropped her ice cream." />
      </el-col>
    </el-row>
    <el-dialog v-model:visible="dialogVisible" title="随便看">
      <img :src="ewizardClap" class="pan-img" />
    </el-dialog>
  </div>
</template>

<script>
import errGif from '@/assets/401_images/401.gif'

export default {
  name: 'Page_401',
  data() {
    return {
      errGif: errGif + '?' + +new Date(),
      ewizardClap:
        'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
      dialogVisible: false,
    }
  },
  methods: {
    back() {
      if (this.$route.query.noGoBack) {
        this.$router.push({ path: '/dashboard' })
      } else {
        this.$router.go(-1)
      }
    },
  },
}
</script>

<style lang="scss" scoped>
.errPage-container {
  width: 800px;
  max-width: 100%;
  margin: 100px auto;
  .pan-back-btn {
    background: #008489;
    color: #fff;
    border: none !important;
  }
  .pan-gif {
    margin: 0 auto;
    display: block;
  }
  .pan-img {
    display: block;
    margin: 0 auto;
    width: 100%;
  }
  .text-jumbo {
    font-size: 60px;
    font-weight: 700;
    color: #484848;
  }
  .list-unstyled {
    font-size: 14px;
    li {
      padding-bottom: 5px;
    }
    a {
      color: #008489;
      text-decoration: none;
      &:hover {
        text-decoration: underline;
      }
    }
  }
}
</style>
  • src/assets/401_images
    401.gif
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-txt7Z8dX-1684392651799)(img/401.gif)]

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/540135.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

加密解密软件VMProtect入门使用教程(八)控制台版本

VMProtect是新一代软件保护实用程序。VMProtect支持德尔菲、Borland C Builder、Visual C/C、Visual Basic&#xff08;本机&#xff09;、Virtual Pascal和XCode编译器。 同时&#xff0c;VMProtect有一个内置的反汇编程序&#xff0c;可以与Windows和Mac OS X可执行文件一起…

【C++初阶】模板

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C初阶 ⭐代码仓库&#xff1a;C初阶 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们的支持是我…

Java应用程序性能调优的秘诀:掌握JVM自带的命令行工具

Java 自带了多个 JVM 调优工具&#xff0c;以下是其中一些常用的工具&#xff1a; 1&#xff09;jps&#xff08;JVM Process Status Tool&#xff09;&#xff1a; 用于显示当前系统中所有正在运行的 Java 进程的 PID 和相关信息。 2&#xff09;jstat&#xff08;JVM Stati…

常用的表格检测识别方法-表格区域检测方法(上)

常用的表格检测识别方法 表格检测识别一般分为三个子任务&#xff1a;表格区域检测、表格结构识别和表格内容识别。本章将围绕这三个表格识别子任务&#xff0c;从传统方法、深度学习方法等方面&#xff0c;综述该领域国内国外的发展历史和最新进展&#xff0c;并提供几个先进…

ERP、SCM与CRM系统的关系和区别是什么?

在当今数字化时代&#xff0c;企业管理系统扮演着至关重要的角色&#xff0c;而ERP、SCM和CRM系统是其中三个核心组成部分。 虽然它们都在企业管理中发挥着关键作用&#xff0c;但它们各自的功能和应用领域存在一些区别。 我们先来看看&#xff0c;ERP、SCM与CRM系统分别是啥…

张驰咨询:突破瓶颈降低成本-精益生产咨询的实践策略

在现代企业运营中&#xff0c;提高效率、优化流程是实现成功的关键因素之一。为了帮助企业在这方面取得突破性的进展&#xff0c;精益生产咨询成为了一种备受推崇的方法。本文将介绍精益生产咨询的基本原理、优势以及如何将其应用于企业实践中。 精益生产咨询是一种源于丰田生…

软考- 受限双端队列出队顺序--后面有历年真题

前提&#xff1a;栈和队列 栈&#xff1a; 先进后出 队列&#xff1a;先进先出 对于元素1&#xff0c;2&#xff0c;3&#xff0c;4按照顺序进出栈和队列时&#xff1a; 对于入栈&#xff0c;出栈时&#xff1a; 可以进一个元素&#xff0c;出一个元素&#xff1a; 能得…

基于MATLAB的车牌识别系统+GUI界面的毕业设计(完整源码+课题报告+说明文档+数据)

文章目录 1. 前言2. 实现步骤1&#xff09;颜色信息提取2&#xff09;倾斜校正3&#xff09;字符分割4&#xff09;字体识别5&#xff09;语音播报6&#xff09;存储数据 3. 效果展示4. 总结5. 完整源码下载 1. 前言 近年来,随着交通现代化的发展要求,汽车牌照自动识别技术已经…

网络安全实验——web安全

目录 实验目的 实验原理 实验内容 实验1 1. 地址栏中执行脚本 2. 对DOM技术的利用 实验2 1.jump1 2.jump2 3.get 4.xss 5.注册bss 6.盗取cookie 7.分析cookie 实验3 一.搭建IIS服务器 二.身份验证和权限设置 三.IP地址和域名限制 四.安全通信 五. 单点登录 …

《终身成长》笔记四——如何面对失败

目录 经典摘录 秉性 一个英雄具备的所有特质 ​编辑 什么是成功 什么是失败 掌控成功 领导力与固定型思维模式 成长型思维模式领导者的行为 害羞 经典摘录 秉性 天才们&#xff0c;因为自己拥有的优势而得意忘形&#xff0c;不去学习如何努力奋斗以及如何面对挫折。…

Win10系统开机使用一段时间会变成蓝屏怎么办?

Win10系统开机使用一段时间会变成蓝屏怎么办&#xff1f;最近有用户在使用电脑的时候遇到了一个问题&#xff0c;当自己开机使用了一段时间之后&#xff0c;电脑就会变成蓝屏无法操作&#xff0c;导致自己的使用中断了。如果经常出现这样的问题&#xff0c;那么就需要去进行电脑…

图解LeetCode——141. 环形链表

一、题目 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置&a…

使用jsDelivr和GitHub,上传本地静态资源到免费CDN

目标&#xff1a;将本地图片资源上传到免费CDN&#xff0c;以便随时使用&#xff01;其他静态资源亦可&#xff0c;例如&#xff1a;js、css、pdf、word、excel 等等 ①在github上创建新仓库&#xff1a;resources&#xff0c;用于存放要上传到 CDN 的静态资源&#xff1a; ②上…

成都欢蓬信息:抖音电商去年GMV增速超80%

在今年的抖音电商生态大会上&#xff0c;抖音电商交出了年度“成绩单”。 5月16日&#xff0c;抖音电商总裁魏雯雯披露&#xff0c;近一年抖音电商GMV&#xff08;成交额&#xff09;增幅超80%。其中&#xff0c;商城GMV同比增长277%&#xff0c;电商搜索GMV同比增长159%&#…

NAVICAT 自动备份数据库到本地

1:设置备份文件存储路径地址 右键数据库连接 ——》编辑连接——》高级 2:选择要备份的数据库 点击 备份——》新建备份——》对象选择 ——》保存 输入文件名称 ——》确定 备份 下出现 保存的备份文件 3:设置自动备份 点击 自动运行——》新建批处理作业 点击 新建…

家电回收APP小程序开发 上门回收旧物管理专家

家用电器使我们日常生活中必不可少的用品&#xff0c;随着使用年限的增加&#xff0c;可能会出现老化问题&#xff0c;人们买了新的之后&#xff0c;废旧电器的处理也成为一大难题。笨重不易移动&#xff0c;扔了可惜&#xff0c;放置占地&#xff0c;该怎么办呢&#xff1f;废…

【Linux下】 线程同步 生产者与消费者模型

文章目录 【Linux下】 线程同步 生产者与消费者模型线程同步同步概念与竞态条件条件变量条件变量本质 操作条件变量初始化和销毁条件变量等待唤醒 通过条件变量实现的简单线程同步例子为什么pthread_cond_wait需要互斥锁条件变量使用规范 生产者与消费者模型生活中的生产者与消…

jQuery学习记录--jQuery语法,选择器,事件及hide(),show(), toggle()

jQuery学习记录–jQuery语法&#xff0c;选择器&#xff0c;事件及hide&#xff08;&#xff09;&#xff0c;show()&#xff0c;toggle() jQuery 简介 jQuery 是一个 JavaScript 库。jQuery 极大地简化了 JavaScript 编程。 jQuery 库包含以下功能&#xff1a; HTML 元素选…

AMBER分子动力学模拟之结果分析(构象分析)-- HIV蛋白酶-抑制剂复合物(6)

AMBER分子动力学模拟之结果分析(构象分析)-- HIV蛋白酶-抑制剂复合物(6) RMSD RMSF b-facto计算 RMSD RMSD measures the deviation of a target set of coordinates (i.e. a structure) to a reference set of coordinates, with R M S D 0.0 \mathrm{RMSD}0.0 RMSD0.0 i…

【分享】又找到几个免费使用gpt4的方法!

哈喽&#xff0c;大家好&#xff0c;我是木易巷~ GPT-4是OpenAI推出的最新人工智能语言模型&#xff0c;拥有惊人的1750亿个参数&#xff0c;是目前最大、最强大的语言模型之一&#xff0c;能够根据给定的文本或关键词生成高质量的文本&#xff0c;可以处理多模态数据&#xf…