第四部分:前端开发
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>