zheng 1 Minggu lalu
induk
melakukan
8310012114
33 mengubah file dengan 1162 tambahan dan 3 penghapusan
  1. 5 1
      21.react/高阶/project4/package.json
  2. 2 2
      21.react/高阶/project5/src/store/index.js
  3. 24 0
      21.react/高阶/project6/.gitignore
  4. 8 0
      21.react/高阶/project6/.oxlintrc.json
  5. 16 0
      21.react/高阶/project6/README.md
  6. 13 0
      21.react/高阶/project6/index.html
  7. 29 0
      21.react/高阶/project6/package.json
  8. 0 0
      21.react/高阶/project6/public/favicon.svg
  9. 24 0
      21.react/高阶/project6/public/icons.svg
  10. 38 0
      21.react/高阶/project6/public/product.json
  11. 32 0
      21.react/高阶/project6/src/Layout/index.css
  12. 125 0
      21.react/高阶/project6/src/Layout/index.jsx
  13. 3 0
      21.react/高阶/project6/src/api/product.js
  14. TEMPAT SAMPAH
      21.react/高阶/project6/src/assets/hero.png
  15. 0 0
      21.react/高阶/project6/src/assets/react.svg
  16. 0 0
      21.react/高阶/project6/src/assets/vite.svg
  17. 88 0
      21.react/高阶/project6/src/components/StartMain.jsx
  18. 41 0
      21.react/高阶/project6/src/hooks/useAuth.jsx
  19. 111 0
      21.react/高阶/project6/src/index.css
  20. 15 0
      21.react/高阶/project6/src/main.jsx
  21. 10 0
      21.react/高阶/project6/src/pages/Home.jsx
  22. 64 0
      21.react/高阶/project6/src/pages/Login.jsx
  23. 8 0
      21.react/高阶/project6/src/pages/NoMatch.jsx
  24. 82 0
      21.react/高阶/project6/src/pages/Product.jsx
  25. 101 0
      21.react/高阶/project6/src/pages/Setting.jsx
  26. 85 0
      21.react/高阶/project6/src/pages/Shop.jsx
  27. 33 0
      21.react/高阶/project6/src/pages/login.css
  28. 71 0
      21.react/高阶/project6/src/router/index.jsx
  29. 35 0
      21.react/高阶/project6/src/store/index.jsx
  30. 37 0
      21.react/高阶/project6/src/store/slice/menuSlice.jsx
  31. 30 0
      21.react/高阶/project6/src/store/slice/productSlice.jsx
  32. 25 0
      21.react/高阶/project6/src/store/slice/userSlice.jsx
  33. 7 0
      21.react/高阶/project6/vite.config.js

+ 5 - 1
21.react/高阶/project4/package.json

@@ -10,10 +10,14 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@reduxjs/toolkit": "^2.12.0",
+    "antd": "^6.5.3",
     "axios": "^1.19.0",
     "react": "^19.2.8",
     "react-dom": "^19.2.8",
