ソースを参照

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

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

+ 86
- 0
README-image-localization.md ファイルの表示

@@ -0,0 +1,86 @@
# 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 ファイルの表示

@@ -35,14 +35,16 @@ body {
text-rendering: optimizeLegibility;
font-feature-settings: "palt";
letter-spacing: 0.02em;
font-weight: 400;
font-style: normal;
}


/* 仅在日语环境下应用日文字体 */
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 {
font-family: 'Montserrat', sans-serif !important;
font-family: 'Montserrat', 'Noto Sans JP', sans-serif !important;
}

+ 76
- 16
components/TheFooter.vue ファイルの表示

@@ -19,15 +19,15 @@
</p>
</div>

<!-- 快捷链接 -->
<!-- 产品分类链接 -->
<div class="hidden lg:block">
<h3
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>
<ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<li v-for="item in menuProductsItems" :key="item.path">
<NuxtLink
:to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition"
@@ -37,15 +37,15 @@
</li>
</ul>
</div>
<!-- 快捷链接 -->
<!-- 网站快捷链接 -->
<div class="hidden lg:block">
<h3
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>
<ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<li v-for="item in menuWebsiteItems" :key="item.path">
<NuxtLink
:to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition"
@@ -55,15 +55,15 @@
</li>
</ul>
</div>
<!-- 快捷链接 -->
<!-- 网站快捷链接 -->
<div class="hidden lg:block">
<h3
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>
<ul class="space-y-4">
<li v-for="item in menuItems" :key="item.path">
<li v-for="item in menuHomeItems" :key="item.path">
<NuxtLink
:to="item.path"
class="text-zinc-500 text-sm font-normal hover:text-white transition"
@@ -83,12 +83,72 @@
* 页脚组件
* 包含网站导航、联系信息和版权信息
*/
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>

+ 32
- 16
components/TheHeader.vue ファイルの表示

