watermax 1 month ago
parent
commit
246894389e

+ 1
- 0
components/LanguageSwitcher.vue View File

@@ -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);
// 这里可以添加用户反馈,例如显示一个错误提示

+ 55
- 18
components/TheFooter.vue View File

@@ -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>

+ 105
- 40
components/TheHeader.vue View File

@@ -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}`,
})),
},
],
},

+ 15
- 1
content/usages/zh/3.md View File

@@ -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技术带来接近内存的访问速度
- **低延迟**:减少游戏中的卡顿和画面撕裂
- **大容量**:轻松应对现代游戏的海量存储需求
- **散热设计**:长时间游戏也能保持稳定性能

+ 1
- 0
i18n/locales/en.ts View File

@@ -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",

+ 1
- 0
i18n/locales/ja.ts View File

@@ -72,6 +72,7 @@ export default {
strong_point_title: "当社の強み/選ばれる理由",
view_details: "詳細を見る",
product_count: "製品",
loadError: "製品の読み込みに失敗しました。後でもう一度お試しください。",
},
faq: {
title: "よくある質問",

+ 1
- 0
i18n/locales/zh.ts View File

@@ -72,6 +72,7 @@ export default {
strong_point_title: "我们的优势/选择我们的理由",
view_details: "查看详情",
product_count: "款产品",
loadError: "加载产品失败,请稍后重试",
},
faq: {
title: "常见问题",

+ 62
- 39
nuxt.config.ts View File

@@ -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",
});

+ 2
- 1
package-lock.json View File

@@ -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",

+ 5
- 3
package.json View File

@@ -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",

+ 27
- 27
pages/about.vue View File

@@ -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>

+ 25
- 25
pages/contact.vue View File

@@ -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>

+ 26
- 15
pages/faq.vue View File

@@ -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];

+ 225
- 61
pages/index.vue View File

@@ -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[];
});


+ 110
- 235
pages/products/[id].vue View File

@@ -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`,

+ 215
- 0
prerenderRoutes.json View File

@@ -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"
]

+ 83
- 0
public/data/categories-en.json View File

@@ -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"
]
}
]

+ 84
- 0
public/data/categories-ja.json View File

@@ -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"
]
}
]

+ 84
- 0
public/data/categories-zh.json View File

@@ -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"
]
}
]

+ 1202
- 0
public/data/products-en.json
File diff suppressed because it is too large
View File


+ 1202
- 0
public/data/products-ja.json
File diff suppressed because it is too large
View File


+ 1280
- 0
public/data/products-zh.json
File diff suppressed because it is too large
View File


+ 34
- 0
public/data/usages-en.json View File

@@ -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"
}
]

+ 34
- 0
public/data/usages-ja.json View File

@@ -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": "メモリアップグレード/パフォーマンス向上"
}
]

+ 34
- 0
public/data/usages-zh.json View File

@@ -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": "内存升级/性能提升"
}
]

+ 293
- 0
scripts/generateRoutes.js View File

@@ -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();

Loading…
Cancel
Save