写点什么

「工作小记」后台系统代码简洁之路 - 详情页设计

作者:叶一一
  • 2022 年 9 月 10 日
    北京
  • 本文字数:15791 字

    阅读完需:约 52 分钟

「工作小记」后台系统代码简洁之路-详情页设计

乱花迷人眼

我就是被迷的那双眼。有时候需求来了,用熟悉的套路进行开发,确实很节省时间也能保证功能的稳定,但是这些开发的惯性无形中阻碍了我对技术的探索。


我一直想改造详情页,解放重复功能开发的劳动力,但是详情页一眼望都是内容平铺,好像并没有什么可做的代码设计。


后来我拨开繁花,发现详情页的组件化不必想的过于复杂,后台系统风格统一即可。因为大部分的详情页面是内容的展示,偶尔会出现少量的操作功能。将风格统一的部分进行组件化处理,操作功能使用回调函数放回当前页面,避免组件里做过多的业务逻辑。看,这不就成了。


项目基于 React 框架开发的,所以代码写法是 JSX 语法,组件开发使用的 hooks 函数式组件,UI 框架使用的是 antd。

欲起高楼,先建地基

开发前进行功能设计是我逐渐养成的一个良好习惯,有时候急于开发,可能漏掉一些设计细节或者功能。这次的详情页设计主要包括四个部分,UI 组件、模块划分、数据重组、操作回调。


设计的功能如下:

其中操作回调是为了实现功能性操作按钮的功能,比如取消操作、审核操作、查看等详情页常见的操作按钮。

设计实现

我捋了一下现有的业务,除了极个别的详情页设计的比较有自己的风格特点,其他基本都是包括 2-n 个模块展示数据,部分模块下会有操作按钮,某些模块下的某些数据项会有操作按钮,较长的页面会有快速定位导航等。


所以我会根据功能的复杂度递增,逐步的实现这个详情页 UI 组件。


注:前面功能实现我主要放关键代码,会把完整代码放在文章的末尾。

基础款详情页

纯展示,根据接口返回的字段,重组数据,之所以用重组数据的方式是因为某些数据需要特殊处理,比如时间数据,需要将时间戳转成日期格式;枚举数据,需要将返回值展示为具体文字。

模块划分

假设当前详情页有四个模块:用户信息、订单信息、快递信息、支付信息。四个模块内容展示有相似有不同,但是依旧可以把展示方式分成两种:一排两个的平铺展示和 Table 表格展示。


模块划分完成之后,页面呈现在脑海中也有了大致的结构。第一个明确的设计点也就有了,既然模块展示具有相似性。我就可以把 UI 渲染设计成数组循环的方式。对于不同的展示方式,可以根据模块的 key 值去区分定义展示类型。


detailBase.jsx


  • 根据模块的划分,定义 dataList 数组对象,后续页面渲染是使用 dataList 进行渲染的;

  • 设置 contentType-展示形式分类变量,其值为 row-平铺,table-表格。会根据 contentType 将模块展示成不同的形式;

  • 订单列表因为是 Table 格式,它的表格列的配置描述维护在常量管理文件中;


