File size: 3,129 Bytes
395e9dc
 
 
 
 
 
 
 
 
 
 
 
1f4cfcb
395e9dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1f4cfcb
395e9dc
2204456
1f4cfcb
2204456
1f4cfcb
 
395e9dc
 
 
 
 
2204456
1f4cfcb
2204456
395e9dc
 
 
2204456
 
 
395e9dc
 
 
 
2204456
 
395e9dc
 
 
 
2204456
 
 
395e9dc
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<!--
  AtButton — 全站统一三档按钮 (F4)
  variant: primary | secondary | danger | ghost
  size:    sm | md
  disabled / loading 态完整,icon 槽位
-->
<template>
  <button
    :type="type"
    :disabled="disabled || loading"
    @click="onClick"
    :class="classes"
    class="relative inline-flex items-center justify-center gap-2 font-medium rounded-lg
           lift-hover focus-ring select-none whitespace-nowrap
           disabled:cursor-not-allowed disabled:opacity-50">
    <span v-if="loading"
      class="inline-block w-3.5 h-3.5 rounded-full border-2 border-current border-t-transparent animate-spin"
      aria-hidden="true"></span>
    <slot name="icon" v-else></slot>
    <span :class="loading ? 'opacity-70' : ''" class="leading-none">
      <slot />
    </span>
  </button>
</template>

<script setup>
import { computed, ref } from 'vue'

const props = defineProps({
  variant: { type: String, default: 'secondary' }, // primary | secondary | danger | ghost
  size: { type: String, default: 'md' }, // sm | md
  type: { type: String, default: 'button' },
  disabled: Boolean,
  loading: Boolean,
  // danger 二次确认 — 第一次点击进入 confirming 状态,2.4s 内再点才真触发
  confirm: Boolean,
  confirmHint: { type: String, default: '再点一次确认' },
})
const emit = defineEmits(['click'])

const confirming = ref(false)
let confirmTimer = null

function onClick(e) {
  if (props.disabled || props.loading) return
  if (props.variant === 'danger' && props.confirm && !confirming.value) {
    confirming.value = true
    clearTimeout(confirmTimer)
    confirmTimer = setTimeout(() => {
      confirming.value = false
    }, 2400)
    return
  }
  confirming.value = false
  clearTimeout(confirmTimer)
  emit('click', e)
}

const sizeClasses = {
  sm: 'h-7 px-2.5 text-xs',
  md: 'h-9 px-3.5 text-sm',
}

const variantClasses = computed(() => {
  if (props.variant === 'primary') {
    // round-12 F1 — text-on-accent 绕过旧深色兼容层,保留高对比前景
    return [
      'text-on-accent border border-indigo-500/30',
      'bg-indigo-600',
      'shadow-card hover:shadow-ring-accent',
      'hover:bg-indigo-500',
      'active:bg-indigo-700',
    ].join(' ')
  }
  if (props.variant === 'danger') {
    if (confirming.value) {
      return [
        'text-on-accent border border-rose-500/30',
        'bg-rose-600',
        'shadow-card ring-2 ring-rose-400/40',
      ].join(' ')
    }
    return [
      'text-rose-700 border border-rose-200',
      'bg-rose-50 hover:bg-rose-100',
      'hover:text-rose-800 hover:border-rose-300',
    ].join(' ')
  }
  if (props.variant === 'ghost') {
    return [
      'text-ink-600 border border-transparent',
      'hover:bg-ink-100 hover:text-ink-950',
    ].join(' ')
  }
  // secondary
  return [
    'text-ink-700 border border-hairline bg-surface',
    'hover:bg-ink-100 hover:text-ink-950 hover:border-hairline-strong',
    'active:bg-ink-200',
  ].join(' ')
})

const classes = computed(() => [
  sizeClasses[props.size] || sizeClasses.md,
  variantClasses.value,
])

defineExpose({ confirming })
</script>