Skip to content

业务组件总览

介绍

RuoYi-Plus-UniApp 前端管理端提供了一套完整的业务组件库,涵盖表单输入、数据展示、弹窗交互、图表可视化、AI 辅助等多个领域。这些组件基于 Element Plus 进行二次封装,遵循统一的设计规范和 API 约定。

核心特性:

  • 高度模块化 - 70+ 个业务组件,按功能分类清晰
  • 统一 API 设计 - 所有组件遵循一致的属性命名和事件处理规范
  • 响应式布局 - 内置智能响应式系统,支持多种屏幕尺寸自适应
  • 国际化支持 - 全面集成 i18n,所有文本支持多语言切换
  • TypeScript 类型 - 完整的类型定义,提供优秀的开发体验
  • 权限控制 - 与 v-permi 指令无缝集成

组件分类

1. 表单组件 (13个)

组件名称组件标识主要功能
文本输入框AFormInput单行/多行文本输入
下拉选择器AFormSelect单选/多选下拉框
复选框组AFormCheckbox复选框选择
单选框组AFormRadio单选框选择
日期选择器AFormDate日期/日期范围选择
开关AFormSwitch布尔值切换
树形选择器AFormTreeSelect树形结构选择
级联选择器AFormCascader级联数据选择
文件上传AFormFileUpload文件上传
图片上传AFormImgUpload图片上传
富文本编辑器AFormEditor富文本编辑
地图选择器AFormMap地理位置选择
AI辅助输入框AFormInputWithAiAI增强输入

2. 搜索和表格组件 (3个)

组件名称组件标识主要功能
搜索表单ASearchForm搜索条件容器
表格工具栏TableToolbar表格操作工具栏
分页器Pagination数据分页

3. 弹窗组件 (2个)

组件名称组件标识主要功能
通用弹窗AModal对话框/抽屉容器
详情弹窗ADetail数据详情展示

4. 业务选择器 (2个)

组件名称组件标识主要功能
用户选择器UserSelect用户选择
字典标签DictTag字典值显示

5. 卡片组件 (23个)

类别组件数量典型组件
统计卡片3AStatsCard, ABarStatsCard, ALineStatsCard
图表卡片5ABarChartCard, ALineChartCard, APieChartCard
内容卡片4AFormCard, ATableCard, AImageCard, AInfoCard
业务卡片4AUserCard, AProfileCard, APricingCard, ASocialCard
列表卡片5ADataListCard, ATimelineListCard, AActivityCard
特殊卡片2ADataCard, AEmptyCard

6. 图表组件 (10个)

组件名称图表类型使用场景
AChart通用图表ECharts 基础封装
ALineChart折线图趋势分析
ABarChart竖向柱状图数据对比
ABarHorizontalChart横向柱状图排名展示
ABarBidirectionalChart双向柱状图对比分析
APieChart饼图/环形图占比展示
ARadarChart雷达图多维度评估
AScatterChart散点图分布关系
ACandlestickChartK线图股票数据
AMapChart地图地理分布

7. AI 组件 (4个)

组件名称功能
AAiAssistantAI 助手面板
AAiTextOptimizer文本优化工具
AAiContentReviewer内容审查工具
AAiDataGenerator数据生成工具

8. 其他组件 (13个)

组件名称功能
AImportExcelExcel 导入
AOssMediaManager媒体管理器
AThemeColorPicker主题配色选择器
AThemeSvgSVG 渲染
AGeometricBackground几何背景
AWatermark水印
Icon图标渲染
IconSelect图标选择器
ImagePreview图片预览
ASelectionTags选中标签
AResizablePanels可拖拽面板
IFrameContainerIFrame 容器
ARecharge充值组件

核心功能详解

响应式布局系统

所有表单组件都支持响应式布局,通过 span 属性实现。

vue
<template>
  <!-- 固定 span -->
  <AFormInput label="用户名" v-model="form.userName" :span="12" />

  <!-- 响应式对象 -->
  <AFormInput
    label="邮箱"
    v-model="form.email"
    :span="{ xs: 24, sm: 24, md: 12, lg: 8, xl: 6 }"
  />

  <!-- 预设响应式 -->
  <AFormInput label="手机" v-model="form.phone" span="auto" />
</template>

响应式模式:

模式说明场景
screen基于屏幕尺寸默认,页面级表单
container基于容器尺寸弹窗内表单
modal-size基于 AModal size弹窗场景推荐

智能数据类型转换

AFormSelect 组件具备智能的数据类型检测和转换功能。

vue
<script setup lang="ts">
const form = ref({
  roleIds: '1,2,3' // 字符串输入
})

// 组件自动转换为数组 [1, 2, 3] 用于内部选择
// 确认后自动转回字符串 "1,2,3" 保持类型一致
</script>

类型检测规则:

  • 如果数组中有任何字符串元素,输出字符串数组
  • 如果全是数字且在安全整数范围内,输出数字数组
  • 如果数字超过 15 位,自动转为字符串防止精度丢失

选项禁用条件配置

AFormSelect 支持灵活的选项禁用配置。

vue
<template>
  <!-- 默认禁用 (status = '0') -->
  <AFormSelect v-model="form.postIds" :options="postOptions" :multiple="true" />

  <!-- 自定义禁用字段 -->
  <AFormSelect
    v-model="form.roleId"
    :options="roleList"
    disabled-field="isActive"
    :disabled-value="false"
  />

  <!-- 函数判断 -->
  <AFormSelect
    v-model="form.goodsId"
    :options="productList"
    :disabled-value="(item) => item.status === '0' || item.stock < 10"
  />
