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的双向数据绑定实时获取和更新数据。

  1. 发送请求获取验证码:使用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条)时,删除最后一个元素,以控制历史记录数量。

复制代码
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元素上,实现购物车列表的动态渲染。

复制代码
<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;
相关推荐
wuxuanok32 分钟前
Web后端开发-请求响应
java·开发语言·笔记·学习
诗句藏于尽头1 小时前
内网使用rustdesk搭建远程桌面详细版
笔记
蜡笔小电芯1 小时前
【C语言】指针与回调机制学习笔记
c语言·笔记·学习
丰锋ff1 小时前
瑞斯拜考研词汇课笔记
笔记
DKPT3 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
KoiHeng6 小时前
操作系统简要知识
linux·笔记
巴伦是只猫7 小时前
【机器学习笔记Ⅰ】11 多项式回归
笔记·机器学习·回归
DKPT11 小时前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
巴伦是只猫12 小时前
【机器学习笔记Ⅰ】13 正则化代价函数
人工智能·笔记·机器学习
X_StarX18 小时前
【Unity笔记02】订阅事件-自动开门
笔记·学习·unity·游戏引擎·游戏开发·大学生