Commit fa35f0cf933797c75441e375cf7846441bed539c

Authored by boyang
1 parent 877cab94

feat: 开发面包屑导航

components/CategoryList.vue
@@ -79,7 +79,9 @@ import { useProductListStore } from "@/stores/product_list"; @@ -79,7 +79,9 @@ import { useProductListStore } from "@/stores/product_list";
79 import type { CategoryRootType } from "@/type"; 79 import type { CategoryRootType } from "@/type";
80 import { computed } from "vue"; 80 import { computed } from "vue";
81 import { useRouter, useRoute } from "vue-router"; 81 import { useRouter, useRoute } from "vue-router";
  82 +import { useRouteQuery } from "@/stores/route_query";
82 83
  84 +const routeQuery = useRouteQuery();
83 const route = useRoute(); 85 const route = useRoute();
84 const router = useRouter(); 86 const router = useRouter();
85 87
@@ -91,11 +93,13 @@ watchEffect(async () => { @@ -91,11 +93,13 @@ watchEffect(async () => {
91 productStore.updateKeyword(""); 93 productStore.updateKeyword("");
92 const categories = route.query.categories.split(","); 94 const categories = route.query.categories.split(",");
93 const mainCategory = categories[0].trim(); // 取第一个值 95 const mainCategory = categories[0].trim(); // 取第一个值
94 - const subCategoryName = categories[1] ? categories[1].trim() : null; // 取第二个值(如果存在) 96 + const subCategoryName = categories[1]
  97 + ? categories[1].trim()
  98 + : "Not specified"; // 取第二个值(如果存在)
95 99
96 // 2. 更新选中的主分类 100 // 2. 更新选中的主分类
97 categoryStore.updateCategory(mainCategory); 101 categoryStore.updateCategory(mainCategory);
98 - 102 + routeQuery.updateCategories(mainCategory + "," + subCategoryName);
99 // 3. 如果有子分类名称,查找其对应的 ID 103 // 3. 如果有子分类名称,查找其对应的 ID
100 if (subCategoryName) { 104 if (subCategoryName) {
101 const subCategoryList = computed(() => { 105 const subCategoryList = computed(() => {
@@ -123,6 +127,7 @@ watchEffect(async () => { @@ -123,6 +127,7 @@ watchEffect(async () => {
123 // 5. 判断 query 中是否存在 function,并查找对应的 ID 127 // 5. 判断 query 中是否存在 function,并查找对应的 ID
124 if (route.query.function) { 128 if (route.query.function) {
125 const functionName = route.query.function.trim(); 129 const functionName = route.query.function.trim();
  130 + routeQuery.updateFunction(functionName);
126 const funcCategoryList = computed(() => { 131 const funcCategoryList = computed(() => {
127 if (categoryStore.selectedCategory) { 132 if (categoryStore.selectedCategory) {
128 const tmp = categoryStore.list.filter( 133 const tmp = categoryStore.list.filter(
@@ -152,8 +157,23 @@ watchEffect(async () => { @@ -152,8 +157,23 @@ watchEffect(async () => {
152 // // 使用找到的 funcCategoryId 157 // // 使用找到的 funcCategoryId
153 // categoryStore.updateFuncCategory(funcCategoryId); 158 // categoryStore.updateFuncCategory(funcCategoryId);
154 // } 159 // }
  160 + } else if (route.query.categories.includes("Energy materials")) {
  161 + const defaultCategory = categoryStore.list[0];
  162 + const defaultFuncCategory = defaultCategory.productFunctions[0];
  163 + if (defaultFuncCategory) {
  164 + categoryStore.updateFuncCategory(defaultFuncCategory.id);
  165 + }
  166 + router.push({
  167 + query: {
  168 + categories: route.query.categories,
  169 + function: defaultFuncCategory.name,
  170 + },
  171 + });
155 } 172 }
156 - } else if (Object.keys(route.query).length === 0) { 173 + } else if (
  174 + Object.keys(route.query).length === 0 &&
  175 + !route.path.includes("/products/detail")
  176 + ) {
157 // 检查是否有默认的分类 177 // 检查是否有默认的分类
158 const defaultCategory = categoryStore.list[0]; // 假设第一个分类是默认的 178 const defaultCategory = categoryStore.list[0]; // 假设第一个分类是默认的
159 179
components/ProductDetail.vue
1 <template> 1 <template>
2 <v-container> 2 <v-container>
  3 + <div>
  4 + <v-breadcrumbs divider="/" style="padding-top: 0">
  5 + <template v-for="(item, index) in items" :key="index">
  6 + <v-breadcrumbs-item
  7 + :disabled="item.disabled"
  8 + :href="item.href"
  9 + :class="{
  10 + 'breadcrumb-disabled': item.disabled,
  11 + 'breadcrumb-active': !item.disabled,
  12 + }"
  13 + >
  14 + {{ item.title }}
  15 + </v-breadcrumbs-item>
  16 +
  17 + <!-- 添加分隔符,排除最后一个 item -->
  18 + <span v-if="index < items.length - 1" class="breadcrumb-divider"
  19 + >/</span
  20 + >
  21 + </template>
  22 + <span class="breadcrumb-divider">/</span>
  23 + <span style="margin-left: 5px">{{ info.name }}</span>
  24 + </v-breadcrumbs>
  25 + </div>
3 <v-row class="mb-16 ma-0"> 26 <v-row class="mb-16 ma-0">
4 - <v-col cols="12" sm="5"> 27 + <v-col cols="12" sm="5" class="carousel-container">
5 <v-carousel 28 <v-carousel
6 - class="tw-float-left" 29 + class="tw-float-left tw-sticky tw-top-[16px]"
7 height="450" 30 height="450"
8 v-model="slide" 31 v-model="slide"
9 hide-delimiter-background 32 hide-delimiter-background
  33 + style="top: 16px"
10 > 34 >
11 <v-carousel-item 35 <v-carousel-item
12 cover 36 cover
@@ -18,10 +42,11 @@ @@ -18,10 +42,11 @@
18 </v-carousel-item> 42 </v-carousel-item>
19 </v-carousel> 43 </v-carousel>
20 </v-col> 44 </v-col>
21 - <v-col cols="12" sm="7">  
22 - <v-row class="bg-white mb-sm-10 text-h4 font-weight-medium"> 45 + <v-col cols="12" sm="7" class="table-container">
  46 + <!-- <v-row class="bg-white mb-sm-10 text-h4 font-weight-medium">
23 <v-col>{{ info.name }}</v-col> 47 <v-col>{{ info.name }}</v-col>
24 - </v-row> 48 + </v-row> -->
  49 + <h1>{{ info.name }}</h1>
25 <div class="tw-flex tw-flex-wrap"> 50 <div class="tw-flex tw-flex-wrap">
26 <div class="tw-w-1/2"> 51 <div class="tw-w-1/2">
27 <span class="tw-leading-[10px] tw-m-[16px]"> 52 <span class="tw-leading-[10px] tw-m-[16px]">
@@ -65,10 +90,15 @@ @@ -65,10 +90,15 @@
65 > 90 >
66 </div> 91 </div>
67 </div> 92 </div>
68 - <v-table 93 + <!-- <v-table
69 density="comfortable" 94 density="comfortable"
70 class="table1 tw-mt-[32px]" 95 class="table1 tw-mt-[32px]"
71 v-if="info.ticketTypes?.length" 96 v-if="info.ticketTypes?.length"
  97 + > -->
  98 + <v-table
  99 + density="comfortable"
  100 + class="table1 tw-mt-[32px] tw-overflow-x-auto"
  101 + v-if="info.ticketTypes?.length"
72 > 102 >
73 <thead> 103 <thead>
74 <tr class="bg-grey-lighten-3"> 104 <tr class="bg-grey-lighten-3">
@@ -145,7 +175,7 @@ @@ -145,7 +175,7 @@
145 <v-divider class="tw-mb-[12px]"></v-divider> 175 <v-divider class="tw-mb-[12px]"></v-divider>
146 <div v-html="info.physicalproperty"></div> 176 <div v-html="info.physicalproperty"></div>
147 </div> 177 </div>
148 - <div v-if="info.advantage" class="tw-mb-[24px]"> 178 + <div v-if="info.storage" class="tw-mb-[24px]">
149 <div class="text-h6">Storage</div> 179 <div class="text-h6">Storage</div>
150 <v-divider class="tw-mb-[12px]"></v-divider> 180 <v-divider class="tw-mb-[12px]"></v-divider>
151 <div v-html="info.storage"></div> 181 <div v-html="info.storage"></div>
@@ -155,7 +185,7 @@ @@ -155,7 +185,7 @@
155 <v-divider class="tw-mb-[12px]"></v-divider> 185 <v-divider class="tw-mb-[12px]"></v-divider>
156 <div v-html="info.introduction"></div> 186 <div v-html="info.introduction"></div>
157 </div> 187 </div>
158 - <div v-if="info.advantage" class="tw-mb-[24px]"> 188 + <div v-if="info.description" class="tw-mb-[24px]">
159 <div class="text-h6">Description</div> 189 <div class="text-h6">Description</div>
160 <v-divider class="tw-mb-[12px]"></v-divider> 190 <v-divider class="tw-mb-[12px]"></v-divider>
161 <div v-html="info.description"></div> 191 <div v-html="info.description"></div>
@@ -185,20 +215,116 @@ @@ -185,20 +215,116 @@
185 import type { Product, ProductImage } from "~/type"; 215 import type { Product, ProductImage } from "~/type";
186 import { onMounted, ref } from "vue"; 216 import { onMounted, ref } from "vue";
187 import { useDialogStore } from "~/stores/dialog"; 217 import { useDialogStore } from "~/stores/dialog";
  218 +import { useRouter, useRoute } from "vue-router";
  219 +import { useRouteQuery } from "@/stores/route_query";
  220 +
  221 +const route = useRoute();
  222 +const router = useRouter();
  223 +const routeQuery = useRouteQuery();
188 const dialogStore = useDialogStore(); 224 const dialogStore = useDialogStore();
  225 +const href1 = ref("/products");
  226 +const href2 = ref("/products");
  227 +// 定义单个 item 的接口
  228 +interface BreadcrumbItem {
  229 + title: string; // 标题
  230 + disabled: boolean; // 是否禁用
  231 + href: string; // 链接地址
  232 +}
  233 +
  234 +// 示例数据
  235 +const items = ref<BreadcrumbItem[]>([
  236 + {
  237 + title: "Products",
  238 + disabled: false,
  239 + href: "/products",
  240 + },
  241 + {
  242 + title: "CATEGORY",
  243 + disabled: false,
  244 + href: href1.value,
  245 + },
  246 + {
  247 + title: "DEVICE TYPE",
  248 + disabled: false,
  249 + href: href2.value,
  250 + },
  251 + {
  252 + title: "FUNCTION",
  253 + disabled: false,
  254 + href: "/products",
  255 + },
  256 + // {
  257 + // title: "TITLE",
  258 + // disabled: false,
  259 + // href: "",
  260 + // },
  261 +]);
189 262
190 const props = defineProps<{ 263 const props = defineProps<{
191 info: Product; 264 info: Product;
192 }>(); 265 }>();
193 const info = props.info; 266 const info = props.info;
194 - 267 +watchEffect(() => {
  268 + if (routeQuery.categories) {
  269 + if (!routeQuery.categories.includes("Energy materials")) {
  270 + routeQuery.updateFunction("Not specified");
  271 + }
  272 + const categories = routeQuery.categories.split(",");
  273 + const mainCategory = categories[0].trim(); // 取第一个值
  274 + const subCategoryName = ref(
  275 + categories[1] ? categories[1].trim() : "Not specified"
  276 + ); // 取第二个值(如果存在)
  277 + if (subCategoryName.value == "Accessories & fixtures") {
  278 + subCategoryName.value = encodeURIComponent("Accessories & fixtures");
  279 + items.value[2].title = "Accessories & fixtures";
  280 + } else {
  281 + items.value[2].title = subCategoryName.value;
  282 + }
  283 + items.value[1].title = mainCategory;
  284 + items.value[1].href = href1.value + "?categories=" + mainCategory;
  285 + href1.value = href1.value + "?categories=" + mainCategory;
  286 + // items.value[1].title = subCategoryName.value;
  287 + href2.value = href1.value + "," + subCategoryName.value;
  288 + items.value[2].href = href1.value + "," + subCategoryName.value;
  289 + if (routeQuery?.selectedFunction) {
  290 + // items.value.push({
  291 + // title: routeQuery.selectedFunction,
  292 + // disabled: false,
  293 + // href: href2.value + "&function=" + routeQuery.selectedFunction,
  294 + // });
  295 + items.value[3].title = routeQuery.selectedFunction;
  296 + items.value[3].href =
  297 + href2.value + "&function=" + routeQuery.selectedFunction;
  298 + // routeQuery.updateFunction(null);
  299 + }
  300 + } else if (info?.productCrumbsVO?.category1) {
  301 + items.value[1].title = info.productCrumbsVO.category1;
  302 + items.value[1].href =
  303 + href1.value + "?categories=" + info.productCrumbsVO.category1;
  304 + href1.value = href1.value + "?categories=" + info.productCrumbsVO.category1;
  305 + if (info?.productCrumbsVO?.category2) {
  306 + items.value[2].title = info.productCrumbsVO.category2;
  307 + href2.value = href1.value + "," + info.productCrumbsVO.category2;
  308 + items.value[2].href = href1.value + "," + info.productCrumbsVO.category2;
  309 + }
  310 + if (info?.productCrumbsVO?.function) {
  311 + // items.value.push({
  312 + // title: info.productCrumbsVO.function,
  313 + // disabled: false,
  314 + // href: href2.value + "&function=" + info.productCrumbsVO.function,
  315 + // });
  316 + items.value[3].title = info.productCrumbsVO.function;
  317 + items.value[3].href =
  318 + href2.value + "&function=" + info.productCrumbsVO.function;
  319 + }
  320 + }
  321 +});
195 // onMounted(() => { 322 // onMounted(() => {
196 // dialogStore.updateDialog(true) 323 // dialogStore.updateDialog(true)
197 // }) 324 // })
198 325
199 const tab = ref(0); 326 const tab = ref(0);
200 const slide = ref(0); 327 const slide = ref(0);
201 -const router = useRouter();  
202 </script> 328 </script>
203 329
204 <style lang="scss" scoped> 330 <style lang="scss" scoped>
@@ -231,4 +357,42 @@ th { @@ -231,4 +357,42 @@ th {
231 border-bottom: 1px solid #dcdcdc !important; 357 border-bottom: 1px solid #dcdcdc !important;
232 } 358 }
233 } 359 }
  360 +
  361 +.tw-sticky {
  362 + position: sticky;
  363 +}
  364 +
  365 +.carousel-container {
  366 + position: relative;
  367 +}
  368 +
  369 +.table-container {
  370 + overflow-x: auto; /* 防止表格超出页面宽度 */
  371 +}
  372 +
  373 +.table1 {
  374 + width: 100%;
  375 + min-width: 600px; /* 设置表格最小宽度 */
  376 +}
  377 +
  378 +.tr {
  379 + border-bottom: 1px solid #e0e0e0; /* 表格行样式 */
  380 +}
  381 +
  382 +.headerBorder {
  383 + border-bottom: 2px solid #ccc; /* 表头边框 */
  384 +}
  385 +
  386 +.text-grey-darken-4 {
  387 + color: #333; /* 表格文字颜色 */
  388 +}
  389 +.breadcrumb-disabled {
  390 + color: black;
  391 + pointer-events: none; /* 禁用点击 */
  392 + text-decoration: none; /* 移除链接样式 */
  393 +}
  394 +
  395 +.breadcrumb-active {
  396 + color: gray;
  397 +}
234 </style> 398 </style>
pages/products/detail/[id]/index.vue
@@ -11,7 +11,7 @@ import type { Product, ProductImage } from &quot;~/type&quot;; @@ -11,7 +11,7 @@ import type { Product, ProductImage } from &quot;~/type&quot;;
11 import { useRoute, useRouter } from "vue-router"; 11 import { useRoute, useRouter } from "vue-router";
12 12
13 const route = useRoute(); 13 const route = useRoute();
14 - 14 +const router = useRouter();
15 const info = ref<Partial<Product>>({ 15 const info = ref<Partial<Product>>({
16 productimageliststore: [], 16 productimageliststore: [],
17 productAttributeList: [], 17 productAttributeList: [],
@@ -44,22 +44,17 @@ watchEffect(() =&gt; { @@ -44,22 +44,17 @@ watchEffect(() =&gt; {
44 }, 44 },
45 { 45 {
46 name: "keywords", 46 name: "keywords",
47 - content:  
48 - info.value.name ||  
49 - "科路得,Equipment,High-precision,Machining center,Design,Manufacturing capabilities,Equipment supply,Production line planning,Construction services,Battery assembly lines,Pouch cell testing lines", 47 + content: info.value.name || info.value.metakeywords,
50 }, 48 },
51 { 49 {
52 name: "description", 50 name: "description",
53 - content:  
54 - info.value.name ||  
55 - "科路得,助您科研之路势在必得。Equipment Business: With our self-built high-precision machining center, we possess robust design and manufacturing capabilities. We offer comprehensive equipment supply, production line planning, and construction services, including battery assembly lines, pouch cell testing lines, and more. Our aim is to provide complete equipment solutions that cater to the diverse needs of our clients. Expect top-quality equipment and professional services that will help you stand out in a fiercely competitive market!", 51 + content: info.value.metadescription || info.value.name,
56 }, 52 },
57 ], 53 ],
58 }); 54 });
59 }); 55 });
60 56
61 const newData: Product = resData.value.data; 57 const newData: Product = resData.value.data;
62 -  
63 newData.productimageliststore = 58 newData.productimageliststore =
64 typeof newData.productimageliststore === "string" 59 typeof newData.productimageliststore === "string"
65 ? JSON.parse(newData.productimageliststore) || [] 60 ? JSON.parse(newData.productimageliststore) || []
pages/products/index.vue
@@ -86,9 +86,21 @@ const productStore = useProductListStore(); @@ -86,9 +86,21 @@ const productStore = useProductListStore();
86 const categoryStore = useCategoryStore(); 86 const categoryStore = useCategoryStore();
87 const loading = ref(false); 87 const loading = ref(false);
88 const route = useRoute(); // 获取路由信息 88 const route = useRoute(); // 获取路由信息
  89 +const router = useRouter(); // 获取路由信息
  90 +const title = ref("");
  91 +watchEffect(() => {
  92 + // 遍历数组
  93 + if (router.currentRoute.value.query.categories) {
  94 + title.value = `${router.currentRoute.value.query.categories}`;
  95 + }
  96 + if (router.currentRoute.value.query.function) {
  97 + title.value += `,${router.currentRoute.value.query.function}`;
  98 + }
  99 + // document.title = title.value;
  100 +});
89 101
90 useHead({ 102 useHead({
91 - title: "canrud", 103 + title: title.value,
92 meta: [ 104 meta: [
93 { 105 {
94 name: "title", 106 name: "title",
@@ -109,7 +121,6 @@ useHead({ @@ -109,7 +121,6 @@ useHead({
109 }); 121 });
110 122
111 const loadProducts = async () => { 123 const loadProducts = async () => {
112 - console.log(productStore, "5656productStore");  
113 let params: any = { 124 let params: any = {
114 pageNo: productStore.pageNo, 125 pageNo: productStore.pageNo,
115 pageSize: 20, 126 pageSize: 20,
stores/product_list.ts
@@ -32,7 +32,7 @@ export const useProductListStore = defineStore(&quot;productList&quot;, () =&gt; { @@ -32,7 +32,7 @@ export const useProductListStore = defineStore(&quot;productList&quot;, () =&gt; {
32 imgList: JSON.parse( 32 imgList: JSON.parse(
33 record.productimageliststore as unknown as string 33 record.productimageliststore as unknown as string
34 ).map((item: ProductImage) => ({ 34 ).map((item: ProductImage) => ({
35 - url: `/api/show/image?fileKey=${item.fileKey}&psize=p512`, 35 + url: `/api/show/image?fileKey=${item.fileKey}&psize=p256`,
36 })), 36 })),
37 })) || []; 37 })) || [];
38 total.value = data.value?.data?.total || 0; 38 total.value = data.value?.data?.total || 0;
stores/route_query.ts 0 → 100644
  1 +import { ref } from "vue";
  2 +import { defineStore } from "pinia";
  3 +
  4 +export const useRouteQuery = defineStore("routeQuery", () => {
  5 + const categories = ref();
  6 + const selectedFunction = ref();
  7 +
  8 + const updateCategories = (value: string) => {
  9 + categories.value = value;
  10 + };
  11 +
  12 + const updateFunction = (value: string | null) => {
  13 + selectedFunction.value = value;
  14 + };
  15 +
  16 + return {
  17 + categories,
  18 + selectedFunction,
  19 + updateCategories,
  20 + updateFunction,
  21 + };
  22 +});