feat: вложенные роуты хаб/товар с хлебными крошками
All checks were successful
Build Docker Image / build (push) Successful in 4m34s
All checks were successful
Build Docker Image / build (push) Successful in 4m34s
- /catalog/hubs/[id] — список товаров с графиками - /catalog/hubs/[id]/[productId] — страница товара с большим графиком и предложениями - CatalogBreadcrumbs — компонент хлебных крошек
This commit is contained in:
126
app/pages/catalog/hubs/[id]/index.vue
Normal file
126
app/pages/catalog/hubs/[id]/index.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<Stack gap="0">
|
||||
<!-- Loading -->
|
||||
<Section v-if="isLoading" variant="plain" paddingY="lg">
|
||||
<Stack align="center" justify="center" gap="4">
|
||||
<Spinner />
|
||||
<Text tone="muted">{{ t('catalogHub.states.loading') }}</Text>
|
||||
</Stack>
|
||||
</Section>
|
||||
|
||||
<!-- Error / Not Found -->
|
||||
<Section v-else-if="!hub" variant="plain" paddingY="lg">
|
||||
<Card padding="lg">
|
||||
<Stack align="center" gap="4">
|
||||
<IconCircle tone="primary">
|
||||
<Icon name="lucide:map-pin" size="24" />
|
||||
</IconCircle>
|
||||
<Heading :level="2">{{ t('catalogHub.not_found.title') }}</Heading>
|
||||
<Text tone="muted">{{ t('catalogHub.not_found.subtitle') }}</Text>
|
||||
<Button @click="navigateTo(localePath('/catalog'))">
|
||||
{{ t('catalogHub.actions.back_to_catalog') }}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<!-- Content -->
|
||||
<Section v-else variant="plain" paddingY="lg">
|
||||
<Stack gap="4">
|
||||
<!-- Breadcrumbs -->
|
||||
<CatalogBreadcrumbs :hub-id="hubId" :hub-name="hub.name" />
|
||||
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<Heading :level="1">{{ hub.name }}</Heading>
|
||||
<Text tone="muted" size="sm">{{ hub.country }}</Text>
|
||||
</div>
|
||||
|
||||
<!-- Products grid -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<HubProductCard
|
||||
v-for="product in products"
|
||||
:key="product.uuid"
|
||||
:name="product.name"
|
||||
:price-history="getMockPriceHistory(product.uuid)"
|
||||
@select="goToProduct(product.uuid)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<Stack v-if="products.length === 0" align="center" gap="2">
|
||||
<Icon name="lucide:package-x" size="32" class="text-base-content/40" />
|
||||
<Text tone="muted">{{ t('catalogHub.products.empty') }}</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Stack>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GetNodeConnectionsDocument } from '~/composables/graphql/public/geo-generated'
|
||||
import { GetAvailableProductsDocument } from '~/composables/graphql/public/exchange-generated'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'topnav'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const localePath = useLocalePath()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const hub = ref<any>(null)
|
||||
const products = ref<Array<{ uuid: string; name: string }>>([])
|
||||
|
||||
const hubId = computed(() => route.params.id as string)
|
||||
|
||||
// Navigate to product page
|
||||
const goToProduct = (productId: string) => {
|
||||
navigateTo(localePath(`/catalog/hubs/${hubId.value}/${productId}`))
|
||||
}
|
||||
|
||||
// Mock price history generator (seeded by uuid for consistent results)
|
||||
const getMockPriceHistory = (uuid: string): number[] => {
|
||||
const seed = uuid.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
|
||||
const basePrice = 100 + (seed % 200)
|
||||
return Array.from({ length: 7 }, (_, i) => {
|
||||
const variation = Math.sin(seed + i * 0.5) * 20 + Math.cos(seed * 0.3 + i) * 10
|
||||
return Math.round(basePrice + variation)
|
||||
})
|
||||
}
|
||||
|
||||
// Initial load
|
||||
try {
|
||||
const [{ data: connectionsData }, { data: productsData }] = await Promise.all([
|
||||
useServerQuery('hub-connections', GetNodeConnectionsDocument, { uuid: hubId.value }, 'public', 'geo'),
|
||||
useServerQuery('available-products', GetAvailableProductsDocument, {}, 'public', 'exchange')
|
||||
])
|
||||
|
||||
hub.value = connectionsData.value?.nodeConnections?.hub || null
|
||||
products.value = (productsData.value?.getAvailableProducts || [])
|
||||
.filter((p): p is { uuid: string; name: string } => p !== null && !!p.uuid && !!p.name)
|
||||
.map(p => ({ uuid: p.uuid!, name: p.name! }))
|
||||
} catch (error) {
|
||||
console.error('Error loading hub:', error)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
// SEO
|
||||
useHead(() => ({
|
||||
title: hub.value?.name
|
||||
? t('catalogHub.meta.title_with_name', { name: hub.value.name })
|
||||
: t('catalogHub.meta.title'),
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: t('catalogHub.meta.description', {
|
||||
name: hub.value?.name || '',
|
||||
country: hub.value?.country || '',
|
||||
products: products.value.length
|
||||
})
|
||||
}
|
||||
]
|
||||
}))
|
||||
</script>
|
||||
Reference in New Issue
Block a user