Building a Shopping Cart Feature with Vue.js and Vant UI

1. Overview

This implementation covers the shopping cart functionality including cart management, quantity adjustments, item selection, and category browsing.

2. Home Page Layout Update

The home page displays a product list with pagination support:

<van-list
  v-if="isLoggedIn"
  v-model="loadingState"
  :finished="allLoaded"
  finished-text="No more items"
  @load="fetchProducts"
>
  <ProductList :products="productData"/>
</van-list>
<div v-else>
  Please login to view more products
  <router-link to="/login">Login</router-link>
</div>

3. Shopping Cart Functionality

API endpoint: /cart/add?userId=1&productId=2&quantity=1&authToken=111

3.1 Modifying Login API

The login endpoint returns additional user information after succesfull authentication.

Backend: routes/users.js

// User login handler
router.post('/login', (req, res, next) => {
  const { tel, password } = req.body;
  
  User.find({ tel }, { _id: 0 }).then(data => {
    if (data.length === 0) {
      res.send({ code: '10086', message: 'User not registered' });
    } else {
      const storedPassword = data[0].password;
      const passwordValid = bcrypt.compareSync(password, storedPassword);
      
      if (passwordValid) {
        const userId = data[0].userid;
        const username = data[0].username;
        const token = jwt.sign({ userId }, 'secretkey', {
          expiresIn: 60 * 60 * 24 * 7
        });
        res.send({
          code: '10010',
          message: 'Login successful',
          token: token,
          userId: userId,
          username: username
        });
      } else {
        res.send({
          code: '10100',
          message: 'Invalid password'
        });
      }
    }
  });
});

3.2 Login Page Storage

Views: login/index.vue

methods: {
  handleLogin () {
    if (this.tel === '' || this.phoneError) {
      this.feedback = 'Invalid phone number';
      return;
    }
    if (this.password === '' || this.passwordError) {
      this.feedback = 'Invalid password format';
      return;
    }
    
    axios.post('/users/login', {
      tel: this.tel,
      password: this.password
    }).then(res => {
      const code = res.data.code;
      
      if (code === '10086') {
        this.feedback = 'User not registered';
      } else if (code === '10100') {
        this.feedback = 'Invalid password';
      } else {
        this.feedback = '';
        localStorage.setItem('authToken', res.data.token);
        localStorage.setItem('userId', res.data.userId);
        localStorage.setItem('username', res.data.username);
        this.$router.back();
      }
    });
  }
}

3.3 Product Detail Add to Cart

The detail page includes cart action buttons:

<van-goods-action-icon icon="cart-o" @click="goToCart" text="Cart"/>
<van-goods-action-button type="warning" @click="addToCart" text="Add to Cart"/>
methods: {
  addToCart () {
    const userId = localStorage.getItem('userId');
    const authToken = localStorage.getItem('authToken');
    const productId = this.productId;
    const quantity = 1;
    const url = `/cart/add?userId=${userId}&productId=${productId}&quantity=${quantity}&authToken=${authToken}`;
    
    axios.get(url).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else {
        Toast('Added to cart');
      }
    });
  },
  goToCart () {
    this.$router.push('/cart');
  }
}

4. Shopping Cart View

<template>
  <div class="cart-container">
    <header class="cart-header">Shopping Cart</header>
    <div class="cart-body">
      <ul class="items" v-if="hasItems">
        <li class="item" v-for="(item, index) in cartItems" :key="item.productId" @click="viewDetail(item.productId)">
          <div class="item-image">
            <img :src="item.productImage" alt="">
          </div>
          <div class="item-details">
            <h2>{{ index }}-{{ item.productName }}</h2>
            <h3>{{ item.brand }}</h3>
            <p>{{ '¥' + item.price }}</p>
            <div class="quantity-control">
              <button @click="decrement(item)">-</button>{{ item.quantity }}<button @click="increment(item)">+</button>
            </div>
          </div>
        </li>
      </ul>
      <div v-else>
        Your cart is empty, <router-link to="/home">Start Shopping</router-link>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
export default {
  data () {
    return {
      cartItems: [],
      hasItems: false
    };
  },
  created () {
    const userId = localStorage.getItem('userId');
    const authToken = localStorage.getItem('authToken');
    const url = `/cart?userId=${userId}&authToken=${authToken}`;
    
    axios.get(url).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else if (res.data.code === '11000') {
        this.hasItems = false;
      } else {
        this.hasItems = true;
        this.cartItems = res.data.data;
      }
    });
  }
};
</script>

