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.
- 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 +758 -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,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createInfoCard = createInfoCard;
|
|
4
|
+
const cheerio = require('cheerio');
|
|
5
|
+
const http_1 = require("../http");
|
|
6
|
+
const rendering_1 = require("../rendering");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
// ================= 详情页卡片 =================
|
|
9
|
+
// ================= 详情页卡片 (资料/物品/通用) =================
|
|
10
|
+
// ================= 详情页卡片 (资料/物品/通用) - 深度解析版 =================
|
|
11
|
+
async function createInfoCard(url, type) {
|
|
12
|
+
// 1. 获取并解析页面
|
|
13
|
+
const html = await (0, http_1.fetchMcmodText)(url, { headers: (0, http_1.getHeaders)('https://search.mcmod.cn/') });
|
|
14
|
+
const $ = cheerio.load(html);
|
|
15
|
+
// --- 基础信息 ---
|
|
16
|
+
// 标题:尝试从 .itemname 或 h3 获取
|
|
17
|
+
let title = (0, utils_1.cleanText)($('.itemname .name h5, .itemname .name').first().text());
|
|
18
|
+
if (!title)
|
|
19
|
+
title = (0, utils_1.cleanText)($('title').text().split('-')[0].trim());
|
|
20
|
+
// 来源/模组:面包屑导航倒数第三个通常是模组名
|
|
21
|
+
let source = (0, utils_1.cleanText)($('.common-nav .item').eq(1).text());
|
|
22
|
+
// 或者尝试从 nav 链接判断
|
|
23
|
+
if (!source)
|
|
24
|
+
source = (0, utils_1.cleanText)($('.common-nav li a[href*="/class/"]').last().text());
|
|
25
|
+
// 图标:优先获取高清大图 (128x128),其次普通图标
|
|
26
|
+
let imgUrl = (0, utils_1.fixUrl)($('.item-info-table img[width="128"]').attr('src'));
|
|
27
|
+
if (!imgUrl)
|
|
28
|
+
imgUrl = (0, utils_1.fixUrl)($('.item-info-table img').first().attr('src'));
|
|
29
|
+
if (!imgUrl)
|
|
30
|
+
imgUrl = (0, utils_1.fixUrl)($('.common-icon-text-frame img').attr('src'));
|
|
31
|
+
// --- 属性列表 ---
|
|
32
|
+
const props = [];
|
|
33
|
+
// 1. 抓取右侧/下方的表格数据 (.item-data table, .item-info-table table)
|
|
34
|
+
// 排除包含图片的行,只抓取文字属性
|
|
35
|
+
$('table.table-bordered tr').each((i, tr) => {
|
|
36
|
+
const tds = $(tr).find('td');
|
|
37
|
+
if (tds.length >= 2) {
|
|
38
|
+
// 可能是 <th>key</th><td>value</td> 或者 <td>key</td><td>value</td>
|
|
39
|
+
let key = (0, utils_1.cleanText)($(tds[0]).text()).replace(/[::]/g, '');
|
|
40
|
+
let val = (0, utils_1.cleanText)($(tds[1]).text());
|
|
41
|
+
// 过滤无效行 (如图标行)
|
|
42
|
+
if (key && val && val.length > 0 && !$(tds[1]).find('img').length) {
|
|
43
|
+
// 排除重复
|
|
44
|
+
if (!props.some(p => p.l === key)) {
|
|
45
|
+
props.push({ l: key, v: val });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
// --- 简介 ---
|
|
51
|
+
// 优先 .item-content,其次 meta description
|
|
52
|
+
let desc = '';
|
|
53
|
+
const contentDiv = $('.item-content.common-text').first();
|
|
54
|
+
if (contentDiv.length) {
|
|
55
|
+
desc = (0, utils_1.cleanText)(contentDiv.text());
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
desc = $('meta[name="description"]').attr('content') || '暂无简介';
|
|
59
|
+
}
|
|
60
|
+
// 清理 "MCmod does not have a description..." 等默认文本
|
|
61
|
+
if (desc.includes('MCmod does not have a description'))
|
|
62
|
+
desc = '暂无简介';
|
|
63
|
+
// --- 相关物品 (新增) ---
|
|
64
|
+
const relations = [];
|
|
65
|
+
$('.common-imglist-block .common-imglist li').each((i, el) => {
|
|
66
|
+
if (i >= 7)
|
|
67
|
+
return; // 最多显示7个
|
|
68
|
+
const name = $(el).attr('data-original-title') || (0, utils_1.cleanText)($(el).find('.text').text());
|
|
69
|
+
const icon = (0, utils_1.fixUrl)($(el).find('img').attr('src'));
|
|
70
|
+
if (name && icon)
|
|
71
|
+
relations.push({ n: name, i: icon });
|
|
72
|
+
});
|
|
73
|
+
// ================= 绘图逻辑 =================
|
|
74
|
+
const width = 800;
|
|
75
|
+
const font = rendering_1.GLOBAL_FONT_FAMILY;
|
|
76
|
+
const margin = 20;
|
|
77
|
+
const winPadding = 30;
|
|
78
|
+
const contentW = width - margin * 2 - winPadding * 2;
|
|
79
|
+
const dummyC = (0, rendering_1.createCanvas)(100, 100);
|
|
80
|
+
const dummy = dummyC.getContext('2d');
|
|
81
|
+
dummy.font = `bold 32px "${font}"`;
|
|
82
|
+
// 1. 高度计算
|
|
83
|
+
// Header (Title + Source)
|
|
84
|
+
let headerH = 60;
|
|
85
|
+
if (source)
|
|
86
|
+
headerH += 30;
|
|
87
|
+
// Content Layout: Left (Icon + Props) | Right (Desc)
|
|
88
|
+
const iconSize = 100;
|
|
89
|
+
const leftColW = 240; // 左侧宽度
|
|
90
|
+
const rightColW = contentW - leftColW - 20; // 右侧宽度
|
|
91
|
+
// Props Height
|
|
92
|
+
let propsH = 0;
|
|
93
|
+
if (props.length) {
|
|
94
|
+
propsH = props.length * 28 + 20;
|
|
95
|
+
}
|
|
96
|
+
const leftH = iconSize + 20 + propsH;
|
|
97
|
+
// Desc Height
|
|
98
|
+
dummy.font = `16px "${font}"`;
|
|
99
|
+
const descLines = (0, rendering_1.wrapText)(dummy, desc, 0, 0, rightColW, 26, 30, false) / 26;
|
|
100
|
+
const descH = 40 + descLines * 26; // Title + Text
|
|
101
|
+
// Relations Height
|
|
102
|
+
let relH = 0;
|
|
103
|
+
if (relations.length) {
|
|
104
|
+
relH = 90; // Title + Icons
|
|
105
|
+
}
|
|
106
|
+
// Main Content Height (取左右最大值)
|
|
107
|
+
let mainH = Math.max(leftH, descH);
|
|
108
|
+
// Total Layout
|
|
109
|
+
let cursorY = margin + 50; // Top traffic lights
|
|
110
|
+
const gap = 20;
|
|
111
|
+
cursorY += headerH + gap;
|
|
112
|
+
cursorY += mainH + gap;
|
|
113
|
+
if (relH)
|
|
114
|
+
cursorY += relH + gap;
|
|
115
|
+
const windowH = cursorY;
|
|
116
|
+
const totalH = windowH + margin * 2;
|
|
117
|
+
// 2. 绘制背景与窗口
|
|
118
|
+
const canvas = (0, rendering_1.createCanvas)(width, totalH);
|
|
119
|
+
const ctx = canvas.getContext('2d');
|
|
120
|
+
// 背景 (Bing)
|
|
121
|
+
try {
|
|
122
|
+
const bgUrl = 'https://bing.biturl.top/?resolution=1920&format=image&index=0&mkt=zh-CN';
|
|
123
|
+
const bgImg = await (0, rendering_1.loadImage)(bgUrl);
|
|
124
|
+
const r = Math.max(width / bgImg.width, totalH / bgImg.height);
|
|
125
|
+
ctx.drawImage(bgImg, (width - bgImg.width * r) / 2, (totalH - bgImg.height * r) / 2, bgImg.width * r, bgImg.height * r);
|
|
126
|
+
ctx.fillStyle = 'rgba(0,0,0,0.1)';
|
|
127
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
const grad = ctx.createLinearGradient(0, 0, 0, totalH);
|
|
131
|
+
grad.addColorStop(0, '#e6dee9');
|
|
132
|
+
grad.addColorStop(1, '#dad4ec'); // 柔和紫灰
|
|
133
|
+
ctx.fillStyle = grad;
|
|
134
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
135
|
+
}
|
|
136
|
+
// 窗口 (Acrylic)
|
|
137
|
+
const winX = margin, winY = margin;
|
|
138
|
+
ctx.save();
|
|
139
|
+
ctx.shadowColor = 'rgba(0,0,0,0.2)';
|
|
140
|
+
ctx.shadowBlur = 40;
|
|
141
|
+
ctx.shadowOffsetY = 20;
|
|
142
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
|
|
143
|
+
(0, rendering_1.roundRect)(ctx, winX, winY, width - margin * 2, windowH, 16);
|
|
144
|
+
ctx.fill();
|
|
145
|
+
ctx.restore();
|
|
146
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
|
|
147
|
+
ctx.lineWidth = 1;
|
|
148
|
+
(0, rendering_1.roundRect)(ctx, winX, winY, width - margin * 2, windowH, 16);
|
|
149
|
+
ctx.stroke();
|
|
150
|
+
// 交通灯
|
|
151
|
+
['#ff5f56', '#ffbd2e', '#27c93f'].forEach((c, i) => {
|
|
152
|
+
ctx.beginPath();
|
|
153
|
+
ctx.arc(winX + 20 + i * 25, winY + 20, 6, 0, Math.PI * 2);
|
|
154
|
+
ctx.fillStyle = c;
|
|
155
|
+
ctx.fill();
|
|
156
|
+
});
|
|
157
|
+
// --- 内容绘制 ---
|
|
158
|
+
let dy = winY + 50;
|
|
159
|
+
const cx = winX + winPadding;
|
|
160
|
+
// 1. Header
|
|
161
|
+
ctx.fillStyle = '#333';
|
|
162
|
+
ctx.font = `bold 32px "${font}"`;
|
|
163
|
+
ctx.textBaseline = 'top';
|
|
164
|
+
ctx.fillText(title, cx, dy);
|
|
165
|
+
if (source) {
|
|
166
|
+
ctx.fillStyle = '#888';
|
|
167
|
+
ctx.font = `bold 16px "${font}"`;
|
|
168
|
+
// 绘制所属模组标签
|
|
169
|
+
const tagW = ctx.measureText(source).width + 16;
|
|
170
|
+
ctx.fillStyle = '#f0f0f0';
|
|
171
|
+
(0, rendering_1.roundRect)(ctx, cx, dy + 45, tagW, 26, 6);
|
|
172
|
+
ctx.fill();
|
|
173
|
+
ctx.fillStyle = '#666';
|
|
174
|
+
ctx.fillText(source, cx + 8, dy + 49);
|
|
175
|
+
}
|
|
176
|
+
dy += headerH + gap;
|
|
177
|
+
// 2. Left Column (Icon + Props)
|
|
178
|
+
const leftX = cx;
|
|
179
|
+
let leftY = dy;
|
|
180
|
+
// Icon
|
|
181
|
+
if (imgUrl) {
|
|
182
|
+
try {
|
|
183
|
+
const img = await (0, rendering_1.loadImage)(imgUrl);
|
|
184
|
+
// 保持比例绘制在 100x100 区域居中
|
|
185
|
+
const r = Math.min(iconSize / img.width, iconSize / img.height);
|
|
186
|
+
const dw = img.width * r, dh = img.height * r;
|
|
187
|
+
ctx.drawImage(img, leftX + (iconSize - dw) / 2, leftY + (iconSize - dh) / 2, dw, dh);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
ctx.fillStyle = '#eee';
|
|
191
|
+
(0, rendering_1.roundRect)(ctx, leftX, leftY, iconSize, iconSize, 12);
|
|
192
|
+
ctx.fill();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
leftY += iconSize + 20;
|
|
196
|
+
// Props
|
|
197
|
+
if (props.length) {
|
|
198
|
+
props.forEach(p => {
|
|
199
|
+
ctx.fillStyle = '#999';
|
|
200
|
+
ctx.font = `12px "${font}"`;
|
|
201
|
+
ctx.fillText(p.l, leftX, leftY);
|
|
202
|
+
ctx.fillStyle = '#333';
|
|
203
|
+
ctx.font = `bold 14px "${font}"`;
|
|
204
|
+
let v = p.v;
|
|
205
|
+
if (v.length > 20)
|
|
206
|
+
v = v.substring(0, 18) + '...';
|
|
207
|
+
ctx.fillText(v, leftX, leftY + 16);
|
|
208
|
+
leftY += 38;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// 3. Right Column (Description)
|
|
212
|
+
const rightX = cx + leftColW + 20;
|
|
213
|
+
let rightY = dy;
|
|
214
|
+
ctx.fillStyle = '#333';
|
|
215
|
+
ctx.font = `bold 20px "${font}"`;
|
|
216
|
+
ctx.fillText('简介', rightX, rightY);
|
|
217
|
+
ctx.fillStyle = '#3498db';
|
|
218
|
+
ctx.fillRect(rightX, rightY + 25, 30, 4);
|
|
219
|
+
rightY += 40;
|
|
220
|
+
ctx.fillStyle = '#555';
|
|
221
|
+
ctx.font = `16px "${font}"`;
|
|
222
|
+
(0, rendering_1.wrapText)(ctx, desc, rightX, rightY, rightColW, 26, 30, true);
|
|
223
|
+
// 更新 dy 到主内容下方
|
|
224
|
+
dy += mainH + gap;
|
|
225
|
+
// 4. Relations (Bottom)
|
|
226
|
+
if (relations.length) {
|
|
227
|
+
// 分割线
|
|
228
|
+
ctx.strokeStyle = '#eee';
|
|
229
|
+
ctx.lineWidth = 1;
|
|
230
|
+
ctx.beginPath();
|
|
231
|
+
ctx.moveTo(cx, dy);
|
|
232
|
+
ctx.lineTo(cx + contentW, dy);
|
|
233
|
+
ctx.stroke();
|
|
234
|
+
dy += 20;
|
|
235
|
+
ctx.fillStyle = '#333';
|
|
236
|
+
ctx.font = `bold 18px "${font}"`;
|
|
237
|
+
ctx.fillText('相关物品', cx, dy);
|
|
238
|
+
let rx = cx + 90;
|
|
239
|
+
const rIconSize = 32;
|
|
240
|
+
for (const r of relations) {
|
|
241
|
+
try {
|
|
242
|
+
const img = await (0, rendering_1.loadImage)(r.i);
|
|
243
|
+
ctx.drawImage(img, rx, dy - 5, rIconSize, rIconSize);
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
ctx.fillStyle = '#eee';
|
|
247
|
+
ctx.fillRect(rx, dy - 5, rIconSize, rIconSize);
|
|
248
|
+
}
|
|
249
|
+
// 简单显示名字 tooltip 效果不太好做,这里只画图标,或者简单的名字
|
|
250
|
+
// 为了美观,这里只画图标,名字太长会乱
|
|
251
|
+
// ctx.fillStyle = '#666'; ctx.font = `10px "${font}"`;
|
|
252
|
+
// ctx.fillText(r.n.substring(0, 5), rx, dy + 40);
|
|
253
|
+
rx += rIconSize + 15;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Footer
|
|
257
|
+
ctx.fillStyle = '#aaa';
|
|
258
|
+
ctx.font = `12px "${font}"`;
|
|
259
|
+
ctx.textAlign = 'center';
|
|
260
|
+
ctx.fillText('mcmod.cn | Powered by Koishi', width / 2, totalH - 15);
|
|
261
|
+
return await canvas.encode('png');
|
|
262
|
+
}
|