当前位置: 首页 > article >正文

用Vue3+SpringBoot实现餐厅点餐系统的购物车功能

前言

在餐厅点餐系统中,购物车是一个核心功能模块。本文将详细介绍如何使用Vue3和SpringBoot技术栈来实现一个功能完整的购物车系统。我们将从前端到后端,逐步展开讲解实现过程。

技术栈

  • 前端:Vue3 + Vite + Pinia + Element Plus
  • 后端:SpringBoot 2.7.x + MyBatis-Plus + MySQL
  • 开发工具:IDEA、VS Code
  • 依赖管理:Maven

一、数据库设计

首先,我们需要设计相关的数据表结构。主要涉及以下几个表:

-- 购物车表
CREATE TABLE `cart` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `user_id` bigint(20) NOT NULL COMMENT '用户ID',
    `dish_id` bigint(20) NOT NULL COMMENT '菜品ID',
    `quantity` int(11) NOT NULL COMMENT '数量',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表';

-- 菜品表
CREATE TABLE `dish` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `name` varchar(50) NOT NULL COMMENT '菜品名称',
    `price` decimal(10,2) NOT NULL COMMENT '价格',
    `description` varchar(200) DEFAULT NULL COMMENT '描述',
    `image` varchar(200) DEFAULT NULL COMMENT '图片',
    `status` tinyint(4) DEFAULT '1' COMMENT '状态:1-在售 0-停售',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表';

二、后端实现

1. 实体类定义

@Data
@TableName("cart")
public class Cart {
    private Long id;
    private Long userId;
    private Long dishId;
    private Integer quantity;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    
    // 扩展字段,用于前端展示
    @TableField(exist = false)
    private String dishName;
    @TableField(exist = false)
    private BigDecimal price;
}

2. Service层实现

@Service
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements CartService {
    
    @Autowired
    private DishService dishService;
    
    @Override
    public void addToCart(Long userId, Long dishId, Integer quantity) {
        // 查询是否已存在购物车记录
        LambdaQueryWrapper<Cart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Cart::getUserId, userId)
                   .eq(Cart::getDishId, dishId);
        Cart cart = this.getOne(queryWrapper);
        
        if (cart != null) {
            // 已存在,更新数量
            cart.setQuantity(cart.getQuantity() + quantity);
            this.updateById(cart);
        } else {
            // 不存在,新增记录
            cart = new Cart();
            cart.setUserId(userId);
            cart.setDishId(dishId);
            cart.setQuantity(quantity);
            this.save(cart);
        }
    }
    
    @Override
    public List<CartVO> getUserCart(Long userId) {
        // 查询用户购物车列表
        LambdaQueryWrapper<Cart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Cart::getUserId, userId);
        List<Cart> cartList = this.list(queryWrapper);
        
        // 填充菜品信息
        return cartList.stream().map(cart -> {
            CartVO vo = new CartVO();
            BeanUtils.copyProperties(cart, vo);
            Dish dish = dishService.getById(cart.getDishId());
            vo.setDishName(dish.getName());
            vo.setPrice(dish.getPrice());
            vo.setSubtotal(dish.getPrice().multiply(new BigDecimal(cart.getQuantity())));
            return vo;
        }).collect(Collectors.toList());
    }
}

3. Controller层实现

@RestController
@RequestMapping("/api/cart")
public class CartController {
    
    @Autowired
    private CartService cartService;
    
    @PostMapping("/add")
    public Result<Void> addToCart(@RequestBody CartDTO dto) {
        // 从Token中获取userId
        Long userId = UserContext.getCurrentUserId();
        cartService.addToCart(userId, dto.getDishId(), dto.getQuantity());
        return Result.success();
    }
    
    @GetMapping("/list")
    public Result<List<CartVO>> getCartList() {
        Long userId = UserContext.getCurrentUserId();
        List<CartVO> cartList = cartService.getUserCart(userId);
        return Result.success(cartList);
    }
    