-    "react-router-dom": "^7.18.2"
+    "react-redux": "^9.3.0",
+    "react-router-dom": "^7.18.2",
+    "redux-persist": "^6.0.0"
   },
   "devDependencies": {
     "@eslint/js": "^10.0.1",

+ 2 - 2
21.react/高阶/project5/src/store/index.js

@@ -24,11 +24,11 @@ const persistConfig = {
 }
 
 // 将reducer传入持久化中 // 数据持久化 redux-persist
-const persistedReducer = persistReducer(persistConfig, rootReducer)
+const news = persistReducer(persistConfig, rootReducer)
 
 // 将切片集成到仓库上
 const store = configureStore({
-    reducer: persistedReducer,
+    reducer: news,
     middleware: getDefaultMiddleware => getDefaultMiddleware({
         serializableCheck: {
             ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]

+ 24 - 0
21.react/高阶/project6/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 8 - 0
21.react/高阶/project6/.oxlintrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "./node_modules/oxlint/configuration_schema.json",
+  "plugins": ["react", "oxc"],
+  "rules": {
+    "react/rules-of-hooks": "error",
+    "react/only-export-components": ["warn", { "allowConstantExport": true }]
+  }
+}

+ 16 - 0
21.react/高阶/project6/README.md

@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the Oxlint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project.

+ 13 - 0
21.react/高阶/project6/index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>project6</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.jsx"></script>
+  </body>
+</html>

+ 29 - 0
21.react/高阶/project6/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "project6",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "lint": "oxlint",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@reduxjs/toolkit": "^2.12.0",
+    "antd": "^6.5.3",
+    "axios": "^1.19.0",
+    "react": "^19.2.8",
+    "react-dom": "^19.2.8",
+    "react-redux": "^9.3.0",
+    "react-router-dom": "^7.18.2",
+    "redux-persist": "^6.0.0"
+  },
+  "devDependencies": {
+    "@types/react": "^19.2.17",
+    "@types/react-dom": "^19.2.3",
+    "@vitejs/plugin-react": "^6.0.4",
+    "oxlint": "^1.75.0",
+    "vite": "^8.2.0"
+  }
+}

File diff ditekan karena terlalu besar
+ 0 - 0
21.react/高阶/project6/public/favicon.svg


+ 24 - 0
21.react/高阶/project6/public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

+ 38 - 0
21.react/高阶/project6/public/product.json

@@ -0,0 +1,38 @@
+[
+    {
+        "category": "Fruits",
+        "price": "$1",
+        "stocked": true,
+        "name": "Apple"
+    },
+    {
+        "category": "Fruits",
+        "price": "$1",
+        "stocked": true,
+        "name": "Dragonfruit"
+    },
+    {
+        "category": "Fruits",
+        "price": "$2",
+        "stocked": false,
+        "name": "Passionfruit"
+    },
+    {
+        "category": "Vegetables",
+        "price": "$2",
+        "stocked": true,
+        "name": "Spinach"
+    },
+    {
+        "category": "Vegetables",
+        "price": "$4",
+        "stocked": false,
+        "name": "Pumpkin"
+    },
+    {
+        "category": "Vegetables",
+        "price": "$1",
+        "stocked": true,
+        "name": "Peas"
+    }
+]

+ 32 - 0
21.react/高阶/project6/src/Layout/index.css

@@ -0,0 +1,32 @@
+.main {
+    height: 98vh;
+}
+
+.nav {
+    background: #fff;
+}
+.btns {
+  width: 96%;
+  height: 50px;
+  margin: 5px 0px 5px 3px;
+}
+
+.navItem {
+    /* width: 200 x; */
+}
+
+.header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .left {
+        display: flex;
+        align-items: center;
+    }
+    .right {
+        margin-right: 20px;
+        span {
+            margin-left: 5px;
+        }
+    }
+}

+ 125 - 0
21.react/高阶/project6/src/Layout/index.jsx

@@ -0,0 +1,125 @@
+import {
+    MenuFoldOutlined,
+    MenuUnfoldOutlined,
+    UploadOutlined,
+    UserOutlined,
+    VideoCameraOutlined,
+    HomeOutlined,
+    DownOutlined
+} from '@ant-design/icons';
+
+import { Button, Layout, Menu, theme, Breadcrumb, Avatar, Space, Dropdown } from 'antd';
+import { useEffect, useState } from 'react';
+import './index.css';
+import { Outlet, useLocation, useNavigate } from 'react-router-dom';
+import { useDispatch, useSelector } from 'react-redux';
+import { loginOut, setToken } from '../store/slice/userSlice';
+const { Header, Sider, Content, Footer } = Layout;
+function LayoutMain() {
+    const [collapsed, setCollapsed] = useState(false);
+    const [title, setTitle] = useState("");
+    const navigate = useNavigate()
+    const location = useLocation();
+    const dispatch = useDispatch();
+    const {
+        token: { colorBgContainer, borderRadiusLG },
+    } = theme.useToken();
+    const { menus } = useSelector(s => s.menuSlice)
+    const { userInfo } = useSelector(s => s.userSlice)
+    const handleClick = ({ key, itemData }) => {
+        navigate(key)
+        setTitle(itemData.label)
+    }
+    useEffect(() => {
+        const menu = menus.find((item) => item.key === location.pathname);
+        setTitle(menu.label)
+    }, [location.pathname])
+    const handleLoginOut = () => {
+        dispatch(loginOut())
+    }
+    const items = [
+        {
+            label: <span onClick={() => navigate("/setting")}>对对碰</span>,
+            key: '/1',
+        },
+        {
+            type: 'divider',
+        },
+        {
+            label: <span onClick={() => handleLoginOut()}>退出登录</span>,
+            key: '2',
+        },
+    ];
+    return (
+        <Layout className='main'>
+            <Sider trigger={null} collapsible collapsed={collapsed} className='nav'>
+                <div className="demo-logo-vertical" />
+                <Button type="primary" ghost className="btns">
+                    导航菜单
+                </Button>
+                <Menu
+                    theme="light"
+                    mode="inline"
+                    defaultSelectedKeys={[location.pathname]}
+                    items={menus}
+                    onClick={handleClick}
+                />
+            </Sider>
+            <Layout>
+                <Header style={{ padding: 0, background: colorBgContainer }} className='header'>
+                    <div className="left">
+                        <Button
+                            type="text"
+                            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
+                            onClick={() => setCollapsed(!collapsed)}
+                            style={{
+                                fontSize: '16px',
+                                width: 64,
+                                height: 64,
+                            }}
+                        />
+                        <Breadcrumb
+                            className='navItem'
+                            items={[
+                                {
+                                    href: '/',
+                                    title: <HomeOutlined />,
+                                },
+                                {
+                                    href: '',
+                                    title: <span>{title}</span>,
+                                }
+                            ]}
+                        />
+                    </div>
+                    <div className="right">
+                        <Avatar style={{ backgroundColor: '#1A73E8' }} icon={<UserOutlined />} />
+                        <Dropdown menu={{ items }}>
+                            <a onClick={e => e.preventDefault()}>
+                                <Space>
+                                    {userInfo.userName}
+                                    <DownOutlined />
+                                </Space>
+                            </a>
+                        </Dropdown>
+                    </div>
+                </Header>
+                <Content
+                    style={{
+                        margin: '24px 16px',
+                        padding: 24,
+                        minHeight: 280,
+                        background: colorBgContainer,
+                        borderRadius: borderRadiusLG,
+                    }}
+                >
+                    <Outlet></Outlet>
+                </Content>
+                <Footer style={{ textAlign: 'center' }}>
+                    React 19 商城管理系统 ©{new Date().getFullYear()}
+                </Footer>
+            </Layout>
+        </Layout>
+    );
+};
+export default LayoutMain;

+ 3 - 0
21.react/高阶/project6/src/api/product.js

@@ -0,0 +1,3 @@
+import axios from 'axios';
+
+export const getAllProducts = () => axios.get('/product.json');

TEMPAT SAMPAH
21.react/高阶/project6/src/assets/hero.png


File diff ditekan karena terlalu besar
+ 0 - 0
21.react/高阶/project6/src/assets/react.svg


File diff ditekan karena terlalu besar
+ 0 - 0
21.react/高阶/project6/src/assets/vite.svg


+ 88 - 0
21.react/高阶/project6/src/components/StartMain.jsx

@@ -0,0 +1,88 @@
+import { useEffect, useRef } from "react";
+
+function StartMain() {
+    const canvasRef = useRef(null);
+    useEffect(() => {
+        // 取到canvas画布
+        const canvas = canvasRef.current;
+        if (!canvas) return;
+        // canvas 环境
+        const ctx = canvas.getContext("2d");
+        if (!ctx) return;
+
+        let w = 0;
+        let h = 0
+        let stars = [];
+        // 窗口发生变化时候 重新计算
+        const resize = () => {
+            console.log("触发")
+            w = canvas.width = window.innerWidth;
+            h = canvas.height = window.innerHeight;
+            // 绘制星星
+            initStar();
+        }
+        const initStar = () => {
+            const count = Math.random() * 200 + 800;
+            console.log(count)
+            stars = [];
+            for (let i = 0; i < count; i++) {
+                stars.push({
+                    x: Math.random() * w,
+                    y: Math.random() * h,
+                    r: Math.random() * 1.5 + 0.5,
+                    opacity: Math.random() * 0.6 + 0.2,
+                    speed: Math.random() * 2 + 3,
+                    deviation: Math.random() * Math.PI * 2
+                })
+            }
+        }
+        const draw = (time) => {
+            ctx.clearRect(0, 0, w, h);
+            const bgCanvas = ctx.createRadialGradient(w * 0.3, h * 0.4, 0, w * 0.5, h * 0.5, w * 1.4);
+            bgCanvas.addColorStop(0, "#1a0f2e");
+            bgCanvas.addColorStop(0.5, "#120a1f");
+            bgCanvas.addColorStop(1, "#06030d");
+            ctx.fillStyle = bgCanvas;
+            ctx.fillRect(0, 0, w, h);
+
+            for (const s of stars) {
+                const flicker = s.opacity + Math.sin(time * s.speed + s.deviation) * 0.3;
+                const alpha = Math.max(0.1, Math.min(1, flicker));
+                ctx.beginPath();
+                ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
+                ctx.fillStyle = `rgba(255,255,255,${alpha})`
+                ctx.fill();
+
+                if (s.r > 1.5) {
+                    ctx.beginPath();
+                    ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
+                    ctx.fillStyle = `rgba(218, 112, 214,${alpha * 0.8})`
+                    ctx.fill();
+
+                }
+            }
+
+
+            requestAnimationFrame(draw)
+        }
+        resize();
+        window.addEventListener('resize', resize)
+        const raf = requestAnimationFrame(draw);
+        return () => {
+            window.removeEventListener('resize', resize)
+            cancelAnimationFrame(raf)
+        }
+    }, [])
+    return (
+        <canvas ref={canvasRef} style={{
+            position: 'fixed',
+            top: 0,
+            left: 0,
+            width: '100%',
+            height: '100%',
+            zIndex: 0
+        }}></canvas>
+    )
+}
+
+export default StartMain;

+ 41 - 0
21.react/高阶/project6/src/hooks/useAuth.jsx

@@ -0,0 +1,41 @@
+// import { useEffect } from "react";
+import { useSelector } from "react-redux";
+import { Navigate, useLocation, useNavigate } from "react-router-dom";
+
+// // 路由守卫组件
+// export function useAuth() {
+//     const { token } = useSelector(x => x.userSlice);
+//     const location = useLocation();
+//     const navigate = useNavigate();
+//     useEffect(() => {
+//         console.log('走进来', token)
+//         // 用户未登录 且不再白名单
+//         if (!token && !whiteList.includes[location.pathname]) {
+//             console.log('走进来1')
+//             navigate('/login')
+//         }
+//         // 用户已登录 重定向到首页
+//         console.log(location.pathname, token, '子')
+//         if (token && location.pathname == '/login') {
+//             console.log("走进来12")
+//             navigate('/home')
+//         }
+//     }, [location.pathname, token, navigate])
+// }
+
+function UseAuth({ children }) {
+    const whiteList = ['/login'];
+    const { token } = useSelector(x => x.userSlice);
+    const location = useLocation();
+    const navigate = useNavigate();
+    if (!token && !whiteList.includes(location.pathname)) {
+        return <Navigate to='/login' replace></Navigate>
+    }
+
+    if (token && location.pathname == '/login') {
+        return <Navigate to='/' replace></Navigate>
+    }
+    return children;
+}
+
+export default UseAuth;

+ 111 - 0
21.react/高阶/project6/src/index.css

@@ -0,0 +1,111 @@
+:root {
+  --text: #6b6375;
+  --text-h: #08060d;
+  --bg: #fff;
+  --border: #e5e4e7;
+  --code-bg: #f4f3ec;
+  --accent: #aa3bff;
+  --accent-bg: rgba(170, 59, 255, 0.1);
+  --accent-border: rgba(170, 59, 255, 0.5);
+  --social-bg: rgba(244, 243, 236, 0.5);
+  --shadow:
+    rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+  --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+  --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+  --mono: ui-monospace, Consolas, monospace;
+
+  font: 18px/145% var(--sans);
+  letter-spacing: 0.18px;
+  color-scheme: light dark;
+  color: var(--text);
+  background: var(--bg);
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+  @media (max-width: 1024px) {
+    font-size: 16px;
+  }
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --text: #9ca3af;
+    --text-h: #f3f4f6;
+    --bg: #16171d;
+    --border: #2e303a;
+    --code-bg: #1f2028;
+    --accent: #c084fc;
+    --accent-bg: rgba(192, 132, 252, 0.15);
+    --accent-border: rgba(192, 132, 252, 0.5);
+    --social-bg: rgba(47, 48, 58, 0.5);
+    --shadow:
+      rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+  }
+
+  #social .button-icon {
+    filter: invert(1) brightness(2);
+  }
+}
+
+body {
+  margin: 0;
+}
+
+#root {
+  width: 1126px;
+  max-width: 100%;
+  margin: 0 auto;
+  text-align: center;
+  border-inline: 1px solid var(--border);
+  min-height: 100svh;
+  display: flex;
+  flex-direction: column;
+  box-sizing: border-box;
+}
+
+h1,
+h2 {
+  font-family: var(--heading);
+  font-weight: 500;
+  color: var(--text-h);
+}
+
+h1 {
+  font-size: 56px;
+  letter-spacing: -1.68px;
+  margin: 32px 0;
+  @media (max-width: 1024px) {
+    font-size: 36px;
+    margin: 20px 0;
+  }
+}
+h2 {
+  font-size: 24px;
+  line-height: 118%;
+  letter-spacing: -0.24px;
+  margin: 0 0 8px;
+  @media (max-width: 1024px) {
+    font-size: 20px;
+  }
+}
+p {
+  margin: 0;
+}
+
+code,
+.counter {
+  font-family: var(--mono);
+  display: inline-flex;
+  border-radius: 4px;
+  color: var(--text-h);
+}
+
+code {
+  font-size: 15px;
+  line-height: 135%;
+  padding: 4px 8px;
+  background: var(--code-bg);
+}

