特点与企业级规范:
- 完整支持 ElementPlus 所有常用按钮属性。
- 避免 loading 或 disabled 状态下重复点击,防止业务异常。
- 支持 slot 自定义文本,支持 icon 前置/圆形图标。
- 类型和事件声明完整,符合 TypeScript 企业级规范。
- 样式可扩展,通过 SCSS 全局覆盖或 scoped 自定义。
支持:
loading(加载中状态)disabled(禁用状态)icon(前置/后置图标)size(按钮尺寸:medium | small | mini)type(按钮类型:primary | success | warning | danger | info | text)- 支持自定义事件
click
组件封装:BaseButton.vue
<template>
<el-button
:type="type" <!-- ElementPlus 按钮类型 -->
:size="size" <!-- 按钮尺寸 -->
:loading="loading" <!-- 是否显示加载动画 -->
:disabled="disabled || loading" <!-- 禁用状态,同时避免 loading 状态点击 -->
:icon="icon" <!-- 前置图标 -->
:plain="plain" <!-- 是否朴素按钮 -->
:round="round" <!-- 是否圆角按钮 -->
:circle="circle" <!-- 是否圆形按钮 -->
@click="handleClick"
>
<slot /> <!-- 按钮文本通过 slot 传入 -->
</el-button>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from 'vue'
interface Props {
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text'
size?: 'medium' | 'small' | 'mini'
loading?: boolean
disabled?: boolean
icon?: string
plain?: boolean
round?: boolean
circle?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'click', event: MouseEvent): void
}>()
/**
* 点击事件封装
* 避免在 loading 状态下重复触发
*/
function handleClick(event: MouseEvent) {
if (!props.loading && !props.disabled) {
emit('click', event)
}
}
</script>
<style lang="scss" scoped>
/* 可扩展自定义按钮样式 */
</style>页面使用示例:DemoPage.vue
<template>
<div class="demo-buttons">
<!-- 普通按钮 -->
<BaseButton @click="handleClick">默认按钮</BaseButton>
<!-- 主色按钮 -->
<BaseButton type="primary" @click="handleClick">主色按钮</BaseButton>
<!-- 带图标按钮 -->
<BaseButton type="success" icon="Check" @click="handleClick">
成功按钮
</BaseButton>
<!-- 加载中按钮 -->
<BaseButton type="warning" :loading="loading" @click="startLoading">
加载中按钮
</BaseButton>
<!-- 禁用按钮 -->
<BaseButton type="danger" :disabled="true">禁用按钮</BaseButton>
<!-- 小尺寸圆角按钮 -->
<BaseButton type="info" size="small" round>小圆角按钮</BaseButton>
<!-- 圆形按钮 -->
<BaseButton type="primary" circle icon="el-icon-search" @click="handleClick" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseButton from '@/components/BaseButton.vue'
const loading = ref(false)
function handleClick() {
console.log('按钮点击事件触发')
}
/**
* 模拟异步操作,触发 loading 状态
*/
function startLoading() {
loading.value = true
setTimeout(() => {
loading.value = false
console.log('异步操作完成')
}, 2000)
}
</script>
<style lang="scss" scoped>
.demo-buttons {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
</style>✅ 企业级封装特点:
- 完整支持 ElementPlus 表单验证体系。
- 双向绑定
v-model,兼容 Vue3 + TS。 - 支持前置/后置图标、TextArea、清除按钮。
- 可自定义错误提示或使用 Form 校验规则。
- 易于在大型项目中复用,表单项统一样式和逻辑。
功能包括:
- 普通 Input / Textarea
- 前置/后置图标
- 表单校验(必填、正则、自定义校验)
- 自定义提示信息
- 支持
v-model双向绑定
组件封装:BaseInput.vue
<template>
<el-form-item
:label="label" <!-- 表单项标题 -->
:prop="prop" <!-- 用于 Form 校验规则 -->
:rules="rules" <!-- 校验规则数组 -->
>
<el-input
v-model="inputValue"
:type="type" <!-- 输入类型 text/password/textarea 等 -->
:placeholder="placeholder"
:rows="rows" <!-- textarea行数 -->
:prefix-icon="prefixIcon"
:suffix-icon="suffixIcon"
:disabled="disabled"
:clearable="clearable"
@change="$emit('change', $event)"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
</el-input>
<!-- 自定义提示 -->
<template #error>
<span v-if="errorMessage" class="input-error">{{ errorMessage }}</span>
</template>
</el-form-item>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import type { FormItemRule } from 'element-plus'
/**
* 组件 Props
*/
interface Props {
modelValue: string | number
label?: string
prop?: string // 用于 Form 校验
placeholder?: string
type?: 'text' | 'password' | 'textarea' | 'number'
rows?: number // textarea 行数
prefixIcon?: string
suffixIcon?: string
disabled?: boolean
clearable?: boolean
rules?: FormItemRule[] // element-plus 表单规则
errorMessage?: string // 自定义错误信息
}
const props = defineProps<Props>()
/**
* 组件事件
*/
const emit = defineEmits<{
(e: 'update:modelValue', value: string | number): void
(e: 'change', value: string | number): void
(e: 'focus', event: FocusEvent): void
(e: 'blur', event: FocusEvent): void
}>()
/**
* 双向绑定
*/
const inputValue = computed({
get: () => props.modelValue,
set: (val: string | number) => {
emit('update:modelValue', val)
}
})
</script>
<style lang="scss" scoped>
.input-error {
color: #f56c6c;
font-size: 12px;
}
</style>页面使用示例:DemoInputPage.vue
<template>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<!-- 普通输入 -->
<BaseInput
v-model="formData.username"
label="用户名"
placeholder="请输入用户名"
:rules="[ { required: true, message: '用户名不能为空', trigger: 'blur' } ]"
/>
<!-- 密码输入 -->
<BaseInput
v-model="formData.password"
label="密码"
type="password"
placeholder="请输入密码"
:rules="[ { required: true, message: '密码不能为空', trigger: 'blur' } ]"
suffix-icon="View"
/>
<!-- 带前置图标输入 -->
<BaseInput
v-model="formData.email"
label="邮箱"
placeholder="请输入邮箱"
prefix-icon="Message"
:rules="[ { type: 'email', message: '邮箱格式不正确', trigger: 'blur' } ]"
/>
<!-- 多行文本 -->
<BaseInput
v-model="formData.description"
label="描述"
type="textarea"
:rows="3"
placeholder="请输入描述"
/>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseInput from '@/components/BaseInput.vue'
import type { FormInstance } from 'element-plus'
const formRef = ref<FormInstance>()
const formData = ref({
username: '',
password: '',
email: '',
description: ''
})
const formRules = {
username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }]
}
/**
* 提交表单
*/
function submitForm() {
formRef.value?.validate((valid) => {
if (valid) {
console.log('表单校验通过', formData.value)
} else {
console.log('表单校验失败')
}
})
}
/**
* 重置表单
*/
function resetForm() {
formRef.value?.resetFields()
}
</script>
<style lang="scss" scoped>
.el-form-item {
margin-bottom: 16px;
}
</style>✅ 企业级封装特点:
- 完整支持 ElementPlus Dialog 属性,支持 destroyOnClose、宽度、点击遮罩关闭等。
- 支持弹窗内容通过 slot 注入,自由嵌入表单或其他组件。
- 支持
v-model:modelValue双向控制显示。 - 支持确定按钮 loading,防止重复提交。
- 提供 cancel、confirm、close 三个事件,方便业务处理。
- TS 类型完整,易于大型企业项目复用。
功能包括:
- 通用 Dialog 封装
- 可自定义标题
- 支持表单嵌入 slot
- 支持确认 / 取消事件
- 支持
v-model:visible双向控制弹窗显示 - 支持确定按钮 loading 状态
组件封装:BaseDialog.vue
<template>
<el-dialog
:model-value="visible" <!-- 弹窗显示控制 -->
:title="title" <!-- 弹窗标题 -->
:width="width" <!-- 弹窗宽度 -->
:destroy-on-close="destroyOnClose" <!-- 关闭时销毁内容 -->
:close-on-click-modal="closeOnClickModal"
:before-close="handleBeforeClose"
@close="onClose"
>
<!-- 弹窗内容插槽 -->
<slot name="body" />
<!-- 弹窗底部操作按钮 -->
<template #footer>
<el-button @click="handleCancel" :disabled="confirmLoading">取消</el-button>
<el-button
type="primary"
:loading="confirmLoading"
@click="handleConfirm"
>
确定
</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref } from 'vue'
interface Props {
modelValue: boolean
title?: string
width?: string
destroyOnClose?: boolean
closeOnClickModal?: boolean
confirmLoading?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'confirm'): void
(e: 'cancel'): void
(e: 'close'): void
}>()
const visible = ref(props.modelValue)
/**
* 同步外部 v-model
*/
watch(
() => props.modelValue,
(val) => {
visible.value = val
}
)
/**
* 关闭前回调(ElementPlus 支持)
*/
function handleBeforeClose(done: () => void) {
handleCancel()
done()
}
/**
* 点击取消
*/
function handleCancel() {
emit('cancel')
emit('update:modelValue', false)
}
/**
* 点击确定
*/
function handleConfirm() {
emit('confirm')
}
/**
* 弹窗关闭事件
*/
function onClose() {
emit('close')
}
</script>
<style lang="scss" scoped>
/* 可扩展自定义弹窗样式 */
</style>页面使用示例:DemoDialogPage.vue
<template>
<div class="demo-dialog">
<el-button type="primary" @click="showDialog = true">打开弹窗</el-button>
<!-- 弹窗封装组件 -->
<BaseDialog
v-model:modelValue="showDialog"
title="编辑用户"
:width="'500px'"
:confirmLoading="loading"
@confirm="submitForm"
@cancel="handleCancel"
>
<!-- 弹窗内容插槽 -->
<template #body>
<el-form ref="formRef" :model="formData" label-width="100px">
<BaseInput
v-model="formData.username"
label="用户名"
placeholder="请输入用户名"
:rules="[ { required: true, message: '用户名不能为空', trigger: 'blur' } ]"
/>
<BaseInput
v-model="formData.email"
label="邮箱"
placeholder="请输入邮箱"
prefix-icon="Message"
:rules="[ { type: 'email', message: '邮箱格式不正确', trigger: 'blur' } ]"
/>
</el-form>
</template>
</BaseDialog>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseDialog from '@/components/BaseDialog.vue'
import BaseInput from '@/components/BaseInput.vue'
import type { FormInstance } from 'element-plus'
const showDialog = ref(false)
const loading = ref(false)
const formRef = ref<FormInstance>()
const formData = ref({
username: '',
email: ''
})
/**
* 弹窗确认事件
*/
function submitForm() {
formRef.value?.validate((valid) => {
if (valid) {
loading.value = true
setTimeout(() => {
loading.value = false
showDialog.value = false
console.log('表单提交成功', formData.value)
}, 1500)
} else {
console.log('表单校验失败')
}
})
}
/**
* 弹窗取消事件
*/
function handleCancel() {
console.log('弹窗已取消')
}
</script>
<style lang="scss" scoped>
.demo-dialog {
.el-button {
margin-bottom: 16px;
}
}
</style>功能点如下:
- 动态表单项,可新增/删除行
- 支持表单校验(必填、正则、自定义)
- 支持
v-model双向绑定整个表单数组 - 可自定义每行表单内容,通过 slot 插入
- 支持表单项序号显示
组件封装:DynamicForm.vue
<template>
<div class="dynamic-form">
<el-form
:model="internalData"
:rules="rules"
ref="formRef"
label-width="100px"
>
<div v-for="(item, index) in internalData" :key="item.id" class="form-row">
<div class="row-index">{{ index + 1 }}</div>
<!-- 每行表单内容插槽 -->
<slot
name="row"
:item="item"
:index="index"
:update-item="updateItem"
>
<!-- 默认表单项示例 -->
<el-form-item
:prop="`${index}.name`"
label="名称"
>
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
</slot>
<!-- 行操作 -->
<div class="row-actions">
<el-button type="danger" size="mini" @click="removeRow(index)">删除</el-button>
</div>
</div>
<!-- 新增按钮 -->
<el-form-item>
<el-button type="primary" size="mini" @click="addRow">新增行</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, reactive, watch } from 'vue'
import type { FormInstance } from 'element-plus'
interface ItemType {
id: string
[key: string]: any
}
interface Props {
modelValue: ItemType[]
rules?: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: ItemType[]): void
}>()
const formRef = ref<FormInstance>()
const internalData = reactive<ItemType[]>([])
/**
* 初始化 internalData
*/
watch(
() => props.modelValue,
(val) => {
internalData.splice(0, internalData.length, ...(val || []))
},
{ immediate: true }
)
/**
* 新增行
*/
function addRow() {
const newItem: ItemType = { id: `${Date.now()}-${Math.random()}` }
internalData.push(newItem)
emit('update:modelValue', internalData)
}
/**
* 删除行
*/
function removeRow(index: number) {
internalData.splice(index, 1)
emit('update:modelValue', internalData)
}
/**
* 更新行数据(用于 slot 回调)
*/
function updateItem(index: number, newData: Partial<ItemType>) {
internalData[index] = { ...internalData[index], ...newData }
emit('update:modelValue', internalData)
}
/**
* 校验表单
*/
function validate(callback: (valid: boolean) => void) {
formRef.value?.validate(callback)
}
export { formRef, validate }
</script>
<style lang="scss" scoped>
.dynamic-form {
.form-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
.row-index {
width: 24px;
text-align: center;
line-height: 32px;
}
.row-actions {
display: flex;
align-items: center;
margin-top: 4px;
}
}
}
</style>页面使用示例:DemoDynamicFormPage.vue
<template>
<div class="demo-dynamic-form">
<DynamicForm
v-model="formData"
:rules="formRules"
ref="dynamicFormRef"
>
<template #row="{ item, index, updateItem }">
<el-form-item
:prop="`${index}.name`"
label="名称"
>
<el-input
v-model="item.name"
placeholder="请输入名称"
@input="val => updateItem(index, { name: val })"
/>
</el-form-item>
<el-form-item
:prop="`${index}.email`"
label="邮箱"
>
<el-input
v-model="item.email"
placeholder="请输入邮箱"
@input="val => updateItem(index, { email: val })"
/>
</el-form-item>
</template>
</DynamicForm>
<el-button type="primary" @click="submitForm">提交表单</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import DynamicForm from '@/components/DynamicForm.vue'
import type { FormInstance } from 'element-plus'
const dynamicFormRef = ref<FormInstance>()
const formData = ref([{ id: '1', name: '', email: '' }])
const formRules = {
'0.name': [{ required: true, message: '名称不能为空', trigger: 'blur' }],
'0.email': [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }]
}
/**
* 提交表单
*/
function submitForm() {
dynamicFormRef.value?.validate((valid) => {
if (valid) {
console.log('动态表单数据:', formData.value)
} else {
console.log('动态表单校验失败')
}
})
}
</script>
<style lang="scss" scoped>
.demo-dynamic-form {
.el-button {
margin-top: 16px;
}
}
</style>✅ 企业级封装特点:
- 支持动态行增删,适合复杂表单场景(如联系人列表、商品规格等)。
- 支持表单校验和 ElementPlus Form 规则结合使用。
- 支持通过 slot 自定义每行表单项,灵活性高。
- 使用 TS + reactive + v-model 完整双向绑定,类型安全。
- 易于在大型项目中复用,结构清晰、操作统一。
功能点如下:
- 动态列配置,可自定义列标题、字段、宽度
- 支持多表头(表头合并)
- 支持排序、筛选
- 支持分页
- 支持行操作按钮(编辑/删除等)
- 支持
v-model:data双向绑定表格数据
组件封装:BaseTable.vue
<template>
<div class="base-table">
<el-table
:data="data"
border
stripe
style="width: 100%"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
>
<!-- 动态列渲染 -->
<template v-for="col in columns">
<el-table-column
v-if="!col.children"
:key="col.prop"
:prop="col.prop"
:label="col.label"
:width="col.width"
:sortable="col.sortable"
:filters="col.filters"
:filter-method="col.filterMethod"
/>
<!-- 支持多表头(children) -->
<el-table-column
v-else
:key="col.label"
:label="col.label"
>
<el-table-column
v-for="child in col.children"
:key="child.prop"
:prop="child.prop"
:label="child.label"
:width="child.width"
:sortable="child.sortable"
:filters="child.filters"
:filter-method="child.filterMethod"
/>
</el-table-column>
</template>
<!-- 行操作按钮插槽 -->
<el-table-column label="操作" fixed="right" width="150">
<template #default="{ row, $index }">
<slot name="actions" :row="row" :index="$index">
<!-- 默认操作 -->
<el-button type="primary" size="mini" @click="editRow(row)">编辑</el-button>
<el-button type="danger" size="mini" @click="deleteRow(row)">删除</el-button>
</slot>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-if="pagination"
class="mt-2"
background
layout="total, prev, pager, next, jumper"
:page-size="pagination.pageSize"
:current-page="pagination.currentPage"
:total="pagination.total"
@current-change="handlePageChange"
/>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits } from 'vue'
interface ColumnType {
label: string
prop?: string
width?: string | number
sortable?: boolean
filters?: { text: string; value: any }[]
filterMethod?: (value: any, row: any, column: any) => boolean
children?: ColumnType[]
}
interface PaginationType {
currentPage: number
pageSize: number
total: number
}
interface Props {
data: any[]
columns: ColumnType[]
pagination?: PaginationType
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'edit', row: any): void
(e: 'delete', row: any): void
(e: 'page-change', page: number): void
(e: 'sort-change', sort: any): void
(e: 'filter-change', filter: any): void
}>()
/**
* 默认编辑行
*/
function editRow(row: any) {
emit('edit', row)
}
/**
* 默认删除行
*/
function deleteRow(row: any) {
emit('delete', row)
}
/**
* 分页变化
*/
function handlePageChange(page: number) {
emit('page-change', page)
}
/**
* 排序变化
*/
function handleSortChange(sort: any) {
emit('sort-change', sort)
}
/**
* 过滤变化
*/
function handleFilterChange(filter: any) {
emit('filter-change', filter)
}
</script>
<style lang="scss" scoped>
.base-table {
.mt-2 {
margin-top: 12px;
}
}
</style>页面使用示例:DemoTablePage.vue
<template>
<div class="demo-table">
<BaseTable
:data="tableData"
:columns="tableColumns"
:pagination="pagination"
@edit="handleEdit"
@delete="handleDelete"
@page-change="handlePageChange"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
>
<template #actions="{ row }">
<el-button type="primary" size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="mini" @click="handleDelete(row)">删除</el-button>
</template>
</BaseTable>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseTable from '@/components/BaseTable.vue'
const tableData = ref([
{ id: 1, name: '张三', age: 28, gender: '男' },
{ id: 2, name: '李四', age: 22, gender: '女' },
{ id: 3, name: '王五', age: 35, gender: '男' }
])
const tableColumns = ref([
{ label: 'ID', prop: 'id', width: 80 },
{ label: '姓名', prop: 'name', width: 120, sortable: true },
{ label: '年龄', prop: 'age', width: 100, sortable: true },
{
label: '性别信息',
children: [
{ label: '性别', prop: 'gender', width: 100 },
{ label: '备注', prop: 'remark', width: 150 }
]
}
])
const pagination = ref({
currentPage: 1,
pageSize: 10,
total: 50
})
function handleEdit(row: any) {
console.log('编辑行:', row)
}
function handleDelete(row: any) {
console.log('删除行:', row)
}
function handlePageChange(page: number) {
console.log('分页切换到第', page, '页')
}
function handleSortChange(sort: any) {
console.log('排序变化:', sort)
}
function handleFilterChange(filter: any) {
console.log('过滤变化:', filter)
}
</script>
<style lang="scss" scoped>
.demo-table {
padding: 12px;
}
</style>✅ 企业级封装特点:
- 完整支持动态列配置和多表头(children)
- 支持排序、过滤事件回调
- 支持分页,统一样式
- 支持行操作按钮自定义插槽
- TS 类型完整,易于在大型项目中复用
- 适用于后台管理系统、数据统计、企业报表等场景
功能点如下:
- 封装 ElementPlus Pagination
- 统一样式(背景色、间距、尺寸等)
- 支持当前页码、每页条数、总条数配置
- 支持
page-change事件回调 - 支持跳转页码输入(jumper)
- 支持
page-size-change事件(可选,每页条数变化)
组件封装:BasePagination.vue
<template>
<div class="base-pagination">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:current-page="currentPage"
:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch } from 'vue'
interface Props {
currentPage?: number
pageSize?: number
total?: number
pageSizes?: number[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:currentPage', page: number): void
(e: 'update:pageSize', size: number): void
(e: 'page-change', page: number): void
(e: 'size-change', size: number): void
}>()
const currentPage = ref(props.currentPage || 1)
const pageSize = ref(props.pageSize || 10)
const total = ref(props.total || 0)
const pageSizes = ref(props.pageSizes || [10, 20, 50, 100])
/**
* 监听外部 props 变化
*/
watch(
() => props.currentPage,
(val) => {
if (val !== undefined) currentPage.value = val
}
)
watch(
() => props.pageSize,
(val) => {
if (val !== undefined) pageSize.value = val
}
)
watch(
() => props.total,
(val) => {
if (val !== undefined) total.value = val
}
)
/**
* 当前页变化
*/
function handleCurrentChange(page: number) {
currentPage.value = page
emit('update:currentPage', page)
emit('page-change', page)
}
/**
* 每页条数变化
*/
function handleSizeChange(size: number) {
pageSize.value = size
emit('update:pageSize', size)
emit('size-change', size)
}
</script>
<style lang="scss" scoped>
.base-pagination {
display: flex;
justify-content: flex-end;
padding: 12px 0;
.el-pagination {
font-size: 14px;
}
}
</style>页面使用示例:DemoPaginationPage.vue
<template>
<div class="demo-pagination">
<BasePagination
v-model:currentPage="pagination.currentPage"
v-model:pageSize="pagination.pageSize"
:total="pagination.total"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
<p>当前页: {{ pagination.currentPage }}</p>
<p>每页条数: {{ pagination.pageSize }}</p>
<p>总条数: {{ pagination.total }}</p>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import BasePagination from '@/components/BasePagination.vue'
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 123
})
function handlePageChange(page: number) {
console.log('切换到第', page, '页')
pagination.currentPage = page
}
function handleSizeChange(size: number) {
console.log('每页条数修改为', size)
pagination.pageSize = size
}
</script>
<style lang="scss" scoped>
.demo-pagination {
padding: 12px;
}
</style>✅ 企业级封装特点:
- 完全封装 ElementPlus Pagination,统一布局和样式
- 支持跳转页码输入、每页条数选择
- TS 类型安全,支持 v-model 双向绑定
- 提供 page-change 和 size-change 事件回调,方便业务逻辑处理
- 易于在企业后台系统列表页中复用
功能点如下:
- 支持单选 / 多选
- 支持远程搜索(输入关键字请求数据)
- 支持动态加载选项(prop 数据驱动)
- 支持 placeholder、自定义清空按钮
- 支持
v-model双向绑定值 - 支持事件回调:
change、search
组件封装:BaseSelect.vue
<template>
<el-select
v-model="modelValue"
:multiple="multiple"
:placeholder="placeholder"
:filterable="remote"
:clearable="clearable"
:loading="loading"
@change="handleChange"
@visible-change="handleVisibleChange"
@clear="handleClear"
@input="handleInput"
style="width: 100%;"
>
<el-option
v-for="option in optionsData"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch } from 'vue'
interface OptionType {
label: string
value: string | number
}
interface Props {
modelValue: string | number | (string | number)[]
options?: OptionType[]
placeholder?: string
multiple?: boolean
remote?: boolean
loading?: boolean
clearable?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void
(e: 'change', value: any): void
(e: 'search', keyword: string): void
(e: 'clear'): void
}>()
const modelValue = ref(props.modelValue)
const optionsData = ref<OptionType[]>(props.options || [])
const loading = ref(props.loading || false)
/**
* 监听外部 modelValue 变化
*/
watch(
() => props.modelValue,
(val) => {
modelValue.value = val
}
)
/**
* 监听外部 options 更新
*/
watch(
() => props.options,
(val) => {
optionsData.value = val || []
}
)
/**
* 值变化
*/
function handleChange(value: any) {
emit('update:modelValue', value)
emit('change', value)
}
/**
* 下拉框输入搜索关键字(远程搜索)
*/
function handleInput(keyword: string) {
if (props.remote) {
emit('search', keyword)
}
}
/**
* 可见性变化,用于懒加载
*/
function handleVisibleChange(visible: boolean) {
// 可在 visible 为 true 时触发远程加载
}
/**
* 清空
*/
function handleClear() {
emit('clear')
}
</script>
<style lang="scss" scoped>
/* 可扩展自定义样式 */
</style>页面使用示例:DemoSelectPage.vue
<template>
<div class="demo-select">
<!-- 普通单选 -->
<BaseSelect
v-model="selected"
:options="options"
placeholder="请选择"
@change="handleChange"
/>
<!-- 多选 -->
<BaseSelect
v-model="multiSelected"
:options="options"
placeholder="请选择多个"
:multiple="true"
@change="handleMultiChange"
/>
<!-- 远程搜索 -->
<BaseSelect
v-model="remoteSelected"
placeholder="输入搜索"
:remote="true"
:loading="loading"
:options="remoteOptions"
@search="handleSearch"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseSelect from '@/components/BaseSelect.vue'
const options = ref([
{ label: '选项A', value: 'A' },
{ label: '选项B', value: 'B' },
{ label: '选项C', value: 'C' }
])
const selected = ref('')
const multiSelected = ref<string[]>([])
/** 远程搜索相关 */
const remoteSelected = ref('')
const remoteOptions = ref<{ label: string; value: string }[]>([])
const loading = ref(false)
/**
* 远程搜索处理函数
*/
function handleSearch(keyword: string) {
loading.value = true
setTimeout(() => {
remoteOptions.value = [
{ label: `${keyword} 1`, value: `${keyword}-1` },
{ label: `${keyword} 2`, value: `${keyword}-2` }
]
loading.value = false
}, 1000)
}
/** 单选/多选变化事件 */
function handleChange(value: any) {
console.log('单选选中:', value)
}
function handleMultiChange(value: any) {
console.log('多选选中:', value)
}
</script>
<style lang="scss" scoped>
.demo-select {
display: flex;
flex-direction: column;
gap: 12px;
width: 300px;
}
</style>✅ 企业级封装特点:
- 支持单选、多选和远程搜索,满足复杂业务需求
- 支持动态 options,输入关键字触发远程加载
- TS 类型完整,事件声明明确,易于大型项目复用
- 统一样式和交互,兼容 ElementPlus 2.13.0
- 支持清空、placeholder、loading 等常用属性
功能点如下:
- 支持单文件 / 多文件上传
- 支持图片预览
- 支持文件类型、大小限制
- 支持上传状态(loading、成功、失败)
- 支持
v-model双向绑定上传文件列表 - 支持事件回调:
success、error、remove - 支持 ElementPlus Upload 属性扩展
组件封装:BaseUpload.vue
<template>
<el-upload
class="base-upload"
action="" <!-- 后端上传 URL,可通过 prop 或 slot 自定义 -->
:file-list="fileList"
:multiple="multiple"
:limit="limit"
:accept="accept"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-success="handleSuccess"
:on-error="handleError"
:auto-upload="autoUpload"
:before-upload="beforeUpload"
list-type="picture-card"
>
<template #default>
<el-button size="small" type="primary">上传文件</el-button>
</template>
</el-upload>
<!-- 图片预览 -->
<el-dialog :visible.sync="previewVisible" width="50%">
<img width="100%" :src="previewImage" alt="预览" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, defineProps, defineEmits, watch } from 'vue'
import type { UploadFile } from 'element-plus'
interface Props {
modelValue: UploadFile[]
multiple?: boolean
limit?: number
accept?: string
autoUpload?: boolean
maxSizeMB?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', fileList: UploadFile[]): void
(e: 'success', file: UploadFile, response: any): void
(e: 'error', file: UploadFile, err: any): void
(e: 'remove', file: UploadFile, fileList: UploadFile[]): void
}>()
const fileList = ref<UploadFile[]>(props.modelValue || [])
const previewVisible = ref(false)
const previewImage = ref('')
/**
* 监听外部 v-model 更新
*/
watch(
() => props.modelValue,
(val) => {
fileList.value = val || []
}
)
/**
* 上传前校验文件大小
*/
function beforeUpload(file: UploadFile) {
if (props.maxSizeMB && file.size / 1024 / 1024 > props.maxSizeMB) {
ElMessage.error(`文件大小不能超过 ${props.maxSizeMB} MB`)
return false
}
return true
}
/**
* 文件上传成功
*/
function handleSuccess(file: UploadFile, response: any) {
emit('success', file, response)
emit('update:modelValue', fileList.value)
}
/**
* 文件上传失败
*/
function handleError(file: UploadFile, err: any) {
emit('error', file, err)
}
/**
* 文件移除
*/
function handleRemove(file: UploadFile, fileListInner: UploadFile[]) {
emit('remove', file, fileListInner)
emit('update:modelValue', fileListInner)
}
/**
* 预览图片
*/
function handlePreview(file: UploadFile) {
if (file.url) {
previewImage.value = file.url
previewVisible.value = true
} else if (file.raw) {
previewImage.value = URL.createObjectURL(file.raw)
previewVisible.value = true
}
}
</script>
<style lang="scss" scoped>
.base-upload {
.el-upload {
width: 200px;
}
}
</style>页面使用示例:DemoUploadPage.vue
<template>
<div class="demo-upload">
<BaseUpload
v-model="fileList"
:multiple="true"
:limit="5"
accept=".jpg,.png,.pdf"
:maxSizeMB="2"
@success="handleSuccess"
@error="handleError"
@remove="handleRemove"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseUpload from '@/components/BaseUpload.vue'
import type { UploadFile } from 'element-plus'
const fileList = ref<UploadFile[]>([])
function handleSuccess(file: UploadFile, response: any) {
console.log('上传成功', file, response)
}
function handleError(file: UploadFile, err: any) {
console.error('上传失败', file, err)
}
function handleRemove(file: UploadFile, fileListInner: UploadFile[]) {
console.log('移除文件', file, fileListInner)
}
</script>
<style lang="scss" scoped>
.demo-upload {
padding: 12px;
}
</style>✅ 企业级封装特点:
- 支持单文件 / 多文件上传,支持 ElementPlus Upload 所有常用属性
- 支持图片预览(本地或远程 URL)
- 支持文件类型、文件大小限制
- 支持上传状态回调(成功、失败、删除)
- 支持 v-model 双向绑定上传列表
- TS 类型安全,可在企业后台系统文件管理、商品图片上传等场景复用
功能点如下:
- 支持单日期选择
DatePicker - 支持日期范围选择
RangePicker - 支持快捷选项(如今天、最近7天、最近30天)
- 支持禁用日期(
disabledDate) - 支持时间选择(可选)
- 支持
v-model双向绑定值 - 支持事件回调:
change、clear
组件封装:BaseDatePicker.vue
<template>
<el-date-picker
v-model="modelValue"
:type="type"
:placeholder="placeholder"
:format="format"
:value-format="valueFormat"
:disabled-date="disabledDate"
:shortcut="shortcuts"
:clearable="clearable"
:editable="editable"
:picker-options="pickerOptions"
style="width: 100%;"
@change="handleChange"
@clear="handleClear"
/>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch } from 'vue'
import type { Dayjs } from 'dayjs'
interface ShortcutType {
text: string
onClick: (picker: any) => void
}
interface Props {
modelValue: string | [string, string] | null
type?: 'date' | 'daterange' | 'datetime' | 'datetimerange'
placeholder?: string
format?: string
valueFormat?: string
shortcuts?: ShortcutType[]
clearable?: boolean
editable?: boolean
pickerOptions?: Record<string, any>
disabledDate?: (date: Date) => boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: any): void
(e: 'change', value: any): void
(e: 'clear'): void
}>()
const modelValue = ref(props.modelValue)
/**
* 监听外部 v-model 更新
*/
watch(
() => props.modelValue,
(val) => {
modelValue.value = val
}
)
/**
* 值变化
*/
function handleChange(value: any) {
emit('update:modelValue', value)
emit('change', value)
}
/**
* 清空事件
*/
function handleClear() {
emit('clear')
}
</script>
<style lang="scss" scoped>
/* 可扩展自定义样式 */
</style>页面使用示例:DemoDatePickerPage.vue
<template>
<div class="demo-date-picker">
<!-- 单日期 -->
<BaseDatePicker
v-model="singleDate"
type="date"
placeholder="请选择日期"
:shortcuts="singleShortcuts"
/>
<!-- 日期范围 -->
<BaseDatePicker
v-model="rangeDate"
type="daterange"
placeholder="请选择日期范围"
:shortcuts="rangeShortcuts"
:disabled-date="disabledFuture"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseDatePicker from '@/components/BaseDatePicker.vue'
import dayjs from 'dayjs'
const singleDate = ref<string | null>(null)
const rangeDate = ref<[string, string] | null>(null)
/**
* 单日期快捷选项
*/
const singleShortcuts = [
{
text: '今天',
onClick: (picker: any) => {
picker.$emit('pick', dayjs().format('YYYY-MM-DD'))
}
}
]
/**
* 日期范围快捷选项
*/
const rangeShortcuts = [
{
text: '最近7天',
onClick: (picker: any) => {
const end = dayjs()
const start = dayjs().subtract(6, 'day')
picker.$emit('pick', [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')])
}
},
{
text: '最近30天',
onClick: (picker: any) => {
const end = dayjs()
const start = dayjs().subtract(29, 'day')
picker.$emit('pick', [start.format('YYYY-MM-DD'), end.format('YYYY-MM-DD')])
}
}
]
/**
* 禁用未来日期
*/
function disabledFuture(date: Date) {
return date.getTime() > new Date().getTime()
}
</script>
<style lang="scss" scoped>
.demo-date-picker {
display: flex;
flex-direction: column;
gap: 12px;
width: 300px;
}
</style>✅ 企业级封装特点:
- 单日期和日期范围统一封装,方便在项目中复用
- 支持快捷选项,可自定义最近7天、今天等业务快捷
- 支持禁用特定日期(如未来日期、过去日期)
- TS 类型完整,事件声明清晰
- 支持
v-model双向绑定,统一 ElementPlus DatePicker API - 易于在后台系统、报表筛选、数据分析页面中使用
功能点如下:
- 封装 ElementPlus Switch
- 支持
v-model双向绑定 - 支持禁用状态、大小、文本显示(on/off)
- 支持事件回调:
change - 支持统一样式,便于大型项目复用
组件封装:BaseSwitch.vue
<template>
<el-switch
v-model="modelValue"
:disabled="disabled"
:active-text="activeText"
:inactive-text="inactiveText"
:active-color="activeColor"
:inactive-color="inactiveColor"
:width="width"
@change="handleChange"
/>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch } from 'vue'
interface Props {
modelValue: boolean
disabled?: boolean
activeText?: string
inactiveText?: string
activeColor?: string
inactiveColor?: string
width?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'change', value: boolean): void
}>()
const modelValue = ref(props.modelValue)
/**
* 监听外部 v-model 更新
*/
watch(
() => props.modelValue,
(val) => {
modelValue.value = val
}
)
/**
* 值变化
*/
function handleChange(value: boolean) {
emit('update:modelValue', value)
emit('change', value)
}
</script>
<style lang="scss" scoped>
/* 可统一样式 */
</style>页面使用示例:DemoSwitchPage.vue
<template>
<div class="demo-switch">
<p>普通开关:</p>
<BaseSwitch v-model="switchValue" @change="handleChange" />
<p>禁用开关:</p>
<BaseSwitch v-model="switchValueDisabled" :disabled="true" />
<p>自定义文本与颜色:</p>
<BaseSwitch
v-model="switchCustom"
active-text="开启"
inactive-text="关闭"
active-color="#13ce66"
inactive-color="#ff4949"
width="70"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseSwitch from '@/components/BaseSwitch.vue'
const switchValue = ref(false)
const switchValueDisabled = ref(true)
const switchCustom = ref(false)
function handleChange(value: boolean) {
console.log('开关状态:', value)
}
</script>
<style lang="scss" scoped>
.demo-switch {
display: flex;
flex-direction: column;
gap: 12px;
width: 300px;
}
</style>✅ 企业级封装特点:
- 完全封装 ElementPlus Switch,统一使用 v-model
- 支持禁用状态、自定义宽度、文本、颜色
- TS 类型安全,事件声明清晰
- 统一样式,可在后台系统、表单、控制面板中复用
- 支持 change 事件,方便业务逻辑处理
功能点如下:
- 封装 ElementPlus
Tag和Badge - 支持动态文本内容
- 支持颜色(状态颜色)
- 支持状态标识(例如 success / warning / error / info)
- 支持可关闭(Tag 可选)
- 支持事件回调:
close
组件封装:BaseTag.vue
<template>
<el-tag
:type="type"
:effect="effect"
:closable="closable"
:disable-transitions="disableTransitions"
:hit="hit"
:color="color"
@close="handleClose"
>
<slot>{{ text }}</slot>
</el-tag>
<el-badge
v-if="badgeCount !== undefined"
:value="badgeCount"
:type="badgeType"
class="ml-2"
/>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, ref, watch } from 'vue'
interface Props {
text?: string // 标签文本
type?: 'success' | 'warning' | 'info' | 'danger' // 标签类型
color?: string // 自定义颜色
effect?: 'dark' | 'light' | 'plain' // 标签效果
closable?: boolean // 是否可关闭
disableTransitions?: boolean // 禁用动画
hit?: boolean // 高亮样式
badgeCount?: number // 徽标数量
badgeType?: 'primary' | 'success' | 'warning' | 'danger' | 'info' // 徽标状态颜色
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'close'): void
}>()
const text = ref(props.text)
const type = ref(props.type || 'info')
const color = ref(props.color)
const effect = ref(props.effect || 'light')
const closable = ref(props.closable || false)
const disableTransitions = ref(props.disableTransitions || false)
const hit = ref(props.hit || false)
const badgeCount = ref(props.badgeCount)
const badgeType = ref(props.badgeType || 'primary')
/**
* 关闭事件
*/
function handleClose() {
emit('close')
}
</script>
<style lang="scss" scoped>
.ml-2 {
margin-left: 8px;
}
</style>页面使用示例:DemoTagPage.vue
<template>
<div class="demo-tag">
<!-- 普通状态标签 -->
<BaseTag text="进行中" type="info" />
<!-- 成功状态 -->
<BaseTag text="完成" type="success" />
<!-- 警告状态,可关闭 -->
<BaseTag text="异常" type="warning" closable @close="handleClose" />
<!-- 自定义颜色 -->
<BaseTag text="自定义" color="#ff9900" />
<!-- 带徽标 -->
<BaseTag text="消息" type="primary" :badgeCount="5" badgeType="danger" />
</div>
</template>
<script lang="ts" setup>
import BaseTag from '@/components/BaseTag.vue'
function handleClose() {
console.log('标签已关闭')
}
</script>
<style lang="scss" scoped>
.demo-tag {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 12px;
}
</style>✅ 企业级封装特点:
- 完全封装 ElementPlus Tag 和 Badge
- 支持动态文本、状态颜色、徽标数量
- 支持可关闭标签并触发事件
- TS 类型安全,事件声明清晰
- 统一样式,可在后台管理系统状态展示、消息通知、标签分类等场景复用
功能点如下:
- 封装 ElementPlus
Message、Notification、Tooltip - 支持统一调用方式(BaseMessage.show / BaseNotification.show / BaseTooltip)
- 支持不同类型(success / warning / info / error)
- 支持自定义标题、内容、持续时间
- 支持可关闭、全局样式统一
- 支持 TS 类型安全
组件封装:BaseMessage.ts
import { ElMessage, ElNotification } from 'element-plus'
type MessageType = 'success' | 'warning' | 'info' | 'error'
interface MessageOptions {
message: string
type?: MessageType
duration?: number
showClose?: boolean
}
/**
* 全局消息提示封装
*/
export const BaseMessage = {
show(options: MessageOptions) {
ElMessage({
message: options.message,
type: options.type || 'info',
duration: options.duration || 3000,
showClose: options.showClose ?? true
})
},
success(message: string, duration = 3000) {
this.show({ message, type: 'success', duration })
},
warning(message: string, duration = 3000) {
this.show({ message, type: 'warning', duration })
},
info(message: string, duration = 3000) {
this.show({ message, type: 'info', duration })
},
error(message: string, duration = 3000) {
this.show({ message, type: 'error', duration })
}
}
/**
* 全局通知封装
*/
export const BaseNotification = {
show(options: MessageOptions & { title?: string }) {
ElNotification({
title: options.title || '提示',
message: options.message,
type: options.type || 'info',
duration: options.duration || 4000,
showClose: options.showClose ?? true
})
},
success(title: string, message: string, duration = 4000) {
this.show({ title, message, type: 'success', duration })
},
warning(title: string, message: string, duration = 4000) {
this.show({ title, message, type: 'warning', duration })
},
info(title: string, message: string, duration = 4000) {
this.show({ title, message, type: 'info', duration })
},
error(title: string, message: string, duration = 4000) {
this.show({ title, message, type: 'error', duration })
}
}Tooltip 封装组件:BaseTooltip.vue
<template>
<el-tooltip
:content="content"
:placement="placement"
:effect="effect"
:visible-arrow="visibleArrow"
>
<slot />
</el-tooltip>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
interface Props {
content: string
placement?: 'top' | 'bottom' | 'left' | 'right'
effect?: 'dark' | 'light'
visibleArrow?: boolean
}
const props = defineProps<Props>()
const placement = props.placement || 'top'
const effect = props.effect || 'dark'
const visibleArrow = props.visibleArrow ?? true
</script>
<style lang="scss" scoped>
/* 可统一样式 */
</style>页面使用示例:DemoMessagePage.vue
<template>
<div class="demo-message">
<button @click="showMessage">Message 提示</button>
<button @click="showNotification">Notification 提示</button>
<BaseTooltip content="这是 Tooltip 提示" placement="top">
<button>悬浮显示 Tooltip</button>
</BaseTooltip>
</div>
</template>
<script lang="ts" setup>
import { BaseMessage, BaseNotification } from '@/components/BaseMessage'
import BaseTooltip from '@/components/BaseTooltip.vue'
function showMessage() {
BaseMessage.success('操作成功!')
BaseMessage.warning('这是警告提示')
}
function showNotification() {
BaseNotification.success('成功', '操作已完成')
BaseNotification.error('错误', '操作失败,请重试')
}
</script>
<style lang="scss" scoped>
.demo-message {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
</style>✅ 企业级封装特点:
- 全局统一调用方式,业务无需每次重复配置 ElementPlus API
- 支持 Message、Notification、Tooltip 三类常用提示
- 支持类型、标题、内容、持续时间、关闭按钮统一设置
- TS 类型安全,事件调用清晰
- 易于在后台系统操作反馈、报表提示、按钮操作反馈场景复用
功能点如下:
- 封装 ElementPlus
Loading(全屏/局部加载) - 封装 ElementPlus
Skeleton(骨架屏占位) - 支持动态绑定显示状态
- 支持自定义样式、尺寸、数量
- 支持 TS 类型安全和事件回调
组件封装:BaseLoading.vue
<template>
<el-skeleton
v-if="!loaded"
:rows="rows"
:animated="animated"
:loading="true"
style="width: 100%;"
>
<slot />
</el-skeleton>
<div v-else>
<slot />
</div>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
interface Props {
loaded?: boolean // 数据是否已加载
rows?: number // 骨架行数
animated?: boolean // 是否开启动画
}
const props = defineProps<Props>()
const loaded = props.loaded ?? false
const rows = props.rows ?? 3
const animated = props.animated ?? true
</script>
<style lang="scss" scoped>
/* 可扩展自定义骨架样式 */
</style>全局加载封装:BaseFullLoading.ts
import { ElLoading } from 'element-plus'
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
interface LoadingOptions {
text?: string
fullscreen?: boolean
lock?: boolean
background?: string
}
export const BaseLoading = {
/**
* 显示全局或局部加载
*/
show(options?: LoadingOptions) {
loadingInstance = ElLoading.service({
text: options?.text || '加载中...',
fullscreen: options?.fullscreen ?? true,
lock: options?.lock ?? true,
background: options?.background || 'rgba(0, 0, 0, 0.3)'
})
},
/**
* 关闭加载
*/
close() {
if (loadingInstance) {
loadingInstance.close()
loadingInstance = null
}
}
}页面使用示例:DemoLoadingPage.vue
<template>
<div class="demo-loading">
<p>骨架屏示例:</p>
<BaseLoading :loaded="loaded" :rows="5">
<div>
<p v-for="i in 5" :key="i">这是加载完成后的真实内容第 {{ i }} 行</p>
</div>
</BaseLoading>
<p>全局加载示例:</p>
<button @click="showGlobalLoading">显示全局加载</button>
<button @click="closeGlobalLoading">关闭全局加载</button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import BaseLoading from '@/components/BaseLoading.vue'
import { BaseLoading as BaseFullLoading } from '@/components/BaseFullLoading'
const loaded = ref(false)
/**
* 模拟加载数据
*/
setTimeout(() => {
loaded.value = true
}, 2000)
function showGlobalLoading() {
BaseFullLoading.show({ text: '全局加载中...' })
setTimeout(() => {
BaseFullLoading.close()
}, 3000)
}
function closeGlobalLoading() {
BaseFullLoading.close()
}
</script>
<style lang="scss" scoped>
.demo-loading {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>✅ 企业级封装特点:
- 骨架屏与全局加载统一封装,方便在列表页、表单、详情页等复用
- 支持动态显示状态(loaded)、动画效果、行数自定义
- 全局加载支持文本、背景、锁屏、全屏配置
- TS 类型安全,事件和状态清晰
- 统一样式,易于企业后台系统、管理平台、大型前端应用使用
弹出框封装
- 封装
Popover / Dropdown,统一触发方式、位置控制、菜单数据结构和样式。
组件封装代码:BasePopover.vue
<template>
<!-- Popover 弹出框 -->
<el-popover
:placement="placement" <!-- 弹出位置 -->
:width="width" <!-- 弹出宽度 -->
:trigger="trigger" <!-- 触发方式 -->
:popper-class="popperClass" <!-- 自定义样式 -->
>
<!-- 弹出内容 -->
<div class="popover-content">
<slot name="content">
{{ content }}
</slot>
</div>
<!-- 触发元素 -->
<template #reference>
<slot />
</template>
</el-popover>
</template>
<script lang="ts" setup>
import { defineProps, withDefaults } from 'vue'
interface Props {
content?: string
width?: number | string
trigger?: 'hover' | 'click' | 'focus' | 'contextmenu'
placement?:
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'right'
popperClass?: string
}
withDefaults(defineProps<Props>(), {
trigger: 'hover',
width: 200,
placement: 'top',
popperClass: 'base-popover'
})
</script>
<style lang="scss" scoped>
.popover-content {
font-size: 14px; // 内容字体大小
color: #606266; // 文本颜色
line-height: 1.6; // 行高
padding: 4px 0; // 内边距
}
</style>组件封装代码:BaseDropdown.vue
<template>
<!-- 下拉菜单 -->
<el-dropdown
:trigger="trigger" <!-- 触发方式 -->
:placement="placement" <!-- 弹出位置 -->
@command="handleCommand"
>
<!-- 触发元素 -->
<span class="dropdown-trigger">
<slot />
</span>
<!-- 菜单列表 -->
<template #dropdown>
<el-dropdown-menu class="base-dropdown-menu">
<el-dropdown-item
v-for="item in options"
:key="item.value"
:command="item.value"
:disabled="item.disabled"
>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts" setup>
import { defineProps, defineEmits, withDefaults } from 'vue'
interface DropdownOption {
label: string
value: string | number
disabled?: boolean
}
interface Props {
options: DropdownOption[]
trigger?: 'hover' | 'click' | 'contextmenu'
placement?: 'top' | 'bottom' | 'bottom-start' | 'bottom-end'
}
const props = withDefaults(defineProps<Props>(), {
trigger: 'click',
placement: 'bottom'
})
const emit = defineEmits<{
(e: 'command', value: string | number): void
}>()
/**
* 点击菜单项事件
*/
function handleCommand(command: string | number) {
emit('command', command)
}
</script>
<style lang="scss" scoped>
.dropdown-trigger {
cursor: pointer; // 鼠标样式
display: inline-flex; // 行内flex布局
align-items: center; // 垂直居中
}
.base-dropdown-menu {
min-width: 120px; // 菜单最小宽度
}
</style>页面使用代码:DemoPopoverDropdown.vue
<template>
<div class="demo">
<!-- Popover 示例 -->
<BasePopover content="这是 Popover 提示" placement="top">
<el-button type="primary">
悬浮提示
</el-button>
</BasePopover>
<!-- 自定义 Popover 内容 -->
<BasePopover placement="bottom">
<template #content>
<div>
<p>自定义内容</p>
<el-button size="small">操作</el-button>
</div>
</template>
<el-button>
点击弹出
</el-button>
</BasePopover>
<!-- Dropdown 示例 -->
<BaseDropdown
:options="menuList"
@command="handleCommand"
>
<el-button type="success">
操作菜单
</el-button>
</BaseDropdown>
</div>
</template>
<script lang="ts" setup>
import BasePopover from '@/components/BasePopover.vue'
import BaseDropdown from '@/components/BaseDropdown.vue'
const menuList = [
{ label: '查看', value: 'view' },
{ label: '编辑', value: 'edit' },
{ label: '删除', value: 'delete' }
]
/**
* 菜单点击回调
*/
function handleCommand(command: string | number) {
console.log('选择操作:', command)
}
</script>
<style lang="scss" scoped>
.demo {
display: flex; // flex布局
gap: 16px; // 间距
padding: 20px; // 内边距
}
</style>卡片封装
- 封装通用
Card组件,支持标题区、内容区、操作区(header / default / footer),统一样式结构,适用于信息卡片、统计卡片、列表卡片等场景。
组件封装代码:BaseCard.vue
<template>
<el-card
class="base-card"
:shadow="shadow" <!-- 阴影效果 -->
:body-style="bodyStyle" <!-- 内容区域样式 -->
>
<!-- 标题区 -->
<template v-if="title || $slots.header" #header>
<div class="card-header">
<!-- 标题 -->
<div class="card-title">
<slot name="header">
{{ title }}
</slot>
</div>
<!-- 操作区 -->
<div v-if="$slots.action" class="card-actions">
<slot name="action"></slot>
</div>
</div>
</template>
<!-- 内容区 -->
<div class="card-body">
<slot></slot>
</div>
<!-- 底部 -->
<div v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { defineProps, withDefaults, CSSProperties } from 'vue'
interface Props {
title?: string // 卡片标题
shadow?: 'always' | 'hover' | 'never' // 阴影模式
bodyStyle?: CSSProperties // 内容区样式
}
withDefaults(defineProps<Props>(), {
shadow: 'hover',
bodyStyle: () => ({
padding: '16px'
})
})
</script>
<style lang="scss" scoped>
.base-card {
border-radius: 6px; // 卡片圆角
}
/* 头部 */
.card-header {
display: flex; // flex布局
justify-content: space-between; // 左右布局
align-items: center; // 垂直居中
}
/* 标题 */
.card-title {
font-size: 16px; // 标题大小
font-weight: 500; // 字体加粗
color: #303133; // 标题颜色
}
/* 操作区 */
.card-actions {
display: flex; // flex布局
gap: 8px; // 按钮间距
}
/* 内容区 */
.card-body {
font-size: 14px; // 内容字体
color: #606266; // 内容颜色
}
/* 底部 */
.card-footer {
margin-top: 12px; // 上间距
padding-top: 12px; // 内边距
border-top: 1px solid #ebeef5; // 分割线
display: flex;
justify-content: flex-end; // 右对齐
}
</style>页面使用代码:DemoCard.vue
<template>
<div class="card-demo">
<!-- 基础卡片 -->
<BaseCard title="用户信息">
<p>姓名:张三</p>
<p>部门:技术部</p>
<p>职位:后端工程师</p>
</BaseCard>
<!-- 带操作区 -->
<BaseCard title="订单信息">
<template #action>
<el-button size="small">编辑</el-button>
<el-button size="small" type="danger">删除</el-button>
</template>
<p>订单号:A20240501</p>
<p>金额:¥399</p>
</BaseCard>
<!-- 带底部操作 -->
<BaseCard title="系统通知">
<p>系统将在今晚 22:00 进行维护升级。</p>
<template #footer>
<el-button size="small">取消</el-button>
<el-button size="small" type="primary">确认</el-button>
</template>
</BaseCard>
</div>
</template>
<script lang="ts" setup>
import BaseCard from '@/components/BaseCard.vue'
</script>
<style lang="scss" scoped>
.card-demo {
display: grid; // grid布局
grid-template-columns: repeat(2, 1fr); // 两列布局
gap: 20px; // 卡片间距
}
</style>