Trending.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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="px-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. <Link href={`/products/${item.urlKey}_${item.id}.html`} className="relative rounded-xl w-full aspect-[4/5] overflow-hidden rounded-xl block">
  127. <Image
  128. src={item.image_url}
  129. alt={item.name}
  130. fill
  131. style={{ objectFit: "cover" }}
  132. />
  133. {/* 居中圆形播放按钮 */}
  134. <button
  135. onClick={() =>
  136. handlePlayClick(item.videoUrl || "/video/hair-demo-1.mp4")
  137. }
  138. className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
  139. w-24 h-24 rounded-full bg-white/60 backdrop-blur-sm
  140. flex items-center justify-center transition hover:bg-white/80"
  141. >
  142. <svg
  143. width="36"
  144. height="36"
  145. viewBox="0 0 24 24"
  146. fill="none"
  147. stroke="#222"
  148. strokeWidth="2"
  149. >
  150. <polygon points="5 3 19 12 5 21 5 3" />
  151. </svg>
  152. </button>
  153. </Link>
  154. {/* 底部文字价格区域 */}
  155. <div className="p-3 flex flex-col rounded-xl mt-3 bg-[#f0f8f5] gap-3">
  156. <Link href={`/products/${item.urlKey}_${item.id}.html`} className="text-sm text-gray-800 truncate">
  157. {item.name}
  158. </Link>
  159. {/* 价格 + 购物车图标 */}
  160. <div className="flex items-center justify-between">
  161. <div className="flex items-center gap-1">
  162. <span className="font-semibold">${item.min_price}</span>
  163. <span className="text-xs text-gray-500 line-through">
  164. ${item.price}
  165. </span>
  166. </div>
  167. {/* 购物车图标 */}
  168. <OpenAddToCartModalButton
  169. productId={item.id}
  170. icon={
  171. <svg
  172. xmlns="http://www.w3.org/2000/svg"
  173. width="32"
  174. height="32"
  175. viewBox="0 0 32 32"
  176. fill="none"
  177. >
  178. <rect
  179. x="0"
  180. y="0"
  181. width="32"
  182. height="32"
  183. rx="8"
  184. fill="#036141"
  185. ></rect>
  186. <path
  187. stroke="rgba(255, 255, 255, 1)"
  188. stroke-width="1.3333333333333333"
  189. stroke-linejoin="round"
  190. stroke-linecap="round"
  191. 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"
  192. ></path>
  193. <ellipse
  194. cx="11.066040515899658"
  195. cy="23.199928283691406"
  196. rx="1.4000248908996582"
  197. ry="1.4000015258789062"
  198. stroke="rgba(255, 255, 255, 1)"
  199. stroke-width="1.3333333333333333"
  200. stroke-linejoin="round"
  201. stroke-linecap="round"
  202. ></ellipse>
  203. <ellipse
  204. cx="23.1998291015625"
  205. cy="23.199928283691406"
  206. rx="1.4000244140625"
  207. ry="1.4000015258789062"
  208. stroke="rgba(255, 255, 255, 1)"
  209. stroke-width="1.3333333333333333"
  210. stroke-linejoin="round"
  211. stroke-linecap="round"
  212. ></ellipse>
  213. <path
  214. stroke="rgba(255, 255, 255, 1)"
  215. stroke-width="1.3333333333333333"
  216. stroke-linejoin="round"
  217. stroke-linecap="round"
  218. d="M15.2656 15.2665L18.999 15.2665"
  219. ></path>
  220. <path
  221. stroke="rgba(255, 255, 255, 1)"
  222. stroke-width="1.3333333333333333"
  223. stroke-linejoin="round"
  224. stroke-linecap="round"
  225. d="M17.1348 17.1332L17.1348 13.3999"
  226. ></path>
  227. </svg>
  228. }
  229. />
  230. </div>
  231. </div>
  232. </div>
  233. </SwiperSlide>
  234. ))}
  235. </Swiper>
  236. </div>
  237. {/* ====================== 全屏视频遮罩弹窗 ====================== */}
  238. {openVideoModal && (
  239. <div
  240. className="fixed inset-0 z-[9999] bg-black/95 flex items-center justify-center p-4"
  241. onClick={closeModal} // 点击遮罩空白关闭
  242. >
  243. {/* 弹窗内层,阻止冒泡 */}
  244. <div
  245. className="relative w-full max-w-4xl"
  246. onClick={(e) => e.stopPropagation()}
  247. >
  248. {/* 右上角关闭按钮 */}
  249. <button
  250. onClick={closeModal}
  251. className="absolute -top-12 right-0 text-white text-3xl hover:text-gray-300"
  252. >
  253. ✕
  254. </button>
  255. {/* 视频播放器 */}
  256. <video
  257. ref={videoRef}
  258. src={currentVideoSrc}
  259. controls
  260. className="w-full rounded-lg"
  261. />
  262. </div>
  263. </div>
  264. )}
  265. {!isBeginning && (
  266. <button
  267. type="button"
  268. aria-label="Previous"
  269. onClick={handlePrev}
  270. className="
  271. absolute
  272. left-18
  273. top-1/2
  274. z-10
  275. flex
  276. h-10
  277. w-10
  278. -translate-y-1/2
  279. items-center
  280. justify-center
  281. "
  282. >
  283. <svg
  284. xmlns="http://www.w3.org/2000/svg"
  285. width="40"
  286. height="40"
  287. viewBox="0 0 40 40"
  288. fill="none"
  289. className="rotate-180"
  290. >
  291. <circle cx="20" cy="20" r="20" fill="#000000" fillOpacity="0.5" />
  292. <path
  293. d="M17 26L23 20L17 14"
  294. stroke="rgba(255, 255, 255, 1)"
  295. strokeWidth="1.5"
  296. strokeLinejoin="round"
  297. strokeLinecap="square"
  298. />
  299. </svg>
  300. </button>
  301. )}
  302. {/* Next - 最后一页不显示 */}
  303. {!isEnd && (
  304. <button
  305. type="button"
  306. aria-label="Next"
  307. onClick={handleNext}
  308. className="
  309. absolute
  310. right-6
  311. top-1/2
  312. z-10
  313. flex
  314. h-10
  315. w-10
  316. -translate-y-1/2
  317. items-center
  318. justify-center
  319. "
  320. >
  321. <svg
  322. xmlns="http://www.w3.org/2000/svg"
  323. width="40"
  324. height="40"
  325. viewBox="0 0 40 40"
  326. fill="none"
  327. >
  328. <circle cx="20" cy="20" r="20" fill="#000000" fillOpacity="0.5" />
  329. <path
  330. d="M17 26L23 20L17 14"
  331. stroke="rgba(255, 255, 255, 1)"
  332. strokeWidth="1.5"
  333. strokeLinejoin="round"
  334. strokeLinecap="square"
  335. />
  336. </svg>
  337. </button>
  338. )}
  339. {/* Pagination */}
  340. {pageCount > 1 && (
  341. <div
  342. className="
  343. mt-4
  344. flex
  345. items-center
  346. justify-center
  347. gap-2
  348. "
  349. >
  350. {Array.from({
  351. length: pageCount,
  352. }).map((_, index) => (
  353. <button
  354. key={index}
  355. type="button"
  356. aria-label={`Go to page ${index + 1}`}
  357. onClick={() => handlePaginationClick(index)}
  358. className={`
  359. h-2
  360. w-10
  361. rounded-full
  362. transition-all
  363. duration-300
  364. ${activePage === index ? "bg-black" : "bg-[#DBDBDB]!"}
  365. `}
  366. />
  367. ))}
  368. </div>
  369. )}
  370. </div>
  371. );
  372. };
  373. export default Trending;