</template>

表单组件详解

AFormInput - 文本输入框

vue
<template>
  <el-form :model="form">
    <AFormInput label="用户名" v-model="form.userName" prop="userName" :span="12" />
    <AFormInput label="备注" v-model="form.remark" type="textarea" :maxlength="200" :rows="4" />
    <AFormInput label="年龄" v-model="form.age" type="number" :min="0" :max="150" :span="8" />
    <AFormInput label="密码" v-model="form.password" type="password" show-password prevent-autofill />
  </el-form>
</template>

Props 属性

参数说明类型默认值
modelValue绑定值string | number-
label标签文本string-
prop表单域字段名string-
type输入框类型'text' | 'textarea' | 'number' | 'password''text'
span栅格占据列数number | SpanType-
maxlength最大长度number255
showWordLimit显示字数统计booleantrue
showPassword显示密码可见性切换booleanfalse
preventAutofill防止自动填充booleanfalse
clearable显示清除按钮booleantrue
disabled是否禁用booleanfalse
responsiveMode响应式模式'screen' | 'container' | 'modal-size''screen'

Events 事件

事件名说明回调参数
update:modelValue值变化时触发(value: string | number) => void
input输入时触发(value: string | number) => void
change值改变时触发(value: string | number) => void
blur失去焦点时触发(event: FocusEvent) => void
enter按下回车键时触发(value: string | number) => void

Slots 插槽

插槽名说明
prepend输入框前置内容
append输入框后置内容
prefix输入框头部图标
suffix输入框尾部图标

AFormSelect - 下拉选择器

vue
<template>
  <el-form :model="form">
    <AFormSelect label="类型" v-model="form.type" :options="sys_enable_status" :span="12" />
    <AFormSelect
      label="角色"
      v-model="form.roleIds"
      :options="roleList"
      value-field="roleId"
      label-field="roleName"
      :multiple="true"
    />
    <AFormSelect label="代码" v-model="form.code" :options="codeList" :show-value="true" />
  </el-form>
</template>

<script setup lang="ts">
const { sys_enable_status } = useDict(DictTypes.sys_enable_status)
</script>

Props 属性

参数说明类型默认值
modelValue绑定值string | number | Array-
label标签文本string-
options选项数据any[][]
span栅格占据列数number | SpanType-
multiple是否多选booleanfalse
filterable是否可搜索booleantrue
clearable是否可清空booleantrue
valueFieldvalue 字段名string'value'
labelFieldlabel 字段名string'label'
disabledField禁用判断字段名string'status'
disabledValue禁用条件值DisabledCondition'0'
showValue是否显示选项值boolean-
multipleLimit多选最多可选项数number0
collapseTags多选是否折叠标签booleanfalse

AFormDate - 日期选择器

vue
<template>
  <el-form :model="form">
    <AFormDate label="出生日期" v-model="form.birthday" :span="12" />
    <AFormDate label="创建时间" v-model="form.dateRange" type="daterange" :span="12" />
    <AFormDate label="预约时间" v-model="form.appointmentTime" type="datetime" />
  </el-form>
</template>

AFormTreeSelect - 树形选择器

vue
<template>
  <AFormTreeSelect
    label="所属部门"
    v-model="form.deptId"
    :options="deptTree"
    :props="{ label: 'deptName', value: 'deptId', children: 'children' }"
    :span="12"
  />
</template>

AFormUpload - 文件上传

vue
<template>
  <el-form :model="form">
    <AFormFileUpload label="附件" v-model="form.fileList" :limit="5" :accept="'.pdf,.doc,.docx'" />
    <AFormImgUpload label="头像" v-model="form.avatar" :limit="1" :max-size="2" />
  </el-form>
</template>

搜索和表格组件

ASearchForm - 搜索表单

vue
<template>
  <ASearchForm v-model="queryParams" title="搜索条件">
    <AFormInput label="用户名" prop="userName" v-model="queryParams.userName" />
    <AFormSelect label="状态" prop="status" v-model="queryParams.status" :options="statusOptions" />
    <AFormDate label="创建时间" v-model="queryParams.dateRange" type="daterange" />
  </ASearchForm>
</template>

Props 属性

参数说明类型默认值
modelValue表单数据模型Record<string, any>{}
visible显示/隐藏booleantrue
inline是否行内表单booleantrue
labelWidth标签宽度string'auto'
title卡片标题string-
collapsible是否可展开收起booleantrue
defaultExpanded默认是否展开booleanfalse

TableToolbar - 表格工具栏

vue
<template>
  <TableToolbar
    :columns="tableColumns"
    :show-search="showSearch"
    @resetQuery="resetQuery"
    @queryTable="getList"
  >
    <template #left>
      <el-button type="primary" @click="handleAdd">新增</el-button>
    </template>
  </TableToolbar>
</template>

Pagination - 分页器

vue
<template>
  <Pagination
    v-model:page="queryParams.pageNum"
    v-model:limit="queryParams.pageSize"
    :total="total"
    @pagination="getList"
  />
</template>

弹窗组件详解

AModal - 通用弹窗

支持对话框和抽屉两种模式。

