【创新实训】前端开发记录(二):主页面

山软创新实训前端开发记录

【创新实训】前端开发记录(一):基础篇
【创新实训】前端开发记录(二):主页面
【创新实训】前端开发记录(三):特殊效果



前端组件库

该项目使用Material-UI作为组件库。这是一个符合Material Design设计规范的React组件库。
使用以下命令分别安装Material-UI的核心组件库、图标包和实验性功能。

$ npm install @material-ui/core
$ npm install @material-ui/icons
$ npm install @material-ui/lab

主页面设计

需求描述

主页面分为两个模块,分别为“电影展厅”和“表格展示”。电影展厅模块将电影列表以海报+评分的格式展现给用户,并可以给用户提供部分电影的信息(例如导演、主演等)。电影列表模块则使用表格详细列举电影信息,并可以按照电影评分和上映日期进行排序。
为了适应不同尺寸设备的浏览需求,需要使用响应式技术制作页面,达到在PC、平板和手机上都能有较好的浏览体验的效果。

效果预览

PC端

电影展厅PC端效果预览
表格展示PC端效果预览

移动端

电影展厅移动端效果预览
表格展示移动端效果预览

设计拆分

主页布局

主页面的整块布局大体可以分为:左侧抽屉Drawer,顶部标题栏ToolBar,搜索区域以及数据展示区域。其中除了数据展示区域在两个模块中无法共用,其余组件都可以进行复用。
抽屉中放入切换“电影展厅”模式和“列表展示”模式的按钮,并在PC端屏幕中位于左侧常驻,在移动端屏幕中隐藏,通过标题栏左侧的汉堡图标点击打开。这样设计也比较符合操作逻辑。

搜索栏

目前搜索栏的功能需求为:按照电影名称、评分、上映日期、主演、导演和来源等参数为限制进行搜索。电影名称、主演和导演为需要手动输入的文本框;评分和上映日期为了方便起见,除了文本框外还提供一个滑块,便于用户进行操作;来源提供一个下拉框供用户选择。

数据展示区域

对于电影展厅模式而言,PC端需要海报、电影名称和评分从上至下排布作为一个卡片,鼠标移到图片上时,卡片变大,展示更多信息。而这种操作和布局对于移动端来说是不可接受的,移动端需要采用更为直观的卡片展示方式:即一行一个卡片,海报和详细信息左右排布。
对于列表展示模式,PC端与移动端可以共用一个表格,只需要保证表格能够在数据超出时进行滑动即可。

部分代码实现

TopBar

左侧抽屉的宽度定义为标准的240px,样式使用Material-UI提供的makeStyles函数动态生成,无需在CSS文件中定义。动态生成样式的好处是可以更方便地绘制响应式布局。

const drawerWidth = 240;
const navLabels = [ '电影展厅', '表格展示' ]
const navIcons = [ <AppIcon />, <ArtTrackIcon /> ]

const drawer = (
  <div>
    <div className={classes.toolbar} />
    <Divider />
    <List>
      {navLabels.map((text, index) => (
        <ListItem
        	button key={text}
        	selected={props.mode === index}
        	onClick={() => handleListItemClick(index)}>
          <ListItemIcon>{navIcons[index]}</ListItemIcon>
          <ListItemText primary={text} />
        </ListItem>
      ))}
    </List>
  </div>
);

加上AppBarDrawer组件后的JSX如下:

<div className={classes.root}>
  <AppBar position="fixed" className={classes.appBar}>
    <Toolbar>
      <IconButton>
        <MenuIcon />
      </IconButton>
      <Typography variant="h6" noWrap>
        MVRankings 电影评价网
      </Typography>
    </Toolbar>
  </AppBar>
  <nav className={classes.drawer} aria-label="main">
    <Hidden mdUp implementation="css">
      <Drawer>
        {drawer}
      </Drawer>
    </Hidden>
    <Hidden smDown implementation="css">
      <Drawer>
        {drawer}
      </Drawer>
    </Hidden>
  </nav>
  <main className={classes.content}>
    <div className={classes.toolbar} />
    {props.child}
  </main>
</div>

SearchBar

搜索栏部分使用ExpansionPanel组件作为容器,并使用Grid组件进行输入组件的编排。Grid使用flex进行弹性布局,能够很好地兼容不同尺寸的屏幕。配合Material-UI的Grid栅格系统,可以十分便捷地定义不同尺寸屏幕下组件所占大小。
表格区域的HTML如下:

<form noValidate autoComplete="off" style={{width: '100%'}}>
    <Grid container spacing={4}>
        <Grid item xs={12} sm={6} md={6} lg={4} xl={3}>
            <SearchBarMovieName data={props.data} update={props.update} disabled={props.loading} />
        </Grid>
        <Grid item xs={12} sm={6} md={6} lg={4} xl={3}>
            <SearchBarMovieRate data={props.data} update={props.update} disabled={props.loading} />
        ......
    </Grid>
</form>

由于搜索数据需要被搜索栏和数据展示区域共享,所以这里利用React的“状态提升”,将搜索的数据保存至共同父组件的state中,即包含了这些组件的Page。

