async-switch.vue 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <template>
  2. <view class="async-switch"
  3. :class="{
  4. 'async-switch--active': value,
  5. 'async-switch--disabled': disabled,
  6. 'async-switch--loading': innerLoading
  7. }"
  8. @click="handleClick"
  9. >
  10. <!-- 开关轨道 -->
  11. <view class="async-switch__track"></view>
  12. <!-- 开关按钮 -->
  13. <view class="async-switch__thumb"></view>
  14. <!-- 加载状态遮罩 -->
  15. <view v-if="innerLoading" class="async-switch__loading-mask">
  16. <u-loading mode="circle"></u-loading>
  17. </view>
  18. </view>
  19. </template>
  20. <script>
  21. export default {
  22. name: 'AsyncSwitch',
  23. props: {
  24. // 开关当前状态(双向绑定)
  25. value: {
  26. type: Boolean,
  27. default: false
  28. },
  29. // 是否禁用
  30. disabled: {
  31. type: Boolean,
  32. default: false
  33. },
  34. // 加载状态(外部可控制)
  35. loading: {
  36. type: Boolean,
  37. default: false
  38. },
  39. // 开关激活色
  40. activeColor: {
  41. type: String,
  42. default: '#016BF6'
  43. },
  44. // 开关未激活色
  45. inactiveColor: {
  46. type: String,
  47. default: '#ffffff'
  48. },
  49. // 异步变更处理函数(必须返回Promise)
  50. asyncChange: {
  51. type: Function,
  52. required: true
  53. }
  54. },
  55. data() {
  56. return {
  57. // 内部加载状态(避免外部直接覆盖)
  58. innerLoading: this.loading
  59. };
  60. },
  61. watch: {
  62. // 监听外部传入的loading属性变化,同步到内部状态
  63. loading(newVal) {
  64. this.innerLoading = newVal;
  65. }
  66. },
  67. methods: {
  68. /**
  69. * 处理开关点击事件(Vue2 异步方法写法)
  70. */
  71. async handleClick() {
  72. // 禁用/加载中时不响应点击
  73. if (this.disabled || this.innerLoading) return;
  74. // 目标状态(当前状态取反)
  75. const targetValue = !this.value;
  76. try {
  77. // 开启加载状态
  78. this.innerLoading = true;
  79. // 执行外部传入的异步操作
  80. const result = await this.asyncChange(targetValue);
  81. // 异步操作成功且返回值不为false时,更新开关状态
  82. if (result !== false) {
  83. // Vue2 触发自定义事件,实现v-model双向绑定
  84. this.$emit('input', targetValue); // v-model 底层依赖input事件
  85. this.$emit('change', targetValue); // 额外的change回调
  86. }
  87. } catch (error) {
  88. // 异步操作失败,触发error事件
  89. this.$emit('error', error);
  90. console.error('Switch异步更新失败:', error);
  91. } finally {
  92. // 无论成功/失败,关闭加载状态
  93. this.innerLoading = false;
  94. }
  95. }
  96. }
  97. };
  98. </script>
  99. <style scoped>
  100. .async-switch {
  101. position: relative;
  102. width: 52px;
  103. height: 32px;
  104. border-radius: 16px;
  105. background-color: #ffffff;
  106. transition: background-color 0.3s ease;
  107. cursor: pointer;
  108. box-sizing: border-box;
  109. border: 1px solid #eee;
  110. }
  111. /* 激活状态 */
  112. .async-switch--active {
  113. background-color: #016BF6;
  114. }
  115. /* 禁用状态 */
  116. .async-switch--disabled {
  117. opacity: 0.5;
  118. cursor: not-allowed;
  119. }
  120. /* 加载状态 */
  121. .async-switch--loading {
  122. opacity: 0.8;
  123. cursor: wait;
  124. }
  125. .async-switch__track {
  126. width: 100%;
  127. height: 100%;
  128. border-radius: 16px;
  129. }
  130. .async-switch__thumb {
  131. position: absolute;
  132. top: 1px;
  133. left: 1px;
  134. width: 28px;
  135. height: 28px;
  136. border-radius: 50%;
  137. background-color: #fff;
  138. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  139. transition: left 0.3s ease;
  140. }
  141. .async-switch--active .async-switch__thumb {
  142. left: 22px;
  143. }
  144. /* 加载遮罩 */
  145. .async-switch__loading-mask {
  146. position: absolute;
  147. top: 0;
  148. left: 0;
  149. width: 100%;
  150. height: 100%;
  151. border-radius: 16px;
  152. background-color: rgba(0, 0, 0, 0.15);
  153. display: flex;
  154. align-items: center;
  155. justify-content: center;
  156. z-index: 10;
  157. }
  158. </style>