基于vue的黑马前端项目小兔鲜
目录
项目学习
初始化项目
建立项目
引入elementplus
elementPlus主题设置
配置axios
路由
引入静态资源
自动导入scss变量
Layout页
组件结构快速搭建
字体图标渲染
一级导航渲染
吸顶导航交互实现
Pinia优化重复请求
Home页
分类实现
新鲜好物实现
人气推荐实现
懒加载指令实现
产品列表实现
GoodsItem组件封装
一级分类页
导航栏
分类Banner渲染
导航激活
分类列表渲染
路由缓存问题
业务逻辑的函数拆分
二级分类页
实现
定制路由的滚动行为
商品详情
路由配置
数据渲染
热榜区域
适配热榜类型
图片预览组件封装
sku插件
登录页
表单
登录实现
登录失败的提示
Pinia管理用户
Pinia持久化存储
登录和非登录状态
请求拦截器携带token
退出登录
token失效处理
购物车
本地加入购物车
头部购物车
购物车页面
订单页
支付页
会员中心
基本页面
个人中心信息渲染
我的订单
分页
项目学习
视频:黑马程序员-小兔鲜
黑马资料:文档
vue调试:vue-devtools
DEV:HBuilder
初始化项目
建立项目
安装node
安装过node的需要查看node的版本是否大于或等于15,否则报错
Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree.
windows下更新node需要重新安装,即覆盖原有的node。
查看镜像地址
npm config get registry
设置淘宝镜像地址
npm config set registry https://registry.npm.taobao.org/
安装脚手架
npm i -g @vue/cli
下载create-vue
npm install create-vue@3.7.3
注意版本,或者安装最新版。
初始化项目
安装依赖
npm install
配置别名路径
在根目录下新建jsconfig.json
{"compilerOptions" : {"baseUrl" : "./","paths" : {"@/*":["src/*"]}}
}
引入elementplus
官网
// 引入插件
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'export default defineConfig({plugins: [// 配置插件AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),]
})
vit.config.js
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script><template><header><img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /><div class="wrapper"><HelloWorld msg="You did it!" /><nav><RouterLink to="/">Home</RouterLink><RouterLink to="/about">About</RouterLink></nav></div></header><el-button type="primary">Primary</el-button><RouterView />
</template><style scoped>
header {line-height: 1.5;max-height: 100vh;
}.logo {display: block;margin: 0 auto 2rem;
}nav {width: 100%;font-size: 12px;text-align: center;margin-top: 2rem;
}nav a.router-link-exact-active {color: var(--color-text);
}nav a.router-link-exact-active:hover {background-color: transparent;
}nav a {display: inline-block;padding: 0 1rem;border-left: 1px solid var(--color-border);
}nav a:first-of-type {border: 0;
}@media (min-width: 1024px) {header {display: flex;place-items: center;padding-right: calc(var(--section-gap) / 2);}.logo {margin: 0 2rem 0 0;}header .wrapper {display: flex;place-items: flex-start;flex-wrap: wrap;}nav {text-align: left;margin-left: -1rem;font-size: 1rem;padding: 1rem 0;margin-top: 1rem;}
}
</style>
测试以下elementplus是否生效。
在app.vue的template下引入
<el-button type="primary">Primary</el-button>
重启运行。
测试完成。
elementPlus主题设置
安装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
import { fileURLToPath, URL } from 'node:url'import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
//elementplus按需导入
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [//配置elementPlus采用sass样式配色系统ElementPlusResolver({importStyle:"sass"}),],}),],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},css: {preprocessorOptions: {scss: {// 自动导入定制化样式文件进行样式覆盖additionalData: `@use "@/styles/element/index.scss" as *;`,}},}
})
测试
重启运行
配置axios
安装axios
npm i axios
配置
utils/http.js
import axios from 'axios'// 创建axios实例
let 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
测试接口
api/testAPI.js下
import http from '@/utils/http'export function getCategory () {return http({url: 'home/category/head'})
}
main.js下新增
//测试接口
import {getCategory} from '@/api/testAPI'
getCategory().then(res => {console.log(res)
})
重启运行
路由
views/Login/index.vue
<template>我是登录页
</template>
views/Layout/index.vue
<template>我是首页
</template>
views/Home/index.vue
<template>我是home页
</template>
views/Category/index.vue
<template>我是分类页
</template>
router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '@/views/login/index.vue'
import Layout from '@/views/layout/index.vue'
import Home from '@/views/home/index.vue'
import Category from '@/views/category/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',component: Layout,children: [{path: '',component: Home},{path: 'category',component: Category}]},{path: '/login',component: Login}]
})export default router
重启输入对应url可以看到不同页面。
引入静态资源
- 图片资源 - 把 images 文件夹放到 assets 目录下
- 样式资源 - 把 common.scss 文件放到 styles 目录下
链接:https://pan.baidu.com/s/15PoJhfpPDzf_WTsakO0jmg?pwd=3kgh
提取码:3kgh
自动导入scss变量
var.scss
$xtxColor: #27ba9b;
$helpColor: #e26237;
$sucColor: #1dc779;
$warnColor: #ffb302;
$priceColor: #cf4444;
修改main.js
css: {preprocessorOptions: {scss: {// 自动导入scss文件additionalData: `@use "@/styles/element/index.scss" as *;@use "@/styles/var.scss" as *;`,}}
}
测试
修改App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script><template><RouterView /><div class="test">test</div>
</template><style scoped lang="scss">
header {line-height: 1.5;max-height: 100vh;
}
.test {color: $priceColor;
}
.logo {display: block;margin: 0 auto 2rem;
}nav {width: 100%;font-size: 12px;text-align: center;margin-top: 2rem;
}nav a.router-link-exact-active {color: var(--color-text);
}nav a.router-link-exact-active:hover {background-color: transparent;
}nav a {display: inline-block;padding: 0 1rem;border-left: 1px solid var(--color-border);
}nav a:first-of-type {border: 0;
}@media (min-width: 1024px) {header {display: flex;place-items: center;padding-right: calc(var(--section-gap) / 2);}.logo {margin: 0 2rem 0 0;}header .wrapper {display: flex;place-items: flex-start;flex-wrap: wrap;}nav {text-align: left;margin-left: -1rem;font-size: 1rem;padding: 1rem 0;margin-top: 1rem;}
}
</style>
测试成功,还原代码。
Layout页
组件结构快速搭建
LayoutNav.vue
<script setup></script><template><nav class="app-topnav"><div class="container"><ul><template v-if="true"><li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li><li><el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消"><template #reference><a href="javascript:;">退出登录</a></template></el-popconfirm></li><li><a href="javascript:;">我的订单</a></li><li><a href="javascript:;">会员中心</a></li></template><template v-else><li><a href="javascript:;">请先登录</a></li><li><a href="javascript:;">帮助中心</a></li><li><a href="javascript:;">关于我们</a></li></template></ul></div></nav>
</template><style scoped lang="scss">
.app-topnav {background: #333;ul {display: flex;height: 53px;justify-content: flex-end;align-items: center;li {a {padding: 0 15px;color: #cdcdcd;line-height: 1;display: inline-block;i {font-size: 14px;margin-right: 2px;}&:hover {color: $xtxColor;}}~li {a {border-left: 2px solid #666;}}}}
}
</style>
LayoutHeader.vue
<script setup></script><template><header class='app-header'><div class="container"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><ul class="app-header-nav"><li class="home"><RouterLink to="/">首页</RouterLink></li><li> <RouterLink to="/">居家</RouterLink> </li><li> <RouterLink to="/">美食</RouterLink> </li><li> <RouterLink to="/">服饰</RouterLink> </li></ul><div class="search"><i class="iconfont icon-search"></i><input type="text" placeholder="搜一搜"></div><!-- 头部购物车 --></div></header>
</template><style scoped lang='scss'>
.app-header {background: #fff;.container {display: flex;align-items: center;}.logo {width: 200px;a {display: block;height: 132px;width: 100%;text-indent: -9999px;background: url('@/assets/images/logo.png') no-repeat center 18px / contain;}}.app-header-nav {width: 820px;display: flex;padding-left: 40px;position: relative;z-index: 998;li {margin-right: 40px;width: 38px;text-align: center;a {font-size: 16px;line-height: 32px;height: 32px;display: inline-block;&:hover {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}.active {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}}.search {width: 170px;height: 32px;position: relative;border-bottom: 1px solid #e7e7e7;line-height: 32px;.icon-search {font-size: 18px;margin-left: 5px;}input {width: 140px;padding-left: 5px;color: #666;}}.cart {width: 50px;.curr {height: 32px;line-height: 32px;text-align: center;position: relative;display: block;.icon-cart {font-size: 22px;}em {font-style: normal;position: absolute;right: 0;top: 0;padding: 1px 6px;line-height: 1;background: $helpColor;color: #fff;font-size: 12px;border-radius: 10px;font-family: Arial;}}}
}
</style>
LayoutFooter.vue
<template><footer class="app_footer"><!-- 联系我们 --><div class="contact"><div class="container"><dl><dt>客户服务</dt><dd><i class="iconfont icon-kefu"></i> 在线客服</dd><dd><i class="iconfont icon-question"></i> 问题反馈</dd></dl><dl><dt>关注我们</dt><dd><i class="iconfont icon-weixin"></i> 公众号</dd><dd><i class="iconfont icon-weibo"></i> 微博</dd></dl><dl><dt>下载APP</dt><dd class="qrcode"><img src="@/assets/images/qrcode.jpg" /></dd><dd class="download"><span>扫描二维码</span><span>立马下载APP</span><a href="javascript:;">下载页面</a></dd></dl><dl><dt>服务热线</dt><dd class="hotline">400-0000-000 <small>周一至周日 8:00-18:00</small></dd></dl></div></div><!-- 其它 --><div class="extra"><div class="container"><div class="slogan"><a href="javascript:;"><i class="iconfont icon-footer01"></i><span>价格亲民</span></a><a href="javascript:;"><i class="iconfont icon-footer02"></i><span>物流快捷</span></a><a href="javascript:;"><i class="iconfont icon-footer03"></i><span>品质新鲜</span></a></div><!-- 版权信息 --><div class="copyright"><p><a href="javascript:;">关于我们</a><a href="javascript:;">帮助中心</a><a href="javascript:;">售后服务</a><a href="javascript:;">配送与验收</a><a href="javascript:;">商务合作</a><a href="javascript:;">搜索推荐</a><a href="javascript:;">友情链接</a></p><p>CopyRight © 小兔鲜儿</p></div></div></div></footer>
</template><style scoped lang='scss'>
.app_footer {overflow: hidden;background-color: #f5f5f5;padding-top: 20px;.contact {background: #fff;.container {padding: 60px 0 40px 25px;display: flex;}dl {height: 190px;text-align: center;padding: 0 72px;border-right: 1px solid #f2f2f2;color: #999;&:first-child {padding-left: 0;}&:last-child {border-right: none;padding-right: 0;}}dt {line-height: 1;font-size: 18px;}dd {margin: 36px 12px 0 0;float: left;width: 92px;height: 92px;padding-top: 10px;border: 1px solid #ededed;.iconfont {font-size: 36px;display: block;color: #666;}&:hover {.iconfont {color: $xtxColor;}}&:last-child {margin-right: 0;}}.qrcode {width: 92px;height: 92px;padding: 7px;border: 1px solid #ededed;}.download {padding-top: 5px;font-size: 14px;width: auto;height: auto;border: none;span {display: block;}a {display: block;line-height: 1;padding: 10px 25px;margin-top: 5px;color: #fff;border-radius: 2px;background-color: $xtxColor;}}.hotline {padding-top: 20px;font-size: 22px;color: #666;width: auto;height: auto;border: none;small {display: block;font-size: 15px;color: #999;}}}.extra {background-color: #333;}.slogan {height: 178px;line-height: 58px;padding: 60px 100px;border-bottom: 1px solid #434343;display: flex;justify-content: space-between;a {height: 58px;line-height: 58px;color: #fff;font-size: 28px;i {font-size: 50px;vertical-align: middle;margin-right: 10px;font-weight: 100;}span {vertical-align: middle;text-shadow: 0 0 1px #333;}}}.copyright {height: 170px;padding-top: 40px;text-align: center;color: #999;font-size: 15px;p {line-height: 1;margin-bottom: 20px;}a {color: #999;line-height: 1;padding: 0 10px;border-right: 1px solid #999;&:last-child {border-right: none;}}}
}
</style>
Layout/index.vue
<script setup>
import LayoutNav from './components/LayoutNav.vue'
import LayoutHeader from './components/LayoutHeader.vue'
import LayoutFooter from './components/LayoutFooter.vue'
</script><template><LayoutNav /><LayoutHeader /><RouterView /><LayoutFooter />
</template>
重启项目
字体图标渲染
根目录下index.html引入
<link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">
一级导航渲染
api/layout.js
import httpInstance from '@/utils/http'export function getCategoryAPI () {return httpInstance({url: '/home/category/head'})
}
LayoutHeader.vue
<script setup>import { getCategoryAPI } from '@/api/layout'import { onMounted, ref } from 'vue'const categoryList = ref([])const getCategory = async () => {const res = await getCategoryAPI()categoryList.value = res.result}onMounted(() => getCategory())</script><template><header class='app-header'><div class="container"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><ul class="app-header-nav"><li class="home" v-for="item in categoryList" :key="item.id"><RouterLink to="/">{{ item.name }}</RouterLink></li></ul><div class="search"><i class="iconfont icon-search"></i><input type="text" placeholder="搜一搜"></div><!-- 头部购物车 --></div></header>
</template><style scoped lang='scss'>
.app-header {background: #fff;.container {display: flex;align-items: center;}.logo {width: 200px;a {display: block;height: 132px;width: 100%;text-indent: -9999px;background: url('@/assets/images/logo.png') no-repeat center 18px / contain;}}.app-header-nav {width: 820px;display: flex;padding-left: 40px;position: relative;z-index: 998;li {margin-right: 40px;width: 38px;text-align: center;a {font-size: 16px;line-height: 32px;height: 32px;display: inline-block;&:hover {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}.active {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}}.search {width: 170px;height: 32px;position: relative;border-bottom: 1px solid #e7e7e7;line-height: 32px;.icon-search {font-size: 18px;margin-left: 5px;}input {width: 140px;padding-left: 5px;color: #666;}}.cart {width: 50px;.curr {height: 32px;line-height: 32px;text-align: center;position: relative;display: block;.icon-cart {font-size: 22px;}em {font-style: normal;position: absolute;right: 0;top: 0;padding: 1px 6px;line-height: 1;background: $helpColor;color: #fff;font-size: 12px;border-radius: 10px;font-family: Arial;}}}
}
</style>
吸顶导航交互实现
实现吸顶交互
<script setup>import { useScroll } from '@vueuse/core'const { y } = useScroll(window)
</script>
components/LayoutFixed.vue
<script setup>import { useScroll } from '@vueuse/core'const { y } = useScroll(window)
</script><template><div class="app-header-sticky" :class="{ show: y > 78 }"><div class="container"><RouterLink class="logo" to="/" /><!-- 导航区域 --><ul class="app-header-nav "><li class="home"><RouterLink to="/">首页</RouterLink></li><li><RouterLink to="/">居家</RouterLink></li><li><RouterLink to="/">美食</RouterLink></li><li><RouterLink to="/">服饰</RouterLink></li><li><RouterLink to="/">母婴</RouterLink></li><li><RouterLink to="/">个护</RouterLink></li><li><RouterLink to="/">严选</RouterLink></li><li><RouterLink to="/">数码</RouterLink></li><li><RouterLink to="/">运动</RouterLink></li><li><RouterLink to="/">杂项</RouterLink></li></ul><div class="right"><RouterLink to="/">品牌</RouterLink><RouterLink to="/">专题</RouterLink></div></div></div>
</template><style scoped lang='scss'>
.app-header-sticky {width: 100%;height: 80px;position: fixed;left: 0;top: 0;z-index: 999;background-color: #fff;border-bottom: 1px solid #e4e4e4;// 此处为关键样式!!!// 状态一:往上平移自身高度 + 完全透明transform: translateY(-100%);opacity: 0;// 状态二:移除平移 + 完全不透明&.show {transition: all 0.3s linear;transform: none;opacity: 1;}.container {display: flex;align-items: center;}.logo {width: 200px;height: 80px;background: url("@/assets/images/logo.png") no-repeat right 2px;background-size: 160px auto;}.right {width: 220px;display: flex;text-align: center;padding-left: 40px;border-left: 2px solid $xtxColor;a {width: 38px;margin-right: 40px;font-size: 16px;line-height: 1;&:hover {color: $xtxColor;}}}
}.app-header-nav {width: 820px;display: flex;padding-left: 40px;position: relative;z-index: 998;li {margin-right: 40px;width: 38px;text-align: center;a {font-size: 16px;line-height: 32px;height: 32px;display: inline-block;&:hover {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}.active {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}
}
</style>
Pinia优化重复请求
两个导航栏会分别发送一个请求,可以利用pinia只发送一个请求。
stores/category.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { getCategoryAPI } from '@/api/layout'
export const useCategoryStore = defineStore('category', () => {// 导航列表的数据管理// state 导航列表数据const categoryList = ref([])// action 获取导航数据的方法const getCategory = async () => {const res = await getCategoryAPI()categoryList.value = res.result}return {categoryList,getCategory}
})
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 './components/LayoutFixed.vue'//触发导航列表的action
import {useCategoryStore} from '@/stores/category'
import {onMounted} from 'vue'
const categoryStore = useCategoryStore()
onMounted(() => categoryStore.getCategory())
</script><template><LayoutFixed /><LayoutNav /><LayoutHeader /><RouterView /><LayoutFooter />
</template>
layout/components/LayoutFixed.vue
<script setup>import {useCategoryStore} from '@/stores/category'import { useScroll } from '@vueuse/core'const { y } = useScroll(window)//使用pinia中的数据const categoryStore = useCategoryStore()</script><template><div class="app-header-sticky" :class="{ show: y > 78 }"><div class="container"><RouterLink class="logo" to="/" /><!-- 导航区域 --><ul class="app-header-nav "><li class="home" v-for="item in categoryStore.categoryList" :key="item.id"><RouterLink to="/">{{ item.name }}</RouterLink></li></ul><div class="right"><RouterLink to="/">品牌</RouterLink><RouterLink to="/">专题</RouterLink></div></div></div>
</template><style scoped lang='scss'>
.app-header-sticky {width: 100%;height: 80px;position: fixed;left: 0;top: 0;z-index: 999;background-color: #fff;border-bottom: 1px solid #e4e4e4;// 此处为关键样式!!!// 状态一:往上平移自身高度 + 完全透明transform: translateY(-100%);opacity: 0;// 状态二:移除平移 + 完全不透明&.show {transition: all 0.3s linear;transform: none;opacity: 1;}.container {display: flex;align-items: center;}.logo {width: 200px;height: 80px;background: url("@/assets/images/logo.png") no-repeat right 2px;background-size: 160px auto;}.right {width: 220px;display: flex;text-align: center;padding-left: 40px;border-left: 2px solid $xtxColor;a {width: 38px;margin-right: 40px;font-size: 16px;line-height: 1;&:hover {color: $xtxColor;}}}
}.app-header-nav {width: 820px;display: flex;padding-left: 40px;position: relative;z-index: 998;li {margin-right: 40px;width: 38px;text-align: center;a {font-size: 16px;line-height: 32px;height: 32px;display: inline-block;&:hover {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}.active {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}
}
</style>
layout/components/LayoutHeader.vue
<script setup>import {useCategoryStore} from '@/stores/category'import { useScroll } from '@vueuse/core'const { y } = useScroll(window)//使用pinia中的数据const categoryStore = useCategoryStore()</script><template><div class="app-header-sticky" :class="{ show: y > 78 }"><div class="container"><RouterLink class="logo" to="/" /><!-- 导航区域 --><ul class="app-header-nav "><li class="home" v-for="item in categoryStore.categoryList" :key="item.id"><RouterLink to="/">{{ item.name }}</RouterLink></li></ul><div class="right"><RouterLink to="/">品牌</RouterLink><RouterLink to="/">专题</RouterLink></div></div></div>
</template><style scoped lang='scss'>
.app-header-sticky {width: 100%;height: 80px;position: fixed;left: 0;top: 0;z-index: 999;background-color: #fff;border-bottom: 1px solid #e4e4e4;// 此处为关键样式!!!// 状态一:往上平移自身高度 + 完全透明transform: translateY(-100%);opacity: 0;// 状态二:移除平移 + 完全不透明&.show {transition: all 0.3s linear;transform: none;opacity: 1;}.container {display: flex;align-items: center;}.logo {width: 200px;height: 80px;background: url("@/assets/images/logo.png") no-repeat right 2px;background-size: 160px auto;}.right {width: 220px;display: flex;text-align: center;padding-left: 40px;border-left: 2px solid $xtxColor;a {width: 38px;margin-right: 40px;font-size: 16px;line-height: 1;&:hover {color: $xtxColor;}}}
}.app-header-nav {width: 820px;display: flex;padding-left: 40px;position: relative;z-index: 998;li {margin-right: 40px;width: 38px;text-align: center;a {font-size: 16px;line-height: 32px;height: 32px;display: inline-block;&:hover {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}.active {color: $xtxColor;border-bottom: 1px solid $xtxColor;}}
}
</style>
发现只有一次请求。
Home页
在home/components下新建5个vue文件
- HomeCategory
<script setup>
</script><template><div> 我是分类 </div>
</template>
- HomeBanner
<script setup>
</script><template><div> 我是banner </div>
</template>
- HomeNew
<script setup>
</script><template><div> 人气推荐 </div>
</template>
- HomeHot
<script setup>
</script><template><div> 新鲜好物 </div>
</template>
- HomeProduct
<script setup>
</script><template><div> 产品列表 </div>
</template>
home/index.vue
<script setup>
import HomeCategory from './components/HomeCategory.vue'
import HomeBanner from './components/HomeBanner.vue'
import HomeNew from './components/HomeNew.vue'
import HomeHot from './components/HomeHot.vue'
import HomeProduct from './components/HomeProduct.vue'
</script><template><div class="container"><HomeCategory /><HomeBanner /></div><HomeNew /><HomeHot /><HomeProduct />
</template>
分类实现
home/components/HomeCategory.vue
<script setup>import {useCategoryStore} from '@/stores/category'const categoryStore = useCategoryStore()
</script><template><div class="home-category"><ul class="menu"><li v-for="item in categoryStore.categoryList" :key="item.id"><RouterLink to="/">{{item.name}}</RouterLink><RouterLink v-for="i in item.children.slice(0,2)" :key="i" to="/">{{i.name}}</RouterLink><!-- 弹层layer位置 --><div class="layer"><h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4><ul><li v-for="i in item.goods" :key="i.id"><RouterLink to="/"><img :src="i.picture" alt="" /><div class="info"><p class="name ellipsis-2">{{i.name}}</p><p class="desc ellipsis">{{i.desc}}</p><p class="price"><i>¥</i>{{i.price}}</p></div></RouterLink></li></ul></div></li></ul></div>
</template><style scoped lang='scss'>
.home-category {width: 250px;height: 500px;background: rgba(0, 0, 0, 0.8);position: relative;z-index: 99;.menu {li {padding-left: 40px;height: 55px;line-height: 55px;&:hover {background: $xtxColor;}a {margin-right: 4px;color: #fff;&:first-child {font-size: 16px;}}.layer {width: 990px;height: 500px;background: rgba(255, 255, 255, 0.8);position: absolute;left: 250px;top: 0;display: none;padding: 0 15px;h4 {font-size: 20px;font-weight: normal;line-height: 80px;small {font-size: 16px;color: #666;}}ul {display: flex;flex-wrap: wrap;li {width: 310px;height: 120px;margin-right: 15px;margin-bottom: 15px;border: 1px solid #eee;border-radius: 4px;background: #fff;&:nth-child(3n) {margin-right: 0;}a {display: flex;width: 100%;height: 100%;align-items: center;padding: 10px;&:hover {background: #e3f9f4;}img {width: 95px;height: 95px;}.info {padding-left: 10px;line-height: 24px;overflow: hidden;.name {font-size: 16px;color: #666;}.desc {color: #999;}.price {font-size: 22px;color: $priceColor;i {font-size: 16px;}}}}}}}// 关键样式 hover状态下的layer盒子变成block&:hover {.layer {display: block;}}}}
}
</style>
banner轮播图
home/components/HomeBanner.vue
<script setup></script><template><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in 4" :key="item"><img src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/6d202d8e-bb47-4f92-9523-f32ab65754f4.jpg" alt=""></el-carousel-item></el-carousel></div>
</template><style scoped lang='scss'>
.home-banner {width: 1240px;height: 500px;position: absolute;left: 0;top: 0;z-index: 98;img {width: 100%;height: 500px;}
}
</style>
获取数据
api/home.js
import httpInstance from '@/utils/http'
export function getBannerAPI (){return httpInstance({url:'home/banner'})
}
home/components/HomeBanner.vue
<script setup>
import { getBannerAPI } from '@/api/home'
import { onMounted, ref } from 'vue'const bannerList = ref([])const getBanner = async () => {const res = await getBannerAPI()console.log(res)bannerList.value = res.result
}onMounted(() => getBanner())</script><template><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in bannerList" :key="item.id"><img :src="item.imgUrl" alt=""></el-carousel-item></el-carousel></div>
</template><style scoped lang='scss'>
.home-banner {width: 1240px;height: 500px;position: absolute;left: 0;top: 0;z-index: 98;img {width: 100%;height: 500px;}
}
</style>
轮播图就实现了。
新鲜好物实现
home/components/HomePanel.vue
<script setup>defineProps({title: {type: String,default: ''},subTitle: {type: String,default: ''}
})</script><template><div class="home-panel"><div class="container"><div class="head"><!-- 主标题和副标题 --><h3>{{ title }}<small>{{ subTitle }}</small></h3></div><!-- 主体内容区域 --><slot name="main" /></div></div>
</template><style scoped lang='scss'>
.home-panel {background-color: #fff;.head {padding: 40px 0;display: flex;align-items: flex-end;h3 {flex: 1;font-size: 32px;font-weight: normal;margin-left: 6px;height: 35px;line-height: 35px;small {font-size: 16px;color: #999;margin-left: 20px;}}}
}
</style>
api/home添加:
/*** @description: 获取新鲜好物* @param {*}* @return {*}*/
export const getNewAPI = () => {return httpInstance({url:'/home/new'})
}
home/components/HomeNew.vue
<script setup>
import HomePanel from './HomePanel.vue'
import { getNewAPI } from '@/api/home'
import { ref } from 'vue'
const newList = ref([])
const getNewList = async () => {const res = await getNewAPI()newList.value = res.result
}getNewList()
</script><template><HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱"><template #main><ul class="goods-list"><li v-for="item in newList" :key="item.id"><RouterLink :to="`/detail/${item.id}`"><img :src="item.picture" alt="" /><p class="name">{{ item.name }}</p><p class="price">¥{{ item.price }}</p></RouterLink></li></ul></template></HomePanel>
</template><style scoped lang='scss'>
.goods-list {display: flex;justify-content: space-between;height: 406px;li {width: 306px;height: 406px;background: #f0f9f4;transition: all .5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 306px;height: 306px;}p {font-size: 22px;padding-top: 12px;text-align: center;text-overflow: ellipsis;overflow: hidden;white-space: nowrap;}.price {color: $priceColor;}}
}
</style>
人气推荐实现
api/home添加:
/*** @description: 获取人气推荐* @param {*}* @return {*}*/
export const getHotAPI = () => {return httpInstance({url:'/home/hot'})
}
home/components/HomeHot.vue
<script setup>
import HomePanel from './HomePanel.vue'
import { getHotAPI } from '@/api/home'
import { ref } from 'vue'
const hotList = ref([])
const getHotList = async () => {const res = await getHotAPI()hotList.value = res.result
}
getHotList()
console.log(hotList)</script><template><HomePanel title="人气推荐" sub-title="人气爆款 不容错过"><template #main><ul class="goods-list"><li v-for="item in hotList" :key="item.id"><RouterLink :to="`/detail/${item.id}`"><img :src="item.picture" alt="" /><p class="name">{{ item.title }}</p><p class="desc">{{ item.alt }}</p></RouterLink></li></ul></template></HomePanel>
</template><style scoped lang='scss'>
.goods-list {display: flex;justify-content: space-between;height: 426px;li {width: 306px;height: 406px;transition: all .5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 306px;height: 306px;}p {font-size: 22px;padding-top: 12px;text-align: center;}.desc {color: #999;font-size: 18px;}}
}
</style>
懒加载指令实现
directives/index.js
// 定义懒加载插件
import { useIntersectionObserver } from '@vueuse/core'export const directivePlugin = {install (app) {// 懒加载指令逻辑app.directive('img-lazy', {mounted (el, binding) {// el: 指令绑定的那个元素 img// binding: binding.value 指令等于号后面绑定的表达式的值 图片urlconsole.log(el, binding.value)useIntersectionObserver(el,([{ isIntersecting }]) => {console.log(isIntersecting)if (isIntersecting) {// 进入视口区域el.src = binding.valuestop()}},)}})}
}
修改main.js
app.use(createPinia())
app.use(router)
app.use(directivePlugin)
app.mount('#app')
产品列表实现
api/home.js新增
/*** @description: 获取所有商品模块* @param {*}* @return {*}*/
export const getGoodsAPI = () => {return httpInstance({url: '/home/goods'})
}
home/components/HomeProduct.vue
<script setup>
import HomePanel from './HomePanel.vue'
import {getGoodsAPI} from '@/api/home'
import {onMounted,ref} from 'vue'//获取数据列表
const goodsProduct = ref([])
const getGoods = async () => {const res = await getGoodsAPI()goodsProduct.value = res.result
}onMounted(()=>getGoods())</script><template><div class="home-product"><HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id"><template #main><div class="box"><RouterLink class="cover" to="/"><img :src="cate.picture" /><strong class="label"><span>{{ cate.name }}馆</span><span>{{ cate.saleInfo }}</span></strong></RouterLink><ul class="goods-list"><li v-for="good in cate.goods" :key="good.id"><RouterLink to="/" class="goods-item"><img :src="good.picture" alt="" /><p class="name ellipsis">{{ good.name }}</p><p class="desc ellipsis">{{ good.desc }}</p><p class="price">¥{{ good.price }}</p></RouterLink></li></ul></div></template></HomePanel></div>
</template><style scoped lang='scss'>
.home-product {background: #fff;margin-top: 20px;.sub {margin-bottom: 2px;a {padding: 2px 12px;font-size: 16px;border-radius: 4px;&:hover {background: $xtxColor;color: #fff;}&:last-child {margin-right: 80px;}}}.box {display: flex;.cover {width: 240px;height: 610px;margin-right: 10px;position: relative;img {width: 100%;height: 100%;}.label {width: 188px;height: 66px;display: flex;font-size: 18px;color: #fff;line-height: 66px;font-weight: normal;position: absolute;left: 0;top: 50%;transform: translate3d(0, -50%, 0);span {text-align: center;&:first-child {width: 76px;background: rgba(0, 0, 0, 0.9);}&:last-child {flex: 1;background: rgba(0, 0, 0, 0.7);}}}}.goods-list {width: 990px;display: flex;flex-wrap: wrap;li {width: 240px;height: 300px;margin-right: 10px;margin-bottom: 10px;&:nth-last-child(-n + 4) {margin-bottom: 0;}&:nth-child(4n) {margin-right: 0;}}}.goods-item {display: block;width: 220px;padding: 20px 30px;text-align: center;transition: all .5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}}}
}
</style>
这里没有用懒加载的形式,因为总有一些图片加载不出来,原因不明。
GoodsItem组件封装
home/components/GoodsItem.vue
<script setup>
defineProps({goods: {type: Object,default: () => { }}
})
</script><template><RouterLink to="/" class="goods-item"><img :src="goods.picture" alt="" /><p class="name ellipsis">{{ goods.name }}</p><p class="desc ellipsis">{{ goods.desc }}</p><p class="price">¥{{ goods.price }}</p></RouterLink>
</template><style scoped lang="scss">
.goods-item {display: block;width: 220px;padding: 20px 30px;text-align: center;transition: all .5s;&:hover {transform: translate3d(0, -3px, 0);box-shadow: 0 3px 8px rgb(0 0 0 / 20%);}img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}
}
</style>
home/components/HomeProduct.vue修改
import GoodsItem from './GoodsItem.vue'<ul class="goods-list"><li v-for="good in cate.goods" :key="good.id"><GoodsItem :goods="good"/></li>
</ul>
一级分类页
导航栏
router/index.js修改
children: [{path: '',component: Home},{path: 'category/:id',component: Category}]
layout/components/LayoutHeader.vue修改
layout/components/LayoutFixed.vue修改
<RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
api/category.vue
import httpInstance from '@/utils/http'/*** @description: 获取分类数据* @param {*} id 分类id * @return {*}*/
export const findTopCategoryAPI = (id) => {return httpInstance({url:'/category',params:{id}})
}
views/category/index.vue
<script setup>import { findTopCategoryAPI } from '@/api/category'import {ref} from 'vue'import {useRoute} from 'vue-router'const categoryData = ref({})const route = useRoute()const getCategory = async (id) => {// 如何在setup中获取路由参数 useRoute() -> route 等价于this.$routeconst res = await findTopCategoryAPI(id)categoryData.value = res.result}getCategory(route.params.id)
</script><template><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb></div>
</template><style scoped lang="scss">
.top-category {h3 {font-size: 28px;color: #666;font-weight: normal;text-align: center;line-height: 100px;}.sub-list {margin-top: 20px;background-color: #fff;ul {display: flex;padding: 0 32px;flex-wrap: wrap;li {width: 168px;height: 160px;a {text-align: center;display: block;font-size: 16px;img {width: 100px;height: 100px;}p {line-height: 40px;}&:hover {color: $xtxColor;}}}}}.ref-goods {background-color: #fff;margin-top: 20px;position: relative;.head {.xtx-more {position: absolute;top: 20px;right: 20px;}.tag {text-align: center;color: #999;font-size: 20px;position: relative;top: -20px;}}.body {display: flex;justify-content: space-around;padding: 0 40px 30px;}}.bread-container {padding: 25px 0;}
}
</style>
分类Banner渲染
api/home.js修改
export function getBannerAPI (params = {}) {// 默认为1 商品为2const { distributionSite = '1' } = paramsreturn httpInstance({url: '/home/banner',params: {distributionSite}})
}
category/index.vue
<script setup>import { findTopCategoryAPI } from '@/api/category'import {ref,onMounted} from 'vue'import {useRoute} from 'vue-router'import { getBannerAPI } from '@/api/home'const categoryData = ref({})const route = useRoute()const getCategory = async (id) => {// 如何在setup中获取路由参数 useRoute() -> route 等价于this.$routeconst res = await findTopCategoryAPI(id)categoryData.value = res.result}getCategory(route.params.id)//获取bannerconst bannerList = ref([])const getBanner = async () => {const res = await getBannerAPI()console.log(res)bannerList.value = res.result}onMounted(() => getBanner())
</script><template><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb></div><!-- 轮播图 --><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in bannerList" :key="item.id"><img :src="item.imgUrl" alt=""></el-carousel-item></el-carousel></div>
</template><style scoped lang="scss">
.home-banner {width: 1240px;height: 500px;margin: 0 auto;z-index: 98;img {width: 100%;height: 500px;}
}
.top-category {h3 {font-size: 28px;color: #666;font-weight: normal;text-align: center;line-height: 100px;}.sub-list {margin-top: 20px;background-color: #fff;ul {display: flex;padding: 0 32px;flex-wrap: wrap;li {width: 168px;height: 160px;a {text-align: center;display: block;font-size: 16px;img {width: 100px;height: 100px;}p {line-height: 40px;}&:hover {color: $xtxColor;}}}}}.ref-goods {background-color: #fff;margin-top: 20px;position: relative;.head {.xtx-more {position: absolute;top: 20px;right: 20px;}.tag {text-align: center;color: #999;font-size: 20px;position: relative;top: -20px;}}.body {display: flex;justify-content: space-around;padding: 0 40px 30px;}}.bread-container {padding: 25px 0;}
}
</style>
导航激活
layout/components/LayoutHeader.vue修改
layout/components/LayoutFixed.vue修改
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
分类列表渲染
category/index.vue
<script setup>import { findTopCategoryAPI } from '@/api/category'import {ref,onMounted} from 'vue'import {useRoute} from 'vue-router'import { getBannerAPI } from '@/api/home'import GoodsItem from '../home/components/GoodsItem.vue'const categoryData = ref({})const route = useRoute()const getCategory = async (id) => {// 如何在setup中获取路由参数 useRoute() -> route 等价于this.$routeconst res = await findTopCategoryAPI(id)categoryData.value = res.result}getCategory(route.params.id)//获取bannerconst bannerList = ref([])const getBanner = async () => {const res = await getBannerAPI()console.log(res)bannerList.value = res.result}onMounted(() => getBanner())
</script><template><!-- 分类 --><div class="top-category"><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item></el-breadcrumb></div><!-- 轮播图 --><div class="home-banner"><el-carousel height="500px"><el-carousel-item v-for="item in bannerList" :key="item.id"><img :src="item.imgUrl" alt=""></el-carousel-item></el-carousel></div><div class="sub-list"><h3>全部分类</h3><ul><li v-for="i in categoryData.children" :key="i.id"><RouterLink to="/"><img :src="i.picture" /><p>{{ i.name }}</p></RouterLink></li></ul></div><div class="ref-goods" v-for="item in categoryData.children" :key="item.id"><div class="head"><h3>- {{ item.name }}-</h3></div><div class="body"><GoodsItem v-for="good in item.goods" :goods="good" :key="good.id" /></div></div></div>
</template><style scoped lang="scss">
.top-category {width: 1240px;margin: 0 auto;h3 {font-size: 28px;color: #666;font-weight: normal;text-align: center;line-height: 100px;}.bread-container {padding: 25px 0;}.home-banner {width: 1240px;height: 500px;margin: 0 auto;z-index: 98;img {width: 100%;height: 500px;}}.sub-list {margin-top: 20px;background-color: #fff;ul {display: flex;padding: 0 32px;flex-wrap: wrap;justify-content: space-around;li {width: 168px;height: 160px;a {text-align: center;display: block;font-size: 16px;img {width: 100px;height: 100px;}p {line-height: 40px;}&:hover {color: $xtxColor;}}}}}.ref-goods {background-color: #fff;margin-top: 20px;position: relative;.head {.xtx-more {position: absolute;top: 20px;right: 20px;}.tag {text-align: center;color: #999;font-size: 20px;position: relative;top: -20px;}}.body {display: flex;justify-content: space-around;padding: 0 40px 30px;}}
}
</style>
路由缓存问题
banner与分类商品在刷新的时候都会请求一次,但banner是没必要刷新的,且导航栏需要刷新才能更新。
category/index.vue修改
import {useRoute,onBeforeRouteUpdate} from 'vue-router'// 目标:路由参数变化的时候 可以把分类数据接口重新发送onBeforeRouteUpdate((to) => {// 存在问题:使用最新的路由参数请求最新的分类数据getCategory(to.params.id)})
业务逻辑的函数拆分
将banner和分类抽象出来。
useBanner.js
// 封装banner轮播图相关的业务代码
import { ref, onMounted } from 'vue'
import { getBannerAPI } from '@/api/home'export function useBanner () {//获取bannerconst bannerList = ref([])const getBanner = async () => {const res = await getBannerAPI()console.log(res)bannerList.value = res.result}onMounted(() => getBanner())return {bannerList}
}
useCategory.js
// 封装分类数据业务相关代码
import { onMounted, ref } from 'vue'
import { findTopCategoryAPI } from '@/api/category'
import { useRoute } from 'vue-router'
import { onBeforeRouteUpdate } from 'vue-router'export function useCategory () {const categoryData = ref({})const route = useRoute()const getCategory = async (id) => {// 如何在setup中获取路由参数 useRoute() -> route 等价于this.$routeconst res = await findTopCategoryAPI(id)categoryData.value = res.result}getCategory(route.params.id)// 目标:路由参数变化的时候 可以把分类数据接口重新发送onBeforeRouteUpdate((to) => {// 存在问题:使用最新的路由参数请求最新的分类数据getCategory(to.params.id)})return {categoryData}
}
index.vue修改
<script setup>import GoodsItem from '../home/components/GoodsItem.vue'import {useBanner} from './composables/useBanner'import { useCategory } from './composables/useCategory'const {bannerList} = useBanner()const { categoryData } = useCategory()
</script>
二级分类页
实现
views下新建
index.vue
<script setup></script><template><div class="container "><!-- 面包屑 --><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: '/' }">居家</el-breadcrumb-item><el-breadcrumb-item>居家生活用品</el-breadcrumb-item></el-breadcrumb></div><div class="sub-container"><el-tabs><el-tab-pane label="最新商品" name="publishTime"></el-tab-pane><el-tab-pane label="最高人气" name="orderNum"></el-tab-pane><el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane></el-tabs><div class="body"><!-- 商品列表--></div></div></div></template><style lang="scss" scoped>
.bread-container {padding: 25px 0;color: #666;
}.sub-container {padding: 20px 10px;background-color: #fff;.body {display: flex;flex-wrap: wrap;padding: 0 10px;}.goods-item {display: block;width: 220px;margin-right: 20px;padding: 20px 30px;text-align: center;img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}}.pagination-container {margin-top: 20px;display: flex;justify-content: center;}}
</style>
router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '@/views/login/index.vue'
import Layout from '@/views/layout/index.vue'
import Home from '@/views/home/index.vue'
import Category from '@/views/category/index.vue'
import SubCategory from '@/views/subCategory/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',component: Layout,children: [{path: '',component: Home},{path: 'category/:id',component: Category},{path: 'category/sub/:id',name: 'subCategory',component: SubCategory},]},{path: '/login',component: Login}]
})export default router
category/index.vue修改
<div class="sub-list"><h3>全部分类</h3><ul><li v-for="i in categoryData.children" :key="i.id"><RouterLink :to="`/category/sub/${i.id}`"><img :src="i.picture" /><p>{{ i.name }}</p></RouterLink></li></ul></div>
api/category.js
/*** @description: 获取二级分类列表数据* @param {*} id 分类id * @return {*}*/export const getCategoryFilterAPI = (id) => {return httpInstance({url:'/category/sub/filter',params:{id}})
}
/*** @description: 获取导航数据* @data { categoryId: 1005000 ,page: 1,pageSize: 20,sortField: 'publishTime' | 'orderNum' | 'evaluateNum'} * @return {*}*/
export const getSubCategoryAPI = (data) => {return httpInstance({url:'/category/goods/temporary',method:'POST',data})
}
subCategory/index.vue
<script setup>
import { getCategoryFilterAPI,getSubCategoryAPI} from '@/api/category'
import {ref,onMounted} from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../home/components/GoodsItem.vue'
// 获取面包屑导航数据
const filterData = ref({})
const route = useRoute()
const getFilterData = async () => {const res = await getCategoryFilterAPI(route.params.id)filterData.value = res.result
}
getFilterData()// 获取基础列表数据渲染
const goodList = ref([])
const reqData = ref({categoryId: route.params.id,page: 1,pageSize: 20,sortField: 'publishTime'
})const getGoodList = async () => {const res = await getSubCategoryAPI(reqData.value)console.log(res)goodList.value = res.result.items
}onMounted(() => getGoodList())
</script><template><div class="container"><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/${filterData.parentId}` }">{{ filterData.parentName }}</el-breadcrumb-item><el-breadcrumb-item>{{ filterData.name }}</el-breadcrumb-item></el-breadcrumb></div><div class="sub-container"><el-tabs><el-tab-pane label="最新商品" name="publishTime"></el-tab-pane><el-tab-pane label="最高人气" name="orderNum"></el-tab-pane><el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane></el-tabs><div class="body"><!-- 商品列表--><GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"></GoodsItem></div></div></div>
</template><style lang="scss" scoped>
.container {width: 1240px;margin: 0 auto;.bread-container {padding: 25px 0;color: #666;}.sub-container {padding: 20px 10px;background-color: #fff;.body {display: flex;flex-wrap: wrap;padding: 0 10px;}.goods-item {display: block;width: 220px;margin-right: 20px;padding: 20px 30px;text-align: center;img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}}.pagination-container {margin-top: 20px;display: flex;justify-content: center;}}
}</style>
subCategory/index.vue修改
<div class="body" v-infinite-scroll="load"><!-- 商品列表--><GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"></GoodsItem>
</div><script setup>
import { getCategoryFilterAPI,getSubCategoryAPI} from '@/api/category'
import {ref,onMounted} from 'vue'
import { useRoute } from 'vue-router'
import GoodsItem from '../home/components/GoodsItem.vue'
// 获取面包屑导航数据
const filterData = ref({})
const route = useRoute()
const getFilterData = async () => {const res = await getCategoryFilterAPI(route.params.id)filterData.value = res.result
}
getFilterData()// 获取基础列表数据渲染
const goodList = ref([])
const reqData = ref({categoryId: route.params.id,page: 1,pageSize: 20,sortField: 'publishTime'
})const getGoodList = async () => {const res = await getSubCategoryAPI(reqData.value)console.log(res)goodList.value = res.result.items
}onMounted(() => getGoodList())// tab切换回调
const tabChange = () => {console.log('tab切换了', reqData.value.sortField)reqData.value.page = 1getGoodList()
}
// 加载更多
const disabled = ref(false)
const load = async () => {console.log('加载更多数据咯')// 获取下一页的数据reqData.value.page++const res = await getSubCategoryAPI(reqData.value)goodList.value = [...goodList.value, ...res.result.items]// 加载完毕 停止监听if (res.result.items.length === 0) {disabled.value = true}
}
</script>
无限加载。
定制路由的滚动行为
自动回到顶部。
router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import Login from '@/views/login/index.vue'
import Layout from '@/views/layout/index.vue'
import Home from '@/views/home/index.vue'
import Category from '@/views/category/index.vue'
import SubCategory from '@/views/subCategory/index.vue'const router = createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: '/',component: Layout,children: [{path: '',component: Home},{path: 'category/:id',component: Category},{path: 'category/sub/:id',name: 'subCategory',component: SubCategory},]},{path: '/login',component: Login}],//路由滚动行为scrollBehavior() {return {top: 0}}
})export default router
商品详情
路由配置
views/detail/index.vue
<script setup></script><template><div class="xtx-goods-page"><div class="container"><div class="bread-container"><el-breadcrumb separator=">"><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: '/' }">母婴</el-breadcrumb-item><el-breadcrumb-item :to="{ path: '/' }">跑步鞋</el-breadcrumb-item><el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item></el-breadcrumb></div><!-- 商品信息 --><div class="info-container"><div><div class="goods-info"><div class="media"><!-- 图片预览区 --><!-- 统计数量 --><ul class="goods-sales"><li><p>销量人气</p><p> 100+ </p><p><i class="iconfont icon-task-filling"></i>销量人气</p></li><li><p>商品评价</p><p>200+</p><p><i class="iconfont icon-comment-filling"></i>查看评价</p></li><li><p>收藏人气</p><p>300+</p><p><i class="iconfont icon-favorite-filling"></i>收藏商品</p></li><li><p>品牌信息</p><p>400+</p><p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p></li></ul></div><div class="spec"><!-- 商品信息区 --><p class="g-name"> 抓绒保暖,毛毛虫儿童鞋 </p><p class="g-desc">好穿 </p><p class="g-price"><span>200</span><span> 100</span></p><div class="g-service"><dl><dt>促销</dt><dd>12月好物放送,App领券购买直降120元</dd></dl><dl><dt>服务</dt><dd><span>无忧退货</span><span>快速退款</span><span>免费包邮</span><a href="javascript:;">了解详情</a></dd></dl></div><!-- sku组件 --><!-- 数据组件 --><!-- 按钮组件 --><div><el-button size="large" class="btn">加入购物车</el-button></div></div></div><div class="goods-footer"><div class="goods-article"><!-- 商品详情 --><div class="goods-tabs"><nav><a>商品详情</a></nav><div class="goods-detail"><!-- 属性 --><ul class="attrs"><li v-for="item in 3" :key="item.value"><span class="dt">白色</span><span class="dd">纯棉</span></li></ul><!-- 图片 --></div></div></div><!-- 24热榜+专题推荐 --><div class="goods-aside"></div></div></div></div></div></div>
</template><style scoped lang='scss'>
.xtx-goods-page {.goods-info {min-height: 600px;background: #fff;display: flex;.media {width: 580px;height: 600px;padding: 30px 50px;}.spec {flex: 1;padding: 30px 30px 30px 0;}}.goods-footer {display: flex;margin-top: 20px;.goods-article {width: 940px;margin-right: 20px;}.goods-aside {width: 280px;min-height: 1000px;}}.goods-tabs {min-height: 600px;background: #fff;}.goods-warn {min-height: 600px;background: #fff;margin-top: 20px;}.number-box {display: flex;align-items: center;.label {width: 60px;color: #999;padding-left: 10px;}}.g-name {font-size: 22px;}.g-desc {color: #999;margin-top: 10px;}.g-price {margin-top: 10px;span {&::before {content: "¥";font-size: 14px;}&:first-child {color: $priceColor;margin-right: 10px;font-size: 22px;}&:last-child {color: #999;text-decoration: line-through;font-size: 16px;}}}.g-service {background: #f5f5f5;width: 500px;padding: 20px 10px 0 10px;margin-top: 10px;dl {padding-bottom: 20px;display: flex;align-items: center;dt {width: 50px;color: #999;}dd {color: #666;&:last-child {span {margin-right: 10px;&::before {content: "•";color: $xtxColor;margin-right: 2px;}}a {color: $xtxColor;}}}}}.goods-sales {display: flex;width: 400px;align-items: center;text-align: center;height: 140px;li {flex: 1;position: relative;~li::after {position: absolute;top: 10px;left: 0;height: 60px;border-left: 1px solid #e4e4e4;content: "";}p {&:first-child {color: #999;}&:nth-child(2) {color: $priceColor;margin-top: 10px;}&:last-child {color: #666;margin-top: 10px;i {color: $xtxColor;font-size: 14px;margin-right: 2px;}&:hover {color: $xtxColor;cursor: pointer;}}}}}
}.goods-tabs {min-height: 600px;background: #fff;nav {height: 70px;line-height: 70px;display: flex;border-bottom: 1px solid #f5f5f5;a {padding: 0 40px;font-size: 18px;position: relative;>span {color: $priceColor;font-size: 16px;margin-left: 10px;}}}
}.goods-detail {padding: 40px;.attrs {display: flex;flex-wrap: wrap;margin-bottom: 30px;li {display: flex;margin-bottom: 10px;width: 50%;.dt {width: 100px;color: #999;}.dd {flex: 1;color: #666;}}}>img {width: 100%;}
}.btn {margin-top: 20px;}.bread-container {padding: 25px 0;
}
</style>
router/index.js中children中添加
{path: 'detail/:id',component: Detail
}
home/components/HomeNew.vue修改
<li v-for="item in newList" :key="item.id"><RouterLink :to="`/detail/${item.id}`"><img :src="item.picture" alt="" /><p class="name">{{ item.name }}</p><p class="price">¥{{ item.price }}</p></RouterLink></li>
数据渲染
detail/index.vue
<script setup>
import { ref } from 'vue'
import { getDetail } from '@/api/detail'
import { useRoute } from 'vue-router'const goods = ref([])
const route = useRoute()
const getGoods = async () => {const res = await getDetail(route.params.id)goods.value = res.result
}
getGoods()</script><template><div class="xtx-goods-page"><div class="container"><div class="bread-container"><el-breadcrumb separator=">"><!-- 可选链 --><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/${goods.categories?.[1].id}` }">{{goods.categories?.[1].name}}</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories?.[0].id}` }">{{goods.categories?.[0].name}}</el-breadcrumb-item><el-breadcrumb-item>{{goods.name}}</el-breadcrumb-item></el-breadcrumb></div><!-- 商品信息 --><div class="info-container"><div><div class="goods-info"><div class="media"><!-- 图片预览区 --><!-- 统计数量 --><ul class="goods-sales"><li><p>销量人气</p><p> {{goods.salesCount}}+</p><p><i class="iconfont icon-task-filling"></i>销量人气</p></li><li><p>商品评价</p><p>{{goods.commentCount}}+</p><p><i class="iconfont icon-comment-filling"></i>查看评价</p></li><li><p>收藏人气</p><p>{{goods.collectCount}}+</p><p><i class="iconfont icon-favorite-filling"></i>收藏商品</p></li><li><p>品牌信息</p><p>{{goods.brand.name}}</p><p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p></li></ul></div><div class="spec"><!-- 商品信息区 --><p class="g-name"> {{goods.name}} </p><p class="g-desc">{{goods.desc}} </p><p class="g-price"><span>{{goods.oldPrice}}</span><span>{{goods.price}}</span></p><div class="g-service"><dl><dt>促销</dt><dd>12月好物放送,App领券购买直降120元</dd></dl><dl><dt>服务</dt><dd><span>无忧退货</span><span>快速退款</span><span>免费包邮</span><a href="javascript:;">了解详情</a></dd></dl></div><!-- sku组件 --><!-- 数据组件 --><!-- 按钮组件 --><div><el-button size="large" class="btn">加入购物车</el-button></div></div></div><div class="goods-footer"><div class="goods-article"><!-- 商品详情 --><div class="goods-tabs"><nav><a>商品详情</a></nav><div class="goods-detail"><!-- 属性 --><ul class="attrs"><li v-for="item in goods.details.properties" :key="item.value"><span class="dt">{{item.name}}</span><span class="dd">{{item.value}}</span></li></ul><!-- 图片 --><img v-for="img in goods.details.pictures" :src="img" key="img" alt=""/></div></div></div><!-- 24热榜+专题推荐 --><div class="goods-aside"></div></div></div></div></div></div>
</template><style scoped lang='scss'>
.xtx-goods-page {.goods-info {min-height: 600px;background: #fff;display: flex;.media {width: 580px;height: 600px;padding: 30px 50px;}.spec {flex: 1;padding: 30px 30px 30px 0;}}.goods-footer {display: flex;margin-top: 20px;.goods-article {width: 940px;margin-right: 20px;}.goods-aside {width: 280px;min-height: 1000px;}}.goods-tabs {min-height: 600px;background: #fff;}.goods-warn {min-height: 600px;background: #fff;margin-top: 20px;}.number-box {display: flex;align-items: center;.label {width: 60px;color: #999;padding-left: 10px;}}.g-name {font-size: 22px;}.g-desc {color: #999;margin-top: 10px;}.g-price {margin-top: 10px;span {&::before {content: "¥";font-size: 14px;}&:first-child {color: $priceColor;margin-right: 10px;font-size: 22px;}&:last-child {color: #999;text-decoration: line-through;font-size: 16px;}}}.g-service {background: #f5f5f5;width: 500px;padding: 20px 10px 0 10px;margin-top: 10px;dl {padding-bottom: 20px;display: flex;align-items: center;dt {width: 50px;color: #999;}dd {color: #666;&:last-child {span {margin-right: 10px;&::before {content: "•";color: $xtxColor;margin-right: 2px;}}a {color: $xtxColor;}}}}}.goods-sales {display: flex;width: 400px;align-items: center;text-align: center;height: 140px;li {flex: 1;position: relative;~li::after {position: absolute;top: 10px;left: 0;height: 60px;border-left: 1px solid #e4e4e4;content: "";}p {&:first-child {color: #999;}&:nth-child(2) {color: $priceColor;margin-top: 10px;}&:last-child {color: #666;margin-top: 10px;i {color: $xtxColor;font-size: 14px;margin-right: 2px;}&:hover {color: $xtxColor;cursor: pointer;}}}}}
}.goods-tabs {min-height: 600px;background: #fff;nav {height: 70px;line-height: 70px;display: flex;border-bottom: 1px solid #f5f5f5;a {padding: 0 40px;font-size: 18px;position: relative;>span {color: $priceColor;font-size: 16px;margin-left: 10px;}}}
}.goods-detail {padding: 40px;.attrs {display: flex;flex-wrap: wrap;margin-bottom: 30px;li {display: flex;margin-bottom: 10px;width: 50%;.dt {width: 100px;color: #999;}.dd {flex: 1;color: #666;}}}>img {width: 100%;}
}.btn {margin-top: 20px;}.bread-container {padding: 25px 0;
}
</style>
热榜区域
api/detail.js添加
export const fetchHotGoodsAPI = ({ id, type, limit = 3 }) => {return httpInstance({url:'/goods/hot',params:{id, type, limit}})
}
DetailHot.vue
<script setup>
import { ref } from 'vue'
import { fetchHotGoodsAPI } from '@/api/detail'
import { useRoute } from 'vue-router'const goodList = ref([])
const route = useRoute()
const getHotList = async () => {const res = await fetchHotGoodsAPI({id: route.params.id,type: 1})goodList.value = res.result
}
getHotList()</script><template><div class="goods-hot"><h3> 24小时热榜 </h3><!-- 商品区块 --><RouterLink :to="`/detail/${item.id}`" class="goods-item" v-for="item in goodList" :key="item.id"><img :src="item.picture" alt="" /><p class="name ellipsis">{{ item.name }}</p><p class="desc ellipsis">{{ item.desc }}</p><p class="price">¥{{ item.price }}</p></RouterLink></div>
</template><style scoped lang="scss">
.goods-hot {h3 {height: 70px;background: $helpColor;color: #fff;font-size: 18px;line-height: 70px;padding-left: 25px;margin-bottom: 10px;font-weight: normal;}.goods-item {display: block;padding: 20px 30px;text-align: center;background: #fff;img {width: 160px;height: 160px;}p {padding-top: 10px;}.name {font-size: 16px;}.desc {color: #999;height: 29px;}.price {color: $priceColor;font-size: 20px;}}
}
</style>
detail/index.vue
<script setup>
import { ref } from 'vue'
import { getDetail } from '@/api/detail'
import { useRoute } from 'vue-router'
import DetailHot from './components/DetailHot.vue'const goods = ref([])
const route = useRoute()
const getGoods = async () => {const res = await getDetail(route.params.id)goods.value = res.result
}
getGoods()
</script><template><div class="xtx-goods-page"><div class="container"><div class="bread-container"><el-breadcrumb separator=">"><!-- 可选链 --><el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/${goods.categories?.[1].id}` }">{{goods.categories?.[1].name}}</el-breadcrumb-item><el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories?.[0].id}` }">{{goods.categories?.[0].name}}</el-breadcrumb-item><el-breadcrumb-item>{{goods.name}}</el-breadcrumb-item></el-breadcrumb></div><!-- 商品信息 --><div class="info-container"><div><div class="goods-info"><div class="media"><!-- 图片预览区 --><!-- 统计数量 --><ul class="goods-sales"><li><p>销量人气</p><p> {{goods.salesCount}}+</p><p><i class="iconfont icon-task-filling"></i>销量人气</p></li><li><p>商品评价</p><p>{{goods.commentCount}}+</p><p><i class="iconfont icon-comment-filling"></i>查看评价</p></li><li><p>收藏人气</p><p>{{goods.collectCount}}+</p><p><i class="iconfont icon-favorite-filling"></i>收藏商品</p></li><li><p>品牌信息</p><p>{{goods?.brand?.name}}</p><p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p></li></ul></div><div class="spec"><!-- 商品信息区 --><p class="g-name"> {{goods.name}} </p><p class="g-desc">{{goods.desc}} </p><p class="g-price"><span>{{goods.oldPrice}}</span><span>{{goods.price}}</span></p><div class="g-service"><dl><dt>促销</dt><dd>12月好物放送,App领券购买直降120元</dd></dl><dl><dt>服务</dt><dd><span>无忧退货</span><span>快速退款</span><span>免费包邮</span><a href="javascript:;">了解详情</a></dd></dl></div><!-- sku组件 --><!-- 数据组件 --><!-- 按钮组件 --><div><el-button size="large" class="btn">加入购物车</el-button></div></div></div><div class="goods-footer"><div class="goods-article"><!-- 商品详情 --><div class="goods-tabs"><nav><a>商品详情</a></nav><div class="goods-detail"><!-- 属性 --><ul class="attrs"><li v-for="item in goods?.details?.properties" :key="item.value"><span class="dt">{{item.name}}</span><span class="dd">{{item.value}}</span></li></ul><!-- 图片 --><img v-for="img in goods?.details?.pictures" :src="img" key="img" alt=""/></div></div></div><!-- 24热榜+专题推荐 --><div class="goods-aside"><DetailHot/><DetailHot/></div></div></div></div></div></div>
</template><style scoped lang='scss'>
.xtx-goods-page {.goods-info {min-height: 600px;background: #fff;display: flex;.media {width: 580px;height: 600px;padding: 30px 50px;}.spec {flex: 1;padding: 30px 30px 30px 0;}}.goods-footer {display: flex;margin-top: 20px;.goods-article {width: 940px;margin-right: 20px;}.goods-aside {width: 280px;min-height: 1000px;}}.goods-tabs {min-height: 600px;background: #fff;}.goods-warn {min-height: 600px;background: #fff;margin-top: 20px;}.number-box {display: flex;align-items: center;.label {width: 60px;color: #999;padding-left: 10px;}}.g-name {font-size: 22px;}.g-desc {color: #999;margin-top: 10px;}.g-price {margin-top: 10px;span {&::before {content: "¥";font-size: 14px;}&:first-child {color: $priceColor;margin-right: 10px;font-size: 22px;}&:last-child {color: #999;text-decoration: line-through;font-size: 16px;}}}.g-service {background: #f5f5f5;width: 500px;padding: 20px 10px 0 10px;margin-top: 10px;dl {padding-bottom: 20px;display: flex;align-items: center;dt {width: 50px;color: #999;}dd {color: #666;&:last-child {span {margin-right: 10px;&::before {content: "•";color: $xtxColor;margin-right: 2px;}}a {color: $xtxColor;}}}}}.goods-sales {display: flex;width: 400px;align-items: center;text-align: center;height: 140px;li {flex: 1;position: relative;~li::after {position: absolute;top: 10px;left: 0;height: 60px;border-left: 1px solid #e4e4e4;content: "";}p {&:first-child {color: #999;}&:nth-child(2) {color: $priceColor;margin-top: 10px;}&:last-child {color: #666;margin-top: 10px;i {color: $xtxColor;font-size: 14px;margin-right: 2px;}&:hover {color: $xtxColor;cursor: pointer;}}}}}
}.goods-tabs {min-height: 600px;background: #fff;nav {height: 70px;line-height: 70px;display: flex;border-bottom: 1px solid #f5f5f5;a {padding: 0 40px;font-size: 18px;position: relative;>span {color: $priceColor;font-size: 16px;margin-left: 10px;}}}
}.goods-detail {padding: 40px;.attrs {display: flex;flex-wrap: wrap;margin-bottom: 30px;li {display: flex;margin-bottom: 10px;width: 50%;.dt {width: 100px;color: #999;}.dd {flex: 1;color: #666;}}}>img {width: 100%;}
}.btn {margin-top: 20px;}.bread-container {padding: 25px 0;
}
</style>
适配热榜类型
detail/components/DeatinHot.vue修改
<script setup>
import { ref,computed} from 'vue'
import { fetchHotGoodsAPI } from '@/api/detail'
import { useRoute } from 'vue-router'// type适配不同类型热榜数据
const props = defineProps({hotType: {type: Number, // 1代表24小时热销榜 2代表周热销榜 3代表总热销榜 可以使用type去适配title和数据列表default: 1}
})
const TITLEMAP = {1: '24小时热榜',2: '周热榜',
}
const title = computed(() => TITLEMAP[props.hotType])const goodList = ref([])
const route = useRoute()
const getHotList = async () => {const res = await fetchHotGoodsAPI({id: route.params.id,type: props.hotType})goodList.value = res.result
}
getHotList()</script><!-- 24热榜+专题推荐 --><div class="goods-aside"><DetailHot :hotType="1"/><DetailHot :hotType="2"/></div>
detail/index.vue
<!-- 24热榜+专题推荐 --><div class="goods-aside"><DetailHot :hotType="1"/><DetailHot :hotType="2"/></div>
图片预览组件封装
src/components/imgView/index.vue
<script setup>
import { ref, watch } from 'vue'
import { useMouseInElement } from '@vueuse/core'defineProps({imageList: {type: Array,default: () => []}
})// // 图片列表
// const imageList = [
// "https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png",
// "https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg",
// "https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg",
// "https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg",
// "https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg"
// ]// 1.小图切换大图显示
const activeIndex = ref(0)const enterhandler = (i) => {activeIndex.value = i
}// 2. 获取鼠标相对位置
const target = ref(null)
const { elementX, elementY, isOutside } = useMouseInElement(target)// 3. 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top)
const left = ref(0)
const top = ref(0)const positionX = ref(0)
const positionY = ref(0)
watch([elementX, elementY, isOutside], () => {console.log('xy变化了')// 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑if (isOutside.value) returnconsole.log('后续逻辑执行了')// 有效范围内控制滑块距离// 横向if (elementX.value > 100 && elementX.value < 300) {left.value = elementX.value - 100}// 纵向if (elementY.value > 100 && elementY.value < 300) {top.value = elementY.value - 100}// 处理边界if (elementX.value > 300) { left.value = 200 }if (elementX.value < 100) { left.value = 0 }if (elementY.value > 300) { top.value = 200 }if (elementY.value < 100) { top.value = 0 }// 控制大图的显示positionX.value = -left.value * 2positionY.value = -top.value * 2})</script><template><div class="goods-image"><!-- 左侧大图--><div class="middle" ref="target"><img :src="imageList[activeIndex]" alt="" /><!-- 蒙层小滑块 --><div class="layer" v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }"></div></div><!-- 小图列表 --><ul class="small"><li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{ active: i === activeIndex }"><img :src="img" alt="" /></li></ul><!-- 放大镜大图 --><div class="large" :style="[{backgroundImage: `url(${imageList[activeIndex]})`,backgroundPositionX: `${positionX}px`,backgroundPositionY: `${positionY}px`,},]" v-show="!isOutside"></div></div>
</template><style scoped lang="scss">
.goods-image {width: 480px;height: 400px;position: relative;display: flex;.middle {width: 400px;height: 400px;background: #f5f5f5;}.large {position: absolute;top: 0;left: 412px;width: 400px;height: 400px;z-index: 500;box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);background-repeat: no-repeat;// 背景图:盒子的大小 = 2:1 将来控制背景图的移动来实现放大的效果查看 background-positionbackground-size: 800px 800px;background-color: #f8f8f8;}.layer {width: 200px;height: 200px;background: rgba(0, 0, 0, 0.2);// 绝对定位 然后跟随咱们鼠标控制left和top属性就可以让滑块移动起来left: 0;top: 0;position: absolute;}.small {width: 80px;li {width: 68px;height: 68px;margin-left: 12px;margin-bottom: 15px;cursor: pointer;&:hover,&.active {border: 2px solid $xtxColor;}}}
}
</style>
detail/index.vue修改
import ImageView from '@/components/imageView/index.vue'<!-- 图片预览区 -->
<ImageView :image-list="goods.mainPictures"/>
sku插件
XtxSku
链接:https://pan.baidu.com/s/1KQ5-OFMoD7bpL8DJjsmsIA?pwd=3kgh
提取码:3kgh
detain/index.vue修改
import XtxSku from '@/components/XtxSku/index.vue'<!-- sku组件 -->
<XtxSku :goods="goods"/>
登录页
表单
login/index.vue
<script setup>
import { ref } from 'vue'
// 表单数据对象
const userInfo = ref({account: '1311111111',password: '123456',agree: true
})// 规则数据对象
const rules = {account: [{ required: true, message: '用户名不能为空' }],password: [{ required: true, message: '密码不能为空' },{ min: 6, max: 24, message: '密码长度要求6-14个字符' }],agree: [{validator: (rule, val, callback) => {return val ? callback() : new Error('请先同意协议')}}]
}</script><template><div><header class="login-header"><div class="container m-top-20"><h1 class="logo"><RouterLink to="/">小兔鲜</RouterLink></h1><RouterLink class="entry" to="/">进入网站首页<i class="iconfont icon-angle-right"></i><i class="iconfont icon-angle-right"></i></RouterLink></div></header><section class="login-section"><div class="wrapper"><nav><a href="javascript:;">账户登录</a></nav><div class="account-box"><div class="form"><el-form ref="formRef" :model="userInfo" :rules="rules" status-icon><el-form-item prop="account" label="账户"><el-input v-model="userInfo.account" /></el-form-item><el-form-item prop="password" label="密码"><el-input v-model="userInfo.password" /></el-form-item><el-form-item prop="agree" label-width="22px"><el-checkbox v-model="userInfo.agree" size="large">我已同意隐私条款和服务条款</el-checkbox></el-form-item><el-button size="large" class="subBtn" >点击登录</el-button></el-form></div></div></div></section><footer class="login-footer"><div class="container"><p><a href="javascript:;">关于我们</a><a href="javascript:;">帮助中心</a><a href="javascript:;">售后服务</a><a href="javascript:;">配送与验收</a><a href="javascript:;">商务合作</a><a href="javascript:;">搜索推荐</a><a href="javascript:;">友情链接</a></p><p>CopyRight © 小兔鲜儿</p></div></footer></div>
</template><style scoped lang='scss'>
.login-header {background: #fff;border-bottom: 1px solid #e4e4e4;.container {display: flex;align-items: flex-end;justify-content: space-between;}.logo {width: 200px;a {display: block;height: 132px;width: 100%;text-indent: -9999px;background: url("@/assets/images/logo.png") no-repeat center 18px / contain;}}.sub {flex: 1;font-size: 24px;font-weight: normal;margin-bottom: 38px;margin-left: 20px;color: #666;}.entry {width: 120px;margin-bottom: 38px;font-size: 16px;i {font-size: 14px;color: $xtxColor;letter-spacing: -5px;}}
}.login-section {background: url('@/assets/images/login-bg.png') no-repeat center / cover;height: 488px;position: relative;.wrapper {width: 380px;background: #fff;position: absolute;left: 50%;top: 54px;transform: translate3d(100px, 0, 0);box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);nav {font-size: 14px;height: 55px;margin-bottom: 20px;border-bottom: 1px solid #f5f5f5;display: flex;padding: 0 40px;text-align: right;align-items: center;a {flex: 1;line-height: 1;display: inline-block;font-size: 18px;position: relative;text-align: center;}}}
}.login-footer {padding: 30px 0 50px;background: #fff;p {text-align: center;color: #999;padding-top: 20px;a {line-height: 1;padding: 0 10px;color: #999;display: inline-block;~a {border-left: 1px solid #ccc;}}}
}.account-box {.toggle {padding: 15px 40px;text-align: right;a {color: $xtxColor;i {font-size: 14px;}}}.form {padding: 0 20px 20px 20px;&-item {margin-bottom: 28px;.input {position: relative;height: 36px;>i {width: 34px;height: 34px;background: #cfcdcd;color: #fff;position: absolute;left: 1px;top: 1px;text-align: center;line-height: 34px;font-size: 18px;}input {padding-left: 44px;border: 1px solid #cfcdcd;height: 36px;line-height: 36px;width: 100%;&.error {border-color: $priceColor;}&.active,&:focus {border-color: $xtxColor;}}.code {position: absolute;right: 1px;top: 1px;text-align: center;line-height: 34px;font-size: 14px;background: #f5f5f5;color: #666;width: 90px;height: 34px;cursor: pointer;}}>.error {position: absolute;font-size: 12px;line-height: 28px;color: $priceColor;i {font-size: 14px;margin-right: 2px;}}}.agree {a {color: #069;}}.btn {display: block;width: 100%;height: 40px;color: #fff;text-align: center;line-height: 40px;background: $xtxColor;&.disabled {background: #cfcdcd;}}}.action {padding: 20px 40px;display: flex;justify-content: space-between;align-items: center;.url {a {color: #999;margin-left: 10px;}}}
}.subBtn {background: $xtxColor;width: 100%;color: #fff;
}
</style>
登录实现
账号:cdshi0006
密码:123456
login/index.vue修改
<script setup>
import { ref } from 'vue'
import {loginAPI} from '@/api/user'
import { ElMessage } from 'element-plus'
import {useRouter} from 'vue-router'
import 'element-plus/theme-chalk/el-message.css'
// 表单数据对象
const userInfo = ref({account: '',password: '',agree: true
})// 规则数据对象
const rules = {account: [{ required: true, message: '用户名不能为空' }],password: [{ required: true, message: '密码不能为空' },{ min: 6, max: 24, message: '密码长度要求6-14个字符' }],agree: [{validator: (rule, val, callback) => {return val ? callback() : new Error('请先同意协议')}}]
}const formRef = ref(null)
const router = useRouter()
const doLogin = () => {const {account,password} = userInfo.valueformRef.value.validate(async(valid) => {if(valid) {const res = await loginAPI({account,password})// 1. 提示用户ElMessage({ type: 'success', message: '登录成功' })// 2. 跳转首页router.replace({ path: '/' })}})
}</script>
登录失败的提示
utils/http.js修改
import 'element-plus/theme-chalk/el-message.css'
import { ElMessage } from 'element-plus'// axios响应式拦截器
http.interceptors.response.use(res=>res.data,e => {ElMessage({type: 'warning',message: e.response.data.msg})console.log(e)return Promise.reject(e)
})
Pinia管理用户
stores/user.js
// 管理用户数据相关import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginAPI } from '@/api/user'export const useUserStore = defineStore('user', () => {// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取接口数据的action函数const getUserInfo = async ({ account, password }) => {const res = await loginAPI({ account, password })userInfo.value = res.result}// 3. 以对象的格式把state和action returnreturn {userInfo,getUserInfo}
}, {persist: true,
})
Pinia持久化存储
需要使用插件,自动本地存储,官网
下载pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
main.js修改
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
login/index.vue修改
export const useUserStore = defineStore('user', () => {//......
}, {persist: true,
})
登录和非登录状态
LayoutNav.vue修改
<script setup>import {useUserStore} from '@/stores/user'const userStore = useUserStore()
</script><template><nav class="app-topnav"><div class="container"><ul><template v-if="userStore.userInfo.token"><li><a href="javascript:;"><i class="iconfont icon-user"></i>{{userStore.userInfo.account}}</a></li><li>......</ul></div></nav>
</template>
请求拦截器携带token
http.js
import {useUserStore} from '@/stores/user'http.interceptors.request.use(config => {// 1. 从pinia获取token数据const userStore = useUserStore()// 2. 按照后端的要求拼接token数据const token = userStore.userInfo.tokenif (token) {config.headers.Authorization = `Bearer ${token}`}return config
}, e => Promise.reject(e))
退出登录
user.js
// 管理用户数据相关import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginAPI } from '@/api/user'export const useUserStore = defineStore('user', () => {// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取接口数据的action函数const getUserInfo = async ({ account, password }) => {const res = await loginAPI({ account, password })userInfo.value = res.result}// 退出时清除用户信息const clearUserInfo = () => {userInfo.value = {}}// 3. 以对象的格式把state和action returnreturn {userInfo,getUserInfo,clearUserInfo}
}, {persist: true,
})
LayoutNav.vue修改
<script setup>import {useUserStore} from '@/stores/user'import {useRouter} from 'vue-router'const userStore = useUserStore()const router = useRouter()const confirm = () => {console.log("登录")userStore.clearUserInfo()router.push('/login')}
</script><template><nav class="app-topnav"><div class="container"><ul><template v-if="userStore.userInfo.token"><li><a href="javascript:;"><i class="iconfont icon-user"></i>{{userStore.userInfo.account}}</a></li><li><el-popconfirm @confirm="confirm" title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">。。。。。。</div></nav>
</template>
token失效处理
http.js
import router from '@/router'// axios响应式拦截器
http.interceptors.response.use(res=>res.data,e => {const userStore = useUserStore()ElMessage({type: 'warning',message: e.response.data.msg})if(e.response.status === 401) {userStore.clearUserInfo()router.push('/login')}return Promise.reject(e)
})
购物车
本地加入购物车
stores/car.js
// 封装购物车模块import { defineStore } from 'pinia'
import { ref } from 'vue'export const useCartStore = defineStore('cart', () => {// 1. 定义state - cartListconst cartList = ref([])// 2. 定义action - addCartconst addCart = (goods) => {console.log('添加', goods)// 添加购物车操作// 已添加过 - count + 1// 没有添加过 - 直接push// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过const item = cartList.value.find((item) => goods.skuId === item.skuId)if (item) {// 找到了item.count++} else {// 没找到cartList.value.push(goods)}}return {cartList,addCart}
}, {persist: true,
})
detail/index.vue修改
<script setup>
import { ref } from 'vue'
import { getDetail } from '@/api/detail'
import { useRoute } from 'vue-router'
import DetailHot from './components/DetailHot.vue'
import ImageView from '@/components/imageView/index.vue'
import XtxSku from '@/components/XtxSku/index.vue'
import { ElMessage } from 'element-plus'
import {useCartStore} from '@/stores/car'const carStore = useCartStore()
const goods = ref([])
const route = useRoute()
const getGoods = async () => {const res = await getDetail(route.params.id)goods.value = res.result
}
getGoods()//sku
let skuObj = {}
const skuChange = (sku) => {skuObj = sku
}
//count
const count = ref(1)
const countChange = (count) => {console.log(count)
}
//购物车
const addCar = () => {if(skuObj.skuId) {carStore.addCart({id: goods.value.id,name: goods.value.name,picture: goods.value.mainPictures[0],orice: goods.value.price,count: count.value,skuId: skuObj.skuId,attrsText: skuObj.specsText,selected: true})} else {ElMessage.warning('请选择规格')}
}
</script><!-- sku组件 --><XtxSku :goods="goods" @change="skuChange"/><!-- 数据组件 --><el-input-number v-model="count" @change="countChange"></el-input-number><!-- 按钮组件 --><div><el-button size="large" class="btn" @click="addCar">加入购物车</el-button></div>
头部购物车
detail/index.vue修改
<script setup>
import { ref } from 'vue'
import { getDetail } from '@/api/detail'
import { useRoute } from 'vue-router'
import DetailHot from './components/DetailHot.vue'
import ImageView from '@/components/imageView/index.vue'
import XtxSku from '@/components/XtxSku/index.vue'
import { ElMessage } from 'element-plus'
import {useCartStore} from '@/stores/car'const carStore = useCartStore()
const goods = ref([])
const route = useRoute()
const getGoods = async () => {const res = await getDetail(route.params.id)goods.value = res.result
}
getGoods()//sku
let skuObj = {}
const skuChange = (sku) => {skuObj = sku
}
//count
const count = ref(1)
const countChange = (count) => {console.log(count)
}
//购物车
const addCar = () => {if(skuObj.skuId) {carStore.addCart({id: goods.value.id,name: goods.value.name,picture: goods.value.mainPictures[0],price: goods.value.price,count: count.value,skuId: skuObj.skuId,attrsText: skuObj.specsText,selected: true})} else {ElMessage.warning('请选择规格')}
}
</script>
car.js
// 封装购物车模块import { defineStore } from 'pinia'
import { computed, ref } from 'vue'export const useCartStore = defineStore('cart', () => {// 1. 定义state - cartListconst cartList = ref([])// 2. 定义action - addCartconst addCart = (goods) => {// console.log('添加', goods)// 添加购物车操作// 已添加过 - count + 1// 没有添加过 - 直接push// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过const item = cartList.value.find((item) => goods.skuId === item.skuId)if (item) {// 找到了item.count++} else {// 没找到cartList.value.push(goods)}}//删除购物车const delCart = (skuId) => {const idx = cartList.value.findIndex((item)=>skuId === item.skuId)cartList.value.splice(idx,1)}//总数const allCount = computed(()=>cartList.value.reduce((a,c)=>a+c.count,0))//总价const allPrice = computed(()=>cartList.value.reduce((a,c)=>a+c.price*c.count,0))return {cartList,addCart,delCart,allPrice,allCount}
}, {persist: true,
})
购物车页面
router.js添加
{path: 'cartlist',component: CartList}
views/cartList/index.vue
<script setup>import {useCartStore} from '@/stores/car'const cartStore = useCartStore()const singleCheck = (i, selected) => {cartStore.singleCheck(i.skuId,selected)}const allCheck = (selected) => {cartStore.allCheck(selected)}
</script><template><div class="xtx-cart-page"><div class="container m-top-20"><div class="cart"><table><thead><tr><th width="120"><el-checkbox :model-value="cartStore.isAll" @change="allCheck"/></th><th width="400">商品信息</th><th width="220">单价</th><th width="180">数量</th><th width="180">小计</th><th width="140">操作</th></tr></thead><!-- 商品列表 --><tbody><tr v-for="i in cartStore.cartList" :key="i.id"><td><el-checkbox :model-value="i.selected" @change="(selected)=>singleCheck(i,selected)"/></td><td><div class="goods"><RouterLink to="/"><img :src="i.picture" alt="" /></RouterLink><div><p class="name ellipsis">{{ i.name }}</p></div></div></td><td class="tc"><p>¥{{ i.price }}</p></td><td class="tc"><el-input-number v-model="i.count" /></td><td class="tc"><p class="f16 red">¥{{ (i.price * i.count).toFixed(2) }}</p></td><td class="tc"><p><el-popconfirm title="确认删除吗?" confirm-button-text="确认" cancel-button-text="取消" @confirm="delCart(i)"><template #reference><a href="javascript:;">删除</a></template></el-popconfirm></p></td></tr><tr v-if="cartStore.cartList.length === 0"><td colspan="6"><div class="cart-none"><el-empty description="购物车列表为空"><el-button type="primary">随便逛逛</el-button></el-empty></div></td></tr></tbody></table></div><!-- 操作栏 --><div class="action"><div class="batch">共 {{cartStore.allCount}} 件商品,已选择 {{cartStore.selectCount}} 件,商品合计:<span class="red">¥ {{cartStore.selectPrice.toFixed(2)}} </span></div><div class="total"><el-button size="large" type="primary" >下单结算</el-button></div></div></div></div>
</template><style scoped lang="scss">
.xtx-cart-page {margin-top: 20px;.cart {background: #fff;color: #666;table {border-spacing: 0;border-collapse: collapse;line-height: 24px;th,td {padding: 10px;border-bottom: 1px solid #f5f5f5;&:first-child {text-align: left;padding-left: 30px;color: #999;}}th {font-size: 16px;font-weight: normal;line-height: 50px;}}}.cart-none {text-align: center;padding: 120px 0;background: #fff;p {color: #999;padding: 20px 0;}}.tc {text-align: center;a {color: $xtxColor;}.xtx-numbox {margin: 0 auto;width: 120px;}}.red {color: $priceColor;}.green {color: $xtxColor;}.f16 {font-size: 16px;}.goods {display: flex;align-items: center;img {width: 100px;height: 100px;}>div {width: 280px;font-size: 16px;padding-left: 10px;.attr {font-size: 14px;color: #999;}}}.action {display: flex;background: #fff;margin-top: 20px;height: 80px;align-items: center;font-size: 16px;justify-content: space-between;padding: 0 30px;.xtx-checkbox {color: #999;}.batch {a {margin-left: 20px;}}.red {font-size: 18px;margin-right: 20px;font-weight: bold;}}.tit {color: #666;font-size: 16px;font-weight: normal;line-height: 50px;}}
</style>
stores/car.js
// 封装购物车模块import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import {insertCartAPI,findNewCartListAPI,delCartAPI} from '@/api/cart'
import {useUserStore} from '@/stores/user'export const useCartStore = defineStore('cart', () => {const userStore = useUserStore()const isLogin = computed(()=>userStore.userInfo.token)// 1. 定义state - cartListconst cartList = ref([])// 2. 定义action - addCartconst addCart = async (goods) => {const {skuId,count} = goodsif(isLogin.value) {//登录过后await insertCartAPI({skuId,count}) const res = await findNewCartListAPI()cartList.value = res.result} else {// console.log('添加', goods)// 添加购物车操作// 已添加过 - count + 1// 没有添加过 - 直接push// 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过const item = cartList.value.find((item) => goods.skuId === item.skuId)if (item) {// 找到了item.count++} else {// 没找到cartList.value.push(goods)}}}//更新const updateNewList = async () => {const res = await findNewCartListAPI()cartList.value = res.result}//删除购物车const delCart = async(skuId) => {if(isLogin.value) {await delCartAPI([skuId])const res = await findNewCartListAPI()cartList.value = res.result} else {const idx = cartList.value.findIndex((item)=>skuId === item.skuId)cartList.value.splice(idx,1)}}//总数const allCount = computed(()=>cartList.value.reduce((a,c)=>a+c.count,0))//总价const allPrice = computed(()=>cartList.value.reduce((a,c)=>a+c.price*c.count,0))// 单选功能const singleCheck = (skuId, selected) => {// 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selectedconst item = cartList.value.find((item) => item.skuId === skuId)item.selected = selected}//是否全选const isAll = computed(()=>cartList.value.every((item) => item.selected))//全选const allCheck = (selected) => {cartList.value.forEach(item => item.selected = selected)}//已选择的数量const selectCount = computed(()=>cartList.value.filter(item=>item.selected).reduce((a,c)=>a+c.count,0))//已选择价钱const selectPrice = computed(()=>cartList.value.filter(item=>item.selected).reduce((a,c)=>a+c.price*c.count,0))//清除购物车const clearCart = () => {cartList.value = []}return {cartList,addCart,delCart,allPrice,allCount,singleCheck,isAll,allCheck,selectCount,selectPrice,clearCart,updateNewList}
}, {persist: true,
})
api/cart.js
import httpInstance from '@/utils/http'
// 加入购物车
export const insertCartAPI = ({ skuId, count }) => {return httpInstance({url: '/member/cart',method: 'POST',data: {skuId,count}})
}//登录购物车
export const findNewCartListAPI = () => {return httpInstance({url: '/member/cart'})
}// 删除购物车
export const delCartAPI = (ids) => {return httpInstance({url: '/member/cart',method: 'DELETE',data: {ids}})
}//合并购物车
export const mergeCartAPI = (data) => {return httpInstance ({url: '/member/cart/merge',method: 'POST',data})
}
stores/user.js
// 管理用户数据相关import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginAPI } from '@/api/user'
import {useCartStore} from './car'
import {mergeCartAPI} from '@/api/cart'export const useUserStore = defineStore('user', () => {const cartStore = useCartStore()// 1. 定义管理用户数据的stateconst userInfo = ref({})// 2. 定义获取接口数据的action函数const getUserInfo = async ({ account, password }) => {const res = await loginAPI({ account, password })userInfo.value = res.resultmergeCartAPI(cartStore.cartList.map(item => {return {skuId: item.skuId,selected: item.selected,count: item.count}}))cartStore.updateNewList()}// 退出时清除用户信息const clearUserInfo = () => {userInfo.value = {}cartStore.clearCart()}// 3. 以对象的格式把state和action returnreturn {userInfo,getUserInfo,clearUserInfo}
}, {persist: true,
})
单选按钮
全选按钮
加入、删除购物车
清空购物车
合并本地购物车
订单页
路由
{path: 'checkout',component: CheckOut}
api/checkout.js
import httpInstance from '@/utils/http'
/*** 获取结算信息*/
export const getCheckoutInfoAPI = () => {return httpInstance({url:'/member/order/pre'})
}// 创建订单
export const createOrderAPI = (data) => {return httpInstance({url: '/member/order',method: 'POST',data})
}
views/pay/index.vue
<script setup>import {getCheckoutInfoAPI,createOrderAPI} from '@/api/checkout'import {ref,onMounted} from 'vue'import {useRouter} from 'vue-router'import {useCartStore} from '@/stores/car'const cartStore = useCartStore()const router = useRouter()const checkInfo = ref({}) // 订单对象const curAddress = ref({})const getCheckInfo = async () => {const res = await getCheckoutInfoAPI()checkInfo.value = res.resultconst item = checkInfo.value.userAddresses.find(item=>item.isDefault === 0)curAddress.value = item}onMounted(()=>getCheckInfo())const showDialog = ref(false)const activeAddress = ref({})//切换地址const switchAddress = (item) => {switchAddress.value = item}const confirm = () => {curAddress.value = activeAddress.valueshowDialog.value = falseactiveAddress.value = {}}//创建订单const createOrder = async () => {const res = await createOrderAPI({deliveryTimeType: 1,payType: 1,payChannel: 1,buyerMessage: '',goods: checkInfo.value.goods.map(item => {return {skuId: item.skuId,count: item.count}}),addressId: curAddress.value.id})const orderId = res.result.idrouter.push({path: '/pay',query: {id: orderId}})//更新购物车cartStore.updateNewList()}</script><template><div class="xtx-pay-checkout-page"><div class="container"><div class="wrapper"><!-- 收货地址 --><h3 class="box-title">收货地址</h3><div class="box-body"><div class="address"><div class="text"><div class="none" v-if="!curAddress">您需要先添加收货地址才可提交订单。</div><ul v-else><li><span>收<i />货<i />人:</span>{{ curAddress.receiver }}</li><li><span>联系方式:</span>{{ curAddress.contact }}</li><li><span>收货地址:</span>{{ curAddress.fullLocation }} {{ curAddress.address }}</li></ul></div><div class="action"><el-button size="large" @click="showDialog = true">切换地址</el-button><el-button size="large" @click="addFlag = true">添加地址</el-button></div></div></div><!-- 商品信息 --><h3 class="box-title">商品信息</h3><div class="box-body"><table class="goods"><thead><tr><th width="520">商品信息</th><th width="170">单价</th><th width="170">数量</th><th width="170">小计</th><th width="170">实付</th></tr></thead><tbody><tr v-for="i in checkInfo.goods" :key="i.id"><td><a href="javascript:;" class="info"><img :src="i.picture" alt=""><div class="right"><p>{{ i.name }}</p><p>{{ i.attrsText }}</p></div></a></td><td>¥{{ i.price }}</td><td>{{ i.price }}</td><td>¥{{ i.totalPrice }}</td><td>¥{{ i.totalPayPrice }}</td></tr></tbody></table></div><!-- 配送时间 --><h3 class="box-title">配送时间</h3><div class="box-body"><a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a><a class="my-btn" href="javascript:;">工作日送货:周一至周五</a><a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a></div><!-- 支付方式 --><h3 class="box-title">支付方式</h3><div class="box-body"><a class="my-btn active" href="javascript:;">在线支付</a><a class="my-btn" href="javascript:;">货到付款</a><span style="color:#999">货到付款需付5元手续费</span></div><!-- 金额明细 --><h3 class="box-title">金额明细</h3><div class="box-body"><div class="total"><dl><dt>商品件数:</dt><dd>{{ checkInfo.summary?.goodsCount }}件</dd></dl><dl><dt>商品总价:</dt><dd>¥{{ checkInfo.summary?.totalPrice.toFixed(2) }}</dd></dl><dl><dt>运<i></i>费:</dt><dd>¥{{ checkInfo.summary?.postFee.toFixed(2) }}</dd></dl><dl><dt>应付总额:</dt><dd class="price">{{ checkInfo.summary?.totalPayPrice.toFixed(2) }}</dd></dl></div></div><!-- 提交订单 --><div class="submit"><el-button type="primary" size="large" @click="createOrder">提交订单</el-button></div></div></div></div><!-- 切换地址 --><el-dialog v-model="showDialog" title="切换收货地址" width="30%" center><div class="addressWrapper"><div class="text item" :class="{active:activeAddress.id===item.id}" @click="switchAddress(item)" v-for="item in checkInfo.userAddresses" :key="item.id"><ul><li><span>收<i />货<i />人:</span>{{ item.receiver }} </li><li><span>联系方式:</span>{{ item.contact }}</li><li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li></ul></div></div><template #footer><span class="dialog-footer"><el-button>取消</el-button><el-button type="primary" @click="confirm">确定</el-button></span></template></el-dialog><!-- 添加地址 -->
</template><style scoped lang="scss">
.xtx-pay-checkout-page {margin-top: 20px;.wrapper {background: #fff;padding: 0 20px;.box-title {font-size: 16px;font-weight: normal;padding-left: 10px;line-height: 70px;border-bottom: 1px solid #f5f5f5;}.box-body {padding: 20px 0;}}
}.address {border: 1px solid #f5f5f5;display: flex;align-items: center;.text {flex: 1;min-height: 90px;display: flex;align-items: center;.none {line-height: 90px;color: #999;text-align: center;width: 100%;}>ul {flex: 1;padding: 20px;li {line-height: 30px;span {color: #999;margin-right: 5px;>i {width: 0.5em;display: inline-block;}}}}>a {color: $xtxColor;width: 160px;text-align: center;height: 90px;line-height: 90px;border-right: 1px solid #f5f5f5;}}.action {width: 420px;text-align: center;.btn {width: 140px;height: 46px;line-height: 44px;font-size: 14px;&:first-child {margin-right: 10px;}}}
}.goods {width: 100%;border-collapse: collapse;border-spacing: 0;.info {display: flex;text-align: left;img {width: 70px;height: 70px;margin-right: 20px;}.right {line-height: 24px;p {&:last-child {color: #999;}}}}tr {th {background: #f5f5f5;font-weight: normal;}td,th {text-align: center;padding: 20px;border-bottom: 1px solid #f5f5f5;&:first-child {border-left: 1px solid #f5f5f5;}&:last-child {border-right: 1px solid #f5f5f5;}}}
}.my-btn {width: 228px;height: 50px;border: 1px solid #e4e4e4;text-align: center;line-height: 48px;margin-right: 25px;color: #666666;display: inline-block;&.active,&:hover {border-color: $xtxColor;}
}.total {dl {display: flex;justify-content: flex-end;line-height: 50px;dt {i {display: inline-block;width: 2em;}}dd {width: 240px;text-align: right;padding-right: 70px;&.price {font-size: 20px;color: $priceColor;}}}
}.submit {text-align: right;padding: 60px;border-top: 1px solid #f5f5f5;
}.addressWrapper {max-height: 500px;overflow-y: auto;
}.text {flex: 1;min-height: 90px;display: flex;align-items: center;&.item {border: 1px solid #f5f5f5;margin-bottom: 10px;cursor: pointer;&.active,&:hover {border-color: $xtxColor;background: lighten($xtxColor, 50%);}>ul {padding: 10px;font-size: 14px;line-height: 30px;}}
}
</style>
支付页
router.js
{path: 'pay',component: Pay}
api/pay.js
import httpInstance from '@/utils/http'export const getOrderAPI = (id) => {return httpInstance({url: `/member/order/${id}`})
}
composables/useCountDown.js
// 封装倒计时逻辑函数
import { computed, onUnmounted, ref } from 'vue'
import dayjs from 'dayjs'
export const useCountDown = () => {// 1. 响应式的数据let timer = nullconst time = ref(0)// 格式化时间 为 xx分xx秒const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))// 2. 开启倒计时的函数const start = (currentTime) => {// 开始倒计时的逻辑// 核心逻辑的编写:每隔1s就减一time.value = currentTimetimer = setInterval(() => {time.value--}, 1000)}// 组件销毁时清除定时器onUnmounted(() => {timer && clearInterval(timer)})return {formatTime,start}
}
pay/index.vue
<script setup>
import { getOrderAPI } from '@/api/pay'
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import {useCountDown} from '@/composables/useCountDown'
const {formatTime,start} = useCountDown()
// 获取订单数据
const route = useRoute()
const payInfo = ref({})
const getPayInfo = async () => {const res = await getOrderAPI(route.query.id)payInfo.value = res.resultstart(res.result.countdown)
}
onMounted(() => getPayInfo())// 支付地址
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}`
</script><template><div class="xtx-pay-page"><div class="container"><!-- 付款信息 --><div class="pay-info"><span class="icon iconfont icon-queren2"></span><div class="tip"><p>订单提交成功!请尽快完成支付。</p><p>支付还剩 <span>{{ formatTime }}</span>, 超时后将取消订单</p></div><div class="amount"><span>应付总额:</span><span>¥{{ payInfo.payMoney?.toFixed(2) }}</span></div></div><!-- 付款方式 --><div class="pay-type"><p class="head">选择以下支付方式付款</p><div class="item"><p>支付平台</p><a class="btn wx" href="javascript:;"></a><a class="btn alipay" :href="payUrl"></a></div><div class="item"><p>支付方式</p><a class="btn" href="javascript:;">招商银行</a><a class="btn" href="javascript:;">工商银行</a><a class="btn" href="javascript:;">建设银行</a><a class="btn" href="javascript:;">农业银行</a><a class="btn" href="javascript:;">交通银行</a></div></div></div></div>
</template><style scoped lang="scss">
.xtx-pay-page {margin-top: 20px;
}.pay-info {background: #fff;display: flex;align-items: center;height: 240px;padding: 0 80px;.icon {font-size: 80px;color: #1dc779;}.tip {padding-left: 10px;flex: 1;p {&:first-child {font-size: 20px;margin-bottom: 5px;}&:last-child {color: #999;font-size: 16px;}}}.amount {span {&:first-child {font-size: 16px;color: #999;}&:last-child {color: $priceColor;font-size: 20px;}}}
}.pay-type {margin-top: 20px;background-color: #fff;padding-bottom: 70px;p {line-height: 70px;height: 70px;padding-left: 30px;font-size: 16px;&.head {border-bottom: 1px solid #f5f5f5;}}.btn {width: 150px;height: 50px;border: 1px solid #e4e4e4;text-align: center;line-height: 48px;margin-left: 30px;color: #666666;display: inline-block;&.active,&:hover {border-color: $xtxColor;}&.alipay {background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7b6b02396368c9314528c0bbd85a2e06.png) no-repeat center / contain;}&.wx {background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/c66f98cff8649bd5ba722c2e8067c6ca.jpg) no-repeat center / contain;}}
}
</style>
账号
askgxl8276@sandbox.com
登录密码
111111
支付密码
111111
会员中心
基本页面
router.js
import Member from '@/views/member/index.vue'
import MemberInfo from '@/views/member/components/UserInfo.vue'
import MemberOrder from '@/views/member/components/UserOrder.vue'{path: 'member',component: Member,children: [{path: 'user',component: MemberInfo},{path: 'order',component: MemberOrder},]}
LayoutNav.vue
<li><a href="javascript:;" @click="$router.push('/member')">会员中心</a></li>
UserInfo.vue
<script setup>import { useUserStore } from '@/stores/user'const userStore = useUserStore()
</script><template><div class="home-overview"><!-- 用户信息 --><div class="user-meta"><div class="avatar"><img :src="userStore.userInfo?.avatar" /></div><h4>{{ userStore.userInfo?.account }}</h4></div><div class="item"><a href="javascript:;"><span class="iconfont icon-hy"></span><p>会员中心</p></a><a href="javascript:;"><span class="iconfont icon-aq"></span><p>安全设置</p></a><a href="javascript:;"><span class="iconfont icon-dw"></span><p>地址管理</p></a></div></div><div class="like-container"><div class="home-panel"><div class="header"><h4 data-v-bcb266e0="">猜你喜欢</h4></div><div class="goods-list"><!-- <GoodsItem v-for="good in likeList" :key="good.id" :goods="good" /> --></div></div></div>
</template><style scoped lang="scss">
.home-overview {height: 132px;background: url(@/assets/images/center-bg.png) no-repeat center / cover;display: flex;.user-meta {flex: 1;display: flex;align-items: center;.avatar {width: 85px;height: 85px;border-radius: 50%;overflow: hidden;margin-left: 60px;img {width: 100%;height: 100%;}}h4 {padding-left: 26px;font-size: 18px;font-weight: normal;color: white;}}.item {flex: 1;display: flex;align-items: center;justify-content: space-around;&:first-child {border-right: 1px solid #f4f4f4;}a {color: white;font-size: 16px;text-align: center;.iconfont {font-size: 32px;}p {line-height: 32px;}}}
}.like-container {margin-top: 20px;border-radius: 4px;background-color: #fff;
}.home-panel {background-color: #fff;padding: 0 20px;margin-top: 20px;height: 400px;.header {height: 66px;border-bottom: 1px solid #f5f5f5;padding: 18px 0;display: flex;justify-content: space-between;align-items: baseline;h4 {font-size: 22px;font-weight: 400;}}.goods-list {display: flex;justify-content: space-around;}
}
</style>
UserOrder.vue
<script setup>
// tab列表
const tabTypes = [{ name: "all", label: "全部订单" },{ name: "unpay", label: "待付款" },{ name: "deliver", label: "待发货" },{ name: "receive", label: "待收货" },{ name: "comment", label: "待评价" },{ name: "complete", label: "已完成" },{ name: "cancel", label: "已取消" }
]
// 订单列表
const orderList = []</script><template><div class="order-container"><el-tabs><!-- tab切换 --><el-tab-pane v-for="item in tabTypes" :key="item.name" :label="item.label" /><div class="main-container"><div class="holder-container" v-if="orderList.length === 0"><el-empty description="暂无订单数据" /></div><div v-else><!-- 订单列表 --><div class="order-item" v-for="order in orderList" :key="order.id"><div class="head"><span>下单时间:{{ order.createTime }}</span><span>订单编号:{{ order.id }}</span><!-- 未付款,倒计时时间还有 --><span class="down-time" v-if="order.orderState === 1"><i class="iconfont icon-down-time"></i><b>付款截止: {{order.countdown}}</b></span></div><div class="body"><div class="column goods"><ul><li v-for="item in order.skus" :key="item.id"><a class="image" href="javascript:;"><img :src="item.image" alt="" /></a><div class="info"><p class="name ellipsis-2">{{ item.name }}</p><p class="attr ellipsis"><span>{{ item.attrsText }}</span></p></div><div class="price">¥{{ item.realPay?.toFixed(2) }}</div><div class="count">x{{ item.quantity }}</div></li></ul></div><div class="column state"><p>{{ order.orderState }}</p><p v-if="order.orderState === 3"><a href="javascript:;" class="green">查看物流</a></p><p v-if="order.orderState === 4"><a href="javascript:;" class="green">评价商品</a></p><p v-if="order.orderState === 5"><a href="javascript:;" class="green">查看评价</a></p></div><div class="column amount"><p class="red">¥{{ order.payMoney?.toFixed(2) }}</p><p>(含运费:¥{{ order.postFee?.toFixed(2) }})</p><p>在线支付</p></div><div class="column action"><el-button v-if="order.orderState === 1" type="primary"size="small">立即付款</el-button><el-button v-if="order.orderState === 3" type="primary" size="small">确认收货</el-button><p><a href="javascript:;">查看详情</a></p><p v-if="[2, 3, 4, 5].includes(order.orderState)"><a href="javascript:;">再次购买</a></p><p v-if="[4, 5].includes(order.orderState)"><a href="javascript:;">申请售后</a></p><p v-if="order.orderState === 1"><a href="javascript:;">取消订单</a></p></div></div></div><!-- 分页 --><div class="pagination-container"><el-pagination background layout="prev, pager, next" /></div></div></div></el-tabs></div></template><style scoped lang="scss">
.order-container {padding: 10px 20px;.pagination-container {display: flex;justify-content: center;}.main-container {min-height: 500px;.holder-container {min-height: 500px;display: flex;justify-content: center;align-items: center;}}
}.order-item {margin-bottom: 20px;border: 1px solid #f5f5f5;.head {height: 50px;line-height: 50px;background: #f5f5f5;padding: 0 20px;overflow: hidden;span {margin-right: 20px;&.down-time {margin-right: 0;float: right;i {vertical-align: middle;margin-right: 3px;}b {vertical-align: middle;font-weight: normal;}}}.del {margin-right: 0;float: right;color: #999;}}.body {display: flex;align-items: stretch;.column {border-left: 1px solid #f5f5f5;text-align: center;padding: 20px;>p {padding-top: 10px;}&:first-child {border-left: none;}&.goods {flex: 1;padding: 0;align-self: center;ul {li {border-bottom: 1px solid #f5f5f5;padding: 10px;display: flex;&:last-child {border-bottom: none;}.image {width: 70px;height: 70px;border: 1px solid #f5f5f5;}.info {width: 220px;text-align: left;padding: 0 10px;p {margin-bottom: 5px;&.name {height: 38px;}&.attr {color: #999;font-size: 12px;span {margin-right: 5px;}}}}.price {width: 100px;}.count {width: 80px;}}}}&.state {width: 120px;.green {color: $xtxColor;}}&.amount {width: 200px;.red {color: $priceColor;}}&.action {width: 140px;a {display: block;&:hover {color: $xtxColor;}}}}}
}
</style>
个人中心信息渲染
api/user.js
export const getLikeListAPI = ({ limit = 4 }) => {return httpInstance({url:'/goods/relevant',params: {limit }})
}
UserInfo.vue
<script setup>import { useUserStore } from '@/stores/user'import {getLikeListAPI} from '@/api/user'import { onMounted, ref } from 'vue'// 导入GoodsItem组件import GoodsItem from '@/views/home/components/GoodsItem.vue'const userStore = useUserStore()// 获取猜你喜欢列表const likeList = ref([])const getLikeList = async () => {const res = await getLikeListAPI({ limit: 4 })likeList.value = res.result}onMounted(() => getLikeList())
</script>
我的订单
api/user.js
export const getUserOrder = (params) => {return httpInstance({url:'/member/order',method:'GET',params})
}
UserOrder.vue
<script setup>
import { getUserOrder } from '@/api/user'
import { onMounted, ref } from 'vue'
// tab列表
const tabTypes = [{ name: "all", label: "全部订单" },{ name: "unpay", label: "待付款" },{ name: "deliver", label: "待发货" },{ name: "receive", label: "待收货" },{ name: "comment", label: "待评价" },{ name: "complete", label: "已完成" },{ name: "cancel", label: "已取消" }
]
// tab切换
const tabChange = (type) => {params.value.orderState = typegetOrderList()
}// 获取订单列表
const orderList = ref([])
const params = ref({orderState: 0,page: 1,pageSize: 2
})
const getOrderList = async () => {const res = await getUserOrder(params.value)orderList.value = res.result.itemstotal.value = res.result.counts
}
onMounted(() => getOrderList())</script>
<el-tabs @tab-change="tabChange">
分页
UserOrder.vue
<!-- 分页 --><div class="pagination-container"><el-pagination :total="total" @current-change="pageChange" :page-size="params.pageSize" background layout="prev, pager, next" /></div>
// 页数切换
const pageChange = (page) => {params.value.page = pagegetOrderList()
}
相关文章:
基于vue的黑马前端项目小兔鲜
目录 项目学习 初始化项目 建立项目 引入elementplus elementPlus主题设置 配置axios 路由 引入静态资源 自动导入scss变量 Layout页 组件结构快速搭建 字体图标渲染 一级导航渲染 吸顶导航交互实现 Pinia优化重复请求 Home页 分类实现 banner轮播图 …...
细节决定成败!jdbc的List<?> qryList4Sql(String sql)报错-标志符过长
问题产生背景: 在写sql时,想着简单直接就偷懒了,没有看清细节 操作步骤跟发现问题: 1. sql语句的执行选用的是jdbc提供的List<?> qryList4Sql(String sql) 方法 2,这是我的sql语句(简化处理) String sql "…...
ChatGLM Pytorch从0编写Transformer算法
预备工作 # !pip install http://download.pytorch.org/whl/cu80/torch-0.3.0.post4-cp36-cp36m-linux_x86_64.whl numpy matplotlib spacy torchtext seaborn import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import math, copy, tim…...
9.18算法
机器人重物1126 注意编号是方块的,而不是格点的 及如果为n*m的矩阵,需要开(n1)*(m1)的矩阵 //如果没有转向,就是走迷宫,结合记忆化,如果这个点之前走过就不走了 //又转向的话,就用一个变量记录当前转向&…...
【Spring Bean的生命周期】
文章目录 Spring Bean的生命周期实例化构造器实例化工厂方法实例化 属性赋值XML方式注解方式 初始化postProcessBeforeInitialization()和postProcessAfterInitialization()InitializingBean接口的afterPropertiesSet()方法通过Bean注解定义的初始化方法使用PostConstruct注解标…...
信息化发展49
软件设计 1 、软件设计是需求分析的延伸与拓展。需求分析阶段解决“做什么” 的问题,而软件设计阶段解决“怎么做” 的问题。同时, 它也是系统实施的基础, 为系统实施工作做好铺垫。合理的软件设计方案既可以保证系统的质量, 也可…...
linux常用命令(4):mkdir命令(创建目录)
文章目录 一、命令简介二、命令格式三、常用示例 一、命令简介 mkdir(make directories)创建目录。 若指定目录不存在则创建目录。若指定目录已存在,则会提示已存在而不继续创建。 touch与mkdir的区别? 很多人可能会把这个搞混淆ÿ…...
企业架构LNMP学习笔记58
开始学习Tomcat: 学习目标和内容: 1)能够描述Tomcat的使用场景; 2)能够简单描述Tomcat的工作原理; 3)能够实现部署安装Tomcat; 4)能够实现和配置Tomcat的Server服务…...
[JAVAee]SpringBoot配置文件
配置文件的介绍 配置文件当中记录了许多重要的配置信息,例如: 数据库的连接信息(用户的账户与密码)项目的启动端口第三方系统的调用密匙用于记录问题产生的日志 在spring框架中一些特定的框架会自动调用配置文件中的配置信息来运用. 配置文件中的属性也起到了类似全局变量的…...
复制远程连接到Linux使用VIM打开的内容到Windows
我们经常是使用SSH工具远程连接到Linux服务器上进行工作,有时候需要将Linux下使用VIM打开的文件内容复制到Windows上来,默认情况下,可能会复制不了,因为VIM默认情况下是使用的set mousea的设置,它会让鼠标选中的时候进…...
左神算法之中级提升班(9)
目录 【案例1】 【题目描述】 【思路解析】 【代码实现】 【案例2】 【题目描述】 【思路解析 平凡解技巧 从业务中分析终止条件 重点】 【代码实现】 【案例3】 【题目描述】 【思路解析】 【案例4】 【题目描述】 【思路解析】 【代码实现】 【动态规划代码】…...
SmartNews 基于 Flink 的 Iceberg 实时数据湖实践
摘要:本文整理自 SmartNews 数据平台架构师 Apache Iceberg Contributor 戢清雨,在 Flink Forward Asia 2022 实时湖仓专场的分享。本篇内容主要分为五个部分: SmartNews 数据湖介绍基于 Icebergv1 格式的数据湖实践基于 Flink 实时更新的数据…...
websocket请求通过IteratorAggregate实现流式输出
对接国内讯飞星火模型,官方文档接口采用的是websocket跟国外chatgpt有些差异。 虽然官网给出一个简单demo通过while(true),websocket的receive()可以实现逐条接受并输出给前端,但是通用和灵活度不高。不能兼容现有项目框架的流式输出。故模仿…...
《C和指针》笔记28:可变参数和stdarg宏
可变参数列表可以通过宏来实现,这些宏定义于stdarg.h头文件,它是标准库的一部分。这个头文件声明了一个类型va_list和三个宏——va_start、va_arg和va_end 。我们可以声明一个类型为va_list的变量,与这几个宏配合使用,访问参数的值…...
Matlab论文插图绘制模板第114期—带图形标记的图
之前的文章中,分享了Matlab带线标记的图: 带阴影标记的图: 带箭头标记的图: 进一步,分享一下带图形标记的图,先来看一下成品效果: 特别提示:本期内容『数据代码』已上传资源群中&…...
Python:用于有效对象管理的单例模式
1. 写在前面 在本文中,我们将介绍一种常用的软件设计模式 —— 单例模式。 通过示例,演示单例创建,并确保该实例在整个应用程序生命周期中保持一致。同时探讨它在 Python 中的目的、益处和实际应用。 关键点: 1、单例模式只有…...
【TCP】滑动窗口、流量控制 以及拥塞控制
滑动窗口、流量控制 以及拥塞控制 1. 滑动窗口(效率机制)2. 流量控制(安全机制)3. 拥塞控制(安全机制) 1. 滑动窗口(效率机制) TCP 使用 确认应答 策略,对每一个发送的数…...
Xilinx FPGA管脚约束语法规则(UCF和XDC文件)
文章目录 1. ISE环境(UCF文件)2. Vivado环境(XDC文件) 本文介绍ISE和Vivado管脚约束的语句使用,仅仅是管脚和电平状态指定,不包括时钟约束等其他语法。 ISE使用UCF文件格式,Vivado使用XDC文件&…...
服务网格和CI/CD集成:讨论服务网格在持续集成和持续交付中的应用。
🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 &a…...
代码随想录训练营第56天|583.两个字符串的删除操作,72.编辑距离
代码随想录训练营第56天|583.两个字符串的删除操作,72.编辑距离 583.两个字符串的删除操作文章思路代码 72.编辑距离文章思路代码 总结 583.两个字符串的删除操作 文章 代码随想录|0583.两个字符串的删除操作 思路 如果不按照编辑距离考虑的话,只需要…...
【JDK 8-Lambda】3.1 Java高级核心玩转 JDK8 Lambda 表达式
一、 什么是函数式编程 ? 二、 什么是lambda表达式? 1. 先看两个示例 A.【创建线程】 B.【数组排序-降序】 2. lambda表达式特性 A. 使用场景(前提): B. 语法 (params) -> expression C. 参数列表 D. 方法体 F. 好处 一、 什么是函数式编…...
【C#】XML的基础知识以及读取XML文件
最近在学读取文件 目录 介绍特点结构XML的语法规则XML 命名规则 C#操作XML新建读取第一种第二种第三种 读取属性 介绍 XML (可扩展标记语言,eXtensible Markup Language) 是一种标记语言,它被设计用来传输和存储数据。 特点 可扩展性:由于…...
Immutable.js简介
引子 看一段大家熟悉的代码 const state {str: wwming,obj: {y: 1},arr: [1, 2, 3] } const newState stateconsole.log(newState state) // truenewState和state是相等的 原因: 由于js的对象和数组都是引用类型。所以newState的state实际上是指向于同一块内存…...
C语言进阶教程(位操作和进制数的表示)
文章目录 前言一、左移和右移二、清除对应的位为0和设置对应的位为11.设置对应的位为12.清除对应的位为0 三、进制数的表示四、& ^ | ~总结 前言 本篇文章给大家讲解一下C语言中的位操作,在嵌入式中位操作是经常需要使用的,那么下面就让我们来学习一…...
Loguru:功能强大、简单易用的Python日志库
文章目录 Loguru:Python的日志库安装 Loguru基本用法配置 Loguruadd() 语句remove() 语句设置日志文件保留日志的等级设置控制台日志显示等级Loguru:Python的日志库 Loguru 是一个功能强大、简单易用的日志库,可以让 Python 的日志记录变得更加轻松。它提供了丰富的功能和配…...
idea之maven的安装与配置
我们到maven的官网里下载maven,地址:https://maven.apache.org/download.cgi下载完成后解压即可配置环境变量 此电脑–>右键–>属性–>高级系统设置–>环境变量–>系统变量(S)–>新建一个系统变量 变量名&…...
【最新面试问题记录持续更新,java,kotlin,android,flutter】
最近找工作,复习了下java相关的知识。发现已经对很多概念模糊了。记录一下。部分是往年面试题重新整理,部分是自己面试遇到的问题。持续更新中~ 目录 java相关1. 面向对象设计原则2. 面向对象的特征是什么3. 重载和重写4. 基本数据类型5. 装箱和拆箱6. …...
面试:经典问题解决思路
1. 秒杀系统架构 参考:秒杀系统架构优化思路 2. 如何防止订单重复提交 重复提交原因: 一种是由于用户在短时间内多次点击下单按钮,或浏览器刷新按钮导致。另一种则是由于Nginx或类似于SpringCloud Gateway的网关层,进行超时重试造成的。 方案…...
CG MAGIC分享3ds Max卡顿未保存处理方法有哪些?
3ds Max进行建模、渲染这一系列过程中,大家使用中都会遇到各种原因导致软件卡顿或崩溃是很常见的情况。 可以说卡机没关系,可是卡顿发生时,如果之前的工作没有及时保存,可能会导致数据的丢失和时间的浪费。这就是最让人烦躁的了&…...
[python 刷题] 238 Product of Array Except Self
[python 刷题] 238 Product of Array Except Self 题目: Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]. The product of any prefix or suffix of nums is guar…...
龙岗 网站建设/手机流畅优化软件
大家都知道rhel想要跟新软件都是要注册的,对于平民百姓来说那费用还是有点高的。呵呵,看了网上的资料写的都是杂乱无章的,今天刚好有时间,整理下我的redhat as5.X 的yum配置,希望对大家有所帮助 配置rhel 5 使用CentOS…...
上海市住房和城乡建设管理委员会门户网站/seo关键词优化提高网站排名
搬到姨家还没一段时间,跟姨睡一张床也还没多久,我就又要一个人睡一个房间了,陪伴我的又是除了电脑就是电视,我不喜欢这样,更多时候我希望晚上的自己是忙碌的,不希望自己有空闲时间,一直我都是喜…...
wordpress注册页面修改/百度搜索引擎怎么弄
目录题目描述动归五部曲代码如下题目描述 动归五部曲 代码如下 class Solution {public int numTrees(int n) {int[] dp new int[n1];dp[0] 1;for(int i 1; i<n ; i){for(int j 1; j <i; j){dp[i] dp[j-1] * dp[i-j]; // 注意,这里是 不是 ,…...
wordpress网站正在建设中/微友圈推广平台怎么加入
文章目录 零、写在前面一、题目描述二、解题思路三、代码详解四、推荐专栏五、习题练习零、写在前面 目前本专栏正在进行优惠活动,在博主主页添加博主好友(好友位没有满的话),可以获取 付费专栏优惠券。 今天是学习 「 C语言 」 打卡的第三十一天,学习方式很简单,每天…...
让网站限制国内打不开/精品成品网站源码
ROS系列——错误syntax error near unexpected token $do\r说明解决方法问题原因解决1.终端运行2.本文使用的方法,适用于代码行数较少其他方法,本质就是替换3.重新运行脚本说明 在运行.sh脚本时,报错: syntax error near unexpec…...
昆明网站排名优化公司/廊坊seo关键词优化
6.引用 1.什么是引用? 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。 **类型& 引用变量名(对象名) 引用实体;**void TestRef(…...