FitKick 电商APP项目总结一

66 阅读6分钟

FitKick 电商APP项目总结

FitKick是一个基于React技术栈的电商移动端应用,集成了传统电商功能与现代AI能力,采用了现代化的前端架构和最佳实践。项目特色在于结合了React生态的高效开发与大模型AI能力,打造了一个具有创新交互体验的移动电商平台。我需要对这个项目做一篇总结,对项目的技术栈的巩固,难点亮点分析,更好的掌握一些技术。

一:项目整体架构

做项目之前要先想好自己需要做什么,在这个项目中共有八个页面,主页、商品页、喜欢页面、AI聊天助手、我的页面、搜索页面和商品详情页,还有一个和coze相关的图片生成界面。这些都是页面级别组件,放在pages目录下。 还需要有一些通用组件放在Components目录下。整体架构如下: text

/mock       # 服务器模拟
/src
api/        # API请求封装
components/ # 通用组件开发
hooks/      # 自定义hooks
LLM/        # 大模型相关功能
mock/       # 模拟后端数据
pages/      # 页面组件
store/      # Zustand状态管理
unils/      # 工具函数
/vite.config.js   # 构建配置

屏幕截图_20250807_105723.png

在进行开发之前有一项重要工作就是安装一些依赖,我这个项目依赖有: react-router-dom、 zustand、 axios、 react-vant(UI组件库)、 @react-vant/icons、 lib-flexible(移动端适配)、 mockjs react-markdown、 mitt(事件总线)。 开发期间的依赖 vite-plugin-mock 、jwt 、postcss-pxtorem

路由系统

安装好依赖之后就可以开始路由的搭建,包含一些路由的懒加载,路由守卫,使用react-router-dom来实现,我们在项目的main.jsx中用Router将APP给包着,或者在APP里面直接使用Router在最外层实现路由功能。

image.png

import {
  Suspense,
  lazy
} from 'react'
import {
  Routes,
  Route,
  Navigate
} from 'react-router-dom'
import './App.css'
import Toast from '@/components/Toast'

const Loading = lazy(() => import('@/components/Loading'))
const MainLayout = lazy(() => import('@/components/MainLayout'))
const BlankLayout = lazy(() => import('@/components/BlankLayout'))
const Home = lazy(() => import('@/pages/Home'))
const UserProfile = lazy(() => import('@/pages/UserProfile'))
const ProductList = lazy(() => import('@/pages/ProductList'))
const Like = lazy(() => import('@/pages/Like'))
const Search = lazy(() => import('@/pages/Search'))
const ProductDetail = lazy(() => import('@/pages/ProductDetail'))
const Login = lazy(() => import('@/pages/Login'))
const Chat = lazy(() => import('@/pages/Chat'))
const RequireAuth = lazy(() => import('@/components/RequireAuth'))
const Coze = lazy(() => import('@/pages/Coze'))

function App() {

  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/" element={<MainLayout />}>
          <Route path="/" element={<Navigate to="/home" />} />
          <Route path="/home" element={<Home />} />
          <Route path="/user" element={<UserProfile />} />
          <Route path="/product" element={<ProductList />} />
          <Route path="/like" element={<RequireAuth><Like /></RequireAuth>} /> //requireAuth
          <Route path="/chat" element={<RequireAuth><Chat /></RequireAuth>} />//requireAuth
        </Route>
        {/* 空的Layout */}
        <Route path="/" element={<BlankLayout />}>
          <Route path="/search" element={<Search />} />
          <Route path="/detail/:id" element={<RequireAuth><ProductDetail /></RequireAuth>} />//requireAuth
          <Route path="/login" element={<Login />} />
          <Route path="/coze" element={<Coze />} />
        </Route>
      </Routes>
      <Toast />
    </Suspense>
  )
}

export default App

image.png

可以看到pages目录下对应相应的路由,为什么还会有一个MainLayout和BlankLayout呢,那是因为这些页面级别组件也是不一样的,有些像我的,喜欢,主页都是在底部有Tabbar的页面的,点击下方可以进行切换的,而像search,detail这些页面则应该在应该空白页来显示。 image.png

