Trending.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import React, { useState, useRef, useEffect } from "react";
  2. import { Swiper, SwiperSlide } from "swiper/react";
  3. import { Scrollbar, Navigation } from "swiper/modules";
  4. import "swiper/css";
  5. import "swiper/css/scrollbar";
  6. import "swiper/css/navigation";
  7. import Image from "next/image";
  8. import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
  9. // 商品数据,每个卡片携带videoUrl视频地址
  10. type ProductItem = {
  11. id: number;
  12. img: string;
  13. title: string;
  14. price: number;
  15. videoUrl: string;
  16. };
  17. const productList: ProductItem[] = [
  18. {
  19. id: 1,
  20. img: "/img/hair-video-1.jpg",
  21. title: "14-36 In Water Wave Human Hair HD Inv...",
  22. price: 146.1,
  23. videoUrl: "/video/hair-demo-1.mp4",
  24. },
  25. {
  26. id: 2,
  27. img: "/img/hair-video-2.jpg",
  28. title: "14-36 In Water Wave Human Hair HD Inv...",
  29. price: 146.1,
  30. videoUrl: "/video/hair-demo-2.mp4",
  31. },
  32. {
  33. id: 3,
  34. img: "/img/hair-video-3.jpg",
  35. title: "14-36 In Water Wave Human Hair HD Inv...",
  36. price: 146.1,
  37. videoUrl: "/video/hair-demo-3.mp4",
  38. },
  39. {
  40. id: 4,
  41. img: "/img/hair-video-4.jpg",
  42. title: "14-36 In Water Wave Human Hair HD Inv...",
  43. price: 146.1,
  44. videoUrl: "/video/hair-demo-4.mp4",
  45. },
  46. ];
  47. const Trending = () => {
  48. // 视频弹窗状态
  49. const [openVideoModal, setOpenVideoModal] = useState(false);
  50. const [currentVideoSrc, setCurrentVideoSrc] = useState("");
  51. const videoRef = useRef<HTMLVideoElement>(null);
  52. // 打开弹窗并赋值视频地址
  53. const handlePlayClick = (videoUrl: string) => {
  54. setCurrentVideoSrc(videoUrl);
  55. setOpenVideoModal(true);
  56. };
  57. // 关闭弹窗,暂停视频
  58. const closeModal = () => {
  59. setOpenVideoModal(false);
  60. if (videoRef.current) {
  61. videoRef.current.pause();
  62. videoRef.current.currentTime = 0;
  63. }
  64. };
  65. // 弹窗打开自动播放
  66. useEffect(() => {
  67. if (openVideoModal && videoRef.current) {
  68. videoRef.current.play().catch((err) => console.log("播放拦截", err));
  69. }
  70. }, [openVideoModal]);
  71. return (
  72. <div className="w-full max-w-[960px] mx-auto">
  73. <div className="w-full max-w-[960px] mx-auto px-4 py-6">
  74. {/* 顶部标题 */}
  75. <h2 className="text-[clamp(1.5rem,4vw,2.2rem)] font-bold mb-6 text-black">
  76. Real looks,Trending Now
  77. </h2>
  78. {/* Swiper外层容器:进度条深层Tailwind样式 */}
  79. <div
  80. className="
  81. [&_.custom-scrollbar]:h-[3px]
  82. [&_.custom-scrollbar]:bg-gray-200
  83. [&_.custom-scrollbar]:rounded-full
  84. [&_.swiper-scrollbar-drag]:h-full
  85. [&_.swiper-scrollbar-drag]:bg-black
  86. [&_.swiper-scrollbar-drag]:rounded-full
  87. [&_.swiper-scrollbar-drag]:shadow-none
  88. [&_.swiper-scrollbar]:bg-transparent
  89. /* 自定义左右箭头样式 匹配截图灰色圆形按钮 */
  90. [&_.swiper-button-next]:w-11
  91. [&_.swiper-button-next]:h-11
  92. [&_.swiper-button-next]:rounded-full
  93. [&_.swiper-button-next]:bg-black/40
  94. [&_.swiper-button-next]:text-white
  95. [&_.swiper-button-next]:after:text-base
  96. [&_.swiper-button-next]:after:font-bold
  97. [&_.swiper-button-prev]:w-11
  98. [&_.swiper-button-prev]:h-11
  99. [&_.swiper-button-prev]:rounded-full
  100. [&_.swiper-button-prev]:bg-black/40
  101. [&_.swiper-button-prev]:text-white
  102. [&_.swiper-button-prev]:after:text-base
  103. [&_.swiper-button-prev]:after:font-bold
  104. [&_.swiper-button-next]:right-3
  105. [&_.swiper-button-prev]:left-3
  106. "
  107. >
  108. <Swiper
  109. modules={[Scrollbar, Navigation]}
  110. spaceBetween={16}
  111. slidesPerView={1.2} // 露出右侧下一张,和截图一致
  112. navigation={{
  113. prevEl: ".btn-prev", // 左箭头class
  114. nextEl: ".btn-next", // 右箭头class
  115. disabledClass: "opacity-20", // 滑到首尾自动加这个类,Tailwind直接用
  116. }}
  117. breakpoints={{
  118. 640: { slidesPerView: 2.2 },
  119. 1024: { slidesPerView: 2.5 },
  120. }}
  121. >
  122. {productList.map((item) => (
  123. <SwiperSlide key={item.id} className="!h-auto">
  124. {/* 单个商品卡片 */}
  125. <div className=" overflow-hidden flex flex-col">
  126. {/* 图片区域 + 中间播放按钮 */}
  127. <div className="relative rounded-xl w-full aspect-[4/5] overflow-hidden rounded-xl">
  128. <img
  129. src={item.img}
  130. alt={item.title}
  131. className="w-full h-full object-cover"
  132. />
  133. {/* 居中圆形播放按钮 */}
  134. <button
  135. onClick={() => handlePlayClick(item.videoUrl)}
  136. className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
  137. w-24 h-24 rounded-full bg-white/60 backdrop-blur-sm
  138. flex items-center justify-center transition hover:bg-white/80"
  139. >
  140. <svg
  141. width="36"
  142. height="36"
  143. viewBox="0 0 24 24"
  144. fill="none"
  145. stroke="#222"
  146. strokeWidth="2"
  147. >
  148. <polygon points="5 3 19 12 5 21 5 3" />
  149. </svg>
  150. </button>
  151. </div>
  152. {/* 底部文字价格区域 */}
  153. <div className="p-3 flex flex-col rounded-xl mt-3 bg-[#f0f8f5] gap-3">
  154. <p className="text-base text-gray-900 truncate">
  155. {item.title}
  156. </p>
  157. {/* 价格 + 购物车图标 */}
  158. <div className="flex items-center justify-between">
  159. <div className="flex items-center gap-1">
  160. <span className="font-semibold">${item.price}</span>
  161. <span className="text-xs text-gray-500 line-through">
  162. ${item.price}
  163. </span>
  164. </div>
  165. {/* 购物车图标 */}
  166. <OpenAddToCartModalButton
  167. productUrlKey={
  168. "http://nshop.test/hd-transparent-deep-wave-&-curly-lace-frontal-closure-wig"
  169. }
  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内部,同级slide */}
  236. <button className="btn-prev absolute left-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
  237. {/* 自定义左箭头SVG */}
  238. <svg
  239. width="16"
  240. height="16"
  241. viewBox="0 0 24 24"
  242. fill="none"
  243. stroke="#fff"
  244. stroke-width="2"
  245. >
  246. <path d="M15 18l-6-6 6-6" />
  247. </svg>
  248. </button>
  249. <button className="btn-next absolute right-2 top-1/2 -translate-y-1/2 z-10 w-9 h-9 rounded-full bg-black/50 shadow flex items-center justify-center">
  250. {/* 自定义右箭头SVG */}
  251. <svg
  252. width="16"
  253. height="16"
  254. viewBox="0 0 24 24"
  255. fill="none"
  256. stroke="#fff"
  257. stroke-width="2"
  258. >
  259. <path d="M9 18l6-6-6-6" />
  260. </svg>
  261. </button>
  262. </Swiper>
  263. </div>
  264. {/* ====================== 全屏视频遮罩弹窗 ====================== */}
  265. {openVideoModal && (
  266. <div
  267. className="fixed inset-0 z-[9999] bg-black/95 flex items-center justify-center p-4"
  268. onClick={closeModal} // 点击遮罩空白关闭
  269. >
  270. {/* 弹窗内层,阻止冒泡 */}
  271. <div
  272. className="relative w-full max-w-4xl"
  273. onClick={(e) => e.stopPropagation()}
  274. >
  275. {/* 右上角关闭按钮 */}
  276. <button
  277. onClick={closeModal}
  278. className="absolute -top-12 right-0 text-white text-3xl hover:text-gray-300"
  279. >
  280. </button>
  281. {/* 视频播放器 */}
  282. <video
  283. ref={videoRef}
  284. src={currentVideoSrc}
  285. controls
  286. className="w-full rounded-lg"
  287. />
  288. </div>
  289. </div>
  290. )}
  291. </div>
  292. <div className="relative w-full aspect-[3/4] mt-8">
  293. <Image
  294. src={
  295. "https://cdn.asteriahair.com/uploads/202607/01/20260701165110_3fff435adf276b89.jpg"
  296. }
  297. alt={"cscscs"}
  298. fill
  299. className="object-cover rounded-md"
  300. />
  301. </div>
  302. </div>
  303. );
  304. };
  305. export default Trending;