真的不是卷,为了方便统计公会战积分,我开发了个小系统

238 阅读7分钟

2022年,这一年真的不好过,但它也终于要过去了

在8月的时候就从上一家公司离开了,在家待了两个月,媳妇又怀孕了,所以也就一直没找工作,想等宝宝出生后在找了,不知道到时候还能找到工作不,现在的行情真的是特别的不好,不过,管他呢,总会有出路的。

主要技术

  • 原型: axure
  • 前端: vue3+vite+element-plus
  • 后端: node+express+nedb

声明

本人初次投稿,写的不好也很啰嗦,技术水平也有限,希望大家多鼓励

项目背景

在家待的时候玩了一款游戏,应该从2022年一月的时候开始玩的,这也快玩了有一年了,因为我们打公会战的时候总是掉级,有很多人在公会里就是佛系的玩家,想打就打一打,不想打就摆烂了,这对公会里付出努力的人是不太公平的。。。实在编不下去了,好吧,就是在家待的无聊,时间长不写代码手艺是会退步的,给自己找点事干,所以创造需求,自己既是产品,又是美工,又是前端,还要做后端接口。。。

image.png 没有系统之前都是用excel做个统计,方便看一期中得了多少分和人员的表现。。。但是有几个痛点

  1. 不能实现多期单人分数统计
  2. 排序操作太过复杂
  3. 一般游戏里都是给出当前期的总分,往excel里写的时候还要自己算每个阶段的分数
  4. 人员的增删改查操作过于复杂
  5. 就是用着不爽

开始

ue

把主要的功能在ue软件中写布置一下,这样后边做页面也会快很多,不会ue的前端不是好前端, 这个出来后大概的功能,逻辑点,和要用到哪些接口的规划也就差不多了。。

image.png

先写前端

首先就是vite+vue3安装了,这个教程应该有很多,我这就不cv了,按着ue把所需要的页面搭建出来,主要功能页有以下几页

image.png

image.png

image.png

目录结构

image.png

前端这块也遇到了一些问题,我集中到后边去写

后端接口

这块采用的node+express+nedb的技术栈,其实这块我之前真的是知识盲区,之前有一点理解但没有做过具体的项目,所以这块还是很懵的,做完之后也不知道自己写的方式,目录结构什么的合理不合理,有大佬看到给指点一下就非常感谢了。 数据库用的nedb, 其实也想用mongodb的,但是都差不多,这个nedb比mongo小一些,就选择这个了,毕竟自己还是产品,可以说了算了,哈哈,实现自驱了。。。

先根据原型把接口都定下来,因为太多,只列两个,就是做好前后端的一个约定

phase: {
  name: '2022年12月第一期',
  stage: '准备',
  beginDate: '2016-05-03',
  endDate: '2016-05-03',
  level: '登封',
  status: '已结束',
}

1 活动列表接口
  name: getPhaselist
  参数: 无
  返回值: phase数组

2 获取某一期活动信息
  name: getPhaseInfoById
  参数: id 
  返回值: phase

前端参数处理,比如我要存一个数据,这些数据是要从前端接口传过来的,这时候就用了body-parser这个中间件了,这个还挺简单,下面代码有使用方法

接口写完下来,大概有了一点后端处理数据的印象和概念,总结大致模型如下

image.png

后端文件结构

image.png

主文件index.js代码如下:

const express = require('express');
const app = express();
const port = 5000 

// 引入路由处理模块
const phase = require('./module/phase.js')  
const user = require('./module/user.js')
const score = require('./module/score.js')

// 引入参数处理中间件
const bodyParser = require('body-parser')
app.use(bodyParser.json())

// phase request 
//--------------------------------------------------------------------------------------------
app.get('/getPhaseList',     phase.getPhaseListRouter)
app.get('/getPhaseInfoById', phase.getPhaseInfoByIdRouter)
app.post('/createPhase',     phase.createPhaseRouter)
app.post('/editPhase',       phase.editPhaseRouter)
app.post('/deletePhase',     phase.deletePhaseRouter)
//--------------------------------------------------------------------------------------------

