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