vue
<template>
  <!-- 对话框模式 -->
  <AModal v-model="dialogVisible" title="新增用户" @confirm="handleSubmit">
    <el-form :model="form">
      <AFormInput label="用户名" v-model="form.userName" />
    </el-form>
  </AModal>

  <!-- 抽屉模式 -->
  <AModal v-model="drawerVisible" title="用户详情" mode="drawer" direction="rtl" :show-footer="false">
    <UserDetail :user="selectedUser" />
  </AModal>
</template>

Props 属性

参数说明类型默认值
modelValue显示/隐藏状态booleanfalse
mode模式'dialog' | 'drawer''dialog'
title标题string-
size预设尺寸'small' | 'medium' | 'large' | 'xl''medium'
width自定义宽度string | number-
fullscreen是否全屏booleanfalse
movable是否可拖动booleanfalse
direction抽屉方向'ltr' | 'rtl' | 'ttb' | 'btt''rtl'
showFooter显示底部booleantrue
footerType底部按钮类型'default' | 'close-only''default'
loading加载状态booleanfalse
destroyOnClose关闭时销毁booleantrue

Events 事件

事件名说明回调参数
update:modelValue状态变化(value: boolean) => void
confirm确认按钮点击() => void
cancel取消按钮点击() => void
open开始打开() => void
opened完全打开() => void
close开始关闭() => void
closed完全关闭() => void

尺寸配置

typescript
const sizeMap = {
  small: { dialog: '600px', drawer: '600px' },
  medium: { dialog: '800px', drawer: '800px' },
  large: { dialog: '1000px', drawer: '1000px' },
  xl: { dialog: '1200px', drawer: '1200px' }
}

ADetail - 详情弹窗

vue
<template>
  <ADetail
    v-model="detailVisible"
    title="用户详情"
    :data="detailData"
    :fields="detailFields"
    mode="drawer"
  />
</template>

<script setup lang="ts">
const detailFields = ref([
  { label: '用户ID', prop: 'userId', type: 'text' },
  { label: '用户名', prop: 'userName', type: 'copyable' },
  { label: '密码', prop: 'password', type: 'password' },
  { label: '头像', prop: 'avatar', type: 'image' },
  { label: '状态', prop: 'status', type: 'dict', dictType: 'sys_enable_status' },
  { label: '创建时间', prop: 'createTime', type: 'datetime' }
])
</script>

字段类型

类型说明
text普通文本
password密码(可切换显示)
copyable可复制文本
dict字典标签
image图片预览
htmlHTML 内容
file文件链接
date日期格式化
datetime日期时间格式化
currency货币格式化
boolean是/否
array数组显示

业务选择器详解

UserSelect - 用户选择器

vue
<template>
  <UserSelect v-model="selectedUsers" :multiple="true" show-inline-tags />
  <UserSelect v-model="selectedUser" :multiple="false" />
  <UserSelect
    v-model="userIds"
    :initial-user-names="userNamesString"
    :multiple="true"
    button-text="选择成员"
  />
</template>

Props 属性

参数说明类型默认值
modelValue绑定值string | number | SysUserVo | Array-
multiple是否多选booleanfalse
userIds限制用户 ID 范围string | number | Array-
defaultReturnType默认返回类型'object' | 'id''object'
showInlineTags显示内置标签booleanfalse
buttonText按钮文本string'选择用户'
buttonType按钮类型string'primary'
disabled是否禁用booleanfalse
initialUserNames初始用户名string | string[]-

智能返回类型

typescript
// 传入用户对象,返回用户对象
const user1 = ref<SysUserVo>({ userId: '1', userName: 'admin' })

// 传入用户 ID,返回用户 ID
const user2 = ref('1')

// 传入空值,根据 defaultReturnType 决定
const user3 = ref(null)

DictTag - 字典标签

vue
<template>
  <!-- dict 模式 -->
  <DictTag :options="sys_enable_status" :value="user.status" />

  <!-- region 模式 -->
  <DictTag mode="region" :value="user.regionCode" />

  <!-- cascader 模式 -->
  <DictTag mode="cascader" :value="user.categoryId" :cascader-data="categoryTree" />
</template>

Props 属性

参数说明类型默认值
mode模式'dict' | 'region' | 'cascader''dict'
options字典选项any[][]
valuestring | number-
cascaderData级联数据any[][]
valueFieldvalue 字段名string'value'
labelFieldlabel 字段名string'label'

图表组件

基本用法

vue
<template>
  <!-- 通用图表 -->
  <AChart :option="chartOption" height="400px" />

  <!-- 折线图 -->
  <ALineChart title="访问量趋势" :xData="xData" :series="series" height="300px" />

  <!-- 柱状图 -->
  <ABarChart title="产品销量" :xData="products" :series="sales" height="350px" />

  <!-- 饼图 -->
  <APieChart title="流量来源" :data="trafficData" height="400px" />
</template>

<script setup lang="ts">
const xData = ref(['1月', '2月', '3月', '4月', '5月', '6月'])
const series = ref([
  { name: 'PV', data: [120, 132, 101, 134, 90, 230] },
  { name: 'UV', data: [45, 62, 48, 71, 53, 98] }
])

const trafficData = ref([
  { name: '直接访问', value: 335 },
  { name: '搜索引擎', value: 310 },
  { name: '外部链接', value: 234 }
])
</script>

