koishi-plugin-tmp-bot 1.17.4 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,3 +25,9 @@ export function playerInfo(http: any, tmpId: any): Promise<{
25
25
  export function dlcList(http: any, type: any): Promise<{
26
26
  error: boolean;
27
27
  }>;
28
+ /**
29
+ * 玩家里程排行
30
+ */
31
+ export function mileageRankingList(http: any, rankingType: any, tmpId: any): Promise<{
32
+ error: boolean;
33
+ }>;
@@ -88,5 +88,27 @@ module.exports = {
88
88
  data.data = result.data;
89
89
  }
90
90
  return data;
91
+ },
92
+ /**
93
+ * 玩家里程排行
94
+ */
95
+ async mileageRankingList(http, rankingType, tmpId) {
96
+ let result = null;
97
+ try {
98
+ result = await http.get(`${BASE_API}/statistics/mileageRankingList?rankingType=${rankingType}&tmpId=${tmpId || ''}&rankingCount=10`);
99
+ }
100
+ catch (e) {
101
+ return {
102
+ error: true
103
+ };
104
+ }
105
+ // 拼接返回数据
106
+ let data = {
107
+ error: result.code !== 200
108
+ };
109
+ if (!data.error) {
110
+ data.data = result.data;
111
+ }
112
+ return data;
91
113
  }
92
114
  };
@@ -0,0 +1,3 @@
1
+ declare function _exports(ctx: any, session: any, rankingType: any): Promise<segment | "未启用 Puppeteer 功能" | "渲染异常,请重试" | "查询排行榜信息失败" | "暂无数据">;
2
+ export = _exports;
3
+ import { segment } from "@koishijs/core";
@@ -0,0 +1,55 @@
1
+ const { segment } = require('koishi');
2
+ const { resolve } = require('path');
3
+ const common = require('../util/common');
4
+ const evmOpenApi = require('../api/evmOpenApi');
5
+ const guildBind = require('../database/guildBind');
6
+ module.exports = async (ctx, session, rankingType) => {
7
+ if (!ctx.puppeteer) {
8
+ return '未启用 Puppeteer 功能';
9
+ }
10
+ // 查询排行榜信息
11
+ let mileageRankingList = await evmOpenApi.mileageRankingList(ctx.http, rankingType, null);
12
+ if (mileageRankingList.error) {
13
+ return '查询排行榜信息失败';
14
+ }
15
+ else if (mileageRankingList.data.length === 0) {
16
+ return '暂无数据';
17
+ }
18
+ // 查询当前玩家的排行信息
19
+ let guildBindData = await guildBind.get(ctx.database, session.platform, session.userId);
20
+ let playerMileageRanking = null;
21
+ if (guildBindData) {
22
+ let playerMileageRankingResult = await evmOpenApi.mileageRankingList(ctx.http, rankingType, guildBindData.tmp_id);
23
+ if (!playerMileageRankingResult.error && playerMileageRankingResult.data.length > 0) {
24
+ playerMileageRanking = playerMileageRankingResult.data[0];
25
+ }
26
+ }
27
+ // 拼接页面数据
28
+ let data = {
29
+ rankingType: rankingType,
30
+ mileageRankingList: mileageRankingList.data,
31
+ playerMileageRanking: playerMileageRanking
32
+ };
33
+ let page;
34
+ try {
35
+ page = await ctx.puppeteer.page();
36
+ await page.setViewport({ width: 1000, height: 1000 });
37
+ await page.goto(`file:///${resolve(__dirname, '../resource/mileage-leaderboard.html')}`);
38
+ await page.evaluate(`setData(${JSON.stringify(data)})`);
39
+ await page.waitForNetworkIdle();
40
+ await common.sleep(500);
41
+ const element = await page.$("#container");
42
+ return (segment.image(await element.screenshot({
43
+ encoding: "binary"
44
+ }), "image/jpg"));
45
+ }
46
+ catch (e) {
47
+ console.info(e);
48
+ return '渲染异常,请重试';
49
+ }
50
+ finally {
51
+ if (page) {
52
+ await page.close();
53
+ }
54
+ }
55
+ };
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ exports.Config = exports.inject = exports.name = void 0;
4
4
  exports.apply = apply;
