
本文翻自 [週四寫程式系列] – 來做 IOS 動態月曆結合待辦行程吧!直播影片,若是想要老闆手把手帶你飛可以跟著影片做,這邊也附上這次成品歡迎大家一起動腦動手做。
這次老闆要帶大家來做個 ios 動態月曆與待辦清單,本篇會提到行事曆與待辦清單的畫面切版,並利用這個專案來學習使用 vue 的資料與畫面綁定。
礙於直播時間,這次專案僅會實現以下功能:產出假的月份資料來展示、換算農曆、偏移天數、切換不同天觀看該天工作項目、新增工作、刪除工作、工作項目排序以及 vue 的進出場動畫。
完成這次專案後,大家也可以發想還有什麼功能可以加進來,例如月份時間的切換,不同類別的事件項目…等。如果想要了解更詳細的製作流程和其他創作內容,可以去支持老闆的網頁課程哦!
這次直播筆記會帶大家學會以下內容:
- sass – mixin:可以將重複使用的 CSS 做成工具,減少重複 css 樣式的撰寫。
- css – flex 排版
- vue:資料與畫面綁定、生命週期 mounted
製作動態月曆網頁的事前準備
開發環境
開發使用 codepen 線上撰寫程式碼,大家可以先將環境設定成跟老闆一樣,如果想知道較詳細的設定,可以到成品這邊看老闆的設定。
- html: Pug
- css: reset
- js: vue.js
會使用到的 API:
這次專案會使用以下的 api,先重點整理給大家,不清楚的地方,可以透過後面跟著老闆操作,了解每個 api 使用時機。
vue – javascript
- el:資料要綁定的區塊
- data:vue 要綁定的資料放置處
- mounted: vue 的生命週期, el 被掛載之後會執行裡面的程式碼
- methods:使用到的 function 放置處
- computed:計算屬性,會因為 data 內的值改變,而跟著變動
// javascript
var vm = new Vue({
el: '#app',
data: {
text: 'Hello World',
texts: ['H', 'i']
},
mounted () {...},
methods: {
changeText () { ... }
},
computed: {
showText () { return ...}
}
})
vue – html:畫面部分會使用到以下內容
- {{text}}:將資料綁定到畫面中顯示
- v-model:將資料綁定到畫面中顯示或修改
- :class:動態綁定屬性,也等於 v-bind:class=””,簡寫為 :class,也可以綁定其他屬性
- v-for:讓陣列資料重複產生 dom,可以搭配索引值綁定
- :key:可以搭配 v-for 使用,提供 vue 識別每個 dom 是不同的,在傳入 v-for 的陣列中,key 要是獨特的值,避免識別上出錯。
- @click=””:當點擊目標物會觸發傳入 click 的內容
// html
#app
p {{text}}
input (v-model="text)
p(v-model="showText")
div(:class="")
div(v-for="(item, idx) in texts", :key="idx") {{item}}
button(@click="changeText()")
vue – transition-group:
vue 提供給在 dom 要被加入、移除或更新時的動態效果,使用方法會在後面實做中解說。若想要參閱官方說明文件可點此閱讀。
Javascript
- Math.random():會產出一個大於等於 0、小於 1 之間的隨機小數。若想要參閱更詳細的說明文件可點此閱讀。
- sort:對一個陣列的所有元素進行排序,並回傳此陣列,可以自訂規則函式做為參數傳入。若想要參閱說明文件可點此閱讀。
Sass
mixin,可以將重複使用的 CSS 做成工具, 也能傳入參數、參數名稱以 $ 開頭。使用方法如下,後面實作中也會再帶大家操作一次。
//CSS @mixin size($w, $h: $w) width: $w height: $h
css
這次專案會頻繁使用 flex 來排版,以下介紹常見的樣式,實際操作可以透過提供的遊戲及專案製作的過程了解。
- display: flex:預設會將子層水平排列不斷行。
- flex-wrap: nowrap | wrap | wrap-reverse 預設為 nowrap,超過寬度的子元素是否換行。
- flex-direction: row | row-reverse | column | column-reverse 主軸線的方向,預設為橫向 row。
- justify-content: 元素在主軸上排列的方式。
- align-items: 元素在副軸上排列的方式。
下面舉例這段 css 會呈現的畫面,同學也可以嘗試看看換成不同數值,只看文件無法理解的話,可以透過遊戲學習 flex 的系列屬性,網路上有非常多的相關遊戲,這邊提供其中一款遊戲 FLEXBOX FROGGY大家可以挑戰一下!

跟著老闆開始動手做
1. 畫面切版
1-1 mixin
同學們在樣式開發中,肯定遇過重複撰寫相同程式碼的情況,如果有方法可以減少這種情況,除了加速開發外,也能讓程式碼更簡潔。Sass 中有一個工具 mixin,就能滿足我們的需求,不用一直重複造輪子,就可以在需要的位置引入,甚至可以傳入參數,傳入的參數要以 $ 符號為開頭,下面兩個 mixin 分別展示,有沒有傳入參數的差異,並教大家如何引用到 Sass 中。
第一個 mixin 工具為設定長寬,傳入的第一個參數會被 mixin 做為 width 的值,第二個參數則為 height,若是沒有傳入第二個參數,則會將第一個參數做為第二個參數傳入。
第二個 mixin 工具為使用 flex 將內容物垂直置中。
// CSS @mixin size($w, $h: $w) width: $w height: $h @mixin flex_center display: flex align-items: center justify-content: center flex-direction: column .box +size(100px, 100px) // 也可寫為 +size(100px) +flex_center
1-2 vue 資料畫面綁定
雖然是畫面切版階段,但是老闆先帶大家認識 vue 畫面綁定資料的方式,讓大家不用在 html 中,用土法煉鋼的方式將星期分別輸入。
首先我們先創造一個 vue 實體,裡面的 el 值為資料要綁定的區塊,所以將 #app 填入, 這時html 中 id 為 app 的區塊就能進行畫面與資料綁定,data 內的值為資料,可以將要綁定或操作的資料放在此處。
要實現星期的內容能夠與畫面綁定,先在 data 中新增 key 值 tags,內容為每天的字串,在 html 中可以使用 v-for 將陣列的資料綁入,在 html 中可以看到 .tag 的位置後面有使用 v-for,tag 為每一個 tags 資料內的值,雙花括{{tag}}則可以將資料顯示在畫面中。這邊比較有趣的是,因為資料較單純,也可以將 tags 設定為字串去跑 v-for ,結果也會一樣。
// html
#app
.phone
.calender
.head
.tag(v-for="tag in tags") {{tag}}
.body
.daybox.active(v-for="i in 31")
.infos
.num {{i}}
.lunar 初一
.eventdot
// javascript
var vm = new Vue({
el: "#app",
data: {
tags: ["日","一","二","三","四","五","六"]
// tags: "日一二三四五六"
}
})
1-3 畫面樣式切版
開始套資料前,先將畫面準備好,再來做資料與畫面綁定,利用前面的 mixin,將行事曆水平垂直置中在畫面中間,並將 .phone 設定長寬與背景色。.phone 中我們將行事曆分為兩層, .head 顯示日到六的標題, .body 則顯示各天日期、農曆與當天是否有工作項目。
要怎麼做到七個項目就會斷行呢?之前老闆有帶大家使用過 inline-block,應用在這邊會發現到了第六個項目,就會神奇地斷行,這是因為 inline-block 有一些預設樣式,導致父層寬度不夠,就把第七個項目往下推了。所以我們這邊改在父層使用 flex,flex 預設會將子元件併排在同一行,所以要斷行必須要加上 flex-wrap: wrap,當寬度不夠時,子元件就會換行。
至於 .daybox 中有 &.active 則是用來作為被選定時的樣式,該天被選定後,日期與農曆會被變成黑底白字,這邊我們先模擬此樣式,在後面資料綁定後,就能針對特定項目加上樣式。
//CSS
@mixin size($w, $h:$w)
width: $w
height: $h
@mixin flex_center
display: flex
justify-content: center
align-items: center
html, body, #app
+flex_center
+size(100%)
background-color: #333
color: #555
.phone
+size(360px, 560px)
background-color: #fff
.head
display: flex
padding-top: 50px
padding-bottom: 5px
.tag
width: calc(100% / 7)
text-align: center
font-size: 12px
.body
display: flex
flex-wrap: wrap
.daybox
+flex_center
flex-direction: column
width: calc(100% / 7)
text-align: center
padding: 5px 0px
&.active
.infos
background-color: #222
border-radius: 50%
color: #fff
.infos
+size(40px)
.num
font-size: 20px
padding-top: 5px
.lunar
font-size: 12px
.eventdot
+size(6px)
background-color: #ddd
border-radius: 50%
margin-top: 5px

2. 選定日期
有了基本畫面,接著要將畫面綁定資料。前面我們天數是使用 v-for 直接針對數字 1~ 31 跑迴圈,這邊調整成在 mounted 時,創造 31 天的資料後存放到新的資料變數 days 中,代入到畫面。mounted 是 vue 生命週期的其中一個階段,會在元素被掛載且 $el(實體) 建立好的時候執行,可以理解成:當要放置內容的 DOM 產生時,執行 mounted 內的程式碼。
再來,我們要實現只會有一天被選定的功能,也就是只會有一天的 daybox 會有 active 的 class (黑色圓圈背景),使用者的互動會是點選不同天時,當天的樣式就會改變。
先改寫 v-for ,在前面的 day 改為 (day, day_idx),這個值就是 day 在 days 中的索引值。並在 data 新增一個 selected 值,記錄目前被觸發的為何者,接著使用 vue 中的語法 @click,當該 daybox 被按下時,將目前被選定的值改為被選定的 day_idx。結合前面學到的動態綁定 class,來將 active 的 class 賦予給 daybox ,也為 daybox 加上手指游標的樣式。就可以達成「當 selected 的值等於該天的 day_idx 時,就會為其加上 active 的 class」。
// HTML
...
.daybox(v-for="(day, day_idx) in days", @click="selected = day_idx", :class="day_idx === selected ? 'active' : ''")
.infos
.num {{day.number}}
.lunar 初一
.eventdot
// CSS .daybox ... cursor: pointer
// javascript
...
data: {
tags: ["日","一","二","三","四","五","六"],
days: [],
selected: 0 // 目前被選定的值
},
mounted() {
for(var day=1; day<=31; day++){
var new_day = {
number: day
}
this.days.push(new_day);
}
}
...

3. 農曆日期換算與偏移天數
接下來的動作,會使用到 vue 的 methods,可以將 methods 的功用理解成,將需要使用的 function 統一管理,需要使用的時候就能直接呼叫來觸發這些 function。
在農曆的日期換算的部分,雖然我們的月曆是假資料,為了仿真,讓這份月曆第一天的農曆,不是從初一開始,我們在 mounted 的一開始,新增一個 lunar 變數,做為農曆的偏移天數。每次 for 迴圈結束前,將 lunar 變數加一,使農曆一直往前推移,農曆數字轉化為國字的規則如下:
- 如果傳入的 lunar 參數超過 30,用餘數換算成 0~29,確保農曆永遠從初一到三十中循環
- lunar 小於等於 10,用”初”為開頭,並從國字字串取出特定位置的值組裝在一起
- lunar 小於 20 時,用”十”為開頭,並從國字字串取出特定位置的值組裝在一起
- lunar 等於 20,直接顯示二十
- lunar 小於 30 時,用”廿”為開頭,並從國字字串取出特定位置的值組裝在一起
- lunar 等於 30,直接顯示三十
我們也製造一個偏移天數 start_day 變數,讓這份月曆的 1 號,不一定從周日開始排序。利用 get_pen(d) 傳入一個偏移天數,讓第一天的 DOM 產生一個 margin-left 的 style。
// HTML
...
.daybox( v-for="(day, day_idx) in days", @click="selected = day_idx", :class="day_idx === selected ? 'active' : ''", :style="get_pan(day_idx)")
.infos
.num {{day.number}}
.lunar {{lunar(day.lunar)}} //農曆換算
.eventdot
// javascript
var vm = new Vue({
data:{
...
start_day: 2
},
mounted () {
var lunar = 6; // 農曆偏移天數
for(var day=1; day<=31; day++){
var new_day = {
number: day,
lunar: lunar, // 傳入的每天資料新增一個紀錄農曆的數字
events: [],
}
this.days.push(new_day);
lunar++ // 下一天能拿到新的農曆值
}
},
methods: {
chinese_num(num) {
var list = "十一二三四五六七八九";
return list[num];
},
lunar(num) { // 換算農曆
if (num > 30) num = num % 30;
if (num <= 10) {
return "初" + this.chinese_num(num % 10);
} else if (num < 20) {
return "十" + this.chinese_num(num % 10);
} else if (num == 20) {
return "二十";
} else if (num < 30) {
return "廿" + this.chinese_num(num % 10);
} else if (num == 30) {
return "三十";
}
},
get_pan(id){ // 第一天的偏移位置
if (id==0){
return { "margin-left": "calc( "+this.start_day+" * 100% / 7)"};
}
},
},
...
})

4. 產生每天工作項目
工作內容的資料會頻繁使用 js 原生語法 – Math.random() 來製作。Math.random() 在沒有輸入參數時,會隨機產生 0~1之間的小數。利用這點,可以拿來製作當天是否有工作、工作量、工作項目、時間、工作類型。
- 是否有工作:新增每天資料時,利用
random產出的數字,達成機率性決定當天有沒有工作。若設定小於 0.4 ,則代表有 4/10 的機率當天會有工作,繼續跑下面產出工作內容的程式碼,否則events就是空陣列。目前階段有沒有工作只會決定該天有沒有小灰點,後面會介紹每天的工作清單如何製作。 - 工作量:
Math.ranodm() * 3,會產出 0~3間的隨機小數,將產出的數值代入以下的for迴圈,就會隨機產出 0~3 項的工作。 - 工作項目、工作類型:將所有工作項目的陣列代入,並使用前面的產出的單日工作量的整數值作為
id,去取得工作項目陣列中對應的工作名稱。這邊要注意,random值除了要轉為整數外,也要等於工作項目陣列的長度,避免程式碼取到undefined的值。 - 時間:使用
Math.random()產出時間字串。時間顯示的格式,老闆設定如下: 小時的區塊為 0~24 小時,中間組合 : 符號,分鐘的部分避免程式碼太過複雜,在前面先創造分鐘變數,利用parseInt讓分鐘的值只會有四種 0, 15, 30, 45。要注意的是,如果只有 0,我們希望呈現的會是兩個 0 ,所以在這邊用三元運算子判斷,如果minute的值為 0,則在前面多補一個 0。
此時,31 天的資料順利產生,利用 events 是否有內容,判斷要不要呈現小灰點,畫面上也能使用資料來動態綁定 class,當該天存在工作項目時,則為 eventdot 加上 has_event 的 class。再開發時,同學也可以使用 codepen 中的 console 工具,檢查該天是不是有工作內容,方法為 vm.days[日期].events
// HTML
...
.body
.daybox( v-for="(day, day_idx) in days", @click="selected = day_idx", :class="day_idx === selected ? 'active' : ''", :style="get_pan(day_idx)")
.infos
.num {{day.number}}
.lunar {{lunar(day.lunar)}} // 農曆換算
.eventdot(:class="{'has_event': day.events.length > 0}") // 當天是否有工作
// CSS
...
.eventdot
+size(6px)
background-color: #ddd
border-radius: 50%
margin-top: 5px
opacity: 0 // 當天沒工作的樣式
&.has_event
opacity: 1 // 當天有工作的樣式
// javascript
...
mounted() {
var lunar = 6;
for (var day = 1; day <= 31; day++) {
var new_day = {
number: day,
lunar: lunar,
events: []
};
if (Math.random() < 0.4) { // 機率性決定當天有沒有工作
var count = parseInt(Math.random() * 3); // 新增0~3項工作
for (var o = 0; o < count; o++) {
var minute = parseInt(Math.random() * 4) * 15; // 產出 0, 15, 30, 45
new_day.events.push({
title: ["整理房間丟垃圾", "出門參加活動", "打包行李"][parseInt(Math.random() * 3)], // 從工作陣列中,隨機取一個值
time: parseInt(Math.random() * 24) + ":" + (minute == 0 ? "0" : "") + minute // 如果分鐘數為 0,則補一個 0 為開頭
});
}
}
this.days.push(new_day);
lunar++;
}
},
5. 待辦事項與項目排序
完成日曆的顯示後,要開始製作當天的工作項目清單 todo_list 。我們會使用 vue 的另外一個功能 computed,computed 無法傳入參數,會因為 data 內資料變動,動態改變回傳的結果,也就是將資料代入後重新計算的屬性。當使用者選擇不同天,當天如果有工作內容,就會顯示該天的工作項目。
使用 data 內的 days 陣列與 selected ,能夠取得目前選擇的當天資料,這邊要注意的是,因為一開始 days 長度是 0,取當天資料會失敗,所以要多使用 if 判斷式,在資料還沒創造完畢前,避免回傳的值出錯。
取得當天資料後,我們想把工作項目依照時間排序,因為創造工作項目時,時間是隨機產生,所以畫面工作項目順序是隨機排列的,利用 js 的原生語法 sort 來進行時間排序。sort 內可以傳入一個函式做為規則,我們傳入一個 function 作為比較的規則,將傳入的兩個值進行比較,將不必要的分號取代成空值後,時間會變成四位數的字串來做比較。
todo 的畫面樣式如下,因為篇幅原因,先不針對不同工作項目給予不同樣式,如果同學想嘗試,做法與前面相同,只要對每一筆工作多設定一個值來記錄工作類別,針對不同的工作類別,給予不同的樣式即可。如果在製作上有問題,可以參考文章中附上老闆的成品,了解詳細的做法。
// HTML
#app
.phone
.calender
.head ...
.body ...
.todos
.item(v-for="(todo,id) in current_items", :key="todo")
.time {{todo.time}}
.title {{todo.title}}
// CSS
.head,.body
border-bottom: solid 1px rgba(black,0.1)
background-color: #f7f7f7
.body
padding-bottom: 10px
...
.todos
.item
padding: 3px 10px
display: flex
height: 40px
border-bottom: solid 1px rgba(0,0,0,0.1)
.time, .title
padding: 4px 10px
.time
width: 55px
border-right: solid 2px
border-color: #3ca9f2
// javascript
...
computed: {
current_day(){
return this.days[this.selected];
},
current_items(){
var day=this.current_day;
if(!day)
return null;
else
return day.events
.sort((a,b)=>(parseInt(a.time.replace(":",""))-parseInt(b.time.replace(":","")) ));
}
}

6. todo 新增與移除工作
除了顯示隨機產生的工作項目外,我們希望也能在每天的工作項目中產生新工作,或是刪除不需要的工作。
新增資料的部分,先在畫面產出兩個 input 區塊,來輸入工作名及時間,並新增一個 button 按鈕,來做最後的新增按鈕。在成品中,老闆還有放入工作類型的選擇,大家也可以挑戰看看。
新增資料需要注意的部分是,由於新增的資料是傳物件參考進去,所以要新增新的資料時,已經加進去的也會被移動,所以我們將物件字串化之後,需要再把它轉回物件,確保傳入的資料是一組全新的。
刪除資料部分,我們只要在 .item 中新增一個 div,當點擊它時,會從目前的 now_events,使用 splice 刪除被點選的該筆資料。
// HTML
#app
...
.phone
.calender
ul.todos
.item(v-for="(todo,id) in now_events", :key="todo")
.time {{todo.time}}
.title {{todo.title}}
.close_btn(@click="now_events.splice(id,1)") x // 刪除工作項目按鈕
.form
input(name="title" v-model="newtodo.title" placeholder="標題")
input(name="time" v-model="newtodo.time" placeholder="時間")
button(type="submit" @click="add_item") +
// CSS
.form
box-sizing: border-box
padding: 10px
position: absolute
bottom: 0px
width: 100%
left: 0
+flex_center
flex-direction: row
input,select
box-sizing: border-box
margin-right: 10px
padding: 5px 10px
border-radius: 2px
min-width: 150px
height: 30px
color: white
background-color: transparent
border: none
border: solid 1px white
input[name='title']
width: 300px
// javascript
...
data: {
...
newtodo: {
title: '',
time: ''
}
},
methods: {
...
add_item(){
this.days[this.selected].events.push(JSON.parse(JSON.stringify(this.newtodo)));
}
},
computed: {
now_events(){
var day = this.days[this.selected_day];
if (day)
return day.events;
else
return [];
console.log();
},
current_day)_{
...
}
}

