Slider.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <template>
  2. <view
  3. class="nana-slider"
  4. :id="id"
  5. :style="{
  6. opacity: disabled ? 0.6 : 1,
  7. width: direction == 'horizontal' ? undefined : themeContext.resolveSize(dotSize),
  8. height: direction == 'vertical' ? undefined : themeContext.resolveSize(dotSize),
  9. minWidth: themeContext.resolveSize(dotSize),
  10. minHeight: themeContext.resolveSize(dotSize),
  11. ...innerStyle,
  12. }"
  13. @touchstart="handleTouchStart"
  14. @touchmove="handleTouchMove"
  15. @touchend="handleTouchEnd"
  16. @touchcancel="handleTouchEnd"
  17. @mousedown="handleTouchStart"
  18. @mousemove="handleTouchMove"
  19. @mouseup="handleTouchEnd"
  20. >
  21. <view
  22. class="dot"
  23. :style="{
  24. ...themeStyles.toggleDot.value,
  25. width: themeContext.resolveSize(dotSize),
  26. height: themeContext.resolveSize(dotSize),
  27. borderRadius: themeContext.resolveSize(dotSize / 2),
  28. backgroundColor: themeContext.resolveThemeColor(dotColor),
  29. left: direction == 'horizontal' ? `calc(${valuePrecent}% - ${themeContext.resolveSize(dotSize / 2)})` : undefined ,
  30. top: direction == 'vertical' ? `calc(${valuePrecent}% - ${themeContext.resolveSize(dotSize / 2)})` : undefined,
  31. ...dotStyle,
  32. }"
  33. />
  34. <view class="bar" :style="{
  35. backgroundColor: themeContext.resolveThemeColor(inactiveColor),
  36. width: direction == 'horizontal' ? '100%' : themeContext.resolveThemeSize(size),
  37. height: direction == 'vertical' ? '100%' : themeContext.resolveThemeSize(size),
  38. }">
  39. <view
  40. class="active"
  41. :style="{
  42. backgroundColor: themeContext.resolveThemeColor(activeColor),
  43. width: direction == 'horizontal' ? `${valuePrecent}%` : themeContext.resolveThemeSize(size),
  44. height: direction == 'vertical' ? `${valuePrecent}%` : themeContext.resolveThemeSize(size),
  45. }"
  46. />
  47. </view>
  48. </view>
  49. </template>
  50. <script setup lang="ts">
  51. import { computed, getCurrentInstance, onMounted, toRef } from 'vue';
  52. import { propGetThemeVar, useTheme, type ViewStyle } from '../theme/ThemeDefine';
  53. import { DynamicVar } from '../theme/ThemeTools';
  54. import { useFieldChildValueInjector } from './FormContext';
  55. import { RandomUtils } from '@imengyu/imengyu-utils';
  56. export interface SliderProps {
  57. /**
  58. * 数值
  59. * @default 50
  60. */
  61. modelValue?: number;
  62. /**
  63. * 滑动方向
  64. * @default horizontal
  65. */
  66. direction?: 'horizontal' | 'vertical';
  67. /**
  68. * 主颜色
  69. * @default primary
  70. */
  71. activeColor?: string;
  72. /**
  73. * 反色
  74. * @default grey
  75. */
  76. inactiveColor?: string;
  77. /**
  78. * 条的大小
  79. * @default 6
  80. */
  81. size?: string|number;
  82. /**
  83. * 开关点的颜色
  84. * @default lightButton
  85. */
  86. dotColor?: string;
  87. /**
  88. * 开关大小
  89. * @default 40
  90. */
  91. dotSize?: number;
  92. /**
  93. * 点的自定义样式
  94. */
  95. dotStyle?: ViewStyle;
  96. /**
  97. * 是否禁用
  98. * @default false
  99. */
  100. disabled?: boolean;
  101. /**
  102. * 最小值
  103. * @default 0
  104. */
  105. min?: number;
  106. /**
  107. * 最大值
  108. * @default 100
  109. */
  110. max?: number;
  111. /**
  112. * 步长
  113. */
  114. step?: number;
  115. /**
  116. * 内部样式
  117. */
  118. innerStyle?: ViewStyle,
  119. }
  120. const emit = defineEmits([ 'update:modelValue' ])
  121. const props = withDefaults(defineProps<SliderProps>(), {
  122. modelValue: 50,
  123. activeColor: () => propGetThemeVar('SliderActiveColor', 'primary'),
  124. inactiveColor: () => propGetThemeVar('SliderInactiveColor', 'background.switch'),
  125. dotColor: () => propGetThemeVar('SliderDotColor', 'lightButton'),
  126. dotSize: () => propGetThemeVar('SliderDotSize', 40),
  127. disabled: false,
  128. direction: 'horizontal',
  129. min: 0,
  130. max: 100,
  131. size: () => propGetThemeVar('SliderSize', 6),
  132. });
  133. const themeContext = useTheme();
  134. const themeStyles = themeContext.useThemeStyles({
  135. toggleDot: {
  136. boxShadow: DynamicVar('SliderDotShadow', 'rgba(0, 0, 0, 0.1) 2px 2px 8px 2px'),
  137. },
  138. });
  139. const valuePrecent = computed(() => {
  140. return (value.value - props.min) / (props.max - props.min) * 100
  141. });
  142. const {
  143. value,
  144. updateValue,
  145. context,
  146. } = useFieldChildValueInjector(
  147. toRef(props, 'modelValue'),
  148. (v) => emit('update:modelValue', v)
  149. );
  150. const instance = getCurrentInstance();
  151. const id = RandomUtils.genNonDuplicateID(12);
  152. let absLeft = 0;
  153. let absWidth = 0;
  154. let pressed = false;
  155. function doUpdateValue(v: number) {
  156. v = Math.max(props.min, Math.min(props.max, v));
  157. if (props.step)
  158. v = Math.round(v / props.step) * props.step;
  159. updateValue(v);
  160. }
  161. function handleDrag(x: number) {
  162. let v = (x - absLeft) / absWidth;
  163. v = v * (props.max - props.min) + props.min;
  164. if (v !== value.value)
  165. doUpdateValue(v);
  166. }
  167. function handleTouchStart(e: any) {
  168. if (props.disabled)
  169. return;
  170. e.stopPropagation();
  171. const query = uni.createSelectorQuery();
  172. query
  173. // #ifdef MP
  174. .in(instance)
  175. // #endif
  176. .select('#' + id)
  177. .boundingClientRect((res) => {
  178. if (res) {
  179. absLeft = (res as any).left;
  180. absWidth = (res as any).width;
  181. pressed = true;
  182. }
  183. handleDrag(e.touches[0]?.clientX);
  184. }).exec();
  185. }
  186. function handleTouchMove(e: any) {
  187. if (props.disabled)
  188. return;
  189. if (!pressed)
  190. return;
  191. e.stopPropagation();
  192. handleDrag(e.touches[0]?.clientX);
  193. }
  194. function handleTouchEnd() {
  195. pressed = false;
  196. }
  197. defineOptions({
  198. options: {
  199. styleIsolation: "shared",
  200. virtualHost: true,
  201. }
  202. })
  203. </script>
  204. <style lang="scss">
  205. .nana-slider {
  206. display: flex;
  207. flex-direction: row;
  208. justify-content: center;
  209. align-items: center;
  210. position: relative;
  211. overflow: visible;
  212. flex: 1;
  213. .bar {
  214. display: flex;
  215. position: relative;
  216. overflow: hidden;
  217. .active {
  218. position: absolute;
  219. }
  220. }
  221. .dot {
  222. display: flex;
  223. position: absolute;
  224. z-index: 10,
  225. }
  226. }
  227. </style>