3D走马灯(网页小程序)
一、网页reactantd-mobileimport { useNavigate } from react-router-dom; import { useEffect, useState, useRef } from react; import { observer } from mobx-react; import { DialogRoleItemModel } from /models/aiDialog; import { Image, Swiper } from antd-mobile; import { SwiperRef } from antd-mobile/es/components/swiper; import api from /api; import { useStores } from /store; import classNames from classnames; import styles from ./index.module.less; const DialogEnter observer(() { const { dialogStore: { visitorList, setVisitorList, setDialogIndex, setCurrentAgent } } useStores(); const navigate useNavigate(); const [swiperIndex, setSwiperIndex] useStatenumber(0); const [currentId, setCurrentId] useStatestring(); const swiperRef useRefSwiperRef(null); const [startX, setStartX] useState(0); const [moveX, setMoveX] useState(0); // x移动的距离 0:左滑 0:右滑 const [nextTranslateX, setNextTranslateX] useStatenumber(380); // x移动的距离 const [nextScale, setNextScale] useStatenumber(0.6); // 其他的缩放比例 const [currentScale, setCurrentScale] useStatenumber(1); // 当前选中的缩放比例 const [currentOpacity, setCurrentOpacity] useStatenumber(1); // 当前选中的透明度比例 const [nextOpacity, setNextOpacity] useStatenumber(0.6); // 其他的透明度比例 const screenWidth window.innerWidth; // 获取当前屏幕宽度 const maxScreenWidth screenWidth / 2; const [nameList, setNameList] useStateDialogRoleItemModel[]([]); const changeSwiperIndex (roleItem: DialogRoleItemModel) { const index visitorList.findIndex((item) item.id roleItem.id); setSwiperIndex(index); if (swiperRef.current) { swiperRef.current.swipeTo(index); } }; const getCurrentStyle (index: number) { if (index swiperIndex) { // 当前卡片 return { opacity: ${currentOpacity}, transform: scale(${currentScale}) }; } if (index swiperIndex 1 || (swiperIndex visitorList.length - 1 index 0)) { // 右边卡片 // 左滑 return { opacity: ${moveX 0 ? nextOpacity : 0.6}, transform: scale(${moveX 0 ? nextScale : 0.6}) translateX(${moveX 0 ? nextTranslateX px : -200px}) }; } else if ((swiperIndex - 1 0 index swiperIndex - 1) || (swiperIndex 0 index visitorList.length - 1)) { // 左边卡片 // 右滑 return { opacity: ${moveX 0 ? nextOpacity : 0.6}, transform: scale(${moveX 0 ? nextScale : 0.6}) translateX(${moveX 0 ? nextTranslateX px : 200px}) }; } else { return { opacity: 0 }; } }; const getScale (dx: number) { // 获取缩放比例 const moveMax dx screenWidth ? screenWidth : dx -screenWidth ? -screenWidth : dx; const move Math.abs(moveMax) * (100 / screenWidth); const scale move 60 ? 60 / 100 : move / 100; return Math.round(scale * 100) / 100; }; const handleStartCapture (e: React.TouchEventHTMLDivElement) { setStartX(e.touches[0].clientX); }; const handleMove (e: React.TouchEventHTMLDivElement) { let offsetTranslateX 0; // 移动偏差值 const deltaX e.touches[0].clientX - startX; // 判断左右滑动 const currentScaleNum getScale(deltaX); // 当前选中的缩放比例 const offsetScale 1 - currentScaleNum; // 缩放偏差值 setCurrentScale(currentScaleNum); // 设置当前选中的缩放比例 setNextScale(1 - Math.abs(offsetScale) 0.3); // 设置其他缩放偏差值 setCurrentOpacity(0.6); setNextOpacity(1); if (deltaX 0 deltaX maxScreenWidth) { // 右滑 // 滑动在范围内才处理其他的缩放 offsetTranslateX maxScreenWidth deltaX; // 移动偏差值 const nextTranslateX maxScreenWidth - offsetTranslateX 200 ? 200 : maxScreenWidth - offsetTranslateX; setNextTranslateX(nextTranslateX); // 设置其他移动值 } else if (deltaX 0 deltaX -maxScreenWidth) { // 左滑 // 滑动在范围内才处理其他的缩放 offsetTranslateX maxScreenWidth deltaX; // 移动偏差值 const nextTranslateX maxScreenWidth offsetTranslateX -200 ? -200 : maxScreenWidth - offsetTranslateX; setNextTranslateX(nextTranslateX); // 设置其他移动值 } setMoveX(deltaX); }; const handleTachEnd (e: React.TouchEventHTMLDivElement) { // 重置缩放和移动 setCurrentScale(1); setMoveX(0); setCurrentOpacity(1); setNextOpacity(0.6); }; const goTalkPage () {}; // 获取ai聊愈角色信息 const getVisitorListFun async () { try { const { code, data } await api.getVisitorList(); if (code 200 data.length) { setVisitorList(data); setSwiperIndex(0); setCurrentId(data[0]?.id); } } catch (error) { console.log(error); } }; const forMatName () { const name: DialogRoleItemModel[] []; if (visitorList.length 3) { setNameList(visitorList); return; } if (!visitorList[swiperIndex - 1]) { name.push(visitorList[visitorList.length - 1]); name.push(visitorList[swiperIndex]); name.push(visitorList[swiperIndex 1]); setNameList(name); return; } if (!visitorList[swiperIndex 1]) { name.push(visitorList[swiperIndex - 1]); name.push(visitorList[swiperIndex]); name.push(visitorList[0]); setNameList(name); return; } name.push(visitorList[swiperIndex - 1]); name.push(visitorList[swiperIndex]); name.push(visitorList[swiperIndex 1]); setNameList(name); }; useEffect(() { if (visitorList.length) { forMatName(); setCurrentId(visitorList[swiperIndex]?.id); } }, [visitorList, swiperIndex]); useEffect(() { getVisitorListFun(); }, []); return ( div className{styles.ai_dialog_enter} div className{styles.main} div className{styles.roles_name_box} div className{styles.roles_name_list} {nameList?.map((roleItem: DialogRoleItemModel, roleIndex: number) { return ( div className{classNames({ [styles.item]: true, [styles.item_active]: currentId roleItem?.id })} onClick{() { changeSwiperIndex(roleItem); }} key{name-${roleIndex}} {roleItem.aiUserName} /div ); })} /div /div div className{styles.roles_list} Swiper className{styles.swiper_box} slideSize{50} trackOffset{25} stuckAtBoundary{false} total{visitorList.length} indicator{false} defaultIndex{swiperIndex} loop{true} onIndexChange{(index) { setSwiperIndex(index); }} ref{swiperRef} {visitorList.map((role, index) { return ( Swiper.Item key{index} div style{getCurrentStyle(index)} onTouchStartCapture{handleStartCapture} onTouchMove{handleMove} onTouchEnd{handleTachEnd} className{styles.swiper_item} onClick{() { changeSwiperIndex(role); }} {role?.characterImage Image src{role.characterImage || } className{styles.avatar} /} /div /Swiper.Item ); })} /Swiper /div div className{styles.roles_content} div className{styles.content_tag}{visitorList[swiperIndex]?.tag}/div div className{styles.content_aiUserBrief}{visitorList[swiperIndex]?.aiUserBrief}/div div className{styles.content_btn} onClick{() { goTalkPage(); }} 选择TA /div /div /div /div ); }); export default DialogEnter;import css/mixins.less; .ai_dialog_enter { height: 100%; width: 100%; background: url(images/dialog/role-bg-1.jpg) 0 0 no-repeat; background-size: cover; background-position: center 55%; position: relative; z-index: 2; overflow: hidden; .role_bottom { position: absolute; background: url(images/dialog/role-bottom.webp); background-size: cover; background-position: 30% 10%; top: 56vh; width: 100vw; height: 53vh; left: 0; z-index: 1; } :global { .adm-button { border-color: #333; ::before { background-color: #333; border-color: #333; } } } .main { padding: 0 30px; position: relative; z-index: 2; .roles_name_box, .roles_name_list, .item, .roles_list, .roles_content { display: flex; align-items: center; justify-content: center; } .roles_name_box { min-height: 230px; max-height: 230px; } .roles_name_list { height: 124px; width: 720px; border-radius: 57px; padding: 0 10px; background: rgba(#469c86, 0.5); border: 1px solid rgba(255, 255, 255, 0.4); .item { width: 232px; height: 104px; font-size: 36px; cursor: pointer; color: rgba(255, 255, 255, 0.5); } .item_active { background: rgba(255, 255, 255, 0.12); border-radius: 47px; font-weight: 800; color: #ffffff; } } .roles_list { width: 100%; height: calc(100% - 213px); margin-top: 100px; margin-bottom: 60px; .swiper_box { width: 65%; height: 50%; .swiper_item { transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1); } .avatar { position: relative; } .role_avatar_active { left: -50%; } } } .roles_content { flex-direction: column; color: #ffffff; .content_tag { font-size: 44px; } .content_aiUserBrief { font-size: 28px; margin-top: 20px; } .content_btn { width: 337px; height: 104px; background: #419798; border-radius: 52px; text-align: center; line-height: 104px; font-size: 44px; margin-top: 60px; } } } }二、小程序import { useState, forwardRef } from react; import { View, Image, MovableView } from tarojs/components; import { useStores } from /store; import { DialogRoleItemModel } from /models; import ./index.scss; interface RoleCarouselProps { roles: DialogRoleItemModel[]; setAiRoleCurrentIndex: (index: number) void; } const RoleCarousel: React.FCRoleCarouselProps forwardRef((props) { const { configStore: { systemInfo }, } useStores(); const [currentIndex, setCurrentIndex] useStatenumber(0); // 当前选中的下标 const [startX, setStartX] useStatenumber(0); // x起始位置 const [currentScale, setCurrentScale] useStatenumber(1); // 当前选中的缩放比例 const [currentTranslateX, setCurrentTranslateX] useStatenumber(0); // x移动的距离 0:左滑 0:右滑 const [nextScale, setNextScale] useStatenumber(0.35); // 其他的缩放比例 const [nextTranslateX, setNextTranslateX] useStatenumber(380); // x移动的距离 const [startTime, setStartTime] useStatenumber(0); // 开始滑动时间 const [currentOpacity, setCurrentOpacity] useStatenumber(1); // 当前选中的透明度比例 const [nextOpacity, setNextOpacity] useStatenumber(0.6); // 其他的透明度比例 const maxScreenWidth systemInfo.screenWidth / 2; // 变换样式 const getTransformStyle (index: number) { const offset index - currentIndex; if (index currentIndex) { // 当前卡片 return { opacity: ${currentOpacity}, zIndex: props.roles.length - Math.abs(offset), transform: scale(${currentScale}) translateX(${currentTranslateX}px), }; } else if (index currentIndex 1 || (currentIndex props.roles.length - 1 index 0)) { // 右边卡片 // 左滑 return { opacity: ${currentTranslateX 0 ? nextOpacity : 0.6}, zIndex: props.roles.length - Math.abs(offset), transform: scale(${currentTranslateX 0 ? nextScale : 0.35}) translateX(${currentTranslateX 0 ? nextTranslateX px : 380px}), }; } else if ((currentIndex - 1 0 index currentIndex - 1) || (currentIndex 0 index props.roles.length - 1)) { // 左边卡片 // 右滑 return { opacity: ${currentTranslateX 0 ? nextOpacity : 0.6}, zIndex: props.roles.length - Math.abs(offset), transform: scale(${currentTranslateX 0 ? nextScale : 0.35}) translateX(${currentTranslateX 0 ? nextTranslateX px : -380px}), }; } else { return { opacity: 0, }; } }; // 触摸开始 const handleTouchStart (e) { setStartX(e.touches[0].pageX); setStartTime(Date.now()); }; const getScale (dx: number) { // 获取缩放比例 let moveMax dx systemInfo.screenWidth ? systemInfo.screenWidth : dx -systemInfo.screenWidth ? -systemInfo.screenWidth : dx; let move Math.abs(moveMax) * (100 / systemInfo.screenWidth); let scale move 35 ? 35 / 100 : move / 100; return Math.round(scale * 100) / 100; }; const getOpacity (dx: number) { // 获取透明度 let move Math.abs(dx); return Math.round(move * 100) / 10000 1 ? 1 : Math.round(move * 100) / 10000; }; // 触摸移动 const handleTouchMove (e) { let offsetTranslateX 0; // 移动偏差值 const currentX e.touches[0].pageX; // 当前移动x let deltaX currentX - startX; // 判断左右滑动 let currentScaleNum getScale(deltaX); // 当前选中的缩放比例 const offsetScale 1 - currentScaleNum; // 缩放偏差值 setCurrentScale(currentScaleNum); // 设置当前选中的缩放比例 setNextScale(1 - Math.abs(offsetScale) 0.3); // 设置其他缩放偏差值 setCurrentOpacity(0.6); setNextOpacity(1); console.log(比例, getScale(deltaX)); console.log(deltaX, deltaX); if (deltaX 0 deltaX maxScreenWidth) { // 右滑 // 滑动在范围内才处理其他的缩放 deltaX maxScreenWidth; offsetTranslateX maxScreenWidth deltaX; // 移动偏差值 let nextTranslateX maxScreenWidth - offsetTranslateX 380 ? 380 : maxScreenWidth - offsetTranslateX 100; setNextTranslateX(nextTranslateX); // 设置其他移动值 } else if (deltaX 0 deltaX -maxScreenWidth) { // 左滑 // 滑动在范围内才处理其他的缩放 deltaX -maxScreenWidth; offsetTranslateX maxScreenWidth deltaX; // 移动偏差值 let nextTranslateX maxScreenWidth offsetTranslateX -380 ? -380 : maxScreenWidth - offsetTranslateX - 100; setNextTranslateX(nextTranslateX); // 设置其他移动值 } // 计算水平移动 setCurrentTranslateX(deltaX); }; // 触摸结束 const handleTouchEnd (e) { const endX e.changedTouches[0].pageX; const endTime Date.now(); const deltaX endX - startX; const deltaTime endTime - startTime; // 滑动速度计算 const velocity deltaX / deltaTime; let direction 0; // 阈值判断滑动距离50px 或速度0.3px/ms if (Math.abs(deltaX) 50 || Math.abs(velocity) 0.3) { direction deltaX 0 ? -1 : 1; // 1左滑-1右滑 } if (direction ! 0) { const newIndex (currentIndex direction props.roles.length) % props.roles.length; setCurrentIndex(newIndex); props.setAiRoleCurrentIndex props.setAiRoleCurrentIndex(newIndex); } // 重置缩放和移动 setCurrentScale(1); setCurrentTranslateX(0); setCurrentOpacity(1); setNextOpacity(0.6); }; return ( View classNamecontainer MovableView classNamecarousel-wrapper directionhorizontal inertia onTouchStart{handleTouchStart} onTouchEnd{handleTouchEnd} onTouchMove{handleTouchMove} View classNamescene {props.roles.map((role, index) ( View key{role.id} classNamecard style{{ ...getTransformStyle(index), backgroundImage: url(${role?.characterImage}) }} /View ))} /View /MovableView /View ); }); export default RoleCarousel;/* 样式文件 role-carousel.scss */ .container { height: 50vh; position: relative; perspective: 1200px; .carousel-wrapper { width: 100%; height: 100%; .scene { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; .card { position: absolute; width: 90%; height: 80%; left: 5%; top: 5%; transition: all 0.6s cubic-bezier(0.23, 1, 0.32, 1); transform-origin: center center; background-repeat: no-repeat; background-position: center; background-size: contain; .role-title { font-size: 36px; color: #000000; text-align: center; } .avatar { width: 100%; height: 100%; border-radius: 16px; } .name { font-weight: 800; font-size: 36px; color: #000000; text-align: center; } .role-text { font-size: 24px; color: #000000; line-height: 34px; margin-top: 20px; } .role-btn { width: 340px; height: 96px; background: #8edbc9; border-radius: 58px; font-weight: 700; font-size: 32px; color: #ffffff; text-align: center; line-height: 96px; margin: 20px auto; z-index: 999999; } } } } }