Vue模仿饿了么实现商户综合推荐

本文介绍了一个基于Vue的前端综合筛选组件的实现细节,包括HTML结构、CSS样式、JavaScript交互逻辑以及模拟数据处理。该组件提供了商家列表的综合筛选、条件排序和滚动加载功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

由于自己的项目一直是现在普通h5项目下造好轮子,然后再挪到脚手架进去,再此分享一下
功能(综合筛选,条件排序,滚动加载)
效果图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

先上代码:
html部分:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <title>商家列表过滤</title>
    <link rel="stylesheet" href="./css/font/iconfont.css">
    <link rel="stylesheet" href="./css/style.css">
    <script type="text/javascript" src="./css/font/iconfont.js"></script>
    <script type="text/javascript" src="./js/common/mock.js"></script>
    <script type="text/javascript" src="./js/common/axios.js"></script>
    <script type="text/javascript" src="./js/index.js"></script>
    <script type="text/javascript" src="./js/common/vue.js"></script>
    <script type="text/javascript" src="./js/filter.js"></script>
</head>

<body>
    <div id="filter" v-cloak>
        <div class="filter-container">
            <div class="filter-nav">
                <div class="nav-left nav-left_active" @click="toggleItem(0)">
                    <span>{{curSelectedDisName}}</span>
                    <i class="iconfont iconsanjiao" :class="selectedLeft ? 'up' : 'down'"></i>
                </div>
                <div class="nav-middle">
                    <span class="middle-left" :class="{'selected': curSelectedDis === true}"
                        @click="selectedDiscount(true)">距离最近</span>
                    <span class="middle-right" :class="{'selected': curSelectedDis === false}"
                        @click="selectedDiscount(false)">销量最高</span>
                </div>
                <div class="nav-right nav-right_active" @click="toggleItem(1)">
                    <span>筛选</span>
                    <i class="iconfont iconshaixuan"></i>
                </div>
            </div>
            <div class="filter-shadow" v-show="selectedLeft || selectedRight" @click="closeAll()" ref="filterShadow">
            </div>
            <div class="filter-detail">
                <div class="filter-sort" v-if="selectedLeft">
                    <ul>
                        <li v-for="(dis, d) in disItems" :key="d" :class="{'li-active': curSelectedDis === dis.flagId}"
                            @click="selectedDiscount(dis)">
                            <span>{{dis.name}}</span>
                            <i class="iconfont icondagou"></i>
                        </li>
                    </ul>
                </div>
                <div class="filter-filtrate" v-if="selectedRight">
                    <ul>
                        <li>
                            <div class="discount-title">{{deliveryType.name}}</div>
                            <div class="discount-item">
                                <!-- class="dis-selected" -->
                                <span :class="{'dis-selected': selectedItems.curType === true}"
                                    @click="selectedItems.curType = !selectedItems.curType">
                                    <i class="iconfont icondagou" v-if="selectedItems.curType"></i>
                                    {{deliveryType.disDetail.detailName}}
                                </span>
                            </div>
                        </li>
                        <li>
                            <div class="discount-title">{{averageConsume.name}}</div>
                            <div class="discount-item">
                                <span v-for="(avg, a) in averageConsume.disDetail.detailItems" :key="a"
                                    :class="{'dis-selected': a===selectedItems.curAvg}" @click="selectAvgConsume(a)">
                                    <i class="iconfont icondagou" v-if="a===selectedItems.curAvg"></i>
                                    {{avg.name | priceFilter}}
                                </span>
                            </div>
                        </li>
                        <li>
                            <div class="discount-title">{{specialOffer.name}}</div>
                            <div class="discount-item">
                                <span v-for="(spec, s) in specialOffer.disDetail.detailItems" :key="s"
                                    :class="{'dis-selected': s===selectedItems.curSpec}" @click="selectSpeOffer(s)">
                                    <i class="iconfont icondagou" v-if="s===selectedItems.curSpec"></i>
                                    <a v-else>{{spec.name | specFilter}}</a>
                                    {{spec.name}}
                                </span>
                            </div>
                        </li>
                    </ul>
                    <div class="filter-footer">
                        <div class="footer-top"></div>
                        <div class="footer-bottom">
                            <span class="footer-left" @click="clearAllSelected">清空</span>
                            <span class="footer-right">查看商家</span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div class="shop-list" v-if="shopList.shops.length > 0">
            <div v-for="(shop, s) in shopList.shops" :key="s">
                <div class="shop-item">
                    <div class="shop-logo">
                        <img :src="shop.shopLogo">
                    </div>
                    <div class="shop-content">
                        <div class="shop-content-header">
                            <span>{{shop.shopName}}</span>
                        </div>
                        <div class="shop-content-body">
                            <div class="item">
                                <span class="shop-score">
                                    <span class="star-item" v-for="(star, store) in shop.shopScore.stars" :key="store">
                                        <img :src="star" />
                                    </span>
                                    <span class="score-item">{{shop.shopScore.score}}</span>
                                </span>
                                <span class="sell-count">月售{{shop.sellCount}}</span>
                                <div class="goods-send" v-if="shop.shopSend">蜂鸟专送</div>
                            </div>
                            <div class="item">
                                <span class="start-send">{{shop.shopStartSend | priceFilter}}起送</span>
                                <span class="swiper-line">|</span>
                                <span class="new-delivery">配送{{shop.newDelivery | priceFilter}}</span>
                                <div class="deliver-time">
                                    <span>{{shop.deliverTime}}分钟</span>
                                    <span>|</span>
                                    <span>{{shop.shopDistant}}km</span>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="shop-footer-item">
                    <div class="dicount-left">
                        <div class="shop-discount" v-if="!shop.shopDiscount[0].showAll">
                            <a>{{shop.shopDiscount[0].taggedWord}}</a>
                            <span>{{shop.shopDiscount[0].disCounts}}</span>
                        </div>
                        <div class="shop-discount" v-for="(dis, d2) in shop.shopDiscount" :key="d2" v-else>
                            <a>{{dis.taggedWord}}</a>
                            <span>{{dis.disCounts}}</span>
                        </div>
                    </div>
                    <div class="arrow" @click="toggleActive(s, shop.shopDiscount[0].showAll)">
                        <span>{{shop.shopDiscount.length}}活动</span>
                        <i class="iconfont iconsanjiao" :class="shop.shopDiscount[0].showAll ? 'up' : 'down'"></i>
                    </div>
                </div>
                <div class="shop-footer-line"></div>
            </div>
        </div>
    </div>
