本文翻自 [週四寫程式系列] – 來做 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 協助整理