课程名称: 2022持续升级 Vue3 从入门到实战 掌握完整知识体系
课程章节: 商家展示功能开发(下)
主讲老师: Dell
课程内容:
今天学习的内容包括:
继续开发展示功能
课程收获:
11.1 心得:
按键修饰符:enter,tab,delete,esc,up,down,left,right(只有你点击回车,tab,backspace键时才会执行事件函数)
鼠标修饰符:left,right,middle(当鼠标左键,右键,中键点击时才会执行事件函数)
精确修饰符:exact
@click.ctrl.exact表示的是必须是按住ctrl键再点击元素才可以触发事件,如果不加exact的话表示只要包含ctrl键都可以触发事件
11.2/11.3 心得:
源代码:
ShopInfo.vue:
<template>
<div class="shop">
<img :src="item.imgUrl" class="shop__img">
<div
:class="{'shop__content': true, 'shop__content--bordered': hideBorder ? false: true}"
>
<div class="shop__content__title">{{item.name}}</div>
<div class="shop__content__tags">
<span class="shop__content__tag">月售: {{item.sales}}</span>
<span class="shop__content__tag">起送: {{item.expressLimit}}</span>
<span class="shop__content__tag">基础运费: {{item.expressPrice}}</span>
</div>
<p class="shop__content__highlight">{{item.slogan}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'ShopInfo',
props: ['item', 'hideBorder']
}
</script>
<style lang="scss" scoped>
@import '../style/viriables.scss';
.shop {
display: flex;
padding-top: .12rem;
&__img {
margin-right: .16rem;
width: .56rem;
height: .56rem;
}
&__content {
flex: 1;
padding-bottom: .12rem;
&--bordered {
border-bottom: 1px solid $content-bgColor;
}
&__title {
line-height: .22rem;
font-size: .16rem;
color: $content-fontcolor;
}
&__tags {
margin-top: .08rem;
line-height: .18rem;
font-size: .13rem;
color: $content-fontcolor;
}
&__tag {
margin-right: .16rem;
}
&__highlight {
margin: .08rem 0 0 0;
line-height: .18rem;
font-size: .13rem;
color: $hightlight-fontColor;
}
}
}
</style>
Toast.vue:
<template>
<div class="toast">{{message}}</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
props: ['message']
}
export const useToastEffect = () => {
const toastData = reactive({
show: false,
toastMessage: ''
})
const showToast = (message) => {
toastData.show = true
toastData.toastMessage = message
setTimeout(() => {
toastData.show = false
toastData.toastMessage = ''
}, 2000)
}
const { show, toastMessage } = toRefs(toastData)
return { show, toastMessage, showToast }
}
</script>
<style lang="scss" scoped>
@import '../style/viriables.scss';
.toast {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: .1rem;
background: rgba(0, 0, 0, .35);
border-radius: .05rem;
color: $bgColor;
}
</style>
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '../views/home/Home')
}, {
path: '/shop/:id',
name: 'Shop',
component: () => import(/* webpackChunkName: "shop" */ '../views/shop/Shop')
}, {
path: '/register',
name: 'Register',
component: () => import(/* webpackChunkName: "register" */ '../views/register/Register'),
beforeEnter(to, from, next) {
const { isLogin } = localStorage;
isLogin ? next({ name: 'Home'}): next();
}
}, {
path: '/login',
name: 'Login',
component: () => import(/* webpackChunkName: "login" */ '../views/login/Login'),
beforeEnter(to, from, next) {
const { isLogin } = localStorage;
isLogin ? next({ name: 'Home'}): next();
}
},
// {
// path: '/about',
// name: 'About',
// // route level code-splitting
// // this generates a separate chunk (about.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
//
// }
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
router.beforeEach((to, from ,next) => {
const { isLogin } = localStorage;
const { name } = to;
const isLoginOrRegister = (name === "Login" || name === "Register");
(isLogin || isLoginOrRegister) ? next() : next({ name: 'Login'});
})
export default router
import Vuex from 'vuex'
export default Vuex.createStore({
state: {
cartList: {
// 第一层级是商铺的id
// shopId: {
// 第二层是商品id
// 第二层内容是商品内容以及购物数量
// productId: {
// _id: '1',
// name: '番茄250g/份',
// imgUrl: 'http://www.dell-lee.com/imgs/vue3/tomato.png',
// sales: 10,
// price: 33.6,
// oldPrice: 39.6,
// count: 2
// },
// },
}
},
mutations: {
addItemToCart(state, payload) {
const { shopId, productId, productInfo } = payload;
let shopInfo = state.cartList[shopId]
if(!shopInfo) { shopInfo = {} }
let product = shopInfo[productId]
if(!product) {
product = productInfo
product.count = 0
}
product.count += 1
shopInfo[productId] = product
state.cartList[shopId] = shopInfo
}
},
actions: {
},
modules: {
}
})
import axios from 'axios'
const instance = axios.create({
baseURL: 'https://www.fastmock.site/mock/ae8e9031947a302fed5f92425995aa19/jd',
timeout: 10000
})
export const get = (url, params = {}) => {
return new Promise((resolve, reject) => {
instance.get(url, { params }).then((response) => {
resolve(response.data)
}, err => {
reject(err)
})
})
}
export const post = (url, data = {}) => {
return new Promise((resolve, reject) => {
instance.post(url, data, {
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
resolve(response.data)
}, err => {
reject(err)
})
})
}
11.4 心得:
<template>
<div class="wrapper">
<img class="wrapper__img" src="http://www.dell-lee.com/imgs/vue3/user.png"/>
<div class="wrapper__input">
<input
class="wrapper__input__content"
placeholder="请输入用户名"
v-model="username"
/>
</div>
<div class="wrapper__input">
<input
type="password"
class="wrapper__input__content"
placeholder="请输入密码"
autocomplete="new-password"
v-model="password"
/>
</div>
<div class="wrapper__input">
<input
class="wrapper__input__content"
placeholder="确认密码"
type="password"
v-model="ensurement"
/>
</div>
<div class="wrapper__register-button" @click="handleRegister">注册</div>
<div class="wrapper__register-link" @click="handleLoginClick">已有账号去登陆</div>
<Toast v-if="show" :message="toastMessage"/>
</div>
</template>
<script>
import { useRouter } from 'vue-router';
import { reactive, toRefs } from 'vue'
import { post } from '../../utils/request'
import Toast, { useToastEffect } from '../../components/Toast'
// 处理注册相关逻辑
const useRegisterEffect = (showToast) => {
const router = useRouter()
const data = reactive({
username: '',
password: '',
ensurement: ''
})
const handleRegister = async () => {
try {
const result = await post('/api/user/register', {
username: data.username,
password: data.password
})
if (result?.errno === 0) {
router.push({ name: 'Login' })
} else {
showToast('注册失败')
}
} catch (e) {
showToast('请求失败')
}
}
const { username, password, ensurement } = toRefs(data)
return { username, password, ensurement, handleRegister}
}
// 处理登陆跳转
const useLoginEffect = () => {
const router = useRouter()
const handleLoginClick = () => {
router.push({ name: 'Login'});
}
return { handleLoginClick }
}
export default {
name: 'Register',
components: { Toast },
setup() {
const { show, toastMessage, showToast } = useToastEffect()
const { username, password, ensurement, handleRegister } = useRegisterEffect(showToast)
const { handleLoginClick } = useLoginEffect();
return {
username, password, ensurement, show, toastMessage,
handleRegister,handleLoginClick
}
}
}
</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
.wrapper {
position: absolute;
top: 50%;
left: 0;
right: 0;
transform: translateY(-50%);
&__img {
display: block;
margin: 0 auto .4rem auto;
width: .66rem;
height: .66rem;
}
&__input {
height: .48rem;
margin: 0 .4rem .16rem .4rem;
padding: 0 .16rem;
background: #F9F9F9;
border: 1px solid rgba(0,0,0,0.10);
border-radius: 6px;
border-radius: 6px;
&__content {
line-height: .48rem;
border: none;
outline: none;
width: 100%;
background: none;
font-size: .16rem;
color: $content-notice-fontcolor;
&::placeholder {
color: $content-notice-fontcolor;
}
}
}
&__register-button {
margin: .32rem .4rem .16rem .4rem;
line-height: .48rem;
background: $btn-bgColor;
box-shadow: 0 .04rem .08rem 0 rgba(0,145,255,0.32);
border-radius: .04rem;
border-radius: .04rem;
color: $bgColor;
font-size: .16rem;
text-align: center;
}
&__register-link {
text-align: center;
font-size: .14rem;
color: $content-notice-fontcolor;
}
}
</style>
11.5/11.6 心得:
export default {
name: 'Cart',
setup() {
const route = useRoute();
const shopId = route.params.id;
const { changeCartItemInfo } = useCommonCartEffect()
const { total, price, productList } = useCartEffect(shopId)
return { total, price, shopId, productList,changeCartItemInfo}
}
Toast.vue:
<template>
<div class="toast">{{message}}</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
props: ['message']
}
export const useToastEffect = () => {
const toastData = reactive({
show: false,
toastMessage: ''
})
const showToast = (message) => {
toastData.show = true
toastData.toastMessage = message
setTimeout(() => {
toastData.show = false
toastData.toastMessage = ''
}, 2000)
}
const { show, toastMessage } = toRefs(toastData)
return { show, toastMessage, showToast }
}
</script>
<style lang="scss" scoped>
@import '../style/viriables.scss';
.toast {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: .1rem;
background: rgba(0, 0, 0, .35);
border-radius: .05rem;
color: $bgColor;
}
</style>
当html标签新增@事件(如@click=“changeCart(shopId, item.id, item, -1)”
)时,响应参数(shopId,cartList)要在setup里return出来
11.7/11.8 心得:
<template>
<div class="cart">
<div class="product">
<div class="product__header">
</div>
<template
v-for="item in productList"
:key="item._id"
>
<div class="product__item" v-if="item.count > 0">
<div
class="product__item__checked iconfont"
v-html="item.check ? '': ''"
@click="() => changeCartItemChecked(shopId, item._id)"
/>
<img class="product__item__img" :src="item.imgUrl" />
<div class="product__item__detail">
<h4 class="product__item__title">{{item.name}}</h4>
<p class="product__item__price">
<span class="product__item__yen">¥</span>{{item.price}}
<span class="product__item__origin">¥{{item.oldPrice}}</span>
</p>
</div>
<div class="product__number">
<span
class="product__number__minus"
@click="() => { changeCartItemInfo(shopId, item._id, item, -1) }"
>-</span>
{{item.count || 0}}
<span
class="product__number__plus"
@click="() => { changeCartItemInfo(shopId, item._id, item, 1) }"
>+</span>
</div>
</div>
</template>
</div>
<div class="check">
<div class="check__icon">
<img
src="http://www.dell-lee.com/imgs/vue3/basket.png"
class="check__icon__img"
/>
<div class="check__icon__tag">{{total}}</div>
</div>
<div class="check__info">
总计:<span class="check__info__price">¥ {{price}}</span>
</div>
<div class="check__btn">去结算</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
import { useRoute } from 'vue-router'
import { useCommonCartEffect } from './commonCartEffect'
// 获取购物车信息逻辑
const useCartEffect = (shopId) => {
const { changeCartItemInfo } = useCommonCartEffect()
const store = useStore()
const cartList = store.state.cartList
const total = computed(() => {
const productList = cartList[shopId]
let count = 0
if(productList) {
for(let i in productList) {
const product = productList[i]
count += product.count
}
}
return count
})
const price = computed(() => {
const productList = cartList[shopId]
let count = 0
if(productList) {
for(let i in productList) {
const product = productList[i]
if(product.check) {
count += (product.count * product.price)
}
}
}
return count.toFixed(2)
})
const productList = computed(() => {
const productList = cartList[shopId] || []
return productList
})
const changeCartItemChecked = (shopId, productId) => {
store.commit('changeCartItemChecked', {shopId, productId})
}
return { total, price, productList, changeCartItemInfo, changeCartItemChecked}
}
export default {
name: 'Cart',
setup() {
const route = useRoute();
const shopId = route.params.id;
const { total, price, productList, changeCartItemInfo, changeCartItemChecked } = useCartEffect(shopId)
return { total, price, shopId, productList, changeCartItemInfo, changeCartItemChecked }
}
}
</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
@import '../../style/mixins.scss';
.cart {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
.product {
overflow-y: scroll;
flex: 1;
background: #FFF;
&__header {
height: .52rem;
border-bottom: 1px solid #F1F1F1;
}
&__item {
position: relative;
display: flex;
padding: .12rem 0;
margin: 0 .16rem;
border-bottom: .01rem solid $content-bgColor;
&__checked {
line-height: .5rem;
margin-right: .2rem;
color: #0091FF;
font-size: .2rem;
}
&__detail {
overflow: hidden;
}
&__img {
width: .46rem;
height: .46rem;
margin-right: .16rem;
}
&__title {
margin: 0;
line-height: .2rem;
font-size: .14rem;
color: $content-fontcolor;
@include ellipsis;
}
&__price {
margin: .06rem 0 0 0;
line-height: .2rem;
font-size: .14rem;
color: $hightlight-fontColor;
}
&__yen {
font-size: .12rem;
}
&__origin {
margin-left: .06rem;
line-height: .2rem;
font-size: .12rem;
color: $light-fontColor;
text-decoration: line-through;
}
.product__number {
position: absolute;
right: 0;
bottom: .12rem;
&__minus, &__plus
{
display: inline-block;
width: .2rem;
height: .2rem;
line-height: .16rem;;
border-radius: 50%;
font-size: .2rem;
text-align: center;
}
&__minus {
border: .01rem solid $medium-fontColor;
color: $medium-fontColor;
margin-right: .05rem;
}
&__plus {
background: $btn-bgColor;
color: $bgColor;
margin-left: .05rem;
}
}
}
}
.check {
display: flex;
height: .49rem;
border-top: .01rem solid $content-bgColor;
line-height: .49rem;
&__icon {
position: relative;
width: .84rem;
&__img {
display: block;
margin: .12rem auto;
width: .28rem;
height: .26rem;
}
&__tag {
position: absolute;
left: .46rem;
top: .04rem;
padding: 0 .04rem;
min-width: .2rem;
height: .2rem;
line-height: .2rem;
background-color: $hightlight-fontColor;
border-radius: .1rem;
font-size: .12rem;
text-align: center;
color: #fff;
transform: scale(.5);
transform-origin: left center;
}
}
&__info {
flex: 1;
color: $content-fontcolor;
font-size: .12rem;
&__price {
line-height: .49rem;
color: $hightlight-fontColor;
font-size: .18rem;
}
}
&__btn {
width: .98rem;
background-color: #4FB0F9;
text-align: center;
color: #FFF;
font-size: .14rem;
}
}
</style>