    @PutMapping("/update")
    public Result<Void> updateQuantity(@RequestBody CartDTO dto) {
        cartService.updateQuantity(dto.getId(), dto.getQuantity());
        return Result.success();
    }
    
    @DeleteMapping("/{id}")
    public Result<Void> removeFromCart(@PathVariable Long id) {
        cartService.removeById(id);
        return Result.success();
    }
}

三、前端实现

1. Pinia状态管理

// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { CartItem } from '@/types'
import { getCartList, addToCart, updateCart, removeFromCart } from '@/api/cart'

export const useCartStore = defineStore('cart', () => {
  const cartItems = ref<CartItem[]>([])
  const loading = ref(false)
  
  // 计算属性:总价
  const total = computed(() => {
    return cartItems.value.reduce((sum, item) => {
      return sum + item.price * item.quantity
    }, 0)
  })
  
  // 获取购物车列表
  async function fetchCartList() {
    loading.value = true
    try {
      const { data } = await getCartList()
      cartItems.value = data
    } finally {
      loading.value = false
    }
  }
  
  // 添加到购物车
  async function add(dishId: number, quantity: number) {
    await addToCart({ dishId, quantity })
    await fetchCartList()
  }
  
  // 更新数量
  async function updateQuantity(id: number, quantity: number) {
    await updateCart({ id, quantity })
    await fetchCartList()
  }
  
  // 移除商品
  async function remove(id: number) {
    await removeFromCart(id)
    await fetchCartList()
  }
  
  return {
    cartItems,
    loading,
    total,
    fetchCartList,
    add,
    updateQuantity,
    remove
  }
})

2. 购物车组件实现

<!-- components/ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <el-card class="cart-container">
      <template #header>
        <div class="cart-header">
          <span>购物车</span>
          <span>{{ cartStore.cartItems.length }}件商品</span>
        </div>
      </template>
      
      <div v-if="cartStore.loading" class="loading">
        <el-skeleton :rows="3" animated />
      </div>
      
      <template v-else>
        <div v-if="cartStore.cartItems.length === 0" class="empty-cart">
          <el-empty description="购物车是空的" />
        </div>
        
        <div v-else class="cart-items">
          <div v-for="item in cartStore.cartItems" 
               :key="item.id" 
               class="cart-item">
            <div class="item-info">
              <img :src="item.image" :alt="item.dishName" class="item-image">
              <div class="item-details">
                <h4>{{ item.dishName }}</h4>
                <p class="price">¥{{ item.price }}</p>
              </div>
            </div>
            
            <div class="item-actions">
              <el-input-number 
                v-model="item.quantity"
                :min="1"
                :max="99"
                @change="(val) => handleQuantityChange(item.id, val)"
              />
              <el-button 
                type="danger" 
                circle
                @click="handleRemove(item.id)"
              >
                <el-icon><Delete /></el-icon>
              </el-button>
            </div>
          </div>
        </div>
        
        <div class="cart-footer">
          <div class="total">
            总计: <span class="price">¥{{ cartStore.total }}</span>
          </div>
          <el-button type="primary" @click="handleCheckout">
            去结算
          </el-button>
        </div>
      </template>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { useCartStore } from '@/stores/cart'
import { ElMessage } from 'element-plus'

const cartStore = useCartStore()

onMounted(() => {
  cartStore.fetchCartList()
})

const handleQuantityChange = async (id: number, quantity: number) => {
  try {
    await cartStore.updateQuantity(id, quantity)
    ElMessage.success('数量已更新')
  } catch (error) {
    ElMessage.error('更新失败')
  }
}

const handleRemove = async (id: number) => {
  try {
    await cartStore.remove(id)
    ElMessage.success('商品已移除')
  } catch (error) {
    ElMessage.error('移除失败')
  }
}

const handleCheckout = () => {
  // 跳转到结算页面
  router.push('/checkout')
}
</script>