因为如果直接用import引入文件会直接运行,为了性能考虑我们使用lazy实现懒加载,需要加载的时候进行加载就可以实现性能的优化。

基于JWT的权限控制

RequireAuth 是一个 权限验证组件(通常是高阶组件HOC),Chat 是实际要渲染的聊天组件, 这种嵌套表示 Chat 组件需要经过 RequireAuth 的验证才能被访问,将想要保护的组件套在里面实现页面级访问控制。

搭建完了路由就可以正式开始写组件了。

登录界面

因为我没有搞后端,数据也都是用mock.js获取一些随机数据,所以简单做了一下登录界面,主要就是涉及到jwt鉴权。将状态放在文件useLoginStore.js中。

login.js

import jwt from "jsonwebtoken";
// 安全性 编码的时候用于加密
// 解码的时候用于解密
// 加盐
const secret = '!&05152054.'


// login 模块 mock
export default [
    {
        url: '/api/login',
        method: 'post',
        timeout: 2000,//手动指定请求耗时
        response: (req, res) => {
            // req,username,password
            const { username, password } = req.body
            if (username !== 'admin' || password !== '123456') {
                return {
                    code: 1,
                    message: '用户名或密码错误',
                }
            }
            // JSON 用户数据对象
            const token = jwt.sign({
                user: {
                    id: "001",
                    username: "admin",
                    nickname: "海绵宝宝"  // 添加nickname字段
                }
            }, secret, {
                expiresIn: '24h',
            })
            // console.log(token, '///')

            // 生成token 颁发令牌
            return {
                code: 0,
                token,
                data: {
                    id: "001",
                    username: "admin",
                    nickname: "海绵宝宝"  // 添加nickname字段
                }
            }
        },
    },
    {
        url: '/api/user',
        method: 'get',
        response: (req, res) => {
            // 用户端 token headers
            const token = req.headers["authorization"].split(' ')[1]
            // console.log(token)
            try {
                const decode = jwt.decode(token, secret)
                // console.log(decode)
                return {
                    code: 0,
                    data: decode.user
                }
            } catch (err) {
                return {
                    code: 1,
                    message: 'Invalid token'
                }
            }
        }
    }
]
  • 用户在登录页面输入用户名密码并提交
  • useLoginStore 中的 login 方法调用 doLogin API
  • 后端验证成功后生成 token 并返回
  • 前端将 token 存储到 localStorage ,并更新登录状态
import {
    create
} from 'zustand'
import {
    doLogin,
    getUser
} from '@/api/user'

export const useLoginStore = create((set) => ({
    user: null, // 用户信息
    isLogin: localStorage.getItem('token') ? true : false, // 是否登录
    login: async ({ username = "", password = "" }) => {
        try {
            const data = await doLogin({ username, password })

            // 检查响应是否成功
            if (data.code !== 0) {
                throw new Error(data.message || '登录失败')
            }

            const { token, data: user } = data
            localStorage.setItem('token', token)
            // 登录成功后获取用户详细信息
            const userData = await getUser()
            if (userData.code === 0) {
                set({
                    isLogin: true,
                    user: userData.data,
                })
            } else {
                throw new Error('获取用户信息失败')
            }
        } catch (err) {
            console.error('登录失败:', err)
            throw err
        }
    },
    logout: () => {
        localStorage.removeItem('token')
        set({
            isLogin: false,
            user: null,
        })
    },
    setUser: (user) => set({ user }),
}))

界面也很简单:

image.png

index.js

import { useRef } from 'react';
import { useLoginStore } from '@/store/useLoginStore';
import { useNavigate } from 'react-router-dom';
import styles from './login.module.css';
import { useToastStore } from '@/store/useToastStore'
import { ArrowLeft } from '@react-vant/icons';

