Button.vue 3.75 KB
<script setup>
/**
 * Button —— 通用按钮,与业务无关,只认 props/slot。
 *
 * @typedef {'primary'|'secondary'|'ghost'|'danger'} ButtonVariant
 * @typedef {'sm'|'md'|'lg'} ButtonSize
 * @typedef {'button'|'submit'|'reset'} ButtonType
 */

const props = defineProps({
  /** @type {import('vue').PropType<ButtonVariant>} 视觉变体。 */
  variant: { type: String, default: 'primary' },
  /** @type {import('vue').PropType<ButtonSize>} 尺寸。 */
  size: { type: String, default: 'md' },
  /** 禁用态:阻断点击且降透明度。 */
  disabled: { type: Boolean, default: false },
  /** 加载态:显示 spinner、阻断点击。 */
  loading: { type: Boolean, default: false },
  /** @type {import('vue').PropType<ButtonType>} 原生按钮类型。 */
  type: { type: String, default: 'button' },
})

const emit = defineEmits(['click'])

/**
 * 转发原生点击;disabled / loading 时吞掉事件。
 * @param {MouseEvent} e
 */
function onClick(e) {
  if (props.disabled || props.loading) return
  emit('click', e)
}
</script>

<template>
  <button
    class="drama-btn"
    :class="[`drama-btn--${variant}`, `drama-btn--${size}`, { 'is-loading': loading }]"
    :disabled="disabled || loading"
    :aria-busy="loading ? 'true' : 'false'"
    :type="type"
    @click="onClick"
  >
    <span
      v-if="loading"
      class="drama-btn__spinner"
      aria-hidden="true"
    />
    <span class="drama-btn__label"><slot /></span>
  </button>
</template>

<style scoped>
.drama-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--drama-space-sm);
  border: 1px solid transparent;
  border-radius: var(--drama-radius-md);
  font-family: var(--drama-font-family-body);
  font-weight: var(--drama-font-weight-medium);
  cursor: pointer;
  transition: background-color 0.15s ease, opacity 0.15s ease;
}

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

/* min-height 取 control token:按钮和同尺寸的 Input / Select 等高,横排在一起底边成一条线。
   仍留纵向 padding —— 文案折行时不至于贴边。 */
.drama-btn--sm {
  min-height: var(--drama-control-height-sm);
  padding: var(--drama-space-xs) var(--drama-space-md);
  font-size: var(--drama-font-size-sm);
}

.drama-btn--md {
  min-height: var(--drama-control-height-md);
  padding: var(--drama-space-sm) var(--drama-space-lg);
  font-size: var(--drama-font-size-md);
}

.drama-btn--lg {
  min-height: var(--drama-control-height-lg);
  padding: var(--drama-space-md) var(--drama-space-xl);
  font-size: var(--drama-font-size-lg);
}

.drama-btn--primary {
  background-color: var(--drama-color-primary);
  color: var(--drama-color-on-primary);
}

.drama-btn--primary:hover:not(:disabled) {
  background-color: var(--drama-color-primary-hover);
}

.drama-btn--secondary {
  background-color: var(--drama-color-surface-alt);
  color: var(--drama-color-text);
  border-color: var(--drama-color-border);
}

.drama-btn--ghost {
  background-color: transparent;
  color: var(--drama-color-text);
}

.drama-btn--ghost:hover:not(:disabled) {
  background-color: var(--drama-color-surface-alt);
}

.drama-btn--danger {
  background-color: var(--drama-color-danger);
  color: var(--drama-color-on-primary);
}

.drama-btn--danger:hover:not(:disabled) {
  background-color: var(--drama-color-danger-hover);
}

.drama-btn__spinner {
  width: var(--drama-space-lg);
  height: var(--drama-space-lg);
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--drama-radius-full);
  animation: drama-btn-spin 0.6s linear infinite;
}

.drama-btn__label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--drama-space-sm);
  line-height: 1;
}

@keyframes drama-btn-spin {
  to {
    transform: rotate(360deg);
  }
}
</style>