- 收货人:{{ curAddress.receiver }}
- 联系方式:{{ curAddress.contact }}
- 收货地址:{{ curAddress.fullLocation }} {{ curAddress.address }}
文件名: utils/http.ts
```js
import axios from 'axios'
// 创建axios实例
const instance = axios.create({
baseURL: `http://pcapi-xiaotuxian-front-devtest.itheima.net`,
timeout: 5000
});
// axios请求拦截器
instance.interceptors.request.use(config => {
return config;
}, e => Promise.reject(e))
// axios响应拦截器
instance.interceptors.response.use(res => res.data, e => {
return Promise.reject(e)
})
export default instance
```
## 3. 封装请求函数并测试
文件名: apps/testAPI.js
```js
import instance from '@/utils/http';
export function getCategoryAPI() {
return instance({
url: 'home/category/head'
})
}
```
# 路由整体设计
**路由设计原则:**找页面的切换方式
- 如果是整体切换,则为一级路由
- 如果是在一级路由的内部进行的内容切换,则为二级路由
## 一级路由

文件名: views/Login/index.vue
```vue
文件名: views/Layout/index.vue
```vue
一级路由: router/index.js
```
routes: [
{
name: 'layout',
path: '/',
component: Layout
},
{
name: 'login',
path: '/login',
component: Login
}
]
```
## 二级路由

文件名: views/Home/index.vue
```vue
文件名: views/Category/index.vue
```vue
文件名: styles/var.scss
```scss
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
```
文件名: vite.config.js
```js
css: {
preprocessorOptions: {
scss: {
// 自动导入scss文件
additionalData: `
@use "@/styles/element/index.scss" as *;
@use "@/styles/var.scss" as *;
`,
}
}
}
```
# 补充eslint配置
文件名: .eslintrc.cjs
```
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'vue/multi-word-component-names': 0, // 不再强制要求组件命名
},
}
```
# Layout页
## Layout页组件结构快速搭建

详情参照:**feat: layout页面静态结构结构搭建**
## 字体图标引入

阿里的字体图标库支持多种引入方式,小兔鲜项目采用的是**font-class**引用的方式
在 `index.html`文件中引入即可
```html
```
更多的引用方式参考官网:https://www.iconfont.cn/help/detail?spm=a313x.home_index.i3.28.58a33a815XEfoF&helptype=code
**font-class**引用方式,图标的使用步骤如下:
- 第一步:拷贝项目下面生成的fontclass代码:
```js
//at.alicdn.com/t/font_8d5l8fzk5b87iudi.css
```
- 第二步:挑选相应图标并获取类名,应用于页面:
```css
```
## 一级导航渲染

**功能描述**:使用后端接口渲染一级路由导航
**实现步骤**
1. 封装接口函数
2. 调用接口函数
3. v-for渲染模版
**代码如下**
文件名: apis/layout.js
```js
import instance from '@/utils/http'
export function getCategoryAPI() {
return instance({
url: 'home/category/head'
})
}
```
文件名: views/components/LayoutHeader.vue
```vue
文件名: views/components/LayoutFixed.vue
```vue
文件名: views/index.vue
```vue
文件名: views/Home/components/HomeBanner.vue
```vue
```
### 2. 获取接口数据渲染组件
#### 1)封装接口
文件名: apis/home.js
```js
import instance from '@/utils/http'
/**
* @description: 获取banner图
*/
export function getBannerAPI() {
return instance({
url: 'home/banner'
})
}
```
#### 2)获取接口数据渲染模版
文件名: views/Home/components/HomeBanner.vue
```vue
```
## 面板组件封装

### 组件封装核心思路
把可复用的的结构只写一次,把**可能发生变化的部分抽象成组件参数(props / 插槽)**

