后台端

目录结构

├──📂 node_modules                     # node依赖
├──📂 public                           # 公共目录
├──📂 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配置项
├── .stylelintrc.cjs                   # stylelintrc配置项
├── 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-variables

  • .env.development: 开发环境适用
# 环境模式
NODE_ENV = 'development'

# 请求域名
VITE_APP_BASE_URL='https://fastapi.waitadmin.cn'  
  • .env.production: 生产环境适用
# 环境模式
NODE_ENV = 'production'

# 请求域名
VITE_APP_BASE_URL='https://fastapi.waitadmin.cn'  

系统配置

const config = {
    // 网站标题
    title: 'WaitAdmin(Python)后台管理系统',
    // 版本编号
    version: '1.0.0',
    // 请求域名
    baseUrl: `${import.meta.env.VITE_APP_BASE_URL || ''}`,
    // 请求前缀
    urlPrefix: '/spi',
    // 请求超时
    timeout: 10 * 1000
}

风格配置

const defaultSetting = {
    // 主题配色
    theme: 'black-blue',
    // 页面布局
    layout: 'classic',
    // 历史标签风格
    tagsStyle: 'nimble',
    // 页面切换动画
    pagesAnimation: 'slide-fade',
    // 菜单高亮风格
    menuLightStyle: 'card',
    // 菜单手风琴
    isUniqueOpened: false,
    // 暗黑主题色
    isDarkColor: false,

    // 显示标签栏
    isTabMultiple: true,
    // 显示面包屑
    isBreadcrumb: true,
    // 显示图标 (Logo)
    isLayoutLogo: true,
    // 页面缓存
    isKeepAlive: true,

    // 颜色风格
    primaryTheme: '#0f70d8', // 默认主题色
    successTheme: '#67c23a', // 成功主题色
    warningTheme: '#e6a23c', // 警告主题色
    dangerTheme: '#f56c6c',  // 危险主题色
    errorTheme: '#f56c6c',   // 错误主题色
    infoTheme: '#909399'     // 信息主题色
}

路由

目前路由分为两部分:

  • 静态路由src/router/index.ts
  • 动态路由:在系统中的菜单中添加

路由说明

{
    path: '/path'               // 路由路径
    name:'router-name'          // 路由名称
    meta : {
        type: 'M'              // 路由类型     
        icon: 'icon-name'      // 设置该路由的图标
        title: 'title'         // 设置该路由的标题
        perms: 'admin:lists'   // 设置该路由的权限
        query: '{"id": 1}'     // 访问路由的默认传递参数
        hidden: true           // 当设置 true 的时候该路由不会在侧边栏出现
    }
    component: () => import('@/views/admin/index.vue')  // 路由组件
}

静态路由

src/router/index.ts

const router: Router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            path: '/:pathMatch(.*)*',
            component: () => import('@/views/error/404.vue')
        },
        {
            path: '/403',
            component: () => import('@/views/error/403.vue')
        },
        {
            path: '/login',
            component: () => import('@/views/login.vue')
        },
        {
            path: '/auth/account',
            component: Layout,
            children: [
                {
                    path: 'setting',
                    component: () => import('@/views/auth/admin/setting.vue'),
                    name: Symbol(),
                    meta: {
                        title: '个人设置'
                    }
                }
            ]
        }
        // 更多静态路由添加在这里...
    ]
})

动态路由

  • 动态路由的渲染完全由服务端返回的数据进行控制。
  • 为什么要这么做? 主要是为了权限控制没权限不显示。
  • 下面演示如何添加动态菜单:
    • 在系统的 权限>菜单管理>新增, 按照提示输入便可添加菜单。

杂项

权限控制

系统内部集成了两种精细化的权限管理机制,以确保系统的安全性与用户体验的灵活性。

页面级权限控制:
    系统采用了一种动态路由生成策略,依据服务端根据登录管理员角色权限筛选后的菜单数据,前端接收到这些过滤后的菜单项后, 通过一系列高效的数据转换与处理,动态构建出路由表。这一过程确保了只有拥有相应权限的管理员才能访问到被授权的页面, 未授权的页面则不会注册到路由中,从而实现了严格的页面级权限控制。

按钮级权限控制:
    为了进一步提升权限管理的精细度,系统还引入了按钮级权限控制机制。服务端会向前端返回当前管理员的详细权限列表。 前端接收到这些权限数据后,利用自定义的Vue指令v-perms,对页面上的每一个按钮进行权限比对。 若按钮所需的权限存在于返回的权限列表中,则按钮正常显示;否则,按钮将被隐藏或限制点击。 这一机制使得系统能够灵活应对复杂的权限需求,确保每个操作都符合当前管理员的权限范围。

<!-- 需要与添加菜单时的权限字符一致 -->
<el-button v-perms="['auth:admin:edit']">编辑</el-button>

<!-- 多个权限字符控制 -->
<el-button v-perms="['auth:admin:edit','auth:admin:add']">编辑</el-button>

本地缓存

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

设置缓存:

import { cacheEnum } from '~/enums/cache'
import cacheUtil from '@/utils/cache'

