浏览代码

feat: 添加图片本地化功能,优化静态站点生成

- 在nuxt.config.ts中新增图片本地化配置,支持将API返回的远程图片下载到本地。
- 更新package.json,新增生成静态站点并本地化图片的命令。
- 新增README-image-localization.md文档,详细说明图片本地化功能的使用方法和注意事项。
- 在TheHeader和TheFooter组件中优化产品分类和网站链接的展示逻辑,提升用户体验。
- 更新多个页面的内容和样式,确保多语言支持的一致性。
- 新增图片下载和本地化的脚本,确保静态站点完全脱离原始服务器运行。
master
lizhuang 3 个月前
父节点
当前提交
2174c29a81

+ 86
- 0
README-image-localization.md 查看文件

# API图片本地化功能

此功能用于将API返回的远程图片链接下载到本地并替换成本地路径,方便生成的静态站点完全脱离原始服务器。

## 功能概述

1. 在Nuxt静态站点生成完成后,处理所有API响应JSON文件
2. 自动识别数据中的图片URL字段并下载图片到本地
3. 替换JSON数据中的远程URL为本地路径
4. 确保静态生成的网站能完全离线运行,所有图片资源都在本地

## 使用方法

### 基本使用

直接使用以下命令生成静态站点并本地化图片:

```bash
npm run generate:localize
```

这个命令会:
1. 运行标准的`nuxt generate`生成静态站点
2. 运行图片本地化脚本处理API数据中的图片URL

### 只处理图片

如果你已经生成了静态站点,只需要处理图片:

```bash
npm run localize-images
```

## 自定义配置

### 支持的图片字段

默认情况下,系统会查找以下字段名称中的图片URL:
- image
- imageUrl
- thumbnail
- cover
- avatar
- photo
- src

如需自定义,可以在`scripts/localize-images.mjs`中修改`localizeImages`函数的默认参数。

### 图片保存位置

默认情况下,下载的图片会保存到`public/images/remote`目录,网站访问路径为`/images/remote/[图片文件名]`。

## 工作原理

1. Nuxt先正常执行静态生成过程,将API数据缓存到`.output/server/api`目录下
2. 脚本`scripts/localize-images.mjs`在生成完成后执行,扫描并处理缓存的JSON文件
3. 识别并下载所有图片到`public/images/remote`目录
4. 更新JSON响应文件,将远程图片URL替换为本地路径

## 注意事项

1. 此方法不需要修改源代码,完全在构建后处理
2. 下载的图片使用URL的MD5哈希作为文件名,确保相同URL的图片只下载一次
3. 如果图片下载失败,会保留原始URL,不会破坏网站功能
4. 图片本地化过程会在控制台输出详细日志

## 常见问题

### 生成后图片仍然是远程链接

检查以下几点:
1. 确认使用了`npm run generate:localize`而不是`npm run generate`
2. 检查控制台输出中是否有错误信息
3. 检查`public/images/remote`目录是否已创建并包含图片

### 图片下载失败

可能的原因:
1. 网络问题,无法访问原始图片URL
2. 原始URL返回错误,如404或403
3. 目标目录没有写入权限

解决方法:
1. 检查网络连接和图片URL是否可访问
2. 确保目标目录有写入权限
3. 手动修复失败的图片URL

+ 4
- 2
assets/css/styles.css 查看文件

text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
font-feature-settings: "palt"; font-feature-settings: "palt";
letter-spacing: 0.02em; letter-spacing: 0.02em;
font-weight: 400;
font-style: normal;
} }




/* 仅在日语环境下应用日文字体 */ /* 仅在日语环境下应用日文字体 */
html[lang="ja"] body { html[lang="ja"] body {
font-family: 'M PLUS 1p', sans-serif !important;
font-family: 'Noto Sans JP', 'M PLUS 1p', sans-serif !important;
} }


html[lang="en"] body { html[lang="en"] body {
font-family: 'Montserrat', sans-serif !important;
font-family: 'Montserrat', 'Noto Sans JP', sans-serif !important;
} }

+ 76
- 16
components/TheFooter.vue 查看文件

</p> </p>
</div> </div>


<!-- 快捷链接 -->
<!-- 产品分类链接 -->
<div class="hidden lg:block"> <div class="hidden lg:block">
<h3 <h3
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4"
> >
{{ $t("common.home") }}
{{ $t("common.footer.productsLinks.title") }}
</h3> </h3>
<ul class="space-y-4"> <ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<li v-for="item in menuProductsItems" :key="item.path">
<NuxtLink <NuxtLink
:to="item.path" :to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition" class="text-zinc-500 text-sm font-normal hover:text-white transition"
</li> </li>
</ul> </ul>
</div> </div>
<!-- 快捷链接 -->
<!-- 网站快捷链接 -->
<div class="hidden lg:block"> <div class="hidden lg:block">
<h3 <h3
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4"
> >
{{ $t("common.home") }}
{{ $t("common.footer.websiteLinks.title") }}
</h3> </h3>
<ul class="space-y-4"> <ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<li v-for="item in menuWebsiteItems" :key="item.path">
<NuxtLink <NuxtLink
:to="item.path" :to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition" class="text-zinc-500 text-sm font-normal hover:text-white transition"
</li> </li>
</ul> </ul>
</div> </div>
<!-- 快捷链接 -->
<!-- 网站快捷链接 -->
<div class="hidden lg:block"> <div class="hidden lg:block">
<h3 <h3
class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4" class="w-36 justify-start text-zinc-300 text-xl font-normal leading-snug mb-4"
> >
{{ $t("common.home") }}
{{ $t("common.footer.quickLinks.title") }}
</h3> </h3>
<ul class="space-y-4"> <ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<li v-for="item in menuHomeItems" :key="item.path">
<NuxtLink <NuxtLink
:to="item.path" :to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition" class="text-zinc-500 text-sm font-normal hover:text-white transition"
* 页脚组件 * 页脚组件
* 包含网站导航、联系信息和版权信息 * 包含网站导航、联系信息和版权信息
*/ */
import { useI18n } from "vue-i18n";
const { locale } = 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 productCategories = computed(() => {
if (!categoryResponse.value?.data) return [];
return categoryResponse.value.data;
});

