Przeglądaj źródła

fix: bug营销项目修改

jcq 1 tydzień temu
rodzic
commit
839e2e0193

+ 2 - 2
.env

@@ -7,8 +7,8 @@ VITE_PUBLIC_PATH = /
 # 后端请求前缀
 # VITE_API_URL = http://192.168.10.101:9999 
 # VITE_API_URL = http://192.168.3.118:9999
- VITE_API_URL = http://192.168.3.17:9999
-#  VITE_API_URL = http://16.162.25.197:9999
+#  VITE_API_URL = http://192.168.3.17:9999
+ VITE_API_URL = http://43.199.205.45:9999
 
 # OAUTH2 密码模式客户端信息
 VITE_OAUTH2_PASSWORD_CLIENT='pig:pig'

+ 20 - 0
src/api/marketing/data.ts

@@ -31,3 +31,23 @@ export const getOnlineUsers = () => {
 	});
 };
 
+
+//获取访客地图
+export const getMapData = (data : Object) => {
+	return request({
+		url: 'marketing/tcp/statistics/users/region',
+		method: 'post',
+		data
+	});
+};
+
+//获取关键词频率
+export const getKeywordData = (params : Object) => { 
+	return request({
+		url: 'marketing/push/stat/keyword',
+		method: 'get',
+		params
+	});
+};
+
+

+ 5 - 1
src/views/home/echarts/custom-panel.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-card shadow="hover">
+  <el-card shadow="hover" v-loading="prop.loading">
     <div class="panel-top">
       <div class="panel-name">{{prop.title}}</div>
       <div :class="['panel-refresh', { 'panel-refresh-rotate': roading }]"  @click="handleRefresh">
@@ -22,6 +22,10 @@ const prop = defineProps({
   title: {
     type: String,
     default: ''
+  },
+  loading: {
+    type: Boolean,
+    default: false
   }
 })
 

+ 121 - 80
src/views/home/echarts/keyword-frequency.vue

@@ -12,32 +12,41 @@
 
 <script setup lang="ts" name="keywordFrequency">
 import { ref, onMounted, onUnmounted, watch } from 'vue';
+import { getKeywordData } from '/@/api/marketing/data';
+
 import * as echarts from 'echarts';
+const emit = defineEmits(['success']);
 
-const chartData = ref([
-	{ name: '下载', value: 30, color: '#ff6384' },
-	{ name: '宅六', value: 10, color: '#4bc0c0' },
-	{ name: '张三', value: 20, color: '#36a2eb' },
-	{ name: '里斯', value: 25, color: '#cc65fe' },
-	{ name: '王五', value: 15, color: '#ffce56' },
-]);
+interface KeywordDataItem {
+	keyword: string;
+	count: number;
+}
 
-	const value = ref('7');
-	const options = [
-		{
-			value: '7',
-			label: '7天',
-			selected: true,
-		},
-		{
-			value: '30',
-			label: '30天',
-		},
-	];
+const chartData = ref<KeywordDataItem[]>([
+
+]);
+const prop = defineProps({
+	flushed: {
+	  type: Boolean,
+	  default: false
+	}
+  })
+
+const value = ref(7);
+const options = [
+	{
+		value: 7,
+		label: '7天',
+		selected: true,
+	},
+	{
+		value: 30,
+		label: '30天',
+	},
+];
 
 const chartRef = ref(null);
 let chartInstance: echarts.ECharts | null = null;
