当前位置: 首页 > article >正文

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;


http://www.kler.cn/a/539849.html

相关文章:

  • 算法15(力扣347)——前k个高频元素
  • Python截图轻量化工具
  • 通讯录管理小程序
  • PostgreSQL 18新特性之DML语句RETURNING增强
  • 音频进阶学习十一——离散傅里叶级数DFS
  • Http 的响应码有哪些? 分别代表的是什么?
  • verilog练习:i2c slave 模块设计
  • 链表(LinkedList) 1
  • 基于HTML5 Canvas 和 JavaScript 实现的烟花动画效果
  • 2.6 寒假训练营补题
  • oracle-函数-concat(c1,c2)
  • Linux下Gufw防火墙安装指南
  • Java入门与进阶指南
  • 小哆啦探秘《JavaScript高级程序设计》
  • 力扣动态规划-25【算法学习day.119】
  • Versal - Petalinux 2024.2(下载与安装+VD100+安装JupyterLab+SD卡分区+SDT流程)
  • 机器学习数学公式推导笔记
  • 2025清华:DeepSeek从入门到精通.pdf(附下载)
  • vscode中使用code-runner插件运行c程序语法报错code: 1
  • MyBatis的工作流程是怎样的?
  • Spring Boot Actuator使用
  • AI时代医疗大健康微服务编程提升路径和具体架构设计
  • C++11详解(四) -- 新的类功能和包装器
  • GIT创建子模块(submodule)
  • 【共享文件夹】使用Samba服务可在Ubuntu和Windows系统之间共享一个实际的文件夹
  • 告别人工检测!casaim自动化三维激光扫描