// user request 
//--------------------------------------------------------------------------------------------
app.get('/getUserList',      user.getUserListRouter)
app.get('/getUserInfoById',  user.getUserInfoByIdRouter)
app.post('/addUser',         user.addUserRouter)
app.post('/editUser',        user.editUserRouter)
app.post('/deleteUser',      user.deleteUserRouter)
//--------------------------------------------------------------------------------------------

// score request 
//--------------------------------------------------------------------------------------------
app.get('/getScoreList',     score.getScoreListRouter)
app.post('/updateScore',     score.updateScoreRouter)
//--------------------------------------------------------------------------------------------


const server = app.listen(port, function () {
  // const host = server.address().address
  // const port = server.address().port
  console.log("server is run at http://127.0.0.1:5000")
})

路由处理模块之一, module/phase.js

const methods = require('../db/methods.phase.js')
const { createPhase, getPhaseList,getPhaseInfoById,editPhase, deletePhase } = methods
function getPhaseListRouter (req, res) {
	getPhaseList().then((data) => {
		res.send({
			data,
			status: 0
		});
	})
}

function getPhaseInfoByIdRouter(req, res) {
	getPhaseInfoById(req.query.id).then((data) => {
		res.send({
			data,
			status: 0
		});
	}).catch((err) => {
		res.send({
			message: '没有找到相关期号!',
			status: 1
		});
	})
}

function createPhaseRouter(req, res) {
	let phase = req.body
	//console.log(req.body)
	if(phase.name === ''){
		res.send({
			status: 1,
			message: '没有期号!'
		})
		return
	}

	createPhase(phase).then((data) => {
		res.send({
			status: 0,
			data: data
		})
	})
}


function deletePhaseRouter(req, res) {
	let id = req.body.id
	//console.log(req.body)
	if(id === undefined ){
		res.send({
			status: 1,
			message: '没有这个id!'
		})
		return 
	}

	deletePhase(id).then(() => {
		res.send({
			status: 0
		})
	})
}


function editPhaseRouter(req, res) {
	let phase = req.body
	//console.log(req.body)
	if(phase.name === ''){
		res.send({
			status: 1,
			message: '没有期号!'
		})
		return
	}
	editPhase(phase).then((data) => {
		res.send({
			status: 0,
			data: data
		})
	})
}

module.exports = {
	getPhaseListRouter,
	getPhaseInfoByIdRouter,
	createPhaseRouter,
	deletePhaseRouter,
	editPhaseRouter
}

还有数据库处理模块文件, db/methods.phase.js

const db = require('./index.js')

// 添加期号
function createPhase(data) {
	return new Promise((resolve, reject) => {
		db.phase.insert(data, (error, result) => {
	    if (error === null) {
	    	console.log('result is:', result);
	    	db.score.insert({pid: result._id, score: []}, (err, res) => {
	    		if(err === null) {
	    			resolve(result)
	    		} 
	    	})
	    	
	    }else{
	    	console.log('error:', error);
	    	reject(error)
	    }
		});
	})
}

//编辑期号信息
function editPhase(data) {
	return new Promise((resolve, reject) => {
		db.phase.update({_id: data._id},data,{}, (error, result) => {
	    if (error === null) {
	    	console.log('result:', result.data);
	    	resolve(result)
	    }else{
	    	console.log('error:', error);
	    	reject(error)
	    }
		});
	})
}

// 获取期号列表
function getPhaseList() {
	return new Promise((resolve, reject) => {
		db.phase.find({}).sort({beginDate: -1}).skip(0).limit(10).exec( (error, result) => {
	    if (error === null) {
	    	//console.log('result:', result);
	    	resolve(result)
	    }else{
	    	console.log('error:', error);
	    	reject(error)
	    }
		});
	})
}

