- 修改了styles.css文件,优化了字体导入和选择样式。 - 在TheHeader组件中调整了下拉菜单的样式和交互逻辑,提升了用户体验。 - 更新了about.vue、contact.vue和faq.vue页面,增加了加载状态和错误边界处理,改善了用户体验。 - 在index.vue和products/index.vue中优化了产品展示逻辑,增强了页面的响应式设计。 - 新增了多个API接口,支持产品数据的动态加载和分类筛选功能,提升了数据交互的灵活性。 - 完善了产品详情页的加载状态和错误处理,确保用户在浏览时的流畅体验。master
@@ -1,4 +1,4 @@ | |||
@import url('https://fonts.googleapis.com/css2?family=M+PLUS+1p&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Noto+Sans+JP:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); | |||
@import url('https://fonts.googleapis.com/css2?family=M+PLUS+1p:wght@100;300;400;500;700;800;900&family=Montserrat:ital,wght@0,100..900;1,100..900&family=Noto+Sans+JP:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); | |||
:root { | |||
/* 颜色变量 */ | |||
@@ -16,7 +16,8 @@ | |||
--color-text-light: #6b7280; | |||
--color-border: #e5e7eb; | |||
} | |||
::selection{ | |||
::selection { | |||
background-color: var(--color-accent) !important; | |||
color: var(--color-bg) !important; | |||
} | |||
@@ -29,6 +30,11 @@ body { | |||
/* 其他基础样式 */ | |||
font-size: 16px; | |||
line-height: 1.6; | |||
-webkit-font-smoothing: antialiased; | |||
-moz-osx-font-smoothing: grayscale; | |||
text-rendering: optimizeLegibility; | |||
font-feature-settings: "palt"; | |||
letter-spacing: 0.02em; | |||
} | |||
@@ -35,28 +35,14 @@ | |||
<!-- Dropdown Trigger --> | |||
<div | |||
@mouseenter="handleMouseEnter(item.label)" | |||
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2" | |||
:class="{ | |||
'text-white font-bold opacity-100': | |||
openDropdown === item.label || | |||
$route.path.startsWith(item.pathPrefix), | |||
}" | |||
class="justify-start text-white text-sm opacity-80 hover:opacity-100 transition-opacity cursor-pointer flex items-center gap-1 py-2 px-3 rounded-md" | |||
:class="[ | |||
$route.path.startsWith(item.pathPrefix) | |||
? 'font-bold opacity-100 bg-white/15' | |||
: '', | |||
]" | |||
> | |||
<span>{{ $t(item.label) }}</span> | |||
<!-- Dropdown Arrow --> | |||
<svg | |||
class="h-3 w-3 text-white/60" | |||
fill="none" | |||
viewBox="0 0 24 24" | |||
stroke="currentColor" | |||
> | |||
<path | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
stroke-width="3" | |||
d="M19 9l-7 7-7-7" | |||
/> | |||
</svg> | |||
</div> | |||
<!-- Dropdown Panel --> | |||
@@ -64,32 +50,24 @@ | |||
<div | |||
v-if="item.isDropdown && openDropdown === item.label" | |||
@mouseenter="handleMouseEnter(item.label)" | |||
class="absolute left-0 top-full mt-1 w-max min-w-[450px] bg-slate-800/90 backdrop-blur-md rounded-lg shadow-xl p-4 z-10 grid grid-cols-2 gap-4" | |||
class="absolute left-0 top-full mt-4 w-max min-w-[700px] bg-slate-900 rounded-none border-none shadow-none p-0 z-10 grid grid-cols-2 gap-0 transition-all duration-300" | |||
> | |||
<div | |||
v-for="(section, index) in item.children" | |||
:key="index" | |||
class="bg-black/10 p-4 rounded-md" | |||
class="bg-slate-900 rounded-none p-6 flex flex-col gap-1" | |||
> | |||
<h3 | |||
class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-4 flex items-center gap-2" | |||
> | |||
<i | |||
:class="[ | |||
index === 0 ? 'icon-tag' : 'icon-target', | |||
'text-gray-400', | |||
]" | |||
></i> | |||
<span>{{ $t(section.title) }}</span> | |||
<h3 class="text-base font-medium text-white mb-2"> | |||
{{ $t(section.title) }} | |||
</h3> | |||
<ul class="space-y-3"> | |||
<ul class="flex flex-col gap-1"> | |||
<li v-for="link in section.items" :key="link.path"> | |||
<nuxt-link | |||
:to="link.path" | |||
@click="handleMouseLeave" | |||
class="block text-base text-gray-200 hover:text-white hover:bg-white/10 transition-all duration-150 rounded px-2 py-1" | |||
class="block text-base text-gray-200 rounded-none py-2 transition-all duration-200 hover:text-white/80 hover:font-bold" | |||
:class="{ | |||
'text-white font-bold bg-white/5': | |||
'text-white font-bold bg-white/15': | |||
$route.path === link.path, | |||
}" | |||
> |
@@ -1,246 +1,260 @@ | |||
<template> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<div | |||
class="max-w-full px-4 py-16 md:px-8 lg:px-10 bg-gray-900 text-gray-300 min-h-screen relative overflow-hidden" | |||
> | |||
<!-- Subtle Pattern Background --> | |||
<div class="absolute inset-0 opacity-[0.03] pointer-events-none" | |||
style="background-image: linear-gradient(45deg, #fff 12%, transparent 12.5%, transparent 87%, #fff 87.5%, #fff), | |||
linear-gradient(-45deg, #fff 12%, transparent 12.5%, transparent 87%, #fff 87.5%, #fff); | |||
background-size: 8px 8px;"> | |||
</div> | |||
<!-- Content Wrapper --> | |||
<div class="relative max-w-screen-xl mx-auto"> | |||
<h1 class="text-4xl md:text-6xl mb-12 text-center font-normal text-white"> | |||
{{ $t("about.title") }} | |||
</h1> | |||
<div class="space-y-12"> | |||
<!-- Introduction Section --> | |||
<section | |||
class="relative bg-gray-800/80 border border-gray-700 rounded-xl overflow-hidden shadow-xl backdrop-blur-sm p-6 sm:p-8 lg:p-10" | |||
> | |||
<div class="mb-8"> | |||
<h2 | |||
class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block" | |||
<div> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<ErrorBoundary :error="error"> | |||
<div v-if="isLoading" class="flex justify-center py-12"> | |||
<!-- 加载中 --> | |||
<div | |||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||
></div> | |||
</div> | |||
<div v-else> | |||
<!-- 面包屑导航 --> | |||
<div class="max-w-full mb-6 xl:px-2 lg:px-2 md:px-4 px-4 mt-6"> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<nuxt-link | |||
to="/" | |||
class="justify-start text-white/60 text-base font-normal hover:text-white transition-colors duration-300" | |||
>ホーム</nuxt-link | |||
> | |||
{{ $t("about.intro.title") }} | |||
</h2> | |||
<div | |||
class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2" | |||
></div> | |||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||
<span class="text-white text-base font-normal">会社概要</span> | |||
</div> | |||
<div class="space-y-4 text-lg text-gray-300 leading-relaxed"> | |||
<p>{{ $t("about.intro.paragraph1") }}</p> | |||
<p>{{ $t("about.intro.paragraph2") }}</p> | |||
<p>{{ $t("about.intro.paragraph3") }}</p> | |||
</div> | |||
<!-- 顶部大标题 --> | |||
<div class="flex flex-col items-center justify-center px-2 mb-10"> | |||
<h1 class="text-white text-5xl font-bold mb-4 tracking-tight text-center"> | |||
{{ companyInfo.name }} | |||
</h1> | |||
<div class="text-stone-400 text-xl leading-relaxed text-center max-w-2xl"> | |||
{{ companyInfo.description }} | |||
</div> | |||
</section> | |||
<!-- Company Overview Section --> | |||
<section | |||
class="relative bg-gray-800/80 border border-gray-700 rounded-xl overflow-hidden shadow-xl backdrop-blur-sm p-6 sm:p-8 lg:p-10" | |||
> | |||
<div class="mb-8"> | |||
<h2 | |||
class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block" | |||
> | |||
{{ $t("about.overview.title") }} | |||
</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="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> | |||
</h2> | |||
<div | |||
class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2" | |||
></div> | |||
</div> | |||
<dl | |||
class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-2 lg:grid-cols-3" | |||
> | |||
<div class="sm:col-span-1"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.companyNameLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.companyNameValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-1"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.englishNameLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.englishNameValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-1"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.ceoLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.ceoValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-1"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.employeesLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.employeesValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-2"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.addressLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.addressValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-1"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.telLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.telValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-1"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.faxLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.faxValue") }} | |||
</dd> | |||
</div> | |||
<div class="sm:col-span-2 lg:col-span-3"> | |||
<dt class="text-base font-semibold text-gray-100"> | |||
{{ $t("about.overview.businessLabel") }} | |||
</dt> | |||
<dd class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.overview.businessValue1") }}<br /> | |||
{{ $t("about.overview.businessValue2") }}<br /> | |||
{{ $t("about.overview.businessValue3") }} | |||
</dd> | |||
<div class="flex flex-col gap-3"> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">設立</span> | |||
<span class="text-white text-lg font-bold">{{ companyInfo.established }}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">代表者</span> | |||
<span class="text-white text-lg font-bold">{{ companyInfo.ceo }}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">従業員数</span> | |||
<span class="text-white text-lg font-bold">{{ companyInfo.employees }}</span> | |||
</div> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">所在地</span> | |||
<span class="text-white text-lg font-bold">{{ companyInfo.location }}</span> | |||
</div> | |||
</div> | |||
</dl> | |||
</section> | |||
<!-- Contact Info Section --> | |||
<section | |||
class="relative bg-gray-800/80 border border-gray-700 rounded-xl overflow-hidden shadow-xl backdrop-blur-sm p-6 sm:p-8 lg:p-10" | |||
> | |||
<div class="mb-8"> | |||
<h2 | |||
class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block" | |||
> | |||
{{ $t("about.contact.title") }} | |||
</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> | |||
</h2> | |||
<div | |||
class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2" | |||
></div> | |||
<div class="text-stone-400 text-lg leading-relaxed"> | |||
{{ companyInfo.philosophy }} | |||
</div> | |||
</div> | |||
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3"> | |||
<div class="flex items-start space-x-4"> | |||
<div | |||
class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-lg shadow-md" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
> | |||
<path | |||
d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" | |||
/> | |||
<path | |||
d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" | |||
/> | |||
</svg> | |||
<!-- 联系方式卡片 --> | |||
<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> | |||
</h2> | |||
<div class="flex flex-col gap-3"> | |||
<div class="flex flex-col gap-1"> | |||
<span class="text-stone-400 text-base">E-mail</span> | |||
<a :href="'mailto:' + companyInfo.email" class="text-white text-lg font-bold hover:text-blue-400 transition-colors">{{ companyInfo.email }}</a> | |||
</div> | |||
<div> | |||
<h3 class="text-base font-medium text-gray-100"> | |||
{{ $t("about.contact.emailLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.contact.emailValue") }} | |||
</p> | |||
<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> | |||
</div> | |||
</div> | |||
<div class="flex items-start space-x-4"> | |||
<div | |||
class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-lg shadow-md" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
> | |||
<path | |||
fill-rule="evenodd" | |||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" | |||
clip-rule="evenodd" | |||
/> | |||
</svg> | |||
<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> | |||
</div> | |||
<div> | |||
<h3 class="text-base font-medium text-gray-100"> | |||
{{ $t("about.contact.hoursLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.contact.hoursValue1") }} | |||
</p> | |||
<p class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.contact.hoursValue2") }} | |||
</p> | |||
<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> | |||
</div> | |||
</div> | |||
<div class="flex items-start space-x-4"> | |||
<div | |||
class="flex-shrink-0 h-10 w-10 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-lg shadow-md" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
> | |||
<path | |||
d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" | |||
/> | |||
</svg> | |||
</div> | |||
<div> | |||
<h3 class="text-base font-medium text-gray-100"> | |||
{{ $t("about.contact.phoneLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400"> | |||
{{ $t("about.contact.phoneValue") }} | |||
</p> | |||
<p class="mt-1 text-sm text-gray-500"> | |||
{{ $t("about.contact.phoneNote") }} | |||
</p> | |||
<div class="flex flex-col gap-1 mt-2"> | |||
<span class="text-stone-400 text-base">事業内容</span> | |||
<div class="text-white text-base font-bold space-y-1"> | |||
<p v-for="(item, idx) in companyInfo.businessActivities" :key="idx">{{ item }}</p> | |||
</div> | |||
</div> | |||
</div> | |||
</section> | |||
</div> | |||
</div> | |||
</div> | |||
</ErrorBoundary> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
import { useI18n } from "vue-i18n"; | |||
/** | |||
* 公司简介页面 | |||
* 展示公司基本信息、理念等 | |||
*/ | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
import companyImage from "@/assets/images/product-banner.webp"; | |||
// 公司信息接口定义 | |||
interface CompanyInfo { | |||
name: string; | |||
englishName: string; | |||
description: string; | |||
established: string; | |||
ceo: string; | |||
employees: string; | |||
location: string; | |||
philosophy: string; | |||
email: string; | |||
tel: string; | |||
fax: string; | |||
businessHours: string; | |||
businessActivities: string[]; | |||
} | |||
const { t } = useI18n(); | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const companyInfo = ref<CompanyInfo>({ | |||
name: "Hanye", | |||
englishName: "Hanye Technology Co., Ltd.", | |||
description: "Hanye成立于2003年,运营总部设在中国沈阳。創業から現在に至るまで、メモリ(記憶媒体) 及び 関連製品の開発・製造・販売を統べる総合企業に成長いたしました。我司还将构筑完善的售后服务体系,全力提供技术支持。以成为客户信赖的合作伙伴为目标而更加努力。", | |||
established: "2003年", | |||
ceo: "ZHENG XIAO DONG", | |||
employees: "30人", | |||
location: "803, NO.6, AiTe, 90-6# SanHao Street, Heping District, ShenYang, China", | |||
philosophy: "私たちは、革新的な技術と優れた品質で、お客様のデジタルライフをより快適に、より安全にすることを目指しています。持続可能な開発と環境への配慮を重視し、社会に貢献する企業として成長し続けます。", | |||
email: "hanye@hanye.cn", | |||
tel: "86)024-8399-0696", | |||
fax: "86)024-8399-0696", | |||
businessHours: "平日 9:00-18:00 / 土日祝日休み", | |||
businessActivities: [ | |||
"闪存的开发、生产和销售", | |||
"开发、制造和销售SSD产品", | |||
"其相关业务" | |||
] | |||
}); | |||
// SEO | |||
// SEO优化 | |||
useHead({ | |||
title: t("about.meta.title"), | |||
title: "会社概要 - Hanye", | |||
meta: [ | |||
{ | |||
name: "description", | |||
content: t("about.meta.description"), | |||
content: "Hanyeの会社概要、経営理念、企業情報をご紹介します。", | |||
}, | |||
], | |||
}); | |||
</script> | |||
<style scoped> | |||
/* 信息卡片效果 */ | |||
.info-card { | |||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||
} | |||
.info-card:hover { | |||
transform: translateY(-2px); | |||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), | |||
0 2px 4px -1px rgba(0, 0, 0, 0.06); | |||
} | |||
/* 动画效果 */ | |||
@keyframes float { | |||
0%, 100% { | |||
transform: translateY(0) scale(1); | |||
opacity: 0.9; | |||
} | |||
50% { | |||
transform: translateY(-5px) scale(1.2); | |||
opacity: 1; | |||
} | |||
} | |||
.animate-float { | |||
animation: float 3s ease-in-out infinite; | |||
} | |||
@keyframes spin-slow { | |||
from { | |||
transform: rotate(0deg); | |||
} | |||
to { | |||
transform: rotate(360deg); | |||
} | |||
} | |||
@keyframes spin-slow-reverse { | |||
from { | |||
transform: rotate(360deg); | |||
} | |||
to { | |||
transform: rotate(0deg); | |||
} | |||
} | |||
.animate-spin-slow { | |||
animation: spin-slow 120s linear infinite; | |||
transform-origin: center; | |||
} | |||
.animate-spin-slow-reverse { | |||
animation: spin-slow-reverse 100s linear infinite; | |||
transform-origin: center; | |||
} | |||
@keyframes dash { | |||
from { | |||
stroke-dashoffset: 500; | |||
} | |||
to { | |||
stroke-dashoffset: 0; | |||
} | |||
} | |||
.animate-dash { | |||
stroke-dasharray: 5, 5; | |||
animation: dash 10s linear infinite; | |||
} | |||
.animate-dash-delay { | |||
stroke-dasharray: 5, 5; | |||
animation: dash 10s linear infinite; | |||
animation-delay: 3s; | |||
} | |||
.animate-pulse { | |||
animation: pulse 2s ease-in-out infinite; | |||
} | |||
@keyframes pulse { | |||
0%, 100% { | |||
opacity: 1; | |||
filter: drop-shadow(0 0 2px rgba(59, 130, 246, 0.6)); | |||
} | |||
50% { | |||
opacity: 0.7; | |||
filter: drop-shadow(0 0 5px rgba(59, 130, 246, 0.9)); | |||
} | |||
} | |||
/* 其他样式保持不变 */ | |||
</style> |
@@ -1,507 +1,318 @@ | |||
<template> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<div | |||
class="max-w-full px-4 py-16 md:px-8 lg:px-10 bg-gradient-to-br from-gray-900 via-gray-900 to-black text-gray-300 min-h-screen" | |||
> | |||
<div class="max-w-screen-xl mx-auto"> | |||
<h1 class="text-4xl md:text-6xl mb-12 text-center font-normal text-white"> | |||
{{ $t("contact.title") }} | |||
</h1> | |||
<div> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<ErrorBoundary :error="error"> | |||
<div v-if="isLoading" class="flex justify-center py-12"> | |||
<div | |||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||
></div> | |||
</div> | |||
<div class="grid grid-cols-1 gap-10 lg:grid-cols-2 lg:gap-16"> | |||
<!-- 联系表单 --> | |||
<div v-else> | |||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-20"> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<nuxt-link | |||
to="/" | |||
class="justify-start text-white/60 text-base font-normal" | |||
>ホーム</nuxt-link | |||
> | |||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||
<nuxt-link to="/contact" class="text-white text-base font-normal" | |||
>お問い合わせ</nuxt-link | |||
> | |||
</div> | |||
</div> | |||
<div | |||
class="relative bg-gray-800/70 border border-gray-700 rounded-xl overflow-hidden shadow-2xl backdrop-blur-sm transition-all duration-300 ease-in-out hover:shadow-blue-500/30 hover:border-blue-500/50 group" | |||
class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4" | |||
> | |||
<div | |||
class="absolute inset-0 bg-gradient-to-r from-transparent via-blue-900/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" | |||
></div> | |||
<div class="relative p-6 sm:p-8 lg:p-10"> | |||
<div class="mb-8"> | |||
<h2 | |||
class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block" | |||
> | |||
{{ $t("contact.form.title") }} | |||
</h2> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<div class="w-full grid grid-cols-1 md:grid-cols-2 gap-8"> | |||
<!-- 联系表单 --> | |||
<div | |||
class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2" | |||
></div> | |||
</div> | |||
<ErrorBoundary :error="error"> | |||
<form @submit.prevent="submitForm" class="space-y-10"> | |||
<div class="relative"> | |||
<input | |||
type="text" | |||
id="name" | |||
v-model="formData.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]" | |||
:class="[ | |||
formErrors.name | |||
? 'border-red-500 focus:border-red-500' | |||
: 'border-gray-600 focus:border-blue-500', | |||
]" | |||
:placeholder="$t('contact.name')" | |||
required | |||
:aria-invalid="formErrors.name ? 'true' : 'false'" | |||
aria-describedby="name-error" | |||
/> | |||
<label | |||
for="name" | |||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium" | |||
:class="[ | |||
formErrors.name | |||
? 'text-red-400 peer-focus:text-red-400' | |||
: 'text-gray-400 peer-focus:text-blue-400', | |||
]" | |||
>{{ $t("contact.name") }}</label | |||
> | |||
<p | |||
v-if="formErrors.name" | |||
id="name-error" | |||
class="mt-1.5 text-xs text-red-400 sm:text-sm" | |||
> | |||
{{ formErrors.name }} | |||
</p> | |||
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> | |||
<form | |||
@submit.prevent="handleSubmit" | |||
class="flex flex-col gap-6" | |||
> | |||
<div class="relative"> | |||
<input | |||
v-model="form.name" | |||
type="text" | |||
id="name" | |||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" | |||
placeholder="お名前を入力してください" | |||
required | |||
/> | |||
<label | |||
for="name" | |||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" | |||
> | |||
お名前 | |||
</label> | |||
</div> | |||
<div class="relative"> | |||
<input | |||
type="email" | |||
id="email" | |||
v-model="formData.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]" | |||
:class="[ | |||
formErrors.email | |||
? 'border-red-500 focus:border-red-500' | |||
: 'border-gray-600 focus:border-blue-500', | |||
]" | |||
:placeholder="$t('contact.email')" | |||
required | |||
:aria-invalid="formErrors.email ? 'true' : 'false'" | |||
aria-describedby="email-error" | |||
/> | |||
<label | |||
for="email" | |||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium" | |||
:class="[ | |||
formErrors.email | |||
? 'text-red-400 peer-focus:text-red-400' | |||
: 'text-gray-400 peer-focus:text-blue-400', | |||
]" | |||
>{{ $t("contact.email") }}</label | |||
> | |||
<p | |||
v-if="formErrors.email" | |||
id="email-error" | |||
class="mt-1.5 text-xs text-red-400 sm:text-sm" | |||
> | |||
{{ formErrors.email }} | |||
</p> | |||
</div> | |||
<div class="relative"> | |||
<input | |||
v-model="form.email" | |||
type="email" | |||
id="email" | |||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" | |||
placeholder="メールアドレスを入力してください" | |||
required | |||
/> | |||
<label | |||
for="email" | |||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" | |||
> | |||
メールアドレス | |||
</label> | |||
</div> | |||
<div class="relative"> | |||
<textarea | |||
id="message" | |||
v-model="formData.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]" | |||
:class="[ | |||
formErrors.message | |||
? 'border-red-500 focus:border-red-500' | |||
: 'border-gray-600 focus:border-blue-500', | |||
]" | |||
:placeholder="$t('contact.message')" | |||
required | |||
rows="5" | |||
:aria-invalid="formErrors.message ? 'true' : 'false'" | |||
aria-describedby="message-error" | |||
></textarea> | |||
<label | |||
for="message" | |||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium" | |||
:class="[ | |||
formErrors.message | |||
? 'text-red-400 peer-focus:text-red-400' | |||
: 'text-gray-400 peer-focus:text-blue-400', | |||
]" | |||
>{{ $t("contact.message") }}</label | |||
> | |||
<p | |||
v-if="formErrors.message" | |||
id="message-error" | |||
class="mt-1.5 text-xs text-red-400 sm:text-sm" | |||
> | |||
{{ formErrors.message }} | |||
</p> | |||
</div> | |||
<div class="relative"> | |||
<textarea | |||
v-model="form.message" | |||
id="message" | |||
class="peer block w-full appearance-none bg-transparent border-0 border-b-2 px-1 pt-5 pb-2 text-base text-gray-100 focus:outline-none focus:ring-0 placeholder-transparent h-36 resize-none transition-colors duration-300 focus:border-b-[3px] border-gray-600 focus:border-blue-500" | |||
placeholder="お問い合わせ内容を入力してください" | |||
required | |||
></textarea> | |||
<label | |||
for="message" | |||
class="absolute left-1 top-5 origin-[0] -translate-y-4 scale-75 transform text-sm duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:font-medium text-gray-400 peer-focus:text-blue-400" | |||
> | |||
お問い合わせ内容 | |||
</label> | |||
</div> | |||
<!-- Captcha Section --> | |||
<div class="relative pt-2"> | |||
<div class="flex items-center space-x-3"> | |||
<!-- Captcha Input --> | |||
<div class="flex-grow relative"> | |||
<input | |||
type="text" | |||
id="captcha" | |||
v-model="captcha.userInput.value" | |||
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]" | |||
:class="[ | |||
captcha.error.value | |||
? 'border-red-500 focus:border-red-500' | |||
: 'border-gray-600 focus:border-blue-500', | |||
]" | |||
:placeholder="$t('contact.form.captchaLabel')" | |||
required | |||
autocomplete="off" | |||
aria-describedby="captcha-error" | |||
:aria-invalid="captcha.error.value ? 'true' : 'false'" | |||
/> | |||
<label | |||
for="captcha" | |||
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" | |||
:class="[ | |||
captcha.error.value | |||
? 'text-red-400 peer-focus:text-red-400' | |||
: 'text-gray-400 peer-focus:text-blue-400', | |||
]" | |||
>{{ $t("contact.form.captchaLabel") }}</label | |||
<!-- 验证码部分 --> | |||
<div class="relative pt-2"> | |||
<div class="flex items-center space-x-3"> | |||
<div class="flex-grow relative"> | |||
<input | |||
type="text" | |||
id="captcha" | |||
v-model="captcha.userInput.value" | |||
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]" | |||
:class="[ | |||
captcha.error.value | |||
? 'border-red-500 focus:border-red-500' | |||
: 'border-gray-600 focus:border-blue-500', | |||
]" | |||
placeholder="验证码" | |||
required | |||
autocomplete="off" | |||
aria-describedby="captcha-error" | |||
:aria-invalid="captcha.error.value ? 'true' : 'false'" | |||
/> | |||
<label | |||
for="captcha" | |||
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" | |||
:class="[ | |||
captcha.error.value | |||
? 'text-red-400 peer-focus:text-red-400' | |||
: 'text-gray-400 peer-focus:text-blue-400', | |||
]" | |||
> | |||
验证码 | |||
</label> | |||
</div> | |||
<div | |||
class="flex-shrink-0 cursor-pointer select-none rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:shadow-md active:scale-100" | |||
v-html="captcha.captchaSvg.value" | |||
@click="captcha.generateCaptcha()" | |||
title="刷新验证码" | |||
style="line-height: 0" | |||
></div> | |||
<button | |||
type="button" | |||
@click="captcha.generateCaptcha()" | |||
class="flex-shrink-0 p-2 text-gray-500 hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out" | |||
aria-label="刷新验证码" | |||
title="刷新验证码" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5" | |||
fill="none" | |||
viewBox="0 0 24 24" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
> | |||
<path | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" | |||
/> | |||
</svg> | |||
</button> | |||
</div> | |||
<!-- Captcha Image/SVG --> | |||
<div | |||
class="flex-shrink-0 cursor-pointer rounded-md overflow-hidden transition-all duration-200 ease-in-out hover:scale-105 hover:shadow-md active:scale-100" | |||
v-html="captcha.captchaSvg.value" | |||
@click="captcha.generateCaptcha()" | |||
:title="$t('contact.form.captchaRefresh')" | |||
style="line-height: 0" | |||
></div> | |||
<!-- Refresh Button --> | |||
<button | |||
type="button" | |||
@click="captcha.generateCaptcha()" | |||
class="flex-shrink-0 p-2 text-gray-500 hover:text-blue-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 rounded-full hover:bg-gray-700/50 transition-all duration-200 ease-in-out" | |||
:aria-label="$t('contact.form.captchaRefresh')" | |||
:title="$t('contact.form.captchaRefresh')" | |||
<p | |||
v-if="captcha.error.value" | |||
id="captcha-error" | |||
class="mt-1.5 text-xs text-red-400 sm:text-sm" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5" | |||
fill="none" | |||
viewBox="0 0 24 24" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
> | |||
<path | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" | |||
/> | |||
</svg> | |||
</button> | |||
{{ captcha.error.value }} | |||
</p> | |||
</div> | |||
<p | |||
v-if="captcha.error.value" | |||
id="captcha-error" | |||
class="mt-1.5 text-xs text-red-400 sm:text-sm" | |||
> | |||
{{ | |||
captcha.error.value === "请输入验证码" | |||
? $t("contact.validation.captchaRequired") | |||
: $t("contact.validation.captchaIncorrect") | |||
}} | |||
</p> | |||
</div> | |||
<div class="pt-6"> | |||
<button | |||
type="submit" | |||
class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3.5 px-6 rounded-lg shadow-lg hover:shadow-xl hover:from-blue-500 hover:to-purple-500 transform hover:-translate-y-0.5 transition-all duration-300 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-md text-base tracking-wide focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 focus-visible:ring-offset-gray-900" | |||
:disabled="isLoading" | |||
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" | |||
> | |||
<span | |||
v-if="isLoading" | |||
class="flex items-center justify-center" | |||
> | |||
<span | |||
class="animate-spin h-5 w-5 border-2 border-white rounded-full border-t-transparent mr-2.5" | |||
></span> | |||
{{ $t("contact.form.submitLoading") }} | |||
</span> | |||
<span v-else>{{ $t("contact.submit") }}</span> | |||
{{ isSubmitting ? "送信中..." : "送信する" }} | |||
</button> | |||
</div> | |||
<div | |||
v-if="submitSuccess" | |||
class="mt-6 p-4 bg-green-600/20 text-green-300 rounded-lg border border-green-500/50 text-sm flex items-center space-x-2" | |||
role="alert" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5 flex-shrink-0" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
> | |||
<path | |||
fill-rule="evenodd" | |||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" | |||
clip-rule="evenodd" | |||
/> | |||
</svg> | |||
<span>{{ $t("contact.form.successMessage") }}</span> | |||
</div> | |||
</form> | |||
</ErrorBoundary> | |||
</div> | |||
</div> | |||
<!-- 联系信息 --> | |||
<div | |||
class="relative bg-gray-800/70 border border-gray-700 rounded-xl overflow-hidden shadow-2xl backdrop-blur-sm transition-all duration-300 ease-in-out hover:shadow-purple-500/30 hover:border-purple-500/50 group" | |||
> | |||
<div | |||
class="absolute inset-0 bg-gradient-to-r from-transparent via-purple-900/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" | |||
></div> | |||
<div class="relative p-6 sm:p-8 lg:p-10"> | |||
<div class="mb-8"> | |||
<h2 | |||
class="text-2xl font-semibold text-gray-100 sm:text-3xl inline-block" | |||
> | |||
{{ $t("contact.info.title") }} | |||
</h2> | |||
<div | |||
class="h-1 w-20 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full mt-2" | |||
></div> | |||
</div> | |||
<div class="space-y-8"> | |||
<div class="flex items-center"> | |||
<div | |||
class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-6 w-6" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
aria-hidden="true" | |||
> | |||
<path | |||
fill-rule="evenodd" | |||
d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" | |||
clip-rule="evenodd" | |||
/> | |||
</svg> | |||
</div> | |||
<div class="ml-4"> | |||
<h3 class="text-lg font-medium text-gray-100"> | |||
{{ $t("contact.info.addressLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400"> | |||
{{ $t("contact.info.addressValue1") }}<br /> | |||
{{ $t("contact.info.addressValue2") }} | |||
</p> | |||
</div> | |||
</form> | |||
</div> | |||
<div class="flex items-center"> | |||
<!-- 公司信息和营业时间 --> | |||
<div class="flex flex-col gap-8"> | |||
<div | |||
class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg" | |||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-6 w-6" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
aria-hidden="true" | |||
> | |||
<path | |||
d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" | |||
/> | |||
</svg> | |||
</div> | |||
<div class="ml-4"> | |||
<h3 class="text-lg font-medium text-gray-100"> | |||
{{ $t("contact.info.phoneLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400">+86 123 456 7890</p> | |||
</div> | |||
</div> | |||
<div class="flex items-center"> | |||
<div | |||
class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-6 w-6" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
aria-hidden="true" | |||
> | |||
<path | |||
d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" | |||
/> | |||
<path | |||
d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" | |||
/> | |||
</svg> | |||
</div> | |||
<div class="ml-4"> | |||
<h3 class="text-lg font-medium text-gray-100"> | |||
{{ $t("contact.info.emailLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400"> | |||
contact@example.com | |||
</p> | |||
<div class="text-white text-3xl font-medium mb-6"> | |||
会社情報 | |||
</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"> | |||
株式会社ハニエ | |||
</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"> | |||
〒123-4567 東京都渋谷区神宮前1-1-1 | |||
</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"> | |||
03-1234-5678 | |||
</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"> | |||
info@hanye.co.jp | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="flex items-center"> | |||
<div | |||
class="flex-shrink-0 h-12 w-12 flex items-center justify-center bg-gradient-to-br from-blue-500 to-purple-600 text-white rounded-full shadow-lg" | |||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-6 w-6" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
aria-hidden="true" | |||
> | |||
<path | |||
fill-rule="evenodd" | |||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" | |||
clip-rule="evenodd" | |||
/> | |||
</svg> | |||
</div> | |||
<div class="ml-4"> | |||
<h3 class="text-lg font-medium text-gray-100"> | |||
{{ $t("contact.info.hoursLabel") }} | |||
</h3> | |||
<p class="mt-1 text-base text-gray-400"> | |||
{{ $t("contact.info.hoursValue1") }}<br /> | |||
{{ $t("contact.info.hoursValue2") }} | |||
</p> | |||
<div class="text-white text-3xl font-medium mb-6"> | |||
営業時間 | |||
</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> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</ErrorBoundary> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
/** | |||
* 联系我们页面 | |||
* 提供联系表单和联系信息 | |||
* 联系页面 | |||
* 展示公司信息和联系表单 | |||
*/ | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
import { useCaptcha } from "~/composables/useCaptcha"; | |||
import { useI18n } from "vue-i18n"; | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const captcha = useCaptcha(); | |||
const isSubmitting = ref(false); | |||
const submitSuccess = ref(false); | |||
const { t } = useI18n(); | |||
// 表单数据 | |||
const formData = reactive({ | |||
name: "", | |||
email: "", | |||
message: "", | |||
}); | |||
// 表单错误 | |||
const formErrors = reactive({ | |||
const form = ref({ | |||
name: "", | |||
email: "", | |||
message: "", | |||
}); | |||
/** | |||
* 验证表单输入 | |||
* @returns 表单是否有效 | |||
*/ | |||
function validateForm(): boolean { | |||
let isValid = true; | |||
// 重置错误 | |||
formErrors.name = ""; | |||
formErrors.email = ""; | |||
formErrors.message = ""; | |||
// 验证姓名 | |||
if (!formData.name.trim()) { | |||
formErrors.name = t("contact.validation.nameRequired"); | |||
isValid = false; | |||
} | |||
// 验证邮箱 | |||
if (!formData.email.trim()) { | |||
formErrors.email = t("contact.validation.emailRequired"); | |||
isValid = false; | |||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { | |||
formErrors.email = t("contact.validation.emailInvalid"); | |||
isValid = false; | |||
} | |||
// 验证消息 | |||
if (!formData.message.trim()) { | |||
formErrors.message = t("contact.validation.messageRequired"); | |||
isValid = false; | |||
} | |||
return isValid; | |||
} | |||
/** | |||
* 提交表单 | |||
* 处理表单提交 | |||
*/ | |||
async function submitForm() { | |||
// 重置成功状态 | |||
async function handleSubmit() { | |||
isSubmitting.value = true; | |||
submitSuccess.value = false; | |||
try { | |||
await wrapAsync(async () => { | |||
// 验证验证码 | |||
if (!captcha.validateCaptcha()) { | |||
return; | |||
} | |||
// 验证表单(姓名、邮箱、消息) | |||
if (!validateForm()) { | |||
return; | |||
// TODO: 实现表单提交逻辑 | |||
await new Promise((resolve) => setTimeout(resolve, 1000)); | |||
form.value = { | |||
name: "", | |||
email: "", | |||
message: "", | |||
}; | |||
captcha.generateCaptcha(); // 成功后刷新验证码 | |||
submitSuccess.value = true; | |||
return { success: true }; | |||
}); | |||
} finally { | |||
isSubmitting.value = false; | |||
} | |||
// 验证验证码 | |||
if (!captcha.validateCaptcha()) { | |||
return; | |||
} | |||
// 提交表单数据 | |||
await wrapAsync(async () => { | |||
// 模拟API请求 | |||
console.log("Form Data:", formData); | |||
console.log("Captcha Validated!"); | |||
await new Promise((resolve) => setTimeout(resolve, 1500)); | |||
// 模拟成功响应 | |||
submitSuccess.value = true; | |||
// 清空表单和验证码 | |||
formData.name = ""; | |||
formData.email = ""; | |||
formData.message = ""; | |||
captcha.generateCaptcha(); // 成功后也刷新验证码 | |||
return true; | |||
}); | |||
} | |||
// SEO优化 | |||
useHead({ | |||
title: t("contact.meta.title"), | |||
title: "联系我们 - Hanye", | |||
meta: [ | |||
{ | |||
name: "description", | |||
content: t("contact.meta.description"), | |||
content: "联系我们,获取更多产品信息和支持。", | |||
}, | |||
], | |||
}); | |||
</script> | |||
<style scoped> | |||
/* 移除之前的 focus 样式,因为现在使用 border-b 样式 */ | |||
</style> |
@@ -1,220 +1,322 @@ | |||
<template> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<div | |||
class="max-w-full px-4 py-16 md:px-8 lg:px-10 bg-gradient-to-br from-gray-900 via-gray-900 to-black text-gray-300 min-h-screen" | |||
> | |||
<div class="max-w-screen-xl mx-auto"> | |||
<h1 class="text-4xl md:text-6xl mb-12 text-center font-normal text-white"> | |||
{{ $t("faq.title") }} | |||
</h1> | |||
<!-- Search Bar --> | |||
<div class="mb-12 max-w-xl mx-auto"> | |||
<div class="relative"> | |||
<input | |||
type="search" | |||
v-model="searchTerm" | |||
:placeholder="$t('faq.searchPlaceholder')" | |||
class="block w-full appearance-none rounded-lg border border-gray-600 bg-gray-700/50 px-4 py-3 pl-10 pr-4 text-base text-gray-100 placeholder-gray-400 shadow-inner transition duration-200 ease-in-out focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:ring-offset-2 focus:ring-offset-gray-900" | |||
/> | |||
<div | |||
class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="h-5 w-5 text-gray-400" | |||
viewBox="0 0 20 20" | |||
fill="currentColor" | |||
<div> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<ErrorBoundary :error="error"> | |||
<div v-if="isLoading" class="flex justify-center py-12"> | |||
<div | |||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||
></div> | |||
</div> | |||
<div v-else> | |||
<div class="max-w-full xl:px-2 lg:px-2 md:px-4 px-4 mt-6 mb-20"> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<nuxt-link | |||
to="/" | |||
class="justify-start text-white/60 text-base font-normal" | |||
>ホーム</nuxt-link | |||
> | |||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||
<nuxt-link to="/faq" class="text-white text-base font-normal" | |||
>よくある質問</nuxt-link | |||
> | |||
<path | |||
fill-rule="evenodd" | |||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" | |||
clip-rule="evenodd" | |||
/> | |||
</svg> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- FAQ Accordion --> | |||
<div class="space-y-5"> | |||
<div | |||
v-for="faq in filteredFaqs" | |||
:key="faq.id" | |||
class="relative border rounded-xl overflow-hidden shadow-lg backdrop-blur-sm transition-all duration-300 ease-in-out group" | |||
:class="[ | |||
openAccordionIds.has(faq.id) | |||
? 'bg-gray-750/70 border-blue-500/60 shadow-blue-500/20' | |||
: 'bg-gray-800/60 border-gray-700 hover:border-blue-500/40 hover:bg-gray-750/70', | |||
]" | |||
class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4" | |||
> | |||
<button | |||
@click="toggleAccordion(faq.id)" | |||
class="flex w-full items-center justify-between px-6 py-5 text-left text-lg font-medium text-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-800 transition-colors duration-200" | |||
:class="{ 'bg-gray-700/50': openAccordionIds.has(faq.id) }" | |||
:aria-expanded="openAccordionIds.has(faq.id)" | |||
:aria-controls="`faq-answer-${faq.id}`" | |||
> | |||
<span class="pr-4">{{ faq.question }}</span> | |||
<!-- Plus/Minus Icon --> | |||
<div | |||
class="relative h-6 w-6 flex-shrink-0 text-blue-400 group-hover:text-blue-300" | |||
> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="absolute h-6 w-6 transition-opacity duration-300 ease-in-out" | |||
:class=" | |||
openAccordionIds.has(faq.id) ? 'opacity-0' : 'opacity-100' | |||
" | |||
fill="none" | |||
viewBox="0 0 24 24" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
> | |||
<path | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
d="M12 4v16m8-8H4" | |||
/> | |||
</svg> | |||
<svg | |||
xmlns="http://www.w3.org/2000/svg" | |||
class="absolute h-6 w-6 transition-opacity duration-300 ease-in-out" | |||
:class=" | |||
openAccordionIds.has(faq.id) ? 'opacity-100' : 'opacity-0' | |||
" | |||
fill="none" | |||
viewBox="0 0 24 24" | |||
stroke="currentColor" | |||
stroke-width="2" | |||
> | |||
<path | |||
stroke-linecap="round" | |||
stroke-linejoin="round" | |||
d="M20 12H4" | |||
/> | |||
</svg> | |||
</div> | |||
</button> | |||
<transition | |||
enter-active-class="transition-[grid-template-rows,opacity] ease-in-out duration-300" | |||
enter-from-class="grid-template-rows-[0fr] opacity-0" | |||
enter-to-class="grid-template-rows-[1fr] opacity-100" | |||
leave-active-class="transition-[grid-template-rows,opacity] ease-in-out duration-300" | |||
leave-from-class="grid-template-rows-[1fr] opacity-100" | |||
leave-to-class="grid-template-rows-[0fr] opacity-0" | |||
> | |||
<div | |||
v-show="openAccordionIds.has(faq.id)" | |||
:id="`faq-answer-${faq.id}`" | |||
class="grid overflow-hidden" | |||
role="region" | |||
> | |||
<div class="overflow-hidden"> | |||
<div | |||
class="px-6 pt-4 pb-8 text-base text-gray-300 leading-relaxed" | |||
> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<div class="w-full grid grid-cols-1 md:grid-cols-10 gap-8 md:gap-2"> | |||
<!-- 左侧分类导航 --> | |||
<div class="col-span-1 md:col-span-2"> | |||
<div class="flex flex-col gap-4"> | |||
<div class="text-white text-3xl font-medium">カテゴリー</div> | |||
<div class="flex flex-col gap-4 w-fit"> | |||
<div | |||
v-for="category in categories" | |||
:key="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-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 }} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 右侧FAQ列表 --> | |||
<div class="col-span-1 md:col-span-8"> | |||
<!-- 搜索框 --> | |||
<div class="mb-8 relative"> | |||
<input | |||
v-model="searchTerm" | |||
ref="searchInputRef" | |||
type="search" | |||
placeholder="キーワードで検索..." | |||
class="block w-full appearance-none rounded-lg border border-gray-600 bg-zinc-800/70 px-4 py-3 text-base text-gray-100 placeholder-gray-400 shadow-inner transition duration-200 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:ring-offset-2 focus:ring-offset-zinc-900" | |||
/> | |||
<button | |||
v-if="searchTerm" | |||
@click="clearSearch" | |||
class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-full bg-zinc-700/80 hover:bg-blue-500/90 text-gray-300 hover:text-white transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" | |||
aria-label="検索キーワードをクリア" | |||
tabindex="0" | |||
type="button" | |||
> | |||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> | |||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> | |||
</svg> | |||
</button> | |||
</div> | |||
<div class="flex flex-col gap-8"> | |||
<div | |||
v-for="faq in filteredFaqs" | |||
:key="faq.id" | |||
class="bg-zinc-900 rounded-lg p-8 transition-all duration-300 hover:bg-zinc-800 hover:shadow-lg" | |||
> | |||
<div | |||
class="flex items-center justify-between cursor-pointer" | |||
@click="toggleFaq(faq.id)" | |||
> | |||
<div class="text-white text-xl font-medium"> | |||
<template | |||
v-for="(part, i) in highlightKeyword(faq.question)" | |||
:key="i" | |||
> | |||
<span v-if="typeof part === 'string'">{{ | |||
part | |||
}}</span> | |||
<component v-else :is="part"></component> | |||
</template> | |||
</div> | |||
<div | |||
class="text-white text-2xl transition-transform duration-300" | |||
:class="{ 'rotate-180': expandedFaqs.includes(faq.id) }" | |||
> | |||
▼ | |||
</div> | |||
</div> | |||
<div | |||
v-if="expandedFaqs.includes(faq.id)" | |||
class="mt-4 text-white/80 text-base font-normal leading-relaxed" | |||
> | |||
<template | |||
v-for="(part, i) in highlightKeyword(faq.answer)" | |||
:key="i" | |||
> | |||
<span v-if="typeof part === 'string'">{{ part }}</span> | |||
<component v-else :is="part"></component> | |||
</template> | |||
</div> | |||
</div> | |||
<div | |||
class="prose prose-invert max-w-none prose-p:text-gray-300 prose-a:text-blue-400 hover:prose-a:text-blue-300" | |||
v-if="filteredFaqs.length === 0" | |||
class="text-center text-gray-400 py-8" | |||
> | |||
<p v-html="faq.answer"></p> | |||
該当する質問が見つかりません。 | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</transition> | |||
</div> | |||
<div | |||
v-if="filteredFaqs.length === 0" | |||
class="text-center text-gray-400 py-8" | |||
> | |||
{{ $t("faq.noResults") }} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</ErrorBoundary> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
import { ref, computed } from "vue"; | |||
import { useI18n } from "vue-i18n"; | |||
/** | |||
* FAQ页面 | |||
* 展示常见问题及其答案 | |||
*/ | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
const { t } = useI18n(); | |||
const searchTerm = ref(""); | |||
const openAccordionIds = ref<Set<string>>(new Set()); | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
// Define FAQ item type with actual strings | |||
interface FaqItem { | |||
id: string; | |||
// FAQ数据 | |||
interface FAQ { | |||
id: number; | |||
category: string; | |||
question: string; | |||
answer: string; | |||
} | |||
// Placeholder FAQ data with actual strings (replace with your real data) | |||
const faqs = ref<FaqItem[]>([ | |||
const faqs = ref<FAQ[]>([ | |||
{ | |||
id: "faq-1", | |||
question: "如何购买 Hanye 产品?", // Example Question 1 | |||
id: 1, | |||
category: "製品について", | |||
question: "製品の保証期間はどのくらいですか?", | |||
answer: | |||
"您可以通过我们的官方在线商店或授权的零售商处购买 Hanye 产品。我们建议您在官方渠道购买以确保正品和售后服务。", // Example Answer 1 | |||
"当社の製品は購入日から1年間の保証期間があります。保証期間内に製品に問題が発生した場合は、無償で修理または交換いたします。", | |||
}, | |||
{ | |||
id: "faq-2", | |||
question: "产品保修期是多久?", | |||
id: 2, | |||
category: "製品について", | |||
question: "製品の取扱説明書はどこで入手できますか?", | |||
answer: | |||
"不同产品的保修期可能不同,请参考具体产品的说明页面或联系我们的客服获取详细信息。通常固态硬盘提供3-5年保修,内存条提供终身保固。", | |||
"製品の取扱説明書は、当社ウェブサイトの製品ページからダウンロードできます。また、製品パッケージにも同梱されています。", | |||
}, | |||
{ | |||
id: "faq-3", | |||
question: "如何申请售后服务?", | |||
id: 3, | |||
category: "購入について", | |||
question: "支払い方法は何がありますか?", | |||
answer: | |||
"如果您需要售后服务,请准备好您的购买凭证,并通过我们的官方网站提交售后申请或直接联系客服中心。", | |||
"クレジットカード、銀行振込、コンビニ決済など、様々な支払い方法をご利用いただけます。詳細はお支払いページをご確認ください。", | |||
}, | |||
{ | |||
id: "faq-4", | |||
question: "Hanye SSD 是否兼容我的电脑?", | |||
id: 4, | |||
category: "購入について", | |||
question: "返品・交換は可能ですか?", | |||
answer: | |||
"Hanye SSD 兼容大多数台式机和笔记本电脑。请确认您的设备支持相应的接口(如 SATA 或 NVMe)和规格。具体兼容性列表请参考产品页面。", | |||
"商品到着後7日以内であれば、未使用・未開封の商品に限り返品・交換が可能です。詳細は返品・交換ポリシーをご確認ください。", | |||
}, | |||
{ | |||
id: "faq-5", | |||
question: "忘记密码怎么办?", | |||
id: 5, | |||
category: "サポートについて", | |||
question: "技術サポートはどのように受けられますか?", | |||
answer: | |||
'如果是指 Hanye 相关的在线服务账户密码,请使用"忘记密码"功能进行重置。如果是加密 U 盘或 SSD 的密码,很抱歉,为了数据安全,我们无法提供密码破解服务。', | |||
"メール、電話、オンラインチャットなど、様々な方法で技術サポートをご利用いただけます。営業時間内であれば、即時対応いたします。", | |||
}, | |||
{ | |||
id: 6, | |||
category: "サポートについて", | |||
question: "修理依頼はどのように行えばよいですか?", | |||
answer: | |||
"修理依頼は、当社ウェブサイトの修理依頼フォームからお申し込みください。修理状況はマイページから確認できます。", | |||
}, | |||
]); | |||
// Filter FAQs based on actual string content | |||
// 分类列表 | |||
const categories = ref([ | |||
"すべて", | |||
"製品について", | |||
"購入について", | |||
"サポートについて", | |||
]); | |||
// 选中的分类 | |||
const selectedCategory = ref("すべて"); | |||
// 展开的FAQ ID列表 | |||
const expandedFaqs = ref<number[]>([]); | |||
// 搜索关键词 | |||
const searchTerm = ref(""); | |||
const searchInputRef = ref<HTMLInputElement | null>(null); | |||
// 过滤后的FAQ列表 | |||
const filteredFaqs = computed(() => { | |||
if (!searchTerm.value) { | |||
return faqs.value; | |||
let result = faqs.value; | |||
if (selectedCategory.value !== "すべて") { | |||
result = result.filter( | |||
(faq: FAQ) => faq.category === selectedCategory.value | |||
); | |||
} | |||
const lowerSearchTerm = searchTerm.value.toLowerCase(); | |||
return faqs.value.filter( | |||
(faq: FaqItem) => | |||
faq.question.toLowerCase().includes(lowerSearchTerm) || | |||
faq.answer.toLowerCase().includes(lowerSearchTerm) | |||
); | |||
if (searchTerm.value.trim()) { | |||
const keyword = searchTerm.value.trim().toLowerCase(); | |||
result = result.filter( | |||
(faq: FAQ) => | |||
faq.question.toLowerCase().includes(keyword) || | |||
faq.answer.toLowerCase().includes(keyword) | |||
); | |||
} | |||
return result; | |||
}); | |||
// Toggle accordion item (modified for Set) | |||
const toggleAccordion = (id: string) => { | |||
if (openAccordionIds.value.has(id)) { | |||
openAccordionIds.value.delete(id); | |||
/** | |||
* 高亮显示匹配的关键字 | |||
* @param text 原始文本 | |||
* @returns 高亮后的VNode数组 | |||
*/ | |||
function highlightKeyword(text: string): (string | any)[] { | |||
const keyword = searchTerm.value.trim(); | |||
if (!keyword) return [text]; | |||
// 构建正则,忽略大小写,转义特殊字符 | |||
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | |||
const reg = new RegExp(escaped, "gi"); | |||
const parts = text.split(reg); | |||
const matches = text.match(reg); | |||
if (!matches) return [text]; | |||
// 组装高亮 | |||
const result: (string | any)[] = []; | |||
parts.forEach((part, i) => { | |||
result.push(part); | |||
if (i < matches.length) { | |||
result.push( | |||
h( | |||
"span", | |||
{ class: "text-blue-400 bg-blue-400/10 font-bold" }, | |||
matches[i] | |||
) | |||
); | |||
} | |||
}); | |||
return result; | |||
} | |||
/** | |||
* 切换FAQ展开状态 | |||
*/ | |||
function toggleFaq(id: number) { | |||
const index = expandedFaqs.value.indexOf(id); | |||
if (index === -1) { | |||
expandedFaqs.value.push(id); | |||
} else { | |||
openAccordionIds.value.add(id); | |||
expandedFaqs.value.splice(index, 1); | |||
} | |||
}; | |||
} | |||
/** | |||
* 处理分类筛选 | |||
*/ | |||
function handleCategoryFilter(category: string) { | |||
selectedCategory.value = category; | |||
} | |||
// 自动展开匹配项 | |||
watch([ | |||
filteredFaqs, | |||
searchTerm | |||
], ([faqs, keyword]: [FAQ[], string]) => { | |||
if (keyword.trim()) { | |||
expandedFaqs.value = faqs.map((faq: FAQ) => faq.id); | |||
} else { | |||
expandedFaqs.value = []; | |||
} | |||
}); | |||
function clearSearch() { | |||
searchTerm.value = ""; | |||
// 让输入框重新聚焦 | |||
searchInputRef.value?.focus(); | |||
} | |||
// SEO (Still uses i18n) | |||
// SEO优化 | |||
useHead({ | |||
title: t("faq.meta.title"), | |||
title: "常见问题 - Hanye", | |||
meta: [ | |||
{ | |||
name: "description", | |||
content: t("faq.meta.description"), | |||
content: "浏览常见问题,获取产品使用和购买相关的帮助。", | |||
}, | |||
], | |||
}); | |||
</script> | |||
<style scoped> | |||
/* 添加过渡动画 */ | |||
.fade-enter-active, | |||
.fade-leave-active { | |||
transition: opacity 0.3s; | |||
} | |||
.fade-enter-from, | |||
.fade-leave-to { | |||
opacity: 0; | |||
} | |||
</style> |
@@ -1,176 +1,394 @@ | |||
<template> | |||
<div class="py-8"> | |||
<div class="container-custom"> | |||
<div class="mb-4"> | |||
<NuxtLink to="/products" class="text-blue-600 hover:underline flex items-center"> | |||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> | |||
</svg> | |||
{{ $t('products.title') }} | |||
</NuxtLink> | |||
<div> | |||
<div class="w-full h-[55px] sm:h-[72px]"></div> | |||
<ErrorBoundary :error="error"> | |||
<div v-if="isLoading" class="flex justify-center py-12"> | |||
<!-- 加载中 --> | |||
<div | |||
class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" | |||
></div> | |||
</div> | |||
<ErrorBoundary :error="error"> | |||
<div v-if="isLoading" class="flex justify-center py-12"> | |||
<!-- 加载中 --> | |||
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div> | |||
</div> | |||
<div v-else-if="product" class="bg-white border border-gray-200 rounded-lg overflow-hidden"> | |||
<div class="h-64 bg-gray-100 flex items-center justify-center"> | |||
<!-- 产品图片占位符 --> | |||
<span class="text-gray-400 text-xl">{{ product.title }}</span> | |||
<div v-else> | |||
<!-- 面包屑导航 --> | |||
<div class="max-w-full mb-6 xl:px-2 lg:px-2 md:px-4 px-4 mt-6"> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<nuxt-link | |||
to="/" | |||
class="justify-start text-white/60 text-base font-normal" | |||
>ホーム</nuxt-link | |||
> | |||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||
<nuxt-link to="/products" class="text-white/60 text-base font-normal" | |||
>製品一覧</nuxt-link | |||
> | |||
<span class="text-white/60 text-base font-normal px-2"> / </span> | |||
<span class="text-white text-base font-normal">{{ product?.name }}</span> | |||
</div> | |||
<div class="p-8"> | |||
<h1 class="text-3xl font-bold mb-4">{{ product.title }}</h1> | |||
<p class="text-gray-600 mb-6">{{ product.description }}</p> | |||
<div class="border-t border-gray-200 pt-6 mt-6"> | |||
<h2 class="text-xl font-semibold mb-4">产品特点</h2> | |||
<ul class="list-disc pl-5 space-y-2"> | |||
<li>高品质材料,经久耐用</li> | |||
<li>精心设计,使用便捷</li> | |||
<li>多种配置可选,满足不同需求</li> | |||
<li>售后服务完善,解决后顾之忧</li> | |||
</ul> | |||
</div> | |||
<div class="border-t border-gray-200 pt-6 mt-6"> | |||
<h2 class="text-xl font-semibold mb-4">技术规格</h2> | |||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |||
<div class="flex items-center"> | |||
<span class="font-medium mr-2">尺寸:</span> | |||
<span>200 x 300 x 100 mm</span> | |||
</div> | |||
<!-- 产品详情内容 --> | |||
<div class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"> | |||
<div class="max-w-screen-2xl mx-auto"> | |||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16"> | |||
<!-- 左侧产品图片 --> | |||
<div class="flex flex-col gap-6"> | |||
<!-- 主图展示 --> | |||
<div | |||
class="bg-zinc-900 rounded-lg p-8 relative overflow-hidden group aspect-square" | |||
> | |||
<!-- 加载状态 --> | |||
<div v-if="isImageLoading" class="absolute inset-0 flex items-center justify-center bg-zinc-800/50 z-10"> | |||
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div> | |||
</div> | |||
<!-- 主图容器 --> | |||
<div class="relative w-full h-full"> | |||
<!-- 当前图片 --> | |||
<img | |||
:src="currentImage" | |||
:alt="product?.name" | |||
class="absolute inset-0 w-full h-full object-contain rounded-lg transition-all duration-500" | |||
:class="{ | |||
'opacity-0': isImageLoading, | |||
'opacity-100': !isImageLoading | |||
}" | |||
@load="handleImageLoad" | |||
@error="handleImageError" | |||
/> | |||
<!-- 预加载图片 --> | |||
<img | |||
v-if="preloadImage" | |||
:src="preloadImage" | |||
class="absolute inset-0 w-full h-full object-contain rounded-lg opacity-0" | |||
@load="handlePreloadComplete" | |||
/> | |||
</div> | |||
<!-- 错误提示 --> | |||
<div | |||
v-if="imageError" | |||
class="absolute inset-0 flex items-center justify-center bg-red-900/50 z-20" | |||
> | |||
<div class="flex flex-col items-center gap-2"> | |||
<span class="text-white">画像の読み込みに失敗しました</span> | |||
<button | |||
@click.stop="retryLoadImage" | |||
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-300" | |||
> | |||
再試行 | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="flex items-center"> | |||
<span class="font-medium mr-2">重量:</span> | |||
<span>2.5 kg</span> | |||
<!-- 缩略图列表 --> | |||
<div class="flex gap-4 overflow-x-auto pb-2 scrollbar-hide"> | |||
<div | |||
v-for="(image, index) in [product?.image, ...(product?.gallery || [])]" | |||
:key="index" | |||
@click="changeImage(image)" | |||
class="flex-shrink-0 w-20 h-20 cursor-pointer rounded-lg transition-all duration-300 relative group aspect-square p-0.5" | |||
:class="{ | |||
'bg-gradient-to-r from-blue-500 to-blue-600': currentImage === image, | |||
'hover:bg-gradient-to-r hover:from-blue-500/50 hover:to-blue-600/50': currentImage !== image, | |||
'opacity-50': isThumbnailLoading[index] || thumbnailErrors[index] | |||
}" | |||
> | |||
<!-- 缩略图加载状态 --> | |||
<div v-if="isThumbnailLoading[index]" class="absolute inset-0 flex items-center justify-center bg-zinc-800 rounded-lg"> | |||
<div class="animate-spin h-4 w-4 border-2 border-blue-500 rounded-full border-t-transparent"></div> | |||
</div> | |||
<!-- 缩略图遮罩 --> | |||
<div | |||
class="absolute inset-0 bg-black/0 transition-all duration-300 rounded-lg" | |||
:class="{ | |||
'bg-black/30': currentImage === image, | |||
'group-hover:bg-black/20': currentImage !== image | |||
}" | |||
></div> | |||
<img | |||
:src="image" | |||
:alt="`${product?.name} - 画像 ${index + 1}`" | |||
class="w-full h-full object-cover transition-all duration-300 rounded-lg" | |||
:class="{ | |||
'opacity-0': isThumbnailLoading[index], | |||
'opacity-100': !isThumbnailLoading[index], | |||
'group-hover:scale-110': currentImage !== image | |||
}" | |||
@load="handleThumbnailLoad(index)" | |||
@error="handleThumbnailError(index)" | |||
/> | |||
<!-- 选中标记 --> | |||
<div | |||
v-if="currentImage === image" | |||
class="absolute top-1 right-1 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center" | |||
> | |||
<div class="w-2 h-2 bg-white rounded-full"></div> | |||
</div> | |||
<!-- 缩略图错误提示 --> | |||
<div | |||
v-if="thumbnailErrors[index]" | |||
class="absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg" | |||
> | |||
<div class="flex flex-col items-center gap-1"> | |||
<span class="text-white text-xs">エラー</span> | |||
<button | |||
@click.stop="retryLoadThumbnail(index)" | |||
class="px-2 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600 transition-colors duration-300" | |||
> | |||
再試行 | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- 右侧产品信息 --> | |||
<div class="flex flex-col gap-8"> | |||
<!-- 产品名称 --> | |||
<div class="bg-zinc-900 rounded-lg p-6"> | |||
<h1 class="text-white text-3xl font-medium mb-4"> | |||
{{ product?.name }} | |||
</h1> | |||
<div class="text-stone-400 text-lg leading-relaxed"> | |||
{{ product?.description }} | |||
</div> | |||
</div> | |||
<div class="flex items-center"> | |||
<span class="font-medium mr-2">材质:</span> | |||
<span>高级金属合金</span> | |||
<!-- 产品参数 --> | |||
<div class="bg-zinc-900 rounded-lg p-6"> | |||
<h2 class="text-white text-xl font-medium mb-6">製品仕様</h2> | |||
<div class="grid grid-cols-1 gap-4"> | |||
<div class="flex justify-between items-center py-2 border-b border-zinc-800"> | |||
<span class="text-stone-400">カテゴリー</span> | |||
<span class="text-white font-medium">{{ product?.category }}</span> | |||
</div> | |||
<div class="flex justify-between items-center py-2 border-b border-zinc-800"> | |||
<span class="text-stone-400">用途</span> | |||
<span class="text-white font-medium">{{ product?.usage }}</span> | |||
</div> | |||
<div class="flex justify-between items-center py-2"> | |||
<span class="text-stone-400">容量</span> | |||
<span class="text-white font-medium">{{ product?.capacities.join(" / ") }}</span> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="flex items-center"> | |||
<span class="font-medium mr-2">颜色:</span> | |||
<span>银色、黑色、金色</span> | |||
<!-- 产品描述 --> | |||
<div class="bg-zinc-900 rounded-lg p-6"> | |||
<h2 class="text-white text-xl font-medium mb-6">製品説明</h2> | |||
<div class="text-stone-400 leading-relaxed space-y-4"> | |||
<p>{{ product?.description }}</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="mt-8"> | |||
<button class="btn btn-primary"> | |||
联系我们了解更多 | |||
</button> | |||
</div> | |||
</div> | |||
</div> | |||
<div v-else class="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 rounded-md"> | |||
产品不存在或已被删除 | |||
</div> | |||
</ErrorBoundary> | |||
</div> | |||
</div> | |||
</ErrorBoundary> | |||
</div> | |||
</template> | |||
<script setup lang="ts"> | |||
/** | |||
* 产品详情页面 | |||
* 展示单个产品的详细信息 | |||
* 展示产品主图、参数和描述 | |||
*/ | |||
import { ref, onMounted } from 'vue'; | |||
import { useErrorHandler } from '~/composables/useErrorHandler'; | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
// 产品接口定义 | |||
interface Product { | |||
id: number; | |||
title: string; | |||
name: string; | |||
category: string; | |||
usage: string; | |||
capacities: string[]; | |||
image: string; | |||
description: string; | |||
gallery?: string[]; // 添加相册图片数组 | |||
} | |||
const route = useRoute(); | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const route = useRoute(); | |||
const product = ref<Product | null>(null); | |||
// 获取产品ID | |||
const productId = computed(() => { | |||
const id = route.params.id; | |||
return typeof id === 'string' ? parseInt(id, 10) : -1; | |||
}); | |||
const currentImage = ref<string>(""); | |||
const isImageLoading = ref(true); | |||
const isThumbnailLoading = ref<boolean[]>([]); | |||
const imageError = ref(false); | |||
const thumbnailErrors = ref<boolean[]>([]); | |||
const preloadImage = ref<string | null>(null); | |||
/** | |||
* 加载产品详情数据 | |||
* 加载产品详情 | |||
*/ | |||
async function loadProduct() { | |||
if (productId.value <= 0) { | |||
error.value = new Error('无效的产品ID'); | |||
return; | |||
} | |||
await wrapAsync(async () => { | |||
// 模拟API请求延迟 | |||
await new Promise(resolve => setTimeout(resolve, 500)); | |||
// 模拟数据,实际项目中应从API获取 | |||
const mockProducts = [ | |||
{ | |||
id: 1, | |||
title: '产品一', | |||
description: '这是产品一的详细描述,介绍了产品的功能、特点和适用场景。产品一是我们公司的明星产品,采用了最新技术,具有高效、稳定的特点,广泛应用于各种场景。' | |||
}, | |||
{ | |||
id: 2, | |||
title: '产品二', | |||
description: '这是产品二的详细描述,介绍了产品的功能、特点和适用场景。产品二是我们的经济型产品,性价比高,适合中小型企业使用。' | |||
}, | |||
{ | |||
id: 3, | |||
title: '产品三', | |||
description: '这是产品三的详细描述,介绍了产品的功能、特点和适用场景。产品三专为高端用户设计,提供了全方位的定制服务和专业支持。' | |||
}, | |||
{ | |||
id: 4, | |||
title: '产品四', | |||
description: '这是产品四的详细描述,介绍产品的功能、特点和适用场景。产品四采用模块化设计,可以根据需求进行灵活配置。' | |||
}, | |||
{ | |||
id: 5, | |||
title: '产品五', | |||
description: '这是产品五的详细描述,介绍产品的功能、特点和适用场景。产品五是新一代智能产品,具有自学习能力和远程控制功能。' | |||
}, | |||
{ | |||
id: 6, | |||
title: '产品六', | |||
description: '这是产品六的详细描述,介绍产品的功能、特点和适用场景。产品六是我们的入门级产品,简单易用,适合初学者。' | |||
} | |||
]; | |||
const foundProduct = mockProducts.find(p => p.id === productId.value); | |||
if (foundProduct) { | |||
product.value = foundProduct; | |||
} else { | |||
error.value = new Error('未找到该产品'); | |||
} | |||
return product.value; | |||
const id = route.params.id; | |||
const response = await $fetch<Product>(`/api/products/${id}`); | |||
product.value = response; | |||
currentImage.value = response.image; | |||
return response; | |||
}); | |||
} | |||
/** | |||
* 预加载下一张图片 | |||
*/ | |||
function preloadNextImage(image: string) { | |||
preloadImage.value = image; | |||
} | |||
/** | |||
* 处理预加载完成 | |||
*/ | |||
function handlePreloadComplete() { | |||
preloadImage.value = null; | |||
} | |||
/** | |||
* 处理图片加载完成 | |||
*/ | |||
function handleImageLoad() { | |||
isImageLoading.value = false; | |||
imageError.value = false; | |||
} | |||
/** | |||
* 处理图片加载错误 | |||
*/ | |||
function handleImageError() { | |||
isImageLoading.value = false; | |||
imageError.value = true; | |||
} | |||
/** | |||
* 重试加载图片 | |||
*/ | |||
function retryLoadImage() { | |||
isImageLoading.value = true; | |||
imageError.value = false; | |||
// 强制重新加载图片 | |||
const img = new Image(); | |||
img.src = currentImage.value; | |||
img.onload = () => { | |||
handleImageLoad(); | |||
}; | |||
img.onerror = () => { | |||
handleImageError(); | |||
}; | |||
} | |||
/** | |||
* 重试加载缩略图 | |||
*/ | |||
function retryLoadThumbnail(index: number) { | |||
isThumbnailLoading.value[index] = true; | |||
thumbnailErrors.value[index] = false; | |||
// 强制重新加载缩略图 | |||
const img = new Image(); | |||
const images = [product.value?.image, ...(product.value?.gallery || [])]; | |||
img.src = images[index] || ''; | |||
img.onload = () => { | |||
handleThumbnailLoad(index); | |||
}; | |||
img.onerror = () => { | |||
handleThumbnailError(index); | |||
}; | |||
} | |||
/** | |||
* 处理缩略图加载完成 | |||
*/ | |||
function handleThumbnailLoad(index: number) { | |||
isThumbnailLoading.value[index] = false; | |||
thumbnailErrors.value[index] = false; | |||
} | |||
/** | |||
* 处理缩略图加载错误 | |||
*/ | |||
function handleThumbnailError(index: number) { | |||
isThumbnailLoading.value[index] = false; | |||
thumbnailErrors.value[index] = true; | |||
} | |||
/** | |||
* 切换图片 | |||
*/ | |||
function changeImage(image: string | undefined) { | |||
if (image && image !== currentImage.value) { | |||
isImageLoading.value = true; | |||
imageError.value = false; | |||
preloadNextImage(image); | |||
currentImage.value = image; | |||
} | |||
} | |||
// 页面加载时获取产品数据 | |||
onMounted(() => { | |||
loadProduct(); | |||
// 初始化缩略图加载状态数组 | |||
isThumbnailLoading.value = Array(4).fill(true); | |||
thumbnailErrors.value = Array(4).fill(false); | |||
}); | |||
// SEO优化 | |||
useHead({ | |||
title: computed(() => product.value ? `${product.value.title} - Hanye` : '产品详情 - Hanye'), | |||
useHead(() => ({ | |||
title: `${product.value?.name || "产品详情"} - Hanye`, | |||
meta: [ | |||
{ | |||
name: 'description', | |||
content: computed(() => product.value?.description || '查看产品详细信息、特点和技术规格。') | |||
} | |||
] | |||
}); | |||
</script> | |||
name: "description", | |||
content: product.value?.description || "产品详情页面", | |||
}, | |||
], | |||
})); | |||
</script> | |||
<style scoped> | |||
/* 隐藏滚动条但保持滚动功能 */ | |||
.scrollbar-hide { | |||
-ms-overflow-style: none; /* IE and Edge */ | |||
scrollbar-width: none; /* Firefox */ | |||
} | |||
.scrollbar-hide::-webkit-scrollbar { | |||
display: none; /* Chrome, Safari and Opera */ | |||
} | |||
/* 图片过渡动画 */ | |||
.main-image { | |||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||
} | |||
/* 缩略图悬停效果 */ | |||
.thumbnail-item { | |||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||
} | |||
.thumbnail-item:hover { | |||
transform: translateY(-2px); | |||
} | |||
/* 缩略图选中效果 */ | |||
.thumbnail-item.selected { | |||
transform: scale(1.05); | |||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |||
} | |||
/* 产品信息卡片效果 */ | |||
.info-card { | |||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |||
} | |||
.info-card:hover { | |||
transform: translateY(-2px); | |||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |||
} | |||
</style> |
@@ -55,37 +55,37 @@ | |||
class="col-span-1 md:col-span-2 flex flex-col gap-16 mb-8 md:mb-0" | |||
> | |||
<div class="flex flex-col gap-4"> | |||
<div class="text-white text-3xl font-medium"> | |||
製品カテゴリー | |||
</div> | |||
<div class="flex flex-col gap-4"> | |||
<div | |||
class="opacity-80 justify-start text-white text-base font-normal" | |||
> | |||
PCメモリ | |||
</div> | |||
<div class="text-white text-3xl font-medium">製品カテゴリー</div> | |||
<div class="flex flex-col gap-4 w-fit"> | |||
<div | |||
class="opacity-80 justify-start text-white text-base font-normal" | |||
v-for="category in categories" | |||
:key="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-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 | |||
}" | |||
> | |||
PCメモリ | |||
{{ category }} | |||
</div> | |||
</div> | |||
</div> | |||
<div class="flex flex-col gap-4"> | |||
<div class="text-white text-3xl font-medium"> | |||
製品カテゴリー | |||
</div> | |||
<div class="flex flex-col gap-4"> | |||
<div | |||
class="opacity-80 justify-start text-white text-base font-normal" | |||
> | |||
PCメモリ | |||
</div> | |||
<div class="text-white text-3xl font-medium">用途分類</div> | |||
<div class="flex flex-col gap-4 w-fit"> | |||
<div | |||
class="opacity-80 justify-start text-white text-base font-normal" | |||
v-for="usage in usages" | |||
:key="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-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 | |||
}" | |||
> | |||
PCメモリ | |||
{{ usage }} | |||
</div> | |||
</div> | |||
</div> | |||
@@ -93,122 +93,39 @@ | |||
<div class="col-span-1 md:col-span-8"> | |||
<div class="flex flex-col gap-16"> | |||
<div class="flex flex-col gap-4"> | |||
<div class="w-full text-white text-4xl font-normal mb-4"> | |||
2.5-inch SSD | |||
</div> | |||
<div | |||
class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" | |||
> | |||
<div class="bg-zinc-900 rounded-lg"> | |||
<div class="w-full p-8"> | |||
<img | |||
src="https://placehold.co/400x400" | |||
alt="" | |||
class="w-full h-full object-cover rounded-lg mb-4" | |||
/> | |||
<div | |||
class="text-center justify-start text-white text-xl font-normal" | |||
> | |||
Hanye Q60-256GST3 | |||
</div> | |||
<div | |||
class="text-center justify-start text-stone-400 text-base font-normal leading-normal" | |||
> | |||
256GB / 512GB / 1TB / 2TB | |||
</div> | |||
</div> | |||
</div> | |||
<div class="bg-zinc-900 rounded-lg"></div> | |||
<div class="bg-zinc-900 rounded-lg"></div> | |||
<div class="bg-zinc-900 rounded-lg"></div> | |||
</div> | |||
</div> | |||
<div class="flex flex-col gap-4"> | |||
<div class="w-full text-white text-4xl font-normal mb-4"> | |||
2.5-inch SSD | |||
</div> | |||
<div | |||
class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" | |||
> | |||
<div class="bg-zinc-900 rounded-lg"> | |||
<div class="w-full p-8"> | |||
<img | |||
src="https://placehold.co/400x400" | |||
alt="" | |||
class="w-full h-full object-cover rounded-lg mb-4" | |||
/> | |||
<div | |||
class="text-center justify-start text-white text-xl font-normal" | |||
> | |||
Hanye Q60-256GST3 | |||
</div> | |||
<div | |||
class="text-center justify-start text-stone-400 text-base font-normal leading-normal" | |||
> | |||
256GB / 512GB / 1TB / 2TB | |||
</div> | |||
</div> | |||
</div> | |||
<div class="bg-zinc-900 rounded-lg"> | |||
<div class="w-full p-8"> | |||
<img | |||
src="https://placehold.co/400x400" | |||
alt="" | |||
class="w-full h-full object-cover rounded-lg mb-4" | |||
/> | |||
<div | |||
class="text-center justify-start text-white text-xl font-normal" | |||
> | |||
Hanye Q60-256GST3 | |||
</div> | |||
<div | |||
class="text-center justify-start text-stone-400 text-base font-normal leading-normal" | |||
> | |||
256GB / 512GB / 1TB / 2TB | |||
</div> | |||
</div> | |||
<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 class="w-full text-white text-4xl font-normal mb-4"> | |||
{{ category }} | |||
</div> | |||
<div class="bg-zinc-900 rounded-lg"> | |||
<div class="w-full p-8"> | |||
<img | |||
src="https://placehold.co/400x400" | |||
alt="" | |||
class="w-full h-full object-cover rounded-lg mb-4" | |||
/> | |||
<div | |||
class="text-center justify-start text-white text-xl font-normal" | |||
> | |||
Hanye Q60-256GST3 | |||
<transition-group name="fade" tag="div" class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> | |||
<nuxt-link | |||
v-for="product in filteredProducts.filter(p => p.category === category)" | |||
:key="product.id" | |||
:to="`/products/${product.id}`" | |||
class="bg-zinc-900 rounded-lg transition-all duration-300 hover:bg-zinc-800 hover:scale-105 hover:shadow-lg hover:ring-2 hover:ring-blue-400" | |||
> | |||
<div class="w-full p-8"> | |||
<img | |||
:src="product.image" | |||
:alt="product.name" | |||
class="w-full h-full object-cover rounded-lg mb-4" | |||
/> | |||
<div | |||
class="text-center justify-start text-white text-xl font-normal" | |||
> | |||
{{ product.name }} | |||
</div> | |||
<div | |||
class="text-center justify-start text-stone-400 text-base font-normal leading-normal" | |||
> | |||
{{ product.capacities.join(" / ") }} | |||
</div> | |||
</div> | |||
<div | |||
class="text-center justify-start text-stone-400 text-base font-normal leading-normal" | |||
> | |||
256GB / 512GB / 1TB / 2TB | |||
</div> | |||
</div> | |||
</div> | |||
<div class="bg-zinc-900 rounded-lg"> | |||
<div class="w-full p-8"> | |||
<img | |||
src="https://placehold.co/400x400" | |||
alt="" | |||
class="w-full h-full object-cover rounded-lg mb-4" | |||
/> | |||
<div | |||
class="text-center justify-start text-white text-xl font-normal" | |||
> | |||
Hanye Q60-256GST3 | |||
</div> | |||
<div | |||
class="text-center justify-start text-stone-400 text-base font-normal leading-normal" | |||
> | |||
256GB / 512GB / 1TB / 2TB | |||
</div> | |||
</div> | |||
</div> | |||
</nuxt-link> | |||
</transition-group> | |||
</div> | |||
</div> | |||
</template> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -222,7 +139,7 @@ | |||
<script setup lang="ts"> | |||
/** | |||
* 产品列表页面 | |||
* 展示所有产品 | |||
* 展示所有产品,支持按分类和用途筛选 | |||
*/ | |||
import { useErrorHandler } from "~/composables/useErrorHandler"; | |||
import banner from "@/assets/images/product-banner.webp"; | |||
@@ -230,57 +147,70 @@ import banner from "@/assets/images/product-banner.webp"; | |||
// 产品接口定义 | |||
interface Product { | |||
id: number; | |||
title: string; | |||
name: string; | |||
category: string; | |||
usage: string; | |||
capacities: string[]; | |||
image: string; | |||
description: string; | |||
} | |||
interface ProductResponse { | |||
products: Product[]; | |||
categories: string[]; | |||
usages: string[]; | |||
} | |||
const { error, isLoading, wrapAsync } = useErrorHandler(); | |||
const products = ref<Product[]>([]); | |||
const allProducts = ref<Product[]>([]); | |||
const filteredProducts = ref<Product[]>([]); | |||
const categories = ref<string[]>([]); | |||
const usages = ref<string[]>([]); | |||
const selectedCategory = ref<string>(""); | |||
const selectedUsage = ref<string>(""); | |||
/** | |||
* 加载产品数据 | |||
*/ | |||
async function loadProducts() { | |||
await wrapAsync(async () => { | |||
// 模拟API请求延迟 | |||
await new Promise((resolve) => setTimeout(resolve, 500)); | |||
const response = await $fetch<ProductResponse>("/api/products"); | |||
allProducts.value = response.products; | |||
categories.value = response.categories; | |||
usages.value = response.usages; | |||
filterProducts(); | |||
return response; | |||
}); | |||
} | |||
// 模拟数据,实际项目中应从API获取 | |||
products.value = [ | |||
{ | |||
id: 1, | |||
title: "产品一", | |||
description: "这是产品一的详细描述,介绍产品特点和用途。", | |||
}, | |||
{ | |||
id: 2, | |||
title: "产品二", | |||
description: "这是产品二的详细描述,介绍产品特点和用途。", | |||
}, | |||
{ | |||
id: 3, | |||
title: "产品三", | |||
description: "这是产品三的详细描述,介绍产品特点和用途。", | |||
}, | |||
{ | |||
id: 4, | |||
title: "产品四", | |||
description: "这是产品四的详细描述,介绍产品特点和用途。", | |||
}, | |||
{ | |||
id: 5, | |||
title: "产品五", | |||
description: "这是产品五的详细描述,介绍产品特点和用途。", | |||
}, | |||
{ | |||
id: 6, | |||
title: "产品六", | |||
description: "这是产品六的详细描述,介绍产品特点和用途。", | |||
}, | |||
]; | |||
/** | |||
* 本地筛选产品 | |||
*/ | |||
function filterProducts() { | |||
let result = [...allProducts.value]; | |||
if (selectedCategory.value) { | |||
result = result.filter((p) => p.category === selectedCategory.value); | |||
} | |||
if (selectedUsage.value) { | |||
result = result.filter((p) => p.usage === selectedUsage.value); | |||
} | |||
filteredProducts.value = result; | |||
} | |||
return products.value; | |||
}); | |||
/** | |||
* 处理分类筛选 | |||
*/ | |||
function handleCategoryFilter(category: string) { | |||
selectedCategory.value = selectedCategory.value === category ? "" : category; | |||
filterProducts(); | |||
} | |||
/** | |||
* 处理用途筛选 | |||
*/ | |||
function handleUsageFilter(usage: string) { | |||
selectedUsage.value = selectedUsage.value === usage ? "" : usage; | |||
filterProducts(); | |||
} | |||
// 页面加载时获取产品数据 | |||
@@ -299,3 +229,12 @@ useHead({ | |||
], | |||
}); | |||
</script> | |||
<style scoped> | |||
.fade-enter-active, .fade-leave-active { | |||
transition: opacity 0.3s; | |||
} | |||
.fade-enter-from, .fade-leave-to { | |||
opacity: 0; | |||
} | |||
</style> |
@@ -0,0 +1,82 @@ | |||
/** | |||
* 首页轮播图数据接口 | |||
* @returns 轮播图数据列表 | |||
* | |||
* 替换真实接口说明: | |||
* 1. 替换真实接口时,需要修改以下内容: | |||
* - 将模拟数据替换为真实接口调用 | |||
* - 添加错误处理 | |||
* - 添加接口参数处理 | |||
* - 添加数据转换逻辑 | |||
* | |||
* 2. 真实接口示例: | |||
* const response = await $fetch('https://api.example.com/carousel', { | |||
* method: 'GET', | |||
* headers: { | |||
* 'Authorization': 'Bearer your-token', | |||
* 'Content-Type': 'application/json' | |||
* }, | |||
* params: { | |||
* page: 1, | |||
* limit: 10 | |||
* } | |||
* }) | |||
* | |||
* 3. 错误处理示例: | |||
* try { | |||
* const response = await $fetch('...') | |||
* return { | |||
* code: 200, | |||
* data: response.data, | |||
* message: '获取轮播图数据成功' | |||
* } | |||
* } catch (error) { | |||
* return { | |||
* code: 500, | |||
* data: [], | |||
* message: '获取轮播图数据失败' | |||
* } | |||
* } | |||
* | |||
* 4. 数据转换示例: | |||
* const transformedData = response.data.map(item => ({ | |||
* id: item.id, | |||
* title: item.title, | |||
* image: item.imageUrl, | |||
* link: `/products/${item.productId}` | |||
* })) | |||
* | |||
* 5. 接口参数处理示例: | |||
* const query = getQuery(event) | |||
* const page = Number(query.page) || 1 | |||
* const limit = Number(query.limit) || 10 | |||
*/ | |||
export default defineEventHandler(async () => { | |||
// 模拟数据 | |||
const carouselList = [ | |||
{ | |||
id: 1, | |||
title: '轮播图1', | |||
image: 'https://picsum.photos/1920/1080?random=1', | |||
link: '/products/1' | |||
}, | |||
{ | |||
id: 2, | |||
title: '轮播图2', | |||
image: 'https://picsum.photos/1920/1080?random=2', | |||
link: '/products/2' | |||
}, | |||
{ | |||
id: 3, | |||
title: '轮播图3', | |||
image: 'https://picsum.photos/1920/1080?random=3', | |||
link: '/products/3' | |||
} | |||
] | |||
return { | |||
code: 200, | |||
data: carouselList, | |||
message: '获取轮播图数据成功' | |||
} | |||
}) |
@@ -0,0 +1,86 @@ | |||
/** | |||
* 获取单个产品详情 | |||
*/ | |||
export default defineEventHandler(async (event) => { | |||
// 模拟延迟 | |||
await new Promise((resolve) => setTimeout(resolve, 500)); | |||
// 获取产品ID | |||
const id = event.context.params?.id; | |||
if (!id) { | |||
throw createError({ | |||
statusCode: 400, | |||
message: "产品ID不能为空", | |||
}); | |||
} | |||
// 模拟数据 | |||
const products = [ | |||
{ | |||
id: 1, | |||
name: "Hanye Q60-256GST3", | |||
category: "2.5-inch SSD", | |||
usage: "PC高速化・アップグレード", | |||
capacities: ["256GB", "512GB", "1TB", "2TB"], | |||
image: "https://picsum.photos/400/400?random=1", | |||
gallery: [ | |||
"https://picsum.photos/400/400?random=11", | |||
"https://picsum.photos/400/400?random=12", | |||
"https://picsum.photos/400/400?random=13", | |||
], | |||
description: "高性能2.5インチSSD、読み書き速度が速く、信頼性が高い。最新のNANDフラッシュ技術を採用し、高速なデータ転送と安定した性能を実現。PCの起動時間を大幅に短縮し、アプリケーションの読み込みを高速化。耐久性に優れ、長時間の使用にも耐えられる設計。", | |||
}, | |||
{ | |||
id: 2, | |||
name: "Hanye Q60-512GST3", | |||
category: "2.5-inch SSD", | |||
usage: "PC高速化・アップグレード", | |||
capacities: ["512GB", "1TB", "2TB"], | |||
image: "https://picsum.photos/400/400?random=2", | |||
gallery: [ | |||
"https://picsum.photos/400/400?random=21", | |||
"https://picsum.photos/400/400?random=22", | |||
"https://picsum.photos/400/400?random=23", | |||
], | |||
description: "大容量2.5インチSSD、高速転送と安定した性能を実現。512GBから2TBまでの容量オプションを提供し、様々な用途に対応。データの読み書き速度が速く、PCのパフォーマンスを大幅に向上。", | |||
}, | |||
{ | |||
id: 3, | |||
name: "Hanye Q60-1TBST3", | |||
category: "2.5-inch SSD", | |||
usage: "外付けストレージ化", | |||
capacities: ["1TB", "2TB"], | |||
image: "https://picsum.photos/400/400?random=3", | |||
gallery: [ | |||
"https://picsum.photos/400/400?random=31", | |||
"https://picsum.photos/400/400?random=32", | |||
"https://picsum.photos/400/400?random=33", | |||
], | |||
description: "大容量ストレージソリューション、データバックアップに最適。1TBと2TBの容量オプションを提供し、大量のデータ保存に対応。高速なデータ転送と安定した性能を実現。", | |||
}, | |||
{ | |||
id: 4, | |||
name: "Hanye Q60-2TBST3", | |||
category: "2.5-inch SSD", | |||
usage: "外付けストレージ化", | |||
capacities: ["2TB"], | |||
image: "https://picsum.photos/400/400?random=4", | |||
gallery: [ | |||
"https://picsum.photos/400/400?random=41", | |||
"https://picsum.photos/400/400?random=42", | |||
"https://picsum.photos/400/400?random=43", | |||
], | |||
description: "超大容量SSD、プロフェッショナル向けストレージソリューション。2TBの大容量を実現し、大量のデータ保存に対応。高速なデータ転送と安定した性能を実現。", | |||
}, | |||
]; | |||
const product = products.find((p) => p.id === Number(id)); | |||
if (!product) { | |||
throw createError({ | |||
statusCode: 404, | |||
message: "产品不存在", | |||
}); | |||
} | |||
return product; | |||
}); |
@@ -0,0 +1,66 @@ | |||
export default defineEventHandler(async (event) => { | |||
// 模拟数据 | |||
const mockData = { | |||
code: 200, | |||
message: "success", | |||
data: [ | |||
{ | |||
id: 1, | |||
title: "PC高速化", | |||
description: "2.5-inch SSD & M.2 SSD", | |||
features: [ | |||
"PC高速化", | |||
"起動・読込 高速" | |||
], | |||
image: "https://picsum.photos/seed/ssd/400/400", | |||
link: "/products" | |||
}, | |||
{ | |||
id: 2, | |||
title: "データ保存", | |||
description: "HDD & SSD", | |||
features: [ | |||
"大容量保存", | |||
"データバックアップ" | |||
], | |||
image: "https://picsum.photos/seed/hdd/400/400", | |||
link: "/products" | |||
}, | |||
{ | |||
id: 3, | |||
title: "メモリ拡張", | |||
description: "DDR4 & DDR5", | |||
features: [ | |||
"メモリ増設", | |||
"パフォーマンス向上" | |||
], | |||
image: "https://picsum.photos/seed/ram/400/400", | |||
link: "/products" | |||
}, | |||
{ | |||
id: 4, | |||
title: "周辺機器", | |||
description: "USB & Thunderbolt", | |||
features: [ | |||
"高速転送", | |||
"多機能接続" | |||
], | |||
image: "https://picsum.photos/seed/usb/400/400", | |||
link: "/products" | |||
}, | |||
{ | |||
id: 5, | |||
title: "冷却システム", | |||
description: "CPU & GPU Cooler", | |||
features: [ | |||
"効率的冷却", | |||
"静音設計" | |||
], | |||
image: "https://picsum.photos/seed/cooler/400/400", | |||
link: "/products" | |||
} | |||
] | |||
}; | |||
return mockData; | |||
}); |
@@ -0,0 +1,68 @@ | |||
/** | |||
* 获取产品列表 | |||
* 支持按分类和用途筛选 | |||
*/ | |||
export default defineEventHandler(async (event) => { | |||
// 模拟延迟 | |||
await new Promise((resolve) => setTimeout(resolve, 500)); | |||
// 获取查询参数 | |||
const query = getQuery(event); | |||
const category = query.category as string; | |||
const usage = query.usage as string; | |||
// 模拟数据 | |||
const products = [ | |||
{ | |||
id: 1, | |||
name: "Hanye Q60-256GST3", | |||
category: "2.5-inch SSD", | |||
usage: "PC高速化・アップグレード", | |||
capacities: ["256GB", "512GB", "1TB", "2TB"], | |||
image: "https://picsum.photos/400/400?random=1", | |||
description: "高性能2.5インチSSD、読み書き速度が速く、信頼性が高い", | |||
}, | |||
{ | |||
id: 2, | |||
name: "Hanye Q60-512GST3", | |||
category: "2.5-inch SSD", | |||
usage: "PC高速化・アップグレード", | |||
capacities: ["512GB", "1TB", "2TB"], | |||
image: "https://picsum.photos/400/400?random=2", | |||
description: "大容量2.5インチSSD、高速転送と安定した性能", | |||
}, | |||
{ | |||
id: 3, | |||
name: "Hanye Q60-1TBST3", | |||
category: "2.5-inch SSD", | |||
usage: "外付けストレージ化", | |||
capacities: ["1TB", "2TB"], | |||
image: "https://picsum.photos/400/400?random=3", | |||
description: "大容量ストレージソリューション、データバックアップに最適", | |||
}, | |||
{ | |||
id: 4, | |||
name: "Hanye Q60-2TBST3", | |||
category: "2.5-inch SSD", | |||
usage: "外付けストレージ化", | |||
capacities: ["2TB"], | |||
image: "https://picsum.photos/400/400?random=4", | |||
description: "超大容量SSD、プロフェッショナル向けストレージソリューション", | |||
}, | |||
]; | |||
// 筛选逻辑 | |||
let filteredProducts = [...products]; | |||
if (category) { | |||
filteredProducts = filteredProducts.filter((p) => p.category === category); | |||
} | |||
if (usage) { | |||
filteredProducts = filteredProducts.filter((p) => p.usage === usage); | |||
} | |||
return { | |||
products: filteredProducts, | |||
categories: Array.from(new Set(products.map((p) => p.category))), | |||
usages: Array.from(new Set(products.map((p) => p.usage))), | |||
}; | |||
}); |
@@ -0,0 +1,110 @@ | |||
/** | |||
* 按用途产品展示接口 | |||
* @returns 按用途分类的产品数据 | |||
*/ | |||
export default defineEventHandler(async () => { | |||
// 模拟数据 | |||
const usageList = [ | |||
{ | |||
id: 1, | |||
name: '外付けストレージ化', | |||
products: [ | |||
{ | |||
id: 1, | |||
title: 'Hanye Q60-2TST3', | |||
description: '2TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=1', | |||
link: '/products/1' | |||
}, | |||
{ | |||
id: 2, | |||
title: 'Hanye Q60-4TST3', | |||
description: '4TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=2', | |||
link: '/products/2' | |||
}, | |||
{ | |||
id: 3, | |||
title: 'Hanye Q60-8TST3', | |||
description: '8TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=3', | |||
link: '/products/3' | |||
}, | |||
{ | |||
id: 4, | |||
title: 'Hanye Q60-16TST3', | |||
description: '16TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=4', | |||
link: '/products/4' | |||
}, | |||
{ | |||
id: 5, | |||
title: 'Hanye Q60-32TST3', | |||
description: '32TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=5', | |||
link: '/products/5' | |||
} | |||
] | |||
}, | |||
{ | |||
id: 2, | |||
name: 'PC高速化', | |||
products: [ | |||
{ | |||
id: 4, | |||
title: 'Hanye Q60-2TST3', | |||
description: '2TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=4', | |||
link: '/products/4' | |||
}, | |||
{ | |||
id: 5, | |||
title: 'Hanye Q60-4TST3', | |||
description: '4TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=5', | |||
link: '/products/5' | |||
}, | |||
{ | |||
id: 6, | |||
title: 'Hanye Q60-8TST3', | |||
description: '8TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=6', | |||
link: '/products/6' | |||
} | |||
] | |||
}, | |||
{ | |||
id: 3, | |||
name: 'データバックアップ', | |||
products: [ | |||
{ | |||
id: 7, | |||
title: 'Hanye Q60-2TST3', | |||
description: '2TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=7', | |||
link: '/products/7' | |||
}, | |||
{ | |||
id: 8, | |||
title: 'Hanye Q60-4TST3', | |||
description: '4TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=8', | |||
link: '/products/8' | |||
}, | |||
{ | |||
id: 9, | |||
title: 'Hanye Q60-8TST3', | |||
description: '8TB SSD UP TO 550MB/s', | |||
image: 'https://picsum.photos/300/300?random=9', | |||
link: '/products/9' | |||
} | |||
] | |||
} | |||
] | |||
return { | |||
code: 200, | |||
data: usageList, | |||
message: '获取按用途产品数据成功' | |||
} | |||
}) |