观察发现有三个地方不同:
- 主标题
- 副标题
- 主体内容(图片及其下面的文字内容)
### 实现步骤分析
- 1)先不做任何的抽象,准备静态模板
- 2)抽象可变的部分
- 主标题 和 副标题 是**纯文本**,可以抽象成**prop**传入
- 主体内容是复杂的**模板**,抽象成**插槽**传入
### 纯静态结构
文件名: views/Home/components/HomePanel.vue
```vue
文件名: views/Home/components/HomePanel.vue
```vue
文件名: views/Home/components/HomeNew.vue
```vue
```
### 2. 封装接口
文件名: apis/home.js
```js
/**
* @description: 获取新鲜好物
*/
export const findNewAPI = () => {
return instance({
url: '/home/new'
})
}
```
### 3. 获取数据渲染模版
调用接口获取数据,将数据渲染到插槽位置
完整代码如下
```vue
{{ item.name }}
¥{{ item.price }}
文件名: apis/home.js
```js
/**
* @description: 获取新鲜好物
*/
export const findNewAPI = () => {
return instance({
url: '/home/new'
})
}
```
### 2. 获取数据渲染模版
调用接口获取数据,将数据渲染到插槽位置
完整代码如下
文件名: views/Home/components/HomeHot.vue
```vue
{{ item.title }}
{{ item.alt }}
文件名: directives/index.js
```js
// 定义懒加载插件
import type {App} from "vue";
import {useIntersectionObserver} from "@vueuse/core";
export const lazyPlugin = {
install: (app: App): void => {
// 懒加载指令逻辑
app.directive('img-lazy', {
mounted(el, binding) {
// el 指定绑定哪个元素 img
// binding binding.value 指令等于后后面绑定的表达式的值 url
// console.log(el, binding.value)
const {stop} = useIntersectionObserver(
el,
([{isIntersecting}]) => {
// console.log(isIntersecting)
if (isIntersecting) {
// 进入视口区域
el.src = binding.value
stop() // 在图片第一次加载后,停止监听视口,避免内存浪费
}
},
)
}
})
}
}
```
### 2. 注册全局指令
文件名: main.js
```js
// 引入懒加载指令插件并注册
import {lazyPlugin} from '@/directives'
app.use(lazyPlugin)
```
### 3. 按需使用
在需要使用自定义指令的地方使用即可,详情参照gti提交记录**feat: 图片懒加载优化**
## Product产品列表实现

