3D走马灯(网页&&小程序)

一、网页

react+antd-mobile

javascript 复制代码
import { 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] = useState<number>(0);
	const [currentId, setCurrentId] = useState<string>("");
	const swiperRef = useRef<SwiperRef>(null);
	const [startX, setStartX] = useState(0);
	const [moveX, setMoveX] = useState(0); // x移动的距离 <0:左滑  >0:右滑
	const [nextTranslateX, setNextTranslateX] = useState<number>(380); // x移动的距离
	const [nextScale, setNextScale] = useState<number>(0.6); // 其他的缩放比例
	const [currentScale, setCurrentScale] = useState<number>(1); // 当前选中的缩放比例
	const [currentOpacity, setCurrentOpacity] = useState<number>(1); // 当前选中的透明度比例
	const [nextOpacity, setNextOpacity] = useState<number>(0.6); // 其他的透明度比例
	const screenWidth = window.innerWidth; // 获取当前屏幕宽度
	const maxScreenWidth = screenWidth / 2;
	const [nameList, setNameList] = useState<DialogRoleItemModel[]>([]);

	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.TouchEvent<HTMLDivElement>) => {
		setStartX(e.touches[0].clientX);
	};

	const handleMove = (e: React.TouchEvent<HTMLDivElement>) => {
		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.TouchEvent<HTMLDivElement>) => {
		// 重置缩放和移动
		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;
css 复制代码
@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;
			}
		}
	}
}

二、小程序

javascript 复制代码
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.FC<RoleCarouselProps> = forwardRef((props) => {
  const {
    configStore: { systemInfo },
  } = useStores();
  const [currentIndex, setCurrentIndex] = useState<number>(0); // 当前选中的下标
  const [startX, setStartX] = useState<number>(0); // x起始位置
  const [currentScale, setCurrentScale] = useState<number>(1); // 当前选中的缩放比例
  const [currentTranslateX, setCurrentTranslateX] = useState<number>(0); // x移动的距离 <0:左滑  >0:右滑
  const [nextScale, setNextScale] = useState<number>(0.35); // 其他的缩放比例
  const [nextTranslateX, setNextTranslateX] = useState<number>(380); // x移动的距离
  const [startTime, setStartTime] = useState<number>(0); // 开始滑动时间
  const [currentOpacity, setCurrentOpacity] = useState<number>(1); // 当前选中的透明度比例
  const [nextOpacity, setNextOpacity] = useState<number>(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 className="container">
      <MovableView
        className="carousel-wrapper"
        direction="horizontal"
        inertia
        onTouchStart={handleTouchStart}
        onTouchEnd={handleTouchEnd}
        onTouchMove={handleTouchMove}
      >
        <View className="scene">
          {props.roles.map((role, index) => (
            <View key={role.id} className="card" style={{ ...getTransformStyle(index), backgroundImage: `url(${role?.characterImage})` }}>
            </View>
          ))}
        </View>
      </MovableView>
    </View>
  );
});

export default RoleCarousel;
css 复制代码
/* 样式文件 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;
        }
      }
    }
  }
}
相关推荐
扶苏10022 小时前
记一次 uni-app开发微信小程序 textarea 的“伪遮挡”踩坑实录
微信小程序·小程序·uni-app
Greg_Zhong2 小时前
认识前端自动化测试、小程序中如何实现单元测试
前端·小程序·单元测试
普密斯科技2 小时前
高精度车载插座多维度检测方案——基于3D线激光轮廓传感器的实践应用
大数据·人工智能·深度学习·计算机视觉·3d·测量
Struart_R2 小时前
StreamVGGT、Stream3R、InfiniteVGGT论文解读
人工智能·计算机视觉·3d·视频·多模态
NPUQS3 小时前
【Unity 3D学习】Unity 与 Python 互通入门:点击按钮调用 Python(超简单示例)
学习·3d·unity
早點睡3904 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-dropdown-picker
javascript·react native·react.js
英俊潇洒美少年13 小时前
React 最核心 3 大底层原理:Fiber + Diff + 事件系统
前端·react.js·前端框架
我命由我1234513 小时前
React Router 6 - 概述、基础路由、重定向、NavLink、路由表
前端·javascript·react.js·前端框架·ecmascript·html5·js
GISer_Jing14 小时前
ReAct规划原理实战指南
前端·react.js·ai·aigc