还在头疼后台管理页面?Vue3+ElementPlus封装的页面级组件让你5分钟一个页面

​Vue3实现常规后台查询页面。

上一篇我用vue2实现了对后台管理常规查询页面的封装。今天我们来用vue3来实现一下。Vue2的文章大家可以去看看

还在写重复的增删改查的重复代码?还在重复写Ajax请求?Vue2+Element-ui实现常规后台查询展示页面来了

需求我们就不分析那么多了跟vue2的一样,我们这次直接上代码。看看vue3的实现逻辑是怎么样的。

源码地址: github.com/fengligao/v… 也可以扫描下方二维码关注我的公众号或微信获取。

首先我们来看看页面实现的效果:

还在头疼后台管理页面?Vue3+ElementPlus封装的页面级组件让你5分钟一个页面

编辑

可以看到页面中集成了查询条件操作,表格数据展示,表格数据等操作,分页切换等基础功能。

我们可以根据功能分为4个基础操作区域:

还在头疼后台管理页面?Vue3+ElementPlus封装的页面级组件让你5分钟一个页面

编辑

我们先来看看条件查询区域该如何实现Vue3和Vue2写法不同 可以使用组合式api

不墨迹具体实现思路和Vue2的思路一样 还是根据form的数组来动态渲染条件类型。来看代码:

import { defineComponent, onBeforeMount, ref } from 'vue'
import { ElForm, ElFormItem, ElButton } from 'element-plus'
import { formProps } from './props'
import type { IFormItem } from './interface'
import MiJiInput from "@/components/miji-input.vue"
import MiJiSelect from '@/components/miji-select.vue'
import MiJiDateRange from '@/components/miji-daterange.vue'
import MiJiDate from '@/components/miji-date.vue'
import MiJiRadio from '@/components/miji-radio.vue'
import MiJiCheckbox from '@/components/miji-checkbox.vue'
import MiJiFormItem from './formItem.vue'
export default defineComponent({
props: formProps,
emits: ['search'],
setup(props, { emit }) {
const { form, isAsync } = props
const forms = ref<Array<IFormItem>>([])
const formData = ref({})
// 表单 异步数据处理
const optionsMap = (item: IFormItem) => {
return new Promise((resolve) => {
item.getOptions().then((res: any) => {
const options = res.map(v => {
return {
label: v[item.optionsLabel || 'label'],
value: v[item.optionsValue || 'value']
}
});
resolve(options)
})
})
}
// 初始化表单项
const initForm = async (_form) => {
for (let i = 0; i < _form.length; i++) {
const newItem: IFormItem = Object.assign(_form[i])
newItem.weight = newItem.weight || 1; // 保留了用户可以自定义权重排序
switch (newItem.type) {
case 'daterange':
newItem.value = Array.isArray(newItem.defaultValue) ? newItem.defaultValue : [];
newItem.weight = 2;
break;
case 'date':
newItem.value = newItem.defaultValue || '';
break;
case 'text':
newItem.value = newItem.defaultValue || '';
break;
case 'select':
case 'checkbox':
newItem.value = newItem.defaultValue || (newItem.type === 'select' ? newItem.multiple ? [] : '' : newItem.multiple ? [] : []); // 多选 默认:空数组
newItem.options = newItem.options || [];
if (typeof newItem.getOptions === 'function') {
if (isAsync || newItem.type === 'checkbox') {
newItem.options = await optionsMap(newItem)
} else {
optionsMap(newItem).then(res => {
newItem.options = res
})
}
}
break;
case 'radio-button':
case 'radio':
newItem.value = newItem.defaultValue || '';
newItem.options = newItem.options || [];
if (typeof newItem.getOptions === 'function') {
newItem.options = await optionsMap(newItem)
}
break;
case 'selectInput':
// 选择 默认第一个
newItem[newItem.selectKey] = newItem.selectOptions[0] ? newItem.selectOptions[0].value : '';
newItem[newItem.selectInputKey] = '';
break;
default:
break;
}
forms.value.push(newItem);
}
}
const sortForm = (formlist) => {
return new Promise((resolve) => {
// 权重大的前置
formlist.sort((prev, next) => prev.weight - next.weight);
// radio checkbox 选项后置
formlist.sort((prev, next) => {
if (next.type === 'checkbox' || next.type === 'radio' || next.type === 'radio-button') {
return -1
}
});
formlist.sort((prev, next) => {
if (next.label && prev.label) {
return 0;
}
if (next.label && (!prev.label)) {
return 1;
}
if ((!next.label) && prev.label) {
return -1;
}
});
resolve(formlist)
})
}
// 生成表单的默认数据
const initFormData = () => {
const data = {}
forms.value.forEach(item => {
switch (item.type) {
case 'daterange':
data[item.key] = Array.isArray(item.defaultValue) ? item.defaultValue : [];
break;
case 'select':
case 'checkbox':
data[item.key] = item.defaultValue || (item.type === 'select' ? item.multiple ? [] : '' : []); // 多选 默认:空数组
break;
default:
data[item.key] = item.defaultValue || '';
break;
}
});
return data
}
onBeforeMount(async () => {
// 条件排序
const newForm = await sortForm(form)
await initForm(newForm)
formData.value = initFormData()
console.log('默认值:', formData.value);
const param = await getParamByForm(formData.value);
emit('search', param)
})
const onChangeFormValue = (value, index) => {
// forms.value[index].value = value
formData.value[forms.value[index].key] = value;
}
const renderInput = (formItem: IFormItem, formIndex: number) => {
return <MiJiInput
value={formData.value[formItem.key]}
placeholder={formItem.placeholder}
onInput={(value) => onChangeFormValue(value, formIndex)}
/>
}
// 下拉选择框
const renderSelect = (formItem: IFormItem, formIndex: number) => {
return <MiJiSelect
options={formItem.options}
value={formData.value[formItem.key]}
placeholder={formItem.placeholder}
valueFormat={formItem.valueFormat}
onChange={(value) => onChangeFormValue(value, formIndex)}
/>
}
// 时间范围选择器
const renderDateRange = (formItem: IFormItem, formIndex: number) => {
return <MiJiDateRange
value={formData.value[formItem.key]}
startPlaceholder={formItem.startPlaceholder}
valueFormat={formItem.valueFormat}
endPlaceholder={formItem.endPlaceholder}
onChange={(value) => onChangeFormValue(value, formIndex)}
/>
}
// 时间选择器
const renderDate = (formItem: IFormItem, formIndex: number) => {
return <MiJiDate
value={formData.value[formItem.key]}
placeholder={formItem.placeholder}
valueFormat={formItem.valueFormat}
onChange={(value) => onChangeFormValue(value, formIndex)}
/>
}
// 单选框组
const renderRadio = (formItem: IFormItem, formIndex: number) => {
return <MiJiRadio
value={formData.value[formItem.key]}
type={formItem.type}
options={formItem.options}
onChange={(value) => onChangeFormValue(value, formIndex)}
/>
}
// 多选框组
const renderCheckbox = (formItem: IFormItem, formIndex: number) => {
return <MiJiCheckbox
value={formData.value[formItem.key]}
options={formItem.options}
onChange={(value) => onChangeFormValue(value, formIndex)}
/>
}
// 渲染表单
const renderFormItem = (item: IFormItem, i: number) => {
let formItem = null
switch (item.type) {
case 'text':
formItem = renderInput(item, i)
break;
case 'select':
formItem = renderSelect(item, i)
break;
case 'daterange':
formItem = renderDateRange(item, i)
break;
case 'date':
formItem = renderDate(item, i)
break;
case 'radio-button':
formItem = renderRadio(item, i)
break;
case 'radio':
formItem = renderRadio(item, i)
break;
case 'checkbox':
formItem = renderCheckbox(item, i)
break;
default:
break;
}
return <MiJiFormItem
label={item.label}
width={item.width}
type={item.type}
>
{formItem}
</MiJiFormItem>
}
const renderForm = () => {
const formItem: any = []
for (let i = 0; i < forms.value.length; i++) {
const item = forms.value[i];
formItem.push(renderFormItem(item, i))
}
return <ElForm
class="template-page__form"
inline={true}
model={formData}
label-width={'80px'}
>
{formItem}
<ElFormItem style={{ paddingLeft: 0, borderColor: 'rgba(0,0,0,0)' }}>
<ElButton class="search-btn" type="primary" onClick={onSubmit}>查 询</ElButton>
<ElButton style={{ height: '100%' }} onClick={onCancel}>重 置</ElButton>
</ElFormItem>
</ElForm>
}
const getParamByForm = (data) => {
const param = {}
for (const i in data) {
const item: any = forms.value.find(v => v.key === i)
switch (item.type) {
// 时间范围
case 'daterange':
if (item.isSelectKey) {
param[item.key] = data[i]
param['isArrayKey'] = item.key
} else {
param[item.startDateKeyName || 'kaiShiSJ'] = Array.isArray(data[i]) ? data[i][0] : '';
param[item.endDateKeyName || 'jieShuSJ'] = Array.isArray(data[i]) ? data[i][1] : '';
}
break;
case 'checkbox':
param[item.key] = (data[i] || []).join();
break;
case 'select':
param[item.key] = Array.isArray(data[i]) ? data[i].join() : data[i];
break;
default:
param[item.key] = data[i];
}
}
return param
}
const onSubmit = () => {
const param = getParamByForm(formData.value);
emit('search', param)
}
const onCancel = async () => {
formData.value = initFormData()
}
return () => renderForm()
}
})