<style scoped lang="scss">
.shopping-cart {
  .cart-container {
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
  }
  
  .cart-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  
  .cart-items {
    .cart-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 12px 0;
      border-bottom: 1px solid #eee;
      
      &:last-child {
        border-bottom: none;
      }
      
      .item-info {
        display: flex;
        align-items: center;
        gap: 12px;
        
        .item-image {
          width: 80px;
          height: 80px;
          object-fit: cover;
          border-radius: 4px;
        }
        
        .item-details {
          h4 {
            margin: 0 0 8px;
          }
          
          .price {
            color: #ff4d4f;
            font-weight: bold;
          }
        }
      }
      
      .item-actions {
        display: flex;
        align-items: center;
        gap: 12px;
      }
    }
  }
  
  .cart-footer {
    margin-top: 20px;
    padding-top: 20px;
    border-top: 1px solid #eee;
    display: flex;
    justify-content: space-between;
    align-items: center;
    
    .total {
      font-size: 16px;
      
      .price {
        color: #ff4d4f;
        font-size: 20px;
        font-weight: bold;
      }
    }
  }
}
</style>

四、功能亮点

  1. 实时数据同步
  • 使用Pinia进行状态管理
  • 操作后自动刷新购物车数据
  • 价格实时计算
  1. 性能优化
  • 使用computed计算总价
  • 列表使用key优化渲染
  • 防抖处理频繁操作
  1. 用户体验
  • Loading状态展示
  • 操作反馈提示
  • 空状态处理
  • 数量输入限制
  1. 代码质量
  • TypeScript类型检查
  • 组件化开发
  • 状态集中管理
  • 统一的错误处理

五、扩展优化建议

  1. 功能扩展
  • 添加商品规格选择
  • 支持批量删除
  • 商品库存检查
  • 添加购物车商品备注
  1. 性能优化
  • 添加接口缓存
  • 大列表虚拟滚动
  • 图片懒加载
  • 防抖节流处理
  1. 体验优化
  • 添加动画效果
  • 草稿数据本地存储
  • 移动端适配
  • 快捷键支持

总结

本文详细介绍了如何使用Vue3和SpringBoot实现餐厅点餐系统的购物车功能。通过合理的技术选型、数据结构设计和代码实现,我们实现了一个功能完整、性能优良的购物车系统。在实际开发中,我们还需要根据具体业务需求进行适当的调整和扩展。


http://www.kler.cn/a/388828.html

相关文章:

  • 结构体是否包含特定类型的成员变量
  • 【MySQL】MySQL函数之JSON_EXTRACT
  • Blender进阶:图像纹理节点和映射节点
  • 同三维T610UDP-4K60 4K60 DP或HDMI或手机信号采集卡
  • 【知识科普】SPA单页应用程序介绍
  • 量化交易系统开发-实时行情自动化交易-3.4.2.Okex行情交易数据
  • 数据库系统概论(期末复习版)
  • 简单叙述 Spring 是如何解决循环依赖问题的呢?
  • ubuntu 22.04 server 安装 mysql 5.7.40 LTS
  • layui xm-select的使用
  • ASP.NET Core 路由规则,自定义特性路由 ,IActionConstraint 路由约束 总结 mvc
  • Swift 开发教程系列 - 第12章:协议与协议扩展
  • 利用RANSAC算法拟合平面并生成包围框的点云处理方法,点云聚类、质心坐标、倾斜角度、点云最小外接矩形
  • 【JAVA】正则表达式的贪婪模式与非贪婪模式
  • 详解MySQL安装
  • 使用原生Redis完成分布式锁
  • Rust安全性与最佳实践————安全编程技巧
  • 网络安全---安全见闻
  • 安卓/华为手机恢复出厂设置后如何恢复照片
  • 树莓派AI视觉小车--5.机器人小车超声波避障
  • Typora导出pdf手动分页和设置字体样式
  • 图像信号处理器(ISP,Image Signal Processor)详解
  • 如何让其他人连接到我们的数据库、进行项目前后端分离
  • Elasticsearch+kibana+filebeat的安装及使用
  • 刘卫国MATLAB程序设计与应用课后答案PDF第三版
  • SQL--查询连续三天登录数据详解