koishi-plugin-monetary-bourse 2.0.3-Alpha.12 → 2.1.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.
@@ -1,560 +1,625 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <style>
6
- :root {
7
- --bg-color: #0c0f15;
8
- --card-bg: #161b22;
9
- --item-bg: #1f242e;
10
- --text-primary: #f0f3f5;
11
- --text-secondary: #8b949e;
12
- --border-color: #30363d;
13
-
14
- /* 语义色 */
15
- --buy-color: #f87171; /* 涨/买 */
16
- --sell-color: #4ade80; /* 跌/卖 */
17
- --buy-bg: rgba(248, 113, 113, 0.15);
18
- --sell-bg: rgba(74, 222, 128, 0.15);
19
-
20
- --pending-color: #eab308; /* 黄色 */
21
- --pending-bg: rgba(234, 179, 8, 0.15);
22
-
23
- --accent-color: #58a6ff;
24
- }
25
-
26
- * { margin: 0; padding: 0; box-sizing: border-box; }
27
-
28
- body {
29
- /* 修改 3: 增大页面外间距 (32px -> 64px) */
30
- padding: 64px;
31
- /* 还原回原本的字体组合 */
32
- font-family: 'Roboto Mono', 'Trebuchet MS', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
33
- background-color: var(--bg-color);
34
- width: 960px;
35
- height: 720px;
36
- color: var(--text-primary);
37
- display: flex;
38
- justify-content: center;
39
- align-items: center;
40
- }
41
-
42
- .card {
43
- background-color: var(--card-bg);
44
- width: 100%;
45
- height: 100%;
46
- padding: 32px 40px; /* 内部左右间距也稍微增加一点 */
47
- border-radius: 16px;
48
- box-shadow: 0 32px 64px rgba(0,0,0,0.6), 0 0 0 1px var(--border-color);
49
- display: flex;
50
- flex-direction: column;
51
- position: relative;
52
- overflow: hidden;
53
- }
54
-
55
- /* --- 顶部区域:股票信息 + 价格 --- */
56
- .top-section {
57
- display: flex;
58
- justify-content: space-between;
59
- align-items: center;
60
- margin-bottom: 24px;
61
- }
62
-
63
- .stock-group {
64
- display: flex;
65
- align-items: center;
66
- gap: 16px;
67
- }
68
-
69
- .stock-icon {
70
- width: 64px; /* 稍微加大 */
71
- height: 64px;
72
- border-radius: 14px;
73
- background: linear-gradient(135deg, #2b313a, #1f242e);
74
- border: 1px solid var(--border-color);
75
- display: flex;
76
- align-items: center;
77
- justify-content: center;
78
- font-size: 32px;
79
- color: var(--text-primary);
80
- }
81
-
82
- .stock-meta {
83
- display: flex;
84
- flex-direction: column;
85
- justify-content: center;
86
- gap: 8px;
87
- }
88
-
89
- .stock-name {
90
- font-size: 28px; /* 加大 */
91
- font-weight: 700;
92
- line-height: 1.1;
93
- }
94
-
95
- /* 徽标容器 */
96
- .badges-container {
97
- display: flex;
98
- gap: 8px;
99
- }
100
-
101
- .badge {
102
- display: inline-flex;
103
- align-items: center;
104
- padding: 6px 12px; /* 修改 1: 加大内边距 */
105
- border-radius: 6px;
106
- font-size: 14px; /* 修改 1: 增大字号 (13px -> 14px) */
107
- font-weight: 700;
108
- text-transform: uppercase;
109
- letter-spacing: 0.5px;
110
- line-height: 1;
111
- }
112
-
113
- .badge.buy { background: var(--buy-bg); color: var(--buy-color); border: 1px solid rgba(248, 113, 113, 0.2); }
114
- .badge.sell { background: var(--sell-bg); color: var(--sell-color); border: 1px solid rgba(74, 222, 128, 0.2); }
115
-
116
- .badge.pending {
117
- background: var(--pending-bg);
118
- color: var(--pending-color);
119
- border: 1px solid rgba(234, 179, 8, 0.2);
120
- }
121
- .badge.pending::before {
122
- content: '';
123
- display: inline-block;
124
- width: 6px;
125
- height: 6px;
126
- border-radius: 50%;
127
- background-color: currentColor;
128
- margin-right: 6px;
129
- animation: pulse 2s infinite;
130
- }
131
- @keyframes pulse {
132
- 0% { opacity: 1; }
133
- 50% { opacity: 0.4; }
134
- 100% { opacity: 1; }
135
- }
136
-
137
- .price-group {
138
- text-align: right;
139
- }
140
-
141
- .main-price {
142
- font-size: 56px; /* 加大 */
143
- font-weight: 700;
144
- letter-spacing: -2px;
145
- line-height: 1;
146
- margin-bottom: 8px;
147
- /* 修改 2: 强制使用数字字体 */
148
- font-family: 'Roboto Mono', monospace;
149
- }
150
- .main-price.buy { color: var(--buy-color); }
151
- .main-price.sell { color: var(--sell-color); }
152
-
153
- .sub-detail {
154
- font-size: 16px; /* 修改 1: 增大字号 (15px -> 16px) */
155
- color: var(--text-secondary);
156
- display: flex;
157
- align-items: center;
158
- justify-content: flex-end;
159
- gap: 16px;
160
- }
161
- .sub-detail span {
162
- display: inline-flex;
163
- align-items: center;
164
- gap: 6px;
165
- }
166
- .val-highlight {
167
- color: var(--text-primary);
168
- font-weight: 600;
169
- /* 修改 2: 强制使用数字字体 */
170
- font-family: 'Roboto Mono', monospace;
171
- }
172
-
173
- /* --- 图表区域 --- */
174
- .chart-wrapper {
175
- flex: 1;
176
- width: 100%;
177
- min-height: 0;
178
- margin-bottom: 32px; /* 增加底部间距 */
179
- border-radius: 12px;
180
- background: rgba(0,0,0,0.2);
181
- border: 1px solid var(--border-color);
182
- position: relative;
183
- }
184
-
185
- canvas {
186
- display: block;
187
- width: 100%;
188
- height: 100%;
189
- }
190
-
191
- /* --- 底部数据卡片 --- */
192
- .stats-grid {
193
- display: grid;
194
- grid-template-columns: 1fr 1fr 1fr;
195
- gap: 20px;
196
- height: 120px; /* 增加高度以容纳更大的字体 */
197
- }
198
-
199
- .stat-item {
200
- background: var(--item-bg);
201
- border: 1px solid var(--border-color);
202
- border-radius: 12px;
203
- padding: 20px 28px;
204
- display: flex;
205
- flex-direction: column;
206
- justify-content: center;
207
- position: relative;
208
- }
209
-
210
- .stat-item.highlight { background: rgba(88, 166, 255, 0.05); border-color: rgba(88, 166, 255, 0.2); }
211
- .stat-item.profit { background: rgba(248, 113, 113, 0.05); border-color: rgba(248, 113, 113, 0.2); }
212
- .stat-item.loss { background: rgba(74, 222, 128, 0.05); border-color: rgba(74, 222, 128, 0.2); }
213
-
214
- .stat-label {
215
- font-size: 14px; /* 修改 1: 增大字号 (13px -> 14px) */
216
- text-transform: uppercase;
217
- color: var(--text-secondary);
218
- letter-spacing: 0.5px;
219
- margin-bottom: 10px;
220
- font-weight: 600;
221
- }
222
-
223
- .stat-value {
224
- font-size: 28px; /* 加大 */
225
- font-weight: 700;
226
- color: var(--text-primary);
227
- /* 修改 2: 强制使用数字字体 */
228
- font-family: 'Roboto Mono', monospace;
229
- }
230
-
231
- .stat-value.profit { color: var(--buy-color); }
232
- .stat-value.loss { color: var(--sell-color); }
233
-
234
- .stat-sub {
235
- font-size: 14px; /* 修改 1: 增大字号 */
236
- color: var(--text-secondary);
237
- opacity: 0.8;
238
- margin-top: 6px;
239
- /* 百分比也属于数字,用一下 Roboto Mono 更好看 */
240
- font-family: 'Roboto Mono', sans-serif;
241
- }
242
-
243
- /* 底部时间戳 */
244
- .footer-meta {
245
- position: absolute;
246
- bottom: 0px;
247
- right: 0px;
248
- display: flex;
249
- gap: 16px;
250
- align-items: center;
251
- }
252
-
253
- .time-tag {
254
- font-size: 12px;
255
- color: var(--text-secondary);
256
- padding: 6px 8px;
257
- border-radius: 6px;
258
- font-family: 'Roboto Mono', sans-serif; /* 包含时间数字 */
259
- }
260
-
261
- </style>
262
- </head>
263
- <body>
264
- <div class="card">
265
- <!-- 顶部信息栏 -->
266
- <div class="top-section">
267
- <div class="stock-group">
268
- <div class="stock-icon">⚡</div>
269
- <div class="stock-meta">
270
- <div class="stock-name" id="stock-name">--</div>
271
- <!-- 这里会动态插入徽标 -->
272
- <div class="badges-container" id="badges-container"></div>
273
- </div>
274
- </div>
275
-
276
- <div class="price-group">
277
- <div class="main-price" id="trade-price">--</div>
278
- <div class="sub-detail">
279
- <span>数量 <b class="val-highlight" id="trade-amount">--</b></span>
280
- <span style="opacity: 0.2">|</span>
281
- <span>总额 <b class="val-highlight" id="trade-total">--</b></span>
282
- </div>
283
- </div>
284
- </div>
285
-
286
- <!-- 图表区域 -->
287
- <div class="chart-wrapper" id="chart-container">
288
- <canvas id="chart"></canvas>
289
- </div>
290
-
291
- <!-- 底部数据 -->
292
- <div class="stats-grid" id="stats-grid">
293
- <!-- JS填充 -->
294
- </div>
295
-
296
- <!-- 仅用于显示时间,不干扰布局 -->
297
- <div class="footer-meta">
298
- <div class="time-tag" id="trade-time">--</div>
299
- </div>
300
- </div>
301
-
302
- <!-- 数据源 -->
303
- <script type="application/json" id="data-source">
304
- {{DATA}}
305
- </script>
306
-
307
- <script>
308
- // 模拟数据用于测试
309
- /*
310
- const TEST_DATA = {
311
- tradeType: 'buy',
312
- stockName: 'NVDA · NVIDIA Corp',
313
- status: 'pending',
314
- pendingMinutes: 5.5,
315
- pendingEndTime: '14:35',
316
- tradePrice: 485.20,
317
- amount: 10,
318
- totalCost: 4852.00,
319
- tradeTime: '2023-10-27 10:30:05',
320
- newHolding: 150,
321
- currency: 'USD',
322
- prices: [480, 482, 481, 483, 485, 484, 486, 485.2, 487, 488, 486],
323
- timestamps: [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000],
324
- tradeIndex: 7
325
- };
326
- */
327
-
328
- let DATA;
329
- try {
330
- DATA = JSON.parse(document.getElementById('data-source').textContent);
331
- } catch (e) {
332
- DATA = typeof TEST_DATA !== 'undefined' ? TEST_DATA : {};
333
- }
334
-
335
- const isBuy = DATA.tradeType === 'buy';
336
- const status = DATA.status || 'settled';
337
- const isPending = status === 'pending';
338
-
339
- const colors = {
340
- buy: '#f87171',
341
- sell: '#4ade80',
342
- pending: '#eab308',
343
- grid: '#30363d',
344
- text: '#f0f3f5',
345
- textDim: '#8b949e'
346
- };
347
-
348
- const mainColor = isBuy ? colors.buy : colors.sell;
349
-
350
- // --- 1. 渲染顶部基本信息 ---
351
- document.getElementById('stock-name').textContent = DATA.stockName || 'Unknown Stock';
352
- document.getElementById('trade-price').textContent = DATA.tradePrice.toFixed(2);
353
- document.getElementById('trade-price').className = `main-price ${DATA.tradeType}`;
354
- document.getElementById('trade-amount').textContent = DATA.amount;
355
- document.getElementById('trade-total').textContent = DATA.totalCost.toFixed(2);
356
- document.getElementById('trade-time').textContent = (isPending ? '下单时间 ' : '交易时间 ') + DATA.tradeTime;
357
-
358
- // --- 2. 徽标渲染 ---
359
- const badgesContainer = document.getElementById('badges-container');
360
- badgesContainer.innerHTML = '';
361
-
362
- // (A) 交易类型徽标
363
- const typeBadge = document.createElement('div');
364
- typeBadge.className = `badge ${DATA.tradeType}`;
365
- typeBadge.innerHTML = isBuy ? 'BUY / 买入' : 'SELL / 卖出';
366
- badgesContainer.appendChild(typeBadge);
367
-
368
- // (B) 状态徽标
369
- if (isPending) {
370
- const pendingBadge = document.createElement('div');
371
- pendingBadge.className = 'badge pending';
372
- const pendingText = DATA.pendingEndTime
373
- ? `PENDING 预计 ${DATA.pendingEndTime} 完成`
374
- : 'PENDING 挂单处理中';
375
- pendingBadge.innerHTML = pendingText;
376
- badgesContainer.appendChild(pendingBadge);
377
- }
378
-
379
- // --- 3. 底部数据卡片渲染 ---
380
- const statsGrid = document.getElementById('stats-grid');
381
- if (isBuy) {
382
- statsGrid.innerHTML = `
383
- <div class="stat-item highlight">
384
- <div class="stat-label">Current Holdings 持仓</div>
385
- <div class="stat-value">${DATA.newHolding}</div>
386
- <div class="stat-sub">股</div>
387
- </div>
388
- <div class="stat-item">
389
- <div class="stat-label">Avg Price 均价</div>
390
- <div class="stat-value">${DATA.tradePrice.toFixed(2)}</div>
391
- <div class="stat-sub">${DATA.currency}</div>
392
- </div>
393
- <div class="stat-item">
394
- <div class="stat-label">Total Value 市值</div>
395
- <div class="stat-value">${(DATA.newHolding * DATA.tradePrice).toFixed(2)}</div>
396
- <div class="stat-sub">${DATA.currency}</div>
397
- </div>
398
- `;
399
- } else {
400
- const profit = DATA.profit;
401
- const isProfit = profit >= 0;
402
- const profitClass = isProfit ? 'profit' : 'loss';
403
- const profitSign = isProfit ? '+' : '';
404
-
405
- statsGrid.innerHTML = `
406
- <div class="stat-item highlight">
407
- <div class="stat-label">Revenue 卖出金额</div>
408
- <div class="stat-value">${DATA.totalCost.toFixed(2)}</div>
409
- <div class="stat-sub">${DATA.currency}</div>
410
- </div>
411
- <div class="stat-item">
412
- <div class="stat-label">Cost Basis 买入成本</div>
413
- <div class="stat-value">${DATA.avgBuyPrice ? DATA.avgBuyPrice.toFixed(2) : '--'}</div>
414
- <div class="stat-sub">每股</div>
415
- </div>
416
- <div class="stat-item ${profitClass}">
417
- <div class="stat-label">P/L 盈亏</div>
418
- <div class="stat-value ${profitClass}">${profit !== null ? profitSign + profit.toFixed(2) : '--'}</div>
419
- <div class="stat-sub">${profit !== null ? profitSign + DATA.profitPercent.toFixed(2) + '%' : ''}</div>
420
- </div>
421
- `;
422
- }
423
-
424
-
425
- // --- 4. 图表绘制 ---
426
- const container = document.getElementById('chart-container');
427
- const canvas = document.getElementById('chart');
428
- const rect = container.getBoundingClientRect();
429
- const dpr = window.devicePixelRatio || 2;
430
-
431
- canvas.width = rect.width * dpr;
432
- canvas.height = rect.height * dpr;
433
-
434
- const ctx = canvas.getContext('2d');
435
- ctx.scale(dpr, dpr);
436
-
437
- const W = rect.width;
438
- const H = rect.height;
439
- const pad = { top: 30, bottom: 30, left: 10, right: 60 };
440
-
441
- if (DATA.prices && DATA.prices.length > 0) {
442
- const prices = DATA.prices;
443
- const minP = Math.min(...prices);
444
- const maxP = Math.max(...prices);
445
- const rangeP = maxP - minP || 1;
446
-
447
- const yMin = minP - rangeP * 0.2;
448
- const yMax = maxP + rangeP * 0.2;
449
- const yRange = yMax - yMin;
450
-
451
- const stepX = (W - pad.left - pad.right) / (prices.length - 1);
452
-
453
- const getX = i => pad.left + i * stepX;
454
- const getY = p => H - pad.bottom - ((p - yMin) / yRange) * (H - pad.top - pad.bottom);
455
-
456
- // (A) 绘制网格线
457
- ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
458
- ctx.lineWidth = 1;
459
- const gridLines = 4;
460
- for (let i = 0; i <= gridLines; i++) {
461
- const y = H - pad.bottom - (i / gridLines) * (H - pad.top - pad.bottom);
462
- ctx.beginPath();
463
- ctx.moveTo(pad.left, y);
464
- ctx.lineTo(W - pad.right, y);
465
- ctx.stroke();
466
-
467
- // 价格标签 (字体也同步调整为 Roboto Mono 且加大)
468
- const val = yMin + (i / gridLines) * yRange;
469
- ctx.fillStyle = colors.textDim;
470
- // 修改 1 & 2: 增大字号到 14px 并使用 Roboto Mono
471
- ctx.font = '500 14px "Roboto Mono", monospace';
472
- ctx.textAlign = 'left';
473
- ctx.fillText(val.toFixed(2), W - pad.right + 8, y + 4);
474
- }
475
-
476
- // (B) 绘制面积图
477
- const points = prices.map((p, i) => ({ x: getX(i), y: getY(p) }));
478
-
479
- ctx.beginPath();
480
- ctx.moveTo(points[0].x, H - pad.bottom);
481
- points.forEach(p => ctx.lineTo(p.x, p.y));
482
- ctx.lineTo(points[points.length-1].x, H - pad.bottom);
483
- ctx.closePath();
484
-
485
- const gradient = ctx.createLinearGradient(0, pad.top, 0, H - pad.bottom);
486
- gradient.addColorStop(0, isBuy ? 'rgba(248, 113, 113, 0.25)' : 'rgba(74, 222, 128, 0.25)');
487
- gradient.addColorStop(1, 'rgba(0,0,0,0)');
488
- ctx.fillStyle = gradient;
489
- ctx.fill();
490
-
491
- // (C) 绘制折线
492
- ctx.beginPath();
493
- ctx.moveTo(points[0].x, points[0].y);
494
- for (let i = 0; i < points.length - 1; i++) {
495
- const xc = (points[i].x + points[i + 1].x) / 2;
496
- const yc = (points[i].y + points[i + 1].y) / 2;
497
- ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
498
- }
499
- ctx.lineTo(points[points.length-1].x, points[points.length-1].y);
500
-
501
- ctx.strokeStyle = mainColor;
502
- ctx.lineWidth = 2.5;
503
- ctx.lineCap = 'round';
504
- ctx.stroke();
505
-
506
- // (D) 绘制交易点
507
- const tIdx = DATA.tradeIndex !== undefined ? DATA.tradeIndex : points.length - 1;
508
- const tp = points[tIdx];
509
-
510
- ctx.beginPath();
511
- ctx.setLineDash([4, 4]);
512
- ctx.strokeStyle = 'rgba(255,255,255,0.4)';
513
- ctx.lineWidth = 1;
514
- ctx.moveTo(tp.x, pad.top);
515
- ctx.lineTo(tp.x, H - pad.bottom);
516
- ctx.stroke();
517
- ctx.setLineDash([]);
518
-
519
- const glow = ctx.createRadialGradient(tp.x, tp.y, 0, tp.x, tp.y, 16);
520
- glow.addColorStop(0, isBuy ? 'rgba(248, 113, 113, 0.6)' : 'rgba(74, 222, 128, 0.6)');
521
- glow.addColorStop(1, 'rgba(0,0,0,0)');
522
- ctx.fillStyle = glow;
523
- ctx.beginPath();
524
- ctx.arc(tp.x, tp.y, 16, 0, Math.PI * 2);
525
- ctx.fill();
526
-
527
- ctx.fillStyle = '#fff';
528
- ctx.beginPath();
529
- ctx.arc(tp.x, tp.y, 4, 0, Math.PI * 2);
530
- ctx.fill();
531
-
532
- // 价格标签 Tag (同步字体和大小)
533
- const tagText = DATA.tradePrice.toFixed(2);
534
- // 修改 1 & 2: 增大字号到 14px 并使用 Roboto Mono
535
- ctx.font = 'bold 14px "Roboto Mono", monospace';
536
- const tm = ctx.measureText(tagText);
537
- const tagW = tm.width + 16;
538
- const tagH = 26; // 稍微增加高度适应大字号
539
- const tagX = tp.x - tagW / 2;
540
- const tagY = tp.y - 36;
541
-
542
- ctx.fillStyle = mainColor;
543
- ctx.beginPath();
544
- ctx.roundRect(tagX, tagY, tagW, tagH, 4);
545
- ctx.fill();
546
-
547
- ctx.beginPath();
548
- ctx.moveTo(tp.x - 4, tagY + tagH);
549
- ctx.lineTo(tp.x + 4, tagY + tagH);
550
- ctx.lineTo(tp.x, tagY + tagH + 4);
551
- ctx.fill();
552
-
553
- ctx.fillStyle = '#1e232e';
554
- ctx.textAlign = 'center';
555
- ctx.fillText(tagText, tp.x, tagY + 17); // 微调垂直对齐
556
- }
557
-
558
- </script>
559
- </body>
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <style>
7
+ :root {
8
+ --bg-color: #0c0f15;
9
+ --card-bg: #161b22;
10
+ --item-bg: #1f242e;
11
+ --text-primary: #f0f3f5;
12
+ --text-secondary: #8b949e;
13
+ --border-color: #30363d;
14
+
15
+ /* 语义色 */
16
+ --buy-color: #f87171;
17
+ /* 涨/买 */
18
+ --sell-color: #4ade80;
19
+ /* 跌/卖 */
20
+ --buy-bg: rgba(248, 113, 113, 0.15);
21
+ --sell-bg: rgba(74, 222, 128, 0.15);
22
+
23
+ --pending-color: #eab308;
24
+ /* 黄色 */
25
+ --pending-bg: rgba(234, 179, 8, 0.15);
26
+
27
+ --accent-color: #58a6ff;
28
+ }
29
+
30
+ * {
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ body {
37
+ /* 修改 3: 增大页面外间距 (32px -> 64px) */
38
+ padding: 64px;
39
+ /* 统一字体组合 */
40
+ font-family: 'Roboto Mono', 'Trebuchet MS', 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
41
+ background-color: var(--bg-color);
42
+ width: 960px;
43
+ height: 720px;
44
+ color: var(--text-primary);
45
+ display: flex;
46
+ justify-content: center;
47
+ align-items: center;
48
+ }
49
+
50
+ .card {
51
+ background-color: var(--card-bg);
52
+ width: 100%;
53
+ height: 100%;
54
+ padding: 32px 40px;
55
+ /* 内部左右间距也稍微增加一点 */
56
+ border-radius: 16px;
57
+ box-shadow: 0 32px 64px rgba(0, 0, 0, 0.6), 0 0 0 1px var(--border-color);
58
+ display: flex;
59
+ flex-direction: column;
60
+ position: relative;
61
+ overflow: hidden;
62
+ }
63
+
64
+ /* --- 顶部区域:股票信息 + 价格 --- */
65
+ .top-section {
66
+ display: flex;
67
+ justify-content: space-between;
68
+ align-items: center;
69
+ margin-bottom: 24px;
70
+ }
71
+
72
+ .stock-group {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 16px;
76
+ }
77
+
78
+ .stock-icon {
79
+ width: 64px;
80
+ /* 稍微加大 */
81
+ height: 64px;
82
+ border-radius: 14px;
83
+ background: linear-gradient(135deg, #2b313a, #1f242e);
84
+ border: 1px solid var(--border-color);
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ font-size: 32px;
89
+ color: var(--text-primary);
90
+ }
91
+
92
+ .stock-meta {
93
+ display: flex;
94
+ flex-direction: column;
95
+ justify-content: center;
96
+ gap: 8px;
97
+ }
98
+
99
+ .stock-name {
100
+ font-size: 28px;
101
+ /* 加大 */
102
+ font-weight: 700;
103
+ line-height: 1.1;
104
+ }
105
+
106
+ /* 徽标容器 */
107
+ .badges-container {
108
+ display: flex;
109
+ gap: 8px;
110
+ }
111
+
112
+ .badge {
113
+ display: inline-flex;
114
+ align-items: center;
115
+ padding: 6px 12px;
116
+ /* 修改 1: 加大内边距 */
117
+ border-radius: 6px;
118
+ font-size: 14px;
119
+ /* 修改 1: 增大字号 (13px -> 14px) */
120
+ font-weight: 700;
121
+ text-transform: uppercase;
122
+ letter-spacing: 0.5px;
123
+ line-height: 1;
124
+ }
125
+
126
+ .badge.buy {
127
+ background: var(--buy-bg);
128
+ color: var(--buy-color);
129
+ border: 1px solid rgba(248, 113, 113, 0.2);
130
+ }
131
+
132
+ .badge.sell {
133
+ background: var(--sell-bg);
134
+ color: var(--sell-color);
135
+ border: 1px solid rgba(74, 222, 128, 0.2);
136
+ }
137
+
138
+ .badge.pending {
139
+ background: var(--pending-bg);
140
+ color: var(--pending-color);
141
+ border: 1px solid rgba(234, 179, 8, 0.2);
142
+ }
143
+
144
+ .badge.pending::before {
145
+ content: '';
146
+ display: inline-block;
147
+ width: 6px;
148
+ height: 6px;
149
+ border-radius: 50%;
150
+ background-color: currentColor;
151
+ margin-right: 6px;
152
+ animation: pulse 2s infinite;
153
+ }
154
+
155
+ @keyframes pulse {
156
+ 0% {
157
+ opacity: 1;
158
+ }
159
+
160
+ 50% {
161
+ opacity: 0.4;
162
+ }
163
+
164
+ 100% {
165
+ opacity: 1;
166
+ }
167
+ }
168
+
169
+ .price-group {
170
+ text-align: right;
171
+ }
172
+
173
+ .main-price {
174
+ font-size: 56px;
175
+ /* 加大 */
176
+ font-weight: 500;
177
+ letter-spacing: -2px;
178
+ line-height: 1;
179
+ margin-bottom: 8px;
180
+ /* 修改 2: 强制使用数字字体 */
181
+ font-family: 'Roboto Mono';
182
+ }
183
+
184
+ .main-price.buy {
185
+ color: var(--buy-color);
186
+ }
187
+
188
+ .main-price.sell {
189
+ color: var(--sell-color);
190
+ }
191
+
192
+ .sub-detail {
193
+ font-size: 16px;
194
+ /* 修改 1: 增大字号 (15px -> 16px) */
195
+ color: var(--text-secondary);
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: flex-end;
199
+ gap: 16px;
200
+ }
201
+
202
+ .sub-detail span {
203
+ display: inline-flex;
204
+ align-items: center;
205
+ gap: 6px;
206
+ }
207
+
208
+ .val-highlight {
209
+ color: var(--text-primary);
210
+ font-weight: 600;
211
+ /* 修改 2: 强制使用数字字体 */
212
+ font-family: 'Roboto Mono', monospace;
213
+ }
214
+
215
+ /* --- 图表区域 --- */
216
+ .chart-wrapper {
217
+ flex: 1;
218
+ width: 100%;
219
+ min-height: 0;
220
+ margin-bottom: 32px;
221
+ /* 增加底部间距 */
222
+ border-radius: 12px;
223
+ background: rgba(0, 0, 0, 0.2);
224
+ border: 1px solid var(--border-color);
225
+ position: relative;
226
+ }
227
+
228
+ canvas {
229
+ display: block;
230
+ width: 100%;
231
+ height: 100%;
232
+ }
233
+
234
+ /* --- 底部数据卡片 --- */
235
+ .stats-grid {
236
+ display: grid;
237
+ grid-template-columns: 1fr 1fr 1fr;
238
+ gap: 20px;
239
+ height: 120px;
240
+ /* 增加高度以容纳更大的字体 */
241
+ }
242
+
243
+ .stat-item {
244
+ background: var(--item-bg);
245
+ border: 1px solid var(--border-color);
246
+ border-radius: 12px;
247
+ padding: 20px 28px;
248
+ display: flex;
249
+ flex-direction: column;
250
+ justify-content: center;
251
+ position: relative;
252
+ }
253
+
254
+ .stat-item.highlight {
255
+ background: rgba(88, 166, 255, 0.05);
256
+ border-color: rgba(88, 166, 255, 0.2);
257
+ }
258
+
259
+ .stat-item.profit {
260
+ background: rgba(248, 113, 113, 0.05);
261
+ border-color: rgba(248, 113, 113, 0.2);
262
+ }
263
+
264
+ .stat-item.loss {
265
+ background: rgba(74, 222, 128, 0.05);
266
+ border-color: rgba(74, 222, 128, 0.2);
267
+ }
268
+
269
+ .stat-label {
270
+ font-size: 14px;
271
+ /* 修改 1: 增大字号 (13px -> 14px) */
272
+ text-transform: uppercase;
273
+ color: var(--text-secondary);
274
+ letter-spacing: 0.5px;
275
+ margin-bottom: 10px;
276
+ font-weight: 600;
277
+ }
278
+
279
+ .stat-value {
280
+ font-size: 28px;
281
+ /* 加大 */
282
+ font-weight: 700;
283
+ color: var(--text-primary);
284
+ /* 修改 2: 强制使用数字字体 */
285
+ font-family: 'Roboto Mono', monospace;
286
+ }
287
+
288
+ .stat-value.profit {
289
+ color: var(--buy-color);
290
+ }
291
+
292
+ .stat-value.loss {
293
+ color: var(--sell-color);
294
+ }
295
+
296
+ .stat-sub {
297
+ font-size: 14px;
298
+ /* 修改 1: 增大字号 */
299
+ color: var(--text-secondary);
300
+ opacity: 0.8;
301
+ margin-top: 6px;
302
+ /* 百分比也属于数字,用一下 Roboto Mono 更好看 */
303
+ font-family: 'Roboto Mono', sans-serif;
304
+ }
305
+
306
+ /* 底部时间戳 */
307
+ .footer-meta {
308
+ position: absolute;
309
+ bottom: 0px;
310
+ right: 0px;
311
+ display: flex;
312
+ gap: 16px;
313
+ align-items: center;
314
+ }
315
+
316
+ .time-tag {
317
+ font-size: 12px;
318
+ color: var(--text-secondary);
319
+ padding: 6px 8px;
320
+ border-radius: 6px;
321
+ font-family: 'Roboto Mono', sans-serif;
322
+ /* 包含时间数字 */
323
+ }
324
+ </style>
325
+ </head>
326
+
327
+ <body>
328
+ <div class="card">
329
+ <!-- 顶部信息栏 -->
330
+ <div class="top-section">
331
+ <div class="stock-group">
332
+ <div class="stock-icon">⚡</div>
333
+ <div class="stock-meta">
334
+ <div class="stock-name" id="stock-name">--</div>
335
+ <!-- 这里会动态插入徽标 -->
336
+ <div class="badges-container" id="badges-container"></div>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="price-group">
341
+ <div class="main-price" id="trade-price"></div>
342
+ <div class="sub-detail">
343
+ <span>数量 <b class="val-highlight" id="trade-amount"></b></span>
344
+ <span style="opacity: 0.2">|</span>
345
+ <span>总额 <b class="val-highlight" id="trade-total"></b></span>
346
+ </div>
347
+ </div>
348
+ </div>
349
+
350
+ <!-- 图表区域 -->
351
+ <div class="chart-wrapper" id="chart-container">
352
+ <canvas id="chart"></canvas>
353
+ </div>
354
+
355
+ <!-- 底部数据 -->
356
+ <div class="stats-grid" id="stats-grid">
357
+ <!-- JS填充 -->
358
+ </div>
359
+
360
+ <!-- 仅用于显示时间,不干扰布局 -->
361
+ <div class="footer-meta">
362
+ <div class="time-tag" id="trade-time">--</div>
363
+ </div>
364
+ </div>
365
+
366
+ <!-- 数据源 -->
367
+ <script type="application/json" id="data-source">
368
+ {{DATA}}
369
+ </script>
370
+
371
+ <script>
372
+ // 模拟数据用于测试
373
+ /*
374
+ const TEST_DATA = {
375
+ tradeType: 'buy',
376
+ stockName: 'NVDA · NVIDIA Corp',
377
+ status: 'pending',
378
+ pendingMinutes: 5.5,
379
+ pendingEndTime: '14:35',
380
+ tradePrice: 485.20,
381
+ amount: 10,
382
+ totalCost: 4852.00,
383
+ tradeTime: '2023-10-27 10:30:05',
384
+ newHolding: 150,
385
+ currency: 'USD',
386
+ prices: [480, 482, 481, 483, 485, 484, 486, 485.2, 487, 488, 486],
387
+ timestamps: [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000],
388
+ tradeIndex: 7
389
+ };
390
+ */
391
+
392
+ let DATA;
393
+ try {
394
+ DATA = JSON.parse(document.getElementById('data-source').textContent);
395
+ } catch (e) {
396
+ DATA = typeof TEST_DATA !== 'undefined' ? TEST_DATA : {};
397
+ }
398
+
399
+ const isBuy = DATA.tradeType === 'buy';
400
+ const status = DATA.status || 'settled';
401
+ const isPending = status === 'pending';
402
+
403
+ const colors = {
404
+ buy: '#f87171',
405
+ sell: '#4ade80',
406
+ pending: '#eab308',
407
+ grid: '#30363d',
408
+ text: '#f0f3f5',
409
+ textDim: '#8b949e'
410
+ };
411
+
412
+ const mainColor = isBuy ? colors.buy : colors.sell;
413
+
414
+ // --- 1. 渲染顶部基本信息 ---
415
+ document.getElementById('stock-name').textContent = DATA.stockName || 'Unknown Stock';
416
+ document.getElementById('trade-price').textContent = DATA.tradePrice.toFixed(2);
417
+ document.getElementById('trade-price').className = `main-price ${DATA.tradeType}`;
418
+ document.getElementById('trade-amount').textContent = DATA.amount;
419
+ document.getElementById('trade-total').textContent = DATA.totalCost.toFixed(2);
420
+ document.getElementById('trade-time').textContent = (isPending ? '下单时间 ' : '交易时间 ') + DATA.tradeTime;
421
+
422
+ // --- 2. 徽标渲染 ---
423
+ const badgesContainer = document.getElementById('badges-container');
424
+ badgesContainer.innerHTML = '';
425
+
426
+ // (A) 交易类型徽标
427
+ const typeBadge = document.createElement('div');
428
+ typeBadge.className = `badge ${DATA.tradeType}`;
429
+ typeBadge.innerHTML = isBuy ? 'BUY / 买入' : 'SELL / 卖出';
430
+ badgesContainer.appendChild(typeBadge);
431
+
432
+ // (B) 状态徽标
433
+ if (isPending) {
434
+ const pendingBadge = document.createElement('div');
435
+ pendingBadge.className = 'badge pending';
436
+ const pendingText = DATA.pendingEndTime
437
+ ? `PENDING 预计 ${DATA.pendingEndTime} 完成`
438
+ : 'PENDING 挂单处理中';
439
+ pendingBadge.innerHTML = pendingText;
440
+ badgesContainer.appendChild(pendingBadge);
441
+ }
442
+
443
+ // --- 3. 底部数据卡片渲染 ---
444
+ const statsGrid = document.getElementById('stats-grid');
445
+ if (isBuy) {
446
+ statsGrid.innerHTML = `
447
+ <div class="stat-item highlight">
448
+ <div class="stat-label">Current Holdings 持仓</div>
449
+ <div class="stat-value">${DATA.newHolding}</div>
450
+ <div class="stat-sub">股</div>
451
+ </div>
452
+ <div class="stat-item">
453
+ <div class="stat-label">Avg Price 均价</div>
454
+ <div class="stat-value">${DATA.tradePrice.toFixed(2)}</div>
455
+ <div class="stat-sub">${DATA.currency}</div>
456
+ </div>
457
+ <div class="stat-item">
458
+ <div class="stat-label">Total Value 市值</div>
459
+ <div class="stat-value">${(DATA.newHolding * DATA.tradePrice).toFixed(2)}</div>
460
+ <div class="stat-sub">${DATA.currency}</div>
461
+ </div>
462
+ `;
463
+ } else {
464
+ const profit = DATA.profit;
465
+ const isProfit = profit >= 0;
466
+ const profitClass = isProfit ? 'profit' : 'loss';
467
+ const profitSign = isProfit ? '+' : '';
468
+
469
+ statsGrid.innerHTML = `
470
+ <div class="stat-item highlight">
471
+ <div class="stat-label">Revenue 卖出金额</div>
472
+ <div class="stat-value">${DATA.totalCost.toFixed(2)}</div>
473
+ <div class="stat-sub">${DATA.currency}</div>
474
+ </div>
475
+ <div class="stat-item">
476
+ <div class="stat-label">Cost Basis 买入成本</div>
477
+ <div class="stat-value">${DATA.avgBuyPrice ? DATA.avgBuyPrice.toFixed(2) : '--'}</div>
478
+ <div class="stat-sub">每股</div>
479
+ </div>
480
+ <div class="stat-item ${profitClass}">
481
+ <div class="stat-label">P/L 盈亏</div>
482
+ <div class="stat-value ${profitClass}">${profit !== null ? profitSign + profit.toFixed(2) : '--'}</div>
483
+ <div class="stat-sub">${profit !== null ? profitSign + DATA.profitPercent.toFixed(2) + '%' : ''}</div>
484
+ </div>
485
+ `;
486
+ }
487
+
488
+
489
+ // --- 4. 图表绘制 ---
490
+ const container = document.getElementById('chart-container');
491
+ const canvas = document.getElementById('chart');
492
+ const rect = container.getBoundingClientRect();
493
+ const dpr = window.devicePixelRatio || 2;
494
+
495
+ canvas.width = rect.width * dpr;
496
+ canvas.height = rect.height * dpr;
497
+
498
+ const ctx = canvas.getContext('2d');
499
+ ctx.scale(dpr, dpr);
500
+
501
+ const W = rect.width;
502
+ const H = rect.height;
503
+ const pad = { top: 30, bottom: 30, left: 10, right: 60 };
504
+
505
+ if (DATA.prices && DATA.prices.length > 0) {
506
+ const prices = DATA.prices;
507
+ const minP = Math.min(...prices);
508
+ const maxP = Math.max(...prices);
509
+ const rangeP = maxP - minP || 1;
510
+
511
+ const yMin = minP - rangeP * 0.2;
512
+ const yMax = maxP + rangeP * 0.2;
513
+ const yRange = yMax - yMin;
514
+
515
+ const stepX = (W - pad.left - pad.right) / (prices.length - 1);
516
+
517
+ const getX = i => pad.left + i * stepX;
518
+ const getY = p => H - pad.bottom - ((p - yMin) / yRange) * (H - pad.top - pad.bottom);
519
+
520
+ // (A) 绘制网格线
521
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
522
+ ctx.lineWidth = 1;
523
+ const gridLines = 4;
524
+ for (let i = 0; i <= gridLines; i++) {
525
+ const y = H - pad.bottom - (i / gridLines) * (H - pad.top - pad.bottom);
526
+ ctx.beginPath();
527
+ ctx.moveTo(pad.left, y);
528
+ ctx.lineTo(W - pad.right, y);
529
+ ctx.stroke();
530
+
531
+ // 价格标签 (字体也同步调整为 Roboto Mono 且加大)
532
+ const val = yMin + (i / gridLines) * yRange;
533
+ ctx.fillStyle = colors.textDim;
534
+ // 修改 1 & 2: 增大字号到 14px 并使用 Roboto Mono
535
+ ctx.font = '500 14px "Roboto Mono", monospace';
536
+ ctx.textAlign = 'left';
537
+ ctx.fillText(val.toFixed(2), W - pad.right + 8, y + 4);
538
+ }
539
+
540
+ // (B) 绘制面积图
541
+ const points = prices.map((p, i) => ({ x: getX(i), y: getY(p) }));
542
+
543
+ ctx.beginPath();
544
+ ctx.moveTo(points[0].x, H - pad.bottom);
545
+ points.forEach(p => ctx.lineTo(p.x, p.y));
546
+ ctx.lineTo(points[points.length - 1].x, H - pad.bottom);
547
+ ctx.closePath();
548
+
549
+ const gradient = ctx.createLinearGradient(0, pad.top, 0, H - pad.bottom);
550
+ gradient.addColorStop(0, isBuy ? 'rgba(248, 113, 113, 0.25)' : 'rgba(74, 222, 128, 0.25)');
551
+ gradient.addColorStop(1, 'rgba(0,0,0,0)');
552
+ ctx.fillStyle = gradient;
553
+ ctx.fill();
554
+
555
+ // (C) 绘制折线
556
+ ctx.beginPath();
557
+ ctx.moveTo(points[0].x, points[0].y);
558
+ for (let i = 0; i < points.length - 1; i++) {
559
+ const xc = (points[i].x + points[i + 1].x) / 2;
560
+ const yc = (points[i].y + points[i + 1].y) / 2;
561
+ ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc);
562
+ }
563
+ ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
564
+
565
+ ctx.strokeStyle = mainColor;
566
+ ctx.lineWidth = 2.5;
567
+ ctx.lineCap = 'round';
568
+ ctx.stroke();
569
+
570
+ // (D) 绘制交易点
571
+ const tIdx = DATA.tradeIndex !== undefined ? DATA.tradeIndex : points.length - 1;
572
+ const tp = points[tIdx];
573
+
574
+ ctx.beginPath();
575
+ ctx.setLineDash([4, 4]);
576
+ ctx.strokeStyle = 'rgba(255,255,255,0.4)';
577
+ ctx.lineWidth = 1;
578
+ ctx.moveTo(tp.x, pad.top);
579
+ ctx.lineTo(tp.x, H - pad.bottom);
580
+ ctx.stroke();
581
+ ctx.setLineDash([]);
582
+
583
+ const glow = ctx.createRadialGradient(tp.x, tp.y, 0, tp.x, tp.y, 16);
584
+ glow.addColorStop(0, isBuy ? 'rgba(248, 113, 113, 0.6)' : 'rgba(74, 222, 128, 0.6)');
585
+ glow.addColorStop(1, 'rgba(0,0,0,0)');
586
+ ctx.fillStyle = glow;
587
+ ctx.beginPath();
588
+ ctx.arc(tp.x, tp.y, 16, 0, Math.PI * 2);
589
+ ctx.fill();
590
+
591
+ ctx.fillStyle = '#fff';
592
+ ctx.beginPath();
593
+ ctx.arc(tp.x, tp.y, 4, 0, Math.PI * 2);
594
+ ctx.fill();
595
+
596
+ // 价格标签 Tag (同步字体和大小)
597
+ const tagText = DATA.tradePrice.toFixed(2);
598
+ // 修改 1 & 2: 增大字号到 14px 并使用 Roboto Mono
599
+ ctx.font = 'bold 14px "Roboto Mono", monospace';
600
+ const tm = ctx.measureText(tagText);
601
+ const tagW = tm.width + 16;
602
+ const tagH = 26; // 稍微增加高度适应大字号
603
+ const tagX = tp.x - tagW / 2;
604
+ const tagY = tp.y - 36;
605
+
606
+ ctx.fillStyle = mainColor;
607
+ ctx.beginPath();
608
+ ctx.roundRect(tagX, tagY, tagW, tagH, 4);
609
+ ctx.fill();
610
+
611
+ ctx.beginPath();
612
+ ctx.moveTo(tp.x - 4, tagY + tagH);
613
+ ctx.lineTo(tp.x + 4, tagY + tagH);
614
+ ctx.lineTo(tp.x, tagY + tagH + 4);
615
+ ctx.fill();
616
+
617
+ ctx.fillStyle = '#1e232e';
618
+ ctx.textAlign = 'center';
619
+ ctx.fillText(tagText, tp.x, tagY + 17); // 微调垂直对齐
620
+ }
621
+
622
+ </script>
623
+ </body>
624
+
560
625
  </html>