Improve frontend layout and auth routing
This commit is contained in:
@@ -3,20 +3,41 @@
|
||||
import { useEffect, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { getUserInfo } from '@/lib/api';
|
||||
import Layout from '@/components/layout';
|
||||
import { Activity, BadgeCheck, Boxes, ShieldCheck } from 'lucide-react';
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: '欢迎回来',
|
||||
value: 'IMTIM Portal',
|
||||
description: '您已成功登录企业门户',
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
title: '账户状态',
|
||||
value: '已激活',
|
||||
description: '当前账号可正常访问系统',
|
||||
icon: BadgeCheck,
|
||||
},
|
||||
{
|
||||
title: '系统版本',
|
||||
value: '1.0.0',
|
||||
description: '基础登录门户服务',
|
||||
icon: Boxes,
|
||||
},
|
||||
];
|
||||
|
||||
function DashboardContent() {
|
||||
const { login, initialize } = useAuthStore();
|
||||
const { user, isAuthenticated, isInitialized, login, initialize } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const token = searchParams.get('token');
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('token');
|
||||
if (token) {
|
||||
const fetchUser = async () => {
|
||||
try {
|
||||
@@ -39,25 +60,74 @@ function DashboardContent() {
|
||||
};
|
||||
fetchUser();
|
||||
}
|
||||
}, [searchParams, login, router]);
|
||||
}, [token, login, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized && !isAuthenticated && !token) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [isInitialized, isAuthenticated, token, router]);
|
||||
|
||||
if (token || !isInitialized || !isAuthenticated) {
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center justify-center px-4 py-10 sm:px-6">
|
||||
<div className="rounded-xl border bg-white px-5 py-4 text-sm text-muted-foreground shadow-sm">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">仪表板</h1>
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="rounded-lg border p-6 shadow-sm">
|
||||
<h2 className="text-xl font-semibold mb-2">欢迎</h2>
|
||||
<p className="text-muted-foreground">
|
||||
您已成功登录 IMTIM Portal
|
||||
<div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 sm:py-10">
|
||||
<div className="mb-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-end">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-primary">控制台</p>
|
||||
<h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">仪表板</h1>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
{user?.username ? `${user.username},查看登录状态、账户可用性和基础系统信息。` : '查看登录状态、账户可用性和基础系统信息。'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-6 shadow-sm">
|
||||
<h2 className="text-xl font-semibold mb-2">账户状态</h2>
|
||||
<p className="text-green-600 font-medium">已激活</p>
|
||||
<div className="inline-flex w-fit items-center gap-2 rounded-full border bg-white px-3 py-1.5 text-sm font-medium text-emerald-700 shadow-sm">
|
||||
<Activity className="h-4 w-4" />
|
||||
在线
|
||||
</div>
|
||||
<div className="rounded-lg border p-6 shadow-sm">
|
||||
<h2 className="text-xl font-semibold mb-2">系统信息</h2>
|
||||
<p className="text-muted-foreground">版本 1.0.0</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
return (
|
||||
<div key={card.title} className="rounded-xl border bg-white p-5 shadow-sm sm:p-6">
|
||||
<div className="mb-5 flex items-center justify-between gap-4">
|
||||
<span className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Icon className="h-5 w-5" />
|
||||
</span>
|
||||
{card.title === '账户状态' ? (
|
||||
<span className="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-700">
|
||||
Active
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-muted-foreground">{card.title}</h2>
|
||||
<p className="mt-2 text-2xl font-semibold tracking-tight">{card.value}</p>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">{card.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-xl border bg-white p-5 shadow-sm sm:mt-6 sm:p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">认证流程</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
OAuth 回调、用户信息获取和本地会话写入均已就绪。
|
||||
</p>
|
||||
</div>
|
||||
<span className="w-fit rounded-full bg-secondary px-3 py-1 text-sm font-medium text-secondary-foreground">
|
||||
Ready
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +137,13 @@ function DashboardContent() {
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<Suspense fallback={<div className="container mx-auto px-4 py-8">加载中...</div>}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="mx-auto w-full max-w-6xl px-4 py-8 text-sm text-muted-foreground sm:px-6">
|
||||
加载中...
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DashboardContent />
|
||||
</Suspense>
|
||||
</Layout>
|
||||
|
||||
@@ -1,34 +1,66 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--background: oklch(0.985 0.006 247.858);
|
||||
--foreground: oklch(0.21 0.034 264.665);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.21 0.034 264.665);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.21 0.034 264.665);
|
||||
--primary: oklch(0.52 0.19 253.3);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.96 0.013 247.858);
|
||||
--secondary-foreground: oklch(0.279 0.041 260.031);
|
||||
--muted: oklch(0.96 0.013 247.858);
|
||||
--muted-foreground: oklch(0.446 0.043 257.281);
|
||||
--accent: oklch(0.94 0.025 255.585);
|
||||
--accent-foreground: oklch(0.279 0.041 260.031);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--border: oklch(0.9 0.022 252.894);
|
||||
--input: oklch(0.9 0.022 252.894);
|
||||
--ring: oklch(0.52 0.19 253.3);
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
html {
|
||||
min-width: 320px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
a {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,22 @@ import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { getLoginUrl } from '@/lib/api';
|
||||
import { LogIn } from 'lucide-react';
|
||||
import { BadgeCheck, KeyRound, LogIn } from 'lucide-react';
|
||||
import Layout from '@/components/layout';
|
||||
|
||||
export default function LoginPage() {
|
||||
const { isAuthenticated } = useAuthStore();
|
||||
const { isAuthenticated, isInitialized, initialize } = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.push('/dashboard');
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized && isAuthenticated) {
|
||||
router.replace('/dashboard');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
}, [isInitialized, isAuthenticated, router]);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
@@ -28,25 +32,66 @@ export default function LoginPage() {
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex items-center justify-center min-h-[calc(100vh-12rem)]">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="rounded-lg border p-8 shadow-sm">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold mb-2">登录</h1>
|
||||
<p className="text-muted-foreground">
|
||||
<div className="mx-auto grid min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center gap-10 px-4 py-10 sm:px-6 lg:grid-cols-[1fr_0.88fr]">
|
||||
<section className="hidden lg:block">
|
||||
<p className="text-sm font-medium text-primary">统一身份认证</p>
|
||||
<h1 className="mt-3 max-w-xl text-4xl font-bold tracking-tight">
|
||||
通过 Casdoor 安全接入 IMTIM Portal
|
||||
</h1>
|
||||
<div className="mt-8 grid max-w-xl gap-4">
|
||||
<div className="rounded-xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<KeyRound className="h-5 w-5" />
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="font-semibold">单点登录入口</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||
使用统一账号完成认证,减少重复登录和权限分散。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-emerald-100 text-emerald-700">
|
||||
<BadgeCheck className="h-5 w-5" />
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="font-semibold">认证状态同步</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-muted-foreground">
|
||||
登录成功后自动写入会话并跳转到控制台。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto w-full max-w-md lg:mx-0 lg:justify-self-end">
|
||||
<div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8">
|
||||
<div className="mb-8">
|
||||
<div className="mb-5 flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-primary-foreground">
|
||||
<LogIn className="h-6 w-6" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">登录</h1>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
使用 Casdoor 账号登录 IMTIM Portal
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<LogIn className="h-5 w-5" />
|
||||
<span>使用 Casdoor 登录</span>
|
||||
</button>
|
||||
<p className="mt-5 text-center text-xs leading-5 text-muted-foreground">
|
||||
登录即表示将跳转到 Casdoor 完成身份验证
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
+87
-37
@@ -3,71 +3,121 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { ArrowRight, Shield, Zap, Users } from 'lucide-react';
|
||||
import { ArrowRight, CheckCircle2, Shield, Users, Zap } from 'lucide-react';
|
||||
import Layout from '@/components/layout';
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: '安全可靠',
|
||||
description: '基于 OAuth 2.0 的企业级认证,支持统一身份入口和多因素认证。',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
title: '快速部署',
|
||||
description: '面向容器化交付,适合快速接入现有服务并降低运维成本。',
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
title: '易于扩展',
|
||||
description: '模块化架构设计,便于扩展自定义业务逻辑和第三方集成。',
|
||||
icon: Users,
|
||||
},
|
||||
];
|
||||
|
||||
const metrics = [
|
||||
['OAuth 2.0', '认证协议'],
|
||||
['Docker', '部署方式'],
|
||||
['SSO', '统一登录'],
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const { isAuthenticated, initialize } = useAuthStore();
|
||||
const { initialize } = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Hero Section */}
|
||||
<section className="w-full py-24 bg-gradient-to-b from-background to-secondary/20">
|
||||
<div className="container px-4 text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold tracking-tight mb-4">
|
||||
<div className="overflow-hidden">
|
||||
<section className="border-b bg-[linear-gradient(180deg,white_0%,oklch(0.96_0.013_247.858)_100%)]">
|
||||
<div className="mx-auto grid min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center gap-10 px-4 py-12 sm:px-6 sm:py-16 lg:grid-cols-[1fr_0.84fr] lg:py-20">
|
||||
<div className="max-w-2xl">
|
||||
<div className="mb-5 inline-flex items-center gap-2 rounded-full border bg-white px-3 py-1 text-sm font-medium text-muted-foreground shadow-sm">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-600" />
|
||||
企业登录基座
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
||||
IMTIM Portal
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
<p className="mt-5 max-w-xl text-base leading-7 text-muted-foreground sm:text-lg">
|
||||
企业级门户登录基座应用,安全、高效、易于扩展
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 text-base font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
className="inline-flex h-12 items-center justify-center gap-2 rounded-lg bg-primary px-6 text-base font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
开始使用
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.push('/dashboard')}
|
||||
className="inline-flex h-12 items-center justify-center rounded-lg border bg-white px-6 text-base font-medium shadow-sm transition hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
查看控制台
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-white p-4 shadow-sm sm:p-5">
|
||||
<div className="rounded-lg bg-secondary p-4">
|
||||
<div className="flex items-center justify-between gap-4 border-b pb-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">身份服务状态</p>
|
||||
<p className="mt-1 text-2xl font-semibold">运行正常</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-emerald-100 px-3 py-1 text-sm font-medium text-emerald-700">
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-3 lg:grid-cols-1 xl:grid-cols-3">
|
||||
{metrics.map(([value, label]) => (
|
||||
<div key={label} className="rounded-lg bg-white p-4">
|
||||
<p className="text-lg font-semibold">{value}</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="w-full py-16">
|
||||
<div className="container px-4">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">核心特性</h2>
|
||||
<div className="grid gap-8 md:grid-cols-3 max-w-5xl mx-auto">
|
||||
<div className="rounded-lg border p-6 text-center">
|
||||
<Shield className="h-12 w-12 mx-auto mb-4 text-primary" />
|
||||
<h3 className="text-xl font-semibold mb-2">安全可靠</h3>
|
||||
<p className="text-muted-foreground">
|
||||
基于 OAuth 2.0 的企业级认证,支持多因素认证
|
||||
<section className="bg-white py-12 sm:py-16">
|
||||
<div className="mx-auto w-full max-w-6xl px-4 sm:px-6">
|
||||
<div className="max-w-2xl">
|
||||
<h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">核心特性</h2>
|
||||
<p className="mt-3 text-muted-foreground">
|
||||
围绕认证、部署和扩展体验整理基础能力,适合作为内部系统统一入口。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-6 text-center">
|
||||
<Zap className="h-12 w-12 mx-auto mb-4 text-primary" />
|
||||
<h3 className="text-xl font-semibold mb-2">快速部署</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Docker 一键部署,开箱即用,降低运维成本
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-6 text-center">
|
||||
<Users className="h-12 w-12 mx-auto mb-4 text-primary" />
|
||||
<h3 className="text-xl font-semibold mb-2">易于扩展</h3>
|
||||
<p className="text-muted-foreground">
|
||||
模块化架构设计,支持自定义业务逻辑和第三方集成
|
||||
<div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{features.map((feature) => {
|
||||
const Icon = feature.icon;
|
||||
return (
|
||||
<div key={feature.title} className="rounded-xl border bg-card p-6 shadow-sm">
|
||||
<div className="mb-5 flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold">{feature.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from 'next/link';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { getLoginUrl } from '@/lib/api';
|
||||
import { LogOut, User } from 'lucide-react';
|
||||
import { LogIn, LogOut, User } from 'lucide-react';
|
||||
import * as Avatar from '@radix-ui/react-avatar';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
|
||||
@@ -23,18 +23,23 @@ export default function Header() {
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center justify-between px-4">
|
||||
<Link href="/" className="text-xl font-bold">
|
||||
<header className="sticky top-0 z-40 border-b bg-white/85 backdrop-blur-xl supports-[backdrop-filter]:bg-white/70">
|
||||
<div className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between gap-3 px-4 sm:px-6">
|
||||
<Link href="/" className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary text-sm font-bold text-primary-foreground shadow-sm">
|
||||
IM
|
||||
</span>
|
||||
<span className="truncate text-base font-semibold tracking-tight sm:text-lg">
|
||||
IMTIM Portal
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-4">
|
||||
<nav className="flex shrink-0 items-center gap-2">
|
||||
{isAuthenticated && user ? (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button className="flex items-center gap-2 rounded-full hover:bg-accent p-1">
|
||||
<Avatar.Root className="h-8 w-8 rounded-full bg-secondary flex items-center justify-center overflow-hidden">
|
||||
<button className="flex max-w-[44vw] items-center gap-2 rounded-full border bg-white p-1 pr-2 text-left shadow-sm transition hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:max-w-none sm:pr-3">
|
||||
<Avatar.Root className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-secondary">
|
||||
{user.avatar ? (
|
||||
<Avatar.Image
|
||||
src={user.avatar}
|
||||
@@ -47,16 +52,17 @@ export default function Header() {
|
||||
</Avatar.Fallback>
|
||||
)}
|
||||
</Avatar.Root>
|
||||
<span className="text-sm font-medium">{user.username}</span>
|
||||
<span className="truncate text-sm font-medium">{user.username}</span>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="min-w-[220px] bg-popover rounded-md p-1 shadow-md border"
|
||||
className="z-50 min-w-56 rounded-lg border bg-popover p-1.5 shadow-xl"
|
||||
align="end"
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 text-sm rounded cursor-pointer hover:bg-accent outline-none">
|
||||
<DropdownMenu.Item className="flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm outline-none hover:bg-accent focus:bg-accent">
|
||||
<User className="h-4 w-4" />
|
||||
<span>个人资料</span>
|
||||
</DropdownMenu.Item>
|
||||
@@ -64,7 +70,7 @@ export default function Header() {
|
||||
<DropdownMenu.Separator className="h-px bg-border my-1" />
|
||||
|
||||
<DropdownMenu.Item
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm rounded cursor-pointer hover:bg-accent text-red-600 outline-none"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm text-red-600 outline-none hover:bg-red-50 focus:bg-red-50"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
@@ -76,9 +82,10 @@ export default function Header() {
|
||||
) : (
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:px-4"
|
||||
>
|
||||
登录
|
||||
<LogIn className="h-4 w-4" />
|
||||
<span>登录</span>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
@@ -6,11 +6,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div className="flex min-h-screen flex-col bg-background text-foreground">
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<footer className="border-t py-6">
|
||||
<div className="container px-4 text-center text-sm text-muted-foreground">
|
||||
<footer className="border-t bg-white/70 py-5">
|
||||
<div className="mx-auto w-full max-w-6xl px-4 text-center text-xs text-muted-foreground sm:px-6">
|
||||
© 2026 IMTIM Portal. All rights reserved.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
|
||||
export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
const { isAuthenticated, initialize } = useAuthStore();
|
||||
const { isAuthenticated, isInitialized, initialize } = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -13,15 +13,17 @@ export default function ProtectedRoute({ children }: { children: React.ReactNode
|
||||
}, [initialize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
router.push('/login');
|
||||
if (isInitialized && !isAuthenticated) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [isAuthenticated, router]);
|
||||
}, [isInitialized, isAuthenticated, router]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (!isInitialized || !isAuthenticated) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[200px]">
|
||||
<div className="text-muted-foreground">加载中...</div>
|
||||
<div className="mx-auto flex min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center justify-center px-4 py-10 sm:px-6">
|
||||
<div className="rounded-xl border bg-white px-5 py-4 text-sm text-muted-foreground shadow-sm">
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ interface AuthState {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isInitialized: boolean;
|
||||
login: (token: string, user: User) => void;
|
||||
logout: () => void;
|
||||
initialize: () => void;
|
||||
@@ -22,6 +23,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
isInitialized: false,
|
||||
|
||||
login: (token, user) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -36,7 +38,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
set({ user: null, token: null, isAuthenticated: false });
|
||||
set({ user: null, token: null, isAuthenticated: false, isInitialized: true });
|
||||
},
|
||||
|
||||
initialize: () => {
|
||||
@@ -44,12 +46,20 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
const token = localStorage.getItem('token');
|
||||
const userStr = localStorage.getItem('user');
|
||||
if (token && userStr) {
|
||||
try {
|
||||
set({
|
||||
token,
|
||||
user: JSON.parse(userStr),
|
||||
isAuthenticated: true,
|
||||
isInitialized: true,
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
}
|
||||
set({ user: null, token: null, isAuthenticated: false, isInitialized: true });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user