feat:临时隐藏登录入口,修复当日盈亏的显示问题

This commit is contained in:
hzm
2026-02-07 00:22:21 +08:00
parent 30f34d2cff
commit 1cb590beee
4 changed files with 447 additions and 53 deletions

View File

@@ -1,10 +1,10 @@
'use client';
import { useLayoutEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import Script from 'next/script';
export default function AnalyticsGate({ GA_ID }) {
const [enabled, setEnabled] = useState(false);
useLayoutEffect(() => {
useEffect(() => {
try {
const href = window.location.href || '';
setEnabled(href.includes('hzm0321'));

View File

@@ -1309,17 +1309,6 @@ input[type="number"] {
/* ========== 移动端响应式 ========== */
@media (max-width: 640px) {
.user-menu-dropdown {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
min-width: 100%;
border-radius: 20px 20px 0 0;
padding: 16px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
.user-menu-header {
padding: 16px 8px;

View File

@@ -2,7 +2,7 @@ import { createClient } from '@supabase/supabase-js';
// Supabase 配置
// 注意:此处使用 publishable key可安全在客户端使用
const supabaseUrl = 'https://mouvsqlmgymsaxikvqsh.supabase.co/auth/v1/otp';
const supabaseUrl = 'https://mouvsqlmgymsaxikvqsh.supabase.co';
const supabaseAnonKey = 'sb_publishable_c5f58knbVz8UgOh6L88MUQ_p9j8c1Q-';
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {

View File

@@ -36,6 +36,14 @@ function SettingsIcon(props) {
);
}
function CloudIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M20 17.5a4.5 4.5 0 0 0-1.5-8.77A6 6 0 1 0 6 16.5H18a3.5 3.5 0 0 0 2-6.4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function RefreshIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
@@ -1177,6 +1185,51 @@ function SuccessModal({ message, onClose }) {
);
}
function CloudConfigModal({ onConfirm, onCancel }) {
return (
<motion.div
className="modal-overlay"
role="dialog"
aria-modal="true"
aria-label="云端同步提示"
onClick={onCancel}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal"
style={{ maxWidth: '420px' }}
onClick={(e) => e.stopPropagation()}
>
<div className="title" style={{ marginBottom: 12, justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<CloudIcon width="20" height="20" />
<span>云端暂无配置</span>
</div>
<button className="icon-button" onClick={onCancel} style={{ border: 'none', background: 'transparent' }}>
<CloseIcon width="20" height="20" />
</button>
</div>
<p className="muted" style={{ marginBottom: 20, fontSize: '14px', lineHeight: '1.6' }}>
是否将本地配置同步到云端
</p>
<div className="row" style={{ flexDirection: 'column', gap: 12 }}>
<button className="button" onClick={onConfirm}>
同步本地到云端
</button>
<button className="button secondary" onClick={onCancel}>
暂不同步
</button>
</div>
</motion.div>
</motion.div>
);
}
function ConfirmModal({ title, message, onConfirm, onCancel, confirmText = "确定删除" }) {
return (
<motion.div
@@ -1733,6 +1786,7 @@ export default function HomePage() {
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState('');
const [loginSuccess, setLoginSuccess] = useState('');
const [loginOtp, setLoginOtp] = useState('');
// 反馈弹窗状态
const [feedbackOpen, setFeedbackOpen] = useState(false);
@@ -1763,6 +1817,7 @@ export default function HomePage() {
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const [isMobile, setIsMobile] = useState(false);
const [logoutConfirmOpen, setLogoutConfirmOpen] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined') {
const checkMobile = () => setIsMobile(window.innerWidth <= 640);
@@ -1863,6 +1918,8 @@ export default function HomePage() {
const now = new Date();
const isAfter9 = now.getHours() >= 9;
const hasTodayData = fund.jzrq === todayStr;
const hasTodayValuation = typeof fund.gztime === 'string' && fund.gztime.startsWith(todayStr);
const canCalcTodayProfit = hasTodayData || hasTodayValuation;
// 如果是交易日且9点以后且今日净值未出则强制使用估值隐藏涨跌幅列模式
const useValuation = isTradingDay && isAfter9 && !hasTodayData;
@@ -1875,10 +1932,14 @@ export default function HomePage() {
currentNav = Number(fund.dwjz);
if (!currentNav) return null;
const amount = holding.share * currentNav;
// 优先用 zzl (真实涨跌幅), 降级用 gszzl
const rate = fund.zzl !== undefined ? Number(fund.zzl) : (Number(fund.gszzl) || 0);
profitToday = amount - (amount / (1 + rate / 100));
if (canCalcTodayProfit) {
const amount = holding.share * currentNav;
// 优先用 zzl (真实涨跌幅), 降级用 gszzl
const rate = fund.zzl !== undefined ? Number(fund.zzl) : (Number(fund.gszzl) || 0);
profitToday = amount - (amount / (1 + rate / 100));
} else {
profitToday = null;
}
} else {
// 否则使用估值
currentNav = fund.estPricedCoverage > 0.05
@@ -1887,10 +1948,14 @@ export default function HomePage() {
if (!currentNav) return null;
const amount = holding.share * currentNav;
// 估值涨跌幅
const gzChange = fund.estPricedCoverage > 0.05 ? fund.estGszzl : (Number(fund.gszzl) || 0);
profitToday = amount - (amount / (1 + gzChange / 100));
if (canCalcTodayProfit) {
const amount = holding.share * currentNav;
// 估值涨跌幅
const gzChange = fund.estPricedCoverage > 0.05 ? fund.estGszzl : (Number(fund.gszzl) || 0);
profitToday = amount - (amount / (1 + gzChange / 100));
} else {
profitToday = null;
}
}
// 持仓金额
@@ -2053,6 +2118,11 @@ export default function HomePage() {
// 成功提示弹窗
const [successModal, setSuccessModal] = useState({ open: false, message: '' });
const [cloudConfigModal, setCloudConfigModal] = useState({ open: false, userId: null });
const syncDebounceRef = useRef(null);
const lastSyncedRef = useRef('');
const skipSyncRef = useRef(false);
const userIdRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
@@ -2064,6 +2134,56 @@ export default function HomePage() {
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
userIdRef.current = user?.id || null;
}, [user]);
useEffect(() => {
const keys = new Set(['funds', 'favorites', 'groups', 'collapsedCodes', 'refreshMs', 'holdings']);
const scheduleSync = () => {
if (!userIdRef.current) return;
if (skipSyncRef.current) return;
if (syncDebounceRef.current) clearTimeout(syncDebounceRef.current);
syncDebounceRef.current = setTimeout(() => {
const payload = collectLocalPayload();
const next = getComparablePayload(payload);
if (next === lastSyncedRef.current) return;
lastSyncedRef.current = next;
syncUserConfig(userIdRef.current, false);
}, 9000);
};
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);
if (keys.has(key)) scheduleSync();
};
localStorage.removeItem = (key) => {
originalRemoveItem(key);
if (keys.has(key)) scheduleSync();
};
localStorage.clear = () => {
originalClear();
scheduleSync();
};
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);
};
}, []);
const toggleFavorite = (code) => {
setFavorites(prev => {
const next = new Set(prev);
@@ -2208,11 +2328,6 @@ export default function HomePage() {
if (Array.isArray(savedGroups)) {
setGroups(savedGroups);
}
// 加载视图模式
const savedViewMode = localStorage.getItem('viewMode');
if (savedViewMode === 'card' || savedViewMode === 'list') {
setViewMode(savedViewMode);
}
// 加载持仓数据
const savedHoldings = JSON.parse(localStorage.getItem('holdings') || '{}');
if (savedHoldings && typeof savedHoldings === 'object') {
@@ -2226,6 +2341,9 @@ export default function HomePage() {
// 获取当前 session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
if (session?.user) {
fetchCloudConfig(session.user.id);
}
});
// 监听认证状态变化
@@ -2236,19 +2354,42 @@ export default function HomePage() {
setLoginEmail('');
setLoginSuccess('');
setLoginError('');
fetchCloudConfig(session.user.id);
}
});
return () => subscription.unsubscribe();
}, []);
// 发送魔术链接邮件
const handleSendMagicLink = async (e) => {
useEffect(() => {
if (!user?.id) return;
const channel = supabase
.channel(`user-configs-${user.id}`)
.on('postgres_changes', { event: 'INSERT', 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);
})
.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);
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [user?.id]);
const handleSendOtp = async (e) => {
e.preventDefault();
setLoginError('');
setLoginSuccess('');
// 简单的邮箱格式验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!loginEmail.trim()) {
setLoginError('请输入邮箱地址');
@@ -2264,29 +2405,63 @@ export default function HomePage() {
const { error } = await supabase.auth.signInWithOtp({
email: loginEmail.trim(),
options: {
emailRedirectTo: window.location.origin
shouldCreateUser: true
}
});
if (error) throw error;
setLoginSuccess('验证邮件已发送,请查收邮箱并点击链接完成登录');
setLoginSuccess('验证已发送,请查收邮箱输入验证码完成注册/登录');
} catch (err) {
if (err.message?.includes('rate limit')) {
setLoginError('请求过于频繁,请稍后再试');
} else if (err.message?.includes('network')) {
setLoginError('网络错误,请检查网络连接');
} else {
setLoginError(err.message || '发送验证邮件失败,请稍后再试');
setLoginError(err.message || '发送验证失败,请稍后再试');
}
} finally {
setLoginLoading(false);
}
};
const handleVerifyEmailOtp = async () => {
setLoginError('');
if (!loginOtp || loginOtp.length < 4) {
setLoginError('请输入邮箱中的验证码');
return;
}
try {
setLoginLoading(true);
const { data, error } = await supabase.auth.verifyOtp({
email: loginEmail.trim(),
token: loginOtp.trim(),
type: 'email'
});
if (error) throw error;
if (data?.user) {
setLoginModalOpen(false);
setLoginEmail('');
setLoginOtp('');
setLoginSuccess('');
setLoginError('');
fetchCloudConfig(data.user.id);
}
} catch (err) {
setLoginError(err.message || '验证失败,请检查验证码或稍后再试');
}
setLoginLoading(false);
};
// 登出
const handleLogout = async () => {
try {
await supabase.auth.signOut();
const { error } = await supabase.auth.signOut();
if (error?.code === 'session_not_found') {
await supabase.auth.signOut({ scope: 'local' });
} else if (error) {
throw error;
}
setUserMenuOpen(false);
setUser(null);
} catch (err) {
console.error('登出失败', err);
}
@@ -2769,7 +2944,6 @@ export default function HomePage() {
const toggleViewMode = () => {
const nextMode = viewMode === 'card' ? 'list' : 'card';
setViewMode(nextMode);
localStorage.setItem('viewMode', nextMode);
};
const requestRemoveFund = (fund) => {
@@ -2893,6 +3067,187 @@ export default function HomePage() {
const importFileRef = useRef(null);
const [importMsg, setImportMsg] = useState('');
function getComparablePayload(payload) {
if (!payload || typeof payload !== 'object') return '';
return JSON.stringify({
funds: Array.isArray(payload.funds) ? payload.funds : [],
favorites: Array.isArray(payload.favorites) ? payload.favorites : [],
groups: Array.isArray(payload.groups) ? payload.groups : [],
collapsedCodes: Array.isArray(payload.collapsedCodes) ? payload.collapsedCodes : [],
refreshMs: Number.isFinite(payload.refreshMs) ? payload.refreshMs : 30000,
holdings: payload.holdings && typeof payload.holdings === 'object' ? payload.holdings : {}
});
}
const collectLocalPayload = () => {
try {
const funds = JSON.parse(localStorage.getItem('funds') || '[]');
const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
const groups = JSON.parse(localStorage.getItem('groups') || '[]');
const collapsedCodes = JSON.parse(localStorage.getItem('collapsedCodes') || '[]');
const fundCodes = new Set(
Array.isArray(funds)
? funds.map((f) => f?.code).filter(Boolean)
: []
);
const holdings = JSON.parse(localStorage.getItem('holdings') || '{}');
const cleanedHoldings = holdings && typeof holdings === 'object' && !Array.isArray(holdings)
? Object.entries(holdings).reduce((acc, [code, value]) => {
if (!fundCodes.has(code) || !value || typeof value !== 'object') return acc;
const parsedShare = typeof value.share === 'number'
? value.share
: typeof value.share === 'string'
? Number(value.share)
: NaN;
const parsedCost = typeof value.cost === 'number'
? value.cost
: typeof value.cost === 'string'
? Number(value.cost)
: NaN;
const nextShare = Number.isFinite(parsedShare) ? parsedShare : null;
const nextCost = Number.isFinite(parsedCost) ? parsedCost : null;
if (nextShare === null && nextCost === null) return acc;
acc[code] = {
...value,
share: nextShare,
cost: nextCost
};
return acc;
}, {})
: {};
const cleanedFavorites = Array.isArray(favorites)
? favorites.filter((code) => fundCodes.has(code))
: [];
const cleanedCollapsed = Array.isArray(collapsedCodes)
? collapsedCodes.filter((code) => fundCodes.has(code))
: [];
const cleanedGroups = Array.isArray(groups)
? groups.map((group) => ({
...group,
codes: Array.isArray(group?.codes)
? group.codes.filter((code) => fundCodes.has(code))
: []
}))
: [];
return {
version: 1,
funds,
favorites: cleanedFavorites,
groups: cleanedGroups,
collapsedCodes: cleanedCollapsed,
refreshMs: parseInt(localStorage.getItem('refreshMs') || '30000', 10),
holdings: cleanedHoldings,
exportedAt: new Date().toISOString()
};
} catch {
return {
version: 1,
funds: [],
favorites: [],
groups: [],
collapsedCodes: [],
refreshMs: 30000,
holdings: {},
exportedAt: new Date().toISOString()
};
}
};
const applyCloudConfig = async (cloudData) => {
if (!cloudData || typeof cloudData !== 'object') return;
skipSyncRef.current = true;
try {
const nextFunds = Array.isArray(cloudData.funds) ? dedupeByCode(cloudData.funds) : [];
setFunds(nextFunds);
localStorage.setItem('funds', JSON.stringify(nextFunds));
const nextFavorites = Array.isArray(cloudData.favorites) ? cloudData.favorites : [];
setFavorites(new Set(nextFavorites));
localStorage.setItem('favorites', JSON.stringify(nextFavorites));
const nextGroups = Array.isArray(cloudData.groups) ? cloudData.groups : [];
setGroups(nextGroups);
localStorage.setItem('groups', JSON.stringify(nextGroups));
const nextCollapsed = Array.isArray(cloudData.collapsedCodes) ? cloudData.collapsedCodes : [];
setCollapsedCodes(new Set(nextCollapsed));
localStorage.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));
if (cloudData.viewMode === 'card' || cloudData.viewMode === 'list') {
setViewMode(cloudData.viewMode);
}
const nextHoldings = cloudData.holdings && typeof cloudData.holdings === 'object' ? cloudData.holdings : {};
setHoldings(nextHoldings);
localStorage.setItem('holdings', JSON.stringify(nextHoldings));
if (nextFunds.length) {
const codes = Array.from(new Set(nextFunds.map((f) => f.code)));
if (codes.length) await refreshAll(codes);
}
const payload = collectLocalPayload();
lastSyncedRef.current = getComparablePayload(payload);
} finally {
skipSyncRef.current = false;
}
};
const fetchCloudConfig = async (userId) => {
if (!userId) return;
try {
const { data, error } = await supabase
.from('user_configs')
.select('id, data')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
if (!data?.id) {
const { error: insertError } = await supabase
.from('user_configs')
.insert({ user_id: userId });
if (insertError) throw insertError;
setCloudConfigModal({ open: true, userId });
return;
}
if (data?.data && typeof data.data === 'object' && Object.keys(data.data).length > 0) {
await applyCloudConfig(data.data);
return;
}
setCloudConfigModal({ open: true, userId });
} catch (e) {
console.error('获取云端配置失败', e);
}
};
const syncUserConfig = async (userId, showTip = true) => {
if (!userId) return;
try {
const payload = collectLocalPayload();
const { error: updateError } = await supabase
.from('user_configs')
.update({ data: payload, updated_at: new Date().toISOString() })
.eq('user_id', userId);
if (updateError) throw updateError;
if (showTip) {
setSuccessModal({ open: true, message: '已同步云端配置' });
}
} catch (e) {
console.error('同步云端配置异常', e);
}
};
const handleSyncLocalConfig = async () => {
const userId = cloudConfigModal.userId;
setCloudConfigModal({ open: false, userId: null });
await syncUserConfig(userId);
};
const exportLocalData = async () => {
try {
const payload = {
@@ -2902,7 +3257,7 @@ export default function HomePage() {
groups: JSON.parse(localStorage.getItem('groups') || '[]'),
collapsedCodes: JSON.parse(localStorage.getItem('collapsedCodes') || '[]'),
refreshMs: parseInt(localStorage.getItem('refreshMs') || '30000', 10),
viewMode: localStorage.getItem('viewMode') || 'card',
viewMode,
holdings: JSON.parse(localStorage.getItem('holdings') || '{}'),
exportedAt: new Date().toISOString()
};
@@ -3007,7 +3362,6 @@ export default function HomePage() {
}
if (data.viewMode === 'card' || data.viewMode === 'list') {
setViewMode(data.viewMode);
localStorage.setItem('viewMode', data.viewMode);
}
if (data.holdings && typeof data.holdings === 'object') {
@@ -3045,6 +3399,8 @@ export default function HomePage() {
groupManageOpen ||
groupModalOpen ||
successModal.open ||
cloudConfigModal.open ||
logoutConfirmOpen ||
holdingModal.open ||
actionModal.open ||
tradeModal.open ||
@@ -3069,6 +3425,8 @@ export default function HomePage() {
groupManageOpen,
groupModalOpen,
successModal.open,
cloudConfigModal.open,
logoutConfirmOpen,
holdingModal.open,
actionModal.open,
tradeModal.open,
@@ -3177,7 +3535,13 @@ export default function HomePage() {
<SettingsIcon width="16" height="16" />
<span>设置</span>
</button>
<button className="user-menu-item danger" onClick={handleLogout}>
<button
className="user-menu-item danger"
onClick={() => {
setUserMenuOpen(false);
setLogoutConfirmOpen(true);
}}
>
<LogoutIcon width="16" height="16" />
<span>登出</span>
</button>
@@ -3382,7 +3746,7 @@ export default function HomePage() {
<div className="view-toggle" style={{ display: 'flex', background: 'rgba(255,255,255,0.05)', borderRadius: '10px', padding: '2px' }}>
<button
className={`icon-button ${viewMode === 'card' ? 'active' : ''}`}
onClick={() => { setViewMode('card'); localStorage.setItem('viewMode', 'card'); }}
onClick={() => { setViewMode('card'); }}
style={{ border: 'none', width: '32px', height: '32px', background: viewMode === 'card' ? 'var(--primary)' : 'transparent', color: viewMode === 'card' ? '#05263b' : 'var(--muted)' }}
title="卡片视图"
>
@@ -3390,7 +3754,7 @@ export default function HomePage() {
</button>
<button
className={`icon-button ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => { setViewMode('list'); localStorage.setItem('viewMode', 'list'); }}
onClick={() => { setViewMode('list'); }}
style={{ border: 'none', width: '32px', height: '32px', background: viewMode === 'list' ? 'var(--primary)' : 'transparent', color: viewMode === 'list' ? '#05263b' : 'var(--muted)' }}
title="表格视图"
>
@@ -4029,6 +4393,21 @@ export default function HomePage() {
)}
</AnimatePresence>
<AnimatePresence>
{logoutConfirmOpen && (
<ConfirmModal
title="确认登出"
message="确定要退出当前账号吗?"
confirmText="确认登出"
onConfirm={() => {
setLogoutConfirmOpen(false);
handleLogout();
}}
onCancel={() => setLogoutConfirmOpen(false)}
/>
)}
</AnimatePresence>
<div className="footer">
<p style={{ marginBottom: 8 }}>数据源实时估值与重仓直连东方财富仅供个人学习及参考使用数据可能存在延迟不作为任何投资建议</p>
<p style={{ marginBottom: 12 }}>估算数据与真实结算数据会有1%左右误差非股票型基金误差较大</p>
@@ -4208,6 +4587,15 @@ export default function HomePage() {
)}
</AnimatePresence>
<AnimatePresence>
{cloudConfigModal.open && (
<CloudConfigModal
onConfirm={handleSyncLocalConfig}
onCancel={() => setCloudConfigModal({ open: false, userId: null })}
/>
)}
</AnimatePresence>
{settingsOpen && (
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="设置" onClick={() => setSettingsOpen(false)}>
<div className="glass card modal" onClick={(e) => e.stopPropagation()}>
@@ -4299,34 +4687,49 @@ export default function HomePage() {
<span className="muted">使用邮箱验证登录</span>
</div>
<form onSubmit={handleSendMagicLink}>
<form onSubmit={handleSendOtp}>
<div className="form-group" style={{ marginBottom: 16 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>
输入您的邮箱地址我们将发送一封验证邮件
输入邮箱我们将发送验证码到您的邮箱
</div>
<input
style={{width: '100%'}}
className="input"
type="email"
placeholder="your@email.com"
value={loginEmail}
onChange={(e) => setLoginEmail(e.target.value)}
disabled={loginLoading}
autoFocus
disabled={loginLoading || !!loginSuccess}
/>
</div>
{loginError && (
<div className="login-message error" style={{ marginBottom: 12 }}>
<span>{loginError}</span>
</div>
)}
{loginSuccess && (
<div className="login-message success" style={{ marginBottom: 12 }}>
<span>{loginSuccess}</span>
</div>
)}
{loginSuccess && (
<div className="form-group" style={{ marginBottom: 16 }}>
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>
请输入邮箱验证码以完成注册/登录
</div>
<input
className="input"
type="text"
placeholder="输入验证码"
value={loginOtp}
onChange={(e) => setLoginOtp(e.target.value)}
disabled={loginLoading}
maxLength={6}
/>
</div>
)}
{loginError && (
<div className="login-message error" style={{ marginBottom: 12 }}>
<span>{loginError}</span>
</div>
)}
<div className="row" style={{ justifyContent: 'flex-end', gap: 12 }}>
<button
type="button"
@@ -4336,6 +4739,7 @@ export default function HomePage() {
setLoginError('');
setLoginSuccess('');
setLoginEmail('');
setLoginOtp('');
}}
disabled={loginLoading}
>
@@ -4343,10 +4747,11 @@ export default function HomePage() {
</button>
<button
className="button"
type="submit"
disabled={loginLoading || loginSuccess}
type={loginSuccess ? 'button' : 'submit'}
onClick={loginSuccess ? handleVerifyEmailOtp : undefined}
disabled={loginLoading || (loginSuccess && !loginOtp)}
>
{loginLoading ? '发送中...' : loginSuccess ? '已发送' : '发送验证邮件'}
{loginLoading ? '处理中...' : loginSuccess ? '确认验证码' : '发送邮箱验证码'}
</button>
</div>
</form>