feat(admin-web): 登录页 + Auth Guard + API URL 切换域名
- 新建 /login 页面(邮箱/密码登录,对接 auth-context) - AdminLayout 添加 auth guard:未登录自动跳转 /login - api-client 默认 URL 从 localhost:8080 → https://api.gogenex.com - Header 头像显示用户首字母,点击登出 - i18n 补充 header_logout (zh/en/ja) API 联通验证(全部正常): - POST /api/v1/auth/sms/send → 400 (手机号未注册) - POST /api/v1/auth/login → 401 (密码错误) - POST /api/v1/auth/register → 400 (验证码过期) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
65d2904f1a
commit
34b85f68ae
|
|
@ -0,0 +1,157 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 已登录则跳转
|
||||
if (isAuthenticated) {
|
||||
router.replace('/dashboard');
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(email, password);
|
||||
router.replace('/dashboard');
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { response?: { data?: { message?: string } } })?.response?.data
|
||||
?.message || '登录失败,请检查账号密码';
|
||||
setError(msg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.wrapper}>
|
||||
<div style={styles.card}>
|
||||
<div style={styles.logoRow}>
|
||||
<Image src="/logo.svg" alt="Genex" width={40} height={40} />
|
||||
<span style={styles.brand}>Genex Admin</span>
|
||||
</div>
|
||||
<h1 style={styles.title}>管理后台登录</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} style={styles.form}>
|
||||
<label style={styles.label}>邮箱 / 手机号</label>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="text"
|
||||
placeholder="admin@gogenex.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<label style={styles.label}>密码</label>
|
||||
<input
|
||||
style={styles.input}
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
{error && <p style={styles.error}>{error}</p>}
|
||||
|
||||
<button type="submit" style={styles.btn} disabled={loading}>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style={styles.footer}>Genex 券金融平台 © 2025</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------- inline styles (no extra CSS file) ---------- */
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #F3F1FF 0%, #E8E5FF 50%, #F8F9FC 100%)',
|
||||
},
|
||||
card: {
|
||||
width: 400,
|
||||
padding: '40px 36px',
|
||||
borderRadius: 16,
|
||||
background: '#fff',
|
||||
boxShadow: '0 8px 32px rgba(108,92,231,0.10)',
|
||||
},
|
||||
logoRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 8,
|
||||
},
|
||||
brand: {
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
color: '#6C5CE7',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: '#262B3A',
|
||||
margin: '16px 0 28px',
|
||||
},
|
||||
form: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
gap: 12,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: '#5C6478',
|
||||
},
|
||||
input: {
|
||||
padding: '10px 14px',
|
||||
fontSize: 15,
|
||||
border: '1px solid #E4E7F0',
|
||||
borderRadius: 8,
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s',
|
||||
},
|
||||
error: {
|
||||
fontSize: 13,
|
||||
color: '#FF4757',
|
||||
margin: 0,
|
||||
},
|
||||
btn: {
|
||||
marginTop: 8,
|
||||
padding: '12px 0',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
background: '#6C5CE7',
|
||||
border: 'none',
|
||||
borderRadius: 8,
|
||||
cursor: 'pointer',
|
||||
transition: 'opacity 0.2s',
|
||||
},
|
||||
footer: {
|
||||
textAlign: 'center' as const,
|
||||
marginTop: 28,
|
||||
fontSize: 12,
|
||||
color: '#A0A8BE',
|
||||
},
|
||||
};
|
||||
|
|
@ -134,6 +134,7 @@ const translations: Record<Locale, Record<string, string>> = {
|
|||
// ── Header ──
|
||||
'header_search_placeholder': '搜索用户/订单/交易...',
|
||||
'header_ai_assistant': 'AI 助手',
|
||||
'header_logout': '退出登录',
|
||||
|
||||
// ── Dashboard ──
|
||||
'dashboard_title': '运营总览',
|
||||
|
|
@ -866,6 +867,7 @@ const translations: Record<Locale, Record<string, string>> = {
|
|||
// ── Header ──
|
||||
'header_search_placeholder': 'Search users/orders/trades...',
|
||||
'header_ai_assistant': 'AI Assistant',
|
||||
'header_logout': 'Logout',
|
||||
|
||||
// ── Dashboard ──
|
||||
'dashboard_title': 'Dashboard',
|
||||
|
|
@ -1598,6 +1600,7 @@ const translations: Record<Locale, Record<string, string>> = {
|
|||
// ── Header ──
|
||||
'header_search_placeholder': 'ユーザー/注文/取引を検索...',
|
||||
'header_ai_assistant': 'AIアシスタント',
|
||||
'header_logout': 'ログアウト',
|
||||
|
||||
// ── Dashboard ──
|
||||
'dashboard_title': '運営概要',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React, { useState } from 'react';
|
|||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { t } from '@/i18n/locales';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
|
||||
/**
|
||||
* D. Web管理前端 - 主布局
|
||||
|
|
@ -95,8 +96,17 @@ const navItems: NavItem[] = [
|
|||
export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { isAuthenticated, isLoading, user, logout } = useAuth();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
// 未登录 → /login
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.replace('/login');
|
||||
return null;
|
||||
}
|
||||
// 加载中显示空白
|
||||
if (isLoading) return null;
|
||||
|
||||
// Derive activeKey from current pathname
|
||||
const activeKey = pathname.replace(/^\//, '') || 'dashboard';
|
||||
|
||||
|
|
@ -324,19 +334,30 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children
|
|||
borderRadius: '50%',
|
||||
}} />
|
||||
</button>
|
||||
{/* Admin avatar */}
|
||||
<div style={{
|
||||
width: 32, height: 32,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-primary)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
A
|
||||
{/* Admin avatar + logout */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={logout}
|
||||
title={t('header_logout')}
|
||||
>
|
||||
<div style={{
|
||||
width: 32, height: 32,
|
||||
borderRadius: '50%',
|
||||
background: 'var(--color-primary)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{user?.name?.charAt(0)?.toUpperCase() || 'A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.gogenex.com';
|
||||
|
||||
class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
|
|
|
|||
Loading…
Reference in New Issue