5
5
  const koishi_1 = require("koishi");
6
6
  const model = require('./database/model');
7
+ const { MileageRankingType } = require('./util/constant');
7
8
  const tmpQuery = require('./command/tmpQuery/tmpQuery');
8
9
  const tmpServer = require('./command/tmpServer');
9
10
  const tmpBind = require('./command/tmpBind');
@@ -11,6 +12,7 @@ const tmpTraffic = require('./command/tmpTraffic/tmpTraffic');
11
12
  const tmpPosition = require('./command/tmpPosition');
12
13
  const tmpVersion = require('./command/tmpVersion');
13
14
  const tmpDlcMap = require('./command/tmpDlcMap');
15
+ const tmpMileageRanking = require('./command/tmpMileageRanking');
14
16
  exports.name = 'tmp-bot';
15
17
  exports.inject = {
16
18
  required: ['database'],
@@ -46,4 +48,6 @@ function apply(ctx, cfg) {
46
48
  ctx.command('tmpposition <tmpId>').action(async ({ session }, tmpId) => await tmpPosition(ctx, cfg, session, tmpId));
47
49
  ctx.command('tmpversion').action(async () => await tmpVersion(ctx));
48
50
  ctx.command('tmpdlcmap').action(async ({ session }) => await tmpDlcMap(ctx, session));
51
+ ctx.command('tmpmileageranking').action(async ({ session }) => await tmpMileageRanking(ctx, session, MileageRankingType.total));
52
+ ctx.command('tmptodaymileageranking').action(async ({ session }) => await tmpMileageRanking(ctx, session, MileageRankingType.today));
49
53
  }
@@ -0,0 +1,368 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Mileage Leaderboard</title>
6
+ <style>
7
+ @font-face {
8
+ font-family: 'segui-emj';
9
+ src: url('./package/SEGUIEMJ.TTF');
10
+ font-weight: normal;
11
+ font-style: normal;
12
+ }
13
+ body {
14
+ font-family: 'segui-emj', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
15
+ margin: 0;
16
+ padding: 0;
17
+ background-color: #000;
18
+ }
19
+
20
+ .emoji {
21
+ font-family: 'emoji-font', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
22
+ }
23
+
24
+ #container {
25
+ width: 340px;
26
+ background: linear-gradient(135deg, #1f2f54, #0f2c2a);
27
+ overflow: hidden;
28
+ padding-bottom: 12px;
29
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
30
+ }
31
+
32
+ .header {
33
+ height: 45px;
34
+ background-color: rgba(0, 0, 0, .1);
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ padding: 0 20px;
39
+ box-shadow: 0 0 16px rgba(0, 0, 0, .4);
40
+ }
41
+
42
+ .header .title {
43
+ color: #b0c7ff;
44
+ font-size: 16px;
45
+ font-weight: 600;
46
+ text-align: center;
47
+ }
48
+
49
+ .leaderboard-container {
50
+ padding: 12px 16px 0 16px;
51
+ }
52
+
53
+ .leaderboard-item {
54
+ background-color: rgba(0, 0, 0, 0.25);
55
+ margin-bottom: 4px;
56
+ border-radius: 6px;
57
+ overflow: hidden;
58
+ border: 1px solid rgba(255, 255, 255, 0.1);
59
+ }
60
+
61
+ .leaderboard-item.top3 {
62
+ background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.1));
63
+ border-color: rgba(255, 215, 0, 0.3);
64
+ box-shadow: 0 2px 8px rgba(255, 215, 0, 0.2);
65
+ }
66
+
67
+ .leaderboard-item.current-player {
68
+ background: linear-gradient(135deg, rgba(92, 227, 92, 0.3), rgba(92, 227, 92, 0.15));
69
+ border: 1px solid rgba(92, 227, 92, 0.6);
70
+ box-shadow: 0 2px 10px rgba(92, 227, 92, 0.3);
71
+ }
72
+
73
+ .item-content {
74
+ display: flex;
75
+ align-items: center;
76
+ padding: 8px 14px;
77
+ }
78
+
79
+ .rank {
80
+ width: 32px;
81
+ color: #ffffff;
82
+ font-size: 15px;
83
+ font-weight: bold;
84
+ text-align: center;
85
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
86
+ }
87
+
88
+ .current-player .rank {
89
+ min-width: 48px;
90
+ flex-shrink: 0;
91
+ }
92
+
93
+ .rank.top1 {
94
+ color: #ffd700;
95
+ text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
96
+ }
97
+
98
+ .rank.top2 {
99
+ color: #c0c0c0;
100
+ text-shadow: 0 0 6px rgba(192, 192, 192, 0.5);
101
+ }
102
+
103
+ .rank.top3 {
104
+ color: #cd7f32;
105
+ text-shadow: 0 0 6px rgba(205, 127, 50, 0.5);
106
+ }
107
+
108
+ .player-info {
109
+ flex: 1;
110
+ padding: 0 10px;
111
+ min-width: 0;
112
+ }
113
+
114
+ .player-name {
115
+ color: #ffffff;
116
+ font-size: 13px;
117
+ font-weight: 600;
118
+ white-space: nowrap;
119
+ overflow: hidden;
120
+ text-overflow: ellipsis;
121
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
122
+ max-width: 160px;
123
+ }
124
+
125
+ .current-player .player-name {
126
+ max-width: 144px;
127
+ }
128
+
129
+
130
+ .mileage-value {
131
+ color: #ffffff;
132
+ font-size: 13px;
133
+ font-weight: 700;
134
+ text-align: right;
135
+ white-space: nowrap;
136
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
137
+ min-width: 65px;
138
+ }
139
+
140
+ .divider {
141
+ height: 1px;
142
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
143
+ margin: 12px 16px;
144
+ }
145
+
146
+ .current-player-container {
147
+ padding: 0 16px;
148
+ }
149
+
150
+ .current-player-title {
151
+ color: #aaaaaa;
152
+ font-size: 12px;
153
+ text-align: center;
154
+ margin-bottom: 8px;
155
+ }
156
+
157
+ .footer-note {
158
+ color: #666666;
159
+ font-size: 10px;
160
+ text-align: center;
161
+ margin-top: 12px;
162
+ padding: 0 16px;
163
+ opacity: 0.8;
164
+ }
165
+ </style>
166
+ </head>
167
+ <body>
168
+ <div id="container">
169
+ <div class="header">
170
+ <div class="title">🏆 行驶里程排行榜</div>
171
+ </div>
172
+
173
+ <div class="leaderboard-container">
174
+ <!-- 排行榜前10名 -->
175
+ <div id="top-rankings"></div>
176
+ </div>
177
+
178
+ <div class="divider"></div>
179
+
180
+ <div class="current-player-container">
181
+ <div class="current-player-title">你的排名</div>
182
+ <div id="current-player"></div>
183
+ </div>
184
+
185
+ <div class="footer-note" id="footer-note">
186
+ * 数据统计说明
187
+ </div>
188
+ </div>
189
+
190
+ <script>
191
+ function createLeaderboardItem(rank, playerName, mileage, isCurrentPlayer = false, isTop3 = false) {
192
+ const item = document.createElement('div');
193
+ item.className = `leaderboard-item${isTop3 ? ' top3' : ''}${isCurrentPlayer ? ' current-player' : ''}`;
194
+
195
+ let rankClass = '';
196
+ if (rank === 1) rankClass = 'top1';
197
+ else if (rank === 2) rankClass = 'top2';
198
+ else if (rank === 3) rankClass = 'top3';
199
+
200
+ item.innerHTML = `
201
+ <div class="item-content">
202
+ <div class="rank ${rankClass}">#${rank}</div>
203
+ <div class="player-info">
204
+ <div class="player-name">${playerName}</div>
205
+ </div>
206
+ <div class="mileage-value">${formatMileage(mileage)}</div>
207
+ </div>
208
+ `;
209
+
210
+ return item;
211
+ }
212
+
213
+ function formatMileage(mileage) {
214
+ const meters = parseInt(mileage);
215
+ if (isNaN(meters) || meters < 0) return '0 km';
216
+
217
+ const km = meters / 1000;
218
+
219
+ if (km >= 1000000) {
220
+ const value = (km / 1000000).toFixed(2);
221
+ return value.endsWith('.00') ? Math.floor(km / 1000000) + 'M km' : value + 'M km';
222
+ } else if (km >= 1000) {
223
+ const value = (km / 1000).toFixed(2);
224
+ return value.endsWith('.00') ? Math.floor(km / 1000) + 'K km' : value + 'K km';
225
+ } else if (km >= 1) {
226
+ return Math.floor(km).toLocaleString() + ' km';
227
+ } else {
228
+ return meters.toLocaleString() + ' m';
229
+ }
230
+ }
231
+
232
+ // 新增:处理来自后端的动态数据
233
+ function setData(apiData) {
234
+ if (!apiData) {
235
+ console.error('数据无效');
236
+ return;
237
+ }
238
+
239
+ // 设置标题和底部脚注
240
+ const titleElement = document.querySelector('.header .title');
241
+ const footerElement = document.getElementById('footer-note');
242
+
243
+ if (titleElement) {
244
+ const titles = {
245
+ 1: '🏆 总行驶里程排行榜',
246
+ 2: '🏆 今日行驶里程排行榜'
247
+ };
248
+ const footerNotes = {
249
+ 1: '* 数据自 2025年8月23日 20:00 开始统计',
250
+ 2: '* 每日 0:00 重置统计'
251
+ };
252
+
253
+ const titleText = titles[apiData.rankingType] || '🏆 行驶里程排行榜';
254
+ titleElement.innerHTML = `<span class="emoji">${titleText.substring(0, 2)}</span>${titleText.substring(2)}`;
255
+ if (footerElement) {
256
+ footerElement.textContent = footerNotes[apiData.rankingType] || '* 数据统计说明';
257
+ }
258
+ }
259
+
260
+ const topRankings = document.getElementById('top-rankings');
261
+ const currentPlayerContainer = document.getElementById('current-player');
262
+
263
+ // 清空现有内容
264
+ topRankings.innerHTML = '';
265
+ currentPlayerContainer.innerHTML = '';
266
+
267
+ // 渲染前10名排行榜
268
+ if (apiData.mileageRankingList && Array.isArray(apiData.mileageRankingList)) {
269
+ apiData.mileageRankingList.slice(0, 10).forEach((player) => {
270
+ if (player && player.tmpName && typeof player.distance !== 'undefined' && typeof player.ranking !== 'undefined') {
271
+ const isTop3 = player.ranking <= 3;
272
+ const item = createLeaderboardItem(player.ranking, player.tmpName, player.distance, false, isTop3);
273
+ topRankings.appendChild(item);
274
+ }
275
+ });
276
+ }
277
+
278
+ // 渲染当前玩家排名
279
+ if (apiData.playerMileageRanking && apiData.playerMileageRanking.tmpName && typeof apiData.playerMileageRanking.ranking !== 'undefined') {
280
+ const currentItem = createLeaderboardItem(
281
+ apiData.playerMileageRanking.ranking,
282
+ apiData.playerMileageRanking.tmpName,
283
+ apiData.playerMileageRanking.distance,
284
+ true,
285
+ false
286
+ );
287
+ currentPlayerContainer.appendChild(currentItem);
288
+ } else {
289
+ // 如果没有当前玩家数据,隐藏该部分
290
+ const divider = document.querySelector('.divider');
291
+ const currentPlayerSection = document.querySelector('.current-player-container');
292
+ if (divider) divider.style.display = 'none';
293
+ if (currentPlayerSection) currentPlayerSection.style.display = 'none';
294
+ }
295
+ }
296
+
297
+ function init(data) {
298
+ if (!data) {
299
+ console.error('数据无效');
300
+ return;
301
+ }
302
+
303
+ const topRankings = document.getElementById('top-rankings');
304
+ const currentPlayerContainer = document.getElementById('current-player');
305
+
306
+ // 清空现有内容
307
+ topRankings.innerHTML = '';
308
+ currentPlayerContainer.innerHTML = '';
309
+
310
+ // 渲染前10名排行榜
311
+ if (data.topRankings && Array.isArray(data.topRankings)) {
312
+ data.topRankings.slice(0, 10).forEach((player, index) => {
313
+ if (player && player.name && typeof player.mileage !== 'undefined') {
314
+ const rank = index + 1;
315
+ const isTop3 = rank <= 3;
316
+ const item = createLeaderboardItem(rank, player.name, player.mileage, false, isTop3);
317
+ topRankings.appendChild(item);
318
+ }
319
+ });
320
+ }
321
+
322
+ // 渲染当前玩家排名
323
+ if (data.currentPlayer && data.currentPlayer.name && typeof data.currentPlayer.rank !== 'undefined') {
324
+ const currentItem = createLeaderboardItem(
325
+ data.currentPlayer.rank,
326
+ data.currentPlayer.name,
327
+ data.currentPlayer.mileage,
328
+ true,
329
+ false
330
+ );
331
+ currentPlayerContainer.appendChild(currentItem);
332
+ } else {
333
+ // 如果没有当前玩家数据,隐藏该部分
334
+ const divider = document.querySelector('.divider');
335
+ const currentPlayerSection = document.querySelector('.current-player-container');
336
+ if (divider) divider.style.display = 'none';
337
+ if (currentPlayerSection) currentPlayerSection.style.display = 'none';
338
+ }
339
+ }
340
+
341
+ // 示例数据 - 实际使用时将被外部数据替换
342
+ const sampleData = {
343
+ topRankings: [
344
+ { name: "Afo780", mileage: 347167 },
345
+ { name: "lαdу ναlєяijα", mileage: 331177 },
346
+ { name: "Stefan Z", mileage: 330658 },
347
+ { name: "YorkshireSlacka", mileage: 328871 },
348
+ { name: "Sun [PL]", mileage: 327208 },
349
+ { name: "joewales96 LAG", mileage: 324952 },
350
+ { name: "Turbo_Express", mileage: 320211 },
351
+ { name: "gapcio9933", mileage: 319599 },
352
+ { name: "R8 anderson", mileage: 319004 },
353
+ { name: "lorddarki85", mileage: 317788 }
354
+ ],
355
+ currentPlayer: {
356
+ rank: 3365,
357
+ name: "ZyRoX Drum",
358
+ mileage: 59515
359
+ }
360
+ };
361
+
362
+ // 页面加载时初始化示例数据
363
+ document.addEventListener('DOMContentLoaded', function() {
364
+ init(sampleData);
365
+ });
366
+ </script>
367
+ </body>
368
+ </html>
@@ -2,3 +2,7 @@ export namespace TmpTrafficType {
2
2
  let text: number;
3
3
  let heatMap: number;
4
4
  }
5
+ export namespace MileageRankingType {
6
+ let total: number;
7
+ let today: number;
8
+ }
@@ -5,5 +5,12 @@ module.exports = {
5
5
  TmpTrafficType: {
6
6
  text: 1, // 文本
7
7
  heatMap: 2, // 热力图
8
+ },
9
+ /**
10
+ * 里程排行榜类型
11
+ */
12
+ MileageRankingType: {
13
+ total: 1,
14
+ today: 2
8
15
  }
9
16
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-tmp-bot",
3
3
  "description": "欧洲卡车模拟2 TMP查询插件,不会部署的可以直接使用此机器人->QQ:3523283907",
4
- "version": "1.17.4",
4
+ "version": "1.18.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "homepage": "https://github.com/79887143/koishi-plugin-tmp-bot",