Taro+vue3 实现电影切换列表

1.需求

我们在做类似于猫眼电影的小程序或者H5 的时候 我们会做到那种 左右滑动的电影列表,这种列表一般带有电影场次

2.效果

3.说明

这种效果在淘票票 猫眼电影上 都有的 ,一般电影类型的H5 或者小程序 这个是都有的 第一是好看 第二是客观性比较好

4.代码、

1.整体页面
<template>
    <div class="movie-container-index">
        <Header></Header>
        <div class="swiper-main">
            <image class="background-img-vague" :src="chooseMovice.posterUrl" style="height: 100%;"></image>
            <div class="wrap">
                <MovieList :list="movieList" @onchangeMovie="MovieChange" :model-value="chooseMovice.movieId"></MovieList>
            </div>
            <div class="box"></div>

        </div>
        <div class="movie-detail">
            <div class="name">{{ chooseMovice.movieName }}</div>
            <div class="center-detail">
                <div>{{ chooseMovice.movieType }}</div>
                <div>{{ chooseMovice.duration }}</div>

            </div>
            <div class="cast">{{ chooseMovice.cast }}</div>

        </div>
        <div class="movie-time-container">
            <Filter v-if="timeList.length" :data="timeList" @onChanged="onTimeChanged"></Filter>
        </div>
        <div class="movie-arrgement-container" v-if="arrangementList.length && !loading">
            <Item v-for="(item, index) in arrangementList" :info="item" :key="index"></Item>
        </div>
        <template v-if="arrangementList.length === 0 && loading">
            <div style="padding:  0 15px;">
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
                <nut-skeleton :style="{ width: '100%', margin: '0.5rem 0' }" width="100%" height="15px" title animated
                    row="1">
                </nut-skeleton>
            </div>
        </template>
        <template v-if="arrangementList.length === 0 && !loading">
            <nut-empty description="暂无场次"></nut-empty>
        </template>


    </div>
