Trending.tsx 12 KB

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