iT邦幫忙

2022 iThome 鐵人賽

DAY 19
0

今日目標,房間列表的頁面、建立和加入房間功能。

Room List 頁面

我們首先建立房間列表的頁面以及對應的 Controller 做請求分配,HTML 的部分可以直接抄,只單純運用 Thymeleaf Layout 和 Bootstrap~~

  1. 在 templates 底下建立一個 HTML file,名稱為 rooms,內容為:
    <!DOCTYPE html>
    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
        xmlns:th="http://www.thymeleaf.org"
        xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
        layout:decorate="~{layout.html}"
    >
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <div layout:fragment="content" class="card game-window">
        <div class="text-center">
            <div class="card page-title">
                <div class="h3">房間列表</div>
            </div>
        </div>
        <div class="card room-list">
            <div id="rooms">
                <table class="table table-hover">
                    <thead>
                    <tr>
                        <th scope="col" style="width: 28%">房號</th>
                        <th scope="col" style="width: 28%">房主</th>
                        <th scope="col" style="width: 28%">成員數量</th>
                        <th scope="col" style="width: 16%">#</th>
                    </tr>
                    </thead>
                    <tbody>
                    </tbody>
                </table>
            </div>
        </div>
        <br />
        <div class="container">
            <div class="row">
                <div class="col-10"></div>
                <div class="col-1">
                    <button type="button" class="btn btn-primary" id="create" th:disabled="${disableJoinRoomButton}">Create</button>
                </div>
                <div class="col-1"></div>
            </div>
        </div>
    
        <div class="position-fixed bottom-0 right-0 p-3" style="z-index: 5; top: 20px; left: 50%; transform: translate(-50%, 0px);">
            <div id="alert-toast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true" data-delay="2000">
                <div class="text-danger" style="margin: 10px; font-size: 20px;">
                    <i class="bi bi-x-circle-fill"></i>
                    <strong class="mr-auto" id="alert-toast-title" style="margin-left: 5px;"></strong>
                </div>
            </div>
        </div>
    </div>
    
    <div layout:fragment="js-and-css"></div>
    </body>
    </html>
    
    • th:disabled:根據 controller 給定的變數(此處是 disableJoinRoomButton)決定是否要禁用(disable)這個按鈕
  2. 在 room package 底下建立一個 java class,名稱為 RoomController,內容為:
    package com.example.room;
    
    import com.example.user.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    
    @Controller
    public class RoomController {
        @Autowired
        private UserService userService;
    
        @Autowired
        private UserStatus userStatus;
    
        @GetMapping("/rooms")
        public String viewAllRoomsPage(Model model) {
            if (userService.isLogin()) {
                String username = userService.getUsername();
                // 判斷是否已經在房間,是的話重新導向到對應的房間,解決 Day 17 提到的其中一種狀況,`/room` 還沒設定對應分配,所以目前會 404
                if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
                    return "redirect:/room/" + this.userStatus.getUserRoomId(username);
                }
    
                // 否則就初始化他的狀態
                this.userStatus.initialize(username);
            }
    
            // 只允許已經登入的使用者可以建立房間
            model.addAttribute("disableJoinRoomButton", !userService.isLogin());
            return "rooms";
        }
    }
    

建立和加入房間

接下來我們要透過 API 實現「建立房間」、「加入房間」的功能。

  1. 先在 room package 底下建立一個 java class,名稱為 RoomService,內容為:
    package com.example.room;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    
    @Service
    public class RoomService {
        @Autowired
        private RoomList roomList;
    
        @Autowired
        private UserStatus userStatus;
    
        public boolean createRoom(String username) {
            // 檢查使用者已經有狀態,並且目前不在任何房間內
            if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
                return false;
            }
    
            // 建立房間
            String roomId = roomList.create(username);
    
            // 修改使用者目前的狀態
            this.userStatus.setUserInRoom(username, true);
            this.userStatus.setUserRoomId(username, roomId);
            return true;
        }
    
        public boolean joinInRoom(String username, String roomId) {
            // 檢查使用者已經有狀態,並且目前不在任何房間內
            if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
                return false;
            }
    
            // 檢查要加入的房間人數小於 4
            if (roomList.getRoomById(roomId).count() == 4) {
                return false;
            }
    
            // 將使用者加入該房間
            Room room = roomList.getRoomById(roomId);
            if (room.addGuest(username)) {
                if (!this.userStatus.containsUser(username)) {
                    this.userStatus.initialize(username);
                }
                this.userStatus.setUserInRoom(username, true);
                this.userStatus.setUserRoomId(username, roomId);
                return true;
            }
            return false;
        }
    }
    
  2. 再來我們定義呼叫 API 的
    • 方法為 POST
    • url 為 /api/room/join
    • Request 時的資料為 動作(action)、房號(roomId),動作是用來辨別使用者是「建立新房間」還是「加入已經存在的房間」
  3. 要接收 Request 的資料前,我們要先建立一個 class,用來定義資料的內容和型態,所以在 room package 底下建立一個 java class,名稱為 UserJoinRoomMessage,內容為:
    package com.example.room;
    
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter @Setter
    public class UserJoinRoomMessage {
        private String action;
        private String roomId;
    }
    
  4. 再來我們修改 RoomController,對 API 的請求做處理
    package com.example.room;
    
    import com.example.user.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @Controller
    public class RoomController {
        @Autowired
        private UserService userService;
    
        @Autowired
        private UserStatus userStatus;
    
        @Autowired
        private RoomService roomService;
    
        @GetMapping("/rooms")
        public String viewAllRoomsPage(Model model) {
            if (userService.isLogin()) {
                String username = userService.getUsername();
                if (this.userStatus.containsUser(username) && this.userStatus.isUserInRoom(username)) {
                    return "redirect:/room/" + this.userStatus.getUserRoomId(username);
                }
                this.userStatus.initialize(username);
            }
            model.addAttribute("disableJoinRoomButton", !userService.isLogin());
            return "rooms";
        }
    
        @PostMapping("/api/room/join")
        @ResponseBody
        public ResponseEntity<Map<String, Object>> joinRoomProcess(UserJoinRoomMessage userJoinRoomMessage) {
            Map<String, Object> response = new HashMap<>();
            HttpStatus httpStatus;
            String message;
    
            // 檢驗是否登入
            if (!userService.isLogin()) {
                httpStatus = HttpStatus.FORBIDDEN;
                message = "未登入";
                response.put("message", message);
                return ResponseEntity.status(httpStatus).body(response);
            }
    
            String username = userService.getUsername();
            String action = userJoinRoomMessage.getAction();
            String roomId = userJoinRoomMessage.getRoomId();
            if (action.equals("create") && roomService.createRoom(username)) {
                httpStatus = HttpStatus.OK;
                message = "建立成功";
            }
            else if (action.equals("join") && roomService.joinInRoom(username, roomId)) {
                httpStatus = HttpStatus.OK;
                message = "加入成功";
            }
            else {
                httpStatus = HttpStatus.BAD_REQUEST;
                message = "Error";
            }
    
            if (httpStatus == HttpStatus.OK) {
                roomId = this.userStatus.getUserRoomId(username);
            }
            else {
                roomId = "";
            }
            response.put("message", message);
            response.put("roomId", roomId);
            return ResponseEntity.status(httpStatus).body(response);
        }
    }
    
    • @ResponseBody:用於讓一般的 Controller 能夠回傳 JSON 型態的資料
      • 若只用於 API 的 Controller 可以 RestController 註解,並省略 @ResponseBody 註解
    • ResponseEntity<Map<String, Object>>:將資料封裝成 JSON 型態回傳
    • HttpStatus:用於決定 API 回傳的 HTTP Status code
  5. 再來,我們要將頁面上的按鈕綁定 API 的請求,先在 resource/static/js 底下建立 rooms.js,內容為:
    function createRoom() {
        let data = {
            action : "create",
        }
        jq.post(
            "/api/room/join",
            data,
            (response) => {
                window.location.href = `/room/${response.roomId}`;
            }
        )
        .fail(function(e) {
            $("#alert-toast-title").text(e.responseJSON.message);
            $("#alert-toast").toast('show');
        })
    }
    
    function joinRoom(roomId) {
        let data = {
            action : "join",
            roomId : roomId,
        }
        jq.post(
            "/api/room/join",
            data,
            (response) => {
                window.location.href = `/room/${response.roomId}`;
            }
        )
        .fail(function(e) {
            $("#alert-toast-title").text(e.responseJSON.message);
            $("#alert-toast").toast('show');
        })
    }
    
    $(document).ready(() => {
        $("#create").click(function() {
            createRoom();
        })
    })
    
    • createRoom():用來呼叫 API 來建立房間的函數,並根據回傳的房號來做頁面跳轉
    • joinRoom():用來呼叫 API 加入房間的函數,並根據回傳的房號來做頁面跳轉
      • 目前我們還沒用到這個函數,將會在之後顯示房間列表本身的資料時使用
  6. 然後修改 rooms.html,只修改 layout:fragment="js-and-css" 的部分:
    <div layout:fragment="js-and-css">
        <script type="text/javascript" th:src="@{/js/rooms.js}"></script>
    </div>
    
  7. 之後我們回到頁面,登入後到網址 http://localhost:8080/rooms/ ,然後點選 create 按鈕來建立房間,如果建立房間成功,這時候會跳轉到類似 http://localhost:8080/room/udqTejH8 ,的網址,但因為我們還沒撰寫 room 的頁面,所以這時候會出現 Whitelabel Error Page

今天我們只實現頁面和功能,但還沒使用 WebSoceket 的相關技術,所以房間列表目前是沒有資料的~~
/images/emoticon/emoticon13.gif


上一篇
Day 17 - 記錄使用者的狀態
下一篇
Day 19 - 即時顯示房間列表的資料
系列文
Spring Boot... 深不可測31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言