- 在nuxt.config.ts中新增图片本地化配置,支持将API返回的远程图片下载到本地。 - 更新package.json,新增生成静态站点并本地化图片的命令。 - 新增README-image-localization.md文档,详细说明图片本地化功能的使用方法和注意事项。 - 在TheHeader和TheFooter组件中优化产品分类和网站链接的展示逻辑,提升用户体验。 - 更新多个页面的内容和样式,确保多语言支持的一致性。 - 新增图片下载和本地化的脚本,确保静态站点完全脱离原始服务器运行。master
# 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 |
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; | |||||
} | } |
</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> |
// 添加热门关键字 | // 添加热门关键字 | ||||
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}` | |||||
})) | |||||
}, | }, | ||||
], | ], | ||||
}, | }, |
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...", | |||||
}, | }, | ||||
}; | }; |
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: "送信中...", | |||||
}, | }, | ||||
}; | }; |
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: "正在提交...", | |||||
}, | }, | ||||
}; | }; |
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", | ||||
}, | |||||
} | |||||
}); | }); |
"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" | ||||
}, | }, |
<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)); | ||||
} | } |
<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> |
<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"), | |||||
}, | }, | ||||
], | ], | ||||
}); | }); |
: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); |
<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> |
#!/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() |
/** | /** | ||||
* 首页轮播图数据接口 | * 首页轮播图数据接口 | ||||
* @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: "获取轮播图数据成功", | |||||
}; | |||||
}); |
"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の起動時間を大幅に短縮し、アプリケーションの読み込みを高速化。耐久性に優れ、長時間の使用にも耐えられる設計。", | ||||
}, | }, | ||||
{ | { |
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" |
* 按用途产品展示接口 | * 按用途产品展示接口 | ||||
* @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' ? '获取按用途产品数据成功' : '用途別製品データの取得に成功しました' | |||||
} | } | ||||
}) | }) |
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 |
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 | |||||
} |