最佳实践

1. 列表页面开发流程

vue
<template>
  <div class="app-container">
    <!-- 1. 搜索表单 -->
    <ASearchForm v-model="queryParams">
      <AFormInput label="用户名" prop="userName" v-model="queryParams.userName" />
      <AFormSelect label="状态" prop="status" v-model="queryParams.status" :options="sys_enable_status" />
    </ASearchForm>

    <!-- 2. 表格工具栏 -->
    <TableToolbar :columns="tableColumns" @queryTable="getList">
      <template #left>
        <el-button type="primary" @click="handleAdd">新增</el-button>
      </template>
    </TableToolbar>

    <!-- 3. 数据表格 -->
    <el-table :data="tableData" v-loading="loading">
      <el-table-column label="用户ID" prop="userId" />
      <el-table-column label="状态" prop="status">
        <template #default="{ row }">
          <DictTag :options="sys_enable_status" :value="row.status" />
        </template>
      </el-table-column>
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 4. 分页 -->
    <Pagination v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />

    <!-- 5. 编辑弹窗 -->
    <AModal v-model="editVisible" :title="editTitle" @confirm="handleSubmit">
      <el-form :model="form" :rules="rules" ref="formRef">
        <AFormInput label="用户名" prop="userName" v-model="form.userName" :span="12" />
      </el-form>
    </AModal>
  </div>
</template>

<script setup lang="ts">
import { pageUsers, addUser, updateUser } from '@/api/system/core/user/userApi'

const { sys_enable_status } = useDict(DictTypes.sys_enable_status)

const queryParams = ref({ pageNum: 1, pageSize: 10, userName: '', status: '' })
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const editVisible = ref(false)
const form = ref({})

const getList = async () => {
  loading.value = true
  const [err, data] = await pageUsers(queryParams.value)
  if (!err) {
    tableData.value = data.records || []
    total.value = data.total
  }
  loading.value = false
}

const handleSubmit = async () => {
  const api = form.value.userId ? updateUser : addUser
  const [err] = await api(form.value)
  if (!err) {
    ElMessage.success('操作成功')
    editVisible.value = false
    getList()
  }
}

onMounted(() => getList())
</script>

2. 表单验证集成

vue
<script setup lang="ts">
const rules = {
  userName: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
  ],
  email: [
    { required: true, message: '请输入邮箱', trigger: 'blur' },
    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
  ],
  phone: [
    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
  ]
}
</script>

3. 权限控制集成

vue
<template>
  <el-button v-permi="['system:user:add']" type="primary" @click="handleAdd">新增</el-button>
  <el-button v-permi="['system:user:edit']" type="primary" link @click="handleEdit(row)">编辑</el-button>
  <el-button v-permi="['system:user:delete']" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>

4. 响应式布局最佳实践

vue
<template>
  <!-- 弹窗内使用 modal-size 模式 -->
  <AModal v-model="visible" size="large">
    <el-form>
      <AFormInput label="标题" v-model="form.title" :span="12" responsiveMode="modal-size" :modalSize="'large'" />
    </el-form>
  </AModal>

  <!-- 页面中使用 screen 模式 -->
  <el-form>
    <AFormInput label="用户名" v-model="form.userName" :span="{ xs: 24, sm: 24, md: 12, lg: 8, xl: 6 }" />
  </el-form>
</template>

常见问题

1. AFormSelect 多选时类型不一致?

解决方案:

typescript
// 方案1: 让组件自动处理类型转换
const form = ref({
  roleIds: '1,2,3' // 字符串输入,组件自动转换
})

// 方案2: 统一使用数组格式
const form = ref({
  roleIds: [1, 2, 3] // 数组输入
})

2. AFormInput 防自动填充不生效?

解决方案:

vue
<template>
  <AFormInput label="密码" v-model="form.password" type="password" show-password prevent-autofill />
</template>

<!-- 如果仍然不生效,可以在表单外层添加 -->
<el-form autocomplete="new-password">

3. UserSelect 返回类型如何控制?

解决方案:

typescript
// 方式1: 通过 v-model 的初始值类型自动推断
const userId = ref('')           // 返回字符串 ID
const user = ref<SysUserVo>()    // 返回用户对象
const userIds = ref<string[]>([]) // 返回 ID 数组

// 方式2: 使用 defaultReturnType 指定(当 v-model 为空时)
// <UserSelect v-model="emptyValue" defaultReturnType="id" />

4. ASearchForm 展开/收起按钮不显示?

原因: 表单项少于 2 行时不显示

解决方案: 确保表单项足够多(至少能形成 2 行),或检查 collapsible 属性是否为 true


5. AModal 弹窗内容闪烁或初始化不正确

问题描述

打开 AModal 弹窗时,内容出现闪烁,或者表单数据显示上一次的旧值而不是新值。

vue
<!-- 点击编辑时,弹窗中显示的是上一次编辑的数据 -->
<template>
  <AModal v-model="visible" title="编辑用户">
    <el-form :model="form">
      <AFormInput label="用户名" v-model="form.userName" />
    </el-form>
  </AModal>
</template>

问题原因

  • destroyOnClose 默认为 true,但表单数据没有在打开时正确初始化
  • 异步数据加载和弹窗渲染时序问题
  • v-model 的响应式更新延迟

解决方案

在打开弹窗时正确初始化表单数据:

