【Vue】移动端项目流程
【课堂笔记】vue-pro-移动端
移动端项目
O 项目技术栈说明
- 脚手架: Vite 3
还有 vue-cli - 底层 webpack
- 脚本:typescript
- 路由:vue-router4
- 状态管理器: vuex4
还有 pinia
- 组件库:vant@3.6.3
- 组件API:选项式API
一、Vite 脚手架的使用
官网:https://vitejs.cn/
vue3 Vite官网:https://cn.vitejs.dev/
Vite 下一代的前端工具链 为开发提供极速响应
$ cnpm i vite -g
1.1 介绍
Vite(法语意为 “快速的”,发音 /vit/
,发音同 “veet”)是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
- 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
- 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
Vite 意在提供开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并有完整的类型支持。
1.2 搭建vue3项目
1.2.1 vite官方建议
使用 NPM:
$ npm create vite@latest
使用 Yarn: 如果电脑尚未安装yarn,可通过
cnpm i yarn -g
完成安装$ yarn create vite
使用 PNPM: 如果电脑尚未安装pnpm,可通过
cnpm i pnpm -g
完成安装$ pnpm create vite
然后按照提示操作即可!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Prq95Hvt-1673602369087)(assets/image-20220923090140128.png)]
1.2.2 vue官网生态系统建议
要使用 Vite 来创建一个 Vue 项目,非常简单
$ npm init vue@latest
这个命令会安装和执行 create-vue,它是 Vue 提供的官方脚手架工具。跟随命令行的提示继续操作即可。
√ Project name: ... mobile-vue-app # 输入项目名称
√ Add TypeScript? ... No / Yes # Yes 选择使用ts
√ Add JSX Support? ... No / Yes # Yes 选择使用jsx支持
√ Add Vue Router for Single Page Application development? ... No / Yes # Yes 选择使用vue路由
√ Add Pinia for state management? ... No / Yes # No 后期使用vuex作为状态管理器
√ Add Vitest for Unit Testing? ... No / Yes # No 不选择测试
√ Add Cypress for both Unit and End-to-End testing? ... No / Yes # No 不选择
√ Add ESLint for code quality? ... No / Yes # Yes 选择代码格式校验
√ Add Prettier for code formatting? ... No / Yes # Yes 选择Prettier作为代码格式规范
$ cd mobile-vue-app # 进入项目目录
$ npm i # 安装依赖 如果安装出错,建议可以考虑使用手机热点安装
$ npm run lint # 代码格式校验
$ npm run dev # 启动项目
如果同局域网内想要通过ip地址访问项目,可以修改 运行命令
// package.json { ..., "scripts": { "dev": "vite"// ----- "dev": "vite --host" // +++++ ..., }, .... }
1.3 目录结构分析
|- mobile-vue-app # 项目名称
|- .vscode
|- node_modules # 项目依赖
|- public # 图标文件夹
favicon.ico # 网页图标
|- src # 写代码主场
|- assets # 资源文件
base.css # 基础样式
logo.svg # logo
main.css # 项目样式
|- components # 自定义组件
|- icons # 图标组件
HelloWorld.vue # 自定义组件
TheWelcome.vue # 自定义组件
WelcomeItem.vue # 自定义组件
|- router # 路由文件夹
index.ts # 路由的配置
|- views # 项目页面组件
AboutView.vue # 页面组件
HomeView.vue # 页面组件
App.vue # 项目根组件
main.ts # 项目入口文件
.eslintrc.cjs # 代码格式化规则
.gitignore # git上传忽略文件
.prettierrc.json # 右键格式化的时候,就会自动帮我们补全符号
.env.d.ts # 环境配置声明文件
index.html # 页面的模板
package.json # 项目依赖说明
README.md # 说明文档
tsconfig.config.json # ts配置文件说明
tsconfig.json # ts配置文件
vite.config.ts # vite配置文件
二、Vue3单文件组件
1.SFC语法定义
一个 Vue 单文件组件 (SFC),通常使用 *.vue
作为文件扩展名,它是一种使用了类似 HTML 语法的自定义文件格式,用于定义 Vue 组件。一个 Vue 单文件组件在语法上是兼容 HTML 的。
每一个 *.vue
文件都由三种顶层语言块构成:<template>
、<script>
和 <style>
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
打开 main.ts 发现 引入App.vue 的地方画红线,说明项目中没有说明 .vue文件的声明文件
// src/env.d.ts
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
再次打开main.ts 发现,红线消失
复制项目src文件夹,然后保留App.vue以及main.ts的基本内容
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
选项式API
<!-- src/App.vue 选项式API-->
<script lang="ts">
import { defineComponent } from 'vue'
// 如果是js作为脚本语言 只需要 export default {}
export default defineComponent({
data () {
return {
msg: 'hello sfc',
count: 10
}
}
})
</script>
<template>
<div class="example">
{{ msg }} --- {{ count }}
<button @click="count++">加1</button>
</div>
</template>
<!-- 如果样式只针对当前组件是有效的,只需要给style添加 scoped 属性即可 -->
<style scoped>
.example {
color: #f66;
}
</style>
vue 组合式API
<!-- src/App.vue 组合式API-->
<script lang="ts">
import { defineComponent, ref } from 'vue'
// 如果是js作为脚本语言 只需要 export default {}
export default defineComponent({
// data () {
// return {
// msg: 'hello sfc',
// count: 10
// }
// }
setup () {
const msg = 'hello sfc'
const count = ref(10)
return {
msg,
count
}
}
})
</script>
<template>
<div class="example">
{{ msg }} --- {{ count }}
<button @click="count++">加1</button>
</div>
</template>
<!-- 如果样式只针对当前组件是有效的,只需要给style添加 scoped 属性即可 -->
<style scoped>
.example {
color: #f66;
}
</style>
组合式API简写形式
<!-- src/App.vue 组合式API简写形式-->
<script lang="ts" setup>
import { ref } from 'vue'
const msg = 'hello sfc'
const count = ref(10)
</script>
<template>
<div class="example">
{{ msg }} --- {{ count }}
<button @click="count++">加1</button>
</div>
</template>
<!-- 如果样式只针对当前组件是有效的,只需要给style添加 scoped 属性即可 -->
<style scoped>
.example {
color: #f66;
}
</style>
说明:如果使用 js 作为脚本语言,是不需要引入
import { defineComponent } from 'vue'
的,组件中只需要 通过export defaut {}
作为组件的js逻辑即可
2.相应语言块(template、script、style)
2.1 template
- 每个
*.vue
文件最多可以包含一个顶层<template>
块。 - 语块包裹的内容将会被提取、传递给
@vue/compiler-dom
,预编译为 JavaScript 渲染函数,并附在导出的组件上作为其render
选项。
2.2 script
- 每个
*.vue
文件最多可以包含一个<script>
块。(使用<script setup>
的情况除外) - 这个脚本代码块将作为 ES 模块执行。
- 默认导出应该是 Vue 的组件选项对象,可以是一个对象字面量或是 defineComponent 函数的返回值。
2.3 style
- 每个
*.vue
文件可以包含多个<style>
标签。 - 一个
<style>
标签可以使用scoped
或module
attribute 来帮助封装当前组件的样式。使用了不同封装模式的多个<style>
标签可以被混合入同一个组件。
3.预处理器
代码块可以使用 lang
这个 attribute 来声明预处理器语言,最常见的用例就是在 <script>
中使用 TypeScript:
<script lang="ts">
// use TypeScript
</script>
lang
在任意块上都能使用,比如我们可以在 <style>
标签中使用 SASS 或是 <template>
中使用 Pug:
<template lang="pug">
p {{ msg }}
</template>
<style lang="scss">
$primary-color: #333;
body {
color: $primary-color;
}
</style>
Vite 提供了对 .scss
, .sass
, .less
, .styl
和 .stylus
文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖:
# .scss and .sass
$ npm i -D sass
# .less
$ npm i -D less
# .styl and .stylus
$ npm i -D stylus
4. src导入
如果你更喜欢将 *.vue
组件分散到多个文件中,可以为一个语块使用 src
这个 attribute 来导入一个外部文件:
<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>
请注意 src
导入和 JS 模块导入遵循相同的路径解析规则,这意味着:
- 相对路径需要以
./
开头
5. 注释
在每一个语块中你都可以按照相应语言 (HTML、CSS、JavaScript 和 Pug 等等) 的语法书写注释。对于顶层注释,请使用 HTML 的注释语法 <!-- comment contents here -->
四、TS与选项式API
1.为组件的prop标注类型
选项式 API 中对 props 的类型推导需要用 defineComponent()
来包装组件。有了它,Vue 才可以通过 props
以及一些额外的选项,比如 required: true
和 default
来推导出 props 的类型:
import { defineComponent } from 'vue'
export default defineComponent({
// 启用了类型推导
props: {
name: String,
id: [Number, String],
msg: { type: String, required: true },
metadata: null
},
mounted() {
this.name // 类型:string | undefined
this.id // 类型:number | string | undefined
this.msg // 类型:string
this.metadata // 类型:any
}
})
然而,这种运行时 props
选项仅支持使用构造函数来作为一个 prop 的类型——没有办法指定多层级对象或函数签名之类的复杂类型。
我们可以使用 PropType
这个工具类型来标记更复杂的 props 类型
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
interface Book {
title: string
author: string
year: number
}
export default defineComponent({
props: {
book: {
// 提供相对 `Object` 更确定的类型
type: Object as PropType<Book>,
required: true
},
// 也可以标记函数
callback: Function as PropType<(id: number) => void>
},
mounted() {
this.book.title // string
this.book.year // number
// TS Error: argument of type 'string' is not
// assignable to parameter of type 'number'
this.callback?('123')
}
})
案例如下:
<!-- src/App.vue 选项式API-->
<script lang="ts" >
import { defineComponent, ref } from 'vue'
import Child from './Child.vue'
export default defineComponent({
components: {
Child
},
data () {
return {
user: {
userName: '张三',
age: 18,
sex: '男'
}
}
}
})
</script>
<template>
<div class="example">
<Child :user="user"></Child>
<Child :user="{ userName: '李四', age: 28, sex: '女'}"></Child>
<!-- Missing required prop: "user" -->
<Child />
</div>
</template>
<style scoped lang="css">
</style>
<!-- src/Child.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
interface IUser {
userName: string
age: number
sex: string
}
export default defineComponent({
props: {
// user: Object as PropType<IUser> // 类型断言
user: {
type: Object as PropType<IUser>,
required: true,
default: (): IUser => {
return {
userName: '王五',
age: 10,
sex: '男'
}
}
}
}
})
</script>
<template>
<div>子组件 - {{ user.userName }} - {{ user.age }} - {{ user.sex }}</div>
</template>
2.为组件的emit 标注类型
我们可以给 emits
选项提供一个对象来声明组件所触发的事件,以及这些事件所期望的参数类型。试图触发未声明的事件会抛出一个类型错误:
import { defineComponent } from 'vue'
export default defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// 执行运行时校验
return payload.bookName.length > 0
}
},
methods: {
onSubmit() {
this.$emit('addBook', {
bookName: 123 // 类型错误
})
this.$emit('non-declared-event') // 类型错误
}
}
})
案例如下:
<!-- src/App.vue 选项式API-->
<script lang="ts" >
import { defineComponent, ref } from 'vue'
import Child from './Child.vue'
export default defineComponent({
components: {
Child
},
data () {
return {
user: {
userName: '张三',
age: 18,
sex: '男'
}
}
},
methods: {
getData (val: { money: number }) {
console.log(val)
}
}
})
</script>
<template>
<div class="example">
<Child :user="user" @my-event="getData"></Child>
</div>
</template>
<style scoped lang="css">
</style>
<!-- src/Child.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
interface IUser {
userName: string
age: number
sex: string
}
export default defineComponent({
props: {
// user: Object as PropType<IUser> // 类型断言
user: {
type: Object as PropType<IUser>,
required: true,
default: (): IUser => {
return {
userName: '王五',
age: 10,
sex: '男'
}
}
}
},
emits: {
'my-event': (payload: { money: number }): boolean => {
return payload.money > 100001
}
},
methods: {
sendData () {
this.$emit('my-event', {
money: 10000 // Invalid event arguments: event validation failed for event "my-event".
})
}
}
})
</script>
<template>
<div>
子组件 - {{ user.userName }} - {{ user.age }} - {{ user.sex }}
<button @click="sendData">子组件给父组件传值</button>
</div>
</template>
3.为计算属性标注类型
计算属性会自动根据其返回值来推导其类型:
mport { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
message: 'Hello!'
}
},
computed: {
greeting() {
return this.message + '!'
}
},
mounted() {
this.greeting // 类型:string
}
})
在某些场景中,你可能想要显式地标记出计算属性的类型以确保其实现是正确的:
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
message: 'Hello!'
}
},
computed: {
// 显式标注返回类型
greeting(): string {
return this.message + '!'
},
// 标注一个可写的计算属性
greetingUppercased: {
get(): string {
return this.greeting.toUpperCase()
},
set(newValue: string) {
this.message = newValue.toUpperCase()
}
}
}
})
在某些 TypeScript 因循环引用而无法推导类型的情况下,可能必须进行显式的类型标注。
案例如下:
<!-- src/App.vue 选项式API-->
<script lang="ts" >
import { defineComponent, ref } from 'vue'
export default defineComponent({
data () {
return {
firstName: '',
lastName: ''
}
},
computed: {
// fullName (): string {
// return this.firstName + this.lastName
// }
fullName: {
get (): string { return this.firstName + this.lastName },
set (val: string): void {
this.firstName = val.split(' ')[0]
this.lastName = val.split(' ')[1]
}
}
},
methods: {
changeName () {
this.fullName = '张 三丰'
}
}
})
</script>
<template>
<div class="example">
<input type="text" v-model="firstName" /> +
<input type="text" v-model="lastName" /> =
{{ fullName }}
<button @click="changeName">设置fullName</button>
</div>
</template>
<style scoped lang="css">
</style>
4.为事件处理器标注类型
在处理原生 DOM 事件时,应该为我们传递给事件处理函数的参数正确地标注类型。
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
methods: {
handleChange(event) {
// `event` 隐式地标注为 `any` 类型
console.log(event.target.value)
}
}
})
</script>
<template>
<input type="text" @change="handleChange" />
</template>
没有类型标注时,这个 event
参数会隐式地标注为 any
类型。这也会在 tsconfig.json
中配置了 "strict": true
或 "noImplicitAny": true
时抛出一个 TS 错误。因此,建议显式地为事件处理函数的参数标注类型。此外,你可能需要显式地强制转换 event
上的属性:
import { defineComponent } from 'vue'
export default defineComponent({
methods: {
handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
}
})
案例如下:
<!-- src/App.vue 选项式API-->
<script lang="ts" >
import { defineComponent } from 'vue'
export default defineComponent({
methods: {
handleChange (event: Event) {
console.log((event.target as HTMLInputElement).value)
}
}
})
</script>
<template>
<div >
<input type="text" @change="handleChange" />
</div>
</template>
<style scoped lang="css">
</style>
5.扩充全局property
某些插件会通过 app.config.globalProperties
为所有组件都安装全局可用的属性。举例来说,我们可能为了请求数据而安装了 this.$http
,或者为了国际化而安装了 this.$translate
。为了使 TypeScript 更好地支持这个行为,Vue 暴露了一个被设计为可以通过 TypeScript 模块扩展来扩展的 ComponentCustomProperties
接口:
import axios from 'axios'
declare module 'vue' {
interface ComponentCustomProperties {
$http: typeof axios
$translate: (key: string) => string
}
}
我们可以将这些类型扩展放在一个 .ts
文件,或是一个影响整个项目的 *.d.ts
文件中。无论哪一种,都应确保在 tsconfig.json
中包括了此文件。对于库或插件作者,这个文件应该在 package.json
的 types
属性中被列出。
为了利用模块扩展的优势,你需要确保将扩展的模块放在 TypeScript 模块 中。 也就是说,该文件需要包含至少一个顶级的 import
或 export
,即使它只是 export {}
。如果扩展被放在模块之外,它将覆盖原始类型,而不是扩展!
// 正常工作。
export {}
declare module 'vue' {
interface ComponentCustomProperties {
$translate: (key: string) => string
}
}
案例如下:
// src/main.ts
import { createApp } from 'vue'
// import axios from 'axios'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.test = '测试'
app.mount('#app')
<!-- src/App.vue 选项式API-->
<script lang="ts" >
import { defineComponent } from 'vue'
export default defineComponent({
mounted () {
console.log(this.test) // 测试 报警告信息
},
methods: {
handleChange (event: Event) {
console.log((event.target as HTMLInputElement).value)
}
}
})
</script>
<template>
<div >
<input type="text" @change="handleChange" />
</div>
</template>
<style scoped lang="css">
</style>
打印 this.test 时遇到 ts 的问题
6.扩充自定义选项
某些插件,比如 vue-router
,提供了一些自定义的组件选项,比如 beforeRouteEnter
:
import { defineComponent } from 'vue'
export default defineComponent({
beforeRouteEnter(to, from, next) {
// ...
}
})
如果没有确切的类型标注,这个钩子函数的参数会隐式地标注为 any
类型。我们可以为 ComponentCustomOptions
接口扩展自定义的选项来支持:
import { Route } from 'vue-router'
declare module 'vue' {
interface ComponentCustomOptions {
beforeRouteEnter?(to: Route, from: Route, next: () => void): void
}
}
现在这个 beforeRouteEnter
选项会被准确地标注类型。注意这只是一个例子——像 vue-router
这种类型完备的库应该在它们自己的类型定义中自动执行这些扩展。
五、开始项目构建
只保留src文件夹下的 main.ts 以及 App.vue组件,
// src/App.vue <script lang="ts"> </script> <template> <div>App.vue</div> </template> <style> </style>
// src/main.ts import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app')
5.1 准备工作
- 假如项目使用styl / stylus 作为css的预处理器
$ cnpm i stylus -D
-
回顾移动端布局
-
弹性盒布局
display: flex; flex-direction: column; justify-content: center; align-items: center;
-
rem布局
html { font-size: 100px; } div { /* height: 50px; */ height: 0.5rem; }
? rem以及em布局的区别
-
VW/VH布局
html { font-size: 100px; /* 100 / 375 * 100 100 进行375等分,然后乘以好计算的100 */ /* iphone6 750 * 1334 375 * 667*/ /* iphne5 640 * 1136 320 * 568 100/320*100 = 31.25vw*/ /* font-size: 26.666667vw; */ /* iphone6 1rem=100px 其余机型自动适配*/ } body { /* 设计稿中的最小的字体大小 */ font-size: 12px; } div { /* height: 50px; */ height: 0.5rem; }
-
媒体查询
@media screen and (max-width: 300px) { body { background-color:lightblue; } } @media only screen and (orientation: landscape) { // 横屏 portrait 竖屏 html { font-size: 100px; } }
-
-
ts基础知识(类型注解、类型断言、!、?、interface、type、声明文件*.d.ts 。。。。。。)
https://www.bilibili.com/video/BV1H44y157gq/?spm_id_from=333.337.search-card.all.click
5.2 构建移动端基本页面结构
5.2.1 基本构建
编写重置样式表如下:
// src/assets/main.styl
* {
padding: 0;
margin: 0;// src/assets/main.styl
// stylus语法考验写代码的规范
*
padding 0
margin 0
list-style none
html
height 100%
font-size 26.6666667vw
body
height 100%
font-size 14px
#app
height 100%
list-style: none;
text-decoration: none;
}
html, body, #app {
height: 100%;
}
项目入口文件代码如下:
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.styl'
const app = createApp(App)
app.mount('#app')
项目根组件如下:
<!-- src/App.vue -->
<script lang="ts">
</script>
<template>
<div class="container">
<div class="box">
<header class="header">header</header>
<div class="content">content</div>
</div>
<footer class="footer">footer</footer>
</div>
</template>
<style lang="stylus">
.container
height 100%
display flex
flex-direction column
.box
flex 1
display flex
flex-direction column
.header
height 0.44rem
background-color #f66
.content
flex 1
.footer
height 0.5rem
background-color #efefef
</style>
运行项目,发现页面呈现上中下结构,这就是我们的移动端页面的主要布局了。
那么问题来了,假如用户把手机横屏,这个时候如果还是使用vw和rem布局的话就用容易出问题了。(通过点击模拟器旋转屏幕按钮查看)
5.2.2 媒体查询处理横屏
可以设置横屏情况下不采用vw布局或者给用户展示一个提示信息
// src/assets/main.styl
// stylus语法考验写代码的规范
*
padding 0
margin 0
list-style none
html
height 100%
font-size 26.6666667vw
body
height 100%
font-size 14px
#app
height 100%
@media only screen and (orientation landscape) // 横屏
// 横屏状态下不要使用vw布局
html
font-size 100px
根组件这样写:
<!-- src/App.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({})
</script>
<template>
<div class="container">
<div class="box">
<header class="header">header</header>
<div class="content">content</div>
</div>
<footer class="footer">footer</footer>
</div>
<div class="landscape-tip">
请将屏幕竖向浏览
</div>
</template>
<style lang="stylus">
.container
height 100%
display flex
flex-direction column
.box
flex 1
display flex
flex-direction column
.header
height 0.44rem
background-color #f66
.content
flex 1
.footer
height 0.5rem
background-color #efefef
user-select none
.landscape-tip
position fixed
top 0
right 0
bottom 0
left 0
background-color rgba(0, 0, 0, .8)
color #fff
display none
justify-content center
align-items center
@media only screen and (orientation landscape) // 横屏
// 横屏状态下使用flex布局,其他情况不显示
.landscape-tip
display flex
</style>
现在审查元素 查看css 没有自动补全样式,如何通过配置自动补全css
5.2.4 自动补全css
$ cnpm i postcss postcss-preset-env -D
项目根目录下创建 postcss.config.js
// postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')
]
}
给页面的底部添加 user-select: none
,重启项目,审查元素看前后的变化
// 未配置 postcss 时,渲染为
.container .footer {
height: 0.5rem;
background-color: #efefef;
user-select: none;
}
// 配置过 postcss 时,渲染为
.container .footer {
height: 0.5rem;
background-color: #efefef;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
5.3 构建项目基本页面
思考每个页面的头部和内容区域是根据用户的选择而一起改变的,那么可以创建以下四个基本页面
5.3.1 构建首页面
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<header class="header">home header</header>
<div class="content">home content</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang='stylus'>
</style>
5.3.2 构建分类页面
<!-- src/views/kind/index.vue -->
<template>
<div class="box">
<header class="header">kind header</header>
<div class="content">kind content</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang='stylus'>
</style>
5.3.3 构建购物车页面
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">cart header</header>
<div class="content">cart content</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang='stylus'>
</style>
5.3.4 构建个人中心页面
<!-- src/views/user/index.vue -->
<template>
<div class="box">
<header class="header">user header</header>
<div class="content">user content</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang='stylus'>
</style>
5.4 引入路由
创建项目时,已经选择过使用vue-router,所以不需要安装
如果没有选择,那么使用以下语句安装
$ cnpm i vue-router@4 -S
然后再配置
5.4.1 创建路由
一般情况下,会将路由设置到 src/router/index.ts
文件中
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router'
// 直接引入组件,不管你用不用这个页面,我都引入了 --- 静态导入
import Home from '@/views/home/index.vue'
// import Kind from '@/views/kind/index.vue'
// import Cart from '@/views/cart/index.vue'
// import User from '@/views/user/index.vue'
// 使用路由懒加载 - 访问该路由时才加载该组件 --- 动态导入
const Kind = () => import('@/views/kind/index.vue')
const Cart = () => import('@/views/cart/index.vue')
const User = () => import('@/views/user/index.vue')
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: Home
},
{
path: '/kind',
name: 'kind',
component: Kind
},
{
path: '/cart',
name: 'cart',
component: Cart
},
{
path: '/user',
name: 'user',
component: User
}
]
const router: Router = createRouter({
history: createWebHistory(),
routes
})
export default router
5.4.2 入口文件中使用路由
调用路由以插件的形式
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router'
import './assets/main.styl'
const app = createApp(App)
app.use(router)
app.mount('#app')
5.4.3 路由出口
vue-router
内置了<router-view></router-view>
组件
router-view
将显示与 url 对应的组件。你可以把它放在任何地方,以适应你的布局
<!-- src/App.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({})
</script>
<template>
<div class="container">
<!-- <div class="box">
<header class="header">header</header>
<div class="content">content</div>
</div> -->
<RouterView />
<footer class="footer">footer</footer>
</div>
<div class="landscape-tip">
请将屏幕竖向浏览
</div>
</template>
<style lang="stylus">
.container
height 100%
display flex
flex-direction column
.box
flex 1
display flex
flex-direction column
.header
height 0.44rem
background-color #f66
.content
flex 1
.footer
height 0.5rem
background-color #efefef
user-select none
.landscape-tip
position fixed
top 0
right 0
bottom 0
left 0
background-color rgba(0, 0, 0, .8)
color #fff
display none
justify-content center
align-items center
@media only screen and (orientation landscape) // 横屏
// 横屏状态下使用flex布局,其他情况不显示
.landscape-tip
display flex
</style>
此时地址栏分别输入
http://127.0.0.1:5173/home
、http://127.0.0.1:5173/kind
、http://127.0.0.1:5173/cart
、http://127.0.0.1:5173/user
查看项目运行结果,
可以得知已经可以通过路由显示不同的页面
但是用户一般都是通过底部选项卡来切换页面的
5.4.4 构建页面底部组件
在src
文件夹下创建components
文件夹,在components
文件夹下创建底部组件
因为底部选项卡需要字体图标,可以选择 iconfont阿里字体图标库,搜索图标,加入购物车,添加至项目
mobile-vue-app
,选择font-class
,点击查看在线链接
,拷贝css链接
项目根目录下index.html
中引入css链接
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_3665887_h3lsrioddkk.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
底部组件展示如下:
<!-- src/components/Footer.vue -->
<template>
<footer class="footer">
<ul>
<li>
<span class="iconfont icon-shouye"></span>
<p>首页</p>
</li>
<li>
<span class="iconfont icon-fenlei"></span>
<p>分类</p>
</li>
<li>
<span class="iconfont icon-gouwuche"></span>
<p>购物车</p>
</li>
<li>
<span class="iconfont icon-shouye1"></span>
<p>我的</p>
</li>
</ul>
</footer>
</template>
根组件引入底部组件
<!-- src/App.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
import Footer from '@/components/Footer.vue'
export default defineComponent({
components: {
Footer
}
})
</script>
<template>
<div class="container">
<!-- <div class="box">
<header class="header">header</header>
<div class="content">content</div>
</div> -->
<RouterView />
<!-- <footer class="footer">footer</footer> -->
<Footer />
</div>
<div class="landscape-tip">
请将屏幕竖向浏览
</div>
</template>
<style lang="stylus">
.container
height 100%
display flex
flex-direction column
.box
flex 1
display flex
flex-direction column
.header
height 0.44rem
background-color #f66
.content
flex 1
.footer
height 0.5rem
background-color #efefef
user-select none
ul
width 100%
height 100%
display flex
li
flex 1
display flex
flex-direction column
justify-content center
align-items center
span
font-size 0.24rem
.landscape-tip
position fixed
top 0
right 0
bottom 0
left 0
background-color rgba(0, 0, 0, .8)
color #fff
display none
justify-content center
align-items center
@media only screen and (orientation landscape) // 横屏
// 横屏状态下使用flex布局,其他情况不显示
.landscape-tip
display flex
</style>
5.4.5 声明式导航跳转页面
声明式跳转 a href
编程式跳转 window.location.href = ‘’
vue-router
中提供了<router-link>
组件来完成页面的声明式跳转。
vue2 - vue-router3
<router-link to="/kind"></router-link>
===><a href="/kind"></a>
<router-link to="/kind" tag="p"></router-link>
===><p></p>
vue3 - vue-router4 移除了 tag 属性
<router-link>
会默认渲染为a标签,并且为了凸显出用户选择了哪一个选项,需要使用到vue-router
中router-link
组件的custom
、v-slot
的属性,且需要解构出isActive
、href
、navigate
等属性,具体代码如下
参考:https://router.vuejs.org/zh/api/#custom
<!-- src/components/Footer.vue -->
<template>
<footer class="footer">
<ul>
<RouterLink to="/home" custom v-slot="{ href, isActive, navigate }">
<li :href="href" @click="navigate" :class="isActive ? 'active': ''">
<span class="iconfont icon-shouye"></span>
<p>首页</p>
</li>
</RouterLink>
<RouterLink to="/kind" custom v-slot="{ href, isActive, navigate }">
<li :href="href" @click="navigate" :class="isActive ? 'active': ''">
<span class="iconfont icon-fenlei"></span>
<p>分类</p>
</li>
</RouterLink>
<RouterLink to="/cart" custom v-slot="{ href, isActive, navigate }">
<li :href="href" @click="navigate" :class="isActive ? 'active': ''">
<span class="iconfont icon-gouwuche"></span>
<p>购物车</p>
</li>
</RouterLink>
<RouterLink to="/user" custom v-slot="{ href, isActive, navigate }">
<li :href="href" @click="navigate" :class="isActive ? 'active': ''">
<span class="iconfont icon-shouye1"></span>
<p>我的</p>
</li>
</RouterLink>
</ul>
</footer>
</template>
<!-- src/App.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
import Footer from '@/components/Footer.vue'
export default defineComponent({
components: {
Footer
}
})
</script>
<template>
<div class="container">
<!-- <div class="box">
<header class="header">header</header>
<div class="content">content</div>
</div> -->
<RouterView />
<!-- <footer class="footer">footer</footer> -->
<Footer />
</div>
<div class="landscape-tip">
请将屏幕竖向浏览
</div>
</template>
<style lang="stylus">
.container
height 100%
display flex
flex-direction column
.box
flex 1
display flex
flex-direction column
.header
height 0.44rem
background-color #f66
.content
flex 1
.footer
height 0.5rem
background-color #efefef
user-select none
ul
width 100%
height 100%
display flex
li
flex 1
display flex
flex-direction column
justify-content center
align-items center
&.active // 底部选项卡选中的样式
color #f66
span
font-size 0.24rem
.landscape-tip
position fixed
top 0
right 0
bottom 0
left 0
background-color rgba(0, 0, 0, .8)
color #fff
display none
justify-content center
align-items center
@media only screen and (orientation landscape) // 横屏
// 横屏状态下使用flex布局,其他情况不显示
.landscape-tip
display flex
</style>
vue-router 3版本中 只需要通过一个tag属性即可生成目标标签。
5.5 引入UI组件库
使用vue实现移动端项目,最好的组件库是 vant,以前还有一个叫做 mint-ui
官网:https://vant-contrib.gitee.io/vant/#/zh-CN
5.5.1 介绍
轻量、可靠的移动端 Vue 组件库
5.5.2 快速上手
-
安装
$ cnpm i vant -S
-
按需引入组件
$ cnpm i unplugin-vue-components -D
基于
vite
的项目,在vite.config.js
文件中配置插件// vite.config.ts import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import Components from 'unplugin-vue-components/vite'; import { VantResolver } from 'unplugin-vue-components/resolvers'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), vueJsx(), Components({ resolvers: [VantResolver()], }), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
-
引入函数组件的样式
Vant 中有个别组件是以函数的形式提供的,包括 Toast
,Dialog
,Notify
和 ImagePreview
组件。在使用函数组件时,unplugin-vue-components
无法自动引入对应的样式,因此需要手动引入样式
你可以在项目的入口文件或公共模块中引入以上组件的样式,这样在业务代码中使用组件时,便不再需要重复引入样式了。
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from '@/router'
import 'vant/es/toast/style'
import 'vant/es/dialog/style'
import 'vant/es/notify/style'
import 'vant/es/image-preview/style'
import './assets/main.styl'
const app = createApp(App)
app.use(router)
app.mount('#app')
首页组件测试
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<header class="header">home header</header>
<div class="content">
<van-button type="success" @click="test">成功按钮</van-button>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showToast } from 'vant'
export default defineComponent({
methods: {
test () {
showToast({
message: '底部展示',
position: 'bottom',
});
}
}
})
</script>
<style lang='stylus'>
</style>
5.6 封装数据请求
在vue/react项目中建议使用 axios
作为数据请求的方案
axios官网:http://www.axios-js.com/
5.6.1 介绍
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
5.6.2 安装
$ cnpm i axios -S
5.6.3 案例
执行 GET
请求
// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 上面的请求也可以这样做
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// axios
axios({
url: '/user?ID=12345',
method: 'GET',
}).then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// 也可以这么写
axios({
url: '/user',
method: 'GET',
data: {
ID: '12345'
}
}).then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
执行post请求
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
// axios
axios({
url: '/user',
method: 'POST',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
}).then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
执行多个并发请求
function getUserAccount() {
return axios.get('/user/12345');
}
function getUserPermissions() {
return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));
5.6.4 axios项目封装
一般封装时,需要判断用户当前所处的环境:开发环境、测试环境、生产环境
开发环境:http://localhost:5173 开发阶段
生产环境: https://www.baidu.com 项目上线,被人通过 ip或者域名访问项目
process.env.NODE_ENV 判断环境。development production
如果封装过程中 出现
找不到名称“process”。是否需要为节点安装类型定义? 请尝试使用
npm i --save-dev @types/node,然后将 “node” 添加到类型字段。
$ cnpm i @types/node -D
修改
tsconfig.json
{ "extends": "@vue/tsconfig/tsconfig.web.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] }, "types": [ // ++++++++ "node" ] }, "references": [ { "path": "./tsconfig.config.json" } ] }
完整封装代码如下: http://121.89.205.189:3000/apidoc/
// src/utils/request.ts
import axios from 'axios'
const isDev = process.env.NODE_ENV === 'development' // 真 - 开发环境,假-生产环境
// http://121.89.205.189:3000/apidoc/
// npm run dev 走?后的
// npm run build 走:后的
const ins = axios.create({
baseURL: isDev ? 'http://121.89.205.189:3000/api' : 'http://121.89.205.189:3000/api'
})
// 拦截器
ins.interceptors.request.use((config) => {
return config
}, (err) => {
return Promise.reject(err)
})
ins.interceptors.response.use((response) => {
return response
}, (err) => {
return Promise.reject(err)
})
export default ins
5.7 构建首页
5.7.1 封装首页相关数据请求
接口文档:http://121.89.205.189:3000/apidoc/
需要使用接口
- 查看轮播图:http://121.89.205.189:3000/apidoc/#api-Banner-GetBannerList
- 获取秒杀产品列表数据:http://121.89.205.189:3000/apidoc/#api-Pro-GetProSeckillList
- 获取产品分页列表数据:http://121.89.205.189:3000/apidoc/#api-Pro-GetProList
遵循模块化开发思想,将数据请求方法统一封装
// src/api/home.ts
import request from '@/utils/request'
interface IPager {
count: number
limitNum: number
}
// 轮播图数据
export function getBannerList () {
return request.get('/banner/list')
}
// 秒杀列表数据
export function getSeckilllist (params?: IPager) {
return request.get('/pro/seckilllist', { params })
}
// 产品列表数据
export function getProList (params?: IPager) {
return request.get('/pro/list', { params })
}
5.7.2 构建首页轮播图组件以及渲染
-
构建轮播图组件
<!-- src/views/home/components/Banner.vue --> <template> <div>轮播图</div> </template>
-
首页注册引入组件
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner /> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' export default defineComponent({ components: { Banner } }) </script> <style lang='stylus'> </style>
-
首页请求数据以及传递数据
// src/views/home/home.d.ts export interface IBanner { bannerid: string alt: string link: string flag: boolean img: string }
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import { getBannerList } from '@/api/home' import type { IBanner } from './home' interface IData { bannerList: IBanner[] } export default defineComponent({ components: { Banner }, data (): IData { return { bannerList: [] } }, mounted () { getBannerList().then(res => { console.log(res.data) this.bannerList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
渲染轮播图组件
<!-- src/views/home/components/Banner.vue --> <template> <div class="my-swipers"> <van-swipe :autoplay="3000" indicator-color="white"> <van-swipe-item v-for="item of bannerList" :key="item.bannerid"> <van-image fit="fill" class="banner-img" :src="item.img" /> </van-swipe-item> </van-swipe> </div> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType} from 'vue' import type { IBanner } from '../home' export default defineComponent({ props: { // bannerList: Array, // js环境 bannerList: Array as PropType<IBanner[]> // ts环境 } }) </script> <style lang="stylus"> .my-swipers width 94% height 1.6rem margin-left 3% background-color #00f overflow hidden border-radius 10px .van-swipe height 100% .banner-img width 100% height 100% </style>
5.7.3 构建nav导航组件以及渲染
-
构建nav导航组件
<!-- src/views/home/components/Nav.vue --> <template> <div>nav</div> </template>
-
首页引入以及注册nav导航组件
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> <Nav/> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import { getBannerList } from '@/api/home' import type { IBanner } from './home' interface IData { bannerList: IBanner[] } export default defineComponent({ components: { Banner, Nav }, data (): IData { return { bannerList: [] } }, mounted () { getBannerList().then(res => { console.log(res.data) this.bannerList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
首页准备nav导航数据并且传递
// src/views/home/home.d.ts export interface IBanner { bannerid: string alt: string link: string flag: boolean img: string } export interface INav { navid: number title: string imgurl: string }
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> <Nav :navList="navList"/> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import { getBannerList } from '@/api/home' import type { IBanner, INav } from './home' interface IData { bannerList: IBanner[] navList: INav[] } export default defineComponent({ components: { Banner, Nav }, data (): IData { return { bannerList: [], navList: [ { navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' }, { navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' }, { navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' }, { navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' }, { navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' }, { navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' }, { navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' }, { navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' }, { navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' }, { navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' } ] } }, mounted () { getBannerList().then(res => { console.log(res.data) this.bannerList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
nav导航组件的渲染
<!-- src/views/home/components/Nav.vue --> <template> <van-grid :column-num="5" icon-size="44" :border="false" :square="true"> <van-grid-item v-for="item in navList" :key="item.navid" :icon="item.imgurl" :text="item.title" /> </van-grid> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType } from 'vue' import type { INav } from '../home' export default defineComponent({ props: { navList: Array as PropType<INav[]> } }) </script>
5.7.4 构建秒杀列表组件以及渲染
-
构建秒杀列表组件
<!-- src/views/home/components/Seckill.vue --> <template> <div>秒杀</div> </template> <script lang='ts'> import { defineComponent } from 'vue'; export default defineComponent({}) </script> <style lang='stylus'> </style>
-
首页组件引入以及注册秒杀列表组件
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> <Nav :navList="navList"/> <Seckill /> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import Seckill from './components/Seckill.vue' import { getBannerList } from '@/api/home' import type { IBanner, INav } from './home' interface IData { bannerList: IBanner[] navList: INav[] } export default defineComponent({ components: { Banner, Nav, Seckill }, data (): IData { return { bannerList: [], navList: [ { navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' }, { navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' }, { navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' }, { navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' }, { navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' }, { navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' }, { navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' }, { navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' }, { navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' }, { navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' } ] } }, mounted () { getBannerList().then(res => { console.log(res.data) this.bannerList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
首页请求秒杀数据以及传递
// src/views/home/home.d.ts export interface IBanner { bannerid: string alt: string link: string flag: boolean img: string } export interface INav { navid: number title: string imgurl: string } export interface IPro { banners: string[] brand: string category: string desc: string discount: number img1: string img2: string img3: string img4: string isrecommend: number issale: number isseckill: number originprice: number proid: string proname: string sales: number stock: number }
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> <Nav :navList="navList"/> <Seckill :seckillList="seckillList"/> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import Seckill from './components/Seckill.vue' import { getBannerList, getSeckilllist } from '@/api/home' import type { IBanner, INav, IPro } from './home' interface IData { bannerList: IBanner[] navList: INav[] seckillList: IPro[] } export default defineComponent({ components: { Banner, Nav, Seckill }, data (): IData { return { bannerList: [], navList: [ { navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' }, { navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' }, { navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' }, { navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' }, { navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' }, { navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' }, { navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' }, { navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' }, { navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' }, { navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' } ], seckillList: [] } }, mounted () { getBannerList().then(res => { // console.log(res.data) this.bannerList = res.data.data }) getSeckilllist().then(res => { console.log(res.data) this.seckillList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
秒杀列表组件渲染
<!-- src/views/home/components/Seckill.vue --> <template> <div class="seckillList"> <h4> 嗨购秒杀 </h4> <ul class="list"> <li v-for="item of seckillList" :key="item.proid"> <van-image :src="item.img1"></van-image> <p>¥{{ item.originprice }}</p> </li> </ul> </div> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType } from 'vue'; import type { IPro } from '../home'; export default defineComponent({ props: { seckillList: Array as PropType<IPro[]> } }) </script> <style lang='stylus'> .seckillList height 1.1rem overflow hidden background-color #fff margin-top 10px .list width 100% display flex li flex 1 .van-image width 0.6rem height 0.6rem p text-align center color #f66 </style>
作业:实现倒计时
5.7.5 构建产品列表组件以及渲染
-
构建产品列表组件
<!-- src/views/home/components/Pro.vue --> <template> <div>产品列表</div> </template> <script lang='ts'> import { defineComponent } from 'vue'; export default defineComponent({}) </script> <style lang='stylus'> </style>
-
注册组件以及使用产品列表组件
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> <Nav :navList="navList"/> <Seckill :seckillList="seckillList"/> <Pro/> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import Seckill from './components/Seckill.vue' import Pro from './components/Pro.vue' import { getBannerList, getSeckilllist } from '@/api/home' import type { IBanner, INav, IPro } from './home' interface IData { bannerList: IBanner[] navList: INav[] seckillList: IPro[] } export default defineComponent({ components: { Banner, Nav, Seckill, Pro }, data (): IData { return { bannerList: [], navList: [ { navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' }, { navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' }, { navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' }, { navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' }, { navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' }, { navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' }, { navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' }, { navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' }, { navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' }, { navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' } ], seckillList: [] } }, mounted () { getBannerList().then(res => { // console.log(res.data) this.bannerList = res.data.data }) getSeckilllist().then(res => { // console.log(res.data) this.seckillList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
请求列表组件的数据以及传递
<!-- src/views/home/index.vue --> <template> <div class="box"> <header class="header">home header</header> <div class="content"> <Banner :bannerList = "bannerList"/> <Nav :navList="navList"/> <Seckill :seckillList="seckillList"/> <Pro :proList="proList"/> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import Seckill from './components/Seckill.vue' import Pro from './components/Pro.vue' import { getBannerList, getSeckilllist, getProList } from '@/api/home' import type { IBanner, INav, IPro } from './home' interface IData { bannerList: IBanner[] navList: INav[] seckillList: IPro[] proList: IPro[] } export default defineComponent({ components: { Banner, Nav, Seckill, Pro }, data (): IData { return { bannerList: [], navList: [ { navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' }, { navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' }, { navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' }, { navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' }, { navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' }, { navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' }, { navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' }, { navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' }, { navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' }, { navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' } ], seckillList: [], proList: [] } }, mounted () { getBannerList().then(res => { // console.log(res.data) this.bannerList = res.data.data }) getSeckilllist().then(res => { // console.log(res.data) this.seckillList = res.data.data }) getProList().then(res => { // console.log(res.data) this.proList = res.data.data }) } }) </script> <style lang='stylus'> </style>
-
渲染列表组件
<!-- src/views/home/components/Pro.vue --> <template> <ul class="proList"> <li class="proItem" v-for="item of proList" :key="item.proid"> <div class="itemImage"> <van-image :src="item.img1"></van-image> </div> <div class="itemInfo"> <div class="title van-multi-ellipsis--l2">{{ item.proname }}</div> <div class="price">¥{{ item.originprice }}</div> <div class="other"> <van-tag type="danger">{{ item.category }}</van-tag> </div> </div> </li> <!-- <li class="proItem"> <div class="itemImage"> <van-image src=""></van-image> </div> <div class="itemInfo"> <div class="title">产品名称</div> <div class="price">¥1999</div> <div class="other">苹果</div> </div> </li> <li class="proItem"> <div class="itemImage"> <van-image src=""></van-image> </div> <div class="itemInfo"> <div class="title">产品名称</div> <div class="price">¥1999</div> <div class="other">苹果</div> </div> </li> --> </ul> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType } from 'vue' import type { IPro } from '../home' export default defineComponent({ props: { proList: Array as PropType<IPro[]> } }) </script> <style lang='stylus'> .proList display flex flex-wrap wrap .proItem width 46% margin 8px 2% min-height 2.6rem background-color #fff border-radius 10px overflow hidden .itemImage width 100% height 1.9rem .van-image width 100% height 100% display block .itemInfo padding 10px .price color #f66 margin-top 5px .other margin-top 5px </style>
因为列表的数据展示 内容超过了容器的高度,此时将 App.vue 中的box 以及 content中设置 overflow:auto
5.7.6 实现上拉加载功能
List 组件通过 loading
和 finished
两个变量控制加载状态,当组件滚动到底部时,会触发 load
事件并将 loading
设置成 true
。此时可以发起异步操作并更新数据,数据更新完毕后,将 loading
设置成 false
即可。若数据已全部加载完毕,则直接将 finished
设置成 true
即可。
隐藏条件 ,每页页码
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<header class="header">home header</header>
<div class="content">
<Banner :bannerList = "bannerList"/>
<Nav :navList="navList"/>
<Seckill :seckillList="seckillList"/>
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<Pro :proList="proList"/>
</van-list>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
import Banner from './components/Banner.vue'
import Nav from './components/Nav.vue'
import Seckill from './components/Seckill.vue'
import Pro from './components/Pro.vue'
import { getBannerList, getSeckilllist, getProList } from '@/api/home'
import type { IBanner, INav, IPro } from './home'
interface IData {
bannerList: IBanner[]
navList: INav[]
seckillList: IPro[]
proList: IPro[]
loading: boolean
finished: boolean
count: number
}
export default defineComponent({
components: {
Banner,
Nav,
Seckill,
Pro
},
data (): IData {
return {
bannerList: [],
navList: [
{ navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' },
{ navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' },
{ navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' },
{ navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' },
{ navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' },
{ navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' },
{ navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' },
{ navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' },
{ navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' },
{ navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' }
],
seckillList: [],
proList: [],
loading: false,
finished: false,
count: 2 // 默认已经请求第一页的数据,下一次从2开始
}
},
mounted () {
getBannerList().then(res => {
// console.log(res.data)
this.bannerList = res.data.data
})
getSeckilllist().then(res => {
// console.log(res.data)
this.seckillList = res.data.data
})
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
})
},
methods: {
onLoad () {
this.loading = true
getProList({ count: this.count, limitNum: 10 }).then(res => {
if (res.data.data.length === 0) {
// 没有数据了
this.finished = true
} else {
// 有数据 合并数据
this.proList = [...this.proList, ...res.data.data ]
this.count++
}
this.loading = false
})
}
}
})
</script>
<style lang='stylus'>
</style>
5.7.7 实现下拉刷新功能
下拉刷新时会触发 refresh
事件,在事件的回调函数中可以进行同步或异步操作,操作完成后将 v-model
设置为 false
,表示加载完成。
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<header class="header">home header</header>
<div class="content">
<van-pull-refresh v-model="loading" @refresh="onRefresh">
<Banner :bannerList = "bannerList"/>
<Nav :navList="navList"/>
<Seckill :seckillList="seckillList"/>
<van-list
:immediate-check="false"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<Pro :proList="proList"/>
</van-list>
</van-pull-refresh>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
import Banner from './components/Banner.vue'
import Nav from './components/Nav.vue'
import Seckill from './components/Seckill.vue'
import Pro from './components/Pro.vue'
import { getBannerList, getSeckilllist, getProList } from '@/api/home'
import type { IBanner, INav, IPro } from './home'
interface IData {
bannerList: IBanner[]
navList: INav[]
seckillList: IPro[]
proList: IPro[]
loading: boolean
finished: boolean
count: number
}
export default defineComponent({
components: {
Banner,
Nav,
Seckill,
Pro
},
data (): IData {
return {
bannerList: [],
navList: [
{ navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' },
{ navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' },
{ navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' },
{ navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' },
{ navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' },
{ navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' },
{ navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' },
{ navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' },
{ navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' },
{ navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' }
],
seckillList: [],
proList: [],
loading: false,
finished: false,
count: 2 // 默认已经请求第一页的数据,下一次从2开始
}
},
mounted () {
getBannerList().then(res => {
// console.log(res.data)
this.bannerList = res.data.data
})
getSeckilllist().then(res => {
// console.log(res.data)
this.seckillList = res.data.data
})
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
})
},
methods: {
onLoad () {
this.loading = true
getProList({ count: this.count, limitNum: 10 }).then(res => {
if (res.data.data.length === 0) {
// 没有数据了
this.finished = true
} else {
// 有数据 合并数据
this.proList = [...this.proList, ...res.data.data ]
this.count++
}
this.loading = false
})
},
onRefresh () {
this.loading = true
// 下拉刷新其实就是请求第一页的数据,要记得重置状态
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
this.count = 2
this.finished = false
this.loading = false
})
}
}
})
</script>
<style lang='stylus'>
</style>
5.7.8 实现返回顶部功能
分析清除到底是哪一个容器产生了滚动条
分析得知 content 容器产生了滚动条,可以给它绑定一个 scroll 事件用于判断 回到顶部按钮显示还是不显示
通过 content 的dom的scrollTop 属性可以设置滚动条距离
当使用vantui组件库4版本时使用如下代码
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<header class="header">home header</header>
<div class="content">
<van-pull-refresh v-model="loading" @refresh="onRefresh">
<Banner :bannerList = "bannerList"/>
<Nav :navList="navList"/>
<Seckill :seckillList="seckillList"/>
<van-list
:immediate-check="false"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<Pro :proList="proList"/>
</van-list>
</van-pull-refresh>
<van-back-top target=".content" bottom="10vh" />
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
import Banner from './components/Banner.vue'
import Nav from './components/Nav.vue'
import Seckill from './components/Seckill.vue'
import Pro from './components/Pro.vue'
import { getBannerList, getSeckilllist, getProList } from '@/api/home'
import type { IBanner, INav, IPro } from './home'
interface IData {
bannerList: IBanner[]
navList: INav[]
seckillList: IPro[]
proList: IPro[]
loading: boolean
finished: boolean
count: number
}
export default defineComponent({
components: {
Banner,
Nav,
Seckill,
Pro
},
data (): IData {
return {
bannerList: [],
navList: [
{ navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' },
{ navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' },
{ navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' },
{ navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' },
{ navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' },
{ navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' },
{ navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' },
{ navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' },
{ navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' },
{ navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' }
],
seckillList: [],
proList: [],
loading: false,
finished: false,
count: 2 // 默认已经请求第一页的数据,下一次从2开始
}
},
mounted () {
getBannerList().then(res => {
// console.log(res.data)
this.bannerList = res.data.data
})
getSeckilllist().then(res => {
// console.log(res.data)
this.seckillList = res.data.data
})
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
})
},
methods: {
onLoad () {
this.loading = true
getProList({ count: this.count, limitNum: 10 }).then(res => {
if (res.data.data.length === 0) {
// 没有数据了
this.finished = true
} else {
// 有数据 合并数据
this.proList = [...this.proList, ...res.data.data ]
this.count++
}
this.loading = false
})
},
onRefresh () {
this.loading = true
// 下拉刷新其实就是请求第一页的数据,要记得重置状态
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
this.count = 2
this.finished = false
this.loading = false
})
}
}
})
</script>
<style lang='stylus'>
</style>
如果使用4版本以前,参考如下代码
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<header class="header">home header</header>
<div class="content" @scroll="onScroll" ref="content">
<van-pull-refresh v-model="loading" @refresh="onRefresh">
<Banner :bannerList = "bannerList"/>
<Nav :navList="navList"/>
<Seckill :seckillList="seckillList"/>
<van-list
:immediate-check="false"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<Pro :proList="proList"/>
</van-list>
</van-pull-refresh>
<!-- <van-back-top target=".content" bottom="10vh" /> -->
<div class="backTop" @click="backTop" v-if="scrollTop > 300">
<van-icon name="back-top" size="28"/>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
import Banner from './components/Banner.vue'
import Nav from './components/Nav.vue'
import Seckill from './components/Seckill.vue'
import Pro from './components/Pro.vue'
import { getBannerList, getSeckilllist, getProList } from '@/api/home'
import type { IBanner, INav, IPro } from './home'
interface IData {
bannerList: IBanner[]
navList: INav[]
seckillList: IPro[]
proList: IPro[]
loading: boolean
finished: boolean
count: number
scrollTop: number
}
export default defineComponent({
components: {
Banner,
Nav,
Seckill,
Pro
},
data (): IData {
return {
bannerList: [],
navList: [
{ navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' },
{ navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' },
{ navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' },
{ navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' },
{ navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' },
{ navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' },
{ navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' },
{ navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' },
{ navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' },
{ navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' }
],
seckillList: [],
proList: [],
loading: false,
finished: false,
count: 2, // 默认已经请求第一页的数据,下一次从2开始
scrollTop: 0
}
},
mounted () {
getBannerList().then(res => {
// console.log(res.data)
this.bannerList = res.data.data
})
getSeckilllist().then(res => {
// console.log(res.data)
this.seckillList = res.data.data
})
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
})
},
methods: {
onLoad () {
this.loading = true
getProList({ count: this.count, limitNum: 10 }).then(res => {
if (res.data.data.length === 0) {
// 没有数据了
this.finished = true
} else {
// 有数据 合并数据
this.proList = [...this.proList, ...res.data.data ]
this.count++
}
this.loading = false
})
},
onRefresh () {
this.loading = true
// 下拉刷新其实就是请求第一页的数据,要记得重置状态
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
this.count = 2
this.finished = false
this.loading = false
})
},
onScroll () {
this.scrollTop = (this.$refs.content as HTMLDivElement).scrollTop
},
backTop () {
(this.$refs.content as HTMLDivElement).scrollTop = 0
}
}
})
</script>
<style lang='stylus'>
.backTop
position fixed
bottom 0.6rem
right 10px
width 36px
height 36px
background-color #fff
border 1px solid #efefef
border-radius 50%
display flex
justify-content center
align-items center
user-select none
</style>
5.7.9 自定义头部
-
自定义头部组件
<!-- src/views/home/components/Header.vue --> <template> <header class="header"> header </header> </template>
-
注册及使用组件
<!-- src/views/home/index.vue --> <template> <div class="box"> <!-- <header class="header">home header</header> --> <Header /> <div class="content" @scroll="onScroll" ref="content"> <van-pull-refresh v-model="loading" @refresh="onRefresh"> <Banner :bannerList = "bannerList"/> <Nav :navList="navList"/> <Seckill :seckillList="seckillList"/> <van-list :immediate-check="false" v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad" > <Pro :proList="proList"/> </van-list> </van-pull-refresh> <!-- <van-back-top target=".content" bottom="10vh" /> --> <div class="backTop" @click="backTop" v-if="scrollTop > 300"> <van-icon name="back-top" size="28"/> </div> </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue' import Banner from './components/Banner.vue' import Nav from './components/Nav.vue' import Seckill from './components/Seckill.vue' import Pro from './components/Pro.vue' import Header from './components/Header.vue' import { getBannerList, getSeckilllist, getProList } from '@/api/home' import type { IBanner, INav, IPro } from './home' interface IData { bannerList: IBanner[] navList: INav[] seckillList: IPro[] proList: IPro[] loading: boolean finished: boolean count: number scrollTop: number } export default defineComponent({ components: { Banner, Nav, Seckill, Pro, Header }, data (): IData { return { bannerList: [], navList: [ { navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' }, { navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' }, { navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' }, { navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' }, { navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' }, { navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' }, { navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' }, { navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' }, { navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' }, { navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' } ], seckillList: [], proList: [], loading: false, finished: false, count: 2, // 默认已经请求第一页的数据,下一次从2开始 scrollTop: 0 } }, mounted () { getBannerList().then(res => { // console.log(res.data) this.bannerList = res.data.data }) getSeckilllist().then(res => { // console.log(res.data) this.seckillList = res.data.data }) getProList().then(res => { // console.log(res.data) this.proList = res.data.data }) }, methods: { onLoad () { this.loading = true getProList({ count: this.count, limitNum: 10 }).then(res => { if (res.data.data.length === 0) { // 没有数据了 this.finished = true } else { // 有数据 合并数据 this.proList = [...this.proList, ...res.data.data ] this.count++ } this.loading = false }) }, onRefresh () { this.loading = true // 下拉刷新其实就是请求第一页的数据,要记得重置状态 getProList().then(res => { // console.log(res.data) this.proList = res.data.data this.count = 2 this.finished = false this.loading = false }) }, onScroll () { this.scrollTop = (this.$refs.content as HTMLDivElement).scrollTop }, backTop () { (this.$refs.content as HTMLDivElement).scrollTop = 0 } } }) </script> <style lang='stylus'> .backTop position fixed bottom 0.6rem right 10px width 36px height 36px background-color #fff border 1px solid #efefef border-radius 50% display flex justify-content center align-items center user-select none </style>
-
完善头部组件
<!-- src/views/home/components/Header.vue --> <template> <header class="header"> <ul> <li>太原</li> <li> <div class="searchBox"> <van-image :src="logo" height="24"></van-image> <span class="divider ">|</span> <van-icon name="search" size="24"/> <span class="searchText">游戏主机</span> </div> </li> <li>登录</li> </ul> </header> </template> <script lang='ts'> import { defineComponent } from 'vue'; import logo from '@/assets/logo.png' export default defineComponent({ data () { return { logo } } }) </script> <style lang='stylus'> .header { ul { width: 100%; height: 100%; display: flex; li { height: 100%; display: flex; justify-content: center; align-items: center; color: #fff; &:nth-child(1), &:nth-child(3) { width: 50px; } &:nth-child(2) { flex: 1; .searchBox { width: 100%; height: 70%; background-color: #fff; border-radius: 16px; color: #666; display: flex; .van-image { width: 32px; margin-top: 4px; margin-left: 10px; } .divider { width: 12px; font-size: 22px; margin-left: 10px; color: #999; } .van-icon { width: 24px; display: flex; justify-content: center; align-items: center; } .searchText { flex: 1; line-height: .31rem; display: flex; align-items: center; } } } } } } </style>
5.7.10 优化代码
// src/views/home/components/index.ts
// import Banner from './Banner.vue'
// import Nav from './Nav.vue'
// import Seckill from './Seckill.vue'
// import Pro from './Pro.vue'
// import Header from './Header.vue'
// export {
// Banner,
// Nav,
// Seckill,
// Pro,
// Header
// }
export { default as Banner } from './Banner.vue'
export { default as Nav } from './Nav.vue'
export { default as Seckill } from './Seckill.vue'
export { default as Pro } from './Pro.vue'
export { default as Header } from './Header.vue'
<!-- src/views/home/index.vue -->
<template>
<div class="box">
<!-- <header class="header">home header</header> -->
<Header />
<div class="content" @scroll="onScroll" ref="content">
<van-pull-refresh v-model="loading" @refresh="onRefresh">
<Banner :bannerList = "bannerList"/>
<Nav :navList="navList"/>
<Seckill :seckillList="seckillList"/>
<van-list
:immediate-check="false"
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<Pro :proList="proList"/>
</van-list>
</van-pull-refresh>
<!-- <van-back-top target=".content" bottom="10vh" /> -->
<div class="backTop" @click="backTop" v-if="scrollTop > 300">
<van-icon name="back-top" size="28"/>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
// import Banner from './components/Banner.vue'
// import Nav from './components/Nav.vue'
// import Seckill from './components/Seckill.vue'
// import Pro from './components/Pro.vue'
// import Header from './components/Header.vue'
import { Banner, Nav, Seckill, Pro, Header } from './components/index'
import { getBannerList, getSeckilllist, getProList } from '@/api/home'
import type { IBanner, INav, IPro } from './home'
interface IData {
bannerList: IBanner[]
navList: INav[]
seckillList: IPro[]
proList: IPro[]
loading: boolean
finished: boolean
count: number
scrollTop: number
}
export default defineComponent({
components: {
Banner,
Nav,
Seckill,
Pro,
Header
},
data (): IData {
return {
bannerList: [],
navList: [
{ navid: 1, title: '嗨购超市', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/125678/35/5947/4868/5efbf28cEbf04a25a/e2bcc411170524f0.png' },
{ navid: 2, title: '数码电器', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/178015/31/13828/6862/60ec0c04Ee2fd63ac/ccf74d805a059a44.png' },
{ navid: 3, title: '嗨购服饰', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/41867/2/15966/7116/60ec0e0dE9f50d596/758babcb4f911bf4.png' },
{ navid: 4, title: '嗨购生鲜', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/177902/16/13776/5658/60ec0e71E801087f2/a0d5a68bf1461e6d.png' },
{ navid: 5, title: '嗨购到家', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196472/7/12807/7127/60ec0ea3Efe11835b/37c65625d94cae75.png' },
{ navid: 6, title: '充值缴费', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/185733/21/13527/6648/60ec0f31E0fea3e0a/d86d463521140bb6.png' },
{ navid: 7, title: '9.9元拼', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/36069/14/16068/6465/60ec0f67E155f9488/595ff3e606a53f02.png' },
{ navid: 8, title: '领券', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/186080/16/13681/8175/60ec0fcdE032af6cf/c5acd2f8454c40e1.png' },
{ navid: 9, title: '领金贴', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/196711/35/12751/6996/60ec1000E21b5bab4/38077313cb9eac4b.png' },
{ navid: 10, title: 'plus会员', imgurl: 'https://m.360buyimg.com/mobilecms/s120x120_jfs/t1/37709/6/15279/6118/60ec1046E4b5592c6/a7d6b66354efb141.png' }
],
seckillList: [],
proList: [],
loading: false,
finished: false,
count: 2, // 默认已经请求第一页的数据,下一次从2开始
scrollTop: 0
}
},
mounted () {
getBannerList().then(res => {
// console.log(res.data)
this.bannerList = res.data.data
})
getSeckilllist().then(res => {
// console.log(res.data)
this.seckillList = res.data.data
})
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
})
},
methods: {
onLoad () {
this.loading = true
getProList({ count: this.count, limitNum: 10 }).then(res => {
if (res.data.data.length === 0) {
// 没有数据了
this.finished = true
} else {
// 有数据 合并数据
this.proList = [...this.proList, ...res.data.data ]
this.count++
}
this.loading = false
})
},
onRefresh () {
this.loading = true
// 下拉刷新其实就是请求第一页的数据,要记得重置状态
getProList().then(res => {
// console.log(res.data)
this.proList = res.data.data
this.count = 2
this.finished = false
this.loading = false
})
},
onScroll () {
this.scrollTop = (this.$refs.content as HTMLDivElement).scrollTop
},
backTop () {
(this.$refs.content as HTMLDivElement).scrollTop = 0
}
}
})
</script>
<style lang='stylus'>
.backTop
position fixed
bottom 0.6rem
right 10px
width 36px
height 36px
background-color #fff
border 1px solid #efefef
border-radius 50%
display flex
justify-content center
align-items center
user-select none
</style>
5.8 实现详情
5.8.1 构建详情页面以及详情路由
/detail/1 ======> /detail/:proid =====> this.$route.params.proid ====> params
/detail?proid=1 ======> /detail =====> this.$route.query.proid ====> query
vue中推荐大家使用 params 形式
-
构建详情页面组件
<!-- src/views/detail/index.vue --> <template> <header class="header">detail header</header> <div class="content">detail content</div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({}) </script> <style lang="stylus"></style>
-
添加详情路由
// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' import type { RouteRecordRaw, Router } from 'vue-router' // 直接引入组件,不管你用不用这个页面,我都引入了 --- 静态导入 import Home from '@/views/home/index.vue' // import Kind from '@/views/kind/index.vue' // import Cart from '@/views/cart/index.vue' // import User from '@/views/user/index.vue' // 使用路由懒加载 - 访问该路由时才加载该组件 --- 动态导入 const Kind = () => import('@/views/kind/index.vue') const Cart = () => import('@/views/cart/index.vue') const User = () => import('@/views/user/index.vue') const Detail = () => import('@/views/detail/index.vue') const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/home' }, { path: '/home', name: 'home', component: Home }, { path: '/kind', name: 'kind', component: Kind }, { path: '/cart', name: 'cart', component: Cart }, { path: '/user', name: 'user', component: User }, { path: '/detail/:proid', name: 'detail', component: Detail } ] const router: Router = createRouter({ history: createWebHistory(), routes }) export default router
地址栏输入 http://localhost:5173/detail/100
但是此时发现,详情页面底部应该隐藏原来的底部的选项卡
5.8.2 改造路由实现页面底部自由
以上案例中 一个路由控制了一个区域的组件,那么可以一个路由控制多个区域的变化吗?
命名视图:https://router.vuejs.org/zh/guide/essentials/named-views.html
<router-view></router-view> <router-view name="footer"></router-view>
{ path: '', name: '', components: { default: '', footer: '' } }
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router'
// 直接引入组件,不管你用不用这个页面,我都引入了 --- 静态导入
import Home from '@/views/home/index.vue'
import Footer from '@/components/Footer.vue'
// import Kind from '@/views/kind/index.vue'
// import Cart from '@/views/cart/index.vue'
// import User from '@/views/user/index.vue'
// 使用路由懒加载 - 访问该路由时才加载该组件 --- 动态导入
const Kind = () => import('@/views/kind/index.vue')
const Cart = () => import('@/views/cart/index.vue')
const User = () => import('@/views/user/index.vue')
const Detail = () => import('@/views/detail/index.vue')
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
// component: Home
components: {
default: Home,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: Kind
components: {
default: Kind,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: Cart
components: {
default: Cart,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: User
components: {
default: User,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
component: Detail
}
]
const router: Router = createRouter({
history: createWebHistory(),
routes
})
export default router
<!-- src/App.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
// import Footer from '@/components/Footer.vue'
export default defineComponent({
components: {
// Footer
}
})
</script>
<template>
<div class="container">
<!-- <div class="box">
<header class="header">header</header>
<div class="content">content</div>
</div> -->
<RouterView />
<!-- <footer class="footer">footer</footer> -->
<!-- <Footer /> -->
<RouterView name="footer"></RouterView>
</div>
<div class="landscape-tip">
请将屏幕竖向浏览
</div>
</template>
<style lang="stylus">
.container
height 100%
display flex
flex-direction column
.box
flex 1
display flex
flex-direction column
overflow auto
.header
height 0.44rem
background-color #f66
.content
flex 1
overflow auto
.footer
height 0.5rem
background-color #efefef
user-select none
ul
width 100%
height 100%
display flex
li
flex 1
display flex
flex-direction column
justify-content center
align-items center
&.active
color #f66
span
font-size 0.24rem
.landscape-tip
position fixed
top 0
right 0
bottom 0
left 0
background-color rgba(0, 0, 0, .8)
color #fff
display none
justify-content center
align-items center
@media only screen and (orientation landscape) // 横屏
// 横屏状态下使用flex布局,其他情况不显示
.landscape-tip
display flex
</style>
5.8.3 点击列表进入产品详情
-
点击秒杀列表声明式跳转至详情
<a href=""></a>
<router-link to="/detail/1"></router-link>
<router-link :to="{ name: 'detail', params: { proid: 1 }}"></router-link>
<router-link :to="{ path: 'detail/' + 1}"></router-link>
<router-link :to=
/detail/${1}></router-link>
<!-- src/views/home/components/Seckill.vue --> <template> <div class="seckillList"> <h4> 嗨购秒杀 </h4> <ul class="list"> <!-- <RouterLink custom v-slot="{ href, navigate }" v-for="item of seckillList" :key="item.proid" :to="{ name: 'detail', params: { proid: item.proid }}" > <li :href="href" @click="navigate"> <van-image :src="item.img1"></van-image> <p>¥{{ item.originprice }}</p> </li> </RouterLink> --> <!-- <RouterLink custom v-slot="{ href, navigate }" v-for="item of seckillList" :key="item.proid" :to="{ path: '/detail/' + item.proid }" > <li :href="href" @click="navigate"> <van-image :src="item.img1"></van-image> <p>¥{{ item.originprice }}</p> </li> </RouterLink> --> <!-- <RouterLink custom v-slot="{ href, navigate }" v-for="item of seckillList" :key="item.proid" :to="'/detail/' + item.proid" > <li :href="href" @click="navigate"> <van-image :src="item.img1"></van-image> <p>¥{{ item.originprice }}</p> </li> </RouterLink> --> <RouterLink custom v-slot="{ href, navigate }" v-for="item of seckillList" :key="item.proid" :to="`/detail/${item.proid}`" > <li :href="href" @click="navigate"> <van-image :src="item.img1"></van-image> <p>¥{{ item.originprice }}</p> </li> </RouterLink> </ul> </div> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType } from 'vue'; import type { IPro } from '../home'; export default defineComponent({ props: { seckillList: Array as PropType<IPro[]> } }) </script> <style lang='stylus'> .seckillList height 1.1rem overflow hidden background-color #fff margin-top 10px padding 5px 10px .list width 100% display flex li flex 1 margin 3px .van-image width 100% height 0.55rem p text-align center color #f66 </style>
-
点击产品列表编程式跳转至详情
window.location.href=""
this.$router.push('/detail/1')
this.$router.push({ name: 'detail', params: { proid: 1}})
this.$router.push({ path: '/detail/1'})
<!-- src/views/home/components/Pro.vue --> <template> <ul class="proList"> <li class="proItem" v-for="item of proList" :key="item.proid" @click="toDetail(item.proid)"> <div class="itemImage"> <van-image :src="item.img1"></van-image> </div> <div class="itemInfo"> <div class="title van-multi-ellipsis--l2">{{ item.proname }}</div> <div class="price">¥{{ item.originprice }}</div> <div class="other"> <van-tag type="danger">{{ item.category }}</van-tag> </div> </div> </li> <!-- <li class="proItem"> <div class="itemImage"> <van-image src=""></van-image> </div> <div class="itemInfo"> <div class="title">产品名称</div> <div class="price">¥1999</div> <div class="other">苹果</div> </div> </li> <li class="proItem"> <div class="itemImage"> <van-image src=""></van-image> </div> <div class="itemInfo"> <div class="title">产品名称</div> <div class="price">¥1999</div> <div class="other">苹果</div> </div> </li> --> </ul> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType } from 'vue' import type { IPro } from '../home' export default defineComponent({ props: { proList: Array as PropType<IPro[]> }, methods: { toDetail (proid: string) { this.$router.push(`/detail/${proid}`) // this.$router.push('/detail/' + proid) // this.$router.push({ path: '/detail/' + proid }) // this.$router.push({ name: 'detail', params: { proid: proid } }) } } }) </script> <style lang='stylus'> .proList display flex flex-wrap wrap .proItem width 46% margin 8px 2% min-height 2.6rem background-color #fff border-radius 10px overflow hidden .itemImage width 100% height 1.9rem .van-image width 100% height 100% display block .itemInfo padding 10px .price color #f66 margin-top 5px .other margin-top 5px </style>
5.8.4 详情页获取路由参数
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header">detail header</header>
<div class="content">detail content</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
interface IData {
proid: string
}
export default defineComponent({
data (): IData {
return {
proid: ''
}
},
mounted () {
console.log(this.$route.params)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
}
})
</script>
<style lang="stylus"></style>
5.8.5 封装详情页数据请求
// src/api/detail.ts
import request from '@/utils/request'
interface IPager {
count: number
limitNum?: number
}
export function getProDetail (proid: string) {
return request.get('/pro/detail/' + proid)
}
// 详情 猜你喜欢 - 推荐
export function getRecommendList (params?: IPager) {
return request.get('/pro/recommendlist', { params })
}
5.8.6 渲染详情页面
- 请求数据
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header">detail header</header>
<div class="content">detail content</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getProDetail } from '@/api/detail'
interface IData {
proid: string
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proname: string
sales: number
stock: number
}
export default defineComponent({
data (): IData {
return {
proid: '',
banners: [],
brand: '',
category: '',
desc: '',
discount: 0,
img1: '',
isrecommend: 0,
issale: 0,
isseckill: 0,
originprice: 0,
proname: '',
sales: 0,
stock: 0
}
},
mounted () {
console.log(this.$route.params)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
getProDetail(this.proid).then(res => {
console.log(res.data.data)
this.banners = res.data.data.banners[0].split(',')
this.brand = res.data.data.brand
this.category = res.data.data.category
this.desc = res.data.data.desc
this.discount = res.data.data.discount
this.img1 = res.data.data.img1
this.isrecommend = res.data.data.isrecommend
this.issale = res.data.data.issale
this.isseckill = res.data.data.isseckill
this.originprice = res.data.data.originprice
this.proname = res.data.data.proname
this.sales = res.data.data.sales
this.stock = res.data.data.stock
})
}
})
</script>
<style lang="stylus"></style>
5.8.7 渲染轮播图数据
-
轮播图组件
<!-- src/views/detail/components/Banner.vue --> <template> <div class="detail-swipers"> <van-swipe indicator-color="white" @change="changeIndex" :initial-swipe="current"> <van-swipe-item v-for="(item, index) of list" :key="index" @click="previewImage(index)"> <van-image fit="fill" class="banner-img" :src="item" /> </van-swipe-item> </van-swipe> <div class="indicator-tip"> {{ current + 1 }} / {{ total }} </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType} from 'vue' import { showImagePreview } from 'vant' export default defineComponent({ props: { list: Array as PropType<string[]> // ts环境 }, data () { return { current: 0 } }, computed: { total () { return this.list!.length } }, methods: { changeIndex (index: number) { this.current = index }, previewImage (index: number) { showImagePreview({ images: this.list!, startPosition: index, onChange: (idx: number) => { // 保证大图预览完毕 序号要一致 给 轮播图设置 initial-swipe 属性 console.log('666', idx) this.current = idx } }) } } }) </script> <style lang="stylus"> .detail-swipers width 100% height 2.6rem background-color #00f overflow hidden position relative .van-swipe height 100% .banner-img width 100% height 100% .indicator-tip position absolute bottom 20px right 0 width 50px height 30px background-color rgba(0, 0, 0, 0.5) border-radius 15px 0 0 15px color #fff display flex justify-content center align-items center </style>
-
详情页导入轮播图
<!-- src/views/detail/index.vue --> <template> <div class="box"> <header class="header">detail header</header> <div class="content"> <Banner :list="banners" /> </div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { getProDetail } from '@/api/detail' import { Banner } from './components' interface IData { proid: string banners: string[] brand: string category: string desc: string discount: number img1: string isrecommend: number issale: number isseckill: number originprice: number proname: string sales: number stock: number } export default defineComponent({ components: { Banner }, data (): IData { return { proid: '', banners: [], brand: '', category: '', desc: '', discount: 0, img1: '', isrecommend: 0, issale: 0, isseckill: 0, originprice: 0, proname: '', sales: 0, stock: 0 } }, mounted () { console.log(this.$route.params) // this.proid = String(this.$route.params.proid) this.proid = this.$route.params.proid as string getProDetail(this.proid).then(res => { console.log(res.data.data) this.banners = res.data.data.banners[0].split(',') this.brand = res.data.data.brand this.category = res.data.data.category this.desc = res.data.data.desc this.discount = res.data.data.discount this.img1 = res.data.data.img1 this.isrecommend = res.data.data.isrecommend this.issale = res.data.data.issale this.isseckill = res.data.data.isseckill this.originprice = res.data.data.originprice this.proname = res.data.data.proname this.sales = res.data.data.sales this.stock = res.data.data.stock }) } }) </script> <style lang="stylus"></style>
5.8.8 点击播放视频
获取视频时长: dom.duration
获取当前视频播放进度: dom.currentTime
修改当前视频播放进度: dom.currentTime = num
播放视频: dom.play()
暂停视频: dom.pause()
调整音量:dom.volume = 0-1
全屏: dom.requestFullScreen() ---- 需要设置video的宽和高也为全屏
静音: dom.muted = true
5.8.9 构建产品的详细信息
<!-- src/views/detal/components/ProInfo.vue -->
<template>
<div>
产品信息
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang='stylus'>
</style>
// src/views/detail/components/index.ts
export { default as Banner } from './Banner.vue'
export { default as ProInfo } from './ProInfo.vue'
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header">detail header</header>
<div class="content">
<Banner :list="banners" />
<ProInfo
:brand="brand"
:category="category"
:desc="desc"
:discount="discount"
:originprice="originprice"
:proname="proname"
:sales="sales"
:stock="stock"
/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getProDetail } from '@/api/detail'
import { Banner, ProInfo } from './components'
interface IData {
proid: string
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proname: string
sales: number
stock: number
}
export default defineComponent({
components: {
Banner, ProInfo
},
data (): IData {
return {
proid: '',
banners: [],
brand: '',
category: '',
desc: '',
discount: 0,
img1: '',
isrecommend: 0,
issale: 0,
isseckill: 0,
originprice: 0,
proname: '',
sales: 0,
stock: 0
}
},
mounted () {
console.log(this.$route.params)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
getProDetail(this.proid).then(res => {
console.log(res.data.data)
this.banners = res.data.data.banners[0].split(',')
this.brand = res.data.data.brand
this.category = res.data.data.category
this.desc = res.data.data.desc
this.discount = res.data.data.discount
this.img1 = res.data.data.img1
this.isrecommend = res.data.data.isrecommend
this.issale = res.data.data.issale
this.isseckill = res.data.data.isseckill
this.originprice = res.data.data.originprice
this.proname = res.data.data.proname
this.sales = res.data.data.sales
this.stock = res.data.data.stock
})
}
})
</script>
<style lang="stylus"></style>
<!-- src/views/detal/components/ProInfo.vue -->
<template>
<div class="proInfo">
<div class="priceBox">
<span>¥{{ originprice }}</span>
<span>销量:{{ sales }}</span>
</div>
<div class="proName">
<van-tag type="danger">{{ brand }}</van-tag>
<van-tag type="primary">{{ category }}</van-tag>
<h3>{{ proname }}</h3>
<p>{{ desc }}</p>
</div>
</div>
<div class="address">
<van-field
v-model="fieldValue"
is-link
readonly
label="送至"
placeholder="请选择所在地区"
@click="show = true"
/>
<van-popup v-model:show="show" round position="bottom">
<van-cascader
v-model="cascaderValue"
title="请选择所在地区"
:options="options"
@close="show = false"
@finish="onFinish"
/>
</van-popup>
</div>
</template>
<script lang='ts'>
import type { CascaderOption } from 'vant';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
brand: String,
category: String,
desc: String,
discount: Number,
originprice: Number,
proname: String,
sales: Number,
stock: Number
},
data () {
return {
fieldValue: '',
show: false,
cascaderValue: '',
options: [
{
text: '浙江省',
value: '330000',
children: [{ text: '杭州市', value: '330100' }],
},
{
text: '江苏省',
value: '320000',
children: [{ text: '南京市', value: '320100' }],
},
],
}
},
methods: {
onFinish ({ selectedOptions }: { selectedOptions: CascaderOption[]}) {
this.show = false;
this.fieldValue = selectedOptions.map((option) => option.text).join('/');
}
}
})
</script>
<style lang='stylus'>
.proInfo {
background-color: #fff;
padding: 15px;
border-bottom-right-radius: 16px;
border-bottom-left-radius: 16px;
.priceBox {
span {
line-height: 32px;
&:nth-child(1) {
font-size: 24px;
color: #f66;
}
&:nth-child(2) {
float: right;
}
}
}
.proName {
// font-weight: bold;
font-size: 0.14rem;
.van-tag {
margin-right: 5px;
}
p {
margin-top: 15px;
}
}
}
.address {
margin-top: 10px;
}
</style>
5.8.11 猜你喜欢
拷贝了首页的产品列表组件,同时要保证点击推荐列表修改详情页的数据(通过子组件给父组件传值)
<!-- src/views/detail/components/Pro.vue -->
<template>
<ul class="detail-proList">
<li class="proItem" v-for="item of proList" :key="item.proid" @click="toDetail(item.proid)">
<div class="itemImage">
<van-image :src="item.img1"></van-image>
</div>
<div class="itemInfo">
<div class="title van-multi-ellipsis--l2">{{ item.proname }}</div>
<div class="price">¥{{ item.originprice }}</div>
<div class="other">
<van-tag type="danger">{{ item.category }}</van-tag>
</div>
</div>
</li>
</ul>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import type { PropType } from 'vue'
import type { IPro } from '../detail'
export default defineComponent({
props: {
proList: Array as PropType<IPro[]>
},
methods: {
toDetail (proid: string) {
this.$router.push(`/detail/${proid}`)
// this.$router.push('/detail/' + proid)
// this.$router.push({ path: '/detail/' + proid })
// this.$router.push({ name: 'detail', params: { proid: proid } })
}
}
})
</script>
<style lang='stylus'>
.detail-proList
display flex
flex-wrap wrap
.proItem
width 28%
margin 8px 2%
// min-height 2.6rem
background-color #fff
border-radius 10px
overflow hidden
.itemImage
width 100%
height 1.2rem
.van-image
width 100%
height 100%
display block
.itemInfo
padding 10px
.price
color #f66
margin-top 5px
.other
margin-top 5px
</style>
// src/views/detail/components/index.ts
export { default as Banner } from './Banner.vue'
export { default as ProInfo } from './ProInfo.vue'
export { default as Pro } from './Pro.vue'
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header">detail header</header>
<div class="content">
<Banner :list="banners" />
<ProInfo
:brand="brand"
:category="category"
:desc="desc"
:discount="discount"
:originprice="originprice"
:proname="proname"
:sales="sales"
:stock="stock"
/>
<div class="recommendBox">
<h4 >猜你喜欢</h4>
<Pro :proList="recommendList" />
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getProDetail, getRecommendList } from '@/api/detail'
import { Banner, ProInfo, Pro } from './components'
import type { IPro } from './detail';
interface IData {
proid: string
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proname: string
sales: number
stock: number
recommendList: IPro[]
}
export default defineComponent({
components: {
Banner, ProInfo, Pro
},
data (): IData {
return {
proid: '',
banners: [],
brand: '',
category: '',
desc: '',
discount: 0,
img1: '',
isrecommend: 0,
issale: 0,
isseckill: 0,
originprice: 0,
proname: '',
sales: 0,
stock: 0,
recommendList: []
}
},
mounted () {
console.log(this.$route.params)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
})
getRecommendList().then(res => {
this.recommendList = res.data.data
})
}
})
</script>
<style lang="stylus">
.recommendBox
width 100%
min-height 300px
border-radius 16px
background-color #fff
margin-top 15px
h4
padding 15px
</style>
直接在父组件监听路由的变化实现
<!-- src/views/detail/index.vue --> <template> <div class="box"> <header class="header">detail header</header> <div class="content"> <Banner :list="banners" /> <ProInfo :brand="brand" :category="category" :desc="desc" :discount="discount" :originprice="originprice" :proname="proname" :sales="sales" :stock="stock" /> <div class="recommendBox"> <h4 >猜你喜欢</h4> <Pro :proList="recommendList" /> </div> </div> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { getProDetail, getRecommendList } from '@/api/detail' import { Banner, ProInfo, Pro } from './components' import type { IPro } from './detail'; interface IData { proid: string banners: string[] brand: string category: string desc: string discount: number img1: string isrecommend: number issale: number isseckill: number originprice: number proname: string sales: number stock: number recommendList: IPro[] } export default defineComponent({ components: { Banner, ProInfo, Pro }, data (): IData { return { proid: '', banners: [], brand: '', category: '', desc: '', discount: 0, img1: '', isrecommend: 0, issale: 0, isseckill: 0, originprice: 0, proname: '', sales: 0, stock: 0, recommendList: [] } }, mounted () { console.log(this.$route.params) // this.proid = String(this.$route.params.proid) this.proid = this.$route.params.proid as string getProDetail(this.proid).then(res => { // console.log(result) const result = res.data.data this.banners = result.banners[0].split(',') this.brand = result.brand this.category = result.category this.desc = result.desc this.discount = result.discount this.img1 = result.img1 this.isrecommend = result.isrecommend this.issale = result.issale this.isseckill = result.isseckill this.originprice = result.originprice this.proname = result.proname this.sales = result.sales this.stock = result.stock }) getRecommendList().then(res => { this.recommendList = res.data.data }) }, watch: { // 点击猜你喜欢的商品后 路由变化 重新渲染数据 $route (newVal) { this.proid = newVal.params.proid getProDetail(this.proid).then(res => { // console.log(result) const result = res.data.data this.banners = result.banners[0].split(',') this.brand = result.brand this.category = result.category this.desc = result.desc this.discount = result.discount this.img1 = result.img1 this.isrecommend = result.isrecommend this.issale = result.issale this.isseckill = result.isseckill this.originprice = result.originprice this.proname = result.proname this.sales = result.sales this.stock = result.stock }) } } }) </script> <style lang="stylus"> .recommendBox width 100% min-height 300px border-radius 16px background-color #fff margin-top 15px h4 padding 15px </style>
一旦轮播图滑动,切换商品后 轮播图组件的属性 current 没有重置
<!-- src/views/detail/components/Banner.vue --> <template> <div class="detail-swipers"> <van-swipe @change="changeIndex" :initial-swipe="current"> <van-swipe-item v-for="(item, index) of list" :key="index" @click="previewImage(index)"> <van-image fit="fill" class="banner-img" :src="item" /> </van-swipe-item> </van-swipe> <div class="indicator-tip"> {{ current + 1 }} / {{ total }} </div> </div> </template> <script lang='ts'> import { defineComponent } from 'vue'; import type { PropType} from 'vue' import { showImagePreview } from 'vant' export default defineComponent({ props: { list: Array as PropType<string[]> // ts环境 }, data () { return { current: 0 } }, computed: { total () { return this.list!.length } }, watch: { $route () { // 因为组件一旦被创建,后续组件的更新 不会重置状态 this.current = 0 } }, methods: { changeIndex (index: number) { this.current = index }, previewImage (index: number) { showImagePreview({ images: this.list!, startPosition: index, onChange: (idx: number) => { // 保证大图预览完毕 序号要一致 给 轮播图设置 initial-swipe 属性 console.log('666', idx) this.current = idx } }) } } }) </script> <style lang="stylus"> .detail-swipers width 100% height 2.6rem background-color #00f overflow hidden position relative .van-swipe height 100% .banner-img width 100% height 100% .indicator-tip position absolute bottom 20px right 0 width 50px height 30px background-color rgba(0, 0, 0, 0.5) border-radius 15px 0 0 15px color #fff display flex justify-content center align-items center </style>
5.8.12 详情页面底部
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header">detail header</header>
<div class="content">
<Banner :list="banners" />
<ProInfo
:brand="brand"
:category="category"
:desc="desc"
:discount="discount"
:originprice="originprice"
:proname="proname"
:sales="sales"
:stock="stock"
/>
<div class="recommendBox">
<h4 >猜你喜欢</h4>
<Pro :proList="recommendList" />
</div>
<van-action-bar>
<van-action-bar-icon icon="chat-o" text="客服" color="#ee0a24" />
<van-action-bar-icon icon="cart-o" text="购物车" />
<van-action-bar-icon icon="star" text="已收藏" color="#ff5000" />
<van-action-bar-button type="warning" v-if="issale === 1" text="加入购物车" />
<van-action-bar-button type="danger" disabled v-else text="商品已下架" />
</van-action-bar>
<div style="height: 60px"></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getProDetail, getRecommendList } from '@/api/detail'
import { Banner, ProInfo, Pro } from './components'
import type { IPro } from './detail';
interface IData {
proid: string
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proname: string
sales: number
stock: number
recommendList: IPro[]
}
export default defineComponent({
components: {
Banner, ProInfo, Pro
},
data (): IData {
return {
proid: '',
banners: [],
brand: '',
category: '',
desc: '',
discount: 0,
img1: '',
isrecommend: 0,
issale: 0,
isseckill: 0,
originprice: 0,
proname: '',
sales: 0,
stock: 0,
recommendList: []
}
},
mounted () {
console.log(this.$route.params)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
})
getRecommendList().then(res => {
this.recommendList = res.data.data
})
},
watch: { // 点击猜你喜欢的商品后 路由变化 重新渲染数据
$route (newVal) {
this.proid = newVal.params.proid
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
})
}
}
})
</script>
<style lang="stylus">
.recommendBox
width 100%
min-height 300px
border-radius 16px
background-color #fff
margin-top 15px
h4
padding 15px
</style>
5.8.13 详情页面头部
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header detail-header" >
<div class="header1" :class="op">
<!-- <van-nav-bar left-arrow >
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar> -->
<ul>
<li>
<van-icon name="arrow-left" size="18" />
</li>
<li></li>
<li>
<van-icon name="weapp-nav" size="18" />
</li>
</ul>
</div>
<div class="header2" :class="op">
<van-nav-bar left-arrow>
<template #right>
<van-icon name="weapp-nav" size="18" />
</template>
<template #title>
<ul class="header-color">
<li :class="scrollTop < recommendTop - 44 ? 'active' : ''" @click="backTop(0)">商品</li>
<li :class="scrollTop >= recommendTop - 44 ? 'active' : ''" @click="backTop(recommendTop - 44)">推荐</li>
</ul>
</template>
</van-nav-bar>
</div>
</header>
<div class="content" @scroll="onScroll" ref="content">
<Banner :list="banners" />
<ProInfo
:brand="brand"
:category="category"
:desc="desc"
:discount="discount"
:originprice="originprice"
:proname="proname"
:sales="sales"
:stock="stock"
/>
<div class="recommendBox" ref="recommend">
<h4 >猜你喜欢</h4>
<Pro :proList="recommendList" />
</div>
<van-action-bar>
<van-action-bar-icon icon="chat-o" text="客服" color="#ee0a24" />
<van-action-bar-icon icon="cart-o" text="购物车" />
<van-action-bar-icon icon="star" text="已收藏" color="#ff5000" />
<van-action-bar-button type="warning" v-if="issale === 1" text="加入购物车" />
<van-action-bar-button type="danger" disabled v-else text="商品已下架" />
</van-action-bar>
<div style="height: 60px"></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getProDetail, getRecommendList } from '@/api/detail'
import { Banner, ProInfo, Pro } from './components'
import type { IPro } from './detail';
interface IData {
proid: string
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proname: string
sales: number
stock: number
recommendList: IPro[]
op: string,
recommendTop: number
scrollTop: number
}
export default defineComponent({
components: {
Banner, ProInfo, Pro
},
data (): IData {
return {
proid: '',
banners: [],
brand: '',
category: '',
desc: '',
discount: 0,
img1: '',
isrecommend: 0,
issale: 0,
isseckill: 0,
originprice: 0,
proname: '',
sales: 0,
stock: 0,
recommendList: [],
op: 'op0',
recommendTop: 0,
scrollTop: 0
}
},
mounted () {
console.log(this.$route.params)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
// 动态计算某个元素距页面顶部的距离
this.$nextTick(() => {
console.log((this.$refs.recommend as HTMLDivElement).offsetTop)
this.recommendTop = (this.$refs.recommend as HTMLDivElement).offsetTop
})
})
getRecommendList().then(res => {
this.recommendList = res.data.data
})
},
watch: { // 点击猜你喜欢的商品后 路由变化 重新渲染数据
$route (newVal) {
this.proid = newVal.params.proid
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
// 动态计算某个元素距页面顶部的距离
this.$nextTick(() => {
console.log((this.$refs.recommend as HTMLDivElement).offsetTop)
this.recommendTop = (this.$refs.recommend as HTMLDivElement).offsetTop
})
})
}
},
methods: {
onScroll () {
// 核心算法 计算 滚动比例
console.log((this.$refs.content as HTMLDivElement).scrollTop)
this.scrollTop = (this.$refs.content as HTMLDivElement).scrollTop
const m = Math.floor((this.$refs.content as HTMLDivElement).scrollTop / 10)
if (m > 10) {
this.op = 'op10'
} else {
this.op = 'op' + m
}
},
backTop (num: number) {
(this.$refs.content as HTMLDivElement).scrollTop = num
}
}
})
</script>
<style lang="stylus">
// 层级修改背景
.container
.box
.header
background-color #f66
&.detail-header
background-color transparent
.detail-header
position fixed
top 0
width 100%
z-index 999
// opacity 0
// background-color #fff
.header1
position fixed
width 100%
top 0
height 0.44rem
opacity 1
&.op0
opacity 1
&.op1
opacity 0.9
&.op2
opacity 0.8
&.op3
opacity 0.7
&.op4
opacity 0.6
&.op5
opacity 0.5
&.op6
opacity 0.4
&.op7
opacity 0.3
&.op8
opacity 0.2
&.op9
opacity 0.1
&.op10
opacity 0
ul
display flex
height 100%
// justify-content space-between
li
// flex 1
height 100%
display flex
justify-content center
align-items center
color #000
&:nth-child(1), &:nth-child(3){
width 50px
}
&:nth-child(2) {
flex 1
}
.header2
position fixed
width 100%
top 0
color #000
&.op0
opacity 0
&.op1
opacity 0.1
&.op2
opacity 0.2
&.op3
opacity 0.3
&.op4
opacity 0.4
&.op5
opacity 0.5
&.op6
opacity 0.6
&.op7
opacity 0.7
&.op8
opacity 0.8
&.op9
opacity 0.9
&.op10
opacity 1
.header-color
color #000
display flex
li
flex 1
&:nth-child(1) {
margin-right 10px
}
&:nth-child(2) {
margin-left 10px
}
&.active
border-bottom 3px solid #f66
.recommendBox
width 100%
min-height 300px
border-radius 16px
background-color #fff
margin-top 15px
h4
padding 15px
</style>
5.8.14 收藏功能 - 作业
本操作属于纯前端操作
1.进入详情页,需要先判断该商品是否被收藏过,如果收藏过,显示已收藏,如果未被收藏过,显示收藏
2.如果当前商品是收藏的,点击要取消收藏
3.如果当前商品是未收藏的,点击收藏
4.数据可以存到本地,localStorage 只能保存 字符串
5.收藏夹内只保存 产品id,以数组的形式存在
6.检测到路由变化 ,执行判断是否收藏
<!-- src/views/detail/IndexView.vue -->
<template>
<div class="myHeader">
<Transition name="fade">
<header class="header1" v-show="scrollTop < 300">
<ul>
<li class="left" @click="$router.back()">
<van-icon name="arrow-left" />
</li>
<li class="middle"></li>
<li class="right">
<van-popover placement="bottom-end" v-model:show="showPopover" :actions="actions" @select="onSelect" theme="dark">
<template #reference>
<van-icon name="ellipsis" />
</template>
</van-popover>
</li>
</ul>
</header>
</Transition>
<Transition name="fade">
<header class="header2" v-show="scrollTop > 300">
<ul>
<li class="left" @click="$router.back()">
<van-icon name="arrow-left" />
</li>
<li class="middle">
<span>详情</span>
<span>推荐</span>
</li>
<li class="right">
<van-popover placement="bottom-end" v-model:show="showPopover" :actions="actions" @select="onSelect" theme="dark">
<template #reference>
<van-icon name="ellipsis" />
</template>
</van-popover>
</li>
</ul>
</header>
</Transition>
</div>
<div class="content" @scroll="scroll">
<BannerComponent :list="banners" :video="video"></BannerComponent>
<ProInfoComponent :proname="proname" :discount="discount" :originprice="originprice" :sales="sales" :brand="brand" :category="category"></ProInfoComponent>
<ProComponent :list="recommendList"></ProComponent>
<van-action-bar>
<van-action-bar-icon icon="chat-o" text="客服" />
<van-action-bar-icon icon="cart-o" text="购物车" />
<van-action-bar-icon v-if="isStar" icon="star" color="#f66" text="已收藏" @click="changeStar"/>
<van-action-bar-icon v-else icon="star-o" text="收藏" @click="changeStar"/>
<van-action-bar-button type="danger" v-if="issale" text="加入购物车" />
<van-action-bar-button type="danger" :disabled="issale === 0" v-else text="商品已下架" />
</van-action-bar>
<van-share-sheet
v-model:show="showShare"
title="立即分享给好友"
:options="options"
@select="onShareSelect"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getDetailData, getDetailRecommendData } from '@/api/detail'
import BannerComponent from './components/BannerComponent.vue'
import ProInfoComponent from './components/ProInfoComponent.vue'
import ProComponent from './components/ProComponent.vue'
import type { IPro } from '../home/home';
interface IData {
proid: string
banners: string[]
proname: string // 名称
originprice: number // 原价
discount: number // 折扣
brand: string // 品牌
category: string // 分类
sales: number // 销量
issale: number // 1表示正在售卖,0表示已下架
video: string
recommendList: IPro[]
scrollTop: number,
showPopover: boolean
actions: Array<{ text: string, icon: string }>
options: Array<{ name: string, icon: string }>,
showShare: boolean
isStar: boolean // ++++++++
}
export default defineComponent({
components: {
BannerComponent,
ProInfoComponent,
ProComponent
},
data (): IData {
return {
proid: '',
banners: [],
proname: '',
originprice: 0,
discount: 0,
brand: '',
category: '',
sales: 0,
issale: 1,
video: '',
recommendList: [],
scrollTop: 0,
showPopover: false,
actions: [
{ text: '首页', icon: 'wap-home-o' },
{ text: '我的', icon: 'manager-o' },
{ text: '分享', icon: 'share-o' },
],
options: [
{ name: '微信', icon: 'wechat' },
{ name: '微博', icon: 'weibo' },
{ name: '复制链接', icon: 'link' },
{ name: '分享海报', icon: 'poster' },
{ name: '二维码', icon: 'qrcode' }
],
showShare: false,
isStar: true // ++++++++
}
},
mounted () {
console.log(this.$route.params.proid)
this.proid = (this.$route.params.proid as string)
this.getData(this.proid)
getDetailRecommendData().then(res => {
this.recommendList = res.data.data
})
this.getStarFlag(this.proid) // ++++++++
},
methods: {
changeStar () { // ++++++++
if (this.isStar) {
const starArr = JSON.parse(localStorage.getItem('stars')!)
const index = starArr.findIndex((item: string) => item === this.proid)
starArr.splice(index, 1)
localStorage.setItem('stars', JSON.stringify(starArr))
this.isStar = false
} else {
const arrStr: any = localStorage.getItem('stars') || '[]'
const starArr = JSON.parse(arrStr)
starArr.push(this.proid)
localStorage.setItem('stars', JSON.stringify(starArr))
this.isStar = true
}
},
getStarFlag (proid: string) { // ++++++++
const starArr = JSON.parse(localStorage.getItem('stars')!)
if (starArr) {
const index = starArr.findIndex((item: string) => item === proid)
if (index !== -1) {
this.isStar = true
} else {
this.isStar = false
}
} else {
this.isStar = false
}
},
getData (proid:string) {
getDetailData(proid).then(res => {
console.log(res.data.data)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.proname = result.proname
this.originprice = result.originprice
this.discount = result.discount
this.brand = result.brand
this.category = result.category
this.sales = result.sales
this.issale = result.issale
this.video = 'https://vod.300hu.com/4c1f7a6atransbjngwcloud1oss/542359bf391145770504425473/v.f20.mp4'
})
},
scroll (event: Event) {
this.scrollTop = (event.target as HTMLDivElement).scrollTop
},
onSelect (action: { text: string, icon: string }) {
console.log(action)
switch (action.text) {
case '首页':
this.$router.push('/home')
break;
case '我的':
this.$router.push('/user')
break;
case '分享':
this.showShare = true
break;
}
},
onShareSelect (option: { name: string, icon: string }) {
console.log(option)
}
},
watch: {
$route (val) {
console.log(val)
this.getData(val.params.proid)
this.getStarFlag(val.params.proid) // ++++++++
}
}
})
</script>
<style lang="scss">
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.fade-enter-active {
transition: all 0.5s;
}
.fade-leave-active {
transition: all 0s;
}
.fade-enter-to, .fade-leave-from {
opacity: 1;
}
.myHeader {
user-select: none;
position: fixed;
top: 0;
width: 100%;
z-index: 999;
.header1 {
height: 0.44rem;
padding: 6px 15px;
box-sizing: border-box;
ul {
width: 100%;
height: 100%;
display: flex;
li {
&:nth-child(1), &:nth-child(3) {
font-size: 32px;
width: 44px;
}
&:nth-child(2) {
flex: 1;
}
}
}
}
.header2 {
height: 0.44rem;
padding: 6px 15px;
box-sizing: border-box;
background-color: #fff;
ul {
width: 100%;
height: 100%;
display: flex;
li {
&:nth-child(1), &:nth-child(3) {
font-size: 32px;
width: 44px;
}
&:nth-child(2) {
flex: 1;
display: flex;
span {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
}
}
</style>
5.8.15 分析接下来操作
加入购物车(是谁,加了几件,加了哪个商品进入购物车)
是谁
登录
注册
5.9 注册功能
5.9.1 注册思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRZak2KJ-1673602369091)(assets/image-20220923073954061.png)]
5.9.2 嵌套路由
一些应用程序的 UI 由多层嵌套的组件组成。在这种情况下,URL 的片段通常对应于特定的嵌套组件结构,例如:
/user/johnny/profile /user/johnny/posts
+------------------+ +-----------------+
| User | | User |
| +--------------+ | | +-------------+ |
| | Profile | | +------------> | | Posts | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
通过 Vue Router,你可以使用嵌套路由配置来表达这种关系。
/register 注册路由
/register/index 注册第一步
/register/sms 注册第二步
/register/pwd 注册第三步
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UWfKNJk3-1673602369092)(assets/image-20220927101429498.png)]
<!-- src/views/register/components/CheckTel.vue -->
<template>
<div>校验手机号</div>
</template>
<!-- src/views/register/components/SendTelCode.vue -->
<template>
<div>发送短信验证码</div>
</template>
<!-- src/views/register/components/SetPassword.vue -->
<template>
<div>设置密码</div>
</template>
// src/views/register/components/index.ts
export { default as CheckTel } from './CheckTel.vue'
export { default as SendTelCode } from './SendTelCode.vue'
export { default as SetPassword } from './SetPassword.vue'
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router'
// 直接引入组件,不管你用不用这个页面,我都引入了 --- 静态导入
import Home from '@/views/home/index.vue'
import Footer from '@/components/Footer.vue'
import { CheckTel, SendTelCode, SetPassword } from '@/views/register/components'
// import Kind from '@/views/kind/index.vue'
// import Cart from '@/views/cart/index.vue'
// import User from '@/views/user/index.vue'
// 使用路由懒加载 - 访问该路由时才加载该组件 --- 动态导入
const Kind = () => import('@/views/kind/index.vue')
const Cart = () => import('@/views/cart/index.vue')
const User = () => import('@/views/user/index.vue')
const Detail = () => import('@/views/detail/index.vue')
const Vdo = () => import('@/views/video/index.vue')
const Register = () => import('@/views/register/index.vue')
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
// component: Home
components: {
default: Home,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: Kind
components: {
default: Kind,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: Cart
components: {
default: Cart,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: User
components: {
default: User,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
// component: Detail
components: { // 与 component: Detail 等价
default: Detail
}
},
{
path: '/vdo',
name: 'vdo',
components: {
default: Vdo
}
},
{
path: '/register',
name: 'register',
redirect: '/register/index',
component: Register,
children: [
{
path: 'index', // /register/index
component: CheckTel
},
{
path: 'sms', // /register/sms
component: SendTelCode
},
{
path: 'pwd', // /register/pwd
component: SetPassword
}
]
}
]
const router: Router = createRouter({
history: createWebHistory(),
routes
})
export default router
<!-- src/views/register/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="注册"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<RouterView />
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang='stylus'>
</style>
5.9.3 注册第一步页面
<!-- src/views/register/components/CheckTel.vue -->
<template>
<div class="register-form">
<div class="form-input">
<van-field v-model="tel" placeholder="请输入手机号" clearable/>
</div>
<div class="form-btn">
<van-button color="linear-gradient(to right, #ee0a24, #ff6034)" block round>下一步</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
tel: ''
}
}
})
</script>
<style lang="stylus">
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.9.4注册第二步页面
<!-- src/views/register/components/SendTelCode.vue -->
<template>
<div class="register-form">
<div class="form-input">
<van-field
v-model="telcode"
center
clearable
placeholder="请输入手机验证码"
>
<template #button>
<van-button class="sms-btn" color="rgba(226,35,30,.2)" size="small" round>
<span style="color: #e2231a">获取验证码</span>
</van-button>
</template>
</van-field>
</div>
<div class="form-btn">
<van-button color="linear-gradient(to right, #ee0a24, #ff6034)" block round>下一步</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
tel: '',
telcode: ''
}
}
})
</script>
<style lang="stylus">
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
overflow hidden
.sms-btn
border-color rgba(226,35,30,.2)
padding 0 20px
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.9.5注册第三步页面
<!-- src/views/register/components/SetPassword.vue -->
<template>
<div class="register-form">
<div class="form-input">
<van-field v-model="password" :type="type ? 'password' : 'text'" placeholder="请设置6-20位登录密码" clearable @click-right-icon="type = !type" :right-icon="type ? 'closed-eye' : 'eye-o'"/>
</div>
<div class="form-tip">
密码由6-20位大小写字母数字等组成
</div>
<div class="form-btn">
<van-button color="linear-gradient(to right, #ee0a24, #ff6034)" block round>完成</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
tel: '',
password: '',
type: true
}
}
})
</script>
<style lang="stylus">
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
.form-tip
color: #c6c6c6
font-size 14px
margin-top 9px
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.9.6 封装注册登录接口
// src/api/user.ts
import request from '@/utils/request'
// 检测手机号是否被注册过
export function doCheckPhone (params: { tel: string }) {
return request.post('/user/docheckphone', params)
}
// 发送短信验证码
export function doSendMsgCode (params: { tel: string }) {
return request.post('/user/dosendmsgcode', params)
}
// 验证验证码
export function doCheckCode (params: { tel: string, telcode: string }) {
return request.post('/user/docheckcode', params)
}
// 设置密码完成注册
export function doFinishRegister (params: { tel: string, password: string }) {
return request.post('/user/dofinishregister', params)
}
// 登录
export function doLogin (params: { loginname: string, password: string }) {
return request.post('/user/login', params)
}
5.9.7 实现注册第一步功能
<!-- src/views/register/components/CheckTel.vue -->
<template>
<div class="register-form">
<div class="form-input">
<van-field v-model="tel" placeholder="请输入手机号" clearable/>
</div>
<div class="form-btn">
<van-button :disabled="!flag" @click="next" color="linear-gradient(to right, #ee0a24, #ff6034)" block round>下一步</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { doCheckPhone } from '@/api/user'
import { showConfirmDialog } from 'vant';
export default defineComponent({
data () {
return {
tel: '18813007814'
}
},
computed: {
flag () {
return /^1[3-9]\d{9}$/.test(this.tel)
}
},
methods: {
next () {
doCheckPhone({ tel: this.tel }).then(res => {
if (res.data.code === '10005') {
showConfirmDialog({
message:
'该手机号已被注册,是否立即登录。',
})
.then(() => {
// on confirm
// ** 登录 注册
this.$router.back()
})
.catch(() => {
// on cancel
})
} else {
// 将手机号存入本地:后面需要,跳转到发送验证码页面
localStorage.setItem('tel', this.tel)
// 注册第一步 注册第二步 注册第三步 push
// 注册第一步
// 注册第二步
// 注册第三步 repalce
this.$router.push('/register/sms')
}
})
}
}
})
</script>
<style lang="stylus">
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.9.8 实现注册第二步功能
发送短信验证码实际上没有发送至手机,请至控制台查看验证码
<!-- src/views/register/components/SendTelCode.vue -->
<template>
<div class="register-form">
<div class="form-input">
<van-field
v-model="telcode"
center
clearable
placeholder="请输入手机验证码"
>
<template #button>
<van-button :disabled="btnFlag" class="sms-btn" color="rgba(226,35,30,.2)" @click="sendCode" size="small" round>
<span style="color: #e2231a">{{ text }}</span>
</van-button>
</template>
</van-field>
</div>
<div class="form-btn">
<van-button :disabled="flag" @click="checkCode" color="linear-gradient(to right, #ee0a24, #ff6034)" block round>下一步</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { showToast } from 'vant';
import { doSendMsgCode, doCheckCode } from '@/api/user'
export default defineComponent({
data () {
return {
tel: '',
telcode: '',
text: '获取验证码',
time: 10,
btnFlag: false
}
},
mounted () {
this.tel = localStorage.getItem('tel')!
},
computed: {
flag () {
return this.telcode === ''
}
},
methods: {
sendCode () {
doSendMsgCode({ tel: this.tel }).then(res => {
console.log(res.data)
})
const timer = setInterval(() => {
this.time--
if (this.time === 0) {
this.text = `获取验证码`
this.btnFlag = false
this.time = 10
clearInterval(timer)
} else {
this.text = `重新发送(${this.time}s)`
this.btnFlag = true
}
}, 1000)
},
checkCode () {
doCheckCode({ tel: this.tel, telcode: this.telcode }).then(res => {
if (res.data.code === '10007') {
showToast({
message: '验证码错误',
position: 'bottom',
});
} else {
this.$router.push('/register/pwd')
}
})
}
}
})
</script>
<style lang="stylus">
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
overflow hidden
.sms-btn
border-color rgba(226,35,30,.2)
padding 0 20px
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.9.9 实现注册第三步功能
<!-- src/views/register/components/SetPassword.vue -->
<template>
<div class="register-form">
<div class="form-input">
<van-field v-model="password" :type="type ? 'password' : 'text'" placeholder="请设置6-20位登录密码" clearable @click-right-icon="type = !type" :right-icon="type ? 'closed-eye' : 'eye-o'"/>
</div>
<div class="form-tip" :style="{color: color}">
密码由6-20位大小写字母数字等组成
</div>
<div class="form-btn">
<van-button @click="finish" :disabled="flag" color="linear-gradient(to right, #ee0a24, #ff6034)" block round>完成</van-button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { showToast } from 'vant';
import { doFinishRegister } from '@/api/user'
export default defineComponent({
data () {
return {
tel: '',
password: '',
type: true,
color: '#c6c6c6'
}
},
mounted () {
this.tel = localStorage.getItem('tel')!
},
computed: {
flag () {
// return /^\S*(?=\S{6,20})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])\S*$/.test(this.password)
return this.password === ''
}
},
methods: {
finish () {
if (/^\S*(?=\S{6,20})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])\S*$/.test(this.password)) {
// 调用接口
this.color = '#c6c6c6'
doFinishRegister({ tel: this.tel, password: this.password }).then(() => {
// 删除本地手机号
localStorage.removeItem('tel')
// 去登录 --- 登录 1 2 3
this.$router.go(-3)
})
} else {
this.color = '#f66'
showToast({
message: '密码格式错误',
position: 'bottom',
});
}
}
}
})
</script>
<style lang="stylus">
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
.form-tip
color: #c6c6c6
font-size 14px
margin-top 9px
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.10 登录实现
5.10.1 登录思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qWhS9FoA-1673602369093)(assets/image-20220923074218453.png)]
5.10.2 构建登录页面
<!-- src/views/login/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="登录"
left-arrow
@click-left="$router.back()"
:border="false"
/>
</header>
<div class="content register-content">
<div class="register-form">
<div class="form-input">
<van-field v-model="loginname" placeholder="账户名/邮箱/手机号" clearable/>
</div>
<div class="form-input">
<van-field v-model="password" :type="type ? 'password' : 'text'" placeholder="请设置6-20位登录密码" clearable @click-right-icon="type = !type" :right-icon="type ? 'closed-eye' : 'eye-o'"/>
</div>
<div class="form-btn">
<van-button :disabled="!flag" @click="login" color="linear-gradient(to right, #ee0a24, #ff6034)" block round>登录</van-button>
</div>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({
data () {
return {
loginname: '',
password: '',
type: true,
}
},
computed: {
flag () {
return this.loginname !== '' && this.password !==''
}
},
methods: {
login () {
}
}
})
</script>
<style lang='stylus'>
.register-content
background-color #fff
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
.form-tip
color: #c6c6c6
font-size 14px
margin-top 9px
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.10.3 注册登录路由
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router'
// 直接引入组件,不管你用不用这个页面,我都引入了 --- 静态导入
import Home from '@/views/home/index.vue'
import Footer from '@/components/Footer.vue'
import { CheckTel, SendTelCode, SetPassword } from '@/views/register/components'
// import Kind from '@/views/kind/index.vue'
// import Cart from '@/views/cart/index.vue'
// import User from '@/views/user/index.vue'
// 使用路由懒加载 - 访问该路由时才加载该组件 --- 动态导入
const Kind = () => import('@/views/kind/index.vue')
const Cart = () => import('@/views/cart/index.vue')
const User = () => import('@/views/user/index.vue')
const Detail = () => import('@/views/detail/index.vue')
const Vdo = () => import('@/views/video/index.vue')
const Register = () => import('@/views/register/index.vue')
const Login = () => import('@/views/login/index.vue')
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
// component: Home
components: {
default: Home,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: Kind
components: {
default: Kind,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: Cart
components: {
default: Cart,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: User
components: {
default: User,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
// component: Detail
components: { // 与 component: Detail 等价
default: Detail
}
},
{
path: '/vdo',
name: 'vdo',
components: {
default: Vdo
}
},
{
path: '/register',
name: 'register',
redirect: '/register/index',
component: Register,
children: [
{
path: 'index', // /register/index
component: CheckTel
},
{
path: 'sms', // /register/sms
component: SendTelCode
},
{
path: 'pwd', // /register/pwd
component: SetPassword
}
]
},
{
path: '/login',
name: 'login',
component: Login
},
]
const router: Router = createRouter({
history: createWebHistory(),
routes
})
export default router
5.10.4 实现登录功能
<!-- src/views/login/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="登录"
left-arrow
@click-left="$router.back()"
:border="false"
/>
</header>
<div class="content register-content">
<div class="register-form">
<div class="form-input">
<van-field v-model="loginname" placeholder="账户名/邮箱/手机号" clearable/>
</div>
<div class="form-input">
<van-field v-model="password" :type="type ? 'password' : 'text'" placeholder="请设置6-20位登录密码" clearable @click-right-icon="type = !type" :right-icon="type ? 'closed-eye' : 'eye-o'"/>
</div>
<div class="form-btn">
<van-button :disabled="!flag" @click="login" color="linear-gradient(to right, #ee0a24, #ff6034)" block round>登录</van-button>
</div>
<div style="float: right; margin-top: 20px">
<router-link to="/register">手机号立即注册</router-link>
</div>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { doLogin } from '@/api/user'
import { showConfirmDialog, showToast } from 'vant'
export default defineComponent({
data () {
return {
loginname: '18818007814',
password: 'Ty2206',
type: true,
}
},
computed: {
flag () {
return this.loginname !== '' && this.password !==''
}
},
methods: {
login () {
doLogin({ loginname: this.loginname, password: this.password }).then(res => {
if (res.data.code === '10010') {
// 用户不存在
showConfirmDialog({
message:
'该账户未注册,是否立即注册。',
})
.then(() => {
// on confirm
// ** 登录 注册
this.$router.push('/register/index')
})
.catch(() => {
// on cancel
})
} else if (res.data.code === '10011') {
// 密码错误
showToast({
message: '密码错误',
position: 'bottom',
});
} else {
// 登录成功
console.log(res.data.data)
localStorage.setItem('token', res.data.data.token)
localStorage.setItem('userid', res.data.data.userid)
this.$router.back()
}
})
}
}
})
</script>
<style lang='stylus'>
.register-content
background-color #fff
.register-form
width calc(100% - 50px)
// min-height 300px
// background-color #00f
margin 10px 25px 10px 25px
padding 10px 0
.form-input
height 50px
// background-color #0f0
display flex
align-items center
border-bottom 1px solid #efefef
.form-tip
color: #c6c6c6
font-size 14px
margin-top 9px
.form-btn
margin-top 59px
.van-button
height 50px
font-size 16px
font-weight bold
box-shadow: 0 1px 10px 0 rgb(255 62 62 / 20%);
background-color: #efefef;
</style>
5.11 加入购物车
前端校验用户登录状态,如果登录,调用后端接口加入购物车,如果前端校验未登录,需要跳转到登录页面
后端校验未登录也需要跳转到登录页面
5.11.1 重新封装 数据请求
// src/utils/request.ts
import axios from 'axios'
import router from '@/router'
const isDev = process.env.NODE_ENV === 'development' // 真 - 开发环境,假-生产环境
// http://121.89.205.189:3000/apidoc/
// npm run dev 走?后的
// npm run build 走:后的
const ins = axios.create({
baseURL: isDev ? 'http://121.89.205.189:3000/api' : 'http://121.89.205.189:3000/api'
})
// 拦截器
ins.interceptors.request.use((config: any) => {
(config.headers!.token as string) = localStorage.getItem('token')! // +++++
return config
}, (err) => {
return Promise.reject(err)
})
ins.interceptors.response.use((response: any) => {
// 判断登录标识 是否有效,如果无效跳转至登录页面,如果有效,不做操作
if (response.data.code === '10119') {
// 没有传递token 或者 token过期
// 跳转到登录页面 重新登录
router.push('/login')
return null
} else {
return response
}
}, (err) => {
return Promise.reject(err)
})
export default ins
5.11.2 封装购物车相关数据请求
// src/api/cart.ts
import request from '@/utils/request'
// 加入购物车
export function addCart (params: { userid: string, proid: string, num: number }) {
return request.post('/cart/add', params)
}
// 获取购物车列表数据
export function getCartListData (params: { userid: string }) {
return request.post('/cart/list', params)
}
// 删除某个用户的购物车的所有数据
export function removeAllData (params: { userid: string }) {
return request.post('/cart/removeall', params)
}
// 删除某个用户的一条购物车的数据
export function removeOneData (params: { cartid: string }) {
return request.post('/cart/remove', params)
}
// 更新某个用户的一条购物车的数据的选中状态
export function selectOneData (params: { cartid: string, flag: boolean }) {
return request.post('/cart/selectone', params)
}
// 更新某个用户的购物车的所有数据的选中状态
export function selectAllData (params: { userid: string, type: boolean }) {
return request.post('/cart/selectall', params)
}
// 更新某个用户的购物车的某个产品的数量
export function updateOneDataNum (params: { cartid: string, num: number }) {
return request.post('/cart/updatenum', params)
}
// 推荐商品接口
export function getCartRecommendData () {
return request.get('/pro/recommendlist')
}
5.11.3 加入购物车
<!-- src/views/detail/index.vue -->
<template>
<div class="box">
<header class="header detail-header" >
<div class="header1" :class="op">
<!-- <van-nav-bar left-arrow >
<template #right>
<van-icon name="search" size="18" />
</template>
</van-nav-bar> -->
<ul>
<li>
<van-icon name="arrow-left" size="18" />
</li>
<li></li>
<li>
<van-icon name="weapp-nav" size="18" />
</li>
</ul>
</div>
<div class="header2" :class="op">
<van-nav-bar left-arrow>
<template #right>
<van-icon name="weapp-nav" size="18" />
</template>
<template #title>
<ul class="header-color">
<li :class="scrollTop < recommendTop - 44 ? 'active' : ''" @click="backTop(0)">商品</li>
<li :class="scrollTop >= recommendTop - 44 ? 'active' : ''" @click="backTop(recommendTop - 44)">推荐</li>
</ul>
</template>
</van-nav-bar>
</div>
</header>
<div class="content" @scroll="onScroll" ref="content">
<Banner :list="banners" />
<ProInfo
:brand="brand"
:category="category"
:desc="desc"
:discount="discount"
:originprice="originprice"
:proname="proname"
:sales="sales"
:stock="stock"
/>
<div class="recommendBox" ref="recommend">
<h4 >猜你喜欢</h4>
<Pro :proList="recommendList" />
</div>
<van-action-bar>
<van-action-bar-icon icon="chat-o" text="客服" color="#ee0a24" />
<van-action-bar-icon icon="cart-o" @click="toCart" text="购物车" />
<van-action-bar-icon icon="star-o" text="收藏" color="#ff5000" />
<van-action-bar-button @click="addCartFn" type="warning" v-if="issale === 1" text="加入购物车" />
<van-action-bar-button type="danger" disabled v-else text="商品已下架" />
</van-action-bar>
<div style="height: 60px"></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { showToast } from 'vant'
import { getProDetail, getRecommendList } from '@/api/detail'
import { addCart } from '@/api/cart'
import { Banner, ProInfo, Pro } from './components'
import type { IPro } from './detail';
interface IData {
proid: string
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proname: string
sales: number
stock: number
recommendList: IPro[]
op: string,
recommendTop: number
scrollTop: number
}
export default defineComponent({
components: {
Banner, ProInfo, Pro
},
data (): IData {
return {
proid: '',
banners: [],
brand: '',
category: '',
desc: '',
discount: 0,
img1: '',
isrecommend: 0,
issale: 0,
isseckill: 0,
originprice: 0,
proname: '',
sales: 0,
stock: 0,
recommendList: [],
op: 'op0',
recommendTop: 0,
scrollTop: 0
}
},
mounted () {
console.log(this)
// this.proid = String(this.$route.params.proid)
this.proid = this.$route.params.proid as string
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
// 动态计算某个元素距页面顶部的距离
this.$nextTick(() => {
console.log((this.$refs.recommend as HTMLDivElement).offsetTop)
this.recommendTop = (this.$refs.recommend as HTMLDivElement).offsetTop
})
})
getRecommendList().then(res => {
this.recommendList = res.data.data
})
},
watch: { // 点击猜你喜欢的商品后 路由变化 重新渲染数据
$route (newVal) {
this.proid = newVal.params.proid
getProDetail(this.proid).then(res => {
// console.log(result)
const result = res.data.data
this.banners = result.banners[0].split(',')
this.brand = result.brand
this.category = result.category
this.desc = result.desc
this.discount = result.discount
this.img1 = result.img1
this.isrecommend = result.isrecommend
this.issale = result.issale
this.isseckill = result.isseckill
this.originprice = result.originprice
this.proname = result.proname
this.sales = result.sales
this.stock = result.stock
// 动态计算某个元素距页面顶部的距离
this.$nextTick(() => {
// console.log((this.$refs.recommend as HTMLDivElement).offsetTop);
(this.$refs.content as HTMLDivElement).scrollTop = 0
this.recommendTop = (this.$refs.recommend as HTMLDivElement).offsetTop
})
})
}
},
methods: {
onScroll () {
// 核心算法 计算 滚动比例
console.log((this.$refs.content as HTMLDivElement).scrollTop)
this.scrollTop = (this.$refs.content as HTMLDivElement).scrollTop
const m = Math.floor((this.$refs.content as HTMLDivElement).scrollTop / 10)
if (m > 10) {
this.op = 'op10'
} else {
this.op = 'op' + m
}
},
backTop (num: number) {
(this.$refs.content as HTMLDivElement).scrollTop = num
},
addCartFn () {
// 前端判断用户有无登录
if (localStorage.getItem('userid')) {
addCart({
userid: localStorage.getItem('userid')!,
proid: this.proid,
num: 1
}).then(res => {
console.log(res)
if (res) {
showToast({
message: '加入购物车成功'
})
}
})
} else {
this.$router.push('/login')
}
},
toCart () {
if (localStorage.getItem('userid')) {
this.$router.push('/cart')
} else {
this.$router.push('/login')
}
}
}
})
</script>
<style lang="stylus">
// 层级修改背景
.container
.box
.header
background-color #f66
&.detail-header
background-color transparent
.detail-header
position fixed
top 0
width 100%
z-index 999
// opacity 0
// background-color #fff
.header1
position fixed
width 100%
top 0
height 0.44rem
opacity 1
&.op0
opacity 1
&.op1
opacity 0.9
&.op2
opacity 0.8
&.op3
opacity 0.7
&.op4
opacity 0.6
&.op5
opacity 0.5
&.op6
opacity 0.4
&.op7
opacity 0.3
&.op8
opacity 0.2
&.op9
opacity 0.1
&.op10
opacity 0
ul
display flex
height 100%
// justify-content space-between
li
// flex 1
height 100%
display flex
justify-content center
align-items center
color #000
&:nth-child(1), &:nth-child(3){
width 50px
}
&:nth-child(2) {
flex 1
}
.header2
position fixed
width 100%
top 0
color #000
&.op0
opacity 0
&.op1
opacity 0.1
&.op2
opacity 0.2
&.op3
opacity 0.3
&.op4
opacity 0.4
&.op5
opacity 0.5
&.op6
opacity 0.6
&.op7
opacity 0.7
&.op8
opacity 0.8
&.op9
opacity 0.9
&.op10
opacity 1
.header-color
color #000
display flex
li
flex 1
&:nth-child(1) {
margin-right 10px
}
&:nth-child(2) {
margin-left 10px
}
&.active
border-bottom 3px solid #f66
.recommendBox
width 100%
min-height 300px
border-radius 16px
background-color #fff
margin-top 15px
h4
padding 15px
</style>
5.12 购物车功能实现
5.12.1 基本购物车结构
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-card
num="2"
price="2.00"
desc="描述信息"
title="商品标题"
thumb="https://fastly.jsdelivr.net/npm/@vant/assets/ipad.jpeg"
/>
<van-submit-bar :price="3050" button-text="提交订单" >
<van-checkbox >全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({
data () {
return {
cartList: []
}
},
computed: {
empty () {
return this.cartList.length === 0
}
}
})
</script>
<style lang='stylus'>
</style>
5.12.2 请求购物车的数据并且渲染
// src/views/cart/cart.d.ts
export interface ICartItem {
cartid: string
discount: number
flag: boolean
img1: string
num: number
originprice: number
proid: string
proname: string
userid: string
}
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-card
v-for="item of cartList"
:num="item.num"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
/>
<van-submit-bar :price="3050" button-text="提交订单" >
<van-checkbox >全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { getCartListData } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[]
}
export default defineComponent({
data (): IData {
return {
cartList: []
}
},
mounted () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
}
})
},
computed: {
empty () {
return this.cartList.length === 0
}
}
})
</script>
<style lang='stylus'>
</style>
5.12.3 删除数据
异步删除 - 全局变量传值
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell v-for="item of cartList" :key="item.cartid">
<van-card
:num="item.num"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
/>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :price="3050" button-text="提交订单" >
<van-checkbox >全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[]
}
export default defineComponent({
data (): IData {
return {
cartList: []
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
5.12.4 修改购物车数量
使用插槽自定义数量位置,使用步进器组件完成数量更改,传参传对象
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell v-for="item of cartList" :key="item.cartid">
<van-card
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :price="3050" button-text="提交订单" >
<van-checkbox >全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[]
}
export default defineComponent({
data (): IData {
return {
cartList: []
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
5.12.5 计算总价和总数量
总数位0 按钮不可点
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell v-for="item of cartList" :key="item.cartid">
<van-card
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :disabled="totalNum <= 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : '去结算'" >
<van-checkbox >全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[]
}
export default defineComponent({
data (): IData {
return {
cartList: []
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () {
return this.cartList.reduce((sum, item) => {
return sum + item.num
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return sum + item.num * item.originprice
}, 0) * 100 // 单位为分
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
实际上总价以及总数需要选中才计算
5.12.6 全选以及单选
以 layout组件 以及checkbox 组件完成 基本布局
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell style="border-bottom: 1px solid #efefef" v-for="item of cartList" :key="item.cartid">
<van-row style="background: white">
<van-col span="2">
<van-checkbox style="margin-top: 40px;margin-left: 12px" v-model="item.flag"></van-checkbox>
</van-col>
<van-col span="22">
<van-card
style="background: white"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
</van-col>
</van-row>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :disabled="totalNum <= 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : '去结算'" >
<van-checkbox v-model="checked">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[],
checked: boolean
}
export default defineComponent({
data (): IData {
return {
cartList: [],
checked: false
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () {
return this.cartList.reduce((sum, item) => {
return sum + item.num
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return sum + item.num * item.originprice
}, 0) * 100 // 单位为分
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
默认全选不选中,通过列表的数据控制全选的效果
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell style="border-bottom: 1px solid #efefef" v-for="item of cartList" :key="item.cartid">
<van-row style="background: white">
<van-col span="2">
<van-checkbox style="margin-top: 40px;margin-left: 12px" v-model="item.flag"></van-checkbox>
</van-col>
<van-col span="22">
<van-card
style="background: white"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
</van-col>
</van-row>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :disabled="totalNum <= 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : '去结算'" >
<van-checkbox v-model="checked">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[],
checked: boolean
}
export default defineComponent({
data (): IData {
return {
cartList: [],
checked: false
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () {
return this.cartList.reduce((sum, item) => {
return sum + item.num
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return sum + item.num * item.originprice
}, 0) * 100 // 单位为分
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
this.checked = this.cartList.every(item => item.flag)
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
点击全选控制列表选中状态
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell style="border-bottom: 1px solid #efefef" v-for="item of cartList" :key="item.cartid">
<van-row style="background: white">
<van-col span="2">
<van-checkbox style="margin-top: 40px;margin-left: 12px" v-model="item.flag"></van-checkbox>
</van-col>
<van-col span="22">
<van-card
style="background: white"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
</van-col>
</van-row>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :disabled="totalNum <= 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : '去结算'" >
<van-checkbox @click="checkAll" v-model="checked">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum, selectAllData } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[],
checked: boolean
}
export default defineComponent({
data (): IData {
return {
cartList: [],
checked: false
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () {
return this.cartList.reduce((sum, item) => {
return sum + item.num
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return sum + item.num * item.originprice
}, 0) * 100 // 单位为分
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
this.checked = this.cartList.every(item => item.flag)
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
},
checkAll () {
console.log(this.checked)
selectAllData({
userid: localStorage.getItem('userid')!,
type: this.checked
}).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
单个列表选中
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
</div>
<div class="shop-list" v-else>
<van-swipe-cell style="border-bottom: 1px solid #efefef" v-for="item of cartList" :key="item.cartid">
<van-row style="background: white">
<van-col span="2">
<van-checkbox style="margin-top: 40px;margin-left: 12px" v-model="item.flag" @change="selectOne(item)"></van-checkbox>
</van-col>
<van-col span="22">
<van-card
style="background: white"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
</van-col>
</van-row>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-submit-bar :disabled="totalNum <= 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : '去结算'" >
<van-checkbox @click="checkAll" v-model="checked">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum, selectAllData, selectOneData } from '@/api/cart'
import type { ICart } from './cart';
interface IData {
cartList: ICart[],
checked: boolean
}
export default defineComponent({
data (): IData {
return {
cartList: [],
checked: false
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () {
return this.cartList.reduce((sum, item) => {
return item.flag ? sum + item.num : sum + 0
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return item.flag ? sum + item.num * item.originprice: sum + 0
}, 0) * 100 // 单位为分
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
this.checked = this.cartList.every(item => item.flag)
}
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
},
checkAll () {
console.log(this.checked)
selectAllData({
userid: localStorage.getItem('userid')!,
type: this.checked
}).then(() => {
this.getCartListDataFn()
})
},
selectOne (item: ICart) {
console.log(item)
selectOneData({
cartid: item.cartid,
flag: item.flag
}).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
5.12.7 推荐列表实现
<!-- src/views/cart/components/Pro.vue -->
<template>
<ul class="proList">
<li class="proItem" v-for="item of proList" :key="item.proid" @click="toDetail(item.proid)">
<div class="itemImage">
<van-image :src="item.img1"></van-image>
</div>
<div class="itemInfo">
<div class="title van-multi-ellipsis--l2">{{ item.proname }}</div>
<div class="price">¥{{ item.originprice }}</div>
<div class="other">
<van-tag type="danger">{{ item.category }}</van-tag>
</div>
</div>
</li>
<!-- <li class="proItem">
<div class="itemImage">
<van-image src=""></van-image>
</div>
<div class="itemInfo">
<div class="title">产品名称</div>
<div class="price">¥1999</div>
<div class="other">苹果</div>
</div>
</li>
<li class="proItem">
<div class="itemImage">
<van-image src=""></van-image>
</div>
<div class="itemInfo">
<div class="title">产品名称</div>
<div class="price">¥1999</div>
<div class="other">苹果</div>
</div>
</li> -->
</ul>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import type { PropType } from 'vue'
import type { IPro } from '../home'
export default defineComponent({
props: {
proList: Array as PropType<IPro[]>
},
methods: {
toDetail (proid: string) {
this.$router.push(`/detail/${proid}`)
// this.$router.push('/detail/' + proid)
// this.$router.push({ path: '/detail/' + proid })
// this.$router.push({ name: 'detail', params: { proid: proid } })
}
}
})
</script>
<style lang='stylus'>
.proList
display flex
flex-wrap wrap
.proItem
width 46%
margin 8px 2%
min-height 2.6rem
background-color #fff
border-radius 10px
overflow hidden
.itemImage
width 100%
height 1.9rem
.van-image
width 100%
height 100%
display block
.itemInfo
padding 10px
.price
color #f66
margin-top 5px
.other
margin-top 5px
</style>
// src/views/cart/cart.d.ts
export interface ICart {
cartid: string
discount: number
flag: boolean
img1: string
num: number
originprice: number
proid: string
proname: string
userid: string
}
export interface IPro {
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
img2: string
img3: string
img4: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proid: string
proname: string
sales: number
stock: number
}
<!-- src/views/cart/index.vue -->
<template>
<div class="box">
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也" image="https://img1.baidu.com/it/u=1635842218,3687466488&fm=253&fmt=auto&app=120&f=JPEG?w=610&h=500" image-size="120">
<van-button round color="linear-gradient(to right, #ee0a24, #ff6034)" class="bottom-button">立即购物</van-button>
</van-empty>
<van-divider :style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }">可能你喜欢</van-divider>
<Pro :proList="proList" />
</div>
<div class="shop-list" v-else>
<van-swipe-cell style="border-bottom: 1px solid #efefef" v-for="item of cartList" :key="item.cartid">
<van-row style="background: white">
<van-col span="2">
<van-checkbox style="margin-top: 40px;margin-left: 12px" v-model="item.flag" @change="selectOne(item)"></van-checkbox>
</van-col>
<van-col span="22">
<van-card
style="background: white"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper @change="changeNum(item)" v-model="item.num" theme="round" button-size="22" disable-input />
</template>
</van-card>
</van-col>
</van-row>
<template #right>
<van-button square text="删除" @click="deleteItem(item.cartid)" type="danger" class="delete-button" />
</template>
</van-swipe-cell>
<van-divider :style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }">猜你还想要</van-divider>
<Pro :proList="proList" />
<van-submit-bar :disabled="totalNum <= 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : '去结算'" >
<van-checkbox @click="checkAll" v-model="checked">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue';
import { showConfirmDialog } from 'vant'
import { getCartListData, removeOneData, updateOneDataNum, selectAllData, selectOneData, getCartRecommendData } from '@/api/cart'
import type { ICart, IPro } from './cart';
import Pro from './components/Pro.vue'
interface IData {
cartList: ICart[],
checked: boolean,
proList: IPro[]
}
export default defineComponent({
components: {
Pro
},
data (): IData {
return {
cartList: [],
checked: false,
proList: []
}
},
mounted () {
// getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
// if (res.data.code === '10020') {
// this.cartList = []
// } else {
// this.cartList = res.data.data
// }
// })
this.getCartListDataFn()
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () {
return this.cartList.reduce((sum, item) => {
return item.flag ? sum + item.num : sum + 0
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return item.flag ? sum + item.num * item.originprice: sum + 0
}, 0) * 100 // 单位为分
}
},
methods: {
getCartListDataFn () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
// console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
this.checked = this.cartList.every(item => item.flag)
}
// 获取推荐列表
this.getRecommendDataFn()
})
},
getRecommendDataFn () {
// if (this.empty) {
// 接口1
// } else {
// 接口2
// }
getCartRecommendData().then((res) => {
this.proList = res.data.data
})
},
deleteItem (cartid: string) {
showConfirmDialog({
message:
'便宜不等人,请三思而行',
})
.then(() => {
// on confirm
removeOneData({ cartid }).then(() => {
this.getCartListDataFn()
})
})
.catch(() => {
// on cancel
});
},
changeNum (item: ICart) {
console.log(item)
updateOneDataNum({ cartid: item.cartid, num: item.num }).then(() => {
this.getCartListDataFn()
})
},
checkAll () {
console.log(this.checked)
selectAllData({
userid: localStorage.getItem('userid')!,
type: this.checked
}).then(() => {
this.getCartListDataFn()
})
},
selectOne (item: ICart) {
console.log(item)
selectOneData({
cartid: item.cartid,
flag: item.flag
}).then(() => {
this.getCartListDataFn()
})
}
}
})
</script>
<style lang='stylus'>
.goods-card {
margin: 0;
background-color: #f66;
}
.delete-button {
height: 100%;
}
</style>
5.12.8 分析接下来思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3GGGsXA4-1673602369095)(assets/未命名文件.png)]
5.13 提交订单
5.13.1 封装订单相关接口
// src/api/order.ts
import request from '../service/request'
// 添加订单
export function addOrderData (params: { userid: string }) {
return request.post('/order/addOrder', params)
}
// 获取订单信息
export function getOrderListData (params: { userid: string, time: string }) {
return request.get('/order/confirmOrder', {params})
}
export interface IOrderAddress {
userid: string
time: string
name: string
tel: string
province: string
city: string
county: string
addressDetail: string
}
// 获取订单信息
export function updateOrderAddressData (params: IOrderAddress) {
return request.post('/order/updateOrderAddress', params)
}
<!-- src/views/cart/IndexView.vue -->
<template>
<header class="header">
<van-nav-bar
title="购物车"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<div class="no-shop" v-if="empty">
<van-empty description="购物车空空如也">
<van-button round type="danger" class="bottom-button" @click="$router.push('/kind')">立即购物</van-button>
</van-empty>
<van-divider
:style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }"
>
快点来看看
</van-divider>
<ProComponent :list="proList"></ProComponent>
</div>
<div class="shop-list" v-else>
<van-swipe-cell
v-for="item of cartList"
:key="item.cartid"
:before-close="beforeClose"
>
<van-row class="myItem">
<van-col span="2">
<van-checkbox v-model="item.flag" @change="selectOne(item)"></van-checkbox>
</van-col>
<van-col span="22">
<van-card
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
>
<template #num>
<van-stepper v-model="item.num" theme="round" button-size="22" disable-input @change="updateNum(item)" />
</template>
</van-card>
</van-col>
</van-row>
<template #right>
<van-button square text="删除" type="danger" @click="getDeleteId(item.cartid)" class="delete-button" />
</template>
</van-swipe-cell>
<van-divider
:style="{ color: '#1989fa', borderColor: '#1989fa', padding: '0 16px' }"
>
可能你想要
</van-divider>
<ProComponent :list="proList"></ProComponent>
<van-submit-bar @click="submit" :disabled="totalNum === 0" :price="totalPrice" :button-text="totalNum > 0 ? `去结算(${totalNum})` : `去结算`">
<van-checkbox v-model="checked" @click="changeType">全选</van-checkbox>
</van-submit-bar>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Dialog } from 'vant'
import { getCartListData, removeOneData, selectAllData, selectOneData, updateOneDataNum, getCartRecommendData } from '@/api/cart'
import { addOrderData } from '@/api/order'
import type { ICartItem } from './cart'
import type { IPro } from '../home/home';
import ProComponent from './components/ProComponent.vue'
interface IData {
cartList: ICartItem[],
id: string
checked: boolean
proList: IPro[]
}
export default defineComponent({
components: {
ProComponent
},
data (): IData {
return {
cartList: [],
id: '',
checked: false,
proList: []
}
},
computed: {
empty () {
return this.cartList.length === 0
},
totalNum () { // 选中才累加
return this.cartList.reduce((sum, item) => {
return item.flag ? sum += item.num : sum += 0
}, 0)
},
totalPrice () {
return this.cartList.reduce((sum, item) => {
return item.flag ? sum += item.num * item.originprice : sum += 0
}, 0) * 100
}
},
mounted () {
if (localStorage.getItem('userid')) {
this.getCartList()
} else {
this.$router.push('/login')
}
getCartRecommendData().then(res => {
this.proList = res.data.data
})
},
methods: {
getCartList () {
getCartListData({ userid: localStorage.getItem('userid')! }).then(res => {
console.log(res.data)
if (res.data.code === '10020') {
this.cartList = []
} else {
this.cartList = res.data.data
this.checked = this.cartList.every(item => item.flag)
}
})
},
beforeClose ({ position }: { position: 'left' | 'right' | 'cell' | 'outside'}): any {
console.log(position)
switch (position) {
case 'right':
return new Promise<void>((resolve) => {
Dialog.confirm({
title: '确定删除吗?',
}).then(() => {
removeOneData({cartid: this.id}).then(() => {
this.getCartList() // 重置列表
this.id = ''
resolve()
})
});
});
case 'outside':
return true
}
},
getDeleteId (cartid: string) {
console.log(cartid)
this.id =cartid
},
updateNum ({ cartid, num }: ICartItem) {
console.log(cartid, num)
updateOneDataNum({ cartid, num }).then(() => {
this.getCartList()
})
},
changeType () {
console.log(this.checked)
selectAllData({ userid: localStorage.getItem('userid')!, type: this.checked}).then(() => {
this.getCartList()
})
},
selectOne ({cartid, flag}: ICartItem) {
console.log(cartid, flag)
selectOneData({cartid, flag}).then(() => {
this.getCartList()
})
},
submit () { // +++++++++++
addOrderData({ userid: localStorage.getItem('userid')! }).then(res => {
console.log(res.data)
// this.$router.push('/order/' + res.data.time) // /order/:time
this.$router.push('/order?time=' + res.data.time) // /order
})
}
}
})
</script>
<style lang="scss" scoped>
.delete-button {
height: 100%;
}
.myItem {
background: var(--van-card-background-color);
.van-col--2 {
display: flex;
justify-content: center;
align-items: center;
}
}
</style>
5.14 确定订单实现
5.14.1 创建页面以及更新路由
<!-- src/views/order/IndexView.vue -->
<template>
<header class="header">
<van-nav-bar
title="确定订单"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">order content</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({})
</script>
<style lang="scss"></style>
// src/router/index.ts
// 1.引入 创建路由的模块 以及 路由历史记录模式的模块
// 路由的历史模式:
// createWebHistory HTML5模式 /home /kind /cart /user 需要后端配合
// createWebHashHistory Hash 模式 /#/home /#/kind /#/cart /#/user
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router' // 路由规则的类型 ---- 路由规则的类型注解
// 2.引入页面组件
import Footer from '../components/FooterComponent.vue'
import HomeView from './../views/home/IndexView.vue'
import KindView from './../views/kind/IndexView.vue'
import CartView from './../views/cart/IndexView.vue'
import UserView from './../views/user/IndexView.vue'
import DetailView from '../views/detail/IndexView.vue'
import RegisterView from '../views/register/IndexView.vue'
import CheckTelComponent from '../views/register/components/CheckTelComponent.vue'
import SendTelCodeComponent from '../views/register/components/SendTelCodeComponent.vue'
import SetPasswordComponent from '../views/register/components/SetPasswordComponent.vue'
import LoginView from '../views/login/IndexView.vue'
import OrderView from '../views/order/IndexView.vue' // ++++++++++
// 3.构建路由的规则
const routes: RouteRecordRaw[] = [
{ // 路由的重定向
path: '/',
redirect: '/home'
},
{
path: '/home', // 地址栏地址 - 路由
name: 'home', // 命名路由 --- 唯一性
// component: HomeView // 路由映射的页面组件
components: {
default: HomeView,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: KindView
components: {
default: KindView,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: CartView
components: {
default: CartView,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: UserView
components: {
default: UserView,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
// component: DetailView
components: {
default: DetailView
}
},
{
path: '/register',
name: 'register',
redirect: '/register/index',
component: RegisterView,
children: [
{
path: 'index',
component: CheckTelComponent
},
{
path: 'sms',
component: SendTelCodeComponent
},
{
path: 'pwd',
component: SetPasswordComponent
}
]
},
{
path: '/login',
name: 'login',
component: LoginView
},
{ // ++++++++++
path: '/order',
name: 'order',
component: OrderView
}
]
// 4.生成路由
const router: Router = createRouter({
// vue2 vue-router3 history: 'history' | 'hash'
history: createWebHistory(),
routes // routes: routes 简写形式
})
// 5.暴露路由
export default router
5.14.2 布局
<!-- src/views/order/IndexView.vue -->
<template>
<header class="header">
<van-nav-bar
title="确定订单"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<van-cell center v-if="flag" title="请添加地址" is-link />
<van-cell center v-else title="musk 18813007814" is-link label="陕西省西安市雁塔区千锋教育" />
<van-card
v-for="item of orderList"
:key="item.orderid"
:num="item.num"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
/>
<div class="tip">
<p><span>原价:</span><span>{{ totalPrice }}</span></p>
<p><span>快递费:</span><span>{{ express }}</span></p>
</div>
<van-submit-bar :price="totalPrice * 100 + express * 100" button-text="去支付" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getOrderListData } from '../../api/order'
interface IOrderItem {
addressDetail: string
city: string
county: string
discount: number
img1: string
name: string
num: number
orderid: string
originprice: number
proid: string
proname: string
province: string
status:number
tel: string
time: string
userid: string
}
interface IData {
orderList: IOrderItem[],
time: string
express: number
}
export default defineComponent({
data (): IData {
return {
orderList: [],
time: '',
express: 0
}
},
computed: {
totalPrice () {
return this.orderList.reduce((sum, item) => {
return sum += item.num * item.originprice
}, 0)
},
flag () { // 显示地址
return this.orderList[0]?.tel === ''
}
},
mounted () {
// console.log(this.$route)
this.time = (this.$route.query.time as string)
this.express = Math.floor(Math.random() * 20)
const userid = localStorage.getItem('userid')!
getOrderListData({ time: this.time, userid }).then(res => {
console.log(res.data)
this.orderList = res.data.data
})
}
})
</script>
<style lang="scss" scoped>
.tip {
margin-top: 15px;
margin-right: 10px;
text-align: right;
p {
display: flex;
span {
&:nth-child(1) {
flex: 1;
}
&:nth-child(2) {
width: 40px;
}
}
}
}
</style>
5.14.3 点击选择地址
点击进入地址选择页面
<!-- src/views/order/AddressListView.vue -->
<template>
<header class="header"><van-nav-bar
title="地址列表"
left-arrow
@click-left="$router.back()"
/></header>
<div class="content">
<van-address-list
v-model="chosenAddressId"
:list="list"
default-tag-text="默认"
@add="onAdd"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
data () {
return {
chosenAddressId: '',
list: [],
time: ''
}
},
mounted () {
this.time = (this.$route.query.time as string)
},
methods: {
onAdd () {
this.$router.push('/orderAddAddress?time=' + this.time)
}
}
})
</script>
<style lang="scss"></style>
// src/router/index.ts
// 1.引入 创建路由的模块 以及 路由历史记录模式的模块
// 路由的历史模式:
// createWebHistory HTML5模式 /home /kind /cart /user 需要后端配合
// createWebHashHistory Hash 模式 /#/home /#/kind /#/cart /#/user
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router' // 路由规则的类型 ---- 路由规则的类型注解
// 2.引入页面组件
import Footer from '../components/FooterComponent.vue'
import HomeView from './../views/home/IndexView.vue'
import KindView from './../views/kind/IndexView.vue'
import CartView from './../views/cart/IndexView.vue'
import UserView from './../views/user/IndexView.vue'
import DetailView from '../views/detail/IndexView.vue'
import RegisterView from '../views/register/IndexView.vue'
import CheckTelComponent from '../views/register/components/CheckTelComponent.vue'
import SendTelCodeComponent from '../views/register/components/SendTelCodeComponent.vue'
import SetPasswordComponent from '../views/register/components/SetPasswordComponent.vue'
import LoginView from '../views/login/IndexView.vue'
import OrderView from '../views/order/IndexView.vue'
import OrderAddressListView from '../views/order/AddressListView.vue'
// 3.构建路由的规则
const routes: RouteRecordRaw[] = [
{ // 路由的重定向
path: '/',
redirect: '/home'
},
{
path: '/home', // 地址栏地址 - 路由
name: 'home', // 命名路由 --- 唯一性
// component: HomeView // 路由映射的页面组件
components: {
default: HomeView,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: KindView
components: {
default: KindView,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: CartView
components: {
default: CartView,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: UserView
components: {
default: UserView,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
// component: DetailView
components: {
default: DetailView
}
},
{
path: '/register',
name: 'register',
redirect: '/register/index',
component: RegisterView,
children: [
{
path: 'index',
component: CheckTelComponent
},
{
path: 'sms',
component: SendTelCodeComponent
},
{
path: 'pwd',
component: SetPasswordComponent
}
]
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/order',
name: 'order',
component: OrderView
},
{ // ++++++++
path: '/orderAddress',
name: 'orderAddress',
component: OrderAddressListView
}
]
// 4.生成路由
const router: Router = createRouter({
// vue2 vue-router3 history: 'history' | 'hash'
history: createWebHistory(),
routes // routes: routes 简写形式
})
// 5.暴露路由
export default router
点击进入地址列表页面
<!-- src/views/order/IndexView.vue -->
<template>
<header class="header">
<van-nav-bar
title="确定订单"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<van-cell center v-if="flag" title="请添加地址" is-link @click="toAddressList" />
<van-cell center v-else title="musk 18813007814" is-link label="陕西省西安市雁塔区千锋教育" @click="toAddressList" />
<van-card
v-for="item of orderList"
:key="item.orderid"
:num="item.num"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
/>
<div class="tip">
<p><span>原价:</span><span>{{ totalPrice }}</span></p>
<p><span>快递费:</span><span>{{ express }}</span></p>
</div>
<van-submit-bar :price="totalPrice * 100 + express * 100" button-text="去支付" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getOrderListData } from '../../api/order'
interface IOrderItem {
addressDetail: string
city: string
county: string
discount: number
img1: string
name: string
num: number
orderid: string
originprice: number
proid: string
proname: string
province: string
status:number
tel: string
time: string
userid: string
}
interface IData {
orderList: IOrderItem[],
time: string
express: number
}
export default defineComponent({
data (): IData {
return {
orderList: [],
time: '',
express: 0
}
},
computed: {
totalPrice () {
return this.orderList.reduce((sum, item) => {
return sum += item.num * item.originprice
}, 0)
},
flag () { // 显示地址
return this.orderList[0]?.tel === ''
}
},
mounted () {
// console.log(this.$route)
this.time = (this.$route.query.time as string)
this.express = Math.floor(Math.random() * 20)
const userid = localStorage.getItem('userid')!
getOrderListData({ time: this.time, userid }).then(res => {
console.log(res.data)
this.orderList = res.data.data
})
},
methods: {
toAddressList () { // ++++++++++++
this.$router.push('/orderAddress?time=' + this.time)
}
}
})
</script>
<style lang="scss" scoped>
.tip {
margin-top: 15px;
margin-right: 10px;
text-align: right;
p {
display: flex;
span {
&:nth-child(1) {
flex: 1;
}
&:nth-child(2) {
width: 40px;
}
}
}
}
</style>
5.14.4 添加地址页面
cnpm i -S @vant/area-data
<!-- src/views/order/AddAddressView.vue -->
<template>
<header class="header"><van-nav-bar
title="添加地址"
left-arrow
@click-left="$router.back()"
/></header>
<div class="content">
<van-address-edit
:area-list="areaList"
show-set-default
:area-columns-placeholder="['请选择', '请选择', '请选择']"
@save="onSave"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { areaList } from '@vant/area-data';
export default defineComponent({
data () {
return {
areaList
}
},
methods: {
onSave (content: any) {
console.log(content)
}
}
})
</script>
<style lang="scss"></style>
// src/router/index.ts
// 1.引入 创建路由的模块 以及 路由历史记录模式的模块
// 路由的历史模式:
// createWebHistory HTML5模式 /home /kind /cart /user 需要后端配合
// createWebHashHistory Hash 模式 /#/home /#/kind /#/cart /#/user
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router' // 路由规则的类型 ---- 路由规则的类型注解
// 2.引入页面组件
import Footer from '../components/FooterComponent.vue'
import HomeView from './../views/home/IndexView.vue'
import KindView from './../views/kind/IndexView.vue'
import CartView from './../views/cart/IndexView.vue'
import UserView from './../views/user/IndexView.vue'
import DetailView from '../views/detail/IndexView.vue'
import RegisterView from '../views/register/IndexView.vue'
import CheckTelComponent from '../views/register/components/CheckTelComponent.vue'
import SendTelCodeComponent from '../views/register/components/SendTelCodeComponent.vue'
import SetPasswordComponent from '../views/register/components/SetPasswordComponent.vue'
import LoginView from '../views/login/IndexView.vue'
import OrderView from '../views/order/IndexView.vue'
import OrderAddressListView from '../views/order/AddressListView.vue'
import OrderAddAddressView from '../views/order/AddAddressView.vue'
// 3.构建路由的规则
const routes: RouteRecordRaw[] = [
{ // 路由的重定向
path: '/',
redirect: '/home'
},
{
path: '/home', // 地址栏地址 - 路由
name: 'home', // 命名路由 --- 唯一性
// component: HomeView // 路由映射的页面组件
components: {
default: HomeView,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: KindView
components: {
default: KindView,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: CartView
components: {
default: CartView,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: UserView
components: {
default: UserView,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
// component: DetailView
components: {
default: DetailView
}
},
{
path: '/register',
name: 'register',
redirect: '/register/index',
component: RegisterView,
children: [
{
path: 'index',
component: CheckTelComponent
},
{
path: 'sms',
component: SendTelCodeComponent
},
{
path: 'pwd',
component: SetPasswordComponent
}
]
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/order',
name: 'order',
component: OrderView
},
{
path: '/orderAddress',
name: 'orderAddress',
component: OrderAddressListView
},
{ // +++++++++
path: '/orderAddAddress',
name: 'orderAddAddress',
component: OrderAddAddressView
}
]
// 4.生成路由
const router: Router = createRouter({
// vue2 vue-router3 history: 'history' | 'hash'
history: createWebHistory(),
routes // routes: routes 简写形式
})
// 5.暴露路由
export default router
添加并使用该地址(添加i地址进入地址列表页面,修改订单的地址)
5.14.5 封装地址相关接口
// src/api/address.ts
import request from '../service/request'
export interface IAddress {
userid: string
name: string
tel: string
province: string
city: string
county: string
addressDetail: string
isDefault: boolean
}
export function addAddRessData (params: IAddress ) {
return request.post('/address/add', params)
}
export function getAddressList (params: {userid: string} ) {
return request.get('/address/add', {params})
}
5.14.6 添加地址实现
<!-- src/views/order/AddAddressView.vue -->
<template>
<header class="header"><van-nav-bar
title="添加地址"
left-arrow
@click-left="$router.back()"
/></header>
<div class="content">
<van-address-edit
:area-list="areaList"
show-set-default
:area-columns-placeholder="['请选择', '请选择', '请选择']"
@save="onSave"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { areaList } from '@vant/area-data';
import { addAddRessData } from '@/api/address';
import { updateOrderAddressData } from '@/api/order';
export default defineComponent({
data () {
return {
areaList,
time: ''
}
},
mounted () {
this.time = (this.$route.query.time as string)
},
methods: {
onSave (content: any) {
console.log(content)
addAddRessData(content).then(res => {
console.log(res.data) // 添加地址成功
// 修改订单地址
content.userid = localStorage.getItem('userid')!
content.time = this.time
updateOrderAddressData(content).then(res => {
// 返回前两页 确认订单- 地址列表 - 新增地址
this.$router.go(-2)
})
})
}
}
})
</script>
<style lang="scss"></style>
5.14.7 确认订单页面处理地址
计算属性即可完成
<!-- src/views/order/IndexView.vue -->
<template>
<header class="header">
<van-nav-bar
title="确定订单"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<van-cell center v-if="flag" title="请添加地址" is-link @click="toAddressList" />
<van-cell center v-else :title="name + ' ' + tel" is-link :label="address" @click="toAddressList" />
<van-card
v-for="item of orderList"
:key="item.orderid"
:num="item.num"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
/>
<div class="tip">
<p><span>原价:</span><span>{{ totalPrice }}</span></p>
<p><span>快递费:</span><span>{{ express }}</span></p>
</div>
<van-submit-bar :price="totalPrice * 100 + express * 100" button-text="去支付" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getOrderListData } from '../../api/order'
interface IOrderItem {
addressDetail: string
city: string
county: string
discount: number
img1: string
name: string
num: number
orderid: string
originprice: number
proid: string
proname: string
province: string
status:number
tel: string
time: string
userid: string
}
interface IData {
orderList: IOrderItem[],
time: string
express: number
}
export default defineComponent({
data (): IData {
return {
orderList: [],
time: '',
express: 0
}
},
computed: {
totalPrice () {
return this.orderList.reduce((sum, item) => {
return sum += item.num * item.originprice
}, 0)
},
flag () { // 显示地址
return this.orderList[0]?.tel === ''
},
name () {
return this.orderList[0]?.name
},
tel () {
return this.orderList[0]?.tel
},
e="item.proname"
:thumb="item.img1"
/>
<div class="tip">
<p><span>原价:</span><span>{{ totalPrice }}</span></p>
<p><span>快递费:</span><span>{{ express }}</span></p>
</div>
<van-submit-bar :price="totalPrice * 100 + express * 100" button-text="去支付" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getOrderListData } from '../../api/order'
interface IOrderItem {
addressDetail: string
city: string
county: string
discount: number
img1: string
name: string
num: number
orderid: string
originprice: number
proid: string
proname: string
province: string
status:number
tel: string
time: string
userid: string
}
interface IData {
orderList: IOrderItem[],
time: string
express: number
}
export default defineComponent({
data (): IData {
return {
orderList: [],
time: '',
express: 0
}
},
computed: {
totalPrice () {
return this.orderList.reduce((sum, item) => {
return sum += item.num * item.originprice
}, 0)
},
flag () { // 显示地址
return this.orderList[0]?.tel === ''
}
},
mounted () {
// console.log(this.$route)
this.time = (this.$route.query.time as string)
this.express = Math.floor(Math.random() * 20)
const userid = localStorage.getItem('userid')!
getOrderListData({ time: this.time, userid }).then(res => {
console.log(res.data)
this.orderList = res.data.data
})
},
methods: {
toAddressList () { // ++++++++++++
this.$router.push('/orderAddress?time=' + this.time)
}
}
})
</script>
<style lang="scss" scoped>
.tip {
margin-top: 15px;
margin-right: 10px;
text-align: right;
p {
display: flex;
span {
&:nth-child(1) {
flex: 1;
}
&:nth-child(2) {
width: 40px;
}
}
}
}
</style>
5.14.4 添加地址页面
cnpm i -S @vant/area-data
<!-- src/views/order/AddAddressView.vue -->
<template>
<header class="header"><van-nav-bar
title="添加地址"
left-arrow
@click-left="$router.back()"
/></header>
<div class="content">
<van-address-edit
:area-list="areaList"
show-set-default
:area-columns-placeholder="['请选择', '请选择', '请选择']"
@save="onSave"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { areaList } from '@vant/area-data';
export default defineComponent({
data () {
return {
areaList
}
},
methods: {
onSave (content: any) {
console.log(content)
}
}
})
</script>
<style lang="scss"></style>
// src/router/index.ts
// 1.引入 创建路由的模块 以及 路由历史记录模式的模块
// 路由的历史模式:
// createWebHistory HTML5模式 /home /kind /cart /user 需要后端配合
// createWebHashHistory Hash 模式 /#/home /#/kind /#/cart /#/user
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw, Router } from 'vue-router' // 路由规则的类型 ---- 路由规则的类型注解
// 2.引入页面组件
import Footer from '../components/FooterComponent.vue'
import HomeView from './../views/home/IndexView.vue'
import KindView from './../views/kind/IndexView.vue'
import CartView from './../views/cart/IndexView.vue'
import UserView from './../views/user/IndexView.vue'
import DetailView from '../views/detail/IndexView.vue'
import RegisterView from '../views/register/IndexView.vue'
import CheckTelComponent from '../views/register/components/CheckTelComponent.vue'
import SendTelCodeComponent from '../views/register/components/SendTelCodeComponent.vue'
import SetPasswordComponent from '../views/register/components/SetPasswordComponent.vue'
import LoginView from '../views/login/IndexView.vue'
import OrderView from '../views/order/IndexView.vue'
import OrderAddressListView from '../views/order/AddressListView.vue'
import OrderAddAddressView from '../views/order/AddAddressView.vue'
// 3.构建路由的规则
const routes: RouteRecordRaw[] = [
{ // 路由的重定向
path: '/',
redirect: '/home'
},
{
path: '/home', // 地址栏地址 - 路由
name: 'home', // 命名路由 --- 唯一性
// component: HomeView // 路由映射的页面组件
components: {
default: HomeView,
footer: Footer
}
},
{
path: '/kind',
name: 'kind',
// component: KindView
components: {
default: KindView,
footer: Footer
}
},
{
path: '/cart',
name: 'cart',
// component: CartView
components: {
default: CartView,
footer: Footer
}
},
{
path: '/user',
name: 'user',
// component: UserView
components: {
default: UserView,
footer: Footer
}
},
{
path: '/detail/:proid',
name: 'detail',
// component: DetailView
components: {
default: DetailView
}
},
{
path: '/register',
name: 'register',
redirect: '/register/index',
component: RegisterView,
children: [
{
path: 'index',
component: CheckTelComponent
},
{
path: 'sms',
component: SendTelCodeComponent
},
{
path: 'pwd',
component: SetPasswordComponent
}
]
},
{
path: '/login',
name: 'login',
component: LoginView
},
{
path: '/order',
name: 'order',
component: OrderView
},
{
path: '/orderAddress',
name: 'orderAddress',
component: OrderAddressListView
},
{ // +++++++++
path: '/orderAddAddress',
name: 'orderAddAddress',
component: OrderAddAddressView
}
]
// 4.生成路由
const router: Router = createRouter({
// vue2 vue-router3 history: 'history' | 'hash'
history: createWebHistory(),
routes // routes: routes 简写形式
})
// 5.暴露路由
export default router
添加并使用该地址(添加i地址进入地址列表页面,修改订单的地址)
5.14.5 封装地址相关接口
// src/api/address.ts
import request from '../service/request'
export interface IAddress {
userid: string
name: string
tel: string
province: string
city: string
county: string
addressDetail: string
isDefault: boolean
}
export function addAddRessData (params: IAddress ) {
return request.post('/address/add', params)
}
export function getAddressList (params: {userid: string} ) {
return request.get('/address/add', {params})
}
5.14.6 添加地址实现
<!-- src/views/order/AddAddressView.vue -->
<template>
<header class="header"><van-nav-bar
title="添加地址"
left-arrow
@click-left="$router.back()"
/></header>
<div class="content">
<van-address-edit
:area-list="areaList"
show-set-default
:area-columns-placeholder="['请选择', '请选择', '请选择']"
@save="onSave"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { areaList } from '@vant/area-data';
import { addAddRessData } from '@/api/address';
import { updateOrderAddressData } from '@/api/order';
export default defineComponent({
data () {
return {
areaList,
time: ''
}
},
mounted () {
this.time = (this.$route.query.time as string)
},
methods: {
onSave (content: any) {
console.log(content)
addAddRessData(content).then(res => {
console.log(res.data) // 添加地址成功
// 修改订单地址
content.userid = localStorage.getItem('userid')!
content.time = this.time
updateOrderAddressData(content).then(res => {
// 返回前两页 确认订单- 地址列表 - 新增地址
this.$router.go(-2)
})
})
}
}
})
</script>
<style lang="scss"></style>
5.14.7 确认订单页面处理地址
计算属性即可完成
<!-- src/views/order/IndexView.vue -->
<template>
<header class="header">
<van-nav-bar
title="确定订单"
left-arrow
@click-left="$router.back()"
/>
</header>
<div class="content">
<van-cell center v-if="flag" title="请添加地址" is-link @click="toAddressList" />
<van-cell center v-else :title="name + ' ' + tel" is-link :label="address" @click="toAddressList" />
<van-card
v-for="item of orderList"
:key="item.orderid"
:num="item.num"
:price="item.originprice"
:title="item.proname"
:thumb="item.img1"
/>
<div class="tip">
<p><span>原价:</span><span>{{ totalPrice }}</span></p>
<p><span>快递费:</span><span>{{ express }}</span></p>
</div>
<van-submit-bar :price="totalPrice * 100 + express * 100" button-text="去支付" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { getOrderListData } from '../../api/order'
interface IOrderItem {
addressDetail: string
city: string
county: string
discount: number
img1: string
name: string
num: number
orderid: string
originprice: number
proid: string
proname: string
province: string
status:number
tel: string
time: string
userid: string
}
interface IData {
orderList: IOrderItem[],
time: string
express: number
}
export default defineComponent({
data (): IData {
return {
orderList: [],
time: '',
express: 0
}
},
computed: {
totalPrice () {
return this.orderList.reduce((sum, item) => {
return sum += item.num * item.originprice
}, 0)
},
flag () { // 显示地址
return this.orderList[0]?.tel === ''
},
name () {
return this.orderList[0]?.name
},
tel () {
return this.orderList[0]?.tel
},
a
更多推荐
所有评论(0)