前端

前序准备

首先需要在本地安装nodeopen in new window,推荐node的版本14.18+、16+。

编辑器推荐使用Visual Studio Code,编辑器插件推荐安装ESLintopen in new windowPrettier ESLintopen in new windowTailwind CSS IntelliSenseopen in new windowVue Language Features (Volar)open in new windowTypeScript Vue Plugin (Volar)open in new window

项目规范

本项目中使用了eslint去检查代码规范,使用prettier去格式化代码

编辑器自动校验

使用vscode进行开发,可以搭配vscode的一些插件,实现自动修改一些错误,同时项目中也自带了vscode的一些配置,在 .vscode/setting.json 文件中。 注意:要自动修复错误需要使用vscode打开admin文件夹才行

如果使用vscode格式化后还是出现很多eslint错误,有可能是格式化程序设置有误,只需要设置默认的格式化程序为Prettier ESLint即可

手动校验代码

执行命令:

yarn lint 
// 如果没安装yarn,使用npm run lint

目录结构

├──📂 .vscode                          # vscode配置文件
├──📂 scripts                          # js脚本
├──📂 src                              # 源代码
│  ├──📂 api                           # 所有请求
│  ├──📂 assets                        # 字体,图片等静态资源
│  ├──📂 components                    # 全局公用组件
│  ├──📂 config                        # 配置相关
│  ├──📂 enums                         # 全局枚举
│  ├──📂 hooks                         # 全局hook
│  ├──📂 install                       # 插件安装及自定义指令组册
│  ├──📂 layout                        # 布局组件
│  ├──📂 router                        # 路由
│  ├──📂 stores                        # 全局状态管理
│  ├──📂 styles                        # 全局样式
│  ├──📂 utils                         # 全局公用方法
│  ├──📂 views                         # 所有页面
│  ├── App.vue                         # 入口页面
│  ├── main.ts                         # 入口文件 初始化,组册插件等
│  └── permission.ts                   # 路由拦截,权限管理
├──📂 typings                          # ts声明文件
├── .env.xxx                           # 环境变量配置
├── .eslintrc.cjs                      # eslint 配置项
├── package.json                       # package.json
├── postcss.config.js                  # postcss 配置项
├── tailwind.config.js                 # tailwindcss 配置项
├── tsconfig.json                      # ts 配置项
├── vite.config.ts                     # vite 配置项

配置项

修改默认的配置

环境变量

变量命名规则:需要以VITE_为前缀的
如何使用:import.meta.env.VITE_
更多细节见https://vitejs.cn/guide/env-and-mode.html#env-variablesopen in new window

  • .env.development
    开发环境适用
NODE_ENV = 'development'

# 请求域名
VITE_APP_BASE_URL='https://likeadmin-java.yixiangonline.com'  
  • .env.production
    生产环境适用
NODE_ENV = 'production'

# 请求域名
VITE_APP_BASE_URL=''  //填空则跟着网站的域名来请求

系统配置项

路径:src/config/index.ts,说明如下:

const config = {
    terminal: 1, //终端
    title: '后台管理系统', //网站默认标题
    version: '1.2.1', //版本号
    baseUrl: `${import.meta.env.VITE_APP_BASE_URL}/`, //请求接口域名
    urlPrefix: 'adminapi', //请求默认前缀
    timeout: 10 * 1000 //请求超时时长
}

主题配置项

修改系统默认的主题 路径:src/config/setting.ts,说明如下:

const defaultSetting = {
    sideWidth: 200, //侧边栏宽度
    sideTheme: 'light', //侧边栏主题
    sideDarkColor: '#1d2124', //侧边栏深色主题颜色
    theme: '#4A5DFF', //主题色
    successTheme: '#67c23a', //成功主题色
    warningTheme: '#e6a23c', //警告主题色
    dangerTheme: '#f56c6c', //危险主题色
    errorTheme: '#f56c6c', //错误主题色
    infoTheme: '#909399' //信息主题色
}
//以上各种主题色分别对应element-plus的几种行为主题

路由

目前路由分为两部分,一部分是静态路由:src/router/routes.ts,一部分是动态路由:在系统中的菜单中添加。