vue
<!-- ❌ 错误:直接赋值对象引用 -->
<script setup lang="ts">
const form = ref({})

const handleEdit = (row: UserVo) => {
  form.value = row // 错误:直接引用会导致修改影响原数据
  visible.value = true
}
</script>

<!-- ✅ 正确:深拷贝数据并等待DOM更新 -->
<script setup lang="ts">
import { cloneDeep } from 'lodash-es'

const form = ref({})
const formRef = ref()

const handleEdit = async (row: UserVo) => {
  // 深拷贝数据
  form.value = cloneDeep(row)
  visible.value = true

  // 等待弹窗渲染完成后清除表单验证状态
  await nextTick()
  formRef.value?.clearValidate()
}

const handleAdd = async () => {
  // 重置表单为初始状态
  form.value = {
    userName: '',
    email: '',
    phone: '',
    status: '0'
  }
  visible.value = true

  await nextTick()
  formRef.value?.clearValidate()
}
</script>

使用 @opened 事件确保数据加载时机正确:

vue
<template>
  <AModal
    v-model="visible"
    title="编辑用户"
    @opened="handleOpened"
    @closed="handleClosed"
  >
    <el-form :model="form" ref="formRef" v-loading="loading">
      <AFormInput label="用户名" v-model="form.userName" />
    </el-form>
  </AModal>
</template>

<script setup lang="ts">
const loading = ref(false)
const currentId = ref<string>('')

const openEdit = (id: string) => {
  currentId.value = id
  visible.value = true
}

// 弹窗完全打开后加载数据
const handleOpened = async () => {
  if (currentId.value) {
    loading.value = true
    const [err, data] = await getUserDetail(currentId.value)
    if (!err) {
      form.value = data
    }
    loading.value = false
  }
}

// 弹窗关闭后清理状态
const handleClosed = () => {
  currentId.value = ''
  form.value = {}
  formRef.value?.resetFields()
}
</script>

6. AFormTreeSelect 数据回显不正确

问题描述

使用 AFormTreeSelect 时,编辑模式下已选中的值无法正确显示,显示的是ID而不是对应的标签。

vue
<!-- 编辑用户时,部门显示的是 "102" 而不是 "研发部" -->
<template>
  <AFormTreeSelect
    label="所属部门"
    v-model="form.deptId"
    :options="deptTree"
  />
</template>

问题原因

  • 树形数据和表单值的加载时序问题:表单值先于树形数据加载完成
  • 树形数据的节点配置 props 不正确
  • 数据类型不匹配(字符串 vs 数字)

解决方案

确保树形数据先于表单数据加载:

vue
<script setup lang="ts">
const deptTree = ref([])
const form = ref({})
const loading = ref(false)

// 方案1: 页面初始化时优先加载树形数据
onMounted(async () => {
  // 先加载树形数据
  const [err, data] = await getDeptTree()
  if (!err) {
    deptTree.value = data
  }
})

const handleEdit = async (row: UserVo) => {
  // 确保树形数据已加载
  if (deptTree.value.length === 0) {
    const [err, data] = await getDeptTree()
    if (!err) {
      deptTree.value = data
    }
  }

  // 然后设置表单数据
  form.value = cloneDeep(row)
  visible.value = true
}

// 方案2: 使用 watch 监听数据变化
watch(
  () => [deptTree.value, form.value.deptId],
  ([tree, deptId]) => {
    if (tree.length > 0 && deptId) {
      // 数据都准备好了,可以正确回显
      console.log('树形数据和表单值都已就绪')
    }
  }
)
</script>

确保 props 配置正确:

vue
<template>
  <AFormTreeSelect
    label="所属部门"
    v-model="form.deptId"
    :options="deptTree"
    :props="{
      value: 'deptId',
      label: 'deptName',
      children: 'children',
      disabled: 'disabled'
    }"
    check-strictly
    value-key="deptId"
  />
</template>

处理数据类型不匹配问题:

typescript
// 后端返回的 deptId 可能是数字类型
// 但树形数据的 deptId 是字符串类型

// 方案1: 统一转换为字符串
const handleEdit = (row: UserVo) => {
  form.value = {
    ...cloneDeep(row),
    deptId: String(row.deptId) // 统一转换
  }
  visible.value = true
}

// 方案2: 在树形数据中统一类型
const normalizeTree = (tree: any[]): any[] => {
  return tree.map(node => ({
    ...node,
    deptId: String(node.deptId),
    children: node.children ? normalizeTree(node.children) : undefined
  }))
}

const loadDeptTree = async () => {
  const [err, data] = await getDeptTree()
  if (!err) {
    deptTree.value = normalizeTree(data)
  }
}

7. DictTag 在表格中性能问题

问题描述

表格中大量使用 DictTag 组件时,页面渲染缓慢,滚动卡顿。

vue
<!-- 表格有500行,每行3个DictTag,总共1500个组件实例 -->
<template>
  <el-table :data="tableData">
    <el-table-column label="性别">
      <template #default="{ row }">
        <DictTag :options="sys_user_sex" :value="row.sex" />
      </template>
    </el-table-column>
    <el-table-column label="状态">
      <template #default="{ row }">
        <DictTag :options="sys_enable_status" :value="row.status" />
      </template>
    </el-table-column>
    <el-table-column label="类型">
      <template #default="{ row }">
        <DictTag :options="sys_user_type" :value="row.userType" />
      </template>
    </el-table-column>
  </el-table>
