Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28352e87c1 | ||
|
|
ed5fd98c7c | ||
|
|
003271a684 | ||
|
|
7772de1acf | ||
|
|
4d4b931e30 | ||
|
|
406f14150d | ||
|
|
2964cb2318 | ||
|
|
ff4a10c84d | ||
|
|
d69bf547ac | ||
|
|
fe5577265a | ||
|
|
e7b28dfb30 |
304
app/page.jsx
304
app/page.jsx
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState, useMemo, useLayoutEffect } from 'react';
|
||||
import { useEffect, useRef, useState, useMemo, useLayoutEffect, useCallback } from 'react';
|
||||
import { motion, AnimatePresence, Reorder } from 'framer-motion';
|
||||
import { createAvatar } from '@dicebear/core';
|
||||
import { glass } from '@dicebear/collection';
|
||||
@@ -345,7 +345,7 @@ function DatePicker({ value, onChange }) {
|
||||
}
|
||||
|
||||
function DonateTabs() {
|
||||
const [method, setMethod] = useState('alipay'); // alipay, wechat
|
||||
const [method, setMethod] = useState('wechat'); // alipay, wechat
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
|
||||
@@ -1625,7 +1625,7 @@ function CountUp({ value, prefix = '', suffix = '', decimals = 2, className = ''
|
||||
|
||||
const start = previousValue.current;
|
||||
const end = value;
|
||||
const duration = 1000; // 1秒动画
|
||||
const duration = 600; // 0.6秒动画
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime) => {
|
||||
@@ -1768,6 +1768,7 @@ export default function HomePage() {
|
||||
const [error, setError] = useState('');
|
||||
const timerRef = useRef(null);
|
||||
const refreshingRef = useRef(false);
|
||||
const isLoggingOutRef = useRef(false);
|
||||
|
||||
// 刷新频率状态
|
||||
const [refreshMs, setRefreshMs] = useState(30000);
|
||||
@@ -1857,6 +1858,7 @@ export default function HomePage() {
|
||||
const [hasUpdate, setHasUpdate] = useState(false);
|
||||
const [latestVersion, setLatestVersion] = useState('');
|
||||
const [updateContent, setUpdateContent] = useState('');
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkUpdate = async () => {
|
||||
@@ -2084,7 +2086,7 @@ export default function HomePage() {
|
||||
} else {
|
||||
next[code] = data;
|
||||
}
|
||||
localStorage.setItem('holdings', JSON.stringify(next));
|
||||
storageHelper.setItem('holdings', JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
setHoldingModal({ open: false, fund: null });
|
||||
@@ -2173,6 +2175,18 @@ export default function HomePage() {
|
||||
|
||||
// 成功提示弹窗
|
||||
const [successModal, setSuccessModal] = useState({ open: false, message: '' });
|
||||
// 轻提示 (Toast)
|
||||
const [toast, setToast] = useState({ show: false, message: '', type: 'info' }); // type: 'info' | 'success' | 'error'
|
||||
const toastTimeoutRef = useRef(null);
|
||||
|
||||
const showToast = (message, type = 'info') => {
|
||||
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
|
||||
setToast({ show: true, message, type });
|
||||
toastTimeoutRef.current = setTimeout(() => {
|
||||
setToast((prev) => ({ ...prev, show: false }));
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const [updateModalOpen, setUpdateModalOpen] = useState(false);
|
||||
const [cloudConfigModal, setCloudConfigModal] = useState({ open: false, userId: null });
|
||||
const syncDebounceRef = useRef(null);
|
||||
@@ -2194,9 +2208,7 @@ export default function HomePage() {
|
||||
userIdRef.current = user?.id || null;
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings']);
|
||||
const scheduleSync = () => {
|
||||
const scheduleSync = useCallback(() => {
|
||||
if (!userIdRef.current) return;
|
||||
if (skipSyncRef.current) return;
|
||||
if (syncDebounceRef.current) clearTimeout(syncDebounceRef.current);
|
||||
@@ -2207,51 +2219,48 @@ export default function HomePage() {
|
||||
lastSyncedRef.current = next;
|
||||
syncUserConfig(userIdRef.current, false);
|
||||
}, 2000);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const originalSetItem = localStorage.setItem.bind(localStorage);
|
||||
const originalRemoveItem = localStorage.removeItem.bind(localStorage);
|
||||
const originalClear = localStorage.clear.bind(localStorage);
|
||||
|
||||
localStorage.setItem = (key, value) => {
|
||||
originalSetItem(key, value);
|
||||
const storageHelper = useMemo(() => {
|
||||
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings']);
|
||||
const triggerSync = (key) => {
|
||||
if (keys.has(key)) {
|
||||
if (!skipSyncRef.current) {
|
||||
originalSetItem('localUpdatedAt', new Date().toISOString());
|
||||
window.localStorage.setItem('localUpdatedAt', new Date().toISOString());
|
||||
}
|
||||
scheduleSync();
|
||||
}
|
||||
};
|
||||
localStorage.removeItem = (key) => {
|
||||
originalRemoveItem(key);
|
||||
if (keys.has(key)) {
|
||||
return {
|
||||
setItem: (key, value) => {
|
||||
window.localStorage.setItem(key, value);
|
||||
triggerSync(key);
|
||||
},
|
||||
removeItem: (key) => {
|
||||
window.localStorage.removeItem(key);
|
||||
triggerSync(key);
|
||||
},
|
||||
clear: () => {
|
||||
window.localStorage.clear();
|
||||
if (!skipSyncRef.current) {
|
||||
originalSetItem('localUpdatedAt', new Date().toISOString());
|
||||
window.localStorage.setItem('localUpdatedAt', new Date().toISOString());
|
||||
}
|
||||
scheduleSync();
|
||||
}
|
||||
};
|
||||
localStorage.clear = () => {
|
||||
originalClear();
|
||||
if (!skipSyncRef.current) {
|
||||
originalSetItem('localUpdatedAt', new Date().toISOString());
|
||||
}
|
||||
scheduleSync();
|
||||
};
|
||||
}, [scheduleSync]);
|
||||
|
||||
useEffect(() => {
|
||||
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings']);
|
||||
const onStorage = (e) => {
|
||||
if (!e.key || keys.has(e.key)) scheduleSync();
|
||||
};
|
||||
window.addEventListener('storage', onStorage);
|
||||
|
||||
return () => {
|
||||
localStorage.setItem = originalSetItem;
|
||||
localStorage.removeItem = originalRemoveItem;
|
||||
localStorage.clear = originalClear;
|
||||
window.removeEventListener('storage', onStorage);
|
||||
if (syncDebounceRef.current) clearTimeout(syncDebounceRef.current);
|
||||
};
|
||||
}, []);
|
||||
}, [scheduleSync]);
|
||||
|
||||
const toggleFavorite = (code) => {
|
||||
setFavorites(prev => {
|
||||
@@ -2261,7 +2270,7 @@ export default function HomePage() {
|
||||
} else {
|
||||
next.add(code);
|
||||
}
|
||||
localStorage.setItem('favorites', JSON.stringify(Array.from(next)));
|
||||
storageHelper.setItem('favorites', JSON.stringify(Array.from(next)));
|
||||
if (next.size === 0) setCurrentTab('all');
|
||||
return next;
|
||||
});
|
||||
@@ -2276,7 +2285,7 @@ export default function HomePage() {
|
||||
next.add(code);
|
||||
}
|
||||
// 同步到本地存储
|
||||
localStorage.setItem('collapsedCodes', JSON.stringify(Array.from(next)));
|
||||
storageHelper.setItem('collapsedCodes', JSON.stringify(Array.from(next)));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -2289,7 +2298,7 @@ export default function HomePage() {
|
||||
};
|
||||
const next = [...groups, newGroup];
|
||||
setGroups(next);
|
||||
localStorage.setItem('groups', JSON.stringify(next));
|
||||
storageHelper.setItem('groups', JSON.stringify(next));
|
||||
setCurrentTab(newGroup.id);
|
||||
setGroupModalOpen(false);
|
||||
};
|
||||
@@ -2297,13 +2306,13 @@ export default function HomePage() {
|
||||
const handleRemoveGroup = (id) => {
|
||||
const next = groups.filter(g => g.id !== id);
|
||||
setGroups(next);
|
||||
localStorage.setItem('groups', JSON.stringify(next));
|
||||
storageHelper.setItem('groups', JSON.stringify(next));
|
||||
if (currentTab === id) setCurrentTab('all');
|
||||
};
|
||||
|
||||
const handleUpdateGroups = (newGroups) => {
|
||||
setGroups(newGroups);
|
||||
localStorage.setItem('groups', JSON.stringify(newGroups));
|
||||
storageHelper.setItem('groups', JSON.stringify(newGroups));
|
||||
// 如果当前选中的分组被删除了,切换回“全部”
|
||||
if (currentTab !== 'all' && currentTab !== 'fav' && !newGroups.find(g => g.id === currentTab)) {
|
||||
setCurrentTab('all');
|
||||
@@ -2322,7 +2331,7 @@ export default function HomePage() {
|
||||
return g;
|
||||
});
|
||||
setGroups(next);
|
||||
localStorage.setItem('groups', JSON.stringify(next));
|
||||
storageHelper.setItem('groups', JSON.stringify(next));
|
||||
setAddFundToGroupOpen(false);
|
||||
setSuccessModal({ open: true, message: `成功添加 ${codes.length} 支基金` });
|
||||
};
|
||||
@@ -2338,7 +2347,7 @@ export default function HomePage() {
|
||||
return g;
|
||||
});
|
||||
setGroups(next);
|
||||
localStorage.setItem('groups', JSON.stringify(next));
|
||||
storageHelper.setItem('groups', JSON.stringify(next));
|
||||
};
|
||||
|
||||
const toggleFundInGroup = (code, groupId) => {
|
||||
@@ -2353,7 +2362,7 @@ export default function HomePage() {
|
||||
return g;
|
||||
});
|
||||
setGroups(next);
|
||||
localStorage.setItem('groups', JSON.stringify(next));
|
||||
storageHelper.setItem('groups', JSON.stringify(next));
|
||||
};
|
||||
|
||||
// 按 code 去重,保留第一次出现的项,避免列表重复
|
||||
@@ -2373,7 +2382,7 @@ export default function HomePage() {
|
||||
if (Array.isArray(saved) && saved.length) {
|
||||
const deduped = dedupeByCode(saved);
|
||||
setFunds(deduped);
|
||||
localStorage.setItem('funds', JSON.stringify(deduped));
|
||||
storageHelper.setItem('funds', JSON.stringify(deduped));
|
||||
const codes = Array.from(new Set(deduped.map((f) => f.code)));
|
||||
if (codes.length) refreshAll(codes);
|
||||
}
|
||||
@@ -2414,12 +2423,37 @@ export default function HomePage() {
|
||||
|
||||
const handleSession = async (session, event) => {
|
||||
if (!session?.user) {
|
||||
if (event === 'SIGNED_OUT' && !isLoggingOutRef.current) {
|
||||
setLoginError('会话已过期,请重新登录');
|
||||
setLoginModalOpen(true);
|
||||
}
|
||||
isLoggingOutRef.current = false;
|
||||
clearAuthState();
|
||||
return;
|
||||
}
|
||||
if (session.expires_at && session.expires_at * 1000 <= Date.now()) {
|
||||
isLoggingOutRef.current = true;
|
||||
await supabase.auth.signOut({ scope: 'local' });
|
||||
try {
|
||||
const storageKeys = Object.keys(localStorage);
|
||||
storageKeys.forEach((key) => {
|
||||
if (key === 'supabase.auth.token' || (key.startsWith('sb-') && key.endsWith('-auth-token'))) {
|
||||
storageHelper.removeItem(key);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
try {
|
||||
const sessionKeys = Object.keys(sessionStorage);
|
||||
sessionKeys.forEach((key) => {
|
||||
if (key === 'supabase.auth.token' || (key.startsWith('sb-') && key.endsWith('-auth-token'))) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
clearAuthState();
|
||||
setLoginError('会话已过期,请重新登录');
|
||||
showToast('会话已过期,请重新登录', 'error');
|
||||
setLoginModalOpen(true);
|
||||
return;
|
||||
}
|
||||
setUser(session.user);
|
||||
@@ -2539,24 +2573,43 @@ export default function HomePage() {
|
||||
|
||||
// 登出
|
||||
const handleLogout = async () => {
|
||||
isLoggingOutRef.current = true;
|
||||
try {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
if (!session) {
|
||||
await supabase.auth.signOut({ scope: 'local' });
|
||||
setUserMenuOpen(false);
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error?.code === 'session_not_found') {
|
||||
await supabase.auth.signOut({ scope: 'local' });
|
||||
} else if (error) {
|
||||
if (session) {
|
||||
const { error } = await supabase.auth.signOut({ scope: 'local' });
|
||||
if (error && error.code !== 'session_not_found') {
|
||||
throw error;
|
||||
}
|
||||
setUserMenuOpen(false);
|
||||
setUser(null);
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error')
|
||||
console.error('登出失败', err);
|
||||
} finally {
|
||||
try {
|
||||
await supabase.auth.signOut({ scope: 'local' });
|
||||
} catch { }
|
||||
try {
|
||||
const storageKeys = Object.keys(localStorage);
|
||||
storageKeys.forEach((key) => {
|
||||
if (key === 'supabase.auth.token' || (key.startsWith('sb-') && key.endsWith('-auth-token'))) {
|
||||
storageHelper.removeItem(key);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
try {
|
||||
const sessionKeys = Object.keys(sessionStorage);
|
||||
sessionKeys.forEach((key) => {
|
||||
if (key === 'supabase.auth.token' || (key.startsWith('sb-') && key.endsWith('-auth-token'))) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
setLoginModalOpen(false);
|
||||
setLoginError('');
|
||||
setLoginSuccess('');
|
||||
setLoginEmail('');
|
||||
setLoginOtp('');
|
||||
setUserMenuOpen(false);
|
||||
setUser(null);
|
||||
}
|
||||
@@ -2976,7 +3029,7 @@ export default function HomePage() {
|
||||
if (newFunds.length > 0) {
|
||||
const updated = dedupeByCode([...newFunds, ...funds]);
|
||||
setFunds(updated);
|
||||
localStorage.setItem('funds', JSON.stringify(updated));
|
||||
storageHelper.setItem('funds', JSON.stringify(updated));
|
||||
}
|
||||
|
||||
setSelectedFunds([]);
|
||||
@@ -3024,7 +3077,7 @@ export default function HomePage() {
|
||||
}
|
||||
});
|
||||
const deduped = dedupeByCode(merged);
|
||||
localStorage.setItem('funds', JSON.stringify(deduped));
|
||||
storageHelper.setItem('funds', JSON.stringify(deduped));
|
||||
return deduped;
|
||||
});
|
||||
}
|
||||
@@ -3086,7 +3139,7 @@ export default function HomePage() {
|
||||
} else {
|
||||
const next = dedupeByCode([...newFunds, ...funds]);
|
||||
setFunds(next);
|
||||
localStorage.setItem('funds', JSON.stringify(next));
|
||||
storageHelper.setItem('funds', JSON.stringify(next));
|
||||
}
|
||||
setSearchTerm('');
|
||||
setSelectedFunds([]);
|
||||
@@ -3105,7 +3158,7 @@ export default function HomePage() {
|
||||
const removeFund = (removeCode) => {
|
||||
const next = funds.filter((f) => f.code !== removeCode);
|
||||
setFunds(next);
|
||||
localStorage.setItem('funds', JSON.stringify(next));
|
||||
storageHelper.setItem('funds', JSON.stringify(next));
|
||||
|
||||
// 同步删除分组中的失效代码
|
||||
const nextGroups = groups.map(g => ({
|
||||
@@ -3113,14 +3166,14 @@ export default function HomePage() {
|
||||
codes: g.codes.filter(c => c !== removeCode)
|
||||
}));
|
||||
setGroups(nextGroups);
|
||||
localStorage.setItem('groups', JSON.stringify(nextGroups));
|
||||
storageHelper.setItem('groups', JSON.stringify(nextGroups));
|
||||
|
||||
// 同步删除展开收起状态
|
||||
setCollapsedCodes(prev => {
|
||||
if (!prev.has(removeCode)) return prev;
|
||||
const nextSet = new Set(prev);
|
||||
nextSet.delete(removeCode);
|
||||
localStorage.setItem('collapsedCodes', JSON.stringify(Array.from(nextSet)));
|
||||
storageHelper.setItem('collapsedCodes', JSON.stringify(Array.from(nextSet)));
|
||||
return nextSet;
|
||||
});
|
||||
|
||||
@@ -3129,7 +3182,7 @@ export default function HomePage() {
|
||||
if (!prev.has(removeCode)) return prev;
|
||||
const nextSet = new Set(prev);
|
||||
nextSet.delete(removeCode);
|
||||
localStorage.setItem('favorites', JSON.stringify(Array.from(nextSet)));
|
||||
storageHelper.setItem('favorites', JSON.stringify(Array.from(nextSet)));
|
||||
if (nextSet.size === 0) setCurrentTab('all');
|
||||
return nextSet;
|
||||
});
|
||||
@@ -3139,7 +3192,7 @@ export default function HomePage() {
|
||||
if (!prev[removeCode]) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[removeCode];
|
||||
localStorage.setItem('holdings', JSON.stringify(next));
|
||||
storageHelper.setItem('holdings', JSON.stringify(next));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -3155,7 +3208,7 @@ export default function HomePage() {
|
||||
e?.preventDefault?.();
|
||||
const ms = Math.max(10, Number(tempSeconds)) * 1000;
|
||||
setRefreshMs(ms);
|
||||
localStorage.setItem('refreshMs', String(ms));
|
||||
storageHelper.setItem('refreshMs', String(ms));
|
||||
setSettingsOpen(false);
|
||||
};
|
||||
|
||||
@@ -3251,28 +3304,28 @@ export default function HomePage() {
|
||||
skipSyncRef.current = true;
|
||||
try {
|
||||
if (cloudUpdatedAt) {
|
||||
localStorage.setItem('localUpdatedAt', new Date(cloudUpdatedAt).toISOString());
|
||||
storageHelper.setItem('localUpdatedAt', new Date(cloudUpdatedAt).toISOString());
|
||||
}
|
||||
const nextFunds = Array.isArray(cloudData.funds) ? dedupeByCode(cloudData.funds) : [];
|
||||
setFunds(nextFunds);
|
||||
localStorage.setItem('funds', JSON.stringify(nextFunds));
|
||||
storageHelper.setItem('funds', JSON.stringify(nextFunds));
|
||||
|
||||
const nextFavorites = Array.isArray(cloudData.favorites) ? cloudData.favorites : [];
|
||||
setFavorites(new Set(nextFavorites));
|
||||
localStorage.setItem('favorites', JSON.stringify(nextFavorites));
|
||||
storageHelper.setItem('favorites', JSON.stringify(nextFavorites));
|
||||
|
||||
const nextGroups = Array.isArray(cloudData.groups) ? cloudData.groups : [];
|
||||
setGroups(nextGroups);
|
||||
localStorage.setItem('groups', JSON.stringify(nextGroups));
|
||||
storageHelper.setItem('groups', JSON.stringify(nextGroups));
|
||||
|
||||
const nextCollapsed = Array.isArray(cloudData.collapsedCodes) ? cloudData.collapsedCodes : [];
|
||||
setCollapsedCodes(new Set(nextCollapsed));
|
||||
localStorage.setItem('collapsedCodes', JSON.stringify(nextCollapsed));
|
||||
storageHelper.setItem('collapsedCodes', JSON.stringify(nextCollapsed));
|
||||
|
||||
const nextRefreshMs = Number.isFinite(cloudData.refreshMs) && cloudData.refreshMs >= 5000 ? cloudData.refreshMs : 30000;
|
||||
setRefreshMs(nextRefreshMs);
|
||||
setTempSeconds(Math.round(nextRefreshMs / 1000));
|
||||
localStorage.setItem('refreshMs', String(nextRefreshMs));
|
||||
storageHelper.setItem('refreshMs', String(nextRefreshMs));
|
||||
|
||||
if (cloudData.viewMode === 'card' || cloudData.viewMode === 'list') {
|
||||
setViewMode(cloudData.viewMode);
|
||||
@@ -3280,7 +3333,7 @@ export default function HomePage() {
|
||||
|
||||
const nextHoldings = cloudData.holdings && typeof cloudData.holdings === 'object' ? cloudData.holdings : {};
|
||||
setHoldings(nextHoldings);
|
||||
localStorage.setItem('holdings', JSON.stringify(nextHoldings));
|
||||
storageHelper.setItem('holdings', JSON.stringify(nextHoldings));
|
||||
|
||||
if (nextFunds.length) {
|
||||
const codes = Array.from(new Set(nextFunds.map((f) => f.code)));
|
||||
@@ -3317,14 +3370,11 @@ export default function HomePage() {
|
||||
const cloudComparable = getComparablePayload(data.data);
|
||||
|
||||
if (localComparable !== cloudComparable) {
|
||||
const cloudTime = new Date(data.updated_at || 0).getTime();
|
||||
const localTime = new Date(localStorage.getItem('localUpdatedAt') || 0).getTime();
|
||||
|
||||
if (localTime > cloudTime + 2000) {
|
||||
// 如果数据不一致,无论时间戳如何,都提示用户
|
||||
// 用户可以选择使用本地数据覆盖云端,或者使用云端数据覆盖本地
|
||||
setCloudConfigModal({ open: true, userId, type: 'conflict', cloudData: data.data });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await applyCloudConfig(data.data, data.updated_at);
|
||||
return;
|
||||
@@ -3336,11 +3386,15 @@ export default function HomePage() {
|
||||
};
|
||||
|
||||
const syncUserConfig = async (userId, showTip = true) => {
|
||||
if (!userId) return;
|
||||
if (!userId) {
|
||||
showToast(`userId 不存在,请重新登录`, 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsSyncing(true);
|
||||
const payload = collectLocalPayload();
|
||||
const now = new Date().toISOString();
|
||||
const { error: updateError } = await supabase
|
||||
const { data: upsertData, error: updateError } = await supabase
|
||||
.from('user_configs')
|
||||
.upsert(
|
||||
{
|
||||
@@ -3349,16 +3403,24 @@ export default function HomePage() {
|
||||
updated_at: now
|
||||
},
|
||||
{ onConflict: 'user_id' }
|
||||
);
|
||||
if (updateError) throw updateError;
|
||||
)
|
||||
.select();
|
||||
|
||||
localStorage.setItem('localUpdatedAt', now);
|
||||
if (updateError) throw updateError;
|
||||
if (!upsertData || upsertData.length === 0) {
|
||||
throw new Error('同步失败:未写入任何数据,请检查账号状态或重新登录');
|
||||
}
|
||||
|
||||
storageHelper.setItem('localUpdatedAt', now);
|
||||
|
||||
if (showTip) {
|
||||
setSuccessModal({ open: true, message: '已同步云端配置' });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('同步云端配置异常', e);
|
||||
showToast(`同步云端配置异常:${e}`, 'error');
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3440,13 +3502,13 @@ export default function HomePage() {
|
||||
appendedCodes = newItems.map(f => f.code);
|
||||
mergedFunds = [...currentFunds, ...newItems];
|
||||
setFunds(mergedFunds);
|
||||
localStorage.setItem('funds', JSON.stringify(mergedFunds));
|
||||
storageHelper.setItem('funds', JSON.stringify(mergedFunds));
|
||||
}
|
||||
|
||||
if (Array.isArray(data.favorites)) {
|
||||
const mergedFav = Array.from(new Set([...currentFavorites, ...data.favorites]));
|
||||
setFavorites(new Set(mergedFav));
|
||||
localStorage.setItem('favorites', JSON.stringify(mergedFav));
|
||||
storageHelper.setItem('favorites', JSON.stringify(mergedFav));
|
||||
}
|
||||
|
||||
if (Array.isArray(data.groups)) {
|
||||
@@ -3464,19 +3526,19 @@ export default function HomePage() {
|
||||
}
|
||||
});
|
||||
setGroups(mergedGroups);
|
||||
localStorage.setItem('groups', JSON.stringify(mergedGroups));
|
||||
storageHelper.setItem('groups', JSON.stringify(mergedGroups));
|
||||
}
|
||||
|
||||
if (Array.isArray(data.collapsedCodes)) {
|
||||
const mergedCollapsed = Array.from(new Set([...currentCollapsed, ...data.collapsedCodes]));
|
||||
setCollapsedCodes(new Set(mergedCollapsed));
|
||||
localStorage.setItem('collapsedCodes', JSON.stringify(mergedCollapsed));
|
||||
storageHelper.setItem('collapsedCodes', JSON.stringify(mergedCollapsed));
|
||||
}
|
||||
|
||||
if (typeof data.refreshMs === 'number' && data.refreshMs >= 5000) {
|
||||
setRefreshMs(data.refreshMs);
|
||||
setTempSeconds(Math.round(data.refreshMs / 1000));
|
||||
localStorage.setItem('refreshMs', String(data.refreshMs));
|
||||
storageHelper.setItem('refreshMs', String(data.refreshMs));
|
||||
}
|
||||
if (data.viewMode === 'card' || data.viewMode === 'list') {
|
||||
setViewMode(data.viewMode);
|
||||
@@ -3485,7 +3547,7 @@ export default function HomePage() {
|
||||
if (data.holdings && typeof data.holdings === 'object') {
|
||||
const mergedHoldings = { ...JSON.parse(localStorage.getItem('holdings') || '{}'), ...data.holdings };
|
||||
setHoldings(mergedHoldings);
|
||||
localStorage.setItem('holdings', JSON.stringify(mergedHoldings));
|
||||
storageHelper.setItem('holdings', JSON.stringify(mergedHoldings));
|
||||
}
|
||||
|
||||
// 导入成功后,仅刷新新追加的基金
|
||||
@@ -3580,6 +3642,35 @@ export default function HomePage() {
|
||||
<path d="M5 14c2-4 7-6 14-5" stroke="var(--primary)" strokeWidth="2" />
|
||||
</svg>
|
||||
<span>基估宝</span>
|
||||
<AnimatePresence>
|
||||
{isSyncing && (
|
||||
<motion.div
|
||||
key="sync-icon"
|
||||
initial={{ opacity: 0, width: 0, marginLeft: 0 }}
|
||||
animate={{ opacity: 1, width: 'auto', marginLeft: 8 }}
|
||||
exit={{ opacity: 0, width: 0, marginLeft: 0 }}
|
||||
style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}
|
||||
title="正在同步到云端..."
|
||||
>
|
||||
<motion.svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
animate={{ opacity: [0.5, 1, 0.5] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
|
||||
>
|
||||
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242" stroke="var(--primary)" />
|
||||
<path d="M12 12v9" stroke="var(--accent)" />
|
||||
<path d="m16 16-4-4-4 4" stroke="var(--accent)" />
|
||||
</motion.svg>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{hasUpdate && (
|
||||
@@ -4997,6 +5088,51 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 全局轻提示 Toast */}
|
||||
<AnimatePresence>
|
||||
{toast.show && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20, x: '-50%' }}
|
||||
animate={{ opacity: 1, y: 0, x: '-50%' }}
|
||||
exit={{ opacity: 0, y: -20, x: '-50%' }}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 24,
|
||||
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)' :
|
||||
'rgba(30, 41, 59, 0.9)',
|
||||
color: '#fff',
|
||||
borderRadius: '8px',
|
||||
backdropFilter: 'blur(8px)',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
maxWidth: '90vw',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
{toast.type === 'error' && (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2"/>
|
||||
<path d="M12 8v4M12 16h.01" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
)}
|
||||
{toast.type === 'success' && (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6L9 17l-5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
{toast.message}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-time-fund",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user