- Backend: FastAPI + SQLAlchemy + Celery (Python 3.11+) - Frontend: Vue 3 + TypeScript + Pinia + Tailwind - Admin Frontend: separate Vue 3 app for management - Docker Compose: 9 services orchestration - Specs: design prototypes, memory system PRD, product roadmap Cleanup performed: - Removed temporary debug scripts from backend root - Removed deprecated admin_app.py (embedded UI) - Removed duplicate docs from admin-frontend - Updated .gitignore for Vite cache and egg-info
88 lines
2.4 KiB
Vue
88 lines
2.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, useAttrs } from 'vue'
|
|
import type { Component } from 'vue'
|
|
|
|
defineOptions({ inheritAttrs: false })
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
|
|
size?: 'sm' | 'md' | 'lg'
|
|
loading?: boolean
|
|
disabled?: boolean
|
|
icon?: Component
|
|
as?: string | Record<string, unknown>
|
|
}>(),
|
|
{
|
|
variant: 'primary',
|
|
size: 'md',
|
|
loading: false,
|
|
disabled: false,
|
|
as: 'button',
|
|
},
|
|
)
|
|
|
|
const attrs = useAttrs()
|
|
|
|
const isButton = computed(() => props.as === 'button' || !props.as)
|
|
const isDisabled = computed(() => props.disabled || props.loading)
|
|
|
|
const sizeClasses = computed(() => {
|
|
if (props.size === 'sm') return 'px-3 py-2 text-sm rounded-lg'
|
|
if (props.size === 'lg') return 'px-6 py-3 text-base rounded-xl'
|
|
return 'px-4 py-2.5 text-sm rounded-xl'
|
|
})
|
|
|
|
const variantClasses = computed(() => {
|
|
switch (props.variant) {
|
|
case 'secondary':
|
|
return 'bg-white border border-gray-200 text-gray-700 hover:bg-gray-50'
|
|
case 'danger':
|
|
return 'bg-red-500 text-white hover:bg-red-600'
|
|
case 'ghost':
|
|
return 'bg-transparent text-gray-600 hover:bg-gray-100'
|
|
default:
|
|
return 'btn-magic text-white'
|
|
}
|
|
})
|
|
|
|
const baseClasses = computed(() => [
|
|
'inline-flex items-center justify-center gap-2 font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-300',
|
|
sizeClasses.value,
|
|
variantClasses.value,
|
|
isDisabled.value ? 'opacity-60 cursor-not-allowed' : '',
|
|
])
|
|
|
|
const passthroughAttrs = computed(() => {
|
|
const { class: _class, type: _type, ...rest } = attrs
|
|
return rest
|
|
})
|
|
|
|
function handleClick(event: MouseEvent) {
|
|
if (!isButton.value && isDisabled.value) {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<component
|
|
:is="props.as || 'button'"
|
|
:type="isButton ? (attrs.type as string || 'button') : undefined"
|
|
:disabled="isButton ? isDisabled : undefined"
|
|
:aria-disabled="!isButton && isDisabled ? 'true' : undefined"
|
|
:class="[baseClasses, attrs.class]"
|
|
v-bind="passthroughAttrs"
|
|
@click="handleClick"
|
|
>
|
|
<span
|
|
v-if="props.loading"
|
|
class="inline-flex h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
aria-hidden="true"
|
|
></span>
|
|
<component v-else-if="props.icon" :is="props.icon" class="h-5 w-5" aria-hidden="true" />
|
|
<slot />
|
|
</component>
|
|
</template>
|