基于SpringBoot3+Vue3的运动户外交易小程序全栈开发实战
近几年运动户外消费快速增长围绕跑步、骑行、露营、徒步等场景的电商小程序越来越多。很多朋友在找完整的学习项目时会遇到“基于SpringBoot4Vue3的运动户外交易小程序”这类标题但真正打开资料后往往一头雾水后端怎么搭、管理后台怎么写、小程序端怎么对接、数据库该怎么设计都缺少一套连贯的演示。这篇文章会围绕同一个业务案例把 Spring Boot 后端、Vue3 管理后台、微信小程序商城三条线完整串起来既能用于课程设计也可以作为二次开发的基础工程。不管你是第一次做商城类小程序还是已经有 Vue2/小程序经验想迁移到 Vue3都能从这套代码结构中拆出可复用的部分。我先把技术选型讲清楚标题里写到的 SpringBoot4在真正编码时需要落到具体的官方稳定版本上目前 Spring Boot 的正代版本已经演进到 3.x本文以 Spring Boot 3.x 示例如果你拿到的是其他版本命名的项目代码分层和业务写法基本一致只需替换依赖版本。前端项目采用 Vue3 Vite Pinia Element PlusC 端小程序采用 uni-appVue3 语法编译到微信小程序。这样一套方案的优势是后端接口能同时服务管理后台和小程序端前端代码逻辑统一也方便以后扩展到 H5。1. 项目背景与系统定位1.1 运动户外交易小程序的业务价值运动户外交易小程序本质上是一个“重展示、轻咨询、强交易”的电商系统。和传统 PC 商城不同移动端用户更习惯在小程序里完成浏览、加购、下单、支付的全流程。从业务价值来看它具有三个明显特点第一运动户外商品有强场景属性。用户不是随机逛店而是带着“周末露营缺帐篷”“跑步需要一双新鞋”这样的明确需求进入商城。因此首页分类、搜索、商品详情展示做得越清晰转化率越高。第二小程序获客成本低。微信生态内可以通过分享卡片、朋友圈、公众号文章进入商城对中小商家来说不需要单独开发 App就能触达目标用户。第三订单和库存管理需要后端支撑。商品上下架、库存扣减、订单状态流转、会员信息这些核心数据只有放在服务端才可靠。小程序端只负责展示与交互不直接操作数据库。1.2 功能模块拆解在开发之前先把系统拆成三个端功能边界会更清楚用户端小程序端微信授权登录首页轮播图与今日推荐商品分类浏览商品搜索商品详情购物车生成订单与模拟支付订单列表与状态查看个人中心管理后台Vue3 端管理员登录首页数据概览分类管理商品管理订单管理会员管理后端服务Spring Boot会员登录接口商品与分类接口购物车接口订单与库存事务文件上传商品图片管理端鉴权考虑到这是一篇教程型长文下面不会把所有页面代码逐行贴完而会围绕“购物车、商品、订单”这几个核心链路做重点实现。其余模块用同样结构可以补齐。1.3 技术选型分析后端使用 Spring Boot 是目前 Java 开发电商系统的默认选项。它内置了 Web 容器、自动配置、健康检查、统一异常处理等能力可以大幅减少配置代码。数据访问层选择 MyBatis Plus因为它提供了 BaseMapper 和 ServiceImpl能很方便地完成单表 CRUD 和分页非常适合中小型商城项目。前端管理后台使用 Vue3 Vite。Vue3 的组合式 API 让业务逻辑复用更加方便Vite 的开发服务器启动速度比 Webpack 快很多日常页面联调体验更好。状态管理使用 Pinia和 Vue3 的配合比 Vuex 更简洁且对 TypeScript 支持更友好。小程序端可以使用原生微信小程序也可以使用 uni-app。考虑到项目标题包含 Vue3很多读者希望一套前端语法多端复用因此本文选择 uni-app 的 Vue3 模式。它可以编译到微信小程序、H5、App业务代码风格接近 Vue3 SFC后续维护成本较低。2. 环境准备与版本约定2.1 开发环境清单开始编码前建议先统一开发环境工具建议版本说明JDK17Spring Boot 3.x 必须使用 JDK 17 及以上Maven3.6管理后端依赖MySQL8.0业务数据库字符集 utf8mb4Node.js18Vue3 与 uni-app 项目的运行环境微信开发者工具最新稳定版运行小程序端IDEA2023后端和 Vue3 开发版本需要根据你的实际环境调整本文示例以常见环境为例重点演示配置思路。如果你的电脑已经装了 JDK 8并且暂时不方便升级那么创建 Spring Boot 项目时应选择 2.7.x代码主体结构差别不大但部分 API 需要回退兼容。2.2 工程目录设计推荐把三个端放在同一个总目录下方便统一提交和排查sport-mall/ ├── sport-mall-server # Spring Boot 后端 ├── sport-mall-admin # Vue3 管理后台 └── sport-mall-miniapp # uni-app 小程序端后端按业务模块分包而不是把所有类都堆在一个 Controller 里com.sport.mall ├── MallApplication.java ├── common # 统一返回、异常、常量 ├── config # 跨域、MyBatis Plus 配置 ├── controller # 接口层 ├── service # 业务层 ├── mapper # 数据访问层 ├── entity # 数据库实体 └── dto # 入参出参对象这样的分包方式在课程设计和企业项目中都适用可以让“接口、业务、数据”三层边界清晰。小项目不会增加太多工作量后续扩展时却可以减少大量重构时间。3. 数据库表设计与初始化脚本3.1 业务表规划电商类项目最重要的是表结构设计。这里规划了 7 张核心表member小程序会员表保存微信 openid、昵称、头像category商品分类表product商品表保存价格、库存、封面图、详情cart购物车表orders订单主表保存收货人信息和订单金额order_item订单明细表保存下单时的商品快照banner首页轮播图表把订单拆分主表和明细表是电商系统的通用做法。如果一个订单包含多件商品orders 表只需要记录一次收货地址和总金额每件商品独立记录在 order_item 中这样后续做售后、统计、对账都会方便很多。3.2 建表 SQL以下 SQL 仅用于测试环境如果在已有业务库执行请先备份数据CREATE DATABASE IF NOT EXISTS sport_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE sport_mall; DROP TABLE IF EXISTS order_item; DROP TABLE IF EXISTS orders; DROP TABLE IF EXISTS cart; DROP TABLE IF EXISTS product; DROP TABLE IF EXISTS category; DROP TABLE IF EXISTS member; DROP TABLE IF EXISTS banner; CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(64) DEFAULT COMMENT 昵称, avatar VARCHAR(255) DEFAULT COMMENT 头像, phone VARCHAR(20) DEFAULT COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) COMMENT 小程序会员表; CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, icon VARCHAR(255) DEFAULT COMMENT 分类图标, sort INT DEFAULT 0 COMMENT 排序号, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ) COMMENT 商品分类表; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 商品名称, subtitle VARCHAR(255) DEFAULT COMMENT 商品卖点, cover VARCHAR(255) NOT NULL COMMENT 封面图, detail TEXT COMMENT 商品详情, price DECIMAL(10,2) NOT NULL COMMENT 销售价, original_price DECIMAL(10,2) DEFAULT 0 COMMENT 原价, stock INT NOT NULL COMMENT 库存, sales INT DEFAULT 0 COMMENT 销量, category_id BIGINT NOT NULL COMMENT 分类id, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category (category_id) ) COMMENT 商品表; CREATE TABLE cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, checked TINYINT DEFAULT 1 COMMENT 1勾选 0未勾选, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_member_product (member_id, product_id) ) COMMENT 购物车表; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, member_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, receiver_name VARCHAR(50) NOT NULL, receiver_phone VARCHAR(20) NOT NULL, receiver_address VARCHAR(255) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL, UNIQUE KEY uk_order_no (order_no), KEY idx_member (member_id) ) COMMENT 订单表; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_title VARCHAR(200) NOT NULL, product_cover VARCHAR(255) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, KEY idx_order (order_id) ) COMMENT 订单明细表; CREATE TABLE banner ( id BIGINT PRIMARY KEY AUTO_INCREMENT, image_url VARCHAR(255) NOT NULL, link_url VARCHAR(255) DEFAULT COMMENT 跳转链接, sort INT DEFAULT 0, status TINYINT DEFAULT 1 ) COMMENT 轮播图表;3.3 关键表关系说明member 表与 cart、orders 是一对多关系member.id 作为外键逻辑关联。product 表通过 category_id 关联 category。order_item 表通过 order_id 关联 orders但商品字段直接冗余在明细表里比如 product_title、product_cover。这样设计的目的是用户下单后即使商品被删除或改名历史订单仍然能保留当时的商品快照。这里的“外键”没有在数据库里创建物理约束而是通过应用层逻辑保持一致。在电商高并发场景下物理外键会增加维护成本我们通常只建普通索引就够了。4. Spring Boot 后端核心开发4.1 初始化后端工程打开 Spring Initializr选择 Spring Boot 3.x、JDK 17、Java 工程添加 Web、MySQL、Validation 依赖。创建完成后修改 pom.xml补充 MyBatis Plus 和 Lombok。!-- 文件路径sport-mall-server/pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies如果你复制代码后下载依赖失败可以先用 start.spring.io 创建一个最新稳定版本的空工程再把 MyBatis Plus Starter 的版本替换成当前仓库能拉到的最新版本。4.2 配置 application.yml后端默认端口设置为 8080。为了避免管理后台开发时跨域我通常会在后端统一开启跨域配置而在生产环境更推荐使用 Nginx 反向代理不依赖后端跨域。# 文件路径sport-mall-server/src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sport_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 wx: appid: 你的小程序appid secret: 你的小程序secretmap-underscore-to-camel-case 开启后数据库字段 order_no 可以自动映射到实体的 orderNo 属性。这里的 wx.appid 和 wx.secret 是敏感配置在现实项目中不要提交到 Git应放到环境变量或配置中心。4.3 统一返回结构与全局异常接口层不能直接把 null 或异常堆栈抛给前端。定义统一返回结构后小程序前端只需要判断 code 是否为 200就可以拿到 data 数据或提示 message。package com.sport.mall.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }再添加一个全局异常处理器。这样业务代码里抛出 RuntimeException 时前端得到的是简洁错误信息而不是默认的错误页。package com.sport.mall.common; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(RuntimeException.class) public ResultString handleRuntimeException(RuntimeException e) { return Result.error(e.getMessage()); } }4.4 商品分类与商品列表接口小程序首页需要展示商品列表后端可以提供一个分页查询接口/api/product/page支持按分类筛选、按关键字模糊搜索并且只返回上架状态的商品。先定义实体类package com.sport.mall.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private String title; private String subtitle; private String cover; private String detail; private BigDecimal price; private BigDecimal originalPrice; private Integer stock; private Integer sales; private Long categoryId; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }然后创建 ProductServicepackage com.sport.mall.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.sport.mall.entity.Product; import com.sport.mall.mapper.ProductMapper; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; Service public class ProductService { Resource private ProductMapper productMapper; public PageProduct page(Integer pageNum, Integer pageSize, Long categoryId, String keyword) { PageProduct page new Page(pageNum, pageSize); QueryWrapperProduct wrapper new QueryWrapper(); wrapper.eq(status, 1); if (categoryId ! null) { wrapper.eq(category_id, categoryId); } if (StringUtils.hasText(keyword)) { wrapper.like(title, keyword); } wrapper.orderByDesc(sales); return productMapper.selectPage(page, wrapper); } }Controller 的路径不需要带.json后缀按照 REST 风格写即可package com.sport.mall.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.sport.mall.common.Result; import com.sport.mall.entity.Product; import com.sport.mall.service.ProductService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; RestController RequestMapping(/api/product) public class ProductController { Resource private ProductService productService; GetMapping(/page) public ResultPageProduct page( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { return Result.success(productService.page(pageNum, pageSize, categoryId, keyword)); } }在启动类上添加 Mapper 扫描package com.sport.mall; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.sport.mall.mapper) public class MallApplication { public static void main(String[] args) { SpringApplication.run(MallApplication.class, args); } }这里需要注意 MyBatis Plus 的分页插件配置。如果不配置分页插件selectPage 物理分页不会生效接口会返回全表数据。创建配置文件package com.sport.mall.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(100L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }设置 maxLimit 是防止前端把 pageSize 调成 100000 导致数据库压力激增这也是接口层最基本的自我保护。4.5 购物车与下单接口购物车保存的是用户选中的商品关系下单时才计算价格并扣库存。购物车新增接口可以这样设计package com.sport.mall.controller; import com.sport.mall.common.Result; import com.sport.mall.entity.Cart; import com.sport.mall.mapper.CartMapper; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; RestController RequestMapping(/api/cart) public class CartController { Resource private CartMapper cartMapper; GetMapping(/list) public ResultListCart list(RequestHeader(X-Member-Id) Long memberId) { return Result.success(cartMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.QueryWrapperCart() .eq(member_id, memberId) .orderByDesc(id))); } PostMapping(/add) public ResultString add(RequestHeader(X-Member-Id) Long memberId, RequestBody Cart cart) { cart.setMemberId(memberId); cart.setChecked(1); cartMapper.insert(cart); return Result.success(加入购物车成功); } }这里用 X-Member-Id 请求头表示当前用户 ID只是为了方便阅读代码。真实项目中应该使用登录后的 Token 换取用户信息而不是直接把用户 ID 放在请求头里。订单接口是最需要体现事务控制的地方。创建订单时需要先查询商品、计算金额、扣减库存再插入订单主表和明细表。任何一个环节失败整个订单都不应该落库。package com.sport.mall.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.sport.mall.dto.OrderCreateDTO; import com.sport.mall.entity.OrderItem; import com.sport.mall.entity.Orders; import com.sport.mall.entity.Product; import com.sport.mall.mapper.OrderItemMapper; import com.sport.mall.mapper.OrdersMapper; import com.sport.mall.mapper.ProductMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; Service public class OrderService { Resource private ProductMapper productMapper; Resource private OrdersMapper ordersMapper; Resource private OrderItemMapper orderItemMapper; Transactional(rollbackFor Exception.class) public Long createOrder(Long memberId, OrderCreateDTO dto) { String orderNo SP System.currentTimeMillis(); ListOrderItem itemList new ArrayList(); BigDecimal totalAmount BigDecimal.ZERO; for (OrderCreateDTO.Item itemDTO : dto.getItems()) { Product product productMapper.selectById(itemDTO.getProductId()); if (product null || product.getStatus() ! 1) { throw new RuntimeException(商品不存在或已下架); } if (product.getStock() itemDTO.getQuantity()) { throw new RuntimeException(库存不足 product.getTitle()); } product.setStock(product.getStock() - itemDTO.getQuantity()); productMapper.updateById(product); OrderItem item new OrderItem(); item.setProductId(product.getId()); item.setProductTitle(product.getTitle()); item.setProductCover(product.getCover()); item.setPrice(product.getPrice()); item.setQuantity(itemDTO.getQuantity()); itemList.add(item); totalAmount totalAmount.add( product.getPrice().multiply(new BigDecimal(itemDTO.getQuantity()))); } Orders order new Orders(); order.setOrderNo(orderNo); order.setMemberId(memberId); order.setTotalAmount(totalAmount); order.setStatus(0); order.setReceiverName(dto.getReceiverName()); order.setReceiverPhone(dto.getReceiverPhone()); order.setReceiverAddress(dto.getReceiverAddress()); order.setCreateTime(LocalDateTime.now()); ordersMapper.insert(order); for (OrderItem item : itemList) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } return order.getId(); } }这段代码体现了三个关键操作Transactional(rollbackFor Exception.class)保证事务回滚。下单时更新库存避免后插入订单却扣减失败。订单明细复制商品价格和封面保存商品快照。需要说明的是上面的库存扣减使用了“先查再改”的逻辑适合业务量不大的初版系统。如果后续参加秒杀、促销活动并发量上来后容易超卖那时候需要把 update 语句改成带库存条件的原子更新UPDATE product SET stock stock - 1 WHERE id ? AND stock 0这样才能保证多个事务并发扣减时不会把库存扣成负数。4.6 小程序登录接口小程序端拿到微信登录 code 后不能只靠前端解析用户信息必须把 code 传给后端由后端调用微信接口换取 openid。这样做的原因是 appid 和 secret 不能暴露在小程序包里。package com.sport.mall.controller; import com.sport.mall.common.Result; import com.sport.mall.entity.Member; import com.sport.mall.mapper.MemberMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.Map; RestController RequestMapping(/api/member) public class MemberController { Value(${wx.appid}) private String wxAppId; Value(${wx.secret}) private String wxSecret; Resource private RestTemplate restTemplate; Resource private MemberMapper memberMapper; PostMapping(/login) public ResultString login(RequestBody MapString, String params) { String code params.get(code); String url https://api.weixin.qq.com/sns/jscode2session?appid wxAppId secret wxSecret js_code code grant_typeauthorization_code; MapString, Object response restTemplate.getForObject(url, Map.class); if (response null || response.get(openid) null) { return Result.error(微信登录失败); } String openid response.get(openid).toString(); Member member memberMapper.selectOne( new com.baomidou.mybatisplus.core.conditions.query.QueryWrapperMember() .eq(openid, openid)); if (member null) { member new Member(); member.setOpenid(openid); memberMapper.insert(member); } return Result.success(String.valueOf(member.getId())); } }在小程序登录演示阶段用返回的 memberId 模拟登录态。正式上线时这里应该生成一个随机 Token 并存储到 Redis设置过期时间客户端后续请求携带 Authorization 请求头后端通过拦截器解析用户身份。5. Vue3 管理后台实现5.1 创建 Vue3 工程并安装依赖管理后台选择 Vue3 Element Plus。执行以下命令npm create vitelatest sport-mall-admin -- --template vue cd sport-mall-admin npm install npm install element-plus axios pinia vue-router npm run dev创建好的 Vue3 项目结构里只需要关注 src 目录src/ ├── api/ # 接口请求 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由 ├── stores/ # Pinia 状态管理 ├── views/ # 页面 ├── App.vue └── main.js5.2 配置 Vite 代理在开发环境中Vue3 项目默认运行在 5173 端口后端接口在 8080 端口直接跨域。最简单的方案是在 vite.config.js 里配置代理把 /api 开头的请求转发到后端。// 文件路径sport-mall-admin/vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })之后前端请求接口使用request.get(/api/product/page)开发服务器会转发到http://localhost:8080/api/product/page。线上部署时只需要 Nginx 将 /api 反代到后端地址前端代码不用大改。5.3 axios 封装与 Pinia 状态管理封装 axios 时要统一处理 Token、响应结构、错误提示。这里给出最小封装// 文件路径sport-mall-admin/src/api/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(admin_token) if (token) { config.headers.Authorization token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(admin_token) router.push(/login) } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } ) export default requestPinia 在 Vue3 中通常用来保存用户信息和菜单权限。创建 user store// 文件路径sport-mall-admin/src/stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ name: localStorage.getItem(admin_name) || }), actions: { setUser(name) { this.name name localStorage.setItem(admin_name, name) }, logout() { this.name localStorage.removeItem(admin_name) localStorage.removeItem(admin_token) } } })5.4 商品管理页面商品管理页面是后台最核心的表格页。使用 Vue3 组合式 API 时可以把接口请求和数据响应放在 script setup 里!-- 文件路径sport-mall-admin/src/views/product/index.vue -- template div el-card shadownever el-form :inlinetrue el-form-item label商品名称 el-input v-modelquery.title placeholder请输入商品名称 clearable stylewidth: 220px / /el-form-item el-form-item el-button typeprimary clickloadList查询/el-button /el-form-item /el-form /el-card el-card shadownever stylemargin-top: 16px el-table v-loadingloading :dataproductList border stripe el-table-column propid labelID width70 / el-table-column propcover label封面图 width100 template #default{ row } el-image :srcrow.cover stylewidth:
上一篇/下一篇内容由系统自动关联
返回资讯列表 →