QA seven's blog


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

小QA学习前端系列之vue实战4

发表于 2017-11-16

接上节 我们继续学习

回到router
我们再来看看add路径

1
2
3
4
5
6
{
path: '/add',
name: 'add',
component: resolve => require(['./views/new.vue'], resolve),
meta: { requiresAuth: true }
}

最后调用的是一个 new.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<template>
<div>
<nv-head page-type="主题"
:show-menu="false"
:fix-head="true"></nv-head>
<div class="add-container">
<div class="line">选择分类:
<select class="add-tab" v-model="topic.tab">
<option value="share">分享</option>
<option value="ask">问答</option>
<option value="job">招聘</option>
</select>
<a class="add-btn" @click="addTopic">发布</a>
</div>
<div class="line">
<input class="add-title" v-model="topic.title"
type="text" :class="{'err':err=='title'}"
placeholder="标题,字数10字以上" max-length="100"/>
</div>
<textarea v-model="topic.content" rows="35" class="add-content"
:class="{'err':err=='content'}"
placeholder='回复支持Markdown语法,请注意标记代码'>
</textarea>
</div>
</div>
</template>
<script>
import $ from 'webpack-zepto';
import nvHead from '../components/header.vue';
import {
mapGetters
} from 'vuex';
export default {
data() {
return {
topic: {
tab: 'share',
title: '',
content: ''
},
err: '',
authorTxt: '<br/><br/><a class="from" href="https://github.com/shinygang/Vue-cnodejs">I‘m webapp-cnodejs-vue</a>'
};
},
computed: {
...mapGetters({
userInfo: 'getUserInfo'
})
},
methods: {
addTopic() {
console.log(this.userInfo);
let title = $.trim(this.topic.title);
let contents = $.trim(this.topic.content);
if (!title || title.length < 10) {
this.err = 'title';
return false;
}
if (!contents) {
this.err = 'content';
return false;
}
let postData = {
...this.topic,
content: this.topic.content + this.authorTxt,
accesstoken: this.userInfo.token
};
$.ajax({
type: 'POST',
url: 'https://cnodejs.org/api/v1/topics',
data: postData,
dataType: 'json',
success: (res) => {
if (res.success) {
this.$router.push({
name: 'list'
});
}
},
error: (res) => {
let error = JSON.parse(res.responseText);
this.$alert(error.error_msg);
return false;
}
});
}
},
components: {
nvHead
}
};
</script>
<style lang="scss">
.add-container {
margin-top: 50px;
background-color: #fff;
.line {
padding: 10px 15px;
border-bottom: solid 1px #d4d4d4;
.add-btn {
color: #fff;
background-color: #80bd01;
padding: 5px 15px;
border-radius: 5px;
}
.add-tab {
display: inline-block;
width: calc(100% - 140px);
min-width: 50%;
font-size: 16px;
background: transparent;
:after {
content: 'xe60e';
}
;
}
.add-title {
font-size: 16px;
border: none;
width: 100%;
background: transparent;
height: 25px;
}
.err {
border: solid 1px red;
}
}
.add-content {
margin: 15px 2%;
width: 96%;
border-color: #d4d4d4;
color: #000;
}
.err {
border: solid 1px red;
}
}
</style>

nv-head设置标题

1
2
3
<nv-head page-type="主题"
:show-menu="false"
:fix-head="true"></nv-head>

内容部分
一个option标签来选择具体的分类,一个输入栏 一个textarea

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="add-container">
<div class="line">选择分类:
<select class="add-tab" v-model="topic.tab">
<option value="share">分享</option>
<option value="ask">问答</option>
<option value="job">招聘</option>
</select>
<a class="add-btn" @click="addTopic">发布</a>
</div>
<div class="line">
<input class="add-title" v-model="topic.title"
type="text" :class="{'err':err=='title'}"
placeholder="标题,字数10字以上" max-length="100"/>
</div>
<textarea v-model="topic.content" rows="35" class="add-content"
:class="{'err':err=='content'}"
placeholder='回复支持Markdown语法,请注意标记代码'>
</textarea>
</div>
</div>

小QA学习前端系列之vue实战3

发表于 2017-11-13

接上节 我们继续学习

接下来我们要看看router

1
2
3
4
5
6
7
8
9
{
path: '/topic/:id',
name: 'topic',
component(resolve) {
require.ensure(['./views/topic.vue'], () => {
resolve(require('./views/topic.vue'));
});
}
}

topic router指向的是topic vue components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
{
<template>
<div>
<nv-head page-type="主题"
:show-menu="showMenu"
:need-add="true"
:fix-head="true">
</nv-head>
<div id="page"
:class="{'show-menu':showMenu}"
v-if="topic.title">
<h2 class="topic-title" v-text="topic.title"></h2>
<section class="author-info">
<img class="avatar" :src="topic.author.avatar_url" />
<div class="col">
<span>{{topic.author.loginname}}</span>
<time>
发布于:{{topic.create_at | getLastTimeStr(true)}}
</time>
</div>
<div class="right">
<span class="tag"
:class="getTabInfo(topic.tab, topic.good, topic.top, true)"
v-text="getTabInfo(topic.tab, topic.good, topic.top, false)">
</span>
<span class="name">{{topic.visit_count}}次浏览</span>
</div>
</section>
<section class='markdown-body topic-content' v-html="topic.content">
</section>
<h3 class="topic-reply">
<strong>{{topic.reply_count}}</strong> 回复
</h3>
<section class="reply-list">
<ul>
<li v-for="item in topic.replies">
<section class="user">
<router-link :to="{name:'user',params:{loginname:item.author.loginname}}" >
<img class="head" :src="item.author.avatar_url"/>
</router-link>
<div class="info">
<span class="cl">
<span class="name" v-text="item.author.loginname"></span>
<span class="name mt10">
<span></span>
发布于:{{item.create_at | getLastTimeStr(true)}}</span>
</span>
<span class="cr">
<span class="iconfont icon"
:class="{'uped':isUps(item.ups)}"
@click="upReply(item)">&#xe608;</span>
{{item.ups.length}}
<span class="iconfont icon" @click="addReply(item.id)">&#xe609;</span>
</span>
</div>
</section>
<div class="reply_content" v-html="item.content"></div>
<nv-reply :topic.sync="topic"
:topic-id="topicId"
:reply-id="item.id"
:reply-to="item.author.loginname"
:show.sync="curReplyId"
@close="hideItemReply"
v-if="userInfo.userId && curReplyId === item.id"></nv-reply>
</li>
</ul>
</section>
<nv-top></nv-top>
<nv-reply v-if="userInfo.userId"
:topic="topic"
:topic-id="topicId">
</nv-reply>
</div>
<div class='no-data' v-if="noData">
<i class="iconfont icon-empty">&#xe60a;</i>
该话题不存在!
</div>
</div>
</template>
<script>
import $ from 'webpack-zepto';
import utils from '../libs/utils.js';
import nvHead from '../components/header.vue';
import nvReply from '../components/reply.vue';
import nvTop from '../components/backtotop.vue';
import {
mapGetters
} from 'vuex';
export default {
data() {
return {
showMenu: false, // 是否展开左侧菜单
topic: {}, // 主题
noData: false,
topicId: '',
curReplyId: ''
};
},
computed: {
...mapGetters({
userInfo: 'getUserInfo'
})
},
mounted() {
// 隐藏左侧展开菜单
this.showMenu = false;
// 获取url传的tab参数
this.topicId = this.$route.params.id;
// 加载主题数据
$.get('https://cnodejs.org/api/v1/topic/' + this.topicId, (d) => {
if (d && d.data) {
this.topic = d.data;
} else {
this.noData = true;
}
});
},
methods: {
getTabInfo(tab, good = false, top, isClass) {
return utils.getTabInfo(tab, good, top, isClass);
},
getLastTimeStr(time, ago) {
return utils.getLastTimeStr(time, ago);
},
isUps(ups) {
return $.inArray(this.userInfo.userId, ups) >= 0;
},
addReply(id) {
this.curReplyId = id;
if (!this.userInfo.userId) {
this.$router.push({
name: 'login',
params: {
redirect: encodeURIComponent(this.$route.path)
}
});
}
},
hideItemReply() {
this.curReplyId = '';
},
upReply(item) {
if (!this.userInfo.userId) {
this.$router.push({
name: 'login',
params: {
redirect: encodeURIComponent(this.$route.path)
}
});
} else {
$.ajax({
type: 'POST',
url: 'https://cnodejs.org/api/v1/reply/' + item.id + '/ups',
data: {
accesstoken: this.userInfo.token
},
dataType: 'json',
success: (res) => {
if (res.success) {
if (res.action === 'down') {
let index = $.inArray(this.userInfo.userId, item.ups);
item.ups.splice(index, 1);
} else {
item.ups.push(this.userInfo.userId);
}
}
},
error: (res) => {
let error = JSON.parse(res.responseText);
this.$alert(error.error_msg);
return false;
}
});
}
}
},
components: {
nvHead,
nvReply,
nvTop
}
};
</script>
}