</body>
</html>

mcok部分:

const Random = Mock.Random
let shops = []
for (let i = 0; i < 100; i++) {
    const shop = {
        shopLogo: Random.image('480x480', '#00A7FF', '#ffffff', 'CateShop'),
        shopName: Random.title(1),
        shopScore: {
            score: Random.float(3.0, 4.9, 0, 1)
        },
        sellCount: Random.integer(400, 1000),
        shopStartSend: Random.integer(10, 20),
        newDelivery: Random.float(1, 2, 0, 1),
        deliverTime: Random.integer(20, 40),
        shopDistant: Random.float(1, 5, 0, 1),
        wellComent: Random.integer(0, 5),
        shopDiscount: Random.pick([
            [
                { disCounts: '26减5, 38减7, 46减8, 58减10, 66减12', taggedWord: '减', showAll: false},
                { disCounts: '6元会员红包', taggedWord: '返' },
                { disCounts: '首次光顾减10', taggedWord: '首' }
            ],
            [
                { disCounts: '15减14, 30减25, 50减35, 80减45', taggedWord: '减', showAll: false },
                { disCounts: '6元会员红包', taggedWord: '返' },
                { disCounts: '1.68元特价', taggedWord: '特' }
            ],
            [
                { disCounts: '20减2, 30减5, 40减8, 50减10', taggedWord: '减', showAll: false },
                { disCounts: '6元会员红包', taggedWord: '返' },
                { disCounts: '首次光顾减10', taggedWord: '首' }
            ],
            [
                { disCounts: '25减8, 35减11, 45减14, 55减17', taggedWord: '减', showAll: false },
                { disCounts: '6元会员红包', taggedWord: '返' },
                { disCounts: '品质联盟', taggedWord: '品' },
                { disCounts: '0.3元特价', taggedWord: '特' }
            ],
            [
                { disCounts: '10减1, 15减3, 20减5, 25减8', taggedWord: '减', showAll: false },
                { disCounts: '品质联盟', taggedWord: '品' },
                { disCounts: '0.3元特价', taggedWord: '特' }
            ]
        ]),
        shopSend: Random.boolean(),
        avgConsume: Random.integer(0, 3),
        specOffer: Random.integer(0, 5)
    }
    shops.push(shop)
}

compositeFilter(['wellComent', 'shopStartSend', 'deliverTime', 'newDelivery', 'shopDistant', 'sellCount'], shops)

// 综合排序
function compositeFilter(conditions, curFilterShops) {
    for(let i = 0; i < conditions.length; i++) {
        if (i == 0 || i == conditions.length - 1) {
            signalDownFilter(conditions[i], curFilterShops)
        } else {
            signalUpFilter(conditions[i], curFilterShops)
        }
    }
}

