likes
comments
collection
share

前端如何做好ToB系统UI&交互体验

作者站长头像
站长
· 阅读数 22

一、背景

ToB系统,或者另一个较多的叫法中后台系统,往往采用element-uiiview组件库提高开发效率

该类系统往往是交互逻辑复杂,不怎么注重UI(UI资源充足的团队当我没说),不过不管有没有UI,都比不上C端更注重UI

但身为一名前端,注重页面体验,UI交互是我们的关心的事情,本文结合自身经验讲前端如何做好ToB系统UI&交互体验

二、遇到的问题

如果你是一个系统还好说,乱不到哪里去;多个ToB系统,如果没有一个规范的话很容易做成五花八门

我当时交接过来五个系统,交接前这五个系统是不同部门做的,有用element,有用view;我们把这五个系统联合在一起,做成一个完整的数字化运营解决方案的产品;这交互、UI实在拉垮,且我们没有UI支持

所以自己探索了一些规范,并落地实现

三、UI&交互统一

为了研究到底哪种UI交互是相对来说最好的,我参考了antd pro、element-ui 等其他中后台系统;大概分为以下几类

  • loading
  • 导航
    • 菜单
    • 面包屑
    • 选项卡切换
  • 表格搜索表单
  • 表格

1、loading

分为两种,路由切换时loading,接口请求时loading,具体分析如下

  • 路由切换:可在路由的钩子函数中处理
    • 优点:统一处理,成本低
    • 缺点:对数据是否加载完成无法精确
  • 接口请求:在初始化页面的接口增加loading处理,比如查询列表的接口是需要加loading的,但是删除一行的接口不需要加loading
    • 优点:相对来说可以确保精确
    • 缺点:成本高

注:表格的增删改查,表格应该统一封装,在表格的局部loading,表格相关接口配合设置loading

拿antd pro举例,切换路由时,内容区域整个开始loading, 前端如何做好ToB系统UI&交互体验 路由切换完之后,表格开始loading,对表格进行增删改查时,仅表格loading 前端如何做好ToB系统UI&交互体验 还有一种比较常见的路由切换loading,在浏览器的头部 前端如何做好ToB系统UI&交互体验 对于这种方式,iview 提供了iview.LoadingBar可以直接使用,element 可以利用nprogress包,代码如下

step 1:

npm install --save nprogress

step 2:

// router/index.js
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

router.beforeEach((to, from, next) => {
    NProgress.start();
    next()
})

router.afterEach(to => {
    NProgress.done();
})

step 3:

// App.vue
// 将loading颜色改为主题色
#nprogress .bar {
    background: #2d8cf0 !important;\
}

2、导航

导航是至关重要的一部分,主要包括头部导航侧边菜单导航,还有面包屑,以及页面切换选卡

给大家贴几个图感受一下我们未改造之前的体验 前端如何做好ToB系统UI&交互体验

菜单

  • 一级菜单有图标
  • 二级菜单无图标
  • 各系统颜色、交互保持一致
  • 在当前页,及当前页的子页面菜单均高亮
  • 各系统菜单命名、分布有部分调整,现有改动,有不妥可联系 前端如何做好ToB系统UI&交互体验

面包屑

  • 面包屑根据页面菜单层级展示
  • 当前页面为高亮
  • 当前页前面页面,鼠标滑入高亮,可点击,点击非根结点,回到该页面,点击跟根点,回到根结点第一个菜单下 前端如何做好ToB系统UI&交互体验

3、表格搜索表单

规范如下

  • 查询类表单独立成一个小模块放在表格上面,距离表格margin-bottom:16px
  • 表单每行三个表单项
  • 表单数量大于2时,可收起、展开按钮
  • 水平分栏右对齐,垂直每行之间margin:10px
  • 【查询】,【重置】放最后一个表单项后,总在其所在行的最右侧
  • 操作类(新建、删除)放在表头上面(见下),跟表格视为一个模块
  • 按钮之间间距统一为margin:16px
  • 查询类表单先【查询】后【重置】;查询按钮为主题色,重置按钮为白色
  • 查询表单类操作类按钮统一文案为【查询】【重置 】;不要图标
  • 查询表单无label的统一增加label  
  • 操作类按钮,【新增按钮】根据具体业务命名删除按钮文档统一为【删除】、【批量删除】
  • 查询类表单大于两个时,增加展开、收缩功能;默认收缩,收缩后展示一行 前端如何做好ToB系统UI&交互体验
  • 选项卡放最上面,如下