### 1. 准备静态模版
文件名: views/Home/components/HomeProduct.vue
```vue
文件名: apis/home.js
```js
// 获取所有商品模块
export const getGoodsAPI = () => instance.get('/home/goods')
```
### 3. 获取并渲染数据
文件名: views/Home/components/HomeProduct.vue
```vue
{{ good.name }}
{{ good.desc }}
¥{{ good.price }}
文件名: views/Home/components/HomeProduct.vue
```vue
{{ good.name }}
{{ good.desc }}
¥{{ good.price }}
文件名: views/Home/components/GoodsItem.vue
```vue
{{ goods.name }}
{{ goods.desc }}
¥{{ goods.price }}
文件名: views/Home/components/HomeProduct.vue
```html
文件名: views/Category/index.vue
```vue
文件名: views/components/LayoutHeader.vue
```html
文件名: views/components/LayoutFixed.vue
```html
文件名: views/Category/index.vue
```vue
文件名: apis/category.js
```js
import instance from '@/utils/http'
// 获取-二级分类列表
export const findTopCategoryAPI = (id) => {
return instance({
url: '/category',
params: {
id
}
})
}
```
### 3. 渲染面包屑导航
文件名: views/Category/index.vue
```vue
文件名: apis/home.js
```js
/**
* @description: 获取banner图
*/
export function getBannerAPI(params = {}) {
// 1为首页,2为分类商品页 默认是1
const {distributionSite = '1'} = params
return instance({
url: 'home/banner',
params: {
distributionSite
}
})
}
```
### 2. 迁移首页Banner逻辑
文件名: views/Category/components/CategoryBanner.vue
```vue
```
### 3.渲染分类轮播图
文件名: views/Category/index.vue
```vue
文件名: views/layout/LayoutHeader.vue
```vue
文件名: views/layout/LayoutFixed.vue
```vue
文件名: views/Category/index.vue
```vue
{{ i.name }}
文件名: views/subCategory/index.vue
```vue
文件名: router/index.vue
```js
import { createRouter, createWebHistory } from 'vue-router'
import Login from '@/views/Login/index.vue'
import Layout from '@/views/Layout/index.vue'
import Home from '@/views/Home/index.vue'
import Category from '@/views/Category/index.vue'
import SubCategory from '@/views/SubCategory/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
{
// '' 默认二级路由
path: '',
component: Home
},
{
name: 'category',
path: 'category/:id',
component: Category
},
{
name:'subCategory',
path: '/category/sub/:id',
component: SubCategory
},
]
},
{
name: 'login',
path: '/login',
component: Login
}
]
})
export default router
```
#### 3. 路由跳转配置
文件名: views/Category/index.vue
```vue
{{ i.name }}
文件名: apis/category.js
```js
// 获取二级分类列表数据
export const getCategoryFilterAPI = (id) => {
return instance({
url: '/category/sub/filter',
params: {
id
}
})
}
```
### 2. 获取数据渲染模版
文件名: views/subCategory/index.vue
```vue
文件名: apis/category.js
```js
/**
* @description: 获取导航数据
* @data {
* categoryId: 1005000 ,
* page: 1,
* pageSize: 20,
* sortField: 'publishTime' | 'orderNum' | 'evaluateNum'
}
* @return {*}
*/
export const getSubCategoryAPI = (data) => {
return instance({
url: '/category/goods/temporary',
method: 'POST',
data
})
}
```
#### 2. 获取数据列表并渲染
文件名: views/subCategory/index.vue
```vue
文件名: views/subCategory/index.vue
tab组件切换时修改reqData中的sortField字段,重新拉取接口列表
```vue
文件名: views/subCategory/index.vue
```vue
文件名: views/Detail/index.vue
```vue
销量人气
100+
销量人气
商品评价
200+
查看评价
收藏人气
300+
收藏商品
品牌信息
400+
品牌主页
文件名: router/index.vue
```js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
{
// '' 默认二级路由
path: '',
component: Home
},
{
name: 'category',
path: 'category/:id',
component: Category
},
{
name:'subCategory',
path: '/category/sub/:id',
component: SubCategory
},
{
name:'detail',
path: '/detail/:id',
component: ProductDetail
}
]
},
{
name: 'login',
path: '/login',
component: Login
}
],
scrollBehavior (){
return {
top: 0
}
}
})
export default router
```
#### 3. 绑定模版测试跳转
文件名: views/home/components/HomeNew.vue
```vue
{{ item.name }}
¥{{ item.price }}
文件名: apis/detail.js
```js
import request from '@/utils/http'
// 获取商品详情
export const getDetailAPI = (id: number) => {
return request({
url: '/goods',
params: {
id
}
})
}
```
### 2. 获取数据渲染模版
文件名: views/Detail/index.vue
```vue
销量人气
{{ good.salesCount }}+
销量人气
商品评价
{{ good.commentCount }}+
查看评价
收藏人气
{{ good.collectCount }}+
收藏商品
品牌信息
{{ good.brand?.name }}
品牌主页
{{ good.name }}
{{ good.desc }}
{{ good.oldPrice }} {{ good.price }}
文件名: view/Detail/components/DetailHot.vue
```vue
一双男鞋
一双好穿的男鞋
¥200.00
文件名: apis/detail.js
```js
/**
* 获取热榜商品
* @param {Number} id - 商品id
* @param {Number} type - 1代表24小时热销榜 2代表周热销榜
* @param {Number} limit - 获取个数
*/
export const getHotGoodsAPI = ({ id, type, limit = 3 }) => {
return request({
url: '/goods/hot',
params: {
id,
type,
limit
}
})
}
```
#### 3 获取基础数据渲染模版
文件名: view/Detail/components/DetailHot.vue
```vue
{{item.name }}
{{ item.desc }}
¥{{ item.price }}
文件名: view/Detail/components/DetailHot.vue
```js
// 设计props参数,适配不同的title和数据
const props = defineProps({
hotType: {
type: Number, // 1代表24小时热销榜 2代表周热销榜
},
});
// 适配title // 1代表24小时热销榜 2代表周热销榜
const TYPEMAP = {
1: "24小时热榜",
2: "周热榜",
};
const title = computed(() => TYPEMAP[props.hotType]);
{{ item.name }}
{{ item.desc }}
¥{{ item.price }}
文件名: views/Detail/index.vue
```vue
文件名: view/Detail/components/DetailHot.vue
```js
const getHotGoods = async () => {
const res = await getHotGoodsAPI({
id: route.params.id,
type: props.hotType,
});
// console.log(res.result);
hotList.value = res.result;
};
```
## 图片预览组件封装

### 1. 小图切换大图显示

**核心思路:**
- 准备组件静态模板
- 为小图榜单事件,记录当前激活的下标值
- 通过下标切换大图显示
- 通过下表实现激活状态显示
#### 1)准备模版
文件名:components/ImageView/index.vue
```vue
文件名:components/ImageView/index.vue
```vue
文件名:components/ImageView/index.vue
```vue
文件名:components/ImageView/index.vue
```vue
文件名:components/ImageView/index.vue
```vue
文件名:components/ImageView/index.vue
```js
// props适配图片列表
defineProps({
imageList: {
type: Array,
default: () => [],
},
});
```
文件名:views/Detail/index.vue
```html
文件名:views/Detail/index.vue
```vue
文件名:components/index.js
```js
// 把components中的所组件都进行全局化注册
// 通过插件的方式
import ImageView from './ImageView/index.vue'
import XtxSku from './ImageView/index.vue'
export const componentPlugin = {
install(app) {
// app.component('组件名字',组件配置对象)
app.component('XtxImageView', ImageView)
app.component('XtxSku', XtxSku)
}
}
```
### 2. 插件注册
文件名:main.js
```js
// 引入全局组件插件
import { componentPlugin } from '@/components'
app.use(componentPlugin)
```
# 登录页
## 整体认识和路由配置
### 整体认识