// 删除一个期号
function deletePhase(id) {
	return new Promise((resolve, reject) => {
		db.phase.remove({_id: id}, (error, result) => {
	    if (error === null) {
	    	//console.log('result:', result);
	    	resolve(result)
	    }else{
	    	console.log('error:', error);
	    	reject(error)
	    }
		});
	})
}


// 获取一期信息
function getPhaseInfoById(id) {
	return new Promise((resolve, reject) => {
		db.phase.find({_id: id}, (error, result) => {
	    if (error === null) {
	    	console.log('result:', result);
	    	if(result.length === 1) resolve(result[0])
	    		else reject(error)
	    }else{
	    	console.log('error:', error);
	    	reject(error)
	    }
		});
	})
}


module.exports = {
	createPhase,
	editPhase,
	deletePhase,
	getPhaseList,
	getPhaseInfoById
}

然后后端这里在命令行运行 node index.js 就会在5000端口上跑起来一个后端的服务器,接口也都挂在这个服务上的,前端可以通过相应的url链接来调用接口

注意: 由于 vite默认的端口是跑在5173上的,所以肯定这里是会跨域的,这里踩了个小坑,开始以为还要在vue.config.js里去配置,其实vite的项目,要配置在vite.config.js这个文件上,代理和配置设置如下, 注意这里的写法,属性名和vue.config.js里的写法都不一样

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  server: {
    proxy: {
      //端口号
      //port: 5173,
      '/api': {// api 表示拦截以 /api开头的请求路径
        target: 'http://127.0.0.1:5000',//跨域的域名(不需要写路径)process.env.VUE_APP_URL
        changeOrigin: true,             //是否开启跨域
        //ws: true,                     //是否代理websocked
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  }
})

这里配置好后前端就可以通过接口去调用数据了,因为比较简单,请求也没有对axios再进行封装, 这里列一个请求示例,status等于0表明后台返回正常

const getPhaseData = () => {
  axios.get('/api/getPhaselist').then(({data:req})=> {
    if (req.status === 0) {
      req.data.forEach((item) => {
        item.beginDate = dayjs(item.beginDate).format("YYYY-MM-DD")
        item.endDate = dayjs(item.endDate).format("YYYY-MM-DD")
        item.stage = item.stage === 0 ? '准备中' : `第${item.stage}阶段`
        item.status = item.status === 0 ? '未开始' : item.status === 1 ? '进行中' : '已结束'
      } )
      phase.tableData = [...req.data]
    }
  })
}

再来说下前端的那些问题

  1. 表格数据处理

这里因为要用到排序,就直接想用到el-table里的sort属性了,这个还是比较简单的,如果列表数据是不可编辑的,自带的这个sort完全能胜任,可是我想要的是,它既能查看,我也能在这个页面进行编辑,这时候el-table就有问题了,下图所示功能 ,如果el-talbe的sort排好的表格,你在改其内容的时候,他还会实时的进行排序,就导致你输入的时候行的内容就已经变了,所以就自己写了个简单的排序,反正自己说了算,就是任性,这也是自己写着玩和企业级项目之间最大的区别吧。

image.png

列一些表格里数据处理的逻辑吧,这块感觉还有优化空间

// 设置单人合计值
const setRowTotal = (row) => {
  nextTick(() => {
    // 输入合计值时,计算出当前阶段分数并进行回填
    switch (data.phaseInfo.stage) {
      case 0:
        break;
      case 1:
        row.s1 = row.total - row.s2 - row.s3 - row.s4
        break;
      case 2:
        row.s2 = row.total - row.s1 - row.s3 - row.s4
        break;
      case 3:
        row.s3 = row.total - row.s1 - row.s2 - row.s4
        break;
      case 4:
        row.s4 = row.total - row.s1 - row.s2 - row.s3
        break;
      default:
        break;
    }
  })
}

// 设置阶段值,自动计算单人合计
const setStageScore = (row) => {
  nextTick(() => {
    // 更新合计值
    row.total = row.s1 + row.s2 + row.s3 + row.s4
  })
}

