# 小專案二: Movie Seat Booking | 電影訂票系統 ###### tags: `JS小專案` ## 成品示意圖 ![](https://i.imgur.com/GVA8kvb.png) ## HTML的結構 ### 基礎設定 首先是基礎的設定,將**網頁標題**、**CSS**與**JS**的連結都設定好 ```htmlembedded= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 開頭設定CSS連結路徑 --> <link rel="stylesheet" href="style.css"> <!-- 設定網頁標題 --> <title>Movie Seat Booking</title> </head> <body> ``` ### 電影選單 接著咱們一動一動來,首先創建一個div,其class就取名為"movie-container" 使用**label**與**select**的標籤來創建下拉式列表 ```htmlembedded= <!-- 設計一個container,主要是針對最上方的選單列表 --> <div class="movie-container"> <label>Pick a movie:</label> <select id="movie"> <!-- 設計四個選項,主要是撰寫票價選擇 --> <option value="10">Avengers: Endgame ($10)</option> <option value="12">Joker ($12)</option> <option value="8">Toy Story 4 ($8)</option> <option value="9">The Lion King ($9)</option> </select> </div> ``` ### 座位範例 創建一個**unorder list**,列表(**li**)包含座位的三種狀態與說明文字 ```htmlembedded= <!-- 展示座位的三種型態 --> <ul class="showcase"> <li> <div class="seat"></div> <small>N/A</small> </li> <li> <div class="seat selected"></div> <small>Selected</small> </li> <li> <div class="seat occupied"></div> <small>Occupied</small> </li> </ul> ``` ### 螢幕與座位排列 建立一個div命名為container,其中包含螢幕(screen)與座位(row) 座位為一列8個,共計6排,針對幾個座位我們隨機命名為佔位的狀態(seat occupied) ```htmlembedded= <!-- 把螢幕與座位規劃在一區 --> <div class="container"> <div class="screen"></div> <!-- 一排座位8個,共計規劃六排 --> <div class="row"> <!-- 預計先有幾個座位被占 --> <div class="seat occupied"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat occupied"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> </div> <div class="row"> <div class="seat"></div> <div class="seat"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat"></div> </div> <div class="row"> <div class="seat"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> </div> <div class="row"> <div class="seat occupied"></div> <div class="seat occupied"></div> <div class="seat occupied"></div> <div class="seat occupied"></div> <div class="seat occupied"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> </div> <div class="row"> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> </div> <div class="row"> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> <div class="seat"></div> </div> </div> ``` ### 票價計算 我們透過使用標籤**p**來建立文字,為了能讓特定的數字方便後面程式碼來計算,使用**span**座位數與票價給框起來 喔對了,JS的引用在最後面 ```htmlembedded= <p class="text">You have selected <span id="count">0</span> seats for a price of $<span id="total">0</span></p> <!-- 設定js檔案路徑 --> <script src="script.js"></script> </body> </html> ``` ## CSS的結構 ### 字型設定 首先,咱們先用import的方式設定字型為Lato ```css= @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); ``` ### 邊框設定 當我們使用元素樣式為 box-sizing: border-box;,這個元素的內距和邊框將不會增加元素本身的寬度。 [參考資料](https://zh-tw.learnlayout.com/box-sizing.html) ```css= *{ box-sizing: border-box; } ``` 示意圖: ![](https://i.imgur.com/Eoiby2I.png) ### 整體排列 為了讓整體背景顏色、字體一致,排列置中,採用**display: flex**,讓物件於中央集中 ```css= body { background-color: #242333; color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: 'Lato', sans-serif; margin: 0; } ``` ### 下拉式選單 用margin的方式讓選單與上下各保持一點距離 ```css= /* 電影選擇區 */ .movie-container { margin: 20px 0; } /* 電影選項欄位的美工 */ .movie-container select{ background-color: #fff; border: 0px; border-radius: 5px; font-size: 14px; margin-left: 10px; padding: 5px 15px 5px 15px; -moz-appearance: none; -webkit-appearance: none; appearance: none; } ``` ### 座位區設計 主要掌握幾個關鍵點: 1. 先匡出一個區域來包含螢幕跟座位 2. 座位的美工與原角的修飾 3. 使用`nth-of-type()`、`nth-last-of-type()`來排列座位 ```css= /* 螢幕與座位區的整體配置 */ .container { perspective: 1000px; margin-bottom: 30px; } /* 座位的美工 */ .seat { background-color: #444451; height: 12px; width: 15px; margin: 3px; border-top-left-radius: 10px; border-top-right-radius: 10px; } /* 每排有八個座位 */ .seat.selected { background-color: #6feaf6; } .seat.occupied { background-color: #fff; } /* 選取第n個元素 */ .seat:nth-of-type(2){ margin-right: 18px; } /* 選取最後數來第n個元素 */ .seat:nth-last-of-type(2){ margin-left: 18px; } ``` ### 座位區動畫 使用`hover`功能,分別針對已經被占走和空缺的位子進行動畫設計 ```css= /* 使用hover的功能,讓游標指到可選取的座位時將會放大1.2倍 */ .seat:not(.occupied):hover{ cursor: pointer; transform: scale(1.2); } /* 讓showcase(範例)的圖示呈現靜止狀態 */ .showcase .seat:not(.occupied):hover{ cursor: default; transform: scale(1); } ``` ### 座位狀態範例設計 主要針對圖案與文字的排列是否整齊 ```css= /* 範例區塊的文字與配置 */ .showcase { background: rgba(0,0,0,0.1); padding: 5px 10px; border-radius: 5px; color: #777; list-style-type: none; display: flex; justify-content: space-between; } /* 排列範例內的物件 */ /* 使用flex讓物件並排 */ .showcase li{ display: flex; align-items: center; justify-content: center; margin: 0 10px; } /* 讓範例內的文字跟圖示有些區隔 */ .showcase li small { margin-left: 2px; } /* 使用flex讓每列座位整齊排列 */ .row { display: flex; } ``` ### 電影螢幕 ```css= /* 電影螢幕的設計 */ .screen { background-color: #fff; height: 70px; width: 100%; margin: 15px 0; transform: rotateX(-45deg); box-shadow: 0 3px 10px rgba(255, 255, 255, 0.7); } ``` ### 票價顯示文字 ```css= p.text { margin: 5px 0; } p.text span{ color: #6feaf6; } ``` ## Javascript語言架構 ### 針對需要回傳資料的部分進行設定 由於JS語言就是要讓網頁可以被互動,所以針對特定區塊或物件,使用`querySelector`、`querySelectorAll`與`getElementByID`來設定 這邊不要輕易地使用字面上的意思來猜測可能的功能,每個方法都有特定的回傳手法,例如只回傳一個值,或是回傳全部的元素 ```javascript= // 抓取container的資料 const container = document.querySelector('.container'); // querySelctor: Returns the first element that is a descendant of node that matches selectors. const seats = document.querySelectorAll('.row .seat:not(.occupied)'); // querySelectorAll: Returns all element descendants of node that match selectors. const count = document.getElementById('count'); // getElementById: Returns a reference to the first object with the specified value of the ID attribute. const total = document.getElementById('total'); const movieSelect = document.getElementById('movie'); ``` ### 回傳票價、座位 這裡有幾個重點 1. 讓票價可以將選擇的項目轉換成值,並且將計算結果顯示在頁面上 2. 讓座位可以被點選或取消 3. 票價、電影選擇與座位的結果全部都能被本地端所儲存 ```javascript= populateUI(); // const ticketPrice = parseInt(movieSelect.Value); let ticketPrice = parseInt(movieSelect.value); // Save Selected movie index and price function setMovieData(movieIndex, moviePrice){ localStorage.setItem('selectedMovieIndex',movieIndex); localStorage.setItem('selectMoviePrice',moviePrice); } //Update total and Count function updateSelectedCount() { //注意: .row .seat.selected 不等於 .row.seat.selected // .row .seat.selected 代表<div>row下面的class "seat selected" const selectedSeats = document.querySelectorAll('.row .seat.selected'); // Copy selected seats into arr // Map through aray // Return a new array indexes // 使用...可以把多個目標內的內容依次選取完成 // 使用map可以用來傳遞陣列 // 被選取的座位會回傳以陣列所屬的index const seatsIndex = [...selectedSeats].map(seat => [...seats].indexOf(seat)); localStorage.setItem('selectedSeats', JSON.stringify(seatsIndex)); console.log(seatsIndex); //使用Length回傳所選取的座位數量 //Returns the number of nodes in the collection. const selectedSeatsCount = selectedSeats.length; // 把選取的座位數跟票價進行運算,反應回頁面數字 count.innerText = selectedSeatsCount; total.innerText = selectedSeatsCount * ticketPrice; } // Get data from localstorage and populate UI function populateUI() { //把選取的位置紀錄儲存在本地端的記憶體 const selectedSeats = JSON.parse(localStorage.getItem('selectedSeats') ); // 條件一: 只要選取的seat不為null且選取的座位數量大於0 // 條件二: index>-1下,把seat的classlist新增selected // indexOf下,如值為-1,則代表無回傳任何東西 if(selectedSeats !== null && selectedSeats.length > 0) { seats.forEach((seat, index) => { if(selectedSeats.indexOf(index) > -1){ seat.classList.add('selected'); } }); } // 把選取的電影紀錄儲存在本地端當中 const selectedMovieIndex = localStorage.getItem('selectedMovieIndex'); if (selectedMovieIndex !== null) { movieSelect.selectedIndex = selectedMovieIndex; } } // Movie Select event movieSelect.addEventListener('change', e => { ticketPrice = +e.target.value; setMovieData(e.target.selectedIndex, e.target.value); updateSelectedCount(); }); // Seat Click Event // 使用addEventListener來追蹤每個點選動作 // 目標為container container.addEventListener('click', e =>{ // Target內有多個Class,所以使用e.target.classList.contains的方法來確認是否有相符的字串 // 欲探討主題: e.target.classList與contains放在一起的用法 if(e.target.classList.contains('seat') && !e.target.classList.contains('occupied')){ console.log(e.target); }{ // 使用toggle把座位變成按鈕,把target property變成該id的class特性 // 疑點: 為什麼這個會作用? e.target.classList.toggle('selected'); updateSelectedCount(); } }) // Initial count and total set // 讓一開始就顯示正確計算的票價 updateSelectedCount();