yudao-cloud 开发指南 yudao-cloud 开发指南
  • 萌新必读
  • 后端手册
  • 微服务手册
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 前端手册 Vue 2.x
  • 前端手册 Vue 3.x
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 系统手册
视频教程
  • Vue3 + element-plus (opens new window)
  • Vue3 + vben(ant-design-vue) (opens new window)
  • Vue2 + element-ui (opens new window)
单体版 (opens new window)
作者博客 (opens new window)
GitHub (opens new window)
  • 萌新必读
  • 后端手册
  • 微服务手册
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 前端手册 Vue 2.x
  • 前端手册 Vue 3.x
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 系统手册
视频教程
  • Vue3 + element-plus (opens new window)
  • Vue3 + vben(ant-design-vue) (opens new window)
  • Vue2 + element-ui (opens new window)
单体版 (opens new window)
作者博客 (opens new window)
GitHub (opens new window)
  • 萌新必读

    • 简介
    • 交流群
    • 视频教程
    • 功能列表
    • 快速启动(后端项目)
    • 快速启动(前端项目)
    • 接口文档
    • 技术选型
    • 项目结构
    • 代码热加载
    • 一键改包
    • 迁移模块(适合新项目)
    • 删除功能(以租户为例)
    • 表结构变更(版本升级)
    • 国产信创数据库(DM 达梦、大金、OpenGauss)
    • 内网穿透
    • 面试题、简历模版、简历优化
    • 项目外包
  • 后端手册

    • 新建服务
    • 代码生成【单表】(新增功能)
    • 代码生成【主子表】
    • 代码生成(树表)
    • 功能权限
    • 数据权限
    • 用户体系
    • 三方登录
    • OAuth 2.0(SSO 单点登录)
    • SaaS 多租户【字段隔离】
    • SaaS 多租户【数据库隔离】
    • WebSocket 实时通信
    • 异常处理(错误码)
    • 参数校验、时间传参
    • 分页实现
    • VO 对象转换、数据翻译
    • 文件存储(上传下载)
    • Excel 导入导出
    • 操作日志、访问日志、异常日志
    • MyBatis 数据库
    • MyBatis 联表&分页查询
    • 多数据源(读写分离)
    • Redis 缓存
    • 本地缓存
    • 异步任务
    • 分布式锁
    • 幂等性(防重复提交)
    • 请求限流(RateLimiter)
    • HTTP 接口签名(防篡改)
    • 单元测试
    • 验证码
    • 工具类 Util
    • 数据库文档
  • 微服务手册

    • 微服务调试(必读)
    • 注册中心 Nacos
    • 配置中心 Nacos
    • 服务网关 Spring Cloud Gateway
    • 服务调用 Feign
    • 定时任务 XXL Job
    • 消息队列(内存)
    • 消息队列(Redis)
    • 消息队列(RocketMQ)
    • 消息队列(RabbitMQ)
    • 消息队列(Kafka)
    • 消息队列(Cloud)
    • 分布式事务 Seata
    • 服务保障 Sentinel
  • 工作流手册

    • 工作流演示
    • 功能开启
    • 工作流(达梦适配)
    • 审批接入(流程表单)
    • 审批接入(业务表单)
    • 流程设计器(BPMN)
    • 流程设计器(钉钉、飞书)
    • 选择审批人、发起人自选
    • 会签、或签、依次审批
    • 流程发起、取消、重新发起
    • 审批通过、不通过、驳回
    • 审批加签、减签
    • 审批转办、委派、抄送
    • 执行监听器、任务监听器
    • 流程表达式
    • 流程审批通知
  • 大屏手册

    • 报表设计器
    • 大屏设计器
  • 支付手册

    • 功能开启
    • 支付宝支付接入
    • 微信公众号支付接入
    • 微信小程序支付接入
    • 支付宝、微信退款接入
    • 钱包充值、支付、退款
    • 模拟支付、退款
  • 会员手册

    • 功能开启
    • 微信公众号登录
    • 微信小程序登录
    • 微信小程序订阅消息
    • 微信小程序码
    • 会员用户、标签、分组
    • 会员等级、积分、签到
  • 商城手册

    • 商城演示
    • 功能开启
    • 商城装修
    • 在线客服
    • 【商品】商品分类
    • 【商品】商品属性
    • 【商品】商品 SPU 与 SKU
    • 【商品】商品评价
    • 【交易】购物车
    • 【交易】交易订单
    • 【交易】售后退款
    • 【交易】快递发货
    • 【交易】门店自提
    • 【交易】分销返佣
    • 【营销】优惠劵
    • 【营销】积分商城
    • 【营销】拼团活动
    • 【营销】秒杀活动
    • 【营销】砍价活动
    • 【营销】满减送活动
    • 【营销】限时折扣
    • 【营销】内容管理
    • 【统计】会员、商品、交易统计
  • ERP手册

    • ERP 演示
    • 功能开启
    • 【产品】产品信息、分类、单位
    • 【库存】产品库存、库存明细
    • 【库存】其它入库、其它出库
    • 【库存】库存调拨、库存盘点
    • 【采购】采购订单、入库、退货
    • 【销售】销售订单、出库、退货
    • 【财务】采购付款、销售收款
  • CRM手册

    • CRM 演示
    • 功能开启
    • 【线索】线索管理
    • 【客户】客户管理、公海客户
    • 【商机】商机管理、商机状态
    • 【合同】合同管理、合同提醒
    • 【回款】回款管理、回款计划
    • 【产品】产品管理、产品分类
    • 【通用】数据权限
    • 【通用】跟进记录、待办事项
  • AI大模型手册

    • AI 大模型演示
    • 功能开启
    • AI 聊天对话
    • AI 绘画创作
    • AI 知识库
    • AI 音乐创作
    • AI 写作助手
    • AI 思维导图
    • AI 工具(function calling)
    • AI 工作流
    • Dify 工作流
    • FastGPT 工作流
    • Coze 智能体
    • 【模型接入】OpenAI
    • 【模型接入】通义千问
    • 【模型接入】DeepSeek
    • 【模型接入】字节豆包
    • 【模型接入】腾讯混元
    • 【模型接入】硅基流动
    • 【模型接入】MiniMax
    • 【模型接入】月之暗灭
    • 【模型接入】百川智能
    • 【模型接入】文心一言
    • 【模型接入】LLAMA
    • 【模型接入】智谱 GLM
    • 【模型接入】讯飞星火
    • 【模型接入】微软 OpenAI
    • 【模型接入】谷歌 Gemini
    • 【模型接入】Stable Diffusion
    • 【模型接入】Midjourney
    • 【模型接入】Suno
  • IoT物联网手册

    • 功能开启
  • 公众号手册

    • 功能开启
    • 公众号接入
    • 公众号粉丝
    • 公众号标签
    • 公众号消息
    • 自动回复
    • 公众号菜单
    • 公众号素材
    • 公众号图文
    • 公众号统计
  • 系统手册

    • 短信配置
    • 邮件配置
    • 站内信配置
    • 数据脱敏
    • 敏感词
    • 地区 & IP 库
  • 运维手册

    • 开发环境
    • Linux 部署
    • Docker 部署
    • Jenkins 部署
    • 宝塔部署
    • HTTPS 证书
    • 服务监控
  • 前端手册 Vue 3.x

    • 开发规范
      • 0. 实战案例
        • 0.1 普通列表
        • 0.2 树形列表
        • 0.3 高性能列表
        • 0.4 详情弹窗
      • 1. view 页面
      • 2. api 请求
        • 2.1 请求封装
        • 2.2 交互流程
      • 3. component 组件
        • 3.1 全局组件
        • 3.2 模块内组件
      • 4. style 样式
      • 5. 项目规范
    • 菜单路由
    • Icon 图标
    • 字典数据
    • 系统组件
    • 通用方法
    • 配置读取
    • CRUD 组件
    • 国际化
    • IDE 调试
    • 代码格式化
  • 前端手册 Vue 2.x

    • 开发规范
    • 菜单路由
    • Icon 图标
    • 字典数据
    • 系统组件
    • 通用方法
    • 配置读取
  • 更新日志

    • 【v2.5.0】开发中...
    • 【v2.4.2】2025-04-12
    • 【v2.4.1】2025-02-09
    • 【v2.4.0】2024-12-31
    • 【v2.3.0】2024-10-07
    • 【v2.2.0】2024-08-02
    • 【v2.1.0】2024-05-05
    • 【v2.0.1】2024-03-01
    • 【v2.0.0】2024-01-26
    • 【v1.9.0】2023-12-01
  • 开发指南
  • 前端手册 Vue 3.x
