Forráskód Böngészése

Merge branch 'refs/heads/test' into xinyan

xinyan 9 hónapja
szülő
commit
f22815db5f

+ 2 - 2
.env.development

@@ -5,8 +5,8 @@ ENV = 'development'
 # 本地环境接口地址
 # VITE_API_URL = 'http://127.0.0.1:8000'
  VITE_API_URL = 'http://192.168.1.225/'
-# VITE_API_URL = 'http://192.168.1.27:8080/'
-# VITE_API_URL = 'https://ads.vzzon.com'
+# VITE_API_URL = 'http://192.168.1.16:8080/'
+# VITE_API_URL = 'http://amzads.zositechc.cn'
 
 # 是否启用按钮权限
 VITE_PM_ENABLED = true

+ 221 - 0
package-lock.json

@@ -73,6 +73,7 @@
 				"unplugin-auto-import": "^0.16.7",
 				"unplugin-vue-components": "^0.25.2",
 				"vite": "^4.0.0",
+				"vite-plugin-compression": "^0.5.1",
 				"vite-plugin-vue-setup-extend": "^0.4.0",
 				"vue-eslint-parser": "^9.1.0"
 			},
@@ -6598,6 +6599,20 @@
 				"node": "*"
 			}
 		},
+		"node_modules/fs-extra": {
+			"version": "10.1.0",
+			"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+			"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+			"dev": true,
+			"dependencies": {
+				"graceful-fs": "^4.2.0",
+				"jsonfile": "^6.0.1",
+				"universalify": "^2.0.0"
+			},
+			"engines": {
+				"node": ">=12"
+			}
+		},
 		"node_modules/fs.realpath": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6739,6 +6754,12 @@
 				"get-intrinsic": "^1.1.3"
 			}
 		},
+		"node_modules/graceful-fs": {
+			"version": "4.2.11",
+			"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+			"dev": true
+		},
 		"node_modules/graphemer": {
 			"version": "1.4.0",
 			"resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
@@ -7253,6 +7274,18 @@
 			"resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.29.0.tgz",
 			"integrity": "sha512-TyTe22nW1rUi7bzbGwLwg/6EN88CJuxUO0193nv/6cQ8lMBV6XtfeQIAgU3dkr8wnCn2okycqHjCDlPGNcWqoQ=="
 		},
+		"node_modules/jsonfile": {
+			"version": "6.1.0",
+			"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz",
+			"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+			"dev": true,
+			"dependencies": {
+				"universalify": "^2.0.0"
+			},
+			"optionalDependencies": {
+				"graceful-fs": "^4.1.6"
+			}
+		},
 		"node_modules/jsonrepair": {
 			"version": "3.2.4",
 			"resolved": "https://registry.npmmirror.com/jsonrepair/-/jsonrepair-3.2.4.tgz",
@@ -9183,6 +9216,15 @@
 				"node": ">=12"
 			}
 		},
+		"node_modules/universalify": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+			"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+			"dev": true,
+			"engines": {
+				"node": ">= 10.0.0"
+			}
+		},
 		"node_modules/unplugin": {
 			"version": "1.10.0",
 			"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.10.0.tgz",
@@ -9526,6 +9568,90 @@
 				}
 			}
 		},
+		"node_modules/vite-plugin-compression": {
+			"version": "0.5.1",
+			"resolved": "https://registry.npmmirror.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
+			"integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==",
+			"dev": true,
+			"dependencies": {
+				"chalk": "^4.1.2",
+				"debug": "^4.3.3",
+				"fs-extra": "^10.0.0"
+			},
+			"peerDependencies": {
+				"vite": ">=2.0.0"
+			}
+		},
+		"node_modules/vite-plugin-compression/node_modules/ansi-styles": {
+			"version": "4.3.0",
+			"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+			"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+			"dev": true,
+			"dependencies": {
+				"color-convert": "^2.0.1"
+			},
+			"engines": {
+				"node": ">=8"
+			},
+			"funding": {
+				"url": "https://github.com/chalk/ansi-styles?sponsor=1"
+			}
+		},
+		"node_modules/vite-plugin-compression/node_modules/chalk": {
+			"version": "4.1.2",
+			"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+			"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+			"dev": true,
+			"dependencies": {
+				"ansi-styles": "^4.1.0",
+				"supports-color": "^7.1.0"
+			},
+			"engines": {
+				"node": ">=10"
+			},
+			"funding": {
+				"url": "https://github.com/chalk/chalk?sponsor=1"
+			}
+		},
+		"node_modules/vite-plugin-compression/node_modules/color-convert": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+			"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+			"dev": true,
+			"dependencies": {
+				"color-name": "~1.1.4"
+			},
+			"engines": {
+				"node": ">=7.0.0"
+			}
+		},
+		"node_modules/vite-plugin-compression/node_modules/color-name": {
+			"version": "1.1.4",
+			"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+			"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+			"dev": true
+		},
+		"node_modules/vite-plugin-compression/node_modules/has-flag": {
+			"version": "4.0.0",
+			"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+			"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+			"dev": true,
+			"engines": {
+				"node": ">=8"
+			}
+		},
+		"node_modules/vite-plugin-compression/node_modules/supports-color": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+			"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+			"dev": true,
+			"dependencies": {
+				"has-flag": "^4.0.0"
+			},
+			"engines": {
+				"node": ">=8"
+			}
+		},
 		"node_modules/vite-plugin-vue-setup-extend": {
 			"version": "0.4.0",
 			"resolved": "https://registry.npmmirror.com/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz",
@@ -15489,6 +15615,17 @@
 			"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.6.tgz",
 			"integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg=="
 		},
+		"fs-extra": {
+			"version": "10.1.0",
+			"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+			"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.2.0",
+				"jsonfile": "^6.0.1",
+				"universalify": "^2.0.0"
+			}
+		},
 		"fs.realpath": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -15604,6 +15741,12 @@
 				"get-intrinsic": "^1.1.3"
 			}
 		},
+		"graceful-fs": {
+			"version": "4.2.11",
+			"resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+			"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+			"dev": true
+		},
 		"graphemer": {
 			"version": "1.4.0",
 			"resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
@@ -16022,6 +16165,16 @@
 				}
 			}
 		},
+		"jsonfile": {
+			"version": "6.1.0",
+			"resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz",
+			"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+			"dev": true,
+			"requires": {
+				"graceful-fs": "^4.1.6",
+				"universalify": "^2.0.0"
+			}
+		},
 		"jsonrepair": {
 			"version": "3.2.4",
 			"resolved": "https://registry.npmmirror.com/jsonrepair/-/jsonrepair-3.2.4.tgz",
@@ -17549,6 +17702,12 @@
 				}
 			}
 		},
+		"universalify": {
+			"version": "2.0.1",
+			"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+			"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+			"dev": true
+		},
 		"unplugin": {
 			"version": "1.10.0",
 			"resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.10.0.tgz",
@@ -17822,6 +17981,68 @@
 				}
 			}
 		},