依然是有一个fix header 和list中一样
我们继续看看第二部分,构成了页面内容的只要部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
\\是否展示topic title
<div id="page"
:class="{'show-menu':showMenu}"
v-if="topic.title">
<h2 class="topic-title" v-text="topic.title"></h2>
\\author个人信息 照片等
<section class="author-info">
<img class="avatar" :src="topic.author.avatar_url" />
\\author 登录名
<div class="col">
<span>{{topic.author.loginname}}</span>
\\发布时间
<time>
发布于:{{topic.create_at | getLastTimeStr(true)}}
</time>
</div>
<div class="right">
<span class="tag"
:class="getTabInfo(topic.tab, topic.good, topic.top, true)"
v-text="getTabInfo(topic.tab, topic.good, topic.top, false)">
</span>
\\浏览量
<span class="name">{{topic.visit_count}}次浏览</span>
</div>
</section>
<section class='markdown-body topic-content' v-html="topic.content">
</section>
\\ 回复数
<h3 class="topic-reply">
<strong>{{topic.reply_count}}</strong> 回复
</h3>
\\ 回复列表
<section class="reply-list">
<ul>
<li v-for="item in topic.replies">
<section class="user">
<router-link :to="{name:'user',params:{loginname:item.author.loginname}}" >
<img class="head" :src="item.author.avatar_url"/>
</router-link>
<div class="info">
<span class="cl">
<span class="name" v-text="item.author.loginname"></span>
<span class="name mt10">
<span></span>
发布于:{{item.create_at | getLastTimeStr(true)}}</span>
</span>
<span class="cr">
<span class="iconfont icon"
:class="{'uped':isUps(item.ups)}"
@click="upReply(item)">&#xe608;</span>
{{item.ups.length}}
\\拿到relpyid 且判断用户是否登陆
<span class="iconfont icon" @click="addReply(item.id)">&#xe609;</span>
</span>
</div>
</section>
<div class="reply_content" v-html="item.content"></div>
<nv-reply :topic.sync="topic"
:topic-id="topicId"
:reply-id="item.id"
:reply-to="item.author.loginname"
:show.sync="curReplyId"
@close="hideItemReply"
v-if="userInfo.userId && curReplyId === item.id"></nv-reply>
</li>
</ul>
</section>
<nv-top></nv-top>
<nv-reply v-if="userInfo.userId"
:topic="topic"
:topic-id="topicId">
</nv-reply>
</div>
<div class='no-data' v-if="noData">
<i class="iconfont icon-empty">&#xe60a;</i>
该话题不存在!
</div>
</div>

非常简单主要是进行topic的展示 回复 更新等

小QA学习前端系列之vue实战2

发表于 2017-11-07

继续了解 list view

接上一节代码
我们首先去看看nvhead component
import nvHead from ‘../components/header.vue’;
其实就是 header.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<template>
<div>
<div class="page-cover" v-if="show&&fixHead" @click="showMenus">
</div>
<header :class="{'show':show&&fixHead,'fix-header':fixHead,'no-fix':!fixHead}" id="hd">
<div class="nv-toolbar">
<div class="toolbar-nav" @click="openMenu" v-if="fixHead">
</div>
<span v-text="pageType"></span>
<i class="num" v-if="messageCount > 0"> {{messageCount}}</i>
<router-link to="/add">
<i v-if="needAdd" v-show="!messageCount || messageCount <= 0" class="iconfont add-icon">&#xe60f;</i>
</router-link>
</div>
</header>
<nv-menu :show-menu="show" :page-type="pageType" :nick-name="nickname" :profile-url="profileimgurl" v-if="fixHead"></nv-menu>
</div>
</template>
<script>
import $ from 'webpack-zepto';
export default {
replace: true,
props: {
pageType: String,
fixHead: Boolean,
messageCount: Number,
scrollTop: 0,
needAdd: {
type: Boolean,
default: true
}
},
data () {
return {
nickname: '',
profileimgurl: '',
show: false
};
},
methods: {
openMenu() {
// $('html, body, #page').addClass('scroll-hide');
$('body').css('overflow', 'hidden');
this.show = !this.show;
},
showMenus() {
this.show = !this.show;
$('body').css('overflow', 'auto');
// $('html, body, #page').removeClass('scroll-hide');
}
},
components: {
'nvMenu': require('./menu.vue')
}
};
</script>

ok我们一行一行代码过

1
<div class="page-cover" v-if="show&&fixHead" @click="showMenus">

fixHead来自于list.vue中传递过来的参数

1
2
3
4
5
<nv-head :page-type="getTitleStr(searchKey.tab)"
ref="head"
:fix-head="true"
:need-add="true">
</nv-head>

show来自于 data()
click 调用showMenus()

1
2
3
4
5
showMenus() {
this.show = !this.show;
$('body').css('overflow', 'auto');
// $('html, body, #page').removeClass('scroll-hide');
}

1
<header :class="{'show':show&&fixHead,'fix-header':fixHead,'no-fix':!fixHead}" id="hd">

通过一些参数来定制header的样式

1
<div class="toolbar-nav" @click="openMenu" v-if="fixHead">

click方法调用 openmenu

1
2
3
4
5
openMenu() {
// $('html, body, #page').addClass('scroll-hide');
$('body').css('overflow', 'hidden');
this.show = !this.show;
}

1
<i class="num" v-if="messageCount > 0"> {{messageCount}}</i>

messageCount 来自于messagfe.vue

1
2
3
4
5
<nv-head page-type="消息"
:fix-head="true"
:show-menu="showMenu"
:message-count="no_read_len"
:need-add="true" ></nv-head>
1
2
3
<router-link to="/add">
<i v-if="needAdd" v-show="!messageCount || messageCount <= 0" class="iconfont add-icon">&#xe60f;</i>
</router-link>

router-link 指向 add 路由

1
<nv-menu :show-menu="show" :page-type="pageType" :nick-name="nickname" :profile-url="profileimgurl" v-if="fixHead"></nv-menu>

:show-menu=”show” :page-type=”pageType” :nick-name=”nickname” :profile-url=”profileimgurl”
调用方法 传参数 返回其值
上面基本的header 组件已经详尽介绍
下来让我回到list中继续
list 中 第二部分由一个html5 section标签构成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<section id="page">
<!-- 首页列表 -->
<ul class="posts-list">
<li v-for="item in topics" :key="item.id">
<router-link :to="{name:'topic',params:{id:item.id}}">
<h3 v-text="item.title"
:class="getTabInfo(item.tab, item.good, item.top, true)"
:title="getTabInfo(item.tab, item.good, item.top, false)">
</h3>
<div class="content">
<img class="avatar" :src="item.author.avatar_url" />
<div class="info">
<p>
<span class="name">
{{item.author.loginname}}
</span>
<span class="status" v-if="item.reply_count > 0">
<b>{{item.reply_count}}</b>
/{{item.visit_count}}
</span>
</p>
<p>
<time>{{item.create_at | getLastTimeStr(true)}}</time>
<time>{{item.last_reply_at | getLastTimeStr(true)}}</time>
</p>
</div>
</div>
</router-link>
</li>
</ul>
</section>