</template>

问题原因

  • 每个 DictTag 都是独立的组件实例,大量实例导致内存和渲染开销
  • 每次渲染都会遍历 options 数组查找匹配项
  • 表格虚拟滚动未启用

解决方案

方案一:使用函数式渲染代替组件:

vue
<template>
  <el-table :data="tableData">
    <el-table-column label="状态">
      <template #default="{ row }">
        <!-- 直接使用函数渲染,避免组件实例开销 -->
        <el-tag :type="getStatusTag(row.status).type">
          {{ getStatusTag(row.status).label }}
        </el-tag>
      </template>
    </el-table-column>
  </el-table>
</template>

<script setup lang="ts">
import { useDictMap } from '@/composables/use-dict'

// 预先构建字典Map,O(1)查找
const statusMap = useDictMap('sys_enable_status')

const getStatusTag = (value: string) => {
  return statusMap.value.get(value) || { label: value, type: 'info' }
}
</script>

方案二:创建轻量级的字典标签:

typescript
// composables/use-dict-tag.ts
export function useDictTag(dictType: string) {
  const { proxy } = getCurrentInstance()!
  const dict = proxy.$dict[dictType] || []

  // 构建Map缓存
  const dictMap = new Map<string, { label: string; tagType: string }>()

  dict.forEach((item: any) => {
    dictMap.set(String(item.value), {
      label: item.label,
      tagType: item.tagType || 'info'
    })
  })

  const getLabel = (value: string | number) => {
    const item = dictMap.get(String(value))
    return item?.label || String(value)
  }

  const getTagType = (value: string | number) => {
    const item = dictMap.get(String(value))
    return item?.tagType || 'info'
  }

  return {
    getLabel,
    getTagType,
    dictMap
  }
}

方案三:启用表格虚拟滚动:

vue
<template>
  <el-table-v2
    :columns="columns"
    :data="tableData"
    :width="tableWidth"
    :height="500"
    fixed
  />
</template>

<script setup lang="ts">
import { TableV2FixedDir } from 'element-plus'

const columns = [
  {
    key: 'status',
    title: '状态',
    dataKey: 'status',
    width: 100,
    cellRenderer: ({ cellData }) => {
      const tag = getStatusTag(cellData)
      return h(ElTag, { type: tag.type }, () => tag.label)
    }
  }
]
</script>

8. AFormEditor 富文本编辑器图片上传失败

问题描述

AFormEditor 中插入图片时,上传失败或图片无法显示。

vue
<template>
  <AFormEditor
    label="文章内容"
    v-model="form.content"
    :span="24"
  />
</template>

问题原因

  • 上传接口配置不正确或未配置
  • 图片格式或大小不符合要求
  • 跨域问题导致上传失败
  • Token 认证问题

解决方案

配置正确的上传选项:

vue
<template>
  <AFormEditor
    label="文章内容"
    v-model="form.content"
    :upload-options="uploadOptions"
    :span="24"
  />
</template>

<script setup lang="ts">
import { getToken } from '@/utils/auth'

const uploadOptions = {
  // 上传地址
  action: import.meta.env.VITE_API_URL + '/resource/oss/upload',

  // 请求头(包含认证信息)
  headers: {
    Authorization: 'Bearer ' + getToken()
  },

  // 文件大小限制(单位:MB)
  maxSize: 5,

  // 允许的文件类型
  accept: 'image/jpeg,image/png,image/gif,image/webp',

  // 上传成功回调
  onSuccess: (response: any, file: File) => {
    if (response.code === 200) {
      return response.data.url
    }
    ElMessage.error('上传失败:' + response.msg)
    return null
  },

  // 上传失败回调
  onError: (error: Error) => {
    console.error('上传错误:', error)
    ElMessage.error('图片上传失败,请重试')
  },

  // 上传前验证
  beforeUpload: (file: File) => {
    const isValidType = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(file.type)
    const isValidSize = file.size / 1024 / 1024 < 5

    if (!isValidType) {
      ElMessage.error('只支持 JPG/PNG/GIF/WebP 格式的图片')
      return false
    }

    if (!isValidSize) {
      ElMessage.error('图片大小不能超过 5MB')
      return false
    }

    return true
  }
}
</script>

处理跨域问题(需后端配合):

typescript
// 如果使用代理,确保 vite.config.ts 配置正确
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: path => path.replace(/^\/api/, '')
      }
    }
  }
})

使用 Base64 方式(不推荐用于大图):

vue
<template>
  <AFormEditor
    label="文章内容"
    v-model="form.content"
    :upload-options="{ mode: 'base64' }"
    :span="24"
  />
</template>

9. 表单组件在动态切换时状态残留

问题描述

使用 v-if 动态切换表单组件时,组件内部状态残留,导致数据显示错误。

vue
<!-- 切换表单类型时,输入框的值没有清除 -->
<template>
  <div>
    <el-radio-group v-model="formType">
      <el-radio-button label="user">用户表单</el-radio-button>
      <el-radio-button label="dept">部门表单</el-radio-button>
    </el-radio-group>

    <div v-if="formType === 'user'">
      <AFormInput label="用户名" v-model="userForm.userName" />
    </div>
    <div v-else>
      <AFormInput label="部门名" v-model="deptForm.deptName" />
    </div>
  </div>