<style lang="scss">
@import '@/lib/reset.scss';
.items {
  @include rect(100%, auto);
  .item {
    @include rect(100%, 1rem);
    @include border(0 0 1px 0, #efefef, solid);
    @include flexbox();
    .item-image {
      @include rect(1rem, 1rem);
      img {
        @include rect(0.9rem, 0.9rem);
        @include border(1px, #f66, solid);
        @include margin(0.05rem);
        @include display(block);
      }
    }
    .item-details {
      @include flex();
    }
  }
}
</style>

5. Cart Quantity Management

<div class="quantity-control">
  <button @click="decrease(item)">-</button>{{ item.quantity }}<button @click="increase(item)">+</button>
  <button @click="removeItem(item, index)">Delete</button>
</div>
methods: {
  decrease (item) {
    const authToken = localStorage.getItem('authToken');
    const quantity = item.quantity > 1 ? --item.quantity : 1;
    
    axios.get(`/cart/update?authToken=${authToken}&cartId=${item.cartId}&quantity=${quantity}`).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else {
        item.quantity = quantity;
      }
    });
  },
  increase (item) {
    const authToken = localStorage.getItem('authToken');
    const quantity = ++item.quantity;
    
    axios.get(`/cart/update?authToken=${authToken}&cartId=${item.cartId}&quantity=${quantity}`).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else {
        item.quantity = quantity;
      }
    });
  },
  removeItem (item, index) {
    const authToken = localStorage.getItem('authToken');
    
    axios.get(`/cart/delete?authToken=${authToken}&userId=${item.userId}&productId=${item.productId}`).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else {
        this.cartItems.splice(index, 1);
      }
    });
  }
}

6. Order Summary Calculation

6.1 Layout Structure

<div class="order-summary">
  <ul>
    <li>
      <p>Total Items: <span>{{ totalQuantity }}</span></p>
    </li>
    <li>
      <p>Total: <span>{{ totalAmount }}</span></p>
    </li>
    <li class="submit-btn">Submit Order</li>
  </ul>
</div>

