diff --git a/app/components/FundHistoryNetValue.jsx b/app/components/FundHistoryNetValue.jsx
new file mode 100644
index 0000000..2644905
--- /dev/null
+++ b/app/components/FundHistoryNetValue.jsx
@@ -0,0 +1,221 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import {
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from '@tanstack/react-table';
+import { fetchFundHistory } from '../api/fund';
+import { cachedRequest } from '../lib/cacheRequest';
+import FundHistoryNetValueModal from './FundHistoryNetValueModal';
+
+/**
+ * 历史净值表格行:日期、净值、日涨幅(按日期降序,涨红跌绿)
+ */
+function buildRows(history) {
+ if (!Array.isArray(history) || history.length === 0) return [];
+ const reversed = [...history].reverse();
+ return reversed.map((item, i) => {
+ const prev = reversed[i + 1];
+ let dailyChange = null;
+ if (prev && Number.isFinite(item.value) && Number.isFinite(prev.value) && prev.value !== 0) {
+ dailyChange = ((item.value - prev.value) / prev.value) * 100;
+ }
+ return {
+ date: item.date,
+ netValue: item.value,
+ dailyChange,
+ };
+ });
+}
+
+const columns = [
+ {
+ accessorKey: 'date',
+ header: '日期',
+ cell: (info) => info.getValue(),
+ meta: { align: 'left' },
+ },
+ {
+ accessorKey: 'netValue',
+ header: '净值',
+ cell: (info) => {
+ const v = info.getValue();
+ return v != null && Number.isFinite(v) ? Number(v).toFixed(4) : '—';
+ },
+ meta: { align: 'center' },
+ },
+ {
+ accessorKey: 'dailyChange',
+ header: '日涨幅',
+ cell: (info) => {
+ const v = info.getValue();
+ if (v == null || !Number.isFinite(v)) return '—';
+ const sign = v > 0 ? '+' : '';
+ const cls = v > 0 ? 'up' : v < 0 ? 'down' : '';
+ return {sign}{v.toFixed(2)}%;
+ },
+ meta: { align: 'right' },
+ },
+];
+
+export default function FundHistoryNetValue({ code, range = '1m', theme }) {
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [modalOpen, setModalOpen] = useState(false);
+
+ useEffect(() => {
+ if (!code) {
+ setData([]);
+ setLoading(false);
+ return;
+ }
+ let active = true;
+ setLoading(true);
+ setError(null);
+ const cacheKey = `fund_history_${code}_${range}`;
+ cachedRequest(() => fetchFundHistory(code, range), cacheKey, { cacheTime: 10 * 60 * 1000 })
+ .then((res) => {
+ if (active) {
+ setData(buildRows(res || []));
+ setLoading(false);
+ }
+ })
+ .catch((err) => {
+ if (active) {
+ setError(err);
+ setData([]);
+ setLoading(false);
+ }
+ });
+ return () => { active = false; };
+ }, [code, range]);
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ const visibleRows = table.getRowModel().rows.slice(0, 5);
+
+ if (!code) return null;
+ if (loading) {
+ return (
+
+ 加载历史净值...
+
+ );
+ }
+ if (error || data.length === 0) {
+ return (
+
+
+ {error ? '加载失败' : '暂无历史净值'}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {table.getHeaderGroups().map((hg) => (
+
+ {hg.headers.map((h) => (
+ |
+ {flexRender(h.column.columnDef.header, h.getContext())}
+ |
+ ))}
+
+ ))}
+
+
+ {visibleRows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ |
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ |
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+
+ {modalOpen && (
+
+ )}
+
+ );
+}
diff --git a/app/components/FundHistoryNetValueModal.jsx b/app/components/FundHistoryNetValueModal.jsx
new file mode 100644
index 0000000..8b2ad83
--- /dev/null
+++ b/app/components/FundHistoryNetValueModal.jsx
@@ -0,0 +1,321 @@
+'use client';
+
+import { useEffect, useMemo, useRef, useState } from 'react';
+import {
+ flexRender,
+ getCoreRowModel,
+ useReactTable,
+} from '@tanstack/react-table';
+import { fetchFundHistory } from '../api/fund';
+import { cachedRequest } from '../lib/cacheRequest';
+import { CloseIcon } from './Icons';
+import {
+ Dialog,
+ DialogContent,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerHeader,
+ DrawerTitle,
+} from '@/components/ui/drawer';
+
+function buildRows(history) {
+ if (!Array.isArray(history) || history.length === 0) return [];
+ const reversed = [...history].reverse();
+ return reversed.map((item, i) => {
+ const prev = reversed[i + 1];
+ let dailyChange = null;
+ if (prev && Number.isFinite(item.value) && Number.isFinite(prev.value) && prev.value !== 0) {
+ dailyChange = ((item.value - prev.value) / prev.value) * 100;
+ }
+ return {
+ date: item.date,
+ netValue: item.value,
+ dailyChange,
+ };
+ });
+}
+
+const columns = [
+ {
+ accessorKey: 'date',
+ header: '日期',
+ cell: (info) => info.getValue(),
+ meta: { align: 'left' },
+ },
+ {
+ accessorKey: 'netValue',
+ header: '净值',
+ cell: (info) => {
+ const v = info.getValue();
+ return v != null && Number.isFinite(v) ? Number(v).toFixed(4) : '—';
+ },
+ meta: { align: 'center' },
+ },
+ {
+ accessorKey: 'dailyChange',
+ header: '日涨幅',
+ cell: (info) => {
+ const v = info.getValue();
+ if (v == null || !Number.isFinite(v)) return '—';
+ const sign = v > 0 ? '+' : '';
+ const cls = v > 0 ? 'up' : v < 0 ? 'down' : '';
+ return {sign}{v.toFixed(2)}%;
+ },
+ meta: { align: 'right' },
+ },
+];
+
+export default function FundHistoryNetValueModal({ open, onOpenChange, code, theme }) {
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [visibleCount, setVisibleCount] = useState(30);
+ const [isMobile, setIsMobile] = useState(false);
+ const scrollRef = useRef(null);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+ const mq = window.matchMedia('(max-width: 768px)');
+ const update = () => setIsMobile(mq.matches);
+ update();
+ mq.addEventListener('change', update);
+ return () => mq.removeEventListener('change', update);
+ }, []);
+
+ useEffect(() => {
+ if (!open || !code) return;
+ let active = true;
+ setLoading(true);
+ setError(null);
+ setVisibleCount(30);
+
+ const cacheKey = `fund_history_${code}_all_modal`;
+ cachedRequest(() => fetchFundHistory(code, 'all'), cacheKey, { cacheTime: 10 * 60 * 1000 })
+ .then((res) => {
+ if (!active) return;
+ setData(buildRows(res || []));
+ setLoading(false);
+ })
+ .catch((err) => {
+ if (!active) return;
+ setError(err);
+ setData([]);
+ setLoading(false);
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [open, code]);
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ });
+
+ const rows = table.getRowModel().rows.slice(0, visibleCount);
+ const hasMore = table.getRowModel().rows.length > visibleCount;
+
+ const handleOpenChange = (next) => {
+ if (!next) {
+ onOpenChange?.(false);
+ }
+ };
+
+ const handleScroll = (e) => {
+ const target = e.currentTarget;
+ if (!target || !hasMore) return;
+ const distance = target.scrollHeight - target.scrollTop - target.clientHeight;
+ if (distance < 40) {
+ setVisibleCount((prev) => {
+ const next = prev + 30;
+ const total = table.getRowModel().rows.length;
+ return next > total ? total : next;
+ });
+ }
+ };
+
+ const header = (
+
+
+ 历史净值
+
+
+
+ );
+
+ const body = (
+
+ {loading && (
+
+ 加载历史净值...
+
+ )}
+ {!loading && (error || data.length === 0) && (
+
+
+ {error ? '加载失败' : '暂无历史净值'}
+
+
+ )}
+ {!loading && data.length > 0 && (
+
+
+
+ {table.getHeaderGroups().map((hg) => (
+
+ {hg.headers.map((h) => (
+ |
+ {flexRender(h.column.columnDef.header, h.getContext())}
+ |
+ ))}
+
+ ))}
+
+
+ {rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+ |
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ |
+ ))}
+
+ ))}
+
+
+
+ )}
+ {!loading && hasMore && (
+
+ 向下滚动以加载更多...
+
+ )}
+
+ );
+
+ if (!open) return null;
+
+ if (isMobile) {
+ return (
+
+
+
+
+ 历史净值
+
+
+
+
+
+
+ {body}
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
diff --git a/app/components/FundTrendChart.jsx b/app/components/FundTrendChart.jsx
index 129dccb..5a71e0d 100644
--- a/app/components/FundTrendChart.jsx
+++ b/app/components/FundTrendChart.jsx
@@ -16,7 +16,8 @@ import {
Filler
} from 'chart.js';
import { Line } from 'react-chartjs-2';
-import {cachedRequest} from "../lib/cacheRequest";
+import { cachedRequest } from '../lib/cacheRequest';
+import FundHistoryNetValue from './FundHistoryNetValue';
ChartJS.register(
CategoryScale,
@@ -522,6 +523,8 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
))}
+
+
>
);