前端如何做好ToB系统UI&交互体验 搜索表单组件代码实现

// form-search/index.vue
<template>
  <div class="form-search-container">
    <!-- 放选项卡 -->
    <slot></slot>
    <el-form
      class="form-search-container__form"
      :inline="true"
      label-suffix=":"
      :label-width="labelWidth"
      :style="value.length % 3 == 0 && !collapse ? 'padding-bottom: 40px;' : ''"
    >
      <form-search-item
        v-for="(item, index) in formList"
        :key="index"
        :type="item.type"
        :label="item.label"
        :placeholder="item.placeholder"
        :options="item.options"
        :name="item.key"
        :isSlot="item.isSlot"
        :filterable="item.filterable"
        v-bind:value.sync="item.value"
      >
        <template v-slot:[item.key]>
          <slot :name="Array.isArray(item.key) ? item.key[0] : item.key"></slot>
        </template>
      </form-search-item>
      <el-form-item
        class="form-search-container__item form-search-container__btn"
      >
        <el-button type="primary" size="medium" @click="handleSearch"
          >查询</el-button
        >
        <el-button @click="resetForm" size="medium">重置</el-button>
        <el-button v-if="value.length > 2 && !hideCollapse" type="text" @click="handleCollapse"
          >{{ collapse ? '展开' : '收起' }}
          <i v-if="!collapse" class="el-icon-arrow-up"></i>
          <i v-else class="el-icon-arrow-down"></i>
        </el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
<script>
import FormSearchItem from './components/form-search-item'
import bus from '../../utils/event-bus'
export default {
  components: { FormSearchItem },
  props: {
    value: {
      type: Array,
      default: () => [],
    },
    labelWidth: {
      type: String,
      default: '80px',
    },
    hideCollapse: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    formList() {
      this.$nextTick(() => {
        bus.emit('form-search-collapse')
      })
      return this.collapse ? this.value.slice(0, 2) : this.value
    },
  },
  data() {
    return {
      collapse: false,
      initObj: {},
    }
  },
  created() {
    this.value.map((item) => {
      Object.assign(this.initObj, { [item.key]: item.value })
    })
  },
  methods: {
    resetForm() {
      this.value.map((item) => {
        item.value = this.initObj[item.key]
      })
      this.$emit('update:value', this.value)
      this.$emit('reset-click', this.initObj)
    },
    handleSearch() {
      const resObj = {}
      this.value.map((item) => {
        if (Array.isArray(item.key)) {
          item.key.map((keyItem, index) => {
            Object.assign(resObj, { [keyItem]: item.value[index] })
          })
        } else {
          Object.assign(resObj, { [item.key]: item.value })
        }
      })
      this.$emit('update:value', this.value)
      this.$emit('search-click', resObj)
    },
    handleCollapse() {
      this.collapse = !this.collapse
      bus.emit('form-search-collapse')
    },
  },
}
</script>

<style lang="less" scoped>
.form-search-container {
  background-color: #fff;
  margin-bottom: 8px;
  padding: 16px 10px 0;
}

.form-search-container__form {
  display: flex;
  flex-wrap: wrap;
  position: relative;
}

.form-search-container__item {
  flex: 0 0 30%;
  height: 32px;
}

