koishi-plugin-cfmrmod 1.1.2 → 1.1.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.
@@ -0,0 +1,509 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.drawCenterCard = drawCenterCard;
4
+ exports.drawCenterCardImpl = drawCenterCardImpl;
5
+ const cheerio = require('cheerio');
6
+ const constants_1 = require("../constants");
7
+ const http_1 = require("../http");
8
+ const rendering_1 = require("../rendering");
9
+ const utils_1 = require("../utils");
10
+ // ================= 普通用户卡片 (Center Card) =================
11
+ async function drawCenterCard(uid, logger) { return drawCenterCardImpl(uid, logger); }
12
+ async function drawCenterCardImpl(uid, logger) {
13
+ var _a, _b, _c, _d;
14
+ const centerUrl = `${constants_1.CENTER_URL}/${uid}/`;
15
+ const bbsUrl = `https://bbs.mcmod.cn/center/${uid}/`;
16
+ const homeApiUrl = `${constants_1.CENTER_URL}/frame/CenterHome/`;
17
+ const commentApiUrl = `${constants_1.CENTER_URL}/frame/CenterComment/`;
18
+ const chartApiUrl = `${constants_1.CENTER_URL}/object/UserHistoryChartData/`;
19
+ const params = new URLSearchParams();
20
+ params.append('uid', uid);
21
+ const currentYear = new Date().getFullYear();
22
+ const chartParams = new URLSearchParams();
23
+ chartParams.append('data', JSON.stringify({ uid: parseInt(uid), year: currentYear }));
24
+ const apiHeaders = { ...(0, http_1.getHeaders)(centerUrl), 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' };
25
+ let mainHtml = '', homeJson = null, commentJson = null, chartJson = null, bbsHtml = '';
26
+ // 1. 并行获取所有数据
27
+ try {
28
+ const results = await Promise.allSettled([
29
+ (0, http_1.fetchWithTimeout)(centerUrl, { headers: (0, http_1.getHeaders)() }),
30
+ (0, http_1.fetchWithTimeout)(homeApiUrl, { method: 'POST', headers: apiHeaders, body: params }),
31
+ (0, http_1.fetchWithTimeout)(commentApiUrl, { method: 'POST', headers: apiHeaders, body: params }),
32
+ (0, http_1.fetchWithTimeout)(chartApiUrl, { method: 'POST', headers: apiHeaders, body: chartParams }),
33
+ (0, http_1.fetchWithTimeout)(bbsUrl, { headers: (0, http_1.getHeaders)() })
34
+ ]);
35
+ if (results[0].status === 'fulfilled')
36
+ mainHtml = await results[0].value.text();
37
+ if (results[1].status === 'fulfilled' && results[1].value.ok)
38
+ try {
39
+ homeJson = await results[1].value.json();
40
+ }
41
+ catch (e) { }
42
+ if (results[2].status === 'fulfilled' && results[2].value.ok)
43
+ try {
44
+ commentJson = await results[2].value.json();
45
+ }
46
+ catch (e) { }
47
+ if (results[3].status === 'fulfilled' && results[3].value.ok)
48
+ try {
49
+ chartJson = await results[3].value.json();
50
+ }
51
+ catch (e) { }
52
+ if (results[4].status === 'fulfilled' && results[4].value.ok)
53
+ bbsHtml = await results[4].value.text();
54
+ }
55
+ catch (e) {
56
+ logger.error(`[Card] 数据获取部分失败: ${e.message}`);
57
+ }
58
+ // 2. 解析 Center 主站数据
59
+ const $main = cheerio.load(mainHtml || '');
60
+ const header = $main('.center-header');
61
+ const username = (0, utils_1.cleanText)(header.find('.user-un').text()) || 'User';
62
+ const levelText = (0, utils_1.cleanText)(header.find('.user-lv').text()) || 'Lv.?';
63
+ const signature = (0, utils_1.cleanText)(header.find('.user-sign').text()) || '(无签名)';
64
+ let avatarUrl = (0, utils_1.fixUrl)(header.find('.user-icon-img img').attr('src'));
65
+ let bannerUrl = null;
66
+ $main('style').each((i, el) => {
67
+ const styleText = $main(el).html() || '';
68
+ const bodyBgMatch = styleText.match(/body\s*\{\s*background\s*:\s*url\(([^)]+)\)/i);
69
+ if (bodyBgMatch && bodyBgMatch[1] && (!styleText.includes('.copyright') || styleText.includes('body{background'))) {
70
+ bannerUrl = (0, utils_1.fixUrl)(bodyBgMatch[1].replace(/['"]/g, ''));
71
+ }
72
+ });
73
+ if (!bannerUrl)
74
+ bannerUrl = (0, utils_1.fixUrl)((_b = (_a = (header.attr('style') || '').match(/url\((.*?)\)/)) === null || _a === void 0 ? void 0 : _a[1]) === null || _b === void 0 ? void 0 : _b.replace(/['"]/g, ''));
75
+ // 3. 解析 BBS 数据
76
+ const bbsData = { medals: [], points: [], detailed: [], profile: [], times: [] };
77
+ if (bbsHtml) {
78
+ const $bbs = cheerio.load(bbsHtml);
79
+ if (!avatarUrl)
80
+ avatarUrl = (0, utils_1.fixUrl)($bbs('.icn.avt img').attr('src'));
81
+ // 勋章墙 (修复:$(el) -> $bbs(el))
82
+ $bbs('.md_ctrl img').each((i, el) => {
83
+ const src = (0, utils_1.fixUrl)($bbs(el).attr('src'));
84
+ const name = $bbs(el).attr('alt') || $bbs(el).attr('title') || '勋章';
85
+ if (src)
86
+ bbsData.medals.push({ src, name });
87
+ });
88
+ // 积分统计 (修复:$(el) -> $bbs(el))
89
+ $bbs('#psts .pf_l li').each((i, el) => {
90
+ const label = (0, utils_1.cleanText)($bbs(el).find('em').text());
91
+ const val = (0, utils_1.cleanText)($bbs(el).text()).replace(label, '').trim();
92
+ if (label && val)
93
+ bbsData.points.push({ l: label, v: val });
94
+ });
95
+ // 详细贡献 (修复:$(el) -> $bbs(el))
96
+ $bbs('.u_profile .bbda.pbm.mbm li p').each((i, el) => {
97
+ const txt = $bbs(el).text();
98
+ if (txt.includes(':') && ($bbs(el).find('.green').length > 0 || txt.includes('/'))) {
99
+ const label = txt.split(':')[0].trim();
100
+ const add = (0, utils_1.cleanText)($bbs(el).find('.green').text()) || '0';
101
+ const edit = (0, utils_1.cleanText)($bbs(el).find('.blue').text()) || '0';
102
+ if (label && !label.includes('以下数据')) {
103
+ bbsData.detailed.push({ l: label, add, edit });
104
+ }
105
+ }
106
+ });
107
+ // 个人档案 (修复:$(el) -> $bbs(el))
108
+ $bbs('.u_profile .pf_l.cl li').each((i, el) => {
109
+ const label = (0, utils_1.cleanText)($bbs(el).find('em').text());
110
+ const val = (0, utils_1.cleanText)($bbs(el).text()).replace(label, '').trim();
111
+ if (label && val)
112
+ bbsData.profile.push({ l: label, v: val });
113
+ });
114
+ // 完整时间统计 (修复:$(el) -> $bbs(el))
115
+ $bbs('#pbbs li').each((i, el) => {
116
+ const label = (0, utils_1.cleanText)($bbs(el).find('em').text());
117
+ const val = (0, utils_1.cleanText)($bbs(el).text()).replace(label, '').trim();
118
+ if (label && val)
119
+ bbsData.times.push({ l: label, v: val });
120
+ });
121
+ }
122
+ // 4. 解析原有 API 数据
123
+ const statsMap = {};
124
+ if (homeJson === null || homeJson === void 0 ? void 0 : homeJson.html) {
125
+ const $h = cheerio.load(homeJson.html);
126
+ $h('li').each((i, el) => {
127
+ const t = (0, utils_1.cleanText)($h(el).find('.title').text());
128
+ const v = (0, utils_1.cleanText)($h(el).find('.text').text());
129
+ if (t && v) {
130
+ if (t.includes('用户组'))
131
+ statsMap.group = v;
132
+ else if (t.includes('编辑次数'))
133
+ statsMap.edits = v;
134
+ else if (t.includes('编辑字数'))
135
+ statsMap.words = v;
136
+ else if (t.includes('短评'))
137
+ statsMap.comments = v;
138
+ else if (t.includes('教程'))
139
+ statsMap.tutorials = v;
140
+ else if (t.includes('注册'))
141
+ statsMap.reg = v;
142
+ }
143
+ });
144
+ }
145
+ // 基础统计列表
146
+ const basicStats = [
147
+ { l: '用户组', v: statsMap.group || '未知' }, { l: '总编辑次数', v: statsMap.edits || '0' },
148
+ { l: '总编辑字数', v: statsMap.words || '0' }, { l: '总短评数', v: statsMap.comments || '0' },
149
+ { l: '个人教程', v: statsMap.tutorials || '0' }
150
+ ];
151
+ // 如果 BBS 数据里没有注册时间,则从 API 补充
152
+ if (!bbsData.times.some(t => t.l.includes('注册')) && statsMap.reg) {
153
+ bbsData.times.unshift({ l: '注册时间', v: statsMap.reg });
154
+ }
155
+ const reactions = [];
156
+ if (commentJson === null || commentJson === void 0 ? void 0 : commentJson.html) {
157
+ const $c = cheerio.load(commentJson.html);
158
+ $c('li').each((i, el) => {
159
+ const t = (0, utils_1.cleanText)($c(el).text());
160
+ const m = t.match(/被评[“"'](.+?)[”"']\s*[::]\s*([\d,]+)/);
161
+ if (m)
162
+ reactions.push({ l: m[1], c: m[2] });
163
+ });
164
+ }
165
+ const activityMap = {};
166
+ if ((_c = chartJson === null || chartJson === void 0 ? void 0 : chartJson.chartdata) === null || _c === void 0 ? void 0 : _c.total) {
167
+ chartJson.chartdata.total.forEach(item => {
168
+ if (Array.isArray(item) && typeof item[1] === 'number')
169
+ activityMap[item[0]] = item[1];
170
+ });
171
+ }
172
+ // ================= 绘图逻辑 =================
173
+ const width = 800;
174
+ const font = rendering_1.GLOBAL_FONT_FAMILY;
175
+ const bannerH = 160;
176
+ const headerH = 140;
177
+ const cardOverlap = 40;
178
+ const padding = 20;
179
+ const gap = 15;
180
+ let currentY = bannerH - cardOverlap + headerH + padding;
181
+ // BBS 勋章墙
182
+ let medalsH = 0;
183
+ if (bbsData.medals.length > 0) {
184
+ const rows = Math.ceil(bbsData.medals.length / 12);
185
+ medalsH = 50 + rows * 40 + 20;
186
+ currentY += medalsH + gap;
187
+ }
188
+ // BBS 积分
189
+ let pointsH = 0;
190
+ if (bbsData.points.length > 0) {
191
+ const rows = Math.ceil(bbsData.points.length / 4);
192
+ pointsH = 50 + rows * 60 + 20;
193
+ currentY += pointsH + gap;
194
+ }
195
+ // BBS 详细贡献
196
+ let detailedH = 0;
197
+ if (bbsData.detailed.length > 0) {
198
+ const rows = Math.ceil(bbsData.detailed.length / 2);
199
+ detailedH = 50 + rows * 50 + 20;
200
+ currentY += detailedH + gap;
201
+ }
202
+ // 基础统计
203
+ const statsH = 180;
204
+ currentY += statsH + gap;
205
+ // 表态
206
+ let reactionSectionH = 80;
207
+ if (reactions.length > 0) {
208
+ const tempC = (0, rendering_1.createCanvas)(100, 100);
209
+ const tempCtx = tempC.getContext('2d');
210
+ tempCtx.font = `14px "${font}"`;
211
+ let rx = 50, lines = 1;
212
+ reactions.forEach(item => {
213
+ const t = `${item.l}: ${item.c}`;
214
+ const w = tempCtx.measureText(t).width + 30;
215
+ if (rx + w > width - 50) {
216
+ rx = 50;
217
+ lines++;
218
+ }
219
+ rx += w + 10;
220
+ });
221
+ reactionSectionH = 50 + (lines * 35) + 20;
222
+ }
223
+ currentY += reactionSectionH + gap;
224
+ // 热力图
225
+ const mapH = 200;
226
+ currentY += mapH + gap;
227
+ // 时间信息区域高度
228
+ let timesH = 0;
229
+ if (bbsData.times.length > 0) {
230
+ timesH = 80;
231
+ currentY += timesH;
232
+ }
233
+ const totalHeight = currentY + 30; // 底部版权留白
234
+ const canvas = (0, rendering_1.createCanvas)(width, totalHeight);
235
+ const ctx = canvas.getContext('2d');
236
+ // 背景
237
+ ctx.fillStyle = '#f0f2f5';
238
+ ctx.fillRect(0, 0, width, totalHeight);
239
+ try {
240
+ if (bannerUrl) {
241
+ const img = await (0, rendering_1.loadImage)(bannerUrl);
242
+ const r = Math.max(width / img.width, bannerH / img.height);
243
+ ctx.drawImage(img, 0, 0, img.width, img.height, (width - img.width * r) / 2, (bannerH - img.height * r) / 2, img.width * r, img.height * r);
244
+ }
245
+ else {
246
+ ctx.fillStyle = '#3498db';
247
+ ctx.fillRect(0, 0, width, bannerH);
248
+ }
249
+ }
250
+ catch (e) {
251
+ ctx.fillStyle = '#3498db';
252
+ ctx.fillRect(0, 0, width, bannerH);
253
+ }
254
+ const overlay = ctx.createLinearGradient(0, 80, 0, bannerH);
255
+ overlay.addColorStop(0, 'rgba(0,0,0,0)');
256
+ overlay.addColorStop(1, 'rgba(0,0,0,0.5)');
257
+ ctx.fillStyle = overlay;
258
+ ctx.fillRect(0, 0, width, bannerH);
259
+ // Header
260
+ const cardTop = bannerH - cardOverlap;
261
+ ctx.shadowColor = 'rgba(0,0,0,0.1)';
262
+ ctx.shadowBlur = 10;
263
+ ctx.fillStyle = '#fff';
264
+ (0, rendering_1.roundRect)(ctx, 20, cardTop, width - 40, headerH, 10);
265
+ ctx.fill();
266
+ ctx.shadowBlur = 0;
267
+ const avX = 50, avY = cardTop - 30;
268
+ ctx.beginPath();
269
+ ctx.arc(avX + 50, avY + 50, 54, 0, Math.PI * 2);
270
+ ctx.fillStyle = '#fff';
271
+ ctx.fill();
272
+ if (avatarUrl) {
273
+ try {
274
+ const img = await (0, rendering_1.loadImage)(avatarUrl);
275
+ ctx.save();
276
+ ctx.beginPath();
277
+ ctx.arc(avX + 50, avY + 50, 50, 0, Math.PI * 2);
278
+ ctx.clip();
279
+ ctx.drawImage(img, avX, avY, 100, 100);
280
+ ctx.restore();
281
+ }
282
+ catch (e) { }
283
+ }
284
+ const nameX = 180, nameY = cardTop + 20;
285
+ ctx.textBaseline = 'top';
286
+ ctx.fillStyle = '#333';
287
+ ctx.font = `bold 32px "${font}"`;
288
+ ctx.fillText(username, nameX, nameY);
289
+ const nameW = ctx.measureText(username).width;
290
+ ctx.fillStyle = '#f39c12';
291
+ (0, rendering_1.roundRect)(ctx, nameX + nameW + 15, nameY + 5, 50, 24, 4);
292
+ ctx.fill();
293
+ ctx.fillStyle = '#fff';
294
+ ctx.font = `bold 16px "${font}"`;
295
+ ctx.fillText(levelText, nameX + nameW + 22, nameY + 8);
296
+ ctx.textAlign = 'right';
297
+ ctx.fillStyle = '#999';
298
+ ctx.font = `bold 20px "${font}"`;
299
+ ctx.fillText(`UID: ${uid}`, width - 50, nameY + 10);
300
+ ctx.textAlign = 'left';
301
+ const mcid = (_d = bbsData.profile.find(p => p.l === 'MCID')) === null || _d === void 0 ? void 0 : _d.v;
302
+ const subText = mcid ? `MCID: ${mcid} | ${signature}` : signature;
303
+ ctx.fillStyle = '#666';
304
+ ctx.font = `16px "${font}"`;
305
+ (0, rendering_1.wrapText)(ctx, subText, nameX, nameY + 50, width - 250, 24, 2);
306
+ let dy = cardTop + headerH + padding;
307
+ // 绘制 BBS 勋章
308
+ if (bbsData.medals.length > 0) {
309
+ ctx.fillStyle = '#fff';
310
+ (0, rendering_1.roundRect)(ctx, 20, dy, width - 40, medalsH, 10);
311
+ ctx.fill();
312
+ ctx.fillStyle = '#333';
313
+ ctx.font = `bold 18px "${font}"`;
314
+ ctx.fillText('勋章墙', 40, dy + 25);
315
+ ctx.strokeStyle = '#eee';
316
+ ctx.beginPath();
317
+ ctx.moveTo(40, dy + 50);
318
+ ctx.lineTo(width - 40, dy + 50);
319
+ ctx.stroke();
320
+ let mx = 40, my = dy + 60;
321
+ const iconSize = 32;
322
+ for (const m of bbsData.medals) {
323
+ try {
324
+ const img = await (0, rendering_1.loadImage)(m.src);
325
+ ctx.drawImage(img, mx, my, iconSize, iconSize);
326
+ }
327
+ catch (e) { }
328
+ mx += iconSize + 15;
329
+ if (mx > width - 80) {
330
+ mx = 40;
331
+ my += iconSize + 10;
332
+ }
333
+ }
334
+ dy += medalsH + gap;
335
+ }
336
+ // 绘制 BBS 积分
337
+ if (bbsData.points.length > 0) {
338
+ ctx.fillStyle = '#fff';
339
+ (0, rendering_1.roundRect)(ctx, 20, dy, width - 40, pointsH, 10);
340
+ ctx.fill();
341
+ ctx.fillStyle = '#333';
342
+ ctx.font = `bold 18px "${font}"`;
343
+ ctx.fillText('积分统计', 40, dy + 25);
344
+ ctx.beginPath();
345
+ ctx.moveTo(40, dy + 50);
346
+ ctx.lineTo(width - 40, dy + 50);
347
+ ctx.stroke();
348
+ const colW = (width - 80) / 4;
349
+ bbsData.points.forEach((p, i) => {
350
+ const col = i % 4;
351
+ const row = Math.floor(i / 4);
352
+ const px = 40 + col * colW;
353
+ const py = dy + 70 + row * 60;
354
+ ctx.fillStyle = '#999';
355
+ ctx.font = `12px "${font}"`;
356
+ ctx.fillText(p.l, px, py);
357
+ ctx.fillStyle = '#333';
358
+ ctx.font = `bold 20px "${font}"`;
359
+ ctx.fillText(p.v, px, py + 20);
360
+ });
361
+ dy += pointsH + gap;
362
+ }
363
+ // 绘制 BBS 详细贡献
364
+ if (bbsData.detailed.length > 0) {
365
+ ctx.fillStyle = '#fff';
366
+ (0, rendering_1.roundRect)(ctx, 20, dy, width - 40, detailedH, 10);
367
+ ctx.fill();
368
+ ctx.fillStyle = '#333';
369
+ ctx.font = `bold 18px "${font}"`;
370
+ ctx.fillText('详细贡献', 40, dy + 25);
371
+ ctx.beginPath();
372
+ ctx.moveTo(40, dy + 50);
373
+ ctx.lineTo(width - 40, dy + 50);
374
+ ctx.stroke();
375
+ const colW = (width - 80) / 2;
376
+ bbsData.detailed.forEach((d, i) => {
377
+ const col = i % 2;
378
+ const row = Math.floor(i / 2);
379
+ const dx = 40 + col * colW;
380
+ const dyLoc = dy + 70 + row * 50;
381
+ ctx.fillStyle = '#555';
382
+ ctx.font = `16px "${font}"`;
383
+ ctx.fillText(d.l, dx, dyLoc);
384
+ ctx.fillStyle = '#2ecc71';
385
+ ctx.font = `bold 16px "${font}"`;
386
+ const addTxt = `+${d.add}`;
387
+ const addW = ctx.measureText(addTxt).width;
388
+ ctx.fillText(addTxt, dx + 120, dyLoc);
389
+ ctx.fillStyle = '#3498db';
390
+ const editTxt = `~${d.edit}`;
391
+ ctx.fillText(editTxt, dx + 120 + addW + 15, dyLoc);
392
+ });
393
+ dy += detailedH + gap;
394
+ }
395
+ // 绘制 基础统计
396
+ ctx.fillStyle = '#fff';
397
+ (0, rendering_1.roundRect)(ctx, 20, dy, width - 40, statsH, 10);
398
+ ctx.fill();
399
+ ctx.fillStyle = '#333';
400
+ ctx.font = `bold 18px "${font}"`;
401
+ ctx.fillText('基础统计', 40, dy + 25);
402
+ ctx.beginPath();
403
+ ctx.moveTo(40, dy + 50);
404
+ ctx.lineTo(width - 40, dy + 50);
405
+ ctx.stroke();
406
+ const colW = (width - 40) / 3;
407
+ basicStats.forEach((s, i) => {
408
+ const col = i % 3, row = Math.floor(i / 3);
409
+ const cx = 20 + col * colW;
410
+ const cy = dy + 70 + row * 50;
411
+ ctx.fillStyle = '#999';
412
+ ctx.font = `14px "${font}"`;
413
+ ctx.fillText(s.l, cx + 30, cy);
414
+ ctx.fillStyle = '#333';
415
+ ctx.font = `bold 16px "${font}"`;
416
+ ctx.fillText(s.v, cx + 30, cy + 25);
417
+ });
418
+ dy += statsH + gap;
419
+ // 绘制 表态
420
+ ctx.fillStyle = '#fff';
421
+ (0, rendering_1.roundRect)(ctx, 20, dy, width - 40, reactionSectionH, 10);
422
+ ctx.fill();
423
+ ctx.fillStyle = '#333';
424
+ ctx.font = `bold 18px "${font}"`;
425
+ ctx.fillText('表态统计', 40, dy + 25);
426
+ ctx.beginPath();
427
+ ctx.moveTo(40, dy + 50);
428
+ ctx.lineTo(width - 40, dy + 50);
429
+ ctx.stroke();
430
+ if (reactions.length) {
431
+ let rx = 50, ry = dy + 75;
432
+ ctx.font = `14px "${font}"`;
433
+ reactions.forEach(r => {
434
+ const t = `${r.l}: ${r.c}`;
435
+ const w = ctx.measureText(t).width + 30;
436
+ if (rx + w > width - 50) {
437
+ rx = 50;
438
+ ry += 35;
439
+ }
440
+ ctx.fillStyle = '#f0f2f5';
441
+ (0, rendering_1.roundRect)(ctx, rx, ry - 18, w, 28, 14);
442
+ ctx.fill();
443
+ ctx.fillStyle = '#e74c3c';
444
+ ctx.beginPath();
445
+ ctx.arc(rx + 10, ry - 4, 3, 0, Math.PI * 2);
446
+ ctx.fill();
447
+ ctx.fillStyle = '#555';
448
+ ctx.fillText(t, rx + 20, ry - 10);
449
+ rx += w + 10;
450
+ });
451
+ }
452
+ else {
453
+ ctx.fillStyle = '#ccc';
454
+ ctx.font = `14px "${font}"`;
455
+ ctx.fillText('暂无表态', 50, dy + 75);
456
+ }
457
+ dy += reactionSectionH + gap;
458
+ // 绘制 热力图
459
+ ctx.fillStyle = '#fff';
460
+ (0, rendering_1.roundRect)(ctx, 20, dy, width - 40, mapH, 10);
461
+ ctx.fill();
462
+ ctx.fillStyle = '#333';
463
+ ctx.font = `bold 18px "${font}"`;
464
+ ctx.fillText(`活跃度 (${currentYear})`, 40, dy + 25);
465
+ ctx.beginPath();
466
+ ctx.moveTo(40, dy + 50);
467
+ ctx.lineTo(width - 40, dy + 50);
468
+ ctx.stroke();
469
+ const box = 11, g = 3, sx = 50, sy = dy + 70;
470
+ const start = new Date(currentYear, 0, 1);
471
+ let curr = new Date(currentYear, 0, 1);
472
+ const end = new Date(currentYear, 11, 31);
473
+ while (curr <= end) {
474
+ const doy = Math.floor((curr.getTime() - start.getTime()) / 86400000);
475
+ const c = Math.floor((doy + start.getDay() + 6) / 7);
476
+ const r = (curr.getDay() + 6) % 7;
477
+ if (c < 53) {
478
+ const count = activityMap[curr.toISOString().split('T')[0]] || 0;
479
+ ctx.fillStyle = count === 0 ? '#ebedf0' : count <= 2 ? '#9be9a8' : count <= 5 ? '#40c463' : '#216e39';
480
+ (0, rendering_1.roundRect)(ctx, sx + c * (box + g), sy + r * (box + g), box, box, 2);
481
+ ctx.fill();
482
+ }
483
+ curr.setDate(curr.getDate() + 1);
484
+ }
485
+ dy += mapH + gap;
486
+ // 绘制详细时间列表
487
+ if (bbsData.times.length > 0) {
488
+ ctx.fillStyle = '#666';
489
+ ctx.font = `12px "${font}"`;
490
+ let tx = 40, ty = dy;
491
+ bbsData.times.forEach(t => {
492
+ const str = `${t.l}: ${t.v}`;
493
+ const w = ctx.measureText(str).width;
494
+ if (tx + w > width - 40) {
495
+ tx = 40; // 换行
496
+ ty += 20;
497
+ }
498
+ ctx.fillText(str, tx, ty);
499
+ tx += w + 30; // 字段间距
500
+ });
501
+ dy = ty + 30; // 更新总高度游标
502
+ }
503
+ // Footer
504
+ ctx.fillStyle = '#999';
505
+ ctx.font = `12px "${font}"`;
506
+ ctx.textAlign = 'center';
507
+ ctx.fillText('mcmod.cn & bbs.mcmod.cn | Powered by Koishi | Plugin By Mai_xiyu', width / 2, totalHeight - 15);
508
+ return await canvas.encode('png');
509
+ }