cube-ui 是一个基于 Vue.js 实现的精致移动端组件库。 它响应迅速、动画流畅,追求极致的交互体验。 总体分为基础、弹层、滚动三大组件模块,可以说基本涵盖了我们移动端所有的组件需求。
那么这里呢,我们想利用它来做一个浏览赛事的 APP ,大体可以分为顶部导航、比赛列表和弹窗三大部分。 首先,用户要可以流畅的滚动列表来查看比赛,并且可以通过下拉列表来刷新赛事情况,以及通过上拉列表的方式来加载更多赛事;其次,用户可以左右滑动 Tab 页,或者点击顶部 Tab 来切换不同状态的比赛;接着,用户还可以对比赛的类型进行选择,你想看篮球、足球还是 Dota ?然后,用户可以对感兴趣的比赛进行订阅,我们利用弹窗来给用户反馈一些提示信息。先给你看一下,最终完成的效果是这样哒~ 。好了,现在就让我们从初始化项目开始吧……
你可以扫码体验哦~
cube-ui 为我们提供了脚手架,可以方便的迅速初始化一个 cube-ui 项目,里面有基本配置和基础代码,然后你就可以开始的使用它了。
# 在当前目录下初始化一个 cube-ui 项目
$ vue init cube-ui/cube-template
# 在当前目录下创建一个叫application的文件夹,在里面初始化项目
$ vue init cube-ui/template some-sub-application
上面两种方法都可以,建议你已经创建了一个空文件夹,然后进入空文件夹后再使用第一种命令,或直接用第二种。 接着,你需要回答一些问题:
$ vue init cube-ui/cube-template
Generate project in current directory? Yes
A newer version of vue-cli is available.
latest: 2.9.3
installed: 2.8.2
# 为你的项目起个名字
? Project name cube-application-guide
# 起你的项目写一段描述
? Project description A guide for cube application
# 作者
? Author AmyFoxFN <********.com>
# 选择vue种类,第一种是运行时编译,第二种是只运行,建议选后者将编译交给webpack并且体积要小大约30%
? Vue build (Use arrow keys)
❯ Runtime + Compiler: recommended for most users
Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONL
Y allowed in .vue files - render functions are required elsewhere
# 是否后编译
? Use post-compile? Yes
# 按需引入组件还是全部引入
? Import type Partly
# 是否自定义主题,使用后编译的情况下可用
? Custom theme? Yes
# rem 布局,使用后编译的情况下可用
? Use rem layout? No
# 是否安装vue-router
? Install vue-router? No
# 是否用ESLint来规范你的代码
? Use ESLint to lint your code? Yes
# 选择一个ESLint预设标准
? Pick an ESLint preset Standard
# 是否建立单元测试
? Set up unit tests No
# 是否建立端对端测试
? Setup e2e tests with Nightwatch? No
你可以在这里对以上的问题有更深入的了解 注意,cube-cli 脚手架默认帮我们在 main.js 中注册了很多 cube-ui 的组件,你可以将你不需要的删除。 现在,我们就有了最基本的项目结构,可参考 stage-1 分支。
现在,你可以通过以下命令,来启动一下项目了。成功后,你会看到一个有 Vue 标志的页面。
# 安装依赖
$ npm install
# 在本地的8080端口起一个有热刷新功能的服务
$ npm run dev
现在,根据功能我们将项目划分出赛事列表组件 MatchList ,以及弹窗组件 SubscribeDialog 。首先,我们在 App.vue 中写一下应用的基本结构。
我们先构建一下顶部导航部分,此部分可参考 stage-2 。
<div id="app">
<div class="header">
<div class="title" @click="showPicker">
<span>全部赛事</span>
<i class="cubeic-select" :class="{flip: toFlip}" ref="select"></i>
</div>
<div class="navigator">
<ul class="nav-list">
<li v-for="(item, index) in tabList" :key="index"
@click="switchTab(index)" :class="{active: currentPage === index}">
{{ item }}
</li>
</ul>
<div class="triangle-up" :class="{left: currentPage === 0, right: currentPage === 2}"></div>
</div>
</div>
<div class="content">
<cube-slide>
</cube-slide>
</div>
</div>
switchTab
和 currentPage
是为了 Tab 下的小角标随着 Tab 切换而移动的,详细可看具体代码 。
但是,用户在手机上只能通过点击顶部的 Tab 来切换不同状态的比赛,是不是太不方便了。而 cube-ui 的 Slide 组件,提供了很流畅的轮播及 Swipe 功能,我们就可以通过它,来丰富我们的移动端体验。
<div class="content">
<cube-slide
:data="tabList"
:initialIndex="currentPage"
:loop="false"
:autoPlay="false"
:threshold="0.1"
@change="slideChange">
<cube-slide-item v-for="(item, index) in tabList" :key="index">
<div class="match-list-wrapper">
<match-list :type="type" :status="index"></match-list>
</div>
</cube-slide-item>
<div slot="dots"></div>
</cube-slide>
</div>
由于我们的列表内容比较复杂,又是一个组件,所以我们使用了自定义内容。Slide 组件为我们提供了默认插槽和 cube-slide-item
来自定义每个轮播页的结构,cube-slide
也为我们预留了很多的参数可供设置,比如这里,我们设置了它的初始索引值为1、不循环播放、不自动播放、切换页面的滑动阈值等,详细参考官方文档。现在,用户就可以很方便的通过左右滑动来切换 Tab 页了,你可以去 stage-2 查看这一步的效果,接下来,我们继续来实现列表部分。
此部分参考 stage-3 分支。 我们先来实现一下比赛列表的静态样式。
<ul class="match-inner">
<li v-for="(item, index) in matchList" :key="index" class="match-item">
<div class="left-team">
<img :src="item.hostLogoUrl" alt="" class="logo">
<p class="name">{{item.hostTeamName}}</p>
</div>
<div class="center">
<p v-if="item.live" class="guest" :class="{end : item.isEnd}">{{item.live}}</p>
<p v-if="item.order" class="order" @click="subscribe">{{item.order}}</p>
<p class="score" :class="{last: item.isEnd}">
{{item.hostScore}} - {{item.guestScore}}
</p>
<p v-if="!item.isEnd" class="time">{{item.endTime}}</p>
</div>
<div class="right-team">
<img :src="item.guestLogoUrl" alt="" class="logo">
<p class="name">{{item.guestTeamName}}</p>
</div>
</li>
</ul>
MatchList 是我们模拟的列表数据,每个 li
都是一条赛事,整个 ul
是赛事列表。具体的数据渲染以及元素样式,就不一一解释啦,你可以去仓库代码里详细查看。接下来,就要为我们的列表组件添加一些丰富的功能了。
在比赛列表这个组件中,我们的主要功能就是滚动,所以我们可以使用 cube-ui 的 Scroll 组件来完成,从而为用户提供了更流畅的浏览体验。
由上一步可以看到,我们的列表内容还是比较复杂的,所以可以采用默认插槽来自定义内容,也就是说,直接把我们上面的列表内容,包在 <cube-scroll>
标签里就行了。当然,如果你的数据十分简单,可以直接将数据传入 <cube-scroll>
的 data
字段 。
<cube-scroll
ref="scroll"
:data="matchList"
:options="options"
@pulling-down="onPullingDown"
@pulling-up="onPullingUp">
<ul class="match-inner">
<li v-for="(item, index) in matchList" :key="index" class="match-item">
<!-- 内容省略 -->
</li>
</ul>
</cube-scroll>
虽然我们是自定义内容,但同样建议为 Scroll 的 data
属性传入数据,这样 Scroll 组件可以帮我们自动进行数据监听和重新渲染。
<cube-scroll>
还为我们提供了很多的参数,你可以通过他们来设置滚动条、滚动方向、开启下拉刷新功能等,详细请参考官方文档。
比如这里,我们通过 options
的 scrollbar
来对滚动条进行设置,你可以选择显示或不显示,这里,我们选择淡入淡出的滚动条。
options: {
scrollbar: {
fade: true
}
},
这里还需提醒大家注意一下,在使用 Scroll 组件的时候,为保证正常滚动,内容元素 .cube-scroll-content
在滚动方向上的长度必须大于容器元素 .cube-scroll-wrapper
,具体滚动原理请去往官方文档里查看。
比赛的变化瞬息万变,用户就需要时不时的刷新列表来获取最新的数据。
Scroll 组件默认无下拉刷新功能,我们需要通过配置项 pullDownRefresh
开启。
pullDownRefresh: {
threshold: 90,
stop: 50,
txt: '刷新成功'
}
我们为这次下拉动画设置了下拉距离阈值、回弹位置以及刷新成功的文案。 下拉距离阈值你可以理解为,是你下拉到某一个可以开始刷新数据的位置; 回弹位置则是你放手之后列表的恢复位置。这些你都可以去 文档 里详细的查看或尝试。
刚才,我们对下拉动作进行了设置。现在,就该写触发了这次动作后你想做的事情,在这里也就是刷新数据了。当用户下拉列表超过阈值时,会触发 Scroll 组件的 pulling-down
事件,这个事件我们已经在之前的模板里面绑定了处理函数,而在这个事件的处理函数中,我们可以刷新数据了。
methods: {
onPullingDown () {
this.loadMatch('down')
},
loadMatch (type) {
setTimeout(() => { //这里用setTimeout模拟数据请求,真实情况下你需要向接口请求数据
if (Math.random() > 0.5) {
let match = []
for (let index = 5; index > 0; index--) {
match.push(this.matchList[index])
}
if (type === DOWN) {
this.matchList.unshift(...match)
} else if (type === UP) {
this.matchList = this.matchList.concat(match)
}
} else {
this.$refs.scroll.forceUpdate()
if (type === UP) { //上拉加载时,无更多数据的提示文案显示之后,让列表回到原位
setTimeout(() => {
this.$refs.scroll.scroll.scrollBy(0, 64, 800)
}, 1000)
}
}
}, 1000)
}
}
这里需要注意的是,如果请求结果是没有新数据,则必须使用 this.$refs.scroll.forceUpdate()
结束此次下拉刷新。这样,Scroll 组件才会开始监听下一次下拉刷新操作。
另外,如果你不喜欢内置的下拉刷新动画,还可以用作用域插槽做自定义动画,详情见官方文档。
我们不可能一次放入所有的列表数据,用户可以通过上来加载来查看更多的比赛。
与下拉刷新相同,Scroll 组件默认无上拉加载功能,可通过配置项 pullUpLoad
开启。
pullUpLoad: {
threshold: 100,
txt: {
more: '加载更多',
noMore: '没有更多的比赛啦'
}
}
我们为这次上拉动作设置了上拉距离阈值,以及提示加载和没有更多数据的文案。
那么同样,当用户上拉列表超过阈值时,会触发 Scroll 组件的 pulling-up
事件,而在这个事件的处理函数中,我们可以刷新数据了。
methods: {
onPullingUp () {
this.loadMatch('up')
},
loadMatch (type) {
setTimeout(() => { //这里用setTimeout模拟数据请求,真实情况下你需要向接口请求数据
if (Math.random() > 0.5) {
let match = []
for (let index = 5; index > 0; index--) {
match.push(this.matchList[index])
}
if (type === DOWN) {
this.matchList.unshift(...match)
} else if (type === UP) {
this.matchList = this.matchList.concat(match)
}
} else {
this.$refs.scroll.forceUpdate()
if (type === UP) { //上拉加载时,无更多数据的提示文案显示之后,让列表回到原位
setTimeout(() => {
this.$refs.scroll.scroll.scrollBy(0, 64, 800)
}, 1000)
}
}
}, 1000)
}
}
与下拉刷新相同,请注意没有数据的情况,以及同样可以用作用域插槽做自定义动画,详情见官方文档。
我们需要在用户点击订阅后,为用户弹一个开启订阅的弹窗,因为弹窗样子比较花,哈哈,所以我们可以基于 cube-ui 的 Popup 封装一个,这也是 cube-ui 为我们提供的一个很好的功能。那这里,就不得不先说一下,cube-ui 的另一个是非重要的 API 了。
createAPI
的作用是把我们之前声明式的组件使用方式改变成 API 式的调用。我们知道 Vue 推荐的就是声明式的组件使用方式,比如在使用一个组件 xxx,我们简单在使用的地方声明它就好了。
<tempalte>
<xxx/>
</tempalte>
对于一般组件,这样使用并没有问题,但对于全屏类的弹窗组件,如果在一个层级嵌套很深的子组件中使用,仍然通过声明式的方式,很可能它的样式会受到父元素某些 CSS 的影响导致渲染不符合预期。这类组件最好的使用方式就是挂载到 body 下,但是我们如果是声明式地把这些组件挂载到最外层,对它们的控制也非常不灵活。其实最理想的方式是动态把这类组件挂载到 body 下,createAPI
就是干这个事情的。
我们现在就用它来做我们的订阅弹窗。
cube-ui 提供了所有弹窗类组件的基类组件 Popup,如果是新增一个弹窗类组件,推荐基于 Popup 做二次开发。我们这里就是基于 Proup 封装了名为 subscribe-dialog
的弹窗组件。
首先我们在 main.js 中通过 createAPI
创建一个 this.$createSubscribeDialog
API,把 SubscribeDialog
变成一个 API 式调用的组件:
import SubscribeDialog from './components/subscribe-dialog/subscribe-dialog'
createAPI(Vue, SubscribeDialog, [], true)
接着我们就可以在 MatchList 组件内部通过 this.$createSubscribeDialog()
的方式调用它
this.subscribeDialog = this.$createSubscribeDialog()
当你想让这个弹窗显示,就执行 .show()
方法。当执行 .show()
的时候,cube-ui 内部会把 SubscribeDialog
组件动态挂载到 body 下。
上图就是我们完成好的弹窗,在右边的审查元素中,我用两个小红点标出了 App.vue
挂载的元素和我们的弹窗组件,你可以看到他们是并列的关系,弹窗组件确实是直接被挂载到了 body 下。
你还可以为你的弹出层做 mask 之类更详细的配置,这一部分你可以参考项目中的 stage-4 分支。
cube-ui 的弹出层组件部分,还包含了 Picker、TimePicker、Dialog 等其他弹出层,详情见官方文档。
我们希望用户在点击顶部的全部赛事后,由屏幕下方上滑出一个弹窗,供用户选择比赛类型,像这样:
这时,就可以使用 cube-ui 的 Picker 组件。用户可以通过屏幕下边上滑出的 Picker,来选择想看的比赛类型,点击确认后,如果确认值有变,则更新比赛列表。并且点击取消、确定以及 Picker 之外的地方时,都让 Picker 消失。同样,Picker 也是用 createAPI
创建的,此部分见 stage-5 分支。
mounted () {
this.picker = this.$createPicker({
title: '赛事',
data: [this.pickerList],
onSelect: () => { //点击确定时
this.toFlip = !this.toFlip
},
onCancel: () => { //点击取消时
this.toFlip = !this.toFlip
},
onValueChange: (selectedVal) => { //确认的值有改变时
this.type = selectedVal[0]
}
})
}
我们创建了 Picker,并为它的 data
字段传入表示比赛类型的数组,然后你可以在它的各种事件处理函数中做你想做的事情,还可以为这个 Picker 做一些其他更详细的配置,详情见官方文档 。
到这里,我们的应用可以说是基本完成了。但其实,也还只是用了 cube-ui 几个比较主要的组件。cube-ui 还有很多很棒的组件,如表单、开关、loading 等,如果你有兴趣的话,也可以继续丰富我们这个赛事应用。当然,你也已经完全可以借助 cube-ui ,开始一个自己的项目啦~