Quellcode durchsuchen

Merge branch 'master' into feature/cmn-dashboard

# Conflicts:
#	src/views/home/index.vue
cmy vor 3 Wochen
Ursprung
Commit
9bd9784042

+ 52 - 0
mock.json

@@ -0,0 +1,52 @@
+{
+    "code": 0,
+    "msg": null,
+    "data": 
+    {
+      "total": 2,
+      "records": [
+        {
+            "id": 1,
+            "url": "/admin/menu/tree",
+            "config": [
+              {
+                "ip": "美国",
+                "appName": "数据营销平台",
+                "download": "https://www.baidu.com"
+              },
+              {
+                "ip": "中国",
+                "appName": "数据营销平台",
+                "download": "https://www.baidu.com"
+              },
+              {
+                "ip": "英国",
+                "appName": "数据营销平台",
+                "download": "https://www.baidu.com"
+              }
+            ]
+        },
+        {
+            "id": 2,
+            "url": "/admin/menu/t63546ee",
+            "config": [
+              {
+                "ip": "美国",
+                "appName": "数据营销平台",
+                "download": "https://www.baidu.com"
+              },
+              {
+                "ip": "中国",
+                "appName": "数据营销平台",
+                "download": "https://www.baidu.com"
+              },
+              {
+                "ip": "英国",
+                "appName": "数据营销平台",
+                "download": "https://www.baidu.com"
+              }
+            ]
+        }
+    ]
+    }
+}

+ 54 - 0
src/api/marketing/config.ts

@@ -0,0 +1,54 @@
+import request from '/@/utils/request';
+
+export const pageList = (params?: Object) => {
+	return request({
+		url: '/admin/marketing/config/page',
+		method: 'get',
+		params,
+	});
+};
+export const info = (id: String) => {
+	return request({
+		url: `/admin/marketing/config/${id}`,
+		method: 'get',
+	});
+};
+
+export const save = (data: Object) => {
+	return request({
+		url: '/admin/marketing/config/save',
+		method: 'post',
+		data: data,
+	});
+};
+
+export const delObj = (ids: Array<String>) => {
+	return request({
+		url: '/admin/marketing/config/remove',
+		method: 'post',
+		data: ids
+	});
+};
+
+export const getAppList = () => {
+	return request({
+		url: '/admin/marketing/config/apps',
+		method: 'get',
+	});
+};
+
+/**
+ * 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
+ * @method getAdminMenu 获取后端动态路由菜单(admin)
+ */
+export function useMenuApi() {
+	return {
+		getAdminMenu: (params?: object) => {
+			return request({
+				url: '/admin/menu',
+				method: 'get',
+				params,
+			});
+		},
+	};
+}

+ 9 - 0
src/api/marketing/statistics.ts

@@ -0,0 +1,9 @@
+import request from '/@/utils/request';
+
+export function pageList(query: object) {
+    return request({
+        url: 'https://m1.apifoxmock.com/m1/6687089-6396408-default/marketing/statistics/page',
+        method: 'get',
+        data: query,
+    });
+}

+ 1 - 1
src/utils/validate.ts

@@ -191,7 +191,7 @@ export const getRegExp = function (validatorName) {
 		noChinese: '^[^\u4e00-\u9fa5]+$',
 		chinese: '^[\u4e00-\u9fa5]+$',
 		email: '^([-_A-Za-z0-9.]+)@([_A-Za-z0-9]+\\.)+[A-Za-z0-9]{2,3}$',
-		url: '(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]'
+		url: '(https?|ftp|file|http)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]'
 	};
 	return commonRegExp[validatorName];
 };

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

@@ -2,7 +2,7 @@
   <el-card>
     <div class="panel-top">
       <div class="panel-name">{{prop.title}}</div>
