superxuu commited on
Commit
b396dad
·
1 Parent(s): e20b269

feat: Add share button to ReturnChartModal using html2canvas for image generation

Browse files
frontend/src/components/TradePanel.tsx CHANGED
@@ -6,10 +6,11 @@
6
  */
7
 
8
  import { useState, useEffect, useMemo, useRef } from 'react';
9
- import { TrendingUp, TrendingDown, Wallet, Package, RotateCcw, Eye, ChevronRight, Play, Pause, BarChart3, Trophy, X, TrendingUpIcon } from 'lucide-react';
10
  import { useGameStore, calculateReturnRate, calculateTotalAsset } from '@/store/gameStore';
11
  import { api } from '@/lib/api';
12
  import { createChart, ColorType, LineData, Time, LineStyle } from 'lightweight-charts';
 
13
 
14
  export default function TradePanel() {
15
  const [volume, setVolume] = useState(100);
@@ -439,11 +440,55 @@ interface ReturnChartModalProps {
439
 
440
  function ReturnChartModal({ onClose, allKlines, history, initialIndex, currentIndex, initialCapital, stats }: ReturnChartModalProps) {
441
  const chartContainerRef = useRef<HTMLDivElement>(null);
 
442
  const chartRef = useRef<any>(null);
443
  const [hs300Data, setHS300Data] = useState<{date: string, close: number}[]>([]);
444
  const [isLoading, setIsLoading] = useState(true);
 
445
 
446
- useEffect(() => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  const fetchHS300 = async () => {
448
  try {
449
  const visibleKlines = allKlines.slice(initialIndex, currentIndex + 1);
@@ -591,10 +636,20 @@ function ReturnChartModal({ onClose, allKlines, history, initialIndex, currentIn
591
 
592
  return (
593
  <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
594
- <div className="bg-[#1E212B] rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl border border-[#2A2D3C]">
595
  <div className="px-4 py-3 border-b border-[#2A2D3C] flex items-center justify-between">
596
  <h3 className="text-lg font-bold text-white flex items-center gap-2"><TrendingUpIcon size={18} className="text-purple-500" />收益率曲线</h3>
597
- <button onClick={onClose} className="w-8 h-8 flex items-center justify-center rounded-lg bg-[#2A2D3C] hover:bg-[#35394B] text-gray-400 hover:text-white transition-colors"><X size={18} /></button>
 
 
 
 
 
 
 
 
 
 
598
  </div>
599
  <div className="pt-3 pb-2 px-1 lg:px-2">
600
  {isLoading ? <div className="w-full h-[240px] bg-[#191B28] rounded-xl flex items-center justify-center text-gray-400">加载中...</div> : benchmarkData.length === 0 ? <div className="w-full h-[240px] bg-[#191B28] rounded-xl flex items-center justify-center text-gray-400">暂无沪深300数据</div> : <div ref={chartContainerRef} className="w-full h-[240px] bg-[#191B28] rounded-xl overflow-hidden" />}
 
6
  */
7
 
8
  import { useState, useEffect, useMemo, useRef } from 'react';
9
+ import { TrendingUp, TrendingDown, Wallet, Package, RotateCcw, Eye, ChevronRight, Play, Pause, BarChart3, Trophy, X, TrendingUpIcon, Share2 } from 'lucide-react';
10
  import { useGameStore, calculateReturnRate, calculateTotalAsset } from '@/store/gameStore';
11
  import { api } from '@/lib/api';
12
  import { createChart, ColorType, LineData, Time, LineStyle } from 'lightweight-charts';
13
+ import html2canvas from 'html2canvas';
14
 
15
  export default function TradePanel() {
16
  const [volume, setVolume] = useState(100);
 
440
 
441
  function ReturnChartModal({ onClose, allKlines, history, initialIndex, currentIndex, initialCapital, stats }: ReturnChartModalProps) {
442
  const chartContainerRef = useRef<HTMLDivElement>(null);
443
+ const modalRef = useRef<HTMLDivElement>(null);
444
  const chartRef = useRef<any>(null);
445
  const [hs300Data, setHS300Data] = useState<{date: string, close: number}[]>([]);
446
  const [isLoading, setIsLoading] = useState(true);
447
+ const [isSharing, setIsSharing] = useState(false);
448
 
449
+ // 分享功能
450
+ const handleShare = async () => {
451
+ if (!modalRef.current) return;
452
+ setIsSharing(true);
453
+ try {
454
+ // 稍微延迟确保 UI 渲染完成
455
+ await new Promise(resolve => setTimeout(resolve, 100));
456
+
457
+ const canvas = await html2canvas(modalRef.current, {
458
+ backgroundColor: '#1E212B',
459
+ scale: 2, // 提高清晰度
460
+ useCORS: true,
461
+ logging: false,
462
+ ignoreElements: (element) => {
463
+ // 忽略关闭和分享按钮
464
+ return element.tagName === 'BUTTON';
465
+ }
466
+ });
467
+
468
+ const image = canvas.toDataURL('image/png');
469
+
470
+ // 移动端尝试调用原生分享,否则下载
471
+ if (navigator.share) {
472
+ const blob = await (await fetch(image)).blob();
473
+ const file = new File([blob], 'profit_chart.png', { type: 'image/png' });
474
+ await navigator.share({
475
+ files: [file],
476
+ title: '我的复盘收益率',
477
+ text: '看看我在 StockReplay 的复盘战绩!',
478
+ });
479
+ } else {
480
+ const link = document.createElement('a');
481
+ link.href = image;
482
+ link.download = `StockReplay_Profit_${new Date().getTime()}.png`;
483
+ link.click();
484
+ }
485
+ } catch (error) {
486
+ console.error('Share failed:', error);
487
+ alert('生成分享图片失败');
488
+ } finally {
489
+ setIsSharing(false);
490
+ }
491
+ };
492
  const fetchHS300 = async () => {
493
  try {
494
  const visibleKlines = allKlines.slice(initialIndex, currentIndex + 1);
 
636
 
637
  return (
638
  <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
639
+ <div ref={modalRef} className="bg-[#1E212B] rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col shadow-2xl border border-[#2A2D3C]">
640
  <div className="px-4 py-3 border-b border-[#2A2D3C] flex items-center justify-between">
641
  <h3 className="text-lg font-bold text-white flex items-center gap-2"><TrendingUpIcon size={18} className="text-purple-500" />收益率曲线</h3>
642
+ <div className="flex items-center gap-2">
643
+ <button
644
+ onClick={handleShare}
645
+ disabled={isSharing}
646
+ className="w-8 h-8 flex items-center justify-center rounded-lg bg-[#2A2D3C] hover:bg-[#35394B] text-blue-400 hover:text-white transition-colors"
647
+ title="分享战绩"
648
+ >
649
+ <Share2 size={18} className={isSharing ? 'animate-pulse' : ''} />
650
+ </button>
651
+ <button onClick={onClose} className="w-8 h-8 flex items-center justify-center rounded-lg bg-[#2A2D3C] hover:bg-[#35394B] text-gray-400 hover:text-white transition-colors"><X size={18} /></button>
652
+ </div>
653
  </div>
654
  <div className="pt-3 pb-2 px-1 lg:px-2">
655
  {isLoading ? <div className="w-full h-[240px] bg-[#191B28] rounded-xl flex items-center justify-center text-gray-400">加载中...</div> : benchmarkData.length === 0 ? <div className="w-full h-[240px] bg-[#191B28] rounded-xl flex items-center justify-center text-gray-400">暂无沪深300数据</div> : <div ref={chartContainerRef} className="w-full h-[240px] bg-[#191B28] rounded-xl overflow-hidden" />}
package-lock.json CHANGED
@@ -2,5 +2,60 @@
2
  "name": "Paper_Trading",
3
  "lockfileVersion": 3,
4
  "requires": true,
5
- "packages": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
 
2
  "name": "Paper_Trading",
3
  "lockfileVersion": 3,
4
  "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "dependencies": {
8
+ "html2canvas": "^1.4.1"
9
+ }
10
+ },
11
+ "node_modules/base64-arraybuffer": {
12
+ "version": "1.0.2",
13
+ "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
14
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
15
+ "license": "MIT",
16
+ "engines": {
17
+ "node": ">= 0.6.0"
18
+ }
19
+ },
20
+ "node_modules/css-line-break": {
21
+ "version": "2.1.0",
22
+ "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
23
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "utrie": "^1.0.2"
27
+ }
28
+ },
29
+ "node_modules/html2canvas": {
30
+ "version": "1.4.1",
31
+ "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
32
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "css-line-break": "^2.1.0",
36
+ "text-segmentation": "^1.0.3"
37
+ },
38
+ "engines": {
39
+ "node": ">=8.0.0"
40
+ }
41
+ },
42
+ "node_modules/text-segmentation": {
43
+ "version": "1.0.3",
44
+ "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
45
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "utrie": "^1.0.2"
49
+ }
50
+ },
51
+ "node_modules/utrie": {
52
+ "version": "1.0.2",
53
+ "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
54
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "base64-arraybuffer": "^1.0.2"
58
+ }
59
+ }
60
+ }
61
  }
package.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "dependencies": {
3
+ "html2canvas": "^1.4.1"
4
+ }
5
+ }