Ver código fonte

feature:数据统计功能样式更新

cmy 3 semanas atrás
pai
commit
9198e26169

+ 15 - 0
src/views/home/i18n/en.ts

@@ -6,6 +6,21 @@ export default {
 		auditLogsTip: 'audit logs',
 		moreTip: 'more',
 		newsletterTip: 'news letter',
+
+		// 访客趋势图
+		visitorsTrend: 'Visits Over Time',
+		// 引用域
+		referenceDomain: 'Referring Domain',
+		// 访客地图
+		visitorsMap: 'Visitor Map',
+		// 访客概览
+		visitorsOverview: 'Visits Overview',
+		// 关键词频率
+		keywordsFrequency: 'Keywords Frequency',
+		// 待办任务
+		pendingTasks: 'Pending Tasks',
+		// 抄送任务
+		ccTasks: 'CC Tasks',
 	},
 	schedule: {
 		index: '#',

+ 14 - 0
src/views/home/i18n/zh-cn.ts

@@ -6,6 +6,20 @@ export default {
 		auditLogsTip: '审计日志',
 		moreTip: '更多',
 		newsletterTip: '站内信',
+		// 访客趋势图
+		visitorsTrend: '访客趋势图',
+		// 引用域
+		referenceDomain: '引用域',
+		// 访客地图
+		visitorsMap: '访客地图',
+		// 访客概览
+		visitorsOverview: '访客概览',
+		// 关键词频率
+		keywordsFrequency: '关键词频率',
+		// 待办任务
+		pendingTasks: '待办任务',
+		// 抄送任务
+		ccTasks: '抄送任务',
 	},
 	schedule: {
 		index: '#',

+ 8 - 5
src/views/home/index.vue

@@ -4,27 +4,27 @@
       <userInfo />
     </el-col>
     <el-col :xs="24" :sm="24" :md="16" :lg="10" :xl="10">
-      <custom-panel :title="'访客趋势图'" @refresh="handlePanelRefresh('visitorTrend')">
+      <custom-panel :title="t('home.visitorsTrend')" @refresh="handlePanelRefresh('visitorTrend')">
         <visitor-trend ref="visitorTrendRef" />
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="24" :md="24" :lg="8" :xl="9">
-      <custom-panel :title="'引用域'" @refresh="handlePanelRefresh('trafficSources')">
+      <custom-panel :title="t('home.referenceDomain')" @refresh="handlePanelRefresh('trafficSources')">
         <traffic-sources ref="trafficSourcesRef" />
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="24" :md="24" :lg="10" :xl="10">
-      <custom-panel :title="'访客地图'">
+      <custom-panel :title="t('home.visitorsMap')">
         <visitor-map />
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="10" :md="10" :lg="6" :xl="6">
-      <custom-panel :title="'访客概览'">
+      <custom-panel :title="t('home.visitorsOverview')">
         <visitor-overview />
       </custom-panel>
     </el-col>
     <el-col :xs="24" :sm="14" :md="14" :lg="8" :xl="8">
-      <custom-panel :title="'关键词频率'">
+      <custom-panel :title="t('home.keywordsFrequency')">
         <keyword-frequency />
       </custom-panel>
     </el-col>
@@ -34,6 +34,9 @@
 <script setup lang="ts" name="home">
 import { defineAsyncComponent } from 'vue';
 import { debounce } from 'lodash';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const customPanel = defineAsyncComponent(() => import('./echarts/custom-panel.vue'));
 const userInfo = defineAsyncComponent(() => import('./user-info.vue'));

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

@@ -34,7 +34,7 @@
       </div>
       <div class="task-list">
         <div class="task-item">
-          <div class="task-title">代办任务</div>
+          <div class="task-title">{{t('home.pendingTasks')}}</div>
           <div class="task-num">0</div>
           <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
             <path d="M3.125 11.875H26.875V25.625C26.875 26.3154 26.3154 26.875 25.625 26.875H4.375C3.68464 26.875 3.125 26.3154 3.125 25.625V11.875Z" stroke="#167AF0" stroke-width="2" stroke-linejoin="round"/>
@@ -45,7 +45,7 @@
           </svg>
         </div>
         <div class="task-item">
-          <div class="task-title">抄送任务</div>
+          <div class="task-title">{{t('home.ccTasks')}}</div>
           <div class="task-num">0</div>
           <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
             <path d="M26.875 3.125L18.5625 26.875L13.8125 16.1875L3.125 11.4375L26.875 3.125Z" stroke="#167AF0" stroke-width="2" stroke-linejoin="round"/>
@@ -59,6 +59,9 @@
 
 <script setup lang="ts">
 import {useUserInfo} from '/@/stores/userInfo';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const { user } = useUserInfo().userInfos;
 console.log('user', user);

+ 1 - 0
src/views/marketing/statistics/i18n/en.ts

@@ -10,5 +10,6 @@ export default {
 		offlineBtn: 'offline',
 		offlineConfirmText: 'offline confirm',
 		offlineSuccessText: 'offline success',
+		fingerprint: 'fingerprint',
 	},
 };

+ 1 - 0
src/views/marketing/statistics/i18n/zh-cn.ts

@@ -12,5 +12,6 @@ export default {
 		inputDomainTip: '请输入域名',
 		queryBtn: '查询',
 		resetBtn: '重置',
+		fingerprint: '指纹',
 	},
 };

