接使用uni-app从0.5到1搭建App记录-2一文,上一篇主要在第一小章节项目完善章节讲到路由配置、service封装,还有我当时碰到的疑难汇总。因为作者是边写项目边做记录的,所以本文还是会在已有章节上做相应补充。
上一文主要围绕项目前期配置、前期框架搭建作个技巧分享,接下来将从单页面、单组件来继续记录!
1.项目完善(接上文)
1.1 UI库引入和使用
使用好的UI库可以让项目开展事半功倍!我认为ColorUI和uview2.0就很不错,推荐一波!搭配起来可以很快的开展项目,首先Colorui可以很好的缩短你书写css的时间,缺点是需要先熟悉它书写语法。uview2如果你需要表单之类的组件的话还是极力推荐的!
1.ColorUI的引入
可以参考官网快速部署,这里就不赘述了。
ColorUi包含这些组件,可以自行按需引入,文件下载地址
2.uView2的引入
可以参考官网,这里推荐在uniapp插件市场引入会更好!
1.2 常用组件的使用
1.2.1图表组件和日期组件结合仿苹果天气界面
如下图:
上图由日历组件CustomCalendar 和图表组件ucharts组成,所以我们的代码结构很清晰。
代码结构如下:
html
<view class="container bg-white">
<CustomCalendar @current="getday"></CustomCalendar>
<view>
<view v-if="isShow" class="chartBox">
<qiun-data-charts
type="line"
:opts="opts"
:chartData="chartData"
:ontouch="true"
:onmovetip="true"
/>
</view>
<view v-else class="chartBox">
<u-empty
mode="data"
text="数据为空"
textSize="30"
iconSize="200"
width="500"
height="400"
marginTop="100"
>
</u-empty>
</view>
我们通过选择上面的日期来变换当天相关数据,其中日历组件我是引用这个作者的,就不重复造轮子了,在它的基础上我加了禁止选择当天之后的日期
html代码有些变化,如下:
前:
html
<template v-if="!showAll">
<swiper class="swiper" :current="swiperCurrent">
<swiper-item v-for="item in daysArr" :key="item.id">
<view class="day-wrap">
<view :class="['day-item',{'active':current === i.value}]" v-for="i in item.arr" :key="i.id"
@click="selectDate(i.value)">
<text class="day">{{i.value}}</text>
</view>
</view>
</swiper-item>
</swiper>
</template>
<template v-else>
<view class="day-wrap" v-for="item in daysArr" :key="item.id">
<view :class="['day-item',{'active':current === i.value}]" v-for="i in item.arr" :key="i.id"
@click="selectDate(i.value)">
<text class="day">{{i.value}}</text>
</view>
</view>
</template>
后:
html
<template v-if="!showAll">
<swiper class="swiper" :current="swiperCurrent">
<swiper-item v-for="item in daysArr" :key="item.id">
<view class="day-wrap">
<view :class="['day-item',{'active':current === i.value},{'disabled':compareCurrentDate(i.value)}]" v-for="i in item.arr" :key="i.id"
@click="!compareCurrentDate(i.value)&&selectDate(i.value)" :disabled="compareCurrentDate(i.value)">
<text class="day">{{ i.value }}</text>
</view>
</view>
</swiper-item>
</swiper>
</template>
<template v-else>
<view class="day-wrap" v-for="item in daysArr" :key="item.id">
<view :class="['day-item',{'active':current === i.value},{'disabled':compareCurrentDate(i.value)}]" v-for="i in item.arr" :key="i.id"
@click="!compareCurrentDate(i.value)&&selectDate(i.value)" :disabled="compareCurrentDate(i.value)">
<text class="day">{{ i.value }}</text>
</view>
</view>
</template>
并且增加几个变量(nowYear,nowMonth,nowDay)来更好比较日期:
js
data() {
return {
// false展示一行 true展示所有
showAll: false,
// 年
year: "",
// 月
month: "",
// 日
day: "",
swiperCurrent: 0,
// 星期几
weekday: 1,
// 每天
daysArr: [],
// 当前选中
current: 1,
nowYear: "",
nowMonth: "",
nowDay: "",
weekArr: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
}
},
所以我们的组件界面现在是这样:
其中我们增加了compareCurrentDate来控制日期的禁止还是激活状态,我们通过计算方法来比较日期来动态显示,代码如下:
js
computed:{
// 比较日期
compareCurrentDate() {
return (current)=>{
const nowDate = this.nowYear + '-' + this.nowMonth + '-' + this.nowDay;
const nextDate = this.year + '-' + this.month + '-' + current;
return ((new Date(nowDate.replace(/-/g, "/"))) < (new Date(nextDate.replace(/-/g, "/"))));
}
}
},
那我们的完整代码变成这样:
js
<template>
<view class="custom-calendar">
<view class="title flex align-center justify-center">
<u-icon name="arrow-left" color="#8F9BB3" size="32upx" class="left" @click="lastMonth"></u-icon>
{{ year }}年{{ month < 10 ? '0' + month : month }}月
<u-icon name="arrow-right" color="#8F9BB3" size="32upx" class="right" @click="nextMonth"></u-icon>
</view>
<view class="week">
<view class="week-item" v-for="(item,index) in weekArr" :key="index">
{{ item }}
</view>
</view>
<template v-if="!showAll">
<swiper class="swiper" :current="swiperCurrent">
<swiper-item v-for="item in daysArr" :key="item.id">
<view class="day-wrap">
<view :class="['day-item',{'active':current === i.value},{'disabled':compareCurrentDate(i.value)}]" v-for="i in item.arr" :key="i.id"
@click="!compareCurrentDate(i.value)&&selectDate(i.value)" :disabled="compareCurrentDate(i.value)">
<text class="day">{{ i.value }}</text>
</view>
</view>
</swiper-item>
</swiper>
</template>
<template v-else>
<view class="day-wrap" v-for="item in daysArr" :key="item.id">
<view :class="['day-item',{'active':current === i.value},{'disabled':compareCurrentDate(i.value)}]" v-for="i in item.arr" :key="i.id"
@click="!compareCurrentDate(i.value)&&selectDate(i.value)" :disabled="compareCurrentDate(i.value)">
<text class="day">{{ i.value }}</text>
</view>
</view>
</template>
<view class="flex align-center justify-center">
<u-icon :name="!showAll?'arrow-down':'arrow-up'" size="40" @click="switchShowMode" color="#8F9BB3"></u-icon>
</view>
</view>
</template>
<script>
export default {
data() {
return {
// false展示一行 true展示所有
showAll: false,
// 年
year: "",
// 月
month: "",
// 日
day: "",
swiperCurrent: 0,
// 星期几
weekday: 1,
// 每天
daysArr: [],
// 当前选中
current: 1,
nowYear: "",
nowMonth: "",
nowDay: "",
weekArr: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
}
},
created() {
this.init();
},
computed:{
// 比较日期
compareCurrentDate() {
return (current)=>{
const nowDate = this.nowYear + '-' + this.nowMonth + '-' + this.nowDay;
const nextDate = this.year + '-' + this.month + '-' + current;
return ((new Date(nowDate.replace(/-/g, "/"))) < (new Date(nextDate.replace(/-/g, "/"))));
}
}
},
methods: {
// 选择日期
selectDate(val) {
if (!val) return;
this.current = val;
//这里我组装成了{year:YYYY, month:MM, day:DD}的格式
//你们可以自己返回想要的数据格式
this.$emit('current', {
year: this.year,
month: this.month,
day: this.current,
})
},
// 切换显示模式
switchShowMode() {
this.showAll = !this.showAll;
this.getSwiperCurrent();
},
// 获取当前Swiper Current
getSwiperCurrent() {
this.swiperCurrent = this.daysArr.findIndex((item) => {
return item.arr.some(i => i.value === this.current);
});
},
// 初始化
init() {
let now = new Date();
this.year = now.getFullYear();
this.month = now.getMonth() + 1;
this.day = now.getDate();
this.nowYear = this.year;
this.nowMonth = this.month;
this.nowDay = this.day;
this.current = this.day;
//这里我组装成了{year:YYYY, month:MM, day:DD}的格式
//你们可以自己返回想要的数据格式
this.$emit('current', {
year: this.year,
month: this.month,
day: this.current,
})
this.changeData();
this.getSwiperCurrent();
},
// 获取当前星期几
getWeekday(year, month) {
let date = new Date(`${year}/${month}/01 00:00:00`);
return date.getDay() === 0 ? 7 : date.getDay();
},
//一个月有多少天
getMonthDay(year, month) {
let days = new Date(year, month, 0).getDate();
return days;
},
// 切换日期 该函数逻辑完善不建议改动 但可精简优化
changeData() {
this.day = this.getMonthDay(this.year, this.month);
this.weekday = this.getWeekday(this.year, this.month);
let daysArr = this.generateArray(1, this.day)
for (let i = 0; i < this.weekday - 1; i++) {
daysArr.unshift("")
}
let arr = [];
daysArr.map((item, index) => {
if (index !== 0 && index % 7 === 0) {
if (index === daysArr.length - 1) {
this.daysArr.push({
id: this.$u.guid(),
arr
});
arr = [];
arr.push({
id: this.$u.guid(),
value: item
});
this.daysArr.push({
id: this.$u.guid(),
arr
});
if (arr.length !== 7) {
const len = arr.length
for (let i = 0; i < 7 - len; i++) {
arr.push("")
}
}
} else {
this.daysArr.push({
id: this.$u.guid(),
arr
});
arr = [];
arr.push({
id: this.$u.guid(),
value: item
});
}
} else if (index === daysArr.length - 1) {
arr.push({
id: this.$u.guid(),
value: item
});
if (arr.length !== 7) {
const len = arr.length
for (let i = 0; i < 7 - len; i++) {
arr.push("")
}
}
this.daysArr.push({
id: this.$u.guid(),
arr
});
} else {
arr.push({
id: this.$u.guid(),
value: item
});
}
});
this.daysArr = this.daysArr
},
generateArray: function (start, end) {
return Array.from(new Array(end + 1).keys()).slice(start);
},
// 上一月
lastMonth() {
if (this.month == 1) {
this.month = 12
this.year = this.year - 1
} else {
this.month = this.month - 1
}
this.day = 1
this.daysArr = []
this.changeData();
this.$emit('current', {
year: this.year,
month: this.month,
day: this.current,
})
this.showAll = true
},
//下一月
nextMonth() {
// 判断是否需要切到下一月
let month=this.month;
let year=this.year;
if (this.month == 12) {
month = 1
year = this.year + 1
} else {
month = this.month + 1
}
const nowDate = this.nowYear + '-' + this.nowMonth + '-' + this.nowDay;
const nextDate = year + '-' + month + '-' + this.current;
if((new Date(nowDate.replace(/-/g, "/"))) < (new Date(nextDate.replace(/-/g, "/")))){
return;
}
if (this.month == 12) {
this.month = 1
this.year = this.year + 1
} else {
this.month = this.month + 1
}
this.day = 1
this.daysArr = []
this.changeData();
this.$emit('current', {
year: this.year,
month: this.month,
day: this.current,
})
this.showAll = true
},
}
}
</script>
<style scoped lang="scss">
.custom-calendar {
background-color: #FFFFFF;
position: sticky;
top: 0;
/* #ifdef H5 */
top: 44px;
/* #endif */
z-index: 99;
padding: 0 20upx;
.title {
padding: 20upx 32upx;
text-align: center;
font-weight: bold;
.left {
padding: 10upx;
margin-right: 30upx;
background-color: #E4F0FC;
border-radius: 8upx;
}
.right {
padding: 10upx;
margin-left: 30upx;
background-color: #E4F0FC;
border-radius: 8upx;
}
}
.week {
display: flex;
align-items: center;
text-align: center;
.week-item {
flex: 1;
}
}
.day-wrap {
display: flex;
.day-item {
display: flex;
justify-content: center;
flex: 1;
padding: 10upx 0;
flex-direction: column;
text-align: center;
border-radius: 8upx;
.day {
font-weight: bold;
}
.num {
font-size: 24upx;
color: #8F9BB3;
transform: scale(.8);
}
&.active {
background-color: #0676ED;
color: #FFFFFF;
.num {
color: #FFFFFF;
}
}
&.disabled {
color: #ccc;
.num {
color: #ccc;
}
}
}
}
.swiper {
height: 60upx !important;
}
.more {
text-align: center;
}
}
</style>
那我们通过日历组件获得了选择日期,我们现在回到折线图来了。图表组件我们使用的是ucharts组件
图表html代码如下:
html
<view v-if="isShow" class="chartBox">
<qiun-data-charts
type="line"
:opts="opts"
:chartData="chartData"
:ontouch="true"
:onmovetip="true"
/>
</view>
<view v-else class="chartBox">
<u-empty
mode="data"
text="数据为空"
textSize="30"
iconSize="200"
width="500"
height="400"
marginTop="100"
>
</u-empty>
</view>
获得日期加载图表数据
js
getday(item){
this.date=item.year+"-"+item.month+"-"+item.day;
this.chartData.categories.length=0;
this.chartData.series.length=0;
setTimeout(()=>{
this.serverData({code:this.code,tunnelCode:this.tunnelCode,date:this.date});
},100)
// 格式为:{year:YYYY, month:MM, day:DD}
},
代码结构很清晰,数据为空则显示空状态,不为空则为显示折线图。我们开启平滑显示提示onmovetip,如果我们需要自定义图表tooltip格式化需要这样做:
js
this.chartData.series[i].format="myTooltip" //给data中series 格式化数据
当时我搞这个搞了很久,需要请求数据后动态设置,否则不成功
ini
this.chartData.series[i].format="myTooltip"
如果你需要一个这样相同的组件
你可以这样配置opts
js
opts: {
color: ["#1890FF","#91CB74","#FAC858","#EE6666","#73C0DE","#3CA272","#FC8452","#9A60B4","#ea7ccc"],
padding: [30, 18, 10, 10],
dataLabel: false,
dataPointShape: false,
enableScroll: false,
legend: {},
xAxis: {
labelCount: 4,
},
yAxis: {
gridType: "dash",
showTitle: false,
data: [
{
axisLineColor: '#fff',
unit:'',
// title: "单位"
},
],
},
extra: {
line: {
type: "curve",
width: 2,
activeType: "hollow",
linearType: "custom",
onShadow: true,
animation: "horizontal"
}
}
},
1.2.2 滚动加载
这个组件我们是很常用的,可能大多数人都会选择mescroll-uni,但是我当时写的时候碰到一个问题,列表数据总是会超出或者高度太短,因为我的底部tab是自定义的,所以会出现这个问题。这个时候同事推荐试试z-paging,太好用了,极力推荐! 在使用z-paging的途中,我也发现有个高度不占满的问题,我们可以把fixed设置成false,完美解决!
官网常见问题中是这样说的: 演示效果如下:
上图使用官网demo做个演示。我们如何实现相应效果,我们最关键还是得看query函数
html
<z-paging ref="paging" v-model="warningArr" @query="queryTunnelEarlyWarn" :fixed="false">
<u-empty
slot="empty"
mode="data"
text="恭喜您无告警数据!"
textSize="30"
iconSize="200"
width="500"
height="400"
marginTop="0"
>
</u-empty>
<view class="cu-card dynamic" v-for="(item,index) in warningArr" :key="index" @tap="lookDetail(item)">
<view class="cu-item shadow">
<view class="cu-list menu-avatar">
...列表
</view>
<view class="text-content margin-top-xs">
<view class="text-bold">{{ item.warnContent }}</view>
</view>
</view>
</view>
</z-paging>
</view>
query会返回页码和页长度给我们,这样我们便可以比较对应的数据总长度进而判读是否需要触发上拉加载和下拉刷新。
js
queryTunnelEarlyWarn(pageNo, pageSize) {
let warnStatus = this.tabCur;
let warnTypeCode=this.tipId;
let page = pageNo || this.page; // 页码, 默认从1开始
let rows = pageSize || 10; // 页长, 默认每页10条
let params = {
page,
rows,
warnStatus,
warnTypeCode
}
indexApi.warningApi.queryTunnelEarlyWarn({...params}).then(res => {
if (res.statusCode == 200) {
const {data} = res;
if (data.tunnelEarlyWarnDataGridDTO.rows.length > 0) {
this.$refs.paging.complete(data.tunnelEarlyWarnDataGridDTO.rows);
} else {
this.$refs.paging.complete(false);
}
}
}
}).catch(res => {
this.$refs.paging.complete(false);
})
}
如果我们有tab页切换的情况下我们可以通过ref刷新数据
js
this.$refs.paging && this.$refs.paging.reload(true);
总得来说使用方便,挺不错,推荐给大家!
1.2.3 仿微信扩展加号
实现如下图功能:
首先我们在需要的地方引入下面组件:
html
<popover :actions="menuList" @select="onSelect" placement="bottom-end" :width="'300upx'" theme="dark">
<template slot="reference">
<u-icon name="plus" color="white" size="40"></u-icon>
</template>
</popover>
其中需要提一嘴的是menulist 属性,其他属性一看便知,menulist为需要展示的菜单列表,结构如下:
js
menuList: [
{
id: 0,
text: '新建test1',
iconClass: ["iconfont", "iconfont-xs", "icon-gongjuguanli"]
},
{
id: 1,
text: '新建test2',
iconClass: ["iconfont", "iconfont-xs", "icon-gaojing"]
},
],
text 为显示的文本,iconClass为图标的class属性,空则不展示。 那我们现在来看popover组件内部结构和样式代码,这个组件更需要关注的是样式代码。
html结构代码:
html
<template>
<div class="popoer">
<div class="reference" @click="showMenu($event)">
<slot name="reference"></slot>
</div>
<div class="menu-body" :class="[placement]" :style="{width:width + 'rpx'}">
<div class="menu" v-show="isShowMenu" :class="[theme,placement]">
<div @click="closeAllPopover">
<slot v-show="isShowMenu"></slot>
</div>
<div class="menu-item" v-for="(item,index) in actions" :key="index" :class="{'disabled':item.disabled}" @click="onSelect(item)">
<span :class="item.iconClass">{{item.text}}</span>
</div>
</div>
</div>
<div v-if="isShowMenu" @click="closeAllPopover" class="mask"></div>
</div>
</template>
首先,我们定义一个插槽,命名为reference,用来给父组件注入按钮图标。事件showMenu来控制菜单显隐状态。
html
<div class="reference" @click="showMenu($event)">
<slot name="reference"></slot>
</div>
之后我们在后面定义一个菜单容器(menu-body),遍历我们的菜单actions列表(menu-item)。
html
<div class="menu-body" :class="[placement]" :style="{width:width + 'rpx'}">
<div class="menu" v-show="isShowMenu" :class="[theme,placement]">
<div @click="closeAllPopover">
<slot v-show="isShowMenu"></slot>
</div>
<div class="menu-item" v-for="(item,index) in actions" :key="index" :class="{'disabled':item.disabled}" @click="onSelect(item)">
<span :class="item.iconClass">{{item.text}}</span>
</div>
</div>
</div>
其中我们从父组件定义了主题theme、显示位置placement,容器宽度width三个参数来改变样式表,一个参数actions数组来改变菜单展示列表数据。 js代码:
js
export default {
props: {
theme: {
type: String,
default: ''
},
actions: {
type: Array,
default: []
},
placement: {
type: String,
default: 'bottom'
},
width: {
type: String,
default: 'auto'
}
},
data() {
return {
isShowMenu: false
}
},
methods: {
closeAllPopover() {
this.$parent.$children.forEach(item => {
item.isShowMenu = false;
})
},
onSelect(item) {
if (!item.disabled) {
this.closeAllPopover();
this.$emit('select', item);
this.isShowMenu = false;
}
},
showMenu(e) {
if (!this.isShowMenu) this.closeAllPopover();
this.isShowMenu = !this.isShowMenu;
}
}
}
我们定义了三个函数方法,分别是closeAllPopover点击除了菜单列表的其他区域来控制所有的子类显隐状态,比如幕布mask状态,菜单框箭头等;showMneu点击按钮控制菜单容器的显示;onSelect选择菜单的触发事件! css代码:
css
.popoer {
position: relative;
display: inline-block;
}
.popoer > div {
z-index: 999;
}
.popoer .menu-body {
position: absolute;
z-index: 10;
}
.popoer .menu-body.bottom {
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
.popoer .menu-body.bottom-start {
bottom: 0;
left: 0;
}
.popoer .menu-body.bottom-end {
bottom: 0;
right: 0;
}
.popoer .menu-body.top,
.popoer .menu-body.top-start,
.popoer .menu-body.top-end {
top: 0;
}
.popoer .menu-body.top {
left: 50%;
transform: translateX(-50%);
}
.popoer .menu-body.top-end {
right: 0;
}
.popoer .menu {
position: absolute;
background: #ccc;
right: 16rpx;
border-radius: 5px;
color: #515a6e;
box-shadow: 0upx 0upx 30upx rgba(0, 0, 0, 0.2);
background: #fff;
padding: 10rpx 0;
}
.popoer .menu::after {
content: "";
position: absolute;
border-width: 0 20rpx 20rpx;
border-style: solid;
border-color: transparent transparent #fff;
}
.popoer .menu.dark {
background: #4A4A4A;
color: #fff;
}
.popoer .menu.dark::after {
border-color: transparent transparent #4A4A4A;
}
.popoer .menu.bottom {
top: 22rpx;
left: 0;
right: 0;
margin: auto;
}
.popoer .menu.bottom-start {
top: 22rpx;
left: 0;
}
.popoer .menu.bottom-end {
top: 22rpx;
right: -20rpx;
}
.popoer .menu.bottom::after {
top: -18rpx;
right: 10rpx;
right: 50%;
transform: translateX(50%);
}
.popoer .menu.bottom-start::after {
top: -18rpx;
right: 10rpx;
right: initial;
left: 10px;
}
.popoer .menu.bottom-end::after {
top: -18rpx;
right: 10rpx;
left: initial;
right: 10px;
}
.popoer .menu.top,
.popoer .menu.top-start,
.popoer .menu.top-end {
bottom: 22rpx;
}
.popoer .menu.top {
left: 0;
right: 0;
margin: auto;
}
.popoer .menu.top-start {
left: 0;
}
.popoer .menu.top-end {
right: 0;
}
.popoer .menu.top::after,
.popoer .menu.top-start::after,
.popoer .menu.top-end::after {
border-width: 20rpx 20rpx 0;
border-style: solid;
border-color: #fff transparent transparent;
bottom: -18rpx;
}
.popoer .menu.top::after {
right: 50%;
transform: translateX(50%);
}
.popoer .menu.top-start::after {
left: 0;
}
.popoer .menu.top-end::after {
right: 0;
}
.popoer .menu-item {
border-bottom: 1px solid #eee;
padding: 20rpx;
white-space: nowrap;
}
.popoer .menu-item:last-child {
border: none;
}
.popoer .menu-item.disabled {
color: #c8c9cc;
cursor: not-allowed;
}
.popoer .menu.dark .menu-item.disabled {
color: #9fa0a2;
}
.popoer > div.mask{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
我们通过css代码来控制样式的展示(如位置、主题、大小),这些属性都是从父组件传过来的,最外层div.popoer我们设置成相对定位,并且设置为inline-block对象, (注:其中希望元素具有宽度高度特性,又具有同行特性,这个时候我们可以使用inline-block) 我们把真正显示的容器div.menu-body设置成绝对定位,并且我们传了一个变量placement,用来区分不同的位置弹出菜单。我们可以画张图来更直观看出内部结构!
其中的几个关键点我们来深究一下。 (1).使用伪元素after和利用边框实现小三角气泡框 先来理解after伪元素的用法吧,before从字面上的意思可以理解为前面的意思,它一般和content属性一起使用,把内容插入到其他元素的后面了。
css
.popoer .menu::after {
content: "";
position: absolute;
border-width: 0 20rpx 20rpx;
border-style: solid;
border-color: transparent transparent #fff;
}
如上就是在menu后面加了一个元素。 简单理解完伪元素after,我们来看css border属性,容器的宽度为0时和不为0时的对比图
很显然要实现三角形,只需要伪元素after宽度设置为0,然后控制边框的宽度各个位置的透明度便可以实现三角形!
1.3总结
大多数组件参照了大佬的代码,学习到很多封装原理和技巧!在这里跪谢代码奉献者!
后续我们将围绕系统升级更新、推送展开,以此作个收尾篇。