<style lang="scss">
.order-summary {
  @include rect(100%, 0.5rem);
  @include border(1px 0 0 0, #f66, solid);
  @include fixed();
  @include bottom(0.5rem);
  @include background-color(#fff);
  ul {
    @include rect(100%, 100%);
    @include flexbox();
    li {
      @include flexbox();
      @include justify-content();
      @include align-items();
      &:nth-child(1) { @include flex(4); }
      &:nth-child(2) { @include flex(4); }
      &:nth-child(3) {
        @include flex(2);
        @include background-color(#f66);
        @include color(#fff);
      }
      p span { @include color(#f66); }
    }
  }
}
</style>

6.2 Computed Properties

computed: {
  totalQuantity () {
    let count = 0;
    this.cartItems.map(item => {
      count += item.quantity;
    });
    return count;
  },
  totalAmount () {
    let sum = 0;
    this.cartItems.map(item => {
      sum += item.quantity * item.price;
    });
    return sum.toFixed(2);
  }
}

7. Item Selection and Select All

7.1 Adding Selection Flags

Each cart item gets a selection flag during data fetch:

created () {
  const userId = localStorage.getItem('userId');
  const authToken = localStorage.getItem('authToken');
  const url = `/cart?userId=${userId}&authToken=${authToken}`;
  
  axios.get(url).then(res => {
    if (res.data.code === '10119') {
      this.$router.push('/login');
    } else if (res.data.code === '11000') {
      this.hasItems = false;
    } else {
      this.hasItems = true;
      const items = res.data.data;
      items.map(item => {
        item.selected = true;
      });
      this.cartItems = items;
    }
  });
}

7.2 Selection Calculation

computed: {
  totalQuantity () {
    let count = 0;
    this.cartItems.map(item => {
      item.selected ? count += item.quantity : count += 0;
    });
    return count;
  },
  totalAmount () {
    let sum = 0;
    this.cartItems.map(item => {
      item.selected ? sum += item.quantity * item.price : sum += 0;
    });
    return sum.toFixed(2);
  }
}

7.3 Individual Selection Toggle

<input type="checkbox" v-model="item.selected" @change="toggleSelection(item)">
data () {
  return {
    cartItems: [],
    hasItems: false,
    selectAll: true
  };
},
methods: {
  toggleSelection (item) {
    const allSelected = this.cartItems.every(item => item.selected === true);
    this.selectAll = allSelected;
  }
}

7.4 Select All Functionality

<input type="checkbox" v-model="selectAll" @change="toggleAll">Select All
methods: {
  toggleAll () {
    if (this.selectAll) {
      this.cartItems.map(item => item.selected = true);
    } else {
      this.cartItems.map(item => item.selected = false);
    }
  }
}

8. Submitting Selected Items

When submitting an order, collect all selected cart items along with user information and send to the order processing endpoint. Remove purchased items from the cart after successful submission.

9. Category Browse Feature

9.1 Backend API Endpoints

Routes: pro.js

// Get category types
router.get('/category', (req, res, next) => {
  const { type } = req.query;
  
  Product.find({ type }, { _id: 0, brand: 1, brandImage: 1 }).then(data => {
    const seen = {};
    data = data.reduce((unique, item) => {
      if (!seen[item.brand]) {
        seen[item.brand] = true;
        unique.push(item);
      }
      return unique;
    }, []);
    res.send({
      code: '200',
      message: 'Category list retrieved',
      data: data
    });
  });
});

// Get products by brand
router.get('/brandcategory', (req, res, next) => {
  const { brand } = req.query;
  
  Product.find({ brand }, { _id: 0 }).then(data => {
    res.send({
      code: '200',
      message: 'Brand products retrieved',
      data: data
    });
  });
});

// Search products
router.get('/search', (req, res, next) => {
  const { text } = req.query;
  
  Product.find({ productName: eval('/' + text + '/') }, { _id: 0 }).then(data => {
    res.send({
      code: '200',
      message: 'Search results',
      data: data
    });
  });
});

9.2 Category Page Layout

<template>
  <div class="category-page">
    <header class="header">Categories</header>
    <div class="content">
      <div class="category-layout">
        <div class="sidebar">
          <ul>
            <li>Phones</li>
            <li>Accessories</li>
          </ul>
        </div>
        <div class="main-content"></div>
      </div>
    </div>
  </div>
</template>

<style lang="scss">
@import '@/lib/reset.scss';
.content {
  .category-layout {
    @include rect(100%, 100%);
    @include flexbox();
    .sidebar {
      @include rect(1rem, 100%);
      @include background-color(#00f);
      ul {
        @include rect(100%, 100%);
        li {
          @include rect(100%, 0.36rem);
          @include border(0 0 1px, #efefef, solid);
          @include line-height(0.36rem);
          @include text-align();
        }
      }
    }
    .main-content {
      @include flex();
      @include rect(auto, 100%);
      @include background-color(#0f0);
      @include overflow();
    }
  }
}
</style>

9.3 Loading Category List

data () {
  return {
    categoryList: []
  };
},
created () {
  const authToken = localStorage.getItem('authToken');
  const url = `/product/types?authToken=${authToken}`;
  
  axios.get(url).then(res => {
    if (res.data.code === '10119') {
      this.$router.push('/login');
    } else {
      this.categoryList = res.data.data;
    }
  });
}
<div class="sidebar">
  <ul>
    <li v-for="(category, index) in categoryList" :key="index">{{ category }}</li>
  </ul>
</div>

9.4 Loading Brand Data by Category

methods: {
  loadBrands (category) {
    const authToken = localStorage.getItem('authToken');
    const url = `/product/category?authToken=${authToken}&type=${category}`;
    
    axios.get(url).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else {
        this.brandList = res.data.data;
      }
    });
  }
}
<div class="main-content">
  <div class="brand-section">
    <ul>
      <li v-for="(brand, index) in brandList" :key="index">
        {{ brand.brand }}
      </li>
    </ul>
  </div>
</div>

9.5 Loading Products by Brand

data () {
  return {
    categoryList: [],
    brandList: [],
    productList: []
  };
},
methods: {
  loadProducts (brand) {
    const authToken = localStorage.getItem('authToken');
    const url = `/product/brandcategory?authToken=${authToken}&brand=${brand.brand}`;
    
    axios.get(url).then(res => {
      if (res.data.code === '10119') {
        this.$router.push('/login');
      } else {
        this.productList = res.data.data;
      }
    });
  }
}

9.6 Default Selection States

data () {
  return {
    categoryList: [],
    brandList: [],
    productList: [],
    categoryIndex: 0,
    brandIndex: 0
  };
},
methods: {
  selectCategory (category, index) {
    this.categoryIndex = index;
    this.brandIndex = 0;
    this.loadBrands(category);
  },
  selectBrand (brand, index) {
    this.brandIndex = index;
    this.loadProducts(brand);
  }
}
<li :class="{ active: categoryIndex === index }" 
    v-for="(category, index) in categoryList" 
    @click="selectCategory(category, index)">
  {{ category }}
</li>

<li :class="{ active: brandIndex === index }" 
    v-for="(brand, index) in brandList" 
    @click="selectBrand(brand, index)">
  {{ brand.brand }}
</li>

9.7 Loading Default Data

created () {
  const authToken = localStorage.getItem('authToken');
  const url = `/product/types?authToken=${authToken}`;
  
  axios.get(url).then(res => {
    if (res.data.code === '10119') {
      this.$router.push('/login');
    } else {
      this.categoryList = res.data.data;
      this.selectCategory(this.categoryList[0], 0);
    }
  });
}
loadBrands (category) {
  this.categoryIndex = this.categoryIndex;
  this.brandIndex = 0;
  const authToken = localStorage.getItem('authToken');
  const url = `/product/category?authToken=${authToken}&type=${category}`;
  
  axios.get(url).then(res => {
    if (res.data.code === '10119') {
      this.$router.push('/login');
    } else {
      this.brandList = res.data.data;
      this.loadProducts(this.brandList[0]);
    }
  });
}

10. Search Functionality

The search feature provides real-time product discovery through keyword-based queries.

Tags: Vue.js Vant UI shopping cart javascript Frontend Development

Posted on Mon, 21 Sep 2026 16:58:32 +0000 by digitallookout