+ 235 - 48
src/views/marketing/statistics/index.vue

@@ -1,45 +1,65 @@
 <template>
-	<div class="layout-padding">
-		<div class="layout-padding-auto layout-padding-view">
-			<el-row class="ml10" v-show="showSearch">
-				<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
-					<el-form-item :label="t('systoken.ip')" prop="ip">
-						<el-input :placeholder="t('systoken.inputIpTip')" v-model="state.queryForm.ip"></el-input>
-					</el-form-item>
-                    <el-form-item :label="t('systoken.domain')" prop="domain">
-						<el-input :placeholder="t('systoken.inputDomainTip')" v-model="state.queryForm.domain"></el-input>
-					</el-form-item>
-                     <el-form-item :label="t('systoken.referrer')" prop="referrer">
-						<el-input :placeholder="t('systoken.inputReferrer')" v-model="state.queryForm.referrer"></el-input>
-					</el-form-item>
-					<el-form-item>
-						<el-button @click="getDataList" icon="Search" type="primary">{{ t('common.queryBtn') }} </el-button>
-						<el-button @click="resetQuery" icon="Refresh">{{ t('common.resetBtn') }}</el-button>
-					</el-form-item>
-				</el-form>
-			</el-row>
-			<el-table
-				:data="state.dataList"
-				@sort-change="sortChangeHandle"
-				style="width: 100%"
-				v-loading="state.loading"
-				border
-				:cell-style="tableStyle.cellStyle"
-				:header-cell-style="tableStyle.headerCellStyle"
-			>
-				<!-- <el-table-column align="center" type="selection" width="40" /> -->
-				<el-table-column :label="t('systoken.ip')" prop="ip" show-overflow-tooltip></el-table-column>
-				<el-table-column :label="t('systoken.domain')" prop="domain" show-overflow-tooltip ></el-table-column>
-				<el-table-column :label="t('systoken.content')" prop="total" show-overflow-tooltip></el-table-column>
-				<el-table-column :label="t('systoken.dayActive')" prop="daily" show-overflow-tooltip></el-table-column>
-				<el-table-column :label="t('systoken.hourActive')" prop="hourly" show-overflow-tooltip></el-table-column>
-				<el-table-column :label="t('systoken.fifteenOnline')" prop="online" show-overflow-tooltip></el-table-column>
-				<el-table-column :label="t('systoken.referrer')" prop="referrer" show-overflow-tooltip></el-table-column>
-			</el-table>
-
-			<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination"> </pagination>
-		</div>
-	</div>
+  <div class="layout-padding">
+    <div class="layout-padding-auto layout-padding-view">
+      <el-row class="ml10" v-show="showSearch">
+        <el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
+          <el-form-item :label="t('systoken.ip')" prop="ip">
+            <el-input :placeholder="t('systoken.inputIpTip')" v-model="state.queryForm.ip"></el-input>
+          </el-form-item>
+          <el-form-item :label="t('systoken.domain')" prop="domain">
+            <el-input :placeholder="t('systoken.inputDomainTip')" v-model="state.queryForm.domain"></el-input>
+          </el-form-item>
+          <el-form-item :label="t('systoken.referrer')" prop="referrer">
+            <el-input :placeholder="t('systoken.inputReferrer')" v-model="state.queryForm.referrer"></el-input>
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="getDataList" icon="Search" type="primary">{{ t('common.queryBtn') }} </el-button>
+            <el-button @click="resetQuery" icon="Refresh">{{ t('common.resetBtn') }}</el-button>
+          </el-form-item>
+        </el-form>
+      </el-row>
+      <el-table class="statistics-table" :data="state.dataList" row-key="ip" @sort-change="sortChangeHandle" style="width: 100%"
+        v-loading="state.loading" border :cell-style="tableStyle.cellStyle"
+        :header-cell-style="tableStyle.headerCellStyle" @row-click="toggleRowExpansion"
+        :expand-row-keys="expandedRowKeys"
+        @expand-change="handleExpandChange"
+        >
+        <!-- <el-table-column align="center" type="selection" width="40" /> -->
+        <el-table-column show-overflow-tooltip type="expand">
+          <template #default="{ row }">
+            <div class="child-table-container">
+              <el-table :data="row.childTableData" v-loading="row.childTableData.childLoading" border :cell-style="tableStyle.cellStyle"
+                :header-cell-style="tableStyle.headerCellStyle">
+                <!-- <el-table-column :label="t('systoken.ip')" prop="ip" show-overflow-tooltip></el-table-column> -->
+                <el-table-column :label="t('systoken.domain')" prop="domain" show-overflow-tooltip></el-table-column>
+                <el-table-column :label="t('systoken.fingerprint')" prop="finger" show-overflow-tooltip></el-table-column>
+                <el-table-column :label="t('systoken.referrer')" prop="referrer" show-overflow-tooltip></el-table-column>
+                <el-table-column :label="t('systoken.content')" prop="total" show-overflow-tooltip></el-table-column>
+                <el-table-column :label="t('systoken.dayActive')" prop="daily" show-overflow-tooltip></el-table-column>
+                <el-table-column :label="t('systoken.hourActive')" prop="hourly" show-overflow-tooltip></el-table-column>
+                <el-table-column :label="t('systoken.fifteenOnline')" prop="online" show-overflow-tooltip></el-table-column>
+              </el-table>
+              <div class="pagination-container" @click.stop>
+                <pagination v-bind="row.childPagination" @size-change="size => handlePageSizeChange(row, size)"
+                  @current-change="page => handlePageChange(row, page)" />
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('systoken.ip')" prop="ip" show-overflow-tooltip></el-table-column>
+        <!-- <el-table-column :label="t('systoken.domain')" prop="domain" show-overflow-tooltip></el-table-column> -->
+        <!-- <el-table-column :label="t('systoken.fingerprint')" show-overflow-tooltip>--</el-table-column>
+        <el-table-column :label="t('systoken.referrer')" show-overflow-tooltip>--</el-table-column> -->
+        <el-table-column :label="t('systoken.content')" prop="total" show-overflow-tooltip></el-table-column>
+        <el-table-column :label="t('systoken.dayActive')" prop="daily" show-overflow-tooltip></el-table-column>
+        <el-table-column :label="t('systoken.hourActive')" prop="hourly" show-overflow-tooltip></el-table-column>
+        <el-table-column :label="t('systoken.fifteenOnline')" prop="online" show-overflow-tooltip></el-table-column>
+      </el-table>
+
+      <pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination">
+      </pagination>
+    </div>
+  </div>
 </template>
 
 <script lang="ts" setup>