class HomePage extends React.Component {
    constructor(props) {
        super(props)
        const movieSources = [
            '任意', '豆瓣', '猫眼', '时光网'
        ]
        this.state = {
            loading: false,
            data: {},
            searchData: {
                name: '',
                rate_min: 0,
                rate_max: 10,
                time_min: 1895,
                time_max: new Date().getFullYear() + 5,
                directors: '',
                stars: '',
                source: movieSources[0]
            },
            totalPages: 1,
            curPage: 1,
            ......
        }
    }
    ......
}

其中,输入框数据更改、提交查询等操作也转移到了父组件中,由HomePage进行数据的请求和加载工作。

List

电影展厅模块

电影展厅模式的数据展示区域需要为PC端和移动端准备两个List,这里就利用makeStyles函数动态生成样式,在屏幕尺寸的间断点切换display属性,就能够做到移动端和PC端的不同显示了。这种做法虽然在开发上非常便捷,但带来一个问题,即同样的数据生成了2个不同的DOM元素,有影响性能之嫌,好在每页的数据量不会很大,性能问题可以忽略不计。

const useStyles = makeStyles((theme) => {
	......
    desktop: {
        [theme.breakpoints.down('sm')]: {
            display: 'none'
        },
    },
    mobile: {
        [theme.breakpoints.up('md')]: {
            display: 'none'
        },
    }
})

export default function SquareList(props) {
  ......
  return (
     <>
         <Grid className={classes.desktop} container justify="space-evenly" spacing={6} style={{flexGrow: 1}} onMouseEnter={moveHandler.cancelAll}>
             {props.loading || props.data.data === undefined ? getLoadingItems() : getItems() }
         </Grid>
         <Grid className={classes.mobile} container justify="space-evenly" spacing={3} style={{flexGrow: 1}}>
             {props.loading || props.data.data === undefined ? getMobileLoadingItems() : getMobileItems() }
         </Grid>
     </>
 )
}

实现PC端鼠标移过卡片时卡片放大的效果,单纯地在CSS中使用:hover伪元素实现起来比较困难,这时可以借助每个卡片的state来实现。分别在卡片组件的onMouseEnteronMouseLeave事件中更新是否需要展示详情的状态即可实现。

handleMouseEnter() {
	this.setState({
		hover: true
	})
}

handleMouseLeave() {
	this.setState({
		hover: false
	})
}

return (
	...
	<div onMouseLeave={this.handleMouseLeave}>
		详情卡片内容...
	</div>
	<div onMouseEnter={this.handleMouseEnter}>
		卡片内容...
	</div>
	...
)

但是这种方法有一个弊端,即鼠标快速在多个卡片上滑过时,有大概率一些卡片的onMouseEnter事件和onMouseLeave事件不会触发,最终导致即使鼠标移开,也有一些卡片呈详情状态,严重影响使用体验。
这里的解决方案如下:使用一个额外的控制器控制所有卡片是否放大的状态,在一个卡片放大时,保证其余所有卡片都不会呈放大的状态。在鼠标移开卡片(也就是移入了列表的空白区域)时,对所有卡片进行取消放大的操作。
空白区域和卡片
这里再次用到了状态提升。不过这一次是另一种做法。在卡片的父组件(列表)中使用一个全局的MoveHandler保存所有卡片的this引用,并在MoveHandler中调用this.setState()函数进行卡片状态的更新。列表在生成每个卡片时,调用handler的regComponent函数进行组件的注册(即this引用的注册)。如此一来,便解决了上述问题。

class MoveHandler {
    components = {}

    constructor() {
        this.cancelAll = this.cancelAll.bind(this)
    }

    requestEnter(key) {
        Object.keys(this.components).forEach(k => {
            if (key !== k) {
                this.requestQuit(k)
            }
        })
        if (this.components[key]) {
            this.components[key].setState({ hover: true })
        }
    }

    requestQuit(key) {
        if (this.components[key]) {
            this.components[key].setState({ hover: false })
        }
    }

    regComponent(card, key) {
        this.components[key] = card
    }

    cancelAll() {
        Object.keys(this.components ?? {}).forEach(k => {
            this.requestQuit(k)
        })
    }
}

表格展示模块

该部分与电影展厅模块的列表实现非常相似,只是需要额外编写一个支持排序的表头。在列名旁边加上一个箭头即可,向下表示倒序,向上表示正序。Material-UI已经准备好了Table组件相关的Demo可供参考。我们只需要处理排序的事件即可。
支持排序的表头
对于排序的处理,同样是交由HomePage进行。HomePage的状态中保存该列表的排序关键字和排序顺序,触发排序的函数中及时更新排序状态。

class HomePage extends React.Component {
	constructor(props) {
		super(props)
		
		this.state = {
		    ...
		    orderBy: 'rate_douban',
		    order: 'desc'
		}
		...
	}
	
	handleOrderChange(prop, order) {
	    this.setState({
	        orderBy: prop,
	        order: order
	    }, () => {
	        this.doSearch(this.state.searchData, this.state.curPage)
	    })
	}
	...
}

总结

主页面是这个项目中非常重要的一个部分,负责了大量的数据操作和展示的部分。主页面对于响应式UI和操作逻辑的要求也十分严格,在编码的过程中也出现了不少问题,包括网络数据在加载过程中的占位动画、数据加载出错时的应对处理等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nullptrjzz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值