### 路由配置
#### 1. 准备模板
文件名:views/Login/index.vue
```vue
文件名:router/index.js
```js
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
{
// '' 默认二级路由
path: '',
component: Home
},
{
name: 'category',
path: 'category/:id',
component: Category
},
{
name:'subCategory',
path: '/category/sub/:id',
component: SubCategory
},
{
name:'detail',
path: '/detail/:id',
component: ProductDetail
}
]
},
{
name: 'login',
path: '/login',
component: Login
}
]
```
#### 3. 配置路由跳转
文件名:views/Layout/components/LayoutNav.vue
```vue
文件名:views/Login/index.vue
```vue
文件名:views/Login/index.vue
```vue
文件名:views/Login/index.vue
```vue
文件名:apis/user.js
```js
// 封装所有与用户相关的接口
import request from '@/utils/http'
export const loginAPI = ({ account, password }) => {
return request({
url: '/login',
method: 'POST',
data: {
account,
password
}
})
}
```
### 2. 实现主逻辑
文件名:views/Login/index.vue
```vue
```
### 3. 拦截器统一处理错误信息
文件名:utils/http.js
```js
// 按需导入的方式需要手动导入样式
import { ElMessage } from 'element-plus'
import 'element-plus/theme-chalk/el-message.css'
// axios响应拦截器
instance.interceptors.response.use(res => res.data, e => {
// 错误提示统一处理
ElMessage({
type: 'warning',
message: e.response.data.message
})
return Promise.reject(e)
})
```
## Pinia管理用户数据


### 1.定义useUserStore
文件名:stores/user.js
```js
import { defineStore } from "pinia";
import { ref } from "vue";
import { loginAPI } from '@/apis/user'
// 官方推荐使用hooks的那种命名规范
/**
* 参数一:id (建议与文件名一致)
* 参数二:options 配置对象
*/
// 组合式写法
export const useUserStore = defineStore('user', () => {
// 1.定义管理用户数据的state
const userInfo = ref({})
// 2.定义获取接口数据的action函数
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password })
// console.log(res)
userInfo.value = res.result
}
// 3.以对象的形式把state和action返回
return { userInfo, getUserInfo }
})
```
### 2. 登录组件中使用
文件名:views/Login/index.vue
```vue
```
### 3. Pinia用户数据持久化

`pinia-plugin-persistedstate`参考: https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/
1、安装插件
```shell
npm i pinia-plugin-persistedstate
```
2、将插件添加到 pinia 实例上
文件名:main.js
```js
import {createPinia} from 'pinia'
// 将插件添加到 pinia 实例上
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
```
3、使用
创建 Store 时,将 `persist` 选项设置为 `true`。
文件名:stores/user.js
```js
import { defineStore } from "pinia";
import { ref } from "vue";
import { loginAPI } from '@/apis/user'
// 组合式写法
export const useUserStore = defineStore('user', () => {
// 1.定义管理用户数据的state
const userInfo = ref({})
// 2.定义获取接口数据的action函数
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password })
// console.log(res)
userInfo.value = res.result
}
// 3.以对象的形式把state和action返回
return { userInfo, getUserInfo }
},
{
persist: true // store持久化配置
}
)
```
## 登录与非登录状态的模板匹配


文件名:views/Layout/components/LayoutNav.vue
```vue
```
## 请求拦截器携带token

文件名:utils/http.js
```js
import { useUserStore } from '@/stores/user';
// axios请求拦截器
instance.interceptors.request.use(config => {
// 1.从pinia中获取token
const userStore = useUserStore()
const token = userStore.userInfo.token
// 2.按照后端的要求进行token拼接处理
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config;
}, e => Promise.reject(e))
```
## 退出登录实现

**核心思路:**
- 点击退出登录弹出确认框
- 点击确认按钮实现退出登录逻辑
- 退出登录逻辑包括
- 1、清理当前用户信息(store、localStorage)
- 2、跳转到登录页面
el-popconfirm 组件的使用参考elementPlus官方:
文件名:views/Layout/components/LayoutNav.vue
```vue
文件名:stores/user.js
```js
// 组合式写法
export const useUserStore = defineStore('user', () => {
// 1.定义管理用户数据的state
const userInfo = ref({})
// 2.定义获取接口数据的action函数
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password })
// console.log(res)
userInfo.value = res.result
}
// 清除用户信息:会同步清空local Storage(由pinia-plugin-persistedstate插件实现)
const clearUserInfo = () => {
userInfo.value = {}
}
// 3.以对象的形式把state和action返回
return { userInfo, getUserInfo, clearUserInfo }
},
{
persist: true // store持久化配置
}
)
```
## Token失效401实现