</template>

问题原因

  • Vue 的组件复用机制导致 DOM 节点被复用
  • 表单组件内部状态未正确重置
  • 相同位置的组件被 Vue 认为是同一个组件

解决方案

使用 key 强制重新创建组件:

vue
<template>
  <div>
    <el-radio-group v-model="formType">
      <el-radio-button label="user">用户表单</el-radio-button>
      <el-radio-button label="dept">部门表单</el-radio-button>
    </el-radio-group>

    <!-- 使用 key 确保组件重新创建 -->
    <div v-if="formType === 'user'" key="user-form">
      <AFormInput label="用户名" v-model="userForm.userName" />
    </div>
    <div v-else key="dept-form">
      <AFormInput label="部门名" v-model="deptForm.deptName" />
    </div>
  </div>
</template>

或者使用动态组件配合 key:

vue
<template>
  <component
    :is="currentFormComponent"
    :key="formType"
    v-model="currentFormData"
  />
</template>

<script setup lang="ts">
import UserForm from './UserForm.vue'
import DeptForm from './DeptForm.vue'

const formType = ref('user')

const currentFormComponent = computed(() => {
  return formType.value === 'user' ? UserForm : DeptForm
})

const currentFormData = computed({
  get: () => formType.value === 'user' ? userForm.value : deptForm.value,
  set: (val) => {
    if (formType.value === 'user') {
      userForm.value = val
    } else {
      deptForm.value = val
    }
  }
})
</script>

监听类型变化并重置表单:

vue
<script setup lang="ts">
watch(formType, (newType, oldType) => {
  if (newType !== oldType) {
    // 切换时重置表单
    if (newType === 'user') {
      userForm.value = { userName: '', email: '' }
    } else {
      deptForm.value = { deptName: '', leader: '' }
    }
  }
})
</script>

10. 图表组件在隐藏后显示尺寸异常

问题描述

图表组件放在 Tab 页或可折叠面板中,初始隐藏后再显示时,图表尺寸不正确(宽度为0或高度为0)。

vue
<template>
  <el-tabs v-model="activeTab">
    <el-tab-pane label="基本信息" name="info">
      <!-- 基本信息内容 -->
    </el-tab-pane>
    <el-tab-pane label="统计图表" name="charts">
      <!-- 初始隐藏,显示时尺寸异常 -->
      <ALineChart :xData="xData" :series="series" height="300px" />
    </el-tab-pane>
  </el-tabs>
</template>

问题原因

  • ECharts 在容器隐藏时无法获取正确的容器尺寸
  • 图表初始化时容器 display: none,导致宽高为0
  • Tab 切换后没有触发图表重新渲染

解决方案

方案一:使用 v-if 延迟渲染:

vue
<template>
  <el-tabs v-model="activeTab">
    <el-tab-pane label="基本信息" name="info">
      <!-- 基本信息内容 -->
    </el-tab-pane>
    <el-tab-pane label="统计图表" name="charts">
      <!-- 只有激活时才渲染 -->
      <ALineChart
        v-if="activeTab === 'charts'"
        :xData="xData"
        :series="series"
        height="300px"
      />
    </el-tab-pane>
  </el-tabs>
</template>

方案二:使用 lazy 属性(Element Plus Tabs):

vue
<template>
  <el-tabs v-model="activeTab">
    <el-tab-pane label="基本信息" name="info">
      <!-- 基本信息内容 -->
    </el-tab-pane>
    <el-tab-pane label="统计图表" name="charts" lazy>
      <ALineChart :xData="xData" :series="series" height="300px" />
    </el-tab-pane>
  </el-tabs>
</template>

方案三:监听显示状态并手动触发 resize:

vue
<template>
  <el-tabs v-model="activeTab" @tab-change="handleTabChange">
    <el-tab-pane label="统计图表" name="charts">
      <ALineChart ref="chartRef" :xData="xData" :series="series" height="300px" />
    </el-tab-pane>
  </el-tabs>
</template>

<script setup lang="ts">
const chartRef = ref()

const handleTabChange = (tabName: string) => {
  if (tabName === 'charts') {
    // 延迟执行,确保DOM已更新
    nextTick(() => {
      chartRef.value?.resize()
    })
  }
}
</script>

方案四:使用 ResizeObserver 自动处理:

vue
<template>
  <div ref="chartContainer" class="chart-container">
    <ALineChart :xData="xData" :series="series" height="100%" auto-resize />
  </div>
</template>

<script setup lang="ts">
import { useResizeObserver } from '@vueuse/core'

const chartContainer = ref<HTMLElement>()

useResizeObserver(chartContainer, (entries) => {
  // 容器尺寸变化时,图表会自动调整
  console.log('容器尺寸变化:', entries[0].contentRect)
})
</script>

<style scoped>
.chart-container {
  width: 100%;
  height: 300px;
}
</style>

11. ASearchForm 搜索条件无法重置

问题描述

点击重置按钮后,搜索条件没有恢复到初始值,或者部分字段没有被重置。

vue
<template>
  <ASearchForm v-model="queryParams" @reset="handleReset">
    <AFormInput label="用户名" prop="userName" v-model="queryParams.userName" />
    <AFormSelect label="状态" prop="status" v-model="queryParams.status" :options="statusOptions" />
    <AFormDate label="创建时间" prop="dateRange" v-model="queryParams.dateRange" type="daterange" />
  </ASearchForm>
