ソースを参照

Update package.json and layout components to integrate authentication. Added new dependency for Ant Design patch and modified layout to include AuthProvider. Updated page routing logic for user authentication flow.

0es 4 ヶ月 前
コミット
5c3c628cde

+ 4 - 3
package.json

@@ -3,14 +3,15 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "dev": "next dev",
-    "build": "next build",
-    "start": "next start",
+    "dev": "next dev --webpack",
+    "build": "next build --webpack",
+    "start": "next start --webpack",
     "lint": "biome check",
     "format": "biome format --write"
   },
   "dependencies": {
     "@ant-design/nextjs-registry": "^1.2.0",
+    "@ant-design/v5-patch-for-react-19": "^1.0.3",
     "antd": "^5.28.0",
     "next": "16.0.1",
     "react": "19.2.0",

+ 9 - 0
src/app/(auth)/layout.tsx

@@ -0,0 +1,9 @@
+import type React from "react";
+
+export default function AuthLayout({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  return <>{children}</>;
+}

+ 115 - 0
src/app/(auth)/login/page.tsx

@@ -0,0 +1,115 @@
+"use client";
+
+import { LockOutlined, UserOutlined } from "@ant-design/icons";
+import { Button, Card, Form, Input, Typography } from "antd";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { useAuth } from "@/contexts/AuthContext";
+
+const { Title, Text } = Typography;
+
+export default function LoginPage() {
+  const { login, isAuthenticated } = useAuth();
+  const router = useRouter();
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    // If already authenticated, redirect to home
+    if (isAuthenticated) {
+      router.push("/home");
+    }
+  }, [isAuthenticated, router]);
+
+  const onFinish = async (values: { username: string; password: string }) => {
+    setLoading(true);
+    try {
+      await login(values.username, values.password);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div
+      style={{
+        minHeight: "100vh",
+        display: "flex",
+        alignItems: "center",
+        justifyContent: "center",
+        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
+        padding: "20px",
+      }}
+    >
+      <Card
+        style={{
+          width: "100%",
+          maxWidth: "420px",
+          boxShadow: "0 8px 24px rgba(0, 0, 0, 0.12)",
+          borderRadius: "16px",
+        }}
+      >
+        <div style={{ textAlign: "center", marginBottom: "32px" }}>
+          <Title level={2} style={{ marginBottom: "8px", color: "#667eea" }}>
+            Lanu 后台管理系统
+          </Title>
+          <Text type="secondary">欢迎回来,请登录您的账号</Text>
+        </div>
+
+        <Form
+          name="login"
+          initialValues={{ username: "admin", password: "admin123" }}
+          onFinish={onFinish}
+          size="large"
+          layout="vertical"
+        >
+          <Form.Item
+            name="username"
+            label="用户名"
+            rules={[{ required: true, message: "请输入用户名!" }]}
+          >
+            <Input
+              prefix={<UserOutlined style={{ color: "#667eea" }} />}
+              placeholder="请输入用户名"
+              autoComplete="username"
+            />
+          </Form.Item>
+
+          <Form.Item
+            name="password"
+            label="密码"
+            rules={[{ required: true, message: "请输入密码!" }]}
+          >
+            <Input.Password
+              prefix={<LockOutlined style={{ color: "#667eea" }} />}
+              placeholder="请输入密码"
+              autoComplete="current-password"
+            />
+          </Form.Item>
+
+          <Form.Item style={{ marginBottom: "16px" }}>
+            <Button
+              type="primary"
+              htmlType="submit"
+              block
+              loading={loading}
+              style={{
+                height: "44px",
+                fontSize: "16px",
+                background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
+                border: "none",
+              }}
+            >
+              {loading ? "登录中..." : "登录"}
+            </Button>
+          </Form.Item>
+        </Form>
+
+        <div style={{ textAlign: "center", marginTop: "16px" }}>
+          <Text type="secondary" style={{ fontSize: "12px" }}>
+            默认账号: admin / 123456
+          </Text>
+        </div>
+      </Card>
+    </div>
+  );
+}

+ 13 - 0
src/app/(dashboard)/home/page.tsx

@@ -0,0 +1,13 @@
+import type React from "react";
+
+const HomePage: React.FC = () => {
+  return (
+    <div>
+      <h1>首页</h1>
+      <p>欢迎来到 Lanu 运营管理后台</p>
+    </div>
+  );
+};
+
+export default HomePage;
+

+ 190 - 0
src/app/(dashboard)/layout.tsx

@@ -0,0 +1,190 @@
+"use client";
+
+import {
+  HomeOutlined,
+  LogoutOutlined,
+  TeamOutlined,
+  UserOutlined,
+} from "@ant-design/icons";
+import type { MenuProps } from "antd";
+import {
+  Avatar,
+  Breadcrumb,
+  Dropdown,
+  Layout,
+  Menu,
+  Space,
+  Typography,
+  theme,
+} from "antd";
+import { usePathname, useRouter } from "next/navigation";
+import type React from "react";
+import { useEffect, useMemo, useState } from "react";
+import { useAuth } from "@/contexts/AuthContext";
+
+const { Text } = Typography;
+
+const { Header, Content, Footer, Sider } = Layout;
+
+type MenuItem = Required<MenuProps>["items"][number];
+
+function getItem(
+  label: React.ReactNode,
+  key: React.Key,
+  icon?: React.ReactNode,
+  children?: MenuItem[],
+): MenuItem {
+  return {
+    key,
+    icon,
+    children,
+    label,
+  } as MenuItem;
+}
+
+const items: MenuItem[] = [
+  getItem("首页", "home", <HomeOutlined />),
+  getItem("陪玩管理", "user", <UserOutlined />, [
+    getItem("陪玩列表", "user-list"),
+  ]),
+  getItem("后台用户管理", "user-admin", <TeamOutlined />, [
+    getItem("用户列表", "user-admin-list"),
+  ]),
+];
+
+// Path mapping
+const pathToKeyMap: Record<string, string> = {
+  "/home": "home",
+  "/user/list": "user-list",
+  "/user-admin/list": "user-admin-list",
+};
+
+const keyToPathMap: Record<string, string> = {
+  home: "/home",
+  "user-list": "/user/list",
+  "user-admin-list": "/user-admin/list",
+};
+
+const keyToOpenKeysMap: Record<string, string[]> = {
+  "user-list": ["user"],
+  "user-admin-list": ["user-admin"],
+};
+
+const pathToBreadcrumb: Record<string, { title: string }[]> = {
+  "/home": [{ title: "首页" }],
+  "/user/list": [{ title: "陪玩管理" }, { title: "陪玩列表" }],
+  "/user-admin/list": [{ title: "后台用户管理" }, { title: "用户列表" }],
+};
+
+export default function DashboardLayout({
+  children,
+}: {
+  children: React.ReactNode;
+}) {
+  const [collapsed, setCollapsed] = useState(false);
+  const router = useRouter();
+  const pathname = usePathname();
+  const { user, logout, isAuthenticated, isLoading } = useAuth();
+  const {
+    token: { colorBgContainer, borderRadiusLG },
+  } = theme.useToken();
+
+  // Redirect to login if not authenticated
+  useEffect(() => {
+    if (!isLoading && !isAuthenticated) {
+      router.push("/login");
+    }
+  }, [isAuthenticated, isLoading, router]);
+
+  // Get current selected key and open keys based on pathname
+  const selectedKey = useMemo(() => {
+    return pathToKeyMap[pathname] || "home";
+  }, [pathname]);
+
+  const defaultOpenKeys = useMemo(() => {
+    return keyToOpenKeysMap[selectedKey] || [];
+  }, [selectedKey]);
+
+  const breadcrumbItems = useMemo(() => {
+    return pathToBreadcrumb[pathname] || [{ title: "首页" }];
+  }, [pathname]);
+
+  const handleMenuClick = (e: { key: string }) => {
+    const path = keyToPathMap[e.key];
+    if (path) {
+      router.push(path);
+    }
+  };
+
+  // User dropdown menu
+  const userMenuItems: MenuProps["items"] = [
+    {
+      key: "logout",
+      icon: <LogoutOutlined />,
+      label: "退出登录",
+      onClick: logout,
+    },
+  ];
+
+  // Show loading or redirect
+  if (isLoading || !isAuthenticated) {
+    return null;
+  }
+
+  return (
+    <Layout style={{ minHeight: "100vh" }}>
+      <Sider
+        collapsible
+        collapsed={collapsed}
+        onCollapse={(value) => setCollapsed(value)}
+      >
+        <div className="demo-logo-vertical" />
+        <Menu
+          theme="dark"
+          selectedKeys={[selectedKey]}
+          defaultOpenKeys={defaultOpenKeys}
+          mode="inline"
+          items={items}
+          onClick={handleMenuClick}
+        />
+      </Sider>
+      <Layout>
+        <Header
+          style={{
+            padding: "0 24px",
+            background: colorBgContainer,
+            display: "flex",
+            alignItems: "center",
+            justifyContent: "flex-end",
+          }}
+        >
+          <Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
+            <Space style={{ cursor: "pointer" }}>
+              <Avatar
+                style={{ backgroundColor: "#667eea" }}
+                icon={<UserOutlined />}
+              />
+              <Text strong>{user?.username}</Text>
+            </Space>
+          </Dropdown>
+        </Header>
+        <Content style={{ margin: "0 16px" }}>
+          <Breadcrumb style={{ margin: "16px 0" }} items={breadcrumbItems} />
+          <div
+            style={{
+              padding: 24,
+              minHeight: 360,
+              background: colorBgContainer,
+              borderRadius: borderRadiusLG,
+            }}
+          >
+            {children}
+          </div>
+        </Content>
+        <Footer style={{ textAlign: "center" }}>
+          Lanu ©{new Date().getFullYear()} Created by Lanu FE Team
+        </Footer>
+      </Layout>
+    </Layout>
+  );
+}

+ 13 - 0
src/app/(dashboard)/user-admin/list/page.tsx

@@ -0,0 +1,13 @@
+import type React from "react";
+
+const UserAdminListPage: React.FC = () => {
+  return (
+    <div>
+      <h1>后台用户列表</h1>
+      <p>这里将展示后台管理用户列表</p>
+    </div>
+  );
+};
+
+export default UserAdminListPage;
+

+ 13 - 0
src/app/(dashboard)/user/list/page.tsx

@@ -0,0 +1,13 @@
+import type React from "react";
+
+const UserListPage: React.FC = () => {
+  return (
+    <div>
+      <h1>陪玩列表</h1>
+      <p>这里将展示陪玩用户列表</p>
+    </div>
+  );
+};
+
+export default UserListPage;
+

+ 9 - 4
src/app/layout.tsx

@@ -1,7 +1,10 @@
+import "@ant-design/v5-patch-for-react-19";
+
 import { AntdRegistry } from "@ant-design/nextjs-registry";
 import type { Metadata } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
+import { AuthProvider } from "@/contexts/AuthContext";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
@@ -14,8 +17,8 @@ const geistMono = Geist_Mono({
 });
 
 export const metadata: Metadata = {
-  title: "Create Next App",
-  description: "Generated by create next app",
+  title: "Lanu OP - 运营管理后台",
+  description: "Lanu operation platform",
 };
 
 export default function RootLayout({
@@ -24,11 +27,13 @@ export default function RootLayout({
   children: React.ReactNode;
 }>) {
   return (
-    <html lang="en">
+    <html lang="zh-CN">
       <body
         className={`${geistSans.variable} ${geistMono.variable} antialiased`}
       >
-        <AntdRegistry>{children}</AntdRegistry>
+        <AntdRegistry>
+          <AuthProvider>{children}</AuthProvider>
+        </AntdRegistry>
       </body>
     </html>
   );

+ 29 - 79
src/app/page.tsx

@@ -1,84 +1,34 @@
 "use client";
 
-import { HomeOutlined, TeamOutlined, UserOutlined } from "@ant-design/icons";
-import type { MenuProps } from "antd";
-import { Breadcrumb, Layout, Menu, theme } from "antd";
-import type React from "react";
-import { useState } from "react";
-
-const { Header, Content, Footer, Sider } = Layout;
-
-type MenuItem = Required<MenuProps>["items"][number];
-
-function getItem(
-  label: React.ReactNode,
-  key: React.Key,
-  icon?: React.ReactNode,
-  children?: MenuItem[],
-): MenuItem {
-  return {
-    key,
-    icon,
-    children,
-    label,
-  } as MenuItem;
-}
-
-const items: MenuItem[] = [
-  getItem("首页", "home", <HomeOutlined />),
-  getItem("陪玩管理", "user", <UserOutlined />, [
-    getItem("陪玩列表", "user-list"),
-  ]),
-  getItem("后台用户管理", "user-admin", <TeamOutlined />, [
-    getItem("用户列表", "user-admin-list"),
-  ]),
-];
-
-const App: React.FC = () => {
-  const [collapsed, setCollapsed] = useState(false);
-  const {
-    token: { colorBgContainer, borderRadiusLG },
-  } = theme.useToken();
+import { Spin } from "antd";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { useAuth } from "@/contexts/AuthContext";
+
+export default function RootPage() {
+  const { isAuthenticated, isLoading } = useAuth();
+  const router = useRouter();
+
+  useEffect(() => {
+    if (!isLoading) {
+      if (isAuthenticated) {
+        router.push("/home");
+      } else {
+        router.push("/login");
+      }
+    }
+  }, [isAuthenticated, isLoading, router]);
 
   return (
-    <Layout style={{ minHeight: "100vh" }}>
-      <Sider
-        collapsible
-        collapsed={collapsed}
-        onCollapse={(value) => setCollapsed(value)}
-      >
-        <div className="demo-logo-vertical" />
-        <Menu
-          theme="dark"
-          defaultSelectedKeys={["1"]}
-          mode="inline"
-          items={items}
-        />
-      </Sider>
-      <Layout>
-        <Header style={{ padding: 0, background: colorBgContainer }} />
-        <Content style={{ margin: "0 16px" }}>
-          <Breadcrumb
-            style={{ margin: "16px 0" }}
-            items={[{ title: "User" }, { title: "Bill" }]}
-          />
-          <div
-            style={{
-              padding: 24,
-              minHeight: 360,
-              background: colorBgContainer,
-              borderRadius: borderRadiusLG,
-            }}
-          >
-            Bill is a cat.
-          </div>
-        </Content>
-        <Footer style={{ textAlign: "center" }}>
-          Lanu ©{new Date().getFullYear()} Created by Lanu FE Team
-        </Footer>
-      </Layout>
-    </Layout>
+    <div
+      style={{
+        display: "flex",
+        alignItems: "center",
+        justifyContent: "center",
+        minHeight: "100vh",
+      }}
+    >
+      <Spin size="large" />
+    </div>
   );
-};
-
-export default App;
+}

+ 112 - 0
src/contexts/AuthContext.tsx

@@ -0,0 +1,112 @@
+"use client";
+
+import { message } from "antd";
+import { useRouter } from "next/navigation";
+import type React from "react";
+import { createContext, useContext, useEffect, useState } from "react";
+
+interface User {
+  id: string;
+  username: string;
+  email: string;
+  role: string;
+}
+
+interface AuthContextType {
+  user: User | null;
+  isLoading: boolean;
+  isAuthenticated: boolean;
+  login: (username: string, password: string) => Promise<boolean>;
+  logout: () => void;
+}
+
+const AuthContext = createContext<AuthContextType | undefined>(undefined);
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
+  children,
+}) => {
+  const [user, setUser] = useState<User | null>(null);
+  const [isLoading, setIsLoading] = useState(true);
+  const router = useRouter();
+
+  // Check if user is logged in on mount
+  useEffect(() => {
+    const storedUser = localStorage.getItem("user");
+    const token = localStorage.getItem("token");
+
+    if (storedUser && token) {
+      try {
+        setUser(JSON.parse(storedUser));
+      } catch (error) {
+        console.error("Failed to parse user data:", error);
+        localStorage.removeItem("user");
+        localStorage.removeItem("token");
+      }
+    }
+    setIsLoading(false);
+  }, []);
+
+  const login = async (
+    username: string,
+    password: string,
+  ): Promise<boolean> => {
+    try {
+      // TODO: Replace with actual API call
+      // Simulating API call with setTimeout
+      await new Promise((resolve) => setTimeout(resolve, 1000));
+
+      // Mock authentication logic
+      if (username === "admin" && password === "admin123") {
+        const mockUser: User = {
+          id: "1",
+          username: "admin",
+          email: "admin@lanu.com",
+          role: "admin",
+        };
+
+        const mockToken = "mock-jwt-token-" + Date.now();
+
+        setUser(mockUser);
+        localStorage.setItem("user", JSON.stringify(mockUser));
+        localStorage.setItem("token", mockToken);
+
+        message.success("登录成功!");
+        router.push("/home");
+        return true;
+      } else {
+        message.error("用户名或密码错误!");
+        return false;
+      }
+    } catch (error) {
+      console.error("Login error:", error);
+      message.error("登录失败,请稍后再试!");
+      return false;
+    }
+  };
+
+  const logout = () => {
+    setUser(null);
+    localStorage.removeItem("user");
+    localStorage.removeItem("token");
+    message.success("已退出登录");
+    router.push("/login");
+  };
+
+  const value: AuthContextType = {
+    user,
+    isLoading,
+    isAuthenticated: !!user,
+    login,
+    logout,
+  };
+
+  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
+};
+
+export const useAuth = (): AuthContextType => {
+  const context = useContext(AuthContext);
+  if (context === undefined) {
+    throw new Error("useAuth must be used within an AuthProvider");
+  }
+  return context;
+};

+ 5 - 0
yarn.lock

@@ -75,6 +75,11 @@
     resize-observer-polyfill "^1.5.1"
     throttle-debounce "^5.0.0"
 
+"@ant-design/v5-patch-for-react-19@^1.0.3":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@ant-design/v5-patch-for-react-19/-/v5-patch-for-react-19-1.0.3.tgz#6f522be2dd3440952617590bde5a1237a848a416"
+  integrity sha512-iWfZuSUl5kuhqLUw7jJXUQFMMkM7XpW7apmKzQBQHU0cpifYW4A79xIBt9YVO5IBajKpPG5UKP87Ft7Yrw1p/w==
+
 "@babel/helper-string-parser@^7.27.1":
   version "7.27.1"
   resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"