Swiper.svelte 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <script>
  2. import { onMount } from 'svelte';
  3. /** @typedef {import('../../index.d').Swiper} SwiperProps */
  4. /** @type {SwiperProps} */
  5. let {
  6. data = [],
  7. interval = 4,
  8. duration = 1000,
  9. autoplay = true,
  10. lazyplay = true,
  11. initActive = 0,
  12. indicatePosition = 'inner',
  13. indicateAlign = 'center',
  14. indicateStyle = 'pointLine',
  15. indicateRadius = true,
  16. indicateInjClass = '',
  17. indicateColor = '',
  18. indicateActiveColor = '',
  19. aspectRatio = [16, 9],
  20. containerWidth = 0,
  21. px = '0',
  22. py = '0',
  23. translateX = 0,
  24. translateZ = 0,
  25. rotateX = 0,
  26. rotateY = 0,
  27. rotateZ = 0,
  28. activeInjClass = '',
  29. notActiveInjClass = '',
  30. radius = 'none',
  31. innerInjClass = '',
  32. triggerLong = 30,
  33. notTriggerLong = 10,
  34. triggerSpeed = 0.5,
  35. onchange,
  36. onclick,
  37. } = $props();
  38. let width = containerWidth === 0 ? document.body.clientWidth : containerWidth; //宽度 width
  39. let active = $state(data.length > 1 ? initActive + 1 : 1); //当前激活的item current active item
  40. let currentIndicate = $state(data.length > 1 ? initActive : 0); //当前激活的指示器 current active indicate
  41. let longTransition = $state(true); //长线指示器过渡 long line indicate transition
  42. let long = $state(false); //长线指示器是否较长状态 long line indicate is long
  43. let once = $state(true); //是否是第首次轮播,处理轮播间隔与过渡时间的差异 is first time play, handle interval and duration difference
  44. let translateXTransition = $state(true); //是否进行过渡动画 is transition animation
  45. let initialState = true; //是否是初始状态 is initial state
  46. let startX = 0; //滑动开始X坐标 when start touch x position
  47. let moveX = $state(0); //滑动移动X坐标 when move touch x position
  48. let startTime = 0; //滑动开始时间 when start touch time
  49. let endTime = 0; //滑动结束时间 when end touch time
  50. let isMove = false; //是否滑动 is touch move
  51. // let transition = true;
  52. let swiperDom = $state(null); //Swiper容器
  53. let movePercent = $derived(moveX / width); //滑动距离占总宽度的百分比 touch width percent
  54. const dataNew =
  55. data.length > 1
  56. ? [data[data.length - 1], ...data, data[0], data[1]]
  57. : data.length === 1
  58. ? [data[data.length - 1], ...data, data[0]]
  59. : data; //实现无限轮播,复制一个新数组,防止改变原数组 implement infinite loop, copy a new array to prevent change original array
  60. const indicateAlignObj = { left: 'justify-start', center: 'justify-center', right: 'justify-end' };
  61. const pxObj = { '0': '', '1': 'px-1', '2': 'px-2', '4': 'px-4', '6': 'px-6', '8': 'px-8', '12': 'px-12', '16': 'px-16', '24': 'px-24' };
  62. const pyObj = { '0': '', '1': 'py-1', '2': 'py-2', '4': 'py-4', '6': 'py-6', '8': 'py-8', '12': 'py-12' };
  63. const radiusObj = { none: 'rounded-none', base: 'rounded-base', xl: 'rounded-xl', '2xl': 'rounded-2xl', full: 'rounded-full' };
  64. //根据 indicateStyle 和是否激活生成不同样式的 indicate
  65. //generate different style indicate according to indicateStyle and isActive
  66. const indicateStyleInnerFun = (indicateStyle, isActive, indicateColor, indicateActiveColor) => {
  67. switch (indicateStyle) {
  68. case 'point':
  69. return isActive
  70. ? `w-1.5 h-1.5 ${indicateActiveColor === '' ? 'bg-white' : indicateActiveColor}`
  71. : `w-1.5 h-1.5 ${indicateColor === '' ? 'bg-white/40' : indicateColor}`;
  72. case 'line':
  73. return isActive
  74. ? `w-6 h-1 ${indicateActiveColor === '' ? 'bg-white' : indicateActiveColor}`
  75. : `w-6 h-1 ${indicateColor === '' ? 'bg-white/40' : indicateColor}`;
  76. case 'pointLine':
  77. return isActive
  78. ? `w-6 h-1 ${indicateActiveColor === '' ? 'bg-white' : indicateActiveColor}`
  79. : `w-1 h-1 ${indicateColor === '' ? 'bg-white' : indicateColor}`;
  80. case 'longLine':
  81. return isActive
  82. ? `w-16 h-1 ${indicateColor === '' ? 'bg-white/40' : indicateColor}`
  83. : `w-1 h-1 ${indicateColor === '' ? 'bg-white/40' : indicateColor}`;
  84. default:
  85. console.log('传入的indicateStyle不在备选参数之内,将使用默认值!');
  86. return isActive
  87. ? `w-6 h-1 ${indicateActiveColor === '' ? 'bg-white' : indicateActiveColor}`
  88. : `w-1 h-1 ${indicateColor === '' ? 'bg-white' : indicateColor}`;
  89. }
  90. };
  91. const indicateStyleOutFun = (indicateStyle, isActive, indicateColor, indicateActiveColor) => {
  92. switch (indicateStyle) {
  93. case 'point':
  94. return isActive
  95. ? `w-1.5 h-1.5 ${indicateActiveColor === '' ? 'bg-primary dark:bg-dark' : indicateActiveColor}`
  96. : `w-1.5 h-1.5 ${indicateColor === '' ? 'bg-black/20 dark:bg-white/40' : indicateColor}`;
  97. case 'line':
  98. return isActive
  99. ? `w-6 h-1 ${indicateActiveColor === '' ? 'bg-primary dark:bg-dark' : indicateActiveColor}`
  100. : `w-6 h-1 ${indicateColor === '' ? 'bg-black/20 dark:bg-white/40' : indicateColor}`;
  101. case 'pointLine':
  102. return isActive
  103. ? `w-6 h-1 ${indicateActiveColor === '' ? 'bg-primary dark:bg-dark' : indicateActiveColor}`
  104. : `w-1 h-1 ${indicateColor === '' ? 'bg-primary dark:bg-dark' : indicateColor}`;
  105. case 'longLine':
  106. return isActive
  107. ? `w-16 h-1 ${indicateColor === '' ? 'bg-black/5 dark:bg-white/10' : indicateColor}`
  108. : `w-1 h-1 ${indicateColor === '' ? 'bg-black/20 dark:bg-white/40' : indicateColor}`;
  109. default:
  110. console.log('传入的indicateStyle不在备选参数之内,将使用默认值!');
  111. return isActive
  112. ? `w-6 h-1 ${indicateActiveColor === '' ? 'bg-primary dark:bg-dark' : indicateActiveColor}`
  113. : `w-1 h-1 ${indicateColor === '' ? 'bg-primary dark:bg-dark' : indicateColor}`;
  114. }
  115. };
  116. //对需要处理的偏移与旋转数值进行处理
  117. //process the offset and rotate value that need to be processed
  118. const translateXFun = (i, active, translateX) => {
  119. if (i === active) {
  120. return '0px';
  121. }
  122. if (i < active) {
  123. return `${translateX}px`;
  124. }
  125. if (i > active) {
  126. return `${-translateX}px`;
  127. }
  128. };
  129. const translateZFun = (i, active, translateZ, movePercent) =>
  130. i === active
  131. ? `${0 - Math.abs(translateZ * (isMove ? movePercent : 0))}px`
  132. : `${-translateZ + Math.abs(translateZ * (isMove ? movePercent : 0))}px`;
  133. const rotateXFun = (i, active, rotateX, movePercent) => {
  134. if (i === active) {
  135. return `${-rotateX * (isMove ? movePercent : 0)}deg`;
  136. }
  137. if (i < active) {
  138. return `${rotateX - rotateX * (isMove ? movePercent : 0)}deg`;
  139. }
  140. if (i > active) {
  141. return `${-rotateX - rotateX * (isMove ? movePercent : 0)}deg`;
  142. }
  143. };
  144. const rotateYFun = (i, active, rotateY, movePercent) => {
  145. if (i === active) {
  146. return `${-rotateY * (isMove ? movePercent : 0)}deg`;
  147. }
  148. if (i < active) {
  149. return `${rotateY - rotateY * (isMove ? movePercent : 0)}deg`;
  150. }
  151. if (i > active) {
  152. return `${-rotateY - rotateY * (isMove ? movePercent : 0)}deg`;
  153. }
  154. };
  155. const rotateZFun = (i, active, rotateZ, movePercent) => {
  156. if (i === active) {
  157. return `${-rotateZ * (isMove ? movePercent : 0)}deg`;
  158. }
  159. if (i < active) {
  160. return `${rotateZ - rotateZ * (isMove ? movePercent : 0)}deg`;
  161. }
  162. if (i > active) {
  163. return `${-rotateZ - rotateZ * (isMove ? movePercent : 0)}deg`;
  164. }
  165. };
  166. const zIndexFun = (i, active, dataNew) => {
  167. if (i === active) {
  168. return i + 2;
  169. }
  170. if (i < active) {
  171. return -(dataNew.length - i - 1) + dataNew.length;
  172. }
  173. if (i > active) {
  174. return 2 * active - i + 2;
  175. }
  176. };
  177. //定时器
  178. // timer
  179. let intervalTime = null;
  180. //定时器执行函数
  181. // timer function
  182. const intervalTimeFun = () => {
  183. intervalTime = setInterval(() => {
  184. once = false;
  185. initialState = false;
  186. active++;
  187. currentIndicate++;
  188. long = false;
  189. longTransition = false;
  190. setTimeout(() => {
  191. long = autoplay;
  192. longTransition = true;
  193. }, duration);
  194. if (active === dataNew.length - 2) {
  195. //最后一个
  196. // the last one
  197. currentIndicate = 0;
  198. setTimeout(() => {
  199. // 切换动画完成后,关闭过渡动画,同时将展示的部分偷偷替换为最开始的样子。
  200. // After the switching animation is completed, turn off the transition animation, and at the same time, the displayed part is secretly replaced with the original appearance.
  201. active = 1;
  202. // transition = false;
  203. translateXTransition = false;
  204. }, duration);
  205. } else {
  206. //开启过渡
  207. // turn on transition
  208. // transition = true;
  209. translateXTransition = !initialState;
  210. }
  211. //派发Swiper容器change事件,i表示当前容器索引值
  212. // dispatch the Swiper container change event, i indicates the current container index value
  213. onchange && onchange(currentIndicate);
  214. }, interval * 1000);
  215. };
  216. //判断Swiper容器是否在可视区域内,如果在,则开启定时器,否则不开启定时器
  217. // Determine whether the Swiper container is in the visible area. If it is, start the timer, otherwise do not start the timer
  218. const io = new IntersectionObserver(entries => {
  219. entries.forEach(item => {
  220. // isIntersecting是一个Boolean值,判断目标元素当前是否可见
  221. // isIntersecting is a Boolean value that determines whether the target element is currently visible
  222. if (item.isIntersecting) {
  223. if (autoplay) {
  224. clearInterval(intervalTime); //清除定时器 clear timer
  225. intervalTimeFun();
  226. }
  227. } else {
  228. clearInterval(intervalTime); //清除定时器 clear timer
  229. }
  230. });
  231. });
  232. onMount(() => {
  233. if (duration >= interval * 1000) {
  234. console.log('间隔时间必须大于过渡时间');
  235. return false;
  236. }
  237. // 加载完成后,通过定时器使item自动轮播。
  238. // After loading, use the timer to make the item automatically rotate.
  239. if (data.length < 2 || !autoplay) return false;
  240. long = false;
  241. longTransition = false;
  242. setTimeout(() => {
  243. long = autoplay;
  244. longTransition = true;
  245. }, 0);
  246. //懒轮播
  247. if (lazyplay) {
  248. io.observe(swiperDom);
  249. } else {
  250. if (autoplay) {
  251. clearInterval(intervalTime); //清除定时器 clear timer
  252. intervalTimeFun();
  253. }
  254. }
  255. return () => clearInterval(intervalTime); //清除定时器 clear timer
  256. });
  257. //点击事件
  258. // click event
  259. const clickImgFun = () => {
  260. //派发Swiper容器点击事件,currentIndicate表示点击的容器索引值
  261. // dispatch the Swiper container click event, currentIndicate indicates the index value of the clicked container
  262. onclick && onclick(currentIndicate);
  263. };
  264. //滑动开始
  265. // slide start
  266. const touchstartFun = e => {
  267. // 阻止默认事件
  268. // prevent default event
  269. e.preventDefault();
  270. isMove = true;
  271. startTime = new Date().getTime();
  272. translateXTransition = false;
  273. startX = e.clientX;
  274. };
  275. //滑动中
  276. // slideing
  277. const touchmoveFun = e => {
  278. if (!isMove) return false;
  279. swiperDom.setPointerCapture(e.pointerId);
  280. clearInterval(intervalTime); //清除定时器 clear timer
  281. moveX = e.clientX - startX;
  282. };
  283. //滑动结束
  284. // slide end
  285. const touchendFun = () => {
  286. endTime = new Date().getTime();
  287. const moveXABS = Math.abs(moveX); //滑动距离,moveX绝对值。 slide distance, moveX absolute value
  288. const timeLong = endTime - startTime; //滑动时间 slide time
  289. const speed = moveXABS / timeLong; //滑动速度阈值 slide speed threshold
  290. //滑动距离大于等于容器宽度 triggerLong/100 不用考虑滑动速度,都触发切换。
  291. //滑动距离小于等于容器宽度 notTriggerLong/100,无论速度快慢都不触发切换。
  292. //滑动距离大于容器宽度 notTriggerLong/100 且小于 triggerLong/100,需要判断滑动速度,用一个速度阈值来判断。阈值大于等于 triggerSpeed 切换,否则不切换。
  293. // The sliding distance is greater than or equal to the container width triggerLong/100, and the sliding speed does not need to be considered, and the switching is triggered.
  294. // The sliding distance is less than or equal to the container width notTriggerLong/100, and the switching is not triggered regardless of the speed.
  295. // The sliding distance is greater than the container width notTriggerLong/100 and less than triggerLong/100, the sliding speed needs to be judged, and a speed threshold is used to judge. The threshold is greater than or equal to triggerSpeed to switch, otherwise it will not switch.
  296. isMove = false;
  297. translateXTransition = true;
  298. long = false;
  299. longTransition = false;
  300. setTimeout(() => {
  301. long = autoplay;
  302. longTransition = true;
  303. }, duration);
  304. if (moveX <= -(width * (triggerLong / 100))) {
  305. //左滑
  306. // slide left
  307. moveX = 0;
  308. currentIndicate++;
  309. active++;
  310. if (active === dataNew.length - 2) {
  311. //最后一个
  312. // the last one
  313. currentIndicate = 0;
  314. setTimeout(() => {
  315. // 切换动画完成后,关闭过渡动画,同时将展示的部分偷偷替换为最开始的样子。
  316. // After the switching animation is completed, turn off the transition animation, and at the same time, replace the displayed part with the original appearance.
  317. active = 1;
  318. translateXTransition = false;
  319. }, duration);
  320. }
  321. } else if (moveX >= width * (triggerLong / 100)) {
  322. moveX = 0;
  323. currentIndicate--;
  324. active--;
  325. if (active === 0) {
  326. //第0个
  327. // the first one
  328. currentIndicate = dataNew.length - 4;
  329. setTimeout(() => {
  330. // 切换动画完成后,关闭过渡动画,同时将展示的部分偷偷替换为最开始的样子。
  331. // After the switching animation is completed, turn off the transition animation, and at the same time, replace the displayed part with the original appearance.
  332. active = dataNew.length - 3;
  333. translateXTransition = false;
  334. }, duration);
  335. }
  336. } else if (moveXABS > (notTriggerLong / 100) * width && moveXABS < width * (triggerLong / 100)) {
  337. if (moveX < 0 && speed >= triggerSpeed) {
  338. //左滑
  339. // slide left
  340. moveX = 0;
  341. currentIndicate++;
  342. active++;
  343. if (active === dataNew.length - 2) {
  344. //最后一个
  345. // last one
  346. currentIndicate = 0;
  347. setTimeout(() => {
  348. // 切换动画完成后,关闭过渡动画,同时将展示的部分偷偷替换为最开始的样子。
  349. // After the switching animation is completed, turn off the transition animation, and at the same time, replace the displayed part with the original appearance.
  350. active = 1;
  351. translateXTransition = false;
  352. }, duration);
  353. }
  354. } else if (moveX > 0 && speed >= triggerSpeed) {
  355. //右滑
  356. // slide right
  357. moveX = 0;
  358. currentIndicate--;
  359. active--;
  360. if (active === 0) {
  361. //第0个
  362. // the first one
  363. currentIndicate = dataNew.length - 4;
  364. setTimeout(() => {
  365. // 切换动画完成后,关闭过渡动画,同时将展示的部分偷偷替换为最开始的样子。
  366. // After the switching animation is completed, turn off the transition animation, and at the same time, replace the displayed part with the original appearance.
  367. active = dataNew.length - 3;
  368. translateXTransition = false;
  369. }, duration);
  370. }
  371. } else {
  372. moveX = 0;
  373. }
  374. } else {
  375. moveX = 0;
  376. }
  377. if (autoplay) {
  378. clearInterval(intervalTime); //清除定时器 clear timer
  379. intervalTimeFun();
  380. } else {
  381. //派发Swiper容器change事件,i表示当前容器索引值 Dispatch the Swiper container change event, i indicates the current container index value
  382. onchange && onchange(currentIndicate);
  383. }
  384. };
  385. </script>
  386. <div
  387. bind:this={swiperDom}
  388. onpointerdown={touchstartFun}
  389. onpointermove={touchmoveFun}
  390. onpointerup={touchendFun}
  391. class="touch-none cursor-move"
  392. >
  393. <!-- 轮播容器 -->
  394. <!-- Carousel container -->
  395. <div class="overflow-hidden relative" style="width:{width}px;height:{(width * aspectRatio[1]) / aspectRatio[0]}px;perspective:{width}px;">
  396. <!-- 通过循环dataNew渲染多个item -->
  397. <!-- Render multiple item through loop dataNew -->
  398. {#each dataNew as item, i}
  399. <div
  400. class="absolute {pxObj[px] || ''} {pyObj[py] || ''} {translateXTransition ? 'transition-all' : 'transition-none'} {i === active
  401. ? activeInjClass
  402. : notActiveInjClass}"
  403. style="width:{width}px;height:{(width * aspectRatio[1]) / aspectRatio[0]}px;left:{width * (i - active) +
  404. moveX}px;transition-duration: {duration}ms;z-index:{zIndexFun(i, active, dataNew)};transform:translateX({translateXFun(
  405. i,
  406. active,
  407. translateX,
  408. )}) translateZ({translateZFun(i, active, translateZ, movePercent)}) rotateX({rotateXFun(
  409. i,
  410. active,
  411. rotateX,
  412. movePercent,
  413. )}) rotateY({rotateYFun(i, active, rotateY, movePercent)}) rotateZ({rotateZFun(i, active, rotateZ, movePercent)});"
  414. >
  415. {#if item.type === 'img'}
  416. <!-- svelte-ignore a11y_click_events_have_key_events -->
  417. <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
  418. <img onclick={clickImgFun} class="object-cover w-full h-full {radiusObj[radius] || ''} {innerInjClass}" src={item.url} alt="" />
  419. {/if}
  420. {#if item.type === 'component'}
  421. <div class="w-full h-full {radiusObj[radius] || ''} {innerInjClass}">
  422. <item.component />
  423. </div>
  424. {/if}
  425. </div>
  426. {/each}
  427. <!-- 轮播指示器(内部) -->
  428. <!-- Carousel indicator (inner) -->
  429. <div
  430. class:hidden={data.length < 2 || indicatePosition === 'out' || indicatePosition === null}
  431. class="absolute bottom-0 flex pt-4 pb-2 px-4 space-x-2 w-full bg-gradient-to-b from-black/0 to-black/40 z-50 {indicateInjClass} {indicateAlignObj[
  432. indicateAlign
  433. ] || indicateAlignObj.center}"
  434. >
  435. <!-- eslint-disable-next-line no-unused-vars -->
  436. {#each data as item, i}
  437. <div
  438. class="{indicateStyleInnerFun(indicateStyle, currentIndicate === i, indicateColor, indicateActiveColor)} {indicateRadius &&
  439. 'rounded-full'} transition-all"
  440. style="transition-duration: {duration}ms;"
  441. >
  442. {#if indicateStyle === 'longLine'}
  443. <div
  444. class="absolute h-1 transition-all {long ? 'ease-linear' : ''} {indicateRadius && 'rounded-full'} {long &&
  445. currentIndicate === i
  446. ? 'w-16'
  447. : 'w-1'} {indicateActiveColor === '' ? 'bg-white' : indicateActiveColor}"
  448. style={longTransition
  449. ? `transition-duration: ${currentIndicate === 0 && once ? interval * 1000 : interval * 1000 - duration}ms`
  450. : `transition-duration:${duration}ms`}
  451. ></div>
  452. {/if}
  453. </div>
  454. {/each}
  455. </div>
  456. </div>
  457. <!-- 轮播指示器(外部) -->
  458. <!-- Carousel indicator (outer) -->
  459. <div
  460. class:hidden={data.length < 2 || indicatePosition === 'inner' || indicatePosition === null}
  461. class="flex py-3 px-4 space-x-2 w-full {indicateAlignObj[indicateAlign] || indicateAlignObj.center} {indicateInjClass}"
  462. >
  463. {#each data as item, i}
  464. <div
  465. class="{indicateStyleOutFun(indicateStyle, currentIndicate === i, indicateColor, indicateActiveColor)} {indicateRadius &&
  466. 'rounded-full'} transition-all"
  467. style="transition-duration: {duration}ms;"
  468. >
  469. {#if indicateStyle === 'longLine'}
  470. <div
  471. class="absolute h-1 transition-all {long ? 'ease-linear' : ''} {indicateRadius && 'rounded-full'} {long && currentIndicate === i
  472. ? 'w-16'
  473. : 'w-1'} {indicateActiveColor === '' ? 'bg-primary dark:bg-dark' : indicateActiveColor}"
  474. style={longTransition
  475. ? `transition-duration: ${currentIndicate === 0 && once ? interval * 1000 : interval * 1000 - duration}ms;`
  476. : `transition-duration:${duration}ms`}
  477. ></div>
  478. {/if}
  479. </div>
  480. {/each}
  481. </div>
  482. </div>