• 基于vue的黑马前端项目小兔鲜


    目录

    项目学习 

    初始化项目 

    建立项目 

    引入elementplus

    elementPlus主题设置 

    配置axios

    路由 

    引入静态资源

    自动导入scss变量

    Layout页

    组件结构快速搭建

     字体图标渲染

    一级导航渲染 

    吸顶导航交互实现 

    Pinia优化重复请求

    Home页 

    分类实现

    banner轮播图 

    新鲜好物实现

    人气推荐实现 

    懒加载指令实现 

    产品列表实现

    GoodsItem组件封装 

    一级分类页 

    导航栏

    分类Banner渲染 

    导航激活 

    分类列表渲染 

    路由缓存问题 

    业务逻辑的函数拆分 

    二级分类页 

    实现

    定制路由的滚动行为

    商品详情

    路由配置

    数据渲染

    热榜区域 

    适配热榜类型 

    图片预览组件封装

    sku插件

     登录页

    表单

    登录实现 

     登录失败的提示

     Pinia管理用户

    Pinia持久化存储

    登录和非登录状态

    请求拦截器携带token

    退出登录

    token失效处理

    购物车

    本地加入购物车

    头部购物车

    购物车页面

    订单页 

    支付页 

    会员中心 

    基本页面

    个人中心信息渲染

    我的订单 

    分页


    项目学习 

    视频:黑马程序员-小兔鲜

    黑马资料:文档

    vue调试:vue-devtools

    DEV:HBuilder

    初始化项目 

    建立项目 

    安装node

    安装过node的需要查看node的版本是否大于或等于15,否则报错

     Error: @vitejs/plugin-vue requires vue (>=3.2.13) or @vue/compiler-sfc to be present in the dependency tree.

    windows下更新node需要重新安装,即覆盖原有的node。 

    查看镜像地址 

    npm config get registry

    设置淘宝镜像地址 

    npm config set registry https://registry.npm.taobao.org/ 

    安装脚手架 

    npm i -g @vue/cli

    下载create-vue

    npm install create-vue@3.7.3

    注意版本,或者安装最新版。

    初始化项目

    安装依赖

    npm install

    配置别名路径

    在根目录下新建jsconfig.json

    1. {
    2. "compilerOptions" : {
    3. "baseUrl" : "./",
    4. "paths" : {
    5. "@/*":["src/*"]
    6. }
    7. }
    8. }

    引入elementplus

    官网

    1. // 引入插件
    2. import AutoImport from 'unplugin-auto-import/vite'
    3. import Components from 'unplugin-vue-components/vite'
    4. import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
    5. export default defineConfig({
    6. plugins: [
    7. // 配置插件
    8. AutoImport({
    9. resolvers: [ElementPlusResolver()],
    10. }),
    11. Components({
    12. resolvers: [ElementPlusResolver()],
    13. }),
    14. ]
    15. })

    vit.config.js

    1. <script setup>
    2. import { RouterLink, RouterView } from 'vue-router'
    3. import HelloWorld from './components/HelloWorld.vue'
    4. </script>
    5. <template>
    6. <header>
    7. <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
    8. <div class="wrapper">
    9. <HelloWorld msg="You did it!" />
    10. <nav>
    11. <RouterLink to="/">Home</RouterLink>
    12. <RouterLink to="/about">About</RouterLink>
    13. </nav>
    14. </div>
    15. </header>
    16. <el-button type="primary">Primary</el-button>
    17. <RouterView />
    18. </template>
    19. <style scoped>
    20. header {
    21. line-height: 1.5;
    22. max-height: 100vh;
    23. }
    24. .logo {
    25. display: block;
    26. margin: 0 auto 2rem;
    27. }
    28. nav {
    29. width: 100%;
    30. font-size: 12px;
    31. text-align: center;
    32. margin-top: 2rem;
    33. }
    34. nav a.router-link-exact-active {
    35. color: var(--color-text);
    36. }
    37. nav a.router-link-exact-active:hover {
    38. background-color: transparent;
    39. }
    40. nav a {
    41. display: inline-block;
    42. padding: 0 1rem;
    43. border-left: 1px solid var(--color-border);
    44. }
    45. nav a:first-of-type {
    46. border: 0;
    47. }
    48. @media (min-width: 1024px) {
    49. header {
    50. display: flex;
    51. place-items: center;
    52. padding-right: calc(var(--section-gap) / 2);
    53. }
    54. .logo {
    55. margin: 0 2rem 0 0;
    56. }
    57. header .wrapper {
    58. display: flex;
    59. place-items: flex-start;
    60. flex-wrap: wrap;
    61. }
    62. nav {
    63. text-align: left;
    64. margin-left: -1rem;
    65. font-size: 1rem;
    66. padding: 1rem 0;
    67. margin-top: 1rem;
    68. }
    69. }
    70. </style>

    测试以下elementplus是否生效。

    在app.vue的template下引入

    <el-button type="primary">Primary</el-button>

     重启运行。

    测试完成。

    elementPlus主题设置 

    安装sass

    npm i sass -D

    样式文件

    styles/element/index.scss

    1. /* 只需要重写你需要的即可 */
    2. @forward 'element-plus/theme-chalk/src/common/var.scss' with (
    3. $colors: (
    4. 'primary': (
    5. // 主色
    6. 'base': #27ba9b,
    7. ),
    8. 'success': (
    9. // 成功色
    10. 'base': #1dc779,
    11. ),
    12. 'warning': (
    13. // 警告色
    14. 'base': #ffb302,
    15. ),
    16. 'danger': (
    17. // 危险色
    18. 'base': #e26237,
    19. ),
    20. 'error': (
    21. // 错误色
    22. 'base': #cf4444,
    23. ),
    24. )
    25. )

    配置

    vite.config.js

    1. import { fileURLToPath, URL } from 'node:url'
    2. import { defineConfig } from 'vite'
    3. import vue from '@vitejs/plugin-vue'
    4. //elementplus按需导入
    5. import AutoImport from 'unplugin-auto-import/vite'
    6. import Components from 'unplugin-vue-components/vite'
    7. import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
    8. // https://vitejs.dev/config/
    9. export default defineConfig({
    10. plugins: [
    11. vue(),
    12. AutoImport({
    13. resolvers: [ElementPlusResolver()],
    14. }),
    15. Components({
    16. resolvers: [
    17. //配置elementPlus采用sass样式配色系统
    18. ElementPlusResolver({importStyle:"sass"}),
    19. ],
    20. }),
    21. ],
    22. resolve: {
    23. alias: {
    24. '@': fileURLToPath(new URL('./src', import.meta.url))
    25. }
    26. },
    27. css: {
    28. preprocessorOptions: {
    29. scss: {
    30. // 自动导入定制化样式文件进行样式覆盖
    31. additionalData: `
    32. @use "@/styles/element/index.scss" as *;
    33. `,
    34. }
    35. },
    36. }
    37. })

    测试

    重启运行

    配置axios

    安装axios

    npm i axios

    配置 

    utils/http.js

    1. import axios from 'axios'
    2. // 创建axios实例
    3. let http = axios.create({
    4. baseURL: 'http://pcapi-xiaotuxian-front-devtest.itheima.net',
    5. timeout: 5000
    6. })
    7. // axios请求拦截器
    8. http.interceptors.request.use(config => {
    9. return config
    10. }, e => Promise.reject(e))
    11. // axios响应式拦截器
    12. http.interceptors.response.use(res => res.data, e => {
    13. return Promise.reject(e)
    14. })
    15. export default http

     测试接口

    api/testAPI.js下

    1. import http from '@/utils/http'
    2. export function getCategory () {
    3. return http({
    4. url: 'home/category/head'
    5. })
    6. }

    main.js下新增

    1. //测试接口
    2. import {getCategory} from '@/api/testAPI'
    3. getCategory().then(res => {
    4. console.log(res)
    5. })

    重启运行

    路由 

    views/Login/index.vue

    1. <template>
    2. 我是登录页
    3. </template>

    views/Layout/index.vue

    1. <template>
    2. 我是首页
    3. </template>

    views/Home/index.vue

    1. <template>
    2. 我是home页
    3. </template>

    views/Category/index.vue

    1. <template>
    2. 我是分类页
    3. </template>

    router/index.js

    1. import { createRouter, createWebHistory } from 'vue-router'
    2. import HomeView from '../views/HomeView.vue'
    3. import Login from '@/views/login/index.vue'
    4. import Layout from '@/views/layout/index.vue'
    5. import Home from '@/views/home/index.vue'
    6. import Category from '@/views/category/index.vue'
    7. const router = createRouter({
    8. history: createWebHistory(import.meta.env.BASE_URL),
    9. routes: [
    10. {
    11. path: '/',
    12. component: Layout,
    13. children: [
    14. {
    15. path: '',
    16. component: Home
    17. },
    18. {
    19. path: 'category',
    20. component: Category
    21. }
    22. ]
    23. },
    24. {
    25. path: '/login',
    26. component: Login
    27. }
    28. ]
    29. })
    30. export default router

    重启输入对应url可以看到不同页面。

    引入静态资源

    1. 图片资源 - 把 images 文件夹放到 assets 目录下
    2. 样式资源 - 把 common.scss 文件放到 styles 目录下

    链接:https://pan.baidu.com/s/15PoJhfpPDzf_WTsakO0jmg?pwd=3kgh 
    提取码:3kgh

    自动导入scss变量

    var.scss

    1. $xtxColor: #27ba9b;
    2. $helpColor: #e26237;
    3. $sucColor: #1dc779;
    4. $warnColor: #ffb302;
    5. $priceColor: #cf4444;

     修改main.js

    1. css: {
    2. preprocessorOptions: {
    3. scss: {
    4. // 自动导入scss文件
    5. additionalData: `
    6. @use "@/styles/element/index.scss" as *;
    7. @use "@/styles/var.scss" as *;
    8. `,
    9. }
    10. }
    11. }

    测试 

    修改App.vue

    1. <script setup>
    2. import { RouterLink, RouterView } from 'vue-router'
    3. import HelloWorld from './components/HelloWorld.vue'
    4. script>
    5. <template>
    6. <RouterView />
    7. <div class="test">testdiv>
    8. template>
    9. <style scoped lang="scss">
    10. header {
    11. line-height: 1.5;
    12. max-height: 100vh;
    13. }
    14. .test {
    15. color: $priceColor;
    16. }
    17. .logo {
    18. display: block;
    19. margin: 0 auto 2rem;
    20. }
    21. nav {
    22. width: 100%;
    23. font-size: 12px;
    24. text-align: center;
    25. margin-top: 2rem;
    26. }
    27. nav a.router-link-exact-active {
    28. color: var(--color-text);
    29. }
    30. nav a.router-link-exact-active:hover {
    31. background-color: transparent;
    32. }
    33. nav a {
    34. display: inline-block;
    35. padding: 0 1rem;
    36. border-left: 1px solid var(--color-border);
    37. }
    38. nav a:first-of-type {
    39. border: 0;
    40. }
    41. @media (min-width: 1024px) {
    42. header {
    43. display: flex;
    44. place-items: center;
    45. padding-right: calc(var(--section-gap) / 2);
    46. }
    47. .logo {
    48. margin: 0 2rem 0 0;
    49. }
    50. header .wrapper {
    51. display: flex;
    52. place-items: flex-start;
    53. flex-wrap: wrap;
    54. }
    55. nav {
    56. text-align: left;
    57. margin-left: -1rem;
    58. font-size: 1rem;
    59. padding: 1rem 0;
    60. margin-top: 1rem;
    61. }
    62. }
    63. style>

    测试成功,还原代码。

    Layout页

    组件结构快速搭建

    LayoutNav.vue

    1. <script setup>
    2. </script>
    3. <template>
    4. <nav class="app-topnav">
    5. <div class="container">
    6. <ul>
    7. <template v-if="true">
    8. <li><a href="javascript:;"><i class="iconfont icon-user"></i>周杰伦</a></li>
    9. <li>
    10. <el-popconfirm title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
    11. <template #reference>
    12. <a href="javascript:;">退出登录</a>
    13. </template>
    14. </el-popconfirm>
    15. </li>
    16. <li><a href="javascript:;">我的订单</a></li>
    17. <li><a href="javascript:;">会员中心</a></li>
    18. </template>
    19. <template v-else>
    20. <li><a href="javascript:;">请先登录</a></li>
    21. <li><a href="javascript:;">帮助中心</a></li>
    22. <li><a href="javascript:;">关于我们</a></li>
    23. </template>
    24. </ul>
    25. </div>
    26. </nav>
    27. </template>
    28. <style scoped lang="scss">
    29. .app-topnav {
    30. background: #333;
    31. ul {
    32. display: flex;
    33. height: 53px;
    34. justify-content: flex-end;
    35. align-items: center;
    36. li {
    37. a {
    38. padding: 0 15px;
    39. color: #cdcdcd;
    40. line-height: 1;
    41. display: inline-block;
    42. i {
    43. font-size: 14px;
    44. margin-right: 2px;
    45. }
    46. &:hover {
    47. color: $xtxColor;
    48. }
    49. }
    50. ~li {
    51. a {
    52. border-left: 2px solid #666;
    53. }
    54. }
    55. }
    56. }
    57. }
    58. </style>

    LayoutHeader.vue

    1. <script setup>
    2. </script>
    3. <template>
    4. <header class='app-header'>
    5. <div class="container">
    6. <h1 class="logo">
    7. <RouterLink to="/">小兔鲜</RouterLink>
    8. </h1>
    9. <ul class="app-header-nav">
    10. <li class="home">
    11. <RouterLink to="/">首页</RouterLink>
    12. </li>
    13. <li> <RouterLink to="/">居家</RouterLink> </li>
    14. <li> <RouterLink to="/">美食</RouterLink> </li>
    15. <li> <RouterLink to="/">服饰</RouterLink> </li>
    16. </ul>
    17. <div class="search">
    18. <i class="iconfont icon-search"></i>
    19. <input type="text" placeholder="搜一搜">
    20. </div>
    21. <!-- 头部购物车 -->
    22. </div>
    23. </header>
    24. </template>
    25. <style scoped lang='scss'>
    26. .app-header {
    27. background: #fff;
    28. .container {
    29. display: flex;
    30. align-items: center;
    31. }
    32. .logo {
    33. width: 200px;
    34. a {
    35. display: block;
    36. height: 132px;
    37. width: 100%;
    38. text-indent: -9999px;
    39. background: url('@/assets/images/logo.png') no-repeat center 18px / contain;
    40. }
    41. }
    42. .app-header-nav {
    43. width: 820px;
    44. display: flex;
    45. padding-left: 40px;
    46. position: relative;
    47. z-index: 998;
    48. li {
    49. margin-right: 40px;
    50. width: 38px;
    51. text-align: center;
    52. a {
    53. font-size: 16px;
    54. line-height: 32px;
    55. height: 32px;
    56. display: inline-block;
    57. &:hover {
    58. color: $xtxColor;
    59. border-bottom: 1px solid $xtxColor;
    60. }
    61. }
    62. .active {
    63. color: $xtxColor;
    64. border-bottom: 1px solid $xtxColor;
    65. }
    66. }
    67. }
    68. .search {
    69. width: 170px;
    70. height: 32px;
    71. position: relative;
    72. border-bottom: 1px solid #e7e7e7;
    73. line-height: 32px;
    74. .icon-search {
    75. font-size: 18px;
    76. margin-left: 5px;
    77. }
    78. input {
    79. width: 140px;
    80. padding-left: 5px;
    81. color: #666;
    82. }
    83. }
    84. .cart {
    85. width: 50px;
    86. .curr {
    87. height: 32px;
    88. line-height: 32px;
    89. text-align: center;
    90. position: relative;
    91. display: block;
    92. .icon-cart {
    93. font-size: 22px;
    94. }
    95. em {
    96. font-style: normal;
    97. position: absolute;
    98. right: 0;
    99. top: 0;
    100. padding: 1px 6px;
    101. line-height: 1;
    102. background: $helpColor;
    103. color: #fff;
    104. font-size: 12px;
    105. border-radius: 10px;
    106. font-family: Arial;
    107. }
    108. }
    109. }
    110. }
    111. </style>

    LayoutFooter.vue

    1. <template>
    2. <footer class="app_footer">
    3. <!-- 联系我们 -->
    4. <div class="contact">
    5. <div class="container">
    6. <dl>
    7. <dt>客户服务</dt>
    8. <dd><i class="iconfont icon-kefu"></i> 在线客服</dd>
    9. <dd><i class="iconfont icon-question"></i> 问题反馈</dd>
    10. </dl>
    11. <dl>
    12. <dt>关注我们</dt>
    13. <dd><i class="iconfont icon-weixin"></i> 公众号</dd>
    14. <dd><i class="iconfont icon-weibo"></i> 微博</dd>
    15. </dl>
    16. <dl>
    17. <dt>下载APP</dt>
    18. <dd class="qrcode"><img src="@/assets/images/qrcode.jpg" /></dd>
    19. <dd class="download">
    20. <span>扫描二维码</span>
    21. <span>立马下载APP</span>
    22. <a href="javascript:;">下载页面</a>
    23. </dd>
    24. </dl>
    25. <dl>
    26. <dt>服务热线</dt>
    27. <dd class="hotline">400-0000-000 <small>周一至周日 8:00-18:00</small></dd>
    28. </dl>
    29. </div>
    30. </div>
    31. <!-- 其它 -->
    32. <div class="extra">
    33. <div class="container">
    34. <div class="slogan">
    35. <a href="javascript:;">
    36. <i class="iconfont icon-footer01"></i>
    37. <span>价格亲民</span>
    38. </a>
    39. <a href="javascript:;">
    40. <i class="iconfont icon-footer02"></i>
    41. <span>物流快捷</span>
    42. </a>
    43. <a href="javascript:;">
    44. <i class="iconfont icon-footer03"></i>
    45. <span>品质新鲜</span>
    46. </a>
    47. </div>
    48. <!-- 版权信息 -->
    49. <div class="copyright">
    50. <p>
    51. <a href="javascript:;">关于我们</a>
    52. <a href="javascript:;">帮助中心</a>
    53. <a href="javascript:;">售后服务</a>
    54. <a href="javascript:;">配送与验收</a>
    55. <a href="javascript:;">商务合作</a>
    56. <a href="javascript:;">搜索推荐</a>
    57. <a href="javascript:;">友情链接</a>
    58. </p>
    59. <p>CopyRight © 小兔鲜儿</p>
    60. </div>
    61. </div>
    62. </div>
    63. </footer>
    64. </template>
    65. <style scoped lang='scss'>
    66. .app_footer {
    67. overflow: hidden;
    68. background-color: #f5f5f5;
    69. padding-top: 20px;
    70. .contact {
    71. background: #fff;
    72. .container {
    73. padding: 60px 0 40px 25px;
    74. display: flex;
    75. }
    76. dl {
    77. height: 190px;
    78. text-align: center;
    79. padding: 0 72px;
    80. border-right: 1px solid #f2f2f2;
    81. color: #999;
    82. &:first-child {
    83. padding-left: 0;
    84. }
    85. &:last-child {
    86. border-right: none;
    87. padding-right: 0;
    88. }
    89. }
    90. dt {
    91. line-height: 1;
    92. font-size: 18px;
    93. }
    94. dd {
    95. margin: 36px 12px 0 0;
    96. float: left;
    97. width: 92px;
    98. height: 92px;
    99. padding-top: 10px;
    100. border: 1px solid #ededed;
    101. .iconfont {
    102. font-size: 36px;
    103. display: block;
    104. color: #666;
    105. }
    106. &:hover {
    107. .iconfont {
    108. color: $xtxColor;
    109. }
    110. }
    111. &:last-child {
    112. margin-right: 0;
    113. }
    114. }
    115. .qrcode {
    116. width: 92px;
    117. height: 92px;
    118. padding: 7px;
    119. border: 1px solid #ededed;
    120. }
    121. .download {
    122. padding-top: 5px;
    123. font-size: 14px;
    124. width: auto;
    125. height: auto;
    126. border: none;
    127. span {
    128. display: block;
    129. }
    130. a {
    131. display: block;
    132. line-height: 1;
    133. padding: 10px 25px;
    134. margin-top: 5px;
    135. color: #fff;
    136. border-radius: 2px;
    137. background-color: $xtxColor;
    138. }
    139. }
    140. .hotline {
    141. padding-top: 20px;
    142. font-size: 22px;
    143. color: #666;
    144. width: auto;
    145. height: auto;
    146. border: none;
    147. small {
    148. display: block;
    149. font-size: 15px;
    150. color: #999;
    151. }
    152. }
    153. }
    154. .extra {
    155. background-color: #333;
    156. }
    157. .slogan {
    158. height: 178px;
    159. line-height: 58px;
    160. padding: 60px 100px;
    161. border-bottom: 1px solid #434343;
    162. display: flex;
    163. justify-content: space-between;
    164. a {
    165. height: 58px;
    166. line-height: 58px;
    167. color: #fff;
    168. font-size: 28px;
    169. i {
    170. font-size: 50px;
    171. vertical-align: middle;
    172. margin-right: 10px;
    173. font-weight: 100;
    174. }
    175. span {
    176. vertical-align: middle;
    177. text-shadow: 0 0 1px #333;
    178. }
    179. }
    180. }
    181. .copyright {
    182. height: 170px;
    183. padding-top: 40px;
    184. text-align: center;
    185. color: #999;
    186. font-size: 15px;
    187. p {
    188. line-height: 1;
    189. margin-bottom: 20px;
    190. }
    191. a {
    192. color: #999;
    193. line-height: 1;
    194. padding: 0 10px;
    195. border-right: 1px solid #999;
    196. &:last-child {
    197. border-right: none;
    198. }
    199. }
    200. }
    201. }
    202. </style>

    Layout/index.vue

    1. <script setup>
    2. import LayoutNav from './components/LayoutNav.vue'
    3. import LayoutHeader from './components/LayoutHeader.vue'
    4. import LayoutFooter from './components/LayoutFooter.vue'
    5. </script>
    6. <template>
    7. <LayoutNav />
    8. <LayoutHeader />
    9. <RouterView />
    10. <LayoutFooter />
    11. </template>

    重启项目

     字体图标渲染

    根目录下index.html引入

      <link rel="stylesheet" href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css">

    一级导航渲染 

    api/layout.js 

    1. import httpInstance from '@/utils/http'
    2. export function getCategoryAPI () {
    3. return httpInstance({
    4. url: '/home/category/head'
    5. })
    6. }

    LayoutHeader.vue

    1. <script setup>
    2. import { getCategoryAPI } from '@/api/layout'
    3. import { onMounted, ref } from 'vue'
    4. const categoryList = ref([])
    5. const getCategory = async () => {
    6. const res = await getCategoryAPI()
    7. categoryList.value = res.result
    8. }
    9. onMounted(() => getCategory())
    10. script>
    11. <template>
    12. <header class='app-header'>
    13. <div class="container">
    14. <h1 class="logo">
    15. <RouterLink to="/">小兔鲜RouterLink>
    16. h1>
    17. <ul class="app-header-nav">
    18. <li class="home" v-for="item in categoryList" :key="item.id">
    19. <RouterLink to="/">{{ item.name }}RouterLink>
    20. li>
    21. ul>
    22. <div class="search">
    23. <i class="iconfont icon-search">i>
    24. <input type="text" placeholder="搜一搜">
    25. div>
    26. div>
    27. header>
    28. template>
    29. <style scoped lang='scss'>
    30. .app-header {
    31. background: #fff;
    32. .container {
    33. display: flex;
    34. align-items: center;
    35. }
    36. .logo {
    37. width: 200px;
    38. a {
    39. display: block;
    40. height: 132px;
    41. width: 100%;
    42. text-indent: -9999px;
    43. background: url('@/assets/images/logo.png') no-repeat center 18px / contain;
    44. }
    45. }
    46. .app-header-nav {
    47. width: 820px;
    48. display: flex;
    49. padding-left: 40px;
    50. position: relative;
    51. z-index: 998;
    52. li {
    53. margin-right: 40px;
    54. width: 38px;
    55. text-align: center;
    56. a {
    57. font-size: 16px;
    58. line-height: 32px;
    59. height: 32px;
    60. display: inline-block;
    61. &:hover {
    62. color: $xtxColor;
    63. border-bottom: 1px solid $xtxColor;
    64. }
    65. }
    66. .active {
    67. color: $xtxColor;
    68. border-bottom: 1px solid $xtxColor;
    69. }
    70. }
    71. }
    72. .search {
    73. width: 170px;
    74. height: 32px;
    75. position: relative;
    76. border-bottom: 1px solid #e7e7e7;
    77. line-height: 32px;
    78. .icon-search {
    79. font-size: 18px;
    80. margin-left: 5px;
    81. }
    82. input {
    83. width: 140px;
    84. padding-left: 5px;
    85. color: #666;
    86. }
    87. }
    88. .cart {
    89. width: 50px;
    90. .curr {
    91. height: 32px;
    92. line-height: 32px;
    93. text-align: center;
    94. position: relative;
    95. display: block;
    96. .icon-cart {
    97. font-size: 22px;
    98. }
    99. em {
    100. font-style: normal;
    101. position: absolute;
    102. right: 0;
    103. top: 0;
    104. padding: 1px 6px;
    105. line-height: 1;
    106. background: $helpColor;
    107. color: #fff;
    108. font-size: 12px;
    109. border-radius: 10px;
    110. font-family: Arial;
    111. }
    112. }
    113. }
    114. }
    115. style>

    吸顶导航交互实现 

    实现吸顶交互

    1. <script setup>
    2. import { useScroll } from '@vueuse/core'
    3. const { y } = useScroll(window)
    4. script>

    components/LayoutFixed.vue

    1. <script setup>
    2. import { useScroll } from '@vueuse/core'
    3. const { y } = useScroll(window)
    4. </script>
    5. <template>
    6. <div class="app-header-sticky" :class="{ show: y > 78 }">
    7. <div class="container">
    8. <RouterLink class="logo" to="/" />
    9. <!-- 导航区域 -->
    10. <ul class="app-header-nav ">
    11. <li class="home">
    12. <RouterLink to="/">首页</RouterLink>
    13. </li>
    14. <li>
    15. <RouterLink to="/">居家</RouterLink>
    16. </li>
    17. <li>
    18. <RouterLink to="/">美食</RouterLink>
    19. </li>
    20. <li>
    21. <RouterLink to="/">服饰</RouterLink>
    22. </li>
    23. <li>
    24. <RouterLink to="/">母婴</RouterLink>
    25. </li>
    26. <li>
    27. <RouterLink to="/">个护</RouterLink>
    28. </li>
    29. <li>
    30. <RouterLink to="/">严选</RouterLink>
    31. </li>
    32. <li>
    33. <RouterLink to="/">数码</RouterLink>
    34. </li>
    35. <li>
    36. <RouterLink to="/">运动</RouterLink>
    37. </li>
    38. <li>
    39. <RouterLink to="/">杂项</RouterLink>
    40. </li>
    41. </ul>
    42. <div class="right">
    43. <RouterLink to="/">品牌</RouterLink>
    44. <RouterLink to="/">专题</RouterLink>
    45. </div>
    46. </div>
    47. </div>
    48. </template>
    49. <style scoped lang='scss'>
    50. .app-header-sticky {
    51. width: 100%;
    52. height: 80px;
    53. position: fixed;
    54. left: 0;
    55. top: 0;
    56. z-index: 999;
    57. background-color: #fff;
    58. border-bottom: 1px solid #e4e4e4;
    59. // 此处为关键样式!!!
    60. // 状态一:往上平移自身高度 + 完全透明
    61. transform: translateY(-100%);
    62. opacity: 0;
    63. // 状态二:移除平移 + 完全不透明
    64. &.show {
    65. transition: all 0.3s linear;
    66. transform: none;
    67. opacity: 1;
    68. }
    69. .container {
    70. display: flex;
    71. align-items: center;
    72. }
    73. .logo {
    74. width: 200px;
    75. height: 80px;
    76. background: url("@/assets/images/logo.png") no-repeat right 2px;
    77. background-size: 160px auto;
    78. }
    79. .right {
    80. width: 220px;
    81. display: flex;
    82. text-align: center;
    83. padding-left: 40px;
    84. border-left: 2px solid $xtxColor;
    85. a {
    86. width: 38px;
    87. margin-right: 40px;
    88. font-size: 16px;
    89. line-height: 1;
    90. &:hover {
    91. color: $xtxColor;
    92. }
    93. }
    94. }
    95. }
    96. .app-header-nav {
    97. width: 820px;
    98. display: flex;
    99. padding-left: 40px;
    100. position: relative;
    101. z-index: 998;
    102. li {
    103. margin-right: 40px;
    104. width: 38px;
    105. text-align: center;
    106. a {
    107. font-size: 16px;
    108. line-height: 32px;
    109. height: 32px;
    110. display: inline-block;
    111. &:hover {
    112. color: $xtxColor;
    113. border-bottom: 1px solid $xtxColor;
    114. }
    115. }
    116. .active {
    117. color: $xtxColor;
    118. border-bottom: 1px solid $xtxColor;
    119. }
    120. }
    121. }
    122. </style>

    Pinia优化重复请求

    两个导航栏会分别发送一个请求,可以利用pinia只发送一个请求。

    stores/category.js

    1. import { ref } from 'vue'
    2. import { defineStore } from 'pinia'
    3. import { getCategoryAPI } from '@/api/layout'
    4. export const useCategoryStore = defineStore('category', () => {
    5. // 导航列表的数据管理
    6. // state 导航列表数据
    7. const categoryList = ref([])
    8. // action 获取导航数据的方法
    9. const getCategory = async () => {
    10. const res = await getCategoryAPI()
    11. categoryList.value = res.result
    12. }
    13. return {
    14. categoryList,
    15. getCategory
    16. }
    17. })

     layout/index.vue

    1. <script setup>
    2. import LayoutNav from './components/LayoutNav.vue'
    3. import LayoutHeader from './components/LayoutHeader.vue'
    4. import LayoutFooter from './components/LayoutFooter.vue'
    5. import LayoutFixed from './components/LayoutFixed.vue'
    6. //触发导航列表的action
    7. import {useCategoryStore} from '@/stores/category'
    8. import {onMounted} from 'vue'
    9. const categoryStore = useCategoryStore()
    10. onMounted(() => categoryStore.getCategory())
    11. </script>
    12. <template>
    13. <LayoutFixed />
    14. <LayoutNav />
    15. <LayoutHeader />
    16. <RouterView />
    17. <LayoutFooter />
    18. </template>

    layout/components/LayoutFixed.vue

    1. <script setup>
    2. import {useCategoryStore} from '@/stores/category'
    3. import { useScroll } from '@vueuse/core'
    4. const { y } = useScroll(window)
    5. //使用pinia中的数据
    6. const categoryStore = useCategoryStore()
    7. </script>
    8. <template>
    9. <div class="app-header-sticky" :class="{ show: y > 78 }">
    10. <div class="container">
    11. <RouterLink class="logo" to="/" />
    12. <!-- 导航区域 -->
    13. <ul class="app-header-nav ">
    14. <li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
    15. <RouterLink to="/">{{ item.name }}</RouterLink>
    16. </li>
    17. </ul>
    18. <div class="right">
    19. <RouterLink to="/">品牌</RouterLink>
    20. <RouterLink to="/">专题</RouterLink>
    21. </div>
    22. </div>
    23. </div>
    24. </template>
    25. <style scoped lang='scss'>
    26. .app-header-sticky {
    27. width: 100%;
    28. height: 80px;
    29. position: fixed;
    30. left: 0;
    31. top: 0;
    32. z-index: 999;
    33. background-color: #fff;
    34. border-bottom: 1px solid #e4e4e4;
    35. // 此处为关键样式!!!
    36. // 状态一:往上平移自身高度 + 完全透明
    37. transform: translateY(-100%);
    38. opacity: 0;
    39. // 状态二:移除平移 + 完全不透明
    40. &.show {
    41. transition: all 0.3s linear;
    42. transform: none;
    43. opacity: 1;
    44. }
    45. .container {
    46. display: flex;
    47. align-items: center;
    48. }
    49. .logo {
    50. width: 200px;
    51. height: 80px;
    52. background: url("@/assets/images/logo.png") no-repeat right 2px;
    53. background-size: 160px auto;
    54. }
    55. .right {
    56. width: 220px;
    57. display: flex;
    58. text-align: center;
    59. padding-left: 40px;
    60. border-left: 2px solid $xtxColor;
    61. a {
    62. width: 38px;
    63. margin-right: 40px;
    64. font-size: 16px;
    65. line-height: 1;
    66. &:hover {
    67. color: $xtxColor;
    68. }
    69. }
    70. }
    71. }
    72. .app-header-nav {
    73. width: 820px;
    74. display: flex;
    75. padding-left: 40px;
    76. position: relative;
    77. z-index: 998;
    78. li {
    79. margin-right: 40px;
    80. width: 38px;
    81. text-align: center;
    82. a {
    83. font-size: 16px;
    84. line-height: 32px;
    85. height: 32px;
    86. display: inline-block;
    87. &:hover {
    88. color: $xtxColor;
    89. border-bottom: 1px solid $xtxColor;
    90. }
    91. }
    92. .active {
    93. color: $xtxColor;
    94. border-bottom: 1px solid $xtxColor;
    95. }
    96. }
    97. }
    98. </style>

    layout/components/LayoutHeader.vue

    1. <script setup>
    2. import {useCategoryStore} from '@/stores/category'
    3. import { useScroll } from '@vueuse/core'
    4. const { y } = useScroll(window)
    5. //使用pinia中的数据
    6. const categoryStore = useCategoryStore()
    7. </script>
    8. <template>
    9. <div class="app-header-sticky" :class="{ show: y > 78 }">
    10. <div class="container">
    11. <RouterLink class="logo" to="/" />
    12. <!-- 导航区域 -->
    13. <ul class="app-header-nav ">
    14. <li class="home" v-for="item in categoryStore.categoryList" :key="item.id">
    15. <RouterLink to="/">{{ item.name }}</RouterLink>
    16. </li>
    17. </ul>
    18. <div class="right">
    19. <RouterLink to="/">品牌</RouterLink>
    20. <RouterLink to="/">专题</RouterLink>
    21. </div>
    22. </div>
    23. </div>
    24. </template>
    25. <style scoped lang='scss'>
    26. .app-header-sticky {
    27. width: 100%;
    28. height: 80px;
    29. position: fixed;
    30. left: 0;
    31. top: 0;
    32. z-index: 999;
    33. background-color: #fff;
    34. border-bottom: 1px solid #e4e4e4;
    35. // 此处为关键样式!!!
    36. // 状态一:往上平移自身高度 + 完全透明
    37. transform: translateY(-100%);
    38. opacity: 0;
    39. // 状态二:移除平移 + 完全不透明
    40. &.show {
    41. transition: all 0.3s linear;
    42. transform: none;
    43. opacity: 1;
    44. }
    45. .container {
    46. display: flex;
    47. align-items: center;
    48. }
    49. .logo {
    50. width: 200px;
    51. height: 80px;
    52. background: url("@/assets/images/logo.png") no-repeat right 2px;
    53. background-size: 160px auto;
    54. }
    55. .right {
    56. width: 220px;
    57. display: flex;
    58. text-align: center;
    59. padding-left: 40px;
    60. border-left: 2px solid $xtxColor;
    61. a {
    62. width: 38px;
    63. margin-right: 40px;
    64. font-size: 16px;
    65. line-height: 1;
    66. &:hover {
    67. color: $xtxColor;
    68. }
    69. }
    70. }
    71. }
    72. .app-header-nav {
    73. width: 820px;
    74. display: flex;
    75. padding-left: 40px;
    76. position: relative;
    77. z-index: 998;
    78. li {
    79. margin-right: 40px;
    80. width: 38px;
    81. text-align: center;
    82. a {
    83. font-size: 16px;
    84. line-height: 32px;
    85. height: 32px;
    86. display: inline-block;
    87. &:hover {
    88. color: $xtxColor;
    89. border-bottom: 1px solid $xtxColor;
    90. }
    91. }
    92. .active {
    93. color: $xtxColor;
    94. border-bottom: 1px solid $xtxColor;
    95. }
    96. }
    97. }
    98. </style>

     发现只有一次请求。

    Home页 

    在home/components下新建5个vue文件

    • HomeCategory
    1. <script setup>
    2. </script>
    3. <template>
    4. <div> 我是分类 </div>
    5. </template>
    • HomeBanner
    1. <script setup>
    2. </script>
    3. <template>
    4. <div> 我是banner </div>
    5. </template>
    • HomeNew
    1. <script setup>
    2. </script>
    3. <template>
    4. <div> 人气推荐 </div>
    5. </template>
    • HomeHot
    1. <script setup>
    2. </script>
    3. <template>
    4. <div> 新鲜好物 </div>
    5. </template>
    • HomeProduct
    1. <script setup>
    2. </script>
    3. <template>
    4. <div> 产品列表 </div>
    5. </template>

    home/index.vue

    1. <script setup>
    2. import HomeCategory from './components/HomeCategory.vue'
    3. import HomeBanner from './components/HomeBanner.vue'
    4. import HomeNew from './components/HomeNew.vue'
    5. import HomeHot from './components/HomeHot.vue'
    6. import HomeProduct from './components/HomeProduct.vue'
    7. </script>
    8. <template>
    9. <div class="container">
    10. <HomeCategory />
    11. <HomeBanner />
    12. </div>
    13. <HomeNew />
    14. <HomeHot />
    15. <HomeProduct />
    16. </template>

    分类实现

    home/components/HomeCategory.vue

    1. <script setup>
    2. import {useCategoryStore} from '@/stores/category'
    3. const categoryStore = useCategoryStore()
    4. </script>
    5. <template>
    6. <div class="home-category">
    7. <ul class="menu">
    8. <li v-for="item in categoryStore.categoryList" :key="item.id">
    9. <RouterLink to="/">{{item.name}}</RouterLink>
    10. <RouterLink v-for="i in item.children.slice(0,2)" :key="i" to="/">{{i.name}}</RouterLink>
    11. <!-- 弹层layer位置 -->
    12. <div class="layer">
    13. <h4>分类推荐 <small>根据您的购买或浏览记录推荐</small></h4>
    14. <ul>
    15. <li v-for="i in item.goods" :key="i.id">
    16. <RouterLink to="/">
    17. <img :src="i.picture" alt="" />
    18. <div class="info">
    19. <p class="name ellipsis-2">
    20. {{i.name}}
    21. </p>
    22. <p class="desc ellipsis">{{i.desc}}</p>
    23. <p class="price"><i>¥</i>{{i.price}}</p>
    24. </div>
    25. </RouterLink>
    26. </li>
    27. </ul>
    28. </div>
    29. </li>
    30. </ul>
    31. </div>
    32. </template>
    33. <style scoped lang='scss'>
    34. .home-category {
    35. width: 250px;
    36. height: 500px;
    37. background: rgba(0, 0, 0, 0.8);
    38. position: relative;
    39. z-index: 99;
    40. .menu {
    41. li {
    42. padding-left: 40px;
    43. height: 55px;
    44. line-height: 55px;
    45. &:hover {
    46. background: $xtxColor;
    47. }
    48. a {
    49. margin-right: 4px;
    50. color: #fff;
    51. &:first-child {
    52. font-size: 16px;
    53. }
    54. }
    55. .layer {
    56. width: 990px;
    57. height: 500px;
    58. background: rgba(255, 255, 255, 0.8);
    59. position: absolute;
    60. left: 250px;
    61. top: 0;
    62. display: none;
    63. padding: 0 15px;
    64. h4 {
    65. font-size: 20px;
    66. font-weight: normal;
    67. line-height: 80px;
    68. small {
    69. font-size: 16px;
    70. color: #666;
    71. }
    72. }
    73. ul {
    74. display: flex;
    75. flex-wrap: wrap;
    76. li {
    77. width: 310px;
    78. height: 120px;
    79. margin-right: 15px;
    80. margin-bottom: 15px;
    81. border: 1px solid #eee;
    82. border-radius: 4px;
    83. background: #fff;
    84. &:nth-child(3n) {
    85. margin-right: 0;
    86. }
    87. a {
    88. display: flex;
    89. width: 100%;
    90. height: 100%;
    91. align-items: center;
    92. padding: 10px;
    93. &:hover {
    94. background: #e3f9f4;
    95. }
    96. img {
    97. width: 95px;
    98. height: 95px;
    99. }
    100. .info {
    101. padding-left: 10px;
    102. line-height: 24px;
    103. overflow: hidden;
    104. .name {
    105. font-size: 16px;
    106. color: #666;
    107. }
    108. .desc {
    109. color: #999;
    110. }
    111. .price {
    112. font-size: 22px;
    113. color: $priceColor;
    114. i {
    115. font-size: 16px;
    116. }
    117. }
    118. }
    119. }
    120. }
    121. }
    122. }
    123. // 关键样式 hover状态下的layer盒子变成block
    124. &:hover {
    125. .layer {
    126. display: block;
    127. }
    128. }
    129. }
    130. }
    131. }
    132. </style>

    banner轮播图 

    home/components/HomeBanner.vue

    1. <script setup>
    2. </script>
    3. <template>
    4. <div class="home-banner">
    5. <el-carousel height="500px">
    6. <el-carousel-item v-for="item in 4" :key="item">
    7. <img src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-15/6d202d8e-bb47-4f92-9523-f32ab65754f4.jpg" alt="">
    8. </el-carousel-item>
    9. </el-carousel>
    10. </div>
    11. </template>
    12. <style scoped lang='scss'>
    13. .home-banner {
    14. width: 1240px;
    15. height: 500px;
    16. position: absolute;
    17. left: 0;
    18. top: 0;
    19. z-index: 98;
    20. img {
    21. width: 100%;
    22. height: 500px;
    23. }
    24. }
    25. </style>

    获取数据 

    api/home.js

    1. import httpInstance from '@/utils/http'
    2. export function getBannerAPI (){
    3. return httpInstance({
    4. url:'home/banner'
    5. })
    6. }

    home/components/HomeBanner.vue

    1. <script setup>
    2. import { getBannerAPI } from '@/api/home'
    3. import { onMounted, ref } from 'vue'
    4. const bannerList = ref([])
    5. const getBanner = async () => {
    6. const res = await getBannerAPI()
    7. console.log(res)
    8. bannerList.value = res.result
    9. }
    10. onMounted(() => getBanner())
    11. </script>
    12. <template>
    13. <div class="home-banner">
    14. <el-carousel height="500px">
    15. <el-carousel-item v-for="item in bannerList" :key="item.id">
    16. <img :src="item.imgUrl" alt="">
    17. </el-carousel-item>
    18. </el-carousel>
    19. </div>
    20. </template>
    21. <style scoped lang='scss'>
    22. .home-banner {
    23. width: 1240px;
    24. height: 500px;
    25. position: absolute;
    26. left: 0;
    27. top: 0;
    28. z-index: 98;
    29. img {
    30. width: 100%;
    31. height: 500px;
    32. }
    33. }
    34. </style>

     轮播图就实现了。

    新鲜好物实现

    home/components/HomePanel.vue

    1. <script setup>
    2. defineProps({
    3. title: {
    4. type: String,
    5. default: ''
    6. },
    7. subTitle: {
    8. type: String,
    9. default: ''
    10. }
    11. })
    12. script>
    13. <template>
    14. <div class="home-panel">
    15. <div class="container">
    16. <div class="head">
    17. <h3>
    18. {{ title }}<small>{{ subTitle }}small>
    19. h3>
    20. div>
    21. <slot name="main" />
    22. div>
    23. div>
    24. template>
    25. <style scoped lang='scss'>
    26. .home-panel {
    27. background-color: #fff;
    28. .head {
    29. padding: 40px 0;
    30. display: flex;
    31. align-items: flex-end;
    32. h3 {
    33. flex: 1;
    34. font-size: 32px;
    35. font-weight: normal;
    36. margin-left: 6px;
    37. height: 35px;
    38. line-height: 35px;
    39. small {
    40. font-size: 16px;
    41. color: #999;
    42. margin-left: 20px;
    43. }
    44. }
    45. }
    46. }
    47. style>

    api/home添加:

    1. /**
    2. * @description: 获取新鲜好物
    3. * @param {*}
    4. * @return {*}
    5. */
    6. export const getNewAPI = () => {
    7. return httpInstance({
    8. url:'/home/new'
    9. })
    10. }

    home/components/HomeNew.vue

    1. <script setup>
    2. import HomePanel from './HomePanel.vue'
    3. import { getNewAPI } from '@/api/home'
    4. import { ref } from 'vue'
    5. const newList = ref([])
    6. const getNewList = async () => {
    7. const res = await getNewAPI()
    8. newList.value = res.result
    9. }
    10. getNewList()
    11. </script>
    12. <template>
    13. <HomePanel title="新鲜好物" sub-title="新鲜出炉 品质靠谱">
    14. <template #main>
    15. <ul class="goods-list">
    16. <li v-for="item in newList" :key="item.id">
    17. <RouterLink :to="`/detail/${item.id}`">
    18. <img :src="item.picture" alt="" />
    19. <p class="name">{{ item.name }}</p>
    20. <p class="price">&yen;{{ item.price }}</p>
    21. </RouterLink>
    22. </li>
    23. </ul>
    24. </template>
    25. </HomePanel>
    26. </template>
    27. <style scoped lang='scss'>
    28. .goods-list {
    29. display: flex;
    30. justify-content: space-between;
    31. height: 406px;
    32. li {
    33. width: 306px;
    34. height: 406px;
    35. background: #f0f9f4;
    36. transition: all .5s;
    37. &:hover {
    38. transform: translate3d(0, -3px, 0);
    39. box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    40. }
    41. img {
    42. width: 306px;
    43. height: 306px;
    44. }
    45. p {
    46. font-size: 22px;
    47. padding-top: 12px;
    48. text-align: center;
    49. text-overflow: ellipsis;
    50. overflow: hidden;
    51. white-space: nowrap;
    52. }
    53. .price {
    54. color: $priceColor;
    55. }
    56. }
    57. }
    58. </style>

    人气推荐实现 

    api/home添加:

    1. /**
    2. * @description: 获取人气推荐
    3. * @param {*}
    4. * @return {*}
    5. */
    6. export const getHotAPI = () => {
    7. return httpInstance({
    8. url:'/home/hot'
    9. })
    10. }

    home/components/HomeHot.vue

    1. <script setup>
    2. import HomePanel from './HomePanel.vue'
    3. import { getHotAPI } from '@/api/home'
    4. import { ref } from 'vue'
    5. const hotList = ref([])
    6. const getHotList = async () => {
    7. const res = await getHotAPI()
    8. hotList.value = res.result
    9. }
    10. getHotList()
    11. console.log(hotList)
    12. </script>
    13. <template>
    14. <HomePanel title="人气推荐" sub-title="人气爆款 不容错过">
    15. <template #main>
    16. <ul class="goods-list">
    17. <li v-for="item in hotList" :key="item.id">
    18. <RouterLink :to="`/detail/${item.id}`">
    19. <img :src="item.picture" alt="" />
    20. <p class="name">{{ item.title }}</p>
    21. <p class="desc">{{ item.alt }}</p>
    22. </RouterLink>
    23. </li>
    24. </ul>
    25. </template>
    26. </HomePanel>
    27. </template>
    28. <style scoped lang='scss'>
    29. .goods-list {
    30. display: flex;
    31. justify-content: space-between;
    32. height: 426px;
    33. li {
    34. width: 306px;
    35. height: 406px;
    36. transition: all .5s;
    37. &:hover {
    38. transform: translate3d(0, -3px, 0);
    39. box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    40. }
    41. img {
    42. width: 306px;
    43. height: 306px;
    44. }
    45. p {
    46. font-size: 22px;
    47. padding-top: 12px;
    48. text-align: center;
    49. }
    50. .desc {
    51. color: #999;
    52. font-size: 18px;
    53. }
    54. }
    55. }
    56. </style>

    懒加载指令实现 

    directives/index.js

    1. // 定义懒加载插件
    2. import { useIntersectionObserver } from '@vueuse/core'
    3. export const directivePlugin = {
    4. install (app) {
    5. // 懒加载指令逻辑
    6. app.directive('img-lazy', {
    7. mounted (el, binding) {
    8. // el: 指令绑定的那个元素 img
    9. // binding: binding.value 指令等于号后面绑定的表达式的值 图片url
    10. console.log(el, binding.value)
    11. useIntersectionObserver(
    12. el,
    13. ([{ isIntersecting }]) => {
    14. console.log(isIntersecting)
    15. if (isIntersecting) {
    16. // 进入视口区域
    17. el.src = binding.value
    18. stop()
    19. }
    20. },
    21. )
    22. }
    23. })
    24. }
    25. }

    修改main.js

    1. app.use(createPinia())
    2. app.use(router)
    3. app.use(directivePlugin)
    4. app.mount('#app')

    产品列表实现

    api/home.js新增

    1. /**
    2. * @description: 获取所有商品模块
    3. * @param {*}
    4. * @return {*}
    5. */
    6. export const getGoodsAPI = () => {
    7. return httpInstance({
    8. url: '/home/goods'
    9. })
    10. }

     home/components/HomeProduct.vue

    1. <script setup>
    2. import HomePanel from './HomePanel.vue'
    3. import {getGoodsAPI} from '@/api/home'
    4. import {onMounted,ref} from 'vue'
    5. //获取数据列表
    6. const goodsProduct = ref([])
    7. const getGoods = async () => {
    8. const res = await getGoodsAPI()
    9. goodsProduct.value = res.result
    10. }
    11. onMounted(()=>getGoods())
    12. </script>
    13. <template>
    14. <div class="home-product">
    15. <HomePanel :title="cate.name" v-for="cate in goodsProduct" :key="cate.id">
    16. <template #main>
    17. <div class="box">
    18. <RouterLink class="cover" to="/">
    19. <img :src="cate.picture" />
    20. <strong class="label">
    21. <span>{{ cate.name }}馆</span>
    22. <span>{{ cate.saleInfo }}</span>
    23. </strong>
    24. </RouterLink>
    25. <ul class="goods-list">
    26. <li v-for="good in cate.goods" :key="good.id">
    27. <RouterLink to="/" class="goods-item">
    28. <img :src="good.picture" alt="" />
    29. <p class="name ellipsis">{{ good.name }}</p>
    30. <p class="desc ellipsis">{{ good.desc }}</p>
    31. <p class="price">&yen;{{ good.price }}</p>
    32. </RouterLink>
    33. </li>
    34. </ul>
    35. </div>
    36. </template>
    37. </HomePanel>
    38. </div>
    39. </template>
    40. <style scoped lang='scss'>
    41. .home-product {
    42. background: #fff;
    43. margin-top: 20px;
    44. .sub {
    45. margin-bottom: 2px;
    46. a {
    47. padding: 2px 12px;
    48. font-size: 16px;
    49. border-radius: 4px;
    50. &:hover {
    51. background: $xtxColor;
    52. color: #fff;
    53. }
    54. &:last-child {
    55. margin-right: 80px;
    56. }
    57. }
    58. }
    59. .box {
    60. display: flex;
    61. .cover {
    62. width: 240px;
    63. height: 610px;
    64. margin-right: 10px;
    65. position: relative;
    66. img {
    67. width: 100%;
    68. height: 100%;
    69. }
    70. .label {
    71. width: 188px;
    72. height: 66px;
    73. display: flex;
    74. font-size: 18px;
    75. color: #fff;
    76. line-height: 66px;
    77. font-weight: normal;
    78. position: absolute;
    79. left: 0;
    80. top: 50%;
    81. transform: translate3d(0, -50%, 0);
    82. span {
    83. text-align: center;
    84. &:first-child {
    85. width: 76px;
    86. background: rgba(0, 0, 0, 0.9);
    87. }
    88. &:last-child {
    89. flex: 1;
    90. background: rgba(0, 0, 0, 0.7);
    91. }
    92. }
    93. }
    94. }
    95. .goods-list {
    96. width: 990px;
    97. display: flex;
    98. flex-wrap: wrap;
    99. li {
    100. width: 240px;
    101. height: 300px;
    102. margin-right: 10px;
    103. margin-bottom: 10px;
    104. &:nth-last-child(-n + 4) {
    105. margin-bottom: 0;
    106. }
    107. &:nth-child(4n) {
    108. margin-right: 0;
    109. }
    110. }
    111. }
    112. .goods-item {
    113. display: block;
    114. width: 220px;
    115. padding: 20px 30px;
    116. text-align: center;
    117. transition: all .5s;
    118. &:hover {
    119. transform: translate3d(0, -3px, 0);
    120. box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    121. }
    122. img {
    123. width: 160px;
    124. height: 160px;
    125. }
    126. p {
    127. padding-top: 10px;
    128. }
    129. .name {
    130. font-size: 16px;
    131. }
    132. .desc {
    133. color: #999;
    134. height: 29px;
    135. }
    136. .price {
    137. color: $priceColor;
    138. font-size: 20px;
    139. }
    140. }
    141. }
    142. }
    143. </style>

    这里没有用懒加载的形式,因为总有一些图片加载不出来,原因不明。 

    GoodsItem组件封装 

     home/components/GoodsItem.vue

    1. <script setup>
    2. defineProps({
    3. goods: {
    4. type: Object,
    5. default: () => { }
    6. }
    7. })
    8. </script>
    9. <template>
    10. <RouterLink to="/" class="goods-item">
    11. <img :src="goods.picture" alt="" />
    12. <p class="name ellipsis">{{ goods.name }}</p>
    13. <p class="desc ellipsis">{{ goods.desc }}</p>
    14. <p class="price">&yen;{{ goods.price }}</p>
    15. </RouterLink>
    16. </template>
    17. <style scoped lang="scss">
    18. .goods-item {
    19. display: block;
    20. width: 220px;
    21. padding: 20px 30px;
    22. text-align: center;
    23. transition: all .5s;
    24. &:hover {
    25. transform: translate3d(0, -3px, 0);
    26. box-shadow: 0 3px 8px rgb(0 0 0 / 20%);
    27. }
    28. img {
    29. width: 160px;
    30. height: 160px;
    31. }
    32. p {
    33. padding-top: 10px;
    34. }
    35. .name {
    36. font-size: 16px;
    37. }
    38. .desc {
    39. color: #999;
    40. height: 29px;
    41. }
    42. .price {
    43. color: $priceColor;
    44. font-size: 20px;
    45. }
    46. }
    47. </style>

     home/components/HomeProduct.vue修改

    1. import GoodsItem from './GoodsItem.vue'
    2. <ul class="goods-list">
    3. <li v-for="good in cate.goods" :key="good.id">
    4. <GoodsItem :goods="good"/>
    5. </li>
    6. </ul>

    一级分类页 

    导航栏

    router/index.js修改

    1. children: [
    2. {
    3. path: '',
    4. component: Home
    5. },
    6. {
    7. path: 'category/:id',
    8. component: Category
    9. }
    10. ]

    layout/components/LayoutHeader.vue修改

    layout/components/LayoutFixed.vue修改

    <RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink>

    api/category.vue

    1. import httpInstance from '@/utils/http'
    2. /**
    3. * @description: 获取分类数据
    4. * @param {*} id 分类id
    5. * @return {*}
    6. */
    7. export const findTopCategoryAPI = (id) => {
    8. return httpInstance({
    9. url:'/category',
    10. params:{
    11. id
    12. }
    13. })
    14. }

    views/category/index.vue

    1. <script setup>
    2. import { findTopCategoryAPI } from '@/api/category'
    3. import {ref} from 'vue'
    4. import {useRoute} from 'vue-router'
    5. const categoryData = ref({})
    6. const route = useRoute()
    7. const getCategory = async (id) => {
    8. // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route
    9. const res = await findTopCategoryAPI(id)
    10. categoryData.value = res.result
    11. }
    12. getCategory(route.params.id)
    13. script>
    14. <template>
    15. <div class="bread-container">
    16. <el-breadcrumb separator=">">
    17. <el-breadcrumb-item :to="{ path: '/' }">首页el-breadcrumb-item>
    18. <el-breadcrumb-item>{{ categoryData.name }}el-breadcrumb-item>
    19. el-breadcrumb>
    20. div>
    21. template>
    22. <style scoped lang="scss">
    23. .top-category {
    24. h3 {
    25. font-size: 28px;
    26. color: #666;
    27. font-weight: normal;
    28. text-align: center;
    29. line-height: 100px;
    30. }
    31. .sub-list {
    32. margin-top: 20px;
    33. background-color: #fff;
    34. ul {
    35. display: flex;
    36. padding: 0 32px;
    37. flex-wrap: wrap;
    38. li {
    39. width: 168px;
    40. height: 160px;
    41. a {
    42. text-align: center;
    43. display: block;
    44. font-size: 16px;
    45. img {
    46. width: 100px;
    47. height: 100px;
    48. }
    49. p {
    50. line-height: 40px;
    51. }
    52. &:hover {
    53. color: $xtxColor;
    54. }
    55. }
    56. }
    57. }
    58. }
    59. .ref-goods {
    60. background-color: #fff;
    61. margin-top: 20px;
    62. position: relative;
    63. .head {
    64. .xtx-more {
    65. position: absolute;
    66. top: 20px;
    67. right: 20px;
    68. }
    69. .tag {
    70. text-align: center;
    71. color: #999;
    72. font-size: 20px;
    73. position: relative;
    74. top: -20px;
    75. }
    76. }
    77. .body {
    78. display: flex;
    79. justify-content: space-around;
    80. padding: 0 40px 30px;
    81. }
    82. }
    83. .bread-container {
    84. padding: 25px 0;
    85. }
    86. }
    87. style>

    分类Banner渲染 

    api/home.js修改

    1. export function getBannerAPI (params = {}) {
    2. // 默认为1 商品为2
    3. const { distributionSite = '1' } = params
    4. return httpInstance({
    5. url: '/home/banner',
    6. params: {
    7. distributionSite
    8. }
    9. })
    10. }

    category/index.vue 

    1. <script setup>
    2. import { findTopCategoryAPI } from '@/api/category'
    3. import {ref,onMounted} from 'vue'
    4. import {useRoute} from 'vue-router'
    5. import { getBannerAPI } from '@/api/home'
    6. const categoryData = ref({})
    7. const route = useRoute()
    8. const getCategory = async (id) => {
    9. // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route
    10. const res = await findTopCategoryAPI(id)
    11. categoryData.value = res.result
    12. }
    13. getCategory(route.params.id)
    14. //获取banner
    15. const bannerList = ref([])
    16. const getBanner = async () => {
    17. const res = await getBannerAPI()
    18. console.log(res)
    19. bannerList.value = res.result
    20. }
    21. onMounted(() => getBanner())
    22. script>
    23. <template>
    24. <div class="bread-container">
    25. <el-breadcrumb separator=">">
    26. <el-breadcrumb-item :to="{ path: '/' }">首页el-breadcrumb-item>
    27. <el-breadcrumb-item>{{ categoryData.name }}el-breadcrumb-item>
    28. el-breadcrumb>
    29. div>
    30. <div class="home-banner">
    31. <el-carousel height="500px">
    32. <el-carousel-item v-for="item in bannerList" :key="item.id">
    33. <img :src="item.imgUrl" alt="">
    34. el-carousel-item>
    35. el-carousel>
    36. div>
    37. template>
    38. <style scoped lang="scss">
    39. .home-banner {
    40. width: 1240px;
    41. height: 500px;
    42. margin: 0 auto;
    43. z-index: 98;
    44. img {
    45. width: 100%;
    46. height: 500px;
    47. }
    48. }
    49. .top-category {
    50. h3 {
    51. font-size: 28px;
    52. color: #666;
    53. font-weight: normal;
    54. text-align: center;
    55. line-height: 100px;
    56. }
    57. .sub-list {
    58. margin-top: 20px;
    59. background-color: #fff;
    60. ul {
    61. display: flex;
    62. padding: 0 32px;
    63. flex-wrap: wrap;
    64. li {
    65. width: 168px;
    66. height: 160px;
    67. a {
    68. text-align: center;
    69. display: block;
    70. font-size: 16px;
    71. img {
    72. width: 100px;
    73. height: 100px;
    74. }
    75. p {
    76. line-height: 40px;
    77. }
    78. &:hover {
    79. color: $xtxColor;
    80. }
    81. }
    82. }
    83. }
    84. }
    85. .ref-goods {
    86. background-color: #fff;
    87. margin-top: 20px;
    88. position: relative;
    89. .head {
    90. .xtx-more {
    91. position: absolute;
    92. top: 20px;
    93. right: 20px;
    94. }
    95. .tag {
    96. text-align: center;
    97. color: #999;
    98. font-size: 20px;
    99. position: relative;
    100. top: -20px;
    101. }
    102. }
    103. .body {
    104. display: flex;
    105. justify-content: space-around;
    106. padding: 0 40px 30px;
    107. }
    108. }
    109. .bread-container {
    110. padding: 25px 0;
    111. }
    112. }
    113. style>

    导航激活 

    layout/components/LayoutHeader.vue修改

    layout/components/LayoutFixed.vue修改

    <RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>

    分类列表渲染 

    category/index.vue

    1. <script setup>
    2. import { findTopCategoryAPI } from '@/api/category'
    3. import {ref,onMounted} from 'vue'
    4. import {useRoute} from 'vue-router'
    5. import { getBannerAPI } from '@/api/home'
    6. import GoodsItem from '../home/components/GoodsItem.vue'
    7. const categoryData = ref({})
    8. const route = useRoute()
    9. const getCategory = async (id) => {
    10. // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route
    11. const res = await findTopCategoryAPI(id)
    12. categoryData.value = res.result
    13. }
    14. getCategory(route.params.id)
    15. //获取banner
    16. const bannerList = ref([])
    17. const getBanner = async () => {
    18. const res = await getBannerAPI()
    19. console.log(res)
    20. bannerList.value = res.result
    21. }
    22. onMounted(() => getBanner())
    23. </script>
    24. <template>
    25. <!-- 分类 -->
    26. <div class="top-category">
    27. <div class="bread-container">
    28. <el-breadcrumb separator=">">
    29. <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    30. <el-breadcrumb-item>{{ categoryData.name }}</el-breadcrumb-item>
    31. </el-breadcrumb>
    32. </div>
    33. <!-- 轮播图 -->
    34. <div class="home-banner">
    35. <el-carousel height="500px">
    36. <el-carousel-item v-for="item in bannerList" :key="item.id">
    37. <img :src="item.imgUrl" alt="">
    38. </el-carousel-item>
    39. </el-carousel>
    40. </div>
    41. <div class="sub-list">
    42. <h3>全部分类</h3>
    43. <ul>
    44. <li v-for="i in categoryData.children" :key="i.id">
    45. <RouterLink to="/">
    46. <img :src="i.picture" />
    47. <p>{{ i.name }}</p>
    48. </RouterLink>
    49. </li>
    50. </ul>
    51. </div>
    52. <div class="ref-goods" v-for="item in categoryData.children" :key="item.id">
    53. <div class="head">
    54. <h3>- {{ item.name }}-</h3>
    55. </div>
    56. <div class="body">
    57. <GoodsItem v-for="good in item.goods" :goods="good" :key="good.id" />
    58. </div>
    59. </div>
    60. </div>
    61. </template>
    62. <style scoped lang="scss">
    63. .top-category {
    64. width: 1240px;
    65. margin: 0 auto;
    66. h3 {
    67. font-size: 28px;
    68. color: #666;
    69. font-weight: normal;
    70. text-align: center;
    71. line-height: 100px;
    72. }
    73. .bread-container {
    74. padding: 25px 0;
    75. }
    76. .home-banner {
    77. width: 1240px;
    78. height: 500px;
    79. margin: 0 auto;
    80. z-index: 98;
    81. img {
    82. width: 100%;
    83. height: 500px;
    84. }
    85. }
    86. .sub-list {
    87. margin-top: 20px;
    88. background-color: #fff;
    89. ul {
    90. display: flex;
    91. padding: 0 32px;
    92. flex-wrap: wrap;
    93. justify-content: space-around;
    94. li {
    95. width: 168px;
    96. height: 160px;
    97. a {
    98. text-align: center;
    99. display: block;
    100. font-size: 16px;
    101. img {
    102. width: 100px;
    103. height: 100px;
    104. }
    105. p {
    106. line-height: 40px;
    107. }
    108. &:hover {
    109. color: $xtxColor;
    110. }
    111. }
    112. }
    113. }
    114. }
    115. .ref-goods {
    116. background-color: #fff;
    117. margin-top: 20px;
    118. position: relative;
    119. .head {
    120. .xtx-more {
    121. position: absolute;
    122. top: 20px;
    123. right: 20px;
    124. }
    125. .tag {
    126. text-align: center;
    127. color: #999;
    128. font-size: 20px;
    129. position: relative;
    130. top: -20px;
    131. }
    132. }
    133. .body {
    134. display: flex;
    135. justify-content: space-around;
    136. padding: 0 40px 30px;
    137. }
    138. }
    139. }
    140. </style>

    路由缓存问题 

    banner与分类商品在刷新的时候都会请求一次,但banner是没必要刷新的,且导航栏需要刷新才能更新。

    category/index.vue修改

    1. import {useRoute,onBeforeRouteUpdate} from 'vue-router'
    2. // 目标:路由参数变化的时候 可以把分类数据接口重新发送
    3. onBeforeRouteUpdate((to) => {
    4. // 存在问题:使用最新的路由参数请求最新的分类数据
    5. getCategory(to.params.id)
    6. })

    业务逻辑的函数拆分 

    将banner和分类抽象出来。

    useBanner.js

    1. // 封装banner轮播图相关的业务代码
    2. import { ref, onMounted } from 'vue'
    3. import { getBannerAPI } from '@/api/home'
    4. export function useBanner () {
    5. //获取banner
    6. const bannerList = ref([])
    7. const getBanner = async () => {
    8. const res = await getBannerAPI()
    9. console.log(res)
    10. bannerList.value = res.result
    11. }
    12. onMounted(() => getBanner())
    13. return {
    14. bannerList
    15. }
    16. }

    useCategory.js

    1. // 封装分类数据业务相关代码
    2. import { onMounted, ref } from 'vue'
    3. import { findTopCategoryAPI } from '@/api/category'
    4. import { useRoute } from 'vue-router'
    5. import { onBeforeRouteUpdate } from 'vue-router'
    6. export function useCategory () {
    7. const categoryData = ref({})
    8. const route = useRoute()
    9. const getCategory = async (id) => {
    10. // 如何在setup中获取路由参数 useRoute() -> route 等价于this.$route
    11. const res = await findTopCategoryAPI(id)
    12. categoryData.value = res.result
    13. }
    14. getCategory(route.params.id)
    15. // 目标:路由参数变化的时候 可以把分类数据接口重新发送
    16. onBeforeRouteUpdate((to) => {
    17. // 存在问题:使用最新的路由参数请求最新的分类数据
    18. getCategory(to.params.id)
    19. })
    20. return {
    21. categoryData
    22. }
    23. }

     index.vue修改

    1. <script setup>
    2. import GoodsItem from '../home/components/GoodsItem.vue'
    3. import {useBanner} from './composables/useBanner'
    4. import { useCategory } from './composables/useCategory'
    5. const {bannerList} = useBanner()
    6. const { categoryData } = useCategory()
    7. script>

    二级分类页 

    实现

    views下新建

    index.vue

    1. <script setup>
    2. </script>
    3. <template>
    4. <div class="container ">
    5. <!-- 面包屑 -->
    6. <div class="bread-container">
    7. <el-breadcrumb separator=">">
    8. <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    9. <el-breadcrumb-item :to="{ path: '/' }">居家
    10. </el-breadcrumb-item>
    11. <el-breadcrumb-item>居家生活用品</el-breadcrumb-item>
    12. </el-breadcrumb>
    13. </div>
    14. <div class="sub-container">
    15. <el-tabs>
    16. <el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
    17. <el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
    18. <el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
    19. </el-tabs>
    20. <div class="body">
    21. <!-- 商品列表-->
    22. </div>
    23. </div>
    24. </div>
    25. </template>
    26. <style lang="scss" scoped>
    27. .bread-container {
    28. padding: 25px 0;
    29. color: #666;
    30. }
    31. .sub-container {
    32. padding: 20px 10px;
    33. background-color: #fff;
    34. .body {
    35. display: flex;
    36. flex-wrap: wrap;
    37. padding: 0 10px;
    38. }
    39. .goods-item {
    40. display: block;
    41. width: 220px;
    42. margin-right: 20px;
    43. padding: 20px 30px;
    44. text-align: center;
    45. img {
    46. width: 160px;
    47. height: 160px;
    48. }
    49. p {
    50. padding-top: 10px;
    51. }
    52. .name {
    53. font-size: 16px;
    54. }
    55. .desc {
    56. color: #999;
    57. height: 29px;
    58. }
    59. .price {
    60. color: $priceColor;
    61. font-size: 20px;
    62. }
    63. }
    64. .pagination-container {
    65. margin-top: 20px;
    66. display: flex;
    67. justify-content: center;
    68. }
    69. }
    70. </style>

    router/index.js

    1. import { createRouter, createWebHistory } from 'vue-router'
    2. import HomeView from '../views/HomeView.vue'
    3. import Login from '@/views/login/index.vue'
    4. import Layout from '@/views/layout/index.vue'
    5. import Home from '@/views/home/index.vue'
    6. import Category from '@/views/category/index.vue'
    7. import SubCategory from '@/views/subCategory/index.vue'
    8. const router = createRouter({
    9. history: createWebHistory(import.meta.env.BASE_URL),
    10. routes: [
    11. {
    12. path: '/',
    13. component: Layout,
    14. children: [
    15. {
    16. path: '',
    17. component: Home
    18. },
    19. {
    20. path: 'category/:id',
    21. component: Category
    22. },
    23. {
    24. path: 'category/sub/:id',
    25. name: 'subCategory',
    26. component: SubCategory
    27. },
    28. ]
    29. },
    30. {
    31. path: '/login',
    32. component: Login
    33. }
    34. ]
    35. })
    36. export default router

    category/index.vue修改

    1. <div class="sub-list">
    2. <h3>全部分类</h3>
    3. <ul>
    4. <li v-for="i in categoryData.children" :key="i.id">
    5. <RouterLink :to="`/category/sub/${i.id}`">
    6. <img :src="i.picture" />
    7. <p>{{ i.name }}</p>
    8. </RouterLink>
    9. </li>
    10. </ul>
    11. </div>

    api/category.js

    1. /**
    2. * @description: 获取二级分类列表数据
    3. * @param {*} id 分类id
    4. * @return {*}
    5. */
    6. export const getCategoryFilterAPI = (id) => {
    7. return httpInstance({
    8. url:'/category/sub/filter',
    9. params:{
    10. id
    11. }
    12. })
    13. }
    14. /**
    15. * @description: 获取导航数据
    16. * @data {
    17. categoryId: 1005000 ,
    18. page: 1,
    19. pageSize: 20,
    20. sortField: 'publishTime' | 'orderNum' | 'evaluateNum'
    21. }
    22. * @return {*}
    23. */
    24. export const getSubCategoryAPI = (data) => {
    25. return httpInstance({
    26. url:'/category/goods/temporary',
    27. method:'POST',
    28. data
    29. })
    30. }

     subCategory/index.vue

    1. <script setup>
    2. import { getCategoryFilterAPI,getSubCategoryAPI} from '@/api/category'
    3. import {ref,onMounted} from 'vue'
    4. import { useRoute } from 'vue-router'
    5. import GoodsItem from '../home/components/GoodsItem.vue'
    6. // 获取面包屑导航数据
    7. const filterData = ref({})
    8. const route = useRoute()
    9. const getFilterData = async () => {
    10. const res = await getCategoryFilterAPI(route.params.id)
    11. filterData.value = res.result
    12. }
    13. getFilterData()
    14. // 获取基础列表数据渲染
    15. const goodList = ref([])
    16. const reqData = ref({
    17. categoryId: route.params.id,
    18. page: 1,
    19. pageSize: 20,
    20. sortField: 'publishTime'
    21. })
    22. const getGoodList = async () => {
    23. const res = await getSubCategoryAPI(reqData.value)
    24. console.log(res)
    25. goodList.value = res.result.items
    26. }
    27. onMounted(() => getGoodList())
    28. </script>
    29. <template>
    30. <div class="container">
    31. <div class="bread-container">
    32. <el-breadcrumb separator=">">
    33. <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    34. <el-breadcrumb-item :to="{ path: `/category/${filterData.parentId}` }">{{ filterData.parentName }}
    35. </el-breadcrumb-item>
    36. <el-breadcrumb-item>{{ filterData.name }}</el-breadcrumb-item>
    37. </el-breadcrumb>
    38. </div>
    39. <div class="sub-container">
    40. <el-tabs>
    41. <el-tab-pane label="最新商品" name="publishTime"></el-tab-pane>
    42. <el-tab-pane label="最高人气" name="orderNum"></el-tab-pane>
    43. <el-tab-pane label="评论最多" name="evaluateNum"></el-tab-pane>
    44. </el-tabs>
    45. <div class="body">
    46. <!-- 商品列表-->
    47. <GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"></GoodsItem>
    48. </div>
    49. </div>
    50. </div>
    51. </template>
    52. <style lang="scss" scoped>
    53. .container {
    54. width: 1240px;
    55. margin: 0 auto;
    56. .bread-container {
    57. padding: 25px 0;
    58. color: #666;
    59. }
    60. .sub-container {
    61. padding: 20px 10px;
    62. background-color: #fff;
    63. .body {
    64. display: flex;
    65. flex-wrap: wrap;
    66. padding: 0 10px;
    67. }
    68. .goods-item {
    69. display: block;
    70. width: 220px;
    71. margin-right: 20px;
    72. padding: 20px 30px;
    73. text-align: center;
    74. img {
    75. width: 160px;
    76. height: 160px;
    77. }
    78. p {
    79. padding-top: 10px;
    80. }
    81. .name {
    82. font-size: 16px;
    83. }
    84. .desc {
    85. color: #999;
    86. height: 29px;
    87. }
    88. .price {
    89. color: $priceColor;
    90. font-size: 20px;
    91. }
    92. }
    93. .pagination-container {
    94. margin-top: 20px;
    95. display: flex;
    96. justify-content: center;
    97. }
    98. }
    99. }
    100. </style>

    subCategory/index.vue修改 

    1. <div class="body" v-infinite-scroll="load">
    2. <!-- 商品列表-->
    3. <GoodsItem v-for="goods in goodList" :goods="goods" :key="goods.id"></GoodsItem>
    4. </div>
    5. <script setup>
    6. import { getCategoryFilterAPI,getSubCategoryAPI} from '@/api/category'
    7. import {ref,onMounted} from 'vue'
    8. import { useRoute } from 'vue-router'
    9. import GoodsItem from '../home/components/GoodsItem.vue'
    10. // 获取面包屑导航数据
    11. const filterData = ref({})
    12. const route = useRoute()
    13. const getFilterData = async () => {
    14. const res = await getCategoryFilterAPI(route.params.id)
    15. filterData.value = res.result
    16. }
    17. getFilterData()
    18. // 获取基础列表数据渲染
    19. const goodList = ref([])
    20. const reqData = ref({
    21. categoryId: route.params.id,
    22. page: 1,
    23. pageSize: 20,
    24. sortField: 'publishTime'
    25. })
    26. const getGoodList = async () => {
    27. const res = await getSubCategoryAPI(reqData.value)
    28. console.log(res)
    29. goodList.value = res.result.items
    30. }
    31. onMounted(() => getGoodList())
    32. // tab切换回调
    33. const tabChange = () => {
    34. console.log('tab切换了', reqData.value.sortField)
    35. reqData.value.page = 1
    36. getGoodList()
    37. }
    38. // 加载更多
    39. const disabled = ref(false)
    40. const load = async () => {
    41. console.log('加载更多数据咯')
    42. // 获取下一页的数据
    43. reqData.value.page++
    44. const res = await getSubCategoryAPI(reqData.value)
    45. goodList.value = [...goodList.value, ...res.result.items]
    46. // 加载完毕 停止监听
    47. if (res.result.items.length === 0) {
    48. disabled.value = true
    49. }
    50. }
    51. </script>

    无限加载。

    定制路由的滚动行为

    自动回到顶部。

    router/index.js

    1. import { createRouter, createWebHistory } from 'vue-router'
    2. import HomeView from '../views/HomeView.vue'
    3. import Login from '@/views/login/index.vue'
    4. import Layout from '@/views/layout/index.vue'
    5. import Home from '@/views/home/index.vue'
    6. import Category from '@/views/category/index.vue'
    7. import SubCategory from '@/views/subCategory/index.vue'
    8. const router = createRouter({
    9. history: createWebHistory(import.meta.env.BASE_URL),
    10. routes: [
    11. {
    12. path: '/',
    13. component: Layout,
    14. children: [
    15. {
    16. path: '',
    17. component: Home
    18. },
    19. {
    20. path: 'category/:id',
    21. component: Category
    22. },
    23. {
    24. path: 'category/sub/:id',
    25. name: 'subCategory',
    26. component: SubCategory
    27. },
    28. ]
    29. },
    30. {
    31. path: '/login',
    32. component: Login
    33. }
    34. ],
    35. //路由滚动行为
    36. scrollBehavior() {
    37. return {
    38. top: 0
    39. }
    40. }
    41. })
    42. export default router

    商品详情

    路由配置

    views/detail/index.vue

    1. <script setup>
    2. </script>
    3. <template>
    4. <div class="xtx-goods-page">
    5. <div class="container">
    6. <div class="bread-container">
    7. <el-breadcrumb separator=">">
    8. <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    9. <el-breadcrumb-item :to="{ path: '/' }">母婴
    10. </el-breadcrumb-item>
    11. <el-breadcrumb-item :to="{ path: '/' }">跑步鞋
    12. </el-breadcrumb-item>
    13. <el-breadcrumb-item>抓绒保暖,毛毛虫子儿童运动鞋</el-breadcrumb-item>
    14. </el-breadcrumb>
    15. </div>
    16. <!-- 商品信息 -->
    17. <div class="info-container">
    18. <div>
    19. <div class="goods-info">
    20. <div class="media">
    21. <!-- 图片预览区 -->
    22. <!-- 统计数量 -->
    23. <ul class="goods-sales">
    24. <li>
    25. <p>销量人气</p>
    26. <p> 100+ </p>
    27. <p><i class="iconfont icon-task-filling"></i>销量人气</p>
    28. </li>
    29. <li>
    30. <p>商品评价</p>
    31. <p>200+</p>
    32. <p><i class="iconfont icon-comment-filling"></i>查看评价</p>
    33. </li>
    34. <li>
    35. <p>收藏人气</p>
    36. <p>300+</p>
    37. <p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>
    38. </li>
    39. <li>
    40. <p>品牌信息</p>
    41. <p>400+</p>
    42. <p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>
    43. </li>
    44. </ul>
    45. </div>
    46. <div class="spec">
    47. <!-- 商品信息区 -->
    48. <p class="g-name"> 抓绒保暖,毛毛虫儿童鞋 </p>
    49. <p class="g-desc">好穿 </p>
    50. <p class="g-price">
    51. <span>200</span>
    52. <span> 100</span>
    53. </p>
    54. <div class="g-service">
    55. <dl>
    56. <dt>促销</dt>
    57. <dd>12月好物放送,App领券购买直降120</dd>
    58. </dl>
    59. <dl>
    60. <dt>服务</dt>
    61. <dd>
    62. <span>无忧退货</span>
    63. <span>快速退款</span>
    64. <span>免费包邮</span>
    65. <a href="javascript:;">了解详情</a>
    66. </dd>
    67. </dl>
    68. </div>
    69. <!-- sku组件 -->
    70. <!-- 数据组件 -->
    71. <!-- 按钮组件 -->
    72. <div>
    73. <el-button size="large" class="btn">
    74. 加入购物车
    75. </el-button>
    76. </div>
    77. </div>
    78. </div>
    79. <div class="goods-footer">
    80. <div class="goods-article">
    81. <!-- 商品详情 -->
    82. <div class="goods-tabs">
    83. <nav>
    84. <a>商品详情</a>
    85. </nav>
    86. <div class="goods-detail">
    87. <!-- 属性 -->
    88. <ul class="attrs">
    89. <li v-for="item in 3" :key="item.value">
    90. <span class="dt">白色</span>
    91. <span class="dd">纯棉</span>
    92. </li>
    93. </ul>
    94. <!-- 图片 -->
    95. </div>
    96. </div>
    97. </div>
    98. <!-- 24热榜+专题推荐 -->
    99. <div class="goods-aside">
    100. </div>
    101. </div>
    102. </div>
    103. </div>
    104. </div>
    105. </div>
    106. </template>
    107. <style scoped lang='scss'>
    108. .xtx-goods-page {
    109. .goods-info {
    110. min-height: 600px;
    111. background: #fff;
    112. display: flex;
    113. .media {
    114. width: 580px;
    115. height: 600px;
    116. padding: 30px 50px;
    117. }
    118. .spec {
    119. flex: 1;
    120. padding: 30px 30px 30px 0;
    121. }
    122. }
    123. .goods-footer {
    124. display: flex;
    125. margin-top: 20px;
    126. .goods-article {
    127. width: 940px;
    128. margin-right: 20px;
    129. }
    130. .goods-aside {
    131. width: 280px;
    132. min-height: 1000px;
    133. }
    134. }
    135. .goods-tabs {
    136. min-height: 600px;
    137. background: #fff;
    138. }
    139. .goods-warn {
    140. min-height: 600px;
    141. background: #fff;
    142. margin-top: 20px;
    143. }
    144. .number-box {
    145. display: flex;
    146. align-items: center;
    147. .label {
    148. width: 60px;
    149. color: #999;
    150. padding-left: 10px;
    151. }
    152. }
    153. .g-name {
    154. font-size: 22px;
    155. }
    156. .g-desc {
    157. color: #999;
    158. margin-top: 10px;
    159. }
    160. .g-price {
    161. margin-top: 10px;
    162. span {
    163. &::before {
    164. content: "¥";
    165. font-size: 14px;
    166. }
    167. &:first-child {
    168. color: $priceColor;
    169. margin-right: 10px;
    170. font-size: 22px;
    171. }
    172. &:last-child {
    173. color: #999;
    174. text-decoration: line-through;
    175. font-size: 16px;
    176. }
    177. }
    178. }
    179. .g-service {
    180. background: #f5f5f5;
    181. width: 500px;
    182. padding: 20px 10px 0 10px;
    183. margin-top: 10px;
    184. dl {
    185. padding-bottom: 20px;
    186. display: flex;
    187. align-items: center;
    188. dt {
    189. width: 50px;
    190. color: #999;
    191. }
    192. dd {
    193. color: #666;
    194. &:last-child {
    195. span {
    196. margin-right: 10px;
    197. &::before {
    198. content: "•";
    199. color: $xtxColor;
    200. margin-right: 2px;
    201. }
    202. }
    203. a {
    204. color: $xtxColor;
    205. }
    206. }
    207. }
    208. }
    209. }
    210. .goods-sales {
    211. display: flex;
    212. width: 400px;
    213. align-items: center;
    214. text-align: center;
    215. height: 140px;
    216. li {
    217. flex: 1;
    218. position: relative;
    219. ~li::after {
    220. position: absolute;
    221. top: 10px;
    222. left: 0;
    223. height: 60px;
    224. border-left: 1px solid #e4e4e4;
    225. content: "";
    226. }
    227. p {
    228. &:first-child {
    229. color: #999;
    230. }
    231. &:nth-child(2) {
    232. color: $priceColor;
    233. margin-top: 10px;
    234. }
    235. &:last-child {
    236. color: #666;
    237. margin-top: 10px;
    238. i {
    239. color: $xtxColor;
    240. font-size: 14px;
    241. margin-right: 2px;
    242. }
    243. &:hover {
    244. color: $xtxColor;
    245. cursor: pointer;
    246. }
    247. }
    248. }
    249. }
    250. }
    251. }
    252. .goods-tabs {
    253. min-height: 600px;
    254. background: #fff;
    255. nav {
    256. height: 70px;
    257. line-height: 70px;
    258. display: flex;
    259. border-bottom: 1px solid #f5f5f5;
    260. a {
    261. padding: 0 40px;
    262. font-size: 18px;
    263. position: relative;
    264. >span {
    265. color: $priceColor;
    266. font-size: 16px;
    267. margin-left: 10px;
    268. }
    269. }
    270. }
    271. }
    272. .goods-detail {
    273. padding: 40px;
    274. .attrs {
    275. display: flex;
    276. flex-wrap: wrap;
    277. margin-bottom: 30px;
    278. li {
    279. display: flex;
    280. margin-bottom: 10px;
    281. width: 50%;
    282. .dt {
    283. width: 100px;
    284. color: #999;
    285. }
    286. .dd {
    287. flex: 1;
    288. color: #666;
    289. }
    290. }
    291. }
    292. >img {
    293. width: 100%;
    294. }
    295. }
    296. .btn {
    297. margin-top: 20px;
    298. }
    299. .bread-container {
    300. padding: 25px 0;
    301. }
    302. </style>

    router/index.js中children中添加

    1. {
    2. path: 'detail/:id',
    3. component: Detail
    4. }

    home/components/HomeNew.vue修改

    1. <li v-for="item in newList" :key="item.id">
    2. <RouterLink :to="`/detail/${item.id}`">
    3. <img :src="item.picture" alt="" />
    4. <p class="name">{{ item.name }}</p>
    5. <p class="price">&yen;{{ item.price }}</p>
    6. </RouterLink>
    7. </li>

    数据渲染

    detail/index.vue

    1. <script setup>
    2. import { ref } from 'vue'
    3. import { getDetail } from '@/api/detail'
    4. import { useRoute } from 'vue-router'
    5. const goods = ref([])
    6. const route = useRoute()
    7. const getGoods = async () => {
    8. const res = await getDetail(route.params.id)
    9. goods.value = res.result
    10. }
    11. getGoods()
    12. script>
    13. <template>
    14. <div class="xtx-goods-page">
    15. <div class="container">
    16. <div class="bread-container">
    17. <el-breadcrumb separator=">">
    18. <el-breadcrumb-item :to="{ path: '/' }">首页el-breadcrumb-item>
    19. <el-breadcrumb-item :to="{ path: `/category/${goods.categories?.[1].id}` }">{{goods.categories?.[1].name}}
    20. el-breadcrumb-item>
    21. <el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories?.[0].id}` }">{{goods.categories?.[0].name}}
    22. el-breadcrumb-item>
    23. <el-breadcrumb-item>{{goods.name}}el-breadcrumb-item>
    24. el-breadcrumb>
    25. div>
    26. <div class="info-container">
    27. <div>
    28. <div class="goods-info">
    29. <div class="media">
    30. <ul class="goods-sales">
    31. <li>
    32. <p>销量人气p>
    33. <p> {{goods.salesCount}}+p>
    34. <p><i class="iconfont icon-task-filling">i>销量人气p>
    35. li>
    36. <li>
    37. <p>商品评价p>
    38. <p>{{goods.commentCount}}+p>
    39. <p><i class="iconfont icon-comment-filling">i>查看评价p>
    40. li>
    41. <li>
    42. <p>收藏人气p>
    43. <p>{{goods.collectCount}}+p>
    44. <p><i class="iconfont icon-favorite-filling">i>收藏商品p>
    45. li>
    46. <li>
    47. <p>品牌信息p>
    48. <p>{{goods.brand.name}}p>
    49. <p><i class="iconfont icon-dynamic-filling">i>品牌主页p>
    50. li>
    51. ul>
    52. div>
    53. <div class="spec">
    54. <p class="g-name"> {{goods.name}} p>
    55. <p class="g-desc">{{goods.desc}} p>
    56. <p class="g-price">
    57. <span>{{goods.oldPrice}}span>
    58. <span>{{goods.price}}span>
    59. p>
    60. <div class="g-service">
    61. <dl>
    62. <dt>促销dt>
    63. <dd>12月好物放送,App领券购买直降120元dd>
    64. dl>
    65. <dl>
    66. <dt>服务dt>
    67. <dd>
    68. <span>无忧退货span>
    69. <span>快速退款span>
    70. <span>免费包邮span>
    71. <a href="javascript:;">了解详情a>
    72. dd>
    73. dl>
    74. div>
    75. <div>
    76. <el-button size="large" class="btn">
    77. 加入购物车
    78. el-button>
    79. div>
    80. div>
    81. div>
    82. <div class="goods-footer">
    83. <div class="goods-article">
    84. <div class="goods-tabs">
    85. <nav>
    86. <a>商品详情a>
    87. nav>
    88. <div class="goods-detail">
    89. <ul class="attrs">
    90. <li v-for="item in goods.details.properties" :key="item.value">
    91. <span class="dt">{{item.name}}span>
    92. <span class="dd">{{item.value}}span>
    93. li>
    94. ul>
    95. <img v-for="img in goods.details.pictures" :src="img" key="img" alt=""/>
    96. div>
    97. div>
    98. div>
    99. <div class="goods-aside">
    100. div>
    101. div>
    102. div>
    103. div>
    104. div>
    105. div>
    106. template>
    107. <style scoped lang='scss'>
    108. .xtx-goods-page {
    109. .goods-info {
    110. min-height: 600px;
    111. background: #fff;
    112. display: flex;
    113. .media {
    114. width: 580px;
    115. height: 600px;
    116. padding: 30px 50px;
    117. }
    118. .spec {
    119. flex: 1;
    120. padding: 30px 30px 30px 0;
    121. }
    122. }
    123. .goods-footer {
    124. display: flex;
    125. margin-top: 20px;
    126. .goods-article {
    127. width: 940px;
    128. margin-right: 20px;
    129. }
    130. .goods-aside {
    131. width: 280px;
    132. min-height: 1000px;
    133. }
    134. }
    135. .goods-tabs {
    136. min-height: 600px;
    137. background: #fff;
    138. }
    139. .goods-warn {
    140. min-height: 600px;
    141. background: #fff;
    142. margin-top: 20px;
    143. }
    144. .number-box {
    145. display: flex;
    146. align-items: center;
    147. .label {
    148. width: 60px;
    149. color: #999;
    150. padding-left: 10px;
    151. }
    152. }
    153. .g-name {
    154. font-size: 22px;
    155. }
    156. .g-desc {
    157. color: #999;
    158. margin-top: 10px;
    159. }
    160. .g-price {
    161. margin-top: 10px;
    162. span {
    163. &::before {
    164. content: "¥";
    165. font-size: 14px;
    166. }
    167. &:first-child {
    168. color: $priceColor;
    169. margin-right: 10px;
    170. font-size: 22px;
    171. }
    172. &:last-child {
    173. color: #999;
    174. text-decoration: line-through;
    175. font-size: 16px;
    176. }
    177. }
    178. }
    179. .g-service {
    180. background: #f5f5f5;
    181. width: 500px;
    182. padding: 20px 10px 0 10px;
    183. margin-top: 10px;
    184. dl {
    185. padding-bottom: 20px;
    186. display: flex;
    187. align-items: center;
    188. dt {
    189. width: 50px;
    190. color: #999;
    191. }
    192. dd {
    193. color: #666;
    194. &:last-child {
    195. span {
    196. margin-right: 10px;
    197. &::before {
    198. content: "•";
    199. color: $xtxColor;
    200. margin-right: 2px;
    201. }
    202. }
    203. a {
    204. color: $xtxColor;
    205. }
    206. }
    207. }
    208. }
    209. }
    210. .goods-sales {
    211. display: flex;
    212. width: 400px;
    213. align-items: center;
    214. text-align: center;
    215. height: 140px;
    216. li {
    217. flex: 1;
    218. position: relative;
    219. ~li::after {
    220. position: absolute;
    221. top: 10px;
    222. left: 0;
    223. height: 60px;
    224. border-left: 1px solid #e4e4e4;
    225. content: "";
    226. }
    227. p {
    228. &:first-child {
    229. color: #999;
    230. }
    231. &:nth-child(2) {
    232. color: $priceColor;
    233. margin-top: 10px;
    234. }
    235. &:last-child {
    236. color: #666;
    237. margin-top: 10px;
    238. i {
    239. color: $xtxColor;
    240. font-size: 14px;
    241. margin-right: 2px;
    242. }
    243. &:hover {
    244. color: $xtxColor;
    245. cursor: pointer;
    246. }
    247. }
    248. }
    249. }
    250. }
    251. }
    252. .goods-tabs {
    253. min-height: 600px;
    254. background: #fff;
    255. nav {
    256. height: 70px;
    257. line-height: 70px;
    258. display: flex;
    259. border-bottom: 1px solid #f5f5f5;
    260. a {
    261. padding: 0 40px;
    262. font-size: 18px;
    263. position: relative;
    264. >span {
    265. color: $priceColor;
    266. font-size: 16px;
    267. margin-left: 10px;
    268. }
    269. }
    270. }
    271. }
    272. .goods-detail {
    273. padding: 40px;
    274. .attrs {
    275. display: flex;
    276. flex-wrap: wrap;
    277. margin-bottom: 30px;
    278. li {
    279. display: flex;
    280. margin-bottom: 10px;
    281. width: 50%;
    282. .dt {
    283. width: 100px;
    284. color: #999;
    285. }
    286. .dd {
    287. flex: 1;
    288. color: #666;
    289. }
    290. }
    291. }
    292. >img {
    293. width: 100%;
    294. }
    295. }
    296. .btn {
    297. margin-top: 20px;
    298. }
    299. .bread-container {
    300. padding: 25px 0;
    301. }
    302. style>

    热榜区域 

    api/detail.js添加

    1. export const fetchHotGoodsAPI = ({ id, type, limit = 3 }) => {
    2. return httpInstance({
    3. url:'/goods/hot',
    4. params:{
    5. id,
    6. type,
    7. limit
    8. }
    9. })
    10. }

    DetailHot.vue

    1. <script setup>
    2. import { ref } from 'vue'
    3. import { fetchHotGoodsAPI } from '@/api/detail'
    4. import { useRoute } from 'vue-router'
    5. const goodList = ref([])
    6. const route = useRoute()
    7. const getHotList = async () => {
    8. const res = await fetchHotGoodsAPI({
    9. id: route.params.id,
    10. type: 1
    11. })
    12. goodList.value = res.result
    13. }
    14. getHotList()
    15. </script>
    16. <template>
    17. <div class="goods-hot">
    18. <h3> 24小时热榜 </h3>
    19. <!-- 商品区块 -->
    20. <RouterLink :to="`/detail/${item.id}`" class="goods-item" v-for="item in goodList" :key="item.id">
    21. <img :src="item.picture" alt="" />
    22. <p class="name ellipsis">{{ item.name }}</p>
    23. <p class="desc ellipsis">{{ item.desc }}</p>
    24. <p class="price">&yen;{{ item.price }}</p>
    25. </RouterLink>
    26. </div>
    27. </template>
    28. <style scoped lang="scss">
    29. .goods-hot {
    30. h3 {
    31. height: 70px;
    32. background: $helpColor;
    33. color: #fff;
    34. font-size: 18px;
    35. line-height: 70px;
    36. padding-left: 25px;
    37. margin-bottom: 10px;
    38. font-weight: normal;
    39. }
    40. .goods-item {
    41. display: block;
    42. padding: 20px 30px;
    43. text-align: center;
    44. background: #fff;
    45. img {
    46. width: 160px;
    47. height: 160px;
    48. }
    49. p {
    50. padding-top: 10px;
    51. }
    52. .name {
    53. font-size: 16px;
    54. }
    55. .desc {
    56. color: #999;
    57. height: 29px;
    58. }
    59. .price {
    60. color: $priceColor;
    61. font-size: 20px;
    62. }
    63. }
    64. }
    65. </style>

    detail/index.vue

    1. <script setup>
    2. import { ref } from 'vue'
    3. import { getDetail } from '@/api/detail'
    4. import { useRoute } from 'vue-router'
    5. import DetailHot from './components/DetailHot.vue'
    6. const goods = ref([])
    7. const route = useRoute()
    8. const getGoods = async () => {
    9. const res = await getDetail(route.params.id)
    10. goods.value = res.result
    11. }
    12. getGoods()
    13. </script>
    14. <template>
    15. <div class="xtx-goods-page">
    16. <div class="container">
    17. <div class="bread-container">
    18. <el-breadcrumb separator=">">
    19. <!-- 可选链 -->
    20. <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
    21. <el-breadcrumb-item :to="{ path: `/category/${goods.categories?.[1].id}` }">{{goods.categories?.[1].name}}
    22. </el-breadcrumb-item>
    23. <el-breadcrumb-item :to="{ path: `/category/sub/${goods.categories?.[0].id}` }">{{goods.categories?.[0].name}}
    24. </el-breadcrumb-item>
    25. <el-breadcrumb-item>{{goods.name}}</el-breadcrumb-item>
    26. </el-breadcrumb>
    27. </div>
    28. <!-- 商品信息 -->
    29. <div class="info-container">
    30. <div>
    31. <div class="goods-info">
    32. <div class="media">
    33. <!-- 图片预览区 -->
    34. <!-- 统计数量 -->
    35. <ul class="goods-sales">
    36. <li>
    37. <p>销量人气</p>
    38. <p> {{goods.salesCount}}+</p>
    39. <p><i class="iconfont icon-task-filling"></i>销量人气</p>
    40. </li>
    41. <li>
    42. <p>商品评价</p>
    43. <p>{{goods.commentCount}}+</p>
    44. <p><i class="iconfont icon-comment-filling"></i>查看评价</p>
    45. </li>
    46. <li>
    47. <p>收藏人气</p>
    48. <p>{{goods.collectCount}}+</p>
    49. <p><i class="iconfont icon-favorite-filling"></i>收藏商品</p>
    50. </li>
    51. <li>
    52. <p>品牌信息</p>
    53. <p>{{goods?.brand?.name}}</p>
    54. <p><i class="iconfont icon-dynamic-filling"></i>品牌主页</p>
    55. </li>
    56. </ul>
    57. </div>
    58. <div class="spec">
    59. <!-- 商品信息区 -->
    60. <p class="g-name"> {{goods.name}} </p>
    61. <p class="g-desc">{{goods.desc}} </p>
    62. <p class="g-price">
    63. <span>{{goods.oldPrice}}</span>
    64. <span>{{goods.price}}</span>
    65. </p>
    66. <div class="g-service">
    67. <dl>
    68. <dt>促销</dt>
    69. <dd>12月好物放送,App领券购买直降120</dd>
    70. </dl>
    71. <dl>
    72. <dt>服务</dt>
    73. <dd>
    74. <span>无忧退货</span>
    75. <span>快速退款</span>
    76. <span>免费包邮</span>
    77. <a href="javascript:;">了解详情</a>
    78. </dd>
    79. </dl>
    80. </div>
    81. <!-- sku组件 -->
    82. <!-- 数据组件 -->
    83. <!-- 按钮组件 -->
    84. <div>
    85. <el-button size="large" class="btn">
    86. 加入购物车
    87. </el-button>
    88. </div>
    89. </div>
    90. </div>
    91. <div class="goods-footer">
    92. <div class="goods-article">
    93. <!-- 商品详情 -->
    94. <div class="goods-tabs">
    95. <nav>
    96. <a>商品详情</a>
    97. </nav>
    98. <div class="goods-detail">
    99. <!-- 属性 -->
    100. <ul class="attrs">
    101. <li v-for="item in goods?.details?.properties" :key="item.value">
    102. <span class="dt">{{item.name}}</span>
    103. <span class="dd">{{item.value}}</span>
    104. </li>
    105. </ul>
    106. <!-- 图片 -->
    107. <img v-for="img in goods?.details?.pictures" :src="img" key="img" alt=""/>
    108. </div>
    109. </div>
    110. </div>
    111. <!-- 24热榜+专题推荐 -->
    112. <div class="goods-aside">
    113. <DetailHot/>
    114. <DetailHot/>
    115. </div>
    116. </div>
    117. </div>
    118. </div>
    119. </div>
    120. </div>
    121. </template>
    122. <style scoped lang='scss'>
    123. .xtx-goods-page {
    124. .goods-info {
    125. min-height: 600px;
    126. background: #fff;
    127. display: flex;
    128. .media {
    129. width: 580px;
    130. height: 600px;
    131. padding: 30px 50px;
    132. }
    133. .spec {
    134. flex: 1;
    135. padding: 30px 30px 30px 0;
    136. }
    137. }
    138. .goods-footer {
    139. display: flex;
    140. margin-top: 20px;
    141. .goods-article {
    142. width: 940px;
    143. margin-right: 20px;
    144. }
    145. .goods-aside {
    146. width: 280px;
    147. min-height: 1000px;
    148. }
    149. }
    150. .goods-tabs {
    151. min-height: 600px;
    152. background: #fff;
    153. }
    154. .goods-warn {
    155. min-height: 600px;
    156. background: #fff;
    157. margin-top: 20px;
    158. }
    159. .number-box {
    160. display: flex;
    161. align-items: center;
    162. .label {
    163. width: 60px;
    164. color: #999;
    165. padding-left: 10px;
    166. }
    167. }
    168. .g-name {
    169. font-size: 22px;
    170. }
    171. .g-desc {
    172. color: #999;
    173. margin-top: 10px;
    174. }
    175. .g-price {
    176. margin-top: 10px;
    177. span {
    178. &::before {
    179. content: "¥";
    180. font-size: 14px;
    181. }
    182. &:first-child {
    183. color: $priceColor;
    184. margin-right: 10px;
    185. font-size: 22px;
    186. }
    187. &:last-child {
    188. color: #999;
    189. text-decoration: line-through;
    190. font-size: 16px;
    191. }
    192. }
    193. }
    194. .g-service {
    195. background: #f5f5f5;
    196. width: 500px;
    197. padding: 20px 10px 0 10px;
    198. margin-top: 10px;
    199. dl {
    200. padding-bottom: 20px;
    201. display: flex;
    202. align-items: center;
    203. dt {
    204. width: 50px;
    205. color: #999;
    206. }
    207. dd {
    208. color: #666;
    209. &:last-child {
    210. span {
    211. margin-right: 10px;
    212. &::before {
    213. content: "•";
    214. color: $xtxColor;
    215. margin-right: 2px;
    216. }
    217. }
    218. a {
    219. color: $xtxColor;
    220. }
    221. }
    222. }
    223. }
    224. }
    225. .goods-sales {
    226. display: flex;
    227. width: 400px;
    228. align-items: center;
    229. text-align: center;
    230. height: 140px;
    231. li {
    232. flex: 1;
    233. position: relative;
    234. ~li::after {
    235. position: absolute;
    236. top: 10px;
    237. left: 0;
    238. height: 60px;
    239. border-left: 1px solid #e4e4e4;
    240. content: "";
    241. }
    242. p {
    243. &:first-child {
    244. color: #999;
    245. }
    246. &:nth-child(2) {
    247. color: $priceColor;
    248. margin-top: 10px;
    249. }
    250. &:last-child {
    251. color: #666;
    252. margin-top: 10px;
    253. i {
    254. color: $xtxColor;
    255. font-size: 14px;
    256. margin-right: 2px;
    257. }
    258. &:hover {
    259. color: $xtxColor;
    260. cursor: pointer;
    261. }
    262. }
    263. }
    264. }
    265. }
    266. }
    267. .goods-tabs {
    268. min-height: 600px;
    269. background: #fff;
    270. nav {
    271. height: 70px;
    272. line-height: 70px;
    273. display: flex;
    274. border-bottom: 1px solid #f5f5f5;
    275. a {
    276. padding: 0 40px;
    277. font-size: 18px;
    278. position: relative;
    279. >span {
    280. color: $priceColor;
    281. font-size: 16px;
    282. margin-left: 10px;
    283. }
    284. }
    285. }
    286. }
    287. .goods-detail {
    288. padding: 40px;
    289. .attrs {
    290. display: flex;
    291. flex-wrap: wrap;
    292. margin-bottom: 30px;
    293. li {
    294. display: flex;
    295. margin-bottom: 10px;
    296. width: 50%;
    297. .dt {
    298. width: 100px;
    299. color: #999;
    300. }
    301. .dd {
    302. flex: 1;
    303. color: #666;
    304. }
    305. }
    306. }
    307. >img {
    308. width: 100%;
    309. }
    310. }
    311. .btn {
    312. margin-top: 20px;
    313. }
    314. .bread-container {
    315. padding: 25px 0;
    316. }
    317. </style>

    适配热榜类型 

    detail/components/DeatinHot.vue修改

    1. <script setup>
    2. import { ref,computed} from 'vue'
    3. import { fetchHotGoodsAPI } from '@/api/detail'
    4. import { useRoute } from 'vue-router'
    5. // type适配不同类型热榜数据
    6. const props = defineProps({
    7. hotType: {
    8. type: Number, // 1代表24小时热销榜 2代表周热销榜 3代表总热销榜 可以使用type去适配title和数据列表
    9. default: 1
    10. }
    11. })
    12. const TITLEMAP = {
    13. 1: '24小时热榜',
    14. 2: '周热榜',
    15. }
    16. const title = computed(() => TITLEMAP[props.hotType])
    17. const goodList = ref([])
    18. const route = useRoute()
    19. const getHotList = async () => {
    20. const res = await fetchHotGoodsAPI({
    21. id: route.params.id,
    22. type: props.hotType
    23. })
    24. goodList.value = res.result
    25. }
    26. getHotList()
    27. </script>
    28. <!-- 24热榜+专题推荐 -->
    29. <div class="goods-aside">
    30. <DetailHot :hotType="1"/>
    31. <DetailHot :hotType="2"/>
    32. </div>

    detail/index.vue

    1. <!-- 24热榜+专题推荐 -->
    2. <div class="goods-aside">
    3. <DetailHot :hotType="1"/>
    4. <DetailHot :hotType="2"/>
    5. </div>

    图片预览组件封装

    src/components/imgView/index.vue

    1. <script setup>
    2. import { ref, watch } from 'vue'
    3. import { useMouseInElement } from '@vueuse/core'
    4. defineProps({
    5. imageList: {
    6. type: Array,
    7. default: () => []
    8. }
    9. })
    10. // // 图片列表
    11. // const imageList = [
    12. // "https://yanxuan-item.nosdn.127.net/d917c92e663c5ed0bb577c7ded73e4ec.png",
    13. // "https://yanxuan-item.nosdn.127.net/e801b9572f0b0c02a52952b01adab967.jpg",
    14. // "https://yanxuan-item.nosdn.127.net/b52c447ad472d51adbdde1a83f550ac2.jpg",
    15. // "https://yanxuan-item.nosdn.127.net/f93243224dc37674dfca5874fe089c60.jpg",
    16. // "https://yanxuan-item.nosdn.127.net/f881cfe7de9a576aaeea6ee0d1d24823.jpg"
    17. // ]
    18. // 1.小图切换大图显示
    19. const activeIndex = ref(0)
    20. const enterhandler = (i) => {
    21. activeIndex.value = i
    22. }
    23. // 2. 获取鼠标相对位置
    24. const target = ref(null)
    25. const { elementX, elementY, isOutside } = useMouseInElement(target)
    26. // 3. 控制滑块跟随鼠标移动(监听elementX/Y变化,一旦变化 重新设置left/top
    27. const left = ref(0)
    28. const top = ref(0)
    29. const positionX = ref(0)
    30. const positionY = ref(0)
    31. watch([elementX, elementY, isOutside], () => {
    32. console.log('xy变化了')
    33. // 如果鼠标没有移入到盒子里面 直接不执行后面的逻辑
    34. if (isOutside.value) return
    35. console.log('后续逻辑执行了')
    36. // 有效范围内控制滑块距离
    37. // 横向
    38. if (elementX.value > 100 && elementX.value < 300) {
    39. left.value = elementX.value - 100
    40. }
    41. // 纵向
    42. if (elementY.value > 100 && elementY.value < 300) {
    43. top.value = elementY.value - 100
    44. }
    45. // 处理边界
    46. if (elementX.value > 300) { left.value = 200 }
    47. if (elementX.value < 100) { left.value = 0 }
    48. if (elementY.value > 300) { top.value = 200 }
    49. if (elementY.value < 100) { top.value = 0 }
    50. // 控制大图的显示
    51. positionX.value = -left.value * 2
    52. positionY.value = -top.value * 2
    53. })
    54. </script>
    55. <template>
    56. <div class="goods-image">
    57. <!-- 左侧大图-->
    58. <div class="middle" ref="target">
    59. <img :src="imageList[activeIndex]" alt="" />
    60. <!-- 蒙层小滑块 -->
    61. <div class="layer" v-show="!isOutside" :style="{ left: `${left}px`, top: `${top}px` }"></div>
    62. </div>
    63. <!-- 小图列表 -->
    64. <ul class="small">
    65. <li v-for="(img, i) in imageList" :key="i" @mouseenter="enterhandler(i)" :class="{ active: i === activeIndex }">
    66. <img :src="img" alt="" />
    67. </li>
    68. </ul>
    69. <!-- 放大镜大图 -->
    70. <div class="large" :style="[
    71. {
    72. backgroundImage: `url(${imageList[activeIndex]})`,
    73. backgroundPositionX: `${positionX}px`,
    74. backgroundPositionY: `${positionY}px`,
    75. },
    76. ]" v-show="!isOutside"></div>
    77. </div>
    78. </template>
    79. <style scoped lang="scss">
    80. .goods-image {
    81. width: 480px;
    82. height: 400px;
    83. position: relative;
    84. display: flex;
    85. .middle {
    86. width: 400px;
    87. height: 400px;
    88. background: #f5f5f5;
    89. }
    90. .large {
    91. position: absolute;
    92. top: 0;
    93. left: 412px;
    94. width: 400px;
    95. height: 400px;
    96. z-index: 500;
    97. box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    98. background-repeat: no-repeat;
    99. // 背景图:盒子的大小 = 2:1 将来控制背景图的移动来实现放大的效果查看 background-position
    100. background-size: 800px 800px;
    101. background-color: #f8f8f8;
    102. }
    103. .layer {
    104. width: 200px;
    105. height: 200px;
    106. background: rgba(0, 0, 0, 0.2);
    107. // 绝对定位 然后跟随咱们鼠标控制lefttop属性就可以让滑块移动起来
    108. left: 0;
    109. top: 0;
    110. position: absolute;
    111. }
    112. .small {
    113. width: 80px;
    114. li {
    115. width: 68px;
    116. height: 68px;
    117. margin-left: 12px;
    118. margin-bottom: 15px;
    119. cursor: pointer;
    120. &:hover,
    121. &.active {
    122. border: 2px solid $xtxColor;
    123. }
    124. }
    125. }
    126. }
    127. </style>

    detail/index.vue修改

    1. import ImageView from '@/components/imageView/index.vue'
    2. <ImageView :image-list="goods.mainPictures"/>

     

    sku插件

     XtxSku

    链接:https://pan.baidu.com/s/1KQ5-OFMoD7bpL8DJjsmsIA?pwd=3kgh 
    提取码:3kgh

    detain/index.vue修改 

    1. import XtxSku from '@/components/XtxSku/index.vue'
    2. <XtxSku :goods="goods"/>

     登录页

    表单

    login/index.vue

    1. <script setup>
    2. import { ref } from 'vue'
    3. // 表单数据对象
    4. const userInfo = ref({
    5. account: '1311111111',
    6. password: '123456',
    7. agree: true
    8. })
    9. // 规则数据对象
    10. const rules = {
    11. account: [
    12. { required: true, message: '用户名不能为空' }
    13. ],
    14. password: [
    15. { required: true, message: '密码不能为空' },
    16. { min: 6, max: 24, message: '密码长度要求6-14个字符' }
    17. ],
    18. agree: [
    19. {
    20. validator: (rule, val, callback) => {
    21. return val ? callback() : new Error('请先同意协议')
    22. }
    23. }
    24. ]
    25. }
    26. </script>
    27. <template>
    28. <div>
    29. <header class="login-header">
    30. <div class="container m-top-20">
    31. <h1 class="logo">
    32. <RouterLink to="/">小兔鲜</RouterLink>
    33. </h1>
    34. <RouterLink class="entry" to="/">
    35. 进入网站首页
    36. <i class="iconfont icon-angle-right"></i>
    37. <i class="iconfont icon-angle-right"></i>
    38. </RouterLink>
    39. </div>
    40. </header>
    41. <section class="login-section">
    42. <div class="wrapper">
    43. <nav>
    44. <a href="javascript:;">账户登录</a>
    45. </nav>
    46. <div class="account-box">
    47. <div class="form">
    48. <el-form ref="formRef" :model="userInfo" :rules="rules" status-icon>
    49. <el-form-item prop="account" label="账户">
    50. <el-input v-model="userInfo.account" />
    51. </el-form-item>
    52. <el-form-item prop="password" label="密码">
    53. <el-input v-model="userInfo.password" />
    54. </el-form-item>
    55. <el-form-item prop="agree" label-width="22px">
    56. <el-checkbox v-model="userInfo.agree" size="large">
    57. 我已同意隐私条款和服务条款
    58. </el-checkbox>
    59. </el-form-item>
    60. <el-button size="large" class="subBtn" >点击登录</el-button>
    61. </el-form>
    62. </div>
    63. </div>
    64. </div>
    65. </section>
    66. <footer class="login-footer">
    67. <div class="container">
    68. <p>
    69. <a href="javascript:;">关于我们</a>
    70. <a href="javascript:;">帮助中心</a>
    71. <a href="javascript:;">售后服务</a>
    72. <a href="javascript:;">配送与验收</a>
    73. <a href="javascript:;">商务合作</a>
    74. <a href="javascript:;">搜索推荐</a>
    75. <a href="javascript:;">友情链接</a>
    76. </p>
    77. <p>CopyRight &copy; 小兔鲜儿</p>
    78. </div>
    79. </footer>
    80. </div>
    81. </template>
    82. <style scoped lang='scss'>
    83. .login-header {
    84. background: #fff;
    85. border-bottom: 1px solid #e4e4e4;
    86. .container {
    87. display: flex;
    88. align-items: flex-end;
    89. justify-content: space-between;
    90. }
    91. .logo {
    92. width: 200px;
    93. a {
    94. display: block;
    95. height: 132px;
    96. width: 100%;
    97. text-indent: -9999px;
    98. background: url("@/assets/images/logo.png") no-repeat center 18px / contain;
    99. }
    100. }
    101. .sub {
    102. flex: 1;
    103. font-size: 24px;
    104. font-weight: normal;
    105. margin-bottom: 38px;
    106. margin-left: 20px;
    107. color: #666;
    108. }
    109. .entry {
    110. width: 120px;
    111. margin-bottom: 38px;
    112. font-size: 16px;
    113. i {
    114. font-size: 14px;
    115. color: $xtxColor;
    116. letter-spacing: -5px;
    117. }
    118. }
    119. }
    120. .login-section {
    121. background: url('@/assets/images/login-bg.png') no-repeat center / cover;
    122. height: 488px;
    123. position: relative;
    124. .wrapper {
    125. width: 380px;
    126. background: #fff;
    127. position: absolute;
    128. left: 50%;
    129. top: 54px;
    130. transform: translate3d(100px, 0, 0);
    131. box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
    132. nav {
    133. font-size: 14px;
    134. height: 55px;
    135. margin-bottom: 20px;
    136. border-bottom: 1px solid #f5f5f5;
    137. display: flex;
    138. padding: 0 40px;
    139. text-align: right;
    140. align-items: center;
    141. a {
    142. flex: 1;
    143. line-height: 1;
    144. display: inline-block;
    145. font-size: 18px;
    146. position: relative;
    147. text-align: center;
    148. }
    149. }
    150. }
    151. }
    152. .login-footer {
    153. padding: 30px 0 50px;
    154. background: #fff;
    155. p {
    156. text-align: center;
    157. color: #999;
    158. padding-top: 20px;
    159. a {
    160. line-height: 1;
    161. padding: 0 10px;
    162. color: #999;
    163. display: inline-block;
    164. ~a {
    165. border-left: 1px solid #ccc;
    166. }
    167. }
    168. }
    169. }
    170. .account-box {
    171. .toggle {
    172. padding: 15px 40px;
    173. text-align: right;
    174. a {
    175. color: $xtxColor;
    176. i {
    177. font-size: 14px;
    178. }
    179. }
    180. }
    181. .form {
    182. padding: 0 20px 20px 20px;
    183. &-item {
    184. margin-bottom: 28px;
    185. .input {
    186. position: relative;
    187. height: 36px;
    188. >i {
    189. width: 34px;
    190. height: 34px;
    191. background: #cfcdcd;
    192. color: #fff;
    193. position: absolute;
    194. left: 1px;
    195. top: 1px;
    196. text-align: center;
    197. line-height: 34px;
    198. font-size: 18px;
    199. }
    200. input {
    201. padding-left: 44px;
    202. border: 1px solid #cfcdcd;
    203. height: 36px;
    204. line-height: 36px;
    205. width: 100%;
    206. &.error {
    207. border-color: $priceColor;
    208. }
    209. &.active,
    210. &:focus {
    211. border-color: $xtxColor;
    212. }
    213. }
    214. .code {
    215. position: absolute;
    216. right: 1px;
    217. top: 1px;
    218. text-align: center;
    219. line-height: 34px;
    220. font-size: 14px;
    221. background: #f5f5f5;
    222. color: #666;
    223. width: 90px;
    224. height: 34px;
    225. cursor: pointer;
    226. }
    227. }
    228. >.error {
    229. position: absolute;
    230. font-size: 12px;
    231. line-height: 28px;
    232. color: $priceColor;
    233. i {
    234. font-size: 14px;
    235. margin-right: 2px;
    236. }
    237. }
    238. }
    239. .agree {
    240. a {
    241. color: #069;
    242. }
    243. }
    244. .btn {
    245. display: block;
    246. width: 100%;
    247. height: 40px;
    248. color: #fff;
    249. text-align: center;
    250. line-height: 40px;
    251. background: $xtxColor;
    252. &.disabled {
    253. background: #cfcdcd;
    254. }
    255. }
    256. }
    257. .action {
    258. padding: 20px 40px;
    259. display: flex;
    260. justify-content: space-between;
    261. align-items: center;
    262. .url {
    263. a {
    264. color: #999;
    265. margin-left: 10px;
    266. }
    267. }
    268. }
    269. }
    270. .subBtn {
    271. background: $xtxColor;
    272. width: 100%;
    273. color: #fff;
    274. }
    275. </style>

    登录实现 

    账号:cdshi0006

    密码:123456

    login/index.vue修改

    1. <script setup>
    2. import { ref } from 'vue'
    3. import {loginAPI} from '@/api/user'
    4. import { ElMessage } from 'element-plus'
    5. import {useRouter} from 'vue-router'
    6. import 'element-plus/theme-chalk/el-message.css'
    7. // 表单数据对象
    8. const userInfo = ref({
    9. account: '',
    10. password: '',
    11. agree: true
    12. })
    13. // 规则数据对象
    14. const rules = {
    15. account: [
    16. { required: true, message: '用户名不能为空' }
    17. ],
    18. password: [
    19. { required: true, message: '密码不能为空' },
    20. { min: 6, max: 24, message: '密码长度要求6-14个字符' }
    21. ],
    22. agree: [
    23. {
    24. validator: (rule, val, callback) => {
    25. return val ? callback() : new Error('请先同意协议')
    26. }
    27. }
    28. ]
    29. }
    30. const formRef = ref(null)
    31. const router = useRouter()
    32. const doLogin = () => {
    33. const {account,password} = userInfo.value
    34. formRef.value.validate(async(valid) => {
    35. if(valid) {
    36. const res = await loginAPI({account,password})
    37. // 1. 提示用户
    38. ElMessage({ type: 'success', message: '登录成功' })
    39. // 2. 跳转首页
    40. router.replace({ path: '/' })
    41. }
    42. })
    43. }
    44. </script>

     

     登录失败的提示

    utils/http.js修改

    1. import 'element-plus/theme-chalk/el-message.css'
    2. import { ElMessage } from 'element-plus'
    3. // axios响应式拦截器
    4. http.interceptors.response.use(res=>res.data,e => {
    5. ElMessage({
    6. type: 'warning',
    7. message: e.response.data.msg
    8. })
    9. console.log(e)
    10. return Promise.reject(e)
    11. })

     Pinia管理用户

    stores/user.js

    1. // 管理用户数据相关
    2. import { defineStore } from 'pinia'
    3. import { ref } from 'vue'
    4. import { loginAPI } from '@/api/user'
    5. export const useUserStore = defineStore('user', () => {
    6. // 1. 定义管理用户数据的state
    7. const userInfo = ref({})
    8. // 2. 定义获取接口数据的action函数
    9. const getUserInfo = async ({ account, password }) => {
    10. const res = await loginAPI({ account, password })
    11. userInfo.value = res.result
    12. }
    13. // 3. 以对象的格式把state和action return
    14. return {
    15. userInfo,
    16. getUserInfo
    17. }
    18. }, {
    19. persist: true,
    20. })

    Pinia持久化存储

    需要使用插件,自动本地存储,官网

    下载pinia-plugin-persistedstate

    npm i pinia-plugin-persistedstate

    main.js修改

    1. import { createPinia } from 'pinia'
    2. import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    3. const pinia = createPinia()
    4. pinia.use(piniaPluginPersistedstate)

     login/index.vue修改

    1. export const useUserStore = defineStore('user', () => {
    2. //......
    3. }, {
    4. persist: true,
    5. })

     

    登录和非登录状态

    LayoutNav.vue修改

    1. <script setup>
    2. import {useUserStore} from '@/stores/user'
    3. const userStore = useUserStore()
    4. </script>
    5. <template>
    6. <nav class="app-topnav">
    7. <div class="container">
    8. <ul>
    9. <template v-if="userStore.userInfo.token">
    10. <li><a href="javascript:;"><i class="iconfont icon-user"></i>{{userStore.userInfo.account}}</a></li>
    11. <li>
    12. ......
    13. </ul>
    14. </div>
    15. </nav>
    16. </template>

    请求拦截器携带token

    http.js

    1. import {useUserStore} from '@/stores/user'
    2. http.interceptors.request.use(config => {
    3. // 1. 从pinia获取token数据
    4. const userStore = useUserStore()
    5. // 2. 按照后端的要求拼接token数据
    6. const token = userStore.userInfo.token
    7. if (token) {
    8. config.headers.Authorization = `Bearer ${token}`
    9. }
    10. return config
    11. }, e => Promise.reject(e))

    退出登录

     user.js

    1. // 管理用户数据相关
    2. import { defineStore } from 'pinia'
    3. import { ref } from 'vue'
    4. import { loginAPI } from '@/api/user'
    5. export const useUserStore = defineStore('user', () => {
    6. // 1. 定义管理用户数据的state
    7. const userInfo = ref({})
    8. // 2. 定义获取接口数据的action函数
    9. const getUserInfo = async ({ account, password }) => {
    10. const res = await loginAPI({ account, password })
    11. userInfo.value = res.result
    12. }
    13. // 退出时清除用户信息
    14. const clearUserInfo = () => {
    15. userInfo.value = {}
    16. }
    17. // 3. 以对象的格式把state和action return
    18. return {
    19. userInfo,
    20. getUserInfo,
    21. clearUserInfo
    22. }
    23. }, {
    24. persist: true,
    25. })

    LayoutNav.vue修改

    1. <script setup>
    2. import {useUserStore} from '@/stores/user'
    3. import {useRouter} from 'vue-router'
    4. const userStore = useUserStore()
    5. const router = useRouter()
    6. const confirm = () => {
    7. console.log("登录")
    8. userStore.clearUserInfo()
    9. router.push('/login')
    10. }
    11. </script>
    12. <template>
    13. <nav class="app-topnav">
    14. <div class="container">
    15. <ul>
    16. <template v-if="userStore.userInfo.token">
    17. <li><a href="javascript:;"><i class="iconfont icon-user"></i>{{userStore.userInfo.account}}</a></li>
    18. <li>
    19. <el-popconfirm @confirm="confirm" title="确认退出吗?" confirm-button-text="确认" cancel-button-text="取消">
    20. 。。。。。。
    21. </div>
    22. </nav>
    23. </template>

    token失效处理

    http.js

    1. import router from '@/router'
    2. // axios响应式拦截器
    3. http.interceptors.response.use(res=>res.data,e => {
    4. const userStore = useUserStore()
    5. ElMessage({
    6. type: 'warning',
    7. message: e.response.data.msg
    8. })
    9. if(e.response.status === 401) {
    10. userStore.clearUserInfo()
    11. router.push('/login')
    12. }
    13. return Promise.reject(e)
    14. })

    购物车

    本地加入购物车

    stores/car.js

    1. // 封装购物车模块
    2. import { defineStore } from 'pinia'
    3. import { ref } from 'vue'
    4. export const useCartStore = defineStore('cart', () => {
    5. // 1. 定义state - cartList
    6. const cartList = ref([])
    7. // 2. 定义action - addCart
    8. const addCart = (goods) => {
    9. console.log('添加', goods)
    10. // 添加购物车操作
    11. // 已添加过 - count + 1
    12. // 没有添加过 - 直接push
    13. // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
    14. const item = cartList.value.find((item) => goods.skuId === item.skuId)
    15. if (item) {
    16. // 找到了
    17. item.count++
    18. } else {
    19. // 没找到
    20. cartList.value.push(goods)
    21. }
    22. }
    23. return {
    24. cartList,
    25. addCart
    26. }
    27. }, {
    28. persist: true,
    29. })

    detail/index.vue修改

    1. <script setup>
    2. import { ref } from 'vue'
    3. import { getDetail } from '@/api/detail'
    4. import { useRoute } from 'vue-router'
    5. import DetailHot from './components/DetailHot.vue'
    6. import ImageView from '@/components/imageView/index.vue'
    7. import XtxSku from '@/components/XtxSku/index.vue'
    8. import { ElMessage } from 'element-plus'
    9. import {useCartStore} from '@/stores/car'
    10. const carStore = useCartStore()
    11. const goods = ref([])
    12. const route = useRoute()
    13. const getGoods = async () => {
    14. const res = await getDetail(route.params.id)
    15. goods.value = res.result
    16. }
    17. getGoods()
    18. //sku
    19. let skuObj = {}
    20. const skuChange = (sku) => {
    21. skuObj = sku
    22. }
    23. //count
    24. const count = ref(1)
    25. const countChange = (count) => {
    26. console.log(count)
    27. }
    28. //购物车
    29. const addCar = () => {
    30. if(skuObj.skuId) {
    31. carStore.addCart({
    32. id: goods.value.id,
    33. name: goods.value.name,
    34. picture: goods.value.mainPictures[0],
    35. orice: goods.value.price,
    36. count: count.value,
    37. skuId: skuObj.skuId,
    38. attrsText: skuObj.specsText,
    39. selected: true
    40. })
    41. } else {
    42. ElMessage.warning('请选择规格')
    43. }
    44. }
    45. </script>
    46. <!-- sku组件 -->
    47. <XtxSku :goods="goods" @change="skuChange"/>
    48. <!-- 数据组件 -->
    49. <el-input-number v-model="count" @change="countChange"></el-input-number>
    50. <!-- 按钮组件 -->
    51. <div>
    52. <el-button size="large" class="btn" @click="addCar">
    53. 加入购物车
    54. </el-button>
    55. </div>

    头部购物车

    detail/index.vue修改

    1. <script setup>
    2. import { ref } from 'vue'
    3. import { getDetail } from '@/api/detail'
    4. import { useRoute } from 'vue-router'
    5. import DetailHot from './components/DetailHot.vue'
    6. import ImageView from '@/components/imageView/index.vue'
    7. import XtxSku from '@/components/XtxSku/index.vue'
    8. import { ElMessage } from 'element-plus'
    9. import {useCartStore} from '@/stores/car'
    10. const carStore = useCartStore()
    11. const goods = ref([])
    12. const route = useRoute()
    13. const getGoods = async () => {
    14. const res = await getDetail(route.params.id)
    15. goods.value = res.result
    16. }
    17. getGoods()
    18. //sku
    19. let skuObj = {}
    20. const skuChange = (sku) => {
    21. skuObj = sku
    22. }
    23. //count
    24. const count = ref(1)
    25. const countChange = (count) => {
    26. console.log(count)
    27. }
    28. //购物车
    29. const addCar = () => {
    30. if(skuObj.skuId) {
    31. carStore.addCart({
    32. id: goods.value.id,
    33. name: goods.value.name,
    34. picture: goods.value.mainPictures[0],
    35. price: goods.value.price,
    36. count: count.value,
    37. skuId: skuObj.skuId,
    38. attrsText: skuObj.specsText,
    39. selected: true
    40. })
    41. } else {
    42. ElMessage.warning('请选择规格')
    43. }
    44. }
    45. </script>

    car.js

    1. // 封装购物车模块
    2. import { defineStore } from 'pinia'
    3. import { computed, ref } from 'vue'
    4. export const useCartStore = defineStore('cart', () => {
    5. // 1. 定义state - cartList
    6. const cartList = ref([])
    7. // 2. 定义action - addCart
    8. const addCart = (goods) => {
    9. // console.log('添加', goods)
    10. // 添加购物车操作
    11. // 已添加过 - count + 1
    12. // 没有添加过 - 直接push
    13. // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
    14. const item = cartList.value.find((item) => goods.skuId === item.skuId)
    15. if (item) {
    16. // 找到了
    17. item.count++
    18. } else {
    19. // 没找到
    20. cartList.value.push(goods)
    21. }
    22. }
    23. //删除购物车
    24. const delCart = (skuId) => {
    25. const idx = cartList.value.findIndex((item)=>skuId === item.skuId)
    26. cartList.value.splice(idx,1)
    27. }
    28. //总数
    29. const allCount = computed(()=>cartList.value.reduce((a,c)=>a+c.count,0))
    30. //总价
    31. const allPrice = computed(()=>cartList.value.reduce((a,c)=>a+c.price*c.count,0))
    32. return {
    33. cartList,
    34. addCart,
    35. delCart,
    36. allPrice,
    37. allCount
    38. }
    39. }, {
    40. persist: true,
    41. })

    购物车页面

    router.js添加

    1. {
    2. path: 'cartlist',
    3. component: CartList
    4. }

    views/cartList/index.vue

    1. <script setup>
    2. import {useCartStore} from '@/stores/car'
    3. const cartStore = useCartStore()
    4. const singleCheck = (i, selected) => {
    5. cartStore.singleCheck(i.skuId,selected)
    6. }
    7. const allCheck = (selected) => {
    8. cartStore.allCheck(selected)
    9. }
    10. </script>
    11. <template>
    12. <div class="xtx-cart-page">
    13. <div class="container m-top-20">
    14. <div class="cart">
    15. <table>
    16. <thead>
    17. <tr>
    18. <th width="120">
    19. <el-checkbox :model-value="cartStore.isAll" @change="allCheck"/>
    20. </th>
    21. <th width="400">商品信息</th>
    22. <th width="220">单价</th>
    23. <th width="180">数量</th>
    24. <th width="180">小计</th>
    25. <th width="140">操作</th>
    26. </tr>
    27. </thead>
    28. <!-- 商品列表 -->
    29. <tbody>
    30. <tr v-for="i in cartStore.cartList" :key="i.id">
    31. <td>
    32. <el-checkbox :model-value="i.selected" @change="(selected)=>singleCheck(i,selected)"/>
    33. </td>
    34. <td>
    35. <div class="goods">
    36. <RouterLink to="/"><img :src="i.picture" alt="" /></RouterLink>
    37. <div>
    38. <p class="name ellipsis">
    39. {{ i.name }}
    40. </p>
    41. </div>
    42. </div>
    43. </td>
    44. <td class="tc">
    45. <p>&yen;{{ i.price }}</p>
    46. </td>
    47. <td class="tc">
    48. <el-input-number v-model="i.count" />
    49. </td>
    50. <td class="tc">
    51. <p class="f16 red">&yen;{{ (i.price * i.count).toFixed(2) }}</p>
    52. </td>
    53. <td class="tc">
    54. <p>
    55. <el-popconfirm title="确认删除吗?" confirm-button-text="确认" cancel-button-text="取消" @confirm="delCart(i)">
    56. <template #reference>
    57. <a href="javascript:;">删除</a>
    58. </template>
    59. </el-popconfirm>
    60. </p>
    61. </td>
    62. </tr>
    63. <tr v-if="cartStore.cartList.length === 0">
    64. <td colspan="6">
    65. <div class="cart-none">
    66. <el-empty description="购物车列表为空">
    67. <el-button type="primary">随便逛逛</el-button>
    68. </el-empty>
    69. </div>
    70. </td>
    71. </tr>
    72. </tbody>
    73. </table>
    74. </div>
    75. <!-- 操作栏 -->
    76. <div class="action">
    77. <div class="batch">
    78. 共 {{cartStore.allCount}} 件商品,已选择 {{cartStore.selectCount}} 件,商品合计:
    79. <span class="red">¥ {{cartStore.selectPrice.toFixed(2)}} </span>
    80. </div>
    81. <div class="total">
    82. <el-button size="large" type="primary" >下单结算</el-button>
    83. </div>
    84. </div>
    85. </div>
    86. </div>
    87. </template>
    88. <style scoped lang="scss">
    89. .xtx-cart-page {
    90. margin-top: 20px;
    91. .cart {
    92. background: #fff;
    93. color: #666;
    94. table {
    95. border-spacing: 0;
    96. border-collapse: collapse;
    97. line-height: 24px;
    98. th,
    99. td {
    100. padding: 10px;
    101. border-bottom: 1px solid #f5f5f5;
    102. &:first-child {
    103. text-align: left;
    104. padding-left: 30px;
    105. color: #999;
    106. }
    107. }
    108. th {
    109. font-size: 16px;
    110. font-weight: normal;
    111. line-height: 50px;
    112. }
    113. }
    114. }
    115. .cart-none {
    116. text-align: center;
    117. padding: 120px 0;
    118. background: #fff;
    119. p {
    120. color: #999;
    121. padding: 20px 0;
    122. }
    123. }
    124. .tc {
    125. text-align: center;
    126. a {
    127. color: $xtxColor;
    128. }
    129. .xtx-numbox {
    130. margin: 0 auto;
    131. width: 120px;
    132. }
    133. }
    134. .red {
    135. color: $priceColor;
    136. }
    137. .green {
    138. color: $xtxColor;
    139. }
    140. .f16 {
    141. font-size: 16px;
    142. }
    143. .goods {
    144. display: flex;
    145. align-items: center;
    146. img {
    147. width: 100px;
    148. height: 100px;
    149. }
    150. >div {
    151. width: 280px;
    152. font-size: 16px;
    153. padding-left: 10px;
    154. .attr {
    155. font-size: 14px;
    156. color: #999;
    157. }
    158. }
    159. }
    160. .action {
    161. display: flex;
    162. background: #fff;
    163. margin-top: 20px;
    164. height: 80px;
    165. align-items: center;
    166. font-size: 16px;
    167. justify-content: space-between;
    168. padding: 0 30px;
    169. .xtx-checkbox {
    170. color: #999;
    171. }
    172. .batch {
    173. a {
    174. margin-left: 20px;
    175. }
    176. }
    177. .red {
    178. font-size: 18px;
    179. margin-right: 20px;
    180. font-weight: bold;
    181. }
    182. }
    183. .tit {
    184. color: #666;
    185. font-size: 16px;
    186. font-weight: normal;
    187. line-height: 50px;
    188. }
    189. }
    190. </style>

    stores/car.js

    1. // 封装购物车模块
    2. import { defineStore } from 'pinia'
    3. import { computed, ref } from 'vue'
    4. import {insertCartAPI,findNewCartListAPI,delCartAPI} from '@/api/cart'
    5. import {useUserStore} from '@/stores/user'
    6. export const useCartStore = defineStore('cart', () => {
    7. const userStore = useUserStore()
    8. const isLogin = computed(()=>userStore.userInfo.token)
    9. // 1. 定义state - cartList
    10. const cartList = ref([])
    11. // 2. 定义action - addCart
    12. const addCart = async (goods) => {
    13. const {skuId,count} = goods
    14. if(isLogin.value) {
    15. //登录过后
    16. await insertCartAPI({skuId,count})
    17. const res = await findNewCartListAPI()
    18. cartList.value = res.result
    19. } else {
    20. // console.log('添加', goods)
    21. // 添加购物车操作
    22. // 已添加过 - count + 1
    23. // 没有添加过 - 直接push
    24. // 思路:通过匹配传递过来的商品对象中的skuId能不能在cartList中找到,找到了就是添加过
    25. const item = cartList.value.find((item) => goods.skuId === item.skuId)
    26. if (item) {
    27. // 找到了
    28. item.count++
    29. } else {
    30. // 没找到
    31. cartList.value.push(goods)
    32. }
    33. }
    34. }
    35. //更新
    36. const updateNewList = async () => {
    37. const res = await findNewCartListAPI()
    38. cartList.value = res.result
    39. }
    40. //删除购物车
    41. const delCart = async(skuId) => {
    42. if(isLogin.value) {
    43. await delCartAPI([skuId])
    44. const res = await findNewCartListAPI()
    45. cartList.value = res.result
    46. } else {
    47. const idx = cartList.value.findIndex((item)=>skuId === item.skuId)
    48. cartList.value.splice(idx,1)
    49. }
    50. }
    51. //总数
    52. const allCount = computed(()=>cartList.value.reduce((a,c)=>a+c.count,0))
    53. //总价
    54. const allPrice = computed(()=>cartList.value.reduce((a,c)=>a+c.price*c.count,0))
    55. // 单选功能
    56. const singleCheck = (skuId, selected) => {
    57. // 通过skuId找到要修改的那一项 然后把它的selected修改为传过来的selected
    58. const item = cartList.value.find((item) => item.skuId === skuId)
    59. item.selected = selected
    60. }
    61. //是否全选
    62. const isAll = computed(()=>cartList.value.every((item) => item.selected))
    63. //全选
    64. const allCheck = (selected) => {
    65. cartList.value.forEach(item => item.selected = selected)
    66. }
    67. //已选择的数量
    68. const selectCount = computed(()=>cartList.value.filter(item=>item.selected).reduce((a,c)=>a+c.count,0))
    69. //已选择价钱
    70. const selectPrice = computed(()=>cartList.value.filter(item=>item.selected).reduce((a,c)=>a+c.price*c.count,0))
    71. //清除购物车
    72. const clearCart = () => {
    73. cartList.value = []
    74. }
    75. return {
    76. cartList,
    77. addCart,
    78. delCart,
    79. allPrice,
    80. allCount,
    81. singleCheck,
    82. isAll,
    83. allCheck,
    84. selectCount,
    85. selectPrice,
    86. clearCart,
    87. updateNewList
    88. }
    89. }, {
    90. persist: true,
    91. })

    api/cart.js

    1. import httpInstance from '@/utils/http'
    2. // 加入购物车
    3. export const insertCartAPI = ({ skuId, count }) => {
    4. return httpInstance({
    5. url: '/member/cart',
    6. method: 'POST',
    7. data: {
    8. skuId,
    9. count
    10. }
    11. })
    12. }
    13. //登录购物车
    14. export const findNewCartListAPI = () => {
    15. return httpInstance({
    16. url: '/member/cart'
    17. })
    18. }
    19. // 删除购物车
    20. export const delCartAPI = (ids) => {
    21. return httpInstance({
    22. url: '/member/cart',
    23. method: 'DELETE',
    24. data: {
    25. ids
    26. }
    27. })
    28. }
    29. //合并购物车
    30. export const mergeCartAPI = (data) => {
    31. return httpInstance ({
    32. url: '/member/cart/merge',
    33. method: 'POST',
    34. data
    35. })
    36. }

    stores/user.js

    1. // 管理用户数据相关
    2. import { defineStore } from 'pinia'
    3. import { ref } from 'vue'
    4. import { loginAPI } from '@/api/user'
    5. import {useCartStore} from './car'
    6. import {mergeCartAPI} from '@/api/cart'
    7. export const useUserStore = defineStore('user', () => {
    8. const cartStore = useCartStore()
    9. // 1. 定义管理用户数据的state
    10. const userInfo = ref({})
    11. // 2. 定义获取接口数据的action函数
    12. const getUserInfo = async ({ account, password }) => {
    13. const res = await loginAPI({ account, password })
    14. userInfo.value = res.result
    15. mergeCartAPI(cartStore.cartList.map(item => {
    16. return {
    17. skuId: item.skuId,
    18. selected: item.selected,
    19. count: item.count
    20. }
    21. }))
    22. cartStore.updateNewList()
    23. }
    24. // 退出时清除用户信息
    25. const clearUserInfo = () => {
    26. userInfo.value = {}
    27. cartStore.clearCart()
    28. }
    29. // 3. 以对象的格式把state和action return
    30. return {
    31. userInfo,
    32. getUserInfo,
    33. clearUserInfo
    34. }
    35. }, {
    36. persist: true,
    37. })

    单选按钮

    全选按钮

    加入、删除购物车

    清空购物车

    合并本地购物车 

    订单页 

    路由

    1. {
    2. path: 'checkout',
    3. component: CheckOut
    4. }

     api/checkout.js

    1. import httpInstance from '@/utils/http'
    2. /**
    3. * 获取结算信息
    4. */
    5. export const getCheckoutInfoAPI = () => {
    6. return httpInstance({
    7. url:'/member/order/pre'
    8. })
    9. }
    10. // 创建订单
    11. export const createOrderAPI = (data) => {
    12. return httpInstance({
    13. url: '/member/order',
    14. method: 'POST',
    15. data
    16. })
    17. }

    views/pay/index.vue

    1. <script setup>
    2. import {getCheckoutInfoAPI,createOrderAPI} from '@/api/checkout'
    3. import {ref,onMounted} from 'vue'
    4. import {useRouter} from 'vue-router'
    5. import {useCartStore} from '@/stores/car'
    6. const cartStore = useCartStore()
    7. const router = useRouter()
    8. const checkInfo = ref({}) // 订单对象
    9. const curAddress = ref({})
    10. const getCheckInfo = async () => {
    11. const res = await getCheckoutInfoAPI()
    12. checkInfo.value = res.result
    13. const item = checkInfo.value.userAddresses.find(item=>item.isDefault === 0)
    14. curAddress.value = item
    15. }
    16. onMounted(()=>getCheckInfo())
    17. const showDialog = ref(false)
    18. const activeAddress = ref({})
    19. //切换地址
    20. const switchAddress = (item) => {
    21. switchAddress.value = item
    22. }
    23. const confirm = () => {
    24. curAddress.value = activeAddress.value
    25. showDialog.value = false
    26. activeAddress.value = {}
    27. }
    28. //创建订单
    29. const createOrder = async () => {
    30. const res = await createOrderAPI({
    31. deliveryTimeType: 1,
    32. payType: 1,
    33. payChannel: 1,
    34. buyerMessage: '',
    35. goods: checkInfo.value.goods.map(item => {
    36. return {
    37. skuId: item.skuId,
    38. count: item.count
    39. }
    40. }),
    41. addressId: curAddress.value.id
    42. })
    43. const orderId = res.result.id
    44. router.push({
    45. path: '/pay',
    46. query: {
    47. id: orderId
    48. }
    49. })
    50. //更新购物车
    51. cartStore.updateNewList()
    52. }
    53. </script>
    54. <template>
    55. <div class="xtx-pay-checkout-page">
    56. <div class="container">
    57. <div class="wrapper">
    58. <!-- 收货地址 -->
    59. <h3 class="box-title">收货地址</h3>
    60. <div class="box-body">
    61. <div class="address">
    62. <div class="text">
    63. <div class="none" v-if="!curAddress">您需要先添加收货地址才可提交订单。</div>
    64. <ul v-else>
    65. <li><span><i /><i />人:</span>{{ curAddress.receiver }}</li>
    66. <li><span>联系方式:</span>{{ curAddress.contact }}</li>
    67. <li><span>收货地址:</span>{{ curAddress.fullLocation }} {{ curAddress.address }}</li>
    68. </ul>
    69. </div>
    70. <div class="action">
    71. <el-button size="large" @click="showDialog = true">切换地址</el-button>
    72. <el-button size="large" @click="addFlag = true">添加地址</el-button>
    73. </div>
    74. </div>
    75. </div>
    76. <!-- 商品信息 -->
    77. <h3 class="box-title">商品信息</h3>
    78. <div class="box-body">
    79. <table class="goods">
    80. <thead>
    81. <tr>
    82. <th width="520">商品信息</th>
    83. <th width="170">单价</th>
    84. <th width="170">数量</th>
    85. <th width="170">小计</th>
    86. <th width="170">实付</th>
    87. </tr>
    88. </thead>
    89. <tbody>
    90. <tr v-for="i in checkInfo.goods" :key="i.id">
    91. <td>
    92. <a href="javascript:;" class="info">
    93. <img :src="i.picture" alt="">
    94. <div class="right">
    95. <p>{{ i.name }}</p>
    96. <p>{{ i.attrsText }}</p>
    97. </div>
    98. </a>
    99. </td>
    100. <td>&yen;{{ i.price }}</td>
    101. <td>{{ i.price }}</td>
    102. <td>&yen;{{ i.totalPrice }}</td>
    103. <td>&yen;{{ i.totalPayPrice }}</td>
    104. </tr>
    105. </tbody>
    106. </table>
    107. </div>
    108. <!-- 配送时间 -->
    109. <h3 class="box-title">配送时间</h3>
    110. <div class="box-body">
    111. <a class="my-btn active" href="javascript:;">不限送货时间:周一至周日</a>
    112. <a class="my-btn" href="javascript:;">工作日送货:周一至周五</a>
    113. <a class="my-btn" href="javascript:;">双休日、假日送货:周六至周日</a>
    114. </div>
    115. <!-- 支付方式 -->
    116. <h3 class="box-title">支付方式</h3>
    117. <div class="box-body">
    118. <a class="my-btn active" href="javascript:;">在线支付</a>
    119. <a class="my-btn" href="javascript:;">货到付款</a>
    120. <span style="color:#999">货到付款需付5元手续费</span>
    121. </div>
    122. <!-- 金额明细 -->
    123. <h3 class="box-title">金额明细</h3>
    124. <div class="box-body">
    125. <div class="total">
    126. <dl>
    127. <dt>商品件数:</dt>
    128. <dd>{{ checkInfo.summary?.goodsCount }}件</dd>
    129. </dl>
    130. <dl>
    131. <dt>商品总价:</dt>
    132. <dd>¥{{ checkInfo.summary?.totalPrice.toFixed(2) }}</dd>
    133. </dl>
    134. <dl>
    135. <dt><i></i>费:</dt>
    136. <dd>¥{{ checkInfo.summary?.postFee.toFixed(2) }}</dd>
    137. </dl>
    138. <dl>
    139. <dt>应付总额:</dt>
    140. <dd class="price">{{ checkInfo.summary?.totalPayPrice.toFixed(2) }}</dd>
    141. </dl>
    142. </div>
    143. </div>
    144. <!-- 提交订单 -->
    145. <div class="submit">
    146. <el-button type="primary" size="large" @click="createOrder">提交订单</el-button>
    147. </div>
    148. </div>
    149. </div>
    150. </div>
    151. <!-- 切换地址 -->
    152. <el-dialog v-model="showDialog" title="切换收货地址" width="30%" center>
    153. <div class="addressWrapper">
    154. <div class="text item" :class="{active:activeAddress.id===item.id}" @click="switchAddress(item)" v-for="item in checkInfo.userAddresses" :key="item.id">
    155. <ul>
    156. <li><span><i /><i />人:</span>{{ item.receiver }} </li>
    157. <li><span>联系方式:</span>{{ item.contact }}</li>
    158. <li><span>收货地址:</span>{{ item.fullLocation + item.address }}</li>
    159. </ul>
    160. </div>
    161. </div>
    162. <template #footer>
    163. <span class="dialog-footer">
    164. <el-button>取消</el-button>
    165. <el-button type="primary" @click="confirm">确定</el-button>
    166. </span>
    167. </template>
    168. </el-dialog>
    169. <!-- 添加地址 -->
    170. </template>
    171. <style scoped lang="scss">
    172. .xtx-pay-checkout-page {
    173. margin-top: 20px;
    174. .wrapper {
    175. background: #fff;
    176. padding: 0 20px;
    177. .box-title {
    178. font-size: 16px;
    179. font-weight: normal;
    180. padding-left: 10px;
    181. line-height: 70px;
    182. border-bottom: 1px solid #f5f5f5;
    183. }
    184. .box-body {
    185. padding: 20px 0;
    186. }
    187. }
    188. }
    189. .address {
    190. border: 1px solid #f5f5f5;
    191. display: flex;
    192. align-items: center;
    193. .text {
    194. flex: 1;
    195. min-height: 90px;
    196. display: flex;
    197. align-items: center;
    198. .none {
    199. line-height: 90px;
    200. color: #999;
    201. text-align: center;
    202. width: 100%;
    203. }
    204. >ul {
    205. flex: 1;
    206. padding: 20px;
    207. li {
    208. line-height: 30px;
    209. span {
    210. color: #999;
    211. margin-right: 5px;
    212. >i {
    213. width: 0.5em;
    214. display: inline-block;
    215. }
    216. }
    217. }
    218. }
    219. >a {
    220. color: $xtxColor;
    221. width: 160px;
    222. text-align: center;
    223. height: 90px;
    224. line-height: 90px;
    225. border-right: 1px solid #f5f5f5;
    226. }
    227. }
    228. .action {
    229. width: 420px;
    230. text-align: center;
    231. .btn {
    232. width: 140px;
    233. height: 46px;
    234. line-height: 44px;
    235. font-size: 14px;
    236. &:first-child {
    237. margin-right: 10px;
    238. }
    239. }
    240. }
    241. }
    242. .goods {
    243. width: 100%;
    244. border-collapse: collapse;
    245. border-spacing: 0;
    246. .info {
    247. display: flex;
    248. text-align: left;
    249. img {
    250. width: 70px;
    251. height: 70px;
    252. margin-right: 20px;
    253. }
    254. .right {
    255. line-height: 24px;
    256. p {
    257. &:last-child {
    258. color: #999;
    259. }
    260. }
    261. }
    262. }
    263. tr {
    264. th {
    265. background: #f5f5f5;
    266. font-weight: normal;
    267. }
    268. td,
    269. th {
    270. text-align: center;
    271. padding: 20px;
    272. border-bottom: 1px solid #f5f5f5;
    273. &:first-child {
    274. border-left: 1px solid #f5f5f5;
    275. }
    276. &:last-child {
    277. border-right: 1px solid #f5f5f5;
    278. }
    279. }
    280. }
    281. }
    282. .my-btn {
    283. width: 228px;
    284. height: 50px;
    285. border: 1px solid #e4e4e4;
    286. text-align: center;
    287. line-height: 48px;
    288. margin-right: 25px;
    289. color: #666666;
    290. display: inline-block;
    291. &.active,
    292. &:hover {
    293. border-color: $xtxColor;
    294. }
    295. }
    296. .total {
    297. dl {
    298. display: flex;
    299. justify-content: flex-end;
    300. line-height: 50px;
    301. dt {
    302. i {
    303. display: inline-block;
    304. width: 2em;
    305. }
    306. }
    307. dd {
    308. width: 240px;
    309. text-align: right;
    310. padding-right: 70px;
    311. &.price {
    312. font-size: 20px;
    313. color: $priceColor;
    314. }
    315. }
    316. }
    317. }
    318. .submit {
    319. text-align: right;
    320. padding: 60px;
    321. border-top: 1px solid #f5f5f5;
    322. }
    323. .addressWrapper {
    324. max-height: 500px;
    325. overflow-y: auto;
    326. }
    327. .text {
    328. flex: 1;
    329. min-height: 90px;
    330. display: flex;
    331. align-items: center;
    332. &.item {
    333. border: 1px solid #f5f5f5;
    334. margin-bottom: 10px;
    335. cursor: pointer;
    336. &.active,
    337. &:hover {
    338. border-color: $xtxColor;
    339. background: lighten($xtxColor, 50%);
    340. }
    341. >ul {
    342. padding: 10px;
    343. font-size: 14px;
    344. line-height: 30px;
    345. }
    346. }
    347. }
    348. </style>

    支付页 

    router.js

    1. {
    2. path: 'pay',
    3. component: Pay
    4. }

    api/pay.js

    1. import httpInstance from '@/utils/http'
    2. export const getOrderAPI = (id) => {
    3. return httpInstance({
    4. url: `/member/order/${id}`
    5. })
    6. }

    composables/useCountDown.js

    1. // 封装倒计时逻辑函数
    2. import { computed, onUnmounted, ref } from 'vue'
    3. import dayjs from 'dayjs'
    4. export const useCountDown = () => {
    5. // 1. 响应式的数据
    6. let timer = null
    7. const time = ref(0)
    8. // 格式化时间 为 xx分xx秒
    9. const formatTime = computed(() => dayjs.unix(time.value).format('mm分ss秒'))
    10. // 2. 开启倒计时的函数
    11. const start = (currentTime) => {
    12. // 开始倒计时的逻辑
    13. // 核心逻辑的编写:每隔1s就减一
    14. time.value = currentTime
    15. timer = setInterval(() => {
    16. time.value--
    17. }, 1000)
    18. }
    19. // 组件销毁时清除定时器
    20. onUnmounted(() => {
    21. timer && clearInterval(timer)
    22. })
    23. return {
    24. formatTime,
    25. start
    26. }
    27. }

    pay/index.vue

    1. <script setup>
    2. import { getOrderAPI } from '@/api/pay'
    3. import { onMounted, ref } from 'vue'
    4. import { useRoute } from 'vue-router'
    5. import {useCountDown} from '@/composables/useCountDown'
    6. const {formatTime,start} = useCountDown()
    7. // 获取订单数据
    8. const route = useRoute()
    9. const payInfo = ref({})
    10. const getPayInfo = async () => {
    11. const res = await getOrderAPI(route.query.id)
    12. payInfo.value = res.result
    13. start(res.result.countdown)
    14. }
    15. onMounted(() => getPayInfo())
    16. // 支付地址
    17. const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
    18. const backURL = 'http://127.0.0.1:5173/paycallback'
    19. const redirectUrl = encodeURIComponent(backURL)
    20. const payUrl = `${baseURL}pay/aliPay?orderId=${route.query.id}&redirect=${redirectUrl}`
    21. </script>
    22. <template>
    23. <div class="xtx-pay-page">
    24. <div class="container">
    25. <!-- 付款信息 -->
    26. <div class="pay-info">
    27. <span class="icon iconfont icon-queren2"></span>
    28. <div class="tip">
    29. <p>订单提交成功!请尽快完成支付。</p>
    30. <p>支付还剩 <span>{{ formatTime }}</span>, 超时后将取消订单</p>
    31. </div>
    32. <div class="amount">
    33. <span>应付总额:</span>
    34. <span>¥{{ payInfo.payMoney?.toFixed(2) }}</span>
    35. </div>
    36. </div>
    37. <!-- 付款方式 -->
    38. <div class="pay-type">
    39. <p class="head">选择以下支付方式付款</p>
    40. <div class="item">
    41. <p>支付平台</p>
    42. <a class="btn wx" href="javascript:;"></a>
    43. <a class="btn alipay" :href="payUrl"></a>
    44. </div>
    45. <div class="item">
    46. <p>支付方式</p>
    47. <a class="btn" href="javascript:;">招商银行</a>
    48. <a class="btn" href="javascript:;">工商银行</a>
    49. <a class="btn" href="javascript:;">建设银行</a>
    50. <a class="btn" href="javascript:;">农业银行</a>
    51. <a class="btn" href="javascript:;">交通银行</a>
    52. </div>
    53. </div>
    54. </div>
    55. </div>
    56. </template>
    57. <style scoped lang="scss">
    58. .xtx-pay-page {
    59. margin-top: 20px;
    60. }
    61. .pay-info {
    62. background: #fff;
    63. display: flex;
    64. align-items: center;
    65. height: 240px;
    66. padding: 0 80px;
    67. .icon {
    68. font-size: 80px;
    69. color: #1dc779;
    70. }
    71. .tip {
    72. padding-left: 10px;
    73. flex: 1;
    74. p {
    75. &:first-child {
    76. font-size: 20px;
    77. margin-bottom: 5px;
    78. }
    79. &:last-child {
    80. color: #999;
    81. font-size: 16px;
    82. }
    83. }
    84. }
    85. .amount {
    86. span {
    87. &:first-child {
    88. font-size: 16px;
    89. color: #999;
    90. }
    91. &:last-child {
    92. color: $priceColor;
    93. font-size: 20px;
    94. }
    95. }
    96. }
    97. }
    98. .pay-type {
    99. margin-top: 20px;
    100. background-color: #fff;
    101. padding-bottom: 70px;
    102. p {
    103. line-height: 70px;
    104. height: 70px;
    105. padding-left: 30px;
    106. font-size: 16px;
    107. &.head {
    108. border-bottom: 1px solid #f5f5f5;
    109. }
    110. }
    111. .btn {
    112. width: 150px;
    113. height: 50px;
    114. border: 1px solid #e4e4e4;
    115. text-align: center;
    116. line-height: 48px;
    117. margin-left: 30px;
    118. color: #666666;
    119. display: inline-block;
    120. &.active,
    121. &:hover {
    122. border-color: $xtxColor;
    123. }
    124. &.alipay {
    125. background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7b6b02396368c9314528c0bbd85a2e06.png) no-repeat center / contain;
    126. }
    127. &.wx {
    128. background: url(https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/c66f98cff8649bd5ba722c2e8067c6ca.jpg) no-repeat center / contain;
    129. }
    130. }
    131. }
    132. </style>

    账号

    askgxl8276@sandbox.com 

    登录密码

    111111

    支付密码

    111111

    会员中心 

    基本页面

    router.js

    1. import Member from '@/views/member/index.vue'
    2. import MemberInfo from '@/views/member/components/UserInfo.vue'
    3. import MemberOrder from '@/views/member/components/UserOrder.vue'
    4. {
    5. path: 'member',
    6. component: Member,
    7. children: [
    8. {
    9. path: 'user',
    10. component: MemberInfo
    11. },
    12. {
    13. path: 'order',
    14. component: MemberOrder
    15. },
    16. ]
    17. }

    LayoutNav.vue

    <li><a href="javascript:;" @click="$router.push('/member')">会员中心</a></li>

    UserInfo.vue

    1. <script setup>
    2. import { useUserStore } from '@/stores/user'
    3. const userStore = useUserStore()
    4. </script>
    5. <template>
    6. <div class="home-overview">
    7. <!-- 用户信息 -->
    8. <div class="user-meta">
    9. <div class="avatar">
    10. <img :src="userStore.userInfo?.avatar" />
    11. </div>
    12. <h4>{{ userStore.userInfo?.account }}</h4>
    13. </div>
    14. <div class="item">
    15. <a href="javascript:;">
    16. <span class="iconfont icon-hy"></span>
    17. <p>会员中心</p>
    18. </a>
    19. <a href="javascript:;">
    20. <span class="iconfont icon-aq"></span>
    21. <p>安全设置</p>
    22. </a>
    23. <a href="javascript:;">
    24. <span class="iconfont icon-dw"></span>
    25. <p>地址管理</p>
    26. </a>
    27. </div>
    28. </div>
    29. <div class="like-container">
    30. <div class="home-panel">
    31. <div class="header">
    32. <h4 data-v-bcb266e0="">猜你喜欢</h4>
    33. </div>
    34. <div class="goods-list">
    35. <!-- <GoodsItem v-for="good in likeList" :key="good.id" :goods="good" /> -->
    36. </div>
    37. </div>
    38. </div>
    39. </template>
    40. <style scoped lang="scss">
    41. .home-overview {
    42. height: 132px;
    43. background: url(@/assets/images/center-bg.png) no-repeat center / cover;
    44. display: flex;
    45. .user-meta {
    46. flex: 1;
    47. display: flex;
    48. align-items: center;
    49. .avatar {
    50. width: 85px;
    51. height: 85px;
    52. border-radius: 50%;
    53. overflow: hidden;
    54. margin-left: 60px;
    55. img {
    56. width: 100%;
    57. height: 100%;
    58. }
    59. }
    60. h4 {
    61. padding-left: 26px;
    62. font-size: 18px;
    63. font-weight: normal;
    64. color: white;
    65. }
    66. }
    67. .item {
    68. flex: 1;
    69. display: flex;
    70. align-items: center;
    71. justify-content: space-around;
    72. &:first-child {
    73. border-right: 1px solid #f4f4f4;
    74. }
    75. a {
    76. color: white;
    77. font-size: 16px;
    78. text-align: center;
    79. .iconfont {
    80. font-size: 32px;
    81. }
    82. p {
    83. line-height: 32px;
    84. }
    85. }
    86. }
    87. }
    88. .like-container {
    89. margin-top: 20px;
    90. border-radius: 4px;
    91. background-color: #fff;
    92. }
    93. .home-panel {
    94. background-color: #fff;
    95. padding: 0 20px;
    96. margin-top: 20px;
    97. height: 400px;
    98. .header {
    99. height: 66px;
    100. border-bottom: 1px solid #f5f5f5;
    101. padding: 18px 0;
    102. display: flex;
    103. justify-content: space-between;
    104. align-items: baseline;
    105. h4 {
    106. font-size: 22px;
    107. font-weight: 400;
    108. }
    109. }
    110. .goods-list {
    111. display: flex;
    112. justify-content: space-around;
    113. }
    114. }
    115. </style>

    UserOrder.vue

    1. <script setup>
    2. // tab列表
    3. const tabTypes = [
    4. { name: "all", label: "全部订单" },
    5. { name: "unpay", label: "待付款" },
    6. { name: "deliver", label: "待发货" },
    7. { name: "receive", label: "待收货" },
    8. { name: "comment", label: "待评价" },
    9. { name: "complete", label: "已完成" },
    10. { name: "cancel", label: "已取消" }
    11. ]
    12. // 订单列表
    13. const orderList = []
    14. </script>
    15. <template>
    16. <div class="order-container">
    17. <el-tabs>
    18. <!-- tab切换 -->
    19. <el-tab-pane v-for="item in tabTypes" :key="item.name" :label="item.label" />
    20. <div class="main-container">
    21. <div class="holder-container" v-if="orderList.length === 0">
    22. <el-empty description="暂无订单数据" />
    23. </div>
    24. <div v-else>
    25. <!-- 订单列表 -->
    26. <div class="order-item" v-for="order in orderList" :key="order.id">
    27. <div class="head">
    28. <span>下单时间:{{ order.createTime }}</span>
    29. <span>订单编号:{{ order.id }}</span>
    30. <!-- 未付款,倒计时时间还有 -->
    31. <span class="down-time" v-if="order.orderState === 1">
    32. <i class="iconfont icon-down-time"></i>
    33. <b>付款截止: {{order.countdown}}</b>
    34. </span>
    35. </div>
    36. <div class="body">
    37. <div class="column goods">
    38. <ul>
    39. <li v-for="item in order.skus" :key="item.id">
    40. <a class="image" href="javascript:;">
    41. <img :src="item.image" alt="" />
    42. </a>
    43. <div class="info">
    44. <p class="name ellipsis-2">
    45. {{ item.name }}
    46. </p>
    47. <p class="attr ellipsis">
    48. <span>{{ item.attrsText }}</span>
    49. </p>
    50. </div>
    51. <div class="price">¥{{ item.realPay?.toFixed(2) }}</div>
    52. <div class="count">x{{ item.quantity }}</div>
    53. </li>
    54. </ul>
    55. </div>
    56. <div class="column state">
    57. <p>{{ order.orderState }}</p>
    58. <p v-if="order.orderState === 3">
    59. <a href="javascript:;" class="green">查看物流</a>
    60. </p>
    61. <p v-if="order.orderState === 4">
    62. <a href="javascript:;" class="green">评价商品</a>
    63. </p>
    64. <p v-if="order.orderState === 5">
    65. <a href="javascript:;" class="green">查看评价</a>
    66. </p>
    67. </div>
    68. <div class="column amount">
    69. <p class="red">¥{{ order.payMoney?.toFixed(2) }}</p>
    70. <p>(含运费:¥{{ order.postFee?.toFixed(2) }})</p>
    71. <p>在线支付</p>
    72. </div>
    73. <div class="column action">
    74. <el-button v-if="order.orderState === 1" type="primary"
    75. size="small">
    76. 立即付款
    77. </el-button>
    78. <el-button v-if="order.orderState === 3" type="primary" size="small">
    79. 确认收货
    80. </el-button>
    81. <p><a href="javascript:;">查看详情</a></p>
    82. <p v-if="[2, 3, 4, 5].includes(order.orderState)">
    83. <a href="javascript:;">再次购买</a>
    84. </p>
    85. <p v-if="[4, 5].includes(order.orderState)">
    86. <a href="javascript:;">申请售后</a>
    87. </p>
    88. <p v-if="order.orderState === 1"><a href="javascript:;">取消订单</a></p>
    89. </div>
    90. </div>
    91. </div>
    92. <!-- 分页 -->
    93. <div class="pagination-container">
    94. <el-pagination background layout="prev, pager, next" />
    95. </div>
    96. </div>
    97. </div>
    98. </el-tabs>
    99. </div>
    100. </template>
    101. <style scoped lang="scss">
    102. .order-container {
    103. padding: 10px 20px;
    104. .pagination-container {
    105. display: flex;
    106. justify-content: center;
    107. }
    108. .main-container {
    109. min-height: 500px;
    110. .holder-container {
    111. min-height: 500px;
    112. display: flex;
    113. justify-content: center;
    114. align-items: center;
    115. }
    116. }
    117. }
    118. .order-item {
    119. margin-bottom: 20px;
    120. border: 1px solid #f5f5f5;
    121. .head {
    122. height: 50px;
    123. line-height: 50px;
    124. background: #f5f5f5;
    125. padding: 0 20px;
    126. overflow: hidden;
    127. span {
    128. margin-right: 20px;
    129. &.down-time {
    130. margin-right: 0;
    131. float: right;
    132. i {
    133. vertical-align: middle;
    134. margin-right: 3px;
    135. }
    136. b {
    137. vertical-align: middle;
    138. font-weight: normal;
    139. }
    140. }
    141. }
    142. .del {
    143. margin-right: 0;
    144. float: right;
    145. color: #999;
    146. }
    147. }
    148. .body {
    149. display: flex;
    150. align-items: stretch;
    151. .column {
    152. border-left: 1px solid #f5f5f5;
    153. text-align: center;
    154. padding: 20px;
    155. >p {
    156. padding-top: 10px;
    157. }
    158. &:first-child {
    159. border-left: none;
    160. }
    161. &.goods {
    162. flex: 1;
    163. padding: 0;
    164. align-self: center;
    165. ul {
    166. li {
    167. border-bottom: 1px solid #f5f5f5;
    168. padding: 10px;
    169. display: flex;
    170. &:last-child {
    171. border-bottom: none;
    172. }
    173. .image {
    174. width: 70px;
    175. height: 70px;
    176. border: 1px solid #f5f5f5;
    177. }
    178. .info {
    179. width: 220px;
    180. text-align: left;
    181. padding: 0 10px;
    182. p {
    183. margin-bottom: 5px;
    184. &.name {
    185. height: 38px;
    186. }
    187. &.attr {
    188. color: #999;
    189. font-size: 12px;
    190. span {
    191. margin-right: 5px;
    192. }
    193. }
    194. }
    195. }
    196. .price {
    197. width: 100px;
    198. }
    199. .count {
    200. width: 80px;
    201. }
    202. }
    203. }
    204. }
    205. &.state {
    206. width: 120px;
    207. .green {
    208. color: $xtxColor;
    209. }
    210. }
    211. &.amount {
    212. width: 200px;
    213. .red {
    214. color: $priceColor;
    215. }
    216. }
    217. &.action {
    218. width: 140px;
    219. a {
    220. display: block;
    221. &:hover {
    222. color: $xtxColor;
    223. }
    224. }
    225. }
    226. }
    227. }
    228. }
    229. </style>

     

    个人中心信息渲染

    api/user.js

    1. export const getLikeListAPI = ({ limit = 4 }) => {
    2. return httpInstance({
    3. url:'/goods/relevant',
    4. params: {
    5. limit
    6. }
    7. })
    8. }

    UserInfo.vue

    1. <script setup>
    2. import { useUserStore } from '@/stores/user'
    3. import {getLikeListAPI} from '@/api/user'
    4. import { onMounted, ref } from 'vue'
    5. // 导入GoodsItem组件
    6. import GoodsItem from '@/views/home/components/GoodsItem.vue'
    7. const userStore = useUserStore()
    8. // 获取猜你喜欢列表
    9. const likeList = ref([])
    10. const getLikeList = async () => {
    11. const res = await getLikeListAPI({ limit: 4 })
    12. likeList.value = res.result
    13. }
    14. onMounted(() => getLikeList())
    15. </script>

    我的订单 

    api/user.js 

    1. export const getUserOrder = (params) => {
    2. return httpInstance({
    3. url:'/member/order',
    4. method:'GET',
    5. params
    6. })
    7. }

    UserOrder.vue

    1. <script setup>
    2. import { getUserOrder } from '@/api/user'
    3. import { onMounted, ref } from 'vue'
    4. // tab列表
    5. const tabTypes = [
    6. { name: "all", label: "全部订单" },
    7. { name: "unpay", label: "待付款" },
    8. { name: "deliver", label: "待发货" },
    9. { name: "receive", label: "待收货" },
    10. { name: "comment", label: "待评价" },
    11. { name: "complete", label: "已完成" },
    12. { name: "cancel", label: "已取消" }
    13. ]
    14. // tab切换
    15. const tabChange = (type) => {
    16. params.value.orderState = type
    17. getOrderList()
    18. }
    19. // 获取订单列表
    20. const orderList = ref([])
    21. const params = ref({
    22. orderState: 0,
    23. page: 1,
    24. pageSize: 2
    25. })
    26. const getOrderList = async () => {
    27. const res = await getUserOrder(params.value)
    28. orderList.value = res.result.items
    29. total.value = res.result.counts
    30. }
    31. onMounted(() => getOrderList())
    32. </script>
    <el-tabs @tab-change="tabChange">

    分页

    UserOrder.vue

    1. <!-- 分页 -->
    2. <div class="pagination-container">
    3. <el-pagination :total="total" @current-change="pageChange" :page-size="params.pageSize" background layout="prev, pager, next" />
    4. </div>
    1. // 页数切换
    2. const pageChange = (page) => {
    3. params.value.page = page
    4. getOrderList()
    5. }

  • 相关阅读:
    猿创征文|【HTML】标签学习之路
    POSIX信号量
    QT+OSG/osgEarth编译之四十:blend2d+Qt编译(一套代码、一套框架,跨平台编译,版本:blend2d-beta18)
    RHCSA之Linux基础
    html+css实现容器显示两行文本超出部分以省略号显示
    computer architecture simulator汇总
    [附源码]SSM计算机毕业设计疫情防控期间人员档案追寻系统JAVA
    Redis持久化
    Java贪心算法
    C++ 基础知识 ∈ C++ 编程笔记
  • 原文地址:https://blog.csdn.net/qq_63806300/article/details/132828206