+		"vite-plugin-compression": {
+			"version": "0.5.1",
+			"resolved": "https://registry.npmmirror.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
+			"integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==",
+			"dev": true,
+			"requires": {
+				"chalk": "^4.1.2",
+				"debug": "^4.3.3",
+				"fs-extra": "^10.0.0"
+			},
+			"dependencies": {
+				"ansi-styles": {
+					"version": "4.3.0",
+					"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+					"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+					"dev": true,
+					"requires": {
+						"color-convert": "^2.0.1"
+					}
+				},
+				"chalk": {
+					"version": "4.1.2",
+					"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+					"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+					"dev": true,
+					"requires": {
+						"ansi-styles": "^4.1.0",
+						"supports-color": "^7.1.0"
+					}
+				},
+				"color-convert": {
+					"version": "2.0.1",
+					"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+					"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+					"dev": true,
+					"requires": {
+						"color-name": "~1.1.4"
+					}
+				},
+				"color-name": {
+					"version": "1.1.4",
+					"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+					"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+					"dev": true
+				},
+				"has-flag": {
+					"version": "4.0.0",
+					"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+					"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+					"dev": true
+				},
+				"supports-color": {
+					"version": "7.2.0",
+					"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+					"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+					"dev": true,
+					"requires": {
+						"has-flag": "^4.0.0"
+					}
+				}
+			}
+		},
 		"vite-plugin-vue-setup-extend": {
 			"version": "0.4.0",
 			"resolved": "https://registry.npmmirror.com/vite-plugin-vue-setup-extend/-/vite-plugin-vue-setup-extend-0.4.0.tgz",

+ 1 - 0
package.json

@@ -73,6 +73,7 @@
 		"unplugin-auto-import": "^0.16.7",
 		"unplugin-vue-components": "^0.25.2",
 		"vite": "^4.0.0",
+		"vite-plugin-compression": "^0.5.1",
 		"vite-plugin-vue-setup-extend": "^0.4.0",
 		"vue-eslint-parser": "^9.1.0"
 	},

+ 2 - 1
src/theme/app.scss

@@ -82,7 +82,8 @@ body,
     padding: 0 !important;
     overflow: hidden;
     width: 100%;
-    background-color: var(--next-bg-main-color);
+    //background-color: var(--next-bg-main-color);
+    background-color: #f7f7f7;
     display: flex;
     flex-direction: column;
     // 内层 el-scrollbar样式,用于界面高度自适应(main.vue)

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 317 - 291
src/views/adManage/sp/campaigns/CreateCampaigns/index.vue


+ 2 - 2
src/views/productCenter/productAnalysis/components/ExchangeProduct.vue

@@ -136,7 +136,7 @@ onBeforeUnmount(() => {
             <div class="list-content">
               <img :src="item.Image" class="list-item-image" />
               <div>
-                <el-tooltip effect="dark" :content="item.Title" placement="top-start">
+                <el-tooltip effect="dark" :content="item.Title" placement="top-start" :show-after="300">
                   <span class="list-item-title">{{ item.Title }}</span>
                 </el-tooltip>
                 <div>
@@ -174,7 +174,7 @@ onBeforeUnmount(() => {
             <div class="list-content">
               <img :src="item.Image" class="asin-list-item-image" />
               <div>
-                <el-tooltip effect="dark" :content="item.Title" placement="top-start">
+                <el-tooltip effect="dark" :content="item.Title" placement="top-start" :show-after="300">
                   <span class="list-item-title">{{ item.Title }}</span>
                 </el-tooltip>
                 <div>

+ 5 - 3
src/views/productCenter/productAnalysis/components/TopParentAsin.vue

@@ -108,9 +108,11 @@ onBeforeUnmount(() => {
               <div class="classification-title" :style="classificationStyle">{{ classification }}</div>
             </div>
           </div>
-          <div class="product-title">
-            {{ dataSet.Title ? dataSet.Title : '--' }}
-          </div>
+          <el-tooltip effect="dark" :content="dataSet.Title" placement="top-start">
+            <div class="product-title">
+              {{ dataSet.Title ? dataSet.Title : '--' }}
+            </div>
+          </el-tooltip>
           <div class="product-detail">
             <span class="product-detail__label"> 父ASIN: </span>
             <span>{{ dataSet.parentAsin ? dataSet.parentAsin : '--' }}</span>

+ 86 - 56
src/views/productCenter/productList/components/ProductSelectCard.vue

@@ -1,52 +1,55 @@
 <script setup lang="ts">
-import { Delete, Edit } from '@element-plus/icons-vue'
-import * as echarts from 'echarts'
-import { ElMessage } from 'element-plus'
-import { inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
-import emitter from '/@/utils/emitter'
-import { getProductCardData, postDeleteProductLine } from '/@/views/productCenter/productList/api'
+import { Delete, Edit } from '@element-plus/icons-vue';
+import * as echarts from 'echarts';
+import { ElMessage } from 'element-plus';
+import { inject, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import emitter from '/@/utils/emitter';
+import { getProductCardData, postDeleteProductLine } from '/@/views/productCenter/productList/api';
 
 emitter.on('ProductList-updateCardData', (value: any) => {
   if (value.isUpdate) {
-    cardData.value.splice(0)
-    fetchProductCardData()
+    cardData.value.splice(0);
+    fetchProductCardData();
   }
-})
+});
 
 emitter.on('TopFilters-selectValue', (value: any) => {
-  const newIndex = cardData.value.findIndex((item) => item.productlineId === value.selectValue)
+  const newIndex = cardData.value.findIndex((item) => item.productlineId === value.selectValue);
   if (newIndex !== -1) {
-    selectedCardIndex.value = newIndex
+    selectedCardIndex.value = newIndex;
   }
-})
+});
 
-const profile = <any>inject('profile')
-const dateRange = <any>inject('dateRange')
-const loading = ref(false)
-const cardData = ref([])
-const pieChartRefs = ref<HTMLDivElement[]>([])
+const profile = <any>inject('profile');
+const dateRange = <any>inject('dateRange');
+const loading = ref(false);
+const cardData = ref([]);
+const pieChartRefs = ref<HTMLDivElement[]>([]);
 
-const selectedCardIndex = ref(0)
+const selectedCardIndex = ref(0);
 
 async function fetchProductCardData() {
+  loading.value = true;
   try {
     const { data } = await getProductCardData({
       profileId: profile.value.profile_id,
       startDate: dateRange.value[0],
-      endDate: dateRange.value[1]
-    })
-    cardData.value = data
+      endDate: dateRange.value[1],
+    });
+    cardData.value = data;
   } catch (error) {
-    console.log('error:', error)
+    console.log('error:', error);
+  } finally {
+    loading.value = false;
   }
 }
 
 function initChart() {
   cardData.value.forEach((item, index) => {
-    const chartId = `chart${index}-${item.productlineId}`
-    const el = document.getElementById(chartId)
+    const chartId = `chart${index}-${item.productlineId}`;
+    const el = document.getElementById(chartId);
     if (el) {
-      const pieChart = echarts.init(el)
+      const pieChart = echarts.init(el);
       const option = {
         animation: false,
         series: [
@@ -91,72 +94,76 @@ function initChart() {
             ],
           },
         ],
-      }
-      pieChart.setOption(option)
-      pieChartRefs.value[chartId] = pieChart
+      };
+      pieChart.setOption(option);
+      pieChartRefs.value[chartId] = pieChart;
     }
-  })
+  });
 }
 
 function selectCard(index: number, item: any) {
-  selectedCardIndex.value = index
-  const productlineId = item.productlineId
-  emitter.emit('ProductSelectCard-cardId', { productlineId: productlineId })
+  selectedCardIndex.value = index;
+  const productlineId = item.productlineId;
+  emitter.emit('ProductSelectCard-cardId', { productlineId: productlineId });
 }
 
 function editCard(item) {
-  emitter.emit('ProductTab-editProductCard', { isVisible: true, data: item })
+  emitter.emit('ProductTab-editProductCard', { isVisible: true, data: item });
 }
 
 async function deleteProductLine(item) {
   const obj = {
     productlineId: item.productlineId,
-  }
+  };
   try {
-    const response = await postDeleteProductLine(obj)
+    const response = await postDeleteProductLine(obj);
     if (response.data.code == 'success') {
-      ElMessage({ message: '已删除', type: 'success' })
-      await fetchProductCardData()
-      emitter.emit('ProductSelectCard-reloading', { reloading: true })
+      ElMessage({ message: '已删除', type: 'success' });
+      await fetchProductCardData();
+      emitter.emit('ProductSelectCard-reloading', { reloading: true });
     } else {
-      ElMessage({ message: '删除失败', type: 'error' })
+      ElMessage({ message: '删除失败', type: 'error' });
     }
   } catch (error) {
-    console.log('error:', error)
+    console.log('error:', error);
   }
 }
 