@@ -54,18 +74,185 @@ const showSearch = ref(true);
 
 //  table hook
 const state: BasicTableProps = reactive<BasicTableProps>({
-	queryForm: {
-		ip: '',
-        domain:''
-	},
-	pageList: pageList,
+  queryForm: {
+    ip: '',
+    domain: ''
+  },
+  pageList: pageList,
 });
 const { getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
 
 // 清空搜索条件
 const resetQuery = () => {
-	queryRef.value?.resetFields();
-	getDataList();
+  queryRef.value?.resetFields();
+  getDataList();
+};
+
+// 初始化子表格数据和分页
+const initChildData = (parentRow) => {
+  parentRow.expanded = false;
+  parentRow.childTableData = [
+    {
+      "ip": "",
+      "domain": "192.168.3.9",
+      "referrer": "http://192.168.3.9:5174/",
+      "total": "1",
+      "daily": "0",
+      "hourly": "0",
+      "online": null
+    },
+    {
+      "ip": "",
+      "domain": "192.168.3.9",
+      "referrer": "http://192.168.3.9:5174/www.baidu.com",
+      "total": "2",
+      "daily": "0",
+      "hourly": "0",
+      "online": null
+    },
+    {
+      "ip": "",
+      "domain": "localhost",
+      "referrer": "",
+      "total": "73",
+      "daily": "0",
+      "hourly": "0",
+      "online": null
+    },
+    {
+      "ip": "",
+      "domain": "192.168.3.9",
+      "referrer": "",
+      "total": "75",
+      "daily": "1",
+      "hourly": "0",
+      "online": null
+    },
+    {
+      "ip": "",
+      "domain": "192.168.3.9",
+      "referrer": "",
+      "total": "2",
+      "daily": "0",
+      "hourly": "0",
+      "online": null
+    },
+    {
+      "ip": "",
+      "domain": "192.168.0.77",
+      "referrer": "",
+      "total": "25",
+      "daily": "0",
+      "hourly": "0",
+      "online": null
+    }
+  ];
+  parentRow.childLoading = false;
+  parentRow.childPagination = {
+    page: 1,
+    size: 5,
+    total: 6,
+    pageSizes: [1, 5, 10, 20]
+  };
+};
+
+// 初始化所有父行
+watchEffect(() => {
+  state?.dataList?.forEach(row => {
+    initChildData(row);
+  })
+}, [state.pageList])
+
+// 切换行展开状态
+const toggleRowExpansion = (row: any, column: any, event: Event) => {
+  console.log(row);
+  if ((event.target as HTMLElement).closest('.pagination-container')) {
+    return;
+  }
+
+  row.expanded = !row.expanded;
+  if (row.expanded && row.childTableData.length === 0) {
+    loadChildData(row);
+  }
+};
+
+// 加载子表格数据(模拟API请求)
+const loadChildData = async (parentRow) => {
+  parentRow.childLoading = true;
+  try {
+    // 模拟API请求参数
+    const params = {
+      parentId: parentRow.id,
+      page: parentRow.childPagination.page,
+      pageSize: parentRow.childPagination.size
+    };
+
+    // 模拟API请求延迟
+    await new Promise(resolve => setTimeout(resolve, 800));
+
+    // 生成模拟数据
+    const mockData = [];
+    const startIndex = (params.page - 1) * params.pageSize;
+
+    for (let i = 0; i < params.pageSize; i++) {
+      const index = startIndex + i;
+      if (index >= parentRow.childPagination.total) break;
+
+      mockData.push({
+        "ip": parentRow.ip,
+        "domain": "192.168.3.9",
+        "referrer": "http://192.168.3.9:5174/",
+        "total": "1",
+        "daily": "0",
+        "hourly": "0",
+        "online": Math.random() < 0.5 ? null : Math.floor(Math.random() * 100)
+      });
+    }
+
+    // 更新数据
+    parentRow.childTableData = mockData;
+    parentRow.childPagination.total = parentRow.childPagination.total+=0;
+  } finally {
+    parentRow.childLoading = false;
+  }
+};
+
+// 处理分页大小变化
+const handlePageSizeChange = (row: any, size: number) => {
+  row.childPagination.size = size;
+  row.childPagination.page = 1; // 重置为第一页
+  loadChildData(row);
+};
+
+// 处理页码变化
+const handlePageChange = (row: any, page: number) => {
+  row.childPagination.page = page;
+  console.log('page', page);
+  
+  loadChildData(row);
+};
+
+const expandedRowKeys = ref<number[]>([]);
+const handleExpandChange = (row: any, expandedRows: any[]) => {
+  expandedRowKeys.value = expandedRows.map(item => item.ip);
 };
 
 </script>
+
+<style lang="scss">
+// .statistics-table {
+//   tr>.el-table__cell.el-table__expanded-cell {
+//     padding-top: 0;
+//   }
+// }
+.child-table-container {
+  margin: 20px;
+}
+// .child-table-container {
+//   margin: -1px 0 0 48px;
+
+//   .el-table__header {
+//     display: none;
+//   }
+// }
+</style>