koishi-plugin-hg-sign 1.0.1

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/lib/card.js ADDED
@@ -0,0 +1,1122 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ export class CardGenerator {
5
+ ctx;
6
+ constructor(ctx) {
7
+ this.ctx = ctx;
8
+ }
9
+ /**
10
+ * 生成终末地角色卡片
11
+ */
12
+ async generateEndfieldCard(data) {
13
+ const page = await this.ctx.puppeteer.page();
14
+ try {
15
+ // 计算需要的图片高度(根据角色数量)
16
+ const base = data.detail.base;
17
+ const chars = data.detail.chars || [];
18
+ const charCount = Math.min(chars.length, 12); // 最多显示12个角色
19
+ // 2列网格,计算行数
20
+ const rows = Math.ceil(charCount / 2);
21
+ // 头部(350) + 标题(50) + 行高(140 * rows) + Padding
22
+ // Original calculation: 350 + (rows * 140)
23
+ const estimatedHeight = 350 + (rows * 140);
24
+ const pageHeight = Math.max(700, Math.min(estimatedHeight, 2000));
25
+ // 增加宽度以容纳侧边栏 (原 700 -> 850)
26
+ await page.setViewport({ width: 850, height: pageHeight, deviceScaleFactor: 2 });
27
+ const html = this.getCardHTML(data);
28
+ await page.setContent(html, { waitUntil: 'networkidle0' });
29
+ // 等待图片加载
30
+ await new Promise(resolve => setTimeout(resolve, 1000));
31
+ const card = await page.$('.container');
32
+ if (!card) {
33
+ throw new Error('未找到卡片元素 .container');
34
+ }
35
+ const screenshot = await card.screenshot({
36
+ type: 'png',
37
+ encoding: 'binary'
38
+ });
39
+ return screenshot;
40
+ }
41
+ finally {
42
+ await page.close();
43
+ }
44
+ }
45
+ /**
46
+ * 生成签到报告卡片
47
+ */
48
+ async generateSignReportCard(results) {
49
+ const page = await this.ctx.puppeteer.page();
50
+ try {
51
+ const rows = results.length;
52
+ // Card height dynamic calculation
53
+ const pageHeight = Math.max(500, 200 + (rows * 100)); // Base + 100px per row
54
+ await page.setViewport({ width: 850, height: pageHeight, deviceScaleFactor: 2 });
55
+ const html = this.getSignReportHTML(results);
56
+ await page.setContent(html, { waitUntil: 'networkidle0' });
57
+ const card = await page.$('.container');
58
+ if (!card)
59
+ throw new Error('Container not found');
60
+ return await card.screenshot({ type: 'png', encoding: 'binary' });
61
+ }
62
+ finally {
63
+ await page.close();
64
+ }
65
+ }
66
+ getSignReportHTML(results) {
67
+ const date = new Date().toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-');
68
+ const listItems = results.map(item => `
69
+ <div class="result-row ${item.success ? 'status-success' : 'status-fail'}">
70
+ <div class="row-left">
71
+ <div class="game-tag">${item.game}</div>
72
+ <div class="char-name">${item.char}</div>
73
+ </div>
74
+ <div class="row-right">
75
+ <div class="status-text">${item.status}</div>
76
+ ${item.awards ? `<div class="awards-text">🎁 ${item.awards}</div>` : ''}
77
+ </div>
78
+ </div>
79
+ `).join('');
80
+ return `
81
+ <!DOCTYPE html>
82
+ <html>
83
+ <head>
84
+ <meta charset="UTF-8">
85
+ <link href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,400;0,600;0,800;0,900;1,800&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
86
+ <style>
87
+ :root {
88
+ --ef-yellow: #FFE600;
89
+ --ef-black: #000000;
90
+ --ef-dark: #1F1F1F;
91
+ --ef-gray: #F5F5F7;
92
+ --ef-line: #E5E5E5;
93
+ --ef-green: #00A651;
94
+ --ef-red: #E60000;
95
+ }
96
+
97
+ * { margin: 0; padding: 0; box-sizing: border-box; }
98
+
99
+ body {
100
+ background: transparent;
101
+ font-family: 'Barlow', 'Noto Sans SC', sans-serif;
102
+ color: var(--ef-black);
103
+ -webkit-font-smoothing: antialiased;
104
+ }
105
+
106
+ .container {
107
+ width: 850px;
108
+ background: #FFFFFF;
109
+ display: flex;
110
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
111
+ overflow: hidden;
112
+ position: relative;
113
+ min-height: 500px; /* match viewport min */
114
+ }
115
+
116
+ /* 侧边栏 */
117
+ .sidebar {
118
+ width: 80px;
119
+ background: var(--ef-yellow);
120
+ flex-shrink: 0;
121
+ position: relative;
122
+ display: flex;
123
+ flex-direction: column;
124
+ align-items: center;
125
+ padding-top: 30px;
126
+ padding-bottom: 30px;
127
+ border-right: 1px solid rgba(0,0,0,0.1);
128
+ }
129
+
130
+ .sidebar-deco-text {
131
+ writing-mode: vertical-lr;
132
+ transform: rotate(180deg);
133
+ font-family: 'Barlow';
134
+ font-weight: 800;
135
+ font-size: 24px;
136
+ letter-spacing: 4px;
137
+ margin-top: auto;
138
+ margin-bottom: auto;
139
+ color: var(--ef-black);
140
+ }
141
+
142
+ .content {
143
+ flex: 1;
144
+ padding: 40px 60px;
145
+ display: flex;
146
+ flex-direction: column;
147
+ }
148
+
149
+ .header {
150
+ margin-bottom: 40px;
151
+ border-bottom: 4px solid var(--ef-black);
152
+ padding-bottom: 20px;
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: flex-end;
156
+ }
157
+
158
+ .title-group h1 {
159
+ font-family: 'Barlow';
160
+ font-weight: 900;
161
+ font-size: 48px;
162
+ line-height: 1;
163
+ font-style: italic;
164
+ text-transform: uppercase;
165
+ margin-bottom: 5px;
166
+ }
167
+
168
+ .title-group h2 {
169
+ font-size: 20px;
170
+ font-weight: 700;
171
+ color: #666;
172
+ letter-spacing: 2px;
173
+ }
174
+
175
+ .date-badge {
176
+ font-family: 'Barlow';
177
+ font-weight: 700;
178
+ font-size: 24px;
179
+ background: var(--ef-black);
180
+ color: var(--ef-yellow);
181
+ padding: 5px 15px;
182
+ border-radius: 4px; /* Optional rounded */
183
+ }
184
+
185
+ .report-list {
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 20px;
189
+ }
190
+
191
+ .result-row {
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: center;
195
+ background: var(--ef-gray);
196
+ padding: 20px 25px;
197
+ border-left: 6px solid #999;
198
+ }
199
+
200
+ .result-row.status-success { border-left-color: var(--ef-green); }
201
+ .result-row.status-fail { border-left-color: var(--ef-red); }
202
+
203
+ .row-left {
204
+ display: flex;
205
+ flex-direction: column;
206
+ gap: 5px;
207
+ }
208
+
209
+ .game-tag {
210
+ font-size: 14px;
211
+ font-weight: 700;
212
+ background: #DDD;
213
+ color: #555;
214
+ padding: 2px 8px;
215
+ width: fit-content;
216
+ border-radius: 2px;
217
+ }
218
+
219
+ .char-name {
220
+ font-size: 24px;
221
+ font-weight: 700;
222
+ }
223
+
224
+ .row-right {
225
+ text-align: right;
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 5px;
229
+ }
230
+
231
+ .status-text {
232
+ font-size: 18px;
233
+ font-weight: 700;
234
+ }
235
+
236
+ .status-success .status-text { color: var(--ef-green); }
237
+ .status-fail .status-text { color: var(--ef-red); }
238
+
239
+ .awards-text {
240
+ font-size: 14px;
241
+ color: #666;
242
+ }
243
+ </style>
244
+ </head>
245
+ <body>
246
+ <div class="container">
247
+ <div class="sidebar">
248
+ <div class="sidebar-deco-text">HYPERGRYPH // SKLAND</div>
249
+ </div>
250
+ <div class="content">
251
+ <div class="header">
252
+ <div class="title-group">
253
+ <h1>Daily Report</h1>
254
+ <h2>森空岛签到报告</h2>
255
+ </div>
256
+ <div class="date-badge">${date}</div>
257
+ </div>
258
+
259
+ <div class="report-list">
260
+ ${listItems}
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </body>
265
+ </html>
266
+ `;
267
+ }
268
+ /*
269
+ Requires appList array of names, and potential file paths for icons depending on original implementation
270
+ Original used: generateLoginQRCard(qrFilePath, appList)
271
+ We will modify this to take the QR code data URL directly or generate it.
272
+ Since we used qrcode lib in index.ts, we can pass the data url directly.
273
+ */
274
+ async generateLoginQRCard(qrDataUrl, appList = ['森空岛', '明日方舟', '明日方舟:终末地']) {
275
+ const page = await this.ctx.puppeteer.page();
276
+ try {
277
+ // App Icons - Read from local assets just like original
278
+ const appIcons = [];
279
+ const iconMap = {
280
+ '明日方舟': 'arknights.png',
281
+ '森空岛': 'skland.png',
282
+ '明日方舟:终末地': 'endfield.png'
283
+ };
284
+ // Locate assets directory
285
+ // We try to find it relative to the current file (src or lib)
286
+ // This handles both dev (src/../assets) and prod (lib/../assets)
287
+ let assetsDir = '';
288
+ try {
289
+ // ESM mode
290
+ const __filename = fileURLToPath(import.meta.url);
291
+ assetsDir = path.resolve(path.dirname(__filename), '../assets/apps');
292
+ }
293
+ catch (e) {
294
+ // CJS fallback (if compiled to CJS, import.meta might be undefined or transpiled differently, but we are targeting ESM now)
295
+ // @ts-ignore
296
+ if (typeof __dirname !== 'undefined') {
297
+ // @ts-ignore
298
+ assetsDir = path.resolve(__dirname, '../assets/apps');
299
+ }
300
+ }
301
+ // Final Fallback: assume running from project root
302
+ if (!fs.existsSync(assetsDir)) {
303
+ assetsDir = path.resolve(this.ctx.baseDir, 'assets/apps');
304
+ }
305
+ this.ctx.logger('hg-sign').debug(`Using assets dir: ${assetsDir}`);
306
+ for (const appName of appList) {
307
+ let iconUrl = '';
308
+ const filename = iconMap[appName];
309
+ if (filename) {
310
+ const iconPath = path.join(assetsDir, filename);
311
+ if (fs.existsSync(iconPath)) {
312
+ const iconBitmap = fs.readFileSync(iconPath);
313
+ const iconBase64 = Buffer.from(iconBitmap).toString('base64');
314
+ iconUrl = `data:image/png;base64,${iconBase64}`;
315
+ }
316
+ }
317
+ appIcons.push({ name: appName, icon: iconUrl });
318
+ }
319
+ const pageHeight = 600;
320
+ await page.setViewport({ width: 850, height: pageHeight, deviceScaleFactor: 2 });
321
+ const html = this.getLoginCardHTML(qrDataUrl, appIcons);
322
+ await page.setContent(html, { waitUntil: 'networkidle0' });
323
+ const card = await page.$('.container');
324
+ if (!card)
325
+ throw new Error('Container not found');
326
+ return await card.screenshot({ type: 'png', encoding: 'binary' });
327
+ }
328
+ finally {
329
+ await page.close();
330
+ }
331
+ }
332
+ getLoginCardHTML(qrDataUrl, appIcons) {
333
+ const appListHTML = appIcons.map(app => `
334
+ <div class="app-tag">
335
+ ${app.icon ? `<img src="${app.icon}" class="app-icon">` : ''}
336
+ <span class="app-name">${app.name}</span>
337
+ </div>
338
+ `).join('');
339
+ return `
340
+ <!DOCTYPE html>
341
+ <html>
342
+ <head>
343
+ <meta charset="UTF-8">
344
+ <link href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,400;0,600;0,800;0,900;1,800&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
345
+ <style>
346
+ :root {
347
+ --ef-yellow: #FFE600;
348
+ --ef-black: #000000;
349
+ --ef-dark: #1F1F1F;
350
+ --ef-gray: #F5F5F7;
351
+ --ef-line: #DCDCDC;
352
+ --ef-text-gray: #5F5F5F;
353
+ }
354
+
355
+ * { margin: 0; padding: 0; box-sizing: border-box; }
356
+
357
+ body {
358
+ background: transparent;
359
+ font-family: 'Barlow', 'Noto Sans SC', sans-serif;
360
+ color: var(--ef-black);
361
+ -webkit-font-smoothing: antialiased;
362
+ }
363
+
364
+ .container {
365
+ width: 850px;
366
+ height: 600px;
367
+ background: #FFFFFF;
368
+ display: flex;
369
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
370
+ overflow: hidden;
371
+ position: relative;
372
+ }
373
+
374
+ /* 侧边栏 */
375
+ .sidebar {
376
+ width: 80px;
377
+ background: var(--ef-yellow);
378
+ flex-shrink: 0;
379
+ position: relative;
380
+ display: flex;
381
+ flex-direction: column;
382
+ align-items: center;
383
+ padding-top: 30px;
384
+ padding-bottom: 30px;
385
+ border-right: 1px solid rgba(0,0,0,0.1);
386
+ }
387
+
388
+ .sidebar-logo {
389
+ width: 40px;
390
+ height: 40px;
391
+ border: 2px solid var(--ef-black);
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ font-weight: 900;
396
+ font-size: 24px;
397
+ margin-bottom: 60px;
398
+ }
399
+
400
+ .sidebar-text {
401
+ writing-mode: vertical-rl;
402
+ text-orientation: mixed;
403
+ font-weight: 900;
404
+ font-size: 32px;
405
+ letter-spacing: 4px;
406
+ text-transform: uppercase;
407
+ transform: rotate(180deg);
408
+ margin-top: auto;
409
+ }
410
+
411
+ /* 主内容 */
412
+ .main-content {
413
+ flex: 1;
414
+ padding: 40px;
415
+ background: #FCFCFC;
416
+ background-image: radial-gradient(#E0E0E0 1px, transparent 1px);
417
+ background-size: 20px 20px;
418
+ position: relative;
419
+ display: flex;
420
+ flex-direction: column;
421
+ }
422
+
423
+ /* 标题区 */
424
+ .header {
425
+ margin-bottom: 40px;
426
+ border-bottom: 4px solid var(--ef-black);
427
+ padding-bottom: 10px;
428
+ display: flex;
429
+ justify-content: space-between;
430
+ align-items: flex-end;
431
+ }
432
+ .big-title {
433
+ font-size: 48px;
434
+ font-weight: 900;
435
+ font-style: italic;
436
+ line-height: 0.8;
437
+ letter-spacing: -1px;
438
+ text-transform: uppercase;
439
+ }
440
+ .sub-title {
441
+ font-size: 14px;
442
+ font-weight: 700;
443
+ color: var(--ef-yellow);
444
+ background: var(--ef-black);
445
+ padding: 2px 8px;
446
+ display: inline-block;
447
+ margin-bottom: 10px;
448
+ }
449
+
450
+ /* 登录内容区 - Flex 布局调整 */
451
+ .login-box {
452
+ display: flex;
453
+ gap: 40px;
454
+ flex: 1; /* 占据剩余高度 */
455
+ align-items: flex-start; /* 顶部对齐 */
456
+ }
457
+
458
+ /* 二维码容器 */
459
+ .qr-section {
460
+ width: 300px;
461
+ display: flex;
462
+ flex-direction: column;
463
+ gap: 10px;
464
+ }
465
+
466
+ .qr-frame {
467
+ width: 300px;
468
+ height: 300px;
469
+ border: 2px solid var(--ef-black);
470
+ padding: 10px;
471
+ position: relative;
472
+ background: #FFF;
473
+ }
474
+ /* 扫描框装饰角 */
475
+ .qr-frame::before {
476
+ content: ''; position: absolute; top: -2px; left: -2px; width: 20px; height: 20px;
477
+ border-top: 4px solid var(--ef-yellow); border-left: 4px solid var(--ef-yellow);
478
+ }
479
+ .qr-frame::after {
480
+ content: ''; position: absolute; bottom: -2px; right: -2px; width: 20px; height: 20px;
481
+ border-bottom: 4px solid var(--ef-yellow); border-right: 4px solid var(--ef-yellow);
482
+ }
483
+
484
+ .qr-img {
485
+ width: 100%; height: 100%;
486
+ image-rendering: pixelated;
487
+ }
488
+
489
+ .qr-info {
490
+ font-size: 12px;
491
+ color: var(--ef-text-gray);
492
+ font-weight: 600;
493
+ text-align: center;
494
+ font-family: monospace;
495
+ }
496
+
497
+ /* 说明文字 */
498
+ .instr-section {
499
+ flex: 1;
500
+ display: flex;
501
+ flex-direction: column;
502
+ justify-content: space-between; /* 上下分布 */
503
+ height: 100%; /* 填满父容器高度 */
504
+ max-height: 330px; /* 限制高度与左侧二维码区域差不多 */
505
+ }
506
+
507
+ .instr-title {
508
+ font-size: 36px;
509
+ font-weight: 900;
510
+ margin-bottom: 25px;
511
+ font-style: italic;
512
+ letter-spacing: -1px;
513
+ line-height: 1;
514
+ }
515
+
516
+ .app-list {
517
+ display: flex;
518
+ flex-direction: column;
519
+ gap: 24px;
520
+ }
521
+
522
+ .app-tag {
523
+ display: flex;
524
+ align-items: center;
525
+ gap: 16px;
526
+ border-left: 5px solid var(--ef-yellow);
527
+ padding-left: 16px;
528
+ }
529
+
530
+ .app-icon {
531
+ width: 56px;
532
+ height: 56px;
533
+ object-fit: contain;
534
+ border-radius: 8px;
535
+ background: #f0f0f0;
536
+ }
537
+
538
+ .app-name {
539
+ font-size: 28px;
540
+ font-weight: 800;
541
+ font-family: 'Barlow', 'Noto Sans SC', monospace;
542
+ color: var(--ef-black);
543
+ text-transform: uppercase;
544
+ }
545
+
546
+ .expire-alert {
547
+ color: #FF4444;
548
+ font-size: 12px;
549
+ font-weight: 700;
550
+ border: 1px solid #FF4444;
551
+ padding: 8px;
552
+ background: rgba(255, 68, 68, 0.05);
553
+ /* 去除 margin-top: auto,改为由父容器控制 */
554
+ }
555
+
556
+ /* 装饰元素 - 改为 absolute 定位到底部,不参与 Flex 流 */
557
+ .deco-line {
558
+ position: absolute;
559
+ bottom: 40px;
560
+ right: 40px;
561
+ font-size: 10px;
562
+ font-weight: 700;
563
+ color: #CCC;
564
+ text-align: right;
565
+ border-top: 1px solid #EEE;
566
+ padding-top: 4px;
567
+ }
568
+
569
+ </style>
570
+ </head>
571
+ <body>
572
+ <div class="container">
573
+ <div class="sidebar">
574
+ <div class="sidebar-logo">E</div>
575
+ <div class="sidebar-text">LOGIN</div>
576
+ </div>
577
+
578
+ <div class="main-content">
579
+ <div class="header">
580
+ <div>
581
+ <div class="sub-title">HG_PASSPORT // AUTHENTICATION</div>
582
+ <div class="big-title">ACCESS<br>TERMINAL</div>
583
+ </div>
584
+ <div style="text-align:right">
585
+ <div style="font-weight:900; font-size:24px;">NO.001</div>
586
+ <div style="font-size:10px; color:#999">SECURE CONNECTION</div>
587
+ </div>
588
+ </div>
589
+
590
+ <div class="login-box">
591
+ <div class="qr-section">
592
+ <div class="qr-frame">
593
+ <img class="qr-img" src="${qrDataUrl}" />
594
+ </div>
595
+ <div class="qr-info">
596
+ SCAN QR CODE TO LOGIN<br>
597
+ // WAITING_FOR_INPUT...
598
+ </div>
599
+ </div>
600
+
601
+ <div class="instr-section">
602
+ <div>
603
+ <div class="instr-title">SUPPORTED APPS</div>
604
+ <div class="app-list">
605
+ ${appListHTML}
606
+ </div>
607
+ </div>
608
+
609
+ <div class="expire-alert">
610
+ ! SESSION EXPIRES IN 2 MINUTES
611
+ </div>
612
+ </div>
613
+ </div>
614
+
615
+ <div class="deco-line">
616
+ HYPERGRYPH NETWORK TECHNOLOGY // HG-SIGN TERMINAL
617
+ </div>
618
+ </div>
619
+ </div>
620
+ </body>
621
+ </html>
622
+ `;
623
+ }
624
+ getCardHTML(data) {
625
+ const base = data.detail.base;
626
+ const chars = data.detail.chars || [];
627
+ // 角色列表 HTML
628
+ let charsHTML = '';
629
+ for (let i = 0; i < Math.min(chars.length, 12); i++) {
630
+ const char = chars[i];
631
+ const cd = char.charData;
632
+ // 职业映射 (Strictly copied from source)
633
+ const professionMap = {
634
+ 'WARRIOR': '近卫', 'SNIPER': '狙击', 'TANK': '重装', 'MEDIC': '医疗',
635
+ 'SUPPORT': '辅助', 'CASTER': '术师', 'SPECIAL': '特种', 'PIONEER': '先锋'
636
+ };
637
+ // 英文职业名,用于装饰
638
+ const profEn = (cd.profession?.value || 'OPERATOR').toUpperCase();
639
+ const weapon = char.weapon;
640
+ // 技能 (仅显示数量或最高等级)
641
+ const skillCount = cd.skills?.length || 0;
642
+ const maxSkillLv = cd.skills?.reduce((max, s) => {
643
+ const lv = char.userSkills?.[s.id]?.level || 0;
644
+ return lv > max ? lv : max;
645
+ }, 0) || 0;
646
+ // 装备简略
647
+ let equipCount = 0;
648
+ if (char.bodyEquip)
649
+ equipCount++;
650
+ if (char.armEquip)
651
+ equipCount++;
652
+ if (char.firstAccessory)
653
+ equipCount++;
654
+ if (char.secondAccessory)
655
+ equipCount++;
656
+ // Rarity check
657
+ const rarity = cd.rarity?.value || 3;
658
+ charsHTML += `
659
+ <div class="char-card">
660
+ <div class="char-id-deco">OP-${String(i + 1).padStart(2, '0')}</div>
661
+ <div class="char-main">
662
+ <div class="char-avatar-box">
663
+ <img src="${cd.avatarSqUrl}" onerror="this.src='https://via.placeholder.com/80'" alt="${cd.name}">
664
+ <div class="rarity-bar r-${rarity}"></div>
665
+ </div>
666
+ <div class="char-info">
667
+ <div class="char-name-row">
668
+ <span class="cn-name">${cd.name}</span>
669
+ <span class="prof-en">${profEn}</span>
670
+ </div>
671
+ <div class="char-metrics">
672
+ <div class="metric">
673
+ <div class="m-val">Lv.${char.level}</div>
674
+ <div class="m-lbl">LEVEL</div>
675
+ </div>
676
+ <div class="metric">
677
+ <div class="m-val">${char.potentialLevel || 0}</div>
678
+ <div class="m-lbl">POTENTIAL</div>
679
+ </div>
680
+ <div class="metric">
681
+ <div class="m-val">${weapon ? 'Lv.' + weapon.level : 'N/A'}</div>
682
+ <div class="m-lbl">WEAPON</div>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ <div class="char-footer">
688
+ <div class="tech-bits">
689
+ <span>SKILL_MAX: ${maxSkillLv}</span>
690
+ <span> // </span>
691
+ <span>EQUIP: ${equipCount}/4</span>
692
+ </div>
693
+ </div>
694
+ </div>
695
+ `;
696
+ }
697
+ return `
698
+ <!DOCTYPE html>
699
+ <html>
700
+ <head>
701
+ <meta charset="UTF-8">
702
+ <link href="https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,400;0,600;0,800;0,900;1,800&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
703
+ <style>
704
+ :root {
705
+ --ef-yellow: #FFE600; /* 更加鲜艳的荧光黄 */
706
+ --ef-black: #000000;
707
+ --ef-dark: #1F1F1F;
708
+ --ef-gray: #F5F5F7;
709
+ --ef-line: #DCDCDC;
710
+ --ef-text-gray: #5F5F5F;
711
+ }
712
+
713
+ * { margin: 0; padding: 0; box-sizing: border-box; }
714
+
715
+ body {
716
+ background: transparent;
717
+ font-family: 'Barlow', 'Noto Sans SC', sans-serif;
718
+ color: var(--ef-black);
719
+ -webkit-font-smoothing: antialiased;
720
+ }
721
+
722
+ .container {
723
+ width: 850px;
724
+ background: #FFFFFF;
725
+ display: flex;
726
+ min-height: 600px;
727
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
728
+ }
729
+
730
+ /* 左侧黄色侧边栏 styled like the reference image sidebar */
731
+ .sidebar {
732
+ width: 80px;
733
+ background: var(--ef-yellow);
734
+ flex-shrink: 0;
735
+ position: relative;
736
+ display: flex;
737
+ flex-direction: column;
738
+ align-items: center;
739
+ padding-top: 30px;
740
+ padding-bottom: 30px;
741
+ border-right: 1px solid rgba(0,0,0,0.1);
742
+ }
743
+
744
+ .sidebar-logo {
745
+ width: 40px;
746
+ height: 40px;
747
+ border: 2px solid var(--ef-black);
748
+ display: flex;
749
+ align-items: center;
750
+ justify-content: center;
751
+ font-weight: 900;
752
+ font-size: 24px;
753
+ margin-bottom: 60px;
754
+ }
755
+
756
+ /* 竖排文字 */
757
+ .sidebar-text {
758
+ writing-mode: vertical-rl;
759
+ text-orientation: mixed;
760
+ font-weight: 900;
761
+ font-size: 32px;
762
+ letter-spacing: 4px;
763
+ text-transform: uppercase;
764
+ transform: rotate(180deg);
765
+ margin-top: auto;
766
+ }
767
+
768
+ .sidebar-deco {
769
+ margin-top: 20px;
770
+ font-size: 10px;
771
+ font-weight: 700;
772
+ font-family: monospace;
773
+ opacity: 0.6;
774
+ writing-mode: vertical-rl;
775
+ text-orientation: mixed;
776
+ transform: rotate(180deg);
777
+ }
778
+
779
+ /* 右侧内容区 */
780
+ .main-content {
781
+ flex: 1;
782
+ padding: 40px;
783
+ background: #FCFCFC;
784
+ background-image: radial-gradient(#E0E0E0 1px, transparent 1px);
785
+ background-size: 20px 20px;
786
+ position: relative;
787
+ }
788
+
789
+ /* 顶部大标题区 */
790
+ .header {
791
+ margin-bottom: 40px;
792
+ position: relative;
793
+ }
794
+
795
+ .header-title-box {
796
+ border-bottom: 4px solid var(--ef-black);
797
+ padding-bottom: 10px;
798
+ margin-bottom: 20px;
799
+ display: flex;
800
+ justify-content: space-between;
801
+ align-items: flex-end;
802
+ }
803
+
804
+ .big-title {
805
+ font-size: 64px;
806
+ font-weight: 900;
807
+ font-style: italic;
808
+ line-height: 0.8;
809
+ letter-spacing: -2px;
810
+ text-transform: uppercase;
811
+ }
812
+
813
+ .header-meta {
814
+ text-align: right;
815
+ font-weight: 600;
816
+ font-size: 12px;
817
+ color: var(--ef-text-gray);
818
+ font-family: monospace;
819
+ }
820
+
821
+ /* 用户信息卡片 (黑底白字 Technical Style) */
822
+ .user-dossier {
823
+ background: var(--ef-black);
824
+ color: #fff;
825
+ padding: 25px;
826
+ display: flex;
827
+ justify-content: space-between;
828
+ align-items: center;
829
+ position: relative;
830
+ margin-bottom: 30px;
831
+ }
832
+ /* 装饰角标 */
833
+ .user-dossier::before {
834
+ content: '';
835
+ position: absolute;
836
+ top: 0; right: 0;
837
+ width: 0; height: 0;
838
+ border-style: solid;
839
+ border-width: 0 30px 30px 0;
840
+ border-color: transparent var(--ef-yellow) transparent transparent;
841
+ }
842
+
843
+ .user-info .role-label {
844
+ color: var(--ef-yellow);
845
+ font-size: 10px;
846
+ font-weight: 700;
847
+ letter-spacing: 2px;
848
+ margin-bottom: 5px;
849
+ display: block;
850
+ }
851
+ .user-info .name {
852
+ font-size: 36px;
853
+ font-weight: 700;
854
+ line-height: 1;
855
+ }
856
+ .user-info .uid {
857
+ color: #666;
858
+ font-family: monospace;
859
+ font-size: 14px;
860
+ margin-top: 5px;
861
+ }
862
+
863
+ .user-stats {
864
+ display: flex;
865
+ gap: 30px;
866
+ }
867
+ .stat-unit {
868
+ text-align: right;
869
+ }
870
+ .stat-unit .val {
871
+ font-size: 32px;
872
+ font-weight: 300;
873
+ font-family: 'Barlow', sans-serif;
874
+ color: var(--ef-yellow);
875
+ line-height: 1;
876
+ }
877
+ .stat-unit .lbl {
878
+ font-size: 10px;
879
+ text-transform: uppercase;
880
+ font-weight: 700;
881
+ color: #888;
882
+ margin-top: 5px;
883
+ }
884
+
885
+ /* 统计数据条 */
886
+ .data-strip {
887
+ display: flex;
888
+ background: white;
889
+ border: 1px solid var(--ef-line);
890
+ margin-bottom: 40px;
891
+ padding: 25px;
892
+ gap: 0;
893
+ justify-content: space-between;
894
+ }
895
+ .ds-item {
896
+ flex: 1;
897
+ border-left: 4px solid var(--ef-yellow);
898
+ padding-left: 20px;
899
+ margin-right: 20px;
900
+ }
901
+ .ds-item:last-child {
902
+ margin-right: 0;
903
+ }
904
+ .ds-lbl {
905
+ font-size: 12px;
906
+ font-weight: 700;
907
+ color: var(--ef-text-gray);
908
+ margin-bottom: 8px;
909
+ text-transform: uppercase;
910
+ letter-spacing: 1px;
911
+ }
912
+ .ds-val {
913
+ font-size: 32px;
914
+ font-weight: 800;
915
+ line-height: 1;
916
+ font-family: 'Barlow', sans-serif;
917
+ letter-spacing: -1px;
918
+ }
919
+
920
+ /* 角色区域标题 */
921
+ .section-separator {
922
+ display: flex;
923
+ align-items: center;
924
+ margin-bottom: 20px;
925
+ font-weight: 900;
926
+ font-size: 20px;
927
+ font-style: italic;
928
+ }
929
+ .section-separator::after {
930
+ content: '';
931
+ flex: 1;
932
+ height: 2px;
933
+ background: var(--ef-black);
934
+ margin-left: 15px;
935
+ }
936
+
937
+ /* 角色网格 */
938
+ .char-grid {
939
+ display: grid;
940
+ grid-template-columns: 1fr 1fr;
941
+ gap: 20px;
942
+ }
943
+
944
+ /* 角色卡片样式更新 */
945
+ .char-card {
946
+ background: #FFF;
947
+ border: 1px solid var(--ef-line);
948
+ padding: 15px;
949
+ position: relative;
950
+ box-shadow: 0 4px 0 rgba(0,0,0,0.05); /* 底部硬阴影 */
951
+ transition: transform 0.2s;
952
+ }
953
+
954
+ .char-id-deco {
955
+ position: absolute;
956
+ top: 10px;
957
+ right: 10px;
958
+ font-size: 10px;
959
+ font-weight: 900;
960
+ color: #E0E0E0;
961
+ z-index: 0;
962
+ }
963
+
964
+ .char-main {
965
+ display: flex;
966
+ gap: 15px;
967
+ position: relative;
968
+ z-index: 1;
969
+ }
970
+
971
+ .char-avatar-box {
972
+ width: 70px;
973
+ height: 70px;
974
+ flex-shrink: 0;
975
+ position: relative;
976
+ background: #333;
977
+ }
978
+ .char-avatar-box img {
979
+ width: 100%; height: 100%; object-fit: cover;
980
+ }
981
+ .rarity-bar {
982
+ position: absolute;
983
+ bottom: 0; left: 0; right: 0; height: 4px;
984
+ }
985
+ .r-6 { background: #FF7F27; }
986
+ .r-5 { background: #FFCF00; }
987
+ .r-4 { background: #999; }
988
+ .r-3 { background: #555; }
989
+
990
+ .char-info {
991
+ flex: 1;
992
+ display: flex;
993
+ flex-direction: column;
994
+ justify-content: space-between;
995
+ }
996
+
997
+ .char-name-row {
998
+ border-bottom: 1px solid #EEE;
999
+ padding-bottom: 5px;
1000
+ margin-bottom: 5px;
1001
+ }
1002
+ .cn-name { font-size: 18px; font-weight: 800; color: var(--ef-black); }
1003
+ .prof-en { font-size: 10px; font-weight: 700; color: var(--ef-yellow); background: var(--ef-black); padding: 1px 4px; float: right; }
1004
+
1005
+ .char-metrics {
1006
+ display: flex;
1007
+ justify-content: space-between;
1008
+ }
1009
+ .metric { text-align: left; }
1010
+ .m-val { font-size: 14px; font-weight: 700; font-family: 'Barlow'; }
1011
+ .m-lbl { font-size: 8px; font-weight: 600; color: #999; }
1012
+
1013
+ .char-footer {
1014
+ margin-top: 10px;
1015
+ padding-top: 8px;
1016
+ border-top: 1px dashed #DDD;
1017
+ }
1018
+ .tech-bits {
1019
+ font-family: monospace;
1020
+ font-size: 10px;
1021
+ color: #888;
1022
+ }
1023
+
1024
+ /* 底部 */
1025
+ .footer-info {
1026
+ margin-top: 40px;
1027
+ border-top: 2px solid var(--ef-black);
1028
+ padding-top: 10px;
1029
+ display: flex;
1030
+ justify-content: space-between;
1031
+ font-size: 10px;
1032
+ font-weight: 700;
1033
+ }
1034
+
1035
+ /* 装饰浮动元素 */
1036
+ .float-plus {
1037
+ position: absolute;
1038
+ font-weight: 300;
1039
+ color: #CCC;
1040
+ font-size: 20px;
1041
+ pointer-events: none;
1042
+ }
1043
+ </style>
1044
+ </head>
1045
+ <body>
1046
+ <div class="container">
1047
+ <!-- 左侧栏 -->
1048
+ <div class="sidebar">
1049
+ <div class="sidebar-logo">E</div>
1050
+ <div class="sidebar-text">ENDFIELD</div>
1051
+ <div class="sidebar-deco">// TERMINAL_ACCESS</div>
1052
+ </div>
1053
+
1054
+ <!-- 主内容 -->
1055
+ <div class="main-content">
1056
+ <!-- 装饰背景十字 -->
1057
+ <div class="float-plus" style="top: 20px; left: 20px;">+</div>
1058
+ <div class="float-plus" style="bottom: 20px; right: 20px;">+</div>
1059
+
1060
+ <div class="header">
1061
+ <div class="header-title-box">
1062
+ <div class="big-title">PERCEPTUAL<br>DATA</div>
1063
+ <div class="header-meta">
1064
+ SYS.VER 2.0.26<br>
1065
+ ${new Date().toLocaleDateString('en-GB').replace(/\//g, '.')}
1066
+ </div>
1067
+ </div>
1068
+
1069
+ <div class="user-dossier">
1070
+ <div class="user-info">
1071
+ <span class="role-label">// ADMINISTRATOR</span>
1072
+ <div class="name">${base.name}</div>
1073
+ <div class="uid">ID: ${base.roleId || base.uid}</div>
1074
+ </div>
1075
+ <div class="user-stats">
1076
+ <div class="stat-unit">
1077
+ <div class="val">${base.level}</div>
1078
+ <div class="lbl">访问等级</div>
1079
+ </div>
1080
+ <div class="stat-unit">
1081
+ <div class="val">${base.worldLevel}</div>
1082
+ <div class="lbl">世界等级</div>
1083
+ </div>
1084
+ </div>
1085
+ </div>
1086
+
1087
+ <div class="data-strip">
1088
+ <div class="ds-item">
1089
+ <div class="ds-lbl">总经验值</div>
1090
+ <div class="ds-val">${base.exp || 0}</div>
1091
+ </div>
1092
+ <div class="ds-item">
1093
+ <div class="ds-lbl">干员数量</div>
1094
+ <div class="ds-val">${base.charNum || chars.length}</div>
1095
+ </div>
1096
+ <div class="ds-item">
1097
+ <div class="ds-lbl">武器数量</div>
1098
+ <div class="ds-val">${base.weaponNum || 0}</div>
1099
+ </div>
1100
+ </div>
1101
+ </div>
1102
+
1103
+ <div class="section-separator">
1104
+ // DEPLOYMENT_SQUAD [${chars.length}]
1105
+ </div>
1106
+
1107
+ <div class="char-grid">
1108
+ ${charsHTML}
1109
+ </div>
1110
+
1111
+ <div class="footer-info">
1112
+ <span>HYPERGRYPH // ENDFIELD INDUSTRIES</span>
1113
+ <span>HG-SIGN PLUGIN</span>
1114
+ </div>
1115
+ </div>
1116
+ </div>
1117
+ </body>
1118
+ </html>
1119
+ `;
1120
+ }
1121
+ }
1122
+ //# sourceMappingURL=card.js.map