</template>

<script setup lang="ts">
const queryParams = ref({
  userName: '',
  status: '',
  dateRange: []
})

const handleReset = () => {
  // 重置后 dateRange 仍然保留旧值
}
</script>

问题原因

  • 重置时只清空了简单类型字段,复杂类型(数组、对象)引用未变化
  • 没有正确定义初始值状态
  • 自定义字段没有被表单组件正确跟踪

解决方案

定义并使用初始值对象:

vue
<script setup lang="ts">
import { cloneDeep } from 'lodash-es'

// 定义初始值常量
const INITIAL_QUERY = {
  userName: '',
  status: '',
  dateRange: [],
  pageNum: 1,
  pageSize: 10
}

// 使用深拷贝创建响应式对象
const queryParams = ref(cloneDeep(INITIAL_QUERY))

const handleReset = () => {
  // 深拷贝初始值进行重置
  queryParams.value = cloneDeep(INITIAL_QUERY)

  // 重置后重新查询
  getList()
}
</script>

使用工厂函数创建初始值:

vue
<script setup lang="ts">
// 使用工厂函数确保每次都是新对象
const createInitialQuery = () => ({
  userName: '',
  status: '',
  dateRange: [] as string[],
  deptId: null as number | null,
  pageNum: 1,
  pageSize: 10
})

const queryParams = ref(createInitialQuery())

const handleReset = () => {
  // 使用工厂函数重置
  queryParams.value = createInitialQuery()
  getList()
}

// 也可以在组件卸载时重置
onUnmounted(() => {
  queryParams.value = createInitialQuery()
})
</script>

结合表单 ref 进行重置:

vue
<template>
  <ASearchForm
    ref="searchFormRef"
    v-model="queryParams"
    @reset="handleReset"
  >
    <!-- 表单项 -->
  </ASearchForm>
</template>

<script setup lang="ts">
const searchFormRef = ref()

const handleReset = () => {
  // 使用表单组件的重置方法
  searchFormRef.value?.resetFields()

  // 手动处理自定义字段
  queryParams.value.dateRange = []
  queryParams.value.customField = null

  getList()
}
</script>

12. UserSelect 初始化时获取不到用户名

问题描述

UserSelect 组件在编辑模式下,传入用户ID后无法正确显示用户名,显示的是ID数字。

vue
<template>
  <!-- 显示 "123" 而不是 "张三" -->
  <UserSelect v-model="form.userId" :initial-user-names="form.userName" />
</template>

<script setup lang="ts">
const form = ref({
  userId: 123,
  userName: '' // 后端没有返回用户名
})
</script>

问题原因

  • 后端接口只返回了用户ID,没有返回关联的用户名
  • initial-user-names 属性没有正确设置
  • 组件内部的用户信息查询失败

解决方案

方案一:后端接口返回完整用户信息:

typescript
// 建议后端在返回关联用户时同时返回用户名
interface OrderVo {
  orderId: string
  orderNo: string
  userId: number
  userName: string // 同时返回用户名
}

方案二:使用 initial-user-names 属性:

vue
<template>
  <UserSelect
    v-model="form.userId"
    :initial-user-names="form.userName"
  />
</template>

<script setup lang="ts">
// 确保 userName 有值
const form = ref({
  userId: 123,
  userName: '张三'
})
</script>

方案三:组件加载后查询用户名:

vue
<template>
  <UserSelect
    v-model="form.userId"
    :initial-user-names="displayUserName"
  />
</template>

<script setup lang="ts">
const displayUserName = ref('')

// 监听 userId 变化,查询对应的用户名
watch(
  () => form.value.userId,
  async (userId) => {
    if (userId && !form.value.userName) {
      const [err, user] = await getUserById(userId)
      if (!err) {
        displayUserName.value = user.userName
      }
    } else {
      displayUserName.value = form.value.userName
    }
  },
  { immediate: true }
)
</script>

方案四:使用用户对象作为 v-model:

vue
<template>
  <!-- 传入完整用户对象,组件自动解析用户名 -->
  <UserSelect v-model="selectedUser" />
</template>

<script setup lang="ts">
import type { SysUserVo } from '@/api/system/core/user/userModel'

// 使用完整用户对象
const selectedUser = ref<SysUserVo | null>(null)

// 编辑时设置完整对象
const handleEdit = async (row: OrderVo) => {
  // 先查询用户详情
  const [err, user] = await getUserById(row.userId)
  if (!err) {
    selectedUser.value = user
  }
}

// 提交时获取用户ID
const handleSubmit = () => {
  const userId = selectedUser.value?.userId
  // ...
}
</script>

总结

RuoYi-Plus-UniApp 前端业务组件库提供了完整的后台管理系统开发解决方案:

  • 13 个表单组件 - 完整的表单输入方案
  • 3 个搜索表格组件 - 列表页核心组件
  • 2 个弹窗组件 - 对话框和详情展示
  • 2 个业务选择器 - 用户选择和字典显示
  • 23 个卡片组件 - 丰富的展示卡片
  • 10 个图表组件 - 完整的数据可视化
  • 4 个 AI 组件 - 智能辅助工具

通过这些组件,开发者可以快速构建列表页、灵活定制表单、展示丰富数据。所有组件都经过生产环境验证,性能优秀,易于使用。