+ 15 - 0
21.react/高阶/project6/src/main.jsx

@@ -0,0 +1,15 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { RouterProvider } from 'react-router-dom'
+import router from './router/index.jsx'
+import { Provider } from 'react-redux'
+import store, { persistor } from './store/index.jsx'
+import { PersistGate } from 'redux-persist/integration/react'
+createRoot(document.getElementById('root')).render(
+
+  <Provider store={store}>
+    <PersistGate persistor={persistor} loading={null}>
+      <RouterProvider router={router}></RouterProvider>
+    </PersistGate>
+  </Provider>
+)

+ 10 - 0
21.react/高阶/project6/src/pages/Home.jsx

@@ -0,0 +1,10 @@
+export default function Home() {
+    return (
+        <div>
+            <h1>🏠 首页</h1>
+            <p style={{ color: '#666', marginTop: 12 }}>
+                欢迎来到 React 19 商城管理系统
+            </p>
+        </div>
+    )
+}

+ 64 - 0
21.react/高阶/project6/src/pages/Login.jsx

@@ -0,0 +1,64 @@
+import StartMain from "../components/StartMain";
+import { useDispatch } from 'react-redux';
+import { LockOutlined, UserOutlined } from '@ant-design/icons';
+import { Button, Checkbox, Form, Input } from 'antd';
+import { setToken, setUserInfo } from "../store/slice/userSlice.jsx";
+import './login.css';
+import { useNavigate } from "react-router-dom";
+function Login() {
+    const dispatch = useDispatch();
+    const navigate = useNavigate();
+    const onFinish = values => {
+        console.log('Success:', values);
+        if (values) {
+            // 设置令牌 设置用户信息
+            dispatch(setToken(Date.now()));
+            dispatch(setUserInfo({ userName: values.username }))
+            navigate("/")
+        }
+    };
+    const onFinishFailed = errorInfo => {
+        console.log('Failed:', errorInfo);
+    };
+    return (
+        <div className="login">
+            <StartMain></StartMain>
+            <div className="loginMain">
+                <h1>欢迎登录</h1>
+                <p>React 19 商城管理系统</p>
+                <Form
+                    className="forms"
+                    name="basic"
+                    onFinish={onFinish}
+                    onFinishFailed={onFinishFailed}
+                    autoComplete="off"
+                >
+                    <Form.Item
+                        name="username"
+
+                        rules={[{ required: true, message: 'Please input your username!' }]}
+                    >
+                        <Input prefix={<UserOutlined />} placeholder="请输入用户名" />
+                    </Form.Item>
+
+                    <Form.Item
+                        name="password"
+
+                        rules={[{ required: true, message: 'Please input your password!' }]}
+                    >
+                        <Input.Password prefix={<LockOutlined />} placeholder="请输入密码" />
+                    </Form.Item>
+
+
+                    <Form.Item label={null}>
+                        <Button className="btn" color="purple" variant="solid" htmlType="submit">
+                            登录
+                        </Button>
+                    </Form.Item>
+                </Form>
+            </div>
+        </div>
+    )
+}
+
+export default Login;

