前言
在前文前端项目基建思路——深入响应式系统,更灵活的视图渲染方案中,介绍了一种通过借助vue数据驱动能力实现视图的方案
上文中提到,为了充分发挥数据驱动的能力,我们封装了视图组件加载器,并封装了一些视图组件;接下来将用以下几个示例为大家提供一些使用场景上的解决方案。
响应式布局+视图组件加载器实现视图开发
在现在经常使用的一些主流UI库中,都有对响应式布局提供方案支持,下面我们借助Element-Plus的layout布局做响应式布局用例的说明。
布局
假定我们现在收到一个需求,需要完成一个提交信息的表单,我们先使用 row 和 col 组件完成基础的布局,效果如下:
<template>
<el-card class="page-container is-always-shadow">
<!-- 布局示例 -->
<el-card>
<h5 class="demo-title">布局示例</h5>
<div class="success">
请完善下面的表单
</div>
<div class="demo-content">
<el-form>
<el-row :gutter="20">
<template v-for="field in fields" :key="field.label">
<el-col :span="field.span">
<el-form-item>
<div class="grid-content">
{{field.label}}
</div>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</div>
</el-card>
</el-card>
</template>
<script setup>
import { reactive } from 'vue'
const fields = reactive([
{ span: 6, label:'姓名' },
{ span: 8, label:'年龄' },
{ span: 4, label:'身高' },
{ span: 6, label:'体重' },
{ span: 6, label:'联系电话' },
{ span: 6, label:'住址' },
{ span: 6, label:'紧急联系人' },
{ span: 6, label:'联系人联系电话' },
{ span: 12, label:'擅长' },
{ span: 12, label:'爱好' },
{ span: 24, label:'自我评价' }
])
</script>
<style scoped>
.page-container {
min-height: 100%;
}
.demo-title{
line-height: 36px;
font-size: 24px;
font-weight: bold;
padding: 5px;
}
.demo-content{
line-height: 24px;
padding: 5px;
}
.el-card.is-always-shadow {
min-height: 100%!important;
}
.page-container .el-card {
margin-bottom: 20px;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
line-height: 36px;
text-align: center;
background: #d3dce6;
}
:deep(.el-col){
margin-bottom: 10px;
}
.success{
padding: 8px 16px;
background-color: #fff6f7;
border-radius: 4px;
border-left: 5px solid #67c23a;
margin: 20px 0;
}
</style>
呈现元素
接下来我们用视图组件加载器载入视图组件,呈现出表单内容,并为fields添加一些新的视图控制信息
<template>
<el-card class="page-container is-always-shadow">
<!-- 布局示例 -->
<el-card>
<h5 class="demo-title">布局示例</h5>
<div class="success">
请完善下面的表单
</div>
<div class="demo-content">
<el-form>
<el-row :gutter="20">
<template v-for="field in fields" :key="field.label">
<el-col :span="field.span">
<el-form-item :label="field.label">
<dynamic-view-loader
v-model:data="formData[field.key]"
:type="field.type"
:placeholder="field.label"
/>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</div>
</el-card>
</el-card>
</template>
<script setup>
import { reactive } from 'vue'
import DynamicViewLoader from '@/components/DynamicViewLoader.vue'
const formData = reactive({})
const fields = reactive([
{ span: 6, label:'姓名', key: 'fullName', type: 'input' },
{ span: 8, label:'年龄', key: 'age', type: 'input' },
{ span: 4, label:'身高', key: 'height', type: 'input' },
{ span: 6, label:'体重', key: 'weight', type: 'input' },
{ span: 6, label:'联系电话', key: 'tel', type: 'input' },
{ span: 6, label:'住址', key: 'address', type: 'input' },
{ span: 6, label:'紧急联系人', key: 'linkman', type: 'input' },
{ span: 6, label:'联系人联系电话', key: 'linktel', type: 'input' },
{ span: 12, label:'擅长', key: 'skill', type: 'input' },
{ span: 12, label:'爱好', key: 'hobby', type: 'input' },
{ span: 24, label:'自我评价', key: 'selfIntro', type: 'textarea' }
])
</script>
<style scoped>
.page-container {
min-height: 100%;
}
.demo-title{
line-height: 36px;
font-size: 24px;
font-weight: bold;
padding: 5px;
}
.demo-content{
line-height: 24px;
padding: 5px;
}
.el-card.is-always-shadow {
min-height: 100%!important;
}
.page-container .el-card {
margin-bottom: 20px;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
line-height: 36px;
text-align: center;
background: #d3dce6;
}
.success{
padding: 8px 16px;
background-color: #fff6f7;
border-radius: 4px;
border-left: 5px solid #67c23a;
margin: 20px 0;
}
</style>
添加一些验证规则、props属性和交互事件:
<template>
<el-card class="page-container is-always-shadow">
<!-- 布局示例 -->
<el-card>
<h5 class="demo-title">布局示例</h5>
<div class="success">
请完善下面的表单
</div>
<div class="demo-content">
<el-form ref="formRef" :rules="rules" :model="formData">
<el-row :gutter="20">
<template v-for="field in fields" :key="field.label">
<el-col :span="field.span">
<el-form-item :label="field.label" :prop="field.key">
<dynamic-view-loader
v-model:data="formData[field.key]"
:type="field.type"
:placeholder="field.label"
v-bind="fieldAttr[field.key]||{}"
v-on="fieldEvent[field.key]||{}"
/>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</div>
</el-card>
</el-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
import DynamicViewLoader from '@/components/DynamicViewLoader.vue'
const formRef = ref()
const formData = reactive({})
const fields = reactive([
{ span: 6, label:'姓名', key: 'fullName', type: 'input' },
{ span: 8, label:'年龄', key: 'age', type: 'input' },
{ span: 4, label:'身高', key: 'height', type: 'input' },
{ span: 6, label:'体重', key: 'weight', type: 'input' },
{ span: 6, label:'联系电话', key: 'tel', type: 'input' },
{ span: 6, label:'住址', key: 'address', type: 'input' },
{ span: 6, label:'紧急联系人', key: 'linkman', type: 'input' },
// { span: 6, label:'联系人联系电话', key: 'linktel', type: 'input' },
{ span: 12, label:'擅长', key: 'skill', type: 'input' },
{ span: 12, label:'爱好', key: 'hobby', type: 'input' },
{ span: 24, label:'自我评价', key: 'selfIntro', type: 'textarea' }
])
// 自定义只能输入正整数的校验函数
const numberTypeValValidator = (rule, value, callback) => {
if (value === '') {
callback()
} else {
const reg = /^([0-9]\d*)$/
if (!reg.test(value)) {
callback(new Error('只能输入正整数'))
}
callback();
}
}
// 校验规则管理
const rules = reactive({
fullName: [
{ required: true, message: '请输入姓名', trigger: ['blur', 'change'] }
],
age: [
{ required: true, message: '请输入年龄', trigger: ['blur', 'change'] },
{ validator: numberTypeValValidator, trigger: ['blur', 'change'] }
],
tel: [
{ required: true, message: '请输入联系电话', trigger: ['blur', 'change'] }
]
})
// 组件prop属性管理对象
const fieldAttr = reactive({
fullName: {
maxlength: 10,
'show-word-limit': true
}
})
// 组件event示例函数,限制只能输入数字
const limtNumber = (val, key) => {
if(typeof val === 'object' && val.srcElement){
return
}
formData[key] = val.replace(/\D/g,'')
}
// 组件event示例函数,有联系人信息时才显示联系人电话这一栏
const toggleView = (val) => {
if(typeof val === 'object' && val.srcElement){
return
}
const telIdx = fields.findIndex(item => item.key === 'linktel')
if(val && telIdx === -1){
const idx = fields.findIndex(item => item.key === 'linkman')
fields.splice(idx + 1, 0, { span: 6, label:'联系人联系电话', key: 'linktel', type: 'input' } )
return
}
if(!val && telIdx !== -1){
fields.splice(telIdx, 1)
return
}
}
// 组件event管理对象
const fieldEvent = reactive({
height: {
input: (val) => { limtNumber(val, 'height') }
},
weight: {
input: (val) => { limtNumber(val, 'weight') }
},
linkman: {
input: toggleView
}
})
</script>
<style scoped>
.page-container {
min-height: 100%;
}
.demo-title{
line-height: 36px;
font-size: 24px;
font-weight: bold;
padding: 5px;
}
.demo-content{
line-height: 24px;
padding: 5px;
}
.el-card.is-always-shadow {
min-height: 100%!important;
}
.page-container .el-card {
margin-bottom: 20px;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
line-height: 36px;
text-align: center;
background: #d3dce6;
}
.success{
padding: 8px 16px;
background-color: #fff6f7;
border-radius: 4px;
border-left: 5px solid #67c23a;
margin: 20px 0;
}
</style>
flex布局用例
使用flex布局,让每一行显示的元素数量随页面宽度自适应
完整代码戳这里
关键代码:
<template>
<el-card class="page-container is-always-shadow">
<!-- 布局示例——flex -->
<el-card>
<h5 class="demo-title">布局示例——flex</h5>
<div class="success">
请完善下面的表单
</div>
<div class="demo-content">
<el-form ref="flexFormRef" :label-width="120" :rules="rules" :model="formData">
<div class="flex-box">
<template v-for="field in fields" :key="field.label">
<div class="flex-box-item" :style="field.style||{}">
<el-form-item :label="field.label" :prop="field.key">
<dynamic-view-loader
v-model:data="formData[field.key]"
:type="field.type"
:placeholder="field.label"
v-bind="fieldAttr[field.key]||{}"
v-on="fieldEvent[field.key]||{}"
/>
</el-form-item>
</div>
</template>
</div>
</el-form>
</div>
</el-card>
</el-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
import DynamicViewLoader from '@/components/DynamicViewLoader.vue'
const flexFormRef = ref()
const formData = reactive({})
const fields = reactive([
// ... 其他字段配置信息
{
span: 24,
label:'自我评价',
key: 'selfIntro',
type: 'textarea',
style:{ width: '100%' } // 利用style指定某个元素的特殊样式
}
])
// ... 其他js内容
</script>
<style scoped>
// ... 其他演示class
// 定义flex布局class
.flex-box{
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
}
.flex-box-item{
min-width: 300px;
}
</style>
兼容视图中部分字段呈现内容不使用视图加载器的情况
需要兼容部分字段不使用视图加载器加载视图时,在字段信息管理对象中定义属性,并在模板代码中使用条件语句处理即可。
如在处理下面的视图时,我们在字段信息管理对象中定义一个disLoader属性标识当前字段对应的视图不使用视图加载器加载。然后在模板代码中通过字段信息的disLoader属性是否为空判断字段如何渲染。
完整代码戳这里
关键代码:
<template>
<el-card class="page-container is-always-shadow">
<!-- 布局示例——flex -->
<el-card>
<h5 class="demo-title">布局示例——flex</h5>
<div class="success">
请完善下面的表单
</div>
<div class="demo-content">
<el-form ref="flexFormRef" :label-width="120" :rules="rules" :model="formData">
<div class="flex-box">
<template v-for="field in fields" :key="field.label">
<div class="flex-box-item" :style="field.style||{}">
<el-form-item :label="field.label" :prop="field.key">
<template v-if="!field.disLoader">
<dynamic-view-loader
v-model:data="formData[field.key]"
:type="field.type"
:placeholder="field.label"
v-bind="fieldAttr[field.key]||{}"
v-on="fieldEvent[field.key]||{}"
/>
</template>
<template v-else-if="field.key === 'hobby'">
<div>
<el-checkbox-group v-model="formData[field.key]">
<el-checkbox label="唱" name="type"></el-checkbox>
<el-checkbox label="跳" name="type"></el-checkbox>
<el-checkbox label="Rap" name="type"></el-checkbox>
<el-checkbox label="🏀" name="type"></el-checkbox>
</el-checkbox-group>
</div>
</template>
</el-form-item>
</div>
</template>
</div>
</el-form>
</div>
</el-card>
</el-card>
</template>
<script setup>
import { ref, reactive } from 'vue'
import DynamicViewLoader from '@/components/DynamicViewLoader.vue'
const flexFormRef = ref()
const formData = reactive({
hobby: []
})
const fields = reactive([
// ...
{
span: 12,
label:'爱好',
key: 'hobby',
type: 'input',
disLoader: true,
style: {width: '100%'}
},
])
</script>