今日目標,房間列表的頁面、建立和加入房間功能。
我們首先建立房間列表的頁面以及對應的 Controller 做請求分配,HTML 的部分可以直接抄,只單純運用 Thymeleaf Layout 和 Bootstrap~~
<!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)這個按鈕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 實現「建立房間」、「加入房間」的功能。
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;
}
}
/api/room/join
package com.example.room;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class UserJoinRoomMessage {
private String action;
private String roomId;
}
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 型態的資料
RestController
註解,並省略 @ResponseBody
註解ResponseEntity<Map<String, Object>>
:將資料封裝成 JSON 型態回傳HttpStatus
:用於決定 API 回傳的 HTTP Status coderesource/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 加入房間的函數,並根據回傳的房號來做頁面跳轉
rooms.html
,只修改 layout:fragment="js-and-css"
的部分:
<div layout:fragment="js-and-css">
<script type="text/javascript" th:src="@{/js/rooms.js}"></script>
</div>
今天我們只實現頁面和功能,但還沒使用 WebSoceket 的相關技術,所以房間列表目前是沒有資料的~~