文件名:utils/http.js
```js
import { useUserStore } from '@/stores/user';
import router from '@/router';
// axios响应拦截器
instance.interceptors.response.use(res => res.data, e => {
// 错误提示统一处理
ElMessage({
type: 'warning',
message: e.response.data.message
})
// 401 token失效处理
// 2.跳转到登录页
// 1.清除本地用户数据(pinia与local storage)
if (e.response.status === 401) {
const userStore = useUserStore()
userStore.clearUserInfo()
router.replace('/login')
}
return Promise.reject(e)
})
```
# 购物车

## 本地购物车
### 加入购物车实现

文件名:stores/cart.js
```js
// 封装购物车模块
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCartStore = defineStore('cart', () => {
// 定义state
const cartList = ref([])
// 定义action
// 加入购物车
const addCart = (good) => {
console.log('加入购物车action执行')
// 添加入购物车
// 已添加过:count + 1
// 没有添加过:直接push
// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
const item = cartList.value.find((item) => good.skuId === item.skuId)
if (item) {
// 找到啦
item.count++
} else {
// 没找到
cartList.value.push(good)
}
}
return { cartList, addCart }
},
{
persist: true // store持久化配置
}
)
```
文件名:views/Detail/index.vue
```vue
文件名:views/Layout/components/HeaderCart.vue
```vue
共 10 件商品
¥ 100.00
文件名:views/Layout/components/LayoutHeader.vue
```vue
文件名:views/Layout/components/HeaderCart.vue
```vue
{{ i.name }}
{{ i.attrsText }}
¥{{ i.price }}
x{{ i.count }}
文件名:stores/cart.js
```js
export const useCartStore = defineStore('cart', () => {
// 定义state
const cartList = ref([])
// 删除购物车
const delCart = (skuId) => {
console.log('删除购物车action执行')
// 方法一:找到要删除项的下标值 splice
// 方法二:使用数组的过滤方法 filter
const idx = cartList.value.findIndex((item) => item.skuId === skuId)
cartList.value.splice(idx, 1)
}
return { cartList, addCart, delCart }
}
)
```
文件名:views/Layout/components/HeaderCart.vue
```vue
```
#### 头部购物车统计计算

