实现对局录像页面

This commit is contained in:
flykhan 2023-03-10 22:23:01 +08:00
parent 8fd376084e
commit 63dc49ccd7
13 changed files with 448 additions and 28 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package com.kob.backend.service.record;
import com.alibaba.fastjson2.JSONObject;
public interface GetRecordListService {
// 参数:分页面列表编号
JSONObject getList(Integer page);
}

View File

@ -163,6 +163,35 @@ export class GameMap extends AcGameObject {
// 添加监听:用于绑定键盘输入,以便获取用户操作控制蛇
add_listening_events() {
// 如果是录像,则从数据库中的数据信息(历史存储的操作步骤)获取输入,并将之前的对局回放;如果不是录像,则获取用户输入
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();
@ -183,10 +212,12 @@ export class GameMap extends AcGameObject {
// 如果进行了合法的移动操作:向后端发送方向
if (d >= 0) {
// 使用 socket.send() 传递信息给后端,使用 JSON.stringify() 将一个 JSON 封装成一字符串
this.store.state.pk.socket.send(JSON.stringify({
this.store.state.pk.socket.send(
JSON.stringify({
event: "move", // 事件类型: move
direction: d, // 方向: d
}))
})
);
}
/*
@ -198,6 +229,7 @@ export class GameMap extends AcGameObject {
*/
});
}
}
start() {
// 开始时调用一次创建墙的函数

View File

@ -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 表示上右下左方向

View File

@ -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",

View File

@ -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,
},
});

46
web/src/store/record.js Normal file
View File

@ -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: {},
};

View File

@ -37,6 +37,7 @@ export default {
})
store.commit("updateLoser", "none");
store.commit("updateIsRecord", false); // is_record false,
// WebSocket
socket = new WebSocket(socketUrl);

View File

@ -1,5 +1,5 @@
<template>
<ContentBase>对局列表</ContentBase>
<ContentBase>排行榜</ContentBase>
</template>
<script>

View File

@ -0,0 +1,18 @@
<template>
<PlayGround />
</template>
<script>
import PlayGround from "../../components/PlayGround.vue";
export default {
components: {
PlayGround,
},
setup() {
}
};
</script>
<style scoped></style>

View File

@ -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 表示行; &nbsp 表示一个空格 -->
<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">
&nbsp;
<span class="record-user-username"> {{ record.a_username }}</span>
</td>
<td>
<img :src="record.b_photo" alt="" class="record-user-photo">
&nbsp;
<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">&laquo;</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">&raquo;</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>