瀏覽代碼

feat: 更新页面样式和功能,提升用户体验

- 修改了styles.css文件,优化了字体导入和选择样式。
- 在TheHeader组件中调整了下拉菜单的样式和交互逻辑,提升了用户体验。
- 更新了about.vue、contact.vue和faq.vue页面,增加了加载状态和错误边界处理,改善了用户体验。
- 在index.vue和products/index.vue中优化了产品展示逻辑,增强了页面的响应式设计。
- 新增了多个API接口,支持产品数据的动态加载和分类筛选功能,提升了数据交互的灵活性。
- 完善了产品详情页的加载状态和错误处理,确保用户在浏览时的流畅体验。
master
lizhuang 1 月之前
父節點
當前提交
2fa9662053

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

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



+ 13
- 35
components/TheHeader.vue 查看文件

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

+ 229
- 215
pages/about.vue 查看文件

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

+ 252
- 441
pages/contact.vue 查看文件

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

+ 266
- 164
pages/faq.vue 查看文件

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

+ 513
- 426
pages/index.vue
文件差異過大導致無法顯示
查看文件


+ 351
- 133
pages/products/[id].vue 查看文件

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

+ 114
- 175
pages/products/index.vue 查看文件

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

+ 82
- 0
server/api/home.ts 查看文件

@@ -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: '获取轮播图数据成功'
}
})

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

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

+ 66
- 0
server/api/products/category.get.ts 查看文件

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

+ 68
- 0
server/api/products/index.get.ts 查看文件

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

+ 110
- 0
server/api/products/usage.ts 查看文件

@@ -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: '获取按用途产品数据成功'
}
})

Loading…
取消
儲存