+ 8 - 0
21.react/高阶/project6/src/pages/NoMatch.jsx

@@ -0,0 +1,8 @@
+function NoMatch() {
+    return (
+        <div>
+            <h1>报错</h1>
+        </div>
+    )
+}
+export default NoMatch;

+ 82 - 0
21.react/高阶/project6/src/pages/Product.jsx

@@ -0,0 +1,82 @@
+import { useEffect, useMemo } from 'react';
+import { Fragment } from 'react'
+import { getAllProducts } from '../api/product';
+import { useDispatch, useSelector } from 'react-redux';
+import { fetchProduct, setShowStock, setFilterName } from '../store/slice/productSlice';
+function Product() {
+    const dispatch = useDispatch();
+    useEffect(() => {
+        dispatch(fetchProduct())
+    }, [dispatch])
+    const { products, filterName, showStock } = useSelector(x => x.productSlice);
+
+
+    const filterProducts = useMemo(() => {
+        return products.filter(item => item.name.includes(filterName) && (showStock ? item.stocked : true))
+    }, [products, filterName, showStock])
+
+    const renderProduct = () => {
+        let prevCategory = ''
+        return filterProducts.map((product, index) => {
+            const showCategoryHeader = prevCategory !== product.category
+            prevCategory = product.category
+
+            return (
+                <Fragment key={product.name + index}>
+                    {showCategoryHeader && (
+                        <tr style={{ background: '#e8f0fe' }}>
+                            <td colSpan={2}>
+                                <strong>{product.category}</strong>
+                            </td>
+                        </tr>
+                    )}
+                    <tr>
+                        <td>
+                            <span style={{ color: product.stocked ? '#333' : '#e74c3c' }}>
+                                {product.name}
+                            </span>
+                            {!product.stocked && (
+                                <span style={{ color: '#e74c3c', fontSize: 12, marginLeft: 8 }}>
+                                    (缺货)
+                                </span>
+                            )}
+                        </td>
+                        <td>{product.price}</td>
+                    </tr>
+                </Fragment>
+            )
+        })
+    }
+    return (
+        <div>
+            <h1>📦 商品列表</h1>
+            <div style={{ margin: '16px 0', display: 'flex', gap: 16, alignItems: 'center' }}>
+                <input type="text" placeholder='搜索商品名称...' onChange={(e) => dispatch(setFilterName(e.target.value))} style={{ padding: '8px 12px', borderRadius: 6, border: '1px solid #d9d9d9', width: 240 }} />
+
+                <label style={{ cursor: 'pointer' }}>
+                    <input type="checkbox" style={{ marginRight: 6 }} onChange={(e) => dispatch(setShowStock(e.target.checked))} />仅显示有库存的商品</label>
+            </div>
+            <table border={1} cellPadding={12} style={{ borderCollapse: 'collapse', width: '100%' }}>
+                <thead>
+                    <tr style={{ background: '#1a73e8', color: '#fff' }}>
+                        <th>商品名称</th>
+                        <th>商品价格</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {filterProducts.length === 0 ? (
+                        <tr>
+                            <td colSpan={2} style={{ textAlign: 'center', padding: 40, color: '#999' }}>
+                                📭 暂无匹配商品
+                            </td>
+                        </tr>
+                    ) : (
+                        renderProduct()
+                    )}
+                </tbody>
+            </table>
+        </div>
+    )
+}
+
+export default Product;

