课程名称: 2022持续升级 Vue3 从入门到实战 掌握完整知识体系
课程章节: 【2022加餐】商品搜索 & 购物车功能实现
主讲老师: Dell
课程内容:
今天学习的内容包括:
如何进行购物车的优化及mixin的复用
课程收获:
src/views/search/Search.vue
<template>
<div class="wrapper">
<div class="search">
<span class="iconfont"></span>
<input
class="search__area"
@change="handleSearchChange"
placeholder="山姆会员商店优惠商品"
/>
<div class="search__cancel" @click="handleCancelSearchClick">取消</div>
</div>
<div class="area" v-if="history.length">
<h4 class="area__title">
搜索历史
<span
class="area__title__clear"
@click="handleClearHistoryClick"
>清除搜索历史</span>
</h4>
<ul class="area__list">
<li
class="area__list__item"
v-for="item in history"
:key="item"
@click="() => goToSearchList(item)"
>{{item}}</li>
</ul>
</div>
<div class="area">
<h4 class="area__title">热门搜索</h4>
<ul class="area__list">
<li
class="area__list__item"
v-for="item in hotWordList"
:key="item"
@click="() => goToSearchList(item)"
>{{item}}</li>
</ul>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { get } from '../../utils/request';
// 热词相关逻辑
const useHotWordListEffect = () => {
const hotWordList = ref([]);
const getHotWorList = async () => {
const result = await get('/api/shop/search/hot-words')
if (result?.errno === 0 && result?.data?.length) {
hotWordList.value = result.data
}
}
return { hotWordList, getHotWorList };
}
export default {
name: 'Search',
setup() {
const router = useRouter();
const history = ref(JSON.parse(localStorage.history || '[]'));
// 当用户输入搜索内容后,执行的操作
const handleSearchChange = (e) => {
const searchValue = e.target.value;
if(!searchValue) return;
const hasValue = history.value.find(item => item === searchValue);
if(!hasValue) {
history.value.push(searchValue);
localStorage.history = JSON.stringify(history.value);
}
router.push(`/searchList?keyword=${searchValue}`);
}
// 当清理历史记录时,执行的操作
const handleClearHistoryClick = () => {
history.value = [];
localStorage.history = JSON.stringify([]);
}
// 当取消搜索时,执行的操作
const handleCancelSearchClick = () => {
router.back();
}
// 页面跳转逻辑
const goToSearchList = (keyword) => {
router.push(`/searchList?keyword=${keyword}`);
}
// 使用热词逻辑
const { hotWordList, getHotWorList } = useHotWordListEffect();
getHotWorList();
return {
history,
hotWordList,
goToSearchList,
handleSearchChange,
handleClearHistoryClick,
handleCancelSearchClick
};
}
}
</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
.wrapper {
margin: 0 .18rem;
.search {
position: relative;
display: flex;
line-height: .32rem;
margin-top: .16rem;
color: $content-fontcolor;
.iconfont {
position: absolute;
left: .16rem;
color: $search-fontColor;
}
&__area {
flex: 1;
padding: 0 .12rem 0 .44rem;
background: $search-bgColor;
border-radius: .16rem;
border: none;
outline: none;
font-size: .14rem;
}
&__cancel {
margin-left: .12rem;
font-size: .16rem;
}
}
.area {
margin-top: .24rem;
&__title {
line-height: .22rem;
margin: 0;
font-size: .16rem;
font-weight: normal;
color: $content-fontcolor;
&__clear {
float: right;
font-size: .14rem;
}
}
&__list {
margin: 0 0 0 -.1rem;
padding: 0;
list-style-type: none;
&__item {
line-height: .32rem;
margin-left: .1rem;
margin-top: .12rem;
padding: 0 .1rem;
font-size: .14rem;
background: $search-bgColor;
display: inline-block;
border-radius: .02rem;
color: $medium-fontColor;
}
}
}
}
</style>
src/views/home/Nearby.vue
<template>
<div class="nearby">
<h3 class="nearby__title">附近店铺</h3>
<router-link
v-for="item in nearbyList"
:key="item._id"
:to="`/shop/${item._id}`"
>
<ShopInfo :item="item" />
</router-link>
</div>
</template>
<script>
import { ref } from 'vue';
import { get } from '../../utils/request';
import ShopInfo from '../../components/ShopInfo';
const useNearbyListEffect = () => {
const nearbyList = ref([]);
const getNearbyList = async () => {
const result = await get('/api/shop/hot-list')
if (result?.errno === 0 && result?.data?.length) {
nearbyList.value = result.data
}
}
return { nearbyList, getNearbyList}
}
export default {
name: 'Nearby',
components: { ShopInfo },
setup() {
const { nearbyList, getNearbyList } = useNearbyListEffect();
getNearbyList();
return { nearbyList };
}
}
</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
.nearby {
&__title {
margin: .16rem 0 .02rem 0;
font-size: .18rem;
font-weight: normal;
color: $content-fontcolor;
}
a {
text-decoration: none;
}
}
</style>
src/views/searchList/searchList.vue
<template>
<div class="wrapper">
<div class="search">
<div class="search__back iconfont" @click="handleBackClick">

