try { | try { | ||||
// 使用类型断言,确保 langCode 是有效的 LocaleCode | // 使用类型断言,确保 langCode 是有效的 LocaleCode | ||||
await setLocale(langCode as LocaleCode); | await setLocale(langCode as LocaleCode); | ||||
window.location.reload(); | |||||
} catch (error) { | } catch (error) { | ||||
console.error("Failed to set locale:", error); | console.error("Failed to set locale:", error); | ||||
// 这里可以添加用户反馈,例如显示一个错误提示 | // 这里可以添加用户反馈,例如显示一个错误提示 |
<h3 | <h3 | ||||
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" | class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" | ||||
> | > | ||||
{{ $t("common.footer.productsLinks.title") }} | |||||
{{ t("common.footer.productsLinks.title") }} | |||||
</h3> | </h3> | ||||
<ul class="space-y-4"> | <ul class="space-y-4"> | ||||
<li v-for="item in menuProductsItems" :key="item.path"> | <li v-for="item in menuProductsItems" :key="item.path"> | ||||
:to="item.path" | :to="item.path" | ||||
class="text-zinc-500 text-sm font-normal hover:text-white transition" | class="text-zinc-500 text-sm font-normal hover:text-white transition" | ||||
> | > | ||||
{{ $t(item.label) }} | |||||
{{ t(item.label) }} | |||||
</NuxtLink> | </NuxtLink> | ||||
</li> | </li> | ||||
</ul> | </ul> | ||||
<h3 | <h3 | ||||
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" | class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" | ||||
> | > | ||||
{{ $t("common.footer.websiteLinks.title") }} | |||||
{{ t("common.footer.websiteLinks.title") }} | |||||
</h3> | </h3> | ||||
<ul class="space-y-4"> | <ul class="space-y-4"> | ||||
<li v-for="item in menuWebsiteItems" :key="item.path"> | <li v-for="item in menuWebsiteItems" :key="item.path"> | ||||
:to="item.path" | :to="item.path" | ||||
class="text-zinc-500 text-sm font-normal hover:text-white transition" | class="text-zinc-500 text-sm font-normal hover:text-white transition" | ||||
> | > | ||||
{{ $t(item.label) }} | |||||
{{ t(item.label) }} | |||||
</NuxtLink> | </NuxtLink> | ||||
</li> | </li> | ||||
</ul> | </ul> | ||||
<h3 | <h3 | ||||
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" | class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" | ||||
> | > | ||||
{{ $t("common.footer.quickLinks.title") }} | |||||
{{ t("common.footer.quickLinks.title") }} | |||||
</h3> | </h3> | ||||
<ul class="space-y-4"> | <ul class="space-y-4"> | ||||
<li v-for="item in menuHomeItems" :key="item.path"> | <li v-for="item in menuHomeItems" :key="item.path"> | ||||
:to="item.path" | :to="item.path" | ||||
class="text-zinc-500 text-sm font-normal hover:text-white transition" | class="text-zinc-500 text-sm font-normal hover:text-white transition" | ||||
> | > | ||||
{{ $t(item.label) }} | |||||
{{ t(item.label) }} | |||||
</NuxtLink> | </NuxtLink> | ||||
</li> | </li> | ||||
</ul> | </ul> | ||||
* 包含网站导航、联系信息和版权信息 | * 包含网站导航、联系信息和版权信息 | ||||
*/ | */ | ||||
import { useI18n } from "vue-i18n"; | import { useI18n } from "vue-i18n"; | ||||
const { locale } = useI18n(); | |||||
const { locale, t } = useI18n(); | |||||
const config = useRuntimeConfig(); | const config = useRuntimeConfig(); | ||||
const defaultLocale = config.public.i18n?.defaultLocale || "en"; | const defaultLocale = config.public.i18n?.defaultLocale || "en"; | ||||
// 获取产品分类数据 | // 获取产品分类数据 | ||||
const { data: categoryResponse } = useFetch(`/api/products/category?lang=${locale.value}`, { | |||||
key: `category-${locale.value}` | |||||
}); | |||||
const { data: categoryResponse } = await useAsyncData( | |||||
`footer-categories-${locale.value}`, | |||||
async () => { | |||||
try { | |||||
// 使用queryCollection从content目录获取数据 | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/categories/${locale.value}/%`) | |||||
.all(); | |||||
if (!content || !Array.isArray(content)) { | |||||
console.log("No category content found for footer"); | |||||
return []; | |||||
} | |||||
// 转换为需要的格式 | |||||
return content | |||||
.map((item: any) => { | |||||
// 从路径中提取ID - 文件名就是ID | |||||
const pathParts = item.path?.split("/"); | |||||
const idFile = pathParts?.[pathParts.length - 1] || ""; | |||||
// 如果ID在元数据中有提供,优先使用元数据中的ID | |||||
const id = item.id | |||||
? parseInt(item.id) | |||||
: parseInt(idFile.replace(".md", "")) || 0; | |||||
return { | |||||
title: item.title || "", | |||||
id: id, | |||||
}; | |||||
}) | |||||
.sort((a, b) => (a.id || 0) - (b.id || 0)); | |||||
} catch (error) { | |||||
console.error("Error loading category data for footer:", error); | |||||
return []; | |||||
} | |||||
} | |||||
); | |||||
// 使用计算属性处理产品分类数据 | |||||
const productCategories = computed(() => { | const productCategories = computed(() => { | ||||
if (!categoryResponse.value?.data) return []; | |||||
return categoryResponse.value.data; | |||||
return categoryResponse.value || []; | |||||
}); | }); | ||||
// 导航菜单项 | // 导航菜单项 | ||||
const menuProductsItems = computed(() => { | const menuProductsItems = computed(() => { | ||||
// 构建路径前缀 | // 构建路径前缀 | ||||
const prefix = locale.value === defaultLocale ? "" : `/${locale.value}`; | const prefix = locale.value === defaultLocale ? "" : `/${locale.value}`; | ||||
// 使用API获取的产品分类数据 | |||||
// 使用修改后的产品分类数据 | |||||
return productCategories.value.map((category: any) => ({ | return productCategories.value.map((category: any) => ({ | ||||
label: category.title, | label: category.title, | ||||
path: `${prefix}/products?category=${category.id}` | |||||
path: `${prefix}/products?category=${encodeURIComponent(category.title)}`, | |||||
})); | })); | ||||
}); | }); | ||||
// 这里可以根据需要添加首页快捷链接 | // 这里可以根据需要添加首页快捷链接 | ||||
{ | { | ||||
label: "common.footer.quickLinks.support", | label: "common.footer.quickLinks.support", | ||||
path: locale.value === defaultLocale ? "/support" : `/${locale.value}/support`, | |||||
path: | |||||
locale.value === defaultLocale ? "/support" : `/${locale.value}/support`, | |||||
}, | }, | ||||
{ | { | ||||
label: "common.footer.quickLinks.privacy", | label: "common.footer.quickLinks.privacy", | ||||
path: locale.value === defaultLocale ? "/privacy" : `/${locale.value}/privacy`, | |||||
path: | |||||
locale.value === defaultLocale ? "/privacy" : `/${locale.value}/privacy`, | |||||
}, | }, | ||||
{ | { | ||||
label: "common.footer.quickLinks.terms", | label: "common.footer.quickLinks.terms", | ||||
path: locale.value === defaultLocale ? "/terms" : `/${locale.value}/terms`, | path: locale.value === defaultLocale ? "/terms" : `/${locale.value}/terms`, | ||||
} | |||||
}, | |||||
]); | ]); | ||||
</script> | </script> |
class="main-nav-link relative inline-block justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity py-2 px-3 rounded-md" | class="main-nav-link relative inline-block justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity py-2 px-3 rounded-md" | ||||
:to="item.path" | :to="item.path" | ||||
:class="[ | :class="[ | ||||
$route.path === item.path | |||||
route.path === item.path | |||||
? 'font-bold opacity-100 bg-white/15' | ? 'font-bold opacity-100 bg-white/15' | ||||
: '', | : '', | ||||
]" | ]" | ||||
> | > | ||||
{{ $t(item.label) }} | |||||
{{ t(item.label) }} | |||||
</nuxt-link> | </nuxt-link> | ||||
<!-- Dropdown Container --> | <!-- Dropdown Container --> | ||||
@mouseenter="handleMouseEnter(item.label)" | @mouseenter="handleMouseEnter(item.label)" | ||||
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md" | class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md" | ||||
:class="[ | :class="[ | ||||
$route.path.startsWith(item.pathPrefix) | |||||
route.path.startsWith(item.pathPrefix) | |||||
? 'font-bold opacity-100 bg-white/15' | ? 'font-bold opacity-100 bg-white/15' | ||||
: '', | : '', | ||||
]" | ]" | ||||
> | > | ||||
<span>{{ $t(item.label) }}</span> | |||||
<span>{{ t(item.label) }}</span> | |||||
</div> | </div> | ||||
<!-- Dropdown Panel --> | <!-- Dropdown Panel --> | ||||
class="bg-slate-900 rounded-none p-6 flex flex-col gap-1" | class="bg-slate-900 rounded-none p-6 flex flex-col gap-1" | ||||
> | > | ||||
<h3 class="text-base font-medium text-white mb-2"> | <h3 class="text-base font-medium text-white mb-2"> | ||||
{{ $t(section.title) }} | |||||
{{ t(section.title) }} | |||||
</h3> | </h3> | ||||
<ul class="flex flex-col gap-1"> | <ul class="flex flex-col gap-1"> | ||||
<li v-for="link in section.items" :key="link.path"> | <li v-for="link in section.items" :key="link.path"> | ||||
<nuxt-link | <nuxt-link | ||||
:to="link.path" | :to="link.path" | ||||
@click="handleMouseLeave" | @click="handleMouseLeave" | ||||
class="block text-base text-gray-200 rounded-none py-2 transition-all duration-200 hover:text-white/80 hover:font-bold" | |||||
class="block text-base text-gray-200 rounded-none py-2 transition-all duration-200 hover:text-white/80 hover:font-bold" | |||||
:class="{ | :class="{ | ||||
'text-white font-bold bg-white/15': | 'text-white font-bold bg-white/15': | ||||
$route.path === link.path, | |||||
route.path === link.path, | |||||
}" | }" | ||||
> | > | ||||
{{ $t(link.label) }} | |||||
{{ t(link.label) }} | |||||
</nuxt-link> | </nuxt-link> | ||||
</li> | </li> | ||||
</ul> | </ul> | ||||
<span | <span | ||||
class="hidden lg:inline-block ml-1 text-white text-sm opacity-80" | class="hidden lg:inline-block ml-1 text-white text-sm opacity-80" | ||||
> | > | ||||
{{ $t("common.search") }} | |||||
{{ t("common.search") }} | |||||
</span> | </span> | ||||
<!-- Input overlay could go here if implementing search --> | <!-- Input overlay could go here if implementing search --> | ||||
</div> | </div> | ||||
@click="closeMobileMenu" | @click="closeMobileMenu" | ||||
class="block px-3 py-2 rounded-md text-base font-medium" | class="block px-3 py-2 rounded-md text-base font-medium" | ||||
:class="[ | :class="[ | ||||
$route.path === item.path | |||||
route.path === item.path | |||||
? 'bg-gray-900 text-white' | ? 'bg-gray-900 text-white' | ||||
: 'text-gray-300 hover:bg-gray-700 hover:text-white', | : 'text-gray-300 hover:bg-gray-700 hover:text-white', | ||||
]" | ]" | ||||
> | > | ||||
{{ $t(item.label) }} | |||||
{{ t(item.label) }} | |||||
</nuxt-link> | </nuxt-link> | ||||
<!-- Mobile Dropdown Section --> | <!-- Mobile Dropdown Section --> | ||||
<h3 | <h3 | ||||
class="px-3 pt-2 pb-1 text-sm font-semibold text-gray-400 uppercase tracking-wider" | class="px-3 pt-2 pb-1 text-sm font-semibold text-gray-400 uppercase tracking-wider" | ||||
> | > | ||||
{{ $t(item.label) }} | |||||
{{ t(item.label) }} | |||||
</h3> | </h3> | ||||
<div | <div | ||||
v-for="section in item.children" | v-for="section in item.children" | ||||
class="mt-1" | class="mt-1" | ||||
> | > | ||||
<!-- Optional: Section title for mobile? --> | <!-- Optional: Section title for mobile? --> | ||||
<!-- <h4 class="px-3 pt-1 text-xs font-medium text-gray-500">{{ $t(section.title) }}</h4> --> | |||||
<!-- <h4 class="px-3 pt-1 text-xs font-medium text-gray-500">{{ t(section.title) }}</h4> --> | |||||
<nuxt-link | <nuxt-link | ||||
v-for="link in section.items" | v-for="link in section.items" | ||||
:key="link.path" | :key="link.path" | ||||
@click="closeMobileMenu" | @click="closeMobileMenu" | ||||
class="block pl-6 pr-3 py-2 rounded-md text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" | class="block pl-6 pr-3 py-2 rounded-md text-base font-medium text-gray-300 hover:bg-gray-700 hover:text-white" | ||||
:class="{ | :class="{ | ||||
'bg-gray-900 text-white': $route.path === link.path, | |||||
'bg-gray-900 text-white': route.path === link.path, | |||||
}" | }" | ||||
> | > | ||||
{{ $t(link.label) }} | |||||
{{ t(link.label) }} | |||||
</nuxt-link> | </nuxt-link> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
/> | /> | ||||
</svg> | </svg> | ||||
</button> | </button> | ||||
<h2 class="text-white text-xl mb-6">{{ $t("common.search") }}</h2> | |||||
<h2 class="text-white text-xl mb-6">{{ t("common.search") }}</h2> | |||||
<!-- Input with Icon --> | <!-- Input with Icon --> | ||||
<div class="relative mb-6"> | <div class="relative mb-6"> | ||||
ref="searchInputRef" | ref="searchInputRef" | ||||
type="text" | type="text" | ||||
: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-blue-500 focus:ring-1 focus:ring-blue-500" | ||||
/> | /> | ||||
<!-- Hot Keywords Section --> | <!-- Hot Keywords Section --> | ||||
<div class="mt-6"> | <div class="mt-6"> | ||||
<h3 class="text-gray-400 text-sm mb-3"> | <h3 class="text-gray-400 text-sm mb-3"> | ||||
{{ $t("common.hotKeywords") || "热门搜索" }} | |||||
{{ t("common.hotKeywords") || "热门搜索" }} | |||||
</h3> | </h3> | ||||
<div class="flex flex-wrap gap-3"> | <div class="flex flex-wrap gap-3"> | ||||
<button | <button | ||||
*/ | */ | ||||
const { t, locale } = useI18n(); | const { t, locale } = useI18n(); | ||||
const config = useRuntimeConfig(); | const config = useRuntimeConfig(); | ||||
// 从运行时配置获取默认语言,如果未配置则默认为 'en' | |||||
const defaultLocale = config.public.i18n?.defaultLocale || "en"; | const defaultLocale = config.public.i18n?.defaultLocale || "en"; | ||||
const mobileMenuOpen = ref(false); | const mobileMenuOpen = ref(false); | ||||
const isSearchOpen = ref(false); | const isSearchOpen = ref(false); | ||||
const openDropdown = ref<string | null>(null); | const openDropdown = ref<string | null>(null); | ||||
let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay | let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay | ||||
const route = useRoute(); | |||||
// 添加热门关键字 | // 添加热门关键字 | ||||
const hotKeywords = ref(["SSD", "SD", "DDR4"]); | const hotKeywords = ref(["SSD", "SD", "DDR4"]); | ||||
// 获取产品分类数据 | // 获取产品分类数据 | ||||
const { data: categoryResponse } = useFetch(`/api/products/category?lang=${locale.value}`, { | |||||
key: `category-${locale.value}` | |||||
}); | |||||
const productCategories = computed(() => { | |||||
if (!categoryResponse.value?.data) return []; | |||||
return categoryResponse.value.data.map((category: any) => ({ | |||||
label: category.title, | |||||
path: `/products?category=${category.id}` | |||||
})); | |||||
}); | |||||
const { data: categoryResponse } = await useAsyncData( | |||||
`header-categories-${locale.value}`, | |||||
async () => { | |||||
try { | |||||
// 使用queryCollection从content目录获取数据 | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/categories/${locale.value}/%`) | |||||
.all(); | |||||
if (!content || !Array.isArray(content)) { | |||||
console.log("No category content found for header"); | |||||
return []; | |||||
} | |||||
// 转换为需要的格式 | |||||
return content | |||||
.map((item: any) => { | |||||
// 从路径中提取ID - 文件名就是ID | |||||
const pathParts = item.path?.split("/"); | |||||
const idFile = pathParts?.[pathParts.length - 1] || ""; | |||||
// 如果ID在元数据中有提供,优先使用元数据中的ID | |||||
const id = item.id | |||||
? parseInt(item.id) | |||||
: parseInt(idFile.replace(".md", "")) || 0; | |||||
return { | |||||
label: item.title || "", | |||||
path: `/products?category=${encodeURIComponent(item.title)}`, | |||||
id: id, | |||||
}; | |||||
}) | |||||
.sort((a, b) => (a.id || 0) - (b.id || 0)); | |||||
} catch (error) { | |||||
console.error("Error loading category data for header:", error); | |||||
return []; | |||||
} | |||||
} | |||||
); | |||||
// 获取产品用途数据 | // 获取产品用途数据 | ||||
const { data: usageResponse } = useFetch(`/api/products/usage?lang=${locale.value}`, { | |||||
key: `usage-${locale.value}` | |||||
const { data: usageResponse } = await useAsyncData( | |||||
`header-usages-${locale.value}`, | |||||
async () => { | |||||
try { | |||||
// 使用queryCollection从content目录获取数据 | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/usages/${locale.value}/%`) | |||||
.all(); | |||||
if (!content || !Array.isArray(content)) { | |||||
console.log("No usage content found for header"); | |||||
return []; | |||||
} | |||||
// 转换为需要的格式 | |||||
return content | |||||
.map((item: any) => { | |||||
// 从路径中提取ID - 文件名就是ID | |||||
const pathParts = item.path?.split("/"); | |||||
const idFile = pathParts?.[pathParts.length - 1] || ""; | |||||
// 从文件名提取ID,去掉可能的扩展名 | |||||
const id = parseInt(idFile.replace(".md", "")) || 0; | |||||
return { | |||||
label: item.title, | |||||
path: `/products?usage=${encodeURIComponent(item.title)}`, | |||||
id: id, | |||||
}; | |||||
}) | |||||
.sort((a, b) => (a.id || 0) - (b.id || 0)); | |||||
} catch (error) { | |||||
console.error("Error loading usage data for header:", error); | |||||
return []; | |||||
} | |||||
} | |||||
); | |||||
// 使用计算属性处理产品分类数据 | |||||
const productCategories = computed(() => { | |||||
return categoryResponse.value || []; | |||||
}); | }); | ||||
// 使用计算属性处理产品用途数据 | |||||
const productUsages = computed(() => { | const productUsages = computed(() => { | ||||
if (!usageResponse.value?.data) return []; | |||||
return usageResponse.value.data.map((usage: any) => ({ | |||||
label: usage.name, | |||||
path: `/products?usage=${usage.id}` | |||||
})); | |||||
return usageResponse.value || []; | |||||
}); | }); | ||||
// 使用 computed 来定义 homePath,根据是否为默认语言调整路径 | // 使用 computed 来定义 homePath,根据是否为默认语言调整路径 | ||||
title: "common.productCategories", | title: "common.productCategories", | ||||
items: productCategories.value.map((category: any) => ({ | items: productCategories.value.map((category: any) => ({ | ||||
...category, | ...category, | ||||
path: `${prefix}${category.path}` | |||||
})) | |||||
path: `${prefix}${category.path}`, | |||||
})), | |||||
}, | }, | ||||
{ | { | ||||
title: "common.byUsage", | title: "common.byUsage", | ||||
items: productUsages.value.map((usage: any) => ({ | items: productUsages.value.map((usage: any) => ({ | ||||
...usage, | ...usage, | ||||
path: `${prefix}${usage.path}` | |||||
})) | |||||
path: `${prefix}${usage.path}`, | |||||
})), | |||||
}, | }, | ||||
], | ], | ||||
}, | }, |
category: 游戏加速/存储 | category: 游戏加速/存储 | ||||
products: ["M.2 SSD", "2.5英寸 SSD", "内存条", "microSD存储卡"] | products: ["M.2 SSD", "2.5英寸 SSD", "内存条", "microSD存储卡"] | ||||
summary: 游戏性能优化和存储解决方案 | summary: 游戏性能优化和存储解决方案 | ||||
--- | |||||
--- | |||||
# 游戏加速/存储解决方案 | |||||
## 电脑游戏性能提升 | |||||
现代大型PC游戏动辄上百GB,高品质的M.2 SSD和2.5英寸SSD可显著减少游戏载入时间,提升游戏体验。高性能内存条也是游戏流畅运行的关键组件。 | |||||
## 游戏主机存储扩展 | |||||
针对PS5、Xbox Series X/S、Nintendo Switch等游戏主机,我们提供兼容的存储扩展方案,让您存储更多游戏而无需频繁删除。 | |||||
## 产品优势 | |||||
- **极速读写**:NVMe技术带来接近内存的访问速度 | |||||
- **低延迟**:减少游戏中的卡顿和画面撕裂 | |||||
- **大容量**:轻松应对现代游戏的海量存储需求 | |||||
- **散热设计**:长时间游戏也能保持稳定性能 |
strong_point_title: "Our Strengths / Why Choose Us", | strong_point_title: "Our Strengths / Why Choose Us", | ||||
view_details: "View Details", | view_details: "View Details", | ||||
product_count: "Products", | product_count: "Products", | ||||
loadError: "Failed to load product. Please try again later.", | |||||
}, | }, | ||||
faq: { | faq: { | ||||
title: "Frequently Asked Questions", | title: "Frequently Asked Questions", |
strong_point_title: "当社の強み/選ばれる理由", | strong_point_title: "当社の強み/選ばれる理由", | ||||
view_details: "詳細を見る", | view_details: "詳細を見る", | ||||
product_count: "製品", | product_count: "製品", | ||||
loadError: "製品の読み込みに失敗しました。後でもう一度お試しください。", | |||||
}, | }, | ||||
faq: { | faq: { | ||||
title: "よくある質問", | title: "よくある質問", |
strong_point_title: "我们的优势/选择我们的理由", | strong_point_title: "我们的优势/选择我们的理由", | ||||
view_details: "查看详情", | view_details: "查看详情", | ||||
product_count: "款产品", | product_count: "款产品", | ||||
loadError: "加载产品失败,请稍后重试", | |||||
}, | }, | ||||
faq: { | faq: { | ||||
title: "常见问题", | title: "常见问题", |
import i18nConfig from "./i18n.config"; | import i18nConfig from "./i18n.config"; | ||||
import type { Strategies } from "@nuxtjs/i18n"; | import type { Strategies } from "@nuxtjs/i18n"; | ||||
import { resolve } from 'path'; | |||||
import { resolve } from "path"; | |||||
import { readFileSync } from 'fs'; | |||||
import { fileURLToPath } from 'url'; | |||||
import { dirname, join } from 'path'; | |||||
// 尝试导入预生成的路由配置,如果文件不存在则使用默认路由 | |||||
let prerenderRoutes = [ | |||||
"/", | |||||
"/products", | |||||
"/faq", | |||||
"/contact", | |||||
"/about", | |||||
"/en", | |||||
"/ja", | |||||
"/en/products", | |||||
"/ja/products", | |||||
"/en/faq", | |||||
"/ja/faq", | |||||
"/en/contact", | |||||
"/ja/contact", | |||||
"/en/about", | |||||
"/ja/about", | |||||
]; | |||||
try { | |||||
const __dirname = dirname(fileURLToPath(import.meta.url)); | |||||
const routesPath = join(__dirname, 'prerenderRoutes.json'); | |||||
prerenderRoutes = JSON.parse(readFileSync(routesPath, 'utf-8')); | |||||
console.log(`已加载预渲染路由,共 ${prerenderRoutes.length} 条`); | |||||
} catch (err) { | |||||
console.warn('未找到预渲染路由配置文件,使用默认路由'); | |||||
} | |||||
// https://nuxt.com/docs/api/configuration/nuxt-config | // https://nuxt.com/docs/api/configuration/nuxt-config | ||||
export default defineNuxtConfig({ | export default defineNuxtConfig({ | ||||
compatibilityDate: "2025-05-06", | |||||
devtools: { enabled: true }, | devtools: { enabled: true }, | ||||
// 添加CSS | // 添加CSS | ||||
// content模块配置 | // content模块配置 | ||||
content: { | content: { | ||||
// 配置导航 | |||||
navigation: { | |||||
fields: ['title', 'description', 'category'] | |||||
}, | |||||
// 配置 Markdown | // 配置 Markdown | ||||
markdown: { | |||||
// 启用目录 | |||||
toc: { | |||||
depth: 3, | |||||
searchDepth: 3 | |||||
build: { | |||||
markdown: { | |||||
toc: { | |||||
depth: 3, | |||||
searchDepth: 3, | |||||
}, | |||||
highlight: { | |||||
// Theme used in all color schemes. | |||||
theme: "github-light", | |||||
}, | |||||
}, | }, | ||||
// 启用高亮 | |||||
highlight: { | |||||
theme: 'github-dark' | |||||
} | |||||
} | |||||
}, | |||||
}, | }, | ||||
// i18n 配置 (从外部文件加载) | // i18n 配置 (从外部文件加载) | ||||
i18n: { | i18n: { | ||||
...i18nConfig, | ...i18nConfig, | ||||
defaultLocale: i18nConfig.defaultLocale as 'zh' | 'en' | 'ja' | undefined, | |||||
defaultLocale: i18nConfig.defaultLocale as "zh" | "en" | "ja" | undefined, | |||||
strategy: i18nConfig.strategy as Strategies, // 显式类型转换 strategy | strategy: i18nConfig.strategy as Strategies, // 显式类型转换 strategy | ||||
}, | }, | ||||
// 静态站点生成配置 | // 静态站点生成配置 | ||||
nitro: { | nitro: { | ||||
prerender: { | prerender: { | ||||
crawlLinks: true, | |||||
routes: [ | |||||
'/', | |||||
'/products', | |||||
'/faq', | |||||
'/contact', | |||||
'/about', | |||||
'/en', | |||||
'/ja', | |||||
'/zh' | |||||
], | |||||
ignore: [ | |||||
'/api/**', | |||||
'/admin/**' | |||||
], | |||||
failOnError: false | |||||
crawlLinks: false, // 不自动抓取链接 | |||||
routes: prerenderRoutes, | |||||
ignore: ["/api/**", "/admin/**"], | |||||
failOnError: false, // 遇到错误继续生成其他页面 | |||||
}, | }, | ||||
// 添加图片本地化配置 | // 添加图片本地化配置 | ||||
publicAssets: [ | publicAssets: [ | ||||
{ | { | ||||
dir: 'public', | |||||
baseURL: '/' | |||||
} | |||||
] | |||||
dir: "public", | |||||
baseURL: "/", | |||||
}, | |||||
], | |||||
}, | |||||
// 禁用 payload 提取,优化静态生成 | |||||
experimental: { | |||||
payloadExtraction: false, | |||||
renderJsonPayloads: false, | |||||
}, | }, | ||||
devServer: { | devServer: { | ||||
host: "0.0.0.0", | host: "0.0.0.0", | ||||
} | |||||
}); | |||||
}, | |||||
compatibilityDate: "2025-05-07", | |||||
}); |
"swiper": "^11.2.6", | "swiper": "^11.2.6", | ||||
"vue": "^3.5.13", | "vue": "^3.5.13", | ||||
"vue-i18n": "^11.1.3", | "vue-i18n": "^11.1.3", | ||||
"vue-router": "^4.5.0" | |||||
"vue-router": "^4.5.0", | |||||
"yaml": "^2.7.1" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@tailwindcss/typography": "^0.5.10", | "@tailwindcss/typography": "^0.5.10", |
"type": "module", | "type": "module", | ||||
"scripts": { | "scripts": { | ||||
"build": "nuxt build", | "build": "nuxt build", | ||||
"dev": "nuxt dev", | |||||
"generate": "nuxt generate", | |||||
"dev": "npm run prepare-routes && nuxt dev", | |||||
"generate": "npm run prepare-routes && nuxt generate", | |||||
"prepare-routes": "node scripts/generateRoutes.js", | |||||
"preview": "nuxt preview", | "preview": "nuxt preview", | ||||
"postinstall": "nuxt prepare" | "postinstall": "nuxt prepare" | ||||
}, | }, | ||||
"swiper": "^11.2.6", | "swiper": "^11.2.6", | ||||
"vue": "^3.5.13", | "vue": "^3.5.13", | ||||
"vue-i18n": "^11.1.3", | "vue-i18n": "^11.1.3", | ||||
"vue-router": "^4.5.0" | |||||
"vue-router": "^4.5.0", | |||||
"yaml": "^2.7.1" | |||||
}, | }, | ||||
"devDependencies": { | "devDependencies": { | ||||
"@tailwindcss/typography": "^0.5.10", | "@tailwindcss/typography": "^0.5.10", |
<nuxt-link | <nuxt-link | ||||
to="/" | to="/" | ||||
class="justify-start text-white/60 text-base font-normal hover:text-white transition-colors duration-300" | class="justify-start text-white/60 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-base font-normal px-2"> / </span> | ||||
<span class="text-white text-base font-normal">{{ | <span class="text-white text-base font-normal">{{ | ||||
$t("about.overview.title") | |||||
t("about.overview.title") | |||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<h1 | <h1 | ||||
class="text-white text-5xl font-bold mb-4 tracking-tight text-center" | class="text-white text-5xl font-bold mb-4 tracking-tight text-center" | ||||
> | > | ||||
{{ $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-stone-400 text-xl leading-relaxed text-center max-w-2xl break-words whitespace-pre-wrap" | ||||
> | > | ||||
{{ $t("about.companyInfo.description") }} | |||||
{{ t("about.companyInfo.description") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<h2 | <h2 | ||||
class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | ||||
> | > | ||||
{{ $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-gradient-to-r from-blue-500 to-blue-300 rounded-full" | ||||
></span> | ></span> | ||||
<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-stone-400 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">{{ | ||||
$t("about.companyInfo.name") | |||||
t("about.companyInfo.name") | |||||
}}</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-stone-400 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">{{ | ||||
$t("about.companyInfo.englishName") | |||||
t("about.companyInfo.englishName") | |||||
}}</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-stone-400 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">{{ | ||||
$t("about.companyInfo.established") | |||||
t("about.companyInfo.established") | |||||
}}</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-stone-400 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">{{ | ||||
$t("about.companyInfo.ceo") | |||||
t("about.companyInfo.ceo") | |||||
}}</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-stone-400 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">{{ | ||||
$t("about.companyInfo.employees") | |||||
t("about.companyInfo.employees") | |||||
}}</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-stone-400 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.companyInfo.location") | |||||
t("about.companyInfo.location") | |||||
}}</span> | }}</span> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<h2 | <h2 | ||||
class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | ||||
> | > | ||||
{{ $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-gradient-to-r from-blue-500 to-blue-300 rounded-full" | ||||
></span> | ></span> | ||||
</h2> | </h2> | ||||
<div class="text-stone-400 text-lg leading-relaxed"> | <div class="text-stone-400 text-lg leading-relaxed"> | ||||
{{ $t("about.companyInfo.philosophy") }} | |||||
{{ t("about.companyInfo.philosophy") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<!-- 联系方式卡片 --> | <!-- 联系方式卡片 --> | ||||
<h2 | <h2 | ||||
class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | ||||
> | > | ||||
{{ $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-gradient-to-r from-blue-500 to-blue-300 rounded-full" | ||||
></span> | ></span> | ||||
<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-stone-400 text-base">{{ | ||||
$t("about.overview.email") | |||||
t("about.overview.email") | |||||
}}</span> | }}</span> | ||||
<a | <a | ||||
:href="'mailto:' + companyInfo.email" | :href="'mailto:' + companyInfo.email" | ||||
</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-stone-400 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, '')" | ||||
</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-stone-400 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">{{ | ||||
companyInfo.fax | companyInfo.fax | ||||
</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-stone-400 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">{{ | ||||
$t("about.companyInfo.businessHours") | |||||
t("about.companyInfo.businessHours") | |||||
}}</span> | }}</span> | ||||
</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-stone-400 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"> | ||||
<p> | <p> | ||||
{{ $t("about.companyInfo.businessActivities") }} | |||||
{{ t("about.companyInfo.businessActivities") }} | |||||
</p> | </p> | ||||
</div> | </div> | ||||
</div> | </div> |
<nuxt-link | <nuxt-link | ||||
to="/" | to="/" | ||||
class="justify-start text-white/60 text-base font-normal" | class="justify-start text-white/60 text-base font-normal" | ||||
>{{ $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-base font-normal px-2"> / </span> | ||||
<nuxt-link to="/contact" class="text-white text-base font-normal">{{ | <nuxt-link to="/contact" class="text-white text-base font-normal">{{ | ||||
$t("contact.title") | |||||
t("contact.title") | |||||
}}</nuxt-link> | }}</nuxt-link> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | ||||
> | > | ||||
<div class="text-white text-3xl font-medium mb-6"> | <div class="text-white text-3xl font-medium mb-6"> | ||||
{{ $t("contact.title") }} | |||||
{{ t("contact.title") }} | |||||
</div> | </div> | ||||
<form | <form | ||||
@submit.prevent="handleSubmit" | @submit.prevent="handleSubmit" | ||||
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-blue-500" | ||||
: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-blue-400" | ||||
> | > | ||||
{{ $t("contact.name") }} | |||||
{{ t("contact.name") }} | |||||
</label> | </label> | ||||
</div> | </div> | ||||
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-blue-500" | ||||
: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-blue-400" | ||||
> | > | ||||
{{ $t("contact.email") }} | |||||
{{ t("contact.email") }} | |||||
</label> | </label> | ||||
</div> | </div> | ||||
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-blue-500" | ||||
: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-blue-400" | ||||
> | > | ||||
{{ $t("contact.message") }} | |||||
{{ t("contact.message") }} | |||||
</label> | </label> | ||||
</div> | </div> | ||||
? '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-blue-500', | ||||
]" | ]" | ||||
:placeholder="$t('contact.captcha')" | |||||
:placeholder="t('contact.captcha')" | |||||
required | required | ||||
autocomplete="off" | autocomplete="off" | ||||
aria-describedby="captcha-error" | aria-describedby="captcha-error" | ||||
: 'text-gray-400 peer-focus:text-blue-400', | : 'text-gray-400 peer-focus:text-blue-400', | ||||
]" | ]" | ||||
> | > | ||||
{{ $t("contact.captcha") }} | |||||
{{ t("contact.captcha") }} | |||||
</label> | </label> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="flex-shrink-0 cursor-pointer select-none rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:shadow-md active:scale-100" | class="flex-shrink-0 cursor-pointer select-none rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:shadow-md active:scale-100" | ||||
v-html="captcha.captchaSvg.value" | v-html="captcha.captchaSvg.value" | ||||
@click="captcha.generateCaptcha()" | @click="captcha.generateCaptcha()" | ||||
:title="$t('contact.refreshCaptcha')" | |||||
:title="t('contact.refreshCaptcha')" | |||||
style="line-height: 0" | style="line-height: 0" | ||||
></div> | ></div> | ||||
<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-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" | ||||
:aria-label="$t('contact.refreshCaptcha')" | |||||
:title="$t('contact.refreshCaptcha')" | |||||
:aria-label="t('contact.refreshCaptcha')" | |||||
:title="t('contact.refreshCaptcha')" | |||||
> | > | ||||
<svg | <svg | ||||
xmlns="http://www.w3.org/2000/svg" | xmlns="http://www.w3.org/2000/svg" | ||||
> | > | ||||
{{ | {{ | ||||
isSubmitting | isSubmitting | ||||
? $t("contact.submitting") | |||||
: $t("contact.submit") | |||||
? t("contact.submitting") | |||||
: t("contact.submit") | |||||
}} | }} | ||||
</button> | </button> | ||||
</form> | </form> | ||||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | ||||
> | > | ||||
<div class="text-white text-3xl font-medium mb-6"> | <div class="text-white text-3xl font-medium mb-6"> | ||||
{{ $t("about.companyInfo.name") }} | |||||
{{ t("about.companyInfo.name") }} | |||||
</div> | </div> | ||||
<div class="flex flex-col gap-4"> | <div class="flex flex-col gap-4"> | ||||
<div class="flex items-center gap-4"> | <div class="flex items-center gap-4"> | ||||
<div class="text-white/60 text-base font-normal"> | <div class="text-white/60 text-base font-normal"> | ||||
{{ $t("about.overview.companyName") }} | |||||
{{ t("about.overview.companyName") }} | |||||
</div> | </div> | ||||
<div class="text-white text-base font-normal"> | <div class="text-white text-base font-normal"> | ||||
{{ $t("about.companyInfo.companyName") }} | |||||
{{ t("about.companyInfo.companyName") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="flex items-center gap-4"> | <div class="flex items-center gap-4"> | ||||
<div class="text-white/60 text-base font-normal"> | <div class="text-white/60 text-base font-normal"> | ||||
{{ $t("about.overview.location") }} | |||||
{{ t("about.overview.location") }} | |||||
</div> | </div> | ||||
<div class="text-white text-base font-normal"> | <div class="text-white text-base font-normal"> | ||||
{{ $t("about.companyInfo.location") }} | |||||
{{ t("about.companyInfo.location") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="flex items-center gap-4"> | <div class="flex items-center gap-4"> | ||||
<div class="text-white/60 text-base font-normal"> | <div class="text-white/60 text-base font-normal"> | ||||
{{ $t("about.overview.tel") }} | |||||
{{ t("about.overview.tel") }} | |||||
</div> | </div> | ||||
<div class="text-white text-base font-normal"> | <div class="text-white text-base font-normal"> | ||||
86)024-8399-0696 | 86)024-8399-0696 | ||||
</div> | </div> | ||||
<div class="flex items-center gap-4"> | <div class="flex items-center gap-4"> | ||||
<div class="text-white/60 text-base font-normal"> | <div class="text-white/60 text-base font-normal"> | ||||
{{ $t("about.overview.email") }} | |||||
{{ t("about.overview.email") }} | |||||
</div> | </div> | ||||
<div class="text-white text-base font-normal"> | <div class="text-white text-base font-normal"> | ||||
hanye@hanye.cn | hanye@hanye.cn | ||||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | ||||
> | > | ||||
<div class="text-white text-3xl font-medium mb-6"> | <div class="text-white text-3xl font-medium mb-6"> | ||||
{{ $t("about.overview.businessHours") }} | |||||
{{ t("about.overview.businessHours") }} | |||||
</div> | </div> | ||||
<div class="text-white text-base font-normal"> | <div class="text-white text-base font-normal"> | ||||
{{ $t("about.companyInfo.businessHours") }} | |||||
{{ t("about.companyInfo.businessHours") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> |
// FAQ数据 | // FAQ数据 | ||||
interface FAQ { | interface FAQ { | ||||
id: number; | |||||
id: string; | |||||
category: string; | category: string; | ||||
question: string; | question: string; | ||||
answer: string; | answer: string; | ||||
console.log('Loading FAQ data for locale:', locale.value); | console.log('Loading FAQ data for locale:', locale.value); | ||||
try { | try { | ||||
// 使用 queryCollection 加载 FAQ 数据 | // 使用 queryCollection 加载 FAQ 数据 | ||||
const content = await queryCollection('content').all(); | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/faq/${locale.value}/%`) | |||||
.all(); | |||||
// 在代码中过滤内容 | |||||
const filteredContent = content.filter((item: any) => { | |||||
// 检查路径是否包含当前语言 | |||||
return item._path && item._path.includes(`/faq/${locale.value}/`); | |||||
}); | |||||
if (!content || !Array.isArray(content)) { | |||||
console.error('No FAQ content found or invalid format:', content); | |||||
return []; | |||||
} | |||||
console.log('Raw FAQ data:', content); | |||||
// 转换数据格式 | // 转换数据格式 | ||||
const faqs = filteredContent.map((item: any) => ({ | |||||
id: item._id || '', | |||||
const faqItems = content.map((item: any) => { | |||||
// 提取路径中的ID | |||||
const pathParts = item.path?.split('/'); | |||||
const id = pathParts?.[pathParts.length - 1] || ''; | |||||
return { | |||||
id: id, | |||||
category: item.category || '', | category: item.category || '', | ||||
question: item.title || '', | question: item.title || '', | ||||
answer: item.body?.value || '', | |||||
answer: item.body || '', | |||||
title: item.title || '', | title: item.title || '', | ||||
description: item.description || '', | description: item.description || '', | ||||
sort: item.sort || 0 | sort: item.sort || 0 | ||||
})); | |||||
}; | |||||
}); | |||||
console.log('Processed FAQ items:', faqItems); | |||||
// 按 sort 字段排序 | // 按 sort 字段排序 | ||||
return faqs.sort((a, b) => a.sort - b.sort); | |||||
return faqItems.sort((a, b) => a.sort - b.sort); | |||||
} catch (error) { | } catch (error) { | ||||
console.error('Error loading FAQ content:', error); | console.error('Error loading FAQ content:', error); | ||||
return []; | return []; | ||||
watch: [locale] | watch: [locale] | ||||
}); | }); | ||||
console.log('FAQ data:', faqData.value); | |||||
// 处理FAQ数据变化 | // 处理FAQ数据变化 | ||||
watchEffect(() => { | watchEffect(() => { | ||||
if (faqData.value) { | if (faqData.value) { | ||||
// 从FAQ数据中提取所有不同的分类 | // 从FAQ数据中提取所有不同的分类 | ||||
const uniqueCategories = [ | const uniqueCategories = [ | ||||
...new Set(faqData.value.map((faq: FAQ) => faq.category)), | ...new Set(faqData.value.map((faq: FAQ) => faq.category)), | ||||
].sort(); // 对分类进行排序 | |||||
].filter(category => category).sort(); // 过滤掉空分类并排序 | |||||
console.log('Unique categories:', uniqueCategories); | |||||
// 设置分类列表和默认选中的分类 | // 设置分类列表和默认选中的分类 | ||||
categoriesList.value = [allOption, ...uniqueCategories]; | categoriesList.value = [allOption, ...uniqueCategories]; |
backgroundRepeat: 'no-repeat', | backgroundRepeat: 'no-repeat', | ||||
}" | }" | ||||
> | > | ||||
<div class="w-full h-full flex-col justify-center hidden md:flex relative z-10"> | |||||
<div | |||||
class="w-full h-full flex-col justify-center hidden md:flex relative z-10" | |||||
> | |||||
<div | <div | ||||
class="rounded border border-white w-11 h-6 leading-none justify-center flex items-center text-white text-sm font-normal" | class="rounded border border-white w-11 h-6 leading-none justify-center flex items-center text-white text-sm font-normal" | ||||
> | > | ||||
</div> | </div> | ||||
<div class="justify-center"> | <div class="justify-center"> | ||||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | <span class="text-white text-6xl font-normal leading-[78px]">{{ | ||||
$t("home.carousel.one.title") | |||||
t("home.carousel.one.title") | |||||
}}</span> | }}</span> | ||||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | <span class="text-white text-6xl font-normal leading-[78px]">{{ | ||||
$t("home.carousel.one.description") | |||||
t("home.carousel.one.description") | |||||
}}</span> | }}</span> | ||||
<br /> | <br /> | ||||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | <span class="text-white text-6xl font-normal leading-[78px]">{{ | ||||
$t("home.carousel.one.description2") | |||||
t("home.carousel.one.description2") | |||||
}}</span | }}</span | ||||
><br /> | ><br /> | ||||
<span | <span | ||||
class="text-cyan-400 text-6xl font-normal leading-[78px]" | class="text-cyan-400 text-6xl font-normal leading-[78px]" | ||||
>{{ $t("home.carousel.one.description3") }}</span | |||||
>{{ t("home.carousel.one.description3") }}</span | |||||
> | > | ||||
</div> | </div> | ||||
<div class="flex flex-col gap-2 mt-4"> | <div class="flex flex-col gap-2 mt-4"> | ||||
class="flex items-center gap-2 text-stone-50 text-xl font-normal" | class="flex items-center gap-2 text-stone-50 text-xl font-normal" | ||||
> | > | ||||
<div class="w-2 h-2 bg-white rounded-full"></div> | <div class="w-2 h-2 bg-white rounded-full"></div> | ||||
{{ $t("home.carousel.one.description4") }} | |||||
{{ t("home.carousel.one.description4") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="flex items-center gap-2 text-stone-50 text-xl font-normal" | class="flex items-center gap-2 text-stone-50 text-xl font-normal" | ||||
> | > | ||||
<div class="w-2 h-2 bg-white rounded-full"></div> | <div class="w-2 h-2 bg-white rounded-full"></div> | ||||
{{ $t("home.carousel.one.description5") }} | |||||
{{ t("home.carousel.one.description5") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
to="/products/1" | to="/products/1" | ||||
class="w-full h-full !flex items-center justify-center text-zinc-900" | class="w-full h-full !flex items-center justify-center text-zinc-900" | ||||
> | > | ||||
{{ $t("products.view_details") }} | |||||
{{ t("products.view_details") }} | |||||
</nuxt-link> | </nuxt-link> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4" | class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4" | ||||
> | > | ||||
{{ $t("products.usage") }} | |||||
{{ t("products.usage") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-center text-white font-normal mb-8 md:mb-16 text-xl sm:text-2xl md:text-4xl lg:text-6xl" | class="justify-center text-white font-normal mb-8 md:mb-16 text-xl sm:text-2xl md:text-4xl lg:text-6xl" | ||||
> | > | ||||
{{ $t("products.usage_title") }} | |||||
{{ t("products.usage_title") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="max-w-screen-2xl mx-auto"> | <div class="max-w-screen-2xl mx-auto"> | ||||
@click="handleUsageClick(usage.id)" | @click="handleUsageClick(usage.id)" | ||||
> | > | ||||
<div | <div | ||||
class="text-center text-xs sm:text-sm font-normal leading-tight md:text-base transition-colors duration-300 relative z-10" | |||||
class="usage-name text-center text-xs sm:text-sm font-normal leading-tight md:text-base transition-colors duration-300 relative z-10" | |||||
> | > | ||||
{{ usage.name }} | {{ usage.name }} | ||||
<!-- 用途名称 --> | |||||
</div> | </div> | ||||
<div | <div | ||||
class="absolute inset-0 rounded-full bg-cyan-400/20 scale-0 transition-transform duration-300 group-hover:scale-100" | class="absolute inset-0 rounded-full bg-cyan-400/20 scale-0 transition-transform duration-300 group-hover:scale-100" | ||||
<div | <div | ||||
class="px-4 py-2 bg-cyan-400/20 backdrop-blur-sm rounded-full text-white text-sm font-medium border border-cyan-400/30 transform transition-transform duration-300 group-hover:scale-105" | class="px-4 py-2 bg-cyan-400/20 backdrop-blur-sm rounded-full text-white text-sm font-medium border border-cyan-400/30 transform transition-transform duration-300 group-hover:scale-105" | ||||
> | > | ||||
{{ $t("products.view_details") }} | |||||
{{ t("products.view_details") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
class="flex flex-col gap-2 opacity-80 group-hover:opacity-100 transition-opacity duration-300" | class="flex flex-col gap-2 opacity-80 group-hover:opacity-100 transition-opacity duration-300" | ||||
> | > | ||||
<div | <div | ||||
v-for="feature in category.features" | |||||
:key="feature" | |||||
v-for="capacitie in category.capacities" | |||||
:key="capacitie" | |||||
class="text-white text-sm md:text-base font-normal leading-tight flex gap-2 items-center group-hover:text-cyan-400 transition-colors duration-300" | class="text-white text-sm md:text-base font-normal leading-tight flex gap-2 items-center group-hover:text-cyan-400 transition-colors duration-300" | ||||
> | > | ||||
<i | <i | ||||
class="icon-star text-sm group-hover:scale-110 transition-transform duration-300" | class="icon-star text-sm group-hover:scale-110 transition-transform duration-300" | ||||
></i> | ></i> | ||||
<span>{{ feature }}</span> | |||||
<span>{{ capacitie }}</span> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
<div | <div | ||||
class="justify-start text-neutral-200 text-xs md:text-sm font-medium uppercase leading-relaxed group-hover:text-cyan-400 transition-colors duration-300" | class="justify-start text-neutral-200 text-xs md:text-sm font-medium uppercase leading-relaxed group-hover:text-cyan-400 transition-colors duration-300" | ||||
> | > | ||||
{{ category.description }} | |||||
{{ category.title }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-start text-white font-medium text-1xl md:text-lg" | class="justify-start text-white font-medium text-1xl md:text-lg" | ||||
> | > | ||||
{{ $t("products.support") }} | |||||
{{ t("products.support") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | ||||
> | > | ||||
{{ $t("products.support_description") }} | |||||
{{ t("products.support_description") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-start text-white font-medium text-1xl md:text-lg" | class="justify-start text-white font-medium text-1xl md:text-lg" | ||||
> | > | ||||
{{ $t("products.development") }} | |||||
{{ t("products.development") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | ||||
> | > | ||||
{{ $t("products.development_description") }} | |||||
{{ t("products.development_description") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-start text-white font-medium text-1xl md:text-lg" | class="justify-start text-white font-medium text-1xl md:text-lg" | ||||
> | > | ||||
{{ $t("products.develop") }} | |||||
{{ t("products.develop") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | ||||
> | > | ||||
{{ $t("products.develop_description") }} | |||||
{{ t("products.develop_description") }} | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4" | class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4" | ||||
> | > | ||||
{{ $t("products.strong_point") }} | |||||
{{ t("products.strong_point") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="justify-center text-white font-normal mb-8 md:mb-16 text-xl sm:text-2xl md:text-4xl lg:text-6xl" | class="justify-center text-white font-normal mb-8 md:mb-16 text-xl sm:text-2xl md:text-4xl lg:text-6xl" | ||||
> | > | ||||
{{ $t("products.strong_point_title") }} | |||||
{{ t("products.strong_point_title") }} | |||||
</div> | </div> | ||||
<div | <div | ||||
class="absolute right-0 top-1/2 -translate-y-1/2 z-10 lg:block hidden" | class="absolute right-0 top-1/2 -translate-y-1/2 z-10 lg:block hidden" | ||||
<h1 | <h1 | ||||
class="text-center justify-start text-white font-normal text-xl sm:text-2xl md:text-3xl px-2" | class="text-center justify-start text-white font-normal text-xl sm:text-2xl md:text-3xl px-2" | ||||
> | > | ||||
{{ $t("products.consultation") }} | |||||
{{ t("products.consultation") }} | |||||
</h1> | </h1> | ||||
<nuxt-link | <nuxt-link | ||||
:to="locale === defaultLocale ? '/contact' : `/${locale}/contact`" | :to="locale === defaultLocale ? '/contact' : `/${locale}/contact`" | ||||
class="w-32 h-10 md:w-40 md:h-11 bg-zinc-300/10 rounded-lg outline outline-1 flex items-center justify-center gap-2 outline-white/20 backdrop-blur-[10px] cursor-pointer hover:bg-zinc-300/20 transition-colors duration-200" | class="w-32 h-10 md:w-40 md:h-11 bg-zinc-300/10 rounded-lg outline outline-1 flex items-center justify-center gap-2 outline-white/20 backdrop-blur-[10px] cursor-pointer hover:bg-zinc-300/20 transition-colors duration-200" | ||||
> | > | ||||
<span class="text-xs md:text-sm font-normal">{{ | <span class="text-xs md:text-sm font-normal">{{ | ||||
$t("products.consultation_button") | |||||
t("products.consultation_button") | |||||
}}</span> | }}</span> | ||||
<i class="icon-arrow-right text-sm font-normal"></i> | <i class="icon-arrow-right text-sm font-normal"></i> | ||||
</nuxt-link> | </nuxt-link> | ||||
import "swiper/css/pagination"; | import "swiper/css/pagination"; | ||||
import { useBreakpoints, breakpointsTailwind } from "@vueuse/core"; | import { useBreakpoints, breakpointsTailwind } from "@vueuse/core"; | ||||
import { useI18n } from "vue-i18n"; | |||||
import { useI18n, useRoute, useAsyncData, queryCollection } from "#imports"; | |||||
import video from "@/assets/videos/video.mp4"; | import video from "@/assets/videos/video.mp4"; | ||||
import videoWebp from "@/assets/videos/video.webp"; | import videoWebp from "@/assets/videos/video.webp"; | ||||
import homeA1Webp from "@/assets/images/home-a-1.webp"; | import homeA1Webp from "@/assets/images/home-a-1.webp"; | ||||
import homeC1Webp from "@/assets/images/home-c-1.webp"; | import homeC1Webp from "@/assets/images/home-c-1.webp"; | ||||
const { t, locale } = useI18n(); | |||||
const config = useRuntimeConfig(); | |||||
// 从运行时配置获取默认语言,如果未配置则默认为 'en' | |||||
const defaultLocale = config.public.i18n?.defaultLocale || "en"; | |||||
const videoSrc = ref(video); | |||||
// Define breakpoints | |||||
const breakpoints = useBreakpoints(breakpointsTailwind); | |||||
// Check if the device is mobile (smaller than md) | |||||
const isMobile = breakpoints.smaller("md"); | |||||
// 获取轮播图数据 | |||||
const { data: carouselData, error: carouselError } = await useFetch( | |||||
"/api/home" | |||||
); | |||||
const carouselList = ref(carouselData.value?.data || []); | |||||
// 获取按用途产品数据 | |||||
const { data: usageData, error: usageError } = await useFetch( | |||||
"/api/products/usage" | |||||
); | |||||
const usageList = ref(usageData.value?.data || []); | |||||
const activeUsageId = ref(1); | |||||
// 获取按分类栏目数据 | |||||
const { data: categoryData, error: categoryError } = await useFetch( | |||||
"/api/products/category" | |||||
); | |||||
const categoryList = ref(categoryData.value?.data || []); | |||||
// 数据类型定义 | |||||
interface Product { | interface Product { | ||||
id: number; | id: number; | ||||
title: string; | title: string; | ||||
interface Usage { | interface Usage { | ||||
id: number; | id: number; | ||||
name: string; | name: string; | ||||
category?: string; | |||||
products: Product[]; | products: Product[]; | ||||
} | } | ||||
id: number; | id: number; | ||||
title: string; | title: string; | ||||
description: string; | description: string; | ||||
features: string[]; | |||||
capacities: string[]; | |||||
summary: string; | |||||
sort: number; | |||||
image: string; | image: string; | ||||
link: string; | link: string; | ||||
} | } | ||||
// 使用i18n | |||||
const { t, locale } = useI18n(); | |||||
// 默认语言 | |||||
const defaultLocale = "ja"; | |||||
// 获取路由 | |||||
const route = useRoute(); | |||||
// 断点 | |||||
const breakpoints = useBreakpoints(breakpointsTailwind); | |||||
// 是否移动端 | |||||
const isMobile = breakpoints.smaller("md"); | |||||
const videoSrc = ref(video); | |||||
// 获取轮播图数据(由于没有对应的content文件,这里保留为静态数据) | |||||
const carouselList = ref([ | |||||
{ | |||||
id: 1, | |||||
title: "高性能SSD", | |||||
image: homeA1Webp, | |||||
link: "/products/W400-1TBSY01", | |||||
}, | |||||
]); | |||||
/** | |||||
* 使用计算属性获取当前语言的数据文件URL | |||||
*/ | |||||
const usageDataUrl = computed(() => { | |||||
return `/data/usages-${locale.value}.json`; | |||||
}); | |||||
const categoryDataUrl = computed(() => { | |||||
return `/data/categories-${locale.value}.json`; | |||||
}); | |||||
const productsDataUrl = computed(() => { | |||||
return `/data/products-${locale.value}.json`; | |||||
}); | |||||
// 获取按用途产品数据 | |||||
const usageList = ref<Usage[]>([]); | |||||
const isLoadingUsage = ref(true); | |||||
const activeUsageId = ref(1); | |||||
// 获取按分类栏目数据 | |||||
const categoryList = ref<Category[]>([]); | |||||
const isLoadingCategory = ref(true); | |||||
// 加载用途数据 | |||||
const loadUsageData = async () => { | |||||
try { | |||||
isLoadingUsage.value = true; | |||||
// 使用fetch获取数据 | |||||
if (process.client) { | |||||
const response = await fetch(usageDataUrl.value); | |||||
if (!response.ok) { | |||||
throw new Error(`加载用途数据失败: ${response.status}`); | |||||
} | |||||
const usageData = await response.json(); | |||||
// 加载对应的产品数据 | |||||
const productsResponse = await fetch(productsDataUrl.value); | |||||
if (!productsResponse.ok) { | |||||
throw new Error(`加载产品数据失败: ${productsResponse.status}`); | |||||
} | |||||
const productsData = await productsResponse.json(); | |||||
// 处理数据 | |||||
usageList.value = usageData | |||||
.map((usage: any) => { | |||||
console.log(`处理用途: ${usage.id} - ${usage.title}`, usage); | |||||
// 为每种用途找到对应的产品 | |||||
const usageProducts = []; | |||||
// 按照正确的设计模式:使用用途的title与产品的usage数组进行匹配 | |||||
// 找出所有usage属性包含当前用途title的产品 | |||||
const matchedProducts = productsData.filter( | |||||
(product: any) => | |||||
product.usage && | |||||
Array.isArray(product.usage) && | |||||
product.usage.includes(usage.title) | |||||
); | |||||
console.log( | |||||
`用途"${usage.title}"匹配到 ${matchedProducts.length} 个产品` | |||||
); | |||||
// 将匹配的产品添加到列表 | |||||
if (matchedProducts.length > 0) { | |||||
matchedProducts.forEach((product: any) => { | |||||
usageProducts.push({ | |||||
id: product.id, | |||||
title: product.title, | |||||
image: product.image, | |||||
link: `/products/${product.id}`, | |||||
description: product.summary, | |||||
}); | |||||
}); | |||||
} else { | |||||
// 如果没有找到匹配的产品,添加一个占位产品 | |||||
console.log(`用途 ${usage.title} 没有匹配到任何产品,添加占位产品`); | |||||
usageProducts.push({ | |||||
id: `placeholder-${usage.id}`, | |||||
title: usage.title || "未命名用途", | |||||
image: ``, | |||||
link: `/products?usage=${encodeURIComponent(usage.title)}`, | |||||
description: "", | |||||
}); | |||||
} | |||||
return { | |||||
id: parseInt(usage.id) || 0, | |||||
name: usage.title, | |||||
category: usage.category || "", | |||||
products: usageProducts, | |||||
}; | |||||
}) | |||||
.sort((a: any, b: any) => a.id - b.id); | |||||
// 设置默认选中的用途 | |||||
if (usageList.value.length > 0) { | |||||
activeUsageId.value = usageList.value[0].id; | |||||
} | |||||
} | |||||
} catch (error) { | |||||
console.error("Error loading usage data:", error); | |||||
} finally { | |||||
isLoadingUsage.value = false; | |||||
} | |||||
}; | |||||
// 加载分类数据 | |||||
const loadCategoryData = async () => { | |||||
try { | |||||
isLoadingCategory.value = true; | |||||
// 使用fetch获取数据 | |||||
if (process.client) { | |||||
const response = await fetch(categoryDataUrl.value); | |||||
if (!response.ok) { | |||||
throw new Error(`加载分类数据失败: ${response.status}`); | |||||
} | |||||
const data = await response.json(); | |||||
console.log("Raw category data:", data); | |||||
// 处理数据 | |||||
categoryList.value = data | |||||
.map((category: any) => { | |||||
return { | |||||
id: parseInt(category.id) || 0, | |||||
title: category.title || "", | |||||
description: category.description || "", | |||||
image: category.image || "", | |||||
link: `/products?category=${encodeURIComponent(category.title)}`, | |||||
capacities: category.capacities || [], | |||||
summary: category.summary || "", | |||||
sort: category.sort || 0, | |||||
}; | |||||
}) | |||||
.sort((a: any, b: any) => a.id - b.id); | |||||
} | |||||
} catch (error) { | |||||
console.error("Error loading category data:", error); | |||||
} finally { | |||||
isLoadingCategory.value = false; | |||||
} | |||||
}; | |||||
// 当页面加载或语言改变时加载数据 | |||||
onMounted(() => { | |||||
loadUsageData(); | |||||
loadCategoryData(); | |||||
}); | |||||
watch(locale, () => { | |||||
loadUsageData(); | |||||
loadCategoryData(); | |||||
}); | |||||
// 处理数据变化 | |||||
watchEffect(() => { | |||||
if (usageList.value) { | |||||
console.log("Updated usage list:", usageList.value); | |||||
} | |||||
if (categoryList.value) { | |||||
console.log("Updated category list:", categoryList.value); | |||||
} | |||||
}); | |||||
// 计算当前用途列表 | // 计算当前用途列表 | ||||
const typedUsageList = computed(() => { | const typedUsageList = computed(() => { | ||||
console.log("Typed Usage List:", usageList.value); | console.log("Typed Usage List:", usageList.value); | ||||
const currentUsage = typedUsageList.value.find( | const currentUsage = typedUsageList.value.find( | ||||
(item: Usage) => item.id === activeUsageId.value | (item: Usage) => item.id === activeUsageId.value | ||||
); | ); | ||||
console.log("Current Usage:", currentUsage); | |||||
if (currentUsage?.products) { | if (currentUsage?.products) { | ||||
console.log( | console.log( | ||||
"Products:", | "Products:", | ||||
// 计算当前分类列表 | // 计算当前分类列表 | ||||
const typedCategoryList = computed(() => { | const typedCategoryList = computed(() => { | ||||
console.log("Typed Category List:", categoryList.value); | |||||
return categoryList.value as Category[]; | return categoryList.value as Category[]; | ||||
}); | }); | ||||
* 展示产品主图、参数和描述 | * 展示产品主图、参数和描述 | ||||
*/ | */ | ||||
import { useErrorHandler } from "~/composables/useErrorHandler"; | import { useErrorHandler } from "~/composables/useErrorHandler"; | ||||
import { useAsyncData, useRoute, useI18n } from "#imports"; | |||||
import { useRoute, useI18n, useAsyncData } from "#imports"; | |||||
import { queryCollection } from "#imports"; | import { queryCollection } from "#imports"; | ||||
import { ContentRenderer } from "#components"; | import { ContentRenderer } from "#components"; | ||||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||||
const { error, isLoading } = useErrorHandler(); | |||||
const route = useRoute(); | const route = useRoute(); | ||||
const product = ref<Product | null>(null); | |||||
const relatedProducts = ref<Product[]>([]); | |||||
const { locale, t } = useI18n(); | const { locale, t } = useI18n(); | ||||
const id = route.params.id as string; | |||||
// 图片状态 | |||||
const currentImage = ref<string>(""); | const currentImage = ref<string>(""); | ||||
const isImageLoading = ref(true); | const isImageLoading = ref(true); | ||||
const isThumbnailLoading = ref<boolean[]>([]); | const isThumbnailLoading = ref<boolean[]>([]); | ||||
title?: string; | title?: string; | ||||
} | } | ||||
interface ContentCollectionItem { | |||||
_path?: string; | |||||
_id?: string; | |||||
name?: string; | |||||
title?: string; | |||||
usage?: string[]; | |||||
capacities?: string[]; | |||||
categoryId?: string; | |||||
description?: string; | |||||
summary?: string; | |||||
image?: string; | |||||
gallery?: string[]; | |||||
body?: string; | |||||
content?: any; | |||||
series?: string[]; | |||||
meta?: { | |||||
series?: string[]; | |||||
name?: string; | |||||
title?: string; | |||||
image?: string; | |||||
summary?: string; | |||||
}; | |||||
} | |||||
interface ContentData { | |||||
path: string; | |||||
name?: string; | |||||
title?: string; | |||||
usage?: string[]; | |||||
capacities?: string[]; | |||||
categoryId?: string; | |||||
category?: string; | |||||
description?: string; | |||||
summary?: string; | |||||
image?: string; | |||||
gallery?: string[]; | |||||
body?: string; | |||||
content?: any; | |||||
meta?: { | |||||
series?: string[]; | |||||
name?: string; | |||||
title?: string; | |||||
image?: string; | |||||
summary?: string; | |||||
}; | |||||
} | |||||
/** | |||||
* 将 ContentCollectionItem 转换为 ContentData | |||||
*/ | |||||
function convertToContentData(item: any): ContentData { | |||||
console.log("Raw item:", item); // 添加原始数据日志 | |||||
// 直接从原始数据中获取字段 | |||||
const converted = { | |||||
path: item._path || "", | |||||
name: item.name || item.title || "", | |||||
title: item.title || "", | |||||
usage: item.meta.usage || [], | |||||
capacities: item.meta.capacities || [], | |||||
categoryId: item.meta.categoryId || "", | |||||
category: item.meta.category || "", | |||||
description: item.description || "", | |||||
summary: item.meta.summary || "", | |||||
image: item.meta.image || "", | |||||
gallery: item.meta.gallery || [], | |||||
body: item.body || "", | |||||
content: item.body || null, | |||||
series: item.meta.series || [], | |||||
}; | |||||
console.log("Converting item:", item); // 添加日志 | |||||
console.log("Converted result:", converted); // 添加日志 | |||||
return converted; | |||||
} | |||||
/** | /** | ||||
* 加载产品详情 | |||||
* 使用queryCollection获取产品数据 | |||||
*/ | */ | ||||
async function loadProduct() { | |||||
const { data: productContent } = await useAsyncData(`product-${id}`, async () => { | |||||
try { | try { | ||||
const id = route.params.id as string; | |||||
if (!id) { | |||||
throw new Error("Product ID is required"); | |||||
} | |||||
const { data } = await useAsyncData(`product-${id}`, () => | |||||
queryCollection("content") | |||||
// 使用queryCollection从content目录获取数据 | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/products/${locale.value}/${id}`) | .where("path", "LIKE", `/products/${locale.value}/${id}`) | ||||
.first() | |||||
); | |||||
.first(); | |||||
return content; | |||||
} catch (err) { | |||||
console.error("Error fetching product content:", err); | |||||
error.value = new Error(t("products.loadError")); | |||||
return null; | |||||
} | |||||
}); | |||||
if (!data.value) { | |||||
throw new Error("Product not found"); | |||||
/** | |||||
* 获取分类信息 | |||||
*/ | |||||
const { data: categoryContent } = await useAsyncData( | |||||
`category-${productContent.value?.categoryId}`, | |||||
async () => { | |||||
if (!productContent.value?.categoryId) return null; | |||||
try { | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/categories/${locale.value}/${productContent.value.categoryId}`) | |||||
.first(); | |||||
return content; | |||||
} catch (err) { | |||||
console.error("Error fetching category:", err); | |||||
return null; | |||||
} | } | ||||
}, | |||||
{ | |||||
immediate: !!productContent.value?.categoryId | |||||
} | |||||
); | |||||
const rawData = data.value as unknown as any; | |||||
console.log("Raw product data:", rawData); // 添加日志 | |||||
const productData = convertToContentData(rawData); | |||||
console.log("Converted product data:", productData); // 添加日志 | |||||
// 获取分类信息 | |||||
const { data: categoryData } = await useAsyncData( | |||||
`category-${productData.categoryId}`, | |||||
() => | |||||
queryCollection("content") | |||||
.where( | |||||
"path", | |||||
"LIKE", | |||||
`/categories/${locale.value}/${productData.categoryId}` | |||||
) | |||||
.first() | |||||
); | |||||
console.log("Category data:", categoryData.value); // 添加日志 | |||||
const categoryItem = categoryData.value | |||||
? convertToContentData(categoryData.value as unknown as any) | |||||
: null; | |||||
console.log("Converted category data:", categoryItem); // 添加日志 | |||||
// 设置产品数据,添加默认值 | |||||
product.value = { | |||||
/** | |||||
* 使用计算属性解析产品数据 | |||||
*/ | |||||
const product = computed<Product | null>(() => { | |||||
if (!productContent.value) return null; | |||||
// 提取产品数据 | |||||
const meta = productContent.value.meta || {}; | |||||
return { | |||||
id: id, | id: id, | ||||
name: productData.name || productData.title || "", | |||||
title: productData.title || productData.name || "", | |||||
usage: productData.usage || [], | |||||
capacities: productData.capacities || [], | |||||
category: categoryItem?.title || productData.category || "", | |||||
description: productData.description || "", | |||||
summary: productData.summary || "", | |||||
image: productData.image || "", | |||||
gallery: productData.gallery || [], | |||||
body: productData.body || "", | |||||
content: productData.content || null, | |||||
name: String(meta.name || productContent.value.title || ""), | |||||
title: String(productContent.value.title || meta.name || ""), | |||||
usage: Array.isArray(meta.usage) ? meta.usage : [], | |||||
capacities: Array.isArray(meta.capacities) ? meta.capacities : [], | |||||
category: categoryContent.value?.title || "", | |||||
description: productContent.value.description || "", | |||||
summary: String(meta.summary || ""), | |||||
image: String(meta.image || ""), | |||||
gallery: Array.isArray(meta.gallery) ? meta.gallery : [], | |||||
body: productContent.value.body || "", | |||||
content: productContent.value, | |||||
meta: { | meta: { | ||||
series: productData.meta?.series || [], | |||||
name: productData.name, | |||||
title: productData.title, | |||||
image: productData.image, | |||||
summary: productData.summary, | |||||
series: Array.isArray(meta.series) ? meta.series : [], | |||||
name: String(meta.name || ""), | |||||
title: String(productContent.value.title || ""), | |||||
image: String(meta.image || ""), | |||||
summary: String(meta.summary || ""), | |||||
}, | }, | ||||
}; | }; | ||||
}); | |||||
console.log("Final product data:", product.value); // 添加日志 | |||||
// 设置当前图片 | |||||
if (product.value?.image) { | |||||
currentImage.value = product.value.image; | |||||
} | |||||
// 加载相关产品 | |||||
await loadRelatedProducts(); | |||||
/** | |||||
* 获取相关产品 | |||||
*/ | |||||
const { data: relatedProductsContent } = await useAsyncData( | |||||
`related-products-${id}`, | |||||
async () => { | |||||
try { | |||||
// 获取产品列表 | |||||
const content = await queryCollection("content") | |||||
.where("path", "LIKE", `/products/${locale.value}/%`) | |||||
.all(); | |||||
return content; | |||||
} catch (err) { | } catch (err) { | ||||
console.error("Error loading product:", err); | |||||
error.value = new Error(t("products.loadError")); | |||||
console.error("Error fetching related products:", err); | |||||
return []; | |||||
} | |||||
} | } | ||||
} | |||||
); | |||||
/** | |||||
* 处理相关产品数据 | |||||
*/ | |||||
const relatedProducts = computed(() => { | |||||
if (!relatedProductsContent.value || !product.value) return []; | |||||
return relatedProductsContent.value | |||||
.filter((item: any) => item._path !== `/products/${locale.value}/${id}`) | |||||
.map((item: any) => { | |||||
const meta = item.meta || {}; | |||||
return { | |||||
id: item._path?.split('/').pop() || "", | |||||
name: meta.name || item.title || "", | |||||
title: item.title || meta.name || "", | |||||
image: meta.image || "", | |||||
summary: meta.summary || "", | |||||
}; | |||||
}) | |||||
.slice(0, 6); // 最多显示6个相关产品 | |||||
}); | |||||
/** | /** | ||||
* 预加载下一张图片 | * 预加载下一张图片 | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 加载相关产品 | |||||
*/ | |||||
async function loadRelatedProducts() { | |||||
try { | |||||
if (!product.value) return; | |||||
const { data } = await useAsyncData( | |||||
`related-products-${product.value.id}`, | |||||
() => | |||||
queryCollection("content") | |||||
.where("path", "LIKE", `/products/${locale.value}/%`) | |||||
.all() | |||||
); | |||||
if (!data.value) return; | |||||
const relatedItems = (data.value as unknown as ContentCollectionItem[]).map( | |||||
(item) => convertToContentData(item) | |||||
); | |||||
relatedProducts.value = relatedItems | |||||
.filter( | |||||
(item) => item.path !== `/products/${locale.value}/${product.value?.id}` | |||||
) | |||||
.map((item) => ({ | |||||
id: item.path.split("/").pop() || "", | |||||
name: item.name || item.title || "", | |||||
title: item.title || item.name || "", | |||||
usage: item.usage || [], | |||||
capacities: item.capacities || [], | |||||
category: "", | |||||
description: item.description || "", | |||||
summary: item.summary || "", | |||||
image: item.image || "", | |||||
gallery: item.gallery || [], | |||||
body: item.body || "", | |||||
meta: { | |||||
series: item.meta?.series || [], | |||||
name: item.name, | |||||
title: item.title, | |||||
image: item.image, | |||||
summary: item.summary, | |||||
}, | |||||
})); | |||||
} catch (err) { | |||||
console.error("Error loading related products:", err); | |||||
} | |||||
} | |||||
// 页面加载时获取产品数据 | |||||
// 页面加载时初始化状态 | |||||
onMounted(() => { | onMounted(() => { | ||||
loadProduct(); | |||||
// 设置当前图片 | |||||
if (product.value?.image) { | |||||
currentImage.value = product.value.image; | |||||
} | |||||
// 初始化缩略图加载状态数组 | // 初始化缩略图加载状态数组 | ||||
isThumbnailLoading.value = Array(4).fill(true); | |||||
thumbnailErrors.value = Array(4).fill(false); | |||||
const galleryLength = (product.value?.gallery?.length || 0) + 1; // +1是因为主图也算一张 | |||||
isThumbnailLoading.value = Array(galleryLength).fill(true); | |||||
thumbnailErrors.value = Array(galleryLength).fill(false); | |||||
}); | }); | ||||
// 监听路由参数变化 | |||||
watch( | |||||
() => route.params.id, | |||||
async (newId) => { | |||||
if (newId) { | |||||
await loadProduct(); | |||||
} | |||||
}, | |||||
{ immediate: true } | |||||
); | |||||
// 监听语言变化 | |||||
watch( | |||||
() => locale.value, | |||||
async () => { | |||||
if (route.params.id) { | |||||
await loadProduct(); | |||||
} | |||||
} | |||||
); | |||||
// 监听产品数据变化,加载相关产品 | |||||
watch( | |||||
() => product.value, | |||||
() => { | |||||
if (product.value) { | |||||
loadRelatedProducts(); | |||||
} | |||||
} | |||||
); | |||||
// SEO优化 | // SEO优化 | ||||
useHead(() => ({ | useHead(() => ({ | ||||
title: `${product.value?.name || "产品详情"} - Hanye`, | title: `${product.value?.name || "产品详情"} - Hanye`, |
[ | |||||
"/", | |||||
"/products", | |||||
"/faq", | |||||
"/contact", | |||||
"/about", | |||||
"/ja", | |||||
"/ja/products", | |||||
"/ja/faq", | |||||
"/ja/contact", | |||||
"/ja/about", | |||||
"/en", | |||||
"/en/products", | |||||
"/en/faq", | |||||
"/en/contact", | |||||
"/en/about", | |||||
"/ja/products/CR-201BK", | |||||
"/ja/products/DDR3-SODIMM-8GB-1.35V", | |||||
"/ja/products/DDR3-SODIMM-8GB-1.5V", | |||||
"/ja/products/DDR3-UDIMM-8GB-1.5V", | |||||
"/ja/products/E30-1TBTN1", | |||||
"/ja/products/E30-256GTN1", | |||||
"/ja/products/E30-2TBTN1", | |||||
"/ja/products/E30-512GTN1", | |||||
"/ja/products/HE70-1TBNHS1", | |||||
"/ja/products/HE70-2TBNHS1", | |||||
"/ja/products/HE70-4TBNHS1", | |||||
"/ja/products/HE71-2TBNDGH", | |||||
"/ja/products/HE80-2TGHS", | |||||
"/ja/products/HE80-2TNLHS", | |||||
"/ja/products/HSE-M20GRC01", | |||||
"/ja/products/HSE-M40GRC01", | |||||
"/ja/products/HSE-M4IN1GRC01", | |||||
"/ja/products/HSE-M6IN1GRC01", | |||||
"/ja/products/M200-SC256G", | |||||
"/ja/products/M200-SC512G", | |||||
"/ja/products/ME70-1TA01", | |||||
"/ja/products/ME70-2TA01", | |||||
"/ja/products/MN50-1000GA01", | |||||
"/ja/products/MN50-512GA01", | |||||
"/ja/products/N400-128GSY03", | |||||
"/ja/products/N400-1TSY03", | |||||
"/ja/products/N400-256GSY03", | |||||
"/ja/products/N400-512GSY03", | |||||
"/ja/products/PE-CR003", | |||||
"/ja/products/PE-CR405", | |||||
"/ja/products/Q60-1TST3", | |||||
"/ja/products/Q60-256GST3", | |||||
"/ja/products/Q60-2TST3", | |||||
"/ja/products/Q60-512GST3", | |||||
"/ja/products/SD-HC32GBU1", | |||||
"/ja/products/SD-XC128GBU3", | |||||
"/ja/products/SD-XC64GBU3", | |||||
"/ja/products/SD4-08GB-2666-1R8", | |||||
"/ja/products/SD4-08GB-3200-1R8", | |||||
"/ja/products/SD4-16GB-2666-2R8", | |||||
"/ja/products/SD4-16GB-3200-2R8", | |||||
"/ja/products/UD-032G01A1", | |||||
"/ja/products/UD-064G01A1", | |||||
"/ja/products/UD4-08GB-2666-1R8", | |||||
"/ja/products/UD4-08GB-3200-1R8", | |||||
"/ja/products/UD4-16GB-2666-2R8", | |||||
"/ja/products/UD4-16GB-3200-2R8", | |||||
"/ja/products/W400-128GSY01", | |||||
"/ja/products/W400-1TBSY01", | |||||
"/ja/products/W400-256GSY01", | |||||
"/ja/products/W400-512GSY01", | |||||
"/ja/products?usage=%E5%86%99%E7%9C%9F%E3%83%BB%E3%83%93%E3%83%87%E3%82%AA%E6%92%AE%E5%BD%B1%E7%94%A8%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8", | |||||
"/ja/products?usage=%E3%82%B9%E3%83%9E%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%B3%2F%E3%82%BF%E3%83%96%E3%83%AC%E3%83%83%E3%83%88%E7%94%A8%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%E6%8B%A1%E5%BC%B5", | |||||
"/ja/products?usage=%E3%82%B2%E3%83%BC%E3%83%A0%E7%94%A8%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%2F%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E5%90%91%E4%B8%8A", | |||||
"/ja/products?usage=%E3%82%AA%E3%83%95%E3%82%A3%E3%82%B9%2F%E5%AD%A6%E7%BF%92%E7%94%A8%E3%82%B9%E3%83%88%E3%83%AC%E3%83%BC%E3%82%B8%E6%8B%A1%E5%BC%B5", | |||||
"/ja/products?usage=%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB%E3%83%87%E3%83%BC%E3%82%BF%E8%BB%A2%E9%80%81%2F%E3%83%90%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97", | |||||
"/ja/products?usage=%E3%83%A1%E3%83%87%E3%82%A3%E3%82%A2%2F%E3%82%A8%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%86%E3%82%A4%E3%83%A1%E3%83%B3%E3%83%88", | |||||
"/ja/products?usage=%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E5%88%B6%E4%BD%9C%2F%E5%8B%95%E7%94%BB%E7%B7%A8%E9%9B%86", | |||||
"/ja/products?usage=%E3%83%A1%E3%83%A2%E3%83%AA%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89%2F%E3%83%91%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%B3%E3%82%B9%E5%90%91%E4%B8%8A", | |||||
"/ja/products?category=1", | |||||
"/ja/products?category=2", | |||||
"/ja/products?category=3", | |||||
"/ja/products?category=4", | |||||
"/ja/products?category=5", | |||||
"/ja/products?category=6", | |||||
"/en/products/CR-201BK", | |||||
"/en/products/DDR3-SODIMM-8GB-1.35V", | |||||
"/en/products/DDR3-SODIMM-8GB-1.5V", | |||||
"/en/products/DDR3-UDIMM-8GB-1.5V", | |||||
"/en/products/E30-1TBTN1", | |||||
"/en/products/E30-256GTN1", | |||||
"/en/products/E30-2TBTN1", | |||||
"/en/products/E30-512GTN1", | |||||
"/en/products/HE70-1TBNHS1", | |||||
"/en/products/HE70-2TBNHS1", | |||||
"/en/products/HE70-4TBNHS1", | |||||
"/en/products/HE71-2TBNDGH", | |||||
"/en/products/HE80-2TGHS", | |||||
"/en/products/HE80-2TNLHS", | |||||
"/en/products/HSE-M20GRC01", | |||||
"/en/products/HSE-M40GRC01", | |||||
"/en/products/HSE-M4IN1GRC01", | |||||
"/en/products/HSE-M6IN1GRC01", | |||||
"/en/products/M200-SC256G", | |||||
"/en/products/M200-SC512G", | |||||
"/en/products/ME70-1TA01", | |||||
"/en/products/ME70-2TA01", | |||||
"/en/products/MN50-1000GA01", | |||||
"/en/products/MN50-512GA01", | |||||
"/en/products/N400-128GSY03", | |||||
"/en/products/N400-1TSY03", | |||||
"/en/products/N400-256GSY03", | |||||
"/en/products/N400-512GSY03", | |||||
"/en/products/PE-CR003", | |||||
"/en/products/PE-CR405", | |||||
"/en/products/Q60-1TST3", | |||||
"/en/products/Q60-256GST3", | |||||
"/en/products/Q60-2TST3", | |||||
"/en/products/Q60-512GST3", | |||||
"/en/products/SD-HC32GBU1", | |||||
"/en/products/SD-XC128GBU3", | |||||
"/en/products/SD-XC64GBU3", | |||||
"/en/products/SD4-08GB-2666-1R8", | |||||
"/en/products/SD4-08GB-3200-1R8", | |||||
"/en/products/SD4-16GB-2666-2R8", | |||||
"/en/products/SD4-16GB-3200-2R8", | |||||
"/en/products/UD-032G01A1", | |||||
"/en/products/UD-064G01A1", | |||||
"/en/products/UD4-08GB-2666-1R8", | |||||
"/en/products/UD4-08GB-3200-1R8", | |||||
"/en/products/UD4-16GB-2666-2R8", | |||||
"/en/products/UD4-16GB-3200-2R8", | |||||
"/en/products/W400-128GSY01", | |||||
"/en/products/W400-1TBSY01", | |||||
"/en/products/W400-256GSY01", | |||||
"/en/products/W400-512GSY01", | |||||
"/en/products?usage=Photography%20Storage", | |||||
"/en/products?usage=Mobile%20Device%20Storage", | |||||
"/en/products?usage=Gaming%20Storage", | |||||
"/en/products?usage=System%20Boot%20Drive", | |||||
"/en/products?usage=Mobile%20Data%20Transfer%2FBackup", | |||||
"/en/products?usage=Media%2FEntertainment", | |||||
"/en/products?usage=Content%20Creation%2FVideo%20Editing", | |||||
"/en/products?usage=Memory%20Upgrade%2FPerformance%20Enhancement", | |||||
"/en/products?category=1", | |||||
"/en/products?category=2", | |||||
"/en/products?category=3", | |||||
"/en/products?category=4", | |||||
"/en/products?category=5", | |||||
"/en/products?category=6", | |||||
"/products/CR-201BK", | |||||
"/products/DDR3-SODIMM-8GB-1.35V", | |||||
"/products/DDR3-SODIMM-8GB-1.5V", | |||||
"/products/DDR3-UDIMM-8GB-1.5V", | |||||
"/products/E30-1TBTN1", | |||||
"/products/E30-256GTN1", | |||||
"/products/E30-2TBTN1", | |||||
"/products/E30-512GTN1", | |||||
"/products/HE70-1TBNHS1", | |||||
"/products/HE70-2TBNHS1", | |||||
"/products/HE70-4TBNHS1", | |||||
"/products/HE71-2TBNDGH", | |||||
"/products/HE80-2TGHS", | |||||
"/products/HE80-2TNLHS", | |||||
"/products/HSE-M20GRC01", | |||||
"/products/HSE-M40GRC01", | |||||
"/products/HSE-M4IN1GRC01", | |||||
"/products/HSE-M6IN1GRC01", | |||||
"/products/M200-SC256G", | |||||
"/products/M200-SC512G", | |||||
"/products/ME70-1TA01", | |||||
"/products/ME70-2TA01", | |||||
"/products/MN50-1000GA01", | |||||
"/products/MN50-512GA01", | |||||
"/products/N400-128GSY03", | |||||
"/products/N400-1TSY03", | |||||
"/products/N400-256GSY03", | |||||
"/products/N400-512GSY03", | |||||
"/products/PE-CR003", | |||||
"/products/PE-CR405", | |||||
"/products/Q60-1TST3", | |||||
"/products/Q60-256GST3", | |||||
"/products/Q60-2TST3", | |||||
"/products/Q60-512GST3", | |||||
"/products/SD-HC32GBU1", | |||||
"/products/SD-XC128GBU3", | |||||
"/products/SD-XC64GBU3", | |||||
"/products/SD4-08GB-2666-1R8", | |||||
"/products/SD4-08GB-3200-1R8", | |||||
"/products/SD4-16GB-2666-2R8", | |||||
"/products/SD4-16GB-3200-2R8", | |||||
"/products/UD-032G01A1", | |||||
"/products/UD-064G01A1", | |||||
"/products/UD-128G01A1", | |||||
"/products/UD-256G01A1", | |||||
"/products/UD-512G01A1", | |||||
"/products/UD4-08GB-2666-1R8", | |||||
"/products/UD4-08GB-3200-1R8", | |||||
"/products/UD4-16GB-2666-2R8", | |||||
"/products/UD4-16GB-3200-2R8", | |||||
"/products/W400-128GSY01", | |||||
"/products/W400-1TBSY01", | |||||
"/products/W400-256GSY01", | |||||
"/products/W400-512GSY01", | |||||
"/products?usage=%E6%91%84%E5%BD%B1%E5%AD%98%E5%82%A8", | |||||
"/products?usage=%E6%89%8B%E6%9C%BA%2F%E5%B9%B3%E6%9D%BF%E6%89%A9%E5%AE%B9", | |||||
"/products?usage=%E6%B8%B8%E6%88%8F%E5%8A%A0%E9%80%9F%2F%E5%AD%98%E5%82%A8", | |||||
"/products?usage=%E5%8A%9E%E5%85%AC%2F%E5%AD%A6%E4%B9%A0%E6%89%A9%E5%AE%B9", | |||||
"/products?usage=%E7%A7%BB%E5%8A%A8%E6%95%B0%E6%8D%AE%E4%BC%A0%E8%BE%93%2F%E5%A4%87%E4%BB%BD", | |||||
"/products?usage=%E5%AA%92%E4%BD%93%2F%E5%BD%B1%E9%9F%B3%E5%A8%B1%E4%B9%90", | |||||
"/products?usage=%E5%86%85%E5%AE%B9%E5%88%9B%E4%BD%9C%2F%E8%A7%86%E9%A2%91%E7%BC%96%E8%BE%91", | |||||
"/products?usage=%E5%86%85%E5%AD%98%E5%8D%87%E7%BA%A7%2F%E6%80%A7%E8%83%BD%E6%8F%90%E5%8D%87", | |||||
"/products?category=1", | |||||
"/products?category=2", | |||||
"/products?category=3", | |||||
"/products?category=4", | |||||
"/products?category=5", | |||||
"/products?category=6" | |||||
] |
[ | |||||
{ | |||||
"id": "1", | |||||
"title": "SD Cards", | |||||
"description": "SD cards for cameras, camcorders, and other devices", | |||||
"image": "/images/categories/sd-card.jpg", | |||||
"summary": "High-speed, high-capacity SD cards for professional photography and videography", | |||||
"capacities": [ | |||||
"32GB", | |||||
"64GB", | |||||
"128GB", | |||||
"256GB", | |||||
"512GB", | |||||
"1TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "2", | |||||
"title": "microSD Cards", | |||||
"description": "microSD cards for smartphones, tablets, action cameras, and other mobile devices", | |||||
"image": "/images/categories/microsd-card.jpg", | |||||
"summary": "Compact microSD cards providing additional storage for mobile devices", | |||||
"capacities": [ | |||||
"32GB", | |||||
"64GB", | |||||
"128GB", | |||||
"256GB", | |||||
"512GB", | |||||
"1TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "3", | |||||
"title": "2.5-inch SSD & M.2 SSD", | |||||
"description": "Solid state drives for computers, game consoles, and other devices", | |||||
"image": "/images/categories/ssd.jpg", | |||||
"summary": "High-speed, reliable SSDs providing excellent performance and storage capacity", | |||||
"capacities": [ | |||||
"256GB", | |||||
"512GB", | |||||
"1TB", | |||||
"2TB", | |||||
"4TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "4", | |||||
"title": "Memory Modules", | |||||
"description": "Memory modules for computers, servers, and other devices", | |||||
"image": "/images/categories/ram.jpg", | |||||
"summary": "High-performance memory modules providing fast data access and processing capabilities", | |||||
"capacities": [ | |||||
"8GB", | |||||
"16GB", | |||||
"32GB", | |||||
"64GB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "5", | |||||
"title": "Card Readers", | |||||
"description": "Devices for reading various types of memory cards", | |||||
"image": "/images/categories/card-reader.jpg", | |||||
"summary": "Card readers supporting multiple memory card formats, providing fast data transfer", | |||||
"capacities": [ | |||||
"USB 3.0", | |||||
"USB 3.1", | |||||
"USB-C" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "6", | |||||
"title": "External Enclosures", | |||||
"description": "Enclosures for external hard drives and SSDs", | |||||
"image": "/images/categories/enclosure.jpg", | |||||
"summary": "External enclosures providing convenient storage expansion and data backup solutions", | |||||
"capacities": [ | |||||
"2.5-inch", | |||||
"3.5-inch", | |||||
"M.2" | |||||
] | |||||
} | |||||
] |
[ | |||||
{ | |||||
"id": "1", | |||||
"title": "SDカード", | |||||
"description": "カメラ、ビデオカメラなどのデバイス用のSDカード", | |||||
"image": "/images/categories/sd-card.jpg", | |||||
"summary": "プロフェッショナルな写真撮影やビデオ撮影に適した高速・大容量のSDカード", | |||||
"capacities": [ | |||||
"32GB", | |||||
"64GB", | |||||
"128GB", | |||||
"256GB", | |||||
"512GB", | |||||
"1TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "2", | |||||
"title": "microSDカード", | |||||
"description": "スマートフォン、タブレット、アクションカメラなどのデバイス用のmicroSDカード", | |||||
"image": "/images/categories/microsd-card.jpg", | |||||
"summary": "モバイルデバイスに追加のストレージ容量を提供するコンパクトなmicroSDカード", | |||||
"capacities": [ | |||||
"32GB", | |||||
"64GB", | |||||
"128GB", | |||||
"256GB", | |||||
"512GB", | |||||
"1TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "3", | |||||
"title": "2.5インチ SSD & M.2 SSD", | |||||
"description": "パソコン、ゲーム機などのデバイス用のソリッドステートドライブ", | |||||
"image": "/images/categories/ssd.jpg", | |||||
"summary": "優れたパフォーマンスとストレージ容量を提供する高速・信頼性の高いSSD", | |||||
"capacities": [ | |||||
"256GB", | |||||
"512GB", | |||||
"1TB", | |||||
"2TB", | |||||
"4TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "4", | |||||
"title": "メモリ", | |||||
"description": "パソコン、サーバーなどのデバイス用のメモリモジュール", | |||||
"image": "/images/categories/ram.jpg", | |||||
"summary": "高速なデータアクセスと処理能力を提供する高性能メモリ", | |||||
"capacities": [ | |||||
"8GB", | |||||
"16GB", | |||||
"32GB", | |||||
"64GB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "5", | |||||
"title": "カードリーダー", | |||||
"description": "様々なメモリカードを読み取るためのデバイス", | |||||
"image": "/images/categories/card-reader.jpg", | |||||
"summary": "複数のメモリカードフォーマットをサポートし、高速なデータ転送を提供するカードリーダー", | |||||
"capacities": [ | |||||
"USB 3.0", | |||||
"USB 3.1", | |||||
"USB-C" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "6", | |||||
"title": "外付けケース", | |||||
"description": "内蔵ハードディスクを外付けストレージデバイスに変換するためのデバイス", | |||||
"image": "/images/categories/hdd-enclosure.jpg", | |||||
"summary": "複数のインターフェースをサポートし、便利なストレージソリューションを提供する外付けケース", | |||||
"capacities": [ | |||||
"USB 3.0", | |||||
"USB 3.1", | |||||
"USB-C", | |||||
"Thunderbolt" | |||||
] | |||||
} | |||||
] |
[ | |||||
{ | |||||
"id": "1", | |||||
"title": "SD存储卡", | |||||
"description": "适用于相机、摄像机等设备的SD存储卡", | |||||
"image": "/images/categories/sd-card.jpg", | |||||
"summary": "高速、大容量的SD存储卡,适用于专业摄影和摄像", | |||||
"capacities": [ | |||||
"32GB", | |||||
"64GB", | |||||
"128GB", | |||||
"256GB", | |||||
"512GB", | |||||
"1TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "2", | |||||
"title": "microSD存储卡", | |||||
"description": "适用于手机、平板、运动相机等设备的microSD存储卡", | |||||
"image": "/images/categories/microsd-card.jpg", | |||||
"summary": "小巧便携的microSD存储卡,为移动设备提供额外存储空间", | |||||
"capacities": [ | |||||
"32GB", | |||||
"64GB", | |||||
"128GB", | |||||
"256GB", | |||||
"512GB", | |||||
"1TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "3", | |||||
"title": "2.5英寸 SSD & M.2 SSD", | |||||
"description": "适用于电脑、游戏主机等设备的固态硬盘", | |||||
"image": "/images/categories/ssd.jpg", | |||||
"summary": "高速、可靠的固态硬盘,提供卓越的性能和存储容量", | |||||
"capacities": [ | |||||
"256GB", | |||||
"512GB", | |||||
"1TB", | |||||
"2TB", | |||||
"4TB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "4", | |||||
"title": "内存条", | |||||
"description": "适用于电脑、服务器等设备的内存条", | |||||
"image": "/images/categories/ram.jpg", | |||||
"summary": "高性能内存条,提供快速的数据访问和处理能力", | |||||
"capacities": [ | |||||
"8GB", | |||||
"16GB", | |||||
"32GB", | |||||
"64GB" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "5", | |||||
"title": "读卡器", | |||||
"description": "用于读取各种存储卡的设备", | |||||
"image": "/images/categories/card-reader.jpg", | |||||
"summary": "支持多种存储卡格式的读卡器,提供快速的数据传输", | |||||
"capacities": [ | |||||
"USB 3.0", | |||||
"USB 3.1", | |||||
"USB-C" | |||||
] | |||||
}, | |||||
{ | |||||
"id": "6", | |||||
"title": "外接硬盘盒", | |||||
"description": "用于将内部硬盘转换为外接存储设备的设备", | |||||
"image": "/images/categories/hdd-enclosure.jpg", | |||||
"summary": "支持多种接口的外接硬盘盒,提供便捷的存储解决方案", | |||||
"capacities": [ | |||||
"USB 3.0", | |||||
"USB 3.1", | |||||
"USB-C", | |||||
"Thunderbolt" | |||||
] | |||||
} | |||||
] |
[ | |||||
{ | |||||
"id": 1, | |||||
"title": "Photography Storage" | |||||
}, | |||||
{ | |||||
"id": 2, | |||||
"title": "Mobile Device Storage" | |||||
}, | |||||
{ | |||||
"id": 3, | |||||
"title": "Gaming Storage" | |||||
}, | |||||
{ | |||||
"id": 4, | |||||
"title": "System Boot Drive" | |||||
}, | |||||
{ | |||||
"id": 5, | |||||
"title": "Mobile Data Transfer/Backup" | |||||
}, | |||||
{ | |||||
"id": 6, | |||||
"title": "Media/Entertainment" | |||||
}, | |||||
{ | |||||
"id": 7, | |||||
"title": "Content Creation/Video Editing" | |||||
}, | |||||
{ | |||||
"id": 8, | |||||
"title": "Memory Upgrade/Performance Enhancement" | |||||
} | |||||
] |
[ | |||||
{ | |||||
"id": 1, | |||||
"title": "写真・ビデオ撮影用ストレージ" | |||||
}, | |||||
{ | |||||
"id": 2, | |||||
"title": "スマートフォン/タブレット用ストレージ拡張" | |||||
}, | |||||
{ | |||||
"id": 3, | |||||
"title": "ゲーム用ストレージ/パフォーマンス向上" | |||||
}, | |||||
{ | |||||
"id": 4, | |||||
"title": "オフィス/学習用ストレージ拡張" | |||||
}, | |||||
{ | |||||
"id": 5, | |||||
"title": "モバイルデータ転送/バックアップ" | |||||
}, | |||||
{ | |||||
"id": 6, | |||||
"title": "メディア/エンターテイメント" | |||||
}, | |||||
{ | |||||
"id": 7, | |||||
"title": "コンテンツ制作/動画編集" | |||||
}, | |||||
{ | |||||
"id": 8, | |||||
"title": "メモリアップグレード/パフォーマンス向上" | |||||
} | |||||
] |
[ | |||||
{ | |||||
"id": 1, | |||||
"title": "摄影存储" | |||||
}, | |||||
{ | |||||
"id": 2, | |||||
"title": "手机/平板扩容" | |||||
}, | |||||
{ | |||||
"id": 3, | |||||
"title": "游戏加速/存储" | |||||
}, | |||||
{ | |||||
"id": 4, | |||||
"title": "办公/学习扩容" | |||||
}, | |||||
{ | |||||
"id": 5, | |||||
"title": "移动数据传输/备份" | |||||
}, | |||||
{ | |||||
"id": 6, | |||||
"title": "媒体/影音娱乐" | |||||
}, | |||||
{ | |||||
"id": 7, | |||||
"title": "内容创作/视频编辑" | |||||
}, | |||||
{ | |||||
"id": 8, | |||||
"title": "内存升级/性能提升" | |||||
} | |||||
] |
import fs from 'fs'; | |||||
import path from 'path'; | |||||
import { fileURLToPath } from 'url'; | |||||
import { parse } from 'yaml'; | |||||
const __filename = fileURLToPath(import.meta.url); | |||||
const __dirname = path.dirname(__filename); | |||||
/** | |||||
* 读取Markdown文件的frontmatter部分 | |||||
* @param {string} filePath - Markdown文件路径 | |||||
* @returns {Object} - frontmatter数据对象 | |||||
*/ | |||||
function readMarkdownFrontmatter(filePath) { | |||||
try { | |||||
const content = fs.readFileSync(filePath, 'utf-8'); | |||||
const frontmatterMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/m); | |||||
if (frontmatterMatch && frontmatterMatch[1]) { | |||||
try { | |||||
const yamlContent = frontmatterMatch[1].trim(); | |||||
console.log(`解析文件: ${filePath}`); | |||||
console.log(`YAML内容:\n${yamlContent}`); | |||||
const metadata = parse(yamlContent); | |||||
console.log(`解析结果:`, metadata); | |||||
// 确保所有字段都有默认值 | |||||
return { | |||||
title: metadata.title || '', | |||||
name: metadata.name || metadata.title || '', | |||||
description: metadata.description || '', | |||||
summary: metadata.summary || '', | |||||
usage: Array.isArray(metadata.usage) ? metadata.usage : [], | |||||
categoryId: metadata.categoryId || '', | |||||
category: metadata.category || '', | |||||
image: metadata.image || '', | |||||
series: Array.isArray(metadata.series) ? metadata.series : [], | |||||
gallery: Array.isArray(metadata.gallery) ? metadata.gallery : [], | |||||
capacities: Array.isArray(metadata.capacities) ? metadata.capacities : [], | |||||
products: Array.isArray(metadata.products) ? metadata.products : [], | |||||
id: metadata.id || '' | |||||
}; | |||||
} catch (err) { | |||||
console.error(`解析frontmatter失败: ${filePath}`, err); | |||||
return {}; | |||||
} | |||||
} | |||||
return {}; | |||||
} catch (err) { | |||||
console.error(`读取文件失败: ${filePath}`, err); | |||||
return {}; | |||||
} | |||||
} | |||||
/** | |||||
* 获取所有产品ID和详细信息的函数 | |||||
* @param {string} locale - 语言代码 | |||||
* @returns {Object[]} - 产品信息数组 | |||||
*/ | |||||
function getProducts(locale) { | |||||
try { | |||||
const productsDir = path.resolve(__dirname, '../content/products', locale); | |||||
if (!fs.existsSync(productsDir)) { | |||||
console.log(`找不到产品目录: ${productsDir}`); | |||||
return []; | |||||
} | |||||
// 读取所有.md文件 | |||||
const productFiles = fs.readdirSync(productsDir) | |||||
.filter(file => file.endsWith('.md')); | |||||
console.log(`找到 ${locale} 语言的产品文件: ${productFiles.length}个`); | |||||
// 读取每个产品文件的frontmatter | |||||
return productFiles.map(file => { | |||||
const filePath = path.join(productsDir, file); | |||||
const metadata = readMarkdownFrontmatter(filePath); | |||||
console.log(`metadata: ${JSON.stringify(metadata)}`); | |||||
const id = path.basename(file, '.md'); | |||||
// 使用原始图片路径,如果没有则使用默认路径 | |||||
const imagePath = metadata.image; | |||||
return { | |||||
id, | |||||
title: metadata.title || id, | |||||
name: metadata.name || metadata.title || id, | |||||
usage: metadata.usage || [], | |||||
category: metadata.categoryId || '', | |||||
image: imagePath, | |||||
description: metadata.description || '', | |||||
summary: metadata.summary || '', | |||||
series: metadata.series || [], | |||||
gallery: metadata.gallery || [], | |||||
capacities: metadata.capacities || [] | |||||
}; | |||||
}); | |||||
} catch (err) { | |||||
console.error(`获取产品信息时出错 (${locale}):`, err); | |||||
return []; | |||||
} | |||||
} | |||||
/** | |||||
* 获取所有用途类别 | |||||
* @param {string} locale - 语言代码 | |||||
* @returns {Object[]} - 用途类别数组 | |||||
*/ | |||||
function getUsages(locale) { | |||||
try { | |||||
const usagesDir = path.resolve(__dirname, '../content/usages', locale); | |||||
if (!fs.existsSync(usagesDir)) { | |||||
console.log(`找不到用途目录: ${usagesDir}`); | |||||
return []; | |||||
} | |||||
// 读取所有.md文件 | |||||
const usageFiles = fs.readdirSync(usagesDir) | |||||
.filter(file => file.endsWith('.md')); | |||||
// 读取每个用途文件的frontmatter | |||||
return usageFiles.map(file => { | |||||
const filePath = path.join(usagesDir, file); | |||||
const content = fs.readFileSync(filePath, 'utf-8'); | |||||
const frontmatterMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/m); | |||||
if (frontmatterMatch && frontmatterMatch[1]) { | |||||
try { | |||||
const yamlContent = frontmatterMatch[1].trim(); | |||||
const metadata = parse(yamlContent); | |||||
const id = path.basename(file, '.md'); | |||||
return { | |||||
id: metadata.id || id, | |||||
title: metadata.title || id | |||||
}; | |||||
} catch (err) { | |||||
console.error(`解析frontmatter失败: ${filePath}`, err); | |||||
return {}; | |||||
} | |||||
} | |||||
return { | |||||
id: path.basename(file, '.md'), | |||||
title: path.basename(file, '.md'), | |||||
}; | |||||
}); | |||||
} catch (err) { | |||||
console.error(`获取用途信息时出错 (${locale}):`, err); | |||||
return []; | |||||
} | |||||
} | |||||
/** | |||||
* 获取所有分类 | |||||
* @param {string} locale - 语言代码 | |||||
* @returns {Object[]} - 分类数组 | |||||
*/ | |||||
function getCategories(locale) { | |||||
try { | |||||
const categoriesDir = path.resolve(__dirname, '../content/categories', locale); | |||||
if (!fs.existsSync(categoriesDir)) { | |||||
console.log(`找不到分类目录: ${categoriesDir}`); | |||||
return []; | |||||
} | |||||
// 读取所有.md文件 | |||||
const categoryFiles = fs.readdirSync(categoriesDir) | |||||
.filter(file => file.endsWith('.md')); | |||||
console.log(`找到 ${locale} 语言的分类文件: ${categoryFiles.length}个`); | |||||
// 读取每个分类文件的frontmatter | |||||
return categoryFiles.map(file => { | |||||
const filePath = path.join(categoriesDir, file); | |||||
const metadata = readMarkdownFrontmatter(filePath); | |||||
const id = path.basename(file, '.md'); | |||||
return { | |||||
id, | |||||
title: metadata.title, | |||||
description: metadata.description, | |||||
image: metadata.image, | |||||
summary: metadata.summary, | |||||
capacities: metadata.capacities, | |||||
sort: metadata.sort, | |||||
}; | |||||
}); | |||||
} catch (err) { | |||||
console.error(`获取分类信息时出错 (${locale}):`, err); | |||||
return []; | |||||
} | |||||
} | |||||
// 定义基础路由和支持的语言 | |||||
const baseRoutes = [ | |||||
'/', | |||||
'/products', | |||||
'/faq', | |||||
'/contact', | |||||
'/about', | |||||
]; | |||||
const locales = ['ja', 'en', 'zh']; | |||||
// 生成所有路由 | |||||
function generateAllRoutes() { | |||||
let routes = [...baseRoutes]; | |||||
// 添加语言前缀路由 zh 是默认语言 | |||||
locales.forEach(locale => { | |||||
if (locale !== 'zh') { | |||||
routes.push(`/${locale}`); | |||||
baseRoutes.forEach(route => { | |||||
if (route !== '/') { | |||||
routes.push(`/${locale}${route}`); | |||||
} | |||||
}); | |||||
} | |||||
}); | |||||
// 为每个语言添加产品详情页路由 | |||||
locales.forEach(locale => { | |||||
const products = getProducts(locale); | |||||
const prefixPath = locale === 'zh' ? '' : `/${locale}`; | |||||
products.forEach(product => { | |||||
routes.push(`${prefixPath}/products/${product.id}`); | |||||
}); | |||||
// 添加按用途筛选的产品列表页 | |||||
const usages = getUsages(locale); | |||||
usages.forEach(usage => { | |||||
routes.push(`${prefixPath}/products?usage=${encodeURIComponent(usage.title)}`); | |||||
}); | |||||
// 添加按分类筛选的产品列表页 | |||||
const categories = getCategories(locale); | |||||
categories.forEach(category => { | |||||
routes.push(`${prefixPath}/products?category=${category.id}`); | |||||
}); | |||||
}); | |||||
// 移除重复项 | |||||
routes = [...new Set(routes)]; | |||||
console.log(`总共生成了 ${routes.length} 条路由`); | |||||
return routes; | |||||
} | |||||
// 生成路由并保存到文件 | |||||
const routes = generateAllRoutes(); | |||||
fs.writeFileSync(path.resolve(__dirname, '../prerenderRoutes.json'), JSON.stringify(routes, null, 2)); | |||||
console.log('路由生成完成,已保存到 prerenderRoutes.json'); | |||||
// 输出数据文件,供前端使用 | |||||
function generateDataFiles() { | |||||
const dataDir = path.resolve(__dirname, '../public/data'); | |||||
// 确保目录存在 | |||||
if (!fs.existsSync(dataDir)) { | |||||
fs.mkdirSync(dataDir, { recursive: true }); | |||||
} | |||||
// 为每种语言生成产品数据 | |||||
locales.forEach(locale => { | |||||
const products = getProducts(locale); | |||||
const usages = getUsages(locale); | |||||
const categories = getCategories(locale); | |||||
// 保存产品数据 | |||||
fs.writeFileSync( | |||||
path.resolve(dataDir, `products-${locale}.json`), | |||||
JSON.stringify(products, null, 2) | |||||
); | |||||
// 保存用途数据 | |||||
fs.writeFileSync( | |||||
path.resolve(dataDir, `usages-${locale}.json`), | |||||
JSON.stringify(usages, null, 2) | |||||
); | |||||
// 保存分类数据 | |||||
fs.writeFileSync( | |||||
path.resolve(dataDir, `categories-${locale}.json`), | |||||
JSON.stringify(categories, null, 2) | |||||
); | |||||
}); | |||||
console.log('数据文件生成完成,已保存到 public/data 目录'); | |||||
} | |||||
// 生成数据文件 | |||||
generateDataFiles(); |