最近看了 Vue 的文檔,想着應該寫點什麼加深下印象,能力有限,就照着葫蘆畫下吧😂,此次的葫蘆是 圖靈社區 移動端頁面
Github: github.com/Jimzjy/itur…
預覽: jimzjy.github.io/ituring-mob…
前端新手,CSS / TS / JS 寫的很爛,望見諒前端
使用 vue-cli 建立項目,個人配置是vue
vue-router
vuex
dart-sass
babel
typescript
eslint
複製代碼
刪除自動建立的 HelloWorld,About 等組件、頁面以及路由git
咱們看下要作的部分的大致導航結構github
歸納爲(上面的圖少 more-books 的頁面)vue-router
- pages
- [pages-content]
- header-bar
- home / book / article
- [user-info]
- bottom-navbar
- login
- more-books
複製代碼
新建如下文件vuex
views/
Pages.vue
PgaesContent.vue
Login.vue
MoreBooks.vue
NotFound.vue
pagesContents/
Home.vue
Book.vue
Article.vue
User.vue // 對應上文結構中的 user-info, 按照結構其實應該直接放在 views/ 目錄下
components/
HeaderMenu.vue
HeaderNav.vue // 兩個 Header 組件一塊兒對應 header-bar
BottomNavbar.vue
複製代碼
添加 routevue-cli
routePageNames = ['home', 'book', 'article', 'user']
{
path: '/',
component: Pages,
children: [
{
path: '',
component: PagesContent,
children: [
{
path: '',
component: Home,
name: routePageNames[0]
},
{
path: 'book',
component: Book,
name: routePageNames[1],
},
{
path: 'article',
component: Article,
name: routePageNames[2],
}
]
},
{
path: 'user',
component: User,
name: routePageNames[3],
}
]
},
{
path: '/login',
component: Login,
name: 'login',
},
{
path: '/more-books',
component: MoreBooks,
name: 'more-books',
}
{
path: '*',
component: NotFound,
name: 'not-found'
}
複製代碼
OK,如今文件結構已經知足了上面結構,接下來開始填充結構內容typescript
用 MockJS 來模擬數據npm
npm install mockjs
npm install @types/mockjs -D
複製代碼
新建文件api
src/
mock/
index.ts // mock 數據
service/
index.ts // 獲取數據的 Url
複製代碼
頭部由兩個組件組成 HeaderMenu 和 HeaderNav, 代碼能夠在最上面的連接中找到
HeaderMenu
service/index.ts
const topicsUrl = api + '/topics'
mock/index.ts
Mock.mock(`${topicsUrl}/${routePageNames[0]}`, shuffle(topicsHome).concat('最新上線', '每週特價'))
Mock.mock(`${topicsUrl}/${routePageNames[1]}`, topicsBook)
Mock.mock(`${topicsUrl}/${routePageNames[2]}`, topicsArticle)
// 內容均可以在源碼中找到,篇幅有限就只列重點了,下同
HeaderNav.vue
@Watch('$route')
updateTopics () {
this.isHome = this.$route.name === routePageNames[0] // 經過 isHome 來判斷是不是 Home,routePageNames 是以前在 route 裏寫的
this.$http.get(`${topicsUrl}/${this.$route.name}`).then((resposne: any) => {
this.topicsData = resposne.data
})
}
<div v-if="isHome">...</div>
<div v-if="!isHome">...</div>
複製代碼
在 PagesContent.vue 添加 HeaderNav 和 HeaderMenu
<header>
<header-menu></header-menu>
<header-nav></header-nav>
</header>
<router-view/> // Home / Book / Article
<div id="bottom-space"></div> // 爲 BottomNavbar 留底部空間
複製代碼
BottomNavbar 用到了 vue-awesome 這個 icon 庫
npm install vue-awesome
複製代碼
在這個項目中我用的都是自定義icon,能夠在 src/asstes/icon/customIcons.ts 中找到
經過 fill 和 .router-link-active 一塊兒配合,能夠很輕鬆的實現根據不一樣路由改變 icon 的顏色
.fa-icon {
...
fill: #C8CDD4;
}
.navbar-tab {
...
&.router-link-active {
color: $primary-color;
.fa-icon {
fill: $primary-color;
}
}
}
複製代碼
在 Pages.vue 中添加 BottomNavbar
<router-view/> // PagesContent
<bottom-navbar></bottom-navbar>
複製代碼
登陸狀態能夠用 Vuex 來設置一個 loginStatus(true爲登陸)
state: {
loginStatus: false
},
mutations: {
login (state) {
state.loginStatus = true
},
logout (state) {
state.loginStatus = false
}
},
複製代碼
當用戶登陸狀態爲 false 時,不容許用戶進入 /user ,因此咱們在 /user 上添加一個路由守衛
{
path: 'user',
component: User,
name: routePageNames[3],
+ beforeEnter: authGuard
}
function authGuard (to: Route, from: Route, next: Function) {
if ($store.state.loginStatus) {
next()
return
}
next({ name: 'login', query: { to: to.name } })
}
複製代碼
在上面的代碼中給到 login 的路由加了 query: { to: to.name } } ,以後咱們在 Login 組件中就能夠獲取到原本用戶想要去的路由,在登陸後就能夠跳轉到用戶原來想要取得路由,咱們能夠經過 $route.query 獲取到信息,或者可使用
route.ts
{
path: '/login',
component: Login,
name: 'login',
+ props: (route) => ({ to: route.query.to })
}
Login.vue
@Prop({ default: 'home' }) readonly to!: string
複製代碼
在路由中添加 props 將 query.to 變成添加給 Login 的屬性
在 Login 中添加 login 方法,在 User 中添加 logout 方法
Login.vue
onLoginClick () {
this.$store.commit('login')
this.$router.push({ name: this.to })
}
User.vue
onLogoutClick () {
this.$store.commit('logout')
this.$router.push('/')
}
複製代碼
開始填充 PagesContent 內部,MoreBooks 中的組件會複用內部用過的,因此會和內部一塊兒說明
Home 中的第一個組件是一個 Swiper,使用 vue-swipe
npm install vue-swipe
複製代碼
再新建 components/Swiper.vue 再封裝下 vue-swipe
Swiper.vue
<swipe :showIndicators=false :speed=3000>
<swipe-item v-for="(item, index) in data" :key="index" class="swipe-item">
<p class="title">{{ item.title }}</p>
<p class="content">{{ item.content }}</p>
</swipe-item>
</swipe>
@Prop() readonly data!: Array<InfoSwipe>
複製代碼
在 Home 中經過 Mock 獲取數據,傳給 Swiper
created () {
this.updateData()
}
updateData () {
this.$http.get(homeDataUrl).then((resposne: any) => {
this.data = resposne.data
})
}
<swiper :data="data.infoSwipe"></swiper>
複製代碼
在 Home 中有兩種組件有着一樣的外組件
書的列表
SpecialView.vue
<div class="title">
<div class="title-content">
...
</div>
<router-link class="more" v-if="more" :to="{ name: 'more-books', query: { title } }">更多</router-link>
</div>
<slot></slot>
Home.vue
<special-view :title="item.title" :more="item.books.length > 4">
...
</special-view>
複製代碼
這裏我偷了下懶,更多按鈕只去 /more-books
書的列表也有兩種樣子
橫向滾動
經過 wrap 屬性改變 class 來改變樣式
components/BookListView.vue
<div :class="['book-list-view', wrap ? 'wrap-list' : '']">
...
</div>
@Prop() readonly books!: Array<Book>
@Prop({ default: false }) readonly wrap!: boolean
Home.vue
<div v-for="(item, index) in data.booksWithTitle" :key="index" >
<special-view :title="item.title" :more="item.books.length > 4">
<book-list-view :books="item.books"></book-list-view>
</special-view>
<sepline></sepline>
</div>
MoreBooks.vue
<book-list-view :books="books" :wrap=true></book-list-view>
複製代碼
在 Book 和 Article 中都有一個 tabview
components/TopicTabView.vue
@Emit()
onTopicClick (n: number) {
this.topicChecked = n
return n
}
Book.vue
<topic-tab-view @on-topic-click="onTopicChange"></topic-tab-view>
onTopicChange (n: number) {
this.updateData()
}
updateData () {
this.$http.get(moreBooksUrl).then((response: any) => {
this.books = response.data
})
}
複製代碼
Book 的內容能夠直接複用 BookListView 的 wrap 模式
Article 和 Home 中都有文章的列表,樣式相差並很少,能夠像以前同樣經過屬性改變 class
components/ArticleListView.vue
<div v-if="showTag">
...
</div>
<div v-if="!showTag">
...
</div>
Article.vue
<article-list-view :articles="articles" class="articles"></article-list-view>
Home.vue
<special-view title="推薦文章">
<article-list-view :articles="data.articles" :showTag=false></article-list-view>
</special-view>
複製代碼
如今從樣子的角度來講已經構建完了,可是還有一個問題,當在 Book 和 Article 中點擊 HeaderNav 的選項不會有任何效果,由於 HeaderNav 和 Book 以及 Article 的 內容部分,既不是父子也不是兄弟,咱們又不能直接傳參或者監聽了
那咱們能夠經過在 route 上加屬性,HeaderNav 經過 route 上的屬性切換,點擊後改變 route 的屬性刷新頁面,嗯...,感受消耗有點大
那咱們還能夠經過 Vuex
state: {
+ currentHeaderNav: 0
},
mutations: {
+ changeCurrentHeaderNav (state, n) {
state.currentHeaderNav = n
}
},
複製代碼
咱們添加了一個 currentHeaderNav 用於表示如今的 HeaderNav 序號
<div :class="[isCurrentNav(index) ? 'topic-content-foucused' : 'topic-content']" @click="onTopicClick(index)">{{ topic }}</div>
isCurrentNav (n: number): boolean {
return this.currentHeaderNav === n
}
onTopicClick (n: number) {
this.$store.commit('changeCurrentHeaderNav', n)
}
複製代碼
在 Book 和 Article 中添加監聽
created () {
this.updateData()
this.subscription = this.$store.subscribe(mutation => {
if (mutation.type === 'changeCurrentHeaderNav') {
this.updateData()
}
})
}
destroyed () {
this.subscription()
}
複製代碼
如今已經能夠正常更新內容了,可是還有一個問題,在 Book 和 Article 中序號是共享的,因此咱們要在進入 Book 和 Article 前初始化序號,否則的話序號就亂套了,添加路由守衛
{
path: 'book',
component: Book,
name: routePageNames[1],
+ beforeEnter: refreshHeaderNav
},
{
path: 'article',
component: Article,
name: routePageNames[2],
+ beforeEnter: refreshHeaderNav
}
function refreshHeaderNav (to: Route, from: Route, next: Function) {
$store.commit('changeCurrentHeaderNav', 0)
next()
}
複製代碼
OK!如今就完成了