业务组件总览
介绍
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辅助输入框 | AFormInputWithAi | AI增强输入 |
2. 搜索和表格组件 (3个)
| 组件名称 | 组件标识 | 主要功能 |
|---|---|---|
| 搜索表单 | ASearchForm | 搜索条件容器 |
| 表格工具栏 | TableToolbar | 表格操作工具栏 |
| 分页器 | Pagination | 数据分页 |
3. 弹窗组件 (2个)
| 组件名称 | 组件标识 | 主要功能 |
|---|---|---|
| 通用弹窗 | AModal | 对话框/抽屉容器 |
| 详情弹窗 | ADetail | 数据详情展示 |
4. 业务选择器 (2个)
| 组件名称 | 组件标识 | 主要功能 |
|---|---|---|
| 用户选择器 | UserSelect | 用户选择 |
| 字典标签 | DictTag | 字典值显示 |
5. 卡片组件 (23个)
| 类别 | 组件数量 | 典型组件 |
|---|---|---|
| 统计卡片 | 3 | AStatsCard, ABarStatsCard, ALineStatsCard |
| 图表卡片 | 5 | ABarChartCard, ALineChartCard, APieChartCard |
| 内容卡片 | 4 | AFormCard, ATableCard, AImageCard, AInfoCard |
| 业务卡片 | 4 | AUserCard, AProfileCard, APricingCard, ASocialCard |
| 列表卡片 | 5 | ADataListCard, ATimelineListCard, AActivityCard |
| 特殊卡片 | 2 | ADataCard, AEmptyCard |
6. 图表组件 (10个)
| 组件名称 | 图表类型 | 使用场景 |
|---|---|---|
| AChart | 通用图表 | ECharts 基础封装 |
| ALineChart | 折线图 | 趋势分析 |
| ABarChart | 竖向柱状图 | 数据对比 |
| ABarHorizontalChart | 横向柱状图 | 排名展示 |
| ABarBidirectionalChart | 双向柱状图 | 对比分析 |
| APieChart | 饼图/环形图 | 占比展示 |
| ARadarChart | 雷达图 | 多维度评估 |
| AScatterChart | 散点图 | 分布关系 |
| ACandlestickChart | K线图 | 股票数据 |
| AMapChart | 地图 | 地理分布 |
7. AI 组件 (4个)
| 组件名称 | 功能 |
|---|---|
| AAiAssistant | AI 助手面板 |
| AAiTextOptimizer | 文本优化工具 |
| AAiContentReviewer | 内容审查工具 |
| AAiDataGenerator | 数据生成工具 |
8. 其他组件 (13个)
| 组件名称 | 功能 |
|---|---|
| AImportExcel | Excel 导入 |
| AOssMediaManager | 媒体管理器 |
| AThemeColorPicker | 主题配色选择器 |
| AThemeSvg | SVG 渲染 |
| AGeometricBackground | 几何背景 |
| AWatermark | 水印 |
| Icon | 图标渲染 |
| IconSelect | 图标选择器 |
| ImagePreview | 图片预览 |
| ASelectionTags | 选中标签 |
| AResizablePanels | 可拖拽面板 |
| IFrameContainer | IFrame 容器 |
| ARecharge | 充值组件 |
核心功能详解
响应式布局系统
所有表单组件都支持响应式布局,通过 span 属性实现。
<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 组件具备智能的数据类型检测和转换功能。
<script setup lang="ts">
const form = ref({
roleIds: '1,2,3' // 字符串输入
})
// 组件自动转换为数组 [1, 2, 3] 用于内部选择
// 确认后自动转回字符串 "1,2,3" 保持类型一致
</script>类型检测规则:
- 如果数组中有任何字符串元素,输出字符串数组
- 如果全是数字且在安全整数范围内,输出数字数组
- 如果数字超过 15 位,自动转为字符串防止精度丢失
选项禁用条件配置
AFormSelect 支持灵活的选项禁用配置。
<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 - 文本输入框
<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 | 最大长度 | number | 255 |
| showWordLimit | 显示字数统计 | boolean | true |
| showPassword | 显示密码可见性切换 | boolean | false |
| preventAutofill | 防止自动填充 | boolean | false |
| clearable | 显示清除按钮 | boolean | true |
| disabled | 是否禁用 | boolean | false |
| 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 - 下拉选择器
<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 | 是否多选 | boolean | false |
| filterable | 是否可搜索 | boolean | true |
| clearable | 是否可清空 | boolean | true |
| valueField | value 字段名 | string | 'value' |
| labelField | label 字段名 | string | 'label' |
| disabledField | 禁用判断字段名 | string | 'status' |
| disabledValue | 禁用条件值 | DisabledCondition | '0' |
| showValue | 是否显示选项值 | boolean | - |
| multipleLimit | 多选最多可选项数 | number | 0 |
| collapseTags | 多选是否折叠标签 | boolean | false |
AFormDate - 日期选择器
<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 - 树形选择器
<template>
<AFormTreeSelect
label="所属部门"
v-model="form.deptId"
:options="deptTree"
:props="{ label: 'deptName', value: 'deptId', children: 'children' }"
:span="12"
/>
</template>AFormUpload - 文件上传
<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 - 搜索表单
<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 | 显示/隐藏 | boolean | true |
| inline | 是否行内表单 | boolean | true |
| labelWidth | 标签宽度 | string | 'auto' |
| title | 卡片标题 | string | - |
| collapsible | 是否可展开收起 | boolean | true |
| defaultExpanded | 默认是否展开 | boolean | false |
TableToolbar - 表格工具栏
<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 - 分页器
<template>
<Pagination
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</template>弹窗组件详解
AModal - 通用弹窗
支持对话框和抽屉两种模式。
<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 | 显示/隐藏状态 | boolean | false |
| mode | 模式 | 'dialog' | 'drawer' | 'dialog' |
| title | 标题 | string | - |
| size | 预设尺寸 | 'small' | 'medium' | 'large' | 'xl' | 'medium' |
| width | 自定义宽度 | string | number | - |
| fullscreen | 是否全屏 | boolean | false |
| movable | 是否可拖动 | boolean | false |
| direction | 抽屉方向 | 'ltr' | 'rtl' | 'ttb' | 'btt' | 'rtl' |
| showFooter | 显示底部 | boolean | true |
| footerType | 底部按钮类型 | 'default' | 'close-only' | 'default' |
| loading | 加载状态 | boolean | false |
| destroyOnClose | 关闭时销毁 | boolean | true |
Events 事件
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| update:modelValue | 状态变化 | (value: boolean) => void |
| confirm | 确认按钮点击 | () => void |
| cancel | 取消按钮点击 | () => void |
| open | 开始打开 | () => void |
| opened | 完全打开 | () => void |
| close | 开始关闭 | () => void |
| closed | 完全关闭 | () => void |
尺寸配置
const sizeMap = {
small: { dialog: '600px', drawer: '600px' },
medium: { dialog: '800px', drawer: '800px' },
large: { dialog: '1000px', drawer: '1000px' },
xl: { dialog: '1200px', drawer: '1200px' }
}ADetail - 详情弹窗
<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 | 图片预览 |
| html | HTML 内容 |
| file | 文件链接 |
| date | 日期格式化 |
| datetime | 日期时间格式化 |
| currency | 货币格式化 |
| boolean | 是/否 |
| array | 数组显示 |
业务选择器详解
UserSelect - 用户选择器
<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 | 是否多选 | boolean | false |
| userIds | 限制用户 ID 范围 | string | number | Array | - |
| defaultReturnType | 默认返回类型 | 'object' | 'id' | 'object' |
| showInlineTags | 显示内置标签 | boolean | false |
| buttonText | 按钮文本 | string | '选择用户' |
| buttonType | 按钮类型 | string | 'primary' |
| disabled | 是否禁用 | boolean | false |
| initialUserNames | 初始用户名 | string | string[] | - |
智能返回类型
// 传入用户对象,返回用户对象
const user1 = ref<SysUserVo>({ userId: '1', userName: 'admin' })
// 传入用户 ID,返回用户 ID
const user2 = ref('1')
// 传入空值,根据 defaultReturnType 决定
const user3 = ref(null)DictTag - 字典标签
<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[] | [] |
| valueField | value 字段名 | string | 'value' |
| labelField | label 字段名 | string | 'label' |
图表组件
基本用法
<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. 列表页面开发流程
<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. 表单验证集成
<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. 权限控制集成
<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. 响应式布局最佳实践
<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 多选时类型不一致?
解决方案:
// 方案1: 让组件自动处理类型转换
const form = ref({
roleIds: '1,2,3' // 字符串输入,组件自动转换
})
// 方案2: 统一使用数组格式
const form = ref({
roleIds: [1, 2, 3] // 数组输入
})2. AFormInput 防自动填充不生效?
解决方案:
<template>
<AFormInput label="密码" v-model="form.password" type="password" show-password prevent-autofill />
</template>
<!-- 如果仍然不生效,可以在表单外层添加 -->
<el-form autocomplete="new-password">3. UserSelect 返回类型如何控制?
解决方案:
// 方式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 弹窗时,内容出现闪烁,或者表单数据显示上一次的旧值而不是新值。
<!-- 点击编辑时,弹窗中显示的是上一次编辑的数据 -->
<template>
<AModal v-model="visible" title="编辑用户">
<el-form :model="form">
<AFormInput label="用户名" v-model="form.userName" />
</el-form>
</AModal>
</template>问题原因
destroyOnClose默认为true,但表单数据没有在打开时正确初始化- 异步数据加载和弹窗渲染时序问题
- v-model 的响应式更新延迟
解决方案
在打开弹窗时正确初始化表单数据:
<!-- ❌ 错误:直接赋值对象引用 -->
<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 事件确保数据加载时机正确:
<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而不是对应的标签。
<!-- 编辑用户时,部门显示的是 "102" 而不是 "研发部" -->
<template>
<AFormTreeSelect
label="所属部门"
v-model="form.deptId"
:options="deptTree"
/>
</template>问题原因
- 树形数据和表单值的加载时序问题:表单值先于树形数据加载完成
- 树形数据的节点配置
props不正确 - 数据类型不匹配(字符串 vs 数字)
解决方案
确保树形数据先于表单数据加载:
<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 配置正确:
<template>
<AFormTreeSelect
label="所属部门"
v-model="form.deptId"
:options="deptTree"
:props="{
value: 'deptId',
label: 'deptName',
children: 'children',
disabled: 'disabled'
}"
check-strictly
value-key="deptId"
/>
</template>处理数据类型不匹配问题:
// 后端返回的 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 组件时,页面渲染缓慢,滚动卡顿。
<!-- 表格有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 数组查找匹配项
- 表格虚拟滚动未启用
解决方案
方案一:使用函数式渲染代替组件:
<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>方案二:创建轻量级的字典标签:
// 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
}
}方案三:启用表格虚拟滚动:
<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 中插入图片时,上传失败或图片无法显示。
<template>
<AFormEditor
label="文章内容"
v-model="form.content"
:span="24"
/>
</template>问题原因
- 上传接口配置不正确或未配置
- 图片格式或大小不符合要求
- 跨域问题导致上传失败
- Token 认证问题
解决方案
配置正确的上传选项:
<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>处理跨域问题(需后端配合):
// 如果使用代理,确保 vite.config.ts 配置正确
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})使用 Base64 方式(不推荐用于大图):
<template>
<AFormEditor
label="文章内容"
v-model="form.content"
:upload-options="{ mode: 'base64' }"
:span="24"
/>
</template>9. 表单组件在动态切换时状态残留
问题描述
使用 v-if 动态切换表单组件时,组件内部状态残留,导致数据显示错误。
<!-- 切换表单类型时,输入框的值没有清除 -->
<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 强制重新创建组件:
<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:
<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>监听类型变化并重置表单:
<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)。
<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 延迟渲染:
<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):
<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:
<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 自动处理:
<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 搜索条件无法重置
问题描述
点击重置按钮后,搜索条件没有恢复到初始值,或者部分字段没有被重置。
<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>问题原因
- 重置时只清空了简单类型字段,复杂类型(数组、对象)引用未变化
- 没有正确定义初始值状态
- 自定义字段没有被表单组件正确跟踪
解决方案
定义并使用初始值对象:
<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>使用工厂函数创建初始值:
<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 进行重置:
<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数字。
<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属性没有正确设置- 组件内部的用户信息查询失败
解决方案
方案一:后端接口返回完整用户信息:
// 建议后端在返回关联用户时同时返回用户名
interface OrderVo {
orderId: string
orderNo: string
userId: number
userName: string // 同时返回用户名
}方案二:使用 initial-user-names 属性:
<template>
<UserSelect
v-model="form.userId"
:initial-user-names="form.userName"
/>
</template>
<script setup lang="ts">
// 确保 userName 有值
const form = ref({
userId: 123,
userName: '张三'
})
</script>方案三:组件加载后查询用户名:
<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:
<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 组件 - 智能辅助工具
通过这些组件,开发者可以快速构建列表页、灵活定制表单、展示丰富数据。所有组件都经过生产环境验证,性能优秀,易于使用。
