feat: 暂时隐藏用户登录认证功能
This commit is contained in:
325
app/globals.css
325
app/globals.css
@@ -14,7 +14,8 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -65,13 +66,29 @@ body {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.col-12 { grid-column: span 12; }
|
||||
.col-6 { grid-column: span 6; }
|
||||
.col-4 { grid-column: span 4; }
|
||||
.col-3 { grid-column: span 3; }
|
||||
.col-12 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
|
||||
.col-6 {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.col-4 {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.col-3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.col-6, .col-4, .col-3 { grid-column: span 12; }
|
||||
|
||||
.col-6,
|
||||
.col-4,
|
||||
.col-3 {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
@@ -101,6 +118,7 @@ body {
|
||||
.content {
|
||||
padding-top: 90px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -110,6 +128,7 @@ body {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.add-fund-section {
|
||||
margin-top: 60px;
|
||||
}
|
||||
@@ -133,6 +152,7 @@ body {
|
||||
font-size: 14px;
|
||||
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
|
||||
@@ -144,6 +164,7 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
@@ -159,10 +180,12 @@ input[type="number"] {
|
||||
cursor: pointer;
|
||||
transition: transform 150ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 20px rgba(34, 211, 238, 0.25);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
@@ -200,10 +223,12 @@ input[type="number"] {
|
||||
background: #0b1220;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.badge-v span {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.badge-v strong {
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -213,14 +238,17 @@ input[type="number"] {
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat .label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.stat .value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat .badge {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
@@ -228,60 +256,80 @@ input[type="number"] {
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.input {
|
||||
font-size: 16px; /* 防止 iOS 在输入时自动放大 */
|
||||
font-size: 16px;
|
||||
/* 防止 iOS 在输入时自动放大 */
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat .label {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stat .value {
|
||||
font-size: 15px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stat .badge {
|
||||
padding: 2px 6px;
|
||||
font-size: 13px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.card .title {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.item .name {
|
||||
max-width: 100px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.item .badge {
|
||||
padding: 2px 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.up { color: var(--danger); }
|
||||
.down { color: var(--success); }
|
||||
|
||||
.up {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.down {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.list { grid-template-columns: 1fr; }
|
||||
.list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
@@ -293,6 +341,7 @@ input[type="number"] {
|
||||
border: 1px solid var(--border);
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.item .name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -306,6 +355,7 @@ input[type="number"] {
|
||||
max-width: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.item .weight {
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
@@ -359,18 +409,22 @@ input[type="number"] {
|
||||
cursor: pointer;
|
||||
transition: box-shadow 200ms ease, border-color 200ms ease, transform 150ms ease, color 200ms ease;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
color: var(--text);
|
||||
transform: translateY(-1px);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.icon-button.danger {
|
||||
background: linear-gradient(180deg, #ef4444, #f87171);
|
||||
color: #2b0b0b;
|
||||
}
|
||||
|
||||
.icon-button.danger:hover {
|
||||
box-shadow: 0 10px 20px rgba(248, 113, 113, 0.25);
|
||||
}
|
||||
@@ -385,9 +439,11 @@ input[type="number"] {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.fav-button:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.fav-button.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
@@ -456,8 +512,15 @@ input[type="number"] {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-right { text-align: right; justify-content: flex-end; }
|
||||
.text-center { text-align: center; justify-content: center; }
|
||||
.text-right {
|
||||
text-align: right;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
gap: 8px;
|
||||
@@ -501,6 +564,7 @@ input[type="number"] {
|
||||
.table-header-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: 1fr 80px 100px;
|
||||
grid-template-areas:
|
||||
@@ -509,23 +573,49 @@ input[type="number"] {
|
||||
gap: 4px 12px;
|
||||
padding: 12px !important;
|
||||
}
|
||||
.name-cell { grid-area: name; }
|
||||
.value-cell { grid-area: value; }
|
||||
.change-cell { grid-area: change; }
|
||||
.profit-cell { grid-area: profit; }
|
||||
|
||||
.name-cell {
|
||||
grid-area: name;
|
||||
}
|
||||
|
||||
.value-cell {
|
||||
grid-area: value;
|
||||
}
|
||||
|
||||
.change-cell {
|
||||
grid-area: change;
|
||||
}
|
||||
|
||||
.profit-cell {
|
||||
grid-area: profit;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
grid-area: time;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.action-cell { display: none; }
|
||||
.holding-cell { display: none; }
|
||||
.holding-amount-cell { display: none; }
|
||||
|
||||
.action-cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.holding-cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.holding-amount-cell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-cell.time-cell span {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-compact .up { color: var(--danger); }
|
||||
.stat-compact .up {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-cell .danger {
|
||||
display: none;
|
||||
@@ -534,7 +624,8 @@ input[type="number"] {
|
||||
|
||||
.swipe-action-bg {
|
||||
position: absolute;
|
||||
top: 1px; /* 留出一点缝隙,或者与 border 对齐 */
|
||||
top: 1px;
|
||||
/* 留出一点缝隙,或者与 border 对齐 */
|
||||
bottom: 1px;
|
||||
right: 0;
|
||||
width: 80px;
|
||||
@@ -551,7 +642,10 @@ input[type="number"] {
|
||||
cursor: pointer;
|
||||
box-shadow: inset 10px 0 20px -10px rgba(0,0,0,0.2); /* 增加一点内阴影,增加层次感 */
|
||||
}
|
||||
.stat-compact .down { color: var(--success); }
|
||||
|
||||
.stat-compact .down {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
transition: all 0.3s ease;
|
||||
@@ -572,9 +666,11 @@ input[type="number"] {
|
||||
flex-direction: column;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
width: 100%;
|
||||
justify-content: flex-start; /* 移动端改为左对齐 */
|
||||
justify-content: flex-start;
|
||||
/* 移动端改为左对齐 */
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
backdrop-filter: none;
|
||||
@@ -626,16 +722,19 @@ input[type="number"] {
|
||||
justify-content: center;
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
.modal {
|
||||
width: 560px;
|
||||
max-width: 92vw;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1056,3 +1155,183 @@ input[type="number"] {
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 用户菜单样式 ========== */
|
||||
.user-menu-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-trigger {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-menu-trigger.logged-in {
|
||||
border-color: var(--primary);
|
||||
background: rgba(34, 211, 238, 0.1);
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: #05263b;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-menu-dropdown {
|
||||
position: fixed;
|
||||
top: 76px;
|
||||
right: 16px;
|
||||
min-width: 200px;
|
||||
padding: 8px;
|
||||
z-index: 100;
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.user-menu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.user-avatar-large {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: #05263b;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
font-size: 11px;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.user-menu-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.user-menu-item.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.user-menu-item.danger:hover {
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
}
|
||||
|
||||
/* ========== 登录模态框样式 ========== */
|
||||
.login-modal {
|
||||
max-width: 400px !important;
|
||||
}
|
||||
|
||||
.login-message {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-message.error {
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
border: 1px solid rgba(248, 113, 113, 0.3);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.login-message.success {
|
||||
background: rgba(52, 211, 153, 0.15);
|
||||
border: 1px solid rgba(52, 211, 153, 0.3);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
border-color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ========== 移动端响应式 ========== */
|
||||
@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;
|
||||
}
|
||||
|
||||
.user-menu-item {
|
||||
padding: 14px 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-modal {
|
||||
max-width: 100% !important;
|
||||
margin: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
292
app/page.jsx
292
app/page.jsx
@@ -6,6 +6,7 @@ import Announcement from "./components/Announcement";
|
||||
import zhifubaoImg from "./assets/zhifubao.jpg";
|
||||
import weixinImg from "./assets/weixin.jpg";
|
||||
import githubImg from "./assets/github.svg";
|
||||
import { supabase } from './lib/supabase';
|
||||
|
||||
function PlusIcon(props) {
|
||||
return (
|
||||
@@ -62,6 +63,44 @@ function SortIcon(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function UserIcon(props) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="8" r="4" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M4 20c0-4 4-6 8-6s8 2 8 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoutIcon(props) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<polyline points="16 17 21 12 16 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginIcon(props) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<polyline points="10 17 15 12 10 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<line x1="15" y1="12" x2="3" y2="12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MailIcon(props) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M22 6l-10 7L2 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GridIcon(props) {
|
||||
return (
|
||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||
@@ -1686,6 +1725,15 @@ export default function HomePage() {
|
||||
// 视图模式
|
||||
const [viewMode, setViewMode] = useState('card'); // card, list
|
||||
|
||||
// 用户认证状态
|
||||
const [user, setUser] = useState(null);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||
const [loginEmail, setLoginEmail] = useState('');
|
||||
const [loginLoading, setLoginLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState('');
|
||||
const [loginSuccess, setLoginSuccess] = useState('');
|
||||
|
||||
// 反馈弹窗状态
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
const [feedbackNonce, setFeedbackNonce] = useState(0);
|
||||
@@ -2173,6 +2221,91 @@ export default function HomePage() {
|
||||
} catch { }
|
||||
}, []);
|
||||
|
||||
// 初始化认证状态监听
|
||||
useEffect(() => {
|
||||
// 获取当前 session
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
});
|
||||
|
||||
// 监听认证状态变化
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
setLoginModalOpen(false);
|
||||
setLoginEmail('');
|
||||
setLoginSuccess('');
|
||||
setLoginError('');
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
// 发送魔术链接邮件
|
||||
const handleSendMagicLink = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoginError('');
|
||||
setLoginSuccess('');
|
||||
|
||||
// 简单的邮箱格式验证
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!loginEmail.trim()) {
|
||||
setLoginError('请输入邮箱地址');
|
||||
return;
|
||||
}
|
||||
if (!emailRegex.test(loginEmail.trim())) {
|
||||
setLoginError('请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginLoading(true);
|
||||
try {
|
||||
const { error } = await supabase.auth.signInWithOtp({
|
||||
email: loginEmail.trim(),
|
||||
options: {
|
||||
emailRedirectTo: window.location.origin
|
||||
}
|
||||
});
|
||||
if (error) throw error;
|
||||
setLoginSuccess('验证邮件已发送,请查收邮箱并点击链接完成登录');
|
||||
} catch (err) {
|
||||
if (err.message?.includes('rate limit')) {
|
||||
setLoginError('请求过于频繁,请稍后再试');
|
||||
} else if (err.message?.includes('network')) {
|
||||
setLoginError('网络错误,请检查网络连接');
|
||||
} else {
|
||||
setLoginError(err.message || '发送验证邮件失败,请稍后再试');
|
||||
}
|
||||
} finally {
|
||||
setLoginLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 登出
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
setUserMenuOpen(false);
|
||||
} catch (err) {
|
||||
console.error('登出失败', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭用户菜单(点击外部时)
|
||||
const userMenuRef = useRef(null);
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (userMenuRef.current && !userMenuRef.current.contains(event.target)) {
|
||||
setUserMenuOpen(false);
|
||||
}
|
||||
};
|
||||
if (userMenuOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [userMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
timerRef.current = setInterval(() => {
|
||||
@@ -2994,6 +3127,89 @@ export default function HomePage() {
|
||||
>
|
||||
<SettingsIcon width="18" height="18" />
|
||||
</button>
|
||||
|
||||
{/* 临时隐藏用户菜单入口 */}
|
||||
<div className="user-menu-container" ref={userMenuRef} hidden>
|
||||
<button
|
||||
className={`icon-button user-menu-trigger ${user ? 'logged-in' : ''}`}
|
||||
aria-label={user ? '用户菜单' : '登录'}
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
title={user ? (user.email || '用户') : '用户菜单'}
|
||||
>
|
||||
{user ? (
|
||||
<div className="user-avatar-small">
|
||||
{user.email?.charAt(0).toUpperCase() || 'U'}
|
||||
</div>
|
||||
) : (
|
||||
<UserIcon width="18" height="18" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{userMenuOpen && (
|
||||
<motion.div
|
||||
className="user-menu-dropdown glass"
|
||||
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
style={{ transformOrigin: 'top right' }}
|
||||
>
|
||||
{user ? (
|
||||
<>
|
||||
<div className="user-menu-header">
|
||||
<div className="user-avatar-large">
|
||||
{user.email?.charAt(0).toUpperCase() || 'U'}
|
||||
</div>
|
||||
<div className="user-info">
|
||||
<span className="user-email">{user.email}</span>
|
||||
<span className="user-status">已登录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-menu-divider" />
|
||||
<button
|
||||
className="user-menu-item"
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
setSettingsOpen(true);
|
||||
}}
|
||||
>
|
||||
<SettingsIcon width="16" height="16" />
|
||||
<span>设置</span>
|
||||
</button>
|
||||
<button className="user-menu-item danger" onClick={handleLogout}>
|
||||
<LogoutIcon width="16" height="16" />
|
||||
<span>登出</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="user-menu-item"
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
setLoginModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<LoginIcon width="16" height="16" />
|
||||
<span>登录</span>
|
||||
</button>
|
||||
<button
|
||||
className="user-menu-item"
|
||||
onClick={() => {
|
||||
setUserMenuOpen(false);
|
||||
setSettingsOpen(true);
|
||||
}}
|
||||
>
|
||||
<SettingsIcon width="16" height="16" />
|
||||
<span>设置</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4061,6 +4277,82 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 登录模态框 */}
|
||||
{loginModalOpen && (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="登录"
|
||||
onClick={() => {
|
||||
setLoginModalOpen(false);
|
||||
setLoginError('');
|
||||
setLoginSuccess('');
|
||||
setLoginEmail('');
|
||||
}}
|
||||
>
|
||||
<div className="glass card modal login-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="title" style={{ marginBottom: 16 }}>
|
||||
<MailIcon width="20" height="20" />
|
||||
<span>邮箱登录</span>
|
||||
<span className="muted">使用邮箱验证登录</span>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSendMagicLink}>
|
||||
<div className="form-group" style={{ marginBottom: 16 }}>
|
||||
<div className="muted" style={{ marginBottom: 8, fontSize: '0.8rem' }}>
|
||||
输入您的邮箱地址,我们将发送一封验证邮件
|
||||
</div>
|
||||
<input
|
||||
className="input"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
disabled={loginLoading}
|
||||
autoFocus
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="row" style={{ justifyContent: 'flex-end', gap: 12 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="button secondary"
|
||||
onClick={() => {
|
||||
setLoginModalOpen(false);
|
||||
setLoginError('');
|
||||
setLoginSuccess('');
|
||||
setLoginEmail('');
|
||||
}}
|
||||
disabled={loginLoading}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
type="submit"
|
||||
disabled={loginLoading || loginSuccess}
|
||||
>
|
||||
{loginLoading ? '发送中...' : loginSuccess ? '已发送' : '发送验证邮件'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
153
package-lock.json
generated
153
package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "real-time-fund",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.78.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
@@ -164,6 +165,85 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/auth-js/-/auth-js-2.78.0.tgz",
|
||||
"integrity": "sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/functions-js/-/functions-js-2.78.0.tgz",
|
||||
"integrity": "sha512-t1jOvArBsOINyqaRee1xJ3gryXLvkBzqnKfi6q3YRzzhJbGS6eXz0pXR5fqmJeB01fLC+1njpf3YhMszdPEF7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/node-fetch": {
|
||||
"version": "2.6.15",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/postgrest-js/-/postgrest-js-2.78.0.tgz",
|
||||
"integrity": "sha512-AwhpYlSvJ+PSnPmIK8sHj7NGDyDENYfQGKrMtpVIEzQA2ApUjgpUGxzXWN4Z0wEtLQsvv7g4y9HVad9Hzo1TNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/realtime-js/-/realtime-js-2.78.0.tgz",
|
||||
"integrity": "sha512-rCs1zmLe7of7hj4s7G9z8rTqzWuNVtmwDr3FiCRCJFawEoa+RQO1xpZGbdeuVvVmKDyVN6b542Okci+117y/LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tslib": "2.8.1",
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/storage-js/-/storage-js-2.78.0.tgz",
|
||||
"integrity": "sha512-n17P0JbjHOlxqJpkaGFOn97i3EusEKPEbWOpuk1r4t00Wg06B8Z4GUiq0O0n1vUpjiMgJUkLIMuBVp+bEgunzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"tslib": "2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.78.0",
|
||||
"resolved": "https://registry.npmmirror.com/@supabase/supabase-js/-/supabase-js-2.78.0.tgz",
|
||||
"integrity": "sha512-xYMRNBFmKp2m1gMuwcp/gr/HlfZKqjye1Ib8kJe29XJNsgwsfO/f8skxnWiscFKTlkOKLuBexNgl5L8dzGt6vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.78.0",
|
||||
"@supabase/functions-js": "2.78.0",
|
||||
"@supabase/node-fetch": "2.6.15",
|
||||
"@supabase/postgrest-js": "2.78.0",
|
||||
"@supabase/realtime-js": "2.78.0",
|
||||
"@supabase/storage-js": "2.78.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -180,6 +260,30 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.2.1.tgz",
|
||||
"integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/phoenix/-/phoenix-1.6.7.tgz",
|
||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -460,11 +564,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.78.0",
|
||||
"framer-motion": "^12.29.2",
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
|
||||
Reference in New Issue
Block a user