</div>
<div class="search__content">
<span class="search__content__icon iconfont"></span>
<input
class="search__content__input"
placeholder="请输入商品名称"
v-model="keyword"
@change="handleSearchInputChange"
/>
</div>
</div>
<router-link
v-for="item in searchList"
:key="item._id"
:to="`/shop/${item._id}`"
>
<ShopInfo :item="item" />
</router-link>
</div>
</template>
<script>
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { get } from '../../utils/request';
import ShopInfo from '../../components/ShopInfo';
// 点击回退逻辑
const useBackRouterEffect = () => {
const router = useRouter()
const handleBackClick = () => {
router.back()
}
return handleBackClick
}
// 热词相关逻辑
const useSearchListEffect = () => {
const searchList = ref([]);
const getSearchList = async (keyword) => {
const result = await get('/api/shop/search', { keyword });
if (result?.errno === 0 && result?.data?.length) {
searchList.value = result.data
}
}
return { searchList, getSearchList };
}
export default {
name: 'SearchList',
components: { ShopInfo },
setup() {
const route = useRoute();
// 搜索关键词逻辑
const keyword = ref(route.query.keyword || '');
const handleSearchInputChange = () => {
getSearchList(keyword.value)
}
const handleBackClick = useBackRouterEffect();
// 获取搜索列表
const { searchList, getSearchList } = useSearchListEffect();
getSearchList(keyword.value);
return {
keyword,
searchList,
handleBackClick,
handleSearchInputChange
}
}
}
</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
.wrapper {
padding: 0 .18rem;
a {
text-decoration: none;
}
}
.search {
display: flex;
margin: .14rem 0 .04rem 0;
line-height: .32rem;
&__back {
width: .3rem;
font-size: .24rem;
color: #B6B6B6;
}
&__content {
display: flex;
flex: 1;
background: $search-bgColor;
border-radius: .16rem;
&__icon {
width: .44rem;
text-align: center;
color: $search-fontColor;
}
&__input {
display: block;
width: 100%;
padding-right: .2rem;
border: none;
outline: none;
background: none;
height: .32rem;
font-size: .14rem;
color: $content-fontcolor;
&::placeholder {
color: $content-fontcolor;
}
}
}
}
</style>
src/views/cartList/CartList.vue
<template>
<div class="wrapper">
<div class="title">我的全部购物车</div>
<div
class="cart"
v-for="(cart, key) in list"
:key="key"
@click="() => handleCartClick(key)"
>
<div className="cart__title">{{cart.shopName}}</div>
<div class="cart__item" v-for="(product, innerKey) in cart.productList" :key="innerKey">
<img class="cart__image" :src="product.imgUrl" />
<div class="cart__content">
<p class="cart__content__title">{{product.name}}</p>
<p class="cart__content__price">
<span class="yen">¥</span>{{product.price}} X {{product.count}}
<span class="cart__content__total">
<span class="yen">¥</span>{{(product.price * product.count).toFixed(2)}}
</span>
</p>
</div>
</div>
<div class="cart__total">
共计 {{cart.total}} 件
</div>
</div>
<div
v-if="Object.keys(list).length === 0"
class="empty"
>暂无购物数据</div>
</div>
<Docker :currentIndex="1"/>
</template>
<script>
import Docker from '../../components/Docker';
import { useRouter } from 'vue-router';
export default {
name: 'CartList',
components: { Docker },
setup() {
const list = JSON.parse(localStorage.cartList || '[]');
// 计算购物车总件数的逻辑
for(let i in list) {
const cart = list[i];
const productList = cart.productList;
let total = 0;
for(let j in productList) {
const product = productList[j];
total += product['count'];
}
cart.total = total;
}
// 处理点击
const router = useRouter();
const handleCartClick = (key) => {
router.push(`/orderConfirmation/${key}`);
}
return { list, handleCartClick }
}
}
</script>
<style lang="scss" scoped>
@import '../../style/viriables.scss';
@import '../../style/mixins.scss';
.wrapper {
overflow-y: auto;
@include fix-content;
background: $darkBgColor;
}
.title {
@include title;
}
.cart {
margin: .16rem;
padding-bottom: .16rem;
background: $bgColor;
&__title {
padding: .16rem;
line-height: .22rem;
font-size: .16rem;
color: $content-fontcolor;
@include ellipsis;
}
&__item {
display: flex;
padding: 0 .16rem .16rem .16rem;
}
&__image {
margin-right: .16rem;
width: .46rem;
height:.46rem;
}
&__content {
flex: 1;
.yen {
font-size: .12rem;
}
&__title {
margin: 0;
line-height: .2rem;
font-size: .14rem;
color: $content-fontcolor;
@include ellipsis;
}
&__price {
margin: 0;
font-size: .14rem;
color: $hightlight-fontColor;
}
&__total {
float: right;
color: $dark-fontColor;
}
}
&__total {
line-height: .28rem;
margin: 0 .16rem;
color: $light-fontColor;
font-size: .14rem;
text-align: center;
background: $search-bgColor;
}
}
.empty {
margin-top: .5rem;
line-height: .5rem;
text-align: center;
font-size: .16rem;
color: $light-fontColor;
}
</style>
src/components/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 v-if="item.products" class="shop__products">
<div v-for="product in item.products" :key="product.name" class="shop__product">
<img :src="product.imgUrl" class="shop__product__img" />
<p class="shop__product__title">{{product.name}}</p>
<p class="shop__product__price">
<span class="yen">¥</span><span class="price">{{product.price}}</span>
<span class="origin">¥{{product.price}}</span>
</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ShopInfo',
props: ['item', 'hideBorder']
}
</script>
<style lang="scss" scoped>
@import '../style/viriables.scss';
@import '../style/mixins.scss';
.shop {
display: flex;
padding-top: .12rem;
&__img {
margin-right: .16rem;
width: .56rem;
height: .56rem;
}
&__content {
flex: 1;
padding-bottom: .12rem;
&--bordered {
border-bottom: .01rem 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;
}
}
&__products {
overflow: hidden;
margin: .08rem .07rem 0 -.18rem;
}
&__product {
width: 33.33%;
padding-left: .18rem;
box-sizing: border-box;
float: left;
&__img {
width: 100%;
}
&__title {
margin: .04rem 0 0 0;
line-height: .17rem;
font-size: .12rem;
color: $content-fontcolor;
@include ellipsis;
}
&__price {
line-height: .2rem;
margin: .02rem 0 0 0;
color: $light-fontColor;
font-size: .14rem;
@include ellipsis;
.yen {
font-size: .12rem;
color: $hightlight-fontColor;
}
.price {
color: $hightlight-fontColor;
}
.origin {
margin-left: .06rem;
font-size: .12rem;
text-decoration: line-through;
}
}
}
}
</style>
src/style/minixs.scss
@mixin ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
@mixin fix-content {
position: absolute;
left: 0;
top: 0;
bottom: .5rem;
right: 0;
}
@mixin title {
line-height: .44rem;
background: $bgColor;
font-size: .16rem;
color: $content-fontcolor;
text-align: center;
}