前言
在重构的路上,总能写点什么东西出来 , 这组件并不复杂,放出来的总觉得有点用处
一方面当做笔记,一方面可以给有需要的人; 有兴趣的小伙伴可以瞅瞅。
效果图
实现的功能
- 渲染支持图片,文字,图文
- 支持删除条目(并给予父回调)
用到技术点:
css module
: 包括内置的继承特性,类似less
的嵌套写法那种- 用到的
react 16.6
特性
lazy, Suspense
来实现子组件的懒加载memo
让函数式组件有PureComponent
的特性(浅比较)
flexbox
来布局- 用了
lodash
的isEqual
来深度比较对象,用于getDerivedStateFromProps
(避免每次都更新state
)
代码实现
index.js
: 组件的主入口
import React, { PureComponent, lazy, Suspense } from 'react'; import { Avatar, Icon, Popover } from 'antd'; import style from './index.css'; // lodash 深比较 import isEqual from 'lodash/isEqual'; // 渲染不同内容的组件 const LazyComponent = lazy(() => import('./RenderContent')); export default class index extends PureComponent { state = { deleteBtnSpin: false, loading: true, list: [ { time: '2018-11-12 15:35:15', avatar: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90', nickname: '用户甲', pos: 1, voice: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-chat/3_508340417_c84f79407f5bc16b9e7ee0373631cf35.aac', text: '', }, { time: '2018-11-12 15:36:15', avatar: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90', nickname: '用户甲', pos: 1, voice: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-chat/3_508340417_c84f79407f5bc16b9e7ee0373631cf35.aac', text: '', }, { time: '2018-11-12 15:37:15', avatar: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90', nickname: '卡布奇诺', pos: 2, voice: '', text: '该词语多用于讽刺和揶揄调侃。也有送快递、查水电气、社区送温暖等引申说法。例如:(1)有人在网络上发表了不合乎相关法律法规或者破坏社会稳定和谐等消息而被警方捕;(2)在贴吧或论坛里拥有删帖权限的大小吧主,检查贴吧里是否存在灌水的帖子或跟帖,遇到就进行删除的行为。', }, { time: '2018-11-12 15:38:15', avatar: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90', nickname: '卡布奇诺', pos: 2, voice: '', img: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3040115650,4147729993&fm=26&gp=0.jpg', text: '该词语多用于讽刺和揶揄调侃。也有送快递、查水电气、社区送温暖等引申说法。例如:(1)有人在网络上发表了不合乎相关法律法规或者破坏社会稳定和谐等消息而被警方捕;(2)在贴吧或论坛里拥有删帖权限的大小吧主,检查贴吧里是否存在灌水的帖子或跟帖,遇到就进行删除的行为。', }, { time: '2018-11-12 15:39:15', avatar: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90', nickname: '卡布奇诺', pos: 2, voice: '', img: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3040115650,4147729993&fm=26&gp=0.jpg', }, ], }; static getDerivedStateFromProps(nextProps, prevState) { const { data } = nextProps; // 若是props和缓存state一致,则不更新state if (isEqual(prevState.prevData, nextProps.data)) { return null; } // 若是没有传入props也是 if (!data || !Array.isArray(data) || data.length <= 0) { return null; } return { list: data, prevData: nextProps.data, }; } // 唤醒子组件的回调过程 wakeUpLazyComponent = () => { return <div>loading.....</div>; }; // 悬浮条目显示删除按钮 showOperatBtn = index => { let tmpList = [...this.state.list]; tmpList = tmpList.map((item, innerIndex) => { if (index === innerIndex) { item.operatBtn = true; } else { item.operatBtn = false; } return item; }); this.setState({ list: tmpList }); }; // 关闭操作按钮 hideOperatBtn = index => { let tmpList = [...this.state.list]; tmpList = tmpList.map((item, innerIndex) => { item.operatBtn = false; return item; }); this.setState({ list: tmpList }); }; // 删除这条回复 deleteCurrentReplay = (index, itemInfo) => { let tmpList = [...this.state.list]; tmpList.splice(index, 1); this.setState({ list: tmpList }); // 给父的回调,把该item的所有信息返回,外部再去执行接口操作什么的 if (this.props.deleteItem) { this.props.deleteItem(itemInfo); } }; render() { const { list, deleteBtnSpin } = this.state; // 是否显示操作区域 const { operate } = this.props; // 渲染组件的前置条件 const isRender = list && list.length > 0; return ( <ul className={style['list-wrapper']}> {isRender && list.map((item, listIndex) => { return ( <Suspense fallback={this.wakeUpLazyComponent()} key={listIndex}> <li className={style['list-item']} onMouseOver={() => this.showOperatBtn(listIndex)} onMouseLeave={() => this.hideOperatBtn(listIndex)} > <span className={style['time']}>{item.time ? item.time : '时间占位符'}</span> <div className={ item.pos === 1 ? style['list-item-horizontal'] : style['list-item-horizontal-reverse'] } > <Avatar shape="square" src={ item.avatar ? item.avatar : 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png' } /> <div className={ item.pos === 1 ? style['content-wrapper-flex'] : style['content-wrapper'] } > <p className={item.pos === 1 ? style.nickname : style['nickname-right']}> {item.nickname ? item.nickname : '用户昵称占位符'} </p> <div className={style.content}> <LazyComponent {...item} /> </div> </div> {!!operate && item.operatBtn ? ( <Popover content={'此操作会删除该记录'} title="谨慎操作!" onMouseEnter={() => { this.setState({ deleteBtnSpin: true }); }} onMouseLeave={() => { this.setState({ deleteBtnSpin: false }); }} > <Icon type="delete" spin={deleteBtnSpin} style={{ fontSize: 24, alignSelf: 'flex-end', color: `${this.state.deleteBtnSpin ? '#ec1414' : '#1890ff'}`, }} onClick={() => this.deleteCurrentReplay(listIndex, item)} /> </Popover> ) : null} </div> </li> </Suspense> ); })} </ul> ); } }
RenderContent.js
:渲染对话条目
import React, { memo } from 'react'; import style from './index.css'; // antd 图文组件 import { Card } from 'antd'; const { Meta } = Card; const RenderContent = memo(props => { if (props.img && props.text) { return ( <Card hoverable style={{ width: 300 }} cover={<img alt="example" src={props.img ? props.img : ''} />} > <Meta description={props.text ? props.text : ''} /> </Card> ); } if (props.img) { return ( <div className={style['img-wrapper']}> <img className={style['img-preview']} src={props.img ? props.img : ''} alt="photos" /> </div> ); } if (props.text) { return <div className={style['bubble']}>{props.text}</div>; } if (props.voice) { return <audio src={props.voice ? props.voice : ''} controls />; } return null; }); export default RenderContent;
index.css
: 样式
composes
是css module
能识别的特殊字段,用于继承其他样式的
/* 列表全局样式 */ .list-wrapper { list-style-type: none; list-style: none; padding-left: 0; } /* 列表基本样式 */ .list-item { display: flex; flex-direction: column; justify-content: flex-start; align-content: flex-start; margin: 15px 0; } /* 水平展开 */ .list-item-horizontal { display: flex; justify-content: flex-start; align-items: flex-start; } /* 右对齐方式变化 */ .list-item-horizontal-reverse { composes: list-item-horizontal; flex-direction: row-reverse; } /* 用户名 */ .nickname { font-size: 12px; padding:0 10px; color: #8a8484; margin-bottom: 5px; } /* 用户名右对齐 */ .nickname-right { composes: nickname; text-align: right; } /* 时间样式 */ .time { text-align: center; background-color: #cecece; color: #fff; border-radius: 3px; align-self: center; font-size: 12px; padding: 5px; margin:5px; } /* 内容区域 */ .content-wrapper { margin: 0 15px; } /* 弹性伸缩 */ .content-wrapper-responsive { flex: 1; } /* 气泡文字 */ .bubble { padding: 8px; color: #333; max-width: 300px; line-height: 1.5; background-color: #a7e544; border-radius: 3px; text-align: left; text-indent: 10px; margin:0 3px; } /* 图片预览 */ .img-wrapper { box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 2px 1px -1px rgba(0, 0, 0, 0.12); border-radius: 3px; padding: 5px; } .img-preview { max-width: 200px; }
使用姿势
接受的props
data
, 格式是[Obj]
(数组对象);operate
: 布尔值(是否显示操作区域)
列表条目字段解释
- time: 时间
- avatar: 用户头像
- nickname:用户昵称
- pos: 1 (1在左侧渲染,2在右侧渲染)
- voice(音频)/text(文本内容)/ img(图片内容) => voice(唯一)/text + img / text(唯一)
{ time: '2018-11-12 15:35:15', avatar: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-avatar/3_avatar.jpg?x-oss-process=image/resize,m_fixed,w_90,h_90/quality,q_90', nickname: '用户甲', pos: 1, voice: 'https://sx-stag.oss-cn-shenzhen.aliyuncs.com/user-chat/3_508340417_c84f79407f5bc16b9e7ee0373631cf35.aac', text: '', img: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3040115650,4147729993&fm=26&gp=0.jpg' },