如果你想开发一个应用(1-17)

数据模型

mvvm是数据驱动的,数据模型占了举足轻重的地位,所以,在做首页最终要的todo列表组件的时候,先暂时在客户端使用数据模型进行开发。而既然已经想到了这些数据需要通过交互从服务端获取,所以这个模型直接放入vuex中,数据模型的代码上一章已经分析过,所以这里直接复制过来:

indexTodos:[
    {
        month:0,              //月份
        default:1,            //正在显示的月份
        todos:[{
            createTime:new Date(),   //记录时间
            item:'',                 //标题
            content:'',                 //内容
            weather:0,                 //天气
            mood:0,                     //心情
            bookmark:0,                 //标记
            groupid:0,                 //所属组
        }]
    }
]

这里使用了两个数组,即月份组,每个月是一个项,而月份内呢,记事也是一个数组,每个记事项就是一个项。

引用vuex

具体到页面中怎么用呢?也就是说,页面如何使用vuex库中的值呢?

这里使用computed关键字,在vue中computed关键字的意思是实时计算,或者说监控值的变化,具体到代码中,首先需要我们需要的vuex组件,这里需要状态:

import { mapState } from 'vuex'

然后使用computed来引用组件中的值:

computed: mapState({
        groupId: state=>state.groupId,
        items:state=>state.indexTodos
}),

这样,Index.vue就可以像是使用data节点那样,通过this引用这两个值了。

组件化

这里需要思考一下,head有三个item,每个item对应的panel都需要在内容部分显示,那么,该如何控制具体到每个panel的显示或者加载呢?首先pass掉的肯定是做三个相同head和foot的页,这样的很明显不符合单页的需求,第二个被pass掉的应该是在这个页创建三个div,然后通过tabitem来控制div的隐藏显示了,那么,第三种方法应该是第二种的升级版,创建三个组件,通过tabitem来选择不同的组件加载。

这里我们先创建一个components,用来放我们所需要的组件。

首先,我们至少需要三个组件,也就是对应tabitem的三个,分别为:

  • DiaryPanel.vue 记录项(为防止与日记记录组相混淆,这里统一改为记录,标题为点滴,略微文青些)
  • Calendar.vue 日历项
  • Mine.vue 我的项

反正组件文件已经建立,那么我们先将他们一股脑的在Index页面中引用。

import DiaryPanelComponents from '../components/DiaryPanel.vue'
import CalendarComponents from '../components/Calendar.vue'
import MineComponents from '../components/Mine.vue'

因为完成之后,紧接着就是要对它们进行注册:

components:{
         DiaryPanelComponents,
         CalendarComponents,
         MineComponents
},

这时,就可以和html标签一样使用了。

<div id="contentPanel">
    <transition   name="custom-classes-transition"
    enter-active-class="animated bounceInLeft"
    leave-active-class="animated fadeOut"
    :duration="{ enter: 700, leave: 200 }"
    >
        <DiaryPanelComponents></DiaryPanelComponents>
    </transition>
</div>

但是,我们想想,这样的这个页面只能使用DiaryPanelComponents这一个组件,其他组件怎么办,如果将三个组件一股脑的全写在这个div节点中,控制显示隐藏,岂不是又回到了老路上?

好在vue提供了动态绑定组件的功能,我们在data数据模型中新增一个表示组件名称的属性currentView表示当前处于显示状态的组件:

data () {
    return {
        currentView:'DiaryPanelComponents',
           ...
    }
},

然后修改组件部分的模板html:

<div id="contentPanel">
    <transition   name="custom-classes-transition"
    enter-active-class="animated bounceInLeft"
    leave-active-class="animated fadeOut"
    :duration="{ enter: 700, leave: 200 }"
    >
        <component v-bind:is="currentView">
        </component>
    </transition>
</div>

这样,tab的item选择操作,就变成了最基本的的字符串赋值操作:

tabChange:function(event){
    ...
    var componentName = ''
    switch (event) {
        case 'tab1':
        componentName = 'DiaryPanelComponents'
        break
        case 'tab2':
        componentName = 'CalendarComponents'
        break
        case 'tab3':
        componentName = 'MineComponents'
        break
    }
    this.currentView = componentName
}

