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.