From 6ae47f68c78bc97b095534fb9f54bf7a2bf3a595 Mon Sep 17 00:00:00 2001
From: hzm <934585316@qq.com>
Date: Thu, 5 Feb 2026 01:40:57 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E5=BD=93?=
=?UTF-8?q?=E6=97=A5=E6=9C=80=E6=96=B0=E6=B6=A8=E8=B7=8C=E5=B9=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/globals.css | 7 +
app/page.jsx | 392 ++++++++++++++++++++++++++++++++++++++----------
2 files changed, 318 insertions(+), 81 deletions(-)
diff --git a/app/globals.css b/app/globals.css
index 4cc8eb4..a49c322 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -541,6 +541,13 @@ input[type="number"] {
}
}
+/* 禁止移动端输入框自动缩放 */
+@media (max-width: 640px) {
+ .input.no-zoom {
+ font-size: 16px !important;
+ }
+}
+
.tab {
padding: 0 20px;
border-radius: 8px;
diff --git a/app/page.jsx b/app/page.jsx
index 459642f..0696f25 100644
--- a/app/page.jsx
+++ b/app/page.jsx
@@ -1,6 +1,6 @@
'use client';
-import { useEffect, useRef, useState, useMemo } from 'react';
+import { useEffect, useRef, useState, useMemo, useLayoutEffect } from 'react';
import { motion, AnimatePresence, Reorder } from 'framer-motion';
import Announcement from "./components/Announcement";
@@ -309,7 +309,7 @@ function NumericInput({ value, onChange, step = 1, min = 0, placeholder }) {
onChange(e.target.value)}
placeholder={placeholder}
@@ -330,9 +330,9 @@ function NumericInput({ value, onChange, step = 1, min = 0, placeholder }) {
function Stat({ label, value, delta }) {
const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : '';
return (
-
-
{label}
-
{value}
+
+ {label}
+ {value}
);
}
@@ -1294,8 +1294,60 @@ function GroupModal({ onClose, onConfirm }) {
);
}
+// 数字滚动组件
+function CountUp({ value, prefix = '', suffix = '', decimals = 2, className = '', style = {} }) {
+ const [displayValue, setDisplayValue] = useState(value);
+ const previousValue = useRef(value);
+
+ useEffect(() => {
+ if (previousValue.current === value) return;
+
+ const start = previousValue.current;
+ const end = value;
+ const duration = 1000; // 1秒动画
+ const startTime = performance.now();
+
+ const animate = (currentTime) => {
+ const elapsed = currentTime - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ // easeOutQuart
+ const ease = 1 - Math.pow(1 - progress, 4);
+
+ const current = start + (end - start) * ease;
+ setDisplayValue(current);
+
+ if (progress < 1) {
+ requestAnimationFrame(animate);
+ } else {
+ previousValue.current = value;
+ }
+ };
+
+ requestAnimationFrame(animate);
+ }, [value]);
+
+ return (
+
+ {prefix}{Math.abs(displayValue).toFixed(decimals)}{suffix}
+
+ );
+}
+
function GroupSummary({ funds, holdings, groupName, getProfit }) {
- const [showPercent, setShowPercent] = useState(false);
+ const [showPercent, setShowPercent] = useState(true);
+ const rowRef = useRef(null);
+ const [assetSize, setAssetSize] = useState(24);
+ const [metricSize, setMetricSize] = useState(18);
+ const [winW, setWinW] = useState(0);
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ setWinW(window.innerWidth);
+ const onR = () => setWinW(window.innerWidth);
+ window.addEventListener('resize', onR);
+ return () => window.removeEventListener('resize', onR);
+ }
+ }, []);
const summary = useMemo(() => {
let totalAsset = 0;
@@ -1326,16 +1378,32 @@ function GroupSummary({ funds, holdings, groupName, getProfit }) {
return { totalAsset, totalProfitToday, totalHoldingReturn, hasHolding, returnRate };
}, [funds, holdings, getProfit]);
+ useLayoutEffect(() => {
+ const el = rowRef.current;
+ if (!el) return;
+ const height = el.clientHeight;
+ // 使用 80px 作为更严格的阈值,因为 margin/padding 可能导致实际占用更高
+ const tooTall = height > 80;
+ if (tooTall) {
+ setAssetSize(s => Math.max(16, s - 1));
+ setMetricSize(s => Math.max(12, s - 1));
+ } else {
+ // 如果高度正常,尝试适当恢复字体大小,但不要超过初始值
+ // 这里的逻辑可以优化:如果当前远小于阈值,可以尝试增大,但为了稳定性,主要处理缩小的场景
+ // 或者:如果高度非常小(例如远小于80),可以尝试+1,但要小心死循环
+ }
+ }, [winW, summary.totalAsset, summary.totalProfitToday, summary.totalHoldingReturn, summary.returnRate, showPercent, assetSize, metricSize]); // 添加 assetSize, metricSize 到依赖,确保逐步缩小生效
+
if (!summary.hasHolding) return null;
return (
-
+
{groupName}
¥
- {summary.totalAsset.toFixed(2)}
+
@@ -1345,8 +1413,9 @@ function GroupSummary({ funds, holdings, groupName, getProfit }) {
className={summary.totalProfitToday > 0 ? 'up' : summary.totalProfitToday < 0 ? 'down' : ''}
style={{ fontSize: '18px', fontWeight: 700, fontFamily: 'var(--font-mono)' }}
>
- {summary.totalProfitToday > 0 ? '+' : summary.totalProfitToday < 0 ? '-' : ''}
- ¥{Math.abs(summary.totalProfitToday).toFixed(2)}
+ {summary.totalProfitToday > 0 ? '+' : summary.totalProfitToday < 0 ? '-' : ''}
+ ¥
+
@@ -1357,11 +1426,15 @@ function GroupSummary({ funds, holdings, groupName, getProfit }) {
onClick={() => setShowPercent(!showPercent)}
title="点击切换金额/百分比"
>
- {summary.totalHoldingReturn > 0 ? '+' : summary.totalHoldingReturn < 0 ? '-' : ''}
- {showPercent
- ? `${Math.abs(summary.returnRate).toFixed(2)}%`
- : `¥${Math.abs(summary.totalHoldingReturn).toFixed(2)}`
- }
+ {summary.totalHoldingReturn > 0 ? '+' : summary.totalHoldingReturn < 0 ? '-' : ''}
+ {showPercent ? (
+
+ ) : (
+ <>
+ ¥
+
+ >
+ )}
@@ -1423,8 +1496,68 @@ export default function HomePage() {
const [clearConfirm, setClearConfirm] = useState(null); // { fund }
const [holdings, setHoldings] = useState({}); // { [code]: { share: number, cost: number } }
const [percentModes, setPercentModes] = useState({}); // { [code]: boolean }
+ const [isTradingDay, setIsTradingDay] = useState(true); // 默认为交易日,通过接口校正
const tabsRef = useRef(null);
+ const today = new Date();
+ const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
+
+ // 检查交易日状态
+ const checkTradingDay = () => {
+ const now = new Date();
+ const isWeekend = now.getDay() === 0 || now.getDay() === 6;
+
+ // 周末直接判定为非交易日
+ if (isWeekend) {
+ setIsTradingDay(false);
+ return;
+ }
+
+ // 工作日通过上证指数判断是否为节假日
+ // 接口返回示例: v_sh000001="1~上证指数~...~20260205150000~..."
+ // 第30位是时间字段
+ const script = document.createElement('script');
+ script.src = `https://qt.gtimg.cn/q=sh000001&_t=${Date.now()}`;
+ script.onload = () => {
+ const data = window.v_sh000001;
+ if (data) {
+ const parts = data.split('~');
+ if (parts.length > 30) {
+ const dateStr = parts[30].slice(0, 8); // 20260205
+ const currentStr = todayStr.replace(/-/g, '');
+
+ if (dateStr === currentStr) {
+ setIsTradingDay(true); // 日期匹配,确认为交易日
+ } else {
+ // 日期不匹配 (显示的是旧数据)
+ // 如果已经过了 09:30 还是旧数据,说明今天休市
+ const minutes = now.getHours() * 60 + now.getMinutes();
+ if (minutes >= 9 * 60 + 30) {
+ setIsTradingDay(false);
+ } else {
+ // 9:30 之前,即使是旧数据,也默认是交易日(盘前)
+ setIsTradingDay(true);
+ }
+ }
+ }
+ }
+ document.body.removeChild(script);
+ };
+ script.onerror = () => {
+ document.body.removeChild(script);
+ // 接口失败,降级为仅判断周末
+ setIsTradingDay(!isWeekend);
+ };
+ document.body.appendChild(script);
+ };
+
+ useEffect(() => {
+ checkTradingDay();
+ // 每分钟检查一次
+ const timer = setInterval(checkTradingDay, 60000);
+ return () => clearInterval(timer);
+ }, []);
+
// 过滤和排序后的基金列表
const displayFunds = funds
.filter(f => {
@@ -1468,19 +1601,41 @@ export default function HomePage() {
const getHoldingProfit = (fund, holding) => {
if (!holding || typeof holding.share !== 'number') return null;
- // 当前净值
- const currentNav = fund.estPricedCoverage > 0.05
- ? fund.estGsz
- : (typeof fund.gsz === 'number' ? fund.gsz : Number(fund.dwjz));
-
- if (!currentNav) return null;
-
- // 持仓金额 = 份额 * 当前净值
- const amount = holding.share * currentNav;
+ const now = new Date();
+ const isAfter9 = now.getHours() >= 9;
+ const hasTodayData = fund.jzrq === todayStr;
- // 估算收益 = 份额 * (当前净值 - 昨日净值)
- // 注意:这里用估值涨跌幅计算当日盈亏
- const profitToday = amount * (fund.estPricedCoverage > 0.05 ? fund.estGszzl : (Number(fund.gszzl) || 0)) / 100;
+ // 如果是交易日且9点以后,且今日净值未出,则强制使用估值(隐藏涨跌幅列模式)
+ const useValuation = isTradingDay && isAfter9 && !hasTodayData;
+
+ let currentNav;
+ let profitToday;
+
+ if (!useValuation) {
+ // 使用确权净值 (dwjz)
+ currentNav = Number(fund.dwjz);
+ if (!currentNav) return null;
+
+ const amount = holding.share * currentNav;
+ // 优先用 zzl (真实涨跌幅), 降级用 gszzl
+ const rate = fund.zzl !== undefined ? Number(fund.zzl) : (Number(fund.gszzl) || 0);
+ profitToday = amount - (amount / (1 + rate / 100));
+ } else {
+ // 否则使用估值
+ currentNav = fund.estPricedCoverage > 0.05
+ ? fund.estGsz
+ : (typeof fund.gsz === 'number' ? fund.gsz : Number(fund.dwjz));
+
+ if (!currentNav) return null;
+
+ const amount = holding.share * currentNav;
+ // 估值涨跌幅
+ const gzChange = fund.estPricedCoverage > 0.05 ? fund.estGszzl : (Number(fund.gszzl) || 0);
+ profitToday = amount - (amount / (1 + gzChange / 100));
+ }
+
+ // 持仓金额
+ const amount = holding.share * currentNav;
// 总收益 = (当前净值 - 成本价) * 份额
const profitTotal = typeof holding.cost === 'number'
@@ -1822,67 +1977,106 @@ export default function HomePage() {
dwjz: json.dwjz,
gsz: json.gsz,
gztime: json.gztime,
+ jzrq: json.jzrq,
gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
};
- // 获取重仓股票列表
- const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&rt=${Date.now()}`;
- loadScript(holdingsUrl).then(async () => {
- let holdings = [];
- const html = window.apidata?.content || '';
- const rows = html.match(/
/gi) || [];
- for (const r of rows) {
- const cells = (r.match(/| ([\s\S]*?)<\/td>/gi) || []).map(td => td.replace(/<[^>]*>/g, '').trim());
- const codeIdx = cells.findIndex(txt => /^\d{6}$/.test(txt));
- const weightIdx = cells.findIndex(txt => /\d+(?:\.\d+)?\s*%/.test(txt));
- if (codeIdx >= 0 && weightIdx >= 0) {
- holdings.push({
- code: cells[codeIdx],
- name: cells[codeIdx + 1] || '',
- weight: cells[weightIdx],
- change: null
+ // 并行获取:1. 腾讯接口获取最新确权净值和涨跌幅;2. 东方财富接口获取持仓
+ const tencentPromise = new Promise((resolveT) => {
+ const tUrl = `https://qt.gtimg.cn/q=jj${c}`;
+ const tScript = document.createElement('script');
+ tScript.src = tUrl;
+ tScript.onload = () => {
+ const v = window[`v_jj${c}`];
+ if (v) {
+ const p = v.split('~');
+ // p[5]: 单位净值, p[7]: 涨跌幅, p[8]: 净值日期
+ resolveT({
+ dwjz: p[5],
+ zzl: parseFloat(p[7]),
+ jzrq: p[8] ? p[8].slice(0, 10) : ''
});
+ } else {
+ resolveT(null);
}
- }
+ if (document.body.contains(tScript)) document.body.removeChild(tScript);
+ };
+ tScript.onerror = () => {
+ if (document.body.contains(tScript)) document.body.removeChild(tScript);
+ resolveT(null);
+ };
+ document.body.appendChild(tScript);
+ });
- holdings = holdings.slice(0, 10);
+ const holdingsPromise = new Promise((resolveH) => {
+ const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&rt=${Date.now()}`;
+ loadScript(holdingsUrl).then(async () => {
+ let holdings = [];
+ const html = window.apidata?.content || '';
+ const rows = html.match(/ |
/gi) || [];
+ for (const r of rows) {
+ const cells = (r.match(/| ([\s\S]*?)<\/td>/gi) || []).map(td => td.replace(/<[^>]*>/g, '').trim());
+ const codeIdx = cells.findIndex(txt => /^\d{6}$/.test(txt));
+ const weightIdx = cells.findIndex(txt => /\d+(?:\.\d+)?\s*%/.test(txt));
+ if (codeIdx >= 0 && weightIdx >= 0) {
+ holdings.push({
+ code: cells[codeIdx],
+ name: cells[codeIdx + 1] || '',
+ weight: cells[weightIdx],
+ change: null
+ });
+ }
+ }
- if (holdings.length) {
- try {
- const tencentCodes = holdings.map(h => `s_${getTencentPrefix(h.code)}${h.code}`).join(',');
- const quoteUrl = `https://qt.gtimg.cn/q=${tencentCodes}`;
+ holdings = holdings.slice(0, 10);
- await new Promise((resQuote) => {
- const scriptQuote = document.createElement('script');
- scriptQuote.src = quoteUrl;
- scriptQuote.onload = () => {
- holdings.forEach(h => {
- const varName = `v_s_${getTencentPrefix(h.code)}${h.code}`;
- const dataStr = window[varName];
- if (dataStr) {
- const parts = dataStr.split('~');
- // parts[5] 是涨跌幅
- if (parts.length > 5) {
- h.change = parseFloat(parts[5]);
+ if (holdings.length) {
+ try {
+ const tencentCodes = holdings.map(h => `s_${getTencentPrefix(h.code)}${h.code}`).join(',');
+ const quoteUrl = `https://qt.gtimg.cn/q=${tencentCodes}`;
+
+ await new Promise((resQuote) => {
+ const scriptQuote = document.createElement('script');
+ scriptQuote.src = quoteUrl;
+ scriptQuote.onload = () => {
+ holdings.forEach(h => {
+ const varName = `v_s_${getTencentPrefix(h.code)}${h.code}`;
+ const dataStr = window[varName];
+ if (dataStr) {
+ const parts = dataStr.split('~');
+ if (parts.length > 5) {
+ h.change = parseFloat(parts[5]);
+ }
}
- }
- });
- if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote);
- resQuote();
- };
- scriptQuote.onerror = () => {
- if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote);
- resQuote();
- };
- document.body.appendChild(scriptQuote);
- });
- } catch (e) {
- console.error('获取股票涨跌幅失败', e);
+ });
+ if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote);
+ resQuote();
+ };
+ scriptQuote.onerror = () => {
+ if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote);
+ resQuote();
+ };
+ document.body.appendChild(scriptQuote);
+ });
+ } catch (e) {
+ console.error('获取股票涨跌幅失败', e);
+ }
+ }
+ resolveH(holdings);
+ }).catch(() => resolveH([]));
+ });
+
+ Promise.all([tencentPromise, holdingsPromise]).then(([tData, holdings]) => {
+ if (tData) {
+ // 如果腾讯数据的日期更新(或相同),优先使用腾讯的净值数据(通常更准且包含涨跌幅)
+ if (tData.jzrq && (!gzData.jzrq || tData.jzrq >= gzData.jzrq)) {
+ gzData.dwjz = tData.dwjz;
+ gzData.jzrq = tData.jzrq;
+ gzData.zzl = tData.zzl; // 真实涨跌幅
}
}
-
resolve({ ...gzData, holdings });
- }).catch(() => resolve({ ...gzData, holdings: [] }));
+ });
};
scriptGz.onerror = () => {
@@ -2754,7 +2948,27 @@ export default function HomePage() {
)}
- {f.name}
+
+ {f.name}
+ {f.jzrq === todayStr && (
+
+ ✓
+
+ )}
+
#{f.code}
@@ -2779,6 +2993,22 @@ export default function HomePage() {
+ {(() => {
+ const now = new Date();
+ const isAfter9 = now.getHours() >= 9;
+ const hasTodayData = f.jzrq === todayStr;
+ const shouldHideChange = isTradingDay && isAfter9 && !hasTodayData;
+
+ if (shouldHideChange) return null;
+
+ return (
+ 0 ? '+' : ''}${Number(f.zzl).toFixed(2)}%` : '--'}
+ delta={f.zzl}
+ />
+ );
+ })()}
0.05 ? f.estGsz.toFixed(4) : (f.gsz ?? '—')} />
+
持仓金额
setActionModal({ open: true, fund: f })}
>
@@ -2819,7 +3049,7 @@ export default function HomePage() {
¥{profit.amount.toFixed(2)}
-
+
当日盈亏
0 ? 'up' : profit.profitToday < 0 ? 'down' : ''}`}>
{profit.profitToday > 0 ? '+' : profit.profitToday < 0 ? '-' : ''}¥{Math.abs(profit.profitToday).toFixed(2)}
@@ -2832,7 +3062,7 @@ export default function HomePage() {
e.stopPropagation();
setPercentModes(prev => ({ ...prev, [f.code]: !prev[f.code] }));
}}
- style={{ cursor: 'pointer' }}
+ style={{ cursor: 'pointer', flexDirection: 'column', gap: 4 }}
title="点击切换金额/百分比"
>
持有收益{percentModes[f.code] ? '(%)' : ''}
|