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

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.
@@ -1,10 +1,15 @@
1
+ const apiLog = require('../util/apiLog')
1
2
  const BASE_APIS = ['https://evmapi.cxnnn.cn']
2
3
 
3
4
  async function requestWithFallback(http, path) {
4
5
  let lastError = null
5
6
  for (const baseUrl of BASE_APIS) {
6
7
  try {
7
- const result = await http.get(`${baseUrl}${path}`)
8
+ const result = await apiLog.get(http, `evm${path}`, `${baseUrl}${path}`)
9
+ // 业务层错误码日志(如 10001 玩家不存在等,仅调试模式输出)
10
+ if (result && result.code !== undefined && result.code !== 200) {
11
+ apiLog.debug(`evm${path}`, `业务响应错误码: code=${result.code}, msg=${result.msg || '无'}`)
12
+ }
8
13
  return result
9
14
  } catch (e) {
10
15
  lastError = e
@@ -21,7 +26,7 @@ module.exports = {
21
26
  let result = null
22
27
  try {
23
28
  result = game === 'ATS'
24
- ? await http.get(`${BASE_APIS[0]}/proxy/truckersmp/servers`)
29
+ ? await apiLog.get(http, 'evm.proxy.truckersmp.servers', `${BASE_APIS[0]}/proxy/truckersmp/servers`)
25
30
  : await requestWithFallback(http, `/server/list`)
26
31
  } catch {
27
32
  return {
@@ -1,5 +1,17 @@
1
+ const apiLog = require('../util/apiLog');
1
2
  const BASE_API = 'https://evmapi.cxnnn.cn/proxy/truckersmp';
2
3
  // const BASE_API = 'https://api.truckersmp.com/v2';
4
+
5
+ // 解析 TMP 接口的 error 字段;响应结构异常时记录日志并按错误处理,避免 JSON.parse 直接崩溃
6
+ function parseError(result, tag) {
7
+ try {
8
+ return !!JSON.parse(result?.error)
9
+ } catch {
10
+ apiLog.error(tag, `响应结构异常: ${JSON.stringify(result).slice(0, 300)}`)
11
+ return true
12
+ }
13
+ }
14
+
3
15
  module.exports = {
4
16
  /**
5
17
  * 查询玩家信息
@@ -7,7 +19,7 @@ module.exports = {
7
19
  async player(http, tmpId) {
8
20
  let result = null;
9
21
  try {
10
- result = await http.get(`${BASE_API}/player/${tmpId}`);
22
+ result = await apiLog.get(http, 'tmp.player', `${BASE_API}/player/${tmpId}`);
11
23
  }
12
24
  catch {
13
25
  return {
@@ -16,7 +28,7 @@ module.exports = {
16
28
  }
17
29
  // 拼接返回数据
18
30
  let data = {
19
- error: JSON.parse(result.error)
31
+ error: parseError(result, 'tmp.player')
20
32
  };
21
33
  if (!data.error) {
22
34
  data.data = result.response;
@@ -29,7 +41,7 @@ module.exports = {
29
41
  async servers(http) {
30
42
  let result = null;
31
43
  try {
32
- result = await http.get(`${BASE_API}/servers`);
44
+ result = await apiLog.get(http, 'tmp.servers', `${BASE_API}/servers`);
33
45
  }
34
46
  catch {
35
47
  return {
@@ -38,7 +50,7 @@ module.exports = {
38
50
  }
39
51
  // 拼接返回数据
40
52
  let data = {
41
- error: JSON.parse(result.error)
53
+ error: parseError(result, 'tmp.servers')
42
54
  };
43
55
  if (!data.error) {
44
56
  data.data = result.response;
@@ -51,7 +63,7 @@ module.exports = {
51
63
  async bans(http, tmpId) {
52
64
  let result = null;
53
65
  try {
54
- result = await http.get(`${BASE_API}/bans/${tmpId}`);
66
+ result = await apiLog.get(http, 'tmp.bans', `${BASE_API}/bans/${tmpId}`);
55
67
  }
56
68
  catch {
57
69
  return {
@@ -60,7 +72,7 @@ module.exports = {
60
72
  }
61
73
  // 拼接返回数据
62
74
  let data = {
63
- error: JSON.parse(result.error)
75
+ error: parseError(result, 'tmp.bans')
64
76
  };
65
77
  if (!data.error) {
66
78
  data.data = result.response;
@@ -73,7 +85,7 @@ module.exports = {
73
85
  async version(http) {
74
86
  let result = null;
75
87
  try {
76
- result = await http.get(`${BASE_API}/version`);
88
+ result = await apiLog.get(http, 'tmp.version', `${BASE_API}/version`);
77
89
  }
78
90
  catch {
79
91
  return {
@@ -92,7 +104,7 @@ module.exports = {
92
104
  async vtcMember(http, vtcId, memberId) {
93
105
  let result = null;
94
106
  try {
95
- result = await http.get(`${BASE_API}/vtc/${vtcId}/member/${memberId}`);
107
+ result = await apiLog.get(http, 'tmp.vtcMember', `${BASE_API}/vtc/${vtcId}/member/${memberId}`);
96
108
  }
97
109
  catch {
98
110
  return {
@@ -101,7 +113,7 @@ module.exports = {
101
113
  }
102
114
  // 拼接返回数据
103
115
  let data = {
104
- error: JSON.parse(result.error)
116
+ error: parseError(result, 'tmp.vtcMember')
105
117
  };
106
118
  if (!data.error) {
107
119
  data.data = result.response;
@@ -114,7 +126,7 @@ module.exports = {
114
126
  async vtc(http, vtcId) {
115
127
  let result = null
116
128
  try {
117
- result = await http.get(`${BASE_API}/vtc/${vtcId}`)
129
+ result = await apiLog.get(http, 'tmp.vtc', `${BASE_API}/vtc/${vtcId}`)
118
130
  } catch {
119
131
  return {
120
132
  error: true
@@ -123,7 +135,7 @@ module.exports = {
123
135
 
124
136
  // 拼接返回数据
125
137
  let data = {
126
- error: JSON.parse(result.error)
138
+ error: parseError(result, 'tmp.vtc')
127
139
  }
128
140
  if (!data.error) {
129
141
  data.data = result.response
@@ -1,3 +1,4 @@
1
+ const apiLog = require('../util/apiLog');
1
2
  const BASE_API = 'https://evmapi.cxnnn.cn/proxy/ets2map';
2
3
  // const BASE_API = 'https://tracker.ets2map.com';
3
4
  module.exports = {
@@ -7,7 +8,7 @@ module.exports = {
7
8
  async area(http, serverId, x1, y1, x2, y2) {
8
9
  let result = null;
9
10
  try {
10
- result = await http.get(`${BASE_API}/v3/area?x1=${x1}&y1=${y1}&x2=${x2}&y2=${y2}&server=${serverId}`);
11
+ result = await apiLog.get(http, 'ets2map.area', `${BASE_API}/v3/area?x1=${x1}&y1=${y1}&x2=${x2}&y2=${y2}&server=${serverId}`);
11
12
  }
12
13
  catch {
13
14
  return {
@@ -1,3 +1,4 @@
1
+ const apiLog = require('../util/apiLog')
1
2
  const BASE_API = 'https://evmapi.cxnnn.cn/proxy/trucky'
2
3
  // const BASE_API = 'https://api.codetabs.com/v1/proxy/?quest=https://api.truckyapp.com'
3
4
 
@@ -8,7 +9,7 @@ module.exports = {
8
9
  async online (http, tmpId) {
9
10
  let result = null
10
11
  try {
11
- result = await http.get(`${BASE_API}/v3/map/online?playerID=${tmpId}`)
12
+ result = await apiLog.get(http, 'trucky.online', `${BASE_API}/v3/map/online?playerID=${tmpId}`)
12
13
  } catch {
13
14
  return {
14
15
  error: true
@@ -30,7 +31,7 @@ module.exports = {
30
31
  async trafficTop (http, serverName) {
31
32
  let result = null
32
33
  try {
33
- result = await http.get(`${BASE_API}/v2/traffic/top?game=ets2&server=${serverName}`)
34
+ result = await apiLog.get(http, 'trucky.trafficTop', `${BASE_API}/v2/traffic/top?game=ets2&server=${serverName}`)
34
35
  } catch {
35
36
  return {
36
37
  error: true
@@ -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
package/lib/index.js CHANGED
@@ -28,6 +28,7 @@ const commands = {
28
28
  tmpFootprint: require('./command/tmpFootprint')
29
29
  };
30
30
  const { ActivityService } = require('./command/tmpActivityService');
31
+ const apiLog = require('./util/apiLog');
31
32
 
32
33
  const __defProp = Object.defineProperty;
33
34
  const __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -437,6 +438,9 @@ function registerBaseCommands(ctx, cfg) {
437
438
  }
438
439
 
439
440
  function apply(ctx, cfg) {
441
+ // 初始化 API 请求日志(调试模式开启后,truckersmp/trucky 等接口会输出请求与错误日志)
442
+ apiLog.init(ctx, cfg.debugMode);
443
+
440
444
  try {
441
445
  model(ctx);
442
446
  if (cfg.debugMode) {
@@ -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>
@@ -0,0 +1,52 @@
1
+ // API 请求日志工具:为各 API 模块提供统一的调试/错误日志
2
+ // debug 日志(请求地址、耗时)仅在插件开启调试模式时输出;
3
+ // 请求失败日志始终输出,便于排查接口异常
4
+ let logger = null
5
+ let debugMode = false
6
+
7
+ module.exports = {
8
+ /**
9
+ * 初始化(在插件 apply 时调用)
10
+ */
11
+ init(ctx, enabled) {
12
+ logger = ctx.logger
13
+ debugMode = !!enabled
14
+ },
15
+ /**
16
+ * 记录调试日志(仅调试模式开启时输出)
17
+ */
18
+ debug(tag, message) {
19
+ if (debugMode && logger) {
20
+ logger.info(`[TMP-BOT API] [${tag}] ${message}`)
21
+ }
22
+ },
23
+ /**
24
+ * 记录接口业务层错误日志(始终输出)
25
+ */
26
+ error(tag, message) {
27
+ if (logger) {
28
+ logger.error(`[TMP-BOT API] [${tag}] ${message}`)
29
+ }
30
+ },
31
+ /**
32
+ * 发起 GET 请求并记录日志,失败时记录错误日志后抛出异常(由调用方原有 catch 处理)
33
+ */
34
+ async get(http, tag, url) {
35
+ if (debugMode && logger) {
36
+ logger.info(`[TMP-BOT API] [${tag}] GET ${url}`)
37
+ }
38
+ const start = Date.now()
39
+ try {
40
+ const result = await http.get(url)
41
+ if (debugMode && logger) {
42
+ logger.info(`[TMP-BOT API] [${tag}] 响应成功, 耗时: ${Date.now() - start}ms`)
43
+ }
44
+ return result
45
+ } catch (e) {
46
+ if (logger) {
47
+ logger.error(`[TMP-BOT API] [${tag}] 请求失败: ${url} (${e.message || e})`)
48
+ }
49
+ throw e
50
+ }
51
+ }
52
+ }
@@ -13,8 +13,16 @@ module.exports = async (ctx, cfg, content, cache = true) => {
13
13
  }
14
14
  let randomInt = Math.floor(Math.random() * 10000);
15
15
  let sign = md5(cfg.baiduTranslate.appId + content + randomInt + cfg.baiduTranslate.key);
16
- let result = await ctx.http.get(`${TRANSLATE_API}?q=${encodeURI(content)}&from=auto&to=zh&appid=${cfg.baiduTranslate.appId}&salt=${randomInt}&sign=${sign}`);
16
+ let result;
17
+ try {
18
+ result = await ctx.http.get(`${TRANSLATE_API}?q=${encodeURI(content)}&from=auto&to=zh&appid=${cfg.baiduTranslate.appId}&salt=${randomInt}&sign=${sign}`);
19
+ } catch (error) {
20
+ // 翻译失败时降级返回原文,不影响主流程
21
+ ctx.logger.error(`[TMP-BOT] 百度翻译请求失败: ${error.message || error}`);
22
+ return content;
23
+ }
17
24
  if (result.error_code) {
25
+ ctx.logger.error(`[TMP-BOT] 百度翻译返回错误: error_code=${result.error_code}, error_msg=${result.error_msg || '无'}`);
18
26
  return content;
19
27
  }
20
28
  if (cfg.baiduTranslate.enableCache && cache) {
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.3",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>",
7
7
  "bot_actions <168329908@qq.com>"