路由配置说明

path: '/path'                      // 路由路径
name:'router-name'                 // 设定路由的名字
meta : {
    title: 'title'                  // 设置该路由在侧边栏的名字
    icon: 'icon-name'                // 设置该路由的图标
    activeMenu: '/system/user'      // 当路由设置了该属性,则会高亮相对应的侧边栏。
    query: '{"id": 1}'             // 访问路由的默认传递参数
    hidden: true                   // 当设置 true 的时候该路由不会在侧边栏出现 
    hideTab: true                   //当设置 true 的时候该路由不会在多标签tab栏出现
}
component: () => import('@/views/user/setting.vue')  // 路由组件
 

meta配置ts扩展

typings/router.d.ts

import 'vue-router'
declare module 'vue-router' {
    // 扩展 RouteMeta
    interface RouteMeta {
        title?: string
        icon?: string
        hidden?: boolean
        activeMenu?: string
        hideTab?: boolean
    }
}

静态路由

src/router/routes.ts

export const constantRoutes: Array<RouteRecordRaw> = [
    {
        path: PageEnum.ERROR_404,
        component: () => import('@/views/error/404.vue')
    },
    {
        path: PageEnum.ERROR_403,
        component: () => import('@/views/error/403.vue')
    },
    {
        path: PageEnum.LOGIN,
        component: () => import('@/views/account/login.vue')
    },
    //多级路由:LAYOUT布局
    {
        path: '/user',
        component: LAYOUT,
        children: [
            {
                path: 'setting',
                component: () => import('@/views/user/setting.vue'),
                meta: {
                    title: '个人设置'
                }
            }
        ]
    },
    {
        path: '/dev_tools',
        component: LAYOUT,
        children: [
            {
                path: 'code/edit',
                component: () => import('@/views/dev_tools/code/edit.vue'),
                meta: {
                    title: '编辑数据表',
                    activeMenu: '/dev_tools/code'
                }
            }
        ]
    },
    {
        path: '/setting',
        component: LAYOUT,
        children: [
            {
                path: 'dict/data',
                component: () => import('@/views/setting/dict/data/index.vue'),
                meta: {
                    title: '数据管理',
                    activeMenu: '/setting/dict'
                }
            }
        ]
    }
    //要添加路由可直接在这里加
]

动态路由

如何添加一个动态路由:详见下面的菜单

菜单

菜单的渲染完全由服务端返回的数据控制,如何添加一个菜单:

在系统的权限管理>菜单>新增,按照提示输入便可添加菜单

菜单经过前端转化为路由动态插进路由表中

菜单有三种类型:

export enum MenuEnum {
    CATALOGUE = 'M', //目录
    MENU = 'C', //菜单
    BUTTON = 'A' //按钮
}

目录:不会渲染具体的页面,下图中所有含有小三角图标的都为目录
菜单:菜单代表着具体的页面,下图的工作台即为一个菜单
按钮:用于按钮级别的权限控制

权限

系统中有两种权限控制方式

  • 根据后台返回的菜单,动态生成路由表,实现对页面的权限控制
  • 根据后台返回的权限列表,控制到具体每个页面的按钮的显示隐藏

页面级别的权限:服务端通过登录的管理员的角色权限过滤掉没有权限的菜单,前端拿到过滤好的菜单,经过一系列转化,动态生成路由表,没有权限的页面将不会注册在路由中,最终实现的页面级别的权限控制。

按钮级别的权限:服务端返回该管理员的权限列表,前端拿到权限列表后,通过自定义指令v-perms对每个按钮的权限进行比对,如果按钮对应的权限在返回的权限列表中,则显示该菜单,反之则隐藏。

控制按钮权限说明:

// auth.admin/edit 需要与添加菜单时的权限字符一致,一般对应服务端的api接口
<el-button v-perms="['auth.admin/edit']">编辑</el-button>
//多个控制一个按钮
<el-button v-perms="['auth.admin/edit','auth.admin/add']">编辑</el-button>

接口请求

