文章目录
一、项目创建准备工作
src文件夹下建立api发送请求
文件夹和utils常用设置
文件夹
1、配置vite和ts默认项
1、删除一些vite创建时的样式和页面
- views中只保留HomeView.vue页面,删除一些引入
- components删除里面所有的页面
- 删除App.vue中的样式和一些引入,保留如下
<script setup lang="ts"> import { RouterView } from 'vue-router' </script> <template> <RouterView /> </template>
2、vite.config配置默认项
export default defineConfig({
plugins: [vue()],
resolve: {
// 重定向
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@a': fileURLToPath(new URL('./src/api', import.meta.url)),
'@c': fileURLToPath(new URL('./src/components', import.meta.url)),
'@r': fileURLToPath(new URL('./src/router', import.meta.url)),
'@s': fileURLToPath(new URL('./src/stores', import.meta.url)),
'@u': fileURLToPath(new URL('./src/utils', import.meta.url)),
'@v': fileURLToPath(new URL('./src/views', import.meta.url))
},
},
// 服务器配置
server: {
host:'0.0.0.0',
port: 8080,
open: true
}
})
3、tsconfig文件夹中进行配置重定向
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@a/*": ["./src/api/*"],
"@c/*": ["./src/components/*"],
"@r/*": ["./src/router/*"],
"@s/*": ["./src/stores/*"],
"@u/*": ["./src/utils/*"],
"@v/*": ["./src/views/*"],
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}
2、配置路由命名
vite中没有自带的命名,如果不使用命名路由,views文件夹中后期需要定义文件夹为路由名称,里面的index.vue文件才是最终的文件名,在浏览器中会直接输入index的命名,所以需要给其加上一个重命名
1、给setup语法糖定义组件name属性,有很多种方案,我这里直接用下面这种,安装插件
npm i vite-plugin-vue-setup-extend -D
2、vite.config.ts中引入和使用
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// 引入setup语法糖命名安装的插件
import vueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
// 1、plugins中使用刚引入的命名插件函数
plugins: [vue(), vueSetupExtend()]
})
注意配置完一次就要重新运行一下,不然待会后面的效果是没有的
3、在views中HomeView路由组件中的setup中直接定义命名属性
<script setup lang="ts" name="RenameView">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>
4、打开浏览器切换vue可以看到名字已经改成我们需要的RenameView了
3、配置环境源env
1、src文件夹下建立两个文件分别是.env.development和.env.production
2、development中写的是开发时的环境,production中写的是生产环境源
VITE_BASE_URL=“/api”
vite中环境源以VITE_开头
4、二次封装axios
1、安装axios
npm i axios -S
2、utils文件夹中创建request.ts文件
3、引入axios,定义baseURL和过期时间,添加拦截器
参考axios中的官方文档使用
// 引入axios
import axios from "axios";
// 定义baseURL和过期时间
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL;
axios.defaults.timeout = 8000;
// 添加拦截器interceptor
axios.interceptors.request.use(
function (config) {
// 处理token
return config;
},
function (error) {
return Promise.reject(error);
}
);
axios.interceptors.response.use(
function (response) {
// 处理登录状态问题
return response;
},
function (error) {
return Promise.reject(error);
}
);
// 导出axios使用
export default axios
5、配置反向代理
1、找到vite.config中服务器server配置反向代理,我这里使用的是fastmock接口,实际写业务接口
2、更改目标源,注意这里一般路径后自带api,需要手动删除,重写api
// 服务器配置
server: {
host:'0.0.0.0',
port: 8080,
open: true,
// 配置反向代理
proxy: {
'/api': {
// 请求的源(注意有些自带api,需要手动删除,不然路径会有两个api)
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
// 重写api
rewrite: (path) => path.replace(/^\/api/, '/api')
}
}
}
6、测试请求接口
1、在api文件夹下建立index.ts文件夹
2、引入二次封装后的axios,写一个测试请求,导出该请求方法
// 引入二次封装后的axios
import axios from "@u/request";
// 请求分类
const fetchCates = (params = {}) => {
// 第一个参数就是请求的接口
return axios.get('/shop/goods/category/all', {params})
}
// 导出接口
export {
fetchCates
}
3、回HomeView测试请求
<template>
<main>
</main>
</template>
<script setup lang="ts" name="RenameView">
// 引入ref和onMounted
import { ref, onMounted } from 'vue'
// 引入api中请求分类的函数
import { fetchCates } from '@/api';
// 重新定义一个函数用于触发引入的请求函数
const getCates = () => {
fetchCates().then(res => {
console.log(res)
})
}
// 在onMouted中触发一下请求
onMounted(() => {
getCates()
})
</script>
4、ok!数据就可以请求到啦!
7、配置rem,配置移动端
1、安装lib-flexible
npm i lib-flexible -S
2、安装后在main.ts中引入即可
// 引入lib-flexible,配置rem
import 'lib-flexible'
2、找到assets文件夹中的base.css文件,加上reset重置一些默认样式的代码
* {
padding: 0;
margin: 0;
font: inherit;
vertical-align: baseline;
}
* {
/* iOS出现一个半透明的灰色背景 */
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
html {
-webkit-text-size-adjust: 100%; /* 禁止字体变化 */
}
body {
font-size: 14px;
font-weight: 400;
font-family: Helvetica,Arial,sans-serif;
line-height: 1;
-webkit-overflow-scrolling: touch; /* 设置滚动容器的滚动效果 */
-webkit-font-smoothing: antialiased; /* 字体抗锯齿渲染 */
}
a, a:active, a:hover {
/* 某些浏览器会给 a 设置默认颜色 */
color: unset;
text-decoration: none;
}
ol, ul, li {
/* 去掉列表样式 */
list-style: none;
}
img {
border: 0;
vertical-align: middle
}
table {
/* 去掉 td 与 td 之间的空隙 */
border-collapse: collapse;
border-spacing: 0;
}
input, textarea, select {
outline: none; /* 去掉fouce时边框高亮效果 */
background: unset; /* 去掉默认背景 */
appearance: none;
-webkit-appearance: none; /* 去除ios输入框阴影 */
}
/* 禁止选中文本内容 */
*:not(input, select, textArea) {
-webkit-user-select: none;
}
4、进入main.css删除vite创建时自带的css效果,只保留引入base.css
5、进入项目文件夹中的index.html中meta视口标签上加上禁止缩放的代码
<meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no" >
6、安装postcss-px,自动将px转换成rem单位,设计图是多少px直接写多少px,该插件会自动转换单位
npm install postcss-pxtorem --save-dev
7、在vite中配置postcss-px
引入postcss-px
// 引入postcss-px:px自动转rem的插件
import postCssPxToRem from "postcss-pxtorem"
并在下面的导出文件中定义css转换的一些数据
// 定义css转换的一些数据
export default defineConfig({
// 1、plugins中使用刚引入的命名插件函数
plugins: [vue(),vueSetupExtend()],
// 定义css转换的一些数据
css: {
postcss: {
plugins: [
postCssPxToRem({
rootValue: 40, // 根据设计图 计算1rem单位 设计图十分之一
propList: ['*'], // 需要转换的属性,这里选择全部都进行转换 单位使用PX默认不转换rem
})
]
}
},
})
8、此时postcss-pxtorem会飘红
因为在使用 Typescript 的过程中, 第三方类库并没有ts的.d.ts 类型的声明文件,所以无法在目前的项目中正常使用。如果要使用这些库,添加声明文件
- 解决方案:
- 直接在env.d.ts文件中写如下内容:
declare module 'postcss-pxtorem'
- 直接在env.d.ts文件中写如下内容:
9、安装scss
npm i scss -D
10、进入HomeView.vue中测试,定义一个盒子宽高,运行项目,看看px是否转成rem了
8、配置vant以及按需引入
1、安装vant
npm i vant
2、安装按需引入组件样式
npm i unplugin-vue-components -D
3、配置按需引入插件,在vite.config中引入,在plugins中配置
import Components from 'unplugin-vue-components/vite';
import { VantResolver } from 'unplugin-vue-components/resolvers';
export default {
plugins: [
vue(),
Components({
resolvers: [VantResolver()],
}),
],
};
4、进入src下的main.ts文件,引入想要使用的组件样式并注册
// 1. 引入你需要的组件
import { Button } from 'vant';
// 2. 引入组件样式,这里使用的按需引入的组件,不需要单独引入样式了
// import 'vant/lib/index.css';
const app = createApp(App)
// 3. 注册你需要的组件
app.use(Button);
5、进入HomeView测试下按钮是否成功,template中加入,运行项目会发现成功了
<van-button type="primary" size="mini">主要按钮</van-button>
二、项目正式开始
1、创建Tabbar底部标签栏
1、删除HomeView测试文件,创建四个底部标签栏文件夹作为四个页面,并建立index.vue文件,引入基本结构,定义命名
<template>
<div>
购物车
</div>
</template>
<script setup lang="ts" name="CartPage"> //这里name对应文件夹名字
</script>
<style scoped>
</style>
由于首页,分类,购物车,个人中心点击进入后都有Tabbar,为了代码简洁,这里将Tabbar单独放到components中,在每个里面引入Tabbar即可
2、进入main.ts文件按需引入Tabbar并注册
import { createApp } from 'vue';
import { Tabbar, TabbarItem } from 'vant';
const app = createApp();
app.use(Tabbar);
app.use(TabbarItem);
3、在src下components文件夹建立Tabbar.vue文件,vtss快速创建ts结构,在template中放入官网给的Tbbar代码注意删除v-mode,这里不需要通过v-model变换激活,后面会使用router路由跳转进行激活
<van-tabbar>
<van-tabbar-item icon="home-o">标签</van-tabbar-item>
<van-tabbar-item icon="search">标签</van-tabbar-item>
<van-tabbar-item icon="friends-o">标签</van-tabbar-item>
<van-tabbar-item icon="setting-o">标签</van-tabbar-item>
</van-tabbar>
4、进入刚在views中创建的四个文件夹中的index.vue文件下引入和使用Tabbar,四个都有Tabbar,所以都需要引入和使用子组件
<template>
<div>
首页
<Tabbar></Tabbar>
</div>
</template>
<script setup lang="ts" name="HomePage">
// 引入Tabbar子组件,在template中使用,这里无需注册
import Tabbar from '@/components/Tabbar.vue';
</script>
<style scoped>
</style>
5、此时无法显示,需要配置路由,进入src下的router文件夹的index.ts文件,删除内部的引入HomeView文件和routes数组中的对象,待会需要自己重新定义
import { createRouter, createWebHistory } from 'vue-router'
// 引入主页文件
import HomePage from '@v/HomePage/index.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
// 路由重定向
path:'/',
redirect:'/home'
},
// 主页路由
{
path: '/home',
name: 'home',
component: HomePage
},
// 分类页路由
{
path: '/cate',
name: 'cate',
component: () => import('@v/CatePage/index.vue')
},
// 购物车页路由
{
path: '/cart',
name: 'cart',
component: () => import('@v/CartPage/index.vue')
},
// 个人中心页路由
{
path: '/user',
name: 'user',
component: () => import('@v/UserPage/index.vue')
},
]
})
export default router
6、运行项目,会发现底部标签栏已经渲染完成,但是无法实现路由点击跳转
7、回到Tabbar文件,在van-tabbar标签加上route属性,实现点击根据路由跳转,文档中默认route为false,这里开启路由跳转,并在van-tabbar-item中定义to属性实现要跳转的路径即可实现点击跳转对应的页面。
<template>
<div>
<van-tabbar route >
<van-tabbar-item to="/home" icon="home-o">标签</van-tabbar-item>
<van-tabbar-item to="/cart" icon="search">标签</van-tabbar-item>
<van-tabbar-item to="/cate" icon="friends-o">标签</van-tabbar-item>
<van-tabbar-item to="/user" icon="setting-o">标签</van-tabbar-item>
</van-tabbar>
</div>
</template>
2、配置icon图标
1、提前注册好iconfont账号,进入iconfont网站找到自己想要的图标加入购物车,创建项目单独管理一个完整项目需要的图标,点击资源管理,后我的项目,切换到font-class,下载至本地
2、下载好的文件解压,只需要部分,为了减少包体积,里面有些样式是demo的样式,项目中用不到选中文字就是需要的部分
,在assets文件夹下建立fonts文件,粘贴过来,并删除里面的logo.svg,后面logo根据项目更换
3、进入main.ts引入icon
// 引入icon图标
import './assets/fonts/iconfont.css'
4、iconfont结合vant中的icon标签实现图标为自己需要的图标
- 进入main.ts文件中按需引入icon并注册
import { createApp } from 'vue'; import { Icon } from 'vant'; const app = createApp(); app.use(Icon);
- 进入Tabbar引入main.ts中的icon并使用,文档中有相应属性可以更改对应的图标,值为icon
在之前使用icon图标时需要在标签中定义两个class名字,iconfont 和icon-名字,这里使用了图标前缀更改为icon,对应icon的css文件中也要进行更改
<template> <div> <van-tabbar route active-color="#f53f3f"> <van-tabbar-item to="/home" icon="jingdong1" icon-prefix="icon">首页</van-tabbar-item> <van-tabbar-item to="/cart" icon="fenlei2" icon-prefix="icon">分类</van-tabbar-item> <van-tabbar-item to="/cate" icon="gouwuche" icon-prefix="icon">购物车</van-tabbar-item> <van-tabbar-item to="/user" icon="yonghu" icon-prefix="icon">未登录</van-tabbar-item> </van-tabbar> </div> </template>
成功更改!
3、首页头部
1、头部搜索框,按需引入Search搜索和注册
import { createApp } from 'vue';
import { Search } from 'vant';
const app = createApp();
app.use(Search);
2、添加样式和结构
<template>
<div style="height:3000px">
<header>
<van-icon name="fenlei3" class-prefix="icon" />
<van-search
v-model="value"
placeholder="请输入搜索关键词"
shape="round"
background="#e1251b"
/>
<span>登录</span>
</header>
<Tabbar></Tabbar>
</div>
</template>
<style scoped lang="scss">
header{
width: 100%;
display: flex;
position: fixed;
align-items: center;
// background-color: #e1251b;
background-color: transparent;
justify-content: space-between;
padding: 0 15px;
font-size: 16px;
color: #fff;
z-index: 1000;
:deep .van-search{
padding: 10px 0;
width: 75%;
}
:deep .icon-fenlei3{
font-size: 25px;
}
}
</style>
3、实现鼠标滚动后头部自带颜色,需要用到自定义指令来实现一个动作或者效果,在< script setup >中任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令,在标签中使用即可,我这里主要是实现滚动条一旦开始滚动后本来透明的颜色变成固定颜色,所以自定义好的指令直接给头绑定即可
// 自定义指令实现滚动后变色
const vScrollDirective = {
mounted(el) {
window.onscroll = function () {
const sTop = document.documentElement.scrollTop || this.document.body.scrollTop;
if(sTop>40){
// el.style.position = 'fixed'
el.style.backgroundColor = '#e43130'
}else{
// el.style.position = 'static'
el.style.backgroundColor = 'transparent'
}
}
},
}
<header v-scroll-directive>
4、实现点击输入框内容部分跳转到搜索页,
- 首先创建一个SearchPage的文件夹,作为跳转的搜索页,还是vtss创建基本结构,定义name
<template> <div> 搜索 </div> </template> <script setup lang="ts" name="SearchPage"> </script> <style scoped> </style>
- 创建路由,进入router中引入SearchPage,定义跳转的路由路径和地址等
// 引入搜索页面 import SearchPage from '@v/SearchPage/index.vue' routes: [ // 搜索页面 { path: '/search', name: 'hosearchme', component: SearchPage }, ]
- 回到HomePage中头部定义搜索部分点击跳转,van-search标签中定义属性点击搜索部分跳转
@click-input=“toSearchPage”
- 参考文件
- 引入一个useRouter函数,该函数实例上有push方法,调用方法实现路由调转,也可以实现传参
<script setup lang="ts" name="HomePage"> import { ref,onMounted } from 'vue'; // 引入Tabbar子组件,在template中使用,这里无需注册 import Tabbar from '@/components/Tabbar.vue'; // 引入一个useRouter 函数 import { useRouter } from "vue-router" const value = ref(''); // useRouter 函数会返回一个router对象 这是一个全局路由对象 里面会包含很多方法 let router = useRouter() console.log(useRouter) // 点击搜索框跳转搜索页 const toSearchPage = () => { router.push("/search"); } </script>
4、首页轮播
1、main.js按需引入Swipe
import { createApp } from 'vue';
import { Swipe, SwipeItem } from 'vant';
const app = createApp();
app.use(Swipe);
app.use(SwipeItem);
2、找到vant中轮播代码,放到页面主体当中
<!-- 页面主体 -->
<section>
<!-- 轮播开始 -->
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item>
<img src="" alt="">
</van-swipe-item>
</van-swipe>
<!-- 轮播结束 -->
</section>
3、进入api文件夹中的index中创建请求
// 请求首页轮播图
const fetchHomeBanner = (params = {}) => axios.get('/banner/list', {params})
4、回到HomePage中引入axios请求
// 引入axios请求
import {fetchHomeBanner } from '@a/index'
5、处理轮播图数据,在实例创建立马请求轮播图数据
// 轮播图数据
const banners = ref<{id:number, picUrl:string}[]>([])
const getHomeBanner = () => {
fetchHomeBanner().then((res)=>{
console.log(res)
if(res.data.code===0) {
banners.value = res.data.data
}
})
}
// 实例创建时就触发的函数
onMounted(() => {
getHomeBanner()
})
我这里请求的数据成功后的code码为0,根据项目实际情况而定,上面的id和picUrl也是一样
6、请求成功之后将data数组循环遍历,每一项对应的数据绑定到对应的标签当中
<!-- 轮播开始 -->
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item v-for="banner in banners" :key="banner.id">
<img :src="banner.picUrl" alt="">
</van-swipe-item>
</van-swipe>
<!-- 轮播结束 -->
7、此时数据已经请求到了,css中更改一下img的大小和其他样式即可
// 页面主体样式
section {
.home-wrap{
// width: 100%;
padding-top: 54px;
box-sizing: border-box;
.slider-bg{
background-image: -webkit-gradient(linear,left bottom,left top,from(#f1503b),color-stop(50%,#c82519));
background-image: -webkit-linear-gradient(bottom,#f1503b,#c82519 50%);
background-image: linear-gradient(0deg,#f1503b,#c82519 50%);
position: absolute;
top: 0;
left: -25%;
height: 3.5rem;
width: 150%;
border-bottom-left-radius: 100%;
border-bottom-right-radius: 100%;
}
.my-swipe {
position: absolute;
width: 92%;
height: 140px;
border-radius: 8px;
left: 4%;
.van-swipe__track{
img {
width: 100%;
height: 100%;
}
}
}
}
}
5、首页宫格导航
1、找到vant组件库当中的宫格导航部分,按需引入宫格导航需要的内容
vant-宫格导航链接
import { createApp } from 'vue';
import { Grid, GridItem } from 'vant';
const app = createApp();
app.use(Grid);
app.use(GridItem);
2、在api的index中封装请求分类的函数
// 请求分类
const fetchCates = (params = {}) => {
// 第一个参数就是请求的接口
return axios.get('/shop/goods/category/all', {params})
}
3、回到HomePage中的index,引入刚封装好的请求
// 引入axios请求
import {fetchHomeBanner,fetchCates } from '@a/index'
4、实例创建完成即可请求宫格导航的数据
// 宫格导航数据
const cates = ref<{id:number,name:string,icon:string}[]>([])
const getCates = () => {
fetchCates().then(res => {
if(res.data.code === 0) {
cates.value = res.data.data
}
})
}
// 实例创建时就触发的函数
onMounted(() => {
getHomeBanner(),
getCates()
})
5、回到html中将请求到的数据回显到宫格导航栏中,v-for循环刚请求到的数据,将每一项显示出来即可,一行显示5个,具体可以参考文档
<!-- 宫格导航开始 -->
<van-grid :column-num="5" icon-size="40px">
<van-grid-item
v-for="cate in cates"
:key="cate.id"
:icon="cate.icon"
:text="cate.name" />
</van-grid>
<!-- 宫格导航结束 -->
section {
// 宫格导航样式
:deep .van-grid{
background-color: #f6f6f6 !important;
.van-grid-item:last-child{
display: none;
}
.van-grid-item__text{
color: #666;
font-size: 12px;
}
}
}
:deep .van-grid-item__content {
background-color: #f6f6f6;
}
6、首页秒杀页面
1、在HomePage文件夹下建立components文件夹,再在里面创建一个Seckill.vue文件注意首字母一定要大写
,进入index.vue中引入刚创建的Seckill文件,并在template中使用
// 引入秒杀页
import Seckill from './components/Seckill.vue'
2、回到Seckill中写入具体样式和基本结构
<template>
<div class="home-floor">
<div class="floor-content">
<div class="floor-top">
<span>京东秒杀</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
// 秒杀栏样式
.home-floor{
padding: 10px;
width: 100%;
height: 155px;
:deep .floor-content{
border-radius: 10px;
background-color: #fff;
width: 100%;
height: 100%;
padding: 10px;
box-sizing: border-box;
.floor-top{
span{
color: #000;
font-size: 14px;
font-weight: 600;
}
}
}
}
</style>
7、商品列表
1、1、在HomePage的components文件夹里面创建一个Items.vue文件注意首字母一定要大写
,进入index.vue中引入刚创建的Items文件,并在template中使用`
// 引入商品列表页面
import Items from './components/Items.vue'
2、回到Items中写入具体样式和基本结构,样式渲染成自己想要的效果之后开始请求数据了
<template>
<div class="items">
<div class="item" >
<img src="../../../assets/imgs/1.jpeg" alt="">
<div class="item-content">
<p>打撒罚款设计费撒酒疯考拉</p>
<div>¥<span>2599</span>.00</div>
<ul>
<li>5000+评论</li>
<li>
<van-icon name="cart-o" color="#1989fa" size="20"/>
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
.items{
display: flex;
flex-wrap: wrap;
justify-content: space-around;
.item{
width: 46%;
border-radius: 10px;
background-color: #fff;
img{
width: 100%;
border-radius: 10px 10px 0 0 ;
}
.item-content{
padding:5px 10px 10px 10px;
box-sizing: border-box;
p{
color: #434343;
font-size: 14px;
line-height: 20px;
}
div{
color: #ff4142;
font-size: 14px;
margin-top: 5px;
position: relative;
left: -2px;
span{
font-size: 20px;
}
}
ul{
display: flex;
justify-content: space-between;
align-items: flex-end;
li:first-child{
font-size: 12px;
color: #999;
}
}
}
}
}
</style>
3、进入api的index文件定义一个数据请求的axios,并导出
// 请求商品数据
const fetchItems = (params = {}) => axios.post('shop/goods/list/v2',params)
// 导出接口
export {
fetchItems
}
4、注意数据的所有处理还是在父组件当中完成
引入刚才创建的axios请求
// 引入axios请求
import {fetchHomeBanner,fetchCates,fetchItems } from '@a/index'
5、重新定义请求函数,并给予一定的数据显示规则
// 商品列表数据
const items = ref<{id:number,name:string,minPrice:number,pic:string,stores:number}[]>([])
const getItems = () => {
fetchItems(
{
// 传入一个id值,指定相应的数据
categoryId:72399
}
).then(res => {
if(res.data.code === 0) {
items.value = res.data.data.result
}
})
}
6、实例创建完成onMounted就出发封装的函数
// 实例创建时就触发的函数
onMounted(() => {
getItems()
})
7、此时打印一下已经成功,但是由于是老的接口,不支持解析前端传递的json格式参数,开发 axios的post请求默认是json格式,导致会有其他格式的混入新项目中一般不会有这种错误,直接就可以请求到精准数据了
,所以这里还要对数据进行简单的处理,使用qs包进行处理数据,qs.stringify() 将 对象转换成query {a: 10,b: 2} “a=10&b=20”,qs.parse() 将query字符串 转换成对象格式
- 安装qs包
npm i qs -S
- 在api中的index引入qs
// 引入qs包 import qs from 'qs'
- 在请求之前拦截器内做一下post数据格式的处理
// 处理post请求传递的数据格式,post请求且传参 if(config.method === 'post' && config.data) { config.data = qs.stringify(config.data) }
写入位置如下
8、此时可以请求到需要数据了
9、子组件通过props接受数据
// 子组件通过props接受父组件传递过来的数据
import { defineProps } from 'vue';
const props = defineProps<{
items: {id: number, name:string, minPrice:number,pic: string,stores:number}[]
}>()
10、在template中循环出所有数据,然后在对应标签显示
<template>
<div class="items">
<div class="item" v-for="item in items" :key="item.id">
<img :src="item.pic" alt="">
<div class="item-content">
<p>{{item.name}}</p>
<div>¥<span>{{ item.minPrice }}</span>.00</div>
<ul>
<li>{{ item.stores }}+评论</li>
<li>
<van-icon name="cart-o" color="#1989fa" size="20"/>
</li>
</ul>
</div>
</div>
</div>
</template>
11、此时会报一个items数据没有传过去的警告,需要回到index中定义自定义属性,放在子组件标签上
<Items :items="items"></Items>
8、穿插一个小问题,在控制台会出现这样的错误,是因为vue中::deep class的写法已经不用了,需要将所有的写法改成:deep(class)才能够影响到组件库内部的样式
9、以及静止用户左右滑动
/* 禁止左右滑动 */
html,body {
touch-action: pan-y;
}
9、商品列表下拉加载
1、按需引入list
import { createApp } from 'vue';
import { List } from 'vant';
const app = createApp();
app.use(List);
2、使用van-list标签包裹商品列表内容,也就是Items标签注意这里要让初始化是不触发的,设置:immediate-check="false"
<!-- 商品列表开始 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
:immediate-check="false"
>
<Items :items="items"></Items>
</van-list>
<!-- 商品列表结束 -->
3、定义好vant-list中的几个字段
// 商品列表上拉加载
const loading = ref<boolean>(false)//触底加载中的状态,布尔值,true代表触底
const finished = ref<boolean>(false)//是否已经没有数据,true代表没有数据
const onLoad = () => {
// 上拉触底
console.log('触底')
}
4、并根据接口当中的page和pageSize定义好请求的第几页和一页多少的数据,并在获取数据的时候需要携带参数page和pageSize---三个杠的注释为后面添加的内容
// 商品列表数据
const page = ref<number>(1) //--- 请求过程中定义的参数page
const pageSize = ref<number>(6) //--- 请求过程中定义的参数pageSize
const items = ref<{id:number,name:string,minPrice:number,pic:string,stores:number}[]>([])
const getItems = () => {
fetchItems(
{
// 请求时携带的参数id值,指定相应的数据
categoryId:72399,
// --- 请求时携带的参数page和pageSize
page:page.value,
pageSize:pageSize.value
}
).then(res => {
// console.log(res)
if(res.data.code === 0) {
items.value = res.data.data.result
}
})
}
5、此时发现数据已经变成五条了,但是要继续请求下面的数据,需要在刚才定义的onload当中让page加1,请求下一页数据,同时发送请求
const onLoad = () => {
// 上拉触底
console.log('触底')
// --- page+1请求下一页数据
page.value++
// --- 重新请求数据
getItems()
}
6、此时会直接替换刚才第一页的内容,我们想要的是在第一页的内容基础上往下加一页内容,所以需要对getItems请求成功的数据进行处理,不能将数据直接复制给items.value了,不然就是直接替换了,使用简单的结构赋值,将两个数据进行合并
const getItems = () => {
fetchItems(
{
// 请求时携带的参数id值,指定相应的数据
categoryId:72399,
// --- 请求时携带的参数page和pageSize
page:page.value,
pageSize:pageSize.value
}
).then(res => {
// console.log(res)
if(res.data.code === 0) {
// items.value = res.data.data.result
// 解构赋值合并
items.value = [
...items.value,
...res.data.data.result
]
}
})
}
7、这个时候已经可以请求下一页数据并进行组合显示了,由于加载的过程比较快,所以可以通过滚动时右侧滚动条进行观察,没有到达请求下一页范围时,滚动条比较长,请求第二页变短,也可以在network中查看请求
8、最后需要手动将loading锁关闭,让其继续请求下一页数据,但是会出现一个小问题,就是触底后虽然已经没有数据了但是一直处于触底状态,会不断发送请求,需要进行判断一下暂无数据的code码,是暂无数据的code码,开启finished,显示触底
const getItems = () => {
fetchItems(
{
// 请求时携带的参数id值,指定相应的数据
categoryId:72399,
// --- 请求时携带的参数page和pageSize
page:page.value,
pageSize:pageSize.value
}
).then(res => {
// console.log(res)
// --- 关闭loading,否则无法加载下一页
loading.value = false
if(res.data.code === 0) {
// items.value = res.data.data.result
// 解构赋值合并
items.value = [
...items.value,
...res.data.data.result
]
}
if(res.data.code === 700){
// --- 已经是最后一页没有数据,开启finished
finished.value = true
}
})
}
10、上拉刷新的处理已经和下拉加载的组合
1、按需引入PullRefresh
import { createApp } from 'vue';
import { PullRefresh } from 'vant';
const app = createApp();
app.use(PullRefresh);
2、找到van-pull-refresh,包裹header和section,同时改变一下v-model的值,不然会和下拉加载的冲突。
<template>
<div class="home-page">
<van-pull-refresh v-model="pullDownLoading" @refresh="onRefresh">
<!-- 页面头部开始 -->
<header v-scroll-directive>
</header>
<!-- 页面头部开结束 -->
<!-- 页面主体 -->
<section>
</section>
<Tabbar></Tabbar>
</van-pull-refresh>
</div>
</template>
3、定义van-pull-refresh中的几个字段,注意这里下拉后要显示的第一页数据,而且后面的数据是重新下滑加载,到底时需要关闭触底效果,不然后面的数据无法加载了,最后请求数据,显示出来
//首页下拉刷新
const pullDownLoading = ref<boolean>(false)
// 定义下拉事件
const onRefresh = () => {
// 重新请求列表数据
page.value = 1 //数据需要变成第一页的数据
items.value = [] //所有的数据要变成空
finished.value = false //关闭已经完成,可以进行重新加载
getItems() //再次请求需要的数据
}
4、此时会发现一直显示加载中,要手动关闭刷新的状态,同时为了刷新效果更佳,加上toast的效果
- 在main.js中按需引入Toast
import { createApp } from 'vue'; import { Toast } from 'vant'; const app = createApp(); app.use(Toast);
- 回到HomePage的index,引入对应的方法和Toast样式
// 引入toast样式和响应的方法 import 'vant/es/toast/style'; import 'vant/es/notify/style' import { showToast } from 'vant';
- 在onRefresh中写入一个定时器来让刷新持续时间和手动将loading变成false,从而关闭一直加载中的状态,然后再刷新就没有一直加载中且中间会提示刷新成功的轻提示
const onRefresh = () => { setTimeout(() => { showToast('刷新成功'); pullDownLoading.value = false; }, 1000); // 重新请求列表数据 page.value = 1 //数据需要变成第一页的数据 items.value = [] //所有的数据要变成空 finished.value = false //关闭已经完成,可以进行重新加载 getItems() //再次请求需要的数据 }
11、商品详情路由跳转并传入id
1、进入HomePage中的子组件Items绑定点击事件,点击对应的商品传入商品id
<template>
<div class="items">
<div class="item" v-for="item in items" :key="item.id" @click="clickItem(item.id)">
</div>
</div>
</template>
2、定义clickItem方法,触发自定义事件
vue3组合式api声明触发的事件链接
// ts与组合式api当中定义声明触发的事件
const emit = defineEmits<{
(e:'clickItem',id:number): void
}>()
// 定义点击事件,接收传过来的参数id
const clickItem = (id:number) => {
// 点击谁谁的id传递
emit('clickItem',id)
}
3、回到index中,给Items再添加自定义事件来拿到传递过来的参数
<Items :items="items" @clickItem="enterDetail"></Items>
4、自定义点击事件要执行点击跳转到详情页,这里之前在做搜索框跳转已经导入了useRouter实例,上面的push方法跳转到对应的详情页,直接使用
// 点击商品进入详情页
const enterDetail = (id: number) => {
router.push({
path: '/detail',
query:{id}
})
}
5、在views下建立DetailPage详情页的组件文件夹
6、进入src下的router文件夹中写商品详情路由,此时已经可以跳转到详情页了
// 商品详情路由
{
path:'/detail',
name:'detail',
component: () => import('@v/DetailPage/index.vue')
}
12、封装公共的NavBar组件
1、按需引入NavBar
import { createApp } from 'vue';
import { NavBar } from 'vant';
const app = createApp();
app.use(NavBar);
2、在src下的components文件夹中建立NavBar.vue文件,加上vant中的NavBar
<template>
<div>
<van-nav-bar title="标题" left-text="返回" left-arrow>
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped lang="scss">
</style>
3、在views中的DetailPage的index中引入并用标签导入子组件
<template>
<div>
<Navbar title="详情页"/>
</div>
</template>
<script setup lang="ts" name="DetailPage">
import Navbar from '@c/NavBar.vue';
</script>
<style scoped>
</style>
4、在NavBar中写入滚动时头部navbar样式改变
<template>
<div>
<div class="header" :style="{ backgroundColor: navbarBg }" >
<div :style="{ backgroundColor: iconBg }"><van-icon :style="{ color: iconColor }" name="arrow-left" /></div>
<ul :style="{opacity:itemShow}">
<li>商品</li>
<li>评价</li>
<li>详情</li>
<li>推荐</li>
</ul>
<div class="fenlei" :style="{ backgroundColor: iconBg }"><van-icon :style="{ color: iconColor }" name="qita" class-prefix="icon"/></div>
</div>
<section>
<img src="../../src/assets/imgs/鸡.png" alt="">
</section>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 滚动事件
const navbarBg = ref('rgba(255,255,255,0)')
const iconBg = ref()
const iconColor = ref()
const itemShow = ref()
let IndexTitleScroll = () => {
// 获取距离顶部的距离
let scrollTop =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop;
if (scrollTop > 10) {
navbarBg.value = "rgba(255,255,255,1)";
iconBg.value = 'transparent'
iconColor.value = '#252525'
itemShow.value = '1'
} else {
navbarBg.value = "rgba(255,255,255,0)"
iconBg.value = '#666'
iconColor.value = '#fff'
itemShow.value = '0'
}
}
window.addEventListener("scroll", IndexTitleScroll);
</script>
<style scoped lang="scss">
.header{
color: #252525;
height: 44px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px 0 10px;
box-sizing: border-box;
position: fixed;
z-index: 1000;
div{
width: 30px;
height: 30px;
background-color: #666;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
.van-icon{
font-size: 20px;
color: #fff;
}
}
ul{
font-size: 14px;
display: flex;
width: 235px;
justify-content: space-between;
height: 100%;
align-items: center;
opacity: 0;
li{
width: 25%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: #262626;
}
}
}
.fenlei{
color: #fff;
}
</style>
5、点击返回键回到上一页,给返回键的van-icon绑定一个一个点击事件@click="clickBack"
,导入useRouter,通过路由实例上的方法go,赋值-1返回上一页
// 导入路由实例上的方法
import { useRouter } from 'vue-router'; //需要放在顶部,不然会报错
// 点击返回上一页
const router = useRouter()
const clickBack = () => {
router.go(-1)
}
13、处理商品详情数据
1、接下来处理商品详情数据,进入api中定义请求商品详情的接口并导出
// 请求商品详情
const fetchItemDetail = (params = {}) => axios.get('/shop/goods/detail', {params})
// 导出接口
export {
fetchItemDetail
}
2、在DetailPage中引入,定义useroute拿到路由上的参数,通过query传过来的id获取,定义一个方法来触发请求,在实例初始化完成就调用方法,成功拿到数据
import {ref,reactive,onMounted} from 'vue'
// 引入NavBar子组件
import Navbar from '@c/NavBar.vue';
// 引入api中的请求数据详情的函数
import { fetchItemDetail } from '@a/index';
// 拿到路由上的参数
import { useRoute } from 'vue-router';
const route = useRoute()
// 定义请求的方法
const getDetail = () => {
fetchItemDetail({
id:route.query.id
}).then(res => {
console.log(res)
})
}
// 实例初始化完成发送调用函数
onMounted(() => {
getDetail()
})
3、定义三个需要用的请求成功后商品详情的数据,将请求到的数据对应的字段赋值给刚定义的常量
/ 商品详情的数据,请求成功后的字段
const basicInfo = ref<{name:string, minPrice:number}>({})
const pics = ref<{id:number, pic:string}[]>([])
const content = ref('')
// 定义请求的方法
const getDetail = () => {
fetchItemDetail({
id:route.query.id
}).then(res => {
// console.log(res)
// 请求到的数据赋值给上面定义的字段
basicInfo.value = res.data.data.basicInfo
pics.value = res.data.data.pics
content.value = res.data.data.content
})
}
4、在页面中进行相应的渲染和css样式
<template>
<div style="height:1000px" class="detail-box">
<Navbar title="详情页"/>
<!-- 商品信息 -->
<div class="detail-container">
<!-- 轮播 -->
<van-swipe class="my-swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item class="content" v-for="banner in pics" :key="banner.id">
<img :src="banner.pic" alt="" />
</van-swipe-item>
</van-swipe>
<img src="../../assets/imgs/gift.png" alt="" class="imgCent">
<!-- 商品详情介绍 -->
<div class="goods-detail">
<ul>
<li>¥<span>{{ basicInfo.minPrice }}</span>.00</li>
<li class="detailRig">
<div>
<van-icon name="gold-coin-o" />
<span>降价提醒</span>
</div>
<div>
<van-icon class="goods-icon" name="shoucang1" class-prefix="icon"></van-icon>
<span>收藏</span>
</div>
</li>
</ul>
<div v-html="content" class="goods-content"></div>
</div>
</div>
<!-- 商品详情 -->
<div class="item-floor"></div>
</div>
</template>
<style scoped lang="scss">
.detail-container {
.goods-detail{
padding:0 20px 10px 20px;
background-color: #fff;
border-radius: 0 0 10px 10px;
ul{
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
li:first-child{
color: #f2270c;
font-size: 18px;
span{
font-size: 30px;
}
}
.detailRig{
display: flex;
align-items: center;
color: #262626;
font-size: 20px;
div{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-left: 10px;
span{
font-size: 10px;
margin-top: 3px;
}
}
}
}
.goods-content{
font-size: 16px;
color: #262626;
font-weight: 700;
line-height: 21px;
}
}
}
.goods-icon{
font-size: 20px;
}
.content{
:deep(img){
display: block;
width: 100%;
}
}
.my-swipe {
width: 10rem;
height: 10rem;
img {
width: 10rem;
height: 10rem;
}
}
.imgCent{
width: 100vw;
}
.detail-box{
background-color: #eee;
}
.item-floor{
width: 100vw;
height: 500px;
border-radius: 10px 10px 0 0 ;
background-color: #fff;
margin-top: 10px;
}
</style>
14、处理规格数据
1、定义商品规格数据sku,这里需要注意的是商品规格的数据为商品详情加上初始数量为1的数据即可。---为新添加内容
// 定义请求的方法
const getDetail = () => {
fetchItemDetail({
id:route.query.id
}).then(res => {
// console.log(res)
// 请求到的数据赋值给上面定义的字段
basicInfo.value = res.data.data.basicInfo
pics.value = res.data.data.pics
content.value = res.data.data.content
// --- 商品规格数据,初始数量为1
sku.value = {
...res.data.data.basicInfo,
number:1
}
})
}
// 商品规格
const sku = ref<{number:number,minPrice:number,name:string,pic:string}>({})
2、进入vant找到商品行动栏,注册并拿到template中,同时拿到路由实例上的方法router,实现点击跳转到对应页面
import { createApp } from 'vue';
import { ActionBar, ActionBarIcon, ActionBarButton } from 'vant';
const app = createApp();
app.use(ActionBar);
app.use(ActionBarIcon);
app.use(ActionBarButton);
拿到路由实例上的push方法
import { useRoute,useRouter } from 'vue-router';
const router = useRouter()
提前定义好了绑定的事件,用router中的push方法跳转到对应页面
<!-- 商品行动栏 -->
<van-action-bar>
<van-action-bar-icon @click="router.push('')" icon="shop-o" color="#f53f3f" text="店铺" />
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon @click="router.push('/cart')" icon="cart-o" text="购物车" />
<van-action-bar-button class="btn btnLeft" type="danger" text="加入购物车" @click="showSkuAction('addCart')" color="#f2140c"/>
<van-action-bar-button class="btn" type="danger" text="立即购买" @click="showSkuAction('confirmOrder')" color="#ffba0d"/>
</van-action-bar>
// 商品行动栏样式
.btn{
border-radius: 40px;
width: 103px;
height: 38px;
}
.btnLeft{
margin-right: 10px;
}
3、点击加入购物车弹出层,两个组件组合完成,Popup 弹出层和Card 卡片和Stepper 步进器,按需引入要用到的组件
import { createApp } from 'vue';
import { Popup,Card,Stepper } from 'vant';
const app = createApp();
app.use(Popup)
.user(Card)
.use(Stepper);
4、写入大致结构和样式,绑定两个加入购物车和立即购买按钮
<!-- 商品规格弹窗 -->
<van-popup
v-model:show="showSku"
:style="{ padding: '10px',height:'40%' }"
position="bottom"
round
closeable
>
<div>
<van-card
:num="sku.number"
:price="sku.minPrice"
:title="sku.name"
:thumb="sku.pic"
>
<template #footer>
<div class="goods-number">
<span>数量</span>
<div class="change-number">
<van-stepper v-model="sku.number" :min="1"/>
</div>
</div>
<van-button class="cofirm-btn" size="large" @click="clickConfirmBtn" round type="danger">确定</van-button>
</template>
</van-card>
</div>
</van-popup>
<script setup lang="ts" name="DetailPage">
import { useRoute,useRouter } from 'vue-router';
const router = useRouter()
// 商品规格
const sku = ref<{number:number,minPrice:number,name:string,pic:string}>({})
// 商品规格弹出层
const showSku = ref<boolean>(false)
const showSkuAction = () => {
showSku.value = true
}
5、由于点击加入购物车和立即购买是同一个弹出层,但是要实现的动作不一样,加入购物车去往购物车页面,立即购买则是进入购买页,定义一个条件,点击按钮的行为是什么,在点击按钮的时候传入一个参数actionMsg,赋值给clickAction,此时点击两个按钮传入的参数是不一样的,加入购物车是addCart,立即购买是confirmOrder,再定义好点击确认按钮的事件。进行一个判断,如果clickAction的值是addCart,加入购物车,否则去订单页,同时关闭弹出层
// 商品规格弹出层
const showSku = ref<boolean>(false)
let clickAction = 'addCart'; // 点击商品规格行为
const showSkuAction = (actionMsg:string) => {
showSku.value = true
clickAction = actionMsg
}
// 点击商品规格弹窗中的确定按钮
const clickConfirmBtn = () => {
if (clickAction === 'addCart') {
alert('加入购物车')
} else {
alert('去订单页')
}
showSku.value = false
}
15、商品列表页
1、定义一个商品列表页,在views中建立文件夹ItemLists文件夹,vtss创建基本结构,定义name
2、进入router中定义路由
// 商品分类页路由
{
path: '/itemLists',
name: 'itemLists',
component: () => import('@v/ItemLists/index.vue')
},
3、回到首页的index中,找到宫格导航,van-grid有一个自带的属性to属性,可以进行跳转到对应的路由,同时query传参将对应的分类id传过去
<!-- 宫格导航开始 -->
<van-grid :column-num="5" :border="false" icon-size="40px">
<van-grid-item
v-for="cate in cates"
:key="cate.id"
:icon="cate.icon"
:text="cate.name"
:to="{ path: '/itemLists', query: { cateId: cate.id } }"
/>
</van-grid>
<!-- 宫格导航结束 -->
4、回到ItemLists的index中,开始写相关结构和样式,头部搜索和右侧分类页如下
<template>
<div class="itemLists-page">
<header>
<van-icon name="arrow-left" @click="backHomePage"/>
<van-search
v-model="value"
placeholder="请输入搜索关键词"
shape="round"
background="transparent"
@click-input="toSearchPage"
/>
<van-icon name="qita" class-prefix="icon" @click="show = true"></van-icon>
</header>
<!-- 右侧三点遮罩层 -->
<van-overlay
:show="show"
@click="show = false"
z-index="1000"
:custom-style="{background:'rgba(255,255,255,0)'}"
bind:close="closePopup"
>
<div class="wrapper">
<div class="block">
<div><van-icon name="31shouye" class-prefix="icon"><span>首页</span></van-icon></div>
<div><van-icon name="fenlei" class-prefix="icon"><span>分类搜索</span></van-icon></div>
<div><van-icon name="gouwuche1" class-prefix="icon"><span>购物车</span></van-icon></div>
<div><van-icon name="wode" class-prefix="icon"><span>我的京东</span></van-icon></div>
<div><van-icon name="liulanjilu" class-prefix="icon"><span>浏览记录</span></van-icon></div>
</div>
</div>
</van-overlay>
</div>
</template>
<script setup lang="ts" name="ItemLists">
import { ref } from 'vue';
import { useRouter,useRoute } from "vue-router"
let router = useRouter()
const route = useRoute()
const value = ref('')
// 点击搜索框跳转搜索页
const toSearchPage = () => {
// router.push("/search");
}
// 点击图标返回上一页
const backHomePage = () => {
router.go(-1)
}
// 遮罩层
const show = ref(false);
</script>
<style scoped lang="scss">
// 页面头部样式
header{
width: 100%;
display: flex;
position: fixed;
align-items: center;
// background-color: #e1251b;
background-color: transparent;
justify-content: space-between;
padding: 0 15px;
z-index: 1000;
:deep(.van-search){
padding: 10px 0;
width: 85%;
}
:deep(.icon-fenlei3){
font-size: 25px;
}
}
// 遮罩层样式
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background-color: rgba(255,255,255,0);
.block {
height: 205px;
width: 125px;
background-color: rgba(0,0,0,.9);
position: absolute;
top: 53px;
right: 10px;
border-radius: 4px;
font-size: 14px;
color: #fff;
font-size: 14px;
div:last-child{
border-bottom: none;
}
div{
width: 100%;
height: 20%;
border-bottom: 1px solid #fff;
display: flex;
align-items: center;
padding-left: 10px;
box-sizing: border-box;
span{
margin-left: 10px;
font-size: 14px;
}
}
}
}
</style>
5、接下来就是完成最重要的一个部分,排序栏order-bar,还是一样,先写样式
<!-- order-bar -->
<div class="order-bar">
<div class="comprehensive">
<span>综合</span>
<van-icon name="sanjiaoxing_shang" class-prefix="icon"></van-icon>
</div>
<div class="sales">
<span>销量</span>
<van-icon name="sanjiaoxing_shang" class-prefix="icon"></van-icon>
</div>
<div class="price">
<span>价格</span>
<van-icon name="sanjiaoxing_shang" class-prefix="icon"></van-icon>
</div>
<div class="filtrate">
<span>筛选</span>
<van-icon name="shaixuan" class-prefix="icon"></van-icon>
</div>
</div>
<style scoped lang="scss">
// orderBar样式
.order-bar{
height: 40px;
background-color: #fff;
width: 100%;
display: flex;
align-items: center;
justify-content: space-around;
z-index: 1000;
div{
height: 40px;
display: flex;
align-items: center;
}
}
</style>
6、定义一个reactive来监听对象内数据的变化,里面两个属性,order和upDown,分别代表按谁排序,是升序还是降序
// 控制排序高亮状态,reactive监听对象内的值变化
const orderStatus = reactive({
order:0, //按照什么进行排序,0是综合,1是销量,2是价格
upDown:0 //0 升序,1降序
})
7、给标签的class属性绑定条件,class条件是order为1时,icon则是点击了并且unDown为0时为升序
<div class="comprehensive">
<span :class="[{active:orderStatus.order === 0}]">综合</span>
<van-icon
:class="[{
active: orderStatus.order === 1 && orderStatus.upDown === 0
}]"
name="sanjiaoxing_shang"
class-prefix="icon"
></van-icon>
</div>
8、绑定点击事件让点击谁的时候高亮以及图标切换,这里需要在标签内绑定事件,动态添加active高亮,对于图标的处理相对复杂点,需要双重判断和一个初始图标,开始朝上的图标,当点击事件产生时,如果order为0也就是第一个栏目,同时updown上下图标为0,这时让类变成朝下的icon类,再次点击时这时updown为1,让其变成朝上的类,但是我感觉我这里写的代码还是过于繁琐了,后面在看如何优化
<!-- order-bar -->
<div class="order-bar">
<div class="comprehensive" @click="switchOrder(0)">
<span :class="[{active:orderStatus.order === 0}]">综合</span>
<div
:class="[ 'icon','icon-sanjiaoxing_shang',{
'icon-sanjiaoxing_shang': orderStatus.order === 0 && orderStatus.upDown === 0,
'icon-sanjiaoxing_shang-copy': orderStatus.order === 0 && orderStatus.upDown === 1,
active: orderStatus.order === 0
}]"
>
</div>
</div>
<div class="sales" @click="switchOrder(1)">
<span :class="[{active:orderStatus.order === 1}]">销量</span>
<div
:class="[ 'icon','icon-sanjiaoxing_shang',{
'icon-sanjiaoxing_shang': orderStatus.order === 1 && orderStatus.upDown === 0,
'icon-sanjiaoxing_shang-copy': orderStatus.order === 1 && orderStatus.upDown === 1,
active: orderStatus.order === 1
}]"
>
</div>
</div>
<div class="price" @click="switchOrder(2)">
<span :class="[{active:orderStatus.order === 2}]">价格</span>
<div
:class="[ 'icon','icon-sanjiaoxing_shang',{
'icon-sanjiaoxing_shang': orderStatus.order === 2 && orderStatus.upDown === 0,
'icon-sanjiaoxing_shang-copy': orderStatus.order === 2 && orderStatus.upDown === 1,
active: orderStatus.order === 2
}]"
>
</div>
</div>
<div class="filtrate" @click="switchOrder(3)">
<span :class="[{active:orderStatus.order === 3}]">筛选</span>
<div
:class="[ 'icon','icon-shaixuan',{
active: orderStatus.order === 3
}]"
>
</div>
</div>
</div>
// 点击改变排序的规则
const switchOrder = (status:number) => {
// status是按照什么排序,0是综合,1销量,2价格
if(status === 0) {
orderStatus.order = 0
if (orderStatus.upDown === 0) {
orderStatus.upDown = 1
} else {
orderStatus.upDown = 0
}
}
else if(status === 1) {
orderStatus.order = 1
if (orderStatus.upDown === 0) {
orderStatus.upDown = 1
} else {
orderStatus.upDown = 0
}
}
else if(status === 2) {
orderStatus.order = 2
if (orderStatus.upDown === 0) {
orderStatus.upDown = 1
} else {
orderStatus.upDown = 0
}
}
else if(status === 3) {
orderStatus.order = 3
}
}
9、下面就是处理数据渲染和点击排序以及点击商品跳转详情页,这里主要难点是怎么进行排序,在文档中一般都有对应的排序字段,只要进行判断升序降序的字段即可,同时调用请求数据,在template中进行渲染,onMounted中自动触发即可。
<!-- 详细商品数据 -->
<div class="list-container">
<div class="items">
<div class="item" v-for="item in items" :key="item.id" @click="enterDetail(item.id)">
<img :src="item.pic" alt="" class="item-img">
<div class="item-info">
<div class="item-name">
<img src="../../assets/imgs/kaixue.png" width="34" alt="">
<h5>{{ item.name }}</h5>
</div>
<div class="item-action">
<span class="price">¥{{ item.minPrice }}</span>
</div>
</div>
</div>
</div>
</div>
// 引入请求
import { fetchItems } from '@a/index'
// 点击商品跳转详情页
const enterDetail = (id: number) => {
router.push({
path: '/detail',
query: {
id
}
})
}
// 请求排序的字段值
const fetchOrderText = computed(()=>{
// 初始就是降序
let orderText = 'orderDown'
switch (orderStatus.order) {
case 0:
// 销量降序排序
orderText = 'ordersDown'
break;
case 1:
// 价格排序
// 判断升序还是降序
if (orderStatus.upDown === 0) {
// 价格升序
orderText = 'priceUp'
} else {
orderText = 'priceDown'
}
break;
case 2:
// 按照发布事件 降序排序
orderText = 'addedDown'
break;
default:
break;
}
return orderText
})
// 请求商品列表数据
const items = ref<any[]>([])
const getItems = () => {
fetchItems({
// 排序需要的两个字段
categoryId: route.query.cateId,
// 排序字段,文档中有
orderBy: fetchOrderText.value
}).then(res => {
if(res.data.code === 0){
items.value = res.data.data.result
}
})
}
onMounted(() => {
getItems()
})
// 商品分类详情样式
.list-container{
.items{
.title{
font-size: 16px;
line-height: 30px;
text-align: center;
background-color: #f9f6f6;
}
.item{
display: flex;
padding: 0 8px;
margin-top: 5px;
&-img{
width: 4rem;
}
&-info{
flex: 1;
padding-left: 8px;
.item-name{
display: flex;
align-items: center;
img{
width: 34px;
height: 14px;
margin-right: 10px;
}
h5{
font-size: 14px;
line-height: 30px;
}
}
.item-action{
display: flex;
justify-content: space-between;
.price {
font-size: 18px;
color: #e4393c;
}
}
}
}
}
}
16、商品分类页
1、商品分类页主要就是请求所有分类数据,和根据高亮id默认高亮下面的所有数据放到右侧,高亮类似于选项卡,高亮等于下标即可,其他基本跟前面差不多。原理一样,要注意的一点就是code等于700的时候是没有数据的,渲染要为空
<template>
<div class="head">
<header>
<van-icon name="arrow-left" @click="backHomePage"/>
<van-search
v-model="value"
placeholder="请输入搜索关键词"
shape="round"
background="transparent"
@click-input="toSearchPage"
/>
<van-icon name="qita" class-prefix="icon" @click="show = true"></van-icon>
</header>
<!-- 右侧三点遮罩层 -->
<van-overlay
:show="show"
@click="show = false"
z-index="1000"
:custom-style="{background:'rgba(255,255,255,0)'}"
bind:close="closePopup"
>
<div class="wrapper">
<div class="block">
<div><van-icon name="31shouye" class-prefix="icon"><span>首页</span></van-icon></div>
<div><van-icon name="fenlei" class-prefix="icon"><span>分类搜索</span></van-icon></div>
<div><van-icon name="gouwuche1" class-prefix="icon"><span>购物车</span></van-icon></div>
<div><van-icon name="wode" class-prefix="icon"><span>我的京东</span></van-icon></div>
<div><van-icon name="liulanjilu" class-prefix="icon"><span>浏览记录</span></van-icon></div>
</div>
</div>
</van-overlay>
</div>
<div class="cate-container">
<div class="cate-wrap">
<div class="cates">
<div
v-for="(cate,index) in cates"
:key="cate.id"
@click="switchActive(index)"
:class="['cate', {
active: activeIndex === index
}]">
{{ cate.name }}
</div>
</div>
</div>
<div class="item-wrap">
<div class="items">
<div class="item" v-for="item in items" :key="item.id" @click="enterDetail(item.id)">
<img :src="item.pic" alt="" class="item-img">
<div class="item-info">
<h5>{{ item.name }}</h5>
<div class="item-action">
<span class="price">¥{{ item.minPrice }}</span>
<van-icon name="gouwuchetianjia" size="18" color="#0ED397" class-prefix="icon"></van-icon>
</div>
</div>
</div>
</div>
</div>
</div>
<Tabbar />
</template>
<script setup lang="ts" name="CatePage">
import { ref, onMounted } from 'vue'
import Tabbar from '@/components/Tabbar.vue';
import { fetchCates, fetchItems } from '@/api';
import { useRouter } from 'vue-router';
const router = useRouter();
const value = ref('')
// 点击搜索框跳转搜索页
const toSearchPage = () => {
// router.push("/search");
}
// 点击图标返回上一页
const backHomePage = () => {
router.go(-1)
}
// 遮罩层
const show = ref(false);
// 点击商品跳转详情页
const enterDetail = (id: number) => {
router.push({
path: '/detail',
query: {
id
}
})
}
// 定义分类有哪些
const cates = ref<any[]>([]);
// 定义数据分类详情
const items = ref<any[]>([]);
// 高亮分类下标
const activeIndex = ref<number>(0);
// 点击切换高亮状态
const switchActive = (index: number) => {
activeIndex.value = index
getItems()
}
// 请求所有分类
const getCates = () => {
fetchCates().then(res=> {
if (res.data.code === 0) {
cates.value = res.data.data
getItems();
}
})
}
// 请求高亮分类下的商品
const getItems = () => {
fetchItems({
categoryId: cates.value[activeIndex.value].id
}).then(res => {
if (res.data.code === 0) {
items.value = res.data.data.result
}
if (res.data.code === 700) {
// 当前分类下没有商品
items.value = []
}
})
}
onMounted(() => {
getCates();
})
</script>
<style scoped lang="scss">
// 页面头部样式
header{
width: 100%;
display: flex;
align-items: center;
// background-color: #e1251b;
background-color: transparent;
justify-content: space-between;
padding: 0 15px;
z-index: 1000;
:deep(.van-search){
padding: 10px 0;
width: 85%;
}
:deep(.icon-fenlei3){
font-size: 25px;
}
}
// 遮罩层样式
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background-color: rgba(255,255,255,0);
.block {
height: 205px;
width: 125px;
background-color: rgba(0,0,0,.9);
position: absolute;
top: 53px;
right: 10px;
border-radius: 4px;
font-size: 14px;
color: #fff;
font-size: 14px;
z-index: 100000;
div:last-child{
border-bottom: none;
}
div{
width: 100%;
height: 20%;
border-bottom: 1px solid #fff;
display: flex;
align-items: center;
padding-left: 10px;
box-sizing: border-box;
span{
margin-left: 10px;
font-size: 14px;
}
}
}
}
// 分类内容
.cate-container {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 50px;
display: flex;
padding-top: 54px;
.cate-wrap {
width: 3rem;
height: 100%;
border-right: 1px solid #eee;
overflow-y: auto;
background-color: #f1f1f1;
.cate {
font-size: 14px;
color: #333;
text-align: center;
line-height: 36px;
position: relative;
&.active {
background-color: #fff;
&::after {
content: '';
width: 2px;
height: 36px;
background-color: #e4393c;
position: absolute;
left: 0;
top: 0;
}
}
}
}
.item-wrap {
width: 7rem;
height: 100%;
overflow-y: auto;
.item{
display: flex;
padding: 0 8px;
margin-top: 5px;
&-img{
width: 2rem;
}
&-info{
flex: 1;
padding-left: 8px;
h5{
font-size: 14px;
line-height: 30px;
}
.item-action{
display: flex;
justify-content: space-between;
.price {
font-size: 18px;
color: #e4393c;
}
}
}
}
}
}
</style>
17、登录鉴权
1、进入utils中建立一个index.ts文件封装两个方法,判断是否登录状态以及获取token
const isLogin = () => !!localStorage.getItem('token');
const getToken = () => localStorage.getItem('token');
export {
isLogin,
getToken
}
2、添加路由鉴权,进入router文件夹,在路由中定义路由鉴权,这里不同于b端,只需要判断哪些页面需要登录即可,有些页面不登录也是可以访问的,所以meta定义在需要登录的路由当中,meta来判断哪些需要登录访问,这里也只有个人中心和购物车需要登录后才能访问,所以给他们加上meta
// 导入utils中的isLogin方法判断是否登录
import { isLogin } from '@/utils'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
// 路由重定向
path:'/',
redirect:'/home'
},
// 主页路由
{
path: '/home',
name: 'home',
component: HomePage
},
// 分类页路由
{
path: '/cate',
name: 'cate',
component: () => import('@v/CatePage/index.vue')
},
// 购物车页路由
{
path: '/cart',
name: 'cart',
meta: {
needAuth: true
},
component: () => import('@v/CartPage/index.vue')
},
// 个人中心页路由
{
path: '/user',
name: 'user',
meta: {
needAuth: true
},
component: () => import('@v/UserPage/index.vue')
},
// 搜索页面
{
path: '/search',
name: 'hosearchme',
component: SearchPage
},
// 商品详情路由
{
path:'/detail',
name:'detail',
component: () => import('@v/DetailPage/index.vue')
},
// 商品分类页路由
{
path: '/itemLists',
name: 'itemLists',
component: () => import('@v/ItemLists/index.vue')
},
]
})
// 路由前置守卫,跳转前判断是否登录了
router.beforeEach((to) => {
// console.log(to.meta.needAuth)
if (to.meta.needAuth) {
// 需要登录才能访问
if (!isLogin()) {
// 未登录
return {
path: '/login',
// 判断从哪儿跳回来的
query: {
from: to.path
}
}
}
}
})
3、建立登录页文件夹
<template>
<div>
登录页
</div>
</template>
<script setup lang="ts" name="LoginPage">
</script>
<style scoped>
</style>
4、配置路由跳转,进入router中创建路由,此时点击需要登录的购物车页面和我的页面会自动跳转到登录页
{
path: '/login',
name: 'login',
component: () => import('@v/LoginPage/index.vue')
}
5、实现接口鉴权,进入utils中的request文件,正常情况下我们直接在请求头当中添加token即可,因为这里接口比较老,需要再params中传参携带token,所以这里如果按照正常项目来说,直接在请求头添加token即可,注意判断是否添加token一定要放在处理post请求格式的前面,不然下面的处理会让请求变成字符串,报错!
// 引入定义的getToken方法
import { getToken } from "@u/index"
// 添加拦截器interceptor
axios.interceptors.request.use(
function (config) {
/*
大部分接口是在请求头中添加token
这里接口特殊:请求参数中添加token
*/
if (getToken()) {
// 判断是什么请求
if (config.method === 'get') {
config.params = {
// 增加token而不是替换params中的数据,这里解构赋值直接组合到一起
...config.params,
token: getToken()
}
} else {
// post请求
config.data = {
...config.data,
token: getToken()
}
}
}
// 处理post请求传递的数据格式,post请求且传参
if(config.method === 'post' && config.data) {
config.data = qs.stringify(config.data)
}
// 处理token
return config;
},
function (error) {
return Promise.reject(error);
}
);
// 处理字符串接口的校验
axios.interceptors.response.use(
function (res) {
// 处理登录状态问题
if (res.data.code === 2000) {
// 2000是未登录或者登录状态过期
}
return res;
},
function (error) {
return Promise.reject(error);
}
);
6、正式写登录注册页面,进入LoginPage当中
-
找到vant中form表单控件,引入
import { createApp } from 'vue'; import { Form, Field, CellGroup,Button } from 'vant'; const app = createApp(); app.use(Form); app.use(Field); app.use(CellGroup); app.use(Button)
-
拿到其中表单的代码放到template中
<template> <div> <van-form @submit="userLogin"> <van-cell-group inset> <van-field v-model="user.username" name="用户名" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" /> <van-field v-model="user.pwd" type="password" name="密码" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" /> </van-cell-group> <div style="margin: 16px"> <van-button round block type="primary" native-type="submit"> 提交 </van-button> <van-button round block type="danger" native-type="reset"> 重置 </van-button> <van-button round block type="success" @click="router.push('/reg')"> 去注册 </van-button> </div> </van-form> </div> </template>
-
利用reactive形容对象格式,来定义两个参数,用户名和密码,表单提交就是用户登录字段
<script setup lang="ts" name="LoginPage"> import { reactive } from "vue"; const user = reactive({ username: "", pwd: "", }); // 用户登录 const userLogin = () => { } </script>
-
创建注册页面,进入router定义路由跳转
{ path: '/reg', name: 'reg', component: () => import('@v/RegPage/index.vue') }
-
在将登录页复制一份修改一下数据
<template> <div> <van-form @submit="userRegister"> <van-cell-group inset> <van-field v-model="user.username" name="用户名" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" /> <van-field v-model="user.pwd" type="password" name="密码" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" /> </van-cell-group> <div style="margin: 16px"> <van-button round block type="primary" native-type="submit"> 提交 </van-button> <van-button round block type="danger" native-type="reset"> 重置 </van-button> <van-button round block type="success" @click="router.push('/login')"> 直接登录 </van-button> </div> </van-form> </div> </template> <script setup lang="ts" name="LoginPage"> import { reactive } from "vue"; const user = reactive({ username: "", pwd: "", }); // 用户注册 const userRegister = () => { } </script> <style scoped> </style>
-
回到登录页,引入route和router,实现页面跳转,这里因为上面html内容我已经在去注册按钮里面定义了使用了router.push跳转,所以就直接引入了
import { useRouter, useRoute } from "vue-router"; const router = useRouter(); const route = useRoute();
-
进入注册页同样定义跳转,但是不用route,因为登录页我需要判断从而调过来,登录完成之后还要调回去,但是注册页不需要,只需要引入router即可
同样这里去登录按钮点击路由跳转到登录页在前面也写好了,就直接引入router了
import { useRouter } from "vue-router"; const router = useRouter();
-
回到登录页进行页面优化样式,哪里组件效果没出,注意找到对应文档引入即可,我这里写了很多就直接放写好的页面效果
<template> <div> <header> <van-icon name="arrow-left" @click="backHomePage"></van-icon> <p>京东登录注册</p> <span></span> </header> <van-form @submit="userLogin"> <van-cell-group inset> <van-field v-model="user.username" name="用户名" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" /> <van-field v-model="user.pwd" type="password" name="密码" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" /> </van-cell-group> <div style="margin: 16px"> <van-button round block type="primary" native-type="submit" color="#e4393c"> 提交 </van-button> </div> <div class="fastReg"> <p>短信验证码登录</p> <p @click="router.push('/reg')"> 手机快速注册 </p> </div> </van-form> <footer> <div class="other-login"> <span class="line"></span> <span class="text">其他登录方式</span> <span class="line"></span> </div> <div class="login-icon"> <img src="../../assets/imgs/qq.png" alt=""> <img src="../../assets/imgs/wx.png" alt=""> <img src="../../assets/imgs/iphone.png" alt=""> </div> <div class="user-agree"> <van-checkbox v-model="checked" shape="square" icon-size="13px">登录即代表你已同意<span>用户隐私政策</span></van-checkbox> </div> </footer> </div> </template> <script setup lang="ts" name="LoginPage"> import {ref,reactive } from "vue"; import { useRouter, useRoute } from "vue-router"; const router = useRouter(); const route = useRoute(); const checked = ref(true); const user = reactive({ username: "", pwd: "", }); // 用户登录 const userLogin = () => { } // 点击图标返回上一页 const backHomePage = () => { router.go(-1) } </script> <style scoped lang="scss"> /* 头部样式 */ header{ display: flex; align-items: center; height: 44px; padding: 0 25px; justify-content: space-between; margin-bottom: 20px; .van-icon{ text-align: left; font-size: 20px; } p{ font-size: 17px; text-align: center; margin-left: -20px; } } // 去注册页样式 .fastReg{ color: #00000066; font-size: 14px; padding: 0 25px; display: flex; justify-content: space-between; align-items: center; } // 其他登录方式 .other-login{ display: flex; justify-content: space-between; align-items: center; margin: 60px auto; padding: 0 25px; } .text{ min-width: 120px; font-size: 12px; text-align: center; color: #00000033; } .line{ width: 40%; height: 1px; background-color: #00000033; } // 页面底部 footer{ padding: 0 25px; .other-login{ display: flex; justify-content: space-between; align-items: center; margin: 60px auto; padding: 0 25px; .text{ min-width: 120px; font-size: 12px; text-align: center; color: #00000033; } .line{ width: 40%; height: 1px; background-color: #00000033; } } .login-icon{ display: flex; justify-content: space-around; padding: 0 25px; img{ width: 50px; } } .user-agree{ margin-top: 30px; display: flex; align-items: center; justify-content: center; :deep(.van-checkbox){ border-radius: 4px; .van-checkbox__label{ font-size: 13px; color: #0000004D; span{ color: #4a90e2; margin-left: 5px; } } } } } </style>
-
同时优化注册页样式
<template> <div> <header> <van-icon name="arrow-left" @click="backHomePage"></van-icon> <p>京东注册</p> <div></div> </header> <van-form @submit="userRegister"> <van-cell-group inset> <van-field v-model="user.username" name="用户名" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" /> <van-field v-model="user.pwd" type="password" name="密码" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" /> </van-cell-group> <div style="margin: 16px"> <van-button round block type="primary" native-type="submit" color="#e4393c"> 提交 </van-button> </div> </van-form> </div> </template> <script setup lang="ts" name="LoginPage"> import { reactive } from "vue"; import { useRouter } from "vue-router"; const router = useRouter(); const user = reactive({ username: "", pwd: "", }); // 用户注册 const userRegister = () => { } // 点击图标返回上一页 const backHomePage = () => { router.go(-1) } </script> <style scoped lang="scss"> /* 头部样式 */ header{ display: flex; align-items: center; height: 44px; padding: 0 25px; justify-content: space-between; margin-bottom: 20px; .van-icon{ text-align: left; font-size: 20px; } p{ font-size: 17px; text-align: center; margin-left: -20px; } } </style>
-
先完成注册页面,不然无法登录,进入api文件夹中写入注册和登录两个接口并导出
// 用户注册接口 /user/username/register const userReg = (params = {}) => axios.post('/user/username/register', params) // 用户登录接口 /user/username/register const userLog = (params = {}) => axios.post('/user/username/login', params) // 导出接口 export { userReg, userLog }
-
回到注册页,引入刚刚写好的请求,同时引入成功的弹窗,点击提交时发送请求
这里点击提交的方法在前面写template中也写好了
,请求发送后显示注册成功,同时关闭后自动跳转到登录页
import { userReg } from "@a/index";
import { showSuccessToast } from "vant";
// 用户注册
const userRegister = () => {
userReg(user).then((res) => {
showSuccessToast({
message: "注册成功",
duration: 1000,
onClose: () => {
router.replace("/login");
},
});
});
};