文件名:stores/cart.js
```js
export const useCartStore = defineStore('cart', () => {
// 定义state
const cartList = ref([])
// getters 计算属性
// 1.总数量 所有项的 count 之和
const allCount = computed(() => {
return cartList.value.reduce((x, y) => x + y.count, 0)
})
// 2.总价格 所有项的 count * price
const allPrice = computed(() => {
return cartList.value.reduce((x, y) => x + y.count * y.price, 0)
})
return { cartList, allCount, allPrice, addCart, delCart }
}
)
```
文件名:views/Layout/components/HeaderCart.vue
```vue
共 {{ cartStore.allCount }} 件商品
¥ {{ cartStore.allPrice.toFixed(2) }}
文件名:views/CartList/index.vue
```vue
|
商品信息 | 单价 | 数量 | 小计 | 操作 |
---|---|---|---|---|---|
|
{{ i.name }} |
¥{{ i.price }} |
|
¥{{ (i.price * i.count).toFixed(2) }} |
|
|
文件名:router/index.js
```js
import CartList from '@/views/CartList/index.vue'
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
{
// '' 默认二级路由
path: '',
component: Home
},
{
name: 'cartList',
path: 'cartList',
component: CartList
}
]
},
]
```
##### 绑定路由
文件名:views/Layout/components/HeaderCart.vue
```vue
文件名:views/CartList/index.vue
```vue
文件名:stores/cart.js
```js
export const useCartStore = defineStore('cart', () => {
// 定义state
const cartList = ref([])
// 单选功能
const singleCheck = (skuId, selected) => {
// 根据skuId找到要修改的项,修改其selected
const item = cartList.value.find((item) => item.skuId === skuId)
item.selected = selected
};
return { cartList, allCount, allPrice, addCart, delCart, singleCheck }
}
)
```
文件名:views/CartList/index.vue
```vue
文件名:stores/cart.js
```js
export const useCartStore = defineStore('cart', () => {
// 定义state
const cartList = ref([])
// 全选功能
const allCheck = (selected) => {
// 把cartList中所有项的selected都设置为当前的全选框状态
cartList.value.forEach(item => item.selected = selected)
};
// 是否全选
const isAll = computed(() => {
return cartList.value.every((item) => item.selected)
})
return { cartList, allCount, allPrice, isAll, addCart, delCart, singleCheck, allCheck }
)
```
文件名:views/CartList/index.vue
```vue
文件名:stores/cart.js
```js
export const useCartStore = defineStore('cart', () => {
// 定义state
const cartList = ref([])
// 3.选中数量
const selectedCount = computed(() => {
return cartList.value
.filter(item => item.selected)
.reduce((x, y) => x + y.count, 0)
})
// 4.选中商品总价格
const selectedPrice = computed(() => {
return cartList.value
.filter(item => item.selected)
.reduce((x, y) => x + y.count * y.price, 0)
})
return { cartList, allCount, allPrice, isAll, selectedCount, selectedPrice, addCart, delCart, singleCheck, allCheck }
}
)
```
文件名:views/CartList/index.vue
```vue
文件名:apis/cart.js
```js
// 封装购物车相关接口
import request from '@/utils/http'
// 加入购物车
export const insertCartAPI = ({ skuId, count }) => {
return request({
url: '/member/cart',
method: 'POST',
data: {
skuId,
count
}
})
}
// 获取最新购物车列表
export const findNewCartListAPI = () => {
return request({
url: '/member/cart',
})
}
```
#### 2.加入购物车实现
文件名:stores/cart.js
```js
// 在cartStore中使用userStore
import { useUserStore } from './user'
import { insertCartAPI, findNewCartListAPI } from '@/apis/cart'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const isLogin = computed(() => userStore.userInfo.token)
// 定义state
const cartList = ref([])
// 定义action
// 加入购物车
const addCart = async (good) => {
// console.log('加入购物车action执行')
if (isLogin) {
// 登录之后的加入购物车逻辑
const { skuId, count } = good
await insertCartAPI({ skuId, count })
const res = await findNewCartListAPI()
cartList.value = res.result
} else {
// 本地购物车添加入购物车逻辑
// 已添加过:count + 1
// 没有添加过:直接push
// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
const item = cartList.value.find((item) => good.skuId === item.skuId)
if (item) {
// 找到啦
item.count++
} else {
// 没找到
cartList.value.push(good)
}
}
}
}
)
```
### 删除购物车

#### 1.封装购物车接口
文件名:apis/cart.js
```js
// 删除购物车
export const delCartAPI = (ids) => {
return request({
url: '/member/cart',
method: 'DELETE',
data: {
ids
}
})
}
```
#### 2.加入购物车实现
文件名:stores/cart.js
```js
import { insertCartAPI, findNewCartListAPI, delCartAPI } from '@/apis/cart'
// 删除购物车
const delCart = async (skuId) => {
// console.log('删除购物车action执行')
if (isLogin) {
await delCartAPI([skuId])
// 获取最新购物车
const res = await findNewCartListAPI()
cartList.value = res.result
} else {
// 方法一:找到要删除项的下标值 splice
// 方法二:使用数组的过滤方法 filter
const idx = cartList.value.findIndex((item) => item.skuId === skuId)
cartList.value.splice(idx, 1)
}
}
```
### 清空购物车

文件名:stores/cart.js
```js
export const useCartStore = defineStore('cart', () => {
// 清除购物车
const clearCart = () => {
cartList.value = []
}
}
)
```
文件名:stores/user.js
```js
// 在userStore中使用CartStore
import { useCartStore } from "@/stores/cart";
// 组合式写法
export const useUserStore = defineStore('user', () => {
const cartStore = useCartStore()
// 清除用户信息:会同步清空local Storage(由pinia-plugin-persistedstate插件实现)
const clearUserInfo = () => {
userInfo.value = {}
// 清空用户购物车
cartStore.clearCart()
}
}
)
```
## 合并本地购物车到服务器

### 1.封装接口
文件名:apis/cart.js
```js
// 合并购物车
export const mergeCartAPI = (data) => {
return request({
url: '/member/cart/merge',
method: 'POST',
data
})
}
```
### 2.合并本地购物车到服务器实现
在用户登录时,进行合并购物车操作
```js
import { mergeCartAPI } from "@/apis/cart";
export const useUserStore = defineStore('user', () => {
const cartStore = useCartStore()
// 1.定义管理用户数据的state
const userInfo = ref({})
// 2.定义获取接口数据的action函数
const getUserInfo = async ({ account, password }) => {
const res = await loginAPI({ account, password })
// console.log(res)
userInfo.value = res.result
// 合并购物车操作
const data = cartStore.cartList.map(item => {
return {
skuId: item.skuId,
selected: item.selected,
count: item.count
}
})
await mergeCartAPI(data)
// 获取最新购物车列表
cartStore.updateNewCartList()
}
}
)
```
# 订单页 结算页
## 路由配置和基础数据渲染
### 路由配置

#### 1. 准备组件模版
文件名:views/Checkout/index.vue
```vue
商品信息 | 单价 | 数量 | 小计 | 实付 |
---|---|---|---|---|
{{ i.name }} {{ i.attrsText }} |
¥{{ i.price }} | {{ i.price }} | ¥{{ i.totalPrice }} | ¥{{ i.totalPayPrice }} |
文件名:router/index.js
```js
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
{
// '' 默认二级路由
path: '',
component: Home
},
{
name: 'category',
path: 'category/:id',
component: Category
},
{
name:'subCategory',
path: '/category/sub/:id',
component: SubCategory
},
{
name:'detail',
path: '/detail/:id',
component: ProductDetail
},
{
name: 'cartList',
path: 'cartList',
component: CartList
},
{
name: 'checkout',
path: 'checkout',
component: Checkout
}
]
},
{
// 登录页
name: 'login',
path: '/login',
component: Login
}
]
})
```
#### 3. 配置路由跳转
文件名:views/CartList/index.vue
```vue
文件名:apis/checkout.js
```js
import request from '@/utils/http'
// 获取详情
export const getCheckInfoAPI = () => {
return request({
url: '/member/order/pre'
})
}
```
#### 2. 获取数据
文件名:views/Checkout/index.vue
```vue
```
#### 3. 默认地址和商品列表渲染
文件名:views/Checkout/index.vue
```vue
商品信息 | 单价 | 数量 | 小计 | 实付 |
---|---|---|---|---|
{{ i.name }} {{ i.attrsText }} |
¥{{ i.price }} | {{ i.price }} | ¥{{ i.totalPrice }} | ¥{{ i.totalPayPrice }} |
文件名:views/Checkout/index.vue
```vue
文件名:views/Checkout/index.vue
```vue
文件名:views/Checkout/index.vue
```vue
文件名:views/Pay/index.vue
```vue
文件名:router/index.js
```js
import Pay from '@/views/Pay/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
{
// '' 默认二级路由
path: '',
component: Home
},
// 支付页
{
name: 'pay',
path: 'pay',
component: Pay
}
]
},
]
})
```
### 封装生成订单接口
文件名:apis/checkout.js
```js
// 创建订单
export const createOrderAPI = (data) => {
return request({
url: '/member/order',
method: 'POST',
data
})
}
```
### 提交订单(调用接口携带id跳转路由)
点击`提交订单`按钮会做如下两件事情:
- 1、调用生成订单接口,得到订单id
- 2、携带订单id,完成路由跳转
文件名:views/Checkout/index.vue
```vue
文件名:views/Checkout/index.vue
```vue
```
# 支付页
## 基础数据渲染

**核心思路:**
- 封装获取订单详情的接口
- 获取关键数据并渲染
### 1. 封装接口
文件名:apis/pay.js
```js
import request from '@/utils/http'
export const getOrderAPI = (id) => {
return request({
url: `/member/order/${id}`
})
}
```
### 2. 获取数据渲染内容
文件名:views/Pay/index.vue
```vue
```
## 支付功能实现

### 1. 支付携带参数
文件名:views/Pay/index.vue
```vue
```
### 2. 支付宝沙箱账号信息
| 账号 | fukuvb7569@sandbox.com |
| -------- | ---------------------- |
| 登录密码 | 111111 |
| 支付密码 | 111111 |
## 支付结果页展示

### 1.准备模板组件
文件名:views/Pay/components/PayBack.vue
```vue
支付成功
我们将尽快为您发货,收货期间请保持手机畅通
支付方式:支付宝
支付金额:¥200.00
温馨提示:小兔鲜儿不会以订单异常、系统升级为由要求您点击任何网址链接进行退款操作,保护资产、谨慎操作。
文件名:router/index.js
```js
import PayBack from '@/views/Pay/components/PayBack.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
// 支付页
{
name: 'pay',
path: 'pay',
component: Pay
},
// 支付结果跳转页
{
name: 'payBack',
path: 'paycallback',
component: PayBack
}
]
},
]
})
```
### 3、根据支付结果适配支付状态
文件名:views/Pay/components/PayBack.vue
```vue
支付{{ $route.query.payResult === "true" ? "成功" : "失败" }}
``` ### 4、获取订单数据渲染支付信息文件名:views/Pay/components/PayBack.vue
```vue
支付金额:¥{{ orderInfo.payMoney?.toFixed(2) }}
``` ## 封装倒计时函数  **核心思路:** - 编写函数框架,确认函数的入参、返回值 - 编写核心倒计时逻辑实现基础倒计时功能 - 实现格式化文件名:composables/useCountDown.js
```js
// 封装倒计时逻辑函数
import { computed, onUnmounted, ref } from "vue"
import dayjs from "dayjs" // npm i dayjs
export const useCountDown = () => {
let timer = null
// 1.响应式数据
const time = ref(0)
// 格式化时间 xx分xx秒
const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
// 2.开启倒计时函数
const start = (currentTime) => {
// 开始倒计时核心逻辑
// 逻辑: 每隔1s减1
time.value = currentTime
// 定时器
timer = setInterval(() => {
time.value--
}, 1000)
}
// 组件卸载时清除定时器
onUnmounted(() => {
timer && clearInterval(timer)
})
return {
formatTime,
start
}
}
```
# 会员中心
## 整体功能梳理