组件嵌套

首页现在基本只起一个调度作用,具体的内容交给了组件来完成,下面打开DiaryPanel.vue,对这个组件进行开发。

分析一下这个组件,这个组同样分为两部分,头部一个作为标题的月份,下边循环显示一个此月所有的记录项。

但无论开发哪个部分,我们都需要先从vuex中将数据取出来:

import { mapState } from 'vuex'

export default {
    computed: mapState({
        indexTodos: state=>state.indexTodos,
    })
}

剩下的就很简单了,先把显示的部分代码写完,这里用了museui的组件sub-header:

<div  v-for="item in indexTodos" >
    <mu-sub-header class="day_title">{{ item.month }}</mu-sub-header>
    <DiaryListComponents></DiaryListComponents>
</div>

然后根据实际情况修改css样式:

.day_title{
    font-size: 50px; 
    line-height: 55px;
    font-family: 'Microsoft YaHei',arial,tahoma,\5b8b\4f53,sans-serif;
    font-weight: 500;
    color: #5e35b1;
    text-align: center;
    padding: 0px;
}

接下来就是循环显示记录列表了,想一下原型中,这个todo放到了一个面板块内,而面板块还是比较复杂的,并且每个月都要使用,所以,我们也把他提炼到一个组件中,嗯,就叫DiaryList,从这里也可以看出,vue的组件是支持嵌套的。接下来在components文件夹内创建DiaryList文件。

同时,由于用户会滑动页面,也就是说,这个组件内所需要的值,即todo数组,是与父组件联系紧密的,所以需要通过参数的方式,将父组件循环得来的值传送到子组件中,vue中传值也非常方便,在标签引用的地方绑定一下就行了:

<DiaryListComponents v-bind:todos="item.todos"></DiaryListComponents>

然后子组件获取更加简单:

DiaryList.vue:

export default {
    props:["todos"]
}

这样就可以直接使用todos变量。

而面板使用museui的pager控件就可以了,还自带阴影效果,并且是在循环体内,使用todos变量的pager代码如下;

<mu-paper class="diaryitem" :zDepth="2" v-for="(item) in todos" >
</mu-paper>

过滤器

接下来就是对这个组件的开发了。观察一下这个块的内容:

首先四周都有个边框,所以用一个父级的mu-content-block包裹一下,然后看内容,是一个左中右的结构,刚好museui有个布局表格,就直接使用了,布局表格的权重,暂时就20-6-20吧,最终布局部分代码如下:

<mu-paper class="diaryitem" :zDepth="2" v-for="(item) in todos" >
    <mu-content-block>
        <mu-row gutter>
           <mu-col width="20">
           </mu-col>
            <mu-col width="60">
            </mu-col>
            <mu-col width="20" style="text-align:right">
            </mu-col>
        </mu-row>
    </mu-content-block>
</mu-paper>

剩下的内容,如果先不考虑样式的话,最简单应该就是标题和内容了,直接输入就好:

<mu-col width="60">
    <div class="item_time">12:34</div>
    <div class="item_title">{{ item.item }}</div>
    <div class="item_content">{{item.content}}</div>
</mu-col>

css的一会在完善,接下来就是时间了,其实时间虽然现实了这么多,但是具体到了数据项上,实际上只有一个,就是createtime,接下来要做的就是如何提取显示的问题了,这时候vue提供的过滤器就登场了,下面以日期为例介绍一下过滤器的用法.

过滤器其实就是一个通过filter标记的普通js的方法,然后我们先让他返回一个固定数字的写法:

filters: {
    getDay(time) {
        return 3;
    }
}

调用方法为:

{{ item.createTime | getDay }}

其中item.createTime对应模型中的值和过滤器方法的参数,getDay很明显,就是我们过滤器的方法了。有了这些,完成过滤器就很简单了:

getDay(time) {
    var date = new Date(time);
    return  date.getDate();    
}

这时页面上就回只显示日期值的。