系统中使用axios这个库来发起请求,并对其进行了更深一步的封装
位于src/utils/request

├──📂 request
│  ├── axios.ts    # 封装的axios实例
│  ├── cancel.ts   # 封装的取消重复请求实例
│  ├── index.ts    # 接口返回统一处理及默认配置
│  ├── type.d.ts   # 类型声明文件

一般只需要修改index.ts文件,其他文件无需修改

index.ts文件说明:

默认配置

const defaultOptions: AxiosRequestConfig = {
    //接口超时时间
    timeout: configs.timeout,
    // 基础接口地址
    baseURL: configs.baseUrl,
    //请求头
    headers: { 'Content-Type': ContentTypeEnum.JSON, version: configs.version },
    // 处理 axios的钩子函数
    axiosHooks: axiosHooks,
    // 每个接口可以单独配置
    requestOptions: {
        // 是否将params视为data参数,仅限post请求
        isParamsToData: true,
        //是否返回默认的响应
        isReturnDefaultResponse: false,
        // 需要对返回数据进行处理
        isTransformResponse: true,
        // 接口拼接地址
        urlPrefix: configs.urlPrefix,
        // 忽略重复请求
        ignoreCancelToken: false,
        // 是否携带token
        withToken: true,
        // 开启请求超时重新发起请求请求机制
        isOpenRetry: true,
        // 重新请求次数
        retryCount: 2
    }
}

请求拦截器配置

const axiosHooks: AxiosHooks = {
    requestInterceptorsHook(config) {
        NProgress.start()
        const { withToken, isParamsToData } = config.requestOptions
        const params = config.params || {}
        const headers = config.headers || {}

        // 添加token
        if (withToken) {
            const token = getToken()
            headers.token = token
        }
        // POST请求下如果无data,则将params视为data
        if (
            isParamsToData &&
            !Reflect.has(config, 'data') &&
            config.method?.toUpperCase() === RequestMethodsEnum.POST
        ) {
            config.data = params
            config.params = {}
        }
        config.headers = headers
        return config
    },
    requestInterceptorsCatchHook(err) {
        NProgress.done()
        return err
    },
    async responseInterceptorsHook(response) {
        NProgress.done()
        const { isTransformResponse, isReturnDefaultResponse } = response.config.requestOptions

        //返回默认响应,当需要获取响应头及其他数据时可使用
        if (isReturnDefaultResponse) {
            return response
        }
        // 是否需要对数据进行处理
        if (!isTransformResponse) {
            return response.data
        }
        const { code, data, show, msg } = response.data
        switch (code) {
            case RequestCodeEnum.SUCCESS: //成功
                if (show) {
                    msg && feedback.msgSuccess(msg)
                }
                return data
            case RequestCodeEnum.FAIL: //失败
                if (show) {
                    msg && feedback.msgError(msg)
                }
                return Promise.reject(data)
            case RequestCodeEnum.LOGIN_FAILURE: //token过期
                clearAuthInfo()
                router.push(PageEnum.LOGIN)
                return Promise.reject()
            case RequestCodeEnum.OPEN_NEW_PAGE: //重定向打开页面
                window.location.href = data.url
                return data
            default:
                return data
        }
    },
    responseInterceptorsCatchHook(error) {
        NProgress.done()
        if (error.code !== AxiosError.ERR_CANCELED) {
            error.message && feedback.msgError(error.message)
        }
        return Promise.reject(error)
    }
}

如何在单个接口中单独使用这些配置

// 配置
export function xxxx(data) {
    return request.post({ 
        url: 'xxx',
        header: {
            'Content-type': ContentTypeEnum.FORM_DATA
        },
        data
    }, {
        // 忽略重复请求
        ignoreCancelToken: true,
        // 开启请求超时重新发起请求请求机制
        isOpenRetry: false,
         // 需要对返回数据进行处理
        isTransformResponse: false,
    })
}

组件注册

使用了插件unplugin-auto-importopen in new windowunplugin-vue-componentsopen in new windowvite-plugin-style-importopen in new window
写在components中的组件和element-plus的组件都是自动且按需引入的,不需要在组件中注册

使用vue插件

