From 374b34fcf566c346f4399fbbabdc0da936f3a162 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Sat, 7 Feb 2026 15:41:17 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/layout.jsx | 4 +- app/page.jsx | 197 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 175 insertions(+), 26 deletions(-) diff --git a/app/layout.jsx b/app/layout.jsx index 954bf9d..f729165 100644 --- a/app/layout.jsx +++ b/app/layout.jsx @@ -1,9 +1,9 @@ -import Script from 'next/script'; import './globals.css'; import AnalyticsGate from './components/AnalyticsGate'; +import packageJson from '../package.json'; export const metadata = { - title: '基估宝', + title: `基估宝 V${packageJson.version}`, description: '输入基金编号添加基金,实时显示估值与前10重仓' }; diff --git a/app/page.jsx b/app/page.jsx index 27f4b2c..134eee8 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -9,6 +9,7 @@ import zhifubaoImg from "./assets/zhifubao.jpg"; import weixinImg from "./assets/weixin.jpg"; import githubImg from "./assets/github.svg"; import { supabase } from './lib/supabase'; +import packageJson from '../package.json'; function PlusIcon(props) { return ( @@ -18,6 +19,16 @@ function PlusIcon(props) { ); } +function UpdateIcon(props) { + return ( + + + + + + ); +} + function TrashIcon(props) { return ( @@ -1187,14 +1198,15 @@ function SuccessModal({ message, onClose }) { ); } -function CloudConfigModal({ onConfirm, onCancel }) { +function CloudConfigModal({ onConfirm, onCancel, type = 'empty' }) { + const isConflict = type === 'conflict'; return (
- 云端暂无配置 + {isConflict ? '发现配置冲突' : '云端暂无配置'}
- + {!isConflict && ( + + )}

- 是否将本地配置同步到云端? + {isConflict + ? '检测到本地配置比云端更新,请选择操作:' + : '是否将本地配置同步到云端?'}

@@ -1837,6 +1853,34 @@ export default function HomePage() { } }, []); + // 检查更新 + const [hasUpdate, setHasUpdate] = useState(false); + const [latestVersion, setLatestVersion] = useState(''); + + useEffect(() => { + const checkUpdate = async () => { + try { + const res = await fetch('https://api.github.com/repos/hzm0321/real-time-fund/releases/latest'); + console.log(packageJson.version) + if (!res.ok) return; + const data = await res.json(); + if (data.tag_name) { + const remoteVersion = data.tag_name.replace(/^v/, ''); + if (remoteVersion !== packageJson.version) { + setHasUpdate(true); + setLatestVersion(remoteVersion); + } + } + } catch (e) { + console.error('Check update failed:', e); + } + }; + + checkUpdate(); + const interval = setInterval(checkUpdate, 10 * 60 * 1000); // 10 minutes + return () => clearInterval(interval); + }, []); + // 存储当前被划开的基金代码 const [swipedFundCode, setSwipedFundCode] = useState(null); @@ -2128,6 +2172,7 @@ export default function HomePage() { // 成功提示弹窗 const [successModal, setSuccessModal] = useState({ open: false, message: '' }); + const [updateModalOpen, setUpdateModalOpen] = useState(false); const [cloudConfigModal, setCloudConfigModal] = useState({ open: false, userId: null }); const syncDebounceRef = useRef(null); const lastSyncedRef = useRef(''); @@ -2169,14 +2214,27 @@ export default function HomePage() { localStorage.setItem = (key, value) => { originalSetItem(key, value); - if (keys.has(key)) scheduleSync(); + if (keys.has(key)) { + if (!skipSyncRef.current) { + originalSetItem('localUpdatedAt', new Date().toISOString()); + } + scheduleSync(); + } }; localStorage.removeItem = (key) => { originalRemoveItem(key); - if (keys.has(key)) scheduleSync(); + if (keys.has(key)) { + if (!skipSyncRef.current) { + originalSetItem('localUpdatedAt', new Date().toISOString()); + } + scheduleSync(); + } }; localStorage.clear = () => { originalClear(); + if (!skipSyncRef.current) { + originalSetItem('localUpdatedAt', new Date().toISOString()); + } scheduleSync(); }; @@ -2397,14 +2455,14 @@ export default function HomePage() { if (!incoming || typeof incoming !== 'object') return; const incomingComparable = getComparablePayload(incoming); if (!incomingComparable || incomingComparable === lastSyncedRef.current) return; - await applyCloudConfig(incoming); + await applyCloudConfig(incoming, payload.new.updated_at); }) .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'user_configs', filter: `user_id=eq.${user.id}` }, async (payload) => { const incoming = payload?.new?.data; if (!incoming || typeof incoming !== 'object') return; const incomingComparable = getComparablePayload(incoming); if (!incomingComparable || incomingComparable === lastSyncedRef.current) return; - await applyCloudConfig(incoming); + await applyCloudConfig(incoming, payload.new.updated_at); }) .subscribe(); return () => { @@ -2498,6 +2556,8 @@ export default function HomePage() { setUser(null); } catch (err) { console.error('登出失败', err); + setUserMenuOpen(false); + setUser(null); } }; @@ -3187,10 +3247,13 @@ export default function HomePage() { } }; - const applyCloudConfig = async (cloudData) => { + const applyCloudConfig = async (cloudData, cloudUpdatedAt) => { if (!cloudData || typeof cloudData !== 'object') return; skipSyncRef.current = true; try { + if (cloudUpdatedAt) { + localStorage.setItem('localUpdatedAt', new Date(cloudUpdatedAt).toISOString()); + } const nextFunds = Array.isArray(cloudData.funds) ? dedupeByCode(cloudData.funds) : []; setFunds(nextFunds); localStorage.setItem('funds', JSON.stringify(nextFunds)); @@ -3237,7 +3300,7 @@ export default function HomePage() { try { const { data, error } = await supabase .from('user_configs') - .select('id, data') + .select('id, data, updated_at') .eq('user_id', userId) .maybeSingle(); if (error) throw error; @@ -3246,14 +3309,22 @@ export default function HomePage() { .from('user_configs') .insert({ user_id: userId }); if (insertError) throw insertError; - setCloudConfigModal({ open: true, userId }); + setCloudConfigModal({ open: true, userId, type: 'empty' }); return; } if (data?.data && typeof data.data === 'object' && Object.keys(data.data).length > 0) { - await applyCloudConfig(data.data); + 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; } - setCloudConfigModal({ open: true, userId }); + setCloudConfigModal({ open: true, userId, type: 'empty' }); } catch (e) { console.error('获取云端配置失败', e); } @@ -3263,11 +3334,21 @@ export default function HomePage() { if (!userId) return; try { const payload = collectLocalPayload(); + const now = new Date().toISOString(); const { error: updateError } = await supabase .from('user_configs') - .update({ data: payload, updated_at: new Date().toISOString() }) - .eq('user_id', userId); + .upsert( + { + user_id: userId, + data: payload, + updated_at: now + }, + { onConflict: 'user_id' } + ); if (updateError) throw updateError; + + localStorage.setItem('localUpdatedAt', now); + if (showTip) { setSuccessModal({ open: true, message: '已同步云端配置' }); } @@ -3440,7 +3521,8 @@ export default function HomePage() { tradeModal.open || !!clearConfirm || donateOpen || - !!fundDeleteConfirm; + !!fundDeleteConfirm || + updateModalOpen; if (isAnyModalOpen) { document.body.style.overflow = 'hidden'; @@ -3465,7 +3547,8 @@ export default function HomePage() { actionModal.open, tradeModal.open, clearConfirm, - donateOpen + donateOpen, + updateModalOpen ]); useEffect(() => { @@ -3497,6 +3580,16 @@ export default function HomePage() {
项目Github地址 window.open("https://github.com/hzm0321/real-time-fund")} /> + {hasUpdate && ( +
setUpdateModalOpen(true)} + > + +
+ )}
刷新 {Math.round(refreshMs / 1000)}秒 @@ -4636,8 +4729,14 @@ export default function HomePage() { {cloudConfigModal.open && ( setCloudConfigModal({ open: false, userId: null })} + onCancel={() => { + if (cloudConfigModal.type === 'conflict' && cloudConfigModal.cloudData) { + applyCloudConfig(cloudConfigModal.cloudData); + } + setCloudConfigModal({ open: false, userId: null }); + }} /> )} @@ -4712,6 +4811,56 @@ export default function HomePage() {
)} + {/* 更新提示弹窗 */} + + {updateModalOpen && ( + setUpdateModalOpen(false)} + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + style={{ zIndex: 10002 }} + > + e.stopPropagation()} + > +
+ + 更新提示 +
+

+ 检测到新版本,是否刷新浏览器以更新 +

+
+ + +
+
+
+ )} +
+ {/* 登录模态框 */} {loginModalOpen && (