由一个无序例表组成


  • 循环输出topics 对象 key 是item.id
    topic则来自于 data()返回 由mounted()方法计算后得出
    具体流程mounted 调用 sessionstorage或者 调用getTopics ,getTopics又去调用mergeTopics 之后将 topic返回

    跳转到参数为item.id topic页面
  • <h3 v-text="item.title"
                               :class="getTabInfo(item.tab, item.good, item.top, true)"
                               :title="getTabInfo(item.tab, item.good, item.top, false)">
                       </h3>
    

    设置样式

    其他也没什么了 都去 item对象中取值

    小QA学习前端系列之vue实战

    发表于 2017-10-31

    ##通过一个简单vue项目了解vue整个流程

    1
    https://github.com/shinygang/Vue-cnodejs.git

    大神写的一个高仿cnodejs
    直接clone

    1
    yarn

    启动服务(http://localhost:8020)

    1
    npm run dev

    然而我们并不是让你们看如何这个页面如何好看,那没啥用
    还是进入code中,才能理解其中的玄学
    好的编辑器是成功的一半,所以我选vscode,你呢 哈哈
    好了 进入主题 先看项目结构

    项目结构

    项目结构
    采用 vue2 vue-router2 vuex 组件思想构建了整个项目
    先来看看 main.js main文件为整个文件的入口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    import Vue from 'vue';
    import $ from 'webpack-zepto';
    import VueRouter from 'vue-router';
    import filters from './filters';
    import routes from './routers';
    import Alert from './libs/alert';
    import store from './vuex/user';
    import FastClick from 'fastclick';
    //引入Vuerouter
    Vue.use(VueRouter);
    //引入alert
    Vue.use(Alert);
    $.ajaxSettings.crossDomain = true;
    // 实例化Vue的filter
    Object.keys(filters).forEach(k => Vue.filter(k, filters[k]));
    // 实例化VueRouter
    const router = new VueRouter({
    mode: 'history',
    routes
    });
    //FastClick是一个非常方便的库,在移动浏览器上发生介于轻敲及点击之间的指令时,能够让你//摆脱300毫秒的延迟。FastClick可以让你的应用程序更加灵敏迅捷。
    FastClick.attach(document.body);
    // 处理刷新的时候vuex被清空但是用户已经登录的情况
    if (window.sessionStorage.user) {
    store.dispatch('setUserInfo', JSON.parse(window.sessionStorage.user));
    }
    // 登录中间验证,页面需要登录而没有登录的情况直接跳转登录
    router.beforeEach((to, from, next) => {
    // 处理左侧滚动不影响右边
    // $('html, body, #page').removeClass('scroll-hide');
    $('body').css('overflow', 'auto');
    if (to.matched.some(record => record.meta.requiresAuth)) {
    if (store.state.userInfo.userId) {
    next();
    } else {
    next({
    path: '/login',
    query: { redirect: to.fullPath }
    });
    }
    } else {
    next();
    }
    });
    new Vue({
    router,
    store
    }).$mount('#app');

    这就是整个main文件 只是将vuex store 与router 最后都实例化到vue对象中并挂在到id为app的节点下

    路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    // require.ensure 是 Webpack 的特殊语法,用来设置 code-split point
    const Home = resolve => {
    require.ensure(['./views/index.vue'], () => {
    resolve(require('./views/index.vue'));
    });
    };
    const List = resolve => {
    require.ensure(['./views/list.vue'], () => {
    resolve(require('./views/list.vue'));
    });
    };
    const routers = [{
    path: '/',
    name: 'home',
    component: Home
    }, {
    path: '/cnodevue',
    name: 'cnodevue',
    component: Home
    }, {
    path: '/list',
    name: 'list',
    component: List
    }, {
    path: '/topic/:id',
    name: 'topic',
    component(resolve) {
    require.ensure(['./views/topic.vue'], () => {
    resolve(require('./views/topic.vue'));
    });
    }
    }, {
    path: '/add',
    name: 'add',
    component(resolve) {
    require.ensure(['./views/new.vue'], () => {
    resolve(require('./views/new.vue'));
    });
    },
    meta: { requiresAuth: true }
    }, {
    path: '/message',
    name: 'message',
    component(resolve) {
    require.ensure(['./views/message.vue'], () => {
    resolve(require('./views/message.vue'));
    });
    },
    meta: { requiresAuth: true }
    }, {
    path: '/user/:loginname',
    name: 'user',
    component(resolve) {
    require.ensure(['./views/user.vue'], () => {
    resolve(require('./views/user.vue'));
    });
    }
    }, {
    path: '/about',
    name: 'about',
    component(resolve) {
    require.ensure(['./views/about.vue'], () => {
    resolve(require('./views/about.vue'));
    });
    }
    }, {
    path: '/login',
    name: 'login',
    component(resolve) {
    require.ensure(['./views/login.vue'], () => {
    resolve(require('./views/login.vue'));
    });
    }
    }, {
    path: '*',
    component: Home
    }];
    export default routers;

    上面定义了系统所有的路由路径,以及需要实现懒加载的路由

    组件

    我们先看看系统由多少组件组合而成
    /image/component.png
    再来看看 views
    /image/view.png
    回到我们的router 当我们进入一个应用时,首先呈现在我们眼前的应该是index.
    我们看看router的代码

    1
    2
    3
    path: '/',
    name: 'home',
    component: Home

    而component 则是来自 index.vue

    1
    2
    3
    4
    5
    const Home = resolve => {
    require.ensure(['./views/index.vue'], () => {
    resolve(require('./views/index.vue'));
    });
    };

    ok 我们看看 index.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <template>
    <div>
    <img class="index" src="../assets/images/index.png">
    </div>
    </template>
    <script>
    require('../assets/scss/CV.scss');
    require('../assets/scss/iconfont/iconfont.css');
    require('../assets/scss/github-markdown.css');
    export default {
    mounted() {
    setTimeout(() => {
    this.$router.push({
    name: 'list'
    });
    }, 2000);
    }
    };
    </script>
    <style lang="scss">
    .index {
    width: 100%;
    background-color: #fff;
    margin-top: 40%;
    }
    </style>

    index.vue 只是利用mount去向$router push一个{ name: ‘list’}对象
    设置了一个异步操作 2秒后 跳转到list页面
    在来看看 index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>Vue.js-Cnodejs社区</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
    <meta content="yes" name="apple-mobile-web-app-capable">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta content="black" name="apple-mobile-web-app-status-bar-style">
    </head>
    <body>
    <div id="app" v-cloak>
    <router-view></router-view>
    </div>
    <!-- built files will be auto injected -->
    </body>
    </html>

    他是整个SPA的基础页面

    将真个应用会挂载的id 为app 这个节点下。

    再来看看index跳转的list页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    <template>
    <div>
    <!-- 全局header -->
    <nv-head :page-type="getTitleStr(searchKey.tab)"
    ref="head"
    :fix-head="true"
    :need-add="true">
    </nv-head>
    <section id="page">
    <!-- 首页列表 -->
    <ul class="posts-list">
    <li v-for="item in topics" :key="item.id">
    <router-link :to="{name:'topic',params:{id:item.id}}">
    <h3 v-text="item.title"
    :class="getTabInfo(item.tab, item.good, item.top, true)"
    :title="getTabInfo(item.tab, item.good, item.top, false)">
    </h3>
    <div class="content">
    <img class="avatar" :src="item.author.avatar_url" />
    <div class="info">
    <p>
    <span class="name">
    {{item.author.loginname}}
    </span>
    <span class="status" v-if="item.reply_count > 0">
    <b>{{item.reply_count}}</b>
    /{{item.visit_count}}
    </span>
    </p>
    <p>
    <time>{{item.create_at | getLastTimeStr(true)}}</time>
    <time>{{item.last_reply_at | getLastTimeStr(true)}}</time>
    </p>
    </div>
    </div>
    </router-link>
    </li>
    </ul>
    </section>
    <nv-top></nv-top>
    </div>
    </template>
    <script>
    import $ from 'webpack-zepto';
    import utils from '../libs/utils.js';
    import nvHead from '../components/header.vue';
    import nvTop from '../components/backtotop.vue';
    export default {
    filters: {
    getLastTimeStr(time, isFromNow) {
    return utils.getLastTimeStr(time, isFromNow);
    }
    },
    data() {
    return {
    scroll: true,
    topics: [],
    index: {},
    searchKey: {
    page: 1,
    limit: 20,
    tab: 'all',
    mdrender: true
    },
    searchDataStr: ''
    };
    },
    mounted() {
    if (this.$route.query && this.$route.query.tab) {
    this.searchKey.tab = this.$route.query.tab;
    }
    // 如果从详情返回并且之前存有对应的查询条件和参数
    // 则直接渲染之前的数据
    if (window.window.sessionStorage.searchKey && window.window.sessionStorage.tab === this.searchKey.tab) {
    this.topics = JSON.parse(window.window.sessionStorage.topics);
    this.searchKey = JSON.parse(window.window.sessionStorage.searchKey);
    this.$nextTick(() => $(window).scrollTop(window.window.sessionStorage.scrollTop));
    } else {
    this.getTopics();
    }
    // 滚动加载
    $(window).on('scroll', utils.throttle(this.getScrollData, 300, 1000));
    },
    beforeRouteLeave(to, from, next) {
    // 如果跳转到详情页面,则记录关键数据
    // 方便从详情页面返回到该页面的时候继续加载之前位置的数据
    if (to.name === 'topic') {
    // 当前滚动条位置
    window.window.sessionStorage.scrollTop = $(window).scrollTop();
    // 当前页面主题数据
    window.window.sessionStorage.topics = JSON.stringify(this.topics);
    // 查询参数
    window.window.sessionStorage.searchKey = JSON.stringify(this.searchKey);
    // 当前tab
    window.window.sessionStorage.tab = from.query.tab || 'all';
    }
    $(window).off('scroll');
    next();
    },
    beforeRouteEnter(to, from, next) {
    if (from.name !== 'topic') {
    // 页面切换移除之前记录的数据集
    if (window.window.sessionStorage.tab) {
    window.window.sessionStorage.removeItem('topics');
    window.window.sessionStorage.removeItem('searchKey');
    window.window.sessionStorage.removeItem('tab');
    }
    }
    next();
    },
    methods: {
    // 获取title文字
    getTitleStr(tab) {
    let str = '';
    switch (tab) {
    case 'share':
    str = '分享';
    break;
    case 'ask':
    str = '问答';
    break;
    case 'job':
    str = '招聘';
    break;
    case 'good':
    str = '精华';
    break;
    default:
    str = '全部';
    break;
    }
    return str;
    },
    // 获取不同tab的样式或者标题
    getTabInfo(tab, good, top, isClass) {
    return utils.getTabInfo(tab, good, top, isClass);
    },
    // 获取主题数据
    getTopics() {
    let params = $.param(this.searchKey);
    $.get('https://cnodejs.org/api/v1/topics?' + params, (d) => {
    this.scroll = true;
    if (d && d.data) {
    d.data.forEach(this.mergeTopics);
    }
    });
    },
    mergeTopics(topic) {
    if (typeof this.index[topic.id] === 'number') {
    const topicsIndex = this.index[topic.id];
    this.topics[topicsIndex] = topic;
    } else {
    this.index[topic.id] = this.topics.length;
    this.topics.push(topic);
    }
    },
    // 滚动加载数据
    getScrollData() {
    if (this.scroll) {
    let totalheight = parseInt($(window).height(), 20) + parseInt($(window).scrollTop(), 20);
    if ($(document).height() <= totalheight + 200) {
    this.scroll = false;
    this.searchKey.page += 1;
    this.getTopics();
    }
    }
    }
    },
    watch: {
    // 切换页面
    '$route' (to, from) {
    // 如果是当前页面切换分类的情况
    if (to.query && to.query.tab) {
    this.searchKey.tab = to.query.tab;
    this.topics = [];
    this.index = {};
    }
    this.searchKey.page = 1;
    this.getTopics();
    // 隐藏导航栏
    this.$refs.head.show = false;
    }
    },
    components: {
    nvHead,
    nvTop
    }
    };
    </script>

    利用了2个component nvHEAD nvTop
    下一篇 继续讲解 list view

    小QA学习前端系列之vue router

    发表于 2017-10-30

    创建组件

    首先引入vue.js和vue-router.js:

    1
    2
    3
    4
    5
    6
    7
    8
    npm install vue-router --save
    "dependencies": {
    ...
    "vue-router": "^2.1.1"
    ...
    },
    或者
    npm install vue-router --save-dev

    然后创建两个组件构造器Home和About:
    App.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <template>
    <div id="app">
    <router-link to="/">1</router-link>
    <router-link to="/2">2</router-link>
    <br/>
    <router-view></router-view>
    </div>
    </template>

    1. 创建Router
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      import Vue from 'vue'
      import App from './App'
      import VueRouter from 'vue-router'
      import Page1 from './components/page1'
      import Page2 from './components/page2'
      var router = new VueRouter({
      routes
      })
      new Vue({
      el: '#app',
      template: '<App/>',
      components: { App },
      router
      })

    调用构造器VueRouter,创建一个路由器实例router。

    1. 映射路由
      1
      2
      3
      4
      const routes = [
      { path: '/', component: Page1 },
      { path: '/2', component: Page2 },
      ]

    page1.vue

    1
    2
    3
    4
    5
    6
    <template>
    <div>
    <h1>page2</h1>
    </div>
    </template>

    page2.vue

    1
    2
    3
    4
    5
    <template>
    <div>
    <h1>page2</h1>
    </div>
    </template>

    实现步骤:

    * npm安装vue-router
    * Vue.use(VueRouter)全局安装路由功能
    * 定义路径数组routes并创建路由对象router
    * 将路由注入到Vue对象中
    * 在根组件中使用<router-link>定义跳转路径
    * 在根组件中使用<router-view>来渲染组件
    * 创建子组件
    

    路由的跳转

    router-link

    router-link标签用于页面的跳转

    1
    <router-link to="/page1">page1</router-link>

    当点击这个router-link标签时 router-view就会渲染路径为/page1的页面。
    注意:router-link默认是一个a标签的形式,如果需要显示不同的样子,可以在router-link标签中写入不同标签元素,如下显示为button按钮。

    1
    2
    3
    <router-link to="/04">
    <button>to04</button>
    </router-link>

    会被渲染为 标签, to 会被渲染为 href,当 被点击的时候,url 会发生相应的改变

    如果使用 v-bind 指令,还可以在 to 后面接变量,配合 v-for 指令可以渲染导航菜单
    如果对于所有 ID 各不相同的用户,都要使用 home 组件来渲染,可以在 routers.js 中添加动态参数:

    {
    path: ‘/home/:id’,
    component: Home
    }

    这样 “/home/user01”、”/home/user02”、”/home/user03” 等路由,都会映射到 Home 组件

    然后还可以使用 $route.params.id 来获取到对应的 id

    动态路由匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="app">
    <h1>Hello App!</h1>
    <p>
    <router-link to="/user/foo/post/123">Go to Foo</router-link>
    <router-link to="/user/bar/post/456">Go to Bar</router-link>
    </p>
    <router-view></router-view>
    </div>
    <template id='user'>
    <p>User:{{ $route.params.id }},Post:{{$route.params.post_id}}</p>
    </template>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 1. 定义组件。
    const User = {
    template:'#user',
    watch:{
    '$route'(to,from){
    console.log('从'+from.params.id+'到'+to.params.id);
    }
    }
    };
    // 2. 创建路由实例 (可设置多段路径参数)
    const router = new VueRouter({
    routes:[
    { path:'/user/:id/post/:post_id',component:User }
    ]
    });
    //3. 创建和挂载根实例
    const app = new Vue({ router:router }).$mount('#app');

    编程式导航 router.push

    也可以通过JS代码控制路由的界面渲染,方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    // 字符串
    router.push('home')
    // 对象
    router.push({ path: 'home' })
    // 命名的路由
    router.push({ name: 'user', params: { userId: 123 }})
    // 带查询参数,变成 /register?plan=private
    router.push({ path: 'register', query: { plan: 'private' }})

    实际情况下,有很多按钮在执行跳转之前,还会执行一系列方法,这时可以使用 this.$router.push(location) 来修改 url,完成跳转

    1
    2
    3
    4
    <div>
    <button>
    </button class="登录" @click="go"> </button>
    </div>

    1
    2
    3
    4
    5
    6
    7
    8
    new Vue({
    el: '#app',
    method: {
    go:()=>{
    this.$router.push({path:"/home/test"})
    }
    }
    })

    嵌套路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <div id="app">
    <h1>Hello App!</h1>
    <p>
    <router-link to="/user/foo">Go to Foo</router-link>
    <router-link to="/user/foo/profile">Go to profile</router-link>
    <router-link to="/user/foo/posts">Go to posts</router-link>
    </p>
    <router-view></router-view>
    </div>
    <template id='user'>
    <div>
    <h2>User:{{ $route.params.id }}</h2>
    <router-view></router-view>
    </div>
    </template>
    <template id="userHome">
    <p>主页</p>
    </template>
    <template id="userProfile">
    <p>概况</p>
    </template>
    <template id="userPosts">
    <p>登录信息</p>
    </template>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    / 1. 定义组件。
    const User = {
    template:'#user'
    };
    const UserHome = {
    template:'#userHome'
    };
    const UserProfile = {
    template:'#userProfile'
    };
    const UserPosts = {
    template:'#userPosts'
    };
    // 2. 创建路由实例
    const router = new VueRouter({
    routes:[
    { path:'/user/:id', component:User,
    children:[
    // 当 /user/:id 匹配成功,
    // UserHome 会被渲染在 User 的 <router-view> 中
    { path: '', component: UserHome},
    // 当 /user/:id/profile 匹配成功,
    // UserProfile 会被渲染在 User 的 <router-view> 中
    { path:'profile', component:UserProfile },
    // 当 /user/:id/posts 匹配成功
    // UserPosts 会被渲染在 User 的 <router-view> 中
    { path: 'posts', component: UserPosts }
    ]
    }
    ]
    });
    //3. 创建和挂载根实例
    const app = new Vue({ router:router }).$mount('#app');

    小QA学习前端系列之vue 组件

    发表于 2017-10-26

    vue组件

    组件可以扩展HTML元素,封装可重用的HTML代码,我们可以将组件看作自定义的HTML元素。

    全局组件

    要注册一个全局组件,可以使用 Vue.component(tagName, options)。例如:

    1
    2
    3
    Vue.component('my-component', {
    // 选项
    })

    组件在注册之后,便可以作为自定义元素 在一个实例的模板中使用。注意确保在初始化根实例之前注册组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div id="example">
    <my-component></my-component>
    </div>
    // 注册
    Vue.component('my-component', {
    template: '<div>A custom component!</div>'
    })
    // 创建根实例
    new Vue({
    el: '#example'
    })

    局部组件

    你不必把每个组件都注册到全局。你可以通过某个 Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var Child = {
    template: '<div>A custom component!</div>'
    }
    new Vue({
    // ...
    components: {
    // <my-component> 将只在父组件模板中可用
    'my-component': Child
    }
    })

    这种封装也适用于其它可注册的 Vue 功能,比如指令。
    DOM 模板解析注意事项

    当使用 DOM 作为模板时 (例如,使用 el 选项来把 Vue 实例挂载到一个已有内容的元素上),你会受到 HTML 本身的一些限制,因为 Vue 只有在浏览器解析、规范化模板之后才能获取其内容。尤其要注意,像

      、
        、、

    在自定义组件中使用这些受限制的元素时会导致一些问题,例如:


    …

    自定义组件 会被当作无效的内容,因此会导致错误的渲染结果。变通的方案是使用特殊的 is 特性:



    应当注意,如果使用来自以下来源之一的字符串模板,则没有这些限制:

    <script type="text/x-template">
    JavaScript 内联模板字符串
    .vue 组件
    

    因此,请尽可能使用字符串模板。

    data 必须为函数

    构造 Vue 实例时传入的各种选项大多数都可以在组件里使用。只有一个例外:data 必须是函数。实际上,如果你这么做:

    1
    2
    3
    4
    5
    6
    Vue.component('my-component', {
    template: '<span>{{ message }}</span>',
    data: {
    message: 'hello'
    }
    })

    那么 Vue 会停止运行,并在控制台发出警告,告诉你在组件实例中 data 必须是一个函数。但理解这种规则为何存在也是很有益处的,所以让我们先作个弊:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <div id="example-2">
    <simple-counter></simple-counter>
    <simple-counter></simple-counter>
    <simple-counter></simple-counter>
    </div>
    var data = { counter: 0 }
    Vue.component('simple-counter', {
    template: '<button v-on:click="counter += 1">{{ counter }}</button>',
    // 技术上 data 的确是一个函数了,因此 Vue 不会警告,
    // 但是我们却给每个组件实例返回了同一个对象的引用
    data: function () {
    return data
    }
    })
    new Vue({
    el: '#example-2'
    })

    由于这三个组件实例共享了同一个 data 对象,因此递增一个 counter 会影响所有组件!这就错了。我们可以通过为每个组件返回全新的数据对象来修复这个问题:

    1
    2
    3
    4
    5
    data: function () {
    return {
    counter: 0
    }
    }

    由于这三个组件实例共享了同一个 data 对象,因此递增一个 counter 会影响所有组件!这就错了。我们可以通过为每个组件返回全新的数据对象来修复这个问题:

    1
    2
    3
    4
    5
    data: function () {
    return {
    counter: 0
    }
    }

    组件组合

    在 Vue 中,父子组件的关系可以总结为 prop 向下传递,事件向上传递。父组件通过 prop 给子组件下发数据,子组件通过事件给父组件发送消息。看看它们是怎么工作的。
    https://cn.vuejs.org/images/props-events.png

    Prop

    使用 Prop 传递数据

    组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。

    子组件要显式地用 props 选项声明它预期的数据:

    1
    2
    3
    4
    5
    6
    7
    Vue.component('child', {
    // 声明 props
    props: ['message'],
    // 就像 data 一样,prop 也可以在模板中使用
    // 同样也可以在 vm 实例中通过 this.message 来使用
    template: '<span>{{ message }}</span>'
    })

    传递

    1
    <child message="hello!"></child>

    camelCase vs. kebab-case

    HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名):

    1
    2
    3
    4
    5
    6
    7
    8
    Vue.component('child', {
    // 在 JavaScript 中使用 camelCase
    props: ['myMessage'],
    template: '<span>{{ myMessage }}</span>'
    })
    <!-- 在 HTML 中使用 kebab-case -->
    <child my-message="hello!"></child>

    如果你使用字符串模板,则没有这些限制。

    动态 Prop

    与绑定到任何普通的 HTML 特性相类似,我们可以用 v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:

    1
    2
    3
    4
    5
    <div>
    <input v-model="parentMsg">
    <br>
    <child v-bind:my-message="parentMsg"></child>
    </div>

    你也可以使用 v-bind 的缩写语法:

    1
    <child :my-message="parentMsg"></child>

    如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一个 todo 对象:

    1
    2
    3
    4
    todo: {
    text: 'Learn Vue',
    isComplete: false
    }

    然后:

    1
    <todo-item v-bind="todo"></todo-item>

    将等价于:

    1
    2
    3
    4
    <todo-item
    v-bind:text="todo.text"
    v-bind:is-complete="todo.isComplete"
    ></todo-item>

    字面量语法 vs 动态语法

    初学者常犯的一个错误是使用字面量语法传递数值:

    1
    2
    <!-- 传递了一个字符串 "1" -->
    <comp some-prop="1"></comp>

    因为它是一个字面量 prop,它的值是字符串 “1” 而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 v-bind,从而让它的值被当作 JavaScript 表达式计算:

    1
    2
    <!-- 传递真正的数值 -->
    <comp v-bind:some-prop="1"></comp>

    单向数据流

    Prop 验证

    我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用。

    要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组:

    Vue.component(‘example’, {
    props: {
    // 基础类型检测 (null 指允许任何类型)
    propA: Number,
    // 可能是多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
    type: String,
    required: true
    },
    // 数值且有默认值
    propD: {
    type: Number,
    default: 100
    },
    // 数组/对象的默认值应当由一个工厂函数返回
    propE: {
    type: Object,
    default: function () {
    return { message: ‘hello’ }
    }
    },
    // 自定义验证函数
    propF: {
    validator: function (value) {
    return value > 10
    }
    }
    }
    })

    type 可以是下面原生构造器:

    String
    Number
    Boolean
    Function
    Object
    Array
    Symbol
    

    type 也可以是一个自定义构造器函数,使用 instanceof 检测。

    当 prop 验证失败,Vue 会抛出警告 (如果使用的是开发版本)。注意 prop 会在组件实例创建之前进行校验,所以在 default 或 validator 函数里,诸如 data、computed 或 methods 等实例属性还无法使用。

    自定义事件

    我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。
    使用 v-on 绑定自定义事件

    每个 Vue 实例都实现了事件接口,即:

    使用 $on(eventName) 监听事件
    使用 $emit(eventName) 触发事件
    

    Vue 的事件系统与浏览器的 EventTarget API 有所不同。尽管它们的运行起来类似,但是 $on 和 $emit 并不是addEventListener 和 dispatchEvent 的别名。

    另外,父组件可以在使用子组件的地方直接用 v-on 来监听子组件触发的事件。

    不能用 $on 侦听子组件释放的事件,而必须在模板里直接用 v-on 绑定,参见下面的例子。

    下面是一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    <div id="counter-event-example">
    <p>{{ total }}</p>
    <button-counter v-on:increment="incrementTotal"></button-counter>
    <button-counter v-on:increment="incrementTotal"></button-counter>
    </div>
    Vue.component('button-counter', {
    template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
    data: function () {
    return {
    counter: 0
    }
    },
    methods: {
    incrementCounter: function () {
    this.counter += 1
    this.$emit('increment')
    }
    },
    })
    new Vue({
    el: '#counter-event-example',
    data: {
    total: 0
    },
    methods: {
    incrementTotal: function () {
    this.total += 1
    }
    }
    })

    .sync 修饰符

    我们可能会需要对一个 prop 进行“双向绑定”。
    如下代码

    会被扩展为:

    当子组件需要更新 foo 的值时,它需要显式地触发一个更新事件:

    this.$emit(‘update:foo’, newValue)

    小QA学习前端系列之vue 表单输入绑定

    发表于 2017-10-25

    基础用法

    v-model 指令在表单控件元素上创建双向数据绑定。
    v-model 本质上不过是语法糖,它负责监听用户的输入事件以更新数据,并特别处理一些极端的例子。
    v-model 会忽略所有表单元素的 value、checked、selected 特性的初始值。因为它会选择 Vue 实例数据来作为具体的值。你应该通过 JavaScript 在组件的 data 选项中声明初始值。

    对于要求 IME (如中文、日语、韩语等) (IME 意为“输入法”)的语言,你会发现 v-model 不会在 ime 输入中得到更新。如果你也想实现更新,请使用 input 事件。

    文本

    1
    2
    <input v-model="message" placeholder="edit me">
    <p>Message is: {{ message }}</p>

    多行文本

    1
    2
    3
    4
    <span>Multiline message is:</span>
    <p style="white-space: pre-line;">{{ message }}</p>
    <br>
    <textarea v-model="message" placeholder="add multiple lines"></textarea>

    复选框

    单个勾选框,逻辑值:

    1
    2
    <input type="checkbox" id="checkbox" v-model="checked">
    <label for="checkbox">{{ checked }}</label>

    多个勾选框,绑定到同一个数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <div id='example-3'>
    <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
    <label for="jack">Jack</label>
    <input type="checkbox" id="john" value="John" v-model="checkedNames">
    <label for="john">John</label>
    <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
    <label for="mike">Mike</label>
    <br>
    <span>Checked names: {{ checkedNames }}</span>
    </div>
    new Vue({
    el: '#example-3',
    data: {
    checkedNames: []
    }
    })

    单选按钮

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <div id="example-4">
    <input type="radio" id="one" value="One" v-model="picked">
    <label for="one">One</label>
    <br>
    <input type="radio" id="two" value="Two" v-model="picked">
    <label for="two">Two</label>
    <br>
    <span>Picked: {{ picked }}</span>
    </div>
    new Vue({
    el: '#example-4',
    data: {
    picked: ''
    }
    })

    选择列表

    单选列表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <div id="example-5">
    <select v-model="selected">
    <option disabled value="">请选择</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
    </select>
    <span>Selected: {{ selected }}</span>
    </div>
    new Vue({
    el: '...',
    data: {
    selected: ''
    }
    })

    多选列表 (绑定到一个数组):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <div id="example-6">
    <select v-model="selected" multiple style="width: 50px;">
    <option>A</option>
    <option>B</option>
    <option>C</option>
    </select>
    <br>
    <span>Selected: {{ selected }}</span>
    </div>
    new Vue({
    el: '#example-6',
    data: {
    selected: []
    }
    })

    动态选项,用 v-for 渲染:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <select v-model="selected">
    <option v-for="option in options" v-bind:value="option.value">
    {{ option.text }}
    </option>
    </select>
    <span>Selected: {{ selected }}</span>
    new Vue({
    el: '...',
    data: {
    selected: 'A',
    options: [
    { text: 'One', value: 'A' },
    { text: 'Two', value: 'B' },
    { text: 'Three', value: 'C' }
    ]
    }
    })

    值绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!-- 当选中时,`picked` 为字符串 "a" -->
    <input type="radio" v-model="picked" value="a">
    <!-- `toggle` 为 true 或 false -->
    <input type="checkbox" v-model="toggle">
    <!-- 当选中时,`selected` 为字符串 "abc" -->
    <select v-model="selected">
    <option value="abc">ABC</option>
    </select>

    复选框

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <input
    type="checkbox"
    v-model="toggle"
    v-bind:true-value="a"
    v-bind:false-value="b"
    >
    // 当选中时
    vm.toggle === vm.a
    // 当没有选中时
    vm.toggle === vm.b

    单选按钮

    1
    2
    3
    4
    <input type="radio" v-model="pick" v-bind:value="a">
    // 当选中时
    vm.pick === vm.a

    选择列表的选项

    1
    2
    3
    4
    5
    6
    7
    8
    select v-model="selected">
    <!-- 内联对象字面量 -->
    <option v-bind:value="{ number: 123 }">123</option>
    </select>
    // 当选中时
    typeof vm.selected // => 'object'
    vm.selected.number // => 123

    修饰符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    .lazy
    在默认情况下,v-model 在 input 事件中同步输入框的值与数据 (除了 上述 IME 部分),但你可以添加一个修饰符 lazy ,从而转变为在 change 事件中同步:
    <!-- 在 "change" 而不是 "input" 事件中更新 -->
    <input v-model.lazy="msg" >
    .number
    如果想自动将用户的输入值转为 Number 类型 (如果原值的转换结果为 NaN 则返回原值),可以添加一个修饰符 number 给 v-model 来处理输入值:
    <input v-model.number="age" type="number">
    这通常很有用,因为在 type="number" 时 HTML 中输入的值也总是会返回字符串类型。
    .trim
    如果要自动过滤用户输入的首尾空格,可以添加 trim 修饰符到 v-model 上过滤输入:
    <input v-model.trim="msg">
    v-model 与组件
    如果你还不熟悉 Vue 的组件,跳过这里即可。
    HTML 内建的 input 类型有时不能满足你的需求。还好,Vue 的组件系统允许你创建一个具有自定义行为可复用的 input 类型,这些 input 类型甚至可以和 v-model 一起使用!要了解更多,请参阅自定义 input 类型。

    小QA学习前端系列之vue 事件处理

    发表于 2017-10-24

    事件处理

    用 v-on 指令监听 DOM 事件来触发一些 JavaScript 代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    div id="example-1">
    <button v-on:click="counter += 1">增加 1</button>
    <p>这个按钮被点击了 {{ counter }} 次。</p>
    </div>
    var example1 = new Vue({
    el: '#example-1',
    data: {
    counter: 0
    }
    })

    方法事件处理器

    v-on 可以接收一个定义的方法来调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    示例:
    <div id="example-2">
    <!-- `greet` 是在下面定义的方法名 -->
    <button v-on:click="greet">Greet</button>
    </div>
    var example2 = new Vue({
    el: '#example-2',
    data: {
    name: 'Vue.js'
    },
    // 在 `methods` 对象中定义方法
    methods: {
    greet: function (event) {
    // `this` 在方法里指当前 Vue 实例
    alert('Hello ' + this.name + '!')
    // `event` 是原生 DOM 事件
    if (event) {
    alert(event.target.tagName)
    }
    }
    }
    })
    // 也可以用 JavaScript 直接调用方法
    example2.greet() // => 'Hello Vue.js!'

    内联处理器里的方法

    除了直接绑定到一个方法,也可以用内联 JavaScript 语句:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <div id="example-3">
    <button v-on:click="say('hi')">Say hi</button>
    <button v-on:click="say('what')">Say what</button>
    </div>
    new Vue({
    el: '#example-3',
    methods: {
    say: function (message) {
    alert(message)
    }
    }
    })

    内联语句处理器中访问原生 DOM 事件。可以用特殊变量 $event 把它传入方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <button v-on:click="warn('Form cannot be submitted yet.', $event)">
    Submit
    </button>
    // ...
    methods: {
    warn: function (message, event) {
    // 现在我们可以访问原生事件对象
    if (event) event.preventDefault()
    alert(message)
    }
    }

    事件修饰符

    在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。
    Vue.js 为 v-on 提供了事件修饰符。通过由点 (.) 表示的指令后缀来调用修饰符。

    .stop
    .prevent
    .capture
    .self
    .once
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- 阻止单击事件冒泡 -->
    <a v-on:click.stop="doThis"></a>
    <!-- 提交事件不再重载页面 -->
    <form v-on:submit.prevent="onSubmit"></form>
    <!-- 修饰符可以串联 -->
    <a v-on:click.stop.prevent="doThat"></a>
    <!-- 只有修饰符 -->
    <form v-on:submit.prevent></form>
    <!-- 添加事件侦听器时使用事件捕获模式 -->
    <div v-on:click.capture="doThis">...</div>
    <!-- 只当事件在该元素本身 (比如不是子元素) 触发时触发回调 -->
    <div v-on:click.self="doThat">...</div>
    <!-- 点击事件将只会触发一次 -->
    <a v-on:click.once="doThis"></a>

    键值修饰符

    Vue 允许为 v-on 在监听键盘事件时添加关键修饰符:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 只有在 keyCode 是 13 时调用 vm.submit() -->
    <input v-on:keyup.13="submit">
    记住所有的 keyCode 比较困难,所以 Vue 为最常用的按键提供了别名:
    <!-- 同上 -->
    <input v-on:keyup.enter="submit">
    <!-- 缩写语法 -->
    <input @keyup.enter="submit"

    自动匹配按键修饰符

    你也可以通过将它们转换到 kebab-case 来直接使用由 KeyboardEvent.key 暴露的任意有效按键名作为修饰符:

    在上面的例子中,处理函数仅在 $event.key === ‘PageDown’ 时被调用。

    系统修饰键

    可以用如下修饰符开启鼠标或键盘事件监听,使在按键按下时发生响应。

    1
    2
    3
    4
    5
    6
    .ctrl
    .alt
    .shift
    .meta
    注意:在 Mac 系统键盘上,meta 对应命令键 (⌘)。在 Windows 系统键盘 meta 对应 windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 键盘及其后续,比如 Knight 键盘,space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。

    例如:

    1
    2
    3
    4
    <!-- Alt + C -->
    <input @keyup.alt.67="clear">
    <!-- Ctrl + Click -->
    <div @click.ctrl="doSomething">Do something</div>

    .exact 修饰符

    .exact 修饰符应与其他系统修饰符组合使用,以指示处理程序只在精确匹配该按键组合时触发。

    1
    2
    3
    4
    5
    <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
    <button @click.ctrl="onClick">A</button>
    <!-- 只有在 Ctrl 被按下的时候触发 -->
    <button @click.ctrl.exact="onCtrlClick">A</button>

    鼠标按钮修饰符

    1
    2
    3
    4
    5
    6
    .left
    .right
    .middle
    这些修饰符会限制处理程序监听特定的滑鼠按键。

    小QA学习前端系列之vue 列表渲染

    发表于 2017-10-23

    小QA学习前端系列之vue 列表渲染

    用 v-for 把一个数组对应为一组元素

    我们用 v-for 指令根据一组数组的选项列表进行渲染。v-for 指令需要使用 item in items 形式的特殊语法,items 是源数据数组并且 item 是数组元素迭代的别名。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <ul id="example-1">
    <li v-for="item in items">
    {{ item.message }}
    </li>
    </ul>
    var example1 = new Vue({
    el: '#example-1',
    data: {
    items: [
    { message: 'Foo' },
    { message: 'Bar' }
    ]
    }
    })

    在v-for 使用index

    v-for 还支持一个可选的第二个参数为当前项的索引。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <ul id="example-2">
    <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
    </li>
    </ul>
    var example2 = new Vue({
    el: '#example-2',
    data: {
    parentMessage: 'Parent',
    items: [
    { message: 'Foo' },
    { message: 'Bar' }
    ]
    }
    })

    of 替代 in 作为分隔符,因为它是最接近 JavaScript 迭代器的语法

    一个对象的 v-for

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <ul id="v-for-object" class="demo">
    <li v-for="value in object">
    {{ value }}
    </li>
    </ul>
    new Vue({
    el: '#v-for-object',
    data: {
    object: {
    firstName: 'John',
    lastName: 'Doe',
    age: 30
    }
    }
    })

    你也可以提供第二个的参数为键名:

    1
    2
    3
    <div v-for="(value, key) in object">
    {{ key }}: {{ value }}
    </div>

    第三个参数为索引:

    1
    2
    3
    <div v-for="(value, key, index) in object">
    {{ index }}. {{ key }}: {{ value }}
    </div>

    数组更新检测

    变异方法

    Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下:

    push()
    pop()
    shift()
    unshift()
    splice()
    sort()
    reverse()
    

    你打开控制台,然后用前面例子的 items 数组调用变异方法:example1.items.push({ message: ‘Baz’ }) 。

    替换数组

    变异方法 (mutation method),顾名思义,会改变被这些方法调用的原始数组。相比之下,也有非变异 (non-mutating method) 方法,例如:filter(), concat() 和 slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组:

    example1.items = example1.items.filter(function (item) {
    return item.message.match(/Foo/)
    })

    你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

    对象更改检测注意事项

    Vue 不能检测对象属性的添加或删除:
    对于已经创建的实例,Vue 不能动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, key, value) 方法向嵌套对象添加响应式属性。例如,对于:

    1
    2
    3
    4
    5
    6
    7
    var vm = new Vue({
    data: {
    userProfile: {
    name: 'Anika'
    }
    }
    })

    你可以添加一个新的 age 属性到嵌套的 userProfile 对象:

    1
    Vue.set(vm.userProfile, 'age', 27)

    你还可以使用 vm.$set 实例方法,它只是全局 Vue.set 的别名:

    1
    this.$set(this.userProfile, 'age', 27)

    有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign() 或 _.extend()。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样:

    1
    2
    3
    4
    5
    6
    Object.assign(this.userProfile, {
    age: 27,
    favoriteColor: 'Vue Green'
    })
    ``
    你应该这样做:

    this.userProfile = Object.assign({}, this.userProfile, {
    age: 27,
    favoriteColor: ‘Vue Green’
    })

    1
    2
    3
    4
    ## 显示过滤/排序结果
    有时,我们想要显示一个数组的过滤或排序副本,而不实际改变或重置原始数据。在这种情况下,可以创建返回过滤或排序数组的计算属性。
    例如:

  • data: {
    numbers: [ 1, 2, 3, 4, 5 ]
    },
    computed: {
    evenNumbers: function () {
    return this.numbers.filter(function (number) {
    return number % 2 === 0
    })
    }
    }

    1
    在计算属性不适用的情况下 (例如,在嵌套 v-for 循环中) 你可以使用一个 method 方法:

  • data: {
    numbers: [ 1, 2, 3, 4, 5 ]
    },
    methods: {
    even: function (numbers) {
    return numbers.filter(function (number) {
    return number % 2 === 0
    })
    }
    }

    1
    2
    3
    ## 一段取值范围的 v-for
    v-for 也可以取整数。在这种情况下,它将重复多次模板。




    1
    2
    ## v-for on a <template>
    类似于 v-if,你也可以利用带有 v-for 的 <template> 渲染多个元素。比如:




    1
    2
    ## v-for with v-if
    当它们处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。当你想为仅有的一些项渲染节点时,这种优先级的机制会十分有用,如下:




  • 1
    2
    3
    上面的代码只传递了未 complete 的 todos。
    而如果你的目的是有条件地跳过循环的执行,那么可以将 v-if 置于外层元素 (或 <template>)上。如:





    No todos left!


    1
    2
    ## 一个组件的 v-for
    在自定义组件里,你可以像任何普通元素一样用 v-for 。


    1
    2
    3
    2.2.0+ 的版本里,当在组件中使用 v-for 时,key 现在是必须的。
    然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要用 props :

    <my-component
    v-for=”(item, index) in items”
    v-bind:item=”item”
    v-bind:index=”index”
    v-bind:key=”item.id”


    1
    2
    3
    不自动将 item 注入到组件里的原因是,这会使得组件与 v-for 的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。
    下面是一个简单的 todo list 的完整例子:




      <li
      is=”todo-item”
      v-for=”(todo, index) in todos”
      v-bind:key=”todo.id”
      v-bind:title=”todo.title”
      v-on:remove=”todos.splice(index, 1)”

    ></li>
    



    1
    注意这里的 is="todo-item" 属性。这种做法在使用 DOM 模板时是十分必要的,因为在 <ul> 元素内只有 <li> 元素会被看作有效内容。这样做实现的效果与 <todo-item> 相同,但是可以避开一些潜在的浏览器解析错误。查看 DOM 模板解析说明 来了解更多信息。

    Vue.component(‘todo-item’, {
    template: ‘\

  • \
    小QA学习前端系列之vue 列表渲染\
    \
  • \
    ‘,
    props: [‘title’]
    })
    new Vue({
    el: ‘#todo-list-example’,
    data: {
    newTodoText: ‘’,
    todos: [
    {
    id: 1,
    title: ‘Do the dishes’,
    },
    {
    id: 2,
    title: ‘Take out the trash’,
    },
    {
    id: 3,
    title: ‘Mow the lawn’
    }
    ],
    nextTodoId: 4
    },
    methods: {
    addNewTodo: function () {
    this.todos.push({
    id: this.nextTodoId++,
    title: this.newTodoText
    })
    this.newTodoText = ‘’
    }
    }
    })
    ```

    小QA学习前端系列之vue 条件渲染

    发表于 2017-10-22

    小QA学习前端系列之vue 条件渲染

    前段时间我们大概介绍了一下模板语法,今天我们要详细介绍模板语法中比较常用的一些。

    v-if

    v-if 指令条件渲染指令,根据其后表达式的bool值进行判断是否渲染该元素

    1
    <h1 v-if="ok">Yes</h1>

    与v-else 添加一个“else 块”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div id="example01">
    <h1 v-if="a>20">{{a}} </h1>
    <h1 v-else>不显示</h1>
    </div>
    var vm= new Vue({
    el:"#example01",
    data:{
    a:30
    }
    })

    所以,v-if指令只渲染他身后表达式为true的元素

    在

    1
    2
    3
    4
    5
    <template v-if="ok">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
    </template>

    v-else

    v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。

    v-else-if

    v-else-if,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:
    相当于一个for-each

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div v-if="type === 'A'">
    A
    </div>
    <div v-else-if="type === 'B'">
    B
    </div>
    <div v-else-if="type === 'C'">
    C
    </div>
    <div v-else>
    Not A/B/C
    </div>

    用 key 管理可复用的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <div id="key-example">
    <template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username" key="username-input">
    </template>
    <template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address" key="email-input">
    </template>
    <button v-on="toggleLoginType()">
    </div>
    new Vue({
    el: '#key-example',
    data: {
    loginType: 'username'
    },
    methods: {
    toggleLoginType: function () {
    return this.loginType = this.loginType === 'username' ? 'email' : 'username'
    }
    }
    })

    v-show

    v-show 与v-if类似,只是会渲染其身后表达式为false的元素,而且会给这样的元素添加css代码:style=”display:none”;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    1
    2
    3
    4
    5
    6
    7
    <div id="app">
    <h1 v-if="age >= 25">Age: {{ age }}</h1>
    <h1 v-else>Name: {{ name }}</h1>
    <hr>
    <h1 v-show="name.indexOf('cool') = 0">Name: {{ name }}</h1>
    <h1 v-else>Sex: {{ sex }}</h1>
    </div>
    <script>
    var vm = new Vue({
    el: '#app',
    data: {
    age: 21,
    name: 'keepcool',
    sex: 'Male'
    }
    })
    </script>

    v-if vs v-show

    v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

    v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

    相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

    一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

    v-if 与 v-for 一起使用

    当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级。

    12…5
    seven

    seven

    小qa 在thoughtworks苦苦挣扎中

    42 日志
    16 标签
    RSS
    © 2017 seven
    由 Hexo 强力驱动
    主题 - NexT.Pisces