cl-switch.uvue 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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="pt.loading?.size ?? height / 2"
  40. :color="pt.loading?.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 { useForm } from "../../hooks";
  54. import type { ClLoadingProps } from "../cl-loading/props";
  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: 24
  84. },
  85. // 宽度
  86. width: {
  87. type: Number,
  88. default: 40
  89. }
  90. });
  91. // 定义组件事件
  92. const emit = defineEmits(["update:modelValue", "change"]);
  93. // 透传样式类型定义
  94. type PassThrough = {
  95. className?: string;
  96. track?: PassThroughProps;
  97. thumb?: PassThroughProps;
  98. loading?: ClLoadingProps;
  99. };
  100. // 解析透传样式配置
  101. const pt = computed(() => parsePt<PassThrough>(props.pt));
  102. // cl-form 上下文
  103. const { disabled } = useForm();
  104. // 是否禁用
  105. const isDisabled = computed(() => props.disabled || disabled.value);
  106. // 绑定值
  107. const value = ref(props.modelValue);
  108. // 是否为选中状态
  109. const isChecked = computed(() => value.value);
  110. // 计算开关组件的尺寸样式
  111. type Rect = {
  112. height: string;
  113. width: string;
  114. size: string;
  115. left: string;
  116. translateX: string;
  117. };
  118. const rect = computed<Rect>(() => {
  119. // 获取开关轨道高度
  120. const height = props.height;
  121. // 获取开关轨道宽度
  122. const width = props.width;
  123. // 计算圆形按钮尺寸,比轨道高度小4px
  124. const size = height - 4;
  125. // 设置圆形按钮初始位置,距离左侧px
  126. const left = 2;
  127. // 计算圆形按钮移动距离,为轨道宽度减去轨道高度
  128. const translateX = width - height;
  129. return {
  130. height: height + "px",
  131. width: width + "px",
  132. size: size + "px",
  133. left: left + "px",
  134. translateX: translateX + "px"
  135. };
  136. });
  137. /**
  138. * 点击事件处理函数
  139. * 在非禁用且非加载状态下切换开关状态
  140. */
  141. function onTap() {
  142. if (!isDisabled.value && !props.loading) {
  143. // 切换开关状态
  144. const val = !value.value;
  145. value.value = val;
  146. // 触发更新事件
  147. emit("update:modelValue", val);
  148. emit("change", val);
  149. // 震动
  150. uni.$emit("cool.vibrate");
  151. }
  152. }
  153. watch(
  154. computed(() => props.modelValue),
  155. (val: boolean) => {
  156. value.value = val;
  157. }
  158. );
  159. </script>
  160. <style lang="scss" scoped>
  161. .cl-switch {
  162. @apply flex flex-row items-center duration-200 opacity-100;
  163. &__track {
  164. @apply flex flex-row items-center relative rounded-full duration-200;
  165. @apply bg-surface-200;
  166. &.is-dark {
  167. @apply bg-surface-500;
  168. }
  169. &.is-checked {
  170. @apply bg-primary-500;
  171. }
  172. }
  173. &__thumb {
  174. @apply flex items-center justify-center absolute;
  175. @apply bg-white rounded-full duration-300;
  176. }
  177. &.cl-switch--disabled {
  178. @apply opacity-50;
  179. }
  180. }
  181. </style>