一、网页
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;
}
}
}
}
}