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) => ( + + ))} + + ))} + + + {visibleRows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender(h.column.columnDef.header, h.getContext())} +
+ {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) => ( + + ))} + + ))} + + + {rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+ {flexRender(h.column.columnDef.header, h.getContext())} +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ )} + {!loading && hasMore && ( +
+ 向下滚动以加载更多... +
+ )} +
+ ); + + if (!open) return null; + + if (isMobile) { + return ( + + + + + 历史净值 + + + + + +
+ {body} +
+
+
+ ); + } + + return ( + + + 历史净值 + {header} + {body} + + + ); +} + 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 ))} + + );