-watch(dateRange, async() => {
-  loading.value = true
-  await fetchProductCardData()
-  loading.value = false
-})
+watch(dateRange, async () => {
+  loading.value = true;
+  await fetchProductCardData();
+  loading.value = false;
+});
 
 onMounted(async () => {
-  await fetchProductCardData()
-  initChart()
-})
+  await fetchProductCardData();
+  initChart();
+});
 
 onBeforeUnmount(() => {
-  emitter.all.clear()
-})
+  emitter.all.clear();
+});
 </script>
 
 <template>
-  <div class="out-container" v-loading="loading">
-    <el-scrollbar>
-      <div class="scrollbar-flex-content">
+  <div class="out-container">
+    <el-scrollbar v-loading="loading">
+      <div class="scrollbar-flex-content" v-show="cardData">
         <el-card
+          v-if="cardData.length !== 0"
           v-for="(item, index) in cardData"
           :key="item.productlineId"
-          body-style="padding: 0;box-sizing: border-box; position: relative; width: 100%;"
+          shadow="hover"
+          body-style="padding: 0; box-sizing: border-box; position: relative; width: 100%;"
           class="scrollbar-demo-item"
           :class="{ selected: selectedCardIndex === index }"
           @click="selectCard(index, item)">
           <div class="pct-chart" :id="`chart${index}-${item.productlineId}`"></div>
           <el-popover v-if="index !== 0" placement="bottom" :width="150" trigger="click">
             <template #reference>
-              <el-icon class="custom-icon" @click.stop=""><Setting /></el-icon>
+              <el-icon class="custom-icon" @click.stop="">
+                <Setting />
+              </el-icon>
             </template>
             <div class="custom-popoer">
               <el-button :icon="Edit" text size="small" @click="editCard(item)">编辑</el-button>
