feat(页脚): 添加多语言支持和动画效果
重构页脚组件,增加中英文翻译支持 创建 FooterLogo 和 AnimatedButton 通用组件 实现滚动动画和按钮悬停效果
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import gsap from 'gsap'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string
|
||||||
|
to?: string
|
||||||
|
theme?: 'dark' | 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
theme: 'dark'
|
||||||
|
})
|
||||||
|
|
||||||
|
const btnRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const onHover = () => {
|
||||||
|
if (!btnRef.value) return
|
||||||
|
const origs = btnRef.value.querySelectorAll('.btn-char-orig')
|
||||||
|
const clones = btnRef.value.querySelectorAll('.btn-char-clone')
|
||||||
|
const bg = btnRef.value.querySelector('.btn-bg-hover')
|
||||||
|
|
||||||
|
// 背景从底部滑入 (yPercent: 100 -> 0)
|
||||||
|
gsap.to(bg, { yPercent: 0, duration: 0.4, ease: 'power3.out', overwrite: true })
|
||||||
|
|
||||||
|
gsap.to(origs, { yPercent: -100, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
|
||||||
|
gsap.to(clones, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLeave = () => {
|
||||||
|
if (!btnRef.value) return
|
||||||
|
const origs = btnRef.value.querySelectorAll('.btn-char-orig')
|
||||||
|
const clones = btnRef.value.querySelectorAll('.btn-char-clone')
|
||||||
|
const bg = btnRef.value.querySelector('.btn-bg-hover')
|
||||||
|
|
||||||
|
// 背景向下反向滑出回收 (yPercent: 0 -> 100)
|
||||||
|
gsap.to(bg, { yPercent: 100, duration: 0.4, ease: 'power3.out', overwrite: true })
|
||||||
|
|
||||||
|
gsap.to(origs, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
|
||||||
|
gsap.to(clones, { yPercent: 100, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (btnRef.value) {
|
||||||
|
const clones = btnRef.value.querySelectorAll('.btn-char-clone')
|
||||||
|
const bg = btnRef.value.querySelector('.btn-bg-hover')
|
||||||
|
gsap.set(clones, { yPercent: 100 })
|
||||||
|
gsap.set(bg, { yPercent: 100 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLink :to="props.to" class="inline-flex items-center shrink-0">
|
||||||
|
<button
|
||||||
|
ref="btnRef"
|
||||||
|
@mouseenter="onHover"
|
||||||
|
@mouseleave="onLeave"
|
||||||
|
class="relative overflow-hidden px-6 py-3 md:px-7 md:py-3.5 rounded-[2rem] font-semibold text-sm md:text-[1rem] flex items-center justify-center group"
|
||||||
|
:class="[
|
||||||
|
props.theme === 'dark' ? 'bg-black text-white shadow-[0_15px_30px_-5px_rgba(0,0,0,0.3)]' : 'bg-[#1f1f1f] text-white',
|
||||||
|
$attrs.class
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 默认背景层,防止文字动画时漏底 -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-[2rem]"
|
||||||
|
:class="props.theme === 'dark' ? 'bg-black' : 'bg-[#1f1f1f]'"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Hover 时的背景色块层 -->
|
||||||
|
<div
|
||||||
|
class="btn-bg-hover absolute inset-0 rounded-[2rem] will-change-transform"
|
||||||
|
:class="props.theme === 'dark' ? 'bg-[#333333]' : 'bg-[#2f2f2f]'"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- 文本层 -->
|
||||||
|
<span class="relative inline-flex overflow-hidden z-10">
|
||||||
|
<span class="inline-flex">
|
||||||
|
<span
|
||||||
|
v-for="(char, index) in props.text.split('')"
|
||||||
|
:key="`orig-${index}`"
|
||||||
|
class="btn-char-orig block will-change-transform"
|
||||||
|
v-html="char === ' ' ? ' ' : char"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
<span class="absolute inset-0 inline-flex" aria-hidden="true">
|
||||||
|
<span
|
||||||
|
v-for="(char, index) in props.text.split('')"
|
||||||
|
:key="`clone-${index}`"
|
||||||
|
class="btn-char-clone block will-change-transform"
|
||||||
|
v-html="char === ' ' ? ' ' : char"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import gsap from 'gsap'
|
||||||
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const logoRef = ref<HTMLElement | null>(null)
|
||||||
|
let logoObserver: ReturnType<typeof ScrollTrigger.observe> | null = null
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoHover = () => {
|
||||||
|
if (!logoRef.value) return
|
||||||
|
|
||||||
|
logoObserver?.kill()
|
||||||
|
|
||||||
|
const firstChild = logoRef.value.children[0] as HTMLElement
|
||||||
|
const secondChild = logoRef.value.children[1] as HTMLElement
|
||||||
|
|
||||||
|
if (!firstChild || !secondChild) return
|
||||||
|
|
||||||
|
gsap.set(secondChild, {
|
||||||
|
opacity: 0,
|
||||||
|
visibility: 'hidden',
|
||||||
|
y: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
logoObserver = ScrollTrigger.observe({
|
||||||
|
target: logoRef.value,
|
||||||
|
onHover: (self) => {
|
||||||
|
gsap.to(firstChild, {
|
||||||
|
y: -10,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power2.out',
|
||||||
|
})
|
||||||
|
gsap.to(secondChild, {
|
||||||
|
y: 10,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power2.out',
|
||||||
|
opacity: 1,
|
||||||
|
visibility: 'visible',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onHoverEnd: (self) => {
|
||||||
|
gsap.to(firstChild, {
|
||||||
|
y: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power2.out',
|
||||||
|
})
|
||||||
|
gsap.to(secondChild, {
|
||||||
|
y: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: 'power2.out',
|
||||||
|
opacity: 0,
|
||||||
|
visibility: 'hidden',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
logoHover()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
logoObserver?.kill()
|
||||||
|
if (logoRef.value) {
|
||||||
|
gsap.killTweensOf([logoRef.value.children[0], logoRef.value.children[1]])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center cursor-pointer gap-2" @click="handleClick">
|
||||||
|
<div class="logo-icon flex h-[1.8rem] w-[1.8rem] items-center justify-center rounded-full bg-black overflow-hidden">
|
||||||
|
<video
|
||||||
|
src="/assets/logo.mp4"
|
||||||
|
class="h-[220%] w-[220%] object-cover pointer-events-none"
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsinline
|
||||||
|
disablepictureinpicture
|
||||||
|
disableremoteplayback
|
||||||
|
controlslist="nodownload noplaybackrate noremoteplayback"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div ref="logoRef" class="relative flex items-center overflow-hidden min-w-[70px] md:min-w-[100px] h-10">
|
||||||
|
<span class="absolute left-0 text-[20px] font-bold leading-none tracking-wider text-white">{{ t('app.brand') }}</span>
|
||||||
|
<span class="absolute left-0 text-[12px] font-bold leading-none text-white/60 opacity-0 invisible">{{ t('app.name') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,7 @@ import { onMounted, onBeforeUnmount, ref } from 'vue'
|
|||||||
import { useNuxtApp } from '#app'
|
import { useNuxtApp } from '#app'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
|
import AnimatedButton from '~/components/common/AnimatedButton.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -82,6 +83,13 @@ const play = () => new Promise<void>((resolve) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 设置页面向下滚动时的视差缩放逻辑(绑定到 Lenis 的平滑滚动数值上)
|
// 设置页面向下滚动时的视差缩放逻辑(绑定到 Lenis 的平滑滚动数值上)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 组件挂载时,先将整个 Hero 强行置于屏幕下方,等待外部调用 `play()` 才滑入
|
||||||
|
if (heroRef.value) gsap.set(heroRef.value, { yPercent: 100 })
|
||||||
|
// 初始化滚动监听逻辑
|
||||||
|
setupHeroScroll()
|
||||||
|
})
|
||||||
const setupHeroScroll = () => {
|
const setupHeroScroll = () => {
|
||||||
if (!heroStageRef.value || !heroPanelRef.value) return
|
if (!heroStageRef.value || !heroPanelRef.value) return
|
||||||
|
|
||||||
@@ -126,44 +134,7 @@ const setupHeroScroll = () => {
|
|||||||
cleanupHeroScroll = () => gsap.ticker.remove(updateHeroPanelScale)
|
cleanupHeroScroll = () => gsap.ticker.remove(updateHeroPanelScale)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTalkHover = () => {
|
// 设置页面向下滚动时的视差缩放逻辑(绑定到 Lenis 的平滑滚动数值上)
|
||||||
if (!talkBtnRef.value) return
|
|
||||||
const origs = talkBtnRef.value.querySelectorAll('.btn-char-orig')
|
|
||||||
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
|
|
||||||
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
|
|
||||||
|
|
||||||
// 背景从底部滑入 (yPercent: 100 -> 0)
|
|
||||||
gsap.to(bg, { yPercent: 0, duration: 0.4, ease: 'power3.out', overwrite: true })
|
|
||||||
|
|
||||||
gsap.to(origs, { yPercent: -100, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
|
|
||||||
gsap.to(clones, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTalkLeave = () => {
|
|
||||||
if (!talkBtnRef.value) return
|
|
||||||
const origs = talkBtnRef.value.querySelectorAll('.btn-char-orig')
|
|
||||||
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
|
|
||||||
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
|
|
||||||
|
|
||||||
// 背景向下反向滑出回收 (yPercent: 0 -> 100)
|
|
||||||
gsap.to(bg, { yPercent: 100, duration: 0.4, ease: 'power3.out', overwrite: true })
|
|
||||||
|
|
||||||
gsap.to(origs, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
|
|
||||||
gsap.to(clones, { yPercent: 100, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (talkBtnRef.value) {
|
|
||||||
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
|
|
||||||
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
|
|
||||||
gsap.set(clones, { yPercent: 100 })
|
|
||||||
gsap.set(bg, { yPercent: 100 })
|
|
||||||
}
|
|
||||||
// 组件挂载时,先将整个 Hero 强行置于屏幕下方,等待外部调用 `play()` 才滑入
|
|
||||||
if (heroRef.value) gsap.set(heroRef.value, { yPercent: 100 })
|
|
||||||
// 初始化滚动监听逻辑
|
|
||||||
setupHeroScroll()
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
cleanupHeroScroll?.()
|
cleanupHeroScroll?.()
|
||||||
|
|||||||
@@ -1,5 +1,208 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { onMounted, ref, onUnmounted } from 'vue'
|
||||||
|
import { gsap } from 'gsap'
|
||||||
|
import { ScrollTrigger } from 'gsap/ScrollTrigger'
|
||||||
|
import FooterLogo from '~/components/common/FooterLogo.vue'
|
||||||
|
import AnimatedButton from '~/components/common/AnimatedButton.vue'
|
||||||
|
|
||||||
|
gsap.registerPlugin(ScrollTrigger)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const footerRef = ref<HTMLElement | null>(null)
|
||||||
|
let ctx: gsap.Context
|
||||||
|
|
||||||
|
const pagesLinks = [
|
||||||
|
{ name: 'nav.home', url: '/' },
|
||||||
|
{ name: 'nav.studio', url: '/studio' },
|
||||||
|
{ name: 'nav.work', url: '/work' },
|
||||||
|
{ name: 'nav.news', url: '/news' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const onTalkHover = () => {
|
||||||
|
if (!talkBtnRef.value) return
|
||||||
|
const origs = talkBtnRef.value.querySelectorAll('.btn-char-orig')
|
||||||
|
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
|
||||||
|
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
|
||||||
|
|
||||||
|
// 背景从底部滑入 (yPercent: 100 -> 0)
|
||||||
|
gsap.to(bg, { yPercent: 0, duration: 0.4, ease: 'power3.out', overwrite: true })
|
||||||
|
|
||||||
|
gsap.to(origs, { yPercent: -100, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
|
||||||
|
gsap.to(clones, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'start' }, ease: 'power3.out', overwrite: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTalkLeave = () => {
|
||||||
|
if (!talkBtnRef.value) return
|
||||||
|
const origs = talkBtnRef.value.querySelectorAll('.btn-char-orig')
|
||||||
|
const clones = talkBtnRef.value.querySelectorAll('.btn-char-clone')
|
||||||
|
const bg = talkBtnRef.value.querySelector('.btn-bg-hover')
|
||||||
|
|
||||||
|
// 背景向下反向滑出回收 (yPercent: 0 -> 100)
|
||||||
|
gsap.to(bg, { yPercent: 100, duration: 0.4, ease: 'power3.out', overwrite: true })
|
||||||
|
|
||||||
|
gsap.to(origs, { yPercent: 0, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
|
||||||
|
gsap.to(clones, { yPercent: 100, duration: 0.4, stagger: { amount: 0.2, from: 'end' }, ease: 'power3.out', overwrite: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!footerRef.value) return
|
||||||
|
|
||||||
|
ctx = gsap.context(() => {
|
||||||
|
const items = gsap.utils.toArray<HTMLElement>('.animated-item')
|
||||||
|
|
||||||
|
gsap.from(items, {
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: footerRef.value,
|
||||||
|
start: 'top 80%',
|
||||||
|
toggleActions: 'play none none reverse',
|
||||||
|
},
|
||||||
|
y: 20,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.6,
|
||||||
|
stagger: 0.1,
|
||||||
|
ease: 'power3.out'
|
||||||
|
})
|
||||||
|
}, footerRef.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
ctx?.revert()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer>
|
<footer ref="footerRef" class="bg-black text-white px-6 py-16 md:px-12 md:py-24 overflow-hidden">
|
||||||
<h1>Arctic New One Footer</h1>
|
<div class="max-w-[1440px] mx-auto">
|
||||||
|
<!-- Top Section -->
|
||||||
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-end gap-8 mb-16">
|
||||||
|
<h2 class="text-3xl md:text-4xl lg:text-[44px] font-normal tracking-normal !leading-[1.4] max-w-none whitespace-pre-line">
|
||||||
|
{{ t('footer.title') }}
|
||||||
|
</h2>
|
||||||
|
<div class="inline-flex items-center shrink-0">
|
||||||
|
<AnimatedButton
|
||||||
|
:text="t('footer.collaborate')"
|
||||||
|
to="/contact"
|
||||||
|
theme="light"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<hr class="border-white/10 w-full mb-16" />
|
||||||
|
|
||||||
|
<!-- Bottom Section -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2">
|
||||||
|
|
||||||
|
<!-- Left Column -->
|
||||||
|
<div class="flex flex-col justify-between lg:border-r border-white/10 lg:pr-16 pb-16 lg:pb-0 border-b lg:border-b-0 mb-16 lg:mb-0">
|
||||||
|
<div>
|
||||||
|
<FooterLogo class="mb-16 animated-item justify-start" />
|
||||||
|
|
||||||
|
<h3 class="text-2xl font-medium mb-6 animated-item">{{ t('footer.newsletterTitle') }}</h3>
|
||||||
|
|
||||||
|
<form @submit.prevent class="relative w-full max-w-sm mb-4 animated-item">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
:placeholder="t('footer.emailPlaceholder')"
|
||||||
|
class="w-full h-[64px] bg-[#1f1f1f] border border-white/10 rounded-full pl-6 pr-[120px] text-sm text-white placeholder-white/40 focus:outline-none focus:bg-[#2f2f2f] transition-colors"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="absolute right-2 top-2 bottom-2 bg-black text-white text-sm px-6 rounded-full font-medium hover:opacity-75 transition-opacity"
|
||||||
|
>
|
||||||
|
{{ t('footer.subscribe') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="text-xs text-white/40 flex items-center gap-2 animated-item">
|
||||||
|
<span class="w-1.5 h-1.5 bg-white rounded-full inline-block shrink-0"></span>
|
||||||
|
{{ t('footer.newsletterNote') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-24 lg:mt-0 flex items-center gap-2 animated-item">
|
||||||
|
<div class="text-[16px] font-bold text-white/30 font-sans">
|
||||||
|
{{ t('footer.copyright') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<div class="flex flex-col gap-[50px] lg:pl-16">
|
||||||
|
|
||||||
|
<!-- Links Grid (Top Half) -->
|
||||||
|
<div>
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li v-for="link in pagesLinks" :key="link.name" class="animated-item">
|
||||||
|
<NuxtLink :to="link.url" class="group flex items-center overflow-hidden text-[16px] font-bold text-white/80 hover:text-white transition-colors" style="justify-content: flex-start;">
|
||||||
|
<div class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[21px] pr-[21px]">
|
||||||
|
<span class="mr-[10px] h-[11px] w-[11px] rounded-full bg-white shrink-0"></span>
|
||||||
|
<span>{{ t(link.name) }}</span>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact / Location / Social Grid (Bottom Half) -->
|
||||||
|
<!-- Contacts Row -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-8">
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="animated-item">
|
||||||
|
<a href="mailto:chenchun@virtheart.com" class="group flex items-center overflow-hidden text-[16px] font-bold text-white/80 hover:text-white transition-colors" style="justify-content: flex-start;">
|
||||||
|
<div class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[21px] pr-[21px]">
|
||||||
|
<span class="mr-[10px] h-[11px] w-[11px] rounded-full bg-white shrink-0"></span>
|
||||||
|
<span>chenchun@virtheart.com</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="animated-item">
|
||||||
|
<span class="group flex items-center overflow-hidden text-[16px] font-bold text-white/80 transition-colors cursor-default" style="justify-content: flex-start;">
|
||||||
|
<div class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[21px] pr-[21px]">
|
||||||
|
<span class="mr-[10px] h-[11px] w-[11px] rounded-full bg-white shrink-0"></span>
|
||||||
|
<span>{{ t('footer.wechat') }}: LucskyNew</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="space-y-3">
|
||||||
|
<li class="animated-item">
|
||||||
|
<a href="mailto:info@virtheart.com" class="group flex items-center overflow-hidden text-[16px] font-bold text-white/80 hover:text-white transition-colors" style="justify-content: flex-start;">
|
||||||
|
<div class="flex items-center transition-transform duration-300 group-hover:translate-x-0 -translate-x-[21px] pr-[21px]">
|
||||||
|
<span class="mr-[10px] h-[11px] w-[11px] rounded-full bg-white shrink-0"></span>
|
||||||
|
<span>info@virtheart.com</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Address & Social Row -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-8 items-start">
|
||||||
|
<div class="group flex items-start overflow-hidden text-[16px] font-bold text-white/80 hover:text-white transition-colors animated-item cursor-default" style="justify-content: flex-start;">
|
||||||
|
<div class="flex items-start transition-transform duration-300 group-hover:translate-x-0 -translate-x-[21px] pr-[21px]">
|
||||||
|
<span class="mr-[10px] mt-[6px] h-[11px] w-[11px] rounded-full bg-white shrink-0"></span>
|
||||||
|
<span class="leading-relaxed whitespace-pre-line">{{ t('footer.address') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 animated-item">
|
||||||
|
<a href="#" class="w-10 h-10 rounded-full bg-[#1f1f1f] hover:bg-[#2f2f2f] flex items-center justify-center transition-colors text-white">
|
||||||
|
<Icon name="ph:instagram-logo" class="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
<a href="#" class="w-10 h-10 rounded-full bg-[#1f1f1f] hover:bg-[#2f2f2f] flex items-center justify-center transition-colors text-white">
|
||||||
|
<Icon name="pajamas:twitter-x" class="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
<a href="#" class="w-10 h-10 rounded-full bg-[#1f1f1f] hover:bg-[#2f2f2f] flex items-center justify-center transition-colors text-white">
|
||||||
|
<Icon name="ph:diamond-fill" class="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,5 +25,16 @@
|
|||||||
},
|
},
|
||||||
"description": "No cookie cutter sites. No empty claims. Only practical tools and smart strategies that drive growth and build brands.",
|
"description": "No cookie cutter sites. No empty claims. Only practical tools and smart strategies that drive growth and build brands.",
|
||||||
"letsTalk": "Let's talk"
|
"letsTalk": "Let's talk"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"title": "Crafting visuals. Shaping stories.\nLet's create great work together!",
|
||||||
|
"collaborate": "Let's Collaborate",
|
||||||
|
"newsletterTitle": "Be the first to know what's new.",
|
||||||
|
"emailPlaceholder": "E-mail",
|
||||||
|
"subscribe": "Subscribe",
|
||||||
|
"newsletterNote": "No noise. Just curated updates.",
|
||||||
|
"copyright": "© 2026 ArcticNewOne Studio",
|
||||||
|
"address": "Binhe West Road, Qixingguan District, Bijie City, Guizhou Province",
|
||||||
|
"wechat": "WeChat"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,5 +25,16 @@
|
|||||||
},
|
},
|
||||||
"description": "拒绝千篇一律的网站。拒绝空洞的承诺。只提供实用的工具和明智的策略,以推动增长并建立品牌。",
|
"description": "拒绝千篇一律的网站。拒绝空洞的承诺。只提供实用的工具和明智的策略,以推动增长并建立品牌。",
|
||||||
"letsTalk": "联系我们"
|
"letsTalk": "联系我们"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"title": "雕琢视觉,塑造故事。\n让我们共创卓越作品!",
|
||||||
|
"collaborate": "联系合作",
|
||||||
|
"newsletterTitle": "抢先获取最新资讯",
|
||||||
|
"emailPlaceholder": "输入您的邮箱",
|
||||||
|
"subscribe": "订阅",
|
||||||
|
"newsletterNote": "无广告干扰。仅提供精选更新。",
|
||||||
|
"copyright": "© 2026 北极新一工作室",
|
||||||
|
"address": "贵州省毕节市七星关区滨河西路",
|
||||||
|
"wechat": "微信"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user