在表单的组件中我有单独分装了一下input、select、checkbox等组件,包括项目中扩展了新增编辑的通用组件,这里也可以直接使用这些表单组件。

表单区域实现以后我们来看下页面的代码

页面模版中分为 条件查询、表格展示、表格分页、插槽内容。

这里我预留了只保留条件查询,查询数据后不使用常规的表格展示使用的自定义内容的插槽。

import { defineComponent, ref } from 'vue'
import './index.scss'
import { pageProps } from './props'
import MiJiPage from '../miji-page.vue'
import Form from './form'
import Table from './table'
import Pagination from './pagination.vue'
export default defineComponent({
name: 'miji-template-page',
props: pageProps,
setup(props, ctx) {
console.log(ctx);
const {
url,
form,
method,
beforeRequest,
afterResponse,
showPagination,
columns,
isAsync,
tableConfig = {}
} = props
const dataSource = ref()
const requestData = ref()
const urlQuery = ref()
const pageNo = ref<number>(1)
const total = ref<number>(0)
const pageSize = ref<number>(20)
const initPage = () => {
return <MiJiPage className="template-page">
<Form
form={form}
isAsync={isAsync}
onSearch={(param) => onSearch(param)}
/>
<div class="template-page__content">
{
ctx.slots.tableOptions || tableConfig.title
? <div className="content-options">
<div className="content-options__title">{tableConfig.title || ''}</div>
<div className="content-options__slot">
{ctx.slots.tableOptions ? ctx.slots.tableOptions() : ''}
</div>
</div>
: ''
}
{
ctx.slots.defaultContent ? ctx.slots.defaultContent() : <Table
dataSource={dataSource.value}
columns={columns}
>
{{
...ctx.slots
}}
</Table>
}
</div>
{
!ctx.slots.defaultContent && showPagination && total.value > 0 ? <Pagination
pageNo={pageNo.value}
pageSize={pageSize.value}
total={total.value}
onCurrentChange={(page: number) => handleCurrentChange(page)}
onPageSizeChange={(size: number) => handlePageSizeChange(size)}
/> : ''
}
</MiJiPage>
}
const handleCurrentChange = (page: number) => {
pageNo.value = page
getData()
}
const handlePageSizeChange = (size: number) => {
pageSize.value = size
pageNo.value = 1
getData()
}
const onSearch = (param) => {
pageNo.value = 1
requestData.value = param
if (method === 'get' && param.isArrayKey) {
urlQuery.value = param.isArrayKey ? '?' + param[param.isArrayKey].map(v => param.isArrayKey + '=' + v).join(',').replace(',', '&') : ''
delete requestData.value[param.isArrayKey]
delete requestData.value.isArrayKey
}
getData()
}
const getData = () => {
if (showPagination) {
const page = {
current: pageNo.value,
size: pageSize.value
}
Object.assign(requestData.value, page);
}
if (beforeRequest) {
requestData.value = beforeRequest(requestData.value);
if (!requestData.value) return false; // 如果返回false,则取消当前请求
}
const data = method === 'get' ? { params: requestData.value } : requestData.value; // 请求入参
console.log('最终入参:', data);
setTimeout(() => {
let data: { records: any } = { records: [] }
data = afterResponse ? afterResponse(data) : data;
console.log('处理后的出参:', data);
dataSource.value = data.records
pageNo.value = 1
pageSize.value = 10
total.value = 100
}, 2000)
}
return () => initPage()
}
})

