实现项目前端
前端模仿几次就会了
项目前端总体设计
导航栏的实现
组件
创建组件,在components
文件夹下面创建文件-表示组件。
例如在components
文件夹下创建NavBar.vue
(vue规定创建的项目名称要有两个大写字母组成)
对于每一个组件,包含三个部分:html
, css
, js
scoped
的作用是加入一个随机字符串,使得该组件不会影响到组件以外的内容
<template>
</template>
<script>
</script>
<style scoped>
</style>
BootStrap
可以让程序员很轻松的做美工的工作
进入BootStrap官网,可以对样式进行搜索,这里以实现导航栏为例,搜索NavBar
,然后选择一个合适的样式,复制到NavBar.vue
。
<template>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar w/ text</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
</ul>
<span class="navbar-text">
Navbar text with an inline element
</span>
</div>
</div>
</nav>
</template>
<script>
</script>
<style scoped>
//scoped的作用是加入一个随机字符串,使得该组件不会影响到组件以外的内容
</style>
在App.vue
中导入组件
安装@popperjs/core
依赖,导入BootStrap
的依赖,导入NavBar.vue
,并且使用components
关键字,并且在<templements>
中写入<NavBar/>
<template>
<NavBar/>
<router-view></router-view>
</template>
<script>
import NavBar from './components/NavBar.vue'
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap/dist/js/bootstrap"
export default{
components:{
NavBar
}
}
</script>
<style>
body{
background-image: url("@/assets/background.png");
background-size: cover;
}
</style>
实现效果:
对样式进行微调,下拉菜单依然到bootstrap
找样式(有没有想过为啥网站的导航栏都一样啊,都是自己写的吗,都不是~,都是bootstrap)
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="#">King of Bots</a>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link " aria-current="page" href="#">对战</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">对局列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">排行榜</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
hxw
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">我的Bot</a></li>
<li><a class="dropdown-item" href="#">退出</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
</script>
<style scoped>
</style>
实现页面跳转
页面的组件可以放到components
文件夹下面,也可以放在views
文件夹下面,一般习惯于放在views
文件夹下面。由于每一个页面又包含多个组件,所以每一个页面可以用一个文件夹表示。分别在文件夹下创建组件。取名方式如下图所示。
可以先在每个页面中写入文字区分,搭建完框架之后,再进一步完善。这样每一个页面的内容就完成了,下面考虑如何点击导航栏按钮,跳转到具体的页面。
在router
文件夹下的index.js
文件中添加所有页面
- 导入所有页面组件文件
- 地址和组件的绑定:定义路径对象,实现页面组件和地址的绑定,可以实现在指定路径下,显示对应页面组件的内容
- 导航栏的按钮和地址的绑定(不刷新):通过修改
NavBar.vue
中用router-link
标签替换<a>
标签
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">King of Bots</router-link>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link class="nav-link " :to="{name: 'pk_index'}" >对战</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name: 'record_index'}">对局列表</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" :to="{name:'ranklist_index'}">排行榜</router-link>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
hxw
</a>
<ul class="dropdown-menu">
<li><router-link class="dropdown-item" :to="{name:'user_bot_index'}">我的Bot</router-link></li>
<li><router-link class="dropdown-item" href="#">退出</router-link></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
</script>
<style scoped>
</style>
import { createRouter, createWebHistory } from 'vue-router'
import ErrorView from '../views/error/NotFound.vue'
import PkIndexView from '../views/pk/PkIndexView.vue'
import RankListView from '../views/ranklist/RankListIndexView.vue'
import RecordView from '../views/record/RecordIndexView.vue'
import UserBotIndexView from '../views/user/bot/UserBotIndexView.vue'
const routes = [
{
path:"/",
name:"home",
redirect:"/pk/"
// 重定向,当输入根目录的时候,自动跳转到pk页面
},
{
path: "/404/",
name: "not_found_index",
component: ErrorView,
},
{
path: "/pk/",
name: "pk_index",
component: PkIndexView,
},
{
path: "/ranklist/",
name: "ranklist_index",
component: RankListView,
},
{
path: "/record/",
name: "record_index",
component: RecordView,
},
{
path: "/user/bot/",
name: "user_bot_index",
component: UserBotIndexView,
},
{
//输入其他乱七八糟的路径,自动跳转到404
path: "/:catchAll()",
redirect: "/404/",
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
完善页面
- 给页面添加一个框框框起来
- 每一个页面都需要用一个框框,所以将框框独立出来作为一个单独的组件,在
componets
文件夹下创建ContentField.vue
- 在每一个页面中引入
ContentField.vue
组件,这里给出PkIndexView.vue
其余页面组件引入方式类似。 - 导航栏按钮点击指定按钮,实现点击高亮:判断当前在哪一个页面,对该页面添加
active
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">King of Bots</router-link>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link :class="rout_name === 'pk_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'pk_index'}" >对战</router-link>
</li>
<li class="nav-item">
<router-link :class="rout_name === 'record_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'record_index'}">对局列表</router-link>
</li>
<li class="nav-item">
<router-link :class="rout_name === 'ranklist_index' ? 'nav-link active' : 'nav-link'" :to="{name:'ranklist_index'}">排行榜</router-link>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
hxw
</a>
<ul class="dropdown-menu">
<li><router-link class="dropdown-item" :to="{name:'user_bot_index'}">我的Bot</router-link></li>
<li><router-link class="dropdown-item" href="#">退出</router-link></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
import { useRouter } from "vue-router";
import {computed} from "vue";
export default {
setup(){
const route = useRouter();
let rout_name = computed(() => route.name)
return{
rout_name
}
}
}
</script>
<style scoped>
</style>
<template>
<div class="container content-field">
<div class="card">
<div class="card-body">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
</script>
<style scoped>
div.content-field{
margin-top: 20px;
}
</style>
<template>
<ContentField>对战</ContentField>
</template>
<script>
import ContentField from '../../components/ContentField.vue'
export default {
components: {
ContentField
}
}
</script>
<style scoped>
</style>
导航栏功能实现完成,效果如下
游戏页面的实现
实现地图(13*13)功能
设计思路:
- 周围一圈是墙壁,空地中间设置随机障碍物
- 为了公平,实现障碍物的轴对称或者中心对称
- 能够从左上角走到右上角
游戏中,物体是如何动起来的
一秒钟刷新60张图片(60帧),当前帧把上一帧覆盖掉,实现视觉上的运动。
所有物体每秒钟都需要刷新60次,所以将其抽象出来,设计一个基类AcGameObject.js
代码脚本通常放在asset
文件夹中。在asset
文件夹中创建scripts
文件夹和images
文件夹,将背景文件加入到images
。
先实现游戏基类
const AC_GAME_OBJECTS = [];
export class AcGameObject{
constructor() {
AC_GAME_OBJECTS.push(this);
this.timedelta = 0;
this.has_called_start = false;
}
//只执行一次
start(){
}
//每帧执行一次,除了第一帧之外
update(){
}
//删除之前执行
on_destory(){
}
//删除
destory(){
this.on_destory()
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){//如果没有执行过start函数,就执行start函数
obj.has_called_start = true;// 表示已经执行过了
obj.start();
}else{
obj.timedelta = timestamp - last_timestamp;
obj.update();
}
}
last_timestamp = timestamp;
//执行完step,又会在下一帧执行step
requestAnimationFrame(step)
}
//第一次调用会在下一帧执行step
requestAnimationFrame(step)
游戏中每一组件的都是一个类,一个class
pk页面需要展示地图,所以pk页面的背景可以删掉,单独给pk页面写一个游戏页面组件PlayGround.vue
,在游戏页面中在写一个游戏板子组件GameMap.vue
,所以可以看出PlayGround.vue
包含着GameMap.vue
。
现在就是要求PlayGround.vue
中包含的面积最大的row * col
的矩形。
画地图的实现方式,奇数画浅色,偶数画深色。
复制鼠标点击位置的颜色的方式:
- 鼠标悬浮在指定颜色区域的上方
- 打开QQ
- 键盘同时按住
Ctrl + Alt + A
随后按住C
实现复制色号
接着实现地图中的障碍物,在script
包下创建Wall.js
,墙是在地图的后面,所以墙会将地图覆盖。
最终代码总结:
js脚本
const AC_GAME_OBJECTS = [];
export class AcGameObject{
constructor(){
AC_GAME_OBJECTS.push(this);
this.timedelata = 0;//记录时间差
this.has_called_start = false;//用于标记是否执行过
}
//start执行一次
start(){
}
update(){
}
//删除之前执行
on_destory(){
}
destroy(){
this.on_destory();//删除之前执行
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 = timstamp => {
for(let obj of AC_GAME_OBJECTS){
if(!obj.has_called_start){
obj.has_called_start = true;
obj.start();
}else{
obj.timedelata = timstamp - last_timestamp;
obj.update();
}
}
last_timestamp = timstamp;
requestAnimationFrame(step);
}
//第一次调用会在下一帧执行step
requestAnimationFrame(step);
import {AcGameObject} from "@/assets/scripts/AcGameObject";
import {Wall} from "@/assets/scripts/Wall";
export class GameMap extends AcGameObject{
constructor(ctx,parent){//画布,画布的父元素(动态修改画布的长宽)
super();
this.ctx = ctx;
this.parent = parent;
this.L = 0;// L 用来存储绝对距离,L表示一个单位的长度
//设置内部地图的行数和列数
this.rows = 13;
this.cols = 13;
//设置障碍物的数量
this.inner_walls_count = 20;
//开一个数组,用来存储墙
this.walls = [];
}
//编写一个函数判断是否连通,从左下角到右上角
check_connectivity(g, sx, sy, tx, ty){
if(sx == tx && sy == ty){
return true;
}else{
//将当前位置标记为已经走过
g[sx][sy] = true;
}
//定义上下左右四个方向的偏移量
const dx = [-1, 0, 1, 0], dy = [0, 1, 0, -1];
for(let i = 0 ; i < 4; i ++){
let nx = sx + dx[i], ny = sy + dy[i];
if(!g[nx][ny] && this.check_connectivity(g, nx, ny,tx,ty)){
return true;
}
}
return false;
}
creat_walls(){
const g = [];
for(let r = 0; r < this.rows; r ++){
g[r] = [];
for(let c = 0; c < this.cols; c++){
g[r][c] = false;
}
}
//给四周加上障碍物
for(let r = 0; r < this.rows; r ++){
g[r][0] = g[r][this.cols - 1] = true;
}
for(let c = 0; c < this.cols; c ++){
g[0][c] = g[this.rows - 1][c] = true;
}
// 随机生成障碍物, 每次放两个
for(let i = 0; i < this.inner_walls_count / 2; i ++){
//每次随机一个位置,重复了继续随机,直到随机到不同位置指定数量的障碍物
for(let j = 0; j < 1000; j ++){
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
if(g[r][c] || g[c][r]){
continue;
}
//确保不覆盖左下角和右上角
if((r == this.rows - 2 && c == 1) || (r == 1 && c == this.cols - 2)){
continue;
}
g[r][c] = g[c][r] = true;
break;
}
}
//数组的复制
const copy_g = JSON.parse(JSON.stringify(g));
if(!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2)){
return false;
}
//枚举数组
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));
}
}
}
return true;
}
start(){
//不建议写死循环,可以循环足够多的次数
for(let i = 0; i < 1000; i ++){
if(this.creat_walls()){
break;
}
}
this.creat_walls();
}
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();
this.render();
}
render(){
//将当前的地图画出来
const color_even = "#AAD751";//偶数颜色
const color_odd = "#A2D149";//奇数颜色
for(let r = 0; r < this.rows; r++ ){
for(let c = 0; c < this.cols; c++){
if((c + r) % 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);
}
}
}
}
import {AcGameObject} from "@/assets/scripts/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; // L会动态变化,需要动态取
const ctx = this.gamemap.ctx;
ctx.fillStyle = this.color;
ctx.fillRect(this.c * L, this.r * L, L, L);
}
}
组件
<template>
<div class="container content-field">
<div class="card">
<div class="card-body">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
</script>
<style scoped>
div.content-field{
margin-top: 20px;
}
</style>
<template>
<div ref = "parent" class="gamemap">
<canvas ref="canvas">
</canvas>
</div>
</template>
<script>
import {GameMap} from "@/assets/scripts/GameMap";
import {ref, onMounted} from "vue";
export default {
setup(){
let parent = ref(null);
let canvas = ref(null);
onMounted(() => {
new GameMap(canvas.value.getContext('2d'),parent.value)
});
return {
parent,
canvas
}
}
}
</script>
<style scoped>
div.gamemap{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
alien-items: center;
}
</style>
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<router-link class="navbar-brand" :to="{name: 'home'}">King of Bots</router-link>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link :class="rout_name === 'pk_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'pk_index'}" >对战</router-link>
</li>
<li class="nav-item">
<router-link :class="rout_name === 'record_index' ? 'nav-link active' : 'nav-link'" :to="{name: 'record_index'}">对局列表</router-link>
</li>
<li class="nav-item">
<router-link :class="rout_name === 'ranklist_index' ? 'nav-link active' : 'nav-link'" :to="{name:'ranklist_index'}">排行榜</router-link>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
hxw
</a>
<ul class="dropdown-menu">
<li><router-link class="dropdown-item" :to="{name:'user_bot_index'}">我的Bot</router-link></li>
<li><router-link class="dropdown-item" href="#">退出</router-link></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
import { useRouter } from "vue-router";
import {computed} from "vue";
export default {
setup(){
const route = useRouter();
let rout_name = computed(() => route.name)
return{
rout_name
}
}
}
</script>
<style scoped>
</style>
<template>
<div class="playground">
<GameMap/>
</div>
</template>
<script>
import GameMap from "./GameMap.vue"
export default {
components:{
GameMap,
}
}
</script>
<style scoped>
div.playground{
width: 60vw;
height: 70vh;
margin: 50px auto;
}
</style>
跟换网址图标以及最终效果展示
更换图标,点击图片另存为,到指定目录,打开所有文件,双击替换的图片,实现替换
总结
本次写的是前端代码,每次用户刷新浏览器都会将页面加载出来,每次加载,会先创建游戏地图,游戏地图每秒钟刷新60次。创建完地图之后,创建障碍物,障碍物是先创建四周,然后随机创建内部障碍物,这里需要判断创建的障碍物是否满足连通性,如不满足,重新创建。
最后,记得上传git哦~