芋道源码
2022-04-17
目录

开发规范

# 0. 实战案例

本小节,提供大家开发管理后台的功能时,最常用的普通列表、树形列表、新增与修改的表单弹窗、详情表单弹窗的实战案例。

# 0.1 普通列表

可参考 [系统管理 -> 岗位管理] 菜单:

  • API 接口:/src/api/system/post/index.ts (opens new window)
  • 列表界面:/src/views/system/post/index.vue (opens new window)
  • 表单界面:/src/views/system/post/PostForm.vue (opens new window)

为什么界面拆成列表和表单两个 Vue 文件?

每个 Vue 文件,只实现一个功能,更简洁,维护性更好,Git 代码冲突概率低。

# 0.2 树形列表

可参考 [系统管理 -> 部门管理] 菜单:

  • API 接口:/src/api/system/dept/index.ts (opens new window)
  • 列表界面:/src/views/system/dept/index.vue (opens new window)
  • 表单界面:/src/views/system/dept/DeptForm.vue (opens new window)

# 0.3 高性能列表

可参考 [系统管理 -> 地区管理] 菜单,对应 /src/views/system/area/index.vue (opens new window) 列表界面

基于 Virtualized Table 虚拟化表格 (opens new window) 实现,解决一屏里超过 1000 条数据记录时,就会出现卡顿等性能问题。