</template>
<script setup>
import Header from './header.vue'
import MovieList from './movie-list.vue'
import Filter from './filter.vue'
import { ref, reactive, onMounted } from 'vue'
import Item from './item.vue'
const chooseMovice = ref({
    movieId: 12343,
    duration: 149,
    movieType: '剧情|历史|战争',
    cast: '吴京 易烊千玺 段奕宏 张涵予 朱亚文',
    posterUrl: "https://gw.alicdn.com/i1/O1CN01sSmj2b1daSm6IAUcs_!!6000000003752-0-alipicbeacon.jpg_480x480Q30s150.jpg",
    movieName: '长津湖之水门桥',


})
const timeList = ref(["2024-01-09", "2024-01-10", "2024-01-13"])
const arrangementList = ref([])
const loading = ref(true)
const movieList = ref([
    {
        movieId: 12343,
        duration: 149,
        movieType: '剧情|历史|战争',
        cast: '吴京 易烊千玺 段奕宏 张涵予 朱亚文',
        movieName: '长津湖之水门桥',
        posterUrl: "https://gw.alicdn.com/i1/O1CN01sSmj2b1daSm6IAUcs_!!6000000003752-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    },
    {
        cast: "易烊千玺 田雨 陈哈琳 齐溪 公磊 许君聪 王宁 黄尧 巩金国",
        duration: 106,
        movieId: 147885,
        movieName: "奇迹·笨小孩",
        movieType: "剧情",
        posterUrl: "https://gw.alicdn.com/i1/O1CN013Ggc2s1Z8HwrwxAfn_!!6000000003149-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    },
    {
        cast: "张译,李晨,魏晨,曹炳琨,王骁,张子贤,杨新鸣",
        duration: 106,
        movieId: 147886,
        movieName: "三大队",
        movieType: "剧情,犯罪",
        posterUrl: "https://gw.alicdn.com/i4/O1CN01zQvWon1SuCCkUXTr8_!!6000000002306-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    },
    {
        cast: "岑珈其,梁朝伟,刘德华,蔡卓妍,任达华,陈家乐,白只,姜皓文,方中信,太保,钱嘉乐,周家怡",
        duration: 106,
        movieId: 147887,
        movieName: "金手指",
        movieType: "犯罪,剧情",
        posterUrl: "https://gw.alicdn.com/i4/O1CN01mdCwok1K4QV7FV8gv_!!6000000001110-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    },
    {
        cast: "李栋,大鹏,白客,庄达菲,王迅,孙艺洲,李乃文",
        duration: 106,
        movieId: 147888,
        movieName: "年会不能停!",
        movieType: "喜剧,剧情",
        posterUrl: "https://gw.alicdn.com/i1/O1CN01v6g8341QMBaLa2FhI_!!6000000001961-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    },
    {
        cast: "屈楚萧,张佳宁,傅菁,蒋昀霖,牛超,田壮壮,沙溢",
        duration: 106,
        movieId: 147889,
        movieName: "一闪一闪亮星星",
        movieType: "爱情,奇幻",
        posterUrl: "https://gw.alicdn.com/i4/O1CN01W8mzt61aa3k1Xtw3N_!!6000000003345-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    },
    {
        cast: "江户川柯南,高山南,山崎和佳奈,小山力也,林原惠美,置鲇龙太郎,三石琴乃,土师孝也,乃村健次,飞田展男,绪方贤一,松井菜樱子,岩居由希子,大谷育江,茶风林",
        duration: 106,
        movieId: 147890,
        movieName: "名侦探柯南:黑铁的鱼影",
        movieType: "动画,悬疑,动作",
        posterUrl: "https://gw.alicdn.com/i1/O1CN01U9wWR81pfku0aqB6O_!!6000000005388-0-alipicbeacon.jpg_480x480Q30s150.jpg"
    }

])
const MovieChange = (e) => {
    console.log(e);
    chooseMovice.value = movieList.value.filter((item) => item.movieId == e)[0]

}
onMounted(() => {
    arrangementList.value = [
        {
            finishTime: "1704782520000",
            hallName: "7号激光杜比全景声厅",
            id: "1194219979173253121",
            pickUpPrice: 3380,
            price: 3500,
            showTime: "1704775500000"
        }
    ]
    loading.value = false
})
</script>
<style lang="scss">
.movie-container-index {
    display: flex;
    flex-direction: column;

    .movie-arregment-container {}


    .swiper-main {
        height: 324px;
        position: relative;
        overflow: hidden;
        width: 100%;

        .background-img-vague {
            position: absolute;
            left: 0;
            right: 0;
            width: 100%;
            height: 100%;
            filter: blur(15px);
            -webkit-filter: blur(15px);
        }
    }

    .movie-detail {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        background-color: #fff;
        padding: 20px;

        .name {
            font-size: 30px;
            font-weight: 700;
            color: #15181d;

        }

        .center-detail {
            display: flex;
            align-items: center;
            color: #858a99;
            font-size: 24px;
            margin-top: 5px;

        }

        .cast {
            color: #858a99;
            font-size: 24px;
            margin-top: 5px;
            word-break: break-all;
            text-align: center;
            overflow: hidden;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;

        }
    }



    .wrap {
        padding: 10px 0;


    }

    .box {
        width: 0;
        height: 0;
        border: 10rpx solid;
        border-color: transparent transparent #fff #fff;
        transform: rotate(135deg);
        position: absolute;
        bottom: -10rpx;
        left: 0;
        right: 0;
        margin: 0 auto;
    }
}
</style>

2.movie-list代码


<template>
    <div class="movie-list-component">
        <div class="list-container">
            <scroll-view id="contentScroll" class="scroll-view" :scroll-with-animation="true" :scroll-left="scrollLeft"
                :scroll-x="true">
                <div class="movie-item seat"></div>
                <div :data-id="item.movieId" @click="selectMovie" :id="`movieItem${item.movieId}`" class="movie-item" 
                    :class="{ active: modelValue == item.movieId }" v-for="( item, index ) in  list " :key="index">
                    <div class="img-container">
                        <image class="img" :src="item?.posterUrl" alt="" />
                    </div>
                </div>
                <div class="movie-item seat1"></div>
            </scroll-view>
        </div>
    </div>
</template>
<script setup lang="ts">
import Taro from "@tarojs/taro";
import { onMounted, ref, reactive, toRefs, watch } from "vue";
const props = defineProps({
    // 子组件接收父组件传递过来的值
    list: {
        type: Array<any>,
        required: true,
    },
    modelValue: {
        type: Number,
        required: true,
    },
});
//使用父组件传递过来的值
const { list, modelValue } = toRefs(props);
const emit = defineEmits(["onchangeMovie"]);

onMounted(() => {
    // selectMovie(list.value[0].movieId)
    list?.value.map((item, index) => {
        if (item.id === modelValue?.value) {
            list?.value.unshift(list?.value.splice(index, 1)[0]);
        }
    });

});
const scrollLeft = ref(0);
//选择电影
const selectMovie = (e) => {
    let offsetLeft = e.currentTarget.offsetLeft;
    let { id } = e.currentTarget.dataset;
    if (!id) {
        id = list.value[0].movieId;
    }
    if (modelValue.value == id) {
        return;
    }
    emit("onchangeMovie", id);
    getRect(id, offsetLeft);
};
//计算电影item的偏移量
const getRect = async (id, offsetLeft) => {

    const eleId = `#movieItem${id}`;

    const contentScrollWidth: any = await getContentScrollWidth("#contentScroll");
    const query = Taro.createSelectorQuery();
    query.select(eleId).boundingClientRect();
    query.selectViewport().scrollOffset();
    query.exec(async (res) => {

        //获取item的宽度de 一半
        const subhalfwidth = res[0].width / 2;
        //需要scrollview 移动的距离是
        const juli = offsetLeft - contentScrollWidth / 2 + subhalfwidth;
        scrollLeft.value = juli;

    });
};
// 获取ScrollView的宽度
const getContentScrollWidth = (ele) => {
    return new Promise((resolve) => {
        const query = Taro.createSelectorQuery();
        query.select(ele).boundingClientRect();
        query.selectViewport().scrollOffset();
        query.exec((res) => {
            const width = res[0].width;
            resolve(width);
        });
    });
};
</script>
<style lang="scss">
.movie-list-component {
    display: flex;
    flex-direction: column;

    .list-container {
        display: flex;
        flex-direction: column;
        justify-content: center;
        height: 324px;

        .scroll-view {
            width: 100%;
            height: 324px;
            white-space: nowrap;
            position: relative;

            .movie-item:nth-child(n+2) {
                margin-left: 35px;
            }

            .movie-item {
                display: inline-block;
                position: relative;

                margin-top: 30px;
                border-radius: 18px;
                width: 156px;
                height: 218px;
                // line-height: 208px;
                transition: width 1s;
                transition: height 1s;

                .img-container {
                    border-radius: 8px;
                    width: 100%;
                    height: 100%;
                    overflow: hidden;
                    position: relative;
                    z-index: 2;
                    // border: 5px #ffffff solid;

                    .img {
                        width: 100%;
                        height: 100%;
                    }
                }
            }

            .movie-item.active {
                transform: scale(1.15);
                /* 放大1.2倍 */
                transition: transform 1s ease;
                /* 过渡效果 */
            }

            .seat {
                display: inline-block;
                width: 50%;
                // height: 290px;
                margin-left: -110px;
            }

            .seat1 {
                display: inline-block;
                width: 35%;
            }
        }
    }
}
</style>

filter 组件

<template>
    <div class="cinema-detail-filter-container">
        <div class="date-tab">
            <nut-tabs :title-gutter="15" v-model="tabTimesIndex" title-scroll>
                <nut-tab-pane v-for="(item, index) in tabTimesList" :pane-key="index" :title="`${item.t} ${item.a}`">
                </nut-tab-pane>
            </nut-tabs>
        </div>
    </div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
import moment from "moment";
const tabTimesIndex = ref(0)
const tabTimesList: any = ref([])
const props = defineProps({
    //子组件接收父组件传递过来的值
    data: Object,
});
//使用父组件传递过来的值
const { data: timesList } = toRefs(props);
onMounted(() => {
    getTimes()
});
const emit = defineEmits(['onChanged'])
watch(tabTimesIndex, (index) => {
    emit('onChanged', tabTimesList.value[index])
})
watch(tabTimesList, () => {
    emit('onChanged', tabTimesList.value[0])
})
const getTimes = () => {
    const times = timesList?.value
    
    const arr: any = []
    times?.forEach(item => {
        const time = item
        let t = getweek(moment(time).startOf('day').format('E'))
        if (time === moment().format('YYYY-MM-DD')) {
            t = '今天'
        } else if (time === moment().subtract(-1, 'days').format('YYYY-MM-DD')) {
            t = '明天'
        } else if (time === moment().subtract(-2, 'days').format('YYYY-MM-DD')) {
            t = '后天'
        }
        arr.push({
            time: time,
            t: t,
            a: time.substr(5, 9)
        })
    });
    tabTimesList.value = arr

}
const getweek = (val) => {
    const week = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
    return week[val - 1];
};
</script>
<style lang="scss">
.cinema-detail-filter-container {
    display: flex;
    flex-direction: column;

    .nut-tabs {
        .nut-tabs__titles {
            background: #ffffff !important;

            .nut-tabs__titles-item {
                .nut-tabs__titles-item__smile {
                    display: none;
                }

                .nut-tabs__titles-item__text {
                    color: #858a99;
                    font-size: 22px !important;

                }
            }

            .nut-tabs__titles-item__line {
                background: linear-gradient(to right, #5232B7, #7237BC, #5232B7) !important;
                border-radius: 30px !important;
            }

            .nut-tabs__titles-item.active {
                .nut-tabs__titles-item__smile {
                    display: block;
                    margin-top: 10px;
                }

                .nut-tabs__titles-item__text {
                    color: #15181d;
                }
            }
        }

        .nut-tabs__content {
            display: none !important;
        }
    }
}
</style>

header 组件

<template>
    <div class="movie-header-box">
        <div class="left">
            <div class="name">万达影城(北京昌平保利光魔店)</div>
            <div class="address">昌平区鼓楼街贾琏时代广场四楼</div>

        </div>
        <div class="right">
            <IconFont name="locationg3" color="#5232B7"></IconFont>
        </div>
    </div>
</template>
<script setup>
import { IconFont } from '@nutui/icons-vue-taro';
</script>
<style lang="scss">
.movie-header-box {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background-color: #fff;
    padding: 25px 30px;

    .left {
        .name {
            color: #15181d;
            font-weight: 700;
            font-size: 26px;
        }

        .address {
            color: #858a99;
            font-size: 22px;
            margin-top: 10px;
        }
    }
}
</style>

5.我这个项目是基于Taro +vue3 +ts 来写的 用的组件库也是京东的nut-ui 以上的代码和组件 也有的是我二次封装的组件 组件也挺方便的

相关推荐
Engss5 天前
taro RN 左右滑动切换页面
前端·javascript·react.js·taro
Lyda8 天前
uniapp vs taro3 vue 小程序动态渲染
javascript·微信小程序·taro
哈哈皮皮虾的皮9 天前
react和taro之间的关系
前端·react.js·taro
少恭写代码10 天前
使用duxapp开发 React Native App 事半功倍
react native·小程序·taro·duxapp
少恭写代码13 天前
duxapp:基于Taro使用模块化开发,提升开发效率
react native·小程序·taro·duxapp
谢尔登15 天前
【Taro】初识 Taro
taro
书边事.22 天前
Taro实现微信小程序自定义拍照截图识别
微信小程序·小程序·taro
游小北24 天前
Taro + Vue 的 CSS Module 解决方案
css·vue.js·taro
程序设计实验室24 天前
项目完成小结:使用DjangoStarter v3和Taro开发的微信小程序
微信小程序·django·taro·web前端·djangostarter
二豆是富婆1 个月前
taro ui 小程序at-calendar日历组件自定义样式+选择范围日历崩溃处理
小程序·taro