/** * @description 详情页 */import React, { useState, useEffect } from 'react';......import { ORDER_COLUMNS } from '@/constants/detailBase';
const DetailBase = () => { /** @name 页面内容数组对象 */ let dataListInit = [ { key: 'userInfo', // 模块key值 name: '用户信息', //模块标题 }, { key: 'orderList', name: '订单信息', columns: _.cloneDeep(ORDER_COLUMNS), }, { key: 'postInfo', name: '快递信息', }, { key: 'payInfo', name: '支付信息', }, ];
// 列表数据重组 dataListInit.map(item => { item.list = []; // 模块展示内容数组 item.contentType = 'row'; // 展示形式类型 row-平铺 table-表格 // =>true: 订单信息 展示为表格 if (item.key === 'orderList') { item.contentType = 'table'; } }); let [dataList, setDataList] = useState(dataListInit); return <></>;};export default DetailBase;
复制代码


constants/detailBase.js


对于常量管理,一般会放到常量文件中。(具体内容可以查看 3.3.3,为了节省空间此处不再具体列出)

数据重组

detailBase.jsx


  • 请求详情数据,获得返回值。一般返回值都是嵌套对象的格式,所以可以将返回值的对象 key 值和设置的 dataList 中 key 一一对应;

  • 根据模块设置模块的 list 值,最终页面渲染使用的是每个模块的 list 对象。contentType 类型为 table 时,可以直接将返回值赋值给该模块的 list 变量;contentType 类型为 row 时,需要进行数据的重组。(注:之所以需要重组数据是因为要特殊处理时间戳、枚举值等特殊返回值,比如时间戳要展示为日期格式,枚举值根据返回值展示文字描述等);


/** * @description 详情页 */import React, { useState, useEffect } from 'react';......const DetailBase = () => {  ......  /**   * 用户信息-展示数据重组   * @param {Object} data 需要获取的项的对象   * @return {Object} 获得的值   */  const getUserData = data => {    let list = [      {        name: '姓名',        value: data.name,      },      {        name: '年龄',        value: data.age,      },      {        name: '电话',        value: data.phone,      },      {        name: '收货地址',        value: data.address,      },    ];    return list;  };
/** * 快递信息-展示数据重组 * @param {Object} data 需要获取的项的对象 * @return {Object} 获得的值 */ const getPostData = data => { let list = [ { name: '付款单号', value: data.postNum, }, { name: '付款公司', value: data.postName, }, ]; return list; };
/** * 支付信息-展示数据重组 * @param {Object} data 需要获取的项的对象 * @return {Object} 获得的值 */ const getPayData = data => { let list = [ { name: '付款时间', value: data.payAt ? moment(data.payAt).format('YYYY-MM-DD HH:mm:ss') : '', }, { name: '付款金额', value: data.payMoney, }, { name: '操作时间', value: data.payOperateAt ? moment(data.payOperateAt).format('YYYY-MM-DD HH:mm:ss') : '', }, ]; return list; };
/** * 获取列表项的实际值 * @param {Object} item 需要获取的项的对象 * @param {Object} res 接口请求数据 * @return {Object} 获得的值 */ const getItemList = (item, data) => { let obj = { userInfo: getUserData(data), postInfo: getPostData(data), payInfo: getPayData(data), }; return obj[item.key]; };
/** * 初始化数据 */ const initData = () => { // 请求接口获取返回值 let res = { userInfo: { name: '张三', age: 30, phone: '12345678912', address: '北京市朝阳区', }, payInfo: { payAt: 1641039600000, payMoney: 999, payOperateAt: 1641038400000, }, orderList: [ { name: '跑鞋·追光者', color: '白色', creatAt: 1641038400000, payAt: 1641039600000, haveFreight: 1, }, { name: '运动裤·逐梦', color: '黑色', creatAt: 1641038400000, payAt: 1641039600000, haveFreight: 1, }, { name: '外套·闪光者', color: '蓝色', creatAt: 1641038400000, payAt: 1641039600000, haveFreight: 1, }, ], postInfo: { postName: '顺丰', postNum: '1111', }, }; let list = _.cloneDeep(dataListInit); // 数据重置 list.map(item => { if (item.contentType === 'table') { item.list = res[item.key]; } else { let data = res[item.key]; item.list = getItemList(item, data); } }); setDataList(list); };
useEffect(() => { initData(); }, []);
return <></>;};
export default DetailBase;
复制代码

详情组件

因为是根据业务进行的功能设计,所以我把详情组件放到了业务组件下面。

bundleComponents/common/DetailBase.js


  • 模块的展示,使用 antd 提供的 Card 卡片组件进行页面布局;Card卡片官网地址

  • row 平布类型的展示,使用 antd 提供的 Row、Col 栅格组件进行页面布局;Row、Col栅格组件官网地址

  • table 类型的展示,使用 ante 提供的 Table 组件进行页面布局;Table组件官网地址

  • 组件通信,props 传参为 dataList 数据数组对象;

  • 注:像边距 mt/mb 之类的样式设置,我们的项目里面是定义的全局样式,直接使用的。


/** * @description 公共业务组件-详情 */import React from 'react';import PropTypes from 'prop-types';import { Card, Row, Col, Table } from 'antd';
const CommonDetailBase = ({ ...props }) => { const { dataList } = props;
/** * row类页面内容回显 * @param {Object} data 展示内容对象 * @return {Element} 展示内容 */ const dataRowContent = data => { const list = data.list ? data.list : []; return ( <> {list.map((rowItem, rowIndex) => { return ( <Col span={12} key={rowIndex}> <Card size='small'> <div> {rowItem.name}:{rowItem.value} </div> </Card> </Col> ); })} </> ); };
/** * Table类页面内容回显 * @param {Object} item 展示内容对象 * @return {Node} 展示内容 */ const dataTableContent = item => { let list = item.list ? item.list : []; return <Table dataSource={list} columns={item.columns} rowKey={record => record.id} pagination={false} size='small' />; };
return ( <div> <div className='view-content'> {dataList.map(item => { return ( <Card type='inner' title={item.name} id={item.key} key={item.key} className='mb20'> {item.contentType === 'row' ? <Row gutter={[12, 12]}>{dataRowContent(item)}</Row> : null} {item.contentType === 'table' ? dataTableContent(item) : null} {item.moduleBottomName ? ( <Button type='primary' onClick={() => item.moduleBottomView(item)} className='mt20'> {item.moduleBottomName} </Button> ) : null} </Card> ); })} </div> </div> );};
CommonDetailBase.propTypes = { dataList: PropTypes.array, // 页面展示数组对象};
CommonDetailBase.defaultProps = { dataList: [],};
export default CommonDetailBase;
复制代码


页面使用

/** * @description 详情页 */import React, { useState, useEffect } from 'react';......// 引入组件import { CommonDetailBase } from '@/bundleComponents';......const DetailBase = () => {  return <CommonDetailBase dataList={dataList} />;};
export default DetailBase;
复制代码

升级款详情页

所谓升级款,即在原来的基础上功能更丰富。比如我们的业务需求,模块下面会跟着操作按钮,页面底部会有操作按钮,页面带导航条。以及如果我们想组件功能更强,需要支持的情况更多,可以支持某个模块自定义展示。这个时候需要在原来的基础上进行功能扩展.

详情组件

详情组件已开发好了,新增功能只需要在原来的基础上新增代码逻辑即可

bundleComponents/common/DetailBase.js


  • 导航条,使用 antd 提供的 Affix 固钉组件,Affix固钉官网地址

  • affixTabs:导航条数据对象,数组类型

  • afffixIndex:当前选中导航变量,字符串类型

  • 模块可以使用自定义展示,在模块代码中加入 children 变量的判断,如果存在,则展示 children 内容,如果不存在,则按照组件中的展示;

  • 模块底部可以添加操作按钮,支持按钮组,根据 moduleBottomList 数组变量的值判断,如果有值,则循环展示按钮组,如果不存在,则不展示;

  • 数据项可以使用自定义展示,在数据项代码中加入 children 变量的判断,如果存在,则展示 children 内容,如果不存在,则按照组件中的展示;

  • 数据项左侧可以添加操作按钮,支持按钮组,根据 colBtnList 数组变量的值判断,如果有值,则循环展示按钮组,如果不存在,则不展示;

/** * @description 公共业务组件-详情 */import React, { useState } from 'react';......const CommonDetailBase = ({ ...props }) => {  const { dataList, affixTabs } = props;
/** @name 当前所在导航index值 */ const [afffixIndex, setAfffixIndex] = useState(props.afffixIndex);
/** @name 是否展示导航 */ const hasAffixTabs = !!affixTabs;
/** * 快速定位方法 * @param {string} id 定位到的id值 * @return {void} 无 */ const fastGo = id => { let element = document.getElementById(id); let view = document.querySelector('.view'); view.scrollTo({ top: element.offsetTop - 90, }); };
/** * 右侧锚点导航-切换 * @param {Object} item 选择的导航标签 * @return {void} 无 */ const tabChange = item => { setAfffixIndex(item.key); fastGo(item.key); };
/** * row类页面内容回显 * @param {Object} data 展示内容对象 * @return {Element} 展示内容 */ const dataRowContent = data => { const list = data.list ? data.list : []; return ( <> {list.map((rowItem, rowIndex) => { return ( <Col span={12} key={rowIndex}> <Card size='small'> {rowItem.children ? ( <>{rowItem.children}</> ) : ( <div> {rowItem.name}:{rowItem.value} {rowItem.colBtnList && rowItem.colBtnList.map((colBtnItem, colBtnIndex) => { return ( <Button className='ml10' type='primary' onClick={() => colBtnItem.colBtnCallback(colBtnItem, rowItem)} key={colBtnIndex}> {colBtnItem.name} </Button> ); })} </div> )} </Card> </Col> ); })} </> ); };
/** * Table类页面内容回显 * @param {Object} item 展示内容对象 * @return {Node} 展示内容 */ const dataTableContent = item => { let list = item.list ? item.list : []; return <Table dataSource={list} columns={item.columns} rowKey={record => record.id} pagination={false} size='small' />; };
return ( <div className={style['detail-base']}> <div className='view-content' id='view'> {dataList.map(item => { return ( <Card type='inner' title={item.name} id={item.key} key={item.key} className='mb20'> {item.children ? ( <>{item.children}</> ) : ( <> {item.contentType === 'row' ? <Row gutter={[12, 12]}>{dataRowContent(item)}</Row> : null} {item.contentType === 'table' ? dataTableContent(item) : null} </> )} {item.moduleBottomList && item.moduleBottomList.map((moduleBtnItem, moduleBtnIndex) => { return ( <Button className='mr10 mt20' type='primary' onClick={() => moduleBtnItem.moduleBtnCallback(moduleBtnItem, item)} key={moduleBtnIndex}> {moduleBtnItem.name} </Button> ); })} </Card> ); })} {/* 右侧锚点导航 */} {hasAffixTabs ? ( <Affix offsetTop={120} className='sider-affix'> <ul className='affix'> {affixTabs.map((item, index) => ( <li key={index}> <div className={classnames('affix-item', { current: afffixIndex === item.key })} onClick={() => tabChange(item)}> {item.name} </div> </li> ))} </ul> </Affix> ) : null} </div> </div> );};
CommonDetailBase.propTypes = { dataList: PropTypes.array, // 页面展示数组对象 affixTabs: PropTypes.array, // 导航数组对象 afffixIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // 导航默认定位};
CommonDetailBase.defaultProps = { dataList: [],};
export default CommonDetailBase;
复制代码


bundleComponents/common/style.less

重置了一下导航的样式。(样式的具体内容可以查看 3.3.1,为了节省空间此处不再具体列出)

导航条

detailBase.jsx


  • 导航条数据可以直接使用页面列表数据,因为定位的 key 和页面列表的 key 值做了一致性的处理;

  • 通过设置 afffixIndex 的值,可以控制当前导航固定的位置;

  • 当子组件的 props 传参比较复杂的时候,可以设置一个 config 对象,比如 detailConfig 包含了所有 props,子组件使用时直接用...(扩展运算符)进行展开;

/** * @description 详情页 */import React, { useState, useEffect } from 'react';......import { CommonDetailBase } from '@/bundleComponents';const DetailBase = () => {  /** @name 页面导航  */  const affixTabs = [];  ......  // 列表数据重组  dataListInit.map(item => {    ......    // 设置导航条数据    affixTabs.push(item);  });  ......  /** @name 详情组件配置项  */  const detailConfig = {    afffixIndex: 'userInfo',    affixTabs: affixTabs,    dataList: dataList,  };
return ( <div> <CommonDetailBase {...detailConfig} /> </div> );};export default DetailBase;
复制代码

模块下的操作按钮

detailBase.jsx

  • moduleBottomList:模块下的按钮组数组变量,控制操作按钮组是否展示,当它有值时按钮展示,没值时按钮不展示;

  • moduleBottomCallback:操作按钮元素的操作回调函数,可以做一些操作处理;

/** * @description 详情页 */import React, { useState, useEffect } from 'react';......const DetailBase = () => {  ......  let dataListInit = [    ......    {      key: 'postInfo',      name: '快递信息',    },    ......  ];  ......  const initData = () => {    let list = _.cloneDeep(dataListInit);      list.map(item => {      ......      // =>true: 快递信息 表格项处理      if (item.key === 'postInfo') {        item.moduleBottomList = [          {            name: '快递详情',            moduleBtnCallback: (_, data) => moduleBottomCallback(data, res),          },          {            name: '快递详情2',            moduleBtnCallback: (_, data) => moduleBottomCallback(data, res),          },        ];      }    })  }  ......};export default DetailBase;
复制代码

数据项的操作按钮

detailBase.jsx


  • colBtnList:数据项的操作按钮组,控制操作按钮是否展示,当它有值时按钮展示,没值时按钮不展示;

  • colBtnCallback:操作按钮元素的操作回调函数,可以做一些操作处理;

/** * @description 详情页 */import React, { useState, useEffect } from 'react';......const DetailBase = () => {  ......  const getUserData = data => {    let list = [      ......      {        name: '收货地址',        value: data.address,        colBtnList: [          {            name: '地址详情',            colBtnCallback: () => {              window.open('https://juejin.cn/', '_blank');            },          },        ],      },      .......    ];    return list;  };  ......};export default DetailBase;
复制代码

模块为自定义展示

detailBase.jsx

将需要自定义展示的模块对象的 children 值设置为需要展示的内容即可

/** * @description 详情页 */import React, { useState, useEffect } from 'react';......const DetailBase = () => {  ......  /**  * 支付模块展示  * @param {Object} dafa 展示的数据对象  */  const getPayInfo = data => {    return (      <Row gutter={[8, 8]}>        {data.list.map((item, index) => {          return (            <Col span={24} key={index}>              {item.name}:{item.value}            </Col>          );        })}      </Row>    );  };  ......  const initData = () => {    ......    let list = _.cloneDeep(dataListInit);    list.map(item => {    ......      // =>true: 支付信息 自定义展示      if (item.key === 'payInfo') {        item.children = getPayInfo(item);      }    });    setDataList(list);  };  ......};export default DetailBase;
复制代码

数据项为自定义展示

detailBase.jsx

将需要自定义展示的模块下的数据项对象的 children 值设置为需要展示的内容即可

/** * @description 详情页 */import React, { useState, useEffect } from 'react';......const DetailBase = () => {  ......  /**  * 图片类型展示  * @param {Object} data 展示的数据对象  */  const getImageView = data => {    return (      <>        头像:<Button type="link">编辑</Button>        <Row gutter={(12, 12)}>          <Col span={4}>            <img style={{ width: '80px', height: '80px', display: 'block' }} src={data.headImage} />          </Col>        </Row>      </>    );  };  ......  const getUserData = data => {    let list = [      ......      {        name: '头像',        children: getImageView(data),      },    ];    return list;  };  ......};export default DetailBase;
复制代码


以上,一个功能相对全面的详情页组件就完成了。

完整代码

详情页组件

bundleComponents/common/DetailBase.js

/** * @description 公共业务组件-详情 */import React, { useState } from 'react';import PropTypes from 'prop-types';import classnames from 'classnames';import { Button, Card, Row, Col, Table, Affix } from 'antd';import style from './style';
const CommonDetailBase = ({ ...props }) => { const { dataList, affixTabs } = props;
/** @name 当前所在导航index值 */ const [afffixIndex, setAfffixIndex] = useState(props.afffixIndex);
/** @name 是否展示导航 */ const hasAffixTabs = !!affixTabs;
/** * 快速定位方法 * @param {string} id 定位到的id值 * @return {void} 无 */ const fastGo = id => { let element = document.getElementById(id); let view = document.querySelector('.view'); view.scrollTo({ top: element.offsetTop - 90, }); };
/** * 右侧锚点导航-切换 * @param {Object} item 选择的导航标签 * @return {void} 无 */ const tabChange = item => { setAfffixIndex(item.key); fastGo(item.key); };
/** * row类页面内容回显 * @param {Object} data 展示内容对象 * @return {Element} 展示内容 */ const dataRowContent = data => { const list = data.list ? data.list : []; return ( <> {list.map((rowItem, rowIndex) => { return ( <Col span={12} key={rowIndex}> <Card size='small'> {rowItem.children ? ( <>{rowItem.children}</> ) : ( <div> {rowItem.name}:{rowItem.value} {rowItem.colBtnList && rowItem.colBtnList.map((colBtnItem, colBtnIndex) => { return ( <Button className='ml10' type='primary' onClick={() => colBtnItem.colBtnCallback(colBtnItem, rowItem)} key={colBtnIndex}> {colBtnItem.name} </Button> ); })} </div> )} </Card> </Col> ); })} </> ); };
/** * Table类页面内容回显 * @param {Object} item 展示内容对象 * @return {Node} 展示内容 */ const dataTableContent = item => { let list = item.list ? item.list : []; return <Table dataSource={list} columns={item.columns} rowKey={record => record.id} pagination={false} size='small' />; };
return ( <div className={style['detail-base']}> <div className='view-content' id='view'> {dataList.map(item => { return ( <Card type='inner' title={item.name} id={item.key} key={item.key} className='mb20'> {item.children ? ( <>{item.children}</> ) : ( <> {item.contentType === 'row' ? <Row gutter={[12, 12]}>{dataRowContent(item)}</Row> : null} {item.contentType === 'table' ? dataTableContent(item) : null} </> )} {item.moduleBottomList && item.moduleBottomList.map((moduleBtnItem, moduleBtnIndex) => { return ( <Button className='mr10 mt20' type='primary' onClick={() => moduleBtnItem.moduleBtnCallback(moduleBtnItem, item)} key={moduleBtnIndex}> {moduleBtnItem.name} </Button> ); })} </Card> ); })} {/* 右侧锚点导航 */} {hasAffixTabs ? ( <Affix offsetTop={120} className='sider-affix'> <ul className='affix'> {affixTabs.map((item, index) => ( <li key={index}> <div className={classnames('affix-item', { current: afffixIndex === item.key })} onClick={() => tabChange(item)}> {item.name} </div> </li> ))} </ul> </Affix> ) : null} </div> </div> );};
CommonDetailBase.propTypes = { dataList: PropTypes.array, // 页面展示数组对象 affixTabs: PropTypes.array, // 导航数组对象 afffixIndex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // 导航默认定位};
CommonDetailBase.defaultProps = { dataList: [],};
export default CommonDetailBase;
复制代码


bundleComponents/common/style.less

:local(.detail-base) {  position: relative;  padding-left: 20px;  background: #fff;
.view-content { padding-top: 20px; padding-right: 120px; padding-bottom: 80px; }
.sider-affix { position: fixed; top: 50px; right: 10px;
.affix { padding-left: 0; margin: 16px 0; font-size: 12px; list-style: none; border-left: 1px solid #f0f0f0;
li { padding-left: 0; margin-left: 0; line-height: 2; list-style: none; }
.affix-item { display: block; width: 110px; padding-left: 16px; margin-left: -1px; overflow: hidden; font-size: 16px; color: rgba(0, 0, 0, 0.85); text-overflow: ellipsis; white-space: nowrap; cursor: pointer; border-left: 1px solid transparent;
&.current { color: #1890ff; border-color: #1890ff; } } } }}
复制代码

详情页面

detailBase.jsx

/** * @description 详情页 */import React, { useState, useEffect } from 'react';import moment from 'moment';import _ from 'lodash';import { Row, Col, Button } from 'antd';import { ORDER_COLUMNS } from '@/constants/detailBase';import { CommonDetailBase } from '@/bundleComponents';
const DetailBase = () => { /** @name 页面导航 */ const affixTabs = [];
let dataListInit = [ { key: 'userInfo', // 模块key值 name: '用户信息', //模块标题 }, { key: 'orderList', name: '订单信息', columns: _.cloneDeep(ORDER_COLUMNS), }, { key: 'postInfo', name: '快递信息', }, { key: 'payInfo', name: '支付信息', }, ];
// 列表数据重组 dataListInit.map(item => { item.list = []; // 模块展示内容数组 item.contentType = 'row'; // 展示形式类型 row-平铺 table-表格 // =>true: 订单信息 展示为表格 if (item.key === 'orderList') { item.contentType = 'table'; } affixTabs.push(item); });
let [dataList, setDataList] = useState(dataListInit);
/** * 图片类型展示 * @param {Object} data 展示的数据对象 */ const getImageView = data => { return ( <> 头像:<Button type='link'>编辑</Button> <Row gutter={(12, 12)}> <Col span={4}> <img style={{ width: '80px', height: '80px', display: 'block' }} src={data.headImage} /> </Col> </Row> </> ); };
/** * 用户信息-展示数据重组 * @param {Object} data 需要获取的项的对象 * @return {Object} 获得的值 */ const getUserData = data => { let list = [ { name: '姓名', value: data.name, }, { name: '年龄', value: data.age, }, { name: '电话', value: data.phone, }, { name: '收货地址', value: data.address, colBtnList: [ { name: '地址详情', colBtnCallback: () => { window.open('https://juejin.cn/', '_blank'); }, }, ], }, { name: '头像', children: getImageView(data), }, ]; return list; };
/** * 快递信息-展示数据重组 * @param {Object} data 需要获取的项的对象 * @return {Object} 获得的值 */ const getPostData = data => { let list = [ { name: '付款单号', value: data.postNum, colBtnList: [ { name: '快递详情', colBtnCallback: () => { window.open('https://juejin.cn/', '_blank'); }, }, ], }, { name: '付款公司', value: data.postName, }, ]; return list; };
/** * 支付信息-展示数据重组 * @param {Object} data 需要获取的项的对象 * @return {Object} 获得的值 */ const getPayData = data => { let list = [ { name: '付款时间', value: data.payAt ? moment(data.payAt).format('YYYY-MM-DD HH:mm:ss') : '', }, { name: '付款金额', value: data.payMoney, }, { name: '操作时间', value: data.payOperateAt ? moment(data.payOperateAt).format('YYYY-MM-DD HH:mm:ss') : '', }, ]; return list; }; /** * 获取列表项的实际值 * @param {Object} item 需要获取的项的对象 * @param {Object} res 接口请求数据 * @return {Object} 获得的值 */ const getItemList = (item, data) => { let obj = { userInfo: getUserData(data), postInfo: getPostData(data), payInfo: getPayData(data), }; return obj[item.key]; };
/** * 支付模块展示 * @param {Object} dafa 展示的数据对象 */ const getPayInfo = data => { return ( <Row gutter={[8, 8]}> {data.list.map((item, index) => { return ( <Col span={24} key={index}> {item.name}:{item.value} </Col> ); })} </Row> ); };
/** * 模块下的按钮操作 * @param {Object} dafa 数据对象 */ const moduleBottomCallback = data => { // 按钮操作 };
/** * 初始化数据 */ const initData = () => { let res = { userInfo: { name: '张三', age: 30, phone: '12345678912', address: '北京市朝阳区', headImage: 'https://p6-passport.byteacctimg.com/img/user-avatar/c6c1a335a3b48adc43e011dd21bfdc60~200x200.image', }, payInfo: { payAt: 1641039600000, payMoney: 999, payOperateAt: 1641038400000, }, orderList: [ { name: '跑鞋·追光者', color: '白色', creatAt: 1641038400000, payAt: 1641039600000, haveFreight: 1, }, { name: '运动裤·逐梦', color: '黑色', creatAt: 1641038400000, payAt: 1641039600000, haveFreight: 2, }, { name: '外套·闪光者', color: '蓝色', creatAt: 1641038400000, payAt: 1641039600000, haveFreight: 1, }, ], postInfo: { postName: '顺丰', postNum: '1111', }, }; let list = _.cloneDeep(dataListInit); list.map(item => { if (item.contentType === 'table') { item.list = res[item.key]; } else { let data = res[item.key]; item.list = getItemList(item, data); } // =>true: 快递信息 表格项处理 if (item.key === 'postInfo') { item.moduleBottomList = [ { name: '快递详情', moduleBtnCallback: (_, data) => moduleBottomCallback(data, res), }, { name: '快递详情2', moduleBtnCallback: (_, data) => moduleBottomCallback(data, res), }, ]; } // =>true: 支付信息 自定义展示 if (item.key === 'payInfo') { item.children = getPayInfo(item); } }); setDataList(list); };
useEffect(() => { initData(); }, []);
/** @name 详情组件配置项 */ const detailConfig = { afffixIndex: 'userInfo', affixTabs: affixTabs, dataList: dataList, };
return ( <div> <CommonDetailBase {...detailConfig} /> </div> );};
export default DetailBase;
复制代码

常量

constants/detailBase.js

/** * @description 详情页公共常量 */import moment from 'moment';
/** @name 订单信息列表项 */export const ORDER_COLUMNS = [ { title: '商品名称', dataIndex: 'name', key: 'name', }, { title: '颜色', dataIndex: 'color', key: 'color', }, { title: '下单时间', dataIndex: 'creatAt', key: 'creatAt', render(val) { return val ? moment(val).format('YYYY-MM-DD HH:mm:ss') : ''; }, }, { title: '支付时间', dataIndex: 'payAt', key: 'payAt', render(val) { return val ? moment(val).format('YYYY-MM-DD HH:mm:ss') : ''; }, }, { title: '是否有运费险', dataIndex: 'haveFreight', key: 'haveFreight', render(val) { return val === 2 ? '有' : '无'; }, },];
复制代码

完整 UI

总结

对后台系统低代码的开发与设计,仍在探索中,后续想实现列表页和操作表单项的设计,这样后台系统的基础的页面能快速完成搭建。


当然了,更好的方式是通过拖拽实现页面的搭建,但是现有的开发精力并不能支撑完成这种复杂的开发。所以先从基础出发,逐步升级自己的技能。

发布于: 刚刚阅读数: 4
用户头像

叶一一

关注

苍生涂涂,天下缭燎,诸子百家,唯我纵横。 2022.09.01 加入

非职业传道受业解惑前端程序媛,华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。

评论

发布
暂无评论
「工作小记」后台系统代码简洁之路-详情页设计_前端_叶一一_InfoQ写作社区