cwg-wiper.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. <template>
  2. <view class="swiper-container">
  3. <swiper ref="swiperRef" :indicator-dots="indicatorDots" :autoplay="autoplay" :interval="interval"
  4. :duration="duration" :circular="circular" :current="currentIndex" @change="onSwiperChange"
  5. @animationfinish="onAnimationFinish" :previous-margin="previousMargin" :next-margin="nextMargin"
  6. class="custom-swiper" :style="{ height: height }">
  7. <swiper-item v-for="(item, index) in list" :key="index">
  8. <view class="swiper-item">
  9. <!-- 图片内容 -->
  10. <image v-if="item.image" :src="item.image" mode="aspectFill" class="swiper-image"
  11. @click="onItemClick(index)" />
  12. <!-- 自定义内容插槽 -->
  13. <slot v-else :item="item" :index="index">
  14. <view class="default-content">{{ item.content || `内容 ${index + 1}` }}</view>
  15. </slot>
  16. </view>
  17. </swiper-item>
  18. </swiper>
  19. <!-- 自定义指示器 -->
  20. <view v-if="showDots" class="swiper-dots">
  21. <view v-for="(item, index) in list" :key="index" class="dot"
  22. :class="{ 'dot-active': currentIndex === index }" @click="goToSlide(index)"></view>
  23. </view>
  24. <!-- 滚动条 -->
  25. <view v-if="showScrollbar" class="swiper-scrollbar">
  26. <view class="swiper-scrollbar-drag" :style="{
  27. width: scrollbarWidth,
  28. transform: `translateX(${scrollbarOffset})`
  29. }"></view>
  30. </view>
  31. <!-- 导航按钮 -->
  32. <view v-if="showNavigation" class="swiper-navigation">
  33. <view class="swiper-button-prev" :class="{ 'swiper-button-disabled': currentIndex === 0 && !circular }"
  34. @click="swipePrev">
  35. <svg class="swiper-navigation-icon" width="11" height="20" viewBox="0 0 11 20" fill="none"
  36. xmlns="http://www.w3.org/2000/svg">
  37. <path
  38. d="M10.4341 0.482966C10.7052 0.754138 10.7052 1.19379 10.4341 1.46497L1.61942 10.2796L10.4341 19.0942C10.7052 19.3654 10.7052 19.805 10.4341 20.0762C10.1629 20.3474 9.72321 20.3474 9.45204 20.0762L0.382867 11.007C-0.0188908 10.6053 -0.0188908 9.9539 0.382867 9.55214L9.45204 0.482966C9.72321 0.211794 10.1629 0.211794 10.4341 0.482966Z"
  39. fill="currentColor" />
  40. </svg>
  41. </view>
  42. <view class="swiper-button-next"
  43. :class="{ 'swiper-button-disabled': currentIndex === list.length - 1 && !circular }" @click="swipeNext">
  44. <svg class="swiper-navigation-icon" width="11" height="20" viewBox="0 0 11 20" fill="none"
  45. xmlns="http://www.w3.org/2000/svg">
  46. <path
  47. d="M0.38296 20.0762C0.111788 19.805 0.111788 19.3654 0.38296 19.0942L9.19758 10.2796L0.38296 1.46497C0.111788 1.19379 0.111788 0.754138 0.38296 0.482966C0.654131 0.211794 1.09379 0.211794 1.36496 0.482966L10.4341 9.55214C10.8359 9.9539 10.8359 10.6053 10.4341 11.007L1.36496 20.0762C1.09379 20.3474 0.654131 20.3474 0.38296 20.0762Z"
  48. fill="currentColor" />
  49. </svg>
  50. </view>
  51. </view>
  52. </view>
  53. </template>
  54. <script setup lang="ts">
  55. import { ref, computed, watch, onMounted } from 'vue'
  56. const props = defineProps({
  57. // 轮播数据
  58. list: {
  59. type: Array,
  60. default: () => []
  61. },
  62. // 当前索引
  63. modelValue: {
  64. type: Number,
  65. default: 0
  66. },
  67. // 高度
  68. height: {
  69. type: String,
  70. default: '400rpx'
  71. },
  72. // 是否自动播放
  73. autoplay: {
  74. type: Boolean,
  75. default: true
  76. },
  77. // 自动播放间隔(毫秒)
  78. interval: {
  79. type: Number,
  80. default: 3000
  81. },
  82. // 滑动动画时长(毫秒)
  83. duration: {
  84. type: Number,
  85. default: 500
  86. },
  87. // 是否采用衔接滑动
  88. circular: {
  89. type: Boolean,
  90. default: true
  91. },
  92. // 是否显示面板指示点
  93. indicatorDots: {
  94. type: Boolean,
  95. default: false
  96. },
  97. // 是否显示自定义圆点指示器
  98. showDots: {
  99. type: Boolean,
  100. default: false
  101. },
  102. // 是否显示滚动条
  103. showScrollbar: {
  104. type: Boolean,
  105. default: true
  106. },
  107. // 是否显示导航按钮
  108. showNavigation: {
  109. type: Boolean,
  110. default: true
  111. },
  112. // 前边距,可用于露出前一项的一小部分
  113. previousMargin: {
  114. type: String,
  115. default: '0px'
  116. },
  117. // 后边距,可用于露出后一项的一小部分
  118. nextMargin: {
  119. type: String,
  120. default: '0px'
  121. }
  122. })
  123. const emit = defineEmits(['update:modelValue', 'change', 'click'])
  124. // 当前索引
  125. const currentIndex = ref(props.modelValue)
  126. // 监听外部变化
  127. watch(() => props.modelValue, (val) => {
  128. currentIndex.value = val
  129. })
  130. const swiperRef = ref<any>(null)
  131. // 计算滚动条宽度
  132. const scrollbarWidth = computed(() => {
  133. const total = props.list.length
  134. if (total === 0) return '0%'
  135. return `${100 / total}%`
  136. })
  137. // 计算滚动条偏移
  138. const scrollbarOffset = computed(() => {
  139. return `${currentIndex.value * 100}%`
  140. })
  141. // 滑动变化
  142. const onSwiperChange = (e: any) => {
  143. const { current } = e.detail
  144. currentIndex.value = current
  145. emit('update:modelValue', current)
  146. emit('change', current)
  147. }
  148. // 动画完成
  149. const onAnimationFinish = (e: any) => {
  150. // 可以在这里处理动画完成后的逻辑
  151. }
  152. // 跳转到指定幻灯片
  153. const goToSlide = (index: number) => {
  154. currentIndex.value = index
  155. emit('update:modelValue', index)
  156. emit('change', index)
  157. }
  158. // 上一页
  159. const swipePrev = () => {
  160. if (currentIndex.value === 0 && !props.circular) return
  161. swiperRef.value?.swipePrev?.()
  162. const newIndex = currentIndex.value === 0 ? props.list.length - 1 : currentIndex.value - 1
  163. goToSlide(newIndex)
  164. }
  165. // 下一页
  166. const swipeNext = () => {
  167. if (currentIndex.value === props.list.length - 1 && !props.circular) return
  168. swiperRef.value?.swipeNext?.()
  169. const newIndex = currentIndex.value === props.list.length - 1 ? 0 : currentIndex.value + 1
  170. goToSlide(newIndex)
  171. }
  172. // 点击项目
  173. const onItemClick = (index: number) => {
  174. emit('click', index)
  175. }
  176. </script>
  177. <style scoped lang="scss">
  178. @import "@/uni.scss";
  179. .swiper-container {
  180. position: relative;
  181. width: 100%;
  182. overflow: hidden;
  183. }
  184. .custom-swiper {
  185. width: 100%;
  186. }
  187. .swiper-item {
  188. width: 100%;
  189. height: 100%;
  190. display: flex;
  191. align-items: center;
  192. justify-content: center;
  193. .swiper-image {
  194. width: 100%;
  195. height: 100%;
  196. object-fit: cover;
  197. }
  198. .default-content {
  199. font-size: px2rpx(24);
  200. color: var(--bs-heading-color);
  201. }
  202. }
  203. // 自定义指示点
  204. .swiper-dots {
  205. position: absolute;
  206. bottom: px2rpx(20);
  207. left: 0;
  208. right: 0;
  209. display: flex;
  210. align-items: center;
  211. justify-content: center;
  212. gap: px2rpx(16);
  213. z-index: 10;
  214. .dot {
  215. width: px2rpx(16);
  216. height: px2rpx(16);
  217. border-radius: 50%;
  218. background-color: rgba(255, 255, 255, 0.5);
  219. transition: all 0.3s;
  220. &-active {
  221. width: px2rpx(32);
  222. border-radius: px2rpx(16);
  223. background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;
  224. }
  225. }
  226. }
  227. // 滚动条
  228. .swiper-scrollbar {
  229. position: absolute;
  230. bottom: 0;
  231. left: 0;
  232. right: 0;
  233. height: px2rpx(4);
  234. background-color: rgba(0, 0, 0, 0.1);
  235. z-index: 10;
  236. .swiper-scrollbar-drag {
  237. height: 100%;
  238. background: #00000080;
  239. border-radius: px2rpx(10);
  240. transition: transform 0.3s;
  241. }
  242. }
  243. // 导航按钮
  244. .swiper-navigation {
  245. position: absolute;
  246. top: 50%;
  247. left: 0;
  248. right: 0;
  249. transform: translateY(-50%);
  250. display: flex;
  251. align-items: center;
  252. justify-content: space-between;
  253. padding: 0 px2rpx(20);
  254. z-index: 10;
  255. pointer-events: none;
  256. .swiper-button-prev,
  257. .swiper-button-next {
  258. width: px2rpx(60);
  259. height: px2rpx(60);
  260. background-color: rgba(0, 0, 0, 0.3);
  261. border-radius: 50%;
  262. display: flex;
  263. align-items: center;
  264. justify-content: center;
  265. cursor: pointer;
  266. pointer-events: auto;
  267. transition: all 0.3s;
  268. &.swiper-button-disabled {
  269. opacity: 0.3;
  270. pointer-events: none;
  271. }
  272. &:active {
  273. background-color: rgba(0, 0, 0, 0.5);
  274. }
  275. .swiper-navigation-icon {
  276. width: px2rpx(24);
  277. height: px2rpx(40);
  278. color: var(--color-white);
  279. }
  280. }
  281. }
  282. // 深色背景适配
  283. .dark-theme {
  284. .swiper-dots .dot {
  285. background-color: rgba(255, 255, 255, 0.3);
  286. &-active {
  287. background-color: #ffd700;
  288. }
  289. }
  290. .swiper-scrollbar {
  291. background-color: rgba(255, 255, 255, 0.2);
  292. .swiper-scrollbar-drag {
  293. background-color: #ffd700;
  294. }
  295. }
  296. }
  297. </style>