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,一经查实,立即删除!

相关文章

成都网站改版公司优设网app安卓下载

文章目录 一、文件流打开方式参数1、文件流打开方式参数2、文件指针3、组合打开方式4、文件打开失败 一、文件流打开方式参数 1、文件流打开方式参数 文件流打开方式参数 : ios::in : 以只读方式打开文件 ;ios::out : 以只写方式打开文件 , 默认打开方式 , 如果文件已存在则清…

小企业如何优化网站建设潍坊网站建设公司

R语言实验报告R语言实验报告R语言判别分析实验报告班级:应数1201学号姓名:麦琼辉时间:2016年11月28号1 实验目的及要求1) 了解判别分析的目的和意义;2) 熟悉R语言中有关判别分析的算法基础。2 实验设备及要求个人计算机一台&#…

好上手的做海报网站网站分站开发计划书

【OpenCV 例程200篇】87. 频率域钝化掩蔽 欢迎关注 『OpenCV 例程200篇』 系列,持续更新中 欢迎关注 『Python小白的OpenCV学习课』 系列,持续更新中 4.2 频率域钝化掩蔽 简单地,从原始图像中减去一幅平滑处理的钝化图像,也可以实…

公司网站开发的工作内容怎么做网站黑链

c返回指针时候注意提防这对函数式编程并不会造成太大的影响,这真棒。 这是关于某些实践的警告,您很可能会将其应用于您的代码,而这是完全错误的! 。 高阶函数对于函数式编程是必不可少的,因此,谈论它们将帮…

如何搜索网站的内容西安seo网站设计公司

很好的一道模拟题,做起来很舒服 做模拟题重要的还是心态,能静得下心读题 思路: 读完题后,很直观的一个思路就是暴力,但2e5的O(n^2)过不去,怎么优化?? 就是转换 sum ∑a[i] (sum - S0) % m 0 结果 v S0 % m 可以这么转换 sum % m S0 % m 令sum % m k 那么就是…

浙江住房城乡建设厅网站用wordpress做淘宝客

塑料检查井配套开发的注塑成型井盖、井筒、井座——城市基础设施的新选择 随着城市化进程的加快,城市基础设施建设的品质与效率日益受到重视。在这个背景下,塑料检查井及其配套开发的注塑成型井盖、井筒、井座以其独特的优势,正在逐渐取代传…

免费手机网站空间软件开发各阶段时间比例

5.1 加载一个可执行文件 默认情况下IDA Pro的反汇编代码中不包含PE头或资源节,可以手动指定加载。 5.2 IDA Pro接口 5.2.1 反汇编窗口模式 二进制模式/图形模式: 图形模式:红色表示一个条件跳转没有被采用,绿色表示这个条件跳转被…

名师工作室网站建设舟山 做企业网站

什么是线程模型: Java字节码运行在JVM中,JVM运行在各个操作系统上。所以当JVM想要进行线程创建回收这种操作时,势必需要调用操作系统的相关接口。也就是说,JVM线程与操作系统线程之间存在着某种映射关系,这两种不同维…

微信推送在哪个网站做静安区建设工程招标投标管理部门网站

定义 责任链模式是一种行为型设计模式,用于在对象间建立一条处理请求的链。它允许多个对象有机会处理请求,从而减少请求的发送者和接收者之间的耦合。在责任链模式中,每个接收者包含对另一个接收者的引用,形成一条链。如果一个对…

wap手机网站程序游戏交易网站怎么做

1、epubjs核心工作原理 1.1 epubjs的核心工作原理解析 epub电子书,会通过epubjs去实例化一个Book对象,Book对象会对电子书进行解析。Book对象可以通过renderTo方法去生成一个Rendition对象,Rendition主要负责电子书的渲染,通过R…

cdr做图时怎么找到网站的网站内容的设计

本文主要记录如何编译出windows版本的openss的lib库 1.下载openssl,获得openssl-master.zip。 a.可以通过github(网址在下方)上下载最新的代码、今天是2023.12.1我用的master版本,下载之后恭喜大侠获得《openssl-master.zip》 …

北京网站搭建服务seo软件定制

目录 前言 开篇语 准备工作 npm 概念 常见指令 项目中的包 创建项目 启动项目 服务器搭建 express 基本步骤 搭建应用 创建路由 监听端口 启动服务器 面试相关 结束语 前言 开篇语 本系列博客分享Node.js的相关知识点,本章讲解npm与服务器的简单…

国外网站风格七台河新闻联播视频

1、Port Security解析触发Port Security的条件:未授权的MAC地址端口MAC地址数量超过了限制触发Port Security后的动作:protect Security violation protect mode 丢弃数据,不发送SNMP Trap消息 restrict Security violation restr…

网站开发标书网站根目录验证文件是什么

在有些情况下,我们需要程序逻辑自动触发元素的事件,例如js提供了click(), form提供了reset(),submit()等方法!在jquery中提供了trigger()方法帮助我们自动触发事件,原理是什么呢?接下来让我们一探究竟&…

网站建设排版页面所有网站302跳转百度

日志记录在整个java工程开发中占着很重要的比重,因为很多问题的排查需要通过日志分析才能确认。在SpringBoot中我用得最多的就是log4j这个日志框架。接下来我们具体配置log4j. log4j定义了8个级别的log(除去OFF和ALL,可以说分为6个级别&#…

手机ftp传网站文件做家政网上推广网站

http://nicethemes.cn/news/txtlist_i28391v.html 这次来分享一下ES报错:java.io.IOException: Connection reset by peer 的解决经历 问题描述 本人最近负责了定时获取Prometheus Metrics并发送到ES做持久化存储的任务。然而在Metrics采集粒度从3分钟变为1小时后…

网站公司一站式服务企业网站建设训

1. 超链接导航失效: 小程序规则——wx.navigateTo 和 wx.redirectTo 不允许跳转到 tabbar 页面,只能用 wx.switchTab 跳转到 tabbar 页面转载于:https://www.cnblogs.com/xyyt/p/9252835.html

flask做大型网站开发高级私人定制制衣店

最近,摆摊经济开始火了起来,于是各路诸侯纷纷举起大旗开始摆摊。我周围也不乏有亲朋好友蠢蠢欲动,有的甚至已经初有规模。但这波摆摊风是否真的可行,对谁可行,有哪些风口,有哪些坑,我们慢慢分析…

网站不备案访问资讯网站如何做聚合

一、简介 1、NAS_CAB介绍 跨平台NAS软件,远程管理照片,影音和文件,无需专用设备,个人版永久免费。官网地址:https://www.nascab.cn/。 2、NatCross介绍 NatCross是内网穿透工具,也是免费的端口映射和DDNS动态域名解析软件。软件从2021年上线以来&…

创新的网站建设公司排名关键词网站排名顾问

简易电压采集装置(限MSP430、STM32单片机) 任务要求: 设计制作一个简易电压采集与显示装置,实现如下基本功能: 评分细则: 1.通过单片机内部ADC模块采集1路电压并通过OLED屏显示电压大小;&#x…