Trending.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. import { useState, useRef, useEffect } from "react";
  2. import { Swiper, SwiperSlide } from "swiper/react";
  3. import type { Swiper as SwiperType } from "swiper/types";
  4. // import { Navigation } from "swiper/modules";
  5. import Image from "next/image";
  6. import Link from "next/link";
  7. import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
  8. import type { HomeProduct,HomeSection } from "@/types/home/type";
  9. interface newArrivalSectionProps{
  10. trendingSection:HomeSection |undefined;
  11. }
  12. // // 商品数据,每个卡片携带videoUrl视频地址
  13. // type ProductItem = {
  14. // id: number;
  15. // img: string;
  16. // title: string;
  17. // price: number;
  18. // videoUrl: string;
  19. // };
  20. // const productList: ProductItem[] = [
  21. // {
  22. // id: 1,
  23. // img: "/img/hair-video-1.jpg",
  24. // title: "14-36 In Water Wave Human Hair HD Inv...",
  25. // price: 146.1,
  26. // videoUrl: "/video/hair-demo-1.mp4",
  27. // },
  28. // {
  29. // id: 2,
  30. // img: "/img/hair-video-2.jpg",
  31. // title: "14-36 In Water Wave Human Hair HD Inv...",
  32. // price: 146.1,
  33. // videoUrl: "/video/hair-demo-2.mp4",
  34. // },
  35. // {
  36. // id: 3,
  37. // img: "/img/hair-video-3.jpg",
  38. // title: "14-36 In Water Wave Human Hair HD Inv...",
  39. // price: 146.1,
  40. // videoUrl: "/video/hair-demo-3.mp4",
  41. // },
  42. // {
  43. // id: 4,
  44. // img: "/img/hair-video-4.jpg",
  45. // title: "14-36 In Water Wave Human Hair HD Inv...",
  46. // price: 146.1,
  47. // videoUrl: "/video/hair-demo-4.mp4",
  48. // },
  49. // ];
  50. const SLIDES_PER_VIEW = 6.5;
  51. const SLIDES_PER_GROUP = 6;
  52. const SPACE_BETWEEN = 24;
  53. const Trending = ({ trendingSection }: newArrivalSectionProps) => {
  54. const trending: HomeProduct[] = trendingSection?.products ?? [];
  55. // 视频弹窗状态
  56. const [openVideoModal, setOpenVideoModal] = useState(false);
  57. const [currentVideoSrc, setCurrentVideoSrc] = useState("");
  58. const videoRef = useRef<HTMLVideoElement>(null);
  59. // 弹窗打开自动播放
  60. useEffect(() => {
  61. if (openVideoModal && videoRef.current) {
  62. videoRef.current.play().catch((err) => console.log("播放拦截", err));
  63. }
  64. }, [openVideoModal]);
  65. const swiperRef = useRef<SwiperType | null>(null);
  66. const [activePage, setActivePage] = useState(0);
  67. const [isBeginning, setIsBeginning] = useState(true);
  68. const [isEnd, setIsEnd] = useState(trending.length <= SLIDES_PER_VIEW);
  69. if (!trending || trending.length === 0) return null;
  70. // const productList = trending;
  71. // 打开弹窗并赋值视频地址
  72. const handlePlayClick = (videoUrl: string) => {
  73. setCurrentVideoSrc(videoUrl);
  74. setOpenVideoModal(true);
  75. };
  76. // 关闭弹窗,暂停视频
  77. const closeModal = () => {
  78. setOpenVideoModal(false);
  79. if (videoRef.current) {
  80. videoRef.current.pause();
  81. videoRef.current.currentTime = 0;
  82. }
  83. };
  84. const pageCount = Math.ceil(trending.length / SLIDES_PER_VIEW);
  85. const syncSwiperState = (swiper: SwiperType) => {
  86. const pageIndex = Math.min(swiper.snapIndex, Math.max(pageCount - 1, 0));
  87. setActivePage(pageIndex);
  88. setIsBeginning(swiper.isBeginning);
  89. setIsEnd(swiper.isEnd);
  90. };
  91. const handlePrev = () => {
  92. swiperRef.current?.slidePrev();
  93. };
  94. const handleNext = () => {
  95. swiperRef.current?.slideNext();
  96. };
  97. const handlePaginationClick = (pageIndex: number) => {
  98. const swiper = swiperRef.current;
  99. if (!swiper) return;
  100. swiper.slideTo(pageIndex * SLIDES_PER_GROUP);
  101. };
  102. return (
  103. <div className=" relative w-full mx-auto">
  104. <h2 className="text-4xl text-center font-bold leading-ly-46 mt-12 mb-8">{trendingSection?.title}</h2>
  105. <div className="pl-12">
  106. <Swiper
  107. slidesPerView={SLIDES_PER_VIEW}
  108. slidesPerGroup={SLIDES_PER_GROUP}
  109. spaceBetween={SPACE_BETWEEN}
  110. speed={300}
  111. watchOverflow
  112. onSwiper={(swiper) => {
  113. swiperRef.current = swiper;
  114. syncSwiperState(swiper);
  115. }}
  116. onSlideChange={syncSwiperState}
  117. onReachBeginning={syncSwiperState}
  118. onReachEnd={syncSwiperState}
  119. onFromEdge={syncSwiperState}
  120. className="w-full"
  121. >
  122. {trending.map((item, index) => (
  123. <SwiperSlide key={`${item.id}-${index}`}>
  124. <div className=" overflow-hidden flex flex-col">
  125. {/* 图片区域 + 中间播放按钮 */}
  126. <div className="relative rounded-xl w-full aspect-[4/5] overflow-hidden rounded-xl">
  127. <img
  128. src={item.image_url}
  129. alt={item.name}
  130. className="w-full h-full object-cover"
  131. />
  132. {/* 居中圆形播放按钮 */}
  133. <button
  134. onClick={() =>
  135. handlePlayClick(item.videoUrl || "/video/hair-demo-1.mp4")
  136. }
  137. className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
  138. w-24 h-24 rounded-full bg-white/60 backdrop-blur-sm
  139. flex items-center justify-center transition hover:bg-white/80"
  140. >
  141. <svg
  142. width="36"
  143. height="36"
  144. viewBox="0 0 24 24"
  145. fill="none"
  146. stroke="#222"
  147. strokeWidth="2"
  148. >
  149. <polygon points="5 3 19 12 5 21 5 3" />
  150. </svg>
  151. </button>
  152. </div>
  153. {/* 底部文字价格区域 */}
  154. <div className="p-3 flex flex-col rounded-xl mt-3 bg-[#f0f8f5] gap-3">
  155. <Link href={`/${item.urlKey}.html`} className="text-sm text-gray-800 truncate">
  156. {item.name}
  157. </Link>
  158. {/* 价格 + 购物车图标 */}
  159. <div className="flex items-center justify-between">
  160. <div className="flex items-center gap-1">
  161. <span className="font-semibold">${item.min_price}</span>
  162. <span className="text-xs text-gray-500 line-through">
  163. ${item.price}
  164. </span>
  165. </div>
  166. {/* 购物车图标 */}
  167. <OpenAddToCartModalButton
  168. productId={item.id}
  169. icon={
  170. <svg
  171. xmlns="http://www.w3.org/2000/svg"
  172. width="32"
  173. height="32"
  174. viewBox="0 0 32 32"
  175. fill="none"
  176. >
  177. <rect
  178. x="0"
  179. y="0"
  180. width="32"
  181. height="32"
  182. rx="8"
  183. fill="#036141"
  184. ></rect>
  185. <path
  186. stroke="rgba(255, 255, 255, 1)"
  187. stroke-width="1.3333333333333333"
  188. stroke-linejoin="round"
  189. stroke-linecap="round"
  190. d="M6.40039 7.79993L8.03372 7.79993L8.73372 10.5999M8.73372 10.5999L11.0671 19.9333L23.2004 19.9333L25.5337 10.5999L8.73372 10.5999Z"
  191. ></path>
  192. <ellipse
  193. cx="11.066040515899658"
  194. cy="23.199928283691406"
  195. rx="1.4000248908996582"
  196. ry="1.4000015258789062"
  197. stroke="rgba(255, 255, 255, 1)"
  198. stroke-width="1.3333333333333333"
  199. stroke-linejoin="round"
  200. stroke-linecap="round"
  201. ></ellipse>
  202. <ellipse
  203. cx="23.1998291015625"
  204. cy="23.199928283691406"
  205. rx="1.4000244140625"
  206. ry="1.4000015258789062"
  207. stroke="rgba(255, 255, 255, 1)"
  208. stroke-width="1.3333333333333333"
  209. stroke-linejoin="round"
  210. stroke-linecap="round"
  211. ></ellipse>
  212. <path
  213. stroke="rgba(255, 255, 255, 1)"
  214. stroke-width="1.3333333333333333"
  215. stroke-linejoin="round"
  216. stroke-linecap="round"
  217. d="M15.2656 15.2665L18.999 15.2665"
  218. ></path>
  219. <path
  220. stroke="rgba(255, 255, 255, 1)"
  221. stroke-width="1.3333333333333333"
  222. stroke-linejoin="round"
  223. stroke-linecap="round"
  224. d="M17.1348 17.1332L17.1348 13.3999"
  225. ></path>
  226. </svg>
  227. }
  228. />
  229. </div>
  230. </div>
  231. </div>
  232. </SwiperSlide>
  233. ))}
  234. </Swiper>
  235. </div>
  236. {/* ====================== 全屏视频遮罩弹窗 ====================== */}
  237. {openVideoModal && (
  238. <div
  239. className="fixed inset-0 z-[9999] bg-black/95 flex items-center justify-center p-4"
  240. onClick={closeModal} // 点击遮罩空白关闭
  241. >
  242. {/* 弹窗内层,阻止冒泡 */}
  243. <div
  244. className="relative w-full max-w-4xl"
  245. onClick={(e) => e.stopPropagation()}
  246. >
  247. {/* 右上角关闭按钮 */}
  248. <button
  249. onClick={closeModal}
  250. className="absolute -top-12 right-0 text-white text-3xl hover:text-gray-300"
  251. >
  252. ✕
  253. </button>
  254. {/* 视频播放器 */}
  255. <video
  256. ref={videoRef}
  257. src={currentVideoSrc}
  258. controls
  259. className="w-full rounded-lg"
  260. />
  261. </div>
  262. </div>
  263. )}
  264. {!isBeginning && (
  265. <button
  266. type="button"
  267. aria-label="Previous"
  268. onClick={handlePrev}
  269. className="
  270. absolute
  271. left-18
  272. top-1/2
  273. z-10
  274. flex
  275. h-10
  276. w-10
  277. -translate-y-1/2
  278. items-center
  279. justify-center
  280. "
  281. >
  282. <svg
  283. xmlns="http://www.w3.org/2000/svg"
  284. width="40"
  285. height="40"
  286. viewBox="0 0 40 40"
  287. fill="none"
  288. className="rotate-180"
  289. >
  290. <circle cx="20" cy="20" r="20" fill="#000000" fillOpacity="0.5" />
  291. <path
  292. d="M17 26L23 20L17 14"
  293. stroke="rgba(255, 255, 255, 1)"
  294. strokeWidth="1.5"
  295. strokeLinejoin="round"
  296. strokeLinecap="square"
  297. />
  298. </svg>
  299. </button>
  300. )}
  301. {/* Next - 最后一页不显示 */}
  302. {!isEnd && (
  303. <button
  304. type="button"
  305. aria-label="Next"
  306. onClick={handleNext}
  307. className="
  308. absolute
  309. right-6
  310. top-1/2
  311. z-10
  312. flex
  313. h-10
  314. w-10
  315. -translate-y-1/2
  316. items-center
  317. justify-center
  318. "
  319. >
  320. <svg
  321. xmlns="http://www.w3.org/2000/svg"
  322. width="40"
  323. height="40"
  324. viewBox="0 0 40 40"
  325. fill="none"
  326. >
  327. <circle cx="20" cy="20" r="20" fill="#000000" fillOpacity="0.5" />
  328. <path
  329. d="M17 26L23 20L17 14"
  330. stroke="rgba(255, 255, 255, 1)"
  331. strokeWidth="1.5"
  332. strokeLinejoin="round"
  333. strokeLinecap="square"
  334. />
  335. </svg>
  336. </button>
  337. )}
  338. {/* Pagination */}
  339. {pageCount > 1 && (
  340. <div
  341. className="
  342. mt-4
  343. flex
  344. items-center
  345. justify-center
  346. gap-2
  347. "
  348. >
  349. {Array.from({
  350. length: pageCount,
  351. }).map((_, index) => (
  352. <button
  353. key={index}
  354. type="button"
  355. aria-label={`Go to page ${index + 1}`}
  356. onClick={() => handlePaginationClick(index)}
  357. className={`
  358. h-2
  359. w-10
  360. rounded-full
  361. transition-all
  362. duration-300
  363. ${activePage === index ? "bg-black" : "bg-[#DBDBDB]!"}
  364. `}
  365. />
  366. ))}
  367. </div>
  368. )}
  369. <div className="relative w-full aspect-[4/3] mt-8">
  370. <Image
  371. src={
  372. "https://cdn.asteriahair.com/uploads/202607/01/20260701165110_3fff435adf276b89.jpg"
  373. }
  374. alt={"cscscs"}
  375. fill
  376. className="object-cover rounded-md"
  377. />
  378. </div>
  379. </div>
  380. );
  381. };
  382. export default Trending;