一、路由设计配置--一级路由配置
在路由文件(一般是 router/index.js )里定义路由对象数组,每个对象包含 path (路由路径,如 '/' 代表首页)、 name (路由名称,方便代码引用)、 component (对应路由的组件 )。配置时要保证路径唯一,组件引入路径正确,否则会导致路由错误
二、登录页静态布局&图形验证码功能
(一)登录页静态布局
通过HTML构建页面结构,定义登录表单、输入框、按钮等元素。运用CSS进行样式设计,调整元素的大小、颜色、位置和间距,让页面布局合理且美观,提升用户视觉体验,确保各元素在不同屏幕尺寸下显示正常。
(二)图形验证码功能
在Vue项目中,借助相关插件或自定义代码生成图形验证码。用户输入验证码后,前端进行初步格式校验,然后与后端生成的验证码比对,验证用户身份,防止恶意登录。还会涉及验证码刷新功能,若用户看不清,可点击刷新获取新的验证码。
三、api接口模块
(一)封装图片验证码接口
1.接口封装目的:在Vue项目开发中,将图片验证码接口进行封装,能够让代码结构更清晰,增强代码的复用性,便于后续的维护和管理。同时,这也有助于提高前端与后端交互的效率,保障数据传输的准确性。
2.实现方式:通常会使用Axios库来实现接口封装。首先要安装Axios,然后在项目中引入。接着,根据后端提供的接口文档,配置请求的URL、请求方法(一般为GET请求来获取图片验证码)等参数。例如,创建一个专门的API文件,在其中定义获取图片验证码的函数,函数内部使用Axios发送请求,并对响应结果进行处理 。
3.注意事项:封装过程中要确保接口请求的安全性,对可能出现的错误(如网络异常、接口响应错误等)进行合理处理。同时,要注意接口的版本兼容性,若后端接口发生变化,及时调整前端的封装代码,以保证功能正常运
(二)实现登录
1.接口封装要点:使用Axios等工具构建登录接口。在项目中创建专门的API文件,定义登录函数。配置请求URL、方法(一般是POST),将用户输入的账号、密码作为参数传递。比如 axios.post('/login', { username, password }) ,提升代码复用性与维护性。
2.登录功能实现流程:用户在登录页输入信息,触发登录事件调用封装接口。前端收集数据并发送请求,后端验证账号密码。若正确,返回包含用户信息或token的响应;前端据此存储信息,进行页面跳转(如跳转到首页);若错误,前端接收错误提示,展示给用户。
3.安全与优化措施:对密码进行加密处理,防止传输中泄露。在前端对用户输入进行格式校验,减少无效请求。
<template><div><input v-model="username" placeholder="用户名"><input v-model="password" type="password" placeholder="密码"><button @click="login">登录</button></div>
</template><script>
import axios from 'axios';export default {data() {return {username: '',password: ''};},methods: {async login() {try {const response = await axios.post('/login', {username: this.username,password: this.password});if (response.data.success) {// 登录成功,可进行页面跳转、存储用户信息等操作console.log('登录成功');} else {console.log('登录失败', response.data.message);}} catch (error) {console.error('登录请求出错', error);}}}
};
</script>
四、toast
1.作用:toast轻提示用于在不中断用户操作的情况下,短暂展示提示信息,如操作结果反馈、系统通知等,能有效提升用户体验。
2.在登录、注册、表单提交等功能模块中广泛应用。登录成功时,显示“登录成功”的toast提示;表单提交失败时,提示“提交失败
五、短信验证功能
1.获取手机号:在前端页面利用表单组件收集用户输入的手机号,通过Vue的双向数据绑定实时获取和更新数据。
2. 发送请求获取验证码:使用Axios等工具将手机号发送到后端。后端接收到请求后,生成验证码并借助短信平台发送给用户手机。
3.倒计时与校验:前端设置倒计时,防止用户频繁获取验证码。用户收到验证码后输入,前端对其进行格式校验,再发送到后端与存储
六、响应拦截式--统一处理错误
1.概念:响应拦截器是在前端请求获取到后端响应后,在数据到达具体业务逻辑代码前,对响应数据进行统一处理的机制。在Vue项目中,通常借助Axios库实现,能有效提升代码的健壮性和用户体验。
2.优势:集中管理错误处理逻辑,避免在各个请求处理处重复编写错误处理代码,使代码更简洁、易维护。
3.在项目中配置Axios的响应拦截器,通过判断响应状态码或响应数据中的错误标识,执行不同的错误处理逻辑。例如,当状态码为404时,弹出“页面未找到”的提示;若为500,提示“服务器内部错误” 。还可以针对特定业务错误,根据响应数据中的自定义错误信息,给出相应的提示内容。
七、将token权证信息存入vuex
1.token作为用户身份验证和授权的关键凭证,在前后端交互中用于确认用户身份。通过验证token,后端能判断用户是否有权限访问特定资源,保障系统安全。
2.Vuex是Vue的状态管理工具,将token存入Vuex,能在整个Vue应用内集中管理和共享token信息。各组件可方便获取token,避免重复传递数据,提高数据管理效率,优化组件间通信。
3.登录成功后,从后端响应获取token;在Vuex的 store.js 中,定义 state 存储token,如 state: { token: null } ;利用 mutations 方法修改 state 中的token值,如 SET_TOKEN(state, token) { state.token = token } ;在登录组件内调用 mutations 方法,将token存入Vuex,实现数据全局管理
。
八、storage存储模块--vuex持久化处理
1.Vuex持久化的必要性:Vuex存储在内存中,页面刷新数据丢失。在实际应用里,如用户登录状态(token等信息)需要保持,所以要进行持久化处理,提升用户体验。
2.storage存储模块应用:借助浏览器的 localStorage 或 sessionStorage 。 localStorage 存储的数据长期有效, sessionStorage 在会话结束(关闭页面)时清除。将Vuex中的数据,如登录信息、用户设置等,转换为字符串存入storage,在页面加载时再读取并还原到Vuex 。
3.安装 vuex-persist 插件简化操作。配置插件时,指定要持久化的状态模块、存储方式(如 localStorage )。在 store.js 中引入并使用插件,完成后刷新页面,Vuex数据会从storage重新加载,实现数据持久化 。
九、添加请求loading效果
1.当用户发起网络请求时,由于网络延迟或服务器响应时间的不确定性,页面可能会出现短暂的无响应状态。添加loading效果能让用户直观感知到系统正在处理请求,提升用户体验,避免用户因等待而重复操作。
2.借助拦截器(如Axios的请求和响应拦截器)来控制loading效果的显示与隐藏。在请求发送前,设置loading状态为true,触发显示loading动画;请求完成后,无论成功或失败,将loading状态设为false,隐藏loading动画。
安装Axios:如果项目尚未安装Axios,通过npm或yarn进行安装。
bash
npm install axios
# 或
yarn add axios
javascriptimport axios from 'axios';
import Vue from 'vue';// 创建Axios实例
const service = axios.create({baseURL: process.env.VUE_APP_BASE_API, // 根据实际情况配置基础URLtimeout: 5000 // 请求超时时间
});// 请求拦截器
service.interceptors.request.use(config => {// 在请求发送前,设置loading状态为true,可在Vuex或组件data中定义loading状态Vue.$store.commit('SET_LOADING', true); // 假设在Vuex中管理loading状态,需先配置好Vuexreturn config;},error => {return Promise.reject(error);}
);// 响应拦截器
service.interceptors.response.use(response => {// 请求成功后,设置loading状态为falseVue.$store.commit('SET_LOADING', false);return response;},error => {// 请求失败时,同样设置loading状态为falseVue.$store.commit('SET_LOADING', false);return Promise.reject(error);}
);export default service;
在组件中使用并显示loading效果:在需要发起请求的组件中,假设使用Vuex管理loading状态。
html<template><div><button @click="fetchData">获取数据</button><!-- 根据loading状态显示loading效果,这里以简单的加载提示为例 --><div v-if="loading" class="loading">Loading...</div> </div>
</template><script>
import api from '@/api'; // 引入配置好的Axios实例
import { mapState } from 'vuex';export default {computed: {...mapState(['loading']) // 从Vuex中获取loading状态},methods: {async fetchData() {try {const response = await api.get('/your-api-url');console.log(response.data);} catch (error) {console.error(error);}}}
};
</script><style scoped>
.loading {margin-top: 10px;color: gray;
}
</style>
十、全局前置导航守卫
1.在Vue Router中,全局前置导航守卫是一种在路由切换前触发的函数,能对每次路由跳转进行全局控制,决定是否允许跳转、重定向到其他页面等。它在路由配置和应用逻辑间起到关键作用,增强了应用的安全性和交互性。
2.当用户尝试进行路由切换时,全局前置导航守卫函数会首先被调用。函数接收 to (即将进入的目标路由对象)、 from (当前离开的路由对象)和 next (用于控制路由跳转的函数)作为参数。根据业务逻辑判断是否调用 next() 允许跳转,或调用 next('/login') 等进行重定向。若不调用 next ,路由切换会被阻塞。
javascriptimport Vue from 'vue';
import Router from 'vue-router';
import Home from '@/views/Home.vue';
import Login from '@/views/Login.vue';Vue.use(Router);const router = new Router({routes: [{path: '/',name: 'Home',component: Home,meta: { requiresAuth: true } // 标记该路由需要登录权限},{path: '/login',name: 'Login',component: Login}]
});// 全局前置导航守卫
router.beforeEach((to, from, next) => {// 假设从Vuex中获取用户登录状态,需提前配置好Vuexconst isLoggedIn = Vue.prototype.$store.getters.isLoggedIn; if (to.matched.some(record => record.meta.requiresAuth)) {// 如果目标路由需要登录权限if (isLoggedIn) {// 用户已登录,允许跳转next(); } else {// 用户未登录,重定向到登录页next('/login'); }} else {// 目标路由不需要登录权限,直接允许跳转next(); }
});export default router;
十一、首页--静态结构准备&动态渲染
1.使用HTML构建首页的基本框架,包括页面布局的划分,如头部、主体内容区、侧边栏、底部等部分。运用CSS对各个部分进行样式设计,设置元素的大小、颜色、字体、边距、对齐方式等样式属性,让页面具备初步的视觉效果,符合设计稿要求。
2.动态渲染:在Vue环境下,利用Vue的响应式原理和数据绑定机制实现动态渲染。通过 v - for 指令遍历数据数组,为列表类数据生成对应的DOM元素;使用 v - if 或 v - show 指令根据数据状态控制元素的显示与隐藏;将数据绑定到HTML元素的属性或文本内容上,实现数据驱动视图更新。通常会在组件的 created 或 mounted 生命周期钩子函数中发起数据请求,获取后端数据后进行动态渲染。
html<template><div class="home"><!-- 头部 --><header class="header"><h1>网站首页</h1></header><!-- 主体内容区 --><main class="main"><!-- 动态渲染列表 --><ul class="article - list"><li v - for="article in articles" :key="article.id" class="article - item"><h2>{{ article.title }}</h2><p>{{ article.content }}</p></li></ul></main><!-- 底部 --><footer class="footer"><p>版权所有 © 2024</p></footer></div>
</template><style scoped>
.header {background - color: #333;color: white;text - align: center;padding: 20px;
}.main {padding: 20px;
}.article - list {list - style: none;padding: 0;
}.article - item {border: 1px solid #ccc;padding: 10px;margin - bottom: 10px;
}.footer {background - color: #333;color: white;text - align: center;padding: 10px;margin - top: 20px;
}
</style>Vue逻辑代码(在 Home.vue 的 <script> 标签内)javascriptexport default {data() {return {articles: []};},created() {// 模拟从后端获取数据,实际开发中使用Axios等库发送请求this.fetchArticles();},methods: {async fetchArticles() {// 假设后端返回的数据结构如下const responseData = [{ id: 1, title: '文章1标题', content: '文章1内容' },{ id: 2, title: '文章2标题', content: '文章2内容' }];this.articles = responseData;}}
};
十二、搜索历史管理
1.搜索历史管理在应用中起着提升用户体验的关键作用。它能帮助用户快速找回之前的搜索内容,减少重复输入,提高搜索效率。对于开发人员而言,需要考虑数据存储、展示和更新等多方面的问题。
2.数据存储:通常利用浏览器的 localStorage 或 sessionStorage 来存储搜索历史。 localStorage 存储的数据长期有效,关闭浏览器后数据依然存在; sessionStorage 则在会话期间有效,关闭页面后数据消失。将搜索历史以数组形式存储,每个搜索关键词作为数组的一个元素。
3.在页面上展示搜索历史列表,用户可以点击历史记录进行快速搜索。提供删除功能,用户能手动删除单个或全部搜索历史,保证搜索历史的时效性和相关性。
4.更新逻辑:每次用户进行新的搜索时,检查搜索关键词是否已存在于历史记录中。若存在,将其移至数组头部,保持最近搜索的记录在最前面;若不存在,将新关键词添加到数组头部,并判断数组长度,超过一定数量(如10条)时,删除最后一个元素,以控制历史记录数量。
5.
html<template><div><input v-model="searchQuery" @input="handleSearch" placeholder="搜索"><ul><li v-for="(history, index) in searchHistory" :key="index" @click="searchByHistory(history)">{{ history }}<button @click.stop="deleteHistory(index)">删除</button></li></ul><button @click="clearAllHistory">清空历史</button></div>
</template><script>
export default {data() {return {searchQuery: '',searchHistory: []};},created() {const storedHistory = localStorage.getItem('searchHistory');if (storedHistory) {this.searchHistory = JSON.parse(storedHistory);}},methods: {handleSearch() {if (this.searchQuery.trim() === '') return;if (this.searchHistory.includes(this.searchQuery)) {this.searchHistory.splice(this.searchHistory.indexOf(this.searchQuery), 1);}this.searchHistory.unshift(this.searchQuery);if (this.searchHistory.length > 10) {this.searchHistory.pop();}localStorage.setItem('searchHistory', JSON.stringify(this.searchHistory));// 这里可添加实际搜索逻辑,如调用搜索接口},searchByHistory(history) {this.searchQuery = history;// 这里可添加实际搜索逻辑,如调用搜索接口},deleteHistory(index) {this.searchHistory.splice(index, 1);localStorage.setItem('searchHistory', JSON.stringify(this.searchHistory));},clearAllHistory() {this.searchHistory = [];localStorage.removeItem('searchHistory');}}
};
</script>
十三、搜索列表页--静态布局与渲染
(一)静态布局
搜索列表页的静态布局需构建清晰的页面结构。顶部通常设置搜索框,方便用户进行新的搜索操作。中间主体部分用于展示搜索结果列表,每个结果项应包含关键信息,如标题、简介、图片(若有)等。底部可能添加分页导航,以便用户浏览多页搜索结果。利用HTML和CSS实现布局搭建,通过CSS设置元素样式,包括字体、颜色、间距、背景等,确保页面美观且符合交互逻辑。
(二)动态渲染
在Vue环境下,通过数据绑定和指令实现动态渲染。使用 v - for 指令遍历搜索结果数组,为每个结果生成对应的DOM元素。将数据绑定到HTML元素的属性或文本内容上,如 { { item.title }} 显示标题。通常在组件的生命周期钩子函数(如 created 或 mounted )中发起请求获取搜索结果数据,然后更新页面显示。
html<template><div class="search - list - page"><!-- 搜索框 --><input v - model="searchQuery" placeholder="输入关键词搜索" @input="search"><!-- 搜索结果列表 --><ul class="result - list"><li v - for="(item, index) in searchResults" :key="index" class="result - item"><img v - if="item.imageUrl" :src="item.imageUrl" alt="结果图片" class="result - image"><div class="result - content"><h3 class="result - title">{{ item.title }}</h3><p class="result - desc">{{ item.description }}</p></div></li></ul><!-- 分页导航 --><div class="pagination"><button @click="prevPage" :disabled="currentPage === 1">上一页</button><span>{{ currentPage }}/{{ totalPages }}</span><button @click="nextPage" :disabled="currentPage === totalPages">下一页</button></div></div>
</template><script>
export default {data() {return {searchQuery: '',searchResults: [],currentPage: 1,totalPages: 1,perPage: 10};},methods: {async search() {// 模拟请求搜索结果,实际需替换为真实API请求const response = await fetch(`/api/search?query=${this.searchQuery}&page=${this.currentPage}&limit=${this.perPage}`);const data = await response.json();this.searchResults = data.results;this.totalPages = data.totalPages;},prevPage() {if (this.currentPage > 1) {this.currentPage--;this.search();}},nextPage() {if (this.currentPage < this.totalPages) {this.currentPage++;this.search();}}},created() {this.search();}
};
</script><style scoped>
.search - list - page {padding: 20px;
}input {width: 300px;padding: 10px;margin - bottom: 20px;
}.result - list {list - style: none;padding: 0;
}.result - item {display: flex;align - items: start;margin - bottom: 20px;border: 1px solid #ccc;padding: 10px;
}.result - image {width: 80px;height: 80px;object - fit: cover;margin - right: 10px;
}.result - content {flex: 1;
}.result - title {margin - top: 0;margin - bottom: 5px;
}.result - desc {margin: 0;color: #666;
}.pagination {margin - top: 20px;
}button {padding: 8px 16px;margin: 0 5px;
}
</style>
十四、商品详情页--静态结构和动态渲染
(一)静态结构搭建
商品详情页的静态结构要全面展示商品信息。顶部通常是商品图片展示区域,可使用轮播图形式展示多图;接着是商品基本信息,如名称、价格、销量等;中间部分为商品详情描述,可能包含文字介绍、产品参数表格;底部设置购买按钮、评论区入口等交互元素。利用HTML构建页面框架,通过CSS进行样式设计,调整各元素的布局、字体、颜色和间距,确保页面布局合理、美观且符合用户浏览习惯。
(二)动态渲染实现
基于Vue框架,利用数据绑定和生命周期函数实现动态渲染。在组件的 created 或 mounted 钩子函数中,通过Axios等工具向后端发送请求获取商品数据。使用 v - for 指令遍历数组类型的数据(如多图数组)进行图片轮播展示;通过插值表达式(如 { { product.name }} )将商品数据绑定到对应的HTML元素,实现数据实时更新页面。若涉及复杂数据结构,如对象嵌套,要正确获取和绑定数据,保证页面展示准确。
<template><div class="product - detail - page"><!-- 商品图片区域 --><div class="product - images"><div v - for="(image, index) in product.images" :key="index" class="image - item"><img :src="image" alt="商品图片"></div></div><!-- 商品基本信息区域 --><div class="product - info"><h1>{{ product.name }}</h1><p>价格: {{ product.price }}元</p><p>销量: {{ product.sales }}</p></div><!-- 商品详情描述区域 --><div class="product - description"><h2>商品详情</h2><p v - html="product.description"></p></div><!-- 购买按钮和其他交互区域 --><div class="action - area"><button @click="addToCart">加入购物车</button><button @click="goToComments">查看评论</button></div></div>
</template><script>
import axios from 'axios';export default {data() {return {product: {}};},created() {this.fetchProductDetails();},methods: {async fetchProductDetails() {try {const response = await axios.get('/api/products/1');// 假设商品ID为1,实际根据路由参数获取this.product = response.data;} catch (error) {console.error('获取商品详情失败', error);}},addToCart() {// 加入购物车逻辑,如调用后端接口添加商品到购物车console.log('已加入购物车');},goToComments() {// 跳转到评论页逻辑,如使用Vue Router进行路由跳转console.log('跳转到评论页');}}
};
</script><style scoped>
.product - detail - page {padding: 20px;
}.product - images {display: flex;overflow - x: scroll;
}.image - item {margin - right: 10px;
}.image - item img {width: 200px;height: 200px;object - fit: cover;
}.product - info {margin - top: 20px;
}.product - description {margin - top: 20px;
}.action - area {margin - top: 20px;
}button {padding: 10px 20px;margin - right: 10px;
}
</style>
十五、加入购物车
(一)弹层显示
<template><div><!-- 商品详情页,简化示例 --><h1>商品名称: {{ product.name }}</h1><button @click="addToCart">加入购物车</button><!-- 加入购物车弹层 --><div v - if="isCartPopupVisible" class="cart - popup"><div class="cart - popup - content"><p>商品已加入购物车</p><p>商品名称: {{ product.name }}</p><p>数量: 1</p><button @click="goToCart">查看购物车</button><button @click="continueShopping">继续购物</button></div></div></div>
</template><script>
import axios from 'axios';export default {data() {return {product: { name: '示例商品' },// 实际从商品详情页获取isCartPopupVisible: false};},methods: {async addToCart() {try {// 模拟加入购物车的API请求await axios.post('/api/cart/add', { productId: 1 });this.isCartPopupVisible = true;// 自动关闭弹层,3秒后执行setTimeout(() => {this.isCartPopupVisible = false;}, 3000);} catch (error) {console.error('加入购物车失败', error);}},goToCart() {this.isCartPopupVisible = false;// 实际使用Vue Router进行路由跳转至购物车页面console.log('跳转到购物车页面');},continueShopping() {this.isCartPopupVisible = false;console.log('继续购物');}}
};
</script><style scoped>
.cart - popup {position: fixed;top: 20px;right: 20px;background - color: rgba(0, 0, 0, 0.8);color: white;padding: 20px;border - radius: 5px;box - shadow: 0 0 5px rgba(0, 0, 0, 0.3);z - index: 1000;animation: fadeIn 0.3s ease - in - out;
}@keyframes fadeIn {from {opacity: 0;transform: translateY(-20px);}to {opacity: 1;transform: translateY(0);}
}.cart - popup - content {text - align: center;
}button {margin: 10px;padding: 10px 20px;background - color: #007BFF;color: white;border: none;border - radius: 5px;cursor: pointer;
}
</style>
(二)数字框基本封装
<template><div class="quantity - box"><button @click="decreaseQuantity">-</button><inputv - model.number="quantity"type="number":min="minQuantity":max="maxQuantity"@input="handleInput"/><button @click="increaseQuantity">+</button></div>
</template><script>
export default {props: {// 初始数量initialQuantity: {type: Number,default: 1},// 最小数量minQuantity: {type: Number,default: 1},// 最大数量maxQuantity: {type: Number,default: Infinity}},data() {return {quantity: this.initialQuantity};},methods: {decreaseQuantity() {if (this.quantity > this.minQuantity) {this.quantity--;this.$emit('quantity - change', this.quantity);}},increaseQuantity() {if (this.quantity < this.maxQuantity) {this.quantity++;this.$emit('quantity - change', this.quantity);}},handleInput(event) {const inputValue = parseInt(event.target.value, 10);if (isNaN(inputValue) || inputValue < this.minQuantity) {this.quantity = this.minQuantity;} else if (inputValue > this.maxQuantity) {this.quantity = this.maxQuantity;} else {this.quantity = inputValue;}this.$emit('quantity - change', this.quantity);}}
};
</script><style scoped>
.quantity - box {display: flex;align - items: center;
}.quantity - box button {padding: 5px 10px;border: 1px solid #ccc;background - color: #f9f9f9;cursor: pointer;
}.quantity - box input {width: 50px;padding: 5px;text - align: center;border: 1px solid #ccc;margin: 0 5px;
}
</style>
(三)判断token登录提示
<template><div><button @click="addToCart">加入购物车</button></div>
</template><script>
import axios from 'axios';
export default {methods: {addToCart() {const token = localStorage.getItem('token');if (!token) {// 使用window.alert简单提示,实际可使用更美观的自定义弹窗组件const confirmLogin = window.confirm('您还未登录,请先登录');if (confirmLogin) {// 假设使用Vue Router,跳转到登录页面this.$router.push('/login');}return;}// 模拟商品数据,实际从当前商品详情获取const product = { id: 1, name: '示例商品' };this.addProductToCart(product);},async addProductToCart(product) {try {const response = await axios.post('/api/cart/add', {productId: product.id});if (response.data.success) {// 使用window.alert简单提示,实际可优化提示方式window.alert('加入购物车成功');} else {window.alert('加入购物车失败');}} catch (error) {console.error('加入购物车请求出错', error);window.alert('加入购物车失败,请稍后重试');}}}
};
</script>
十六、购物车
(一)基本静态布局
<template><div class="shopping - cart - page"><!-- 标题栏 --><header class="cart - header"><h1>购物车</h1></header><!-- 商品列表 --><div class="cart - items"><!-- 模拟商品项,实际数据从后端获取并动态渲染 --><div v - for="(item, index) in cartItems" :key="index" class="cart - item"><img :src="item.imageUrl" alt="商品图片" class="cart - item - image"><div class="cart - item - info"><p class="cart - item - name">{{ item.name }}</p><p class="cart - item - price">价格: {{ item.price }}元</p><div class="quantity - control"><span>数量: {{ item.quantity }}</span><!-- 预留数量调整按钮,后续添加交互逻辑 --><button>-</button><button>+</button></div></div><button class="delete - button" @click="deleteItem(index)">删除</button></div></div><!-- 底部信息 --><footer class="cart - footer"><div class="total - price"><p>总价: {{ totalPrice }}元</p></div><button class="checkout - button">结算</button></footer></div>
</template><script>
export default {data() {return {cartItems: [{imageUrl: 'example1.jpg',name: '商品1',price: 100,quantity: 1},{imageUrl: 'example2.jpg',name: '商品2',price: 200,quantity: 2}]};},computed: {totalPrice() {return this.cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);}},methods: {deleteItem(index) {this.cartItems.splice(index, 1);}}
};
</script><style scoped>
.shopping - cart - page {padding: 20px;
}.cart - header {text - align: center;margin - bottom: 20px;
}.cart - items {list - style: none;padding: 0;
}.cart - item {display: flex;align - items: center;border: 1px solid #ccc;padding: 10px;margin - bottom: 10px;
}.cart - item - image {width: 80px;height: 80px;object - fit: cover;margin - right: 10px;
}.cart - item - info {flex: 1;
}.cart - item - name {margin - top: 0;margin - bottom: 5px;
}.cart - item - price {margin: 0;color: #666;
}.quantity - control {margin - top: 5px;
}.delete - button {background - color: #ff0000;color: white;border: none;padding: 5px 10px;border - radius: 3px;cursor: pointer;
}.cart - footer {margin - top: 20px;display: flex;justify - content: space - between;align - items: center;
}.total - price {font - weight: bold;
}.checkout - button {background - color: #007BFF;color: white;border: none;padding: 10px 20px;border - radius: 3px;cursor: pointer;
}
</style>
(二)构建vue模块,获取数据存储
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';Vue.use(Vuex);const cartModule = {namespaced: true,state: {cartItems: [],totalPrice: 0},mutations: {SET_CART_ITEMS(state, items) {state.cartItems = items;},UPDATE_TOTAL_PRICE(state) {state.totalPrice = state.cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);}},actions: {async fetchCartItems({ commit }) {try {const response = await axios.get('/api/cart');commit('SET_CART_ITEMS', response.data);commit('UPDATE_TOTAL_PRICE');} catch (error) {console.error('获取购物车数据失败', error);}}}
};export default new Vuex.Store({modules: {cart: cartModule}
});
(三)mapState动态计算展示
1. mapState 是Vuex提供的辅助函数,用于简化从Vuex的 state 中获取数据并映射到组件计算属性的过程。在购物车功能中,借助 mapState ,能便捷地将购物车相关数据(如购物车商品列表、总价等)引入到组件中,实现数据驱动视图的更新。
2. mapState 将Vuex中购物车商品列表数据映射为组件的计算属性。利用Vue的 v - for 指令遍历该计算属性,为每个商品项生成对应的DOM元素。将商品的各项信息(如名称、价格、数量)绑定到相应的HTML元素上,实现购物车列表的动态渲染。
3.
<template><div class="shopping - cart - page"><!-- 标题栏 --><header class="cart - header"><h1>购物车</h1></header><!-- 商品列表 --><div class="cart - items"><div v - for="(item, index) in cartItems" :key="index" class="cart - item"><img :src="item.imageUrl" alt="商品图片" class="cart - item - image"><div class="cart - item - info"><p class="cart - item - name">{{ item.name }}</p><p class="cart - item - price">价格: {{ item.price }}元</p><div class="quantity - control"><span>数量: {{ item.quantity }}</span></div></div><button class="delete - button" @click="deleteItem(index)">删除</button></div></div><!-- 底部信息 --><footer class="cart - footer"><div class="total - price"><p>总价: {{ totalPrice }}元</p></div><button class="checkout - button">结算</button></footer></div>
</template><script>
import { mapState } from 'vuex';export default {computed: {...mapState('cart', {cartItems: state => state.cartItems,totalPrice: state => state.totalPrice})},methods: {deleteItem(index) {// 这里暂未实现删除功能的具体逻辑,实际需调用Vuex的action修改购物车数据console.log(`删除第 ${index + 1} 个商品`);}}
};
</script><style scoped>
.shopping - cart - page {padding: 20px;
}.cart - header {text - align: center;margin - bottom: 20px;
}.cart - items {list - style: none;padding: 0;
}.cart - item {display: flex;align - items: center;border: 1px solid #ccc;padding: 10px;margin - bottom: 10px;
}.cart - item - image {width: 80px;height: 80px;object - fit: cover;margin - right: 10px;
}.cart - item - info {flex: 1;
}.cart - item - name {margin - top: 0;margin - bottom: 5px;
}.cart - item - price {margin: 0;color: #666;
}.quantity - control {margin - top: 5px;
}.delete - button {background - color: #ff0000;color: white;border: none;padding: 5px 10px;border - radius: 3px;cursor: pointer;
}.cart - footer {margin - top: 20px;display: flex;justify - content: space - between;align - items: center;
}.total - price {font - weight: bold;
}.checkout - button {background - color: #007BFF;color: white;border: none;padding: 10px 20px;border - radius: 3px;cursor: pointer;
}
</style>
(四)封装getters动态计算展示
1.etters用于对store中的state进行加工处理,相当于store的计算属性。在购物车功能里,利用getters可以对购物车商品数据进行动态计算,如计算选中商品的总价、商品数量等,方便在组件中复用这些计算结果。
2.封装getters,将复杂的计算逻辑集中管理,避免在多个组件中重复编写相同的计算代码,提高代码的可维护性和复用性。当购物车数据结构或计算规则发生变化时,只需在getters中修改,所有依赖该计算结果的组件会自动更新。
3.state中的购物车商品列表数据进行计算。在组件中,通过 mapGetters 辅助函数将getters映射到组件的计算属性,然后在模板中使用这些计算属性进行数据展示。
javascriptconst cartModule = {namespaced: true,state: {cartItems: [{ id: 1, name: '商品1', price: 100, quantity: 1, isChecked: false },{ id: 2, name: '商品2', price: 200, quantity: 2, isChecked: true }]},getters: {// 计算选中商品的总价selectedTotalPrice(state) {return state.cartItems.reduce((total, item) => {if (item.isChecked) {return total + item.price * item.quantity;}return total;}, 0);},// 计算选中商品的数量selectedItemCount(state) {return state.cartItems.reduce((count, item) => {if (item.isChecked) {return count + item.quantity;}return count;}, 0);}}
};export default cartModule;
(五)全选反选
javascriptconst cartModule = {namespaced: true,state: {cartItems: [{ id: 1, name: '商品1', price: 100, quantity: 1, isChecked: false },{ id: 2, name: '商品2', price: 200, quantity: 2, isChecked: false }]},mutations: {// 全选购物车商品SELECT_ALL_ITEMS(state) {state.cartItems.forEach(item => item.isChecked = true);},// 反选购物车商品INVERT_SELECTION(state) {state.cartItems.forEach(item => item.isChecked =!item.isChecked);}},actions: {selectAllItems({ commit }) {commit('SELECT_ALL_ITEMS');},invertSelection({ commit }) {commit('INVERT_SELECTION');}}
};export default cartModule;
(六)数字框修改数量
javascriptconst cartModule = {namespaced: true,state: {cartItems: [{ id: 1, name: '商品1', price: 100, quantity: 1, isChecked: false },{ id: 2, name: '商品2', price: 200, quantity: 2, isChecked: false }]},mutations: {// 修改购物车中商品的数量UPDATE_CART_ITEM_QUANTITY(state, { itemId, newQuantity }) {const item = state.cartItems.find(i => i.id === itemId);if (item) {item.quantity = newQuantity;}}},actions: {updateCartItemQuantity({ commit }, payload) {commit('UPDATE_CART_ITEM_QUANTITY', payload);},// 重新计算总价(可在数量变化等操作后调用)recalculateTotalPrice({ state }) {// 假设总价计算逻辑const total = state.cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);// 可在这里更新总价到state中的总价变量}},getters: {// 计算购物车总价cartTotalPrice(state) {return state.cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);}}
};export default cartModule;购物车组件(假设在 components/Cart.vue )html<template><div><h1>购物车</h1><ul><li v-for="item in cartItems" :key="item.id">{{ item.name }} - {{ item.price }}元<!-- 数字框及增减按钮 --><div><button @click="decreaseQuantity(item.id)">-</button><input v-model.number="item.quantity" type="number" min="1"><button @click="increaseQuantity(item.id)">+</button></div></li></ul><p>购物车总价: {{ cartTotalPrice }}元</p></div>
</template><script>
import { mapState, mapActions, mapGetters } from 'vuex';export default {computed: {...mapState('cart', ['cartItems']),...mapGetters('cart', ['cartTotalPrice'])},methods: {...mapActions('cart', ['updateCartItemQuantity','recalculateTotalPrice']),increaseQuantity(itemId) {const item = this.cartItems.find(i => i.id === itemId);if (item) {this.updateCartItemQuantity({ itemId, newQuantity: item.quantity + 1 });this.recalculateTotalPrice();}},decreaseQuantity(itemId) {const item = this.cartItems.find(i => i.id === itemId);if (item && item.quantity > 1) {this.updateCartItemQuantity({ itemId, newQuantity: item.quantity - 1 });this.recalculateTotalPrice();}}}
};
</script>
(七)编辑、删除、空购物车处理
<template><div><h1>购物车</h1><ul><li v - for="(item, index) in cartItems" :key="index"><!-- 非编辑状态展示 --><div v - if="!item.isEditing"><span>{{ item.name }}</span><span> - 数量: {{ item.quantity }}</span><button @click="editItem(index)">编辑</button><button @click="deleteItem(index)">删除</button></div><!-- 编辑状态展示 --><div v - if="item.isEditing"><input v - model="item.name" type="text"><input v - model.number="item.quantity" type="number" min="1"><button @click="saveItem(index)">保存</button></div></li></ul><button @click="emptyCart">清空购物车</button><!-- 空购物车提示 --><div v - if="cartItems.length === 0" class="empty - cart - msg">您的购物车目前为空,<router - link to="/">去逛逛</router - link></div></div>
</template><script>
import { mapState, mapActions } from 'vuex';export default {computed: {...mapState('cart', ['cartItems'])},methods: {...mapActions('cart', ['deleteCartItem', 'emptyCart']),editItem(index) {this.cartItems[index].isEditing = true;},saveItem(index) {this.cartItems[index].isEditing = false;// 可在此处添加向服务器更新数据的逻辑},async deleteItem(index) {const confirmDelete = window.confirm('确定要删除该商品吗?');if (confirmDelete) {const itemId = this.cartItems[index].id;await this.deleteCartItem(itemId);}},async emptyCart() {const confirmEmpty = window.confirm('确定要清空购物车吗?');if (confirmEmpty) {await this.emptyCart();}}}
};
</script>// store/modules/cart.js
const cartModule = {namespaced: true,state: {cartItems: [{ id: 1, name: '商品1', quantity: 1, isEditing: false },{ id: 2, name: '商品2', quantity: 2, isEditing: false }]},mutations: {// 修改购物车商品列表SET_CART_ITEMS(state, items) {state.cartItems = items;},// 删除单个商品DELETE_CART_ITEM(state, itemId) {state.cartItems = state.cartItems.filter(item => item.id!== itemId);},// 清空购物车EMPTY_CART(state) {state.cartItems = [];}},actions: {async deleteCartItem({ commit }, itemId) {// 可在此处添加向服务器发送删除请求的逻辑commit('DELETE_CART_ITEM', itemId);},async emptyCart({ commit }) {// 可在此处添加向服务器发送清空购物车请求的逻辑commit('EMPTY_CART');}}
};export default cartModule;