在常规页面中我们使用的表格组件正是我上一篇文章的组件这里也不过多介绍了可以看看这篇文章 Vue3 ElementPlus 二次封装常用表格展示组件 或者直接下载本篇文章的 源代码

下面这个弹层是封装的一个通用新增编辑表单的组件

还在头疼后台管理页面?Vue3+ElementPlus封装的页面级组件让你5分钟一个页面

编辑

组件的实现思路和页面查询表单的动态渲染一样的原理,这里呢 我用了一个element-plus的抽屉组件来当作统一弹层,大家有想用dialog得或者切换页面新增编辑的可以在这个基础之上进行拓展。下面来看看代码:

import { defineComponent, ref, reactive, onMounted } from "vue";
import {
ElDrawer,
ElFooter,
ElForm,
ElFormItem,
ElButton,
ElNotification
} from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import './index.scss'
import IProps from './props'
import MiJiRadio from '@/components/miji-radio.vue'
import MiJiInput from "@/components/miji-input.vue"
import MiJiSelect from '@/components/miji-select.vue'
import MiJiDateRange from '@/components/miji-daterange.vue'
import MiJiDate from '@/components/miji-date.vue'
import MiJiCheckbox from '@/components/miji-checkbox.vue'
import {
initFormValues,
initFormRules,
initFormItems,
} from './form'
export default defineComponent({
name: 'miji-a-u-drawer', // 右侧抽屉新增编辑形式 a: add u: update
props: IProps,
emits: {
close: null
},
setup(props, { emit, slots }) {
const drawer = ref(true)
// props
const {
method,
url,
title,
size,
showFooter,
labelWidth,
formSize,
forms,
labelPosition,
className,
formValues,
cancelText,
enterText
} = props
// 插槽
const { header } = slots
// 表单ref
const ruleFormRef = ref<FormInstance>()
console.log('新增编辑的数据:', forms);
const formItems = ref<any>([])
onMounted(async () => {
formItems.value = await initFormItems(forms)
console.log(formItems.value);
})
const rulesItem = initFormRules(forms) // 初始化表单校验规则
const ruleFormItem = initFormValues(forms, formValues) // 初始化表单数据
const rules = reactive<FormRules>(rulesItem)
const ruleForm = reactive<any>(ruleFormItem)
console.log('初始化数据:', ruleForm, rules);
const handleEnter = (formEl) => {
console.log('enter option', formEl, ruleFormRef);
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
console.log('submit!', ruleForm)
ElNotification({
title: '提示',
message: '新增编辑成功',
})
} else {
console.log('error submit!')
return false
}
})
}
const handleCancel = () => {
console.log('cancel option');
emit('close', false)
}
const onChangeFormValue = (value, v) => {
// forms.value[index].value = value
ruleForm[v.name] = value;
// 表单值发生变化后 针对当前字段重新验证
if (!ruleFormRef.value) return false
ruleFormRef.value.validateField(v.name)
}
const renderFormItem = (v) => {
let item = null
switch (v.type) {
case 'input':
item = <MiJiInput
value={ruleForm[v.name]}
placeholder={v.placeholder}
onInput={(value) => onChangeFormValue(value, v)}
/>
break;
case 'select':
item = <MiJiSelect
options={v.options}
value={ruleForm[v.name]}
placeholder={v.placeholder}
onChange={(value) => onChangeFormValue(value, v)}
/>
break;
case 'checkbox':
item = <MiJiCheckbox
value={ruleForm[v.name]}
options={v.options}
onChange={(value) => onChangeFormValue(value, v)}
/>
break;
case 'radio':
item = <MiJiRadio
value={ruleForm[v.name]}
type={v.radioType}
options={v.options}
onChange={(value) => onChangeFormValue(value, v)}
/>
break;
case 'daterange':
item = <MiJiDateRange
value={ruleForm[v.name]}
startPlaceholder={v.startPlaceholder}
valueFormat={v.valueFormat}
endPlaceholder={v.endPlaceholder}
onChange={(value) => onChangeFormValue(value, v)}
/>
break;
case 'date':
item = <MiJiDate
value={ruleForm[v.name]}
placeholder={v.placeholder}
valueFormat={v.valueFormat}
onChange={(value) => onChangeFormValue(value, v)}
/>
break;
default:
break;
}
return <ElFormItem
label={v.label}
prop={v.name}
>
{item}
</ElFormItem>
}
const renderForm = () => {
const items: any = []
console.log(formItems);
formItems.value.forEach((v: any) => {
items.push(renderFormItem(v))
})
return <ElForm
ref={ruleFormRef}
model={ruleForm}
rules={rules}
labelPosition={labelPosition}
labelWidth={labelWidth}
class={className}
size={formSize}
>
{items}
</ElForm>
}
// 关闭弹窗
const beforeClose = () => [
emit('close', false)
]
return () => (
<ElDrawer
modelValue={drawer.value}
title={title || '提示'}
size={size}
beforeClose={beforeClose}
v-slots={{
header: () => header && header(),
footer: () => showFooter && <ElFooter>
<div style={{
height: '100%',
alignItems: 'flex-end',
justifyContent: 'flex-end',
display: 'flex'
}}>
<ElButton onClick={handleCancel}>{cancelText}</ElButton>
<ElButton
type="primary"
onClick={() => handleEnter(ruleFormRef.value)}
>{enterText}</ElButton>
</div>
</ElFooter>
}}
>
{renderForm()}
</ElDrawer>
)
},
})