-      <div style="cursor: pointer;">
+      <div class="cursor-pointer"  @click="$emit('reload')">
         <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
           <path d="M16.7167 6.66667C15.4895 4.19712 12.9411 2.5 9.99637 2.5C7.05158 2.5 4.56062 4.19712 3.33337 6.66667" stroke="#ADBFD7" stroke-linecap="round"/>
           <path d="M3.33337 3.33334V6.66667" stroke="#ADBFD7" stroke-linecap="round"/>

+ 134 - 0
src/views/home/echarts/keyword-frequency.vue

@@ -0,0 +1,134 @@
+<!-- 访客趋势图组件 -->
+<template>
+	<div class="visitor-trend">
+		<div ref="chartRef" style="width: 100%; height: 300px"></div>
+		<div style="position: absolute; top: 20px; right: 0">
+			<el-select v-model="value" style="width: 84px" @change="initChart">
+				<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+			</el-select>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="keywordFrequency">
+import { ref, onMounted, onUnmounted, watch } from 'vue';
+import * as echarts from 'echarts';
+
+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' },
+]);
+
+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 = () => {
+	if (!chartRef.value) return;
+
+	// 销毁旧实例(如果存在)
+	if (chartInstance) {
+		chartInstance.dispose();
+	}
+
+	// 创建新实例
+	chartInstance = echarts.init(chartRef.value);
+	console.log(chartInstance);
+
+	// 处理数据
+	const scatterData = chartData.value.map((item) => ({
+		value: [Math.random() * 100, Math.random() * 100, 30 + Math.random() * 20],
+		itemStyle: { color: item.color },
+		name: `${item.name}\n${item.value}`,
+	}));
+
+	// 配置项
+	const option = {
+		tooltip: {
+			trigger: 'item',
+			formatter: '{b}',
+		},
+		grid: {
+            top: '40%',
+		},
+		xAxis: { show: false },
+		yAxis: { show: false },
+		series: [
+			{
+				type: 'scatter',
+				data: scatterData,
+				symbolSize: (value: any) => {
+					return Math.sqrt(value[2]) * 10; // 根据值计算气泡大小
+				},
+				label: {
+					show: true,
+					position: 'inside',
+					color: '#fff',
+					fontSize: 14,
+					formatter: '{b}',
+				},
+				emphasis: {
+					label: { show: true },
+				},
+			},
+		],
+	};
+
+	// 设置配置项
+	chartInstance.setOption(option);
+
+	// 添加响应式
+	window.addEventListener('resize', handleResize);
+};
+
+// 响应窗口大小变化
+const handleResize = () => {
+	if (chartInstance) {
+		chartInstance.resize();
+	}
+};
+
+// 生命周期钩子
+onMounted(() => {
+	initChart();
+});
+
+onUnmounted(() => {
+	if (chartInstance) {
+		window.removeEventListener('resize', handleResize);
+		chartInstance.dispose();
+		chartInstance = null;
+	}
+});
+</script>
+
+<style scoped lang="scss">
+.bubble-chart-container {
+	width: 465px;
+	height: 330px;
+	min-height: 330px;
+}
+
+.chart {
+	width: 100%;
+	height: 100%;
+}
+</style>    

+ 3 - 2
src/views/home/index.vue

@@ -25,18 +25,19 @@
     </el-col>
     <el-col :xs="24" :sm="24" :md="14" :lg="8" :xl="8">
       <custom-panel :title="'关键词频率'">
-        <visitor-overview />
+        <keyword-frequency />
       </custom-panel>
     </el-col>
   </el-row>
 </template>
 
 <script setup lang="ts" name="home">
-import { defineAsyncComponent } from 'vue';
+import { defineAsyncComponent,ref } from 'vue';
 
 const customPanel = defineAsyncComponent(() => import('./custom-panel.vue'));
 const userInfo = defineAsyncComponent(() => import('./user-info.vue'));
 const visitorTrend = defineAsyncComponent(() => import('./visitor-trend.vue'));
 const trafficSources = defineAsyncComponent(() => import('./traffic-sources.vue'));
 const VisitorOverview = defineAsyncComponent(() => import('./visitor-overview.vue'));
