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[][]
value值string | 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 组件 - 智能辅助工具

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