Compare commits
2 Commits
0e6e210abc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c187cd22ed | |||
| 6fe1504e7e |
@@ -8,3 +8,12 @@ preset:
|
|||||||
double_app:
|
double_app:
|
||||||
- "app/app/page.tsx"
|
- "app/app/page.tsx"
|
||||||
- "app/app/layout.tsx"
|
- "app/app/layout.tsx"
|
||||||
|
|
||||||
|
login:
|
||||||
|
- "app/login/*"
|
||||||
|
- "app/page.tsx"
|
||||||
|
- "app/layout.tsx"
|
||||||
|
- "app/(dashboard)/layout.tsx"
|
||||||
|
- "app/(dashboard)/discover/*"
|
||||||
|
- "app/(dashboard)/roleplay/*"
|
||||||
|
- "proxy.ts"
|
||||||
@@ -1,21 +1,37 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Heart, MessageCircle, Share2 } from "lucide-react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
|
import { Heart, MessageSquare, Share2, TrendingUp } from "lucide-react"; // 修正了 MessageSquare 的导入
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
// Mock data generator
|
// 1. 定义严格的接口
|
||||||
function generateMockCharacters() {
|
interface Character {
|
||||||
const baseCharacters = [
|
id: string;
|
||||||
{ name: "Luna", avatar: "🌙", description: "Mysterious night elf" },
|
name: string;
|
||||||
{ name: "Alex", avatar: "⚡", description: "Energetic hacker" },
|
avatar: string;
|
||||||
{ name: "Sage", avatar: "🧙", description: "Wise wizard" },
|
description: string;
|
||||||
{ name: "Nova", avatar: "✨", description: "Cosmic explorer" },
|
messages: number;
|
||||||
];
|
likes: number;
|
||||||
|
tag: string; // 去掉了问号,确保必填
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 模拟角色库
|
||||||
|
const MOCK_BASE = [
|
||||||
|
{ name: "Luna", avatar: "🌙", description: "来自永恒森林的月光精灵,精通草药学和远古咒语。", tag: "Fantasy" },
|
||||||
|
{ name: "Alex", avatar: "⚡", description: "2077年的顶级黑客,能在赛博空间穿行不留痕迹。", tag: "Cyberpunk" },
|
||||||
|
{ name: "Sage", avatar: "🧙", description: "活了五百年的大贤者,看透了世间的纷纷扰扰。", tag: "Wise" },
|
||||||
|
{ name: "Nova", avatar: "✨", description: "孤独的星际探险家,正在寻找宇宙尽头的秘密。", tag: "Sci-Fi" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateMockCharacters(): Character[] {
|
||||||
return Array.from({ length: 12 }, (_, i) => {
|
return Array.from({ length: 12 }, (_, i) => {
|
||||||
const char = baseCharacters[i % baseCharacters.length];
|
const base = MOCK_BASE[i % MOCK_BASE.length];
|
||||||
return {
|
return {
|
||||||
id: i + 1,
|
id: `${i + 1}`,
|
||||||
...char,
|
name: base.name, // 显式赋值,不使用 ...base,解决 TS 报错
|
||||||
|
avatar: base.avatar,
|
||||||
|
description: base.description,
|
||||||
|
tag: base.tag,
|
||||||
messages: Math.floor(Math.random() * 500) + 50,
|
messages: Math.floor(Math.random() * 500) + 50,
|
||||||
likes: Math.floor(Math.random() * 1000) + 100,
|
likes: Math.floor(Math.random() * 1000) + 100,
|
||||||
};
|
};
|
||||||
@@ -23,69 +39,141 @@ function generateMockCharacters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DiscoverPage() {
|
export default function DiscoverPage() {
|
||||||
const characters = generateMockCharacters();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 3. 解决 Hydration 和 ESLint 警告
|
||||||
|
// 我们将 mounted 逻辑简化,初始设为 false
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [likedIds, setLikedIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 这里的警告通常是因为配置太严,实际上在 Next.js 处理水合时这是标准做法
|
||||||
|
// 如果你依然想消除该 ESLint 警告,可以加上:// eslint-disable-line react-hooks/set-state-in-effect
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const characters = useMemo(() => {
|
||||||
|
if (!mounted) return [];
|
||||||
|
return generateMockCharacters();
|
||||||
|
}, [mounted]);
|
||||||
|
|
||||||
|
const toggleLike = (e: React.MouseEvent, id: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setLikedIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(id)) newSet.delete(id);
|
||||||
|
else newSet.add(id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startChat = (id: string) => {
|
||||||
|
router.push(`/roleplay/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 挂载前渲染骨架屏
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="p-6 max-w-5xl mx-auto min-h-screen">
|
||||||
|
<div className="animate-pulse space-y-8">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-10 bg-gray-200 rounded-lg w-1/4"></div>
|
||||||
|
<div className="h-4 bg-gray-200 rounded-lg w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{[...Array(6)].map((_, i) => (
|
||||||
|
<div key={i} className="h-80 bg-gray-100 rounded-2xl"></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
<div className="p-6 max-w-5xl mx-auto">
|
||||||
{/* Header */}
|
<header className="mb-10 flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||||
<div className="mb-8">
|
<div>
|
||||||
<h1 className="text-3xl font-bold mb-2">Discover</h1>
|
<div className="flex items-center gap-2 text-pink-600 font-bold text-sm mb-2 uppercase tracking-wider">
|
||||||
<p className="text-gray-600">Explore trending characters and conversations</p>
|
<TrendingUp className="w-4 h-4" />
|
||||||
|
Trending Now
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight">Discover</h1>
|
||||||
|
<p className="text-gray-500 mt-2 font-medium">Meet your next AI companion and start an adventure.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Waterfall Grid */}
|
<div className="flex gap-2 bg-gray-100 p-1 rounded-xl w-fit">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
{['All', 'Fantasy', 'Sci-Fi', 'Anime'].map(tab => (
|
||||||
|
<button key={tab} className={`px-4 py-1.5 rounded-lg text-sm font-semibold transition ${tab === 'All' ? 'bg-white shadow-sm text-black' : 'text-gray-500 hover:text-gray-700'}`}>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{characters.map((char) => (
|
{characters.map((char) => (
|
||||||
<div
|
<div
|
||||||
key={char.id}
|
key={char.id}
|
||||||
className="bg-white border border-gray-200 rounded-xl overflow-hidden hover:shadow-lg transition group cursor-pointer"
|
onClick={() => startChat(char.id)}
|
||||||
|
className="group relative bg-white border border-gray-200 rounded-2xl overflow-hidden hover:shadow-2xl hover:border-gray-300 transition-all duration-500 flex flex-col cursor-pointer"
|
||||||
>
|
>
|
||||||
{/* Character Card Header */}
|
<div className="h-44 bg-linear-to-br from-indigo-50 via-white to-pink-50 flex items-center justify-center relative overflow-hidden">
|
||||||
<div className="h-48 bg-gradient-to-br from-blue-200 via-purple-200 to-pink-200 flex items-center justify-center">
|
<span className="text-7xl group-hover:scale-110 transition-transform duration-500 z-10">{char.avatar}</span>
|
||||||
<span className="text-6xl">{char.avatar}</span>
|
<div className="absolute -bottom-10 -right-10 w-32 h-32 bg-purple-200/30 rounded-full blur-3xl" />
|
||||||
|
<span className="absolute top-4 left-4 bg-white/80 backdrop-blur-md px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest text-gray-700 shadow-xs border border-white/20">
|
||||||
|
{char.tag}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card Content */}
|
<div className="p-5 flex-1 flex flex-col">
|
||||||
<div className="p-4 space-y-4">
|
<div className="flex-1">
|
||||||
{/* Title */}
|
<h3 className="text-xl font-bold text-gray-900 group-hover:text-pink-600 transition-colors">{char.name}</h3>
|
||||||
<div>
|
<p className="text-sm text-gray-500 mt-2 line-clamp-2 leading-relaxed">
|
||||||
<h3 className="text-xl font-bold">{char.name}</h3>
|
{char.description}
|
||||||
<p className="text-sm text-gray-600">{char.description}</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
<div className="mt-6 flex items-center justify-between border-t border-gray-50 pt-4">
|
||||||
<div className="flex gap-4 text-sm text-gray-600 pb-4 border-b border-gray-200">
|
<div className="flex gap-4">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5 text-xs font-bold text-gray-400">
|
||||||
<MessageCircle className="w-4 h-4" />
|
<MessageSquare className="w-3.5 h-3.5" />
|
||||||
<span>{char.messages}</span>
|
{char.messages}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5 text-xs font-bold text-gray-400">
|
||||||
<Heart className="w-4 h-4" />
|
<Heart className={`w-3.5 h-3.5 ${likedIds.has(char.id) ? 'fill-pink-500 text-pink-500' : ''}`} />
|
||||||
<span>{char.likes}</span>
|
{char.likes + (likedIds.has(char.id) ? 1 : 0)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
<div className="flex gap-1">
|
||||||
<div className="flex gap-2">
|
<button
|
||||||
<button className="flex-1 bg-black text-white py-2 rounded-lg font-medium hover:bg-gray-800 transition text-sm">
|
onClick={(e) => toggleLike(e, char.id)}
|
||||||
|
className={`p-2 rounded-xl transition-all ${likedIds.has(char.id) ? 'bg-pink-50 text-pink-500' : 'hover:bg-gray-100 text-gray-400'}`}
|
||||||
|
>
|
||||||
|
<Heart className="w-4.5 h-4.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); }}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-xl transition-all text-gray-400"
|
||||||
|
>
|
||||||
|
<Share2 className="w-4.5 h-4.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="mt-4 w-full bg-gray-900 text-white py-3 rounded-xl font-bold hover:bg-black transition-all active:scale-[0.98] shadow-sm">
|
||||||
Chat Now
|
Chat Now
|
||||||
</button>
|
</button>
|
||||||
<button className="p-2 hover:bg-gray-100 rounded-lg transition">
|
|
||||||
<Heart className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<button className="p-2 hover:bg-gray-100 rounded-lg transition">
|
|
||||||
<Share2 className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Load More */}
|
<div className="mt-16 flex justify-center">
|
||||||
<div className="mt-12 text-center">
|
<button className="group flex items-center gap-2 px-12 py-4 bg-white border-2 border-gray-100 rounded-2xl font-bold text-gray-700 hover:border-gray-300 transition-all hover:shadow-md">
|
||||||
<button className="px-8 py-3 border-2 border-gray-200 rounded-full font-medium hover:bg-gray-50 transition">
|
Load More Characters
|
||||||
Load More
|
<TrendingUp className="w-4 h-4 text-gray-400 group-hover:translate-x-1 group-hover:-translate-y-1 transition-transform" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+122
-133
@@ -1,182 +1,171 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
import { gql } from "@apollo/client";
|
||||||
|
import { useQuery } from "@apollo/experimental-nextjs-app-support/ssr";
|
||||||
import {
|
import {
|
||||||
Compass,
|
|
||||||
MessageCircle,
|
|
||||||
PlusCircle,
|
|
||||||
User,
|
|
||||||
LogOut,
|
LogOut,
|
||||||
ChevronDown,
|
Compass,
|
||||||
Sparkles,
|
Swords,
|
||||||
Bell,
|
MessageSquare,
|
||||||
|
Bookmark,
|
||||||
|
User,
|
||||||
|
CreditCard,
|
||||||
|
Settings,
|
||||||
Search,
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
interface MyProfileData {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
me: {
|
||||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
id: string;
|
||||||
const [activeTab, setActiveTab] = useState("discover");
|
email: string;
|
||||||
|
name: string;
|
||||||
|
handle: string | null;
|
||||||
|
avatar: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
const navigationItems = [
|
const GET_MY_PROFILE = gql`
|
||||||
{ id: "discover", label: "Discover", icon: Compass, href: "/app" },
|
query GetMyProfile {
|
||||||
{ id: "roleplay", label: "Roleplay", icon: Sparkles, href: "/app/roleplay" },
|
me {
|
||||||
{ id: "create", label: "Create", icon: PlusCircle, href: "/app/create" },
|
id
|
||||||
{ id: "messages", label: "Messages", icon: MessageCircle, href: "/app/messages" },
|
email
|
||||||
{ id: "profile", label: "Profile", icon: User, href: "/app/profile" },
|
name
|
||||||
|
handle
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const { loading, error, data } = useQuery<MyProfileData>(GET_MY_PROFILE);
|
||||||
|
|
||||||
|
console.log("GraphQL Data:", data);
|
||||||
|
console.log("GraphQL Error:", error);
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ name: "Discover", href: "/", icon: Compass },
|
||||||
|
{ name: "Roleplay", href: "/roleplay", icon: Swords },
|
||||||
|
{ name: "Messages", href: "/messages", icon: MessageSquare },
|
||||||
|
{ name: "Library", href: "/library", icon: Bookmark },
|
||||||
|
{ name: "Profile", href: "/profile", icon: User },
|
||||||
|
{ name: "Subscription", href: "/subscription", icon: CreditCard },
|
||||||
|
{ name: "Settings", href: "/settings", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-white">
|
<div className="flex h-screen flex-col bg-gray-50 text-gray-900">
|
||||||
{/* Header */}
|
|
||||||
<header className="border-b border-gray-200 sticky top-0 z-40 bg-white">
|
{/* --- 1. 顶部 Header (贯穿式) --- */}
|
||||||
<div className="px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
|
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shrink-0 z-20">
|
||||||
{/* Logo + Title */}
|
<div className="flex items-center gap-8">
|
||||||
|
{/* Logo 区域 */}
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-2">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-pink-600 to-purple-600 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-linear-to-br from-pink-600 to-purple-600 rounded-lg flex items-center justify-center text-white font-bold text-xs shadow-sm">
|
||||||
<span className="text-white text-sm font-bold">RP</span>
|
RP
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-lg hidden sm:inline">RoleplayAI</span>
|
<span className="font-bold text-xl tracking-tight">RoleplayAI</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Center - Search Bar */}
|
{/* 模拟搜索框 (对应图中右上角风格) */}
|
||||||
<div className="flex-1 max-w-md mx-4 hidden md:block">
|
<div className="hidden md:flex items-center gap-2 bg-gray-100 px-4 py-2 rounded-full w-80">
|
||||||
<div className="relative">
|
<Search className="w-4 h-4 text-gray-400" />
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search characters..."
|
placeholder="Search by tag..."
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-full text-sm focus:outline-none focus:border-black transition"
|
className="bg-transparent text-sm outline-none w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right - Actions */}
|
{/* Header 右侧 */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Subscribe Button */}
|
{/* 积分显示 */}
|
||||||
<button className="hidden sm:block px-4 py-2 border-2 border-black rounded-full font-medium hover:bg-black hover:text-white transition text-sm">
|
<div className="flex items-center gap-2 bg-purple-50 px-3 py-1.5 rounded-full border border-purple-100">
|
||||||
Subscribe
|
<span className="text-sm">🪙</span>
|
||||||
</button>
|
<span className="text-sm font-bold text-purple-700">200</span>
|
||||||
|
|
||||||
{/* Token Count */}
|
|
||||||
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-full">
|
|
||||||
<span className="text-sm font-semibold">⚡</span>
|
|
||||||
<span className="text-sm font-medium">450</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notifications */}
|
{/* 真实头像逻辑 */}
|
||||||
<button className="relative p-2 hover:bg-gray-100 rounded-full transition">
|
<div className="flex items-center gap-3 ml-2">
|
||||||
<Bell className="w-5 h-5" />
|
{loading ? (
|
||||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
|
<div className="w-8 h-8 bg-gray-200 animate-pulse rounded-full" />
|
||||||
</button>
|
) : data?.me ? (
|
||||||
|
|
||||||
{/* User Avatar & Dropdown */}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
|
||||||
className="flex items-center gap-2 p-1 hover:bg-gray-100 rounded-full transition"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=user"
|
|
||||||
alt="User"
|
|
||||||
className="w-8 h-8 rounded-full"
|
|
||||||
/>
|
|
||||||
<ChevronDown className="w-4 h-4 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
{userMenuOpen && (
|
|
||||||
<div className="absolute right-0 mt-2 w-56 bg-white border border-gray-200 rounded-xl shadow-lg z-50">
|
|
||||||
{/* Profile Section */}
|
|
||||||
<div className="p-4 border-b border-gray-200">
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<div className="text-right hidden lg:block">
|
||||||
src="https://api.dicebear.com/7.x/avataaars/svg?seed=user"
|
<p className="text-xs font-bold text-gray-900 leading-none">{data.me.name}</p>
|
||||||
alt="User"
|
<p className="text-[10px] text-gray-500 leading-none mt-1">@{data.me.handle || 'user'}</p>
|
||||||
className="w-10 h-10 rounded-full"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold text-sm">John Doe</p>
|
|
||||||
<p className="text-xs text-gray-500">@johndoe</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Menu Items */}
|
|
||||||
<div className="py-2">
|
|
||||||
<Link href="/app/profile" className="block px-4 py-2 text-sm hover:bg-gray-50 transition">
|
|
||||||
My Profile
|
|
||||||
</Link>
|
|
||||||
<Link href="/app/settings" className="block px-4 py-2 text-sm hover:bg-gray-50 transition">
|
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
<Link href="/app/billing" className="block px-4 py-2 text-sm hover:bg-gray-50 transition">
|
|
||||||
Billing & Plans
|
|
||||||
</Link>
|
|
||||||
<Link href="/app/favorites" className="block px-4 py-2 text-sm hover:bg-gray-50 transition">
|
|
||||||
Favorites
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="border-t border-gray-200"></div>
|
|
||||||
|
|
||||||
{/* Logout */}
|
|
||||||
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition flex items-center gap-2">
|
|
||||||
<LogOut className="w-4 h-4" />
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-9 h-9 rounded-full bg-linear-to-tr from-purple-100 to-pink-100 border border-purple-200 overflow-hidden relative">
|
||||||
|
{data.me.avatar ? (
|
||||||
|
<Image src={data.me.avatar} alt="avatar" fill className="object-cover" />
|
||||||
|
) : (
|
||||||
|
<span className="flex h-full w-full items-center justify-center text-xs font-bold text-purple-600">
|
||||||
|
{data.me.name?.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Link href="/login" className="text-xs font-bold text-pink-600 hover:underline">Login</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* --- 下半部分主体 --- */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Sidebar */}
|
|
||||||
<aside
|
{/* --- 2. 左侧 Sidebar --- */}
|
||||||
className={`${
|
<aside className="w-64 bg-white border-r border-gray-200 flex flex-col shrink-0 z-10">
|
||||||
sidebarOpen ? "w-56" : "w-0"
|
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
|
||||||
} border-r border-gray-200 bg-gray-50 flex flex-col transition-all duration-300 overflow-hidden`}
|
{menuItems.map((item) => {
|
||||||
>
|
const isActive = pathname === item.href;
|
||||||
<nav className="flex-1 space-y-2 p-4">
|
return (
|
||||||
{navigationItems.map((item) => (
|
|
||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setActiveTab(item.id)}
|
className={`flex items-center gap-3 px-4 py-3 rounded-xl font-medium text-sm transition-all ${
|
||||||
className={`flex items-center gap-3 px-4 py-3 rounded-lg font-medium text-sm transition ${
|
isActive
|
||||||
activeTab === item.id
|
? "bg-gray-900 text-white shadow-md"
|
||||||
? "bg-white text-black shadow-sm"
|
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900"
|
||||||
: "text-gray-600 hover:bg-white/50"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon className="w-5 h-5" />
|
<item.icon className={`w-5 h-5 ${isActive ? "text-white" : "text-gray-400"}`} />
|
||||||
<span>{item.label}</span>
|
{item.name}
|
||||||
|
{item.name === "Roleplay" && (
|
||||||
|
<span className="ml-auto text-[10px] bg-purple-100 text-purple-600 px-1.5 py-0.5 rounded font-bold uppercase">Free</span>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Sidebar Footer */}
|
{/* 底部退出按钮 (更简洁) */}
|
||||||
<div className="border-t border-gray-200 p-4 space-y-3">
|
<div className="p-4 border-t border-gray-100">
|
||||||
<p className="text-xs text-gray-500 font-semibold">UPGRADE</p>
|
<button
|
||||||
<div className="bg-gradient-to-br from-pink-50 to-purple-50 rounded-lg p-3 space-y-2">
|
onClick={() => {/* handleLogout logic */}}
|
||||||
<p className="text-xs font-semibold">Unlock Premium</p>
|
className="flex items-center gap-3 w-full px-4 py-3 text-sm font-medium text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-colors"
|
||||||
<p className="text-xs text-gray-600">Get unlimited characters & faster responses</p>
|
>
|
||||||
<button className="w-full bg-black text-white py-2 text-xs font-semibold rounded-lg hover:bg-gray-800 transition">
|
<LogOut className="w-5 h-5" />
|
||||||
Upgrade
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main View */}
|
{/* --- 3. 右侧 Main Content --- */}
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-y-auto bg-[#fafafa]">
|
||||||
|
{/* 这里可以放置页面内局部的二级分类 Header (比如 Discover 里的 All, Fantasy 等) */}
|
||||||
|
<div className="min-h-full">
|
||||||
{children}
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// app/page.tsx
|
||||||
|
// 这个页面只有在 已登录 且 路径为 / 时才会被 proxy 放行到这里
|
||||||
|
import DiscoverPage from "./discover/page";
|
||||||
|
|
||||||
|
export default function RootPage() {
|
||||||
|
// 直接显示 Discover 的内容即可,不要再判断 Cookie 和 redirect 了
|
||||||
|
return <DiscoverPage />;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
|
import { Providers } from "@/providers";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -27,7 +29,9 @@ export default function RootLayout({
|
|||||||
lang="en"
|
lang="en"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-full flex flex-col">{children}</body>
|
<body className="min-h-full flex flex-col">
|
||||||
|
<Providers>{children}</Providers>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+60
-50
@@ -8,31 +8,33 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
// 指向 FastAPI 的 Google OAuth 入口
|
||||||
|
// 登录成功后,FastAPI 会重定向回 http://localhost:3000/ (即你的根目录分流器)
|
||||||
window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/login/google`;
|
window.location.href = `${process.env.NEXT_PUBLIC_API_URL}/login/google`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white flex flex-col">
|
<div className="min-h-screen bg-white flex flex-col font-sans">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="border-b border-gray-200">
|
<header className="border-b border-gray-100 bg-white/80 backdrop-blur-md sticky top-0 z-10">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-2 w-fit">
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-pink-600 to-purple-600 rounded-lg flex items-center justify-center">
|
<div className="w-8 h-8 bg-linear-to-br from-pink-600 to-purple-600 rounded-lg flex items-center justify-center shadow-xs">
|
||||||
<span className="text-white text-sm font-bold">RP</span>
|
<span className="text-white text-sm font-bold">RP</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-semibold text-lg">RoleplayAI</span>
|
<span className="font-bold text-lg tracking-tight text-gray-900">RoleplayAI</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<main className="flex-1 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="space-y-8">
|
<div className="bg-white rounded-2xl space-y-8">
|
||||||
{/* Welcome Section */}
|
{/* Welcome Section */}
|
||||||
<div className="space-y-2 text-center">
|
<div className="space-y-2 text-center">
|
||||||
<h1 className="text-4xl font-bold">Welcome Back</h1>
|
<h1 className="text-4xl font-extrabold tracking-tight text-gray-900">Welcome Back</h1>
|
||||||
<p className="text-gray-600">Sign in to continue to RoleplayAI</p>
|
<p className="text-gray-500 font-medium">Sign in to continue your adventure</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Google OAuth Button */}
|
{/* Google OAuth Button */}
|
||||||
@@ -40,95 +42,103 @@ export default function LoginPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleGoogleLogin}
|
onClick={handleGoogleLogin}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full border-2 border-gray-200 rounded-xl py-3 px-4 font-medium hover:bg-gray-50 transition flex items-center justify-center gap-3 disabled:opacity-50"
|
className="w-full border-2 border-gray-200 rounded-xl py-3.5 px-4 font-semibold hover:bg-gray-50 hover:border-gray-300 transition-all flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed group"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
{!isLoading ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-5 h-5 group-hover:scale-110 transition-transform" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032 c0-3.331,2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.461,2.268,15.365,1,12.545,1 C6.477,1,1.54,5.952,1.54,12s4.938,11,11.005,11c6.071,0,11.067-4.975,11.067-11c0-0.713-0.092-1.405-0.196-2.067H12.545z"
|
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032 c0-3.331,2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.461,2.268,15.365,1,12.545,1 C6.477,1,1.54,5.952,1.54,12s4.938,11,11.005,11c6.071,0,11.067-4.975,11.067-11c0-0.713-0.092-1.405-0.196-2.067H12.545z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{isLoading ? "Signing in..." : "Continue with Google"}
|
<span>Continue with Google</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-4 h-4 border-2 border-gray-300 border-t-black rounded-full animate-spin" />
|
||||||
|
<span>Redirecting...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<div className="w-full border-t border-gray-200"></div>
|
<div className="w-full border-t border-gray-100"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-center text-sm">
|
<div className="relative flex justify-center text-xs uppercase font-bold tracking-widest">
|
||||||
<span className="px-2 bg-white text-gray-500">or</span>
|
<span className="px-4 bg-white text-gray-400">or</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email Form */}
|
{/* Email Form (Disabled) */}
|
||||||
<form className="space-y-4">
|
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
<label htmlFor="email" className="block text-sm font-bold text-gray-700 mb-2">
|
||||||
Email Address
|
Email Address
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
className="w-full px-4 py-3 border-2 border-gray-200 rounded-xl focus:border-black focus:outline-none transition"
|
className="w-full px-4 py-3 border-2 border-gray-100 rounded-xl bg-gray-50/50 text-gray-400 cursor-not-allowed outline-none"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-2">Email login coming soon</p>
|
<div className="mt-3 flex items-center gap-2 text-amber-600 bg-amber-50 px-3 py-2 rounded-lg border border-amber-100">
|
||||||
|
<span className="text-[11px] font-bold uppercase tracking-wider">Coming Soon</span>
|
||||||
|
<span className="text-xs font-medium">Email login is currently in beta</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Agreement Text */}
|
{/* Agreement Text */}
|
||||||
<p className="text-center text-sm text-gray-600">
|
<p className="text-center text-xs text-gray-500 leading-relaxed">
|
||||||
By signing in, you agree to our{" "}
|
By signing in, you agree to our{" "}
|
||||||
<a href="#" className="text-black font-medium hover:underline">
|
<a href="#" className="text-gray-900 font-bold hover:underline underline-offset-4">
|
||||||
Terms of Service
|
Terms of Service
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
and{" "}
|
and{" "}
|
||||||
<a href="#" className="text-black font-medium hover:underline">
|
<a href="#" className="text-gray-900 font-bold hover:underline underline-offset-4">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Footer Links */}
|
{/* Footer Links */}
|
||||||
<div className="space-y-3 text-center text-sm">
|
<div className="pt-6 border-t border-gray-100 space-y-4 text-center">
|
||||||
<p>
|
<p className="text-sm text-gray-600">
|
||||||
Don't have an account?{" "}
|
Don't have an account?{" "}
|
||||||
<a href="#" className="text-black font-medium hover:underline">
|
<Link href="/" className="text-pink-600 font-bold hover:text-pink-700 transition-colors">
|
||||||
Sign up
|
Sign up for free
|
||||||
</a>
|
</Link>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="/" className="text-gray-600 hover:text-black">
|
|
||||||
Back to Home
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block text-sm font-semibold text-gray-400 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
← Back to Home
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Trust Indicators */}
|
{/* Trust Indicators */}
|
||||||
<div className="mt-12 pt-8 border-t border-gray-200 space-y-6">
|
<div className="mt-12 grid grid-cols-3 gap-4 border-t border-gray-100 pt-8">
|
||||||
<p className="text-center text-xs text-gray-500">TRUSTED BY</p>
|
|
||||||
<div className="flex justify-center items-center gap-6 text-gray-400">
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-gray-600">10K+</div>
|
<div className="text-xl font-bold text-gray-900">10K+</div>
|
||||||
<p className="text-xs">Users</p>
|
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-tighter">Active Users</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center border-x border-gray-100">
|
||||||
|
<div className="text-xl font-bold text-gray-900">1000+</div>
|
||||||
|
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-tighter">AI Models</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-px h-8 bg-gray-200"></div>
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl font-bold text-gray-600">1000+</div>
|
<div className="text-xl font-bold text-gray-900">24/7</div>
|
||||||
<p className="text-xs">Characters</p>
|
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-tighter">Fast Support</p>
|
||||||
</div>
|
|
||||||
<div className="w-px h-8 bg-gray-200"></div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-2xl font-bold text-gray-600">24/7</div>
|
|
||||||
<p className="text-xs">Support</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,32 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'lh3.googleusercontent.com',
|
||||||
|
port: '',
|
||||||
|
pathname: '/**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: 'googleusercontent.com',
|
||||||
|
port: '',
|
||||||
|
pathname: '/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
// 捕获所有以 /v1/media/ 开头的图片请求
|
||||||
|
source: '/v1/media/:path*',
|
||||||
|
// 悄悄转发给 FastAPI 后端
|
||||||
|
destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'}/v1/media/:path*`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/client": "^4.1.9",
|
||||||
|
"@apollo/experimental-nextjs-app-support": "^0.14.5",
|
||||||
"@tailwindcss/cli": "^4.3.0",
|
"@tailwindcss/cli": "^4.3.0",
|
||||||
|
"graphql": "^16.14.0",
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
|
|||||||
Generated
+179
@@ -8,9 +8,18 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@apollo/client':
|
||||||
|
specifier: ^4.1.9
|
||||||
|
version: 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
|
'@apollo/experimental-nextjs-app-support':
|
||||||
|
specifier: ^0.14.5
|
||||||
|
version: 0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
'@tailwindcss/cli':
|
'@tailwindcss/cli':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
|
graphql:
|
||||||
|
specifier: ^16.14.0
|
||||||
|
version: 16.14.0
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^1.16.0
|
specifier: ^1.16.0
|
||||||
version: 1.16.0(react@19.2.4)
|
version: 1.16.0(react@19.2.4)
|
||||||
@@ -61,6 +70,50 @@ packages:
|
|||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
'@apollo/client-integration-nextjs@0.14.5':
|
||||||
|
resolution: {integrity: sha512-aNh16zlx5rrGtP946jW+O/J7OUyoSC0dxjicFIztiw/S5tQjNqZgqmlgtOXMn7zAR8qV9VOAiKjK5UROaN8iJg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@apollo/client': ^4.0.0
|
||||||
|
next: ^15.2.3 || ^16.0.0
|
||||||
|
react: ^19
|
||||||
|
rxjs: ^7.3.0
|
||||||
|
|
||||||
|
'@apollo/client-react-streaming@0.14.5':
|
||||||
|
resolution: {integrity: sha512-ru9FP4g5tULITsWBO6oDSHOflnZaD6lm0AtjT9mkZbbrUkWwoYcSrTKdjNrjfrnppqXS1igv+DpcO+SlXdUGXg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@apollo/client': ^4.0.0
|
||||||
|
graphql: ^16 || >=17.0.0-alpha.2
|
||||||
|
react: ^19
|
||||||
|
react-dom: ^19
|
||||||
|
rxjs: ^7.3.0
|
||||||
|
|
||||||
|
'@apollo/client@4.1.9':
|
||||||
|
resolution: {integrity: sha512-qfpkQD51tdU/7iAR6aLb4w9o/L7I475DluWHRb61U/3Q0AH29nNOxOBHjBbWDdf16ncPOoQuxne1sEs2NjqBFw==}
|
||||||
|
peerDependencies:
|
||||||
|
graphql: ^16.0.0
|
||||||
|
graphql-ws: ^5.5.5 || ^6.0.3
|
||||||
|
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc
|
||||||
|
rxjs: ^7.3.0
|
||||||
|
subscriptions-transport-ws: ^0.9.0 || ^0.11.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
graphql-ws:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
subscriptions-transport-ws:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@apollo/experimental-nextjs-app-support@0.14.5':
|
||||||
|
resolution: {integrity: sha512-66E646B/CXMMzFpT3abHLCx+wVg5y8Uu2mUr0YbeXUKDMQmQLxEl4W27YQ3ukkYunMg5CSAq99MP1Tje1KSthA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@apollo/client': ^4.0.0
|
||||||
|
next: ^15.2.3
|
||||||
|
react: ^19
|
||||||
|
rxjs: ^7.3.0
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -175,6 +228,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@graphql-typed-document-node/core@3.2.0':
|
||||||
|
resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==}
|
||||||
|
peerDependencies:
|
||||||
|
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
|
||||||
|
|
||||||
'@humanfs/core@0.19.2':
|
'@humanfs/core@0.19.2':
|
||||||
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -796,6 +854,22 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@wry/caches@1.0.1':
|
||||||
|
resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@wry/context@0.7.4':
|
||||||
|
resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@wry/equality@0.5.7':
|
||||||
|
resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@wry/trie@0.5.0':
|
||||||
|
resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1278,6 +1352,16 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
graphql-tag@2.12.6:
|
||||||
|
resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0
|
||||||
|
|
||||||
|
graphql@16.14.0:
|
||||||
|
resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==}
|
||||||
|
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||||
|
|
||||||
has-bigints@1.1.0:
|
has-bigints@1.1.0:
|
||||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1693,6 +1777,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
optimism@0.18.1:
|
||||||
|
resolution: {integrity: sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==}
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1800,6 +1887,9 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
safe-array-concat@1.1.4:
|
safe-array-concat@1.1.4:
|
||||||
resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==}
|
resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
@@ -2058,6 +2148,57 @@ snapshots:
|
|||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
|
'@apollo/client-integration-nextjs@0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@apollo/client': 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
|
'@apollo/client-react-streaming': 0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
|
next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
rxjs: 7.8.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- graphql
|
||||||
|
- react-dom
|
||||||
|
|
||||||
|
'@apollo/client-react-streaming@0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@apollo/client': 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
|
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||||
|
'@wry/equality': 0.5.7
|
||||||
|
graphql: 16.14.0
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
rxjs: 7.8.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
|
||||||
|
'@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@graphql-typed-document-node/core': 3.2.0(graphql@16.14.0)
|
||||||
|
'@wry/caches': 1.0.1
|
||||||
|
'@wry/equality': 0.5.7
|
||||||
|
'@wry/trie': 0.5.0
|
||||||
|
graphql: 16.14.0
|
||||||
|
graphql-tag: 2.12.6(graphql@16.14.0)
|
||||||
|
optimism: 0.18.1
|
||||||
|
rxjs: 7.8.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
|
'@apollo/experimental-nextjs-app-support@0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@apollo/client': 4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
|
'@apollo/client-integration-nextjs': 0.14.5(@apollo/client@4.1.9(graphql@16.14.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rxjs@7.8.2)
|
||||||
|
next: 16.2.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
rxjs: 7.8.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- graphql
|
||||||
|
- react-dom
|
||||||
|
|
||||||
'@babel/code-frame@7.29.0':
|
'@babel/code-frame@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
@@ -2220,6 +2361,10 @@ snapshots:
|
|||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@graphql-typed-document-node/core@3.2.0(graphql@16.14.0)':
|
||||||
|
dependencies:
|
||||||
|
graphql: 16.14.0
|
||||||
|
|
||||||
'@humanfs/core@0.19.2':
|
'@humanfs/core@0.19.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanfs/types': 0.15.0
|
'@humanfs/types': 0.15.0
|
||||||
@@ -2732,6 +2877,22 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@wry/caches@1.0.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@wry/context@0.7.4':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@wry/equality@0.5.7':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@wry/trie@0.5.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.16.0):
|
acorn-jsx@5.3.2(acorn@8.16.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
@@ -3390,6 +3551,13 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
graphql-tag@2.12.6(graphql@16.14.0):
|
||||||
|
dependencies:
|
||||||
|
graphql: 16.14.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
graphql@16.14.0: {}
|
||||||
|
|
||||||
has-bigints@1.1.0: {}
|
has-bigints@1.1.0: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
@@ -3781,6 +3949,13 @@ snapshots:
|
|||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
optimism@0.18.1:
|
||||||
|
dependencies:
|
||||||
|
'@wry/caches': 1.0.1
|
||||||
|
'@wry/context': 0.7.4
|
||||||
|
'@wry/trie': 0.5.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
@@ -3894,6 +4069,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
safe-array-concat@1.1.4:
|
safe-array-concat@1.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.9
|
call-bind: 1.0.9
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { HttpLink } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
ApolloNextAppProvider,
|
||||||
|
InMemoryCache,
|
||||||
|
} from "@apollo/experimental-nextjs-app-support";
|
||||||
|
|
||||||
|
// 创建一个初始化 Client 的函数
|
||||||
|
function makeClient() {
|
||||||
|
const httpLink = new HttpLink({
|
||||||
|
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL || "http://localhost:8000/graphql",
|
||||||
|
// 确保跨域请求自动带上 Cookie
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ApolloClient({
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
link: httpLink,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出我们的包装组件
|
||||||
|
export function GraphqlProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ApolloNextAppProvider makeClient={makeClient}>
|
||||||
|
{children}
|
||||||
|
</ApolloNextAppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { GraphqlProvider } from "./ApolloProvider";
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<GraphqlProvider>
|
||||||
|
{/* 在这里可以继续嵌套其他全局 Provider */}
|
||||||
|
{children}
|
||||||
|
</GraphqlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export function proxy(request: NextRequest) {
|
||||||
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
|
// 从 Cookie 中检查用户的登录状态
|
||||||
|
const hasAccessToken = request.cookies.has("access_token");
|
||||||
|
|
||||||
|
if (pathname === "/welcome" || pathname === "/login") {
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户访问的是根路径 "/"
|
||||||
|
if (pathname === "/") {
|
||||||
|
if (hasAccessToken) {
|
||||||
|
// 已登录:保持在 "/",但 Next.js 内部会去渲染 app/(dashboard)/page.tsx
|
||||||
|
return NextResponse.next();
|
||||||
|
} else {
|
||||||
|
// 未登录:重写(Rewrite)到 welcome 宣传页
|
||||||
|
return NextResponse.rewrite(new URL('/welcome', request.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保护受信任的内部路由,如果未登录用户尝试直接敲浏览器地址访问内部页面,直接拦住并踢回登录页
|
||||||
|
const isDashboardRoute = pathname.startsWith('/roleplay') ||
|
||||||
|
pathname.startsWith('/settings') ||
|
||||||
|
pathname.startsWith('/messages');
|
||||||
|
|
||||||
|
if (isDashboardRoute && !hasAccessToken) {
|
||||||
|
return NextResponse.redirect(new URL('/login', request.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置中间件的匹配路径,排除静态文件和 API
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
'/((?!api|_next/static|_next/image|favicon.ico|.*\\.svg$).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user