@@ -307,6 +307,30 @@ let leaveTimeout: ReturnType<typeof setTimeout> | null = null; // Timeout for mo
// 添加热门关键字
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,根据是否为默认语言调整路径
const homePath = computed(() => {
// 如果是默认语言,路径为根路径 '/'
@@ -330,25 +354,17 @@ const menuItems = computed(() => {
children: [
{
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",
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 ファイルの表示

@@ -11,14 +11,52 @@ export default {
hotKeywords: "Hot Keywords",
productCategories: "Product Categories",
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: {
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",
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: {
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",
consultation:
"Feel free to inquire about product consultations and quotes.",
@@ -36,98 +74,70 @@ export default {
"Diversifying products and relentlessly pursuing new creativity.",
strong_point: "Our Strengths",
strong_point_title: "Our Strengths / Why Choose Us",
view_details: "View Details",
},
faq: {
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",
category: "Category",
noResults: "No results found",
clearSearch: "Clear search",
},
about: {
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: {
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: {
title: "Contact Us",
description: "Contact us for more product information and support.",
keywords: "Hanye, contact us, product information, technical support",
name: "Name",
email: "Email Address",
email: "Email",
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 ファイルの表示

@@ -11,14 +11,50 @@ export default {
hotKeywords: "人気のキーワード",
productCategories: "製品カテゴリー",
byUsage: "用途で選ぶ",
footer: {
productsLinks: {
title: "製品",
},
websiteLinks: {
title: "ウェブサイト",
home: "ホーム",
products: "製品",
faq: "よくある質問",
about: "会社概要",
contact: "お問い合わせ",
},
quickLinks: {
title: "クイックリンク",
},
},
},
home: {
title: "Hanye ウェブサイトへようこそ",
description: "高品質の製品とサービスを提供しています",
keywords: "Hanye, メモリ, ストレージ, 製品, サービス, SD, SSD, microSD",
learnMore: "詳細を見る",
carousel: {
one: {
title: "新技術",
description: "3D NAND TLC",
description2: "フラッシュ採用、",
description3: "信頼性が高く耐久性に優\nれている",
description4: "ストレスのないゲーム体験をお楽しみください",
description5: "パソコンの起動時間が劇的に速くなった!",
},
},
},
products: {
title: "当社の製品",
description: "当社の製品、製品の使用方法や購入に関する助けを得ることができます。",
keywords: "Hanye, 製品, 製品情報, 技術サポート",
product_list: "製品一覧",
product_list_description:
"優れた製品は、10年以上の経験と継続的な革新デザインの結果です。",
product_categories_title: "製品カテゴリー",
product_categories_description: "製品の種類で選ぶ。",
product_categories_usage: "用途で選ぶ",
product_categories_usage_description: "製品の用途で選ぶ。",
viewDetails: "詳細を見る",
consultation: "製品に関するご相談、お見積もりはお気軽にどうぞ",
consultation_button: "お問い合わせ",
@@ -33,89 +69,64 @@ export default {
develop_description: "製品の多様化を図り、新たな創意への飽くなき挑戦",
strong_point: "当社の強み",
strong_point_title: "当社の強み/選ばれる理由",
view_details: "詳細を見る",
},
faq: {
title: "よくある質問",
description: "よくある質問、製品の使用方法や購入に関する助けを得ることができます。",
keywords: "Hanye, よくある質問, 製品情報, 技術サポート",
searchPlaceholder: "質問を検索",
category: "カテゴリー",
noResults: "該当する質問はありません",
clearSearch: "検索をクリア",
},
about: {
title: "当社について",
meta: {
title: "当社について - Hanye",
description: "Hanyeの会社情報、沿革、事業内容をご覧ください。メモリ及び関連製品の開発・製造・販売に取り組んでいます。"
},
intro: {
title: "当社について",
paragraph1: "Hanye は中国瀋陽に運営本部をおき 2003年に設立されました。",
paragraph2: "創業から現在に至るまで、メモリ(記憶媒体) 及び 関連製品の開発・製造・販売を統べる総合企業に成長いたしました。",
paragraph3: "また万全なアフターサービス体制も構築し全力でサポートいたします。お客様に信頼いただけるパートナーを目指し 一層の努力を重ねてまいります。",
},
title: "当社について - Hanye",
description: "Hanyeの会社概要、経営理念、企業情報をご紹介します。",
keywords: "Hanye, 会社概要, 経営理念, 企業情報",
overview: {
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: {
title: "お問い合わせ",
description: "Hanyeにお問い合わせください。",
keywords: "Hanye, お問い合わせ, 製品情報, 技術サポート",
name: "お名前",
email: "メールアドレス",
message: "メッセージ",
captcha: "検証コード",
refreshCaptcha: "検証コードを更新",
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 ファイルの表示

@@ -11,14 +11,51 @@ export default {
hotKeywords: "热门搜索",
productCategories: "产品分类",
byUsage: "按用途",
footer: {
productsLinks: {
title: "产品",
},
websiteLinks: {
title: "网站",
home: "首页",
products: "产品",
faq: "常见问题",
about: "关于我们",
contact: "联系我们",
},
quickLinks: {
title: "快捷链接",
},
},
},
home: {
title: "欢迎来到Hanye官网",
description: "我们提供高质量的产品和服务",
title: "Hanye 官网",
description:
"我们提供高质量的产品和服务,包括内存及SD,SSD,microSD相关产品, 并提供专业的技术支持",
keywords: "Hanye, 内存, 存储, 产品, 服务, SD, SSD, microSD, 技术支持",
learnMore: "了解更多",
carousel: {
one: {
title: "新科技",
description: "3D NAND TLC",
description2: "Flash-based,",
description3: "高可靠性、高耐用性",
description4: "享受无压力的游戏体验",
description5: "电脑启动时间大幅提升!",
},
},
},
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: "查看详情",
consultation: "欢迎进行产品咨询,我们将在第一时间回复您",
consultation_button: "联系我们",
@@ -32,100 +69,65 @@ export default {
develop_description: "不断开发和制造产品,提供创新解决方案",
strong_point: "我们的优势",
strong_point_title: "我们的优势/选择我们的理由",
view_details: "查看详情",
},
faq: {
title: "常见问题",
description: "常见问题,帮助您更好地了解Hanye的产品和服务。",
keywords: "Hanye, 常见问题, 产品信息, 技术支持",
searchPlaceholder: "搜索问题",
category: "分类",
clearSearch: "清除搜索",
noResults: "没有找到相关问题",
},
about: {
title: "关于我们",
meta: {
title: "关于我们 - Hanye",
description: "了解 Hanye 的公司信息、发展历程和业务范围。我们致力于内存及相关产品的开发、制造和销售。"
},
intro: {
title: "公司简介",
paragraph1: "Hanye 成立于2003年,运营总部位于中国沈阳。",
paragraph2: "自创立至今,我们已成长为集内存(存储介质)及相关产品的研发、制造、销售于一体的综合性企业。",
paragraph3: "我们建立了完善的售后服务体系,竭诚为您提供支持。我们致力于成为客户信赖的合作伙伴,并将为此付出更多努力。",
},
description: "介绍Hanye的公司概况、经营理念、企业信息",
keywords: "Hanye, 公司概况, 经营理念, 企业信息",
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: {
title: "联系我们",
description: "联系我们,获取更多产品信息和支持。",
keywords: "Hanye, 联系我们, 产品信息, 技术支持",
name: "姓名",
email: "邮箱",
message: "消息",
captcha: "验证码",
refreshCaptcha: "点击刷新验证码",
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 ファイルの表示

@@ -48,9 +48,20 @@ export default defineNuxtConfig({
crawlLinks: true,
routes: ["/"],
},
// 添加图片本地化配置
publicAssets: [
{
dir: 'public',
baseURL: '/'
},
{
dir: 'public/images/remote',
baseURL: '/images/remote'
}
]
},

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

+ 2
- 0
package.json ファイルの表示

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

+ 134
- 69
pages/about.vue ファイルの表示

@@ -16,88 +16,170 @@
<nuxt-link
to="/"
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 text-base font-normal">会社概要</span>
<span class="text-white text-base font-normal">{{
$t("about.overview.title")
}}</span>
</div>
</div>

<!-- 顶部大标题 -->
<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>
<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 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>
<div class="flex flex-col gap-3">
<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 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 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 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 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>
<div class="text-stone-400 text-lg leading-relaxed">
{{ companyInfo.philosophy }}
{{ $t("about.companyInfo.philosophy") }}
</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>
<div class="flex flex-col gap-3">
<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 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 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 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 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">
<p v-for="(item, idx) in companyInfo.businessActivities" :key="idx">{{ item }}</p>
<p>
{{ $t("about.companyInfo.businessActivities") }}
</p>
</div>
</div>
</div>
@@ -113,53 +195,34 @@
* 展示公司基本信息、理念等
*/
import { useErrorHandler } from "~/composables/useErrorHandler";
import companyImage from "@/assets/images/product-banner.webp";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();

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

const { error, isLoading, wrapAsync } = useErrorHandler();
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",
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优化
useHead({
title: "会社概要 - Hanye",
title: t("about.title"),
meta: [
{
name: "description",
content: "Hanyeの会社概要、経営理念、企業情報をご紹介します。",
content: t("about.description"),
},
{
name: "keywords",
content: t("about.keywords"),
},
],
});
@@ -179,7 +242,8 @@ useHead({

/* 动画效果 */
@keyframes float {
0%, 100% {
0%,
100% {
transform: translateY(0) scale(1);
opacity: 0.9;
}
@@ -246,7 +310,8 @@ useHead({
}

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

+ 40
- 56
pages/contact.vue ファイルの表示

@@ -14,12 +14,12 @@
<nuxt-link
to="/"
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>
<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
@@ -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") }}
</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="お名前を入力してください"
: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") }}
</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="メールアドレスを入力してください"
: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") }}
</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="お問い合わせ内容を入力してください"
: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") }}
</label>
</div>

@@ -102,7 +102,7 @@
? 'border-red-500 focus:border-red-500'
: 'border-gray-600 focus:border-blue-500',
]"
placeholder="验证码"
: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") }}
</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="刷新验证码"
: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="刷新验证码"
title="刷新验证码"
:aria-label="$t('contact.refreshCaptcha')"
:title="$t('contact.refreshCaptcha')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -164,7 +164,11 @@
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"
>
{{ isSubmitting ? "送信中..." : "送信する" }}
{{
isSubmitting
? $t("contact.submitting")
: $t("contact.submit")
}}
</button>
</form>
</div>
@@ -175,39 +179,39 @@
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") }}
</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") }}
</div>
<div class="text-white text-base font-normal">
株式会社ハニエ
{{ $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") }}
</div>
<div class="text-white text-base font-normal">
〒123-4567 東京都渋谷区神宮前1-1-1
{{ $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") }}
</div>
<div class="text-white text-base font-normal">
03-1234-5678
86)024-8399-0696
</div>
</div>
<div class="flex items-center gap-4">
<div class="text-white/60 text-base font-normal">
メールアドレス
{{ $t("about.overview.email") }}
</div>
<div class="text-white text-base font-normal">
info@hanye.co.jp
hanye@hanye.cn
</div>
</div>
</div>
@@ -217,31 +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") }}
</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>
@@ -260,6 +243,7 @@
*/
import { useErrorHandler } from "~/composables/useErrorHandler";
import { useCaptcha } from "~/composables/useCaptcha";
const { t, locale } = useI18n();

const { error, isLoading, wrapAsync } = useErrorHandler();
const captcha = useCaptcha();
@@ -303,16 +287,16 @@ async function handleSubmit() {

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

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

+ 16
- 8
pages/faq.vue ファイルの表示

@@ -14,11 +14,11 @@
<nuxt-link
to="/"
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>
<nuxt-link to="/faq" class="text-white text-base font-normal"
>よくある質問</nuxt-link
>{{ $t("faq.title") }}</nuxt-link
>
</div>
</div>
@@ -30,7 +30,9 @@
<!-- 左侧分类导航 -->
<div class="col-span-1 md:col-span-2">
<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
v-for="category in categories"
@@ -57,14 +59,14 @@
v-model="searchTerm"
ref="searchInputRef"
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"
/>
<button
v-if="searchTerm"
@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"
aria-label="検索キーワードをクリア"
:aria-label="$t('faq.clearSearch')"
tabindex="0"
type="button"
>
@@ -118,7 +120,7 @@
v-if="filteredFaqs.length === 0"
class="text-center text-gray-400 py-8"
>
該当する質問が見つかりません。
{{ $t("faq.noResults") }}
</div>
</div>
</div>
@@ -139,6 +141,8 @@ import { useErrorHandler } from "~/composables/useErrorHandler";

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

const { t, locale } = useI18n();

// FAQ数据
interface FAQ {
id: number;
@@ -299,11 +303,15 @@ function clearSearch() {

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

+ 110
- 50
pages/index.vue ファイルの表示

@@ -8,11 +8,11 @@
:space-between="30"
:loop="true"
:pagination="{ el: '.swiper-pagination-1', clickable: true }"
:autoplay="{
delay: 5000,
:autoplay="{
delay: 5000,
disableOnInteraction: false,
pauseOnMouseEnter: true,
waitForTransition: true
waitForTransition: true,
}"
effect="creative"
:creativeEffect="{
@@ -34,18 +34,65 @@
:parallax="true"
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>
<div class="max-w-screen-2xl mx-auto relative">
<div
@@ -77,26 +124,35 @@
: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="{
'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)"
>
<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 }}
</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 class="flex items-center justify-center gap-4 ml-auto">
<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"
>
<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
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>
@@ -131,10 +187,7 @@
class="w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
>
<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
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"
>
@@ -174,7 +227,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") }}
</div>
</div>
</div>
@@ -182,7 +235,8 @@
<div
class="w-full mt-4 min-h-[80px] transition-all duration-300 transform"
:class="{
'opacity-0 translate-y-4': !isImageLoaded[product.id],
'opacity-0 translate-y-4':
!isImageLoaded[product.id],
'opacity-100 translate-y-0':
isImageLoaded[product.id],
}"
@@ -232,13 +286,17 @@
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="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
v-for="feature in category.features"
: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"
>
<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>
</div>
</div>
@@ -252,13 +310,17 @@
</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
:src="category.image"
:alt="category.title"
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>
</nuxt-link>
</div>
@@ -470,8 +532,8 @@ import { useBreakpoints, breakpointsTailwind } from "@vueuse/core";
import { useI18n } from "vue-i18n";
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";
import product from "@/assets/images/product.png";

const { t, locale } = useI18n();
const config = useRuntimeConfig();
@@ -504,14 +566,6 @@ const { data: categoryData, error: categoryError } = await useFetch(
);
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 {
id: number;
title: string;
@@ -543,7 +597,9 @@ const typedUsageList = 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(() => {
@@ -629,11 +685,15 @@ const typedCategoryList = computed(() => {

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

+ 38
- 16
pages/products/index.vue ファイルの表示

@@ -18,12 +18,12 @@
<div
class="justify-start text-white text-2xl font-normal md:text-4xl lg:text-6xl"
>
製品一覧
{{ $t("products.product_list") }}
</div>
<div
class="text-white text-sm lg:text-lg font-normal leading-loose"
>
卓越した製品は、実績に裏打ちされた優れた技術と、<br />継続的な革新デザインとの融合により、生み出されます。
{{ $t("products.product_list_description") }}
</div>
</div>
</div>
@@ -38,11 +38,13 @@
<nuxt-link
to="/"
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>
<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>
@@ -55,7 +57,9 @@
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="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
v-for="category in categories"
@@ -63,8 +67,9 @@
@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-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 }}
@@ -73,7 +78,9 @@
</div>

<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
v-for="usage in usages"
@@ -81,8 +88,9 @@
@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-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 }}
@@ -94,13 +102,25 @@
<div class="col-span-1 md:col-span-8">
<div class="flex flex-col gap-16">
<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">
{{ category }}
</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
v-for="product in filteredProducts.filter(p => p.category === category)"
v-for="product in filteredProducts.filter(
(p) => p.category === category
)"
:key="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"
@@ -231,10 +251,12 @@ useHead({
</script>

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

+ 238
- 0
scripts/localize-images.mjs ファイルの表示

@@ -0,0 +1,238 @@
#!/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 ファイルの表示

@@ -1,14 +1,14 @@
/**
* 首页轮播图数据接口
* @returns 轮播图数据列表
*
*
* 替换真实接口说明:
* 1. 替换真实接口时,需要修改以下内容:
* - 将模拟数据替换为真实接口调用
* - 添加错误处理
* - 添加接口参数处理
* - 添加数据转换逻辑
*
*
* 2. 真实接口示例:
* const response = await $fetch('https://api.example.com/carousel', {
* method: 'GET',
@@ -21,7 +21,7 @@
* limit: 10
* }
* })
*
*
* 3. 错误处理示例:
* try {
* const response = await $fetch('...')
@@ -37,7 +37,7 @@
* message: '获取轮播图数据失败'
* }
* }
*
*
* 4. 数据转换示例:
* const transformedData = response.data.map(item => ({
* id: item.id,
@@ -45,7 +45,7 @@
* image: item.imageUrl,
* link: `/products/${item.productId}`
* }))
*
*
* 5. 接口参数处理示例:
* const query = getQuery(event)
* const page = Number(query.page) || 1
@@ -56,27 +56,44 @@ export default defineEventHandler(async () => {
const carouselList = [
{
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,
title: '轮播图2',
image: 'https://picsum.photos/1920/1080?random=2',
link: '/products/2'
image: "",
link: "/products/2",
},
{
id: 3,
title: '轮播图3',
image: 'https://picsum.photos/1920/1080?random=3',
link: '/products/3'
}
]
image: "",
link: "/products/3",
},
];

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

+ 1
- 0
server/api/products/[id].get.ts ファイルの表示

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

+ 24
- 20
server/api/products/category.get.ts ファイルの表示

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

+ 9
- 5
server/api/products/usage.ts ファイルの表示

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

+ 169
- 0
utils/image-downloader.ts ファイルの表示

@@ -0,0 +1,169 @@
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 ファイルの表示

@@ -0,0 +1,89 @@
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
}

読み込み中…
キャンセル
保存