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

feat(新增(数据分析)用户行为、广告用户页面): 新增页面,新增自定义筛选组件

新增用户行为、广告用户界面;新增自定义筛选组件(CustromFIlter);修改Table组件,以支持自定义筛选组件;更正部分文件名和变量名;
fxs 6 hónapja
szülő
commit
fbac8e0bee

+ 2 - 0
.env

@@ -1,3 +1,5 @@
+# 开发
+VITE_API_URL_DEV = "http://192.168.1.139:8000"
 # 测试服和本地开发
 VITE_API_URL_TEST='http://server.ichunhao.cn'
 # 线上

+ 2 - 7
components.d.ts

@@ -7,6 +7,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    CustomFilter: typeof import('./src/components/form/CustomFilter.vue')['default']
     Dialog: typeof import('./src/components/common/Dialog.vue')['default']
     DropDownSelection: typeof import('./src/components/dataAnalysis/DropDownSelection.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
@@ -15,7 +16,6 @@ declare module 'vue' {
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
-    ElDrawer: typeof import('element-plus/es')['ElDrawer']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
@@ -28,7 +28,6 @@ declare module 'vue' {
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
-    ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
     ElPagination: typeof import('element-plus/es')['ElPagination']
     ElPopover: typeof import('element-plus/es')['ElPopover']
     ElSelect: typeof import('element-plus/es')['ElSelect']
@@ -41,20 +40,16 @@ declare module 'vue' {
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
-    ElUpload: typeof import('element-plus/es')['ElUpload']
     FileUpload: typeof import('./src/components/form/FileUpload.vue')['default']
     FilterPopover: typeof import('./src/components/toolsBtn/FilterPopover.vue')['default']
     Form: typeof import('./src/components/form/Form.vue')['default']
     HeaderCard: typeof import('./src/components/dataAnalysis/HeaderCard.vue')['default']
-    IconIcBaselineVisibility: typeof import('~icons/ic/baseline-visibility')['default']
-    IconIcBaselineVisibilityOff: typeof import('~icons/ic/baseline-visibility-off')['default']
     IconIconParkGameThree: typeof import('~icons/icon-park/game-three')['default']
     IconMaterialSymbolsLightLogout: typeof import('~icons/material-symbols-light/logout')['default']
-    IconMdiPassword: typeof import('~icons/mdi/password')['default']
     IconTablerPointFilled: typeof import('~icons/tabler/point-filled')['default']
     MyButton: typeof import('./src/components/form/MyButton.vue')['default']
     MyInput: typeof import('./src/components/form/MyInput.vue')['default']
-    RegreshBtn: typeof import('./src/components/toolsBtn/RegreshBtn.vue')['default']
+    RefreshBtn: typeof import('./src/components/toolsBtn/RefreshBtn.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     StatisticText: typeof import('./src/components/dataAnalysis/StatisticText.vue')['default']

+ 129 - 30
src/components/Table.vue

@@ -2,8 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 18:16:18
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-11-14
- * @FilePath: \Game-Backstage-Management-System\src\components\Table.vue
+ * @LastEditTime: 2024-11-26
+ * @FilePath: \SqueezeTheBus c:\Users\NINGMEI\Desktop\Manage\Game-Backstage-Management-System\src\components\Table.vue
  * @Description: 
  * 
 -->
@@ -19,14 +19,32 @@ import { fuzzySearch, throttleFunc } from '@/utils/common'
 import { computed, onMounted, reactive, ref, toRaw, watch } from 'vue'
 import { useTable } from '@/hooks/useTable'
 import { useRequest } from '@/hooks/useRequest'
+import { Filter } from '@element-plus/icons-vue'
+import { useCustomFilter } from '@/hooks/useCustomFilter.ts'
 
+import CustomFilter from './form/CustomFilter.vue'
 import FilterPopover from './toolsBtn/FilterPopover.vue'
-import RegreshBtn from './toolsBtn/RegreshBtn.vue'
+import RefreshBtn from './toolsBtn/RefreshBtn.vue'
 import axiosInstance from '@/utils/axios/axiosInstance'
 import TableFieldText from './table/TableFieldText.vue'
 
+type CustomFilterRef = InstanceType<typeof CustomFilter>
+
+// 自定义筛选组件的ref
+const customFilterRef = ref<CustomFilterRef>()
+
 const { analysisResCode } = useRequest()
 
+const {
+  customFilterDialog,
+  customFilterInfo,
+  customFilterList,
+  activeCustomFilterKey,
+  initCustomFilterInfo,
+  openCustomFilter,
+
+  confirmCustomFilter
+} = useCustomFilter(customFilterRef)
 // 节流的延迟时间
 const throttleTime = 500
 
@@ -119,9 +137,15 @@ const selectFieldsList = computed(() => {
 
 // 所有类型为date的表单控件信息
 const dateFieldsList = computed(() => {
+  if (!props.queryInfo) return []
   return props.queryInfo?.filter((item) => item.type === FilterType.DATE)
 })
 
+// 所有自定义筛选条件的表单控件信息
+const customFieldsList = computed(() => {
+  return props.queryInfo?.filter((item) => item.type === FilterType.CUSTOM)
+})
+
 // 计算行号
 const computedRowIndex = (index: number) => {
   return (paginationConfig.currentPage - 1) * paginationConfig.limit + index + 1
@@ -147,6 +171,7 @@ const handleSizeChange = (val: number) => {
  * @description 加载表格数据
  */
 const loadTableData = async (): Promise<boolean> => {
+  console.trace()
   return new Promise(async (resolve, reject) => {
     loading.value = true
     if (props.dataList) {
@@ -166,6 +191,7 @@ const loadTableData = async (): Promise<boolean> => {
               (paginationConfig.currentPage - 1) * paginationConfig.limit
             reqconfig.otherOptions.limit = paginationConfig.limit
           }
+          // console.log(reqconfig.otherOptions)
           await getTableData(reqconfig.url, reqconfig.otherOptions, props.openPageQuery)
           backupTableData.splice(0, backupTableData.length, ...tableData)
           resolve(true)
@@ -227,18 +253,36 @@ const getData = () => {
 const throttleGetData = throttleFunc(getData, 1000)
 
 /**
- * @description: 清空表格数据
- * @return {*}
+ * 清空表格数据
  */
 const resetTableData = () => {
   tableData.splice(0, tableData.length)
 }
 
 /**
+ * 删除没有用的自定义筛选字段,如果不删后端会查询不到(可以不用)
+ *
+ */
+// const delUnusedCustomFilter = () => {
+//   let customKeys = customFieldsList.value?.map((item) => {
+//     return item.name
+//   })
+//   for (let [k, v] of Object.entries(queryFormData)) {
+//     if (customKeys && customKeys.includes(k) && typeof v !== 'object') {
+//       console.log(k)
+//     }
+//   }
+// }
+
+/**
  * @description: 按条件查询,如果开启了分页查询,那么会直接重新查询数据,否则,会根据现有数据进行查询
  */
 const queryTableData = () => {
   if (props.openRemoteinquiry && props.requestConfig) {
+    // delUnusedCustomFilter()
+    // console.log('--------')
+    // console.log(props.requestConfig.otherOptions, queryFormData)
+    // console.log('-----------')
     reqconfig.otherOptions = { ...props.requestConfig.otherOptions, ...queryFormData }
     getData()
   } else {
@@ -268,6 +312,8 @@ const throttleQueryTableData = throttleFunc(queryTableData, throttleTime)
  * @param needQuery 是否需要重新查询,默认为true,如果为false,则不重新查询
  */
 const resetQueryForm = (needQuery: boolean = true) => {
+  // 重置自定义筛选字段的值
+  initCustomFilterInfo(customFieldsList)
   // 使用函数返回保存的备份信息,这样可以正确的给queryformdata赋值
   // JSON.stringify()第二个参数可以用来处理undefined的情况,第一个参数设置为_可以避免ts检查
   function resetFormData() {
@@ -275,6 +321,7 @@ const resetQueryForm = (needQuery: boolean = true) => {
       JSON.stringify(backupQueryFormData, (_, v) => (typeof v === 'undefined' ? '' : v))
     )
   }
+
   Object.assign(queryFormData, resetFormData())
   reqconfig.otherOptions = backupReqOtherOptions // 要把请求的参数也重置一次,不然切换平台等操作,会带着原来的查询参数请求
 
@@ -286,9 +333,8 @@ const throttleResetQueryForm = throttleFunc(resetQueryForm, throttleTime)
 
 /**
  * @description: 在获取完数据后,插入均值行
- * @param {*} start 插入的位置
- * @param {*} rowData 插入的数据
- * @return {*}
+ * @param  start 插入的位置
+ * @param  rowData 插入的数据
  */
 const insertRow = (start: number, rowData: any) => {
   if (props.openPageQuery) {
@@ -300,10 +346,9 @@ const insertRow = (start: number, rowData: any) => {
 
 /**
  * @description: 根据计算出来的值去返回对应的颜色深度
- * @return {*}
  */
 const getDecimalFromRange = (number: number) => {
-  if (number === null || number === undefined) return 0
+  if (!number) return 0
   if (number < 25) return 0.25
   else if (number < 50) return 0.5
   else if (number < 75) return 0.75
@@ -315,7 +360,6 @@ const getDecimalFromRange = (number: number) => {
  *              其中使用row-style无效,scope会导致无法覆盖样式
  *               同时由于我自定义了表格内容,哪里的样式会覆盖row的样式,所以只能单独对单元格设置
  * @param {*} info  每个单元格的信息
- * @return {*}
  */
 const tableCellStyle = (info: any) => {
   if (info.row.date === '均值')
@@ -330,9 +374,19 @@ const tableCellStyle = (info: any) => {
 }
 
 /**
+ *
+ *
+ *
+ * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!严重BUG!!!!!!!!!!!!!!!!
+ *
+ *
+ *
+ *
+ *
+ *
  * @description: 监听litmit,currentpage的变化,改变后去重新请求数据
  * 如果是limit的变化,则需要把当前页置为1
- * @return {*}
+
  */
 watch(
   () => [paginationConfig.limit, paginationConfig.currentPage],
@@ -342,7 +396,7 @@ watch(
     if (newLimit != oldLimit) {
       // 需要给分页按钮加上:current-page.sync="current_page" 配置,不然不生效
       // 当改变每页大小时把之前的缓存全部清除,重新开始
-      paginationConfig.currentPage = 1
+      paginationConfig.currentPage = 1 // 不应该在这儿改
     }
 
     // 开启分页查询的情况下,改变limit或者没有这页的缓存的情况下,重新请求
@@ -410,10 +464,9 @@ const watchDateChange = watch(
 
 /**
  * @description: 创建row-key优化表格性能
- * @return {*}
  */
 const createRowKey = () => {
-  return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
 }
 
 // 没有pf取消掉
@@ -431,7 +484,6 @@ if (!props.dataList) {
 
 /**
  * @description: 拷贝一份配置文件
- * @return {*}
  */
 const initpageConfig = () => {
   Object.assign(paginationConfig, props.paginationConfig)
@@ -439,7 +491,6 @@ const initpageConfig = () => {
 
 /**
  * @description: 初始化请求配置,用于把拷贝一份新的数据
- * @return {*}
  */
 const initReqConfig = () => {
   Object.assign(reqconfig, props.requestConfig)
@@ -447,7 +498,6 @@ const initReqConfig = () => {
 
 /**
  * @description: 初始化查询框的数据
- * @return {*}
  */
 const initFormData = () => {
   props.queryInfo?.map((item: any) => {
@@ -461,7 +511,6 @@ const initFormData = () => {
 /**
  * @description: 表格排序
  * @param {*} data 获取到的数据
- * @return {*}
  */
 const tableSortChange = (data: { column: any; prop: string; order: any }) => {
   resetQueryForm(false)
@@ -494,7 +543,6 @@ const deleteRow = (url: string, fieldsInfo: any) => {
 
 /**
  * @description: 下载表格数据
- * @return {*}
  */
 const downLoadTable = () => {
   emits('downLoad', JSON.parse(JSON.stringify(tableData)))
@@ -502,12 +550,16 @@ const downLoadTable = () => {
 
 /**
  * @description: 外部获取数据
- * @return {*}
  */
 const outGetTableData = () => {
   return toRaw(tableData).flat()
 }
 
+/**
+ * 格式化输入框的值,只允许输入数字
+ *
+ * @param key 输入框的key
+ */
 const numberInput = (key: string) => {
   let val = queryFormData[key]
   val = parseInt(val)
@@ -531,6 +583,7 @@ onMounted(() => {
   initpageConfig()
   initReqConfig()
   initFormData()
+  initCustomFilterInfo(customFieldsList)
   backupReqOtherOptions = reqconfig.otherOptions // 备份一份请求参数
   // registerWatchProps()
   if (props.loadingState !== undefined) {
@@ -609,10 +662,18 @@ onMounted(() => {
             <el-date-picker
               v-model="queryFormData[item.name]"
               type="date"
+              :value-format="item.otherOption.valueFormat"
               :placeholder="item.placeholder"
               clearable
             />
           </el-form-item>
+
+          <!-- 所有自定义筛选条件 -->
+          <el-form-item :label="item.label" v-for="item in customFieldsList" class="filterItem">
+            <el-button plain size="default" :icon="Filter" @click="openCustomFilter(item)"
+              >筛选
+            </el-button>
+          </el-form-item>
         </el-form>
         <!-- 分割线 -->
         <!-- <el-divider class="queryPartition" content-position="center" direction="vertical" /> -->
@@ -620,13 +681,38 @@ onMounted(() => {
           <el-divider class="queryPartition" content-position="center" direction="vertical" />
           <div class="queryBtnBox">
             <el-button class="queryBtn" color="#165dff" @click="throttleQueryTableData">
-              <el-icon><Search /></el-icon>查询
+              <el-icon>
+                <Search />
+              </el-icon>
+              查询
             </el-button>
             <el-button class="refreshBtn" color="#f2f3f5" @click="throttleResetQueryForm()">
-              <el-icon><RefreshRight /></el-icon>重置
+              <el-icon>
+                <RefreshRight />
+              </el-icon>
+              重置
             </el-button>
           </div>
         </div>
+        <el-dialog v-model="customFilterDialog" title="自定义筛选条件" width="800">
+          <CustomFilter
+            :filter-list="customFilterList[activeCustomFilterKey]"
+            :filter="customFilterInfo[activeCustomFilterKey]"
+            ref="customFilterRef"
+          ></CustomFilter>
+          <template #footer>
+            <div class="dialog-footer">
+              <el-button
+                type="primary"
+                @click="confirmCustomFilter(queryFormData)"
+                style="margin-right: 20px"
+              >
+                确定
+              </el-button>
+              <el-button @click="customFilterDialog = false">取消</el-button>
+            </div>
+          </template>
+        </el-dialog>
       </div>
     </div>
     <!-- 分割线 -->
@@ -635,7 +721,10 @@ onMounted(() => {
       <div class="leftTools">
         <div class="leftToolsGroup" v-if="needLeftTools" style="display: flex">
           <el-button class="leftToolBtn" color="#165dff" @click="emits('addNewItem')">
-            <el-icon><Plus /></el-icon>新增
+            <el-icon>
+              <Plus />
+            </el-icon>
+            新增
           </el-button>
           <el-button
             class="leftToolBtn"
@@ -643,7 +732,10 @@ onMounted(() => {
             @click="emits('upload', outGetTableData())"
             v-if="needUpload"
           >
-            <el-icon><Upload /></el-icon>上传
+            <el-icon>
+              <Upload />
+            </el-icon>
+            上传
           </el-button>
         </div>
       </div>
@@ -655,10 +747,13 @@ onMounted(() => {
           class="rightToolsItem"
           @click="downLoadTable"
         >
-          <el-icon><Download /></el-icon>下载
+          <el-icon>
+            <Download />
+          </el-icon>
+          下载
         </el-button>
         <!-- throttleFunc(queryTableData, 200) -->
-        <RegreshBtn @refresh-table="throttleGetData" :icon-size="toolsIconSize"></RegreshBtn>
+        <RefreshBtn @refresh-table="throttleGetData" :icon-size="toolsIconSize"></RefreshBtn>
 
         <FilterPopover
           :table-fields-info="tableFieldsInfo"
@@ -735,6 +830,10 @@ onMounted(() => {
               >
               </el-switch>
 
+              <span v-else-if="item.specialEffect?.type === FieldSpecialEffectType.CUSTOM">
+                {{ item.specialEffect.otherInfo.render(scope.row[item.name]) }}
+              </span>
+
               <el-text v-else>
                 <!-- 其他列按默认方式显示 -->
 
@@ -781,6 +880,7 @@ onMounted(() => {
     0 4px 8px 0 rgba(0, 0, 0, 0.02),
     0 1px 3px 0 rgba(0, 0, 0, 0.02);
 }
+
 .filterBox,
 .tableBox {
   width: 100%;
@@ -798,7 +898,7 @@ onMounted(() => {
   width: 98%;
   box-sizing: border-box;
 
-  margin: 0 auto;
+  margin: 10px auto;
 }
 
 .filterHeader {
@@ -854,10 +954,9 @@ onMounted(() => {
 
 .tableTools {
   width: 98%;
-  margin: 0 auto;
   display: flex;
   justify-content: space-between;
-  margin-top: 1%;
+  margin: 1% auto 0;
 }
 
 .leftTools,

+ 231 - 0
src/components/form/CustomFilter.vue

@@ -0,0 +1,231 @@
+<script setup lang="ts">
+import { type CustomFilter, CustomFilterValueType, type FilterItem } from '@/types/customFilter'
+import { Minus, Plus } from '@element-plus/icons-vue'
+import { reactive } from 'vue'
+
+const props = defineProps<{
+  filter: CustomFilter
+  filterList: Array<FilterItem>
+}>()
+
+const conditionOptions: Array<{
+  label: string
+  value: any
+}> = [
+  {
+    label: '大于',
+    value: 'gt'
+  },
+  {
+    label: '大于或等于',
+    value: 'gte'
+  },
+  {
+    label: '小于',
+    value: 'lt'
+  },
+  {
+    label: '小于或等于',
+    value: 'lte'
+  },
+  {
+    label: '不等于',
+    value: 'ne'
+  }
+]
+
+const filterList = reactive<
+  Array<{
+    filterCondition: string
+    value: any
+  }>
+>([])
+
+const initFilterList = () => {
+  console.log(props.filterList)
+  filterList.splice(0, filterList.length, ...props.filterList)
+}
+
+const verifySub = () => {
+  if (props.filter.valueType === CustomFilterValueType.INPUT && props.filter.valueValid) {
+    const func = props.filter.valueValid
+    // 校验不通过或者为空,都视为无效数据
+    const result = filterList.find((item) => {
+      // 是否有效
+      return func(item.value)
+    })
+    if (!result) {
+      ElMessage({
+        type: 'warning',
+        message: '请输入合法信息'
+      })
+
+      return false
+    }
+  }
+  return true
+}
+
+const valueFormat = () => {
+  let backupList = JSON.parse(JSON.stringify(filterList)) as Array<{
+    filterCondition: string
+    value: any
+  }>
+  let format: any = null
+  if (props.filter.valueType === CustomFilterValueType.INPUT) {
+    format = props.filter.valueFormat
+  }
+  if (
+    props.filter.valueType === CustomFilterValueType.DATEPICKER &&
+    props.filter.valueFormat === 'X'
+  ) {
+    format = parseInt
+  }
+  if (format) {
+    backupList.map((item) => {
+      item.value = format(item.value)
+      return item
+    })
+  }
+  return backupList
+}
+
+const subCondition = () => {
+  return valueFormat()
+}
+
+const addFilter = () => {
+  filterList.push({
+    filterCondition: 'gt',
+    value: ''
+  })
+}
+
+const delCondition = (index: number) => {
+  filterList.splice(index, 1)
+}
+
+defineExpose({
+  subCondition,
+  verifySub,
+  initFilterList
+})
+</script>
+
+<template>
+  <div class="custromFilter">
+    <div class="filterHeader">
+      <div class="filterTool">
+        <el-button
+          type="primary"
+          style="width: 100px; height: 30px; font-size: 12px"
+          :icon="Plus"
+          @click="addFilter"
+          >添加条件</el-button
+        >
+      </div>
+    </div>
+    <div class="filterContiner">
+      <div class="filterItem" v-for="(item, index) in filterList">
+        <div class="filterCondition">
+          <el-select
+            style="width: 240px"
+            v-model="item.filterCondition"
+            placeholder="Select"
+            size="default"
+          >
+            <el-option
+              v-for="item in conditionOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+
+          <!-- 输入框 -->
+
+          <el-input
+            v-if="props.filter.valueType === CustomFilterValueType.INPUT"
+            v-model="item.value"
+            style="width: 240px"
+          />
+
+          <!-- 选择框 -->
+          <el-select
+            v-if="props.filter.valueType === CustomFilterValueType.SELECT"
+            v-model="item.value"
+            placeholder="Select"
+            style="width: 240px"
+          >
+            <el-option
+              v-for="item in props.filter.options"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+
+          <!-- 日期选择器 -->
+          <el-date-picker
+            v-if="props.filter.valueType === CustomFilterValueType.DATEPICKER"
+            v-model="item.value"
+            type="date"
+            clearable
+            value-format="X"
+          />
+        </div>
+        <div class="delCondition">
+          <el-button
+            type="danger"
+            v-if="index > 0"
+            @click="delCondition(index)"
+            size="small"
+            :icon="Minus"
+            circle
+          />
+        </div>
+        <!-- 这里作为以后链接条件的占位符 -->
+        <!-- <div class="linkCondition">
+          <el-radio-group v-model="radio2">
+            <el-radio value="1">Option 1</el-radio>
+            <el-radio value="2">Option 2</el-radio>
+          </el-radio-group>
+        </div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.custromFilter {
+  position: relative;
+}
+
+.filterTool {
+  width: 100%;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.filterContiner {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.filterItem {
+  width: 80%;
+  margin: 10 auto;
+  padding: 24px;
+  display: flex;
+  /* justify-content: space-around; */
+  align-items: center;
+}
+
+.filterCondition {
+  width: 90%;
+  display: flex;
+  justify-content: space-around;
+}
+</style>

+ 0 - 0
src/components/toolsBtn/RegreshBtn.vue → src/components/toolsBtn/RefreshBtn.vue


+ 149 - 0
src/hooks/useCustomFilter.ts

@@ -0,0 +1,149 @@
+import type { ComputedRef, Reactive, Ref } from 'vue'
+import type {
+  CustomFilter as CustomFilterInfo,
+  DatePickerCustomFilter,
+  FilterItem,
+  InputCustomFilter,
+  SelectCustomFilter
+} from '@/types/customFilter.ts'
+import type { QueryInfo } from '@/types/table.ts'
+
+import { nextTick, reactive, ref } from 'vue'
+import { CustomFilterValueType } from '@/types/customFilter.ts'
+
+import CustomFilter from '@/components/form/CustomFilter.vue'
+
+type CustomFilterRef = InstanceType<typeof CustomFilter>
+
+export function useCustomFilter(customFilterRef: Ref<CustomFilterRef | undefined>) {
+  // 自定义筛选对话框开关
+  const customFilterDialog = ref<boolean>(false)
+
+  // 筛选的信息,主要保存类型等
+  const customFilterInfo = reactive<Record<string, CustomFilterInfo>>({})
+
+  // 筛选信息的数据,存真正筛选的值
+  const customFilterList = reactive<Record<string, Array<FilterItem>>>({})
+
+  // 当前激活的筛选框是哪个
+  const activeCustomFilterKey = ref('')
+
+  /**
+   * 生成筛选信息
+   *
+   * 根据customFilterValueType 生成不同的模板
+   *
+   * @param item 筛选项
+   */
+  const createCustomFilterItem = (item: QueryInfo) => {
+    if (item.otherOption.customFilterValueType === CustomFilterValueType.DATEPICKER) {
+      return {
+        valueType: CustomFilterValueType.DATEPICKER,
+
+        format: item.otherOption.format,
+        valueFormat: item.otherOption.valueFormat
+      } as DatePickerCustomFilter
+    }
+    if (item.otherOption.customFilterValueType === CustomFilterValueType.INPUT) {
+      return {
+        valueType: CustomFilterValueType.INPUT,
+        placeholder: item.otherOption.placeholder,
+        valueValid: item.otherOption.valueValid,
+        valueFormat: item.otherOption.valueFormat
+      } as InputCustomFilter
+    }
+    if (item.otherOption.customFilterValueType === CustomFilterValueType.SELECT) {
+      return {
+        valueType: CustomFilterValueType.SELECT,
+        options: item.otherOption.options
+      } as SelectCustomFilter
+    }
+    return false
+  }
+
+  /**
+   * 初始化自定义筛选器的信息
+   *
+   * 为每个筛选器的customFilterList给一个初始值,方便用户打开的时候有至少一个选项
+   *
+   * 初始化每个筛选器的信息,为筛选器赋值类型,让筛选器根据类型不同,渲染不同组件。
+   *
+   */
+  const initCustomFilterInfo = (customFieldsList: ComputedRef<QueryInfo[] | undefined>) => {
+    customFieldsList.value?.forEach((item) => {
+      customFilterList[item.name] = [
+        {
+          filterCondition: 'gt',
+          value: '' // 这个值无所谓,因为后面会根据类型来渲染不同的组件
+        }
+      ]
+
+      let customFilterItem = createCustomFilterItem(item)
+      if (customFilterItem !== false) {
+        customFilterInfo[item.name] = customFilterItem
+      }
+    })
+  }
+
+  /**
+   * 打开自定义筛选为筛选器赋初值
+   *
+   * @param item 当前字段信息
+   */
+  const openCustomFilter = (item: QueryInfo) => {
+    activeCustomFilterKey.value = item.name
+    customFilterDialog.value = true
+    // 这里一定要在下一个tick中执行,因为组件还没渲染出来,所以无法获取到dom元素
+    void nextTick(() => {
+      if (customFilterRef.value) {
+        customFilterRef.value.initFilterList()
+      }
+    })
+  }
+
+  /**
+   * 将自定义过滤器列表转换为查询参数。
+   * @param filterList 过滤器值列表
+   */
+  const conversionCustomFilter = (
+    filterList: Array<{
+      filterCondition: string
+      value: any
+    }>
+  ) => {
+    let result: Record<string, any> = {}
+    filterList.forEach((item) => {
+      result[item.filterCondition] = item.value
+    })
+    return result
+  }
+
+  /**
+   * 提交筛选条件
+   *
+   * 提交前需要验证数据是否符合格式,不符合则不提交。
+   *
+   */
+  const confirmCustomFilter = (queryFormData: Reactive<Record<string, any>>) => {
+    if (customFilterRef.value && customFilterRef.value.verifySub()) {
+      customFilterDialog.value = false
+      let result = customFilterRef.value.subCondition()
+      // 拿到数据后,需要转为查询参数
+      queryFormData[activeCustomFilterKey.value] = conversionCustomFilter(result)
+      // 保存筛选值
+      customFilterList[activeCustomFilterKey.value] = result
+    }
+  }
+
+  return {
+    customFilterDialog,
+    customFilterInfo,
+    customFilterList,
+    activeCustomFilterKey,
+
+    initCustomFilterInfo,
+    openCustomFilter,
+
+    confirmCustomFilter
+  }
+}

+ 4 - 11
src/hooks/useRequest.ts

@@ -2,8 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 17:24:06
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-11-06 11:19:21
- * @FilePath: \Quantity-Creation-Management-Systemc:\Users\NINGMEI\Desktop\Game-Backstage-Management-System\src\hooks\useRequest.ts
+ * @LastEditTime: 2024-11-27
+ * @FilePath: \SqueezeTheBusc:\Users\NINGMEI\Desktop\Manage\Game-Backstage-Management-System\src\hooks\useRequest.ts
  * @Description:
  *
  */
@@ -20,17 +20,8 @@ import 'element-plus/theme-chalk/el-message-box.css'
 
 export function useRequest() {
   let baseURL = BASE_URL
-  // if (import.meta.env.MODE === 'test') {
-  //   baseURL = import.meta.env.VITE_API_URL_TEST
-  // } else {
-  //   baseURL = import.meta.env.VITE_API_URL_PRODUCT
-  // }
 
   const AllApi = {
-    // mock: `http://127.0.0.1:8003/mock`,
-    mockEvent: `http://127.0.0.1:8003/mockEvent`,
-    mockTest: `http://127.0.0.1:8003/test`,
-
     getGameTable: `/user/getGidConfig`, // 获取游戏列表
     getUserTable: `/user/userList`, // 获取用户列表
     addGame: `/user/addGidConfig`, // 添加/修改 游戏配置
@@ -51,6 +42,8 @@ export function useRequest() {
     userDataTrades: `/user/dataTrades`, //用户趋势 -数据趋势
     userDataTradesDetail: `/user/dataTradesDetail`, //用户趋势 -数据趋势详情
     userRemainDataBydDay: `/user/remainDataBydDay`, //用户留存数据
+    userBehaviorList: `/user/behaviorList`, // 用户行为
+    userAdRelatedList: `/user/adRelatedList`, // 广告用户数据
 
     // 事件相关
     // 事件

+ 1 - 1
src/router/appManage.ts

@@ -2,7 +2,7 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 14:24:58
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-11-21
+ * @LastEditTime: 2024-11-25
  * @FilePath: \Game-Backstage-Management-System\src\router\appManage.ts
  * @Description:
  *

+ 24 - 2
src/router/home.ts

@@ -2,8 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-20 14:24:58
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-10-10 15:45:26
- * @FilePath: \Game-Backstage-Management-System\src\router\home.ts
+ * @LastEditTime: 2024-11-25
+ * @FilePath: \SqueezeTheBusc:\Users\NINGMEI\Desktop\Manage\Game-Backstage-Management-System\src\router\home.ts
  * @Description:
  *
  */
@@ -79,6 +79,7 @@ export default [
               needKeepAlive: true
             }
           },
+
           {
             path: 'eventAnalysisView',
             name: 'EventAnalysisView',
@@ -118,6 +119,27 @@ export default [
                 }
               }
             ]
+          },
+          {
+            path: 'userBehavior',
+            name: 'UserBehavior',
+            cnName: '用户行为',
+            component: () => import('@/views/Home/Analysis/UserBehavior.vue'),
+            meta: {
+              activeMenu: 'userBehavior',
+              needKeepAlive: true
+            }
+          },
+
+          {
+            path: 'advertisingUsers',
+            name: 'AdvertisingUsers',
+            cnName: '广告用户',
+            component: () => import('@/views/Home/Analysis/AdvertisingUsers.vue'),
+            meta: {
+              activeMenu: 'advertisingUsers',
+              needKeepAlive: true
+            }
           }
         ]
       }

+ 47 - 0
src/types/customFilter.ts

@@ -0,0 +1,47 @@
+enum CustomFilterValueType {
+  INPUT,
+  DATEPICKER,
+  SELECT
+}
+
+interface BaseCustomFilter {
+  valueType: CustomFilterValueType
+}
+
+interface InputCustomFilter extends BaseCustomFilter {
+  valueType: CustomFilterValueType.INPUT
+  valueFormat?: (...args: any[]) => any
+  valueValid?: (val: any) => boolean
+}
+
+interface DatePickerCustomFilter extends BaseCustomFilter {
+  valueType: CustomFilterValueType.DATEPICKER
+  format: string
+  valueFormat: string
+}
+
+interface SelectCustomFilter extends BaseCustomFilter {
+  valueType: CustomFilterValueType.SELECT
+  defaultDate: Date
+  options: Array<{
+    label: string
+    value: string
+  }>
+}
+
+interface FilterItem {
+  filterCondition: string
+  value: any
+}
+
+type CustomFilter = InputCustomFilter | DatePickerCustomFilter | SelectCustomFilter
+
+export { CustomFilterValueType }
+
+export type {
+  CustomFilter,
+  InputCustomFilter,
+  DatePickerCustomFilter,
+  SelectCustomFilter,
+  FilterItem
+}

+ 2 - 1
src/types/table.ts

@@ -41,7 +41,8 @@ export interface TablePaginationSetting {
 export enum FilterType {
   INPUT = 'input',
   SELECT = 'select',
-  DATE = 'date'
+  DATE = 'date',
+  CUSTOM = 'custom'
 }
 
 // 筛选信息的格式

+ 19 - 2
src/types/tableText.ts

@@ -3,7 +3,8 @@ enum FieldSpecialEffectType {
   TEXT = 'text',
   SWITCH = 'switch',
   DROPDOWN = 'dropdown',
-  IMG = 'img'
+  IMG = 'img',
+  CUSTOM = 'custom'
 }
 
 // 文本类型字段类型
@@ -68,15 +69,31 @@ interface SpecialEffectImg {
   otherInfo: {}
 }
 
+// 自定义类型
+interface SpecialEffectCustom {
+  type: FieldSpecialEffectType.CUSTOM
+  otherInfo: {
+    // 自定义渲染函数
+    render: (value: any) => any
+  }
+}
+
 // 所有特殊效果类型
 type SpecialEffect =
   | SpecialEffectText
   | SpecialEffectSwitch
   | SpecialEffectDropdown
   | SpecialEffectImg
+  | SpecialEffectCustom
 
 export { FieldSpecialEffectType, TextType, TagType }
 
 export type { SpecialEffect }
 
-export type { SpecialEffectText, SpecialEffectSwitch, SpecialEffectDropdown, SpecialEffectImg }
+export type {
+  SpecialEffectText,
+  SpecialEffectSwitch,
+  SpecialEffectDropdown,
+  SpecialEffectImg,
+  SpecialEffectCustom
+}

+ 37 - 2
src/utils/common/index.ts

@@ -2,8 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-26 15:46:42
  * @LastEditors: fxs bjnsfxs@163.com
- * @LastEditTime: 2024-10-15 10:47:42
- * @FilePath: \Game-Backstage-Management-System\src\utils\common\index.ts
+ * @LastEditTime: 2024-11-27
+ * @FilePath: \SqueezeTheBusc:\Users\NINGMEI\Desktop\Manage\Game-Backstage-Management-System\src\utils\common\index.ts
  * @Description:
  *
  */
@@ -82,6 +82,9 @@ export function formatDate(dateString: string) {
   // 将月份和日期转换为整数以去除前导零
   const formattedMonth = parseInt(month, 10)
   const formattedDay = parseInt(day, 10)
+  if (year === '' || isNaN(formattedMonth) || isNaN(formattedDay)) {
+    return '无'
+  }
 
   // 生成新的日期字符串
   return `${year}-${formattedMonth}-${formattedDay}`
@@ -129,6 +132,38 @@ export const invaildDateRange = (startTime: string, endTime: string): boolean =>
 }
 
 /**
+ * 获取当前时间戳,单位为毫秒或秒
+ * @param dateString 时间字符串
+ * @param inMilliseconds 是否返回毫秒,true 表示返回毫秒级别时间戳,false(默认)返回秒级时间戳。
+ */
+export const toUnixTimestamp = (dateString: string, inMilliseconds: boolean = false): number => {
+  const date = new Date(dateString)
+
+  if (isNaN(date.getTime())) {
+    throw new Error('Invalid date string format')
+  }
+
+  return inMilliseconds ? date.getTime() : Math.floor(date.getTime() / 1000)
+}
+
+export const formatTimestamp = (timestamp: number, isMilliseconds: boolean = false): string => {
+  // 将秒级时间戳转换为毫秒级时间戳
+  const adjustedTimestamp = isMilliseconds ? timestamp : timestamp * 1000
+
+  const date = new Date(adjustedTimestamp)
+
+  // 格式化为 yyyy-MM-dd HH:mm:ss
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从 0 开始
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  const seconds = String(date.getSeconds()).padStart(2, '0')
+
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
+/**
  * @description: 生成一个打包后可以使用的url
  * @param {string} url  传入一个assets文件夹下的文件名
  * @return {*}

+ 275 - 0
src/views/Home/Analysis/AdvertisingUsers.vue

@@ -0,0 +1,275 @@
+<script setup lang="ts">
+import type { HeaderCardProps, ReqConfig } from '@/types/dataAnalysis'
+import type { QueryInfo, TableFieldInfo, TablePaginationSetting } from '@/types/table'
+import { FilterType } from '@/types/table'
+import { CustomFilterValueType } from '@/types/customFilter'
+
+import { reactive, ref } from 'vue'
+import { useRequest } from '@/hooks/useRequest'
+import { useAnalysis } from '@/hooks/useAnalysis'
+import { useCommonStore } from '@/stores/useCommon'
+import { formatTimestamp } from '@/utils/common'
+import { usePage } from '@/hooks/usePage'
+
+import Table from '@/components/Table.vue'
+import { FieldSpecialEffectType } from '@/types/tableText'
+
+type Table = typeof Table
+
+const { AllApi } = useRequest()
+const { selectInfo } = useCommonStore()
+const { updateReqConfig } = useAnalysis()
+const { watchPageChange } = usePage()
+
+const isSinglePf = ref(true)
+
+const advertisingUsersTable = ref<Table>()
+
+// 表格请求配置
+const requestConfig = reactive<ReqConfig>({
+  // url: 'http://192.168.1.139:8000/user/adRelatedList',
+  url: AllApi.userAdRelatedList,
+  otherOptions: {
+    pf: 'tt',
+    gid: selectInfo.gid
+  }
+})
+
+// 事件表格的上方查询字段信息
+const filterInfo: Array<QueryInfo> = [
+  {
+    name: 'pid',
+    label: '广告父级ID',
+    type: FilterType.INPUT,
+    placeholder: '请输入广告父级ID',
+    valueType: 'int',
+    default: 0
+  },
+  {
+    name: 'aid',
+    label: '广告ID',
+    type: FilterType.INPUT,
+    placeholder: '请输入广告ID',
+    valueType: 'int',
+    default: 0
+  },
+  {
+    name: 'cid',
+    label: '用户点击ID',
+    type: FilterType.INPUT,
+    placeholder: '请输入用户点击ID'
+  },
+  {
+    name: 'createTime',
+    label: '创建时间',
+    type: FilterType.CUSTOM,
+    placeholder: '选择日期',
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.DATEPICKER,
+      valueFormat: 'X'
+    }
+  },
+  {
+    name: 'startNum',
+    label: '启动次数',
+    type: FilterType.CUSTOM,
+    placeholder: '请输入筛选次数',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => !isNaN(val)
+    }
+  },
+  {
+    name: 'revenue',
+    label: '用户收益',
+    type: FilterType.CUSTOM,
+    placeholder: '请输入筛选收益',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseFloat(val)
+      },
+      valueValid: (val: any) => !isNaN(val)
+    }
+  },
+  {
+    name: 'duration',
+    label: '总时长',
+    type: FilterType.CUSTOM,
+    placeholder: '请输入筛选时长',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => !isNaN(val)
+    }
+  },
+  {
+    name: 'reqCount',
+    label: '看广告次数',
+    type: FilterType.CUSTOM,
+    placeholder: '请输入筛选次数',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => !isNaN(val)
+    }
+  },
+  {
+    name: 'expCount',
+    label: '看完广告次数',
+    type: FilterType.CUSTOM,
+    placeholder: '请输入筛选次数',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => !isNaN(val)
+    }
+  }
+]
+
+// 表格分页设置
+const pagingConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pagesizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'pid',
+    cnName: '广告父级ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'aid',
+    cnName: '广告ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'cid',
+    cnName: '用户点击ID',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'createTime',
+    cnName: '创建时间',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.CUSTOM,
+      otherInfo: {
+        render: formatTimestamp
+      }
+    }
+  },
+  {
+    name: 'startNum',
+    cnName: '启动次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'revenue',
+    cnName: '用户收益',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'duration',
+    cnName: '总时长',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'req_count',
+    cnName: '看广告次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'exp_count',
+    cnName: '看完广告次数',
+    isShow: true,
+    needSort: false
+  }
+])
+
+const headerCardInfo: HeaderCardProps = {
+  title: '广告用户',
+  openDateSelect: false
+}
+
+/**
+ * @description: 更新所有监听req的参数
+ * @param {*} pf  新pf
+ * @param {*} gid 新gid
+ */
+const updateAllReq = (pf: string, gid: string) => {
+  pf = isSinglePf ? pf[0] : pf
+  updateReqConfig(requestConfig, { pf, gid })
+}
+
+const backupSelect = reactive([]) // 保存选择数据
+
+watchPageChange(() => [selectInfo.pf, selectInfo.gid], backupSelect, updateAllReq)
+</script>
+
+<template>
+  <div class="advertisingUsers">
+    <div class="header">
+      <HeaderCard :title="headerCardInfo.title"></HeaderCard>
+    </div>
+    <div class="tableBox">
+      <Table
+        ref="advertisingUsersTable"
+        :request-config="requestConfig"
+        :query-info="filterInfo"
+        :pagination-config="pagingConfig"
+        :table-fields-info="tableFieldsInfo"
+        :open-remoteinquiry="true"
+        :open-page-query="true"
+        :open-filter-query="true"
+      ></Table>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.advertisingUsers {
+  width: 98%;
+  margin: 1% auto;
+  background-color: white;
+  border: 1px solid #e5e6eb;
+}
+
+.tableBox {
+  width: 100%;
+  box-sizing: border-box;
+  padding: 0 24px;
+  background-color: white;
+}
+</style>

+ 1 - 0
src/views/Home/Analysis/EventAnalysisTable.vue

@@ -106,6 +106,7 @@ const eventTableFilterInfo: Array<QueryInfo> = [
     placeholder: '输入事件名查询'
   }
 ]
+
 /**
  * @description: 查看详情
  * @param {*} row 行信息

+ 2 - 6
src/views/Home/Analysis/KeepView.vue

@@ -2,12 +2,8 @@
  * @Author: fxs bjnsfxs@163.com
  * @Date: 2024-08-27 17:11:23
  * @LastEditors: fxs bjnsfxs@163.com
-<<<<<<< HEAD
- * @LastEditTime: 2024-09-18 15:01:25
-=======
- * @LastEditTime: 2024-10-15 11:59:31
->>>>>>> develop
- * @FilePath: \Game-Backstage-Management-System\src\views\Home\Analysis\KeepView.vue
+ * @LastEditTime: 2024-11-25
+ * @FilePath: \SqueezeTheBusc:\Users\NINGMEI\Desktop\Manage\Game-Backstage-Management-System\src\views\Home\Analysis\KeepView.vue
  * @Description: 
  * 
 -->

+ 313 - 0
src/views/Home/Analysis/UserBehavior.vue

@@ -0,0 +1,313 @@
+<script setup lang="ts">
+import type { HeaderCardProps, ReqConfig } from '@/types/dataAnalysis'
+import type { QueryInfo, SelectInfo, TableFieldInfo, TablePaginationSetting } from '@/types/table'
+import { FilterType } from '@/types/table'
+import { CustomFilterValueType } from '@/types/customFilter'
+
+import { reactive, ref } from 'vue'
+import { useRequest } from '@/hooks/useRequest'
+import { useAnalysis } from '@/hooks/useAnalysis'
+import { useCommonStore } from '@/stores/useCommon'
+import { formatTimestamp } from '@/utils/common'
+import { formatDate } from '@/utils/common'
+import { usePage } from '@/hooks/usePage'
+
+import Table from '@/components/Table.vue'
+import { FieldSpecialEffectType, TagType, TextType } from '@/types/tableText'
+
+type Table = typeof Table
+
+const { AllApi } = useRequest()
+const { selectInfo } = useCommonStore()
+const { updateReqConfig } = useAnalysis()
+const { watchPageChange } = usePage()
+
+const isSinglePf = ref(true)
+
+const behaviourTable = ref<Table>()
+
+// 表格请求配置
+const requestConfig = reactive<ReqConfig>({
+  // url: 'http://192.168.1.139:8000/user/behaviorList',
+  url: AllApi.userBehaviorList,
+  otherOptions: {
+    pf: 'tt',
+    gid: selectInfo.gid
+  }
+})
+
+// 上报状态
+const uploadStateSelect: Array<SelectInfo> = [
+  {
+    name: 'all',
+    cnName: '全部',
+    value: ''
+  },
+  {
+    name: 'uploaded',
+    cnName: '已上报',
+    value: 'true'
+  },
+  {
+    name: 'unUploaded',
+    cnName: '未上报',
+    value: 'false'
+  }
+]
+
+// 转化状态
+const conversionStateSelect: Array<SelectInfo> = [
+  {
+    name: 'all',
+    cnName: '全部',
+    value: ''
+  },
+  {
+    name: 'converted',
+    cnName: '已转化',
+    value: 'true'
+  },
+  {
+    name: 'unConverted',
+    cnName: '未转化',
+    value: 'false'
+  }
+]
+
+// 事件表格的上方查询字段信息
+const filterInfo: Array<QueryInfo> = [
+  {
+    name: 'openId',
+    label: 'OpenId',
+    type: FilterType.INPUT,
+    placeholder: '输入OpenId'
+  },
+  {
+    name: 'activeStatus',
+    label: '上报状态',
+    type: FilterType.SELECT,
+    placeholder: '是否已经上报',
+    otherOption: uploadStateSelect,
+    default: ''
+  },
+  {
+    name: 'conversionStatus',
+    label: '转化状态',
+    type: FilterType.SELECT,
+    placeholder: '是否已经转化',
+    otherOption: conversionStateSelect,
+    default: ''
+  },
+  {
+    name: 'createTime',
+    label: '创建时间',
+    type: FilterType.CUSTOM,
+    placeholder: '选择日期',
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.DATEPICKER,
+      valueFormat: 'X'
+    }
+  },
+  {
+    name: 'totalDuration',
+    label: '总在线时长',
+    type: FilterType.CUSTOM,
+    placeholder: '请输入筛选时长',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => {
+        return !isNaN(val)
+      }
+    }
+  },
+  {
+    name: 'totalAdReqCount',
+    label: '总广告观看次数',
+    type: FilterType.CUSTOM,
+    placeholder: '总广告观看次数',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => {
+        return !isNaN(val)
+      }
+    }
+  },
+  {
+    name: 'totalAdEposedCount',
+    label: '广告看完次数',
+    type: FilterType.CUSTOM,
+    placeholder: '广告看完次数',
+    default: 0,
+    otherOption: {
+      customFilterValueType: CustomFilterValueType.INPUT,
+      valueFormat: (val: any) => {
+        if (val === '') return val
+        return parseInt(val)
+      },
+      valueValid: (val: any) => {
+        return !isNaN(val)
+      }
+    }
+  }
+]
+
+// 表格分页设置
+const pagingConfig = reactive<TablePaginationSetting>({
+  limit: 20,
+  currentPage: 1,
+  total: 0,
+  pagesizeList: [20, 30]
+})
+
+// 表格字段信息
+const tableFieldsInfo = reactive<Array<TableFieldInfo>>([
+  {
+    name: 'openId',
+    cnName: 'OpenId',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'totalDuration',
+    cnName: '总时长',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'totalAdReqCount',
+    cnName: '总广告观看次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'totalAdEposedCount',
+    cnName: '广告看完次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'createDate',
+    cnName: '创建日期',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.CUSTOM,
+      otherInfo: {
+        render: formatDate
+      }
+    }
+  },
+  {
+    name: 'createTime',
+    cnName: '创建时间',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.CUSTOM,
+      otherInfo: {
+        render: formatTimestamp
+      }
+    }
+  },
+  {
+    name: 'startNum',
+    cnName: '启动次数',
+    isShow: true,
+    needSort: false
+  },
+  {
+    name: 'activeStatus',
+    cnName: '是否激活',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.TEXT,
+      otherInfo: {
+        translateMap: ['是', '否'],
+        tagType: [TagType.SUCCESS, TagType.DANGER],
+        textType: TextType.TAG
+      }
+    }
+  },
+  {
+    name: 'conversionStatus',
+    cnName: '是否转化',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.TEXT,
+      otherInfo: {
+        translateMap: ['是', '否'],
+        tagType: [TagType.SUCCESS, TagType.DANGER],
+        textType: TextType.TAG
+      }
+    }
+  }
+])
+
+const headerCardInfo: HeaderCardProps = {
+  title: '用户行为',
+  openDateSelect: false
+}
+
+/**
+ * @description: 更新所有监听req的参数
+ * @param {*} pf  新pf
+ * @param {*} gid 新gid
+ * @return {*}
+ */
+const updateAllReq = (pf: string, gid: string) => {
+  pf = isSinglePf ? pf[0] : pf
+  updateReqConfig(requestConfig, { pf, gid })
+}
+
+const backupSelect = reactive([]) // 保存选择数据
+
+watchPageChange(() => [selectInfo.pf, selectInfo.gid], backupSelect, updateAllReq)
+</script>
+
+<template>
+  <div class="userBehavior">
+    <div class="header">
+      <HeaderCard :title="headerCardInfo.title"></HeaderCard>
+    </div>
+    <div class="tableBox">
+      <Table
+        ref="behaviourTable"
+        :request-config="requestConfig"
+        :query-info="filterInfo"
+        :pagination-config="pagingConfig"
+        :table-fields-info="tableFieldsInfo"
+        :open-remoteinquiry="true"
+        :open-page-query="true"
+        :open-filter-query="true"
+      ></Table>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.userBehavior {
+  width: 98%;
+  margin: 1% auto;
+  background-color: white;
+  border: 1px solid #e5e6eb;
+}
+
+.tableBox {
+  width: 100%;
+  box-sizing: border-box;
+  padding: 0 24px;
+  background-color: white;
+}
+</style>

+ 44 - 1
src/views/Home/InfoManage/GameManageView.vue

@@ -12,6 +12,8 @@ import { useCommonStore } from '@/stores/useCommon'
 
 import Dialog from '@/components/common/Dialog.vue'
 import Table from '@/components/Table.vue'
+import { FieldSpecialEffectType } from '@/types/tableText.ts'
+import { formatDate } from '@/utils/common'
 
 const { allGameInfo, selectInfo } = useCommonStore()
 const { AllApi } = useRequest()
@@ -22,6 +24,7 @@ const gameDialogRef = ref()
 // 配置请求参数
 const requestConfig = reactive({
   url: AllApi.getGameTable,
+  // url: 'http://192.168.1.139:8000/user/getGidConfig',
   otherOptions: {
     appSecret: '6YJSuc50uJ18zj45'
   }
@@ -72,6 +75,36 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
     cnName: '微信App Secret',
     isShow: true,
     needSort: false
+  },
+  {
+    name: 'ttTplId',
+    cnName: '抖音订阅ID',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.CUSTOM,
+      otherInfo: {
+        render: (val: any) => {
+          if (!val) return '无'
+          return val
+        }
+      }
+    }
+  },
+  {
+    name: 'wxTplId',
+    cnName: '微信订阅ID',
+    isShow: true,
+    needSort: false,
+    specialEffect: {
+      type: FieldSpecialEffectType.CUSTOM,
+      otherInfo: {
+        render: (val: any) => {
+          if (!val) return '无'
+          return val
+        }
+      }
+    }
   }
 ])
 
@@ -208,6 +241,16 @@ const dialogFormFields: Array<FormField> = [
     name: 'wxSecret',
     cnName: '微信App Secret',
     type: FormFieldType.INPUT
+  },
+  {
+    name: 'ttTplId',
+    cnName: '抖音订阅ID',
+    type: FormFieldType.INPUT
+  },
+  {
+    name: 'wxTplId',
+    cnName: '微信订阅ID',
+    type: FormFieldType.INPUT
   }
 ]
 
@@ -220,6 +263,7 @@ const gameDialogConfig = reactive<DialogConfig>({
   rules: gameRules,
   reqConfig: {
     url: AllApi.addGame,
+    // url: 'http://192.168.1.139:8000/user/addGidConfig',
     otherOptions: {
       formData: {
         appSecret: '6YJSuc50uJ18zj45'
@@ -238,7 +282,6 @@ const handleEdit = (row: any) => {
 }
 
 const formSub = (formData: any, type: number) => {
-  console.log(type)
   if (type === 0) {
     allGameInfo.push({
       gid: formData.gid,

+ 0 - 1
src/views/Home/InfoManage/PlayerManageView.vue

@@ -112,7 +112,6 @@ const filedsInfo = reactive<Array<TableFieldInfo>>([
       type: FieldSpecialEffectType.TEXT,
       otherInfo: {
         translateMap: ['是', '否'],
-
         tagType: [TagType.DANGER, TagType.SUCCESS],
         textType: TextType.TAG
       }