koishi-plugin-ets2-tools-tmp 3.0.4 → 3.0.6
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.
package/lib/api/evmOpenApi.js
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const { segment } = require('koishi');
|
|
2
|
+
const { resolve } = require('path');
|
|
3
|
+
const common = require('../../../util/common');
|
|
4
|
+
|
|
5
|
+
module.exports = async (ctx, cfg, session) => {
|
|
6
|
+
// 群组限制检查
|
|
7
|
+
const mainGroups = cfg.tmpActivityService?.mainGroup?.groups || [];
|
|
8
|
+
const adminGroups = cfg.tmpActivityService?.admin?.groups || [];
|
|
9
|
+
const allowedGroups = [...new Set([...mainGroups, ...adminGroups])];
|
|
10
|
+
|
|
11
|
+
if (allowedGroups.length === 0) {
|
|
12
|
+
return '未配置允许使用此指令的群组';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 当前会话的群号(onebot 平台格式为 group:xxx)
|
|
16
|
+
const currentGroupId = session.channelId;
|
|
17
|
+
const isAllowed = allowedGroups.some(g => currentGroupId.includes(g));
|
|
18
|
+
|
|
19
|
+
if (!isAllowed) {
|
|
20
|
+
return '该指令在本群聊不可用';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!ctx.puppeteer) {
|
|
24
|
+
return '未启用 Puppeteer 功能';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const settings = cfg.mainSettings?.settings || {};
|
|
28
|
+
const baseUrl = settings.url || "open.vtcm.link";
|
|
29
|
+
const token = settings.token || "";
|
|
30
|
+
const logOutput = settings.logOutput;
|
|
31
|
+
|
|
32
|
+
const log = (msg) => logOutput && ctx.logger.info(msg);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
log('[积分排行] 开始查询车队成员积分信息');
|
|
36
|
+
|
|
37
|
+
// 分页获取全部成员数据(API page 参数为 1-indexed)
|
|
38
|
+
const allMembers = [];
|
|
39
|
+
let currentPage = 1;
|
|
40
|
+
let totalPage = 1;
|
|
41
|
+
|
|
42
|
+
while (currentPage <= totalPage) {
|
|
43
|
+
const queryUrl = `https://${baseUrl}/api/user/info/list?page=${currentPage}&limit=100&tmpId=&tmpName=&teamId=&qq=&state=0&teamRole=&token=${token}`;
|
|
44
|
+
log(`[积分排行] 请求第 ${currentPage} 页: ${queryUrl.replace(token, "***")}`);
|
|
45
|
+
|
|
46
|
+
const response = await ctx.http.get(queryUrl, { timeout: 10000 });
|
|
47
|
+
|
|
48
|
+
if (response.code !== 0) {
|
|
49
|
+
log(`[积分排行] 第 ${currentPage} 页请求失败: ${response.msg || '未知错误'}`);
|
|
50
|
+
return response.msg || '查询成员信息失败';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!response.page?.list) {
|
|
54
|
+
log(`[积分排行] 第 ${currentPage} 页数据为空`);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
allMembers.push(...response.page.list);
|
|
59
|
+
totalPage = response.page.totalPage || 1;
|
|
60
|
+
currentPage++;
|
|
61
|
+
|
|
62
|
+
log(`[积分排行] 已获取 ${allMembers.length}/${response.page.totalCount} 条记录, 总页数: ${totalPage}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (allMembers.length === 0) {
|
|
66
|
+
return '暂无成员数据';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 按 id 去重(防止分页返回重复数据)
|
|
70
|
+
const uniqueMembers = [];
|
|
71
|
+
const seenIds = new Set();
|
|
72
|
+
for (const m of allMembers) {
|
|
73
|
+
if (m.id && !seenIds.has(m.id)) {
|
|
74
|
+
seenIds.add(m.id);
|
|
75
|
+
uniqueMembers.push(m);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
log(`[积分排行] 去重前: ${allMembers.length}, 去重后: ${uniqueMembers.length}`);
|
|
79
|
+
|
|
80
|
+
// 按 rewardPoints 降序排序
|
|
81
|
+
const sortedMembers = uniqueMembers
|
|
82
|
+
.filter(m => typeof m.rewardPoints === 'number')
|
|
83
|
+
.sort((a, b) => b.rewardPoints - a.rewardPoints);
|
|
84
|
+
|
|
85
|
+
// 取前10
|
|
86
|
+
const top10 = sortedMembers
|
|
87
|
+
.slice(0, 10)
|
|
88
|
+
.map((player, index) => ({
|
|
89
|
+
rank: index + 1,
|
|
90
|
+
tmpName: player.tmpName || '未知',
|
|
91
|
+
tmpId: player.tmpId,
|
|
92
|
+
teamId: player.teamId,
|
|
93
|
+
rewardPoints: player.rewardPoints || 0,
|
|
94
|
+
teamRole: player.teamRole || '',
|
|
95
|
+
avatarUrl: player.avatarUrl || ''
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
log(`[积分排行] 排行榜前10名已生成, 第一名: ${top10[0]?.tmpName} (${top10[0]?.rewardPoints}积分)`);
|
|
99
|
+
|
|
100
|
+
// 查询当前用户的排名
|
|
101
|
+
let currentPlayer = null;
|
|
102
|
+
const currentUserQQ = String(session.userId || '');
|
|
103
|
+
if (currentUserQQ) {
|
|
104
|
+
const playerIndex = sortedMembers.findIndex(m => String(m.qq) === currentUserQQ);
|
|
105
|
+
if (playerIndex !== -1) {
|
|
106
|
+
const player = sortedMembers[playerIndex];
|
|
107
|
+
currentPlayer = {
|
|
108
|
+
rank: playerIndex + 1,
|
|
109
|
+
tmpName: player.tmpName || '未知',
|
|
110
|
+
tmpId: player.tmpId,
|
|
111
|
+
teamId: player.teamId,
|
|
112
|
+
rewardPoints: player.rewardPoints || 0,
|
|
113
|
+
avatarUrl: player.avatarUrl || ''
|
|
114
|
+
};
|
|
115
|
+
log(`[积分排行] 找到当前用户: ${currentPlayer.tmpName}, 排名: ${currentPlayer.rank}, 积分: ${currentPlayer.rewardPoints}`);
|
|
116
|
+
} else {
|
|
117
|
+
log(`[积分排行] 未在车队成员中找到当前用户QQ: ${currentUserQQ}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 拼接页面数据
|
|
122
|
+
const data = {
|
|
123
|
+
top10: top10,
|
|
124
|
+
totalCount: uniqueMembers.length,
|
|
125
|
+
currentPlayer: currentPlayer,
|
|
126
|
+
updateTime: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// 渲染图片
|
|
130
|
+
let page;
|
|
131
|
+
try {
|
|
132
|
+
page = await ctx.puppeteer.page();
|
|
133
|
+
await page.setViewport({ width: 800, height: 1200, deviceScaleFactor: 2 });
|
|
134
|
+
await page.goto(`file:///${resolve(__dirname, '../../../resource/point-leaderboard.html')}`);
|
|
135
|
+
await page.evaluate(`setData(${JSON.stringify(data)})`);
|
|
136
|
+
await page.waitForNetworkIdle();
|
|
137
|
+
await common.sleep(800);
|
|
138
|
+
const element = await page.$("#container");
|
|
139
|
+
return segment.image(await element.screenshot({
|
|
140
|
+
encoding: "binary"
|
|
141
|
+
}), "image/jpg");
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
log(`[积分排行] 渲染异常: ${e.message}`);
|
|
145
|
+
return '渲染异常,请重试';
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (page) {
|
|
149
|
+
await page.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
log(`[积分排行] 系统错误: ${error.message}`);
|
|
154
|
+
if (error.response) {
|
|
155
|
+
return `请求失败: ${error.response.status} ${error.response.statusText}`;
|
|
156
|
+
}
|
|
157
|
+
return '系统错误,请稍后重试';
|
|
158
|
+
}
|
|
159
|
+
};
|
|
@@ -143,6 +143,14 @@ class ActivityService {
|
|
|
143
143
|
}, `无活动通知定时器: ${this.cfg.noActivity.time}`);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
if (this.cfg.onlineCheck?.enable) {
|
|
147
|
+
const [onlineHours, onlineMinutes] = this.cfg.onlineCheck.time.split(":").map(Number);
|
|
148
|
+
this.setupTimer(onlineHours, onlineMinutes, () => {
|
|
149
|
+
this.logger.timing(`执行在线成员检查任务 (${this.cfg.onlineCheck.time})`);
|
|
150
|
+
this.checkAndSendOnlineMemberReport();
|
|
151
|
+
}, `在线成员检查定时器: ${this.cfg.onlineCheck.time}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
146
154
|
const minuteTimer = setInterval(async () => {
|
|
147
155
|
await this.checkAndSendActivityReminders();
|
|
148
156
|
}, koishi_1.Time.minute);
|
|
@@ -342,6 +350,60 @@ class ActivityService {
|
|
|
342
350
|
}
|
|
343
351
|
}
|
|
344
352
|
|
|
353
|
+
async checkAndSendOnlineMemberReport(manualTest = false) {
|
|
354
|
+
try {
|
|
355
|
+
if (this.todayActivities.length === 0 && this.todayTMPEvents.length === 0) {
|
|
356
|
+
this.logger.info(`[在线成员检查] 今日无活动,跳过在线成员检查`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const apiUrl = this.cfg.onlineCheck?.apiUrl;
|
|
361
|
+
if (!apiUrl) {
|
|
362
|
+
this.logger.error(`[在线成员检查] 未配置API地址`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.logger.api(`[在线成员检查] 请求在线成员API: ${apiUrl}`);
|
|
367
|
+
const startTime = Date.now();
|
|
368
|
+
const response = await this.ctx.http.get(apiUrl, { timeout: 10000 });
|
|
369
|
+
const duration = Date.now() - startTime;
|
|
370
|
+
this.logger.api(`[在线成员检查] API响应耗时: ${duration}ms, code: ${response.code}`);
|
|
371
|
+
|
|
372
|
+
if (response.code !== 200 || !Array.isArray(response.data)) {
|
|
373
|
+
this.logger.error(`[在线成员检查] API返回错误: ${response.msg || '未知错误'} (code: ${response.code})`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const onlineMembers = response.data.filter(member => member.isOnline === true);
|
|
378
|
+
this.logger.info(`[在线成员检查] 总成员数: ${response.data.length}, 在线成员数: ${onlineMembers.length}`);
|
|
379
|
+
|
|
380
|
+
if (onlineMembers.length === 0) {
|
|
381
|
+
this.logger.info(`[在线成员检查] 当前无在线成员`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let message = `🎮 今日活动在线成员 (${onlineMembers.length}人)\n`;
|
|
386
|
+
message += `━━━━━━━━━━━━━━━━\n`;
|
|
387
|
+
onlineMembers.forEach((member, index) => {
|
|
388
|
+
message += `${index + 1}. ${member.name}\n`;
|
|
389
|
+
message += ` TMP ID: ${member.tmpId}\n`;
|
|
390
|
+
message += ` 服务器: ${member.serverName || '未知'}\n`;
|
|
391
|
+
message += ` 更新时间: ${member.updateTime || '未知'}\n`;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
for (const groupId of this.cfg.admin.groups) {
|
|
395
|
+
try {
|
|
396
|
+
await this.sendToGroup(groupId, message, "管理群组");
|
|
397
|
+
this.logger.message(`[在线成员检查] 已发送在线成员报告到管理群组 ${groupId}`);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
this.logger.error(`[在线成员检查] 发送到管理群组 ${groupId} 失败:`, error.message);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.logger.error(`[在线成员检查] 失败:`, error.message);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
345
407
|
async checkAndSendActivityReminders() {
|
|
346
408
|
const now = new Date();
|
|
347
409
|
const today = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const commands = {
|
|
|
21
21
|
queryPoint: require('./command/ets-app/queryPoint/queryPoint'),
|
|
22
22
|
changePoint: require('./command/ets-app/changePoint/changePoint'),
|
|
23
23
|
addMember: require('./command/ets-app/addMember/addMemberV2'),
|
|
24
|
+
pointRanking: require('./command/ets-app/pointRanking/pointRanking'),
|
|
24
25
|
tmpVtc: require('./command/tmpVtc'),
|
|
25
26
|
tmpFootprint: require('./command/tmpFootprint')
|
|
26
27
|
};
|
|
@@ -75,6 +76,7 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
75
76
|
tmpVersion: koishi_1.Schema.boolean().default(true).description('是否启用版本查询'),
|
|
76
77
|
tmpDlcMap: koishi_1.Schema.boolean().default(true).description('是否启用DLC地图查询'),
|
|
77
78
|
tmpMileageRanking: koishi_1.Schema.boolean().default(true).description('是否启用里程排行榜'),
|
|
79
|
+
pointRanking: koishi_1.Schema.boolean().default(false).description('是否启用积分排行榜'),
|
|
78
80
|
tmpVtc: koishi_1.Schema.boolean().default(true).description('是否启用VTC查询'),
|
|
79
81
|
tmpFootprint: koishi_1.Schema.boolean().default(true).description('是否启用足迹查询'),
|
|
80
82
|
mainSettings: koishi_1.Schema.boolean().default(false).description('是否启用车队平台积分查询功能'),
|
|
@@ -186,6 +188,11 @@ exports.Config = koishi_1.Schema.intersect([
|
|
|
186
188
|
time: koishi_1.Schema.string().description("今日无活动通知发送时间(HH:mm格式)").default("09:00"),
|
|
187
189
|
message: koishi_1.Schema.string().description("今日无活动通知消息").default("今日没活动")
|
|
188
190
|
}).description("无活动通知配置"),
|
|
191
|
+
onlineCheck: koishi_1.Schema.object({
|
|
192
|
+
enable: koishi_1.Schema.boolean().description("启用今日有活动时的在线成员检查").default(false),
|
|
193
|
+
time: koishi_1.Schema.string().description("在线成员检查发送时间(HH:mm格式)").default("20:30"),
|
|
194
|
+
apiUrl: koishi_1.Schema.string().description("在线成员查询API地址(含vtcId参数)")
|
|
195
|
+
}).description("在线成员检查配置"),
|
|
189
196
|
mainGroup: koishi_1.Schema.object({
|
|
190
197
|
groups: koishi_1.Schema.array(koishi_1.Schema.string()).role("table").description("主群群号列表").default([]),
|
|
191
198
|
activityReminderMessage: koishi_1.Schema.string().description("活动提醒消息模板,支持变量:{name}, {server}, {startingPoint}, {terminalPoint}, {distance}, {banner}, {timeLeft}").default("活动 {name} 还有 {timeLeft} 分钟就要开始啦!\n服务器: {server}\n起点: {startingPoint}\n终点: {terminalPoint}\n距离: {distance}KM"),
|
|
@@ -215,6 +222,7 @@ function logDisabledCommands(ctx, cfg) {
|
|
|
215
222
|
{ key: 'tmpVersion', label: 'tmp版本' },
|
|
216
223
|
{ key: 'tmpDlcMap', label: '地图dlc价格' },
|
|
217
224
|
{ key: 'tmpMileageRanking', label: '里程排行榜/今日里程排行榜' },
|
|
225
|
+
{ key: 'pointRanking', label: '积分排行榜' },
|
|
218
226
|
{ key: 'tmpVtc', label: 'vtc查询' },
|
|
219
227
|
{ key: 'tmpFootprint', label: '足迹查询' },
|
|
220
228
|
{ key: 'resetPassword', label: '重置密码' },
|
|
@@ -289,6 +297,12 @@ function registerBaseCommands(ctx, cfg) {
|
|
|
289
297
|
.action(async ({ session }) => await commands.tmpMileageRanking(ctx, session, MileageRankingType.today));
|
|
290
298
|
}
|
|
291
299
|
|
|
300
|
+
if (cfg.commands?.pointRanking) {
|
|
301
|
+
ctx.command('积分排行')
|
|
302
|
+
.usage("查询车队积分排行榜(仅限总群和管理群使用)")
|
|
303
|
+
.action(async ({ session }) => await commands.pointRanking(ctx, cfg, session));
|
|
304
|
+
}
|
|
305
|
+
|
|
292
306
|
if (cfg.commands?.tmpVtc) {
|
|
293
307
|
ctx.command('vtc查询 <vtcid>')
|
|
294
308
|
.usage("查询TruckersMP VTC信息")
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>积分排行榜</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
|
+
|
|
14
|
+
* {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: 'segui-emj', 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
background-color: #000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#container {
|
|
26
|
+
width: 520px;
|
|
27
|
+
background: linear-gradient(160deg, #1a0b2e 0%, #16213e 35%, #0f3460 70%, #1a0b2e 100%);
|
|
28
|
+
padding: 0 0 20px 0;
|
|
29
|
+
position: relative;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* 装饰性背景光晕 */
|
|
35
|
+
#container::before {
|
|
36
|
+
content: '';
|
|
37
|
+
position: absolute;
|
|
38
|
+
top: -100px;
|
|
39
|
+
left: -100px;
|
|
40
|
+
width: 300px;
|
|
41
|
+
height: 300px;
|
|
42
|
+
background: radial-gradient(circle, rgba(255, 215, 0, 0.15) 0%, transparent 70%);
|
|
43
|
+
pointer-events: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#container::after {
|
|
47
|
+
content: '';
|
|
48
|
+
position: absolute;
|
|
49
|
+
bottom: -100px;
|
|
50
|
+
right: -100px;
|
|
51
|
+
width: 300px;
|
|
52
|
+
height: 300px;
|
|
53
|
+
background: radial-gradient(circle, rgba(138, 43, 226, 0.15) 0%, transparent 70%);
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.header {
|
|
58
|
+
padding: 28px 24px 20px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
position: relative;
|
|
61
|
+
z-index: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.header-icon {
|
|
65
|
+
font-size: 36px;
|
|
66
|
+
margin-bottom: 8px;
|
|
67
|
+
filter: drop-shadow(0 0 12px rgba(255, 215, 0, 0.6));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.header-title {
|
|
71
|
+
color: transparent;
|
|
72
|
+
background: linear-gradient(135deg, #ffd700 0%, #ffaa00 50%, #ff6b35 100%);
|
|
73
|
+
-webkit-background-clip: text;
|
|
74
|
+
background-clip: text;
|
|
75
|
+
font-size: 26px;
|
|
76
|
+
font-weight: 800;
|
|
77
|
+
letter-spacing: 3px;
|
|
78
|
+
text-shadow: 0 2px 8px rgba(255, 215, 0, 0.3);
|
|
79
|
+
margin: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.header-subtitle {
|
|
83
|
+
color: #a8b2c8;
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
margin-top: 6px;
|
|
86
|
+
letter-spacing: 1px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.header-decoration {
|
|
90
|
+
width: 80px;
|
|
91
|
+
height: 2px;
|
|
92
|
+
background: linear-gradient(90deg, transparent, #ffd700, transparent);
|
|
93
|
+
margin: 12px auto 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* 前三名特殊区域 */
|
|
97
|
+
.top3-container {
|
|
98
|
+
padding: 8px 20px 20px;
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
align-items: flex-end;
|
|
102
|
+
gap: 12px;
|
|
103
|
+
position: relative;
|
|
104
|
+
z-index: 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.top3-item {
|
|
108
|
+
flex: 1;
|
|
109
|
+
max-width: 145px;
|
|
110
|
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.02) 100%);
|
|
111
|
+
border-radius: 12px;
|
|
112
|
+
padding: 16px 8px 12px;
|
|
113
|
+
text-align: center;
|
|
114
|
+
position: relative;
|
|
115
|
+
backdrop-filter: blur(10px);
|
|
116
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.top3-item.rank-1 {
|
|
120
|
+
background: linear-gradient(180deg, rgba(255, 215, 0, 0.25) 0%, rgba(255, 165, 0, 0.1) 100%);
|
|
121
|
+
border-color: rgba(255, 215, 0, 0.5);
|
|
122
|
+
box-shadow: 0 0 24px rgba(255, 215, 0, 0.4), inset 0 0 12px rgba(255, 215, 0, 0.1);
|
|
123
|
+
transform: translateY(-12px);
|
|
124
|
+
order: 2;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.top3-item.rank-2 {
|
|
128
|
+
background: linear-gradient(180deg, rgba(192, 192, 192, 0.2) 0%, rgba(150, 150, 150, 0.08) 100%);
|
|
129
|
+
border-color: rgba(192, 192, 192, 0.4);
|
|
130
|
+
box-shadow: 0 0 16px rgba(192, 192, 192, 0.3);
|
|
131
|
+
order: 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.top3-item.rank-3 {
|
|
135
|
+
background: linear-gradient(180deg, rgba(205, 127, 50, 0.2) 0%, rgba(160, 100, 40, 0.08) 100%);
|
|
136
|
+
border-color: rgba(205, 127, 50, 0.4);
|
|
137
|
+
box-shadow: 0 0 16px rgba(205, 127, 50, 0.3);
|
|
138
|
+
order: 3;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.crown {
|
|
142
|
+
font-size: 22px;
|
|
143
|
+
margin-bottom: 4px;
|
|
144
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.rank-1 .crown { font-size: 28px; }
|
|
148
|
+
|
|
149
|
+
.top3-rank-num {
|
|
150
|
+
font-size: 32px;
|
|
151
|
+
font-weight: 900;
|
|
152
|
+
color: #ffffff;
|
|
153
|
+
margin: 4px 0;
|
|
154
|
+
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
|
155
|
+
line-height: 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.rank-1 .top3-rank-num { color: #ffd700; text-shadow: 0 0 12px rgba(255, 215, 0, 0.8); }
|
|
159
|
+
.rank-2 .top3-rank-num { color: #e0e0e0; text-shadow: 0 0 8px rgba(192, 192, 192, 0.6); }
|
|
160
|
+
.rank-3 .top3-rank-num { color: #cd7f32; text-shadow: 0 0 8px rgba(205, 127, 50, 0.6); }
|
|
161
|
+
|
|
162
|
+
.top3-avatar {
|
|
163
|
+
width: 56px;
|
|
164
|
+
height: 56px;
|
|
165
|
+
border-radius: 50%;
|
|
166
|
+
margin: 8px auto;
|
|
167
|
+
display: block;
|
|
168
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
169
|
+
object-fit: cover;
|
|
170
|
+
background: rgba(0, 0, 0, 0.3);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.rank-1 .top3-avatar { width: 64px; height: 64px; border-color: rgba(255, 215, 0, 0.6); }
|
|
174
|
+
|
|
175
|
+
.top3-name {
|
|
176
|
+
color: #ffffff;
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
font-weight: 700;
|
|
179
|
+
margin: 6px 0 2px;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
overflow: hidden;
|
|
182
|
+
text-overflow: ellipsis;
|
|
183
|
+
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.top3-teamid {
|
|
187
|
+
color: #a8b2c8;
|
|
188
|
+
font-size: 10px;
|
|
189
|
+
margin-bottom: 4px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.top3-teamid span {
|
|
193
|
+
color: #ffd700;
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.top3-points {
|
|
198
|
+
color: #ffd700;
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
font-weight: 800;
|
|
201
|
+
text-shadow: 0 0 6px rgba(255, 215, 0, 0.4);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.rank-2 .top3-points { color: #e0e0e0; text-shadow: 0 0 6px rgba(192, 192, 192, 0.4); }
|
|
205
|
+
.rank-3 .top3-points { color: #cd7f32; text-shadow: 0 0 6px rgba(205, 127, 50, 0.4); }
|
|
206
|
+
|
|
207
|
+
.top3-points-label {
|
|
208
|
+
color: #888;
|
|
209
|
+
font-size: 10px;
|
|
210
|
+
margin-left: 2px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* 第4-10名列表 */
|
|
214
|
+
.rank-list {
|
|
215
|
+
padding: 0 20px;
|
|
216
|
+
position: relative;
|
|
217
|
+
z-index: 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.rank-list-title {
|
|
221
|
+
color: #a8b2c8;
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
text-align: center;
|
|
224
|
+
margin: 16px 0 12px;
|
|
225
|
+
letter-spacing: 2px;
|
|
226
|
+
position: relative;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.rank-list-title::before,
|
|
230
|
+
.rank-list-title::after {
|
|
231
|
+
content: '';
|
|
232
|
+
position: absolute;
|
|
233
|
+
top: 50%;
|
|
234
|
+
width: 60px;
|
|
235
|
+
height: 1px;
|
|
236
|
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.rank-list-title::before { left: 80px; }
|
|
240
|
+
.rank-list-title::after { right: 80px; }
|
|
241
|
+
|
|
242
|
+
.rank-item {
|
|
243
|
+
display: flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
padding: 10px 14px;
|
|
246
|
+
margin-bottom: 6px;
|
|
247
|
+
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
|
|
248
|
+
border-radius: 8px;
|
|
249
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
250
|
+
transition: all 0.3s ease;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.rank-item:hover {
|
|
254
|
+
background: rgba(255, 255, 255, 0.08);
|
|
255
|
+
border-color: rgba(255, 255, 255, 0.15);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.rank-num {
|
|
259
|
+
width: 36px;
|
|
260
|
+
height: 36px;
|
|
261
|
+
border-radius: 50%;
|
|
262
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.05));
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
justify-content: center;
|
|
266
|
+
color: #ffffff;
|
|
267
|
+
font-size: 14px;
|
|
268
|
+
font-weight: 700;
|
|
269
|
+
margin-right: 12px;
|
|
270
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
271
|
+
flex-shrink: 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.rank-avatar {
|
|
275
|
+
width: 36px;
|
|
276
|
+
height: 36px;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
margin-right: 12px;
|
|
279
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
280
|
+
object-fit: cover;
|
|
281
|
+
background: rgba(0, 0, 0, 0.3);
|
|
282
|
+
flex-shrink: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.rank-info {
|
|
286
|
+
flex: 1;
|
|
287
|
+
min-width: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.rank-name {
|
|
291
|
+
color: #ffffff;
|
|
292
|
+
font-size: 14px;
|
|
293
|
+
font-weight: 600;
|
|
294
|
+
white-space: nowrap;
|
|
295
|
+
overflow: hidden;
|
|
296
|
+
text-overflow: ellipsis;
|
|
297
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.rank-tmpid {
|
|
301
|
+
color: #666;
|
|
302
|
+
font-size: 10px;
|
|
303
|
+
margin-top: 2px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.rank-tmpid .team-id {
|
|
307
|
+
color: #ffd700;
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
margin-left: 6px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.rank-points {
|
|
313
|
+
color: #ffd700;
|
|
314
|
+
font-size: 16px;
|
|
315
|
+
font-weight: 800;
|
|
316
|
+
text-shadow: 0 0 6px rgba(255, 215, 0, 0.3);
|
|
317
|
+
white-space: nowrap;
|
|
318
|
+
flex-shrink: 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.rank-points-label {
|
|
322
|
+
color: #888;
|
|
323
|
+
font-size: 10px;
|
|
324
|
+
margin-left: 2px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* 底部信息 */
|
|
328
|
+
.footer {
|
|
329
|
+
margin-top: 20px;
|
|
330
|
+
padding: 12px 20px 0;
|
|
331
|
+
text-align: center;
|
|
332
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
333
|
+
position: relative;
|
|
334
|
+
z-index: 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.footer-total {
|
|
338
|
+
color: #a8b2c8;
|
|
339
|
+
font-size: 11px;
|
|
340
|
+
margin-bottom: 4px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.footer-total span {
|
|
344
|
+
color: #ffd700;
|
|
345
|
+
font-weight: 700;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.footer-update {
|
|
349
|
+
color: #555;
|
|
350
|
+
font-size: 10px;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* 当前用户排名区域 */
|
|
354
|
+
.current-player-section {
|
|
355
|
+
margin: 16px 20px 0;
|
|
356
|
+
padding: 14px 16px;
|
|
357
|
+
background: linear-gradient(90deg, rgba(255, 215, 0, 0.15) 0%, rgba(255, 165, 0, 0.08) 100%);
|
|
358
|
+
border-radius: 10px;
|
|
359
|
+
border: 1px solid rgba(255, 215, 0, 0.3);
|
|
360
|
+
box-shadow: 0 0 12px rgba(255, 215, 0, 0.15);
|
|
361
|
+
position: relative;
|
|
362
|
+
z-index: 1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.current-player-title {
|
|
366
|
+
color: #ffd700;
|
|
367
|
+
font-size: 12px;
|
|
368
|
+
text-align: center;
|
|
369
|
+
margin-bottom: 10px;
|
|
370
|
+
letter-spacing: 2px;
|
|
371
|
+
font-weight: 600;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.current-player-content {
|
|
375
|
+
display: flex;
|
|
376
|
+
align-items: center;
|
|
377
|
+
gap: 12px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.current-player-rank {
|
|
381
|
+
width: 44px;
|
|
382
|
+
height: 44px;
|
|
383
|
+
border-radius: 50%;
|
|
384
|
+
background: linear-gradient(135deg, #ffd700, #ff8c00);
|
|
385
|
+
display: flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
justify-content: center;
|
|
388
|
+
color: #1a0b2e;
|
|
389
|
+
font-size: 16px;
|
|
390
|
+
font-weight: 800;
|
|
391
|
+
flex-shrink: 0;
|
|
392
|
+
box-shadow: 0 0 10px rgba(255, 215, 0, 0.4);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.current-player-info {
|
|
396
|
+
flex: 1;
|
|
397
|
+
min-width: 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.current-player-name {
|
|
401
|
+
color: #ffffff;
|
|
402
|
+
font-size: 14px;
|
|
403
|
+
font-weight: 700;
|
|
404
|
+
white-space: nowrap;
|
|
405
|
+
overflow: hidden;
|
|
406
|
+
text-overflow: ellipsis;
|
|
407
|
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.current-player-meta {
|
|
411
|
+
color: #a8b2c8;
|
|
412
|
+
font-size: 11px;
|
|
413
|
+
margin-top: 3px;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.current-player-meta .team-id {
|
|
417
|
+
color: #ffd700;
|
|
418
|
+
margin-left: 6px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.current-player-points {
|
|
422
|
+
color: #ffd700;
|
|
423
|
+
font-size: 18px;
|
|
424
|
+
font-weight: 800;
|
|
425
|
+
text-shadow: 0 0 8px rgba(255, 215, 0, 0.4);
|
|
426
|
+
white-space: nowrap;
|
|
427
|
+
flex-shrink: 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.current-player-points-label {
|
|
431
|
+
color: #888;
|
|
432
|
+
font-size: 11px;
|
|
433
|
+
margin-left: 2px;
|
|
434
|
+
}
|
|
435
|
+
</style>
|
|
436
|
+
</head>
|
|
437
|
+
<body>
|
|
438
|
+
<div id="container">
|
|
439
|
+
<div class="header">
|
|
440
|
+
<div class="header-icon">🏆</div>
|
|
441
|
+
<h1 class="header-title">积 分 排 行 榜</h1>
|
|
442
|
+
<div class="header-subtitle">POINTS LEADERBOARD</div>
|
|
443
|
+
<div class="header-decoration"></div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<!-- 前三名展示 -->
|
|
447
|
+
<div class="top3-container" id="top3"></div>
|
|
448
|
+
|
|
449
|
+
<!-- 第4-10名列表 -->
|
|
450
|
+
<div class="rank-list" id="rankList" style="display:none;">
|
|
451
|
+
<div class="rank-list-title">— 其他排名 —</div>
|
|
452
|
+
<div id="rankListContent"></div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<!-- 当前用户排名 -->
|
|
456
|
+
<div class="current-player-section" id="currentPlayerSection" style="display:none;">
|
|
457
|
+
<div class="current-player-title">— 你的排名 —</div>
|
|
458
|
+
<div class="current-player-content" id="currentPlayerContent"></div>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
<!-- 底部信息 -->
|
|
462
|
+
<div class="footer">
|
|
463
|
+
<div class="footer-total">总成员数: <span id="totalCount">0</span> 人</div>
|
|
464
|
+
<div class="footer-update">数据更新时间: <span id="updateTime">-</span></div>
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
<script>
|
|
469
|
+
function escapeHtml(text) {
|
|
470
|
+
if (!text) return '';
|
|
471
|
+
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
472
|
+
return String(text).replace(/[&<>"']/g, m => map[m]);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function createAvatarHtml(player, sizeClass) {
|
|
476
|
+
if (!player.avatarUrl) {
|
|
477
|
+
return `<div class="${sizeClass}" style="display:flex; align-items:center; justify-content:center; color:#888;">${escapeHtml(player.tmpName.charAt(0))}</div>`;
|
|
478
|
+
}
|
|
479
|
+
return `<img class="${sizeClass}" src="${escapeHtml(player.avatarUrl)}" onerror="this.onerror=null; this.src=''; this.style.display='none'; this.parentElement.querySelector('.avatar-fallback').style.display='flex';">
|
|
480
|
+
<div class="${sizeClass} avatar-fallback" style="display:none; align-items:center; justify-content:center; color:#888;">${escapeHtml(player.tmpName.charAt(0))}</div>`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function createTop3Item(player) {
|
|
484
|
+
const item = document.createElement('div');
|
|
485
|
+
const rankClass = `rank-${player.rank}`;
|
|
486
|
+
item.className = `top3-item ${rankClass}`;
|
|
487
|
+
|
|
488
|
+
const crowns = { 1: '👑', 2: '🥈', 3: '🥉' };
|
|
489
|
+
|
|
490
|
+
item.innerHTML = `
|
|
491
|
+
<div class="crown">${crowns[player.rank]}</div>
|
|
492
|
+
<div class="top3-rank-num">${player.rank}</div>
|
|
493
|
+
<div class="avatar-wrapper">${createAvatarHtml(player, 'top3-avatar')}</div>
|
|
494
|
+
<div class="top3-name">${escapeHtml(player.tmpName)}</div>
|
|
495
|
+
<div class="top3-teamid">车队编号: <span>#${escapeHtml(player.teamId)}</span></div>
|
|
496
|
+
<div class="top3-points">${player.rewardPoints.toLocaleString()}<span class="top3-points-label">分</span></div>
|
|
497
|
+
`;
|
|
498
|
+
return item;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function createRankItem(player) {
|
|
502
|
+
const item = document.createElement('div');
|
|
503
|
+
item.className = 'rank-item';
|
|
504
|
+
|
|
505
|
+
item.innerHTML = `
|
|
506
|
+
<div class="rank-num">${player.rank}</div>
|
|
507
|
+
<div class="avatar-wrapper">${createAvatarHtml(player, 'rank-avatar')}</div>
|
|
508
|
+
<div class="rank-info">
|
|
509
|
+
<div class="rank-name">${escapeHtml(player.tmpName)}</div>
|
|
510
|
+
<div class="rank-tmpid">TMP: ${escapeHtml(player.tmpId)}<span class="team-id">车队 #${escapeHtml(player.teamId)}</span></div>
|
|
511
|
+
</div>
|
|
512
|
+
<div class="rank-points">${player.rewardPoints.toLocaleString()}<span class="rank-points-label">分</span></div>
|
|
513
|
+
`;
|
|
514
|
+
return item;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function createCurrentPlayerItem(player) {
|
|
518
|
+
const content = document.createElement('div');
|
|
519
|
+
content.className = 'current-player-content';
|
|
520
|
+
|
|
521
|
+
content.innerHTML = `
|
|
522
|
+
<div class="current-player-rank">#${player.rank}</div>
|
|
523
|
+
<div class="current-player-info">
|
|
524
|
+
<div class="current-player-name">${escapeHtml(player.tmpName)}</div>
|
|
525
|
+
<div class="current-player-meta">TMP: ${escapeHtml(player.tmpId)}<span class="team-id">车队 #${escapeHtml(player.teamId)}</span></div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="current-player-points">${player.rewardPoints.toLocaleString()}<span class="current-player-points-label">分</span></div>
|
|
528
|
+
`;
|
|
529
|
+
return content;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function setData(data) {
|
|
533
|
+
if (!data || !data.top10 || !Array.isArray(data.top10)) {
|
|
534
|
+
console.error('数据无效');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const top3Container = document.getElementById('top3');
|
|
539
|
+
const rankList = document.getElementById('rankList');
|
|
540
|
+
const rankListContent = document.getElementById('rankListContent');
|
|
541
|
+
const currentPlayerSection = document.getElementById('currentPlayerSection');
|
|
542
|
+
const currentPlayerContent = document.getElementById('currentPlayerContent');
|
|
543
|
+
const totalCountEl = document.getElementById('totalCount');
|
|
544
|
+
const updateTimeEl = document.getElementById('updateTime');
|
|
545
|
+
|
|
546
|
+
top3Container.innerHTML = '';
|
|
547
|
+
rankListContent.innerHTML = '';
|
|
548
|
+
currentPlayerContent.innerHTML = '';
|
|
549
|
+
|
|
550
|
+
// 渲染前三名
|
|
551
|
+
const top3 = data.top10.slice(0, 3);
|
|
552
|
+
top3.forEach(player => {
|
|
553
|
+
top3Container.appendChild(createTop3Item(player));
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// 渲染第4-10名
|
|
557
|
+
const rest = data.top10.slice(3);
|
|
558
|
+
if (rest.length > 0) {
|
|
559
|
+
rankList.style.display = 'block';
|
|
560
|
+
rest.forEach(player => {
|
|
561
|
+
rankListContent.appendChild(createRankItem(player));
|
|
562
|
+
});
|
|
563
|
+
} else {
|
|
564
|
+
rankList.style.display = 'none';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 渲染当前用户排名(仅在找到用户数据时显示)
|
|
568
|
+
if (data.currentPlayer && data.currentPlayer.rank) {
|
|
569
|
+
currentPlayerSection.style.display = 'block';
|
|
570
|
+
currentPlayerContent.appendChild(createCurrentPlayerItem(data.currentPlayer));
|
|
571
|
+
} else {
|
|
572
|
+
currentPlayerSection.style.display = 'none';
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// 底部信息
|
|
576
|
+
if (totalCountEl) totalCountEl.textContent = (data.totalCount || 0).toLocaleString();
|
|
577
|
+
if (updateTimeEl) updateTimeEl.textContent = data.updateTime || '-';
|
|
578
|
+
}
|
|
579
|
+
</script>
|
|
580
|
+
</body>
|
|
581
|
+
</html>
|
package/package.json
CHANGED