Spaces:
Paused
Paused
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>
|