// 设置不会失效缓存
cacheUtil.set(cacheEnum.TOKEN_KEY, 'xxx')

// 设置带过期时间缓存(单位为s)
cacheUtil.set(cacheEnum.TOKEN_KEY, 'xxx', 3600)

获取缓存:

import { cacheEnum } from '~/enums/cache'
import cacheUtil from '@/utils/cache'

const data = cacheUtil.get(cacheEnum.TOKEN_KEY)
console.log(data)

删除缓存:

import { cacheEnum } from '~/enums/cache'
import cacheUtil from '@/utils/cache'

cacheUtil.delete(cacheEnum.TOKEN_KEY)

清空缓存:

import cacheUtil from '@/utils/cache'

cacheUtil.clear()

组件注册

通过集成 unplugin-auto-importunplugin-vue-components 以及 vite-plugin-style-import这三个强大的插件, 我们实现了对Vue项目中组件及Element Plus UI库组件的自动且精确的按需引入功能。这一设置极大地简化了开发流程, 因为无论是自定义组件还是Element Plus的UI组件,都无需在每个组件文件中手动进行注册或引入样式文件, 插件会自动处理这些繁琐的步骤,让开发者能够更加专注于业务逻辑的实现。

使用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)
}

自定义指令

  • v-perms为例子: 在src/install/directives下面新建一个文件perms.ts, 指令名即为文件名
import useUserStore from '@/stores/modules/user'

export default {
    mounted: (el: HTMLElement, binding: any): void => {
        const { value } = binding
        const userStore = useUserStore()
        const permissions = userStore.perms
        const allPermission: string = '*'

        if (Array.isArray(value)) {
            if (value.length > 0) {
                const hasPermission = permissions.some((key: string) => {
                    return allPermission === key || value.includes(key)
                })

                const hide: boolean = false
                if (!hasPermission) {
                    if (hide) {
                        el && el.removeChild(el)
                    } else {
                        el && el.classList.add('is-disabled')
                        el && el.setAttribute('aria-disabled', 'true')
                        el && el.setAttribute('disabled', 'true')
                    }
                }
            }
        } else {
            throw new Error('like v-perms="[\'auth:admin:add\']"')
        }
    }
}

样式

项目中使用了scss 同时也使用了tailwindcss
在写前端页面之前去了解一下tailwindcss将对你的开发很有帮助

样式文件位于src/assets/styles下面:

├──📂 styles
│  ├── element.scss   # 修改element-plus组件的样式
│  ├── index.scss     # 入口文件
│  ├── public.scss    # 公共样式
│  ├── tailwind.css   # 引入tailwindcss样式表
│  ├── theme.scss     # 主题风格样式表
│  ├── variables.css  # css变量  

tailwindcss

具体使用说明详见: https://tailwindcss.com/
Vscode中安装插件: Tailwind CSS IntelliSense

安装插件后可以有代码提示, 如果没有提示出现,就按空格键 ![](/docs/python/images/tailwind_tips.png

tailwindcss配置:
配置文件位于目录的 tailwind.config.ts

/** @type {import('tailwindcss').Config} */
export default {
    darkMode: 'class',
    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)'
            },
            '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': '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)',
            'fill-light': 'var(--el-fill-color-light)',
            'fill-lighter': 'var(--el-fill-color-lighter)',
            'mask': 'var(--el-mask-color)'
        },
        fontFamily: {
            'sans': ['PingFang SC', 'Arial', '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'
        },
        spacing: {
            px: '1px',
            0: '0px',
            0.5: '2px',
            1: '4px',
            1.5: '6px',
            2: '8px',
            2.5: '10px',
            3: '12px',
            3.5: '14px',
            4: '16px',
            5: '20px',
            6: '24px',
            7: '28px',
            8: '32px',
            9: '36px',
            10: '40px',
            11: '44px',
            12: '48px',
            14: '56px',
            16: '64px',
            20: '80px',
            24: '96px',
            28: '112px',
            32: '128px',
            36: '144px',
            40: '160px',
            44: '176px',
            48: '192px',
            52: '208px',
            56: '224px',
            60: '240px',
            64: '256px',
            72: '288px',
            80: '320px',
            96: '384px'
        },
        lineHeight: {
            none: '1',
            tight: '1.25',
            snug: '1.375',
            normal: '1.5',
            relaxed: '1.625',
            loose: '2',
            3: '12px',
            4: '16px',
            5: '20px',
            6: '24px',
            7: '28px',
            8: '32px',
            9: '36px',
            10: '40px'
        }
    },
    plugins: []
}

页面样式

scoped

// 没有加scoped属性, 会污染全局样式
<style scoped lang="scss"></style>

样式穿透

// 开启scoped属性后需要如果需要将样式作用到子组件上, 可以这样处理:
<style scoped lang="scss">
:deep(.el-menu-item) {
    
}
</style>

图标

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中即可
  • PS: 自定义的svg图标,名称都是svg-开头的, 而官方的是el-开头的
// 使用方式
<icon :size="20" name="svg-icon-copy" />
上次更新:
贡献者: zero