## 路由配置

### 1. 准备个人中心组件模版
文件名:views/Member/index.vue
```vue
文件名:router/index.js
```js
import Member from '@/views/Member/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
// 会员中心 个人中心
{
name: 'member',
path: 'member',
component: Member
}
]
},
})
```
### 3. 准备个人中心组件、我的订单组件模板
文件名:views/Member/components/UserInfo.vue
```vue
文件名:views/Member/components/UserOrder.vue
```vue
文件名:router/index.js
```js
import MemberInfo from '@/views/Member/components/UserInfo.vue'
import MemberOrder from '@/views/Member/components/UserOrder.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
// 会员中心 个人中心
{
name: 'member',
path: 'member',
component: Member,
children: [
{
path: '',
component: MemberInfo
},
{
path: 'order',
component: MemberOrder
}
]
}
]
}
]
})
```
## 个人中心信息渲染

### 1. 使用Pinia数据渲染个人信息
文件名:views/Member/components/UserInfo.vue
```vue
```
### 2. 封装猜你喜欢接口
文件名:apis/user.js
```js
export const getLikeListAPI = ({ limit = 4 }) => {
return request({
url: '/goods/relevant',
params: {
limit
}
})
}
```
### 3. 渲染猜你喜欢数据
文件名:views/Member/components/UserInfo.vue
```vue
文件名:apis/order.js
```js
import request from '@/utils/http'
/*
params: {
orderState:0,
page:1,
pageSize:2
}
*/
export const getUserOrder = (params) => {
return request({
url: '/member/order',
method: 'GET',
params
})
}
```
文件名:views/Member/components/UserOrder.vue
```vue
```
### 2. tab切换实现

文件名:views/Member/components/UserOrder.vue
```vue
文件名:views/Member/components/UserOrder.vue
```vue
文件名:router/index.js
```js
import MemberInfo from '@/views/Member/components/UserInfo.vue'
import MemberOrder from '@/views/Member/components/UserOrder.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
// 路由规则:path和component对应关系
routes: [
{
name: 'layout',
path: '/',
component: Layout,
children: [
// 会员中心 个人中心
{
name: 'member',
path: 'member',
component: Member,
children: [
{
path: '', // 默认
component: MemberInfo
},
{
path: 'order',
component: MemberOrder
}
]
}
]
}
]
})
```
文件名:views/Member/index.vue
```vue
文件名:views/Member/components/UserOrder.vue
```vue
{{ formatPayState(order.orderState)}}
```