目录
视频:黑马程序员-小兔鲜
黑马资料:文档
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/*"]
- }
- }
- }
- // 引入插件
- 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>
重启运行。

测试完成。
安装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
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可以看到不同页面。
链接:https://pan.baidu.com/s/15PoJhfpPDzf_WTsakO0jmg?pwd=3kgh
提取码:3kgh
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">testdiv>
- 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>

测试成功,还原代码。

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只发送一个请求。
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/components下新建5个vue文件
- <script setup>
- </script>
-
- <template>
- <div> 我是分类 </div>
- </template>
- <script setup>
- </script>
-
- <template>
- <div> 我是banner </div>
- </template>
- <script setup>
- </script>
-
- <template>
- <div> 人气推荐 </div>
- </template>
- <script setup>
- </script>
-
- <template>
- <div> 新鲜好物 </div>
- </template>
- <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>

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 指令等于号后面绑定的表达式的值 图片url
- console.log(el, binding.value)
- useIntersectionObserver(
- el,
- ([{ isIntersecting }]) => {
- console.log(isIntersecting)
- if (isIntersecting) {
- // 进入视口区域
- el.src = binding.value
- stop()
- }
- },
- )
- }
- })
- }
- }
修改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>
这里没有用懒加载的形式,因为总有一些图片加载不出来,原因不明。

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.$route
- const 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>

api/home.js修改
- export function getBannerAPI (params = {}) {
- // 默认为1 商品为2
- const { distributionSite = '1' } = params
- return 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.$route
- const res = await findTopCategoryAPI(id)
- categoryData.value = res.result
- }
- getCategory(route.params.id)
- //获取banner
- const 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.$route
- const res = await findTopCategoryAPI(id)
- categoryData.value = res.result
- }
- getCategory(route.params.id)
- //获取banner
- const 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 () {
- //获取banner
- const 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.$route
- const 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 = 1
- getGoodList()
- }
- // 加载更多
- 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>
-
-
-
-
-
- <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>
-
- <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) return
- console.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 * 2
- positionY.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-position
- background-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"/>


XtxSku
链接:https://pan.baidu.com/s/1KQ5-OFMoD7bpL8DJjsmsIA?pwd=3kgh
提取码:3kgh
detain/index.vue修改
- import XtxSku from '@/components/XtxSku/index.vue'
-
-
- <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.value
- formRef.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)
- })

stores/user.js
- // 管理用户数据相关
-
- import { defineStore } from 'pinia'
- import { ref } from 'vue'
- import { loginAPI } from '@/api/user'
-
- export const useUserStore = defineStore('user', () => {
- // 1. 定义管理用户数据的state
- const userInfo = ref({})
- // 2. 定义获取接口数据的action函数
- const getUserInfo = async ({ account, password }) => {
- const res = await loginAPI({ account, password })
- userInfo.value = res.result
- }
- // 3. 以对象的格式把state和action return
- return {
- userInfo,
- getUserInfo
- }
- }, {
- persist: true,
- })

需要使用插件,自动本地存储,官网
下载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>

http.js
- import {useUserStore} from '@/stores/user'
-
- http.interceptors.request.use(config => {
- // 1. 从pinia获取token数据
- const userStore = useUserStore()
- // 2. 按照后端的要求拼接token数据
- const token = userStore.userInfo.token
- if (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. 定义管理用户数据的state
- const 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 return
- return {
- 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>
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 - cartList
- const cartList = ref([])
- // 2. 定义action - addCart
- const 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 - cartList
- const cartList = ref([])
- // 2. 定义action - addCart
- const 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 - cartList
- const cartList = ref([])
- // 2. 定义action - addCart
- const addCart = async (goods) => {
- const {skuId,count} = goods
- if(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修改为传过来的selected
- const 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. 定义管理用户数据的state
- const userInfo = ref({})
- // 2. 定义获取接口数据的action函数
- const getUserInfo = async ({ account, password }) => {
- const res = await loginAPI({ account, password })
- userInfo.value = res.result
- mergeCartAPI(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 return
- return {
- 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.result
- const 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.value
- showDialog.value = false
- activeAddress.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.id
- router.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 = null
- const time = ref(0)
- // 格式化时间 为 xx分xx秒
- const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
- // 2. 开启倒计时的函数
- const start = (currentTime) => {
- // 开始倒计时的逻辑
- // 核心逻辑的编写:每隔1s就减一
- time.value = currentTime
- timer = 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.result
- start(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 = type
- getOrderList()
- }
-
- // 获取订单列表
- 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.items
- total.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 = page
- getOrderList()
- }