-console.log(chartInstance);
 
 // 初始化图表
 const initChart = () => {
@@ -54,74 +63,94 @@ const initChart = () => {
 
 	// 处理数据
 	const colorPalette = [
-  '#ff6384', '#4bc0c0', '#36a2eb', '#cc65fe', '#ffce56',
-  '#f87171', '#fbbf24', '#34d399', '#60a5fa', '#a78bfa',
-  '#f472b6', '#facc15', '#2dd4bf', '#818cf8', '#f59e42',
-  '#eab308', '#10b981', '#3b82f6', '#6366f1', '#f43f5e',
-  '#a3e635', '#fcd34d', '#fca5a5', '#c084fc', '#f9fafb'
-];
-
-const realData = chartData.value;
-const totalBubbles = 25;
-const scatterData: any[] = [];
-const minRadius = 10;
-const maxRadius = 18;
-const padding = 2; // 气泡之间的最小间距
-const maxTry = 1000;
-
-function isOverlap(x: number, y: number, r: number, arr: any[]) {
-  for (const b of arr) {
-    const dx = x - b.value[0];
-    const dy = y - b.value[1];
-    const dist = Math.sqrt(dx * dx + dy * dy);
-    if (dist < r + b.value[2] + padding) return true;
-  }
-  return false;
-}
+		'#ff6384',
+		'#4bc0c0',
+		'#36a2eb',
+		'#cc65fe',
+		'#ffce56',
+		'#f87171',
+		'#fbbf24',
+		'#34d399',
+		'#60a5fa',
+		'#a78bfa',
+		'#f472b6',
+		'#facc15',
+		'#2dd4bf',
+		'#818cf8',
+		'#f59e42',
+		'#eab308',
+		'#10b981',
+		'#3b82f6',
+		'#6366f1',
+		'#f43f5e',
+		'#a3e635',
+		'#fcd34d',
+		'#fca5a5',
+		'#c084fc',
+		'#f9fafb',
+	];
 
-function placeBubble(radius: number, arr: any[]): [number, number] {
-  let tryCount = 0;
-  while (tryCount < maxTry) {
-    const x = Math.random() * (100 - 2 * radius) + radius;
-    const y = Math.random() * (100 - 2 * radius) + radius;
-    if (!isOverlap(x, y, radius, arr)) return [x, y];
-    tryCount++;
-  }
-  // fallback: 网格法兜底
-  return [Math.random() * 100, Math.random() * 100];
-}
+	const realData = chartData.value;
+	const totalBubbles = 10;
+	const scatterData: any[] = [];
+	const minRadius = 10;
+	const maxRadius = 18;
+	const padding = 2; // 气泡之间的最小间距
+	const maxTry = 1000;
+
+	function isOverlap(x: number, y: number, r: number, arr: any[]) {
+		for (const b of arr) {
+			const dx = x - b.value[0];
+			const dy = y - b.value[1];
+			const dist = Math.sqrt(dx * dx + dy * dy);
+			if (dist < r + b.value[2] + padding) return true;
+		}
+		return false;
+	}
 
-// 先放真实数据
-realData.forEach((item, i) => {
-  const radius = maxRadius;
-  const [x, y] = placeBubble(radius, scatterData);
-  scatterData.push({
-    value: [x, y, radius],
-    itemStyle: { color: colorPalette[i % colorPalette.length] },
-    name: `${item.name}\n${item.value}`,
-  });
-});
+	function placeBubble(radius: number, arr: any[]): [number, number] {
+		let tryCount = 0;
+		while (tryCount < maxTry) {
+			const x = Math.random() * (100 - 2 * radius) + radius;
+			const y = Math.random() * (100 - 2 * radius) + radius;
+			if (!isOverlap(x, y, radius, arr)) return [x, y];
+			tryCount++;
+		}
+		// fallback: 网格法兜底
+		return [Math.random() * 100, Math.random() * 100];
+	}
 
-// 填充空白气泡
-for (let j = realData.length; j < totalBubbles; j++) {
-  const radius = minRadius + Math.random() * (maxRadius - minRadius) * 0.5;
-  const [x, y] = placeBubble(radius, scatterData);
-  scatterData.push({
-    value: [x, y, radius],
-    itemStyle: { color: colorPalette[j % colorPalette.length] },
-    name: '',
-    label: { show: false }
-  });
-}
+	// 先放真实数据
+	realData.forEach((item, i) => {
+		const radius = maxRadius;
+		const [x, y] = placeBubble(radius, scatterData);
+		scatterData.push({
+			value: [x, y, radius],
+			itemStyle: { color: colorPalette[i % colorPalette.length] },
+			name: `${item.keyword}\n${item.count}`,
+		});
+	});
+
+	// 填充空白气泡
+	for (let j = realData.length; j < totalBubbles; j++) {
+		const radius = minRadius + Math.random() * (maxRadius - minRadius) * 0.5;
+		const [x, y] = placeBubble(radius, scatterData);
+		scatterData.push({
+			value: [x, y, radius],
+			itemStyle: { color: colorPalette[j % colorPalette.length] },
+			name: '',
+			label: { show: false },
+		});
+	}
 
 	// 配置项
 	const option = {
 		tooltip: {
 			trigger: 'item',
-			formatter: '{b}',
+			formatter: '{b}',
 		},
 		grid: {
-            top: '40%',
+			top: '40%',
 		},
 		xAxis: { show: false },
 		yAxis: { show: false },
@@ -130,7 +159,7 @@ for (let j = realData.length; j < totalBubbles; j++) {
 				type: 'scatter',
 				data: scatterData,
 				symbolSize: (value: any) => {
-					return value[2] * 3; // 根据值计算气泡大小
+					return value[2] * 5; // 根据值计算气泡大小
 				},
 				label: {
 					show: true,
@@ -159,10 +188,22 @@ const handleResize = () => {
 		chartInstance.resize();
 	}
 };
+const getData = async () => {
+	const res = await getKeywordData({ days: value.value });
+	chartData.value = res.data;
+	initChart();
+	emit('success');
+};
 
 // 生命周期钩子
 onMounted(() => {
-	initChart();
+	getData();
+});
+
+watch(() => prop.flushed, (val) => {
+	if(val){
+		getData();
+	}
 });
 
 onUnmounted(() => {

+ 74 - 51
src/views/home/echarts/visitor-map.vue

@@ -5,12 +5,12 @@
 			<el-select v-model="selectedMap" style="width: 120px" @change="initChart">
 				<el-option v-for="item in mapOptions" :key="item.value" :label="item.label" :value="item.value" />
 			</el-select>
-			<el-select v-model="classify" class="ml-2" style="width: 160px" @change="initChart">
+			<el-select v-model="classify" class="ml-2" style="width: 160px" @change="getData">
 				<el-option v-for="item in classifyOptions" :key="item.value" :label="item.label" :value="item.value" />
 			</el-select>
 		</div>
 		<div style="position: absolute; top: 20px; right: 0">
-			<el-select v-model="value" style="width: 84px" @change="initChart">
+			<el-select v-model="value" style="width: 84px" @change="getData">
 				<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
 			</el-select>
 		</div>
@@ -23,8 +23,17 @@ import * as echarts from 'echarts/core';
 import { MapChart } from 'echarts/charts';
 import { TooltipComponent, GeoComponent } from 'echarts/components';
 import { CanvasRenderer } from 'echarts/renderers';
+import { getMapData } from '/@/api/marketing/data';
 
 echarts.use([MapChart, TooltipComponent, GeoComponent, CanvasRenderer]);
+const emit = defineEmits(['success']);
+
+const prop = defineProps({
+	flushed: {
+	  type: Boolean,
+	  default: false
+	}
+  })
 
 const chartRef = ref<HTMLElement | null>(null);
 let chartInstance: echarts.ECharts | null = null;
@@ -38,56 +47,49 @@ const mapOptions = [
 	{ value: 'oceania', label: '大洋洲' },
 ];
 const selectedMap = ref('world');
-const value = ref('7');
+const value = ref(7);
 const options = [
 	{
-		value: '7',
+		value: 7,
 		label: '7天',
 		selected: true,
 	},
 	{
-		value: '30',
+		value: 30,
 		label: '30天',
 	},
 ];
 
-const classify = ref('1');
+const classify = ref('visitor');
 const classifyOptions = [
+	// {
+	// 	value: '1',
+	// 	label: '访问',
+	// },
 	{
-		value: '1',
-		label: '访问',
-		selected: true,
-	},
-	{
-		value: '2',
+		value: 'visitor',
 		label: '访客数',
-		selected: true,
-	},
-	{
-		value: '3',
-		label: '活动',
-		selected: true,
-	},
-	{
-		value: '4',
-		label: '用户管理',
-		selected: true,
-	},
-	{
-		value: '5',
-		label: '平均活动次数',
-		selected: true,
-	},
-	{
-		value: '6',
-		label: '平均网站停留时间',
-		selected: true,
-	},
-	{
-		value: '7',
-		label: '跳出率',
-		selected: true,
 	},
+	// {
+	// 	value: '3',
+	// 	label: '活动',
+	// },
+	// {
+	// 	value: '4',
+	// 	label: '用户管理',
+	// },
+	// {
+	// 	value: '5',
+	// 	label: '平均活动次数',
+	// },
+	// {
+	// 	value: '6',
+	// 	label: '平均网站停留时间',
+	// },
+	// {
+	// 	value: '7',
+	// 	label: '跳出率',
+	// },
 ];
 
 const mapFileMap: Record<string, string> = {
@@ -99,6 +101,18 @@ const mapFileMap: Record<string, string> = {
 	africa: '/map/africa.geo.json',
 	oceania: '/map/oceania.geo.json',
 };
+const chartData = ref([
+	{ name: '日本', value: 4000 },
+	{ name: '韩国', value: 3500 },
+	{ name: '印度尼西亚', value: 3000 },
+	{ name: '沙特阿拉伯', value: 2500 },
+	{ name: '伊朗', value: 2000 },
+	{ name: '土耳其', value: 1800 },
+	{ name: '泰国', value: 1600 },
+	{ name: '越南', value: 1400 },
+	{ name: '菲律宾', value: 1200 },
+	{ name: '巴基斯坦 ', value: 1000 },
+]);
 
 async function initChart() {
 	if (!chartRef.value) return;
@@ -158,18 +172,7 @@ async function initChart() {
 						areaColor: '#58a0f4',
 					},
 				},
-				data: [
-					{ name: '日本', value: 4000 },
-					{ name: '韩国', value: 3500 },
-					{ name: '印度尼西亚', value: 3000 },
-					{ name: '沙特阿拉伯', value: 2500 },
-					{ name: '伊朗', value: 2000 },
-					{ name: '土耳其', value: 1800 },
-					{ name: '泰国', value: 1600 },
-					{ name: '越南', value: 1400 },
-					{ name: '菲律宾', value: 1200 },
-					{ name: '巴基斯坦 ', value: 1000 },
-				],
+				data: chartData.value,
 			},
 		],
 	};
@@ -183,10 +186,30 @@ const handleResize = () => {
 	}
 };
 
-onMounted(() => {
+const getData = async () => {
+	const res = await getMapData({
+		queryType: classify.value,
+		days: value.value,
+	});
+	chartData.value = res.data.countryStatistics?.map((item: { country: string; userCount: number }) => {
+		return {
+			name: item.country,
+			value: item.userCount
+		}
+	});
 	initChart();
+	emit('success');
+};
+
+onMounted(() => {
+	getData();
 });
 
+watch(() => prop.flushed, (val) => {
+	if(val){
+		getData();
+	}
+});
 onUnmounted(() => {
 	if (chartInstance) {
 		window.removeEventListener('resize', handleResize);

+ 6 - 4
src/views/home/index.vue

@@ -14,8 +14,8 @@
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="24" :md="24" :lg="10" :xl="10">
-      <custom-panel :title="t('home.visitorsMap')">
-        <visitor-map />
+      <custom-panel :title="t('home.visitorsMap')"  @refresh="flushedMap = true" :loading="flushedMap">
+        <visitor-map :flushed="flushedMap" @success="flushedMap = false"  />
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="10" :md="10" :lg="6" :xl="6">
@@ -24,8 +24,8 @@
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="14" :md="14" :lg="8" :xl="8">
-      <custom-panel :title="t('home.keywordsFrequency')">
-        <keyword-frequency />
+      <custom-panel :title="t('home.keywordsFrequency')" @refresh="flushedKeyword = true" :loading="flushedKeyword">
+        <keyword-frequency :flushed="flushedKeyword" @success="flushedKeyword = false" />
       </custom-panel>
     </el-col>
   </el-row>
@@ -48,6 +48,8 @@ const VisitorOverview = defineAsyncComponent(() => import('./echarts/visitor-ove
 
 const visitorTrendRef = ref(null);
 const trafficSourcesRef = ref(null);
+const flushedKeyword = ref(false);
+const flushedMap = ref(false);
 
 const handlePanelRefresh = (value: string) => {
   if (value == 'visitorTrend' && visitorTrendRef.value) {

+ 0 - 2
src/views/home/user-info.vue

@@ -50,7 +50,6 @@
                 fill="#167AF0" />
             </g>
           </svg>
-
         </div>
         <div class="task-item">
           <div class="task-title">在线用户</div>
@@ -404,7 +403,6 @@ const postName = computed(() => {
     width: 100%;
     border-radius: 8px;
     background-color: #167AF00D;
-    max-width: 330px;
     padding: 20px;
     margin: 0 auto;
     display: flex;

+ 2 - 4
src/views/marketing/push-logs/index.vue

@@ -28,7 +28,7 @@
           show-overflow-tooltip></el-table-column>
         <el-table-column :formatter="statusFormatter" :label="'推送域名'" prop="pushDomain" min-width="200"
           show-overflow-tooltip></el-table-column> -->
-        <el-table-column :label="'客户端ID'" prop="clientId" min-width="200"
+          <el-table-column  :label="'客户端ID'" prop="clientId" min-width="120"
           show-overflow-tooltip>
         </el-table-column>
         <el-table-column :formatter="statusFormatter" :label="'推送内容'" prop="pushContent" min-width="200"
@@ -64,9 +64,7 @@
             {{ row.pushStatus ? '已推送' : '未推送' }}
           </template>
         </el-table-column>
-        <el-table-column :formatter="statusFormatter" :label="'客户端ID'" prop="clientID" min-width="120"
-          show-overflow-tooltip>
-        </el-table-column>
+        
         <el-table-column :formatter="statusFormatter" :label="'推送详情'" prop="pushDetail" min-width="250">
           <template #default="{ row }">
             <div class="trigger-info" @click="showTriggerInfo(row)">