下面以vue-router为例子: 在src/install/plugins下面新建一个文件router.ts

// router.ts 
import router from '@/router'
import type { App } from 'vue'

export default (app: App<Element>) => {
    app.use(router)
}

这样就完成了插件的注册,不需要将文件引入到main.ts

新增自定义指令

下面以v-perms为例子: 在src/install/directives下面新建一个文件perms.ts,指令名即为文件名

// perms.ts
/**
 * perm 操作权限处理
 * 指令用法:
 *  <el-button v-perms="['auth.menu/edit']">编辑</el-button>
 */

import useUserStore from '@/stores/modules/user'

export default {
    mounted: (el: HTMLElement, binding: any) => {
        const { value } = binding
        const userStore = useUserStore()
        const permissions = userStore.perms
        const all_permission = '*'
        if (Array.isArray(value)) {
            if (value.length > 0) {
                const hasPermission = permissions.some((key: string) => {
                    return all_permission == key || value.includes(key)
                })

                if (!hasPermission) {
                    el.parentNode && el.parentNode.removeChild(el)
                }
            }
        } else {
            throw new Error('like v-perms="[\'auth.menu/edit\']"')
        }
    }
}

这样就完成一个自定义指令,不需要将文件引入到main.ts

样式

项目中使用了scssopen in new window作为预处理语言,同时也使用了tailwindcssopen in new window
样式文件位于src/styles下面:

├──📂 styles
│  ├── dark.css       # 深色模式下的css变量
│  ├── element.scss   # 修改element-plus组件的样式
│  ├── index.scss     # 入口
│  ├── tailwind.css   # 引入tailwindcss样式表
│  ├── var.css        # css变量  

tailwindcss

具体使用说明详见https://tailwindcss.com/open in new window
在vscode中安装插件Tailwind CSS IntelliSenseopen in new window,可以得到提示,如果没有提示出现,就按空格键

tailwindcss配置说明:

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
    theme: {
        colors: {
            //白色
            white: 'var(--color-white)',
            //主题色
            primary: {
                DEFAULT: 'var(--el-color-primary)',
                'light-3': 'var(--el-color-primary-light-3)',
                'light-5': 'var(--el-color-primary-light-5)',
                'light-7': 'var(--el-color-primary-light-7)',
                'light-8': 'var(--el-color-primary-light-8)',
                'light-9': 'var(--el-color-primary-light-9)',
                'dark-2': 'var(--el-color-primary-dark-2)'
            },
            //成功
            success: 'var(--el-color-success)',
            //警告
            warning: 'var(--el-color-warning)',
            //危险
            danger: 'var(--el-color-danger)',
            //危险
            error: 'var(--el-color-error)',
            //信息
            info: 'var(--el-color-info)',
            //body背景
            body: 'var(--el-bg-color)',
            //页面背景
            page: 'var(--el-bg-color-page)',
            //主要字体颜色
            'tx-primary': 'var(--el-text-color-primary)',
            //次要字体颜色
            'tx-regular': 'var(--el-text-color-regular)',
            //次次要字体颜色
            'tx-secondary': 'var(--el-text-color-secondary)',
            //占位字体颜色
            'tx-placeholder': 'var(--el-text-color-placeholder)',
            //禁用颜色
            'tx-disabled': 'var(--el-text-color-disabled)',
            //边框颜色
            br: 'var(--el-border-color)',
            //边框颜色-浅
            'br-light': 'var(--el-border-color-light)',
            //边框颜色-更浅
            'br-extra-light': 'var(--el-border-color-extra-light)',
            //边框颜色-深
            'br-dark': 'var( --el-border-color-dark)',
            //填充色
            fill: 'var(--el-fill-color)',
            //朦层颜色
            mask: 'var(--el-mask-color)'
        },
        fontFamily: {
            sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']
        },
        boxShadow: {
            DEFAULT: 'var(--el-box-shadow)',
            light: 'var(--el-box-shadow-light)',
            lighter: 'var(--el-box-shadow-lighter)',
            dark: 'var(--el-box-shadow-dark)'
        },
        fontSize: {
            xs: 'var(--el-font-size-extra-small)',
            sm: 'var( --el-font-size-small)',
            base: 'var( --el-font-size-base)',
            lg: 'var( --el-font-size-medium)',
            xl: 'var( --el-font-size-large)',
            '2xl': 'var( --el-font-size-extra-large)',
            '3xl': '20px',
            '4xl': '24px',
            '5xl': '28px',
            '6xl': '30px',
            '7xl': '36px',
            '8xl': '48px',
            '9xl': '60px'
        }
    },

    plugins: []
}

