前端项目基建思路——视图组件加载器页面布局用例

129 阅读2分钟

前言

在前文前端项目基建思路——深入响应式系统,更灵活的视图渲染方案中,介绍了一种通过借助vue数据驱动能力实现视图的方案

上文中提到,为了充分发挥数据驱动的能力,我们封装了视图组件加载器,并封装了一些视图组件;接下来将用以下几个示例为大家提供一些使用场景上的解决方案。

响应式布局+视图组件加载器实现视图开发

在现在经常使用的一些主流UI库中,都有对响应式布局提供方案支持,下面我们借助Element-Plus的layout布局做响应式布局用例的说明。

布局

假定我们现在收到一个需求,需要完成一个提交信息的表单,我们先使用 row 和 col 组件完成基础的布局,效果如下:

Snipaste_2023-02-18_19-45-57.png

<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添加一些新的视图控制信息

Snipaste_2023-02-18_19-52-04.png

<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属性和交互事件:

Snipaste_2023-02-19_10-18-55.png

<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布局,让每一行显示的元素数量随页面宽度自适应

Snipaste_2023-02-19_14-54-23.png 完整代码戳这里
关键代码:

<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>

Snipaste_2023-02-19_14-38-23.png

写在结尾:

微信图片_20230216225904.jpg