[實作] 電影院訂位介面

學習到的知識點

HTML

  • 下拉選單
  • span is inline-block

CSS

  • -webkit- 私有前綴
  • nth-of-type 與 nth-child
  • rotateX 旋轉技巧
  • perspactive 立體感

Javascript

  • ArrowFunction
  • e.target.classList.toggle
  • NodeList
  • addEventListener(“change”)
  • [… str] 展開運算子
  • Localstorage
  • setItem 跟 getItem
  • json.parse

簡介

DEMO

製作電影訂位的網頁,透過選取座位的方式,紀錄已幾選的位置與不同電影呈現不同的票價。

HTML

電影選單

製作出下拉式選單使用select,在option選項裡加上value,之後可以做為讀取不同電影的不同價格。

1
2
3
4
5
6
7
8
9
<div class="movie-container">
<label>Pick a movie:</label>
<select id="movie">
<option value="10">Avengers: Endgame ($10)</option>
<option value="12">TENET ($12)</option>
<option value="8">Toy Story 4 ($8)</option>
<option value="9">The Lion King ($9)</option>
</select>
</div>

顯示座位狀態清單

座位顯示狀況說明(N/A meaning that the seat is open)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 做出六排座位區

1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="container">
<div class="screen"></div>
<div class="row">
<div class="seat occupied"></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>

顯示已選取幾個座位與價格

span 是屬於 inline-block,所以可以在同一行對數字進行動態調整。

1
2
3
<p class="text">
You have selected <span id="count">0 </span> seats for a price of $<span id="total">0</span>
</p>

CSS

Javascript

引入字體

1
@import url("https://fonts.googleapis.com/css2?family=Lato&display=swap");

將整體元素置中

利用flex屬性使元素置中畫面中央,先使元素column(垂直)排列,再使用flex-direction: columnalign-items:center,主軸副軸置中,視窗高度為100vh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
* {
box-sizing: border-box;
}

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;
}

.movie-container {
margin: 20px 0;
}

調整下拉式選單的選項

-webkit-appearance,使用系統預設的外觀。

-webkit-appearance: none,去除瀏覽器的預設樣式。

webkit 是一種瀏覽器的私有前綴,是瀏覽器對新 css 實現的一個提前支持。
-webkit 是指 webkit 內核的瀏覽器(Safari 和 Google),moz 是 Firefox 內核的瀏覽器。。

為什麼會出現 webkit 跟 moz 這種私有前綴?

因為制定 HTML 和 CSS 標準的組織 W3C 動作較慢,通常,W3C 組織成員提出新屬性,比如說border-radius,但要審核一個屬性通過需要很多審查,瀏覽器商不想等這麼久,就會在瀏覽器中先加入支持。

但是,避免 W3C 公布標準時有變更,就會加入一個私有前綴,比如-webkit-border-radius,通過這種方式來提前支持屬性。

1
2
3
4
5
6
7
8
9
10
11
.movie-container select {
background-color: #fff;
border: 0;
border-radius: 5px;
font-size: 14px;
margin-left: 10px;
padding: 5px 15px 5px 15px;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
}

調整座位元素與位置

在每一組 seat 元素的父元素須加上.row 屬性,才能夠每一排都套用到 flex 屬性。

1
2
3
4
5
6
7
8
9
10
11
12
.seat {
background-color: #444451;
height: 12px;
width: 15px;
margin: 3px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}

.row {
display: flex;
}

不同狀況(Selected,Occupied),給予不同顏色。

1
2
3
4
5
6
7
8
.seat.selected {
background-color: #6feaf6;
}

.seat.occupied {
background-color: #fff;
}

將座位與其他座位空出距離

nth-of-type 與 nth-child() 有什麼不同?

當選取的元素中間有混雜其他元素,像是<hr>,在 nth-child 會將其他元素一起做計算,但 nth-of-type 只會計算選取的元素,例如選擇<p>,就只會選<p>

1
2
3
4
5
6
7
.seat:nth-of-type(2) {
margin-right: 18px;
}

.seat:nth-last-of-type(2) {
margin-left: 18px;
}

滑鼠移動到座位的動畫

不是.occupied的就放大 1.2 倍。

.showcase裡的三個說明位置。scale 不需要變大,使用 default 的 1 倍就好。cursor 也是使用 default。

1
2
3
4
5
6
7
8
9
.seat:not(.occupied):hover {
cursor: pointer;
transform: scale(1.2);
}

