feat: extract productCardsSection:

This commit is contained in:
Yourz
2026-04-15 18:59:37 +08:00
parent c7833ca5f1
commit 10f0602b20
6 changed files with 98 additions and 121 deletions

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import type { Locale, TranslationKey } from '../../i18n/translations'
import { getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import ProductCard from './ProductCard.vue'
type Product = 'local' | 'cloud' | 'api' | 'enterprise'
const {
locale = 'en',
excludeProduct,
labelKey = 'products.label'
} = defineProps<{
locale?: Locale
excludeProduct?: Product
labelKey?: TranslationKey
}>()
const routes = getRoutes(locale)
const allCards: (ReturnType<typeof cardDef> & { product: Product })[] = [
cardDef('local', routes.download, 'bg-primary-warm-gray'),
cardDef('cloud', routes.cloud, 'bg-secondary-mauve'),
cardDef('api', routes.api, 'bg-primary-comfy-plum'),
cardDef('enterprise', routes.cloudEnterprise, 'bg-illustration-forest')
]
function cardDef(product: Product, href: string, bg: string) {
return {
product,
title: t(`products.${product}.title`, locale),
description: t(`products.${product}.description`, locale),
cta: t(`products.${product}.cta`, locale),
href,
bg
}
}
const cards = excludeProduct
? allCards.filter((c) => c.product !== excludeProduct)
: allCards
</script>
<template>
<section class="bg-primary-comfy-ink px-4 py-20 lg:px-20 lg:py-24">
<!-- Header -->
<div class="flex flex-col items-center text-center">
<p
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ t(labelKey, locale) }}
</p>
<h2
class="text-primary-comfy-canvas mt-4 text-4xl font-light whitespace-pre-line lg:text-5xl"
>
{{ t('products.heading', locale) }}
</h2>
<p class="text-primary-comfy-canvas/70 mt-4 text-sm">
{{ t('products.subheading', locale) }}
</p>
</div>
<!-- Cards -->
<div
:class="[
'mt-16 grid grid-cols-1 gap-4',
cards.length === 4 ? 'lg:grid-cols-4' : 'lg:grid-cols-3'
]"
>
<ProductCard v-for="card in cards" :key="card.product" v-bind="card" />
</div>
</section>
</template>

View File

@@ -1,67 +1,11 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import { getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import ProductCard from '../common/ProductCard.vue'
import ProductCardsSection from '../common/ProductCardsSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
const cards = [
{
title: t('products.local.title', locale),
description: t('products.local.description', locale),
cta: t('products.local.cta', locale),
href: routes.download,
bg: 'bg-primary-warm-gray'
},
{
title: t('products.cloud.title', locale),
description: t('products.cloud.description', locale),
cta: t('products.cloud.cta', locale),
href: routes.cloud,
bg: 'bg-secondary-mauve'
},
{
title: t('products.api.title', locale),
description: t('products.api.description', locale),
cta: t('products.api.cta', locale),
href: routes.api,
bg: 'bg-primary-comfy-plum'
},
{
title: t('products.enterprise.title', locale),
description: t('products.enterprise.description', locale),
cta: t('products.enterprise.cta', locale),
href: routes.cloudEnterprise,
bg: 'bg-illustration-forest'
}
]
</script>
<template>
<section class="bg-primary-comfy-ink px-4 py-20 lg:px-20 lg:py-24">
<!-- Header -->
<div class="flex flex-col items-center text-center">
<p
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ t('products.label', locale) }}
</p>
<h2
class="text-primary-comfy-canvas mt-4 text-4xl font-light whitespace-pre-line lg:text-5xl"
>
{{ t('products.heading', locale) }}
</h2>
<p class="text-primary-warm-gray mt-4 text-sm">
{{ t('products.subheading', locale) }}
</p>
</div>
<!-- Cards -->
<div class="mt-16 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<ProductCard v-for="card in cards" :key="card.title" v-bind="card" />
</div>
</section>
<ProductCardsSection :locale="locale" />
</template>

View File

@@ -10,14 +10,14 @@ const { downloadUrl } = useDownloadUrl()
</script>
<template>
<section class="px-4 py-24 md:px-20 md:py-40">
<section class="px-4 py-24 lg:px-20 lg:py-40">
<div
class="flex flex-col-reverse items-stretch gap-10 md:flex-row md:gap-16"
class="flex flex-col-reverse items-stretch gap-10 lg:flex-row lg:gap-16"
>
<!-- Text content -->
<div class="flex flex-1 flex-col justify-between">
<div>
<h2 class="text-primary-comfy-canvas text-3xl font-light md:text-4xl">
<h2 class="text-primary-comfy-canvas text-3xl font-light lg:text-4xl">
{{ t('download.ecosystem.heading', locale) }}
</h2>
<p class="text-primary-comfy-canvas/70 mt-6 text-sm">
@@ -26,7 +26,7 @@ const { downloadUrl } = useDownloadUrl()
</div>
<!-- CTA buttons -->
<div class="mt-10 flex flex-col gap-4 md:flex-row">
<div class="mt-10 flex flex-col gap-4 lg:flex-row">
<a
:href="downloadUrl"
target="_blank"

View File

@@ -11,31 +11,31 @@ const { downloadUrl } = useDownloadUrl()
</script>
<template>
<section class="overflow-hidden px-4 pt-20 pb-16 md:px-20 md:py-24">
<section class="overflow-hidden px-4 pt-20 pb-16 lg:px-20 lg:py-24">
<div class="mx-auto flex max-w-5xl flex-col items-center">
<div class="flex w-full max-w-2xl flex-col items-center text-center">
<ProductHeroBadge />
<h1
class="text-primary-comfy-canvas mt-8 max-w-[10ch] text-5xl/tight font-light whitespace-pre-line md:max-w-none md:text-5xl"
class="text-primary-comfy-canvas mt-8 max-w-[10ch] text-5xl/tight font-light whitespace-pre-line lg:max-w-none lg:text-5xl"
>
{{ t('download.hero.heading', locale) }}
</h1>
<p
class="text-primary-comfy-canvas mt-8 max-w-xs text-sm md:mt-20 md:max-w-xl md:text-base"
class="text-primary-comfy-canvas mt-8 max-w-xs text-sm lg:mt-20 lg:max-w-xl lg:text-base"
>
{{ t('download.hero.subtitle', locale) }}
</p>
<div
class="mt-10 flex w-full max-w-md flex-col gap-4 md:w-auto md:max-w-none md:flex-row"
class="mt-10 flex w-full max-w-md flex-col gap-4 lg:w-auto lg:max-w-none lg:flex-row"
>
<a
:href="downloadUrl"
target="_blank"
rel="noopener"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 md:min-w-60"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('download.hero.downloadLocal', locale) }}
</a>
@@ -43,7 +43,7 @@ const { downloadUrl } = useDownloadUrl()
:href="externalLinks.github"
target="_blank"
rel="noopener"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink flex items-center justify-center gap-2 rounded-full border px-8 py-4 text-sm font-bold tracking-wider transition-colors md:min-w-60"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink flex items-center justify-center gap-2 rounded-full border px-8 py-4 text-sm font-bold tracking-wider transition-colors lg:min-w-60"
>
<span
class="icon-mask size-5 mask-[url('/icons/social/github.svg')]"
@@ -56,7 +56,7 @@ const { downloadUrl } = useDownloadUrl()
<!-- Placeholder for future animation; clipped within hero section -->
<div
class="relative mt-4 flex h-104 w-full max-w-4xl items-start justify-center overflow-hidden md:mt-12 md:h-136 md:items-center"
class="relative mt-4 flex h-104 w-full max-w-4xl items-start justify-center overflow-hidden lg:mt-12 lg:h-136 lg:items-center"
>
<div
class="border-secondary-mauve/35 bg-primary-comfy-ink/35 absolute top-20 left-2 h-64 w-44 rotate-30 rounded-4xl border"
@@ -68,7 +68,7 @@ const { downloadUrl } = useDownloadUrl()
class="border-secondary-mauve/35 bg-primary-comfy-ink/35 absolute top-52 right-4 h-56 w-40 rotate-30 rounded-4xl border"
/>
<div class="relative z-10 mt-28 grid grid-cols-3 gap-0.5 md:mt-0">
<div class="relative z-10 mt-28 grid grid-cols-3 gap-0.5 lg:mt-0">
<span class="bg-secondary-mauve block size-9 rounded-lg" />
<span class="bg-primary-comfy-plum block size-9 rounded-lg" />
<span class="bg-secondary-mauve block size-9 rounded-lg" />

View File

@@ -1,60 +1,15 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { getRoutes } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import ProductCard from '../../common/ProductCard.vue'
import ProductCardsSection from '../../common/ProductCardsSection.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
const cards = [
{
title: t('products.cloud.title', locale),
description: t('products.cloud.description', locale),
cta: t('products.cloud.cta', locale),
href: routes.cloud,
bg: 'bg-secondary-mauve'
},
{
title: t('products.api.title', locale),
description: t('products.api.description', locale),
cta: t('products.api.cta', locale),
href: routes.api,
bg: 'bg-primary-comfy-plum'
},
{
title: t('products.enterprise.title', locale),
description: t('products.enterprise.description', locale),
cta: t('products.enterprise.cta', locale),
href: routes.cloudEnterprise,
bg: 'bg-illustration-forest'
}
]
</script>
<template>
<section class="bg-primary-comfy-ink px-4 py-24 md:px-20 md:py-40">
<!-- Header -->
<div class="flex flex-col items-center text-center">
<p
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
>
{{ t('products.label', locale) }}
</p>
<h2
class="text-primary-comfy-canvas mt-4 text-4xl font-light whitespace-pre-line md:text-5xl"
>
{{ t('products.heading', locale) }}
</h2>
<p class="text-primary-comfy-canvas/70 mt-4 text-sm">
{{ t('products.subheading', locale) }}
</p>
</div>
<!-- Cards -->
<div class="mt-16 grid grid-cols-1 gap-4 md:grid-cols-3">
<ProductCard v-for="card in cards" :key="card.title" v-bind="card" />
</div>
</section>
<ProductCardsSection
:locale="locale"
exclude-product="local"
label-key="products.labelProducts"
/>
</template>

View File

@@ -134,6 +134,10 @@ const translations = {
en: 'Comfy UI',
'zh-CN': 'Comfy UI'
},
'products.labelProducts': {
en: 'Products',
'zh-CN': '产品'
},
'products.heading': {
en: 'The AI creation\nengine for complete control',
'zh-CN': '完全掌控的\nAI 创作引擎'