7. 新增刪除工作加上動態
最後一步,要在執行新增或刪除時能加上動態,會使用到 vue 的 transition-group,使用方法如下:
將 ul.todos 改寫成 transition-group.todos,屬性中 tag=”ul” ,會將這層 DOM 換成 ul 使用。name 為要賦予的動態名稱,mode 為動畫進出方式,這邊使用的是舊的先離開,新的再進來。使用上需要注意,因為 transition-group 跑動態是群組進來群組出去,所以每個子元件都要獨一無二 key,需要在子層加上 key,讓 vue 辨識出每個 DOM 都是獨特的,避免產生奇怪的動畫。
接著來解說 name 裡面填寫的動態要怎麼使用,只是寫上 name 並不會有動態,我們要依照 vue 的規格,在 sass 中加上動畫的名稱,分別會使用到:
fade-enter-active,fade-leave-active:準備要進入的時候,準備要離開的時候fade-enter,fade-leave-to:進來之前和離開之後的狀態
// HTML
...
transition-group.todos(tag="ul", name="fade", mode="out-in")
.item(v-for="(todo,id) in now_events", :key="todo")
.time {{todo.time}}
.title {{todo.title}}
.close_btn(@click="now_events.splice(id,1)") x
// CSS .fade-enter-active, .fade-leave-active transition: 0.5s .fade-enter, .fade-leave-to opacity: 0

