koishi-plugin-ets2-tools-tmp 3.3.1 → 3.3.2

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.
@@ -6,6 +6,51 @@ const { PromodsIds, ServerType } = require('../util/constant')
6
6
  const evmOpenApi = require('../api/evmOpenApi')
7
7
  const guildBind = require('../database/guildBind')
8
8
 
9
+ // 足迹统计数据的开始收集日期(仅用于近一年足迹图片中的提示展示)
10
+ const FOOTPRINT_DATA_START_DATE = '2026-08-19'
11
+
12
+ // 轨迹分段规则与原页面逻辑一致:距离 > 30km / 间隔 > 90s / 跨服 判定为断点
13
+ // 在 Node 侧完成分段与抽稀,避免百万级点位全量传入浏览器导致渲染崩溃
14
+ function buildTrackSegments(points, isValid, minGap) {
15
+ const segments = []
16
+ let current = null
17
+ let lastKept = null
18
+ let prev = null
19
+ let prevTime = NaN
20
+
21
+ for (const p of points) {
22
+ if (!isValid(p)) continue
23
+
24
+ const currTime = Date.parse(String(p.updateTime || '').replace(/-/g, '/'))
25
+
26
+ if (!prev) {
27
+ current = [{ x: p.axisX, y: p.axisY }]
28
+ lastKept = p
29
+ } else {
30
+ const dist = Math.sqrt(Math.pow(p.axisX - prev.axisX, 2) + Math.pow(p.axisY - prev.axisY, 2)) * 19
31
+ const timeDiff = isNaN(currTime) || isNaN(prevTime) ? 0 : (currTime - prevTime) / 1000
32
+
33
+ if (dist > 30000 || timeDiff > 90 || p.serverId !== prev.serverId) {
34
+ // 断点:若上一点被抽稀丢弃,补回作为本段终点,保证线段完整
35
+ if (lastKept !== prev) current.push({ x: prev.axisX, y: prev.axisY })
36
+ segments.push(current)
37
+ current = [{ x: p.axisX, y: p.axisY }]
38
+ lastKept = p
39
+ } else if (Math.sqrt(Math.pow(p.axisX - lastKept.axisX, 2) + Math.pow(p.axisY - lastKept.axisY, 2)) >= minGap) {
40
+ current.push({ x: p.axisX, y: p.axisY })
41
+ lastKept = p
42
+ }
43
+ }
44
+ prev = p
45
+ prevTime = currTime
46
+ }
47
+ if (current && current.length) {
48
+ if (prev && lastKept !== prev) current.push({ x: prev.axisX, y: prev.axisY })
49
+ segments.push(current)
50
+ }
51
+ return segments
52
+ }
53
+
9
54
  module.exports = async (ctx, session, serverType, tmpId, date) => {
10
55
  if (!ctx.puppeteer) {
11
56
  return '未启用 puppeteer 服务'
@@ -91,17 +136,33 @@ module.exports = async (ctx, session, serverType, tmpId, date) => {
91
136
  return '查询玩家历史位置数据失败,请稍后重试'
92
137
  }
93
138
 
94
- // 过滤非对应服务器数据
139
+ // 过滤非对应服务器数据(单次遍历统计轨迹范围与有效点数,避免百万级数据产生额外数组拷贝)
95
140
  const promodsIdSet = new Set(PromodsIds)
96
- const mapPlayerHistoryArr = mapPlayerHistory.data.filter(item => {
141
+ const isTargetServer = item => {
97
142
  if (ServerType.ets === serverType) {
98
143
  return !promodsIdSet.has(item.serverId)
99
144
  } else if (ServerType.promods === serverType) {
100
145
  return promodsIdSet.has(item.serverId)
101
146
  }
102
147
  return false
103
- })
104
- if (mapPlayerHistoryArr.length === 0) {
148
+ }
149
+ const isValid = item => isTargetServer(item) && !(item.axisX === 0 && item.axisY === 0 && item.heading === 0)
150
+
151
+ const history = mapPlayerHistory.data || []
152
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
153
+ let validCount = 0
154
+ let intervalDistance = 0
155
+ for (const item of history) {
156
+ if (!isValid(item)) continue
157
+ validCount++
158
+ // 累加该区间内的行驶距离(API distance 字段,单位米)
159
+ intervalDistance += item.distance || 0
160
+ if (item.axisX < minX) minX = item.axisX
161
+ if (item.axisX > maxX) maxX = item.axisX
162
+ if (item.axisY < minY) minY = item.axisY
163
+ if (item.axisY > maxY) maxY = item.axisY
164
+ }
165
+ if (validCount === 0) {
105
166
  if (date === 'yesterday') {
106
167
  return `昨日暂无数据`;
107
168
  }
@@ -111,22 +172,36 @@ module.exports = async (ctx, session, serverType, tmpId, date) => {
111
172
  if (date === 'sevenday') {
112
173
  return `近七日暂无数据`;
113
174
  }
114
- if (date === 'lastMonth') {
115
- return `上月暂无数据`;
175
+ if (date === 'month') {
176
+ return `本月暂无数据`;
116
177
  }
117
- if (date === 'lastThreeMonths') {
118
- return `近三个月暂无数据`;
178
+ if (date === 'year') {
179
+ return `本年暂无数据`;
119
180
  }
120
181
  return '暂无数据';
121
182
  }
122
183
 
123
- // 拼接数据
184
+ // 抽稀阈值自适应:按轨迹对角线的 1/350 取值(800px 宽图片约 2px 精度),下限 5(约 95 米)
185
+ // 近一年足迹可达百万级点位,经此抽稀后传入浏览器的仅剩数千点,渲染不会崩溃
186
+ const diagonal = Math.sqrt(Math.pow(maxX - minX, 2) + Math.pow(maxY - minY, 2))
187
+ const minGap = Math.max(5, diagonal / 350)
188
+
189
+ // 分段 + 抽稀
190
+ const segments = buildTrackSegments(history, isValid, minGap)
191
+
192
+ // 拼接数据(今日足迹展示今日里程;其余展示该区间里程(distance 累加)与玩家总里程)
193
+ const isToday = date === 'today'
124
194
  let data = {
125
195
  mapType: ServerType.promods === serverType ? 'promods' : 'ets',
126
196
  name: playerInfo.data.name,
127
197
  smallAvatarUrl: playerInfo.data.smallAvatarUrl,
128
- todayMileage: playerInfo.data.todayMileage,
129
- points: mapPlayerHistoryArr
198
+ rangeText: { today: '今日', yesterday: '昨日', sevenday: '近七日', month: '近一个月', year: '近一年' }[date] || '',
199
+ isToday,
200
+ mileageLabel: isToday ? '今日里程' : '该区间里程',
201
+ mileage: isToday ? playerInfo.data.todayMileage : intervalDistance,
202
+ totalMileage: isToday ? null : playerInfo.data.mileage,
203
+ dataStartDate: date === 'year' ? FOOTPRINT_DATA_START_DATE : null,
204
+ segments
130
205
  }
131
206
 
132
207
  let page
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="zh-CN">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <title>玩家足迹展示</title>
@@ -19,77 +19,161 @@
19
19
  }
20
20
  #container {
21
21
  width: 800px;
22
- height: 600px;
23
- background: #1a1a1a;
22
+ height: 800px;
23
+ background: #0b0f1a;
24
24
  overflow: hidden;
25
25
  position: relative;
26
26
  }
27
- .status-bar {
27
+ #map-box, #map {
28
+ width: 100%;
29
+ height: 100%;
30
+ }
31
+ #map {
32
+ background-color: rgba(0, 0, 0, 0.25);
33
+ }
34
+
35
+ /* 氛围层:暗角 + 上下压暗,纯装饰悬浮层,不参与坐标换算 */
36
+ .atmo {
28
37
  position: absolute;
29
- bottom: 0;
30
- left: 0;
31
- right: 0;
32
- height: 32px;
33
- background-color: rgba(0, 0, 0, 0.5);
34
- backdrop-filter: blur(10px);
35
- -webkit-backdrop-filter: blur(10px);
38
+ inset: 0;
39
+ z-index: 500;
40
+ pointer-events: none;
41
+ background:
42
+ radial-gradient(140% 100% at 50% 40%, rgba(3, 7, 18, 0) 55%, rgba(3, 7, 18, .38) 100%),
43
+ linear-gradient(to top, rgba(3, 7, 18, .5), transparent 26%),
44
+ linear-gradient(to bottom, rgba(3, 7, 18, .35), transparent 18%);
45
+ }
46
+
47
+ .glass {
48
+ background: linear-gradient(135deg, rgba(13, 20, 38, .82), rgba(9, 13, 26, .66));
49
+ border: 1px solid rgba(125, 211, 252, .22);
50
+ backdrop-filter: blur(14px);
51
+ -webkit-backdrop-filter: blur(14px);
52
+ box-shadow: 0 10px 30px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .06);
53
+ }
54
+
55
+ /* 顶部玩家卡片 */
56
+ .header {
57
+ position: absolute;
58
+ top: 16px;
59
+ left: 16px;
60
+ z-index: 1001;
36
61
  display: flex;
37
62
  align-items: center;
38
- padding: 0 12px;
39
- box-shadow: 0 -2px 10px rgba(0, 0, 0, .5);
40
- z-index: 1001;
41
- border-top: 1px solid rgba(255, 255, 255, 0.1);
42
- font-size: 13px;
43
- box-sizing: border-box;
63
+ gap: 12px;
64
+ padding: 10px 20px 10px 12px;
65
+ border-radius: 14px;
44
66
  }
45
- .status-bar .avatar {
46
- width: 20px;
47
- height: 20px;
48
- border-radius: 4px;
49
- border: 1px solid rgba(255, 255, 255, 0.3);
50
- margin-right: 8px;
67
+ .header .avatar {
68
+ width: 46px;
69
+ height: 46px;
70
+ border-radius: 11px;
71
+ border: 1px solid rgba(148, 197, 255, .5);
72
+ box-shadow: 0 0 0 3px rgba(34, 211, 238, .12);
51
73
  }
52
- .status-bar .info {
53
- flex: 1;
74
+ .header .kicker {
75
+ font-size: 11px;
76
+ letter-spacing: 3px;
77
+ color: #7dd3fc;
78
+ margin-bottom: 3px;
79
+ }
80
+ .header .name {
81
+ font-size: 19px;
82
+ font-weight: 800;
83
+ color: #eaf2ff;
84
+ line-height: 1.2;
85
+ max-width: 320px;
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+ text-shadow: 0 2px 10px rgba(56, 189, 248, .35);
90
+ }
91
+
92
+ /* 底部状态栏 */
93
+ .status-bar {
94
+ position: absolute;
95
+ left: 16px;
96
+ right: 16px;
97
+ bottom: 14px;
98
+ z-index: 1001;
54
99
  display: flex;
55
100
  align-items: center;
101
+ gap: 18px;
102
+ padding: 11px 20px;
103
+ border-radius: 14px;
104
+ box-sizing: border-box;
56
105
  }
57
- .status-bar .info .name {
58
- color: #b0c7ff;
59
- font-weight: 600;
106
+ .mileage-block {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 2px;
110
+ min-width: 110px;
111
+ }
112
+ .mileage-block .label {
113
+ font-size: 11px;
114
+ letter-spacing: 2px;
115
+ color: #8fa3c8;
116
+ }
117
+ .mileage-block .value {
118
+ font-size: 21px;
119
+ font-weight: 800;
120
+ background: linear-gradient(90deg, #4ade80, #a3e635);
121
+ -webkit-background-clip: text;
122
+ background-clip: text;
123
+ color: transparent;
124
+ }
125
+ /* 区间里程用青色渐变,与轨迹线颜色呼应 */
126
+ .mileage-block .value.cyan {
127
+ background: linear-gradient(90deg, #22d3ee, #a5f3fc);
128
+ -webkit-background-clip: text;
129
+ background-clip: text;
60
130
  }
61
- .status-bar .stats {
131
+ .v-divider {
132
+ width: 1px;
133
+ height: 30px;
134
+ background: rgba(255, 255, 255, .1);
135
+ }
136
+ .legend {
62
137
  display: flex;
63
138
  align-items: center;
64
- color: #aaaaaa;
65
- }
66
- .status-bar .stats .label {
67
- margin-right: 6px;
139
+ gap: 14px;
68
140
  }
69
- .status-bar .stats .value {
70
- font-weight: bold;
71
- color: #54d354;
141
+ .legend .item {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 6px;
145
+ font-size: 11.5px;
146
+ color: #9fb3d9;
72
147
  }
73
-
74
- #map-box {
75
- width: 100%;
76
- height: 100%;
148
+ .legend .line-sample {
149
+ width: 20px;
150
+ height: 4px;
151
+ border-radius: 2px;
152
+ background: #22d3ee;
153
+ box-shadow: 0 0 8px rgba(34, 211, 238, .8);
77
154
  }
78
- #map {
79
- width: 100%;
80
- height: 100%;
81
- background-color: rgba(0, 0, 0, 0.25);
155
+ .legend .dot-sample {
156
+ width: 9px;
157
+ height: 9px;
158
+ border-radius: 50%;
159
+ border: 2px solid #fff;
160
+ box-sizing: border-box;
82
161
  }
162
+ .legend .start { background: #4ade80; }
163
+ .legend .end { background: #f87171; }
83
164
 
84
- .marker-label {
85
- background: rgba(0, 0, 0, 0.6);
86
- border: 1px solid rgba(255, 255, 255, 0.2);
87
- border-radius: 4px;
88
- color: #fff;
89
- padding: 2px 6px;
90
- font-size: 12px;
91
- white-space: nowrap;
165
+ /* 数据说明(近一年足迹时显示统计开始日期) */
166
+ .note {
167
+ margin-left: auto;
168
+ font-size: 11px;
169
+ color: #fcd34d;
170
+ background: rgba(251, 191, 36, .1);
171
+ border: 1px solid rgba(251, 191, 36, .32);
172
+ padding: 5px 12px;
173
+ border-radius: 8px;
174
+ letter-spacing: .5px;
92
175
  }
176
+ .hidden { display: none; }
93
177
  </style>
94
178
  </head>
95
179
  <body>
@@ -97,84 +181,78 @@
97
181
  <div id="map-box">
98
182
  <div id="map"></div>
99
183
  </div>
100
- <div class="status-bar">
184
+ <div class="atmo"></div>
185
+
186
+ <div class="header glass">
101
187
  <img class="avatar" id="avatar" src="https://static.truckersmp.com/avatarsN/small/defaultavatar.png" alt="avatar"/>
102
- <div class="info">
188
+ <div>
189
+ <div class="kicker" id="range-title">行驶足迹</div>
103
190
  <div class="name" id="username">测试玩家</div>
104
191
  </div>
105
- <div class="stats" id="stats-box">
106
- <span class="label">今日里程</span>
192
+ </div>
193
+
194
+ <div class="status-bar glass">
195
+ <div class="mileage-block">
196
+ <span class="label" id="mileage-label">今日里程</span>
107
197
  <span class="value" id="distance">0.0 km</span>
108
198
  </div>
199
+ <div class="v-divider" id="total-divider"></div>
200
+ <div class="mileage-block" id="total-mileage-block">
201
+ <span class="label">总里程</span>
202
+ <span class="value" id="total-distance">0.0 km</span>
203
+ </div>
204
+ <div class="v-divider"></div>
205
+ <div class="legend">
206
+ <div class="item"><span class="line-sample"></span>行驶轨迹</div>
207
+ <div class="item"><span class="dot-sample start"></span>起点</div>
208
+ <div class="item"><span class="dot-sample end"></span>终点</div>
209
+ </div>
210
+ <div class="note hidden" id="stats-note">足迹统计开始日期</div>
109
211
  </div>
110
212
  </div>
111
213
 
112
214
  <script src="./package/ets-map.js"></script>
113
215
  <script>
114
- function calculateDistance(p1, p2) {
115
- return Math.sqrt(Math.pow(p1.axisX - p2.axisX, 2) + Math.pow(p1.axisY - p2.axisY, 2));
116
- }
216
+ function init(data) {
217
+ if (!data) return;
117
218
 
118
- function parseTime(timeStr) {
119
- return new Date(timeStr.replace(/-/g, '/')).getTime();
120
- }
121
-
122
- function init(data) {
123
- if (!data) return;
124
-
125
- document.getElementById('username').innerText = (data.name || 'Unknown') + ' 的今日行驶足迹';
126
- if (data.smallAvatarUrl) {
127
- document.getElementById('avatar').src = data.smallAvatarUrl;
128
- }
129
-
130
- const points = (data.points || []).filter(p => !(p.axisX === 0 && p.axisY === 0 && p.heading === 0));
131
- // 使用传入的今日里程数据(米转千米)
132
- const mileage = data.todayMileage || 0;
133
- const km = (mileage / 1000).toFixed(1);
134
- document.getElementById('distance').innerText = `${km} km`;
135
-
136
- if (points.length === 0) {
137
- return;
138
- }
139
-
140
- const lines = [];
141
- let currentLine = [];
142
-
143
- if (points.length > 0) {
144
- let first = points[0];
145
- currentLine.push({ x: first.axisX, y: first.axisY });
146
-
147
- for (let i = 1; i < points.length; i++) {
148
- const prev = points[i - 1];
149
- const curr = points[i];
150
-
151
- let dist = calculateDistance(prev, curr);
152
- dist = dist * 19;
153
- const isDistJump = dist > 30000; // > 30km
219
+ document.getElementById('username').innerText = data.name || 'Unknown';
220
+ document.getElementById('range-title').innerText = `${data.rangeText || ''} · 行驶足迹`;
221
+ if (data.smallAvatarUrl) {
222
+ document.getElementById('avatar').src = data.smallAvatarUrl;
223
+ }
154
224
 
155
- let timeDiff = 0;
156
- try {
157
- timeDiff = (parseTime(curr.updateTime) - parseTime(prev.updateTime)) / 1000;
158
- } catch (e) { }
225
+ // 里程(米转千米):今日足迹显示今日里程;
226
+ // 其余显示该区间里程(API distance 字段累加)与玩家总里程
227
+ document.getElementById('mileage-label').innerText = data.mileageLabel || '今日里程';
228
+ const mileage = data.mileage || 0;
229
+ const km = Number((mileage / 1000).toFixed(1)).toLocaleString('zh-CN');
230
+ document.getElementById('distance').innerText = `${km} km`;
159
231
 
160
- const isTimeJump = timeDiff > 90;
161
- const isServerJump = prev.serverId !== curr.serverId;
232
+ if (data.isToday === false) {
233
+ document.getElementById('distance').classList.add('cyan');
234
+ const totalKm = Number(((data.totalMileage || 0) / 1000).toFixed(1)).toLocaleString('zh-CN');
235
+ document.getElementById('total-distance').innerText = `${totalKm} km`;
236
+ } else {
237
+ // 今日足迹不显示总里程块
238
+ document.getElementById('total-mileage-block').classList.add('hidden');
239
+ document.getElementById('total-divider').classList.add('hidden');
240
+ }
162
241
 
163
- if (isDistJump || isTimeJump || isServerJump) {
164
- if (currentLine.length > 0) lines.push(currentLine);
165
- currentLine = [];
166
- }
167
- currentLine.push({ x: curr.axisX, y: curr.axisY });
168
- }
169
- if (currentLine.length > 0) lines.push(currentLine);
170
- }
242
+ // 近一年足迹显示统计开始日期
243
+ const noteEl = document.getElementById('stats-note');
244
+ if (data.dataStartDate) {
245
+ noteEl.innerText = `足迹统计开始日期:${data.dataStartDate}`;
246
+ noteEl.classList.remove('hidden');
247
+ }
171
248
 
172
- render(lines, data.mapType);
173
- }
249
+ // 轨迹的分段(断点判定)与抽稀已在 Node 侧完成,这里直接渲染
250
+ render(data.segments || [], data.mapType);
251
+ }
174
252
  function render(lines, mapType) {
175
253
  const config = mapConfig[mapType];
176
254
 
177
- // 边界
255
+ // 边界(坐标系换算逻辑保持原样,勿动)
178
256
  let bounds = L.latLngBounds(
179
257
  map.unproject([0, config.bounds.y], config.maxZoom),
180
258
  map.unproject([config.bounds.x, 0], config.maxZoom)
@@ -192,6 +270,9 @@
192
270
  reuseTiles: true
193
271
  }).addTo(map);
194
272
 
273
+ // Canvas 渲染器:大量分段与标记时性能远优于默认 SVG
274
+ const canvasRenderer = L.canvas({ padding: 0.3 });
275
+
195
276
  let allLatlngs = [];
196
277
 
197
278
  lines.forEach(points => {
@@ -204,18 +285,29 @@
204
285
  allLatlngs.push([unprojected.lat, unprojected.lng]);
205
286
  });
206
287
 
207
- // 轨迹线
288
+ // 底层光晕线 + 主线,双层描边实现发光效果
289
+ L.polyline(latlngs, {
290
+ renderer: canvasRenderer,
291
+ color: '#0e7490',
292
+ weight: 9,
293
+ opacity: 0.35,
294
+ lineJoin: 'round',
295
+ lineCap: 'round'
296
+ }).addTo(map);
208
297
  L.polyline(latlngs, {
209
- color: '#3498db',
210
- weight: 4,
211
- opacity: 0.8,
212
- lineJoin: 'round'
298
+ renderer: canvasRenderer,
299
+ color: '#22d3ee',
300
+ weight: 3.5,
301
+ opacity: 1,
302
+ lineJoin: 'round',
303
+ lineCap: 'round'
213
304
  }).addTo(map);
214
305
 
215
306
  // 起点
216
307
  L.circleMarker(latlngs[0], {
217
- radius: 4,
218
- fillColor: '#2ecc71',
308
+ renderer: canvasRenderer,
309
+ radius: 4.5,
310
+ fillColor: '#4ade80',
219
311
  color: '#fff',
220
312
  weight: 2,
221
313
  fillOpacity: 1
@@ -223,17 +315,21 @@
223
315
 
224
316
  // 终点
225
317
  L.circleMarker(latlngs[latlngs.length - 1], {
226
- radius: 4,
227
- fillColor: '#e74c3c',
318
+ renderer: canvasRenderer,
319
+ radius: 4.5,
320
+ fillColor: '#f87171',
228
321
  color: '#fff',
229
322
  weight: 2,
230
323
  fillOpacity: 1
231
324
  }).addTo(map);
232
325
  });
233
326
 
234
- // 自动适应
327
+ // 自动适应(预留上下 UI 卡片区域;容器为正方形,尽量让正方形瓦片图占满)
235
328
  if (allLatlngs.length > 0) {
236
- map.fitBounds(L.latLngBounds(allLatlngs), { padding: [50, 50] });
329
+ map.fitBounds(L.latLngBounds(allLatlngs), {
330
+ paddingTopLeft: [60, 95],
331
+ paddingBottomRight: [60, 65]
332
+ });
237
333
  }
238
334
  }
239
335
  </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ets2-tools-tmp",
3
3
  "description": "欧卡2 TruckersMP信息查询、车队平台查询及活动提醒",
4
- "version": "3.3.1",
4
+ "version": "3.3.2",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>",
7
7
  "bot_actions <168329908@qq.com>"