feat: 优化对 funds 的同步逻辑
This commit is contained in:
138
app/page.jsx
138
app/page.jsx
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user