cl-switch.uvue 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <template>
  2. <view
  3. class="cl-switch"
  4. :class="[
  5. {
  6. 'cl-switch--disabled': isDisabled,
  7. 'cl-switch--checked': isChecked
  8. },
  9. pt.className
  10. ]"
  11. @tap="onTap"
  12. >
  13. <view
  14. class="cl-switch__track"
  15. :class="[
  16. {
  17. 'is-checked': isChecked,
  18. 'is-dark': isDark
  19. },
  20. pt.track?.className
  21. ]"
  22. :style="{
  23. height: rect.height,
  24. width: rect.width
  25. }"
  26. >
  27. <view
  28. class="cl-switch__thumb"
  29. :class="[pt.thumb?.className]"
  30. :style="{
  31. height: rect.size,
  32. width: rect.size,
  33. left: rect.left,
  34. transform: `translateX(${isChecked ? rect.translateX : 0})`
  35. }"
  36. >
  37. <cl-loading
  38. v-if="loading"
  39. :size="24"
  40. color="primary"
  41. :pt="{
  42. className: parseClass([pt.loading?.className])
  43. }"
  44. ></cl-loading>
  45. </view>
  46. </view>
  47. </view>
  48. </template>
  49. <script lang="ts" setup>
  50. import { computed, ref, watch } from "vue";
  51. import { isDark, parseClass, parsePt } from "@/cool";
  52. import type { PassThroughProps } from "../../types";
  53. import { vibrate } from "@/uni_modules/cool-vibrate";
  54. import { useForm } from "../../hooks";
  55. defineOptions({
  56. name: "cl-switch"
  57. });
  58. // 定义组件属性
  59. const props = defineProps({
  60. // 透传样式配置
  61. pt: {
  62. type: Object,
  63. default: () => ({})
  64. },
  65. // 绑定值 - 开关状态
  66. modelValue: {
  67. type: Boolean,
  68. default: false
  69. },
  70. // 是否禁用
  71. disabled: {
  72. type: Boolean,
  73. default: false
  74. },
  75. // 加载状态
  76. loading: {
  77. type: Boolean,
  78. default: false
  79. },
  80. // 高度
  81. height: {
  82. type: Number,
  83. default: 48
  84. },
  85. // 宽度
  86. width: {
  87. type: Number,
  88. default: 80
  89. }
  90. });
  91. // 定义组件事件
  92. const emit = defineEmits(["update:modelValue", "change"]);
  93. // 透传样式类型定义
  94. type PassThrough = {
  95. className?: string;
  96. track?: PassThroughProps;
  97. thumb?: PassThroughProps;
  98. label?: PassThroughProps;
  99. loading?: PassThroughProps;
  100. };
  101. // 解析透传样式配置
  102. const pt = computed(() => parsePt<PassThrough>(props.pt));
  103. // cl-form 上下文
  104. const { disabled } = useForm();
  105. // 是否禁用
  106. const isDisabled = computed(() => props.disabled || disabled.value);
  107. // 绑定值
  108. const value = ref(props.modelValue);
  109. // 是否为选中状态
  110. const isChecked = computed(() => value.value);
  111. // 计算开关组件的尺寸样式
  112. type Rect = {
  113. height: string;
  114. width: string;
  115. size: string;
  116. left: string;
  117. translateX: string;
  118. };
  119. const rect = computed<Rect>(() => {
  120. // 获取开关轨道高度
  121. const height = props.height;
  122. // 获取开关轨道宽度
  123. const width = props.width;
  124. // 计算圆形按钮尺寸,比轨道高度小8rpx
  125. const size = height - 8;
  126. // 设置圆形按钮初始位置,距离左侧px
  127. const left = 4;
  128. // 计算圆形按钮移动距离,为轨道宽度减去轨道高度
  129. const translateX = width - height;
  130. return {
  131. height: height + "rpx",
  132. width: width + "rpx",
  133. size: size + "rpx",
  134. left: left + "rpx",
  135. translateX: translateX + "rpx"
  136. };
  137. });
  138. /**
  139. * 点击事件处理函数
  140. * 在非禁用且非加载状态下切换开关状态
  141. */
  142. function onTap() {
  143. if (!isDisabled.value && !props.loading) {
  144. // 切换开关状态
  145. const val = !value.value;
  146. value.value = val;
  147. // 触发更新事件
  148. emit("update:modelValue", val);
  149. emit("change", val);
  150. // 震动
  151. vibrate(1);
  152. }
  153. }
  154. watch(
  155. computed(() => props.modelValue),
  156. (val: boolean) => {
  157. value.value = val;
  158. }
  159. );
  160. </script>
  161. <style lang="scss" scoped>
  162. .cl-switch {
  163. @apply flex flex-row items-center duration-200;
  164. &__track {
  165. @apply flex flex-row items-center relative rounded-full duration-200;
  166. @apply bg-surface-200;
  167. &.is-dark {
  168. @apply bg-surface-500;
  169. }
  170. &.is-checked {
  171. @apply bg-primary-500;
  172. }
  173. }
  174. &__thumb {
  175. @apply flex items-center justify-center absolute;
  176. @apply bg-white rounded-full duration-300;
  177. }
  178. &.cl-switch--disabled {
  179. @apply opacity-50;
  180. }
  181. }
  182. </style>