实现对局录像页面
This commit is contained in:
parent
8fd376084e
commit
63dc49ccd7
|
@ -0,0 +1,18 @@
|
|||
// 用于实现对局记录的分页功能
|
||||
package com.kob.backend.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MybatisConfig {
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.kob.backend.controller.record;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.kob.backend.service.record.GetRecordListService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class GetRecordListController {
|
||||
@Autowired
|
||||
private GetRecordListService getRecordListService;
|
||||
|
||||
// 这里只需要获取,因此只需要用 get 方法(需要新建和修改数据时,使用 post 方法)
|
||||
@GetMapping("/record/getlist/")
|
||||
public JSONObject getList(@RequestParam Map<String, String> data) {
|
||||
// 解析出 page 信息
|
||||
Integer page = Integer.parseInt(data.get("page_index"));
|
||||
return getRecordListService.getList(page);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// 实现对局信息分页发送
|
||||
package com.kob.backend.service.impl.record;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.kob.backend.mapper.RecordMapper;
|
||||
import com.kob.backend.mapper.UserMapper;
|
||||
import com.kob.backend.pojo.Record;
|
||||
import com.kob.backend.pojo.User;
|
||||
import com.kob.backend.service.record.GetRecordListService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class GetRecordListServiceImpl implements GetRecordListService {
|
||||
@Autowired
|
||||
private RecordMapper recordMapper;
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper; // 注入 UserMapper 用于查询用户名和用户头像
|
||||
|
||||
@Override
|
||||
public JSONObject getList(Integer page) {
|
||||
// 使用 Mybatis API : IPage 实现
|
||||
IPage<Record> recordIPage = new Page<>(page, 10); // 参数列表:Page<>(传第几页, 每一页有多少项目)
|
||||
QueryWrapper<Record> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.orderByDesc("id"); // 通过 record 的 id 降序排序
|
||||
List<Record> records = recordMapper.selectPage(recordIPage, queryWrapper).getRecords();
|
||||
|
||||
|
||||
JSONObject resp = new JSONObject();
|
||||
List<JSONObject> items = new LinkedList<>();
|
||||
for (Record record : records) { // 枚举对局记录
|
||||
// 获取用户 A 和 B
|
||||
User userA = userMapper.selectById(record.getAId());
|
||||
User userB = userMapper.selectById(record.getBId());
|
||||
|
||||
// 取出 A 和 B 的用户名和头像信息
|
||||
String aName = userA.getUsername(), bName = userB.getUsername();
|
||||
String aPhoto = userA.getPhoto(), bPhoto = userB.getPhoto();
|
||||
|
||||
String winner = "平局!"; // 定义赢家
|
||||
|
||||
// 将 A 和 B 的用户名和头像信息存到 jsonObject 对象 item 中
|
||||
JSONObject item = new JSONObject();
|
||||
item.put("a_username", aName);
|
||||
item.put("a_photo", aPhoto);
|
||||
item.put("b_username", bName);
|
||||
item.put("b_photo", bPhoto);
|
||||
if ("A".equals(record.getLoser())) winner = userB.getUsername() + " 胜";
|
||||
else if ("B".equals(record.getLoser())) winner = userA.getUsername() + " 胜";
|
||||
item.put("winner", winner); // 存入赢家信息
|
||||
item.put("record", record); // 存入对战信息
|
||||
// 将 jsonObject 对象 item 中,添加到 items 列表中
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
resp.put("records", items); // 将 items 信息存入 records 中
|
||||
resp.put("records_count", recordMapper.selectCount(null)); // 存入当前记录页面的总数
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.kob.backend.service.record;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
public interface GetRecordListService {
|
||||
// 参数:分页面列表编号
|
||||
JSONObject getList(Integer page);
|
||||
}
|
|
@ -163,40 +163,72 @@ export class GameMap extends AcGameObject {
|
|||
|
||||
// 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
|
||||
add_listening_events() {
|
||||
// 聚焦到获取输入的画布页面
|
||||
this.ctx.canvas.focus();
|
||||
// 如果是录像,则从数据库中的数据信息(历史存储的操作步骤)获取输入,并将之前的对局回放;如果不是录像,则获取用户输入
|
||||
if (this.store.state.record.is_record) {
|
||||
let k = 0;
|
||||
const a_steps = this.store.state.record.a_steps; // 取出两条蛇的历史步骤
|
||||
const b_steps = this.store.state.record.b_steps;
|
||||
const loser = this.store.state.record.record_loser;
|
||||
const [snake0, snake1] = this.snakes; // 取两条蛇的信息
|
||||
// setInterval 用于设定执行间隔
|
||||
const interval_id = setInterval(() => {
|
||||
if (k >= a_steps.length - 1) {
|
||||
// 如果下一步要死亡,则将死亡的蛇标记一下(最后一步不做移动,因此使用 steps.length - 1; 最后一步可以用来判断蛇眼方向)
|
||||
// 修改最后一步(检测到碰撞后)蛇的眼睛方向
|
||||
snake0.eye_direction = parseInt(a_steps[k]);
|
||||
snake1.eye_direction = parseInt(b_steps[k]);
|
||||
if (loser === "all" || loser === "A") {
|
||||
snake0.status = "die";
|
||||
}
|
||||
if (loser === "all" || loser === "B") {
|
||||
snake1.status = "die";
|
||||
}
|
||||
// 死亡后要取消循环
|
||||
clearImmediate(interval_id);
|
||||
} else {
|
||||
snake0.set_direction(parseInt(a_steps[k])); // 修改下一步的运动方向
|
||||
snake1.set_direction(parseInt(b_steps[k]));
|
||||
k++; // 递增下一步
|
||||
}
|
||||
}, 300); // 每 300 毫秒获取下一步并执行一次
|
||||
} else {
|
||||
// 聚焦到获取输入的画布页面
|
||||
this.ctx.canvas.focus();
|
||||
|
||||
// 先取出两条蛇对象
|
||||
// const [snake0, snake1] = this.snakes;
|
||||
// 先取出两条蛇对象
|
||||
// const [snake0, snake1] = this.snakes;
|
||||
|
||||
// 获取用户信息:绑定 keydown 事件
|
||||
this.ctx.canvas.addEventListener("keydown", (e) => {
|
||||
// 取出移动的方向,需要将方向传给后端
|
||||
let d = -1; // -1 表示没有方向, 0-上, 1-右, 2-下, 3-左
|
||||
// 获取用户信息:绑定 keydown 事件
|
||||
this.ctx.canvas.addEventListener("keydown", (e) => {
|
||||
// 取出移动的方向,需要将方向传给后端
|
||||
let d = -1; // -1 表示没有方向, 0-上, 1-右, 2-下, 3-左
|
||||
|
||||
// 定义 snake0 的键盘绑定事件
|
||||
if (e.key === "w") d = 0;
|
||||
else if (e.key === "d") d = 1;
|
||||
else if (e.key === "s") d = 2;
|
||||
else if (e.key === "a") d = 3;
|
||||
// 定义 snake0 的键盘绑定事件
|
||||
if (e.key === "w") d = 0;
|
||||
else if (e.key === "d") d = 1;
|
||||
else if (e.key === "s") d = 2;
|
||||
else if (e.key === "a") d = 3;
|
||||
|
||||
// 如果进行了合法的移动操作:向后端发送方向
|
||||
if(d>=0){
|
||||
// 使用 socket.send() 传递信息给后端,使用 JSON.stringify() 将一个 JSON 封装成一字符串
|
||||
this.store.state.pk.socket.send(JSON.stringify({
|
||||
event: "move", // 事件类型: move
|
||||
direction : d, // 方向: d
|
||||
}))
|
||||
}
|
||||
// 如果进行了合法的移动操作:向后端发送方向
|
||||
if (d >= 0) {
|
||||
// 使用 socket.send() 传递信息给后端,使用 JSON.stringify() 将一个 JSON 封装成一字符串
|
||||
this.store.state.pk.socket.send(
|
||||
JSON.stringify({
|
||||
event: "move", // 事件类型: move
|
||||
direction: d, // 方向: d
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
// 定义 snake1 的键盘绑定事件(前端调试时启用)
|
||||
else if (e.key === "ArrowUp") snake1.set_direction(0);
|
||||
else if (e.key === "ArrowRight") snake1.set_direction(1);
|
||||
else if (e.key === "ArrowDown") snake1.set_direction(2);
|
||||
else if (e.key === "ArrowLeft") snake1.set_direction(3);
|
||||
*/
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
|
|
|
@ -19,7 +19,7 @@ export class Snake extends AcGameObject {
|
|||
this.cells = [new Cell(info.r, info.c)];
|
||||
this.next_cell = null; // 下一步的目标位置
|
||||
|
||||
this.speed = 5; // 蛇的速度:每秒走五个格子
|
||||
this.speed = 5; // 蛇的速度:每秒走五个格子,每一步需要 200 ms
|
||||
|
||||
// 定义蛇下一步的指令相关属性
|
||||
// -1 表示没有指令, 0、 1、 2、 3 表示上右下左方向
|
||||
|
|
|
@ -3,6 +3,7 @@ import { createRouter, createWebHistory } from "vue-router";
|
|||
import PkIndexView from "../views/pk/PkIndexView.vue";
|
||||
import RanklistIndexView from "../views/ranklist/RanklistIndexView.vue";
|
||||
import RecordIndexView from "../views/record/RecordIndexView.vue";
|
||||
import RecordContentView from "../views/record/RecordContentView.vue";
|
||||
import UserBotIndexView from "../views/user/bot/UserBotIndexView.vue";
|
||||
import NotFound from "../views/error/NotFound.vue";
|
||||
import UserAccountLoginView from "@/views/user/account/UserAccountLoginView.vue";
|
||||
|
@ -47,6 +48,15 @@ const routes = [
|
|||
requestAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// 展示录像内容: :recordId 作为参数
|
||||
path: "/record/:recordId/",
|
||||
name: "record_content",
|
||||
component: RecordContentView,
|
||||
meta: {
|
||||
requestAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/user/bot/",
|
||||
name: "user_bot_index",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { createStore } from "vuex";
|
|||
import ModuleUser from "./user";
|
||||
import ModuleBot from './bot';
|
||||
import ModulePk from './pk';
|
||||
import ModuleRecord from './record';
|
||||
|
||||
export default createStore({
|
||||
state: {},
|
||||
|
@ -12,5 +13,6 @@ export default createStore({
|
|||
user: ModuleUser,
|
||||
bot: ModuleBot,
|
||||
pk: ModulePk,
|
||||
record: ModuleRecord,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import $ from "jquery";
|
||||
import store from ".";
|
||||
|
||||
export default {
|
||||
state: {
|
||||
is_record: false, // 当前页面是不是录像页面
|
||||
a_steps: "", // a 玩家的路径记录
|
||||
b_steps: "",
|
||||
record_loser: "",
|
||||
},
|
||||
getters: {},
|
||||
mutations: {
|
||||
updateIsRecord(state, is_record) {
|
||||
state.is_record = is_record;
|
||||
},
|
||||
updateSteps(state, data) {
|
||||
state.a_steps = data.a_steps;
|
||||
state.b_steps = data.b_steps;
|
||||
},
|
||||
updateRecordLoser(state, loser) {
|
||||
state.record_loser = loser;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
getRecordList(context, data) {
|
||||
// 获取 Bot 列表
|
||||
$.ajax({
|
||||
url: "http://localhost:3000/record/getlist/",
|
||||
data: {
|
||||
page_index: data.page,
|
||||
},
|
||||
type: "GET",
|
||||
headers: {
|
||||
Authorization: "Bearer " + store.state.user.token,
|
||||
},
|
||||
success(resp) {
|
||||
data.success(resp);
|
||||
},
|
||||
error(resp) {
|
||||
data.error(resp);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
modules: {},
|
||||
};
|
|
@ -37,6 +37,7 @@ export default {
|
|||
})
|
||||
|
||||
store.commit("updateLoser", "none");
|
||||
store.commit("updateIsRecord", false); // 将 is_record 改为 false, 表示当前页面不是录像页面
|
||||
|
||||
// 新建 WebSocket
|
||||
socket = new WebSocket(socketUrl);
|
||||
|
@ -89,7 +90,7 @@ export default {
|
|||
snake1.eye_direction = data.b_eyes_finally_direction;
|
||||
|
||||
// 更新 loser 信息,存储到全局 store 中
|
||||
store.commit("updateLoser",data.loser);
|
||||
store.commit("updateLoser", data.loser);
|
||||
|
||||
// 更新 loser 的 snake 对象 status 状态
|
||||
if (data.loser === "all" || data.loser === "A") {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<ContentBase>对局列表</ContentBase>
|
||||
<ContentBase>排行榜</ContentBase>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<PlayGround />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PlayGround from "../../components/PlayGround.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PlayGround,
|
||||
},
|
||||
setup() {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -1,15 +1,208 @@
|
|||
<template>
|
||||
<ContentBase>排行榜</ContentBase>
|
||||
<ContentBase>
|
||||
<table class="table table-striped" style="text-align:center">
|
||||
<!-- <table class="table table-success table-striped" style="text-align:center"> -->
|
||||
<!-- <table class="table table-sm" style="text-align:center"> -->
|
||||
<!-- 创建表头: th 表示列 -->
|
||||
<thead>
|
||||
<th>玩家A</th>
|
||||
<th>玩家B</th>
|
||||
<th>赢家</th>
|
||||
<th>对局时间</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<!-- 创建表身: tr 表示行;   表示一个空格 -->
|
||||
<tbody class="table-group-divider">
|
||||
<tr v-for="record in records" :key="record.record.id" style="text-align:center">
|
||||
<td>
|
||||
<img :src="record.a_photo" alt="" class="record-user-photo">
|
||||
|
||||
<span class="record-user-username"> {{ record.a_username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<img :src="record.b_photo" alt="" class="record-user-photo">
|
||||
|
||||
<span class="record-user-username"> {{ record.b_username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ record.winner }}
|
||||
</td>
|
||||
<td>{{ record.record.createtime }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
@click="open_record_content(record.record.id)">观看录像</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- <nav aria-label="...">
|
||||
<ul class="pagination" style="float: right;">
|
||||
<li class="page-item" @click="click_page(-2)">
|
||||
<a class="page-link" href="#">前一页</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + page.is_active" v-for="page in pages" :key="page.number" @click="click_page(page.number)">
|
||||
<a class="page-link" href="#">{{ page.number }}</a>
|
||||
</li>
|
||||
<li class="page-item" @click="click_page(-1)">
|
||||
<a class="page-link" href="#">后一页</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav> -->
|
||||
|
||||
|
||||
|
||||
<!-- Pagination 分页 -->
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item" @click="click_page(-2)">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<li :class="'page-item ' + page.is_active" v-for="page in pages" :key="page.number"
|
||||
@click="click_page(page.number)">
|
||||
<a class="page-link" href="#">{{ page.number }}</a>
|
||||
</li>
|
||||
<li class="page-item" @click="click_page(-1)">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</ContentBase>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentBase from "../../components/ContentBase.vue";
|
||||
import { useStore } from 'vuex';
|
||||
import { ref } from "vue";
|
||||
import router from "@/router/index";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentBase,
|
||||
},
|
||||
setup: () => {
|
||||
const store = useStore();
|
||||
let records = ref([]);
|
||||
let current_page = 1; // 定义初始页面序号
|
||||
let total_records = 0; // 对局数量
|
||||
let pages = ref([]); // 页面
|
||||
|
||||
|
||||
const click_page = page => {
|
||||
if (page === -2) page = current_page - 1; // 前一页
|
||||
else if (page === -1) page = current_page + 1; // 后一页
|
||||
// 判断 page 合法性
|
||||
let max_pages = parseInt(Math.ceil(total_records / 10));
|
||||
|
||||
if (page >= 1 && page <= max_pages) {
|
||||
pull_page(page);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const update_pages = () => { // 更新页面
|
||||
let max_pages = parseInt(Math.ceil(total_records / 10)); // ceil 上取整
|
||||
let new_pages = [];
|
||||
for (let i = current_page - 2; i <= current_page + 2; i++) { // 分 5 页: current_page 分别向上向下 -2
|
||||
if (i >= 1 && i <= max_pages) {
|
||||
new_pages.push({
|
||||
number: i,
|
||||
is_active: i === current_page ? "active" : "", // 页面序号标签是否被激活:如果选中页是当前页,则激活(active)
|
||||
});
|
||||
}
|
||||
}
|
||||
pages.value = new_pages;
|
||||
}
|
||||
|
||||
const pull_page = (page) => { // 加载分页
|
||||
current_page = page; // 更新页面序号
|
||||
|
||||
store.dispatch("getRecordList", {
|
||||
page,
|
||||
success(resp) {
|
||||
console.log(resp.records);
|
||||
records.value = resp.records;
|
||||
total_records = resp.records_count;
|
||||
update_pages(); // 更新分页
|
||||
},
|
||||
error(resp) {
|
||||
console.log(resp);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// 将 map 从字符串转化成二维数组
|
||||
const stringTo2D = (map, rows, cols) => {
|
||||
let g = [];
|
||||
for (let i = 0, k = 0; i < rows; i++) {
|
||||
let line = [];
|
||||
for (let j = 0; j < cols; j++) {
|
||||
if (map[k] === '0') line.push(0);
|
||||
else line.push(1);
|
||||
k++;
|
||||
}
|
||||
g.push(line);
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
// 打开对局录像的方法
|
||||
const open_record_content = recordId => {
|
||||
for (const record of records.value) {
|
||||
if (record.record.id === recordId) {
|
||||
store.commit("updateIsRecord", true); // 当前页面是录像页面
|
||||
// 传出 record 中的地图信息
|
||||
store.commit("updateGameMap", {
|
||||
game_map: stringTo2D(record.record.map, record.record.mapRows, record.record.mapCols), // 将地图字符串转换为 2D 数组后,再更新存储
|
||||
rows: record.record.mapRows,
|
||||
cols: record.record.mapCols,
|
||||
inner_walls_count: record.record.innerWallsCount,
|
||||
a_id: record.record.aid,
|
||||
a_sx: record.record.asx,
|
||||
a_sy: record.record.asy,
|
||||
b_id: record.record.bid,
|
||||
b_sx: record.record.bsx,
|
||||
b_sy: record.record.bsy,
|
||||
});
|
||||
store.commit("updateSteps", { // 更新本次对局的 step(操作步骤) 信息
|
||||
a_steps: record.record.asteps,
|
||||
b_steps: record.record.bsteps,
|
||||
});
|
||||
store.commit("updateRecordLoser", record.record.loser);
|
||||
console.log(store.state.pk);
|
||||
console.log(store.state.record);
|
||||
router.push({ // 路由跳转(在 router->index.js 中有定义)
|
||||
name: "record_content",
|
||||
params: {
|
||||
recordId: recordId,
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pull_page(current_page);
|
||||
|
||||
return {
|
||||
records,
|
||||
open_record_content,
|
||||
pages,
|
||||
click_page,
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.record-user-photo {
|
||||
border-radius: 50%;
|
||||
width: 5vh;
|
||||
}
|
||||
|
||||
.record-user-username {}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue