Hanye官网
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

index.vue 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <template>
  2. <div>
  3. <div class="w-full h-[55px] sm:h-[72px]"></div>
  4. <ErrorBoundary :error="error">
  5. <div v-if="isLoading" class="flex justify-center py-12">
  6. <!-- 加载中 -->
  7. <div
  8. class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"
  9. ></div>
  10. </div>
  11. <div v-else>
  12. <div class="w-full mb-12 relative">
  13. <div class="absolute top-0 left-0 w-full h-full z-10">
  14. <div
  15. class="max-w-screen-2xl mx-auto h-full flex flex-col justify-center gap-4 p-4"
  16. >
  17. <div
  18. class="justify-start text-white text-2xl font-normal md:text-4xl lg:text-6xl"
  19. >
  20. 製品一覧
  21. </div>
  22. <div
  23. class="text-white text-sm lg:text-lg font-normal leading-loose"
  24. >
  25. 卓越した製品は、実績に裏打ちされた優れた技術と、<br />継続的な革新デザインとの融合により、生み出されます。
  26. </div>
  27. </div>
  28. </div>
  29. <img
  30. :src="banner"
  31. alt="products-banner"
  32. class="w-full object-cover h-60 lg:h-full"
  33. />
  34. </div>
  35. <div class="max-w-full mb-6 xl:px-2 lg:px-2 md:px-4 px-4">
  36. <div class="max-w-screen-2xl mx-auto">
  37. <nuxt-link
  38. to="/"
  39. class="justify-start text-white/60 text-base font-normal"
  40. >ホーム</nuxt-link
  41. >
  42. <span class="text-white/60 text-base font-normal px-2"> / </span>
  43. <nuxt-link to="/products" class="text-white text-base font-normal"
  44. >製品一覧</nuxt-link
  45. >
  46. </div>
  47. </div>
  48. <div
  49. class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"
  50. >
  51. <div class="max-w-screen-2xl mx-auto">
  52. <div class="w-full grid grid-cols-1 md:grid-cols-10 gap-8 md:gap-2">
  53. <div
  54. class="col-span-1 md:col-span-2 flex flex-col gap-16 mb-8 md:mb-0"
  55. >
  56. <div class="flex flex-col gap-4">
  57. <div class="text-white text-3xl font-medium">製品カテゴリー</div>
  58. <div class="flex flex-col gap-4 w-fit">
  59. <div
  60. v-for="category in categories"
  61. :key="category"
  62. @click="handleCategoryFilter(category)"
  63. 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"
  64. :class="{
  65. '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,
  66. 'hover:bg-zinc-800/50': selectedCategory !== category
  67. }"
  68. >
  69. {{ category }}
  70. </div>
  71. </div>
  72. </div>
  73. <div class="flex flex-col gap-4">
  74. <div class="text-white text-3xl font-medium">用途分類</div>
  75. <div class="flex flex-col gap-4 w-fit">
  76. <div
  77. v-for="usage in usages"
  78. :key="usage"
  79. @click="handleUsageFilter(usage)"
  80. 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"
  81. :class="{
  82. '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,
  83. 'hover:bg-zinc-800/50': selectedUsage !== usage
  84. }"
  85. >
  86. {{ usage }}
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. <div class="col-span-1 md:col-span-8">
  92. <div class="flex flex-col gap-16">
  93. <template v-for="category in categories" :key="category">
  94. <div v-if="filteredProducts.filter(p => p.category === category).length > 0" class="flex flex-col gap-4">
  95. <div class="w-full text-white text-4xl font-normal mb-4">
  96. {{ category }}
  97. </div>
  98. <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">
  99. <nuxt-link
  100. v-for="product in filteredProducts.filter(p => p.category === category)"
  101. :key="product.id"
  102. :to="`/products/${product.id}`"
  103. 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"
  104. >
  105. <div class="w-full p-8">
  106. <img
  107. :src="product.image"
  108. :alt="product.name"
  109. class="w-full h-full object-cover rounded-lg mb-4"
  110. />
  111. <div
  112. class="text-center justify-start text-white text-xl font-normal"
  113. >
  114. {{ product.name }}
  115. </div>
  116. <div
  117. class="text-center justify-start text-stone-400 text-base font-normal leading-normal"
  118. >
  119. {{ product.capacities.join(" / ") }}
  120. </div>
  121. </div>
  122. </nuxt-link>
  123. </transition-group>
  124. </div>
  125. </template>
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. </div>
  131. </div>
  132. </ErrorBoundary>
  133. </div>
  134. </template>
  135. <script setup lang="ts">
  136. /**
  137. * 产品列表页面
  138. * 展示所有产品,支持按分类和用途筛选
  139. */
  140. import { useErrorHandler } from "~/composables/useErrorHandler";
  141. import banner from "@/assets/images/product-banner.webp";
  142. // 产品接口定义
  143. interface Product {
  144. id: number;
  145. name: string;
  146. category: string;
  147. usage: string;
  148. capacities: string[];
  149. image: string;
  150. description: string;
  151. }
  152. interface ProductResponse {
  153. products: Product[];
  154. categories: string[];
  155. usages: string[];
  156. }
  157. const { error, isLoading, wrapAsync } = useErrorHandler();
  158. const allProducts = ref<Product[]>([]);
  159. const filteredProducts = ref<Product[]>([]);
  160. const categories = ref<string[]>([]);
  161. const usages = ref<string[]>([]);
  162. const selectedCategory = ref<string>("");
  163. const selectedUsage = ref<string>("");
  164. /**
  165. * 加载产品数据
  166. */
  167. async function loadProducts() {
  168. await wrapAsync(async () => {
  169. const response = await $fetch<ProductResponse>("/api/products");
  170. allProducts.value = response.products;
  171. categories.value = response.categories;
  172. usages.value = response.usages;
  173. filterProducts();
  174. return response;
  175. });
  176. }
  177. /**
  178. * 本地筛选产品
  179. */
  180. function filterProducts() {
  181. let result = [...allProducts.value];
  182. if (selectedCategory.value) {
  183. result = result.filter((p) => p.category === selectedCategory.value);
  184. }
  185. if (selectedUsage.value) {
  186. result = result.filter((p) => p.usage === selectedUsage.value);
  187. }
  188. filteredProducts.value = result;
  189. }
  190. /**
  191. * 处理分类筛选
  192. */
  193. function handleCategoryFilter(category: string) {
  194. selectedCategory.value = selectedCategory.value === category ? "" : category;
  195. filterProducts();
  196. }
  197. /**
  198. * 处理用途筛选
  199. */
  200. function handleUsageFilter(usage: string) {
  201. selectedUsage.value = selectedUsage.value === usage ? "" : usage;
  202. filterProducts();
  203. }
  204. // 页面加载时获取产品数据
  205. onMounted(() => {
  206. loadProducts();
  207. });
  208. // SEO优化
  209. useHead({
  210. title: "产品列表 - Hanye",
  211. meta: [
  212. {
  213. name: "description",
  214. content: "浏览我们的产品列表,找到适合您的解决方案。",
  215. },
  216. ],
  217. });
  218. </script>
  219. <style scoped>
  220. .fade-enter-active, .fade-leave-active {
  221. transition: opacity 0.3s;
  222. }
  223. .fade-enter-from, .fade-leave-to {
  224. opacity: 0;
  225. }
  226. </style>