// 导航菜单项 // 导航菜单项
const menuItems = [
{ label: "common.home", path: "/" },
{ label: "common.products", path: "/products" },
{ label: "common.faq", path: "/faq" },
{ label: "common.about", path: "/about" },
{ label: "common.contact", path: "/contact" },
];
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}`
}));
});

const menuWebsiteItems = computed(() => [
{
label: "common.footer.websiteLinks.home",
path: locale.value === defaultLocale ? "/" : `/${locale.value}`,
},
{
label: "common.footer.websiteLinks.products",
path:
locale.value === defaultLocale
? "/products"
: `/${locale.value}/products`,
},
{
label: "common.footer.websiteLinks.faq",
path: locale.value === defaultLocale ? "/faq" : `/${locale.value}/faq`,
},
{
label: "common.footer.websiteLinks.about",
path: locale.value === defaultLocale ? "/about" : `/${locale.value}/about`,
},
{
label: "common.footer.websiteLinks.contact",
path:
locale.value === defaultLocale ? "/contact" : `/${locale.value}/contact`,
},
]);

const menuHomeItems = computed(() => [
// 这里可以根据需要添加首页快捷链接
{
label: "common.footer.quickLinks.support",
path: locale.value === defaultLocale ? "/support" : `/${locale.value}/support`,
},
{
label: "common.footer.quickLinks.privacy",
path: locale.value === defaultLocale ? "/privacy" : `/${locale.value}/privacy`,
},
{
label: "common.footer.quickLinks.terms",
path: locale.value === defaultLocale ? "/terms" : `/${locale.value}/terms`,
}
]);
</script> </script>

+ 32
- 16
components/TheHeader.vue 查看文件

// 添加热门关键字 // 添加热门关键字
const hotKeywords = ref(["SSD", "SD", "DDR4"]); const hotKeywords = ref(["SSD", "SD", "DDR4"]);


// 获取产品分类数据
const { data: categoryResponse } = useFetch(`/api/products/category?lang=${locale.value}`, {
key: `category-${locale.value}`
});
const productCategories = computed(() => {
if (!categoryResponse.value?.data) return [];
return categoryResponse.value.data.map((category: any) => ({
label: category.title,
path: `/products?category=${category.id}`
}));
});

// 获取产品用途数据
const { data: usageResponse } = useFetch(`/api/products/usage?lang=${locale.value}`, {
key: `usage-${locale.value}`
});
const productUsages = computed(() => {
if (!usageResponse.value?.data) return [];
return usageResponse.value.data.map((usage: any) => ({
label: usage.name,
path: `/products?usage=${usage.id}`
}));
});

// 使用 computed 来定义 homePath,根据是否为默认语言调整路径 // 使用 computed 来定义 homePath,根据是否为默认语言调整路径
const homePath = computed(() => { const homePath = computed(() => {
// 如果是默认语言,路径为根路径 '/' // 如果是默认语言,路径为根路径 '/'
children: [ children: [
{ {
title: "common.productCategories", title: "common.productCategories",
items: [
{ label: "SSD", path: `${prefix}/products?category=ssd` },
{ label: "DRAM", path: `${prefix}/products?category=dram` },
{ label: "NAND", path: `${prefix}/products?category=nand` },
],
items: productCategories.value.map((category: any) => ({
...category,
path: `${prefix}${category.path}`
}))
}, },
{ {
title: "common.byUsage", title: "common.byUsage",
items: [
{
label: "Enterprise",
path: `${prefix}/products?usage=enterprise`,
},
{ label: "Consumer", path: `${prefix}/products?usage=consumer` },
{
label: "Industrial",
path: `${prefix}/products?usage=industrial`,
},
],
items: productUsages.value.map((usage: any) => ({
...usage,
path: `${prefix}${usage.path}`
}))
}, },
], ],
}, },

+ 91
- 81
i18n/locales/en.ts 查看文件

hotKeywords: "Hot Keywords", hotKeywords: "Hot Keywords",
productCategories: "Product Categories", productCategories: "Product Categories",
byUsage: "By Usage", byUsage: "By Usage",
footer: {
productsLinks: {
title: "Products",
},
websiteLinks: {
title: "Website",
home: "Home",
products: "Products",
faq: "FAQ",
about: "About Us",
contact: "Contact",
},
quickLinks: {
title: "Quick Links",
},
},
}, },
home: { home: {
title: "Welcome to Hanye Website",
description: "Providing high-quality products and services.",
title: "Hanye Website",
description:
"Providing high-quality products and services, including memory and SD, SSD, microSD related products, and professional technical support.",
keywords:
"Hanye, memory, storage, products, services, SD, SSD, microSD, technical support",
learnMore: "Learn More", learnMore: "Learn More",
carousel: {
one: {
title: "New Technology",
description: "3D NAND TLC",
description2: "Flash-based,",
description3: "High reliability and durability",
description4: "Enjoy stress-free gaming experience",
description5: "The computer startup time is dramatically faster!",
},
},
}, },
products: { products: {
title: "Our Products",
title: "Products",
description: "Our products, product usage, and purchase help.",
keywords: "Hanye, products, product information, technical support",
product_list: "Product List",
product_list_description:
"Excellent products are the result of 10 years of experience and continuous innovation design.",
product_categories_title: "Categories",
product_categories_description: "Select products by type.",
product_categories_usage: "By Usage",
product_categories_usage_description: "Select products by usage.",
viewDetails: "View Details", viewDetails: "View Details",
consultation: consultation:
"Feel free to inquire about product consultations and quotes.", "Feel free to inquire about product consultations and quotes.",
"Diversifying products and relentlessly pursuing new creativity.", "Diversifying products and relentlessly pursuing new creativity.",
strong_point: "Our Strengths", strong_point: "Our Strengths",
strong_point_title: "Our Strengths / Why Choose Us", strong_point_title: "Our Strengths / Why Choose Us",
view_details: "View Details",
}, },
faq: { faq: {
title: "Frequently Asked Questions", title: "Frequently Asked Questions",
description:
"Frequently asked questions, product usage, and purchase help.",
keywords:
"Hanye, frequently asked questions, product information, technical support",
searchPlaceholder: "Search questions", searchPlaceholder: "Search questions",
category: "Category",
noResults: "No results found",
clearSearch: "Clear search",
}, },
about: { about: {
title: "About Us", title: "About Us",
meta: {
title: "About Us - Hanye",
description:
"Learn about Hanye's company information, history, and business scope. We are dedicated to the development, manufacturing, and sales of memory and related products.",
},
intro: {
title: "About Hanye",
paragraph1:
"Hanye was established in 2003 with its operational headquarters in Shenyang, China.",
paragraph2:
"From its founding to the present, we have grown into a comprehensive enterprise integrating the development, manufacturing, and sales of memory (storage media) and related products.",
paragraph3:
"We have also established a complete after-sales service system to provide full support. We strive to be a trusted partner for our customers and will continue to make further efforts.",
},
description:
"Introduction to Hanye's company overview, business philosophy, and corporate information.",
keywords:
"Hanye, company overview, business philosophy, corporate information",
overview: { overview: {
title: "Company Overview",
companyNameLabel: "Company Name",
companyNameValue: "Hanye",
englishNameLabel: "English Name",
englishNameValue: "Hanye Technology Co., Ltd.",
ceoLabel: "CEO / Representative Director",
ceoValue: "ZHENG XIAO DONG",
employeesLabel: "Number of Employees",
employeesValue: "30",
addressLabel: "Address",
addressValue:
"803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
telLabel: "TEL",
telValue: "86)024-8399-0696",
faxLabel: "FAX",
faxValue: "86)024-8399-0696",
businessLabel: "Business Activities",
businessValue1:
"Development, manufacturing, and sales of flash memory products",
businessValue2: "Development, manufacturing, and sales of SSD products",
businessValue3: "Related businesses",
title: "Overview",
companyInfo: "Company Info",
established: "Established",
ceo: "CEO",
employees: "Employees",
location: "Location",
philosophy: "Philosophy",
contact: "Contact",
email: "Email",
tel: "Tel",
fax: "Fax",
businessHours: "Business Hours",
businessActivities: "Business Activities",
companyName: "Company Name",
englishName: "English Name",
}, },
contact: {
title: "Contact Information",
emailLabel: "E-mail",
emailValue: "hanye#hanye.cn",
hoursLabel: "Business Hours",
hoursValue1: "Weekdays 9:00 - 18:00",
hoursValue2: "Closed on Saturdays, Sundays, and holidays",
phoneLabel: "Phone",
phoneValue: "86)024-8399-0696",
phoneNote: "Please call for inquiries regarding visits.",
companyInfo: {
name: "Hanye",
companyName: "Hanye",
englishName: "Hanye Technology Co., Ltd.",
description:
"Hanye was founded in 2003 and is headquartered in Shenyang, China. Since its inception, we have grown into a comprehensive enterprise that develops, manufactures, and sells memory and related products.",
established: "2003",
ceo: "ZHENG XIAO DONG",
employees: "30",
location:
"803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
philosophy:
"We are committed to providing customers with more comfortable and secure digital lives, focusing on sustainable development, and contributing to society as a growth-oriented enterprise.",
businessHours: "Mon - Fri: 9:00 AM - 6:00 PM",
businessActivities:
"Development, production, and sales of flash memory, development, manufacturing, and sales of SSD products, and other related businesses.",
}, },
}, },
contact: { contact: {
title: "Contact Us", title: "Contact Us",
description: "Contact us for more product information and support.",
keywords: "Hanye, contact us, product information, technical support",
name: "Name", name: "Name",
email: "Email Address",
email: "Email",
message: "Message", message: "Message",
submit: "Send Message",
form: {
title: "Leave us a message",
nameLabel: "Name",
emailLabel: "Email Address",
messageLabel: "Message",
captchaLabel: "Captcha",
submitLabel: "Send Message",
successMessage: "Message sent successfully. We will contact you soon.",
submitLoading: "Sending...",
captchaRefresh: "Refresh Captcha",
captchaRequired: "Please enter the captcha code",
captchaIncorrect: "Incorrect captcha code",
nameRequired: "Please enter your name",
emailRequired: "Please enter your email address",
emailInvalid: "Please enter a valid email address",
messageRequired: "Please enter your message",
},
info: {
title: "Contact Info",
description: "Leave us a message, and we will get back to you shortly.",
addressLabel: "Address",
addressValue1: "803, NO.6, AiTe, 90-6# SanHao Street,",
addressValue2: "Heping District, ShenYang, China",
phoneLabel: "Phone",
phoneValue: "86)024-8399-0696",
emailLabel: "Email",
emailValue: "info#hanye.com",
hoursLabel: "Business Hours",
hoursValue1: "Mon - Fri: 9:00 AM - 6:00 PM",
hoursValue2: "Sat - Sun: Closed",
},
captcha: "Captcha",
refreshCaptcha: "Refresh Captcha",
submit: "Submit",
submitting: "Submitting...",
}, },
}; };

+ 79
- 68
i18n/locales/ja.ts 查看文件

hotKeywords: "人気のキーワード", hotKeywords: "人気のキーワード",
productCategories: "製品カテゴリー", productCategories: "製品カテゴリー",
byUsage: "用途で選ぶ", byUsage: "用途で選ぶ",
footer: {
productsLinks: {
title: "製品",
},
websiteLinks: {
title: "ウェブサイト",
home: "ホーム",
products: "製品",
faq: "よくある質問",
about: "会社概要",
contact: "お問い合わせ",
},
quickLinks: {
title: "クイックリンク",
},
},
}, },
home: { home: {
title: "Hanye ウェブサイトへようこそ", title: "Hanye ウェブサイトへようこそ",
description: "高品質の製品とサービスを提供しています", description: "高品質の製品とサービスを提供しています",
keywords: "Hanye, メモリ, ストレージ, 製品, サービス, SD, SSD, microSD",
learnMore: "詳細を見る", learnMore: "詳細を見る",
carousel: {
one: {
title: "新技術",
description: "3D NAND TLC",
description2: "フラッシュ採用、",
description3: "信頼性が高く耐久性に優\nれている",
description4: "ストレスのないゲーム体験をお楽しみください",
description5: "パソコンの起動時間が劇的に速くなった!",
},
},
}, },
products: { products: {
title: "当社の製品", title: "当社の製品",
description: "当社の製品、製品の使用方法や購入に関する助けを得ることができます。",
keywords: "Hanye, 製品, 製品情報, 技術サポート",
product_list: "製品一覧",
product_list_description:
"優れた製品は、10年以上の経験と継続的な革新デザインの結果です。",
product_categories_title: "製品カテゴリー",
product_categories_description: "製品の種類で選ぶ。",
product_categories_usage: "用途で選ぶ",
product_categories_usage_description: "製品の用途で選ぶ。",
viewDetails: "詳細を見る", viewDetails: "詳細を見る",
consultation: "製品に関するご相談、お見積もりはお気軽にどうぞ", consultation: "製品に関するご相談、お見積もりはお気軽にどうぞ",
consultation_button: "お問い合わせ", consultation_button: "お問い合わせ",
develop_description: "製品の多様化を図り、新たな創意への飽くなき挑戦", develop_description: "製品の多様化を図り、新たな創意への飽くなき挑戦",
strong_point: "当社の強み", strong_point: "当社の強み",
strong_point_title: "当社の強み/選ばれる理由", strong_point_title: "当社の強み/選ばれる理由",
view_details: "詳細を見る",
}, },
faq: { faq: {
title: "よくある質問", title: "よくある質問",
description: "よくある質問、製品の使用方法や購入に関する助けを得ることができます。",
keywords: "Hanye, よくある質問, 製品情報, 技術サポート",
searchPlaceholder: "質問を検索", searchPlaceholder: "質問を検索",
category: "カテゴリー",
noResults: "該当する質問はありません",
clearSearch: "検索をクリア",
}, },
about: { about: {
title: "当社について",
meta: {
title: "当社について - Hanye",
description: "Hanyeの会社情報、沿革、事業内容をご覧ください。メモリ及び関連製品の開発・製造・販売に取り組んでいます。"
},
intro: {
title: "当社について",
paragraph1: "Hanye は中国瀋陽に運営本部をおき 2003年に設立されました。",
paragraph2: "創業から現在に至るまで、メモリ(記憶媒体) 及び 関連製品の開発・製造・販売を統べる総合企業に成長いたしました。",
paragraph3: "また万全なアフターサービス体制も構築し全力でサポートいたします。お客様に信頼いただけるパートナーを目指し 一層の努力を重ねてまいります。",
},
title: "当社について - Hanye",
description: "Hanyeの会社概要、経営理念、企業情報をご紹介します。",
keywords: "Hanye, 会社概要, 経営理念, 企業情報",
overview: { overview: {
title: "会社概要", title: "会社概要",
companyNameLabel: "会社名",
companyNameValue: "Hanye",
englishNameLabel: "英文社名",
englishNameValue: "Hanye Technology Co., Ltd.",
ceoLabel: "代表取締役",
ceoValue: "ZHENG XIAO DONG",
employeesLabel: "従業員数",
employeesValue: "30名",
addressLabel: "所在地",
addressValue: "803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
telLabel: "TEL",
telValue: "86)024-8399-0696",
faxLabel: "FAX",
faxValue: "86)024-8399-0696",
businessLabel: "事業内容",
businessValue1: "フラッシュメモリーの開発・製造・販売",
businessValue2: "SSD製品の開発・製造・販売",
businessValue3: "その関連事業",
companyInfo: "会社情報",
established: "設立",
ceo: "代表取締役",
employees: "従業員数",
location: "所在地",
philosophy: "経営理念",
contact: "お問い合わせ",
email: "メールアドレス",
tel: "電話番号",
fax: "FAX番号",
businessHours: "営業時間",
businessActivities: "事業内容",
companyName: "会社名",
englishName: "英文名",
},
companyInfo: {
name: "Hanye",
companyName: "Hanye",
englishName: "Hanye Technology Co., Ltd.",
description: "Hanye は中国瀋陽に運営本部をおき2003年に設立されました。\n創業から現在に至るまで、メモリ(記憶媒体) 及び 関連製品の開発・製造・販売を統べる総合企業に成長いたしました。\nまた万全なアフターサービス体制も構築し全力でサポートいたします。\nお客様に信頼いただけるパートナーを目指し 一層の努力を重ねてまいります。",
established: "2003年",
ceo: "ZHENG XIAO DONG",
employees: "30人",
location: "803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
philosophy:
"私たちは、革新的な技術と優れた品質で、お客様のデジタルライフをより快適に、より安全にすることを目指しています。持続可能な開発と環境への配慮を重視し、社会に貢献する企業として成長し続けます。",
businessHours: "月曜日 - 金曜日 9:00-18:00",
businessActivities:
"閃存の開発、生産、販売, SSD製品の開発、製造、販売, その他の関連業務",
}, },
contact: {
title: "連絡先",
emailLabel: "E-mail",
emailValue: "hanye#hanye.cn",
hoursLabel: "営業時間",
hoursValue1: "平日9:00~18:00",
hoursValue2: "土日祝定休",
phoneLabel: "電話",
phoneValue: "86)024-8399-0696",
phoneNote: "ご来社の場合はお電話でお問い合わせください",
}
}, },
contact: { contact: {
title: "お問い合わせ", title: "お問い合わせ",
description: "Hanyeにお問い合わせください。",
keywords: "Hanye, お問い合わせ, 製品情報, 技術サポート",
name: "お名前", name: "お名前",
email: "メールアドレス", email: "メールアドレス",
message: "メッセージ", message: "メッセージ",
captcha: "検証コード",
refreshCaptcha: "検証コードを更新",
submit: "送信", submit: "送信",
form: {
title: "メッセージを残してください",
nameLabel: "お名前",
emailLabel: "メールアドレス",
messageLabel: "メッセージ",
captchaLabel: "検証コード",
submitLabel: "送信",
successMessage: "メッセージを送信しました。すぐに連絡いたします。",
submitLoading: "送信中...",
captchaRefresh: "検証コードを更新",
captchaRequired: "検証コードを入力してください",
captchaIncorrect: "検証コードが間違っています",
nameRequired: "お名前を入力してください",
},
info: {
title: "お問い合わせ",
description: "メッセージを残してください。すぐに連絡いたします。",
addressLabel: "住所",
addressValue1: "803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
addressValue2: "",
phoneLabel: "電話番号",
phoneValue: "86)024-8399-0696",
emailLabel: "メールアドレス",
emailValue: "info#hanye.com",
hoursLabel: "営業時間",
hoursValue1: "月曜日 - 金曜日 9:00-18:00",
hoursValue2: "土曜日 - 日曜日 9:00-18:00",
},
submitting: "送信中...",
}, },
}; };

+ 84
- 82
i18n/locales/zh.ts 查看文件

hotKeywords: "热门搜索", hotKeywords: "热门搜索",
productCategories: "产品分类", productCategories: "产品分类",
byUsage: "按用途", byUsage: "按用途",
footer: {
productsLinks: {
title: "产品",
},
websiteLinks: {
title: "网站",
home: "首页",
products: "产品",
faq: "常见问题",
about: "关于我们",
contact: "联系我们",
},
quickLinks: {
title: "快捷链接",
},
},
}, },
home: { home: {
title: "欢迎来到Hanye官网",
description: "我们提供高质量的产品和服务",
title: "Hanye 官网",
description:
"我们提供高质量的产品和服务,包括内存及SD,SSD,microSD相关产品, 并提供专业的技术支持",
keywords: "Hanye, 内存, 存储, 产品, 服务, SD, SSD, microSD, 技术支持",
learnMore: "了解更多", learnMore: "了解更多",
carousel: {
one: {
title: "新科技",
description: "3D NAND TLC",
description2: "Flash-based,",
description3: "高可靠性、高耐用性",
description4: "享受无压力的游戏体验",
description5: "电脑启动时间大幅提升!",
},
},
}, },
products: { products: {
title: "我们的产品",
title: "产品",
description: "我们的产品,帮助您更好地了解Hanye的产品和服务。",
keywords: "Hanye, 产品, 服务, SD, SSD, microSD, 技术支持",
product_list: "产品列表",
product_list_description:
"卓越的产品是基于我们10多年经验的技术和持续创新设计相结合的结果。",
product_categories_title: "产品分类",
product_categories_description: "根据产品类型选择产品。",
product_categories_usage: "按用途",
product_categories_usage_description: "根据产品用途选择产品。",
viewDetails: "查看详情", viewDetails: "查看详情",
consultation: "欢迎进行产品咨询,我们将在第一时间回复您", consultation: "欢迎进行产品咨询,我们将在第一时间回复您",
consultation_button: "联系我们", consultation_button: "联系我们",
develop_description: "不断开发和制造产品,提供创新解决方案", develop_description: "不断开发和制造产品,提供创新解决方案",
strong_point: "我们的优势", strong_point: "我们的优势",
strong_point_title: "我们的优势/选择我们的理由", strong_point_title: "我们的优势/选择我们的理由",
view_details: "查看详情",
}, },
faq: { faq: {
title: "常见问题", title: "常见问题",
description: "常见问题,帮助您更好地了解Hanye的产品和服务。",
keywords: "Hanye, 常见问题, 产品信息, 技术支持",
searchPlaceholder: "搜索问题", searchPlaceholder: "搜索问题",
category: "分类",
clearSearch: "清除搜索",
noResults: "没有找到相关问题",
}, },
about: { about: {
title: "关于我们", title: "关于我们",
meta: {
title: "关于我们 - Hanye",
description: "了解 Hanye 的公司信息、发展历程和业务范围。我们致力于内存及相关产品的开发、制造和销售。"
},
intro: {
title: "公司简介",
paragraph1: "Hanye 成立于2003年,运营总部位于中国沈阳。",
paragraph2: "自创立至今,我们已成长为集内存(存储介质)及相关产品的研发、制造、销售于一体的综合性企业。",
paragraph3: "我们建立了完善的售后服务体系,竭诚为您提供支持。我们致力于成为客户信赖的合作伙伴,并将为此付出更多努力。",
},
description: "介绍Hanye的公司概况、经营理念、企业信息",
keywords: "Hanye, 公司概况, 经营理念, 企业信息",
overview: { overview: {
title: "公司概要",
companyNameLabel: "公司名称",
companyNameValue: "Hanye",
englishNameLabel: "英文名称",
englishNameValue: "Hanye Technology Co., Ltd.",
ceoLabel: "法人代表",
ceoValue: "郑晓东", // Translated name if appropriate, otherwise keep original
employeesLabel: "员工人数",
employeesValue: "30人",
addressLabel: "地址",
addressValue: "中国辽宁省沈阳市和平区三好街90-6号艾特国际6号楼803室", // More detailed Chinese address
telLabel: "电话",
telValue: "86)024-8399-0696",
faxLabel: "传真",
faxValue: "86)024-8399-0696",
businessLabel: "业务范围",
businessValue1: "闪存产品的开发、制造、销售",
businessValue2: "SSD产品的开发、制造、销售",
businessValue3: "及相关业务",
title: "公司概况",
companyInfo: "公司信息",
established: "成立时间",
ceo: "CEO",
employees: "员工人数",
location: "所在地",
philosophy: "经营理念",
contact: "联系方式",
email: "邮箱",
tel: "电话",
fax: "传真",
businessHours: "营业时间",
businessActivities: "经营内容",
companyName: "公司名称",
englishName: "英文名称",
},
companyInfo: {
name: "Hanye",
companyName: "Hanye",
englishName: "Hanye Technology Co., Ltd.",
description:
"Hanye 成立于2003年,运营总部设在中国沈阳。\n创业至今,我司已成长为一家集开发、制造、销售内存(存储媒体)及相关产品于一体的综合性企业。\n我司还将构筑完善的售后服务体系,全力提供技术支持。\n以成为客户信赖的合作伙伴为目标而更加努力。",
established: "2003年",
ceo: "ZHENG XIAO DONG",
employees: "30人",
location: "中国辽宁省沈阳市和平区三好街90-6号艾特国际大厦803室",
philosophy:
"我们致力于为客户提供更舒适、更安全的数字生活,专注于可持续发展,并作为成长型企业为社会做出贡献。",
businessHours: "周一至周五 9:00-18:00 (节假日除外)",
businessActivities:
"闪存的开发、生产和销售,开发、制造和销售SSD产品,其相关业务",
}, },
contact: {
title: "联系方式",
emailLabel: "电子邮箱",
emailValue: "hanye#hanye.cn",
hoursLabel: "营业时间",
hoursValue1: "工作日 9:00 - 18:00",
hoursValue2: "周六、周日及法定节假日休息",
phoneLabel: "电话",
phoneValue: "86)024-8399-0696",
phoneNote: "如需来访,请先电话联系。",
}
}, },
contact: { contact: {
title: "联系我们", title: "联系我们",
description: "联系我们,获取更多产品信息和支持。",
keywords: "Hanye, 联系我们, 产品信息, 技术支持",
name: "姓名", name: "姓名",
email: "邮箱", email: "邮箱",
message: "消息", message: "消息",
captcha: "验证码",
refreshCaptcha: "点击刷新验证码",
submit: "提交", submit: "提交",
form: {
title: "给我们留言",
nameLabel: "姓名",
emailLabel: "邮箱",
messageLabel: "消息",
captchaLabel: "验证码",
submitLabel: "提交",
successMessage: "消息已成功发送,我们会尽快与您联系。",
submitLoading: "正在发送...",
captchaRefresh: "点击刷新验证码",
captchaRequired: "请输入验证码",
captchaIncorrect: "验证码不正确",
nameRequired: "请输入您的姓名",
emailRequired: "请输入您的邮箱",
messageRequired: "请输入您的消息",
emailInvalid: "请输入有效的邮箱地址",
},
validation: {
nameRequired: "请输入您的姓名",
emailRequired: "请输入您的邮箱",
messageRequired: "请输入您的消息",
emailInvalid: "请输入有效的邮箱地址",
},
captcha: {
required: "请输入验证码",
incorrect: "验证码不正确",
},
info: {
title: "联系我们",
description: "欢迎给我们留言,我们将在第一时间回复您",
addressLabel: "地址",
addressValue1: "803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
addressValue2: "中国辽宁省沈阳市和平区三好街90-6号艾特国际大厦803室",
phoneLabel: "电话",
emailLabel: "邮箱",
hoursLabel: "工作时间",
hoursValue1: "周一至周五 9:00-18:00 (节假日除外)",
hoursValue2: "周六至周日 9:00-18:00 (节假日除外)",
},
submitting: "正在提交...",
}, },
}; };

+ 12
- 1
nuxt.config.ts 查看文件

crawlLinks: true, crawlLinks: true,
routes: ["/"], routes: ["/"],
}, },
// 添加图片本地化配置
publicAssets: [
{
dir: 'public',
baseURL: '/'
},
{
dir: 'public/images/remote',
baseURL: '/images/remote'
}
]
}, },


devServer: { devServer: {
host: "0.0.0.0", host: "0.0.0.0",
},
}
}); });

+ 2
- 0
package.json 查看文件

"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev",
"generate": "nuxt generate", "generate": "nuxt generate",
"generate:localize": "nuxt generate && node scripts/localize-images.mjs",
"localize-images": "node scripts/localize-images.mjs",
"preview": "nuxt preview", "preview": "nuxt preview",
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },

+ 134
- 69
pages/about.vue 查看文件

<nuxt-link <nuxt-link
to="/" to="/"
class="justify-start text-white/60 text-base font-normal hover:text-white transition-colors duration-300" class="justify-start text-white/60 text-base font-normal hover:text-white transition-colors duration-300"
>ホーム</nuxt-link
>{{ $t("common.home") }}</nuxt-link
> >
<span class="text-white/60 text-base font-normal px-2"> / </span> <span class="text-white/60 text-base font-normal px-2"> / </span>
<span class="text-white text-base font-normal">会社概要</span>
<span class="text-white text-base font-normal">{{
$t("about.overview.title")
}}</span>
</div> </div>
</div> </div>


<!-- 顶部大标题 --> <!-- 顶部大标题 -->
<div class="flex flex-col items-center justify-center px-2 mb-10"> <div class="flex flex-col items-center justify-center px-2 mb-10">
<h1 class="text-white text-5xl font-bold mb-4 tracking-tight text-center">
{{ companyInfo.name }}
<h1
class="text-white text-5xl font-bold mb-4 tracking-tight text-center"
>
{{ $t("about.companyInfo.name") }}
</h1> </h1>
<div class="text-stone-400 text-xl leading-relaxed text-center max-w-2xl">
{{ companyInfo.description }}
<div
class="text-stone-400 text-xl leading-relaxed text-center max-w-2xl break-words whitespace-pre-wrap"
>
{{ $t("about.companyInfo.description") }}
</div> </div>
</div> </div>


<!-- 横向三栏分区卡片 --> <!-- 横向三栏分区卡片 -->
<div class="w-full flex flex-col lg:flex-row gap-8 justify-center items-stretch max-w-screen-2xl mx-auto px-2 mb-20">
<div
class="w-full flex flex-col lg:flex-row gap-8 justify-center items-stretch max-w-screen-2xl mx-auto px-2 mb-20"
>
<!-- 公司信息卡片 --> <!-- 公司信息卡片 -->
<div class="flex-1 bg-zinc-900/95 backdrop-blur-sm rounded-xl p-8 border border-zinc-800/50 shadow-lg flex flex-col gap-6 min-w-[260px]">
<h2 class="text-white text-2xl font-semibold mb-2 tracking-tight relative">
会社情報
<span class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full"></span>
<div
class="flex-1 bg-zinc-900/95 backdrop-blur-sm rounded-xl p-8 border border-zinc-800/50 shadow-lg flex flex-col gap-6 min-w-[260px]"
>
<h2
class="text-white text-2xl font-semibold mb-2 tracking-tight relative"
>
{{ $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>
</h2> </h2>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">設立</span>
<span class="text-white text-lg font-bold">{{ companyInfo.established }}</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.companyName")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.name")
}}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">{{
$t("about.overview.englishName")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.englishName")
}}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">{{
$t("about.overview.established")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.established")
}}</span>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">代表者</span>
<span class="text-white text-lg font-bold">{{ companyInfo.ceo }}</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.ceo")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.ceo")
}}</span>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">従業員数</span>
<span class="text-white text-lg font-bold">{{ companyInfo.employees }}</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.employees")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.employees")
}}</span>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">所在地</span>
<span class="text-white text-lg font-bold">{{ companyInfo.location }}</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.location")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.location")
}}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- 经营理念卡片 --> <!-- 经营理念卡片 -->
<div class="flex-1 bg-zinc-900/95 backdrop-blur-sm rounded-xl p-8 border border-zinc-800/50 shadow-lg flex flex-col gap-6 min-w-[260px]">
<h2 class="text-white text-2xl font-semibold mb-2 tracking-tight relative">
経営理念
<span class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full"></span>
<div
class="flex-1 bg-zinc-900/95 backdrop-blur-sm rounded-xl p-8 border border-zinc-800/50 shadow-lg flex flex-col gap-6 min-w-[260px]"
>
<h2
class="text-white text-2xl font-semibold mb-2 tracking-tight relative"
>
{{ $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> </h2>
<div class="text-stone-400 text-lg leading-relaxed"> <div class="text-stone-400 text-lg leading-relaxed">
{{ companyInfo.philosophy }}
{{ $t("about.companyInfo.philosophy") }}
</div> </div>
</div> </div>
<!-- 联系方式卡片 --> <!-- 联系方式卡片 -->
<div class="flex-1 bg-zinc-900/95 backdrop-blur-sm rounded-xl p-8 border border-zinc-800/50 shadow-lg flex flex-col gap-6 min-w-[260px]">
<h2 class="text-white text-2xl font-semibold mb-2 tracking-tight relative">
お問い合わせ
<span class="absolute -bottom-2 left-0 w-12 h-1 bg-gradient-to-r from-blue-500 to-blue-300 rounded-full"></span>
<div
class="flex-1 bg-zinc-900/95 backdrop-blur-sm rounded-xl p-8 border border-zinc-800/50 shadow-lg flex flex-col gap-6 min-w-[260px]"
>
<h2
class="text-white text-2xl font-semibold mb-2 tracking-tight relative"
>
{{ $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>
</h2> </h2>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">E-mail</span>
<a :href="'mailto:' + companyInfo.email" class="text-white text-lg font-bold hover:text-blue-400 transition-colors">{{ companyInfo.email }}</a>
<span class="text-stone-400 text-base">{{
$t("about.overview.email")
}}</span>
<a
:href="'mailto:' + companyInfo.email"
class="text-white text-lg font-bold hover:text-blue-400 transition-colors"
>{{ companyInfo.email }}</a
>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">TEL</span>
<a :href="'tel:' + companyInfo.tel.replace(/[^0-9]/g, '')" class="text-white text-lg font-bold hover:text-blue-400 transition-colors">{{ companyInfo.tel }}</a>
<span class="text-stone-400 text-base">{{
$t("about.overview.tel")
}}</span>
<a
:href="'tel:' + companyInfo.tel.replace(/[^0-9]/g, '')"
class="text-white text-lg font-bold hover:text-blue-400 transition-colors"
>{{ companyInfo.tel }}</a
>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">FAX</span>
<span class="text-white text-lg font-bold">{{ companyInfo.fax }}</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.fax")
}}</span>
<span class="text-white text-lg font-bold">{{
companyInfo.fax
}}</span>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<span class="text-stone-400 text-base">営業時間</span>
<span class="text-white text-lg font-bold">{{ companyInfo.businessHours }}</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.businessHours")
}}</span>
<span class="text-white text-lg font-bold">{{
$t("about.companyInfo.businessHours")
}}</span>
</div> </div>
</div> </div>
<div class="flex flex-col gap-1 mt-2"> <div class="flex flex-col gap-1 mt-2">
<span class="text-stone-400 text-base">事業内容</span>
<span class="text-stone-400 text-base">{{
$t("about.overview.businessActivities")
}}</span>
<div class="text-white text-base font-bold space-y-1"> <div class="text-white text-base font-bold space-y-1">
<p v-for="(item, idx) in companyInfo.businessActivities" :key="idx">{{ item }}</p>
<p>
{{ $t("about.companyInfo.businessActivities") }}
</p>
</div> </div>
</div> </div>
</div> </div>
* 展示公司基本信息、理念等 * 展示公司基本信息、理念等
*/ */
import { useErrorHandler } from "~/composables/useErrorHandler"; import { useErrorHandler } from "~/composables/useErrorHandler";
import companyImage from "@/assets/images/product-banner.webp";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();


// 公司信息接口定义 // 公司信息接口定义
interface CompanyInfo { interface CompanyInfo {
name: string;
englishName: string;
description: string;
established: string;
ceo: string;
employees: string;
location: string;
philosophy: string;
email: string; email: string;
tel: string; tel: string;
fax: string; fax: string;
businessHours: string;
businessActivities: string[];
} }


const { error, isLoading, wrapAsync } = useErrorHandler(); const { error, isLoading, wrapAsync } = useErrorHandler();
const companyInfo = ref<CompanyInfo>({ const companyInfo = ref<CompanyInfo>({
name: "Hanye",
englishName: "Hanye Technology Co., Ltd.",
description: "Hanye成立于2003年,运营总部设在中国沈阳。創業から現在に至るまで、メモリ(記憶媒体) 及び 関連製品の開発・製造・販売を統べる総合企業に成長いたしました。我司还将构筑完善的售后服务体系,全力提供技术支持。以成为客户信赖的合作伙伴为目标而更加努力。",
established: "2003年",
ceo: "ZHENG XIAO DONG",
employees: "30人",
location: "803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China",
philosophy: "私たちは、革新的な技術と優れた品質で、お客様のデジタルライフをより快適に、より安全にすることを目指しています。持続可能な開発と環境への配慮を重視し、社会に貢献する企業として成長し続けます。",
email: "hanye@hanye.cn", email: "hanye@hanye.cn",
tel: "86)024-8399-0696",
fax: "86)024-8399-0696",
businessHours: "平日 9:00-18:00 / 土日祝日休み",
businessActivities: [
"闪存的开发、生产和销售",
"开发、制造和销售SSD产品",
"其相关业务"
]
tel: "86)024-8399-0696",
fax: "86)024-8399-0696",
}); });


// SEO优化 // SEO优化
useHead({ useHead({
title: "会社概要 - Hanye",
title: t("about.title"),
meta: [ meta: [
{ {
name: "description", name: "description",
content: "Hanyeの会社概要、経営理念、企業情報をご紹介します。",
content: t("about.description"),
},
{
name: "keywords",
content: t("about.keywords"),
}, },
], ],
}); });


/* 动画效果 */ /* 动画效果 */
@keyframes float { @keyframes float {
0%, 100% {
0%,
100% {
transform: translateY(0) scale(1); transform: translateY(0) scale(1);
opacity: 0.9; opacity: 0.9;
} }
} }


@keyframes pulse { @keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 1; opacity: 1;
filter: drop-shadow(0 0 2px rgba(59, 130, 246, 0.6)); filter: drop-shadow(0 0 2px rgba(59, 130, 246, 0.6));
} }

+ 40
- 56
pages/contact.vue 查看文件

<nuxt-link <nuxt-link
to="/" to="/"
class="justify-start text-white/60 text-base font-normal" class="justify-start text-white/60 text-base font-normal"
>ホーム</nuxt-link
>{{ $t("common.home") }}</nuxt-link
> >
<span class="text-white/60 text-base font-normal px-2"> / </span> <span class="text-white/60 text-base font-normal px-2"> / </span>
<nuxt-link to="/contact" class="text-white text-base font-normal"
>お問い合わせ</nuxt-link
>
<nuxt-link to="/contact" class="text-white text-base font-normal">{{
$t("contact.title")
}}</nuxt-link>
</div> </div>
</div> </div>
<div <div
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
> >
<div class="text-white text-3xl font-medium mb-6"> <div class="text-white text-3xl font-medium mb-6">
お問い合わせフォーム
{{ $t("contact.title") }}
</div> </div>
<form <form
@submit.prevent="handleSubmit" @submit.prevent="handleSubmit"
type="text" type="text"
id="name" id="name"
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500"
placeholder="お名前を入力してください"
:placeholder="$t('contact.name')"
required required
/> />
<label <label
for="name" for="name"
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400"
> >
お名前
{{ $t("contact.name") }}
</label> </label>
</div> </div>


type="email" type="email"
id="email" id="email"
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500"
placeholder="メールアドレスを入力してください"
:placeholder="$t('contact.email')"
required required
/> />
<label <label
for="email" for="email"
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400"
> >
メールアドレス
{{ $t("contact.email") }}
</label> </label>
</div> </div>


v-model="form.message" v-model="form.message"
id="message" id="message"
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent h-36 resize-none transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent h-36 resize-none transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500"
placeholder="お問い合わせ内容を入力してください"
:placeholder="$t('contact.message')"
required required
></textarea> ></textarea>
<label <label
for="message" for="message"
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400"
> >
お問い合わせ内容
{{ $t("contact.message") }}
</label> </label>
</div> </div>


? 'border-red-500 focus:border-red-500' ? 'border-red-500 focus:border-red-500'
: 'border-gray-600 focus:border-blue-500', : 'border-gray-600 focus:border-blue-500',
]" ]"
placeholder="验证码"
:placeholder="$t('contact.captcha')"
required required
autocomplete="off" autocomplete="off"
aria-describedby="captcha-error" aria-describedby="captcha-error"
: 'text-gray-400 peer-focus:text-blue-400', : 'text-gray-400 peer-focus:text-blue-400',
]" ]"
> >
验证码
{{ $t("contact.captcha") }}
</label> </label>
</div> </div>
<div <div
class="flex-shrink-0 cursor-pointer select-none rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:shadow-md active:scale-100" class="flex-shrink-0 cursor-pointer select-none rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:shadow-md active:scale-100"
v-html="captcha.captchaSvg.value" v-html="captcha.captchaSvg.value"
@click="captcha.generateCaptcha()" @click="captcha.generateCaptcha()"
title="刷新验证码"
:title="$t('contact.refreshCaptcha')"
style="line-height: 0" style="line-height: 0"
></div> ></div>
<button <button
type="button" type="button"
@click="captcha.generateCaptcha()" @click="captcha.generateCaptcha()"
class="flex-shrink-0 p-2 text-gray-500 hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out" class="flex-shrink-0 p-2 text-gray-500 hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out"
aria-label="刷新验证码"
title="刷新验证码"
:aria-label="$t('contact.refreshCaptcha')"
:title="$t('contact.refreshCaptcha')"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="bg-gradient-to-r from-blue-700 to-blue-400 text-white text-base font-normal py-4 px-8 rounded-lg transition-all duration-300 hover:scale-105 hover:shadow-lg" class="bg-gradient-to-r from-blue-700 to-blue-400 text-white text-base font-normal py-4 px-8 rounded-lg transition-all duration-300 hover:scale-105 hover:shadow-lg"
:disabled="isSubmitting" :disabled="isSubmitting"
> >
{{ isSubmitting ? "送信中..." : "送信する" }}
{{
isSubmitting
? $t("contact.submitting")
: $t("contact.submit")
}}
</button> </button>
</form> </form>
</div> </div>
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
> >
<div class="text-white text-3xl font-medium mb-6"> <div class="text-white text-3xl font-medium mb-6">
会社情報
{{ $t("about.companyInfo.name") }}
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal"> <div class="text-white/60 text-base font-normal">
会社名
{{ $t("about.overview.companyName") }}
</div> </div>
<div class="text-white text-base font-normal"> <div class="text-white text-base font-normal">
株式会社ハニエ
{{ $t("about.companyInfo.companyName") }}
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal"> <div class="text-white/60 text-base font-normal">
所在地
{{ $t("about.overview.location") }}
</div> </div>
<div class="text-white text-base font-normal"> <div class="text-white text-base font-normal">
〒123-4567 東京都渋谷区神宮前1-1-1
{{ $t("about.companyInfo.location") }}
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal"> <div class="text-white/60 text-base font-normal">
電話番号
{{ $t("about.overview.tel") }}
</div> </div>
<div class="text-white text-base font-normal"> <div class="text-white text-base font-normal">
03-1234-5678
86)024-8399-0696
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal"> <div class="text-white/60 text-base font-normal">
メールアドレス
{{ $t("about.overview.email") }}
</div> </div>
<div class="text-white text-base font-normal"> <div class="text-white text-base font-normal">
info@hanye.co.jp
hanye@hanye.cn
</div> </div>
</div> </div>
</div> </div>
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg"
> >
<div class="text-white text-3xl font-medium mb-6"> <div class="text-white text-3xl font-medium mb-6">
営業時間
{{ $t("about.overview.businessHours") }}
</div> </div>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal">
平日
</div>
<div class="text-white text-base font-normal">
9:00 - 18:00
</div>
</div>
<div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal">
土曜日
</div>
<div class="text-white text-base font-normal">
9:00 - 17:00
</div>
</div>
<div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal">
日曜日・祝日
</div>
<div class="text-white text-base font-normal">休業</div>
</div>
<div class="text-white text-base font-normal">
{{ $t("about.companyInfo.businessHours") }}
</div> </div>
</div> </div>
</div> </div>
*/ */
import { useErrorHandler } from "~/composables/useErrorHandler"; import { useErrorHandler } from "~/composables/useErrorHandler";
import { useCaptcha } from "~/composables/useCaptcha"; import { useCaptcha } from "~/composables/useCaptcha";
const { t, locale } = useI18n();


const { error, isLoading, wrapAsync } = useErrorHandler(); const { error, isLoading, wrapAsync } = useErrorHandler();
const captcha = useCaptcha(); const captcha = useCaptcha();


// SEO优化 // SEO优化
useHead({ useHead({
title: "联系我们 - Hanye",
title: t("contact.title") + " - Hanye",
meta: [ meta: [
{ {
name: "description", name: "description",
content: "联系我们,获取更多产品信息和支持。",
content: t("contact.description"),
},
{
name: "keywords",
content: t("contact.keywords"),
}, },
], ],
}); });
</script> </script>

<style scoped>
/* 移除之前的 focus 样式,因为现在使用 border-b 样式 */
</style>

+ 16
- 8
pages/faq.vue 查看文件

<nuxt-link <nuxt-link
to="/" to="/"
class="justify-start text-white/60 text-base font-normal" class="justify-start text-white/60 text-base font-normal"
>ホーム</nuxt-link
>{{ $t("common.home") }}</nuxt-link
> >
<span class="text-white/60 text-base font-normal px-2"> / </span> <span class="text-white/60 text-base font-normal px-2"> / </span>
<nuxt-link to="/faq" class="text-white text-base font-normal" <nuxt-link to="/faq" class="text-white text-base font-normal"
>よくある質問</nuxt-link
>{{ $t("faq.title") }}</nuxt-link
> >
</div> </div>
</div> </div>
<!-- 左侧分类导航 --> <!-- 左侧分类导航 -->
<div class="col-span-1 md:col-span-2"> <div class="col-span-1 md:col-span-2">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="text-white text-3xl font-medium">カテゴリー</div>
<div class="text-white text-3xl font-medium">
{{ $t("faq.category") }}
</div>
<div class="flex flex-col gap-4 w-fit"> <div class="flex flex-col gap-4 w-fit">
<div <div
v-for="category in categories" v-for="category in categories"
v-model="searchTerm" v-model="searchTerm"
ref="searchInputRef" ref="searchInputRef"
type="search" type="search"
placeholder="キーワードで検索..."
:placeholder="$t('faq.searchPlaceholder')"
class="block w-full appearance-none rounded-lg border border-gray-600 bg-zinc-800/70 px-4 py-3 text-base text-gray-100 placeholder-gray-400 shadow-inner transition duration-200 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:ring-offset-2 focus:ring-offset-zinc-900" class="block w-full appearance-none rounded-lg border border-gray-600 bg-zinc-800/70 px-4 py-3 text-base text-gray-100 placeholder-gray-400 shadow-inner transition duration-200 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:ring-offset-2 focus:ring-offset-zinc-900"
/> />
<button <button
v-if="searchTerm" v-if="searchTerm"
@click="clearSearch" @click="clearSearch"
class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-blue-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-blue-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
aria-label="検索キーワードをクリア"
:aria-label="$t('faq.clearSearch')"
tabindex="0" tabindex="0"
type="button" type="button"
> >
v-if="filteredFaqs.length === 0" v-if="filteredFaqs.length === 0"
class="text-center text-gray-400 py-8" class="text-center text-gray-400 py-8"
> >
該当する質問が見つかりません。
{{ $t("faq.noResults") }}
</div> </div>
</div> </div>
</div> </div>


const { error, isLoading, wrapAsync } = useErrorHandler(); const { error, isLoading, wrapAsync } = useErrorHandler();


const { t, locale } = useI18n();

// FAQ数据 // FAQ数据
interface FAQ { interface FAQ {
id: number; id: number;


// SEO优化 // SEO优化
useHead({ useHead({
title: "常见问题 - Hanye",
title: t("faq.title") + " - Hanye",
meta: [ meta: [
{ {
name: "description", name: "description",
content: "浏览常见问题,获取产品使用和购买相关的帮助。",
content: t("faq.description"),
},
{
name: "keywords",
content: t("faq.keywords"),
}, },
], ],
}); });

+ 110
- 50
pages/index.vue 查看文件

:space-between="30" :space-between="30"
:loop="true" :loop="true"
:pagination="{ el: '.swiper-pagination-1', clickable: true }" :pagination="{ el: '.swiper-pagination-1', clickable: true }"
:autoplay="{
delay: 5000,
:autoplay="{
delay: 5000,
disableOnInteraction: false, disableOnInteraction: false,
pauseOnMouseEnter: true, pauseOnMouseEnter: true,
waitForTransition: true
waitForTransition: true,
}" }"
effect="creative" effect="creative"
:creativeEffect="{ :creativeEffect="{
:parallax="true" :parallax="true"
class="h-[320px] sm:h-[320px] md:h-[768px] lg:h-[900px] swiper-container-1" class="h-[320px] sm:h-[320px] md:h-[768px] lg:h-[900px] swiper-container-1"
> >
<SwiperSlide v-for="item in carouselList" :key="item.id">
<nuxt-link :to="item.link">
<div
class="max-w-screen-2xl mx-auto h-full"
:style="{
backgroundImage: `url(${item.image})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}"
></div>
</nuxt-link>
<SwiperSlide>
<div
class="max-w-screen-2xl mx-auto h-full relative"
:style="{
backgroundImage: `url(${homeA1Webp})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}"
>
<div class="w-full h-full flex-col justify-center hidden md:flex relative z-10">
<div
class="rounded border border-white w-11 h-6 leading-none justify-center flex items-center text-white text-sm font-normal"
>
SSD
</div>
<div class="justify-center">
<span class="text-white text-6xl font-normal leading-[78px]">{{
$t("home.carousel.one.title")
}}</span>
<span class="text-white text-6xl font-normal leading-[78px]">{{
$t("home.carousel.one.description")
}}</span>
<br />
<span class="text-white text-6xl font-normal leading-[78px]">{{
$t("home.carousel.one.description2")
}}</span
><br />
<span
class="text-cyan-400 text-6xl font-normal leading-[78px]"
>{{ $t("home.carousel.one.description3") }}</span
>
</div>
<div class="flex flex-col gap-2 mt-4">
<div
class="flex items-center gap-2 text-stone-50 text-xl font-normal"
>
<div class="w-2 h-2 bg-white rounded-full"></div>
{{ $t("home.carousel.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") }}
</div>
</div>
<div
class="w-36 h-14 mt-12 flex items-center justify-center bg-[#35F1FF] rounded-lg hover:bg-[#35F1FF]/80 transition-colors duration-300"
>
<nuxt-link
to="/products/1"
class="w-full h-full !flex items-center justify-center text-zinc-900"
>
{{ $t("products.view_details") }}
</nuxt-link>
</div>
</div>
</div>
</SwiperSlide> </SwiperSlide>
<div class="max-w-screen-2xl mx-auto relative"> <div class="max-w-screen-2xl mx-auto relative">
<div <div
:key="usage.id" :key="usage.id"
class="cursor-pointer select-none px-4 sm:px-7 py-2 sm:py-3 rounded-full border border-zinc-700 text-white transition-all duration-300 relative group" class="cursor-pointer select-none px-4 sm:px-7 py-2 sm:py-3 rounded-full border border-zinc-700 text-white transition-all duration-300 relative group"
:class="{ :class="{
'bg-cyan-400/10 border-cyan-400 text-cyan-400': activeIndex === index,
'hover:border-zinc-600': activeIndex !== index
'bg-cyan-400/10 border-cyan-400 text-cyan-400':
activeIndex === index,
'hover:border-zinc-600': activeIndex !== index,
}" }"
@click="handleUsageClick(usage.id)" @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">
<div
class="text-center text-xs sm:text-sm font-normal leading-tight md:text-base transition-colors duration-300 relative z-10"
>
{{ usage.name }} {{ usage.name }}
</div> </div>
<div class="absolute inset-0 rounded-full bg-cyan-400/20 scale-0 transition-transform duration-300 group-hover:scale-100"></div>
<div
class="absolute inset-0 rounded-full bg-cyan-400/20 scale-0 transition-transform duration-300 group-hover:scale-100"
></div>
</div> </div>
<div class="flex items-center justify-center gap-4 ml-auto"> <div class="flex items-center justify-center gap-4 ml-auto">
<div <div
class="swiper-button-prev-2 bg-zinc-700 w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200" class="swiper-button-prev-2 bg-zinc-700 w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200"
> >
<i class="icon-arrow-left text-zinc-300 text-xs sm:text-sm font-normal"></i>
<i
class="icon-arrow-left text-zinc-300 text-xs sm:text-sm font-normal"
></i>
</div> </div>
<div <div
class="swiper-button-next-2 bg-zinc-700 w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200" class="swiper-button-next-2 bg-zinc-700 w-8 h-8 sm:w-10 sm:h-10 rounded-full flex items-center justify-center cursor-pointer hover:bg-zinc-600 transition-colors duration-200"
> >
<i class="icon-arrow-right text-zinc-300 text-xs sm:text-sm font-normal"></i>
<i
class="icon-arrow-right text-zinc-300 text-xs sm:text-sm font-normal"
></i>
</div> </div>
</div> </div>
</div> </div>
class="w-full sm:w-1/2 md:w-1/3 lg:w-1/4" class="w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
> >
<div class="w-full h-full p-2"> <div class="w-full h-full p-2">
<nuxt-link
:to="product.link"
class="block w-full h-full"
>
<nuxt-link :to="product.link" class="block w-full h-full">
<div <div
class="w-full h-full bg-zinc-900 rounded-2xl p-4 flex flex-col items-center justify-start relative overflow-hidden group hover:bg-zinc-800 transition-all duration-300 hover:shadow-lg hover:shadow-cyan-400/10" class="w-full h-full bg-zinc-900 rounded-2xl p-4 flex flex-col items-center justify-start relative overflow-hidden group hover:bg-zinc-800 transition-all duration-300 hover:shadow-lg hover:shadow-cyan-400/10"
> >
<div <div
class="px-4 py-2 bg-cyan-400/20 backdrop-blur-sm rounded-full text-white text-sm font-medium border border-cyan-400/30 transform transition-transform duration-300 group-hover:scale-105" class="px-4 py-2 bg-cyan-400/20 backdrop-blur-sm rounded-full text-white text-sm font-medium border border-cyan-400/30 transform transition-transform duration-300 group-hover:scale-105"
> >
查看详情
{{ $t("products.view_details") }}
</div> </div>
</div> </div>
</div> </div>
<div <div
class="w-full mt-4 min-h-[80px] transition-all duration-300 transform" class="w-full mt-4 min-h-[80px] transition-all duration-300 transform"
:class="{ :class="{
'opacity-0 translate-y-4': !isImageLoaded[product.id],
'opacity-0 translate-y-4':
!isImageLoaded[product.id],
'opacity-100 translate-y-0': 'opacity-100 translate-y-0':
isImageLoaded[product.id], isImageLoaded[product.id],
}" }"
class="bg-zinc-950/10 backdrop-blur-[50px] border border-white/10 rounded-lg flex gap-8 p-4 sm:p-8 justify-between category-item group hover:border-cyan-400/30 transition-all duration-300" class="bg-zinc-950/10 backdrop-blur-[50px] border border-white/10 rounded-lg flex gap-8 p-4 sm:p-8 justify-between category-item group hover:border-cyan-400/30 transition-all duration-300"
> >
<div class="col-span-1 flex flex-col gap-4"> <div class="col-span-1 flex flex-col gap-4">
<div class="flex flex-col gap-2 opacity-80 group-hover:opacity-100 transition-opacity duration-300">
<div
class="flex flex-col gap-2 opacity-80 group-hover:opacity-100 transition-opacity duration-300"
>
<div <div
v-for="feature in category.features" v-for="feature in category.features"
:key="feature" :key="feature"
class="text-white text-sm md:text-base font-normal leading-tight flex gap-2 items-center group-hover:text-cyan-400 transition-colors duration-300" class="text-white text-sm md:text-base font-normal leading-tight flex gap-2 items-center group-hover:text-cyan-400 transition-colors duration-300"
> >
<i class="icon-star text-sm group-hover:scale-110 transition-transform duration-300"></i>
<i
class="icon-star text-sm group-hover:scale-110 transition-transform duration-300"
></i>
<span>{{ feature }}</span> <span>{{ feature }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="w-32 h-32 md:w-44 md:h-44 relative overflow-hidden rounded-lg">
<div
class="w-32 h-32 md:w-44 md:h-44 relative overflow-hidden rounded-lg"
>
<img <img
:src="category.image" :src="category.image"
:alt="category.title" :alt="category.title"
class="w-full h-full object-contain transition-transform duration-500 group-hover:scale-110" class="w-full h-full object-contain transition-transform duration-500 group-hover:scale-110"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div
class="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
></div>
</div> </div>
</nuxt-link> </nuxt-link>
</div> </div>
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import video from "@/assets/videos/video.mp4"; import video from "@/assets/videos/video.mp4";
import videoWebp from "@/assets/videos/video.webp"; import videoWebp from "@/assets/videos/video.webp";
import homeA1Webp from "@/assets/images/home-a-1.webp";
import homeC1Webp from "@/assets/images/home-c-1.webp"; import homeC1Webp from "@/assets/images/home-c-1.webp";
import product from "@/assets/images/product.png";


const { t, locale } = useI18n(); const { t, locale } = useI18n();
const config = useRuntimeConfig(); const config = useRuntimeConfig();
); );
const categoryList = ref(categoryData.value?.data || []); const categoryList = ref(categoryData.value?.data || []);


// 调试信息
console.log("Carousel Data:", carouselData.value);
console.log("Carousel Error:", carouselError.value);
console.log("Usage Data:", usageData.value);
console.log("Usage Error:", usageError.value);
console.log("Category Data:", categoryData.value);
console.log("Category Error:", categoryError.value);

interface Product { interface Product {
id: number; id: number;
title: string; title: string;


// 计算当前激活的索引 // 计算当前激活的索引
const activeIndex = computed(() => { const activeIndex = computed(() => {
return typedUsageList.value.findIndex(item => item.id === activeUsageId.value);
return typedUsageList.value.findIndex(
(item) => item.id === activeUsageId.value
);
}); });


const activeProducts = computed(() => { const activeProducts = computed(() => {


// SEO优化 // SEO优化
useHead({ useHead({
title: "Hanye - 首页",
title: t("home.title") + " - Hanye",
meta: [ meta: [
{ {
name: "description", name: "description",
content: "基于 Nuxt3 的静态网站脚手架,支持多语言(中文、英文、日文)。",
content: t("home.description"),
},
{
name: "keywords",
content: t("home.keywords"),
}, },
], ],
}); });
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
opacity: 1; opacity: 1;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
&::before { &::before {
opacity: 1; opacity: 1;
} }
} }
&::before { &::before {
content: '';
content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(45deg, rgba(6, 182, 212, 0.1), transparent); background: linear-gradient(45deg, rgba(6, 182, 212, 0.1), transparent);
// 添加轮播图遮罩效果 // 添加轮播图遮罩效果
.swiper-slide { .swiper-slide {
&::before { &::before {
content: '';
content: "";
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
text-decoration: none; text-decoration: none;
display: block; display: block;
height: 100%; height: 100%;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
} }
z-index: 1; z-index: 1;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover { &:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);

+ 38
- 16
pages/products/index.vue 查看文件

<div <div
class="justify-start text-white text-2xl font-normal md:text-4xl lg:text-6xl" class="justify-start text-white text-2xl font-normal md:text-4xl lg:text-6xl"
> >
製品一覧
{{ $t("products.product_list") }}
</div> </div>
<div <div
class="text-white text-sm lg:text-lg font-normal leading-loose" class="text-white text-sm lg:text-lg font-normal leading-loose"
> >
卓越した製品は、実績に裏打ちされた優れた技術と、<br />継続的な革新デザインとの融合により、生み出されます。
{{ $t("products.product_list_description") }}
</div> </div>
</div> </div>
</div> </div>
<nuxt-link <nuxt-link
to="/" to="/"
class="justify-start text-white/60 text-base font-normal" class="justify-start text-white/60 text-base font-normal"
>ホーム</nuxt-link
>{{ $t("common.home") }}</nuxt-link
> >
<span class="text-white/60 text-base font-normal px-2"> / </span> <span class="text-white/60 text-base font-normal px-2"> / </span>
<nuxt-link to="/products" class="text-white text-base font-normal"
>製品一覧</nuxt-link
<nuxt-link
to="/products"
class="text-white text-base font-normal"
>{{ $t("products.product_list") }}</nuxt-link
> >
</div> </div>
</div> </div>
class="col-span-1 md:col-span-2 flex flex-col gap-16 mb-8 md:mb-0" class="col-span-1 md:col-span-2 flex flex-col gap-16 mb-8 md:mb-0"
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="text-white text-3xl font-medium">製品カテゴリー</div>
<div class="text-white text-3xl font-medium">
{{ $t("products.product_categories_title") }}
</div>
<div class="flex flex-col gap-4 w-fit"> <div class="flex flex-col gap-4 w-fit">
<div <div
v-for="category in categories" v-for="category in categories"
@click="handleCategoryFilter(category)" @click="handleCategoryFilter(category)"
class="opacity-80 justify-start text-white text-base font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-4 py-2 rounded-lg inline-block" class="opacity-80 justify-start text-white text-base font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-4 py-2 rounded-lg inline-block"
:class="{ :class="{
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300': selectedCategory === category,
'hover:bg-zinc-800/50': selectedCategory !== category
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300':
selectedCategory === category,
'hover:bg-zinc-800/50': selectedCategory !== category,
}" }"
> >
{{ category }} {{ category }}
</div> </div>


<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="text-white text-3xl font-medium">用途分類</div>
<div class="text-white text-3xl font-medium">
{{ $t("products.product_categories_usage") }}
</div>
<div class="flex flex-col gap-4 w-fit"> <div class="flex flex-col gap-4 w-fit">
<div <div
v-for="usage in usages" v-for="usage in usages"
@click="handleUsageFilter(usage)" @click="handleUsageFilter(usage)"
class="opacity-80 justify-start text-white text-base font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-4 py-2 rounded-lg inline-block" class="opacity-80 justify-start text-white text-base font-normal cursor-pointer hover:opacity-100 transition-all duration-300 px-4 py-2 rounded-lg inline-block"
:class="{ :class="{
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300': selectedUsage === usage,
'hover:bg-zinc-800/50': selectedUsage !== usage
'opacity-100 font-bold bg-gradient-to-r from-blue-700 to-blue-400 text-white border-0 shadow-lg scale-105 transition-all duration-300':
selectedUsage === usage,
'hover:bg-zinc-800/50': selectedUsage !== usage,
}" }"
> >
{{ usage }} {{ usage }}
<div class="col-span-1 md:col-span-8"> <div class="col-span-1 md:col-span-8">
<div class="flex flex-col gap-16"> <div class="flex flex-col gap-16">
<template v-for="category in categories" :key="category"> <template v-for="category in categories" :key="category">
<div v-if="filteredProducts.filter(p => p.category === category).length > 0" class="flex flex-col gap-4">
<div
v-if="
filteredProducts.filter((p) => p.category === category)
.length > 0
"
class="flex flex-col gap-4"
>
<div class="w-full text-white text-4xl font-normal mb-4"> <div class="w-full text-white text-4xl font-normal mb-4">
{{ category }} {{ category }}
</div> </div>
<transition-group name="fade" tag="div" class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<transition-group
name="fade"
tag="div"
class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
>
<nuxt-link <nuxt-link
v-for="product in filteredProducts.filter(p => p.category === category)"
v-for="product in filteredProducts.filter(
(p) => p.category === category
)"
:key="product.id" :key="product.id"
:to="`/products/${product.id}`" :to="`/products/${product.id}`"
class="bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-blue-400" class="bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-blue-400"
</script> </script>


<style scoped> <style scoped>
.fade-enter-active, .fade-leave-active {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s; transition: opacity 0.3s;
} }
.fade-enter-from, .fade-leave-to {
.fade-enter-from,
.fade-leave-to {
opacity: 0; opacity: 0;
} }
</style> </style>

+ 238
- 0
scripts/localize-images.mjs 查看文件

#!/usr/bin/env node

/**
* 图片本地化处理脚本
* 在静态站点生成后运行,将API返回的图片下载到本地
*/

import fs from 'fs'
import path from 'path'
import { createHash } from 'crypto'
import { fileURLToPath } from 'url'

// 获取当前文件的目录
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const PROJECT_ROOT = path.resolve(__dirname, '..')

/**
* 下载并本地化图片
* @param imageUrl 原始图片URL
* @param basePath 保存图片的基础路径
* @returns 本地化后的图片路径
*/
async function localizeImage(imageUrl, basePath = 'public/images/remote') {
// 检查URL是否有效
if (!imageUrl || !imageUrl.startsWith('http')) {
return imageUrl
}

try {
// 创建保存目录
const fullBasePath = path.resolve(PROJECT_ROOT, basePath)
if (!fs.existsSync(fullBasePath)) {
fs.mkdirSync(fullBasePath, { recursive: true })
}

// 生成唯一文件名(使用URL的MD5哈希)
const urlHash = createHash('md5').update(imageUrl).digest('hex')
const fileExt = path.extname(new URL(imageUrl).pathname) || '.jpg'
const fileName = `${urlHash}${fileExt}`
const filePath = path.join(fullBasePath, fileName)
// 如果文件已存在,直接返回路径
if (fs.existsSync(filePath)) {
return `/images/remote/${fileName}`
}
// 下载图片
const response = await fetch(imageUrl)
if (!response.ok) {
throw new Error(`下载图片失败: ${response.status} ${response.statusText}`)
}
const buffer = Buffer.from(await response.arrayBuffer())
fs.writeFileSync(filePath, buffer)
console.log(`✅ 下载图片成功: ${imageUrl} -> /images/remote/${fileName}`)
// 返回本地URL(相对于public目录)
return `/images/remote/${fileName}`
} catch (error) {
console.error(`❌ 本地化图片失败 ${imageUrl}:`, error)
return imageUrl // 失败时返回原始URL
}
}

/**
* 递归处理对象中的所有图片URL
* @param data 需要处理的数据对象或数组
* @param imageFields 指定哪些字段包含图片URL
* @returns 处理后的数据
*/
async function localizeImages(
data,
imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
) {
if (!data) return data
// 处理数组
if (Array.isArray(data)) {
return Promise.all(data.map(item => localizeImages(item, imageFields)))
}
// 处理对象
if (typeof data === 'object') {
const result = { ...data }
// 处理所有键
for (const [key, value] of Object.entries(result)) {
// 如果是图片字段且值是字符串,本地化图片
if (imageFields.includes(key) && typeof value === 'string') {
result[key] = await localizeImage(value)
}
// 递归处理嵌套对象或数组
else if (typeof value === 'object' && value !== null) {
result[key] = await localizeImages(value, imageFields)
}
}
return result
}
return data
}

/**
* 从JSON文件提取图片URL并下载到本地
* @param jsonFilePath JSON文件路径
* @param imageFields 包含图片URL的字段名称数组
*/
async function extractAndDownloadImages(
jsonFilePath,
imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
) {
// 检查文件是否存在
if (!fs.existsSync(jsonFilePath)) {
console.error(`文件不存在: ${jsonFilePath}`)
return
}

try {
// 读取JSON文件
const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'))
// 递归查找所有图片URL
const imageUrls = new Set()
// 递归函数查找所有图片URL
function findImageUrls(obj) {
if (!obj) return
if (Array.isArray(obj)) {
obj.forEach(item => findImageUrls(item))
return
}
if (typeof obj === 'object') {
for (const [key, value] of Object.entries(obj)) {
if (imageFields.includes(key) && typeof value === 'string' && value.startsWith('http')) {
imageUrls.add(value)
} else if (typeof value === 'object' && value !== null) {
findImageUrls(value)
}
}
}
}
findImageUrls(jsonData)
// 下载所有图片
console.log(`📋 在 ${jsonFilePath} 中找到 ${imageUrls.size} 个图片URL`)
let downloadedCount = 0
for (const url of imageUrls) {
try {
const localUrl = await localizeImage(url)
if (localUrl !== url) {
downloadedCount++
}
} catch (error) {
console.error(`❌ 下载失败 ${url}:`, error)
}
}
console.log(`📊 成功下载 ${downloadedCount}/${imageUrls.size} 个图片`)
// 将本地化的图片URL写回到JSON文件
try {
const localizedData = await localizeImages(jsonData)
fs.writeFileSync(jsonFilePath, JSON.stringify(localizedData, null, 2))
console.log(`💾 已更新图片URL至本地路径: ${jsonFilePath}`)
} catch (error) {
console.error(`❌ 无法更新JSON文件 ${jsonFilePath}:`, error)
}
} catch (error) {
console.error(`❌ 处理文件 ${jsonFilePath} 时出错:`, error)
}
}

/**
* 处理API响应文件中的图片
* @param outputDir 输出目录路径(通常是.output目录)
*/
async function processApiResponseImages(outputDir = '.output') {
console.log('🚀 开始处理API响应文件中的图片...')
// 检查输出目录是否存在
if (!fs.existsSync(outputDir)) {
console.error(`❌ 输出目录不存在: ${outputDir}`)
return
}
const serverDir = path.join(outputDir, 'server/api')
if (!fs.existsSync(serverDir)) {
console.error(`❌ 服务器API目录不存在: ${serverDir}`)
return
}
console.log(`🔍 扫描API响应文件: ${serverDir}`)
// 递归函数查找所有JSON文件
async function processDirectory(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
await processDirectory(fullPath)
} else if (entry.name.endsWith('.json')) {
console.log(`📄 处理JSON文件: ${fullPath}`)
await extractAndDownloadImages(fullPath)
}
}
}
await processDirectory(serverDir)
console.log('✅ 所有API响应文件处理完成')
}

// 确保脚本能单独运行
async function main() {
console.log('===== 🖼️ 开始图片本地化处理 =====')
try {
// 默认的输出目录是.output
const outputDir = process.argv[2] || '.output'
await processApiResponseImages(outputDir)
console.log('===== ✅ 图片本地化处理完成 =====')
process.exit(0)
} catch (error) {
console.error('❌ 图片本地化处理失败:', error)
process.exit(1)
}
}

main()

+ 36
- 19
server/api/home.ts 查看文件

/** /**
* 首页轮播图数据接口 * 首页轮播图数据接口
* @returns 轮播图数据列表 * @returns 轮播图数据列表
*
*
* 替换真实接口说明: * 替换真实接口说明:
* 1. 替换真实接口时,需要修改以下内容: * 1. 替换真实接口时,需要修改以下内容:
* - 将模拟数据替换为真实接口调用 * - 将模拟数据替换为真实接口调用
* - 添加错误处理 * - 添加错误处理
* - 添加接口参数处理 * - 添加接口参数处理
* - 添加数据转换逻辑 * - 添加数据转换逻辑
*
*
* 2. 真实接口示例: * 2. 真实接口示例:
* const response = await $fetch('https://api.example.com/carousel', { * const response = await $fetch('https://api.example.com/carousel', {
* method: 'GET', * method: 'GET',
* limit: 10 * limit: 10
* } * }
* }) * })
*
*
* 3. 错误处理示例: * 3. 错误处理示例:
* try { * try {
* const response = await $fetch('...') * const response = await $fetch('...')
* message: '获取轮播图数据失败' * message: '获取轮播图数据失败'
* } * }
* } * }
*
*
* 4. 数据转换示例: * 4. 数据转换示例:
* const transformedData = response.data.map(item => ({ * const transformedData = response.data.map(item => ({
* id: item.id, * id: item.id,
* image: item.imageUrl, * image: item.imageUrl,
* link: `/products/${item.productId}` * link: `/products/${item.productId}`
* })) * }))
*
*
* 5. 接口参数处理示例: * 5. 接口参数处理示例:
* const query = getQuery(event) * const query = getQuery(event)
* const page = Number(query.page) || 1 * const page = Number(query.page) || 1
const carouselList = [ const carouselList = [
{ {
id: 1, id: 1,
title: '轮播图1',
image: 'https://picsum.photos/1920/1080?random=1',
link: '/products/1'
image: "",
description: `
<div class="rounded border border-white w-11 h-6 leading-none justify-center flex items-center text-white text-sm font-normal">SSD</div>
<div class="justify-center">
<span class="text-white text-6xl font-normal leading-[78px]">新技術</span>
<span class="text-white text-6xl font-normal leading-[78px]">3D NAND TLC</span>
<br />
<span class="text-white text-6xl font-normal leading-[78px]">フラッシュ採用、 </span><br />
<span class="text-cyan-400 text-6xl font-normal leading-[78px]">信頼性が高く耐久性に優<br />れている</span>
</div>
<div class="flex flex-col gap-2 mt-4">
<div class="flex items-center gap-2 text-stone-50 text-xl font-normal">
<div class="w-2 h-2 bg-white rounded-full"></div>
ストレスのないゲーム体験をお楽しみください 
</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>
パソコンの起動時間が劇的に速くなった!
</div>
</div>
`,
link: "/products/1",
}, },
{ {
id: 2, id: 2,
title: '轮播图2',
image: 'https://picsum.photos/1920/1080?random=2',
link: '/products/2'
image: "",
link: "/products/2",
}, },
{ {
id: 3, id: 3,
title: '轮播图3',
image: 'https://picsum.photos/1920/1080?random=3',
link: '/products/3'
}
]
image: "",
link: "/products/3",
},
];


return { return {
code: 200, code: 200,
data: carouselList, data: carouselList,
message: '获取轮播图数据成功'
}
})
message: "获取轮播图数据成功",
};
});

+ 1
- 0
server/api/products/[id].get.ts 查看文件

"https://picsum.photos/400/400?random=12", "https://picsum.photos/400/400?random=12",
"https://picsum.photos/400/400?random=13", "https://picsum.photos/400/400?random=13",
], ],
summary:'摘要',
description: "高性能2.5インチSSD、読み書き速度が速く、信頼性が高い。最新のNANDフラッシュ技術を採用し、高速なデータ転送と安定した性能を実現。PCの起動時間を大幅に短縮し、アプリケーションの読み込みを高速化。耐久性に優れ、長時間の使用にも耐えられる設計。", description: "高性能2.5インチSSD、読み書き速度が速く、信頼性が高い。最新のNANDフラッシュ技術を採用し、高速なデータ転送と安定した性能を実現。PCの起動時間を大幅に短縮し、アプリケーションの読み込みを高速化。耐久性に優れ、長時間の使用にも耐えられる設計。",
}, },
{ {

+ 24
- 20
server/api/products/category.get.ts 查看文件

export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
// 获取查询参数中的语言设置
const query = getQuery(event);
const lang = query.lang || 'ja'; // 默认使用日语
// 模拟数据 // 模拟数据
const mockData = { const mockData = {
code: 200, code: 200,
data: [ data: [
{ {
id: 1, id: 1,
title: "PC高速化",
description: "2.5-inch SSD & M.2 SSD",
title: lang === 'zh' ? "PC高速化" : "PC高速化",
description: lang === 'zh' ? "2.5寸SSD和M.2 SSD" : "2.5-inch SSD & M.2 SSD",
features: [ features: [
"PC高速化",
"起動・読込 高速"
lang === 'zh' ? "电脑加速" : "PC高速化",
lang === 'zh' ? "启动和加载速度快" : "起動・読込 高速"
], ],
image: "https://picsum.photos/seed/ssd/400/400", image: "https://picsum.photos/seed/ssd/400/400",
link: "/products" link: "/products"
}, },
{ {
id: 2, id: 2,
title: "データ保存",
description: "HDD & SSD",
title: lang === 'zh' ? "数据存储" : "データ保存",
description: lang === 'zh' ? "HDD和SSD" : "HDD & SSD",
features: [ features: [
"大容量保存",
"データバックアップ"
lang === 'zh' ? "大容量存储" : "大容量保存",
lang === 'zh' ? "数据备份" : "データバックアップ"
], ],
image: "https://picsum.photos/seed/hdd/400/400", image: "https://picsum.photos/seed/hdd/400/400",
link: "/products" link: "/products"
}, },
{ {
id: 3, id: 3,
title: "メモリ拡張",
description: "DDR4 & DDR5",
title: lang === 'zh' ? "内存扩展" : "メモリ拡張",
description: lang === 'zh' ? "DDR4和DDR5" : "DDR4 & DDR5",
features: [ features: [
"メモリ増設",
"パフォーマンス向上"
lang === 'zh' ? "内存升级" : "メモリ増設",
lang === 'zh' ? "性能提升" : "パフォーマンス向上"
], ],
image: "https://picsum.photos/seed/ram/400/400", image: "https://picsum.photos/seed/ram/400/400",
link: "/products" link: "/products"
}, },
{ {
id: 4, id: 4,
title: "周辺機器",
description: "USB & Thunderbolt",
title: lang === 'zh' ? "外围设备" : "周辺機器",
description: lang === 'zh' ? "USB和雷电接口" : "USB & Thunderbolt",
features: [ features: [
"高速転送",
"多機能接続"
lang === 'zh' ? "高速传输" : "高速転送",
lang === 'zh' ? "多功能连接" : "多機能接続"
], ],
image: "https://picsum.photos/seed/usb/400/400", image: "https://picsum.photos/seed/usb/400/400",
link: "/products" link: "/products"
}, },
{ {
id: 5, id: 5,
title: "冷却システム",
description: "CPU & GPU Cooler",
title: lang === 'zh' ? "散热系统" : "冷却システム",
description: lang === 'zh' ? "CPU和GPU散热器" : "CPU & GPU Cooler",
features: [ features: [
"効率的冷却",
"静音設計"
lang === 'zh' ? "高效散热" : "効率的冷却",
lang === 'zh' ? "静音设计" : "静音設計"
], ],
image: "https://picsum.photos/seed/cooler/400/400", image: "https://picsum.photos/seed/cooler/400/400",
link: "/products" link: "/products"

+ 9
- 5
server/api/products/usage.ts 查看文件

* 按用途产品展示接口 * 按用途产品展示接口
* @returns 按用途分类的产品数据 * @returns 按用途分类的产品数据
*/ */
export default defineEventHandler(async () => {
export default defineEventHandler(async (event) => {
// 获取查询参数中的语言设置
const query = getQuery(event);
const lang = query.lang || 'ja'; // 默认使用日语
// 模拟数据 // 模拟数据
const usageList = [ const usageList = [
{ {
id: 1, id: 1,
name: '外付けストレージ化',
name: lang === 'zh' ? '外接存储转换' : '外付けストレージ化',
products: [ products: [
{ {
id: 1, id: 1,
}, },
{ {
id: 2, id: 2,
name: 'PC高速化',
name: lang === 'zh' ? 'PC加速' : 'PC高速化',
products: [ products: [
{ {
id: 4, id: 4,
}, },
{ {
id: 3, id: 3,
name: 'データバックアップ',
name: lang === 'zh' ? '数据备份' : 'データバックアップ',
products: [ products: [
{ {
id: 7, id: 7,
return { return {
code: 200, code: 200,
data: usageList, data: usageList,
message: '获取按用途产品数据成功'
message: lang === 'zh' ? '获取按用途产品数据成功' : '用途別製品データの取得に成功しました'
} }
}) })

+ 169
- 0
utils/image-downloader.ts 查看文件

import fs from 'fs'
import path from 'path'
import { localizeImage } from './image-localizer'

/**
* 从JSON文件提取图片URL并下载到本地
* @param jsonFilePath JSON文件路径
* @param imageFields 包含图片URL的字段名称数组
*/
async function extractAndDownloadImages(
jsonFilePath: string,
imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
) {
// 检查文件是否存在
if (!fs.existsSync(jsonFilePath)) {
console.error(`文件不存在: ${jsonFilePath}`)
return
}

try {
// 读取JSON文件
const jsonData = JSON.parse(fs.readFileSync(jsonFilePath, 'utf-8'))
// 递归查找所有图片URL
const imageUrls = new Set<string>()
// 递归函数查找所有图片URL
function findImageUrls(obj: any) {
if (!obj) return
if (Array.isArray(obj)) {
obj.forEach(item => findImageUrls(item))
return
}
if (typeof obj === 'object') {
for (const [key, value] of Object.entries(obj)) {
if (imageFields.includes(key) && typeof value === 'string' && value.startsWith('http')) {
imageUrls.add(value)
} else if (typeof value === 'object' && value !== null) {
findImageUrls(value)
}
}
}
}
findImageUrls(jsonData)
// 下载所有图片
console.log(`在 ${jsonFilePath} 中找到 ${imageUrls.size} 个图片URL`)
let downloadedCount = 0
for (const url of imageUrls) {
try {
const localUrl = await localizeImage(url)
if (localUrl !== url) {
downloadedCount++
console.log(`已下载: ${url} -> ${localUrl}`)
}
} catch (error) {
console.error(`下载失败 ${url}:`, error)
}
}
console.log(`成功下载 ${downloadedCount}/${imageUrls.size} 个图片`)
// 将本地化的图片URL写回到JSON文件
try {
const localizedData = await localizeImages(jsonData)
fs.writeFileSync(jsonFilePath, JSON.stringify(localizedData, null, 2))
console.log(`已更新图片URL至本地路径: ${jsonFilePath}`)
} catch (error) {
console.error(`无法更新JSON文件 ${jsonFilePath}:`, error)
}
} catch (error) {
console.error(`处理文件 ${jsonFilePath} 时出错:`, error)
}
}

/**
* 递归处理对象中的所有图片URL
* 此函数是image-localizer.ts中同名函数的复制,以避免在命令行运行时的循环依赖问题
*/
async function localizeImages(
data: any,
imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
): Promise<any> {
if (!data) return data
// 处理数组
if (Array.isArray(data)) {
return Promise.all(data.map(item => localizeImages(item, imageFields)))
}
// 处理对象
if (typeof data === 'object') {
const result = { ...data }
// 处理所有键
for (const [key, value] of Object.entries(result)) {
// 如果是图片字段且值是字符串,本地化图片
if (imageFields.includes(key) && typeof value === 'string') {
result[key] = await localizeImage(value)
}
// 递归处理嵌套对象或数组
else if (typeof value === 'object' && value !== null) {
result[key] = await localizeImages(value, imageFields)
}
}
return result
}
return data
}

/**
* 处理API响应文件中的图片
* @param outputDir 输出目录路径(通常是.output目录)
*/
export async function processApiResponseImages(outputDir = '.output') {
console.log('开始处理API响应文件中的图片...')
// 检查输出目录是否存在
if (!fs.existsSync(outputDir)) {
console.error(`输出目录不存在: ${outputDir}`)
return
}
const serverDir = path.join(outputDir, 'server/api')
if (!fs.existsSync(serverDir)) {
console.error(`服务器API目录不存在: ${serverDir}`)
return
}
console.log(`扫描API响应文件: ${serverDir}`)
// 递归函数查找所有JSON文件
async function processDirectory(dir: string) {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
await processDirectory(fullPath)
} else if (entry.name.endsWith('.json')) {
console.log(`处理JSON文件: ${fullPath}`)
await extractAndDownloadImages(fullPath)
}
}
}
await processDirectory(serverDir)
console.log('所有API响应文件处理完成')
}

// 允许通过命令行直接运行
// 兼容 ESM 和 CommonJS
if (typeof require !== 'undefined' && require.main === module) {
const outputDir = process.argv[2] || '.output'
processApiResponseImages(outputDir)
.then(() => console.log('图片本地化处理完成'))
.catch(error => console.error('图片本地化处理失败:', error))
}

// 导出主函数,供脚本调用
export default processApiResponseImages

+ 89
- 0
utils/image-localizer.ts 查看文件

import fs from 'fs'
import path from 'path'
import { createHash } from 'crypto'

/**
* 下载并本地化图片
* @param imageUrl 原始图片URL
* @param basePath 保存图片的基础路径
* @returns 本地化后的图片路径
*/
export async function localizeImage(imageUrl: string, basePath = 'public/images/remote'): Promise<string> {
// 检查URL是否有效
if (!imageUrl || !imageUrl.startsWith('http')) {
return imageUrl
}

try {
// 创建保存目录
const fullBasePath = path.resolve(process.cwd(), basePath)
if (!fs.existsSync(fullBasePath)) {
fs.mkdirSync(fullBasePath, { recursive: true })
}

// 生成唯一文件名(使用URL的MD5哈希)
const urlHash = createHash('md5').update(imageUrl).digest('hex')
const fileExt = path.extname(new URL(imageUrl).pathname) || '.jpg'
const fileName = `${urlHash}${fileExt}`
const filePath = path.join(fullBasePath, fileName)
// 如果文件已存在,直接返回路径
if (fs.existsSync(filePath)) {
return `/images/remote/${fileName}`
}
// 下载图片
const response = await fetch(imageUrl)
if (!response.ok) {
throw new Error(`下载图片失败: ${response.status} ${response.statusText}`)
}
const buffer = Buffer.from(await response.arrayBuffer())
fs.writeFileSync(filePath, buffer)
// 返回本地URL(相对于public目录)
return `/images/remote/${fileName}`
} catch (error) {
console.error(`本地化图片失败 ${imageUrl}:`, error)
return imageUrl // 失败时返回原始URL
}
}

/**
* 递归处理对象中的所有图片URL
* @param data 需要处理的数据对象或数组
* @param imageFields 指定哪些字段包含图片URL
* @returns 处理后的数据
*/
export async function localizeImages(
data: any,
imageFields = ['image', 'imageUrl', 'thumbnail', 'cover', 'avatar', 'photo', 'src']
): Promise<any> {
if (!data) return data
// 处理数组
if (Array.isArray(data)) {
return Promise.all(data.map(item => localizeImages(item, imageFields)))
}
// 处理对象
if (typeof data === 'object') {
const result = { ...data }
// 处理所有键
for (const [key, value] of Object.entries(result)) {
// 如果是图片字段且值是字符串,本地化图片
if (imageFields.includes(key) && typeof value === 'string') {
result[key] = await localizeImage(value)
}
// 递归处理嵌套对象或数组
else if (typeof value === 'object' && value !== null) {
result[key] = await localizeImages(value, imageFields)
}
}
return result
}
return data
}

正在加载...
取消
保存