feat: 优化对 funds 的同步逻辑

This commit is contained in:
hzm
2026-02-09 10:49:40 +08:00
parent a3d90a756b
commit 2b5f998ab3

View File

@@ -281,7 +281,7 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
}); });
const [isAfter3pm, setIsAfter3pm] = useState(nowInTz().hour() >= 15); const [isAfter3pm, setIsAfter3pm] = useState(nowInTz().hour() >= 15);
const [calcShare, setCalcShare] = useState(null); const [calcShare, setCalcShare] = useState(null);
const currentPendingTrades = useMemo(() => { const currentPendingTrades = useMemo(() => {
return pendingTrades.filter(t => t.fundCode === fund?.code); return pendingTrades.filter(t => t.fundCode === fund?.code);
}, [pendingTrades, fund]); }, [pendingTrades, fund]);
@@ -302,7 +302,7 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
setShowPendingList(false); setShowPendingList(false);
} }
}, [showPendingList, currentPendingTrades]); }, [showPendingList, currentPendingTrades]);
const getEstimatePrice = () => fund?.estPricedCoverage > 0.05 ? fund?.estGsz : (typeof fund?.gsz === 'number' ? fund?.gsz : Number(fund?.dwjz)); const getEstimatePrice = () => fund?.estPricedCoverage > 0.05 ? fund?.estGsz : (typeof fund?.gsz === 'number' ? fund?.gsz : Number(fund?.dwjz));
const [price, setPrice] = useState(getEstimatePrice()); const [price, setPrice] = useState(getEstimatePrice());
const [loadingPrice, setLoadingPrice] = useState(false); const [loadingPrice, setLoadingPrice] = useState(false);
@@ -312,7 +312,7 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
if (date && fund?.code) { if (date && fund?.code) {
setLoadingPrice(true); setLoadingPrice(true);
setActualDate(null); setActualDate(null);
let queryDate = date; let queryDate = date;
if (isAfter3pm) { if (isAfter3pm) {
queryDate = toTz(date).add(1, 'day').format('YYYY-MM-DD'); queryDate = toTz(date).add(1, 'day').format('YYYY-MM-DD');
@@ -347,7 +347,7 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
} else { } else {
sellFee = parseFloat(feeValue) || 0; sellFee = parseFloat(feeValue) || 0;
} }
const estimatedReturn = sellAmount - sellFee; const estimatedReturn = sellAmount - sellFee;
useEffect(() => { useEffect(() => {
@@ -390,7 +390,7 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
const isValid = isBuy const isValid = isBuy
? (!!amount && !!feeRate && !!date && calcShare !== null) ? (!!amount && !!feeRate && !!date && calcShare !== null)
: (!!share && !!date); : (!!share && !!date);
const handleSetShareFraction = (fraction) => { const handleSetShareFraction = (fraction) => {
if(availableShare > 0) { if(availableShare > 0) {
setShare((availableShare * fraction).toFixed(2)); setShare((availableShare * fraction).toFixed(2));
@@ -429,12 +429,12 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
</div> </div>
{!showPendingList && !showConfirm && currentPendingTrades.length > 0 && ( {!showPendingList && !showConfirm && currentPendingTrades.length > 0 && (
<div <div
style={{ style={{
marginBottom: 16, marginBottom: 16,
background: 'rgba(230, 162, 60, 0.1)', background: 'rgba(230, 162, 60, 0.1)',
border: '1px solid rgba(230, 162, 60, 0.2)', border: '1px solid rgba(230, 162, 60, 0.2)',
borderRadius: 8, borderRadius: 8,
padding: '8px 12px', padding: '8px 12px',
fontSize: '12px', fontSize: '12px',
color: '#e6a23c', color: '#e6a23c',
@@ -453,8 +453,8 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
{showPendingList ? ( {showPendingList ? (
<div className="pending-list" style={{ maxHeight: '300px', overflowY: 'auto' }}> <div className="pending-list" style={{ maxHeight: '300px', overflowY: 'auto' }}>
<div className="pending-list-header" style={{ position: 'sticky', top: 0, zIndex: 1, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(6px)', paddingBottom: 8, marginBottom: 8, borderBottom: '1px solid var(--border)' }}> <div className="pending-list-header" style={{ position: 'sticky', top: 0, zIndex: 1, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(6px)', paddingBottom: 8, marginBottom: 8, borderBottom: '1px solid var(--border)' }}>
<button <button
className="button secondary" className="button secondary"
onClick={() => setShowPendingList(false)} onClick={() => setShowPendingList(false)}
style={{ padding: '4px 8px', fontSize: '12px' }} style={{ padding: '4px 8px', fontSize: '12px' }}
> >
@@ -481,9 +481,9 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
<button <button
className="button secondary" className="button secondary"
onClick={() => setRevokeTrade(trade)} onClick={() => setRevokeTrade(trade)}
style={{ style={{
padding: '2px 8px', padding: '2px 8px',
fontSize: '10px', fontSize: '10px',
height: 'auto', height: 'auto',
background: 'rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.1)',
color: 'var(--text)' color: 'var(--text)'
@@ -570,10 +570,10 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
)} )}
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<button <button
type="button" type="button"
className="button secondary" className="button secondary"
onClick={() => setShowConfirm(false)} onClick={() => setShowConfirm(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }} style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}
> >
返回修改 返回修改
@@ -652,10 +652,10 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
)} )}
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<button <button
type="button" type="button"
className="button secondary" className="button secondary"
onClick={() => setShowConfirm(false)} onClick={() => setShowConfirm(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }} style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}
> >
返回修改 返回修改
@@ -895,7 +895,7 @@ function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [
</button> </button>
</div> </div>
</div> </div>
<div style={{ marginBottom: 12, fontSize: '12px' }}> <div style={{ marginBottom: 12, fontSize: '12px' }}>
{loadingPrice ? ( {loadingPrice ? (
<span className="muted">正在查询净值数据...</span> <span className="muted">正在查询净值数据...</span>
@@ -1909,7 +1909,7 @@ export default function HomePage() {
const [holdings, setHoldings] = useState({}); // { [code]: { share: number, cost: number } } const [holdings, setHoldings] = useState({}); // { [code]: { share: number, cost: number } }
const [pendingTrades, setPendingTrades] = useState([]); // [{ id, fundCode, share, date, ... }] const [pendingTrades, setPendingTrades] = useState([]); // [{ id, fundCode, share, date, ... }]
const [percentModes, setPercentModes] = useState({}); // { [code]: boolean } const [percentModes, setPercentModes] = useState({}); // { [code]: boolean }
const holdingsRef = useRef(holdings); const holdingsRef = useRef(holdings);
const pendingTradesRef = useRef(pendingTrades); const pendingTradesRef = useRef(pendingTrades);
@@ -2191,11 +2191,11 @@ export default function HomePage() {
// 尝试获取智能净值 // 尝试获取智能净值
const result = await fetchSmartFundNetValue(trade.fundCode, queryDate); const result = await fetchSmartFundNetValue(trade.fundCode, queryDate);
if (result && result.value > 0) { if (result && result.value > 0) {
// 成功获取,执行交易 // 成功获取,执行交易
const current = tempHoldings[trade.fundCode] || { share: 0, cost: 0 }; const current = tempHoldings[trade.fundCode] || { share: 0, cost: 0 };
let newShare, newCost; let newShare, newCost;
if (trade.type === 'buy') { if (trade.type === 'buy') {
const feeRate = trade.feeRate || 0; const feeRate = trade.feeRate || 0;
@@ -2208,7 +2208,7 @@ export default function HomePage() {
newCost = current.cost; newCost = current.cost;
if (newShare === 0) newCost = 0; if (newShare === 0) newCost = 0;
} }
tempHoldings[trade.fundCode] = { share: newShare, cost: newCost }; tempHoldings[trade.fundCode] = { share: newShare, cost: newCost };
stateChanged = true; stateChanged = true;
processedIds.add(trade.id); processedIds.add(trade.id);
@@ -2218,13 +2218,13 @@ export default function HomePage() {
if (stateChanged) { if (stateChanged) {
setHoldings(tempHoldings); setHoldings(tempHoldings);
storageHelper.setItem('holdings', JSON.stringify(tempHoldings)); storageHelper.setItem('holdings', JSON.stringify(tempHoldings));
setPendingTrades(prev => { setPendingTrades(prev => {
const next = prev.filter(t => !processedIds.has(t.id)); const next = prev.filter(t => !processedIds.has(t.id));
storageHelper.setItem('pendingTrades', JSON.stringify(next)); storageHelper.setItem('pendingTrades', JSON.stringify(next));
return next; return next;
}); });
showToast(`已处理 ${processedIds.size} 笔待定交易`, 'success'); showToast(`已处理 ${processedIds.size} 笔待定交易`, 'success');
} }
}; };
@@ -2246,11 +2246,11 @@ export default function HomePage() {
isAfter3pm: data.isAfter3pm, isAfter3pm: data.isAfter3pm,
timestamp: Date.now() timestamp: Date.now()
}; };
const next = [...pendingTrades, pending]; const next = [...pendingTrades, pending];
setPendingTrades(next); setPendingTrades(next);
storageHelper.setItem('pendingTrades', JSON.stringify(next)); storageHelper.setItem('pendingTrades', JSON.stringify(next));
setTradeModal({ open: false, fund: null, type: 'buy' }); setTradeModal({ open: false, fund: null, type: 'buy' });
showToast('净值暂未更新,已加入待处理队列', 'info'); showToast('净值暂未更新,已加入待处理队列', 'info');
return; return;
@@ -2362,6 +2362,17 @@ export default function HomePage() {
userIdRef.current = user?.id || null; userIdRef.current = user?.id || null;
}, [user]); }, [user]);
const getFundCodesSignature = useCallback((value) => {
try {
const list = JSON.parse(value || '[]');
if (!Array.isArray(list)) return '';
const codes = list.map((item) => item?.code).filter(Boolean);
return Array.from(new Set(codes)).sort().join('|');
} catch (e) {
return '';
}
}, []);
const scheduleSync = useCallback(() => { const scheduleSync = useCallback(() => {
if (!userIdRef.current) return; if (!userIdRef.current) return;
if (skipSyncRef.current) return; if (skipSyncRef.current) return;
@@ -2377,8 +2388,14 @@ export default function HomePage() {
const storageHelper = useMemo(() => { const storageHelper = useMemo(() => {
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings', 'pendingTrades']); const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings', 'pendingTrades']);
const triggerSync = (key) => { const triggerSync = (key, prevValue, nextValue) => {
if (keys.has(key)) { if (keys.has(key)) {
if (key === 'funds') {
const prevSig = getFundCodesSignature(prevValue);
const nextSig = getFundCodesSignature(nextValue);
debugger
if (prevSig === nextSig) return;
}
if (!skipSyncRef.current) { if (!skipSyncRef.current) {
window.localStorage.setItem('localUpdatedAt', nowInTz().toISOString()); window.localStorage.setItem('localUpdatedAt', nowInTz().toISOString());
} }
@@ -2387,12 +2404,14 @@ export default function HomePage() {
}; };
return { return {
setItem: (key, value) => { setItem: (key, value) => {
const prevValue = key === 'funds' ? window.localStorage.getItem(key) : null;
window.localStorage.setItem(key, value); window.localStorage.setItem(key, value);
triggerSync(key); triggerSync(key, prevValue, value);
}, },
removeItem: (key) => { removeItem: (key) => {
const prevValue = key === 'funds' ? window.localStorage.getItem(key) : null;
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
triggerSync(key); triggerSync(key, prevValue, null);
}, },
clear: () => { clear: () => {
window.localStorage.clear(); window.localStorage.clear();
@@ -2402,19 +2421,26 @@ export default function HomePage() {
scheduleSync(); scheduleSync();
} }
}; };
}, [scheduleSync]); }, [getFundCodesSignature, scheduleSync]);
useEffect(() => { useEffect(() => {
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings', 'pendingTrades']); const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings', 'pendingTrades']);
const onStorage = (e) => { const onStorage = (e) => {
if (!e.key || keys.has(e.key)) scheduleSync(); if (!e.key) return;
if (!keys.has(e.key)) return;
if (e.key === 'funds') {
const prevSig = getFundCodesSignature(e.oldValue);
const nextSig = getFundCodesSignature(e.newValue);
if (prevSig === nextSig) return;
}
scheduleSync();
}; };
window.addEventListener('storage', onStorage); window.addEventListener('storage', onStorage);
return () => { return () => {
window.removeEventListener('storage', onStorage); window.removeEventListener('storage', onStorage);
if (syncDebounceRef.current) clearTimeout(syncDebounceRef.current); if (syncDebounceRef.current) clearTimeout(syncDebounceRef.current);
}; };
}, [scheduleSync]); }, [getFundCodesSignature, scheduleSync]);
const toggleFavorite = (code) => { const toggleFavorite = (code) => {
setFavorites(prev => { setFavorites(prev => {
@@ -3347,11 +3373,11 @@ export default function HomePage() {
const { data: upsertData, error: updateError } = await supabase const { data: upsertData, error: updateError } = await supabase
.from('user_configs') .from('user_configs')
.upsert( .upsert(
{ {
user_id: userId, user_id: userId,
data: payload, data: payload,
updated_at: now updated_at: now
}, },
{ onConflict: 'user_id' } { onConflict: 'user_id' }
) )
.select(); .select();
@@ -3360,7 +3386,7 @@ export default function HomePage() {
if (!upsertData || upsertData.length === 0) { if (!upsertData || upsertData.length === 0) {
throw new Error('同步失败:未写入任何数据,请检查账号状态或重新登录'); throw new Error('同步失败:未写入任何数据,请检查账号状态或重新登录');
} }
storageHelper.setItem('localUpdatedAt', now); storageHelper.setItem('localUpdatedAt', now);
if (showTip) { if (showTip) {
@@ -4938,9 +4964,9 @@ export default function HomePage() {
更新内容如下 更新内容如下
</p> </p>
{updateContent && ( {updateContent && (
<div style={{ <div style={{
background: 'rgba(0,0,0,0.2)', background: 'rgba(0,0,0,0.2)',
padding: '12px', padding: '12px',
borderRadius: '8px', borderRadius: '8px',
fontSize: '13px', fontSize: '13px',
lineHeight: '1.5', lineHeight: '1.5',
@@ -4954,16 +4980,16 @@ export default function HomePage() {
)} )}
</div> </div>
<div className="row" style={{ gap: 12 }}> <div className="row" style={{ gap: 12 }}>
<button <button
className="button secondary" className="button secondary"
onClick={() => setUpdateModalOpen(false)} onClick={() => setUpdateModalOpen(false)}
style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }} style={{ flex: 1, background: 'rgba(255,255,255,0.05)', color: 'var(--text)' }}
> >
取消 取消
</button> </button>
<button <button
className="button" className="button"
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
style={{ flex: 1, background: 'var(--success)', color: '#fff', border: 'none' }} style={{ flex: 1, background: 'var(--success)', color: '#fff', border: 'none' }}
> >
刷新浏览器 刷新浏览器
@@ -5078,7 +5104,7 @@ export default function HomePage() {
</div> </div>
</div> </div>
)} )}
{/* 全局轻提示 Toast */} {/* 全局轻提示 Toast */}
<AnimatePresence> <AnimatePresence>
{toast.show && ( {toast.show && (
@@ -5092,8 +5118,8 @@ export default function HomePage() {
left: '50%', left: '50%',
zIndex: 9999, zIndex: 9999,
padding: '10px 20px', padding: '10px 20px',
background: toast.type === 'error' ? 'rgba(239, 68, 68, 0.9)' : background: toast.type === 'error' ? 'rgba(239, 68, 68, 0.9)' :
toast.type === 'success' ? 'rgba(34, 197, 94, 0.9)' : toast.type === 'success' ? 'rgba(34, 197, 94, 0.9)' :
'rgba(30, 41, 59, 0.9)', 'rgba(30, 41, 59, 0.9)',
color: '#fff', color: '#fff',
borderRadius: '8px', borderRadius: '8px',