+ 101 - 0
21.react/高阶/project6/src/pages/Setting.jsx

@@ -0,0 +1,101 @@
+import { useState, useEffect } from "react";
+const pets = [
+    '🐶', '🐱', '🐰', '🐭', "🦊", "🐷", "🐔", "🐑", "🐂", "🦁"
+]
+const getPetsCard = () => {
+    const selected = pets.slice(0, 5);
+    const list = [...selected, ...selected];
+    return list.sort(() => Math.random() - 0.1)
+}
+function Setting() {
+    // 移动次数
+    const [moves, setMoves] = useState(0);
+    // 配对
+    const [pairing, setPairing] = useState(0);
+    // 卡片数组
+    const [cards, setCards] = useState([]);
+    // 翻开
+    const [over, setOver] = useState([]);
+    // 成功
+    const [success, setSuccess] = useState([]);
+
+    useEffect(() => {
+        const result = getPetsCard();
+        setCards(result);
+        console.log(result, result.length)
+        setPairing(result.length / 2);
+    }, [])
+
+    const handleClick = (index) => {
+        if (success.includes(index) || over.includes(index) || over.length >= 2) return;
+        setMoves(prev => prev + 1)
+        const newOver = [...over, index];
+        setOver(newOver)
+        if (newOver.length == 2) {
+            let [first, second] = newOver;
+            if (cards[first] === cards[second]) {
+                // 把相同的内容放到成功数组中
+                setSuccess(prev => [...prev, first, second]);
+                setPairing(prev => prev - 1);
+            }
+            // 未成功 把下标从翻开数组清除
+            setTimeout(() => setOver([]), 1500)
+        }
+
+    }
+
+    const handleReset = () => {
+        const result = getPetsCard();
+        console.log(result.length, '2')
+        setCards(result);
+        setPairing(result.length / 2);
+        setOver([])
+        setSuccess([])
+    }
+
+    const accuracy = moves == 0 ? 0 : (success.length / 2 / moves) * 100
+    return (
+        <div>
+            <h1>🐾 宠物配对游戏</h1>
+            <p>
+                <span>移动次数:{moves}|</span>
+                <span>剩余配对:{pairing}|</span>
+                <span>准确率:{accuracy.toFixed(2)}%</span>
+            </p>
+            <div style={{
+                width: 550,
+                display: 'grid', gridTemplateColumns: 'auto auto auto auto auto',
+                gap: 5, margin: '0 auto'
+            }}>
+                {
+                    cards.map((item, index) => (
+                        <div key={index} onClick={() => handleClick(index)} style={{
+                            width: 100, height: 100, backgroundColor: success.includes(index) ? '#27Ae60' : '#1A73E8', color: 'white',
+                            display: 'flex', justifyContent: 'center',
+                            alignItems: 'center',
+                            borderRadius: 8,
+                            fontSize: 30,
+                            transition: 'all 0.3s',
+                            transform: success.includes(index) || over.includes(index) ? 'rotateY(-180deg)' : 'rotateY(0)',
+
+                        }}>
+                            {
+                                success.includes(index) || over.includes(index) ? item : '?'
+                            }
+                        </div>
+                    ))
+                }
+            </div>
+            <button onClick={() => handleReset()} style={{
+                background: '#27Ae60',
+                color: '#fff',
+                textAlign: 'center',
+                padding: '13px 25px',
+                border: 'none',
+                marginTop: '30px',
+            }}>重新开始</button>
+        </div >
+    )
+}
+
+export default Setting;

