Initial commit: clean project structure
- 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
This commit is contained in:
87
frontend/src/components/ui/BaseButton.vue
Normal file
87
frontend/src/components/ui/BaseButton.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user