全栈(Java + Vue + MySQL)开发商城系统教程(四)

简介: 教程来源 http://vbzcj.cn 本教程详解Vue 3商城前端开发:从脚手架创建、Axios封装(含Token拦截与错误处理)、模块化API定义(用户/商品/购物车/订单),到路由守卫、Pinia状态管理及登录页实现,助力快速构建企业级电商应用。

第四部分:前端开发

4.1 创建Vue项目

# 创建Vue 3项目
npm create vue@latest mall-ui

# 进入项目目录
cd mall-ui

# 安装依赖
npm install

# 安装必要依赖
npm install element-plus axios vue-router@4 pinia

4.2 Axios请求封装

// src/utils/request.js
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router';

const request = axios.create({
    baseURL: '/api',
    timeout: 10000,
});

// 请求拦截器:自动添加Token
request.interceptors.request.use(
    config => {
        const token = localStorage.getItem('token');
        if (token) {
            config.headers['Authorization'] = `Bearer ${token}`;
        }
        return config;
    },
    error => Promise.reject(error)
);

// 响应拦截器:统一错误处理
request.interceptors.response.use(
    response => {
        const res = response.data;
        if (res.code === 200) {
            return res;
        } else if (res.code === 401) {
            ElMessage.error('登录已过期,请重新登录');
            localStorage.removeItem('token');
            router.push('/login');
            return Promise.reject(res);
        } else {
            ElMessage.error(res.message || '请求失败');
            return Promise.reject(res);
        }
    },
    error => {
        ElMessage.error(error.message || '网络错误');
        return Promise.reject(error);
    }
);

export default request;

4.3 API接口定义

// src/api/user.js
import request from '@/utils/request';

// 用户注册
export const register = (data) => request.post('/user/register', data);

// 用户登录
export const login = (data) => request.post('/user/login', data);

// 获取用户信息
export const getUserInfo = () => request.get('/user/info');

// 更新用户信息
export const updateUserInfo = (data) => request.put('/user/info', data);

// 修改密码
export const updatePassword = (data) => request.put('/user/password', data);
// src/api/product.js
import request from '@/utils/request';

// 获取商品列表
export const getProductList = (params) => request.get('/product/list', { params });

// 获取商品详情
export const getProductDetail = (id) => request.get(`/product/detail/${id}`);

// 获取分类列表
export const getCategories = () => request.get('/product/categories');
// src/api/cart.js
import request from '@/utils/request';

// 获取购物车
export const getCartList = () => request.get('/cart/list');

// 添加商品
export const addToCart = (productId, quantity) => 
    request.post('/cart/add', null, { params: { productId, quantity } });

// 更新数量
export const updateCartQuantity = (cartId, quantity) => 
    request.put('/cart/update', null, { params: { cartId, quantity } });

// 删除商品
export const removeCartItem = (cartId) => 
    request.delete(`/cart/remove/${cartId}`);

// 清空购物车
export const clearCart = () => request.delete('/cart/clear');

// 切换选中状态
export const toggleCheck = (cartId, checked) => 
    request.put('/cart/check', null, { params: { cartId, checked } });

// 全选/全不选
export const checkAll = (checked) => 
    request.put('/cart/checkAll', null, { params: { checked } });
// src/api/order.js
import request from '@/utils/request';

// 创建订单
export const createOrder = (data) => request.post('/order/create', data);

// 获取订单列表
export const getOrderList = (params) => request.get('/order/list', { params });

// 获取订单详情
export const getOrderDetail = (id) => request.get(`/order/detail/${id}`);

// 取消订单
export const cancelOrder = (id) => request.put(`/order/cancel/${id}`);

// 确认收货
export const confirmReceipt = (id) => request.put(`/order/confirm/${id}`);