+ 85 - 0
21.react/高阶/project6/src/pages/Shop.jsx

@@ -0,0 +1,85 @@
+/**
+ * 购物车
+ */
+import { useMemo, useState, useCallback } from 'react'
+
+const initList = [
+    { id: 1, name: '算法导论', publish: '2006-9', price: 85, count: 1 },
+    { id: 2, name: 'UNIX编程艺术', publish: '2016-2', price: 39, count: 1 },
+    { id: 3, name: '编程珠玑', publish: '2020-4', price: 189, count: 1 },
+    { id: 4, name: '代码大全', publish: '2013-8', price: 299, count: 1 },
+]
+
+export default function Shop() {
+    const [books, setBooks] = useState(initList)
+
+    // ★ useMemo:仅 books 变化时重新计算总价
+    const total = useMemo(() => {
+        return books.reduce((sum, item) => sum + item.count * item.price, 0)
+    }, [books])
+
+    // ★ 数量 +1(不可变更新)
+    const handleAdd = useCallback((id) => {
+        setBooks(prev =>
+            prev.map(b => b.id === id ? { ...b, count: b.count + 1 } : b)
+        )
+    }, [])
+
+    // ★ 数量 -1(最小为 1)
+    const handleReduce = useCallback((id) => {
+        setBooks(prev =>
+            prev.map(b => b.id === id && b.count > 1
+                ? { ...b, count: b.count - 1 }
+                : b
+            )
+        )
+    }, [])
+
+    // ★ 移除
+    const handleRemove = useCallback((id) => {
+        if (!window.confirm('确定从购物车中移除当前书籍么')) return
+        setBooks(prev => prev.filter(item => item.id !== id))
+    }, [])
+
+    return (
+        <div>
+            <h1>🛒 购物车</h1>
+            <table border={1} cellPadding={25} style={{ marginTop: 16, borderCollapse: 'collapse' }}>
+                <thead>
+                    <tr style={{ background: '#f5f5f5' }}>
+                        <th>#</th>
+                        <th>书籍名称</th>
+                        <th>出版日期</th>
+                        <th>价格</th>
+                        <th>购买数量</th>
+                        <th>操作</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {books.map((item, index) => (
+                        <tr key={item.id}>
+                            <td>{index + 1}</td>
+                            <td>{item.name}</td>
+                            <td>{item.publish}</td>
+                            <td>¥{item.price}</td>
+                            <td>
+                                <button
+                                    disabled={item.count <= 1}
+                                    onClick={() => handleReduce(item.id)}
+                                >−</button>
+                                <span style={{ padding: '0 8px' }}>{item.count}</span>
+                                <button onClick={() => handleAdd(item.id)}>+</button>
+                            </td>
+                            <td>
+                                <button onClick={() => handleRemove(item.id)}>移除</button>
+                            </td>
+                        </tr>
+                    ))}
+                </tbody>
+            </table>
+            <h2 style={{ marginTop: 20, color: '#e74c3c' }}>
+                总价格:¥{total}
+            </h2>
+        </div>
+    )
+}