老闆來結語
讓我們快速回顧一下製作動態日曆的流程:
- 行事曆的靜態資料切版、使用 vue 將星期名稱綁定到畫面中的方式
- 選定特定日期後,用 vue 動態改變屬性
- 農曆日期換算與偏移、天數的偏移:達成該份資料的第一天不是初一,也不是當月1號就從周日開始排序
- 使用
Math.random()隨機產出每天的工作項目 - 待辦事項切版與項目依照時間排序
- 新增待辦事項與移除待辦事項
- 使用 vue – transition-group 新增與移除動作的動態
萬事起頭難,一個作品不可能一步到位,將最終目標拆分成不同階段任務,從一開始的雛型慢慢開發出每個區塊,最後組裝在一起,也可以加上個人的創意去實現其他功能,讓作品更豐富。
礙於直播時間,老闆沒有將所有功能都實現,但製作方式與前面提到的內容雷同,大家可以挑戰看看,例如工作項目多一個類別屬性,在不同類別時顯示不同的樣式,也可以發想其他功能沒提到的功能,例如月份時間用真實的時間去換算、切換年月份功能等…。
再附上這次範例的成品,讓大家在開發時參考。
如果你喜歡老闆的教學,歡迎加入老闆開的課程中一起學習,順便支持一下老闆,課程會帶你看看不一樣的作品,並引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓寫 code 不再是這麼困難的一件事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠在網頁上做出與眾不同的創作。
動畫互動網頁程式入門(HTML/CSS/JS)以簡單例子帶你入門網站的基礎架構及開發,用素材刻出簡單有趣又美觀的網頁和動畫,享受做出獨一無二的網頁所帶來的成就感,在職場上與設計師和工程師合作無間。
打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。長達3085分鐘,超過60個精緻範例與400張的投影片以上,以及四個加碼單元vue-cli、GSAP、D3、Three.js的投影片,成為hahow上最長的課程。
此篇直播筆記由幫手 H 協助整理