// 单项升排序升序
function signalUpFilter(flag, curFilterShops) {
    // 此处使用冒泡排序
    // console.log('升序', flag)
    let len = curFilterShops.length
    for (let i = 0; i < len - 1; i++) {
        for (let j = 0; j < len - i - 1; j++) {
            if (curFilterShops[j][flag] > curFilterShops[j + 1][flag]) {
                let forward = curFilterShops[j]
                curFilterShops[j] = curFilterShops[j + 1]
                curFilterShops[j + 1] = forward
            }
        }
        // console.log(curFilterShops[i][flag])
    }
    return curFilterShops
}

// 单项排序降序
function signalDownFilter(flag, curFilterShops) {
    // console.log('降序', flag)
    let len = curFilterShops.length
    for (let i = 0; i < len - 1; i++) {
        for (let j = 0; j < len - i - 1; j++) {
            if (curFilterShops[j][flag] < curFilterShops[j + 1][flag]) {
                let forward = curFilterShops[j]
                curFilterShops[j] = curFilterShops[j + 1]
                curFilterShops[j + 1] = forward
            }
        }
        // console.log(curFilterShops[i][flag])
    }
    return curFilterShops
}

// 筛选数量/商家
function filterNum (flag, isFilterShop) {
    let curShops = []
    if (flag.curType) {
        for (let i = 0; i < shops.length; i++) {
            if (shops[i].shopSend) curShops.push(shops[i])
        }
    }else {
        curShops = shops
    }
    if (flag.curAvg !== null) {
        let avgShops = []
        for (let i = 0; i < curShops.length; i++) {
            if (curShops[i].avgConsume == flag.curAvg) avgShops.push(curShops[i])
        }
        curShops = avgShops
    }
    if (flag.curSpec !== null) {
        let specShops = []
        for (let i = 0; i < curShops.length; i++) {
            if (curShops[i].specOffer == flag.curSpec) specShops.push(curShops[i])
        }
        curShops = specShops
    }
    return isFilterShop ? curShops : curShops.length
}

// 排序选项
Mock.mock('/api/getDisItems', {
    'items|5': [
        {
            'flagId|+1': 1,
            'name|+1': ['综合排序', '好评优选', '起送价最低', '配送最快', '配送费最低']
        }
    ]
})

// 筛选选项
Mock.mock('/api/getFilters', {
    'items|3': [
        {
            'name|+1': ['配送方式', '人均消费', '优惠活动'],
            'disDetail|+1': [
                {
                    'detailName': '蜂鸟专送'
                },
                {
                    'detailItems': [
                        { 
                            'name': '20以下',
                            'selected': false
                        },
                        {
                            'name': '20-40',
                            'selected': false
                        }, 
                        {
                            'name': '40以上',
                            'selected': false
                        }
                    ]
                },
                {
                    // '首单立减', '下单返红包', '进店领红包', '特价美食', '品质联盟', '首次关顾立减'
                    'detailItems': [
                        {
                            'name': '首单立减',
                            'selected': false
                        },
                        {
                            'name': '下单返红包',
                            'selected': false
                        },
                        {
                            'name': '进店领红包',
                            'selected': false
                        },
                        {
                            'name': '特价美食',
                            'selected': false
                        },
                        {
                            'name': '品质联盟',
                            'selected': false
                        },
                        {
                            'name': '首次关顾立减',
                            'selected': false
                        },
                    ]
                }
            ]
        }
    ]
})

// 商家列表
Mock.mock('/api/shopList', 'post', (params) => {
    const info = JSON.parse(params.body)
    const [index, size] = [info.itemIndex, info.itemSize]
    let curItems = shops.slice(index * size, (index + 1) * size)
    return {
        'code': 0,
        'message': 'success',
        'shops': curItems,
        'shopsNum': shops.length
    }
})

// 综合筛选
Mock.mock('/api/filterShopList', 'post', (params) => {
    const info = JSON.parse(params.body)
    // console.log(typeof info.filterFlag)
    let curFilterShops = shops
    switch(info.filterFlag) {
        case 1:
            curFilterShops = shops
            break
        case 2:
            signalDownFilter('wellComent', curFilterShops)
            break
        case 3:
            signalUpFilter('shopStartSend', curFilterShops)
            break
        case 4:
            signalUpFilter('deliverTime', curFilterShops)
            break
        case 5:
            signalUpFilter('newDelivery', curFilterShops)
            break
        case true:
            signalUpFilter('shopDistant', curFilterShops)
            break
        case false:
            signalDownFilter('sellCount', curFilterShops)
            break
        default:
            break
    }
    return {
        'code': '0',
        'message': 'success',
        'filterShops': curFilterShops
    }
})

