使用 Vue.js 2 创建 Time Tracking 单页应用

Single-Page Applications (SPAs) are web apps that load a single HTML page and dynamically update that page as the user interacts with the app. SPAs use AJAX and HTML5 to create fluid and responsive Web apps, without constant page reloads.

As stated in the above description taken from Wikipedia, the main advantage of SPAs is that the app can respond to user interactions without fully reloading the page, resulting in a much more fluid user experience.

All the code for this post can be found on Github.

Creating the Home Component

Let's kick things off here by applying a navigation bar to the app. But first, we'll need to add Bootstrap for some styling. A quick way to do this is to grab the CSS from Bootstrap's CDN.

<head>
    <meta charset="utf-8">
    <title>vue-time-tracker</title>
    <link href="https://cdn.bootcss.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
</head>

The best place for the navbar is our App.vue file, so let's edit it to have a new template.

<template>
    <div id="wrapper">
        <nav class="navbar navbar-default">
            <div class="container">
                <a class="navbar-brand">
                    <i class="glyphicon glyphicon-time"></i>
                    Vue Time Tracker
                </a>
                <ul class="nav navbar-nav">
                    <router-link tag="li" to="/home">
                        <a>Home</a>
                    </router-link>
    
                    <router-link tag="li" to="/time-entries">
                        <a>Time Entries</a>
                    </router-link>
                </ul>
            </div>
        </nav>
    
        <div class="container">
            <div class="col-sm-3">
                <sidebar :time="totalTime"></sidebar>
            </div>
            <div class="col-sm-9">
                <router-view></router-view>
            </div>
        </div>
    
    </div>
</template>

There's a smaller div that will be used for our sidebar, and a larger one that will show other components through the router-view tag, vue-router does this as well through router-view.

The Home component for our app really just needs to show a simple message. Let's create a Home.vue and give it a template with that message.

<template>
    <div class="jumbotron">
        <h1>Vue Time Tracker</h1>
        <p>
            <strong>
                Get started by
                <router-link to='/time-entries'>creating a time entry</router-link>.
            </strong>
        </p>
    </div>
</template>

<script>
export default {}
</script>
Creating the Time Entries Component

We'll make a component that lists out existing time entries, called TimeEntries.vue, and then another one for entering new time entries, called LogTime.vue. We'll add a link from TimeEntries to LogTime so the user can jump to the spot for adding new time entries quickly.

<template>
    <div>
        <router-link to='/time-entries/log-time' v-if="$route.path !== '/time-entries/log-time'">
            <button class="btn btn-primary">
                Log Time
            </button>
        </router-link>
        <div v-if="$route.path === '/time-entries/log-time'">
            <h3>Log Time</h3>
        </div>
    
        <hr>
    
        <router-view></router-view>
    
        <div class="time-entries">
            <p v-if="!timeEntries.length">
                <strong>No time entries yet</strong>
            </p>
    
            <div class="list-group">
    
                <a class="list-group-item" v-for="timeEntry in timeEntries">
                    <div class="row">
    
                        <div class="col-sm-2 user-details">
                            ![](timeEntry.user.image)
                            <p class="text-center">
                                <strong>
                                    {{ timeEntry.user.firstName }} {{ timeEntry.user.lastName }}
                                </strong>
                            </p>
                        </div>
    
                        <div class="col-sm-2 text-center time-block">
                            <h3 class="list-group-item-text total-time">
                                <i class="glyphicon glyphicon-time"></i>
                                {{ timeEntry.totalTime }}
                            </h3>
                            <p class="label label-primary text-center">
                                <i class="glyphicon glyphicon-calendar"></i>
                                {{ timeEntry.date }}
                            </p>
                        </div>
    
                        <div class="col-sm-7 comment-section">
                            <p>{{ timeEntry.comment }}</p>
                        </div>
    
                        <div class="col-sm-1">
                            <button class="btn btn-xs btn-danger delete-button" @click="deleteTimeEntry(timeEntry)">
                                X
                            </button>
                        </div>
    
                    </div>
                </a>
    
            </div>
        </div>
    </div>
</template>
<script>
import store from '../store'

export default {
    data() {
        return {
            timeEntries: store.fetch()
        }
    },
    watch: {
        timeEntries(val, oldVal){
            store.save(val)
        }
    },
    methods: {
        deleteTimeEntry(timeEntry) {
            let index = this.timeEntries.indexOf(timeEntry)
            if (window.confirm('Are you sure you want to delete this time entry?')) {
                this.timeEntries.splice(index, 1)
                this.$router.app.$emit('deleteTime', timeEntry)
            }
        }
    },
    created() {
        this.$router.app.$on('timeUpdate', timeEntry => {
            this.timeEntries.push(timeEntry)
        })
    }
}
</script>
<style>
.avatar {
    height: 75px;
    margin: 0 auto;
    margin-top: 10px;
    margin-bottom: 10px;
}

