本次提交主要进行了以下修改: 1. 在 `app.vue` 中引入了 `RouteLoader` 组件,以提升路由切换时的用户体验。 2. 在 `components` 目录下新增了 `RouteLoader.vue` 组件,负责显示路由加载进度条。 3. 新增了 `BackToTop.vue` 组件,提供返回顶部的功能,增强页面导航体验。 4. 更新了 `TheFooter.vue` 和 `TheHeader.vue`,分别引入返回顶部按钮和调整了样式以匹配新的设计。 5. 在 `nuxt.config.ts` 中添加了页面过渡效果配置,提升页面切换的流畅性。 6. 更新了样式文件,调整了颜色变量和过渡效果,确保一致性。 这些改动旨在提升用户体验和页面的可用性。master
<template> | <template> | ||||
<div> | <div> | ||||
<RouteLoader /> | |||||
<NuxtLayout> | <NuxtLayout> | ||||
<NuxtPage /> | <NuxtPage /> | ||||
</NuxtLayout> | </NuxtLayout> |
/* 颜色变量 */ | /* 颜色变量 */ | ||||
--color-primary: #3b82f6; | --color-primary: #3b82f6; | ||||
--color-secondary: #10b981; | --color-secondary: #10b981; | ||||
--color-accent: #f59e0b; | |||||
--color-accent: #22d3ee; | |||||
--color-danger: #ef4444; | --color-danger: #ef4444; | ||||
--color-success: #10b981; | --color-success: #10b981; | ||||
--color-warning: #f59e0b; | --color-warning: #f59e0b; | ||||
.prose details[open] summary { | .prose details[open] summary { | ||||
margin-bottom: 0.5rem; | margin-bottom: 0.5rem; | ||||
} | |||||
/* 页面过渡动画 */ | |||||
.page-enter-active, | |||||
.page-leave-active { | |||||
transition: all 0.3s; | |||||
} | |||||
.page-enter-from, | |||||
.page-leave-to { | |||||
opacity: 0; | |||||
transform: translateY(10px); | |||||
} | |||||
/* 加载指示器样式 */ | |||||
.route-loader { | |||||
position: fixed; | |||||
top: 0; | |||||
left: 0; | |||||
height: 3px; | |||||
background-color: var(--color-accent); | |||||
z-index: 9999; | |||||
transition: width 0.2s ease; | |||||
} | |||||
.route-loader-enter-active, | |||||
.route-loader-leave-active { | |||||
transition: opacity 0.3s; | |||||
} | |||||
.route-loader-enter-from, | |||||
.route-loader-leave-to { | |||||
opacity: 0; | |||||
} | } |
<template> | |||||
<div | |||||
v-show="showBackToTop" | |||||
@click="scrollToTop" | |||||
class="fixed right-5 bottom-5 bg-stone-800 hover:bg-stone-700 text-white w-10 h-10 rounded-full flex items-center justify-center cursor-pointer transition-all duration-300 z-50 shadow-lg" | |||||
:aria-label="t('common.backToTop')" | |||||
> | |||||
<svg | |||||
xmlns="http://www.w3.org/2000/svg" | |||||
class="h-5 w-5" | |||||
viewBox="0 0 20 20" | |||||
fill="currentColor" | |||||
> | |||||
<path | |||||
fill-rule="evenodd" | |||||
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" | |||||
clip-rule="evenodd" | |||||
/> | |||||
</svg> | |||||
</div> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* 返回顶部按钮组件 | |||||
* 当页面滚动超过一定距离时显示,点击后平滑滚动回顶部 | |||||
*/ | |||||
import { useI18n } from 'vue-i18n'; | |||||
const { t } = useI18n(); | |||||
const showBackToTop = ref(false); | |||||
const scrollThreshold = 300; // 显示按钮的滚动阈值 | |||||
/** | |||||
* 监听滚动事件,控制按钮显示 | |||||
*/ | |||||
function checkScroll() { | |||||
showBackToTop.value = window.scrollY > scrollThreshold; | |||||
} | |||||
/** | |||||
* 滚动到页面顶部 | |||||
*/ | |||||
function scrollToTop() { | |||||
window.scrollTo({ | |||||
top: 0, | |||||
behavior: "smooth", | |||||
}); | |||||
} | |||||
// 生命周期钩子 | |||||
onMounted(() => { | |||||
window.addEventListener("scroll", checkScroll); | |||||
checkScroll(); // 初始检查 | |||||
}); | |||||
onUnmounted(() => { | |||||
window.removeEventListener("scroll", checkScroll); | |||||
}); | |||||
</script> |
<template> | <template> | ||||
<div class="relative inline-block text-left" ref="dropdownContainerRef" @mouseleave="handleMouseLeave"> | |||||
<div class="relative inline-block text-left" ref="dropdownContainerRef"> | |||||
<div | <div | ||||
@mouseenter="handleMouseEnter" | |||||
@click="toggleDropdown" | |||||
class="flex items-center gap-1 text-white opacity-80 text-sm hover:opacity-100 cursor-pointer py-2 transition-opacity" | class="flex items-center gap-1 text-white opacity-80 text-sm hover:opacity-100 cursor-pointer py-2 transition-opacity" | ||||
> | > | ||||
<i class="icon-i18n mr-1"></i> | <i class="icon-i18n mr-1"></i> | ||||
<span>{{ currentLocaleName || "Language" }}</span> | <span>{{ currentLocaleName || "Language" }}</span> | ||||
<svg | <svg | ||||
class="h-3 w-3 text-white/60 transition-transform duration-200" | class="h-3 w-3 text-white/60 transition-transform duration-200" | ||||
:class="{'rotate-180': isDropdownOpen}" | |||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" | |||||
:class="{ 'rotate-180': isDropdownOpen }" | |||||
fill="none" | |||||
viewBox="0 0 24 24" | |||||
stroke="currentColor" | |||||
> | > | ||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M19 9l-7 7-7-7" /> | |||||
<path | |||||
stroke-linecap="round" | |||||
stroke-linejoin="round" | |||||
stroke-width="3" | |||||
d="M19 9l-7 7-7-7" | |||||
/> | |||||
</svg> | </svg> | ||||
</div> | </div> | ||||
<transition name="fade-down"> | <transition name="fade-down"> | ||||
<div | <div | ||||
v-if="isDropdownOpen" | v-if="isDropdownOpen" | ||||
@mouseenter="handleMouseEnter" | |||||
class="absolute right-0 top-full mt-1 w-max min-w-[120px] bg-slate-800/90 backdrop-blur-md rounded-lg shadow-xl p-2 z-10" | class="absolute right-0 top-full mt-1 w-max min-w-[120px] bg-slate-800/90 backdrop-blur-md rounded-lg shadow-xl p-2 z-10" | ||||
> | > | ||||
<ul class="space-y-1"> | <ul class="space-y-1"> | ||||
<button | <button | ||||
@click="selectLanguage(locale.code)" | @click="selectLanguage(locale.code)" | ||||
class="block w-full text-left text-base text-gray-200 hover:text-white hover:bg-white/10 transition-all duration-150 rounded px-3 py-1.5" | class="block w-full text-left text-base text-gray-200 hover:text-white hover:bg-white/10 transition-all duration-150 rounded px-3 py-1.5" | ||||
:class="[ locale.code === currentLocale ? 'font-bold opacity-100 bg-white/15' : '' ]" | |||||
:class="[ | |||||
locale.code === currentLocale | |||||
? 'font-bold opacity-100 bg-white/15' | |||||
: '', | |||||
]" | |||||
> | > | ||||
{{ locale.name }} | {{ locale.name }} | ||||
</button> | </button> | ||||
<script setup lang="ts"> | <script setup lang="ts"> | ||||
/** | /** | ||||
* 语言切换组件 - 下拉菜单样式 | * 语言切换组件 - 下拉菜单样式 | ||||
* 支持切换配置的语言 | |||||
* 支持切换配置的语言,同时支持移动端和桌面端 | |||||
*/ | */ | ||||
import { ref, computed } from "vue"; | |||||
import { useI18n } from "#imports"; // 修正 useI18n 导入 | |||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue"; | |||||
import { useI18n } from "#imports"; | |||||
// 定义语言代码的类型,应该与 i18n 配置中的一致 | // 定义语言代码的类型,应该与 i18n 配置中的一致 | ||||
type LocaleCode = "zh" | "en" | "ja"; // 你需要根据你的 i18n 配置更新这个类型 | type LocaleCode = "zh" | "en" | "ja"; // 你需要根据你的 i18n 配置更新这个类型 | ||||
const { locale, locales, setLocale } = useI18n(); | const { locale, locales, setLocale } = useI18n(); | ||||
const currentLocale = computed(() => locale.value); | const currentLocale = computed(() => locale.value); | ||||
const isDropdownOpen = ref(false); | const isDropdownOpen = ref(false); | ||||
const dropdownContainerRef = ref(null); // 保留 ref,虽然 onClickOutside 移除了,但未来可能有用 | |||||
let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay | |||||
const dropdownContainerRef = ref(null); | |||||
// 可用语言列表 | // 可用语言列表 | ||||
const availableLocales = computed(() => { | const availableLocales = computed(() => { | ||||
// 当前选中语言的名称 | // 当前选中语言的名称 | ||||
const currentLocaleName = computed(() => { | const currentLocaleName = computed(() => { | ||||
const current = availableLocales.value.find((l: { code: string; name: string }) => | |||||
l.code === locale.value | |||||
const current = availableLocales.value.find( | |||||
(l: { code: string; name: string }) => l.code === locale.value | |||||
); | ); | ||||
return current ? current.name : ""; | return current ? current.name : ""; | ||||
}); | }); | ||||
// 这里可以添加用户反馈,例如显示一个错误提示 | // 这里可以添加用户反馈,例如显示一个错误提示 | ||||
} | } | ||||
} | } | ||||
handleMouseLeave(); // 使用 handleMouseLeave 关闭 | |||||
closeDropdown(); | |||||
} | } | ||||
// --- Dropdown Logic (like Products dropdown) --- | |||||
function handleMouseEnter() { | |||||
if (leaveTimeout) { | |||||
clearTimeout(leaveTimeout); | |||||
leaveTimeout = null; | |||||
} | |||||
isDropdownOpen.value = true; | |||||
/** | |||||
* 切换下拉菜单的显示状态 | |||||
*/ | |||||
function toggleDropdown() { | |||||
isDropdownOpen.value = !isDropdownOpen.value; | |||||
} | |||||
/** | |||||
* 关闭下拉菜单 | |||||
*/ | |||||
function closeDropdown() { | |||||
isDropdownOpen.value = false; | |||||
} | } | ||||
function handleMouseLeave() { | |||||
// Delay closing the dropdown slightly | |||||
leaveTimeout = setTimeout(() => { | |||||
isDropdownOpen.value = false; | |||||
}, 150); // 150ms delay | |||||
/** | |||||
* 处理点击外部区域时关闭下拉菜单 | |||||
*/ | |||||
function handleClickOutside(event: MouseEvent) { | |||||
if ( | |||||
dropdownContainerRef.value && | |||||
!(dropdownContainerRef.value as HTMLElement).contains( | |||||
event.target as Node | |||||
) && | |||||
isDropdownOpen.value | |||||
) { | |||||
closeDropdown(); | |||||
} | |||||
} | } | ||||
// --- End Dropdown Logic --- | |||||
// 监听全局点击事件,用于关闭下拉菜单 | |||||
onMounted(() => { | |||||
document.addEventListener("click", handleClickOutside); | |||||
}); | |||||
onBeforeUnmount(() => { | |||||
document.removeEventListener("click", handleClickOutside); | |||||
}); | |||||
</script> | </script> |
<template> | |||||
<Transition name="route-loader"> | |||||
<div v-if="isLoading" class="route-loader" :style="{ width: `${progress}%` }"></div> | |||||
</Transition> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
/** | |||||
* 路由加载指示器组件 | |||||
* 在路由切换时显示顶部进度条,提升用户体验 | |||||
*/ | |||||
import { ref, onMounted, onUnmounted } from 'vue'; | |||||
import { useRouter } from 'vue-router'; | |||||
// 加载状态和进度 | |||||
const isLoading = ref(false); | |||||
const progress = ref(0); | |||||
const timer = ref<NodeJS.Timeout | null>(null); | |||||
const progressTimer = ref<NodeJS.Timeout | null>(null); | |||||
// 获取路由器实例 | |||||
const router = useRouter(); | |||||
/** | |||||
* 开始加载动画 | |||||
* 显示进度条并逐步增加进度 | |||||
*/ | |||||
function startLoading() { | |||||
// 重置并显示加载条 | |||||
isLoading.value = true; | |||||
progress.value = 0; | |||||
// 清除之前可能存在的计时器 | |||||
if (progressTimer.value) clearInterval(progressTimer.value); | |||||
// 创建随机增长的进度条,最多到90% | |||||
progressTimer.value = setInterval(() => { | |||||
// 根据当前进度计算下一步增长量,进度越高增长越慢 | |||||
const remaining = 100 - progress.value; | |||||
const increment = remaining * 0.1 * Math.random(); | |||||
// 增加进度,但确保不超过90% | |||||
progress.value = Math.min(progress.value + increment, 90); | |||||
}, 300); | |||||
} | |||||
/** | |||||
* 完成加载动画 | |||||
* 将进度迅速增加到100%,然后隐藏进度条 | |||||
*/ | |||||
function completeLoading() { | |||||
// 清除进度增长定时器 | |||||
if (progressTimer.value) { | |||||
clearInterval(progressTimer.value); | |||||
progressTimer.value = null; | |||||
} | |||||
// 完成加载动画,设置100% | |||||
progress.value = 100; | |||||
// 设置延时以显示完成状态 | |||||
if (timer.value) clearTimeout(timer.value); | |||||
timer.value = setTimeout(() => { | |||||
isLoading.value = false; | |||||
progress.value = 0; | |||||
}, 300); | |||||
} | |||||
/** | |||||
* 监听路由事件 | |||||
* 在路由切换开始和结束时触发相应的动画 | |||||
*/ | |||||
onMounted(() => { | |||||
// 路由开始切换 | |||||
router.beforeEach(() => { | |||||
startLoading(); | |||||
return true; | |||||
}); | |||||
// 路由切换完成 | |||||
router.afterEach(() => { | |||||
completeLoading(); | |||||
}); | |||||
// 路由切换出错 | |||||
router.onError(() => { | |||||
completeLoading(); | |||||
}); | |||||
}); | |||||
/** | |||||
* 组件卸载时清理计时器 | |||||
*/ | |||||
onUnmounted(() => { | |||||
if (timer.value) clearTimeout(timer.value); | |||||
if (progressTimer.value) clearInterval(progressTimer.value); | |||||
}); | |||||
</script> |
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 返回顶部按钮 --> | |||||
<BackToTop /> | |||||
</footer> | </footer> | ||||
</template> | </template> | ||||
:placeholder=" | :placeholder=" | ||||
t('common.searchPlaceholder') || 'Enter search term...' | t('common.searchPlaceholder') || 'Enter search term...' | ||||
" | " | ||||
class="w-full p-3 pl-10 pr-10 rounded bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" | |||||
class="w-full p-3 pl-10 pr-10 rounded bg-slate-700 text-white border border-slate-600 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500" | |||||
@input="handleSearch" | @input="handleSearch" | ||||
/> | /> | ||||
<!-- Clear Icon --> | <!-- Clear Icon --> | ||||
@click="clearSearch" | @click="clearSearch" | ||||
> | > | ||||
<button | <button | ||||
class="flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-blue-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" | |||||
class="flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-cyan-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500" | |||||
:aria-label="t('common.clear')" | :aria-label="t('common.clear')" | ||||
tabindex="0" | tabindex="0" | ||||
type="button" | type="button" | ||||
</div> | </div> | ||||
<div | <div | ||||
v-if="searchResults.length > 0" | v-if="searchResults.length > 0" | ||||
class="mt-4 max-h-[60vh] overflow-y-auto pr-2 space-y-4 search-results" | |||||
class="mt-4 max-h-[50vh] overflow-y-auto pr-2 space-y-4 search-results" | |||||
> | > | ||||
<div | <div | ||||
v-for="result in searchResults" | v-for="result in searchResults" | ||||
@click="closeSearch" | @click="closeSearch" | ||||
> | > | ||||
<div | <div | ||||
class="text-white font-medium mb-1 group-hover:text-blue-400 transition-colors" | |||||
class="text-white font-medium mb-1 group-hover:text-cyan-400 transition-colors" | |||||
v-html="highlightText(result.title, searchQuery)" | v-html="highlightText(result.title, searchQuery)" | ||||
></div> | ></div> | ||||
<div | <div | ||||
v-for="keyword in hotKeywords" | v-for="keyword in hotKeywords" | ||||
:key="keyword" | :key="keyword" | ||||
@click="searchHotKeyword(keyword)" | @click="searchHotKeyword(keyword)" | ||||
class="px-4 py-1.5 bg-slate-700 text-white/80 rounded-full text-sm hover:bg-blue-600 hover:text-white transition-colors duration-200" | |||||
class="px-4 py-1.5 bg-slate-700 text-white/80 rounded-full text-sm hover:bg-cyan-600 hover:text-white transition-colors duration-200" | |||||
> | > | ||||
{{ keyword }} | {{ keyword }} | ||||
</button> | </button> |
clear: "Clear", | clear: "Clear", | ||||
noResults: "No results found", | noResults: "No results found", | ||||
searching: "Searching...", | searching: "Searching...", | ||||
backToTop: "Back to Top", | |||||
breadcrumb: { | breadcrumb: { | ||||
home: "Home", | home: "Home", | ||||
privacy: "Privacy Policy", | privacy: "Privacy Policy", |
clear: "クリア", | clear: "クリア", | ||||
noResults: "関連する結果はありません", | noResults: "関連する結果はありません", | ||||
searching: "検索中...", | searching: "検索中...", | ||||
backToTop: "トップに戻る", | |||||
breadcrumb: { | breadcrumb: { | ||||
home: "ホーム", | home: "ホーム", | ||||
privacy: "プライバシーポリシー", | privacy: "プライバシーポリシー", |
clear: "清除", | clear: "清除", | ||||
noResults: "没有找到相关结果", | noResults: "没有找到相关结果", | ||||
searching: "搜索中...", | searching: "搜索中...", | ||||
backToTop: "返回顶部", | |||||
footer: { | footer: { | ||||
productsLinks: { | productsLinks: { | ||||
title: "产品", | title: "产品", |
renderJsonPayloads: false, | renderJsonPayloads: false, | ||||
}, | }, | ||||
devServer: { | |||||
host: "0.0.0.0", | |||||
}, | |||||
compatibilityDate: "2025-05-07", | |||||
// 页面过渡效果配置 | |||||
app: { | app: { | ||||
head: { | head: { | ||||
charset: "utf-8", | charset: "utf-8", | ||||
viewport: "width=device-width, initial-scale=1", | viewport: "width=device-width, initial-scale=1", | ||||
}, | }, | ||||
pageTransition: { | |||||
name: "page", | |||||
mode: "out-in", | |||||
}, | |||||
}, | |||||
// 路由配置 | |||||
router: { | |||||
options: { | |||||
linkActiveClass: "active", | |||||
}, | |||||
}, | |||||
devServer: { | |||||
host: "0.0.0.0", | |||||
}, | }, | ||||
compatibilityDate: "2025-05-07", | |||||
}); | }); |
<div v-if="isLoading" class="flex justify-center py-12"> | <div v-if="isLoading" class="flex justify-center py-12"> | ||||
<!-- 加载中 --> | <!-- 加载中 --> | ||||
<div | <div | ||||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
{{ t("about.companyInfo.name") }} | {{ t("about.companyInfo.name") }} | ||||
</h1> | </h1> | ||||
<div | <div | ||||
class="text-stone-400 text-xl leading-relaxed text-center max-w-2xl break-words whitespace-pre-wrap" | |||||
class="text-[#71717A] text-xl leading-relaxed text-center max-w-2xl break-words whitespace-pre-wrap" | |||||
> | > | ||||
{{ t("about.companyInfo.description") }} | {{ t("about.companyInfo.description") }} | ||||
</div> | </div> | ||||
> | > | ||||
{{ t("about.overview.companyInfo") }} | {{ t("about.overview.companyInfo") }} | ||||
<span | <span | ||||
class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full" | |||||
class="absolute -bottom-2 left-0 w-12 h-1 bg-cyan-400 rounded-full" | |||||
></span> | ></span> | ||||
</h2> | </h2> | ||||
<div class="flex flex-col gap-3"> | <div class="flex flex-col gap-3"> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.companyName") | t("about.overview.companyName") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.englishName") | t("about.overview.englishName") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.established") | t("about.overview.established") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.ceo") | t("about.overview.ceo") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.employees") | t("about.overview.employees") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.location") | t("about.overview.location") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
> | > | ||||
{{ t("about.overview.philosophy") }} | {{ t("about.overview.philosophy") }} | ||||
<span | <span | ||||
class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full" | |||||
class="absolute -bottom-2 left-0 w-12 h-1 bg-cyan-400 rounded-full" | |||||
></span> | ></span> | ||||
</h2> | </h2> | ||||
<div class="text-stone-400 text-lg leading-relaxed"> | |||||
<div class="text-[#71717A] text-lg leading-relaxed"> | |||||
{{ t("about.companyInfo.philosophy") }} | {{ t("about.companyInfo.philosophy") }} | ||||
</div> | </div> | ||||
</div> | </div> | ||||
> | > | ||||
{{ t("about.overview.contact") }} | {{ t("about.overview.contact") }} | ||||
<span | <span | ||||
class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full" | |||||
class="absolute -bottom-2 left-0 w-12 h-1 bg-cyan-400 rounded-full" | |||||
></span> | ></span> | ||||
</h2> | </h2> | ||||
<div class="flex flex-col gap-3"> | <div class="flex flex-col gap-3"> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.email") | t("about.overview.email") | ||||
}}</span> | }}</span> | ||||
<a | <a | ||||
:href="'mailto:' + companyInfo.email" | :href="'mailto:' + companyInfo.email" | ||||
class="text-white text-lg font-bold hover:text-blue-400 transition-colors" | |||||
class="text-white text-lg font-bold hover:text-cyan-400 transition-colors" | |||||
>{{ companyInfo.email }}</a | >{{ companyInfo.email }}</a | ||||
> | > | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.tel") | t("about.overview.tel") | ||||
}}</span> | }}</span> | ||||
<a | <a | ||||
:href="'tel:' + companyInfo.tel.replace(/[^0-9]/g, '')" | :href="'tel:' + companyInfo.tel.replace(/[^0-9]/g, '')" | ||||
class="text-white text-lg font-bold hover:text-blue-400 transition-colors" | |||||
class="text-white text-lg font-bold hover:text-cyan-400 transition-colors" | |||||
>{{ companyInfo.tel }}</a | >{{ companyInfo.tel }}</a | ||||
> | > | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.fax") | t("about.overview.fax") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1"> | <div class="flex flex-col gap-1"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.businessHours") | t("about.overview.businessHours") | ||||
}}</span> | }}</span> | ||||
<span class="text-white text-lg font-bold">{{ | <span class="text-white text-lg font-bold">{{ | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-1 mt-2"> | <div class="flex flex-col gap-1 mt-2"> | ||||
<span class="text-stone-400 text-base">{{ | |||||
<span class="text-[#71717A] text-base">{{ | |||||
t("about.overview.businessActivities") | t("about.overview.businessActivities") | ||||
}}</span> | }}</span> | ||||
<div class="text-white text-base font-bold space-y-1"> | <div class="text-white text-base font-bold space-y-1"> |
<ErrorBoundary :error="error"> | <ErrorBoundary :error="error"> | ||||
<div v-if="isLoading" class="flex justify-center py-12"> | <div v-if="isLoading" class="flex justify-center py-12"> | ||||
<div | <div | ||||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
v-model="form.name" | v-model="form.name" | ||||
type="text" | type="text" | ||||
id="name" | id="name" | ||||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" | |||||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-cyan-400" | |||||
:placeholder="t('contact.name')" | :placeholder="t('contact.name')" | ||||
required | required | ||||
/> | /> | ||||
<label | <label | ||||
for="name" | for="name" | ||||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" | |||||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-cyan-400" | |||||
> | > | ||||
{{ t("contact.name") }} | {{ t("contact.name") }} | ||||
</label> | </label> | ||||
v-model="form.email" | v-model="form.email" | ||||
type="email" | type="email" | ||||
id="email" | id="email" | ||||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" | |||||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-cyan-400" | |||||
:placeholder="t('contact.email')" | :placeholder="t('contact.email')" | ||||
required | required | ||||
/> | /> | ||||
<label | <label | ||||
for="email" | for="email" | ||||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" | |||||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-cyan-400" | |||||
> | > | ||||
{{ t("contact.email") }} | {{ t("contact.email") }} | ||||
</label> | </label> | ||||
<textarea | <textarea | ||||
v-model="form.message" | v-model="form.message" | ||||
id="message" | id="message" | ||||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent h-36 resize-none transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" | |||||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent h-36 resize-none transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-cyan-400" | |||||
:placeholder="t('contact.message')" | :placeholder="t('contact.message')" | ||||
required | required | ||||
></textarea> | ></textarea> | ||||
<label | <label | ||||
for="message" | for="message" | ||||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" | |||||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-cyan-400" | |||||
> | > | ||||
{{ t("contact.message") }} | {{ t("contact.message") }} | ||||
</label> | </label> | ||||
:class="[ | :class="[ | ||||
captcha.error.value | captcha.error.value | ||||
? 'border-red-500 focus:border-red-500' | ? 'border-red-500 focus:border-red-500' | ||||
: 'border-gray-600 focus:border-blue-500', | |||||
: 'border-gray-600 focus:border-cyan-400', | |||||
]" | ]" | ||||
:placeholder="t('contact.captcha')" | :placeholder="t('contact.captcha')" | ||||
required | required | ||||
:class="[ | :class="[ | ||||
captcha.error.value | captcha.error.value | ||||
? 'text-red-400 peer-focus:text-red-400' | ? 'text-red-400 peer-focus:text-red-400' | ||||
: 'text-gray-400 peer-focus:text-blue-400', | |||||
: 'text-gray-400 peer-focus:text-cyan-400', | |||||
]" | ]" | ||||
> | > | ||||
{{ t("contact.captcha") }} | {{ t("contact.captcha") }} | ||||
<button | <button | ||||
type="button" | type="button" | ||||
@click="captcha.generateCaptcha()" | @click="captcha.generateCaptcha()" | ||||
class="flex-shrink-0 p-2 text-gray-500 hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out" | |||||
class="flex-shrink-0 p-2 text-gray-500 hover:text-cyan-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out" | |||||
:aria-label="t('contact.refreshCaptcha')" | :aria-label="t('contact.refreshCaptcha')" | ||||
:title="t('contact.refreshCaptcha')" | :title="t('contact.refreshCaptcha')" | ||||
> | > | ||||
<button | <button | ||||
type="submit" | type="submit" | ||||
class="bg-gradient-to-r from-blue-700 to-blue-400 text-white text-base font-normal py-4 px-8 rounded-lg transition-all duration-300 hover:scale-105 hover:shadow-lg" | |||||
class="bg-cyan-400 text-zinc-900 relative text-base font-normal py-4 px-8 rounded-lg transition-all duration-300 hover:shadow-lg hover:bg-cyan-500" | |||||
:disabled="isSubmitting" | :disabled="isSubmitting" | ||||
> | > | ||||
{{ | {{ |
</div> | </div> | ||||
<div v-else> | <div v-else> | ||||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-20"> | |||||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-12"> | |||||
<div class="max-w-screen-2xl mx-auto"> | <div class="max-w-screen-2xl mx-auto"> | ||||
<nuxt-link | <nuxt-link | ||||
:to="`${homepagePath}/`" | :to="`${homepagePath}/`" | ||||
class="justify-start text-white/60 text-base font-normal" | |||||
class="justify-start text-white/60 text-sm md:text-base font-normal hover:text-white transition-colors duration-300" | |||||
>{{ t("common.home") }}</nuxt-link | >{{ t("common.home") }}</nuxt-link | ||||
> | > | ||||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||||
<span class="text-white/60 text-sm md:text-base font-normal px-2"> | |||||
/ | |||||
</span> | |||||
<nuxt-link | <nuxt-link | ||||
:to="`${homepagePath}/faq`" | :to="`${homepagePath}/faq`" | ||||
class="text-white text-base font-normal" | |||||
class="text-white text-sm md:text-base font-normal" | |||||
>{{ t("faq.title") }}</nuxt-link | >{{ t("faq.title") }}</nuxt-link | ||||
> | > | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4" | |||||
class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:mb-20 xl:px-8 lg:px-6 md:px-4 px-4" | |||||
> | > | ||||
<div class="max-w-screen-2xl mx-auto"> | <div class="max-w-screen-2xl mx-auto"> | ||||
<div class="w-full grid grid-cols-1 md:grid-cols-10 gap-8 md:gap-2"> | |||||
<div class="w-full grid grid-cols-1 md:grid-cols-12 gap-8 md:gap-4"> | |||||
<!-- 左侧分类导航 --> | <!-- 左侧分类导航 --> | ||||
<div class="col-span-1 md:col-span-2"> | |||||
<div class="flex flex-col gap-4"> | |||||
<div class="text-white text-3xl font-medium"> | |||||
{{ t("faq.category") }} | |||||
<div class="col-span-1 md:col-span-3 flex flex-col gap-4 sm:gap-6 md:gap-8 lg:gap-10 xl:gap-12 2xl:gap-16 mb-4 sm:mb-6 md:mb-8 lg:mb-10 xl:mb-12 2xl:mb-16 pr-4"> | |||||
<div class="flex flex-col gap-2 sm:gap-3 md:gap-4 lg:gap-5 xl:gap-6"> | |||||
<div class="flex justify-between items-center"> | |||||
<div class="text-white text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl font-medium"> | |||||
{{ t("faq.category") }} | |||||
</div> | |||||
<button | |||||
v-if="selectedCategory !== categoriesList[0]" | |||||
@click="handleCategoryFilter(categoriesList[0])" | |||||
class="flex items-center gap-1 px-1.5 py-0.5 sm:px-2 sm:py-1 md:px-2.5 md:py-1.5 lg:px-3 lg:py-2 text-xs sm:text-sm md:text-base text-white/60 hover:text-white bg-zinc-800/50 hover:bg-zinc-700/50 rounded-lg transition-all duration-300 active:scale-95" | |||||
> | |||||
<svg | |||||
xmlns="http://www.w3.org/2000/svg" | |||||
class="h-2.5 w-2.5 sm:h-3 sm:w-3 md:h-4 md:w-4" | |||||
viewBox="0 0 20 20" | |||||
fill="currentColor" | |||||
> | |||||
<path | |||||
fill-rule="evenodd" | |||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" | |||||
clip-rule="evenodd" | |||||
/> | |||||
</svg> | |||||
</button> | |||||
</div> | </div> | ||||
<div class="flex flex-col gap-4 w-fit"> | |||||
<div class="flex flex-row md:flex-col gap-1.5 sm:gap-2 md:gap-2.5 lg:gap-3 xl:gap-4 w-full md:w-fit overflow-x-auto md:overflow-x-visible pb-2 md:pb-0 whitespace-nowrap md:whitespace-normal"> | |||||
<div | <div | ||||
v-for="category in categoriesList" | v-for="category in categoriesList" | ||||
:key="category" | :key="category" | ||||
@click="handleCategoryFilter(category)" | @click="handleCategoryFilter(category)" | ||||
class="opacity-80 justify-start text-white text-base font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-4 py-2 rounded-lg inline-block" | |||||
class="select-none text-white text-xs sm:text-sm md:text-base lg:text-lg font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-2 py-1 sm:px-2.5 sm:py-1.5 md:px-3 md:py-2 lg:px-4 lg:py-2.5 rounded-lg inline-block active:scale-95 whitespace-nowrap" | |||||
:class="{ | :class="{ | ||||
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300': | |||||
'font-bold bg-cyan-400 text-zinc-900 border-0 shadow-lg scale-105 transition-all duration-300': | |||||
selectedCategory === category, | selectedCategory === category, | ||||
'hover:bg-zinc-800/50': selectedCategory !== category, | 'hover:bg-zinc-800/50': selectedCategory !== category, | ||||
}" | }" | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 右侧FAQ列表 --> | <!-- 右侧FAQ列表 --> | ||||
<div class="col-span-1 md:col-span-8"> | |||||
<div class="col-span-1 md:col-span-9"> | |||||
<!-- 搜索框 --> | <!-- 搜索框 --> | ||||
<div class="mb-8 relative"> | <div class="mb-8 relative"> | ||||
<input | <input | ||||
/* 添加过渡动画 */ | /* 添加过渡动画 */ | ||||
.fade-enter-active, | .fade-enter-active, | ||||
.fade-leave-active { | .fade-leave-active { | ||||
transition: opacity 0.3s; | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | } | ||||
.fade-enter-from, | .fade-enter-from, | ||||
.fade-leave-to { | .fade-leave-to { | ||||
opacity: 0; | opacity: 0; | ||||
transform: translateY(20px); | |||||
} | |||||
/* 分类悬停效果 */ | |||||
.active\:scale-95:active { | |||||
transform: scale(0.95); | |||||
} | |||||
/* 优化横向滚动 */ | |||||
.overflow-x-auto { | |||||
-webkit-overflow-scrolling: touch; | |||||
scrollbar-width: none; /* Firefox */ | |||||
-ms-overflow-style: none; /* IE and Edge */ | |||||
} | |||||
.overflow-x-auto::-webkit-scrollbar { | |||||
display: none; /* Chrome, Safari, Opera */ | |||||
} | |||||
/* 清除按钮动画 */ | |||||
button { | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
button:hover { | |||||
transform: translateY(-2px); | |||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), | |||||
0 2px 4px -1px rgba(0, 0, 0, 0.06); | |||||
} | |||||
button:active { | |||||
transform: translateY(0); | |||||
} | |||||
/* FAQ项目悬停效果 */ | |||||
.bg-zinc-900 { | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||||
} | |||||
.bg-zinc-900:hover { | |||||
transform: translateY(-4px); | |||||
} | |||||
/* 响应式调整 */ | |||||
@media (max-width: 768px) { | |||||
button:hover { | |||||
transform: translateY(-1px); | |||||
} | |||||
.bg-zinc-900:hover { | |||||
transform: translateY(-2px); | |||||
} | |||||
} | |||||
@media (min-width: 1024px) { | |||||
.bg-zinc-900:hover { | |||||
transform: translateY(-4px); | |||||
} | |||||
} | } | ||||
</style> | </style> |
</div> | </div> | ||||
</div> | </div> | ||||
</SwiperSlide> | </SwiperSlide> | ||||
<SwiperSlide> | |||||
<div | |||||
class="max-w-screen-2xl mx-auto h-full relative" | |||||
:style="{ | |||||
backgroundImage: `url(${homeA2Webp})`, | |||||
backgroundSize: 'cover', | |||||
backgroundPosition: 'center', | |||||
backgroundRepeat: 'no-repeat', | |||||
}" | |||||
> | |||||
<div | |||||
class="w-full h-full flex-col justify-center hidden md:flex relative z-10" | |||||
> | |||||
<div class="justify-center"> | |||||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | |||||
t("home.carousel.two.title") | |||||
}}</span> | |||||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | |||||
t("home.carousel.two.description") | |||||
}}</span> | |||||
<br /> | |||||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | |||||
t("home.carousel.two.description2") | |||||
}}</span | |||||
><br /> | |||||
<span | |||||
class="text-cyan-400 text-6xl font-normal leading-[78px]" | |||||
>{{ t("home.carousel.two.description3") }}</span | |||||
> | |||||
</div> | |||||
<div class="flex flex-col gap-2 mt-4"> | |||||
<div | |||||
class="flex items-center gap-2 text-stone-50 text-xl font-normal" | |||||
> | |||||
<div class="w-2 h-2 bg-white rounded-full"></div> | |||||
{{ t("home.carousel.two.description4") }} | |||||
</div> | |||||
<div | |||||
class="flex items-center gap-2 text-stone-50 text-xl font-normal" | |||||
> | |||||
<div class="w-2 h-2 bg-white rounded-full"></div> | |||||
{{ t("home.carousel.two.description5") }} | |||||
</div> | |||||
</div> | |||||
<div | |||||
class="w-36 h-14 mt-12 flex items-center justify-center bg-[#35F1FF] rounded-lg hover:bg-[#35F1FF]/80 transition-colors duration-300" | |||||
> | |||||
<nuxt-link | |||||
:to="`${homepagePath}/products`" | |||||
class="w-full h-full !flex items-center justify-center text-zinc-900" | |||||
> | |||||
{{ t("products.view_details") }} | |||||
</nuxt-link> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</SwiperSlide> | |||||
<SwiperSlide v-if="!isMobile"> | <SwiperSlide v-if="!isMobile"> | ||||
<div class="max-w-screen-2xl mx-auto h-full relative"> | <div class="max-w-screen-2xl mx-auto h-full relative"> | ||||
<video | <video | ||||
:key="usage.id" | :key="usage.id" | ||||
class="cursor-pointer select-none px-4 sm:px-7 py-2 sm:py-3 rounded-full border border-zinc-700 text-white transition-all duration-300 relative group" | class="cursor-pointer select-none px-4 sm:px-7 py-2 sm:py-3 rounded-full border border-zinc-700 text-white transition-all duration-300 relative group" | ||||
:class="{ | :class="{ | ||||
'bg-gradient-to-r from-blue-700 to-blue-400 text-white border-zinc-900 pointer-events-none': | |||||
'bg-cyan-400 border-zinc-900 pointer-events-none text-zinc-900': | |||||
activeIndex === index, | activeIndex === index, | ||||
'hover:border-zinc-600': activeIndex !== index, | 'hover:border-zinc-600': activeIndex !== index, | ||||
}" | }" | ||||
} | } | ||||
} | } | ||||
// 添加轮播图遮罩效果 | |||||
.swiper-slide { | |||||
&::before { | |||||
content: ""; | |||||
position: absolute; | |||||
top: 0; | |||||
left: 0; | |||||
right: 0; | |||||
bottom: 0; | |||||
background: linear-gradient( | |||||
to bottom, | |||||
rgba(0, 0, 0, 0.2) 0%, | |||||
rgba(0, 0, 0, 0.4) 100% | |||||
); | |||||
z-index: 1; | |||||
} | |||||
} | |||||
// 优化图片加载动画 | // 优化图片加载动画 | ||||
@keyframes fadeIn { | @keyframes fadeIn { |
<div v-if="isLoading" class="flex justify-center py-12"> | <div v-if="isLoading" class="flex justify-center py-12"> | ||||
<!-- 加载中 --> | <!-- 加载中 --> | ||||
<div | <div | ||||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 z-10" | class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 z-10" | ||||
> | > | ||||
<div | <div | ||||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-8 w-8 border-4 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
}}</span> | }}</span> | ||||
<button | <button | ||||
@click.stop="retryLoadSlideImage(slideIndex)" | @click.stop="retryLoadSlideImage(slideIndex)" | ||||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-300" | |||||
class="px-4 py-2 bg-cyan-400 text-white rounded-lg hover:bg-cyan-600 transition-colors duration-300" | |||||
> | > | ||||
{{ t("products.retry") }} | {{ t("products.retry") }} | ||||
</button> | </button> | ||||
class="absolute inset-0 flex items-center justify-center bg-zinc-900/80 z-30" | class="absolute inset-0 flex items-center justify-center bg-zinc-900/80 z-30" | ||||
> | > | ||||
<div | <div | ||||
class="animate-spin h-12 w-12 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-12 w-12 border-4 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="w-full h-full rounded-lg overflow-hidden thumbnail-fixed-size" | class="w-full h-full rounded-lg overflow-hidden thumbnail-fixed-size" | ||||
:class="{ | :class="{ | ||||
'ring-2 ring-blue-500 ring-offset-2 ring-offset-zinc-900': | |||||
'ring-2 ring-cyan-400 ring-offset-2 ring-offset-zinc-900': | |||||
currentSlideIndex === index, | currentSlideIndex === index, | ||||
'hover:ring-1 hover:ring-blue-500/50 hover:ring-offset-1 hover:ring-offset-zinc-900': | |||||
'hover:ring-1 hover:ring-cyan-400/50 hover:ring-offset-1 hover:ring-offset-zinc-900': | |||||
currentSlideIndex !== index, | currentSlideIndex !== index, | ||||
'opacity-50': | 'opacity-50': | ||||
isSlideThumbnailLoading[index] || | isSlideThumbnailLoading[index] || | ||||
class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg z-10" | class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg z-10" | ||||
> | > | ||||
<div | <div | ||||
class="animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-4 w-4 border-2 border-cyan-400 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
}}</span> | }}</span> | ||||
<button | <button | ||||
@click.stop="retryLoadSlideImage(index)" | @click.stop="retryLoadSlideImage(index)" | ||||
class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 transition-colors duration-300" | |||||
class="px-2 py-1 bg-cyan-400 text-white text-xs rounded hover:bg-cyan-600 transition-colors duration-300" | |||||
> | > | ||||
{{ t("products.retry") }} | {{ t("products.retry") }} | ||||
</button> | </button> | ||||
<!-- 缩略图导航按钮 --> | <!-- 缩略图导航按钮 --> | ||||
<button | <button | ||||
class="swiper-thumb-prev absolute top-1/2 left-2 z-10 w-8 h-8 flex items-center justify-center bg-black/50 hover:bg-blue-500 rounded-full transform -translate-y-1/2 transition-all duration-300" | |||||
class="swiper-thumb-prev absolute top-1/2 left-2 z-10 w-8 h-8 flex items-center justify-center bg-black/50 hover:bg-cyan-400 rounded-full transform -translate-y-1/2 transition-all duration-300" | |||||
> | > | ||||
<svg | <svg | ||||
xmlns="http://www.w3.org/2000/svg" | xmlns="http://www.w3.org/2000/svg" | ||||
</svg> | </svg> | ||||
</button> | </button> | ||||
<button | <button | ||||
class="swiper-thumb-next absolute top-1/2 right-2 z-10 w-8 h-8 flex items-center justify-center bg-black/50 hover:bg-blue-500 rounded-full transform -translate-y-1/2 transition-all duration-300" | |||||
class="swiper-thumb-next absolute top-1/2 right-2 z-10 w-8 h-8 flex items-center justify-center bg-black/50 hover:bg-cyan-400 rounded-full transform -translate-y-1/2 transition-all duration-300" | |||||
> | > | ||||
<svg | <svg | ||||
xmlns="http://www.w3.org/2000/svg" | xmlns="http://www.w3.org/2000/svg" | ||||
<h1 class="text-white text-3xl font-medium mb-4"> | <h1 class="text-white text-3xl font-medium mb-4"> | ||||
{{ product.title || product.name }} | {{ product.title || product.name }} | ||||
</h1> | </h1> | ||||
<div class="text-stone-400 text-lg leading-relaxed"> | |||||
<div class="text-[#71717A] text-lg leading-relaxed"> | |||||
{{ product.summary }} | {{ product.summary }} | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="flex justify-between items-center py-2 border-b border-zinc-800" | class="flex justify-between items-center py-2 border-b border-zinc-800" | ||||
> | > | ||||
<span class="text-stone-400">{{ | |||||
<span class="text-[#71717A]">{{ | |||||
t("products.categoryTitle") | t("products.categoryTitle") | ||||
}}</span> | }}</span> | ||||
<span class="text-white font-medium">{{ | <span class="text-white font-medium">{{ | ||||
<div | <div | ||||
class="flex justify-between items-center py-2 border-b border-zinc-800" | class="flex justify-between items-center py-2 border-b border-zinc-800" | ||||
> | > | ||||
<span class="text-stone-400">{{ | |||||
<span class="text-[#71717A]">{{ | |||||
t("products.usageTitle") | t("products.usageTitle") | ||||
}}</span> | }}</span> | ||||
<span class="text-white font-medium">{{ | <span class="text-white font-medium">{{ | ||||
v-if="product.capacities && product.capacities.length > 0" | v-if="product.capacities && product.capacities.length > 0" | ||||
class="flex justify-between items-center py-2" | class="flex justify-between items-center py-2" | ||||
> | > | ||||
<span class="text-stone-400">{{ | |||||
<span class="text-[#71717A]">{{ | |||||
t("products.capacitiesTitle") | t("products.capacitiesTitle") | ||||
}}</span> | }}</span> | ||||
<span class="text-white font-medium">{{ | <span class="text-white font-medium">{{ | ||||
{{ t("products.productDescription") }} | {{ t("products.productDescription") }} | ||||
</h2> | </h2> | ||||
<div | <div | ||||
class="text-stone-400 leading-relaxed space-y-4 prose prose-invert max-w-none" | |||||
class="text-[#71717A] leading-relaxed space-y-4 prose prose-invert max-w-none" | |||||
> | > | ||||
{{ product.description }} | {{ product.description }} | ||||
</div> | </div> | ||||
<div class="bg-zinc-900 rounded-lg p-6"> | <div class="bg-zinc-900 rounded-lg p-6"> | ||||
<div | <div | ||||
class="text-stone-400 leading-relaxed space-y-4 prose prose-invert max-w-none" | |||||
class="text-[#71717A] leading-relaxed space-y-4 prose prose-invert max-w-none" | |||||
> | > | ||||
<ContentRenderer :value="product.content" /> | <ContentRenderer :value="product.content" /> | ||||
</div> | </div> | ||||
> | > | ||||
{{ relatedProduct.title || relatedProduct.name }} | {{ relatedProduct.title || relatedProduct.name }} | ||||
</h3> | </h3> | ||||
<p class="text-stone-400 text-sm line-clamp-2"> | |||||
<p class="text-[#71717A] text-sm line-clamp-2"> | |||||
{{ relatedProduct.summary }} | {{ relatedProduct.summary }} | ||||
</p> | </p> | ||||
</div> | </div> | ||||
:deep(.swiper-pagination-bullet-active) { | :deep(.swiper-pagination-bullet-active) { | ||||
opacity: 1; | opacity: 1; | ||||
background-color: theme("colors.blue.500"); | |||||
background-color: theme("colors.cyan.400"); | |||||
} | } | ||||
:deep(.swiper-button-next), | :deep(.swiper-button-next), | ||||
:deep(.swiper-button-prev) { | :deep(.swiper-button-prev) { | ||||
color: theme("colors.blue.500"); | |||||
color: theme("colors.cyan.500"); | |||||
background-color: rgba(0, 0, 0, 0.3); | background-color: rgba(0, 0, 0, 0.3); | ||||
width: 36px; | width: 36px; | ||||
height: 36px; | height: 36px; | ||||
} | } | ||||
&:hover { | &:hover { | ||||
background-color: theme("colors.blue.500"); | |||||
background-color: theme("colors.cyan.500"); | |||||
color: white; | color: white; | ||||
} | } | ||||
} | } |
<div v-if="isLoading" class="flex justify-center py-8 md:py-12"> | <div v-if="isLoading" class="flex justify-center py-8 md:py-12"> | ||||
<!-- 加载中 --> | <!-- 加载中 --> | ||||
<div | <div | ||||
class="animate-spin h-6 w-6 md:h-8 md:w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-6 w-6 md:h-8 md:w-8 border-4 border-cyan-500 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
> | > | ||||
<div class="max-w-screen-2xl mx-auto"> | <div class="max-w-screen-2xl mx-auto"> | ||||
<div class="w-full grid grid-cols-1 md:grid-cols-12 gap-4"> | <div class="w-full grid grid-cols-1 md:grid-cols-12 gap-4"> | ||||
<div class="sticky top-24 col-span-1 md:col-span-3 flex flex-col gap-4 sm:gap-6 md:gap-8 lg:gap-10 xl:gap-12 2xl:gap-16 mb-4 sm:mb-6 md:mb-8 lg:mb-10 xl:mb-12 2xl:mb-16 pr-4"> | |||||
<div | |||||
class="sticky top-24 col-span-1 md:col-span-3 flex flex-col gap-4 sm:gap-6 md:gap-8 lg:gap-10 xl:gap-12 2xl:gap-16 mb-4 sm:mb-6 md:mb-8 lg:mb-10 xl:mb-12 2xl:mb-16 pr-4" | |||||
> | |||||
<div | <div | ||||
class="flex flex-col gap-2 sm:gap-3 md:gap-4 lg:gap-5 xl:gap-6" | class="flex flex-col gap-2 sm:gap-3 md:gap-4 lg:gap-5 xl:gap-6" | ||||
> | > | ||||
v-for="category in categories" | v-for="category in categories" | ||||
:key="category" | :key="category" | ||||
@click="handleCategoryFilter(category)" | @click="handleCategoryFilter(category)" | ||||
class="opacity-80 select-none text-white text-xs sm:text-sm md:text-base lg:text-lg font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-2 py-1 sm:px-2.5 sm:py-1.5 md:px-3 md:py-2 lg:px-4 lg:py-2.5 rounded-lg inline-block active:scale-95 whitespace-nowrap" | |||||
class="select-none text-white text-xs sm:text-sm md:text-base lg:text-lg font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-2 py-1 sm:px-2.5 sm:py-1.5 md:px-3 md:py-2 lg:px-4 lg:py-2.5 rounded-lg inline-block active:scale-95 whitespace-nowrap" | |||||
:class="{ | :class="{ | ||||
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300': | |||||
'font-bold bg-cyan-400 text-zinc-900 border-0 shadow-lg scale-105 transition-all duration-300': | |||||
selectedCategory === category, | selectedCategory === category, | ||||
'hover:bg-zinc-800/50': selectedCategory !== category, | 'hover:bg-zinc-800/50': selectedCategory !== category, | ||||
}" | }" | ||||
@click="handleUsageFilter(usage)" | @click="handleUsageFilter(usage)" | ||||
class="opacity-80 select-none text-white text-xs sm:text-sm md:text-base lg:text-lg font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-2 py-1 sm:px-2.5 sm:py-1.5 md:px-3 md:py-2 lg:px-4 lg:py-2.5 rounded-lg inline-block active:scale-95" | class="opacity-80 select-none text-white text-xs sm:text-sm md:text-base lg:text-lg font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-2 py-1 sm:px-2.5 sm:py-1.5 md:px-3 md:py-2 lg:px-4 lg:py-2.5 rounded-lg inline-block active:scale-95" | ||||
:class="{ | :class="{ | ||||
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300': | |||||
'opacity-100 font-bold bg-cyan-400 text-white border-0 shadow-lg scale-105 transition-all duration-300': | |||||
selectedUsage === usage, | selectedUsage === usage, | ||||
'hover:bg-zinc-800/50': selectedUsage !== usage, | 'hover:bg-zinc-800/50': selectedUsage !== usage, | ||||
}" | }" | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="col-span-1 md:col-span-9"> | <div class="col-span-1 md:col-span-9"> | ||||
<div class="flex flex-col gap-8 md:gap-12 lg:gap-16"> | <div class="flex flex-col gap-8 md:gap-12 lg:gap-16"> | ||||
<div class="relative"> | <div class="relative"> | ||||
<!-- 系列标题背景 --> | <!-- 系列标题背景 --> | ||||
<div | <div | ||||
class="absolute inset-0 bg-gradient-to-r from-blue-900/20 to-blue-500/20 rounded-lg transform -skew-x-6" | |||||
class="absolute inset-0 bg-gradient-to-r from-cyan-900/20 to-cyan-500/20 rounded-lg transform -skew-x-6" | |||||
></div> | ></div> | ||||
<!-- 系列标题内容 --> | <!-- 系列标题内容 --> | ||||
> | > | ||||
<!-- 装饰线 --> | <!-- 装饰线 --> | ||||
<div | <div | ||||
class="w-1 h-6 md:h-8 bg-gradient-to-b from-blue-500 to-blue-300 rounded-full" | |||||
class="w-1 h-6 md:h-8 bg-cyan-400 text-zinc-900 rounded-full" | |||||
></div> | ></div> | ||||
<!-- 系列名称 --> | <!-- 系列名称 --> | ||||
<!-- 产品数量标签 --> | <!-- 产品数量标签 --> | ||||
<div | <div | ||||
class="ml-auto px-2 py-0.5 md:px-3 md:py-1 bg-blue-500/20 rounded-full text-blue-300 text-xs md:text-sm" | |||||
class="ml-auto px-2 py-0.5 md:px-3 md:py-1 bg-cyan-500/20 rounded-full text-cyan-300 text-xs md:text-sm" | |||||
> | > | ||||
{{ seriesProducts.length }} | {{ seriesProducts.length }} | ||||
{{ t("products.product_count") }} | {{ t("products.product_count") }} | ||||
})" | })" | ||||
:key="product.id" | :key="product.id" | ||||
:to="`${homepagePath}/products/${product.name}`" | :to="`${homepagePath}/products/${product.name}`" | ||||
class="group bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-blue-400 relative overflow-hidden" | |||||
class="group bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-cyan-400 relative overflow-hidden" | |||||
> | > | ||||
<div class="w-full p-4 md:p-6 lg:p-8"> | <div class="w-full p-4 md:p-6 lg:p-8"> | ||||
<div | <div | ||||
class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg" | class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg" | ||||
> | > | ||||
<div | <div | ||||
class="animate-spin h-6 w-6 md:h-8 md:w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-6 w-6 md:h-8 md:w-8 border-4 border-cyan-500 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
<div | <div | ||||
{{ product.name }} | {{ product.name }} | ||||
</div> | </div> | ||||
<div | <div | ||||
class="text-center text-stone-400 text-sm md:text-base font-normal leading-normal" | |||||
class="text-center text-[#71717A] text-sm md:text-base font-normal leading-normal" | |||||
> | > | ||||
{{ product.capacities.join(" / ") }} | {{ product.capacities.join(" / ") }} | ||||
</div> | </div> | ||||
})" | })" | ||||
:key="product.id" | :key="product.id" | ||||
:to="`${homepagePath}/products/${product.name}`" | :to="`${homepagePath}/products/${product.name}`" | ||||
class="group bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-blue-400 relative overflow-hidden" | |||||
class="group bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg hover:ring-2 hover:ring-cyan-400 relative overflow-hidden" | |||||
> | > | ||||
<div class="w-full p-4 md:p-6 lg:p-8"> | <div class="w-full p-4 md:p-6 lg:p-8"> | ||||
<div | <div | ||||
class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg" | class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 rounded-lg" | ||||
> | > | ||||
<div | <div | ||||
class="animate-spin h-6 w-6 md:h-8 md:w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||||
class="animate-spin h-6 w-6 md:h-8 md:w-8 border-4 border-cyan-500 rounded-full border-t-transparent" | |||||
></div> | ></div> | ||||
</div> | </div> | ||||
<div | <div | ||||
{{ product.name }} | {{ product.name }} | ||||
</div> | </div> | ||||
<div | <div | ||||
class="text-center text-stone-400 text-sm md:text-base font-normal leading-normal" | |||||
class="text-center text-[#71717A] text-sm md:text-base font-normal leading-normal" | |||||
> | > | ||||
{{ product.capacities.join(" / ") }} | {{ product.capacities.join(" / ") }} | ||||
</div> | </div> | ||||
/* 系列标题悬停效果 */ | /* 系列标题悬停效果 */ | ||||
.relative:hover .bg-gradient-to-r { | .relative:hover .bg-gradient-to-r { | ||||
@apply from-blue-900/30 to-blue-500/30; | |||||
@apply from-cyan-900/30 to-cyan-500/30; | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | ||||
} | } | ||||
/* 产品数量标签悬停效果 */ | /* 产品数量标签悬停效果 */ | ||||
.relative:hover .bg-blue-500\/20 { | |||||
@apply bg-blue-500/30; | |||||
.relative:hover .bg-cyan-500\/20 { | |||||
@apply bg-cyan-500/30; | |||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | ||||
} | } | ||||
/* 添加过渡动画 */ | /* 添加过渡动画 */ | ||||
.bg-primary { | .bg-primary { | ||||
@apply bg-blue-600; | |||||
@apply bg-cyan-600; | |||||
} | } | ||||
button { | button { |