From 1256b807a97b4f2168633e553b1ec07d4d1d24b0 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Sat, 28 Feb 2026 19:45:54 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E4=BA=AE=E8=89=B2=E4=B8=BB?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/Common.jsx | 41 +- app/components/DcaModal.jsx | 94 +-- app/components/FundIntradayChart.jsx | 68 +- app/components/FundTrendChart.jsx | 92 ++- app/components/HoldingActionModal.jsx | 4 +- app/components/Icons.jsx | 17 + app/components/PcFundTable.jsx | 8 +- app/components/ScanPickModal.jsx | 8 +- app/components/TradeModal.jsx | 119 +-- app/components/TransactionHistoryModal.jsx | 36 +- app/globals.css | 850 +++++++++++++++++++++ app/layout.jsx | 8 +- app/page.jsx | 72 +- 13 files changed, 1108 insertions(+), 309 deletions(-) diff --git a/app/components/Common.jsx b/app/components/Common.jsx index 17ce4cd..ae17dca 100644 --- a/app/components/Common.jsx +++ b/app/components/Common.jsx @@ -73,20 +73,8 @@ export function DatePicker({ value, onChange }) { return (
e.stopPropagation()}>
setIsOpen(!isOpen)} - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: '0 12px', - height: '40px', - background: 'rgba(0,0,0,0.2)', - borderRadius: '8px', - cursor: 'pointer', - border: '1px solid transparent', - transition: 'all 0.2s' - }} > {value || '选择日期'} @@ -98,7 +86,7 @@ export function DatePicker({ value, onChange }) { initial={{ opacity: 0, y: 10, scale: 0.95 }} animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 10, scale: 0.95 }} - className="glass card" + className="date-picker-dropdown glass card" style={{ position: 'absolute', top: '100%', @@ -106,10 +94,7 @@ export function DatePicker({ value, onChange }) { width: '100%', marginTop: 8, padding: 12, - zIndex: 10, - background: 'rgba(30, 41, 59, 0.95)', - backdropFilter: 'blur(12px)', - border: '1px solid rgba(255,255,255,0.1)' + zIndex: 10 }} >
@@ -141,26 +126,8 @@ export function DatePicker({ value, onChange }) { return (
!isFuture && handleSelect(e, d)} - style={{ - height: 28, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - fontSize: '13px', - borderRadius: '6px', - cursor: isFuture ? 'not-allowed' : 'pointer', - background: isSelected ? 'var(--primary)' : isToday ? 'rgba(255,255,255,0.1)' : 'transparent', - color: isFuture ? 'var(--muted)' : isSelected ? '#000' : 'var(--text)', - fontWeight: isSelected || isToday ? 600 : 400, - opacity: isFuture ? 0.3 : 1 - }} - onMouseEnter={(e) => { - if (!isSelected && !isFuture) e.currentTarget.style.background = 'rgba(255,255,255,0.1)'; - }} - onMouseLeave={(e) => { - if (!isSelected && !isFuture) e.currentTarget.style.background = isToday ? 'rgba(255,255,255,0.1)' : 'transparent'; - }} > {d}
diff --git a/app/components/DcaModal.jsx b/app/components/DcaModal.jsx index 79432a8..2a59bd0 100644 --- a/app/components/DcaModal.jsx +++ b/app/components/DcaModal.jsx @@ -185,7 +185,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { initial={{ opacity: 0, scale: 0.95, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.95, y: 20 }} - className="glass card modal" + className="glass card modal dca-modal" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '420px' }} > @@ -220,28 +220,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { gap: 6 }} > - - + + {enabled ? '已启用' : '未启用'} @@ -284,23 +264,13 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { -
+
{CYCLES.map((opt) => ( @@ -314,23 +284,13 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { -
+
{WEEKDAY_OPTIONS.map((opt) => ( @@ -344,20 +304,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { -
+
{Array.from({ length: 28 }).map((_, idx) => { const day = idx + 1; const active = monthlyDay === day; @@ -366,17 +313,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { key={day} ref={active ? monthlyDayRef : null} type="button" + className={`dca-option-btn dca-monthly-btn ${active ? 'active' : ''}`} onClick={() => setMonthlyDay(day)} - style={{ - flex: '0 0 calc(25% - 4px)', - border: 'none', - background: active ? 'var(--primary)' : 'transparent', - color: active ? '#05263b' : 'var(--muted)', - borderRadius: 6, - fontSize: 11, - cursor: 'pointer', - padding: '4px 0' - }} > {day}日 @@ -390,15 +328,7 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) { -
+
{firstDate}
@@ -409,9 +339,9 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
diff --git a/app/components/FundIntradayChart.jsx b/app/components/FundIntradayChart.jsx index e371446..4a49c03 100644 --- a/app/components/FundIntradayChart.jsx +++ b/app/components/FundIntradayChart.jsx @@ -22,14 +22,41 @@ ChartJS.register( Filler ); +const CHART_COLORS = { + dark: { + danger: '#f87171', + success: '#34d399', + primary: '#22d3ee', + muted: '#9ca3af', + border: '#1f2937', + text: '#e5e7eb', + crosshairText: '#0f172a', + }, + light: { + danger: '#dc2626', + success: '#059669', + primary: '#0891b2', + muted: '#475569', + border: '#e2e8f0', + text: '#0f172a', + crosshairText: '#ffffff', + } +}; + +function getChartThemeColors(theme) { + return CHART_COLORS[theme] || CHART_COLORS.dark; +} + /** * 分时图:展示当日(或最近一次记录日)的估值序列,纵轴为相对参考净值的涨跌幅百分比。 * series: Array<{ time: string, value: number, date?: string }> * referenceNav: 参考净值(最新单位净值),用于计算涨跌幅;未传则用当日第一个估值作为参考。 + * theme: 'light' | 'dark',用于亮色主题下坐标轴与 crosshair 样式 */ -export default function FundIntradayChart({ series = [], referenceNav }) { +export default function FundIntradayChart({ series = [], referenceNav, theme = 'dark' }) { const chartRef = useRef(null); const hoverTimeoutRef = useRef(null); + const chartColors = useMemo(() => getChartThemeColors(theme), [theme]); const chartData = useMemo(() => { if (!series.length) return { labels: [], datasets: [] }; @@ -40,9 +67,8 @@ export default function FundIntradayChart({ series = [], referenceNav }) { : values[0]; const percentages = values.map((v) => (ref ? ((v - ref) / ref) * 100 : 0)); const lastPct = percentages[percentages.length - 1]; - const riseColor = '#f87171'; // 涨用红色 - const fallColor = '#34d399'; // 跌用绿色 - // 以最新点相对参考净值的涨跌定色:涨(>=0)红,跌(<0)绿 + const riseColor = chartColors.danger; + const fallColor = chartColors.success; const lineColor = lastPct != null && lastPct >= 0 ? riseColor : fallColor; return { @@ -68,9 +94,11 @@ export default function FundIntradayChart({ series = [], referenceNav }) { } ] }; - }, [series, referenceNav]); + }, [series, referenceNav, chartColors.danger, chartColors.success]); - const options = useMemo(() => ({ + const options = useMemo(() => { + const colors = getChartThemeColors(); + return { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, @@ -88,7 +116,7 @@ export default function FundIntradayChart({ series = [], referenceNav }) { display: true, grid: { display: false }, ticks: { - color: '#9ca3af', + color: colors.muted, font: { size: 10 }, maxTicksLimit: 6 } @@ -96,9 +124,9 @@ export default function FundIntradayChart({ series = [], referenceNav }) { y: { display: true, position: 'left', - grid: { color: '#1f2937', drawBorder: false }, + grid: { color: colors.border, drawBorder: false }, ticks: { - color: '#9ca3af', + color: colors.muted, font: { size: 10 }, callback: (v) => (isNumber(v) ? `${v >= 0 ? '+' : ''}${v.toFixed(2)}%` : v) } @@ -142,7 +170,8 @@ export default function FundIntradayChart({ series = [], referenceNav }) { }, 2000); } } - }), []); + }; + }, [theme]); useEffect(() => { return () => { @@ -152,7 +181,9 @@ export default function FundIntradayChart({ series = [], referenceNav }) { }; }, []); - const plugins = useMemo(() => [{ + const plugins = useMemo(() => { + const colors = getChartThemeColors(theme); + return [{ id: 'crosshair', afterDraw: (chart) => { const ctx = chart.ctx; @@ -175,17 +206,15 @@ export default function FundIntradayChart({ series = [], referenceNav }) { ctx.save(); ctx.setLineDash([3, 3]); ctx.lineWidth = 1; - ctx.strokeStyle = '#9ca3af'; + ctx.strokeStyle = colors.muted; ctx.moveTo(x, topY); ctx.lineTo(x, bottomY); ctx.moveTo(leftX, y); ctx.lineTo(rightX, y); ctx.stroke(); - const prim = typeof document !== 'undefined' - ? (getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#22d3ee') - : '#22d3ee'; - const bgText = '#0f172a'; + const prim = colors.primary; + const textCol = colors.crosshairText; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; @@ -202,7 +231,7 @@ export default function FundIntradayChart({ series = [], referenceNav }) { const labelCenterX = labelLeft + tw / 2; ctx.fillStyle = prim; ctx.fillRect(labelLeft, bottomY, tw, 16); - ctx.fillStyle = bgText; + ctx.fillStyle = textCol; ctx.fillText(timeStr, labelCenterX, bottomY + 8); } if (data && index in data) { @@ -211,12 +240,13 @@ export default function FundIntradayChart({ series = [], referenceNav }) { const vw = ctx.measureText(valueStr).width + 8; ctx.fillStyle = prim; ctx.fillRect(leftX, y - 8, vw, 16); - ctx.fillStyle = bgText; + ctx.fillStyle = textCol; ctx.fillText(valueStr, leftX + vw / 2, y); } ctx.restore(); } - }], []); + }]; + }, [theme]); if (series.length < 2) return null; diff --git a/app/components/FundTrendChart.jsx b/app/components/FundTrendChart.jsx index 08d42c4..4c36db1 100644 --- a/app/components/FundTrendChart.jsx +++ b/app/components/FundTrendChart.jsx @@ -29,7 +29,32 @@ ChartJS.register( Filler ); -export default function FundTrendChart({ code, isExpanded, onToggleExpand, transactions = [] }) { +const CHART_COLORS = { + dark: { + danger: '#f87171', + success: '#34d399', + primary: '#22d3ee', + muted: '#9ca3af', + border: '#1f2937', + text: '#e5e7eb', + crosshairText: '#0f172a', + }, + light: { + danger: '#dc2626', + success: '#059669', + primary: '#0891b2', + muted: '#475569', + border: '#e2e8f0', + text: '#0f172a', + crosshairText: '#ffffff', + } +}; + +function getChartThemeColors(theme) { + return CHART_COLORS[theme] || CHART_COLORS.dark; +} + +export default function FundTrendChart({ code, isExpanded, onToggleExpand, transactions = [], theme = 'dark' }) { const [range, setRange] = useState('1m'); const [data, setData] = useState([]); const [loading, setLoading] = useState(false); @@ -37,6 +62,8 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans const chartRef = useRef(null); const hoverTimeoutRef = useRef(null); + const chartColors = useMemo(() => getChartThemeColors(theme), [theme]); + useEffect(() => { // If collapsed, don't fetch data unless we have no data yet if (!isExpanded && data.length > 0) return; @@ -84,12 +111,11 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans return ((last - first) / first) * 100; }, [data]); - // Red for up, Green for down (CN market style) - // Hardcoded hex values from globals.css for Chart.js - const upColor = '#f87171'; // --danger,与折线图红色一致 - const downColor = '#34d399'; // --success + // Red for up, Green for down (CN market style),随主题使用 CSS 变量 + const upColor = chartColors.danger; + const downColor = chartColors.success; const lineColor = change >= 0 ? upColor : downColor; - const primaryColor = typeof document !== 'undefined' ? (getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#22d3ee') : '#22d3ee'; + const primaryColor = chartColors.primary; const chartData = useMemo(() => { // Calculate percentage change based on the first data point @@ -165,9 +191,10 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans } ] }; - }, [data, lineColor, transactions, primaryColor]); + }, [data, transactions, lineColor, primaryColor, upColor]); const options = useMemo(() => { + const colors = getChartThemeColors(theme); return { responsive: true, maintainAspectRatio: false, @@ -190,7 +217,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans drawBorder: false }, ticks: { - color: '#9ca3af', + color: colors.muted, font: { size: 10 }, maxTicksLimit: 4, maxRotation: 0 @@ -201,12 +228,12 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans display: true, position: 'left', grid: { - color: '#1f2937', + color: colors.border, drawBorder: false, tickLength: 0 }, ticks: { - color: '#9ca3af', + color: colors.muted, font: { size: 10 }, count: 5, callback: (value) => `${value.toFixed(2)}%` @@ -240,7 +267,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans }, onClick: () => {} }; - }, []); + }, [theme]); useEffect(() => { return () => { @@ -250,7 +277,9 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans }; }, []); - const plugins = useMemo(() => [{ + const plugins = useMemo(() => { + const colors = getChartThemeColors(theme); + return [{ id: 'crosshair', afterEvent: (chart, args) => { const { event, replay } = args || {}; @@ -276,7 +305,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans afterDraw: (chart) => { const ctx = chart.ctx; const datasets = chart.data.datasets; - const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#22d3ee'; + const primaryColor = colors.primary; // 绘制圆角矩形(兼容无 roundRect 的环境) const drawRoundRect = (left, top, w, h, r) => { @@ -377,7 +406,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans ctx.beginPath(); ctx.setLineDash([3, 3]); ctx.lineWidth = 1; - ctx.strokeStyle = '#9ca3af'; + ctx.strokeStyle = colors.muted; // Draw vertical line ctx.moveTo(x, topY); @@ -415,7 +444,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans const labelCenterX = labelLeft + textWidth / 2; ctx.fillStyle = primaryColor; ctx.fillRect(labelLeft, bottomY, textWidth, 16); - ctx.fillStyle = '#0f172a'; // --background + ctx.fillStyle = colors.crosshairText; ctx.fillText(dateStr, labelCenterX, bottomY + 8); // Y axis label (value) @@ -423,7 +452,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans const valWidth = ctx.measureText(valueStr).width + 8; ctx.fillStyle = primaryColor; ctx.fillRect(leftX, y - 8, valWidth, 16); - ctx.fillStyle = '#0f172a'; // --background + ctx.fillStyle = colors.crosshairText; ctx.textAlign = 'center'; ctx.fillText(valueStr, leftX + valWidth / 2, y); } @@ -442,7 +471,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans const label = datasets[dsIndex].label; // Determine background color based on dataset index // 1 = Buy (主题色), 2 = Sell (与折线图红色一致) - const bgColor = dsIndex === 1 ? primaryColor : '#f87171'; + const bgColor = dsIndex === 1 ? primaryColor : colors.danger; // If collision, offset Buy label upwards let yOffset = 0; @@ -457,7 +486,8 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans ctx.restore(); } } - }], []); // 移除 data 依赖,因为我们直接从 chart 实例读取数据 + }]; + }, [theme]); // theme 变化时重算以应用亮色/暗色坐标轴与 crosshair return (
e.stopPropagation()}> @@ -501,19 +531,13 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans >
{loading && ( -
+
加载中...
)} {!loading && data.length === 0 && ( -
+
暂无数据
)} @@ -523,23 +547,13 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans )}
-
+
{ranges.map(r => ( diff --git a/app/components/HoldingActionModal.jsx b/app/components/HoldingActionModal.jsx index b700fab..1401c3b 100644 --- a/app/components/HoldingActionModal.jsx +++ b/app/components/HoldingActionModal.jsx @@ -75,9 +75,9 @@ export default function HoldingActionModal({ fund, onClose, onAction, hasHistory 减仓 diff --git a/app/components/Icons.jsx b/app/components/Icons.jsx index 7aa8186..15203f6 100644 --- a/app/components/Icons.jsx +++ b/app/components/Icons.jsx @@ -243,3 +243,20 @@ export function CameraIcon(props) { ); } + +export function SunIcon(props) { + return ( + + + + + ); +} + +export function MoonIcon(props) { + return ( + + + + ); +} diff --git a/app/components/PcFundTable.jsx b/app/components/PcFundTable.jsx index 2ce4d51..7835d56 100644 --- a/app/components/PcFundTable.jsx +++ b/app/components/PcFundTable.jsx @@ -564,7 +564,7 @@ export default function PcFundTable({ left: isLeft ? `${column.getStart('left')}px` : undefined, right: isRight ? `${column.getAfter('right')}px` : undefined, zIndex: isHeader ? 11 : 10, - backgroundColor: isHeader ? '#2a394b' : 'var(--row-bg)', + backgroundColor: isHeader ? 'var(--table-pinned-header-bg)' : 'var(--row-bg)', boxShadow: 'none', textAlign: isNameColumn ? 'left' : 'center', justifyContent: isNameColumn ? 'flex-start' : 'center', @@ -572,14 +572,14 @@ export default function PcFundTable({ }; return ( - <> +