Przeglądaj źródła

feat,数据接收页面

luoy 2 dni temu
rodzic
commit
9360308dc3

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

@@ -0,0 +1,19 @@
+import request from '/@/utils/request';
+//获取数据接收列表
+export const pageList = (data) => {
+	return request({
+		url: 'marketing/tcp/device/page',
+		method: 'post',
+    data: data,
+	});
+};
+
+//根据id查询数据列表分页
+export const getDataPage = (data) => {
+	return request({
+		url: 'marketing/tcp/msg/page',
+		method: 'post',
+    data: data,
+	});
+};
+

+ 259 - 0
src/views/marketing/data/JsonDataDialog.vue

@@ -0,0 +1,259 @@
+<template>
+  <!-- JSON数据展示弹窗 -->
+  <el-dialog
+    v-model="dialogVisible"
+    title="设备信息详情"
+    width="60%"
+    class="json-dialog"
+    :before-close="handleClose"
+  >
+    <!-- 操作栏 -->
+    <div class="json-toolbar">
+      <el-button 
+        type="primary" 
+        link 
+        @click="copyJson"
+        :loading="copying"
+        :icon="CopyDocument"
+      >
+        {{ copying ? '复制中...' : '复制JSON' }}
+      </el-button>
+      <!-- <el-button 
+        type="primary" 
+        link 
+        @click="formatJson"
+        :icon="Refresh"
+      >
+        格式化
+      </el-button>
+      <el-button 
+        type="primary" 
+        link 
+        @click="compressJson"
+        :icon="Minus"
+      >
+        压缩
+      </el-button> -->
+    </div>
+
+    <!-- JSON内容展示区 -->
+    <div class="json-content-wrapper">
+      <pre 
+        ref="jsonContentRef"
+        class="json-content"
+        :class="{ 'compressed': isCompressed }"
+      ><code>{{ displayJson }}</code></pre>
+    </div>
+
+    <!-- 底部信息 -->
+    <template #footer>
+      <div class="dialog-footer">
+        <span class="json-info">
+          数据大小: {{ jsonSize }} | 
+          字段数: {{ fieldCount }}
+        </span>
+        <el-button @click="handleClose">关闭</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+import { CopyDocument, Refresh, Minus } from '@element-plus/icons-vue'
+import { useMessage } from '/@/hooks/message'
+
+// 弹窗可见性
+const dialogVisible = ref(false)
+
+// JSON数据
+const jsonData = ref<any>(null)
+
+// 显示的JSON字符串
+const displayJson = ref('')
+
+// 是否压缩显示
+const isCompressed = ref(false)
+
+// 复制状态
+const copying = ref(false)
+
+// JSON内容引用
+const jsonContentRef = ref<HTMLElement | null>(null)
+
+// 计算JSON大小
+const jsonSize = computed(() => {
+  if (!displayJson.value) return '0 B'
+  const size = new Blob([displayJson.value]).size
+  if (size < 1024) return `${size} B`
+  if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
+  return `${(size / (1024 * 1024)).toFixed(2)} MB`
+})
+
+// 计算字段数量
+const fieldCount = computed(() => {
+  if (!jsonData.value) return 0
+  try {
+    const countFields = (obj: any): number => {
+      if (obj === null || typeof obj !== 'object') return 1
+      if (Array.isArray(obj)) {
+        return obj.reduce((sum, item) => sum + countFields(item), 0)
+      }
+      return Object.keys(obj).length + 
+        Object.values(obj).reduce((sum, value) => sum + countFields(value), 0)
+    }
+    return countFields(jsonData.value)
+  } catch {
+    return 0
+  }
+})
+
+// 打开弹窗方法
+const openDialog = (data: any) => {
+  dialogVisible.value = true
+  jsonData.value = data
+  
+  try {
+    // 初始格式化显示
+    // displayJson.value = JSON.stringify(data, null, 2)
+    displayJson.value = data
+    isCompressed.value = false
+  } catch (error) {
+    displayJson.value = '无效的JSON数据'
+    useMessage().error('无法解析JSON数据')
+  }
+}
+
+// 关闭弹窗
+const handleClose = () => {
+  dialogVisible.value = false
+  jsonData.value = null
+  displayJson.value = ''
+}
+
+// 复制JSON到剪贴板
+const copyJson = async () => {
+  if (!displayJson.value) return
+  
+  copying.value = true
+  try {
+    await navigator.clipboard.writeText(displayJson.value)
+    useMessage().success('JSON数据已复制到剪贴板')
+  } catch (error) {
+    // 降级方案
+    const textArea = document.createElement('textarea')
+    textArea.value = displayJson.value
+    document.body.appendChild(textArea)
+    textArea.select()
+    document.execCommand('copy')
+    document.body.removeChild(textArea)
+    useMessage().success('JSON数据已复制到剪贴板')
+  } finally {
+    copying.value = false
+  }
+}
+
+// 格式化JSON
+const formatJson = () => {
+  if (!jsonData.value) return
+  try {
+    displayJson.value = JSON.stringify(jsonData.value, null, 2)
+    isCompressed.value = false
+    useMessage().success('JSON已格式化')
+  } catch (error) {
+    useMessage().error('格式化失败')
+  }
+}
+
+// 压缩JSON
+const compressJson = () => {
+  if (!jsonData.value) return
+  try {
+    displayJson.value = JSON.stringify(jsonData.value)
+    isCompressed.value = true
+    useMessage().success('JSON已压缩')
+  } catch (error) {
+    useMessage().error('压缩失败')
+  }
+}
+
+// 暴露方法给父组件
+defineExpose({
+  openDialog
+})
+</script>
+
+<style scoped lang="scss">
+.json-dialog {
+  :deep(.el-dialog__body) {
+    padding: 10px 20px;
+  }
+}
+
+.json-toolbar {
+  padding: 10px 0;
+  border-bottom: 1px solid #eee;
+  margin-bottom: 15px;
+  
+  .el-button {
+    margin-right: 15px;
+  }
+}
+
+.json-content-wrapper {
+  min-height: 300px;
+  max-height: 50vh;
+  overflow: auto;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  background-color: #f8f9fa;
+  
+  // 自定义滚动条
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background-color: #c1c1c1;
+    border-radius: 3px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background-color: #f1f1f1;
+  }
+}
+
+.json-content {
+  margin: 0;
+  padding: 15px;
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #333;
+  background-color: transparent;
+  white-space: pre-wrap;
+  word-break: break-all;
+  
+  &.compressed {
+    line-height: 1.2;
+  }
+  
+  code {
+    font-family: inherit;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 15px;
+  border-top: 1px solid #eee;
+}
+
+.json-info {
+  font-size: 12px;
+  color: #999;
+}
+</style>

+ 310 - 0
src/views/marketing/data/dataInfoModal.vue

@@ -0,0 +1,310 @@
+<template>
+  <!-- 富文本详情弹窗 -->
+  <el-dialog
+    v-model="dialogVisible"
+    title="数据详情"
+    width="80%"
+    class="rich-content-dialog"
+    :before-close="handleClose"
+    @opened="onOpened"
+  >
+    <!-- 弹窗内容区 -->
+    <div class="content-wrapper" ref="contentWrapper">
+      <div 
+        v-loading="loading" 
+        element-loading-text="加载中..."
+        class="content-container"
+      >
+        <!-- 富文本内容展示区 -->
+        <div 
+          v-if="content" 
+          class="rich-content-display"
+          v-html="content"
+        ></div>
+        
+        <!-- 空状态 -->
+        <el-empty v-else description="暂无内容" />
+      </div>
+    </div>
+    
+    <!-- 底部信息栏 -->
+    <template #footer>
+      <div class="dialog-footer-info">
+        <span v-if="createTime" class="create-time">
+          创建时间: {{ formatTime(createTime) }}
+        </span>
+        <span v-if="updateTime" class="update-time">
+          更新时间: {{ formatTime(updateTime) }}
+        </span>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue'
+
+// 弹窗可见性
+const dialogVisible = ref(false)
+
+// 加载状态
+const loading = ref(false)
+
+// 富文本内容
+const content = ref('')
+
+// 创建时间
+const createTime = ref<string | Date | null>(null)
+
+// 更新时间
+const updateTime = ref<string | Date | null>(null)
+
+// 内容容器引用
+const contentWrapper = ref<HTMLElement | null>(null)
+
+// 打开弹窗方法
+const openDialog = (
+  richContent: string, 
+  options?: {
+    createTime?: string | Date,
+    updateTime?: string | Date
+  }
+) => {
+  dialogVisible.value = true
+  loading.value = true
+  
+  // 设置内容和时间信息
+  content.value = richContent
+  createTime.value = options?.createTime || null
+  updateTime.value = options?.updateTime || null
+  
+  // 模拟加载延迟
+  setTimeout(() => {
+    loading.value = false
+  }, 300)
+}
+
+// 弹窗打开后的回调
+const onOpened = () => {
+  // 可以在这里处理弹窗打开后的逻辑,如聚焦等
+  if (contentWrapper.value) {
+    // 滚动到顶部
+    contentWrapper.value.scrollTop = 0
+  }
+}
+
+// 格式化时间
+const formatTime = (time: string | Date | null) => {
+  if (!time) return '--'
+  const date = new Date(time)
+  return date.toLocaleString('zh-CN')
+}
+
+// 关闭前的处理
+const handleClose = () => {
+  dialogVisible.value = false
+}
+
+// 暴露方法给父组件
+defineExpose({
+  openDialog
+})
+</script>
+
+<style scoped lang="scss">
+.rich-content-dialog {
+  height: 80vh;
+  
+  :deep(.el-dialog__body) {
+    padding: 0;
+    height: calc(80vh - 120px);
+  }
+  
+  :deep(.el-dialog__header) {
+    padding: 20px 20px 10px;
+    border-bottom: 1px solid #eee;
+  }
+}
+
+.content-wrapper {
+  height: 100%;
+  overflow: hidden;
+  position: relative;
+}
+
+.content-container {
+  height: 100%;
+  overflow-y: auto;
+  padding: 20px;
+  
+  // 自定义滚动条样式
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background-color: #c1c1c1;
+    border-radius: 3px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background-color: #f1f1f1;
+  }
+}
+
+.rich-content-display {
+  min-height: 100%;
+  
+  // 通用富文本样式
+  :deep(img) {
+    max-width: 100%;
+    height: auto;
+    border-radius: 4px;
+  }
+  
+  :deep(h1),
+  :deep(h2),
+  :deep(h3),
+  :deep(h4),
+  :deep(h5),
+  :deep(h6) {
+    margin: 20px 0 10px 0;
+    color: #333;
+    font-weight: 600;
+  }
+  
+  :deep(h1) {
+    font-size: 24px;
+    border-bottom: 1px solid #eee;
+    padding-bottom: 10px;
+  }
+  
+  :deep(h2) {
+    font-size: 22px;
+  }
+  
+  :deep(h3) {
+    font-size: 20px;
+  }
+  
+  :deep(h4) {
+    font-size: 18px;
+  }
+  
+  :deep(p) {
+    margin: 10px 0;
+    line-height: 1.8;
+    color: #555;
+    font-size: 14px;
+  }
+  
+  :deep(ul),
+  :deep(ol) {
+    padding-left: 20px;
+    margin: 10px 0;
+  }
+  
+  :deep(li) {
+    margin: 5px 0;
+    line-height: 1.6;
+  }
+  
+  :deep(strong) {
+    font-weight: 600;
+  }
+  
+  :deep(em) {
+    font-style: italic;
+  }
+  
+  :deep(a) {
+    color: #409eff;
+    text-decoration: none;
+    
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+  
+  :deep(blockquote) {
+    margin: 15px 0;
+    padding: 10px 15px;
+    border-left: 4px solid #409eff;
+    background-color: #f5f8ff;
+    color: #666;
+  }
+  
+  :deep(code) {
+    background-color: #f5f5f5;
+    padding: 2px 5px;
+    border-radius: 3px;
+    font-family: monospace;
+    font-size: 13px;
+  }
+  
+  :deep(pre) {
+    background-color: #2d2d2d;
+    color: #f8f8f2;
+    padding: 15px;
+    border-radius: 5px;
+    overflow-x: auto;
+    margin: 15px 0;
+    font-size: 13px;
+    line-height: 1.5;
+  }
+  
+  :deep(table) {
+    width: 100%;
+    border-collapse: collapse;
+    margin: 15px 0;
+  }
+  
+  :deep(th),
+  :deep(td) {
+    border: 1px solid #ddd;
+    padding: 8px 12px;
+    text-align: left;
+  }
+  
+  :deep(th) {
+    background-color: #f8f9fa;
+    font-weight: 600;
+  }
+  
+  :deep(tr:nth-child(even)) {
+    background-color: #f9f9f9;
+  }
+}
+
+.dialog-footer-info {
+  display: flex;
+  justify-content: space-between;
+  padding: 10px 0;
+  border-top: 1px solid #eee;
+  font-size: 12px;
+  color: #999;
+  
+  .create-time,
+  .update-time {
+    display: inline-block;
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .rich-content-dialog {
+    width: 95%;
+    height: 90vh;
+    
+    :deep(.el-dialog__body) {
+      height: calc(90vh - 120px);
+    }
+  }
+  
+  .dialog-footer-info {
+    flex-direction: column;
+    gap: 5px;
+    align-items: flex-start;
+  }
+}
+</style>

+ 263 - 0
src/views/marketing/data/dataListModal.vue

@@ -0,0 +1,263 @@
+<template>
+  <!-- 弹窗组件 -->
+  <el-dialog v-model="dialogVisible" title="数据信息(23)" width="860px" @close="onClose">
+    <!-- 弹窗头部:搜索区域 -->
+    <div class="dialog-header">
+      <el-form :inline="true" :model="searchForm" @keyup.enter="handleSearch">
+        <el-form-item label="关键词">
+          <el-input 
+            v-model="searchForm.keyWord" 
+            placeholder="请输入关键词查找" 
+          />
+        </el-form-item>
+        <el-form-item label="时间范围">
+          <el-date-picker
+            v-model="searchForm.timeRange"
+            type="datetimerange"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            start-placeholder="选择开始时间"
+            end-placeholder="选择结束时间"
+            style="width: 200px;"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="search" @click="handleSearch">
+            查询
+          </el-button>
+          <el-button @click="resetQuery" icon="Refresh">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 弹窗内容:富文本信息列表 -->
+    <div class="dialog-content">
+      <div v-loading="loading">
+        <div 
+          v-for="(item, index) in dataList" 
+          :key="item.id" 
+          class="data-item"
+          :class="{ 'even': index % 2 === 0, 'odd': index % 2 === 1 }"
+          title="点击查看数据详情"
+          @click="handleOpenDialog(item)"
+        >
+          <div class="rich-content" v-html="item.msgData"></div>
+          <div class="item-time">{{ formatTime(item.reportTime) }}</div>
+        </div>
+        
+        <!-- 无数据提示 -->
+        <el-empty v-if="dataList.length === 0" description="暂无数据" />
+      </div>
+    </div>
+
+    <!-- 弹窗页脚:分页器 -->
+    <div class="dialog-footer">
+      <el-pagination
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+        :current-page="pagination.current"
+        :page-size="pagination.size"
+        :total="pagination.total"
+        layout="total, prev, pager, next, jumper"
+        background
+      />
+    </div>
+  </el-dialog>
+  <dataInfoModal ref="richContentDialogRef"></dataInfoModal>>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { getDataPage } from '/@/api/marketing/data';
+
+// 使用国际化
+const { t } = useI18n()
+
+// 详情弹窗
+const dataInfoModal = defineAsyncComponent(() => import('./dataInfoModal.vue'))
+const richContentDialogRef = ref()
+const handleOpenDialog = (item: any) => {
+  richContentDialogRef.value.openDialog(
+  item.msgData, 
+  {
+    createTime: item.reportTime
+  }
+)
+}
+
+// 弹窗可见性
+const dialogVisible = ref(false)
+
+// 加载状态
+const loading = ref(false)
+
+// 搜索表单
+const searchForm = reactive({
+  keyWord: '',
+  timeRange: null as null | [Date, Date]
+})
+
+// 清空搜索条件
+const resetQuery = () => {
+  searchForm.keyWord = ''
+  searchForm.timeRange = null
+  pagination.current = 1
+  dataList.value = []
+  fetchData();
+}
+
+const clientID = ref('')
+
+// 数据列表
+const dataList = ref<any[]>([])
+
+// 分页信息
+const pagination = reactive({
+  current: 1,
+  size: 5, // 每页5条信息
+  total: 0
+})
+
+// 打开弹窗方法
+const openDialog = (id: string) => {
+  dialogVisible.value = true
+  clientID.value = id
+  fetchData()
+}
+
+// 关闭弹窗
+const onClose = () => {
+  // 重置表单
+  searchForm.keyWord = ''
+  searchForm.timeRange = null
+  pagination.current = 1
+  dataList.value = []
+}
+
+// 格式化时间
+const formatTime = (time: string | Date) => {
+  if (!time) return '--'
+  const date = new Date(time)
+  return date.toLocaleString()
+}
+
+// 搜索处理
+const handleSearch = () => {
+  pagination.current = 1
+  fetchData()
+}
+
+// 获取数据方法(需要根据实际API调整)
+const fetchData = async () => {
+  loading.value = true
+  try {
+    // 这里需要替换为实际的API调用
+    // 示例数据结构,根据实际API调整
+    const params = {
+      keyWord: searchForm.keyWord,
+      startTime: searchForm.timeRange ? searchForm.timeRange[0] : null,
+      endTime: searchForm.timeRange ? searchForm.timeRange[1] : null,
+      current: pagination.current,
+      size: pagination.size,
+      clientID: clientID.value
+    }
+    
+    // 模拟API调用
+    const res = await getDataPage(params)
+    dataList.value = res.data.records
+    pagination.total = res.data.total
+    
+  } catch (error) {
+    console.error('获取数据失败:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 分页变化处理
+const handleCurrentChange = (val: number) => {
+  pagination.current = val
+  fetchData()
+}
+
+// 每页大小变化处理
+const handleSizeChange = (val: number) => {
+  pagination.size = val
+  pagination.current = 1
+  fetchData()
+}
+
+// 暴露方法给父组件
+defineExpose({
+  openDialog
+})
+</script>
+
+<style scoped lang="scss">
+.dialog-header {
+  margin-bottom: 20px;
+  padding-bottom: 15px;
+  border-bottom: 1px solid #eee;
+}
+
+.dialog-content {
+  min-height: 300px;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.data-item {
+  padding: 15px;
+  margin-bottom: 15px;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  
+  &.even {
+    background-color: #fafafa;
+  }
+  
+  &.odd {
+    background-color: #fff;
+  }
+  
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.rich-content {
+  margin-bottom: 10px;
+  line-height: 1.6;
+  max-height: 150px;
+  overflow-y: hidden;
+  
+  :deep(h4) {
+    margin: 0 0 10px 0;
+    color: #333;
+  }
+  
+  :deep(p) {
+    margin: 0;
+    color: #666;
+  }
+}
+
+.item-time {
+  text-align: right;
+  font-size: 12px;
+  color: #999;
+}
+
+.dialog-footer {
+  margin-top: 20px;
+  padding-top: 15px;
+  border-top: 1px solid #eee;
+  display: flex;
+  justify-content: center;
+}
+
+:deep(.el-pagination) {
+  margin: 0;
+}
+</style>

+ 218 - 0
src/views/marketing/data/index.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="layout-padding">
+    <div class="layout-padding-auto layout-padding-view">
+      <el-row shadow="hover" class="ml10">
+        <el-form :inline="true" :model="state.queryForm" @keyup.enter="query" ref="queryRef">
+          <el-form-item label="客户端ID" prop="appName">
+            <el-input placeholder="请输入客户端ID" clearable v-model="state.queryForm.clientID" />
+          </el-form-item>
+          <el-form-item label="时间" prop="appName">
+            <el-date-picker value-format="YYYY-MM-DD HH:mm:ss" style="width: 300px;" v-model="timeRange"
+              type="datetimerange" start-placeholder="开始时间" end-placeholder="截止时间" />
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="query" class="ml10" icon="search" type="primary">
+              查询
+            </el-button>
+            <el-button @click="resetQuery" icon="Refresh">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </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">
+        <el-table-column label="客户端ID" prop="clientID" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.clientID }}</template>
+        </el-table-column>
+        <el-table-column label="IP" prop="updateTime" show-overflow-tooltip>
+          <template #default="{ row }">{{ getIp(row.deviceInfo || '{}') }}</template>
+        </el-table-column>
+        <el-table-column label="设备信息" prop="deviceInfo" show-overflow-tooltip>
+          <template #header>
+            <div class="header-wrapper">
+              <span>设备信息</span>
+              <el-popover placement="top" trigger="hover" content="点击查看具体设备信息" :width="200">
+                <template #reference>
+                  <el-icon class="header-icon">
+                    <QuestionFilled />
+                  </el-icon>
+                </template>
+              </el-popover>
+            </div>
+          </template>
+          <template #default="{ row }">
+            <div @click="showJsonData(row.deviceInfo)">
+              {{ row.deviceInfo }}
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="请求头" prop="headInfo" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.headInfo }}</template>
+        </el-table-column>
+        <el-table-column label="创建时间" prop="createTime" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.createTime }}</template>
+        </el-table-column>
+        <el-table-column label="更新时间" prop="updateTime" show-overflow-tooltip>
+          <template #default="{ row }">{{ row.updateTime }}</template>
+        </el-table-column>
+        <el-table-column label="数据" prop="total">
+          <template #default="{ row }">
+            <!-- <el-tag title="点击查看">共{{ row.total }}条</el-tag> -->
+            <el-button title="点击查看" type="primary" link @click="handleDetail(row.clientID)">共{{ row.total }}条</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
+    </div>
+    <dataListModal ref="dataListRef" />
+    <JsonDataDialog ref="jsonDataDialogRef" />
+  </div>
+</template>
+
+<script lang="ts" name="marketingApps" setup>
+import { pageList } from '/@/api/marketing/data';
+import { BasicTableProps, useTable } from '/@/hooks/table';
+import { useI18n } from 'vue-i18n';
+import { ref } from 'vue'
+import { formatDate } from '/@/utils/formatTime';
+
+const dataListModal = defineAsyncComponent(() => import('./dataListModal.vue'));
+const dataListRef = ref();
+const handleDetail = (clientID: string) => {
+  dataListRef.value.openDialog(clientID);
+}
+
+// 添加JSON数据弹窗组件
+const JsonDataDialog = defineAsyncComponent(() => import('./JsonDataDialog.vue'))
+const jsonDataDialogRef = ref()
+// 添加展示JSON数据的方法
+const showJsonData = (data: any) => {
+  jsonDataDialogRef.value.openDialog(data)
+}
+
+const { t } = useI18n();
+// 定义变量内容
+const tableRef = ref();
+const EditDialogRef = ref();
+const statisticalDialogRef = ref();
+const queryRef = ref();
+const state: BasicTableProps = reactive<BasicTableProps>({
+  pageList: pageList,
+  createdIsNeed: false,
+  queryForm: {
+    clientID: '',
+    startTime: '',
+    endTime: ''
+  },
+  pagination: {
+    current: 1,
+    size: 10,
+    total: 0,
+    pageSizes: [5, 10, 20, 50, 100]
+  },
+});
+
+const getIp = (jsonString: string) => {
+  const ipv4Match = jsonString.match(/"ipv4"\s*:\s*"([^"]+)"/);
+  return ipv4Match ? ipv4Match[1] : null;
+} 
+
+const timeRange = computed({
+  get() {
+    return [state.queryForm.startTime, state.queryForm.endTime]
+  },
+  set(val) {
+    if (val?.length) {
+      // state.queryForm.startTime = formatDate(new Date(val[0]), 'YYYY-mm-dd HH:mm:ss');
+      // state.queryForm.endTime = formatDate(new Date(val[1]), 'YYYY-mm-dd HH:mm:ss');
+      state.queryForm.startTime = val[0];
+      state.queryForm.endTime = val[1];
+    }
+  }
+})
+
+const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
+
+// 搜索事件
+const query = (refresh: boolean = false) => {
+  state.dataList = [];
+  state.queryForm.domainType = state.queryForm.domainSelected == 0 ? '' : state.queryForm.domainSelected;
+  getDataList(refresh);
+};
+
+// 清空搜索条件
+const resetQuery = () => {
+  queryRef.value.resetFields();
+  state.queryForm.clientID = '';
+  state.queryForm.startTime = '';
+  state.queryForm.endTime = '';
+  state.dataList = [];
+  getDataList();
+};
+
+// 打开统计弹窗
+const onOpenStatistical = (row: any) => {
+  statisticalDialogRef.value.openDialog(row, true);
+};
+
+// 格式化数据展示
+const formatNum = (value: string | number = 0) => {
+  let num = Number(value);
+  if (num > 0 && num < 1) {
+    return (num * 100).toFixed(2) + '%';
+
+  } else if (num >= 1 && num < 10000) {
+    return num;
+  }
+  return '--'
+}
+
+const statusFormatter = (row: any, column: any, cellValue: any, index: any) => {
+  return cellValue || '--';
+}
+
+onMounted(() => {
+  query();
+})
+
+</script>
+<style scoped lang="scss">
+:deep(.el-link__inner) {
+  display: inline-block;
+  width: 100%;
+  justify-content: start;
+}
+
+:deep(.el-link.is-underline:hover:after) {
+  display: none;
+}
+
+.table-tabs {
+  margin-bottom: 10px;
+}
+
+.header-wrapper {
+  display: inline-flex;
+  align-items: center;
+}
+
+.device-info-cell {
+  cursor: pointer;
+  color: #409eff;
+  
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.header-icon {
+  margin-left: 5px;
+  color: #999;
+  font-size: 14px;
+  cursor: help;
+  
+  &:hover {
+    color: #409eff;
+  }
+}
+</style>