const Login = () => {
    const usernameRef = useRef(null);
    const passwordRef = useRef(null);
    const { login, user } = useLoginStore();
    const navigate = useNavigate();
    const { showToast } = useToastStore();
    const handleSubmit = (e) => {
        e.preventDefault();
        const username = usernameRef.current.value;
        const password = passwordRef.current.value;

        if (!username || !password) {
            showToast({
                message: '请输入用户名和密码',
                type: 'warning'
            });
            return;
        }

        login({ username, password })
            .then(() => {
                const nickname = user?.nickname || '海绵宝宝';
                showToast({
                    message: `欢迎你, ${nickname}`,
                    type: 'success'
                });
                setTimeout(() => {
                    navigate(-1);
                }, 1000);
            })
            .catch(err => {
                showToast({
                    message: err.message || '登录失败',
                    type: 'error'
                });
            });
    };

    return (
        <div className={styles.container}>
            <div className={styles.back}>
                <ArrowLeft onClick={() => navigate('/user')} />

            </div>
            <h2 className={styles.title}>FITKICK</h2>
            <form onSubmit={handleSubmit}>
                <div className={styles.formGroup}>
                    <label htmlFor="username">用户名</label>
                    <input
                        id="username"
                        ref={usernameRef}
                        type="text"
                        required
                        placeholder="请输入用户名"
                        className={styles.input}
                        defaultValue="admin"
                    />
                </div>
                <div className={styles.formGroup}>
                    <label htmlFor="password">密码</label>
                    <input
                        id="password"
                        ref={passwordRef}
                        type="password"
                        required
                        placeholder="请输入密码"
                        className={styles.input}
                        defaultValue="123456"
                    />
                </div>
                <div>
                    <button type="submit" className={styles.button}>
                        登录
                    </button>
                </div>
            </form>
        </div>
    );
};

export default Login;

login.module.css

.container {
    max-width: 600px;
    height: 500px;
    padding: 20px;
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);

    /* 水平居中 */
    margin: 0 auto;
    /* 垂直居中 - 计算上方margin */
    margin-top: calc(50vh - 50% - 20px);

}

.title {
    text-align: center;
    margin-bottom: 20px;
    /* 调整字体和大小 */
    font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
    font-weight: 700;
    color: #1eaaaf;
    /* 添加字间距增强可读性 */
    letter-spacing: 0.5px;
    /* 轻微阴影增强立体感 */
    text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.formGroup {
    margin-bottom: 15px;
    width: 100%;
    height: 100px;
}

.input {
    width: 100%;
    box-sizing: border-box;
    max-width: 100%;
    padding: 8px;
    margin-top: 5px;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.button {
    width: 100%;
    padding: 10px;
    background-color: #2563eb;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.2s;
}

.button:hover {
    background-color: #1d4ed8;
    /* 稍深的蓝色 */
}

useLoginStore.js

import {
    create
} from 'zustand'
import {
    doLogin,
    getUser
} from '@/api/user'

export const useLoginStore = create((set) => ({
    user: null, // 用户信息
    isLogin: localStorage.getItem('token') ? true : false, // 是否登录
    login: async ({ username = "", password = "" }) => {
        try {
            const data = await doLogin({ username, password })

            // 检查响应是否成功
            if (data.code !== 0) {
                throw new Error(data.message || '登录失败')
            }

            const { token, data: user } = data
            localStorage.setItem('token', token)
            // 登录成功后获取用户详细信息
            const userData = await getUser()
            if (userData.code === 0) {
                set({
                    isLogin: true,
                    user: userData.data,
                })
            } else {
                throw new Error('获取用户信息失败')
            }
        } catch (err) {
            console.error('登录失败:', err)
            throw err
        }
    },
    logout: () => {
        localStorage.removeItem('token')
        set({
            isLogin: false,
            user: null,
        })
    },
    setUser: (user) => set({ user }),
}))

这个实现涵盖了 JWT 鉴权的核心环节,包括 token 的生成、存储、携带和验证,确保了用户身份的安全认证。