最后就是将最终数据导出为图片的功能了,这里用到了canvas画图,也是好长时间没练过这个功能了。看一下生成图片的效果.

image.png

主要功能就是画块,画线,写字,找好坐标点就行了。

代码:

import dayjs from 'dayjs'
export default (data, outputPanel) => {
  let ctx = outputPanel.value.getContext('2d');
  let phaseInfo = data.phaseInfo
  let drawData = JSON.parse(JSON.stringify(data.tableData))
  let tableTitle = ['','名字', '第1阶段', '第2阶段', '第3阶段', '第4阶段', '合计']
  let colPosition = [10, 50, 260, 400, 540, 680, 820]
  ctx.fillStyle = "white"
  ctx.fillRect(0, 0, 900, 2700)
  // 画头部
  drawTitle()
  // 表格头行
  drawTableTitle()
  // 表格数据
  drawTableData()

  function drawTitle(){
    ctx.fillStyle = "#b71c1c"
    ctx.fillRect(0, 0, 900, 70)
    ctx.font = "40px '得意黑'";
    ctx.fillStyle = "yellow"
    //ctx.moveTo(300,300)
    ctx.fillText(phaseInfo.title, 30,50)
    ctx.font = "12px 'Arial'";
    let beginDate = dayjs(phaseInfo.beginDate).format('YYYY-MM-DD')
    let endDate = dayjs(phaseInfo.endDate).format('YYYY-MM-DD')
    let time = `本期时间:${beginDate} -- ${endDate}`
    ctx.fillText(time, 700, 50)
  }

  function drawTableTitle() {
    ctx.fillStyle = "pink"
    ctx.fillRect(0, 70, 900,45)
    ctx.font = "16px '微软雅黑'";
    ctx.fillStyle = "#333333"
    tableTitle.forEach((item,index) => {
      ctx.fillText(item, colPosition[index], 100)
    })
  }

  function drawTableData() {
    ctx.font = "14px '微软雅黑'";
    ctx.fillStyle = "#333333"
    let [s1Summary,s2Summary,s3Summary,s4Summary,totalSummary] = [0,0,0,0,0]

    drawData.forEach((item, index) => {

      // 合计行数据统计
      s1Summary += item.s1
      s2Summary += item.s2
      s3Summary += item.s3
      s4Summary += item.s4
      totalSummary += item.total

      // 单人数据打印
      let row = [index+1, item.name, item.s1,item.s2,item.s3,item.s4, item.total]
      ctx.strokeStyle = "#ddd";
      ctx.moveTo(0.5, 155.5 + index * 40)
      ctx.lineTo(900.5, 155.5 + index * 40)
      ctx.lineWidth = 1
      ctx.stroke()
      for(let n=0; n<row.length; n++){
        ctx.fillText(row[n], colPosition[n], 140+ index* 40)
      }
    })

    // 合计部分
    ctx.fillStyle = "#ddd"
    ctx.fillRect(0, 115 + drawData.length * 40, 900, 50)
    let row = ['', '合计', s1Summary, s2Summary, s3Summary, s4Summary, totalSummary]
    ctx.fillStyle = "#333"
    for (let n = 0; n < row.length; n++) {
      ctx.fillText(row[n], colPosition[n], 145 + drawData.length * 40)
    }
  }
}

那么问题来了,怎么在画布上画1像素的线呢?

总结

这个项目还是起到了练手的作用,那种写代码的舒适感又回来了,完全解决了之前的所有痛点,自已也基本摸通了node + nosql这种开发接口的方式,而且在公会群里起到了很好的装X效果,这不群主立马给我开了副阁主的位置,哇哈哈哈。。。

后续

项目是跑起来了但是还有一些问题,现在项目分为前端和后端,运行的时候要跑两个服务,所以这块其实也考虑过做成electron的,这样最络可以打包个exe出来,给不懂程序的小伙伴也能继续使用,而且成本也不大。

想要看原码的可以留言我。

回顾2022,展望2023,我正在参与2022年终总结征文大赛活动