# 0.4 详情弹窗

可参考 [基础设施 -> API 日志 -> 访问日志] 菜单,对应 /src/views/infra/apiAccessLog/ApiAccessLogDetail.vue (opens new window) 详情弹窗

# 1. view 页面

在 @views (opens new window) 目录下,每个模块对应一个目录,它的所有功能的 .vue 都放在该目录里。

 目录

一般来说,一个路由对应一个 index.vue 文件。

# 2. api 请求

在 @/api (opens new window) 目录下,每个模块对应一个 index.ts API 文件。

 目录

  • API 方法:会调用 request 方法,发起对后端 RESTful API 的调用。
  • interface 类型:定义了 API 的请求参数和返回结果的类型,对应后端的 VO 类型。

# 2.1 请求封装

/src/config/axios/index.ts (opens new window) 基于 axios (opens new window) 封装,统一处理 GET、POST 方法的请求参数、请求头,以及错误提示信息等。

axios/index.ts

# 2.1.1 创建 axios 实例

  • baseURL 基础路径
  • timeout 超时时间,默认为 30000 毫秒
实现代码 /src/config/axios/service.ts
import axios from 'axios'

const { result_code, base_url, request_timeout } = config

// 创建 axios 实例
const service: AxiosInstance = axios.create({
    baseURL: base_url, // api 的 base_url
    timeout: request_timeout, // 请求超时时间
    withCredentials: false // 禁用 Cookie 等信息
})

# 2.1.2 Request 拦截器

  • 【重点】Authorization、tenant-id 请求头
  • GET 请求参数的拼接
实现代码 /src/config/axios/service.ts
import axios, {
    AxiosInstance,
    AxiosRequestHeaders,
    AxiosResponse,
    AxiosError,
    InternalAxiosRequestConfig
} from 'axios'

import { getAccessToken, getTenantId } from '@/utils/auth'

const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE

service.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
        // 是否需要设置 token
        let isToken = (config!.headers || {}).isToken === false
        whiteList.some((v) => {
            if (config.url) {
                config.url.indexOf(v) > -1
                return (isToken = false)
            }
        })
        if (getAccessToken() && !isToken) {
            (config as Recordable).headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
        }
        // 设置租户
        if (tenantEnable && tenantEnable === 'true') {
            const tenantId = getTenantId()
            if (tenantId) (config as Recordable).headers['tenant-id'] = tenantId
        }
        const params = config.params || {}
        const data = config.data || false
        if (
            config.method?.toUpperCase() === 'POST' &&
            (config.headers as AxiosRequestHeaders)['Content-Type'] ===
        'application/x-www-form-urlencoded'
    ) {
            config.data = qs.stringify(data)
        }
        // get参数编码
        if (config.method?.toUpperCase() === 'GET' && params) {
            let url = config.url + '?'
            for (const propName of Object.keys(params)) {
                const value = params[propName]
                if (value !== void 0 && value !== null && typeof value !== 'undefined') {
                    if (typeof value === 'object') {
                        for (const val of Object.keys(value)) {
                            const params = propName + '[' + val + ']'
                            const subPart = encodeURIComponent(params) + '='
                            url += subPart + encodeURIComponent(value[val]) + '&'
                        }
                    } else {
                        url += `${propName}=${encodeURIComponent(value)}&`
                    }
                }
            }
            // 给 get 请求加上时间戳参数,避免从缓存中拿数据
            // const now = new Date().getTime()
            // params = params.substring(0, url.length - 1) + `?_t=${now}`
            url = url.slice(0, -1)
            config.params = {}
            config.url = url
        }
        return config
    },
    (error: AxiosError) => {
        // Do something with request error
        console.log(error) // for debug
        Promise.reject(error)
    }
)

# 2.1.3 Response 拦截器

  • 访问令牌 AccessToken 过期时,使用刷新令牌 RefreshToken 刷新,获得新的访问令牌
  • 刷新令牌失败(过期)时,跳回首页进行登录
  • 请求失败,Message 错误提示
实现代码 /src/config/axios/service.ts
import axios, {
    AxiosInstance,
    AxiosRequestHeaders,
    AxiosResponse,
    AxiosError,
    InternalAxiosRequestConfig
} from 'axios'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'

import { getAccessToken, getRefreshToken, removeToken, setToken } from '@/utils/auth'

// 需要忽略的提示。忽略后,自动 Promise.reject('error')
const ignoreMsgs = [
    '无效的刷新令牌', // 刷新令牌被删除时,不用提示
    '刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
]
// 是否显示重新登录
export const isRelogin = { show: false }
import errorCode from './errorCode'

import { resetRouter } from '@/router'
import { useCache } from '@/hooks/web/useCache'

service.interceptors.response.use(
    async (response: AxiosResponse<any>) => {
        const { data } = response
        const config = response.config
        if (!data) {
            // 返回“[HTTP]请求没有返回值”;
            throw new Error()
        }
        const { t } = useI18n()
        // 未设置状态码则默认成功状态
        const code = data.code || result_code
        // 二进制数据则直接返回
        if (
            response.request.responseType === 'blob' ||
            response.request.responseType === 'arraybuffer'
        ) {
            return response.data
        }
        // 获取错误信息
        const msg = data.msg || errorCode[code] || errorCode['default']
        if (ignoreMsgs.indexOf(msg) !== -1) {
            // 如果是忽略的错误码,直接返回 msg 异常
            return Promise.reject(msg)
        } else if (code === 401) {
            // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
            if (!isRefreshToken) {
                isRefreshToken = true
                // 1. 如果获取不到刷新令牌,则只能执行登出操作
                if (!getRefreshToken()) {
                    return handleAuthorized()
                }
                // 2. 进行刷新访问令牌
                try {
                    const refreshTokenRes = await refreshToken()
                    // 2.1 刷新成功,则回放队列的请求 + 当前请求
                    setToken((await refreshTokenRes).data.data)
                    config.headers!.Authorization = 'Bearer ' + getAccessToken()
                    requestList.forEach((cb: any) => {
                        cb()
                    })
                    requestList = []
                    return service(config)
                } catch (e) {
                    // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
                    // 2.2 刷新失败,只回放队列的请求
                    requestList.forEach((cb: any) => {
                        cb()
                    })
                    // 提示是否要登出。即不回放当前请求!不然会形成递归
                    return handleAuthorized()
                } finally {
                    requestList = []
                    isRefreshToken = false
                }
            } else {
                // 添加到队列,等待刷新获取到新的令牌
                return new Promise((resolve) => {
                    requestList.push(() => {
                        config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
                        resolve(service(config))
                    })
                })
            }
        } else if (code === 500) {
            ElMessage.error(t('sys.api.errMsg500'))
            return Promise.reject(new Error(msg))
        } else if (code === 901) {
            ElMessage.error({
                offset: 300,
                dangerouslyUseHTMLString: true,
                message:
                    '<div>' +
                    t('sys.api.errMsg901') +
                    '</div>' +
                    '<div> &nbsp; </div>' +
                    '<div>参考 https://doc.iocoder.cn/ 教程</div>' +
                    '<div> &nbsp; </div>' +
                    '<div>5 分钟搭建本地环境</div>'
            })
            return Promise.reject(new Error(msg))
        } else if (code !== 200) {
            if (msg === '无效的刷新令牌') {
                // hard coding:忽略这个提示,直接登出
                console.log(msg)
            } else {
                ElNotification.error({ title: msg })
            }
            return Promise.reject('error')
        } else {
            return data
        }
    },
    (error: AxiosError) => {
        console.log('err' + error) // for debug
        let { message } = error
        const { t } = useI18n()
        if (message === 'Network Error') {
            message = t('sys.api.errorMessage')
        } else if (message.includes('timeout')) {
            message = t('sys.api.apiTimeoutMessage')
        } else if (message.includes('Request failed with status code')) {
            message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3)
        }
        ElMessage.error(message)
        return Promise.reject(error)
    }
)

const refreshToken = async () => {
    axios.defaults.headers.common['tenant-id'] = getTenantId()
    return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
}
const handleAuthorized = () => {
    const { t } = useI18n()
    if (!isRelogin.show) {
        isRelogin.show = true
        ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
            confirmButtonText: t('login.relogin'),
            cancelButtonText: t('common.cancel'),
            type: 'warning'
        })
            .then(() => {
                const { wsCache } = useCache()
                resetRouter() // 重置静态路由表
                wsCache.clear()
                removeToken()
                isRelogin.show = false
                window.location.href = '/'
            })
            .catch(() => {
                isRelogin.show = false
            })
    }
    return Promise.reject(t('sys.api.timeoutMessage'))
}

# 2.2 交互流程

一个完整的前端 UI 交互到服务端处理流程,如下图所示:

交互流程

继续以 [系统管理 -> 岗位管理] 菜单为例,查看它是如何读取岗位列表的。代码如下:

// ① api/system/post/index.ts
import request from '@/config/axios'

// 查询岗位列表
export const getPostPage = async (params: PageParam) => {
  return await request.get({ url: '/system/post/page', params })
}

// ② views/system/post/index.vue
<script setup lang="tsx">
const loading = ref(true) // 列表的加载中
const total = ref(0) // 列表的总页数
const list = ref([]) // 列表的数据
const queryParams = reactive({
    pageNo: 1,
    pageSize: 10,
    code: '',
    name: '',
    status: undefined
})

/** 查询岗位列表 */
const getList = async () => {
    loading.value = true
    try {
        const data = await PostApi.getPostPage(queryParams)
        list.value = data.list
        total.value = data.total
    } finally {
        loading.value = false
    }
}
</script>

# 3. component 组件

# 3.1 全局组件

在 @/components (opens new window) 目录下,实现全局组件,被所有模块所公用。

例如说,富文本编辑器、各种各搜索组件、封装的分页组件等等。

全局组件

# 3.2 模块内组件

每个模块的业务组件,可实现在 views 目录下,自己模块的目录的 components 目录下,避免单个 .vue 文件过大,降低维护成功。

例如说,@/views/pay/app/components/xxx.vue:

业务组件

# 4. style 样式

① 在 @/styles (opens new window) 目录下,实现全局样式,被所有页面所公用。

全局样式

② 每个 .vue 页面,可在 <style /> 标签中添加样式,注意需要添加 scoped 表示只作用在当前页面里,避免造成全局的样式污染。

业务样式

更多也可以看看如下两篇文档:

  • 《vue-element-plus-admin —— 项目配置「样式配置」》 (opens new window)
  • 《vue-element-plus-admin —— 样式》 (opens new window)

# 5. 项目规范

可参考 《vue-element-plus-admin —— 项目规范》 (opens new window) 文档。

服务监控
菜单路由

← 服务监控 菜单路由→

Theme by Vdoing | Copyright © 2019-2025 芋道源码 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×