mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-28 00:45:03 +00:00
Compare commits
5 Commits
refactor/m
...
feat/learn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
456b7b2cae | ||
|
|
dd475b7f7b | ||
|
|
5adcf0ad50 | ||
|
|
47314746ce | ||
|
|
2d91640423 |
18
apps/website/src/components/common/Badge.vue
Normal file
18
apps/website/src/components/common/Badge.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { class: className } = defineProps<{ class?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="
|
||||
cn(
|
||||
'text-primary-warm-gray bg-transparency-ink-t80 inline-flex items-center rounded-full px-4 py-1 text-xs',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
53
apps/website/src/components/common/CallToActionSection.vue
Normal file
53
apps/website/src/components/common/CallToActionSection.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale, TranslationKey } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from './BrandButton.vue'
|
||||
|
||||
const {
|
||||
locale = 'en',
|
||||
headingKey,
|
||||
primaryLabelKey,
|
||||
primaryHref,
|
||||
secondaryLabelKey,
|
||||
secondaryHref
|
||||
} = defineProps<{
|
||||
locale?: Locale
|
||||
headingKey: TranslationKey
|
||||
primaryLabelKey: TranslationKey
|
||||
primaryHref?: string
|
||||
secondaryLabelKey?: TranslationKey
|
||||
secondaryHref?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-9xl mx-auto px-6 py-20 lg:py-32">
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<h2
|
||||
class="text-primary-comfy-canvas max-w-5xl text-3xl font-light tracking-tight lg:text-5xl"
|
||||
>
|
||||
{{ t(headingKey, locale) }}
|
||||
</h2>
|
||||
<div class="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||||
<BrandButton
|
||||
:href="primaryHref"
|
||||
variant="solid"
|
||||
size="xs"
|
||||
class="uppercase"
|
||||
>
|
||||
{{ t(primaryLabelKey, locale) }}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
v-if="secondaryLabelKey"
|
||||
:href="secondaryHref"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
class="uppercase"
|
||||
>
|
||||
{{ t(secondaryLabelKey, locale) }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
102
apps/website/src/components/common/EventsSection.vue
Normal file
102
apps/website/src/components/common/EventsSection.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
Locale,
|
||||
LocalizedText,
|
||||
TranslationKey
|
||||
} from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from './BrandButton.vue'
|
||||
|
||||
export type EventItem = {
|
||||
label: LocalizedText
|
||||
title: LocalizedText
|
||||
cta: LocalizedText
|
||||
href: string
|
||||
}
|
||||
|
||||
const {
|
||||
locale = 'en',
|
||||
headingKey,
|
||||
descriptionKey,
|
||||
notifyLabelKey,
|
||||
notifyHref,
|
||||
events
|
||||
} = defineProps<{
|
||||
locale?: Locale
|
||||
headingKey: TranslationKey
|
||||
descriptionKey: TranslationKey
|
||||
notifyLabelKey: TranslationKey
|
||||
notifyHref?: string
|
||||
events: readonly EventItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-9xl mx-auto px-6 py-12">
|
||||
<div
|
||||
class="bg-transparency-white-t4 rounded-4xl px-6 py-12 lg:px-16 lg:py-20"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div class="flex flex-col gap-8">
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-4xl font-light tracking-tight lg:text-6xl"
|
||||
>
|
||||
{{ t(headingKey, locale) }}
|
||||
</h2>
|
||||
<p
|
||||
class="text-primary-comfy-canvas max-w-sm text-sm/relaxed lg:text-base"
|
||||
>
|
||||
{{ t(descriptionKey, locale) }}
|
||||
</p>
|
||||
<div>
|
||||
<BrandButton
|
||||
:href="notifyHref"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
class="uppercase"
|
||||
>
|
||||
{{ t(notifyLabelKey, locale) }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<a
|
||||
v-for="(event, i) in events"
|
||||
:key="i"
|
||||
:href="event.href"
|
||||
class="group border-primary-comfy-canvas/15 flex items-center gap-4 border-b py-6 lg:gap-8"
|
||||
>
|
||||
<span
|
||||
class="text-primary-comfy-canvas shrink-0 text-sm font-medium"
|
||||
>
|
||||
{{ event.label[locale] }}
|
||||
</span>
|
||||
<span class="text-primary-warm-gray flex-1 text-sm">
|
||||
{{ event.title[locale] }}
|
||||
</span>
|
||||
<span
|
||||
class="text-primary-comfy-yellow flex shrink-0 items-center gap-2 text-sm"
|
||||
>
|
||||
{{ event.cta[locale] }}
|
||||
<svg
|
||||
class="size-4 transition-transform group-hover:translate-x-0.5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import BrandButton from '../common/BrandButton.vue'
|
||||
import VideoPlayer from '../common/VideoPlayer.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const tags = ['Seadance 2.0', 'Image To Video']
|
||||
const demoVideoSrc =
|
||||
'https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
|
||||
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div class="flex flex-col gap-8">
|
||||
<div>
|
||||
<h2
|
||||
class="text-primary-comfy-canvas text-4xl font-light tracking-tight lg:text-6xl"
|
||||
>
|
||||
{{ t('learning.featured.title', locale) }}
|
||||
</h2>
|
||||
<p class="text-primary-warm-gray mt-4 text-sm lg:text-base">
|
||||
{{ t('learning.featured.author', locale) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-primary-comfy-canvas max-w-md text-sm/relaxed lg:text-base"
|
||||
>
|
||||
{{ t('learning.featured.description', locale) }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<BrandButton variant="solid" size="xs" class="uppercase">
|
||||
{{ t('learning.featured.watchDemo', locale) }}
|
||||
</BrandButton>
|
||||
<BrandButton variant="outline" size="xs" class="uppercase">
|
||||
{{ t('cta.tryWorkflow', locale) }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-3">
|
||||
<span
|
||||
v-for="tag in tags"
|
||||
:key="tag"
|
||||
class="text-primary-warm-gray border-primary-warm-gray/40 rounded-full border px-4 py-1.5 text-xs"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-primary-warm-gray rounded-4.5xl border p-4">
|
||||
<VideoPlayer :locale :src="demoVideoSrc" minimal />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
23
apps/website/src/components/learning/HeroSection.vue
Normal file
23
apps/website/src/components/learning/HeroSection.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="max-w-9xl mx-auto flex flex-col items-center px-6 pt-24 pb-12 text-center"
|
||||
>
|
||||
<h1
|
||||
class="text-primary-comfy-canvas max-w-4xl text-3xl leading-[110%] font-light tracking-tight lg:text-5xl"
|
||||
>
|
||||
{{ t('learning.heroTitle.before', locale) }}
|
||||
<span class="text-primary-comfy-yellow">ComfyUI</span
|
||||
>{{ t('learning.heroTitle.after', locale) }}
|
||||
<br />
|
||||
{{ t('learning.heroTitle.line2', locale) }}
|
||||
</h1>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, useTemplateRef, watch } from 'vue'
|
||||
|
||||
import type { LearningTutorial } from '../../data/learningTutorials'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import { lockScroll, unlockScroll } from '../../composables/scrollLock'
|
||||
import { t } from '../../i18n/translations'
|
||||
|
||||
const { tutorial, locale = 'en' } = defineProps<{
|
||||
tutorial: LearningTutorial
|
||||
locale?: Locale
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ close: [] }>()
|
||||
|
||||
const dialogRef = useTemplateRef<HTMLDialogElement>('dialogRef')
|
||||
const videoRef = useTemplateRef<HTMLVideoElement>('videoRef')
|
||||
|
||||
const playFromStart = () => {
|
||||
const video = videoRef.value
|
||||
if (!video) return
|
||||
video.currentTime = 0
|
||||
void video.play().catch(() => {})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => tutorial.id,
|
||||
() => {
|
||||
playFromStart()
|
||||
}
|
||||
)
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) emit('close')
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') emit('close')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
lockScroll()
|
||||
dialogRef.value?.showModal()
|
||||
playFromStart()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlockScroll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
ref="dialogRef"
|
||||
:aria-label="tutorial.title[locale]"
|
||||
class="fixed inset-0 z-50 flex size-full max-h-none max-w-none flex-col items-center justify-center border-0 bg-transparent px-4 py-8 backdrop-blur-xl backdrop:bg-transparent lg:px-20 lg:py-8"
|
||||
@click="handleBackdropClick"
|
||||
@keydown="handleKeydown"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<button
|
||||
:aria-label="t('gallery.detail.close', locale)"
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink hover:bg-primary-comfy-yellow group absolute top-8 right-10 z-10 flex size-10 cursor-pointer items-center justify-center rounded-2xl border-2 transition-colors lg:right-26"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<span
|
||||
class="bg-primary-comfy-yellow group-hover:bg-primary-comfy-ink size-5 transition-colors"
|
||||
style="mask: url('/icons/close.svg') center / contain no-repeat"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="border-primary-comfy-yellow bg-primary-comfy-ink rounded-5xl flex w-full max-w-7xl items-center justify-center overflow-hidden border-2 p-3 lg:p-4"
|
||||
>
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="tutorial.videoSrc"
|
||||
:poster="tutorial.poster"
|
||||
class="aspect-video w-full rounded-3xl object-contain lg:rounded-4xl"
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
></video>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="text-primary-comfy-canvas mt-6 text-center text-lg font-medium lg:text-xl"
|
||||
>
|
||||
{{ t('learning.tutorials.titlePrefix', locale) }}
|
||||
{{ tutorial.title[locale] }}
|
||||
</h2>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
112
apps/website/src/components/learning/TutorialsSection.vue
Normal file
112
apps/website/src/components/learning/TutorialsSection.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
|
||||
import {
|
||||
getTutorialPosterSrc,
|
||||
learningTutorials
|
||||
} from '../../data/learningTutorials'
|
||||
import { t } from '../../i18n/translations'
|
||||
import Badge from '../common/Badge.vue'
|
||||
import TutorialDetailDialog from './TutorialDetailDialog.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
|
||||
const activeTutorialId = ref<string | null>(null)
|
||||
const activeTutorial = () =>
|
||||
learningTutorials.find((tutorial) => tutorial.id === activeTutorialId.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-9xl mx-auto px-6 pb-16 lg:pb-24">
|
||||
<ul
|
||||
class="grid grid-cols-1 gap-x-6 gap-y-10 md:grid-cols-2 lg:grid-cols-3 lg:gap-x-8"
|
||||
>
|
||||
<li
|
||||
v-for="tutorial in learningTutorials"
|
||||
:key="tutorial.id"
|
||||
class="bg-transparency-white-t4 flex flex-col gap-4 overflow-hidden rounded-3xl border-0 p-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="group relative block aspect-video cursor-pointer overflow-hidden rounded-3xl"
|
||||
:aria-label="`${t('learning.tutorials.titlePrefix', locale)} ${tutorial.title[locale]}`"
|
||||
@click="activeTutorialId = tutorial.id"
|
||||
>
|
||||
<video
|
||||
:src="getTutorialPosterSrc(tutorial)"
|
||||
:poster="tutorial.poster"
|
||||
class="size-full object-cover"
|
||||
preload="metadata"
|
||||
playsinline
|
||||
muted
|
||||
></video>
|
||||
<span
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
class="flex size-14 items-center justify-center rounded-full bg-white/25 backdrop-blur-sm transition-transform group-hover:scale-105 lg:size-16"
|
||||
>
|
||||
<svg
|
||||
class="ml-1 size-5 text-white lg:size-6"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="flex flex-col space-y-3 p-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h3
|
||||
class="text-primary-comfy-canvas text-sm/snug lg:text-base/snug"
|
||||
>
|
||||
{{ t('learning.tutorials.titlePrefix', locale) }}
|
||||
{{ tutorial.title[locale] }}
|
||||
</h3>
|
||||
<a
|
||||
:href="tutorial.href"
|
||||
class="text-primary-comfy-yellow group flex shrink-0 items-center gap-2 text-xs font-medium tracking-wide uppercase lg:text-sm"
|
||||
>
|
||||
<span
|
||||
class="bg-primary-comfy-yellow flex size-6 items-center justify-center rounded-full transition-transform group-hover:translate-x-0.5 lg:size-7"
|
||||
>
|
||||
<svg
|
||||
class="text-primary-comfy-ink size-3 lg:size-3.5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="9 6 15 12 9 18" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ t('cta.tryWorkflow', locale) }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="flex flex-wrap gap-2">
|
||||
<li v-for="tag in tutorial.tags" :key="tag">
|
||||
<Badge>{{ t(tag, locale) }}</Badge>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<TutorialDetailDialog
|
||||
v-if="activeTutorial()"
|
||||
:tutorial="activeTutorial()!"
|
||||
:locale="locale"
|
||||
@close="activeTutorialId = null"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
31
apps/website/src/data/events.ts
Normal file
31
apps/website/src/data/events.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { EventItem } from '../components/common/EventsSection.vue'
|
||||
|
||||
export const learningEvents: readonly EventItem[] = [
|
||||
{
|
||||
label: { en: 'Live Stream:', 'zh-CN': '直播:' },
|
||||
title: {
|
||||
en: 'Zero to Node: Building Your First Workflow',
|
||||
'zh-CN': '从零到节点:构建你的第一个工作流'
|
||||
},
|
||||
cta: { en: 'Link', 'zh-CN': '链接' },
|
||||
href: '#'
|
||||
},
|
||||
{
|
||||
label: { en: 'Event 1', 'zh-CN': '活动 1' },
|
||||
title: {
|
||||
en: 'Lorem ipsum dollar sita met',
|
||||
'zh-CN': '此处为活动描述的占位文本'
|
||||
},
|
||||
cta: { en: 'London, UK', 'zh-CN': '英国伦敦' },
|
||||
href: '#'
|
||||
},
|
||||
{
|
||||
label: { en: 'Event 2', 'zh-CN': '活动 2' },
|
||||
title: {
|
||||
en: 'Lorem ipsum dollar sita met',
|
||||
'zh-CN': '此处为活动描述的占位文本'
|
||||
},
|
||||
cta: { en: 'San Francisco', 'zh-CN': '旧金山' },
|
||||
href: '#'
|
||||
}
|
||||
] as const
|
||||
72
apps/website/src/data/learningTutorials.ts
Normal file
72
apps/website/src/data/learningTutorials.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { LocalizedText, TranslationKey } from '../i18n/translations'
|
||||
|
||||
export interface LearningTutorial {
|
||||
id: string
|
||||
title: LocalizedText
|
||||
videoSrc: string
|
||||
poster?: string
|
||||
posterTime?: number
|
||||
href: string
|
||||
tags: readonly TranslationKey[]
|
||||
}
|
||||
|
||||
const DEFAULT_POSTER_TIME_SECONDS = 1
|
||||
|
||||
const partnerNodesTag: TranslationKey = 'tags.partnerNodes'
|
||||
const imageToVideoTag: TranslationKey = 'tags.imageToVideo'
|
||||
|
||||
export const getTutorialPosterSrc = (tutorial: LearningTutorial): string =>
|
||||
tutorial.poster
|
||||
? tutorial.videoSrc
|
||||
: `${tutorial.videoSrc}#t=${tutorial.posterTime ?? DEFAULT_POSTER_TIME_SECONDS}`
|
||||
|
||||
export const learningTutorials: readonly LearningTutorial[] = [
|
||||
{
|
||||
id: 'cleanplate_walkthrough_v03',
|
||||
title: { en: 'Title here', 'zh-CN': '标题占位' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/cleanplate_walkthrough_v03.mp4',
|
||||
href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'deaging_workflow_v03',
|
||||
title: { en: 'Title here', 'zh-CN': '标题占位' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/deaging_workflow_v03.mp4',
|
||||
href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'frame_adjustments_demo_v03',
|
||||
title: { en: 'Title here', 'zh-CN': '标题占位' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/frame_adjustments_demo_v03.mp4',
|
||||
href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'mattes_and_utilities_v03',
|
||||
title: { en: 'Title here', 'zh-CN': '标题占位' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/mattes_and_utilities_v03.mp4',
|
||||
href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'seedance_demo_comfyui_v03',
|
||||
title: { en: 'Title here', 'zh-CN': '标题占位' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/seedance_demo_comfyui_v03.mp4',
|
||||
href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
},
|
||||
{
|
||||
id: 'skyreplacement_smaller_v06',
|
||||
title: { en: 'Title here', 'zh-CN': '标题占位' },
|
||||
videoSrc:
|
||||
'https://media.comfy.org/website/learning/skyreplacement_smaller_v06.mp4',
|
||||
href: '#',
|
||||
tags: [partnerNodesTag, imageToVideoTag]
|
||||
}
|
||||
] as const
|
||||
@@ -1,6 +1,22 @@
|
||||
type Locale = 'en' | 'zh-CN'
|
||||
|
||||
const translations = {
|
||||
// Tags (global, reusable across sections)
|
||||
'tags.partnerNodes': {
|
||||
en: 'Partner Nodes',
|
||||
'zh-CN': '合作伙伴节点'
|
||||
},
|
||||
'tags.imageToVideo': {
|
||||
en: 'Image To Video',
|
||||
'zh-CN': '图像生成视频'
|
||||
},
|
||||
|
||||
// CTAs (global, reusable across sections)
|
||||
'cta.tryWorkflow': {
|
||||
en: 'Try Workflow',
|
||||
'zh-CN': '试用工作流'
|
||||
},
|
||||
|
||||
// HeroSection
|
||||
'hero.title': {
|
||||
en: 'Professional Control\nof Visual AI',
|
||||
@@ -1435,6 +1451,58 @@ const translations = {
|
||||
'player.subtitlesOn': { en: 'Subtitles on', 'zh-CN': '开启字幕' },
|
||||
'player.subtitlesOff': { en: 'Subtitles off', 'zh-CN': '关闭字幕' },
|
||||
|
||||
// LearningHeroSection
|
||||
'learning.heroTitle.before': { en: 'Learn', 'zh-CN': '学习' },
|
||||
'learning.heroTitle.after': { en: '.', 'zh-CN': '。' },
|
||||
'learning.heroTitle.line2': {
|
||||
en: 'Build what doesn’t exist yet.',
|
||||
'zh-CN': '构建尚未存在之物。'
|
||||
},
|
||||
|
||||
// LearningFeaturedWorkflowSection
|
||||
'learning.featured.title': {
|
||||
en: 'Sky Replacement',
|
||||
'zh-CN': '天空替换'
|
||||
},
|
||||
'learning.featured.author': {
|
||||
en: 'by Doug Hogan',
|
||||
'zh-CN': '作者:Doug Hogan'
|
||||
},
|
||||
'learning.featured.description': {
|
||||
en: 'Detailed explanation here fpo lorem ipsum Detailed explanation here fpo lorem ipsum Detailed explem ipsum Detailed explanation here fpo lorem ipsum',
|
||||
'zh-CN':
|
||||
'此处为详细说明的占位文本,待补充实际内容。此处为详细说明的占位文本,待补充实际内容。'
|
||||
},
|
||||
'learning.featured.watchDemo': {
|
||||
en: 'Watch Demo',
|
||||
'zh-CN': '观看演示'
|
||||
},
|
||||
|
||||
// LearningTutorialsSection
|
||||
'learning.tutorials.titlePrefix': {
|
||||
en: 'Learn how to:',
|
||||
'zh-CN': '学习如何:'
|
||||
},
|
||||
|
||||
// LearningCallToActionSection
|
||||
'learning.cta.heading': {
|
||||
en: 'Schedule a demo and see how ComfyUI fits your team’s creative needs.',
|
||||
'zh-CN': '预约演示,了解 ComfyUI 如何契合你的团队创作需求。'
|
||||
},
|
||||
'learning.cta.contactSales': {
|
||||
en: 'Contact Sales',
|
||||
'zh-CN': '联系销售'
|
||||
},
|
||||
|
||||
// LearningEventsSection
|
||||
'learning.events.heading': { en: 'Events', 'zh-CN': '活动' },
|
||||
'learning.events.description': {
|
||||
en: 'Check out our upcoming live streams and community meetings. We’re always open to your questions, ideas, and conversations.',
|
||||
'zh-CN':
|
||||
'查看我们即将举办的直播和社区聚会。我们随时欢迎你的提问、想法和交流。'
|
||||
},
|
||||
'learning.events.getNotified': { en: 'Get Notified', 'zh-CN': '获取通知' },
|
||||
|
||||
// GalleryHeroSection
|
||||
'gallery.label': { en: 'GALLERY', 'zh-CN': '画廊' },
|
||||
'gallery.heroTitle.before': {
|
||||
@@ -1471,9 +1539,13 @@ const translations = {
|
||||
},
|
||||
'about.hero.body': {
|
||||
en: 'The team behind Comfy is small, intense, and building what we intend to be our life\u2019s work.',
|
||||
'zh-CN': 'Comfy 背后的团队规模虽小,但充满热情,致力于打造我们毕生的事业。'
|
||||
'zh-CN':
|
||||
'Comfy \u80cc\u540e\u7684\u56e2\u961f\u89c4\u6a21\u867d\u5c0f\uff0c\u4f46\u5145\u6ee1\u70ed\u60c5\uff0c\u81f4\u529b\u4e8e\u6253\u9020\u6211\u4eec\u6bd5\u751f\u7684\u4e8b\u4e1a\u3002'
|
||||
},
|
||||
'about.hero.cta': {
|
||||
en: 'SEE OPEN ROLES',
|
||||
'zh-CN': '\u67e5\u770b\u5f00\u653e\u804c\u4f4d'
|
||||
},
|
||||
'about.hero.cta': { en: 'SEE OPEN ROLES', 'zh-CN': '查看开放职位' },
|
||||
|
||||
// AboutStorySection
|
||||
'about.story.label': { en: 'OUR STORY', 'zh-CN': '我们的故事' },
|
||||
@@ -4240,6 +4312,8 @@ const translations = {
|
||||
|
||||
type TranslationKey = keyof typeof translations
|
||||
|
||||
type LocalizedText = Record<Locale, string>
|
||||
|
||||
export function t(key: TranslationKey, locale: Locale = 'en'): string {
|
||||
return translations[key][locale] ?? translations[key].en
|
||||
}
|
||||
@@ -4250,4 +4324,4 @@ export function hasKey(key: string): boolean {
|
||||
return key in translations
|
||||
}
|
||||
|
||||
export type { Locale, TranslationKey }
|
||||
export type { Locale, LocalizedText, TranslationKey }
|
||||
|
||||
34
apps/website/src/pages/learning.astro
Normal file
34
apps/website/src/pages/learning.astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../components/learning/HeroSection.vue'
|
||||
import FeaturedWorkflowSection from '../components/learning/FeaturedWorkflowSection.vue'
|
||||
import TutorialsSection from '../components/learning/TutorialsSection.vue'
|
||||
import CallToActionSection from '../components/common/CallToActionSection.vue'
|
||||
import EventsSection from '../components/common/EventsSection.vue'
|
||||
import { getRoutes } from '../config/routes'
|
||||
import { learningEvents } from '../data/events'
|
||||
|
||||
const routes = getRoutes('en')
|
||||
---
|
||||
|
||||
<BaseLayout title="Learning — Comfy">
|
||||
<HeroSection client:load />
|
||||
<FeaturedWorkflowSection client:visible />
|
||||
<TutorialsSection client:visible />
|
||||
<CallToActionSection
|
||||
headingKey="learning.cta.heading"
|
||||
primaryLabelKey="learning.cta.contactSales"
|
||||
primaryHref={routes.contact}
|
||||
secondaryLabelKey="cta.tryWorkflow"
|
||||
secondaryHref={routes.demos}
|
||||
client:visible
|
||||
/>
|
||||
<EventsSection
|
||||
headingKey="learning.events.heading"
|
||||
descriptionKey="learning.events.description"
|
||||
notifyLabelKey="learning.events.getNotified"
|
||||
notifyHref={routes.contact}
|
||||
events={learningEvents}
|
||||
client:visible
|
||||
/>
|
||||
</BaseLayout>
|
||||
36
apps/website/src/pages/zh-CN/learning.astro
Normal file
36
apps/website/src/pages/zh-CN/learning.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import HeroSection from '../../components/learning/HeroSection.vue'
|
||||
import FeaturedWorkflowSection from '../../components/learning/FeaturedWorkflowSection.vue'
|
||||
import TutorialsSection from '../../components/learning/TutorialsSection.vue'
|
||||
import CallToActionSection from '../../components/common/CallToActionSection.vue'
|
||||
import EventsSection from '../../components/common/EventsSection.vue'
|
||||
import { getRoutes } from '../../config/routes'
|
||||
import { learningEvents } from '../../data/events'
|
||||
|
||||
const routes = getRoutes('zh-CN')
|
||||
---
|
||||
|
||||
<BaseLayout title="学习 — Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<FeaturedWorkflowSection locale="zh-CN" client:visible />
|
||||
<TutorialsSection locale="zh-CN" client:visible />
|
||||
<CallToActionSection
|
||||
locale="zh-CN"
|
||||
headingKey="learning.cta.heading"
|
||||
primaryLabelKey="learning.cta.contactSales"
|
||||
primaryHref={routes.contact}
|
||||
secondaryLabelKey="cta.tryWorkflow"
|
||||
secondaryHref={routes.demos}
|
||||
client:visible
|
||||
/>
|
||||
<EventsSection
|
||||
locale="zh-CN"
|
||||
headingKey="learning.events.heading"
|
||||
descriptionKey="learning.events.description"
|
||||
notifyLabelKey="learning.events.getNotified"
|
||||
notifyHref={routes.contact}
|
||||
events={learningEvents}
|
||||
client:visible
|
||||
/>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user