.showcase .seat:not(.occupied):hover {
cursor: default;
transform: scale(1);
}

showcase 的排列

使用 flex 屬性自動水平排列,使用justify-content:space-between,空間都留在物件之間。

(space-evenly,平均分配空間。space-around,開頭留一份空間,物件留兩份。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.showcase {
background-color: rgba(0, 0, 0, 0.1);
padding: 5px 10px;
border-radius: 5px;
color: #777;
list-style-type: none;
display: flex;
justify-content: space-between;
}

.showcase li {
display: flex;
align-items: center;
justify-content: center;
margin: 0 10px;
}

.showcase li small {
margin-left: 2px;
}

電影螢幕

rotateX MDN

選轉技巧

1
2
3
4
5
6
7
8
.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);
}

perspactive MDN

perspactive 立體感,數值越高越立體。

1
2
3
4
.container {
perspective: 1000px;
margin-bottom: 30px;
}

計算點選幾個座位與價格

1
2
3
4
5
6
7
8
p.text {
margin: 5px 0;
}

p.text span {
color: #6feaf6;
}

Javascript

讀取 Container (Screen & Seats)的元素

  1. 宣告變數,如果是之後會用到的值,不能使用 const。

  2. ticketPrice 不能用const宣告,因為 value 會改變,改用let

  3. parseInt為轉換型別,字串到數字,也可以使用+

  4. 使用 ES6 語法 ArrowFunctoin 改寫

1
2
3
container.addEventListener("click", function(e) {})
//改成
container.addEventListener("click", (e) => {});
  1. container.addEventListener,點擊後,可以讀取到.Container裡面的元素。
1
2
3
4
5
6
7
8
9
10
11
12
const container = document.querySelector(".container");
const seats = document.querySelectorAll(".row .seat:not(.occupied)");
const count = document.getElementById("count");
const total = document.getElementById("total");
const movieSelect = document.getElementById("movie");

let ticketPrice = parseInt(movieSelect.value);

container.addEventListener("click", (e) => {
console.log(e.target);
});

當空位被點擊時,會轉換成被選取的顏色

  1. 當選取到空位且不是.occupied時,加上.selected的 class 樣式。

  2. e.target.classList.toggle,可以打開或是關閉(像是電燈開關),處理.selected是否加上去。

  3. classList.其他方法,包括 add,remove。

1
2
3
4
5
6
7
8
9
10
const ticketPrice = parseInt(movieSelect.value);

container.addEventListener("click", (e) => {
console.log(e.target);
if (
e.target.classList.contains("seat") && !e.target.classList.contains("occupied")
) {
e.target.classList.toggle("selected");
}
});

加入 Function 之後,動態調整座位數與價錢。

NodeList 是什麼?

NodeList 會經由 querySelectorAll(),childNodes 產生,是陣列的一種,無法使用陣列型別的 method,但可以索引存取內容。

1
2
3
4
5
6
//點選座位後,讀取到NodeList[]
const selectedSeats = document.querySelectorAll(".row .seat.selected");
//轉換成1,2,3,4
const selectedSeatsCount = selectedSeats.length;
//將讀取到的座位數量放到count變數裡
count.innerText = selectedSeatsCount;

完整 Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//更新座位數與價格
function updateSelectedCount() {
const selectedSeats = document.querySelectorAll(".row .seat.selected");
const selectedSeatsCount = selectedSeats.length;
count.innerText = selectedSeatsCount;
total.innerText = selectedSeatsCount * ticketPrice;
}

container.addEventListener("click", (e) => {
if (
e.target.classList.contains("seat") && !e.target.classList.contains("occupied")
) {
e.target.classList.toggle("selected");
//增加這個Function
updateSelectedCount();
}
});

更換電影時,可以更換價格

重新認識 JavaScript: Day 16 那些你知道與不知道的事件們

change 是屬於表單事件中的 change 事件,點擊項目後才觸發。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function updateSelectedCount() {
const selectedSeats = document.querySelectorAll(".row .seat.selected");
const selectedSeatsCount = selectedSeats.length;
count.innerText = selectedSeatsCount;
total.innerText = selectedSeatsCount * ticketPrice;
}

//增加更換電影的事件
movieSelect.addEventListener("change", (e) => {
//確保他轉換成number而不是字串
ticketPrice = parseInt(e.target.value);
updateSelectedCount();
});

儲存被點選的座位位置

[…] 展開運算子。