页面,组件的样式

  • 开启scoped

没有加scoped属性,会污染全局样式

<style scoped></style>
  • 样式穿透

开启scoped属性后需要如果需要将样式作用到子组件上,可以这样处理:

<style scoped>
:deep(.el-menu-item) {
    
}
</style>

使用本地存储

项目中对本地存储进行了封装,位于src/utils/cache.ts,推荐使用时搭配cacheEnums.ts一起使用

设置缓存:

// src/enums/cacheEnums.ts
export const TOKEN_KEY = 'token'  

// xxx/xxx.ts
import { TOKEN_KEY } from '@/enums/cacheEnums'
cache.set(TOKEN_KEY, 'xxxx') 
//带有时间的缓存
cache.set(TOKEN_KEY, 'xxxx', 10 * 12 *  3600) // 10 * 12 *  3600为缓存时间,单位为s

获取缓存:

import { TOKEN_KEY } from '@/enums/cacheEnums'
cache.get(TOKEN_KEY)

删除缓存:

import { TOKEN_KEY } from '@/enums/cacheEnums'
cache.remove(TOKEN_KEY)

清空缓存:

cache.clear()

图标

项目中对图标的使用进行了封装,位于src/components/icon文件夹内
目前有两种图标:1.element-plus带有的图标库。2.本地svg图标库

element-plus图标库

使用:

// 官方
import { Edit } from '@element-plus/icons-vue'
<el-icon :size="20">
    <Edit />
</el-icon>

//推荐
<icon :size="20" name="el-icon-Edit" />

本地图标库

本地图标库位于src/assets/icons内,如果需要添加svg图标,只需要将svg文件放到src/assets/icons中即可
如:copy.svg
使用:

<icon :size="20" name="local-icon-copy" />

如何添加一个页面

下面以管理员为例:

  1. 在view中新建一个permission目录,代表是权限管理的模块,,然后在permission中新建admin目录,代表着管理员相关页面, 新建index.vue文件和edit.vue文件,分别是管理员列表页面和编辑弹窗。
├──📂 views
│  ├──📂 permission
│  │  ├──📂 admin
│  │  │  ├── index.vue    # 管理员列表页面
│  │  │  ├── edit.vue     # 编辑弹窗
  1. 在系统中的权限管理>菜单,点击新增按钮

新增permission目录:

新增admin菜单:

新增管理员列表按钮:

如果说编辑的内容过多,需要将编辑弹窗做成一个页面,需要在src/router/routes.ts中添加路由

// 在变量constantRoutes中添加  
 {
    path: '/permission',
    component: LAYOUT,
    children: [
        {
            path: 'admin/edit',
            component: () => import('@/views/permission/admin/edit.vue'),
            meta: {
                title: '管理员编辑',
                activeMenu: '/permission/admin' //管理员列表页面路由路径
            }
        }
    ]
}

黑暗主题

黑暗模式的原理是利用css变量和在html标签添加class="dark"实现,如果需要改黑暗模式的css变量可修改文件src/styles/dark.css

组件中的字体颜色,背景颜色等颜色样式需要使用这些变量才能适配黑暗模式和正常模式

使用:

//使用tailwindcss
<template>
    <div class="bg-body text-tx-regular">
        默认背景,次要字体样式
    </div>
</template>

//使用css变量
<template>
    <div class="example">
        默认背景,次要字体样式
    </div>
</template>
<style scoped>
.example {
    background-color: var(--el-bg-color-page);
    color: var(--el-text-color-regular);
}
</style>
上次更新:
贡献者: lr, Jason