.user-details {
    background-color: #f5f5f5;
    border-right: 1px solid #ddd;
    margin: -10px 0;
}

.time-block {
    padding: 10px;
}

.comment-section {
    padding: 20px;
}
</style>

We see a router-view below the hr tag, and this is because we'll be registering a sub-route for logging time entries. Essentially we're nesting one route within another, so the LogTime component will be two levels deep. This is the cool thing about routing--we can just keep putting router-view elements in our templates, and as long as we register a component for them, they will keep nesting further down.

Creating the store.js

we're just working with local data by localStorage.

/* 使用 localStorage 临时保存数据 */
const STORAGE_KEY = 'vue-time-tracker'

export default {
    fetch() {
        return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
    },
    save(items) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(items))
    },
    totalTimes() {
        var total = 0
        this.fetch().forEach(function (item) {
            total += parseInt(item.totalTime)
        });
        return total
    }
}
Creating the Log Time Component

We need a component that provides a screen for the user to log time entries.

<template>
    <div class="form-horizontal">
        <div class="form-group">
            <div class="col-sm-6">
                <label>Date</label>
                <input type="date" class="form-control" v-model="timeEntry.date" placeholder="Date" />
            </div>
            <div class="col-sm-6">
                <label>Hours</label>
                <input type="number" class="form-control" v-model="timeEntry.totalTime" placeholder="Hours" />
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-12">
                <label>Comment</label>
                <input type="text" class="form-control" v-model="timeEntry.comment" placeholder="Comment" />
            </div>
        </div>
        <button class="btn btn-primary" @click="save">Save</button>
        <router-link to="/time-entries">
            <button class="btn btn-danger">Cancel</button>
        </router-link>
        <hr>
    </div>
</template>

<script>
export default {
    data() {
        return {
            timeEntry: {}
        }
    },
    methods: {
        save() {
            let user = {
                firstName: 'Zhang',
                lastName: 'Yongjie',
                email: '1091354206@qq.com',
                image: 'http://s.gravatar.com/avatar/c93ccb8362ae464812afc1927c60a90e?s=250'
            }
            this.timeEntry['user'] = user
            this.$router.app.$emit('timeUpdate', this.timeEntry)
            this.timeEntry = {}
        }
    }
}
</script>

We're initializing the timeEntry model with some data for the user so that we can have a name and profile photo.

When our app grows and needs to communicate a lot of data between many components. We'll see how to fix this by using Vuex for state management.

Creating the router.js

We need to add the LogTime component as a sub-route of TimeEntries in our router configuration. By doing this, the router will know that LogTime is a child of TimeEntries, and the appropriate URI structure will be generated when we navigate to it.

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from './components/Home'
import TimeEntries from './components/TimeEntries'
import LogTime from './components/LogTime'

Vue.use(VueRouter)

const routes = [{
        path: '/home',
        component: Home
    },
    {
        path: '/time-entries',
        component: TimeEntries,
        children: [{
            path: 'log-time',
            component: LogTime
        }]
    },
    {
        path: '*',
        redirect: '/home'
    }
]

export default new VueRouter({
    mode: 'history',
    routes
})
Creating the Sidebar Component

Sidebar component will hold the total number of hours for all of our time entries.

<template>
    <div class="panel panel-default">
        <div class="panel-heading">
            <h1 class="text-center">Total Time</h1>
        </div>
    
        <div class="panel-body">
            <h1 class="text-center">{{ time }} hours</h1>
        </div>
    
    </div>
</template>

<script>
export default {
    props: ['time']
}
</script>

The props array is where we can specify any properties that we want to use which are passed into the component, and here we are getting the time prop which is placed on the sidebar element in App.vue.

Screenshot
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 第一印象 今天是我来到上嵌学习的第一天,也是我第一次来到上海移动互联网创新园,一进入大厅人们步伐轻快地走向电梯...
    Tangjiayue92阅读 103评论 0 0
  • 文/井溢 从早忙到晚,却都不是自己喜欢做的事情。真所谓,只有把该做的做完哪怕是不喜欢的事情,才能有时间有机...
    井溢阅读 192评论 0 2
  • 导语:创业要发展就需要有独特的竞争优势。但大多数情况下,创业者都没有太多的资金和资源,那又该如何办呢?Steven...
    StevenPPT阅读 858评论 1 8
  • test
    linxinthu阅读 172评论 0 0