index.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. <template>
  2. <view class="slider-verify" :style="'background-color:' + sBgColor" @touchend="touchend">
  3. <view class="slider-prompt" :style="success ? 'color:' + sColor : 'color:' + dColor">
  4. <view class="arrow-group left">
  5. <text class="arrow" v-for="(item, index) in leftArrows" :key="`left-${index}`"
  6. :style="{opacity: item.opacity, transform: `scale(${item.scale})`, color: arrowColor}">›</text>
  7. </view>
  8. {{ success ? sliderText.successText : sliderText.startText }}
  9. <view class="arrow-group right">
  10. <text class="arrow" v-for="(item, index) in rightArrows" :key="`right-${index}`"
  11. :style="{opacity: item.opacity, transform: `scale(${item.scale})`, color: arrowColor}">›</text>
  12. </view>
  13. </view>
  14. <view class="slider-bg" :style="{ 'transform': 'translateX(' + oldx + 'px)', backgroundColor: dBgColor }"></view>
  15. <movable-area class="slider-area" :animation="true">
  16. <movable-view :class="{ 'movable-btn': true, 'movable-success': success }" :style="sliderStyle" :x="x"
  17. direction="horizontal" @change="onMove" :disabled="isDisable">
  18. {{ sliderText.btnText }}
  19. </movable-view>
  20. </movable-area>
  21. </view>
  22. </template>
  23. <script>
  24. export default {
  25. props: {
  26. //是否禁止拖动
  27. disabled: {
  28. type: Boolean,
  29. default: false
  30. },
  31. //偏移量
  32. offset: {
  33. type: Number,
  34. default: 3
  35. },
  36. //滑动轨道默认背景色
  37. dBgColor: {
  38. type: String,
  39. default: '#f0f0f0'
  40. },
  41. //滑动轨道滑过背景色
  42. sBgColor: {
  43. type: String,
  44. default: '#f0f0f0'
  45. },
  46. //默认文字颜色
  47. dColor: {
  48. type: String,
  49. default: '#8a8a8a'
  50. },
  51. //成功文字颜色
  52. sColor: {
  53. type: String,
  54. default: '#FFFFFF'
  55. },
  56. // 按钮文本
  57. btnType: {
  58. type: String,
  59. default: 'start'
  60. },
  61. sliderText: {
  62. type: Object,
  63. default() {
  64. return {
  65. successText: '验证通过',
  66. startText: '滑动开始服务',
  67. successColor: '#72c13f',
  68. btnText: '开始'
  69. }
  70. }
  71. },
  72. },
  73. data() {
  74. return {
  75. x: 0,
  76. oldx: 0,
  77. success: false, //是否验证成功
  78. verification: 0, //验证次数
  79. isDisable: this.disabled,
  80. screenWidth: 0,
  81. leftArrows: [
  82. { opacity: 0.4, scale: 0.8 },
  83. { opacity: 0.4, scale: 0.8 },
  84. { opacity: 0.4, scale: 0.8 }
  85. ],
  86. rightArrows: [
  87. { opacity: 0.4, scale: 0.8 },
  88. { opacity: 0.4, scale: 0.8 },
  89. { opacity: 0.4, scale: 0.8 }
  90. ],
  91. arrowAnimationTimer: null
  92. };
  93. },
  94. computed: {
  95. sliderStyle() {
  96. if (this.success) {
  97. // 成功状态下根据按钮类型显示不同颜色,然后去使用相同的渐变和阴影效果
  98. return this.btnType === 'upload'
  99. ? 'background: linear-gradient(134deg, #FFD4CB 15%, #FF6E51 79%); box-shadow: 4rpx 4rpx 8rpx 2rpx rgba(0, 0, 0, 0.25), inset 4rpx 4rpx 4rpx 4rpx rgba(193, 193, 193, 0.25);'
  100. : 'background: linear-gradient(134deg, #DAFFCB 15%, #22C55D 79%); box-shadow: 4rpx 4rpx 8rpx 2rpx rgba(0, 0, 0, 0.25), inset 4rpx 4rpx 4rpx 4rpx rgba(193, 193, 193, 0.25);';
  101. }
  102. if (this.btnType === 'upload') {
  103. return 'background: linear-gradient(134deg, #FFD4CB 15%, #FF6E51 79%); box-shadow: 4rpx 4rpx 8rpx 2rpx rgba(0, 0, 0, 0.25), inset 4rpx 4rpx 4rpx 4rpx rgba(193, 193, 193, 0.25);';
  104. } else {
  105. return 'background: linear-gradient(134deg, #DAFFCB 15%, #22C55D 79%); box-shadow: 4rpx 4rpx 8rpx 2rpx rgba(0, 0, 0, 0.25), inset 4rpx 4rpx 4rpx 4rpx rgba(193, 193, 193, 0.25);';
  106. }
  107. },
  108. arrowColor() {
  109. return this.btnType === 'upload' ? '#FF6E51' : '#22C55D';
  110. }
  111. },
  112. mounted() {
  113. const systemInfo = uni.getSystemInfoSync()
  114. this.screenWidth = systemInfo.screenWidth
  115. if (this.btnType === 'upload') {
  116. this.sliderText.btnText = '上传照片';
  117. this.sliderText.startText = '滑动结束服务';
  118. } else {
  119. this.sliderText.btnText = '开始';
  120. this.sliderText.startText = '滑动开始服务';
  121. }
  122. // 启动箭头动画
  123. this.startArrowAnimation();
  124. },
  125. methods: {
  126. onMove(e) {
  127. this.oldx = e.detail.x
  128. // 更新背景的滑动动画
  129. const percent = Math.min(e.detail.x / (this.screenWidth - 152), 1) // 152为按钮宽度
  130. if (percent > 0.9) {
  131. this.$emit('nearSuccess', true)
  132. }
  133. },
  134. touchend() {
  135. if (this.success || (this.oldx < 1 && this.oldx != 0.1)) return
  136. this.x = this.oldx
  137. var promptW = 0
  138. var onTrackW = 0
  139. uni.createSelectorQuery().in(this).select(".slider-prompt").boundingClientRect(data => {
  140. if (data.width > 0) {
  141. promptW = data.width
  142. uni.createSelectorQuery().in(this).select(".movable-btn").boundingClientRect(data => {
  143. if (data.width > 0) {
  144. onTrackW = data.width
  145. if (this.oldx != 0.1) this.verification++
  146. if (this.oldx > (promptW - onTrackW - this.offset)) {
  147. this.success = true
  148. this.isDisable = true
  149. this.verificationSuccess(true)
  150. } else {
  151. this.$nextTick(() => {
  152. this.x = 0
  153. this.oldx = 0
  154. })
  155. this.verificationSuccess(false)
  156. }
  157. }
  158. }).exec()
  159. }
  160. }).exec()
  161. },
  162. // 箭头动画
  163. startArrowAnimation() {
  164. if (this.arrowAnimationTimer) {
  165. clearInterval(this.arrowAnimationTimer);
  166. }
  167. let step = 0;
  168. this.arrowAnimationTimer = setInterval(() => {
  169. if (this.success) {
  170. clearInterval(this.arrowAnimationTimer);
  171. return;
  172. }
  173. // 更新左侧箭头动画
  174. this.leftArrows.forEach((arrow, index) => {
  175. const delay = index * 3;
  176. const phase = (step + delay) % 15;
  177. if (phase < 7) {
  178. arrow.opacity = 0.4 + (phase / 7) * 0.6;
  179. arrow.scale = 0.8 + (phase / 7) * 0.4;
  180. } else {
  181. arrow.opacity = 1 - ((phase - 7) / 8) * 0.6;
  182. arrow.scale = 1.2 - ((phase - 7) / 8) * 0.4;
  183. }
  184. });
  185. // 更新右侧箭头动画
  186. this.rightArrows.forEach((arrow, index) => {
  187. const delay = (2 - index) * 3;
  188. const phase = (step + delay) % 15;
  189. if (phase < 7) {
  190. arrow.opacity = 0.4 + (phase / 7) * 0.6;
  191. arrow.scale = 0.8 + (phase / 7) * 0.4;
  192. } else {
  193. arrow.opacity = 1 - ((phase - 7) / 8) * 0.6;
  194. arrow.scale = 1.2 - ((phase - 7) / 8) * 0.4;
  195. }
  196. });
  197. step = (step + 1) % 15;
  198. }, 100);
  199. },
  200. verificationSuccess(state) {
  201. let obj = {
  202. state: state,
  203. verification: this.verification
  204. }
  205. this.$emit("change", obj)
  206. if (state) {
  207. // 成功后停止动画
  208. if (this.arrowAnimationTimer) {
  209. clearInterval(this.arrowAnimationTimer);
  210. this.arrowAnimationTimer = null;
  211. }
  212. }
  213. },
  214. //重置初始化状态
  215. initialization() {
  216. this.x = 0
  217. this.oldx = 0.1
  218. this.verification = 0
  219. this.success = false
  220. this.isDisable = false
  221. this.startArrowAnimation(); // 重新启动动画
  222. this.touchend()
  223. }
  224. }
  225. }
  226. </script>
  227. <style scoped>
  228. .slider-verify {
  229. position: relative;
  230. width: 100%;
  231. height: 100rpx;
  232. overflow: hidden;
  233. border-radius: 50rpx;
  234. }
  235. .slider-prompt {
  236. width: 100%;
  237. height: 100%;
  238. position: absolute;
  239. left: 0;
  240. top: 0;
  241. display: flex;
  242. align-items: center;
  243. justify-content: center;
  244. font-size: 32rpx;
  245. z-index: 99;
  246. }
  247. .slider-bg {
  248. width: 100%;
  249. height: 100%;
  250. position: absolute;
  251. top: 0;
  252. left: 0;
  253. transition: transform 0.3s ease-out;
  254. }
  255. .slider-area {
  256. position: absolute;
  257. top: 0;
  258. left: 0;
  259. height: 100%;
  260. width: 100%;
  261. z-index: 999;
  262. }
  263. .movable-btn {
  264. width: 152rpx;
  265. height: 100rpx;
  266. border: none;
  267. display: flex;
  268. align-items: center;
  269. justify-content: center;
  270. font-size: 32rpx;
  271. color: #fff;
  272. font-weight: bold;
  273. border-radius: 50rpx;
  274. transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
  275. animation: pulse 2s infinite;
  276. }
  277. .movable-success {
  278. transition: all 0.3s ease;
  279. animation: none;
  280. }
  281. .arrow-group {
  282. display: flex;
  283. align-items: center;
  284. }
  285. .arrow-group.left {
  286. margin-right: 24rpx;
  287. }
  288. .arrow-group.right {
  289. margin-left: 24rpx;
  290. }
  291. .arrow {
  292. font-size: 32rpx;
  293. margin: 0 4rpx;
  294. font-weight: bold;
  295. transition: all 0.1s ease;
  296. }
  297. .arrow-group.left .arrow-img:nth-child(1) {
  298. animation-delay: 0s;
  299. }
  300. .arrow-group.left .arrow-img:nth-child(2) {
  301. animation-delay: 0.2s;
  302. }
  303. .arrow-group.left .arrow-img:nth-child(3) {
  304. animation-delay: 0.4s;
  305. }
  306. .arrow-group.right .arrow-img:nth-child(1) {
  307. animation-delay: 0.4s;
  308. }
  309. .arrow-group.right .arrow-img:nth-child(2) {
  310. animation-delay: 0.2s;
  311. }
  312. .arrow-group.right .arrow-img:nth-child(3) {
  313. animation-delay: 0s;
  314. }
  315. @keyframes breathe {
  316. 0% {
  317. opacity: 0.4;
  318. transform: scale(0.8) translateX(0);
  319. }
  320. 50% {
  321. opacity: 1;
  322. transform: scale(1.2) translateX(0);
  323. }
  324. 100% {
  325. opacity: 0.4;
  326. transform: scale(0.8) translateX(0);
  327. }
  328. }
  329. @keyframes pulse {
  330. 0% {
  331. box-shadow: 0 0 0 0 rgba(34, 197, 93, 0.4);
  332. }
  333. 70% {
  334. box-shadow: 0 0 0 10rpx rgba(34, 197, 93, 0);
  335. }
  336. 100% {
  337. box-shadow: 0 0 0 0 rgba(34, 197, 93, 0);
  338. }
  339. }
  340. .go-right {
  341. display: none;
  342. }
  343. </style>