Vue笔记(六)
一、路由设计配置--一级路由配置
在路由文件(一般是 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
javascript
import axios from 'axios';
import Vue from 'vue';
// 创建Axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // 根据实际情况配置基础URL
timeout: 5000 // 请求超时时间
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 在请求发送前,设置loading状态为true,可在Vuex或组件data中定义loading状态
Vue.$store.commit('SET_LOADING', true); // 假设在Vuex中管理loading状态,需先配置好Vuex
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
// 请求成功后,设置loading状态为false
Vue.$store.commit('SET_LOADING', false);
return response;
},
error => {
// 请求失败时,同样设置loading状态为false
Vue.$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 ,路由切换会被阻塞。
javascript
import 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中获取用户登录状态,需提前配置好Vuex
const 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> 标签内)
javascript
export 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>
<input
v - 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映射到组件的计算属性,然后在模板中使用这些计算属性进行数据展示。
javascript
const 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;
(五)全选反选
javascript
const 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;
(六)数字框修改数量
javascript
const 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;