.form-search-container__btn {
  position: absolute;
  right: 0;
  bottom: 0;
  margin-bottom: 10px;
}
</style>
// form-search/components/form-search-item.vue
<template>
  <el-form-item
    :label="label"
    class="form-search-container__item"
    :prop="Array.isArray(name) ? name[0] : name"
  >
    <slot v-if="isSlot" :name="name"></slot>
    <el-input
      v-if="type === 'input' && !isSlot"
      :placeholder="placeholder"
      v-model="value2"
      @change="handleChange"
    ></el-input>

    <el-select
      v-if="type === 'select'"
      v-model="value2"
      :placeholder="placeholder"
      @change="handleChange"
      :filterable="filterable"
    >
      <el-option
        v-for="(item, index) in options.list"
        :key="index"
        :label="item[options.label || 'label']"
        :value="item[options.value || 'value']"
      ></el-option>
    </el-select>

    <el-date-picker
      v-if="type === 'time'"
      type="daterange"
      v-model="value2"
      range-separator="至"
      start-placeholder="开始日期"
      end-placeholder="结束日期"
      :default-time="['00:00:00', '23:59:59']"
      @change="getTime"
    >
    </el-date-picker>
  </el-form-item>
</template>

<script>
export default {
  props: {
    formModel: {
      type: Object,
      default: () => {},
    },
    label: {
      type: String,
      default: '',
    },
    type: {
      type: String,
      default: 'input', //select
    },
    placeholder: {
      type: String,
      default: '',
    },
    value: {
      type: [String, Number, Array],
      default: '',
    },
    options: {
      type: Object,
      default: () => {
        return {
          list: [],
          label: 'label',
          value: 'value',
        }
      },
    },
    name: {
      type: [String, Array],
      default: () => '',
    },
    isSlot: {
      type: Boolean,
      default: false,
    },
    filterable: {
      type: Boolean,
      default: false,
    }
  },
  data() {
    return {
      value2: this.value,
    }
  },
  watch: {
    value(val) {
      this.value2 = val
    },
  },
  methods: {
    handleChange() {
      this.$emit('update:value', this.value2)
    },
    getTime(timeVl) {
      if (timeVl) {
        this.value2 = [timeVl[0].getTime(),timeVl[1].getTime()]
      }
      !this.value2 && (this.value2 = '')
      this.handleChange()
    },
  },
}
</script>

<style lang="less" scoped>
.form-search-container__item {
  flex: 0 0 30%;
}
.el-range-editor.el-input__inner {
  width: 245px;
}

.form-search-container__item /deep/ .el-form-item__content {
  width: 242px;
}

.form-search-container__item /deep/ .el-form-item__content .el-select {
  width: 100%;
}

.el-form-item {
  margin-bottom: 10px;
  display: flex;
}
</style>

4、表格

  • 排序:数字、时间类型增加排序、其他删除排序
  • 对齐:数字类居中对齐、文字类左对齐
  • 添加斑马纹
  • 表格图片统一
    • 统一处理宽高:60px
    • 表格列居中显示
    • 无图片时,统一文案为【无图片】
    • 图片加载失败时统一处理为自定义图片
  • 带跳转增加亮色样式
  • 不同状态前面增加标记<el-badge is-dot class="item"></el-badge>,左对齐
  • 表格增加全屏功能
  • 表格新增按钮
    • 操作类先【新增类按钮】后【删除类按钮】;新增类按钮为主题色,删除类按钮为红色
  • 表格操作按钮统一
    • 按钮列表头文案统一为【操作】
    • 操作列固定在表格右侧
    • 统一使用文字按钮 <el-button type="text">文字按钮</el-button>
    • 按钮之间加垂直分隔线 <el-divider direction="vertical">
    • 当按钮<=3个时,直接显示
    • 当按钮>3时,第一、二个按钮一般为【编辑】或者【详情】或者【删除】,其他归纳到【更多】按钮里
    • 文案统一为【编辑】【删除】【更多】;部分编辑按钮文档现在为修改
    • 当鼠标放到【更多按钮】或者【点击更多按钮】时,出现下拉菜单 <el-dropdown-menu slot="dropdown">
    • 一般按钮使用主题色,删除按钮红色- 没有权限操作时展示置灰,(还是显示原来颜色,只是置灰),目前都是不展示,不友好
    • <el-link type="info" disabled>信息链接

前端如何做好ToB系统UI&交互体验

写在最后

以上只是我实践过的部分经验,当然还有很多方面

UI&交互规范,往往容易被忽视,但随着页面、场景增多;无规范会使得系统难用,经过这次UI&交互统一,其实成本挺大,收益也很不好衡量,最好还是一开始就制定规范

转载自:https://juejin.cn/post/7081635333787516964
评论
请登录