+ 33 - 0
21.react/高阶/project6/src/pages/login.css

@@ -0,0 +1,33 @@
+.login {
+    /* position: relative; */
+    height: 100vh;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.loginMain {
+    width: 600px;
+    height: 400px;
+    background:rgba(139, 92, 246, 0.1) !important;
+    /* position: static; */
+    z-index: 9;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    border-radius: 12px;
+    border: 1px solid rgba(139, 92, 246, 0.3);
+    box-shadow: 0 0 40px rgba(139, 92, 246, 0.2), 0 20px 60px rgba(0, 0, 0, 0.5);
+    h1 {
+        color: #d8c9ff;
+        margin: 0;
+    }
+    p {
+        color: rgba(180, 160, 220, 0.7) !important;
+    }
+}
+
+.forms,.btn {
+    width: 300px;
+}

+ 71 - 0
21.react/高阶/project6/src/router/index.jsx

@@ -0,0 +1,71 @@
+import { createBrowserRouter, createHashRouter } from 'react-router-dom';
+
+import { lazy, Suspense } from 'react';
+
+import Layout from '../Layout';
+import NoMatch from '../pages/NoMatch';
+import Login from '../pages/Login';
+import UseAuth from '../hooks/useAuth';
+const Home = lazy(() => import('../pages/Home'));
+const Product = lazy(() => import('../pages/Product'));
+const Setting = lazy(() => import('../pages/Setting'));
+const Shop = lazy(() => import('../pages/Shop'));
+
+const withSuspense = (Component, label) => {
+    return (<Suspense fallback={(
+        <div style={{
+            textAlign: 'center', color: '#999'
+        }}>
+            <div>⏳</div>
+            <p>{label}</p>
+        </div>
+    )}>
+        <Component></Component>
+    </Suspense>)
+}
+
+const routes = [
+    {
+        path: '/',
+        element: (
+            <UseAuth>
+                <Layout></Layout>
+            </UseAuth>
+        ),
+        children: [
+            {
+                index: true,
+                element: withSuspense(Home, '加载中...')
+            },
+            {
+                path: 'shop',
+                element: withSuspense(Shop, '加载中...')
+            },
+            {
+                path: 'setting',
+                element: withSuspense(Setting, '加载中...')
+            },
+            {
+                path: 'product',
+                element: withSuspense(Product, '加载中...')
+            }
+        ]
+    },
+    {
+        path: '/login',
+        element: (
+            <UseAuth>
+                <Login></Login>
+            </UseAuth>
+        )
+    },
+    {
+        path: '*',
+        element: <NoMatch />
+    }
+]
+
+
+const router = createBrowserRouter(routes);
+
+export default router;

+ 35 - 0
21.react/高阶/project6/src/store/index.jsx

@@ -0,0 +1,35 @@
+import { combineReducers, configureStore } from '@reduxjs/toolkit';
+
+import userSlice from './slice/userSlice.jsx';
+import menuSlice from './slice/menuSlice.jsx';
+import productSlice from './slice/productSlice.jsx';
+import { persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist'
+import persistStore from 'redux-persist/es/persistStore';
+import storage from 'redux-persist/es/storage'
+
+const persistConfig = {
+    key: 'redux',
+    storage,
+    whitelist: ['userSlice']
+}
+
+const rootReducer = combineReducers({
+    userSlice,
+    menuSlice,
+    productSlice
+})
+
+const persisteds = persistReducer(persistConfig, rootReducer)
+
+const store = configureStore({
+    reducer: persisteds,
+    middleware: getDefaultMiddleware => getDefaultMiddleware({
+        serializableCheck: {
+            ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
+            ignoredPaths: ['menuSlice.menus']
+        }
+    })
+})
+
+export const persistor = persistStore(store);
+export default store;

+ 37 - 0
21.react/高阶/project6/src/store/slice/menuSlice.jsx

@@ -0,0 +1,37 @@
+import { createSlice } from "@reduxjs/toolkit";
+import {
+    HomeOutlined, ShopOutlined, ShoppingCartOutlined, RedditOutlined
+} from '@ant-design/icons';
+const menuSlice = createSlice({
+    name: 'menuSlice',
+    initialState: {
+        menus: [
+            {
+                id: 1,
+                key: '/',
+                icon: <HomeOutlined />,
+                label: '首页'
+            },
+            {
+                id: 2,
+                key: '/product',
+                icon: <ShopOutlined />,
+                label: '商品列表'
+            },
+            {
+                id: 3,
+                key: '/setting',
+                icon: <RedditOutlined />,
+                label: '对对碰'
+            },
+            {
+                id: 4,
+                key: '/shop',
+                icon: <ShoppingCartOutlined />,
+                label: '购物车'
+            },
+        ]
+    }
+})
+
+export default menuSlice.reducer;

+ 30 - 0
21.react/高阶/project6/src/store/slice/productSlice.jsx

@@ -0,0 +1,30 @@
+import { createSlice } from "@reduxjs/toolkit";
+import { getAllProducts } from '../../api/product';
+
+const productSlice = createSlice({
+    name: "productSlice",
+    initialState: {
+        products: [],
+        showStock: false,
+        filterName: ''
+    },
+    reducers: {
+        setProducts(state, { payload }) {
+            state.products = payload;
+        },
+        setShowStock(state, { payload }) {
+            state.showStock = payload;
+        },
+        setFilterName(state, { payload }) {
+            state.filterName = payload;
+        }
+    }
+})
+
+export default productSlice.reducer;
+export const { setProducts, setShowStock, setFilterName } = productSlice.actions;
+
+export const fetchProduct = () => async (dispatch, getState) => {
+    const result = await getAllProducts();
+    dispatch(setProducts(result.data))
+}

+ 25 - 0
21.react/高阶/project6/src/store/slice/userSlice.jsx

@@ -0,0 +1,25 @@
+import { createSlice } from '@reduxjs/toolkit';
+
+const userSlice = createSlice({
+    name: 'userSlice',
+    initialState: {
+        token: null,
+        userInfo: null
+    },
+    reducers: {
+        setToken(state, { payload }) {
+            state.token = payload;
+        },
+        setUserInfo(state, { payload }) {
+            state.userInfo = payload
+        },
+        loginOut(state) {
+            state.token = null;
+            state.userInfo = null;
+        }
+    }
+})
+
+
+export default userSlice.reducer;
+export const { setToken, setUserInfo, loginOut } = userSlice.actions;

+ 7 - 0
21.react/高阶/project6/vite.config.js

@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [react()],
+})

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini