3256ed8ff4
在页脚添加工具页面链接,并优化动画效果使用GSAP实现更流畅的过渡。同时为工具数据添加类型定义以提高代码可维护性。 - 在ArcticNewOneFooter.vue中添加工具链接 - 优化GSAP动画配置,使用fromTo实现更精细控制 - 为tools.vue中的工具数据添加ToolItem接口类型 - 添加CSS类实现动画元素的初始状态
165 lines
5.2 KiB
Vue
165 lines
5.2 KiB
Vue
<template>
|
|
<main class="page-container min-h-screen pt-32 pb-20 px-4 md:px-8 mx-auto max-w-[1200px]">
|
|
<div
|
|
ref="headerWrapRef"
|
|
class="flex flex-col items-center justify-center text-center mb-10 md:mb-16"
|
|
style="opacity: 0; visibility: hidden;"
|
|
>
|
|
<h1 ref="titleRef" class="text-3xl md:text-5xl font-bold mb-4 md:mb-6 tracking-tight text-black">{{ t('tools.title') }}</h1>
|
|
<p ref="descRef" class="text-base md:text-lg text-gray-600 max-w-2xl">{{ t('tools.description') }}</p>
|
|
</div>
|
|
|
|
<div
|
|
ref="cardsWrapRef"
|
|
class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 lg:gap-8"
|
|
style="opacity: 0; visibility: hidden;"
|
|
>
|
|
<a
|
|
v-for="tool in toolsData"
|
|
:key="tool.id"
|
|
:href="tool.url"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="tool-card group relative flex flex-col p-5 md:p-8 rounded-2xl md:rounded-3xl bg-white border border-gray-100 shadow-sm hover:shadow-xl transition-all duration-500 overflow-hidden cursor-pointer hover:-translate-y-2"
|
|
>
|
|
<div
|
|
class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 bg-gradient-to-br"
|
|
:class="tool.color"
|
|
></div>
|
|
|
|
<div class="relative z-10 flex flex-col h-full">
|
|
<div class="flex items-center justify-between mb-6 md:mb-8">
|
|
<div class="w-12 h-12 md:w-14 md:h-14 rounded-2xl flex items-center justify-center bg-gray-50 text-black group-hover:bg-white group-hover:shadow-sm transition-all duration-300">
|
|
<Icon :name="tool.icon" class="w-6 h-6 md:w-8 md:h-8" />
|
|
</div>
|
|
<div class="w-9 h-9 md:w-10 md:h-10 rounded-full flex items-center justify-center bg-gray-50 text-gray-400 group-hover:bg-black group-hover:text-white transition-all duration-300">
|
|
<Icon name="ph:arrow-up-right" class="w-4 h-4 md:w-5 md:h-5" />
|
|
</div>
|
|
</div>
|
|
|
|
<h3 class="text-lg md:text-2xl font-bold mb-2 md:mb-3 text-black">{{ tool.name }}</h3>
|
|
<p class="text-sm md:text-base text-gray-600 leading-snug md:leading-relaxed flex-grow">{{ t(`tools.items.${tool.id}`) }}</p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useSeoMeta } from '#imports'
|
|
import gsap from 'gsap'
|
|
import { useAppStore } from '~/stores/app'
|
|
|
|
const { t } = useI18n()
|
|
const appStore = useAppStore()
|
|
|
|
const headerWrapRef = ref<HTMLElement | null>(null)
|
|
const titleRef = ref<HTMLElement | null>(null)
|
|
const descRef = ref<HTMLElement | null>(null)
|
|
const cardsWrapRef = ref<HTMLElement | null>(null)
|
|
let cardsCtx: gsap.Context | null = null
|
|
|
|
useSeoMeta({
|
|
title: () => t('tools.title'),
|
|
description: () => t('tools.description'),
|
|
})
|
|
|
|
const playPageIntro = async () => {
|
|
await nextTick()
|
|
if (!headerWrapRef.value || !titleRef.value || !descRef.value || !cardsWrapRef.value) return
|
|
|
|
cardsCtx?.revert()
|
|
|
|
cardsCtx = gsap.context(() => {
|
|
const cards = gsap.utils.toArray<HTMLElement>('.tool-card', cardsWrapRef.value!)
|
|
|
|
gsap.delayedCall(0, () => {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } })
|
|
|
|
tl.set(headerWrapRef.value!, { autoAlpha: 1 })
|
|
tl.fromTo(titleRef.value!, { autoAlpha: 0, y: 20 }, { autoAlpha: 1, y: 0, duration: 0.85, ease: 'power4.out', clearProps: 'transform' })
|
|
tl.fromTo(descRef.value!, { autoAlpha: 0, y: 16 }, { autoAlpha: 1, y: 0, duration: 0.7, ease: 'power4.out', clearProps: 'transform' }, '-=0.45')
|
|
|
|
tl.set(cardsWrapRef.value!, { autoAlpha: 1 }, '-=0.05')
|
|
tl.fromTo(
|
|
cards,
|
|
{ autoAlpha: 0, y: 34, scale: 0.965 },
|
|
{
|
|
autoAlpha: 1,
|
|
y: 0,
|
|
scale: 1,
|
|
duration: 0.95,
|
|
ease: 'power4.out',
|
|
stagger: { each: 0.11, from: 'start' },
|
|
clearProps: 'transform',
|
|
},
|
|
'+=0.08'
|
|
)
|
|
|
|
tl.set([headerWrapRef.value!, cardsWrapRef.value!], { clearProps: 'opacity,visibility' })
|
|
})
|
|
})
|
|
})
|
|
}, headerWrapRef.value)
|
|
}
|
|
|
|
onMounted(() => {
|
|
const handleLoadingDone = () => {
|
|
playPageIntro()
|
|
}
|
|
|
|
if (!appStore.isAppLoading) {
|
|
playPageIntro()
|
|
return
|
|
}
|
|
|
|
window.addEventListener('app-loading-done', handleLoadingDone, { once: true })
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
cardsCtx?.revert()
|
|
})
|
|
|
|
interface ToolItem {
|
|
id: string
|
|
name: string
|
|
url: string
|
|
icon: string
|
|
color: string
|
|
}
|
|
|
|
const toolsData: ToolItem[] = [
|
|
{
|
|
id: 'drawio',
|
|
name: 'Drawio',
|
|
url: 'https://drawio.virtheart.com',
|
|
icon: 'ph:flow-arrow-duotone',
|
|
color: 'from-orange-50/50 to-amber-100/50'
|
|
},
|
|
{
|
|
id: 'excalidraw',
|
|
name: 'Excalidraw',
|
|
url: 'https://excalidraw.virtheart.com',
|
|
icon: 'ph:pencil-circle-duotone',
|
|
color: 'from-purple-50/50 to-indigo-100/50'
|
|
},
|
|
{
|
|
id: 'itTools',
|
|
name: 'IT-Tools',
|
|
url: 'https://ittools.virtheart.com',
|
|
icon: 'ph:wrench-duotone',
|
|
color: 'from-blue-50/50 to-cyan-100/50'
|
|
}
|
|
]
|
|
</script>
|
|
|
|
<style scoped>
|
|
p {
|
|
word-break: break-word;
|
|
}
|
|
</style>
|