目录
一、天梯积分更新
可以自己定义一下规则
存之前算一下两名玩家的天梯积分
实现更新,实现后重启看一下能不能实现
标记自己是哪条蛇,在前端可以判断左下角是A右上角是B
二、实现对局列表页面
先写一个API从后端返回对局列表的List
Service Impl Controller
加上分页功能MybatisConfig——配置在讲义里
一路Alt+Enter回车、每页十条超出返回空
需要一个参数传入分页编号
告诉前端一共有多少页
如果在Service、Controller里面注入的话就不需要写set了
不需要定义成静态变量在写set如果是第三方类的话就需要写成
static类型后面写一个set在set方法上去写一个Autowired
老样子,要实现Service层,Controller层
先写service接口,写完写service接口的实现ServiceImpl,在重写方法之前,先创建对应的Controller,@Autowired注入刚刚写的service接口,完善Controller,最后再完善Impl里的内容。
由于我们的对局情况可能会有很多,我们把所有对局情况都展示出来显然是不合适的,因此我们需要添加分页功能。
添加分页配置
在Config.MybatisConfig中添加分页配置:
@Configuration
public class MybatisConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER));
return interceptor;
}
}
添加分页配置
在Config.MybatisConfig中添加分页配置
实现分页功能
由于我们不可能一页展示所有对局信息,因此我们要把所有信息分成多个页展示,这里就需要直到当前的页是第几页的。
要传入参数:当前页的编号;
Mybatis里有api可以实现以下页面展示效果,超出范围的话返回空即可。
1: 0~9
2: 10~19
3: 20~29
api:Ipage:IPage<Record> recordIPage = new Page<>(page,10); (当前是第几页,每一页展示多少个)
Mybatis-Plus 关于分页查询的api:
getRecords():获取查询数据
getCurrent():获取当前页
getSize():获取当前分页大小
backend\service\impl\record\GetRecordListServiceImpl.java:
@Service
public class GetRecordListServiceImpl implements GetRecordListService {
@Autowired
private RecordMapper recordMapper;
@Autowired
private UsersMapper usersMapper;
@Override
public JSONObject getList(Integer page) {
IPage<Record> recordIPage = new Page<>(page, 10); //(当前页码,每页展示数目)
//排序展示最新的条目
QueryWrapper<Record> queryWrapper = new QueryWrapper<>();
//若设置成只能看自己则在后面.eq(自己的id)
queryWrapper.orderByDesc("id");//降序排序
List<Record> records = recordMapper.selectPage(recordIPage, queryWrapper).getRecords();
JSONObject resp = new JSONObject();
List<JSONObject> items = new LinkedList<>();
for (Record record : records) {
Users userA = usersMapper.selectById(record.getAId());
Users userB = usersMapper.selectById(record.getBId());
JSONObject item = new JSONObject();
item.put("a_photo", userA.getPhoto());
item.put("a_username", userA.getUsername());
item.put("b_photo", userB.getPhoto());
item.put("b_username", userB.getUsername());
item.put("record", record);
items.add(item);
}
resp.put("records", items);
resp.put("records_count", recordMapper.selectCount(null));
return resp;
}
}
三、前端测试
查询一下后端数据
将列表显示出来
展示table,把之前的table复制过来即可
前端实现相对比较简单,用$ajax接收后端的数据后,用表格把对战双方的头像、用户名、对战结果、对战时间、录像回放选项用一个表格展示出来了
具体表格样式下面给出参考:
<table class="table table-striped table-hover">
<thead>
<tr class="table-dark">
<th>A</th>
<th>B</th>
<th>PK Result</th>
<th>PK Time</th>
<th>Operation</th>
</tr>
</thead>
<tbody>
<tr v-for="record in records" :key="record.record.id">
<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.result }}</td>
<td> {{ record.record.createTime }}</td>
<td>
<button type="button" class="btn btn-secondary">Watch the Record </button>
</td>
</tr>
</tbody>
</table>
四、实现查看录像功能
点击后跳转页面,需要写一个新的View
直接将pk界面复制过来,需要判断是录像还是对战
只需要一个PlayGround组件就可以了
把页面加到路由里面点开Router加一个路由
一定要用localhost因为有些只对localhost放行
首先我们要写一个record的store作为全局变量存储我们有关录像页面的信息,存储的信息包括:
是否为录像页面 is_record
玩家A的步骤:a_steps
玩家B的步骤:b_steps
其次,写一个录像展示页面RecordContent.vue
在RecordIndex.vue界面点击展示录像按钮就要弹出录像展示页面,因此要绑定事件,用@click="open_record_content(record.record.id)",record.record.id是当前录像的id。
在此函数,我们还要更新对局信息,可以用console.log(record)看看我们传进来的record数据格式是怎么样的,方便我们在后面写update函数。
这里建议不要想当然地觉得后端自己写了什么格式的参数就一定是传你这名字的参数,可能传送过程中会出现把大写压成小写等各种奇奇怪怪的转化,保险一点,还是console输出一下看看里面的参数是什么!
注意:我们传入的map是以一维数组的存储格式存储的,我们下面更新的时候需要把它转化为二维的格式,详情见下面的stringTo2D函数!
const stringTo2D = map => {
let g = [];
for (let i = 0,k = 0;i < 13;i ++) {
let line = [];
for (let j = 0;j < 14;j ++,k ++) {
if (map[k] === '0') line.push(0);
else line.push(1);
}
g.push(line);
}
return g;
}
const open_record_content = recordId => {
for (let record of records.value) {
if (record.record.id === recordId) {
store.commit("updateIsRecord",true); //标记成是录像页面
console.log(record);
store.commit("updateGame",{
map: stringTo2D(record.record.map),
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,
})
router.push({
name: "record_content",
params: {
recordId: recordId, //可以简写成一个recordId
}
});
break;
}
}
}
别忘了要添加路由
router\index.js
{
path: "/record/:recordId/", /*路由添加参数用:*/
name: "record_content",
component: RecordContent,
meta: {
requestAuth: true,
}
//requestAuth: true,
},
在GameMap.js里的监听事件函数里稍微修改一下:
如果是放录像的话我们就放录像:根据玩家历史的操作步骤将步骤回放出来;
如果不是放录像的话,我们就接收用户的输入。
回放录像
因为我们的蛇是每秒中走5个格子,所以相当于200ms一格,我们放录像的时候可以设每300ms走一格,并且确定下一步的方向。
setInterval();可以帮我们实现计时函数。
scripts\GameMap.js:
...
add_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;
console.log(this.store.state.record);
const interval_id = setInterval(() => {
if (k >= a_steps.length - 1) { //最后一步是撞墙的一步,不需要复现,直接在后面把state设为dead即可
if (loser === "all" || loser === "A") {
snake0.status = "dead";
}
if (loser === "all" || loser === "B") {
snake1.status = "dead";
}
clearInterval(interval_id); //结束id为interval_id的setInterval()函数
} else {
snake0.set_direction(parseInt(a_steps[k]));
snake1.set_direction(parseInt(b_steps[k]));
}
k ++;
},300); //计时函数,每300ms做一次函数
} else {
this.ctx.canvas.focus();
this.ctx.canvas.addEventListener("keydown", e => {
let d = - 1;
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) { //一个合法的操作
//前端向后端发消息: 前端 -> 后端
this.store.state.pk.socket.send(JSON.stringify({
event: "move",
direction: d,
}));
}
});
}
}
...
五、实现分页功能
把前端功能加上一个跳转的样式
Bootstrap组件Pagination
放到table外面就可以了
加上Page条组件,我们设定一次展示出来的分页有五页
click_page 实现点击页面就跳转到目标页面,-2表示上一页,-1表示下一页
我们用循环展示数据库里的所有页面
为了方便实现高亮、循环等功能,pages作为一个队列存储的是对象,
包括页面id:number、是否高亮:is_active。
<nav aria-label="...">
<ul class="pagination" style="float:right">
<li class="page-item">
<a class="page-link" @click="click_page(-2)" href="#">Previous</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="#">Next</a>
</li>
</ul>
</nav>
实现按哪一页就返回哪一页的内容:
看看当前页面的前后两页是否存在:
const update_pages = () => {
let max_pages = parseInt(Math.ceil(total_records / 10));
let new_pages = [];
for (let i = current_page - 2; i <= current_page + 2; i++) {
if (i >= 1 && i <= max_pages) {
new_pages.push({
number: i,
is_active: i === current_page ? "active" : "",
});
}
}
pages.value = new_pages;
}
六、后端实现查询排行耪
注入查询用户表的Mapper
查询所有的用户+分页
user里面包含密码,记得返回前清空密码
七、前端展示
和对战列表的差不多直接复制过来即可
玩家天梯分一共两项
链接也要记得修改一下
八、限制Bot数量
consume里面开一个新的线程
每个用户最多只能创建十个Bot多了报错
由于bot列表设置分页比较麻烦,我们可以给用户添加限制,省去这些麻烦。。。
QueryWrapper<Bots> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", user.getId());
if (botsMapper.selectCount(queryWrapper) > 10) {
map.put("message", "bot数量不能超过10个");
return map;
}