在 ES6 中,新增了一個 “…” 的關鍵字,這個關鍵字在不同時間點有不同的效果,有些時候它會被當作展開運算子(spread operator 使用,有時候則是被當作其餘運算子(rest operator)使用。

展開運算子,是把許多的參數轉換成一個陣列,而展開運算子則是可以把陣列中的元素取出。

1
2
3
4
5
6
7
8
9
10
11
12
13
const arr1 = [1,2,3]
const arr2 = [...arr1,4,5]
console.log(arr2)
//> Array [1, 2, 3, 4, 5]


const arr1 = [1,2,3]
const arr2 = [...arr1,4,5]
const arr3 = arr2.map(function(item){
return item*2;
})
console.log(arr3)
//> Array [2, 4, 6, 8, 10]

Spread syntax MDN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function updateSelectedCount() {
const selectedSeats = document.querySelectorAll(".row .seat.selected");

//將藍色座位選取後出現的nodeList轉換成array (Copy seleted seats into array)
//使用map方法印出array (Map through array)
//並且使用indexOf方法回傳seat的索引(return a new array indexes)
const seatsIndex = [...selectedSeats].map((seat) => {
return [...seats].indexOf(seat);
});

//可以取得座位的位置 ex:[0,1,7,32]
console.log(seatsIndex);

// How many seats you clicked
const selectedSeatsCount = selectedSeats.length;
count.innerText = selectedSeatsCount;

加入 LocalStorage

可以在 Application 裡面查看儲存的座位陣列。

Js 的setItem語法,可以將資料寫進瀏覽器裡。setItem 的第一個值是 key 的屬性名,第二個值就是相對應的 value。

1
2
//加在updateSelectedCount Function裡
localStorage.setItem("selectedSeats", JSON.stringify(seatsIndex));

可以在 Application 裡面查看,儲存的 selectedMovieIndex,selectedMoviePrice 陣列。

1
2
3
4
5
//Save selected movie index and price
function setMovieData(movieIndex, moviePrice) {
localStorage.setItem("selectedMovieIndex", movieIndex);
localStorage.setItem("selectedMoviePrice", moviePrice);
}

從 Localhost 取資料,使得點選過後的位置可以填充 UI

  1. 回傳-1 為,陣列內沒有這個元素時會回傳索引值。
1
2
3
const arr2 = [1,2,3,4,5]
console.log(arr2.indexOf(100))
//> -1
  1. Json.parse將字串轉換成陣列或物件的用法。

  2. 把資料存進瀏覽器後,要取出來的話要用getItem語法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Get data from localstorage and populate UI
function populateUI() {
//使用getItem取得要使用的key,為上方設定的'selectedSeats',並指派變數selectedSeats。
//使用parse把JSON資料轉換成array
const selectedSeats = JSON.parse(localStorage.getItem("selectedSeats"));
//判斷讓藍色座椅是否繼續留著
//要使用localstorage裡面儲存的資料判斷是否符合。
if (selectedSeats ! null && selectedSeats.length > 0) {
//將所有座位使用forEach一個個印出
//並且判斷每個座位的index號碼是否跟selectedSeats的一樣
//如果相同則執行加上.selected
seats.forEach((seat, index) => {
// -1 就是 udefined
if (selectedSeats.indexOf(index) > -1) {
seat.classList.add("selected");
}
});
}

//判斷selectedMovieIndex是否為空
//不為空則賦值給selectedMovieIndex下拉式選單選取的電影index
const selectedMovieIndex = localStorage.getItem("selectedMovieIndex");
if (selectedMovieIndex ! null) {
movieSelect.selectedIndex = selectedMovieIndex;
}
}

最後在整個 JS 最下方呼叫一次 updateSelectedCount() 函式,這樣重整網頁時,不需經過事件觸發,直接更新下方文字敘述。

參考文章

CSS

webkit-appearance:none

appearance

CSS3 選擇器「:nth-child()」與「:nth-of-type()」用法大不同

JS

NodeList 與 Array 差異

重新認識 JavaScript: Day 16 那些你知道與不知道的事件們

【ES6 小筆記】Array.map() - 處理陣列最佳選擇

Spread syntax MDN

JSON.parse 函式

JavaScript ES6 中的展開運算子(spread operator)和其餘運算子(rest operator)


[實作] 電影院訂位介面
https://phoebeho.com/Portfolio/20210117/145289634/
作者
Phoebe
發布於
2021年1月17日
許可協議