+const keywordFrequency = defineAsyncComponent(() => import('./echarts/keyword-frequency.vue'));
 </script>

+ 185 - 0
src/views/marketing/config/form.vue

@@ -0,0 +1,185 @@
+<template>
+  <el-dialog :title="state.ruleForm.menuId ? $t('common.editBtn') : $t('common.addBtn')" width="600" v-model="visible"
+             :close-on-click-modal="false" :destroy-on-close="true" draggable>
+    <el-form ref="menuDialogFormRef" :model="state.ruleForm" :rules="dataRules" label-width="90px" v-loading="loading">
+      <el-form-item :label="$t('marketingConfig.name')" prop="domain">
+        <el-input v-model="state.ruleForm.domain" clearable :placeholder="$t('marketingConfig.inputNameTip')"></el-input>
+      </el-form-item>
+      <el-form-item :label="$t('marketingConfig.config')" prop="configs">
+        <div v-for="(item, index) in state.ruleForm.configs" :key="item.index" class="config-container">
+          <span>IP</span> <el-input style="width: 140px;" v-model="item.ip" clearable :placeholder="$t('marketingConfig.inputIPTip')"></el-input>
+          <span>{{ $t('marketingConfig.app') }}</span>
+          <el-select style="width: 140px;" v-model="item.appId" clearable :placeholder="$t('marketingConfig.inputAppSel')">
+            <el-option v-for="item in state.appList" :key="item.appId" :label="item.appName" :value="item.appId"></el-option>
+          </el-select>
+          <div class="config-actions">
+            <svg 
+              v-if="index === state.ruleForm.configs.length - 1" 
+              @click="state.ruleForm.configs.push({ ip: '', appId: '', appName: '' })"
+              style="cursor: pointer;" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M19.5 3H4.5C3.67157 3 3 3.67157 3 4.5V19.5C3 20.3284 3.67157 21 4.5 21H19.5C20.3284 21 21 20.3284 21 19.5V4.5C21 3.67157 20.3284 3 19.5 3Z" stroke="#646464" stroke-linejoin="round"/>
+              <path d="M12 8V16" stroke="#646464" stroke-linecap="round" stroke-linejoin="round"/>
+              <path d="M8 12H16" stroke="#646464" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+            <svg 
+              v-if="state.ruleForm.configs.length > 1" style="cursor: pointer;"
+              @click="state.ruleForm.configs.splice(index, 1)"
+              width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M19.5 3H4.5C3.67157 3 3 3.67157 3 4.5V19.5C3 20.3284 3.67157 21 4.5 21H19.5C20.3284 21 21 20.3284 21 19.5V4.5C21 3.67157 20.3284 3 19.5 3Z" stroke="#646464" stroke-linejoin="round"/>
+              <path d="M8 12H16" stroke="#646464" stroke-linecap="round" stroke-linejoin="round"/>
+            </svg>
+          </div>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+			<span class="dialog-footer">
+				<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
+				<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
+			</span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts" name="systemMenuDialog">
+import {useI18n} from 'vue-i18n';
+import {info, getAppList, save} from '/@/api/marketing/config';
+import {useMessage} from '/@/hooks/message';
+import {rule} from '/@/utils/validate';
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+const {t} = useI18n();
+// 引入组件
+const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
+
+// 定义变量内容
+const visible = ref(false);
+const loading = ref(false);
+const menuDialogFormRef = ref();
+// 定义需要的数据
+const state = reactive({
+  ruleForm: {
+    id: null,
+    domain: '',
+    configs: [
+      {
+        id: '',
+        ip: '',
+        appId: '',
+        appUrl: '',
+        appName: ''
+      }
+      // {
+      //   id: '1',
+      //   ip: '美国',
+      //   appId: '1',
+      //   appUrl: 'www.baidu.com',
+      //   appName: 'adsa'
+      // },
+    ],
+  },
+  appList: [] as any[], // 应用下拉框列表
+});
+
+// 表单校验规则
+const dataRules = reactive({
+  domain: [
+    {required: true, message: '域名不能为空', trigger: 'blur'},
+    { validator: rule.url, trigger: 'blur' }
+  ],
+});
+
+// 打开弹窗
+const openDialog = (type: string, row?: any) => {
+  visible.value = true;
+
+  nextTick(() => {
+    menuDialogFormRef.value?.resetFields();
+  });
+
+  if (row?.id && type === 'edit') {
+    getConfigDetail(row.id);
+  } else {
+    state.ruleForm = {
+      id: null,
+      domain: '',
+      configs: [
+        {
+          id: '',
+          ip: '',
+          appId: '',
+          appUrl: '',
+          appName: ''
+        }
+      ],
+    };
+  }
+  // 获取应用列表
+  getAllAppData();
+};
+
+// 获取当条配置信息
+const getConfigDetail = (id: string) => {
+  info(id).then((res) => {
+    Object.assign(state.ruleForm, res.data);
+  });
+};
+
+// 从后端获取菜单信息(含层级)
+const getAllAppData = () => {
+  state.appList = [];
+  getAppList().then((res) => {
+    console.log(res);
+    state.appList = res.data;
+  });
+};
+
+// 保存数据
+const onSubmit = async () => {
+  const valid = await menuDialogFormRef.value.validate().catch(() => {
+  });
+  if (!valid) return false;
+
+  try {
+    loading.value = true;
+    if (!state.ruleForm.id) {
+      delete state.ruleForm.id;
+    }
+    state.ruleForm.configs.forEach((item: any) => {
+      item.appName = state.appList.find(app => app.appId === item.appId)?.appName || '';
+      item.appUrl = state.appList.find(app => app.appId === item.appId)?.appUrl || '';
+      !item.id && delete item.id; // 删除未定义的id
+    });
+    await save(state.ruleForm);
+    useMessage().success(t(state.ruleForm.id ? 'common.editSuccessText' : 'common.addSuccessText'));
+    visible.value = false;
+    emit('refresh');
+  } catch (err: any) {
+    useMessage().error(err.msg);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 暴露变量 只有暴漏出来的变量 父组件才能使用
+defineExpose({
+  openDialog,
+});
+</script>
+<style scoped>
+.config-container {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+  font-size: 14px;
+  font-weight: 400;
+  width: 100%;
+  justify-content: space-between;
+}
+.config-actions {
+  width: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>

+ 36 - 0
src/views/marketing/config/i18n/en.ts

@@ -0,0 +1,36 @@
+export default {
+	marketingConfig: {
+		index: '#',
+		name: 'domain name',
+		config: 'config scheme',
+		enName: 'en menu name',
+		sortOrder: 'sortOrder',
+		path: 'path',
+		menuType: 'menuType',
+		keepAlive: 'keepAlive',
+		permission: 'permission',
+		inputNameTip: 'input domain name',
+		inputEnNameTip: 'input en name',
+		parentId: 'parent menu',
+		embedded: 'embedded',
+		visible: 'visible',
+		icon: 'icon',
+		inputMenuIdTip: 'input menuId',
+		inputPermissionTip: 'input permission',
+		inputPathTip: 'input path',
+		inputParentIdTip: 'input parentId',
+		inputIconTip: 'input icon',
+		inputVisibleTip: 'input visible',
+		inputSortOrderTip: 'input sortOrder',
+		inputKeepAliveTip: 'input keepAlive',
+		inputMenuTypeTip: 'input menuType',
+		inputCreateByTip: 'input createBy',
+		inputCreateTimeTip: 'input createTime',
+		inputUpdateByTip: 'input updateBy',
+		inputUpdateTimeTip: 'input updateTime',
+		inputDelFlagTip: 'input delFlag',
+		inputTenantIdTip: 'input tenantId',
+		inputEmbeddedTip: 'input embedded',
+		deleteDisabledTip: 'menu inclusion subordinates cannot be deleted',
+	},
+};

+ 11 - 0
src/views/marketing/config/i18n/zh-cn.ts

@@ -0,0 +1,11 @@
+export default {
+	marketingConfig: {
+		index: '#',
+		name: '域名',
+		config: '配置方案',
+		inputNameTip: '请输入域名名称',
+		inputIPTip: '请输入IP名称',
+		app: '应用',
+		inputAppSel: '请选择应用',
+	},
+};

+ 147 - 0
src/views/marketing/config/index.vue

@@ -0,0 +1,147 @@
+<template>
+	<div class="layout-padding">
+		<div class="layout-padding-auto layout-padding-view">
+			<el-row shadow="hover" v-show="showSearch" class="ml10">
+				<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
+					<el-form-item :label="$t('marketingConfig.name')" prop="domain">
+						<el-input :placeholder="$t('marketingConfig.inputNameTip')" clearable style="max-width: 180px" v-model="state.queryForm.domain" />
+					</el-form-item>
+          <el-form-item>
+            <el-button @click="query" class="ml10" 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-row>
+				<div class="mb8" style="width: 100%">
+					<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary" v-auth="'sys_menu_add'">
+						{{ $t('common.addBtn') }}
+					</el-button>
+					<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" v-auth="'sys_user_del'" @click="handleDelete(selectObjs)">
+						{{ $t('common.delBtn') }}
+					</el-button>
+					<right-toolbar
+						v-model:showSearch="showSearch"
+						class="ml10"
+						style="float: right; margin-right: 20px"
+						@queryTable="getDataList"
+					></right-toolbar>
+				</div>
+			</el-row>
+			<el-table
+				ref="tableRef"
+				:data="state.dataList"
+				row-key="path"
+				style="width: 100%"
+				v-loading="state.loading"
+				border
+				:cell-style="tableStyle.cellStyle"
+				:header-cell-style="tableStyle?.headerCellStyle"
+				@selection-change="handleSelectionChange"
+			>
+				<el-table-column type="selection" :selectable="handleSelectable" width="50" align="center" />
+				<el-table-column :label="$t('marketingConfig.name')" fixed prop="domain" width="240" show-overflow-tooltip></el-table-column>
+				<el-table-column :label="$t('marketingConfig.config')" prop="configs">
+					<template #default="scope">
+						<div style="display: inline-block; margin-right: 10px;width: 22%; text-align: left; float: left;" v-for="item in scope.row.configs" :key="item.ip">
+							<!-- <el-tag  class="mr10">{{ item.ip }}</el-tag>: -->
+							<span style="display: inline-block;">{{ item.ip }}</span>:<el-link style="color: var(--el-color-primary);" :href="item.download">{{ item.appName }}</el-link>
+						</div>
+					</template>
+				</el-table-column>
+				<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250">
+					<template #default="scope">
+						<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary" v-auth="'sys_menu_edit'"
+							>{{ $t('common.editBtn') }}
+						</el-button>
+						<span style="margin-left: 12px">
+							<el-button
+								icon="delete"
+								@click="handleDelete([scope.row.id])"
+								text
+								type="primary"
+								v-auth="'sys_menu_del'"
+							>
+								{{ $t('common.delBtn') }}
+							</el-button>
+						</span>
+					</template>
+				</el-table-column>
+			</el-table>
+		</div>
+		<MenuDialog @refresh="getDataList()" ref="menuDialogRef" />
+	</div>
+</template>
+
+<script lang="ts" name="marketingConfig" setup>
+import { delObj, pageList } from '/@/api/marketing/config';
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { useMessage, useMessageBox } from '/@/hooks/message';
+import { useI18n } from 'vue-i18n';
+// 引入组件
+const MenuDialog = defineAsyncComponent(() => import('./form.vue'));
+const { t } = useI18n();
+// 定义变量内容
+const tableRef = ref();
+const menuDialogRef = ref();
+const queryRef = ref();
+const showSearch = ref(true);
+// 多选rows
+const selectObjs = ref([]) as any;
+// 是否可以多选
+const multiple = ref(true);
+const state: BasicTableProps = reactive<BasicTableProps>({
+	pageList: pageList, // H
+	queryForm: {
+		domain: '',
+	}
+});
+
+const { getDataList, tableStyle } = useTable(state);
+
+// 打开新增菜单弹窗
+const onOpenAddMenu = (type?: string, row?: any) => {
+	menuDialogRef.value.openDialog(type, row);
+};
+// 打开编辑菜单弹窗
+const onOpenEditMenu = (type: string, row: any) => {
+	menuDialogRef.value.openDialog(type, row);
+};
+
+// 搜索事件
+const query = () => {
+  state.dataList = [];
+  getDataList();
+};
+
+// 清空搜索条件
+const resetQuery = () => {
+  queryRef.value.resetFields();
+  state.dataList = [];
+  getDataList();
+};
+// 多选事件
+const handleSelectionChange = (objs: { id: string }[]) => {
+	selectObjs.value = objs.map(({ id }) => id);
+	multiple.value = !objs.length;
+};
+
+// 删除操作
+const handleDelete = async (ids: string[]) => {
+	try {
+		await useMessageBox().confirm(t('common.delConfirmText'));
+	} catch {
+		return;
+	}
+
+	try {
+		await delObj(ids);
+		getDataList();
+		useMessage().success(t('common.delSuccessText'));
+	} catch (err: any) {
+		useMessage().error(err.msg);
+	}
+};
+</script>

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

@@ -0,0 +1,14 @@
+export default {
+	systoken: {
+		index: '#',
+		userId: 'userId',
+		username: 'username',
+		clientId: 'clientId',
+		accessToken: 'accessToken',
+		expiresAt: 'expiresAt',
+		inputUsernameTip: 'input Username',
+		offlineBtn: 'offline',
+		offlineConfirmText: 'offline confirm',
+		offlineSuccessText: 'offline success',
+	},
+};

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

@@ -0,0 +1,13 @@
+export default {
+	systoken: {
+		ip: 'IP',
+		domain: '域名',
+		content: '访问量',
+		active: '日活',
+		source: '来源',
+		inputIpTip: '请输入IP地址',
+		inputDomainTip: '请输入域名',
+		queryBtn: '查询',
+		resetBtn: '重置',
+	},
+};

+ 68 - 0
src/views/marketing/statistics/index.vue

@@ -0,0 +1,68 @@
+<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>
+						<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="content" show-overflow-tooltip></el-table-column>
+				<el-table-column :label="$t('systoken.active')" prop="active" 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>
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { pageList } from '/@/api/marketing/statistics';
+
+import { useI18n } from 'vue-i18n';
+import { useMessage, useMessageBox } from '/@/hooks/message';
+import { Session } from '/@/utils/storage';
+
+const { t } = useI18n();
+// 定义变量内容
+const queryRef = ref();
+const showSearch = ref(true);
+
+//  table hook
+const state: BasicTableProps = reactive<BasicTableProps>({
+	queryForm: {
+		ip: '',
+        domain:''
+	},
+	pageList: pageList,
+});
+const { getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
+
+// 清空搜索条件
+const resetQuery = () => {
+	queryRef.value?.resetFields();
+	getDataList();
+};
+
+</script>

+ 1 - 1
tsconfig.json

@@ -69,6 +69,6 @@
 		"skipLibCheck": true /* Skip type checking of declaration files. */,
 		"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
 	},
-	"include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.tsx", "src/**/*.d.ts", "auto-imports.d.ts"], // **Represents any directory, and * represents any file. Indicates that all files in the src directory will be compiled
+	"include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.tsx", "src/**/*.d.ts", "auto-imports.d.ts", "src/api/marketing/statistics.ts"], // **Represents any directory, and * represents any file. Indicates that all files in the src directory will be compiled
 	"exclude": ["node_modules", "dist"] // Indicates the file directory that does not need to be compiled
 }