parent
2c381678af
commit
d4e0391f83
30 changed files with 1998 additions and 76 deletions
@ -1,26 +1,75 @@ |
||||
<template> |
||||
<img alt="Vue logo" src="./assets/logo.png"> |
||||
<HelloWorld msg="Welcome to Your Vue.js App"/> |
||||
<div class="game-body"> |
||||
<MenuView v-if="$store.state.router.router_name === 'menu'" /> |
||||
<PkIndexView v-else-if="$store.state.router.router_name === 'pk'" /> |
||||
<RanklistIndexView |
||||
v-else-if="$store.state.router.router_name === 'ranklist'" |
||||
/> |
||||
<RecordIndexView v-else-if="$store.state.router.router_name === 'record'" /> |
||||
<RecordContentView |
||||
v-else-if="$store.state.router.router_name === 'record_content'" |
||||
/> |
||||
<UserBotIndexView |
||||
v-else-if="$store.state.router.router_name === 'user_bot'" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import HelloWorld from './components/HelloWorld.vue' |
||||
import { useStore } from "vuex"; |
||||
import MenuView from "./views/MenuView.vue"; |
||||
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"; |
||||
|
||||
export default { |
||||
name: 'App', |
||||
components: { |
||||
HelloWorld |
||||
} |
||||
} |
||||
MenuView, |
||||
PkIndexView, |
||||
RanklistIndexView, |
||||
RecordIndexView, |
||||
RecordContentView, |
||||
UserBotIndexView, |
||||
}, |
||||
setup() { |
||||
const store = useStore(); |
||||
// 如果localStorage中保存了token,那就读取出来并和后端信息比对,比对成功进入首页 |
||||
const jwt_token = |
||||
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0YTMyOTdhYzE0YmY0ZDVkYmU2ODAxZDVkNTk3ODhiYyIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY2MjYyMTAwNCwiZXhwIjoxNjYzODMwNjA0fQ.rKZG8n3U_U50Aql25nB2k27cZWtxSpE4__a8bnSP3Fs"; |
||||
if (jwt_token) { |
||||
store.commit("updateToken", jwt_token); |
||||
store.dispatch("getInfo", { |
||||
// 向后端请求信息 |
||||
success() { |
||||
store.commit("updatePullingInfo", false); |
||||
}, |
||||
error() { |
||||
store.commit("updatePullingInfo", false); |
||||
}, |
||||
}); |
||||
} else { |
||||
store.commit("updatePullingInfo", false); |
||||
} |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
<style> |
||||
#app { |
||||
font-family: Avenir, Helvetica, Arial, sans-serif; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
text-align: center; |
||||
color: #2c3e50; |
||||
margin-top: 60px; |
||||
<style scoped> |
||||
body { |
||||
margin: 0; |
||||
} |
||||
|
||||
div.game-body { |
||||
background-image: url("@/assets/images/background.png"); |
||||
background-size: cover; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
div.window { |
||||
width: 100vw; |
||||
height: 100vh; |
||||
} |
||||
</style> |
||||
|
After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,50 @@ |
||||
const AC_GAME_OBJECTS = []; |
||||
|
||||
export class AcGameObject { |
||||
constructor() { |
||||
AC_GAME_OBJECTS.push(this); // 数组添加元素
|
||||
this.timedelta = 0; // 注意单位是毫秒,转化为秒需要除于1000
|
||||
this.has_called_start = false; |
||||
} |
||||
|
||||
start() { // 只执行一次
|
||||
} |
||||
|
||||
update() { // 每一帧执行一次,除了第一帧之外
|
||||
|
||||
} |
||||
|
||||
on_destroy() { // 删除之前执行
|
||||
|
||||
} |
||||
|
||||
destroy() { |
||||
this.on_destroy(); |
||||
|
||||
for (let i in AC_GAME_OBJECTS) { // 遍历数组下标
|
||||
const obj = AC_GAME_OBJECTS[i]; |
||||
if (obj === this) { // 注意是三个等号
|
||||
AC_GAME_OBJECTS.splice(i); // 从数组中移除元素
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
let last_timestamp; // 上一次执行的时刻
|
||||
const step = timestamp => { |
||||
for (let obj of AC_GAME_OBJECTS) { // 遍历数组的值
|
||||
if (!obj.has_called_start) { // 如果对象没有被调用
|
||||
obj.has_called_start = true; |
||||
obj.start(); |
||||
} else { |
||||
obj.timedelta = timestamp - last_timestamp; // 获得当前帧和上一帧的时间差
|
||||
obj.update(); |
||||
} |
||||
} |
||||
|
||||
last_timestamp = timestamp; // 将当前时间更新为上一帧执行时刻
|
||||
requestAnimationFrame(step) // 递归调用该函数
|
||||
} |
||||
|
||||
requestAnimationFrame(step) |
@ -0,0 +1,9 @@ |
||||
export class Cell{ |
||||
constructor(r,c) { // 传入蛇的坐标(r,c)
|
||||
this.r = r; |
||||
this.c = c; |
||||
this.x = c + 0.5; // 将蛇的左上角坐标(r,c)转化为画布上的圆心坐标(x,y)
|
||||
this.y = r + 0.5; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,155 @@ |
||||
import { AcGameObject } from "./AcGameObject"; // 导入js的export class
|
||||
import { Snake } from "./Snake"; |
||||
import { Wall } from "./Wall"; |
||||
|
||||
export class GameMap extends AcGameObject { |
||||
constructor(ctx, parent, store) { |
||||
super(); // 继承类一直要先调用父类的构造函数
|
||||
|
||||
this.ctx = ctx; |
||||
this.parent = parent; |
||||
this.L = 0; |
||||
|
||||
this.rows = 13; |
||||
this.cols = 14; |
||||
|
||||
this.store = store; |
||||
|
||||
this.inner_walls_count = 20; // 障碍物的数量(最大建议80)
|
||||
this.walls = []; // 所有障碍物组成的数组
|
||||
|
||||
// 创建两条蛇
|
||||
|
||||
this.snakes = [ // 蓝蛇和红蛇
|
||||
new Snake({ id: 0, color: "#4876EC", r: this.rows - 2, c: 1 }, this), |
||||
new Snake({ id: 1, color: "#F94848", r: 1, c: this.cols - 2 }, this), |
||||
]; |
||||
} |
||||
|
||||
create_walls() { // 判断是否生成有效的地图
|
||||
const g = this.store.state.pk.gamemap; |
||||
for (let r = 0; r < this.rows; r++) { |
||||
for (let c = 0; c < this.cols; c++) { |
||||
if (g[r][c]) { |
||||
// 每次添加一个障碍物
|
||||
this.walls.push(new Wall(r, c, this)); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
add_listening_events() { // 绑定监听事件
|
||||
if (this.store.state.record.is_record === true) { // 播放录像
|
||||
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; |
||||
|
||||
let interval_id = setInterval(() => { |
||||
if (k >= a_steps.length - 1) { // 不回放最后一步,最后一步直接展示结果
|
||||
if (loser == "all" || loser == "A") { |
||||
snake0.status = "die"; |
||||
} |
||||
if (loser == "all" || loser == "B") { |
||||
snake1.status = "die"; |
||||
} |
||||
clearInterval(interval_id); |
||||
}else { |
||||
snake0.set_direction(parseInt(a_steps[k])); |
||||
snake1.set_direction(parseInt(b_steps[k])); |
||||
} |
||||
k++; |
||||
|
||||
},300); |
||||
}else { |
||||
this.ctx.canvas.focus(); // 聚焦
|
||||
// 用wasd控制方向
|
||||
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; |
||||
this.store.state.pk.socket.send(JSON.stringify({ |
||||
event: "move", |
||||
direction: d, |
||||
})); |
||||
}); |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
|
||||
start() { |
||||
this.create_walls(); |
||||
this.add_listening_events(); |
||||
} |
||||
|
||||
update_size() { // 每过一帧重新生成新的地图尺寸
|
||||
// 取整数
|
||||
this.L = parseInt(Math.min(this.parent.clientWidth / this.cols, this.parent.clientHeight / this.rows)); |
||||
this.ctx.canvas.width = this.L * this.cols; |
||||
this.ctx.canvas.height = this.L * this.rows; |
||||
} |
||||
|
||||
update() { // 更新地图:每隔一帧都要重新渲染
|
||||
this.update_size(); // 更新地图大小
|
||||
if (this.check_ready()) { |
||||
this.next_step(); // 进入下一回合
|
||||
} |
||||
this.render(); // 重新渲染
|
||||
} |
||||
|
||||
check_ready() { // 判断两条蛇是否准备好进行下一步
|
||||
for (const snake of this.snakes) { |
||||
if (snake.status !== "idle") return false; |
||||
if (snake.direction === -1) return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
next_step() { // 让两条蛇进入下一回合
|
||||
for (const snake of this.snakes) { |
||||
snake.next_step(); |
||||
} |
||||
} |
||||
|
||||
check_vaild(cell) { // 检测蛇的目标位置是否合法:是否撞到两条蛇的身体和障碍物
|
||||
for (const wall of this.walls) { |
||||
if (wall.r === cell.r && wall.c === cell.c) { // 撞到墙了
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
for (const snake of this.snakes) { |
||||
let k = snake.cells.length; |
||||
// 特殊处理!!!
|
||||
if (!snake.check_tail_increasing()) { // 当蛇为会前进的时候,蛇尾不需要判断,因为蛇头可以占据蛇尾的位置
|
||||
k--; |
||||
} |
||||
for (let i = 0; i < k; i++) { // 依次判断是否会撞到两条蛇的身体
|
||||
if (snake.cells[i].r === cell.r && snake.cells[i].c === cell.c) { // 撞到蛇的身体了
|
||||
return false; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
render() { |
||||
const color_even = "#AAD751", color_odd = "#A2D149"; |
||||
for (let r = 0; r < this.rows; r++) { |
||||
for (let c = 0; c < this.cols; c++) { |
||||
if ((r + c) % 2 == 0) { |
||||
this.ctx.fillStyle = color_even; |
||||
} else { |
||||
this.ctx.fillStyle = color_odd; |
||||
} |
||||
this.ctx.fillRect(c * this.L, r * this.L, this.L, this.L); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,168 @@ |
||||
import { AcGameObject } from "./AcGameObject"; |
||||
import { Cell } from "./Cell"; |
||||
|
||||
export class Snake extends AcGameObject { |
||||
constructor(info,gamemap) { // 传入蛇的信息和地图
|
||||
super(); |
||||
|
||||
this.id = info.id; // 哪条蛇
|
||||
this.color = info.color; // 蛇的颜色
|
||||
this.gamemap = gamemap; // 方便调用地图的有关参数
|
||||
this.cells = [new Cell(info.r,info.c)] // cells存放蛇的坐标,cells[0]存放蛇头
|
||||
|
||||
this.speed = 5; // 定义蛇的速度,每秒走5个格子
|
||||
this.direction = -1; // -1表示没有指令,0,1,2,3分别表示左右下左
|
||||
this.status = "idle"; // idle表示静止,move表示移动,die表示死亡
|
||||
this.next_cell = null; // 下一步的目标位置
|
||||
|
||||
// 0,1,2,3:上右下左
|
||||
this.dr = [-1,0,1,0]; // 行方向的偏移量
|
||||
this.dc = [0,1,0,-1]; // 列方向的偏移量
|
||||
|
||||
this.step = 0; // 蛇当前的回合数
|
||||
this.eps = 1e-2; // 允许误差为0.01
|
||||
|
||||
this.eye_direction = 0; // 左下角的蛇的眼睛朝上
|
||||
|
||||
if (this.id === 1) this.eye_direction = 2; // 右上角的蛇的眼睛朝下
|
||||
|
||||
// 定义蛇左右眼的x和y坐标的偏移量
|
||||
this.eyes_dx = [ |
||||
[-1,1], |
||||
[1,1], |
||||
[-1,1], |
||||
[-1,-1] |
||||
]; |
||||
|
||||
this.eyes_dy = [ |
||||
[-1,-1], |
||||
[-1,1], |
||||
[1,1], |
||||
[-1,1] |
||||
]; |
||||
|
||||
} |
||||
|
||||
start() { // 第一帧调用该函数
|
||||
|
||||
} |
||||
|
||||
update() { // 更新下一帧
|
||||
if (this.status === "move") { // 只有下一步的状态为移动才可以移动
|
||||
this.update_move(); // 蛇进行移动
|
||||
} |
||||
this.render(); |
||||
} |
||||
|
||||
check_tail_increasing() { // 前10步每步增加1,后面每三步增加1
|
||||
if (this.step <= 10) return true; |
||||
if (this.step % 3 === 1) return true; |
||||
return false; |
||||
} |
||||
|
||||
next_step() { // 将蛇的状态置为下一步
|
||||
const d = this.direction; |
||||
// 更新下一步的位置
|
||||
this.next_cell = new Cell(this.cells[0].r+this.dr[d],this.cells[0].c + this.dc[d]); |
||||
this.direction = -1; // 清空当前方向
|
||||
this.status = "move"; // 改为移动
|
||||
this.eye_direction = d; // 更新蛇的眼睛方向
|
||||
this.step++; // 回合数加一
|
||||
|
||||
const k = this.cells.length; |
||||
for (let i = k; i > 0; i--) { |
||||
// 每个数组元素先后移动一位,相当于复制了一份第一个元素
|
||||
// 1,2,3 => 1,1,2,3
|
||||
this.cells[i] = JSON.parse(JSON.stringify(this.cells[i-1]));
|
||||
} |
||||
} |
||||
|
||||
set_direction(d) { // 通过读取用户输入来设置方向
|
||||
this.direction = d; |
||||
} |
||||
|
||||
update_move() { // 蛇进行移动
|
||||
// // 蛇向右移动5个格子
|
||||
// this.cells[0].x += this.speed * this.timedelta / 1000; // 注意每一帧不一定是1s
|
||||
|
||||
const dx = this.next_cell.x - this.cells[0].x; |
||||
const dy = this.next_cell.y - this.cells[0].y; |
||||
|
||||
const distance = Math.sqrt(dx * dx+dy * dy);
|
||||
if (distance < this.eps) { // 移动到了目标点
|
||||
this.cells[0] = this.next_cell; // 添加一个新蛇头
|
||||
this.next_cell = null; |
||||
this.status = "idle"; // 走完了,停下来
|
||||
if (!this.check_tail_increasing()) { // 蛇不变长,去掉蛇尾
|
||||
this.cells.pop(); // 弹出蛇尾
|
||||
} |
||||
} |
||||
else { |
||||
const move_distance = this.speed * this.timedelta / 1000; //移动距离
|
||||
this.cells[0].x += move_distance * dx / distance; |
||||
this.cells[0].y += move_distance * dy / distance; |
||||
|
||||
if (!this.check_tail_increasing()) { // 蛇不变长,蛇尾需要移动到目标位置
|
||||
const k = this.cells.length; |
||||
const tail = this.cells[k-1],tail_target = this.cells[k-2]; |
||||
const tail_dx = tail_target.x - tail.x; |
||||
const tail_dy = tail_target.y - tail.y; |
||||
tail.x += move_distance * tail_dx / distance; |
||||
tail.y += move_distance * tail_dy / distance; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
|
||||
render() {
|
||||
const L = this.gamemap.L; // 单位距离
|
||||
const ctx = this.gamemap.ctx; // 画布
|
||||
|
||||
ctx.fillStyle = this.color; // 填充颜色
|
||||
|
||||
if (this.status === "die") { |
||||
ctx.fillStyle = "white"; |
||||
} |
||||
|
||||
for (const cell of this.cells) { // 将蛇用圆表示
|
||||
ctx.beginPath(); |
||||
// 前两个参数时圆心坐标,后面的
|
||||
ctx.arc(cell.x * L,cell.y * L,L / 2 * 0.8, 0, Math.PI * 2); |
||||
ctx.fill(); |
||||
} |
||||
|
||||
for (let i = 1; i < this.cells.length; i++) { |
||||
const a = this.cells[i-1],b = this.cells[i]; |
||||
if (Math.abs(a.x - b.x) < this.eps && Math.abs(a.y-b.y) < this.eps) { |
||||
continue; |
||||
} |
||||
|
||||
if (Math.abs(a.x - b.x) < this.eps) { |
||||
// 矩形起始点的 x轴坐标、y轴坐标、矩形width、矩形height
|
||||
// 用Math.min()是因为这两个矩阵上下关系不知道
|
||||
ctx.fillRect((a.x - 0.4) * L,Math.min(a.y,b.y) * L,L*0.8,Math.abs(a.y-b.y) * L); |
||||
} |
||||
else { |
||||
ctx.fillRect(Math.min(a.x,b.x) * L,(a.y - 0.4) * L,Math.abs(a.x-b.x) * L,L*0.8); |
||||
} |
||||
} |
||||
|
||||
|
||||
// 画出蛇的眼睛
|
||||
|
||||
ctx.fillStyle = "black"; |
||||
|
||||
for (let i = 0; i < 2 ; i++) { |
||||
const eye_x = (this.cells[0].x + this.eyes_dx[this.eye_direction][i] * 0.2) * L; |
||||
const eye_y = (this.cells[0].y + this.eyes_dy[this.eye_direction][i] * 0.2) * L; |
||||
|
||||
// 画眼睛
|
||||
|
||||
ctx.beginPath(); |
||||
ctx.arc(eye_x,eye_y,L*0.05,0,Math.PI*2); |
||||
ctx.fill(); |
||||
} |
||||
|
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
import { AcGameObject } from "./AcGameObject"; |
||||
|
||||
export class Wall extends AcGameObject { |
||||
constructor(r, c, gamemap) { |
||||
super(); |
||||
|
||||
this.r = r; |
||||
this.c = c; |
||||
this.gamemap = gamemap; |
||||
this.color = "#B37226"; // 障碍物的颜色
|
||||
} |
||||
|
||||
update() { |
||||
this.render(); |
||||
} |
||||
|
||||
render() { |
||||
const L = this.gamemap.L; |
||||
const ctx = this.gamemap.ctx; |
||||
|
||||
ctx.fillStyle = this.color; |
||||
ctx.fillRect(this.c * L, this.r * L, L, L); // 将对应位置填充为障碍物
|
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
<template> |
||||
<div class="content-field"> |
||||
<slot></slot> |
||||
<div class="back" @click="handleClickBack">返回</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { useStore } from "vuex"; |
||||
|
||||
export default{ |
||||
setup() { |
||||
const store = useStore(); |
||||
const handleClickBack = () => { |
||||
store.commit("updateRouterName","menu"); |
||||
} |
||||
|
||||
return { |
||||
handleClickBack, |
||||
} |
||||
}, |
||||
} |
||||
</script> |
||||
|
||||
|
||||
<style scoped> |
||||
div.content-field { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
div.back { |
||||
position: absolute; |
||||
right: 5vw; |
||||
bottom: 5vh; |
||||
cursor: pointer; |
||||
font-size: 24px; |
||||
font-weight: bold; |
||||
font-style: italic; |
||||
color: white; |
||||
user-select: none; |
||||
|
||||
} |
||||
|
||||
div.back:hover { |
||||
scale: 1.2; |
||||
transition: 400ms; |
||||
} |
||||
</style> |
@ -0,0 +1,38 @@ |
||||
<template> |
||||
<div ref="parent" class="gamemap"> <!--使用下面返回的parent--> |
||||
<!--tabindex=0获取用户的输入操作--> |
||||
<canvas ref="canvas" tabindex="0"></canvas> <!--使用下面返回的canvas--> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { GameMap } from "@/assets/scripts/GameMap"; // 从js中导入GameMap类(public) |
||||
import { ref, onMounted } from 'vue' |
||||
import { useStore } from 'vuex' |
||||
|
||||
export default { |
||||
setup() { |
||||
let parent = ref(null); |
||||
let canvas = ref(null); |
||||
const store = useStore(); |
||||
onMounted(() => { |
||||
store.commit("updateGameObject", new GameMap(canvas.value.getContext('2d'), parent.value,store)); |
||||
}); |
||||
|
||||
return { |
||||
parent, |
||||
canvas |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
div.gamemap { |
||||
width: 100%; |
||||
height: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
</style> |
@ -1,58 +0,0 @@ |
||||
<template> |
||||
<div class="hello"> |
||||
<h1>{{ msg }}</h1> |
||||
<p> |
||||
For a guide and recipes on how to configure / customize this project,<br> |
||||
check out the |
||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>. |
||||
</p> |
||||
<h3>Installed CLI Plugins</h3> |
||||
<ul> |
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li> |
||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li> |
||||
</ul> |
||||
<h3>Essential Links</h3> |
||||
<ul> |
||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li> |
||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li> |
||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li> |
||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li> |
||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li> |
||||
</ul> |
||||
<h3>Ecosystem</h3> |
||||
<ul> |
||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li> |
||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li> |
||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li> |
||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li> |
||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li> |
||||
</ul> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: 'HelloWorld', |
||||
props: { |
||||
msg: String |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only --> |
||||
<style scoped> |
||||
h3 { |
||||
margin: 40px 0 0; |
||||
} |
||||
ul { |
||||
list-style-type: none; |
||||
padding: 0; |
||||
} |
||||
li { |
||||
display: inline-block; |
||||
margin: 0 10px; |
||||
} |
||||
a { |
||||
color: #42b983; |
||||
} |
||||
</style> |
@ -0,0 +1,199 @@ |
||||
<template> |
||||
<div class="matchground_field"> |
||||
<div class="matchground"> |
||||
<div class="matchground-head"> |
||||
<div> |
||||
<div class="user_photo"> |
||||
<img :src="$store.state.user.photo" alt="" /> |
||||
</div> |
||||
<div class="username"> |
||||
{{ $store.state.user.username }} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="select-bot"> |
||||
<select |
||||
class="form-select" |
||||
v-model="select_bot" |
||||
aria-label="Default select example" |
||||
> |
||||
<option value="-1">用户出战</option> |
||||
<option v-for="bot in bots" :key="bot.id" :value="bot.id"> |
||||
{{ bot.title }} |
||||
</option> |
||||
</select> |
||||
</div> |
||||
|
||||
<div> |
||||
<div class="user_photo"> |
||||
<img :src="$store.state.pk.opponent_photo" alt="" /> |
||||
</div> |
||||
<div class="username"> |
||||
{{ $store.state.pk.opponent_username }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="mybtn"> |
||||
<button |
||||
type="button" |
||||
v-if="is_matched" |
||||
class="match-btn" |
||||
@click="click_match_btn" |
||||
> |
||||
{{ match_btn_info }} |
||||
</button> |
||||
<button |
||||
type="button" |
||||
v-else |
||||
class="cancel-btn" |
||||
@click="click_match_btn" |
||||
> |
||||
{{ match_btn_info }} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { ref } from "vue"; |
||||
import { useStore } from "vuex"; |
||||
import $ from "jquery"; |
||||
|
||||
export default { |
||||
setup() { |
||||
const store = useStore(); |
||||
|
||||
let match_btn_info = ref("开始匹配"); |
||||
let is_matched = ref(true); |
||||
let bots = ref([]); |
||||
let select_bot = ref("-1"); |
||||
|
||||
const click_match_btn = () => { |
||||
if (match_btn_info.value === "开始匹配") { |
||||
match_btn_info.value = "取消匹配"; |
||||
is_matched.value = false; |
||||
console.log(select_bot.value); |
||||
// 给后端发送一个json信息表示当前属于哪个事件 |
||||
store.state.pk.socket.send( |
||||
JSON.stringify({ |
||||
event: "start-matching", |
||||
bot_id: select_bot.value, |
||||
}) |
||||
); |
||||
} else { |
||||
match_btn_info.value = "开始匹配"; |
||||
is_matched.value = true; |
||||
store.state.pk.socket.send( |
||||
JSON.stringify({ |
||||
event: "stop-matching", |
||||
}) |
||||
); |
||||
} |
||||
}; |
||||
|
||||
const refresh_bots = () => { |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/bot/getlist/", |
||||
type: "GET", |
||||
headers: { |
||||
Authorization: "Bearer " + store.state.user.token, // 任何需要登录才能显示的都要加这个验证 |
||||
}, |
||||
success(resp) { |
||||
// 将后端得到的数据传给bot |
||||
bots.value = resp; |
||||
}, |
||||
}); |
||||
}; |
||||
refresh_bots(); |
||||
return { |
||||
match_btn_info, |
||||
is_matched, |
||||
click_match_btn, |
||||
bots, |
||||
select_bot, |
||||
}; |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
|
||||
<style scoped> |
||||
div.matchground_field { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
div.matchground { |
||||
width: 60%; |
||||
height: 60%; |
||||
background-color: rgba(123, 197, 212, 0.5); |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-around; |
||||
} |
||||
|
||||
div.matchground-head { |
||||
display: flex; |
||||
justify-content: space-evenly; |
||||
} |
||||
|
||||
img { |
||||
margin-top: 10vh; |
||||
border-radius: 50%; |
||||
width: 12vh; |
||||
} |
||||
|
||||
.user_photo { |
||||
text-align: center; |
||||
} |
||||
|
||||
.username { |
||||
margin-top: 30px; |
||||
font-size: 20px; |
||||
font-weight: bold; |
||||
color: black; |
||||
text-align: center; |
||||
} |
||||
|
||||
.select-bot { |
||||
width: 15vw; |
||||
text-align: center; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.select-bot > select { |
||||
font-size: 18px; |
||||
width: 20vw; |
||||
border-radius: 5px; |
||||
height: 6vh; |
||||
} |
||||
|
||||
.mybtn { |
||||
text-align: center; |
||||
margin-bottom: 10vh; |
||||
} |
||||
|
||||
.mybtn > .match-btn { |
||||
background: #198754; |
||||
border: none; |
||||
border-color: #198754; |
||||
font-size: 20px; |
||||
padding: 5px 5px; |
||||
border-radius: 5px; |
||||
} |
||||
|
||||
.mybtn > .cancel-btn { |
||||
background: #dc3545; |
||||
border: none; |
||||
border-color: #dc3545; |
||||
font-size: 20px; |
||||
padding: 5px 5px; |
||||
border-radius: 5px; |
||||
} |
||||
</style> |
@ -0,0 +1,24 @@ |
||||
<template> |
||||
<div class="playground"> |
||||
<GameMap /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import GameMap from './GameMap.vue' // 导入GameMap组件(.vue结尾) |
||||
|
||||
export default { |
||||
components: { // 使用GameMap组件 |
||||
GameMap, |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
/* div标签的样式 */ |
||||
div.playground { |
||||
width: 60vw; |
||||
height: 70vh; |
||||
margin: 40px auto; |
||||
} |
||||
</style> |
@ -0,0 +1,78 @@ |
||||
<template> |
||||
<div class="result-board"> |
||||
<div class="result-text" v-if="$store.state.pk.loser === 'all'"> |
||||
Draw |
||||
</div> |
||||
|
||||
<div class="result-text" v-else-if="$store.state.pk.loser === 'A' && $store.state.pk.a_id === parseInt($store.state.user.id)"> |
||||
Lose |
||||
</div> |
||||
|
||||
<div class="result-text" v-else-if="$store.state.pk.loser === 'B' && $store.state.pk.b_id == parseInt($store.state.user.id)"> |
||||
Lose |
||||
</div> |
||||
|
||||
<div class="result-text" v-else> |
||||
You Win |
||||
</div> |
||||
|
||||
<div class="reset-btn"> |
||||
<button @click="reset" class="btn btn-warning lg">再来一次!</button> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
|
||||
import { useStore } from 'vuex'; |
||||
|
||||
export default { |
||||
setup() { |
||||
const store = useStore(); |
||||
|
||||
const reset = () => { |
||||
store.commit("updateStatus","matching"); |
||||
store.commit("updateLoser","none"); |
||||
store.commit("updateOpponent", { |
||||
username: "我的对手", |
||||
photo: |
||||
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png", |
||||
}); |
||||
} |
||||
|
||||
return { |
||||
reset, |
||||
} |
||||
} |
||||
|
||||
} |
||||
</script> |
||||
|
||||
|
||||
<style scoped> |
||||
.result-board { |
||||
width: 30vw; |
||||
height: 30vh; |
||||
background-color: rgba(34, 20, 77, 0.5); |
||||
position: absolute; |
||||
top: 30vh; |
||||
left: 35vw; |
||||
} |
||||
|
||||
.result-text { |
||||
font-size: 50px; |
||||
color:yellowgreen; |
||||
font-weight: bold; |
||||
text-align: center; |
||||
font-style: italic; |
||||
padding-top: 4vh; |
||||
padding-bottom: 4vh; |
||||
} |
||||
|
||||
.reset-btn { |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
|
||||
} |
||||
</style> |
@ -0,0 +1,22 @@ |
||||
<template> |
||||
<div class="show"> |
||||
<div class="snakeInfo" style="background-color: #4876ec;" v-if="$store.state.user.id == $store.state.pk.a_id">蓝方</div> |
||||
<div class="snakeInfo" style="background-color: #f94848;" v-if="$store.state.user.id == $store.state.pk.b_id">红方</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.snakeInfo { |
||||
width: 50px; |
||||
height: 50px; |
||||
border-radius: 50%; |
||||
position: absolute; |
||||
top: 30vh; |
||||
left: 8vw; |
||||
text-align: center; |
||||
line-height: 50px; |
||||
} |
||||
</style> |
@ -0,0 +1,52 @@ |
||||
export default { |
||||
state: { // 全局变量
|
||||
status: "matching", // matching表示匹配界面,playing表示对战界面
|
||||
socket: null, |
||||
opponent_username: "", //对手的用户名
|
||||
opponent_photo: "", // 对手的头像
|
||||
gamemap: null, // 对战的地图
|
||||
a_id: 0, |
||||
a_sx: 0, |
||||
a_sy: 0, |
||||
b_id: 0, |
||||
b_sx: 0, |
||||
b_sy: 0, |
||||
gameObject: null, |
||||
loser: "none" //"none","all","A","B" none表示还没有结果
|
||||
}, |
||||
getters: { |
||||
}, |
||||
mutations: { // 用于修改全局数据
|
||||
updateSocket(state, socket) { |
||||
state.socket = socket; |
||||
}, |
||||
updateOpponent(state, opponent) { |
||||
state.opponent_username = opponent.username; |
||||
state.opponent_photo = opponent.photo; |
||||
}, |
||||
updateStatus(state, status) { |
||||
state.status = status; |
||||
}, |
||||
updateGame(state, game) { |
||||
state.gamemap = game.map; |
||||
state.a_id = game.a_id; |
||||
state.a_sx = game.a_sx; |
||||
state.a_sy = game.a_sy; |
||||
state.b_id = game.b_id; |
||||
state.b_sx = game.b_sx; |
||||
state.b_sy = game.b_sy; |
||||
|
||||
}, |
||||
updateGameObject(state, gameObject) { |
||||
state.gameObject = gameObject; |
||||
}, |
||||
updateLoser(state, loser) { |
||||
state.loser = loser; |
||||
} |
||||
}, |
||||
actions: { // 在actions中调用修改全局变量的函数
|
||||
|
||||
}, |
||||
modules: { |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
export default { |
||||
state: { |
||||
is_record: false, |
||||
a_steps: "", |
||||
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,record_loser) { |
||||
state.record_loser = record_loser; |
||||
} |
||||
|
||||
}, |
||||
actions: {
|
||||
|
||||
}, |
||||
modules: { |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
export default { |
||||
state: { |
||||
router_name: "menu", // menu,pk,record,record_content,ranklist,user_bot
|
||||
}, |
||||
getters: { |
||||
|
||||
}, |
||||
mutations: {
|
||||
updateRouterName(state, router_name) { |
||||
state.router_name = router_name; |
||||
}, |
||||
}, |
||||
actions: {
|
||||
|
||||
}, |
||||
modules: { |
||||
} |
||||
} |
@ -0,0 +1,94 @@ |
||||
import $ from 'jquery' |
||||
|
||||
export default { |
||||
state: { // 全局变量
|
||||
id: "", |
||||
username: "", |
||||
photo: "", |
||||
token: "", |
||||
is_login: false, // 默认未登录状态
|
||||
pulling_info: true, // 是否正在拉取信息
|
||||
}, |
||||
getters: { |
||||
}, |
||||
mutations: { // 用于修改全局数据
|
||||
updateUser(state,user) { // 更新用户信息
|
||||
state.id = user.id; |
||||
state.username = user.username; |
||||
state.photo = user.photo; |
||||
state.is_login = user.is_login; |
||||
}, |
||||
updateToken(state,token) { // 更新token
|
||||
state.token = token; |
||||
}, |
||||
logout(state) { |
||||
state.id = ""; |
||||
state.username = ""; |
||||
state.photo = ""; |
||||
state.token = ""; |
||||
state.is_login = false; |
||||
}, |
||||
updatePullingInfo(state,pulling_info) { |
||||
state.pulling_info = pulling_info; |
||||
} |
||||
}, |
||||
actions: { // 在actions中调用修改全局变量的函数
|
||||
login(context,data) { |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/account/token/", |
||||
type : "POST", |
||||
data: { |
||||
username: data.username, |
||||
password: data.password, |
||||
}, |
||||
success(resp) {
|
||||
// resp对应的就是后端的map的json格式,每个字段对应键值对中的key
|
||||
// 登陆成功
|
||||
if(resp.error_msg === "success"){ |
||||
// 调用updateToken函数,形参中的data为resp.token
|
||||
// 将登陆信息的token存入localStorage实现持久化(浏览器刷新退出登陆)
|
||||
localStorage.setItem("jwt_token",resp.token); |
||||
context.commit("updateToken",resp.token); |
||||
data.success(resp); |
||||
}else { |
||||
data.error(resp); |
||||
} |
||||
}, |
||||
error(resp) { |
||||
data.error(resp); |
||||
}, |
||||
}); |
||||
}, |
||||
getInfo(context,data) { |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/account/info/", |
||||
type: "GET", |
||||
headers: { |
||||
Authorization: "Bearer " + context.state.token, |
||||
}, |
||||
success(resp) {
|
||||
if (resp.error_msg === "success"){ |
||||
// 登陆成功后,更新当前用户信息
|
||||
context.commit("updateUser",{ |
||||
...resp, |
||||
is_login: true, |
||||
}); |
||||
data.success(resp); // 调用UserLoginView中的success()回调函数
|
||||
}else { |
||||
data.error(resp); // 调用UserLoginView中的error()回调函数
|
||||
} |
||||
}, |
||||
error(resp) { |
||||
data.error(resp); |
||||
}, |
||||
}); |
||||
}, |
||||
logout(context) { |
||||
// 退出时删除localStorage保存的token
|
||||
localStorage.removeItem("jwt_token"); |
||||
context.commit("logout"); // 退出登录
|
||||
} |
||||
}, |
||||
modules: { |
||||
} |
||||
} |
@ -0,0 +1,78 @@ |
||||
<template> |
||||
<div class="menu-field"> |
||||
<div class="menu"> |
||||
<div class="menu-item" @click="handlePkClick">对战</div> |
||||
<div class="menu-item" @click="handleRecordClick">对局列表</div> |
||||
<div class="menu-item" @click="handleRankListClick">排行榜</div> |
||||
<div class="menu-item" @click="handleUserBotClick">我的bot</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import { useStore } from "vuex"; |
||||
|
||||
export default { |
||||
setup() { |
||||
const store = useStore(); |
||||
const handlePkClick = () => { |
||||
store.commit("updateRouterName","pk"); |
||||
}; |
||||
|
||||
const handleRecordClick = () => { |
||||
store.commit("updateRouterName","record"); |
||||
}; |
||||
|
||||
const handleRankListClick = () => { |
||||
store.commit("updateRouterName","ranklist"); |
||||
}; |
||||
|
||||
const handleUserBotClick = () => { |
||||
store.commit("updateRouterName","user_bot"); |
||||
}; |
||||
|
||||
return { |
||||
handlePkClick, |
||||
handleRecordClick, |
||||
handleRankListClick, |
||||
handleUserBotClick, |
||||
} |
||||
} |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<style scoped> |
||||
div.menu-field { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
height: 100%; |
||||
width: 100%; |
||||
} |
||||
|
||||
div.menu { |
||||
width: 25vw; |
||||
height: 30vh; |
||||
background-color: rgba(0, 0, 0, 0.3); |
||||
} |
||||
|
||||
div.menu-item { |
||||
width: 100%; |
||||
height: 7.5vh; |
||||
line-height: 7.5vh; |
||||
font-size: 24px; |
||||
color: white; |
||||
text-align: center; |
||||
font-style: italic; |
||||
font-weight: bold; |
||||
cursor: pointer; |
||||
user-select: none; |
||||
} |
||||
|
||||
div.menu-item:hover { |
||||
scale: 1.2; |
||||
transition: 400ms; |
||||
} |
||||
|
||||
</style> |
@ -0,0 +1,99 @@ |
||||
<template> |
||||
<ContentField> |
||||
<PlayGround v-if="$store.state.pk.status === 'playing'" /> |
||||
<MatchGround v-if="$store.state.pk.status === 'matching'" /> |
||||
<ResultBoard v-if="$store.state.pk.loser !== 'none'" /> |
||||
<UserInfo v-if="$store.state.pk.status === 'playing'" /> |
||||
</ContentField> |
||||
</template> |
||||
|
||||
<script> |
||||
import PlayGround from "../../components/PlayGround.vue"; |
||||
import MatchGround from "../../components/MatchGround.vue"; |
||||
import ResultBoard from "@/components/ResultBoard.vue"; |
||||
import UserInfo from "@/components/UserInfo.vue"; |
||||
import { onMounted, onUnmounted } from "vue"; // 加载组件和卸载组件 |
||||
import { useStore } from "vuex"; |
||||
import ContentField from "@/components/ContentField.vue"; |
||||
|
||||
export default { |
||||
components: { |
||||
PlayGround, |
||||
MatchGround, |
||||
ResultBoard, |
||||
UserInfo, |
||||
ContentField, |
||||
}, |
||||
setup() { |
||||
const store = useStore(); |
||||
const socket_url = `wss://kob.bnblogs.cc/websocket/${store.state.user.token}/`; |
||||
|
||||
store.commit("updateLoser", "none"); |
||||
store.commit("updateIsRecord", false); |
||||
|
||||
let socket = null; |
||||
// 当前组件被加载(点击pk页面)时执行该函数 |
||||
// onMounted和setup作用相似 |
||||
onMounted(() => { |
||||
store.commit("updateOpponent", { |
||||
username: "我的对手", |
||||
photo: |
||||
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png", |
||||
}); |
||||
// 建立一个socket链接 |
||||
socket = new WebSocket(socket_url); |
||||
|
||||
socket.onopen = () => { |
||||
store.commit("updateSocket", socket); |
||||
console.log("connnected!"); |
||||
}; |
||||
|
||||
// 接收到后端发来的信息 |
||||
socket.onmessage = (msg) => { |
||||
const data = JSON.parse(msg.data); |
||||
|
||||
// 匹配成功 |
||||
if (data.event === "start-matching") { |
||||
store.commit("updateOpponent", { |
||||
username: data.opponent_username, |
||||
photo: data.opponent_photo, |
||||
}); |
||||
|
||||
setTimeout(() => { |
||||
store.commit("updateStatus", "playing"); |
||||
}, 200); // 0.2秒后更新地图(修复穿墙bug) |
||||
|
||||
// 更新游戏信息(包括地图、玩家坐标和id) |
||||
store.commit("updateGame", data.game); |
||||
} else if (data.event === "move") { |
||||
const game = store.state.pk.gameObject; |
||||
const [snake0, snake1] = game.snakes; |
||||
snake0.set_direction(data.a_direction); |
||||
snake1.set_direction(data.b_direction); |
||||
} else if (data.event === "result") { |
||||
const game = store.state.pk.gameObject; |
||||
const [snake0, snake1] = game.snakes; |
||||
if (data.loser == "all" || data.loser == "A") { |
||||
snake0.status = "die"; |
||||
} |
||||
if (data.loser == "all" || data.loser == "B") { |
||||
snake1.status = "die"; |
||||
} |
||||
store.commit("updateLoser", data.loser); |
||||
} |
||||
}; |
||||
|
||||
socket.onclose = () => { |
||||
console.log("disconnected!"); |
||||
store.commit("updateStatus", "matching"); |
||||
}; |
||||
}); |
||||
// 切换其他页面或者关闭pk页面时调用 |
||||
onUnmounted(() => { |
||||
socket.close(); |
||||
}); |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,133 @@ |
||||
<template> |
||||
<ContentField> |
||||
<table class="table table-striped table-hover table-sm" style="text-align: center;"> |
||||
<thead> |
||||
<tr> |
||||
<th>玩家</th> |
||||
<th>积分</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr v-for="user in users" :key="user.id" class=align-middle> |
||||
<td> |
||||
<img :src="user.photo" alt="" class="user_photo"> |
||||
<div class="username userA">{{user.username}}</div> |
||||
</td> |
||||
|
||||
<td> |
||||
{{user.rating}} |
||||
</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> |
||||
|
||||
</ContentField> |
||||
</template> |
||||
|
||||
<script> |
||||
import ContentField from '../../components/ContentField.vue'; |
||||
import { useStore } from 'vuex'; |
||||
import $ from 'jquery'; |
||||
import { ref } from 'vue'; |
||||
|
||||
export default { |
||||
components: { |
||||
ContentField |
||||
}, |
||||
setup() { |
||||
const store = useStore(); |
||||
let currentPage = 1; |
||||
let total_users = 0; // 保存总对局数 |
||||
let users = ref([]); // 保存对战记录 |
||||
let pages = ref([]); // 展示的多个对战页面 |
||||
|
||||
const updatePages = () => { |
||||
let max_pages = parseInt(Math.ceil(total_users / 8)); |
||||
let new_pages = []; |
||||
for (let i = currentPage - 2; i <= currentPage + 2; i++) { |
||||
if (i >= 1 && i <= max_pages) { |
||||
new_pages.push({ |
||||
number: i, |
||||
is_active: i === currentPage ? "active" : "", |
||||
}) |
||||
} |
||||
} |
||||
pages.value = new_pages; |
||||
} |
||||
|
||||
const click_page = page => { |
||||
if (page === -2) page = currentPage - 1; |
||||
else if (page === -1) page = currentPage + 1; |
||||
let max_pages = parseInt(Math.ceil(total_users / 8)); |
||||
|
||||
if (page >= 1 && page <= max_pages) { |
||||
pull_page(page); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
const pull_page = page => { |
||||
currentPage = page; |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/ranklist/getlist/", |
||||
type: "GET", |
||||
data: { |
||||
page |
||||
}, |
||||
headers:{ |
||||
"Authorization": "Bearer " + store.state.user.token, // 任何需要登录才能显示的都要加这个验证 |
||||
}, |
||||
success(resp){ |
||||
users.value = resp.users; |
||||
total_users = resp.users_count; |
||||
updatePages(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
pull_page(currentPage); |
||||
|
||||
return { |
||||
click_page, |
||||
users, |
||||
pages, |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.user_photo{ |
||||
width: 4vh; |
||||
border-radius: 50%; |
||||
} |
||||
.username { |
||||
font-weight: bold; |
||||
font-size: 1vw; |
||||
} |
||||
|
||||
.userA { |
||||
color: blue; |
||||
} |
||||
|
||||
.userB { |
||||
color: red; |
||||
} |
||||
|
||||
</style> |
@ -0,0 +1,19 @@ |
||||
<template> |
||||
<PlayGround /> |
||||
</template> |
||||
|
||||
<script> |
||||
import PlayGround from "../../components/PlayGround.vue"; |
||||
|
||||
export default { |
||||
components: { |
||||
PlayGround, |
||||
}, |
||||
setup() { |
||||
|
||||
} |
||||
|
||||
} |
||||
</script> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,197 @@ |
||||
<template> |
||||
<ContentField> |
||||
<table class="table table-striped table-hover table-sm align-middle" style="text-align: center;"> |
||||
<thead> |
||||
<tr> |
||||
<th>player A</th> |
||||
<th>player B</th> |
||||
<th>对战结果</th> |
||||
<th>对战时间</th> |
||||
<th>操作</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
<tr v-for="record in records" :key="record.record.id" class=align-middle> |
||||
<td > |
||||
<img :src="record.a_photo" alt="" class="user_photo"> |
||||
<div class="username userA">{{record.a_username}}</div> |
||||
</td> |
||||
<td> |
||||
<img :src="record.b_photo" alt="" class="user_photo"> |
||||
<div class="username userB">{{record.b_username}}</div> |
||||
</td> |
||||
<td> |
||||
<div class="info"> |
||||
{{record.result}} |
||||
</div> |
||||
|
||||
</td> |
||||
|
||||
<td> |
||||
<div class="info"> |
||||
{{record.record.createTime}} |
||||
</div> |
||||
</td> |
||||
<td> |
||||
<button class="btn btn-info sm" @click="open_record_content(record.record.id)">查看录像</button> |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
|
||||
<nav aria-label="..." style="float: right;"> |
||||
<ul class="pagination"> |
||||
<li class="page-item"> |
||||
<a class="page-link" href="#" @click="click_page(-2)">上一页</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"> |
||||
<a class="page-link" href="#" @click="click_page(-1)">下一页</a> |
||||
</li> |
||||
</ul> |
||||
</nav> |
||||
</ContentField> |
||||
</template> |
||||
|
||||
<script> |
||||
import ContentField from '../../components/ContentField.vue'; |
||||
import { useStore } from 'vuex'; |
||||
import $ from 'jquery'; |
||||
import { ref } from 'vue'; |
||||
|
||||
export default { |
||||
components: { |
||||
ContentField |
||||
}, |
||||
setup() { |
||||
const store = useStore(); |
||||
let currentPage = 1; |
||||
let total_records = 0; // 保存总对局数 |
||||
let records = ref([]); // 保存对战记录 |
||||
let pages = ref([]); // 展示的多个对战页面 |
||||
|
||||
const updatePages = () => { |
||||
let max_pages = parseInt(Math.ceil(total_records / 8)); |
||||
let new_pages = []; |
||||
for (let i = currentPage - 2; i <= currentPage + 2; i++) { |
||||
if (i >= 1 && i <= max_pages) { |
||||
new_pages.push({ |
||||
number: i, |
||||
is_active: i === currentPage ? "active" : "", |
||||
}) |
||||
} |
||||
} |
||||
pages.value = new_pages; |
||||
} |
||||
|
||||
const click_page = page => { |
||||
if (page === -2) page = currentPage - 1; |
||||
else if (page === -1) page = currentPage + 1; |
||||
let max_pages = parseInt(Math.ceil(total_records / 8)); |
||||
|
||||
if (page >= 1 && page <= max_pages) { |
||||
pull_page(page); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
const pull_page = page => { |
||||
currentPage = page; |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/record/list/", |
||||
type: "GET", |
||||
data: { |
||||
page |
||||
}, |
||||
headers:{ |
||||
"Authorization": "Bearer " + store.state.user.token, // 任何需要登录才能显示的都要加这个验证 |
||||
}, |
||||
success(resp){ |
||||
records.value = resp.records; |
||||
total_records = resp.records_count; |
||||
updatePages(); |
||||
} |
||||
}) |
||||
} |
||||
|
||||
pull_page(currentPage); |
||||
|
||||
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; |
||||
} |
||||
|
||||
|
||||
// 打开对应recordId保存的录像 |
||||
const open_record_content = (recordId) => { |
||||
for (const record of records.value) { |
||||
if (record.record.id === recordId) { |
||||
store.commit("updateIsRecord",true); // 播放录像 |
||||
// 恢复游戏信息 |
||||
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, |
||||
}); |
||||
|
||||
// 恢复蛇的移动信息 |
||||
store.commit("updateSteps",{ |
||||
a_steps: record.record.asteps, |
||||
b_steps: record.record.bsteps, |
||||
}) |
||||
|
||||
// 恢复对局的败者 |
||||
store.commit("updateRecordLoser",record.record.loser); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
return { |
||||
open_record_content, |
||||
click_page, |
||||
records, |
||||
pages, |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.user_photo{ |
||||
width: 4vh; |
||||
border-radius: 50%; |
||||
} |
||||
.username { |
||||
font-weight: bold; |
||||
font-size: 1vw; |
||||
} |
||||
|
||||
.userA { |
||||
color: blue; |
||||
} |
||||
|
||||
.userB { |
||||
color: red; |
||||
} |
||||
|
||||
</style> |
@ -0,0 +1,279 @@ |
||||
<template> |
||||
<ContentField> |
||||
<div class="container"> |
||||
<div class="row"> |
||||
<div class="col-3"> |
||||
<div class="card" style="margin-top: 30px"> |
||||
<div class="card-body"> |
||||
<img :src="$store.state.user.photo" alt=""> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="col-9"> |
||||
<div class="card" style="margin-top: 30px"> |
||||
<div class="card-header" > |
||||
<span style="font-size: 20px; font-weight: bold ">我的bots</span> |
||||
<button type="button" class="btn btn-primary float-end" btn-sm data-bs-toggle="modal" data-bs-target="#add_bot_btn">创建bot</button> |
||||
<!-- Modal --> |
||||
<div class="modal fade" id="add_bot_btn" tabindex="-1"> |
||||
<div class="modal-dialog modal-lg"> |
||||
<div class="modal-content"> |
||||
<div class="modal-header"> |
||||
<h5 class="modal-title" id="exampleModalLabel">创建新的bot</h5> |
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||
</div> |
||||
<div class="modal-body"> |
||||
<form> |
||||
<div class="mb-3"> |
||||
<label for="add_bot_title" class="form-label">bot名称</label> |
||||
<input type="text" v-model="new_bot.title" class="form-control" id="add_bot_title" placeholder="请输入新bot的名称"> |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="add_bot_description" class="form-label">bot简介</label> |
||||
<textarea class="form-control" v-model="new_bot.description" id="add_bot_description" rows= "3" placeholder="请输入新bot的简介"></textarea> |
||||
</div> |
||||
|
||||
<div class="mb-3"> |
||||
<label for="add_bot_code" class="form-label">bot的代码</label> |
||||
<VAceEditor |
||||
v-model:value="new_bot.content" |
||||
@init="editorInit" |
||||
lang="c_cpp" |
||||
theme="textmate" |
||||
:options="{fontSize: 16}" |
||||
style="height: 300px" /> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
<div class="modal-footer"> |
||||
<div class="error_msg">{{ new_bot.error_msg }}</div> |
||||
<button type="button" class="btn btn-success" @click="add_bot" btn-sm>创建</button> |
||||
<button type="button" class="btn btn-danger" btn-sm @click="cancel_add">取消</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="card-body"> |
||||
<table class="table table-striped table-hover"> |
||||
<!--表头--> |
||||
<thead> |
||||
<tr> |
||||
<th>名称</th> |
||||
<th>创建时间</th> |
||||
<th>操作</th> |
||||
</tr> |
||||
</thead> |
||||
<!--内容--> |
||||
<tbody> |
||||
<tr v-for="bot in bots" :key="bot.id"> |
||||
<td>{{bot.title}}</td> |
||||
<td>{{bot.createTime}}</td> |
||||
<td> |
||||
<button type="button" class="btn btn-success" btn-sm style="margin-right: 10px" data-bs-toggle="modal" :data-bs-target="'#update-bot-btn-'+bot.id" >修改</button> |
||||
<button type="button" class="btn btn-danger" btn-sm @click="remove_bot(bot)">删除</button> |
||||
|
||||
<div class="modal fade" :id="'update-bot-btn-'+bot.id" tabindex="-1"> |
||||
<div class="modal-dialog modal-lg"> |
||||
<div class="modal-content"> |
||||
<div class="modal-header"> |
||||
<h5 class="modal-title">修改bot信息</h5> |
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||
</div> |
||||
<div class="modal-body"> |
||||
<form> |
||||
<div class="mb-3"> |
||||
<label for="add_bot_title" class="form-label">bot名称</label> |
||||
<input type="text" v-model="bot.title" class="form-control" id="add_bot_title" > |
||||
</div> |
||||
<div class="mb-3"> |
||||
<label for="add_bot_description" class="form-label">bot简介</label> |
||||
<textarea class="form-control" v-model="bot.description" id="add_bot_description" rows= "3" ></textarea> |
||||
</div> |
||||
|
||||
<div class="mb-3"> |
||||
<label for="add_bot_code" class="form-label">bot的代码</label> |
||||
<VAceEditor |
||||
v-model:value="bot.content" |
||||
@init="editorInit" |
||||
lang="c_cpp" |
||||
theme="textmate" |
||||
:options="{fontSize: 16}" |
||||
style="height: 300px" /> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
<div class="modal-footer"> |
||||
<div class="error_msg">{{ bot.error_msg }}</div> |
||||
<button type="button" class="btn btn-success" @click="update_bot(bot)" btn-sm>保存修改</button> |
||||
<button type="button" class="btn btn-danger" btn-sm @click="cancel_update(bot)">取消</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</ContentField> |
||||
</template> |
||||
|
||||
<script> |
||||
import { ref,reactive } from "vue" |
||||
import $ from 'jquery' |
||||
import { useStore } from "vuex" |
||||
import {Modal} from "bootstrap/dist/js/bootstrap" |
||||
import ContentField from "@/components/ContentField.vue" |
||||
import { VAceEditor } from 'vue3-ace-editor'; |
||||
import ace from 'ace-builds'; |
||||
|
||||
export default { |
||||
components:{ |
||||
VAceEditor, |
||||
ContentField, |
||||
}, |
||||
setup() { |
||||
ace.config.set( |
||||
"basePath", |
||||
"https://cdn.jsdelivr.net/npm/ace-builds@" + require('ace-builds').version + "/src-noconflict/"); |
||||
const store = useStore(); |
||||
let bots = ref([]) // 定义bot列表 |
||||
const new_bot = reactive({ |
||||
title: "", |
||||
description: "", |
||||
content: "", |
||||
error_msg: "", |
||||
}) |
||||
|
||||
const cancel_update = (bot)=> { |
||||
Modal.getInstance("#update-bot-btn-"+bot.id).hide(); |
||||
refresh_bots(); |
||||
} |
||||
|
||||
const cancel_add = ()=> { |
||||
Modal.getInstance("#add_bot_btn").hide(); |
||||
refresh_bots(); |
||||
} |
||||
|
||||
const refresh_bots = () => { |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/bot/getlist/", |
||||
type: "GET", |
||||
headers:{ |
||||
"Authorization": "Bearer " + store.state.user.token, // 任何需要登录才能显示的都要加这个验证 |
||||
}, |
||||
success(resp){ |
||||
// 将后端得到的数据传给bot |
||||
bots.value = resp; |
||||
} |
||||
}) |
||||
} |
||||
refresh_bots(); |
||||
|
||||
// 点击创建bot按钮触发的事件 |
||||
const add_bot = () => { |
||||
new_bot.error_msg = ""; |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/bot/add/", |
||||
type: "POST", |
||||
data: { |
||||
title: new_bot.title, |
||||
description: new_bot.description, |
||||
content: new_bot.content, |
||||
}, |
||||
headers:{ |
||||
"Authorization": "Bearer " + store.state.user.token, |
||||
}, |
||||
success(resp) { |
||||
if (resp.error_msg === "success") { // 添加bot成功 |
||||
new_bot.title = "", |
||||
new_bot.description="", |
||||
new_bot.content="", |
||||
Modal.getInstance("#add_bot_btn").hide(); |
||||
refresh_bots(); |
||||
} |
||||
else { |
||||
new_bot.error_msg = resp.error_msg; // 返回错误信息 |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
|
||||
// 点击更新bot按钮触发的事件 |
||||
const update_bot = (bot) => { |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/bot/update/", |
||||
type: "POST", |
||||
data: { |
||||
bot_id: bot.id, |
||||
title: bot.title, |
||||
description: bot.description, |
||||
content: bot.content, |
||||
}, |
||||
headers:{ |
||||
"Authorization": "Bearer " + store.state.user.token, |
||||
}, |
||||
success(resp) { |
||||
if (resp.error_msg === "success") { // 更新bot成功 |
||||
Modal.getInstance("#update-bot-btn-"+bot.id).hide(); |
||||
refresh_bots(); |
||||
} |
||||
else { |
||||
new_bot.error_msg = resp.error_msg; // 返回错误信息 |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 点击删除bot按钮触发的事件 |
||||
const remove_bot = (bot) => { |
||||
$.ajax({ |
||||
url: "https://kob.bnblogs.cc/api/user/bot/remove/", |
||||
type: "POST", |
||||
data: { |
||||
bot_id: bot.id |
||||
}, |
||||
headers:{ |
||||
"Authorization": "Bearer " + store.state.user.token, |
||||
}, |
||||
success(resp) { |
||||
console.log(resp) |
||||
if (resp.error_msg === "success") { // 添加bot成功 |
||||
refresh_bots(); |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
return { |
||||
bots, |
||||
new_bot, |
||||
add_bot, |
||||
remove_bot, |
||||
update_bot, |
||||
cancel_update, |
||||
cancel_add, |
||||
} |
||||
} |
||||
|
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
img{ |
||||
width: 100% |
||||
} |
||||
.error_msg{ |
||||
font-size: 20px; |
||||
font-weight: bold; |
||||
color: red; |
||||
} |
||||
</style> |
@ -1,4 +1,9 @@ |
||||
const { defineConfig } = require('@vue/cli-service') |
||||
module.exports = defineConfig({ |
||||
transpileDependencies: true |
||||
}) |
||||
transpileDependencies: true, |
||||
configureWebpack: { |
||||
optimization: { |
||||
splitChunks: false |
||||
} |
||||
} |
||||
}) |
Loading…
Reference in new issue