4.4 路由配置

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
    {
        path: '/login',
        name: 'Login',
        component: () => import('@/views/Login.vue'),
        meta: { requiresAuth: false }
    },
    {
        path: '/register',
        name: 'Register',
        component: () => import('@/views/Register.vue'),
        meta: { requiresAuth: false }
    },
    {
        path: '/',
        component: () => import('@/layouts/DefaultLayout.vue'),
        meta: { requiresAuth: true },
        children: [
            { path: '', name: 'Home', component: () => import('@/views/Home.vue') },
            { path: 'product/:id', name: 'ProductDetail', component: () => import('@/views/ProductDetail.vue') },
            { path: 'cart', name: 'Cart', component: () => import('@/views/Cart.vue') },
            { path: 'orders', name: 'Orders', component: () => import('@/views/Orders.vue') }
        ]
    }
];

const router = createRouter({
    history: createWebHistory(),
    routes
});

// 路由守卫
router.beforeEach((to, from, next) => {
    const token = localStorage.getItem('token');
    if (to.meta.requiresAuth && !token) {
        next('/login');
    } else if ((to.path === '/login' || to.path === '/register') && token) {
        next('/');
    } else {
        next();
    }
});

export default router;

4.5 Pinia状态管理

// src/stores/user.js
import { defineStore } from 'pinia';
import { login, getUserInfo } from '@/api/user';

export const useUserStore = defineStore('user', {
    state: () => ({
        userId: localStorage.getItem('userId') || null,
        username: localStorage.getItem('username') || '',
        nickname: localStorage.getItem('nickname') || '',
        role: localStorage.getItem('role') || '',
        token: localStorage.getItem('token') || '',
        avatar: localStorage.getItem('avatar') || ''
    }),

    getters: {
        isLoggedIn: (state) => !!state.token,
        isAdmin: (state) => state.role === 'admin'
    },

    actions: {
        async login(loginData) {
            const res = await login(loginData);
            if (res.code === 200) {
                this.setUserInfo(res.data);
                // 获取用户详细信息
                await this.fetchUserInfo();
                return res;
            }
            return res;
        },

        setUserInfo(userInfo) {
            this.userId = userInfo.userId;
            this.username = userInfo.username;
            this.nickname = userInfo.nickname;
            this.role = userInfo.role;
            this.token = userInfo.token;
            this.avatar = userInfo.avatar;

            localStorage.setItem('userId', userInfo.userId);
            localStorage.setItem('username', userInfo.username);
            localStorage.setItem('nickname', userInfo.nickname);
            localStorage.setItem('role', userInfo.role);
            localStorage.setItem('token', userInfo.token);
            if (userInfo.avatar) localStorage.setItem('avatar', userInfo.avatar);
        },

        async fetchUserInfo() {
            const res = await getUserInfo();
            if (res.code === 200) {
                this.nickname = res.data.nickname;
                this.avatar = res.data.avatar;
                localStorage.setItem('nickname', res.data.nickname);
                if (res.data.avatar) localStorage.setItem('avatar', res.data.avatar);
            }
        },

        logout() {
            this.userId = null;
            this.username = '';
            this.nickname = '';
            this.role = '';
            this.token = '';
            this.avatar = '';

            localStorage.removeItem('userId');
            localStorage.removeItem('username');
            localStorage.removeItem('nickname');
            localStorage.removeItem('role');
            localStorage.removeItem('token');
            localStorage.removeItem('avatar');
        }
    }
});

4.6 登录页面

<template>
    <div class="login-page">
        <div class="login-container">
            <div class="login-header">
                <h2>商城系统</h2>
                <p>欢迎登录,开启购物之旅</p>
            </div>
            <el-form :model="form" :rules="rules" ref="formRef">
                <el-form-item prop="username">
                    <el-input v-model="form.username" placeholder="用户名" prefix-icon="User" size="large" />
                </el-form-item>
                <el-form-item prop="password">
                    <el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" show-password />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" size="large" @click="handleLogin" :loading="loading" block>登录</el-button>
                </el-form-item>
                <div class="login-footer">
                    <span>还没有账号?</span>
                    <router-link to="/register">立即注册</router-link>
                </div>
            </el-form>
        </div>
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/stores/user';

const router = useRouter();
const userStore = useUserStore();
const formRef = ref();
const loading = ref(false);

