Spaces:
Running
Running
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 +59 -4
- package-lock.json +56 -1
- package.json +5 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|