接下来我们想到,不只是日期需要,其他的需要的还有很多,比如月份,时间等,而且在可预见的地方,比如新增记录页,tag的列表页等,所以这个功能有必要提取复用一下,关于日期操作的js方法网上有很多,就不在叙述,这个作为工具类,通服务端代码一样,创建一个util文件夹,然后就叫Date.js文件,最终的代码如下:

export function formatDate(time, fmt) {
    var date = new Date(time);

    if (/(y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    let o = {
        'M+': date.getMonth() + 1,
        'd+': date.getDate(),
        'h+': date.getHours(),
        'm+': date.getMinutes(),
        's+': date.getSeconds(),
        'w+':getWeek(date)
    };
    for (let k in o) {
        if (new RegExp(`(${k})`).test(fmt)) {
            let str = o[k] + '';
            fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
        }
    }
    return fmt;
};

function padLeftZero(str) {
    return ('00' + str).substr(str.length);
}
function getDay(time){
    return formatDate(time,"dd");
}

function getWeek(time){
    var weekName=['星期日','星期一','星期二','星期三','星期四','星期五','星期六']
    return weekName[time.getDay()];
}
function getTime(time){
    return formatDate("hh-mm-dd");
}

然后DirayList组件内引入,并完成剩余的几个过滤器方法:

<script>
    import { formatDate } from '../utils/date.js';
    export default {
        props:["todos"],
        filters: {
            getDay(time) {
                return  formatDate(time,"dd");
            },
            getWeek(time) {
                return  formatDate(time,"w");
            },
            getTime(time) {
                return  formatDate(time,"hh:mm");
            }
        }
    }
</script>

最后,是右边的三个icon图标,这三个db中存储的是int型,而页面显示需要一个String的name,所以,通date中的week一样,分别将int作为数组的下标,这里给出三个最简的形式:

mood.js

export function mood(num) {
    var moodValue=["mood",""]
    if(num==null)
        num=0;
    return moodValue[num];
}

weather.js

export function weather(num) {
    var weatherValue=["wb_sunny",""]
    if(num==null)
        num=0;
    return weatherValue[num];
}

bookmark.js

export function bookmark(num) {
    var bookmarkValue=["bookmark_border","bookmark"]
    if(num==null)
        num=0;
    return bookmarkValue[num];
}

最终标签内的代码如下:

<mu-paper class="diaryitem" :zDepth="2" v-for="(item) in todos" >
    <mu-content-block>
        <mu-row gutter>
           <mu-col width="20">
               <div class="item_day">{{ item.createTime | getDay }}</div>
               <div class="item_week">{{ item.createTime | getWeek }}</div>
           </mu-col>
            <mu-col width="60">
                <div class="item_time">{{ item.createTime | getTime }}</div>
                <div class="item_title">{{ item.item }}</div>
                <div class="item_content">{{item.content}}</div>
            </mu-col>
            <mu-col width="25" style="text-align:right">
                <mu-icon :value=" item.mood | getMoodValue  " :size="16"/>
                <mu-icon :value=" item.weather | getWeatherValue  " :size="16"/>
                <mu-icon :value=" item.bookmark | getBookmarkValue  " :size="16"/>
            </mu-col>
        </mu-row>
    </mu-content-block>
</mu-paper>

js代码如下:

import { formatDate } from '../utils/date.js';
import { mood } from '../utils/mood.js';
import { weather } from '../utils/weather.js';
import { bookmark } from '../utils/bookmark.js';
export default {
    props:["todos"],
    filters: {
        getDay(time) {

             var date = new Date(time);
             console.log(date)
            return  date.getDate();
            //return  formatDate(time,"dd");
        },
        getWeek(time) {
            return  formatDate(time,"w");
        },
        getTime(time) {
            return  formatDate(time,"hh:mm");
        },
        getMoodValue(num){
            return mood(num);
        },
        getWeatherValue(num){
            return weather(num);
        },
        getBookmarkValue(num){
            return bookmark(num);
        }
    }
}

css代码略,请自行查看源码

这时候跑起来,效果如图:

从当前的界面莱克,基本上符合原型的要求。

服务端数据

剩下的内容就简单了,只要解决数据来源的问题就清楚了,我们在贴一下要求的数据格式:

indexTodos:[
    {
        month:0,              //月份
        default:1,            //正在显示的月份
        todos:[{
            createTime:new Date(),   //记录时间
            item:'',                 //标题
            content:'',                 //内容
            weather:0,                 //天气
            mood:0,                     //心情
            bookmark:0,                 //标记
            groupid:0,                 //所属组
        }]
    }
]

同时,还需要一个itemnumber,所以回到服务端的java代码,一步一步的完成这个api功能。

首先,为了和原有的代码区分,新创建一个ApiTodoController控制器,里边新增一个action,apiIndex,这个action除了token外,还需要一个月份作为参数,这个也很容易理解。然后我们需要根据月份查询todo列表,在之前还提到过,由于分多个组,需要设置一个默认组,首页显示默认组的todo,所以,服务层的方法名也就出来了,getTodosByDefaultGroup,参数有两个,用户Id(由token获取)和月份(参数传递)。

其实根据服务层的方法名,他的伪代码就都出来了,根据的《代码大全里》的方法,用说明注释写出来:

  • 根据用户id查询此用户的默认记录组
  • 查询此组此月的所有记录

注释简单,代码当然也就简单了:

public List<Todo> getTodoByDefaultGroup(int userId,int month) {
    TodoGroup todoGroup=todoGroupRepository.findByIsDefaultAndUserId(1,userId);
    DateBetween between=getDate(month);
    List<Todo>  todos= todoRepository.getByGroupIdAndCreateTimeBetween(todoGroup.getId(),between.getFirstDate(),between.getEndDate());
    return todos;
}

repository层内只有方法名没有方法体,所以查看调用就能看到全部内容,不在叙述。

DateBetween类从名字就可以看出来,表示一个日期区间,具体到这个代码中,表示的是这个月的1号到这个月的最后一天,即31号(1月份),他的代码如下:

class DateBetween{
    private Date firstDate;
    private Date endDate;
    //get set
}

getDate就是获取参数月的起始和结束日期,代码如下:

private DateBetween getDate( int month ){
    DateBetween between=new DateBetween();
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
    Calendar firstCalender =  Calendar.getInstance();
    // 获取前月的第一天
    firstCalender = Calendar.getInstance();
    firstCalender.add(Calendar.MONTH, 0);
    firstCalender.set(Calendar.DAY_OF_MONTH, 1);
    between.setFirstDate(firstCalender.getTime());
    // 获取前月的最后一天
    Calendar endCalender =   Calendar.getInstance();
    endCalender.add(Calendar.MONTH, 1);
    endCalender.set(Calendar.DAY_OF_MONTH, 0);
    between.setEndDate(endCalender.getTime());
    return  between;
}

貌似有点啰嗦,先这样回头再慢慢重构吧,这个方法只有这个类用,是private的。

##组装json##

接下来回到Controller,这里没什么好说的,jackson库能直接将Map和类转成Json对象,所以直接把前端需要的数据通过map组装起来就好了,直接贴代码:

@RequestMapping(value = "/api/index",method = RequestMethod.POST)
public Object apiIndex(HttpServletRequest request,@RequestBody Map map){
    //获取首页数据
    String userId=request.getAttribute("tokenId").toString();
    Integer month=Integer.parseInt( map.get("month").toString());
    List<Map<String,Object>> items=new ArrayList<Map<String,Object>>();
    for (int i=0;i<1;i++) {
        List<Todo> todos = todoService.getTodoByDefaultGroup(Integer.parseInt(userId),month);
        //数据结构扩充接口
        Map<String, Object> data = new HashMap<String, Object>();
        data.put("month",month);
        data.put("todos",todos);
        data.put("default",1);
        items.add(data);
    }
    Map<String,Object> resutl=new HashMap<String,Object>();
    resutl.put("items",items);
    resutl.put("itemnumber",items.size());
    return result(resutl);
}

注意这个for循环,现在只走一次,这是为了之后优化效率,一次性返回多个月而预留的代码,现在就直接当它是一个顺序结构即可.

到目前为止的代码:

前端vue
后端java

谢谢观看