const form = reactive({ username: '', password: '' });

const rules = {
    username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
    password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
};

const handleLogin = async () => {
    await formRef.value.validate();
    loading.value = true;
    try {
        const res = await userStore.login(form);
        if (res.code === 200) {
            ElMessage.success('登录成功');
            router.push('/');
        }
    } finally {
        loading.value = false;
    }
};
</script>

<style scoped>
.login-page {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-container {
    width: 400px;
    padding: 40px;
    background: white;
    border-radius: 16px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.1);
}
.login-header { text-align: center; margin-bottom: 32px; }
.login-header h2 { margin-bottom: 8px; }
.login-footer { text-align: center; margin-top: 20px; }
.login-footer a { color: #667eea; text-decoration: none; }
</style>

来源:
http://uklgy.cn

相关文章
|
8天前
|
缓存 人工智能 自然语言处理
我对比了8个Claude API中转站,踩了不少坑,总结给你
本文是个人开发者耗时1周实测的8大Claude中转平台横向评测,聚焦Claude Code真实体验:以加权均价(¥/M token)、内部汇率、缓存支持、模型真实性及稳定性为核心指标。
3332 20
|
20天前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
17769 60
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
|
1天前
|
SQL 人工智能 弹性计算
阿里云发布 Agentic NDR,威胁检测与响应进入智能体时代
欢迎前往阿里云云防火墙控制台体验!
1153 2
|
4天前
|
人工智能 JSON BI
DeepSeek V4 来了!超越 Claude Sonnet 4.5,赶紧对接 Claude Code 体验一把
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro 的真实体验与避坑记录 本文记录我将 Claude Code 对接 DeepSeek 最新模型(V4Pro)后的真实体验,测试了 Skills 自动化查询和积木报表 AI 建表两个场景——有惊喜,也踩
1742 8
|
15天前
|
人工智能 JavaScript Ubuntu
低成本搭建AIP自动化写作系统:Hermes保姆级使用教程,长文和逐步实操贴图
我带着怀疑的态度,深度使用了几天,聚焦微信公众号AIP自动化写作场景,写出来的几篇文章,几乎没有什么修改,至少合乎我本人的意愿,而且排版风格,也越来越完善,同样是起码过得了我自己这一关。 这个其实OpenClaw早可以实现了,但是目前我觉得最大的区别是,Hermes会自主总结提炼,并更新你的写作技能。 相信就冲这一点,就值得一试。 这篇帖子主要就Hermes部署使用,作一个非常详细的介绍,几乎一步一贴图。 关于Hermes,无论你赞成哪种声音,我希望都是你自己动手行动过,发自内心的选择!
3156 29
|
3天前
|
人工智能 缓存 BI
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
JeecgBoot AI专题研究 把 Claude Code 接入 DeepSeek V4Pro,跑完 Skills —— OA 审批、大屏、报表、部署 5 大实战场景后的真实体验 ![](https://oscimg.oschina.net/oscnet/up608d34aeb6bafc47f
1370 3
Claude Code + DeepSeek V4-Pro 真实评测:除了贵,没别的毛病
|
4天前
|
机器学习/深度学习 缓存 测试技术
DeepSeek-V4开源:百万上下文,Agent能力比肩顶级闭源模型
DeepSeek-V4正式开源!含V4-Pro(1.6T参数)与V4-Flash(284B参数)双版本,均支持百万token上下文。首创混合注意力架构,Agent能力、世界知识与推理性能全面领先开源模型,数学/代码评测比肩顶级闭源模型。
1699 6
|
5天前
|
人工智能 测试技术 API
阿里Qwen3.6-27B正式开源:网友直呼“太牛了”!
阿里云千问3.6系列重磅开源Qwen3.6-27B稠密大模型!官网:https://t.aliyun.com/U/JbblVp 仅270亿参数,编程能力媲美千亿模型,在SWE-bench等权威基准中表现卓越。支持多模态理解、本地部署及OpenClaw等智能体集成,已开放Hugging Face与ModelScope下载。