思路一样也是动态根据数组类型来动态渲染表单。

这里注意的是 我用几个方法 单独处理了一下传递进来的表单数组。

把form需要的初始化数据、form的校验规则、form要渲染的内容处理、需要异步加载的数据。

// 初始化表单认值,
export const initFormValues = (forms, values) => {
const obj = {}
for (let i = 0; i < forms.length; i++) {
const e: any = forms[i];
switch (e.type) {
case 'select':
obj[e.name] = values ? values[e.name] : e.value || (e.multiple ? [] : '')
break;
case 'checkbox':
obj[e.name] = values ? values[e.name] : e.value || []
break;
default:
obj[e.name] = values ? values[e.name] : e.value || ''
break;
}
}
return obj
}
// 初始化表单规则
export const initFormRules = (forms) => {
const obj = {}
for (let i = 0; i < forms.length; i++) {
const e: any = forms[i];
obj[e.name] = {
required: e.required,
message: e.message,
trigger: e.trigger,
}
}
return obj
}
// 表单 异步数据处理
const optionsMap = (item) => {
return new Promise((resolve) => {
item.getOptions().then((res: any) => {
const options = res.map(v => {
return {
label: v[item.optionsLabel || 'label'],
value: v[item.optionsValue || 'value']
}
});
resolve(options)
})
})
}
// 初始化表单渲染内容中的异步数据
export const initFormItems = async (forms) => {
const arr: Array<any> = []
for (let i = 0; i < forms.length; i++) {
const item = Object.assign({}, forms[i])
if (typeof item.getOptions === 'function') {
item.options = await optionsMap(item)
}
arr.push(item);
}
return arr
}

这里我们看看在页面中如何使用:

组件的全局组册就这样在入口文件引入就好了

import MiJiTable from './components/templatePage/table'
import TemplatePage from './components/templatePage/index'
import MiJiAuDrawer from './components/addOrUpdateItem/index'
const app = createApp(App)
app.component(MiJiTable.name, MiJiTable)
app.component(TemplatePage.name, TemplatePage)
app.component(MiJiAuDrawer.name, MiJiAuDrawer)

在页面中我们可以直接使用我们组件的name属性声明的组件名称

<miji-template-page
method="get"
url="/api"
:form="form"
:columns="columns"
:beforeRequest="beforeRequest"
:afterResponse="afterResponse"
:pageNo="1"
:pageSize="20"
:tableConfig="{ title: '表格标题' }"
>
<!-- 页面组件自带表格插槽部分 -->
<template #xingBie="scope">{{ '插槽:' }}</template>
<!-- 页面组件 表格前插槽部分 -->
<template #tableOptions>
<el-button type="primary" @click="add">新增</el-button>
</template>
<!-- 自定义内容插槽 -->
<template #defaultContent>
// 这里自定义插槽的内容
<el-tree
style="max-width: 600px"
:data="data"
:props="defaultProps"
@node-click="handleNodeClick"
/>
</template>
</miji-template-page>

页面中可以直接只用内置表格,也可以使用自定义的展示数据的方式:比如树形图结构的

搭配新增编辑的通用组件来实现页面的基本功能

<miji-a-u-drawer
v-if="drawer"
@close="handleClose"
@success="handleSuccess"
url="/api"
labelWidth="100px"
labelPosition="top"
size="50%"
:forms="[
{
type: 'input',
label: '名称',
name: 'mingCheng',
value: '',
required: true,
placeholder: '请输入名称',
message: '名称是不可或缺的入参',
trigger: 'blur'
},
{
type: 'select',
label: '下拉',
name: 'xiaLa',
value: '',
required: true,
placeholder: '请选择下拉',
message: '下拉是不可或缺的入参',
trigger: 'change',
options: [
{
value: 'Option1',
label: 'Option1'
}
],
optionsLabel: 'name',
optionsValue: 'id',
getOptions: getAddOrUpdateOptions
},
// ... 等等其他的所需数据
]"
:formValues="formValues"
>
<template #header>
<h4>自定义头</h4>
</template>
</miji-a-u-drawer>

以上是简单的使用方式大家看兴趣的可以直接去下载源码看看完整代码。

所以一个常规后台管理页面的组件大大提高我们的开发效率,虽然不及低代码,但是在一定程度上也解决了我们重复代码重复开发的难题。

有兴趣的朋友可以在这个基础上面,配合后端的同学把创建页面和一些操作逻辑通过配置存起来。然后再搭配我这个组件来使用也是一种低代码的实现方式。

近期我会出一个开源的后台管理的项目模版,大家可以关注我的公众号及时获取最新文章。

欢迎大家私信留言,多多点评

可以微信搜索 web秘籍 关注我的公众号或添加我的微信 2545070038 联系、沟通。

原文链接:https://juejin.cn/post/7353280369381441562 作者:LGF_MIJI

(0)
上一篇 2024年4月7日 上午10:00
下一篇 2024年4月7日 上午10:11

相关推荐

发表回复

登录后才能评论