2018年网站优化怎么做兰州seo网站排名
web/
2025/10/7 21:05:57/
文章来源:
2018年网站优化怎么做,兰州seo网站排名,建设直播网站软件,报告网站开发环境前端学习笔记 7#xff1a;小兔鲜
准备工作
创建项目
创建项目#xff1a;
npm init vuelatest相关选项如下#xff1a; 在src目录下添加以下目录#xff1a; 别名路径联想
默认情况下在 VSCode 中输入import xxx from ...时不会启用路径联想功能#xff0c;要启用需…前端学习笔记 7小兔鲜
准备工作
创建项目
创建项目
npm init vuelatest相关选项如下 在src目录下添加以下目录 别名路径联想
默认情况下在 VSCode 中输入import xxx from ...时不会启用路径联想功能要启用需要在项目根目录下添加 VSCode 配置文件jsconfig.json
{compilerOptions : {baseUrl : ./,paths : {/*:[src/*]}}
}如果 VSCode 已经自动创建该文件可以跳过这一步。 添加 ElementPlus
ElementPlus 加入的方式分为全部引入和按需引入后者可以减少项目打包后的体积所以这里采用按需引入。
安装 ElementPlus
npm install element-plus --save安装插件
npm install -D unplugin-vue-components unplugin-auto-import修改vite.config.js添加以下内容
// vite.config.ts
import AutoImport from unplugin-auto-import/vite
import Components from unplugin-vue-components/vite
import { ElementPlusResolver } from unplugin-vue-components/resolversexport default defineConfig({// ...plugins: [// ...AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],
})修改App.vue进行验证
script setup
/scripttemplateel-button typeprimaryPrimary/el-button
/template定制主题色
安装 sass
npm i sass -D添加主题色样式文件styles/element/index.scss
/* 只需要重写你需要的即可 */
forward element-plus/theme-chalk/src/common/var.scss with ($colors: (primary: (// 主色base: #27ba9b,),success: (// 成功色base: #1dc779,),warning: (// 警告色base: #ffb302,),danger: (// 危险色base: #e26237,),error: (// 错误色base: #cf4444,),)
)修改vite.config.js
export default defineConfig({plugins: [// ...Components({resolvers: [ElementPlusResolver({ importStyle: sass })],}),],// ...css: {preprocessorOptions: {scss: {// 自动导入定制化样式文件进行样式覆盖additionalData: use /styles/element/index.scss as *;,}}}
})Axios 基础配置
最好在框架代码中创建 Axios 实例并进行统一配置这样可以对所有接口调用都要用的配置信息进行统一管理。
安装
npm i axios添加utils/http.js
import axios from axios// 创建axios实例
const http axios.create({baseURL: http://pcapi-xiaotuxian-front-devtest.itheima.net,timeout: 5000
})// axios请求拦截器
http.interceptors.request.use(config {return config
}, e Promise.reject(e))// axios响应式拦截器
http.interceptors.response.use(res res.data, e {return Promise.reject(e)
})export default http添加测试代码apis/test.js
import http from /utils/httpexport const getCategoryService () {return http.get(home/category/head)
}在 App.vue中进行测试
import { getCategoryService } from /apis/test
getCategoryService().then((res) {console.log(res)
})路由设计
添加views/layout/index.vue作为首页
template首页
/template依次添加
views/login/index.vue登录页views/home/index.vueHome页views/category/index.vue分类页
eslint 会报错提示文件命名不符合标准可以修改.eslintrc.cjs关闭报错
module.exports {// ...rules: {vue/multi-word-component-names: off}
}修改路由配置router/index.js
import { createRouter, createWebHistory } from vue-router
import LayoutVue from /views/layout/index.vue
import LoginVue from /views/login/index.vue
import HomeVue from /views/home/index.vue
import CategoryVue from /views/category/index.vueconst router createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: /,component: LayoutVue,children: [{ path: , component: HomeVue },{ path: /category, component: CategoryVue }]},{ path: /login, component: LoginVue }]
})export default router值得注意的是代表 Home 页的子路由 path 设置为空字符串这样可以让/路径默认展示 Home 页。
修改App.vue添加路由出口
templateRouterView /
/template修改views/layout/index.vue添加路由出口
template首页RouterView /
/template现在项目的路由是
/Home 页/category分类页/login登录页
引入静态资源和样式
将图片相关资源 images 添加到assets目录下将样式文件common.scss添加到styles目录下。
修改 main.js导入样式
// import ./assets/main.css
import /styles/common.scss为了方便查看错误提示信息可以添加插件 sass 自动导入
添加一个存放颜色相关变量的 sass 文件styles/var.scss
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;修改 vite.config.js
css: {preprocessorOptions: {scss: {// 自动导入scss文件additionalData: use /styles/element/index.scss as *;use /styles/var.scss as *;,}}
}测试修改App.vue
templatediv classtestHello World!/divRouterView /
/template
style scoped langscss
.test{color: $helpColor;
}
/styleLayout
页面搭建
在vies/layout中添加以下视图LayoutNav.vue、LayoutHeader.vue、LayoutFooter.vue。
修改views/layout/index.vue使用这些视图填充页面
script setup
import LayoutNav from ./components/LayoutNav.vue
import LayoutHeader from ./components/LayoutHeader.vue
import LayoutFooter from ./components/LayoutFooter.vue
/scripttemplateLayoutNav /LayoutHeader /RouterView /LayoutFooter /
/template字体图标引入
修改根目录下的index.html添加 link relstylesheet href//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css这里使用的是阿里的素材库具体的使用方式可以参考这个视频。 一级导航渲染
封装接口调用添加apis/layout.js
import http from ../utils/http;export const getCategorysService (){return http.get(/home/category/head)
}调用接口将返回值填充进响应式数据用响应式数据完成页面渲染。
修改LayoutHeader.vue
script setup
import { getCategorysService } from /apis/layout.js;
import {ref} from vue
const categorys ref([])
const getCategorys async (){const result await getCategorysService()categorys.value result.result
}
getCategorys()
/script
templateli v-forcat in categorys :keycat.id RouterLink to/{{ cat.name }}/RouterLink /li
/template吸顶导航栏
添加views/layout/component/LayoutFixed.vue。
在 views/layout/index.vue中引入
script setup
import LayoutNav from ./components/LayoutNav.vue
import LayoutHeader from ./components/LayoutHeader.vue
import LayoutFooter from ./components/LayoutFooter.vue
import LayoutFixed from /views/layout/components/LayoutFixed.vue
/scripttemplateLayoutFixed /LayoutNav /LayoutHeader /RouterView /LayoutFooter /
/template吸顶导航栏中用show类别控制是否显示
div classapp-header-sticky show需要知道鼠标在y轴的滚动距离这里用一个函数库 vueuse 获取。
安装
npm i vueuse/core使用函数获取滚动距离
script setup
import { useWindowScroll } from vueuse/coreconst { y } useWindowScroll()
/scriptdiv classapp-header-sticky :class{ show: y 78 }Pinia 优化重复请求
吸顶导航与普通的导航栏使用相同的商品分类数据为了避免重复请求接口可以使用 Pinia 存储数据。
创建分类的数据存储
import { defineStore } from pinia
import { ref } from vue
import { getCategorysService } from /apis/layout
export const useCategoryStore defineStore(category, () {const categorys ref([])const loadCategorys async () {const result await getCategorysService()categorys.value result.result}return { categorys, loadCategorys }
})在吸顶导航和普通导航共同的父组件layout/index.vue中触发 Store 的 action 以加载分类数据
script setup
// ...
import { useCategoryStore } from /stores/category
const categoryStore useCategoryStore()
categoryStore.loadCategorys()
/script在固定导航栏中使用数据填充导航栏
script setup
import { useWindowScroll } from vueuse/core
import {useCategoryStore} from /stores/category
const categoryStore useCategoryStore()
const { y } useWindowScroll()
/scripttemplatediv classapp-header-sticky :class{ show: y 78 }div classcontainerRouterLink classlogo to/ /!-- 导航区域 --ul classapp-header-nav li classhomeRouterLink to/首页/RouterLink/lili v-forcat in categoryStore.categorys :keycat.idRouterLink to/{{ cat.name }}/RouterLink/li/uldiv classrightRouterLink to/品牌/RouterLinkRouterLink to/专题/RouterLink/div/div/div
/template普通导航栏中的使用方式是相同的这里不再赘述。
Home
整体结构拆分
将 Home 页拆分成以下几部分
script setup
import HomeBannerVue from ./components/HomeBanner.vue
import HomeCategoryVue from ./components/HomeCategory.vue
import HomeHotVue from ./components/HomeHot.vue
import HomeNewVue from ./components/HomeNew.vue
import HomeProductVue from ./components/HomeProduct.vue
/script
templatediv classcontainerHomeCategoryVue /HomeBannerVue //divHomeNewVue /HomeHotVue /HomeProductVue /
/template分类
分类组件的基本实现见这里。
所依赖的数据可以从 Pinia 中的分类信息获取
script setup
import { useCategoryStore } from /stores/category
const categoryStore useCategoryStore()
/scripttemplatediv classhome-categoryul classmenuli v-forcat in categoryStore.categorys :keycat.idRouterLink to/{{ cat.name }}/RouterLinkRouterLink v-forchild in cat.children.slice(0, 2) :keychild.id to/{{ child.name }}/RouterLink!-- 弹层layer位置 --div classlayerh4分类推荐 small根据您的购买或浏览记录推荐/small/h4ulli v-forgood in cat.goods :keygood.idRouterLink to/img alt :srcgood.picture/div classinfop classname ellipsis-2{{ good.name }}/pp classdesc ellipsis{{ good.desc }}/pp classpricei¥/i{{ good.price }}/p/div/RouterLink/li/ul/div/li/ul/div
/template轮播图
基本实现代码可以从这里获取。
封装接口
import http from /utils/httpexport const getHomeBannerService (){return http.get(/home/banner)
}加载数据
script setup
import { getHomeBannerService } from /apis/home;
import { ref } from vue;
const banner ref([])
const loadHomeBanner async (){const result await getHomeBannerService()banner.value result.result
}
loadHomeBanner()
/scripttemplatediv classhome-bannerel-carousel height500pxel-carousel-item v-foritem in banner :keyitem.idimg :srcitem.imgUrl alt/el-carousel-item/el-carousel/div
/template面板组件封装
面板组件HomePannel.vue的基本实现可以从这里获取。
将简单信息封装成 props属性将复杂信息封装成 slot插槽
script setup
defineProps({title: {type: String},subTitle: {type: String}
})
/scripttemplatediv classhome-paneldiv classcontainerdiv classhead!-- 主标题和副标题 --h3{{ title }}small{{ subTitle }}/small/h3/div!-- 主体内容区域 --slot/slot/div/div
/template测试
HomePannelVue title新鲜好物 subTitle更多商品新鲜好物
/HomePannelVue
HomePannelVue title热销商品 subTitle更多商品热销商品
/HomePannelVue新鲜好物
新鲜好物页面HomeNew.vue的基本实现见这里。
封装接口
//新鲜好物
export const getNewService (){return http.get(/home/new)
}从接口获取数据渲染页面
script setup
import { getNewService } from /apis/home
import { ref } from vue
import HomePannelVue from ./HomePannel.vue;
const newGoods ref([])
const loadNewGoods async () {const result await getNewService()newGoods.value result.result
}
loadNewGoods()
/scripttemplateHomePannelVue title新鲜好物 subTitle新鲜出炉 品质靠谱ul classgoods-listli v-forgood in newGoods :keygood.idRouterLink to/img :srcgood.picture alt /p classname{{ good.name }}/pp classpriceyen;{{ good.price }}/p/RouterLink/li/ul/HomePannelVue
/template图片懒加载
需要实现一个自定义指令v-img-lazy。
修改main.js
// import ./assets/main.css
import /styles/common.scssimport { createApp } from vue
import { createPinia } from pinia
import { useIntersectionObserver } from vueuse/coreimport App from ./App.vue
import router from ./routerconst app createApp(App)app.use(createPinia())
app.use(router)app.directive(img-lazy, {mounted(el, binding) {//el指令绑定的对象//binding.value指令 后的表达式的值console.log(el, binding.value)useIntersectionObserver(el,([{ isIntersecting }]) {if (isIntersecting) {el.src binding.value}},)},
})
app.mount(#app)插件封装
在入口文件中写入懒加载逻辑是不合适的应当封装成插件。
创建插件文件directives/img-lazy.js
import { useIntersectionObserver } from vueuse/core
//图片懒加载插件
export const imgLazyPlugin {install(app) {// 配置此应用app.directive(img-lazy, {mounted(el, binding) {//el指令绑定的对象//binding.value指令 后的表达式的值console.log(el, binding.value)useIntersectionObserver(el,([{ isIntersecting }]) {if (isIntersecting) {el.src binding.value}},)},})}
}这里的useIntersectionObserver函数是 vueuse 库中用于监听某个控件是否在 Window 中显示的函数。
修改main.js使用插件
// import ./assets/main.css
import /styles/common.scssimport { createApp } from vue
import { createPinia } from pinia
import { imgLazyPlugin } from ./directives/img-lazyimport App from ./App.vue
import router from ./routerconst app createApp(App)app.use(createPinia())
app.use(router)
app.use(imgLazyPlugin)app.mount(#app)避免重复监听
如果不在图片加载后手动停止监听监听行为就一直存在。
修改img-lazy.js手动停止监听
const { stop } useIntersectionObserver(el,([{ isIntersecting }]) {if (isIntersecting) {el.src binding.valuestop()}},
)useIntersectionObserver会返回一个停止的函数在合适的时候调用即可。
商品列表
商品列表控件HomeProduct.vue的初始代码可以从这里获取。
封装接口
export const getGoodsService (){return http.get(/home/goods)
}渲染数据
script setup
import HomePanel from ./HomePannel.vue
import { getGoodsService } from /apis/home
import { ref } from vue
const goodsProduct ref([])
const loadGoods async () {const res await getGoodsService()goodsProduct.value res.result
}
loadGoods()
/scripttemplatediv classhome-productHomePanel :titlecate.name v-forcate in goodsProduct :keycate.iddiv classboxRouterLink classcover to/img :srccate.picture /strong classlabelspan{{ cate.name }}馆/spanspan{{ cate.saleInfo }}/span/strong/RouterLinkul classgoods-listli v-forgood in cate.goods :keygood.idRouterLink to/ classgoods-itemimg :srcgood.picture alt /p classname ellipsis{{ good.name }}/pp classdesc ellipsis{{ good.desc }}/pp classpriceyen;{{ good.price }}/p/RouterLink/li/ul/div/HomePanel/div
/template分类页
导航
分类页的 url 类似于/category/分类ID因此需要修改导航让路径有分类ID routes: [{path: /,component: LayoutVue,children: [{ path: , component: HomeVue },{ path: /category/:id, component: CategoryVue }]},{ path: /login, component: LoginVue }]修改LayoutHeader.vue中的导航栏让超链接定位到分类的 url
RouterLink :to/category/${cat.id}{{ cat.name }}/RouterLink 吸顶导航栏以同样的方式修改这里不再赘述。
面包屑导航
分类页category/index.vue中面包屑导航的基本实现见这里。
封装接口category.js
import http from /utils/http// 获取一级分类详情
export const getCategoryService (id) {return http.get(/category?id id)
}渲染数据
script setup
import { getCategoryService } from /apis/category
import { ref, onMounted } from vue
import { useRoute } from vue-router
const category ref({})
const route useRoute()
const loadCategory async (id) {const res await getCategoryService(id)category.value res.result
}
onMounted(() {loadCategory(route.params.id)
})
/scripttemplatediv classtop-categorydiv classcontainer m-top-20!-- 面包屑 --div classbread-containerel-breadcrumb separatorel-breadcrumb-item :to{ path: / }首页/el-breadcrumb-itemel-breadcrumb-item{{ category.name }}/el-breadcrumb-item/el-breadcrumb/div/div/div
/template轮播
修改接口home.js
//轮播
export const getHomeBannerService (distributionSite 1) {return http.get(/home/banner, { params: { distributionSite } })
}增加分类页轮播控件category/components/CategoryBanner.vue
script setup
import { getHomeBannerService } from /apis/home;
import { ref } from vue;
const banner ref([])
const loadHomeBanner async (){const result await getHomeBannerService(2)banner.value result.result
}
loadHomeBanner()
/scripttemplatediv classhome-bannerel-carousel height500pxel-carousel-item v-foritem in banner :keyitem.idimg :srcitem.imgUrl alt/el-carousel-item/el-carousel/div
/templatestyle scoped langscss
.home-banner {width: 1240px;height: 500px;margin: 0 auto;img {width: 100%;height: 500px;}
}
/style修改category/index.vue
script setup
// ...
import CategoryBannerVue from ./components/CategoryBanner.vue
// ...
/script
template!-- ... --CategoryBannerVue/
/template激活状态控制
RouterLink 增加属性active-classactive
RouterLink active-classactive :to/category/${cat.id}{{ cat.name }}/RouterLink 分类列表渲染
templatediv classtop-categorydiv classcontainer m-top-20!-- 面包屑 --div classbread-containerel-breadcrumb separatorel-breadcrumb-item :to{ path: / }首页/el-breadcrumb-itemel-breadcrumb-item{{ category.name }}/el-breadcrumb-item/el-breadcrumb/div/divCategoryBannerVue /div classsub-listh3全部分类/h3ulli v-fori in category.children :keyi.idRouterLink to/img :srci.picture /p{{ i.name }}/p/RouterLink/li/ul/divdiv classref-goods v-foritem in category.children :keyitem.iddiv classheadh3- {{ item.name }}-/h3/divdiv classbodyGoodsItem v-forgood in item.goods :goodgood :keygood.id //div/div/div
/template路由缓存问题
当路由中包含参数且切换路径时只有参数发生变化会复用组件而不是将组件销毁并重新创建此时组件的相关钩子函数不会被触发setup、onMounted等。
解决这个问题有两种方案
为组件赋予一个独一无二的 key 属性让组件强制销毁监听路径更新钩子手动更新数据
第一种方案修改layout/index.vue
RouterView :key$route.fullPath/这种方案的缺陷是性能较差会将原本可以复用的组件也销毁需要重新通过网络请求创建。
第二种方案可以使用一个 vue-router 的 导航守卫
script setup
import { getCategoryService } from /apis/category
import { ref, onMounted } from vue
import { useRoute, onBeforeRouteUpdate } from vue-router
import CategoryBannerVue from ./components/CategoryBanner.vue
import GoodsItem from /views/home/components/GoodsItem.vue
const category ref({})
const route useRoute()
const loadCategory async (id) {const res await getCategoryService(id)category.value res.result
}
onMounted(() {loadCategory(route.params.id)
})
onBeforeRouteUpdate(async (to) {await loadCategory(to.params.id)
})重构
当 Vue 中的 js 部分包含太多逻辑可以进行封装和重构。
将/category/index.vue中渲染分类数据的部分代码拆分到category/composable/useCategory.js中
import { ref, onMounted } from vue
import { getCategoryService } from /apis/category
import { useRoute, onBeforeRouteUpdate } from vue-router
export const useCategory(){const category ref({})const route useRoute()const loadCategory async (id) {const res await getCategoryService(id)category.value res.result}onMounted(() {loadCategory(route.params.id)})onBeforeRouteUpdate(async (to) {await loadCategory(to.params.id)})return {category}
}/category/index.vue中就只包含以下的 JS 代码
import CategoryBannerVue from ./components/CategoryBanner.vue
import GoodsItem from /views/home/components/GoodsItem.vue
import { useCategory } from ./composable/useCategory
const { category } useCategory()二级分类
跳转
创建二级分类页/views/subcategory/index.vue基本代码见这里。
修改路由/router/index.js
const router createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: /,component: LayoutVue,children: [{ path: , component: HomeVue },{ path: category/:id, component: CategoryVue },{ path: category/sub/:id, component: SubCategoryVue }]},{ path: /login, component: LoginVue }]
})修改分类页/views/category/index.vue让二级分类链接跳转到二级分类页面
RouterLink :to/category/sub/${i.id}面包屑
接口
// 获取二级分类详情
export const getSubCategoryService (id) {return http.get(/category/sub/filter?id id)
}获取数据
import { getSubCategoryService } from /apis/category;
import { useRoute } from vue-router
import { ref } from vue
const route useRoute()
const subCategory ref({})
const loadSubCategory async () {const res await getSubCategoryService(route.params.id)subCategory.value res.result
}
loadSubCategory()渲染数据
div classbread-containerel-breadcrumb separatorel-breadcrumb-item :to{ path: / }首页/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${subCategory.parentId} }{{ subCategory.parentName }}/el-breadcrumb-itemel-breadcrumb-item{{ subCategory.name }}/el-breadcrumb-item/el-breadcrumb
/div基本商品列表
接口
/*** description: 获取导航数据* data { categoryId: 1005000 ,page: 1,pageSize: 20,sortField: publishTime | orderNum | evaluateNum} * return {*}*/export const getSubCategoryGoodsService (data) {return http({url:/category/goods/temporary,method:POST,data})}加载数据
const goods ref([])
const params ref({categoryId: route.params.id,page: 1,pageSize: 20,sortField: publishTime
})
const loadGoods async () {const res await getSubCategoryGoodsService(params.value)goods.value res.result.items
}
loadGoods()渲染数据
div classbody!-- 商品列表--GoodsItem v-forgood in goods :goodgood :keygood.id/
/div筛选
在 ElementPlus 的选项卡组件上绑定数据模型和事件
el-tabs v-modelparams.sortField tab-changetabChangeel-tab-pane label最新商品 namepublishTime/el-tab-paneel-tab-pane label最高人气 nameorderNum/el-tab-paneel-tab-pane label评论最多 nameevaluateNum/el-tab-pane
/el-tabs这样某个选项卡被点击后params.sortField的值就会变为对应选项卡的name并会执行tab-change事件。
tabChange定义
const tabChange (){params.value.page 1loadGoods()
}无限加载
可以通过 ElementPlus 的 无限滚动 功能实现对产品列表的无限加载。
div classbody v-infinite-scrollloadMoreGoods :infinite-scroll-disabledloadMoreDisabled!-- 商品列表--GoodsItem v-forgood in goods :goodgood :keygood.id /
/div这里的v-infinite-scroll属性对应当前窗口滚动到商品列表底部时会触发的方法infinite-scroll-disabled属性对应的响应式数据如果为true将会停止无限加载。
loadMoreGoods函数定义
const loadMoreDisabled ref(false)
const loadMoreGoods async () {// 翻页params.value.page// 获取商品数据const res await getSubCategoryGoodsService(params.value)// 如果已经没有数据了停止加载if (res.result.items.length 0) {loadMoreDisabled.value truereturn}// 与已有商品数据合并goods.value [...goods.value, ...res.result.items]
}定制路由滚动行为
要在切换路由的时候让窗口滚动定位到页面的顶部需要定制路由的滚动行为
const router createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: /,component: LayoutVue,children: [{ path: , component: HomeVue },{ path: category/:id, component: CategoryVue },{ path: category/sub/:id, component: SubCategoryVue }]},{ path: /login, component: LoginVue }],scrollBehavior() {// 始终滚动到顶部return { top: 0 }},
})商品详情页
路由
商品详情页的基本代码见这里。
添加二级路由
{path: /,component: LayoutVue,children: [{ path: , component: HomeVue },{ path: category/:id, component: CategoryVue },{ path: category/sub/:id, component: SubCategoryVue },{ path: detail/:id, component: DetailVue }]
},修改HomeNew.vue添加商品跳转链接
RouterLink :to/detail/${good.id}基础数据
接口新建apis/detail.js
import http from /utils/http// 获取商品详情
export const getGoodService (id) {return http.get(/goods?id id)
}修改detail/index.vue加载数据
script setup
import { getGoodService } from /apis/detail
import { ref } from vue
import { useRoute } from vue-router
const good ref({})
const route useRoute()
const loadGood async () {const res await getGoodService(route.params.id)good.value res.result
}
loadGood()
/script渲染面包屑导航
el-breadcrumb separatorel-breadcrumb-item :to{ path: / }首页/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${good.categories[1].id} }{{ good.categories[1].name }}/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${good.categories[0].id} }{{ good.categories[0].id }}/el-breadcrumb-itemel-breadcrumb-item抓绒保暖毛毛虫子儿童运动鞋/el-breadcrumb-item
/el-breadcrumb实际运行会报错因为页面刚加载时响应式数据good的初始值是空对象所以good.categories的值是undefined因此试图访问其下标会报错。
解决的方式有两种其一是使用条件访问符?.只在good.categories存在时访问其下标
el-breadcrumb separatorel-breadcrumb-item :to{ path: / }首页/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${good.categories?.[1].id} }{{ good.categories?.[1].name }}/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${good.categories?.[0].id} }{{ good.categories?.[0].id }}/el-breadcrumb-itemel-breadcrumb-item抓绒保暖毛毛虫子儿童运动鞋/el-breadcrumb-item
/el-breadcrumb还有一种更简单的方式使用 vue 的v-if指令控制只在存在某属性时才加载对应的控件
div classcontainer v-ifgood.detailsdiv classbread-containerel-breadcrumb separatorel-breadcrumb-item :to{ path: / }首页/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${good.categories[1].id} }{{ good.categories[1].name }}/el-breadcrumb-itemel-breadcrumb-item :to{ path: /category/${good.categories[0].id} }{{ good.categories[0].id }}/el-breadcrumb-itemel-breadcrumb-item抓绒保暖毛毛虫子儿童运动鞋/el-breadcrumb-item/el-breadcrumb/div!-- ... --
/div详情页其他基本数据的页面渲染这里不再赘述。
24小时热榜
新建24小时热榜组件/detail/components/DetailHot.vue其基础代码见这里。
在商品详情页使用
!-- 24热榜专题推荐 --
div classgoods-aside!-- 24小时 --DetailHotVue/!-- 周榜 --DetailHotVue/
/div封装接口
/*** 获取热榜商品* param {Number} id - 商品id* param {Number} type - 1代表24小时热销榜 2代表周热销榜* param {Number} limit - 获取个数*/
export const fetchHotGoodsService ({ id, type, limit 3 }) {return http({url:/goods/hot,params:{id, type, limit}})
}渲染数据
script setup
import { fetchHotGoodsService } from /apis/detail
import { ref } from vue
import { useRoute } from vue-router;
const hotGoods ref([])
const route useRoute()
const loadHotGoods async () {const res await fetchHotGoodsService({id: route.params.id,type: 1})hotGoods.value res.result
}
loadHotGoods()
/scripttemplatediv classgoods-hoth3周日榜单/h3!-- 商品区块 --RouterLink to/ classgoods-item v-foritem in hotGoods :keyitem.idimg :srcitem.picture alt /p classname ellipsis{{ item.name }}/pp classdesc ellipsis{{ item.desc }}/pp classpriceyen;{{ item.price }}/p/RouterLink/div
/template参数化热榜
为了能让周热榜和24小时热榜复用同一个控件可以将热榜参数化
script setup
import { fetchHotGoodsService } from /apis/detail
import { ref } from vue
import { useRoute } from vue-router;
const props defineProps({hotType: {type: Number}
})
const title props.hotType 1 ? 24小时热榜 : 周热榜
const hotGoods ref([])
const route useRoute()
const loadHotGoods async () {const res await fetchHotGoodsService({id: route.params.id,type: props.hotType})hotGoods.value res.result
}
loadHotGoods()
/scripttemplatediv classgoods-hoth3{{ title }}/h3!-- 商品区块 --RouterLink to/ classgoods-item v-foritem in hotGoods :keyitem.idimg :srcitem.picture alt /p classname ellipsis{{ item.name }}/pp classdesc ellipsis{{ item.desc }}/pp classpriceyen;{{ item.price }}/p/RouterLink/div
/template对应的只要在商品详情页指定不同的参数就能加载不同的热榜
!-- 24热榜专题推荐 --
div classgoods-aside!-- 24小时 --DetailHotVue :hotType1 /!-- 周榜 --DetailHotVue :hotType2 /
/div图片预览
新建图片预览控件/src/components/imageview/index.vue基本代码见这里。
实现
script setup
import { ref } from vue
// ...
const activeIndex ref(0)
const mouseEnter (i) {activeIndex.value i
}
/scripttemplatediv classgoods-image!-- ... --!-- 小图列表 --ul classsmallli v-for(img, i) in imageList :keyi mouseentermouseEnter(i) :class{ active: i activeIndex }img :srcimg alt //li/ul!-- ... --/div
/template这里的mouseenter事件对应鼠标移入小图的事件所绑定的mouseEnter方法中用当前小图的下标替换activeIndex的值。:class{ active: i activeIndex }可以让当前生效的下标对应的小图拥有active的class值也就是有被选中的样式。
图片蒙版随鼠标移动
script setup
import { ref, watch } from vue
import { useMouseInElement } from vueuse/core
// ...
const target ref(null)
const { elementX, elementY, isOutside } useMouseInElement(target)
const x elementX
const y elementY
const top ref(0)
const left ref(0)
watch([x, y], () {if (x.value 100 x.value 300) {left.value x.value - 100}if (y.value 100 y.value 300) {top.value y.value - 100}if (x.value 100) {left.value 0}if (x.value 300) {left.value 200}if (y.value 100) {top.value 0}if (y.value 300) {top.value 200}
})
/scripttemplatediv classgoods-image!-- 左侧大图--div classmiddle reftargetimg :srcimageList[activeIndex] alt /!-- 蒙层小滑块 --div classlayer :style{ left: ${left}px, top: ${top}px }/div/div!-- ... --/div
/templateuseMouseInElement是 vue-use 中用于定位鼠标在元素中相对位置的函数。其返回值的含义
elementX鼠标在元素中的 x 轴坐标elementY鼠标在元素中的 y 轴坐标isOutside鼠标是否在元素外
这里用 vue 的 watch 函数监听鼠标在元素中的位置改变位置发生变化后控制蒙版的位置改变。
放大镜
script setup
// ...
const largeLeft ref(0)
const largeTop ref(0)
watch([x, y], () {// ...largeLeft.value -left.value * 2largeTop.value -top.value * 2
})
/scripttemplatediv classgoods-image!-- 左侧大图--div classmiddle reftargetimg :srcimageList[activeIndex] alt /!-- 蒙层小滑块 --div classlayer :style{ left: ${left}px, top: ${top}px } v-show!isOutside/div/div!-- 小图列表 --ul classsmallli v-for(img, i) in imageList :keyi mouseentermouseEnter(i) :class{ active: i activeIndex }img :srcimg alt //li/ul!-- 放大镜大图 --div classlarge :style[{backgroundImage: url(${imageList[activeIndex]}),backgroundPositionX: ${largeLeft}px,backgroundPositionY: ${largeTop}px,},] v-show!isOutside/div/div
/template这里的放大镜实际上是一张长宽是预览图2倍大的图片通过控制图片移动方向与蒙版相反来控制放大镜内容的改变。此外这里还通过v-show!isOutside来控制鼠标移出预览图时隐藏放大镜与蒙版。
组件参数化
将图片预览组件中使用的硬编码图片列表参数化
defineProps({imageList: {type: Array,default: () []}
})修改图片详情页views/detail/index.vue传递参数
ImageViewVue :imageListgood.mainPictures/SKU控件
将 SKU 控件放入/src/components下。
导入并使用控件
script setup
import XtxSkuVue from /components/XtxSku/index.vue;
// ...
const skuChanged (sku) {console.log(sku)
}
/script
template!-- ... --!-- sku组件 --XtxSkuVue :goodsgood changeskuChanged /
/template该控件需要传入一个表示商品的参数在规格被选中时会调用change方法返回选中的规格。
全局组件注册
可以将常用组件注册为全局组件。
新建/src/components/index.js
// 将 components 下的组件注册为全局组件
import ImageView from ./imageview/index.vue
import Sku from ./XtxSku/index.vue
export const componentsPlugin {install: (app) {app.component(XtxImageView, ImageView)app.component(XtxSku, Sku)}
}在main.js中以插件方式使用
// ...
import { componentsPlugin } from ./components
// ...
app.use(componentsPlugin)app.mount(#app)在views/detail/index.vue中直接使用全局控件
XtxImageView :imageListgood.mainPictures /
!-- ... --
XtxSku :goodsgood changeskuChanged /登录
页面
新建登录页login/index.vue基本代码可以从这里获取。
修改页头的用户状态显示/layout/components/LayoutNav.vue强制显示非登录状态
template v-iffalse修改跳转链接
lia hrefjavascript:; click$router.push(/login)请先登录/a/li表单校验
script setup
import { ref } from vue
const loginData ref({account: ,password: ,agree: true
})
const rules {account: [{ required: true, message: 账户不能为空, trigger: blur }],password: [{ required: true, message: 密码不能为空, trigger: blur },{ min: 6, max: 14, message: 密码为6~14个字符, trigger: blur }],agree: [{validator: (rule, value, callback) {if (value) {callback()}else {callback(new Error(请同意用户协议))}}}]
}
/scripttemplate
!-- ... --
el-form :modelloginData :rulesrules label-positionright label-width60px status-iconel-form-item label账户 propaccountel-input v-modelloginData.account //el-form-itemel-form-item label密码 proppasswordel-input v-modelloginData.password //el-form-itemel-form-item label-width22px propagreeel-checkbox sizelarge v-modelloginData.agree我已同意隐私条款和服务条款/el-checkbox/el-form-itemel-button sizelarge classsubBtn点击登录/el-button
/el-form
/template登录统一校验
在表单上配置的校验规则只会在表单元素失去焦点时触发直接点击登录按钮并不会触发校验规则因此需要在点击登录按钮时手动执行表单对象的校验规则
const formRef ref(null)
const btnLoginClick () {formRef.value.validate((valid) {console.log(valid)if(valid){// 执行登录操作}})
}这里的formRef绑定的是表单对象
el-form refformRef :modelloginData :rulesrules label-positionright label-width60px status-iconbtnLoginClick对应的是登录按钮点击事件
el-button sizelarge classsubBtn clickbtnLoginClick点击登录/el-button登录
封装接口新增接口文件/src/apis/user.js
import http from /utils/http/*** 登录* param {String} account* param {String} password* returns */
export const loginService (params) {return http.post(/login, params)
}调用接口进行登录
import { ElMessage } from element-plus;
import element-plus/theme-chalk/el-message.css
// ...
const btnLoginClick () {formRef.value.validate(async (valid) {console.log(valid)if (valid) {// 执行登录操作const { account, password } loginData.valueawait loginService({ account, password })ElMessage.success(登录成功)// 登录成功后跳转到首页router.replace({ path: / })}})
}登录失败的提示信息由 Axios 的响应拦截器完成
import { ElMessage } from element-plus;
import element-plus/theme-chalk/el-message.css
// ...
// axios响应式拦截器
http.interceptors.response.use(res res.data, e {ElMessage.warning(e.response.data.message)return Promise.reject(e)
})Pinia 存储用户数据
创建存储库文件stores/user.js
import { defineStore } from pinia;
import { ref } from vue
import { loginService } from /apis/userexport const useUserStore defineStore(user, () {const userInfo ref({})const loadUserInfo async (account, password) {const res await loginService({ account, password })userInfo.value res.result}return { userInfo, loadUserInfo }
})在登录时调用存储库的 Action 存储用户数据
script setup
// ...
import { useUserStore } from /stores/user
// ...
const userStore useUserStore()
const btnLoginClick () {formRef.value.validate(async (valid) {console.log(valid)if (valid) {// 执行登录操作const { account, password } loginData.valueawait userStore.loadUserInfo(account, password)ElMessage.success(登录成功)// 登录成功后跳转到首页router.replace({ path: / })}})
}
/script用户数据持久化
这里使用 Pinia 插件 pinia-plugin-persistedstate 实现。
安装
npm i pinia-plugin-persistedstate修改main.js使用插件
import piniaPluginPersistedstate from pinia-plugin-persistedstateconst app createApp(App)
const pinia createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.use(router)
app.use(imgLazyPlugin)
app.use(componentsPlugin)app.mount(#app)修改stores/user.js持久化用户数据
export const useUserStore defineStore(user, () {// ...
},{persist: true,}
)登录状态显示
处于登录状态时标题栏显示用户名称。
修改LayoutNav.vue
script setup
import { useUserStore } from /stores/user
const userStore useUserStore()
/scripttemplatenav classapp-topnavdiv classcontainerultemplate v-ifuserStore.userInfo.tokenlia hrefjavascript:;i classiconfont icon-user/i{{ userStore.userInfo.account }}/a/liliel-popconfirm title确认退出吗? confirm-button-text确认 cancel-button-text取消template #referencea hrefjavascript:;退出登录/a/template/el-popconfirm/lilia hrefjavascript:;我的订单/a/lilia hrefjavascript:;会员中心/a/li/templatetemplate v-elselia hrefjavascript:; click$router.push(/login)请先登录/a/lilia hrefjavascript:;帮助中心/a/lilia hrefjavascript:;关于我们/a/li/template/ul/div/nav
/template传递 token
很多接口都要求通过报文头传递token这一点可以通过 Axios 的请求拦截器做到
// axios请求拦截器
http.interceptors.request.use(config {// 获取tokenconst userStore useUserStore()const token userStore.userInfo.token// 将 token 设置为请求头if (token) {config.headers.Authorization Bearer ${token}}return config
}, e Promise.reject(e))退出登录
script setup
import { useUserStore } from /stores/user
import { useRouter } from vue-router
const userStore useUserStore()
const router useRouter()
// 确认退出
const confirmed () {// 清理 userStoreuserStore.clearUserInfo()// 跳转到登录页router.push({ path: /login })
}
/scripttemplate
!-- ... --
el-popconfirm confirmconfirmed title确认退出吗? confirm-button-text确认 cancel-button-text取消template #referencea hrefjavascript:;退出登录/a
/template
/el-popconfirm
!-- ... --
/templateel-popconfirm是一个绑定到按钮的确认框confirm是绑定的点击确认框中确认按钮后的事件。
处理 token 失效
长时间不操作会导致 token 失效服务端接口会返回 401 状态码此时需要在 Axios 的响应拦截器进行统一处理
import router from /router;
// ...
// axios响应式拦截器
http.interceptors.response.use(res res.data, e {ElMessage.warning(e.response.data.message)// token 失效时服务端返回 http 状态码为 401if (e.response.status 401) {// 清理 userStoreconst userStore useUserStore()userStore.clearUserInfo()// 跳转到登录页router.push({ path: /login })}return Promise.reject(e)
})需要注意的是因为加载顺序的关系这里不能使用useRouter函数获取router对象。
购物车
添加购物车
为购物车创建存储库stores/cart.js
import { defineStore } from pinia;
import { ref } from vue;// 购物车
export const useCartStore defineStore(cart, () {// 商品列表const goods ref([])// 添加商品const addGood (good) {console.log(good)const matched goods.value.find((item) item.skuId good.skuId)if (matched) {// 购物车中已经存在相同的 skumatched.count good.count}else {// 购物车中没有goods.value.push(good)}}return { goods, addGood }
}, {persist: true,
})修改商品详情页detail/index.vue
script setup
import { getGoodService } from /apis/detail
import { ref } from vue
import { useRoute } from vue-router
import DetailHotVue from ./components/DetailHot.vue
import { ElMessage } from element-plus;
import element-plus/theme-chalk/el-message.css
import { useCartStore } from /stores/cart
const good ref({})
const route useRoute()
const loadGood async () {const res await getGoodService(route.params.id)good.value res.result
}
loadGood()
// 选中的 sku
let skuSelected {}
const skuChanged (sku) {console.log(sku)skuSelected sku
}
// 选购商品数量
const num ref(1)
const cartStore useCartStore()
// 点击加入购物车按钮
const btnCartClick () {if (!skuSelected.skuId) {// 如果没有选中规格ElMessage.warnning(请选择规格)return}// 如果数量小于等于0if (num.value 0) {ElMessage.warnning(请选择数量)}// 加入购物车cartStore.addGood({id: good.value.id,name: good.value.name,picture: good.value.mainPictures[0],price: good.value.price,count: num.value,skuId: skuSelected.skuId,attrText: skuSelected.specsText,selected: true})
}
/script添加数量控件并绑定购物车按钮点击事件
!-- 数据组件 --
el-input-number v-modelnum :min1 :max10 changehandleChange /
!-- 按钮组件 --
divel-button sizelarge classbtn clickbtnCartClick加入购物车/el-button
/div头部购物车
创建头部购物车控件views/layout/HeaderCart.vue基础代码见这里。
在LayoutHeader.vue中使用头部购物车
!-- 头部购物车 --
HeaderCartVue/为购物车添加删除功能
import { defineStore } from pinia;
import { ref, computed } from vue;// 购物车
export const useCartStore defineStore(cart, () {// ...const delGood (skuId) {const index goods.value.findIndex(item item.skuId skuId)console.log(index)if (index 0) {goods.value.splice(index, 1)}}// ...return { goods, addGood, delGood }
}, {persist: true,
})修改HeaderCart.vue绑定删除按钮点击事件
i classiconfont icon-close-new clickcartStore.delGood(i.skuId)/i为购物车添加计算属性以统计购物车中的总数和总金额
// ...
export const useCartStore defineStore(cart, () {// ...const count computed(() {return goods.value.reduce((totalCount, good) {return totalCount good.count}, 0)})const price computed(() {return goods.value.reduce((totalPrice, good) {return totalPrice good.price * good.count}, 0)})return { goods, addGood, delGood, count, price }
}, {persist: true,
})在头部购物车中显示总数和总金额
div classfootdiv classtotalp共 {{ cartStore.count }} 件商品/ppyen; {{ cartStore.price.toFixed(2) }} /p/divel-button sizelarge typeprimary去购物车结算/el-button
/div列表购物车
创建列表购物车控件/views/cartlist/index.vue基本代码见这里。
添加路由
{path: /,component: LayoutVue,children: [{ path: , component: HomeVue },{ path: category/:id, component: CategoryVue },{ path: category/sub/:id, component: SubCategoryVue },{ path: detail/:id, component: DetailVue },{ path: cartlist, component: CartListVue }]
},修改头部购物车/views/layout/components/HeaderCart.vue绑定点击事件
el-button sizelarge typeprimary click$router.push(/cartlist)去购物车结算/el-button修改购物车列表使用存储库数据渲染列表
import {useCartStore} from /stores/cart
const cartStore useCartStore()
const cartList cartStore.goods单选按钮
为列表购物车的单选按钮绑定事件和值
el-checkbox :model-valuei.selected change(selected) ckboxChanged(i.skuId, selected) /这里并没有直接使用v-model属性进行双向绑定而是采用model-value属性和change事件实现双向绑定这样可以在change事件中加入自定义逻辑更为灵活。
change事件的实现
import { useCartStore } from /stores/cart
const cartStore useCartStore()
const cartList cartStore.goods
const ckboxChanged (skuId, selected) {cartStore.changeSelected(skuId, selected)
}全选按钮
为购物车存储库增加一个计算属性用于表示是否所有商品都被选中
// 是否全部选中
const isAllSelected computed(() {return goods.value.every(g g.selected)
})使用该计算属性作为全选按钮的值
el-checkbox :model-valuecartStore.isAllSelected changeckboxAllChanged /为购物车存储库增加一个 Action用于修改所有商品的选中状态
// 修改所有商品的选中状态
const changeAllSelected (selected) {goods.value.forEach(g g.selected selected)
}使用该 Action 实现全选按钮的change事件
const ckboxAllChanged (selected) {cartStore.changeAllSelected(selected)
}合计
列表购物车中需要显示选中商品的合计情况同样需要使用存储库的计算属性实现
// 选中商品的数目总和
const selectedCount computed(() {return goods.value.filter(g g.selected).reduce((total, g) total g.count, 0)
})
// 选中商品的价格总和
const selectedPrice computed(() {return goods.value.filter(g g.selected).reduce((total, g) total g.count * g.price, 0)
})将相关内容渲染到页面
div classbatch共 {{ cartStore.count }} 件商品已选择 {{ cartStore.selectedCount }} 件商品合计span classred¥ {{ cartStore.selectedPrice.toFixed(2) }} /span
/div购物车接口
加入购物车
修改加入购物车逻辑如果用户已经登录通过接口加入商品到购物车并且通过接口获取最新的购物车信息并覆盖本地购物车数据。
新增购物车相关接口apis/cart.js
import http from /utils/http/*** 添加商品到购物车* param {String} skuId * param {Number} count * returns */
export const addGood2CartService (skuId, count) {return http.post(/member/cart, { skuId, count })
}/*** 从购物车获取商品列表* returns */
export const getGoodsFromCartService () {return http.get(/member/cart)
}修改存储库stores/user.js增加一个表示是否登录的计算属性
const isLogin computed(() {if(userInfo.value.token){return true}return false
})修改存储库stores/cart.js
// ...
import { addGood2CartService, getGoodsFromCartService } from /apis/cart
// 添加商品
const addGood async (good) {// 用户是否登录如果已经登录通过接口添加购物车并获取购物车信息覆盖本地数据const userStore useUserStore()if (userStore.isLogin) {// 用户已经登录// 通过接口添加购物车await addGood2CartService(good.skuId, good.count)// 从接口获取购物车信息const res await getGoodsFromCartService()// 覆盖本地购物车goods.value res.result}else {const matched goods.value.find((item) item.skuId good.skuId)if (matched) {// 购物车中已经存在相同的 skumatched.count good.count}else {// 购物车中没有goods.value.push(good)}}
}删除购物车
封装接口
/*** 从购物车删除商品* param {Array} skuIds skuId 的集合* returns */
export const delGoodsFromCartService (skuIds) {return http.delete(/member/cart, {data: {ids: skuIds}})
}修改购物车存储库的删除 Action
const delGood async (skuId) {const userStore useUserStore()if (userStore.isLogin) {// 用户登录时通过接口删除商品await delGoodsFromCartService([skuId])// 通过接口获取最新购物车数据const res await getGoodsFromCartService()// 覆盖本地购物车数据goods.value res.result}const index goods.value.findIndex(item item.skuId skuId)console.log(index)if (index 0) {goods.value.splice(index, 1)}
}有多个地方都会从服务端更新购物车信息到本地这部分逻辑可以封装复用
// 从服务端读取购物车数据并更新到本地
const loadGoodsFromServer async (){// 从接口获取购物车信息const res await getGoodsFromCartService()// 覆盖本地购物车goods.value res.result
}清空购物车
需要在退出登录时清除购物车信息。
为购物车存储库增加清除信息 Action
// 清除购物车中的商品信息
const clear () {goods.value []
}修改用户存储库在退出时清除购物车信息
const clearUserInfo () {userInfo.value {}// 清除本地购物车const cartStore useCartStore()cartStore.clear()
}合并购物车
封装接口
/*** 合并购物车* param {[skuId:String, selected:string, count:Number]} goods * returns */
export const mergeCartService (goods) {return http.post(/member/cart/merge, goods)
}修改购物车存储库增加合并 Action
// 合并购物车
const merge () {// 合并购物车const items goods.value.map(g {return { skuId: g.skuId, selected: g.selected, count: g.count }})mergeCartService(items)// 更新本地购物车loadGoodsFromServer()
}修改用户存储库在登录后合并购物车
const loadUserInfo async (account, password) {const res await loginService({ account, password })userInfo.value res.result// 合并购物车cartStore.merge()
}结算
基本数据渲染
创捷结算页/views/checkout/index.vue基本代码见这里
封装接口apis/checkout.js
import http from /utils/http// 获取结算页订单信息
export const getCheckoutOrderService () {return http.get(/member/order/pre)
}渲染页面
script setup
import { getCheckoutOrderService } from /apis/checkout
import { onMounted, ref } from vue;
const order ref({})
const loadCheckoutOrder async () {const res await getCheckoutOrderService()order.value res.result
}
const checkInfo ref({}) // 订单对象
const curAddress ref({}) // 地址对象
onMounted(async () {await loadCheckoutOrder()const addr order.value.userAddresses.find(a a.isDefault 0)checkInfo.value order.valuecurAddress.value addr
})
/script切换地址弹窗
!-- 切换地址 --
el-dialog v-modelshowDialog title切换收货地址 width30% centerdiv classaddressWrapperdiv classtext item v-foritem in checkInfo.userAddresses :keyitem.idullispan收i /货i /人/span{{ item.receiver }} /lilispan联系方式/span{{ item.contact }}/lilispan收货地址/span{{ item.fullLocation item.address }}/li/ul/div/divtemplate #footerspan classdialog-footerel-button取消/el-buttonel-button typeprimary确定/el-button/span/template
/el-dialog定义showDialog
// 是否显示切换地址弹窗
const showDialog ref(false)绑定按钮点击事件
el-button sizelarge clickshowDialog true切换地址/el-button切换地址
创建一个变量记录当前激活的地址
// 当前激活的地址
const activeAddr ref({})点击地址信息后记录该地址并设置动态类名显示当前激活的地址
div classtext item :class{ active: item.id activeAddr.id } v-foritem in checkInfo.userAddresses :keyitem.id clickactiveAddr item为弹窗确认按钮绑定点击事件
el-button typeprimary clickbtnDialogConfirmClick确定/el-buttonconst btnDialogConfirmClick () {curAddress.value activeAddr.valueshowDialog.value falseactiveAddr.value {}
}提交订单
创建提交订单后要跳转到的支付页面views/pay/index.vue基本代码见这里。
配置二级路由
{ path: pay, component: PayVue }封装接口
// 提交订单
export const commitOrderService (data) {return http.post(/member/order, data)
}修改结算页增加提交订单点击事件
const router useRouter()
const cartStore useCartStore()
const btnCommitOrderClick async () {const res await commitOrderService({deliveryTimeType: 1,payType: 1,payChannel: 1,buyerMessage: ,goods: checkInfo.value.goods.map(g { return { skuId: g.skuId, count: g.count } }),addressId: curAddress.value.id})// 提交订单成功后需要更新购物车信息await cartStore.loadGoodsFromServer()const orderId res.result.idrouter.push(/pay?id orderId)
}为按钮绑定事件
el-button typeprimary sizelarge clickbtnCommitOrderClick提交订单/el-button支付
渲染数据
封装接口apis/pay.js
import http from /utils/httpexport const getOrderInfoService (id) {return http.get(/member/order/${id})
}渲染数据到支付页
script setup
import { getOrderInfoService } from /apis/pay
import { ref } from vue;
import { useRoute } from vue-router
const payInfo ref({})
const route useRoute()
const loadPayInfo async () {const res await getOrderInfoService(route.query.id)payInfo.value res.result
}
loadPayInfo()
/script支付
拼接支付地址
// 支付地址
const baseURL http://pcapi-xiaotuxian-front-devtest.itheima.net/
const backURL http://127.0.0.1:5173/paycallback
const redirectUrl encodeURIComponent(backURL)
const payUrl ${baseURL}pay/aliPay?orderId${route.query.id}redirect${redirectUrl}让支付链接使用该地址
a classbtn alipay :hrefpayUrl/a点击链接即可跳转到支付宝沙箱环境支付。 黑马程序员提供的沙箱账号已经没有余额无法进行后续步骤。 支付结果展示
新建支付结果页views/pay/PayBack.vue基本代码见这里。
获取订单数据
script setup
import { ref } from vue;
import { getOrderInfoService } from /apis/pay
import { useRoute } from vue-router
const route useRoute()
const payInfo ref({})
const loadPayInfo async () {const res await getOrderInfoService(route.query.orderId)payInfo.value res.result
}
loadPayInfo()
/script渲染数据
span classiconfont icon-queren2 green v-if$route.query.payResult true/span
span classiconfont icon-shanchu red v-else/span
p classtit支付{{ $route.query.payResult true ? 成功 : 失败 }}/p
p classtip我们将尽快为您发货收货期间请保持手机畅通/p
p支付方式span支付宝/span/p
p支付金额span¥{{ payInfo.payMoney?.toFixed(2) }}/span/p倒计时
待支付页面有个倒计时编写一个第三方倒计时组件composables/timer.js
import { ref, onUnmounted, computed } from vue
import { dayjs } from element-plus// 计时器
export const useTimer () {const leftSeconds ref(0)const formatTime computed(() {return dayjs.unix(leftSeconds.value).format(mm分ss秒)})const start (totalSeconds) {if(totalSeconds0){return}leftSeconds.value totalSecondslet interval setInterval(() {leftSeconds.value--if (leftSeconds.value 0) {clearInterval(interval)}}, 1000)// 如果控件销毁时还存在定时任务结束onUnmounted(() {if (interval) {clearInterval(interval)}})}return { formatTime, start }
}修改待支付页面pay/index.vue启动计时器
const timer useTimer()
const loadPayInfo async () {const res await getOrderInfoService(route.query.id)payInfo.value res.resulttimer.start(payInfo.value.countdown)
}渲染计时器
p支付还剩 span{{ timer.formatTime }}/span, 超时后将取消订单/p个人中心
路由
新增个人中心框架组件/views/member/index.vue基本代码见这里。
新增个人中心组件/member/components/UserInfo.vue基本代码见这里。
新增我的订单组件/member/components/UserOrder.vue基本代码见这里。
增加路由
{path: member, component: MemberVue, children: [{ path: user, component: UserInfoVue },{ path: order, component: UserOrderVue }]
}渲染个人中心数据
封装接口
import http from /utils/httpexport const getLikeListService ({ limit 4 }) {return http({url: /goods/relevant,params: {limit}})
}渲染数据
script setup
import { useUserStore } from /stores/user
import { getLikeListService } from /apis/member;
import { ref } from vue
import GoodsItem from /views/home/components/GoodsItem.vue;
const userStore useUserStore()
const likeList ref([])
const loadLikeList async () {const res await getLikeListService({})likeList.value res.result
}
loadLikeList()
/script我的订单
基本数据
新增订单接口/apis/order.js
import http from /utils/http/*
params: {orderState:0,page:1,pageSize:2
}
*/
export const getUserOrderService (params) {return http({url: /member/order,method: GET,params})
}渲染数据
// 订单列表
const orderList ref([])
const loadOrderList async () {const params {orderState: 0,page: 1,pageSize: 2}const res await getUserOrderService(params)orderList.value res.result.items
}
loadOrderList()订单状态切换
定义状态切换事件
// 订单列表
const params ref({orderState: 0,page: 1,pageSize: 2
})
const orderList ref([])
const loadOrderList async () {const res await getUserOrderService(params.value)orderList.value res.result.items
}
loadOrderList()
// 标签页切换事件
const tabChanged (index) {params.value.orderState indexloadOrderList()
}绑定事件
el-tabs tab-changetabChanged分页
设置总条数和页面跳转事件
// 总条数
const total ref(0)
const orderList ref([])
const loadOrderList async () {const res await getUserOrderService(params.value)orderList.value res.result.itemstotal.value res.result.counts
}
loadOrderList()
// 标签页切换事件
const tabChanged (index) {params.value.orderState indexloadOrderList()
}
// 页码跳转
const pageChanged (currentPage){params.value.page currentPageloadOrderList()
}为 ElementPlus 分页组件绑定属性和方法
el-pagination :totaltotal :page-sizeparams.pageSize current-changepageChanged background layoutprev, pager, next /订单状态中文显示
准备转换函数
const fomartPayState (payState) {
const stateMap {1: 待付款,2: 待发货,3: 待收货,4: 待评价,5: 已完成,6: 已取消
}
return stateMap[payState]
}在显示订单状态时用函数转换内容
p{{ fomartPayState(order.orderState) }}/p默认显示个人中心页面
修改路由
path: member, component: MemberVue, children: [{ path: , component: UserInfoVue },{ path: order, component: UserOrderVue }
]修改views/member/index.vue中的菜单路径
RouterLink to/member个人中心/RouterLink修改/views/layout/components/LayoutNav.vue中的链接
lia href/member/order我的订单/a/li
lia href/member会员中心/a/li参考资料
黑马程序员前端Vue3小兔鲜电商项目实战
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/88689.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!