koishi-plugin-cfmrmod 1.1.2 → 1.1.4
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/dist/mcmod/cards/author-card.js +472 -0
- package/dist/mcmod/cards/center-card.js +509 -0
- package/dist/mcmod/cards/info-card.js +262 -0
- package/dist/mcmod/cards/mod-card.js +828 -0
- package/dist/mcmod/cards/tutorial-card.js +531 -0
- package/dist/mcmod/cards.js +14 -0
- package/dist/mcmod/constants.js +13 -0
- package/dist/mcmod/http.js +195 -0
- package/dist/mcmod/index.js +5 -3483
- package/dist/mcmod/plugin.js +310 -0
- package/dist/mcmod/rendering.js +388 -0
- package/dist/mcmod/search.js +122 -0
- package/dist/mcmod/utils.js +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.drawAuthorCard = drawAuthorCard;
|
|
4
|
+
const cheerio = require('cheerio');
|
|
5
|
+
const http_1 = require("../http");
|
|
6
|
+
const rendering_1 = require("../rendering");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
// ================= 渲染:作者卡片 (macOS 风格) =================
|
|
9
|
+
// ================= 渲染:作者卡片 (macOS 风格) =================
|
|
10
|
+
async function drawAuthorCard(url) {
|
|
11
|
+
var _a;
|
|
12
|
+
const uid = ((_a = url.match(/author\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]) || 'Unknown';
|
|
13
|
+
// 1. 获取数据
|
|
14
|
+
const html = await (0, http_1.fetchMcmodText)(url, { headers: (0, http_1.getHeaders)(url) });
|
|
15
|
+
const $ = cheerio.load(html);
|
|
16
|
+
const username = (0, utils_1.cleanText)($('.author-name h5').text()) || $('title').text().split('-')[0].trim();
|
|
17
|
+
const subname = $('.author-name .subname p').map((i, el) => $(el).text().trim()).get().join(' / ');
|
|
18
|
+
const avatarUrl = (0, utils_1.fixUrl)($('.author-user-avatar img').attr('src'));
|
|
19
|
+
const bio = (0, utils_1.cleanText)($('.author-content .text').text()) || '(暂无简介)';
|
|
20
|
+
// 统计数据
|
|
21
|
+
const pageInfo = {};
|
|
22
|
+
const fullText = $('body').text().replace(/\s+/g, ' ');
|
|
23
|
+
function extractStat(regex) {
|
|
24
|
+
const m = fullText.match(regex);
|
|
25
|
+
if (m && m[1] && m[1].length < 20)
|
|
26
|
+
return m[1].trim();
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
pageInfo.views = extractStat(/浏览量[::]\s*([\d,]+)/);
|
|
30
|
+
pageInfo.createDate = extractStat(/创建日期[::]\s*(\d{4}-\d{2}-\d{2}|\d+年前|\d+个月前|\d+天前)/);
|
|
31
|
+
pageInfo.lastEdit = extractStat(/最后编辑[::]\s*(\d{4}-\d{2}-\d{2}|\d+年前|\d+个月前|\d+天前)/);
|
|
32
|
+
pageInfo.editCount = extractStat(/编辑次数[::]\s*(\d+)/);
|
|
33
|
+
let favCount = '0';
|
|
34
|
+
const favEl = $('.author-fav .nums, .common-fuc-group li.like .nums, .fav-count');
|
|
35
|
+
if (favEl.length) {
|
|
36
|
+
favCount = favEl.attr('title') || favEl.text().trim() || '0';
|
|
37
|
+
}
|
|
38
|
+
if (favCount === '0') {
|
|
39
|
+
const favMatch = fullText.match(/收藏\s*(\d+)/);
|
|
40
|
+
if (favMatch)
|
|
41
|
+
favCount = favMatch[1];
|
|
42
|
+
}
|
|
43
|
+
const stats = [];
|
|
44
|
+
if (pageInfo.views)
|
|
45
|
+
stats.push({ l: '浏览量', v: pageInfo.views });
|
|
46
|
+
if (pageInfo.createDate)
|
|
47
|
+
stats.push({ l: '创建日期', v: pageInfo.createDate });
|
|
48
|
+
if (pageInfo.lastEdit)
|
|
49
|
+
stats.push({ l: '最后编辑', v: pageInfo.lastEdit });
|
|
50
|
+
if (pageInfo.editCount)
|
|
51
|
+
stats.push({ l: '编辑次数', v: pageInfo.editCount });
|
|
52
|
+
if (favCount)
|
|
53
|
+
stats.push({ l: '收藏', v: favCount });
|
|
54
|
+
const links = [];
|
|
55
|
+
$('.author-link .common-link-icon-list a, .common-link-icon-frame a').each((i, el) => {
|
|
56
|
+
const h = $(el).attr('href');
|
|
57
|
+
let n = $(el).attr('data-original-title') || $(el).text().trim();
|
|
58
|
+
if (!n && h) {
|
|
59
|
+
if (h.includes('github'))
|
|
60
|
+
n = 'GitHub';
|
|
61
|
+
else if (h.includes('bilibili'))
|
|
62
|
+
n = 'Bilibili';
|
|
63
|
+
else if (h.includes('curseforge'))
|
|
64
|
+
n = 'CurseForge';
|
|
65
|
+
else if (h.includes('modrinth'))
|
|
66
|
+
n = 'Modrinth';
|
|
67
|
+
else if (h.includes('mcbbs'))
|
|
68
|
+
n = 'MCBBS';
|
|
69
|
+
else
|
|
70
|
+
n = 'Link';
|
|
71
|
+
}
|
|
72
|
+
if (n && h && !links.some(l => l.n === n))
|
|
73
|
+
links.push({ n, h });
|
|
74
|
+
});
|
|
75
|
+
// 列表抓取 - 优先使用特定类名,因为它们更稳定
|
|
76
|
+
const teams = [];
|
|
77
|
+
const projects = [];
|
|
78
|
+
const partners = [];
|
|
79
|
+
// 辅助函数:从容器中提取列表项
|
|
80
|
+
function extractListItems(container, targetList, isProject = false) {
|
|
81
|
+
// 增加 .block 选择器以匹配 div.block (用于参与项目)
|
|
82
|
+
container.find('li.block, .block, .row > div').each((i, el) => {
|
|
83
|
+
const n = (0, utils_1.cleanText)($(el).find('.name a, .name, h4').first().text());
|
|
84
|
+
if (!n)
|
|
85
|
+
return;
|
|
86
|
+
const m = (0, utils_1.fixUrl)($(el).find('img').attr('src'));
|
|
87
|
+
// 增加 .count 选择器 (用于相关作者的合作次数)
|
|
88
|
+
const r = (0, utils_1.cleanText)($(el).find('.position, .meta, .count').text());
|
|
89
|
+
// 获取类型标签 (模组/整合包等)
|
|
90
|
+
let t = '';
|
|
91
|
+
if (isProject) {
|
|
92
|
+
const badge = $(el).find('.badge, .badge-mod, .badge-modpack').first().text().trim();
|
|
93
|
+
if (badge)
|
|
94
|
+
t = badge;
|
|
95
|
+
}
|
|
96
|
+
if (!targetList.some(x => x.n === n)) {
|
|
97
|
+
targetList.push({ n, m, r, t });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// 1. 尝试特定类名 (根据用户提供的 HTML 结构修正)
|
|
102
|
+
extractListItems($('.author-member .list, .author-team .list'), teams, false);
|
|
103
|
+
extractListItems($('.author-mods .list'), projects, true);
|
|
104
|
+
extractListItems($('.author-partner .list, .author-users .list'), partners, false);
|
|
105
|
+
// 2. 如果没抓到,尝试通用抓取 (遍历所有 block/panel)
|
|
106
|
+
if (teams.length === 0 || projects.length === 0 || partners.length === 0) {
|
|
107
|
+
$('.common-card-layout, .panel, .block').each((i, el) => {
|
|
108
|
+
const title = $(el).find('.head, .panel-heading, h3, h4').text().trim();
|
|
109
|
+
if (teams.length === 0 && title.includes('参与团队'))
|
|
110
|
+
extractListItems($(el), teams);
|
|
111
|
+
if (projects.length === 0 && (title.includes('参与项目') || title.includes('发布的模组')))
|
|
112
|
+
extractListItems($(el), projects);
|
|
113
|
+
if (partners.length === 0 && (title.includes('相关作者') || title.includes('合作者')))
|
|
114
|
+
extractListItems($(el), partners);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// 2. 布局计算
|
|
118
|
+
const width = 800;
|
|
119
|
+
const font = rendering_1.GLOBAL_FONT_FAMILY;
|
|
120
|
+
const padding = 40;
|
|
121
|
+
const windowMargin = 20;
|
|
122
|
+
const contentW = width - windowMargin * 2 - padding * 2; // 实际内容宽度
|
|
123
|
+
// 严格计算高度
|
|
124
|
+
let cursorY = 60; // Initial padding inside window
|
|
125
|
+
// Avatar area
|
|
126
|
+
cursorY += 100 + 40; // Avatar(100) + gap(40)
|
|
127
|
+
// Stats Grid
|
|
128
|
+
if (stats.length > 0) {
|
|
129
|
+
cursorY += 80 + 30; // StatH(80) + gap(30)
|
|
130
|
+
}
|
|
131
|
+
// Links
|
|
132
|
+
if (links.length > 0) {
|
|
133
|
+
// Simulate link wrapping
|
|
134
|
+
const tempC = (0, rendering_1.createCanvas)(100, 100);
|
|
135
|
+
const tempCtx = tempC.getContext('2d');
|
|
136
|
+
tempCtx.font = `bold 14px "${font}"`;
|
|
137
|
+
let lx = 0;
|
|
138
|
+
let ly = 0;
|
|
139
|
+
let rowH = 34;
|
|
140
|
+
links.forEach(l => {
|
|
141
|
+
const lw = tempCtx.measureText(l.n).width + 30;
|
|
142
|
+
if (lx + lw > contentW) {
|
|
143
|
+
lx = 0;
|
|
144
|
+
ly += 45; // Line gap
|
|
145
|
+
}
|
|
146
|
+
lx += lw + 10;
|
|
147
|
+
});
|
|
148
|
+
cursorY += ly + rowH + 60; // + gap
|
|
149
|
+
}
|
|
150
|
+
// Lists Calculation Helper
|
|
151
|
+
function calcSectionHeight(items, itemH, cols) {
|
|
152
|
+
if (!items.length)
|
|
153
|
+
return 0;
|
|
154
|
+
const rows = Math.ceil(items.length / cols);
|
|
155
|
+
// Title(35) + Rows * (ItemH + 15) + BottomGap(30)
|
|
156
|
+
return 35 + rows * (itemH + 15) + 30;
|
|
157
|
+
}
|
|
158
|
+
cursorY += calcSectionHeight(teams, 70, 3);
|
|
159
|
+
cursorY += calcSectionHeight(projects, 90, 2);
|
|
160
|
+
cursorY += calcSectionHeight(partners, 100, 5);
|
|
161
|
+
// Bio
|
|
162
|
+
let bioH = 0;
|
|
163
|
+
if (bio && bio !== '(暂无简介)') {
|
|
164
|
+
const tempC = (0, rendering_1.createCanvas)(100, 100);
|
|
165
|
+
const tempCtx = tempC.getContext('2d');
|
|
166
|
+
tempCtx.font = `16px "${font}"`;
|
|
167
|
+
// Title(35)
|
|
168
|
+
cursorY += 35;
|
|
169
|
+
// Content
|
|
170
|
+
bioH = (0, rendering_1.wrapText)(tempCtx, bio, 0, 0, contentW - 40, 26, 1000, false);
|
|
171
|
+
cursorY += bioH + 40 + 60; // Padding inside rect(40) + BottomGap(60)
|
|
172
|
+
}
|
|
173
|
+
// Footer
|
|
174
|
+
cursorY += 30;
|
|
175
|
+
const windowH = cursorY;
|
|
176
|
+
const totalH = windowH + windowMargin * 2;
|
|
177
|
+
const canvas = (0, rendering_1.createCanvas)(width, totalH);
|
|
178
|
+
const ctx = canvas.getContext('2d');
|
|
179
|
+
// 3. 绘制背景 (使用微软 Bing 每日图片/自然风格)
|
|
180
|
+
try {
|
|
181
|
+
// 使用 Bing 每日图片 API (1920x1080)
|
|
182
|
+
const bgUrl = 'https://bing.biturl.top/?resolution=1920&format=image&index=0&mkt=zh-CN';
|
|
183
|
+
const bgImg = await (0, rendering_1.loadImage)(bgUrl);
|
|
184
|
+
// 保持比例填充
|
|
185
|
+
const r = Math.max(width / bgImg.width, totalH / bgImg.height);
|
|
186
|
+
const dw = bgImg.width * r;
|
|
187
|
+
const dh = bgImg.height * r;
|
|
188
|
+
const dx = (width - dw) / 2;
|
|
189
|
+
const dy = (totalH - dh) / 2;
|
|
190
|
+
ctx.drawImage(bgImg, dx, dy, dw, dh);
|
|
191
|
+
// 叠加一层模糊遮罩或颜色,保证文字可读性 (虽然有亚克力板,但背景太花也不好)
|
|
192
|
+
// 这里不模糊背景本身(Canvas模糊开销大),而是加一层半透明遮罩
|
|
193
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
194
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
// 失败回退到渐变
|
|
198
|
+
const grad = ctx.createLinearGradient(0, 0, width, totalH);
|
|
199
|
+
grad.addColorStop(0, '#a18cd1');
|
|
200
|
+
grad.addColorStop(1, '#fbc2eb');
|
|
201
|
+
ctx.fillStyle = grad;
|
|
202
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
203
|
+
}
|
|
204
|
+
// 4. 绘制 Acrylic 窗口
|
|
205
|
+
const windowW = width - windowMargin * 2;
|
|
206
|
+
ctx.save();
|
|
207
|
+
// 窗口阴影
|
|
208
|
+
ctx.shadowColor = 'rgba(0,0,0,0.3)';
|
|
209
|
+
ctx.shadowBlur = 40;
|
|
210
|
+
ctx.shadowOffsetY = 20;
|
|
211
|
+
// 窗口背景 (40% Acrylic - 模拟)
|
|
212
|
+
// 使用白色半透明 + 背景模糊效果 (Canvas 无法直接 backdrop-filter,只能通过叠加半透明白)
|
|
213
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)'; // 提高不透明度以遮盖背景杂乱
|
|
214
|
+
(0, rendering_1.roundRect)(ctx, windowMargin, windowMargin, windowW, windowH, 20);
|
|
215
|
+
ctx.fill();
|
|
216
|
+
ctx.restore();
|
|
217
|
+
// 窗口边框
|
|
218
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
|
|
219
|
+
ctx.lineWidth = 1.5;
|
|
220
|
+
(0, rendering_1.roundRect)(ctx, windowMargin, windowMargin, windowW, windowH, 20);
|
|
221
|
+
ctx.stroke();
|
|
222
|
+
// 5. 窗口控件 (Traffic Lights)
|
|
223
|
+
const controlY = windowMargin + 20;
|
|
224
|
+
const controlX = windowMargin + 20;
|
|
225
|
+
const controlR = 6;
|
|
226
|
+
const controlGap = 20;
|
|
227
|
+
ctx.fillStyle = '#ff5f56'; // Red
|
|
228
|
+
ctx.beginPath();
|
|
229
|
+
ctx.arc(controlX, controlY, controlR, 0, Math.PI * 2);
|
|
230
|
+
ctx.fill();
|
|
231
|
+
ctx.fillStyle = '#ffbd2e'; // Yellow
|
|
232
|
+
ctx.beginPath();
|
|
233
|
+
ctx.arc(controlX + controlGap, controlY, controlR, 0, Math.PI * 2);
|
|
234
|
+
ctx.fill();
|
|
235
|
+
ctx.fillStyle = '#27c93f'; // Green
|
|
236
|
+
ctx.beginPath();
|
|
237
|
+
ctx.arc(controlX + controlGap * 2, controlY, controlR, 0, Math.PI * 2);
|
|
238
|
+
ctx.fill();
|
|
239
|
+
// 6. 内容绘制
|
|
240
|
+
// 重置 cursorY 到窗口内部起始位置
|
|
241
|
+
cursorY = windowMargin + 60;
|
|
242
|
+
const contentX = windowMargin + padding;
|
|
243
|
+
// Header: Avatar & Name
|
|
244
|
+
const avatarSize = 100;
|
|
245
|
+
// Avatar
|
|
246
|
+
ctx.save();
|
|
247
|
+
ctx.shadowColor = 'rgba(0,0,0,0.1)';
|
|
248
|
+
ctx.shadowBlur = 10;
|
|
249
|
+
ctx.beginPath();
|
|
250
|
+
ctx.arc(contentX + avatarSize / 2, cursorY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
|
|
251
|
+
ctx.fillStyle = '#fff';
|
|
252
|
+
ctx.fill();
|
|
253
|
+
ctx.shadowBlur = 0;
|
|
254
|
+
ctx.clip();
|
|
255
|
+
if (avatarUrl) {
|
|
256
|
+
try {
|
|
257
|
+
const img = await (0, rendering_1.loadImage)(avatarUrl);
|
|
258
|
+
ctx.drawImage(img, contentX, cursorY, avatarSize, avatarSize);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
ctx.fillStyle = '#ddd';
|
|
262
|
+
ctx.fill();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
ctx.fillStyle = '#ddd';
|
|
267
|
+
ctx.fill();
|
|
268
|
+
}
|
|
269
|
+
ctx.restore();
|
|
270
|
+
// Name & UID
|
|
271
|
+
const textX = contentX + avatarSize + 30;
|
|
272
|
+
ctx.fillStyle = '#333';
|
|
273
|
+
ctx.font = `bold 40px "${font}"`;
|
|
274
|
+
ctx.textBaseline = 'top';
|
|
275
|
+
ctx.fillText(username, textX, cursorY + 10);
|
|
276
|
+
// UID Chip
|
|
277
|
+
const uidText = `UID: ${uid}`;
|
|
278
|
+
ctx.font = `bold 14px "${font}"`;
|
|
279
|
+
const uidW = ctx.measureText(uidText).width + 20;
|
|
280
|
+
ctx.fillStyle = 'rgba(0,0,0,0.05)';
|
|
281
|
+
(0, rendering_1.roundRect)(ctx, textX, cursorY + 60, uidW, 24, 12);
|
|
282
|
+
ctx.fill();
|
|
283
|
+
ctx.fillStyle = '#666';
|
|
284
|
+
ctx.fillText(uidText, textX + 10, cursorY + 64);
|
|
285
|
+
// Subname (Alias)
|
|
286
|
+
if (subname) {
|
|
287
|
+
ctx.fillStyle = '#999';
|
|
288
|
+
ctx.font = `14px "${font}"`;
|
|
289
|
+
// 绘制在 UID 下方,稍微留点间距
|
|
290
|
+
ctx.fillText(subname, textX, cursorY + 95);
|
|
291
|
+
}
|
|
292
|
+
cursorY += avatarSize + 40;
|
|
293
|
+
// Stats Grid
|
|
294
|
+
if (stats.length > 0) {
|
|
295
|
+
const statW = (contentW - (stats.length - 1) * 15) / stats.length;
|
|
296
|
+
const statH = 80;
|
|
297
|
+
stats.forEach((s, i) => {
|
|
298
|
+
const sx = contentX + i * (statW + 15);
|
|
299
|
+
// Card bg
|
|
300
|
+
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
|
301
|
+
(0, rendering_1.roundRect)(ctx, sx, cursorY, statW, statH, 12);
|
|
302
|
+
ctx.fill();
|
|
303
|
+
// Label
|
|
304
|
+
ctx.textAlign = 'center';
|
|
305
|
+
ctx.fillStyle = '#666';
|
|
306
|
+
ctx.font = `14px "${font}"`;
|
|
307
|
+
ctx.fillText(s.l, sx + statW / 2, cursorY + 15);
|
|
308
|
+
// Value
|
|
309
|
+
ctx.fillStyle = '#333';
|
|
310
|
+
ctx.font = `bold 20px "${font}"`;
|
|
311
|
+
// Auto scale font if too long
|
|
312
|
+
let fontSize = 20;
|
|
313
|
+
while (ctx.measureText(s.v).width > statW - 10 && fontSize > 10) {
|
|
314
|
+
fontSize--;
|
|
315
|
+
ctx.font = `bold ${fontSize}px "${font}"`;
|
|
316
|
+
}
|
|
317
|
+
ctx.fillText(s.v, sx + statW / 2, cursorY + 45);
|
|
318
|
+
});
|
|
319
|
+
ctx.textAlign = 'left';
|
|
320
|
+
cursorY += statH + 30;
|
|
321
|
+
}
|
|
322
|
+
// Links
|
|
323
|
+
if (links.length > 0) {
|
|
324
|
+
let lx = contentX;
|
|
325
|
+
let ly = cursorY;
|
|
326
|
+
links.forEach(l => {
|
|
327
|
+
ctx.font = `bold 14px "${font}"`;
|
|
328
|
+
const lw = ctx.measureText(l.n).width + 30;
|
|
329
|
+
if (lx + lw > contentX + contentW) {
|
|
330
|
+
lx = contentX;
|
|
331
|
+
ly += 45;
|
|
332
|
+
}
|
|
333
|
+
ctx.fillStyle = '#fff';
|
|
334
|
+
ctx.shadowColor = 'rgba(0,0,0,0.05)';
|
|
335
|
+
ctx.shadowBlur = 5;
|
|
336
|
+
(0, rendering_1.roundRect)(ctx, lx, ly, lw, 34, 17);
|
|
337
|
+
ctx.fill();
|
|
338
|
+
ctx.shadowBlur = 0;
|
|
339
|
+
ctx.fillStyle = '#333';
|
|
340
|
+
ctx.fillText(l.n, lx + 15, ly + 8);
|
|
341
|
+
lx += lw + 10;
|
|
342
|
+
});
|
|
343
|
+
cursorY = ly + 60;
|
|
344
|
+
}
|
|
345
|
+
// Helper for Lists
|
|
346
|
+
async function drawSection(title, items, itemH, cols, renderItem) {
|
|
347
|
+
if (!items.length)
|
|
348
|
+
return;
|
|
349
|
+
ctx.fillStyle = '#333';
|
|
350
|
+
ctx.font = `bold 22px "${font}"`;
|
|
351
|
+
ctx.fillText(title, contentX, cursorY);
|
|
352
|
+
cursorY += 35;
|
|
353
|
+
const itemW = (contentW - (cols - 1) * 15) / cols;
|
|
354
|
+
for (let i = 0; i < items.length; i++) {
|
|
355
|
+
const col = i % cols;
|
|
356
|
+
const row = Math.floor(i / cols);
|
|
357
|
+
const ix = contentX + col * (itemW + 15);
|
|
358
|
+
const iy = cursorY + row * (itemH + 15);
|
|
359
|
+
// Item Card
|
|
360
|
+
ctx.fillStyle = 'rgba(255,255,255,0.7)';
|
|
361
|
+
(0, rendering_1.roundRect)(ctx, ix, iy, itemW, itemH, 12);
|
|
362
|
+
ctx.fill();
|
|
363
|
+
await renderItem(items[i], ix, iy, itemW, itemH);
|
|
364
|
+
}
|
|
365
|
+
cursorY += Math.ceil(items.length / cols) * (itemH + 15) + 30;
|
|
366
|
+
}
|
|
367
|
+
// Draw Lists
|
|
368
|
+
await drawSection('参与团队', teams, 70, 3, async (item, x, y, w, h) => {
|
|
369
|
+
if (item.m) {
|
|
370
|
+
try {
|
|
371
|
+
const img = await (0, rendering_1.loadImage)(item.m);
|
|
372
|
+
ctx.drawImage(img, x + 10, y + 15, 40, 40);
|
|
373
|
+
}
|
|
374
|
+
catch (e) { }
|
|
375
|
+
}
|
|
376
|
+
ctx.fillStyle = '#333';
|
|
377
|
+
ctx.font = `bold 16px "${font}"`;
|
|
378
|
+
ctx.fillText(item.n, x + 60, y + 15);
|
|
379
|
+
if (item.r) {
|
|
380
|
+
ctx.fillStyle = '#666';
|
|
381
|
+
ctx.font = `12px "${font}"`;
|
|
382
|
+
ctx.fillText(item.r, x + 60, y + 40);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
await drawSection('参与项目', projects, 90, 2, async (item, x, y, w, h) => {
|
|
386
|
+
if (item.m) {
|
|
387
|
+
try {
|
|
388
|
+
const img = await (0, rendering_1.loadImage)(item.m);
|
|
389
|
+
ctx.drawImage(img, x + 10, y + 15, 100, 60);
|
|
390
|
+
}
|
|
391
|
+
catch (e) { }
|
|
392
|
+
}
|
|
393
|
+
// 绘制类型标签 (模组/整合包)
|
|
394
|
+
let nameOffsetX = 120;
|
|
395
|
+
if (item.t) {
|
|
396
|
+
ctx.font = `bold 12px "${font}"`;
|
|
397
|
+
const tagText = item.t;
|
|
398
|
+
const tagW = ctx.measureText(tagText).width + 12;
|
|
399
|
+
const tagH = 20;
|
|
400
|
+
const tagX = x + 120;
|
|
401
|
+
const tagY = y + 12;
|
|
402
|
+
// 根据类型设置颜色:模组=绿色,整合包=橙色,其他=灰色
|
|
403
|
+
let tagBg = '#999';
|
|
404
|
+
if (tagText.includes('模组'))
|
|
405
|
+
tagBg = '#2ecc71';
|
|
406
|
+
else if (tagText.includes('整合包'))
|
|
407
|
+
tagBg = '#e67e22';
|
|
408
|
+
else if (tagText.includes('资料'))
|
|
409
|
+
tagBg = '#3498db';
|
|
410
|
+
ctx.fillStyle = tagBg;
|
|
411
|
+
(0, rendering_1.roundRect)(ctx, tagX, tagY, tagW, tagH, 4);
|
|
412
|
+
ctx.fill();
|
|
413
|
+
ctx.fillStyle = '#fff';
|
|
414
|
+
ctx.fillText(tagText, tagX + 6, tagY + 4);
|
|
415
|
+
nameOffsetX = 120 + tagW + 8;
|
|
416
|
+
}
|
|
417
|
+
// 去掉名称中的类型前缀(避免与标签重复)
|
|
418
|
+
let displayName = item.n;
|
|
419
|
+
if (item.t) {
|
|
420
|
+
// 移除开头的 "模组"、"整合包" 等前缀
|
|
421
|
+
displayName = displayName.replace(/^(模组|整合包|资料)\s*/g, '').trim();
|
|
422
|
+
}
|
|
423
|
+
ctx.fillStyle = '#333';
|
|
424
|
+
ctx.font = `bold 16px "${font}"`;
|
|
425
|
+
(0, rendering_1.wrapText)(ctx, displayName, x + nameOffsetX, y + 15, w - nameOffsetX - 10, 20, 2, true);
|
|
426
|
+
if (item.r) {
|
|
427
|
+
ctx.fillStyle = '#666';
|
|
428
|
+
ctx.font = `12px "${font}"`;
|
|
429
|
+
ctx.fillText(item.r, x + 120, y + 60);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
await drawSection('相关作者', partners, 100, 5, async (item, x, y, w, h) => {
|
|
433
|
+
const iconSize = 50;
|
|
434
|
+
if (item.m) {
|
|
435
|
+
try {
|
|
436
|
+
const img = await (0, rendering_1.loadImage)(item.m);
|
|
437
|
+
ctx.save();
|
|
438
|
+
ctx.beginPath();
|
|
439
|
+
ctx.arc(x + w / 2, y + 25, iconSize / 2, 0, Math.PI * 2);
|
|
440
|
+
ctx.clip();
|
|
441
|
+
ctx.drawImage(img, x + w / 2 - iconSize / 2, y, iconSize, iconSize);
|
|
442
|
+
ctx.restore();
|
|
443
|
+
}
|
|
444
|
+
catch (e) { }
|
|
445
|
+
}
|
|
446
|
+
ctx.textAlign = 'center';
|
|
447
|
+
ctx.fillStyle = '#333';
|
|
448
|
+
ctx.font = `14px "${font}"`;
|
|
449
|
+
(0, rendering_1.wrapText)(ctx, item.n, x + w / 2, y + 60, w - 10, 18, 2, true);
|
|
450
|
+
ctx.textAlign = 'left';
|
|
451
|
+
});
|
|
452
|
+
// Bio
|
|
453
|
+
if (bio && bio !== '(暂无简介)') {
|
|
454
|
+
ctx.fillStyle = '#333';
|
|
455
|
+
ctx.font = `bold 22px "${font}"`;
|
|
456
|
+
ctx.fillText('简介', contentX, cursorY);
|
|
457
|
+
cursorY += 35;
|
|
458
|
+
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
|
459
|
+
(0, rendering_1.roundRect)(ctx, contentX, cursorY, contentW, bioH + 40, 12);
|
|
460
|
+
ctx.fill();
|
|
461
|
+
ctx.fillStyle = '#444';
|
|
462
|
+
ctx.font = `16px "${font}"`;
|
|
463
|
+
(0, rendering_1.wrapText)(ctx, bio, contentX + 20, cursorY + 20, contentW - 40, 26, 1000, true);
|
|
464
|
+
cursorY += bioH + 60;
|
|
465
|
+
}
|
|
466
|
+
// Footer
|
|
467
|
+
ctx.fillStyle = 'rgba(0,0,0,0.5)';
|
|
468
|
+
ctx.font = `12px "${font}"`;
|
|
469
|
+
ctx.textAlign = 'center';
|
|
470
|
+
ctx.fillText('mcmod.cn | Powered by Koishi | Plugin By Mai_xiyu', width / 2, totalH - 15);
|
|
471
|
+
return await canvas.encode('png');
|
|
472
|
+
}
|