// 筛选数量/商家
Mock.mock('/api/filterNum', 'post', (params) => {
    const info = JSON.parse(params.body)
    // console.log(info.filterFlag, info.isFilterShop)
    if(info.isFilterShop) {
        return {
            'code': '0',
            'message': 'success',
            'filterShops': filterNum(info.filterFlag, info.isFilterShop),
        }
    }else {
        return {
            'code': '0',
            'message': 'success',
            'filterNum': filterNum(info.filterFlag, info.isFilterShop),
        }
    }
})

Vue部分:

window.onload = () => {
    new Vue({
        el: '#filter',
        data: {
            disItems: null,
            deliveryType: null,
            averageConsume: null,
            specialOffer: null,
            shopList: {
                shops: [],
                shopsNum: 0,
                curShopsNum: 0,
                itemIndex: 0,
                itemSize: 10,
                filterShops: [],
                curFilterNum: 0
            },
            showAllAcitvity: false,
            curSelectedDisName: '综合排序',
            curSelectedDis: 1,
            selectedItems: {
                curType: false,
                curAvg: null,
                curSpec: null
            },
            selectedLeft: false,
            selectedRight: false,
            filterClientH: 0,
            topNavH: 0,
            scrollFlag: false,
            reachBottom: false,
            timer: null,
            loadingShow: false,
            loadingImg: './img/loading.gif',
            starImg: {
                full: './img/full.png',
                half: './img/half.png',
                empty: './img/empty.png'
            }
        },

        mounted () {
            this.getDisItems()
            this.getFilters()
            this.getShopList()
            this.$nextTick(() => {
                console.log(document.documentElement.clientHeight, this.$refs.topNav.offsetHeight)
                this.filterClientH = document.body.clientHeight || document.documentElement.clientHeight
            })
            window.addEventListener('scroll', this.debounce)
        },

        computed: {
            finallShops () {
                return this.shopList.filterShops.length > 0 ? this.shopList.filterShops : this.shopList.shops
            }
        },

        watch: {
            filterClientH (newVal) {
                if(newVal) {
                    this.$refs.filterShadow.style.height = `${this.filterClientH}px`
                    this.$refs.shopLoad.style.height = `${this.filterClientH}px`
                }
            },

            reachBottom (newVal) {
                if(this.shopList.shops.length === this.shopList.shopsNum) return
                if(newVal) {
                    clearTimeout(this.timer)
                    window.removeEventListener('scroll', this.debounce)
                    this.scrollFlag = false
                    this.timer = setTimeout(() => {
                        this.getShopList()
                    }, 1000)
                }
            }
        },

        filters: {
            priceFilter (data) {
                return `¥${data}`
            },
            
            specFilter (data) {
                return data.slice(0, 1)
            }
        },

        methods: {
            getDisItems () {
                axios({
                    methods: 'get',
                    url: '/api/getDisItems'
                }).then(res => {
                    // console.log(res.data.items)
                    this.disItems = res.data.items
                }).catch(err => {
                    console.log(err)
                })
            },

            getFilters () {
                axios({
                    methods: 'get',
                    url: '/api/getFilters'
                }).then(res => {
                    // console.log(res.data.items)
                    this.deliveryType = res.data.items[0]
                    this.averageConsume = res.data.items[1]
                    this.specialOffer = res.data.items[2]
                }).catch(err => {
                    console.log(err)
                })
            },

            // 分段加载商家
            getShopList () {
                setTimeout(() => {
                    axios.post('/api/shopList', {
                        itemIndex: this.shopList.itemIndex,
                        itemSize: this.shopList.itemSize
                    }).then(res => {
                        // console.log(res.data)
                        this.insertData(this.shopList.shops, res.data.shops)
                        if (this.shopList.shopsNum <= 0) this.shopList.shopsNum = res.data.shopsNum
                        // this.shopList.curShopsNum = this.shopList.shops.length
                        this.shopList.itemIndex++
                    }).catch(err => {
                        console.log(err)
                    })
                    window.addEventListener('scroll', this.debounce)
                }, 500)
            },

            // 综合筛选商家
            getFilterShopList (flag) {
                this.closeShadow()
                this.loadingShow = !this.loadingShow
                setTimeout(() => {
                    axios.post('/api/filterShopList', {
                        filterFlag: flag
                    }).then(res => {
                        // console.log(res.data.filterShops)
                        this.shopList.filterShops = []
                        this.shopList.shops = res.data.filterShops
                        this.loadingShow = !this.loadingShow
                    }).catch(err => {
                        console.log(err)
                    })
                }, 500)
            },

            // 获取过滤商家/数量
            getFilter (flag, isFilterShop) {
                axios.post('/api/filterNum', {
                    filterFlag: flag,
                    isFilterShop: isFilterShop ? isFilterShop : false
                }).then(res => {
                    // console.log(res.data)
                    if (res.data.filterShops) {
                        this.shopList.filterShops = []
                        this.insertData(this.shopList.filterShops, res.data.filterShops)
                        // this.shopList.curShopsNum = res.data.filterShops.length
                    }else {
                        this.shopList.curFilterNum = res.data.filterNum
                    }
                }).catch(err => {
                    console.log(err)
                })
            },

            // 插入数据
            insertData (container, data) {
                for (let i = 0; i < data.length; i++) {
                    this.commentStars(data[i])
                    container.push(data[i])
                }
            },

            // 星级评分
            commentStars (shop) {
                let stars = []
                let starsNum = 0
                let store = Math.floor(shop.shopScore.score * 2)/2 // 取整例如4.8*2取整=9/2=4.5
                let decimalStore = store%1 !==0 //取余数,判断是否存在半星
                let intStore = Math.floor(store)
                for(let i = 0; i < intStore; i++) {
                    stars.push(this.starImg.full)
                }
                if(decimalStore) {
                    stars.push(this.starImg.half)
                }
                starsNum = 5 - stars.length
                for(let j = 0; j < starsNum; j++) {
                    stars.push(this.starImg.empty)
                }
                shop.shopScore.stars = stars
             },

            // 筛选模式切换
            toggleItem (tag) {
                if (document.getElementsByTagName("body")[0].className === '') {
                    document.getElementsByTagName("body")[0].className = 'bodyFixed'
                }
                if(tag === 0) {
                    if (this.selectedRight) this.selectedRight = !this.selectedRight
                    this.selectedLeft = !this.selectedLeft
                }else {
                    if(this.selectedLeft) this.selectedLeft = !this.selectedLeft
                    this.selectedRight = !this.selectedRight
                }
            },

            closeShadow () {
                if(this.selectedLeft && this.selectedRight) return
                this.selectedLeft = false
                this.selectedRight = false
                document.body.removeAttribute("class", "bodyFixed")
            },

            // 综合筛选
            selectedDiscount (flag) {
                if (typeof flag === 'boolean') {
                    flag ? this.curSelectedDis = true : this.curSelectedDis = false
                    this.closeShadow()
                    this.getFilterShopList(flag)
                }else {
                    this.curSelectedDisName = flag.name
                    this.curSelectedDis = flag.flagId
                    this.getFilterShopList(flag.flagId)
                }
            },

            // 蜂鸟专送
            selectDelType () {
                this.selectedItems.curType = !this.selectedItems.curType
                this.getFilter(this.selectedItems)
            },

            // 平均消费
            selectAvgConsume (a) {
                this.selectedItems.curAvg = a
                this.getFilter(this.selectedItems)
            },

            // 优惠活动
            selectSpeOffer (s) {
                this.selectedItems.curSpec = s
                this.getFilter(this.selectedItems)
            },

            // 筛选商家
            checkShops () {
                if(this.shopList.curFilterNum === 0) return
                this.closeShadow()
                this.loadingShow = !this.loadingShow
                setTimeout(() => {
                    this.getFilter(this.selectedItems, true)
                    this.loadingShow = !this.loadingShow
                }, 1000)
            },

            // 清空筛选条件
            clearAllSelected () {
                if (this.shopList.curShopsNum < this.shopList.shopsNum) this.checkShops()
                this.selectedItems.curType = false
                this.selectedItems.curAvg = null
                this.selectedItems.curSpec = null
                this.shopList.curFilterNum = 0 
            },

            // 切换活动
            toggleActive (index, showAll) {
                this.finallShops[index].shopDiscount[0].showAll = !showAll
            },

            getScrollHeight() {
                const windowHeight = window.scrollY + window.innerHeight - this.$refs.topNav.offsetHeight
                console.log(windowHeight, document.documentElement.offsetHeight)
                return windowHeight >= document.documentElement.offsetHeight
            },

            debounce() {
                this.reachBottom = this.getScrollHeight()
            }        
        }    
    }) 
}

以上都是用纯前端mock模拟数据的方式,实现的,不过发现就算造好的轮子,到了脚手架项目,前后台联调的时候,除了模板和基本的状态数据不变,其它的,例如组件之间的通信等等,都要重新写。完整版以上传资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值