项目是刚刚完成的,于是趁热打铁把文档也写了。在这里分享出来,也方便以后回顾
目录
项目介绍
整体设计架构图
网站界面预览图
技术选型和原因
搭建步骤
库表设计
插件说明
后端说明
前端说明
部署说明
完整代码
插件代码
后端代码
前端代码
项目总结
本项目旨在为我的世界基岩版私服搭建一个可视化的后台管理系统,通过 LiteloaderBDS 插件实时收集游戏内数据,并将其存储在轻量级数据库 SQLite 中。后端采用 Spring Boot 和 MyBatis 技术栈实现 RESTful API,前端采用 Vue 框架、Element-UI Plus 组件库以及 Three.js WebGL 库实现三维可视化界面
主页:
方块地图:
数据总表:
本项目采用的部分技术栈:
BB_Data.js 的代码内容分为四大部分:事件监听、定时任务、辅助函数、创表语句
其中,增删改查逻辑集中在事件监听、定时任务和辅助函数部分
被监听的事件:
由于 SQLite 对并发修改支持不佳,代码中的 SQL 执行语句偶尔会出现异常;但总的来说,这仅仅会导致很小一部分行为没有被记录,所以我没有加锁来改善这一问题(加锁影响性能)
有一些测试用的打印语句,可以删掉
后端采用了传统的 Spring Boot + MyBatis 技术栈
相对于持久层设计,简化了数据模型(去除了所有的外键部分),便于前端拿取数据后直接使用
风格为选项式 API,单页面应用(SPA),面向组件设计,解耦较好
多种布局样式,包括传统、绝对位置和 Flex 布局
使用了路由管理
其中一个 svg 图标(ChatGPT),直接封装为组件使用了,在代码中省略
后端打包成 JAR 文件,在服务器用命令行执行
前端打包成静态资源,上传到服务器的 Nginx 服务目录,启动 Nginx
BB_Data:
///
// TODO 删除过早的(根据时间戳)数据
let session;
mc.listen('onServerStarted', () => {
session = initDB();
});
mc.listen('onJoin', (player) => {
let preSelectPlayer = session.prepare('SELECT COUNT(*) as count FROM player_table WHERE xuid = ?;');
let preInsertPlayer = session.prepare('INSERT INTO player_table (xuid, name, bag_uuid, enc_uuid) VALUES (?, ?, ?, ?);');
let preInsertCtr = session.prepare('INSERT INTO ctr_table (uuid, name, content, latest_timestamp) VALUES (?, ?, ?, ?);');
let currentTimestamp = Date.now();
// 1. 插入新玩家(如果不存在)
preSelectPlayer.bind([player.xuid]);
const playerResult = preSelectPlayer.reexec().fetch();
if (playerResult.count === 0) {
// 插入新玩家
let bagUUID = generateUUID();
let encUUID = generateUUID();
preInsertPlayer.bind([player.xuid, player.name, bagUUID, encUUID]);
preInsertPlayer.reexec();
// 插入新容器
let containers = [
{uuid: bagUUID, name: 'bag'},
{uuid: encUUID, name: 'ender_chest'}
];
containers.forEach((container) => {
preInsertCtr.bind([
container.uuid,
container.name,
'{}',
currentTimestamp
]);
preInsertCtr.reexec();
preInsertCtr.clear();
});
log(`向玩家表中插入了 ${player.name}`);
}
// 2. 更新玩家
updatePlayer(player);
// 3. 插入消息
const messageContent = JSON.stringify({text: `${player.name} 进入游戏`});
insertMsg(player, 'join', messageContent);
});
setInterval(() => {
mc.getOnlinePlayers().forEach((player) => {
let preInsertHistoryPos = session.prepare('INSERT INTO history_pos_table (xuid, pos_id, timestamp) VALUES (?, ?, ?);');
let currentTimestamp = Date.now();
// 1. 更新玩家,并获得玩家位置id
const newPosId = updatePlayer(player);
// 2. 添加历史位置
preInsertHistoryPos.bind([player.xuid, newPosId, currentTimestamp]);
preInsertHistoryPos.reexec();
});
}, 2 * 1000);
mc.listen('onOpenContainer', (player, block) => {
if (!block.hasContainer()) {
return;
}
let preInsertCtr = session.prepare('INSERT INTO ctr_table (uuid, name) VALUES (?, ?);');
let preInsertCtrBlock = session.prepare('INSERT INTO ctr_block_table (uuid, pos_id, ctr_uuid) VALUES (?, ?, ?);');
let preUpdateCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid = ?;');
let preUpdateCtrBlock = session.prepare('UPDATE ctr_block_table SET latest_timestamp = ? WHERE uuid = ?;');
let ctrContent = ctrContentJSON(block.getContainer());
let currentTimestamp = Date.now();
const newPosId = insertPos(block.pos);
// 1. 查询或插入新容器方块
let {ctrBlockUuid, ctrUuid} = getCtrBlockAndCtrUUID(newPosId) || {};
if (!ctrBlockUuid || !ctrUuid) {
// 生成新的容器方块和容器 UUID
ctrBlockUuid = generateUUID();
ctrUuid = generateUUID();
// init
preInsertCtr.bind([ctrUuid, block.getContainer().type]);
preInsertCtr.reexec();
preInsertCtrBlock.bind([ctrBlockUuid, newPosId, ctrUuid]);
preInsertCtrBlock.reexec();
}
// 2. 添加容器记录到 ctr_table
preUpdateCtr.bind([ctrContent, currentTimestamp, ctrUuid]);
preUpdateCtr.reexec();
// 3. 添加容器记录到 ctr_block_table
preUpdateCtrBlock.bind([currentTimestamp, ctrBlockUuid]);
preUpdateCtrBlock.reexec();
// 4. 插入消息
const messageContent = JSON.stringify({
text: `${player.name} 打开容器`,
pos_id: newPosId
});
insertMsg(player, 'open_ctr', messageContent);
});
mc.listen('onCloseContainer', (player, block) => {//只能监听到箱子和木桶的关闭
if (!block.hasContainer()) {
return;
}
let preUpdateCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid = ?;');
let preUpdateCtrBlock = session.prepare('UPDATE ctr_block_table SET latest_timestamp = ? WHERE uuid = ?;');
let ctrContent = ctrContentJSON(block.getContainer());
let currentTimestamp = Date.now();
const newPosId = insertPos(block.pos);
// 1. 获取容器方块和容器 UUID
let {ctrBlockUuid, ctrUuid} = getCtrBlockAndCtrUUID(newPosId) || {};
if (!ctrBlockUuid || !ctrUuid) {
colorLog('red', `${player.name} 关闭了未记录的箱子或木桶`);
return;
}
// 2. 添加容器记录到 ctr_table
preUpdateCtr.bind([ctrContent, currentTimestamp, ctrUuid]);
preUpdateCtr.reexec();
// 3. 添加容器记录到 ctr_block_table
preUpdateCtrBlock.bind([currentTimestamp, ctrBlockUuid]);
preUpdateCtrBlock.reexec();
// 4. 插入消息
const messageContent = JSON.stringify({
text: `${player.name} 关闭容器`,
pos_id: newPosId
});
insertMsg(player, 'close_ctr', messageContent);
});
mc.listen('onDestroyBlock', (player, block) => {
colorLog('dk_yellow', `destroy_block:${player.name},${block.name}`)
let preInsertDestruction = session.prepare('INSERT INTO block_change_table (xuid, pos_id, type, name, timestamp) VALUES (?, ?, ?, ?, ?);');
let currentTimestamp = Date.now();
// 1. 插入位置
const newPosId = insertPos(block.pos);
// 2. 插入破坏
preInsertDestruction.bind([player.xuid, newPosId, 'destroy', block.name, currentTimestamp]);
preInsertDestruction.reexec();
// 3. 删除容器
if (block.hasContainer()) {
// 获取容器方块和容器 UUID
let {ctrBlockUuid, ctrUuid} = getCtrBlockAndCtrUUID(newPosId) || {};
if (!ctrBlockUuid || !ctrUuid) {
return;
}
// 删除容器方块和容器
let preDeleteCtrBlock = session.prepare('DELETE FROM ctr_block_table WHERE uuid = ?;');
let preDeleteCtr = session.prepare('DELETE FROM ctr_table WHERE uuid = ?;');
preDeleteCtrBlock.bind([ctrBlockUuid]);
preDeleteCtrBlock.reexec();
preDeleteCtr.bind([ctrUuid]);
preDeleteCtr.reexec();
}
});
mc.listen('afterPlaceBlock', (player, block) => {
let preInsertPlacement = session.prepare('INSERT INTO block_change_table (xuid, pos_id, type, name, timestamp) VALUES (?, ?, ?, ?, ?);');
let currentTimestamp = Date.now();
// 1. 插入位置
const newPosId = insertPos(block.pos);
// 2. 插入添加记录
preInsertPlacement.bind([player.xuid, newPosId, 'place', block.name, currentTimestamp]);
preInsertPlacement.reexec();
// 3. 添加容器
if (block.hasContainer()) {
let preInsertCtr = session.prepare('INSERT INTO ctr_table (uuid, name, content, latest_timestamp) VALUES (?, ?, ?, ?);');
let preInsertCtrBlock = session.prepare('INSERT INTO ctr_block_table (uuid, pos_id, ctr_uuid, latest_timestamp) VALUES (?, ?, ?, ?);');
let containerContent = ctrContentJSON(block.getContainer());
// 创建容器的 UUID
const ctrUuid = generateUUID();
const ctrBlockUuid = generateUUID();
// 添加容器记录到 ctr_table
preInsertCtr.bind([ctrBlockUuid, block.getContainer().type, containerContent, currentTimestamp]);
preInsertCtr.reexec();
// 添加容器记录到 ctr_block_table
preInsertCtrBlock.bind([ctrBlockUuid, newPosId, ctrUuid, currentTimestamp]);
preInsertCtrBlock.reexec();
}
});
mc.listen('onAttackEntity', (player, entity, damage) => {
colorLog('dk_yellow', `attack:${player.name},${entity.name},${damage}`)
let preInsertAttackEntity = session.prepare('INSERT INTO attack_entity_table (xuid, pos_id, damage, name, timestamp) VALUES (?, ?, ?, ?, ?);');
let entName = entity.name ? entity.name : 'null';
let damageNum = damage ? damage : 0;
let currentTimestamp = Date.now();
// 1. 插入位置
const newPosId = insertPos(entity.blockPos);
// 2. 插入攻击实体
preInsertAttackEntity.bind([player.xuid, newPosId, damageNum, entName, currentTimestamp]);
preInsertAttackEntity.reexec();
});
mc.listen('onChat', (player, msg) => {
// 1. 插入消息
const messageContent = JSON.stringify({
text: `${player.name} 发送消息`,
message: msg
});
insertMsg(player, 'chat', messageContent);
});
mc.listen('onLeft', (player) => {
// 1. 更新玩家
let newPosId = updatePlayer(player);
// 2. 插入消息
const messageContent = JSON.stringify({
text: `${player.name} 离开游戏`,
pos_id: newPosId
});
insertMsg(player, 'left', messageContent);
});
// 辅助函数:生成 UUID
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 获取容器内容 JSON
function ctrContentJSON(ctr) {
if (ctr.isEmpty()) {
return '{}';
}
let itemsArray = [];
ctr.getAllItems().forEach((item) => {
if (item.id !== 0) {
let itemObj = {
name: item.name,
count: item.count,
};
itemsArray.push(itemObj);
}
});
let contentJSON = JSON.stringify(itemsArray);
return contentJSON;
}
// 插入新位置(如果不存在),并返回位置 id
function insertPos(blockPos) {
let preSelectPos = session.prepare('SELECT id FROM pos_table WHERE x = ? AND y = ? AND z = ? AND dim_id = ?;');
let preInsertPos = session.prepare('INSERT OR IGNORE INTO pos_table (x, y, z, dim_id) VALUES (?, ?, ?, ?);');
let {x: newX, y: newY, z: newZ, dimid: newDimId} = blockPos;
// 1. 插入新位置(如果不存在)
preInsertPos.bind([newX, newY, newZ, newDimId]);
preInsertPos.reexec();
// 2. 查询新位置的 id
preSelectPos.bind([newX, newY, newZ, newDimId]);
const result = preSelectPos.reexec().fetch();
return Object.values(result)[0];
}
// 更新玩家的位置和容器,并返回玩家位置 id
function updatePlayer(player) {
let preUpdatePlayerPos = session.prepare('UPDATE player_table SET pos_id = ?, latest_timestamp = ? WHERE xuid = ?;');
let preUpdatePlayerBagCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid IN (SELECT bag_uuid FROM player_table WHERE xuid = ?);');
let preUpdatePlayerEncCtr = session.prepare('UPDATE ctr_table SET content = ?, latest_timestamp = ? WHERE uuid IN (SELECT enc_uuid FROM player_table WHERE xuid = ?);');
let bagContent = ctrContentJSON(player.getInventory());
let encContent = ctrContentJSON(player.getEnderChest());
let currentTimestamp = Date.now();
// 1. 插入位置
let newPosId = insertPos(player.blockPos);
// 2. 更新玩家的 pos_id
preUpdatePlayerPos.bind([newPosId, currentTimestamp, player.xuid]);
preUpdatePlayerPos.reexec();
// 3. 更新玩家背包容器和末影容器的内容以及时间戳
preUpdatePlayerBagCtr.bind([bagContent, currentTimestamp, player.xuid]);
preUpdatePlayerBagCtr.reexec();
preUpdatePlayerEncCtr.bind([encContent, currentTimestamp, player.xuid]);
preUpdatePlayerEncCtr.reexec();
return newPosId;
}
function insertMsg(player, type, content) {
let preInsertMsg = session.prepare('INSERT INTO msg_table (uuid, type, content, timestamp) VALUES (?, ?, ?, ?);');
preInsertMsg.bind([generateUUID(), type, content, Date.now()]);
preInsertMsg.reexec();
}
// 获取容器方块和容器的 UUID
function getCtrBlockAndCtrUUID(pos_id) {
let preSelectCtrBlock = session.prepare('SELECT uuid, ctr_uuid FROM ctr_block_table WHERE pos_id = ?;');
preSelectCtrBlock.bind([pos_id]);
const ctr_block_result = preSelectCtrBlock.reexec().fetch();
if (ctr_block_result && ctr_block_result.uuid && ctr_block_result.ctr_uuid) {
return {ctrBlockUuid: Object.values(ctr_block_result)[0], ctrUuid: Object.values(ctr_block_result)[1]};
} else {
return null;
}
}
function initDB() {//初始化数据库
const dirPath = 'plugins/BB_Data';
if (!file.exists(dirPath)) {
colorLog('dk_yellow', `检测到数据库目录./${dirPath}不存在, 现将自动创建`);
file.mkdir(dirPath);
}
const session = new DBSession('sqlite', {path: `./${dirPath}/dat.db`});
session.exec(//位置表
'CREATE TABLE pos_table (\\n' +
' id INTEGER PRIMARY KEY AUTOINCREMENT,\\n' +
' x INTEGER,\\n' +
' y INTEGER,\\n' +
' z INTEGER,\\n' +
' dim_id INTEGER\\n' +//维度id
');'
);
session.exec('CREATE UNIQUE INDEX idx_pos ON pos_table(x, y, z, dim_id);');
session.exec(//容器表
'CREATE TABLE ctr_table (\\n' +
' uuid TEXT PRIMARY KEY,\\n' +
' name TEXT,\\n' +//容器名字
' content TEXT,\\n' +//容器内容JSON
' latest_timestamp INTEGER\\n' +//最后更新时间戳
');'
);
session.exec(//消息表
'CREATE TABLE msg_table (\\n' +
' uuid TEXT,\\n' +
' type TEXT,\\n' +//消息类型
' content TEXT,\\n' +//消息内容JSON
' timestamp INTEGER,\\n' +//时间戳
' PRIMARY KEY (uuid, timestamp)\\n' +
');'
);
session.exec(//玩家表
'CREATE TABLE player_table (\\n' +
' xuid INTEGER PRIMARY KEY,\\n' +
' name TEXT,\\n' +
' pos_id INTEGER, -- 玩家位置id\\n' +
' bag_uuid INTEGER, -- 背包容器\\n' +
' enc_uuid INTEGER, -- 末影容器\\n' +
' latest_timestamp INTEGER, -- 最后更新时间戳\\n' +
' FOREIGN KEY (pos_id) REFERENCES pos_table(id),\\n' +
' FOREIGN KEY (bag_uuid) REFERENCES ctr_table(uuid),\\n' +
' FOREIGN KEY (enc_uuid) REFERENCES ctr_table(uuid)\\n' +
');'
);
session.exec(//历史位置表
'CREATE TABLE history_pos_table (\\n' +
' xuid INTEGER, -- 玩家\\n' +
' pos_id INTEGER, -- 玩家位置id\\n' +
' timestamp INTEGER, -- 时间戳\\n' +
' PRIMARY KEY (xuid, timestamp),\\n' +
' FOREIGN KEY (xuid) REFERENCES player_table(xuid),\\n' +
' FOREIGN KEY (pos_id) REFERENCES pos_table(id)\\n' +
');'
);
session.exec(//容器方块表
'CREATE TABLE ctr_block_table (\\n' +
' uuid TEXT PRIMARY KEY,\\n' +
' pos_id INTEGER, -- 容器位置id\\n' +
' ctr_uuid INTEGER, -- 容器\\n' +
' latest_timestamp INTEGER, -- 最后更新时间戳\\n' +
' FOREIGN KEY (pos_id) REFERENCES pos_table(id),\\n' +
' FOREIGN KEY (ctr_uuid) REFERENCES ctr_table(uuid)\\n' +
');'
);
session.exec(//破坏放置表
'CREATE TABLE block_change_table (\\n' +
' xuid INTEGER, -- 玩家\\n' +
' pos_id INTEGER, -- 方块位置id\\n' +
' type TEXT, -- 动作类型\\n' +
' name TEXT, -- 方块名字\\n' +
' timestamp INTEGER, -- 时间戳\\n' +
' PRIMARY KEY (xuid, timestamp),\\n' +
' FOREIGN KEY (xuid) REFERENCES player_table(xuid),\\n' +
' FOREIGN KEY (pos_id) REFERENCES pos_table(id)\\n' +
');'
);
session.exec(//攻击实体表
'CREATE TABLE attack_entity_table (\\n' +
' xuid INTEGER, -- 玩家\\n' +
' pos_id INTEGER, -- 实体位置id\\n' +
' damage INTEGER, -- 伤害\\n' +
' name TEXT, -- 实体名字\\n' +
' timestamp INTEGER, -- 时间戳\\n' +
' PRIMARY KEY (xuid, timestamp),\\n' +
' FOREIGN KEY (xuid) REFERENCES player_table(xuid),\\n' +
' FOREIGN KEY (pos_id) REFERENCES pos_table(id)\\n' +
');'
);
let dbFile = new File(`./${dirPath}/dat.db`, file.ReadMode);
colorLog('green', `[数据记录]数据库连接完成,当前大小${dbFile.size / 1024}K`);
dbFile.close();
return session;
}
ll.registerPlugin('BB_Data', 'BB数据记录', [2, 0, 0, Version.Release], {});
省略了配置的部分
Entity:
@Data
public class AttackEntityPos {
private String playerName;
private String entityName;
private long damage;
private long x;
private long y;
private long z;
private byte dimId;
private long timestamp;
}
@Data
public class BlockChangePos {
private String playerName;
private String blockName;
private String act;
private long x;
private long y;
private long z;
private byte dimId;
private long timestamp;
}
@Data
public class ContainerPos {
private String containerName;
private String content;
private long x;
private long y;
private long z;
private byte dimId;
private long latestTimestamp;
}
@Data
public class Message {
private String type;
private String content;
private long timestamp;
}
@Data
public class Player {
private String playerName;
private String bagItems;
private String enderItems;
private long latestTimestamp;
}
@Data
public class PlayerHistoryPos {
private String playerName;
private long x;
private long y;
private long z;
private byte dimId;
private long timestamp;
}
Mapper:
@Mapper
public interface AttackEntityPosMapper {
@Select("")
int getTotalCount(@Param("playerName") String playerName);
@Select("")
List findAll(int start, int limit, @Param("playerName") String playerName);
}
@Mapper
public interface BlockChangePosMapper {
@Select("")
int getTotalCount(@Param("playerName") String playerName);
@Select("")
List findAll(int start, int limit, @Param("playerName") String playerName);
}
@Mapper
public interface ContainerPosMapper {
@Select("SELECT COUNT(*) " +
"FROM ctr_block_table cb " +
"JOIN ctr_table c ON cb.ctr_uuid = c.uuid")
int getTotalCount();
@Select("SELECT c.name AS containerName, c.content, p.x, p.y, p.z, p.dim_id AS dimId, cb.latest_timestamp AS latestTimestamp " +
"FROM ctr_block_table cb " +
"JOIN pos_table p ON cb.pos_id = p.id " +
"JOIN ctr_table c ON cb.ctr_uuid = c.uuid " +
"ORDER BY cb.latest_timestamp DESC " +
"LIMIT #{start}, #{limit}")
List findAll(int start, int limit);
@Select("SELECT c.name AS containerName, c.content, p.x, p.y, p.z, p.dim_id AS dimId, cb.latest_timestamp AS latestTimestamp " +
"FROM ctr_block_table cb " +
"JOIN pos_table p ON cb.pos_id = p.id " +
"JOIN ctr_table c ON cb.ctr_uuid = c.uuid " +
"WHERE p.dim_id = #{dimId} " +
"ORDER BY cb.latest_timestamp DESC")
List findByDimId(@Param("dimId") int dimId);
}
@Mapper
public interface MessageMapper {
@Select("")
int getTotalCount(@Param("msgType") String msgType);
@Select("")
List findAll(int start, int limit, @Param("msgType") String msgType);
}
@Mapper
public interface PlayerHistoryPosMapper {
@Select("")
int getTotalCount(@Param("playerName") String playerName);
@Select("")
List findAll(int start, int limit, @Param("playerName") String playerName);
@Select("SELECT x, y, z, dim_id " +
"FROM pos_table " +
"WHERE pos_table.id = #{pos_id}")
PlayerHistoryPos findByPosId(int pos_id);
}
@Mapper
public interface PlayerMapper {
@Select("SELECT COUNT(*) FROM player_table")
int getTotalCount();
@Select("SELECT p.name AS playerName, " +
"c1.content AS bagItems, " +
"c2.content AS enderItems, " +
"p.latest_timestamp AS latestTimestamp " +
"FROM player_table p " +
"JOIN ctr_table c1 ON p.bag_uuid = c1.uuid " +
"JOIN ctr_table c2 ON p.enc_uuid = c2.uuid " +
"ORDER BY p.latest_timestamp DESC " +
"LIMIT #{start}, #{limit}")
List findAll(int start, int limit);
@Select("SELECT name AS playerName FROM player_table")
List getNameList();
}
Controller:
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private PlayerMapper playerMapper;
@Autowired
private PlayerHistoryPosMapper playerHistoryPosMapper;
@Autowired
private ContainerPosMapper containerPosMapper;
@Autowired
private MessageMapper messageMapper;
@Autowired
private BlockChangePosMapper blockChangePosMapper;
@Autowired
private AttackEntityPosMapper attackEntityPosMapper;
@GetMapping("/playerList")
public List getPlayerList(@RequestParam("start") int start, @RequestParam("limit") int limit) {
return playerMapper.findAll(start, limit);
}
@GetMapping("/playerHistoryPosList")
public List getPlayerHistoryPosList(@RequestParam("start") int start, @RequestParam("limit") int limit,
@RequestParam(value = "playerName", required = false) String playerName) {
return playerHistoryPosMapper.findAll(start, limit, playerName);
}
@GetMapping("/containerPosList")
public List getContainerPosList(@RequestParam("start") int start, @RequestParam("limit") int limit) {
return containerPosMapper.findAll(start, limit);
}
@GetMapping("/messageList")
public List getMessageList(@RequestParam("start") int start, @RequestParam("limit") int limit,
@RequestParam(value = "msgType", required = false) String msgType) {
return messageMapper.findAll(start, limit, msgType);
}
@GetMapping("/blockChangePosList")
public List getBlockChangePosList(@RequestParam("start") int start, @RequestParam("limit") int limit,
@RequestParam(value = "playerName", required = false) String playerName) {
return blockChangePosMapper.findAll(start, limit, playerName);
}
@GetMapping("/attackEntityPosList")
public List getAttackEntityPosList(@RequestParam("start") int start, @RequestParam("limit") int limit,
@RequestParam(value = "playerName", required = false) String playerName) {
return attackEntityPosMapper.findAll(start, limit, playerName);
}
@GetMapping("/totalPlayerCount")
public int getTotalPlayerCount() {
return playerMapper.getTotalCount();
}
@GetMapping("/totalPlayerHistoryPosCount")
public int getTotalPlayerHistoryPosCount(@RequestParam(value = "playerName", required = false) String playerName) {
return playerHistoryPosMapper.getTotalCount(playerName);
}
@GetMapping("/totalContainerPosCount")
public int getTotalContainerPosCount() {
return containerPosMapper.getTotalCount();
}
@GetMapping("/totalMessageCount")
public int getTotalMessageCount(@RequestParam(value = "msgType", required = false) String msgType) {
return messageMapper.getTotalCount(msgType);
}
@GetMapping("/totalBlockChangePosCount")
public int getTotalBlockChangePosCount(@RequestParam(value = "playerName", required = false) String playerName) {
return blockChangePosMapper.getTotalCount(playerName);
}
@GetMapping("/totalAttackEntityPosCount")
public int getTotalAttackEntityPosCount(@RequestParam(value = "playerName", required = false) String playerName) {
return attackEntityPosMapper.getTotalCount(playerName);
}
@GetMapping("/playerNameList")
public List getPlayerNameList() {
return playerMapper.getNameList();
}
@GetMapping("/pos")
public PlayerHistoryPos getPos(@RequestParam("pos_id") int pos_id) {
return playerHistoryPosMapper.findByPosId(pos_id);
}
@GetMapping("/containerPosListByDimId")
public List getContainerPosListByDimId(@RequestParam("dimId") int dimId) {
return containerPosMapper.findByDimId(dimId);
}
}
Application:
@SpringBootApplication
public class BbDataServerApplication {
public static void main(String[] args) {
SpringApplication.run(BbDataServerApplication.class, args);
}
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 生产环境中,需要将 "*" 替换为实际的前端域名
registry.addMapping("/**").allowedOrigins("*");
}
};
}
}
App:
main:
import {
nextTick,
createApp
} from 'vue';
import {
createRouter,
createWebHistory
} from 'vue-router';
import App from './App.vue';
import ElementPlus from 'element-plus';
import 'element-plus/theme-chalk/index.css';
import './assets/global.css';
import HomeView from './views/HomeView.vue';
import PosView from './views/PosView.vue';
import TabView from './views/TabView.vue';
const routes = [{
path: '/',
name: 'HomeView',
component: HomeView,
},
{
path: '/pos',
name: 'PosView',
component: PosView,
},
{
path: '/tab',
name: 'TabView',
component: TabView,
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
const app = createApp(App);
app.use(ElementPlus);
app.use(router);
app.mount('#app');
assets(global.css):
/* 通用文字色 */
#text {
color: #27342b;
}
/* 头栏的行内容填满 */
#header-row {
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
/* 头栏每列内容居中 */
#header-col {
display: flex;
align-items: center;
justify-content: center;
}
#page-header {
padding-left: 36px;
padding-top: 1vh;
padding-bottom: 1vh;
border: 2px dashed #27342b;
border-radius: 4px;
}
/* 滑动条样式 */
.el-slider__button {
width: 25px !important;
height: 15px !important;
background: #ffffff !important;
border-color: #27342b !important;
border-radius: 4px !important;
}
.el-slider__bar {
background-color: #f6f5ec !important;
}
.el-slider__runway {
background-color: #f6f5ec !important;
border-radius: 2 !important;
}
.el-button {
border: 2px solid #27342b !important;
border-radius: 4px;
background-color: white !important;
}
.el-button:hover {
background-color: #27342b !important;
}
#tab-expand {
margin-left: 40px;
margin-right: 40px;
}
components:
容器中物品({{props.row.containerName}}):
{{ item.name }}:{{ item.count }}
空空如也
消息内容:
{{JSON.parse(props.row.content).text}}
{{posString}}
背包中物品:
{{ item.name }}:{{ item.count }}
空空如也
末影箱物品:
{{ item.name }}:{{ item.count }}
空空如也
地图中心 x 坐标
地图中心 z 坐标
维度
玩家
utils:
//api.js
import axios from 'axios';
const baseURL = '你的后端URL/api';
export default {
async fetchTotalPlayerCount() {
const response = await axios.get(`${baseURL}/totalPlayerCount`);
return response.data;
},
async fetchTotalPlayerHistoryPosCount(playerName) {
const response = await axios.get(`${baseURL}/totalPlayerHistoryPosCount`, {
params: {
playerName
}
});
return response.data;
},
async fetchTotalContainerPosCount() {
const response = await axios.get(`${baseURL}/totalContainerPosCount`);
return response.data;
},
async fetchTotalMessageCount(msgType) {
const response = await axios.get(`${baseURL}/totalMessageCount`, {
params: {
msgType
}
});
return response.data;
},
async fetchTotalBlockChangePosCount(playerName) {
const response = await axios.get(`${baseURL}/totalBlockChangePosCount`, {
params: {
playerName
}
});
return response.data;
},
async fetchTotalAttackEntityPosCount(playerName) {
const response = await axios.get(`${baseURL}/totalAttackEntityPosCount`, {
params: {
playerName
}
});
return response.data;
},
async fetchPlayerList(start, limit) {
const response = await axios.get(`${baseURL}/playerList`, {
params: {
start,
limit
},
});
return response.data;
},
async fetchPlayerHistoryPosList(start, limit, playerName) {
const response = await axios.get(`${baseURL}/playerHistoryPosList`, {
params: {
start,
limit,
playerName
},
});
return response.data;
},
async fetchContainerPosList(start, limit) {
const response = await axios.get(`${baseURL}/containerPosList`, {
params: {
start,
limit
},
});
return response.data;
},
async fetchMessageList(start, limit, msgType) {
const response = await axios.get(`${baseURL}/messageList`, {
params: {
start,
limit,
msgType
},
});
return response.data;
},
async fetchBlockChangePosList(start, limit, playerName) {
const response = await axios.get(`${baseURL}/blockChangePosList`, {
params: {
start,
limit,
playerName
},
});
return response.data;
},
async fetchAttackEntityPosList(start, limit, playerName) {
const response = await axios.get(`${baseURL}/attackEntityPosList`, {
params: {
start,
limit,
playerName
},
});
return response.data;
},
async fetchTotalPlayerNameList() {
const response = await axios.get(`${baseURL}/playerNameList`);
return response.data;
},
async fetchPosById(id) {
const response = await axios.get(`${baseURL}/pos`, {
params: {
pos_id: id
}
});
return response.data;
},
async fetchContainerPosListByDimId(dimId) {
const response = await axios.get(`${baseURL}/containerPosListByDimId`, {
params: {
dimId
},
});
return response.data;
},
};
//tools.js
export default {
getDimName(dimId) {
switch (dimId) {
case -1:
return '未知';
case 0:
return '主世界';
case 1:
return '下界';
case 2:
return '末地';
default:
return '未知';
}
},
getTimeStr(currentTime, value) {
let timeString = new Date(value).toLocaleString();
timeString += `(${this.getTimeAgo(currentTime, value)})`;
return timeString;
},
getTimeAgo(currentTime, value) {
const recordTime = new Date(value);
const diffInSeconds = Math.floor((currentTime - recordTime) / 1000);
if (diffInSeconds < 60) {
return `${diffInSeconds} 秒前`;
}
const diffInMinutes = Math.floor(diffInSeconds / 60);
if (diffInMinutes < 60) {
return `${diffInMinutes} 分钟前`;
}
const diffInHours = Math.floor(diffInMinutes / 60);
if (diffInHours < 24) {
return `${diffInHours} 小时前`;
}
const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays} 天前`;
}
}
views:
BBMC 我的世界基岩版私人服务器后台数据展览平台(版本:2023.06.01)
方 块 地 图
(在 地 图 中 查 看 任 意 容 器)
数 据 总 表
(以 表 格 的 样 式 展 览 记 录)
玩 家 商 店
(你 可 以 购 买 或 售 卖 物 品)
G P T 问 答
(向 A I 咨 询 服 务 器 的 情 况)
BBMC 我的世界基岩版私人服务器后台数据展览平台(版本:2023.06.01)
方 块 地 图
(在 地 图 中 查 看 任 意 容 器)
数 据 总 表
(以 表 格 的 样 式 展 览 记 录)
玩 家 商 店
(你 可 以 购 买 或 售 卖 物 品)
G P T 问 答
(向 A I 咨 询 服 务 器 的 情 况)
容器地图
数据的上次更新时间 —— {{openTime.toLocaleString()}}
当前维度总方块数量 —— {{positions.length}}
欢迎!(ノ^o^)ノ
请仔细阅读以下说明:
须点击左侧二维地图两次以设置三维地图中相机位置和朝向(相机高度会与箭头最近方块持平)
点击完成后,可拖动出现的黑色滑条来调整相机的高度(你现在还看不到它)
此后,在三维地图中,点击方块以查看它的坐标和内容
作者:邦邦拒绝魔抗
反馈:QQ-842748156
如遇地图选点等问题,请刷新页面
很好,你已经成功确定了三维地图中的相机位置
接下来,再次点击左侧二维地图,设置相机朝向
{{msg}}
数据总表
数据的上次更新时间 —— {{openTime.toLocaleString()}}
所选择的总记录条数 —— {{selectedTableCount}}
筛选玩家
容器类型
消息类型