- 在nuxt.config.ts中引入预渲染路由配置,支持动态加载路由。 - 新增prerenderRoutes.json文件,存储预渲染的路由列表,确保静态站点生成的完整性。 - 更新package.json,添加prepare-routes脚本以生成路由数据。 - 新增generateRoutes.js脚本,自动生成产品、用途和分类数据文件,提升内容管理的效率。 - 更新多个页面和组件,优化多语言支持和用户体验。master
@@ -79,6 +79,7 @@ async function selectLanguage(langCode: string) { | |||
try { | |||
// 使用类型断言,确保 langCode 是有效的 LocaleCode | |||
await setLocale(langCode as LocaleCode); | |||
window.location.reload(); | |||
} catch (error) { | |||
console.error("Failed to set locale:", error); | |||
// 这里可以添加用户反馈,例如显示一个错误提示 |
@@ -24,7 +24,7 @@ | |||
<h3 | |||
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> | |||
<ul class="space-y-4"> | |||
<li v-for="item in menuProductsItems" :key="item.path"> | |||
@@ -32,7 +32,7 @@ | |||
:to="item.path" | |||
class="text-zinc-500 text-sm font-normal hover:text-white transition" | |||
> | |||
{{ $t(item.label) }} | |||
{{ t(item.label) }} | |||
</NuxtLink> | |||
</li> | |||
</ul> | |||
@@ -42,7 +42,7 @@ | |||
<h3 | |||
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> | |||
<ul class="space-y-4"> | |||
<li v-for="item in menuWebsiteItems" :key="item.path"> | |||
@@ -50,7 +50,7 @@ | |||
:to="item.path" | |||
class="text-zinc-500 text-sm font-normal hover:text-white transition" | |||
> | |||
{{ $t(item.label) }} | |||
{{ t(item.label) }} | |||
</NuxtLink> | |||
</li> | |||
</ul> | |||
@@ -60,7 +60,7 @@ | |||
<h3 | |||
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> | |||
<ul class="space-y-4"> | |||
<li v-for="item in menuHomeItems" :key="item.path"> | |||
@@ -68,7 +68,7 @@ | |||
:to="item.path" | |||
class="text-zinc-500 text-sm font-normal hover:text-white transition" | |||
> | |||
{{ $t(item.label) }} | |||
{{ t(item.label) }} | |||
</NuxtLink> | |||
</li> | |||
</ul> | |||
@@ -84,28 +84,63 @@ | |||
* 包含网站导航、联系信息和版权信息 | |||
*/ | |||
import { useI18n } from "vue-i18n"; | |||
const { locale } = useI18n(); | |||
const { locale, t } = useI18n(); | |||
const config = useRuntimeConfig(); | |||
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(() => { | |||
if (!categoryResponse.value?.data) return []; | |||
return categoryResponse.value.data; | |||
return categoryResponse.value || []; | |||
}); | |||
// 导航菜单项 | |||
const menuProductsItems = computed(() => { | |||
// 构建路径前缀 | |||
const prefix = locale.value === defaultLocale ? "" : `/${locale.value}`; | |||
// 使用API获取的产品分类数据 | |||
// 使用修改后的产品分类数据 | |||
return productCategories.value.map((category: any) => ({ | |||
label: category.title, | |||
path: `${prefix}/products?category=${category.id}` | |||
path: `${prefix}/products?category=${encodeURIComponent(category.title)}`, | |||
})); | |||
}); | |||
@@ -140,15 +175,17 @@ const menuHomeItems = computed(() => [ | |||
// 这里可以根据需要添加首页快捷链接 | |||
{ | |||
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", | |||
path: locale.value === defaultLocale ? "/privacy" : `/${locale.value}/privacy`, | |||
path: | |||
locale.value === defaultLocale ? "/privacy" : `/${locale.value}/privacy`, | |||
}, | |||
{ | |||
label: "common.footer.quickLinks.terms", | |||
path: locale.value === defaultLocale ? "/terms" : `/${locale.value}/terms`, | |||
} | |||
}, | |||
]); | |||
</script> |
@@ -18,12 +18,12 @@ | |||
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" | |||
:class="[ | |||
$route.path === item.path | |||
route.path === item.path | |||
? 'font-bold opacity-100 bg-white/15' | |||
: '', | |||
]" | |||
> | |||
{{ $t(item.label) }} | |||
{{ t(item.label) }} | |||
</nuxt-link> | |||
<!-- Dropdown Container --> | |||
@@ -37,12 +37,12 @@ | |||
@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="[ | |||
$route.path.startsWith(item.pathPrefix) | |||
route.path.startsWith(item.pathPrefix) | |||
? 'font-bold opacity-100 bg-white/15' | |||
: '', | |||
]" | |||
> | |||
<span>{{ $t(item.label) }}</span> | |||
<span>{{ t(item.label) }}</span> | |||
</div> | |||
<!-- Dropdown Panel --> | |||
@@ -58,20 +58,20 @@ | |||
class="bg-slate-900 rounded-none p-6 flex flex-col gap-1" | |||
> | |||
<h3 class="text-base font-medium text-white mb-2"> | |||
{{ $t(section.title) }} | |||
{{ t(section.title) }} | |||
</h3> | |||
<ul class="flex flex-col gap-1"> | |||
<li v-for="link in section.items" :key="link.path"> | |||
<nuxt-link | |||
:to="link.path" | |||
@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="{ | |||
'text-white font-bold bg-white/15': | |||
$route.path === link.path, | |||
route.path === link.path, | |||
}" | |||
> | |||
{{ $t(link.label) }} | |||
{{ t(link.label) }} | |||
</nuxt-link> | |||
</li> | |||
</ul> | |||
@@ -98,7 +98,7 @@ | |||
<span | |||
class="hidden lg:inline-block ml-1 text-white text-sm opacity-80" | |||
> | |||
{{ $t("common.search") }} | |||
{{ t("common.search") }} | |||
</span> | |||
<!-- Input overlay could go here if implementing search --> | |||
</div> | |||
@@ -169,12 +169,12 @@ | |||
@click="closeMobileMenu" | |||
class="block px-3 py-2 rounded-md text-base font-medium" | |||
:class="[ | |||
$route.path === item.path | |||
route.path === item.path | |||
? 'bg-gray-900 text-white' | |||
: 'text-gray-300 hover:bg-gray-700 hover:text-white', | |||
]" | |||
> | |||
{{ $t(item.label) }} | |||
{{ t(item.label) }} | |||
</nuxt-link> | |||
<!-- Mobile Dropdown Section --> | |||
@@ -182,7 +182,7 @@ | |||
<h3 | |||
class="px-3 pt-2 pb-1 text-sm font-semibold text-gray-400 uppercase tracking-wider" | |||
> | |||
{{ $t(item.label) }} | |||
{{ t(item.label) }} | |||
</h3> | |||
<div | |||
v-for="section in item.children" | |||
@@ -190,7 +190,7 @@ | |||
class="mt-1" | |||
> | |||
<!-- 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 | |||
v-for="link in section.items" | |||
:key="link.path" | |||
@@ -198,10 +198,10 @@ | |||
@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="{ | |||
'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> | |||
</div> | |||
</div> | |||
@@ -239,7 +239,7 @@ | |||
/> | |||
</svg> | |||
</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 --> | |||
<div class="relative mb-6"> | |||
@@ -250,7 +250,7 @@ | |||
ref="searchInputRef" | |||
type="text" | |||
: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" | |||
/> | |||
@@ -268,7 +268,7 @@ | |||
<!-- Hot Keywords Section --> | |||
<div class="mt-6"> | |||
<h3 class="text-gray-400 text-sm mb-3"> | |||
{{ $t("common.hotKeywords") || "热门搜索" }} | |||
{{ t("common.hotKeywords") || "热门搜索" }} | |||
</h3> | |||
<div class="flex flex-wrap gap-3"> | |||
<button | |||
@@ -296,7 +296,6 @@ import { useI18n } from "vue-i18n"; | |||
*/ | |||
const { t, locale } = useI18n(); | |||
const config = useRuntimeConfig(); | |||
// 从运行时配置获取默认语言,如果未配置则默认为 'en' | |||
const defaultLocale = config.public.i18n?.defaultLocale || "en"; | |||
const mobileMenuOpen = ref(false); | |||
const isSearchOpen = ref(false); | |||
@@ -304,31 +303,97 @@ const searchInputRef = ref<HTMLInputElement | null>(null); | |||
const openDropdown = ref<string | null>(null); | |||
let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mouseleave delay | |||
const route = useRoute(); | |||
// 添加热门关键字 | |||
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(() => { | |||
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,根据是否为默认语言调整路径 | |||
@@ -356,15 +421,15 @@ const menuItems = computed(() => { | |||
title: "common.productCategories", | |||
items: productCategories.value.map((category: any) => ({ | |||
...category, | |||
path: `${prefix}${category.path}` | |||
})) | |||
path: `${prefix}${category.path}`, | |||
})), | |||
}, | |||
{ | |||
title: "common.byUsage", | |||
items: productUsages.value.map((usage: any) => ({ | |||
...usage, | |||
path: `${prefix}${usage.path}` | |||
})) | |||
path: `${prefix}${usage.path}`, | |||
})), | |||
}, | |||
], | |||
}, |
@@ -5,4 +5,18 @@ id: 3 | |||
category: 游戏加速/存储 | |||
products: ["M.2 SSD", "2.5英寸 SSD", "内存条", "microSD存储卡"] | |||
summary: 游戏性能优化和存储解决方案 | |||
--- | |||
--- | |||
# 游戏加速/存储解决方案 | |||
## 电脑游戏性能提升 | |||
现代大型PC游戏动辄上百GB,高品质的M.2 SSD和2.5英寸SSD可显著减少游戏载入时间,提升游戏体验。高性能内存条也是游戏流畅运行的关键组件。 | |||
## 游戏主机存储扩展 | |||
针对PS5、Xbox Series X/S、Nintendo Switch等游戏主机,我们提供兼容的存储扩展方案,让您存储更多游戏而无需频繁删除。 | |||
## 产品优势 | |||
- **极速读写**:NVMe技术带来接近内存的访问速度 | |||
- **低延迟**:减少游戏中的卡顿和画面撕裂 | |||
- **大容量**:轻松应对现代游戏的海量存储需求 | |||
- **散热设计**:长时间游戏也能保持稳定性能 |
@@ -77,6 +77,7 @@ export default { | |||
strong_point_title: "Our Strengths / Why Choose Us", | |||
view_details: "View Details", | |||
product_count: "Products", | |||
loadError: "Failed to load product. Please try again later.", | |||
}, | |||
faq: { | |||
title: "Frequently Asked Questions", |
@@ -72,6 +72,7 @@ export default { | |||
strong_point_title: "当社の強み/選ばれる理由", | |||
view_details: "詳細を見る", | |||
product_count: "製品", | |||
loadError: "製品の読み込みに失敗しました。後でもう一度お試しください。", | |||
}, | |||
faq: { | |||
title: "よくある質問", |
@@ -72,6 +72,7 @@ export default { | |||
strong_point_title: "我们的优势/选择我们的理由", | |||
view_details: "查看详情", | |||
product_count: "款产品", | |||
loadError: "加载产品失败,请稍后重试", | |||
}, | |||
faq: { | |||
title: "常见问题", |
@@ -1,10 +1,40 @@ | |||
import i18nConfig from "./i18n.config"; | |||
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 | |||
export default defineNuxtConfig({ | |||
compatibilityDate: "2025-05-06", | |||
devtools: { enabled: true }, | |||
// 添加CSS | |||
@@ -19,28 +49,25 @@ export default defineNuxtConfig({ | |||
// content模块配置 | |||
content: { | |||
// 配置导航 | |||
navigation: { | |||
fields: ['title', 'description', 'category'] | |||
}, | |||
// 配置 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: { | |||
...i18nConfig, | |||
defaultLocale: i18nConfig.defaultLocale as 'zh' | 'en' | 'ja' | undefined, | |||
defaultLocale: i18nConfig.defaultLocale as "zh" | "en" | "ja" | undefined, | |||
strategy: i18nConfig.strategy as Strategies, // 显式类型转换 strategy | |||
}, | |||
@@ -66,33 +93,29 @@ export default defineNuxtConfig({ | |||
// 静态站点生成配置 | |||
nitro: { | |||
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: [ | |||
{ | |||
dir: 'public', | |||
baseURL: '/' | |||
} | |||
] | |||
dir: "public", | |||
baseURL: "/", | |||
}, | |||
], | |||
}, | |||
// 禁用 payload 提取,优化静态生成 | |||
experimental: { | |||
payloadExtraction: false, | |||
renderJsonPayloads: false, | |||
}, | |||
devServer: { | |||
host: "0.0.0.0", | |||
} | |||
}); | |||
}, | |||
compatibilityDate: "2025-05-07", | |||
}); |
@@ -16,7 +16,8 @@ | |||
"swiper": "^11.2.6", | |||
"vue": "^3.5.13", | |||
"vue-i18n": "^11.1.3", | |||
"vue-router": "^4.5.0" | |||
"vue-router": "^4.5.0", | |||
"yaml": "^2.7.1" | |||
}, | |||
"devDependencies": { | |||
"@tailwindcss/typography": "^0.5.10", |
@@ -4,8 +4,9 @@ | |||
"type": "module", | |||
"scripts": { | |||
"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", | |||
"postinstall": "nuxt prepare" | |||
}, | |||
@@ -19,7 +20,8 @@ | |||
"swiper": "^11.2.6", | |||
"vue": "^3.5.13", | |||
"vue-i18n": "^11.1.3", | |||
"vue-router": "^4.5.0" | |||
"vue-router": "^4.5.0", | |||
"yaml": "^2.7.1" | |||
}, | |||
"devDependencies": { | |||
"@tailwindcss/typography": "^0.5.10", |
@@ -16,11 +16,11 @@ | |||
<nuxt-link | |||
to="/" | |||
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 text-base font-normal">{{ | |||
$t("about.overview.title") | |||
t("about.overview.title") | |||
}}</span> | |||
</div> | |||
</div> | |||
@@ -30,12 +30,12 @@ | |||
<h1 | |||
class="text-white text-5xl font-bold mb-4 tracking-tight text-center" | |||
> | |||
{{ $t("about.companyInfo.name") }} | |||
{{ t("about.companyInfo.name") }} | |||
</h1> | |||
<div | |||
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> | |||
@@ -50,7 +50,7 @@ | |||
<h2 | |||
class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | |||
> | |||
{{ $t("about.overview.companyInfo") }} | |||
{{ t("about.overview.companyInfo") }} | |||
<span | |||
class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full" | |||
></span> | |||
@@ -58,50 +58,50 @@ | |||
<div class="flex flex-col gap-3"> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.companyName") | |||
t("about.overview.companyName") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.name") | |||
t("about.companyInfo.name") | |||
}}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.englishName") | |||
t("about.overview.englishName") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.englishName") | |||
t("about.companyInfo.englishName") | |||
}}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.established") | |||
t("about.overview.established") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.established") | |||
t("about.companyInfo.established") | |||
}}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.ceo") | |||
t("about.overview.ceo") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.ceo") | |||
t("about.companyInfo.ceo") | |||
}}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.employees") | |||
t("about.overview.employees") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.employees") | |||
t("about.companyInfo.employees") | |||
}}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.location") | |||
t("about.overview.location") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.location") | |||
t("about.companyInfo.location") | |||
}}</span> | |||
</div> | |||
</div> | |||
@@ -113,13 +113,13 @@ | |||
<h2 | |||
class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | |||
> | |||
{{ $t("about.overview.philosophy") }} | |||
{{ t("about.overview.philosophy") }} | |||
<span | |||
class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full" | |||
></span> | |||
</h2> | |||
<div class="text-stone-400 text-lg leading-relaxed"> | |||
{{ $t("about.companyInfo.philosophy") }} | |||
{{ t("about.companyInfo.philosophy") }} | |||
</div> | |||
</div> | |||
<!-- 联系方式卡片 --> | |||
@@ -129,7 +129,7 @@ | |||
<h2 | |||
class="text-white text-2xl font-semibold mb-2 tracking-tight relative" | |||
> | |||
{{ $t("about.overview.contact") }} | |||
{{ t("about.overview.contact") }} | |||
<span | |||
class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full" | |||
></span> | |||
@@ -137,7 +137,7 @@ | |||
<div class="flex flex-col gap-3"> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.email") | |||
t("about.overview.email") | |||
}}</span> | |||
<a | |||
:href="'mailto:' + companyInfo.email" | |||
@@ -147,7 +147,7 @@ | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.tel") | |||
t("about.overview.tel") | |||
}}</span> | |||
<a | |||
:href="'tel:' + companyInfo.tel.replace(/[^0-9]/g, '')" | |||
@@ -157,7 +157,7 @@ | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.fax") | |||
t("about.overview.fax") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
companyInfo.fax | |||
@@ -165,20 +165,20 @@ | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.businessHours") | |||
t("about.overview.businessHours") | |||
}}</span> | |||
<span class="text-white text-lg font-bold">{{ | |||
$t("about.companyInfo.businessHours") | |||
t("about.companyInfo.businessHours") | |||
}}</span> | |||
</div> | |||
</div> | |||
<div class="flex flex-col gap-1 mt-2"> | |||
<span class="text-stone-400 text-base">{{ | |||
$t("about.overview.businessActivities") | |||
t("about.overview.businessActivities") | |||
}}</span> | |||
<div class="text-white text-base font-bold space-y-1"> | |||
<p> | |||
{{ $t("about.companyInfo.businessActivities") }} | |||
{{ t("about.companyInfo.businessActivities") }} | |||
</p> | |||
</div> | |||
</div> |
@@ -14,11 +14,11 @@ | |||
<nuxt-link | |||
to="/" | |||
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> | |||
<nuxt-link to="/contact" class="text-white text-base font-normal">{{ | |||
$t("contact.title") | |||
t("contact.title") | |||
}}</nuxt-link> | |||
</div> | |||
</div> | |||
@@ -32,7 +32,7 @@ | |||
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"> | |||
{{ $t("contact.title") }} | |||
{{ t("contact.title") }} | |||
</div> | |||
<form | |||
@submit.prevent="handleSubmit" | |||
@@ -44,14 +44,14 @@ | |||
type="text" | |||
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" | |||
:placeholder="$t('contact.name')" | |||
:placeholder="t('contact.name')" | |||
required | |||
/> | |||
<label | |||
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" | |||
> | |||
{{ $t("contact.name") }} | |||
{{ t("contact.name") }} | |||
</label> | |||
</div> | |||
@@ -61,14 +61,14 @@ | |||
type="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" | |||
:placeholder="$t('contact.email')" | |||
:placeholder="t('contact.email')" | |||
required | |||
/> | |||
<label | |||
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" | |||
> | |||
{{ $t("contact.email") }} | |||
{{ t("contact.email") }} | |||
</label> | |||
</div> | |||
@@ -77,14 +77,14 @@ | |||
v-model="form.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" | |||
:placeholder="$t('contact.message')" | |||
:placeholder="t('contact.message')" | |||
required | |||
></textarea> | |||
<label | |||
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" | |||
> | |||
{{ $t("contact.message") }} | |||
{{ t("contact.message") }} | |||
</label> | |||
</div> | |||
@@ -102,7 +102,7 @@ | |||
? 'border-red-500 focus:border-red-500' | |||
: 'border-gray-600 focus:border-blue-500', | |||
]" | |||
:placeholder="$t('contact.captcha')" | |||
:placeholder="t('contact.captcha')" | |||
required | |||
autocomplete="off" | |||
aria-describedby="captcha-error" | |||
@@ -117,22 +117,22 @@ | |||
: 'text-gray-400 peer-focus:text-blue-400', | |||
]" | |||
> | |||
{{ $t("contact.captcha") }} | |||
{{ t("contact.captcha") }} | |||
</label> | |||
</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" | |||
v-html="captcha.captchaSvg.value" | |||
@click="captcha.generateCaptcha()" | |||
:title="$t('contact.refreshCaptcha')" | |||
:title="t('contact.refreshCaptcha')" | |||
style="line-height: 0" | |||
></div> | |||
<button | |||
type="button" | |||
@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" | |||
:aria-label="$t('contact.refreshCaptcha')" | |||
:title="$t('contact.refreshCaptcha')" | |||
:aria-label="t('contact.refreshCaptcha')" | |||
:title="t('contact.refreshCaptcha')" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
@@ -166,8 +166,8 @@ | |||
> | |||
{{ | |||
isSubmitting | |||
? $t("contact.submitting") | |||
: $t("contact.submit") | |||
? t("contact.submitting") | |||
: t("contact.submit") | |||
}} | |||
</button> | |||
</form> | |||
@@ -179,28 +179,28 @@ | |||
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"> | |||
{{ $t("about.companyInfo.name") }} | |||
{{ t("about.companyInfo.name") }} | |||
</div> | |||
<div class="flex flex-col gap-4"> | |||
<div class="flex items-center gap-4"> | |||
<div class="text-white/60 text-base font-normal"> | |||
{{ $t("about.overview.companyName") }} | |||
{{ t("about.overview.companyName") }} | |||
</div> | |||
<div class="text-white text-base font-normal"> | |||
{{ $t("about.companyInfo.companyName") }} | |||
{{ t("about.companyInfo.companyName") }} | |||
</div> | |||
</div> | |||
<div class="flex items-center gap-4"> | |||
<div class="text-white/60 text-base font-normal"> | |||
{{ $t("about.overview.location") }} | |||
{{ t("about.overview.location") }} | |||
</div> | |||
<div class="text-white text-base font-normal"> | |||
{{ $t("about.companyInfo.location") }} | |||
{{ t("about.companyInfo.location") }} | |||
</div> | |||
</div> | |||
<div class="flex items-center gap-4"> | |||
<div class="text-white/60 text-base font-normal"> | |||
{{ $t("about.overview.tel") }} | |||
{{ t("about.overview.tel") }} | |||
</div> | |||
<div class="text-white text-base font-normal"> | |||
86)024-8399-0696 | |||
@@ -208,7 +208,7 @@ | |||
</div> | |||
<div class="flex items-center gap-4"> | |||
<div class="text-white/60 text-base font-normal"> | |||
{{ $t("about.overview.email") }} | |||
{{ t("about.overview.email") }} | |||
</div> | |||
<div class="text-white text-base font-normal"> | |||
hanye@hanye.cn | |||
@@ -221,10 +221,10 @@ | |||
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"> | |||
{{ $t("about.overview.businessHours") }} | |||
{{ t("about.overview.businessHours") }} | |||
</div> | |||
<div class="text-white text-base font-normal"> | |||
{{ $t("about.companyInfo.businessHours") }} | |||
{{ t("about.companyInfo.businessHours") }} | |||
</div> | |||
</div> | |||
</div> |
@@ -156,7 +156,7 @@ const { t, locale } = useI18n(); | |||
// FAQ数据 | |||
interface FAQ { | |||
id: number; | |||
id: string; | |||
category: string; | |||
question: string; | |||
answer: string; | |||
@@ -177,27 +177,38 @@ const { data: faqData } = await useAsyncData('faqs', async () => { | |||
console.log('Loading FAQ data for locale:', locale.value); | |||
try { | |||
// 使用 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 || '', | |||
question: item.title || '', | |||
answer: item.body?.value || '', | |||
answer: item.body || '', | |||
title: item.title || '', | |||
description: item.description || '', | |||
sort: item.sort || 0 | |||
})); | |||
}; | |||
}); | |||
console.log('Processed FAQ items:', faqItems); | |||
// 按 sort 字段排序 | |||
return faqs.sort((a, b) => a.sort - b.sort); | |||
return faqItems.sort((a, b) => a.sort - b.sort); | |||
} catch (error) { | |||
console.error('Error loading FAQ content:', error); | |||
return []; | |||
@@ -210,8 +221,6 @@ const { data: faqData } = await useAsyncData('faqs', async () => { | |||
watch: [locale] | |||
}); | |||
console.log('FAQ data:', faqData.value); | |||
// 处理FAQ数据变化 | |||
watchEffect(() => { | |||
if (faqData.value) { | |||
@@ -226,7 +235,9 @@ watchEffect(() => { | |||
// 从FAQ数据中提取所有不同的分类 | |||
const uniqueCategories = [ | |||
...new Set(faqData.value.map((faq: FAQ) => faq.category)), | |||
].sort(); // 对分类进行排序 | |||
].filter(category => category).sort(); // 过滤掉空分类并排序 | |||
console.log('Unique categories:', uniqueCategories); | |||
// 设置分类列表和默认选中的分类 | |||
categoriesList.value = [allOption, ...uniqueCategories]; |
@@ -44,7 +44,9 @@ | |||
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 | |||
class="rounded border border-white w-11 h-6 leading-none justify-center flex items-center text-white text-sm font-normal" | |||
> | |||
@@ -52,19 +54,19 @@ | |||
</div> | |||
<div class="justify-center"> | |||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | |||
$t("home.carousel.one.title") | |||
t("home.carousel.one.title") | |||
}}</span> | |||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | |||
$t("home.carousel.one.description") | |||
t("home.carousel.one.description") | |||
}}</span> | |||
<br /> | |||
<span class="text-white text-6xl font-normal leading-[78px]">{{ | |||
$t("home.carousel.one.description2") | |||
t("home.carousel.one.description2") | |||
}}</span | |||
><br /> | |||
<span | |||
class="text-cyan-400 text-6xl font-normal leading-[78px]" | |||
>{{ $t("home.carousel.one.description3") }}</span | |||
>{{ t("home.carousel.one.description3") }}</span | |||
> | |||
</div> | |||
<div class="flex flex-col gap-2 mt-4"> | |||
@@ -72,13 +74,13 @@ | |||
class="flex items-center gap-2 text-stone-50 text-xl font-normal" | |||
> | |||
<div class="w-2 h-2 bg-white rounded-full"></div> | |||
{{ $t("home.carousel.one.description4") }} | |||
{{ t("home.carousel.one.description4") }} | |||
</div> | |||
<div | |||
class="flex items-center gap-2 text-stone-50 text-xl font-normal" | |||
> | |||
<div class="w-2 h-2 bg-white rounded-full"></div> | |||
{{ $t("home.carousel.one.description5") }} | |||
{{ t("home.carousel.one.description5") }} | |||
</div> | |||
</div> | |||
<div | |||
@@ -88,7 +90,7 @@ | |||
to="/products/1" | |||
class="w-full h-full !flex items-center justify-center text-zinc-900" | |||
> | |||
{{ $t("products.view_details") }} | |||
{{ t("products.view_details") }} | |||
</nuxt-link> | |||
</div> | |||
</div> | |||
@@ -108,12 +110,12 @@ | |||
<div | |||
class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4" | |||
> | |||
{{ $t("products.usage") }} | |||
{{ t("products.usage") }} | |||
</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" | |||
> | |||
{{ $t("products.usage_title") }} | |||
{{ t("products.usage_title") }} | |||
</div> | |||
</div> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
@@ -131,9 +133,10 @@ | |||
@click="handleUsageClick(usage.id)" | |||
> | |||
<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 }} | |||
<!-- 用途名称 --> | |||
</div> | |||
<div | |||
class="absolute inset-0 rounded-full bg-cyan-400/20 scale-0 transition-transform duration-300 group-hover:scale-100" | |||
@@ -227,7 +230,7 @@ | |||
<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" | |||
> | |||
{{ $t("products.view_details") }} | |||
{{ t("products.view_details") }} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -290,14 +293,14 @@ | |||
class="flex flex-col gap-2 opacity-80 group-hover:opacity-100 transition-opacity duration-300" | |||
> | |||
<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" | |||
> | |||
<i | |||
class="icon-star text-sm group-hover:scale-110 transition-transform duration-300" | |||
></i> | |||
<span>{{ feature }}</span> | |||
<span>{{ capacitie }}</span> | |||
</div> | |||
</div> | |||
<div | |||
@@ -306,7 +309,7 @@ | |||
<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" | |||
> | |||
{{ category.description }} | |||
{{ category.title }} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -344,12 +347,12 @@ | |||
<div | |||
class="justify-start text-white font-medium text-1xl md:text-lg" | |||
> | |||
{{ $t("products.support") }} | |||
{{ t("products.support") }} | |||
</div> | |||
<div | |||
class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | |||
> | |||
{{ $t("products.support_description") }} | |||
{{ t("products.support_description") }} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -365,12 +368,12 @@ | |||
<div | |||
class="justify-start text-white font-medium text-1xl md:text-lg" | |||
> | |||
{{ $t("products.development") }} | |||
{{ t("products.development") }} | |||
</div> | |||
<div | |||
class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | |||
> | |||
{{ $t("products.development_description") }} | |||
{{ t("products.development_description") }} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -386,12 +389,12 @@ | |||
<div | |||
class="justify-start text-white font-medium text-1xl md:text-lg" | |||
> | |||
{{ $t("products.develop") }} | |||
{{ t("products.develop") }} | |||
</div> | |||
<div | |||
class="justify-start text-zinc-300 text-xs font-normal md:text-sm" | |||
> | |||
{{ $t("products.develop_description") }} | |||
{{ t("products.develop_description") }} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -404,12 +407,12 @@ | |||
<div | |||
class="justify-center text-cyan-400 text-base font-normal leading-tight mb-4" | |||
> | |||
{{ $t("products.strong_point") }} | |||
{{ t("products.strong_point") }} | |||
</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" | |||
> | |||
{{ $t("products.strong_point_title") }} | |||
{{ t("products.strong_point_title") }} | |||
</div> | |||
<div | |||
class="absolute right-0 top-1/2 -translate-y-1/2 z-10 lg:block hidden" | |||
@@ -484,14 +487,14 @@ | |||
<h1 | |||
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> | |||
<nuxt-link | |||
: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" | |||
> | |||
<span class="text-xs md:text-sm font-normal">{{ | |||
$t("products.consultation_button") | |||
t("products.consultation_button") | |||
}}</span> | |||
<i class="icon-arrow-right text-sm font-normal"></i> | |||
</nuxt-link> | |||
@@ -529,43 +532,13 @@ import "swiper/css/navigation"; | |||
import "swiper/css/pagination"; | |||
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 videoWebp from "@/assets/videos/video.webp"; | |||
import homeA1Webp from "@/assets/images/home-a-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 { | |||
id: number; | |||
title: string; | |||
@@ -577,6 +550,7 @@ interface Product { | |||
interface Usage { | |||
id: number; | |||
name: string; | |||
category?: string; | |||
products: Product[]; | |||
} | |||
@@ -584,11 +558,203 @@ interface Category { | |||
id: number; | |||
title: string; | |||
description: string; | |||
features: string[]; | |||
capacities: string[]; | |||
summary: string; | |||
sort: number; | |||
image: 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(() => { | |||
console.log("Typed Usage List:", usageList.value); | |||
@@ -606,7 +772,6 @@ const activeProducts = computed(() => { | |||
const currentUsage = typedUsageList.value.find( | |||
(item: Usage) => item.id === activeUsageId.value | |||
); | |||
console.log("Current Usage:", currentUsage); | |||
if (currentUsage?.products) { | |||
console.log( | |||
"Products:", | |||
@@ -679,7 +844,6 @@ const handleUsageClick = (id: number) => { | |||
// 计算当前分类列表 | |||
const typedCategoryList = computed(() => { | |||
console.log("Typed Category List:", categoryList.value); | |||
return categoryList.value as Category[]; | |||
}); | |||
@@ -302,15 +302,16 @@ | |||
* 展示产品主图、参数和描述 | |||
*/ | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
import { useAsyncData, useRoute, useI18n } from "#imports"; | |||
import { useRoute, useI18n, useAsyncData } from "#imports"; | |||
import { queryCollection } from "#imports"; | |||
import { ContentRenderer } from "#components"; | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const { error, isLoading } = useErrorHandler(); | |||
const route = useRoute(); | |||
const product = ref<Product | null>(null); | |||
const relatedProducts = ref<Product[]>([]); | |||
const { locale, t } = useI18n(); | |||
const id = route.params.id as string; | |||
// 图片状态 | |||
const currentImage = ref<string>(""); | |||
const isImageLoading = ref(true); | |||
const isThumbnailLoading = ref<boolean[]>([]); | |||
@@ -340,166 +341,116 @@ interface Product { | |||
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 { | |||
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}`) | |||
.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, | |||
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: { | |||
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) { | |||
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个相关产品 | |||
}); | |||
/** | |||
* 预加载下一张图片 | |||
@@ -594,95 +545,19 @@ function changeImage(image: string | undefined) { | |||
} | |||
} | |||
/** | |||
* 加载相关产品 | |||
*/ | |||
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(() => { | |||
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优化 | |||
useHead(() => ({ | |||
title: `${product.value?.name || "产品详情"} - Hanye`, |
@@ -0,0 +1,215 @@ | |||
[ | |||
"/", | |||
"/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" | |||
] |
@@ -0,0 +1,83 @@ | |||
[ | |||
{ | |||
"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" | |||
] | |||
} | |||
] |
@@ -0,0 +1,84 @@ | |||
[ | |||
{ | |||
"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" | |||
] | |||
} | |||
] |
@@ -0,0 +1,84 @@ | |||
[ | |||
{ | |||
"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" | |||
] | |||
} | |||
] |
@@ -0,0 +1,34 @@ | |||
[ | |||
{ | |||
"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" | |||
} | |||
] |
@@ -0,0 +1,34 @@ | |||
[ | |||
{ | |||
"id": 1, | |||
"title": "写真・ビデオ撮影用ストレージ" | |||
}, | |||
{ | |||
"id": 2, | |||
"title": "スマートフォン/タブレット用ストレージ拡張" | |||
}, | |||
{ | |||
"id": 3, | |||
"title": "ゲーム用ストレージ/パフォーマンス向上" | |||
}, | |||
{ | |||
"id": 4, | |||
"title": "オフィス/学習用ストレージ拡張" | |||
}, | |||
{ | |||
"id": 5, | |||
"title": "モバイルデータ転送/バックアップ" | |||
}, | |||
{ | |||
"id": 6, | |||
"title": "メディア/エンターテイメント" | |||
}, | |||
{ | |||
"id": 7, | |||
"title": "コンテンツ制作/動画編集" | |||
}, | |||
{ | |||
"id": 8, | |||
"title": "メモリアップグレード/パフォーマンス向上" | |||
} | |||
] |
@@ -0,0 +1,34 @@ | |||
[ | |||
{ | |||
"id": 1, | |||
"title": "摄影存储" | |||
}, | |||
{ | |||
"id": 2, | |||
"title": "手机/平板扩容" | |||
}, | |||
{ | |||
"id": 3, | |||
"title": "游戏加速/存储" | |||
}, | |||
{ | |||
"id": 4, | |||
"title": "办公/学习扩容" | |||
}, | |||
{ | |||
"id": 5, | |||
"title": "移动数据传输/备份" | |||
}, | |||
{ | |||
"id": 6, | |||
"title": "媒体/影音娱乐" | |||
}, | |||
{ | |||
"id": 7, | |||
"title": "内容创作/视频编辑" | |||
}, | |||
{ | |||
"id": 8, | |||
"title": "内存升级/性能提升" | |||
} | |||
] |
@@ -0,0 +1,293 @@ | |||
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(); |