Slider.vue 2.94 KB
<script setup>
/**
 * Slider —— 受控滑块(v-model)。用于"拖到某个目标值"这类连续取值,
 * 离散几选一仍然用 Select / SegmentedControl。
 *
 * 右侧的数值由页面用 `#value` 插槽给:滑块只认数字,"320 名 / 70%" 这种带单位和
 * 换算的措辞归页面,`@drama/ui` 不认识业务也不认识 i18n(和 Pagination 的 summary 同一个道理)。
 */

defineProps({
  /** 受控值。 */
  modelValue: { type: Number, required: true },
  min: { type: Number, default: 0 },
  max: { type: Number, default: 100 },
  step: { type: Number, default: 1 },
  disabled: { type: Boolean, default: false },
})

/**
 * 两个事件是分工的:拖动过程中一路发 `update:modelValue`(标签跟着走),
 * 松手才发一次 `change`。要发请求的调用方监听 `change` —— 挂在 `update:modelValue` 上
 * 会让拖一次滑块打出几十个请求。
 */
const emit = defineEmits(['update:modelValue', 'change'])

/**
 * @param {Event} e
 * @returns {void}
 */
function onInput(e) {
  emit('update:modelValue', Number(/** @type {HTMLInputElement} */ (e.target).value))
}

/**
 * @param {Event} e
 * @returns {void}
 */
function onChange(e) {
  emit('change', Number(/** @type {HTMLInputElement} */ (e.target).value))
}
</script>

<template>
  <div class="drama-slider">
    <input
      class="drama-slider__input"
      type="range"
      :min="min"
      :max="max"
      :step="step"
      :value="modelValue"
      :disabled="disabled"
      @input="onInput"
      @change="onChange"
    >
    <span
      v-if="$slots.value"
      class="drama-slider__value"
    ><slot name="value" /></span>
  </div>
</template>

<style scoped>
.drama-slider {
  display: flex;
  align-items: center;
  gap: var(--drama-space-md);
  height: var(--drama-control-height-sm);
}

.drama-slider__input {
  flex: 1;
  min-width: 96px;
  appearance: none;
  height: 4px;
  border-radius: 2px;
  background: var(--drama-color-border);
  outline: none;
  cursor: pointer;
}

.drama-slider__input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* 两套厂商前缀写不进同一条规则,只能各写一遍。 */
.drama-slider__input::-webkit-slider-thumb {
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--drama-color-primary);
  border: 2px solid var(--drama-color-surface);
  box-shadow: 0 0 0 1px var(--drama-color-border);
  cursor: pointer;
}

.drama-slider__input::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--drama-color-primary);
  border: 2px solid var(--drama-color-surface);
  cursor: pointer;
}

.drama-slider__input:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--drama-color-primary) 30%, transparent);
}

.drama-slider__value {
  flex-shrink: 0;
  min-width: 96px;
  font-size: var(--drama-font-size-sm);
  color: var(--drama-color-text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
</style>