@@ -173,6 +180,17 @@ onBeforeUnmount(() => {
           </div>
         </el-card>
       </div>
+      <el-card
+        v-if="cardData.length === 0"
+        shadow="hover"
+        body-style="padding: 0;box-sizing: border-box; position: relative; width: 100%;"
+        class="scrollbar-demo-item">
+        <el-empty description=" ">
+          <template #image>
+            <div style="color: #919398">暂无数据</div>
+          </template>
+        </el-empty>
+      </el-card>
     </el-scrollbar>
   </div>
 </template>
@@ -182,21 +200,25 @@ onBeforeUnmount(() => {
   width: 100%;
   padding: 5px 12px 0 12px;
 }
+
 .scrollbar-flex-content {
   display: flex;
 }
+
 .scrollbar-demo-item {
   flex-shrink: 0;
   display: flex;
+  gap: 5px;
   width: 202px;
   height: 92px;
-  margin: 10px 5px;
+  margin: 10px 10px 10px 0;
   border-radius: 4px;
   box-sizing: border-box;
   border: 1px solid transparent; /* 添加透明边框 */
   transition: border-color 0.3s; /* 可选,使边框颜色改变更平滑 */
   cursor: pointer;
 }
+
 .product-line-name {
   position: relative;
   padding: 0 12px 8px 0;
@@ -208,19 +230,23 @@ onBeforeUnmount(() => {
   white-space: nowrap;
   text-overflow: ellipsis;
 }
+
 .custom-part {
   padding-top: 8px;
 }
+
 .total-sales {
   color: #4e5969;
   font-size: 13px;
   font-weight: 700;
 }
+
 .label {
   padding-right: 4px;
   color: #c9cdd4;
   font-size: 12px;
 }
+
 .pct-chart {
   box-sizing: border-box;
   position: absolute;
@@ -229,9 +255,11 @@ onBeforeUnmount(() => {
   width: 50px;
   height: 50px;
 }
+
 .selected {
   border: 1px solid #3359b5;
 }
+
 .custom-icon {
   position: absolute;
   top: 8px;
@@ -241,10 +269,12 @@ onBeforeUnmount(() => {
   /* padding: 4px; */
   transform: rotate(90deg);
 }
+
 .custom-popoer {
   display: flex;
   flex-direction: column;
 }
+
 .left-part-container {
   padding: 10px;
   position: relative;

+ 3 - 4
src/views/reportManage/dataCenter/normalDisplay/components/TableDataDisplay.vue

@@ -169,7 +169,6 @@ async function fetchCurrentTotalData(taskIds) {
 
 async function fetchData(taskIds, apiFunc, startDate, endDate, dataColumns, dateTypeKey) {
   try {
-    gridOptions.loading = true;
     loadSortState();
     const resp = await apiFunc({
       page: gridOptions.pagerConfig.currentPage,
@@ -252,8 +251,6 @@ async function fetchData(taskIds, apiFunc, startDate, endDate, dataColumns, date
     saveSortState();
   } catch (error) {
     console.error('Error fetching task data:', error);
-  } finally {
-    gridOptions.loading = false;
   }
 }
 
@@ -270,6 +267,7 @@ async function fetchMonthData(taskIds) {
 }
 
 async function fetchCurrentData(taskIds, resetPage = false) {
+  gridOptions.loading = true;
   if (resetPage) {
     gridOptions.pagerConfig.currentPage = 1; // 重置页码为第一页
   }
@@ -281,6 +279,7 @@ async function fetchCurrentData(taskIds, resetPage = false) {
   } else if (dateType.value === 'month') {
     await fetchMonthData(taskIds);
   }
+  gridOptions.loading = false;
 }
 
 function handleSortChange({ field, order }) {
@@ -413,4 +412,4 @@ onMounted(() => {
   color: #a6d8fc;
   border: 1px solid #0085ff;
 }
-</style>
+</style>

+ 22 - 0
src/views/searchTerm/analysisPage/IndicatorChart.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts">
+/**
+ * @Name: IndicatorChart.vue
+ * @Description: 搜索词-分析页-图表卡片
+ * @Author: Cheney
+ */
+import IndicatorHeatmap from './IndicatorHeatmap.vue';
+import IndicatorFunnel from './IndicatorFunnel.vue';
+import IndicatorXBar from './IndicatorXBar.vue';
+</script>
+
+<template>
+  <el-card shadow="hover" style="border: none; margin-bottom: 10px">
+    <IndicatorHeatmap />
+    <div class="flex gap-5">
+      <IndicatorFunnel class="flex-1" />
+      <IndicatorXBar class="flex-1" />
+    </div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 178 - 0
src/views/searchTerm/analysisPage/IndicatorFunnel.vue

@@ -0,0 +1,178 @@
+<script setup lang="ts">
+/**
+ * @Name: IndicatorFunnel.vue
+ * @Description: 搜索词-分析页-漏斗图
+ * @Author: Cheney
+ */
+import { Search } from '@element-plus/icons-vue';
+import { inject, onBeforeUnmount, reactive, ref, Ref, watch } from 'vue';
+import * as api from './api';
+import * as echarts from 'echarts';
+import emitter from '/@/utils/emitter';
+
+const filter = inject<Ref>('filter');
+const funnelFilter = reactive({
+  layerSelect: 'all_asin',
+  searchTermInp: '',
+});
+
+const funnelLoading = ref(false);
+const chartRef = ref<HTMLElement | null>(null);
+let chart: echarts.ECharts | null = null;
+let resizeObserver: ResizeObserver | null = null;
+const hasData = ref(true);
+
+onBeforeUnmount(() => {
+  emitter.all.clear();
+  // 清理 ResizeObserver
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+  }
+  if (chart) {
+    chart.dispose();
+    chart = null;
+  }
+});
+
+emitter.on('QueryCondition-sendRequest', () => {
+  fetchFunnelData();
+});
+
+watch(filter.value, () => {
+  funnelFilter.layerSelect = filter.value.layerType;
+});
+
+async function fetchFunnelData() {
+  funnelLoading.value = true;
+  const query = {
+    date_start: filter.value.reportDate[0],
+    date_end: filter.value.reportDate[1],
+    report_range: filter.value.reportType,
+    layer_type: funnelFilter.layerSelect,
+  };
+  try {
+    const response = await api.getFunnelData(query);
+    if (!response.data || Object.keys(response.data).length === 0) {
+      hasData.value = false;
+      if (chart) {
+        chart.clear();
+      }
+      return;
+    }
+
+    hasData.value = true;
+
+    // 转换数据
+    const rawData = response.data;
+    const funnelData = Object.entries(rawData)
+      .filter(([key]) => key.endsWith('log2'))
+      .map(([key, value]) => {
+        const name =
+          key.replace('_Total_Countlog2', '').charAt(0).toUpperCase() +
+          key.replace('_Total_Countlog2', '').slice(1).toLowerCase();
+        const originalKey = key.replace('log2', '');
+        return {
+          value: value,
+          name: name,
+          originalValue: rawData[originalKey],
+        };
+      });
+
+    const option = {
+      title: {
+        text: '购买旅程漏斗图',
+      },
+      tooltip: {
+        trigger: 'item',
+        formatter: function (params) {
+          return `${params.name}: ${params.data.originalValue.toLocaleString()}`;
+        },
+      },
+      // toolbox: {
+      //   feature: {
+      //     dataView: { readOnly: false },
+      //   },
+      // },
+      series: [
+        {
+          type: 'funnel',
+          left: '10%',
+          top: 50,
+          bottom: 0,
+          width: '80%',
+          min: 0,
+          max: 100,
+          minSize: '0%',
+          maxSize: '100%',
+          sort: 'descending',
+          gap: 2,
+          labelLine: {
+            length: 10,
+            lineStyle: {
+              width: 1,
+              type: 'solid',
+            },
+          },
+          itemStyle: {
+            borderColor: '#fff',
+            borderWidth: 1,
+          },
+          emphasis: {
+            label: {
+              fontSize: 20,
+            },
+          },
+          data: funnelData,
+        },
+      ],
+    };
+
+    initChart(option);
+  } catch (error) {
+    console.error('==Error==', error);
+  } finally {
+    funnelLoading.value = false;
+  }
+}
+
+function initChart(opt) {
+  if (!chart) {
+    chart = echarts.init(chartRef.value);
+  }
+  chart.setOption(opt);
+
+  // 添加 ResizeObserver 以处理图表大小变化
+  if (!resizeObserver) {
+    resizeObserver = new ResizeObserver(() => {
+      chart?.resize();
+    });
+    resizeObserver.observe(chartRef.value);
+  }
+}
+</script>
+
+<template>
+  <el-card shadow="never" v-loading="funnelLoading" class="mt-5">
+    <div class="flex gap-5 mb-4 justify-center">
+      <div>
+        <span class="font-medium mr-0.5">层级 </span>
+        <el-select v-model="funnelFilter.layerSelect" @change="fetchFunnelData" style="width: 130px">
+          <el-option label="Asin View" value="asin_view" />
+          <el-option label="Brand View" value="brand_view" />
+          <el-option label="All Asin" value="all_asin" />
+          <el-option label="All Brand" value="all_brand" />
+        </el-select>
+      </div>
+      <div>
+        <span class="font-medium mr-0.5">搜索词 </span>
+        <el-input v-model="funnelFilter.searchTermInp" :prefix-icon="Search" @change="fetchFunnelData" style="width: 200px" />
+      </div>
+    </div>
+    <div v-show="!funnelLoading && !hasData" class="no-data-message" style="min-height: 500px">
+      <el-empty />
+    </div>
+    <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 201 - 0
src/views/searchTerm/analysisPage/IndicatorHeatmap.vue

@@ -0,0 +1,201 @@
+<script setup lang="ts">
+/**
+ * @Name: IndicatorHeatmap.vue
+ * @Description: 搜索词-分析页-热力图
+ * @Author: Cheney
+ */
+
+import * as echarts from 'echarts';
+import { inject, onBeforeUnmount, Ref, ref } from 'vue';
+import * as api from './api';
+import emitter from '/@/utils/emitter';
+
+const filter = inject<Ref>('filter');
+const hasData = ref(true);
+const heatmapLoading = ref(false);
+const chartRef = ref<HTMLElement | null>(null);
+let chart: echarts.ECharts | null = null;
+let resizeObserver: ResizeObserver | null = null;
+
+onBeforeUnmount(() => {
+  emitter.all.clear();
+  // 清理 ResizeObserver
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+  }
+  if (chart) {
+    chart.dispose();
+    chart = null;
+  }
+});
+
+emitter.on('QueryCondition-sendRequest', () => {
+  fetchHeatmapData();
+});
+
+async function fetchHeatmapData() {
+  heatmapLoading.value = true;
+  const query = {
+    date_start: filter.value.reportDate[0],
+    date_end: filter.value.reportDate[1],
+    layer_type: filter.value.layerType,
+    search_term: filter.value.searchTerm,
+    report_range: filter.value.reportType,
+    [filter.value.layerType.split('_')[0]]: filter.value.variable,
+    metric: filter.value.metric,
+  };
+  try {
+    const responseData = await api.getHeatmapData(query);
+    if (!responseData.data || responseData.data.length === 0) {
+      // 处理空数据的情况
+      hasData.value = false;
+      if (chart) {
+        chart.clear();
+      }
+      return;
+    }
+    hasData.value = true;
+    const days = responseData.data.map((item) => item.Reporting_Date); // y轴数据
+    const keywords = Object.keys(responseData.data[0]).filter((key) => key !== 'Reporting_Date').slice(0, 10); // x轴数据
+    const data = [];
+
+    // 找出所有数值的最大值,用于设置 visualMap 的 max 值
+    const maxValue = Math.max(
+      ...responseData.data.flatMap((item) =>
+        Object.entries(item)
+          .filter(([key]) => key !== 'Reporting_Date')
+          .map(([, value]) => value as number)
+      )
+    );
+
+    responseData.data.forEach((item, yIndex) => {
+      keywords.forEach((keyword, xIndex) => {
+        if (item[keyword] !== undefined && item[keyword] !== null) {
+          data.push([xIndex, yIndex, item[keyword]]); // 只添加非空值
+        }
+      });
+    });
+
+    const option = {
+      tooltip: {
+        position: 'top',
+      },
+      grid: {
+        height: '65%',
+        top: '15%',
+      },
+      position: 'top',
+      xAxis: {
+        type: 'category',
+        name: '日期',
+        nameGap: 20,
+        nameLocation: 'start',
+        nameTextStyle: {
+          fontWeight: 'bold',
+          fontSize: 14,
+          color: '#333',
+        },
+        axisLabel: {
+          formatter: function (value) {
+            if (value.length > 10) {
+              return value.substring(0, 10) + '...';
+            }
+            return value;
+          },
+        },
+        data: keywords,
+        splitArea: {
+          show: true,
+        },
+      },
+      yAxis: {
+        type: 'category',
+        name: '搜索词',
+        nameGap: 20,
+        nameTextStyle: {
+          fontWeight: 'bold',
+          fontSize: 14,
+          color: '#333',
+        },
+        // nameTruncate: {
+        //   maxWidth: 20,
+        //   ellipsis: '...'
+        // },
+        data: days,
+        splitArea: {
+          show: true,
+        },
+      },
+      visualMap: {
+        min: 0,
+        max: maxValue, // 使用计算出的最大值
+        calculable: true,
+        orient: 'horizontal',
+        left: 'center',
+        bottom: '2%',
+      },
+      series: [
+        {
+          name: 'Punch Card',
+          type: 'heatmap',
+          data: data,
+          label: {
+            show: true,
+          },
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowColor: 'rgba(0, 0, 0, 0.5)',
+            },
+          },
+        },
+      ],
+    };
+
+    if (!chart) {
+      chart = echarts.init(chartRef.value);
+    }
+    chart.setOption(option);
+
+    // 添加 ResizeObserver 以处理图表大小变化
+    if (!resizeObserver) {
+      resizeObserver = new ResizeObserver(() => {
+        chart?.resize();
+      });
+      resizeObserver.observe(chartRef.value);
+    }
+  } catch (error) {
+    hasData.value = false;
+    console.error('==Error==', error);
+  } finally {
+    heatmapLoading.value = false;
+  }
+}
+</script>
+
+<template>
+  <el-card shadow="never" v-loading="heatmapLoading" class="flex flex-col" body-class="w-full">
+    <div class="font-bold text-xl mb-4 text-center" style="color: #464646">搜索词时间段对比热力图</div>
+    <div class="text-center">
+      <el-radio-group v-model="filter.metric" @change="fetchHeatmapData">
+        <el-radio-button label="Search_Query_Score" value="Search_Query_Score" />
+        <el-radio-button label="Search_Query_Volume" value="Search_Query_Volume" />
+        <el-radio-button label="Impressions_Total_Count" value="Impressions_Total_Count" />
+        <el-radio-button label="Impressions_A_B_Count" value="Impressions_A_B_Count" />
+        <el-radio-button label="Clicks_Total_Count" value="Clicks_Total_Count" />
+        <el-radio-button label="Clicks_Price_Median" value="Clicks_Price_Median" />
+        <el-radio-button label="Cart_Adds_Total_Count" value="Cart_Adds_Total_Count" />
+        <el-radio-button label="Purchases_Total_Count" value="Purchases_Total_Count" />
+      </el-radio-group>
+    </div>
+
+    <div class="w-full">
+      <div v-show="!heatmapLoading && !hasData" class="no-data-message" style="min-height: 500px">
+        <el-empty />
+      </div>
+      <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
+    </div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 173 - 0
src/views/searchTerm/analysisPage/IndicatorOverview.vue

@@ -0,0 +1,173 @@
+<script setup lang="ts">
+/**
+ * @Name: IndicatorOverview.vue
+ * @Description: 分析页-指标总览
+ * @Author: Cheney
+ */
+import { inject, onBeforeUnmount, reactive, ref, Ref, watch } from 'vue';
+import { Bottom, Top } from '@element-plus/icons-vue';
+import * as api from '/@/views/searchTerm/analysisPage/api';
+import emitter from '/@/utils/emitter';
+
+const filter = inject<Ref>('filter');
+const metricLoading = ref(false);
+const responseData = ref(null);
+const indicator = reactive([
+  {
+    title: '',
+    value: '',
+    compare: '',
+    color: '#edf6fd',
+  },
+  {
+    title: '',
+    value: '',
+    compare: '',
+    color: '#eefdf0',
+  },
+  {
+    title: '',
+    value: '',
+    compare: '',
+    color: '#fff7ed',
+  },
+  {
+    title: '',
+    value: '',
+    compare: '',
+    color: '#f0f0fe',
+  },
+]);
+// 指标与返回数据字段的映射关系
+const mapping = {
+  imp: {
+    title: '曝光量',
+    key: 0, // indicator 数组中对应的索引
+    kw: 'Impressions',
+  },
+  click: {
+    title: '点击率',
+    key: 1,
+    kw: 'Clicks',
+  },
+  cart_add: {
+    title: '加购份额',
+    key: 2,
+    kw: 'Cart_Adds',
+  },
+  purchases: {
+    title: '购买份额',
+    key: 3,
+    kw: 'Purchases',
+  },
+};
+
+onBeforeUnmount(() => {
+  emitter.all.clear();
+});
+
+emitter.on('QueryCondition-sendRequest', () => {
+  fetchIndicatorData();
+});
+
+watch(responseData, (newData) => {
+  if (newData) {
+    for (const key in mapping) {
+      if (newData[key]) {
+        const index = mapping[key].key;
+        indicator[index].title = mapping[key].title;
+        indicator[index].value = newData[key][`${mapping[key].kw + '_A_B_Share'}`];
+        indicator[index].compare = newData[key][`diff_${mapping[key].kw + '_A_B_Share'}`];
+      }
+    }
+  } else {
+    indicator.forEach((item) => {
+      item.title = '暂无数据';
+      item.value = '--';
+      item.compare = '--';
+    });
+  }
+});
+
+async function fetchIndicatorData() {
+  metricLoading.value = true;
+  const query = {
+    date_start: filter.value.reportDate[0],
+    date_end: filter.value.reportDate[1],
+    layer_type: filter.value.layerType,
+    search_term: filter.value.searchTerm,
+    report_range: filter.value.reportType,
+    [filter.value.layerType.split('_')[0]]: filter.value.variable,
+  };
+  try {
+    const response = await api.getAsinMetrics(query);
+    responseData.value = response.data;
+  } catch (error) {
+    console.error('==Error==', error);
+  } finally {
+    metricLoading.value = false;
+  }
+}
+</script>
+
+<template>
+  <el-card
+    v-loading="metricLoading"
+    shadow="hover"
+    body-class="flex justify-between w-full"
+    style="border: none; margin-bottom: 10px">
+    <el-card
+      v-for="(item, index) in indicator"
+      :key="index"
+      body-class="flex flex-col items-center"
+      class="flex-1 mx-1"
+      :style="{
+        background: item.color
+          ? `linear-gradient(to top, ${item.color}, transparent)`
+          : 'linear-gradient(to top, #fff, transparent)',
+      }">
+      <div class="text-2xl font-bold mb-1.5">{{ item.title || '暂无数据' }}</div>
+      <div class="font-medium text-2xl mb-1.5">
+        {{ item.value !== null && item.value !== undefined && item.value !== '' ? item.value : '--' }}
+        <span v-if="item.value !== null && item.value !== undefined && item.value !== '' && item.value !== '--'">%</span>
+      </div>
+      <div>
+        <span class="mr-2 font-medium" style="color: #9ca3af">较上期</span>
+        <template v-if="item.compare !== '-'">
+          <el-icon
+            v-if="item.compare && item.compare !== '--'"
+            :color="Number(item.compare) > 0 ? '#59b939' : '#e36f53'"
+            style="display: inline-block; padding-top: 2px">
+            <component :is="Number(item.compare) > 0 ? Top : Bottom" />
+          </el-icon>
+          <span
+            class="font-medium"
+            :style="{
+              color:
+                item.compare && item.compare !== '--'
+                  ? Number(item.compare) > 0
+                    ? '#59b939'
+                    : Number(item.compare) < 0
+                    ? '#e36f53'
+                    : ''
+                  : '',
+            }">
+            {{ item.compare !== null && item.compare !== undefined && item.compare !== '' ? item.compare : '--' }}
+          </span>
+          <span
+            v-if="item.compare && item.compare !== '--'"
+            :style="{
+              color: Number(item.compare) > 0 ? '#59b939' : Number(item.compare) < 0 ? '#e36f53' : '',
+            }"
+            >%</span
+          >
+        </template>
+        <template v-else>
+          <span class="font-medium">无数据</span>
+        </template>
+      </div>
+    </el-card>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 191 - 0
src/views/searchTerm/analysisPage/IndicatorXBar.vue

@@ -0,0 +1,191 @@
+<script setup lang="ts">
+/**
+ * @Name: IndicatorXBar.vue
+ * @Description: 搜索词-分析页-柱状图
+ * @Author: Cheney
+ */
+import { inject, onBeforeUnmount, reactive, Ref, ref, watch } from 'vue';
+import * as api from '/@/views/searchTerm/analysisPage/api';
+import * as echarts from 'echarts';
+import emitter from '/@/utils/emitter';
+
+const filter = inject<Ref>('filter');
+const barLoading = ref(false);
+
+const barFilter = reactive({
+  layerSelect: 'Impressions_A_B_Count',
+  searchTermInp: '',
+});
+
+let responseData = null;
+const chartRef = ref<HTMLElement | null>(null);
+let chart: echarts.ECharts | null = null;
+let resizeObserver: ResizeObserver | null = null;
+const hasData = ref(true);
+
+onBeforeUnmount(() => {
+  emitter.all.clear();
+  // 清理 ResizeObserver
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+  }
+  if (chart) {
+    chart.dispose();
+    chart = null;
+  }
+});
+
+emitter.on('QueryCondition-sendRequest', () => {
+  fetchBarData();
+});
+
+// watch(filter.value,() => {
+//   barFilter.layerSelect = filter.value.layerType;
+// })
+
+async function fetchBarData() {
+  barLoading.value = true;
+  const query = {
+    date_start: filter.value.reportDate[0],
+    date_end: filter.value.reportDate[1],
+    report_range: filter.value.reportType,
+    layer_type: filter.value.layerType,
+    search_term: barFilter.searchTermInp,
+    [filter.value.layerType.split('_')[0]]: filter.value.variable,
+    metrics: barFilter.layerSelect
+  };
+  try {
+    responseData = await api.getBarData(query);
+    if (!responseData.data || responseData.data.length === 0) {
+      // 处理空数据的情况
+      hasData.value = false;
+      if (chart) {
+        chart.clear();
+      }
+      return;
+    }
+
+    hasData.value = true;
+
+    const asinList = Object.keys(responseData.data[0]).filter(key => key !== 'Search_Query');
+
+// 准备图表数据
+    const searchQueries = responseData.data.map(item => item.Search_Query);
+    const seriesData = asinList.map(asin => ({
+      name: asin,
+      type: 'bar',
+      stack: 'total',
+      label: {
+        show: true,
+        position: 'inside',
+        formatter: '{c}'
+      },
+      emphasis: {
+        focus: 'series'
+      },
+      data: responseData.data.map(item => item[asin] || 0)
+    }));
+
+    const option = {
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'shadow'
+        }
+      },
+      legend: {
+        orient: 'horizontal',
+        top: 'bottom'
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '15%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'value'
+      },
+      yAxis: {
+        type: 'category',
+        data: searchQueries,
+        axisLabel: {
+          interval: 0,
+          formatter: function (value) {
+            return value.length > 20 ? value.substring(0, 20) + '...' : value;
+          }
+        }
+      },
+      series: seriesData
+    };
+
+    initChart(option);
+  } catch (error) {
+    console.error('==Error==', error);
+  } finally {
+    barLoading.value = false;
+  }
+}
+
+function initChart(opt: echarts.EChartsCoreOption) {
+  if (!chart) {
+    chart = echarts.init(chartRef.value);
+  }
+  chart.setOption(opt);
+
+  // 添加 ResizeObserver 以处理图表大小变化
+  if (!resizeObserver) {
+    resizeObserver = new ResizeObserver(() => {
+      chart?.resize();
+    });
+    resizeObserver.observe(chartRef.value);
+  }
+}
+
+function changeChart() {
+  const option = {
+    title: {
+      text: 'Asin/Brand 关键词情况',
+    },
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+      },
+    },
+    legend: {},
+    // grid: {
+    //   left: '3%',
+    //   right: '4%',
+    //   bottom: '3%',
+    //   containLabel: true,
+    // },
+    dataset: {
+      dimensions: ['Search_Query', 'Impressions_A_B_Count', 'Clicks_Total_Count', 'Cart_Adds_Total_Count', 'Purchases_Total_Count'],
+      source: responseData.data
+    }
+  }
+}
+</script>
+
+<template>
+  <el-card shadow="never" v-loading="barLoading" class="mt-5">
+    <div class="flex gap-5 mb-4 justify-center">
+      <div>
+        <span class="font-medium mr-0.5">层级 </span>
+        <el-select v-model="barFilter.layerSelect" @change="changeChart" style="width: 130px">
+          <el-option label="曝光次数" value="Impressions_A_B_Count" />
+          <el-option label="点击次数" value="Clicks_Total_Count" />
+          <el-option label="购买次数" value="Purchases_Total_Count" />
+          <el-option label="加购次数" value="Cart_Total_Count" />
+        </el-select>
+      </div>
+    </div>
+    <div v-show="!barLoading && !hasData" class="no-data-message" style="min-height: 500px">
+      <el-empty />
+    </div>
+    <div v-show="hasData" ref="chartRef" style="width: 100%; height: 500px"></div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 90 - 0
src/views/searchTerm/analysisPage/QueryCondition.vue

@@ -0,0 +1,90 @@
+<script setup lang="ts">
+/**
+ * @Name: QueryCondition.vue
+ * @Description:  搜索词-分析页-条件筛选栏
+ * @Author: Cheney
+ */
+import { RefreshLeft, Search } from '@element-plus/icons-vue';
+import { onMounted, ref, watch, inject, Ref } from 'vue';
+import emitter from '/@/utils/emitter';
+
+const defaultLabel = ref('ASIN');
+const filter = inject<Ref>('filter');
+
+onMounted(() => {
+  handleQuery();
+});
+
+watch(
+  () => filter.value.layerType,
+  (newValue) => {
+    if (newValue === 'asin_view') {
+      defaultLabel.value = 'ASIN';
+    } else if (newValue === 'brand_view') {
+      defaultLabel.value = 'Brand';
+    } else {
+      defaultLabel.value = 'ASIN';
+      filter.value.variable = '';
+    }
+  }
+);
+
+function handleQuery() {
+  emitter.emit('QueryCondition-sendRequest');
+}
+
+function resetCondition() {
+  filter.value.layerType = '';
+  filter.value.searchTerm = '';
+  filter.value.reportType = '';
+  filter.value.reportDate = '';
+  filter.value.variable = '';
+}
+</script>
+
+<template>
+  <el-card body-class="flex justify-between gap-3.5" shadow="hover" style="border: none; margin-bottom: 10px">
+    <div class="flex flex-wrap gap-7">
+      <div>
+        <span class="font-bold mr-2" style="color: #303133">层级类型:</span>
+        <el-select v-model="filter.layerType" style="width: 130px">
+          <el-option label="Asin View" value="asin_view"></el-option>
+          <el-option label="Brand View" value="brand_view"></el-option>
+        </el-select>
+      </div>
+      <div>
+        <span class="font-bold mr-2" style="color: #303133">报告类型:</span>
+        <el-select v-model="filter.reportType" style="width: 100px">
+          <el-option label="月度" value="MONTHLY"></el-option>
+          <el-option label="周度" value="WEEKLY"></el-option>
+        </el-select>
+      </div>
+      <div>
+        <span class="font-bold mr-2" style="color: #303133">搜索词:</span>
+        <el-input v-model="filter.searchTerm" :prefix-icon="Search" style="width: 180px"></el-input>
+      </div>
+      <div>
+        <span class="font-bold mr-2" style="color: #303133">{{ defaultLabel }}:</span>
+        <el-input v-model="filter.variable" :prefix-icon="Search" style="width: 180px"></el-input>
+      </div>
+      <div>
+        <span class="font-bold mr-2" style="color: #303133">报告日期:</span>
+        <el-date-picker
+            style="width: 280px;"
+          v-model="filter.reportDate"
+          type="daterange"
+          range-separator="To"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="YYYY-MM-DD"
+          :disabled-date="(time: Date) => time > new Date()" />
+      </div>
+    </div>
+    <div class="flex gap-3.5">
+      <el-button type="primary" plain :icon="Search" @click="handleQuery">查询</el-button>
+      <el-button type="warning" plain round :icon="RefreshLeft" color="#0891b2" @click="resetCondition">重置</el-button>
+    </div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 82 - 0
src/views/searchTerm/analysisPage/QuerySummary.vue

@@ -0,0 +1,82 @@
+<script setup lang="ts">
+/**
+ * @Name: IndicatorHeatmap.vue
+ * @Description: 搜索词-分析页-Query汇总
+ * @Author: Cheney
+ */
+
+import { inject, nextTick, onBeforeUnmount, Ref, ref } from 'vue'
+import * as api from './api';
+import { usePagination } from '/@/utils/usePagination';
+import emitter from '/@/utils/emitter'
+
+const { tableData, total, currentPage, pageSize, handlePageChange } = usePagination(fetchTableData);
+const filter = inject<Ref>('filter');
+const loading = ref(false);
+const typeSelect = ref('positive');
+
+onBeforeUnmount(() => {
+  emitter.all.clear();
+});
+
+emitter.on('QueryCondition-sendRequest', () => {
+  fetchTableData();
+});
+
+async function fetchTableData() {
+  loading.value = true;
+  const query = {
+    date_start: filter.value.reportDate[0],
+    date_end: filter.value.reportDate[1],
+    report_range: filter.value.reportType,
+    layer_type: filter.value.layerType,
+    searchTerm_type: typeSelect.value,
+    page: currentPage.value,
+    limit: pageSize
+  };
+  try {
+    const response = await api.getTableData(query);
+    total.value = response.total;
+    tableData.value = response.data;
+  } catch (error) {
+    console.log('error:', error);
+  } finally {
+    loading.value = false;
+    await nextTick();
+    // 触发窗口 resize 事件
+    window.dispatchEvent(new Event('resize'));
+  }
+}
+
+function changeType() {}
+</script>
+
+<template>
+  <el-card shadow="never" v-loading="loading">
+    <div class="flex gap-5 mb-4 justify-center">
+      <div>
+        <span class="font-medium mr-0.5">类型 </span>
+        <el-select v-model="typeSelect" @change="changeType" style="width: 130px">
+          <el-option label="positive" value="positive" />
+          <el-option label="negative" value="negative" />
+        </el-select>
+      </div>
+    </div>
+    <div style="height: 100%; overflow: auto">
+      <el-table :data="tableData" height="550" stripe style="width: 100%">
+        <!--<el-table-column prop="keyword" label="搜索词" width="200" />-->
+      </el-table>
+    </div>
+    <div class="mt-3.5 flex justify-end">
+      <el-pagination
+        v-model:current-page="currentPage"
+        v-model:page-size="pageSize"
+        :page-sizes="[10, 20, 30, 50, 100, 200]"
+        layout="sizes, prev, pager, next"
+        :total="total"
+        @change="handlePageChange" />
+    </div>
+  </el-card>
+</template>
+
+<style scoped></style>

+ 42 - 0
src/views/searchTerm/analysisPage/api.ts

@@ -0,0 +1,42 @@
+import { request } from '/@/utils/service';
+
+const apiPrefix = '/api/searchterm/';
+
+export function getAsinMetrics(query: any) {
+  return request({
+    url: apiPrefix + 'asinmetrics/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getHeatmapData(query: any) {
+  return request({
+    url: apiPrefix + 'heatmap/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getFunnelData(query: any) {
+  return request({
+    url: apiPrefix + 'funnelchart/',
+    method: 'GET',
+    params: query,
+  });
+}
+
+export function getBarData(query: any) {
+  return request({
+    url: apiPrefix + 'barchart/',
+    method: 'GET',
+    params: query,
+  });
+}
+export function getTableData(query: any) {
+  return request({
+    url: apiPrefix + 'rootmetric/',
+    method: 'GET',
+    params: query,
+  });
+}

+ 38 - 0
src/views/searchTerm/analysisPage/index.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description: 分析页
+ * @Author: Cheney
+ */
+import { provide, ref } from 'vue';
+import QueryCondition from './QueryCondition.vue';
+import IndicatorOverview from './IndicatorOverview.vue';
+import IndicatorChart from './IndicatorChart.vue';
+import QuerySummary from './QuerySummary.vue';
+
+const filter = ref({
+  // 初始化 QueryCondition组件的filter
+  layerType: 'asin_view',
+  searchTerm: '',
+  reportType: 'MONTHLY',
+  reportDate: ['2024-04-01', '2024-06-01'],
+  variable: 'B00TEST0001',
+  metric: 'Search_Query_Score',
+});
+provide('filter', filter);
+</script>
+
+<template>
+  <div class="py-2 px-2.5">
+    <QueryCondition />
+    <IndicatorOverview />
+    <IndicatorChart />
+    <el-card shadow="hover" body-class="flex gap-5"  style="border: none;">
+      <QuerySummary class="flex-1" />
+      <QuerySummary class="flex-1" />
+    </el-card>
+
+  </div>
+</template>
+
+<style scoped></style>

+ 24 - 0
src/views/searchTerm/importPage/api.ts

@@ -0,0 +1,24 @@
+import { request } from '/@/utils/service';
+
+const apiPrefix = '/api/searchterm/';
+
+export function uploadFile(file: any) {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request({
+    url: apiPrefix + 'upload_file/',
+    method: 'POST',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data',
+    },
+  });
+}
+
+export function confirmUploadFile(query: any) {
+  return request({
+    url: apiPrefix + 'upload_file/',
+    method: 'POST',
+    params: query,
+  });
+}

+ 198 - 0
src/views/searchTerm/importPage/index.vue

@@ -0,0 +1,198 @@
+<script setup lang="ts">
+/**
+ * @Name: index.vue
+ * @Description: 导入页
+ * @Author: Cheney
+ */
+import { Upload, View } from '@element-plus/icons-vue';
+import { reactive, ref } from 'vue';
+import { ElMessage, genFileId, UploadInstance, UploadRawFile } from 'element-plus';
+import { SUCCESS_CODE, WARNING_CODE } from '/@/utils/requestCode';
+import * as api from './api';
+import { VxeGridProps } from 'vxe-table';
+
+const upload = ref<UploadInstance>();
+const upBtnLoading = ref(false);
+const defaultLabel = ref('ASIN');
+const filter = reactive({
+  reportFilter: '',
+  reportDateFilter: '',
+  typeFilter: '',
+  variableFilter: '',
+});
+
+const gridOptions = reactive<VxeGridProps>({
+  loading: upBtnLoading,
+  round: true,
+  // border: 'inner',
+  stripe: true,
+  resizable: true,
+  height: 900,
+  toolbarConfig: {
+    custom: true,
+  },
+  columns: [
+    // { type: 'seq', width: 70 },
+    // { field: 'name', title: 'Name' },
+  ],
+  data: [],
+});
+
+/**
+ * @description 替换文件并上传
+ * @param files 文件列表
+ */
+function handleExceed(files: any) {
+  upload.value!.clearFiles();
+  const file = files[0] as UploadRawFile;
+  file.uid = genFileId();
+  upload.value!.handleStart(file);
+  upload.value!.submit();
+}
+
+/**
+ * 统一处理响应
+ * @param response 后端返回的响应
+ */
+function handleResponse(response: any) {
+  if (response.code === SUCCESS_CODE) {
+    ElMessage.success({ message: response.msg, plain: true });
+  } else if (response.code === WARNING_CODE) {
+    ElMessage.warning({ message: response.msg, plain: true });
+  } else {
+    ElMessage.error({ message: response.msg, plain: true });
+  }
+}
+
+/**
+ * 上传文件
+ * @param uploadRequest 上传请求
+ */
+async function handleCustomUpload(uploadRequest: any) {
+  upBtnLoading.value = true;
+  try {
+    const { file } = uploadRequest;
+    const response = await api.uploadFile(file);
+    handleResponse(response);
+    processResponseData(response.data);
+    uploadRequest.onSuccess(response); // 通知 el-upload 上传成功
+  } catch (error) {
+    console.log('==Error==', error);
+    uploadRequest.onError(error);
+  } finally {
+    upBtnLoading.value = false;
+  }
+}
+
+/**
+ * 处理响应数据,更新 gridOptions
+ * @param data 响应数据
+ */
+function processResponseData(data: any) {
+  const limitedData = data.length > 15 ? data.slice(0, 15) : data;
+
+  if (data.length > 0) {
+    filter.reportFilter = data[0].Reporting_Range || '';
+    filter.reportDateFilter = data[0].Reporting_Date || '';
+    filter.typeFilter = data[0].ASIN ? 'ASIN View' : 'Brand View';
+    filter.variableFilter = data[0].ASIN ? data[0].ASIN : data[0].brand || '';
+  } else {
+    filter.reportFilter = '';
+    filter.reportDateFilter = '';
+    filter.typeFilter = '';
+    filter.variableFilter = '';
+  }
+
+  // 动态生成 columns 配置
+  gridOptions.columns = Object.keys(limitedData[0] || {}).map((key) => {
+    const title = key.replace(/_/g, ' '); // 将下划线替换成空格
+    let minWidth = title.length * 10;
+    if (key === 'ASIN' || key === 'brand') {
+      minWidth = 130;
+    }
+    return {
+      field: key,
+      title,
+      minWidth,
+    };
+  });
+
+  gridOptions.data = limitedData;
+
+  // 更新默认标签
+  if (limitedData[0].brand) {
+    defaultLabel.value = 'Brand';
+  } else {
+    defaultLabel.value = 'ASIN';
+  }
+}
+
+/**
+ * 确认导入
+ */
+async function confirmUpload() {
+  try {
+    const response = await api.confirmUploadFile({ upload: true });
+    handleResponse(response);
+    if (response.code === SUCCESS_CODE) {
+      gridOptions.data = [];
+      gridOptions.columns = [];
+      filter.reportFilter = '';
+      filter.reportDateFilter = '';
+      filter.typeFilter = '';
+      filter.variableFilter = '';
+    }
+  } catch (error) {
+    console.error('==Error==', error);
+  }
+}
+</script>
+
+<template>
+  <div class="py-2 px-2.5" style="background-color: #f7f7f7">
+    <el-card body-class="flex justify-between gap-3.5" shadow="hover" style="border: none; margin-bottom: 10px">
+      <div class="flex flex-wrap gap-7">
+        <div>
+          <span class="font-bold mr-2" style="color: #303133">报告类型:</span>
+          <el-input v-model="filter.reportFilter" :disabled="true" style="width: 200px"></el-input>
+        </div>
+        <div>
+          <span class="font-bold mr-2" style="color: #303133">报告日期:</span>
+          <el-input v-model="filter.reportDateFilter" :disabled="true" style="width: 200px"></el-input>
+        </div>
+        <div>
+          <span class="font-bold mr-2" style="color: #303133">类型:</span>
+          <el-input v-model="filter.typeFilter" :disabled="true" style="width: 200px"></el-input>
+        </div>
+        <div>
+          <span class="font-bold mr-2" style="color: #303133">{{ defaultLabel }}:</span>
+          <el-input v-model="filter.variableFilter" :disabled="true" style="width: 240px"></el-input>
+        </div>
+      </div>
+      <div class="flex gap-3.5">
+        <!-- 想要不页面不跳动可以加72的高度 -->
+        <div>
+          <el-upload
+            ref="upload"
+            action="#"
+            :limit="1"
+            :show-file-list="false"
+            :auto-upload="true"
+            :on-exceed="handleExceed"
+            :http-request="handleCustomUpload">
+            <template #trigger>
+              <el-button :loading="upBtnLoading" plain color="#6366f1" :icon="View">导入预览</el-button>
+            </template>
+          </el-upload>
+        </div>
+        <el-button plain round type="warning" :icon="Upload" @click="confirmUpload"> 确认导入 </el-button>
+      </div>
+    </el-card>
+    <el-card shadow="hover" style="border: none">
+      <div class="text-xl font-bold text-center font-sans subpixel-antialiased">导入预览</div>
+      <vxe-grid v-bind="gridOptions"></vxe-grid>
+    </el-card>
+  </div>
+</template>
+
+<style scoped></style>

+ 19 - 0
src/views/system/log/loginLog/crud.tsx

@@ -23,6 +23,19 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 				editRequest,
 				delRequest,
 			},
+			table: {
+				border: true,
+				bordered: true,
+				height: "100%",
+				// rowKey: "id",
+				// size: "small",
+				stripe: true,
+			},
+			toolbar: {
+				style: {
+					margin: '8px 0 8px 0',
+				}
+			},
 			actionbar: {
 				buttons: {
 					add: {
@@ -117,6 +130,12 @@ export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProp
 						},
 					},
 				},
+				create_datetime: {
+					title: '登录时间',
+					column: {
+						minWidth: 100,
+					},
+				},
 				isp: {
 					title: '运营商',
 					search: {

+ 4 - 0
src/views/system/login/component/account.vue

@@ -84,6 +84,7 @@ export default defineComponent({
       if (!formRef.value) return
       await formRef.value.validate((valid: any) => {
         if (valid) {
+          state.loading.signIn = true
           loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
             if (res.code === 2000) {
               Session.set('token', res.data.access)
@@ -92,16 +93,19 @@ export default defineComponent({
                 // 前端控制路由,2、请注意执行顺序
                 initFrontEndControlRoutes()
                 loginSuccess()
+                state.loading.signIn = false
               } else {
                 // 模拟后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
                 // 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
                 initBackEndControlRoutes()
                 // 执行完 initBackEndControlRoutes,再执行 signInSuccess
                 loginSuccess()
+                state.loading.signIn = false
               }
             } else if (res.code === 4000) {
               // 登录错误之后,刷新验证码
               refreshCaptcha()
+              state.loading.signIn = false
             }
           })
         } else {

+ 7 - 0
vite.config.ts

@@ -4,6 +4,7 @@ import { defineConfig, loadEnv, ConfigEnv } from 'vite'
 import vueSetupExtend from 'vite-plugin-vue-setup-extend'
 import vueJsx from '@vitejs/plugin-vue-jsx'
 import AutoImport from 'unplugin-auto-import/vite'
+import compression from 'vite-plugin-compression'
 import Components from 'unplugin-vue-components/vite'
 import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
 
@@ -28,6 +29,12 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
         imports: ['vue', 'vue-router', 'pinia'],
         // resolvers: [ElementPlusResolver()],
       }),
+      compression({
+        algorithm: 'gzip', // 使用 gzip 压缩
+        ext: '.gz', // 输出的文件扩展名
+        threshold: 10240, // 只有大小大于该值的资源会被压缩(默认 10KB)
+        deleteOriginFile: false, // 是否删除原始未压缩的文件
+      }),
       // Components({
       //   resolvers: [ElementPlusResolver()],
       // }),

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott