组件类型定义
介绍
组件类型定义是 Vue 3 TypeScript 项目中确保类型安全的核心部分。在 RuoYi-Plus-UniApp 前端项目中,所有业务组件都使用 TypeScript 编写,并为 Props、Emits、Slots 等提供完整的类型定义。
核心特性:
- 类型安全 - 通过 TypeScript 接口定义组件 Props,确保使用时的类型检查
- 智能提示 - IDE 可以提供完整的属性提示和自动补全
- 文档化注释 - 使用 JSDoc 注释为每个属性添加详细说明
- 泛型支持 - 支持泛型组件,实现更灵活的类型推导
- 默认值定义 - 使用
withDefaults为 Props 提供默认值 - 事件类型 - 为组件事件定义精确的参数类型
- 响应式类型 - 支持响应式 Props 配置,如
span属性的多种类型 - 组合式设计 - Props 接口设计遵循单一职责原则,易于扩展
本文档涵盖项目中常用的组件类型定义模式,包括基础组件、表单组件、业务组件、模态框组件等的完整类型定义示例。
基础组件类型
Icon 图标组件
Icon 组件是最基础的 UI 组件之一,用于显示图标。
IconProps 接口定义
/**
* Icon 图标组件 Props
*/
interface IconProps {
/**
* 图标值(字符串形式)
* @example 'user'
*/
value?: string
/**
* 图标代码(IconCode 类型)
* @example { type: 'font', name: 'user' }
*/
code?: IconCode
/**
* 图标尺寸
* - 预设尺寸: xs, sm, md, lg, xl, 2xl
* - 自定义尺寸: 可以是数字或字符串
* @default 'md'
*/
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | string | number
/**
* 图标颜色
* @example '#409EFF' 或 'primary'
*/
color?: string
/**
* 图标动画效果
* @example 'shake' | 'rotate180' | 'moveUp' | 'expand' | 'shrink' | 'breathing'
*/
animate?: AnimateType
}
/**
* 动画类型定义
*/
type AnimateType = 'shake' | 'rotate180' | 'moveUp' | 'expand' | 'shrink' | 'breathing'
/**
* 图标代码类型
*/
interface IconCode {
/** 图标类型: font-字体图标, uno-UnoCSS图标 */
type: 'font' | 'uno'
/** 图标名称 */
name: string
}使用示例
<template>
<div class="icon-demo">
<!-- 基础用法 -->
<Icon value="user" />
<!-- 指定尺寸和颜色 -->
<Icon value="setting" size="lg" color="#409EFF" />
<!-- 使用动画 -->
<Icon value="reload" animate="rotate180" />
<!-- 使用 IconCode -->
<Icon :code="{ type: 'font', name: 'user' }" />
<!-- 自定义尺寸 -->
<Icon value="star" :size="32" />
</div>
</template>
<script setup lang="ts">
import { Icon } from '@/components'
</script>IconSelect 图标选择器
图标选择器组件用于在弹窗中选择图标。
IconSelectProps 接口定义
/**
* IconSelect 图标选择器 Props
*/
interface IconSelectProps {
/**
* 绑定值(v-model)
* 选中的图标代码
*/
modelValue: string
/**
* 选择器宽度
* @default '100%'
*/
width?: string
/**
* 是否禁用
* @default false
*/
disabled?: boolean
/**
* 占位符文本
* @default '请选择图标'
*/
placeholder?: string
/**
* 是否可清空
* @default true
*/
clearable?: boolean
}IconSelectEmits 接口定义
/**
* IconSelect 图标选择器事件
*/
interface IconSelectEmits {
/** 更新 modelValue,实现 v-model 双向绑定 */
(e: 'update:modelValue', value: string): void
/** 选中图标时触发 */
(e: 'select', value: string): void
/** 清空选择时触发 */
(e: 'clear'): void
}使用示例
<template>
<el-form :model="form">
<el-form-item label="菜单图标">
<IconSelect
v-model="form.icon"
width="200px"
@select="handleIconSelect"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { IconSelect } from '@/components'
interface MenuForm {
name: string
icon: string
}
const form = ref<MenuForm>({
name: '',
icon: ''
})
const handleIconSelect = (value: string) => {
console.log('选中图标:', value)
}
</script>表单组件类型
AFormInput 输入框组件
AFormInput 是项目中最常用的表单组件之一,支持文本输入、数字输入、文本域等多种类型。
AFormInputProps 完整接口
/**
* 输入组件的Props接口定义
* @description 定义了输入组件的所有属性和类型
*/
interface AFormInputProps {
/**
* 绑定值,支持字符串或数字
* @default undefined
*/
modelValue?: string | number | null | undefined
/**
* 标签文本
* @default ''
*/
label?: string
/**
* 标签宽度,支持数字或字符串
* @default undefined
*/
labelWidth?: number | string
/**
* 占位符文本
* @default ''
*/
placeholder?: string
/**
* 组件宽度,支持数字或字符串
* @default undefined
*/
width?: number | string
/**
* 表单域model数据字段名
* @default ''
*/
prop?: string
/**
* 最大长度
* @default 255
*/
maxlength?: number | string
/**
* 是否显示字数统计
* @default false
*/
showWordLimit?: boolean
/**
* 是否显示表单项
* @default true
*/
showFormItem?: boolean
/**
* 是否显示密码可见性切换按钮
* @default false
*/
showPassword?: boolean
/**
* 输入框类型
* @default 'text'
*/
type?: 'text' | 'textarea' | 'number' | 'password'
/**
* 文本域自适应配置
* @default { minRows: 2, maxRows: 30 }
*/
autosize?: { minRows?: number; maxRows?: number }
/**
* 组件尺寸
* @default ''
*/
size?: '' | 'default' | 'small' | 'large'
/**
* 栅格占据的列数,支持数字、响应式对象或预设字符串
* - 数字: 固定span值,如 12
* - 响应式对象: { xs: 24, sm: 24, md: 12, lg: 8, xl: 6 }
* - 预设字符串: 'auto' - 自动响应式布局
* @default undefined
*/
span?: SpanType
/**
* 是否显示清除按钮
* @default true
*/
clearable?: boolean
/**
* 是否禁用
* @default false
*/
disabled?: boolean
/**
* 文本域行数
* @default 3
*/
rows?: number
/**
* 提示信息
* @default ''
*/
tooltip?: string
// 数字输入框特有属性
/**
* 数字输入框最小值
* @default undefined
*/
min?: number
/**
* 数字输入框最大值
* @default undefined
*/
max?: number
/**
* 数字输入框步长
* @default 1
*/
step?: number
/**
* 是否只能输入 step 的倍数
* @default false
*/
stepStrictly?: boolean
/**
* 数值精度
* @default undefined
*/
precision?: number
/**
* 是否使用控制按钮
* @default true
*/
controls?: boolean
/**
* 控制按钮位置
* @default ''
*/
controlsPosition?: '' | 'right'
/**
* 数字输入框尺寸
* @default ''
*/
inputNumberSize?: '' | 'default' | 'small' | 'large'
/**
* 响应式模式
* - 'screen': 基于屏幕尺寸
* - 'container': 基于容器尺寸(弹窗场景推荐)
* - 'modal-size': 基于 AModal 的 size 属性
* @default 'screen'
*/
responsiveMode?: 'screen' | 'container' | 'modal-size'
/**
* 当 responsiveMode 为 'modal-size' 时使用
* 应该传入 AModal 的 size 属性值
*/
modalSize?: 'small' | 'medium' | 'large' | 'xl'
/**
* 是否防止浏览器自动填充
* 主要用于密码输入框,防止浏览器自动填充密码
* @default false
*/
preventAutofill?: boolean
}
/**
* SpanType 响应式栅格类型
*/
type SpanType =
| number
| 'auto'
| {
xs?: number
sm?: number
md?: number
lg?: number
xl?: number
}AFormInputEmits 事件接口
/**
* AFormInput 组件事件定义
*/
interface AFormInputEmits {
/** 更新 modelValue */
(e: 'update:modelValue', value: string | number | null | undefined): void
/** 输入时触发 */
(e: 'input', value: string | number | null | undefined): void
/** 失去焦点时触发 */
(e: 'blur', value: any): void
/** 值改变时触发 */
(e: 'change', value: any): void
/** 按下回车键时触发 */
(e: 'enter', value: any): void
}使用示例
<template>
<el-form :model="form" ref="formRef">
<!-- 基础文本输入 -->
<AFormInput
label="用户名"
v-model="form.userName"
prop="userName"
:span="12"
/>
<!-- 密码输入,防止自动填充 -->
<AFormInput
label="密码"
v-model="form.password"
prop="password"
type="password"
show-password
prevent-autofill
:span="12"
/>
<!-- 文本域,显示字数统计 -->
<AFormInput
label="备注"
v-model="form.remark"
prop="remark"
type="textarea"
:maxlength="200"
show-word-limit
:span="24"
/>
<!-- 数字输入 -->
<AFormInput
label="年龄"
v-model="form.age"
prop="age"
type="number"
:min="0"
:max="150"
:span="12"
/>
<!-- 响应式布局 -->
<AFormInput
label="邮箱"
v-model="form.email"
prop="email"
:span="{ xs: 24, sm: 24, md: 12, lg: 8, xl: 6 }"
/>
<!-- 带提示信息 -->
<AFormInput
label="手机号"
v-model="form.phone"
prop="phone"
tooltip="请输入11位手机号码"
:span="12"
/>
<!-- 使用插槽 -->
<AFormInput
label="网址"
v-model="form.website"
prop="website"
:span="12"
>
<template #prepend>
https://
</template>
</AFormInput>
</el-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
interface UserForm {
userName: string
password: string
remark: string
age: number
email: string
phone: string
website: string
}
const formRef = ref<FormInstance>()
const form = ref<UserForm>({
userName: '',
password: '',
remark: '',
age: 0,
email: '',
phone: '',
website: ''
})
</script>AFormSelect 下拉选择组件
AFormSelectProps 接口定义
/**
* 下拉选择组件 Props
*/
interface AFormSelectProps {
/**
* 绑定值
*/
modelValue?: string | number | boolean | object | any[]
/**
* 标签文本
*/
label?: string
/**
* 标签宽度
*/
labelWidth?: number | string
/**
* 占位符
* @default '请选择'
*/
placeholder?: string
/**
* 表单字段名
*/
prop?: string
/**
* 选项数据
*/
options?: SelectOption[]
/**
* 是否多选
* @default false
*/
multiple?: boolean
/**
* 是否可清空
* @default true
*/
clearable?: boolean
/**
* 是否可搜索
* @default false
*/
filterable?: boolean
/**
* 是否禁用
* @default false
*/
disabled?: boolean
/**
* 栅格占据的列数
*/
span?: SpanType
/**
* 是否显示表单项
* @default true
*/
showFormItem?: boolean
/**
* 组件尺寸
*/
size?: '' | 'default' | 'small' | 'large'
/**
* 提示信息
*/
tooltip?: string
/**
* 是否允许用户创建新条目
* @default false
*/
allowCreate?: boolean
/**
* 是否折叠多选标签
* @default false
*/
collapseTags?: boolean
/**
* 多选时最多显示的标签数量
*/
maxCollapseTags?: number
}
/**
* 选项数据类型
*/
interface SelectOption {
/** 显示文本 */
label: string
/** 选项值 */
value: string | number | boolean
/** 是否禁用此选项 */
disabled?: boolean
/** 选项分组 */
options?: SelectOption[]
}使用示例
<template>
<el-form :model="form">
<!-- 基础下拉选择 -->
<AFormSelect
label="状态"
v-model="form.status"
prop="status"
:options="statusOptions"
:span="12"
/>
<!-- 多选 -->
<AFormSelect
label="角色"
v-model="form.roles"
prop="roles"
:options="roleOptions"
multiple
collapse-tags
:span="12"
/>
<!-- 可搜索 -->
<AFormSelect
label="部门"
v-model="form.deptId"
prop="deptId"
:options="deptOptions"
filterable
:span="12"
/>
</el-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const form = ref({
status: '',
roles: [],
deptId: ''
})
const statusOptions = [
{ label: '正常', value: '0' },
{ label: '停用', value: '1' }
]
const roleOptions = [
{ label: '管理员', value: 1 },
{ label: '普通用户', value: 2 },
{ label: '访客', value: 3 }
]
const deptOptions = [
{ label: '研发部', value: 1 },
{ label: '市场部', value: 2 },
{ label: '运营部', value: 3 }
]
</script>AFormFileUpload 文件上传组件
AFormFileUploadProps 接口定义
/**
* 文件上传组件 Props
*/
interface AFormFileUploadProps {
/**
* 绑定值,文件列表
*/
modelValue?: string | UploadFile[]
/**
* 标签文本
*/
label?: string
/**
* 表单字段名
*/
prop?: string
/**
* 最大上传数量
* @default 5
*/
limit?: number
/**
* 文件大小限制(MB)
* @default 10
*/
fileSize?: number
/**
* 允许的文件类型
* @example ['jpg', 'png', 'pdf']
*/
fileType?: string[]
/**
* 是否显示文件列表
* @default true
*/
showFileList?: boolean
/**
* 是否禁用
* @default false
*/
disabled?: boolean
/**
* 栅格占据的列数
*/
span?: SpanType
/**
* 上传按钮文字
* @default '选择文件'
*/
buttonText?: string
/**
* 提示文字
*/
tip?: string
/**
* 是否自动上传
* @default true
*/
autoUpload?: boolean
}
/**
* 上传文件类型
*/
interface UploadFile {
/** 文件名 */
name: string
/** 文件URL */
url: string
/** 文件大小 */
size?: number
/** 文件类型 */
type?: string
/** 上传状态 */
status?: 'uploading' | 'success' | 'error'
/** 上传进度 */
percentage?: number
/** 唯一标识 */
uid?: number
/** 原始文件对象 */
raw?: File
}AFormFileUploadEmits 事件接口
/**
* 文件上传组件事件
*/
interface AFormFileUploadEmits {
/** 更新 modelValue */
(e: 'update:modelValue', value: string | UploadFile[]): void
/** 文件上传成功 */
(e: 'success', file: UploadFile, fileList: UploadFile[]): void
/** 文件上传失败 */
(e: 'error', error: Error, file: UploadFile): void
/** 文件移除 */
(e: 'remove', file: UploadFile, fileList: UploadFile[]): void
/** 文件超出限制 */
(e: 'exceed', files: File[], fileList: UploadFile[]): void
}业务组件类型
AModal 模态框组件
AModal 是项目中统一的模态框组件,支持对话框和抽屉两种模式。
AModalProps 完整接口
/**
* AModal 组件属性接口定义
*/
interface AModalProps {
/**
* 控制模态框显示/隐藏状态
*/
modelValue: boolean
/**
* 模态框模式: dialog-对话框, drawer-抽屉
* @default 'dialog'
*/
mode?: 'dialog' | 'drawer'
/**
* 模态框标题
*/
title?: string
/**
* 自定义宽度/尺寸,可以是具体数值或百分比
*/
width?: string | number
/**
* 预设尺寸: small-小, medium-中等, large-大, xl-超大
* @default 'medium'
*/
size?: 'small' | 'medium' | 'large' | 'xl'
// ========== 行为控制相关 ==========
/**
* 是否显示关闭按钮
* @default true
*/
closable?: boolean
/**
* 是否可以通过点击遮罩层关闭
* @default false
*/
maskClosable?: boolean
/**
* 是否可以通过 ESC 键关闭
* @default true
*/
keyboard?: boolean
/**
* 关闭时是否销毁内部元素
* @default true
*/
destroyOnClose?: boolean
/**
* 是否将模态框挂载到 body 元素下
* @default true
*/
appendToBody?: boolean
/**
* 关闭前的回调函数,可以用来阻止关闭
*/
beforeClose?: (done: () => void) => void
/**
* 是否可以拖动(仅对话框模式有效,全屏模式下无效)
* @default false
*/
movable?: boolean
// ========== 抽屉模式特有属性 ==========
/**
* 抽屉弹出方向: ltr-左到右, rtl-右到左, ttb-上到下, btt-下到上
* @default 'rtl'
*/
direction?: 'ltr' | 'rtl' | 'ttb' | 'btt'
// ========== 内容控制相关 ==========
/**
* 是否显示底部操作区域
* @default true
*/
showFooter?: boolean
/**
* 底部按钮类型: default-确定+取消, close-only-仅关闭按钮
* @default 'default'
*/
footerType?: 'default' | 'close-only'
/**
* 底部按钮对齐方式
* @default 'right'
*/
footerAlign?: 'left' | 'center' | 'right'
/**
* 内容区域是否显示加载状态
* @default false
*/
loading?: boolean
/**
* 是否全屏显示(仅对话框模式有效)
* @default false
*/
fullscreen?: boolean
// ========== 按钮文本自定义 ==========
/**
* 确认按钮文本
* @default '确定'
*/
confirmText?: string
/**
* 取消按钮文本
* @default '取消'
*/
cancelText?: string
}AModalEmits 事件接口
/**
* AModal 组件事件定义
*/
interface AModalEmits {
/** 更新 modelValue,实现 v-model 双向绑定 */
(e: 'update:modelValue', value: boolean): void
/** 用户点击确认按钮时触发 */
(e: 'confirm'): void
/** 用户点击取消按钮时触发 */
(e: 'cancel'): void
/** 模态框开始打开时触发 */
(e: 'open'): void
/** 模态框完全打开后触发 */
(e: 'opened'): void
/** 模态框开始关闭时触发 */
(e: 'close'): void
/** 模态框完全关闭后触发 */
(e: 'closed'): void
}使用示例
<template>
<div>
<!-- 按钮触发 -->
<el-button @click="dialogVisible = true">打开对话框</el-button>
<el-button @click="drawerVisible = true">打开抽屉</el-button>
<!-- 基础对话框 -->
<AModal
v-model="dialogVisible"
title="新增用户"
@confirm="handleSubmit"
@cancel="handleCancel"
>
<el-form :model="form" ref="formRef">
<AFormInput label="用户名" v-model="form.userName" prop="userName" />
<AFormInput label="邮箱" v-model="form.email" prop="email" />
</el-form>
</AModal>
<!-- 抽屉模式 -->
<AModal
v-model="drawerVisible"
title="用户详情"
mode="drawer"
direction="rtl"
size="large"
:show-footer="false"
>
<UserDetail :user="selectedUser" />
</AModal>
<!-- 可拖动对话框 -->
<AModal
v-model="movableVisible"
title="可拖动对话框"
:movable="true"
size="small"
>
<p>可以拖动标题栏移动此对话框</p>
</AModal>
<!-- 全屏对话框 -->
<AModal
v-model="fullscreenVisible"
title="大数据分析"
:fullscreen="true"
>
<DataAnalysis />
</AModal>
<!-- 自定义底部 -->
<AModal v-model="customVisible" title="自定义操作">
<template #footer>
<el-button @click="customVisible = false">取消</el-button>
<el-button type="warning" @click="handleSave">保存草稿</el-button>
<el-button type="primary" @click="handlePublish">发布</el-button>
</template>
<ContentEditor v-model="content" />
</AModal>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
const dialogVisible = ref(false)
const drawerVisible = ref(false)
const movableVisible = ref(false)
const fullscreenVisible = ref(false)
const customVisible = ref(false)
const formRef = ref<FormInstance>()
const form = ref({
userName: '',
email: ''
})
const selectedUser = ref(null)
const content = ref('')
const handleSubmit = async () => {
await formRef.value?.validate()
// 提交逻辑
dialogVisible.value = false
}
const handleCancel = () => {
dialogVisible.value = false
}
const handleSave = () => {
// 保存草稿逻辑
}
const handlePublish = () => {
// 发布逻辑
customVisible.value = false
}
</script>AOssMediaManager OSS 媒体管理器
AOssMediaManagerProps 接口定义
/**
* OSS 媒体管理器组件 Props
*/
interface AOssMediaManagerProps {
/**
* 绑定值,选中的文件URL或URL数组
*/
modelValue?: string | string[]
/**
* 是否多选模式
* @default false
*/
multiple?: boolean
/**
* 最大选择数量(多选模式下有效)
* @default 9
*/
limit?: number
/**
* 文件类型过滤
* @example ['image', 'video', 'audio', 'document']
*/
accept?: string[]
/**
* 是否显示上传按钮
* @default true
*/
showUpload?: boolean
/**
* 是否显示删除按钮
* @default true
*/
showDelete?: boolean
/**
* 是否禁用
* @default false
*/
disabled?: boolean
}泛型组件类型
通用表格组件
泛型组件允许在使用时指定具体的数据类型,实现更强的类型推导。
TableProps 泛型接口
/**
* 通用表格组件 Props
* @template T 表格数据类型
*/
interface TableProps<T = any> {
/**
* 表格数据(分页结果)
*/
data: PageResult<T>
/**
* 字段配置
*/
fields: FieldConfig[]
/**
* 是否加载中
* @default false
*/
loading?: boolean
/**
* 是否显示选择框
* @default false
*/
selection?: boolean
/**
* 是否显示序号列
* @default false
*/
showIndex?: boolean
/**
* 表格高度
*/
height?: string | number
/**
* 是否显示边框
* @default false
*/
border?: boolean
/**
* 是否显示斑马纹
* @default true
*/
stripe?: boolean
/**
* 表格尺寸
* @default 'default'
*/
size?: 'large' | 'default' | 'small'
}
/**
* 分页结果类型
*/
interface PageResult<T> {
/** 数据列表 */
rows: T[]
/** 总记录数 */
total: number
}
/**
* 字段配置类型
*/
interface FieldConfig {
/** 字段名 */
prop: string
/** 列标题 */
label: string
/** 列宽度 */
width?: string | number
/** 最小宽度 */
minWidth?: string | number
/** 是否固定列 */
fixed?: 'left' | 'right'
/** 对齐方式 */
align?: 'left' | 'center' | 'right'
/** 是否可排序 */
sortable?: boolean
/** 自定义渲染函数 */
formatter?: (row: any, column: any, cellValue: any) => string
}TableEmits 泛型接口
/**
* 通用表格组件事件
* @template T 表格数据类型
*/
interface TableEmits<T> {
/** 选择行时触发 */
(e: 'select', rows: T[]): void
/** 点击行时触发 */
(e: 'row-click', row: T): void
/** 查询条件改变 */
(e: 'update:query', query: PageQuery): void
/** 排序改变 */
(e: 'sort-change', column: any, prop: string, order: string): void
}泛型组件使用示例
<template>
<BasicTable
:data="tableData"
:fields="tableFields"
:loading="loading"
selection
show-index
@select="handleSelect"
@row-click="handleRowClick"
/>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { ref } from 'vue'
import type { PageResult, FieldConfig } from '@/types'
/**
* 用户数据类型
*/
interface User {
userId: number
userName: string
email: string
status: string
createTime: string
}
// 指定泛型类型
const tableData = ref<PageResult<User>>({
rows: [],
total: 0
})
const tableFields: FieldConfig[] = [
{ prop: 'userId', label: '用户ID', width: 100 },
{ prop: 'userName', label: '用户名', minWidth: 120 },
{ prop: 'email', label: '邮箱', minWidth: 200 },
{ prop: 'status', label: '状态', width: 100 },
{ prop: 'createTime', label: '创建时间', width: 180 }
]
const loading = ref(false)
// 事件处理函数会自动推导类型
const handleSelect = (rows: User[]) => {
console.log('选中的用户:', rows)
}
const handleRowClick = (row: User) => {
console.log('点击的用户:', row.userName)
}
</script>通用表单组件
FormProps 泛型接口
/**
* 通用表单组件 Props
* @template T 表单数据类型
*/
interface FormProps<T = any> {
/**
* 表单数据
*/
modelValue: T
/**
* 表单模式
*/
mode: 'add' | 'edit' | 'view'
/**
* 表单配置
*/
config: FormConfig[]
/**
* 表单验证规则
*/
rules?: FormRules
/**
* 表单标签宽度
* @default '100px'
*/
labelWidth?: string | number
/**
* 表单尺寸
* @default 'default'
*/
size?: 'large' | 'default' | 'small'
/**
* 是否禁用所有表单项
* @default false
*/
disabled?: boolean
/**
* 是否显示必填星号
* @default true
*/
showRequiredAsterisk?: boolean
}
/**
* 表单配置类型
*/
interface FormConfig {
/** 字段名 */
prop: string
/** 标签文本 */
label: string
/** 组件类型 */
component: 'input' | 'select' | 'date' | 'upload' | 'cascader'
/** 栅格跨度 */
span?: number
/** 组件属性 */
componentProps?: Record<string, any>
/** 是否隐藏 */
hidden?: boolean
}
/**
* 表单验证规则类型
*/
type FormRules = Record<string, FormRule[]>
interface FormRule {
required?: boolean
message?: string
trigger?: 'blur' | 'change'
min?: number
max?: number
pattern?: RegExp
validator?: (rule: any, value: any, callback: any) => void
}FormEmits 泛型接口
/**
* 通用表单组件事件
* @template T 表单数据类型
*/
interface FormEmits<T> {
/** 更新 modelValue */
(e: 'update:modelValue', value: T): void
/** 提交表单 */
(e: 'submit', data: T): void
/** 取消操作 */
(e: 'cancel'): void
/** 重置表单 */
(e: 'reset'): void
}响应式类型定义
SpanType 响应式栅格类型
项目中广泛使用响应式栅格布局,通过 SpanType 类型定义支持多种配置方式。
/**
* 响应式栅格跨度类型
* 支持三种形式:
* 1. 数字: 固定跨度,如 12
* 2. 字符串: 预设配置,如 'auto'
* 3. 响应式对象: 不同屏幕尺寸使用不同跨度
*/
type SpanType =
| number
| 'auto'
| {
/** 超小屏 <768px */
xs?: number
/** 小屏 ≥768px */
sm?: number
/** 中屏 ≥992px */
md?: number
/** 大屏 ≥1200px */
lg?: number
/** 超大屏 ≥1920px */
xl?: number
}使用示例
<template>
<!-- 固定跨度 -->
<AFormInput label="用户名" v-model="form.userName" :span="12" />
<!-- 预设响应式 -->
<AFormInput label="邮箱" v-model="form.email" span="auto" />
<!-- 自定义响应式 -->
<AFormInput
label="手机号"
v-model="form.phone"
:span="{ xs: 24, sm: 24, md: 12, lg: 8, xl: 6 }"
/>
<!-- 部分屏幕自定义,其他使用默认值24 -->
<AFormInput
label="地址"
v-model="form.address"
:span="{ md: 12, lg: 8 }"
/>
</template>ResponsiveMode 响应式模式
/**
* 响应式模式类型
* 用于控制组件根据什么维度进行响应式布局
*/
type ResponsiveMode =
| 'screen' // 基于屏幕尺寸(默认)
| 'container' // 基于容器尺寸(弹窗场景推荐)
| 'modal-size' // 基于 AModal 的 size 属性
/**
* 模态框尺寸类型
*/
type ModalSize = 'small' | 'medium' | 'large' | 'xl'响应式配置示例
<template>
<AModal v-model="visible" size="large">
<el-form :model="form">
<!-- 基于屏幕尺寸响应 -->
<AFormInput
label="用户名"
v-model="form.userName"
responsive-mode="screen"
:span="{ md: 12 }"
/>
<!-- 基于容器尺寸响应 -->
<AFormInput
label="邮箱"
v-model="form.email"
responsive-mode="container"
:span="{ md: 12 }"
/>
<!-- 基于弹窗尺寸响应 -->
<AFormInput
label="手机号"
v-model="form.phone"
responsive-mode="modal-size"
modal-size="large"
:span="12"
/>
</el-form>
</AModal>
</template>Slots 插槽类型定义
组件插槽类型
虽然 Vue 3 没有为插槽提供专门的类型定义语法,但可以通过 JSDoc 注释说明插槽的用途。
/**
* 表单组件插槽
*/
interface FormSlots {
/**
* 默认插槽,表单内容
* @slot default
*/
default?: () => VNode[]
/**
* 表单底部操作按钮
* @slot footer
* @param {Object} scope - 插槽作用域
* @param {Function} scope.submit - 提交表单方法
* @param {Function} scope.reset - 重置表单方法
*/
footer?: (scope: { submit: () => void; reset: () => void }) => VNode[]
/**
* 表单前置内容
* @slot prepend
*/
prepend?: () => VNode[]
/**
* 表单后置内容
* @slot append
*/
append?: () => VNode[]
}作用域插槽使用示例
<template>
<BasicForm v-model="form" :config="formConfig">
<!-- 默认插槽 -->
<template #default>
<AFormInput label="自定义字段" v-model="form.custom" />
</template>
<!-- 作用域插槽 - 自定义底部 -->
<template #footer="{ submit, reset }">
<el-button @click="reset">重置</el-button>
<el-button type="warning" @click="handleSave">保存草稿</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</template>
<!-- 前置内容插槽 -->
<template #prepend>
<el-alert title="请填写完整信息" type="info" :closable="false" />
</template>
</BasicForm>
</template>Expose 暴露方法类型
组件暴露方法的类型定义
使用 defineExpose 暴露组件方法时,可以定义明确的类型。
/**
* 表单组件暴露的方法
*/
interface FormExpose {
/**
* 验证表单
* @returns Promise<boolean> 验证是否通过
*/
validate: () => Promise<boolean>
/**
* 验证指定字段
* @param props 字段名数组
*/
validateField: (props: string[]) => Promise<boolean>
/**
* 重置表单
*/
resetFields: () => void
/**
* 清空验证结果
*/
clearValidate: () => void
/**
* 滚动到指定字段
* @param prop 字段名
*/
scrollToField: (prop: string) => void
}使用组件实例类型
<template>
<BasicForm ref="formRef" v-model="form" :config="formConfig" />
<el-button @click="handleSubmit">提交</el-button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormExpose } from '@/components'
// 使用 InstanceType 获取组件实例类型
const formRef = ref<FormExpose>()
const handleSubmit = async () => {
// TypeScript 会提供完整的类型提示和检查
const valid = await formRef.value?.validate()
if (valid) {
console.log('验证通过')
}
}
</script>最佳实践
1. 使用 interface 而非 type
对于组件 Props,优先使用 interface 而非 type,因为 interface 可以被扩展和合并。
// ✅ 推荐
interface ButtonProps {
type?: ButtonType
size?: ButtonSize
}
// ❌ 不推荐
type ButtonProps = {
type?: ButtonType
size?: ButtonSize
}2. 为所有 Props 添加 JSDoc 注释
interface FormInputProps {
/**
* 输入框类型
* @default 'text'
*/
type?: 'text' | 'password' | 'number'
/**
* 最大长度
* @default 255
*/
maxlength?: number
}3. 使用 withDefaults 提供默认值
const props = withDefaults(defineProps<FormInputProps>(), {
type: 'text',
maxlength: 255,
clearable: true,
disabled: false
})4. 事件定义使用箭头函数语法
// ✅ 推荐 - 更简洁清晰
const emit = defineEmits<{
'update:modelValue': [value: string]
submit: [data: FormData]
cancel: []
}>()
// ❌ 不推荐 - 函数重载语法较繁琐
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'submit', data: FormData): void
(e: 'cancel'): void
}
const emit = defineEmits<Emits>()5. 泛型组件使用 script generic
对于需要泛型的组件,使用 Vue 3.3+ 的 generic 特性:
<script setup lang="ts" generic="T extends Record<string, any>">
interface TableProps {
data: T[]
fields: FieldConfig[]
}
const props = defineProps<TableProps>()
// T 的类型会从使用处自动推导
const emit = defineEmits<{
select: [rows: T[]]
'row-click': [row: T]
}>()
</script>6. 复杂类型抽取为独立类型
// types/form.ts
export interface FormConfig {
prop: string
label: string
component: ComponentType
span?: number
}
export type ComponentType = 'input' | 'select' | 'date' | 'upload'
// 组件中导入使用
import type { FormConfig, ComponentType } from '@/types/form'7. 使用联合类型而非字符串
// ✅ 推荐 - 类型安全,有自动补全
interface ButtonProps {
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
size?: 'large' | 'default' | 'small'
}
// ❌ 不推荐 - 没有类型检查
interface ButtonProps {
type?: string
size?: string
}8. 响应式 Props 使用 toRef
在 setup 中使用 Props 的响应式值时,使用 toRef:
const props = defineProps<ButtonProps>()
// ✅ 推荐 - 保持响应性
const size = toRef(props, 'size')
// ❌ 不推荐 - 丢失响应性
const size = props.size9. 可选链和空值合并优化代码
// ✅ 推荐
const handleSubmit = () => {
formRef.value?.validate()
const width = props.width ?? '100%'
}
// ❌ 不推荐
const handleSubmit = () => {
if (formRef.value) {
formRef.value.validate()
}
const width = props.width ? props.width : '100%'
}10. 使用严格的 null 检查
确保 tsconfig.json 中启用严格模式:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}常见问题
1. Props 默认值设置后仍然是 undefined
问题原因:
- 使用
defineProps时没有使用withDefaults - 默认值是对象或数组,需要使用工厂函数
解决方案:
// ❌ 错误 - 没有默认值
const props = defineProps<{
options?: SelectOption[]
config?: FormConfig
}>()
// ✅ 正确 - 使用 withDefaults
const props = withDefaults(defineProps<{
options?: SelectOption[]
config?: FormConfig
}>(), {
options: () => [], // 数组使用工厂函数
config: () => ({}) // 对象使用工厂函数
})2. 泛型组件类型推导失败
问题原因:
- 没有使用 Vue 3.3+ 的
generic语法 - 泛型约束不正确
解决方案:
<!-- ❌ 错误 - 没有使用 generic -->
<script setup lang="ts">
interface TableProps<T> {
data: T[]
}
// 类型 T 无法推导
</script>
<!-- ✅ 正确 - 使用 generic -->
<script setup lang="ts" generic="T extends Record<string, any>">
interface TableProps {
data: T[]
}
const props = defineProps<TableProps>()
// T 会从使用处自动推导
</script>3. Emits 事件参数类型不匹配
问题原因:
- 事件定义和触发时的参数不一致
- 使用了错误的事件名
解决方案:
// 定义
const emit = defineEmits<{
'update:modelValue': [value: string]
submit: [data: FormData]
}>()
// ✅ 正确使用
emit('update:modelValue', 'hello')
emit('submit', formData)
// ❌ 错误 - 参数类型不匹配
emit('update:modelValue', 123) // 类型错误: number 不能赋值给 string
// ❌ 错误 - 事件名拼写错误
emit('updateModelValue', 'hello') // 类型错误: 事件不存在4. 响应式 Props 在 computed 中丢失响应性
问题原因:
- 直接解构 Props
- 在 computed 外部访问 Props
解决方案:
const props = defineProps<FormProps>()
// ❌ 错误 - 直接解构会丢失响应性
const { label, disabled } = props
// ✅ 正确 - 在 computed 中访问
const isDisabled = computed(() => props.disabled || props.mode === 'view')
// ✅ 正确 - 使用 toRef
const label = toRef(props, 'label')
const disabled = toRef(props, 'disabled')5. 组件 ref 类型不正确
问题原因:
- 使用了错误的实例类型
- 组件没有正确 expose 方法
解决方案:
<template>
<BasicForm ref="formRef" />
</template>
<script setup lang="ts">
// ❌ 错误 - 使用 any
const formRef = ref<any>()
// ❌ 错误 - 使用组件类型
const formRef = ref<typeof BasicForm>()
// ✅ 正确 - 使用 InstanceType 或 expose 的接口类型
import type { FormExpose } from '@/components'
const formRef = ref<FormExpose>()
// 或者使用 InstanceType
const formRef = ref<InstanceType<typeof BasicForm>>()
</script>6. 联合类型 Props 类型收窄问题
问题原因:
- 没有正确使用类型守卫
- TypeScript 无法自动收窄联合类型
解决方案:
interface InputProps {
modelValue: string | number | null
type: 'text' | 'number'
}
const props = defineProps<InputProps>()
// ❌ 错误 - TypeScript 无法确定具体类型
const inputValue = computed({
get: () => props.modelValue,
set: (val) => {
// val 的类型是 string | number | null
}
})
// ✅ 正确 - 使用类型守卫
const inputValue = computed({
get: () => props.modelValue,
set: (val) => {
if (props.type === 'number' && typeof val === 'number') {
emit('update:modelValue', val)
} else if (typeof val === 'string') {
emit('update:modelValue', val)
}
}
})7. 插槽作用域类型定义
问题原因:
- 插槽没有类型提示
- 作用域参数类型不明确
解决方案:
<!-- 定义组件 -->
<template>
<div>
<slot name="item" :row="row" :index="index" />
</div>
</template>
<script setup lang="ts" generic="T">
interface Props {
data: T[]
}
// 虽然无法直接为插槽定义类型,但可以通过 JSDoc 说明
/**
* @slot item - 项目插槽
* @slot item.row - 当前行数据 T
* @slot item.index - 当前行索引 number
*/
</script>
<!-- 使用组件 -->
<template>
<DataList :data="users">
<!-- TypeScript 会推导出 row 的类型为 User -->
<template #item="{ row, index }">
{{ row.userName }} - {{ index }}
</template>
</DataList>
</template>8. Props 验证器中的类型问题
问题原因:
- validator 函数的参数类型不明确
- 返回值不是 boolean
解决方案:
// ❌ 错误 - 参数类型不明确
const props = defineProps({
status: {
type: String,
validator: (value) => {
return ['active', 'inactive'].includes(value)
}
}
})
// ✅ 正确 - 使用 TypeScript 接口定义
interface Props {
status?: 'active' | 'inactive' | 'pending'
}
const props = withDefaults(defineProps<Props>(), {
status: 'pending'
})
// 不需要 validator,类型系统已经保证了类型安全9. 动态组件类型问题
问题原因:
- 动态组件的 Props 类型无法推导
component :is使用时丢失类型
解决方案:
<template>
<component
:is="currentComponent"
v-bind="componentProps"
/>
</template>
<script setup lang="ts">
import { shallowRef, computed } from 'vue'
import FormInput from './FormInput.vue'
import FormSelect from './FormSelect.vue'
type ComponentType = typeof FormInput | typeof FormSelect
const currentComponent = shallowRef<ComponentType>(FormInput)
// ✅ 使用类型断言或条件类型
const componentProps = computed(() => {
if (currentComponent.value === FormInput) {
return {
modelValue: '',
type: 'text'
} as InstanceType<typeof FormInput>['$props']
} else {
return {
modelValue: '',
options: []
} as InstanceType<typeof FormSelect>['$props']
}
})
</script>10. 递归组件类型定义
问题原因:
- 递归组件的 Props 类型引用自身
- TypeScript 无法处理循环引用
解决方案:
/**
* 树形节点数据类型
*/
interface TreeNode {
id: string | number
label: string
children?: TreeNode[] // 递归引用
}
/**
* 树形组件 Props
*/
interface TreeProps {
/**
* 树形数据
*/
data: TreeNode[]
/**
* 节点唯一标识字段名
* @default 'id'
*/
nodeKey?: string
/**
* 默认展开的节点
*/
defaultExpandedKeys?: (string | number)[]
}
// 使用时 TypeScript 能正确处理递归类型
const treeData: TreeNode[] = [
{
id: 1,
label: '一级节点',
children: [
{
id: 11,
label: '二级节点',
children: []
}
]
}
]总结
本文档详细介绍了 RuoYi-Plus-UniApp 前端项目中的组件类型定义规范和最佳实践,涵盖:
- 基础组件类型 - Icon、IconSelect 等基础UI组件的类型定义
- 表单组件类型 - AFormInput、AFormSelect、AFormFileUpload 等完整的表单组件类型
- 业务组件类型 - AModal、AOssMediaManager 等业务组件的详细类型定义
- 泛型组件类型 - 支持泛型的 Table、Form 等通用组件的类型实现
- 响应式类型 - SpanType、ResponsiveMode 等响应式布局相关类型
- Slots 和 Expose - 插槽和暴露方法的类型定义方式
- 最佳实践 - 10 条组件类型定义的最佳实践
- 常见问题 - 10 个常见问题及解决方案
通过遵循这些类型定义规范,可以确保项目中的组件具有良好的类型安全性和开发体验。
