Hanye官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.vue 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. {{ $t("products.product_list") }}
  21. </div>
  22. <div
  23. class="text-white text-sm lg:text-lg font-normal leading-loose"
  24. >
  25. {{ $t("products.product_list_description") }}
  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. >{{ $t("common.home") }}</nuxt-link
  41. >
  42. <span class="text-white/60 text-base font-normal px-2"> / </span>
  43. <nuxt-link
  44. to="/products"
  45. class="text-white text-base font-normal"
  46. >{{ $t("products.product_list") }}</nuxt-link
  47. >
  48. </div>
  49. </div>
  50. <div
  51. class="max-w-full mb-12 md:mb-20 lg:mb-32 xl:px-2 lg:px-2 md:px-4 px-4"
  52. >
  53. <div class="max-w-screen-2xl mx-auto">
  54. <div class="w-full grid grid-cols-1 md:grid-cols-10 gap-8 md:gap-2">
  55. <div
  56. class="col-span-1 md:col-span-2 flex flex-col gap-16 mb-8 md:mb-0"
  57. >
  58. <div class="flex flex-col gap-4">
  59. <div class="text-white text-3xl font-medium">
  60. {{ $t("products.product_categories_title") }}
  61. </div>
  62. <div class="flex flex-col gap-4 w-fit">
  63. <div
  64. v-for="category in categories"
  65. :key="category"
  66. @click="handleCategoryFilter(category)"
  67. 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"
  68. :class="{
  69. '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':
  70. selectedCategory === category,
  71. 'hover:bg-zinc-800/50': selectedCategory !== category,
  72. }"
  73. >
  74. {{ category }}
  75. </div>
  76. </div>
  77. </div>
  78. <div class="flex flex-col gap-4">
  79. <div class="text-white text-3xl font-medium">
  80. {{ $t("products.product_categories_usage") }}
  81. </div>
  82. <div class="flex flex-col gap-4 w-fit">
  83. <div
  84. v-for="usage in usages"
  85. :key="usage"
  86. @click="handleUsageFilter(usage)"
  87. 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"
  88. :class="{
  89. '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':
  90. selectedUsage === usage,
  91. 'hover:bg-zinc-800/50': selectedUsage !== usage,
  92. }"
  93. >
  94. {{ usage }}
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. <div class="col-span-1 md:col-span-8">
  100. <div class="flex flex-col gap-16">
  101. <template v-for="category in categories" :key="category">
  102. <div
  103. v-if="
  104. filteredProducts.filter((p) => p.category === category)
  105. .length > 0
  106. "
  107. class="flex flex-col gap-4"
  108. >
  109. <div class="w-full text-white text-4xl font-normal mb-4">
  110. {{ category }}
  111. </div>
  112. <transition-group
  113. name="fade"
  114. tag="div"
  115. class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"
  116. >
  117. <nuxt-link
  118. v-for="product in filteredProducts.filter(
  119. (p) => p.category === category
  120. )"
  121. :key="product.id"
  122. :to="`/products/${product.id}`"
  123. 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"
  124. >
  125. <div class="w-full p-8">
  126. <img
  127. :src="product.image"
  128. :alt="product.name"
  129. class="w-full h-full object-cover rounded-lg mb-4"
  130. />
  131. <div
  132. class="text-center justify-start text-white text-xl font-normal"
  133. >
  134. {{ product.name }}
  135. </div>
  136. <div
  137. class="text-center justify-start text-stone-400 text-base font-normal leading-normal"
  138. >
  139. {{ product.capacities.join(" / ") }}
  140. </div>
  141. </div>
  142. </nuxt-link>
  143. </transition-group>
  144. </div>
  145. </template>
  146. </div>
  147. </div>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. </ErrorBoundary>
  153. </div>
  154. </template>
  155. <script setup lang="ts">
  156. /**
  157. * 产品列表页面
  158. * 展示所有产品,支持按分类和用途筛选
  159. */
  160. import { useErrorHandler } from "~/composables/useErrorHandler";
  161. import banner from "@/assets/images/product-banner.webp";
  162. // 产品接口定义
  163. interface Product {
  164. id: number;
  165. name: string;
  166. category: string;
  167. usage: string;
  168. capacities: string[];
  169. image: string;
  170. description: string;
  171. }
  172. interface ProductResponse {
  173. products: Product[];
  174. categories: string[];
  175. usages: string[];
  176. }
  177. const { error, isLoading, wrapAsync } = useErrorHandler();
  178. const allProducts = ref<Product[]>([]);
  179. const filteredProducts = ref<Product[]>([]);
  180. const categories = ref<string[]>([]);
  181. const usages = ref<string[]>([]);
  182. const selectedCategory = ref<string>("");
  183. const selectedUsage = ref<string>("");
  184. /**
  185. * 加载产品数据
  186. */
  187. async function loadProducts() {
  188. await wrapAsync(async () => {
  189. const response = await $fetch<ProductResponse>("/api/products");
  190. allProducts.value = response.products;
  191. categories.value = response.categories;
  192. usages.value = response.usages;
  193. filterProducts();
  194. return response;
  195. });
  196. }
  197. /**
  198. * 本地筛选产品
  199. */
  200. function filterProducts() {
  201. let result = [...allProducts.value];
  202. if (selectedCategory.value) {
  203. result = result.filter((p) => p.category === selectedCategory.value);
  204. }
  205. if (selectedUsage.value) {
  206. result = result.filter((p) => p.usage === selectedUsage.value);
  207. }
  208. filteredProducts.value = result;
  209. }
  210. /**
  211. * 处理分类筛选
  212. */
  213. function handleCategoryFilter(category: string) {
  214. selectedCategory.value = selectedCategory.value === category ? "" : category;
  215. filterProducts();
  216. }
  217. /**
  218. * 处理用途筛选
  219. */
  220. function handleUsageFilter(usage: string) {
  221. selectedUsage.value = selectedUsage.value === usage ? "" : usage;
  222. filterProducts();
  223. }
  224. // 页面加载时获取产品数据
  225. onMounted(() => {
  226. loadProducts();
  227. });
  228. // SEO优化
  229. useHead({
  230. title: "产品列表 - Hanye",
  231. meta: [
  232. {
  233. name: "description",
  234. content: "浏览我们的产品列表,找到适合您的解决方案。",
  235. },
  236. ],
  237. });
  238. </script>
  239. <style scoped>
  240. .fade-enter-active,
  241. .fade-leave-active {
  242. transition: opacity 0.3s;
  243. }
  244. .fade-enter-from,
  245. .fade-leave-to {
  246. opacity: 0;
  247. }
  248. </style>