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 [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',
|
||||
|
||||
Reference in New Issue
Block a user